diff --git a/.github/workflows/python-tests.yml b/.github/workflows/python-tests.yml
index 67351c2b387..3273d6ba812 100644
--- a/.github/workflows/python-tests.yml
+++ b/.github/workflows/python-tests.yml
@@ -58,7 +58,7 @@ jobs:
github-env: $env:GITHUB_ENV
name: 'py${{ matrix.python-version }}: ${{ matrix.platform }}'
runs-on: ${{ matrix.os }}
- timeout-minutes: 15 # expected run time: 2-6 min, depending on platform
+ timeout-minutes: 30 # expected run time: 10-16 min, depending on platform
env:
PIP_USE_PEP517: '1'
diff --git a/docs/src/content/docs/features/Multi-User Mode/admin-guide.mdx b/docs/src/content/docs/features/Multi-User Mode/admin-guide.mdx
index 38c654f7d7c..d5fac328a73 100644
--- a/docs/src/content/docs/features/Multi-User Mode/admin-guide.mdx
+++ b/docs/src/content/docs/features/Multi-User Mode/admin-guide.mdx
@@ -219,10 +219,13 @@ If no arguments are provided, the script will run in interactive mode.
:::danger[Data Loss]
Deleting a user removes the user record and cascades to their sessions, board shares, sent
-invitations, and per-user client state. It does **not** delete the boards, images, workflows, queue
-items, or style presets they created — those rows remain in the database, owned by a user_id that no
-longer exists, and will not appear in any user's gallery. Physical image files in `outputs/images`
-are also left in place until a gallery maintenance script is run to remove orphan images.
+invitations, and per-user client state. It does **not** delete the boards, images, videos,
+workflows, queue items, or style presets they created — those rows remain in the database, owned by
+a user_id that no longer exists, and will not appear in other (non-admin) users' galleries.
+Administrators keep gallery visibility of the orphaned images and videos and can review or delete
+them from there. Physical media files in `outputs/images` and `outputs/videos` are also left in
+place: orphan image files can be removed with a gallery maintenance script, while video files should
+be deleted through the gallery (which removes the file along with the record) or cleaned up manually.
If you want their content gone as well, reassign or delete it before deleting the user. Back up the
database first if recovery might be needed.
diff --git a/docs/src/content/docs/features/gallery.mdx b/docs/src/content/docs/features/gallery.mdx
index 48dfedcc5f2..2b34fa77b7d 100644
--- a/docs/src/content/docs/features/gallery.mdx
+++ b/docs/src/content/docs/features/gallery.mdx
@@ -1,14 +1,14 @@
---
title: Gallery Panel
-description: Learn how to manage, organize, and use your generated images and assets with the Gallery Panel in InvokeAI.
-lastUpdated: 2026-02-19
+description: Learn how to manage, organize, and use your generated images, videos, and assets with the Gallery Panel in InvokeAI.
+lastUpdated: 2026-07-19
sidebar:
order: 1
---
import { Card, CardGrid, Steps } from '@astrojs/starlight/components';
-The Gallery Panel is a fast way to review, find, and make use of images you've generated and loaded. The Gallery is divided into **Boards**. The *Uncategorized* board is always present, but you can create your own for better organization.
+The Gallery Panel is a fast way to review, find, and make use of images and videos you've generated and loaded. The Gallery is divided into **Boards**. The *Uncategorized* board is always present, but you can create your own for better organization. Boards are polymorphic — images and videos coexist on the same board and appear together in the gallery, sorted by creation time.

@@ -50,11 +50,17 @@ Each board has a context menu accessible via right-click (or Ctrl+click).

- **Auto-add to this Board:** If *Auto-Assign Board on Click* is disabled in settings, use this option to quickly set the selected board as the default destination for new images.
-- **Download Board:** Packages all images within the board into a `.zip` file. A notification link will be provided when the download is ready.
-- **Delete Board:** Permanently removes the board and all of its contents.
+- **Download Board Images:** Packages all images within the board into a `.zip` file. A notification link will be provided when the download is ready. Videos are not included; download them individually from the gallery or viewer.
+- **Delete Board:** Opens a confirmation dialog. **Delete Board Only** removes the board and leaves its contents uncategorized; **Delete Board and Assets** permanently removes the board, its images, and its videos.
:::danger
-Deleting a board will **permanently delete all images** contained within it. Proceed with caution!
+Only **Delete Board and Assets** permanently deletes the board's images and videos. This action cannot be undone.
+:::
+
+:::note
+In multi-user deployments, **Delete Board and Assets** only permanently deletes *your own* media on a shared board — other users' contributions are preserved and move to Uncategorized. (Administrators delete everything on the board.)
+
+If any item's files cannot be removed (for example, due to a transient storage error), that item is kept rather than left orphaned on disk: its record moves to Uncategorized, and the app shows a warning with the number of items that could not be deleted.
:::
### Image Storage Maintenance
@@ -67,22 +73,22 @@ Every board is organized into two distinct tabs:

-1. **Images:** Images generated directly within InvokeAI.
+1. **Images/Videos:** Images and videos generated directly within InvokeAI, shown interleaved by creation time.
2. **Assets:** External images you have uploaded to use as an [Image Prompt](https://support.invoke.ai/support/solutions/articles/151000159340-using-the-image-prompt-adapter-ip-adapter-) or within the Image to Image tab.
---
## Virtual Boards
-Virtual boards are read-only board groupings that Invoke computes on-the-fly from your image metadata rather than storing in the database. The first available type groups images **By Date**, creating one sub-board per day on which you generated images.
+Virtual boards are read-only board groupings that Invoke computes on-the-fly from your gallery records rather than storing in the database. The first available type groups media **By Date**, creating one sub-board per day on which you generated images or videos.
Virtual boards are **off by default**. To enable them:
1. Open the **board settings** (gear icon at the top of the Gallery).
2. Toggle **Virtual Boards** on.
-3. A collapsible **By Date** section appears in the board list, with a sub-board for each day that has images. Each sub-board shows the date, image / asset counts, and a cover thumbnail.
+3. A collapsible **By Date** section appears in the board list, with a sub-board for each day that has images or videos. Each sub-board shows the date, image / asset / video counts, and a cover thumbnail (the newest item of that day, image or video).
-Selecting a date sub-board filters the gallery to just the images from that day. The collapse state of the By Date section persists across reloads.
+Selecting a date sub-board filters the gallery to just the images and videos from that day. The collapse state of the By Date section persists across reloads.
### Limits
@@ -90,7 +96,7 @@ Because virtual boards are derived, not stored:
- They are **read-only**: no drag-and-drop, no context menu, no auto-add destination.
- You cannot rename or delete them.
-- Generating a new image updates the counts immediately, but the image is still saved to your regular auto-add board — virtual boards are a *view*, not a destination.
+- Generating a new image or video updates the counts immediately, but the item is still saved to your regular auto-add board — virtual boards are a *view*, not a destination.
- Disabling the **Virtual Boards** toggle hides the section and resets the selection to *Uncategorized* if you were viewing a virtual sub-board.
---
@@ -134,6 +140,29 @@ Additionally, each image has a context menu (right-click or Ctrl+click) with pow
---
+## Videos in the Gallery
+
+Videos generated by InvokeAI (currently from the Wan 2.2 model family) appear alongside images in the same gallery view. Each video item displays a first-frame still as its thumbnail with a play badge in the corner; selecting it opens the video in the viewer where you can play it back inline.
+
+### Uploading Videos
+
+You can upload existing videos to a board via the standard drop-or-upload affordance. The upload pipeline accepts **MP4 files only**. Other containers (`.mov`, `.webm`, `.mkv`) are not transcoded on upload and are rejected at the API boundary — re-encode them to MP4 (for example with `ffmpeg -i input.mov -c:v libx264 output.mp4`) before uploading.
+
+The **Concatenate Videos** and **Extract Video Range** nodes encode video frames only; audio tracks are not preserved. Concatenation requires matching source frame rates unless an explicit output FPS is selected, which retimes the source frames.
+
+### Video Context Menu
+
+Each video has a context menu with the same organization actions as images, plus video-appropriate variants:
+
+- **Open in New Tab / Download:** Opens or saves the raw MP4 file.
+- **Star Video:** Pins the video to the top of the gallery.
+- **Change Board:** Moves the video to a different board. *(Drag-and-drop onto board thumbnails also works.)*
+- **Delete Video:** Permanently deletes the video and its thumbnail.
+
+Videos count toward board contents: a board with two images and three videos shows five items in the polymorphic gallery list and reports both totals in its stats.
+
+---
+
## Summary
This walkthrough covers the Gallery interface and Boards. For guidance on prompting and generation workflows, please refer to the [Prompting Guide](/concepts/prompting-guide/) and [AI Image Generation](/concepts/image-generation/).
diff --git a/docs/src/content/docs/features/video-generation.mdx b/docs/src/content/docs/features/video-generation.mdx
new file mode 100644
index 00000000000..c8f205e2d44
--- /dev/null
+++ b/docs/src/content/docs/features/video-generation.mdx
@@ -0,0 +1,267 @@
+---
+title: Video Generation
+description: Generate short videos with the Wan 2.2 model family — text-to-video, image-to-video, and the trick for stitching longer sequences.
+lastUpdated: 2026-07-19
+---
+
+import { Card, CardGrid, Steps } from '@astrojs/starlight/components';
+
+InvokeAI ships **experimental support for the Wan 2.2 model family**, which lets you generate short MP4 clips from a text prompt, an image, or both. Output ranges from a few-second loop (the model's training distribution) up to longer sequences assembled with the [concat trick](#making-longer-videos) below.
+
+:::caution[Experimental]
+Video generation is a prototype feature. Workflows, node fields, and starter-model packaging may change between releases. The underlying models are also new — expect rough edges in coherence and artifacts at longer durations.
+:::
+
+:::tip[Just want to make a video?]
+Start with the [**Video Workflows** guide](/features/video-workflows/) — a plain-language tour of the ten ready-made workflows that ship with InvokeAI and how to run them. This page is the deeper technical reference behind them.
+:::
+
+---
+
+## Models
+
+Wan 2.2 ships three transformer variants, plus a shared text encoder and two VAEs. All share the same diffusion-style sampling but differ in size, conditioning, and intended task.
+
+### The variants
+
+| Variant | Task | Params | VAE | Conditioning |
+|---|---|---|---|---|
+| **T2V-A14B** | Text → Video | 14B × 2 experts | A14B VAE (16-ch, 8× spatial) | Text only |
+| **I2V-A14B** | Image+Text → Video | 14B × 2 experts | A14B VAE (16-ch, 8× spatial) | Text + reference image (36-channel concat) |
+| **TI2V-5B** | Text → Video OR Image+Text → Video | 5B (single) | Wan 2.2-VAE (48-ch, 16× spatial) | Text, optionally with reference image (first-frame mask blend) |
+
+* **T2V-A14B** generates videos from a text prompt alone. Best motion coherence and prompt-following of the three.
+* **I2V-A14B** locks the first frame to a reference image you supply. Best subject-preservation; the image is concatenated to the noise latents at every step so the model "sees" the reference throughout denoising.
+* **TI2V-5B** is the small single-expert variant. It can do **both** text-to-video and image-to-video with the same checkpoint, at substantially lower VRAM, but with somewhat less stable long-range coherence than the A14B variants.
+
+### High-noise and low-noise transformers (A14B variants only)
+
+The A14B models are a **mixture-of-experts (MoE)** pair. There are actually *two* 14B transformers on disk per variant — a "high-noise" expert and a "low-noise" expert — and the denoise loop swaps between them at a model-defined boundary timestep:
+
+* **High-noise expert** runs early in denoising, when the latents are still mostly noise. It's responsible for composition, layout, and broad motion.
+* **Low-noise expert** runs later, when the latents are close to clean. It refines detail and texture.
+
+InvokeAI handles the swap automatically — both experts have to be installed (the starter bundle handles this), but the workflow only references the "high-noise" model as the main and the "low-noise" model is wired alongside it via the loader node. You don't manage the boundary yourself.
+
+**TI2V-5B is single-expert** — no swap, no boundary, just one model that runs every step. Workflows for TI2V-5B are correspondingly simpler.
+
+### Lightning LoRAs (4-step inference for A14B)
+
+The default A14B variants need ~40–50 denoise steps for clean output. The Wan team also released **Lightning distillation LoRAs** that collapse that to 4 steps with minimal quality loss — about a 10× speedup. There's a pair per variant (one LoRA for the high-noise expert, one for the low-noise), wired through the LoRA loader nodes in the starter workflows.
+
+:::tip
+**TI2V-5B doesn't have a Lightning LoRA.** Its smaller size means each step is cheap; you typically run it at 40–50 steps and end up in a similar wall-clock ballpark as A14B + Lightning. (The bundled TI2V-5B workflows ship with 20 steps as a speed/quality compromise — raise the step count if you want cleaner output.)
+:::
+
+### Installing models
+
+The model manager ships two **starter bundles** for video work:
+
+* **Wan 2.2 Text-to-Video** (~36 GB) — UMT5-XXL text encoder, both VAEs, TI2V-5B Q4_K_M, T2V-A14B Q4_K_M (high + low), T2V Lightning (high + low).
+* **Wan 2.2 Image-to-Video** (~32 GB) — UMT5-XXL, A14B VAE, I2V-A14B Q4_K_M (high + low), I2V Lightning (high + low).
+
+The bundles are independent. Installing both ends up at ~56 GB total (shared components — UMT5-XXL and the A14B VAE — are deduplicated on the second install). A 12 GB VRAM card can install only the Text-to-Video bundle and have **TI2V-5B available for both T2V and image-to-video** without ever touching the I2V bundle.
+
+Higher-quality Q8_0 quantizations of every transformer, plus full Diffusers builds of all three variants, are available as a-la-carte installs in the starter models list.
+
+:::tip
+**On a 12 GB VRAM card**: install just the Text-to-Video bundle and use TI2V-5B. The A14B variants will technically run via aggressive offloading but are slow and prone to OOM. TI2V-5B Q4_K_M fits comfortably and is what we recommend for that tier.
+:::
+
+---
+
+## Workflow setup
+
+The shipped starter workflows ("Text to Video - Wan 2.2 Lightning", "Image to Video - Wan 2.2 Lightning") are the easiest starting point — load them from the workflow library, pick your models, set a prompt, and Invoke. The [Video Workflows guide](/features/video-workflows/) walks through all ten and how to choose between them; the sections below describe what's happening inside so you can build your own.
+
+### Constraints that apply to every video workflow
+
+**Frame count**: `num_frames - 1` must be divisible by **4**. This is dictated by the Wan VAE's temporal compression (4 pixel-frames → 1 latent-frame). Valid values: 5, 9, 13, … **81** (the training default, 5 seconds at 16 fps), 85, 89, etc.
+
+**Pixel dimensions**: must be a multiple of **16** for T2V-A14B and I2V-A14B, and a multiple of **32** for TI2V-5B. The constraint comes from the VAE's spatial downsample × the transformer's 2×2 patch size:
+
+| Variant | VAE spatial | Pixel multiple of |
+|---|---|---|
+| T2V-A14B, I2V-A14B | 8× | **16** |
+| TI2V-5B | 16× | **32** |
+
+Reference values that work: 832×480 (480p), 1280×720 (720p, A14B only — TI2V-5B needs 1280×704 instead since 720 isn't divisible by 32).
+
+**Encoder and denoise dimensions must match**: the `Reference Image - Wan 2.2` encoder and the `Denoise Video - Wan 2.2` node both have their own `width` and `height` fields. They have to be identical or the denoise loop will reject the condition tensor.
+
+:::tip[Use Wan 2.2 I2V Ideal Dimensions]
+The **Wan 2.2 I2V Ideal Dimensions** node takes a source image's W×H and a target preset (480p / 720p / 1080p) and outputs valid (width, height) for the encoder + denoise inputs. Wire it once and feed its outputs into both nodes. Saves the manual snap-to-16/snap-to-32 math.
+:::
+
+### Text-to-video workflow
+
+The minimum node chain for T2V:
+
+```
+Wan Main Model Loader ──┐
+ │
+Wan T5 Text Encoder ────┤
+ ▼
+Wan Compel Conditioning (positive)
+ │
+ ▼
+ Denoise Video - Wan 2.2 ──→ Latents to Video - Wan 2.2 ──→ MP4
+ ▲
+Wan Compel Conditioning (negative) ─┘
+```
+
+For **TI2V-5B T2V** this is the entire graph — load the TI2V-5B model and the TI2V-5B VAE, set width/height/num_frames, and run.
+
+For **T2V-A14B** the main model loader also exposes the low-noise expert slot, and you typically add the **Lightning LoRA pair** (one for each expert) to bring step count down to 4. Recommended:
+
+* Steps: **4** (with Lightning) or **40–50** (without)
+* CFG: **5.0** high-noise / **4.0** low-noise (the dedicated `Guidance Scale (Low Noise)` field on the denoise node)
+* Width × Height: 832×480 (faster, default) or 1280×720 (sharper, 4× the memory)
+
+### Image-to-video workflow
+
+I2V adds a **Reference Image** branch alongside the denoise. The reference image gets VAE-encoded into a conditioning tensor that the denoise loop uses to anchor the video's content:
+
+```
+Wan Main Model Loader ──┐
+Wan T5 Text Encoder ────┤
+Wan Compel Conditioning ┤
+ │
+Image Primitive ──→ Reference Image - Wan 2.2 ──┐
+ │ │
+ ▼ ▼
+ Denoise Video - Wan 2.2 ──→ Latents to Video - Wan 2.2 ──→ MP4
+```
+
+For **I2V-A14B**, both the reference encoder and the denoise node need to use the same width/height. The encoder also takes a `num_frames` parameter that must match the denoise's `num_frames` — set both to 81 by default.
+
+For **TI2V-5B image-to-video**, the conditioning math is different (the model uses a first-frame-mask blend rather than channel concatenation), but the workflow shape is the same. The encoder auto-detects TI2V-5B from the VAE's 48 latent channels and emits the right condition tensor.
+
+:::caution[TI2V-5B I2V dimensional constraint]
+TI2V-5B image-to-video requires **width and height divisible by 32** (not just 16). The encoder will refuse the workflow with a clear error if not. 832×480 works; 1280×720 does not (720 is not divisible by 32). Use 1280×704 for 720p-ish on TI2V-5B.
+:::
+
+### First-last-frame interpolation (FLF2V)
+
+The **Reference Image - Wan 2.2** node has an optional **End Image** input. Provide a start image on the regular `image` field and an end image on `End Image (FLF2V)`, and the model interpolates a clip that **begins on the first image and animates to the second** — handy for morphing between two stills or bridging two shots.
+
+```
+Image Primitive (start) ──→ image ────┐
+ ├─→ Reference Image - Wan 2.2 ──→ Denoise Video ──→ MP4
+Image Primitive (end) ──→ End Image ───┘
+```
+
+This is **I2V-A14B only** (`num_frames > 1`); it is not supported on TI2V-5B or single-frame I2V (the node raises a clear error). Wan 2.2 has no dedicated first-last-frame checkpoint — that was a Wan 2.1 model — so the stock I2V-A14B model accepts the end-frame anchor through its existing conditioning channels. Quality is good in practice but not guaranteed for every image pair, so eyeball your results. The shipped **"Interpolate 2 Images to Video - Wan 2.2 Lightning"** workflow wires this up end to end.
+
+### Recommended starting parameters
+
+| | T2V-A14B + Lightning | T2V-A14B | I2V-A14B + Lightning | TI2V-5B (T2V or I2V) |
+|---|---|---|---|---|
+| Steps | 4 | 40–50 | 4 | 40–50 (bundled workflows use 20 for speed) |
+| CFG (high) | 1.0 | 5.0 | 1.0 | 5.0–5.5 |
+| CFG (low) | 1.0 | 4.0 | 1.0 | n/a (single expert) |
+| Num frames | 81 | 81 | 81 | 81 |
+| Width × Height | 832×480 | 832×480 | 832×480 | 832×480 |
+| Scheduler | Auto (UniPC) | Auto (UniPC) | Auto (UniPC) | Auto (UniPC) |
+
+---
+
+## Making longer videos
+
+The Wan 2.2 models were trained on **81-frame** clips (5 seconds at 16 fps). Outputs much longer than that suffer rapidly degrading coherence — the temporal positional encoding goes out of distribution and the model loses track of scene content. So instead of asking for `num_frames=200`, the recommended pattern is **chaining**: render a sequence of 81-frame clips where each one's first frame matches the previous clip's last frame, then concatenate them with the `Concatenate Videos` node.
+
+### The basic chain
+
+
+
+1. **Render the first clip** with I2V or T2V, ending on whatever subject/scene you want to continue.
+
+2. **Extract the last frame** of clip 1 using the `Frame from Video` node. Use `frame_index = -1` for the literal last frame, or `-3` / `-5` to step back a few frames (last frames sometimes have boundary artifacts — see the [troubleshooting](#late-frame-artifacts-text-color-blobs) note).
+
+3. **Feed that frame as the reference image** for an I2V run that becomes clip 2. Adjust the prompt for whatever motion you want next.
+
+4. **Repeat** as many times as you want clips.
+
+5. **Concatenate** all the clips into a single MP4 with `Concatenate Videos`. Pick a transition mode based on whether you want a seamless join (`cut` if the bridge frame matches perfectly), a smooth blend (`crossfade`), or a punctuated scene change (`fade_through_black`).
+
+
+
+### Transition modes
+
+The `Concatenate Videos` node offers three:
+
+* **`cut`** — hard splice. Fastest. Total length = sum of inputs. Use this when the bridge frame is genuinely shared (clip 2's first frame = clip 1's last frame) — the seam is invisible.
+* **`crossfade`** — linear A→B dissolve over `transition_frames`. Consumes `transition_frames` from both sides of each boundary. Total length = `sum(inputs) - transition_frames × (n-1)`. Use this when bridge frames don't quite match.
+* **`fade_through_black`** — A fades to black, then B fades in from black. Total length is preserved. Use this for explicit scene changes.
+
+### Quality degradation across iterations
+
+A real failure mode of long chains: each iteration's reference image is itself a *generation output*, so artifacts compound. The model treats codec artifacts and VAE softness in the bridge frame as "style" and reproduces them in the next clip. By the 4th or 5th iteration you can see noticeable softening or color drift.
+
+**Mitigations**:
+
+1. **Pick a bridge frame a few back from the end** (e.g., `frame_index = -3` or `-5`). The very last frame is often the worst frame of a clip due to boundary effects in the temporal attention.
+2. **Refresh the bridge frame with a low-strength img2img pass** before feeding it into the next I2V. An SDXL or FLUX img2img at strength ~0.2 with a quality-focused negative prompt (`blurry, low quality, compression artifacts`) noticeably suppresses the cumulative drift.
+3. **Don't chain more than 4–5 clips** unless you're explicitly doing img2img refinement between each.
+
+---
+
+## Troubleshooting
+
+### OOM errors
+
+Video denoise is memory-intensive — attention scales roughly as `(T_lat × H/16 × W/16)²`, so resolution and frame count both quadratically affect peak VRAM.
+
+* **Drop resolution before frame count.** Going from 1280×720 to 832×480 is a ~2.4× memory drop and visually subtle in most content. Going from 81 frames to 65 only saves ~20%.
+* **TI2V-5B before A14B.** TI2V-5B Q4_K_M peaks around ~6–8 GB at 832×480, versus ~12–14 GB for A14B Q4_K_M. If you're at the OOM edge, switch model family.
+* **OOM at the *reference image encoder* step** is usually allocator fragmentation from a previous run rather than absolute memory pressure. Restart the dev server and try again; if it recurs reproducibly, file an issue.
+
+### Late-frame artifacts (text, color blobs)
+
+If your video looks great for most of its duration but the last ~20% develops Asian text, watermarks, or floating colored shapes, **that's the model's training-data prior leaking through** as temporal coherence weakens at long temporal distance. It's particularly common on TI2V-5B (smaller model, less capacity to hold scene).
+
+**Mitigations**:
+
+* Add to the negative prompt: `text, watermark, logo, subtitles, chinese characters, kanji, ticker, banner`
+* Use a more specific prompt — describe the action you want to *happen* through the clip, not just the static scene
+* Bump CFG to 5.5 (TI2V-5B tolerates this)
+* Stay at `num_frames=81`; values above push temporal RoPE out of distribution and artifacts accelerate
+
+### "Dimensions must be multiples of 16/32"
+
+The encoder and denoise nodes enforce these at runtime. Either:
+
+* Use the **Wan 2.2 I2V Ideal Dimensions** node to compute valid (W, H) automatically from a source image, or
+* Manually round to the right multiple (16 for A14B variants, 32 for TI2V-5B)
+
+### Reference image / denoise dimension mismatch
+
+If the denoise refuses with `Reference-image dimensions … must match denoise dimensions`, both nodes have their own width/height fields and they need to agree. Wire the same values (or the same Ideal Dimensions output) into both.
+
+### TI2V-5B VAE state-dict load error
+
+If `Latents to Video - Wan 2.2` fails with `Error(s) in loading state_dict for AutoencoderKLWan: ... size mismatch for ...`, you have the **wrong VAE installed** for the chosen transformer. TI2V-5B needs the **Wan 2.2 TI2V-5B VAE** (48-channel, Wan 2.2-VAE), not the A14B VAE (16-channel). Both are in the model manager — check the VAE field on the loader and the latents-to-video node.
+
+### Sampler drift on the standalone TI2V-5B GGUF
+
+Standalone GGUF installs don't ship a `scheduler/` config directory. InvokeAI now defaults to `UniPCMultistepScheduler` with the correct Wan-flow params when the model is TI2V-5B and there's no on-disk scheduler — but if you have an older install behaving oddly, the safer alternative is the **full Diffusers TI2V-5B** install (which includes the scheduler config).
+
+### Preview images don't appear
+
+Two known causes:
+
+1. **A video is already loaded in the viewer.** If the last-selected gallery item is a video, the viewer renders the video element. The progress preview overlays on top of it. If you don't see it at all, **hard-refresh the browser** (`Ctrl+Shift+R` / `Cmd+Shift+R`) — Vite's bundle cache occasionally serves a stale build.
+2. **`Show progress in viewer` is disabled.** Check the gallery settings (gear icon at the top of the gallery panel).
+
+### Pipeline runs but the final MP4 is glitchy
+
+This is almost always a **VAE mismatch** or a **scheduler mismatch** — both surface as garbage at the very end of the pipeline. Check that:
+
+* The VAE matches the transformer family (16-ch for A14B, 48-ch for TI2V-5B)
+* You're using the default (auto-selected) scheduler — manually overriding it is currently not supported
+
+---
+
+## Acknowledgements
+
+Wan 2.2 model family by the Alibaba Wan-AI team. Lightning distillation LoRAs by [lightx2v](https://huggingface.co/lightx2v/Wan2.2-Lightning). GGUF quantizations by [QuantStack](https://huggingface.co/QuantStack).
diff --git a/docs/src/content/docs/features/video-workflows.mdx b/docs/src/content/docs/features/video-workflows.mdx
new file mode 100644
index 00000000000..7bea07ab237
--- /dev/null
+++ b/docs/src/content/docs/features/video-workflows.mdx
@@ -0,0 +1,166 @@
+---
+title: Video Workflows
+description: A plain-language guide to the ready-made Wan 2.2 video workflows that ship with InvokeAI — which one to pick, and how to run it.
+lastUpdated: 2026-05-30
+---
+
+import { Card, CardGrid, Steps, LinkButton } from '@astrojs/starlight/components';
+
+InvokeAI ships **ten ready-made video workflows** so you can start generating video without wiring up nodes yourself. Each one is a complete recipe — open it from the **Workflows** library, pick your models, type a prompt (and/or drop in an image), and press **Invoke**.
+
+This page helps you choose the right workflow and run it. For how the models work under the hood, VRAM details, and troubleshooting, see the [Video Generation guide](/features/video-generation/).
+
+:::caution[Experimental]
+Video generation is a prototype feature. These workflows and the models behind them are new — expect rough edges, and check the [Video Generation guide](/features/video-generation/) if something misbehaves.
+:::
+
+---
+
+## Before you start
+
+**1. Install the video models.** Open the **Model Manager** and install a Wan 2.2 starter bundle (Text-to-Video and/or Image-to-Video). The [Video Generation guide explains the bundles and what fits your GPU](/features/video-generation/#installing-models).
+
+**2. You'll select models when you open a workflow.** The shipped workflows come with their model slots **left empty on purpose** — every InvokeAI install stores models a little differently, so a workflow can't point at yours automatically. When you open one, look at its **Notes** panel (the note icon in the workflow editor): it lists exactly which model to pick in each slot. Drop those in once and you're set.
+
+:::tip
+Whenever a workflow won't run because a model field is empty, open its **Notes** — the shopping list of models to select is right there.
+:::
+
+---
+
+## Which workflow should I use?
+
+The workflows come in three families — **Text to Video**, **Image to Video**, and **Extend Video** — each with a high-quality option, a "concept LoRA" variant, and a lighter low-VRAM option — plus an **Interpolate** workflow that bridges two images and an **Extend Video to Image** variant that steers a continuation toward a target frame.
+
+| I want to… | Use | Notes |
+|---|---|---|
+| Make a clip from a text description | **Text to Video - Wan 2.2 Lightning** | Best quality, fast. Start here. |
+| Animate an existing image | **Image to Video - Wan 2.2 Lightning** | Your image becomes the first frame. |
+| Make a video between two images | **Interpolate 2 Images to Video - Wan 2.2 Lightning** | Provide a start and end image; the model interpolates between them. |
+| Make a video longer than ~5 seconds | **Extend Video - Wan 2.2 Lightning** | Continues an existing video and stitches the pieces together. |
+| Extend a video toward a specific end frame | **Extend Video to Image - Wan 2.2 Lightning** | Continues a video and interpolates the new part to a target image. |
+| Add my own style/subject LoRAs | the **w/ Concept LoRAs** variant of any of the above | Same as the base workflow, plus slots for your LoRAs. |
+| Run on a smaller GPU (≈12–16 GB) | a **TI2V-5B (Low Quality)** workflow | Smaller model, lower memory, lower quality. |
+
+If you're not sure, start with **Text to Video - Wan 2.2 Lightning** to get a feel for it, then move to Image-to-Video and Extend.
+
+---
+
+## Bundled video workflows
+
+### Text to Video
+
+
+
+ Type a prompt, get a ~5-second clip. Uses the high-quality A14B model with the Lightning speed-up (just 4 steps). **The recommended starting point.**
+
+
+ The same workflow, plus extra slots for your own concept LoRAs (a style or character you've trained or downloaded). Every LoRA slot must be filled — if you don't have concept LoRAs to add, use the plain version instead.
+
+
+ Uses the smaller TI2V-5B model. Lower quality and slower per result, but fits comfortably on 12–16 GB GPUs. Good for quick drafts or smaller cards.
+
+
+
+### Image to Video
+
+
+
+ Drop in a starting image and a prompt; the model animates outward from your image as the first frame. Uses the A14B model + Lightning. **The recommended image-to-video starting point.**
+
+
+ Same as above, with slots for your own concept LoRAs.
+
+
+ Image-to-video on the smaller TI2V-5B model. Lower quality, lower VRAM — the low-memory option for animating an image.
+
+
+
+### Interpolate Between Two Images
+
+
+
+ Provide a **start image** and an **end image**; the model generates a clip that begins on the first and animates smoothly to the second (first-last-frame interpolation). Great for morphing between two stills or bridging two shots. Uses I2V-A14B + Lightning (4-step).
+
+
+
+:::caution[The "Concept LoRAs" variants need a LoRA in every slot]
+A **concept LoRA** is an add-on that teaches the model a specific style, character, or subject. The *w/ Concept LoRAs* workflows are the same as their plain counterparts but add extra LoRA slots. **Every LoRA slot is required** — if any slot, including the extra concept ones, is left empty, the workflow won't run. Only use these variants when you actually have concept LoRAs to apply; otherwise use the plain version.
+:::
+
+:::note[About the "TI2V-5B (Low Quality)" variants]
+"Low quality" here means *lower than the A14B Lightning workflows*, in exchange for running on much less video memory. On a 12–16 GB GPU they may be your best (or only) option, and they're handy everywhere for fast drafts.
+:::
+
+### Extend Video
+
+The Wan models are trained on short (~5 second) clips, so the way to make something longer is to **continue** a video and stitch the pieces together. These workflows do that for you.
+
+
+
+ Provide an existing video and use the sliders to choose where to continue from. The workflow generates a new clip starting from that point and joins it onto the original with a smooth transition. Run it repeatedly to keep growing the video.
+
+
+ Same as above, with slots for your own concept LoRAs so the continuation keeps your style or subject.
+
+
+ Continue a video **toward a target image**. Provide a starting video and a destination image; the new segment interpolates from the video's last frame to your image (FLF2V), then joins onto the original with a smooth transition. Combines the Extend and Interpolate ideas — useful for steering where a continuation ends up.
+
+
+
+:::tip[Using "Extend Video to Image"]
+This variant takes **two inputs** — the source video and a destination image:
+
+1. Drop your **video** into the video input and use the **start/end frame sliders** to pick the slice to continue from.
+2. Drop a **destination image** into the image input. The generated continuation interpolates from the video's last selected frame to this image, so the extended clip *ends on your image*.
+3. **Invoke.** The new segment is concatenated onto the source with a cross-fade — tune the join (transition type and frame count) on the **Concatenate Videos** node.
+
+Because the endpoint is pinned to your image, it's ideal for bridging a clip to a known next shot or landing on a specific final frame.
+:::
+
+---
+
+## Running a workflow
+
+
+
+1. **Open the Workflows tab** and load one of the workflows from the library.
+
+2. **Read the Notes** (note icon) and select the models it lists — the main model, VAE, and text encoder, plus the Lightning LoRA pair on the A14B workflows.
+
+3. **Set your inputs:**
+ - *Text to Video* — type a **positive prompt** describing the scene and motion.
+ - *Image to Video* — drop a starting **image** into the image field, and optionally add a prompt.
+ - *Extend Video* — provide a **starting video** and use the sliders to pick where the continuation begins.
+
+4. **(Optional) adjust resolution.** The defaults are chosen to balance speed and memory. If you hit an out-of-memory error, lower the resolution preset — see the [VRAM tips](#which-one-fits-my-gpu) below.
+
+5. **Press Invoke.** The finished MP4 appears in the gallery and plays inline in the viewer.
+
+
+
+:::tip
+Leave the **frame count** and **steps** at their defaults to start. They're already tuned for each workflow (for example, the Lightning workflows use only 4 steps, which is what makes them fast).
+:::
+
+---
+
+## Which one fits my GPU?
+
+A rough guide — exact numbers depend on resolution and your system. See [Video Generation → OOM errors](/features/video-generation/#oom-errors) for the full picture.
+
+| Your GPU | Best choice |
+|---|---|
+| 24 GB and up | Any workflow at 720p. |
+| 16–24 GB | A14B Lightning workflows at 480p; TI2V-5B at up to 720p. |
+| 12–16 GB | The **TI2V-5B (Low Quality)** workflows. |
+
+If you get an **out-of-memory** error, lower the resolution first (it helps far more than reducing the frame count), and switch to a TI2V-5B workflow if you're still over budget.
+
+---
+
+## Going further
+
+These workflows are starting points. Once you're comfortable, the [Video Generation guide](/features/video-generation/) covers what's happening inside them — the model variants, how image conditioning works, the trick for chaining clips into longer videos, recommended parameters, and troubleshooting.
+
+Read the Video Generation guide
diff --git a/docs/src/content/docs/start-here/system-requirements.mdx b/docs/src/content/docs/start-here/system-requirements.mdx
index 06f4a69e224..44b366dbb3f 100644
--- a/docs/src/content/docs/start-here/system-requirements.mdx
+++ b/docs/src/content/docs/start-here/system-requirements.mdx
@@ -29,6 +29,8 @@ The requirements below are rough guidelines for best performance. GPUs with less
| FLUX.2 Klein 4B | 1024x1024 | Nvidia 30xx+ | 12GB | 16GB | FP8 works with 8GB+; Diffusers + encoder |
| FLUX.2 Klein 9B | 1024x1024 | Nvidia 40xx | 24GB | 32GB | FP8 works with 12GB+; Diffusers + encoder |
| Z-Image Turbo | 1024x1024 | Nvidia 20xx+ | 8GB | 16GB | Q4_K 8GB; Q8/BF16 16GB+ |
+| Wan 2.2 A14B (T2V/I2V) | 1280x720 | Nvidia 30xx+ | 12GB | 32GB | Dual-expert MoE; Q4_K_M 12GB; Q8 18GB+; Diffusers requires 32GB+ |
+| Wan 2.2 TI2V-5B | 1280x720 | Nvidia 20xx+ | 8GB | 16GB | Single transformer; Q4_K_M 6GB+; Q8 8GB+; Diffusers 12GB+ |
:::tip[`tmpfs` on Linux]
If your temporary directory is mounted as a `tmpfs`, ensure it has sufficient space.
diff --git a/invokeai/app/api/auth_dependencies.py b/invokeai/app/api/auth_dependencies.py
index 1df1ed6e250..1ba768a94cd 100644
--- a/invokeai/app/api/auth_dependencies.py
+++ b/invokeai/app/api/auth_dependencies.py
@@ -2,7 +2,7 @@
from typing import Annotated
-from fastapi import Depends, HTTPException, status
+from fastapi import Cookie, Depends, HTTPException, status
from fastapi.security import HTTPAuthorizationCredentials, HTTPBearer
from invokeai.app.api.dependencies import ApiDependencies
@@ -13,6 +13,18 @@
# HTTP Bearer token security scheme
security = HTTPBearer(auto_error=False)
+MEDIA_TOKEN_COOKIE = "invokeai_media_token"
+
+
+def _validate_token(token: str, invalid_detail: str) -> TokenData:
+ token_data = verify_token(token)
+ if token_data is None:
+ raise HTTPException(status_code=status.HTTP_401_UNAUTHORIZED, detail=invalid_detail)
+
+ user = ApiDependencies.invoker.services.users.get(token_data.user_id)
+ if user is None or not user.is_active:
+ raise HTTPException(status_code=status.HTTP_401_UNAUTHORIZED, detail="User not found or inactive")
+ return token_data
async def get_current_user(
@@ -116,6 +128,19 @@ async def get_current_user_or_default(
return token_data
+async def get_current_media_user_or_default(
+ credentials: Annotated[HTTPAuthorizationCredentials | None, Depends(security)],
+ media_token: Annotated[str | None, Cookie(alias=MEDIA_TOKEN_COOKIE)] = None,
+) -> TokenData:
+ """Authenticate media requests with a Bearer token or the path-scoped login cookie."""
+ if not ApiDependencies.invoker.services.configuration.multiuser:
+ return TokenData(user_id="system", email="system@system.invokeai", is_admin=True)
+ token = credentials.credentials if credentials is not None else media_token
+ if token is None:
+ raise HTTPException(status_code=status.HTTP_401_UNAUTHORIZED, detail="Authentication required")
+ return _validate_token(token, "Invalid or expired token")
+
+
async def require_admin(
current_user: Annotated[TokenData, Depends(get_current_user)],
) -> TokenData:
@@ -162,5 +187,6 @@ async def require_admin_or_default(
# Type aliases for convenient use in route dependencies
CurrentUser = Annotated[TokenData, Depends(get_current_user)]
CurrentUserOrDefault = Annotated[TokenData, Depends(get_current_user_or_default)]
+CurrentMediaUserOrDefault = Annotated[TokenData, Depends(get_current_media_user_or_default)]
AdminUser = Annotated[TokenData, Depends(require_admin)]
AdminUserOrDefault = Annotated[TokenData, Depends(require_admin_or_default)]
diff --git a/invokeai/app/api/dependencies.py b/invokeai/app/api/dependencies.py
index 3092f5ab71a..dceda34ea93 100644
--- a/invokeai/app/api/dependencies.py
+++ b/invokeai/app/api/dependencies.py
@@ -10,6 +10,7 @@
from invokeai.app.services.board_image_records.board_image_records_sqlite import SqliteBoardImageRecordStorage
from invokeai.app.services.board_images.board_images_default import BoardImagesService
from invokeai.app.services.board_records.board_records_sqlite import SqliteBoardRecordStorage
+from invokeai.app.services.board_video_records.board_video_records_sqlite import SqliteBoardVideoRecordStorage
from invokeai.app.services.boards.boards_default import BoardService
from invokeai.app.services.bulk_download.bulk_download_default import BulkDownloadService
from invokeai.app.services.client_state_persistence.client_state_persistence_sqlite import ClientStatePersistenceSqlite
@@ -24,6 +25,7 @@
SeedreamProvider,
)
from invokeai.app.services.external_generation.startup import sync_configured_external_starter_models
+from invokeai.app.services.gallery.gallery_default import SqliteGalleryService
from invokeai.app.services.image_files.image_files_disk import DiskImageFileStorage
from invokeai.app.services.image_moves.image_moves_default import ImageMoveService
from invokeai.app.services.image_records.image_records_sqlite import SqliteImageRecordStorage
@@ -52,6 +54,9 @@
from invokeai.app.services.style_preset_records.style_preset_records_sqlite import SqliteStylePresetRecordsStorage
from invokeai.app.services.urls.urls_default import LocalUrlService
from invokeai.app.services.users.users_default import UserService
+from invokeai.app.services.video_files.video_files_disk import DiskVideoFileStorage
+from invokeai.app.services.video_records.video_records_sqlite import SqliteVideoRecordStorage
+from invokeai.app.services.videos.videos_default import VideoService
from invokeai.app.services.workflow_records.workflow_records_sqlite import SqliteWorkflowRecordsStorage
from invokeai.app.services.workflow_thumbnails.workflow_thumbnails_disk import WorkflowThumbnailFileStorageDisk
from invokeai.backend.stable_diffusion.diffusion.conditioning_data import (
@@ -63,6 +68,7 @@
QwenImageConditioningInfo,
SD3ConditioningInfo,
SDXLConditioningInfo,
+ WanConditioningInfo,
ZImageConditioningInfo,
)
from invokeai.backend.util.logging import InvokeAILogger
@@ -108,6 +114,7 @@ def initialize(
raise ValueError("Output folder is not set")
image_files = DiskImageFileStorage(f"{output_folder}/images")
+ video_files = DiskVideoFileStorage(f"{output_folder}/videos")
model_images_folder = config.models_path
style_presets_folder = config.style_presets_path
@@ -133,6 +140,10 @@ def initialize(
image_records = SqliteImageRecordStorage(db=db)
image_moves = ImageMoveService(db=db, image_files=image_files, config=configuration, logger=logger)
images = ImageService()
+ video_records = SqliteVideoRecordStorage(db=db)
+ videos = VideoService()
+ board_video_records = SqliteBoardVideoRecordStorage(db=db)
+ gallery = SqliteGalleryService(db=db)
invocation_cache = MemoryInvocationCache(max_cache_size=config.node_cache_size)
tensors = ObjectSerializerForwardCache(
ObjectSerializerDisk[torch.Tensor](
@@ -154,6 +165,7 @@ def initialize(
ZImageConditioningInfo,
QwenImageConditioningInfo,
AnimaConditioningInfo,
+ WanConditioningInfo,
],
ephemeral=True,
),
@@ -224,6 +236,11 @@ def initialize(
workflow_thumbnails=workflow_thumbnails,
client_state_persistence=client_state_persistence,
users=users,
+ videos=videos,
+ video_files=video_files,
+ video_records=video_records,
+ board_video_records=board_video_records,
+ gallery=gallery,
)
ApiDependencies.invoker = Invoker(services)
diff --git a/invokeai/app/api/routers/auth.py b/invokeai/app/api/routers/auth.py
index e0b0c885cd2..f6a767c7c8d 100644
--- a/invokeai/app/api/routers/auth.py
+++ b/invokeai/app/api/routers/auth.py
@@ -5,12 +5,23 @@
from datetime import timedelta
from typing import Annotated
-from fastapi import APIRouter, Body, HTTPException, Path, status
+from fastapi import APIRouter, Body, Depends, HTTPException, Path, Request, Response, status
+from fastapi.security import HTTPAuthorizationCredentials
from pydantic import BaseModel, Field, field_validator
-from invokeai.app.api.auth_dependencies import AdminUser, CurrentUser
+from invokeai.app.api.auth_dependencies import (
+ MEDIA_TOKEN_COOKIE,
+ AdminUser,
+ CurrentUser,
+ CurrentUserOrDefault,
+ security,
+)
from invokeai.app.api.dependencies import ApiDependencies
-from invokeai.app.services.auth.token_service import TokenData, create_access_token
+from invokeai.app.services.auth.token_service import (
+ TokenData,
+ create_access_token,
+ get_token_remaining_seconds,
+)
from invokeai.app.services.users.users_common import (
UserCreateRequest,
UserDTO,
@@ -74,6 +85,40 @@ class LogoutResponse(BaseModel):
success: bool = Field(description="Whether logout was successful")
+class MediaCookieResponse(BaseModel):
+ """Response from refreshing the media cookie."""
+
+ success: bool = Field(description="Whether the media cookie was set (always true in single-user mode)")
+
+
+def _media_cookie_path(request: Request) -> str:
+ """Public path prefix the media cookie is scoped to.
+
+ Behind a sub-path reverse proxy the browser sees routes under e.g. `/invoke/api/v1`,
+ so the cookie path must include the proxy prefix (advertised via `root_path` by
+ SubPathASGIMiddleware) or browsers will omit the cookie from media requests.
+ """
+ return request.scope.get("root_path", "") + "/api/v1"
+
+
+def _set_media_cookie(request: Request, response: Response, token: str, max_age_seconds: int) -> None:
+ """Set the HttpOnly cookie that authenticates / media requests.
+
+ Media elements can't send Authorization headers, so the image and video media routes
+ accept the JWT via this path-scoped cookie instead. Shared by login and
+ /media-cookie so the cookie attributes can't drift between the two.
+ """
+ response.set_cookie(
+ MEDIA_TOKEN_COOKIE,
+ token,
+ max_age=max_age_seconds,
+ httponly=True,
+ secure=request.url.scheme == "https",
+ samesite="lax",
+ path=_media_cookie_path(request),
+ )
+
+
class SetupStatusResponse(BaseModel):
"""Response for setup status check."""
@@ -119,7 +164,9 @@ async def get_setup_status() -> SetupStatusResponse:
@auth_router.post("/login", response_model=LoginResponse)
async def login(
- request: Annotated[LoginRequest, Body(description="Login credentials")],
+ login_request: Annotated[LoginRequest, Body(description="Login credentials")],
+ request: Request,
+ response: Response,
) -> LoginResponse:
"""Authenticate user and return access token.
@@ -143,7 +190,7 @@ async def login(
)
user_service = ApiDependencies.invoker.services.users
- user = user_service.authenticate(request.email, request.password)
+ user = user_service.authenticate(login_request.email, login_request.password)
if user is None:
raise HTTPException(
@@ -156,14 +203,17 @@ async def login(
raise HTTPException(status_code=status.HTTP_403_FORBIDDEN, detail="User account is disabled")
# Create token with appropriate expiration
- expires_delta = timedelta(days=TOKEN_EXPIRATION_REMEMBER_ME if request.remember_me else TOKEN_EXPIRATION_NORMAL)
+ expires_delta = timedelta(
+ days=TOKEN_EXPIRATION_REMEMBER_ME if login_request.remember_me else TOKEN_EXPIRATION_NORMAL
+ )
token_data = TokenData(
user_id=user.user_id,
email=user.email,
is_admin=user.is_admin,
- remember_me=request.remember_me,
+ remember_me=login_request.remember_me,
)
token = create_access_token(token_data, expires_delta)
+ _set_media_cookie(request, response, token, int(expires_delta.total_seconds()))
return LoginResponse(
token=token,
@@ -175,6 +225,8 @@ async def login(
@auth_router.post("/logout", response_model=LogoutResponse)
async def logout(
current_user: CurrentUser,
+ request: Request,
+ response: Response,
) -> LogoutResponse:
"""Logout current user.
@@ -193,9 +245,57 @@ async def logout(
"""
# TODO: Implement token invalidation when server-side session management is added
# For now, this is a no-op since we use stateless JWT tokens
+ response.delete_cookie(MEDIA_TOKEN_COOKIE, path=_media_cookie_path(request))
return LogoutResponse(success=True)
+@auth_router.post("/media-cookie", response_model=MediaCookieResponse)
+async def refresh_media_cookie(
+ request: Request,
+ response: Response,
+ _current_user: CurrentUserOrDefault,
+ credentials: Annotated[HTTPAuthorizationCredentials | None, Depends(security)],
+) -> MediaCookieResponse:
+ """Re-issue the media cookie from a valid Bearer token.
+
+ The media cookie is normally set at login, but a session can hold a valid JWT
+ without it — the session may predate the cookie's introduction, or the cookie
+ may have been cleared while the JWT (in client storage) survived. Media
+ elements can't send Authorization headers, so such sessions silently fail to
+ load videos (black player) while every other API call works. The frontend
+ calls this on app load so an existing session self-heals without re-login.
+
+ The cookie's lifetime is clamped to the presented token's remaining validity —
+ it is the same JWT, so a longer-lived cookie would just yield 401s after
+ expiry anyway.
+
+ Returns:
+ MediaCookieResponse indicating the cookie was set. In single-user mode the
+ media routes don't require authentication, so this is a successful no-op.
+
+ Raises:
+ HTTPException: 401 if the Bearer token is missing, invalid, or expired, or
+ the user no longer exists or is inactive (raised by the auth dependency).
+ """
+ config = ApiDependencies.invoker.services.configuration
+ if not config.multiuser:
+ return MediaCookieResponse(success=True)
+
+ # CurrentUserOrDefault has already validated the Bearer token (signature, expiry,
+ # user exists and is active) — in multiuser mode it 401s otherwise, so credentials
+ # cannot be None here. The raw token is still needed as the cookie value.
+ if credentials is None:
+ raise HTTPException(status_code=status.HTTP_401_UNAUTHORIZED, detail="Authentication required")
+
+ token = credentials.credentials
+ remaining = get_token_remaining_seconds(token)
+ if remaining is None:
+ raise HTTPException(status_code=status.HTTP_401_UNAUTHORIZED, detail="Invalid or expired token")
+
+ _set_media_cookie(request, response, token, remaining)
+ return MediaCookieResponse(success=True)
+
+
@auth_router.get("/me", response_model=UserDTO)
async def get_current_user_info(
current_user: CurrentUser,
diff --git a/invokeai/app/api/routers/boards.py b/invokeai/app/api/routers/boards.py
index 067936834ee..0f4b6e969e4 100644
--- a/invokeai/app/api/routers/boards.py
+++ b/invokeai/app/api/routers/boards.py
@@ -22,6 +22,22 @@ class DeleteBoardResult(BaseModel):
description="The image names of the board-images relationships that were deleted."
)
deleted_images: list[str] = Field(description="The names of the images that were deleted.")
+ deleted_board_videos: list[str] = Field(
+ default_factory=list,
+ description="The video names of the board-videos relationships that were deleted.",
+ )
+ deleted_videos: list[str] = Field(
+ default_factory=list,
+ description="The names of the videos that were deleted.",
+ )
+ failed_images: list[str] = Field(
+ default_factory=list,
+ description="The names of images that could not be deleted and became uncategorized.",
+ )
+ failed_videos: list[str] = Field(
+ default_factory=list,
+ description="The names of videos that could not be deleted and became uncategorized.",
+ )
@boards_router.post(
@@ -106,7 +122,9 @@ async def update_board(
async def delete_board(
current_user: CurrentUserOrDefault,
board_id: str = Path(description="The id of board to delete"),
- include_images: Optional[bool] = Query(description="Permanently delete all images on the board", default=False),
+ include_images: Optional[bool] = Query(
+ description="Permanently delete all images and videos on the board", default=False
+ ),
) -> DeleteBoardResult:
"""Deletes a board (user must have access to it)"""
try:
@@ -117,20 +135,37 @@ async def delete_board(
if not current_user.is_admin and board.user_id != current_user.user_id:
raise HTTPException(status_code=403, detail="Not authorized to delete this board")
+ # Admins delete everything on the board; regular owners only delete their own
+ # contributions so that contributions from other users to a public/shared board
+ # are preserved (they cascade to "uncategorized" via FK on board_videos / board_images).
+ cascade_user_id: Optional[str] = None if current_user.is_admin else current_user.user_id
+ deleted_images: list[str] = []
+ deleted_videos: list[str] = []
+
try:
if include_images is True:
assert_image_move_maintenance_inactive()
- deleted_images = ApiDependencies.invoker.services.board_images.get_all_board_image_names_for_board(
- board_id=board_id,
- categories=None,
- is_intermediate=None,
+ # The services report both outcomes: records whose file delete failed are
+ # preserved (they cascade to "uncategorized" via the board FKs when the
+ # board is deleted below) and returned as failures. This is the ground
+ # truth — reconstructing failures by diffing a router-side board listing
+ # against the deleted names would double the DB work and misreport items
+ # moved or deleted concurrently between the two queries.
+ deleted_images, failed_images = ApiDependencies.invoker.services.images.delete_images_on_board(
+ board_id=board_id, user_id=cascade_user_id
+ )
+ deleted_videos, failed_videos = ApiDependencies.invoker.services.videos.delete_videos_on_board(
+ board_id=board_id, user_id=cascade_user_id
)
- ApiDependencies.invoker.services.images.delete_images_on_board(board_id=board_id)
ApiDependencies.invoker.services.boards.delete(board_id=board_id)
return DeleteBoardResult(
board_id=board_id,
deleted_board_images=[],
deleted_images=deleted_images,
+ deleted_board_videos=[],
+ deleted_videos=deleted_videos,
+ failed_images=failed_images,
+ failed_videos=failed_videos,
)
else:
deleted_board_images = ApiDependencies.invoker.services.board_images.get_all_board_image_names_for_board(
@@ -138,15 +173,34 @@ async def delete_board(
categories=None,
is_intermediate=None,
)
+ deleted_board_videos = (
+ ApiDependencies.invoker.services.board_video_records.get_all_board_video_names_for_board(
+ board_id=board_id,
+ categories=None,
+ is_intermediate=None,
+ )
+ )
ApiDependencies.invoker.services.boards.delete(board_id=board_id)
return DeleteBoardResult(
board_id=board_id,
deleted_board_images=deleted_board_images,
deleted_images=[],
+ deleted_board_videos=deleted_board_videos,
+ deleted_videos=[],
)
except HTTPException:
raise
except Exception:
+ if include_images is True:
+ raise HTTPException(
+ status_code=500,
+ detail={
+ "message": "Failed to delete board after partially deleting media",
+ "deleted_images": deleted_images,
+ "deleted_videos": deleted_videos,
+ "board_deleted": False,
+ },
+ )
raise HTTPException(status_code=500, detail="Failed to delete board")
diff --git a/invokeai/app/api/routers/gallery.py b/invokeai/app/api/routers/gallery.py
new file mode 100644
index 00000000000..a70822c5af1
--- /dev/null
+++ b/invokeai/app/api/routers/gallery.py
@@ -0,0 +1,99 @@
+from typing import Optional
+
+from fastapi import HTTPException, Query
+from fastapi.routing import APIRouter
+
+from invokeai.app.api.auth_dependencies import CurrentUserOrDefault
+from invokeai.app.api.dependencies import ApiDependencies
+from invokeai.app.api.routers.images import _assert_board_read_access
+from invokeai.app.services.gallery.gallery_common import GalleryItem, GalleryItemNamesResult
+from invokeai.app.services.image_records.image_records_common import ImageCategory, ResourceOrigin
+from invokeai.app.services.shared.pagination import MAX_PAGE_SIZE, OffsetPaginatedResults
+from invokeai.app.services.shared.sqlite.sqlite_common import SQLiteDirection
+
+gallery_router = APIRouter(prefix="/v1/gallery", tags=["gallery"])
+
+
+@gallery_router.get(
+ "/items/",
+ operation_id="list_gallery_items",
+ response_model=OffsetPaginatedResults[GalleryItem],
+)
+async def list_gallery_items(
+ current_user: CurrentUserOrDefault,
+ origin: Optional[ResourceOrigin] = Query(default=None, description="The origin of items to list."),
+ categories: Optional[list[ImageCategory]] = Query(
+ default=None,
+ description="The categories to include. Shared between images and videos.",
+ ),
+ is_intermediate: Optional[bool] = Query(default=None, description="Whether to list intermediate items."),
+ board_id: Optional[str] = Query(
+ default=None,
+ description="The board id to filter by. Use 'none' to find items without a board.",
+ ),
+ # Bounds matter: these flow verbatim into SQL, and a negative LIMIT means
+ # *unlimited* in SQLite — one request would materialize the entire gallery.
+ offset: int = Query(default=0, ge=0, description="The page offset"),
+ limit: int = Query(default=10, ge=0, le=MAX_PAGE_SIZE, description="The number of items per page"),
+ order_dir: SQLiteDirection = Query(default=SQLiteDirection.Descending, description="The order of sort"),
+ starred_first: bool = Query(default=True, description="Whether to sort by starred items first"),
+ search_term: Optional[str] = Query(default=None, description="The term to search for"),
+) -> OffsetPaginatedResults[GalleryItem]:
+ """Returns a paginated, time-sorted stream of polymorphic gallery items (images + videos)."""
+ if board_id is not None and board_id != "none":
+ _assert_board_read_access(board_id, current_user)
+
+ return ApiDependencies.invoker.services.gallery.list_items(
+ offset=offset,
+ limit=limit,
+ starred_first=starred_first,
+ order_dir=order_dir,
+ origin=origin,
+ categories=categories,
+ is_intermediate=is_intermediate,
+ board_id=board_id,
+ search_term=search_term,
+ user_id=current_user.user_id,
+ is_admin=current_user.is_admin,
+ )
+
+
+@gallery_router.get(
+ "/items/names",
+ operation_id="get_gallery_item_names",
+ response_model=GalleryItemNamesResult,
+)
+async def get_gallery_item_names(
+ current_user: CurrentUserOrDefault,
+ origin: Optional[ResourceOrigin] = Query(default=None, description="The origin of items to list."),
+ categories: Optional[list[ImageCategory]] = Query(
+ default=None,
+ description="The categories to include. Shared between images and videos.",
+ ),
+ is_intermediate: Optional[bool] = Query(default=None, description="Whether to list intermediate items."),
+ board_id: Optional[str] = Query(
+ default=None,
+ description="The board id to filter by. Use 'none' to find items without a board.",
+ ),
+ order_dir: SQLiteDirection = Query(default=SQLiteDirection.Descending, description="The order of sort"),
+ starred_first: bool = Query(default=True, description="Whether to sort by starred items first"),
+ search_term: Optional[str] = Query(default=None, description="The term to search for"),
+) -> GalleryItemNamesResult:
+ """Returns an ordered (kind, name) list — used to drive virtualized gallery selection."""
+ if board_id is not None and board_id != "none":
+ _assert_board_read_access(board_id, current_user)
+
+ try:
+ return ApiDependencies.invoker.services.gallery.list_item_names(
+ starred_first=starred_first,
+ order_dir=order_dir,
+ origin=origin,
+ categories=categories,
+ is_intermediate=is_intermediate,
+ board_id=board_id,
+ search_term=search_term,
+ user_id=current_user.user_id,
+ is_admin=current_user.is_admin,
+ )
+ except Exception:
+ raise HTTPException(status_code=500, detail="Failed to get gallery item names")
diff --git a/invokeai/app/api/routers/images.py b/invokeai/app/api/routers/images.py
index e028a92cef9..b9e06befb9c 100644
--- a/invokeai/app/api/routers/images.py
+++ b/invokeai/app/api/routers/images.py
@@ -9,7 +9,7 @@
from PIL import Image
from pydantic import BaseModel, Field, model_validator
-from invokeai.app.api.auth_dependencies import CurrentUserOrDefault
+from invokeai.app.api.auth_dependencies import CurrentMediaUserOrDefault, CurrentUserOrDefault
from invokeai.app.api.dependencies import ApiDependencies
from invokeai.app.api.extract_metadata_from_image import extract_metadata_from_image
from invokeai.app.api.routers._access import (
@@ -48,6 +48,12 @@
IMAGE_MAX_AGE = 31536000
+def _get_image_cache_control() -> str:
+ if ApiDependencies.invoker.services.configuration.multiuser:
+ return "private, no-store"
+ return f"max-age={IMAGE_MAX_AGE}"
+
+
class ResizeToDimensions(BaseModel):
width: int = Field(..., gt=0)
height: int = Field(..., gt=0)
@@ -353,15 +359,15 @@ async def get_image_workflow(
},
)
async def get_image_full(
+ current_user: CurrentMediaUserOrDefault,
image_name: str = Path(description="The name of full-resolution image file to get"),
) -> Response:
"""Gets a full-resolution image file.
- This endpoint is intentionally unauthenticated because browsers load images
- via tags which cannot send Bearer tokens. Image names are UUIDs,
- providing security through unguessability. Returns 409 while image storage
- maintenance is active.
+ Browser media requests authenticate with the path-scoped HttpOnly cookie set at login.
+ Returns 409 while image storage maintenance is active.
"""
+ _assert_image_read_access(image_name, current_user)
assert_image_move_maintenance_inactive()
try:
@@ -369,7 +375,7 @@ async def get_image_full(
with open(path, "rb") as f:
content = f.read()
response = Response(content, media_type="image/png")
- response.headers["Cache-Control"] = f"max-age={IMAGE_MAX_AGE}"
+ response.headers["Cache-Control"] = _get_image_cache_control()
response.headers["Content-Disposition"] = f'inline; filename="{image_name}"'
return response
except Exception:
@@ -389,15 +395,15 @@ async def get_image_full(
},
)
async def get_image_thumbnail(
+ current_user: CurrentMediaUserOrDefault,
image_name: str = Path(description="The name of thumbnail image file to get"),
) -> Response:
"""Gets a thumbnail image file.
- This endpoint is intentionally unauthenticated because browsers load images
- via tags which cannot send Bearer tokens. Image names are UUIDs,
- providing security through unguessability. Returns 409 while image storage
- maintenance is active.
+ Browser media requests authenticate with the path-scoped HttpOnly cookie set at login.
+ Returns 409 while image storage maintenance is active.
"""
+ _assert_image_read_access(image_name, current_user)
assert_image_move_maintenance_inactive()
try:
@@ -405,7 +411,7 @@ async def get_image_thumbnail(
with open(path, "rb") as f:
content = f.read()
response = Response(content, media_type="image/webp")
- response.headers["Cache-Control"] = f"max-age={IMAGE_MAX_AGE}"
+ response.headers["Cache-Control"] = _get_image_cache_control()
return response
except Exception:
raise HTTPException(status_code=404)
@@ -492,9 +498,17 @@ async def delete_images_from_list(
raise
try:
+ # Skip — but do not re-raise — auth failures so a foreign name mid-batch doesn't
+ # discard the response payload for items the caller had already legitimately deleted.
+ # Without this, the client cache never learns about the partial successes and the
+ # already-deleted records reappear in the UI until the next full refresh.
deleted_images: set[str] = set()
+ failed_images: set[str] = set()
affected_boards: set[str] = set()
- for image_name in image_names:
+ # Dedup while preserving order: a name repeated in the request would otherwise
+ # be processed twice, and the second pass's not-found error would land the same
+ # name in both deleted_images and failed_images.
+ for image_name in dict.fromkeys(image_names):
try:
_assert_image_owner(image_name, current_user)
image_dto = ApiDependencies.invoker.services.images.get_dto(image_name)
@@ -503,11 +517,14 @@ async def delete_images_from_list(
deleted_images.add(image_name)
affected_boards.add(board_id)
except HTTPException:
- raise
+ continue
except Exception:
- pass
+ # A genuine deletion failure (not an auth/404 skip) — report it so the
+ # client can surface a partial-failure warning, matching the video path.
+ failed_images.add(image_name)
return DeleteImagesResult(
deleted_images=list(deleted_images),
+ failed_images=list(failed_images),
affected_boards=list(affected_boards),
)
except HTTPException:
@@ -529,6 +546,7 @@ async def delete_uncategorized_images(
try:
deleted_images: set[str] = set()
+ failed_images: set[str] = set()
affected_boards: set[str] = set()
for image_name in image_names:
try:
@@ -540,9 +558,10 @@ async def delete_uncategorized_images(
# Skip images not owned by the current user
pass
except Exception:
- pass
+ failed_images.add(image_name)
return DeleteImagesResult(
deleted_images=list(deleted_images),
+ failed_images=list(failed_images),
affected_boards=list(affected_boards),
)
except Exception:
diff --git a/invokeai/app/api/routers/videos.py b/invokeai/app/api/routers/videos.py
new file mode 100644
index 00000000000..75a04f48087
--- /dev/null
+++ b/invokeai/app/api/routers/videos.py
@@ -0,0 +1,882 @@
+import os
+import re
+import tempfile
+import traceback
+from collections.abc import Iterator
+from pathlib import Path
+from typing import BinaryIO, Optional
+
+from fastapi import Body, HTTPException, Query, Request, Response, UploadFile
+from fastapi import Path as PathParam
+from fastapi.responses import FileResponse, StreamingResponse
+from fastapi.routing import APIRouter
+from PIL import Image as PILImage
+from pydantic import BaseModel, Field, ValidationError
+from starlette.concurrency import run_in_threadpool
+
+from invokeai.app.api.auth_dependencies import CurrentMediaUserOrDefault, CurrentUserOrDefault
+from invokeai.app.api.dependencies import ApiDependencies
+from invokeai.app.api.routers.images import WorkflowAndGraphResponse, _assert_board_read_access
+from invokeai.app.invocations.fields import MetadataField, MetadataFieldValidator
+from invokeai.app.services.image_records.image_records_common import ImageCategory, ResourceOrigin
+from invokeai.app.services.shared.pagination import MAX_PAGE_SIZE, OffsetPaginatedResults
+from invokeai.app.services.shared.sqlite.sqlite_common import SQLiteDirection
+from invokeai.app.services.video_records.video_records_common import VideoNamesResult, VideoRecordChanges
+from invokeai.app.services.videos.videos_common import (
+ AddVideosToBoardResult,
+ DeleteVideosResult,
+ RemoveVideosFromBoardResult,
+ StarredVideosResult,
+ UnstarredVideosResult,
+ VideoDTO,
+ VideoUrlsDTO,
+)
+from invokeai.app.util.video_thumbnails import VideoDecodeTimeoutError, extract_video_frame, probe_video
+
+videos_router = APIRouter(prefix="/v1/videos", tags=["videos"])
+
+# Videos are immutable; set a high max-age (1 year)
+VIDEO_MAX_AGE = 31536000
+
+# MP4 only — the names service emits `{uuid}.mp4` unconditionally and we don't transcode on
+# upload. Accepting .mov/.webm/.mkv here previously caused those containers to be stored
+# under a .mp4 name and served with the .mp4 MIME type, which silently broke playback in
+# browsers when the container did not match.
+ACCEPTED_VIDEO_MIME_PREFIXES = ("video/mp4",)
+ACCEPTED_VIDEO_EXTENSIONS = (".mp4",)
+
+# Per-chunk size for HTTP Range responses (1 MB)
+RANGE_CHUNK_SIZE = 1024 * 1024
+
+# Upload streaming chunk size (1 MB) and a coarse per-upload size cap. The cap is generous
+# because Wan-generated MP4s for long sequences can run into the hundreds of megabytes;
+# the goal is to prevent a single client from exhausting RAM, not to be a content policy.
+UPLOAD_CHUNK_SIZE = 1024 * 1024
+MAX_UPLOAD_SIZE = 1024 * 1024 * 1024 # 1 GB
+# Pre-parse ingress cap enforced by VideoUploadLimitASGIMiddleware, applied to the whole
+# request body *before* the multipart parser spools it to temp storage. Slightly larger
+# than MAX_UPLOAD_SIZE to allow for multipart framing and the metadata form field.
+MAX_UPLOAD_REQUEST_SIZE = MAX_UPLOAD_SIZE + 10 * 1024 * 1024
+# Global bound on concurrent video uploads — each in-flight upload can hold up to two
+# copies of the file in temp storage (the multipart spool + the route's own tmp file).
+MAX_CONCURRENT_VIDEO_UPLOADS = 4
+# Per-user bound (multiuser mode only): keeps one tenant's slow uploads from holding
+# every global slot and starving the other users into 429s.
+MAX_CONCURRENT_VIDEO_UPLOADS_PER_USER = 2
+
+
+def _get_video_cache_control() -> str:
+ if ApiDependencies.invoker.services.configuration.multiuser:
+ return "private, no-store"
+ return f"max-age={VIDEO_MAX_AGE}"
+
+
+def _assert_video_owner(video_name: str, current_user: CurrentUserOrDefault) -> None:
+ """Raise 403 if the current user does not own the video and is not an admin."""
+ from invokeai.app.services.board_records.board_records_common import BoardVisibility
+
+ if current_user.is_admin:
+ return
+ owner = ApiDependencies.invoker.services.video_records.get_user_id(video_name)
+ if owner is not None and owner == current_user.user_id:
+ return
+
+ board_id = ApiDependencies.invoker.services.board_video_records.get_board_for_video(video_name)
+ if board_id is not None:
+ try:
+ board = ApiDependencies.invoker.services.boards.get_dto(board_id=board_id)
+ if board.user_id == current_user.user_id:
+ return
+ if board.board_visibility == BoardVisibility.Public:
+ return
+ except Exception:
+ pass
+
+ raise HTTPException(status_code=403, detail="Not authorized to modify this video")
+
+
+def _assert_video_direct_owner(video_name: str, current_user: CurrentUserOrDefault) -> None:
+ """Raise 403 if the current user is not the direct owner of the video.
+
+ Intentionally stricter than _assert_video_owner: board-ownership and public-board
+ fallbacks are NOT honored. Mirrors _assert_image_direct_owner in board_images.py —
+ board-move operations need to verify the *original* owner, otherwise a user could
+ move someone else's video onto their own board via the board-owner branch.
+ """
+ if current_user.is_admin:
+ return
+ owner = ApiDependencies.invoker.services.video_records.get_user_id(video_name)
+ if owner is not None and owner == current_user.user_id:
+ return
+ raise HTTPException(status_code=403, detail="Not authorized to move this video")
+
+
+def _assert_board_write_access(board_id: str, current_user: CurrentUserOrDefault) -> None:
+ """Raise 403 if the current user may not mutate the given board.
+
+ Mirrors _assert_board_write_access in board_images.py: admins and the board owner
+ may write; public boards accept contributions from any user.
+ """
+ from invokeai.app.services.board_records.board_records_common import BoardVisibility
+
+ try:
+ board = ApiDependencies.invoker.services.boards.get_dto(board_id=board_id)
+ except Exception:
+ raise HTTPException(status_code=404, detail="Board not found")
+ if current_user.is_admin:
+ return
+ if board.user_id == current_user.user_id:
+ return
+ if board.board_visibility == BoardVisibility.Public:
+ return
+ raise HTTPException(status_code=403, detail="Not authorized to modify this board")
+
+
+def _assert_video_read_access(video_name: str, current_user: CurrentUserOrDefault) -> None:
+ """Raise 403 if the current user may not view the video."""
+ from invokeai.app.services.board_records.board_records_common import BoardVisibility
+
+ if current_user.is_admin:
+ return
+ owner = ApiDependencies.invoker.services.video_records.get_user_id(video_name)
+ if owner is not None and owner == current_user.user_id:
+ return
+
+ board_id = ApiDependencies.invoker.services.board_video_records.get_board_for_video(video_name)
+ if board_id is not None:
+ try:
+ board = ApiDependencies.invoker.services.boards.get_dto(board_id=board_id)
+ if board.board_visibility in (BoardVisibility.Shared, BoardVisibility.Public):
+ return
+ except Exception:
+ pass
+
+ raise HTTPException(status_code=403, detail="Not authorized to access this video")
+
+
+def _is_accepted_video_upload(file: UploadFile) -> bool:
+ if file.content_type and file.content_type.startswith(ACCEPTED_VIDEO_MIME_PREFIXES):
+ return True
+ if file.filename:
+ return file.filename.lower().endswith(ACCEPTED_VIDEO_EXTENSIONS)
+ return False
+
+
+def _is_mp4_file(path: Path) -> bool:
+ try:
+ with open(path, "rb") as video_file:
+ search_limit = min(path.stat().st_size, 64 * 1024)
+ position = 0
+ while position + 8 <= search_limit:
+ video_file.seek(position)
+ header = video_file.read(8)
+ box_size = int.from_bytes(header[:4], byteorder="big")
+ box_type = header[4:8]
+ header_size = 8
+ if box_size == 1:
+ extended_size = video_file.read(8)
+ if len(extended_size) != 8:
+ return False
+ box_size = int.from_bytes(extended_size, byteorder="big")
+ header_size = 16
+ if box_size < header_size:
+ return False
+ if box_type == b"ftyp":
+ major_brand = video_file.read(4)
+ return len(major_brand) == 4 and major_brand != b"qt "
+ position += box_size
+ except OSError:
+ return False
+ return False
+
+
+def _probe_decodable_video(path: Path) -> tuple[tuple[int, int, float, Optional[float]], Optional[PILImage.Image]]:
+ """Probes metadata and proves the video has a decodable first frame.
+
+ Returns the metadata plus the decoded frame so the save path can reuse it as the
+ thumbnail source instead of spawning another decode worker. A decode timeout is
+ contention on a loaded server, not evidence the video is bad — probe_video already
+ succeeded — so it yields (metadata, None) and the upload proceeds, with save-time
+ thumbnail extraction as the backstop.
+ """
+ metadata = probe_video(path)
+ try:
+ first_frame = extract_video_frame(path, frame_index=0, raise_on_timeout=True)
+ except VideoDecodeTimeoutError:
+ return metadata, None
+ if first_frame is None:
+ raise ValueError("Video has no decodable frame")
+ return metadata, first_frame
+
+
+@videos_router.post(
+ "/upload",
+ operation_id="upload_video",
+ responses={
+ 201: {"description": "The video was uploaded successfully"},
+ 415: {"description": "Video upload failed"},
+ },
+ status_code=201,
+ response_model=VideoDTO,
+)
+async def upload_video(
+ current_user: CurrentUserOrDefault,
+ file: UploadFile,
+ request: Request,
+ response: Response,
+ video_category: ImageCategory = Query(description="The category of the video"),
+ is_intermediate: bool = Query(description="Whether this is an intermediate video"),
+ board_id: Optional[str] = Query(default=None, description="The board to add this video to, if any"),
+ session_id: Optional[str] = Query(default=None, description="The session ID associated with this upload, if any"),
+ metadata: Optional[str] = Body(
+ default=None,
+ description="The metadata to associate with the video, must be a stringified JSON dict",
+ embed=True,
+ ),
+) -> VideoDTO:
+ """Uploads a video for the current user."""
+ if metadata is not None:
+ try:
+ MetadataFieldValidator.validate_json(metadata)
+ except ValidationError as e:
+ raise HTTPException(status_code=422, detail="Metadata must be a JSON object") from e
+
+ # Check board access for uploads to a specific board.
+ if board_id is not None:
+ from invokeai.app.services.board_records.board_records_common import BoardVisibility
+
+ try:
+ board = ApiDependencies.invoker.services.boards.get_dto(board_id=board_id)
+ except Exception:
+ raise HTTPException(status_code=404, detail="Board not found")
+ if (
+ not current_user.is_admin
+ and board.user_id != current_user.user_id
+ and board.board_visibility != BoardVisibility.Public
+ ):
+ raise HTTPException(status_code=403, detail="Not authorized to upload to this board")
+
+ if not _is_accepted_video_upload(file):
+ raise HTTPException(status_code=415, detail="Not a supported video file")
+
+ # Stream the upload to a tmp file so we can probe and then hand its path to the service.
+ # Reading the full body into memory first risked exhausting RAM on multi-GB uploads;
+ # chunk-stream instead and enforce a hard size cap. Filesystem writes, container
+ # validation, ffmpeg probing, and thumbnail extraction are all blocking — run them in
+ # the thread pool so a slow (or hostile) file can't stall the event loop and every
+ # other API request with it.
+ tmp = tempfile.NamedTemporaryFile(prefix="invokeai_upload_", suffix=".mp4", delete=False)
+ tmp_path = Path(tmp.name)
+ try:
+ total = 0
+ while chunk := await file.read(UPLOAD_CHUNK_SIZE):
+ total += len(chunk)
+ if total > MAX_UPLOAD_SIZE:
+ tmp.close()
+ raise HTTPException(
+ status_code=413,
+ detail=f"Video upload exceeds maximum size ({MAX_UPLOAD_SIZE} bytes)",
+ )
+ await run_in_threadpool(tmp.write, chunk)
+ tmp.close()
+ # Release the multipart spool now that the body is copied: each in-flight
+ # upload otherwise holds TWO on-disk copies (Starlette's spool + our tmp file)
+ # through the probe/thumbnail/create phase — up to 2 x MAX_UPLOAD_SIZE x
+ # MAX_CONCURRENT_VIDEO_UPLOADS of temp disk. Closing shrinks the double-copy
+ # window to the copy loop itself.
+ await file.close()
+
+ if not await run_in_threadpool(_is_mp4_file, tmp_path):
+ raise HTTPException(status_code=415, detail="Not an MP4 video file")
+
+ try:
+ (width, height, duration, fps), first_frame = await run_in_threadpool(_probe_decodable_video, tmp_path)
+ except Exception:
+ ApiDependencies.invoker.services.logger.error(traceback.format_exc())
+ raise HTTPException(status_code=415, detail="Failed to read video")
+
+ try:
+ video_dto = await run_in_threadpool(
+ lambda: ApiDependencies.invoker.services.videos.create(
+ source_path=tmp_path,
+ width=width,
+ height=height,
+ duration=duration,
+ fps=fps,
+ first_frame=first_frame,
+ video_origin=ResourceOrigin.EXTERNAL,
+ video_category=video_category,
+ session_id=session_id,
+ board_id=board_id,
+ metadata=metadata,
+ workflow=None,
+ graph=None,
+ is_intermediate=is_intermediate,
+ user_id=current_user.user_id,
+ )
+ )
+
+ response.status_code = 201
+ response.headers["Location"] = video_dto.video_url
+ return video_dto
+ except Exception:
+ ApiDependencies.invoker.services.logger.error(traceback.format_exc())
+ raise HTTPException(status_code=500, detail="Failed to create video")
+ finally:
+ # If create() succeeded the file was moved; this unlink is a no-op then.
+ try:
+ tmp.close()
+ except Exception:
+ pass
+ try:
+ tmp_path.unlink(missing_ok=True)
+ except Exception:
+ pass
+
+
+@videos_router.delete("/i/{video_name}", operation_id="delete_video", response_model=DeleteVideosResult)
+async def delete_video(
+ current_user: CurrentUserOrDefault,
+ video_name: str = PathParam(description="The name of the video to delete"),
+) -> DeleteVideosResult:
+ _assert_video_owner(video_name, current_user)
+
+ # Let service-level failures surface as 500s rather than swallowing them and returning a
+ # success-shaped response. A previous version of this handler caught everything and
+ # returned an empty ``deleted_videos`` list with HTTP 200; the frontend treated that as
+ # success, dropped the item from its cache, and the video stayed on disk — a silent
+ # data-consistency failure that only became visible on the next page reload.
+ try:
+ video_dto = ApiDependencies.invoker.services.videos.get_dto(video_name)
+ except Exception:
+ raise HTTPException(status_code=404, detail="Video not found")
+
+ board_id = video_dto.board_id or "none"
+ try:
+ ApiDependencies.invoker.services.videos.delete(video_name)
+ except Exception:
+ raise HTTPException(status_code=500, detail="Failed to delete video")
+
+ return DeleteVideosResult(
+ deleted_videos=[video_name],
+ failed_videos=[],
+ affected_boards=[board_id],
+ )
+
+
+@videos_router.post("/delete", operation_id="delete_videos_from_list", response_model=DeleteVideosResult)
+async def delete_videos_from_list(
+ current_user: CurrentUserOrDefault,
+ video_names: list[str] = Body(description="The list of names of videos to delete", embed=True),
+) -> DeleteVideosResult:
+ # Skip — but do not re-raise — auth failures so a foreign name mid-batch doesn't
+ # discard the response payload for items the caller had already legitimately deleted.
+ # Without this, the client cache never learns about the partial successes and the
+ # already-deleted records reappear in the UI until the next full refresh.
+ #
+ # HTTPException here means an intentional skip (a foreign name, or a 404 because a
+ # concurrent session deleted it first) — not a failed deletion. Reporting those in
+ # failed_videos would toast a spurious "could not be deleted" warning; the images
+ # endpoints skip them silently, and mixed selections must behave consistently.
+ deleted_videos: set[str] = set()
+ failed_videos: set[str] = set()
+ affected_boards: set[str] = set()
+ # Dedup while preserving order: a name repeated in the request would otherwise be
+ # processed twice, and the second pass's not-found error would land the same name
+ # in both deleted_videos and failed_videos.
+ for video_name in dict.fromkeys(video_names):
+ try:
+ _assert_video_owner(video_name, current_user)
+ video_dto = ApiDependencies.invoker.services.videos.get_dto(video_name)
+ board_id = video_dto.board_id or "none"
+ ApiDependencies.invoker.services.videos.delete(video_name)
+ deleted_videos.add(video_name)
+ affected_boards.add(board_id)
+ except HTTPException:
+ continue
+ except Exception:
+ failed_videos.add(video_name)
+ return DeleteVideosResult(
+ deleted_videos=list(deleted_videos),
+ failed_videos=list(failed_videos),
+ affected_boards=list(affected_boards),
+ )
+
+
+@videos_router.delete("/uncategorized", operation_id="delete_uncategorized_videos", response_model=DeleteVideosResult)
+async def delete_uncategorized_videos(
+ current_user: CurrentUserOrDefault,
+) -> DeleteVideosResult:
+ """Deletes all uncategorized videos owned by the current user (or all if admin).
+
+ Mirrors ``delete_uncategorized_images`` so the "Delete All Uncategorized
+ Images/Videos" board action covers both media kinds.
+ """
+ names_result = ApiDependencies.invoker.services.videos.get_video_names(
+ board_id="none",
+ user_id=current_user.user_id,
+ is_admin=current_user.is_admin,
+ )
+ deleted_videos: set[str] = set()
+ failed_videos: set[str] = set()
+ affected_boards: set[str] = set()
+ for video_name in names_result.video_names:
+ try:
+ _assert_video_owner(video_name, current_user)
+ ApiDependencies.invoker.services.videos.delete(video_name)
+ deleted_videos.add(video_name)
+ affected_boards.add("none")
+ except HTTPException:
+ # Skip videos not owned by the current user — an intentional skip, not a
+ # failed deletion, so it must not be reported (and toasted) as one.
+ continue
+ except Exception:
+ failed_videos.add(video_name)
+ return DeleteVideosResult(
+ deleted_videos=list(deleted_videos),
+ failed_videos=list(failed_videos),
+ affected_boards=list(affected_boards),
+ )
+
+
+@videos_router.patch("/i/{video_name}", operation_id="update_video", response_model=VideoDTO)
+async def update_video(
+ current_user: CurrentUserOrDefault,
+ video_name: str = PathParam(description="The name of the video to update"),
+ video_changes: VideoRecordChanges = Body(description="The changes to apply to the video"),
+) -> VideoDTO:
+ _assert_video_owner(video_name, current_user)
+ try:
+ return ApiDependencies.invoker.services.videos.update(video_name, video_changes)
+ except Exception:
+ raise HTTPException(status_code=400, detail="Failed to update video")
+
+
+@videos_router.get("/i/{video_name}", operation_id="get_video_dto", response_model=VideoDTO)
+async def get_video_dto(
+ current_user: CurrentUserOrDefault,
+ video_name: str = PathParam(description="The name of video to get"),
+) -> VideoDTO:
+ _assert_video_read_access(video_name, current_user)
+ try:
+ return ApiDependencies.invoker.services.videos.get_dto(video_name)
+ except Exception:
+ raise HTTPException(status_code=404)
+
+
+@videos_router.get(
+ "/i/{video_name}/metadata", operation_id="get_video_metadata", response_model=Optional[MetadataField]
+)
+async def get_video_metadata(
+ current_user: CurrentUserOrDefault,
+ video_name: str = PathParam(description="The name of video to get"),
+) -> Optional[MetadataField]:
+ _assert_video_read_access(video_name, current_user)
+ try:
+ return ApiDependencies.invoker.services.videos.get_metadata(video_name)
+ except Exception:
+ raise HTTPException(status_code=404)
+
+
+@videos_router.get(
+ "/i/{video_name}/workflow", operation_id="get_video_workflow", response_model=WorkflowAndGraphResponse
+)
+async def get_video_workflow(
+ current_user: CurrentUserOrDefault,
+ video_name: str = PathParam(description="The name of video whose workflow to get"),
+) -> WorkflowAndGraphResponse:
+ """Gets the workflow and graph saved with a generated video (mirrors the image route)."""
+ _assert_video_read_access(video_name, current_user)
+ try:
+ workflow = ApiDependencies.invoker.services.videos.get_workflow(video_name)
+ graph = ApiDependencies.invoker.services.videos.get_graph(video_name)
+ return WorkflowAndGraphResponse(workflow=workflow, graph=graph)
+ except Exception:
+ raise HTTPException(status_code=404)
+
+
+def _parse_range_header(range_header: str, file_size: int) -> Optional[tuple[int, int]]:
+ """Parses an HTTP Range header of the form `bytes=START-END`. Returns inclusive (start, end)
+ byte offsets, or None if the header is malformed or unsatisfiable."""
+ match = re.match(r"^bytes=(\d*)-(\d*)$", range_header.strip())
+ if match is None:
+ return None
+ if file_size <= 0:
+ # No byte range is satisfiable against an empty file (a suffix range would
+ # otherwise "satisfy" with the invalid pair (0, -1)).
+ return None
+ start_str, end_str = match.group(1), match.group(2)
+ if start_str == "" and end_str == "":
+ return None
+ if start_str == "":
+ # suffix range: last N bytes
+ try:
+ suffix_len = int(end_str)
+ except ValueError:
+ return None
+ if suffix_len == 0:
+ return None
+ start = max(file_size - suffix_len, 0)
+ end = file_size - 1
+ else:
+ try:
+ start = int(start_str)
+ except ValueError:
+ return None
+ if end_str == "":
+ end = file_size - 1
+ else:
+ try:
+ end = int(end_str)
+ except ValueError:
+ return None
+ if start > end or start >= file_size:
+ return None
+ end = min(end, file_size - 1)
+ return start, end
+
+
+@videos_router.get(
+ "/i/{video_name}/full",
+ operation_id="get_video_full",
+ response_class=Response,
+ responses={
+ 200: {"description": "Return the full video file", "content": {"video/mp4": {}}},
+ 206: {"description": "Return a byte-range of the video file", "content": {"video/mp4": {}}},
+ 404: {"description": "Video not found"},
+ },
+)
+@videos_router.head(
+ "/i/{video_name}/full",
+ operation_id="get_video_full_head",
+ response_class=Response,
+ responses={
+ 200: {"description": "Return the full video file", "content": {"video/mp4": {}}},
+ 404: {"description": "Video not found"},
+ },
+)
+async def get_video_full(
+ request: Request,
+ current_user: CurrentMediaUserOrDefault,
+ video_name: str = PathParam(description="The name of video file to get"),
+) -> Response:
+ """Serves the video file with HTTP Range support so HTML5 seek/scrub works.
+
+ Browser media requests authenticate with the path-scoped HttpOnly cookie set at login.
+ """
+ _assert_video_read_access(video_name, current_user)
+ try:
+ path_str = ApiDependencies.invoker.services.videos.get_path(video_name, thumbnail=False)
+ except Exception:
+ raise HTTPException(status_code=404)
+
+ # Open once and serve every branch from the fd. Deletion stages files away via an
+ # atomic rename, so any later path-based stat/open — including FileResponse's lazy
+ # open after the route returns — races with a concurrent delete and surfaces as an
+ # uncontrolled 500. An open fd is immune: the data stays readable until the handle
+ # closes, even after the path is gone.
+ video_file: Optional[BinaryIO] = None
+ try:
+ video_file = open(path_str, "rb")
+ except OSError:
+ raise HTTPException(status_code=404)
+
+ try:
+ file_size = os.fstat(video_file.fileno()).st_size
+ range_header = request.headers.get("range") or request.headers.get("Range")
+
+ common_headers = {
+ "Accept-Ranges": "bytes",
+ "Cache-Control": _get_video_cache_control(),
+ "Content-Disposition": f'inline; filename="{video_name}"',
+ }
+
+ # HEAD: respond with metadata only.
+ if request.method == "HEAD":
+ return Response(
+ status_code=200,
+ media_type="video/mp4",
+ headers={**common_headers, "Content-Length": str(file_size)},
+ )
+
+ if range_header is None:
+ # Stream from the already-open fd in bounded chunks rather than reading the
+ # file into RAM — multi-GB MP4 downloads (clients without Range, CLI tools,
+ # CDN edge fetches) would otherwise allocate a multi-GB bytes object. This
+ # trades FileResponse's sendfile() for delete-race immunity.
+ handle = video_file
+ video_file = None # ownership moves to the streaming iterator
+
+ def iter_video() -> Iterator[bytes]:
+ try:
+ while chunk := handle.read(RANGE_CHUNK_SIZE):
+ yield chunk
+ finally:
+ handle.close()
+
+ return StreamingResponse(
+ iter_video(),
+ media_type="video/mp4",
+ headers={**common_headers, "Content-Length": str(file_size)},
+ )
+
+ parsed = _parse_range_header(range_header, file_size)
+ if parsed is None:
+ # Unsatisfiable range.
+ return Response(
+ status_code=416,
+ headers={**common_headers, "Content-Range": f"bytes */{file_size}"},
+ )
+ start, end = parsed
+ length = end - start + 1
+ video_file.seek(start)
+ # Read at most one chunk; clients ask for more via subsequent ranges.
+ read_length = min(length, RANGE_CHUNK_SIZE)
+ chunk = video_file.read(read_length)
+ actual_end = start + len(chunk) - 1
+ return Response(
+ chunk,
+ status_code=206,
+ media_type="video/mp4",
+ headers={
+ **common_headers,
+ "Content-Range": f"bytes {start}-{actual_end}/{file_size}",
+ "Content-Length": str(len(chunk)),
+ },
+ )
+ finally:
+ if video_file is not None:
+ video_file.close()
+
+
+@videos_router.get(
+ "/i/{video_name}/thumbnail",
+ operation_id="get_video_thumbnail",
+ response_class=Response,
+ responses={
+ 200: {"description": "Return the video thumbnail", "content": {"image/webp": {}}},
+ 404: {"description": "Video not found"},
+ },
+)
+async def get_video_thumbnail(
+ current_user: CurrentMediaUserOrDefault,
+ video_name: str = PathParam(description="The name of thumbnail file to get"),
+) -> Response:
+ """Returns the first-frame WebP thumbnail of an authorized video."""
+ _assert_video_read_access(video_name, current_user)
+ try:
+ path = ApiDependencies.invoker.services.videos.get_path(video_name, thumbnail=True)
+ except Exception:
+ raise HTTPException(status_code=404)
+ # FileResponse stats the file lazily *after* the route returns, which means a missing
+ # thumbnail surfaces as a server-side error rather than the documented 404. Check up
+ # front so callers get the right status. Video saves are allowed without a thumbnail
+ # (see video_files_disk.save), so this is a reachable path.
+ if not Path(path).is_file():
+ raise HTTPException(status_code=404)
+ return FileResponse(
+ path,
+ media_type="image/webp",
+ headers={"Cache-Control": _get_video_cache_control()},
+ )
+
+
+@videos_router.get("/i/{video_name}/urls", operation_id="get_video_urls", response_model=VideoUrlsDTO)
+async def get_video_urls(
+ current_user: CurrentUserOrDefault,
+ video_name: str = PathParam(description="The name of the video whose URL to get"),
+) -> VideoUrlsDTO:
+ _assert_video_read_access(video_name, current_user)
+ try:
+ video_url = ApiDependencies.invoker.services.videos.get_url(video_name)
+ thumbnail_url = ApiDependencies.invoker.services.videos.get_url(video_name, thumbnail=True)
+ return VideoUrlsDTO(video_name=video_name, video_url=video_url, thumbnail_url=thumbnail_url)
+ except Exception:
+ raise HTTPException(status_code=404)
+
+
+@videos_router.get("/", operation_id="list_video_dtos", response_model=OffsetPaginatedResults[VideoDTO])
+async def list_video_dtos(
+ current_user: CurrentUserOrDefault,
+ video_origin: Optional[ResourceOrigin] = Query(default=None, description="The origin of videos to list."),
+ categories: Optional[list[ImageCategory]] = Query(default=None, description="The categories of video to include."),
+ is_intermediate: Optional[bool] = Query(default=None, description="Whether to list intermediate videos."),
+ board_id: Optional[str] = Query(
+ default=None,
+ description="The board id to filter by. Use 'none' to find videos without a board.",
+ ),
+ # Bounds matter: these flow verbatim into SQL, and a negative LIMIT means
+ # *unlimited* in SQLite — one request would materialize every video row.
+ offset: int = Query(default=0, ge=0, description="The page offset"),
+ limit: int = Query(default=10, ge=0, le=MAX_PAGE_SIZE, description="The number of videos per page"),
+ order_dir: SQLiteDirection = Query(default=SQLiteDirection.Descending, description="The order of sort"),
+ starred_first: bool = Query(default=True, description="Whether to sort by starred videos first"),
+ search_term: Optional[str] = Query(default=None, description="The term to search for"),
+) -> OffsetPaginatedResults[VideoDTO]:
+ """Gets a list of video DTOs for the current user."""
+ # Validate that the caller can read from this board. "none" is handled by the SQL layer.
+ if board_id is not None and board_id != "none":
+ _assert_board_read_access(board_id, current_user)
+
+ return ApiDependencies.invoker.services.videos.get_many(
+ offset,
+ limit,
+ starred_first,
+ order_dir,
+ video_origin,
+ categories,
+ is_intermediate,
+ board_id,
+ search_term,
+ current_user.user_id,
+ current_user.is_admin,
+ )
+
+
+@videos_router.get("/names", operation_id="get_video_names")
+async def get_video_names(
+ current_user: CurrentUserOrDefault,
+ video_origin: Optional[ResourceOrigin] = Query(default=None, description="The origin of videos to list."),
+ categories: Optional[list[ImageCategory]] = Query(default=None, description="The categories of video to include."),
+ is_intermediate: Optional[bool] = Query(default=None, description="Whether to list intermediate videos."),
+ board_id: Optional[str] = Query(
+ default=None,
+ description="The board id to filter by. Use 'none' to find videos without a board.",
+ ),
+ order_dir: SQLiteDirection = Query(default=SQLiteDirection.Descending, description="The order of sort"),
+ starred_first: bool = Query(default=True, description="Whether to sort by starred videos first"),
+ search_term: Optional[str] = Query(default=None, description="The term to search for"),
+) -> VideoNamesResult:
+ """Gets ordered list of video names with metadata for optimistic updates."""
+ # Validate that the caller can read from this board. "none" is handled by the SQL layer.
+ if board_id is not None and board_id != "none":
+ _assert_board_read_access(board_id, current_user)
+
+ try:
+ return ApiDependencies.invoker.services.videos.get_video_names(
+ starred_first=starred_first,
+ order_dir=order_dir,
+ video_origin=video_origin,
+ categories=categories,
+ is_intermediate=is_intermediate,
+ board_id=board_id,
+ search_term=search_term,
+ user_id=current_user.user_id,
+ is_admin=current_user.is_admin,
+ )
+ except Exception:
+ raise HTTPException(status_code=500, detail="Failed to get video names")
+
+
+@videos_router.post("/star", operation_id="star_videos_in_list", response_model=StarredVideosResult)
+async def star_videos_in_list(
+ current_user: CurrentUserOrDefault,
+ video_names: list[str] = Body(description="The list of names of videos to star", embed=True),
+) -> StarredVideosResult:
+ # Skip — but do not re-raise — auth failures so a foreign name mid-batch doesn't
+ # discard the response payload for items that were already starred. Mirrors
+ # delete_videos_from_list: re-raising turned partial successes into an error-shaped
+ # response, so the client never invalidated caches for the videos that did change.
+ starred_videos: set[str] = set()
+ affected_boards: set[str] = set()
+ for video_name in video_names:
+ try:
+ _assert_video_owner(video_name, current_user)
+ updated = ApiDependencies.invoker.services.videos.update(
+ video_name, changes=VideoRecordChanges(starred=True)
+ )
+ starred_videos.add(video_name)
+ affected_boards.add(updated.board_id or "none")
+ except HTTPException:
+ continue
+ except Exception:
+ pass
+ return StarredVideosResult(starred_videos=list(starred_videos), affected_boards=list(affected_boards))
+
+
+@videos_router.post("/unstar", operation_id="unstar_videos_in_list", response_model=UnstarredVideosResult)
+async def unstar_videos_in_list(
+ current_user: CurrentUserOrDefault,
+ video_names: list[str] = Body(description="The list of names of videos to unstar", embed=True),
+) -> UnstarredVideosResult:
+ # See star_videos_in_list: skip foreign names instead of re-raising mid-batch.
+ unstarred_videos: set[str] = set()
+ affected_boards: set[str] = set()
+ for video_name in video_names:
+ try:
+ _assert_video_owner(video_name, current_user)
+ updated = ApiDependencies.invoker.services.videos.update(
+ video_name, changes=VideoRecordChanges(starred=False)
+ )
+ unstarred_videos.add(video_name)
+ affected_boards.add(updated.board_id or "none")
+ except HTTPException:
+ continue
+ except Exception:
+ pass
+ return UnstarredVideosResult(unstarred_videos=list(unstarred_videos), affected_boards=list(affected_boards))
+
+
+class VideoBoardArg(BaseModel):
+ board_id: str = Field(description="The id of the board to add or remove the video from")
+ video_name: str = Field(description="The name of the video to add to / remove from the board")
+
+
+@videos_router.post(
+ "/board",
+ operation_id="add_video_to_board",
+ response_model=AddVideosToBoardResult,
+)
+async def add_video_to_board(
+ current_user: CurrentUserOrDefault,
+ arg: VideoBoardArg = Body(),
+) -> AddVideosToBoardResult:
+ _assert_board_write_access(arg.board_id, current_user)
+ _assert_video_direct_owner(arg.video_name, current_user)
+ try:
+ # Capture the source board BEFORE mutating so the frontend can invalidate both
+ # the old and new board caches. Mirrors add_image_to_board.
+ old_board_id = (
+ ApiDependencies.invoker.services.board_video_records.get_board_for_video(arg.video_name) or "none"
+ )
+ ApiDependencies.invoker.services.board_video_records.add_video_to_board(
+ board_id=arg.board_id, video_name=arg.video_name
+ )
+ return AddVideosToBoardResult(
+ added_videos=[arg.video_name],
+ affected_boards=list({arg.board_id, old_board_id}),
+ )
+ except Exception:
+ raise HTTPException(status_code=500, detail="Failed to add video to board")
+
+
+@videos_router.delete(
+ "/board",
+ operation_id="remove_video_from_board",
+ response_model=RemoveVideosFromBoardResult,
+)
+async def remove_video_from_board(
+ current_user: CurrentUserOrDefault,
+ video_name: str = Body(description="The name of the video to remove from its board", embed=True),
+) -> RemoveVideosFromBoardResult:
+ # A video association can be removed by EITHER the direct video owner OR a user with
+ # write access to the destination board (admin, board owner, or any contributor when the
+ # board is Public). This mirrors remove_image_from_board and prevents a video from being
+ # stranded when a non-owner uploads into a Public board that is later made Shared/Private:
+ # without the board-write fallback, neither the uploader nor the board owner could
+ # detach the video. See PR #9163 review.
+ old_board_id = ApiDependencies.invoker.services.board_video_records.get_board_for_video(video_name)
+ try:
+ _assert_video_direct_owner(video_name, current_user)
+ except HTTPException:
+ if old_board_id is None:
+ raise
+ _assert_board_write_access(old_board_id, current_user)
+ try:
+ ApiDependencies.invoker.services.board_video_records.remove_video_from_board(video_name=video_name)
+ return RemoveVideosFromBoardResult(
+ removed_videos=[video_name],
+ affected_boards=list({old_board_id or "none", "none"}),
+ )
+ except Exception:
+ raise HTTPException(status_code=500, detail="Failed to remove video from board")
diff --git a/invokeai/app/api/routers/virtual_boards.py b/invokeai/app/api/routers/virtual_boards.py
index f0c9e2edc51..78902dd5dec 100644
--- a/invokeai/app/api/routers/virtual_boards.py
+++ b/invokeai/app/api/routers/virtual_boards.py
@@ -3,6 +3,7 @@
from invokeai.app.api.auth_dependencies import CurrentUserOrDefault
from invokeai.app.api.dependencies import ApiDependencies
+from invokeai.app.services.gallery.gallery_common import GalleryItemNamesResult
from invokeai.app.services.image_records.image_records_common import ImageCategory, ImageNamesResult
from invokeai.app.services.shared.sqlite.sqlite_common import SQLiteDirection
from invokeai.app.services.virtual_boards.virtual_boards_common import VirtualSubBoardDTO
@@ -18,9 +19,9 @@
async def list_virtual_boards_by_date(
current_user: CurrentUserOrDefault,
) -> list[VirtualSubBoardDTO]:
- """Gets a list of virtual sub-boards grouped by date."""
+ """Gets a list of virtual sub-boards grouped by date. Covers both images and videos."""
try:
- return ApiDependencies.invoker.services.image_records.get_image_dates(
+ return ApiDependencies.invoker.services.gallery.get_dates(
user_id=current_user.user_id,
is_admin=current_user.is_admin,
)
@@ -41,7 +42,8 @@ async def list_virtual_board_image_names_by_date(
categories: list[ImageCategory] | None = Query(default=None, description="The categories of images to include"),
search_term: str | None = Query(default=None, description="Search term to filter images"),
) -> ImageNamesResult:
- """Gets ordered image names for a specific date."""
+ """Gets ordered image names for a specific date. Image-only; kept for API compatibility —
+ the UI uses the polymorphic `/by_date/{date}/item_names` endpoint."""
try:
return ApiDependencies.invoker.services.image_records.get_image_names_by_date(
date=date,
@@ -54,3 +56,32 @@ async def list_virtual_board_image_names_by_date(
)
except Exception:
raise HTTPException(status_code=500, detail="Failed to get image names for date")
+
+
+@virtual_boards_router.get(
+ "/by_date/{date}/item_names",
+ operation_id="list_virtual_board_item_names_by_date",
+ response_model=GalleryItemNamesResult,
+)
+async def list_virtual_board_item_names_by_date(
+ current_user: CurrentUserOrDefault,
+ date: str = Path(description="The ISO date string, e.g. '2026-03-18'"),
+ starred_first: bool = Query(default=True, description="Whether to sort starred items first"),
+ order_dir: SQLiteDirection = Query(default=SQLiteDirection.Descending, description="The sort direction"),
+ categories: list[ImageCategory] | None = Query(default=None, description="The categories of items to include"),
+ search_term: str | None = Query(default=None, description="Search term to filter items"),
+) -> GalleryItemNamesResult:
+ """Gets ordered polymorphic (image + video) item refs for a specific date."""
+ try:
+ return ApiDependencies.invoker.services.gallery.list_item_names(
+ starred_first=starred_first,
+ order_dir=order_dir,
+ categories=categories,
+ is_intermediate=False,
+ search_term=search_term,
+ user_id=current_user.user_id,
+ is_admin=current_user.is_admin,
+ created_date=date,
+ )
+ except Exception:
+ raise HTTPException(status_code=500, detail="Failed to get gallery item names for date")
diff --git a/invokeai/app/api_app.py b/invokeai/app/api_app.py
index 46f340fce73..747542de397 100644
--- a/invokeai/app/api_app.py
+++ b/invokeai/app/api_app.py
@@ -1,5 +1,6 @@
import asyncio
import logging
+from collections.abc import Callable
from contextlib import asynccontextmanager
from pathlib import Path
@@ -7,11 +8,14 @@
from fastapi.middleware.cors import CORSMiddleware
from fastapi.middleware.gzip import GZipMiddleware
from fastapi.openapi.docs import get_redoc_html, get_swagger_ui_html
-from fastapi.responses import HTMLResponse, RedirectResponse
+from fastapi.responses import HTMLResponse, JSONResponse, RedirectResponse
+from fastapi.security.utils import get_authorization_scheme_param
from fastapi_events.handlers.local import local_handler
from fastapi_events.middleware import EventHandlerASGIMiddleware
+from starlette.concurrency import run_in_threadpool
+from starlette.datastructures import Headers
from starlette.middleware.base import BaseHTTPMiddleware, RequestResponseEndpoint
-from starlette.types import ASGIApp, Receive, Scope, Send
+from starlette.types import ASGIApp, Message, Receive, Scope, Send
import invokeai.frontend.web as web_dir
from invokeai.app.api.dependencies import ApiDependencies
@@ -24,6 +28,7 @@
client_state,
custom_nodes,
download_queue,
+ gallery,
image_moves,
images,
model_manager,
@@ -32,6 +37,7 @@
session_queue,
style_presets,
utilities,
+ videos,
virtual_boards,
workflows,
)
@@ -99,18 +105,36 @@ async def dispatch(self, request: Request, call_next: RequestResponseEndpoint):
# genuine user activity. GET requests are often background fetches (RTK Query
# cache revalidation, refetch-on-focus, etc.) and should not reset the
# inactivity timer.
- if response.status_code < 400 and request.method in ("POST", "PUT", "PATCH", "DELETE"):
+ # Behind a sub-path proxy the public path carries the prefix (see SubPathASGIMiddleware),
+ # so strip `root_path` before matching the auth-route exclusions — otherwise a proxied
+ # logout would mint a replacement token/cookie right after the route deleted them.
+ route_path = request.url.path.removeprefix(request.scope.get("root_path", ""))
+ if (
+ response.status_code < 400
+ and request.method in ("POST", "PUT", "PATCH", "DELETE")
+ and route_path not in ("/api/v1/auth/login", "/api/v1/auth/logout", "/api/v1/auth/media-cookie")
+ ):
auth_header = request.headers.get("authorization", "")
if auth_header.startswith("Bearer "):
token = auth_header[7:]
try:
from datetime import timedelta
- from invokeai.app.api.routers.auth import TOKEN_EXPIRATION_NORMAL, TOKEN_EXPIRATION_REMEMBER_ME
- from invokeai.app.services.auth.token_service import create_access_token, verify_token
+ from invokeai.app.api.routers.auth import (
+ TOKEN_EXPIRATION_NORMAL,
+ TOKEN_EXPIRATION_REMEMBER_ME,
+ )
+ from invokeai.app.services.auth.token_service import TokenData, create_access_token, verify_token
token_data = verify_token(token)
if token_data is not None:
+ # The user lookup is a synchronous SQLite query behind a
+ # process-wide lock; run it off the event loop so a contended
+ # lock (e.g. generation-result writes) can't stall every
+ # concurrent request from inside this per-mutation middleware.
+ user = await run_in_threadpool(ApiDependencies.invoker.services.users.get, token_data.user_id)
+ if user is None or not user.is_active:
+ return response
# Use the remember_me claim from the token to determine the
# correct refresh duration. This avoids the bug where a 7-day
# token with <24h remaining would be silently downgraded to 1 day.
@@ -119,7 +143,13 @@ async def dispatch(self, request: Request, call_next: RequestResponseEndpoint):
else:
expires_delta = timedelta(days=TOKEN_EXPIRATION_NORMAL)
- new_token = create_access_token(token_data, expires_delta)
+ refreshed_data = TokenData(
+ user_id=user.user_id,
+ email=user.email,
+ is_admin=user.is_admin,
+ remember_me=token_data.remember_me,
+ )
+ new_token = create_access_token(refreshed_data, expires_delta)
response.headers["X-Refreshed-Token"] = new_token
except Exception:
pass # Don't fail the request if token refresh fails
@@ -148,6 +178,148 @@ async def dispatch(self, request: Request, call_next: RequestResponseEndpoint):
return response
+def _identify_video_upload_user(scope: Scope) -> tuple[bool, str | None]:
+ """Identify the uploader for auth and per-user slot accounting.
+
+ Returns (authenticated, per_user_key). In single-user mode there is only one
+ tenant, so per_user_key is None — the whole global capacity is theirs and no
+ per-user quota applies.
+ """
+ if not ApiDependencies.invoker.services.configuration.multiuser:
+ return True, None
+
+ scheme, token = get_authorization_scheme_param(Headers(scope=scope).get("authorization"))
+ if scheme.lower() != "bearer":
+ return False, None
+ from invokeai.app.services.auth.token_service import verify_token
+
+ token_data = verify_token(token)
+ if token_data is None:
+ return False, None
+ user = ApiDependencies.invoker.services.users.get(token_data.user_id)
+ if user is None or not user.is_active:
+ return False, None
+ return True, token_data.user_id
+
+
+class VideoUploadLimitASGIMiddleware:
+ """Bound video-upload ingress *before* FastAPI's multipart parser runs.
+
+ The upload route's own MAX_UPLOAD_SIZE check only fires after the multipart body has
+ been fully parsed (and spooled to temp storage), so oversized, chunked, or many
+ concurrent uploads could exhaust temp space before ever being rejected. This
+ middleware rejects oversized requests from the Content-Length header, aborts
+ chunked bodies that exceed the cap mid-stream, and bounds concurrent uploads
+ both globally and per user (so one tenant's slow uploads cannot starve the
+ others into 429s).
+ """
+
+ def __init__(
+ self,
+ app: ASGIApp,
+ max_body_bytes: int,
+ max_concurrent: int,
+ max_concurrent_per_user: int | None = None,
+ identify_user: Callable[[Scope], tuple[bool, str | None]] | None = None,
+ idle_timeout_seconds: float = 120.0,
+ max_upload_duration_seconds: float = 30 * 60,
+ ) -> None:
+ self.app = app
+ self.max_body_bytes = max_body_bytes
+ self.max_concurrent = max_concurrent
+ self.max_concurrent_per_user = max_concurrent_per_user
+ self.identify_user = identify_user
+ self.idle_timeout_seconds = idle_timeout_seconds
+ self.max_upload_duration_seconds = max_upload_duration_seconds
+ self._active = 0
+ self._active_by_user: dict[str, int] = {}
+
+ async def __call__(self, scope: Scope, receive: Receive, send: Send) -> None:
+ if scope["type"] != "http":
+ return await self.app(scope, receive, send)
+ # Behind a sub-path proxy the public path carries the prefix (see SubPathASGIMiddleware).
+ route_path: str = scope.get("path", "").removeprefix(scope.get("root_path", ""))
+ if not (scope.get("method") == "POST" and route_path == "/api/v1/videos/upload"):
+ return await self.app(scope, receive, send)
+
+ per_user_key: str | None = None
+ if self.identify_user is not None:
+ authenticated, per_user_key = self.identify_user(scope)
+ if not authenticated:
+ response = JSONResponse(
+ {"detail": "Authentication required"},
+ status_code=401,
+ headers={"WWW-Authenticate": "Bearer"},
+ )
+ return await response(scope, receive, send)
+
+ content_length = Headers(scope=scope).get("content-length")
+ if content_length is not None and content_length.isdigit() and int(content_length) > self.max_body_bytes:
+ response = JSONResponse(
+ {"detail": f"Video upload exceeds maximum request size ({self.max_body_bytes} bytes)"},
+ status_code=413,
+ )
+ return await response(scope, receive, send)
+
+ if self._active >= self.max_concurrent:
+ response = JSONResponse(
+ {"detail": "Too many concurrent video uploads; try again shortly"},
+ status_code=429,
+ headers={"Retry-After": "5"},
+ )
+ return await response(scope, receive, send)
+
+ if (
+ per_user_key is not None
+ and self.max_concurrent_per_user is not None
+ and self._active_by_user.get(per_user_key, 0) >= self.max_concurrent_per_user
+ ):
+ response = JSONResponse(
+ {"detail": "Too many concurrent video uploads for this user; try again shortly"},
+ status_code=429,
+ headers={"Retry-After": "5"},
+ )
+ return await response(scope, receive, send)
+
+ self._active += 1
+ if per_user_key is not None:
+ self._active_by_user[per_user_key] = self._active_by_user.get(per_user_key, 0) + 1
+ received = 0
+ upload_started_at = asyncio.get_running_loop().time()
+
+ async def limited_receive() -> Message:
+ # Backstop for clients that omit Content-Length (chunked transfer): count the
+ # streamed body and abort the request once it exceeds the cap, so the multipart
+ # parser stops spooling. A clean 413 isn't possible mid-parse; the aborted
+ # request surfaces to the client as a dropped connection.
+ nonlocal received
+ remaining_duration = self.max_upload_duration_seconds - (
+ asyncio.get_running_loop().time() - upload_started_at
+ )
+ if remaining_duration <= 0:
+ return {"type": "http.disconnect"}
+ try:
+ message = await asyncio.wait_for(receive(), timeout=min(self.idle_timeout_seconds, remaining_duration))
+ except TimeoutError:
+ return {"type": "http.disconnect"}
+ if message["type"] == "http.request":
+ received += len(message.get("body", b""))
+ if received > self.max_body_bytes:
+ return {"type": "http.disconnect"}
+ return message
+
+ try:
+ await self.app(scope, limited_receive, send)
+ finally:
+ self._active -= 1
+ if per_user_key is not None:
+ remaining = self._active_by_user.get(per_user_key, 0) - 1
+ if remaining > 0:
+ self._active_by_user[per_user_key] = remaining
+ else:
+ self._active_by_user.pop(per_user_key, None)
+
+
class SubPathASGIMiddleware:
"""Make the app work behind a reverse proxy that serves it under a sub-path (e.g. `/invoke`).
@@ -191,6 +363,13 @@ async def __call__(self, scope: Scope, receive: Receive, send: Send) -> None:
# Add the middleware
app.add_middleware(RedirectRootWithQueryStringMiddleware)
app.add_middleware(SlidingWindowTokenMiddleware)
+app.add_middleware(
+ VideoUploadLimitASGIMiddleware,
+ max_body_bytes=videos.MAX_UPLOAD_REQUEST_SIZE,
+ max_concurrent=videos.MAX_CONCURRENT_VIDEO_UPLOADS,
+ max_concurrent_per_user=videos.MAX_CONCURRENT_VIDEO_UPLOADS_PER_USER,
+ identify_user=_identify_video_upload_user,
+)
# Add event handler
@@ -223,6 +402,8 @@ async def __call__(self, scope: Scope, receive: Receive, send: Send) -> None:
app.include_router(download_queue.download_queue_router, prefix="/api")
app.include_router(image_moves.image_moves_router, prefix="/api")
app.include_router(images.images_router, prefix="/api")
+app.include_router(videos.videos_router, prefix="/api")
+app.include_router(gallery.gallery_router, prefix="/api")
app.include_router(boards.boards_router, prefix="/api")
app.include_router(board_images.board_images_router, prefix="/api")
app.include_router(virtual_boards.virtual_boards_router, prefix="/api")
diff --git a/invokeai/app/invocations/fields.py b/invokeai/app/invocations/fields.py
index 4418c86371a..654282868a9 100644
--- a/invokeai/app/invocations/fields.py
+++ b/invokeai/app/invocations/fields.py
@@ -141,6 +141,7 @@ class UIComponent(str, Enum, metaclass=MetaEnum):
None_ = "none"
Textarea = "textarea"
Slider = "slider"
+ VideoFrameIndex = "video-frame-index"
class FieldDescriptions:
@@ -174,6 +175,9 @@ class FieldDescriptions:
z_image_model = "Z-Image model (Transformer) to load"
qwen_image_model = "Qwen Image Edit model (Transformer) to load"
qwen_vl_encoder = "Qwen2.5-VL tokenizer, processor and text/vision encoder"
+ wan_model = "Wan 2.2 model (Transformer) to load"
+ wan_t5_encoder = "UMT5-XXL tokenizer and text encoder for Wan 2.2"
+ wan_ref_image = "Reference-image (VAE-latent) conditioning for Wan 2.2 I2V."
sdxl_main_model = "SDXL Main model (UNet, VAE, CLIP1, CLIP2) to load"
sdxl_refiner_model = "SDXL Refiner Main Modde (UNet, VAE, CLIP2) to load"
onnx_main_model = "ONNX Main model (UNet, VAE, CLIP) to load"
@@ -241,6 +245,12 @@ class ImageField(BaseModel):
image_name: str = Field(description="The name of the image")
+class VideoField(BaseModel):
+ """A video primitive field"""
+
+ video_name: str = Field(description="The name of the video")
+
+
class BoardField(BaseModel):
"""A board primitive field"""
@@ -365,6 +375,39 @@ class AnimaConditioningField(BaseModel):
)
+class WanConditioningField(BaseModel):
+ """A Wan 2.2 conditioning tensor primitive value.
+
+ Wan conditioning is the UMT5-XXL hidden state for the prompt plus an attention
+ mask marking valid (non-padding) tokens.
+ """
+
+ conditioning_name: str = Field(description="The name of conditioning tensor")
+
+
+class WanRefImageConditioningField(BaseModel):
+ """Reference-image conditioning for Wan 2.2 I2V.
+
+ Carries the 20-channel VAE-latent condition tensor (4-channel first-frame
+ mask + 16-channel ref-image latents). The denoise loop concatenates this
+ to the 16-channel noise latents along the channel dim each step, producing
+ the 36-channel input the I2V-A14B transformer expects.
+
+ Also carries the spatial dims and frame count used to encode the image so
+ the denoise node can sanity-check the user's width/height/num_frames — a
+ latent temporal-dim mismatch is hard to debug from the downstream error.
+ """
+
+ condition_tensor_name: str = Field(description="Name of the saved [1, 20, T_lat, H/8, W/8] condition tensor.")
+ width: int = Field(description="Image width used during VAE encoding (matches denoise width).")
+ height: int = Field(description="Image height used during VAE encoding (matches denoise height).")
+ num_frames: int = Field(
+ default=1,
+ description="Pixel-frame count the condition was built for. 1 for single-frame I2V "
+ "(image output), 81+ for video.",
+ )
+
+
class ConditioningField(BaseModel):
"""A conditioning tensor primitive value"""
diff --git a/invokeai/app/invocations/metadata.py b/invokeai/app/invocations/metadata.py
index da24d8802bb..c5acc6757d9 100644
--- a/invokeai/app/invocations/metadata.py
+++ b/invokeai/app/invocations/metadata.py
@@ -174,6 +174,11 @@ def invoke(self, context: InvocationContext) -> MetadataOutput:
"anima_img2img",
"anima_inpaint",
"anima_outpaint",
+ "wan_txt2img",
+ "wan_img2img",
+ "wan_inpaint",
+ "wan_outpaint",
+ "wan_i2v",
]
diff --git a/invokeai/app/invocations/model.py b/invokeai/app/invocations/model.py
index c82b4369bd8..c61835a331c 100644
--- a/invokeai/app/invocations/model.py
+++ b/invokeai/app/invocations/model.py
@@ -87,6 +87,14 @@ class Qwen3EncoderField(BaseModel):
loras: List[LoRAField] = Field(default_factory=list, description="LoRAs to apply on model loading")
+class WanT5EncoderField(BaseModel):
+ """Field for the UMT5-XXL text encoder used by Wan 2.2 models."""
+
+ tokenizer: ModelIdentifierField = Field(description="Info to load tokenizer submodel")
+ text_encoder: ModelIdentifierField = Field(description="Info to load text_encoder submodel")
+ loras: List[LoRAField] = Field(default_factory=list, description="LoRAs to apply on model loading")
+
+
class VAEField(BaseModel):
vae: ModelIdentifierField = Field(description="Info to load vae submodel")
seamless_axes: List[str] = Field(default_factory=list, description='Axes("x" and "y") to which apply seamless')
@@ -101,6 +109,47 @@ class TransformerField(BaseModel):
loras: List[LoRAField] = Field(description="LoRAs to apply on model loading")
+class WanTransformerField(BaseModel):
+ """Transformer field for Wan 2.2 models.
+
+ Wan 2.2 A14B is a Mixture-of-Experts model with two transformer experts:
+ a high-noise expert (active at large timesteps) and a low-noise expert
+ (active at small timesteps). TI2V-5B is a single-transformer model and only
+ populates ``transformer``.
+
+ ``boundary_ratio`` matches Diffusers' ``WanPipeline`` semantics: it's the
+ boundary timestep as a fraction of ``num_train_timesteps`` (typically 1000),
+ so ``boundary_ratio=0.875`` means the high-noise expert handles t >= 875 and
+ the low-noise expert handles t < 875.
+ """
+
+ transformer: ModelIdentifierField = Field(
+ description="Primary transformer submodel. For A14B this is the high-noise expert."
+ )
+ transformer_low_noise: ModelIdentifierField | None = Field(
+ default=None,
+ description="Low-noise transformer expert (Wan 2.2 A14B only). None for TI2V-5B.",
+ )
+ loras: List[LoRAField] = Field(
+ default_factory=list,
+ description="LoRAs to apply to the primary transformer. For A14B applied to the high-noise expert.",
+ )
+ loras_low_noise: List[LoRAField] = Field(
+ default_factory=list,
+ description="LoRAs to apply to the low-noise expert (Wan 2.2 A14B). The Wan LoRA loader "
+ "routes 'both'- and 'low'-targeted LoRAs here; if empty, no LoRAs are applied to the "
+ "low-noise expert (a 'high'-targeted LoRA must not leak onto it).",
+ )
+ boundary_ratio: float = Field(
+ default=0.875,
+ ge=0.0,
+ le=1.0,
+ description="Boundary timestep as a fraction of num_train_timesteps (Wan 2.2 A14B only). "
+ "High-noise expert: t >= boundary_ratio * num_train_timesteps. Low-noise expert: t below. "
+ "Ignored for TI2V-5B.",
+ )
+
+
@invocation_output("unet_output")
class UNetOutput(BaseInvocationOutput):
"""Base class for invocations that output a UNet field."""
diff --git a/invokeai/app/invocations/primitives.py b/invokeai/app/invocations/primitives.py
index 6249de0cd8e..d6b096b2232 100644
--- a/invokeai/app/invocations/primitives.py
+++ b/invokeai/app/invocations/primitives.py
@@ -29,10 +29,14 @@
SD3ConditioningField,
TensorField,
UIComponent,
+ VideoField,
+ WanConditioningField,
+ WanRefImageConditioningField,
ZImageConditioningField,
)
from invokeai.app.services.images.images_common import ImageDTO
from invokeai.app.services.shared.invocation_context import InvocationContext
+from invokeai.app.services.videos.videos_common import VideoDTO
"""
Primitives: Boolean, Integer, Float, String, Image, Latents, Conditioning, Color
@@ -510,6 +514,44 @@ def build(cls, conditioning_name: str) -> "AnimaConditioningOutput":
return cls(conditioning=AnimaConditioningField(conditioning_name=conditioning_name))
+@invocation_output("wan_conditioning_output")
+class WanConditioningOutput(BaseInvocationOutput):
+ """Base class for nodes that output a Wan 2.2 text conditioning tensor."""
+
+ conditioning: WanConditioningField = OutputField(description=FieldDescriptions.cond)
+
+ @classmethod
+ def build(cls, conditioning_name: str) -> "WanConditioningOutput":
+ return cls(conditioning=WanConditioningField(conditioning_name=conditioning_name))
+
+
+@invocation_output("wan_ref_image_output")
+class WanRefImageOutput(BaseInvocationOutput):
+ """Output of a Wan 2.2 reference-image VAE-encoder."""
+
+ ref_image: WanRefImageConditioningField = OutputField(
+ description="VAE-latent reference-image conditioning for Wan 2.2 I2V.",
+ title="Reference Image",
+ )
+
+ @classmethod
+ def build(
+ cls,
+ condition_tensor_name: str,
+ width: int,
+ height: int,
+ num_frames: int = 1,
+ ) -> "WanRefImageOutput":
+ return cls(
+ ref_image=WanRefImageConditioningField(
+ condition_tensor_name=condition_tensor_name,
+ width=width,
+ height=height,
+ num_frames=num_frames,
+ )
+ )
+
+
@invocation_output("conditioning_output")
class ConditioningOutput(BaseInvocationOutput):
"""Base class for nodes that output a single conditioning tensor"""
@@ -521,6 +563,57 @@ def build(cls, conditioning_name: str) -> "ConditioningOutput":
return cls(conditioning=ConditioningField(conditioning_name=conditioning_name))
+@invocation_output("video_output")
+class VideoOutput(BaseInvocationOutput):
+ """Output of a node that produces a video file (e.g. Wan 2.2 latents-to-video)."""
+
+ video: VideoField = OutputField(description="The output video")
+ width: int = OutputField(description="The width of the video in pixels")
+ height: int = OutputField(description="The height of the video in pixels")
+ num_frames: int = OutputField(description="The number of frames in the video")
+ fps: float = OutputField(description="The frames-per-second of the video")
+ duration: float = OutputField(description="The duration of the video in seconds")
+
+ @classmethod
+ def build(cls, video_dto: VideoDTO, num_frames: Optional[int] = None) -> "VideoOutput":
+ # Frame count isn't stored on the DTO; derive it from duration * fps when fps is known.
+ fps = video_dto.fps or 0.0
+ if num_frames is None:
+ num_frames = int(round(video_dto.duration * fps)) if fps > 0 else 0
+ return cls(
+ video=VideoField(video_name=video_dto.video_name),
+ width=video_dto.width,
+ height=video_dto.height,
+ num_frames=num_frames,
+ fps=fps,
+ duration=video_dto.duration,
+ )
+
+
+@invocation(
+ "video",
+ title="Video Primitive",
+ tags=["primitives", "video"],
+ category="primitives",
+ version="1.0.0",
+)
+class VideoInvocation(BaseInvocation):
+ """A video primitive value. Drop a video onto the field to make it available as an input
+ to downstream nodes (e.g. Frame from Video, Concatenate Videos)."""
+
+ video: VideoField = InputField(description="The video to load")
+
+ # Return annotation is a real class (not a forward-ref string) because a previous
+ # `from __future__ import annotations` left wan_l2v with a stringified annotation
+ # and crashed the output-class registry on startup (commit cac366229a).
+ def invoke(self, context: InvocationContext) -> VideoOutput:
+ from invokeai.app.util.video_thumbnails import decoder_frame_count
+
+ video_dto = context.videos.get_dto(self.video.video_name)
+ num_frames = decoder_frame_count(context.videos.get_path(self.video.video_name))
+ return VideoOutput.build(video_dto=video_dto, num_frames=num_frames)
+
+
@invocation_output("conditioning_collection_output")
class ConditioningCollectionOutput(BaseInvocationOutput):
"""Base class for nodes that output a collection of conditioning tensors"""
diff --git a/invokeai/app/invocations/video_concat.py b/invokeai/app/invocations/video_concat.py
new file mode 100644
index 00000000000..93b01cc9abc
--- /dev/null
+++ b/invokeai/app/invocations/video_concat.py
@@ -0,0 +1,286 @@
+"""Concatenate two or more videos with an optional transition.
+
+Pairs naturally with the I2V chaining workflow: feed several Wan-generated
+clips into this node to glue them into one longer video. The transition
+options hide the seam between independently-denoised clips.
+
+Implementation uses imageio (FFMPEG plugin) for both decode and encode, matching
+``wan_latents_to_video`` and ``video_thumbnails`` — so we can read our own
+output without surprises. Frames stream from the decoders into the encoder one
+at a time, buffering only the transition windows, so peak memory stays
+O(transition_frames) even when the inputs are long uploads (the upload cap
+admits files whose decoded frames would run to tens of gigabytes).
+"""
+
+import math
+import tempfile
+from collections import deque
+from pathlib import Path
+from typing import Callable, Iterable, Iterator, Literal, Optional
+
+import numpy as np
+
+from invokeai.app.invocations.baseinvocation import BaseInvocation, Classification, invocation
+from invokeai.app.invocations.fields import (
+ InputField,
+ VideoField,
+ WithBoard,
+ WithMetadata,
+)
+from invokeai.app.invocations.primitives import VideoOutput
+from invokeai.app.services.session_processor.session_processor_common import CanceledException
+from invokeai.app.services.shared.invocation_context import InvocationContext
+from invokeai.app.util.video_encoding import make_mp4_writer
+from invokeai.app.util.video_thumbnails import iter_video_frames, probe_video
+
+TransitionMode = Literal["cut", "crossfade", "fade_through_black"]
+MAX_TRANSITION_MEMORY_BYTES = 512 * 1024 * 1024
+_BLEND_WORKING_FRAMES = 13
+
+
+def _crossfade(a_tail: list[np.ndarray], b_head: list[np.ndarray]) -> Iterator[np.ndarray]:
+ """Yields a linear A to B cross-dissolve without retaining the blended frames."""
+ n = len(a_tail)
+ for i in range(n):
+ alpha = (i + 1) / (n + 1)
+ blended = a_tail[i].astype(np.float32) * (1.0 - alpha) + b_head[i].astype(np.float32) * alpha
+ yield np.clip(blended, 0, 255).astype(np.uint8)
+
+
+def _fade_through_black(a_tail: list[np.ndarray], b_head: list[np.ndarray]) -> Iterator[np.ndarray]:
+ """A fades to black, then black fades to B. Consumes N/2 frames from each side and returns N output frames.
+
+ Asymmetric framing: the first ``len(a_tail)`` output frames are the trailing A frames scaled
+ toward zero brightness; the next ``len(b_head)`` are the leading B frames scaled up from zero.
+ """
+ n_a = len(a_tail)
+ for i, fa in enumerate(a_tail):
+ # 1.0 at i=0 (fully visible) → near 0 at i=n_a-1 (essentially black).
+ alpha = 1.0 - (i + 1) / (n_a + 1)
+ yield np.clip(fa.astype(np.float32) * alpha, 0, 255).astype(np.uint8)
+ n_b = len(b_head)
+ for j, fb in enumerate(b_head):
+ alpha = (j + 1) / (n_b + 1)
+ yield np.clip(fb.astype(np.float32) * alpha, 0, 255).astype(np.uint8)
+
+
+@invocation(
+ "video_concat",
+ title="Concatenate Videos",
+ tags=["video", "concat", "transition"],
+ category="video",
+ version="1.0.0",
+ classification=Classification.Prototype,
+)
+class VideoConcatInvocation(BaseInvocation, WithMetadata, WithBoard):
+ """Join two or more videos into a single MP4.
+
+ Transitions:
+
+ * ``cut`` — hard splice, no blending. Fastest; total length is the sum of inputs.
+ * ``crossfade`` — linear A→B cross-dissolve over ``transition_frames``. Each boundary
+ consumes ``transition_frames`` from both adjacent clips, so total length is
+ ``sum(inputs) - transition_frames * (n - 1)``.
+ * ``fade_through_black`` — A fades to black, then B fades in from black. Each boundary
+ consumes ``transition_frames // 2`` frames from the preceding clip's tail and the
+ remainder (``transition_frames - transition_frames // 2``) from the next clip's head,
+ so the total emitted is exactly ``transition_frames`` per boundary — even for odd
+ ``transition_frames`` — and the overall length equals the sum of inputs.
+
+ All inputs must share the same pixel dimensions. Output frame rate defaults to the
+ first input's fps; override with ``fps`` to force a specific rate (the frames are not
+ resampled, only the container is encoded at the new rate).
+ """
+
+ videos: list[VideoField] = InputField(
+ min_length=2,
+ description="Videos to concatenate, in order. At least two are required.",
+ )
+ transition: TransitionMode = InputField(
+ default="cut",
+ description="Transition between consecutive clips.",
+ )
+ transition_frames: int = InputField(
+ default=8,
+ ge=0,
+ le=240,
+ description="Length of each transition in frames. Ignored when transition is 'cut'.",
+ )
+ fps: Optional[int] = InputField(
+ default=None,
+ ge=1,
+ le=120,
+ description="Output frame rate. Defaults to the first input's fps.",
+ )
+
+ def invoke(self, context: InvocationContext) -> VideoOutput:
+ if len(self.videos) < 2:
+ raise ValueError("video_concat requires at least two input videos.")
+
+ paths: list[Path] = [context.videos.get_path(v.video_name) for v in self.videos]
+
+ # Probe inputs up front: enforce matching dims and pick the default output fps.
+ probes = [probe_video(p) for p in paths]
+ widths = {(w, h) for (w, h, _, _) in probes}
+ if len(widths) > 1:
+ raise ValueError(
+ f"All inputs must share the same dimensions. Got: "
+ f"{sorted(widths)}. Re-render at a single resolution before concatenating."
+ )
+ width, height, _, _first_fps = probes[0]
+ # libx264 + yuv420p needs even dimensions; we encode with macro_block_size=1 to
+ # preserve the source dimensions exactly, so reject odd sources with a clear error.
+ if width % 2 or height % 2:
+ raise ValueError(
+ f"Input videos are {width}x{height}; H.264 encoding requires even dimensions. "
+ "Re-encode or crop the sources to even width and height first."
+ )
+ self._validate_transition_memory(width, height)
+ output_fps = self._resolve_output_fps([probe[3] for probe in probes])
+
+ context.util.signal_progress(f"Joining {len(self.videos)} clip(s) ({self.transition}) @ {output_fps:.2f} fps")
+
+ tmp = tempfile.NamedTemporaryFile(prefix="invokeai_video_concat_", suffix=".mp4", delete=False)
+ tmp.close()
+ tmp_path = Path(tmp.name)
+ try:
+ # Frames stream from the decoders straight into the encoder; only the
+ # transition windows are buffered. See _iter_joined_frames.
+ writer = make_mp4_writer(tmp_path, output_fps)
+ num_frames = 0
+ try:
+ clip_iters = [iter_video_frames(p, is_canceled=context.util.is_canceled) for p in paths]
+ for frame in self._iter_joined_frames(clip_iters, is_canceled=context.util.is_canceled):
+ writer.append_data(frame)
+ num_frames += 1
+ finally:
+ writer.close()
+
+ if num_frames == 0:
+ raise ValueError("Concatenation produced zero output frames.")
+
+ duration = num_frames / output_fps
+ context.logger.info(
+ f"Encoded concatenated MP4: {num_frames} frames @ {output_fps:.2f} fps "
+ f"({duration:.2f}s) at {width}x{height}"
+ )
+ video_dto = context.videos.save(
+ source_path=tmp_path,
+ width=width,
+ height=height,
+ duration=duration,
+ fps=output_fps,
+ )
+ context.logger.info(f"Saved concatenated video: {video_dto.video_name}")
+ return VideoOutput.build(video_dto)
+ finally:
+ try:
+ tmp_path.unlink(missing_ok=True)
+ except Exception:
+ pass
+
+ def _estimate_transition_memory(self, width: int, height: int) -> int:
+ if self.transition == "cut" or self.transition_frames == 0:
+ return 0
+ buffered_frames = self.transition_frames * (2 if self.transition == "crossfade" else 1)
+ frame_bytes = width * height * 3
+ return frame_bytes * (buffered_frames + _BLEND_WORKING_FRAMES)
+
+ def _resolve_output_fps(self, source_rates: list[Optional[float]]) -> float:
+ if self.fps is not None:
+ return float(self.fps)
+ known_rates = [rate for rate in source_rates if rate is not None and rate > 0]
+ if not known_rates:
+ return 16.0
+ if any(not math.isclose(rate, known_rates[0], rel_tol=1e-3) for rate in known_rates[1:]):
+ raise ValueError("Input videos have different frame rates; set Output FPS to retime them.")
+ # An unknown rate mixed with agreeing known ones is not an error: probe_video
+ # deliberately reports None for metadata-poor containers (VFR flags, missing
+ # avg_frame_rate) whose real rate is usually the same as their neighbours'.
+ # Erroring here would break previously-working concat workflows over a metadata
+ # quirk; disagreement between *known* rates is the case that silently retimes.
+ return known_rates[0]
+
+ def _validate_transition_memory(self, width: int, height: int) -> None:
+ estimated_bytes = self._estimate_transition_memory(width, height)
+ if estimated_bytes > MAX_TRANSITION_MEMORY_BYTES:
+ estimated_mib = estimated_bytes / (1024 * 1024)
+ limit_mib = MAX_TRANSITION_MEMORY_BYTES / (1024 * 1024)
+ raise ValueError(
+ f"The requested transition needs an estimated {estimated_mib:.0f} MiB, "
+ f"which exceeds the {limit_mib:.0f} MiB transition memory budget. "
+ "Lower transition_frames or use lower-resolution clips."
+ )
+
+ def _iter_joined_frames(
+ self,
+ clips: list[Iterable[np.ndarray]],
+ is_canceled: Optional[Callable[[], bool]] = None,
+ ) -> Iterator[np.ndarray]:
+ """Yields the joined output frames, pulling lazily from each clip's frame iterator.
+
+ A frame is emitted as soon as it can no longer participate in a transition, so at
+ most one transition window (the previous clip's tail plus the current clip's head,
+ each bounded by ``transition_frames``) is buffered at a time — never a whole clip.
+
+ Transition layout matches the class docstring:
+
+ * ``crossfade`` consumes ``tf`` frames from both sides of each boundary and emits
+ ``tf`` blended frames in their place.
+ * ``fade_through_black`` splits ``tf`` asymmetrically (``tf // 2`` from the previous
+ clip's tail, the remainder from the next clip's head) so an odd ``tf`` still emits
+ exactly ``tf`` frames per boundary.
+
+ Raises ValueError if a clip decodes to zero frames or is too short to supply its
+ transition windows. Because frames stream straight into the encoder, that error can
+ surface mid-encode; the caller discards the partial output file.
+ """
+ if self.transition == "crossfade" and self.transition_frames > 0:
+ tail_need = head_need = self.transition_frames
+ blend = _crossfade
+ elif self.transition == "fade_through_black" and self.transition_frames > 0:
+ tail_need = self.transition_frames // 2
+ head_need = self.transition_frames - tail_need
+ blend = _fade_through_black
+ else:
+ # "cut", or a zero-length transition: plain concatenation.
+ tail_need = head_need = 0
+ blend = None
+
+ a_tail: list[np.ndarray] = []
+ for i, clip in enumerate(clips):
+ head_want = 0 if i == 0 else head_need
+ tail_keep = 0 if i == len(clips) - 1 else tail_need
+ b_head: list[np.ndarray] = []
+ head_complete = head_want == 0
+ tail_buf: deque[np.ndarray] = deque()
+ n_frames = 0
+ for frame in clip:
+ if is_canceled is not None and is_canceled():
+ raise CanceledException
+ frame = np.ascontiguousarray(frame)
+ n_frames += 1
+ # The clip's first head_want frames are consumed into the boundary blend
+ # with the previous clip's tail rather than emitted directly.
+ if not head_complete:
+ b_head.append(frame)
+ if len(b_head) == head_want and blend is not None:
+ yield from blend(a_tail, b_head)
+ a_tail = []
+ b_head = []
+ head_complete = True
+ continue
+ # Hold back the last tail_keep frames seen so far; anything older is
+ # guaranteed not to be part of the next boundary and can be emitted.
+ tail_buf.append(frame)
+ if len(tail_buf) > tail_keep:
+ yield tail_buf.popleft()
+ if n_frames == 0:
+ raise ValueError(f"Input video {i} ({self.videos[i].video_name}) decoded to zero frames.")
+ if n_frames < head_want + tail_keep:
+ raise ValueError(
+ f"Clip {i} has {n_frames} frames but the requested transitions need "
+ f"{head_want} from its head + {tail_keep} from its tail. Lower "
+ f"transition_frames or use longer clips."
+ )
+ a_tail = list(tail_buf)
diff --git a/invokeai/app/invocations/video_frame_extract.py b/invokeai/app/invocations/video_frame_extract.py
new file mode 100644
index 00000000000..ff048ba4ff3
--- /dev/null
+++ b/invokeai/app/invocations/video_frame_extract.py
@@ -0,0 +1,75 @@
+"""Extract a single frame from a video as an image.
+
+Enables I2V "shot extension": take the last frame of one clip and feed it back
+in as the reference image for the next clip, then concatenate the MP4s
+externally to get a video longer than the model's single-shot frame budget.
+Also useful as a general-purpose video-to-image step.
+"""
+
+from invokeai.app.invocations.baseinvocation import BaseInvocation, Classification, invocation
+from invokeai.app.invocations.fields import (
+ InputField,
+ UIComponent,
+ VideoField,
+ WithBoard,
+ WithMetadata,
+)
+from invokeai.app.invocations.primitives import ImageOutput
+from invokeai.app.services.shared.invocation_context import InvocationContext
+from invokeai.app.util.video_thumbnails import decoder_frame_count, extract_video_frame, probe_video
+
+
+@invocation(
+ "video_frame_extract",
+ title="Frame from Video",
+ tags=["video", "image", "frame"],
+ category="image",
+ version="1.1.0",
+ classification=Classification.Prototype,
+)
+class VideoFrameExtractInvocation(BaseInvocation, WithMetadata, WithBoard):
+ """Extract a single frame from a video and save it as an image.
+
+ ``frame_index`` is 0-based. Negative indices count from the end, so the
+ default of -1 returns the final frame — the typical setup for chaining
+ I2V clips into a longer sequence.
+ """
+
+ video: VideoField = InputField(description="The video to extract a frame from.")
+ frame_index: int = InputField(
+ default=-1,
+ description="Index of the frame to extract. 0 = first frame, -1 = last frame, -2 = second-to-last, etc.",
+ ui_component=UIComponent.VideoFrameIndex,
+ )
+
+ def invoke(self, context: InvocationContext) -> ImageOutput:
+ video_path = context.videos.get_path(self.video.video_name)
+
+ # Resolve negative indices against the actual frame count rather than
+ # trusting imageio plugins to accept index=-1 uniformly. Use the decoder's
+ # frame count when available — duration*fps can be off-by-one for VFR
+ # uploads or containers with approximate metadata, causing frame_index=-1
+ # to point past the final frame.
+ index = self.frame_index
+ if index < 0:
+ n_frames = decoder_frame_count(video_path)
+ if n_frames is None:
+ _, _, duration, fps = probe_video(video_path)
+ if not fps or duration <= 0:
+ raise ValueError(
+ f"Cannot resolve negative frame index for video {self.video.video_name}: "
+ f"probe returned duration={duration}, fps={fps}."
+ )
+ n_frames = int(round(duration * fps))
+ if n_frames <= 0:
+ raise ValueError(f"Video {self.video.video_name} has no decodable frames (probed {n_frames}).")
+ index = n_frames + index
+ if index < 0:
+ raise ValueError(f"frame_index {self.frame_index} is out of range for a {n_frames}-frame video.")
+
+ frame = extract_video_frame(video_path, frame_index=index)
+ if frame is None:
+ raise ValueError(f"Failed to extract frame {index} from {self.video.video_name}.")
+
+ image_dto = context.images.save(image=frame)
+ return ImageOutput.build(image_dto=image_dto)
diff --git a/invokeai/app/invocations/video_frame_extract_range.py b/invokeai/app/invocations/video_frame_extract_range.py
new file mode 100644
index 00000000000..b9449e5201c
--- /dev/null
+++ b/invokeai/app/invocations/video_frame_extract_range.py
@@ -0,0 +1,244 @@
+"""Extract a contiguous range of frames from a video and re-encode as MP4.
+
+Companion to ``video_frame_extract`` (single frame → image) and
+``video_concat`` (many videos → one). This node takes a slice of an input
+video and emits a new MP4, so the output can be fed straight into
+Concatenate Videos to splice clips together — e.g. trim a generated clip
+to a usable middle section before chaining it to another shot.
+"""
+
+import tempfile
+from itertools import islice
+from pathlib import Path
+from typing import Callable, Iterator, Optional, Protocol
+
+import numpy as np
+
+from invokeai.app.invocations.baseinvocation import (
+ BaseInvocation,
+ BaseInvocationOutput,
+ Classification,
+ invocation,
+ invocation_output,
+)
+from invokeai.app.invocations.fields import (
+ InputField,
+ OutputField,
+ UIComponent,
+ VideoField,
+ WithBoard,
+ WithMetadata,
+)
+from invokeai.app.invocations.primitives import VideoOutput
+from invokeai.app.services.session_processor.session_processor_common import CanceledException
+from invokeai.app.services.shared.invocation_context import InvocationContext
+from invokeai.app.util.video_encoding import make_mp4_writer
+from invokeai.app.util.video_thumbnails import decoder_frame_count, iter_video_frames, probe_video
+
+
+class _FrameWriter(Protocol):
+ def append_data(self, frame: np.ndarray) -> None: ...
+
+
+def _validate_even_dimensions(width: int, height: int, video_name: str) -> None:
+ """Raises if the source dimensions can't be encoded as-is with libx264 + yuv420p.
+
+ We encode with ``macro_block_size=1`` to preserve the source dimensions exactly
+ (imageio's default of 16 silently rescales), which requires even width and height.
+ """
+ if width % 2 or height % 2:
+ raise ValueError(
+ f"Video {video_name} is {width}x{height}; H.264 encoding requires even dimensions. "
+ "Re-encode or crop the source to even width and height first."
+ )
+
+
+def _write_frame_range(
+ frames: Iterator[np.ndarray],
+ writer: _FrameWriter,
+ start: int,
+ end: int,
+ is_canceled: Optional[Callable[[], bool]] = None,
+) -> int:
+ """Streams frames[start..end] (inclusive) from a lazy decoder into the writer.
+
+ Frames are appended one at a time as they stream past — the full range is never
+ materialized in RAM. The upload cap admits files whose decoded frames would run to
+ tens of gigabytes, so peak memory here must stay one-frame-sized regardless of the
+ requested range. Decoding stops as soon as ``end`` has been written. Returns the
+ number of frames written.
+ """
+ written = 0
+ for idx, frame in enumerate(islice(frames, end + 1)):
+ if is_canceled is not None and is_canceled():
+ raise CanceledException
+ if idx < start:
+ continue
+ writer.append_data(np.ascontiguousarray(frame))
+ written += 1
+ return written
+
+
+@invocation_output("extract_video_range_output")
+class ExtractVideoRangeOutput(BaseInvocationOutput):
+ """Output of ``extract_video_range``: a trimmed video plus the resolved frame indices.
+
+ Mirrors ``VideoOutput`` so the video can be piped directly into Concatenate Videos or
+ any other ``VideoField``-consuming node, and additionally exposes the resolved
+ (positive, clamped) start and end indices so chained workflows can feed them back in
+ — e.g. drive a downstream Frame from Video to pull the same boundary frame.
+ """
+
+ video: VideoField = OutputField(description="The trimmed video")
+ width: int = OutputField(description="The width of the video in pixels")
+ height: int = OutputField(description="The height of the video in pixels")
+ num_frames: int = OutputField(description="The number of frames in the trimmed video")
+ fps: float = OutputField(description="The frames-per-second of the trimmed video")
+ duration: float = OutputField(description="The duration of the trimmed video in seconds")
+ start_frame: int = OutputField(description="The resolved (positive, 0-based) start frame index in the source video")
+ end_frame: int = OutputField(description="The resolved (positive, 0-based) end frame index in the source video")
+
+
+@invocation(
+ "extract_video_range",
+ title="Frame Range from Video",
+ tags=["video", "trim", "range", "frames"],
+ category="video",
+ version="1.1.0",
+ classification=Classification.Prototype,
+)
+class ExtractVideoRangeInvocation(BaseInvocation, WithMetadata, WithBoard):
+ """Trim a video to a contiguous frame range and re-encode as MP4.
+
+ Both bounds are inclusive and 0-based — ``start_frame=10, end_frame=50``
+ emits 41 frames. Negative indices count from the end (``end_frame=-1``
+ is the final frame), matching ``video_frame_extract``. The output frame
+ rate defaults to the source video's frame rate; set ``fps=0`` to inherit
+ it (or 16 fps if the source rate can't be probed).
+
+ The resolved (positive) ``start_frame`` and ``end_frame`` are also emitted as
+ outputs, so chained workflows can re-use the boundary indices — e.g. feeding
+ them into a downstream Frame from Video to extract the same boundary frame.
+ """
+
+ video: VideoField = InputField(description="The video to extract a frame range from.")
+ start_frame: int = InputField(
+ default=0,
+ description=("First frame to keep, inclusive. 0 = first frame. Negative indices count from the end."),
+ ui_component=UIComponent.VideoFrameIndex,
+ )
+ end_frame: int = InputField(
+ default=-1,
+ description=("Last frame to keep, inclusive. -1 = last frame. Negative indices count from the end."),
+ ui_component=UIComponent.VideoFrameIndex,
+ )
+ fps: int = InputField(
+ default=0,
+ ge=0,
+ le=120,
+ description="Output frame rate. 0 = match the source video's frame rate "
+ "(falls back to 16 fps if the source rate can't be probed).",
+ )
+
+ def invoke(self, context: InvocationContext) -> ExtractVideoRangeOutput:
+ video_path = context.videos.get_path(self.video.video_name)
+ width, height, duration, source_fps = probe_video(video_path)
+
+ n_frames = decoder_frame_count(video_path)
+ if n_frames is None:
+ if not source_fps or duration <= 0:
+ raise ValueError(
+ f"Cannot determine frame count for {self.video.video_name}: "
+ f"probe returned duration={duration}, fps={source_fps}."
+ )
+ n_frames = int(round(duration * source_fps))
+ if n_frames <= 0:
+ raise ValueError(f"Video {self.video.video_name} has no decodable frames (probed {n_frames}).")
+
+ start = self._resolve_index(self.start_frame, n_frames, "start_frame")
+ end = self._resolve_index(self.end_frame, n_frames, "end_frame")
+ if end < start:
+ raise ValueError(
+ f"end_frame ({self.end_frame} → {end}) must be >= start_frame "
+ f"({self.start_frame} → {start}) after resolving negative indices."
+ )
+
+ # Derive the output frame rate from the source video when ``fps`` is 0
+ # (the default), so a trimmed clip plays back at the same speed as its
+ # source. Fall back to 16 fps (the Wan video default) when the source
+ # rate couldn't be probed.
+ if self.fps > 0:
+ output_fps = float(self.fps)
+ elif source_fps and source_fps > 0:
+ output_fps = float(source_fps)
+ else:
+ output_fps = 16.0
+
+ # libx264 + yuv420p needs even dimensions. Reject odd sources up front with a
+ # clear message instead of surfacing an opaque ffmpeg error mid-encode.
+ _validate_even_dimensions(width, height, self.video.video_name)
+
+ context.util.signal_progress(f"Transcoding frames {start}-{end} of {n_frames} @ {output_fps:.2f} fps")
+
+ tmp = tempfile.NamedTemporaryFile(prefix="invokeai_video_range_", suffix=".mp4", delete=False)
+ tmp.close()
+ tmp_path = Path(tmp.name)
+ try:
+ # imageio's iter_index isn't exposed by iio.imiter, so we enumerate and skip.
+ # Frames stream straight from the decoder into the encoder; see _write_frame_range.
+ writer = make_mp4_writer(tmp_path, output_fps)
+ try:
+ num_frames = _write_frame_range(
+ iter_video_frames(video_path, is_canceled=context.util.is_canceled),
+ writer,
+ start,
+ end,
+ is_canceled=context.util.is_canceled,
+ )
+ finally:
+ writer.close()
+
+ expected_frames = end - start + 1
+ if num_frames != expected_frames:
+ raise ValueError(
+ f"Decoded only {num_frames} of {expected_frames} requested frames for range {start}-{end} "
+ f"of {self.video.video_name} "
+ f"(probed {n_frames} frames). The container's metadata may be inaccurate."
+ )
+
+ out_duration = num_frames / output_fps
+ context.logger.info(
+ f"Encoded trimmed MP4: {num_frames} frames @ {output_fps:.2f} fps "
+ f"({out_duration:.2f}s) at {width}x{height}"
+ )
+ video_dto = context.videos.save(
+ source_path=tmp_path,
+ width=width,
+ height=height,
+ duration=out_duration,
+ fps=output_fps,
+ )
+ context.logger.info(f"Saved trimmed video: {video_dto.video_name}")
+ base = VideoOutput.build(video_dto)
+ return ExtractVideoRangeOutput(
+ video=base.video,
+ width=base.width,
+ height=base.height,
+ num_frames=base.num_frames,
+ fps=base.fps,
+ duration=base.duration,
+ start_frame=start,
+ end_frame=end,
+ )
+ finally:
+ try:
+ tmp_path.unlink(missing_ok=True)
+ except Exception:
+ pass
+
+ @staticmethod
+ def _resolve_index(value: int, n_frames: int, field_name: str) -> int:
+ resolved = value + n_frames if value < 0 else value
+ if resolved < 0 or resolved >= n_frames:
+ raise ValueError(f"{field_name}={value} is out of range for a {n_frames}-frame video.")
+ return resolved
diff --git a/invokeai/app/invocations/wan_denoise.py b/invokeai/app/invocations/wan_denoise.py
new file mode 100644
index 00000000000..ee7fbb345fc
--- /dev/null
+++ b/invokeai/app/invocations/wan_denoise.py
@@ -0,0 +1,721 @@
+"""Wan 2.2 denoise invocation.
+
+Supports both single-transformer (TI2V-5B) and dual-expert MoE (A14B) denoising.
+For A14B the high-noise expert handles timesteps ``t >= boundary_timestep`` and
+the low-noise expert handles ``t < boundary_timestep``, where
+``boundary_timestep = boundary_ratio * num_train_timesteps`` (typically 1000).
+
+To keep VRAM usage manageable both experts are pinned in the model cache
+(system RAM) but only one is GPU-resident at a time. The boundary is normally
+crossed once per denoise, so the swap incurs a single CPU→GPU transfer.
+
+Phase 8 will add inpaint via :class:`RectifiedFlowInpaintExtension`.
+
+The transformer call signature mirrors Diffusers' ``WanPipeline``:
+
+ transformer(
+ hidden_states=latents_5d, # [B, C, 1, H/s, W/s]
+ timestep=t.expand(B), # scheduler-time
+ encoder_hidden_states=prompt_embeds, # [B, seq_len, 4096]
+ attention_kwargs=None,
+ return_dict=False,
+ )[0]
+"""
+
+from contextlib import ExitStack
+from pathlib import Path
+from typing import Any, Callable, Iterable, Iterator, Optional, Tuple
+
+import torch
+import torchvision.transforms as tv_transforms
+from torchvision.transforms.functional import resize as tv_resize
+from tqdm import tqdm
+
+from invokeai.app.invocations.baseinvocation import BaseInvocation, Classification, invocation
+from invokeai.app.invocations.fields import (
+ DenoiseMaskField,
+ FieldDescriptions,
+ Input,
+ InputField,
+ LatentsField,
+ WanConditioningField,
+ WanRefImageConditioningField,
+)
+from invokeai.app.invocations.model import LoRAField, WanTransformerField
+from invokeai.app.invocations.primitives import LatentsOutput
+from invokeai.app.services.shared.invocation_context import InvocationContext
+from invokeai.backend.model_manager.taxonomy import BaseModelType, ModelFormat, WanVariantType
+from invokeai.backend.patches.layer_patcher import LayerPatcher
+from invokeai.backend.patches.lora_conversions.wan_lora_constants import WAN_LORA_TRANSFORMER_PREFIX
+from invokeai.backend.patches.model_patch_raw import ModelPatchRaw
+from invokeai.backend.rectified_flow.rectified_flow_inpaint_extension import RectifiedFlowInpaintExtension
+from invokeai.backend.stable_diffusion.diffusers_pipeline import PipelineIntermediateState
+from invokeai.backend.stable_diffusion.diffusion.conditioning_data import WanConditioningInfo
+from invokeai.backend.util.devices import TorchDevice
+from invokeai.backend.wan.sampling_utils import get_spatial_scale_factor, make_noise
+
+# Type alias: a factory that produces a fresh iterator of (LoRA patch, weight)
+# pairs each time it is called. We need fresh iterators because the patcher
+# consumes the iterator once per ``apply_smart_model_patches`` invocation, and
+# the expert may be swapped (and re-entered) multiple times in a render.
+LoRAIteratorFactory = Callable[[], Iterable[Tuple[ModelPatchRaw, float]]]
+
+
+def _resolve_variant(context: InvocationContext, transformer_field: WanTransformerField) -> WanVariantType:
+ """Look up the Wan variant from the main model config that produced this transformer."""
+ config = context.models.get_config(transformer_field.transformer)
+ variant = getattr(config, "variant", None)
+ if not isinstance(variant, WanVariantType):
+ raise ValueError(f"Could not determine Wan variant from model {config.name!r}: variant is {variant!r}.")
+ return variant
+
+
+def _validate_spatial_dimensions(variant: WanVariantType, width: int, height: int) -> None:
+ if variant == WanVariantType.TI2V_5B and (width % 32 or height % 32):
+ raise ValueError(
+ f"TI2V-5B requires width and height to be multiples of 32 (got {width}x{height}). "
+ "Wan 2.2-VAE 16x spatial * transformer patch_size 2 = pixel dims must divide by 32."
+ )
+
+
+def _scheduler_path_for_transformer(context: InvocationContext, transformer_field: WanTransformerField) -> Path | None:
+ """Return the on-disk ``scheduler/`` directory for the main model, or None."""
+ config = context.models.get_config(transformer_field.transformer)
+ model_root = context.models.get_absolute_path(config)
+ if model_root.is_file():
+ return None
+ candidate = model_root / "scheduler"
+ if (candidate / "scheduler_config.json").exists():
+ return candidate
+ return None
+
+
+def _default_scheduler_for_variant(variant: WanVariantType):
+ """Build a variant-appropriate scheduler when no on-disk config is available.
+
+ Standalone GGUF / single-file installs don't ship a ``scheduler/`` directory,
+ so we have to reconstruct the scheduler from variant knowledge. Values are
+ verbatim from each variant's ``scheduler/scheduler_config.json`` in the
+ matching ``Wan-AI/Wan2.2-*-Diffusers`` repo.
+ """
+ from diffusers import UniPCMultistepScheduler
+
+ # Both Wan 2.2 families ship UniPCMultistepScheduler with flow_prediction +
+ # use_flow_sigmas; only the exponential flow_shift differs (5.0 for TI2V-5B,
+ # 3.0 for the A14B pair). Without these settings samples drift — an unshifted
+ # schedule also skews how many A14B steps land above the MoE expert boundary.
+ flow_shift = 5.0 if variant == WanVariantType.TI2V_5B else 3.0
+ return UniPCMultistepScheduler(
+ num_train_timesteps=1000,
+ solver_order=2,
+ prediction_type="flow_prediction",
+ flow_shift=flow_shift,
+ use_flow_sigmas=True,
+ solver_type="bh2",
+ final_sigmas_type="zero",
+ )
+
+
+class _ExpertSwapper:
+ """Manages GPU residency and LoRA patching of one or two Wan transformer experts.
+
+ Both experts are kept in the model cache (system RAM); only one is on
+ device at a time. ``get(label)`` returns the model for the requested label,
+ swapping GPU residency when the label changes and applying that expert's
+ LoRA patches via ``LayerPatcher.apply_smart_model_patches``.
+
+ Ordering on swap: exit the active expert's LoRA context (restores weights)
+ -> exit ``model_on_device`` (returns expert to RAM) -> load the new expert
+ (fresh handle) -> enter its device context -> apply its LoRAs. This
+ mirrors the pattern used by ``flux_denoise``/``anima_denoise`` but adds
+ the extra context layer needed for dual experts.
+
+ Model handles are obtained lazily inside ``get()`` rather than cached at
+ construction. With dual ~9 GB GGUF experts plus a UMT5-XXL encoder
+ competing for the RAM cache, holding both ``LoadedModel`` handles upfront
+ can leave one of them stale by the time the swap happens — InvokeAI's
+ model cache emits a ``has already been dropped from the RAM cache``
+ warning and reloads from disk per swap. See issue #7513 for the broader
+ pattern.
+ """
+
+ HIGH = "high"
+ LOW = "low"
+
+ def __init__(
+ self,
+ context: InvocationContext,
+ high_model: Any,
+ low_model: Any | None,
+ inference_dtype: torch.dtype,
+ high_lora_factory: LoRAIteratorFactory | None = None,
+ low_lora_factory: LoRAIteratorFactory | None = None,
+ high_is_quantized: bool = False,
+ low_is_quantized: bool = False,
+ ) -> None:
+ self._context = context
+ self._high_model = high_model
+ self._low_model = low_model
+ self._inference_dtype = inference_dtype
+ self._high_lora_factory = high_lora_factory
+ self._low_lora_factory = low_lora_factory
+ self._high_is_quantized = high_is_quantized
+ self._low_is_quantized = low_is_quantized
+ self._active_label: str | None = None
+ self._active_info: Any | None = None
+ self._active_device_ctx: Any | None = None
+ self._active_lora_ctx: Any | None = None
+ self._active_model: Any | None = None
+
+ def get(self, label: str) -> Any:
+ if label not in (self.HIGH, self.LOW):
+ raise ValueError(f"Unknown expert label: {label!r}")
+ if label == self.LOW and self._low_model is None:
+ raise ValueError("Low-noise expert was requested but is not available.")
+ if label == self._active_label:
+ assert self._active_model is not None
+ return self._active_model
+
+ # Capture the outgoing expert's cache record before _release() drops our handle.
+ # We need it to force-unload below.
+ outgoing_cached_model = None
+ if self._active_info is not None:
+ # ``LoadedModel`` exposes its cache_record only via a private attribute. There
+ # is no public ``unload_from_vram`` on the LoadedModel today, and we don't want
+ # to take on a broader backend refactor in this fix; tolerate AttributeError
+ # so a future refactor doesn't break the swap.
+ outgoing_cached_model = getattr(self._active_info, "_cache_record", None)
+ if outgoing_cached_model is not None:
+ outgoing_cached_model = getattr(outgoing_cached_model, "cached_model", None)
+
+ # Release current GPU residency before bringing the other expert on device.
+ self._release()
+
+ # Force the outgoing expert off GPU. The model cache's automatic offload
+ # (inside lock() -> _offload_unlocked_models) decides how much to free based on
+ # ``torch.cuda.memory_allocated()`` minus a 3 GB working-memory budget. With Wan
+ # 81-frame video the intermediate activations from the previous denoise step are
+ # still allocated alongside the just-unlocked high-noise expert, so the cache
+ # underestimates how much room the new expert really needs and partial-loads
+ # most of its layers to CPU. The user-visible symptom: log line "Loaded model
+ # ... VRAM: 2381 MB (25.9%)" instead of ~100% for the incoming expert.
+ #
+ # Sidestep the heuristic by explicitly unloading every weight of the outgoing
+ # expert to RAM. This is safe even if the cache evicted the entry between unlock
+ # and now — the cached_model object still owns the tensors.
+ if outgoing_cached_model is not None:
+ try:
+ outgoing_cached_model.full_unload_from_vram()
+ except Exception:
+ pass
+
+ # Hand the PyTorch allocator a clean slate before partial_load_to_vram measures
+ # free space — the freed blocks stay pinned in the caching allocator until
+ # empty_cache is called.
+ TorchDevice.empty_cache()
+
+ # Load the requested expert lazily so its ``LoadedModel`` handle is
+ # always fresh — see class docstring for the cache-eviction reasoning.
+ model_id = self._high_model if label == self.HIGH else self._low_model
+ info = self._context.models.load(model_id)
+ device_ctx = info.model_on_device()
+ cached_weights, model = device_ctx.__enter__()
+
+ # Stash the device-context state immediately. If anything below fails (most
+ # likely the LoRA patcher), the surrounding ExitStack will eventually call
+ # ``swapper.close() -> _release()`` to clean up, and ``_release`` needs to
+ # see the device context to exit it. Without this early stash the expert's
+ # weights stay GPU-resident (8-9 GB for GGUF experts) until the cache's LRU
+ # finally evicts them, masquerading as a memory leak.
+ self._active_label = label
+ self._active_info = info
+ self._active_device_ctx = device_ctx
+ self._active_model = model
+
+ # Apply LoRA patches for this expert. GGUF transformers need sidecar
+ # patching since direct patching of GGMLTensors isn't supported.
+ lora_factory = self._high_lora_factory if label == self.HIGH else self._low_lora_factory
+ is_quantized = self._high_is_quantized if label == self.HIGH else self._low_is_quantized
+ lora_ctx: Any | None = None
+ if lora_factory is not None:
+ lora_ctx = LayerPatcher.apply_smart_model_patches(
+ model=model,
+ patches=lora_factory(),
+ prefix=WAN_LORA_TRANSFORMER_PREFIX,
+ dtype=self._inference_dtype,
+ cached_weights=cached_weights,
+ force_sidecar_patching=is_quantized,
+ )
+ lora_ctx.__enter__()
+
+ self._active_lora_ctx = lora_ctx
+ return model
+
+ def _release(self) -> None:
+ # LoRA context first so weights are restored before the model leaves GPU. The
+ # device context must exit even if weight-restore raises — otherwise the cache
+ # record stays locked and the expert is pinned in VRAM for the whole process.
+ # The field-clearing sits in its own finally so that a device-exit failure
+ # cannot leave stale contexts behind: a later close() would double-exit the
+ # already-exited LoRA context, and get() could hand back a stale model.
+ try:
+ if self._active_lora_ctx is not None:
+ self._active_lora_ctx.__exit__(None, None, None)
+ finally:
+ try:
+ if self._active_device_ctx is not None:
+ self._active_device_ctx.__exit__(None, None, None)
+ finally:
+ self._active_label = None
+ self._active_info = None
+ self._active_device_ctx = None
+ self._active_lora_ctx = None
+ self._active_model = None
+
+ def close(self) -> None:
+ self._release()
+
+
+@invocation(
+ "wan_denoise",
+ title="Denoise - Wan 2.2",
+ tags=["image", "wan"],
+ category="image",
+ version="1.0.0",
+ classification=Classification.Prototype,
+)
+class WanDenoiseInvocation(BaseInvocation):
+ """Run the denoising process with a Wan 2.2 model.
+
+ Drives the flow-matching schedule the model ships with (UniPC with flow
+ sigmas for the Wan 2.2 reference repos; see ``_build_scheduler``). CFG is
+ supported when negative conditioning is provided and ``guidance_scale != 1.0``.
+
+ For Wan 2.2 A14B the high-noise expert handles timesteps at and above
+ ``boundary_ratio * num_train_timesteps``; the low-noise expert handles
+ timesteps below. Both experts share the model cache; only the active one is
+ GPU-resident at any time.
+ """
+
+ transformer: WanTransformerField = InputField(
+ description="Wan transformer field (transformer + optional dual-expert metadata).",
+ input=Input.Connection,
+ title="Transformer",
+ )
+ positive_conditioning: WanConditioningField = InputField(
+ description=FieldDescriptions.positive_cond, input=Input.Connection
+ )
+ negative_conditioning: Optional[WanConditioningField] = InputField(
+ default=None, description=FieldDescriptions.negative_cond, input=Input.Connection
+ )
+
+ ref_image: Optional[WanRefImageConditioningField] = InputField(
+ default=None,
+ description=FieldDescriptions.wan_ref_image,
+ input=Input.Connection,
+ title="Reference Image",
+ )
+
+ latents: Optional[LatentsField] = InputField(
+ default=None,
+ description=FieldDescriptions.latents,
+ input=Input.Connection,
+ )
+ denoise_mask: Optional[DenoiseMaskField] = InputField(
+ default=None,
+ description=FieldDescriptions.denoise_mask,
+ input=Input.Connection,
+ )
+
+ denoising_start: float = InputField(default=0.0, ge=0, le=1, description=FieldDescriptions.denoising_start)
+ denoising_end: float = InputField(default=1.0, ge=0, le=1, description=FieldDescriptions.denoising_end)
+ add_noise: bool = InputField(default=True, description="Add noise based on denoising start.")
+
+ guidance_scale: float = InputField(
+ default=4.0,
+ ge=1.0,
+ description="Classifier-free guidance scale. 4.0 is the Wan 2.2 default for A14B; "
+ "TI2V-5B can tolerate higher values up to ~5.5.",
+ title="Guidance Scale",
+ )
+ guidance_scale_low_noise: Optional[float] = InputField(
+ default=None,
+ ge=0.0,
+ description="Optional separate CFG scale for the low-noise expert (Wan 2.2 A14B only). "
+ "Values below 1.0 (including 0) fall back to the primary 'Guidance Scale'. "
+ "Ignored for TI2V-5B.",
+ title="Guidance Scale (Low Noise)",
+ )
+ # Wan transformer has ``patch_size=(1, 2, 2)``: combined with the VAE's
+ # 8x spatial scale, generated H/W must be a multiple of 16 (not just 8)
+ # or the patch round-trip lands off-by-one and the scheduler step fails
+ # with a spatial-dim mismatch.
+ width: int = InputField(default=1024, multiple_of=16, description="Width of the generated image.")
+ height: int = InputField(default=1024, multiple_of=16, description="Height of the generated image.")
+ steps: int = InputField(default=40, gt=0, description="Number of denoising steps.")
+ seed: int = InputField(default=0, description="Randomness seed for reproducibility.")
+
+ @torch.no_grad()
+ def invoke(self, context: InvocationContext) -> LatentsOutput:
+ latents = self._run_diffusion(context)
+ latents = latents.detach().to("cpu")
+ name = context.tensors.save(tensor=latents)
+ return LatentsOutput.build(latents_name=name, latents=latents, seed=None)
+
+ def _run_diffusion(self, context: InvocationContext) -> torch.Tensor:
+ if self.denoising_start >= self.denoising_end:
+ raise ValueError(
+ f"denoising_start ({self.denoising_start}) must be less than denoising_end ({self.denoising_end})."
+ )
+
+ device = TorchDevice.choose_torch_device()
+ inference_dtype = TorchDevice.choose_bfloat16_safe_dtype(device)
+
+ variant = _resolve_variant(context, self.transformer)
+ _validate_spatial_dimensions(variant, self.width, self.height)
+ spatial_scale = get_spatial_scale_factor(variant)
+
+ scheduler = self._build_scheduler(context, device)
+
+ pos_cond = self._load_conditioning(context, self.positive_conditioning, device=device, dtype=inference_dtype)
+ low_cfg_enabled = (
+ self.transformer.transformer_low_noise is not None
+ and self.guidance_scale_low_noise is not None
+ and self.guidance_scale_low_noise >= 1.0
+ and self.guidance_scale_low_noise != 1.0
+ )
+ do_cfg = self.negative_conditioning is not None and (self.guidance_scale != 1.0 or low_cfg_enabled)
+ neg_cond: WanConditioningInfo | None = None
+ if do_cfg:
+ assert self.negative_conditioning is not None
+ neg_cond = self._load_conditioning(
+ context, self.negative_conditioning, device=device, dtype=inference_dtype
+ )
+
+ # Reference-image conditioning (Wan 2.2 I2V-A14B only). The condition
+ # tensor is 20 channels (4 mask + 16 VAE-encoded image latents); it
+ # gets concatenated to the 16-channel noise latents each step,
+ # yielding the 36-channel input the I2V transformer expects.
+ ref_condition: torch.Tensor | None = None
+ if self.ref_image is not None:
+ if variant != WanVariantType.I2V_A14B:
+ raise ValueError(
+ f"Reference-image conditioning is only supported by the Wan 2.2 I2V variant. "
+ f"The selected transformer is {variant.value!r}. Remove the Reference Image input "
+ "or load an I2V model."
+ )
+ if self.ref_image.width != self.width or self.ref_image.height != self.height:
+ raise ValueError(
+ f"Reference-image dimensions ({self.ref_image.width}x{self.ref_image.height}) must "
+ f"match denoise dimensions ({self.width}x{self.height})."
+ )
+ if self.ref_image.num_frames > 1:
+ # The image denoise produces single-frame output; concatenating a multi-frame
+ # condition to a single-frame noise tensor mismatches the temporal dim and the
+ # downstream tensor-shape error would be unhelpful.
+ raise ValueError(
+ f"This denoise node produces a single-frame image but the reference image was "
+ f"encoded for {self.ref_image.num_frames} frames. Use the Denoise Video - Wan 2.2 "
+ "node for video I2V, or set num_frames=1 on the Reference Image node."
+ )
+ ref_condition = context.tensors.load(self.ref_image.condition_tensor_name).to(
+ device=device, dtype=inference_dtype
+ )
+
+ # Schedule timesteps. set_timesteps populates scheduler.timesteps and
+ # scheduler.sigmas (where sigmas is in [0, 1] flow-matching space).
+ scheduler.set_timesteps(num_inference_steps=self.steps, device=device)
+ timesteps = scheduler.timesteps
+ # sigmas has length steps + 1.
+ sigmas = scheduler.sigmas
+
+ # Apply denoising_start / denoising_end clipping.
+ if self.denoising_start > 0 or self.denoising_end < 1:
+ start_idx = int(self.denoising_start * self.steps)
+ end_idx = int(self.denoising_end * self.steps)
+ timesteps = timesteps[start_idx:end_idx]
+ sigmas = sigmas[start_idx : end_idx + 1]
+ total_steps = len(timesteps)
+
+ # Latents stay in fp32 throughout the denoise loop to avoid accumulating
+ # bf16 quantization across the scheduler's small per-step deltas. We
+ # cast to bf16 only when calling the transformer, matching Diffusers'
+ # WanPipeline (which calls ``prepare_latents(..., dtype=torch.float32)``
+ # then ``latent_model_input = latents.to(transformer_dtype)``).
+ latent_dtype = torch.float32
+
+ # Load init latents (img2img) and convert 4D → 5D.
+ init_latents_5d: torch.Tensor | None = None
+ if self.latents is not None:
+ loaded = context.tensors.load(self.latents.latents_name).to(device=device, dtype=latent_dtype)
+ if loaded.ndim == 4:
+ loaded = loaded.unsqueeze(2)
+ init_latents_5d = loaded
+
+ # Determine the latent channel count. Prefer init_latents shape; otherwise
+ # fall back to the variant default. (We avoid loading the transformer just
+ # to read .config.in_channels; the variant gives us the right answer.)
+ latent_channels = (
+ init_latents_5d.shape[1]
+ if init_latents_5d is not None
+ else (48 if variant == WanVariantType.TI2V_5B else 16)
+ )
+
+ noise = make_noise(
+ batch_size=1,
+ latent_channels=latent_channels,
+ height=self.height,
+ width=self.width,
+ spatial_scale_factor=spatial_scale,
+ device=device,
+ dtype=latent_dtype,
+ seed=self.seed,
+ )
+
+ # Combine init latents + noise per the schedule's starting sigma.
+ if init_latents_5d is not None:
+ if self.add_noise:
+ s_0 = float(sigmas[0])
+ latents = s_0 * noise + (1.0 - s_0) * init_latents_5d
+ else:
+ latents = init_latents_5d
+ else:
+ if self.denoising_start > 1e-5:
+ raise ValueError("denoising_start should be 0 when initial latents are not provided.")
+ latents = noise
+
+ if total_steps <= 0:
+ return latents.squeeze(2)
+
+ # Inpaint extension (4D space — the existing extension is shape-agnostic
+ # but operates on the squeezed-T shape we use for masks).
+ inpaint_mask = self._prep_inpaint_mask(context, latents.squeeze(2))
+ inpaint_extension: RectifiedFlowInpaintExtension | None = None
+ if inpaint_mask is not None:
+ if init_latents_5d is None:
+ raise ValueError("Initial latents are required when using an inpaint mask (img2img inpainting).")
+ inpaint_extension = RectifiedFlowInpaintExtension(
+ init_latents=init_latents_5d.squeeze(2),
+ inpaint_mask=inpaint_mask,
+ noise=noise.squeeze(2),
+ )
+
+ step_callback = self._build_step_callback(context)
+
+ # Resolve experts and the boundary timestep that triggers the MoE swap.
+ #
+ # We deliberately do NOT call ``context.models.load(...)`` for the
+ # transformer experts here — that would put both ~9 GB GGUF handles
+ # in the model cache concurrently. With UMT5-XXL (~10 GB) competing
+ # for the same cache, the LRU policy can drop one of them by the
+ # time the denoise loop swaps in, producing the
+ # "has already been dropped from the RAM cache" warning and forcing
+ # a disk reload per swap. The swapper calls ``models.load`` lazily
+ # inside each ``get()`` instead, so handles are always fresh.
+ #
+ # The config metadata (variant / format) is fine to read upfront —
+ # ``get_config`` doesn't touch the weights cache.
+ high_model = self.transformer.transformer
+ low_model = self.transformer.transformer_low_noise
+ low_config = context.models.get_config(low_model) if low_model is not None else None
+ # FlowMatchEulerDiscreteScheduler stores num_train_timesteps in its config
+ # (default 1000). Diffusers' WanPipeline computes:
+ # boundary_timestep = boundary_ratio * num_train_timesteps
+ num_train_timesteps = int(scheduler.config.num_train_timesteps)
+ boundary_timestep = self.transformer.boundary_ratio * num_train_timesteps if low_model is not None else None
+
+ # LoRA wiring. The high-noise expert uses ``transformer.loras``; the
+ # low-noise expert uses ``transformer.loras_low_noise``. No fallback
+ # between the lists: the Wan LoRA loader routes untagged LoRAs to both
+ # and expert-tagged LoRAs to exactly one, so an empty low list means
+ # "no LoRAs on the low expert" — reusing the primary list here would
+ # silently apply high-expert-only LoRAs (e.g. Lightning high-noise
+ # distills) to the wrong expert.
+ # Quantized (GGUF) experts force sidecar patching so GGMLTensor weights
+ # aren't touched directly.
+ high_loras = self.transformer.loras
+ low_loras = self.transformer.loras_low_noise
+ high_config = context.models.get_config(high_model)
+ high_is_quantized = high_config.format == ModelFormat.GGUFQuantized
+ low_is_quantized = low_config.format == ModelFormat.GGUFQuantized if low_config is not None else False
+
+ def high_lora_factory() -> Iterable[Tuple[ModelPatchRaw, float]]:
+ return self._lora_iterator(context, high_loras)
+
+ def low_lora_factory() -> Iterable[Tuple[ModelPatchRaw, float]]:
+ return self._lora_iterator(context, low_loras)
+
+ with ExitStack() as exit_stack:
+ swapper = _ExpertSwapper(
+ context=context,
+ high_model=high_model,
+ low_model=low_model,
+ inference_dtype=inference_dtype,
+ high_lora_factory=high_lora_factory if high_loras else None,
+ low_lora_factory=low_lora_factory if low_loras else None,
+ high_is_quantized=high_is_quantized,
+ low_is_quantized=low_is_quantized,
+ )
+ exit_stack.callback(swapper.close)
+
+ for step_idx, t in enumerate(tqdm(timesteps, desc="Denoising (Wan 2.2)", total=total_steps)):
+ timestep = t.expand(latents.shape[0])
+
+ # Pick the active expert: high-noise for t >= boundary_timestep,
+ # low-noise below. Single-transformer models always use HIGH.
+ if low_model is not None and float(t) < float(boundary_timestep):
+ active_label = _ExpertSwapper.LOW
+ # Treat None or values below 1.0 (incl. the FE's default 0)
+ # as "use the primary guidance_scale".
+ low_cfg = self.guidance_scale_low_noise
+ active_cfg = low_cfg if (low_cfg is not None and low_cfg >= 1.0) else self.guidance_scale
+ else:
+ active_label = _ExpertSwapper.HIGH
+ active_cfg = self.guidance_scale
+
+ transformer = swapper.get(active_label)
+
+ # Cast latents to the transformer's dtype only for the forward
+ # pass; keep the scheduler-level latents in fp32.
+ latent_model_input = latents.to(dtype=inference_dtype)
+
+ # For I2V, concatenate the ref-image condition (4-ch mask + 16-ch
+ # image latents) along the channel dim, producing the 36-channel
+ # input the I2V transformer's patch_embedding expects.
+ if ref_condition is not None:
+ latent_model_input = torch.cat([latent_model_input, ref_condition], dim=1)
+
+ noise_pred_cond = transformer(
+ hidden_states=latent_model_input,
+ timestep=timestep,
+ encoder_hidden_states=pos_cond.prompt_embeds.unsqueeze(0),
+ attention_kwargs=None,
+ return_dict=False,
+ )[0]
+
+ if neg_cond is not None and active_cfg != 1.0:
+ noise_pred_uncond = transformer(
+ hidden_states=latent_model_input,
+ timestep=timestep,
+ encoder_hidden_states=neg_cond.prompt_embeds.unsqueeze(0),
+ attention_kwargs=None,
+ return_dict=False,
+ )[0]
+ noise_pred = noise_pred_uncond + active_cfg * (noise_pred_cond - noise_pred_uncond)
+ else:
+ noise_pred = noise_pred_cond
+
+ latents = scheduler.step(noise_pred, t, latents, return_dict=False)[0]
+
+ if inpaint_extension is not None:
+ sigma_prev = float(sigmas[step_idx + 1])
+ latents_4d = latents.squeeze(2)
+ latents_4d = inpaint_extension.merge_intermediate_latents_with_init_latents(latents_4d, sigma_prev)
+ latents = latents_4d.unsqueeze(2)
+
+ step_callback(
+ PipelineIntermediateState(
+ step=step_idx + 1,
+ order=1,
+ total_steps=total_steps,
+ timestep=int(t.item()),
+ latents=latents.squeeze(2),
+ )
+ )
+
+ # Squeeze T for downstream 4D consumers.
+ return latents.squeeze(2)
+
+ def _build_scheduler(self, context: InvocationContext, device: torch.device):
+ """Construct the scheduler matching the model's on-disk ``scheduler_config.json``.
+
+ All Wan 2.2 variants ship ``UniPCMultistepScheduler`` with flow-matching
+ settings, but with different exponential shifts (TI2V-5B: ``flow_shift=5.0``,
+ A14B: ``3.0``) — and future variants may differ further, so when a
+ ``scheduler/`` directory exists we dispatch on its ``_class_name`` and load
+ it verbatim. When no on-disk config is available (standalone GGUF /
+ single-file installs don't ship one) — or the config is unreadable — fall
+ back to the variant-aware default so the noise schedule still matches what
+ the model was trained against.
+ """
+ import json
+
+ import diffusers
+
+ scheduler_dir = _scheduler_path_for_transformer(context, self.transformer)
+ scheduler_cls = None
+ if scheduler_dir is not None:
+ # Read the on-disk class name and instantiate that class. Diffusers'
+ # SchedulerMixin.from_pretrained does class dispatch internally, but
+ # only when called from the abstract base; calling a concrete subclass
+ # silently builds the wrong type. Resolve it explicitly.
+ config_path = scheduler_dir / "scheduler_config.json"
+ try:
+ with config_path.open("r", encoding="utf-8") as f:
+ cfg = json.load(f)
+ class_name = cfg.get("_class_name")
+ scheduler_cls = getattr(diffusers, class_name, None) if class_name else None
+ except (OSError, json.JSONDecodeError):
+ scheduler_cls = None
+
+ if scheduler_dir is None or scheduler_cls is None:
+ variant = _resolve_variant(context, self.transformer)
+ return _default_scheduler_for_variant(variant)
+
+ return scheduler_cls.from_pretrained(str(scheduler_dir), local_files_only=True)
+
+ def _load_conditioning(
+ self,
+ context: InvocationContext,
+ cond_field: WanConditioningField,
+ *,
+ device: torch.device,
+ dtype: torch.dtype,
+ ) -> WanConditioningInfo:
+ cond_data = context.conditioning.load(cond_field.conditioning_name)
+ assert len(cond_data.conditionings) == 1
+ cond_info = cond_data.conditionings[0]
+ assert isinstance(cond_info, WanConditioningInfo)
+ return cond_info.to(device=device, dtype=dtype)
+
+ def _prep_inpaint_mask(self, context: InvocationContext, latents_4d: torch.Tensor) -> torch.Tensor | None:
+ """Resize the user-supplied mask down to latent resolution.
+
+ Convention matches Anima/FLUX: the original mask has 0 = preserve and
+ 1 = denoise; the extension expects the inverted form.
+ """
+ if self.denoise_mask is None:
+ return None
+ mask = context.tensors.load(self.denoise_mask.mask_name)
+ mask = 1.0 - mask
+ _, _, latent_h, latent_w = latents_4d.shape
+ mask = tv_resize(
+ img=mask,
+ size=[latent_h, latent_w],
+ interpolation=tv_transforms.InterpolationMode.BILINEAR,
+ antialias=False,
+ )
+ return mask.to(device=latents_4d.device, dtype=latents_4d.dtype)
+
+ def _build_step_callback(self, context: InvocationContext) -> Callable[[PipelineIntermediateState], None]:
+ def step_callback(state: PipelineIntermediateState) -> None:
+ context.util.sd_step_callback(state, BaseModelType.Wan)
+
+ return step_callback
+
+ def _lora_iterator(
+ self, context: InvocationContext, loras: list[LoRAField]
+ ) -> Iterator[Tuple[ModelPatchRaw, float]]:
+ """Yield (ModelPatchRaw, weight) pairs for the given LoRA list.
+
+ The caller passes either ``transformer.loras`` (high-noise expert) or
+ ``transformer.loras_low_noise`` (low-noise expert) — the fallback to
+ the primary list when low-noise is empty is handled at the call site.
+ """
+ for lora_field in loras:
+ lora_info = context.models.load(lora_field.lora)
+ assert isinstance(lora_info.model, ModelPatchRaw), (
+ f"Wan LoRA model must be ModelPatchRaw, got {type(lora_info.model).__name__}"
+ )
+ yield (lora_info.model, lora_field.weight)
+ del lora_info
diff --git a/invokeai/app/invocations/wan_ideal_dimensions.py b/invokeai/app/invocations/wan_ideal_dimensions.py
new file mode 100644
index 00000000000..732a9c2b4f4
--- /dev/null
+++ b/invokeai/app/invocations/wan_ideal_dimensions.py
@@ -0,0 +1,209 @@
+"""Compute Wan 2.2-compatible pixel dimensions for a target short-side resolution.
+
+Wan's transformer ``patch_size=(1, 2, 2)`` adds a 2x patchify on top of the VAE's
+spatial compression, so pixel dimensions must be a multiple of (2 × VAE scale):
+
+- I2V-A14B / T2V (8x VAE) → multiples of 16
+- TI2V-5B (Wan 2.2-VAE, 16x VAE) → multiples of 32
+
+This module exposes one node per family. Each takes a source image's W×H and a
+target short-side preset (480p / 720p / 1080p) and returns the scaled, snapped
+(width, height) that can be fed directly into the matching ``wan_ref_image_encoder``
+/ ``wan_denoise`` inputs. Both nodes share :func:`_scale_and_snap`; they differ
+only in the pixel multiple they snap to.
+"""
+
+import math
+from typing import Literal
+
+from invokeai.app.invocations.baseinvocation import BaseInvocation, Classification, invocation
+from invokeai.app.invocations.fields import InputField
+from invokeai.app.invocations.ideal_size import IdealSizeOutput
+from invokeai.app.services.shared.invocation_context import InvocationContext
+
+WanTargetResolution = Literal["480p", "720p", "1080p"]
+WanRounding = Literal["nearest", "floor", "ceiling"]
+
+# Pixel-grid multiple = 2 (transformer patch) × VAE spatial scale. The 8x-VAE
+# I2V/T2V models need multiples of 16; the 16x-VAE TI2V-5B needs multiples of 32.
+WAN_I2V_PIXEL_MULTIPLE = 16
+WAN_TI2V_PIXEL_MULTIPLE = 32
+
+# Short-side pixel count for each preset. "p" notation is by convention the *short*
+# dimension in modern video (so a portrait 720p video is 720 wide × 1280 tall).
+WAN_TARGET_RESOLUTION_PX: dict[str, int] = {
+ "480p": 480,
+ "720p": 720,
+ "1080p": 1080,
+}
+
+WAN_TARGET_RESOLUTION_LABELS: dict[str, str] = {
+ "480p": "480p (Wan native)",
+ "720p": "720p (Wan native, default)",
+ "1080p": "1080p (extrapolated — not a Wan training size)",
+}
+
+
+def _scale_and_snap(
+ width: int,
+ height: int,
+ target_short_side: int,
+ rounding: WanRounding,
+ multiple: int,
+) -> tuple[int, int]:
+ """Scale a source W×H so its shorter side equals ``target_short_side``, then
+ snap each dimension to ``multiple`` using the requested rounding mode.
+
+ ``multiple`` is the Wan pixel-grid constraint (16 for the 8x-VAE I2V/T2V
+ models, 32 for the 16x-VAE TI2V-5B). Shared by both ideal-dimensions nodes.
+ """
+ short = min(width, height)
+ if short <= 0:
+ raise ValueError("Source dimensions must be positive.")
+
+ # Reject sources so narrow that the scaled long side is still under one Wan
+ # pixel grid. The downstream clamp to ``max(w, multiple)`` would otherwise
+ # silently return multiple×multiple, which has no relation to the requested
+ # aspect ratio — better to fail fast and have the workflow author fix inputs.
+ long_side = max(width, height)
+ if long_side < multiple:
+ raise ValueError(
+ f"Source longer side ({long_side}px) is smaller than the Wan pixel grid ({multiple}px). "
+ f"Use an input image at least {multiple}px on its longer side."
+ )
+
+ scale = target_short_side / short
+ raw_w = width * scale
+ raw_h = height * scale
+
+ if rounding == "floor":
+ w = int(raw_w // multiple) * multiple
+ h = int(raw_h // multiple) * multiple
+ elif rounding == "ceiling":
+ w = int(math.ceil(raw_w / multiple)) * multiple
+ h = int(math.ceil(raw_h / multiple)) * multiple
+ else: # nearest
+ w = round(raw_w / multiple) * multiple
+ h = round(raw_h / multiple) * multiple
+
+ # Belt-and-suspenders clamp against floor-of- IdealSizeOutput:
+ target_short_side = WAN_TARGET_RESOLUTION_PX[self.target_resolution]
+ w, h = _scale_and_snap(
+ self.width, self.height, target_short_side, self.rounding, multiple=WAN_I2V_PIXEL_MULTIPLE
+ )
+ return IdealSizeOutput(width=w, height=h)
+
+
+@invocation(
+ "wan_ti2v_ideal_dimensions",
+ title="Wan 2.2 TI2V Ideal Dimensions (5B)",
+ tags=["wan", "video", "dimensions", "math"],
+ category="video",
+ version="1.0.0",
+ classification=Classification.Prototype,
+)
+class WanTI2VIdealDimensionsInvocation(BaseInvocation):
+ """Ideal dimensions for the Wan 2.2 TI2V-5B model.
+
+ Use this node for TI2V-5B only. For the A14B models (I2V-A14B / T2V-A14B) use
+ "Wan 2.2 I2V Ideal Dimensions" instead — those need multiples of 16, and this
+ node's multiples-of-32 dims would overshoot their pixel grid.
+
+ Identical to the A14B node but snaps each dimension to a multiple of 32 instead
+ of 16: the Wan 2.2-VAE used by TI2V-5B applies 16x spatial compression and the
+ transformer adds a 2x patch on top, so pixel dims must divide by 32 for the
+ patchify step. Wire from ``Image Primitive``'s width/height outputs and into
+ the matching ``wan_denoise`` inputs.
+ """
+
+ width: int = InputField(
+ default=1024,
+ gt=0,
+ description="Source image width in pixels.",
+ )
+ height: int = InputField(
+ default=1024,
+ gt=0,
+ description="Source image height in pixels.",
+ )
+ target_resolution: WanTargetResolution = InputField(
+ default="720p",
+ description=(
+ "Short-side resolution preset. 480p and 720p are Wan 2.2's native training "
+ "resolutions; 1080p works but is extrapolation and costs ~2.25x the memory "
+ "of 720p."
+ ),
+ ui_choice_labels=WAN_TARGET_RESOLUTION_LABELS,
+ )
+ rounding: WanRounding = InputField(
+ default="nearest",
+ description=(
+ "How to snap each dimension to a multiple of 32. 'floor' rounds down — "
+ "safest for VRAM, guaranteed not to exceed the unsnapped target. "
+ "'ceiling' rounds up. 'nearest' minimizes aspect-ratio drift (default)."
+ ),
+ )
+
+ def invoke(self, context: InvocationContext) -> IdealSizeOutput:
+ target_short_side = WAN_TARGET_RESOLUTION_PX[self.target_resolution]
+ w, h = _scale_and_snap(
+ self.width, self.height, target_short_side, self.rounding, multiple=WAN_TI2V_PIXEL_MULTIPLE
+ )
+ return IdealSizeOutput(width=w, height=h)
diff --git a/invokeai/app/invocations/wan_image_to_latents.py b/invokeai/app/invocations/wan_image_to_latents.py
new file mode 100644
index 00000000000..22543205af8
--- /dev/null
+++ b/invokeai/app/invocations/wan_image_to_latents.py
@@ -0,0 +1,106 @@
+"""Wan 2.2 image-to-latents invocation.
+
+Encodes an image to latent space using the Wan VAE (AutoencoderKLWan). The Wan
+VAE expects 5D ``[B, C, T, H, W]`` input with ``T=1`` for single images. After
+encoding, latents are normalised against the per-channel ``latents_mean`` and
+``latents_std`` stored in the VAE config — this matches the Diffusers
+``WanPipeline`` reference and is the inverse of the denormalisation in
+``wan_latents_to_image.py``.
+"""
+
+import einops
+import torch
+from diffusers.models.autoencoders import AutoencoderKLWan
+
+from invokeai.app.invocations.baseinvocation import BaseInvocation, Classification, invocation
+from invokeai.app.invocations.fields import (
+ FieldDescriptions,
+ ImageField,
+ Input,
+ InputField,
+ WithBoard,
+ WithMetadata,
+)
+from invokeai.app.invocations.model import VAEField
+from invokeai.app.invocations.primitives import LatentsOutput
+from invokeai.app.services.shared.invocation_context import InvocationContext
+from invokeai.backend.model_manager.load.load_base import LoadedModel
+from invokeai.backend.model_manager.load.model_cache.utils import get_effective_device
+from invokeai.backend.stable_diffusion.diffusers_pipeline import image_resized_to_grid_as_tensor
+from invokeai.backend.util.vae_working_memory import estimate_vae_working_memory_wan
+
+
+@invocation(
+ "wan_i2l",
+ title="Image to Latents - Wan 2.2",
+ tags=["image", "latents", "vae", "i2l", "wan"],
+ category="image",
+ version="1.0.0",
+ classification=Classification.Prototype,
+)
+class WanImageToLatentsInvocation(BaseInvocation, WithMetadata, WithBoard):
+ """Encodes an image with the Wan VAE (AutoencoderKLWan).
+
+ The output latents have the temporal dimension squeezed out, so downstream
+ nodes see 4D ``[B, C, H, W]``. The denoise loop re-adds ``T=1`` before
+ feeding the transformer.
+ """
+
+ image: ImageField = InputField(description="The image to encode.")
+ vae: VAEField = InputField(description=FieldDescriptions.vae, input=Input.Connection)
+
+ @staticmethod
+ def vae_encode(vae_info: LoadedModel, image_tensor: torch.Tensor) -> torch.Tensor:
+ if not isinstance(vae_info.model, AutoencoderKLWan):
+ raise TypeError(f"Expected AutoencoderKLWan for Wan VAE, got {type(vae_info.model).__name__}.")
+
+ estimated_working_memory = estimate_vae_working_memory_wan(
+ operation="encode",
+ vae=vae_info.model,
+ pixel_height=image_tensor.shape[-2],
+ pixel_width=image_tensor.shape[-1],
+ pixel_frames=image_tensor.shape[2] if image_tensor.ndim == 5 else 1,
+ )
+
+ with vae_info.model_on_device(working_mem_bytes=estimated_working_memory) as (_, vae):
+ assert isinstance(vae, AutoencoderKLWan)
+
+ vae_dtype = next(iter(vae.parameters())).dtype
+ image_tensor = image_tensor.to(device=get_effective_device(vae), dtype=vae_dtype)
+
+ with torch.inference_mode():
+ # Wan VAE expects 5D [B, C, T, H, W].
+ if image_tensor.ndim == 4:
+ image_tensor = image_tensor.unsqueeze(2) # [B, C, H, W] -> [B, C, 1, H, W]
+
+ encoded = vae.encode(image_tensor, return_dict=False)[0]
+ latents = encoded.sample().to(dtype=vae_dtype)
+
+ # Normalise to the denoiser's expected zero-centred space:
+ # (latents - mean) / std
+ latents_mean = torch.tensor(vae.config.latents_mean).view(1, -1, 1, 1, 1).to(latents)
+ latents_std = torch.tensor(vae.config.latents_std).view(1, -1, 1, 1, 1).to(latents)
+ latents = (latents - latents_mean) / latents_std
+
+ # Drop the temporal dim to keep the rest of the InvokeAI pipeline 4D.
+ if latents.ndim == 5:
+ latents = latents.squeeze(2)
+
+ return latents
+
+ @torch.no_grad()
+ def invoke(self, context: InvocationContext) -> LatentsOutput:
+ image = context.images.get_pil(self.image.image_name)
+
+ image_tensor = image_resized_to_grid_as_tensor(image.convert("RGB"))
+ if image_tensor.dim() == 3:
+ image_tensor = einops.rearrange(image_tensor, "c h w -> 1 c h w")
+
+ vae_info = context.models.load(self.vae.vae)
+
+ context.util.signal_progress("Running Wan VAE encode")
+ latents = self.vae_encode(vae_info=vae_info, image_tensor=image_tensor)
+
+ latents = latents.to("cpu")
+ name = context.tensors.save(tensor=latents)
+ return LatentsOutput.build(latents_name=name, latents=latents, seed=None)
diff --git a/invokeai/app/invocations/wan_latents_to_image.py b/invokeai/app/invocations/wan_latents_to_image.py
new file mode 100644
index 00000000000..921c5b7ae54
--- /dev/null
+++ b/invokeai/app/invocations/wan_latents_to_image.py
@@ -0,0 +1,119 @@
+"""Wan 2.2 latents-to-image invocation.
+
+Decodes Wan latents using the Wan VAE (AutoencoderKLWan).
+
+Latents from the denoise loop are in normalised space (zero-centred). Before
+VAE decode they are denormalised using the VAE config's per-channel
+``latents_mean`` / ``latents_std`` (matching Diffusers ``WanPipeline``).
+
+The VAE expects 5D ``[B, C, T, H, W]``; downstream nodes work with 4D, so this
+node re-adds ``T=1`` before decode and squeezes it back out afterwards.
+"""
+
+import torch
+from diffusers.models.autoencoders import AutoencoderKLWan
+from einops import rearrange
+from PIL import Image
+
+from invokeai.app.invocations.baseinvocation import BaseInvocation, Classification, invocation
+from invokeai.app.invocations.fields import (
+ FieldDescriptions,
+ Input,
+ InputField,
+ LatentsField,
+ WithBoard,
+ WithMetadata,
+)
+from invokeai.app.invocations.model import VAEField
+from invokeai.app.invocations.primitives import ImageOutput
+from invokeai.app.services.shared.invocation_context import InvocationContext
+from invokeai.backend.model_manager.load.model_cache.utils import get_effective_device
+from invokeai.backend.util.devices import TorchDevice
+from invokeai.backend.util.vae_working_memory import estimate_vae_working_memory_wan
+
+
+@invocation(
+ "wan_l2i",
+ title="Latents to Image - Wan 2.2",
+ tags=["latents", "image", "vae", "l2i", "wan"],
+ category="latents",
+ version="1.0.0",
+ classification=Classification.Prototype,
+)
+class WanLatentsToImageInvocation(BaseInvocation, WithMetadata, WithBoard):
+ """Decodes Wan latents back to RGB."""
+
+ latents: LatentsField = InputField(description=FieldDescriptions.latents, input=Input.Connection)
+ vae: VAEField = InputField(description=FieldDescriptions.vae, input=Input.Connection)
+
+ @torch.no_grad()
+ def invoke(self, context: InvocationContext) -> ImageOutput:
+ latents = context.tensors.load(self.latents.latents_name)
+
+ if latents.ndim not in (4, 5):
+ raise ValueError(
+ f"Wan latents-to-image expects a 4D or 5D latent tensor [B, C, (T), H, W]; got {tuple(latents.shape)}."
+ )
+
+ # This node decodes exactly one image. Multi-frame video latents would otherwise
+ # run the full (expensive) multi-frame VAE decode — under a working-memory
+ # estimate that assumed one frame — and then die in an opaque einops rank error
+ # at the final rearrange. Checked before the VAE is even loaded.
+ if latents.ndim == 5 and latents.shape[2] != 1:
+ raise ValueError(
+ f"These latents hold {latents.shape[2]} frames of video; this node decodes a single "
+ "image. Use 'Latents to Video - Wan 2.2' (wan_l2v) for video latents."
+ )
+
+ vae_info = context.models.load(self.vae.vae)
+ if not isinstance(vae_info.model, AutoencoderKLWan):
+ raise TypeError(f"Expected AutoencoderKLWan for Wan VAE, got {type(vae_info.model).__name__}.")
+
+ spatial_scale = getattr(vae_info.model.config, "scale_factor_spatial", None) or 8
+ estimated_working_memory = estimate_vae_working_memory_wan(
+ operation="decode",
+ vae=vae_info.model,
+ pixel_height=latents.shape[-2] * spatial_scale,
+ pixel_width=latents.shape[-1] * spatial_scale,
+ pixel_frames=1,
+ )
+
+ with vae_info.model_on_device(working_mem_bytes=estimated_working_memory) as (_, vae):
+ context.util.signal_progress("Running Wan VAE decode")
+ assert isinstance(vae, AutoencoderKLWan)
+
+ vae_dtype = next(iter(vae.parameters())).dtype
+ latents = latents.to(device=get_effective_device(vae), dtype=vae_dtype)
+
+ TorchDevice.empty_cache()
+
+ with torch.inference_mode():
+ # Re-add the temporal dim if upstream squeezed it out.
+ if latents.ndim == 4:
+ latents = latents.unsqueeze(2)
+
+ if latents.shape[1] != vae.config.z_dim:
+ raise ValueError(
+ f"Latent channel mismatch: these latents have {latents.shape[1]} channels but the "
+ f"selected VAE expects {vae.config.z_dim}. A14B models need the 16-channel Wan 2.1 "
+ "VAE; TI2V-5B needs the 48-channel Wan 2.2 VAE."
+ )
+
+ # Denormalise from denoiser space back to raw VAE space.
+ latents_mean = torch.tensor(vae.config.latents_mean).view(1, -1, 1, 1, 1).to(latents)
+ latents_std = torch.tensor(vae.config.latents_std).view(1, -1, 1, 1, 1).to(latents)
+ latents = latents * latents_std + latents_mean
+
+ decoded = vae.decode(latents, return_dict=False)[0]
+
+ if decoded.ndim == 5:
+ decoded = decoded.squeeze(2)
+
+ img = decoded.clamp(-1, 1)
+ img = rearrange(img[0], "c h w -> h w c")
+ img_pil = Image.fromarray((127.5 * (img + 1.0)).byte().cpu().numpy())
+
+ TorchDevice.empty_cache()
+
+ image_dto = context.images.save(image=img_pil)
+ return ImageOutput.build(image_dto)
diff --git a/invokeai/app/invocations/wan_latents_to_video.py b/invokeai/app/invocations/wan_latents_to_video.py
new file mode 100644
index 00000000000..8f42f37e6d1
--- /dev/null
+++ b/invokeai/app/invocations/wan_latents_to_video.py
@@ -0,0 +1,228 @@
+"""Wan 2.2 latents-to-video invocation.
+
+Decodes multi-frame Wan latents with the Wan VAE and encodes the result to an
+MP4 file via :mod:`imageio` (backed by the bundled FFmpeg binary from
+``imageio-ffmpeg``). The video is then persisted through ``context.videos.save``,
+which moves the temp file into ``outputs/videos/`` and records the DTO.
+
+Latent shape on input is 5D ``[B, C, T_lat, H_lat, W_lat]`` (typically B=1).
+The VAE expands the temporal dim by 4× during decode minus the initial offset:
+``T_pixel = (T_lat - 1) * 4 + 1`` (e.g. T_lat=21 → 81 pixel frames).
+"""
+
+import tempfile
+from collections.abc import Iterable, Iterator
+from pathlib import Path
+from typing import Callable, Protocol
+
+import numpy as np
+import torch
+from diffusers.models.autoencoders import AutoencoderKLWan
+
+from invokeai.app.invocations.baseinvocation import BaseInvocation, Classification, invocation
+from invokeai.app.invocations.fields import (
+ FieldDescriptions,
+ Input,
+ InputField,
+ LatentsField,
+ WithBoard,
+ WithMetadata,
+)
+from invokeai.app.invocations.model import VAEField
+from invokeai.app.invocations.primitives import VideoOutput
+from invokeai.app.services.session_processor.session_processor_common import CanceledException
+from invokeai.app.services.shared.invocation_context import InvocationContext
+from invokeai.app.util.video_encoding import make_mp4_writer
+from invokeai.backend.model_manager.load.model_cache.utils import get_effective_device
+from invokeai.backend.util.devices import TorchDevice
+from invokeai.backend.util.vae_working_memory import estimate_vae_working_memory_wan
+
+
+class _FrameWriter(Protocol):
+ def append_data(self, frame: np.ndarray) -> None: ...
+
+
+def _validate_video_latent_batch(latents: torch.Tensor) -> None:
+ if latents.ndim in (4, 5) and latents.shape[0] != 1:
+ raise ValueError(f"Wan latents-to-video requires batch size 1; got {latents.shape[0]}.")
+
+
+def _iter_decoded_frames(decoded: torch.Tensor) -> Iterator[np.ndarray]:
+ for index in range(decoded.shape[1]):
+ frame = decoded[:, index]
+ frame = frame.clamp(-1, 1).permute(1, 2, 0).cpu().float()
+ yield (127.5 * (frame + 1.0)).round().clamp(0, 255).byte().numpy()
+
+
+def _write_video_frames(writer: _FrameWriter, frames: Iterable[np.ndarray], is_canceled: Callable[[], bool]) -> None:
+ frames_iter = iter(frames)
+ while True:
+ if is_canceled():
+ raise CanceledException
+ try:
+ frame = next(frames_iter)
+ except StopIteration:
+ return
+ writer.append_data(np.ascontiguousarray(frame))
+
+
+@invocation(
+ "wan_l2v",
+ title="Latents to Video - Wan 2.2",
+ tags=["latents", "video", "vae", "l2v", "wan"],
+ category="latents",
+ version="1.0.0",
+ classification=Classification.Prototype,
+)
+class WanLatentsToVideoInvocation(BaseInvocation, WithMetadata, WithBoard):
+ """Decode 5D Wan latents to RGB frames and encode an MP4."""
+
+ latents: LatentsField = InputField(description=FieldDescriptions.latents, input=Input.Connection)
+ vae: VAEField = InputField(description=FieldDescriptions.vae, input=Input.Connection)
+ fps: int = InputField(
+ default=16,
+ ge=1,
+ le=120,
+ description="Frames-per-second for the encoded MP4. Wan 2.2 was trained at 16 FPS.",
+ )
+
+ @torch.no_grad()
+ def invoke(self, context: InvocationContext) -> VideoOutput:
+ latents = context.tensors.load(self.latents.latents_name)
+ _validate_video_latent_batch(latents)
+ if latents.ndim == 4:
+ # Promote 4D (single-frame) to 5D so this node can also serve as a
+ # one-frame "video" encode if someone wires it that way.
+ latents = latents.unsqueeze(2)
+ if latents.ndim != 5:
+ raise ValueError(
+ f"Wan latents-to-video expects a 5D latent tensor [B, C, T, H, W]; got {tuple(latents.shape)}."
+ )
+
+ vae_info = context.models.load(self.vae.vae)
+ if not isinstance(vae_info.model, AutoencoderKLWan):
+ raise TypeError(f"Expected AutoencoderKLWan for Wan VAE, got {type(vae_info.model).__name__}.")
+
+ if latents.shape[1] != vae_info.model.config.z_dim:
+ raise ValueError(
+ f"Latent channel mismatch: these latents have {latents.shape[1]} channels but the "
+ f"selected VAE expects {vae_info.model.config.z_dim}. A14B models need the 16-channel Wan 2.1 VAE; "
+ "TI2V-5B needs the 48-channel Wan 2.2 VAE."
+ )
+
+ _, _, t_lat, h_lat, w_lat = latents.shape
+ spatial_scale = getattr(vae_info.model.config, "scale_factor_spatial", None) or 8
+ temporal_scale = getattr(vae_info.model.config, "scale_factor_temporal", None) or 4
+ t_pixel = (t_lat - 1) * temporal_scale + 1
+ h_pixel, w_pixel = h_lat * spatial_scale, w_lat * spatial_scale
+
+ estimated_working_memory = estimate_vae_working_memory_wan(
+ operation="decode",
+ vae=vae_info.model,
+ pixel_height=h_pixel,
+ pixel_width=w_pixel,
+ pixel_frames=t_pixel,
+ )
+ # Long/high-res clips can need a working set no card fits. When the full-frame
+ # estimate exceeds the execution device's total VRAM, fall back to spatial tiling
+ # and budget for the tiled working set instead. (A cpu_only VAE runs in system
+ # RAM, where the working set is not the constraint.)
+ use_tiling = False
+ if not getattr(vae_info.config, "cpu_only", None):
+ exec_device = TorchDevice.choose_torch_device()
+ if exec_device.type == "cuda":
+ total_vram = torch.cuda.get_device_properties(exec_device).total_memory
+ if estimated_working_memory > 0.9 * total_vram:
+ use_tiling = True
+ tile_size = int(getattr(vae_info.model, "tile_sample_min_height", 256))
+ estimated_working_memory = estimate_vae_working_memory_wan(
+ operation="decode",
+ vae=vae_info.model,
+ pixel_height=h_pixel,
+ pixel_width=w_pixel,
+ pixel_frames=t_pixel,
+ tile_size=tile_size,
+ )
+
+ with vae_info.model_on_device(working_mem_bytes=estimated_working_memory) as (_, vae):
+ assert isinstance(vae, AutoencoderKLWan)
+ context.logger.info(
+ f"Running Wan VAE decode: {t_lat} latent frames -> {t_pixel} pixel frames at {w_pixel}x{h_pixel}"
+ + (" (tiled)" if use_tiling else "")
+ )
+ context.util.signal_progress("Running Wan VAE decode (video)")
+
+ vae_dtype = next(iter(vae.parameters())).dtype
+ latents = latents.to(device=get_effective_device(vae), dtype=vae_dtype)
+
+ TorchDevice.empty_cache()
+
+ if use_tiling:
+ vae.enable_tiling()
+ try:
+ with torch.inference_mode():
+ # Denormalise from denoiser space back to VAE space.
+ latents_mean = torch.tensor(vae.config.latents_mean).view(1, -1, 1, 1, 1).to(latents)
+ latents_std = torch.tensor(vae.config.latents_std).view(1, -1, 1, 1, 1).to(latents)
+ latents = latents * latents_std + latents_mean
+
+ # [B, C=3, T_pixel, H, W] in [-1, 1] (roughly).
+ decoded = vae.decode(latents, return_dict=False)[0]
+ del latents, latents_mean, latents_std
+ finally:
+ if use_tiling:
+ # The VAE instance is cached and shared; don't leak tiling into other nodes.
+ vae.disable_tiling()
+
+ # Take batch 0 (we generate one video at a time) and move the clip off the
+ # accelerator now — MP4 encoding can take a while, and holding the full
+ # decoded clip in VRAM for its duration starves the next node's model load.
+ decoded = decoded[0].cpu() # [C, T, H, W]
+
+ TorchDevice.empty_cache()
+
+ if context.util.is_canceled():
+ raise CanceledException
+
+ num_frames = decoded.shape[1]
+ if num_frames == 0:
+ raise ValueError("Wan VAE decode produced zero frames.")
+
+ height, width = decoded.shape[2:]
+ duration = num_frames / float(self.fps)
+
+ # Encode to a temporary MP4 (libx264 + yuv420p, exact frame dimensions —
+ # see make_mp4_writer for why macro_block_size matters).
+ tmp = tempfile.NamedTemporaryFile(prefix="invokeai_wan_video_", suffix=".mp4", delete=False)
+ tmp.close()
+ tmp_path = Path(tmp.name)
+ try:
+ context.logger.info(
+ f"Encoding MP4: {num_frames} frames @ {self.fps} fps ({duration:.2f}s) at {width}x{height} via libx264"
+ )
+ context.util.signal_progress(f"Encoding MP4 ({num_frames} frames @ {self.fps} fps)")
+ writer = make_mp4_writer(tmp_path, self.fps)
+ try:
+ _write_video_frames(writer, _iter_decoded_frames(decoded), context.util.is_canceled)
+ finally:
+ writer.close()
+ del decoded
+ TorchDevice.empty_cache()
+ encoded_bytes = tmp_path.stat().st_size
+ context.logger.info(f"MP4 encode complete: {encoded_bytes / 1024:.1f} KB")
+ video_dto = context.videos.save(
+ source_path=tmp_path,
+ width=width,
+ height=height,
+ duration=duration,
+ fps=float(self.fps),
+ )
+ context.logger.info(f"Saved video: {video_dto.video_name}")
+ return VideoOutput.build(video_dto)
+ finally:
+ # If save() moved the file this is a no-op; if it failed earlier, we
+ # don't want a lingering temp file.
+ try:
+ tmp_path.unlink(missing_ok=True)
+ except Exception:
+ pass
diff --git a/invokeai/app/invocations/wan_lora_loader.py b/invokeai/app/invocations/wan_lora_loader.py
new file mode 100644
index 00000000000..10f7593848e
--- /dev/null
+++ b/invokeai/app/invocations/wan_lora_loader.py
@@ -0,0 +1,265 @@
+from typing import Literal, Optional
+
+from invokeai.app.invocations.baseinvocation import (
+ BaseInvocation,
+ BaseInvocationOutput,
+ Classification,
+ invocation,
+ invocation_output,
+)
+from invokeai.app.invocations.fields import FieldDescriptions, Input, InputField, OutputField
+from invokeai.app.invocations.model import LoRAField, ModelIdentifierField, WanTransformerField
+from invokeai.app.services.shared.invocation_context import InvocationContext
+from invokeai.backend.model_manager.taxonomy import (
+ BaseModelType,
+ ModelType,
+ WanLoRAVariantType,
+ WanVariantType,
+)
+
+# Target option for routing a LoRA to one or both Wan A14B expert lists.
+#
+# - ``auto``: read the LoRA config's ``expert`` field (set by the probe / from
+# filename). ``"high"`` -> primary list only, ``"low"`` -> low-noise list
+# only, ``None`` -> both lists.
+# - ``both``: append to both lists regardless of the config.
+# - ``high``: append only to the primary list (high-noise expert).
+# - ``low``: append only to the low-noise list (low-noise expert).
+WanLoRATarget = Literal["auto", "both", "high", "low"]
+
+
+def _assert_is_wan_lora(lora_config: object, lora_key: str) -> None:
+ """Reject an identifier whose *resolved* config is not a Wan LoRA.
+
+ The identifier's own ``base``/``type`` fields are client-supplied and cannot be
+ trusted: a hand-authored workflow can label any existing model key as a Wan LoRA
+ and reach model patching (or fail minutes in, after expensive loading). Only the
+ config the key actually resolves to is authoritative.
+ """
+ config_type = getattr(lora_config, "type", None)
+ config_base = getattr(lora_config, "base", None)
+ if config_type is not ModelType.LoRA or config_base is not BaseModelType.Wan:
+ raise ValueError(
+ f"Model '{lora_key}' is not a Wan LoRA (resolved to "
+ f"type={getattr(config_type, 'value', config_type)}, "
+ f"base={getattr(config_base, 'value', config_base)})."
+ )
+
+
+def _assert_lora_variant_matches_main(lora_config: object, main_config: object, lora_key: str) -> None:
+ """Reject an A14B LoRA wired against a 5B main (and vice versa).
+
+ A mismatch otherwise crashes deep in the layer patcher mid-denoise with an opaque
+ tensor-shape error, after minutes of model loading. Skips silently when either
+ variant is unrecorded (e.g. a LoRA whose targeted layers don't pin the inner dim).
+ """
+ lora_variant = getattr(lora_config, "variant", None)
+ main_variant = getattr(main_config, "variant", None)
+ if lora_variant is None or main_variant is None:
+ return
+ lora_is_5b = lora_variant == WanLoRAVariantType.Wan5B
+ main_is_5b = main_variant == WanVariantType.TI2V_5B
+ if lora_is_5b != main_is_5b:
+ raise ValueError(
+ f"LoRA '{lora_key}' targets Wan {lora_variant.value.upper()} models, but the "
+ f"transformer is a {main_variant.value} model. A14B and 5B LoRAs are not interchangeable."
+ )
+
+
+def _warn_if_low_routing_is_inert(
+ context: InvocationContext, main_config: object, lora_key: str, to_primary: bool, to_low_noise: bool
+) -> None:
+ """Warn when a LoRA is routed only to the low-noise list of a TI2V-5B main.
+
+ The single-transformer TI2V-5B denoise path consumes only the primary list, so
+ such a LoRA silently has no effect — the node would otherwise report success
+ while doing nothing.
+ """
+ if to_primary or not to_low_noise:
+ return
+ if getattr(main_config, "variant", None) == WanVariantType.TI2V_5B:
+ context.logger.warning(
+ f"LoRA '{lora_key}' is routed only to the low-noise expert, which the single-transformer "
+ "TI2V-5B variant never uses — the LoRA will have no effect."
+ )
+
+
+def _resolve_target(target: WanLoRATarget, lora_expert: str | None) -> tuple[bool, bool]:
+ """Return (apply_to_primary, apply_to_low_noise) based on the requested
+ target and the LoRA's recorded expert tag."""
+ if target == "both":
+ return True, True
+ if target == "high":
+ return True, False
+ if target == "low":
+ return False, True
+ # auto
+ if lora_expert == "high":
+ return True, False
+ if lora_expert == "low":
+ return False, True
+ return True, True
+
+
+@invocation_output("wan_lora_loader_output")
+class WanLoRALoaderOutput(BaseInvocationOutput):
+ """Wan 2.2 LoRA loader output."""
+
+ transformer: Optional[WanTransformerField] = OutputField(
+ default=None, description=FieldDescriptions.transformer, title="Wan Transformer"
+ )
+
+
+@invocation(
+ "wan_lora_loader",
+ title="Apply LoRA - Wan 2.2",
+ tags=["lora", "model", "wan"],
+ category="model",
+ version="1.0.1",
+ classification=Classification.Prototype,
+)
+class WanLoRALoaderInvocation(BaseInvocation):
+ """Apply a LoRA to the Wan 2.2 transformer(s).
+
+ For A14B (dual expert) the LoRA's recorded ``expert`` field determines
+ which expert list it lands in: ``"high"`` -> primary list, ``"low"`` ->
+ low-noise list, ``None`` (untagged) -> both lists. Use the ``target``
+ field to override.
+
+ For TI2V-5B (single transformer) only the primary list is used at denoise
+ time; a LoRA routed only to the low-noise list would be inert, so that
+ routing logs a warning.
+ """
+
+ lora: ModelIdentifierField = InputField(
+ description=FieldDescriptions.lora_model,
+ title="LoRA",
+ ui_model_base=BaseModelType.Wan,
+ ui_model_type=ModelType.LoRA,
+ )
+ weight: float = InputField(default=0.75, description=FieldDescriptions.lora_weight)
+ target: WanLoRATarget = InputField(
+ default="auto",
+ description="Which expert(s) to apply this LoRA to. 'auto' uses the LoRA's "
+ "recorded expert tag (or both if untagged); 'both'/'high'/'low' override it.",
+ )
+ transformer: WanTransformerField | None = InputField(
+ default=None,
+ description=FieldDescriptions.transformer,
+ input=Input.Connection,
+ title="Wan Transformer",
+ )
+
+ def invoke(self, context: InvocationContext) -> WanLoRALoaderOutput:
+ lora_key = self.lora.key
+
+ if not context.models.exists(lora_key):
+ raise ValueError(f"Unknown lora: {lora_key}!")
+
+ lora_config = context.models.get_config(self.lora)
+ _assert_is_wan_lora(lora_config, lora_key)
+
+ output = WanLoRALoaderOutput()
+ if self.transformer is None:
+ return output
+
+ main_config = context.models.get_config(self.transformer.transformer)
+ _assert_lora_variant_matches_main(lora_config, main_config, lora_key)
+
+ lora_expert = getattr(lora_config, "expert", None)
+ to_primary, to_low_noise = _resolve_target(self.target, lora_expert)
+ _warn_if_low_routing_is_inert(context, main_config, lora_key, to_primary, to_low_noise)
+
+ # Reject duplicates on whichever list(s) we're about to append to.
+ if to_primary and any(item.lora.key == lora_key for item in self.transformer.loras):
+ raise ValueError(f'LoRA "{lora_key}" already applied to primary transformer list.')
+ if to_low_noise and any(item.lora.key == lora_key for item in self.transformer.loras_low_noise):
+ raise ValueError(f'LoRA "{lora_key}" already applied to low-noise transformer list.')
+
+ output.transformer = self.transformer.model_copy(deep=True)
+ new_lora = LoRAField(lora=self.lora, weight=self.weight)
+ if to_primary:
+ output.transformer.loras.append(new_lora)
+ if to_low_noise:
+ output.transformer.loras_low_noise.append(new_lora)
+
+ return output
+
+
+@invocation(
+ "wan_lora_collection_loader",
+ title="Apply LoRA Collection - Wan 2.2",
+ tags=["lora", "model", "wan"],
+ category="model",
+ version="1.0.1",
+ classification=Classification.Prototype,
+)
+class WanLoRACollectionLoader(BaseInvocation):
+ """Apply a collection of LoRAs to the Wan 2.2 transformer(s).
+
+ Each LoRA is routed to the primary and/or low-noise list based on its
+ recorded ``expert`` tag (set by the probe from the filename). Untagged
+ LoRAs go to both lists.
+ """
+
+ loras: Optional[LoRAField | list[LoRAField]] = InputField(
+ default=None,
+ description="LoRAs to apply. May be a single LoRA or a collection.",
+ title="LoRAs",
+ ui_model_base=[BaseModelType.Wan],
+ ui_model_type=ModelType.LoRA,
+ )
+ transformer: Optional[WanTransformerField] = InputField(
+ default=None,
+ description=FieldDescriptions.transformer,
+ input=Input.Connection,
+ title="Wan Transformer",
+ )
+
+ def invoke(self, context: InvocationContext) -> WanLoRALoaderOutput:
+ output = WanLoRALoaderOutput()
+
+ if self.transformer is None:
+ return output
+
+ output.transformer = self.transformer.model_copy(deep=True)
+
+ if self.loras is None:
+ return output
+
+ loras = self.loras if isinstance(self.loras, list) else [self.loras]
+ added: set[str] = set()
+ main_config = context.models.get_config(self.transformer.transformer)
+
+ for lora in loras:
+ if lora is None or lora.lora.key in added:
+ continue
+
+ lora_key = lora.lora.key
+ if not context.models.exists(lora_key):
+ raise ValueError(f"Unknown lora: {lora_key}!")
+
+ lora_config = context.models.get_config(lora.lora)
+ _assert_is_wan_lora(lora_config, lora_key)
+ _assert_lora_variant_matches_main(lora_config, main_config, lora_key)
+
+ lora_expert = getattr(lora_config, "expert", None)
+ to_primary, to_low_noise = _resolve_target("auto", lora_expert)
+ _warn_if_low_routing_is_inert(context, main_config, lora_key, to_primary, to_low_noise)
+
+ # Reject LoRAs already applied upstream (same invariant the single loader
+ # enforces) — re-appending would silently double the effective weight.
+ # Intra-collection duplicates are skipped via `added` before reaching here.
+ if to_primary and any(item.lora.key == lora_key for item in output.transformer.loras):
+ raise ValueError(f'LoRA "{lora_key}" already applied to primary transformer list.')
+ if to_low_noise and any(item.lora.key == lora_key for item in output.transformer.loras_low_noise):
+ raise ValueError(f'LoRA "{lora_key}" already applied to low-noise transformer list.')
+
+ added.add(lora_key)
+
+ if to_primary:
+ output.transformer.loras.append(lora)
+ if to_low_noise:
+ output.transformer.loras_low_noise.append(lora)
+
+ return output
diff --git a/invokeai/app/invocations/wan_model_loader.py b/invokeai/app/invocations/wan_model_loader.py
new file mode 100644
index 00000000000..f1535b0993b
--- /dev/null
+++ b/invokeai/app/invocations/wan_model_loader.py
@@ -0,0 +1,284 @@
+from typing import Optional
+
+from invokeai.app.invocations.baseinvocation import (
+ BaseInvocation,
+ BaseInvocationOutput,
+ Classification,
+ invocation,
+ invocation_output,
+)
+from invokeai.app.invocations.fields import FieldDescriptions, Input, InputField, OutputField
+from invokeai.app.invocations.model import (
+ ModelIdentifierField,
+ VAEField,
+ WanT5EncoderField,
+ WanTransformerField,
+)
+from invokeai.app.services.shared.invocation_context import InvocationContext
+from invokeai.backend.model_manager.taxonomy import BaseModelType, ModelFormat, ModelType, SubModelType, WanVariantType
+
+
+@invocation_output("wan_model_loader_output")
+class WanModelLoaderOutput(BaseInvocationOutput):
+ """Wan 2.2 model loader output."""
+
+ transformer: WanTransformerField = OutputField(
+ description="Wan transformer (one or two experts depending on the variant)",
+ title="Transformer",
+ )
+ wan_t5_encoder: WanT5EncoderField = OutputField(
+ description=FieldDescriptions.wan_t5_encoder,
+ title="UMT5-XXL Encoder",
+ )
+ vae: VAEField = OutputField(description=FieldDescriptions.vae, title="VAE")
+
+
+@invocation(
+ "wan_model_loader",
+ title="Main Model - Wan 2.2",
+ tags=["model", "wan"],
+ category="model",
+ version="1.0.1",
+ classification=Classification.Prototype,
+)
+class WanModelLoaderInvocation(BaseInvocation):
+ """Loads a Wan 2.2 model, outputting its submodels.
+
+ Components can be mixed and matched, mirroring the Qwen Image loader pattern:
+
+ - Transformer(s):
+ * Diffusers main: emits ``transformer/`` and (for A14B) ``transformer_2/``
+ from the same model record.
+ * GGUF main: emits the single GGUF as the primary transformer; for A14B
+ the second-expert GGUF must be wired to ``Transformer (Low Noise)``.
+ - VAE: standalone Wan VAE > main (if Diffusers) > Component Source (Diffusers).
+ - UMT5-XXL encoder: standalone Wan T5 encoder > main (if Diffusers) >
+ Component Source (Diffusers).
+
+ The Component Source slot lets users supply a Diffusers Wan main model purely
+ for VAE / encoder extraction when the actual transformer is in a single-file
+ format. Together, the standalone VAE + standalone encoder let a GGUF
+ transformer run without a full ~30 GB Diffusers install.
+ """
+
+ model: ModelIdentifierField = InputField(
+ description=FieldDescriptions.wan_model,
+ input=Input.Direct,
+ ui_model_base=BaseModelType.Wan,
+ ui_model_type=ModelType.Main,
+ title="Transformer",
+ )
+
+ transformer_low_noise_model: Optional[ModelIdentifierField] = InputField(
+ default=None,
+ description="Optional second GGUF transformer for the A14B low-noise expert. "
+ "Only relevant when the main model is a single-file GGUF and the variant is A14B; "
+ "ignored when the main is a Diffusers A14B (both experts are pulled from "
+ "transformer/ and transformer_2/ already) or when the variant is TI2V-5B.",
+ input=Input.Direct,
+ ui_model_base=BaseModelType.Wan,
+ ui_model_type=ModelType.Main,
+ ui_model_format=ModelFormat.GGUFQuantized,
+ title="Transformer (Low Noise)",
+ )
+
+ vae_model: Optional[ModelIdentifierField] = InputField(
+ default=None,
+ description="Standalone Wan VAE model. If not set, the VAE is loaded from the main model "
+ "(when in Diffusers format) or from the Component Source.",
+ input=Input.Direct,
+ ui_model_base=BaseModelType.Wan,
+ ui_model_type=ModelType.VAE,
+ title="VAE",
+ )
+
+ wan_t5_encoder_model: Optional[ModelIdentifierField] = InputField(
+ default=None,
+ description="Standalone Wan UMT5-XXL encoder. If not set, the encoder is loaded from the main "
+ "model (when in Diffusers format) or from the Component Source.",
+ input=Input.Direct,
+ ui_model_type=ModelType.WanT5Encoder,
+ title="Wan T5 Encoder",
+ )
+
+ component_source: Optional[ModelIdentifierField] = InputField(
+ default=None,
+ description="Diffusers Wan main model to extract VAE and/or encoder from. "
+ "Use this if you don't have separate VAE/encoder models. "
+ "Ignored for any submodel that is provided separately.",
+ input=Input.Direct,
+ ui_model_base=BaseModelType.Wan,
+ ui_model_type=ModelType.Main,
+ ui_model_format=ModelFormat.Diffusers,
+ title="Component Source (Diffusers)",
+ )
+
+ def invoke(self, context: InvocationContext) -> WanModelLoaderOutput:
+ main_config = context.models.get_config(self.model)
+ main_format = main_config.format
+ main_is_diffusers = main_format == ModelFormat.Diffusers
+ main_is_gguf = main_format == ModelFormat.GGUFQuantized
+ main_variant = getattr(main_config, "variant", None)
+
+ # Resolve transformer + dual-expert wiring + boundary_ratio.
+ #
+ # Diffusers main: transformer/ is the primary, transformer_2/ is the
+ # low-noise expert (A14B only). boundary_ratio comes from the probed
+ # model_index.json.
+ #
+ # GGUF main: the file itself is one expert (high or low). For A14B,
+ # the user wires the other expert to transformer_low_noise_model.
+ # We swap so the *high*-noise expert is always the primary if needed.
+ # boundary_ratio falls back to 0.875 unless a Diffusers component_source
+ # provides a recorded value.
+ boundary_ratio = 0.9 if main_variant == WanVariantType.I2V_A14B else 0.875
+ transformer_low_noise: Optional[ModelIdentifierField] = None
+
+ if main_is_diffusers:
+ transformer = self.model.model_copy(update={"submodel_type": SubModelType.Transformer})
+ if getattr(main_config, "has_dual_expert", False):
+ transformer_low_noise = self.model.model_copy(update={"submodel_type": SubModelType.Transformer2})
+ recorded = getattr(main_config, "boundary_ratio", None)
+ if recorded is not None:
+ boundary_ratio = float(recorded)
+ elif main_is_gguf:
+ primary_expert = getattr(main_config, "expert", "none")
+ primary_id = self.model.model_copy(update={"submodel_type": SubModelType.Transformer})
+
+ if self.transformer_low_noise_model is not None and main_variant == WanVariantType.TI2V_5B:
+ # The field's own docs promise this input is ignored for the
+ # single-expert TI2V-5B — e.g. a leftover wire from an A14B session.
+ context.logger.warning("'Transformer (Low Noise)' is ignored for the single-expert TI2V-5B variant.")
+
+ if self.transformer_low_noise_model is not None and main_variant != WanVariantType.TI2V_5B:
+ low_config = context.models.get_config(self.transformer_low_noise_model)
+ if low_config.format != ModelFormat.GGUFQuantized:
+ raise ValueError(
+ f"'Transformer (Low Noise)' must be a GGUF-format Wan model. "
+ f"'{low_config.name}' is in {low_config.format.value} format."
+ )
+ low_id = self.transformer_low_noise_model.model_copy(update={"submodel_type": SubModelType.Transformer})
+ low_expert = getattr(low_config, "expert", "none")
+
+ if getattr(low_config, "variant", None) != main_variant:
+ raise ValueError("The high-noise and low-noise GGUF models must use the same Wan variant.")
+ if {primary_expert, low_expert} != {"high", "low"}:
+ raise ValueError("A Wan A14B GGUF expert pair must contain one high and one low expert.")
+
+ # Make sure 'transformer' is the high-noise expert and
+ # 'transformer_low_noise' is the low-noise expert. If the user
+ # accidentally swapped them, swap back.
+ if primary_expert == "low" and low_expert == "high":
+ transformer = low_id
+ transformer_low_noise = primary_id
+ else:
+ transformer = primary_id
+ transformer_low_noise = low_id
+ else:
+ transformer = primary_id
+ if main_variant in (WanVariantType.T2V_A14B, WanVariantType.I2V_A14B) and primary_expert != "high":
+ raise ValueError("An unpaired Wan A14B GGUF model must be the high-noise expert.")
+ # A14B without a paired low-noise GGUF will produce degraded
+ # quality (only the high-noise expert runs). Warn but don't
+ # abort — TI2V-5B GGUFs are single-expert and totally fine.
+ if main_variant in (WanVariantType.T2V_A14B, WanVariantType.I2V_A14B):
+ context.logger.warning(
+ "A14B GGUF main was provided without a paired 'Transformer (Low Noise)'. "
+ "Only the high-noise expert will run; image quality will be reduced."
+ )
+
+ # Borrow the boundary_ratio recorded on the optional Diffusers
+ # component_source, when one is wired.
+ if self.component_source is not None:
+ src_cfg = context.models.get_config(self.component_source)
+ src_boundary = getattr(src_cfg, "boundary_ratio", None)
+ if (
+ src_cfg.format == ModelFormat.Diffusers
+ and getattr(src_cfg, "variant", None) == main_variant
+ and src_boundary is not None
+ ):
+ boundary_ratio = float(src_boundary)
+ else:
+ raise ValueError(
+ f"Unsupported main model format for Wan: {main_format.value}. "
+ "Use a Diffusers folder or a GGUF single-file checkpoint."
+ )
+
+ # VAE: standalone override > main (if Diffusers) > component source.
+ if self.vae_model is not None:
+ self._validate_standalone_vae(context, self.vae_model, main_variant)
+ vae = self.vae_model.model_copy(update={"submodel_type": SubModelType.VAE})
+ elif main_is_diffusers:
+ vae = self.model.model_copy(update={"submodel_type": SubModelType.VAE})
+ elif self.component_source is not None:
+ self._validate_component_source_vae(context, self.component_source, main_variant)
+ vae = self.component_source.model_copy(update={"submodel_type": SubModelType.VAE})
+ else:
+ raise ValueError(
+ "No source for VAE. Either set 'VAE' to a standalone Wan VAE, "
+ "or set 'Component Source' to a Diffusers Wan main model."
+ )
+
+ # Tokenizer + text encoder: standalone override > main (if Diffusers) > component source.
+ if self.wan_t5_encoder_model is not None:
+ tokenizer = self.wan_t5_encoder_model.model_copy(update={"submodel_type": SubModelType.Tokenizer})
+ text_encoder = self.wan_t5_encoder_model.model_copy(update={"submodel_type": SubModelType.TextEncoder})
+ elif main_is_diffusers:
+ tokenizer = self.model.model_copy(update={"submodel_type": SubModelType.Tokenizer})
+ text_encoder = self.model.model_copy(update={"submodel_type": SubModelType.TextEncoder})
+ elif self.component_source is not None:
+ self._validate_component_source_format(context, self.component_source)
+ tokenizer = self.component_source.model_copy(update={"submodel_type": SubModelType.Tokenizer})
+ text_encoder = self.component_source.model_copy(update={"submodel_type": SubModelType.TextEncoder})
+ else:
+ raise ValueError(
+ "No source for Wan T5 encoder. "
+ "Either set 'Wan T5 Encoder' to a standalone UMT5-XXL encoder, "
+ "or set 'Component Source' to a Diffusers Wan main model."
+ )
+
+ return WanModelLoaderOutput(
+ transformer=WanTransformerField(
+ transformer=transformer,
+ transformer_low_noise=transformer_low_noise,
+ boundary_ratio=boundary_ratio,
+ ),
+ wan_t5_encoder=WanT5EncoderField(tokenizer=tokenizer, text_encoder=text_encoder),
+ vae=VAEField(vae=vae),
+ )
+
+ @staticmethod
+ def _validate_component_source_format(context: InvocationContext, model: ModelIdentifierField) -> None:
+ source_config = context.models.get_config(model)
+ if source_config.format != ModelFormat.Diffusers:
+ raise ValueError(
+ f"The Component Source model must be in Diffusers format. "
+ f"The selected model '{source_config.name}' is in {source_config.format.value} format."
+ )
+
+ @staticmethod
+ def _validate_component_source_vae(
+ context: InvocationContext, model: ModelIdentifierField, main_variant: WanVariantType
+ ) -> None:
+ WanModelLoaderInvocation._validate_component_source_format(context, model)
+ source_config = context.models.get_config(model)
+ source_variant = getattr(source_config, "variant", None)
+ main_is_ti2v = main_variant == WanVariantType.TI2V_5B
+ source_is_ti2v = source_variant == WanVariantType.TI2V_5B
+ if main_is_ti2v != source_is_ti2v:
+ raise ValueError(
+ "The Component Source VAE is incompatible with the selected transformer. "
+ "TI2V-5B requires the 48-channel Wan 2.2 VAE; A14B models require the 16-channel Wan 2.1 VAE."
+ )
+
+ @staticmethod
+ def _validate_standalone_vae(
+ context: InvocationContext, model: ModelIdentifierField, main_variant: WanVariantType
+ ) -> None:
+ vae_config = context.models.get_config(model)
+ expected_channels = 48 if main_variant == WanVariantType.TI2V_5B else 16
+ if vae_config.latent_channels != expected_channels:
+ raise ValueError(
+ "The standalone VAE is incompatible with the selected transformer. "
+ "TI2V-5B requires the 48-channel Wan 2.2 VAE; A14B models require the 16-channel Wan 2.1 VAE."
+ )
diff --git a/invokeai/app/invocations/wan_ref_image_encoder.py b/invokeai/app/invocations/wan_ref_image_encoder.py
new file mode 100644
index 00000000000..7cf38ccf45c
--- /dev/null
+++ b/invokeai/app/invocations/wan_ref_image_encoder.py
@@ -0,0 +1,207 @@
+"""Reference-image (VAE-latent) encoder for Wan 2.2 image-to-video conditioning.
+
+Wan 2.2 I2V conditions on a reference image by VAE-encoding it. The condition
+shape depends on the wired VAE (which selects the model family):
+
+- **A14B** (16-channel Wan 2.1 VAE): a 20-channel condition tensor (4-ch
+ first-frame mask + 16-ch image latents) that the denoise loop concatenates
+ to the noise latents along the channel dim each step.
+- **TI2V-5B** (48-channel Wan 2.2 VAE): a single-frame 48-channel condition
+ that the denoise loop blends with the noisy latents at every step
+ (expand_timesteps path).
+
+Supports both single-frame (image I2V, ``num_frames=1``) and multi-frame
+(video I2V, e.g. ``num_frames=81``) condition tensors; multi-frame and FLF2V
+(end-image interpolation) are A14B-only.
+"""
+
+from typing import Optional
+
+import torch
+from diffusers.models.autoencoders import AutoencoderKLWan
+
+from invokeai.app.invocations.baseinvocation import BaseInvocation, Classification, invocation
+from invokeai.app.invocations.fields import (
+ FieldDescriptions,
+ ImageField,
+ Input,
+ InputField,
+)
+from invokeai.app.invocations.model import VAEField
+from invokeai.app.invocations.primitives import WanRefImageOutput
+from invokeai.app.services.shared.invocation_context import InvocationContext
+from invokeai.backend.model_manager.load.model_cache.utils import get_effective_device
+from invokeai.backend.util.devices import TorchDevice
+from invokeai.backend.util.vae_working_memory import estimate_vae_working_memory_wan
+from invokeai.backend.wan.extensions.wan_ref_image_extension import (
+ encode_reference_image_to_condition,
+ encode_reference_image_to_ti2v_condition,
+ encode_reference_image_to_video_condition,
+)
+
+
+@invocation(
+ "wan_ref_image_encoder",
+ title="Reference Image - Wan 2.2",
+ tags=["image", "conditioning", "wan", "i2v"],
+ category="conditioning",
+ version="1.2.0",
+ classification=Classification.Prototype,
+)
+class WanRefImageEncoderInvocation(BaseInvocation):
+ """VAE-encode a reference image into Wan 2.2 I2V conditioning.
+
+ The wired VAE selects the output shape. With the 16-channel A14B VAE the
+ output is a ``[1, 20, T_lat, height // 8, width // 8]`` condition (4-ch
+ first-frame mask + 16-ch image latents) that the denoise loop concatenates
+ to the noise latents each step, producing the 36-channel input the
+ I2V-A14B transformer expects. With the 48-channel TI2V-5B VAE the output
+ is a ``[1, 48, 1, height // 16, width // 16]`` condition that the denoise
+ loop blends into the noisy latents at every step.
+
+ For image (single-frame) I2V leave ``num_frames=1`` (T_lat=1). For A14B
+ video I2V set ``num_frames`` to match the value on the video-denoise node
+ (e.g. 81 for the Wan 2.2 reference defaults); TI2V-5B always uses the
+ single-frame condition.
+
+ Supply an optional ``end_image`` for **first-last-frame interpolation
+ (FLF2V)** — the model then interpolates the motion from ``image`` (first
+ frame) to ``end_image`` (final frame). FLF2V is I2V-A14B video only
+ (``num_frames > 1``); it is not supported for TI2V-5B or single-frame I2V.
+
+ For T2V (no reference image), omit this node entirely.
+ """
+
+ image: ImageField = InputField(description="Reference image to condition on (the first frame).")
+ vae: VAEField = InputField(description=FieldDescriptions.vae, input=Input.Connection, title="VAE")
+ # Must match wan_denoise's width/height. multiple_of=16 (not 8) because
+ # Wan's transformer patch_size=(1, 2, 2) needs latent H/W to be even.
+ width: int = InputField(
+ default=1024,
+ multiple_of=16,
+ description="Width to resize the reference image to (must match denoise width).",
+ )
+ height: int = InputField(
+ default=1024,
+ multiple_of=16,
+ description="Height to resize the reference image to (must match denoise height).",
+ )
+ num_frames: int = InputField(
+ default=1,
+ ge=1,
+ description="Pixel-frame count to build the condition for. Use 1 for single-frame image "
+ "I2V. For video I2V, set this to match the video-denoise node's num_frames (and ensure "
+ "(num_frames - 1) %% 4 == 0, e.g. 81).",
+ title="Number of Frames",
+ )
+ end_image: Optional[ImageField] = InputField(
+ default=None,
+ description="Optional end frame for first-last-frame interpolation (FLF2V). When set, the "
+ "video interpolates from the reference image (first frame) to this image (final frame). "
+ "I2V-A14B video only (num_frames > 1); not supported for TI2V-5B or single-frame I2V.",
+ title="End Image (FLF2V)",
+ )
+
+ @torch.no_grad()
+ def invoke(self, context: InvocationContext) -> WanRefImageOutput:
+ if self.num_frames > 1 and (self.num_frames - 1) % 4 != 0:
+ raise ValueError(
+ f"num_frames must satisfy (num_frames - 1) %% 4 == 0 for the Wan VAE's temporal "
+ f"compression (got {self.num_frames}). Try 5, 9, 13, ..., 81, 85, ..."
+ )
+
+ pil_image = context.images.get_pil(self.image.image_name, "RGB")
+ end_pil_image = context.images.get_pil(self.end_image.image_name, "RGB") if self.end_image is not None else None
+
+ vae_info = context.models.load(self.vae.vae)
+ if not isinstance(vae_info.model, AutoencoderKLWan):
+ raise TypeError(f"Reference-image encoder requires AutoencoderKLWan, got {type(vae_info.model).__name__}.")
+
+ estimated_working_memory = estimate_vae_working_memory_wan(
+ operation="encode",
+ vae=vae_info.model,
+ pixel_height=self.height,
+ pixel_width=self.width,
+ pixel_frames=self.num_frames,
+ )
+
+ with vae_info.model_on_device(working_mem_bytes=estimated_working_memory) as (_, vae):
+ assert isinstance(vae, AutoencoderKLWan)
+ # A cpu_only VAE stays in system RAM even when an accelerator is selected —
+ # run the encode where the weights actually live.
+ device = get_effective_device(vae)
+ target_dtype = TorchDevice.choose_bfloat16_safe_dtype(device)
+ context.util.signal_progress(
+ ("VAE-encoding FLF2V start+end images" if end_pil_image is not None else "VAE-encoding reference image")
+ + (f" ({self.num_frames} frames)" if self.num_frames > 1 else "")
+ )
+ # Free cached allocator blocks left over from earlier nodes (denoise expert
+ # swaps in particular can leave the cache fragmented in ways that look like
+ # free VRAM but fail a single large contiguous request). Mirrors the
+ # pattern used in wan_latents_to_image.py / wan_latents_to_video.py.
+ TorchDevice.empty_cache()
+ # Pick the encoder path by VAE z_dim: 48 means the Wan 2.2-VAE (TI2V-5B),
+ # which uses a single-frame 48-channel condition that the denoise loop
+ # blends with the noisy latents at every step (expand_timesteps path).
+ # 16 means the standard Wan VAE (A14B), which uses the 20-channel
+ # mask + latent condition concatenated to noise along the channel dim.
+ is_ti2v_5b = getattr(vae.config, "z_dim", 16) == 48
+ if end_pil_image is not None and (is_ti2v_5b or self.num_frames <= 1):
+ raise ValueError(
+ "End-image (FLF2V) interpolation is only supported for I2V-A14B video "
+ f"(num_frames > 1). Got {'TI2V-5B' if is_ti2v_5b else 'single-frame I2V'}. "
+ "Remove the End Image input, or use an A14B VAE with num_frames > 1."
+ )
+ if is_ti2v_5b:
+ # TI2V-5B I2V needs latent H/W to be even for the transformer
+ # patch_size=(1,2,2), so pixel dims must be multiples of 32
+ # (16x VAE * 2 transformer patch). A14B's 8x VAE only needed
+ # multiples of 16.
+ if self.width % 32 != 0 or self.height % 32 != 0:
+ raise ValueError(
+ f"TI2V-5B I2V requires width and height to be multiples of 32 "
+ f"(got {self.width}x{self.height}). The Wan 2.2-VAE uses 16x "
+ f"spatial compression and the transformer adds a 2x patch on "
+ f"top, so pixel dims must divide by 32 for the patchify step."
+ )
+ condition = encode_reference_image_to_ti2v_condition(
+ image=pil_image,
+ vae=vae,
+ width=self.width,
+ height=self.height,
+ device=device,
+ dtype=target_dtype,
+ )
+ elif self.num_frames <= 1:
+ condition = encode_reference_image_to_condition(
+ image=pil_image,
+ vae=vae,
+ width=self.width,
+ height=self.height,
+ device=device,
+ dtype=target_dtype,
+ )
+ else:
+ condition = encode_reference_image_to_video_condition(
+ image=pil_image,
+ vae=vae,
+ width=self.width,
+ height=self.height,
+ num_frames=self.num_frames,
+ device=device,
+ dtype=target_dtype,
+ last_image=end_pil_image,
+ )
+
+ condition = condition.detach().to("cpu")
+ # Release this node's VAE-encode intermediates before the next node tries to
+ # partial-load the denoise transformer — the OOM we saw in PR #9163 review
+ # was the I2V expert load racing against still-cached encode activations.
+ TorchDevice.empty_cache()
+ name = context.tensors.save(tensor=condition)
+ return WanRefImageOutput.build(
+ condition_tensor_name=name,
+ width=self.width,
+ height=self.height,
+ num_frames=self.num_frames,
+ )
diff --git a/invokeai/app/invocations/wan_text_encoder.py b/invokeai/app/invocations/wan_text_encoder.py
new file mode 100644
index 00000000000..5039230e1a0
--- /dev/null
+++ b/invokeai/app/invocations/wan_text_encoder.py
@@ -0,0 +1,112 @@
+import torch
+
+from invokeai.app.invocations.baseinvocation import BaseInvocation, Classification, invocation
+from invokeai.app.invocations.fields import (
+ FieldDescriptions,
+ Input,
+ InputField,
+ UIComponent,
+)
+from invokeai.app.invocations.model import WanT5EncoderField
+from invokeai.app.invocations.primitives import WanConditioningOutput
+from invokeai.app.services.shared.invocation_context import InvocationContext
+from invokeai.backend.model_manager.load.model_cache.utils import get_effective_device
+from invokeai.backend.stable_diffusion.diffusion.conditioning_data import (
+ ConditioningFieldData,
+ WanConditioningInfo,
+)
+
+# Wan models are trained with 512-token text sequences (matches the
+# upstream config.json's ``text_len: 512`` and the WanPipeline.__call__
+# default). Diffusers' ``_get_t5_prompt_embeds`` has a stale 226 default
+# that gets overridden by ``__call__``; using 512 here matches the actual
+# pipeline behaviour.
+WAN_T5_MAX_SEQ_LEN = 512
+
+
+@invocation(
+ "wan_text_encoder",
+ title="Prompt - Wan 2.2",
+ tags=["prompt", "conditioning", "wan"],
+ category="conditioning",
+ version="1.0.0",
+ classification=Classification.Prototype,
+)
+class WanTextEncoderInvocation(BaseInvocation):
+ """Encodes a text prompt for Wan 2.2 using the UMT5-XXL encoder.
+
+ Output is the encoder's last hidden state (shape: [seq_len=512, 4096]) plus
+ an attention mask marking valid (non-padding) tokens. The Wan transformer
+ consumes these directly as ``encoder_hidden_states``.
+ """
+
+ prompt: str = InputField(description="Text prompt for Wan 2.2.", ui_component=UIComponent.Textarea)
+ wan_t5_encoder: WanT5EncoderField = InputField(
+ title="UMT5-XXL Encoder",
+ description=FieldDescriptions.wan_t5_encoder,
+ input=Input.Connection,
+ )
+
+ @torch.no_grad()
+ def invoke(self, context: InvocationContext) -> WanConditioningOutput:
+ prompt_embeds, attention_mask = self._encode(context)
+
+ # Persist on CPU; the denoise loop will move to device as needed.
+ prompt_embeds = prompt_embeds.detach().to("cpu")
+ attention_mask = attention_mask.detach().to("cpu") if attention_mask is not None else None
+
+ conditioning_data = ConditioningFieldData(
+ conditionings=[WanConditioningInfo(prompt_embeds=prompt_embeds, prompt_attention_mask=attention_mask)]
+ )
+ conditioning_name = context.conditioning.save(conditioning_data)
+ return WanConditioningOutput.build(conditioning_name)
+
+ def _encode(self, context: InvocationContext) -> tuple[torch.Tensor, torch.Tensor | None]:
+ from diffusers.pipelines.wan.pipeline_wan import prompt_clean
+ from transformers import UMT5EncoderModel
+
+ cleaned = prompt_clean(self.prompt)
+
+ # Tokenizer + text encoder both routed through the model cache so the
+ # registered loaders handle the nested-vs-flat directory layout for us
+ # (main-model layout: /tokenizer/ + /text_encoder/;
+ # standalone WanT5Encoder layout may also be flat).
+ tokenizer_info = context.models.load(self.wan_t5_encoder.tokenizer)
+ with tokenizer_info.model_on_device() as (_, tokenizer):
+ text_inputs = tokenizer(
+ [cleaned],
+ padding="max_length",
+ max_length=WAN_T5_MAX_SEQ_LEN,
+ truncation=True,
+ add_special_tokens=True,
+ return_attention_mask=True,
+ return_tensors="pt",
+ )
+
+ text_encoder_info = context.models.load(self.wan_t5_encoder.text_encoder)
+ with text_encoder_info.model_on_device() as (_, text_encoder):
+ assert isinstance(text_encoder, UMT5EncoderModel)
+ device = get_effective_device(text_encoder)
+
+ input_ids = text_inputs.input_ids.to(device)
+ attention_mask = text_inputs.attention_mask.to(device)
+
+ context.util.signal_progress("Running UMT5-XXL text encoder")
+ outputs = text_encoder(input_ids, attention_mask)
+ # Drop the batch dim (we always encode one prompt at a time).
+ prompt_embeds = outputs.last_hidden_state.squeeze(0)
+ attention_mask_out = attention_mask.squeeze(0)
+
+ # Match the Diffusers reference: zero out the embeddings past the valid
+ # token count so the transformer sees clean padding.
+ valid_len = int(attention_mask_out.sum().item())
+ if valid_len < prompt_embeds.shape[0]:
+ prompt_embeds = prompt_embeds.clone()
+ prompt_embeds[valid_len:] = 0
+
+ # If every token is valid we don't need the mask downstream.
+ mask_out: torch.Tensor | None = attention_mask_out
+ if attention_mask_out.all():
+ mask_out = None
+
+ return prompt_embeds.to(dtype=torch.bfloat16), mask_out
diff --git a/invokeai/app/invocations/wan_video_denoise.py b/invokeai/app/invocations/wan_video_denoise.py
new file mode 100644
index 00000000000..6a23eb3d134
--- /dev/null
+++ b/invokeai/app/invocations/wan_video_denoise.py
@@ -0,0 +1,409 @@
+"""Wan 2.2 video denoise invocation (T2V / I2V).
+
+Multi-frame counterpart to :mod:`wan_denoise`. Drives the same flow-matching
+schedule + expert-swap MoE logic, but the noise tensor has a real temporal
+dimension (``T_lat = (num_frames - 1) // 4 + 1``) and the I2V conditioning is
+built across all latent frames (first frame conditioned, rest zero).
+
+Kept as a separate file rather than parameterizing ``WanDenoiseInvocation``
+so the working single-frame T2I path is not risked by the video work; the
+shared bits (expert swapper, scheduler construction, conditioning loading,
+LoRA iteration) live in ``wan_denoise`` and are imported here.
+"""
+
+from contextlib import ExitStack
+from typing import Callable, Iterable, Optional, Tuple
+
+import torch
+from tqdm import tqdm
+
+from invokeai.app.invocations.baseinvocation import BaseInvocation, Classification, invocation
+from invokeai.app.invocations.fields import (
+ FieldDescriptions,
+ Input,
+ InputField,
+ WanConditioningField,
+ WanRefImageConditioningField,
+)
+from invokeai.app.invocations.model import WanTransformerField
+from invokeai.app.invocations.primitives import LatentsOutput
+from invokeai.app.invocations.wan_denoise import (
+ WanDenoiseInvocation,
+ _ExpertSwapper,
+ _resolve_variant,
+ _validate_spatial_dimensions,
+)
+from invokeai.app.services.shared.invocation_context import InvocationContext
+from invokeai.backend.model_manager.taxonomy import BaseModelType, ModelFormat, WanVariantType
+from invokeai.backend.patches.model_patch_raw import ModelPatchRaw
+from invokeai.backend.stable_diffusion.diffusers_pipeline import PipelineIntermediateState
+from invokeai.backend.stable_diffusion.diffusion.conditioning_data import WanConditioningInfo
+from invokeai.backend.util.devices import TorchDevice
+from invokeai.backend.wan.sampling_utils import (
+ get_default_latent_channels,
+ get_spatial_scale_factor,
+ make_noise,
+ num_latent_frames_for,
+)
+
+
+@invocation(
+ "wan_video_denoise",
+ title="Denoise Video - Wan 2.2",
+ tags=["video", "wan"],
+ category="latents",
+ version="1.0.0",
+ classification=Classification.Prototype,
+)
+class WanVideoDenoiseInvocation(BaseInvocation):
+ """Run the Wan 2.2 denoising loop on a multi-frame latent tensor.
+
+ The output is a 5D ``[1, C, T_lat, H/8, W/8]`` latent tensor ready for
+ :class:`WanLatentsToVideoInvocation` to VAE-decode and encode as MP4.
+
+ Mirrors :class:`WanDenoiseInvocation` for the per-step logic (CFG, MoE
+ expert swap at the boundary timestep, LoRA patching, scheduler selection).
+ Differences from the image denoise:
+
+ * The noise tensor has a real temporal dim built from ``num_frames``.
+ * The I2V condition is built across all latent frames (frame 0
+ conditioned, rest zero) via
+ :func:`encode_reference_image_to_video_condition` upstream — the
+ ``ref_image`` field on this node carries a tensor of shape
+ ``[1, 20, T_lat, H_lat, W_lat]`` instead of ``[1, 20, 1, ...]``.
+ * No ``denoising_start`` / ``denoising_end`` / initial-latents inputs.
+ The image denoise node uses those for img2img (noise injection on an
+ existing latent), but image-conditioned video generation flows through
+ the reference-frame conditioning mechanism instead — the first frame
+ drives subsequent frames at every step, so a partial-schedule run from
+ an initial latent has no analogue here. Run the schedule from t=1
+ to t=0 every time. The base ``WanDenoiseInvocation`` still handles
+ the img2img case for stills.
+ """
+
+ transformer: WanTransformerField = InputField(
+ description=(
+ "Wan transformer field. Supported: T2V-A14B / I2V-A14B (dual-expert) and "
+ "TI2V-5B (single-expert, handles both T2V and I2V). All three accept a "
+ "Reference Image input for image-to-video; A14B uses the 36-channel concat "
+ "scheme while TI2V-5B uses the expand_timesteps first-frame-mask blend."
+ ),
+ input=Input.Connection,
+ title="Transformer",
+ )
+ positive_conditioning: WanConditioningField = InputField(
+ description=FieldDescriptions.positive_cond, input=Input.Connection
+ )
+ negative_conditioning: Optional[WanConditioningField] = InputField(
+ default=None, description=FieldDescriptions.negative_cond, input=Input.Connection
+ )
+ ref_image: Optional[WanRefImageConditioningField] = InputField(
+ default=None,
+ description=FieldDescriptions.wan_ref_image,
+ input=Input.Connection,
+ title="Reference Image",
+ )
+
+ guidance_scale: float = InputField(
+ default=5.0,
+ ge=1.0,
+ description="Classifier-free guidance scale. Wan 2.2 video reference uses 5.0 for the "
+ "high-noise expert and 4.0 for the low-noise expert.",
+ title="Guidance Scale",
+ )
+ guidance_scale_low_noise: Optional[float] = InputField(
+ default=4.0,
+ ge=0.0,
+ description="Optional separate CFG scale for the low-noise expert (Wan 2.2 A14B only). "
+ "Values below 1.0 fall back to the primary 'Guidance Scale'.",
+ title="Guidance Scale (Low Noise)",
+ )
+
+ # Wan transformer patch_size=(1, 2, 2) × VAE spatial 8x => H/W multiple of 16.
+ width: int = InputField(default=832, multiple_of=16, description="Width of the generated video.")
+ height: int = InputField(default=480, multiple_of=16, description="Height of the generated video.")
+ num_frames: int = InputField(
+ default=81,
+ ge=5,
+ description="Number of output frames. Must satisfy (num_frames - 1) %% 4 == 0 so the latent "
+ "temporal dim divides cleanly. Wan 2.2 was trained at 81 frames @ 16 FPS (~5 s).",
+ title="Number of Frames",
+ )
+ steps: int = InputField(default=40, gt=0, description="Number of denoising steps.")
+ seed: int = InputField(default=0, description="Randomness seed for reproducibility.")
+
+ @torch.no_grad()
+ def invoke(self, context: InvocationContext) -> LatentsOutput:
+ latents = self._run_diffusion(context)
+ # Keep the 5D shape (B, C, T, H, W) — wan_latents_to_video expects it.
+ latents = latents.detach().to("cpu")
+ name = context.tensors.save(tensor=latents)
+ # LatentsOutput.build uses latents.size()[3] / [2] for width / height.
+ # For 5D the spatial dims are at indices 4 / 3 instead of 3 / 2, so we
+ # call the constructor directly with the actual H/W from the inputs.
+ from invokeai.app.invocations.fields import LatentsField
+
+ return LatentsOutput(
+ latents=LatentsField(latents_name=name, seed=self.seed),
+ width=self.width,
+ height=self.height,
+ )
+
+ def _run_diffusion(self, context: InvocationContext) -> torch.Tensor:
+ if (self.num_frames - 1) % 4 != 0:
+ raise ValueError(
+ f"num_frames must satisfy (num_frames - 1) %% 4 == 0 for the Wan VAE's temporal "
+ f"compression (got {self.num_frames}). Try 5, 9, 13, ..., 81, 85, ..."
+ )
+
+ device = TorchDevice.choose_torch_device()
+ inference_dtype = TorchDevice.choose_bfloat16_safe_dtype(device)
+
+ variant = _resolve_variant(context, self.transformer)
+ _validate_spatial_dimensions(variant, self.width, self.height)
+ spatial_scale = get_spatial_scale_factor(variant)
+
+ # Reuse the image denoise's scheduler construction so we pick up whatever
+ # scheduler the variant ships with (UniPCMultistepScheduler with the
+ # variant's flow_shift, or whatever an on-disk config specifies).
+ scheduler_builder = WanDenoiseInvocation._build_scheduler # bound on instance below
+ # Bind a minimal instance to call _build_scheduler — it only reads
+ # self.transformer, which is shape-compatible.
+ proxy = WanDenoiseInvocation.model_construct(
+ transformer=self.transformer,
+ positive_conditioning=self.positive_conditioning,
+ )
+ scheduler = scheduler_builder(proxy, context, device)
+
+ pos_cond = self._load_conditioning(context, self.positive_conditioning, device=device, dtype=inference_dtype)
+ low_cfg_enabled = (
+ self.transformer.transformer_low_noise is not None
+ and self.guidance_scale_low_noise is not None
+ and self.guidance_scale_low_noise >= 1.0
+ and self.guidance_scale_low_noise != 1.0
+ )
+ do_cfg = self.negative_conditioning is not None and (self.guidance_scale != 1.0 or low_cfg_enabled)
+ neg_cond: WanConditioningInfo | None = None
+ if do_cfg:
+ assert self.negative_conditioning is not None
+ neg_cond = self._load_conditioning(
+ context, self.negative_conditioning, device=device, dtype=inference_dtype
+ )
+
+ # I2V condition tensor. Two flavours:
+ # * A14B I2V — [1, 20, T_lat, H_lat, W_lat] (4 mask + 16 latent channels).
+ # Concatenated to noise latents along the channel dim each step → 36ch.
+ # * TI2V-5B I2V — [1, 48, 1, H_lat, W_lat] (single latent frame, same
+ # channel count as the noise latents). Blended with noise via a
+ # first_frame_mask at every step (expand_timesteps path).
+ # Variant dispatch happens via the condition tensor's channel count below.
+ ref_condition: torch.Tensor | None = None
+ if self.ref_image is not None:
+ if variant not in (WanVariantType.I2V_A14B, WanVariantType.TI2V_5B):
+ raise ValueError(
+ f"Reference-image conditioning is only supported by Wan 2.2 I2V variants "
+ f"(I2V-A14B or TI2V-5B). The selected transformer is {variant.value!r}. "
+ "Remove the Reference Image input or load an I2V variant."
+ )
+ if self.ref_image.width != self.width or self.ref_image.height != self.height:
+ raise ValueError(
+ f"Reference-image dimensions ({self.ref_image.width}x{self.ref_image.height}) must "
+ f"match denoise dimensions ({self.width}x{self.height})."
+ )
+ # A14B encodes one condition tensor per pixel-frame count, so the
+ # encoder's num_frames must match. TI2V-5B's condition is always
+ # single-frame regardless of the output length, so the field's
+ # num_frames is informational only and we skip this check.
+ if variant == WanVariantType.I2V_A14B and self.ref_image.num_frames != self.num_frames:
+ raise ValueError(
+ f"Reference-image num_frames ({self.ref_image.num_frames}) must match denoise "
+ f"num_frames ({self.num_frames}). Re-run the Reference Image - Wan 2.2 node with "
+ f"num_frames={self.num_frames}."
+ )
+ ref_condition = context.tensors.load(self.ref_image.condition_tensor_name).to(
+ device=device, dtype=inference_dtype
+ )
+
+ scheduler.set_timesteps(num_inference_steps=self.steps, device=device)
+ timesteps = scheduler.timesteps
+ total_steps = len(timesteps)
+
+ # fp32 latents through the loop; cast to inference_dtype only when
+ # calling the transformer (same as wan_denoise).
+ latent_dtype = torch.float32
+ # 48 for TI2V-5B (Wan 2.2-VAE z_dim=48), 16 for A14B variants.
+ latent_channels = get_default_latent_channels(variant)
+ t_lat = num_latent_frames_for(self.num_frames)
+
+ latents = make_noise(
+ batch_size=1,
+ latent_channels=latent_channels,
+ height=self.height,
+ width=self.width,
+ spatial_scale_factor=spatial_scale,
+ device=device,
+ dtype=latent_dtype,
+ seed=self.seed,
+ num_latent_frames=t_lat,
+ )
+
+ if total_steps <= 0:
+ return latents
+
+ # Sanity-check ref-condition shape per variant. A14B expects matched T_lat;
+ # TI2V-5B expects a single latent frame regardless of output length.
+ if ref_condition is not None:
+ if variant == WanVariantType.TI2V_5B:
+ if ref_condition.shape[1] != 48 or ref_condition.shape[2] != 1:
+ raise ValueError(
+ f"TI2V-5B reference condition must be shape [1, 48, 1, H_lat, W_lat] "
+ f"(got channels={ref_condition.shape[1]}, frames={ref_condition.shape[2]}). "
+ "Re-run the Reference Image - Wan 2.2 node with a TI2V-5B VAE."
+ )
+ elif ref_condition.shape[2] != t_lat:
+ raise ValueError(
+ f"Reference-image condition has {ref_condition.shape[2]} latent frames but the "
+ f"denoise loop expected {t_lat}. Ensure the ref-image encoder was called with "
+ f"the same num_frames ({self.num_frames})."
+ )
+
+ # Build the TI2V-5B first-frame mask once: 0 at frame 0 (locked to the
+ # condition), 1 elsewhere (free to denoise). Broadcasts across channel dim.
+ first_frame_mask: torch.Tensor | None = None
+ if ref_condition is not None and variant == WanVariantType.TI2V_5B:
+ _, _, _, h_lat, w_lat = latents.shape
+ first_frame_mask = torch.ones(1, 1, t_lat, h_lat, w_lat, device=device, dtype=inference_dtype)
+ first_frame_mask[:, :, 0] = 0
+
+ step_callback = self._build_step_callback(context)
+
+ high_model = self.transformer.transformer
+ low_model = self.transformer.transformer_low_noise
+ low_config = context.models.get_config(low_model) if low_model is not None else None
+ num_train_timesteps = int(scheduler.config.num_train_timesteps)
+ boundary_timestep = self.transformer.boundary_ratio * num_train_timesteps if low_model is not None else None
+
+ # No fallback between the LoRA lists — see the matching comment in
+ # wan_denoise: an empty low list means "no LoRAs on the low expert".
+ high_loras = self.transformer.loras
+ low_loras = self.transformer.loras_low_noise
+ high_config = context.models.get_config(high_model)
+ high_is_quantized = high_config.format == ModelFormat.GGUFQuantized
+ low_is_quantized = low_config.format == ModelFormat.GGUFQuantized if low_config is not None else False
+
+ def high_lora_factory() -> Iterable[Tuple[ModelPatchRaw, float]]:
+ return proxy._lora_iterator(context, high_loras)
+
+ def low_lora_factory() -> Iterable[Tuple[ModelPatchRaw, float]]:
+ return proxy._lora_iterator(context, low_loras)
+
+ with ExitStack() as exit_stack:
+ swapper = _ExpertSwapper(
+ context=context,
+ high_model=high_model,
+ low_model=low_model,
+ inference_dtype=inference_dtype,
+ high_lora_factory=high_lora_factory if high_loras else None,
+ low_lora_factory=low_lora_factory if low_loras else None,
+ high_is_quantized=high_is_quantized,
+ low_is_quantized=low_is_quantized,
+ )
+ exit_stack.callback(swapper.close)
+
+ for step_idx, t in enumerate(
+ tqdm(timesteps, desc=f"Denoising Wan 2.2 video ({self.num_frames} frames)", total=total_steps)
+ ):
+ if low_model is not None and float(t) < float(boundary_timestep):
+ active_label = _ExpertSwapper.LOW
+ low_cfg = self.guidance_scale_low_noise
+ active_cfg = low_cfg if (low_cfg is not None and low_cfg >= 1.0) else self.guidance_scale
+ else:
+ active_label = _ExpertSwapper.HIGH
+ active_cfg = self.guidance_scale
+
+ transformer = swapper.get(active_label)
+
+ latent_model_input = latents.to(dtype=inference_dtype)
+
+ # Per-variant conditioning. Two distinct mechanisms:
+ if first_frame_mask is not None:
+ # TI2V-5B I2V (expand_timesteps): blend the condition into frame 0
+ # and the noisy latents elsewhere. Per-token timestep tensor
+ # gates the model so it sees timestep=0 at frame-0 positions
+ # (nothing to denoise) and the normal `t` everywhere else.
+ assert ref_condition is not None
+ latent_model_input = (1 - first_frame_mask) * ref_condition + first_frame_mask * latent_model_input
+ # Strided slice matches the transformer's spatial patch_size=2;
+ # flatten gives per-token timesteps. Shape: [1, T_lat * H_lat//2 * W_lat//2].
+ temp_ts = (first_frame_mask[0, 0, :, ::2, ::2] * t).flatten()
+ timestep = temp_ts.unsqueeze(0).expand(latents.shape[0], -1).to(dtype=inference_dtype)
+ elif ref_condition is not None:
+ # A14B I2V: concat 20-ch condition along channel dim → 36-ch input.
+ latent_model_input = torch.cat([latent_model_input, ref_condition], dim=1)
+ timestep = t.expand(latents.shape[0])
+ else:
+ # T2V (any variant): scalar timestep per batch.
+ timestep = t.expand(latents.shape[0])
+
+ noise_pred_cond = transformer(
+ hidden_states=latent_model_input,
+ timestep=timestep,
+ encoder_hidden_states=pos_cond.prompt_embeds.unsqueeze(0),
+ attention_kwargs=None,
+ return_dict=False,
+ )[0]
+
+ if neg_cond is not None and active_cfg != 1.0:
+ noise_pred_uncond = transformer(
+ hidden_states=latent_model_input,
+ timestep=timestep,
+ encoder_hidden_states=neg_cond.prompt_embeds.unsqueeze(0),
+ attention_kwargs=None,
+ return_dict=False,
+ )[0]
+ noise_pred = noise_pred_uncond + active_cfg * (noise_pred_cond - noise_pred_uncond)
+ else:
+ noise_pred = noise_pred_cond
+
+ latents = scheduler.step(noise_pred, t, latents, return_dict=False)[0]
+
+ step_callback(
+ PipelineIntermediateState(
+ step=step_idx + 1,
+ order=1,
+ total_steps=total_steps,
+ timestep=int(t.item()),
+ # Preview shows the middle frame for video.
+ latents=latents[:, :, t_lat // 2],
+ )
+ )
+
+ # TI2V-5B: frame 0's latents drifted through the scheduler step at each
+ # iteration; restore them to the clean condition before VAE-decoding so
+ # the first output frame matches the reference image. Mirrors
+ # ``WanImageToVideoPipeline`` (pipeline_wan_i2v.py:813-814).
+ if first_frame_mask is not None:
+ assert ref_condition is not None
+ latents = (1 - first_frame_mask) * ref_condition.to(dtype=latents.dtype) + first_frame_mask * latents
+
+ return latents
+
+ def _load_conditioning(
+ self,
+ context: InvocationContext,
+ cond_field: WanConditioningField,
+ *,
+ device: torch.device,
+ dtype: torch.dtype,
+ ) -> WanConditioningInfo:
+ cond_data = context.conditioning.load(cond_field.conditioning_name)
+ assert len(cond_data.conditionings) == 1
+ cond_info = cond_data.conditionings[0]
+ assert isinstance(cond_info, WanConditioningInfo)
+ return cond_info.to(device=device, dtype=dtype)
+
+ def _build_step_callback(self, context: InvocationContext) -> Callable[[PipelineIntermediateState], None]:
+ def step_callback(state: PipelineIntermediateState) -> None:
+ context.util.sd_step_callback(state, BaseModelType.Wan)
+
+ return step_callback
diff --git a/invokeai/app/run_app.py b/invokeai/app/run_app.py
index 7401b3148c6..79586207dfe 100644
--- a/invokeai/app/run_app.py
+++ b/invokeai/app/run_app.py
@@ -1,5 +1,22 @@
+import os
from typing import Any
+# Suppress the HuggingFace tokenizers fork-after-parallelism warning. The Rust
+# ``tokenizers`` library warms a thread pool the first time a tokenizer is used
+# (e.g. the UMT5 / T5 text encoder during Wan / FLUX / SD3 conditioning), then
+# complains every time we fork() afterwards — which we do, on every MP4 encode,
+# because imageio's FFMPEG plugin shells out to ffmpeg via subprocess.Popen.
+# The warning is harmless (the child correctly falls back to single-threaded
+# tokenization before exec()) but it spams the log on every video generation.
+#
+# This MUST execute before any HF library is imported. The pyproject console-script
+# (``invokeai-web = invokeai.app.run_app:run_app``) reaches this module first via
+# ``from invokeai.app.run_app import run_app``, so setting the env var at module
+# level — not inside ``run_app()`` — guarantees it lands before any transitive HF
+# import. Use ``setdefault`` so anyone who explicitly exports ``true`` upstream
+# keeps their value.
+os.environ.setdefault("TOKENIZERS_PARALLELISM", "false")
+
def get_app():
"""Import the app and event loop. We wrap this in a function to more explicitly control when it happens, because
diff --git a/invokeai/app/services/auth/token_service.py b/invokeai/app/services/auth/token_service.py
index 2d766bb90aa..1a5c0747388 100644
--- a/invokeai/app/services/auth/token_service.py
+++ b/invokeai/app/services/auth/token_service.py
@@ -104,3 +104,23 @@ def verify_token(token: str) -> TokenData | None:
except Exception:
# Catch any other exceptions (e.g., Pydantic validation errors)
return None
+
+
+def get_token_remaining_seconds(token: str) -> int | None:
+ """Return the number of seconds until a *valid* token expires.
+
+ Verifies the token first (signature + expiry + payload shape); returns None if
+ it fails verification. A valid token without an ``exp`` claim gets the default
+ expiration window, matching what ``create_access_token`` would have assigned.
+ """
+ if verify_token(token) is None:
+ return None
+ try:
+ claims = jwt.get_unverified_claims(token)
+ except JWTError:
+ return None
+ exp = claims.get("exp")
+ if exp is None:
+ return int(timedelta(hours=DEFAULT_EXPIRATION_HOURS).total_seconds())
+ remaining = int(cast(float, exp) - datetime.now(timezone.utc).timestamp())
+ return remaining if remaining > 0 else None
diff --git a/invokeai/app/services/board_image_records/board_image_records_base.py b/invokeai/app/services/board_image_records/board_image_records_base.py
index 4ccbaa952db..561eb79ce5f 100644
--- a/invokeai/app/services/board_image_records/board_image_records_base.py
+++ b/invokeai/app/services/board_image_records/board_image_records_base.py
@@ -30,8 +30,13 @@ def get_all_board_image_names_for_board(
board_id: str,
categories: list[ImageCategory] | None,
is_intermediate: bool | None,
+ user_id: Optional[str] = None,
) -> list[str]:
- """Gets all board images for a board, as a list of the image names."""
+ """Gets all board images for a board, as a list of the image names.
+
+ When ``user_id`` is provided, results are restricted to images owned by that user;
+ pass ``None`` for the admin path (no per-user restriction).
+ """
pass
@abstractmethod
diff --git a/invokeai/app/services/board_image_records/board_image_records_sqlite.py b/invokeai/app/services/board_image_records/board_image_records_sqlite.py
index b249bb67334..8d1f9003222 100644
--- a/invokeai/app/services/board_image_records/board_image_records_sqlite.py
+++ b/invokeai/app/services/board_image_records/board_image_records_sqlite.py
@@ -80,6 +80,7 @@ def get_all_board_image_names_for_board(
board_id: str,
categories: list[ImageCategory] | None,
is_intermediate: bool | None,
+ user_id: Optional[str] = None,
) -> list[str]:
with self._db.transaction() as cursor:
params: list[str | bool] = []
@@ -124,6 +125,13 @@ def get_all_board_image_names_for_board(
"""
params.append(is_intermediate)
+ # Per-user filter — admins pass user_id=None to skip this clause.
+ if user_id is not None:
+ stmt += """--sql
+ AND images.user_id = ?
+ """
+ params.append(user_id)
+
# Put a ring on it
stmt += ";"
diff --git a/invokeai/app/services/board_images/board_images_base.py b/invokeai/app/services/board_images/board_images_base.py
index c16d971cd28..269cebfeaea 100644
--- a/invokeai/app/services/board_images/board_images_base.py
+++ b/invokeai/app/services/board_images/board_images_base.py
@@ -30,8 +30,13 @@ def get_all_board_image_names_for_board(
board_id: str,
categories: list[ImageCategory] | None,
is_intermediate: bool | None,
+ user_id: Optional[str] = None,
) -> list[str]:
- """Gets all board images for a board, as a list of the image names."""
+ """Gets all board images for a board, as a list of the image names.
+
+ When ``user_id`` is provided, results are restricted to images owned by that user;
+ pass ``None`` for the admin path (no per-user restriction).
+ """
pass
@abstractmethod
diff --git a/invokeai/app/services/board_images/board_images_default.py b/invokeai/app/services/board_images/board_images_default.py
index 437495189f3..b42ae3db031 100644
--- a/invokeai/app/services/board_images/board_images_default.py
+++ b/invokeai/app/services/board_images/board_images_default.py
@@ -29,11 +29,13 @@ def get_all_board_image_names_for_board(
board_id: str,
categories: list[ImageCategory] | None,
is_intermediate: bool | None,
+ user_id: Optional[str] = None,
) -> list[str]:
return self.__invoker.services.board_image_records.get_all_board_image_names_for_board(
board_id,
categories,
is_intermediate,
+ user_id=user_id,
)
def get_board_for_image(
diff --git a/invokeai/app/services/board_video_records/__init__.py b/invokeai/app/services/board_video_records/__init__.py
new file mode 100644
index 00000000000..e69de29bb2d
diff --git a/invokeai/app/services/board_video_records/board_video_records_base.py b/invokeai/app/services/board_video_records/board_video_records_base.py
new file mode 100644
index 00000000000..244b7814924
--- /dev/null
+++ b/invokeai/app/services/board_video_records/board_video_records_base.py
@@ -0,0 +1,43 @@
+from abc import ABC, abstractmethod
+from typing import Optional
+
+from invokeai.app.services.image_records.image_records_common import ImageCategory
+
+
+class BoardVideoRecordStorageBase(ABC):
+ """Abstract base class for the one-to-many board-video relationship record storage."""
+
+ @abstractmethod
+ def add_video_to_board(self, board_id: str, video_name: str) -> None:
+ """Adds a video to a board."""
+ pass
+
+ @abstractmethod
+ def remove_video_from_board(self, video_name: str) -> None:
+ """Removes a video from a board."""
+ pass
+
+ @abstractmethod
+ def get_all_board_video_names_for_board(
+ self,
+ board_id: str,
+ categories: list[ImageCategory] | None,
+ is_intermediate: bool | None,
+ user_id: Optional[str] = None,
+ ) -> list[str]:
+ """Gets all board videos for a board, as a list of the video names.
+
+ When ``user_id`` is provided, results are restricted to videos owned by that user;
+ pass ``None`` for the admin path (no per-user restriction).
+ """
+ pass
+
+ @abstractmethod
+ def get_board_for_video(self, video_name: str) -> Optional[str]:
+ """Gets a video's board id, if it has one."""
+ pass
+
+ @abstractmethod
+ def get_video_count_for_board(self, board_id: str) -> int:
+ """Gets the number of videos for a board."""
+ pass
diff --git a/invokeai/app/services/board_video_records/board_video_records_sqlite.py b/invokeai/app/services/board_video_records/board_video_records_sqlite.py
new file mode 100644
index 00000000000..6b780f8eaae
--- /dev/null
+++ b/invokeai/app/services/board_video_records/board_video_records_sqlite.py
@@ -0,0 +1,97 @@
+import sqlite3
+from typing import Optional, cast
+
+from invokeai.app.services.board_video_records.board_video_records_base import BoardVideoRecordStorageBase
+from invokeai.app.services.image_records.image_records_common import ImageCategory
+from invokeai.app.services.shared.sqlite.sqlite_database import SqliteDatabase
+
+
+class SqliteBoardVideoRecordStorage(BoardVideoRecordStorageBase):
+ def __init__(self, db: SqliteDatabase) -> None:
+ super().__init__()
+ self._db = db
+
+ def add_video_to_board(self, board_id: str, video_name: str) -> None:
+ with self._db.transaction() as cursor:
+ cursor.execute(
+ """--sql
+ INSERT INTO board_videos (board_id, video_name)
+ VALUES (?, ?)
+ ON CONFLICT (video_name) DO UPDATE SET board_id = ?;
+ """,
+ (board_id, video_name, board_id),
+ )
+
+ def remove_video_from_board(self, video_name: str) -> None:
+ with self._db.transaction() as cursor:
+ cursor.execute(
+ "DELETE FROM board_videos WHERE video_name = ?;",
+ (video_name,),
+ )
+
+ def get_all_board_video_names_for_board(
+ self,
+ board_id: str,
+ categories: list[ImageCategory] | None,
+ is_intermediate: bool | None,
+ user_id: Optional[str] = None,
+ ) -> list[str]:
+ with self._db.transaction() as cursor:
+ params: list[str | bool] = []
+ stmt = """
+ SELECT videos.video_name
+ FROM videos
+ LEFT JOIN board_videos ON board_videos.video_name = videos.video_name
+ WHERE 1=1
+ """
+ if board_id == "none":
+ stmt += " AND board_videos.board_id IS NULL "
+ else:
+ stmt += " AND board_videos.board_id = ? "
+ params.append(board_id)
+
+ if categories is not None:
+ category_strings = [c.value for c in set(categories)]
+ placeholders = ",".join("?" * len(category_strings))
+ stmt += f" AND videos.video_category IN ( {placeholders} ) "
+ for c in category_strings:
+ params.append(c)
+
+ if is_intermediate is not None:
+ stmt += " AND videos.is_intermediate = ? "
+ params.append(is_intermediate)
+
+ if user_id is not None:
+ stmt += " AND videos.user_id = ? "
+ params.append(user_id)
+
+ stmt += ";"
+ cursor.execute(stmt, params)
+ result = cast(list[sqlite3.Row], cursor.fetchall())
+ return [r[0] for r in result]
+
+ def get_board_for_video(self, video_name: str) -> Optional[str]:
+ with self._db.transaction() as cursor:
+ cursor.execute(
+ "SELECT board_id FROM board_videos WHERE video_name = ?;",
+ (video_name,),
+ )
+ result = cursor.fetchone()
+ if result is None:
+ return None
+ return cast(str, result[0])
+
+ def get_video_count_for_board(self, board_id: str) -> int:
+ with self._db.transaction() as cursor:
+ cursor.execute(
+ """--sql
+ SELECT COUNT(*)
+ FROM board_videos
+ INNER JOIN videos ON board_videos.video_name = videos.video_name
+ WHERE videos.is_intermediate = FALSE
+ AND board_videos.board_id = ?;
+ """,
+ (board_id,),
+ )
+ count = cast(int, cursor.fetchone()[0])
+ return count
diff --git a/invokeai/app/services/boards/boards_common.py b/invokeai/app/services/boards/boards_common.py
index 99952fec134..c6310242bde 100644
--- a/invokeai/app/services/boards/boards_common.py
+++ b/invokeai/app/services/boards/boards_common.py
@@ -10,8 +10,14 @@ class BoardDTO(BoardRecord):
cover_image_name: Optional[str] = Field(description="The name of the board's cover image.")
"""The URL of the thumbnail of the most recent image in the board."""
+ cover_video_name: Optional[str] = Field(
+ default=None, description="The name of the board's cover video, when the most recent item is a video."
+ )
+ """The name of the cover video, set when the most-recent item on the board is a video rather than an image."""
image_count: int = Field(description="The number of images in the board.")
"""The number of images in the board."""
+ video_count: int = Field(default=0, description="The number of videos in the board.")
+ """The number of videos in the board."""
asset_count: int = Field(description="The number of assets in the board.")
"""The number of assets in the board."""
owner_username: Optional[str] = Field(default=None, description="The username of the board owner (for admin view).")
@@ -24,12 +30,16 @@ def board_record_to_dto(
image_count: int,
asset_count: int,
owner_username: Optional[str] = None,
+ cover_video_name: Optional[str] = None,
+ video_count: int = 0,
) -> BoardDTO:
"""Converts a board record to a board DTO."""
return BoardDTO(
**board_record.model_dump(exclude={"cover_image_name"}),
cover_image_name=cover_image_name,
+ cover_video_name=cover_video_name,
image_count=image_count,
+ video_count=video_count,
asset_count=asset_count,
owner_username=owner_username,
)
diff --git a/invokeai/app/services/boards/boards_default.py b/invokeai/app/services/boards/boards_default.py
index 71465815ef9..65fb05d6bd3 100644
--- a/invokeai/app/services/boards/boards_default.py
+++ b/invokeai/app/services/boards/boards_default.py
@@ -1,3 +1,5 @@
+from typing import Optional
+
from invokeai.app.services.board_records.board_records_common import BoardChanges, BoardRecordOrderBy
from invokeai.app.services.boards.boards_base import BoardServiceABC
from invokeai.app.services.boards.boards_common import BoardDTO, board_record_to_dto
@@ -12,6 +14,38 @@ class BoardService(BoardServiceABC):
def start(self, invoker: Invoker) -> None:
self.__invoker = invoker
+ def _resolve_cover(self, board_id: str) -> tuple[Optional[str], Optional[str]]:
+ """Pick the cover item for a board, considering both images and videos.
+
+ Returns ``(cover_image_name, cover_video_name)`` — at most one is set.
+ The winner is chosen by ``(starred DESC, created_at DESC)`` across both
+ tables so a recent video can supersede an older image (and vice versa).
+ """
+ cover_image = self.__invoker.services.image_records.get_most_recent_image_for_board(board_id)
+ cover_video = self.__invoker.services.video_records.get_most_recent_video_for_board(board_id)
+
+ if cover_image is None and cover_video is None:
+ return None, None
+ if cover_video is None:
+ assert cover_image is not None
+ return cover_image.image_name, None
+ if cover_image is None:
+ return None, cover_video.video_name
+
+ # Both candidates exist — compare on (starred, created_at).
+ image_key = (cover_image.starred, cover_image.created_at)
+ video_key = (cover_video.starred, cover_video.created_at)
+ if video_key > image_key:
+ return None, cover_video.video_name
+ return cover_image.image_name, None
+
+ def _get_counts(self, board_id: str) -> tuple[int, int, int]:
+ """Return ``(image_count, video_count, asset_count)`` for a board."""
+ image_count = self.__invoker.services.board_image_records.get_image_count_for_board(board_id)
+ asset_count = self.__invoker.services.board_image_records.get_asset_count_for_board(board_id)
+ video_count = self.__invoker.services.board_video_records.get_video_count_for_board(board_id)
+ return image_count, video_count, asset_count
+
def create(
self,
board_name: str,
@@ -22,14 +56,16 @@ def create(
def get_dto(self, board_id: str) -> BoardDTO:
board_record = self.__invoker.services.board_records.get(board_id)
- cover_image = self.__invoker.services.image_records.get_most_recent_image_for_board(board_record.board_id)
- if cover_image:
- cover_image_name = cover_image.image_name
- else:
- cover_image_name = None
- image_count = self.__invoker.services.board_image_records.get_image_count_for_board(board_id)
- asset_count = self.__invoker.services.board_image_records.get_asset_count_for_board(board_id)
- return board_record_to_dto(board_record, cover_image_name, image_count, asset_count)
+ cover_image_name, cover_video_name = self._resolve_cover(board_record.board_id)
+ image_count, video_count, asset_count = self._get_counts(board_id)
+ return board_record_to_dto(
+ board_record,
+ cover_image_name,
+ image_count,
+ asset_count,
+ cover_video_name=cover_video_name,
+ video_count=video_count,
+ )
def update(
self,
@@ -37,15 +73,16 @@ def update(
changes: BoardChanges,
) -> BoardDTO:
board_record = self.__invoker.services.board_records.update(board_id, changes)
- cover_image = self.__invoker.services.image_records.get_most_recent_image_for_board(board_record.board_id)
- if cover_image:
- cover_image_name = cover_image.image_name
- else:
- cover_image_name = None
-
- image_count = self.__invoker.services.board_image_records.get_image_count_for_board(board_id)
- asset_count = self.__invoker.services.board_image_records.get_asset_count_for_board(board_id)
- return board_record_to_dto(board_record, cover_image_name, image_count, asset_count)
+ cover_image_name, cover_video_name = self._resolve_cover(board_record.board_id)
+ image_count, video_count, asset_count = self._get_counts(board_id)
+ return board_record_to_dto(
+ board_record,
+ cover_image_name,
+ image_count,
+ asset_count,
+ cover_video_name=cover_video_name,
+ video_count=video_count,
+ )
def delete(self, board_id: str) -> None:
self.__invoker.services.board_records.delete(board_id)
@@ -65,14 +102,8 @@ def get_many(
)
board_dtos = []
for r in board_records.items:
- cover_image = self.__invoker.services.image_records.get_most_recent_image_for_board(r.board_id)
- if cover_image:
- cover_image_name = cover_image.image_name
- else:
- cover_image_name = None
-
- image_count = self.__invoker.services.board_image_records.get_image_count_for_board(r.board_id)
- asset_count = self.__invoker.services.board_image_records.get_asset_count_for_board(r.board_id)
+ cover_image_name, cover_video_name = self._resolve_cover(r.board_id)
+ image_count, video_count, asset_count = self._get_counts(r.board_id)
# For admin users, include owner username
owner_username = None
@@ -81,7 +112,17 @@ def get_many(
if owner:
owner_username = owner.display_name or owner.email
- board_dtos.append(board_record_to_dto(r, cover_image_name, image_count, asset_count, owner_username))
+ board_dtos.append(
+ board_record_to_dto(
+ r,
+ cover_image_name,
+ image_count,
+ asset_count,
+ owner_username,
+ cover_video_name=cover_video_name,
+ video_count=video_count,
+ )
+ )
return OffsetPaginatedResults[BoardDTO](items=board_dtos, offset=offset, limit=limit, total=len(board_dtos))
@@ -98,14 +139,8 @@ def get_all(
)
board_dtos = []
for r in board_records:
- cover_image = self.__invoker.services.image_records.get_most_recent_image_for_board(r.board_id)
- if cover_image:
- cover_image_name = cover_image.image_name
- else:
- cover_image_name = None
-
- image_count = self.__invoker.services.board_image_records.get_image_count_for_board(r.board_id)
- asset_count = self.__invoker.services.board_image_records.get_asset_count_for_board(r.board_id)
+ cover_image_name, cover_video_name = self._resolve_cover(r.board_id)
+ image_count, video_count, asset_count = self._get_counts(r.board_id)
# For admin users, include owner username
owner_username = None
@@ -114,6 +149,16 @@ def get_all(
if owner:
owner_username = owner.display_name or owner.email
- board_dtos.append(board_record_to_dto(r, cover_image_name, image_count, asset_count, owner_username))
+ board_dtos.append(
+ board_record_to_dto(
+ r,
+ cover_image_name,
+ image_count,
+ asset_count,
+ owner_username,
+ cover_video_name=cover_video_name,
+ video_count=video_count,
+ )
+ )
return board_dtos
diff --git a/invokeai/app/services/gallery/__init__.py b/invokeai/app/services/gallery/__init__.py
new file mode 100644
index 00000000000..e69de29bb2d
diff --git a/invokeai/app/services/gallery/gallery_base.py b/invokeai/app/services/gallery/gallery_base.py
new file mode 100644
index 00000000000..fbadabdeb9f
--- /dev/null
+++ b/invokeai/app/services/gallery/gallery_base.py
@@ -0,0 +1,60 @@
+from abc import ABC, abstractmethod
+from typing import Optional
+
+from invokeai.app.services.gallery.gallery_common import GalleryItem, GalleryItemNamesResult
+from invokeai.app.services.image_records.image_records_common import ImageCategory, ResourceOrigin
+from invokeai.app.services.shared.pagination import OffsetPaginatedResults
+from invokeai.app.services.shared.sqlite.sqlite_common import SQLiteDirection
+from invokeai.app.services.virtual_boards.virtual_boards_common import VirtualSubBoardDTO
+
+
+class GalleryServiceABC(ABC):
+ """High-level service producing a polymorphic stream of images and videos."""
+
+ @abstractmethod
+ def list_items(
+ self,
+ offset: int = 0,
+ limit: int = 10,
+ starred_first: bool = True,
+ order_dir: SQLiteDirection = SQLiteDirection.Descending,
+ origin: Optional[ResourceOrigin] = None,
+ categories: Optional[list[ImageCategory]] = None,
+ is_intermediate: Optional[bool] = None,
+ board_id: Optional[str] = None,
+ search_term: Optional[str] = None,
+ user_id: Optional[str] = None,
+ is_admin: bool = False,
+ ) -> OffsetPaginatedResults[GalleryItem]:
+ """Lists a paginated, time-sorted stream of image + video items."""
+ pass
+
+ @abstractmethod
+ def list_item_names(
+ self,
+ starred_first: bool = True,
+ order_dir: SQLiteDirection = SQLiteDirection.Descending,
+ origin: Optional[ResourceOrigin] = None,
+ categories: Optional[list[ImageCategory]] = None,
+ is_intermediate: Optional[bool] = None,
+ board_id: Optional[str] = None,
+ search_term: Optional[str] = None,
+ user_id: Optional[str] = None,
+ is_admin: bool = False,
+ created_date: Optional[str] = None,
+ ) -> GalleryItemNamesResult:
+ """Returns ordered (kind, name) refs for optimistic UI / virtualized lists.
+
+ `created_date` restricts the result to items created on the given ISO date — used by
+ date-based virtual boards.
+ """
+ pass
+
+ @abstractmethod
+ def get_dates(
+ self,
+ user_id: Optional[str] = None,
+ is_admin: bool = False,
+ ) -> list[VirtualSubBoardDTO]:
+ """Returns date-based virtual sub-boards covering both images and videos."""
+ pass
diff --git a/invokeai/app/services/gallery/gallery_common.py b/invokeai/app/services/gallery/gallery_common.py
new file mode 100644
index 00000000000..9cebc124558
--- /dev/null
+++ b/invokeai/app/services/gallery/gallery_common.py
@@ -0,0 +1,55 @@
+"""Polymorphic gallery types: images and videos appearing in a single time-sorted stream."""
+
+import datetime
+from enum import Enum
+from typing import Optional, Union
+
+from pydantic import BaseModel, Field
+
+from invokeai.app.services.image_records.image_records_common import ImageCategory
+from invokeai.app.util.metaenum import MetaEnum
+from invokeai.app.util.model_exclude_null import BaseModelExcludeNull
+
+
+class GalleryItemKind(str, Enum, metaclass=MetaEnum):
+ """Discriminator for polymorphic gallery items."""
+
+ IMAGE = "image"
+ VIDEO = "video"
+
+
+class GalleryItemRef(BaseModel):
+ """A thin reference to a gallery item — used for ordered name lists."""
+
+ kind: GalleryItemKind = Field(description="Whether the item is an image or video.")
+ name: str = Field(description="The unique name of the image or video.")
+
+
+class GalleryItem(BaseModelExcludeNull):
+ """A gallery item — either an image or a video, with shared fields and a discriminator.
+
+ Frontend code should dispatch on `kind` to render image- vs video-specific UI.
+ """
+
+ kind: GalleryItemKind = Field(description="Whether the item is an image or video.")
+ name: str = Field(description="The unique name of the image or video.")
+ full_url: str = Field(description="URL to the full-resolution image PNG or the full-quality video MP4.")
+ thumbnail_url: str = Field(description="URL to the static (WebP) thumbnail.")
+ width: int = Field(description="The width of the item in pixels.")
+ height: int = Field(description="The height of the item in pixels.")
+ category: ImageCategory = Field(description="The category of the item (images and videos share the same enum).")
+ starred: bool = Field(description="Whether the item is starred.")
+ is_intermediate: bool = Field(description="Whether the item is an intermediate output.")
+ board_id: Optional[str] = Field(default=None, description="Owning board id, if any.")
+ created_at: Union[datetime.datetime, str] = Field(description="The created timestamp of the item.")
+ # Video-only fields. None for images.
+ duration: Optional[float] = Field(default=None, description="Video duration in seconds. None for images.")
+ fps: Optional[float] = Field(default=None, description="Video frames per second. None for images.")
+
+
+class GalleryItemNamesResult(BaseModel):
+ """Ordered list of gallery item references plus counts for optimistic UI."""
+
+ items: list[GalleryItemRef] = Field(description="Ordered list of (kind, name) references.")
+ starred_count: int = Field(description="Number of starred items (when starred_first=True).")
+ total_count: int = Field(description="Total number of items matching the query.")
diff --git a/invokeai/app/services/gallery/gallery_default.py b/invokeai/app/services/gallery/gallery_default.py
new file mode 100644
index 00000000000..b58b68b7a97
--- /dev/null
+++ b/invokeai/app/services/gallery/gallery_default.py
@@ -0,0 +1,405 @@
+import sqlite3
+from typing import Optional, Union, cast
+
+from invokeai.app.services.gallery.gallery_base import GalleryServiceABC
+from invokeai.app.services.gallery.gallery_common import (
+ GalleryItem,
+ GalleryItemKind,
+ GalleryItemNamesResult,
+ GalleryItemRef,
+)
+from invokeai.app.services.image_records.image_records_common import ImageCategory, ResourceOrigin
+from invokeai.app.services.invoker import Invoker
+from invokeai.app.services.shared.pagination import OffsetPaginatedResults
+from invokeai.app.services.shared.sqlite.sqlite_common import SQLiteDirection
+from invokeai.app.services.shared.sqlite.sqlite_database import SqliteDatabase
+from invokeai.app.services.virtual_boards.virtual_boards_common import VirtualSubBoardDTO
+
+
+class SqliteGalleryService(GalleryServiceABC):
+ """Implements a polymorphic gallery via UNION ALL across the `images` and `videos` tables.
+
+ Filters are applied identically on each half. The two halves expose a common column set so
+ the result is shape-compatible (a literal `kind` discriminator + a `name` alias + duration/fps
+ that are NULL for images).
+ """
+
+ __invoker: Invoker
+
+ def __init__(self, db: SqliteDatabase) -> None:
+ super().__init__()
+ self._db = db
+
+ def start(self, invoker: Invoker) -> None:
+ self.__invoker = invoker
+
+ def list_items(
+ self,
+ offset: int = 0,
+ limit: int = 10,
+ starred_first: bool = True,
+ order_dir: SQLiteDirection = SQLiteDirection.Descending,
+ origin: Optional[ResourceOrigin] = None,
+ categories: Optional[list[ImageCategory]] = None,
+ is_intermediate: Optional[bool] = None,
+ board_id: Optional[str] = None,
+ search_term: Optional[str] = None,
+ user_id: Optional[str] = None,
+ is_admin: bool = False,
+ ) -> OffsetPaginatedResults[GalleryItem]:
+ image_half, image_params, image_count_query = self._build_half(
+ kind="image",
+ origin=origin,
+ categories=categories,
+ is_intermediate=is_intermediate,
+ board_id=board_id,
+ search_term=search_term,
+ user_id=user_id,
+ is_admin=is_admin,
+ )
+ video_half, video_params, video_count_query = self._build_half(
+ kind="video",
+ origin=origin,
+ categories=categories,
+ is_intermediate=is_intermediate,
+ board_id=board_id,
+ search_term=search_term,
+ user_id=user_id,
+ is_admin=is_admin,
+ )
+
+ order_clause = self._build_order_clause(starred_first, order_dir)
+
+ union_query = f"""--sql
+ SELECT * FROM (
+ {image_half}
+ UNION ALL
+ {video_half}
+ )
+ {order_clause}
+ LIMIT ? OFFSET ?
+ ;
+ """
+
+ with self._db.transaction() as cursor:
+ cursor.execute(union_query, image_params + video_params + [limit, offset])
+ rows = cast(list[sqlite3.Row], cursor.fetchall())
+
+ cursor.execute(image_count_query, image_params)
+ image_count = cast(int, cursor.fetchone()[0])
+ cursor.execute(video_count_query, video_params)
+ video_count = cast(int, cursor.fetchone()[0])
+
+ urls = self.__invoker.services.urls
+ items = [self._row_to_item(row, urls) for row in rows]
+ return OffsetPaginatedResults[GalleryItem](
+ items=items,
+ offset=offset,
+ limit=limit,
+ total=image_count + video_count,
+ )
+
+ def list_item_names(
+ self,
+ starred_first: bool = True,
+ order_dir: SQLiteDirection = SQLiteDirection.Descending,
+ origin: Optional[ResourceOrigin] = None,
+ categories: Optional[list[ImageCategory]] = None,
+ is_intermediate: Optional[bool] = None,
+ board_id: Optional[str] = None,
+ search_term: Optional[str] = None,
+ user_id: Optional[str] = None,
+ is_admin: bool = False,
+ created_date: Optional[str] = None,
+ ) -> GalleryItemNamesResult:
+ image_half, image_params, _ = self._build_half(
+ kind="image",
+ origin=origin,
+ categories=categories,
+ is_intermediate=is_intermediate,
+ board_id=board_id,
+ search_term=search_term,
+ user_id=user_id,
+ is_admin=is_admin,
+ names_only=True,
+ created_date=created_date,
+ )
+ video_half, video_params, _ = self._build_half(
+ kind="video",
+ origin=origin,
+ categories=categories,
+ is_intermediate=is_intermediate,
+ board_id=board_id,
+ search_term=search_term,
+ user_id=user_id,
+ is_admin=is_admin,
+ names_only=True,
+ created_date=created_date,
+ )
+
+ order_clause = self._build_order_clause(starred_first, order_dir)
+
+ union_query = f"""--sql
+ SELECT * FROM (
+ {image_half}
+ UNION ALL
+ {video_half}
+ )
+ {order_clause}
+ ;
+ """
+
+ with self._db.transaction() as cursor:
+ cursor.execute(union_query, image_params + video_params)
+ rows = cast(list[sqlite3.Row], cursor.fetchall())
+
+ starred_count = 0
+ if starred_first:
+ starred_count = sum(1 for r in rows if r["starred"])
+
+ refs = [GalleryItemRef(kind=GalleryItemKind(row["kind"]), name=row["name"]) for row in rows]
+ return GalleryItemNamesResult(items=refs, starred_count=starred_count, total_count=len(refs))
+
+ def get_dates(
+ self,
+ user_id: Optional[str] = None,
+ is_admin: bool = False,
+ ) -> list[VirtualSubBoardDTO]:
+ image_conditions = " AND images.is_intermediate = 0 "
+ video_conditions = " AND videos.is_intermediate = 0 "
+ image_params: list[Union[int, str, bool]] = []
+ video_params: list[Union[int, str, bool]] = []
+
+ if user_id is not None and not is_admin:
+ image_conditions += " AND images.user_id = ? "
+ image_params.append(user_id)
+ video_conditions += " AND videos.user_id = ? "
+ video_params.append(user_id)
+
+ union = f"""--sql
+ SELECT
+ images.created_at AS created_at,
+ 'image' AS kind,
+ images.image_name AS name,
+ images.image_category AS category
+ FROM images
+ WHERE 1=1 {image_conditions}
+ UNION ALL
+ SELECT
+ videos.created_at AS created_at,
+ 'video' AS kind,
+ videos.video_name AS name,
+ videos.video_category AS category
+ FROM videos
+ WHERE 1=1 {video_conditions}
+ """
+
+ counts_query = f"""--sql
+ SELECT
+ DATE(created_at) AS date,
+ SUM(CASE WHEN kind = 'image' AND category = 'general' THEN 1 ELSE 0 END) AS image_count,
+ SUM(CASE WHEN kind = 'image' AND category != 'general' THEN 1 ELSE 0 END) AS asset_count,
+ SUM(CASE WHEN kind = 'video' THEN 1 ELSE 0 END) AS video_count
+ FROM ({union})
+ GROUP BY DATE(created_at)
+ ORDER BY date DESC;
+ """
+
+ # The cover is the newest item of each date. ROW_NUMBER with kind/name tie-breakers
+ # (rather than a bare-column MAX() aggregate) keeps the choice deterministic when
+ # several items share the maximum timestamp — otherwise the cover could flicker
+ # between equally-new items across refetches.
+ covers_query = f"""--sql
+ SELECT date, kind, name FROM (
+ SELECT
+ DATE(created_at) AS date,
+ kind,
+ name,
+ ROW_NUMBER() OVER (
+ PARTITION BY DATE(created_at)
+ ORDER BY created_at DESC, kind DESC, name DESC
+ ) AS rn
+ FROM ({union})
+ )
+ WHERE rn = 1;
+ """
+
+ with self._db.transaction() as cursor:
+ cursor.execute(counts_query, image_params + video_params)
+ count_rows = cast(list[sqlite3.Row], cursor.fetchall())
+ cursor.execute(covers_query, image_params + video_params)
+ cover_rows = cast(list[sqlite3.Row], cursor.fetchall())
+
+ covers = {row["date"]: (row["kind"], row["name"]) for row in cover_rows}
+
+ boards: list[VirtualSubBoardDTO] = []
+ for row in count_rows:
+ date = row["date"]
+ cover_kind, cover_name = covers.get(date, (None, None))
+ boards.append(
+ VirtualSubBoardDTO(
+ virtual_board_id=f"by_date:{date}",
+ board_name=date,
+ date=date,
+ image_count=row["image_count"],
+ asset_count=row["asset_count"],
+ video_count=row["video_count"],
+ cover_image_name=cover_name if cover_kind == "image" else None,
+ cover_video_name=cover_name if cover_kind == "video" else None,
+ )
+ )
+ return boards
+
+ @staticmethod
+ def _build_order_clause(starred_first: bool, order_dir: SQLiteDirection) -> str:
+ """Ordering with `kind` + `name` tie-breakers.
+
+ Images and videos created within the same timestamp granularity would otherwise
+ have no defined relative order — rows could reorder across refetches or shift
+ between offset pages. The tie-breakers follow `order_dir` so ascending and
+ descending remain exact mirror images.
+ """
+ direction = order_dir.value
+ tie_breakers = f"kind {direction}, name {direction}"
+ if starred_first:
+ return f"ORDER BY starred DESC, created_at {direction}, {tie_breakers}"
+ return f"ORDER BY created_at {direction}, {tie_breakers}"
+
+ def _build_half(
+ self,
+ kind: str,
+ origin: Optional[ResourceOrigin],
+ categories: Optional[list[ImageCategory]],
+ is_intermediate: Optional[bool],
+ board_id: Optional[str],
+ search_term: Optional[str],
+ user_id: Optional[str],
+ is_admin: bool,
+ names_only: bool = False,
+ created_date: Optional[str] = None,
+ ) -> tuple[str, list[Union[int, str, bool]], str]:
+ """Builds one half of the union (either `images` or `videos`).
+
+ Returns `(query_with_select, params, count_query)`. Both halves emit the same columns so
+ UNION ALL is shape-compatible: `kind`, `name`, `width`, `height`, `category`, `starred`,
+ `is_intermediate`, `board_id`, `created_at`, `duration`, `fps`.
+
+ `names_only=True` selects only `kind`, `name`, `starred`, `created_at` (the minimum needed
+ for ordering + the counts result).
+ """
+ if kind == "image":
+ base_table = "images"
+ join_table = "board_images"
+ name_col = "image_name"
+ category_col = "image_category"
+ origin_col = "image_origin"
+ duration_expr = "NULL"
+ fps_expr = "NULL"
+ elif kind == "video":
+ base_table = "videos"
+ join_table = "board_videos"
+ name_col = "video_name"
+ category_col = "video_category"
+ origin_col = "video_origin"
+ duration_expr = f"{base_table}.duration"
+ fps_expr = f"{base_table}.fps"
+ else:
+ raise ValueError(f"Unknown kind: {kind}")
+
+ if names_only:
+ select_cols = (
+ f"'{kind}' AS kind, "
+ f"{base_table}.{name_col} AS name, "
+ f"{base_table}.starred AS starred, "
+ f"{base_table}.created_at AS created_at"
+ )
+ else:
+ select_cols = (
+ f"'{kind}' AS kind, "
+ f"{base_table}.{name_col} AS name, "
+ f"{base_table}.width AS width, "
+ f"{base_table}.height AS height, "
+ f"{base_table}.{category_col} AS category, "
+ f"{base_table}.starred AS starred, "
+ f"{base_table}.is_intermediate AS is_intermediate, "
+ f"{join_table}.board_id AS board_id, "
+ f"{base_table}.created_at AS created_at, "
+ f"{duration_expr} AS duration, "
+ f"{fps_expr} AS fps"
+ )
+
+ from_clause = f"FROM {base_table} LEFT JOIN {join_table} ON {join_table}.{name_col} = {base_table}.{name_col}"
+
+ conditions = ""
+ params: list[Union[int, str, bool]] = []
+
+ if origin is not None:
+ conditions += f" AND {base_table}.{origin_col} = ? "
+ params.append(origin.value)
+
+ if categories is not None:
+ category_strings = [c.value for c in set(categories)]
+ placeholders = ",".join("?" * len(category_strings))
+ conditions += f" AND {base_table}.{category_col} IN ( {placeholders} ) "
+ for c in category_strings:
+ params.append(c)
+
+ if is_intermediate is not None:
+ conditions += f" AND {base_table}.is_intermediate = ? "
+ params.append(is_intermediate)
+
+ if created_date is not None:
+ conditions += f" AND DATE({base_table}.created_at) = ? "
+ params.append(created_date)
+
+ if board_id == "none":
+ conditions += f" AND {join_table}.board_id IS NULL "
+ if user_id is not None and not is_admin:
+ conditions += f" AND {base_table}.user_id = ? "
+ params.append(user_id)
+ elif board_id is not None:
+ conditions += f" AND {join_table}.board_id = ? "
+ params.append(board_id)
+ elif user_id is not None and not is_admin:
+ # No board_id supplied — still enforce per-user isolation so
+ # non-admin callers cannot enumerate other users' items.
+ conditions += f" AND {base_table}.user_id = ? "
+ params.append(user_id)
+
+ if search_term:
+ conditions += f" AND ({base_table}.metadata LIKE ? OR {base_table}.created_at LIKE ?) "
+ params.append(f"%{search_term.lower()}%")
+ params.append(f"%{search_term.lower()}%")
+
+ half_query = f"SELECT {select_cols} {from_clause} WHERE 1=1 {conditions}"
+ count_query = f"SELECT COUNT(*) {from_clause} WHERE 1=1 {conditions}"
+ return half_query, params, count_query
+
+ def _row_to_item(self, row: sqlite3.Row, urls) -> GalleryItem:
+ kind = GalleryItemKind(row["kind"])
+ name = row["name"]
+ if kind == GalleryItemKind.IMAGE:
+ full_url = urls.get_image_url(name)
+ thumbnail_url = urls.get_image_url(name, thumbnail=True)
+ duration = None
+ fps = None
+ else:
+ full_url = urls.get_video_url(name)
+ thumbnail_url = urls.get_video_url(name, thumbnail=True)
+ duration = row["duration"]
+ fps = row["fps"]
+ return GalleryItem(
+ kind=kind,
+ name=name,
+ full_url=full_url,
+ thumbnail_url=thumbnail_url,
+ width=row["width"],
+ height=row["height"],
+ category=ImageCategory(row["category"]),
+ starred=bool(row["starred"]),
+ is_intermediate=bool(row["is_intermediate"]),
+ board_id=row["board_id"],
+ created_at=row["created_at"],
+ duration=duration,
+ fps=fps,
+ )
diff --git a/invokeai/app/services/image_files/image_files_base.py b/invokeai/app/services/image_files/image_files_base.py
index 764efd8833c..782e661c4b0 100644
--- a/invokeai/app/services/image_files/image_files_base.py
+++ b/invokeai/app/services/image_files/image_files_base.py
@@ -61,6 +61,21 @@ def delete(self, image_name: str, image_subfolder: str = "") -> None:
"""Deletes an image and its thumbnail (if one exists)."""
pass
+ @abstractmethod
+ def stage_delete(self, image_name: str, image_subfolder: str = "") -> object:
+ """Moves an image's files out of service and returns a rollback token."""
+ pass
+
+ @abstractmethod
+ def commit_delete(self, token: object) -> None:
+ """Permanently removes files represented by a staged-delete token."""
+ pass
+
+ @abstractmethod
+ def rollback_delete(self, token: object) -> None:
+ """Restores files represented by a staged-delete token."""
+ pass
+
@abstractmethod
def get_workflow(self, image_name: str, image_subfolder: str = "") -> Optional[str]:
"""Gets the workflow of an image."""
diff --git a/invokeai/app/services/image_files/image_files_disk.py b/invokeai/app/services/image_files/image_files_disk.py
index 929775a07f0..7b9028c364f 100644
--- a/invokeai/app/services/image_files/image_files_disk.py
+++ b/invokeai/app/services/image_files/image_files_disk.py
@@ -1,6 +1,11 @@
# Copyright (c) 2022 Kyle Schouviller (https://github.com/kyle0654) and the InvokeAI Team
import io
+import json
+import os
+import shutil
+import tempfile
import zlib
+from dataclasses import dataclass
from pathlib import Path
from queue import Queue
from typing import Optional, Union
@@ -16,6 +21,7 @@
)
from invokeai.app.services.invoker import Invoker
from invokeai.app.util.thumbnails import get_thumbnail_name, make_thumbnail
+from invokeai.backend.util.logging import InvokeAILogger
_PNG_RLE_MIN_PIXELS = 512 * 512
_PNG_RLE_SAMPLE_TILE = 32
@@ -23,6 +29,12 @@
_PNG_RLE_MAX_SAMPLE_SIZE_PERCENT = 102
+@dataclass
+class _StagedDelete:
+ directory: Path
+ files: list[tuple[Path, Path]]
+
+
def _get_png_size(image: PILImageType, compress_type: Optional[int] = None) -> int:
output = io.BytesIO()
options = {"compress_level": 1}
@@ -73,6 +85,7 @@ def __init__(self, output_folder: Union[str, Path]):
def start(self, invoker: Invoker) -> None:
self.__invoker = invoker
+ self.__recover_staged_deletes()
@property
def image_root(self) -> Path:
@@ -157,20 +170,53 @@ def save(
raise ImageFileSaveException from e
def delete(self, image_name: str, image_subfolder: str = "") -> None:
+ token = self.stage_delete(image_name, image_subfolder)
+ self.commit_delete(token)
+
+ def stage_delete(self, image_name: str, image_subfolder: str = "") -> _StagedDelete:
+ candidates = [
+ self.get_path(image_name, image_subfolder=image_subfolder),
+ self.get_path(image_name, thumbnail=True, image_subfolder=image_subfolder),
+ ]
+ staging_dir = Path(tempfile.mkdtemp(prefix=".delete_", dir=self.__output_folder))
+ staged: list[tuple[Path, Path]] = []
try:
- image_path = self.get_path(image_name, image_subfolder=image_subfolder)
-
- if image_path.exists():
- image_path.unlink()
- if image_path in self.__cache:
- del self.__cache[image_path]
+ with open(staging_dir / "manifest.json", "w", encoding="utf-8") as manifest:
+ manifest.write(json.dumps({"image_name": image_name, "image_subfolder": image_subfolder}))
+ manifest.flush()
+ os.fsync(manifest.fileno())
+ for index, source in enumerate(candidates):
+ self.__cache.pop(source, None)
+ if source.exists():
+ destination = staging_dir / str(index)
+ source.replace(destination)
+ staged.append((source, destination))
+ return _StagedDelete(directory=staging_dir, files=staged)
+ except Exception as e:
+ for source, destination in reversed(staged):
+ if destination.exists():
+ source.parent.mkdir(parents=True, exist_ok=True)
+ destination.replace(source)
+ shutil.rmtree(staging_dir, ignore_errors=True)
+ raise ImageFileDeleteException from e
- thumbnail_path = self.get_path(image_name, True, image_subfolder=image_subfolder)
+ def commit_delete(self, token: object) -> None:
+ if not isinstance(token, _StagedDelete):
+ raise ImageFileDeleteException("Invalid staged-delete token")
+ try:
+ shutil.rmtree(token.directory)
+ except Exception as e:
+ raise ImageFileDeleteException from e
- if thumbnail_path.exists():
- thumbnail_path.unlink()
- if thumbnail_path in self.__cache:
- del self.__cache[thumbnail_path]
+ def rollback_delete(self, token: object) -> None:
+ if not isinstance(token, _StagedDelete):
+ raise ImageFileDeleteException("Invalid staged-delete token")
+ try:
+ for source, destination in reversed(token.files):
+ if destination.exists():
+ source.parent.mkdir(parents=True, exist_ok=True)
+ destination.replace(source)
+ shutil.rmtree(token.directory, ignore_errors=True)
except Exception as e:
raise ImageFileDeleteException from e
@@ -240,6 +286,37 @@ def __validate_storage_folders(self) -> None:
for folder in folders:
folder.mkdir(parents=True, exist_ok=True)
+ def __recover_staged_deletes(self) -> None:
+ logger = InvokeAILogger.get_logger()
+ for staging_dir in self.__output_folder.glob(".delete_*"):
+ manifest_path = staging_dir / "manifest.json"
+ if not manifest_path.is_file():
+ if not any(staging_dir.iterdir()):
+ staging_dir.rmdir()
+ continue
+ try:
+ with open(manifest_path, encoding="utf-8") as manifest:
+ data = json.load(manifest)
+ image_name = data["image_name"]
+ image_subfolder = data.get("image_subfolder", "")
+ candidates = [
+ self.get_path(image_name, image_subfolder=image_subfolder),
+ self.get_path(image_name, thumbnail=True, image_subfolder=image_subfolder),
+ ]
+ token = _StagedDelete(
+ directory=staging_dir,
+ files=[(source, staging_dir / str(index)) for index, source in enumerate(candidates)],
+ )
+ self.__invoker.services.image_records.get(image_name)
+ self.rollback_delete(token)
+ except Exception as error:
+ from invokeai.app.services.image_records.image_records_common import ImageRecordNotFoundException
+
+ if isinstance(error, ImageRecordNotFoundException):
+ shutil.rmtree(staging_dir, ignore_errors=True)
+ else:
+ logger.error(f"Failed to recover staged image deletion {staging_dir}: {error}")
+
def __get_cache(self, image_name: Path) -> Optional[PILImageType]:
return None if image_name not in self.__cache else self.__cache[image_name]
diff --git a/invokeai/app/services/images/images_base.py b/invokeai/app/services/images/images_base.py
index aebbead2f35..000c8a43fc7 100644
--- a/invokeai/app/services/images/images_base.py
+++ b/invokeai/app/services/images/images_base.py
@@ -148,8 +148,17 @@ def get_intermediates_count(self, user_id: Optional[str] = None) -> int:
pass
@abstractmethod
- def delete_images_on_board(self, board_id: str):
- """Deletes all images on a board."""
+ def delete_images_on_board(self, board_id: str, user_id: Optional[str] = None) -> tuple[list[str], list[str]]:
+ """Deletes all images on a board; returns ``(deleted_names, failed_names)``.
+
+ When ``user_id`` is provided, only images owned by that user are deleted (other users'
+ contributions to a public/shared board are preserved). Pass ``None`` for the admin
+ path to delete every image on the board regardless of uploader.
+
+ ``failed_names`` is the service's ground truth for per-image failures — callers must
+ not reconstruct it by diffing their own board listing against ``deleted_names``, which
+ races with concurrent moves/deletes.
+ """
pass
@abstractmethod
diff --git a/invokeai/app/services/images/images_common.py b/invokeai/app/services/images/images_common.py
index 311f6e556d2..51679b43f4c 100644
--- a/invokeai/app/services/images/images_common.py
+++ b/invokeai/app/services/images/images_common.py
@@ -47,6 +47,10 @@ class ResultWithAffectedBoards(BaseModel):
class DeleteImagesResult(ResultWithAffectedBoards):
deleted_images: list[str] = Field(description="The names of the images that were deleted")
+ failed_images: list[str] = Field(
+ default_factory=list,
+ description="The names of authorized images that could not be deleted",
+ )
class StarredImagesResult(ResultWithAffectedBoards):
diff --git a/invokeai/app/services/images/images_default.py b/invokeai/app/services/images/images_default.py
index 4a190f37edc..9fded083cbc 100644
--- a/invokeai/app/services/images/images_default.py
+++ b/invokeai/app/services/images/images_default.py
@@ -291,22 +291,51 @@ def delete(self, image_name: str):
self.__invoker.services.logger.error("Problem deleting image record and file")
raise e
- def delete_images_on_board(self, board_id: str):
+ def delete_images_on_board(self, board_id: str, user_id: Optional[str] = None) -> tuple[list[str], list[str]]:
try:
+ # When ``user_id`` is set the lookup filters to images owned by that user so the
+ # cascade doesn't destroy other users' contributions to a public/shared board.
image_names = self.__invoker.services.board_image_records.get_all_board_image_names_for_board(
board_id,
categories=None,
is_intermediate=None,
+ user_id=user_id,
)
+ deleted_image_names: list[str] = []
+ failed_image_names: list[str] = []
+ staged_deletes: list[tuple[str, object]] = []
for image_name in image_names:
try:
record = self.__invoker.services.image_records.get(image_name)
- self.__invoker.services.image_files.delete(image_name, image_subfolder=record.image_subfolder)
- except Exception:
- pass
- self.__invoker.services.image_records.delete_many(image_names)
- for image_name in image_names:
+ token = self.__invoker.services.image_files.stage_delete(
+ image_name, image_subfolder=record.image_subfolder
+ )
+ staged_deletes.append((image_name, token))
+ deleted_image_names.append(image_name)
+ except Exception as e:
+ failed_image_names.append(image_name)
+ self.__invoker.services.logger.error(
+ f"Failed to delete image file {image_name}; keeping record: {str(e)}"
+ )
+ try:
+ self.__invoker.services.image_records.delete_many(deleted_image_names)
+ except Exception:
+ for image_name, token in staged_deletes:
+ try:
+ self.__invoker.services.image_files.rollback_delete(token)
+ except Exception as rollback_error:
+ self.__invoker.services.logger.error(
+ f"Failed to restore staged image files for {image_name}: {rollback_error}"
+ )
+ raise
+ for _, token in staged_deletes:
+ try:
+ self.__invoker.services.image_files.commit_delete(token)
+ except Exception as cleanup_error:
+ self.__invoker.services.logger.error(f"Failed to purge staged image files: {cleanup_error}")
+ for image_name in deleted_image_names:
self._on_deleted(image_name)
+ return deleted_image_names, failed_image_names
except ImageRecordDeleteException:
self.__invoker.services.logger.error("Failed to delete image records")
raise
diff --git a/invokeai/app/services/invocation_cache/invocation_cache_memory.py b/invokeai/app/services/invocation_cache/invocation_cache_memory.py
index d15269caf91..155e7d18ab7 100644
--- a/invokeai/app/services/invocation_cache/invocation_cache_memory.py
+++ b/invokeai/app/services/invocation_cache/invocation_cache_memory.py
@@ -37,6 +37,7 @@ def start(self, invoker: Invoker) -> None:
if self._max_cache_size == 0:
return
self._invoker.services.images.on_deleted(self._delete_by_match)
+ self._invoker.services.videos.on_deleted(self._delete_by_match)
self._invoker.services.tensors.on_deleted(self._delete_by_match)
self._invoker.services.conditioning.on_deleted(self._delete_by_match)
diff --git a/invokeai/app/services/invocation_services.py b/invokeai/app/services/invocation_services.py
index 54f9d82b786..eb80237659a 100644
--- a/invokeai/app/services/invocation_services.py
+++ b/invokeai/app/services/invocation_services.py
@@ -15,6 +15,7 @@
from invokeai.app.services.board_image_records.board_image_records_base import BoardImageRecordStorageBase
from invokeai.app.services.board_images.board_images_base import BoardImagesServiceABC
from invokeai.app.services.board_records.board_records_base import BoardRecordStorageBase
+ from invokeai.app.services.board_video_records.board_video_records_base import BoardVideoRecordStorageBase
from invokeai.app.services.boards.boards_base import BoardServiceABC
from invokeai.app.services.bulk_download.bulk_download_base import BulkDownloadBase
from invokeai.app.services.client_state_persistence.client_state_persistence_base import ClientStatePersistenceABC
@@ -22,6 +23,7 @@
from invokeai.app.services.download import DownloadQueueServiceBase
from invokeai.app.services.events.events_base import EventServiceBase
from invokeai.app.services.external_generation.external_generation_base import ExternalGenerationServiceBase
+ from invokeai.app.services.gallery.gallery_base import GalleryServiceABC
from invokeai.app.services.image_files.image_files_base import ImageFileStorageBase
from invokeai.app.services.image_moves.image_moves_default import ImageMoveService
from invokeai.app.services.image_records.image_records_base import ImageRecordStorageBase
@@ -39,6 +41,9 @@
from invokeai.app.services.session_queue.session_queue_base import SessionQueueBase
from invokeai.app.services.urls.urls_base import UrlServiceBase
from invokeai.app.services.users.users_base import UserServiceBase
+ from invokeai.app.services.video_files.video_files_base import VideoFileStorageBase
+ from invokeai.app.services.video_records.video_records_base import VideoRecordStorageBase
+ from invokeai.app.services.videos.videos_base import VideoServiceABC
from invokeai.app.services.workflow_records.workflow_records_base import WorkflowRecordsStorageBase
from invokeai.app.services.workflow_thumbnails.workflow_thumbnails_base import WorkflowThumbnailServiceBase
from invokeai.backend.stable_diffusion.diffusion.conditioning_data import ConditioningFieldData
@@ -80,6 +85,11 @@ def __init__(
workflow_thumbnails: "WorkflowThumbnailServiceBase",
client_state_persistence: "ClientStatePersistenceABC",
users: "UserServiceBase",
+ videos: "VideoServiceABC",
+ video_files: "VideoFileStorageBase",
+ video_records: "VideoRecordStorageBase",
+ board_video_records: "BoardVideoRecordStorageBase",
+ gallery: "GalleryServiceABC",
image_moves: "ImageMoveService | None" = None,
):
self.board_images = board_images
@@ -114,3 +124,8 @@ def __init__(
self.workflow_thumbnails = workflow_thumbnails
self.client_state_persistence = client_state_persistence
self.users = users
+ self.videos = videos
+ self.video_files = video_files
+ self.video_records = video_records
+ self.board_video_records = board_video_records
+ self.gallery = gallery
diff --git a/invokeai/app/services/model_records/model_records_base.py b/invokeai/app/services/model_records/model_records_base.py
index e06f8f2df91..4d5a9d102ca 100644
--- a/invokeai/app/services/model_records/model_records_base.py
+++ b/invokeai/app/services/model_records/model_records_base.py
@@ -33,6 +33,8 @@
Qwen3VariantType,
QwenImageVariantType,
SchedulerPredictionType,
+ WanLoRAVariantType,
+ WanVariantType,
ZImageVariantType,
)
@@ -134,6 +136,8 @@ def validate_source_url(cls, v: Any) -> Optional[str]:
| Flux2VariantType
| ZImageVariantType
| QwenImageVariantType
+ | WanVariantType
+ | WanLoRAVariantType
| Qwen3VariantType
] = Field(description="The variant of the model.", default=None)
prediction_type: Optional[SchedulerPredictionType] = Field(
diff --git a/invokeai/app/services/names/names_base.py b/invokeai/app/services/names/names_base.py
index f892c43c55a..fd97007ced3 100644
--- a/invokeai/app/services/names/names_base.py
+++ b/invokeai/app/services/names/names_base.py
@@ -9,3 +9,8 @@ class NameServiceBase(ABC):
def create_image_name(self) -> str:
"""Creates a name for an image."""
pass
+
+ @abstractmethod
+ def create_video_name(self) -> str:
+ """Creates a name for a video."""
+ pass
diff --git a/invokeai/app/services/names/names_default.py b/invokeai/app/services/names/names_default.py
index 5804a937d6a..d43ed4864cd 100644
--- a/invokeai/app/services/names/names_default.py
+++ b/invokeai/app/services/names/names_default.py
@@ -10,3 +10,8 @@ def create_image_name(self) -> str:
uuid_str = uuid_string()
filename = f"{uuid_str}.png"
return filename
+
+ def create_video_name(self) -> str:
+ uuid_str = uuid_string()
+ filename = f"{uuid_str}.mp4"
+ return filename
diff --git a/invokeai/app/services/session_queue/session_queue_common.py b/invokeai/app/services/session_queue/session_queue_common.py
index 8d068b3fa65..a7536202a6b 100644
--- a/invokeai/app/services/session_queue/session_queue_common.py
+++ b/invokeai/app/services/session_queue/session_queue_common.py
@@ -15,7 +15,7 @@
)
from pydantic_core import to_jsonable_python
-from invokeai.app.invocations.fields import ImageField
+from invokeai.app.invocations.fields import ImageField, VideoField
from invokeai.app.services.shared.graph import Graph, GraphExecutionState, NodeNotFoundError
from invokeai.app.services.workflow_records.workflow_records_common import (
WorkflowWithoutID,
@@ -51,7 +51,7 @@ class SessionQueueItemNotFoundError(ValueError):
# region Batch
-BatchScalarDataType = Union[StrictStr, float, int, ImageField]
+BatchScalarDataType = Union[StrictStr, float, int, ImageField, VideoField]
BatchDataType = Union[BatchScalarDataType, list[BatchScalarDataType]]
diff --git a/invokeai/app/services/shared/invocation_context.py b/invokeai/app/services/shared/invocation_context.py
index 9c142359bc5..5a9756619de 100644
--- a/invokeai/app/services/shared/invocation_context.py
+++ b/invokeai/app/services/shared/invocation_context.py
@@ -9,7 +9,7 @@
from invokeai.app.invocations.constants import IMAGE_MODES
from invokeai.app.invocations.fields import MetadataField, WithBoard, WithMetadata
-from invokeai.app.services.board_records.board_records_common import BoardRecordOrderBy
+from invokeai.app.services.board_records.board_records_common import BoardRecordOrderBy, BoardVisibility
from invokeai.app.services.boards.boards_common import BoardDTO
from invokeai.app.services.config.config_default import InvokeAIAppConfig
from invokeai.app.services.image_records.image_records_common import ImageCategory, ResourceOrigin
@@ -18,6 +18,7 @@
from invokeai.app.services.model_records.model_records_base import UnknownModelException
from invokeai.app.services.session_processor.session_processor_common import ProgressImage
from invokeai.app.services.shared.sqlite.sqlite_common import SQLiteDirection
+from invokeai.app.services.videos.videos_common import VideoDTO
from invokeai.app.util.step_callback import diffusion_step_callback
from invokeai.backend.model_manager.configs.base import Config_Base
from invokeai.backend.model_manager.configs.factory import AnyModelConfig
@@ -169,6 +170,22 @@ def __init__(self, services: InvocationServices, data: InvocationContextData, ut
super().__init__(services, data)
self._util = util
+ def _assert_read_access(self, image_name: str) -> None:
+ if not self._services.configuration.multiuser:
+ return
+ user_id = self._data.queue_item.user_id
+ user = self._services.users.get(user_id)
+ if user is None:
+ raise PermissionError("Queue user is not authorized to access this image")
+ if user.is_admin or self._services.image_records.get_user_id(image_name) == user_id:
+ return
+ board_id = self._services.board_image_records.get_board_for_image(image_name)
+ if board_id is not None:
+ board = self._services.boards.get_dto(board_id)
+ if board.board_visibility in (BoardVisibility.Shared, BoardVisibility.Public):
+ return
+ raise PermissionError("Queue user is not authorized to access this image")
+
def save(
self,
image: Image,
@@ -211,6 +228,16 @@ def save(
elif isinstance(self._data.invocation, WithBoard) and self._data.invocation.board:
board_id_ = self._data.invocation.board.board_id
+ if board_id_ is not None and self._services.configuration.multiuser:
+ board = self._services.boards.get_dto(board_id_)
+ user = self._services.users.get(self._data.queue_item.user_id)
+ if user is None or (
+ not user.is_admin
+ and board.user_id != self._data.queue_item.user_id
+ and board.board_visibility != BoardVisibility.Public
+ ):
+ raise PermissionError("Queue user is not authorized to save images to this board")
+
workflow_ = None
if self._data.queue_item.workflow:
workflow_ = self._data.queue_item.workflow.model_dump_json()
@@ -243,6 +270,7 @@ def get_pil(self, image_name: str, mode: IMAGE_MODES | None = None) -> Image:
Returns:
The image as a PIL Image object.
"""
+ self._assert_read_access(image_name)
image = self._services.images.get_pil_image(image_name)
if mode and mode != image.mode:
try:
@@ -266,6 +294,7 @@ def get_metadata(self, image_name: str) -> Optional[MetadataField]:
Returns:
The image's metadata, if it has any.
"""
+ self._assert_read_access(image_name)
return self._services.images.get_metadata(image_name)
def get_dto(self, image_name: str) -> ImageDTO:
@@ -277,6 +306,7 @@ def get_dto(self, image_name: str) -> ImageDTO:
Returns:
The image as an ImageDTO object.
"""
+ self._assert_read_access(image_name)
return self._services.images.get_dto(image_name)
def get_path(self, image_name: str, thumbnail: bool = False) -> Path:
@@ -289,9 +319,121 @@ def get_path(self, image_name: str, thumbnail: bool = False) -> Path:
Returns:
The local path of the image or thumbnail.
"""
+ self._assert_read_access(image_name)
return Path(self._services.images.get_path(image_name, thumbnail))
+class VideosInterface(InvocationContextInterface):
+ """Save and look up videos produced by invocations.
+
+ Mirrors :class:`ImagesInterface` but consumes a path to an already-encoded
+ MP4 (or other supported container) rather than an in-memory PIL image —
+ video encoding is the caller's responsibility (e.g. the
+ ``wan_latents_to_video`` node uses ``imageio[ffmpeg]``).
+ """
+
+ def __init__(self, services: InvocationServices, data: InvocationContextData, util: "UtilInterface") -> None:
+ super().__init__(services, data)
+ self._util = util
+
+ def _assert_read_access(self, video_name: str) -> None:
+ if not self._services.configuration.multiuser:
+ return
+ user_id = self._data.queue_item.user_id
+ user = self._services.users.get(user_id)
+ if user is None:
+ raise PermissionError("Queue user is not authorized to access this video")
+ if user.is_admin or self._services.video_records.get_user_id(video_name) == user_id:
+ return
+ board_id = self._services.board_video_records.get_board_for_video(video_name)
+ if board_id is not None:
+ board = self._services.boards.get_dto(board_id)
+ if board.board_visibility in (BoardVisibility.Shared, BoardVisibility.Public):
+ return
+ raise PermissionError("Queue user is not authorized to access this video")
+
+ def save(
+ self,
+ source_path: Path,
+ width: int,
+ height: int,
+ duration: float,
+ fps: Optional[float] = None,
+ board_id: Optional[str] = None,
+ image_category: ImageCategory = ImageCategory.GENERAL,
+ metadata: Optional[MetadataField] = None,
+ ) -> VideoDTO:
+ """Save a video produced by an invocation. The file at ``source_path`` is moved into
+ the videos output folder; the caller should treat the path as consumed after this
+ returns.
+
+ ``board_id`` falls back to the invocation's :class:`WithBoard` mixin if unset, and
+ ``metadata`` falls back to the :class:`WithMetadata` mixin. Both can be overridden
+ explicitly. ``image_category`` reuses the image enum since the gallery's category
+ filter is shared between kinds.
+ """
+
+ self._util.signal_progress("Saving video")
+
+ metadata_ = None
+ if metadata:
+ metadata_ = metadata.model_dump_json()
+ elif isinstance(self._data.invocation, WithMetadata) and self._data.invocation.metadata:
+ metadata_ = self._data.invocation.metadata.model_dump_json()
+
+ board_id_ = None
+ if board_id:
+ board_id_ = board_id
+ elif isinstance(self._data.invocation, WithBoard) and self._data.invocation.board:
+ board_id_ = self._data.invocation.board.board_id
+
+ if board_id_ is not None and self._services.configuration.multiuser:
+ board = self._services.boards.get_dto(board_id_)
+ user = self._services.users.get(self._data.queue_item.user_id)
+ if user is None or (
+ not user.is_admin
+ and board.user_id != self._data.queue_item.user_id
+ and board.board_visibility != BoardVisibility.Public
+ ):
+ raise PermissionError("Queue user is not authorized to save videos to this board")
+
+ workflow_ = None
+ if self._data.queue_item.workflow:
+ workflow_ = self._data.queue_item.workflow.model_dump_json()
+
+ graph_ = None
+ if self._data.queue_item.session.graph:
+ graph_ = self._data.queue_item.session.graph.model_dump_json()
+
+ return self._services.videos.create(
+ source_path=source_path,
+ width=width,
+ height=height,
+ duration=duration,
+ fps=fps,
+ is_intermediate=self._data.invocation.is_intermediate,
+ video_category=image_category,
+ board_id=board_id_,
+ metadata=metadata_,
+ video_origin=ResourceOrigin.INTERNAL,
+ workflow=workflow_,
+ graph=graph_,
+ session_id=self._data.queue_item.session_id,
+ node_id=self._data.invocation.id,
+ user_id=self._data.queue_item.user_id,
+ )
+
+ def get_dto(self, video_name: str) -> VideoDTO:
+ """Get a video DTO by name."""
+ self._assert_read_access(video_name)
+ return self._services.videos.get_dto(video_name)
+
+ def get_path(self, video_name: str, thumbnail: bool = False) -> Path:
+ """Get the on-disk path to a video file or its WebP thumbnail."""
+ self._assert_read_access(video_name)
+ return Path(self._services.videos.get_path(video_name, thumbnail=thumbnail))
+
+
class TensorsInterface(InvocationContextInterface):
def save(self, tensor: Tensor) -> str:
"""Saves a tensor, returning its name.
@@ -738,6 +880,7 @@ class InvocationContext:
def __init__(
self,
images: ImagesInterface,
+ videos: VideosInterface,
tensors: TensorsInterface,
conditioning: ConditioningInterface,
models: ModelsInterface,
@@ -750,6 +893,8 @@ def __init__(
) -> None:
self.images = images
"""Methods to save, get and update images and their metadata."""
+ self.videos = videos
+ """Methods to save and get videos produced by invocations."""
self.tensors = tensors
"""Methods to save and get tensors, including image, noise, masks, and masked images."""
self.conditioning = conditioning
@@ -792,10 +937,12 @@ def build_invocation_context(
conditioning = ConditioningInterface(services=services, data=data)
models = ModelsInterface(services=services, data=data, util=util)
images = ImagesInterface(services=services, data=data, util=util)
+ videos = VideosInterface(services=services, data=data, util=util)
boards = BoardsInterface(services=services, data=data)
ctx = InvocationContext(
images=images,
+ videos=videos,
logger=logger,
config=config,
tensors=tensors,
diff --git a/invokeai/app/services/shared/pagination.py b/invokeai/app/services/shared/pagination.py
index ea342b11013..a2dd4fc8145 100644
--- a/invokeai/app/services/shared/pagination.py
+++ b/invokeai/app/services/shared/pagination.py
@@ -4,6 +4,11 @@
GenericBaseModel = TypeVar("GenericBaseModel", bound=BaseModel)
+# Upper bound for offset-paginated `limit` query params. Values flow verbatim into
+# SQL LIMIT clauses, where SQLite treats a negative limit as unlimited and a huge one
+# materializes every row into DTOs — routes must clamp with ge=0/le=MAX_PAGE_SIZE.
+MAX_PAGE_SIZE = 1000
+
class CursorPaginatedResults(BaseModel, Generic[GenericBaseModel]):
"""
diff --git a/invokeai/app/services/shared/sqlite_migrator/migrations/migration_2026_07_01_add_videos_tables.py b/invokeai/app/services/shared/sqlite_migrator/migrations/migration_2026_07_01_add_videos_tables.py
new file mode 100644
index 00000000000..86dabdb0014
--- /dev/null
+++ b/invokeai/app/services/shared/sqlite_migrator/migrations/migration_2026_07_01_add_videos_tables.py
@@ -0,0 +1,124 @@
+"""Migration: Add `videos` and `board_videos` tables for minimal video support.
+
+The `videos` table parallels `images` but with extra `duration` and `fps` columns.
+The `board_videos` table parallels `board_images`, providing one-to-many board↔video association.
+Foreign-key cascades from `boards` mirror the image side, so deleting a board also removes its videos' associations.
+"""
+
+import sqlite3
+
+from invokeai.app.services.shared.sqlite_migrator.sqlite_migrator_common import Migration
+
+
+class AddVideosTablesCallback:
+ def __call__(self, cursor: sqlite3.Cursor) -> None:
+ self._create_videos(cursor)
+ self._create_board_videos(cursor)
+
+ def _create_videos(self, cursor: sqlite3.Cursor) -> None:
+ tables = [
+ """--sql
+ CREATE TABLE IF NOT EXISTS videos (
+ video_name TEXT NOT NULL PRIMARY KEY,
+ video_origin TEXT NOT NULL,
+ video_category TEXT NOT NULL,
+ width INTEGER NOT NULL,
+ height INTEGER NOT NULL,
+ duration REAL NOT NULL DEFAULT 0.0,
+ fps REAL,
+ session_id TEXT,
+ node_id TEXT,
+ metadata TEXT,
+ is_intermediate BOOLEAN DEFAULT FALSE,
+ starred BOOLEAN DEFAULT FALSE,
+ has_workflow BOOLEAN DEFAULT FALSE,
+ -- Deliberately no FK to users(user_id), matching images/boards/workflows
+ -- (migration_27 adds those user_id columns with an index only): deleting a
+ -- user leaves their media in place for admin review/cleanup rather than
+ -- cascading a row delete that would strand the files on disk.
+ user_id TEXT NOT NULL DEFAULT 'system',
+ video_subfolder TEXT NOT NULL DEFAULT '',
+ created_at DATETIME NOT NULL DEFAULT(STRFTIME('%Y-%m-%d %H:%M:%f', 'NOW')),
+ -- Updated via trigger
+ updated_at DATETIME NOT NULL DEFAULT(STRFTIME('%Y-%m-%d %H:%M:%f', 'NOW')),
+ -- Soft delete, currently unused
+ deleted_at DATETIME
+ );
+ """
+ ]
+
+ indices = [
+ "CREATE UNIQUE INDEX IF NOT EXISTS idx_videos_video_name ON videos(video_name);",
+ "CREATE INDEX IF NOT EXISTS idx_videos_video_origin ON videos(video_origin);",
+ "CREATE INDEX IF NOT EXISTS idx_videos_video_category ON videos(video_category);",
+ "CREATE INDEX IF NOT EXISTS idx_videos_created_at ON videos(created_at);",
+ "CREATE INDEX IF NOT EXISTS idx_videos_starred ON videos(starred);",
+ "CREATE INDEX IF NOT EXISTS idx_videos_user_id ON videos(user_id);",
+ ]
+
+ triggers = [
+ """--sql
+ CREATE TRIGGER IF NOT EXISTS tg_videos_updated_at
+ AFTER UPDATE
+ ON videos FOR EACH ROW
+ BEGIN
+ UPDATE videos SET updated_at = STRFTIME('%Y-%m-%d %H:%M:%f', 'NOW')
+ WHERE video_name = old.video_name;
+ END;
+ """
+ ]
+
+ for stmt in tables + indices + triggers:
+ cursor.execute(stmt)
+
+ def _create_board_videos(self, cursor: sqlite3.Cursor) -> None:
+ tables = [
+ """--sql
+ CREATE TABLE IF NOT EXISTS board_videos (
+ board_id TEXT NOT NULL,
+ video_name TEXT NOT NULL,
+ created_at DATETIME NOT NULL DEFAULT(STRFTIME('%Y-%m-%d %H:%M:%f', 'NOW')),
+ -- updated via trigger
+ updated_at DATETIME NOT NULL DEFAULT(STRFTIME('%Y-%m-%d %H:%M:%f', 'NOW')),
+ -- Soft delete, currently unused
+ deleted_at DATETIME,
+ -- enforce one-to-many board↔video using PK on video_name
+ PRIMARY KEY (video_name),
+ FOREIGN KEY (board_id) REFERENCES boards (board_id) ON DELETE CASCADE,
+ FOREIGN KEY (video_name) REFERENCES videos (video_name) ON DELETE CASCADE
+ );
+ """
+ ]
+
+ indices = [
+ "CREATE INDEX IF NOT EXISTS idx_board_videos_board_id ON board_videos (board_id);",
+ "CREATE INDEX IF NOT EXISTS idx_board_videos_board_id_created_at ON board_videos (board_id, created_at);",
+ ]
+
+ triggers = [
+ """--sql
+ CREATE TRIGGER IF NOT EXISTS tg_board_videos_updated_at
+ AFTER UPDATE
+ ON board_videos FOR EACH ROW
+ BEGIN
+ UPDATE board_videos SET updated_at = STRFTIME('%Y-%m-%d %H:%M:%f', 'NOW')
+ WHERE board_id = old.board_id AND video_name = old.video_name;
+ END;
+ """
+ ]
+
+ for stmt in tables + indices + triggers:
+ cursor.execute(stmt)
+
+
+def build_migration() -> Migration:
+ """Builds the migration that adds the videos and board_videos tables.
+
+ Depends on migration_27, which last reshaped the boards table and created the 'system' user
+ that videos.user_id defaults to.
+ """
+ return Migration(
+ id="2026_07_01_add_videos_tables",
+ depends_on="migration_27",
+ callback=AddVideosTablesCallback(),
+ )
diff --git a/invokeai/app/services/urls/urls_base.py b/invokeai/app/services/urls/urls_base.py
index a5602abb3b4..c566f03f309 100644
--- a/invokeai/app/services/urls/urls_base.py
+++ b/invokeai/app/services/urls/urls_base.py
@@ -9,6 +9,11 @@ def get_image_url(self, image_name: str, thumbnail: bool = False) -> str:
"""Gets the URL for an image or thumbnail."""
pass
+ @abstractmethod
+ def get_video_url(self, video_name: str, thumbnail: bool = False) -> str:
+ """Gets the URL for a video or its first-frame thumbnail."""
+ pass
+
@abstractmethod
def get_model_image_url(self, model_key: str) -> str:
"""Gets the URL for a model image"""
diff --git a/invokeai/app/services/urls/urls_default.py b/invokeai/app/services/urls/urls_default.py
index 2e4f36d9d51..cb21bd02229 100644
--- a/invokeai/app/services/urls/urls_default.py
+++ b/invokeai/app/services/urls/urls_default.py
@@ -17,6 +17,15 @@ def get_image_url(self, image_name: str, thumbnail: bool = False) -> str:
return f"{self._base_url}/images/i/{image_basename}/full"
+ def get_video_url(self, video_name: str, thumbnail: bool = False) -> str:
+ video_basename = os.path.basename(video_name)
+
+ # These paths are determined by the routes in invokeai/app/api/routers/videos.py
+ if thumbnail:
+ return f"{self._base_url}/videos/i/{video_basename}/thumbnail"
+
+ return f"{self._base_url}/videos/i/{video_basename}/full"
+
def get_model_image_url(self, model_key: str) -> str:
return f"{self._base_url_v2}/models/i/{model_key}/image"
diff --git a/invokeai/app/services/video_files/__init__.py b/invokeai/app/services/video_files/__init__.py
new file mode 100644
index 00000000000..e69de29bb2d
diff --git a/invokeai/app/services/video_files/video_files_base.py b/invokeai/app/services/video_files/video_files_base.py
new file mode 100644
index 00000000000..8249b56b13e
--- /dev/null
+++ b/invokeai/app/services/video_files/video_files_base.py
@@ -0,0 +1,67 @@
+from abc import ABC, abstractmethod
+from pathlib import Path
+from typing import Optional
+
+from PIL import Image
+
+
+class VideoFileStorageBase(ABC):
+ """Low-level service responsible for storing and retrieving video files."""
+
+ @abstractmethod
+ def get_path(self, video_name: str, thumbnail: bool = False, video_subfolder: str = "") -> Path:
+ """Gets the internal path to a video or its thumbnail."""
+ pass
+
+ @abstractmethod
+ def save(
+ self,
+ source_path: Path,
+ video_name: str,
+ thumbnail_size: int = 256,
+ video_subfolder: str = "",
+ metadata: Optional[str] = None,
+ workflow: Optional[str] = None,
+ graph: Optional[str] = None,
+ first_frame: Optional[Image.Image] = None,
+ ) -> None:
+ """Saves a video by moving/copying the file at `source_path` into storage, then writes a sibling
+ WEBP thumbnail extracted from the first frame, plus an optional sidecar JSON of metadata/workflow/graph.
+ A caller that already decoded frame 0 can pass it as `first_frame` to skip the extraction.
+ """
+ pass
+
+ @abstractmethod
+ def delete(self, video_name: str, video_subfolder: str = "") -> None:
+ """Deletes a video file and its thumbnail (if one exists)."""
+ pass
+
+ @abstractmethod
+ def stage_delete(self, video_name: str, video_subfolder: str = "") -> object:
+ """Moves a video's files out of service and returns a rollback token."""
+ pass
+
+ @abstractmethod
+ def commit_delete(self, token: object) -> None:
+ """Permanently removes files represented by a staged-delete token."""
+ pass
+
+ @abstractmethod
+ def rollback_delete(self, token: object) -> None:
+ """Restores files represented by a staged-delete token."""
+ pass
+
+ @abstractmethod
+ def get_workflow(self, video_name: str, video_subfolder: str = "") -> Optional[str]:
+ """Gets the workflow JSON sidecar of a video, if any."""
+ pass
+
+ @abstractmethod
+ def get_graph(self, video_name: str, video_subfolder: str = "") -> Optional[str]:
+ """Gets the graph JSON sidecar of a video, if any."""
+ pass
+
+ @abstractmethod
+ def validate_path(self, path: str) -> bool:
+ """Validates the path given for a video or thumbnail."""
+ pass
diff --git a/invokeai/app/services/video_files/video_files_common.py b/invokeai/app/services/video_files/video_files_common.py
new file mode 100644
index 00000000000..4743d8a7cc8
--- /dev/null
+++ b/invokeai/app/services/video_files/video_files_common.py
@@ -0,0 +1,19 @@
+class VideoFileNotFoundException(Exception):
+ """Raised when a video file is not found in storage."""
+
+ def __init__(self, message="Video file not found"):
+ super().__init__(message)
+
+
+class VideoFileSaveException(Exception):
+ """Raised when a video file cannot be saved."""
+
+ def __init__(self, message="Video file not saved"):
+ super().__init__(message)
+
+
+class VideoFileDeleteException(Exception):
+ """Raised when a video file cannot be deleted."""
+
+ def __init__(self, message="Video file not deleted"):
+ super().__init__(message)
diff --git a/invokeai/app/services/video_files/video_files_disk.py b/invokeai/app/services/video_files/video_files_disk.py
new file mode 100644
index 00000000000..0835f2dda64
--- /dev/null
+++ b/invokeai/app/services/video_files/video_files_disk.py
@@ -0,0 +1,281 @@
+import json
+import os
+import shutil
+import tempfile
+from dataclasses import dataclass
+from pathlib import Path
+from typing import Optional, Union
+
+from PIL import Image
+
+from invokeai.app.services.invoker import Invoker
+from invokeai.app.services.video_files.video_files_base import VideoFileStorageBase
+from invokeai.app.services.video_files.video_files_common import (
+ VideoFileDeleteException,
+ VideoFileNotFoundException,
+ VideoFileSaveException,
+)
+from invokeai.app.util.thumbnails import make_thumbnail
+from invokeai.app.util.video_thumbnails import extract_video_frame, get_video_thumbnail_name
+from invokeai.backend.util.logging import InvokeAILogger
+
+
+@dataclass
+class _StagedDelete:
+ directory: Path
+ files: list[tuple[Path, Path]]
+
+
+class DiskVideoFileStorage(VideoFileStorageBase):
+ """Stores video files on disk under {outputs}/videos/, with first-frame WebP thumbnails under
+ {outputs}/videos/thumbnails/ and optional JSON sidecars for metadata/workflow/graph under
+ {outputs}/videos/sidecars/."""
+
+ def __init__(self, output_folder: Union[str, Path]):
+ self.__output_folder = output_folder if isinstance(output_folder, Path) else Path(output_folder)
+ self.__thumbnails_folder = self.__output_folder / "thumbnails"
+ self.__sidecars_folder = self.__output_folder / "sidecars"
+ self.__validate_storage_folders()
+
+ def start(self, invoker: Invoker) -> None:
+ self.__invoker = invoker
+ self.__recover_staged_deletes()
+
+ def save(
+ self,
+ source_path: Path,
+ video_name: str,
+ thumbnail_size: int = 256,
+ video_subfolder: str = "",
+ metadata: Optional[str] = None,
+ workflow: Optional[str] = None,
+ graph: Optional[str] = None,
+ first_frame: Optional[Image.Image] = None,
+ ) -> None:
+ logger = InvokeAILogger.get_logger()
+ try:
+ self.__validate_storage_folders()
+ video_path = self.get_path(video_name, video_subfolder=video_subfolder)
+ video_path.parent.mkdir(parents=True, exist_ok=True)
+
+ # Move if the source is on the same filesystem; otherwise copy then unlink.
+ try:
+ shutil.move(str(source_path), str(video_path))
+ except Exception:
+ shutil.copy2(str(source_path), str(video_path))
+ try:
+ Path(source_path).unlink(missing_ok=True)
+ except Exception:
+ pass
+ logger.info(f"Video file written: {video_path}")
+
+ thumbnail_name = get_video_thumbnail_name(video_name)
+ thumbnail_path = self.get_path(thumbnail_name, thumbnail=True, video_subfolder=video_subfolder)
+ thumbnail_path.parent.mkdir(parents=True, exist_ok=True)
+
+ # Thumbnail extraction is best-effort — if both imageio and cv2 fail, we still want
+ # the video record + file in place and the invocation to complete. A missing
+ # thumbnail leaves the gallery with a broken-image placeholder for that item, which
+ # is annoying but not fatal. The upload path already decoded frame 0 to prove
+ # decodability and passes it in, saving a decode-worker subprocess per upload.
+ frame = first_frame
+ if frame is None:
+ try:
+ frame = extract_video_frame(video_path, frame_index=0)
+ except Exception as e:
+ logger.warning(f"Thumbnail extraction raised for {video_name}: {e}")
+ frame = None
+ if frame is not None:
+ thumbnail = make_thumbnail(frame, thumbnail_size)
+ thumbnail.save(thumbnail_path, "WEBP")
+ logger.info(f"Thumbnail written: {thumbnail_path}")
+ else:
+ logger.warning(
+ f"Could not extract a thumbnail frame for {video_name}; gallery thumbnail will be missing."
+ )
+
+ if metadata is not None or workflow is not None or graph is not None:
+ sidecar_path = self.__get_sidecar_path(video_name, video_subfolder=video_subfolder)
+ sidecar_path.parent.mkdir(parents=True, exist_ok=True)
+ sidecar = {
+ "invokeai_metadata": metadata,
+ "invokeai_workflow": workflow,
+ "invokeai_graph": graph,
+ }
+ with open(sidecar_path, "w", encoding="utf-8") as f:
+ json.dump(sidecar, f)
+ logger.info(f"Sidecar written: {sidecar_path}")
+ except Exception as e:
+ # By this point the source MP4 has usually already been moved into permanent
+ # storage, so bailing out without cleanup would orphan the video (and any
+ # partially written thumbnail/sidecar) on disk with no DB record through which
+ # it can be managed — the caller rolls the record back on this exception.
+ try:
+ self.delete(video_name, video_subfolder=video_subfolder)
+ except Exception as cleanup_err:
+ logger.error(f"Failed to clean up partially saved files for {video_name}: {cleanup_err}")
+ raise VideoFileSaveException from e
+
+ def delete(self, video_name: str, video_subfolder: str = "") -> None:
+ token = self.stage_delete(video_name, video_subfolder)
+ self.commit_delete(token)
+
+ def stage_delete(self, video_name: str, video_subfolder: str = "") -> _StagedDelete:
+ candidates = [
+ self.get_path(video_name, video_subfolder=video_subfolder),
+ self.get_path(video_name, thumbnail=True, video_subfolder=video_subfolder),
+ self.__get_sidecar_path(video_name, video_subfolder=video_subfolder),
+ ]
+ staging_dir = Path(tempfile.mkdtemp(prefix=".delete_", dir=self.__output_folder))
+ staged: list[tuple[Path, Path]] = []
+ try:
+ with open(staging_dir / "manifest.json", "w", encoding="utf-8") as manifest:
+ manifest.write(json.dumps({"video_name": video_name, "video_subfolder": video_subfolder}))
+ manifest.flush()
+ os.fsync(manifest.fileno())
+ for index, source in enumerate(candidates):
+ if source.exists():
+ destination = staging_dir / str(index)
+ source.replace(destination)
+ staged.append((source, destination))
+ return _StagedDelete(directory=staging_dir, files=staged)
+ except Exception as e:
+ for source, destination in reversed(staged):
+ if destination.exists():
+ source.parent.mkdir(parents=True, exist_ok=True)
+ destination.replace(source)
+ shutil.rmtree(staging_dir, ignore_errors=True)
+ raise VideoFileDeleteException from e
+
+ def commit_delete(self, token: object) -> None:
+ if not isinstance(token, _StagedDelete):
+ raise VideoFileDeleteException("Invalid staged-delete token")
+ shutil.rmtree(token.directory)
+
+ def rollback_delete(self, token: object) -> None:
+ if not isinstance(token, _StagedDelete):
+ raise VideoFileDeleteException("Invalid staged-delete token")
+ try:
+ for source, destination in reversed(token.files):
+ if destination.exists():
+ source.parent.mkdir(parents=True, exist_ok=True)
+ destination.replace(source)
+ shutil.rmtree(token.directory, ignore_errors=True)
+ except Exception as e:
+ raise VideoFileDeleteException from e
+
+ def get_path(self, video_name: str, thumbnail: bool = False, video_subfolder: str = "") -> Path:
+ base_folder = self.__thumbnails_folder if thumbnail else self.__output_folder
+ filename = get_video_thumbnail_name(video_name) if thumbnail else video_name
+
+ basename = Path(filename).name
+ if basename != filename:
+ raise ValueError("Invalid video name, potential directory traversal detected")
+
+ if video_subfolder:
+ self._validate_subfolder(video_subfolder)
+ video_path = base_folder / video_subfolder / basename
+ else:
+ video_path = base_folder / basename
+
+ resolved_base = base_folder.resolve()
+ resolved_video_path = video_path.resolve()
+ if not resolved_video_path.is_relative_to(resolved_base):
+ raise ValueError("Video path outside outputs folder, potential directory traversal detected")
+ return resolved_video_path
+
+ def get_workflow(self, video_name: str, video_subfolder: str = "") -> Optional[str]:
+ sidecar = self.__read_sidecar(video_name, video_subfolder)
+ if sidecar is None:
+ return None
+ workflow = sidecar.get("invokeai_workflow")
+ return workflow if isinstance(workflow, str) else None
+
+ def get_graph(self, video_name: str, video_subfolder: str = "") -> Optional[str]:
+ sidecar = self.__read_sidecar(video_name, video_subfolder)
+ if sidecar is None:
+ return None
+ graph = sidecar.get("invokeai_graph")
+ return graph if isinstance(graph, str) else None
+
+ def validate_path(self, path: Union[str, Path]) -> bool:
+ path = path if isinstance(path, Path) else Path(path)
+ return path.exists()
+
+ @staticmethod
+ def _validate_subfolder(subfolder: str) -> None:
+ """Validates a subfolder path to prevent directory traversal."""
+ if not subfolder:
+ return
+ if "\\" in subfolder:
+ raise ValueError("Backslashes not allowed in subfolder path")
+ if subfolder.startswith("/"):
+ raise ValueError("Absolute paths not allowed in subfolder path")
+ for part in subfolder.split("/"):
+ if part == "..":
+ raise ValueError("Parent directory references not allowed in subfolder path")
+ if part == "":
+ raise ValueError("Empty path segments not allowed in subfolder path")
+
+ def __get_sidecar_path(self, video_name: str, video_subfolder: str = "") -> Path:
+ sidecar_name = Path(video_name).stem + ".json"
+ if video_subfolder:
+ self._validate_subfolder(video_subfolder)
+ sidecar_path = self.__sidecars_folder / video_subfolder / sidecar_name
+ else:
+ sidecar_path = self.__sidecars_folder / sidecar_name
+ resolved_base = self.__sidecars_folder.resolve()
+ resolved_sidecar_path = sidecar_path.resolve()
+ if not resolved_sidecar_path.is_relative_to(resolved_base):
+ raise ValueError("Sidecar path outside outputs folder, potential directory traversal detected")
+ return resolved_sidecar_path
+
+ def __read_sidecar(self, video_name: str, video_subfolder: str = "") -> Optional[dict]:
+ path = self.__get_sidecar_path(video_name, video_subfolder=video_subfolder)
+ if not path.exists():
+ return None
+ try:
+ with open(path, encoding="utf-8") as f:
+ return json.load(f)
+ except Exception as e:
+ raise VideoFileNotFoundException from e
+
+ def __validate_storage_folders(self) -> None:
+ for folder in (self.__output_folder, self.__thumbnails_folder, self.__sidecars_folder):
+ folder.mkdir(parents=True, exist_ok=True)
+
+ def __recover_staged_deletes(self) -> None:
+ logger = InvokeAILogger.get_logger()
+ for staging_dir in self.__output_folder.glob(".delete_*"):
+ manifest_path = staging_dir / "manifest.json"
+ if not manifest_path.is_file():
+ if not any(staging_dir.iterdir()):
+ staging_dir.rmdir()
+ continue
+ try:
+ with open(manifest_path, encoding="utf-8") as manifest:
+ data = json.load(manifest)
+ video_name = data["video_name"]
+ video_subfolder = data.get("video_subfolder", "")
+ candidates = [
+ self.get_path(video_name, video_subfolder=video_subfolder),
+ self.get_path(video_name, thumbnail=True, video_subfolder=video_subfolder),
+ self.__get_sidecar_path(video_name, video_subfolder=video_subfolder),
+ ]
+ token = _StagedDelete(
+ directory=staging_dir,
+ files=[(source, staging_dir / str(index)) for index, source in enumerate(candidates)],
+ )
+ # get() raises VideoRecordNotFoundException when the record is gone —
+ # handled below by purging the staging dir (the delete had committed in
+ # the DB, so finish it). A surviving record means the delete never
+ # committed: restore the files.
+ self.__invoker.services.video_records.get(video_name)
+ self.rollback_delete(token)
+ except Exception as error:
+ from invokeai.app.services.video_records.video_records_common import VideoRecordNotFoundException
+
+ if isinstance(error, VideoRecordNotFoundException):
+ shutil.rmtree(staging_dir, ignore_errors=True)
+ else:
+ logger.error(f"Failed to recover staged video deletion {staging_dir}: {error}")
diff --git a/invokeai/app/services/video_records/__init__.py b/invokeai/app/services/video_records/__init__.py
new file mode 100644
index 00000000000..e69de29bb2d
diff --git a/invokeai/app/services/video_records/video_records_base.py b/invokeai/app/services/video_records/video_records_base.py
new file mode 100644
index 00000000000..8492e4c4bf0
--- /dev/null
+++ b/invokeai/app/services/video_records/video_records_base.py
@@ -0,0 +1,108 @@
+from abc import ABC, abstractmethod
+from datetime import datetime
+from typing import Optional
+
+from invokeai.app.invocations.fields import MetadataField
+from invokeai.app.services.image_records.image_records_common import ImageCategory, ResourceOrigin
+from invokeai.app.services.shared.pagination import OffsetPaginatedResults
+from invokeai.app.services.shared.sqlite.sqlite_common import SQLiteDirection
+from invokeai.app.services.video_records.video_records_common import (
+ VideoNamesResult,
+ VideoRecord,
+ VideoRecordChanges,
+)
+
+
+class VideoRecordStorageBase(ABC):
+ """Low-level service responsible for interfacing with the video record store."""
+
+ @abstractmethod
+ def get(self, video_name: str) -> VideoRecord:
+ """Gets a video record."""
+ pass
+
+ @abstractmethod
+ def get_metadata(self, video_name: str) -> Optional[MetadataField]:
+ """Gets a video's metadata."""
+ pass
+
+ @abstractmethod
+ def update(self, video_name: str, changes: VideoRecordChanges) -> None:
+ """Updates a video record."""
+ pass
+
+ @abstractmethod
+ def get_many(
+ self,
+ offset: int = 0,
+ limit: int = 10,
+ starred_first: bool = True,
+ order_dir: SQLiteDirection = SQLiteDirection.Descending,
+ video_origin: Optional[ResourceOrigin] = None,
+ categories: Optional[list[ImageCategory]] = None,
+ is_intermediate: Optional[bool] = None,
+ board_id: Optional[str] = None,
+ search_term: Optional[str] = None,
+ user_id: Optional[str] = None,
+ is_admin: bool = False,
+ ) -> OffsetPaginatedResults[VideoRecord]:
+ """Gets a page of video records."""
+ pass
+
+ @abstractmethod
+ def delete(self, video_name: str) -> None:
+ """Deletes a video record."""
+ pass
+
+ @abstractmethod
+ def delete_many(self, video_names: list[str]) -> None:
+ """Deletes many video records."""
+ pass
+
+ @abstractmethod
+ def save(
+ self,
+ video_name: str,
+ video_origin: ResourceOrigin,
+ video_category: ImageCategory,
+ width: int,
+ height: int,
+ duration: float,
+ fps: Optional[float],
+ has_workflow: bool,
+ is_intermediate: Optional[bool] = False,
+ starred: Optional[bool] = False,
+ session_id: Optional[str] = None,
+ node_id: Optional[str] = None,
+ metadata: Optional[str] = None,
+ user_id: Optional[str] = None,
+ video_subfolder: str = "",
+ ) -> datetime:
+ """Saves a video record."""
+ pass
+
+ @abstractmethod
+ def get_user_id(self, video_name: str) -> Optional[str]:
+ """Gets the user_id of the video owner. Returns None if video not found."""
+ pass
+
+ @abstractmethod
+ def get_most_recent_video_for_board(self, board_id: str) -> Optional[VideoRecord]:
+ """Gets the most recent non-intermediate video on a board, preferring starred videos."""
+ pass
+
+ @abstractmethod
+ def get_video_names(
+ self,
+ starred_first: bool = True,
+ order_dir: SQLiteDirection = SQLiteDirection.Descending,
+ video_origin: Optional[ResourceOrigin] = None,
+ categories: Optional[list[ImageCategory]] = None,
+ is_intermediate: Optional[bool] = None,
+ board_id: Optional[str] = None,
+ search_term: Optional[str] = None,
+ user_id: Optional[str] = None,
+ is_admin: bool = False,
+ ) -> VideoNamesResult:
+ """Gets ordered list of video names with metadata for optimistic updates."""
+ pass
diff --git a/invokeai/app/services/video_records/video_records_common.py b/invokeai/app/services/video_records/video_records_common.py
new file mode 100644
index 00000000000..d67ae657ecb
--- /dev/null
+++ b/invokeai/app/services/video_records/video_records_common.py
@@ -0,0 +1,137 @@
+import datetime
+from typing import Optional, Union
+
+from pydantic import BaseModel, Field, StrictBool, StrictStr
+
+from invokeai.app.services.image_records.image_records_common import (
+ ImageCategory,
+ ResourceOrigin,
+)
+from invokeai.app.util.misc import get_iso_timestamp
+from invokeai.app.util.model_exclude_null import BaseModelExcludeNull
+
+
+class VideoRecordNotFoundException(Exception):
+ """Raised when a video record is not found."""
+
+ def __init__(self, message="Video record not found"):
+ super().__init__(message)
+
+
+class VideoRecordSaveException(Exception):
+ """Raised when a video record cannot be saved."""
+
+ def __init__(self, message="Video record not saved"):
+ super().__init__(message)
+
+
+class VideoRecordDeleteException(Exception):
+ """Raised when a video record cannot be deleted."""
+
+ def __init__(self, message="Video record not deleted"):
+ super().__init__(message)
+
+
+VIDEO_DTO_COLS = ", ".join(
+ [
+ "videos." + c
+ for c in [
+ "video_name",
+ "video_origin",
+ "video_category",
+ "width",
+ "height",
+ "duration",
+ "fps",
+ "session_id",
+ "node_id",
+ "has_workflow",
+ "is_intermediate",
+ "created_at",
+ "updated_at",
+ "deleted_at",
+ "starred",
+ "video_subfolder",
+ ]
+ ]
+)
+
+
+class VideoRecord(BaseModelExcludeNull):
+ """Deserialized video record without metadata."""
+
+ video_name: str = Field(description="The unique name of the video.")
+ video_origin: ResourceOrigin = Field(description="The origin of the video.")
+ video_category: ImageCategory = Field(description="The category of the video (reuses ImageCategory).")
+ width: int = Field(description="The pixel width of the video.")
+ height: int = Field(description="The pixel height of the video.")
+ duration: float = Field(description="The duration of the video in seconds.")
+ fps: Optional[float] = Field(default=None, description="The frames-per-second of the video, if known.")
+ created_at: Union[datetime.datetime, str] = Field(description="The created timestamp of the video.")
+ updated_at: Union[datetime.datetime, str] = Field(description="The updated timestamp of the video.")
+ deleted_at: Optional[Union[datetime.datetime, str]] = Field(
+ default=None, description="The deleted timestamp of the video."
+ )
+ is_intermediate: bool = Field(description="Whether this is an intermediate video.")
+ session_id: Optional[str] = Field(default=None, description="The session ID that produced this video, if any.")
+ node_id: Optional[str] = Field(default=None, description="The node ID that produced this video, if any.")
+ starred: bool = Field(description="Whether this video is starred.")
+ has_workflow: bool = Field(description="Whether this video has a workflow associated.")
+ video_subfolder: str = Field(default="", description="The subfolder where the video is stored on disk.")
+
+
+class VideoRecordChanges(BaseModelExcludeNull, extra="allow"):
+ """Allowed mutations on a video record."""
+
+ video_category: Optional[ImageCategory] = Field(default=None, description="The video's new category.")
+ session_id: Optional[StrictStr] = Field(default=None, description="The video's new session ID.")
+ is_intermediate: Optional[StrictBool] = Field(default=None, description="The video's new `is_intermediate` flag.")
+ starred: Optional[StrictBool] = Field(default=None, description="The video's new `starred` state.")
+
+
+def deserialize_video_record(video_dict: dict) -> VideoRecord:
+ """Deserializes a video record from a sqlite row dict."""
+ video_name = video_dict.get("video_name", "unknown")
+ video_origin = ResourceOrigin(video_dict.get("video_origin", ResourceOrigin.INTERNAL.value))
+ video_category = ImageCategory(video_dict.get("video_category", ImageCategory.GENERAL.value))
+ width = video_dict.get("width", 0)
+ height = video_dict.get("height", 0)
+ duration = video_dict.get("duration", 0.0)
+ fps_raw = video_dict.get("fps", None)
+ fps = float(fps_raw) if fps_raw is not None else None
+ session_id = video_dict.get("session_id", None)
+ node_id = video_dict.get("node_id", None)
+ created_at = video_dict.get("created_at", get_iso_timestamp())
+ updated_at = video_dict.get("updated_at", get_iso_timestamp())
+ deleted_at = video_dict.get("deleted_at", None)
+ is_intermediate = video_dict.get("is_intermediate", False)
+ starred = video_dict.get("starred", False)
+ has_workflow = video_dict.get("has_workflow", False)
+ video_subfolder = video_dict.get("video_subfolder", "")
+
+ return VideoRecord(
+ video_name=video_name,
+ video_origin=video_origin,
+ video_category=video_category,
+ width=width,
+ height=height,
+ duration=float(duration),
+ fps=fps,
+ session_id=session_id,
+ node_id=node_id,
+ created_at=created_at,
+ updated_at=updated_at,
+ deleted_at=deleted_at,
+ is_intermediate=is_intermediate,
+ starred=starred,
+ has_workflow=has_workflow,
+ video_subfolder=video_subfolder,
+ )
+
+
+class VideoNamesResult(BaseModel):
+ """Response containing ordered video names with metadata for optimistic updates."""
+
+ video_names: list[str] = Field(description="Ordered list of video names")
+ starred_count: int = Field(description="Number of starred videos (when starred_first=True)")
+ total_count: int = Field(description="Total number of videos matching the query")
diff --git a/invokeai/app/services/video_records/video_records_sqlite.py b/invokeai/app/services/video_records/video_records_sqlite.py
new file mode 100644
index 00000000000..94c943e5321
--- /dev/null
+++ b/invokeai/app/services/video_records/video_records_sqlite.py
@@ -0,0 +1,380 @@
+import sqlite3
+from datetime import datetime
+from typing import Optional, Union, cast
+
+from invokeai.app.invocations.fields import MetadataField, MetadataFieldValidator
+from invokeai.app.services.image_records.image_records_common import ImageCategory, ResourceOrigin
+from invokeai.app.services.shared.pagination import OffsetPaginatedResults
+from invokeai.app.services.shared.sqlite.sqlite_common import SQLiteDirection
+from invokeai.app.services.shared.sqlite.sqlite_database import SqliteDatabase
+from invokeai.app.services.video_records.video_records_base import VideoRecordStorageBase
+from invokeai.app.services.video_records.video_records_common import (
+ VIDEO_DTO_COLS,
+ VideoNamesResult,
+ VideoRecord,
+ VideoRecordChanges,
+ VideoRecordDeleteException,
+ VideoRecordNotFoundException,
+ VideoRecordSaveException,
+ deserialize_video_record,
+)
+
+
+class SqliteVideoRecordStorage(VideoRecordStorageBase):
+ def __init__(self, db: SqliteDatabase) -> None:
+ super().__init__()
+ self._db = db
+
+ def get(self, video_name: str) -> VideoRecord:
+ with self._db.transaction() as cursor:
+ try:
+ cursor.execute(
+ f"""--sql
+ SELECT {VIDEO_DTO_COLS} FROM videos
+ WHERE video_name = ?;
+ """,
+ (video_name,),
+ )
+ result = cast(Optional[sqlite3.Row], cursor.fetchone())
+ except sqlite3.Error as e:
+ raise VideoRecordNotFoundException from e
+
+ if not result:
+ raise VideoRecordNotFoundException
+ return deserialize_video_record(dict(result))
+
+ def get_user_id(self, video_name: str) -> Optional[str]:
+ with self._db.transaction() as cursor:
+ cursor.execute(
+ """--sql
+ SELECT user_id FROM videos
+ WHERE video_name = ?;
+ """,
+ (video_name,),
+ )
+ result = cast(Optional[sqlite3.Row], cursor.fetchone())
+ if not result:
+ return None
+ return cast(Optional[str], dict(result).get("user_id"))
+
+ def get_most_recent_video_for_board(self, board_id: str) -> Optional[VideoRecord]:
+ with self._db.transaction() as cursor:
+ cursor.execute(
+ f"""--sql
+ SELECT {VIDEO_DTO_COLS}
+ FROM videos
+ JOIN board_videos ON videos.video_name = board_videos.video_name
+ WHERE board_videos.board_id = ?
+ AND videos.is_intermediate = FALSE
+ ORDER BY videos.starred DESC, videos.created_at DESC, videos.video_name DESC
+ LIMIT 1;
+ """,
+ (board_id,),
+ )
+ result = cast(Optional[sqlite3.Row], cursor.fetchone())
+ if result is None:
+ return None
+ return deserialize_video_record(dict(result))
+
+ def get_metadata(self, video_name: str) -> Optional[MetadataField]:
+ with self._db.transaction() as cursor:
+ try:
+ cursor.execute(
+ """--sql
+ SELECT metadata FROM videos
+ WHERE video_name = ?;
+ """,
+ (video_name,),
+ )
+ result = cast(Optional[sqlite3.Row], cursor.fetchone())
+ except sqlite3.Error as e:
+ raise VideoRecordNotFoundException from e
+
+ if not result:
+ raise VideoRecordNotFoundException
+
+ as_dict = dict(result)
+ metadata_raw = cast(Optional[str], as_dict.get("metadata", None))
+ return MetadataFieldValidator.validate_json(metadata_raw) if metadata_raw is not None else None
+
+ def update(self, video_name: str, changes: VideoRecordChanges) -> None:
+ with self._db.transaction() as cursor:
+ try:
+ if changes.video_category is not None:
+ cursor.execute(
+ "UPDATE videos SET video_category = ? WHERE video_name = ?;",
+ (changes.video_category.value, video_name),
+ )
+ if changes.session_id is not None:
+ cursor.execute(
+ "UPDATE videos SET session_id = ? WHERE video_name = ?;",
+ (changes.session_id, video_name),
+ )
+ if changes.is_intermediate is not None:
+ cursor.execute(
+ "UPDATE videos SET is_intermediate = ? WHERE video_name = ?;",
+ (changes.is_intermediate, video_name),
+ )
+ if changes.starred is not None:
+ cursor.execute(
+ "UPDATE videos SET starred = ? WHERE video_name = ?;",
+ (changes.starred, video_name),
+ )
+ except sqlite3.Error as e:
+ raise VideoRecordSaveException from e
+
+ def get_many(
+ self,
+ offset: int = 0,
+ limit: int = 10,
+ starred_first: bool = True,
+ order_dir: SQLiteDirection = SQLiteDirection.Descending,
+ video_origin: Optional[ResourceOrigin] = None,
+ categories: Optional[list[ImageCategory]] = None,
+ is_intermediate: Optional[bool] = None,
+ board_id: Optional[str] = None,
+ search_term: Optional[str] = None,
+ user_id: Optional[str] = None,
+ is_admin: bool = False,
+ ) -> OffsetPaginatedResults[VideoRecord]:
+ with self._db.transaction() as cursor:
+ count_query = """--sql
+ SELECT COUNT(*)
+ FROM videos
+ LEFT JOIN board_videos ON board_videos.video_name = videos.video_name
+ WHERE 1=1
+ """
+ videos_query = f"""--sql
+ SELECT {VIDEO_DTO_COLS}
+ FROM videos
+ LEFT JOIN board_videos ON board_videos.video_name = videos.video_name
+ WHERE 1=1
+ """
+
+ query_conditions = ""
+ query_params: list[Union[int, str, bool]] = []
+
+ if video_origin is not None:
+ query_conditions += " AND videos.video_origin = ? "
+ query_params.append(video_origin.value)
+
+ if categories is not None:
+ category_strings = [c.value for c in set(categories)]
+ placeholders = ",".join("?" * len(category_strings))
+ query_conditions += f" AND videos.video_category IN ( {placeholders} ) "
+ for c in category_strings:
+ query_params.append(c)
+
+ if is_intermediate is not None:
+ query_conditions += " AND videos.is_intermediate = ? "
+ query_params.append(is_intermediate)
+
+ if board_id == "none":
+ query_conditions += " AND board_videos.board_id IS NULL "
+ if user_id is not None and not is_admin:
+ query_conditions += " AND videos.user_id = ? "
+ query_params.append(user_id)
+ elif board_id is not None:
+ query_conditions += " AND board_videos.board_id = ? "
+ query_params.append(board_id)
+ elif user_id is not None and not is_admin:
+ # No board_id supplied — still enforce per-user isolation so
+ # non-admin callers cannot enumerate other users' videos.
+ query_conditions += " AND videos.user_id = ? "
+ query_params.append(user_id)
+
+ if search_term:
+ query_conditions += " AND (videos.metadata LIKE ? OR videos.created_at LIKE ?) "
+ query_params.append(f"%{search_term.lower()}%")
+ query_params.append(f"%{search_term.lower()}%")
+
+ if starred_first:
+ query_pagination = (
+ f" ORDER BY videos.starred DESC, videos.created_at {order_dir.value}, "
+ f"videos.video_name {order_dir.value} LIMIT ? OFFSET ? "
+ )
+ else:
+ query_pagination = (
+ f" ORDER BY videos.created_at {order_dir.value}, videos.video_name {order_dir.value} "
+ "LIMIT ? OFFSET ? "
+ )
+
+ videos_query += query_conditions + query_pagination + ";"
+ videos_params = query_params.copy()
+ videos_params.extend([limit, offset])
+ cursor.execute(videos_query, videos_params)
+ result = cast(list[sqlite3.Row], cursor.fetchall())
+ videos = [deserialize_video_record(dict(r)) for r in result]
+
+ count_query += query_conditions + ";"
+ cursor.execute(count_query, query_params.copy())
+ count = cast(int, cursor.fetchone()[0])
+
+ return OffsetPaginatedResults(items=videos, offset=offset, limit=limit, total=count)
+
+ def delete(self, video_name: str) -> None:
+ with self._db.transaction() as cursor:
+ try:
+ cursor.execute("DELETE FROM videos WHERE video_name = ?;", (video_name,))
+ except sqlite3.Error as e:
+ raise VideoRecordDeleteException from e
+
+ def delete_many(self, video_names: list[str]) -> None:
+ with self._db.transaction() as cursor:
+ try:
+ placeholders = ",".join("?" for _ in video_names)
+ cursor.execute(f"DELETE FROM videos WHERE video_name IN ({placeholders})", video_names)
+ except sqlite3.Error as e:
+ raise VideoRecordDeleteException from e
+
+ def save(
+ self,
+ video_name: str,
+ video_origin: ResourceOrigin,
+ video_category: ImageCategory,
+ width: int,
+ height: int,
+ duration: float,
+ fps: Optional[float],
+ has_workflow: bool,
+ is_intermediate: Optional[bool] = False,
+ starred: Optional[bool] = False,
+ session_id: Optional[str] = None,
+ node_id: Optional[str] = None,
+ metadata: Optional[str] = None,
+ user_id: Optional[str] = None,
+ video_subfolder: str = "",
+ ) -> datetime:
+ with self._db.transaction() as cursor:
+ try:
+ cursor.execute(
+ """--sql
+ INSERT OR IGNORE INTO videos (
+ video_name,
+ video_origin,
+ video_category,
+ width,
+ height,
+ duration,
+ fps,
+ node_id,
+ session_id,
+ metadata,
+ is_intermediate,
+ starred,
+ has_workflow,
+ user_id,
+ video_subfolder
+ )
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?);
+ """,
+ (
+ video_name,
+ video_origin.value,
+ video_category.value,
+ width,
+ height,
+ float(duration),
+ float(fps) if fps is not None else None,
+ node_id,
+ session_id,
+ metadata,
+ is_intermediate,
+ starred,
+ has_workflow,
+ user_id or "system",
+ video_subfolder,
+ ),
+ )
+
+ cursor.execute(
+ "SELECT created_at FROM videos WHERE video_name = ?;",
+ (video_name,),
+ )
+ created_at = datetime.fromisoformat(cursor.fetchone()[0])
+ except sqlite3.Error as e:
+ raise VideoRecordSaveException from e
+ return created_at
+
+ def get_video_names(
+ self,
+ starred_first: bool = True,
+ order_dir: SQLiteDirection = SQLiteDirection.Descending,
+ video_origin: Optional[ResourceOrigin] = None,
+ categories: Optional[list[ImageCategory]] = None,
+ is_intermediate: Optional[bool] = None,
+ board_id: Optional[str] = None,
+ search_term: Optional[str] = None,
+ user_id: Optional[str] = None,
+ is_admin: bool = False,
+ ) -> VideoNamesResult:
+ with self._db.transaction() as cursor:
+ query_conditions = ""
+ query_params: list[Union[int, str, bool]] = []
+
+ if video_origin is not None:
+ query_conditions += " AND videos.video_origin = ? "
+ query_params.append(video_origin.value)
+
+ if categories is not None:
+ category_strings = [c.value for c in set(categories)]
+ placeholders = ",".join("?" * len(category_strings))
+ query_conditions += f" AND videos.video_category IN ( {placeholders} ) "
+ for c in category_strings:
+ query_params.append(c)
+
+ if is_intermediate is not None:
+ query_conditions += " AND videos.is_intermediate = ? "
+ query_params.append(is_intermediate)
+
+ if board_id == "none":
+ query_conditions += " AND board_videos.board_id IS NULL "
+ if user_id is not None and not is_admin:
+ query_conditions += " AND videos.user_id = ? "
+ query_params.append(user_id)
+ elif board_id is not None:
+ query_conditions += " AND board_videos.board_id = ? "
+ query_params.append(board_id)
+ elif user_id is not None and not is_admin:
+ # No board_id supplied — still enforce per-user isolation so
+ # non-admin callers cannot enumerate other users' videos.
+ query_conditions += " AND videos.user_id = ? "
+ query_params.append(user_id)
+
+ if search_term:
+ query_conditions += " AND (videos.metadata LIKE ? OR videos.created_at LIKE ?) "
+ query_params.append(f"%{search_term.lower()}%")
+ query_params.append(f"%{search_term.lower()}%")
+
+ starred_count = 0
+ if starred_first:
+ cursor.execute(
+ f"""--sql
+ SELECT COUNT(*)
+ FROM videos
+ LEFT JOIN board_videos ON board_videos.video_name = videos.video_name
+ WHERE videos.starred = TRUE AND (1=1{query_conditions})
+ """,
+ query_params,
+ )
+ starred_count = cast(int, cursor.fetchone()[0])
+
+ order_clause = (
+ f" ORDER BY videos.starred DESC, videos.created_at {order_dir.value}, "
+ f"videos.video_name {order_dir.value} "
+ if starred_first
+ else f" ORDER BY videos.created_at {order_dir.value}, videos.video_name {order_dir.value} "
+ )
+ cursor.execute(
+ f"""--sql
+ SELECT videos.video_name
+ FROM videos
+ LEFT JOIN board_videos ON board_videos.video_name = videos.video_name
+ WHERE 1=1{query_conditions}
+ {order_clause}
+ """,
+ query_params,
+ )
+ result = cast(list[sqlite3.Row], cursor.fetchall())
+ video_names = [row[0] for row in result]
+ return VideoNamesResult(video_names=video_names, starred_count=starred_count, total_count=len(video_names))
diff --git a/invokeai/app/services/videos/__init__.py b/invokeai/app/services/videos/__init__.py
new file mode 100644
index 00000000000..e69de29bb2d
diff --git a/invokeai/app/services/videos/videos_base.py b/invokeai/app/services/videos/videos_base.py
new file mode 100644
index 00000000000..542e2cdff5d
--- /dev/null
+++ b/invokeai/app/services/videos/videos_base.py
@@ -0,0 +1,166 @@
+from abc import ABC, abstractmethod
+from pathlib import Path
+from typing import Callable, Optional
+
+from PIL import Image
+
+from invokeai.app.invocations.fields import MetadataField
+from invokeai.app.services.image_records.image_records_common import ImageCategory, ResourceOrigin
+from invokeai.app.services.shared.pagination import OffsetPaginatedResults
+from invokeai.app.services.shared.sqlite.sqlite_common import SQLiteDirection
+from invokeai.app.services.video_records.video_records_common import (
+ VideoNamesResult,
+ VideoRecord,
+ VideoRecordChanges,
+)
+from invokeai.app.services.videos.videos_common import VideoDTO
+
+
+class VideoServiceABC(ABC):
+ """High-level service for video management."""
+
+ _on_changed_callbacks: list[Callable[[VideoDTO], None]]
+ _on_deleted_callbacks: list[Callable[[str], None]]
+
+ def __init__(self) -> None:
+ self._on_changed_callbacks = []
+ self._on_deleted_callbacks = []
+
+ def on_changed(self, on_changed: Callable[[VideoDTO], None]) -> None:
+ """Register a callback for when a video is changed."""
+ self._on_changed_callbacks.append(on_changed)
+
+ def on_deleted(self, on_deleted: Callable[[str], None]) -> None:
+ """Register a callback for when a video is deleted."""
+ self._on_deleted_callbacks.append(on_deleted)
+
+ def _on_changed(self, item: VideoDTO) -> None:
+ for callback in self._on_changed_callbacks:
+ callback(item)
+
+ def _on_deleted(self, item_id: str) -> None:
+ for callback in self._on_deleted_callbacks:
+ callback(item_id)
+
+ @abstractmethod
+ def create(
+ self,
+ source_path: Path,
+ width: int,
+ height: int,
+ duration: float,
+ fps: Optional[float],
+ video_origin: ResourceOrigin,
+ video_category: ImageCategory,
+ node_id: Optional[str] = None,
+ session_id: Optional[str] = None,
+ board_id: Optional[str] = None,
+ is_intermediate: Optional[bool] = False,
+ metadata: Optional[str] = None,
+ workflow: Optional[str] = None,
+ graph: Optional[str] = None,
+ user_id: Optional[str] = None,
+ first_frame: Optional[Image.Image] = None,
+ ) -> VideoDTO:
+ """Creates a video by moving/copying the file at `source_path` into storage and recording it.
+
+ ``first_frame``, when provided (e.g. the upload path already decoded frame 0 to
+ prove decodability), is used as the thumbnail source instead of spawning another
+ decode worker.
+ """
+ pass
+
+ @abstractmethod
+ def update(self, video_name: str, changes: VideoRecordChanges) -> VideoDTO:
+ """Updates a video."""
+ pass
+
+ @abstractmethod
+ def get_record(self, video_name: str) -> VideoRecord:
+ """Gets a video record."""
+ pass
+
+ @abstractmethod
+ def get_dto(self, video_name: str) -> VideoDTO:
+ """Gets a video DTO."""
+ pass
+
+ @abstractmethod
+ def get_metadata(self, video_name: str) -> Optional[MetadataField]:
+ """Gets a video's metadata."""
+ pass
+
+ @abstractmethod
+ def get_workflow(self, video_name: str) -> Optional[str]:
+ """Gets a video's workflow."""
+ pass
+
+ @abstractmethod
+ def get_graph(self, video_name: str) -> Optional[str]:
+ """Gets a video's graph."""
+ pass
+
+ @abstractmethod
+ def get_path(self, video_name: str, thumbnail: bool = False) -> str:
+ """Gets a video's on-disk path."""
+ pass
+
+ @abstractmethod
+ def get_url(self, video_name: str, thumbnail: bool = False) -> str:
+ """Gets a video's URL."""
+ pass
+
+ @abstractmethod
+ def get_many(
+ self,
+ offset: int = 0,
+ limit: int = 10,
+ starred_first: bool = True,
+ order_dir: SQLiteDirection = SQLiteDirection.Descending,
+ video_origin: Optional[ResourceOrigin] = None,
+ categories: Optional[list[ImageCategory]] = None,
+ is_intermediate: Optional[bool] = None,
+ board_id: Optional[str] = None,
+ search_term: Optional[str] = None,
+ user_id: Optional[str] = None,
+ is_admin: bool = False,
+ ) -> OffsetPaginatedResults[VideoDTO]:
+ """Gets a paginated list of video DTOs."""
+ pass
+
+ @abstractmethod
+ def delete(self, video_name: str) -> None:
+ """Deletes a video."""
+ pass
+
+ @abstractmethod
+ def delete_videos_on_board(self, board_id: str, user_id: Optional[str] = None) -> tuple[list[str], list[str]]:
+ """Deletes all videos on a board; returns ``(deleted_names, failed_names)``.
+
+ When ``user_id`` is provided, only videos owned by that user are deleted (other users'
+ contributions to a public/shared board are preserved). Pass ``None`` for the admin
+ path to delete every video on the board regardless of uploader.
+
+ Videos whose backing file deletion fails are intentionally retained (their DB records
+ survive and cascade to "uncategorized" via the board_videos FK), so the returned lists
+ are the authoritative ``deleted_videos``/``failed_videos`` for the caller's response —
+ callers must not reconstruct failures by diffing their own board listing, which races
+ with concurrent moves/deletes.
+ """
+ pass
+
+ @abstractmethod
+ def get_video_names(
+ self,
+ starred_first: bool = True,
+ order_dir: SQLiteDirection = SQLiteDirection.Descending,
+ video_origin: Optional[ResourceOrigin] = None,
+ categories: Optional[list[ImageCategory]] = None,
+ is_intermediate: Optional[bool] = None,
+ board_id: Optional[str] = None,
+ search_term: Optional[str] = None,
+ user_id: Optional[str] = None,
+ is_admin: bool = False,
+ ) -> VideoNamesResult:
+ """Gets ordered list of video names."""
+ pass
diff --git a/invokeai/app/services/videos/videos_common.py b/invokeai/app/services/videos/videos_common.py
new file mode 100644
index 00000000000..59200f4ca72
--- /dev/null
+++ b/invokeai/app/services/videos/videos_common.py
@@ -0,0 +1,62 @@
+from typing import Optional
+
+from pydantic import BaseModel, Field
+
+from invokeai.app.services.video_records.video_records_common import VideoRecord
+from invokeai.app.util.model_exclude_null import BaseModelExcludeNull
+
+
+class VideoUrlsDTO(BaseModelExcludeNull):
+ """The URLs for a video and its thumbnail."""
+
+ video_name: str = Field(description="The unique name of the video.")
+ video_url: str = Field(description="The URL of the video file (MP4).")
+ thumbnail_url: str = Field(description="The URL of the video's first-frame thumbnail (WebP).")
+
+
+class VideoDTO(VideoRecord, VideoUrlsDTO):
+ """Deserialized video record, enriched for the frontend."""
+
+ board_id: Optional[str] = Field(
+ default=None, description="The id of the board the video belongs to, if one exists."
+ )
+
+
+def video_record_to_dto(
+ video_record: VideoRecord,
+ video_url: str,
+ thumbnail_url: str,
+ board_id: Optional[str],
+) -> VideoDTO:
+ """Converts a video record to a video DTO."""
+ return VideoDTO(
+ **video_record.model_dump(),
+ video_url=video_url,
+ thumbnail_url=thumbnail_url,
+ board_id=board_id,
+ )
+
+
+class VideoResultWithAffectedBoards(BaseModel):
+ affected_boards: list[str] = Field(description="The ids of boards affected by the operation")
+
+
+class DeleteVideosResult(VideoResultWithAffectedBoards):
+ deleted_videos: list[str] = Field(description="The names of the videos that were deleted")
+ failed_videos: list[str] = Field(description="The names of videos that were not deleted")
+
+
+class StarredVideosResult(VideoResultWithAffectedBoards):
+ starred_videos: list[str] = Field(description="The names of the videos that were starred")
+
+
+class UnstarredVideosResult(VideoResultWithAffectedBoards):
+ unstarred_videos: list[str] = Field(description="The names of the videos that were unstarred")
+
+
+class AddVideosToBoardResult(VideoResultWithAffectedBoards):
+ added_videos: list[str] = Field(description="The video names that were added to the board")
+
+
+class RemoveVideosFromBoardResult(VideoResultWithAffectedBoards):
+ removed_videos: list[str] = Field(description="The video names that were removed from their board")
diff --git a/invokeai/app/services/videos/videos_default.py b/invokeai/app/services/videos/videos_default.py
new file mode 100644
index 00000000000..46e3711dbb5
--- /dev/null
+++ b/invokeai/app/services/videos/videos_default.py
@@ -0,0 +1,406 @@
+from pathlib import Path
+from typing import Optional
+
+from PIL import Image
+
+from invokeai.app.invocations.fields import MetadataField
+from invokeai.app.services.image_records.image_records_common import (
+ ImageCategory,
+ InvalidImageCategoryException,
+ InvalidOriginException,
+ ResourceOrigin,
+)
+from invokeai.app.services.invoker import Invoker
+from invokeai.app.services.shared.pagination import OffsetPaginatedResults
+from invokeai.app.services.shared.sqlite.sqlite_common import SQLiteDirection
+from invokeai.app.services.video_files.video_files_common import (
+ VideoFileDeleteException,
+ VideoFileNotFoundException,
+ VideoFileSaveException,
+)
+from invokeai.app.services.video_records.video_records_common import (
+ VideoNamesResult,
+ VideoRecord,
+ VideoRecordChanges,
+ VideoRecordDeleteException,
+ VideoRecordNotFoundException,
+ VideoRecordSaveException,
+)
+from invokeai.app.services.videos.videos_base import VideoServiceABC
+from invokeai.app.services.videos.videos_common import VideoDTO, video_record_to_dto
+
+
+class VideoService(VideoServiceABC):
+ __invoker: Invoker
+
+ def start(self, invoker: Invoker) -> None:
+ self.__invoker = invoker
+
+ def create(
+ self,
+ source_path: Path,
+ width: int,
+ height: int,
+ duration: float,
+ fps: Optional[float],
+ video_origin: ResourceOrigin,
+ video_category: ImageCategory,
+ node_id: Optional[str] = None,
+ session_id: Optional[str] = None,
+ board_id: Optional[str] = None,
+ is_intermediate: Optional[bool] = False,
+ metadata: Optional[str] = None,
+ workflow: Optional[str] = None,
+ graph: Optional[str] = None,
+ user_id: Optional[str] = None,
+ first_frame: Optional[Image.Image] = None,
+ ) -> VideoDTO:
+ if video_origin not in ResourceOrigin:
+ raise InvalidOriginException
+ if video_category not in ImageCategory:
+ raise InvalidImageCategoryException
+
+ video_name = self.__invoker.services.names.create_video_name()
+
+ # Reuse the image subfolder strategy for video organization.
+ from invokeai.app.services.image_files.image_subfolder_strategy import create_subfolder_strategy
+
+ strategy_name = self.__invoker.services.configuration.image_subfolder_strategy
+ strategy = create_subfolder_strategy(strategy_name)
+ video_subfolder = strategy.get_subfolder(video_name, video_category, is_intermediate or False)
+
+ record_saved = False
+ board_attached = False
+ try:
+ self.__invoker.services.video_records.save(
+ video_name=video_name,
+ video_origin=video_origin,
+ video_category=video_category,
+ width=width,
+ height=height,
+ duration=duration,
+ fps=fps,
+ has_workflow=workflow is not None or graph is not None,
+ is_intermediate=is_intermediate,
+ node_id=node_id,
+ metadata=metadata,
+ session_id=session_id,
+ user_id=user_id,
+ video_subfolder=video_subfolder,
+ )
+ record_saved = True
+ if board_id is not None:
+ # Board attachment is deliberately best-effort, mirroring ImageService.create:
+ # this is reachable when the board is deleted between the caller's access
+ # check and this insert, and failing the whole create here would destroy a
+ # just-generated video over a cosmetic categorization problem. The returned
+ # DTO reports the video's *actual* board (None on fallback), so callers are
+ # not told the attachment succeeded.
+ try:
+ self.__invoker.services.board_video_records.add_video_to_board(
+ board_id=board_id, video_name=video_name
+ )
+ board_attached = True
+ except Exception as e:
+ self.__invoker.services.logger.warning(f"Failed to add video to board {board_id}: {str(e)}")
+
+ self.__invoker.services.video_files.save(
+ source_path=source_path,
+ video_name=video_name,
+ video_subfolder=video_subfolder,
+ metadata=metadata,
+ workflow=workflow,
+ graph=graph,
+ first_frame=first_frame,
+ )
+
+ video_dto = self.get_dto(video_name)
+ self._on_changed(video_dto)
+ return video_dto
+ except VideoRecordSaveException:
+ self.__invoker.services.logger.error("Failed to save video record")
+ raise
+ except Exception as e:
+ # Roll back any DB-side state we created so the gallery doesn't end up with a
+ # ghost record whose file endpoints 404. Most commonly triggered by
+ # VideoFileSaveException (disk save or sidecar write failure), but we also
+ # need to unwind on any unexpected post-record failure.
+ if board_attached:
+ try:
+ self.__invoker.services.board_video_records.remove_video_from_board(video_name=video_name)
+ except Exception as rollback_err:
+ self.__invoker.services.logger.error(
+ f"Failed to roll back board attachment for {video_name}: {str(rollback_err)}"
+ )
+ if record_saved:
+ try:
+ self.__invoker.services.video_records.delete(video_name)
+ except Exception as rollback_err:
+ self.__invoker.services.logger.error(
+ f"Failed to roll back video record for {video_name}: {str(rollback_err)}"
+ )
+ # The disk layer cleans up after itself when the save fails, but a failure
+ # after a successful file save (e.g. building the DTO) would still leave the
+ # files on disk with no record pointing at them. delete() skips files that
+ # don't exist, so this is a no-op when nothing was written.
+ try:
+ self.__invoker.services.video_files.delete(video_name, video_subfolder=video_subfolder)
+ except Exception as rollback_err:
+ self.__invoker.services.logger.error(
+ f"Failed to roll back video files for {video_name}: {str(rollback_err)}"
+ )
+ if isinstance(e, VideoFileSaveException):
+ self.__invoker.services.logger.error("Failed to save video file")
+ else:
+ self.__invoker.services.logger.error(f"Problem saving video record and file: {str(e)}")
+ raise
+
+ def update(self, video_name: str, changes: VideoRecordChanges) -> VideoDTO:
+ try:
+ self.__invoker.services.video_records.update(video_name, changes)
+ video_dto = self.get_dto(video_name)
+ self._on_changed(video_dto)
+ return video_dto
+ except VideoRecordSaveException:
+ self.__invoker.services.logger.error("Failed to update video record")
+ raise
+ except Exception as e:
+ self.__invoker.services.logger.error("Problem updating video record")
+ raise e
+
+ def get_record(self, video_name: str) -> VideoRecord:
+ try:
+ return self.__invoker.services.video_records.get(video_name)
+ except VideoRecordNotFoundException:
+ self.__invoker.services.logger.error("Video record not found")
+ raise
+ except Exception as e:
+ self.__invoker.services.logger.error("Problem getting video record")
+ raise e
+
+ def get_dto(self, video_name: str) -> VideoDTO:
+ try:
+ video_record = self.__invoker.services.video_records.get(video_name)
+ return video_record_to_dto(
+ video_record=video_record,
+ video_url=self.__invoker.services.urls.get_video_url(video_name),
+ thumbnail_url=self.__invoker.services.urls.get_video_url(video_name, thumbnail=True),
+ board_id=self.__invoker.services.board_video_records.get_board_for_video(video_name),
+ )
+ except VideoRecordNotFoundException:
+ self.__invoker.services.logger.error("Video record not found")
+ raise
+ except Exception as e:
+ self.__invoker.services.logger.error("Problem getting video DTO")
+ raise e
+
+ def get_metadata(self, video_name: str) -> Optional[MetadataField]:
+ try:
+ return self.__invoker.services.video_records.get_metadata(video_name)
+ except VideoRecordNotFoundException:
+ self.__invoker.services.logger.error("Video record not found")
+ raise
+ except Exception as e:
+ self.__invoker.services.logger.error("Problem getting video metadata")
+ raise e
+
+ def get_workflow(self, video_name: str) -> Optional[str]:
+ try:
+ record = self.__invoker.services.video_records.get(video_name)
+ return self.__invoker.services.video_files.get_workflow(video_name, video_subfolder=record.video_subfolder)
+ except VideoFileNotFoundException:
+ self.__invoker.services.logger.error("Video file not found")
+ raise
+ except Exception:
+ self.__invoker.services.logger.error("Problem getting video workflow")
+ raise
+
+ def get_graph(self, video_name: str) -> Optional[str]:
+ try:
+ record = self.__invoker.services.video_records.get(video_name)
+ return self.__invoker.services.video_files.get_graph(video_name, video_subfolder=record.video_subfolder)
+ except VideoFileNotFoundException:
+ self.__invoker.services.logger.error("Video file not found")
+ raise
+ except Exception:
+ self.__invoker.services.logger.error("Problem getting video graph")
+ raise
+
+ def get_path(self, video_name: str, thumbnail: bool = False) -> str:
+ try:
+ record = self.__invoker.services.video_records.get(video_name)
+ return str(
+ self.__invoker.services.video_files.get_path(
+ video_name, thumbnail=thumbnail, video_subfolder=record.video_subfolder
+ )
+ )
+ except Exception as e:
+ self.__invoker.services.logger.error("Problem getting video path")
+ raise e
+
+ def get_url(self, video_name: str, thumbnail: bool = False) -> str:
+ try:
+ return self.__invoker.services.urls.get_video_url(video_name, thumbnail=thumbnail)
+ except Exception as e:
+ self.__invoker.services.logger.error("Problem getting video URL")
+ raise e
+
+ def get_many(
+ self,
+ offset: int = 0,
+ limit: int = 10,
+ starred_first: bool = True,
+ order_dir: SQLiteDirection = SQLiteDirection.Descending,
+ video_origin: Optional[ResourceOrigin] = None,
+ categories: Optional[list[ImageCategory]] = None,
+ is_intermediate: Optional[bool] = None,
+ board_id: Optional[str] = None,
+ search_term: Optional[str] = None,
+ user_id: Optional[str] = None,
+ is_admin: bool = False,
+ ) -> OffsetPaginatedResults[VideoDTO]:
+ try:
+ results = self.__invoker.services.video_records.get_many(
+ offset,
+ limit,
+ starred_first,
+ order_dir,
+ video_origin,
+ categories,
+ is_intermediate,
+ board_id,
+ search_term,
+ user_id,
+ is_admin,
+ )
+ video_dtos = [
+ video_record_to_dto(
+ video_record=r,
+ video_url=self.__invoker.services.urls.get_video_url(r.video_name),
+ thumbnail_url=self.__invoker.services.urls.get_video_url(r.video_name, thumbnail=True),
+ board_id=self.__invoker.services.board_video_records.get_board_for_video(r.video_name),
+ )
+ for r in results.items
+ ]
+ return OffsetPaginatedResults[VideoDTO](
+ items=video_dtos, offset=results.offset, limit=results.limit, total=results.total
+ )
+ except Exception as e:
+ self.__invoker.services.logger.error("Problem getting paginated video DTOs")
+ raise e
+
+ def delete(self, video_name: str) -> None:
+ token: object | None = None
+ record_deleted = False
+ try:
+ record = self.__invoker.services.video_records.get(video_name)
+ token = self.__invoker.services.video_files.stage_delete(video_name, video_subfolder=record.video_subfolder)
+ self.__invoker.services.video_records.delete(video_name)
+ record_deleted = True
+ try:
+ self.__invoker.services.video_files.commit_delete(token)
+ except Exception as cleanup_error:
+ self.__invoker.services.logger.error(f"Failed to purge staged video files: {cleanup_error}")
+ self._on_deleted(video_name)
+ except VideoRecordDeleteException:
+ if token is not None:
+ self.__invoker.services.video_files.rollback_delete(token)
+ self.__invoker.services.logger.error("Failed to delete video record")
+ raise
+ except VideoFileDeleteException:
+ self.__invoker.services.logger.error("Failed to delete video file")
+ raise
+ except Exception as e:
+ if token is not None and not record_deleted:
+ try:
+ self.__invoker.services.video_files.rollback_delete(token)
+ except Exception as rollback_error:
+ self.__invoker.services.logger.error(f"Failed to restore video files: {rollback_error}")
+ self.__invoker.services.logger.error("Problem deleting video record and file")
+ raise e
+
+ def delete_videos_on_board(self, board_id: str, user_id: Optional[str] = None) -> tuple[list[str], list[str]]:
+ try:
+ # When ``user_id`` is set the lookup filters to videos owned by that user so the
+ # cascade doesn't destroy other users' contributions to a public/shared board.
+ video_names = self.__invoker.services.board_video_records.get_all_board_video_names_for_board(
+ board_id, categories=None, is_intermediate=None, user_id=user_id
+ )
+ # Only delete records for files we actually managed to remove. Otherwise a
+ # transient FS error would leave the file orphaned on disk with no record
+ # pointing at it — the API would report success and the user would have no
+ # way to clean up the leak. The board itself will still be deleted by the
+ # caller, so any preserved records cascade to "uncategorized" via the
+ # board_videos FK.
+ deleted_video_names: list[str] = []
+ failed_video_names: list[str] = []
+ staged_deletes: list[tuple[str, object]] = []
+ for video_name in video_names:
+ try:
+ record = self.__invoker.services.video_records.get(video_name)
+ token = self.__invoker.services.video_files.stage_delete(
+ video_name, video_subfolder=record.video_subfolder
+ )
+ staged_deletes.append((video_name, token))
+ deleted_video_names.append(video_name)
+ except Exception as e:
+ failed_video_names.append(video_name)
+ self.__invoker.services.logger.error(
+ f"Failed to delete video file {video_name}; keeping record: {str(e)}"
+ )
+ try:
+ self.__invoker.services.video_records.delete_many(deleted_video_names)
+ except Exception:
+ for video_name, token in staged_deletes:
+ try:
+ self.__invoker.services.video_files.rollback_delete(token)
+ except Exception as rollback_error:
+ self.__invoker.services.logger.error(
+ f"Failed to restore staged video files for {video_name}: {rollback_error}"
+ )
+ raise
+ for _, token in staged_deletes:
+ try:
+ self.__invoker.services.video_files.commit_delete(token)
+ except Exception as cleanup_error:
+ self.__invoker.services.logger.error(f"Failed to purge staged video files: {cleanup_error}")
+ for video_name in deleted_video_names:
+ self._on_deleted(video_name)
+ return deleted_video_names, failed_video_names
+ except VideoRecordDeleteException:
+ self.__invoker.services.logger.error("Failed to delete video records")
+ raise
+ except VideoFileDeleteException:
+ self.__invoker.services.logger.error("Failed to delete video files")
+ raise
+ except Exception as e:
+ self.__invoker.services.logger.error(f"Problem deleting video records and files: {str(e)}")
+ raise e
+
+ def get_video_names(
+ self,
+ starred_first: bool = True,
+ order_dir: SQLiteDirection = SQLiteDirection.Descending,
+ video_origin: Optional[ResourceOrigin] = None,
+ categories: Optional[list[ImageCategory]] = None,
+ is_intermediate: Optional[bool] = None,
+ board_id: Optional[str] = None,
+ search_term: Optional[str] = None,
+ user_id: Optional[str] = None,
+ is_admin: bool = False,
+ ) -> VideoNamesResult:
+ try:
+ return self.__invoker.services.video_records.get_video_names(
+ starred_first=starred_first,
+ order_dir=order_dir,
+ video_origin=video_origin,
+ categories=categories,
+ is_intermediate=is_intermediate,
+ board_id=board_id,
+ search_term=search_term,
+ user_id=user_id,
+ is_admin=is_admin,
+ )
+ except Exception as e:
+ self.__invoker.services.logger.error("Problem getting video names")
+ raise e
diff --git a/invokeai/app/services/virtual_boards/virtual_boards_common.py b/invokeai/app/services/virtual_boards/virtual_boards_common.py
index e1df5a81ca5..9b396ecf802 100644
--- a/invokeai/app/services/virtual_boards/virtual_boards_common.py
+++ b/invokeai/app/services/virtual_boards/virtual_boards_common.py
@@ -4,11 +4,16 @@
class VirtualSubBoardDTO(BaseModel):
- """A virtual sub-board computed from image metadata, not stored in the database."""
+ """A virtual sub-board computed from image/video metadata, not stored in the database."""
virtual_board_id: str = Field(description="The virtual board ID, e.g. 'by_date:2026-03-18'.")
board_name: str = Field(description="The display name of the virtual sub-board, e.g. '2026-03-18'.")
date: str = Field(description="The ISO date string, e.g. '2026-03-18'.")
image_count: int = Field(description="The number of general images for this date.")
asset_count: int = Field(description="The number of asset images for this date.")
+ video_count: int = Field(default=0, description="The number of videos for this date.")
cover_image_name: Optional[str] = Field(default=None, description="The most recent image name for this date.")
+ cover_video_name: Optional[str] = Field(
+ default=None,
+ description="The most recent video name for this date. Set instead of cover_image_name when the newest item for the date is a video.",
+ )
diff --git a/invokeai/app/services/workflow_records/default_workflows/Extend Video - Wan 2.2 Lightning w_ Concept LoRAs.json b/invokeai/app/services/workflow_records/default_workflows/Extend Video - Wan 2.2 Lightning w_ Concept LoRAs.json
new file mode 100644
index 00000000000..34333a34f97
--- /dev/null
+++ b/invokeai/app/services/workflow_records/default_workflows/Extend Video - Wan 2.2 Lightning w_ Concept LoRAs.json
@@ -0,0 +1,1622 @@
+{
+ "name": "Extend Video - Wan 2.2 Lightning w/ Concept LoRAs",
+ "author": "InvokeAI",
+ "description": "This workflow adds frames to an existing video. Provide a starting video and use the sliders to select the starting and ending frames. The workflow will create a new video starting from the selected ending frame and then concatenate the new video to the end of the first video, performing a quick cross-fade at the selected ending frame to generate a smooth transition.\n\nInput slots for a concept LoRA pair (high and low noise) are provided.",
+ "version": "1.0.0",
+ "contact": "",
+ "tags": "wan2.2, video to video, video, lightning, lora",
+ "notes": "Each InvokeAI install assigns its own internal model IDs, so the model fields are left blank -- select these in the form after loading:\n\nMain Model node:\n- Main Model: Wan 2.2 I2V A14B High Noise (Q4_K_M GGUF recommended for <=16 GB VRAM)\n- Transformer (Low Noise): Wan 2.2 I2V A14B Low Noise (Q4_K_M)\n- VAE: Wan 2.2 A14B VAE\n- T5 Encoder: Wan T5 Encoder (UMT5-XXL)\n\nLightning LoRAs (4-step distillation, I2V variant):\n- Apply Lightning LoRA (High Noise): Wan 2.2 I2V Lightning High Noise (4-step)\n- Apply Lightning LoRA (Low Noise): Wan 2.2 I2V Lightning Low Noise (4-step)\n\nOptional concept LoRAs (your own style/subject LoRAs):\n- Apply Concept LoRA (High Noise) / (Low Noise): leave empty, or select a high/low-noise concept LoRA pair.\n\nManually edit the \"Concatenate Videos\" node to fine-tune the transition type and number of transition frames.",
+ "exposedFields": [
+ {
+ "nodeId": "1a0e6c7b-9d2f-4b3c-8e1a-2f3d4c5b6a7e",
+ "fieldName": "model"
+ },
+ {
+ "nodeId": "1a0e6c7b-9d2f-4b3c-8e1a-2f3d4c5b6a7e",
+ "fieldName": "transformer_low_noise_model"
+ },
+ {
+ "nodeId": "1a0e6c7b-9d2f-4b3c-8e1a-2f3d4c5b6a7e",
+ "fieldName": "component_source"
+ },
+ {
+ "nodeId": "1a0e6c7b-9d2f-4b3c-8e1a-2f3d4c5b6a7e",
+ "fieldName": "vae_model"
+ },
+ {
+ "nodeId": "1a0e6c7b-9d2f-4b3c-8e1a-2f3d4c5b6a7e",
+ "fieldName": "wan_t5_encoder_model"
+ },
+ {
+ "nodeId": "2b1f7d8c-ae3f-5c4d-9f2b-3a4e5d6c7b8f",
+ "fieldName": "prompt"
+ },
+ {
+ "nodeId": "3c2a8e9d-bf4a-6d5e-af3c-4b5f6e7d8c9a",
+ "fieldName": "prompt"
+ },
+ {
+ "nodeId": "4d3b9faf-c05b-7e6f-bf4d-5c6a7f8e9dab",
+ "fieldName": "steps"
+ },
+ {
+ "nodeId": "4d3b9faf-c05b-7e6f-bf4d-5c6a7f8e9dab",
+ "fieldName": "guidance_scale"
+ },
+ {
+ "nodeId": "4d3b9faf-c05b-7e6f-bf4d-5c6a7f8e9dab",
+ "fieldName": "guidance_scale_low_noise"
+ },
+ {
+ "nodeId": "4d3b9faf-c05b-7e6f-bf4d-5c6a7f8e9dab",
+ "fieldName": "width"
+ },
+ {
+ "nodeId": "4d3b9faf-c05b-7e6f-bf4d-5c6a7f8e9dab",
+ "fieldName": "height"
+ },
+ {
+ "nodeId": "4d3b9faf-c05b-7e6f-bf4d-5c6a7f8e9dab",
+ "fieldName": "num_frames"
+ },
+ {
+ "nodeId": "6f5dcb1c-e27d-9fb0-d16f-7e8cbaa1fcbd",
+ "fieldName": "fps"
+ }
+ ],
+ "meta": {
+ "version": "4.0.0",
+ "category": "default"
+ },
+ "nodes": [
+ {
+ "id": "1a0e6c7b-9d2f-4b3c-8e1a-2f3d4c5b6a7e",
+ "type": "invocation",
+ "data": {
+ "id": "1a0e6c7b-9d2f-4b3c-8e1a-2f3d4c5b6a7e",
+ "version": "1.0.1",
+ "nodePack": "invokeai",
+ "label": "",
+ "notes": "",
+ "type": "wan_model_loader",
+ "inputs": {
+ "model": {
+ "name": "model",
+ "label": "Transformer (High Noise)",
+ "description": ""
+ },
+ "transformer_low_noise_model": {
+ "name": "transformer_low_noise_model",
+ "label": "",
+ "description": ""
+ },
+ "vae_model": {
+ "name": "vae_model",
+ "label": "",
+ "description": ""
+ },
+ "wan_t5_encoder_model": {
+ "name": "wan_t5_encoder_model",
+ "label": "",
+ "description": ""
+ },
+ "component_source": {
+ "name": "component_source",
+ "label": "",
+ "description": ""
+ }
+ },
+ "dynamicInputTemplates": {},
+ "isOpen": true,
+ "isIntermediate": true,
+ "useCache": false
+ },
+ "position": {
+ "x": 0,
+ "y": 933.5
+ }
+ },
+ {
+ "id": "2b1f7d8c-ae3f-5c4d-9f2b-3a4e5d6c7b8f",
+ "type": "invocation",
+ "data": {
+ "id": "2b1f7d8c-ae3f-5c4d-9f2b-3a4e5d6c7b8f",
+ "version": "1.0.0",
+ "nodePack": "invokeai",
+ "label": "Positive Prompt",
+ "notes": "",
+ "type": "wan_text_encoder",
+ "inputs": {
+ "prompt": {
+ "name": "prompt",
+ "label": "",
+ "description": "",
+ "value": ""
+ },
+ "wan_t5_encoder": {
+ "name": "wan_t5_encoder",
+ "label": "",
+ "description": ""
+ }
+ },
+ "dynamicInputTemplates": {},
+ "isOpen": true,
+ "isIntermediate": true,
+ "useCache": true
+ },
+ "position": {
+ "x": 1050,
+ "y": 1595.5
+ }
+ },
+ {
+ "id": "3c2a8e9d-bf4a-6d5e-af3c-4b5f6e7d8c9a",
+ "type": "invocation",
+ "data": {
+ "id": "3c2a8e9d-bf4a-6d5e-af3c-4b5f6e7d8c9a",
+ "version": "1.0.0",
+ "nodePack": "invokeai",
+ "label": "Negative Prompt (unused at CFG=1.0)",
+ "notes": "",
+ "type": "wan_text_encoder",
+ "inputs": {
+ "prompt": {
+ "name": "prompt",
+ "label": "",
+ "description": "",
+ "value": " "
+ },
+ "wan_t5_encoder": {
+ "name": "wan_t5_encoder",
+ "label": "",
+ "description": ""
+ }
+ },
+ "dynamicInputTemplates": {},
+ "isOpen": true,
+ "isIntermediate": true,
+ "useCache": true
+ },
+ "position": {
+ "x": 1050,
+ "y": 1415.5
+ }
+ },
+ {
+ "id": "5e4cab0b-d16c-8faf-c05e-6d7baf90ebbc",
+ "type": "invocation",
+ "data": {
+ "id": "5e4cab0b-d16c-8faf-c05e-6d7baf90ebbc",
+ "version": "1.0.1",
+ "nodePack": "invokeai",
+ "label": "",
+ "notes": "",
+ "type": "rand_int",
+ "inputs": {
+ "low": {
+ "name": "low",
+ "label": "",
+ "description": "",
+ "value": 0
+ },
+ "high": {
+ "name": "high",
+ "label": "",
+ "description": "",
+ "value": 2147483647
+ }
+ },
+ "dynamicInputTemplates": {},
+ "isOpen": true,
+ "isIntermediate": true,
+ "useCache": false
+ },
+ "position": {
+ "x": 1050,
+ "y": 471.5
+ }
+ },
+ {
+ "id": "4d3b9faf-c05b-7e6f-bf4d-5c6a7f8e9dab",
+ "type": "invocation",
+ "data": {
+ "id": "4d3b9faf-c05b-7e6f-bf4d-5c6a7f8e9dab",
+ "version": "1.0.0",
+ "nodePack": "invokeai",
+ "label": "",
+ "notes": "",
+ "type": "wan_video_denoise",
+ "inputs": {
+ "transformer": {
+ "name": "transformer",
+ "label": "",
+ "description": ""
+ },
+ "positive_conditioning": {
+ "name": "positive_conditioning",
+ "label": "",
+ "description": ""
+ },
+ "negative_conditioning": {
+ "name": "negative_conditioning",
+ "label": "",
+ "description": ""
+ },
+ "ref_image": {
+ "name": "ref_image",
+ "label": "",
+ "description": ""
+ },
+ "guidance_scale": {
+ "name": "guidance_scale",
+ "label": "CFG (High Noise)",
+ "description": "",
+ "value": 1
+ },
+ "guidance_scale_low_noise": {
+ "name": "guidance_scale_low_noise",
+ "label": "CFG (Low Noise)",
+ "description": "",
+ "value": 1
+ },
+ "width": {
+ "name": "width",
+ "label": "",
+ "description": "",
+ "value": 832
+ },
+ "height": {
+ "name": "height",
+ "label": "",
+ "description": "",
+ "value": 480
+ },
+ "num_frames": {
+ "name": "num_frames",
+ "label": "Frames",
+ "description": "",
+ "value": 81
+ },
+ "steps": {
+ "name": "steps",
+ "label": "",
+ "description": "",
+ "value": 4
+ },
+ "seed": {
+ "name": "seed",
+ "label": "",
+ "description": "",
+ "value": 0
+ }
+ },
+ "dynamicInputTemplates": {},
+ "isOpen": true,
+ "isIntermediate": true,
+ "useCache": true
+ },
+ "position": {
+ "x": 1400,
+ "y": 905.5
+ }
+ },
+ {
+ "id": "6f5dcb1c-e27d-9fb0-d16f-7e8cbaa1fcbd",
+ "type": "invocation",
+ "data": {
+ "id": "6f5dcb1c-e27d-9fb0-d16f-7e8cbaa1fcbd",
+ "version": "1.0.0",
+ "nodePack": "invokeai",
+ "label": "",
+ "notes": "",
+ "type": "wan_l2v",
+ "inputs": {
+ "board": {
+ "name": "board",
+ "label": "",
+ "description": ""
+ },
+ "metadata": {
+ "name": "metadata",
+ "label": "",
+ "description": ""
+ },
+ "latents": {
+ "name": "latents",
+ "label": "",
+ "description": ""
+ },
+ "vae": {
+ "name": "vae",
+ "label": "",
+ "description": ""
+ },
+ "fps": {
+ "name": "fps",
+ "label": "FPS",
+ "description": "",
+ "value": 16
+ }
+ },
+ "dynamicInputTemplates": {},
+ "isOpen": true,
+ "isIntermediate": false,
+ "useCache": true
+ },
+ "position": {
+ "x": 1750,
+ "y": 960.5
+ }
+ },
+ {
+ "id": "291abec8-b996-4c7b-959c-360f56a3ed60",
+ "type": "invocation",
+ "data": {
+ "id": "291abec8-b996-4c7b-959c-360f56a3ed60",
+ "version": "1.0.1",
+ "nodePack": "invokeai",
+ "label": "Apply Lightning LoRA (High Noise)",
+ "notes": "",
+ "type": "wan_lora_loader",
+ "inputs": {
+ "lora": {
+ "name": "lora",
+ "label": "Lightning LoRA (High Noise)",
+ "description": ""
+ },
+ "weight": {
+ "name": "weight",
+ "label": "",
+ "description": "",
+ "value": 1
+ },
+ "target": {
+ "name": "target",
+ "label": "",
+ "description": "",
+ "value": "high"
+ },
+ "transformer": {
+ "name": "transformer",
+ "label": "",
+ "description": ""
+ }
+ },
+ "dynamicInputTemplates": {},
+ "isOpen": true,
+ "isIntermediate": true,
+ "useCache": true
+ },
+ "position": {
+ "x": 350,
+ "y": 1145.5
+ }
+ },
+ {
+ "id": "ae8f115d-a3b8-4f76-b280-7313366a94ad",
+ "type": "invocation",
+ "data": {
+ "id": "ae8f115d-a3b8-4f76-b280-7313366a94ad",
+ "version": "1.0.1",
+ "nodePack": "invokeai",
+ "label": "Apply Lightning LoRA (Low Noise)",
+ "notes": "",
+ "type": "wan_lora_loader",
+ "inputs": {
+ "lora": {
+ "name": "lora",
+ "label": "Lightning LoRA (Low Noise)",
+ "description": ""
+ },
+ "weight": {
+ "name": "weight",
+ "label": "",
+ "description": "",
+ "value": 1
+ },
+ "target": {
+ "name": "target",
+ "label": "",
+ "description": "",
+ "value": "low"
+ },
+ "transformer": {
+ "name": "transformer",
+ "label": "",
+ "description": ""
+ }
+ },
+ "dynamicInputTemplates": {},
+ "isOpen": true,
+ "isIntermediate": true,
+ "useCache": true
+ },
+ "position": {
+ "x": 700,
+ "y": 1145.5
+ }
+ },
+ {
+ "id": "97c6fecb-8354-4ace-b337-e544b0ba9982",
+ "type": "invocation",
+ "data": {
+ "id": "97c6fecb-8354-4ace-b337-e544b0ba9982",
+ "version": "1.2.0",
+ "nodePack": "invokeai",
+ "label": "",
+ "notes": "",
+ "type": "wan_ref_image_encoder",
+ "inputs": {
+ "image": {
+ "name": "image",
+ "label": "",
+ "description": ""
+ },
+ "vae": {
+ "name": "vae",
+ "label": "",
+ "description": ""
+ },
+ "width": {
+ "name": "width",
+ "label": "",
+ "description": "",
+ "value": 1024
+ },
+ "height": {
+ "name": "height",
+ "label": "",
+ "description": "",
+ "value": 1024
+ },
+ "num_frames": {
+ "name": "num_frames",
+ "label": "",
+ "description": "",
+ "value": 1
+ },
+ "end_image": {
+ "name": "end_image",
+ "label": "",
+ "description": ""
+ }
+ },
+ "dynamicInputTemplates": {},
+ "isOpen": true,
+ "isIntermediate": true,
+ "useCache": true
+ },
+ "position": {
+ "x": 1050,
+ "y": 691.5
+ }
+ },
+ {
+ "id": "d27f29bf-6f26-4088-b1cc-c722ecd9fd61",
+ "type": "invocation",
+ "data": {
+ "id": "d27f29bf-6f26-4088-b1cc-c722ecd9fd61",
+ "version": "1.1.0",
+ "nodePack": "invokeai",
+ "label": "",
+ "notes": "",
+ "type": "wan_i2v_ideal_dimensions",
+ "inputs": {
+ "width": {
+ "name": "width",
+ "label": "",
+ "description": "",
+ "value": 1024
+ },
+ "height": {
+ "name": "height",
+ "label": "",
+ "description": "",
+ "value": 1024
+ },
+ "target_resolution": {
+ "name": "target_resolution",
+ "label": "Video Resolution",
+ "description": "",
+ "value": "480p"
+ },
+ "rounding": {
+ "name": "rounding",
+ "label": "",
+ "description": "",
+ "value": "nearest"
+ }
+ },
+ "dynamicInputTemplates": {},
+ "isOpen": true,
+ "isIntermediate": true,
+ "useCache": true
+ },
+ "position": {
+ "x": 700,
+ "y": 857.5
+ }
+ },
+ {
+ "id": "c857e3e8-334f-4a6e-9013-3326316e4281",
+ "type": "invocation",
+ "data": {
+ "id": "c857e3e8-334f-4a6e-9013-3326316e4281",
+ "version": "1.0.1",
+ "nodePack": "invokeai",
+ "label": "Num Frames",
+ "notes": "",
+ "type": "integer",
+ "inputs": {
+ "value": {
+ "name": "value",
+ "label": "Frames",
+ "description": "",
+ "value": 81
+ }
+ },
+ "dynamicInputTemplates": {},
+ "isOpen": true,
+ "isIntermediate": true,
+ "useCache": true
+ },
+ "position": {
+ "x": 700,
+ "y": 587.5
+ }
+ },
+ {
+ "id": "4b32c22b-679d-4d5e-a167-c3309265fcd9",
+ "type": "invocation",
+ "data": {
+ "id": "4b32c22b-679d-4d5e-a167-c3309265fcd9",
+ "version": "1.0.0",
+ "nodePack": "invokeai",
+ "label": "",
+ "notes": "",
+ "type": "video_concat",
+ "inputs": {
+ "board": {
+ "name": "board",
+ "label": "",
+ "description": "",
+ "value": "auto"
+ },
+ "metadata": {
+ "name": "metadata",
+ "label": "",
+ "description": ""
+ },
+ "videos": {
+ "name": "videos",
+ "label": "",
+ "description": ""
+ },
+ "transition": {
+ "name": "transition",
+ "label": "",
+ "description": "",
+ "value": "crossfade"
+ },
+ "transition_frames": {
+ "name": "transition_frames",
+ "label": "",
+ "description": "",
+ "value": 2
+ },
+ "fps": {
+ "name": "fps",
+ "label": "",
+ "description": "",
+ "value": 0
+ }
+ },
+ "dynamicInputTemplates": {},
+ "isOpen": true,
+ "isIntermediate": false,
+ "useCache": true
+ },
+ "position": {
+ "x": 2450,
+ "y": 173.5
+ }
+ },
+ {
+ "id": "6030b112-b669-4626-86ab-b1bffa96f9d6",
+ "type": "invocation",
+ "data": {
+ "id": "6030b112-b669-4626-86ab-b1bffa96f9d6",
+ "version": "1.1.0",
+ "nodePack": "invokeai",
+ "label": "",
+ "notes": "",
+ "type": "collect",
+ "inputs": {
+ "item": {
+ "name": "item",
+ "label": "",
+ "description": ""
+ },
+ "collection": {
+ "name": "collection",
+ "label": "",
+ "description": ""
+ }
+ },
+ "dynamicInputTemplates": {},
+ "isOpen": true,
+ "isIntermediate": true,
+ "useCache": true
+ },
+ "position": {
+ "x": 1750,
+ "y": 401.5
+ }
+ },
+ {
+ "id": "9d72df22-9794-4c8b-af0b-a01548b5f852",
+ "type": "invocation",
+ "data": {
+ "id": "9d72df22-9794-4c8b-af0b-a01548b5f852",
+ "version": "1.1.0",
+ "nodePack": "invokeai",
+ "label": "",
+ "notes": "",
+ "type": "collect",
+ "inputs": {
+ "item": {
+ "name": "item",
+ "label": "",
+ "description": ""
+ },
+ "collection": {
+ "name": "collection",
+ "label": "",
+ "description": ""
+ }
+ },
+ "dynamicInputTemplates": {},
+ "isOpen": true,
+ "isIntermediate": true,
+ "useCache": true
+ },
+ "position": {
+ "x": 2100,
+ "y": 401.5
+ }
+ },
+ {
+ "id": "bdba5c5c-d0c2-4bfa-a910-3f4405b25cdc",
+ "type": "invocation",
+ "data": {
+ "id": "bdba5c5c-d0c2-4bfa-a910-3f4405b25cdc",
+ "version": "1.0.1",
+ "nodePack": "invokeai",
+ "label": "",
+ "notes": "",
+ "type": "float_to_int",
+ "inputs": {
+ "value": {
+ "name": "value",
+ "label": "",
+ "description": "",
+ "value": 0
+ },
+ "multiple": {
+ "name": "multiple",
+ "label": "",
+ "description": "",
+ "value": 1
+ },
+ "method": {
+ "name": "method",
+ "label": "",
+ "description": "",
+ "value": "Nearest"
+ }
+ },
+ "dynamicInputTemplates": {},
+ "isOpen": true,
+ "isIntermediate": true,
+ "useCache": true
+ },
+ "position": {
+ "x": 1400,
+ "y": 229.5
+ }
+ },
+ {
+ "id": "52e0f43e-0593-4ba6-bb50-df63464a6189",
+ "type": "invocation",
+ "data": {
+ "id": "52e0f43e-0593-4ba6-bb50-df63464a6189",
+ "version": "1.1.0",
+ "nodePack": "invokeai",
+ "label": "",
+ "notes": "",
+ "type": "extract_video_range",
+ "inputs": {
+ "board": {
+ "name": "board",
+ "label": "",
+ "description": "",
+ "value": "auto"
+ },
+ "metadata": {
+ "name": "metadata",
+ "label": "",
+ "description": ""
+ },
+ "video": {
+ "name": "video",
+ "label": "Initial Video",
+ "description": "The video so far. Specify the start and end positions."
+ },
+ "start_frame": {
+ "name": "start_frame",
+ "label": "",
+ "description": "First frame to keep.",
+ "value": 0
+ },
+ "end_frame": {
+ "name": "end_frame",
+ "label": "",
+ "description": "Last frame to keep.",
+ "value": -2
+ },
+ "fps": {
+ "name": "fps",
+ "label": "",
+ "description": "",
+ "value": 0
+ }
+ },
+ "dynamicInputTemplates": {},
+ "isOpen": true,
+ "isIntermediate": true,
+ "useCache": true
+ },
+ "position": {
+ "x": 0,
+ "y": 0
+ }
+ },
+ {
+ "id": "c2b5afb3-d079-4afb-9354-4c1a7aeae73a",
+ "type": "invocation",
+ "data": {
+ "id": "c2b5afb3-d079-4afb-9354-4c1a7aeae73a",
+ "version": "1.1.0",
+ "nodePack": "invokeai",
+ "label": "",
+ "notes": "",
+ "type": "video_frame_extract",
+ "inputs": {
+ "board": {
+ "name": "board",
+ "label": "",
+ "description": "",
+ "value": "auto"
+ },
+ "metadata": {
+ "name": "metadata",
+ "label": "",
+ "description": ""
+ },
+ "video": {
+ "name": "video",
+ "label": "",
+ "description": ""
+ },
+ "frame_index": {
+ "name": "frame_index",
+ "label": "",
+ "description": "",
+ "value": -1
+ }
+ },
+ "dynamicInputTemplates": {},
+ "isOpen": true,
+ "isIntermediate": true,
+ "useCache": true
+ },
+ "position": {
+ "x": 350,
+ "y": 684.5
+ }
+ },
+ {
+ "id": "c1a982eb-c1dd-4565-a7d4-25dfc58339fd",
+ "type": "invocation",
+ "data": {
+ "id": "c1a982eb-c1dd-4565-a7d4-25dfc58339fd",
+ "type": "wan_lora_collection_loader",
+ "version": "1.0.1",
+ "label": "",
+ "notes": "",
+ "isOpen": true,
+ "isIntermediate": true,
+ "useCache": true,
+ "nodePack": "invokeai",
+ "inputs": {
+ "loras": {
+ "name": "loras",
+ "label": "Concept LoRAs",
+ "description": "",
+ "value": []
+ },
+ "transformer": {
+ "name": "transformer",
+ "label": "",
+ "description": ""
+ }
+ },
+ "dynamicInputTemplates": {}
+ },
+ "position": {
+ "x": 1050,
+ "y": 1194.5
+ }
+ }
+ ],
+ "edges": [
+ {
+ "id": "edge-loader-t5-pos",
+ "type": "default",
+ "source": "1a0e6c7b-9d2f-4b3c-8e1a-2f3d4c5b6a7e",
+ "target": "2b1f7d8c-ae3f-5c4d-9f2b-3a4e5d6c7b8f",
+ "sourceHandle": "wan_t5_encoder",
+ "targetHandle": "wan_t5_encoder"
+ },
+ {
+ "id": "edge-loader-t5-neg",
+ "type": "default",
+ "source": "1a0e6c7b-9d2f-4b3c-8e1a-2f3d4c5b6a7e",
+ "target": "3c2a8e9d-bf4a-6d5e-af3c-4b5f6e7d8c9a",
+ "sourceHandle": "wan_t5_encoder",
+ "targetHandle": "wan_t5_encoder"
+ },
+ {
+ "id": "edge-loader-vae-l2v",
+ "type": "default",
+ "source": "1a0e6c7b-9d2f-4b3c-8e1a-2f3d4c5b6a7e",
+ "target": "6f5dcb1c-e27d-9fb0-d16f-7e8cbaa1fcbd",
+ "sourceHandle": "vae",
+ "targetHandle": "vae"
+ },
+ {
+ "id": "edge-pos-cond-denoise",
+ "type": "default",
+ "source": "2b1f7d8c-ae3f-5c4d-9f2b-3a4e5d6c7b8f",
+ "target": "4d3b9faf-c05b-7e6f-bf4d-5c6a7f8e9dab",
+ "sourceHandle": "conditioning",
+ "targetHandle": "positive_conditioning"
+ },
+ {
+ "id": "edge-neg-cond-denoise",
+ "type": "default",
+ "source": "3c2a8e9d-bf4a-6d5e-af3c-4b5f6e7d8c9a",
+ "target": "4d3b9faf-c05b-7e6f-bf4d-5c6a7f8e9dab",
+ "sourceHandle": "conditioning",
+ "targetHandle": "negative_conditioning"
+ },
+ {
+ "id": "edge-rand-seed-denoise",
+ "type": "default",
+ "source": "5e4cab0b-d16c-8faf-c05e-6d7baf90ebbc",
+ "target": "4d3b9faf-c05b-7e6f-bf4d-5c6a7f8e9dab",
+ "sourceHandle": "value",
+ "targetHandle": "seed"
+ },
+ {
+ "id": "edge-denoise-latents-l2v",
+ "type": "default",
+ "source": "4d3b9faf-c05b-7e6f-bf4d-5c6a7f8e9dab",
+ "target": "6f5dcb1c-e27d-9fb0-d16f-7e8cbaa1fcbd",
+ "sourceHandle": "latents",
+ "targetHandle": "latents"
+ },
+ {
+ "id": "reactflow__edge-1a0e6c7b-9d2f-4b3c-8e1a-2f3d4c5b6a7etransformer-291abec8-b996-4c7b-959c-360f56a3ed60transformer",
+ "type": "default",
+ "source": "1a0e6c7b-9d2f-4b3c-8e1a-2f3d4c5b6a7e",
+ "target": "291abec8-b996-4c7b-959c-360f56a3ed60",
+ "sourceHandle": "transformer",
+ "targetHandle": "transformer"
+ },
+ {
+ "id": "reactflow__edge-291abec8-b996-4c7b-959c-360f56a3ed60transformer-ae8f115d-a3b8-4f76-b280-7313366a94adtransformer",
+ "type": "default",
+ "source": "291abec8-b996-4c7b-959c-360f56a3ed60",
+ "target": "ae8f115d-a3b8-4f76-b280-7313366a94ad",
+ "sourceHandle": "transformer",
+ "targetHandle": "transformer"
+ },
+ {
+ "id": "reactflow__edge-97c6fecb-8354-4ace-b337-e544b0ba9982ref_image-4d3b9faf-c05b-7e6f-bf4d-5c6a7f8e9dabref_image",
+ "type": "default",
+ "source": "97c6fecb-8354-4ace-b337-e544b0ba9982",
+ "target": "4d3b9faf-c05b-7e6f-bf4d-5c6a7f8e9dab",
+ "sourceHandle": "ref_image",
+ "targetHandle": "ref_image"
+ },
+ {
+ "id": "reactflow__edge-1a0e6c7b-9d2f-4b3c-8e1a-2f3d4c5b6a7evae-97c6fecb-8354-4ace-b337-e544b0ba9982vae",
+ "type": "default",
+ "source": "1a0e6c7b-9d2f-4b3c-8e1a-2f3d4c5b6a7e",
+ "target": "97c6fecb-8354-4ace-b337-e544b0ba9982",
+ "sourceHandle": "vae",
+ "targetHandle": "vae"
+ },
+ {
+ "id": "reactflow__edge-d27f29bf-6f26-4088-b1cc-c722ecd9fd61width-4d3b9faf-c05b-7e6f-bf4d-5c6a7f8e9dabwidth",
+ "type": "default",
+ "source": "d27f29bf-6f26-4088-b1cc-c722ecd9fd61",
+ "target": "4d3b9faf-c05b-7e6f-bf4d-5c6a7f8e9dab",
+ "sourceHandle": "width",
+ "targetHandle": "width"
+ },
+ {
+ "id": "reactflow__edge-d27f29bf-6f26-4088-b1cc-c722ecd9fd61height-4d3b9faf-c05b-7e6f-bf4d-5c6a7f8e9dabheight",
+ "type": "default",
+ "source": "d27f29bf-6f26-4088-b1cc-c722ecd9fd61",
+ "target": "4d3b9faf-c05b-7e6f-bf4d-5c6a7f8e9dab",
+ "sourceHandle": "height",
+ "targetHandle": "height"
+ },
+ {
+ "id": "reactflow__edge-d27f29bf-6f26-4088-b1cc-c722ecd9fd61width-97c6fecb-8354-4ace-b337-e544b0ba9982width",
+ "type": "default",
+ "source": "d27f29bf-6f26-4088-b1cc-c722ecd9fd61",
+ "target": "97c6fecb-8354-4ace-b337-e544b0ba9982",
+ "sourceHandle": "width",
+ "targetHandle": "width"
+ },
+ {
+ "id": "reactflow__edge-d27f29bf-6f26-4088-b1cc-c722ecd9fd61height-97c6fecb-8354-4ace-b337-e544b0ba9982height",
+ "type": "default",
+ "source": "d27f29bf-6f26-4088-b1cc-c722ecd9fd61",
+ "target": "97c6fecb-8354-4ace-b337-e544b0ba9982",
+ "sourceHandle": "height",
+ "targetHandle": "height"
+ },
+ {
+ "id": "reactflow__edge-c857e3e8-334f-4a6e-9013-3326316e4281value-4d3b9faf-c05b-7e6f-bf4d-5c6a7f8e9dabnum_frames",
+ "type": "default",
+ "source": "c857e3e8-334f-4a6e-9013-3326316e4281",
+ "target": "4d3b9faf-c05b-7e6f-bf4d-5c6a7f8e9dab",
+ "sourceHandle": "value",
+ "targetHandle": "num_frames"
+ },
+ {
+ "id": "reactflow__edge-c857e3e8-334f-4a6e-9013-3326316e4281value-97c6fecb-8354-4ace-b337-e544b0ba9982num_frames",
+ "type": "default",
+ "source": "c857e3e8-334f-4a6e-9013-3326316e4281",
+ "target": "97c6fecb-8354-4ace-b337-e544b0ba9982",
+ "sourceHandle": "value",
+ "targetHandle": "num_frames"
+ },
+ {
+ "id": "reactflow__edge-c2b5afb3-d079-4afb-9354-4c1a7aeae73awidth-d27f29bf-6f26-4088-b1cc-c722ecd9fd61width",
+ "type": "default",
+ "source": "c2b5afb3-d079-4afb-9354-4c1a7aeae73a",
+ "target": "d27f29bf-6f26-4088-b1cc-c722ecd9fd61",
+ "sourceHandle": "width",
+ "targetHandle": "width"
+ },
+ {
+ "id": "reactflow__edge-c2b5afb3-d079-4afb-9354-4c1a7aeae73aheight-d27f29bf-6f26-4088-b1cc-c722ecd9fd61height",
+ "type": "default",
+ "source": "c2b5afb3-d079-4afb-9354-4c1a7aeae73a",
+ "target": "d27f29bf-6f26-4088-b1cc-c722ecd9fd61",
+ "sourceHandle": "height",
+ "targetHandle": "height"
+ },
+ {
+ "id": "reactflow__edge-c2b5afb3-d079-4afb-9354-4c1a7aeae73aimage-97c6fecb-8354-4ace-b337-e544b0ba9982image",
+ "type": "default",
+ "source": "c2b5afb3-d079-4afb-9354-4c1a7aeae73a",
+ "target": "97c6fecb-8354-4ace-b337-e544b0ba9982",
+ "sourceHandle": "image",
+ "targetHandle": "image"
+ },
+ {
+ "id": "reactflow__edge-52e0f43e-0593-4ba6-bb50-df63464a6189video-c2b5afb3-d079-4afb-9354-4c1a7aeae73avideo",
+ "type": "default",
+ "source": "52e0f43e-0593-4ba6-bb50-df63464a6189",
+ "target": "c2b5afb3-d079-4afb-9354-4c1a7aeae73a",
+ "sourceHandle": "video",
+ "targetHandle": "video"
+ },
+ {
+ "id": "reactflow__edge-52e0f43e-0593-4ba6-bb50-df63464a6189video-6030b112-b669-4626-86ab-b1bffa96f9d6item",
+ "type": "default",
+ "source": "52e0f43e-0593-4ba6-bb50-df63464a6189",
+ "target": "6030b112-b669-4626-86ab-b1bffa96f9d6",
+ "sourceHandle": "video",
+ "targetHandle": "item"
+ },
+ {
+ "id": "reactflow__edge-6030b112-b669-4626-86ab-b1bffa96f9d6collection-9d72df22-9794-4c8b-af0b-a01548b5f852collection",
+ "type": "default",
+ "source": "6030b112-b669-4626-86ab-b1bffa96f9d6",
+ "target": "9d72df22-9794-4c8b-af0b-a01548b5f852",
+ "sourceHandle": "collection",
+ "targetHandle": "collection"
+ },
+ {
+ "id": "reactflow__edge-6f5dcb1c-e27d-9fb0-d16f-7e8cbaa1fcbdvideo-9d72df22-9794-4c8b-af0b-a01548b5f852item",
+ "type": "default",
+ "source": "6f5dcb1c-e27d-9fb0-d16f-7e8cbaa1fcbd",
+ "target": "9d72df22-9794-4c8b-af0b-a01548b5f852",
+ "sourceHandle": "video",
+ "targetHandle": "item"
+ },
+ {
+ "id": "reactflow__edge-9d72df22-9794-4c8b-af0b-a01548b5f852collection-4b32c22b-679d-4d5e-a167-c3309265fcd9videos",
+ "type": "default",
+ "source": "9d72df22-9794-4c8b-af0b-a01548b5f852",
+ "target": "4b32c22b-679d-4d5e-a167-c3309265fcd9",
+ "sourceHandle": "collection",
+ "targetHandle": "videos"
+ },
+ {
+ "id": "reactflow__edge-52e0f43e-0593-4ba6-bb50-df63464a6189fps-bdba5c5c-d0c2-4bfa-a910-3f4405b25cdcvalue",
+ "type": "default",
+ "source": "52e0f43e-0593-4ba6-bb50-df63464a6189",
+ "target": "bdba5c5c-d0c2-4bfa-a910-3f4405b25cdc",
+ "sourceHandle": "fps",
+ "targetHandle": "value"
+ },
+ {
+ "id": "reactflow__edge-bdba5c5c-d0c2-4bfa-a910-3f4405b25cdcvalue-4b32c22b-679d-4d5e-a167-c3309265fcd9fps",
+ "type": "default",
+ "source": "bdba5c5c-d0c2-4bfa-a910-3f4405b25cdc",
+ "target": "4b32c22b-679d-4d5e-a167-c3309265fcd9",
+ "sourceHandle": "value",
+ "targetHandle": "fps"
+ },
+ {
+ "id": "reactflow__edge-bdba5c5c-d0c2-4bfa-a910-3f4405b25cdcvalue-6f5dcb1c-e27d-9fb0-d16f-7e8cbaa1fcbdfps",
+ "type": "default",
+ "source": "bdba5c5c-d0c2-4bfa-a910-3f4405b25cdc",
+ "target": "6f5dcb1c-e27d-9fb0-d16f-7e8cbaa1fcbd",
+ "sourceHandle": "value",
+ "targetHandle": "fps"
+ },
+ {
+ "id": "reactflow__edge-ae8f115d-a3b8-4f76-b280-7313366a94adtransformer-c1a982eb-c1dd-4565-a7d4-25dfc58339fdtransformer",
+ "type": "default",
+ "source": "ae8f115d-a3b8-4f76-b280-7313366a94ad",
+ "target": "c1a982eb-c1dd-4565-a7d4-25dfc58339fd",
+ "sourceHandle": "transformer",
+ "targetHandle": "transformer"
+ },
+ {
+ "id": "reactflow__edge-c1a982eb-c1dd-4565-a7d4-25dfc58339fdtransformer-4d3b9faf-c05b-7e6f-bf4d-5c6a7f8e9dabtransformer",
+ "type": "default",
+ "source": "c1a982eb-c1dd-4565-a7d4-25dfc58339fd",
+ "target": "4d3b9faf-c05b-7e6f-bf4d-5c6a7f8e9dab",
+ "sourceHandle": "transformer",
+ "targetHandle": "transformer"
+ }
+ ],
+ "form": {
+ "elements": {
+ "root": {
+ "id": "root",
+ "data": {
+ "layout": "column",
+ "children": [
+ "heading-XDPaQofdEE",
+ "node-field-KcT0glAFDK",
+ "container-1lpcsDWjRx",
+ "container-i8V1oJeQCP",
+ "container-mUeBcw2tYf",
+ "divider-2T3tRpIIJO",
+ "heading-wuPtY8B4vR",
+ "container-AkpUH1WtID",
+ "divider-w19S1gIyNJ",
+ "heading-TwsoE3wQi9",
+ "container-0u7wyxEdNa",
+ "heading-7ZKUFfihCo",
+ "node-field-hFIdNT3hYF",
+ "divider-FKc8lld0Eb",
+ "heading-10PuWMKJg9",
+ "container-C0z6ru33to"
+ ]
+ },
+ "type": "container"
+ },
+ "node-field-QDqQ4XOKvg": {
+ "id": "node-field-QDqQ4XOKvg",
+ "parentId": "container-7g1Xedgg8T",
+ "data": {
+ "fieldIdentifier": {
+ "nodeId": "4d3b9faf-c05b-7e6f-bf4d-5c6a7f8e9dab",
+ "fieldName": "guidance_scale_low_noise"
+ },
+ "showDescription": false,
+ "settings": {
+ "type": "float-field-config",
+ "showShuffle": false,
+ "component": "number-input"
+ }
+ },
+ "type": "node-field"
+ },
+ "node-field-PdU0Sz2m5z": {
+ "id": "node-field-PdU0Sz2m5z",
+ "parentId": "container-y0w7TfJC7e",
+ "data": {
+ "fieldIdentifier": {
+ "nodeId": "4d3b9faf-c05b-7e6f-bf4d-5c6a7f8e9dab",
+ "fieldName": "guidance_scale"
+ },
+ "showDescription": false,
+ "settings": {
+ "type": "float-field-config",
+ "showShuffle": false,
+ "component": "number-input"
+ }
+ },
+ "type": "node-field"
+ },
+ "node-field-UiQ7MaseDR": {
+ "id": "node-field-UiQ7MaseDR",
+ "parentId": "container-mUeBcw2tYf",
+ "data": {
+ "fieldIdentifier": {
+ "nodeId": "4d3b9faf-c05b-7e6f-bf4d-5c6a7f8e9dab",
+ "fieldName": "steps"
+ },
+ "showDescription": false,
+ "settings": {
+ "type": "integer-field-config",
+ "showShuffle": false,
+ "component": "number-input"
+ }
+ },
+ "type": "node-field"
+ },
+ "node-field-KcT0glAFDK": {
+ "id": "node-field-KcT0glAFDK",
+ "parentId": "root",
+ "data": {
+ "fieldIdentifier": {
+ "nodeId": "2b1f7d8c-ae3f-5c4d-9f2b-3a4e5d6c7b8f",
+ "fieldName": "prompt"
+ },
+ "showDescription": false,
+ "settings": {
+ "type": "string-field-config",
+ "component": "textarea"
+ }
+ },
+ "type": "node-field"
+ },
+ "node-field-N9mexEKkYg": {
+ "id": "node-field-N9mexEKkYg",
+ "parentId": "container-C0z6ru33to",
+ "data": {
+ "fieldIdentifier": {
+ "nodeId": "1a0e6c7b-9d2f-4b3c-8e1a-2f3d4c5b6a7e",
+ "fieldName": "wan_t5_encoder_model"
+ },
+ "showDescription": false
+ },
+ "type": "node-field"
+ },
+ "node-field-2u9lAbMBnD": {
+ "id": "node-field-2u9lAbMBnD",
+ "parentId": "container-C0z6ru33to",
+ "data": {
+ "fieldIdentifier": {
+ "nodeId": "1a0e6c7b-9d2f-4b3c-8e1a-2f3d4c5b6a7e",
+ "fieldName": "vae_model"
+ },
+ "showDescription": false
+ },
+ "type": "node-field"
+ },
+ "node-field-CTGqrO3x8o": {
+ "id": "node-field-CTGqrO3x8o",
+ "parentId": "container-C0z6ru33to",
+ "data": {
+ "fieldIdentifier": {
+ "nodeId": "1a0e6c7b-9d2f-4b3c-8e1a-2f3d4c5b6a7e",
+ "fieldName": "component_source"
+ },
+ "showDescription": false
+ },
+ "type": "node-field"
+ },
+ "node-field-7G9srQT2Uw": {
+ "id": "node-field-7G9srQT2Uw",
+ "parentId": "container-7g1Xedgg8T",
+ "data": {
+ "fieldIdentifier": {
+ "nodeId": "1a0e6c7b-9d2f-4b3c-8e1a-2f3d4c5b6a7e",
+ "fieldName": "transformer_low_noise_model"
+ },
+ "showDescription": false
+ },
+ "type": "node-field"
+ },
+ "node-field-Q345WyNzc4": {
+ "id": "node-field-Q345WyNzc4",
+ "parentId": "container-y0w7TfJC7e",
+ "data": {
+ "fieldIdentifier": {
+ "nodeId": "1a0e6c7b-9d2f-4b3c-8e1a-2f3d4c5b6a7e",
+ "fieldName": "model"
+ },
+ "showDescription": false
+ },
+ "type": "node-field"
+ },
+ "container-mUeBcw2tYf": {
+ "id": "container-mUeBcw2tYf",
+ "parentId": "root",
+ "data": {
+ "layout": "row",
+ "children": [
+ "node-field-UiQ7MaseDR",
+ "node-field-FiH14mitcM"
+ ]
+ },
+ "type": "container"
+ },
+ "heading-XDPaQofdEE": {
+ "id": "heading-XDPaQofdEE",
+ "parentId": "root",
+ "data": {
+ "content": "Generation"
+ },
+ "type": "heading"
+ },
+ "container-i8V1oJeQCP": {
+ "id": "container-i8V1oJeQCP",
+ "parentId": "root",
+ "data": {
+ "layout": "row",
+ "children": [
+ "node-field-DonE1WRSA6"
+ ]
+ },
+ "type": "container"
+ },
+ "heading-wuPtY8B4vR": {
+ "id": "heading-wuPtY8B4vR",
+ "parentId": "root",
+ "data": {
+ "content": "Model"
+ },
+ "type": "heading"
+ },
+ "container-AkpUH1WtID": {
+ "id": "container-AkpUH1WtID",
+ "parentId": "root",
+ "data": {
+ "layout": "row",
+ "children": [
+ "container-y0w7TfJC7e",
+ "divider-dzmORn21WD",
+ "container-7g1Xedgg8T"
+ ]
+ },
+ "type": "container"
+ },
+ "container-y0w7TfJC7e": {
+ "id": "container-y0w7TfJC7e",
+ "parentId": "container-AkpUH1WtID",
+ "data": {
+ "layout": "column",
+ "children": [
+ "node-field-Q345WyNzc4",
+ "node-field-PdU0Sz2m5z"
+ ]
+ },
+ "type": "container"
+ },
+ "container-7g1Xedgg8T": {
+ "id": "container-7g1Xedgg8T",
+ "parentId": "container-AkpUH1WtID",
+ "data": {
+ "layout": "column",
+ "children": [
+ "node-field-7G9srQT2Uw",
+ "node-field-QDqQ4XOKvg"
+ ]
+ },
+ "type": "container"
+ },
+ "container-C0z6ru33to": {
+ "id": "container-C0z6ru33to",
+ "parentId": "root",
+ "data": {
+ "layout": "row",
+ "children": [
+ "node-field-N9mexEKkYg",
+ "node-field-2u9lAbMBnD",
+ "node-field-CTGqrO3x8o"
+ ]
+ },
+ "type": "container"
+ },
+ "heading-10PuWMKJg9": {
+ "id": "heading-10PuWMKJg9",
+ "parentId": "root",
+ "data": {
+ "content": "Advanced"
+ },
+ "type": "heading"
+ },
+ "container-0u7wyxEdNa": {
+ "id": "container-0u7wyxEdNa",
+ "parentId": "root",
+ "data": {
+ "layout": "row",
+ "children": [
+ "container-Um9gcFv0lo",
+ "container-4owWd0thWk"
+ ]
+ },
+ "type": "container"
+ },
+ "container-Um9gcFv0lo": {
+ "id": "container-Um9gcFv0lo",
+ "parentId": "container-0u7wyxEdNa",
+ "data": {
+ "layout": "row",
+ "children": [
+ "container-lF8o60KuVb"
+ ]
+ },
+ "type": "container"
+ },
+ "container-4owWd0thWk": {
+ "id": "container-4owWd0thWk",
+ "parentId": "container-0u7wyxEdNa",
+ "data": {
+ "layout": "row",
+ "children": [
+ "container-rvE0oy63qL"
+ ]
+ },
+ "type": "container"
+ },
+ "node-field-a8nZxgz9u9": {
+ "id": "node-field-a8nZxgz9u9",
+ "parentId": "container-lF8o60KuVb",
+ "data": {
+ "fieldIdentifier": {
+ "nodeId": "291abec8-b996-4c7b-959c-360f56a3ed60",
+ "fieldName": "lora"
+ },
+ "showDescription": true
+ },
+ "type": "node-field"
+ },
+ "node-field-NwR9yOugEz": {
+ "id": "node-field-NwR9yOugEz",
+ "parentId": "container-rvE0oy63qL",
+ "data": {
+ "fieldIdentifier": {
+ "nodeId": "ae8f115d-a3b8-4f76-b280-7313366a94ad",
+ "fieldName": "lora"
+ },
+ "showDescription": true
+ },
+ "type": "node-field"
+ },
+ "container-lF8o60KuVb": {
+ "id": "container-lF8o60KuVb",
+ "parentId": "container-Um9gcFv0lo",
+ "data": {
+ "layout": "column",
+ "children": [
+ "node-field-a8nZxgz9u9",
+ "node-field-0al3LtpcGU"
+ ]
+ },
+ "type": "container"
+ },
+ "container-rvE0oy63qL": {
+ "id": "container-rvE0oy63qL",
+ "parentId": "container-4owWd0thWk",
+ "data": {
+ "layout": "column",
+ "children": [
+ "node-field-NwR9yOugEz",
+ "node-field-lgLAjS4nks"
+ ]
+ },
+ "type": "container"
+ },
+ "node-field-0al3LtpcGU": {
+ "id": "node-field-0al3LtpcGU",
+ "parentId": "container-lF8o60KuVb",
+ "data": {
+ "fieldIdentifier": {
+ "nodeId": "291abec8-b996-4c7b-959c-360f56a3ed60",
+ "fieldName": "weight"
+ },
+ "showDescription": true,
+ "settings": {
+ "type": "float-field-config",
+ "showShuffle": false,
+ "component": "number-input"
+ }
+ },
+ "type": "node-field"
+ },
+ "node-field-lgLAjS4nks": {
+ "id": "node-field-lgLAjS4nks",
+ "parentId": "container-rvE0oy63qL",
+ "data": {
+ "fieldIdentifier": {
+ "nodeId": "ae8f115d-a3b8-4f76-b280-7313366a94ad",
+ "fieldName": "weight"
+ },
+ "showDescription": true,
+ "settings": {
+ "type": "float-field-config",
+ "showShuffle": false,
+ "component": "number-input"
+ }
+ },
+ "type": "node-field"
+ },
+ "heading-TwsoE3wQi9": {
+ "id": "heading-TwsoE3wQi9",
+ "parentId": "root",
+ "data": {
+ "content": "Lightning LoRAs"
+ },
+ "type": "heading"
+ },
+ "node-field-DonE1WRSA6": {
+ "id": "node-field-DonE1WRSA6",
+ "parentId": "container-i8V1oJeQCP",
+ "data": {
+ "fieldIdentifier": {
+ "nodeId": "d27f29bf-6f26-4088-b1cc-c722ecd9fd61",
+ "fieldName": "target_resolution"
+ },
+ "showDescription": true
+ },
+ "type": "node-field"
+ },
+ "divider-2T3tRpIIJO": {
+ "id": "divider-2T3tRpIIJO",
+ "parentId": "root",
+ "type": "divider"
+ },
+ "divider-FKc8lld0Eb": {
+ "id": "divider-FKc8lld0Eb",
+ "parentId": "root",
+ "type": "divider"
+ },
+ "node-field-FiH14mitcM": {
+ "id": "node-field-FiH14mitcM",
+ "parentId": "container-mUeBcw2tYf",
+ "data": {
+ "fieldIdentifier": {
+ "nodeId": "c857e3e8-334f-4a6e-9013-3326316e4281",
+ "fieldName": "value"
+ },
+ "showDescription": false,
+ "settings": {
+ "type": "integer-field-config",
+ "showShuffle": false,
+ "component": "number-input"
+ }
+ },
+ "type": "node-field"
+ },
+ "node-field-kHqCwpoZZg": {
+ "id": "node-field-kHqCwpoZZg",
+ "parentId": "container-1lpcsDWjRx",
+ "data": {
+ "fieldIdentifier": {
+ "nodeId": "52e0f43e-0593-4ba6-bb50-df63464a6189",
+ "fieldName": "video"
+ },
+ "showDescription": false
+ },
+ "type": "node-field"
+ },
+ "container-1lpcsDWjRx": {
+ "id": "container-1lpcsDWjRx",
+ "parentId": "root",
+ "data": {
+ "layout": "column",
+ "children": [
+ "node-field-kHqCwpoZZg",
+ "container-hQbats2gzR"
+ ]
+ },
+ "type": "container"
+ },
+ "container-hQbats2gzR": {
+ "id": "container-hQbats2gzR",
+ "parentId": "container-1lpcsDWjRx",
+ "data": {
+ "layout": "row",
+ "children": [
+ "node-field-syuTRDo0YY",
+ "node-field-KvKUkKmRGN"
+ ]
+ },
+ "type": "container"
+ },
+ "node-field-syuTRDo0YY": {
+ "id": "node-field-syuTRDo0YY",
+ "parentId": "container-hQbats2gzR",
+ "data": {
+ "fieldIdentifier": {
+ "nodeId": "52e0f43e-0593-4ba6-bb50-df63464a6189",
+ "fieldName": "start_frame"
+ },
+ "showDescription": true,
+ "settings": {
+ "type": "integer-field-config",
+ "showShuffle": false,
+ "component": "number-input"
+ }
+ },
+ "type": "node-field"
+ },
+ "node-field-KvKUkKmRGN": {
+ "id": "node-field-KvKUkKmRGN",
+ "parentId": "container-hQbats2gzR",
+ "data": {
+ "fieldIdentifier": {
+ "nodeId": "52e0f43e-0593-4ba6-bb50-df63464a6189",
+ "fieldName": "end_frame"
+ },
+ "showDescription": true,
+ "settings": {
+ "type": "integer-field-config",
+ "showShuffle": false,
+ "component": "number-input"
+ }
+ },
+ "type": "node-field"
+ },
+ "divider-dzmORn21WD": {
+ "id": "divider-dzmORn21WD",
+ "parentId": "container-AkpUH1WtID",
+ "type": "divider"
+ },
+ "divider-w19S1gIyNJ": {
+ "id": "divider-w19S1gIyNJ",
+ "parentId": "root",
+ "type": "divider"
+ },
+ "heading-7ZKUFfihCo": {
+ "id": "heading-7ZKUFfihCo",
+ "parentId": "root",
+ "data": {
+ "content": "Concept LoRAs"
+ },
+ "type": "heading"
+ },
+ "node-field-hFIdNT3hYF": {
+ "id": "node-field-hFIdNT3hYF",
+ "type": "node-field",
+ "parentId": "root",
+ "data": {
+ "fieldIdentifier": {
+ "nodeId": "c1a982eb-c1dd-4565-a7d4-25dfc58339fd",
+ "fieldName": "loras"
+ },
+ "showDescription": true
+ }
+ }
+ },
+ "rootElementId": "root"
+ },
+ "id": "default_wan22_extend_video_lightning_concept"
+}
diff --git a/invokeai/app/services/workflow_records/default_workflows/Extend Video - Wan 2.2 Lightning.json b/invokeai/app/services/workflow_records/default_workflows/Extend Video - Wan 2.2 Lightning.json
new file mode 100644
index 00000000000..0c499d819eb
--- /dev/null
+++ b/invokeai/app/services/workflow_records/default_workflows/Extend Video - Wan 2.2 Lightning.json
@@ -0,0 +1,1556 @@
+{
+ "name": "Extend Video - Wan 2.2 Lightning",
+ "author": "InvokeAI",
+ "description": "This workflow adds frames to an existing video. Provide a starting video and use the sliders to select the starting and ending frames. The workflow will create a new video starting from the selected ending frame and then concatenate the new video to the end of the first video, performing a quick cross-fade at the selected ending frame to generate a smooth transition.",
+ "version": "1.0.0",
+ "contact": "",
+ "tags": "wan2.2, video to video, video, lightning, lora",
+ "notes": "Each InvokeAI install assigns its own internal model IDs, so the model fields are left blank -- select these in the form after loading:\n\nMain Model node:\n- Main Model: Wan 2.2 I2V A14B High Noise (Q4_K_M GGUF recommended for <=16 GB VRAM)\n- Transformer (Low Noise): Wan 2.2 I2V A14B Low Noise (Q4_K_M)\n- VAE: Wan 2.2 A14B VAE\n- T5 Encoder: Wan T5 Encoder (UMT5-XXL)\n\nLightning LoRAs (4-step distillation, I2V variant):\n- Apply Lightning LoRA (High Noise): Wan 2.2 I2V Lightning High Noise (4-step)\n- Apply Lightning LoRA (Low Noise): Wan 2.2 I2V Lightning Low Noise (4-step)\n\nEdit the \"Concatenate Videos\" node to adjust the transition and number of transition frames.",
+ "exposedFields": [
+ {
+ "nodeId": "1a0e6c7b-9d2f-4b3c-8e1a-2f3d4c5b6a7e",
+ "fieldName": "model"
+ },
+ {
+ "nodeId": "1a0e6c7b-9d2f-4b3c-8e1a-2f3d4c5b6a7e",
+ "fieldName": "transformer_low_noise_model"
+ },
+ {
+ "nodeId": "1a0e6c7b-9d2f-4b3c-8e1a-2f3d4c5b6a7e",
+ "fieldName": "component_source"
+ },
+ {
+ "nodeId": "1a0e6c7b-9d2f-4b3c-8e1a-2f3d4c5b6a7e",
+ "fieldName": "vae_model"
+ },
+ {
+ "nodeId": "1a0e6c7b-9d2f-4b3c-8e1a-2f3d4c5b6a7e",
+ "fieldName": "wan_t5_encoder_model"
+ },
+ {
+ "nodeId": "3466ad11-a931-4adc-b394-9ec287f0c23b",
+ "fieldName": "lora"
+ },
+ {
+ "nodeId": "3466ad11-a931-4adc-b394-9ec287f0c23b",
+ "fieldName": "weight"
+ },
+ {
+ "nodeId": "3466ad11-a931-4adc-b394-9ec287f0c23b",
+ "fieldName": "target"
+ },
+ {
+ "nodeId": "9b4cde65-699f-42c1-8889-76ac91fcb62f",
+ "fieldName": "lora"
+ },
+ {
+ "nodeId": "9b4cde65-699f-42c1-8889-76ac91fcb62f",
+ "fieldName": "weight"
+ },
+ {
+ "nodeId": "9b4cde65-699f-42c1-8889-76ac91fcb62f",
+ "fieldName": "target"
+ },
+ {
+ "nodeId": "2b1f7d8c-ae3f-5c4d-9f2b-3a4e5d6c7b8f",
+ "fieldName": "prompt"
+ },
+ {
+ "nodeId": "3c2a8e9d-bf4a-6d5e-af3c-4b5f6e7d8c9a",
+ "fieldName": "prompt"
+ },
+ {
+ "nodeId": "4d3b9faf-c05b-7e6f-bf4d-5c6a7f8e9dab",
+ "fieldName": "steps"
+ },
+ {
+ "nodeId": "4d3b9faf-c05b-7e6f-bf4d-5c6a7f8e9dab",
+ "fieldName": "guidance_scale"
+ },
+ {
+ "nodeId": "4d3b9faf-c05b-7e6f-bf4d-5c6a7f8e9dab",
+ "fieldName": "guidance_scale_low_noise"
+ },
+ {
+ "nodeId": "4d3b9faf-c05b-7e6f-bf4d-5c6a7f8e9dab",
+ "fieldName": "width"
+ },
+ {
+ "nodeId": "4d3b9faf-c05b-7e6f-bf4d-5c6a7f8e9dab",
+ "fieldName": "height"
+ },
+ {
+ "nodeId": "4d3b9faf-c05b-7e6f-bf4d-5c6a7f8e9dab",
+ "fieldName": "num_frames"
+ },
+ {
+ "nodeId": "6f5dcb1c-e27d-9fb0-d16f-7e8cbaa1fcbd",
+ "fieldName": "fps"
+ }
+ ],
+ "meta": {
+ "version": "4.0.0",
+ "category": "default"
+ },
+ "nodes": [
+ {
+ "id": "1a0e6c7b-9d2f-4b3c-8e1a-2f3d4c5b6a7e",
+ "type": "invocation",
+ "data": {
+ "id": "1a0e6c7b-9d2f-4b3c-8e1a-2f3d4c5b6a7e",
+ "version": "1.0.1",
+ "nodePack": "invokeai",
+ "label": "",
+ "notes": "",
+ "type": "wan_model_loader",
+ "inputs": {
+ "model": {
+ "name": "model",
+ "label": "Transformer (High Noise)",
+ "description": "",
+ "value": null
+ },
+ "transformer_low_noise_model": {
+ "name": "transformer_low_noise_model",
+ "label": "",
+ "description": "",
+ "value": null
+ },
+ "vae_model": {
+ "name": "vae_model",
+ "label": "",
+ "description": "",
+ "value": null
+ },
+ "wan_t5_encoder_model": {
+ "name": "wan_t5_encoder_model",
+ "label": "",
+ "description": "",
+ "value": null
+ },
+ "component_source": {
+ "name": "component_source",
+ "label": "",
+ "description": ""
+ }
+ },
+ "isOpen": true,
+ "isIntermediate": true,
+ "useCache": false
+ },
+ "position": {
+ "x": 350,
+ "y": 367.5
+ }
+ },
+ {
+ "id": "2b1f7d8c-ae3f-5c4d-9f2b-3a4e5d6c7b8f",
+ "type": "invocation",
+ "data": {
+ "id": "2b1f7d8c-ae3f-5c4d-9f2b-3a4e5d6c7b8f",
+ "version": "1.0.0",
+ "nodePack": "invokeai",
+ "label": "Positive Prompt",
+ "notes": "",
+ "type": "wan_text_encoder",
+ "inputs": {
+ "prompt": {
+ "name": "prompt",
+ "label": "",
+ "description": "",
+ "value": ""
+ },
+ "wan_t5_encoder": {
+ "name": "wan_t5_encoder",
+ "label": "",
+ "description": ""
+ }
+ },
+ "isOpen": true,
+ "isIntermediate": true,
+ "useCache": true
+ },
+ "position": {
+ "x": 1050,
+ "y": 579.5
+ }
+ },
+ {
+ "id": "3c2a8e9d-bf4a-6d5e-af3c-4b5f6e7d8c9a",
+ "type": "invocation",
+ "data": {
+ "id": "3c2a8e9d-bf4a-6d5e-af3c-4b5f6e7d8c9a",
+ "version": "1.0.0",
+ "nodePack": "invokeai",
+ "label": "Negative Prompt (unused at CFG=1.0)",
+ "notes": "",
+ "type": "wan_text_encoder",
+ "inputs": {
+ "prompt": {
+ "name": "prompt",
+ "label": "",
+ "description": "",
+ "value": " "
+ },
+ "wan_t5_encoder": {
+ "name": "wan_t5_encoder",
+ "label": "",
+ "description": ""
+ }
+ },
+ "isOpen": true,
+ "isIntermediate": true,
+ "useCache": true
+ },
+ "position": {
+ "x": 1050,
+ "y": 759.5
+ }
+ },
+ {
+ "id": "5e4cab0b-d16c-8faf-c05e-6d7baf90ebbc",
+ "type": "invocation",
+ "data": {
+ "id": "5e4cab0b-d16c-8faf-c05e-6d7baf90ebbc",
+ "version": "1.0.1",
+ "nodePack": "invokeai",
+ "label": "",
+ "notes": "",
+ "type": "rand_int",
+ "inputs": {
+ "low": {
+ "name": "low",
+ "label": "",
+ "description": "",
+ "value": 0
+ },
+ "high": {
+ "name": "high",
+ "label": "",
+ "description": "",
+ "value": 2147483647
+ }
+ },
+ "isOpen": true,
+ "isIntermediate": true,
+ "useCache": false
+ },
+ "position": {
+ "x": 1050,
+ "y": 939.5
+ }
+ },
+ {
+ "id": "4d3b9faf-c05b-7e6f-bf4d-5c6a7f8e9dab",
+ "type": "invocation",
+ "data": {
+ "id": "4d3b9faf-c05b-7e6f-bf4d-5c6a7f8e9dab",
+ "version": "1.0.0",
+ "nodePack": "invokeai",
+ "label": "",
+ "notes": "",
+ "type": "wan_video_denoise",
+ "inputs": {
+ "transformer": {
+ "name": "transformer",
+ "label": "",
+ "description": ""
+ },
+ "positive_conditioning": {
+ "name": "positive_conditioning",
+ "label": "",
+ "description": ""
+ },
+ "negative_conditioning": {
+ "name": "negative_conditioning",
+ "label": "",
+ "description": ""
+ },
+ "ref_image": {
+ "name": "ref_image",
+ "label": "",
+ "description": ""
+ },
+ "guidance_scale": {
+ "name": "guidance_scale",
+ "label": "CFG (High Noise)",
+ "description": "",
+ "value": 1
+ },
+ "guidance_scale_low_noise": {
+ "name": "guidance_scale_low_noise",
+ "label": "CFG (Low Noise)",
+ "description": "",
+ "value": 1
+ },
+ "width": {
+ "name": "width",
+ "label": "",
+ "description": "",
+ "value": 832
+ },
+ "height": {
+ "name": "height",
+ "label": "",
+ "description": "",
+ "value": 480
+ },
+ "num_frames": {
+ "name": "num_frames",
+ "label": "Frames",
+ "description": "",
+ "value": 81
+ },
+ "steps": {
+ "name": "steps",
+ "label": "",
+ "description": "",
+ "value": 4
+ },
+ "seed": {
+ "name": "seed",
+ "label": "",
+ "description": "",
+ "value": 0
+ }
+ },
+ "isOpen": true,
+ "isIntermediate": true,
+ "useCache": true
+ },
+ "position": {
+ "x": 1511.9383824221495,
+ "y": 1036.079759854583
+ }
+ },
+ {
+ "id": "6f5dcb1c-e27d-9fb0-d16f-7e8cbaa1fcbd",
+ "type": "invocation",
+ "data": {
+ "id": "6f5dcb1c-e27d-9fb0-d16f-7e8cbaa1fcbd",
+ "version": "1.0.0",
+ "nodePack": "invokeai",
+ "label": "",
+ "notes": "",
+ "type": "wan_l2v",
+ "inputs": {
+ "board": {
+ "name": "board",
+ "label": "",
+ "description": ""
+ },
+ "metadata": {
+ "name": "metadata",
+ "label": "",
+ "description": ""
+ },
+ "latents": {
+ "name": "latents",
+ "label": "",
+ "description": ""
+ },
+ "vae": {
+ "name": "vae",
+ "label": "",
+ "description": ""
+ },
+ "fps": {
+ "name": "fps",
+ "label": "FPS",
+ "description": "",
+ "value": 16
+ }
+ },
+ "isOpen": true,
+ "isIntermediate": false,
+ "useCache": true
+ },
+ "position": {
+ "x": 1750,
+ "y": 400.5
+ }
+ },
+ {
+ "id": "291abec8-b996-4c7b-959c-360f56a3ed60",
+ "type": "invocation",
+ "data": {
+ "id": "291abec8-b996-4c7b-959c-360f56a3ed60",
+ "version": "1.0.1",
+ "nodePack": "invokeai",
+ "label": "Apply Lightning LoRA (High Noise)",
+ "notes": "",
+ "type": "wan_lora_loader",
+ "inputs": {
+ "lora": {
+ "name": "lora",
+ "label": "Lightning LoRA (High Noise)",
+ "description": "",
+ "value": null
+ },
+ "weight": {
+ "name": "weight",
+ "label": "",
+ "description": "",
+ "value": 1
+ },
+ "target": {
+ "name": "target",
+ "label": "",
+ "description": "",
+ "value": "high"
+ },
+ "transformer": {
+ "name": "transformer",
+ "label": "",
+ "description": ""
+ }
+ },
+ "isOpen": true,
+ "isIntermediate": true,
+ "useCache": true
+ },
+ "position": {
+ "x": 700,
+ "y": 1139.5
+ }
+ },
+ {
+ "id": "ae8f115d-a3b8-4f76-b280-7313366a94ad",
+ "type": "invocation",
+ "data": {
+ "id": "ae8f115d-a3b8-4f76-b280-7313366a94ad",
+ "version": "1.0.1",
+ "nodePack": "invokeai",
+ "label": "",
+ "notes": "",
+ "type": "wan_lora_loader",
+ "inputs": {
+ "lora": {
+ "name": "lora",
+ "label": "Lightning LoRA (Low Noise)",
+ "description": "",
+ "value": null
+ },
+ "weight": {
+ "name": "weight",
+ "label": "",
+ "description": "",
+ "value": 1
+ },
+ "target": {
+ "name": "target",
+ "label": "",
+ "description": "",
+ "value": "low"
+ },
+ "transformer": {
+ "name": "transformer",
+ "label": "",
+ "description": ""
+ }
+ },
+ "isOpen": true,
+ "isIntermediate": true,
+ "useCache": true
+ },
+ "position": {
+ "x": 1050,
+ "y": 1139.5
+ }
+ },
+ {
+ "id": "97c6fecb-8354-4ace-b337-e544b0ba9982",
+ "type": "invocation",
+ "data": {
+ "id": "97c6fecb-8354-4ace-b337-e544b0ba9982",
+ "version": "1.2.0",
+ "nodePack": "invokeai",
+ "label": "",
+ "notes": "",
+ "type": "wan_ref_image_encoder",
+ "inputs": {
+ "image": {
+ "name": "image",
+ "label": "",
+ "description": ""
+ },
+ "end_image": { "name": "end_image", "label": "" },
+ "vae": {
+ "name": "vae",
+ "label": "",
+ "description": ""
+ },
+ "width": {
+ "name": "width",
+ "label": "",
+ "description": "",
+ "value": 1024
+ },
+ "height": {
+ "name": "height",
+ "label": "",
+ "description": "",
+ "value": 1024
+ },
+ "num_frames": {
+ "name": "num_frames",
+ "label": "",
+ "description": "",
+ "value": 1
+ }
+ },
+ "isOpen": true,
+ "isIntermediate": true,
+ "useCache": true
+ },
+ "position": {
+ "x": 1050,
+ "y": 1447.5
+ }
+ },
+ {
+ "id": "d27f29bf-6f26-4088-b1cc-c722ecd9fd61",
+ "type": "invocation",
+ "data": {
+ "id": "d27f29bf-6f26-4088-b1cc-c722ecd9fd61",
+ "version": "1.1.0",
+ "nodePack": "invokeai",
+ "label": "",
+ "notes": "",
+ "type": "wan_i2v_ideal_dimensions",
+ "inputs": {
+ "width": {
+ "name": "width",
+ "label": "",
+ "description": "",
+ "value": 1024
+ },
+ "height": {
+ "name": "height",
+ "label": "",
+ "description": "",
+ "value": 1024
+ },
+ "target_resolution": {
+ "name": "target_resolution",
+ "label": "Video Resolution",
+ "description": "",
+ "value": "480p"
+ },
+ "rounding": {
+ "name": "rounding",
+ "label": "",
+ "description": "",
+ "value": "nearest"
+ }
+ },
+ "isOpen": true,
+ "isIntermediate": true,
+ "useCache": true
+ },
+ "position": {
+ "x": 700,
+ "y": 1409.5
+ }
+ },
+ {
+ "id": "c857e3e8-334f-4a6e-9013-3326316e4281",
+ "type": "invocation",
+ "data": {
+ "id": "c857e3e8-334f-4a6e-9013-3326316e4281",
+ "version": "1.0.1",
+ "nodePack": "invokeai",
+ "label": "Num Frames",
+ "notes": "",
+ "type": "integer",
+ "inputs": {
+ "value": {
+ "name": "value",
+ "label": "Frames",
+ "description": "",
+ "value": 81
+ }
+ },
+ "isOpen": true,
+ "isIntermediate": true,
+ "useCache": true
+ },
+ "position": {
+ "x": 700,
+ "y": 1697.5
+ }
+ },
+ {
+ "id": "4b32c22b-679d-4d5e-a167-c3309265fcd9",
+ "type": "invocation",
+ "data": {
+ "id": "4b32c22b-679d-4d5e-a167-c3309265fcd9",
+ "version": "1.0.0",
+ "nodePack": "invokeai",
+ "label": "",
+ "notes": "",
+ "type": "video_concat",
+ "inputs": {
+ "board": {
+ "name": "board",
+ "label": "",
+ "description": "",
+ "value": "auto"
+ },
+ "metadata": {
+ "name": "metadata",
+ "label": "",
+ "description": ""
+ },
+ "videos": {
+ "name": "videos",
+ "label": "",
+ "description": ""
+ },
+ "transition": {
+ "name": "transition",
+ "label": "",
+ "description": "",
+ "value": "crossfade"
+ },
+ "transition_frames": {
+ "name": "transition_frames",
+ "label": "",
+ "description": "",
+ "value": 2
+ },
+ "fps": {
+ "name": "fps",
+ "label": "",
+ "description": "",
+ "value": 0
+ }
+ },
+ "isOpen": true,
+ "isIntermediate": false,
+ "useCache": true
+ },
+ "position": {
+ "x": 2450,
+ "y": 108.5
+ }
+ },
+ {
+ "id": "6030b112-b669-4626-86ab-b1bffa96f9d6",
+ "type": "invocation",
+ "data": {
+ "id": "6030b112-b669-4626-86ab-b1bffa96f9d6",
+ "version": "1.1.0",
+ "nodePack": "invokeai",
+ "label": "",
+ "notes": "",
+ "type": "collect",
+ "inputs": {
+ "item": {
+ "name": "item",
+ "label": "",
+ "description": ""
+ },
+ "collection": {
+ "name": "collection",
+ "label": "",
+ "description": ""
+ }
+ },
+ "isOpen": true,
+ "isIntermediate": true,
+ "useCache": true
+ },
+ "position": {
+ "x": 1750,
+ "y": 280.5
+ }
+ },
+ {
+ "id": "9d72df22-9794-4c8b-af0b-a01548b5f852",
+ "type": "invocation",
+ "data": {
+ "id": "9d72df22-9794-4c8b-af0b-a01548b5f852",
+ "version": "1.1.0",
+ "nodePack": "invokeai",
+ "label": "",
+ "notes": "",
+ "type": "collect",
+ "inputs": {
+ "item": {
+ "name": "item",
+ "label": "",
+ "description": ""
+ },
+ "collection": {
+ "name": "collection",
+ "label": "",
+ "description": ""
+ }
+ },
+ "isOpen": true,
+ "isIntermediate": true,
+ "useCache": true
+ },
+ "position": {
+ "x": 2100,
+ "y": 280.5
+ }
+ },
+ {
+ "id": "bdba5c5c-d0c2-4bfa-a910-3f4405b25cdc",
+ "type": "invocation",
+ "data": {
+ "id": "bdba5c5c-d0c2-4bfa-a910-3f4405b25cdc",
+ "version": "1.0.1",
+ "nodePack": "invokeai",
+ "label": "",
+ "notes": "",
+ "type": "float_to_int",
+ "inputs": {
+ "value": {
+ "name": "value",
+ "label": "",
+ "description": "",
+ "value": 0
+ },
+ "multiple": {
+ "name": "multiple",
+ "label": "",
+ "description": "",
+ "value": 1
+ },
+ "method": {
+ "name": "method",
+ "label": "",
+ "description": "",
+ "value": "Nearest"
+ }
+ },
+ "isOpen": true,
+ "isIntermediate": true,
+ "useCache": true
+ },
+ "position": {
+ "x": 463.36245980839465,
+ "y": 105.04759620704448
+ }
+ },
+ {
+ "id": "52e0f43e-0593-4ba6-bb50-df63464a6189",
+ "type": "invocation",
+ "data": {
+ "id": "52e0f43e-0593-4ba6-bb50-df63464a6189",
+ "version": "1.1.0",
+ "nodePack": "invokeai",
+ "label": "",
+ "notes": "",
+ "type": "extract_video_range",
+ "inputs": {
+ "board": {
+ "name": "board",
+ "label": "",
+ "description": "",
+ "value": "auto"
+ },
+ "metadata": {
+ "name": "metadata",
+ "label": "",
+ "description": ""
+ },
+ "video": {
+ "name": "video",
+ "label": "Initial Video",
+ "description": "The video so far. Specify the start and end positions."
+ },
+ "start_frame": {
+ "name": "start_frame",
+ "label": "",
+ "description": "First frame to keep.",
+ "value": 0
+ },
+ "end_frame": {
+ "name": "end_frame",
+ "label": "",
+ "description": "Last frame to keep.",
+ "value": -2
+ },
+ "fps": {
+ "name": "fps",
+ "label": "",
+ "description": "",
+ "value": 0
+ }
+ },
+ "isOpen": true,
+ "isIntermediate": true,
+ "useCache": true
+ },
+ "position": {
+ "x": -210.7591703842118,
+ "y": 102.45237449232519
+ }
+ },
+ {
+ "id": "c2b5afb3-d079-4afb-9354-4c1a7aeae73a",
+ "type": "invocation",
+ "data": {
+ "id": "c2b5afb3-d079-4afb-9354-4c1a7aeae73a",
+ "version": "1.1.0",
+ "nodePack": "invokeai",
+ "label": "",
+ "notes": "",
+ "type": "video_frame_extract",
+ "inputs": {
+ "board": {
+ "name": "board",
+ "label": "",
+ "description": "",
+ "value": "auto"
+ },
+ "metadata": {
+ "name": "metadata",
+ "label": "",
+ "description": ""
+ },
+ "video": {
+ "name": "video",
+ "label": "",
+ "description": ""
+ },
+ "frame_index": {
+ "name": "frame_index",
+ "label": "",
+ "description": "",
+ "value": -1
+ }
+ },
+ "isOpen": true,
+ "isIntermediate": true,
+ "useCache": true
+ },
+ "position": {
+ "x": 287.5156366799214,
+ "y": 1401.677171016275
+ }
+ }
+ ],
+ "edges": [
+ {
+ "id": "edge-loader-t5-pos",
+ "type": "default",
+ "source": "1a0e6c7b-9d2f-4b3c-8e1a-2f3d4c5b6a7e",
+ "target": "2b1f7d8c-ae3f-5c4d-9f2b-3a4e5d6c7b8f",
+ "sourceHandle": "wan_t5_encoder",
+ "targetHandle": "wan_t5_encoder"
+ },
+ {
+ "id": "edge-loader-t5-neg",
+ "type": "default",
+ "source": "1a0e6c7b-9d2f-4b3c-8e1a-2f3d4c5b6a7e",
+ "target": "3c2a8e9d-bf4a-6d5e-af3c-4b5f6e7d8c9a",
+ "sourceHandle": "wan_t5_encoder",
+ "targetHandle": "wan_t5_encoder"
+ },
+ {
+ "id": "edge-loader-vae-l2v",
+ "type": "default",
+ "source": "1a0e6c7b-9d2f-4b3c-8e1a-2f3d4c5b6a7e",
+ "target": "6f5dcb1c-e27d-9fb0-d16f-7e8cbaa1fcbd",
+ "sourceHandle": "vae",
+ "targetHandle": "vae"
+ },
+ {
+ "id": "edge-pos-cond-denoise",
+ "type": "default",
+ "source": "2b1f7d8c-ae3f-5c4d-9f2b-3a4e5d6c7b8f",
+ "target": "4d3b9faf-c05b-7e6f-bf4d-5c6a7f8e9dab",
+ "sourceHandle": "conditioning",
+ "targetHandle": "positive_conditioning"
+ },
+ {
+ "id": "edge-neg-cond-denoise",
+ "type": "default",
+ "source": "3c2a8e9d-bf4a-6d5e-af3c-4b5f6e7d8c9a",
+ "target": "4d3b9faf-c05b-7e6f-bf4d-5c6a7f8e9dab",
+ "sourceHandle": "conditioning",
+ "targetHandle": "negative_conditioning"
+ },
+ {
+ "id": "edge-rand-seed-denoise",
+ "type": "default",
+ "source": "5e4cab0b-d16c-8faf-c05e-6d7baf90ebbc",
+ "target": "4d3b9faf-c05b-7e6f-bf4d-5c6a7f8e9dab",
+ "sourceHandle": "value",
+ "targetHandle": "seed"
+ },
+ {
+ "id": "edge-denoise-latents-l2v",
+ "type": "default",
+ "source": "4d3b9faf-c05b-7e6f-bf4d-5c6a7f8e9dab",
+ "target": "6f5dcb1c-e27d-9fb0-d16f-7e8cbaa1fcbd",
+ "sourceHandle": "latents",
+ "targetHandle": "latents"
+ },
+ {
+ "id": "reactflow__edge-1a0e6c7b-9d2f-4b3c-8e1a-2f3d4c5b6a7etransformer-291abec8-b996-4c7b-959c-360f56a3ed60transformer",
+ "type": "default",
+ "source": "1a0e6c7b-9d2f-4b3c-8e1a-2f3d4c5b6a7e",
+ "target": "291abec8-b996-4c7b-959c-360f56a3ed60",
+ "sourceHandle": "transformer",
+ "targetHandle": "transformer"
+ },
+ {
+ "id": "reactflow__edge-291abec8-b996-4c7b-959c-360f56a3ed60transformer-ae8f115d-a3b8-4f76-b280-7313366a94adtransformer",
+ "type": "default",
+ "source": "291abec8-b996-4c7b-959c-360f56a3ed60",
+ "target": "ae8f115d-a3b8-4f76-b280-7313366a94ad",
+ "sourceHandle": "transformer",
+ "targetHandle": "transformer"
+ },
+ {
+ "id": "reactflow__edge-ae8f115d-a3b8-4f76-b280-7313366a94adtransformer-4d3b9faf-c05b-7e6f-bf4d-5c6a7f8e9dabtransformer",
+ "type": "default",
+ "source": "ae8f115d-a3b8-4f76-b280-7313366a94ad",
+ "target": "4d3b9faf-c05b-7e6f-bf4d-5c6a7f8e9dab",
+ "sourceHandle": "transformer",
+ "targetHandle": "transformer"
+ },
+ {
+ "id": "reactflow__edge-97c6fecb-8354-4ace-b337-e544b0ba9982ref_image-4d3b9faf-c05b-7e6f-bf4d-5c6a7f8e9dabref_image",
+ "type": "default",
+ "source": "97c6fecb-8354-4ace-b337-e544b0ba9982",
+ "target": "4d3b9faf-c05b-7e6f-bf4d-5c6a7f8e9dab",
+ "sourceHandle": "ref_image",
+ "targetHandle": "ref_image"
+ },
+ {
+ "id": "reactflow__edge-1a0e6c7b-9d2f-4b3c-8e1a-2f3d4c5b6a7evae-97c6fecb-8354-4ace-b337-e544b0ba9982vae",
+ "type": "default",
+ "source": "1a0e6c7b-9d2f-4b3c-8e1a-2f3d4c5b6a7e",
+ "target": "97c6fecb-8354-4ace-b337-e544b0ba9982",
+ "sourceHandle": "vae",
+ "targetHandle": "vae"
+ },
+ {
+ "id": "reactflow__edge-d27f29bf-6f26-4088-b1cc-c722ecd9fd61width-4d3b9faf-c05b-7e6f-bf4d-5c6a7f8e9dabwidth",
+ "type": "default",
+ "source": "d27f29bf-6f26-4088-b1cc-c722ecd9fd61",
+ "target": "4d3b9faf-c05b-7e6f-bf4d-5c6a7f8e9dab",
+ "sourceHandle": "width",
+ "targetHandle": "width"
+ },
+ {
+ "id": "reactflow__edge-d27f29bf-6f26-4088-b1cc-c722ecd9fd61height-4d3b9faf-c05b-7e6f-bf4d-5c6a7f8e9dabheight",
+ "type": "default",
+ "source": "d27f29bf-6f26-4088-b1cc-c722ecd9fd61",
+ "target": "4d3b9faf-c05b-7e6f-bf4d-5c6a7f8e9dab",
+ "sourceHandle": "height",
+ "targetHandle": "height"
+ },
+ {
+ "id": "reactflow__edge-d27f29bf-6f26-4088-b1cc-c722ecd9fd61width-97c6fecb-8354-4ace-b337-e544b0ba9982width",
+ "type": "default",
+ "source": "d27f29bf-6f26-4088-b1cc-c722ecd9fd61",
+ "target": "97c6fecb-8354-4ace-b337-e544b0ba9982",
+ "sourceHandle": "width",
+ "targetHandle": "width"
+ },
+ {
+ "id": "reactflow__edge-d27f29bf-6f26-4088-b1cc-c722ecd9fd61height-97c6fecb-8354-4ace-b337-e544b0ba9982height",
+ "type": "default",
+ "source": "d27f29bf-6f26-4088-b1cc-c722ecd9fd61",
+ "target": "97c6fecb-8354-4ace-b337-e544b0ba9982",
+ "sourceHandle": "height",
+ "targetHandle": "height"
+ },
+ {
+ "id": "reactflow__edge-c857e3e8-334f-4a6e-9013-3326316e4281value-4d3b9faf-c05b-7e6f-bf4d-5c6a7f8e9dabnum_frames",
+ "type": "default",
+ "source": "c857e3e8-334f-4a6e-9013-3326316e4281",
+ "target": "4d3b9faf-c05b-7e6f-bf4d-5c6a7f8e9dab",
+ "sourceHandle": "value",
+ "targetHandle": "num_frames"
+ },
+ {
+ "id": "reactflow__edge-c857e3e8-334f-4a6e-9013-3326316e4281value-97c6fecb-8354-4ace-b337-e544b0ba9982num_frames",
+ "type": "default",
+ "source": "c857e3e8-334f-4a6e-9013-3326316e4281",
+ "target": "97c6fecb-8354-4ace-b337-e544b0ba9982",
+ "sourceHandle": "value",
+ "targetHandle": "num_frames"
+ },
+ {
+ "id": "reactflow__edge-c2b5afb3-d079-4afb-9354-4c1a7aeae73awidth-d27f29bf-6f26-4088-b1cc-c722ecd9fd61width",
+ "type": "default",
+ "source": "c2b5afb3-d079-4afb-9354-4c1a7aeae73a",
+ "target": "d27f29bf-6f26-4088-b1cc-c722ecd9fd61",
+ "sourceHandle": "width",
+ "targetHandle": "width"
+ },
+ {
+ "id": "reactflow__edge-c2b5afb3-d079-4afb-9354-4c1a7aeae73aheight-d27f29bf-6f26-4088-b1cc-c722ecd9fd61height",
+ "type": "default",
+ "source": "c2b5afb3-d079-4afb-9354-4c1a7aeae73a",
+ "target": "d27f29bf-6f26-4088-b1cc-c722ecd9fd61",
+ "sourceHandle": "height",
+ "targetHandle": "height"
+ },
+ {
+ "id": "reactflow__edge-c2b5afb3-d079-4afb-9354-4c1a7aeae73aimage-97c6fecb-8354-4ace-b337-e544b0ba9982image",
+ "type": "default",
+ "source": "c2b5afb3-d079-4afb-9354-4c1a7aeae73a",
+ "target": "97c6fecb-8354-4ace-b337-e544b0ba9982",
+ "sourceHandle": "image",
+ "targetHandle": "image"
+ },
+ {
+ "id": "reactflow__edge-52e0f43e-0593-4ba6-bb50-df63464a6189video-c2b5afb3-d079-4afb-9354-4c1a7aeae73avideo",
+ "type": "default",
+ "source": "52e0f43e-0593-4ba6-bb50-df63464a6189",
+ "target": "c2b5afb3-d079-4afb-9354-4c1a7aeae73a",
+ "sourceHandle": "video",
+ "targetHandle": "video"
+ },
+ {
+ "id": "reactflow__edge-52e0f43e-0593-4ba6-bb50-df63464a6189video-6030b112-b669-4626-86ab-b1bffa96f9d6item",
+ "type": "default",
+ "source": "52e0f43e-0593-4ba6-bb50-df63464a6189",
+ "target": "6030b112-b669-4626-86ab-b1bffa96f9d6",
+ "sourceHandle": "video",
+ "targetHandle": "item"
+ },
+ {
+ "id": "reactflow__edge-6030b112-b669-4626-86ab-b1bffa96f9d6collection-9d72df22-9794-4c8b-af0b-a01548b5f852collection",
+ "type": "default",
+ "source": "6030b112-b669-4626-86ab-b1bffa96f9d6",
+ "target": "9d72df22-9794-4c8b-af0b-a01548b5f852",
+ "sourceHandle": "collection",
+ "targetHandle": "collection"
+ },
+ {
+ "id": "reactflow__edge-6f5dcb1c-e27d-9fb0-d16f-7e8cbaa1fcbdvideo-9d72df22-9794-4c8b-af0b-a01548b5f852item",
+ "type": "default",
+ "source": "6f5dcb1c-e27d-9fb0-d16f-7e8cbaa1fcbd",
+ "target": "9d72df22-9794-4c8b-af0b-a01548b5f852",
+ "sourceHandle": "video",
+ "targetHandle": "item"
+ },
+ {
+ "id": "reactflow__edge-9d72df22-9794-4c8b-af0b-a01548b5f852collection-4b32c22b-679d-4d5e-a167-c3309265fcd9videos",
+ "type": "default",
+ "source": "9d72df22-9794-4c8b-af0b-a01548b5f852",
+ "target": "4b32c22b-679d-4d5e-a167-c3309265fcd9",
+ "sourceHandle": "collection",
+ "targetHandle": "videos"
+ },
+ {
+ "id": "reactflow__edge-52e0f43e-0593-4ba6-bb50-df63464a6189fps-bdba5c5c-d0c2-4bfa-a910-3f4405b25cdcvalue",
+ "type": "default",
+ "source": "52e0f43e-0593-4ba6-bb50-df63464a6189",
+ "target": "bdba5c5c-d0c2-4bfa-a910-3f4405b25cdc",
+ "sourceHandle": "fps",
+ "targetHandle": "value"
+ },
+ {
+ "id": "reactflow__edge-bdba5c5c-d0c2-4bfa-a910-3f4405b25cdcvalue-4b32c22b-679d-4d5e-a167-c3309265fcd9fps",
+ "type": "default",
+ "source": "bdba5c5c-d0c2-4bfa-a910-3f4405b25cdc",
+ "target": "4b32c22b-679d-4d5e-a167-c3309265fcd9",
+ "sourceHandle": "value",
+ "targetHandle": "fps"
+ },
+ {
+ "id": "reactflow__edge-bdba5c5c-d0c2-4bfa-a910-3f4405b25cdcvalue-6f5dcb1c-e27d-9fb0-d16f-7e8cbaa1fcbdfps",
+ "type": "default",
+ "source": "bdba5c5c-d0c2-4bfa-a910-3f4405b25cdc",
+ "target": "6f5dcb1c-e27d-9fb0-d16f-7e8cbaa1fcbd",
+ "sourceHandle": "value",
+ "targetHandle": "fps"
+ }
+ ],
+ "form": {
+ "elements": {
+ "root": {
+ "id": "root",
+ "data": {
+ "layout": "column",
+ "children": [
+ "heading-XDPaQofdEE",
+ "node-field-KcT0glAFDK",
+ "container-1lpcsDWjRx",
+ "container-i8V1oJeQCP",
+ "container-mUeBcw2tYf",
+ "divider-2T3tRpIIJO",
+ "heading-wuPtY8B4vR",
+ "container-AkpUH1WtID",
+ "heading-TwsoE3wQi9",
+ "container-0u7wyxEdNa",
+ "divider-FKc8lld0Eb",
+ "heading-10PuWMKJg9",
+ "container-C0z6ru33to"
+ ]
+ },
+ "type": "container"
+ },
+ "node-field-QDqQ4XOKvg": {
+ "id": "node-field-QDqQ4XOKvg",
+ "parentId": "container-7g1Xedgg8T",
+ "data": {
+ "fieldIdentifier": {
+ "nodeId": "4d3b9faf-c05b-7e6f-bf4d-5c6a7f8e9dab",
+ "fieldName": "guidance_scale_low_noise"
+ },
+ "showDescription": false,
+ "settings": {
+ "type": "float-field-config",
+ "showShuffle": false,
+ "component": "number-input"
+ }
+ },
+ "type": "node-field"
+ },
+ "node-field-PdU0Sz2m5z": {
+ "id": "node-field-PdU0Sz2m5z",
+ "parentId": "container-y0w7TfJC7e",
+ "data": {
+ "fieldIdentifier": {
+ "nodeId": "4d3b9faf-c05b-7e6f-bf4d-5c6a7f8e9dab",
+ "fieldName": "guidance_scale"
+ },
+ "showDescription": false,
+ "settings": {
+ "type": "float-field-config",
+ "showShuffle": false,
+ "component": "number-input"
+ }
+ },
+ "type": "node-field"
+ },
+ "node-field-UiQ7MaseDR": {
+ "id": "node-field-UiQ7MaseDR",
+ "parentId": "container-mUeBcw2tYf",
+ "data": {
+ "fieldIdentifier": {
+ "nodeId": "4d3b9faf-c05b-7e6f-bf4d-5c6a7f8e9dab",
+ "fieldName": "steps"
+ },
+ "showDescription": false,
+ "settings": {
+ "type": "integer-field-config",
+ "showShuffle": false,
+ "component": "number-input"
+ }
+ },
+ "type": "node-field"
+ },
+ "node-field-KcT0glAFDK": {
+ "id": "node-field-KcT0glAFDK",
+ "parentId": "root",
+ "data": {
+ "fieldIdentifier": {
+ "nodeId": "2b1f7d8c-ae3f-5c4d-9f2b-3a4e5d6c7b8f",
+ "fieldName": "prompt"
+ },
+ "showDescription": false,
+ "settings": {
+ "type": "string-field-config",
+ "component": "textarea"
+ }
+ },
+ "type": "node-field"
+ },
+ "node-field-N9mexEKkYg": {
+ "id": "node-field-N9mexEKkYg",
+ "parentId": "container-C0z6ru33to",
+ "data": {
+ "fieldIdentifier": {
+ "nodeId": "1a0e6c7b-9d2f-4b3c-8e1a-2f3d4c5b6a7e",
+ "fieldName": "wan_t5_encoder_model"
+ },
+ "showDescription": false
+ },
+ "type": "node-field"
+ },
+ "node-field-2u9lAbMBnD": {
+ "id": "node-field-2u9lAbMBnD",
+ "parentId": "container-C0z6ru33to",
+ "data": {
+ "fieldIdentifier": {
+ "nodeId": "1a0e6c7b-9d2f-4b3c-8e1a-2f3d4c5b6a7e",
+ "fieldName": "vae_model"
+ },
+ "showDescription": false
+ },
+ "type": "node-field"
+ },
+ "node-field-CTGqrO3x8o": {
+ "id": "node-field-CTGqrO3x8o",
+ "parentId": "container-C0z6ru33to",
+ "data": {
+ "fieldIdentifier": {
+ "nodeId": "1a0e6c7b-9d2f-4b3c-8e1a-2f3d4c5b6a7e",
+ "fieldName": "component_source"
+ },
+ "showDescription": false
+ },
+ "type": "node-field"
+ },
+ "node-field-7G9srQT2Uw": {
+ "id": "node-field-7G9srQT2Uw",
+ "parentId": "container-7g1Xedgg8T",
+ "data": {
+ "fieldIdentifier": {
+ "nodeId": "1a0e6c7b-9d2f-4b3c-8e1a-2f3d4c5b6a7e",
+ "fieldName": "transformer_low_noise_model"
+ },
+ "showDescription": false
+ },
+ "type": "node-field"
+ },
+ "node-field-Q345WyNzc4": {
+ "id": "node-field-Q345WyNzc4",
+ "parentId": "container-y0w7TfJC7e",
+ "data": {
+ "fieldIdentifier": {
+ "nodeId": "1a0e6c7b-9d2f-4b3c-8e1a-2f3d4c5b6a7e",
+ "fieldName": "model"
+ },
+ "showDescription": false
+ },
+ "type": "node-field"
+ },
+ "container-mUeBcw2tYf": {
+ "id": "container-mUeBcw2tYf",
+ "parentId": "root",
+ "data": {
+ "layout": "row",
+ "children": [
+ "node-field-UiQ7MaseDR",
+ "node-field-FiH14mitcM"
+ ]
+ },
+ "type": "container"
+ },
+ "heading-XDPaQofdEE": {
+ "id": "heading-XDPaQofdEE",
+ "parentId": "root",
+ "data": {
+ "content": "Generation"
+ },
+ "type": "heading"
+ },
+ "container-i8V1oJeQCP": {
+ "id": "container-i8V1oJeQCP",
+ "parentId": "root",
+ "data": {
+ "layout": "row",
+ "children": [
+ "node-field-DonE1WRSA6"
+ ]
+ },
+ "type": "container"
+ },
+ "heading-wuPtY8B4vR": {
+ "id": "heading-wuPtY8B4vR",
+ "parentId": "root",
+ "data": {
+ "content": "Model"
+ },
+ "type": "heading"
+ },
+ "container-AkpUH1WtID": {
+ "id": "container-AkpUH1WtID",
+ "parentId": "root",
+ "data": {
+ "layout": "row",
+ "children": [
+ "container-y0w7TfJC7e",
+ "container-7g1Xedgg8T"
+ ]
+ },
+ "type": "container"
+ },
+ "container-y0w7TfJC7e": {
+ "id": "container-y0w7TfJC7e",
+ "parentId": "container-AkpUH1WtID",
+ "data": {
+ "layout": "column",
+ "children": [
+ "node-field-Q345WyNzc4",
+ "node-field-PdU0Sz2m5z"
+ ]
+ },
+ "type": "container"
+ },
+ "container-7g1Xedgg8T": {
+ "id": "container-7g1Xedgg8T",
+ "parentId": "container-AkpUH1WtID",
+ "data": {
+ "layout": "column",
+ "children": [
+ "node-field-7G9srQT2Uw",
+ "node-field-QDqQ4XOKvg"
+ ]
+ },
+ "type": "container"
+ },
+ "container-C0z6ru33to": {
+ "id": "container-C0z6ru33to",
+ "parentId": "root",
+ "data": {
+ "layout": "row",
+ "children": [
+ "node-field-N9mexEKkYg",
+ "node-field-2u9lAbMBnD",
+ "node-field-CTGqrO3x8o"
+ ]
+ },
+ "type": "container"
+ },
+ "heading-10PuWMKJg9": {
+ "id": "heading-10PuWMKJg9",
+ "parentId": "root",
+ "data": {
+ "content": "Advanced"
+ },
+ "type": "heading"
+ },
+ "container-0u7wyxEdNa": {
+ "id": "container-0u7wyxEdNa",
+ "parentId": "root",
+ "data": {
+ "layout": "row",
+ "children": [
+ "container-Um9gcFv0lo",
+ "container-4owWd0thWk"
+ ]
+ },
+ "type": "container"
+ },
+ "container-Um9gcFv0lo": {
+ "id": "container-Um9gcFv0lo",
+ "parentId": "container-0u7wyxEdNa",
+ "data": {
+ "layout": "row",
+ "children": [
+ "container-lF8o60KuVb"
+ ]
+ },
+ "type": "container"
+ },
+ "container-4owWd0thWk": {
+ "id": "container-4owWd0thWk",
+ "parentId": "container-0u7wyxEdNa",
+ "data": {
+ "layout": "row",
+ "children": [
+ "container-rvE0oy63qL"
+ ]
+ },
+ "type": "container"
+ },
+ "node-field-a8nZxgz9u9": {
+ "id": "node-field-a8nZxgz9u9",
+ "parentId": "container-lF8o60KuVb",
+ "data": {
+ "fieldIdentifier": {
+ "nodeId": "291abec8-b996-4c7b-959c-360f56a3ed60",
+ "fieldName": "lora"
+ },
+ "showDescription": true
+ },
+ "type": "node-field"
+ },
+ "node-field-NwR9yOugEz": {
+ "id": "node-field-NwR9yOugEz",
+ "parentId": "container-rvE0oy63qL",
+ "data": {
+ "fieldIdentifier": {
+ "nodeId": "ae8f115d-a3b8-4f76-b280-7313366a94ad",
+ "fieldName": "lora"
+ },
+ "showDescription": true
+ },
+ "type": "node-field"
+ },
+ "container-lF8o60KuVb": {
+ "id": "container-lF8o60KuVb",
+ "parentId": "container-Um9gcFv0lo",
+ "data": {
+ "layout": "column",
+ "children": [
+ "node-field-a8nZxgz9u9",
+ "node-field-0al3LtpcGU"
+ ]
+ },
+ "type": "container"
+ },
+ "container-rvE0oy63qL": {
+ "id": "container-rvE0oy63qL",
+ "parentId": "container-4owWd0thWk",
+ "data": {
+ "layout": "column",
+ "children": [
+ "node-field-NwR9yOugEz",
+ "node-field-lgLAjS4nks"
+ ]
+ },
+ "type": "container"
+ },
+ "node-field-0al3LtpcGU": {
+ "id": "node-field-0al3LtpcGU",
+ "parentId": "container-lF8o60KuVb",
+ "data": {
+ "fieldIdentifier": {
+ "nodeId": "291abec8-b996-4c7b-959c-360f56a3ed60",
+ "fieldName": "weight"
+ },
+ "showDescription": true,
+ "settings": {
+ "type": "float-field-config",
+ "showShuffle": false,
+ "component": "number-input"
+ }
+ },
+ "type": "node-field"
+ },
+ "node-field-lgLAjS4nks": {
+ "id": "node-field-lgLAjS4nks",
+ "parentId": "container-rvE0oy63qL",
+ "data": {
+ "fieldIdentifier": {
+ "nodeId": "ae8f115d-a3b8-4f76-b280-7313366a94ad",
+ "fieldName": "weight"
+ },
+ "showDescription": true,
+ "settings": {
+ "type": "float-field-config",
+ "showShuffle": false,
+ "component": "number-input"
+ }
+ },
+ "type": "node-field"
+ },
+ "heading-TwsoE3wQi9": {
+ "id": "heading-TwsoE3wQi9",
+ "parentId": "root",
+ "data": {
+ "content": "Lightning LoRAs"
+ },
+ "type": "heading"
+ },
+ "node-field-DonE1WRSA6": {
+ "id": "node-field-DonE1WRSA6",
+ "parentId": "container-i8V1oJeQCP",
+ "data": {
+ "fieldIdentifier": {
+ "nodeId": "d27f29bf-6f26-4088-b1cc-c722ecd9fd61",
+ "fieldName": "target_resolution"
+ },
+ "showDescription": true
+ },
+ "type": "node-field"
+ },
+ "divider-2T3tRpIIJO": {
+ "id": "divider-2T3tRpIIJO",
+ "parentId": "root",
+ "type": "divider"
+ },
+ "divider-FKc8lld0Eb": {
+ "id": "divider-FKc8lld0Eb",
+ "parentId": "root",
+ "type": "divider"
+ },
+ "node-field-FiH14mitcM": {
+ "id": "node-field-FiH14mitcM",
+ "parentId": "container-mUeBcw2tYf",
+ "data": {
+ "fieldIdentifier": {
+ "nodeId": "c857e3e8-334f-4a6e-9013-3326316e4281",
+ "fieldName": "value"
+ },
+ "showDescription": false,
+ "settings": {
+ "type": "integer-field-config",
+ "showShuffle": false,
+ "component": "number-input"
+ }
+ },
+ "type": "node-field"
+ },
+ "node-field-kHqCwpoZZg": {
+ "id": "node-field-kHqCwpoZZg",
+ "parentId": "container-1lpcsDWjRx",
+ "data": {
+ "fieldIdentifier": {
+ "nodeId": "52e0f43e-0593-4ba6-bb50-df63464a6189",
+ "fieldName": "video"
+ },
+ "showDescription": false
+ },
+ "type": "node-field"
+ },
+ "container-1lpcsDWjRx": {
+ "id": "container-1lpcsDWjRx",
+ "parentId": "root",
+ "data": {
+ "layout": "column",
+ "children": [
+ "node-field-kHqCwpoZZg",
+ "container-hQbats2gzR"
+ ]
+ },
+ "type": "container"
+ },
+ "container-hQbats2gzR": {
+ "id": "container-hQbats2gzR",
+ "parentId": "container-1lpcsDWjRx",
+ "data": {
+ "layout": "row",
+ "children": [
+ "node-field-syuTRDo0YY",
+ "node-field-KvKUkKmRGN"
+ ]
+ },
+ "type": "container"
+ },
+ "node-field-syuTRDo0YY": {
+ "id": "node-field-syuTRDo0YY",
+ "parentId": "container-hQbats2gzR",
+ "data": {
+ "fieldIdentifier": {
+ "nodeId": "52e0f43e-0593-4ba6-bb50-df63464a6189",
+ "fieldName": "start_frame"
+ },
+ "showDescription": true,
+ "settings": {
+ "type": "integer-field-config",
+ "showShuffle": false,
+ "component": "number-input"
+ }
+ },
+ "type": "node-field"
+ },
+ "node-field-KvKUkKmRGN": {
+ "id": "node-field-KvKUkKmRGN",
+ "parentId": "container-hQbats2gzR",
+ "data": {
+ "fieldIdentifier": {
+ "nodeId": "52e0f43e-0593-4ba6-bb50-df63464a6189",
+ "fieldName": "end_frame"
+ },
+ "showDescription": true,
+ "settings": {
+ "type": "integer-field-config",
+ "showShuffle": false,
+ "component": "number-input"
+ }
+ },
+ "type": "node-field"
+ }
+ },
+ "rootElementId": "root"
+ },
+ "id": "default_wan22_extend_video_lightning"
+}
diff --git a/invokeai/app/services/workflow_records/default_workflows/Extend Video to Image - Wan 2.2 Lightning.json b/invokeai/app/services/workflow_records/default_workflows/Extend Video to Image - Wan 2.2 Lightning.json
new file mode 100644
index 00000000000..be40b227d2b
--- /dev/null
+++ b/invokeai/app/services/workflow_records/default_workflows/Extend Video to Image - Wan 2.2 Lightning.json
@@ -0,0 +1,1686 @@
+{
+ "name": "Extend Video to Image - Wan 2.2 Lightning",
+ "author": "InvokeAI",
+ "description": "This workflow adds frames to an existing video and interpolates the new video to a selected end image. Provide a starting video and use the sliders to select the starting and ending frames. Provide a destination image to specify how the last frame of the extended video should look. The workflow will create a new video starting from the selected ending frame and then concatenate the new video to the end of the first video, performing a quick cross-fade at the selected ending frame to generate a smooth transition.",
+ "version": "1.0.0",
+ "contact": "",
+ "tags": "wan2.2, video to video, video, lightning, lora",
+ "notes": "Each InvokeAI install assigns its own internal model IDs, so the model fields are left blank -- select these in the form after loading:\n\nMain Model node:\n- Main Model: Wan 2.2 I2V A14B High Noise (Q4_K_M GGUF recommended for <=16 GB VRAM)\n- Transformer (Low Noise): Wan 2.2 I2V A14B Low Noise (Q4_K_M)\n- VAE: Wan 2.2 A14B VAE\n- T5 Encoder: Wan T5 Encoder (UMT5-XXL)\n\nLightning LoRAs (4-step distillation, I2V variant):\n- Apply Lightning LoRA (High Noise): Wan 2.2 I2V Lightning High Noise (4-step)\n- Apply Lightning LoRA (Low Noise): Wan 2.2 I2V Lightning Low Noise (4-step)\n\nEdit the \"Concatenate Videos\" node to adjust the transition and number of transition frames.",
+ "exposedFields": [
+ {
+ "nodeId": "1a0e6c7b-9d2f-4b3c-8e1a-2f3d4c5b6a7e",
+ "fieldName": "model"
+ },
+ {
+ "nodeId": "1a0e6c7b-9d2f-4b3c-8e1a-2f3d4c5b6a7e",
+ "fieldName": "transformer_low_noise_model"
+ },
+ {
+ "nodeId": "1a0e6c7b-9d2f-4b3c-8e1a-2f3d4c5b6a7e",
+ "fieldName": "component_source"
+ },
+ {
+ "nodeId": "1a0e6c7b-9d2f-4b3c-8e1a-2f3d4c5b6a7e",
+ "fieldName": "vae_model"
+ },
+ {
+ "nodeId": "1a0e6c7b-9d2f-4b3c-8e1a-2f3d4c5b6a7e",
+ "fieldName": "wan_t5_encoder_model"
+ },
+ {
+ "nodeId": "3466ad11-a931-4adc-b394-9ec287f0c23b",
+ "fieldName": "lora"
+ },
+ {
+ "nodeId": "3466ad11-a931-4adc-b394-9ec287f0c23b",
+ "fieldName": "weight"
+ },
+ {
+ "nodeId": "3466ad11-a931-4adc-b394-9ec287f0c23b",
+ "fieldName": "target"
+ },
+ {
+ "nodeId": "9b4cde65-699f-42c1-8889-76ac91fcb62f",
+ "fieldName": "lora"
+ },
+ {
+ "nodeId": "9b4cde65-699f-42c1-8889-76ac91fcb62f",
+ "fieldName": "weight"
+ },
+ {
+ "nodeId": "9b4cde65-699f-42c1-8889-76ac91fcb62f",
+ "fieldName": "target"
+ },
+ {
+ "nodeId": "2b1f7d8c-ae3f-5c4d-9f2b-3a4e5d6c7b8f",
+ "fieldName": "prompt"
+ },
+ {
+ "nodeId": "3c2a8e9d-bf4a-6d5e-af3c-4b5f6e7d8c9a",
+ "fieldName": "prompt"
+ },
+ {
+ "nodeId": "4d3b9faf-c05b-7e6f-bf4d-5c6a7f8e9dab",
+ "fieldName": "steps"
+ },
+ {
+ "nodeId": "4d3b9faf-c05b-7e6f-bf4d-5c6a7f8e9dab",
+ "fieldName": "guidance_scale"
+ },
+ {
+ "nodeId": "4d3b9faf-c05b-7e6f-bf4d-5c6a7f8e9dab",
+ "fieldName": "guidance_scale_low_noise"
+ },
+ {
+ "nodeId": "4d3b9faf-c05b-7e6f-bf4d-5c6a7f8e9dab",
+ "fieldName": "width"
+ },
+ {
+ "nodeId": "4d3b9faf-c05b-7e6f-bf4d-5c6a7f8e9dab",
+ "fieldName": "height"
+ },
+ {
+ "nodeId": "4d3b9faf-c05b-7e6f-bf4d-5c6a7f8e9dab",
+ "fieldName": "num_frames"
+ },
+ {
+ "nodeId": "6f5dcb1c-e27d-9fb0-d16f-7e8cbaa1fcbd",
+ "fieldName": "fps"
+ }
+ ],
+ "meta": {
+ "category": "default",
+ "version": "4.0.0"
+ },
+ "id": "default_wan22_extend_to_image_lightning",
+ "form": {
+ "elements": {
+ "root": {
+ "id": "root",
+ "data": {
+ "layout": "column",
+ "children": [
+ "heading-XDPaQofdEE",
+ "node-field-KcT0glAFDK",
+ "container-1lpcsDWjRx",
+ "node-field-rKS7w1UNbR",
+ "container-i8V1oJeQCP",
+ "container-mUeBcw2tYf",
+ "divider-2T3tRpIIJO",
+ "heading-wuPtY8B4vR",
+ "container-AkpUH1WtID",
+ "heading-TwsoE3wQi9",
+ "container-0u7wyxEdNa",
+ "divider-FKc8lld0Eb",
+ "heading-10PuWMKJg9",
+ "container-C0z6ru33to"
+ ]
+ },
+ "type": "container"
+ },
+ "node-field-QDqQ4XOKvg": {
+ "id": "node-field-QDqQ4XOKvg",
+ "parentId": "container-7g1Xedgg8T",
+ "data": {
+ "fieldIdentifier": {
+ "nodeId": "4d3b9faf-c05b-7e6f-bf4d-5c6a7f8e9dab",
+ "fieldName": "guidance_scale_low_noise"
+ },
+ "showDescription": false,
+ "settings": {
+ "type": "float-field-config",
+ "showShuffle": false,
+ "component": "number-input"
+ }
+ },
+ "type": "node-field"
+ },
+ "node-field-PdU0Sz2m5z": {
+ "id": "node-field-PdU0Sz2m5z",
+ "parentId": "container-y0w7TfJC7e",
+ "data": {
+ "fieldIdentifier": {
+ "nodeId": "4d3b9faf-c05b-7e6f-bf4d-5c6a7f8e9dab",
+ "fieldName": "guidance_scale"
+ },
+ "showDescription": false,
+ "settings": {
+ "type": "float-field-config",
+ "showShuffle": false,
+ "component": "number-input"
+ }
+ },
+ "type": "node-field"
+ },
+ "node-field-UiQ7MaseDR": {
+ "id": "node-field-UiQ7MaseDR",
+ "parentId": "container-mUeBcw2tYf",
+ "data": {
+ "fieldIdentifier": {
+ "nodeId": "4d3b9faf-c05b-7e6f-bf4d-5c6a7f8e9dab",
+ "fieldName": "steps"
+ },
+ "showDescription": false,
+ "settings": {
+ "type": "integer-field-config",
+ "showShuffle": false,
+ "component": "number-input"
+ }
+ },
+ "type": "node-field"
+ },
+ "node-field-KcT0glAFDK": {
+ "id": "node-field-KcT0glAFDK",
+ "parentId": "root",
+ "data": {
+ "fieldIdentifier": {
+ "nodeId": "2b1f7d8c-ae3f-5c4d-9f2b-3a4e5d6c7b8f",
+ "fieldName": "prompt"
+ },
+ "showDescription": false,
+ "settings": {
+ "type": "string-field-config",
+ "component": "textarea"
+ }
+ },
+ "type": "node-field"
+ },
+ "node-field-N9mexEKkYg": {
+ "id": "node-field-N9mexEKkYg",
+ "parentId": "container-C0z6ru33to",
+ "data": {
+ "fieldIdentifier": {
+ "nodeId": "1a0e6c7b-9d2f-4b3c-8e1a-2f3d4c5b6a7e",
+ "fieldName": "wan_t5_encoder_model"
+ },
+ "showDescription": false
+ },
+ "type": "node-field"
+ },
+ "node-field-2u9lAbMBnD": {
+ "id": "node-field-2u9lAbMBnD",
+ "parentId": "container-C0z6ru33to",
+ "data": {
+ "fieldIdentifier": {
+ "nodeId": "1a0e6c7b-9d2f-4b3c-8e1a-2f3d4c5b6a7e",
+ "fieldName": "vae_model"
+ },
+ "showDescription": false
+ },
+ "type": "node-field"
+ },
+ "node-field-CTGqrO3x8o": {
+ "id": "node-field-CTGqrO3x8o",
+ "parentId": "container-C0z6ru33to",
+ "data": {
+ "fieldIdentifier": {
+ "nodeId": "1a0e6c7b-9d2f-4b3c-8e1a-2f3d4c5b6a7e",
+ "fieldName": "component_source"
+ },
+ "showDescription": false
+ },
+ "type": "node-field"
+ },
+ "node-field-7G9srQT2Uw": {
+ "id": "node-field-7G9srQT2Uw",
+ "parentId": "container-7g1Xedgg8T",
+ "data": {
+ "fieldIdentifier": {
+ "nodeId": "1a0e6c7b-9d2f-4b3c-8e1a-2f3d4c5b6a7e",
+ "fieldName": "transformer_low_noise_model"
+ },
+ "showDescription": false
+ },
+ "type": "node-field"
+ },
+ "node-field-Q345WyNzc4": {
+ "id": "node-field-Q345WyNzc4",
+ "parentId": "container-y0w7TfJC7e",
+ "data": {
+ "fieldIdentifier": {
+ "nodeId": "1a0e6c7b-9d2f-4b3c-8e1a-2f3d4c5b6a7e",
+ "fieldName": "model"
+ },
+ "showDescription": false
+ },
+ "type": "node-field"
+ },
+ "container-mUeBcw2tYf": {
+ "id": "container-mUeBcw2tYf",
+ "parentId": "root",
+ "data": {
+ "layout": "row",
+ "children": [
+ "node-field-UiQ7MaseDR",
+ "node-field-FiH14mitcM"
+ ]
+ },
+ "type": "container"
+ },
+ "heading-XDPaQofdEE": {
+ "id": "heading-XDPaQofdEE",
+ "parentId": "root",
+ "data": {
+ "content": "Generation"
+ },
+ "type": "heading"
+ },
+ "container-i8V1oJeQCP": {
+ "id": "container-i8V1oJeQCP",
+ "parentId": "root",
+ "data": {
+ "layout": "row",
+ "children": [
+ "node-field-DonE1WRSA6"
+ ]
+ },
+ "type": "container"
+ },
+ "heading-wuPtY8B4vR": {
+ "id": "heading-wuPtY8B4vR",
+ "parentId": "root",
+ "data": {
+ "content": "Model"
+ },
+ "type": "heading"
+ },
+ "container-AkpUH1WtID": {
+ "id": "container-AkpUH1WtID",
+ "parentId": "root",
+ "data": {
+ "layout": "row",
+ "children": [
+ "container-y0w7TfJC7e",
+ "container-7g1Xedgg8T"
+ ]
+ },
+ "type": "container"
+ },
+ "container-y0w7TfJC7e": {
+ "id": "container-y0w7TfJC7e",
+ "parentId": "container-AkpUH1WtID",
+ "data": {
+ "layout": "column",
+ "children": [
+ "node-field-Q345WyNzc4",
+ "node-field-PdU0Sz2m5z"
+ ]
+ },
+ "type": "container"
+ },
+ "container-7g1Xedgg8T": {
+ "id": "container-7g1Xedgg8T",
+ "parentId": "container-AkpUH1WtID",
+ "data": {
+ "layout": "column",
+ "children": [
+ "node-field-7G9srQT2Uw",
+ "node-field-QDqQ4XOKvg"
+ ]
+ },
+ "type": "container"
+ },
+ "container-C0z6ru33to": {
+ "id": "container-C0z6ru33to",
+ "parentId": "root",
+ "data": {
+ "layout": "row",
+ "children": [
+ "node-field-N9mexEKkYg",
+ "node-field-2u9lAbMBnD",
+ "node-field-CTGqrO3x8o"
+ ]
+ },
+ "type": "container"
+ },
+ "heading-10PuWMKJg9": {
+ "id": "heading-10PuWMKJg9",
+ "parentId": "root",
+ "data": {
+ "content": "Advanced"
+ },
+ "type": "heading"
+ },
+ "container-0u7wyxEdNa": {
+ "id": "container-0u7wyxEdNa",
+ "parentId": "root",
+ "data": {
+ "layout": "row",
+ "children": [
+ "container-Um9gcFv0lo",
+ "container-4owWd0thWk"
+ ]
+ },
+ "type": "container"
+ },
+ "container-Um9gcFv0lo": {
+ "id": "container-Um9gcFv0lo",
+ "parentId": "container-0u7wyxEdNa",
+ "data": {
+ "layout": "row",
+ "children": [
+ "container-lF8o60KuVb"
+ ]
+ },
+ "type": "container"
+ },
+ "container-4owWd0thWk": {
+ "id": "container-4owWd0thWk",
+ "parentId": "container-0u7wyxEdNa",
+ "data": {
+ "layout": "row",
+ "children": [
+ "container-rvE0oy63qL"
+ ]
+ },
+ "type": "container"
+ },
+ "node-field-a8nZxgz9u9": {
+ "id": "node-field-a8nZxgz9u9",
+ "parentId": "container-lF8o60KuVb",
+ "data": {
+ "fieldIdentifier": {
+ "nodeId": "291abec8-b996-4c7b-959c-360f56a3ed60",
+ "fieldName": "lora"
+ },
+ "showDescription": true
+ },
+ "type": "node-field"
+ },
+ "node-field-NwR9yOugEz": {
+ "id": "node-field-NwR9yOugEz",
+ "parentId": "container-rvE0oy63qL",
+ "data": {
+ "fieldIdentifier": {
+ "nodeId": "ae8f115d-a3b8-4f76-b280-7313366a94ad",
+ "fieldName": "lora"
+ },
+ "showDescription": true
+ },
+ "type": "node-field"
+ },
+ "container-lF8o60KuVb": {
+ "id": "container-lF8o60KuVb",
+ "parentId": "container-Um9gcFv0lo",
+ "data": {
+ "layout": "column",
+ "children": [
+ "node-field-a8nZxgz9u9",
+ "node-field-0al3LtpcGU"
+ ]
+ },
+ "type": "container"
+ },
+ "container-rvE0oy63qL": {
+ "id": "container-rvE0oy63qL",
+ "parentId": "container-4owWd0thWk",
+ "data": {
+ "layout": "column",
+ "children": [
+ "node-field-NwR9yOugEz",
+ "node-field-lgLAjS4nks"
+ ]
+ },
+ "type": "container"
+ },
+ "node-field-0al3LtpcGU": {
+ "id": "node-field-0al3LtpcGU",
+ "parentId": "container-lF8o60KuVb",
+ "data": {
+ "fieldIdentifier": {
+ "nodeId": "291abec8-b996-4c7b-959c-360f56a3ed60",
+ "fieldName": "weight"
+ },
+ "showDescription": true,
+ "settings": {
+ "type": "float-field-config",
+ "showShuffle": false,
+ "component": "number-input"
+ }
+ },
+ "type": "node-field"
+ },
+ "node-field-lgLAjS4nks": {
+ "id": "node-field-lgLAjS4nks",
+ "parentId": "container-rvE0oy63qL",
+ "data": {
+ "fieldIdentifier": {
+ "nodeId": "ae8f115d-a3b8-4f76-b280-7313366a94ad",
+ "fieldName": "weight"
+ },
+ "showDescription": true,
+ "settings": {
+ "type": "float-field-config",
+ "showShuffle": false,
+ "component": "number-input"
+ }
+ },
+ "type": "node-field"
+ },
+ "heading-TwsoE3wQi9": {
+ "id": "heading-TwsoE3wQi9",
+ "parentId": "root",
+ "data": {
+ "content": "Lightning LoRAs"
+ },
+ "type": "heading"
+ },
+ "node-field-DonE1WRSA6": {
+ "id": "node-field-DonE1WRSA6",
+ "parentId": "container-i8V1oJeQCP",
+ "data": {
+ "fieldIdentifier": {
+ "nodeId": "d27f29bf-6f26-4088-b1cc-c722ecd9fd61",
+ "fieldName": "target_resolution"
+ },
+ "showDescription": true
+ },
+ "type": "node-field"
+ },
+ "divider-2T3tRpIIJO": {
+ "id": "divider-2T3tRpIIJO",
+ "parentId": "root",
+ "type": "divider"
+ },
+ "divider-FKc8lld0Eb": {
+ "id": "divider-FKc8lld0Eb",
+ "parentId": "root",
+ "type": "divider"
+ },
+ "node-field-FiH14mitcM": {
+ "id": "node-field-FiH14mitcM",
+ "parentId": "container-mUeBcw2tYf",
+ "data": {
+ "fieldIdentifier": {
+ "nodeId": "c857e3e8-334f-4a6e-9013-3326316e4281",
+ "fieldName": "value"
+ },
+ "showDescription": false,
+ "settings": {
+ "type": "integer-field-config",
+ "showShuffle": false,
+ "component": "number-input"
+ }
+ },
+ "type": "node-field"
+ },
+ "node-field-kHqCwpoZZg": {
+ "id": "node-field-kHqCwpoZZg",
+ "parentId": "container-1lpcsDWjRx",
+ "data": {
+ "fieldIdentifier": {
+ "nodeId": "52e0f43e-0593-4ba6-bb50-df63464a6189",
+ "fieldName": "video"
+ },
+ "showDescription": false
+ },
+ "type": "node-field"
+ },
+ "container-1lpcsDWjRx": {
+ "id": "container-1lpcsDWjRx",
+ "parentId": "root",
+ "data": {
+ "layout": "column",
+ "children": [
+ "node-field-kHqCwpoZZg",
+ "container-hQbats2gzR"
+ ]
+ },
+ "type": "container"
+ },
+ "container-hQbats2gzR": {
+ "id": "container-hQbats2gzR",
+ "parentId": "container-1lpcsDWjRx",
+ "data": {
+ "layout": "row",
+ "children": [
+ "node-field-syuTRDo0YY",
+ "node-field-KvKUkKmRGN"
+ ]
+ },
+ "type": "container"
+ },
+ "node-field-syuTRDo0YY": {
+ "id": "node-field-syuTRDo0YY",
+ "parentId": "container-hQbats2gzR",
+ "data": {
+ "fieldIdentifier": {
+ "nodeId": "52e0f43e-0593-4ba6-bb50-df63464a6189",
+ "fieldName": "start_frame"
+ },
+ "showDescription": true,
+ "settings": {
+ "type": "integer-field-config",
+ "showShuffle": false,
+ "component": "number-input"
+ }
+ },
+ "type": "node-field"
+ },
+ "node-field-KvKUkKmRGN": {
+ "id": "node-field-KvKUkKmRGN",
+ "parentId": "container-hQbats2gzR",
+ "data": {
+ "fieldIdentifier": {
+ "nodeId": "52e0f43e-0593-4ba6-bb50-df63464a6189",
+ "fieldName": "end_frame"
+ },
+ "showDescription": true,
+ "settings": {
+ "type": "integer-field-config",
+ "showShuffle": false,
+ "component": "number-input"
+ }
+ },
+ "type": "node-field"
+ },
+ "node-field-rKS7w1UNbR": {
+ "id": "node-field-rKS7w1UNbR",
+ "type": "node-field",
+ "parentId": "root",
+ "data": {
+ "fieldIdentifier": {
+ "nodeId": "f93806c9-a708-48dd-bbe8-83f56f8e9a1b",
+ "fieldName": "image"
+ },
+ "showDescription": true
+ }
+ }
+ },
+ "rootElementId": "root"
+ },
+ "nodes": [
+ {
+ "id": "1a0e6c7b-9d2f-4b3c-8e1a-2f3d4c5b6a7e",
+ "type": "invocation",
+ "data": {
+ "id": "1a0e6c7b-9d2f-4b3c-8e1a-2f3d4c5b6a7e",
+ "version": "1.0.1",
+ "nodePack": "invokeai",
+ "label": "",
+ "notes": "",
+ "type": "wan_model_loader",
+ "inputs": {
+ "model": {
+ "name": "model",
+ "label": "Transformer (High Noise)",
+ "description": "",
+ "value": null
+ },
+ "transformer_low_noise_model": {
+ "name": "transformer_low_noise_model",
+ "label": "",
+ "description": "",
+ "value": null
+ },
+ "vae_model": {
+ "name": "vae_model",
+ "label": "",
+ "description": "",
+ "value": null
+ },
+ "wan_t5_encoder_model": {
+ "name": "wan_t5_encoder_model",
+ "label": "",
+ "description": "",
+ "value": null
+ },
+ "component_source": {
+ "name": "component_source",
+ "label": "",
+ "description": ""
+ }
+ },
+ "isOpen": true,
+ "isIntermediate": true,
+ "useCache": false
+ },
+ "position": {
+ "x": 350,
+ "y": 367.5
+ }
+ },
+ {
+ "id": "2b1f7d8c-ae3f-5c4d-9f2b-3a4e5d6c7b8f",
+ "type": "invocation",
+ "data": {
+ "id": "2b1f7d8c-ae3f-5c4d-9f2b-3a4e5d6c7b8f",
+ "version": "1.0.0",
+ "nodePack": "invokeai",
+ "label": "Positive Prompt",
+ "notes": "",
+ "type": "wan_text_encoder",
+ "inputs": {
+ "prompt": {
+ "name": "prompt",
+ "label": "",
+ "description": "",
+ "value": ""
+ },
+ "wan_t5_encoder": {
+ "name": "wan_t5_encoder",
+ "label": "",
+ "description": ""
+ }
+ },
+ "isOpen": true,
+ "isIntermediate": true,
+ "useCache": true
+ },
+ "position": {
+ "x": 1050,
+ "y": 579.5
+ }
+ },
+ {
+ "id": "3c2a8e9d-bf4a-6d5e-af3c-4b5f6e7d8c9a",
+ "type": "invocation",
+ "data": {
+ "id": "3c2a8e9d-bf4a-6d5e-af3c-4b5f6e7d8c9a",
+ "version": "1.0.0",
+ "nodePack": "invokeai",
+ "label": "Negative Prompt (unused at CFG=1.0)",
+ "notes": "",
+ "type": "wan_text_encoder",
+ "inputs": {
+ "prompt": {
+ "name": "prompt",
+ "label": "",
+ "description": "",
+ "value": " "
+ },
+ "wan_t5_encoder": {
+ "name": "wan_t5_encoder",
+ "label": "",
+ "description": ""
+ }
+ },
+ "isOpen": true,
+ "isIntermediate": true,
+ "useCache": true
+ },
+ "position": {
+ "x": 1050,
+ "y": 759.5
+ }
+ },
+ {
+ "id": "5e4cab0b-d16c-8faf-c05e-6d7baf90ebbc",
+ "type": "invocation",
+ "data": {
+ "id": "5e4cab0b-d16c-8faf-c05e-6d7baf90ebbc",
+ "version": "1.0.1",
+ "nodePack": "invokeai",
+ "label": "",
+ "notes": "",
+ "type": "rand_int",
+ "inputs": {
+ "low": {
+ "name": "low",
+ "label": "",
+ "description": "",
+ "value": 0
+ },
+ "high": {
+ "name": "high",
+ "label": "",
+ "description": "",
+ "value": 2147483647
+ }
+ },
+ "isOpen": true,
+ "isIntermediate": true,
+ "useCache": false
+ },
+ "position": {
+ "x": 1050,
+ "y": 939.5
+ }
+ },
+ {
+ "id": "4d3b9faf-c05b-7e6f-bf4d-5c6a7f8e9dab",
+ "type": "invocation",
+ "data": {
+ "id": "4d3b9faf-c05b-7e6f-bf4d-5c6a7f8e9dab",
+ "version": "1.0.0",
+ "nodePack": "invokeai",
+ "label": "",
+ "notes": "",
+ "type": "wan_video_denoise",
+ "inputs": {
+ "transformer": {
+ "name": "transformer",
+ "label": "",
+ "description": ""
+ },
+ "positive_conditioning": {
+ "name": "positive_conditioning",
+ "label": "",
+ "description": ""
+ },
+ "negative_conditioning": {
+ "name": "negative_conditioning",
+ "label": "",
+ "description": ""
+ },
+ "ref_image": {
+ "name": "ref_image",
+ "label": "",
+ "description": ""
+ },
+ "guidance_scale": {
+ "name": "guidance_scale",
+ "label": "CFG (High Noise)",
+ "description": "",
+ "value": 1
+ },
+ "guidance_scale_low_noise": {
+ "name": "guidance_scale_low_noise",
+ "label": "CFG (Low Noise)",
+ "description": "",
+ "value": 1
+ },
+ "width": {
+ "name": "width",
+ "label": "",
+ "description": "",
+ "value": 832
+ },
+ "height": {
+ "name": "height",
+ "label": "",
+ "description": "",
+ "value": 480
+ },
+ "num_frames": {
+ "name": "num_frames",
+ "label": "Frames",
+ "description": "",
+ "value": 81
+ },
+ "steps": {
+ "name": "steps",
+ "label": "",
+ "description": "",
+ "value": 4
+ },
+ "seed": {
+ "name": "seed",
+ "label": "",
+ "description": "",
+ "value": 0
+ }
+ },
+ "isOpen": true,
+ "isIntermediate": true,
+ "useCache": true
+ },
+ "position": {
+ "x": 1548.5043663570775,
+ "y": 979.0368249160955
+ }
+ },
+ {
+ "id": "6f5dcb1c-e27d-9fb0-d16f-7e8cbaa1fcbd",
+ "type": "invocation",
+ "data": {
+ "id": "6f5dcb1c-e27d-9fb0-d16f-7e8cbaa1fcbd",
+ "version": "1.0.0",
+ "nodePack": "invokeai",
+ "label": "",
+ "notes": "",
+ "type": "wan_l2v",
+ "inputs": {
+ "board": {
+ "name": "board",
+ "label": "",
+ "description": ""
+ },
+ "metadata": {
+ "name": "metadata",
+ "label": "",
+ "description": ""
+ },
+ "latents": {
+ "name": "latents",
+ "label": "",
+ "description": ""
+ },
+ "vae": {
+ "name": "vae",
+ "label": "",
+ "description": ""
+ },
+ "fps": {
+ "name": "fps",
+ "label": "FPS",
+ "description": "",
+ "value": 16
+ }
+ },
+ "isOpen": true,
+ "isIntermediate": false,
+ "useCache": true
+ },
+ "position": {
+ "x": 1750,
+ "y": 400.5
+ }
+ },
+ {
+ "id": "291abec8-b996-4c7b-959c-360f56a3ed60",
+ "type": "invocation",
+ "data": {
+ "id": "291abec8-b996-4c7b-959c-360f56a3ed60",
+ "version": "1.0.1",
+ "nodePack": "invokeai",
+ "label": "Apply Lightning LoRA (High Noise)",
+ "notes": "",
+ "type": "wan_lora_loader",
+ "inputs": {
+ "lora": {
+ "name": "lora",
+ "label": "Lightning LoRA (High Noise)",
+ "description": "",
+ "value": null
+ },
+ "weight": {
+ "name": "weight",
+ "label": "",
+ "description": "",
+ "value": 1
+ },
+ "target": {
+ "name": "target",
+ "label": "",
+ "description": "",
+ "value": "high"
+ },
+ "transformer": {
+ "name": "transformer",
+ "label": "",
+ "description": ""
+ }
+ },
+ "isOpen": true,
+ "isIntermediate": true,
+ "useCache": true
+ },
+ "position": {
+ "x": 700,
+ "y": 1139.5
+ }
+ },
+ {
+ "id": "ae8f115d-a3b8-4f76-b280-7313366a94ad",
+ "type": "invocation",
+ "data": {
+ "id": "ae8f115d-a3b8-4f76-b280-7313366a94ad",
+ "version": "1.0.1",
+ "nodePack": "invokeai",
+ "label": "",
+ "notes": "",
+ "type": "wan_lora_loader",
+ "inputs": {
+ "lora": {
+ "name": "lora",
+ "label": "Lightning LoRA (Low Noise)",
+ "description": "",
+ "value": null
+ },
+ "weight": {
+ "name": "weight",
+ "label": "",
+ "description": "",
+ "value": 1
+ },
+ "target": {
+ "name": "target",
+ "label": "",
+ "description": "",
+ "value": "low"
+ },
+ "transformer": {
+ "name": "transformer",
+ "label": "",
+ "description": ""
+ }
+ },
+ "isOpen": true,
+ "isIntermediate": true,
+ "useCache": true
+ },
+ "position": {
+ "x": 1050,
+ "y": 1139.5
+ }
+ },
+ {
+ "id": "97c6fecb-8354-4ace-b337-e544b0ba9982",
+ "type": "invocation",
+ "data": {
+ "id": "97c6fecb-8354-4ace-b337-e544b0ba9982",
+ "version": "1.2.0",
+ "nodePack": "invokeai",
+ "label": "",
+ "notes": "",
+ "type": "wan_ref_image_encoder",
+ "inputs": {
+ "image": {
+ "name": "image",
+ "label": "",
+ "description": ""
+ },
+ "vae": {
+ "name": "vae",
+ "label": "",
+ "description": ""
+ },
+ "width": {
+ "name": "width",
+ "label": "",
+ "description": "",
+ "value": 1024
+ },
+ "height": {
+ "name": "height",
+ "label": "",
+ "description": "",
+ "value": 1024
+ },
+ "num_frames": {
+ "name": "num_frames",
+ "label": "",
+ "description": "",
+ "value": 1
+ },
+ "end_image": {
+ "name": "end_image",
+ "label": "",
+ "description": ""
+ }
+ },
+ "isOpen": true,
+ "isIntermediate": true,
+ "useCache": true
+ },
+ "position": {
+ "x": 1130.4451646568414,
+ "y": 1519.1693285124582
+ }
+ },
+ {
+ "id": "d27f29bf-6f26-4088-b1cc-c722ecd9fd61",
+ "type": "invocation",
+ "data": {
+ "id": "d27f29bf-6f26-4088-b1cc-c722ecd9fd61",
+ "version": "1.1.0",
+ "nodePack": "invokeai",
+ "label": "",
+ "notes": "",
+ "type": "wan_i2v_ideal_dimensions",
+ "inputs": {
+ "width": {
+ "name": "width",
+ "label": "",
+ "description": "",
+ "value": 1024
+ },
+ "height": {
+ "name": "height",
+ "label": "",
+ "description": "",
+ "value": 1024
+ },
+ "target_resolution": {
+ "name": "target_resolution",
+ "label": "Video Resolution",
+ "description": "",
+ "value": "480p"
+ },
+ "rounding": {
+ "name": "rounding",
+ "label": "",
+ "description": "",
+ "value": "nearest"
+ }
+ },
+ "isOpen": true,
+ "isIntermediate": true,
+ "useCache": true
+ },
+ "position": {
+ "x": 416.24796466495997,
+ "y": 1403.6494425704113
+ }
+ },
+ {
+ "id": "c857e3e8-334f-4a6e-9013-3326316e4281",
+ "type": "invocation",
+ "data": {
+ "id": "c857e3e8-334f-4a6e-9013-3326316e4281",
+ "version": "1.0.1",
+ "nodePack": "invokeai",
+ "label": "Num Frames",
+ "notes": "",
+ "type": "integer",
+ "inputs": {
+ "value": {
+ "name": "value",
+ "label": "Frames",
+ "description": "",
+ "value": 81
+ }
+ },
+ "isOpen": true,
+ "isIntermediate": true,
+ "useCache": true
+ },
+ "position": {
+ "x": 700,
+ "y": 1697.5
+ }
+ },
+ {
+ "id": "4b32c22b-679d-4d5e-a167-c3309265fcd9",
+ "type": "invocation",
+ "data": {
+ "id": "4b32c22b-679d-4d5e-a167-c3309265fcd9",
+ "version": "1.0.0",
+ "nodePack": "invokeai",
+ "label": "",
+ "notes": "",
+ "type": "video_concat",
+ "inputs": {
+ "board": {
+ "name": "board",
+ "label": "",
+ "description": "",
+ "value": "auto"
+ },
+ "metadata": {
+ "name": "metadata",
+ "label": "",
+ "description": ""
+ },
+ "videos": {
+ "name": "videos",
+ "label": "",
+ "description": ""
+ },
+ "transition": {
+ "name": "transition",
+ "label": "",
+ "description": "",
+ "value": "crossfade"
+ },
+ "transition_frames": {
+ "name": "transition_frames",
+ "label": "",
+ "description": "",
+ "value": 2
+ },
+ "fps": {
+ "name": "fps",
+ "label": "",
+ "description": "",
+ "value": 0
+ }
+ },
+ "isOpen": true,
+ "isIntermediate": false,
+ "useCache": true
+ },
+ "position": {
+ "x": 2450,
+ "y": 108.5
+ }
+ },
+ {
+ "id": "6030b112-b669-4626-86ab-b1bffa96f9d6",
+ "type": "invocation",
+ "data": {
+ "id": "6030b112-b669-4626-86ab-b1bffa96f9d6",
+ "version": "1.1.0",
+ "nodePack": "invokeai",
+ "label": "",
+ "notes": "",
+ "type": "collect",
+ "inputs": {
+ "item": {
+ "name": "item",
+ "label": "",
+ "description": ""
+ },
+ "collection": {
+ "name": "collection",
+ "label": "",
+ "description": ""
+ }
+ },
+ "isOpen": true,
+ "isIntermediate": true,
+ "useCache": true
+ },
+ "position": {
+ "x": 1750,
+ "y": 280.5
+ }
+ },
+ {
+ "id": "9d72df22-9794-4c8b-af0b-a01548b5f852",
+ "type": "invocation",
+ "data": {
+ "id": "9d72df22-9794-4c8b-af0b-a01548b5f852",
+ "version": "1.1.0",
+ "nodePack": "invokeai",
+ "label": "",
+ "notes": "",
+ "type": "collect",
+ "inputs": {
+ "item": {
+ "name": "item",
+ "label": "",
+ "description": ""
+ },
+ "collection": {
+ "name": "collection",
+ "label": "",
+ "description": ""
+ }
+ },
+ "isOpen": true,
+ "isIntermediate": true,
+ "useCache": true
+ },
+ "position": {
+ "x": 2100,
+ "y": 280.5
+ }
+ },
+ {
+ "id": "bdba5c5c-d0c2-4bfa-a910-3f4405b25cdc",
+ "type": "invocation",
+ "data": {
+ "id": "bdba5c5c-d0c2-4bfa-a910-3f4405b25cdc",
+ "version": "1.0.1",
+ "nodePack": "invokeai",
+ "label": "",
+ "notes": "",
+ "type": "float_to_int",
+ "inputs": {
+ "value": {
+ "name": "value",
+ "label": "",
+ "description": "",
+ "value": 0
+ },
+ "multiple": {
+ "name": "multiple",
+ "label": "",
+ "description": "",
+ "value": 1
+ },
+ "method": {
+ "name": "method",
+ "label": "",
+ "description": "",
+ "value": "Nearest"
+ }
+ },
+ "isOpen": true,
+ "isIntermediate": true,
+ "useCache": true
+ },
+ "position": {
+ "x": 463.36245980839465,
+ "y": 105.04759620704448
+ }
+ },
+ {
+ "id": "52e0f43e-0593-4ba6-bb50-df63464a6189",
+ "type": "invocation",
+ "data": {
+ "id": "52e0f43e-0593-4ba6-bb50-df63464a6189",
+ "version": "1.1.0",
+ "nodePack": "invokeai",
+ "label": "",
+ "notes": "",
+ "type": "extract_video_range",
+ "inputs": {
+ "board": {
+ "name": "board",
+ "label": "",
+ "description": "",
+ "value": "auto"
+ },
+ "metadata": {
+ "name": "metadata",
+ "label": "",
+ "description": ""
+ },
+ "video": {
+ "name": "video",
+ "label": "Initial Video",
+ "description": "The video so far. Specify the start and end positions."
+ },
+ "start_frame": {
+ "name": "start_frame",
+ "label": "",
+ "description": "First frame to keep.",
+ "value": 0
+ },
+ "end_frame": {
+ "name": "end_frame",
+ "label": "",
+ "description": "Last frame to keep.",
+ "value": -2
+ },
+ "fps": {
+ "name": "fps",
+ "label": "",
+ "description": "",
+ "value": 0
+ }
+ },
+ "isOpen": true,
+ "isIntermediate": true,
+ "useCache": true
+ },
+ "position": {
+ "x": -210.7591703842118,
+ "y": 102.45237449232519
+ }
+ },
+ {
+ "id": "c2b5afb3-d079-4afb-9354-4c1a7aeae73a",
+ "type": "invocation",
+ "data": {
+ "id": "c2b5afb3-d079-4afb-9354-4c1a7aeae73a",
+ "version": "1.1.0",
+ "nodePack": "invokeai",
+ "label": "",
+ "notes": "",
+ "type": "video_frame_extract",
+ "inputs": {
+ "board": {
+ "name": "board",
+ "label": "",
+ "description": "",
+ "value": "auto"
+ },
+ "metadata": {
+ "name": "metadata",
+ "label": "",
+ "description": ""
+ },
+ "video": {
+ "name": "video",
+ "label": "",
+ "description": ""
+ },
+ "frame_index": {
+ "name": "frame_index",
+ "label": "",
+ "description": "",
+ "value": -1
+ }
+ },
+ "isOpen": true,
+ "isIntermediate": true,
+ "useCache": true
+ },
+ "position": {
+ "x": -67.90572716757725,
+ "y": 1373.8870232257298
+ }
+ },
+ {
+ "id": "f93806c9-a708-48dd-bbe8-83f56f8e9a1b",
+ "type": "invocation",
+ "data": {
+ "id": "f93806c9-a708-48dd-bbe8-83f56f8e9a1b",
+ "type": "image",
+ "version": "1.0.2",
+ "label": "",
+ "notes": "",
+ "isOpen": true,
+ "isIntermediate": true,
+ "useCache": true,
+ "nodePack": "invokeai",
+ "inputs": {
+ "image": {
+ "name": "image",
+ "label": "Destination Image",
+ "description": ""
+ }
+ }
+ },
+ "position": {
+ "x": -253.99629537551792,
+ "y": 1000.8575532061163
+ }
+ },
+ {
+ "id": "85041165-5a9f-474d-ba63-08b8de4c119d",
+ "type": "invocation",
+ "data": {
+ "id": "85041165-5a9f-474d-ba63-08b8de4c119d",
+ "type": "img_resize",
+ "version": "1.2.2",
+ "label": "",
+ "notes": "",
+ "isOpen": true,
+ "isIntermediate": true,
+ "useCache": true,
+ "nodePack": "invokeai",
+ "inputs": {
+ "board": {
+ "name": "board",
+ "label": "",
+ "description": "",
+ "value": "auto"
+ },
+ "metadata": {
+ "name": "metadata",
+ "label": "",
+ "description": ""
+ },
+ "image": {
+ "name": "image",
+ "label": "",
+ "description": ""
+ },
+ "width": {
+ "name": "width",
+ "label": "",
+ "description": "",
+ "value": 512
+ },
+ "height": {
+ "name": "height",
+ "label": "",
+ "description": "",
+ "value": 512
+ },
+ "resample_mode": {
+ "name": "resample_mode",
+ "label": "",
+ "description": "",
+ "value": "bicubic"
+ }
+ }
+ },
+ "position": {
+ "x": 321.0254123778118,
+ "y": 923.0288200694315
+ }
+ }
+ ],
+ "edges": [
+ {
+ "id": "edge-loader-t5-pos",
+ "type": "default",
+ "source": "1a0e6c7b-9d2f-4b3c-8e1a-2f3d4c5b6a7e",
+ "target": "2b1f7d8c-ae3f-5c4d-9f2b-3a4e5d6c7b8f",
+ "sourceHandle": "wan_t5_encoder",
+ "targetHandle": "wan_t5_encoder"
+ },
+ {
+ "id": "edge-loader-t5-neg",
+ "type": "default",
+ "source": "1a0e6c7b-9d2f-4b3c-8e1a-2f3d4c5b6a7e",
+ "target": "3c2a8e9d-bf4a-6d5e-af3c-4b5f6e7d8c9a",
+ "sourceHandle": "wan_t5_encoder",
+ "targetHandle": "wan_t5_encoder"
+ },
+ {
+ "id": "edge-loader-vae-l2v",
+ "type": "default",
+ "source": "1a0e6c7b-9d2f-4b3c-8e1a-2f3d4c5b6a7e",
+ "target": "6f5dcb1c-e27d-9fb0-d16f-7e8cbaa1fcbd",
+ "sourceHandle": "vae",
+ "targetHandle": "vae"
+ },
+ {
+ "id": "edge-pos-cond-denoise",
+ "type": "default",
+ "source": "2b1f7d8c-ae3f-5c4d-9f2b-3a4e5d6c7b8f",
+ "target": "4d3b9faf-c05b-7e6f-bf4d-5c6a7f8e9dab",
+ "sourceHandle": "conditioning",
+ "targetHandle": "positive_conditioning"
+ },
+ {
+ "id": "edge-neg-cond-denoise",
+ "type": "default",
+ "source": "3c2a8e9d-bf4a-6d5e-af3c-4b5f6e7d8c9a",
+ "target": "4d3b9faf-c05b-7e6f-bf4d-5c6a7f8e9dab",
+ "sourceHandle": "conditioning",
+ "targetHandle": "negative_conditioning"
+ },
+ {
+ "id": "edge-rand-seed-denoise",
+ "type": "default",
+ "source": "5e4cab0b-d16c-8faf-c05e-6d7baf90ebbc",
+ "target": "4d3b9faf-c05b-7e6f-bf4d-5c6a7f8e9dab",
+ "sourceHandle": "value",
+ "targetHandle": "seed"
+ },
+ {
+ "id": "edge-denoise-latents-l2v",
+ "type": "default",
+ "source": "4d3b9faf-c05b-7e6f-bf4d-5c6a7f8e9dab",
+ "target": "6f5dcb1c-e27d-9fb0-d16f-7e8cbaa1fcbd",
+ "sourceHandle": "latents",
+ "targetHandle": "latents"
+ },
+ {
+ "id": "reactflow__edge-1a0e6c7b-9d2f-4b3c-8e1a-2f3d4c5b6a7etransformer-291abec8-b996-4c7b-959c-360f56a3ed60transformer",
+ "type": "default",
+ "source": "1a0e6c7b-9d2f-4b3c-8e1a-2f3d4c5b6a7e",
+ "target": "291abec8-b996-4c7b-959c-360f56a3ed60",
+ "sourceHandle": "transformer",
+ "targetHandle": "transformer"
+ },
+ {
+ "id": "reactflow__edge-291abec8-b996-4c7b-959c-360f56a3ed60transformer-ae8f115d-a3b8-4f76-b280-7313366a94adtransformer",
+ "type": "default",
+ "source": "291abec8-b996-4c7b-959c-360f56a3ed60",
+ "target": "ae8f115d-a3b8-4f76-b280-7313366a94ad",
+ "sourceHandle": "transformer",
+ "targetHandle": "transformer"
+ },
+ {
+ "id": "reactflow__edge-ae8f115d-a3b8-4f76-b280-7313366a94adtransformer-4d3b9faf-c05b-7e6f-bf4d-5c6a7f8e9dabtransformer",
+ "type": "default",
+ "source": "ae8f115d-a3b8-4f76-b280-7313366a94ad",
+ "target": "4d3b9faf-c05b-7e6f-bf4d-5c6a7f8e9dab",
+ "sourceHandle": "transformer",
+ "targetHandle": "transformer"
+ },
+ {
+ "id": "reactflow__edge-97c6fecb-8354-4ace-b337-e544b0ba9982ref_image-4d3b9faf-c05b-7e6f-bf4d-5c6a7f8e9dabref_image",
+ "type": "default",
+ "source": "97c6fecb-8354-4ace-b337-e544b0ba9982",
+ "target": "4d3b9faf-c05b-7e6f-bf4d-5c6a7f8e9dab",
+ "sourceHandle": "ref_image",
+ "targetHandle": "ref_image"
+ },
+ {
+ "id": "reactflow__edge-1a0e6c7b-9d2f-4b3c-8e1a-2f3d4c5b6a7evae-97c6fecb-8354-4ace-b337-e544b0ba9982vae",
+ "type": "default",
+ "source": "1a0e6c7b-9d2f-4b3c-8e1a-2f3d4c5b6a7e",
+ "target": "97c6fecb-8354-4ace-b337-e544b0ba9982",
+ "sourceHandle": "vae",
+ "targetHandle": "vae"
+ },
+ {
+ "id": "reactflow__edge-d27f29bf-6f26-4088-b1cc-c722ecd9fd61width-4d3b9faf-c05b-7e6f-bf4d-5c6a7f8e9dabwidth",
+ "type": "default",
+ "source": "d27f29bf-6f26-4088-b1cc-c722ecd9fd61",
+ "target": "4d3b9faf-c05b-7e6f-bf4d-5c6a7f8e9dab",
+ "sourceHandle": "width",
+ "targetHandle": "width"
+ },
+ {
+ "id": "reactflow__edge-d27f29bf-6f26-4088-b1cc-c722ecd9fd61height-4d3b9faf-c05b-7e6f-bf4d-5c6a7f8e9dabheight",
+ "type": "default",
+ "source": "d27f29bf-6f26-4088-b1cc-c722ecd9fd61",
+ "target": "4d3b9faf-c05b-7e6f-bf4d-5c6a7f8e9dab",
+ "sourceHandle": "height",
+ "targetHandle": "height"
+ },
+ {
+ "id": "reactflow__edge-d27f29bf-6f26-4088-b1cc-c722ecd9fd61width-97c6fecb-8354-4ace-b337-e544b0ba9982width",
+ "type": "default",
+ "source": "d27f29bf-6f26-4088-b1cc-c722ecd9fd61",
+ "target": "97c6fecb-8354-4ace-b337-e544b0ba9982",
+ "sourceHandle": "width",
+ "targetHandle": "width"
+ },
+ {
+ "id": "reactflow__edge-d27f29bf-6f26-4088-b1cc-c722ecd9fd61height-97c6fecb-8354-4ace-b337-e544b0ba9982height",
+ "type": "default",
+ "source": "d27f29bf-6f26-4088-b1cc-c722ecd9fd61",
+ "target": "97c6fecb-8354-4ace-b337-e544b0ba9982",
+ "sourceHandle": "height",
+ "targetHandle": "height"
+ },
+ {
+ "id": "reactflow__edge-c857e3e8-334f-4a6e-9013-3326316e4281value-4d3b9faf-c05b-7e6f-bf4d-5c6a7f8e9dabnum_frames",
+ "type": "default",
+ "source": "c857e3e8-334f-4a6e-9013-3326316e4281",
+ "target": "4d3b9faf-c05b-7e6f-bf4d-5c6a7f8e9dab",
+ "sourceHandle": "value",
+ "targetHandle": "num_frames"
+ },
+ {
+ "id": "reactflow__edge-c857e3e8-334f-4a6e-9013-3326316e4281value-97c6fecb-8354-4ace-b337-e544b0ba9982num_frames",
+ "type": "default",
+ "source": "c857e3e8-334f-4a6e-9013-3326316e4281",
+ "target": "97c6fecb-8354-4ace-b337-e544b0ba9982",
+ "sourceHandle": "value",
+ "targetHandle": "num_frames"
+ },
+ {
+ "id": "reactflow__edge-c2b5afb3-d079-4afb-9354-4c1a7aeae73awidth-d27f29bf-6f26-4088-b1cc-c722ecd9fd61width",
+ "type": "default",
+ "source": "c2b5afb3-d079-4afb-9354-4c1a7aeae73a",
+ "target": "d27f29bf-6f26-4088-b1cc-c722ecd9fd61",
+ "sourceHandle": "width",
+ "targetHandle": "width"
+ },
+ {
+ "id": "reactflow__edge-c2b5afb3-d079-4afb-9354-4c1a7aeae73aheight-d27f29bf-6f26-4088-b1cc-c722ecd9fd61height",
+ "type": "default",
+ "source": "c2b5afb3-d079-4afb-9354-4c1a7aeae73a",
+ "target": "d27f29bf-6f26-4088-b1cc-c722ecd9fd61",
+ "sourceHandle": "height",
+ "targetHandle": "height"
+ },
+ {
+ "id": "reactflow__edge-c2b5afb3-d079-4afb-9354-4c1a7aeae73aimage-97c6fecb-8354-4ace-b337-e544b0ba9982image",
+ "type": "default",
+ "source": "c2b5afb3-d079-4afb-9354-4c1a7aeae73a",
+ "target": "97c6fecb-8354-4ace-b337-e544b0ba9982",
+ "sourceHandle": "image",
+ "targetHandle": "image"
+ },
+ {
+ "id": "reactflow__edge-52e0f43e-0593-4ba6-bb50-df63464a6189video-c2b5afb3-d079-4afb-9354-4c1a7aeae73avideo",
+ "type": "default",
+ "source": "52e0f43e-0593-4ba6-bb50-df63464a6189",
+ "target": "c2b5afb3-d079-4afb-9354-4c1a7aeae73a",
+ "sourceHandle": "video",
+ "targetHandle": "video"
+ },
+ {
+ "id": "reactflow__edge-52e0f43e-0593-4ba6-bb50-df63464a6189video-6030b112-b669-4626-86ab-b1bffa96f9d6item",
+ "type": "default",
+ "source": "52e0f43e-0593-4ba6-bb50-df63464a6189",
+ "target": "6030b112-b669-4626-86ab-b1bffa96f9d6",
+ "sourceHandle": "video",
+ "targetHandle": "item"
+ },
+ {
+ "id": "reactflow__edge-6030b112-b669-4626-86ab-b1bffa96f9d6collection-9d72df22-9794-4c8b-af0b-a01548b5f852collection",
+ "type": "default",
+ "source": "6030b112-b669-4626-86ab-b1bffa96f9d6",
+ "target": "9d72df22-9794-4c8b-af0b-a01548b5f852",
+ "sourceHandle": "collection",
+ "targetHandle": "collection"
+ },
+ {
+ "id": "reactflow__edge-6f5dcb1c-e27d-9fb0-d16f-7e8cbaa1fcbdvideo-9d72df22-9794-4c8b-af0b-a01548b5f852item",
+ "type": "default",
+ "source": "6f5dcb1c-e27d-9fb0-d16f-7e8cbaa1fcbd",
+ "target": "9d72df22-9794-4c8b-af0b-a01548b5f852",
+ "sourceHandle": "video",
+ "targetHandle": "item"
+ },
+ {
+ "id": "reactflow__edge-9d72df22-9794-4c8b-af0b-a01548b5f852collection-4b32c22b-679d-4d5e-a167-c3309265fcd9videos",
+ "type": "default",
+ "source": "9d72df22-9794-4c8b-af0b-a01548b5f852",
+ "target": "4b32c22b-679d-4d5e-a167-c3309265fcd9",
+ "sourceHandle": "collection",
+ "targetHandle": "videos"
+ },
+ {
+ "id": "reactflow__edge-52e0f43e-0593-4ba6-bb50-df63464a6189fps-bdba5c5c-d0c2-4bfa-a910-3f4405b25cdcvalue",
+ "type": "default",
+ "source": "52e0f43e-0593-4ba6-bb50-df63464a6189",
+ "target": "bdba5c5c-d0c2-4bfa-a910-3f4405b25cdc",
+ "sourceHandle": "fps",
+ "targetHandle": "value"
+ },
+ {
+ "id": "reactflow__edge-bdba5c5c-d0c2-4bfa-a910-3f4405b25cdcvalue-4b32c22b-679d-4d5e-a167-c3309265fcd9fps",
+ "type": "default",
+ "source": "bdba5c5c-d0c2-4bfa-a910-3f4405b25cdc",
+ "target": "4b32c22b-679d-4d5e-a167-c3309265fcd9",
+ "sourceHandle": "value",
+ "targetHandle": "fps"
+ },
+ {
+ "id": "reactflow__edge-bdba5c5c-d0c2-4bfa-a910-3f4405b25cdcvalue-6f5dcb1c-e27d-9fb0-d16f-7e8cbaa1fcbdfps",
+ "type": "default",
+ "source": "bdba5c5c-d0c2-4bfa-a910-3f4405b25cdc",
+ "target": "6f5dcb1c-e27d-9fb0-d16f-7e8cbaa1fcbd",
+ "sourceHandle": "value",
+ "targetHandle": "fps"
+ },
+ {
+ "id": "reactflow__edge-f93806c9-a708-48dd-bbe8-83f56f8e9a1bimage-85041165-5a9f-474d-ba63-08b8de4c119dimage",
+ "type": "default",
+ "source": "f93806c9-a708-48dd-bbe8-83f56f8e9a1b",
+ "target": "85041165-5a9f-474d-ba63-08b8de4c119d",
+ "sourceHandle": "image",
+ "targetHandle": "image"
+ },
+ {
+ "id": "reactflow__edge-d27f29bf-6f26-4088-b1cc-c722ecd9fd61width-85041165-5a9f-474d-ba63-08b8de4c119dwidth",
+ "type": "default",
+ "source": "d27f29bf-6f26-4088-b1cc-c722ecd9fd61",
+ "target": "85041165-5a9f-474d-ba63-08b8de4c119d",
+ "sourceHandle": "width",
+ "targetHandle": "width"
+ },
+ {
+ "id": "reactflow__edge-d27f29bf-6f26-4088-b1cc-c722ecd9fd61height-85041165-5a9f-474d-ba63-08b8de4c119dheight",
+ "type": "default",
+ "source": "d27f29bf-6f26-4088-b1cc-c722ecd9fd61",
+ "target": "85041165-5a9f-474d-ba63-08b8de4c119d",
+ "sourceHandle": "height",
+ "targetHandle": "height"
+ },
+ {
+ "id": "reactflow__edge-85041165-5a9f-474d-ba63-08b8de4c119dimage-97c6fecb-8354-4ace-b337-e544b0ba9982end_image",
+ "type": "default",
+ "source": "85041165-5a9f-474d-ba63-08b8de4c119d",
+ "target": "97c6fecb-8354-4ace-b337-e544b0ba9982",
+ "sourceHandle": "image",
+ "targetHandle": "end_image"
+ }
+ ]
+}
diff --git a/invokeai/app/services/workflow_records/default_workflows/Image to Video - Wan 2.2 Lightning w_ Concept LoRAs.json b/invokeai/app/services/workflow_records/default_workflows/Image to Video - Wan 2.2 Lightning w_ Concept LoRAs.json
new file mode 100644
index 00000000000..4257150ed8e
--- /dev/null
+++ b/invokeai/app/services/workflow_records/default_workflows/Image to Video - Wan 2.2 Lightning w_ Concept LoRAs.json
@@ -0,0 +1,1193 @@
+{
+ "name": "Image to Video - Wan 2.2 Lightning w/ Concept LoRAs",
+ "author": "InvokeAI",
+ "description": "Fast image-to-video generation with Wan 2.2 I2V A14B + the Lightning LoRA pair. Provide an initial image and specify the resolution of the resulting video. The resolution will be applied to the shorter dimension of the image, and the other dimension will be scaled accordingly. Two inputs are provided for concept LoRAs (high noise and low noise pair).",
+ "version": "1.0.0",
+ "contact": "",
+ "tags": "wan2.2, image to video, video, lightning, lora",
+ "notes": "Each InvokeAI install assigns its own internal model IDs, so the model fields are left blank -- select these in the form after loading:\n\nMain Model node:\n- Main Model: Wan 2.2 I2V A14B High Noise (Q4_K_M GGUF recommended for <=16 GB VRAM)\n- Transformer (Low Noise): Wan 2.2 I2V A14B Low Noise (Q4_K_M)\n- VAE: Wan 2.2 A14B VAE\n- T5 Encoder: Wan T5 Encoder (UMT5-XXL)\n\nLightning LoRAs (4-step distillation, I2V variant):\n- Apply Lightning LoRA (High Noise): Wan 2.2 I2V Lightning High Noise (4-step)\n- Apply Lightning LoRA (Low Noise): Wan 2.2 I2V Lightning Low Noise (4-step)\n\nOptional concept LoRAs (your own style/subject LoRAs):\n- Apply Concept LoRA (High Noise) / (Low Noise): leave empty, or select a high/low-noise concept LoRA pair.\n\nVRAM: for <=16 GB, use the 480p resolution (on the \"Ideal Dimensions\" node); 720p and 1080p need more VRAM.",
+ "exposedFields": [
+ {
+ "nodeId": "1a0e6c7b-9d2f-4b3c-8e1a-2f3d4c5b6a7e",
+ "fieldName": "model"
+ },
+ {
+ "nodeId": "1a0e6c7b-9d2f-4b3c-8e1a-2f3d4c5b6a7e",
+ "fieldName": "transformer_low_noise_model"
+ },
+ {
+ "nodeId": "1a0e6c7b-9d2f-4b3c-8e1a-2f3d4c5b6a7e",
+ "fieldName": "component_source"
+ },
+ {
+ "nodeId": "1a0e6c7b-9d2f-4b3c-8e1a-2f3d4c5b6a7e",
+ "fieldName": "vae_model"
+ },
+ {
+ "nodeId": "1a0e6c7b-9d2f-4b3c-8e1a-2f3d4c5b6a7e",
+ "fieldName": "wan_t5_encoder_model"
+ },
+ {
+ "nodeId": "2b1f7d8c-ae3f-5c4d-9f2b-3a4e5d6c7b8f",
+ "fieldName": "prompt"
+ },
+ {
+ "nodeId": "3c2a8e9d-bf4a-6d5e-af3c-4b5f6e7d8c9a",
+ "fieldName": "prompt"
+ },
+ {
+ "nodeId": "4d3b9faf-c05b-7e6f-bf4d-5c6a7f8e9dab",
+ "fieldName": "steps"
+ },
+ {
+ "nodeId": "4d3b9faf-c05b-7e6f-bf4d-5c6a7f8e9dab",
+ "fieldName": "guidance_scale"
+ },
+ {
+ "nodeId": "4d3b9faf-c05b-7e6f-bf4d-5c6a7f8e9dab",
+ "fieldName": "guidance_scale_low_noise"
+ },
+ {
+ "nodeId": "4d3b9faf-c05b-7e6f-bf4d-5c6a7f8e9dab",
+ "fieldName": "width"
+ },
+ {
+ "nodeId": "4d3b9faf-c05b-7e6f-bf4d-5c6a7f8e9dab",
+ "fieldName": "height"
+ },
+ {
+ "nodeId": "4d3b9faf-c05b-7e6f-bf4d-5c6a7f8e9dab",
+ "fieldName": "num_frames"
+ },
+ {
+ "nodeId": "6f5dcb1c-e27d-9fb0-d16f-7e8cbaa1fcbd",
+ "fieldName": "fps"
+ }
+ ],
+ "meta": {
+ "version": "4.0.0",
+ "category": "default"
+ },
+ "nodes": [
+ {
+ "id": "1a0e6c7b-9d2f-4b3c-8e1a-2f3d4c5b6a7e",
+ "type": "invocation",
+ "data": {
+ "id": "1a0e6c7b-9d2f-4b3c-8e1a-2f3d4c5b6a7e",
+ "version": "1.0.1",
+ "nodePack": "invokeai",
+ "label": "",
+ "notes": "",
+ "type": "wan_model_loader",
+ "inputs": {
+ "model": {
+ "name": "model",
+ "label": "Transformer (High Noise)",
+ "description": ""
+ },
+ "transformer_low_noise_model": {
+ "name": "transformer_low_noise_model",
+ "label": "",
+ "description": ""
+ },
+ "vae_model": {
+ "name": "vae_model",
+ "label": "",
+ "description": ""
+ },
+ "wan_t5_encoder_model": {
+ "name": "wan_t5_encoder_model",
+ "label": "",
+ "description": ""
+ },
+ "component_source": {
+ "name": "component_source",
+ "label": "",
+ "description": ""
+ }
+ },
+ "dynamicInputTemplates": {},
+ "isOpen": true,
+ "isIntermediate": true,
+ "useCache": false
+ },
+ "position": {
+ "x": 0,
+ "y": 0
+ }
+ },
+ {
+ "id": "2b1f7d8c-ae3f-5c4d-9f2b-3a4e5d6c7b8f",
+ "type": "invocation",
+ "data": {
+ "id": "2b1f7d8c-ae3f-5c4d-9f2b-3a4e5d6c7b8f",
+ "version": "1.0.0",
+ "nodePack": "invokeai",
+ "label": "Positive Prompt",
+ "notes": "",
+ "type": "wan_text_encoder",
+ "inputs": {
+ "prompt": {
+ "name": "prompt",
+ "label": "",
+ "description": "",
+ "value": ""
+ },
+ "wan_t5_encoder": {
+ "name": "wan_t5_encoder",
+ "label": "",
+ "description": ""
+ }
+ },
+ "dynamicInputTemplates": {},
+ "isOpen": true,
+ "isIntermediate": true,
+ "useCache": true
+ },
+ "position": {
+ "x": 1050,
+ "y": 212
+ }
+ },
+ {
+ "id": "3c2a8e9d-bf4a-6d5e-af3c-4b5f6e7d8c9a",
+ "type": "invocation",
+ "data": {
+ "id": "3c2a8e9d-bf4a-6d5e-af3c-4b5f6e7d8c9a",
+ "version": "1.0.0",
+ "nodePack": "invokeai",
+ "label": "Negative Prompt (unused at CFG=1.0)",
+ "notes": "",
+ "type": "wan_text_encoder",
+ "inputs": {
+ "prompt": {
+ "name": "prompt",
+ "label": "",
+ "description": "",
+ "value": " "
+ },
+ "wan_t5_encoder": {
+ "name": "wan_t5_encoder",
+ "label": "",
+ "description": ""
+ }
+ },
+ "dynamicInputTemplates": {},
+ "isOpen": true,
+ "isIntermediate": true,
+ "useCache": true
+ },
+ "position": {
+ "x": 1050,
+ "y": 392
+ }
+ },
+ {
+ "id": "5e4cab0b-d16c-8faf-c05e-6d7baf90ebbc",
+ "type": "invocation",
+ "data": {
+ "id": "5e4cab0b-d16c-8faf-c05e-6d7baf90ebbc",
+ "version": "1.0.1",
+ "nodePack": "invokeai",
+ "label": "",
+ "notes": "",
+ "type": "rand_int",
+ "inputs": {
+ "low": {
+ "name": "low",
+ "label": "",
+ "description": "",
+ "value": 0
+ },
+ "high": {
+ "name": "high",
+ "label": "",
+ "description": "",
+ "value": 2147483647
+ }
+ },
+ "dynamicInputTemplates": {},
+ "isOpen": true,
+ "isIntermediate": true,
+ "useCache": false
+ },
+ "position": {
+ "x": 1050,
+ "y": 572
+ }
+ },
+ {
+ "id": "4d3b9faf-c05b-7e6f-bf4d-5c6a7f8e9dab",
+ "type": "invocation",
+ "data": {
+ "id": "4d3b9faf-c05b-7e6f-bf4d-5c6a7f8e9dab",
+ "version": "1.0.0",
+ "nodePack": "invokeai",
+ "label": "",
+ "notes": "",
+ "type": "wan_video_denoise",
+ "inputs": {
+ "transformer": {
+ "name": "transformer",
+ "label": "",
+ "description": ""
+ },
+ "positive_conditioning": {
+ "name": "positive_conditioning",
+ "label": "",
+ "description": ""
+ },
+ "negative_conditioning": {
+ "name": "negative_conditioning",
+ "label": "",
+ "description": ""
+ },
+ "ref_image": {
+ "name": "ref_image",
+ "label": "",
+ "description": ""
+ },
+ "guidance_scale": {
+ "name": "guidance_scale",
+ "label": "CFG (High Noise)",
+ "description": "",
+ "value": 1
+ },
+ "guidance_scale_low_noise": {
+ "name": "guidance_scale_low_noise",
+ "label": "CFG (Low Noise)",
+ "description": "",
+ "value": 1
+ },
+ "width": {
+ "name": "width",
+ "label": "",
+ "description": "",
+ "value": 832
+ },
+ "height": {
+ "name": "height",
+ "label": "",
+ "description": "",
+ "value": 480
+ },
+ "num_frames": {
+ "name": "num_frames",
+ "label": "Frames",
+ "description": "",
+ "value": 81
+ },
+ "steps": {
+ "name": "steps",
+ "label": "",
+ "description": "",
+ "value": 4
+ },
+ "seed": {
+ "name": "seed",
+ "label": "",
+ "description": "",
+ "value": 0
+ }
+ },
+ "dynamicInputTemplates": {},
+ "isOpen": true,
+ "isIntermediate": true,
+ "useCache": true
+ },
+ "position": {
+ "x": 1400,
+ "y": 718
+ }
+ },
+ {
+ "id": "6f5dcb1c-e27d-9fb0-d16f-7e8cbaa1fcbd",
+ "type": "invocation",
+ "data": {
+ "id": "6f5dcb1c-e27d-9fb0-d16f-7e8cbaa1fcbd",
+ "version": "1.0.0",
+ "nodePack": "invokeai",
+ "label": "",
+ "notes": "",
+ "type": "wan_l2v",
+ "inputs": {
+ "board": {
+ "name": "board",
+ "label": "",
+ "description": ""
+ },
+ "metadata": {
+ "name": "metadata",
+ "label": "",
+ "description": ""
+ },
+ "latents": {
+ "name": "latents",
+ "label": "",
+ "description": ""
+ },
+ "vae": {
+ "name": "vae",
+ "label": "",
+ "description": ""
+ },
+ "fps": {
+ "name": "fps",
+ "label": "FPS",
+ "description": "",
+ "value": 16
+ }
+ },
+ "dynamicInputTemplates": {},
+ "isOpen": true,
+ "isIntermediate": false,
+ "useCache": true
+ },
+ "position": {
+ "x": 1750,
+ "y": 33
+ }
+ },
+ {
+ "id": "97c6fecb-8354-4ace-b337-e544b0ba9982",
+ "type": "invocation",
+ "data": {
+ "id": "97c6fecb-8354-4ace-b337-e544b0ba9982",
+ "version": "1.2.0",
+ "nodePack": "invokeai",
+ "label": "",
+ "notes": "",
+ "type": "wan_ref_image_encoder",
+ "inputs": {
+ "image": {
+ "name": "image",
+ "label": "",
+ "description": ""
+ },
+ "vae": {
+ "name": "vae",
+ "label": "",
+ "description": ""
+ },
+ "width": {
+ "name": "width",
+ "label": "",
+ "description": "",
+ "value": 1024
+ },
+ "height": {
+ "name": "height",
+ "label": "",
+ "description": "",
+ "value": 1024
+ },
+ "num_frames": {
+ "name": "num_frames",
+ "label": "",
+ "description": "",
+ "value": 1
+ },
+ "end_image": {
+ "name": "end_image",
+ "label": "",
+ "description": ""
+ }
+ },
+ "dynamicInputTemplates": {},
+ "isOpen": true,
+ "isIntermediate": true,
+ "useCache": true
+ },
+ "position": {
+ "x": 1050,
+ "y": 772
+ }
+ },
+ {
+ "id": "d27f29bf-6f26-4088-b1cc-c722ecd9fd61",
+ "type": "invocation",
+ "data": {
+ "id": "d27f29bf-6f26-4088-b1cc-c722ecd9fd61",
+ "version": "1.1.0",
+ "nodePack": "invokeai",
+ "label": "",
+ "notes": "",
+ "type": "wan_i2v_ideal_dimensions",
+ "inputs": {
+ "width": {
+ "name": "width",
+ "label": "",
+ "description": "",
+ "value": 1024
+ },
+ "height": {
+ "name": "height",
+ "label": "",
+ "description": "",
+ "value": 1024
+ },
+ "target_resolution": {
+ "name": "target_resolution",
+ "label": "Video Resolution",
+ "description": "",
+ "value": "480p"
+ },
+ "rounding": {
+ "name": "rounding",
+ "label": "",
+ "description": "",
+ "value": "nearest"
+ }
+ },
+ "dynamicInputTemplates": {},
+ "isOpen": true,
+ "isIntermediate": true,
+ "useCache": true
+ },
+ "position": {
+ "x": 700,
+ "y": 938
+ }
+ },
+ {
+ "id": "738ec1dd-416e-471d-acf0-ca324acd0954",
+ "type": "invocation",
+ "data": {
+ "id": "738ec1dd-416e-471d-acf0-ca324acd0954",
+ "version": "1.0.2",
+ "nodePack": "invokeai",
+ "label": "",
+ "notes": "",
+ "type": "image",
+ "inputs": {
+ "image": {
+ "name": "image",
+ "label": "Starting Image",
+ "description": "First frame of the video"
+ }
+ },
+ "dynamicInputTemplates": {},
+ "isOpen": true,
+ "isIntermediate": true,
+ "useCache": true
+ },
+ "position": {
+ "x": 350,
+ "y": 654
+ }
+ },
+ {
+ "id": "c857e3e8-334f-4a6e-9013-3326316e4281",
+ "type": "invocation",
+ "data": {
+ "id": "c857e3e8-334f-4a6e-9013-3326316e4281",
+ "version": "1.0.1",
+ "nodePack": "invokeai",
+ "label": "Num Frames",
+ "notes": "",
+ "type": "integer",
+ "inputs": {
+ "value": {
+ "name": "value",
+ "label": "Frames",
+ "description": "",
+ "value": 81
+ }
+ },
+ "dynamicInputTemplates": {},
+ "isOpen": true,
+ "isIntermediate": true,
+ "useCache": true
+ },
+ "position": {
+ "x": 700,
+ "y": 1206
+ }
+ },
+ {
+ "id": "54663968-7e96-432a-a59a-dcd7e40418bc",
+ "type": "invocation",
+ "data": {
+ "id": "54663968-7e96-432a-a59a-dcd7e40418bc",
+ "type": "wan_lora_loader",
+ "version": "1.0.1",
+ "label": "",
+ "notes": "",
+ "isOpen": true,
+ "isIntermediate": true,
+ "useCache": true,
+ "nodePack": "invokeai",
+ "inputs": {
+ "lora": {
+ "name": "lora",
+ "label": "Lightning LoRA (High Noise)",
+ "description": ""
+ },
+ "weight": {
+ "name": "weight",
+ "label": "",
+ "description": "",
+ "value": 1
+ },
+ "target": {
+ "name": "target",
+ "label": "",
+ "description": "",
+ "value": "high"
+ },
+ "transformer": {
+ "name": "transformer",
+ "label": "",
+ "description": ""
+ }
+ },
+ "dynamicInputTemplates": {}
+ },
+ "position": {
+ "x": 350,
+ "y": 1354
+ }
+ },
+ {
+ "id": "29f62b13-6e9a-4ee3-b279-01b36565fee8",
+ "type": "invocation",
+ "data": {
+ "id": "29f62b13-6e9a-4ee3-b279-01b36565fee8",
+ "type": "wan_lora_loader",
+ "version": "1.0.1",
+ "label": "",
+ "notes": "",
+ "isOpen": true,
+ "isIntermediate": true,
+ "useCache": true,
+ "nodePack": "invokeai",
+ "inputs": {
+ "lora": {
+ "name": "lora",
+ "label": "Lightning LoRA (Low Noise)",
+ "description": ""
+ },
+ "weight": {
+ "name": "weight",
+ "label": "",
+ "description": "",
+ "value": 1
+ },
+ "target": {
+ "name": "target",
+ "label": "",
+ "description": "",
+ "value": "low"
+ },
+ "transformer": {
+ "name": "transformer",
+ "label": "",
+ "description": ""
+ }
+ },
+ "dynamicInputTemplates": {}
+ },
+ "position": {
+ "x": 700,
+ "y": 1354
+ }
+ },
+ {
+ "id": "9ea995ab-3db6-45ae-b961-4a3b2b9ab8d4",
+ "type": "invocation",
+ "data": {
+ "id": "9ea995ab-3db6-45ae-b961-4a3b2b9ab8d4",
+ "type": "wan_lora_collection_loader",
+ "version": "1.0.1",
+ "label": "",
+ "notes": "",
+ "isOpen": true,
+ "isIntermediate": true,
+ "useCache": true,
+ "nodePack": "invokeai",
+ "inputs": {
+ "loras": {
+ "name": "loras",
+ "label": "Concept LoRAs",
+ "description": "",
+ "value": []
+ },
+ "transformer": {
+ "name": "transformer",
+ "label": "",
+ "description": ""
+ }
+ },
+ "dynamicInputTemplates": {}
+ },
+ "position": {
+ "x": 1050,
+ "y": 1403
+ }
+ }
+ ],
+ "edges": [
+ {
+ "id": "edge-loader-t5-pos",
+ "type": "default",
+ "source": "1a0e6c7b-9d2f-4b3c-8e1a-2f3d4c5b6a7e",
+ "target": "2b1f7d8c-ae3f-5c4d-9f2b-3a4e5d6c7b8f",
+ "sourceHandle": "wan_t5_encoder",
+ "targetHandle": "wan_t5_encoder"
+ },
+ {
+ "id": "edge-loader-t5-neg",
+ "type": "default",
+ "source": "1a0e6c7b-9d2f-4b3c-8e1a-2f3d4c5b6a7e",
+ "target": "3c2a8e9d-bf4a-6d5e-af3c-4b5f6e7d8c9a",
+ "sourceHandle": "wan_t5_encoder",
+ "targetHandle": "wan_t5_encoder"
+ },
+ {
+ "id": "edge-loader-vae-l2v",
+ "type": "default",
+ "source": "1a0e6c7b-9d2f-4b3c-8e1a-2f3d4c5b6a7e",
+ "target": "6f5dcb1c-e27d-9fb0-d16f-7e8cbaa1fcbd",
+ "sourceHandle": "vae",
+ "targetHandle": "vae"
+ },
+ {
+ "id": "edge-pos-cond-denoise",
+ "type": "default",
+ "source": "2b1f7d8c-ae3f-5c4d-9f2b-3a4e5d6c7b8f",
+ "target": "4d3b9faf-c05b-7e6f-bf4d-5c6a7f8e9dab",
+ "sourceHandle": "conditioning",
+ "targetHandle": "positive_conditioning"
+ },
+ {
+ "id": "edge-neg-cond-denoise",
+ "type": "default",
+ "source": "3c2a8e9d-bf4a-6d5e-af3c-4b5f6e7d8c9a",
+ "target": "4d3b9faf-c05b-7e6f-bf4d-5c6a7f8e9dab",
+ "sourceHandle": "conditioning",
+ "targetHandle": "negative_conditioning"
+ },
+ {
+ "id": "edge-rand-seed-denoise",
+ "type": "default",
+ "source": "5e4cab0b-d16c-8faf-c05e-6d7baf90ebbc",
+ "target": "4d3b9faf-c05b-7e6f-bf4d-5c6a7f8e9dab",
+ "sourceHandle": "value",
+ "targetHandle": "seed"
+ },
+ {
+ "id": "edge-denoise-latents-l2v",
+ "type": "default",
+ "source": "4d3b9faf-c05b-7e6f-bf4d-5c6a7f8e9dab",
+ "target": "6f5dcb1c-e27d-9fb0-d16f-7e8cbaa1fcbd",
+ "sourceHandle": "latents",
+ "targetHandle": "latents"
+ },
+ {
+ "id": "reactflow__edge-97c6fecb-8354-4ace-b337-e544b0ba9982ref_image-4d3b9faf-c05b-7e6f-bf4d-5c6a7f8e9dabref_image",
+ "type": "default",
+ "source": "97c6fecb-8354-4ace-b337-e544b0ba9982",
+ "target": "4d3b9faf-c05b-7e6f-bf4d-5c6a7f8e9dab",
+ "sourceHandle": "ref_image",
+ "targetHandle": "ref_image"
+ },
+ {
+ "id": "reactflow__edge-738ec1dd-416e-471d-acf0-ca324acd0954image-97c6fecb-8354-4ace-b337-e544b0ba9982image",
+ "type": "default",
+ "source": "738ec1dd-416e-471d-acf0-ca324acd0954",
+ "target": "97c6fecb-8354-4ace-b337-e544b0ba9982",
+ "sourceHandle": "image",
+ "targetHandle": "image"
+ },
+ {
+ "id": "reactflow__edge-1a0e6c7b-9d2f-4b3c-8e1a-2f3d4c5b6a7evae-97c6fecb-8354-4ace-b337-e544b0ba9982vae",
+ "type": "default",
+ "source": "1a0e6c7b-9d2f-4b3c-8e1a-2f3d4c5b6a7e",
+ "target": "97c6fecb-8354-4ace-b337-e544b0ba9982",
+ "sourceHandle": "vae",
+ "targetHandle": "vae"
+ },
+ {
+ "id": "reactflow__edge-738ec1dd-416e-471d-acf0-ca324acd0954width-d27f29bf-6f26-4088-b1cc-c722ecd9fd61width",
+ "type": "default",
+ "source": "738ec1dd-416e-471d-acf0-ca324acd0954",
+ "target": "d27f29bf-6f26-4088-b1cc-c722ecd9fd61",
+ "sourceHandle": "width",
+ "targetHandle": "width"
+ },
+ {
+ "id": "reactflow__edge-738ec1dd-416e-471d-acf0-ca324acd0954height-d27f29bf-6f26-4088-b1cc-c722ecd9fd61height",
+ "type": "default",
+ "source": "738ec1dd-416e-471d-acf0-ca324acd0954",
+ "target": "d27f29bf-6f26-4088-b1cc-c722ecd9fd61",
+ "sourceHandle": "height",
+ "targetHandle": "height"
+ },
+ {
+ "id": "reactflow__edge-d27f29bf-6f26-4088-b1cc-c722ecd9fd61width-4d3b9faf-c05b-7e6f-bf4d-5c6a7f8e9dabwidth",
+ "type": "default",
+ "source": "d27f29bf-6f26-4088-b1cc-c722ecd9fd61",
+ "target": "4d3b9faf-c05b-7e6f-bf4d-5c6a7f8e9dab",
+ "sourceHandle": "width",
+ "targetHandle": "width"
+ },
+ {
+ "id": "reactflow__edge-d27f29bf-6f26-4088-b1cc-c722ecd9fd61height-4d3b9faf-c05b-7e6f-bf4d-5c6a7f8e9dabheight",
+ "type": "default",
+ "source": "d27f29bf-6f26-4088-b1cc-c722ecd9fd61",
+ "target": "4d3b9faf-c05b-7e6f-bf4d-5c6a7f8e9dab",
+ "sourceHandle": "height",
+ "targetHandle": "height"
+ },
+ {
+ "id": "reactflow__edge-d27f29bf-6f26-4088-b1cc-c722ecd9fd61width-97c6fecb-8354-4ace-b337-e544b0ba9982width",
+ "type": "default",
+ "source": "d27f29bf-6f26-4088-b1cc-c722ecd9fd61",
+ "target": "97c6fecb-8354-4ace-b337-e544b0ba9982",
+ "sourceHandle": "width",
+ "targetHandle": "width"
+ },
+ {
+ "id": "reactflow__edge-d27f29bf-6f26-4088-b1cc-c722ecd9fd61height-97c6fecb-8354-4ace-b337-e544b0ba9982height",
+ "type": "default",
+ "source": "d27f29bf-6f26-4088-b1cc-c722ecd9fd61",
+ "target": "97c6fecb-8354-4ace-b337-e544b0ba9982",
+ "sourceHandle": "height",
+ "targetHandle": "height"
+ },
+ {
+ "id": "reactflow__edge-c857e3e8-334f-4a6e-9013-3326316e4281value-4d3b9faf-c05b-7e6f-bf4d-5c6a7f8e9dabnum_frames",
+ "type": "default",
+ "source": "c857e3e8-334f-4a6e-9013-3326316e4281",
+ "target": "4d3b9faf-c05b-7e6f-bf4d-5c6a7f8e9dab",
+ "sourceHandle": "value",
+ "targetHandle": "num_frames"
+ },
+ {
+ "id": "reactflow__edge-c857e3e8-334f-4a6e-9013-3326316e4281value-97c6fecb-8354-4ace-b337-e544b0ba9982num_frames",
+ "type": "default",
+ "source": "c857e3e8-334f-4a6e-9013-3326316e4281",
+ "target": "97c6fecb-8354-4ace-b337-e544b0ba9982",
+ "sourceHandle": "value",
+ "targetHandle": "num_frames"
+ },
+ {
+ "id": "reactflow__edge-1a0e6c7b-9d2f-4b3c-8e1a-2f3d4c5b6a7etransformer-54663968-7e96-432a-a59a-dcd7e40418bctransformer",
+ "type": "default",
+ "source": "1a0e6c7b-9d2f-4b3c-8e1a-2f3d4c5b6a7e",
+ "target": "54663968-7e96-432a-a59a-dcd7e40418bc",
+ "sourceHandle": "transformer",
+ "targetHandle": "transformer"
+ },
+ {
+ "id": "reactflow__edge-54663968-7e96-432a-a59a-dcd7e40418bctransformer-29f62b13-6e9a-4ee3-b279-01b36565fee8transformer",
+ "type": "default",
+ "source": "54663968-7e96-432a-a59a-dcd7e40418bc",
+ "target": "29f62b13-6e9a-4ee3-b279-01b36565fee8",
+ "sourceHandle": "transformer",
+ "targetHandle": "transformer"
+ },
+ {
+ "id": "reactflow__edge-29f62b13-6e9a-4ee3-b279-01b36565fee8transformer-9ea995ab-3db6-45ae-b961-4a3b2b9ab8d4transformer",
+ "type": "default",
+ "source": "29f62b13-6e9a-4ee3-b279-01b36565fee8",
+ "target": "9ea995ab-3db6-45ae-b961-4a3b2b9ab8d4",
+ "sourceHandle": "transformer",
+ "targetHandle": "transformer"
+ },
+ {
+ "id": "reactflow__edge-9ea995ab-3db6-45ae-b961-4a3b2b9ab8d4transformer-4d3b9faf-c05b-7e6f-bf4d-5c6a7f8e9dabtransformer",
+ "type": "default",
+ "source": "9ea995ab-3db6-45ae-b961-4a3b2b9ab8d4",
+ "target": "4d3b9faf-c05b-7e6f-bf4d-5c6a7f8e9dab",
+ "sourceHandle": "transformer",
+ "targetHandle": "transformer"
+ }
+ ],
+ "form": {
+ "elements": {
+ "root": {
+ "id": "root",
+ "data": {
+ "layout": "column",
+ "children": [
+ "heading-XDPaQofdEE",
+ "node-field-KcT0glAFDK",
+ "container-i8V1oJeQCP",
+ "container-mUeBcw2tYf",
+ "divider-2T3tRpIIJO",
+ "heading-wuPtY8B4vR",
+ "container-AkpUH1WtID",
+ "heading-TwsoE3wQi9",
+ "container-bNU46OmlI8",
+ "heading-GY6f00b3mJ",
+ "node-field-Ec9kbsLX1V",
+ "divider-FKc8lld0Eb",
+ "heading-10PuWMKJg9",
+ "container-C0z6ru33to"
+ ]
+ },
+ "type": "container"
+ },
+ "node-field-zQA9XuRZRW": {
+ "id": "node-field-zQA9XuRZRW",
+ "parentId": "container-mUeBcw2tYf",
+ "data": {
+ "fieldIdentifier": {
+ "nodeId": "6f5dcb1c-e27d-9fb0-d16f-7e8cbaa1fcbd",
+ "fieldName": "fps"
+ },
+ "showDescription": false,
+ "settings": {
+ "type": "integer-field-config",
+ "showShuffle": false,
+ "component": "number-input"
+ }
+ },
+ "type": "node-field"
+ },
+ "node-field-QDqQ4XOKvg": {
+ "id": "node-field-QDqQ4XOKvg",
+ "parentId": "container-7g1Xedgg8T",
+ "data": {
+ "fieldIdentifier": {
+ "nodeId": "4d3b9faf-c05b-7e6f-bf4d-5c6a7f8e9dab",
+ "fieldName": "guidance_scale_low_noise"
+ },
+ "showDescription": false,
+ "settings": {
+ "type": "float-field-config",
+ "showShuffle": false,
+ "component": "number-input"
+ }
+ },
+ "type": "node-field"
+ },
+ "node-field-PdU0Sz2m5z": {
+ "id": "node-field-PdU0Sz2m5z",
+ "parentId": "container-y0w7TfJC7e",
+ "data": {
+ "fieldIdentifier": {
+ "nodeId": "4d3b9faf-c05b-7e6f-bf4d-5c6a7f8e9dab",
+ "fieldName": "guidance_scale"
+ },
+ "showDescription": false,
+ "settings": {
+ "type": "float-field-config",
+ "showShuffle": false,
+ "component": "number-input"
+ }
+ },
+ "type": "node-field"
+ },
+ "node-field-UiQ7MaseDR": {
+ "id": "node-field-UiQ7MaseDR",
+ "parentId": "container-mUeBcw2tYf",
+ "data": {
+ "fieldIdentifier": {
+ "nodeId": "4d3b9faf-c05b-7e6f-bf4d-5c6a7f8e9dab",
+ "fieldName": "steps"
+ },
+ "showDescription": false,
+ "settings": {
+ "type": "integer-field-config",
+ "showShuffle": false,
+ "component": "number-input"
+ }
+ },
+ "type": "node-field"
+ },
+ "node-field-KcT0glAFDK": {
+ "id": "node-field-KcT0glAFDK",
+ "parentId": "root",
+ "data": {
+ "fieldIdentifier": {
+ "nodeId": "2b1f7d8c-ae3f-5c4d-9f2b-3a4e5d6c7b8f",
+ "fieldName": "prompt"
+ },
+ "showDescription": false,
+ "settings": {
+ "type": "string-field-config",
+ "component": "textarea"
+ }
+ },
+ "type": "node-field"
+ },
+ "node-field-N9mexEKkYg": {
+ "id": "node-field-N9mexEKkYg",
+ "parentId": "container-C0z6ru33to",
+ "data": {
+ "fieldIdentifier": {
+ "nodeId": "1a0e6c7b-9d2f-4b3c-8e1a-2f3d4c5b6a7e",
+ "fieldName": "wan_t5_encoder_model"
+ },
+ "showDescription": false
+ },
+ "type": "node-field"
+ },
+ "node-field-2u9lAbMBnD": {
+ "id": "node-field-2u9lAbMBnD",
+ "parentId": "container-C0z6ru33to",
+ "data": {
+ "fieldIdentifier": {
+ "nodeId": "1a0e6c7b-9d2f-4b3c-8e1a-2f3d4c5b6a7e",
+ "fieldName": "vae_model"
+ },
+ "showDescription": false
+ },
+ "type": "node-field"
+ },
+ "node-field-CTGqrO3x8o": {
+ "id": "node-field-CTGqrO3x8o",
+ "parentId": "container-C0z6ru33to",
+ "data": {
+ "fieldIdentifier": {
+ "nodeId": "1a0e6c7b-9d2f-4b3c-8e1a-2f3d4c5b6a7e",
+ "fieldName": "component_source"
+ },
+ "showDescription": false
+ },
+ "type": "node-field"
+ },
+ "node-field-7G9srQT2Uw": {
+ "id": "node-field-7G9srQT2Uw",
+ "parentId": "container-7g1Xedgg8T",
+ "data": {
+ "fieldIdentifier": {
+ "nodeId": "1a0e6c7b-9d2f-4b3c-8e1a-2f3d4c5b6a7e",
+ "fieldName": "transformer_low_noise_model"
+ },
+ "showDescription": false
+ },
+ "type": "node-field"
+ },
+ "node-field-Q345WyNzc4": {
+ "id": "node-field-Q345WyNzc4",
+ "parentId": "container-y0w7TfJC7e",
+ "data": {
+ "fieldIdentifier": {
+ "nodeId": "1a0e6c7b-9d2f-4b3c-8e1a-2f3d4c5b6a7e",
+ "fieldName": "model"
+ },
+ "showDescription": false
+ },
+ "type": "node-field"
+ },
+ "container-mUeBcw2tYf": {
+ "id": "container-mUeBcw2tYf",
+ "parentId": "root",
+ "data": {
+ "layout": "row",
+ "children": [
+ "node-field-UiQ7MaseDR",
+ "node-field-FiH14mitcM",
+ "node-field-zQA9XuRZRW"
+ ]
+ },
+ "type": "container"
+ },
+ "heading-XDPaQofdEE": {
+ "id": "heading-XDPaQofdEE",
+ "parentId": "root",
+ "data": {
+ "content": "Generation"
+ },
+ "type": "heading"
+ },
+ "container-i8V1oJeQCP": {
+ "id": "container-i8V1oJeQCP",
+ "parentId": "root",
+ "data": {
+ "layout": "row",
+ "children": [
+ "node-field-dKizHbAltw",
+ "node-field-DonE1WRSA6"
+ ]
+ },
+ "type": "container"
+ },
+ "heading-wuPtY8B4vR": {
+ "id": "heading-wuPtY8B4vR",
+ "parentId": "root",
+ "data": {
+ "content": "Model"
+ },
+ "type": "heading"
+ },
+ "container-AkpUH1WtID": {
+ "id": "container-AkpUH1WtID",
+ "parentId": "root",
+ "data": {
+ "layout": "row",
+ "children": [
+ "container-y0w7TfJC7e",
+ "container-7g1Xedgg8T"
+ ]
+ },
+ "type": "container"
+ },
+ "container-y0w7TfJC7e": {
+ "id": "container-y0w7TfJC7e",
+ "parentId": "container-AkpUH1WtID",
+ "data": {
+ "layout": "column",
+ "children": [
+ "node-field-Q345WyNzc4",
+ "node-field-PdU0Sz2m5z"
+ ]
+ },
+ "type": "container"
+ },
+ "container-7g1Xedgg8T": {
+ "id": "container-7g1Xedgg8T",
+ "parentId": "container-AkpUH1WtID",
+ "data": {
+ "layout": "column",
+ "children": [
+ "node-field-7G9srQT2Uw",
+ "node-field-QDqQ4XOKvg"
+ ]
+ },
+ "type": "container"
+ },
+ "container-C0z6ru33to": {
+ "id": "container-C0z6ru33to",
+ "parentId": "root",
+ "data": {
+ "layout": "row",
+ "children": [
+ "node-field-N9mexEKkYg",
+ "node-field-2u9lAbMBnD",
+ "node-field-CTGqrO3x8o"
+ ]
+ },
+ "type": "container"
+ },
+ "heading-10PuWMKJg9": {
+ "id": "heading-10PuWMKJg9",
+ "parentId": "root",
+ "data": {
+ "content": "Advanced"
+ },
+ "type": "heading"
+ },
+ "heading-TwsoE3wQi9": {
+ "id": "heading-TwsoE3wQi9",
+ "parentId": "root",
+ "data": {
+ "content": "Lightning LoRAs"
+ },
+ "type": "heading"
+ },
+ "node-field-dKizHbAltw": {
+ "id": "node-field-dKizHbAltw",
+ "parentId": "container-i8V1oJeQCP",
+ "data": {
+ "fieldIdentifier": {
+ "nodeId": "738ec1dd-416e-471d-acf0-ca324acd0954",
+ "fieldName": "image"
+ },
+ "showDescription": true
+ },
+ "type": "node-field"
+ },
+ "node-field-DonE1WRSA6": {
+ "id": "node-field-DonE1WRSA6",
+ "parentId": "container-i8V1oJeQCP",
+ "data": {
+ "fieldIdentifier": {
+ "nodeId": "d27f29bf-6f26-4088-b1cc-c722ecd9fd61",
+ "fieldName": "target_resolution"
+ },
+ "showDescription": true
+ },
+ "type": "node-field"
+ },
+ "divider-2T3tRpIIJO": {
+ "id": "divider-2T3tRpIIJO",
+ "parentId": "root",
+ "type": "divider"
+ },
+ "divider-FKc8lld0Eb": {
+ "id": "divider-FKc8lld0Eb",
+ "parentId": "root",
+ "type": "divider"
+ },
+ "node-field-FiH14mitcM": {
+ "id": "node-field-FiH14mitcM",
+ "parentId": "container-mUeBcw2tYf",
+ "data": {
+ "fieldIdentifier": {
+ "nodeId": "c857e3e8-334f-4a6e-9013-3326316e4281",
+ "fieldName": "value"
+ },
+ "showDescription": false,
+ "settings": {
+ "type": "integer-field-config",
+ "showShuffle": false,
+ "component": "number-input"
+ }
+ },
+ "type": "node-field"
+ },
+ "heading-GY6f00b3mJ": {
+ "id": "heading-GY6f00b3mJ",
+ "parentId": "root",
+ "data": {
+ "content": "Concept LoRAs"
+ },
+ "type": "heading"
+ },
+ "container-bNU46OmlI8": {
+ "id": "container-bNU46OmlI8",
+ "parentId": "root",
+ "type": "container",
+ "data": {
+ "layout": "row",
+ "children": [
+ "node-field-1VoWnL7i65",
+ "node-field-JPXFlP0c6f"
+ ]
+ }
+ },
+ "node-field-1VoWnL7i65": {
+ "id": "node-field-1VoWnL7i65",
+ "type": "node-field",
+ "parentId": "container-bNU46OmlI8",
+ "data": {
+ "fieldIdentifier": {
+ "nodeId": "54663968-7e96-432a-a59a-dcd7e40418bc",
+ "fieldName": "lora"
+ },
+ "showDescription": true
+ }
+ },
+ "node-field-JPXFlP0c6f": {
+ "id": "node-field-JPXFlP0c6f",
+ "type": "node-field",
+ "parentId": "container-bNU46OmlI8",
+ "data": {
+ "fieldIdentifier": {
+ "nodeId": "29f62b13-6e9a-4ee3-b279-01b36565fee8",
+ "fieldName": "lora"
+ },
+ "showDescription": true
+ }
+ },
+ "node-field-Ec9kbsLX1V": {
+ "id": "node-field-Ec9kbsLX1V",
+ "type": "node-field",
+ "parentId": "root",
+ "data": {
+ "fieldIdentifier": {
+ "nodeId": "9ea995ab-3db6-45ae-b961-4a3b2b9ab8d4",
+ "fieldName": "loras"
+ },
+ "showDescription": true
+ }
+ }
+ },
+ "rootElementId": "root"
+ },
+ "id": "default_wan22_i2v_lightning_concept"
+}
diff --git a/invokeai/app/services/workflow_records/default_workflows/Image to Video - Wan 2.2 Lightning.json b/invokeai/app/services/workflow_records/default_workflows/Image to Video - Wan 2.2 Lightning.json
new file mode 100644
index 00000000000..9c6cf476b61
--- /dev/null
+++ b/invokeai/app/services/workflow_records/default_workflows/Image to Video - Wan 2.2 Lightning.json
@@ -0,0 +1,1224 @@
+{
+ "name": "Image to Video - Wan 2.2 Lightning",
+ "author": "InvokeAI",
+ "description": "Fast image-to-video generation with Wan 2.2 I2V A14B + the Lightning LoRA pair. Provide an initial image and specify the resolution of the resulting video. The resolution will be applied to the shorter dimension of the image, and the other dimension will be scaled accordingly. ",
+ "version": "1.0.0",
+ "contact": "",
+ "tags": "wan2.2, image to video, video, lightning, lora",
+ "notes": "Each InvokeAI install assigns its own internal model IDs, so the model fields are left blank -- select these in the form after loading:\n\nMain Model node:\n- Main Model: Wan 2.2 I2V A14B High Noise (Q4_K_M GGUF recommended for <=16 GB VRAM)\n- Transformer (Low Noise): Wan 2.2 I2V A14B Low Noise (Q4_K_M)\n- VAE: Wan 2.2 A14B VAE\n- T5 Encoder: Wan T5 Encoder (UMT5-XXL)\n\nLightning LoRAs (4-step distillation, I2V variant):\n- Apply Lightning LoRA (High Noise): Wan 2.2 I2V Lightning High Noise (4-step)\n- Apply Lightning LoRA (Low Noise): Wan 2.2 I2V Lightning Low Noise (4-step)\n\nVRAM: for <=16 GB, keep the resolution at 480p (on the \"Ideal Dimensions\" node); 720p and 1080p need more VRAM.",
+ "exposedFields": [
+ {
+ "nodeId": "1a0e6c7b-9d2f-4b3c-8e1a-2f3d4c5b6a7e",
+ "fieldName": "model"
+ },
+ {
+ "nodeId": "1a0e6c7b-9d2f-4b3c-8e1a-2f3d4c5b6a7e",
+ "fieldName": "transformer_low_noise_model"
+ },
+ {
+ "nodeId": "1a0e6c7b-9d2f-4b3c-8e1a-2f3d4c5b6a7e",
+ "fieldName": "component_source"
+ },
+ {
+ "nodeId": "1a0e6c7b-9d2f-4b3c-8e1a-2f3d4c5b6a7e",
+ "fieldName": "vae_model"
+ },
+ {
+ "nodeId": "1a0e6c7b-9d2f-4b3c-8e1a-2f3d4c5b6a7e",
+ "fieldName": "wan_t5_encoder_model"
+ },
+ {
+ "nodeId": "3466ad11-a931-4adc-b394-9ec287f0c23b",
+ "fieldName": "lora"
+ },
+ {
+ "nodeId": "3466ad11-a931-4adc-b394-9ec287f0c23b",
+ "fieldName": "weight"
+ },
+ {
+ "nodeId": "3466ad11-a931-4adc-b394-9ec287f0c23b",
+ "fieldName": "target"
+ },
+ {
+ "nodeId": "9b4cde65-699f-42c1-8889-76ac91fcb62f",
+ "fieldName": "lora"
+ },
+ {
+ "nodeId": "9b4cde65-699f-42c1-8889-76ac91fcb62f",
+ "fieldName": "weight"
+ },
+ {
+ "nodeId": "9b4cde65-699f-42c1-8889-76ac91fcb62f",
+ "fieldName": "target"
+ },
+ {
+ "nodeId": "2b1f7d8c-ae3f-5c4d-9f2b-3a4e5d6c7b8f",
+ "fieldName": "prompt"
+ },
+ {
+ "nodeId": "3c2a8e9d-bf4a-6d5e-af3c-4b5f6e7d8c9a",
+ "fieldName": "prompt"
+ },
+ {
+ "nodeId": "4d3b9faf-c05b-7e6f-bf4d-5c6a7f8e9dab",
+ "fieldName": "steps"
+ },
+ {
+ "nodeId": "4d3b9faf-c05b-7e6f-bf4d-5c6a7f8e9dab",
+ "fieldName": "guidance_scale"
+ },
+ {
+ "nodeId": "4d3b9faf-c05b-7e6f-bf4d-5c6a7f8e9dab",
+ "fieldName": "guidance_scale_low_noise"
+ },
+ {
+ "nodeId": "4d3b9faf-c05b-7e6f-bf4d-5c6a7f8e9dab",
+ "fieldName": "width"
+ },
+ {
+ "nodeId": "4d3b9faf-c05b-7e6f-bf4d-5c6a7f8e9dab",
+ "fieldName": "height"
+ },
+ {
+ "nodeId": "4d3b9faf-c05b-7e6f-bf4d-5c6a7f8e9dab",
+ "fieldName": "num_frames"
+ },
+ {
+ "nodeId": "6f5dcb1c-e27d-9fb0-d16f-7e8cbaa1fcbd",
+ "fieldName": "fps"
+ }
+ ],
+ "meta": {
+ "version": "4.0.0",
+ "category": "default"
+ },
+ "nodes": [
+ {
+ "id": "1a0e6c7b-9d2f-4b3c-8e1a-2f3d4c5b6a7e",
+ "type": "invocation",
+ "data": {
+ "id": "1a0e6c7b-9d2f-4b3c-8e1a-2f3d4c5b6a7e",
+ "version": "1.0.1",
+ "nodePack": "invokeai",
+ "label": "",
+ "notes": "",
+ "type": "wan_model_loader",
+ "inputs": {
+ "model": {
+ "name": "model",
+ "label": "Transformer (High Noise)",
+ "description": "",
+ "value": null
+ },
+ "transformer_low_noise_model": {
+ "name": "transformer_low_noise_model",
+ "label": "",
+ "description": "",
+ "value": null
+ },
+ "vae_model": {
+ "name": "vae_model",
+ "label": "",
+ "description": "",
+ "value": null
+ },
+ "wan_t5_encoder_model": {
+ "name": "wan_t5_encoder_model",
+ "label": "",
+ "description": "",
+ "value": null
+ },
+ "component_source": {
+ "name": "component_source",
+ "label": "",
+ "description": ""
+ }
+ },
+ "isOpen": true,
+ "isIntermediate": true,
+ "useCache": false
+ },
+ "position": {
+ "x": 0,
+ "y": 0
+ }
+ },
+ {
+ "id": "2b1f7d8c-ae3f-5c4d-9f2b-3a4e5d6c7b8f",
+ "type": "invocation",
+ "data": {
+ "id": "2b1f7d8c-ae3f-5c4d-9f2b-3a4e5d6c7b8f",
+ "version": "1.0.0",
+ "nodePack": "invokeai",
+ "label": "Positive Prompt",
+ "notes": "",
+ "type": "wan_text_encoder",
+ "inputs": {
+ "prompt": {
+ "name": "prompt",
+ "label": "",
+ "description": "",
+ "value": ""
+ },
+ "wan_t5_encoder": {
+ "name": "wan_t5_encoder",
+ "label": "",
+ "description": ""
+ }
+ },
+ "isOpen": true,
+ "isIntermediate": true,
+ "useCache": true
+ },
+ "position": {
+ "x": 700,
+ "y": 212
+ }
+ },
+ {
+ "id": "3c2a8e9d-bf4a-6d5e-af3c-4b5f6e7d8c9a",
+ "type": "invocation",
+ "data": {
+ "id": "3c2a8e9d-bf4a-6d5e-af3c-4b5f6e7d8c9a",
+ "version": "1.0.0",
+ "nodePack": "invokeai",
+ "label": "Negative Prompt (unused at CFG=1.0)",
+ "notes": "",
+ "type": "wan_text_encoder",
+ "inputs": {
+ "prompt": {
+ "name": "prompt",
+ "label": "",
+ "description": "",
+ "value": " "
+ },
+ "wan_t5_encoder": {
+ "name": "wan_t5_encoder",
+ "label": "",
+ "description": ""
+ }
+ },
+ "isOpen": true,
+ "isIntermediate": true,
+ "useCache": true
+ },
+ "position": {
+ "x": 700,
+ "y": 392
+ }
+ },
+ {
+ "id": "5e4cab0b-d16c-8faf-c05e-6d7baf90ebbc",
+ "type": "invocation",
+ "data": {
+ "id": "5e4cab0b-d16c-8faf-c05e-6d7baf90ebbc",
+ "version": "1.0.1",
+ "nodePack": "invokeai",
+ "label": "",
+ "notes": "",
+ "type": "rand_int",
+ "inputs": {
+ "low": {
+ "name": "low",
+ "label": "",
+ "description": "",
+ "value": 0
+ },
+ "high": {
+ "name": "high",
+ "label": "",
+ "description": "",
+ "value": 2147483647
+ }
+ },
+ "isOpen": true,
+ "isIntermediate": true,
+ "useCache": false
+ },
+ "position": {
+ "x": 700,
+ "y": 572
+ }
+ },
+ {
+ "id": "4d3b9faf-c05b-7e6f-bf4d-5c6a7f8e9dab",
+ "type": "invocation",
+ "data": {
+ "id": "4d3b9faf-c05b-7e6f-bf4d-5c6a7f8e9dab",
+ "version": "1.0.0",
+ "nodePack": "invokeai",
+ "label": "",
+ "notes": "",
+ "type": "wan_video_denoise",
+ "inputs": {
+ "transformer": {
+ "name": "transformer",
+ "label": "",
+ "description": ""
+ },
+ "positive_conditioning": {
+ "name": "positive_conditioning",
+ "label": "",
+ "description": ""
+ },
+ "negative_conditioning": {
+ "name": "negative_conditioning",
+ "label": "",
+ "description": ""
+ },
+ "ref_image": {
+ "name": "ref_image",
+ "label": "",
+ "description": ""
+ },
+ "guidance_scale": {
+ "name": "guidance_scale",
+ "label": "CFG (High Noise)",
+ "description": "",
+ "value": 1
+ },
+ "guidance_scale_low_noise": {
+ "name": "guidance_scale_low_noise",
+ "label": "CFG (Low Noise)",
+ "description": "",
+ "value": 1
+ },
+ "width": {
+ "name": "width",
+ "label": "",
+ "description": "",
+ "value": 832
+ },
+ "height": {
+ "name": "height",
+ "label": "",
+ "description": "",
+ "value": 480
+ },
+ "num_frames": {
+ "name": "num_frames",
+ "label": "Frames",
+ "description": "",
+ "value": 81
+ },
+ "steps": {
+ "name": "steps",
+ "label": "",
+ "description": "",
+ "value": 4
+ },
+ "seed": {
+ "name": "seed",
+ "label": "",
+ "description": "",
+ "value": 0
+ }
+ },
+ "isOpen": true,
+ "isIntermediate": true,
+ "useCache": true
+ },
+ "position": {
+ "x": 1074.3058275334374,
+ "y": 448
+ }
+ },
+ {
+ "id": "6f5dcb1c-e27d-9fb0-d16f-7e8cbaa1fcbd",
+ "type": "invocation",
+ "data": {
+ "id": "6f5dcb1c-e27d-9fb0-d16f-7e8cbaa1fcbd",
+ "version": "1.0.0",
+ "nodePack": "invokeai",
+ "label": "",
+ "notes": "",
+ "type": "wan_l2v",
+ "inputs": {
+ "board": {
+ "name": "board",
+ "label": "",
+ "description": ""
+ },
+ "metadata": {
+ "name": "metadata",
+ "label": "",
+ "description": ""
+ },
+ "latents": {
+ "name": "latents",
+ "label": "",
+ "description": ""
+ },
+ "vae": {
+ "name": "vae",
+ "label": "",
+ "description": ""
+ },
+ "fps": {
+ "name": "fps",
+ "label": "FPS",
+ "description": "",
+ "value": 16
+ }
+ },
+ "isOpen": true,
+ "isIntermediate": false,
+ "useCache": true
+ },
+ "position": {
+ "x": 1400,
+ "y": 33
+ }
+ },
+ {
+ "id": "291abec8-b996-4c7b-959c-360f56a3ed60",
+ "type": "invocation",
+ "data": {
+ "id": "291abec8-b996-4c7b-959c-360f56a3ed60",
+ "version": "1.0.1",
+ "nodePack": "invokeai",
+ "label": "Apply Lightning LoRA (High Noise)",
+ "notes": "",
+ "type": "wan_lora_loader",
+ "inputs": {
+ "lora": {
+ "name": "lora",
+ "label": "Lightning LoRA (High Noise)",
+ "description": "",
+ "value": null
+ },
+ "weight": {
+ "name": "weight",
+ "label": "",
+ "description": "",
+ "value": 1
+ },
+ "target": {
+ "name": "target",
+ "label": "",
+ "description": "",
+ "value": "high"
+ },
+ "transformer": {
+ "name": "transformer",
+ "label": "",
+ "description": ""
+ }
+ },
+ "isOpen": true,
+ "isIntermediate": true,
+ "useCache": true
+ },
+ "position": {
+ "x": 350,
+ "y": 772
+ }
+ },
+ {
+ "id": "ae8f115d-a3b8-4f76-b280-7313366a94ad",
+ "type": "invocation",
+ "data": {
+ "id": "ae8f115d-a3b8-4f76-b280-7313366a94ad",
+ "version": "1.0.1",
+ "nodePack": "invokeai",
+ "label": "",
+ "notes": "",
+ "type": "wan_lora_loader",
+ "inputs": {
+ "lora": {
+ "name": "lora",
+ "label": "Lightning LoRA (Low Noise)",
+ "description": "",
+ "value": null
+ },
+ "weight": {
+ "name": "weight",
+ "label": "",
+ "description": "",
+ "value": 1
+ },
+ "target": {
+ "name": "target",
+ "label": "",
+ "description": "",
+ "value": "low"
+ },
+ "transformer": {
+ "name": "transformer",
+ "label": "",
+ "description": ""
+ }
+ },
+ "isOpen": true,
+ "isIntermediate": true,
+ "useCache": true
+ },
+ "position": {
+ "x": 700,
+ "y": 772
+ }
+ },
+ {
+ "id": "97c6fecb-8354-4ace-b337-e544b0ba9982",
+ "type": "invocation",
+ "data": {
+ "id": "97c6fecb-8354-4ace-b337-e544b0ba9982",
+ "version": "1.2.0",
+ "nodePack": "invokeai",
+ "label": "",
+ "notes": "",
+ "type": "wan_ref_image_encoder",
+ "inputs": {
+ "image": {
+ "name": "image",
+ "label": "",
+ "description": ""
+ },
+ "end_image": { "name": "end_image", "label": "" },
+ "vae": {
+ "name": "vae",
+ "label": "",
+ "description": ""
+ },
+ "width": {
+ "name": "width",
+ "label": "",
+ "description": "",
+ "value": 1024
+ },
+ "height": {
+ "name": "height",
+ "label": "",
+ "description": "",
+ "value": 1024
+ },
+ "num_frames": {
+ "name": "num_frames",
+ "label": "",
+ "description": "",
+ "value": 1
+ }
+ },
+ "isOpen": true,
+ "isIntermediate": true,
+ "useCache": true
+ },
+ "position": {
+ "x": 793.5169473342651,
+ "y": 1028.2344631556177
+ }
+ },
+ {
+ "id": "d27f29bf-6f26-4088-b1cc-c722ecd9fd61",
+ "type": "invocation",
+ "data": {
+ "id": "d27f29bf-6f26-4088-b1cc-c722ecd9fd61",
+ "version": "1.1.0",
+ "nodePack": "invokeai",
+ "label": "",
+ "notes": "",
+ "type": "wan_i2v_ideal_dimensions",
+ "inputs": {
+ "width": {
+ "name": "width",
+ "label": "",
+ "description": "",
+ "value": 1024
+ },
+ "height": {
+ "name": "height",
+ "label": "",
+ "description": "",
+ "value": 1024
+ },
+ "target_resolution": {
+ "name": "target_resolution",
+ "label": "Video Resolution",
+ "description": "",
+ "value": "480p"
+ },
+ "rounding": {
+ "name": "rounding",
+ "label": "",
+ "description": "",
+ "value": "nearest"
+ }
+ },
+ "isOpen": true,
+ "isIntermediate": true,
+ "useCache": true
+ },
+ "position": {
+ "x": 350,
+ "y": 1142
+ }
+ },
+ {
+ "id": "738ec1dd-416e-471d-acf0-ca324acd0954",
+ "type": "invocation",
+ "data": {
+ "id": "738ec1dd-416e-471d-acf0-ca324acd0954",
+ "version": "1.0.2",
+ "nodePack": "invokeai",
+ "label": "",
+ "notes": "",
+ "type": "image",
+ "inputs": {
+ "image": {
+ "name": "image",
+ "label": "Starting Image",
+ "description": "First frame of the video"
+ }
+ },
+ "isOpen": true,
+ "isIntermediate": true,
+ "useCache": true
+ },
+ "position": {
+ "x": 0,
+ "y": 980
+ }
+ },
+ {
+ "id": "c857e3e8-334f-4a6e-9013-3326316e4281",
+ "type": "invocation",
+ "data": {
+ "id": "c857e3e8-334f-4a6e-9013-3326316e4281",
+ "version": "1.0.1",
+ "nodePack": "invokeai",
+ "label": "Num Frames",
+ "notes": "",
+ "type": "integer",
+ "inputs": {
+ "value": {
+ "name": "value",
+ "label": "Frames",
+ "description": "",
+ "value": 81
+ }
+ },
+ "isOpen": true,
+ "isIntermediate": true,
+ "useCache": true
+ },
+ "position": {
+ "x": -42.07490820354809,
+ "y": 751.8724152644219
+ }
+ }
+ ],
+ "edges": [
+ {
+ "id": "edge-loader-t5-pos",
+ "type": "default",
+ "source": "1a0e6c7b-9d2f-4b3c-8e1a-2f3d4c5b6a7e",
+ "target": "2b1f7d8c-ae3f-5c4d-9f2b-3a4e5d6c7b8f",
+ "sourceHandle": "wan_t5_encoder",
+ "targetHandle": "wan_t5_encoder"
+ },
+ {
+ "id": "edge-loader-t5-neg",
+ "type": "default",
+ "source": "1a0e6c7b-9d2f-4b3c-8e1a-2f3d4c5b6a7e",
+ "target": "3c2a8e9d-bf4a-6d5e-af3c-4b5f6e7d8c9a",
+ "sourceHandle": "wan_t5_encoder",
+ "targetHandle": "wan_t5_encoder"
+ },
+ {
+ "id": "edge-loader-vae-l2v",
+ "type": "default",
+ "source": "1a0e6c7b-9d2f-4b3c-8e1a-2f3d4c5b6a7e",
+ "target": "6f5dcb1c-e27d-9fb0-d16f-7e8cbaa1fcbd",
+ "sourceHandle": "vae",
+ "targetHandle": "vae"
+ },
+ {
+ "id": "edge-pos-cond-denoise",
+ "type": "default",
+ "source": "2b1f7d8c-ae3f-5c4d-9f2b-3a4e5d6c7b8f",
+ "target": "4d3b9faf-c05b-7e6f-bf4d-5c6a7f8e9dab",
+ "sourceHandle": "conditioning",
+ "targetHandle": "positive_conditioning"
+ },
+ {
+ "id": "edge-neg-cond-denoise",
+ "type": "default",
+ "source": "3c2a8e9d-bf4a-6d5e-af3c-4b5f6e7d8c9a",
+ "target": "4d3b9faf-c05b-7e6f-bf4d-5c6a7f8e9dab",
+ "sourceHandle": "conditioning",
+ "targetHandle": "negative_conditioning"
+ },
+ {
+ "id": "edge-rand-seed-denoise",
+ "type": "default",
+ "source": "5e4cab0b-d16c-8faf-c05e-6d7baf90ebbc",
+ "target": "4d3b9faf-c05b-7e6f-bf4d-5c6a7f8e9dab",
+ "sourceHandle": "value",
+ "targetHandle": "seed"
+ },
+ {
+ "id": "edge-denoise-latents-l2v",
+ "type": "default",
+ "source": "4d3b9faf-c05b-7e6f-bf4d-5c6a7f8e9dab",
+ "target": "6f5dcb1c-e27d-9fb0-d16f-7e8cbaa1fcbd",
+ "sourceHandle": "latents",
+ "targetHandle": "latents"
+ },
+ {
+ "id": "reactflow__edge-1a0e6c7b-9d2f-4b3c-8e1a-2f3d4c5b6a7etransformer-291abec8-b996-4c7b-959c-360f56a3ed60transformer",
+ "type": "default",
+ "source": "1a0e6c7b-9d2f-4b3c-8e1a-2f3d4c5b6a7e",
+ "target": "291abec8-b996-4c7b-959c-360f56a3ed60",
+ "sourceHandle": "transformer",
+ "targetHandle": "transformer"
+ },
+ {
+ "id": "reactflow__edge-291abec8-b996-4c7b-959c-360f56a3ed60transformer-ae8f115d-a3b8-4f76-b280-7313366a94adtransformer",
+ "type": "default",
+ "source": "291abec8-b996-4c7b-959c-360f56a3ed60",
+ "target": "ae8f115d-a3b8-4f76-b280-7313366a94ad",
+ "sourceHandle": "transformer",
+ "targetHandle": "transformer"
+ },
+ {
+ "id": "reactflow__edge-ae8f115d-a3b8-4f76-b280-7313366a94adtransformer-4d3b9faf-c05b-7e6f-bf4d-5c6a7f8e9dabtransformer",
+ "type": "default",
+ "source": "ae8f115d-a3b8-4f76-b280-7313366a94ad",
+ "target": "4d3b9faf-c05b-7e6f-bf4d-5c6a7f8e9dab",
+ "sourceHandle": "transformer",
+ "targetHandle": "transformer"
+ },
+ {
+ "id": "reactflow__edge-97c6fecb-8354-4ace-b337-e544b0ba9982ref_image-4d3b9faf-c05b-7e6f-bf4d-5c6a7f8e9dabref_image",
+ "type": "default",
+ "source": "97c6fecb-8354-4ace-b337-e544b0ba9982",
+ "target": "4d3b9faf-c05b-7e6f-bf4d-5c6a7f8e9dab",
+ "sourceHandle": "ref_image",
+ "targetHandle": "ref_image"
+ },
+ {
+ "id": "reactflow__edge-738ec1dd-416e-471d-acf0-ca324acd0954image-97c6fecb-8354-4ace-b337-e544b0ba9982image",
+ "type": "default",
+ "source": "738ec1dd-416e-471d-acf0-ca324acd0954",
+ "target": "97c6fecb-8354-4ace-b337-e544b0ba9982",
+ "sourceHandle": "image",
+ "targetHandle": "image"
+ },
+ {
+ "id": "reactflow__edge-1a0e6c7b-9d2f-4b3c-8e1a-2f3d4c5b6a7evae-97c6fecb-8354-4ace-b337-e544b0ba9982vae",
+ "type": "default",
+ "source": "1a0e6c7b-9d2f-4b3c-8e1a-2f3d4c5b6a7e",
+ "target": "97c6fecb-8354-4ace-b337-e544b0ba9982",
+ "sourceHandle": "vae",
+ "targetHandle": "vae"
+ },
+ {
+ "id": "reactflow__edge-738ec1dd-416e-471d-acf0-ca324acd0954width-d27f29bf-6f26-4088-b1cc-c722ecd9fd61width",
+ "type": "default",
+ "source": "738ec1dd-416e-471d-acf0-ca324acd0954",
+ "target": "d27f29bf-6f26-4088-b1cc-c722ecd9fd61",
+ "sourceHandle": "width",
+ "targetHandle": "width"
+ },
+ {
+ "id": "reactflow__edge-738ec1dd-416e-471d-acf0-ca324acd0954height-d27f29bf-6f26-4088-b1cc-c722ecd9fd61height",
+ "type": "default",
+ "source": "738ec1dd-416e-471d-acf0-ca324acd0954",
+ "target": "d27f29bf-6f26-4088-b1cc-c722ecd9fd61",
+ "sourceHandle": "height",
+ "targetHandle": "height"
+ },
+ {
+ "id": "reactflow__edge-d27f29bf-6f26-4088-b1cc-c722ecd9fd61width-4d3b9faf-c05b-7e6f-bf4d-5c6a7f8e9dabwidth",
+ "type": "default",
+ "source": "d27f29bf-6f26-4088-b1cc-c722ecd9fd61",
+ "target": "4d3b9faf-c05b-7e6f-bf4d-5c6a7f8e9dab",
+ "sourceHandle": "width",
+ "targetHandle": "width"
+ },
+ {
+ "id": "reactflow__edge-d27f29bf-6f26-4088-b1cc-c722ecd9fd61height-4d3b9faf-c05b-7e6f-bf4d-5c6a7f8e9dabheight",
+ "type": "default",
+ "source": "d27f29bf-6f26-4088-b1cc-c722ecd9fd61",
+ "target": "4d3b9faf-c05b-7e6f-bf4d-5c6a7f8e9dab",
+ "sourceHandle": "height",
+ "targetHandle": "height"
+ },
+ {
+ "id": "reactflow__edge-d27f29bf-6f26-4088-b1cc-c722ecd9fd61width-97c6fecb-8354-4ace-b337-e544b0ba9982width",
+ "type": "default",
+ "source": "d27f29bf-6f26-4088-b1cc-c722ecd9fd61",
+ "target": "97c6fecb-8354-4ace-b337-e544b0ba9982",
+ "sourceHandle": "width",
+ "targetHandle": "width"
+ },
+ {
+ "id": "reactflow__edge-d27f29bf-6f26-4088-b1cc-c722ecd9fd61height-97c6fecb-8354-4ace-b337-e544b0ba9982height",
+ "type": "default",
+ "source": "d27f29bf-6f26-4088-b1cc-c722ecd9fd61",
+ "target": "97c6fecb-8354-4ace-b337-e544b0ba9982",
+ "sourceHandle": "height",
+ "targetHandle": "height"
+ },
+ {
+ "id": "reactflow__edge-c857e3e8-334f-4a6e-9013-3326316e4281value-4d3b9faf-c05b-7e6f-bf4d-5c6a7f8e9dabnum_frames",
+ "type": "default",
+ "source": "c857e3e8-334f-4a6e-9013-3326316e4281",
+ "target": "4d3b9faf-c05b-7e6f-bf4d-5c6a7f8e9dab",
+ "sourceHandle": "value",
+ "targetHandle": "num_frames"
+ },
+ {
+ "id": "reactflow__edge-c857e3e8-334f-4a6e-9013-3326316e4281value-97c6fecb-8354-4ace-b337-e544b0ba9982num_frames",
+ "type": "default",
+ "source": "c857e3e8-334f-4a6e-9013-3326316e4281",
+ "target": "97c6fecb-8354-4ace-b337-e544b0ba9982",
+ "sourceHandle": "value",
+ "targetHandle": "num_frames"
+ }
+ ],
+ "form": {
+ "elements": {
+ "root": {
+ "id": "root",
+ "data": {
+ "layout": "column",
+ "children": [
+ "heading-XDPaQofdEE",
+ "node-field-KcT0glAFDK",
+ "container-i8V1oJeQCP",
+ "container-mUeBcw2tYf",
+ "divider-2T3tRpIIJO",
+ "heading-wuPtY8B4vR",
+ "container-AkpUH1WtID",
+ "heading-TwsoE3wQi9",
+ "container-0u7wyxEdNa",
+ "divider-FKc8lld0Eb",
+ "heading-10PuWMKJg9",
+ "container-C0z6ru33to"
+ ]
+ },
+ "type": "container"
+ },
+ "node-field-zQA9XuRZRW": {
+ "id": "node-field-zQA9XuRZRW",
+ "parentId": "container-mUeBcw2tYf",
+ "data": {
+ "fieldIdentifier": {
+ "nodeId": "6f5dcb1c-e27d-9fb0-d16f-7e8cbaa1fcbd",
+ "fieldName": "fps"
+ },
+ "showDescription": false,
+ "settings": {
+ "type": "integer-field-config",
+ "showShuffle": false,
+ "component": "number-input"
+ }
+ },
+ "type": "node-field"
+ },
+ "node-field-QDqQ4XOKvg": {
+ "id": "node-field-QDqQ4XOKvg",
+ "parentId": "container-7g1Xedgg8T",
+ "data": {
+ "fieldIdentifier": {
+ "nodeId": "4d3b9faf-c05b-7e6f-bf4d-5c6a7f8e9dab",
+ "fieldName": "guidance_scale_low_noise"
+ },
+ "showDescription": false,
+ "settings": {
+ "type": "float-field-config",
+ "showShuffle": false,
+ "component": "number-input"
+ }
+ },
+ "type": "node-field"
+ },
+ "node-field-PdU0Sz2m5z": {
+ "id": "node-field-PdU0Sz2m5z",
+ "parentId": "container-y0w7TfJC7e",
+ "data": {
+ "fieldIdentifier": {
+ "nodeId": "4d3b9faf-c05b-7e6f-bf4d-5c6a7f8e9dab",
+ "fieldName": "guidance_scale"
+ },
+ "showDescription": false,
+ "settings": {
+ "type": "float-field-config",
+ "showShuffle": false,
+ "component": "number-input"
+ }
+ },
+ "type": "node-field"
+ },
+ "node-field-UiQ7MaseDR": {
+ "id": "node-field-UiQ7MaseDR",
+ "parentId": "container-mUeBcw2tYf",
+ "data": {
+ "fieldIdentifier": {
+ "nodeId": "4d3b9faf-c05b-7e6f-bf4d-5c6a7f8e9dab",
+ "fieldName": "steps"
+ },
+ "showDescription": false,
+ "settings": {
+ "type": "integer-field-config",
+ "showShuffle": false,
+ "component": "number-input"
+ }
+ },
+ "type": "node-field"
+ },
+ "node-field-KcT0glAFDK": {
+ "id": "node-field-KcT0glAFDK",
+ "parentId": "root",
+ "data": {
+ "fieldIdentifier": {
+ "nodeId": "2b1f7d8c-ae3f-5c4d-9f2b-3a4e5d6c7b8f",
+ "fieldName": "prompt"
+ },
+ "showDescription": false,
+ "settings": {
+ "type": "string-field-config",
+ "component": "textarea"
+ }
+ },
+ "type": "node-field"
+ },
+ "node-field-N9mexEKkYg": {
+ "id": "node-field-N9mexEKkYg",
+ "parentId": "container-C0z6ru33to",
+ "data": {
+ "fieldIdentifier": {
+ "nodeId": "1a0e6c7b-9d2f-4b3c-8e1a-2f3d4c5b6a7e",
+ "fieldName": "wan_t5_encoder_model"
+ },
+ "showDescription": false
+ },
+ "type": "node-field"
+ },
+ "node-field-2u9lAbMBnD": {
+ "id": "node-field-2u9lAbMBnD",
+ "parentId": "container-C0z6ru33to",
+ "data": {
+ "fieldIdentifier": {
+ "nodeId": "1a0e6c7b-9d2f-4b3c-8e1a-2f3d4c5b6a7e",
+ "fieldName": "vae_model"
+ },
+ "showDescription": false
+ },
+ "type": "node-field"
+ },
+ "node-field-CTGqrO3x8o": {
+ "id": "node-field-CTGqrO3x8o",
+ "parentId": "container-C0z6ru33to",
+ "data": {
+ "fieldIdentifier": {
+ "nodeId": "1a0e6c7b-9d2f-4b3c-8e1a-2f3d4c5b6a7e",
+ "fieldName": "component_source"
+ },
+ "showDescription": false
+ },
+ "type": "node-field"
+ },
+ "node-field-7G9srQT2Uw": {
+ "id": "node-field-7G9srQT2Uw",
+ "parentId": "container-7g1Xedgg8T",
+ "data": {
+ "fieldIdentifier": {
+ "nodeId": "1a0e6c7b-9d2f-4b3c-8e1a-2f3d4c5b6a7e",
+ "fieldName": "transformer_low_noise_model"
+ },
+ "showDescription": false
+ },
+ "type": "node-field"
+ },
+ "node-field-Q345WyNzc4": {
+ "id": "node-field-Q345WyNzc4",
+ "parentId": "container-y0w7TfJC7e",
+ "data": {
+ "fieldIdentifier": {
+ "nodeId": "1a0e6c7b-9d2f-4b3c-8e1a-2f3d4c5b6a7e",
+ "fieldName": "model"
+ },
+ "showDescription": false
+ },
+ "type": "node-field"
+ },
+ "container-mUeBcw2tYf": {
+ "id": "container-mUeBcw2tYf",
+ "parentId": "root",
+ "data": {
+ "layout": "row",
+ "children": [
+ "node-field-UiQ7MaseDR",
+ "node-field-FiH14mitcM",
+ "node-field-zQA9XuRZRW"
+ ]
+ },
+ "type": "container"
+ },
+ "heading-XDPaQofdEE": {
+ "id": "heading-XDPaQofdEE",
+ "parentId": "root",
+ "data": {
+ "content": "Generation"
+ },
+ "type": "heading"
+ },
+ "container-i8V1oJeQCP": {
+ "id": "container-i8V1oJeQCP",
+ "parentId": "root",
+ "data": {
+ "layout": "row",
+ "children": [
+ "node-field-dKizHbAltw",
+ "node-field-DonE1WRSA6"
+ ]
+ },
+ "type": "container"
+ },
+ "heading-wuPtY8B4vR": {
+ "id": "heading-wuPtY8B4vR",
+ "parentId": "root",
+ "data": {
+ "content": "Model"
+ },
+ "type": "heading"
+ },
+ "container-AkpUH1WtID": {
+ "id": "container-AkpUH1WtID",
+ "parentId": "root",
+ "data": {
+ "layout": "row",
+ "children": [
+ "container-y0w7TfJC7e",
+ "container-7g1Xedgg8T"
+ ]
+ },
+ "type": "container"
+ },
+ "container-y0w7TfJC7e": {
+ "id": "container-y0w7TfJC7e",
+ "parentId": "container-AkpUH1WtID",
+ "data": {
+ "layout": "column",
+ "children": [
+ "node-field-Q345WyNzc4",
+ "node-field-PdU0Sz2m5z"
+ ]
+ },
+ "type": "container"
+ },
+ "container-7g1Xedgg8T": {
+ "id": "container-7g1Xedgg8T",
+ "parentId": "container-AkpUH1WtID",
+ "data": {
+ "layout": "column",
+ "children": [
+ "node-field-7G9srQT2Uw",
+ "node-field-QDqQ4XOKvg"
+ ]
+ },
+ "type": "container"
+ },
+ "container-C0z6ru33to": {
+ "id": "container-C0z6ru33to",
+ "parentId": "root",
+ "data": {
+ "layout": "row",
+ "children": [
+ "node-field-N9mexEKkYg",
+ "node-field-2u9lAbMBnD",
+ "node-field-CTGqrO3x8o"
+ ]
+ },
+ "type": "container"
+ },
+ "heading-10PuWMKJg9": {
+ "id": "heading-10PuWMKJg9",
+ "parentId": "root",
+ "data": {
+ "content": "Advanced"
+ },
+ "type": "heading"
+ },
+ "container-0u7wyxEdNa": {
+ "id": "container-0u7wyxEdNa",
+ "parentId": "root",
+ "data": {
+ "layout": "row",
+ "children": [
+ "container-Um9gcFv0lo",
+ "container-4owWd0thWk"
+ ]
+ },
+ "type": "container"
+ },
+ "container-Um9gcFv0lo": {
+ "id": "container-Um9gcFv0lo",
+ "parentId": "container-0u7wyxEdNa",
+ "data": {
+ "layout": "row",
+ "children": [
+ "container-lF8o60KuVb"
+ ]
+ },
+ "type": "container"
+ },
+ "container-4owWd0thWk": {
+ "id": "container-4owWd0thWk",
+ "parentId": "container-0u7wyxEdNa",
+ "data": {
+ "layout": "row",
+ "children": [
+ "container-rvE0oy63qL"
+ ]
+ },
+ "type": "container"
+ },
+ "node-field-a8nZxgz9u9": {
+ "id": "node-field-a8nZxgz9u9",
+ "parentId": "container-lF8o60KuVb",
+ "data": {
+ "fieldIdentifier": {
+ "nodeId": "291abec8-b996-4c7b-959c-360f56a3ed60",
+ "fieldName": "lora"
+ },
+ "showDescription": true
+ },
+ "type": "node-field"
+ },
+ "node-field-NwR9yOugEz": {
+ "id": "node-field-NwR9yOugEz",
+ "parentId": "container-rvE0oy63qL",
+ "data": {
+ "fieldIdentifier": {
+ "nodeId": "ae8f115d-a3b8-4f76-b280-7313366a94ad",
+ "fieldName": "lora"
+ },
+ "showDescription": true
+ },
+ "type": "node-field"
+ },
+ "container-lF8o60KuVb": {
+ "id": "container-lF8o60KuVb",
+ "parentId": "container-Um9gcFv0lo",
+ "data": {
+ "layout": "column",
+ "children": [
+ "node-field-a8nZxgz9u9",
+ "node-field-0al3LtpcGU"
+ ]
+ },
+ "type": "container"
+ },
+ "container-rvE0oy63qL": {
+ "id": "container-rvE0oy63qL",
+ "parentId": "container-4owWd0thWk",
+ "data": {
+ "layout": "column",
+ "children": [
+ "node-field-NwR9yOugEz",
+ "node-field-lgLAjS4nks"
+ ]
+ },
+ "type": "container"
+ },
+ "node-field-0al3LtpcGU": {
+ "id": "node-field-0al3LtpcGU",
+ "parentId": "container-lF8o60KuVb",
+ "data": {
+ "fieldIdentifier": {
+ "nodeId": "291abec8-b996-4c7b-959c-360f56a3ed60",
+ "fieldName": "weight"
+ },
+ "showDescription": true,
+ "settings": {
+ "type": "float-field-config",
+ "showShuffle": false,
+ "component": "number-input"
+ }
+ },
+ "type": "node-field"
+ },
+ "node-field-lgLAjS4nks": {
+ "id": "node-field-lgLAjS4nks",
+ "parentId": "container-rvE0oy63qL",
+ "data": {
+ "fieldIdentifier": {
+ "nodeId": "ae8f115d-a3b8-4f76-b280-7313366a94ad",
+ "fieldName": "weight"
+ },
+ "showDescription": true,
+ "settings": {
+ "type": "float-field-config",
+ "showShuffle": false,
+ "component": "number-input"
+ }
+ },
+ "type": "node-field"
+ },
+ "heading-TwsoE3wQi9": {
+ "id": "heading-TwsoE3wQi9",
+ "parentId": "root",
+ "data": {
+ "content": "Lightning LoRAs"
+ },
+ "type": "heading"
+ },
+ "node-field-dKizHbAltw": {
+ "id": "node-field-dKizHbAltw",
+ "parentId": "container-i8V1oJeQCP",
+ "data": {
+ "fieldIdentifier": {
+ "nodeId": "738ec1dd-416e-471d-acf0-ca324acd0954",
+ "fieldName": "image"
+ },
+ "showDescription": true
+ },
+ "type": "node-field"
+ },
+ "node-field-DonE1WRSA6": {
+ "id": "node-field-DonE1WRSA6",
+ "parentId": "container-i8V1oJeQCP",
+ "data": {
+ "fieldIdentifier": {
+ "nodeId": "d27f29bf-6f26-4088-b1cc-c722ecd9fd61",
+ "fieldName": "target_resolution"
+ },
+ "showDescription": true
+ },
+ "type": "node-field"
+ },
+ "divider-2T3tRpIIJO": {
+ "id": "divider-2T3tRpIIJO",
+ "parentId": "root",
+ "type": "divider"
+ },
+ "divider-FKc8lld0Eb": {
+ "id": "divider-FKc8lld0Eb",
+ "parentId": "root",
+ "type": "divider"
+ },
+ "node-field-FiH14mitcM": {
+ "id": "node-field-FiH14mitcM",
+ "parentId": "container-mUeBcw2tYf",
+ "data": {
+ "fieldIdentifier": {
+ "nodeId": "c857e3e8-334f-4a6e-9013-3326316e4281",
+ "fieldName": "value"
+ },
+ "showDescription": false,
+ "settings": {
+ "type": "integer-field-config",
+ "showShuffle": false,
+ "component": "number-input"
+ }
+ },
+ "type": "node-field"
+ }
+ },
+ "rootElementId": "root"
+ },
+ "id": "default_wan22_i2v_lightning"
+}
diff --git a/invokeai/app/services/workflow_records/default_workflows/Image to Video - Wan 2.2 TI2V-5B (Low Quality).json b/invokeai/app/services/workflow_records/default_workflows/Image to Video - Wan 2.2 TI2V-5B (Low Quality).json
new file mode 100644
index 00000000000..c11adaed757
--- /dev/null
+++ b/invokeai/app/services/workflow_records/default_workflows/Image to Video - Wan 2.2 TI2V-5B (Low Quality).json
@@ -0,0 +1,872 @@
+{
+ "name": "Image to Video - Wan 2.2 TI2V (Low Quality)",
+ "author": "InvokeAI",
+ "description": "Low quality but fast image-to-video generation with Wan 2.2 TI2V 5B model. Provide an initial image and a target resolution for the video.",
+ "version": "1.0.0",
+ "contact": "",
+ "tags": "wan2.2, image to video, video",
+ "notes": "This workflow needs the Wan 2.2 TI2V-5B model. Each InvokeAI install assigns its own internal model IDs, so the model fields are left blank -- select these in the form after loading:\n\nMain Model node:\n- Main Model: Wan 2.2 TI2V-5B (Q4_K_M GGUF recommended for <=16 GB VRAM)\n- Transformer (Low Noise): leave empty (TI2V-5B is a single-expert model)\n- VAE: Wan 2.2 TI2V-5B VAE\n- T5 Encoder: Wan T5 Encoder (UMT5-XXL)\n\n720p should work on GPUs with 16 GB VRAM and up.",
+ "exposedFields": [
+ {
+ "nodeId": "1a0e6c7b-9d2f-4b3c-8e1a-2f3d4c5b6a7e",
+ "fieldName": "model"
+ },
+ {
+ "nodeId": "1a0e6c7b-9d2f-4b3c-8e1a-2f3d4c5b6a7e",
+ "fieldName": "transformer_low_noise_model"
+ },
+ {
+ "nodeId": "1a0e6c7b-9d2f-4b3c-8e1a-2f3d4c5b6a7e",
+ "fieldName": "component_source"
+ },
+ {
+ "nodeId": "1a0e6c7b-9d2f-4b3c-8e1a-2f3d4c5b6a7e",
+ "fieldName": "vae_model"
+ },
+ {
+ "nodeId": "1a0e6c7b-9d2f-4b3c-8e1a-2f3d4c5b6a7e",
+ "fieldName": "wan_t5_encoder_model"
+ },
+ {
+ "nodeId": "3466ad11-a931-4adc-b394-9ec287f0c23b",
+ "fieldName": "lora"
+ },
+ {
+ "nodeId": "3466ad11-a931-4adc-b394-9ec287f0c23b",
+ "fieldName": "weight"
+ },
+ {
+ "nodeId": "3466ad11-a931-4adc-b394-9ec287f0c23b",
+ "fieldName": "target"
+ },
+ {
+ "nodeId": "9b4cde65-699f-42c1-8889-76ac91fcb62f",
+ "fieldName": "lora"
+ },
+ {
+ "nodeId": "9b4cde65-699f-42c1-8889-76ac91fcb62f",
+ "fieldName": "weight"
+ },
+ {
+ "nodeId": "9b4cde65-699f-42c1-8889-76ac91fcb62f",
+ "fieldName": "target"
+ },
+ {
+ "nodeId": "2b1f7d8c-ae3f-5c4d-9f2b-3a4e5d6c7b8f",
+ "fieldName": "prompt"
+ },
+ {
+ "nodeId": "3c2a8e9d-bf4a-6d5e-af3c-4b5f6e7d8c9a",
+ "fieldName": "prompt"
+ },
+ {
+ "nodeId": "4d3b9faf-c05b-7e6f-bf4d-5c6a7f8e9dab",
+ "fieldName": "steps"
+ },
+ {
+ "nodeId": "4d3b9faf-c05b-7e6f-bf4d-5c6a7f8e9dab",
+ "fieldName": "guidance_scale"
+ },
+ {
+ "nodeId": "4d3b9faf-c05b-7e6f-bf4d-5c6a7f8e9dab",
+ "fieldName": "guidance_scale_low_noise"
+ },
+ {
+ "nodeId": "4d3b9faf-c05b-7e6f-bf4d-5c6a7f8e9dab",
+ "fieldName": "width"
+ },
+ {
+ "nodeId": "4d3b9faf-c05b-7e6f-bf4d-5c6a7f8e9dab",
+ "fieldName": "height"
+ },
+ {
+ "nodeId": "4d3b9faf-c05b-7e6f-bf4d-5c6a7f8e9dab",
+ "fieldName": "num_frames"
+ },
+ {
+ "nodeId": "6f5dcb1c-e27d-9fb0-d16f-7e8cbaa1fcbd",
+ "fieldName": "fps"
+ }
+ ],
+ "meta": {
+ "version": "4.0.0",
+ "category": "default"
+ },
+ "nodes": [
+ {
+ "id": "1a0e6c7b-9d2f-4b3c-8e1a-2f3d4c5b6a7e",
+ "type": "invocation",
+ "data": {
+ "id": "1a0e6c7b-9d2f-4b3c-8e1a-2f3d4c5b6a7e",
+ "version": "1.0.1",
+ "nodePack": "invokeai",
+ "label": "",
+ "notes": "",
+ "type": "wan_model_loader",
+ "inputs": {
+ "model": {
+ "name": "model",
+ "label": "Transformer (High Noise)",
+ "description": "",
+ "value": null
+ },
+ "transformer_low_noise_model": {
+ "name": "transformer_low_noise_model",
+ "label": "",
+ "description": ""
+ },
+ "vae_model": {
+ "name": "vae_model",
+ "label": "",
+ "description": "",
+ "value": null
+ },
+ "wan_t5_encoder_model": {
+ "name": "wan_t5_encoder_model",
+ "label": "",
+ "description": "",
+ "value": null
+ },
+ "component_source": {
+ "name": "component_source",
+ "label": "",
+ "description": ""
+ }
+ },
+ "isOpen": true,
+ "isIntermediate": true,
+ "useCache": false
+ },
+ "position": {
+ "x": 0,
+ "y": 0
+ }
+ },
+ {
+ "id": "2b1f7d8c-ae3f-5c4d-9f2b-3a4e5d6c7b8f",
+ "type": "invocation",
+ "data": {
+ "id": "2b1f7d8c-ae3f-5c4d-9f2b-3a4e5d6c7b8f",
+ "version": "1.0.0",
+ "nodePack": "invokeai",
+ "label": "Positive Prompt",
+ "notes": "",
+ "type": "wan_text_encoder",
+ "inputs": {
+ "prompt": {
+ "name": "prompt",
+ "label": "",
+ "description": "",
+ "value": ""
+ },
+ "wan_t5_encoder": {
+ "name": "wan_t5_encoder",
+ "label": "",
+ "description": ""
+ }
+ },
+ "isOpen": true,
+ "isIntermediate": true,
+ "useCache": true
+ },
+ "position": {
+ "x": 686.9799980678728,
+ "y": 210.13999972398182
+ }
+ },
+ {
+ "id": "3c2a8e9d-bf4a-6d5e-af3c-4b5f6e7d8c9a",
+ "type": "invocation",
+ "data": {
+ "id": "3c2a8e9d-bf4a-6d5e-af3c-4b5f6e7d8c9a",
+ "version": "1.0.0",
+ "nodePack": "invokeai",
+ "label": "Negative Prompt (unused at CFG=1.0)",
+ "notes": "",
+ "type": "wan_text_encoder",
+ "inputs": {
+ "prompt": {
+ "name": "prompt",
+ "label": "",
+ "description": "",
+ "value": " "
+ },
+ "wan_t5_encoder": {
+ "name": "wan_t5_encoder",
+ "label": "",
+ "description": ""
+ }
+ },
+ "isOpen": true,
+ "isIntermediate": true,
+ "useCache": true
+ },
+ "position": {
+ "x": 700,
+ "y": 392
+ }
+ },
+ {
+ "id": "5e4cab0b-d16c-8faf-c05e-6d7baf90ebbc",
+ "type": "invocation",
+ "data": {
+ "id": "5e4cab0b-d16c-8faf-c05e-6d7baf90ebbc",
+ "version": "1.0.1",
+ "nodePack": "invokeai",
+ "label": "",
+ "notes": "",
+ "type": "rand_int",
+ "inputs": {
+ "low": {
+ "name": "low",
+ "label": "",
+ "description": "",
+ "value": 0
+ },
+ "high": {
+ "name": "high",
+ "label": "",
+ "description": "",
+ "value": 2147483647
+ }
+ },
+ "isOpen": true,
+ "isIntermediate": true,
+ "useCache": false
+ },
+ "position": {
+ "x": 700,
+ "y": 572
+ }
+ },
+ {
+ "id": "4d3b9faf-c05b-7e6f-bf4d-5c6a7f8e9dab",
+ "type": "invocation",
+ "data": {
+ "id": "4d3b9faf-c05b-7e6f-bf4d-5c6a7f8e9dab",
+ "version": "1.0.0",
+ "nodePack": "invokeai",
+ "label": "",
+ "notes": "",
+ "type": "wan_video_denoise",
+ "inputs": {
+ "transformer": {
+ "name": "transformer",
+ "label": "",
+ "description": ""
+ },
+ "positive_conditioning": {
+ "name": "positive_conditioning",
+ "label": "",
+ "description": ""
+ },
+ "negative_conditioning": {
+ "name": "negative_conditioning",
+ "label": "",
+ "description": ""
+ },
+ "ref_image": {
+ "name": "ref_image",
+ "label": "",
+ "description": ""
+ },
+ "guidance_scale": {
+ "name": "guidance_scale",
+ "label": "CFG (High Noise)",
+ "description": "",
+ "value": 1
+ },
+ "guidance_scale_low_noise": {
+ "name": "guidance_scale_low_noise",
+ "label": "CFG (Low Noise)",
+ "description": "",
+ "value": 1
+ },
+ "width": {
+ "name": "width",
+ "label": "",
+ "description": "",
+ "value": 832
+ },
+ "height": {
+ "name": "height",
+ "label": "",
+ "description": "",
+ "value": 480
+ },
+ "num_frames": {
+ "name": "num_frames",
+ "label": "Frames",
+ "description": "",
+ "value": 81
+ },
+ "steps": {
+ "name": "steps",
+ "label": "",
+ "description": "",
+ "value": 20
+ },
+ "seed": {
+ "name": "seed",
+ "label": "",
+ "description": "",
+ "value": 0
+ }
+ },
+ "isOpen": true,
+ "isIntermediate": true,
+ "useCache": true
+ },
+ "position": {
+ "x": 1050,
+ "y": 230
+ }
+ },
+ {
+ "id": "6f5dcb1c-e27d-9fb0-d16f-7e8cbaa1fcbd",
+ "type": "invocation",
+ "data": {
+ "id": "6f5dcb1c-e27d-9fb0-d16f-7e8cbaa1fcbd",
+ "version": "1.0.0",
+ "nodePack": "invokeai",
+ "label": "",
+ "notes": "",
+ "type": "wan_l2v",
+ "inputs": {
+ "board": {
+ "name": "board",
+ "label": "",
+ "description": ""
+ },
+ "metadata": {
+ "name": "metadata",
+ "label": "",
+ "description": ""
+ },
+ "latents": {
+ "name": "latents",
+ "label": "",
+ "description": ""
+ },
+ "vae": {
+ "name": "vae",
+ "label": "",
+ "description": ""
+ },
+ "fps": {
+ "name": "fps",
+ "label": "FPS",
+ "description": "",
+ "value": 16
+ }
+ },
+ "isOpen": true,
+ "isIntermediate": false,
+ "useCache": true
+ },
+ "position": {
+ "x": 1400,
+ "y": 33
+ }
+ },
+ {
+ "id": "1473db93-45eb-4468-bba1-d0f9d1c21aed",
+ "type": "invocation",
+ "data": {
+ "id": "1473db93-45eb-4468-bba1-d0f9d1c21aed",
+ "version": "1.2.0",
+ "nodePack": "invokeai",
+ "label": "",
+ "notes": "",
+ "type": "wan_ref_image_encoder",
+ "inputs": {
+ "image": {
+ "name": "image",
+ "label": "",
+ "description": ""
+ },
+ "end_image": { "name": "end_image", "label": "" },
+ "vae": {
+ "name": "vae",
+ "label": "",
+ "description": ""
+ },
+ "width": {
+ "name": "width",
+ "label": "",
+ "description": "",
+ "value": 1024
+ },
+ "height": {
+ "name": "height",
+ "label": "",
+ "description": "",
+ "value": 1024
+ },
+ "num_frames": {
+ "name": "num_frames",
+ "label": "",
+ "description": "",
+ "value": 1
+ }
+ },
+ "isOpen": true,
+ "isIntermediate": true,
+ "useCache": true
+ },
+ "position": {
+ "x": 669.4858247907592,
+ "y": -383.75896955550337
+ }
+ },
+ {
+ "id": "0771d3a4-7f0b-45bd-9c73-a99b40e24b7b",
+ "type": "invocation",
+ "data": {
+ "id": "0771d3a4-7f0b-45bd-9c73-a99b40e24b7b",
+ "version": "1.0.2",
+ "nodePack": "invokeai",
+ "label": "",
+ "notes": "",
+ "type": "image",
+ "inputs": {
+ "image": {
+ "name": "image",
+ "label": "Initial Image",
+ "description": ""
+ }
+ },
+ "isOpen": true,
+ "isIntermediate": true,
+ "useCache": true
+ },
+ "position": {
+ "x": -165.5308813883526,
+ "y": -364.6304014144227
+ }
+ },
+ {
+ "id": "55cb8b4f-b7fc-4d37-9861-d5c84a57353b",
+ "type": "invocation",
+ "data": {
+ "id": "55cb8b4f-b7fc-4d37-9861-d5c84a57353b",
+ "version": "1.0.0",
+ "nodePack": "invokeai",
+ "label": "",
+ "notes": "",
+ "type": "wan_ti2v_ideal_dimensions",
+ "inputs": {
+ "width": {
+ "name": "width",
+ "label": "",
+ "description": "",
+ "value": 1024
+ },
+ "height": {
+ "name": "height",
+ "label": "",
+ "description": "",
+ "value": 1024
+ },
+ "target_resolution": {
+ "name": "target_resolution",
+ "label": "",
+ "description": "",
+ "value": "720p"
+ },
+ "rounding": {
+ "name": "rounding",
+ "label": "",
+ "description": "",
+ "value": "nearest"
+ }
+ },
+ "isOpen": true,
+ "isIntermediate": true,
+ "useCache": true
+ },
+ "position": {
+ "x": 261.9930762062073,
+ "y": -602.4138188363532
+ }
+ }
+ ],
+ "edges": [
+ {
+ "id": "edge-loader-t5-pos",
+ "type": "default",
+ "source": "1a0e6c7b-9d2f-4b3c-8e1a-2f3d4c5b6a7e",
+ "target": "2b1f7d8c-ae3f-5c4d-9f2b-3a4e5d6c7b8f",
+ "sourceHandle": "wan_t5_encoder",
+ "targetHandle": "wan_t5_encoder"
+ },
+ {
+ "id": "edge-loader-t5-neg",
+ "type": "default",
+ "source": "1a0e6c7b-9d2f-4b3c-8e1a-2f3d4c5b6a7e",
+ "target": "3c2a8e9d-bf4a-6d5e-af3c-4b5f6e7d8c9a",
+ "sourceHandle": "wan_t5_encoder",
+ "targetHandle": "wan_t5_encoder"
+ },
+ {
+ "id": "edge-loader-vae-l2v",
+ "type": "default",
+ "source": "1a0e6c7b-9d2f-4b3c-8e1a-2f3d4c5b6a7e",
+ "target": "6f5dcb1c-e27d-9fb0-d16f-7e8cbaa1fcbd",
+ "sourceHandle": "vae",
+ "targetHandle": "vae"
+ },
+ {
+ "id": "edge-pos-cond-denoise",
+ "type": "default",
+ "source": "2b1f7d8c-ae3f-5c4d-9f2b-3a4e5d6c7b8f",
+ "target": "4d3b9faf-c05b-7e6f-bf4d-5c6a7f8e9dab",
+ "sourceHandle": "conditioning",
+ "targetHandle": "positive_conditioning"
+ },
+ {
+ "id": "edge-neg-cond-denoise",
+ "type": "default",
+ "source": "3c2a8e9d-bf4a-6d5e-af3c-4b5f6e7d8c9a",
+ "target": "4d3b9faf-c05b-7e6f-bf4d-5c6a7f8e9dab",
+ "sourceHandle": "conditioning",
+ "targetHandle": "negative_conditioning"
+ },
+ {
+ "id": "edge-rand-seed-denoise",
+ "type": "default",
+ "source": "5e4cab0b-d16c-8faf-c05e-6d7baf90ebbc",
+ "target": "4d3b9faf-c05b-7e6f-bf4d-5c6a7f8e9dab",
+ "sourceHandle": "value",
+ "targetHandle": "seed"
+ },
+ {
+ "id": "edge-denoise-latents-l2v",
+ "type": "default",
+ "source": "4d3b9faf-c05b-7e6f-bf4d-5c6a7f8e9dab",
+ "target": "6f5dcb1c-e27d-9fb0-d16f-7e8cbaa1fcbd",
+ "sourceHandle": "latents",
+ "targetHandle": "latents"
+ },
+ {
+ "id": "reactflow__edge-1a0e6c7b-9d2f-4b3c-8e1a-2f3d4c5b6a7etransformer-4d3b9faf-c05b-7e6f-bf4d-5c6a7f8e9dabtransformer",
+ "type": "default",
+ "source": "1a0e6c7b-9d2f-4b3c-8e1a-2f3d4c5b6a7e",
+ "target": "4d3b9faf-c05b-7e6f-bf4d-5c6a7f8e9dab",
+ "sourceHandle": "transformer",
+ "targetHandle": "transformer"
+ },
+ {
+ "id": "reactflow__edge-1473db93-45eb-4468-bba1-d0f9d1c21aedref_image-4d3b9faf-c05b-7e6f-bf4d-5c6a7f8e9dabref_image",
+ "type": "default",
+ "source": "1473db93-45eb-4468-bba1-d0f9d1c21aed",
+ "target": "4d3b9faf-c05b-7e6f-bf4d-5c6a7f8e9dab",
+ "sourceHandle": "ref_image",
+ "targetHandle": "ref_image"
+ },
+ {
+ "id": "reactflow__edge-1a0e6c7b-9d2f-4b3c-8e1a-2f3d4c5b6a7evae-1473db93-45eb-4468-bba1-d0f9d1c21aedvae",
+ "type": "default",
+ "source": "1a0e6c7b-9d2f-4b3c-8e1a-2f3d4c5b6a7e",
+ "target": "1473db93-45eb-4468-bba1-d0f9d1c21aed",
+ "sourceHandle": "vae",
+ "targetHandle": "vae"
+ },
+ {
+ "id": "reactflow__edge-0771d3a4-7f0b-45bd-9c73-a99b40e24b7bimage-1473db93-45eb-4468-bba1-d0f9d1c21aedimage",
+ "type": "default",
+ "source": "0771d3a4-7f0b-45bd-9c73-a99b40e24b7b",
+ "target": "1473db93-45eb-4468-bba1-d0f9d1c21aed",
+ "sourceHandle": "image",
+ "targetHandle": "image"
+ },
+ {
+ "id": "reactflow__edge-0771d3a4-7f0b-45bd-9c73-a99b40e24b7bwidth-55cb8b4f-b7fc-4d37-9861-d5c84a57353bwidth",
+ "type": "default",
+ "source": "0771d3a4-7f0b-45bd-9c73-a99b40e24b7b",
+ "target": "55cb8b4f-b7fc-4d37-9861-d5c84a57353b",
+ "sourceHandle": "width",
+ "targetHandle": "width"
+ },
+ {
+ "id": "reactflow__edge-0771d3a4-7f0b-45bd-9c73-a99b40e24b7bheight-55cb8b4f-b7fc-4d37-9861-d5c84a57353bheight",
+ "type": "default",
+ "source": "0771d3a4-7f0b-45bd-9c73-a99b40e24b7b",
+ "target": "55cb8b4f-b7fc-4d37-9861-d5c84a57353b",
+ "sourceHandle": "height",
+ "targetHandle": "height"
+ },
+ {
+ "id": "reactflow__edge-55cb8b4f-b7fc-4d37-9861-d5c84a57353bwidth-1473db93-45eb-4468-bba1-d0f9d1c21aedwidth",
+ "type": "default",
+ "source": "55cb8b4f-b7fc-4d37-9861-d5c84a57353b",
+ "target": "1473db93-45eb-4468-bba1-d0f9d1c21aed",
+ "sourceHandle": "width",
+ "targetHandle": "width"
+ },
+ {
+ "id": "reactflow__edge-55cb8b4f-b7fc-4d37-9861-d5c84a57353bheight-1473db93-45eb-4468-bba1-d0f9d1c21aedheight",
+ "type": "default",
+ "source": "55cb8b4f-b7fc-4d37-9861-d5c84a57353b",
+ "target": "1473db93-45eb-4468-bba1-d0f9d1c21aed",
+ "sourceHandle": "height",
+ "targetHandle": "height"
+ },
+ {
+ "id": "reactflow__edge-55cb8b4f-b7fc-4d37-9861-d5c84a57353bwidth-4d3b9faf-c05b-7e6f-bf4d-5c6a7f8e9dabwidth",
+ "type": "default",
+ "source": "55cb8b4f-b7fc-4d37-9861-d5c84a57353b",
+ "target": "4d3b9faf-c05b-7e6f-bf4d-5c6a7f8e9dab",
+ "sourceHandle": "width",
+ "targetHandle": "width"
+ },
+ {
+ "id": "reactflow__edge-55cb8b4f-b7fc-4d37-9861-d5c84a57353bheight-4d3b9faf-c05b-7e6f-bf4d-5c6a7f8e9dabheight",
+ "type": "default",
+ "source": "55cb8b4f-b7fc-4d37-9861-d5c84a57353b",
+ "target": "4d3b9faf-c05b-7e6f-bf4d-5c6a7f8e9dab",
+ "sourceHandle": "height",
+ "targetHandle": "height"
+ }
+ ],
+ "form": {
+ "elements": {
+ "root": {
+ "id": "root",
+ "data": {
+ "layout": "column",
+ "children": [
+ "heading-XDPaQofdEE",
+ "node-field-KcT0glAFDK",
+ "node-field-e3hxuqgTgS",
+ "container-mUeBcw2tYf",
+ "heading-wuPtY8B4vR",
+ "container-AkpUH1WtID",
+ "heading-10PuWMKJg9",
+ "container-C0z6ru33to"
+ ]
+ },
+ "type": "container"
+ },
+ "node-field-zQA9XuRZRW": {
+ "id": "node-field-zQA9XuRZRW",
+ "parentId": "container-mUeBcw2tYf",
+ "data": {
+ "fieldIdentifier": {
+ "nodeId": "6f5dcb1c-e27d-9fb0-d16f-7e8cbaa1fcbd",
+ "fieldName": "fps"
+ },
+ "showDescription": false,
+ "settings": {
+ "type": "integer-field-config",
+ "showShuffle": false,
+ "component": "number-input"
+ }
+ },
+ "type": "node-field"
+ },
+ "node-field-ok9GStueKC": {
+ "id": "node-field-ok9GStueKC",
+ "parentId": "container-mUeBcw2tYf",
+ "data": {
+ "fieldIdentifier": {
+ "nodeId": "4d3b9faf-c05b-7e6f-bf4d-5c6a7f8e9dab",
+ "fieldName": "num_frames"
+ },
+ "showDescription": false,
+ "settings": {
+ "type": "integer-field-config",
+ "showShuffle": false,
+ "component": "number-input"
+ }
+ },
+ "type": "node-field"
+ },
+ "node-field-PdU0Sz2m5z": {
+ "id": "node-field-PdU0Sz2m5z",
+ "parentId": "container-y0w7TfJC7e",
+ "data": {
+ "fieldIdentifier": {
+ "nodeId": "4d3b9faf-c05b-7e6f-bf4d-5c6a7f8e9dab",
+ "fieldName": "guidance_scale"
+ },
+ "showDescription": false,
+ "settings": {
+ "type": "float-field-config",
+ "showShuffle": false,
+ "component": "number-input"
+ }
+ },
+ "type": "node-field"
+ },
+ "node-field-UiQ7MaseDR": {
+ "id": "node-field-UiQ7MaseDR",
+ "parentId": "container-mUeBcw2tYf",
+ "data": {
+ "fieldIdentifier": {
+ "nodeId": "4d3b9faf-c05b-7e6f-bf4d-5c6a7f8e9dab",
+ "fieldName": "steps"
+ },
+ "showDescription": false,
+ "settings": {
+ "type": "integer-field-config",
+ "showShuffle": false,
+ "component": "number-input"
+ }
+ },
+ "type": "node-field"
+ },
+ "node-field-KcT0glAFDK": {
+ "id": "node-field-KcT0glAFDK",
+ "parentId": "root",
+ "data": {
+ "fieldIdentifier": {
+ "nodeId": "2b1f7d8c-ae3f-5c4d-9f2b-3a4e5d6c7b8f",
+ "fieldName": "prompt"
+ },
+ "showDescription": false,
+ "settings": {
+ "type": "string-field-config",
+ "component": "textarea"
+ }
+ },
+ "type": "node-field"
+ },
+ "node-field-N9mexEKkYg": {
+ "id": "node-field-N9mexEKkYg",
+ "parentId": "container-C0z6ru33to",
+ "data": {
+ "fieldIdentifier": {
+ "nodeId": "1a0e6c7b-9d2f-4b3c-8e1a-2f3d4c5b6a7e",
+ "fieldName": "wan_t5_encoder_model"
+ },
+ "showDescription": false
+ },
+ "type": "node-field"
+ },
+ "node-field-2u9lAbMBnD": {
+ "id": "node-field-2u9lAbMBnD",
+ "parentId": "container-C0z6ru33to",
+ "data": {
+ "fieldIdentifier": {
+ "nodeId": "1a0e6c7b-9d2f-4b3c-8e1a-2f3d4c5b6a7e",
+ "fieldName": "vae_model"
+ },
+ "showDescription": false
+ },
+ "type": "node-field"
+ },
+ "node-field-CTGqrO3x8o": {
+ "id": "node-field-CTGqrO3x8o",
+ "parentId": "container-C0z6ru33to",
+ "data": {
+ "fieldIdentifier": {
+ "nodeId": "1a0e6c7b-9d2f-4b3c-8e1a-2f3d4c5b6a7e",
+ "fieldName": "component_source"
+ },
+ "showDescription": false
+ },
+ "type": "node-field"
+ },
+ "node-field-Q345WyNzc4": {
+ "id": "node-field-Q345WyNzc4",
+ "parentId": "container-y0w7TfJC7e",
+ "data": {
+ "fieldIdentifier": {
+ "nodeId": "1a0e6c7b-9d2f-4b3c-8e1a-2f3d4c5b6a7e",
+ "fieldName": "model"
+ },
+ "showDescription": false
+ },
+ "type": "node-field"
+ },
+ "container-mUeBcw2tYf": {
+ "id": "container-mUeBcw2tYf",
+ "parentId": "root",
+ "data": {
+ "layout": "row",
+ "children": [
+ "node-field-UiQ7MaseDR",
+ "node-field-ok9GStueKC",
+ "node-field-zQA9XuRZRW"
+ ]
+ },
+ "type": "container"
+ },
+ "heading-XDPaQofdEE": {
+ "id": "heading-XDPaQofdEE",
+ "parentId": "root",
+ "data": {
+ "content": "Generation"
+ },
+ "type": "heading"
+ },
+ "heading-wuPtY8B4vR": {
+ "id": "heading-wuPtY8B4vR",
+ "parentId": "root",
+ "data": {
+ "content": "Model"
+ },
+ "type": "heading"
+ },
+ "container-AkpUH1WtID": {
+ "id": "container-AkpUH1WtID",
+ "parentId": "root",
+ "data": {
+ "layout": "row",
+ "children": [
+ "container-y0w7TfJC7e"
+ ]
+ },
+ "type": "container"
+ },
+ "container-y0w7TfJC7e": {
+ "id": "container-y0w7TfJC7e",
+ "parentId": "container-AkpUH1WtID",
+ "data": {
+ "layout": "column",
+ "children": [
+ "node-field-Q345WyNzc4",
+ "node-field-PdU0Sz2m5z"
+ ]
+ },
+ "type": "container"
+ },
+ "container-C0z6ru33to": {
+ "id": "container-C0z6ru33to",
+ "parentId": "root",
+ "data": {
+ "layout": "row",
+ "children": [
+ "node-field-N9mexEKkYg",
+ "node-field-2u9lAbMBnD",
+ "node-field-CTGqrO3x8o"
+ ]
+ },
+ "type": "container"
+ },
+ "heading-10PuWMKJg9": {
+ "id": "heading-10PuWMKJg9",
+ "parentId": "root",
+ "data": {
+ "content": "Advanced"
+ },
+ "type": "heading"
+ },
+ "node-field-e3hxuqgTgS": {
+ "id": "node-field-e3hxuqgTgS",
+ "parentId": "root",
+ "data": {
+ "fieldIdentifier": {
+ "nodeId": "0771d3a4-7f0b-45bd-9c73-a99b40e24b7b",
+ "fieldName": "image"
+ },
+ "showDescription": true
+ },
+ "type": "node-field"
+ }
+ },
+ "rootElementId": "root"
+ },
+ "id": "default_wan22_i2v_ti2v5b"
+}
diff --git a/invokeai/app/services/workflow_records/default_workflows/Interpolate 2 Images to Video - Wan 2.2 Lightning.json b/invokeai/app/services/workflow_records/default_workflows/Interpolate 2 Images to Video - Wan 2.2 Lightning.json
new file mode 100644
index 00000000000..8a1515ee6d0
--- /dev/null
+++ b/invokeai/app/services/workflow_records/default_workflows/Interpolate 2 Images to Video - Wan 2.2 Lightning.json
@@ -0,0 +1,1354 @@
+{
+ "name": "Interpolate 2 Images to Video - Wan 2.2 Lightning",
+ "author": "InvokeAI",
+ "description": "Interpolate a video between two images with Wan 2.2 I2V A14B (first-last-frame / FLF2V). Provide a starting image and an ending image; the model generates a clip that begins on the first image and animates smoothly to the second. Uses the Lightning LoRA pair for fast 4-step generation. The chosen resolution is applied to the shorter dimension and the other dimension is scaled to preserve aspect ratio.",
+ "version": "1.0.0",
+ "contact": "",
+ "tags": "wan2.2, image to video, video, lightning, lora",
+ "notes": "Each InvokeAI install assigns its own internal model IDs, so the model fields are left blank -- select these in the form after loading:\n\nMain Model node:\n- Main Model: Wan 2.2 I2V A14B High Noise (Q4_K_M GGUF recommended for <=16 GB VRAM)\n- Transformer (Low Noise): Wan 2.2 I2V A14B Low Noise (Q4_K_M)\n- VAE: Wan 2.2 A14B VAE\n- T5 Encoder: Wan T5 Encoder (UMT5-XXL)\n\nLightning LoRAs (4-step distillation, I2V variant):\n- Apply Lightning LoRA (High Noise): Wan 2.2 I2V Lightning High Noise (4-step)\n- Apply Lightning LoRA (Low Noise): Wan 2.2 I2V Lightning Low Noise (4-step)\n\nProvide BOTH a start image and an end image. VRAM: for <=16 GB, keep the resolution at 480p; 720p and 1080p need more VRAM.",
+ "exposedFields": [
+ {
+ "nodeId": "1a0e6c7b-9d2f-4b3c-8e1a-2f3d4c5b6a7e",
+ "fieldName": "model"
+ },
+ {
+ "nodeId": "1a0e6c7b-9d2f-4b3c-8e1a-2f3d4c5b6a7e",
+ "fieldName": "transformer_low_noise_model"
+ },
+ {
+ "nodeId": "1a0e6c7b-9d2f-4b3c-8e1a-2f3d4c5b6a7e",
+ "fieldName": "component_source"
+ },
+ {
+ "nodeId": "1a0e6c7b-9d2f-4b3c-8e1a-2f3d4c5b6a7e",
+ "fieldName": "vae_model"
+ },
+ {
+ "nodeId": "1a0e6c7b-9d2f-4b3c-8e1a-2f3d4c5b6a7e",
+ "fieldName": "wan_t5_encoder_model"
+ },
+ {
+ "nodeId": "3466ad11-a931-4adc-b394-9ec287f0c23b",
+ "fieldName": "lora"
+ },
+ {
+ "nodeId": "3466ad11-a931-4adc-b394-9ec287f0c23b",
+ "fieldName": "weight"
+ },
+ {
+ "nodeId": "3466ad11-a931-4adc-b394-9ec287f0c23b",
+ "fieldName": "target"
+ },
+ {
+ "nodeId": "9b4cde65-699f-42c1-8889-76ac91fcb62f",
+ "fieldName": "lora"
+ },
+ {
+ "nodeId": "9b4cde65-699f-42c1-8889-76ac91fcb62f",
+ "fieldName": "weight"
+ },
+ {
+ "nodeId": "9b4cde65-699f-42c1-8889-76ac91fcb62f",
+ "fieldName": "target"
+ },
+ {
+ "nodeId": "2b1f7d8c-ae3f-5c4d-9f2b-3a4e5d6c7b8f",
+ "fieldName": "prompt"
+ },
+ {
+ "nodeId": "3c2a8e9d-bf4a-6d5e-af3c-4b5f6e7d8c9a",
+ "fieldName": "prompt"
+ },
+ {
+ "nodeId": "4d3b9faf-c05b-7e6f-bf4d-5c6a7f8e9dab",
+ "fieldName": "steps"
+ },
+ {
+ "nodeId": "4d3b9faf-c05b-7e6f-bf4d-5c6a7f8e9dab",
+ "fieldName": "guidance_scale"
+ },
+ {
+ "nodeId": "4d3b9faf-c05b-7e6f-bf4d-5c6a7f8e9dab",
+ "fieldName": "guidance_scale_low_noise"
+ },
+ {
+ "nodeId": "4d3b9faf-c05b-7e6f-bf4d-5c6a7f8e9dab",
+ "fieldName": "width"
+ },
+ {
+ "nodeId": "4d3b9faf-c05b-7e6f-bf4d-5c6a7f8e9dab",
+ "fieldName": "height"
+ },
+ {
+ "nodeId": "4d3b9faf-c05b-7e6f-bf4d-5c6a7f8e9dab",
+ "fieldName": "num_frames"
+ },
+ {
+ "nodeId": "6f5dcb1c-e27d-9fb0-d16f-7e8cbaa1fcbd",
+ "fieldName": "fps"
+ }
+ ],
+ "meta": {
+ "category": "default",
+ "version": "4.0.0"
+ },
+ "id": "default_wan22_interpolate_lightning",
+ "form": {
+ "elements": {
+ "root": {
+ "id": "root",
+ "data": {
+ "layout": "column",
+ "children": [
+ "heading-XDPaQofdEE",
+ "node-field-KcT0glAFDK",
+ "container-i8V1oJeQCP",
+ "node-field-DonE1WRSA6",
+ "container-mUeBcw2tYf",
+ "divider-2T3tRpIIJO",
+ "heading-wuPtY8B4vR",
+ "container-AkpUH1WtID",
+ "heading-TwsoE3wQi9",
+ "container-0u7wyxEdNa",
+ "divider-FKc8lld0Eb",
+ "heading-10PuWMKJg9",
+ "container-C0z6ru33to"
+ ]
+ },
+ "type": "container"
+ },
+ "node-field-zQA9XuRZRW": {
+ "id": "node-field-zQA9XuRZRW",
+ "parentId": "container-mUeBcw2tYf",
+ "data": {
+ "fieldIdentifier": {
+ "nodeId": "6f5dcb1c-e27d-9fb0-d16f-7e8cbaa1fcbd",
+ "fieldName": "fps"
+ },
+ "showDescription": false,
+ "settings": {
+ "type": "integer-field-config",
+ "showShuffle": false,
+ "component": "number-input"
+ }
+ },
+ "type": "node-field"
+ },
+ "node-field-QDqQ4XOKvg": {
+ "id": "node-field-QDqQ4XOKvg",
+ "parentId": "container-7g1Xedgg8T",
+ "data": {
+ "fieldIdentifier": {
+ "nodeId": "4d3b9faf-c05b-7e6f-bf4d-5c6a7f8e9dab",
+ "fieldName": "guidance_scale_low_noise"
+ },
+ "showDescription": false,
+ "settings": {
+ "type": "float-field-config",
+ "showShuffle": false,
+ "component": "number-input"
+ }
+ },
+ "type": "node-field"
+ },
+ "node-field-PdU0Sz2m5z": {
+ "id": "node-field-PdU0Sz2m5z",
+ "parentId": "container-y0w7TfJC7e",
+ "data": {
+ "fieldIdentifier": {
+ "nodeId": "4d3b9faf-c05b-7e6f-bf4d-5c6a7f8e9dab",
+ "fieldName": "guidance_scale"
+ },
+ "showDescription": false,
+ "settings": {
+ "type": "float-field-config",
+ "showShuffle": false,
+ "component": "number-input"
+ }
+ },
+ "type": "node-field"
+ },
+ "node-field-UiQ7MaseDR": {
+ "id": "node-field-UiQ7MaseDR",
+ "parentId": "container-mUeBcw2tYf",
+ "data": {
+ "fieldIdentifier": {
+ "nodeId": "4d3b9faf-c05b-7e6f-bf4d-5c6a7f8e9dab",
+ "fieldName": "steps"
+ },
+ "showDescription": false,
+ "settings": {
+ "type": "integer-field-config",
+ "showShuffle": false,
+ "component": "number-input"
+ }
+ },
+ "type": "node-field"
+ },
+ "node-field-KcT0glAFDK": {
+ "id": "node-field-KcT0glAFDK",
+ "parentId": "root",
+ "data": {
+ "fieldIdentifier": {
+ "nodeId": "2b1f7d8c-ae3f-5c4d-9f2b-3a4e5d6c7b8f",
+ "fieldName": "prompt"
+ },
+ "showDescription": false,
+ "settings": {
+ "type": "string-field-config",
+ "component": "textarea"
+ }
+ },
+ "type": "node-field"
+ },
+ "node-field-N9mexEKkYg": {
+ "id": "node-field-N9mexEKkYg",
+ "parentId": "container-C0z6ru33to",
+ "data": {
+ "fieldIdentifier": {
+ "nodeId": "1a0e6c7b-9d2f-4b3c-8e1a-2f3d4c5b6a7e",
+ "fieldName": "wan_t5_encoder_model"
+ },
+ "showDescription": false
+ },
+ "type": "node-field"
+ },
+ "node-field-2u9lAbMBnD": {
+ "id": "node-field-2u9lAbMBnD",
+ "parentId": "container-C0z6ru33to",
+ "data": {
+ "fieldIdentifier": {
+ "nodeId": "1a0e6c7b-9d2f-4b3c-8e1a-2f3d4c5b6a7e",
+ "fieldName": "vae_model"
+ },
+ "showDescription": false
+ },
+ "type": "node-field"
+ },
+ "node-field-CTGqrO3x8o": {
+ "id": "node-field-CTGqrO3x8o",
+ "parentId": "container-C0z6ru33to",
+ "data": {
+ "fieldIdentifier": {
+ "nodeId": "1a0e6c7b-9d2f-4b3c-8e1a-2f3d4c5b6a7e",
+ "fieldName": "component_source"
+ },
+ "showDescription": false
+ },
+ "type": "node-field"
+ },
+ "node-field-7G9srQT2Uw": {
+ "id": "node-field-7G9srQT2Uw",
+ "parentId": "container-7g1Xedgg8T",
+ "data": {
+ "fieldIdentifier": {
+ "nodeId": "1a0e6c7b-9d2f-4b3c-8e1a-2f3d4c5b6a7e",
+ "fieldName": "transformer_low_noise_model"
+ },
+ "showDescription": false
+ },
+ "type": "node-field"
+ },
+ "node-field-Q345WyNzc4": {
+ "id": "node-field-Q345WyNzc4",
+ "parentId": "container-y0w7TfJC7e",
+ "data": {
+ "fieldIdentifier": {
+ "nodeId": "1a0e6c7b-9d2f-4b3c-8e1a-2f3d4c5b6a7e",
+ "fieldName": "model"
+ },
+ "showDescription": false
+ },
+ "type": "node-field"
+ },
+ "container-mUeBcw2tYf": {
+ "id": "container-mUeBcw2tYf",
+ "parentId": "root",
+ "data": {
+ "layout": "row",
+ "children": [
+ "node-field-UiQ7MaseDR",
+ "node-field-FiH14mitcM",
+ "node-field-zQA9XuRZRW"
+ ]
+ },
+ "type": "container"
+ },
+ "heading-XDPaQofdEE": {
+ "id": "heading-XDPaQofdEE",
+ "parentId": "root",
+ "data": {
+ "content": "Generation"
+ },
+ "type": "heading"
+ },
+ "container-i8V1oJeQCP": {
+ "id": "container-i8V1oJeQCP",
+ "parentId": "root",
+ "data": {
+ "layout": "row",
+ "children": [
+ "node-field-dKizHbAltw",
+ "node-field-yH0i6aSLQ4"
+ ]
+ },
+ "type": "container"
+ },
+ "heading-wuPtY8B4vR": {
+ "id": "heading-wuPtY8B4vR",
+ "parentId": "root",
+ "data": {
+ "content": "Model"
+ },
+ "type": "heading"
+ },
+ "container-AkpUH1WtID": {
+ "id": "container-AkpUH1WtID",
+ "parentId": "root",
+ "data": {
+ "layout": "row",
+ "children": [
+ "container-y0w7TfJC7e",
+ "container-7g1Xedgg8T"
+ ]
+ },
+ "type": "container"
+ },
+ "container-y0w7TfJC7e": {
+ "id": "container-y0w7TfJC7e",
+ "parentId": "container-AkpUH1WtID",
+ "data": {
+ "layout": "column",
+ "children": [
+ "node-field-Q345WyNzc4",
+ "node-field-PdU0Sz2m5z"
+ ]
+ },
+ "type": "container"
+ },
+ "container-7g1Xedgg8T": {
+ "id": "container-7g1Xedgg8T",
+ "parentId": "container-AkpUH1WtID",
+ "data": {
+ "layout": "column",
+ "children": [
+ "node-field-7G9srQT2Uw",
+ "node-field-QDqQ4XOKvg"
+ ]
+ },
+ "type": "container"
+ },
+ "container-C0z6ru33to": {
+ "id": "container-C0z6ru33to",
+ "parentId": "root",
+ "data": {
+ "layout": "row",
+ "children": [
+ "node-field-N9mexEKkYg",
+ "node-field-2u9lAbMBnD",
+ "node-field-CTGqrO3x8o"
+ ]
+ },
+ "type": "container"
+ },
+ "heading-10PuWMKJg9": {
+ "id": "heading-10PuWMKJg9",
+ "parentId": "root",
+ "data": {
+ "content": "Advanced"
+ },
+ "type": "heading"
+ },
+ "container-0u7wyxEdNa": {
+ "id": "container-0u7wyxEdNa",
+ "parentId": "root",
+ "data": {
+ "layout": "row",
+ "children": [
+ "container-Um9gcFv0lo",
+ "container-4owWd0thWk"
+ ]
+ },
+ "type": "container"
+ },
+ "container-Um9gcFv0lo": {
+ "id": "container-Um9gcFv0lo",
+ "parentId": "container-0u7wyxEdNa",
+ "data": {
+ "layout": "row",
+ "children": [
+ "container-lF8o60KuVb"
+ ]
+ },
+ "type": "container"
+ },
+ "container-4owWd0thWk": {
+ "id": "container-4owWd0thWk",
+ "parentId": "container-0u7wyxEdNa",
+ "data": {
+ "layout": "row",
+ "children": [
+ "container-rvE0oy63qL"
+ ]
+ },
+ "type": "container"
+ },
+ "node-field-a8nZxgz9u9": {
+ "id": "node-field-a8nZxgz9u9",
+ "parentId": "container-lF8o60KuVb",
+ "data": {
+ "fieldIdentifier": {
+ "nodeId": "291abec8-b996-4c7b-959c-360f56a3ed60",
+ "fieldName": "lora"
+ },
+ "showDescription": true
+ },
+ "type": "node-field"
+ },
+ "node-field-NwR9yOugEz": {
+ "id": "node-field-NwR9yOugEz",
+ "parentId": "container-rvE0oy63qL",
+ "data": {
+ "fieldIdentifier": {
+ "nodeId": "ae8f115d-a3b8-4f76-b280-7313366a94ad",
+ "fieldName": "lora"
+ },
+ "showDescription": true
+ },
+ "type": "node-field"
+ },
+ "container-lF8o60KuVb": {
+ "id": "container-lF8o60KuVb",
+ "parentId": "container-Um9gcFv0lo",
+ "data": {
+ "layout": "column",
+ "children": [
+ "node-field-a8nZxgz9u9",
+ "node-field-0al3LtpcGU"
+ ]
+ },
+ "type": "container"
+ },
+ "container-rvE0oy63qL": {
+ "id": "container-rvE0oy63qL",
+ "parentId": "container-4owWd0thWk",
+ "data": {
+ "layout": "column",
+ "children": [
+ "node-field-NwR9yOugEz",
+ "node-field-lgLAjS4nks"
+ ]
+ },
+ "type": "container"
+ },
+ "node-field-0al3LtpcGU": {
+ "id": "node-field-0al3LtpcGU",
+ "parentId": "container-lF8o60KuVb",
+ "data": {
+ "fieldIdentifier": {
+ "nodeId": "291abec8-b996-4c7b-959c-360f56a3ed60",
+ "fieldName": "weight"
+ },
+ "showDescription": true,
+ "settings": {
+ "type": "float-field-config",
+ "showShuffle": false,
+ "component": "number-input"
+ }
+ },
+ "type": "node-field"
+ },
+ "node-field-lgLAjS4nks": {
+ "id": "node-field-lgLAjS4nks",
+ "parentId": "container-rvE0oy63qL",
+ "data": {
+ "fieldIdentifier": {
+ "nodeId": "ae8f115d-a3b8-4f76-b280-7313366a94ad",
+ "fieldName": "weight"
+ },
+ "showDescription": true,
+ "settings": {
+ "type": "float-field-config",
+ "showShuffle": false,
+ "component": "number-input"
+ }
+ },
+ "type": "node-field"
+ },
+ "heading-TwsoE3wQi9": {
+ "id": "heading-TwsoE3wQi9",
+ "parentId": "root",
+ "data": {
+ "content": "Lightning LoRAs"
+ },
+ "type": "heading"
+ },
+ "node-field-dKizHbAltw": {
+ "id": "node-field-dKizHbAltw",
+ "parentId": "container-i8V1oJeQCP",
+ "data": {
+ "fieldIdentifier": {
+ "nodeId": "738ec1dd-416e-471d-acf0-ca324acd0954",
+ "fieldName": "image"
+ },
+ "showDescription": true
+ },
+ "type": "node-field"
+ },
+ "node-field-DonE1WRSA6": {
+ "id": "node-field-DonE1WRSA6",
+ "parentId": "root",
+ "data": {
+ "fieldIdentifier": {
+ "nodeId": "d27f29bf-6f26-4088-b1cc-c722ecd9fd61",
+ "fieldName": "target_resolution"
+ },
+ "showDescription": true
+ },
+ "type": "node-field"
+ },
+ "divider-2T3tRpIIJO": {
+ "id": "divider-2T3tRpIIJO",
+ "parentId": "root",
+ "type": "divider"
+ },
+ "divider-FKc8lld0Eb": {
+ "id": "divider-FKc8lld0Eb",
+ "parentId": "root",
+ "type": "divider"
+ },
+ "node-field-FiH14mitcM": {
+ "id": "node-field-FiH14mitcM",
+ "parentId": "container-mUeBcw2tYf",
+ "data": {
+ "fieldIdentifier": {
+ "nodeId": "c857e3e8-334f-4a6e-9013-3326316e4281",
+ "fieldName": "value"
+ },
+ "showDescription": false,
+ "settings": {
+ "type": "integer-field-config",
+ "showShuffle": false,
+ "component": "number-input"
+ }
+ },
+ "type": "node-field"
+ },
+ "node-field-yH0i6aSLQ4": {
+ "id": "node-field-yH0i6aSLQ4",
+ "parentId": "container-i8V1oJeQCP",
+ "data": {
+ "fieldIdentifier": {
+ "nodeId": "c845a30f-7d6a-4351-9dcd-ea3df4ebdc6e",
+ "fieldName": "image"
+ },
+ "showDescription": true
+ },
+ "type": "node-field"
+ }
+ },
+ "rootElementId": "root"
+ },
+ "nodes": [
+ {
+ "id": "1a0e6c7b-9d2f-4b3c-8e1a-2f3d4c5b6a7e",
+ "type": "invocation",
+ "data": {
+ "id": "1a0e6c7b-9d2f-4b3c-8e1a-2f3d4c5b6a7e",
+ "version": "1.0.1",
+ "nodePack": "invokeai",
+ "label": "",
+ "notes": "",
+ "type": "wan_model_loader",
+ "inputs": {
+ "model": {
+ "name": "model",
+ "label": "Transformer (High Noise)",
+ "description": "",
+ "value": null
+ },
+ "transformer_low_noise_model": {
+ "name": "transformer_low_noise_model",
+ "label": "",
+ "description": "",
+ "value": null
+ },
+ "vae_model": {
+ "name": "vae_model",
+ "label": "",
+ "description": "",
+ "value": null
+ },
+ "wan_t5_encoder_model": {
+ "name": "wan_t5_encoder_model",
+ "label": "",
+ "description": "",
+ "value": null
+ },
+ "component_source": {
+ "name": "component_source",
+ "label": "",
+ "description": ""
+ }
+ },
+ "isOpen": true,
+ "isIntermediate": true,
+ "useCache": false
+ },
+ "position": {
+ "x": 0,
+ "y": 0
+ }
+ },
+ {
+ "id": "2b1f7d8c-ae3f-5c4d-9f2b-3a4e5d6c7b8f",
+ "type": "invocation",
+ "data": {
+ "id": "2b1f7d8c-ae3f-5c4d-9f2b-3a4e5d6c7b8f",
+ "version": "1.0.0",
+ "nodePack": "invokeai",
+ "label": "Positive Prompt",
+ "notes": "",
+ "type": "wan_text_encoder",
+ "inputs": {
+ "prompt": {
+ "name": "prompt",
+ "label": "",
+ "description": "",
+ "value": ""
+ },
+ "wan_t5_encoder": {
+ "name": "wan_t5_encoder",
+ "label": "",
+ "description": ""
+ }
+ },
+ "isOpen": true,
+ "isIntermediate": true,
+ "useCache": true
+ },
+ "position": {
+ "x": 700,
+ "y": 212
+ }
+ },
+ {
+ "id": "3c2a8e9d-bf4a-6d5e-af3c-4b5f6e7d8c9a",
+ "type": "invocation",
+ "data": {
+ "id": "3c2a8e9d-bf4a-6d5e-af3c-4b5f6e7d8c9a",
+ "version": "1.0.0",
+ "nodePack": "invokeai",
+ "label": "Negative Prompt (unused at CFG=1.0)",
+ "notes": "",
+ "type": "wan_text_encoder",
+ "inputs": {
+ "prompt": {
+ "name": "prompt",
+ "label": "",
+ "description": "",
+ "value": " "
+ },
+ "wan_t5_encoder": {
+ "name": "wan_t5_encoder",
+ "label": "",
+ "description": ""
+ }
+ },
+ "isOpen": true,
+ "isIntermediate": true,
+ "useCache": true
+ },
+ "position": {
+ "x": 700,
+ "y": 392
+ }
+ },
+ {
+ "id": "5e4cab0b-d16c-8faf-c05e-6d7baf90ebbc",
+ "type": "invocation",
+ "data": {
+ "id": "5e4cab0b-d16c-8faf-c05e-6d7baf90ebbc",
+ "version": "1.0.1",
+ "nodePack": "invokeai",
+ "label": "",
+ "notes": "",
+ "type": "rand_int",
+ "inputs": {
+ "low": {
+ "name": "low",
+ "label": "",
+ "description": "",
+ "value": 0
+ },
+ "high": {
+ "name": "high",
+ "label": "",
+ "description": "",
+ "value": 2147483647
+ }
+ },
+ "isOpen": true,
+ "isIntermediate": true,
+ "useCache": false
+ },
+ "position": {
+ "x": 700,
+ "y": 572
+ }
+ },
+ {
+ "id": "4d3b9faf-c05b-7e6f-bf4d-5c6a7f8e9dab",
+ "type": "invocation",
+ "data": {
+ "id": "4d3b9faf-c05b-7e6f-bf4d-5c6a7f8e9dab",
+ "version": "1.0.0",
+ "nodePack": "invokeai",
+ "label": "",
+ "notes": "",
+ "type": "wan_video_denoise",
+ "inputs": {
+ "transformer": {
+ "name": "transformer",
+ "label": "",
+ "description": ""
+ },
+ "positive_conditioning": {
+ "name": "positive_conditioning",
+ "label": "",
+ "description": ""
+ },
+ "negative_conditioning": {
+ "name": "negative_conditioning",
+ "label": "",
+ "description": ""
+ },
+ "ref_image": {
+ "name": "ref_image",
+ "label": "",
+ "description": ""
+ },
+ "guidance_scale": {
+ "name": "guidance_scale",
+ "label": "CFG (High Noise)",
+ "description": "",
+ "value": 1
+ },
+ "guidance_scale_low_noise": {
+ "name": "guidance_scale_low_noise",
+ "label": "CFG (Low Noise)",
+ "description": "",
+ "value": 1
+ },
+ "width": {
+ "name": "width",
+ "label": "",
+ "description": "",
+ "value": 832
+ },
+ "height": {
+ "name": "height",
+ "label": "",
+ "description": "",
+ "value": 480
+ },
+ "num_frames": {
+ "name": "num_frames",
+ "label": "Frames",
+ "description": "",
+ "value": 81
+ },
+ "steps": {
+ "name": "steps",
+ "label": "",
+ "description": "",
+ "value": 4
+ },
+ "seed": {
+ "name": "seed",
+ "label": "",
+ "description": "",
+ "value": 0
+ }
+ },
+ "isOpen": true,
+ "isIntermediate": true,
+ "useCache": true
+ },
+ "position": {
+ "x": 1160.9641121313055,
+ "y": 461.16328373638504
+ }
+ },
+ {
+ "id": "6f5dcb1c-e27d-9fb0-d16f-7e8cbaa1fcbd",
+ "type": "invocation",
+ "data": {
+ "id": "6f5dcb1c-e27d-9fb0-d16f-7e8cbaa1fcbd",
+ "version": "1.0.0",
+ "nodePack": "invokeai",
+ "label": "",
+ "notes": "",
+ "type": "wan_l2v",
+ "inputs": {
+ "board": {
+ "name": "board",
+ "label": "",
+ "description": ""
+ },
+ "metadata": {
+ "name": "metadata",
+ "label": "",
+ "description": ""
+ },
+ "latents": {
+ "name": "latents",
+ "label": "",
+ "description": ""
+ },
+ "vae": {
+ "name": "vae",
+ "label": "",
+ "description": ""
+ },
+ "fps": {
+ "name": "fps",
+ "label": "FPS",
+ "description": "",
+ "value": 16
+ }
+ },
+ "isOpen": true,
+ "isIntermediate": false,
+ "useCache": true
+ },
+ "position": {
+ "x": 1400,
+ "y": 33
+ }
+ },
+ {
+ "id": "291abec8-b996-4c7b-959c-360f56a3ed60",
+ "type": "invocation",
+ "data": {
+ "id": "291abec8-b996-4c7b-959c-360f56a3ed60",
+ "version": "1.0.1",
+ "nodePack": "invokeai",
+ "label": "Apply Lightning LoRA (High Noise)",
+ "notes": "",
+ "type": "wan_lora_loader",
+ "inputs": {
+ "lora": {
+ "name": "lora",
+ "label": "Lightning LoRA (High Noise)",
+ "description": "",
+ "value": null
+ },
+ "weight": {
+ "name": "weight",
+ "label": "",
+ "description": "",
+ "value": 1
+ },
+ "target": {
+ "name": "target",
+ "label": "",
+ "description": "",
+ "value": "high"
+ },
+ "transformer": {
+ "name": "transformer",
+ "label": "",
+ "description": ""
+ }
+ },
+ "isOpen": true,
+ "isIntermediate": true,
+ "useCache": true
+ },
+ "position": {
+ "x": 350,
+ "y": 772
+ }
+ },
+ {
+ "id": "ae8f115d-a3b8-4f76-b280-7313366a94ad",
+ "type": "invocation",
+ "data": {
+ "id": "ae8f115d-a3b8-4f76-b280-7313366a94ad",
+ "version": "1.0.1",
+ "nodePack": "invokeai",
+ "label": "",
+ "notes": "",
+ "type": "wan_lora_loader",
+ "inputs": {
+ "lora": {
+ "name": "lora",
+ "label": "Lightning LoRA (Low Noise)",
+ "description": "",
+ "value": null
+ },
+ "weight": {
+ "name": "weight",
+ "label": "",
+ "description": "",
+ "value": 1
+ },
+ "target": {
+ "name": "target",
+ "label": "",
+ "description": "",
+ "value": "low"
+ },
+ "transformer": {
+ "name": "transformer",
+ "label": "",
+ "description": ""
+ }
+ },
+ "isOpen": true,
+ "isIntermediate": true,
+ "useCache": true
+ },
+ "position": {
+ "x": 700,
+ "y": 772
+ }
+ },
+ {
+ "id": "d27f29bf-6f26-4088-b1cc-c722ecd9fd61",
+ "type": "invocation",
+ "data": {
+ "id": "d27f29bf-6f26-4088-b1cc-c722ecd9fd61",
+ "version": "1.1.0",
+ "nodePack": "invokeai",
+ "label": "",
+ "notes": "",
+ "type": "wan_i2v_ideal_dimensions",
+ "inputs": {
+ "width": {
+ "name": "width",
+ "label": "",
+ "description": "",
+ "value": 1024
+ },
+ "height": {
+ "name": "height",
+ "label": "",
+ "description": "",
+ "value": 1024
+ },
+ "target_resolution": {
+ "name": "target_resolution",
+ "label": "Video Resolution",
+ "description": "",
+ "value": "480p"
+ },
+ "rounding": {
+ "name": "rounding",
+ "label": "",
+ "description": "",
+ "value": "nearest"
+ }
+ },
+ "isOpen": true,
+ "isIntermediate": true,
+ "useCache": true
+ },
+ "position": {
+ "x": 182.69055978741454,
+ "y": 1039.2661332027988
+ }
+ },
+ {
+ "id": "738ec1dd-416e-471d-acf0-ca324acd0954",
+ "type": "invocation",
+ "data": {
+ "id": "738ec1dd-416e-471d-acf0-ca324acd0954",
+ "version": "1.0.2",
+ "nodePack": "invokeai",
+ "label": "",
+ "notes": "",
+ "type": "image",
+ "inputs": {
+ "image": {
+ "name": "image",
+ "label": "Starting Image",
+ "description": "First frame of the video"
+ }
+ },
+ "isOpen": true,
+ "isIntermediate": true,
+ "useCache": true
+ },
+ "position": {
+ "x": -270.0433070097871,
+ "y": 1057.784213432167
+ }
+ },
+ {
+ "id": "c857e3e8-334f-4a6e-9013-3326316e4281",
+ "type": "invocation",
+ "data": {
+ "id": "c857e3e8-334f-4a6e-9013-3326316e4281",
+ "version": "1.0.1",
+ "nodePack": "invokeai",
+ "label": "Num Frames",
+ "notes": "",
+ "type": "integer",
+ "inputs": {
+ "value": {
+ "name": "value",
+ "label": "Frames",
+ "description": "",
+ "value": 81
+ }
+ },
+ "isOpen": true,
+ "isIntermediate": true,
+ "useCache": true
+ },
+ "position": {
+ "x": -42.07490820354809,
+ "y": 751.8724152644219
+ }
+ },
+ {
+ "id": "97c6fecb-8354-4ace-b337-e544b0ba9982",
+ "type": "invocation",
+ "data": {
+ "id": "97c6fecb-8354-4ace-b337-e544b0ba9982",
+ "version": "1.2.0",
+ "nodePack": "invokeai",
+ "label": "",
+ "notes": "",
+ "type": "wan_ref_image_encoder",
+ "inputs": {
+ "image": {
+ "name": "image",
+ "label": "",
+ "description": ""
+ },
+ "vae": {
+ "name": "vae",
+ "label": "",
+ "description": ""
+ },
+ "width": {
+ "name": "width",
+ "label": "",
+ "description": "",
+ "value": 1024
+ },
+ "height": {
+ "name": "height",
+ "label": "",
+ "description": "",
+ "value": 1024
+ },
+ "num_frames": {
+ "name": "num_frames",
+ "label": "",
+ "description": "",
+ "value": 1
+ },
+ "end_image": {
+ "name": "end_image",
+ "label": "",
+ "description": ""
+ }
+ },
+ "isOpen": true,
+ "isIntermediate": true,
+ "useCache": true
+ },
+ "position": {
+ "x": 793.5169473342651,
+ "y": 1028.2344631556177
+ }
+ },
+ {
+ "id": "c845a30f-7d6a-4351-9dcd-ea3df4ebdc6e",
+ "type": "invocation",
+ "data": {
+ "id": "c845a30f-7d6a-4351-9dcd-ea3df4ebdc6e",
+ "version": "1.0.2",
+ "nodePack": "invokeai",
+ "label": "",
+ "notes": "",
+ "type": "image",
+ "inputs": {
+ "image": {
+ "name": "image",
+ "label": "Ending Image",
+ "description": ""
+ }
+ },
+ "isOpen": true,
+ "isIntermediate": true,
+ "useCache": true
+ },
+ "position": {
+ "x": -270.4807620633504,
+ "y": 1327.9487214548142
+ }
+ },
+ {
+ "id": "0d821e80-76ad-47a4-b050-d55b3d5084bc",
+ "type": "invocation",
+ "data": {
+ "id": "0d821e80-76ad-47a4-b050-d55b3d5084bc",
+ "version": "1.2.2",
+ "nodePack": "invokeai",
+ "label": "",
+ "notes": "",
+ "type": "img_resize",
+ "inputs": {
+ "board": {
+ "name": "board",
+ "label": "",
+ "description": "",
+ "value": "auto"
+ },
+ "metadata": {
+ "name": "metadata",
+ "label": "",
+ "description": ""
+ },
+ "image": {
+ "name": "image",
+ "label": "",
+ "description": ""
+ },
+ "width": {
+ "name": "width",
+ "label": "",
+ "description": "",
+ "value": 512
+ },
+ "height": {
+ "name": "height",
+ "label": "",
+ "description": "",
+ "value": 512
+ },
+ "resample_mode": {
+ "name": "resample_mode",
+ "label": "",
+ "description": "",
+ "value": "bicubic"
+ }
+ },
+ "isOpen": true,
+ "isIntermediate": true,
+ "useCache": true
+ },
+ "position": {
+ "x": 247.59145192825213,
+ "y": 1381.3336415226458
+ }
+ }
+ ],
+ "edges": [
+ {
+ "id": "edge-loader-t5-pos",
+ "type": "default",
+ "source": "1a0e6c7b-9d2f-4b3c-8e1a-2f3d4c5b6a7e",
+ "target": "2b1f7d8c-ae3f-5c4d-9f2b-3a4e5d6c7b8f",
+ "sourceHandle": "wan_t5_encoder",
+ "targetHandle": "wan_t5_encoder"
+ },
+ {
+ "id": "edge-loader-t5-neg",
+ "type": "default",
+ "source": "1a0e6c7b-9d2f-4b3c-8e1a-2f3d4c5b6a7e",
+ "target": "3c2a8e9d-bf4a-6d5e-af3c-4b5f6e7d8c9a",
+ "sourceHandle": "wan_t5_encoder",
+ "targetHandle": "wan_t5_encoder"
+ },
+ {
+ "id": "edge-loader-vae-l2v",
+ "type": "default",
+ "source": "1a0e6c7b-9d2f-4b3c-8e1a-2f3d4c5b6a7e",
+ "target": "6f5dcb1c-e27d-9fb0-d16f-7e8cbaa1fcbd",
+ "sourceHandle": "vae",
+ "targetHandle": "vae"
+ },
+ {
+ "id": "edge-pos-cond-denoise",
+ "type": "default",
+ "source": "2b1f7d8c-ae3f-5c4d-9f2b-3a4e5d6c7b8f",
+ "target": "4d3b9faf-c05b-7e6f-bf4d-5c6a7f8e9dab",
+ "sourceHandle": "conditioning",
+ "targetHandle": "positive_conditioning"
+ },
+ {
+ "id": "edge-neg-cond-denoise",
+ "type": "default",
+ "source": "3c2a8e9d-bf4a-6d5e-af3c-4b5f6e7d8c9a",
+ "target": "4d3b9faf-c05b-7e6f-bf4d-5c6a7f8e9dab",
+ "sourceHandle": "conditioning",
+ "targetHandle": "negative_conditioning"
+ },
+ {
+ "id": "edge-rand-seed-denoise",
+ "type": "default",
+ "source": "5e4cab0b-d16c-8faf-c05e-6d7baf90ebbc",
+ "target": "4d3b9faf-c05b-7e6f-bf4d-5c6a7f8e9dab",
+ "sourceHandle": "value",
+ "targetHandle": "seed"
+ },
+ {
+ "id": "edge-denoise-latents-l2v",
+ "type": "default",
+ "source": "4d3b9faf-c05b-7e6f-bf4d-5c6a7f8e9dab",
+ "target": "6f5dcb1c-e27d-9fb0-d16f-7e8cbaa1fcbd",
+ "sourceHandle": "latents",
+ "targetHandle": "latents"
+ },
+ {
+ "id": "reactflow__edge-1a0e6c7b-9d2f-4b3c-8e1a-2f3d4c5b6a7etransformer-291abec8-b996-4c7b-959c-360f56a3ed60transformer",
+ "type": "default",
+ "source": "1a0e6c7b-9d2f-4b3c-8e1a-2f3d4c5b6a7e",
+ "target": "291abec8-b996-4c7b-959c-360f56a3ed60",
+ "sourceHandle": "transformer",
+ "targetHandle": "transformer"
+ },
+ {
+ "id": "reactflow__edge-291abec8-b996-4c7b-959c-360f56a3ed60transformer-ae8f115d-a3b8-4f76-b280-7313366a94adtransformer",
+ "type": "default",
+ "source": "291abec8-b996-4c7b-959c-360f56a3ed60",
+ "target": "ae8f115d-a3b8-4f76-b280-7313366a94ad",
+ "sourceHandle": "transformer",
+ "targetHandle": "transformer"
+ },
+ {
+ "id": "reactflow__edge-ae8f115d-a3b8-4f76-b280-7313366a94adtransformer-4d3b9faf-c05b-7e6f-bf4d-5c6a7f8e9dabtransformer",
+ "type": "default",
+ "source": "ae8f115d-a3b8-4f76-b280-7313366a94ad",
+ "target": "4d3b9faf-c05b-7e6f-bf4d-5c6a7f8e9dab",
+ "sourceHandle": "transformer",
+ "targetHandle": "transformer"
+ },
+ {
+ "id": "reactflow__edge-97c6fecb-8354-4ace-b337-e544b0ba9982ref_image-4d3b9faf-c05b-7e6f-bf4d-5c6a7f8e9dabref_image",
+ "type": "default",
+ "source": "97c6fecb-8354-4ace-b337-e544b0ba9982",
+ "target": "4d3b9faf-c05b-7e6f-bf4d-5c6a7f8e9dab",
+ "sourceHandle": "ref_image",
+ "targetHandle": "ref_image"
+ },
+ {
+ "id": "reactflow__edge-738ec1dd-416e-471d-acf0-ca324acd0954image-97c6fecb-8354-4ace-b337-e544b0ba9982image",
+ "type": "default",
+ "source": "738ec1dd-416e-471d-acf0-ca324acd0954",
+ "target": "97c6fecb-8354-4ace-b337-e544b0ba9982",
+ "sourceHandle": "image",
+ "targetHandle": "image"
+ },
+ {
+ "id": "reactflow__edge-1a0e6c7b-9d2f-4b3c-8e1a-2f3d4c5b6a7evae-97c6fecb-8354-4ace-b337-e544b0ba9982vae",
+ "type": "default",
+ "source": "1a0e6c7b-9d2f-4b3c-8e1a-2f3d4c5b6a7e",
+ "target": "97c6fecb-8354-4ace-b337-e544b0ba9982",
+ "sourceHandle": "vae",
+ "targetHandle": "vae"
+ },
+ {
+ "id": "reactflow__edge-738ec1dd-416e-471d-acf0-ca324acd0954width-d27f29bf-6f26-4088-b1cc-c722ecd9fd61width",
+ "type": "default",
+ "source": "738ec1dd-416e-471d-acf0-ca324acd0954",
+ "target": "d27f29bf-6f26-4088-b1cc-c722ecd9fd61",
+ "sourceHandle": "width",
+ "targetHandle": "width"
+ },
+ {
+ "id": "reactflow__edge-738ec1dd-416e-471d-acf0-ca324acd0954height-d27f29bf-6f26-4088-b1cc-c722ecd9fd61height",
+ "type": "default",
+ "source": "738ec1dd-416e-471d-acf0-ca324acd0954",
+ "target": "d27f29bf-6f26-4088-b1cc-c722ecd9fd61",
+ "sourceHandle": "height",
+ "targetHandle": "height"
+ },
+ {
+ "id": "reactflow__edge-d27f29bf-6f26-4088-b1cc-c722ecd9fd61width-4d3b9faf-c05b-7e6f-bf4d-5c6a7f8e9dabwidth",
+ "type": "default",
+ "source": "d27f29bf-6f26-4088-b1cc-c722ecd9fd61",
+ "target": "4d3b9faf-c05b-7e6f-bf4d-5c6a7f8e9dab",
+ "sourceHandle": "width",
+ "targetHandle": "width"
+ },
+ {
+ "id": "reactflow__edge-d27f29bf-6f26-4088-b1cc-c722ecd9fd61height-4d3b9faf-c05b-7e6f-bf4d-5c6a7f8e9dabheight",
+ "type": "default",
+ "source": "d27f29bf-6f26-4088-b1cc-c722ecd9fd61",
+ "target": "4d3b9faf-c05b-7e6f-bf4d-5c6a7f8e9dab",
+ "sourceHandle": "height",
+ "targetHandle": "height"
+ },
+ {
+ "id": "reactflow__edge-d27f29bf-6f26-4088-b1cc-c722ecd9fd61width-97c6fecb-8354-4ace-b337-e544b0ba9982width",
+ "type": "default",
+ "source": "d27f29bf-6f26-4088-b1cc-c722ecd9fd61",
+ "target": "97c6fecb-8354-4ace-b337-e544b0ba9982",
+ "sourceHandle": "width",
+ "targetHandle": "width"
+ },
+ {
+ "id": "reactflow__edge-d27f29bf-6f26-4088-b1cc-c722ecd9fd61height-97c6fecb-8354-4ace-b337-e544b0ba9982height",
+ "type": "default",
+ "source": "d27f29bf-6f26-4088-b1cc-c722ecd9fd61",
+ "target": "97c6fecb-8354-4ace-b337-e544b0ba9982",
+ "sourceHandle": "height",
+ "targetHandle": "height"
+ },
+ {
+ "id": "reactflow__edge-c857e3e8-334f-4a6e-9013-3326316e4281value-4d3b9faf-c05b-7e6f-bf4d-5c6a7f8e9dabnum_frames",
+ "type": "default",
+ "source": "c857e3e8-334f-4a6e-9013-3326316e4281",
+ "target": "4d3b9faf-c05b-7e6f-bf4d-5c6a7f8e9dab",
+ "sourceHandle": "value",
+ "targetHandle": "num_frames"
+ },
+ {
+ "id": "reactflow__edge-c857e3e8-334f-4a6e-9013-3326316e4281value-97c6fecb-8354-4ace-b337-e544b0ba9982num_frames",
+ "type": "default",
+ "source": "c857e3e8-334f-4a6e-9013-3326316e4281",
+ "target": "97c6fecb-8354-4ace-b337-e544b0ba9982",
+ "sourceHandle": "value",
+ "targetHandle": "num_frames"
+ },
+ {
+ "id": "reactflow__edge-c845a30f-7d6a-4351-9dcd-ea3df4ebdc6eimage-0d821e80-76ad-47a4-b050-d55b3d5084bcimage",
+ "type": "default",
+ "source": "c845a30f-7d6a-4351-9dcd-ea3df4ebdc6e",
+ "target": "0d821e80-76ad-47a4-b050-d55b3d5084bc",
+ "sourceHandle": "image",
+ "targetHandle": "image"
+ },
+ {
+ "id": "reactflow__edge-738ec1dd-416e-471d-acf0-ca324acd0954width-0d821e80-76ad-47a4-b050-d55b3d5084bcwidth",
+ "type": "default",
+ "source": "738ec1dd-416e-471d-acf0-ca324acd0954",
+ "target": "0d821e80-76ad-47a4-b050-d55b3d5084bc",
+ "sourceHandle": "width",
+ "targetHandle": "width"
+ },
+ {
+ "id": "reactflow__edge-738ec1dd-416e-471d-acf0-ca324acd0954height-0d821e80-76ad-47a4-b050-d55b3d5084bcheight",
+ "type": "default",
+ "source": "738ec1dd-416e-471d-acf0-ca324acd0954",
+ "target": "0d821e80-76ad-47a4-b050-d55b3d5084bc",
+ "sourceHandle": "height",
+ "targetHandle": "height"
+ },
+ {
+ "id": "reactflow__edge-0d821e80-76ad-47a4-b050-d55b3d5084bcimage-97c6fecb-8354-4ace-b337-e544b0ba9982end_image",
+ "type": "default",
+ "source": "0d821e80-76ad-47a4-b050-d55b3d5084bc",
+ "target": "97c6fecb-8354-4ace-b337-e544b0ba9982",
+ "sourceHandle": "image",
+ "targetHandle": "end_image"
+ }
+ ]
+}
diff --git a/invokeai/app/services/workflow_records/default_workflows/Text to Video - Wan 2.2 Lightning w_ Concept LoRAs.json b/invokeai/app/services/workflow_records/default_workflows/Text to Video - Wan 2.2 Lightning w_ Concept LoRAs.json
new file mode 100644
index 00000000000..612a810df7a
--- /dev/null
+++ b/invokeai/app/services/workflow_records/default_workflows/Text to Video - Wan 2.2 Lightning w_ Concept LoRAs.json
@@ -0,0 +1,991 @@
+{
+ "name": "Text to Video - Wan 2.2 Lightning w/ Concept LoRAs",
+ "author": "InvokeAI",
+ "description": "Fast text-to-video generation with Wan 2.2 T2V A14B + the Lightning LoRA pair. Distillation lets the model converge in 4 steps with CFG 1.0 (no negative branch), roughly 20x faster than the standard T2V workflow at the cost of some image quality. Pick the high-noise Lightning LoRA on 'Apply LoRA (High)' and the low-noise one on 'Apply LoRA (Low)' — the 'auto' target routes each to the right expert when the LoRAs are tagged. Defaults: 832x480, 81 frames @ 16 FPS (~5 s).",
+ "version": "1.0.0",
+ "contact": "",
+ "tags": "wan2.2, text to video, video, lightning, lora",
+ "notes": "Each InvokeAI install assigns its own internal model IDs, so the model fields are left blank -- select these in the form after loading:\n\nMain Model node:\n- Main Model: Wan 2.2 T2V A14B High Noise (Q4_K_M GGUF recommended for <=16 GB VRAM)\n- Transformer (Low Noise): Wan 2.2 T2V A14B Low Noise (Q4_K_M)\n- VAE: Wan 2.2 A14B VAE\n- T5 Encoder: Wan T5 Encoder (UMT5-XXL)\n\nLightning LoRAs (4-step distillation - use the T2V variant, NOT I2V):\n- Apply Lightning LoRA (High Noise): Wan 2.2 T2V Lightning High Noise (4-step)\n- Apply Lightning LoRA (Low Noise): Wan 2.2 T2V Lightning Low Noise (4-step)\n\nThis workflow also has two optional concept-LoRA loaders (routed via 'target' = high/low) -- leave them empty or select your own high/low-noise concept LoRA pair.\n\nThe Lightning LoRAs are A14B-specific and distinct for T2V vs I2V. If your LoRAs are untagged (no 'expert' field), use each LoRA loader's 'target' dropdown to route the high-noise LoRA to 'high' and the low-noise to 'low'. CFG=1.0 skips the negative-conditioning branch, so the Negative Prompt is ignored at runtime.",
+ "exposedFields": [
+ {
+ "nodeId": "1a0e6c7b-9d2f-4b3c-8e1a-2f3d4c5b6a7e",
+ "fieldName": "model"
+ },
+ {
+ "nodeId": "1a0e6c7b-9d2f-4b3c-8e1a-2f3d4c5b6a7e",
+ "fieldName": "transformer_low_noise_model"
+ },
+ {
+ "nodeId": "1a0e6c7b-9d2f-4b3c-8e1a-2f3d4c5b6a7e",
+ "fieldName": "component_source"
+ },
+ {
+ "nodeId": "1a0e6c7b-9d2f-4b3c-8e1a-2f3d4c5b6a7e",
+ "fieldName": "vae_model"
+ },
+ {
+ "nodeId": "1a0e6c7b-9d2f-4b3c-8e1a-2f3d4c5b6a7e",
+ "fieldName": "wan_t5_encoder_model"
+ },
+ {
+ "nodeId": "2b1f7d8c-ae3f-5c4d-9f2b-3a4e5d6c7b8f",
+ "fieldName": "prompt"
+ },
+ {
+ "nodeId": "3c2a8e9d-bf4a-6d5e-af3c-4b5f6e7d8c9a",
+ "fieldName": "prompt"
+ },
+ {
+ "nodeId": "4d3b9faf-c05b-7e6f-bf4d-5c6a7f8e9dab",
+ "fieldName": "steps"
+ },
+ {
+ "nodeId": "4d3b9faf-c05b-7e6f-bf4d-5c6a7f8e9dab",
+ "fieldName": "guidance_scale"
+ },
+ {
+ "nodeId": "4d3b9faf-c05b-7e6f-bf4d-5c6a7f8e9dab",
+ "fieldName": "guidance_scale_low_noise"
+ },
+ {
+ "nodeId": "4d3b9faf-c05b-7e6f-bf4d-5c6a7f8e9dab",
+ "fieldName": "width"
+ },
+ {
+ "nodeId": "4d3b9faf-c05b-7e6f-bf4d-5c6a7f8e9dab",
+ "fieldName": "height"
+ },
+ {
+ "nodeId": "4d3b9faf-c05b-7e6f-bf4d-5c6a7f8e9dab",
+ "fieldName": "num_frames"
+ },
+ {
+ "nodeId": "6f5dcb1c-e27d-9fb0-d16f-7e8cbaa1fcbd",
+ "fieldName": "fps"
+ }
+ ],
+ "meta": {
+ "version": "4.0.0",
+ "category": "default"
+ },
+ "nodes": [
+ {
+ "id": "1a0e6c7b-9d2f-4b3c-8e1a-2f3d4c5b6a7e",
+ "type": "invocation",
+ "data": {
+ "id": "1a0e6c7b-9d2f-4b3c-8e1a-2f3d4c5b6a7e",
+ "version": "1.0.1",
+ "nodePack": "invokeai",
+ "label": "",
+ "notes": "",
+ "type": "wan_model_loader",
+ "inputs": {
+ "model": {
+ "name": "model",
+ "label": "Transformer (High Noise)",
+ "description": ""
+ },
+ "transformer_low_noise_model": {
+ "name": "transformer_low_noise_model",
+ "label": "",
+ "description": ""
+ },
+ "vae_model": {
+ "name": "vae_model",
+ "label": "",
+ "description": ""
+ },
+ "wan_t5_encoder_model": {
+ "name": "wan_t5_encoder_model",
+ "label": "",
+ "description": ""
+ },
+ "component_source": {
+ "name": "component_source",
+ "label": "",
+ "description": ""
+ }
+ },
+ "dynamicInputTemplates": {},
+ "isOpen": true,
+ "isIntermediate": true,
+ "useCache": false
+ },
+ "position": {
+ "x": 0,
+ "y": 0
+ }
+ },
+ {
+ "id": "2b1f7d8c-ae3f-5c4d-9f2b-3a4e5d6c7b8f",
+ "type": "invocation",
+ "data": {
+ "id": "2b1f7d8c-ae3f-5c4d-9f2b-3a4e5d6c7b8f",
+ "version": "1.0.0",
+ "nodePack": "invokeai",
+ "label": "Positive Prompt",
+ "notes": "",
+ "type": "wan_text_encoder",
+ "inputs": {
+ "prompt": {
+ "name": "prompt",
+ "label": "",
+ "description": "",
+ "value": ""
+ },
+ "wan_t5_encoder": {
+ "name": "wan_t5_encoder",
+ "label": "",
+ "description": ""
+ }
+ },
+ "dynamicInputTemplates": {},
+ "isOpen": true,
+ "isIntermediate": true,
+ "useCache": true
+ },
+ "position": {
+ "x": 1400,
+ "y": 212
+ }
+ },
+ {
+ "id": "3c2a8e9d-bf4a-6d5e-af3c-4b5f6e7d8c9a",
+ "type": "invocation",
+ "data": {
+ "id": "3c2a8e9d-bf4a-6d5e-af3c-4b5f6e7d8c9a",
+ "version": "1.0.0",
+ "nodePack": "invokeai",
+ "label": "Negative Prompt (unused at CFG=1.0)",
+ "notes": "",
+ "type": "wan_text_encoder",
+ "inputs": {
+ "prompt": {
+ "name": "prompt",
+ "label": "",
+ "description": "",
+ "value": " "
+ },
+ "wan_t5_encoder": {
+ "name": "wan_t5_encoder",
+ "label": "",
+ "description": ""
+ }
+ },
+ "dynamicInputTemplates": {},
+ "isOpen": true,
+ "isIntermediate": true,
+ "useCache": true
+ },
+ "position": {
+ "x": 1400,
+ "y": 392
+ }
+ },
+ {
+ "id": "5e4cab0b-d16c-8faf-c05e-6d7baf90ebbc",
+ "type": "invocation",
+ "data": {
+ "id": "5e4cab0b-d16c-8faf-c05e-6d7baf90ebbc",
+ "version": "1.0.1",
+ "nodePack": "invokeai",
+ "label": "",
+ "notes": "",
+ "type": "rand_int",
+ "inputs": {
+ "low": {
+ "name": "low",
+ "label": "",
+ "description": "",
+ "value": 0
+ },
+ "high": {
+ "name": "high",
+ "label": "",
+ "description": "",
+ "value": 2147483647
+ }
+ },
+ "dynamicInputTemplates": {},
+ "isOpen": true,
+ "isIntermediate": true,
+ "useCache": false
+ },
+ "position": {
+ "x": 1400,
+ "y": 572
+ }
+ },
+ {
+ "id": "4d3b9faf-c05b-7e6f-bf4d-5c6a7f8e9dab",
+ "type": "invocation",
+ "data": {
+ "id": "4d3b9faf-c05b-7e6f-bf4d-5c6a7f8e9dab",
+ "version": "1.0.0",
+ "nodePack": "invokeai",
+ "label": "",
+ "notes": "",
+ "type": "wan_video_denoise",
+ "inputs": {
+ "transformer": {
+ "name": "transformer",
+ "label": "",
+ "description": ""
+ },
+ "positive_conditioning": {
+ "name": "positive_conditioning",
+ "label": "",
+ "description": ""
+ },
+ "negative_conditioning": {
+ "name": "negative_conditioning",
+ "label": "",
+ "description": ""
+ },
+ "ref_image": {
+ "name": "ref_image",
+ "label": "",
+ "description": ""
+ },
+ "guidance_scale": {
+ "name": "guidance_scale",
+ "label": "CFG (High Noise)",
+ "description": "",
+ "value": 1
+ },
+ "guidance_scale_low_noise": {
+ "name": "guidance_scale_low_noise",
+ "label": "CFG (Low Noise)",
+ "description": "",
+ "value": 1
+ },
+ "width": {
+ "name": "width",
+ "label": "",
+ "description": "",
+ "value": 832
+ },
+ "height": {
+ "name": "height",
+ "label": "",
+ "description": "",
+ "value": 480
+ },
+ "num_frames": {
+ "name": "num_frames",
+ "label": "Frames",
+ "description": "",
+ "value": 81
+ },
+ "steps": {
+ "name": "steps",
+ "label": "",
+ "description": "",
+ "value": 4
+ },
+ "seed": {
+ "name": "seed",
+ "label": "",
+ "description": "",
+ "value": 0
+ }
+ },
+ "dynamicInputTemplates": {},
+ "isOpen": true,
+ "isIntermediate": true,
+ "useCache": true
+ },
+ "position": {
+ "x": 1750,
+ "y": 230
+ }
+ },
+ {
+ "id": "6f5dcb1c-e27d-9fb0-d16f-7e8cbaa1fcbd",
+ "type": "invocation",
+ "data": {
+ "id": "6f5dcb1c-e27d-9fb0-d16f-7e8cbaa1fcbd",
+ "version": "1.0.0",
+ "nodePack": "invokeai",
+ "label": "",
+ "notes": "",
+ "type": "wan_l2v",
+ "inputs": {
+ "board": {
+ "name": "board",
+ "label": "",
+ "description": ""
+ },
+ "metadata": {
+ "name": "metadata",
+ "label": "",
+ "description": ""
+ },
+ "latents": {
+ "name": "latents",
+ "label": "",
+ "description": ""
+ },
+ "vae": {
+ "name": "vae",
+ "label": "",
+ "description": ""
+ },
+ "fps": {
+ "name": "fps",
+ "label": "FPS",
+ "description": "",
+ "value": 16
+ }
+ },
+ "dynamicInputTemplates": {},
+ "isOpen": true,
+ "isIntermediate": false,
+ "useCache": true
+ },
+ "position": {
+ "x": 2100,
+ "y": 33
+ }
+ },
+ {
+ "id": "5d18cf53-905d-4eb6-afd4-6c19f790b374",
+ "type": "invocation",
+ "data": {
+ "id": "5d18cf53-905d-4eb6-afd4-6c19f790b374",
+ "version": "1.0.1",
+ "nodePack": "invokeai",
+ "label": "",
+ "notes": "",
+ "type": "wan_lora_loader",
+ "inputs": {
+ "lora": {
+ "name": "lora",
+ "label": "Lightning LoRA (High Noise)",
+ "description": ""
+ },
+ "weight": {
+ "name": "weight",
+ "label": "",
+ "description": "",
+ "value": 1
+ },
+ "target": {
+ "name": "target",
+ "label": "",
+ "description": "",
+ "value": "high"
+ },
+ "transformer": {
+ "name": "transformer",
+ "label": "",
+ "description": ""
+ }
+ },
+ "dynamicInputTemplates": {},
+ "isOpen": true,
+ "isIntermediate": true,
+ "useCache": true
+ },
+ "position": {
+ "x": 350,
+ "y": 772
+ }
+ },
+ {
+ "id": "eb6b4c66-0bfb-4e0d-bc39-9d8814f3cd75",
+ "type": "invocation",
+ "data": {
+ "id": "eb6b4c66-0bfb-4e0d-bc39-9d8814f3cd75",
+ "version": "1.0.1",
+ "nodePack": "invokeai",
+ "label": "",
+ "notes": "",
+ "type": "wan_lora_loader",
+ "inputs": {
+ "lora": {
+ "name": "lora",
+ "label": "Lightning LoRA (Low Noise)",
+ "description": ""
+ },
+ "weight": {
+ "name": "weight",
+ "label": "",
+ "description": "",
+ "value": 1
+ },
+ "target": {
+ "name": "target",
+ "label": "",
+ "description": "",
+ "value": "low"
+ },
+ "transformer": {
+ "name": "transformer",
+ "label": "",
+ "description": ""
+ }
+ },
+ "dynamicInputTemplates": {},
+ "isOpen": true,
+ "isIntermediate": true,
+ "useCache": true
+ },
+ "position": {
+ "x": 700,
+ "y": 772
+ }
+ },
+ {
+ "id": "f37bd1f0-f0d8-4105-8e07-5ec484298d36",
+ "type": "invocation",
+ "data": {
+ "id": "f37bd1f0-f0d8-4105-8e07-5ec484298d36",
+ "type": "wan_lora_collection_loader",
+ "version": "1.0.1",
+ "label": "",
+ "notes": "",
+ "isOpen": true,
+ "isIntermediate": true,
+ "useCache": true,
+ "nodePack": "invokeai",
+ "inputs": {
+ "loras": {
+ "name": "loras",
+ "label": "",
+ "description": "",
+ "value": []
+ },
+ "transformer": {
+ "name": "transformer",
+ "label": "",
+ "description": ""
+ }
+ },
+ "dynamicInputTemplates": {}
+ },
+ "position": {
+ "x": 1053.289904257177,
+ "y": 770.7485394743496
+ }
+ }
+ ],
+ "edges": [
+ {
+ "id": "edge-loader-t5-pos",
+ "type": "default",
+ "source": "1a0e6c7b-9d2f-4b3c-8e1a-2f3d4c5b6a7e",
+ "target": "2b1f7d8c-ae3f-5c4d-9f2b-3a4e5d6c7b8f",
+ "sourceHandle": "wan_t5_encoder",
+ "targetHandle": "wan_t5_encoder"
+ },
+ {
+ "id": "edge-loader-t5-neg",
+ "type": "default",
+ "source": "1a0e6c7b-9d2f-4b3c-8e1a-2f3d4c5b6a7e",
+ "target": "3c2a8e9d-bf4a-6d5e-af3c-4b5f6e7d8c9a",
+ "sourceHandle": "wan_t5_encoder",
+ "targetHandle": "wan_t5_encoder"
+ },
+ {
+ "id": "edge-loader-vae-l2v",
+ "type": "default",
+ "source": "1a0e6c7b-9d2f-4b3c-8e1a-2f3d4c5b6a7e",
+ "target": "6f5dcb1c-e27d-9fb0-d16f-7e8cbaa1fcbd",
+ "sourceHandle": "vae",
+ "targetHandle": "vae"
+ },
+ {
+ "id": "edge-pos-cond-denoise",
+ "type": "default",
+ "source": "2b1f7d8c-ae3f-5c4d-9f2b-3a4e5d6c7b8f",
+ "target": "4d3b9faf-c05b-7e6f-bf4d-5c6a7f8e9dab",
+ "sourceHandle": "conditioning",
+ "targetHandle": "positive_conditioning"
+ },
+ {
+ "id": "edge-neg-cond-denoise",
+ "type": "default",
+ "source": "3c2a8e9d-bf4a-6d5e-af3c-4b5f6e7d8c9a",
+ "target": "4d3b9faf-c05b-7e6f-bf4d-5c6a7f8e9dab",
+ "sourceHandle": "conditioning",
+ "targetHandle": "negative_conditioning"
+ },
+ {
+ "id": "edge-rand-seed-denoise",
+ "type": "default",
+ "source": "5e4cab0b-d16c-8faf-c05e-6d7baf90ebbc",
+ "target": "4d3b9faf-c05b-7e6f-bf4d-5c6a7f8e9dab",
+ "sourceHandle": "value",
+ "targetHandle": "seed"
+ },
+ {
+ "id": "edge-denoise-latents-l2v",
+ "type": "default",
+ "source": "4d3b9faf-c05b-7e6f-bf4d-5c6a7f8e9dab",
+ "target": "6f5dcb1c-e27d-9fb0-d16f-7e8cbaa1fcbd",
+ "sourceHandle": "latents",
+ "targetHandle": "latents"
+ },
+ {
+ "id": "reactflow__edge-1a0e6c7b-9d2f-4b3c-8e1a-2f3d4c5b6a7etransformer-5d18cf53-905d-4eb6-afd4-6c19f790b374transformer",
+ "type": "default",
+ "source": "1a0e6c7b-9d2f-4b3c-8e1a-2f3d4c5b6a7e",
+ "target": "5d18cf53-905d-4eb6-afd4-6c19f790b374",
+ "sourceHandle": "transformer",
+ "targetHandle": "transformer"
+ },
+ {
+ "id": "reactflow__edge-5d18cf53-905d-4eb6-afd4-6c19f790b374transformer-eb6b4c66-0bfb-4e0d-bc39-9d8814f3cd75transformer",
+ "type": "default",
+ "source": "5d18cf53-905d-4eb6-afd4-6c19f790b374",
+ "target": "eb6b4c66-0bfb-4e0d-bc39-9d8814f3cd75",
+ "sourceHandle": "transformer",
+ "targetHandle": "transformer"
+ },
+ {
+ "id": "reactflow__edge-eb6b4c66-0bfb-4e0d-bc39-9d8814f3cd75transformer-f37bd1f0-f0d8-4105-8e07-5ec484298d36transformer",
+ "type": "default",
+ "source": "eb6b4c66-0bfb-4e0d-bc39-9d8814f3cd75",
+ "target": "f37bd1f0-f0d8-4105-8e07-5ec484298d36",
+ "sourceHandle": "transformer",
+ "targetHandle": "transformer"
+ },
+ {
+ "id": "reactflow__edge-f37bd1f0-f0d8-4105-8e07-5ec484298d36transformer-4d3b9faf-c05b-7e6f-bf4d-5c6a7f8e9dabtransformer",
+ "type": "default",
+ "source": "f37bd1f0-f0d8-4105-8e07-5ec484298d36",
+ "target": "4d3b9faf-c05b-7e6f-bf4d-5c6a7f8e9dab",
+ "sourceHandle": "transformer",
+ "targetHandle": "transformer"
+ }
+ ],
+ "form": {
+ "elements": {
+ "root": {
+ "id": "root",
+ "data": {
+ "layout": "column",
+ "children": [
+ "heading-XDPaQofdEE",
+ "node-field-KcT0glAFDK",
+ "container-i8V1oJeQCP",
+ "container-mUeBcw2tYf",
+ "heading-wuPtY8B4vR",
+ "container-AkpUH1WtID",
+ "heading-TwsoE3wQi9",
+ "container-0u7wyxEdNa",
+ "heading-vSOD5q5gvL",
+ "node-field-EbS4ZMqDPZ",
+ "heading-10PuWMKJg9",
+ "container-C0z6ru33to"
+ ]
+ },
+ "type": "container"
+ },
+ "node-field-zQA9XuRZRW": {
+ "id": "node-field-zQA9XuRZRW",
+ "parentId": "container-mUeBcw2tYf",
+ "data": {
+ "fieldIdentifier": {
+ "nodeId": "6f5dcb1c-e27d-9fb0-d16f-7e8cbaa1fcbd",
+ "fieldName": "fps"
+ },
+ "showDescription": false,
+ "settings": {
+ "type": "integer-field-config",
+ "showShuffle": false,
+ "component": "number-input"
+ }
+ },
+ "type": "node-field"
+ },
+ "node-field-ok9GStueKC": {
+ "id": "node-field-ok9GStueKC",
+ "parentId": "container-mUeBcw2tYf",
+ "data": {
+ "fieldIdentifier": {
+ "nodeId": "4d3b9faf-c05b-7e6f-bf4d-5c6a7f8e9dab",
+ "fieldName": "num_frames"
+ },
+ "showDescription": false,
+ "settings": {
+ "type": "integer-field-config",
+ "showShuffle": false,
+ "component": "number-input"
+ }
+ },
+ "type": "node-field"
+ },
+ "node-field-58JRDH0dfF": {
+ "id": "node-field-58JRDH0dfF",
+ "parentId": "container-i8V1oJeQCP",
+ "data": {
+ "fieldIdentifier": {
+ "nodeId": "4d3b9faf-c05b-7e6f-bf4d-5c6a7f8e9dab",
+ "fieldName": "height"
+ },
+ "showDescription": false,
+ "settings": {
+ "type": "integer-field-config",
+ "showShuffle": false,
+ "component": "number-input"
+ }
+ },
+ "type": "node-field"
+ },
+ "node-field-jIfJ3RUDyT": {
+ "id": "node-field-jIfJ3RUDyT",
+ "parentId": "container-i8V1oJeQCP",
+ "data": {
+ "fieldIdentifier": {
+ "nodeId": "4d3b9faf-c05b-7e6f-bf4d-5c6a7f8e9dab",
+ "fieldName": "width"
+ },
+ "showDescription": false,
+ "settings": {
+ "type": "integer-field-config",
+ "showShuffle": false,
+ "component": "number-input"
+ }
+ },
+ "type": "node-field"
+ },
+ "node-field-QDqQ4XOKvg": {
+ "id": "node-field-QDqQ4XOKvg",
+ "parentId": "container-7g1Xedgg8T",
+ "data": {
+ "fieldIdentifier": {
+ "nodeId": "4d3b9faf-c05b-7e6f-bf4d-5c6a7f8e9dab",
+ "fieldName": "guidance_scale_low_noise"
+ },
+ "showDescription": false,
+ "settings": {
+ "type": "float-field-config",
+ "showShuffle": false,
+ "component": "number-input"
+ }
+ },
+ "type": "node-field"
+ },
+ "node-field-PdU0Sz2m5z": {
+ "id": "node-field-PdU0Sz2m5z",
+ "parentId": "container-y0w7TfJC7e",
+ "data": {
+ "fieldIdentifier": {
+ "nodeId": "4d3b9faf-c05b-7e6f-bf4d-5c6a7f8e9dab",
+ "fieldName": "guidance_scale"
+ },
+ "showDescription": false,
+ "settings": {
+ "type": "float-field-config",
+ "showShuffle": false,
+ "component": "number-input"
+ }
+ },
+ "type": "node-field"
+ },
+ "node-field-UiQ7MaseDR": {
+ "id": "node-field-UiQ7MaseDR",
+ "parentId": "container-mUeBcw2tYf",
+ "data": {
+ "fieldIdentifier": {
+ "nodeId": "4d3b9faf-c05b-7e6f-bf4d-5c6a7f8e9dab",
+ "fieldName": "steps"
+ },
+ "showDescription": false,
+ "settings": {
+ "type": "integer-field-config",
+ "showShuffle": false,
+ "component": "number-input"
+ }
+ },
+ "type": "node-field"
+ },
+ "node-field-KcT0glAFDK": {
+ "id": "node-field-KcT0glAFDK",
+ "parentId": "root",
+ "data": {
+ "fieldIdentifier": {
+ "nodeId": "2b1f7d8c-ae3f-5c4d-9f2b-3a4e5d6c7b8f",
+ "fieldName": "prompt"
+ },
+ "showDescription": false,
+ "settings": {
+ "type": "string-field-config",
+ "component": "textarea"
+ }
+ },
+ "type": "node-field"
+ },
+ "node-field-N9mexEKkYg": {
+ "id": "node-field-N9mexEKkYg",
+ "parentId": "container-C0z6ru33to",
+ "data": {
+ "fieldIdentifier": {
+ "nodeId": "1a0e6c7b-9d2f-4b3c-8e1a-2f3d4c5b6a7e",
+ "fieldName": "wan_t5_encoder_model"
+ },
+ "showDescription": false
+ },
+ "type": "node-field"
+ },
+ "node-field-2u9lAbMBnD": {
+ "id": "node-field-2u9lAbMBnD",
+ "parentId": "container-C0z6ru33to",
+ "data": {
+ "fieldIdentifier": {
+ "nodeId": "1a0e6c7b-9d2f-4b3c-8e1a-2f3d4c5b6a7e",
+ "fieldName": "vae_model"
+ },
+ "showDescription": false
+ },
+ "type": "node-field"
+ },
+ "node-field-CTGqrO3x8o": {
+ "id": "node-field-CTGqrO3x8o",
+ "parentId": "container-C0z6ru33to",
+ "data": {
+ "fieldIdentifier": {
+ "nodeId": "1a0e6c7b-9d2f-4b3c-8e1a-2f3d4c5b6a7e",
+ "fieldName": "component_source"
+ },
+ "showDescription": false
+ },
+ "type": "node-field"
+ },
+ "node-field-7G9srQT2Uw": {
+ "id": "node-field-7G9srQT2Uw",
+ "parentId": "container-7g1Xedgg8T",
+ "data": {
+ "fieldIdentifier": {
+ "nodeId": "1a0e6c7b-9d2f-4b3c-8e1a-2f3d4c5b6a7e",
+ "fieldName": "transformer_low_noise_model"
+ },
+ "showDescription": false
+ },
+ "type": "node-field"
+ },
+ "node-field-Q345WyNzc4": {
+ "id": "node-field-Q345WyNzc4",
+ "parentId": "container-y0w7TfJC7e",
+ "data": {
+ "fieldIdentifier": {
+ "nodeId": "1a0e6c7b-9d2f-4b3c-8e1a-2f3d4c5b6a7e",
+ "fieldName": "model"
+ },
+ "showDescription": false
+ },
+ "type": "node-field"
+ },
+ "container-mUeBcw2tYf": {
+ "id": "container-mUeBcw2tYf",
+ "parentId": "root",
+ "data": {
+ "layout": "row",
+ "children": [
+ "node-field-UiQ7MaseDR",
+ "node-field-ok9GStueKC",
+ "node-field-zQA9XuRZRW"
+ ]
+ },
+ "type": "container"
+ },
+ "heading-XDPaQofdEE": {
+ "id": "heading-XDPaQofdEE",
+ "parentId": "root",
+ "data": {
+ "content": "Generation"
+ },
+ "type": "heading"
+ },
+ "container-i8V1oJeQCP": {
+ "id": "container-i8V1oJeQCP",
+ "parentId": "root",
+ "data": {
+ "layout": "row",
+ "children": [
+ "node-field-jIfJ3RUDyT",
+ "node-field-58JRDH0dfF"
+ ]
+ },
+ "type": "container"
+ },
+ "heading-wuPtY8B4vR": {
+ "id": "heading-wuPtY8B4vR",
+ "parentId": "root",
+ "data": {
+ "content": "Model"
+ },
+ "type": "heading"
+ },
+ "container-AkpUH1WtID": {
+ "id": "container-AkpUH1WtID",
+ "parentId": "root",
+ "data": {
+ "layout": "row",
+ "children": [
+ "container-y0w7TfJC7e",
+ "container-7g1Xedgg8T"
+ ]
+ },
+ "type": "container"
+ },
+ "container-y0w7TfJC7e": {
+ "id": "container-y0w7TfJC7e",
+ "parentId": "container-AkpUH1WtID",
+ "data": {
+ "layout": "column",
+ "children": [
+ "node-field-Q345WyNzc4",
+ "node-field-PdU0Sz2m5z"
+ ]
+ },
+ "type": "container"
+ },
+ "container-7g1Xedgg8T": {
+ "id": "container-7g1Xedgg8T",
+ "parentId": "container-AkpUH1WtID",
+ "data": {
+ "layout": "column",
+ "children": [
+ "node-field-7G9srQT2Uw",
+ "node-field-QDqQ4XOKvg"
+ ]
+ },
+ "type": "container"
+ },
+ "container-C0z6ru33to": {
+ "id": "container-C0z6ru33to",
+ "parentId": "root",
+ "data": {
+ "layout": "row",
+ "children": [
+ "node-field-N9mexEKkYg",
+ "node-field-2u9lAbMBnD",
+ "node-field-CTGqrO3x8o"
+ ]
+ },
+ "type": "container"
+ },
+ "heading-10PuWMKJg9": {
+ "id": "heading-10PuWMKJg9",
+ "parentId": "root",
+ "data": {
+ "content": "Advanced"
+ },
+ "type": "heading"
+ },
+ "container-0u7wyxEdNa": {
+ "id": "container-0u7wyxEdNa",
+ "parentId": "root",
+ "data": {
+ "layout": "row",
+ "children": [
+ "container-Um9gcFv0lo",
+ "container-4owWd0thWk"
+ ]
+ },
+ "type": "container"
+ },
+ "container-Um9gcFv0lo": {
+ "id": "container-Um9gcFv0lo",
+ "parentId": "container-0u7wyxEdNa",
+ "data": {
+ "layout": "row",
+ "children": [
+ "container-lF8o60KuVb"
+ ]
+ },
+ "type": "container"
+ },
+ "container-4owWd0thWk": {
+ "id": "container-4owWd0thWk",
+ "parentId": "container-0u7wyxEdNa",
+ "data": {
+ "layout": "row",
+ "children": [
+ "container-rvE0oy63qL"
+ ]
+ },
+ "type": "container"
+ },
+ "container-lF8o60KuVb": {
+ "id": "container-lF8o60KuVb",
+ "parentId": "container-Um9gcFv0lo",
+ "data": {
+ "layout": "column",
+ "children": [
+ "node-field-tpqKlLmMtv"
+ ]
+ },
+ "type": "container"
+ },
+ "container-rvE0oy63qL": {
+ "id": "container-rvE0oy63qL",
+ "parentId": "container-4owWd0thWk",
+ "data": {
+ "layout": "column",
+ "children": [
+ "node-field-yuWwWH1ucK"
+ ]
+ },
+ "type": "container"
+ },
+ "heading-TwsoE3wQi9": {
+ "id": "heading-TwsoE3wQi9",
+ "parentId": "root",
+ "data": {
+ "content": "Lightning LoRAs"
+ },
+ "type": "heading"
+ },
+ "heading-vSOD5q5gvL": {
+ "id": "heading-vSOD5q5gvL",
+ "parentId": "root",
+ "data": {
+ "content": "Concept LoRAs"
+ },
+ "type": "heading"
+ },
+ "node-field-tpqKlLmMtv": {
+ "id": "node-field-tpqKlLmMtv",
+ "parentId": "container-lF8o60KuVb",
+ "data": {
+ "fieldIdentifier": {
+ "nodeId": "5d18cf53-905d-4eb6-afd4-6c19f790b374",
+ "fieldName": "lora"
+ },
+ "showDescription": true
+ },
+ "type": "node-field"
+ },
+ "node-field-yuWwWH1ucK": {
+ "id": "node-field-yuWwWH1ucK",
+ "parentId": "container-rvE0oy63qL",
+ "data": {
+ "fieldIdentifier": {
+ "nodeId": "eb6b4c66-0bfb-4e0d-bc39-9d8814f3cd75",
+ "fieldName": "lora"
+ },
+ "showDescription": true
+ },
+ "type": "node-field"
+ },
+ "node-field-EbS4ZMqDPZ": {
+ "id": "node-field-EbS4ZMqDPZ",
+ "type": "node-field",
+ "parentId": "root",
+ "data": {
+ "fieldIdentifier": {
+ "nodeId": "f37bd1f0-f0d8-4105-8e07-5ec484298d36",
+ "fieldName": "loras"
+ },
+ "showDescription": true
+ }
+ }
+ },
+ "rootElementId": "root"
+ },
+ "id": "default_wan22_t2v_lightning_concept"
+}
diff --git a/invokeai/app/services/workflow_records/default_workflows/Text to Video - Wan 2.2 Lightning.json b/invokeai/app/services/workflow_records/default_workflows/Text to Video - Wan 2.2 Lightning.json
new file mode 100644
index 00000000000..d4ad5af9996
--- /dev/null
+++ b/invokeai/app/services/workflow_records/default_workflows/Text to Video - Wan 2.2 Lightning.json
@@ -0,0 +1,986 @@
+{
+ "name": "Text to Video - Wan 2.2 Lightning",
+ "author": "InvokeAI",
+ "description": "Fast text-to-video generation with Wan 2.2 T2V A14B + the Lightning LoRA pair. Distillation lets the model converge in 4 steps with CFG 1.0 (no negative branch), roughly 20x faster than the standard T2V workflow at the cost of some image quality. Pick the high-noise Lightning LoRA on 'Apply LoRA (High)' and the low-noise one on 'Apply LoRA (Low)' — the 'auto' target routes each to the right expert when the LoRAs are tagged. Defaults: 832x480, 81 frames @ 16 FPS (~5 s).",
+ "version": "1.0.0",
+ "contact": "",
+ "tags": "wan2.2, text to video, video, lightning, lora",
+ "notes": "Each InvokeAI install assigns its own internal model IDs, so the model fields are left blank -- select these in the form after loading:\n\nMain Model node:\n- Main Model: Wan 2.2 T2V A14B High Noise (Q4_K_M GGUF recommended for <=16 GB VRAM)\n- Transformer (Low Noise): Wan 2.2 T2V A14B Low Noise (Q4_K_M)\n- VAE: Wan 2.2 A14B VAE\n- T5 Encoder: Wan T5 Encoder (UMT5-XXL)\n\nLightning LoRAs (4-step distillation - use the T2V variant, NOT I2V):\n- Apply Lightning LoRA (High Noise): Wan 2.2 T2V Lightning High Noise (4-step)\n- Apply Lightning LoRA (Low Noise): Wan 2.2 T2V Lightning Low Noise (4-step)\n\nThe Lightning LoRAs are A14B-specific and distinct for T2V vs I2V. If your LoRAs are untagged (no 'expert' field), use each LoRA loader's 'target' dropdown to route the first to 'high' and the second to 'low'. CFG=1.0 skips the negative-conditioning branch, so the Negative Prompt is ignored at runtime.",
+ "exposedFields": [
+ {
+ "nodeId": "1a0e6c7b-9d2f-4b3c-8e1a-2f3d4c5b6a7e",
+ "fieldName": "model"
+ },
+ {
+ "nodeId": "1a0e6c7b-9d2f-4b3c-8e1a-2f3d4c5b6a7e",
+ "fieldName": "transformer_low_noise_model"
+ },
+ {
+ "nodeId": "1a0e6c7b-9d2f-4b3c-8e1a-2f3d4c5b6a7e",
+ "fieldName": "component_source"
+ },
+ {
+ "nodeId": "1a0e6c7b-9d2f-4b3c-8e1a-2f3d4c5b6a7e",
+ "fieldName": "vae_model"
+ },
+ {
+ "nodeId": "1a0e6c7b-9d2f-4b3c-8e1a-2f3d4c5b6a7e",
+ "fieldName": "wan_t5_encoder_model"
+ },
+ {
+ "nodeId": "3466ad11-a931-4adc-b394-9ec287f0c23b",
+ "fieldName": "lora"
+ },
+ {
+ "nodeId": "3466ad11-a931-4adc-b394-9ec287f0c23b",
+ "fieldName": "weight"
+ },
+ {
+ "nodeId": "3466ad11-a931-4adc-b394-9ec287f0c23b",
+ "fieldName": "target"
+ },
+ {
+ "nodeId": "9b4cde65-699f-42c1-8889-76ac91fcb62f",
+ "fieldName": "lora"
+ },
+ {
+ "nodeId": "9b4cde65-699f-42c1-8889-76ac91fcb62f",
+ "fieldName": "weight"
+ },
+ {
+ "nodeId": "9b4cde65-699f-42c1-8889-76ac91fcb62f",
+ "fieldName": "target"
+ },
+ {
+ "nodeId": "2b1f7d8c-ae3f-5c4d-9f2b-3a4e5d6c7b8f",
+ "fieldName": "prompt"
+ },
+ {
+ "nodeId": "3c2a8e9d-bf4a-6d5e-af3c-4b5f6e7d8c9a",
+ "fieldName": "prompt"
+ },
+ {
+ "nodeId": "4d3b9faf-c05b-7e6f-bf4d-5c6a7f8e9dab",
+ "fieldName": "steps"
+ },
+ {
+ "nodeId": "4d3b9faf-c05b-7e6f-bf4d-5c6a7f8e9dab",
+ "fieldName": "guidance_scale"
+ },
+ {
+ "nodeId": "4d3b9faf-c05b-7e6f-bf4d-5c6a7f8e9dab",
+ "fieldName": "guidance_scale_low_noise"
+ },
+ {
+ "nodeId": "4d3b9faf-c05b-7e6f-bf4d-5c6a7f8e9dab",
+ "fieldName": "width"
+ },
+ {
+ "nodeId": "4d3b9faf-c05b-7e6f-bf4d-5c6a7f8e9dab",
+ "fieldName": "height"
+ },
+ {
+ "nodeId": "4d3b9faf-c05b-7e6f-bf4d-5c6a7f8e9dab",
+ "fieldName": "num_frames"
+ },
+ {
+ "nodeId": "6f5dcb1c-e27d-9fb0-d16f-7e8cbaa1fcbd",
+ "fieldName": "fps"
+ }
+ ],
+ "meta": {
+ "version": "4.0.0",
+ "category": "default"
+ },
+ "nodes": [
+ {
+ "id": "1a0e6c7b-9d2f-4b3c-8e1a-2f3d4c5b6a7e",
+ "type": "invocation",
+ "data": {
+ "id": "1a0e6c7b-9d2f-4b3c-8e1a-2f3d4c5b6a7e",
+ "version": "1.0.1",
+ "nodePack": "invokeai",
+ "label": "",
+ "notes": "",
+ "type": "wan_model_loader",
+ "inputs": {
+ "model": {
+ "name": "model",
+ "label": "Transformer (High Noise)",
+ "description": "",
+ "value": null
+ },
+ "transformer_low_noise_model": {
+ "name": "transformer_low_noise_model",
+ "label": "",
+ "description": "",
+ "value": null
+ },
+ "vae_model": {
+ "name": "vae_model",
+ "label": "",
+ "description": "",
+ "value": null
+ },
+ "wan_t5_encoder_model": {
+ "name": "wan_t5_encoder_model",
+ "label": "",
+ "description": "",
+ "value": null
+ },
+ "component_source": {
+ "name": "component_source",
+ "label": "",
+ "description": ""
+ }
+ },
+ "isOpen": true,
+ "isIntermediate": true,
+ "useCache": false
+ },
+ "position": {
+ "x": 0,
+ "y": 0
+ }
+ },
+ {
+ "id": "2b1f7d8c-ae3f-5c4d-9f2b-3a4e5d6c7b8f",
+ "type": "invocation",
+ "data": {
+ "id": "2b1f7d8c-ae3f-5c4d-9f2b-3a4e5d6c7b8f",
+ "version": "1.0.0",
+ "nodePack": "invokeai",
+ "label": "Positive Prompt",
+ "notes": "",
+ "type": "wan_text_encoder",
+ "inputs": {
+ "prompt": {
+ "name": "prompt",
+ "label": "",
+ "description": "",
+ "value": ""
+ },
+ "wan_t5_encoder": {
+ "name": "wan_t5_encoder",
+ "label": "",
+ "description": ""
+ }
+ },
+ "isOpen": true,
+ "isIntermediate": true,
+ "useCache": true
+ },
+ "position": {
+ "x": 700,
+ "y": 212
+ }
+ },
+ {
+ "id": "3c2a8e9d-bf4a-6d5e-af3c-4b5f6e7d8c9a",
+ "type": "invocation",
+ "data": {
+ "id": "3c2a8e9d-bf4a-6d5e-af3c-4b5f6e7d8c9a",
+ "version": "1.0.0",
+ "nodePack": "invokeai",
+ "label": "Negative Prompt (unused at CFG=1.0)",
+ "notes": "",
+ "type": "wan_text_encoder",
+ "inputs": {
+ "prompt": {
+ "name": "prompt",
+ "label": "",
+ "description": "",
+ "value": " "
+ },
+ "wan_t5_encoder": {
+ "name": "wan_t5_encoder",
+ "label": "",
+ "description": ""
+ }
+ },
+ "isOpen": true,
+ "isIntermediate": true,
+ "useCache": true
+ },
+ "position": {
+ "x": 700,
+ "y": 392
+ }
+ },
+ {
+ "id": "5e4cab0b-d16c-8faf-c05e-6d7baf90ebbc",
+ "type": "invocation",
+ "data": {
+ "id": "5e4cab0b-d16c-8faf-c05e-6d7baf90ebbc",
+ "version": "1.0.1",
+ "nodePack": "invokeai",
+ "label": "",
+ "notes": "",
+ "type": "rand_int",
+ "inputs": {
+ "low": {
+ "name": "low",
+ "label": "",
+ "description": "",
+ "value": 0
+ },
+ "high": {
+ "name": "high",
+ "label": "",
+ "description": "",
+ "value": 2147483647
+ }
+ },
+ "isOpen": true,
+ "isIntermediate": true,
+ "useCache": false
+ },
+ "position": {
+ "x": 700,
+ "y": 572
+ }
+ },
+ {
+ "id": "4d3b9faf-c05b-7e6f-bf4d-5c6a7f8e9dab",
+ "type": "invocation",
+ "data": {
+ "id": "4d3b9faf-c05b-7e6f-bf4d-5c6a7f8e9dab",
+ "version": "1.0.0",
+ "nodePack": "invokeai",
+ "label": "",
+ "notes": "",
+ "type": "wan_video_denoise",
+ "inputs": {
+ "transformer": {
+ "name": "transformer",
+ "label": "",
+ "description": ""
+ },
+ "positive_conditioning": {
+ "name": "positive_conditioning",
+ "label": "",
+ "description": ""
+ },
+ "negative_conditioning": {
+ "name": "negative_conditioning",
+ "label": "",
+ "description": ""
+ },
+ "ref_image": {
+ "name": "ref_image",
+ "label": "",
+ "description": ""
+ },
+ "guidance_scale": {
+ "name": "guidance_scale",
+ "label": "CFG (High Noise)",
+ "description": "",
+ "value": 1
+ },
+ "guidance_scale_low_noise": {
+ "name": "guidance_scale_low_noise",
+ "label": "CFG (Low Noise)",
+ "description": "",
+ "value": 1
+ },
+ "width": {
+ "name": "width",
+ "label": "",
+ "description": "",
+ "value": 832
+ },
+ "height": {
+ "name": "height",
+ "label": "",
+ "description": "",
+ "value": 480
+ },
+ "num_frames": {
+ "name": "num_frames",
+ "label": "Frames",
+ "description": "",
+ "value": 81
+ },
+ "steps": {
+ "name": "steps",
+ "label": "",
+ "description": "",
+ "value": 4
+ },
+ "seed": {
+ "name": "seed",
+ "label": "",
+ "description": "",
+ "value": 0
+ }
+ },
+ "isOpen": true,
+ "isIntermediate": true,
+ "useCache": true
+ },
+ "position": {
+ "x": 1050,
+ "y": 230
+ }
+ },
+ {
+ "id": "6f5dcb1c-e27d-9fb0-d16f-7e8cbaa1fcbd",
+ "type": "invocation",
+ "data": {
+ "id": "6f5dcb1c-e27d-9fb0-d16f-7e8cbaa1fcbd",
+ "version": "1.0.0",
+ "nodePack": "invokeai",
+ "label": "",
+ "notes": "",
+ "type": "wan_l2v",
+ "inputs": {
+ "board": {
+ "name": "board",
+ "label": "",
+ "description": ""
+ },
+ "metadata": {
+ "name": "metadata",
+ "label": "",
+ "description": ""
+ },
+ "latents": {
+ "name": "latents",
+ "label": "",
+ "description": ""
+ },
+ "vae": {
+ "name": "vae",
+ "label": "",
+ "description": ""
+ },
+ "fps": {
+ "name": "fps",
+ "label": "FPS",
+ "description": "",
+ "value": 16
+ }
+ },
+ "isOpen": true,
+ "isIntermediate": false,
+ "useCache": true
+ },
+ "position": {
+ "x": 1400,
+ "y": 33
+ }
+ },
+ {
+ "id": "291abec8-b996-4c7b-959c-360f56a3ed60",
+ "type": "invocation",
+ "data": {
+ "id": "291abec8-b996-4c7b-959c-360f56a3ed60",
+ "version": "1.0.1",
+ "nodePack": "invokeai",
+ "label": "Apply Lightning LoRA (High Noise)",
+ "notes": "",
+ "type": "wan_lora_loader",
+ "inputs": {
+ "lora": {
+ "name": "lora",
+ "label": "Lightning LoRA (High Noise)",
+ "description": "",
+ "value": null
+ },
+ "weight": {
+ "name": "weight",
+ "label": "",
+ "description": "",
+ "value": 1
+ },
+ "target": {
+ "name": "target",
+ "label": "",
+ "description": "",
+ "value": "high"
+ },
+ "transformer": {
+ "name": "transformer",
+ "label": "",
+ "description": ""
+ }
+ },
+ "isOpen": true,
+ "isIntermediate": true,
+ "useCache": true
+ },
+ "position": {
+ "x": 350,
+ "y": 772
+ }
+ },
+ {
+ "id": "ae8f115d-a3b8-4f76-b280-7313366a94ad",
+ "type": "invocation",
+ "data": {
+ "id": "ae8f115d-a3b8-4f76-b280-7313366a94ad",
+ "version": "1.0.1",
+ "nodePack": "invokeai",
+ "label": "",
+ "notes": "",
+ "type": "wan_lora_loader",
+ "inputs": {
+ "lora": {
+ "name": "lora",
+ "label": "Lightning LoRA (Low Noise)",
+ "description": "",
+ "value": null
+ },
+ "weight": {
+ "name": "weight",
+ "label": "",
+ "description": "",
+ "value": 1
+ },
+ "target": {
+ "name": "target",
+ "label": "",
+ "description": "",
+ "value": "low"
+ },
+ "transformer": {
+ "name": "transformer",
+ "label": "",
+ "description": ""
+ }
+ },
+ "isOpen": true,
+ "isIntermediate": true,
+ "useCache": true
+ },
+ "position": {
+ "x": 700,
+ "y": 772
+ }
+ }
+ ],
+ "edges": [
+ {
+ "id": "edge-loader-t5-pos",
+ "type": "default",
+ "source": "1a0e6c7b-9d2f-4b3c-8e1a-2f3d4c5b6a7e",
+ "target": "2b1f7d8c-ae3f-5c4d-9f2b-3a4e5d6c7b8f",
+ "sourceHandle": "wan_t5_encoder",
+ "targetHandle": "wan_t5_encoder"
+ },
+ {
+ "id": "edge-loader-t5-neg",
+ "type": "default",
+ "source": "1a0e6c7b-9d2f-4b3c-8e1a-2f3d4c5b6a7e",
+ "target": "3c2a8e9d-bf4a-6d5e-af3c-4b5f6e7d8c9a",
+ "sourceHandle": "wan_t5_encoder",
+ "targetHandle": "wan_t5_encoder"
+ },
+ {
+ "id": "edge-loader-vae-l2v",
+ "type": "default",
+ "source": "1a0e6c7b-9d2f-4b3c-8e1a-2f3d4c5b6a7e",
+ "target": "6f5dcb1c-e27d-9fb0-d16f-7e8cbaa1fcbd",
+ "sourceHandle": "vae",
+ "targetHandle": "vae"
+ },
+ {
+ "id": "edge-pos-cond-denoise",
+ "type": "default",
+ "source": "2b1f7d8c-ae3f-5c4d-9f2b-3a4e5d6c7b8f",
+ "target": "4d3b9faf-c05b-7e6f-bf4d-5c6a7f8e9dab",
+ "sourceHandle": "conditioning",
+ "targetHandle": "positive_conditioning"
+ },
+ {
+ "id": "edge-neg-cond-denoise",
+ "type": "default",
+ "source": "3c2a8e9d-bf4a-6d5e-af3c-4b5f6e7d8c9a",
+ "target": "4d3b9faf-c05b-7e6f-bf4d-5c6a7f8e9dab",
+ "sourceHandle": "conditioning",
+ "targetHandle": "negative_conditioning"
+ },
+ {
+ "id": "edge-rand-seed-denoise",
+ "type": "default",
+ "source": "5e4cab0b-d16c-8faf-c05e-6d7baf90ebbc",
+ "target": "4d3b9faf-c05b-7e6f-bf4d-5c6a7f8e9dab",
+ "sourceHandle": "value",
+ "targetHandle": "seed"
+ },
+ {
+ "id": "edge-denoise-latents-l2v",
+ "type": "default",
+ "source": "4d3b9faf-c05b-7e6f-bf4d-5c6a7f8e9dab",
+ "target": "6f5dcb1c-e27d-9fb0-d16f-7e8cbaa1fcbd",
+ "sourceHandle": "latents",
+ "targetHandle": "latents"
+ },
+ {
+ "id": "reactflow__edge-1a0e6c7b-9d2f-4b3c-8e1a-2f3d4c5b6a7etransformer-291abec8-b996-4c7b-959c-360f56a3ed60transformer",
+ "type": "default",
+ "source": "1a0e6c7b-9d2f-4b3c-8e1a-2f3d4c5b6a7e",
+ "target": "291abec8-b996-4c7b-959c-360f56a3ed60",
+ "sourceHandle": "transformer",
+ "targetHandle": "transformer"
+ },
+ {
+ "id": "reactflow__edge-291abec8-b996-4c7b-959c-360f56a3ed60transformer-ae8f115d-a3b8-4f76-b280-7313366a94adtransformer",
+ "type": "default",
+ "source": "291abec8-b996-4c7b-959c-360f56a3ed60",
+ "target": "ae8f115d-a3b8-4f76-b280-7313366a94ad",
+ "sourceHandle": "transformer",
+ "targetHandle": "transformer"
+ },
+ {
+ "id": "reactflow__edge-ae8f115d-a3b8-4f76-b280-7313366a94adtransformer-4d3b9faf-c05b-7e6f-bf4d-5c6a7f8e9dabtransformer",
+ "type": "default",
+ "source": "ae8f115d-a3b8-4f76-b280-7313366a94ad",
+ "target": "4d3b9faf-c05b-7e6f-bf4d-5c6a7f8e9dab",
+ "sourceHandle": "transformer",
+ "targetHandle": "transformer"
+ }
+ ],
+ "form": {
+ "elements": {
+ "root": {
+ "id": "root",
+ "data": {
+ "layout": "column",
+ "children": [
+ "heading-XDPaQofdEE",
+ "node-field-KcT0glAFDK",
+ "container-i8V1oJeQCP",
+ "container-mUeBcw2tYf",
+ "heading-wuPtY8B4vR",
+ "container-AkpUH1WtID",
+ "heading-TwsoE3wQi9",
+ "container-0u7wyxEdNa",
+ "heading-10PuWMKJg9",
+ "container-C0z6ru33to"
+ ]
+ },
+ "type": "container"
+ },
+ "node-field-zQA9XuRZRW": {
+ "id": "node-field-zQA9XuRZRW",
+ "parentId": "container-mUeBcw2tYf",
+ "data": {
+ "fieldIdentifier": {
+ "nodeId": "6f5dcb1c-e27d-9fb0-d16f-7e8cbaa1fcbd",
+ "fieldName": "fps"
+ },
+ "showDescription": false,
+ "settings": {
+ "type": "integer-field-config",
+ "showShuffle": false,
+ "component": "number-input"
+ }
+ },
+ "type": "node-field"
+ },
+ "node-field-ok9GStueKC": {
+ "id": "node-field-ok9GStueKC",
+ "parentId": "container-mUeBcw2tYf",
+ "data": {
+ "fieldIdentifier": {
+ "nodeId": "4d3b9faf-c05b-7e6f-bf4d-5c6a7f8e9dab",
+ "fieldName": "num_frames"
+ },
+ "showDescription": false,
+ "settings": {
+ "type": "integer-field-config",
+ "showShuffle": false,
+ "component": "number-input"
+ }
+ },
+ "type": "node-field"
+ },
+ "node-field-58JRDH0dfF": {
+ "id": "node-field-58JRDH0dfF",
+ "parentId": "container-i8V1oJeQCP",
+ "data": {
+ "fieldIdentifier": {
+ "nodeId": "4d3b9faf-c05b-7e6f-bf4d-5c6a7f8e9dab",
+ "fieldName": "height"
+ },
+ "showDescription": false,
+ "settings": {
+ "type": "integer-field-config",
+ "showShuffle": false,
+ "component": "number-input"
+ }
+ },
+ "type": "node-field"
+ },
+ "node-field-jIfJ3RUDyT": {
+ "id": "node-field-jIfJ3RUDyT",
+ "parentId": "container-i8V1oJeQCP",
+ "data": {
+ "fieldIdentifier": {
+ "nodeId": "4d3b9faf-c05b-7e6f-bf4d-5c6a7f8e9dab",
+ "fieldName": "width"
+ },
+ "showDescription": false,
+ "settings": {
+ "type": "integer-field-config",
+ "showShuffle": false,
+ "component": "number-input"
+ }
+ },
+ "type": "node-field"
+ },
+ "node-field-QDqQ4XOKvg": {
+ "id": "node-field-QDqQ4XOKvg",
+ "parentId": "container-7g1Xedgg8T",
+ "data": {
+ "fieldIdentifier": {
+ "nodeId": "4d3b9faf-c05b-7e6f-bf4d-5c6a7f8e9dab",
+ "fieldName": "guidance_scale_low_noise"
+ },
+ "showDescription": false,
+ "settings": {
+ "type": "float-field-config",
+ "showShuffle": false,
+ "component": "number-input"
+ }
+ },
+ "type": "node-field"
+ },
+ "node-field-PdU0Sz2m5z": {
+ "id": "node-field-PdU0Sz2m5z",
+ "parentId": "container-y0w7TfJC7e",
+ "data": {
+ "fieldIdentifier": {
+ "nodeId": "4d3b9faf-c05b-7e6f-bf4d-5c6a7f8e9dab",
+ "fieldName": "guidance_scale"
+ },
+ "showDescription": false,
+ "settings": {
+ "type": "float-field-config",
+ "showShuffle": false,
+ "component": "number-input"
+ }
+ },
+ "type": "node-field"
+ },
+ "node-field-UiQ7MaseDR": {
+ "id": "node-field-UiQ7MaseDR",
+ "parentId": "container-mUeBcw2tYf",
+ "data": {
+ "fieldIdentifier": {
+ "nodeId": "4d3b9faf-c05b-7e6f-bf4d-5c6a7f8e9dab",
+ "fieldName": "steps"
+ },
+ "showDescription": false,
+ "settings": {
+ "type": "integer-field-config",
+ "showShuffle": false,
+ "component": "number-input"
+ }
+ },
+ "type": "node-field"
+ },
+ "node-field-KcT0glAFDK": {
+ "id": "node-field-KcT0glAFDK",
+ "parentId": "root",
+ "data": {
+ "fieldIdentifier": {
+ "nodeId": "2b1f7d8c-ae3f-5c4d-9f2b-3a4e5d6c7b8f",
+ "fieldName": "prompt"
+ },
+ "showDescription": false,
+ "settings": {
+ "type": "string-field-config",
+ "component": "textarea"
+ }
+ },
+ "type": "node-field"
+ },
+ "node-field-N9mexEKkYg": {
+ "id": "node-field-N9mexEKkYg",
+ "parentId": "container-C0z6ru33to",
+ "data": {
+ "fieldIdentifier": {
+ "nodeId": "1a0e6c7b-9d2f-4b3c-8e1a-2f3d4c5b6a7e",
+ "fieldName": "wan_t5_encoder_model"
+ },
+ "showDescription": false
+ },
+ "type": "node-field"
+ },
+ "node-field-2u9lAbMBnD": {
+ "id": "node-field-2u9lAbMBnD",
+ "parentId": "container-C0z6ru33to",
+ "data": {
+ "fieldIdentifier": {
+ "nodeId": "1a0e6c7b-9d2f-4b3c-8e1a-2f3d4c5b6a7e",
+ "fieldName": "vae_model"
+ },
+ "showDescription": false
+ },
+ "type": "node-field"
+ },
+ "node-field-CTGqrO3x8o": {
+ "id": "node-field-CTGqrO3x8o",
+ "parentId": "container-C0z6ru33to",
+ "data": {
+ "fieldIdentifier": {
+ "nodeId": "1a0e6c7b-9d2f-4b3c-8e1a-2f3d4c5b6a7e",
+ "fieldName": "component_source"
+ },
+ "showDescription": false
+ },
+ "type": "node-field"
+ },
+ "node-field-7G9srQT2Uw": {
+ "id": "node-field-7G9srQT2Uw",
+ "parentId": "container-7g1Xedgg8T",
+ "data": {
+ "fieldIdentifier": {
+ "nodeId": "1a0e6c7b-9d2f-4b3c-8e1a-2f3d4c5b6a7e",
+ "fieldName": "transformer_low_noise_model"
+ },
+ "showDescription": false
+ },
+ "type": "node-field"
+ },
+ "node-field-Q345WyNzc4": {
+ "id": "node-field-Q345WyNzc4",
+ "parentId": "container-y0w7TfJC7e",
+ "data": {
+ "fieldIdentifier": {
+ "nodeId": "1a0e6c7b-9d2f-4b3c-8e1a-2f3d4c5b6a7e",
+ "fieldName": "model"
+ },
+ "showDescription": false
+ },
+ "type": "node-field"
+ },
+ "container-mUeBcw2tYf": {
+ "id": "container-mUeBcw2tYf",
+ "parentId": "root",
+ "data": {
+ "layout": "row",
+ "children": [
+ "node-field-UiQ7MaseDR",
+ "node-field-ok9GStueKC",
+ "node-field-zQA9XuRZRW"
+ ]
+ },
+ "type": "container"
+ },
+ "heading-XDPaQofdEE": {
+ "id": "heading-XDPaQofdEE",
+ "parentId": "root",
+ "data": {
+ "content": "Generation"
+ },
+ "type": "heading"
+ },
+ "container-i8V1oJeQCP": {
+ "id": "container-i8V1oJeQCP",
+ "parentId": "root",
+ "data": {
+ "layout": "row",
+ "children": [
+ "node-field-jIfJ3RUDyT",
+ "node-field-58JRDH0dfF"
+ ]
+ },
+ "type": "container"
+ },
+ "heading-wuPtY8B4vR": {
+ "id": "heading-wuPtY8B4vR",
+ "parentId": "root",
+ "data": {
+ "content": "Model"
+ },
+ "type": "heading"
+ },
+ "container-AkpUH1WtID": {
+ "id": "container-AkpUH1WtID",
+ "parentId": "root",
+ "data": {
+ "layout": "row",
+ "children": [
+ "container-y0w7TfJC7e",
+ "container-7g1Xedgg8T"
+ ]
+ },
+ "type": "container"
+ },
+ "container-y0w7TfJC7e": {
+ "id": "container-y0w7TfJC7e",
+ "parentId": "container-AkpUH1WtID",
+ "data": {
+ "layout": "column",
+ "children": [
+ "node-field-Q345WyNzc4",
+ "node-field-PdU0Sz2m5z"
+ ]
+ },
+ "type": "container"
+ },
+ "container-7g1Xedgg8T": {
+ "id": "container-7g1Xedgg8T",
+ "parentId": "container-AkpUH1WtID",
+ "data": {
+ "layout": "column",
+ "children": [
+ "node-field-7G9srQT2Uw",
+ "node-field-QDqQ4XOKvg"
+ ]
+ },
+ "type": "container"
+ },
+ "container-C0z6ru33to": {
+ "id": "container-C0z6ru33to",
+ "parentId": "root",
+ "data": {
+ "layout": "row",
+ "children": [
+ "node-field-N9mexEKkYg",
+ "node-field-2u9lAbMBnD",
+ "node-field-CTGqrO3x8o"
+ ]
+ },
+ "type": "container"
+ },
+ "heading-10PuWMKJg9": {
+ "id": "heading-10PuWMKJg9",
+ "parentId": "root",
+ "data": {
+ "content": "Advanced"
+ },
+ "type": "heading"
+ },
+ "container-0u7wyxEdNa": {
+ "id": "container-0u7wyxEdNa",
+ "parentId": "root",
+ "data": {
+ "layout": "row",
+ "children": [
+ "container-Um9gcFv0lo",
+ "container-4owWd0thWk"
+ ]
+ },
+ "type": "container"
+ },
+ "container-Um9gcFv0lo": {
+ "id": "container-Um9gcFv0lo",
+ "parentId": "container-0u7wyxEdNa",
+ "data": {
+ "layout": "row",
+ "children": [
+ "container-lF8o60KuVb"
+ ]
+ },
+ "type": "container"
+ },
+ "container-4owWd0thWk": {
+ "id": "container-4owWd0thWk",
+ "parentId": "container-0u7wyxEdNa",
+ "data": {
+ "layout": "row",
+ "children": [
+ "container-rvE0oy63qL"
+ ]
+ },
+ "type": "container"
+ },
+ "node-field-a8nZxgz9u9": {
+ "id": "node-field-a8nZxgz9u9",
+ "parentId": "container-lF8o60KuVb",
+ "data": {
+ "fieldIdentifier": {
+ "nodeId": "291abec8-b996-4c7b-959c-360f56a3ed60",
+ "fieldName": "lora"
+ },
+ "showDescription": true
+ },
+ "type": "node-field"
+ },
+ "node-field-NwR9yOugEz": {
+ "id": "node-field-NwR9yOugEz",
+ "parentId": "container-rvE0oy63qL",
+ "data": {
+ "fieldIdentifier": {
+ "nodeId": "ae8f115d-a3b8-4f76-b280-7313366a94ad",
+ "fieldName": "lora"
+ },
+ "showDescription": true
+ },
+ "type": "node-field"
+ },
+ "container-lF8o60KuVb": {
+ "id": "container-lF8o60KuVb",
+ "parentId": "container-Um9gcFv0lo",
+ "data": {
+ "layout": "column",
+ "children": [
+ "node-field-a8nZxgz9u9",
+ "node-field-0al3LtpcGU"
+ ]
+ },
+ "type": "container"
+ },
+ "container-rvE0oy63qL": {
+ "id": "container-rvE0oy63qL",
+ "parentId": "container-4owWd0thWk",
+ "data": {
+ "layout": "column",
+ "children": [
+ "node-field-NwR9yOugEz",
+ "node-field-lgLAjS4nks"
+ ]
+ },
+ "type": "container"
+ },
+ "node-field-0al3LtpcGU": {
+ "id": "node-field-0al3LtpcGU",
+ "parentId": "container-lF8o60KuVb",
+ "data": {
+ "fieldIdentifier": {
+ "nodeId": "291abec8-b996-4c7b-959c-360f56a3ed60",
+ "fieldName": "weight"
+ },
+ "showDescription": true,
+ "settings": {
+ "type": "float-field-config",
+ "showShuffle": false,
+ "component": "number-input"
+ }
+ },
+ "type": "node-field"
+ },
+ "node-field-lgLAjS4nks": {
+ "id": "node-field-lgLAjS4nks",
+ "parentId": "container-rvE0oy63qL",
+ "data": {
+ "fieldIdentifier": {
+ "nodeId": "ae8f115d-a3b8-4f76-b280-7313366a94ad",
+ "fieldName": "weight"
+ },
+ "showDescription": true,
+ "settings": {
+ "type": "float-field-config",
+ "showShuffle": false,
+ "component": "number-input"
+ }
+ },
+ "type": "node-field"
+ },
+ "heading-TwsoE3wQi9": {
+ "id": "heading-TwsoE3wQi9",
+ "parentId": "root",
+ "data": {
+ "content": "Lightning LoRAs"
+ },
+ "type": "heading"
+ }
+ },
+ "rootElementId": "root"
+ },
+ "id": "default_wan22_t2v_lightning"
+}
diff --git a/invokeai/app/services/workflow_records/default_workflows/Text to Video - Wan 2.2 TI2V-5B (Low Quality).json b/invokeai/app/services/workflow_records/default_workflows/Text to Video - Wan 2.2 TI2V-5B (Low Quality).json
new file mode 100644
index 00000000000..ad368524014
--- /dev/null
+++ b/invokeai/app/services/workflow_records/default_workflows/Text to Video - Wan 2.2 TI2V-5B (Low Quality).json
@@ -0,0 +1,869 @@
+{
+ "name": "Text to Video - Wan 2.2 TI2V-5B (Low Quality)",
+ "author": "InvokeAI",
+ "description": "Text-to-video generation with Wan 2.2 TI2V-5B. Produces an MP4 of `num_frames` frames at `fps` frames-per-second (defaults: 81 frames @ 16 FPS = ~5 s, 720p ~1248x704). The output lands in the gallery alongside images and plays inline in the viewer.",
+ "version": "1.0.0",
+ "contact": "",
+ "tags": "wan2.2, text to video, video",
+ "notes": "This workflow needs the Wan 2.2 TI2V-5B model. Each InvokeAI install assigns its own internal model IDs, so the model fields are left blank -- select these in the form after loading:\n\nMain Model node:\n- Main Model: Wan 2.2 TI2V-5B (Q4_K_M GGUF recommended for <=16 GB VRAM)\n- Transformer (Low Noise): leave empty (TI2V-5B is a single-expert model)\n- VAE: Wan 2.2 TI2V-5B VAE\n- T5 Encoder: Wan T5 Encoder (UMT5-XXL)\n\nDefault output is 720p (~1248x704); reduce the \"Ideal Dimensions\" preset to 480p for less VRAM.",
+ "exposedFields": [
+ {
+ "nodeId": "1a0e6c7b-9d2f-4b3c-8e1a-2f3d4c5b6a7e",
+ "fieldName": "model"
+ },
+ {
+ "nodeId": "1a0e6c7b-9d2f-4b3c-8e1a-2f3d4c5b6a7e",
+ "fieldName": "transformer_low_noise_model"
+ },
+ {
+ "nodeId": "1a0e6c7b-9d2f-4b3c-8e1a-2f3d4c5b6a7e",
+ "fieldName": "component_source"
+ },
+ {
+ "nodeId": "1a0e6c7b-9d2f-4b3c-8e1a-2f3d4c5b6a7e",
+ "fieldName": "vae_model"
+ },
+ {
+ "nodeId": "1a0e6c7b-9d2f-4b3c-8e1a-2f3d4c5b6a7e",
+ "fieldName": "wan_t5_encoder_model"
+ },
+ {
+ "nodeId": "2b1f7d8c-ae3f-5c4d-9f2b-3a4e5d6c7b8f",
+ "fieldName": "prompt"
+ },
+ {
+ "nodeId": "3c2a8e9d-bf4a-6d5e-af3c-4b5f6e7d8c9a",
+ "fieldName": "prompt"
+ },
+ {
+ "nodeId": "4d3b9faf-c05b-7e6f-bf4d-5c6a7f8e9dab",
+ "fieldName": "steps"
+ },
+ {
+ "nodeId": "4d3b9faf-c05b-7e6f-bf4d-5c6a7f8e9dab",
+ "fieldName": "guidance_scale"
+ },
+ {
+ "nodeId": "4d3b9faf-c05b-7e6f-bf4d-5c6a7f8e9dab",
+ "fieldName": "guidance_scale_low_noise"
+ },
+ {
+ "nodeId": "4d3b9faf-c05b-7e6f-bf4d-5c6a7f8e9dab",
+ "fieldName": "width"
+ },
+ {
+ "nodeId": "4d3b9faf-c05b-7e6f-bf4d-5c6a7f8e9dab",
+ "fieldName": "height"
+ },
+ {
+ "nodeId": "4d3b9faf-c05b-7e6f-bf4d-5c6a7f8e9dab",
+ "fieldName": "num_frames"
+ },
+ {
+ "nodeId": "6f5dcb1c-e27d-9fb0-d16f-7e8cbaa1fcbd",
+ "fieldName": "fps"
+ }
+ ],
+ "meta": {
+ "version": "4.0.0",
+ "category": "default"
+ },
+ "nodes": [
+ {
+ "id": "1a0e6c7b-9d2f-4b3c-8e1a-2f3d4c5b6a7e",
+ "type": "invocation",
+ "data": {
+ "id": "1a0e6c7b-9d2f-4b3c-8e1a-2f3d4c5b6a7e",
+ "version": "1.0.1",
+ "nodePack": "invokeai",
+ "label": "",
+ "notes": "",
+ "type": "wan_model_loader",
+ "inputs": {
+ "model": {
+ "name": "model",
+ "label": "",
+ "description": "",
+ "value": null
+ },
+ "transformer_low_noise_model": {
+ "name": "transformer_low_noise_model",
+ "label": "",
+ "description": ""
+ },
+ "vae_model": {
+ "name": "vae_model",
+ "label": "",
+ "description": "",
+ "value": null
+ },
+ "wan_t5_encoder_model": {
+ "name": "wan_t5_encoder_model",
+ "label": "",
+ "description": "",
+ "value": null
+ },
+ "component_source": {
+ "name": "component_source",
+ "label": "",
+ "description": ""
+ }
+ },
+ "isOpen": true,
+ "isIntermediate": true,
+ "useCache": false
+ },
+ "position": {
+ "x": 200,
+ "y": 0
+ }
+ },
+ {
+ "id": "2b1f7d8c-ae3f-5c4d-9f2b-3a4e5d6c7b8f",
+ "type": "invocation",
+ "data": {
+ "id": "2b1f7d8c-ae3f-5c4d-9f2b-3a4e5d6c7b8f",
+ "version": "1.0.0",
+ "nodePack": "invokeai",
+ "label": "Positive Prompt",
+ "notes": "",
+ "type": "wan_text_encoder",
+ "inputs": {
+ "prompt": {
+ "name": "prompt",
+ "label": "Positive Prompt",
+ "description": "",
+ "value": ""
+ },
+ "wan_t5_encoder": {
+ "name": "wan_t5_encoder",
+ "label": "",
+ "description": ""
+ }
+ },
+ "isOpen": true,
+ "isIntermediate": true,
+ "useCache": true
+ },
+ "position": {
+ "x": 700,
+ "y": -200
+ }
+ },
+ {
+ "id": "3c2a8e9d-bf4a-6d5e-af3c-4b5f6e7d8c9a",
+ "type": "invocation",
+ "data": {
+ "id": "3c2a8e9d-bf4a-6d5e-af3c-4b5f6e7d8c9a",
+ "version": "1.0.0",
+ "nodePack": "invokeai",
+ "label": "Negative Prompt",
+ "notes": "",
+ "type": "wan_text_encoder",
+ "inputs": {
+ "prompt": {
+ "name": "prompt",
+ "label": "Negative Prompt",
+ "description": "",
+ "value": ""
+ },
+ "wan_t5_encoder": {
+ "name": "wan_t5_encoder",
+ "label": "",
+ "description": ""
+ }
+ },
+ "isOpen": true,
+ "isIntermediate": true,
+ "useCache": true
+ },
+ "position": {
+ "x": 683.3585813389434,
+ "y": 19.353124950263965
+ }
+ },
+ {
+ "id": "5e4cab0b-d16c-8faf-c05e-6d7baf90ebbc",
+ "type": "invocation",
+ "data": {
+ "id": "5e4cab0b-d16c-8faf-c05e-6d7baf90ebbc",
+ "version": "1.0.1",
+ "nodePack": "invokeai",
+ "label": "",
+ "notes": "",
+ "type": "rand_int",
+ "inputs": {
+ "low": {
+ "name": "low",
+ "label": "",
+ "description": "",
+ "value": 0
+ },
+ "high": {
+ "name": "high",
+ "label": "",
+ "description": "",
+ "value": 2147483647
+ }
+ },
+ "isOpen": true,
+ "isIntermediate": true,
+ "useCache": false
+ },
+ "position": {
+ "x": 382.5329363121502,
+ "y": 709.7864089212082
+ }
+ },
+ {
+ "id": "4d3b9faf-c05b-7e6f-bf4d-5c6a7f8e9dab",
+ "type": "invocation",
+ "data": {
+ "id": "4d3b9faf-c05b-7e6f-bf4d-5c6a7f8e9dab",
+ "version": "1.0.0",
+ "nodePack": "invokeai",
+ "label": "",
+ "notes": "",
+ "type": "wan_video_denoise",
+ "inputs": {
+ "transformer": {
+ "name": "transformer",
+ "label": "",
+ "description": ""
+ },
+ "positive_conditioning": {
+ "name": "positive_conditioning",
+ "label": "",
+ "description": ""
+ },
+ "negative_conditioning": {
+ "name": "negative_conditioning",
+ "label": "",
+ "description": ""
+ },
+ "ref_image": {
+ "name": "ref_image",
+ "label": "",
+ "description": ""
+ },
+ "guidance_scale": {
+ "name": "guidance_scale",
+ "label": "CFG",
+ "description": "",
+ "value": 5
+ },
+ "guidance_scale_low_noise": {
+ "name": "guidance_scale_low_noise",
+ "label": "CFG (Low)",
+ "description": "",
+ "value": 4
+ },
+ "width": {
+ "name": "width",
+ "label": "",
+ "description": "",
+ "value": 720
+ },
+ "height": {
+ "name": "height",
+ "label": "",
+ "description": "",
+ "value": 480
+ },
+ "num_frames": {
+ "name": "num_frames",
+ "label": "Frames",
+ "description": "",
+ "value": 81
+ },
+ "steps": {
+ "name": "steps",
+ "label": "",
+ "description": "",
+ "value": 20
+ },
+ "seed": {
+ "name": "seed",
+ "label": "",
+ "description": "",
+ "value": 0
+ }
+ },
+ "isOpen": true,
+ "isIntermediate": true,
+ "useCache": true
+ },
+ "position": {
+ "x": 1165.285565516453,
+ "y": -39.7591269778113
+ }
+ },
+ {
+ "id": "6f5dcb1c-e27d-9fb0-d16f-7e8cbaa1fcbd",
+ "type": "invocation",
+ "data": {
+ "id": "6f5dcb1c-e27d-9fb0-d16f-7e8cbaa1fcbd",
+ "version": "1.0.0",
+ "nodePack": "invokeai",
+ "label": "",
+ "notes": "",
+ "type": "wan_l2v",
+ "inputs": {
+ "board": {
+ "name": "board",
+ "label": "",
+ "description": ""
+ },
+ "metadata": {
+ "name": "metadata",
+ "label": "",
+ "description": ""
+ },
+ "latents": {
+ "name": "latents",
+ "label": "",
+ "description": ""
+ },
+ "vae": {
+ "name": "vae",
+ "label": "",
+ "description": ""
+ },
+ "fps": {
+ "name": "fps",
+ "label": "FPS",
+ "description": "",
+ "value": 16
+ }
+ },
+ "isOpen": true,
+ "isIntermediate": false,
+ "useCache": true
+ },
+ "position": {
+ "x": 1531.0370931112793,
+ "y": -63.90613171839515
+ }
+ },
+ {
+ "id": "0cc328ea-b3bf-4ac1-98f3-844de0e072c8",
+ "type": "invocation",
+ "data": {
+ "id": "0cc328ea-b3bf-4ac1-98f3-844de0e072c8",
+ "type": "integer",
+ "version": "1.0.1",
+ "label": "",
+ "notes": "",
+ "isOpen": true,
+ "isIntermediate": true,
+ "useCache": true,
+ "nodePack": "invokeai",
+ "inputs": {
+ "value": {
+ "name": "value",
+ "label": "Width",
+ "description": "Width of the video. Will be adjusted to meet the constraints of the selected target resolution.",
+ "value": 832
+ }
+ }
+ },
+ "position": {
+ "x": 370.6719737658655,
+ "y": 436.76341584866606
+ }
+ },
+ {
+ "id": "e20a326b-a312-4cc4-89cf-cfc13887cd4a",
+ "type": "invocation",
+ "data": {
+ "id": "e20a326b-a312-4cc4-89cf-cfc13887cd4a",
+ "type": "integer",
+ "version": "1.0.1",
+ "label": "",
+ "notes": "",
+ "isOpen": true,
+ "isIntermediate": true,
+ "useCache": true,
+ "nodePack": "invokeai",
+ "inputs": {
+ "value": {
+ "name": "value",
+ "label": "Height",
+ "description": "Height of the video. Will be adjusted to meet the constraints of the selected target resolution.",
+ "value": 480
+ }
+ }
+ },
+ "position": {
+ "x": 373.58106252303446,
+ "y": 567.1240792266004
+ }
+ },
+ {
+ "id": "0f05c4e0-f92f-4b23-9a57-c69ccc50c2cd",
+ "type": "invocation",
+ "data": {
+ "id": "0f05c4e0-f92f-4b23-9a57-c69ccc50c2cd",
+ "type": "wan_ti2v_ideal_dimensions",
+ "version": "1.0.0",
+ "label": "",
+ "notes": "",
+ "isOpen": true,
+ "isIntermediate": true,
+ "useCache": true,
+ "nodePack": "invokeai",
+ "inputs": {
+ "width": {
+ "name": "width",
+ "label": "",
+ "description": "",
+ "value": 1024
+ },
+ "height": {
+ "name": "height",
+ "label": "",
+ "description": "",
+ "value": 480
+ },
+ "target_resolution": {
+ "name": "target_resolution",
+ "label": "",
+ "description": "",
+ "value": "720p"
+ },
+ "rounding": {
+ "name": "rounding",
+ "label": "",
+ "description": "",
+ "value": "nearest"
+ }
+ }
+ },
+ "position": {
+ "x": 773.8083794848407,
+ "y": 420.1537431373451
+ }
+ }
+ ],
+ "edges": [
+ {
+ "id": "edge-loader-transformer-denoise",
+ "type": "default",
+ "source": "1a0e6c7b-9d2f-4b3c-8e1a-2f3d4c5b6a7e",
+ "target": "4d3b9faf-c05b-7e6f-bf4d-5c6a7f8e9dab",
+ "sourceHandle": "transformer",
+ "targetHandle": "transformer"
+ },
+ {
+ "id": "edge-loader-t5-pos",
+ "type": "default",
+ "source": "1a0e6c7b-9d2f-4b3c-8e1a-2f3d4c5b6a7e",
+ "target": "2b1f7d8c-ae3f-5c4d-9f2b-3a4e5d6c7b8f",
+ "sourceHandle": "wan_t5_encoder",
+ "targetHandle": "wan_t5_encoder"
+ },
+ {
+ "id": "edge-loader-t5-neg",
+ "type": "default",
+ "source": "1a0e6c7b-9d2f-4b3c-8e1a-2f3d4c5b6a7e",
+ "target": "3c2a8e9d-bf4a-6d5e-af3c-4b5f6e7d8c9a",
+ "sourceHandle": "wan_t5_encoder",
+ "targetHandle": "wan_t5_encoder"
+ },
+ {
+ "id": "edge-loader-vae-l2v",
+ "type": "default",
+ "source": "1a0e6c7b-9d2f-4b3c-8e1a-2f3d4c5b6a7e",
+ "target": "6f5dcb1c-e27d-9fb0-d16f-7e8cbaa1fcbd",
+ "sourceHandle": "vae",
+ "targetHandle": "vae"
+ },
+ {
+ "id": "edge-pos-cond-denoise",
+ "type": "default",
+ "source": "2b1f7d8c-ae3f-5c4d-9f2b-3a4e5d6c7b8f",
+ "target": "4d3b9faf-c05b-7e6f-bf4d-5c6a7f8e9dab",
+ "sourceHandle": "conditioning",
+ "targetHandle": "positive_conditioning"
+ },
+ {
+ "id": "edge-neg-cond-denoise",
+ "type": "default",
+ "source": "3c2a8e9d-bf4a-6d5e-af3c-4b5f6e7d8c9a",
+ "target": "4d3b9faf-c05b-7e6f-bf4d-5c6a7f8e9dab",
+ "sourceHandle": "conditioning",
+ "targetHandle": "negative_conditioning"
+ },
+ {
+ "id": "edge-rand-seed-denoise",
+ "type": "default",
+ "source": "5e4cab0b-d16c-8faf-c05e-6d7baf90ebbc",
+ "target": "4d3b9faf-c05b-7e6f-bf4d-5c6a7f8e9dab",
+ "sourceHandle": "value",
+ "targetHandle": "seed"
+ },
+ {
+ "id": "edge-denoise-latents-l2v",
+ "type": "default",
+ "source": "4d3b9faf-c05b-7e6f-bf4d-5c6a7f8e9dab",
+ "target": "6f5dcb1c-e27d-9fb0-d16f-7e8cbaa1fcbd",
+ "sourceHandle": "latents",
+ "targetHandle": "latents"
+ },
+ {
+ "id": "reactflow__edge-0cc328ea-b3bf-4ac1-98f3-844de0e072c8value-0f05c4e0-f92f-4b23-9a57-c69ccc50c2cdwidth",
+ "type": "default",
+ "source": "0cc328ea-b3bf-4ac1-98f3-844de0e072c8",
+ "target": "0f05c4e0-f92f-4b23-9a57-c69ccc50c2cd",
+ "sourceHandle": "value",
+ "targetHandle": "width"
+ },
+ {
+ "id": "reactflow__edge-0f05c4e0-f92f-4b23-9a57-c69ccc50c2cdwidth-4d3b9faf-c05b-7e6f-bf4d-5c6a7f8e9dabwidth",
+ "type": "default",
+ "source": "0f05c4e0-f92f-4b23-9a57-c69ccc50c2cd",
+ "target": "4d3b9faf-c05b-7e6f-bf4d-5c6a7f8e9dab",
+ "sourceHandle": "width",
+ "targetHandle": "width"
+ },
+ {
+ "id": "reactflow__edge-e20a326b-a312-4cc4-89cf-cfc13887cd4avalue-0f05c4e0-f92f-4b23-9a57-c69ccc50c2cdheight",
+ "type": "default",
+ "source": "e20a326b-a312-4cc4-89cf-cfc13887cd4a",
+ "target": "0f05c4e0-f92f-4b23-9a57-c69ccc50c2cd",
+ "sourceHandle": "value",
+ "targetHandle": "height"
+ },
+ {
+ "id": "reactflow__edge-0f05c4e0-f92f-4b23-9a57-c69ccc50c2cdheight-4d3b9faf-c05b-7e6f-bf4d-5c6a7f8e9dabheight",
+ "type": "default",
+ "source": "0f05c4e0-f92f-4b23-9a57-c69ccc50c2cd",
+ "target": "4d3b9faf-c05b-7e6f-bf4d-5c6a7f8e9dab",
+ "sourceHandle": "height",
+ "targetHandle": "height"
+ }
+ ],
+ "form": {
+ "elements": {
+ "root": {
+ "id": "root",
+ "data": {
+ "layout": "column",
+ "children": [
+ "heading-NVsTvi64mE",
+ "node-field-LCSk79Cykw",
+ "node-field-8eaiK9h1E6",
+ "node-field-z8AjtazZ1C",
+ "container-yTCxecjcVW",
+ "container-tL8WPBUtAl",
+ "container-WfDQ0EqgcT",
+ "divider-dWeIfMmqLs",
+ "heading-4KzzU0571E",
+ "container-ZPG1DkfsBF",
+ "container-LtEu7q4UHX"
+ ]
+ },
+ "type": "container"
+ },
+ "node-field-mz64Xfk59R": {
+ "id": "node-field-mz64Xfk59R",
+ "parentId": "container-tL8WPBUtAl",
+ "data": {
+ "fieldIdentifier": {
+ "nodeId": "6f5dcb1c-e27d-9fb0-d16f-7e8cbaa1fcbd",
+ "fieldName": "fps"
+ },
+ "showDescription": false,
+ "settings": {
+ "type": "integer-field-config",
+ "showShuffle": false,
+ "component": "number-input"
+ }
+ },
+ "type": "node-field"
+ },
+ "node-field-dADhnNIONx": {
+ "id": "node-field-dADhnNIONx",
+ "parentId": "container-tL8WPBUtAl",
+ "data": {
+ "fieldIdentifier": {
+ "nodeId": "4d3b9faf-c05b-7e6f-bf4d-5c6a7f8e9dab",
+ "fieldName": "num_frames"
+ },
+ "showDescription": false,
+ "settings": {
+ "type": "integer-field-config",
+ "showShuffle": false,
+ "component": "number-input"
+ }
+ },
+ "type": "node-field"
+ },
+ "node-field-GM02XkVswM": {
+ "id": "node-field-GM02XkVswM",
+ "parentId": "container-WfDQ0EqgcT",
+ "data": {
+ "fieldIdentifier": {
+ "nodeId": "4d3b9faf-c05b-7e6f-bf4d-5c6a7f8e9dab",
+ "fieldName": "guidance_scale"
+ },
+ "showDescription": false,
+ "settings": {
+ "type": "float-field-config",
+ "showShuffle": false,
+ "component": "number-input"
+ }
+ },
+ "type": "node-field"
+ },
+ "node-field-RN6aZuubTt": {
+ "id": "node-field-RN6aZuubTt",
+ "parentId": "container-WfDQ0EqgcT",
+ "data": {
+ "fieldIdentifier": {
+ "nodeId": "4d3b9faf-c05b-7e6f-bf4d-5c6a7f8e9dab",
+ "fieldName": "steps"
+ },
+ "showDescription": false,
+ "settings": {
+ "type": "integer-field-config",
+ "showShuffle": false,
+ "component": "number-input"
+ }
+ },
+ "type": "node-field"
+ },
+ "node-field-8eaiK9h1E6": {
+ "id": "node-field-8eaiK9h1E6",
+ "parentId": "root",
+ "data": {
+ "fieldIdentifier": {
+ "nodeId": "3c2a8e9d-bf4a-6d5e-af3c-4b5f6e7d8c9a",
+ "fieldName": "prompt"
+ },
+ "showDescription": false,
+ "settings": {
+ "type": "string-field-config",
+ "component": "textarea"
+ }
+ },
+ "type": "node-field"
+ },
+ "node-field-LCSk79Cykw": {
+ "id": "node-field-LCSk79Cykw",
+ "parentId": "root",
+ "data": {
+ "fieldIdentifier": {
+ "nodeId": "2b1f7d8c-ae3f-5c4d-9f2b-3a4e5d6c7b8f",
+ "fieldName": "prompt"
+ },
+ "showDescription": false,
+ "settings": {
+ "type": "string-field-config",
+ "component": "textarea"
+ }
+ },
+ "type": "node-field"
+ },
+ "node-field-tmweV5koO8": {
+ "id": "node-field-tmweV5koO8",
+ "parentId": "container-YUjkEQOaZH",
+ "data": {
+ "fieldIdentifier": {
+ "nodeId": "1a0e6c7b-9d2f-4b3c-8e1a-2f3d4c5b6a7e",
+ "fieldName": "wan_t5_encoder_model"
+ },
+ "showDescription": false
+ },
+ "type": "node-field"
+ },
+ "node-field-Ny6XBv2KCR": {
+ "id": "node-field-Ny6XBv2KCR",
+ "parentId": "container-LtEu7q4UHX",
+ "data": {
+ "fieldIdentifier": {
+ "nodeId": "1a0e6c7b-9d2f-4b3c-8e1a-2f3d4c5b6a7e",
+ "fieldName": "vae_model"
+ },
+ "showDescription": false
+ },
+ "type": "node-field"
+ },
+ "node-field-zmxixZsriN": {
+ "id": "node-field-zmxixZsriN",
+ "parentId": "container-LtEu7q4UHX",
+ "data": {
+ "fieldIdentifier": {
+ "nodeId": "1a0e6c7b-9d2f-4b3c-8e1a-2f3d4c5b6a7e",
+ "fieldName": "component_source"
+ },
+ "showDescription": false
+ },
+ "type": "node-field"
+ },
+ "node-field-FnSN39ojJs": {
+ "id": "node-field-FnSN39ojJs",
+ "parentId": "container-YUjkEQOaZH",
+ "data": {
+ "fieldIdentifier": {
+ "nodeId": "1a0e6c7b-9d2f-4b3c-8e1a-2f3d4c5b6a7e",
+ "fieldName": "model"
+ },
+ "showDescription": false
+ },
+ "type": "node-field"
+ },
+ "heading-NVsTvi64mE": {
+ "id": "heading-NVsTvi64mE",
+ "parentId": "root",
+ "type": "heading",
+ "data": {
+ "content": "Generation"
+ }
+ },
+ "container-WfDQ0EqgcT": {
+ "id": "container-WfDQ0EqgcT",
+ "parentId": "root",
+ "type": "container",
+ "data": {
+ "layout": "row",
+ "children": [
+ "node-field-RN6aZuubTt",
+ "node-field-GM02XkVswM"
+ ]
+ }
+ },
+ "container-bOWWWaSIsY": {
+ "id": "container-bOWWWaSIsY",
+ "parentId": "container-yTCxecjcVW",
+ "type": "container",
+ "data": {
+ "layout": "row",
+ "children": [
+ "node-field-41iwUAOdbd",
+ "node-field-C8Yua39ccI"
+ ]
+ }
+ },
+ "container-yTCxecjcVW": {
+ "id": "container-yTCxecjcVW",
+ "parentId": "root",
+ "type": "container",
+ "data": {
+ "layout": "row",
+ "children": [
+ "container-bOWWWaSIsY"
+ ]
+ }
+ },
+ "container-tL8WPBUtAl": {
+ "id": "container-tL8WPBUtAl",
+ "parentId": "root",
+ "type": "container",
+ "data": {
+ "layout": "row",
+ "children": [
+ "node-field-dADhnNIONx",
+ "node-field-mz64Xfk59R"
+ ]
+ }
+ },
+ "heading-4KzzU0571E": {
+ "id": "heading-4KzzU0571E",
+ "parentId": "root",
+ "type": "heading",
+ "data": {
+ "content": "Model"
+ }
+ },
+ "divider-dWeIfMmqLs": {
+ "id": "divider-dWeIfMmqLs",
+ "parentId": "root",
+ "type": "divider"
+ },
+ "container-YUjkEQOaZH": {
+ "id": "container-YUjkEQOaZH",
+ "parentId": "container-ZPG1DkfsBF",
+ "type": "container",
+ "data": {
+ "layout": "row",
+ "children": [
+ "node-field-FnSN39ojJs",
+ "node-field-tmweV5koO8"
+ ]
+ }
+ },
+ "container-ZPG1DkfsBF": {
+ "id": "container-ZPG1DkfsBF",
+ "parentId": "root",
+ "type": "container",
+ "data": {
+ "layout": "row",
+ "children": [
+ "container-YUjkEQOaZH"
+ ]
+ }
+ },
+ "container-LtEu7q4UHX": {
+ "id": "container-LtEu7q4UHX",
+ "parentId": "root",
+ "type": "container",
+ "data": {
+ "layout": "row",
+ "children": [
+ "node-field-Ny6XBv2KCR",
+ "node-field-zmxixZsriN"
+ ]
+ }
+ },
+ "node-field-z8AjtazZ1C": {
+ "id": "node-field-z8AjtazZ1C",
+ "type": "node-field",
+ "parentId": "root",
+ "data": {
+ "fieldIdentifier": {
+ "nodeId": "0f05c4e0-f92f-4b23-9a57-c69ccc50c2cd",
+ "fieldName": "target_resolution"
+ },
+ "showDescription": true
+ }
+ },
+ "node-field-41iwUAOdbd": {
+ "id": "node-field-41iwUAOdbd",
+ "type": "node-field",
+ "parentId": "container-bOWWWaSIsY",
+ "data": {
+ "fieldIdentifier": {
+ "nodeId": "0cc328ea-b3bf-4ac1-98f3-844de0e072c8",
+ "fieldName": "value"
+ },
+ "settings": {
+ "type": "integer-field-config",
+ "showShuffle": false,
+ "component": "number-input"
+ },
+ "showDescription": true
+ }
+ },
+ "node-field-C8Yua39ccI": {
+ "id": "node-field-C8Yua39ccI",
+ "type": "node-field",
+ "parentId": "container-bOWWWaSIsY",
+ "data": {
+ "fieldIdentifier": {
+ "nodeId": "e20a326b-a312-4cc4-89cf-cfc13887cd4a",
+ "fieldName": "value"
+ },
+ "settings": {
+ "type": "integer-field-config",
+ "showShuffle": false,
+ "component": "number-input"
+ },
+ "showDescription": true
+ }
+ }
+ },
+ "rootElementId": "root"
+ },
+ "id": "default_wan22_t2v_ti2v5b"
+}
diff --git a/invokeai/app/services/workflow_records/default_workflows/Wan 2.2 Image to Image.json b/invokeai/app/services/workflow_records/default_workflows/Wan 2.2 Image to Image.json
new file mode 100644
index 00000000000..f3bebb7faa9
--- /dev/null
+++ b/invokeai/app/services/workflow_records/default_workflows/Wan 2.2 Image to Image.json
@@ -0,0 +1,307 @@
+{
+ "id": "default_wan22_i2v_c2d5e1b3-3e4f-5b6c-af7d-8e9f0a1b2c3e",
+ "name": "Image to Image - Wan 2.2",
+ "author": "InvokeAI",
+ "description": "Image-to-image generation with Wan 2.2 I2V A14B. The reference image is VAE-encoded and concatenated to the noise latents each step (the I2V transformer has in_channels=36). Drop a reference image into the 'Reference Image' input, then invoke. Only the I2V A14B variant is supported — T2V and TI2V-5B don't consume reference images.",
+ "version": "1.0.0",
+ "contact": "",
+ "tags": "wan2.2, image to image",
+ "notes": "Prerequisite model downloads: a Wan 2.2 I2V A14B main (Diffusers or GGUF expert pair). For GGUF mains, also install the Component Source (Diffusers Wan I2V) OR the standalone Wan VAE + UMT5-XXL encoder. Wan 2.2 I2V was trained for video — at single-frame inference it tends to anchor strongly to the reference. Recommended settings: 30-40 steps and CFG 5-7 (or 4 steps and CFG 1 with the Wan I2V Lightning LoRA pair).",
+ "exposedFields": [
+ {
+ "nodeId": "1a0e6c7b-9d2f-4b3c-8e1a-2f3d4c5b6a7e",
+ "fieldName": "model"
+ },
+ {
+ "nodeId": "1a0e6c7b-9d2f-4b3c-8e1a-2f3d4c5b6a7e",
+ "fieldName": "transformer_low_noise_model"
+ },
+ {
+ "nodeId": "1a0e6c7b-9d2f-4b3c-8e1a-2f3d4c5b6a7e",
+ "fieldName": "component_source"
+ },
+ {
+ "nodeId": "1a0e6c7b-9d2f-4b3c-8e1a-2f3d4c5b6a7e",
+ "fieldName": "vae_model"
+ },
+ {
+ "nodeId": "1a0e6c7b-9d2f-4b3c-8e1a-2f3d4c5b6a7e",
+ "fieldName": "wan_t5_encoder_model"
+ },
+ {
+ "nodeId": "7a6edc2d-f38e-a0c1-e27a-8f9dcbb20fce",
+ "fieldName": "image"
+ },
+ {
+ "nodeId": "2b1f7d8c-ae3f-5c4d-9f2b-3a4e5d6c7b8f",
+ "fieldName": "prompt"
+ },
+ {
+ "nodeId": "3c2a8e9d-bf4a-6d5e-af3c-4b5f6e7d8c9a",
+ "fieldName": "prompt"
+ },
+ {
+ "nodeId": "4d3b9faf-c05b-7e6f-bf4d-5c6a7f8e9dab",
+ "fieldName": "steps"
+ },
+ {
+ "nodeId": "4d3b9faf-c05b-7e6f-bf4d-5c6a7f8e9dab",
+ "fieldName": "guidance_scale"
+ },
+ {
+ "nodeId": "4d3b9faf-c05b-7e6f-bf4d-5c6a7f8e9dab",
+ "fieldName": "guidance_scale_low_noise"
+ },
+ {
+ "nodeId": "4d3b9faf-c05b-7e6f-bf4d-5c6a7f8e9dab",
+ "fieldName": "width"
+ },
+ {
+ "nodeId": "4d3b9faf-c05b-7e6f-bf4d-5c6a7f8e9dab",
+ "fieldName": "height"
+ }
+ ],
+ "meta": {
+ "version": "3.0.0",
+ "category": "default"
+ },
+ "nodes": [
+ {
+ "id": "1a0e6c7b-9d2f-4b3c-8e1a-2f3d4c5b6a7e",
+ "type": "invocation",
+ "data": {
+ "id": "1a0e6c7b-9d2f-4b3c-8e1a-2f3d4c5b6a7e",
+ "type": "wan_model_loader",
+ "version": "1.0.1",
+ "label": "",
+ "notes": "",
+ "isOpen": true,
+ "isIntermediate": true,
+ "useCache": false,
+ "inputs": {
+ "model": { "name": "model", "label": "" },
+ "transformer_low_noise_model": { "name": "transformer_low_noise_model", "label": "" },
+ "vae_model": { "name": "vae_model", "label": "" },
+ "wan_t5_encoder_model": { "name": "wan_t5_encoder_model", "label": "" },
+ "component_source": { "name": "component_source", "label": "" }
+ }
+ },
+ "position": { "x": 200, "y": 0 }
+ },
+ {
+ "id": "2b1f7d8c-ae3f-5c4d-9f2b-3a4e5d6c7b8f",
+ "type": "invocation",
+ "data": {
+ "id": "2b1f7d8c-ae3f-5c4d-9f2b-3a4e5d6c7b8f",
+ "type": "wan_text_encoder",
+ "version": "1.0.0",
+ "label": "Positive Prompt",
+ "notes": "",
+ "isOpen": true,
+ "isIntermediate": true,
+ "useCache": true,
+ "inputs": {
+ "prompt": { "name": "prompt", "label": "", "value": "" },
+ "wan_t5_encoder": { "name": "wan_t5_encoder", "label": "" }
+ }
+ },
+ "position": { "x": 700, "y": -200 }
+ },
+ {
+ "id": "3c2a8e9d-bf4a-6d5e-af3c-4b5f6e7d8c9a",
+ "type": "invocation",
+ "data": {
+ "id": "3c2a8e9d-bf4a-6d5e-af3c-4b5f6e7d8c9a",
+ "type": "wan_text_encoder",
+ "version": "1.0.0",
+ "label": "Negative Prompt",
+ "notes": "",
+ "isOpen": true,
+ "isIntermediate": true,
+ "useCache": true,
+ "inputs": {
+ "prompt": { "name": "prompt", "label": "", "value": " " },
+ "wan_t5_encoder": { "name": "wan_t5_encoder", "label": "" }
+ }
+ },
+ "position": { "x": 700, "y": 100 }
+ },
+ {
+ "id": "7a6edc2d-f38e-a0c1-e27a-8f9dcbb20fce",
+ "type": "invocation",
+ "data": {
+ "id": "7a6edc2d-f38e-a0c1-e27a-8f9dcbb20fce",
+ "type": "wan_ref_image_encoder",
+ "version": "1.2.0",
+ "label": "",
+ "notes": "",
+ "isOpen": true,
+ "isIntermediate": true,
+ "useCache": true,
+ "inputs": {
+ "image": { "name": "image", "label": "Reference Image" },
+ "end_image": { "name": "end_image", "label": "" },
+ "num_frames": { "name": "num_frames", "label": "", "value": 1 },
+ "vae": { "name": "vae", "label": "" },
+ "width": { "name": "width", "label": "", "value": 1024 },
+ "height": { "name": "height", "label": "", "value": 1024 }
+ }
+ },
+ "position": { "x": 700, "y": 300 }
+ },
+ {
+ "id": "5e4cab0b-d16c-8faf-c05e-6d7baf90ebbc",
+ "type": "invocation",
+ "data": {
+ "id": "5e4cab0b-d16c-8faf-c05e-6d7baf90ebbc",
+ "type": "rand_int",
+ "version": "1.0.1",
+ "label": "",
+ "notes": "",
+ "isOpen": true,
+ "isIntermediate": true,
+ "useCache": false,
+ "inputs": {
+ "low": { "name": "low", "label": "", "value": 0 },
+ "high": { "name": "high", "label": "", "value": 2147483647 }
+ }
+ },
+ "position": { "x": 700, "y": 550 }
+ },
+ {
+ "id": "4d3b9faf-c05b-7e6f-bf4d-5c6a7f8e9dab",
+ "type": "invocation",
+ "data": {
+ "id": "4d3b9faf-c05b-7e6f-bf4d-5c6a7f8e9dab",
+ "type": "wan_denoise",
+ "version": "1.0.0",
+ "label": "",
+ "notes": "",
+ "isOpen": true,
+ "isIntermediate": true,
+ "useCache": true,
+ "inputs": {
+ "transformer": { "name": "transformer", "label": "" },
+ "positive_conditioning": { "name": "positive_conditioning", "label": "" },
+ "negative_conditioning": { "name": "negative_conditioning", "label": "" },
+ "ref_image": { "name": "ref_image", "label": "" },
+ "latents": { "name": "latents", "label": "" },
+ "denoise_mask": { "name": "denoise_mask", "label": "" },
+ "denoising_start": { "name": "denoising_start", "label": "", "value": 0 },
+ "denoising_end": { "name": "denoising_end", "label": "", "value": 1 },
+ "add_noise": { "name": "add_noise", "label": "", "value": true },
+ "guidance_scale": { "name": "guidance_scale", "label": "CFG", "value": 5.0 },
+ "guidance_scale_low_noise": { "name": "guidance_scale_low_noise", "label": "CFG (Low)" },
+ "width": { "name": "width", "label": "", "value": 1024 },
+ "height": { "name": "height", "label": "", "value": 1024 },
+ "steps": { "name": "steps", "label": "", "value": 30 },
+ "seed": { "name": "seed", "label": "", "value": 0 }
+ }
+ },
+ "position": { "x": 1100, "y": -50 }
+ },
+ {
+ "id": "6f5dcb1c-e27d-9fb0-d16f-7e8cbaa1fcbd",
+ "type": "invocation",
+ "data": {
+ "id": "6f5dcb1c-e27d-9fb0-d16f-7e8cbaa1fcbd",
+ "type": "wan_l2i",
+ "version": "1.0.0",
+ "label": "",
+ "notes": "",
+ "isOpen": true,
+ "isIntermediate": false,
+ "useCache": true,
+ "inputs": {
+ "board": { "name": "board", "label": "" },
+ "metadata": { "name": "metadata", "label": "" },
+ "latents": { "name": "latents", "label": "" },
+ "vae": { "name": "vae", "label": "" }
+ }
+ },
+ "position": { "x": 1550, "y": -50 }
+ }
+ ],
+ "edges": [
+ {
+ "id": "edge-loader-transformer-denoise",
+ "type": "default",
+ "source": "1a0e6c7b-9d2f-4b3c-8e1a-2f3d4c5b6a7e",
+ "target": "4d3b9faf-c05b-7e6f-bf4d-5c6a7f8e9dab",
+ "sourceHandle": "transformer",
+ "targetHandle": "transformer"
+ },
+ {
+ "id": "edge-loader-t5-pos",
+ "type": "default",
+ "source": "1a0e6c7b-9d2f-4b3c-8e1a-2f3d4c5b6a7e",
+ "target": "2b1f7d8c-ae3f-5c4d-9f2b-3a4e5d6c7b8f",
+ "sourceHandle": "wan_t5_encoder",
+ "targetHandle": "wan_t5_encoder"
+ },
+ {
+ "id": "edge-loader-t5-neg",
+ "type": "default",
+ "source": "1a0e6c7b-9d2f-4b3c-8e1a-2f3d4c5b6a7e",
+ "target": "3c2a8e9d-bf4a-6d5e-af3c-4b5f6e7d8c9a",
+ "sourceHandle": "wan_t5_encoder",
+ "targetHandle": "wan_t5_encoder"
+ },
+ {
+ "id": "edge-loader-vae-l2i",
+ "type": "default",
+ "source": "1a0e6c7b-9d2f-4b3c-8e1a-2f3d4c5b6a7e",
+ "target": "6f5dcb1c-e27d-9fb0-d16f-7e8cbaa1fcbd",
+ "sourceHandle": "vae",
+ "targetHandle": "vae"
+ },
+ {
+ "id": "edge-loader-vae-refenc",
+ "type": "default",
+ "source": "1a0e6c7b-9d2f-4b3c-8e1a-2f3d4c5b6a7e",
+ "target": "7a6edc2d-f38e-a0c1-e27a-8f9dcbb20fce",
+ "sourceHandle": "vae",
+ "targetHandle": "vae"
+ },
+ {
+ "id": "edge-pos-cond-denoise",
+ "type": "default",
+ "source": "2b1f7d8c-ae3f-5c4d-9f2b-3a4e5d6c7b8f",
+ "target": "4d3b9faf-c05b-7e6f-bf4d-5c6a7f8e9dab",
+ "sourceHandle": "conditioning",
+ "targetHandle": "positive_conditioning"
+ },
+ {
+ "id": "edge-neg-cond-denoise",
+ "type": "default",
+ "source": "3c2a8e9d-bf4a-6d5e-af3c-4b5f6e7d8c9a",
+ "target": "4d3b9faf-c05b-7e6f-bf4d-5c6a7f8e9dab",
+ "sourceHandle": "conditioning",
+ "targetHandle": "negative_conditioning"
+ },
+ {
+ "id": "edge-refenc-refimage-denoise",
+ "type": "default",
+ "source": "7a6edc2d-f38e-a0c1-e27a-8f9dcbb20fce",
+ "target": "4d3b9faf-c05b-7e6f-bf4d-5c6a7f8e9dab",
+ "sourceHandle": "ref_image",
+ "targetHandle": "ref_image"
+ },
+ {
+ "id": "edge-rand-seed-denoise",
+ "type": "default",
+ "source": "5e4cab0b-d16c-8faf-c05e-6d7baf90ebbc",
+ "target": "4d3b9faf-c05b-7e6f-bf4d-5c6a7f8e9dab",
+ "sourceHandle": "value",
+ "targetHandle": "seed"
+ },
+ {
+ "id": "edge-denoise-latents-l2i",
+ "type": "default",
+ "source": "4d3b9faf-c05b-7e6f-bf4d-5c6a7f8e9dab",
+ "target": "6f5dcb1c-e27d-9fb0-d16f-7e8cbaa1fcbd",
+ "sourceHandle": "latents",
+ "targetHandle": "latents"
+ }
+ ]
+}
diff --git a/invokeai/app/services/workflow_records/default_workflows/Wan 2.2 Text to Image.json b/invokeai/app/services/workflow_records/default_workflows/Wan 2.2 Text to Image.json
new file mode 100644
index 00000000000..3352d09a1f8
--- /dev/null
+++ b/invokeai/app/services/workflow_records/default_workflows/Wan 2.2 Text to Image.json
@@ -0,0 +1,264 @@
+{
+ "id": "default_wan22_t2v_b1c4f0a2-2d3e-4a5b-9f6c-7d8e0a1b2c3d",
+ "name": "Text to Image - Wan 2.2",
+ "author": "InvokeAI",
+ "description": "Text-to-image generation with Wan 2.2 (T2V A14B or TI2V-5B). For A14B GGUFs, wire the second-expert transformer into 'Transformer (Low Noise)' and pick a Diffusers Wan as the Component Source (or use standalone VAE + UMT5-XXL encoder). TI2V-5B is a single-transformer model — leave the low-noise slot empty.",
+ "version": "1.0.0",
+ "contact": "",
+ "tags": "wan2.2, text to image",
+ "notes": "Prerequisite model downloads: a Wan 2.2 main model (Diffusers or GGUF). For GGUF mains, also install the Component Source (Diffusers Wan) OR the standalone Wan VAE + UMT5-XXL encoder. The Wan 2.2 starter bundle in the Model Manager pulls everything you need for T2V A14B Q4_K_M/Q8_0. Recommended settings: 30-40 steps and CFG 5-7 (or 4 steps and CFG 1 with the Wan Lightning LoRA pair).",
+ "exposedFields": [
+ {
+ "nodeId": "1a0e6c7b-9d2f-4b3c-8e1a-2f3d4c5b6a7e",
+ "fieldName": "model"
+ },
+ {
+ "nodeId": "1a0e6c7b-9d2f-4b3c-8e1a-2f3d4c5b6a7e",
+ "fieldName": "transformer_low_noise_model"
+ },
+ {
+ "nodeId": "1a0e6c7b-9d2f-4b3c-8e1a-2f3d4c5b6a7e",
+ "fieldName": "component_source"
+ },
+ {
+ "nodeId": "1a0e6c7b-9d2f-4b3c-8e1a-2f3d4c5b6a7e",
+ "fieldName": "vae_model"
+ },
+ {
+ "nodeId": "1a0e6c7b-9d2f-4b3c-8e1a-2f3d4c5b6a7e",
+ "fieldName": "wan_t5_encoder_model"
+ },
+ {
+ "nodeId": "2b1f7d8c-ae3f-5c4d-9f2b-3a4e5d6c7b8f",
+ "fieldName": "prompt"
+ },
+ {
+ "nodeId": "3c2a8e9d-bf4a-6d5e-af3c-4b5f6e7d8c9a",
+ "fieldName": "prompt"
+ },
+ {
+ "nodeId": "4d3b9faf-c05b-7e6f-bf4d-5c6a7f8e9dab",
+ "fieldName": "steps"
+ },
+ {
+ "nodeId": "4d3b9faf-c05b-7e6f-bf4d-5c6a7f8e9dab",
+ "fieldName": "guidance_scale"
+ },
+ {
+ "nodeId": "4d3b9faf-c05b-7e6f-bf4d-5c6a7f8e9dab",
+ "fieldName": "guidance_scale_low_noise"
+ },
+ {
+ "nodeId": "4d3b9faf-c05b-7e6f-bf4d-5c6a7f8e9dab",
+ "fieldName": "width"
+ },
+ {
+ "nodeId": "4d3b9faf-c05b-7e6f-bf4d-5c6a7f8e9dab",
+ "fieldName": "height"
+ }
+ ],
+ "meta": {
+ "version": "3.0.0",
+ "category": "default"
+ },
+ "nodes": [
+ {
+ "id": "1a0e6c7b-9d2f-4b3c-8e1a-2f3d4c5b6a7e",
+ "type": "invocation",
+ "data": {
+ "id": "1a0e6c7b-9d2f-4b3c-8e1a-2f3d4c5b6a7e",
+ "type": "wan_model_loader",
+ "version": "1.0.1",
+ "label": "",
+ "notes": "",
+ "isOpen": true,
+ "isIntermediate": true,
+ "useCache": false,
+ "inputs": {
+ "model": { "name": "model", "label": "" },
+ "transformer_low_noise_model": { "name": "transformer_low_noise_model", "label": "" },
+ "vae_model": { "name": "vae_model", "label": "" },
+ "wan_t5_encoder_model": { "name": "wan_t5_encoder_model", "label": "" },
+ "component_source": { "name": "component_source", "label": "" }
+ }
+ },
+ "position": { "x": 200, "y": 0 }
+ },
+ {
+ "id": "2b1f7d8c-ae3f-5c4d-9f2b-3a4e5d6c7b8f",
+ "type": "invocation",
+ "data": {
+ "id": "2b1f7d8c-ae3f-5c4d-9f2b-3a4e5d6c7b8f",
+ "type": "wan_text_encoder",
+ "version": "1.0.0",
+ "label": "Positive Prompt",
+ "notes": "",
+ "isOpen": true,
+ "isIntermediate": true,
+ "useCache": true,
+ "inputs": {
+ "prompt": { "name": "prompt", "label": "", "value": "a cat" },
+ "wan_t5_encoder": { "name": "wan_t5_encoder", "label": "" }
+ }
+ },
+ "position": { "x": 700, "y": -200 }
+ },
+ {
+ "id": "3c2a8e9d-bf4a-6d5e-af3c-4b5f6e7d8c9a",
+ "type": "invocation",
+ "data": {
+ "id": "3c2a8e9d-bf4a-6d5e-af3c-4b5f6e7d8c9a",
+ "type": "wan_text_encoder",
+ "version": "1.0.0",
+ "label": "Negative Prompt",
+ "notes": "",
+ "isOpen": true,
+ "isIntermediate": true,
+ "useCache": true,
+ "inputs": {
+ "prompt": { "name": "prompt", "label": "", "value": " " },
+ "wan_t5_encoder": { "name": "wan_t5_encoder", "label": "" }
+ }
+ },
+ "position": { "x": 700, "y": 100 }
+ },
+ {
+ "id": "5e4cab0b-d16c-8faf-c05e-6d7baf90ebbc",
+ "type": "invocation",
+ "data": {
+ "id": "5e4cab0b-d16c-8faf-c05e-6d7baf90ebbc",
+ "type": "rand_int",
+ "version": "1.0.1",
+ "label": "",
+ "notes": "",
+ "isOpen": true,
+ "isIntermediate": true,
+ "useCache": false,
+ "inputs": {
+ "low": { "name": "low", "label": "", "value": 0 },
+ "high": { "name": "high", "label": "", "value": 2147483647 }
+ }
+ },
+ "position": { "x": 700, "y": 400 }
+ },
+ {
+ "id": "4d3b9faf-c05b-7e6f-bf4d-5c6a7f8e9dab",
+ "type": "invocation",
+ "data": {
+ "id": "4d3b9faf-c05b-7e6f-bf4d-5c6a7f8e9dab",
+ "type": "wan_denoise",
+ "version": "1.0.0",
+ "label": "",
+ "notes": "",
+ "isOpen": true,
+ "isIntermediate": true,
+ "useCache": true,
+ "inputs": {
+ "transformer": { "name": "transformer", "label": "" },
+ "positive_conditioning": { "name": "positive_conditioning", "label": "" },
+ "negative_conditioning": { "name": "negative_conditioning", "label": "" },
+ "ref_image": { "name": "ref_image", "label": "" },
+ "latents": { "name": "latents", "label": "" },
+ "denoise_mask": { "name": "denoise_mask", "label": "" },
+ "denoising_start": { "name": "denoising_start", "label": "", "value": 0 },
+ "denoising_end": { "name": "denoising_end", "label": "", "value": 1 },
+ "add_noise": { "name": "add_noise", "label": "", "value": true },
+ "guidance_scale": { "name": "guidance_scale", "label": "CFG", "value": 5.0 },
+ "guidance_scale_low_noise": { "name": "guidance_scale_low_noise", "label": "CFG (Low)" },
+ "width": { "name": "width", "label": "", "value": 1024 },
+ "height": { "name": "height", "label": "", "value": 1024 },
+ "steps": { "name": "steps", "label": "", "value": 30 },
+ "seed": { "name": "seed", "label": "", "value": 0 }
+ }
+ },
+ "position": { "x": 1100, "y": -50 }
+ },
+ {
+ "id": "6f5dcb1c-e27d-9fb0-d16f-7e8cbaa1fcbd",
+ "type": "invocation",
+ "data": {
+ "id": "6f5dcb1c-e27d-9fb0-d16f-7e8cbaa1fcbd",
+ "type": "wan_l2i",
+ "version": "1.0.0",
+ "label": "",
+ "notes": "",
+ "isOpen": true,
+ "isIntermediate": false,
+ "useCache": true,
+ "inputs": {
+ "board": { "name": "board", "label": "" },
+ "metadata": { "name": "metadata", "label": "" },
+ "latents": { "name": "latents", "label": "" },
+ "vae": { "name": "vae", "label": "" }
+ }
+ },
+ "position": { "x": 1550, "y": -50 }
+ }
+ ],
+ "edges": [
+ {
+ "id": "edge-loader-transformer-denoise",
+ "type": "default",
+ "source": "1a0e6c7b-9d2f-4b3c-8e1a-2f3d4c5b6a7e",
+ "target": "4d3b9faf-c05b-7e6f-bf4d-5c6a7f8e9dab",
+ "sourceHandle": "transformer",
+ "targetHandle": "transformer"
+ },
+ {
+ "id": "edge-loader-t5-pos",
+ "type": "default",
+ "source": "1a0e6c7b-9d2f-4b3c-8e1a-2f3d4c5b6a7e",
+ "target": "2b1f7d8c-ae3f-5c4d-9f2b-3a4e5d6c7b8f",
+ "sourceHandle": "wan_t5_encoder",
+ "targetHandle": "wan_t5_encoder"
+ },
+ {
+ "id": "edge-loader-t5-neg",
+ "type": "default",
+ "source": "1a0e6c7b-9d2f-4b3c-8e1a-2f3d4c5b6a7e",
+ "target": "3c2a8e9d-bf4a-6d5e-af3c-4b5f6e7d8c9a",
+ "sourceHandle": "wan_t5_encoder",
+ "targetHandle": "wan_t5_encoder"
+ },
+ {
+ "id": "edge-loader-vae-l2i",
+ "type": "default",
+ "source": "1a0e6c7b-9d2f-4b3c-8e1a-2f3d4c5b6a7e",
+ "target": "6f5dcb1c-e27d-9fb0-d16f-7e8cbaa1fcbd",
+ "sourceHandle": "vae",
+ "targetHandle": "vae"
+ },
+ {
+ "id": "edge-pos-cond-denoise",
+ "type": "default",
+ "source": "2b1f7d8c-ae3f-5c4d-9f2b-3a4e5d6c7b8f",
+ "target": "4d3b9faf-c05b-7e6f-bf4d-5c6a7f8e9dab",
+ "sourceHandle": "conditioning",
+ "targetHandle": "positive_conditioning"
+ },
+ {
+ "id": "edge-neg-cond-denoise",
+ "type": "default",
+ "source": "3c2a8e9d-bf4a-6d5e-af3c-4b5f6e7d8c9a",
+ "target": "4d3b9faf-c05b-7e6f-bf4d-5c6a7f8e9dab",
+ "sourceHandle": "conditioning",
+ "targetHandle": "negative_conditioning"
+ },
+ {
+ "id": "edge-rand-seed-denoise",
+ "type": "default",
+ "source": "5e4cab0b-d16c-8faf-c05e-6d7baf90ebbc",
+ "target": "4d3b9faf-c05b-7e6f-bf4d-5c6a7f8e9dab",
+ "sourceHandle": "value",
+ "targetHandle": "seed"
+ },
+ {
+ "id": "edge-denoise-latents-l2i",
+ "type": "default",
+ "source": "4d3b9faf-c05b-7e6f-bf4d-5c6a7f8e9dab",
+ "target": "6f5dcb1c-e27d-9fb0-d16f-7e8cbaa1fcbd",
+ "sourceHandle": "latents",
+ "targetHandle": "latents"
+ }
+ ]
+}
diff --git a/invokeai/app/util/step_callback.py b/invokeai/app/util/step_callback.py
index 08dc9a2265c..9364ec9b8ce 100644
--- a/invokeai/app/util/step_callback.py
+++ b/invokeai/app/util/step_callback.py
@@ -179,6 +179,84 @@
ANIMA_LATENT_RGB_BIAS = [-0.1835, -0.0868, -0.3360]
+# Wan 2.2 A14B uses the standard 16-channel Wan VAE.
+# Factors come from ComfyUI's Wan21 latent_format (same VAE as A14B).
+WAN_LATENT_RGB_FACTORS = [
+ [-0.1299, -0.1692, 0.2932],
+ [0.0671, 0.0406, 0.0442],
+ [0.3568, 0.2548, 0.1747],
+ [0.0372, 0.2344, 0.1420],
+ [0.0313, 0.0189, -0.0328],
+ [0.0296, -0.0956, -0.0665],
+ [-0.3477, -0.4059, -0.2925],
+ [0.0166, 0.1902, 0.1975],
+ [-0.0412, 0.0267, -0.1364],
+ [-0.1293, 0.0740, 0.1636],
+ [0.0680, 0.3019, 0.1128],
+ [0.0032, 0.0581, 0.0639],
+ [-0.1251, 0.0927, 0.1699],
+ [0.0060, -0.0633, 0.0005],
+ [0.3477, 0.2275, 0.2950],
+ [0.1984, 0.0913, 0.1861],
+]
+
+WAN_LATENT_RGB_BIAS = [-0.1835, -0.0868, -0.3360]
+
+# Wan 2.2 TI2V-5B uses Wan2.2-VAE with 48 latent channels and 16x spatial downscale.
+# Factors come from ComfyUI's Wan22 latent_format.
+WAN22_LATENT_RGB_FACTORS = [
+ [0.0119, 0.0103, 0.0046],
+ [-0.1062, -0.0504, 0.0165],
+ [0.0140, 0.0409, 0.0491],
+ [-0.0813, -0.0677, 0.0607],
+ [0.0656, 0.0851, 0.0808],
+ [0.0264, 0.0463, 0.0912],
+ [0.0295, 0.0326, 0.0590],
+ [-0.0244, -0.0270, 0.0025],
+ [0.0443, -0.0102, 0.0288],
+ [-0.0465, -0.0090, -0.0205],
+ [0.0359, 0.0236, 0.0082],
+ [-0.0776, 0.0854, 0.1048],
+ [0.0564, 0.0264, 0.0561],
+ [0.0006, 0.0594, 0.0418],
+ [-0.0319, -0.0542, -0.0637],
+ [-0.0268, 0.0024, 0.0260],
+ [0.0539, 0.0265, 0.0358],
+ [-0.0359, -0.0312, -0.0287],
+ [-0.0285, -0.1032, -0.1237],
+ [0.1041, 0.0537, 0.0622],
+ [-0.0086, -0.0374, -0.0051],
+ [0.0390, 0.0670, 0.2863],
+ [0.0069, 0.0144, 0.0082],
+ [0.0006, -0.0167, 0.0079],
+ [0.0313, -0.0574, -0.0232],
+ [-0.1454, -0.0902, -0.0481],
+ [0.0714, 0.0827, 0.0447],
+ [-0.0304, -0.0574, -0.0196],
+ [0.0401, 0.0384, 0.0204],
+ [-0.0758, -0.0297, -0.0014],
+ [0.0568, 0.1307, 0.1372],
+ [-0.0055, -0.0310, -0.0380],
+ [0.0239, -0.0305, 0.0325],
+ [-0.0663, -0.0673, -0.0140],
+ [-0.0416, -0.0047, -0.0023],
+ [0.0166, 0.0112, -0.0093],
+ [-0.0211, 0.0011, 0.0331],
+ [0.1833, 0.1466, 0.2250],
+ [-0.0368, 0.0370, 0.0295],
+ [-0.3441, -0.3543, -0.2008],
+ [-0.0479, -0.0489, -0.0420],
+ [-0.0660, -0.0153, 0.0800],
+ [-0.0101, 0.0068, 0.0156],
+ [-0.0690, -0.0452, -0.0927],
+ [-0.0145, 0.0041, 0.0015],
+ [0.0421, 0.0451, 0.0373],
+ [0.0504, -0.0483, -0.0356],
+ [-0.0837, 0.0168, 0.0055],
+]
+
+WAN22_LATENT_RGB_BIAS = [0.0317, -0.0878, -0.1388]
+
def sample_to_lowres_estimated_image(
samples: torch.Tensor,
@@ -270,6 +348,15 @@ def diffusion_step_callback(
# Anima uses Wan 2.1 VAE with 16 latent channels
latent_rgb_factors = ANIMA_LATENT_RGB_FACTORS
latent_rgb_bias = ANIMA_LATENT_RGB_BIAS
+ elif base_model == BaseModelType.Wan:
+ # A14B (16-ch standard Wan VAE, 8x spatial) vs TI2V-5B (48-ch Wan2.2-VAE,
+ # 16x spatial). The latent channel count uniquely identifies the variant.
+ if sample.shape[-3] == 48:
+ latent_rgb_factors = WAN22_LATENT_RGB_FACTORS
+ latent_rgb_bias = WAN22_LATENT_RGB_BIAS
+ else:
+ latent_rgb_factors = WAN_LATENT_RGB_FACTORS
+ latent_rgb_bias = WAN_LATENT_RGB_BIAS
else:
raise ValueError(f"Unsupported base model: {base_model}")
@@ -287,8 +374,13 @@ def diffusion_step_callback(
latent_rgb_bias=latent_rgb_bias_torch,
)
- width = image.width * 8
- height = image.height * 8
+ # Spatial downscale ratio: 8x is the SD/SDXL/FLUX/Wan-A14B default;
+ # Wan TI2V-5B's Wan2.2-VAE uses 16x.
+ spatial_scale = 8
+ if base_model == BaseModelType.Wan and sample.shape[-3] == 48:
+ spatial_scale = 16
+ width = image.width * spatial_scale
+ height = image.height * spatial_scale
percentage = calc_percentage(intermediate_state)
signal_progress("Denoising", percentage, image, (width, height))
diff --git a/invokeai/app/util/video_decode_worker.py b/invokeai/app/util/video_decode_worker.py
new file mode 100644
index 00000000000..b4cf0f23441
--- /dev/null
+++ b/invokeai/app/util/video_decode_worker.py
@@ -0,0 +1,284 @@
+"""Standalone decode worker for untrusted video files.
+
+This script is executed as a short-lived child process by
+``invokeai.app.util.video_thumbnails`` — never imported by the server at runtime — so
+that a decoder hang on a crafted or malformed container can be bounded by a timeout and
+killed. cv2 wheels have historically hung on certain codec/container combinations, and
+a hung *thread* cannot be killed from Python, so process isolation is the only reliable
+bound. It is run by file path (not ``-m``) and deliberately imports only the stdlib plus
+imageio/PIL/cv2, so it starts quickly without pulling in the invokeai package or torch.
+
+Protocol: ``python video_decode_worker.py ``. On success a single
+JSON object is written to stdout and the exit code is 0; on any failure the exit code is
+non-zero with a message on stderr.
+
+Commands:
+ probe -> {"width", "height", "duration", "fps"}
+ frame -> {"ok": true}; the frame is written to out_path as PNG
+ count -> {"count": }
+ stream -> consecutive numpy arrays on stdout
+"""
+
+import io
+import json
+import math
+import struct
+import sys
+from pathlib import Path
+from typing import Optional
+
+import imageio.v3 as iio
+import numpy as np
+from PIL import Image
+
+# Upper bound on the decoded frame size we are willing to allocate (~8K video). A small
+# crafted container can claim absurd dimensions in cheap metadata while the full frame
+# is only allocated at decode time, so dimensions are checked before any decode.
+# Keep in sync with MAX_VIDEO_FRAME_PIXELS in video_thumbnails.py (this script must not
+# import the invokeai package).
+MAX_FRAME_PIXELS = 64 * 1024 * 1024
+# Address-space headroom for everything allocated after startup. Must accommodate a
+# legal max-size decode, not just a typical one: a MAX_FRAME_PIXELS RGB frame is
+# 192 MiB and the emit path holds ~4 frame-sized copies at once (~768 MiB), on top of
+# which RLIMIT_AS also counts the lazily-imported cv2's library mappings, glibc
+# per-thread arenas, and the codec's reference-frame buffers — address space routinely
+# exceeds resident memory by a wide margin. Keep in sync with
+# MAX_VIDEO_DECODE_WORKER_RSS_BYTES in video_thumbnails.py (the parent-side resident
+# bound; this script must not import the invokeai package).
+WORKER_MEMORY_HEADROOM_BYTES = 4 * 1024 * 1024 * 1024
+
+
+def _limit_worker_memory() -> None:
+ """Bound allocations made after worker startup on Linux."""
+ if sys.platform != "linux":
+ return
+ try:
+ import os
+ import resource
+
+ pages = int(Path("/proc/self/statm").read_text().split()[0])
+ limit = pages * os.sysconf("SC_PAGE_SIZE") + WORKER_MEMORY_HEADROOM_BYTES
+ _soft, hard = resource.getrlimit(resource.RLIMIT_AS)
+ if hard != resource.RLIM_INFINITY:
+ limit = min(limit, hard)
+ resource.setrlimit(resource.RLIMIT_AS, (limit, hard))
+ except (OSError, ValueError):
+ pass
+
+
+def _validate_decoded_frame(frame: np.ndarray) -> None:
+ if frame.ndim != 3 or frame.shape[2] != 3:
+ raise ValueError(f"Decoded frame must be RGB; got shape {frame.shape}")
+ height, width = frame.shape[:2]
+ if height <= 0 or width <= 0 or height * width > MAX_FRAME_PIXELS:
+ raise ValueError(f"Decoded frame dimensions {width}x{height} exceed the maximum decodable size")
+
+
+def _assert_decodable_dims(video_path: Path) -> None:
+ """Refuses to decode frames from a video whose reported dimensions exceed the bound.
+
+ If the dimensions cannot be probed, decoding is refused because the parent's frame
+ record bound and PIL checks run only after the decoder has allocated the frame.
+ """
+ try:
+ width, height, _duration, _fps = _probe(video_path)
+ except Exception as error:
+ raise ValueError(f"Unable to validate video dimensions for {video_path}") from error
+ if width <= 0 or height <= 0:
+ raise ValueError(f"Video reports invalid dimensions {width}x{height}")
+ if width * height > MAX_FRAME_PIXELS:
+ raise ValueError(f"Video dimensions {width}x{height} exceed the maximum decodable size")
+
+
+def _extract_frame(video_path: Path, frame_index: int) -> Optional[Image.Image]:
+ """Extracts a single frame from a video file as a PIL Image. Returns None on failure.
+
+ Tries imageio's FFMPEG plugin first since it's the same encoder we use for output,
+ then falls back to cv2 — uploaded videos with unusual codecs may need that path.
+ """
+ try:
+ # iio.imread with index=N seeks to that frame directly. Returns RGB HxWxC uint8.
+ frame = iio.imread(video_path, plugin="FFMPEG", index=frame_index)
+ _validate_decoded_frame(frame)
+ return Image.fromarray(frame)
+ except Exception:
+ pass
+
+ try:
+ import cv2 # local import so the imageio-only path doesn't pay the cv2 import cost
+
+ capture = cv2.VideoCapture(str(video_path))
+ if not capture.isOpened():
+ capture.release()
+ return None
+ try:
+ if frame_index > 0:
+ capture.set(cv2.CAP_PROP_POS_FRAMES, frame_index)
+ ok, frame_bgr = capture.read()
+ if not ok or frame_bgr is None:
+ return None
+ frame_rgb = cv2.cvtColor(frame_bgr, cv2.COLOR_BGR2RGB)
+ _validate_decoded_frame(frame_rgb)
+ return Image.fromarray(frame_rgb)
+ finally:
+ capture.release()
+ except Exception:
+ return None
+
+
+def _probe(video_path: Path) -> tuple[int, int, float, Optional[float]]:
+ """Returns (width, height, duration_seconds, fps_or_none) for a video file.
+
+ Tries imageio's FFMPEG plugin first; falls back to cv2.VideoCapture. Raises if
+ neither backend can read the file.
+ """
+ try:
+ meta = iio.immeta(video_path, plugin="FFMPEG")
+ fps_raw = meta.get("fps")
+ duration = float(meta.get("duration", 0.0)) if meta.get("duration") is not None else 0.0
+ size = meta.get("size")
+ if size is None:
+ # Fall through to cv2 — imageio didn't give us dimensions.
+ raise ValueError("imageio probe missing 'size'")
+ width, height = int(size[0]), int(size[1])
+ fps: Optional[float] = float(fps_raw) if fps_raw and fps_raw > 0 else None
+ return width, height, duration, fps
+ except Exception:
+ pass
+
+ import cv2
+
+ capture = cv2.VideoCapture(str(video_path))
+ if not capture.isOpened():
+ capture.release()
+ raise FileNotFoundError(f"Unable to open video at {video_path}")
+ try:
+ width = int(capture.get(cv2.CAP_PROP_FRAME_WIDTH))
+ height = int(capture.get(cv2.CAP_PROP_FRAME_HEIGHT))
+ frame_count = int(capture.get(cv2.CAP_PROP_FRAME_COUNT))
+ fps_raw = capture.get(cv2.CAP_PROP_FPS)
+ fps_v2: Optional[float] = float(fps_raw) if fps_raw and fps_raw > 0 else None
+ duration = (frame_count / fps_v2) if (fps_v2 and frame_count > 0) else 0.0
+ finally:
+ capture.release()
+ return width, height, duration, fps_v2
+
+
+def _count(video_path: Path) -> Optional[int]:
+ """Return the exact decoded frame count, or None if neither backend can determine it.
+
+ Tries imageio's improps first (works for a handful of codecs that expose nframes in
+ container metadata). For libx264 streams imageio reports ``inf``, so we fall through
+ to cv2's ``CAP_PROP_FRAME_COUNT`` which reads the actual packet count. Both sources
+ are preferred over a ``duration * fps`` estimate, which can overshoot by one on VFR
+ uploads or containers with imprecise metadata.
+ """
+ try:
+ props = iio.improps(video_path, plugin="FFMPEG")
+ except Exception:
+ props = None
+ shape = getattr(props, "shape", None) if props is not None else None
+ if shape:
+ n = shape[0]
+ if not (isinstance(n, float) and not math.isfinite(n)):
+ try:
+ return int(n)
+ except (TypeError, ValueError, OverflowError):
+ pass
+
+ try:
+ import cv2
+
+ capture = cv2.VideoCapture(str(video_path))
+ if not capture.isOpened():
+ capture.release()
+ return None
+ try:
+ count = int(capture.get(cv2.CAP_PROP_FRAME_COUNT))
+ finally:
+ capture.release()
+ return count if count > 0 else None
+ except Exception:
+ return None
+
+
+def _emit_stream_frame(frame: np.ndarray) -> None:
+ _validate_decoded_frame(frame)
+ record = io.BytesIO()
+ np.save(record, np.ascontiguousarray(frame), allow_pickle=False)
+ payload = record.getvalue()
+ sys.stdout.buffer.write(struct.pack(">Q", len(payload)))
+ sys.stdout.buffer.write(payload)
+ sys.stdout.buffer.flush()
+
+
+def _stream(video_path: Path) -> None:
+ """Writes decoded frames as length-prefixed, non-pickled numpy records.
+
+ Tries imageio's FFMPEG plugin first and falls back to cv2, mirroring
+ ``_extract_frame``/``_probe``/``_count`` — a file accepted at upload via the cv2
+ fallback must also stream in the frame-range and concat nodes. The fallback only
+ engages when no frame has been emitted yet: a mid-stream decoder failure must
+ surface as an error, not restart the stream from frame 0.
+ """
+ emitted = False
+ try:
+ for frame in iio.imiter(video_path, plugin="FFMPEG"):
+ _emit_stream_frame(frame)
+ emitted = True
+ except Exception:
+ if emitted:
+ raise
+ if emitted:
+ return
+
+ import cv2
+
+ capture = cv2.VideoCapture(str(video_path))
+ if not capture.isOpened():
+ capture.release()
+ raise FileNotFoundError(f"Unable to open video at {video_path}")
+ try:
+ while True:
+ ok, frame_bgr = capture.read()
+ if not ok or frame_bgr is None:
+ break
+ _emit_stream_frame(cv2.cvtColor(frame_bgr, cv2.COLOR_BGR2RGB))
+ emitted = True
+ finally:
+ capture.release()
+ if not emitted:
+ raise ValueError(f"No frames decoded from {video_path}")
+
+
+def main(argv: list[str]) -> int:
+ try:
+ _limit_worker_memory()
+ command = argv[1]
+ if command == "stream":
+ _assert_decodable_dims(Path(argv[2]))
+ _stream(Path(argv[2]))
+ elif command == "probe":
+ width, height, duration, fps = _probe(Path(argv[2]))
+ print(json.dumps({"width": width, "height": height, "duration": duration, "fps": fps}))
+ elif command == "frame":
+ _assert_decodable_dims(Path(argv[2]))
+ image = _extract_frame(Path(argv[2]), int(argv[3]))
+ if image is None:
+ print("no frame decoded", file=sys.stderr)
+ return 1
+ image.save(argv[4], format="PNG")
+ print(json.dumps({"ok": True}))
+ elif command == "count":
+ print(json.dumps({"count": _count(Path(argv[2]))}))
+ else:
+ print(f"unknown command {command!r}", file=sys.stderr)
+ return 1
+ except Exception as e:
+ print(str(e) or repr(e), file=sys.stderr)
+ return 1
+ return 0
+
+
+if __name__ == "__main__":
+ sys.exit(main(sys.argv))
diff --git a/invokeai/app/util/video_encoding.py b/invokeai/app/util/video_encoding.py
new file mode 100644
index 00000000000..f13aabe2ea8
--- /dev/null
+++ b/invokeai/app/util/video_encoding.py
@@ -0,0 +1,21 @@
+"""Shared MP4 writer construction for video-producing invocations.
+
+All video nodes encode through this helper so the encoder settings stay in one
+place. libx264 + yuv420p (imageio's defaults for the FFMPEG plugin) give
+broadly-compatible browser playback. ``macro_block_size=1`` is load-bearing:
+imageio's default of 16 makes ffmpeg silently *rescale* frames to the next
+multiple of 16 (e.g. 1920x1080 -> 1920x1088), which desynchronizes the encoded
+file from the dimensions recorded in the video DTO and breaks same-dimension
+checks downstream (e.g. concatenating a trimmed clip with its source).
+
+yuv420p requires even dimensions; callers validate that before encoding.
+"""
+
+from pathlib import Path
+
+import imageio.v2 as iio2
+
+
+def make_mp4_writer(path: Path | str, fps: float):
+ """Returns an imageio FFMPEG writer that preserves frame dimensions exactly."""
+ return iio2.get_writer(str(path), format="FFMPEG", mode="I", fps=fps, codec="libx264", macro_block_size=1)
diff --git a/invokeai/app/util/video_thumbnails.py b/invokeai/app/util/video_thumbnails.py
new file mode 100644
index 00000000000..533d6ced4e1
--- /dev/null
+++ b/invokeai/app/util/video_thumbnails.py
@@ -0,0 +1,392 @@
+"""Video frame/probe helpers used by the video file store, upload router, and video nodes.
+
+Decoding runs in a short-lived child process (``video_decode_worker.py``) with a hard
+timeout. Files reaching these helpers are user uploads, and both decode backends can
+hang indefinitely on crafted or malformed containers (cv2 wheels historically, ffmpeg in
+degenerate cases). An in-process hang would tie up the FastAPI request worker that
+called us — repeated crafted uploads could exhaust the worker pool — and a hung thread
+cannot be killed from Python, so process isolation is the only reliable bound.
+The parent explicitly terminates the worker and its descendants when the timeout
+expires, so a hostile file costs at most ``timeout`` seconds and cannot leak a stuck
+FFmpeg process.
+"""
+
+import io
+import json
+import math
+import os
+import queue
+import signal
+import struct
+import subprocess
+import sys
+import tempfile
+import threading
+import time
+from pathlib import Path
+from typing import Any, Callable, Iterator, Optional
+
+import numpy as np
+import psutil
+from PIL import Image
+
+from invokeai.app.services.session_processor.session_processor_common import CanceledException
+
+# Generous — a healthy decode of a single frame or of container metadata takes well
+# under a second even for large files. The timeout exists to bound adversarial or hung
+# decodes, not to police slow ones.
+VIDEO_DECODE_TIMEOUT_SECONDS = 30.0
+MAX_DECODED_FRAME_RECORD_BYTES = 256 * 1024 * 1024
+MAX_DECODE_STDERR_BYTES = 16 * 1024
+# Resident-memory kill threshold for a decode worker *tree* (the worker plus its
+# ffmpeg child). Sized for a legal max-size decode, not a typical one: a
+# MAX_VIDEO_FRAME_PIXELS RGB frame is 192 MiB, the worker's emit path holds ~4
+# frame-sized copies at once (~768 MiB), and the ffmpeg child keeps its own output
+# frame plus YUV reference frames (several hundred MiB more) — so a 1 GiB bound would
+# kill decodes of videos the frame-size validators explicitly accept. This is an
+# anti-runaway bound, not a budget. Keep in sync with WORKER_MEMORY_HEADROOM_BYTES in
+# video_decode_worker.py (the worker-side address-space bound).
+MAX_VIDEO_DECODE_WORKER_RSS_BYTES = 3 * 1024 * 1024 * 1024
+# A bound this size doesn't need 50 ms granularity; each poll walks the process tree
+# via psutil, so a coarser period keeps sustained streaming decodes cheap.
+WORKER_MEMORY_POLL_SECONDS = 0.25
+# Upper bound on decoded frame size (~8K video). Decoder-reported dimensions are
+# untrusted metadata: a tiny crafted container can claim absurd dimensions whose full
+# frame is only allocated at decode time. probe_video rejects such files before the
+# upload path persists them, and the worker refuses to decode frames from them.
+# Keep in sync with MAX_FRAME_PIXELS in video_decode_worker.py.
+MAX_VIDEO_FRAME_PIXELS = 64 * 1024 * 1024
+
+_WORKER_PATH = Path(__file__).parent / "video_decode_worker.py"
+
+
+class VideoDecodeTimeoutError(TimeoutError):
+ """The decode worker did not finish within its time budget.
+
+ A timeout is contention (or an adversarial file), not evidence the video is
+ undecodable — callers that gate acceptance on decodability must treat it as
+ inconclusive rather than as a failed decode.
+ """
+
+
+def get_video_thumbnail_name(video_name: str) -> str:
+ """Given a video file name (e.g. .mp4), returns the matching thumbnail name (e.g. .webp)."""
+ return os.path.splitext(video_name)[0] + ".webp"
+
+
+def _worker_command(*args: str) -> list[str]:
+ """Command line for one decode-worker invocation (patchable in tests).
+
+ The worker is run by file path rather than ``-m`` so the child process doesn't
+ import the invokeai package (and transitively torch) just to decode a frame.
+ """
+ return [sys.executable, str(_WORKER_PATH), *args]
+
+
+def _spawn_worker(*args: str, **kwargs: Any) -> subprocess.Popen[Any]:
+ """Starts a worker in an independently killable process group on POSIX."""
+ if os.name != "nt":
+ kwargs["start_new_session"] = True
+ return subprocess.Popen(_worker_command(*args), **kwargs)
+
+
+def _is_process_running(pid: int) -> bool:
+ """Returns whether a process exists and is not a zombie."""
+ try:
+ process = psutil.Process(pid)
+ return process.is_running() and process.status() != psutil.STATUS_ZOMBIE
+ except psutil.Error:
+ return False
+
+
+def _terminate_process_tree(proc: subprocess.Popen[Any]) -> None:
+ """Kills a worker and every descendant it spawned."""
+ if os.name != "nt":
+ try:
+ os.killpg(os.getpgid(proc.pid), signal.SIGKILL)
+ proc.wait(timeout=5)
+ return
+ except (OSError, subprocess.TimeoutExpired):
+ pass
+ try:
+ parent = psutil.Process(proc.pid)
+ processes = parent.children(recursive=True)
+ processes.append(parent)
+ for process in processes:
+ try:
+ process.kill()
+ except psutil.Error:
+ pass
+ psutil.wait_procs(processes, timeout=5)
+ except psutil.Error:
+ try:
+ proc.kill()
+ except OSError:
+ pass
+
+
+def _worker_tree_rss(proc: subprocess.Popen[Any]) -> int:
+ """Returns resident memory used by the worker and its decoder children."""
+ try:
+ worker = psutil.Process(proc.pid)
+ processes = [worker, *worker.children(recursive=True)]
+ except psutil.Error:
+ return 0
+ total = 0
+ for process in processes:
+ try:
+ total += process.memory_info().rss
+ except psutil.Error:
+ pass
+ return total
+
+
+def _start_worker_memory_monitor(
+ proc: subprocess.Popen[Any],
+) -> tuple[threading.Event, threading.Event, threading.Thread]:
+ """Kills a decoder process tree when its cross-platform RSS bound is exceeded."""
+ stopped = threading.Event()
+ exceeded = threading.Event()
+
+ def monitor() -> None:
+ while not stopped.is_set():
+ if proc.poll() is not None:
+ return
+ if _worker_tree_rss(proc) > MAX_VIDEO_DECODE_WORKER_RSS_BYTES:
+ exceeded.set()
+ _terminate_process_tree(proc)
+ return
+ stopped.wait(WORKER_MEMORY_POLL_SECONDS)
+
+ thread = threading.Thread(target=monitor, name="video-memory-monitor", daemon=True)
+ thread.start()
+ return stopped, exceeded, thread
+
+
+def _run_worker(args: list[str], timeout: float, raise_on_timeout: bool = False) -> Optional[dict[str, Any]]:
+ """Runs the decode worker; returns its parsed JSON output, or None on failure or timeout.
+
+ With ``raise_on_timeout``, a timeout raises VideoDecodeTimeoutError instead of
+ returning None, letting callers distinguish "could not decode" from "ran out of
+ time on a loaded machine".
+ """
+ monitor_stop: threading.Event | None = None
+ monitor: threading.Thread | None = None
+ try:
+ proc = _spawn_worker(*args, stdout=subprocess.PIPE, stderr=subprocess.PIPE, text=True)
+ except Exception:
+ return None
+ try:
+ monitor_stop, _memory_exceeded, monitor = _start_worker_memory_monitor(proc)
+ stdout, _ = proc.communicate(timeout=timeout)
+ except subprocess.TimeoutExpired as error:
+ _terminate_process_tree(proc)
+ proc.communicate()
+ if raise_on_timeout:
+ raise VideoDecodeTimeoutError(f"Video decode worker timed out after {timeout}s") from error
+ return None
+ except Exception:
+ # An unexpected failure (e.g. OSError from communicate()) must not leak the
+ # worker tree: the finally below stops the RSS-monitor backstop, so nothing
+ # else would ever reap a still-running worker and its ffmpeg child.
+ _terminate_process_tree(proc)
+ return None
+ finally:
+ if monitor_stop is not None and monitor is not None:
+ monitor_stop.set()
+ monitor.join(timeout=1)
+ if proc.returncode != 0:
+ return None
+ try:
+ result = json.loads(stdout)
+ except ValueError:
+ return None
+ return result if isinstance(result, dict) else None
+
+
+def iter_video_frames(
+ video_path: Path,
+ timeout: float = VIDEO_DECODE_TIMEOUT_SECONDS,
+ is_canceled: Optional[Callable[[], bool]] = None,
+) -> Iterator[np.ndarray]:
+ """Streams decoded frames from an isolated worker with bounded memory and wait time."""
+ proc = _spawn_worker(
+ "stream",
+ str(video_path),
+ stdout=subprocess.PIPE,
+ stderr=subprocess.PIPE,
+ )
+ if proc.stdout is None or proc.stderr is None:
+ _terminate_process_tree(proc)
+ raise RuntimeError("Unable to open video decoder output stream")
+ memory_monitor_stop, _memory_exceeded, memory_monitor = _start_worker_memory_monitor(proc)
+
+ results: queue.Queue[tuple[str, object]] = queue.Queue(maxsize=1)
+ stopped = threading.Event()
+ stderr_tail = bytearray()
+
+ def read_stderr() -> str:
+ return bytes(stderr_tail).decode(errors="replace").strip()
+
+ def drain_stderr() -> None:
+ while chunk := proc.stderr.read(4096):
+ stderr_tail.extend(chunk)
+ if len(stderr_tail) > MAX_DECODE_STDERR_BYTES:
+ del stderr_tail[:-MAX_DECODE_STDERR_BYTES]
+
+ def read_exactly(size: int) -> bytes:
+ chunks: list[bytes] = []
+ remaining = size
+ while remaining > 0:
+ chunk = proc.stdout.read(remaining)
+ if not chunk:
+ if remaining == size:
+ raise EOFError
+ raise OSError("Truncated frame record from video decoder")
+ chunks.append(chunk)
+ remaining -= len(chunk)
+ return b"".join(chunks)
+
+ def put_result(result: tuple[str, object]) -> None:
+ while not stopped.is_set():
+ try:
+ results.put(result, timeout=0.1)
+ return
+ except queue.Full:
+ continue
+
+ def read_frames() -> None:
+ try:
+ while not stopped.is_set():
+ record_size = struct.unpack(">Q", read_exactly(8))[0]
+ if record_size > MAX_DECODED_FRAME_RECORD_BYTES:
+ raise ValueError(f"Decoded frame record exceeds {MAX_DECODED_FRAME_RECORD_BYTES} bytes")
+ payload = read_exactly(record_size)
+ put_result(("frame", np.load(io.BytesIO(payload), allow_pickle=False)))
+ except (EOFError, ValueError, OSError) as error:
+ put_result(("done", error))
+
+ reader = threading.Thread(target=read_frames, name="video-frame-reader", daemon=True)
+ stderr_reader = threading.Thread(target=drain_stderr, name="video-stderr-reader", daemon=True)
+ reader.start()
+ stderr_reader.start()
+ deadline = time.monotonic() + timeout
+ try:
+ while True:
+ if is_canceled is not None and is_canceled():
+ raise CanceledException
+ remaining = deadline - time.monotonic()
+ if remaining <= 0:
+ raise TimeoutError(f"Timed out decoding frames from {video_path}")
+ try:
+ kind, value = results.get(timeout=min(0.1, remaining))
+ except queue.Empty:
+ continue
+ if kind == "frame":
+ if not isinstance(value, np.ndarray):
+ raise ValueError(f"Decoder returned an invalid frame for {video_path}")
+ yield value
+ deadline = time.monotonic() + timeout
+ continue
+ try:
+ return_code = proc.wait(timeout=min(1, timeout))
+ except subprocess.TimeoutExpired as error:
+ _terminate_process_tree(proc)
+ stderr_reader.join(timeout=1)
+ detail = read_stderr()
+ message = f"Timed out waiting for video decoder worker for {video_path}"
+ raise TimeoutError(f"{message}: {detail}" if detail else message) from error
+ if return_code != 0:
+ stderr_reader.join(timeout=1)
+ detail = read_stderr()
+ message = f"Unable to decode video at {video_path}"
+ raise ValueError(f"{message}: {detail}" if detail else message) from value
+ return
+ finally:
+ memory_monitor_stop.set()
+ memory_monitor.join(timeout=1)
+ stopped.set()
+ if proc.poll() is None:
+ _terminate_process_tree(proc)
+ proc.stdout.close()
+ proc.wait()
+ reader.join(timeout=1)
+ stderr_reader.join(timeout=1)
+ proc.stderr.close()
+
+
+def extract_video_frame(
+ video_path: Path,
+ frame_index: int = 0,
+ timeout: float = VIDEO_DECODE_TIMEOUT_SECONDS,
+ raise_on_timeout: bool = False,
+) -> Optional[Image.Image]:
+ """Extracts a single frame from a video file as a PIL Image. Returns None on failure or timeout.
+
+ With ``raise_on_timeout``, a timeout raises VideoDecodeTimeoutError instead of
+ returning None.
+ """
+ fd, tmp_name = tempfile.mkstemp(prefix="invokeai_frame_", suffix=".png")
+ os.close(fd)
+ try:
+ result = _run_worker(["frame", str(video_path), str(frame_index), tmp_name], timeout, raise_on_timeout)
+ if result is None:
+ return None
+ with Image.open(tmp_name) as image:
+ image.load()
+ return image
+ except VideoDecodeTimeoutError:
+ raise
+ except Exception:
+ return None
+ finally:
+ Path(tmp_name).unlink(missing_ok=True)
+
+
+def probe_video(
+ video_path: Path, timeout: float = VIDEO_DECODE_TIMEOUT_SECONDS
+) -> tuple[int, int, float, Optional[float]]:
+ """Returns (width, height, duration_seconds, fps_or_none) for a video file.
+
+ Raises FileNotFoundError if the file cannot be read — including when the decode
+ times out, since a file we cannot probe within the bound is treated as unreadable —
+ or when the decoder reports metadata no sane video has (non-positive or over-limit
+ dimensions, non-finite or negative duration). Decoder-reported values are untrusted:
+ they come from the uploaded container, and the upload path persists them and sizes
+ thumbnail decoding by them. A non-finite or non-positive fps is coerced to None
+ (unknown) rather than rejected, matching the decoder's own unknown-fps behavior.
+ """
+ result = _run_worker(["probe", str(video_path)], timeout)
+ if result is None:
+ raise FileNotFoundError(f"Unable to open video at {video_path}")
+ try:
+ width = int(result["width"])
+ height = int(result["height"])
+ duration = float(result["duration"])
+ fps_raw = result.get("fps")
+ fps: Optional[float] = float(fps_raw) if fps_raw else None
+ except (KeyError, TypeError, ValueError, OverflowError) as e:
+ raise FileNotFoundError(f"Unable to open video at {video_path}") from e
+ if width <= 0 or height <= 0 or width * height > MAX_VIDEO_FRAME_PIXELS:
+ raise FileNotFoundError(f"Video at {video_path} reports invalid dimensions {width}x{height}")
+ if not math.isfinite(duration) or duration < 0:
+ raise FileNotFoundError(f"Video at {video_path} reports an invalid duration {duration}")
+ if fps is not None and (not math.isfinite(fps) or fps <= 0):
+ fps = None
+ return width, height, duration, fps
+
+
+def decoder_frame_count(video_path: Path, timeout: float = VIDEO_DECODE_TIMEOUT_SECONDS) -> Optional[int]:
+ """Returns the exact decoded frame count, or None if it cannot be determined in time.
+
+ Preferred over a ``duration * fps`` estimate, which can overshoot by one on VFR
+ uploads or containers with imprecise metadata; callers fall back to that estimate
+ when this returns None.
+ """
+ result = _run_worker(["count", str(video_path)], timeout)
+ if result is None:
+ return None
+ count = result.get("count")
+ if isinstance(count, bool) or not isinstance(count, (int, float)):
+ return None
+ return int(count) if count > 0 else None
diff --git a/invokeai/backend/model_manager/configs/factory.py b/invokeai/backend/model_manager/configs/factory.py
index 31f3b14619d..b0b7b0dd461 100644
--- a/invokeai/backend/model_manager/configs/factory.py
+++ b/invokeai/backend/model_manager/configs/factory.py
@@ -55,6 +55,7 @@
LoRA_LyCORIS_SD1_Config,
LoRA_LyCORIS_SD2_Config,
LoRA_LyCORIS_SDXL_Config,
+ LoRA_LyCORIS_Wan_Config,
LoRA_LyCORIS_ZImage_Config,
LoRA_OMI_FLUX_Config,
LoRA_OMI_SDXL_Config,
@@ -80,10 +81,12 @@
Main_Diffusers_SD3_Config,
Main_Diffusers_SDXL_Config,
Main_Diffusers_SDXLRefiner_Config,
+ Main_Diffusers_Wan_Config,
Main_Diffusers_ZImage_Config,
Main_GGUF_Flux2_Config,
Main_GGUF_FLUX_Config,
Main_GGUF_QwenImage_Config,
+ Main_GGUF_Wan_Config,
Main_GGUF_ZImage_Config,
MainModelDefaultSettings,
)
@@ -121,10 +124,13 @@
VAE_Checkpoint_SD1_Config,
VAE_Checkpoint_SD2_Config,
VAE_Checkpoint_SDXL_Config,
+ VAE_Checkpoint_Wan_Config,
VAE_Diffusers_Flux2_Config,
VAE_Diffusers_SD1_Config,
VAE_Diffusers_SDXL_Config,
+ VAE_Diffusers_Wan_Config,
)
+from invokeai.backend.model_manager.configs.wan_t5_encoder import WanT5Encoder_WanT5Encoder_Config
from invokeai.backend.model_manager.model_on_disk import ModelOnDisk
from invokeai.backend.model_manager.taxonomy import (
BaseModelType,
@@ -175,6 +181,7 @@
Annotated[Main_Diffusers_Flux2_Config, Main_Diffusers_Flux2_Config.get_tag()],
Annotated[Main_Diffusers_CogView4_Config, Main_Diffusers_CogView4_Config.get_tag()],
Annotated[Main_Diffusers_QwenImage_Config, Main_Diffusers_QwenImage_Config.get_tag()],
+ Annotated[Main_Diffusers_Wan_Config, Main_Diffusers_Wan_Config.get_tag()],
Annotated[Main_Diffusers_ZImage_Config, Main_Diffusers_ZImage_Config.get_tag()],
# Main (Pipeline) - checkpoint format
# IMPORTANT: FLUX.2 must be checked BEFORE FLUX.1 because FLUX.2 has specific validation
@@ -195,6 +202,7 @@
Annotated[Main_GGUF_Flux2_Config, Main_GGUF_Flux2_Config.get_tag()],
Annotated[Main_GGUF_FLUX_Config, Main_GGUF_FLUX_Config.get_tag()],
Annotated[Main_GGUF_QwenImage_Config, Main_GGUF_QwenImage_Config.get_tag()],
+ Annotated[Main_GGUF_Wan_Config, Main_GGUF_Wan_Config.get_tag()],
Annotated[Main_GGUF_ZImage_Config, Main_GGUF_ZImage_Config.get_tag()],
# VAE - checkpoint format
Annotated[VAE_Checkpoint_SD1_Config, VAE_Checkpoint_SD1_Config.get_tag()],
@@ -202,12 +210,18 @@
Annotated[VAE_Checkpoint_SDXL_Config, VAE_Checkpoint_SDXL_Config.get_tag()],
Annotated[VAE_Checkpoint_FLUX_Config, VAE_Checkpoint_FLUX_Config.get_tag()],
Annotated[VAE_Checkpoint_Flux2_Config, VAE_Checkpoint_Flux2_Config.get_tag()],
+ # IMPORTANT: VAE_Checkpoint_Wan_Config must be checked BEFORE QwenImage —
+ # both share the AutoencoderKLWan architecture and the Wan config relies
+ # on a filename heuristic to claim 16-channel files; ordering here lets
+ # Wan win when the filename suggests it.
+ Annotated[VAE_Checkpoint_Wan_Config, VAE_Checkpoint_Wan_Config.get_tag()],
Annotated[VAE_Checkpoint_QwenImage_Config, VAE_Checkpoint_QwenImage_Config.get_tag()],
Annotated[VAE_Checkpoint_Anima_Config, VAE_Checkpoint_Anima_Config.get_tag()],
# VAE - diffusers format
Annotated[VAE_Diffusers_SD1_Config, VAE_Diffusers_SD1_Config.get_tag()],
Annotated[VAE_Diffusers_SDXL_Config, VAE_Diffusers_SDXL_Config.get_tag()],
Annotated[VAE_Diffusers_Flux2_Config, VAE_Diffusers_Flux2_Config.get_tag()],
+ Annotated[VAE_Diffusers_Wan_Config, VAE_Diffusers_Wan_Config.get_tag()],
# ControlNet - checkpoint format
Annotated[ControlNet_Checkpoint_SD1_Config, ControlNet_Checkpoint_SD1_Config.get_tag()],
Annotated[ControlNet_Checkpoint_SD2_Config, ControlNet_Checkpoint_SD2_Config.get_tag()],
@@ -230,6 +244,13 @@
Annotated[LoRA_LyCORIS_FLUX_Config, LoRA_LyCORIS_FLUX_Config.get_tag()],
Annotated[LoRA_LyCORIS_ZImage_Config, LoRA_LyCORIS_ZImage_Config.get_tag()],
Annotated[LoRA_LyCORIS_QwenImage_Config, LoRA_LyCORIS_QwenImage_Config.get_tag()],
+ # Wan and Anima both target ``blocks.X`` shapes; their LoRA probes are
+ # mutually exclusive — Wan rejects Anima's ``_proj``/``mlp``/
+ # ``adaln_modulation`` markers, Anima requires at least one of those
+ # markers (see ``has_cosmos_dit_*_keys_strict``). Order between these
+ # two doesn't affect correctness; mutual exclusivity is locked in by
+ # ``test_wan_lora_probe_independence.py``.
+ Annotated[LoRA_LyCORIS_Wan_Config, LoRA_LyCORIS_Wan_Config.get_tag()],
Annotated[LoRA_LyCORIS_Anima_Config, LoRA_LyCORIS_Anima_Config.get_tag()],
# LoRA - OMI format
Annotated[LoRA_OMI_SDXL_Config, LoRA_OMI_SDXL_Config.get_tag()],
@@ -255,6 +276,8 @@
# Qwen VL Encoder (Qwen2.5-VL multimodal encoder for Qwen Image)
Annotated[QwenVLEncoder_Diffusers_Config, QwenVLEncoder_Diffusers_Config.get_tag()],
Annotated[QwenVLEncoder_Checkpoint_Config, QwenVLEncoder_Checkpoint_Config.get_tag()],
+ # Wan T5 Encoder (UMT5-XXL for Wan 2.2)
+ Annotated[WanT5Encoder_WanT5Encoder_Config, WanT5Encoder_WanT5Encoder_Config.get_tag()],
# TI - file format
Annotated[TI_File_SD1_Config, TI_File_SD1_Config.get_tag()],
Annotated[TI_File_SD2_Config, TI_File_SD2_Config.get_tag()],
diff --git a/invokeai/backend/model_manager/configs/lora.py b/invokeai/backend/model_manager/configs/lora.py
index fdaebe38565..9479dea8cdb 100644
--- a/invokeai/backend/model_manager/configs/lora.py
+++ b/invokeai/backend/model_manager/configs/lora.py
@@ -28,14 +28,23 @@
FluxLoRAFormat,
ModelFormat,
ModelType,
+ WanLoRAVariantType,
ZImageVariantType,
)
from invokeai.backend.model_manager.util.model_util import lora_token_vector_length
from invokeai.backend.patches.lora_conversions.anima_lora_constants import (
has_cosmos_dit_kohya_keys,
+ has_cosmos_dit_kohya_keys_strict,
has_cosmos_dit_peft_keys,
+ has_cosmos_dit_peft_keys_strict,
)
from invokeai.backend.patches.lora_conversions.flux_control_lora_utils import is_state_dict_likely_flux_control
+from invokeai.backend.patches.lora_conversions.wan_lora_constants import (
+ detect_wan_lora_variant,
+ has_non_wan_architecture_keys,
+ has_wan_kohya_keys,
+ has_wan_peft_keys,
+)
# Defaults used to compute the effective slider range when one or both bounds
# are unset. These intentionally mirror the frontend's DEFAULT_LORA_WEIGHT_CONFIG
@@ -909,16 +918,20 @@ def _validate_looks_like_lora(cls, mod: ModelOnDisk) -> None:
Anima LoRAs have keys like:
- lora_unet_blocks_0_cross_attn_k_proj.lora_down.weight (Kohya format)
- diffusion_model.blocks.0.cross_attn.k_proj.lora_A.weight (diffusers PEFT format)
- - transformer.blocks.0.cross_attn.k_proj.lora_A.weight (diffusers PEFT format)
-
- Detection requires Cosmos DiT-specific subcomponent names (cross_attn,
- self_attn, mlp, adaln_modulation) to avoid false-positives on other
- architectures that also use ``blocks`` in their paths.
+ - transformer.blocks.0.mlp.layer_0.lora_A.weight (Anima-only MLP layer)
+
+ Uses the **strict** Cosmos-DiT detectors, which require an
+ Anima-exclusive subcomponent name (``mlp``, ``adaln_modulation``, or
+ ``_proj``-suffixed attention). The loose detectors would also accept
+ Wan-native LoRAs (which use ``cross_attn``/``self_attn`` too but with
+ bare ``.q``/``.k``/``.v``/``.o`` rather than ``_proj``), so they're not
+ safe for first-match-wins probing — see the regression tests in
+ ``test_wan_lora_probe_independence.py``.
"""
state_dict = mod.load_state_dict()
str_keys = [k for k in state_dict.keys() if isinstance(k, str)]
- has_cosmos_keys = has_cosmos_dit_kohya_keys(str_keys) or has_cosmos_dit_peft_keys(str_keys)
+ has_cosmos_keys = has_cosmos_dit_kohya_keys_strict(str_keys) or has_cosmos_dit_peft_keys_strict(str_keys)
# Also check for LoRA/LoKR weight suffixes
has_lora_suffix = state_dict_has_any_keys_ending_with(
@@ -941,19 +954,112 @@ def _validate_looks_like_lora(cls, mod: ModelOnDisk) -> None:
@classmethod
def _get_base_or_raise(cls, mod: ModelOnDisk) -> BaseModelType:
- """Anima LoRAs target Cosmos DiT blocks (blocks.X.cross_attn, blocks.X.self_attn, etc.).
+ """Anima LoRAs target Cosmos DiT blocks (blocks.X.mlp, blocks.X.adaln_modulation,
+ blocks.X.cross_attn.q_proj, etc.).
- Uses Cosmos DiT-specific subcomponent names to avoid false-positives.
+ Uses the strict Cosmos-DiT detectors to be mutually exclusive with
+ Wan-LoRA detection — see ``_validate_looks_like_lora`` for rationale.
"""
state_dict = mod.load_state_dict()
str_keys = [k for k in state_dict.keys() if isinstance(k, str)]
- if has_cosmos_dit_kohya_keys(str_keys) or has_cosmos_dit_peft_keys(str_keys):
+ if has_cosmos_dit_kohya_keys_strict(str_keys) or has_cosmos_dit_peft_keys_strict(str_keys):
return BaseModelType.Anima
raise NotAMatchError("model does not look like an Anima LoRA")
+class LoRA_LyCORIS_Wan_Config(LoRA_LyCORIS_Config_Base, Config_Base):
+ """Model config for Wan 2.2 LoRA models in LyCORIS format.
+
+ Wan LoRAs target ``WanTransformer3DModel`` blocks. The Wan 2.2 A14B family
+ is dual-expert (high-noise + low-noise) — LoRAs are typically trained
+ against one expert. ``expert`` records which one so the model loader
+ invocation can wire it to the correct ``loras`` / ``loras_low_noise`` list.
+ Many LoRAs are expert-agnostic (TI2V-5B family, or community LoRAs that
+ just don't tag the expert) — these get ``expert=None`` and are applied to
+ both experts by default.
+ """
+
+ base: Literal[BaseModelType.Wan] = Field(default=BaseModelType.Wan)
+ expert: Literal["high", "low"] | None = Field(
+ default=None,
+ description="For Wan 2.2 A14B dual-expert LoRAs: 'high' targets the high-noise expert, "
+ "'low' targets the low-noise expert. None means the LoRA is expert-agnostic "
+ "(TI2V-5B, or community LoRAs without explicit tagging) and is applied to both.",
+ )
+ variant: WanLoRAVariantType | None = Field(
+ default=None,
+ description="The Wan model family this LoRA targets, detected from its inner-dim "
+ "(5120 -> A14B, 3072 -> TI2V-5B). A14B LoRAs are incompatible with TI2V-5B mains "
+ "(and vice versa) — they crash with a shape mismatch in the layer patcher. The "
+ "linear-view graph builder filters LoRAs on variant when building the LoRA "
+ "collection. None means the LoRA's inner-dim couldn't be identified.",
+ )
+
+ @classmethod
+ def _validate_looks_like_lora(cls, mod: ModelOnDisk) -> None:
+ """Wan LoRAs target attn1/attn2/ffn.net (diffusers form) or self_attn/cross_attn/ffn.N (native form)."""
+ state_dict = mod.load_state_dict()
+ str_keys = [k for k in state_dict.keys() if isinstance(k, str)]
+
+ has_wan_keys = has_wan_kohya_keys(str_keys) or has_wan_peft_keys(str_keys)
+ has_lora_suffix = state_dict_has_any_keys_ending_with(
+ state_dict,
+ {
+ "lora_A.weight",
+ "lora_B.weight",
+ "lora_down.weight",
+ "lora_up.weight",
+ "dora_scale",
+ ".lokr_w1",
+ ".lokr_w2",
+ },
+ )
+
+ # Reject if any non-Wan architecture signature is present. Without this
+ # guard a Wan LoRA could be falsely identified by Anima (cross_attn /
+ # self_attn name collision) or vice versa.
+ if has_wan_keys and has_lora_suffix and not has_non_wan_architecture_keys(str_keys):
+ return
+
+ raise NotAMatchError("model does not match Wan LoRA heuristics")
+
+ @classmethod
+ def _get_base_or_raise(cls, mod: ModelOnDisk) -> BaseModelType:
+ state_dict = mod.load_state_dict()
+ str_keys = [k for k in state_dict.keys() if isinstance(k, str)]
+
+ if (has_wan_kohya_keys(str_keys) or has_wan_peft_keys(str_keys)) and not has_non_wan_architecture_keys(
+ str_keys
+ ):
+ return BaseModelType.Wan
+
+ raise NotAMatchError("model does not look like a Wan LoRA")
+
+ @classmethod
+ def from_model_on_disk(cls, mod: ModelOnDisk, override_fields: dict[str, Any]) -> Self:
+ # Run the base-class probe (file-check, lora-suffix, base detection).
+ instance = super().from_model_on_disk(mod, override_fields)
+
+ # Auto-detect the expert tag from the filename if the user didn't
+ # override it. ``high_noise`` / ``low_noise`` / hyphenated / concatenated
+ # variants — mirrors the GGUF transformer probe's heuristic.
+ if instance.expert is None:
+ name = mod.path.stem.lower()
+ if any(s in name for s in ("high_noise", "high-noise", "highnoise")):
+ instance.expert = "high"
+ elif any(s in name for s in ("low_noise", "low-noise", "lownoise")):
+ instance.expert = "low"
+
+ # Auto-detect the model-family variant from inner_dim in the state
+ # dict. The override field skips this if the user has set it.
+ if instance.variant is None:
+ instance.variant = detect_wan_lora_variant(mod.load_state_dict())
+
+ return instance
+
+
class ControlAdapter_Config_Base(ABC, BaseModel):
default_settings: ControlAdapterDefaultSettings | None = Field(None)
diff --git a/invokeai/backend/model_manager/configs/main.py b/invokeai/backend/model_manager/configs/main.py
index 10835b389fc..da416c9c169 100644
--- a/invokeai/backend/model_manager/configs/main.py
+++ b/invokeai/backend/model_manager/configs/main.py
@@ -33,6 +33,7 @@
QwenImageVariantType,
SchedulerPredictionType,
SubModelType,
+ WanVariantType,
ZImageVariantType,
)
from invokeai.backend.quantization.gguf.ggml_tensor import GGMLTensor
@@ -65,7 +66,12 @@ class MainModelDefaultSettings(BaseModel):
def from_base(
cls,
base: BaseModelType,
- variant: Flux2VariantType | FluxVariantType | ModelVariantType | ZImageVariantType | None = None,
+ variant: Flux2VariantType
+ | FluxVariantType
+ | ModelVariantType
+ | WanVariantType
+ | ZImageVariantType
+ | None = None,
) -> Self | None:
match base:
case BaseModelType.StableDiffusion1:
@@ -95,6 +101,12 @@ def from_base(
return cls(steps=4, cfg_scale=1.0, width=1024, height=1024)
case BaseModelType.QwenImage:
return cls(steps=40, cfg_scale=4.0, width=1024, height=1024)
+ case BaseModelType.Wan:
+ # Wan 2.2 recommended defaults differ by variant.
+ if variant == WanVariantType.TI2V_5B:
+ return cls(steps=30, cfg_scale=5.0, width=1024, height=1024)
+ # Default to A14B settings (also used when variant is unknown).
+ return cls(steps=40, cfg_scale=4.0, width=1024, height=1024)
case _:
# TODO(psyche): Do we want defaults for other base types?
return None
@@ -1439,6 +1451,277 @@ def from_model_on_disk(cls, mod: ModelOnDisk, override_fields: dict[str, Any]) -
return cls(**override_fields, variant=explicit_variant)
+def _has_wan_keys(state_dict: dict[str | int, Any]) -> bool:
+ """Check if state dict contains Wan 2.2 transformer keys.
+
+ Two layouts are accepted:
+
+ * **Diffusers** (city96-style GGUF, Wan-AI/*-Diffusers safetensors): the text
+ projection is named ``condition_embedder.text_embedder.linear_1``.
+ * **Native upstream** (QuantStack-style GGUF, ComfyUI, Wan-AI's non-Diffusers
+ releases): the text projection is named ``text_embedding.0``.
+
+ Both layouts share ``patch_embedding.weight`` as the input conv. Combined with
+ the text-projection fingerprint, this won't collide with FLUX
+ (``double_blocks/single_blocks``), Qwen Image (``txt_in/img_in``), Z-Image
+ (``cap_embedder``), or Anima (``llm_adapter``).
+
+ Tolerates both bare keys and the ComfyUI ``model.diffusion_model.`` /
+ ``diffusion_model.`` prefixes.
+ """
+ text_proj_options = (
+ "condition_embedder.text_embedder.linear_1.weight",
+ "text_embedding.0.weight",
+ )
+ prefixes = ("", "model.diffusion_model.", "diffusion_model.")
+ keys = state_dict.keys()
+ if not any((p + "patch_embedding.weight") in keys for p in prefixes):
+ return False
+ return any((p + needle) in keys for p in prefixes for needle in text_proj_options)
+
+
+def _is_native_wan_layout(state_dict: dict[str | int, Any]) -> bool:
+ """True if the state dict uses the native upstream Wan key layout.
+
+ Native layout uses ``text_embedding.0/2``, ``self_attn``/``cross_attn``,
+ ``ffn.0/2``, ``head.head``, ``head.modulation``, etc. — what ComfyUI and
+ QuantStack ship. Diffusers layout uses ``condition_embedder.*``, ``attn1``/
+ ``attn2``, ``ffn.net.*``, ``proj_out``, ``scale_shift_table``.
+ """
+ prefixes = ("", "model.diffusion_model.", "diffusion_model.")
+ keys = state_dict.keys()
+ return any((p + "text_embedding.0.weight") in keys for p in prefixes)
+
+
+def _detect_wan_gguf_variant(state_dict: dict[str | int, Any]) -> WanVariantType | None:
+ """Determine A14B (T2V vs I2V) vs TI2V-5B from the GGUF state dict.
+
+ ``patch_embedding.weight`` has shape ``[inner_dim, in_channels, T, H, W]``;
+ ``in_channels`` uniquely identifies the Wan 2.2 variant:
+
+ - 16 → T2V-A14B (noise latents only).
+ - 36 → I2V-A14B (16 noise + 16 ref-image latents + 4 first-frame mask,
+ concatenated along the channel dim — see diffusers
+ ``WanImageToVideoPipeline.prepare_latents``).
+ - 48 → TI2V-5B (Wan2.2-VAE z_dim=48).
+
+ Returns None if the tensor is missing or the channel count is unrecognised.
+ """
+ candidates = (
+ "patch_embedding.weight",
+ "model.diffusion_model.patch_embedding.weight",
+ "diffusion_model.patch_embedding.weight",
+ )
+ for key in candidates:
+ if key in state_dict:
+ tensor = state_dict[key]
+ shape = getattr(tensor, "tensor_shape", None) or getattr(tensor, "shape", None)
+ if shape is None or len(shape) < 2:
+ return None
+ in_channels = int(shape[1])
+ if in_channels == 16:
+ return WanVariantType.T2V_A14B
+ if in_channels == 36:
+ return WanVariantType.I2V_A14B
+ if in_channels == 48:
+ return WanVariantType.TI2V_5B
+ return None
+ return None
+
+
+def _detect_wan_gguf_expert(filename: str) -> Literal["high", "low", "none"]:
+ """Filename heuristic for the A14B dual-expert MoE.
+
+ Community releases tag each expert in the filename — typically
+ ``high_noise`` / ``low_noise`` (or hyphenated/concatenated variants).
+ Returns 'none' when neither marker is present (single-expert model or
+ ambiguous filename).
+ """
+ name = filename.lower()
+ if any(s in name for s in ("high_noise", "high-noise", "highnoise")):
+ return "high"
+ if any(s in name for s in ("low_noise", "low-noise", "lownoise")):
+ return "low"
+ return "none"
+
+
+class Main_GGUF_Wan_Config(Checkpoint_Config_Base, Main_Config_Base, Config_Base):
+ """Model config for GGUF-quantized Wan 2.2 transformer models.
+
+ A14B's MoE ships as two GGUF files (one per expert); ``expert`` records
+ which one this is so the model loader invocation can pair them. TI2V-5B
+ is a single-transformer model and stores ``expert='none'``.
+ """
+
+ base: Literal[BaseModelType.Wan] = Field(default=BaseModelType.Wan)
+ format: Literal[ModelFormat.GGUFQuantized] = Field(default=ModelFormat.GGUFQuantized)
+ variant: WanVariantType = Field()
+ expert: Literal["high", "low", "none"] = Field(
+ default="none",
+ description="For Wan 2.2 A14B's dual-expert MoE: 'high' for the high-noise expert, "
+ "'low' for the low-noise expert. 'none' for single-transformer models (TI2V-5B).",
+ )
+
+ @classmethod
+ def from_model_on_disk(cls, mod: ModelOnDisk, override_fields: dict[str, Any]) -> Self:
+ raise_if_not_file(mod)
+ raise_for_override_fields(cls, override_fields)
+
+ sd = mod.load_state_dict()
+
+ if not _has_ggml_tensors(sd):
+ raise NotAMatchError("state dict does not look like GGUF quantized")
+ if not _has_wan_keys(sd):
+ raise NotAMatchError("state dict does not look like a Wan transformer")
+ gguf_name = mod.metadata().get("general.name", "")
+ normalized_identity = "".join(
+ character for character in f"{mod.path.stem} {gguf_name}".lower() if character.isalnum()
+ )
+ if "wan21" in normalized_identity:
+ raise NotAMatchError("Wan 2.1 GGUF models are not supported by the Wan 2.2 loader")
+
+ explicit_variant = override_fields.pop("variant", None)
+ variant = explicit_variant or _detect_wan_gguf_variant(sd)
+ if variant is None:
+ raise NotAMatchError("could not determine Wan variant from state dict")
+ if variant in (WanVariantType.T2V_A14B, WanVariantType.I2V_A14B) and "wan22" not in normalized_identity:
+ raise NotAMatchError("Wan A14B GGUF filename or metadata must identify the model as Wan 2.2")
+
+ explicit_expert = override_fields.pop("expert", None)
+ expert = explicit_expert or _detect_wan_gguf_expert(mod.path.stem)
+
+ return cls(**override_fields, variant=variant, expert=expert)
+
+
+class Main_Diffusers_Wan_Config(Diffusers_Config_Base, Main_Config_Base, Config_Base):
+ """Model config for Wan 2.2 diffusers models.
+
+ Covers both the dual-expert T2V-A14B family and the single-transformer TI2V-5B
+ family. Variant is detected from the on-disk transformer config (latent channel
+ count) plus the presence of a sibling ``transformer_2/`` directory.
+ """
+
+ base: Literal[BaseModelType.Wan] = Field(default=BaseModelType.Wan)
+ variant: WanVariantType = Field()
+ has_dual_expert: bool = Field(
+ default=False,
+ description="Whether this model ships two transformer experts (Wan 2.2 A14B MoE). False for TI2V-5B.",
+ )
+ boundary_ratio: float | None = Field(
+ default=None,
+ description="MoE expert switch point as a fraction of num_train_timesteps (typically 1000). "
+ "None for single-transformer models. Read from model_index.json by Diffusers' WanPipeline.",
+ )
+
+ @classmethod
+ def from_model_on_disk(cls, mod: ModelOnDisk, override_fields: dict[str, Any]) -> Self:
+ raise_if_not_dir(mod)
+
+ raise_for_override_fields(cls, override_fields)
+
+ # Wan repos ship with WanPipeline (T2V) or WanImageToVideoPipeline (I2V/TI2V).
+ # Either class name is sufficient to identify a Wan diffusers model.
+ raise_for_class_name(
+ common_config_paths(mod.path),
+ {
+ "WanPipeline",
+ "WanImageToVideoPipeline",
+ },
+ )
+
+ repo_variant = override_fields.pop("repo_variant", None) or cls._get_repo_variant_or_raise(mod)
+
+ explicit_variant = override_fields.pop("variant", None)
+ has_dual_expert = (mod.path / "transformer_2" / "config.json").exists()
+ variant = explicit_variant or cls._detect_wan_variant(mod, has_dual_expert)
+ boundary_ratio = override_fields.pop("boundary_ratio", None)
+ if boundary_ratio is None:
+ boundary_ratio = cls._read_boundary_ratio(mod)
+
+ return cls(
+ **override_fields,
+ repo_variant=repo_variant,
+ variant=variant,
+ has_dual_expert=has_dual_expert,
+ boundary_ratio=boundary_ratio,
+ )
+
+ @classmethod
+ def _read_boundary_ratio(cls, mod: ModelOnDisk) -> float | None:
+ """Pull ``boundary_ratio`` from ``model_index.json`` if present.
+
+ Diffusers' ``WanPipeline.__init__`` registers it via ``register_to_config``,
+ which persists it as a top-level key in the saved pipeline config.
+ """
+ try:
+ model_index = get_config_dict_or_raise(mod.path / "model_index.json")
+ except NotAMatchError:
+ return None
+ value = model_index.get("boundary_ratio")
+ if value is None:
+ return None
+ try:
+ return float(value)
+ except (TypeError, ValueError):
+ return None
+
+ @classmethod
+ def _detect_wan_variant(cls, mod: ModelOnDisk, has_dual_expert: bool) -> WanVariantType:
+ """Detect Wan variant from transformer + VAE config.
+
+ - T2V-A14B: dual transformer experts, standard Wan VAE (z_dim=16),
+ transformer ``in_channels=16`` (text-only conditioning).
+ - I2V-A14B: dual transformer experts, standard Wan VAE,
+ transformer ``in_channels=36`` (text + VAE-encoded reference image
+ + first-frame mask concatenated along the channel dim).
+ - TI2V-5B: single transformer, Wan2.2-VAE (z_dim=48).
+ """
+ if has_dual_expert:
+ # Disambiguate T2V vs I2V via the transformer's input channel count.
+ # Wan 2.2 I2V uses VAE-latent concatenation: 16 noise + 16 ref-image
+ # latents + 4 first-frame mask = 36. (Wan 2.1 I2V used CLIP-vision
+ # via ``image_dim``; that mechanism is absent in Wan 2.2.)
+ in_channels = cls._transformer_in_channels(mod)
+ if in_channels == 36:
+ return WanVariantType.I2V_A14B
+ return WanVariantType.T2V_A14B
+
+ # Single-transformer model: distinguish TI2V-5B from any future single-expert
+ # A14B-derived release by inspecting the VAE latent dimension.
+ try:
+ vae_config = get_config_dict_or_raise(mod.path / "vae" / "config.json")
+ z_dim = vae_config.get("z_dim")
+ if z_dim is not None and int(z_dim) >= 32:
+ return WanVariantType.TI2V_5B
+ except NotAMatchError:
+ # No VAE config to inspect — fall through to the heuristic path below.
+ pass
+
+ # Filename / repo-name heuristic as a last resort.
+ name = mod.path.name.lower()
+ if "5b" in name or "ti2v" in name:
+ return WanVariantType.TI2V_5B
+ return WanVariantType.T2V_A14B
+
+ @staticmethod
+ def _transformer_in_channels(mod: ModelOnDisk) -> int | None:
+ """Read ``in_channels`` from ``transformer/config.json``.
+
+ For Wan 2.2 A14B, this is the canonical discriminator between T2V
+ (``in_channels=16``) and I2V (``in_channels=36``). Returns None if the
+ config can't be read.
+ """
+ try:
+ transformer_config = get_config_dict_or_raise(mod.path / "transformer" / "config.json")
+ except NotAMatchError:
+ return None
+ value = transformer_config.get("in_channels")
+ try:
+ return int(value) if value is not None else None
+ except (TypeError, ValueError):
+ return None
+
+
class Main_Checkpoint_Anima_Config(Checkpoint_Config_Base, Main_Config_Base, Config_Base):
"""Model config for Anima single-file checkpoint models (safetensors).
diff --git a/invokeai/backend/model_manager/configs/vae.py b/invokeai/backend/model_manager/configs/vae.py
index 30735b443ed..ba1f17f01dd 100644
--- a/invokeai/backend/model_manager/configs/vae.py
+++ b/invokeai/backend/model_manager/configs/vae.py
@@ -40,6 +40,11 @@ def _is_qwen_image_vae(state_dict: dict[str | int, Any]) -> bool:
1. Diffusers-format encoder/decoder keys (`encoder.conv_in`, `decoder.conv_in`)
2. 5-dimensional convolution weights (3D causal convolutions vs. standard 2D conv in SD/SDXL/FLUX VAEs)
3. 16-dimensional latent space (z_dim=16)
+
+ Note: Wan 2.2 A14B reuses the same architecture (AutoencoderKLWan with z_dim=16),
+ so this function returns True for both. Disambiguation between the two for
+ standalone files relies on the filename heuristic in :func:`_is_wan_vae` and
+ config registration order.
"""
decoder_conv_in_key = "decoder.conv_in.weight"
if decoder_conv_in_key not in state_dict:
@@ -52,6 +57,34 @@ def _is_qwen_image_vae(state_dict: dict[str | int, Any]) -> bool:
return shape[1] == 16
+def _wan_vae_z_dim(state_dict: dict[str | int, Any]) -> int | None:
+ """Return ``z_dim`` for a Wan-family VAE state dict, or ``None`` if it isn't one.
+
+ Wan-family VAEs (AutoencoderKLWan) have 5D convolution weights and a
+ decoder.conv_in input channel count of 16 (Wan 2.1 / A14B / Qwen Image) or
+ 48 (Wan 2.2 TI2V-5B's Wan2.2-VAE).
+ """
+ decoder_conv_in_key = "decoder.conv_in.weight"
+ if decoder_conv_in_key not in state_dict:
+ return None
+ weight = state_dict[decoder_conv_in_key]
+ shape = getattr(weight, "shape", None)
+ if shape is None or len(shape) != 5:
+ return None
+ z = int(shape[1])
+ return z if z in (16, 48) else None
+
+
+def _filename_suggests_wan(mod: ModelOnDisk) -> bool:
+ """Filename heuristic to distinguish standalone Wan VAE files from Qwen Image VAEs.
+
+ Both use the same ``AutoencoderKLWan`` architecture for 16-channel files, so the
+ state dict alone can't tell them apart. Filenames in the wild (community ports,
+ ComfyUI repacks) typically include ``wan`` for Wan releases.
+ """
+ return "wan" in mod.path.name.lower()
+
+
def _is_flux2_vae(state_dict: dict[str | int, Any]) -> bool:
"""Check if state dict is a FLUX.2 VAE (AutoencoderKLFlux2).
@@ -114,9 +147,10 @@ def _validate_looks_like_vae(cls, mod: ModelOnDisk) -> None:
if _is_flux2_vae(state_dict):
raise NotAMatchError("model is a FLUX.2 VAE, not a standard VAE")
- # Exclude Qwen Image VAEs - they have their own config class
- if _is_qwen_image_vae(state_dict):
- raise NotAMatchError("model is a Qwen Image VAE, not a standard VAE")
+ # Exclude Qwen Image / Wan VAEs - they share the AutoencoderKLWan
+ # architecture and each has its own config class.
+ if _is_qwen_image_vae(state_dict) or _wan_vae_z_dim(state_dict) is not None:
+ raise NotAMatchError("model is a Wan-family VAE, not a standard VAE")
@classmethod
def _get_base_or_raise(cls, mod: ModelOnDisk) -> BaseModelType:
@@ -218,9 +252,98 @@ def from_model_on_disk(cls, mod: ModelOnDisk, override_fields: dict[str, Any]) -
if not _is_qwen_image_vae(state_dict):
raise NotAMatchError("state dict does not look like a Qwen Image VAE")
+ # Defer to VAE_Checkpoint_Wan_Config for files whose names indicate Wan
+ # (both architectures are 16-channel AutoencoderKLWan and otherwise
+ # indistinguishable from the state dict alone).
+ if _filename_suggests_wan(mod):
+ raise NotAMatchError("filename suggests a Wan VAE, not Qwen Image")
+
return cls(**override_fields)
+class VAE_Checkpoint_Wan_Config(Checkpoint_Config_Base, Config_Base):
+ """Model config for Wan 2.2 VAE checkpoint models (AutoencoderKLWan).
+
+ Distinguishes A14B (z_dim=16, standard Wan VAE) from TI2V-5B (z_dim=48,
+ Wan2.2-VAE) via the input channel count of ``decoder.conv_in.weight``.
+ """
+
+ type: Literal[ModelType.VAE] = Field(default=ModelType.VAE)
+ format: Literal[ModelFormat.Checkpoint] = Field(default=ModelFormat.Checkpoint)
+ base: Literal[BaseModelType.Wan] = Field(default=BaseModelType.Wan)
+ cpu_only: bool | None = Field(default=None, description="Whether this model should run on CPU only")
+ latent_channels: Literal[16, 48] = Field(
+ description="VAE latent channel count: 16 for A14B (standard Wan VAE) or 48 for TI2V-5B (Wan2.2-VAE)."
+ )
+
+ @classmethod
+ def from_model_on_disk(cls, mod: ModelOnDisk, override_fields: dict[str, Any]) -> Self:
+ raise_if_not_file(mod)
+
+ raise_for_override_fields(cls, override_fields)
+
+ state_dict = mod.load_state_dict()
+ z_dim = _wan_vae_z_dim(state_dict)
+ if z_dim is None:
+ raise NotAMatchError("state dict does not look like a Wan VAE")
+
+ # 48-channel files are unambiguously Wan2.2-VAE (TI2V-5B). 16-channel
+ # files are architecturally identical to Qwen Image's VAE; require the
+ # filename to suggest Wan to claim them, otherwise let the QwenImage
+ # config win.
+ latent_channels: int = z_dim
+ if latent_channels == 16 and not _filename_suggests_wan(mod):
+ raise NotAMatchError(
+ "16-channel AutoencoderKLWan VAE without 'wan' in filename — deferring to Qwen Image VAE config."
+ )
+
+ explicit = override_fields.pop("latent_channels", None)
+ if explicit is not None:
+ latent_channels = int(explicit)
+
+ return cls(**override_fields, latent_channels=latent_channels)
+
+
+class VAE_Diffusers_Wan_Config(Diffusers_Config_Base, Config_Base):
+ """Model config for Wan 2.2 VAE in diffusers folder layout (AutoencoderKLWan)."""
+
+ type: Literal[ModelType.VAE] = Field(default=ModelType.VAE)
+ format: Literal[ModelFormat.Diffusers] = Field(default=ModelFormat.Diffusers)
+ base: Literal[BaseModelType.Wan] = Field(default=BaseModelType.Wan)
+ cpu_only: bool | None = Field(default=None, description="Whether this model should run on CPU only")
+ latent_channels: Literal[16, 48] = Field(
+ default=16,
+ description="VAE latent channel count: 16 for A14B or 48 for TI2V-5B's Wan2.2-VAE.",
+ )
+
+ @classmethod
+ def from_model_on_disk(cls, mod: ModelOnDisk, override_fields: dict[str, Any]) -> Self:
+ raise_if_not_dir(mod)
+
+ raise_for_override_fields(cls, override_fields)
+
+ raise_for_class_name(
+ common_config_paths(mod.path),
+ {"AutoencoderKLWan"},
+ )
+
+ # Read z_dim from the diffusers config to set latent_channels.
+ latent_channels: int = 16
+ try:
+ config = get_config_dict_or_raise(common_config_paths(mod.path))
+ z = config.get("z_dim")
+ if z is not None and int(z) in (16, 48):
+ latent_channels = int(z)
+ except NotAMatchError:
+ pass
+
+ explicit = override_fields.pop("latent_channels", None)
+ if explicit is not None:
+ latent_channels = int(explicit)
+
+ return cls(**override_fields, latent_channels=latent_channels)
+
+
def _has_anima_vae_keys(state_dict: dict[str | int, Any]) -> bool:
"""Check if state dict looks like an Anima QwenImage VAE (AutoencoderKLQwenImage).
diff --git a/invokeai/backend/model_manager/configs/wan_t5_encoder.py b/invokeai/backend/model_manager/configs/wan_t5_encoder.py
new file mode 100644
index 00000000000..277377516b4
--- /dev/null
+++ b/invokeai/backend/model_manager/configs/wan_t5_encoder.py
@@ -0,0 +1,86 @@
+"""Configurations for the UMT5-XXL text encoder used by Wan 2.2.
+
+Wan ships a UMT5-XXL encoder (not the more common T5-XXL). The two are not
+weight-compatible — UMT5 has a different vocabulary and ``model_type``. We
+register a dedicated config + ModelType so users can't accidentally wire a
+FLUX/SD3-style T5-XXL into a Wan slot.
+
+For Phase 3 we accept the diffusers-folder layout only. Single-file UMT5
+checkpoints are uncommon; if they show up later, a checkpoint config can be
+added alongside this one.
+"""
+
+from __future__ import annotations
+
+import json
+from pathlib import Path
+from typing import Any, Literal, Self
+
+from pydantic import Field
+
+from invokeai.backend.model_manager.configs.base import Config_Base
+from invokeai.backend.model_manager.configs.identification_utils import (
+ NotAMatchError,
+ raise_for_override_fields,
+ raise_if_not_dir,
+)
+from invokeai.backend.model_manager.model_on_disk import ModelOnDisk
+from invokeai.backend.model_manager.taxonomy import BaseModelType, ModelFormat, ModelType
+
+
+def _read_text_encoder_model_type(mod: ModelOnDisk) -> str | None:
+ """Return ``model_type`` from the encoder's ``config.json``.
+
+ Diffusers encoder folders may live at the root (``config.json``) or under a
+ ``text_encoder/`` subdirectory. UMT5-XXL sets ``model_type`` to ``"umt5"``;
+ a regular T5-XXL would be ``"t5"``.
+ """
+ candidates: list[Path] = [mod.path / "text_encoder" / "config.json", mod.path / "config.json"]
+ for path in candidates:
+ if path.exists():
+ try:
+ with path.open("r", encoding="utf-8") as f:
+ config = json.load(f)
+ except (json.JSONDecodeError, OSError):
+ continue
+ mt = config.get("model_type")
+ if isinstance(mt, str):
+ return mt.lower()
+ return None
+
+
+class WanT5Encoder_WanT5Encoder_Config(Config_Base):
+ """UMT5-XXL encoder in diffusers folder layout.
+
+ Accepts either:
+ - A directory containing ``text_encoder/`` (and typically ``tokenizer/``) ─ the
+ shape produced by ``Wan-AI/Wan2.2-T2V-A14B::text_encoder+tokenizer``.
+ - A bare ``text_encoder/`` directory whose own ``config.json`` declares
+ ``model_type: umt5``.
+ """
+
+ base: Literal[BaseModelType.Any] = Field(default=BaseModelType.Any)
+ type: Literal[ModelType.WanT5Encoder] = Field(default=ModelType.WanT5Encoder)
+ format: Literal[ModelFormat.WanT5Encoder] = Field(default=ModelFormat.WanT5Encoder)
+
+ cpu_only: bool | None = Field(default=None, description="Whether this model should run on CPU only")
+
+ @classmethod
+ def from_model_on_disk(cls, mod: ModelOnDisk, override_fields: dict[str, Any]) -> Self:
+ raise_if_not_dir(mod)
+ raise_for_override_fields(cls, override_fields)
+
+ # Refuse to claim full Wan pipelines — they should match Main_Diffusers_Wan_Config.
+ if (mod.path / "model_index.json").exists() or (mod.path / "transformer").exists():
+ raise NotAMatchError(
+ "directory looks like a full Wan pipeline (model_index.json or transformer/), "
+ "not a standalone Wan T5 encoder"
+ )
+
+ model_type = _read_text_encoder_model_type(mod)
+ if model_type is None:
+ raise NotAMatchError("no encoder config.json found at root or text_encoder/")
+ if model_type != "umt5":
+ raise NotAMatchError(f"encoder model_type is {model_type!r}, not 'umt5'")
+
+ return cls(**override_fields)
diff --git a/invokeai/backend/model_manager/load/model_loaders/lora.py b/invokeai/backend/model_manager/load/model_loaders/lora.py
index 15dfa376179..1b4f0cdebd0 100644
--- a/invokeai/backend/model_manager/load/model_loaders/lora.py
+++ b/invokeai/backend/model_manager/load/model_loaders/lora.py
@@ -63,6 +63,7 @@
)
from invokeai.backend.patches.lora_conversions.sd_lora_conversion_utils import lora_model_from_sd_state_dict
from invokeai.backend.patches.lora_conversions.sdxl_lora_conversion_utils import convert_sdxl_keys_to_diffusers_format
+from invokeai.backend.patches.lora_conversions.wan_lora_conversion_utils import lora_model_from_wan_state_dict
from invokeai.backend.patches.lora_conversions.z_image_lora_conversion_utils import lora_model_from_z_image_state_dict
@@ -175,6 +176,10 @@ def _load_model(
elif self._model_base == BaseModelType.Anima:
# Anima LoRAs use Kohya-style or diffusers PEFT format targeting Cosmos DiT blocks.
model = lora_model_from_anima_state_dict(state_dict=state_dict, alpha=None)
+ elif self._model_base == BaseModelType.Wan:
+ # Wan LoRAs use Kohya / diffusers PEFT / native PEFT formats targeting
+ # WanTransformer3DModel attention (attn1/attn2) and FFN blocks.
+ model = lora_model_from_wan_state_dict(state_dict=state_dict, alpha=None)
else:
raise ValueError(f"Unsupported LoRA base model: {self._model_base}")
diff --git a/invokeai/backend/model_manager/load/model_loaders/vae.py b/invokeai/backend/model_manager/load/model_loaders/vae.py
index 720821f3af8..e96fac447e9 100644
--- a/invokeai/backend/model_manager/load/model_loaders/vae.py
+++ b/invokeai/backend/model_manager/load/model_loaders/vae.py
@@ -10,6 +10,8 @@
VAE_Checkpoint_Anima_Config,
VAE_Checkpoint_Config_Base,
VAE_Checkpoint_QwenImage_Config,
+ VAE_Checkpoint_Wan_Config,
+ VAE_Diffusers_Wan_Config,
)
from invokeai.backend.model_manager.load.model_loader_registry import ModelLoaderRegistry
from invokeai.backend.model_manager.load.model_loaders.generic_diffusers import GenericDiffusersLoader
@@ -21,6 +23,133 @@
SubModelType,
)
+# Architectural defaults for the Wan 2.2-VAE (TI2V-5B). Verbatim from the
+# vae/config.json shipped with Wan-AI/Wan2.2-TI2V-5B-Diffusers — only the
+# values that differ from diffusers' AutoencoderKLWan defaults are listed.
+# latents_mean / latents_std are required because the model normalises latents
+# against them at encode/decode time; the wrong arrays produce silent garbage.
+_WAN_TI2V_5B_VAE_CONFIG: dict = {
+ "base_dim": 160,
+ "decoder_base_dim": 256,
+ "z_dim": 48,
+ "in_channels": 12,
+ "out_channels": 12,
+ "patch_size": 2,
+ "scale_factor_spatial": 16,
+ "is_residual": True,
+ "latents_mean": [
+ -0.2289,
+ -0.0052,
+ -0.1323,
+ -0.2339,
+ -0.2799,
+ 0.0174,
+ 0.1838,
+ 0.1557,
+ -0.1382,
+ 0.0542,
+ 0.2813,
+ 0.0891,
+ 0.1570,
+ -0.0098,
+ 0.0375,
+ -0.1825,
+ -0.2246,
+ -0.1207,
+ -0.0698,
+ 0.5109,
+ 0.2665,
+ -0.2108,
+ -0.2158,
+ 0.2502,
+ -0.2055,
+ -0.0322,
+ 0.1109,
+ 0.1567,
+ -0.0729,
+ 0.0899,
+ -0.2799,
+ -0.1230,
+ -0.0313,
+ -0.1649,
+ 0.0117,
+ 0.0723,
+ -0.2839,
+ -0.2083,
+ -0.0520,
+ 0.3748,
+ 0.0152,
+ 0.1957,
+ 0.1433,
+ -0.2944,
+ 0.3573,
+ -0.0548,
+ -0.1681,
+ -0.0667,
+ ],
+ "latents_std": [
+ 0.4765,
+ 1.0364,
+ 0.4514,
+ 1.1677,
+ 0.5313,
+ 0.4990,
+ 0.4818,
+ 0.5013,
+ 0.8158,
+ 1.0344,
+ 0.5894,
+ 1.0901,
+ 0.6885,
+ 0.6165,
+ 0.8454,
+ 0.4978,
+ 0.5759,
+ 0.3523,
+ 0.7135,
+ 0.6804,
+ 0.5833,
+ 1.4146,
+ 0.8986,
+ 0.5659,
+ 0.7069,
+ 0.5338,
+ 0.4889,
+ 0.4917,
+ 0.4069,
+ 0.4999,
+ 0.6866,
+ 0.4093,
+ 0.5709,
+ 0.6065,
+ 0.6415,
+ 0.4944,
+ 0.5726,
+ 1.2042,
+ 0.5458,
+ 1.6887,
+ 0.3971,
+ 1.0600,
+ 0.3943,
+ 0.5537,
+ 0.5444,
+ 0.4089,
+ 0.7468,
+ 0.7744,
+ ],
+}
+
+
+def _wan_vae_init_kwargs_for(latent_channels: int) -> dict:
+ """Return the AutoencoderKLWan constructor kwargs for a given z_dim.
+
+ z_dim=48 means TI2V-5B's Wan 2.2-VAE (different base dim, patchified IO,
+ 16x spatial). Anything else falls back to the A14B / Wan 2.1 defaults.
+ """
+ if latent_channels == 48:
+ return dict(_WAN_TI2V_5B_VAE_CONFIG)
+ return {"z_dim": latent_channels}
+
@ModelLoaderRegistry.register(base=BaseModelType.Any, type=ModelType.VAE, format=ModelFormat.Diffusers)
@ModelLoaderRegistry.register(base=BaseModelType.Any, type=ModelType.VAE, format=ModelFormat.Checkpoint)
@@ -35,10 +164,17 @@ def _load_model(
if isinstance(config, VAE_Checkpoint_Anima_Config):
from diffusers.models.autoencoders import AutoencoderKLWan
+ from invokeai.backend.wan.rocm_causal_conv3d import patch_wan_causal_conv3d_for_rocm
+
+ patch_wan_causal_conv3d_for_rocm()
return AutoencoderKLWan.from_single_file(
config.path,
torch_dtype=self._torch_dtype,
)
+ elif isinstance(config, VAE_Checkpoint_Wan_Config):
+ return self._load_wan_vae(config)
+ elif isinstance(config, VAE_Diffusers_Wan_Config):
+ return self._load_wan_vae_diffusers(config)
elif isinstance(config, VAE_Checkpoint_QwenImage_Config):
return self._load_qwen_image_vae(config)
elif isinstance(config, VAE_Checkpoint_Config_Base):
@@ -49,6 +185,77 @@ def _load_model(
else:
return super()._load_model(config, submodel_type)
+ def _load_wan_vae(self, config: VAE_Checkpoint_Wan_Config) -> AnyModel:
+ """Load a Wan 2.2 VAE from a single safetensors file.
+
+ Picks the correct ``AutoencoderKLWan`` config based on ``z_dim``. The Wan
+ ecosystem ships two distinct VAE architectures:
+
+ * ``z_dim=16`` — the Wan 2.1 / Wan 2.2 A14B VAE. Diffusers' defaults match
+ this one (base_dim=96, 8x spatial, no patchify, 3 in/out channels).
+ * ``z_dim=48`` — the Wan 2.2-VAE used by TI2V-5B. Larger (base_dim=160,
+ decoder_base_dim=256), 16x spatial, patchify with patch_size=2 (so
+ in/out channels are 12 = 3 RGB x 2x2 patch), residual blocks, and
+ its own latents_mean / latents_std.
+
+ Without overriding those params at construction time, the state dict
+ from the TI2V-5B VAE checkpoint won't load (channel and shape mismatches
+ throughout the encoder + decoder).
+
+ Forces bfloat16 (same as ``WanDiffusersModel`` and ``_load_wan_vae_diffusers``) —
+ fp16 is unstable on the Wan VAE, and the default ``precision: auto`` resolves to
+ float16 on CUDA.
+ """
+ import accelerate
+ import torch
+ from diffusers.models.autoencoders.autoencoder_kl_wan import AutoencoderKLWan
+ from safetensors.torch import load_file
+
+ from invokeai.backend.wan.rocm_causal_conv3d import patch_wan_causal_conv3d_for_rocm
+
+ patch_wan_causal_conv3d_for_rocm()
+ sd = load_file(config.path)
+
+ for k in list(sd.keys()):
+ if sd[k].is_floating_point():
+ sd[k] = sd[k].to(torch.bfloat16)
+
+ new_sd_size = sum(t.nelement() * t.element_size() for t in sd.values())
+ self._ram_cache.make_room(new_sd_size)
+
+ init_kwargs = _wan_vae_init_kwargs_for(config.latent_channels)
+ with accelerate.init_empty_weights():
+ model = AutoencoderKLWan(**init_kwargs)
+
+ model.load_state_dict(sd, strict=True, assign=True)
+ model.eval()
+ return model
+
+ def _load_wan_vae_diffusers(self, config: VAE_Diffusers_Wan_Config) -> AnyModel:
+ """Load a Wan 2.2 VAE from a flat diffusers folder (AutoencoderKLWan).
+
+ The standalone install ``Wan-AI/Wan2.2-T2V-A14B-Diffusers::vae`` lands as a
+ single-class folder (``config.json`` + ``diffusion_pytorch_model.safetensors``,
+ no ``model_index.json``). The generic loader rejects this when a
+ ``submodel_type`` is requested — we always pass ``SubModelType.VAE`` from
+ the model loader invocation since that's how cached entries are keyed.
+ Loading ``AutoencoderKLWan`` directly here sidesteps the submodel check.
+
+ Forces bfloat16 (same as ``WanDiffusersModel``) — fp16 is unstable on the
+ Wan VAE.
+ """
+ import torch
+ from diffusers.models.autoencoders.autoencoder_kl_wan import AutoencoderKLWan
+
+ from invokeai.backend.wan.rocm_causal_conv3d import patch_wan_causal_conv3d_for_rocm
+
+ patch_wan_causal_conv3d_for_rocm()
+ return AutoencoderKLWan.from_pretrained(
+ config.path,
+ torch_dtype=torch.bfloat16,
+ local_files_only=True,
+ )
+
def _load_qwen_image_vae(self, config: VAE_Checkpoint_QwenImage_Config) -> AnyModel:
"""Load a Qwen Image VAE from a single safetensors file.
diff --git a/invokeai/backend/model_manager/load/model_loaders/wan.py b/invokeai/backend/model_manager/load/model_loaders/wan.py
new file mode 100644
index 00000000000..f9762a3144b
--- /dev/null
+++ b/invokeai/backend/model_manager/load/model_loaders/wan.py
@@ -0,0 +1,355 @@
+"""Loader registrations for Wan 2.2 image-generation models.
+
+Currently covers:
+- Main: Diffusers format (T2V-A14B with dual experts via Transformer +
+ Transformer2 submodels, plus TI2V-5B). Phase 4 will add a GGUFQuantized loader.
+- WanT5Encoder: standalone UMT5-XXL encoder folder (``text_encoder/`` +
+ ``tokenizer/`` subdirs, or a flat ``text_encoder/`` folder).
+- VAE: handled in ``vae.py`` (registered for type=VAE generically).
+"""
+
+from pathlib import Path
+from typing import Optional
+
+import torch
+
+from invokeai.backend.model_manager.configs.base import Checkpoint_Config_Base, Diffusers_Config_Base
+from invokeai.backend.model_manager.configs.factory import AnyModelConfig
+from invokeai.backend.model_manager.configs.main import Main_GGUF_Wan_Config, _is_native_wan_layout
+from invokeai.backend.model_manager.load.load_default import ModelLoader
+from invokeai.backend.model_manager.load.model_loader_registry import ModelLoaderRegistry
+from invokeai.backend.model_manager.load.model_loaders.generic_diffusers import GenericDiffusersLoader
+from invokeai.backend.model_manager.taxonomy import (
+ AnyModel,
+ BaseModelType,
+ ModelFormat,
+ ModelType,
+ SubModelType,
+ WanVariantType,
+)
+from invokeai.backend.quantization.gguf.ggml_tensor import GGMLTensor
+from invokeai.backend.quantization.gguf.loaders import gguf_sd_loader
+from invokeai.backend.quantization.gguf.utils import TORCH_COMPATIBLE_QTYPES
+from invokeai.backend.util.devices import TorchDevice
+
+
+@ModelLoaderRegistry.register(base=BaseModelType.Wan, type=ModelType.Main, format=ModelFormat.Diffusers)
+class WanDiffusersModel(GenericDiffusersLoader):
+ """Loader for Wan 2.2 diffusers-format models (T2V-A14B and TI2V-5B).
+
+ Forces bfloat16 for the transformer and VAE — fp16 is unstable on Wan VAE
+ (same issue affects the Flux VAE). Resolves the appropriate Hugging Face
+ class for each submodel via the parent loader's ``get_hf_load_class``.
+ """
+
+ def _load_model(
+ self,
+ config: AnyModelConfig,
+ submodel_type: Optional[SubModelType] = None,
+ ) -> AnyModel:
+ if isinstance(config, Checkpoint_Config_Base):
+ raise NotImplementedError("Single-file checkpoint format is not yet supported for Wan models.")
+
+ if submodel_type is None:
+ raise Exception("A submodel type must be provided when loading Wan main pipelines.")
+
+ if submodel_type is SubModelType.VAE:
+ from invokeai.backend.wan.rocm_causal_conv3d import patch_wan_causal_conv3d_for_rocm
+
+ patch_wan_causal_conv3d_for_rocm()
+
+ model_path = Path(config.path)
+ load_class = self.get_hf_load_class(model_path, submodel_type)
+ repo_variant = config.repo_variant if isinstance(config, Diffusers_Config_Base) else None
+ variant = repo_variant.value if repo_variant else None
+ model_path = model_path / submodel_type.value
+
+ def _load_with_variant_fallback(dtype_kwarg: dict[str, torch.dtype]) -> AnyModel:
+ # Some Wan repos ship without a fp16 variant suffix on every submodel.
+ # If the requested variant isn't on disk, fall back to the default weights.
+ try:
+ return load_class.from_pretrained(
+ model_path,
+ **dtype_kwarg,
+ variant=variant,
+ local_files_only=True,
+ )
+ except OSError as e:
+ if variant and "no file named" in str(e):
+ return load_class.from_pretrained(model_path, **dtype_kwarg, local_files_only=True)
+ raise
+
+ # bfloat16 across the board: matches Diffusers WanPipeline reference and
+ # avoids the fp16 instability seen in the Wan VAE.
+ try:
+ result: AnyModel = _load_with_variant_fallback({"dtype": torch.bfloat16})
+ except TypeError:
+ # Older diffusers releases use torch_dtype instead of dtype.
+ result = _load_with_variant_fallback({"torch_dtype": torch.bfloat16})
+
+ return result
+
+
+# Native (upstream) -> Diffusers key rename rules.
+#
+# Mirrors diffusers.loaders.single_file_utils.convert_wan_transformer_to_diffusers
+# (T2V subset; we don't ship VACE / motion / face-adapter conversion). Order
+# matters — `cross_attn`/`self_attn` must come before `.q. .k. .v. .o.` so the
+# attention blocks are renamed before the projection suffix swap. The norm2/3
+# swap uses a placeholder to avoid collisions during the substring rewrite.
+_WAN_NATIVE_TO_DIFFUSERS_RENAMES: tuple[tuple[str, str], ...] = (
+ ("time_embedding.0", "condition_embedder.time_embedder.linear_1"),
+ ("time_embedding.2", "condition_embedder.time_embedder.linear_2"),
+ ("text_embedding.0", "condition_embedder.text_embedder.linear_1"),
+ ("text_embedding.2", "condition_embedder.text_embedder.linear_2"),
+ ("time_projection.1", "condition_embedder.time_proj"),
+ ("cross_attn", "attn2"),
+ ("self_attn", "attn1"),
+ (".o.", ".to_out.0."),
+ (".q.", ".to_q."),
+ (".k.", ".to_k."),
+ (".v.", ".to_v."),
+ (".k_img.", ".add_k_proj."),
+ (".v_img.", ".add_v_proj."),
+ (".norm_k_img.", ".norm_added_k."),
+ ("head.modulation", "scale_shift_table"),
+ ("head.head", "proj_out"),
+ ("modulation", "scale_shift_table"),
+ ("ffn.0", "ffn.net.0.proj"),
+ ("ffn.2", "ffn.net.2"),
+ # norm2 <-> norm3 swap via placeholder
+ ("norm2", "norm__placeholder"),
+ ("norm3", "norm2"),
+ ("norm__placeholder", "norm3"),
+ # I2V-only keys (harmless on T2V)
+ ("img_emb.proj.0", "condition_embedder.image_embedder.norm1"),
+ ("img_emb.proj.1", "condition_embedder.image_embedder.ff.net.0.proj"),
+ ("img_emb.proj.3", "condition_embedder.image_embedder.ff.net.2"),
+ ("img_emb.proj.4", "condition_embedder.image_embedder.norm2"),
+)
+
+
+def _convert_wan_native_to_diffusers(state_dict: dict) -> dict:
+ """Rename native upstream Wan keys (ComfyUI / QuantStack) to diffusers names.
+
+ Pure substring replacement — no tensor manipulation — so it's safe to apply
+ to a dict of GGMLTensors. Returns a new dict; the input is not mutated.
+ """
+ converted: dict = {}
+ for key, value in state_dict.items():
+ if not isinstance(key, str):
+ converted[key] = value
+ continue
+ new_key = key
+ for needle, replacement in _WAN_NATIVE_TO_DIFFUSERS_RENAMES:
+ new_key = new_key.replace(needle, replacement)
+ converted[new_key] = value
+ return converted
+
+
+def _unwrap_unquantized_to_compute_dtype(state_dict: dict) -> dict:
+ """Replace non-quantized GGMLTensor entries with plain tensors at compute_dtype.
+
+ Why: QuantStack-style GGUFs store biases (and other small tensors) as F16,
+ while Wan's ``patch_embedding`` is an ``nn.Conv3d``. ``conv3d`` isn't in
+ GGMLTensor's dispatch table, so PyTorch reads the wrapper's underlying F16
+ storage directly and crashes against bf16 latents
+ (``Input type (c10::BFloat16) and bias type (c10::Half) should be the same``).
+
+ For compatible qtypes (F16/F32/BF16) we just pre-cast to compute_dtype here —
+ they're not quantized, there's no benefit to keeping them wrapped, and
+ unwrapping them sidesteps the missing-op problem entirely. Genuinely
+ quantized tensors (Q4_K, Q6_K, etc.) stay wrapped — their on-demand
+ dequantization through the linear/addmm dispatch path still works.
+ """
+ unwrapped: dict = {}
+ for key, value in state_dict.items():
+ if isinstance(value, GGMLTensor) and value._ggml_quantization_type in TORCH_COMPATIBLE_QTYPES:
+ # GGMLTensor.get_dequantized_tensor() already casts to compute_dtype.
+ unwrapped[key] = value.get_dequantized_tensor()
+ else:
+ unwrapped[key] = value
+ return unwrapped
+
+
+@ModelLoaderRegistry.register(base=BaseModelType.Wan, type=ModelType.Main, format=ModelFormat.GGUFQuantized)
+class WanGGUFCheckpointModel(ModelLoader):
+ """Loader for GGUF-quantized Wan 2.2 transformer models.
+
+ The community typically distributes Wan A14B as two files (one per expert
+ — high-noise + low-noise). Each file is loaded independently here; the
+ pairing happens at the WanModelLoaderInvocation layer. TI2V-5B ships as a
+ single file.
+
+ Mirrors the QwenImage GGUF loader pattern: ``gguf_sd_loader`` -> strip the
+ ComfyUI ``model.diffusion_model.`` / ``diffusion_model.`` prefix if present
+ -> auto-detect arch from state-dict shapes -> ``init_empty_weights`` +
+ ``load_state_dict(strict=False, assign=True)``.
+ """
+
+ def _load_model(
+ self,
+ config: AnyModelConfig,
+ submodel_type: Optional[SubModelType] = None,
+ ) -> AnyModel:
+ if not isinstance(config, Main_GGUF_Wan_Config):
+ raise TypeError(f"Expected Main_GGUF_Wan_Config, got {type(config).__name__}.")
+
+ if submodel_type != SubModelType.Transformer:
+ raise ValueError(
+ "Only the Transformer submodel is available from a GGUF Wan checkpoint. "
+ "Pair with a standalone Wan VAE and Wan T5 encoder for the other components."
+ )
+
+ return self._load_from_singlefile(config)
+
+ def _load_from_singlefile(self, config: Main_GGUF_Wan_Config) -> AnyModel:
+ import accelerate
+ from diffusers import WanTransformer3DModel
+
+ model_path = Path(config.path)
+ target_device = TorchDevice.choose_torch_device()
+ compute_dtype = TorchDevice.choose_bfloat16_safe_dtype(target_device)
+
+ sd = gguf_sd_loader(model_path, compute_dtype=compute_dtype)
+
+ # Strip ComfyUI-style prefixes if present.
+ for prefix in ("model.diffusion_model.", "diffusion_model."):
+ if any(isinstance(k, str) and k.startswith(prefix) for k in sd.keys()):
+ sd = {
+ (k[len(prefix) :] if isinstance(k, str) and k.startswith(prefix) else k): v for k, v in sd.items()
+ }
+ break
+
+ # QuantStack and other community releases ship the native upstream Wan key
+ # layout (text_embedding.0, self_attn/cross_attn, ffn.0/2, head.head, ...);
+ # diffusers' WanTransformer3DModel expects condition_embedder.*, attn1/attn2,
+ # ffn.net.*, proj_out. Convert in place if needed.
+ if _is_native_wan_layout(sd):
+ sd = _convert_wan_native_to_diffusers(sd)
+
+ # Pre-cast non-quantized tensors (F16/F32/BF16 biases, scale_shift_table,
+ # patch_embedding.weight, etc.) to compute_dtype. This avoids dtype
+ # mismatches in conv3d at the input (patch_embedding is the only Conv3d
+ # in WanTransformer3DModel; conv3d isn't in GGMLTensor's dispatch table
+ # so the wrapper's underlying storage dtype reaches PyTorch directly).
+ sd = _unwrap_unquantized_to_compute_dtype(sd)
+
+ # Auto-detect architecture from the state dict.
+ num_layers = 0
+ for key in sd.keys():
+ if isinstance(key, str) and key.startswith("blocks."):
+ parts = key.split(".")
+ if len(parts) >= 2:
+ try:
+ num_layers = max(num_layers, int(parts[1]) + 1)
+ except ValueError:
+ pass
+
+ # Patch embedding gives us in_channels (16=A14B, 48=TI2V-5B) and inner dim.
+ patch_w = sd.get("patch_embedding.weight")
+ if patch_w is None:
+ raise RuntimeError("GGUF state dict missing patch_embedding.weight after prefix strip")
+ patch_shape = patch_w.tensor_shape if isinstance(patch_w, GGMLTensor) else patch_w.shape
+ inner_dim = int(patch_shape[0])
+ in_channels = int(patch_shape[1])
+
+ # Wan uses head_dim=128 throughout the family; num_heads = inner_dim / 128.
+ attention_head_dim = 128
+ num_attention_heads = inner_dim // attention_head_dim
+
+ ffn_w = sd.get("blocks.0.ffn.net.0.proj.weight")
+ if ffn_w is None:
+ raise RuntimeError("GGUF state dict missing blocks.0.ffn.net.0.proj.weight after prefix strip")
+ ffn_shape = ffn_w.tensor_shape if isinstance(ffn_w, GGMLTensor) else ffn_w.shape
+ ffn_dim = int(ffn_shape[0])
+
+ text_w = sd.get("condition_embedder.text_embedder.linear_1.weight")
+ text_dim = 4096
+ if text_w is not None:
+ text_shape = text_w.tensor_shape if isinstance(text_w, GGMLTensor) else text_w.shape
+ text_dim = int(text_shape[1])
+
+ # out_channels is read from proj_out.weight directly rather than assumed
+ # equal to in_channels: I2V-A14B has in_channels=36 (16 noise + 16
+ # ref-image latents + 4 mask, concatenated by the denoise loop) but
+ # out_channels=16 (only the noise prediction comes back). proj_out is
+ # ``nn.Linear(inner_dim, out_channels * prod(patch_size))`` and
+ # patch_size is (1, 2, 2) → prod = 4 for the Wan 2.2 family.
+ proj_out_w = sd.get("proj_out.weight")
+ if proj_out_w is None:
+ raise RuntimeError("GGUF state dict missing proj_out.weight after prefix strip")
+ proj_out_shape = proj_out_w.tensor_shape if isinstance(proj_out_w, GGMLTensor) else proj_out_w.shape
+ out_channels = int(proj_out_shape[0]) // 4
+
+ # Layer count fallback (only triggers if the auto-count loop above
+ # found zero blocks, which shouldn't happen for a valid GGUF). T2V/I2V
+ # A14B have 40 layers; TI2V-5B has 30.
+ layer_count_fallback = 30 if config.variant == WanVariantType.TI2V_5B else 40
+
+ model_config: dict = {
+ "patch_size": (1, 2, 2),
+ "in_channels": in_channels,
+ "out_channels": out_channels,
+ "num_layers": num_layers if num_layers > 0 else layer_count_fallback,
+ "attention_head_dim": attention_head_dim,
+ "num_attention_heads": num_attention_heads,
+ "ffn_dim": ffn_dim,
+ "text_dim": text_dim,
+ }
+
+ with accelerate.init_empty_weights():
+ model = WanTransformer3DModel(**model_config)
+
+ model.load_state_dict(sd, strict=False, assign=True)
+ return model
+
+
+@ModelLoaderRegistry.register(base=BaseModelType.Any, type=ModelType.WanT5Encoder, format=ModelFormat.WanT5Encoder)
+class WanT5EncoderLoader(ModelLoader):
+ """Loader for the standalone Wan UMT5-XXL encoder.
+
+ Accepts two on-disk layouts:
+ 1. Parent dir with ``text_encoder/`` (and typically ``tokenizer/``) subdirs —
+ what ``Wan-AI/Wan2.2-T2V-A14B::text_encoder+tokenizer`` produces.
+ 2. A flat ``text_encoder/`` folder with ``config.json`` declaring
+ ``model_type: umt5`` directly at the root. In this case the tokenizer
+ is loaded from the same folder via ``AutoTokenizer.from_pretrained``.
+ """
+
+ def _load_model(
+ self,
+ config: AnyModelConfig,
+ submodel_type: Optional[SubModelType] = None,
+ ) -> AnyModel:
+ if submodel_type is None:
+ raise ValueError("A submodel type (Tokenizer or TextEncoder) must be provided.")
+
+ root = Path(config.path)
+ nested_text_encoder = root / "text_encoder"
+ nested_tokenizer = root / "tokenizer"
+
+ if submodel_type == SubModelType.TextEncoder:
+ from transformers import UMT5EncoderModel
+
+ target = nested_text_encoder if nested_text_encoder.exists() else root
+ return UMT5EncoderModel.from_pretrained(
+ str(target),
+ torch_dtype=torch.bfloat16,
+ local_files_only=True,
+ )
+ if submodel_type == SubModelType.Tokenizer:
+ from transformers import AutoTokenizer
+
+ # Prefer a sibling tokenizer/ directory; fall back to the encoder dir
+ # itself, which is normal for "flat" downloads.
+ target = (
+ nested_tokenizer
+ if nested_tokenizer.exists()
+ else (nested_text_encoder if nested_text_encoder.exists() else root)
+ )
+ return AutoTokenizer.from_pretrained(str(target), local_files_only=True)
+
+ raise ValueError(
+ f"Unsupported submodel type for WanT5Encoder: {submodel_type.value if submodel_type else 'None'}"
+ )
diff --git a/invokeai/backend/model_manager/model_on_disk.py b/invokeai/backend/model_manager/model_on_disk.py
index acc413b54c0..4ecaf40c2d4 100644
--- a/invokeai/backend/model_manager/model_on_disk.py
+++ b/invokeai/backend/model_manager/model_on_disk.py
@@ -3,13 +3,14 @@
import safetensors.torch
import torch
+from gguf import GGUFValueType
from picklescan.scanner import scan_file_path
from safetensors import safe_open
from invokeai.app.services.config.config_default import get_config
from invokeai.backend.model_hash.model_hash import HASHING_ALGORITHMS, ModelHash
from invokeai.backend.model_manager.taxonomy import ModelRepoVariant
-from invokeai.backend.quantization.gguf.loaders import gguf_sd_loader
+from invokeai.backend.quantization.gguf.loaders import WrappedGGUFReader, gguf_sd_loader
from invokeai.backend.util.logging import InvokeAILogger
from invokeai.backend.util.silence_warnings import SilenceWarnings
@@ -52,9 +53,19 @@ def metadata(self, path: Optional[Path] = None) -> dict[str, str]:
if path in self._metadata_cache:
return self._metadata_cache[path]
try:
- with safe_open(self.path, framework="pt", device="cpu") as f:
- metadata = f.metadata()
- assert isinstance(metadata, dict)
+ if path.suffix == ".gguf":
+ with WrappedGGUFReader(path) as reader:
+ metadata = {
+ name: value
+ for name, field in reader.fields.items()
+ if field.types
+ and field.types[0] == GGUFValueType.STRING
+ and isinstance(value := field.contents(), str)
+ }
+ else:
+ with safe_open(path, framework="pt", device="cpu") as f:
+ metadata = f.metadata()
+ assert isinstance(metadata, dict)
except Exception:
metadata = {}
diff --git a/invokeai/backend/model_manager/starter_models.py b/invokeai/backend/model_manager/starter_models.py
index 64f099554a7..9036d9de2db 100644
--- a/invokeai/backend/model_manager/starter_models.py
+++ b/invokeai/backend/model_manager/starter_models.py
@@ -15,6 +15,7 @@
ModelFormat,
ModelType,
QwenImageVariantType,
+ WanVariantType,
)
@@ -1299,6 +1300,229 @@ def _gemini_3_resolution_presets(
default_settings=ExternalApiModelDefaultSettings(width=1328, height=1328, num_images=1),
panel_schema=ExternalModelPanelSchema(image=[{"name": "dimensions"}]),
)
+# region Wan 2.2 (local)
+# Shared components — all Wan 2.2 variants use the UMT5-XXL text encoder. A14B
+# (both T2V and I2V) uses a 16-channel VAE; TI2V-5B uses a 48-channel VAE. The
+# two VAEs are not interchangeable.
+wan_22_t5_encoder = StarterModel(
+ name="Wan T5 Encoder (UMT5-XXL)",
+ base=BaseModelType.Any,
+ source="Wan-AI/Wan2.2-T2V-A14B-Diffusers::text_encoder+tokenizer",
+ description="UMT5-XXL text encoder used by all Wan 2.2 variants (T2V/I2V A14B and TI2V-5B). "
+ "Required when running a GGUF Wan main without a Diffusers Component Source. (~11GB)",
+ type=ModelType.WanT5Encoder,
+ format=ModelFormat.WanT5Encoder,
+)
+
+wan_22_a14b_vae = StarterModel(
+ name="Wan 2.2 A14B VAE",
+ base=BaseModelType.Wan,
+ source="Wan-AI/Wan2.2-T2V-A14B-Diffusers::vae/diffusion_pytorch_model.safetensors",
+ description="Wan 2.2 A14B VAE (16-channel). Shared between T2V and I2V A14B variants. "
+ "Not interchangeable with the TI2V-5B VAE. (~250MB)",
+ type=ModelType.VAE,
+ format=ModelFormat.Checkpoint,
+)
+
+wan_22_5b_vae = StarterModel(
+ name="Wan 2.2 TI2V-5B VAE",
+ base=BaseModelType.Wan,
+ source="Wan-AI/Wan2.2-TI2V-5B-Diffusers::vae/diffusion_pytorch_model.safetensors",
+ description="Wan 2.2 TI2V-5B VAE (48-channel). Required for the TI2V-5B model family. "
+ "Not interchangeable with the A14B VAE. (~400MB)",
+ type=ModelType.VAE,
+ format=ModelFormat.Checkpoint,
+)
+
+# T2V A14B — full Diffusers + GGUF expert pairs (Q4_K_M and Q8_0).
+# The high-noise GGUF is the "main" entry the user picks; the low-noise GGUF
+# is wired as the partner expert via the Advanced panel. Each high-noise entry
+# lists its low-noise partner plus the shared VAE/encoder as dependencies so
+# the bundle/dependency installer pulls everything together.
+wan_22_t2v_a14b_diffusers = StarterModel(
+ name="Wan 2.2 T2V A14B (Diffusers)",
+ base=BaseModelType.Wan,
+ source="Wan-AI/Wan2.2-T2V-A14B-Diffusers",
+ description="Full Diffusers Wan 2.2 T2V A14B model — both expert transformers, VAE, and UMT5-XXL "
+ "encoder in a single folder. No additional components needed. (~80GB)",
+ type=ModelType.Main,
+ format=ModelFormat.Diffusers,
+ variant=WanVariantType.T2V_A14B,
+)
+
+wan_22_t2v_a14b_low_gguf_q4_k_m = StarterModel(
+ name="Wan 2.2 T2V A14B Low Noise (Q4_K_M)",
+ base=BaseModelType.Wan,
+ source="https://huggingface.co/QuantStack/Wan2.2-T2V-A14B-GGUF/resolve/main/LowNoise/Wan2.2-T2V-A14B-LowNoise-Q4_K_M.gguf",
+ description="Wan 2.2 T2V A14B low-noise expert transformer (Q4_K_M). Paired with the high-noise "
+ "expert; selected via the Advanced 'Transformer (Low Noise)' field. (~9.7GB)",
+ type=ModelType.Main,
+ format=ModelFormat.GGUFQuantized,
+ variant=WanVariantType.T2V_A14B,
+)
+
+wan_22_t2v_a14b_gguf_q4_k_m = StarterModel(
+ name="Wan 2.2 T2V A14B High Noise (Q4_K_M)",
+ base=BaseModelType.Wan,
+ source="https://huggingface.co/QuantStack/Wan2.2-T2V-A14B-GGUF/resolve/main/HighNoise/Wan2.2-T2V-A14B-HighNoise-Q4_K_M.gguf",
+ description="Wan 2.2 T2V A14B high-noise expert transformer (Q4_K_M). Pick this as the main model; "
+ "the low-noise partner is wired in Advanced. Good quality/size balance. (~9.7GB)",
+ type=ModelType.Main,
+ format=ModelFormat.GGUFQuantized,
+ variant=WanVariantType.T2V_A14B,
+ dependencies=[wan_22_a14b_vae, wan_22_t5_encoder, wan_22_t2v_a14b_low_gguf_q4_k_m],
+)
+
+wan_22_t2v_a14b_low_gguf_q8_0 = StarterModel(
+ name="Wan 2.2 T2V A14B Low Noise (Q8_0)",
+ base=BaseModelType.Wan,
+ source="https://huggingface.co/QuantStack/Wan2.2-T2V-A14B-GGUF/resolve/main/LowNoise/Wan2.2-T2V-A14B-LowNoise-Q8_0.gguf",
+ description="Wan 2.2 T2V A14B low-noise expert transformer (Q8_0). Highest quality quantization. (~15.4GB)",
+ type=ModelType.Main,
+ format=ModelFormat.GGUFQuantized,
+ variant=WanVariantType.T2V_A14B,
+)
+
+wan_22_t2v_a14b_gguf_q8_0 = StarterModel(
+ name="Wan 2.2 T2V A14B High Noise (Q8_0)",
+ base=BaseModelType.Wan,
+ source="https://huggingface.co/QuantStack/Wan2.2-T2V-A14B-GGUF/resolve/main/HighNoise/Wan2.2-T2V-A14B-HighNoise-Q8_0.gguf",
+ description="Wan 2.2 T2V A14B high-noise expert transformer (Q8_0). Pick as the main; pair with the "
+ "low-noise Q8_0 partner in Advanced. Highest quality quantization. (~15.4GB)",
+ type=ModelType.Main,
+ format=ModelFormat.GGUFQuantized,
+ variant=WanVariantType.T2V_A14B,
+ dependencies=[wan_22_a14b_vae, wan_22_t5_encoder, wan_22_t2v_a14b_low_gguf_q8_0],
+)
+
+# T2V Lightning LoRAs — V1.1 Seko rank-64 pair (4-step inference).
+wan_22_t2v_lightning_high = StarterModel(
+ name="Wan 2.2 T2V Lightning High Noise (4-step, V1.1)",
+ base=BaseModelType.Wan,
+ source="https://huggingface.co/lightx2v/Wan2.2-Lightning/resolve/main/Wan2.2-T2V-A14B-4steps-lora-rank64-Seko-V1.1/high_noise_model.safetensors",
+ description="Lightning distillation LoRA for the Wan 2.2 T2V A14B high-noise expert — enables "
+ "4-step generation. Use together with the low-noise variant. Settings: Steps=4, CFG=1.",
+ type=ModelType.LoRA,
+)
+
+wan_22_t2v_lightning_low = StarterModel(
+ name="Wan 2.2 T2V Lightning Low Noise (4-step, V1.1)",
+ base=BaseModelType.Wan,
+ source="https://huggingface.co/lightx2v/Wan2.2-Lightning/resolve/main/Wan2.2-T2V-A14B-4steps-lora-rank64-Seko-V1.1/low_noise_model.safetensors",
+ description="Lightning distillation LoRA for the Wan 2.2 T2V A14B low-noise expert — enables "
+ "4-step generation. Use together with the high-noise variant. Settings: Steps=4, CFG=1.",
+ type=ModelType.LoRA,
+)
+
+# I2V A14B — full Diffusers + GGUF expert pairs (Q4_K_M and Q8_0).
+wan_22_i2v_a14b_diffusers = StarterModel(
+ name="Wan 2.2 I2V A14B (Diffusers)",
+ base=BaseModelType.Wan,
+ source="Wan-AI/Wan2.2-I2V-A14B-Diffusers",
+ description="Full Diffusers Wan 2.2 I2V A14B model — both expert transformers, VAE, and UMT5-XXL "
+ "encoder. Use the Reference Images panel to provide the conditioning image. (~80GB)",
+ type=ModelType.Main,
+ format=ModelFormat.Diffusers,
+ variant=WanVariantType.I2V_A14B,
+)
+
+wan_22_i2v_a14b_low_gguf_q4_k_m = StarterModel(
+ name="Wan 2.2 I2V A14B Low Noise (Q4_K_M)",
+ base=BaseModelType.Wan,
+ source="https://huggingface.co/QuantStack/Wan2.2-I2V-A14B-GGUF/resolve/main/LowNoise/Wan2.2-I2V-A14B-LowNoise-Q4_K_M.gguf",
+ description="Wan 2.2 I2V A14B low-noise expert transformer (Q4_K_M). (~9.7GB)",
+ type=ModelType.Main,
+ format=ModelFormat.GGUFQuantized,
+ variant=WanVariantType.I2V_A14B,
+)
+
+wan_22_i2v_a14b_gguf_q4_k_m = StarterModel(
+ name="Wan 2.2 I2V A14B High Noise (Q4_K_M)",
+ base=BaseModelType.Wan,
+ source="https://huggingface.co/QuantStack/Wan2.2-I2V-A14B-GGUF/resolve/main/HighNoise/Wan2.2-I2V-A14B-HighNoise-Q4_K_M.gguf",
+ description="Wan 2.2 I2V A14B high-noise expert transformer (Q4_K_M). Pick as the main; pair with "
+ "the low-noise partner in Advanced. Use the Reference Images panel for the conditioning image. (~9.7GB)",
+ type=ModelType.Main,
+ format=ModelFormat.GGUFQuantized,
+ variant=WanVariantType.I2V_A14B,
+ dependencies=[wan_22_a14b_vae, wan_22_t5_encoder, wan_22_i2v_a14b_low_gguf_q4_k_m],
+)
+
+wan_22_i2v_a14b_low_gguf_q8_0 = StarterModel(
+ name="Wan 2.2 I2V A14B Low Noise (Q8_0)",
+ base=BaseModelType.Wan,
+ source="https://huggingface.co/QuantStack/Wan2.2-I2V-A14B-GGUF/resolve/main/LowNoise/Wan2.2-I2V-A14B-LowNoise-Q8_0.gguf",
+ description="Wan 2.2 I2V A14B low-noise expert transformer (Q8_0). Highest quality quantization. (~15.4GB)",
+ type=ModelType.Main,
+ format=ModelFormat.GGUFQuantized,
+ variant=WanVariantType.I2V_A14B,
+)
+
+wan_22_i2v_a14b_gguf_q8_0 = StarterModel(
+ name="Wan 2.2 I2V A14B High Noise (Q8_0)",
+ base=BaseModelType.Wan,
+ source="https://huggingface.co/QuantStack/Wan2.2-I2V-A14B-GGUF/resolve/main/HighNoise/Wan2.2-I2V-A14B-HighNoise-Q8_0.gguf",
+ description="Wan 2.2 I2V A14B high-noise expert transformer (Q8_0). Highest quality quantization. (~15.4GB)",
+ type=ModelType.Main,
+ format=ModelFormat.GGUFQuantized,
+ variant=WanVariantType.I2V_A14B,
+ dependencies=[wan_22_a14b_vae, wan_22_t5_encoder, wan_22_i2v_a14b_low_gguf_q8_0],
+)
+
+# I2V Lightning LoRAs — Seko rank-64 pair (4-step inference). Currently only V1.
+wan_22_i2v_lightning_high = StarterModel(
+ name="Wan 2.2 I2V Lightning High Noise (4-step, V1)",
+ base=BaseModelType.Wan,
+ source="https://huggingface.co/lightx2v/Wan2.2-Lightning/resolve/main/Wan2.2-I2V-A14B-4steps-lora-rank64-Seko-V1/high_noise_model.safetensors",
+ description="Lightning distillation LoRA for the Wan 2.2 I2V A14B high-noise expert — enables "
+ "4-step image-to-image generation. Use together with the low-noise variant. Settings: Steps=4, CFG=1.",
+ type=ModelType.LoRA,
+)
+
+wan_22_i2v_lightning_low = StarterModel(
+ name="Wan 2.2 I2V Lightning Low Noise (4-step, V1)",
+ base=BaseModelType.Wan,
+ source="https://huggingface.co/lightx2v/Wan2.2-Lightning/resolve/main/Wan2.2-I2V-A14B-4steps-lora-rank64-Seko-V1/low_noise_model.safetensors",
+ description="Lightning distillation LoRA for the Wan 2.2 I2V A14B low-noise expert — enables "
+ "4-step image-to-image generation. Use together with the high-noise variant. Settings: Steps=4, CFG=1.",
+ type=ModelType.LoRA,
+)
+
+# TI2V-5B — single-transformer model (no expert pair). Uses its own 48-channel VAE.
+wan_22_ti2v_5b_diffusers = StarterModel(
+ name="Wan 2.2 TI2V-5B (Diffusers)",
+ base=BaseModelType.Wan,
+ source="Wan-AI/Wan2.2-TI2V-5B-Diffusers",
+ description="Full Diffusers Wan 2.2 TI2V-5B model — single 5B transformer, 48-channel VAE, and "
+ "UMT5-XXL encoder. Smaller and faster than A14B; runs on consumer GPUs. (~20GB)",
+ type=ModelType.Main,
+ format=ModelFormat.Diffusers,
+ variant=WanVariantType.TI2V_5B,
+)
+
+wan_22_ti2v_5b_gguf_q4_k_m = StarterModel(
+ name="Wan 2.2 TI2V-5B (Q4_K_M)",
+ base=BaseModelType.Wan,
+ source="https://huggingface.co/QuantStack/Wan2.2-TI2V-5B-GGUF/resolve/main/Wan2.2-TI2V-5B-Q4_K_M.gguf",
+ description="Wan 2.2 TI2V-5B transformer (Q4_K_M). Single-expert model — no low-noise partner needed. (~3.4GB)",
+ type=ModelType.Main,
+ format=ModelFormat.GGUFQuantized,
+ variant=WanVariantType.TI2V_5B,
+ dependencies=[wan_22_5b_vae, wan_22_t5_encoder],
+)
+
+wan_22_ti2v_5b_gguf_q8_0 = StarterModel(
+ name="Wan 2.2 TI2V-5B (Q8_0)",
+ base=BaseModelType.Wan,
+ source="https://huggingface.co/QuantStack/Wan2.2-TI2V-5B-GGUF/resolve/main/Wan2.2-TI2V-5B-Q8_0.gguf",
+ description="Wan 2.2 TI2V-5B transformer (Q8_0). Highest quality quantization. (~5.4GB)",
+ type=ModelType.Main,
+ format=ModelFormat.GGUFQuantized,
+ variant=WanVariantType.TI2V_5B,
+ dependencies=[wan_22_5b_vae, wan_22_t5_encoder],
+)
+# endregion
+
alibabacloud_wan26_t2i = StarterModel(
name="Wan 2.6 Text-to-Image",
base=BaseModelType.External,
@@ -1744,6 +1968,26 @@ def _gemini_3_resolution_presets(
z_image_qwen3_encoder_quantized,
z_image_controlnet_union,
z_image_controlnet_tile,
+ wan_22_t5_encoder,
+ wan_22_a14b_vae,
+ wan_22_5b_vae,
+ wan_22_t2v_a14b_diffusers,
+ wan_22_t2v_a14b_low_gguf_q4_k_m,
+ wan_22_t2v_a14b_gguf_q4_k_m,
+ wan_22_t2v_a14b_low_gguf_q8_0,
+ wan_22_t2v_a14b_gguf_q8_0,
+ wan_22_t2v_lightning_high,
+ wan_22_t2v_lightning_low,
+ wan_22_i2v_a14b_diffusers,
+ wan_22_i2v_a14b_low_gguf_q4_k_m,
+ wan_22_i2v_a14b_gguf_q4_k_m,
+ wan_22_i2v_a14b_low_gguf_q8_0,
+ wan_22_i2v_a14b_gguf_q8_0,
+ wan_22_i2v_lightning_high,
+ wan_22_i2v_lightning_low,
+ wan_22_ti2v_5b_diffusers,
+ wan_22_ti2v_5b_gguf_q4_k_m,
+ wan_22_ti2v_5b_gguf_q8_0,
gemini_flash_image,
gemini_pro_image_preview,
gemini_3_1_flash_image_preview,
@@ -1861,6 +2105,31 @@ def _gemini_3_resolution_presets(
anima_lllite_sketch,
]
+# Wan 2.2 starter bundles. Split into T2V and I2V so users only pay for the
+# capability they need: a 12 GB card can install just the T2V bundle and have
+# both text-to-video (T2V-A14B) and a low-VRAM image-to-video option (via
+# TI2V-5B, which handles both modes in one ~3.4 GB model). The I2V bundle adds
+# the heavier I2V-A14B path for users with more headroom. Q8 variants and full
+# Diffusers builds stay available as a-la-carte starters.
+wan_t2v_bundle: list[StarterModel] = [
+ wan_22_t5_encoder,
+ wan_22_a14b_vae,
+ wan_22_5b_vae,
+ wan_22_ti2v_5b_gguf_q4_k_m,
+ wan_22_t2v_a14b_gguf_q4_k_m,
+ wan_22_t2v_a14b_low_gguf_q4_k_m,
+ wan_22_t2v_lightning_high,
+ wan_22_t2v_lightning_low,
+]
+wan_i2v_bundle: list[StarterModel] = [
+ wan_22_t5_encoder,
+ wan_22_a14b_vae,
+ wan_22_i2v_a14b_gguf_q4_k_m,
+ wan_22_i2v_a14b_low_gguf_q4_k_m,
+ wan_22_i2v_lightning_high,
+ wan_22_i2v_lightning_low,
+]
+
STARTER_BUNDLES: dict[str, StarterModelBundle] = {
BaseModelType.StableDiffusion1: StarterModelBundle(name="Stable Diffusion 1.5", models=sd1_bundle),
BaseModelType.StableDiffusionXL: StarterModelBundle(name="SDXL", models=sdxl_bundle),
@@ -1869,6 +2138,8 @@ def _gemini_3_resolution_presets(
BaseModelType.ZImage: StarterModelBundle(name="Z-Image Turbo", models=zimage_bundle),
BaseModelType.QwenImage: StarterModelBundle(name="Qwen Image", models=qwen_image_bundle),
BaseModelType.Anima: StarterModelBundle(name="Anima", models=anima_bundle),
+ "wan_t2v": StarterModelBundle(name="Wan 2.2 Text-to-Video", models=wan_t2v_bundle),
+ "wan_i2v": StarterModelBundle(name="Wan 2.2 Image-to-Video", models=wan_i2v_bundle),
}
assert len(STARTER_MODELS) == len({m.source for m in STARTER_MODELS}), "Duplicate starter models"
diff --git a/invokeai/backend/model_manager/taxonomy.py b/invokeai/backend/model_manager/taxonomy.py
index a2e4e58bdc4..7f7b9f21c1e 100644
--- a/invokeai/backend/model_manager/taxonomy.py
+++ b/invokeai/backend/model_manager/taxonomy.py
@@ -58,6 +58,8 @@ class BaseModelType(str, Enum):
"""Indicates the model is associated with Qwen Image Edit 2511 model architecture."""
Anima = "anima"
"""Indicates the model is associated with Anima model architecture (Cosmos Predict2 DiT + LLM Adapter)."""
+ Wan = "wan"
+ """Indicates the model is associated with the Wan 2.2 model architecture (T2V-A14B / TI2V-5B), used for image generation at num_frames=1."""
Unknown = "unknown"
"""Indicates the model's base architecture is unknown."""
@@ -79,6 +81,7 @@ class ModelType(str, Enum):
T5Encoder = "t5_encoder"
Qwen3Encoder = "qwen3_encoder"
QwenVLEncoder = "qwen_vl_encoder"
+ WanT5Encoder = "wan_t5_encoder"
SpandrelImageToImage = "spandrel_image_to_image"
SigLIP = "siglip"
FluxRedux = "flux_redux"
@@ -93,6 +96,7 @@ class SubModelType(str, Enum):
UNet = "unet"
Transformer = "transformer"
+ Transformer2 = "transformer_2"
TextEncoder = "text_encoder"
TextEncoder2 = "text_encoder_2"
TextEncoder3 = "text_encoder_3"
@@ -165,6 +169,44 @@ class QwenImageVariantType(str, Enum):
"""Qwen Image Edit - image editing model with reference image support."""
+class WanVariantType(str, Enum):
+ """Wan 2.2 model variants.
+
+ All variants are used for image generation at num_frames=1. The A14B family
+ is a Mixture-of-Experts (high-noise + low-noise) totalling ~28B params; the
+ T2V sub-variant takes text only, while the I2V sub-variant additionally
+ conditions on a reference image (encoded by the VAE and concatenated to the
+ noise latents along the channel dim — its transformer has ``in_channels=36``
+ instead of ``16``). TI2V-5B is a single ~5B transformer with a
+ higher-compression VAE (z_dim=48).
+ """
+
+ T2V_A14B = "t2v_a14b"
+ """Wan 2.2 T2V-A14B - dual-expert MoE (text only, 16-channel Wan VAE, transformer in_channels=16)."""
+
+ I2V_A14B = "i2v_a14b"
+ """Wan 2.2 I2V-A14B - dual-expert MoE with VAE-latent reference-image conditioning (transformer in_channels=36)."""
+
+ TI2V_5B = "ti2v_5b"
+ """Wan 2.2 TI2V-5B - smaller single-transformer model with Wan2.2-VAE (48 latent channels)."""
+
+
+class WanLoRAVariantType(str, Enum):
+ """Wan 2.2 LoRA variants, identifying which model family a LoRA targets.
+
+ Detected from the LoRA's inner attention dim: A14B has ``inner_dim=5120``,
+ TI2V-5B has ``inner_dim=3072``. A14B and 5B LoRAs are NOT interchangeable —
+ applying one against the wrong main model crashes in the layer patcher
+ with a tensor-shape error.
+ """
+
+ A14B = "a14b"
+ """Targets a Wan 2.2 A14B main (T2V or I2V, inner_dim=5120)."""
+
+ Wan5B = "5b"
+ """Targets the Wan 2.2 TI2V-5B main (inner_dim=3072)."""
+
+
class Qwen3VariantType(str, Enum):
"""Qwen3 text encoder variants based on model size."""
@@ -193,6 +235,7 @@ class ModelFormat(str, Enum):
T5Encoder = "t5_encoder"
Qwen3Encoder = "qwen3_encoder"
QwenVLEncoder = "qwen_vl_encoder"
+ WanT5Encoder = "wan_t5_encoder"
BnbQuantizedLlmInt8b = "bnb_quantized_int8b"
BnbQuantizednf4b = "bnb_quantized_nf4b"
GGUFQuantized = "gguf_quantized"
@@ -248,6 +291,8 @@ class FluxLoRAFormat(str, Enum):
Flux2VariantType,
ZImageVariantType,
QwenImageVariantType,
+ WanVariantType,
+ WanLoRAVariantType,
Qwen3VariantType,
]
variant_type_adapter = TypeAdapter[
@@ -257,6 +302,8 @@ class FluxLoRAFormat(str, Enum):
| Flux2VariantType
| ZImageVariantType
| QwenImageVariantType
+ | WanVariantType
+ | WanLoRAVariantType
| Qwen3VariantType
](
ModelVariantType
@@ -265,5 +312,7 @@ class FluxLoRAFormat(str, Enum):
| Flux2VariantType
| ZImageVariantType
| QwenImageVariantType
+ | WanVariantType
+ | WanLoRAVariantType
| Qwen3VariantType
)
diff --git a/invokeai/backend/patches/lora_conversions/anima_lora_constants.py b/invokeai/backend/patches/lora_conversions/anima_lora_constants.py
index 380e31998a7..5a54de82e86 100644
--- a/invokeai/backend/patches/lora_conversions/anima_lora_constants.py
+++ b/invokeai/backend/patches/lora_conversions/anima_lora_constants.py
@@ -17,7 +17,10 @@
# in ``anima_lora_conversion_utils``) to avoid circular imports.
# ---------------------------------------------------------------------------
-# Cosmos DiT subcomponent names unique to the Anima / Cosmos Predict2 architecture.
+# Cosmos DiT subcomponent names that ALSO appear in Wan (cross_attn, self_attn)
+# plus those unique to Cosmos. Used by ``anima_lora_conversion_utils`` to find
+# block layers during state-dict conversion, where the architecture is already
+# known to be Anima.
_COSMOS_DIT_SUBCOMPONENTS_RE = r"(cross_attn|self_attn|mlp|adaln_modulation)"
# Kohya format: lora_unet_[llm_adapter_]blocks_N_
@@ -29,17 +32,53 @@
)
+# Subcomponents *uniquely* identifying Anima/Cosmos DiT: ``mlp`` and
+# ``adaln_modulation`` (Wan calls those ``ffn`` and ``modulation`` respectively),
+# plus the Cosmos attention naming with a ``_proj`` suffix on the projection
+# letter (Wan native uses bare ``.q``/``.k``/``.v``/``.o`` — no ``_proj``).
+#
+# Used by the probe in ``configs/lora.py`` to make Anima-LoRA detection
+# *mutually exclusive* with Wan-LoRA detection: a state dict carrying only
+# ``cross_attn.q`` / ``ffn.0`` (Wan native) will NOT match here, regardless of
+# the order configs are tried.
+_COSMOS_DIT_EXCLUSIVE_SUBCOMPONENTS_RE = (
+ r"(mlp|adaln_modulation|"
+ r"(?:cross|self)_attn[._](?:[qkv]_proj|output_proj))"
+)
+
+_KOHYA_ANIMA_STRICT_RE = re.compile(r"lora_unet_(llm_adapter_)?blocks_\d+_" + _COSMOS_DIT_EXCLUSIVE_SUBCOMPONENTS_RE)
+_PEFT_ANIMA_STRICT_RE = re.compile(
+ r"(diffusion_model|transformer|base_model\.model\.transformer)\.blocks\.\d+\."
+ + _COSMOS_DIT_EXCLUSIVE_SUBCOMPONENTS_RE
+)
+
+
def has_cosmos_dit_kohya_keys(str_keys: list[str]) -> bool:
- """Check for Kohya-style keys targeting Cosmos DiT blocks with specific subcomponents.
+ """Loose detector — matches any Cosmos-shaped block submodule including
+ those whose names collide with Wan (``cross_attn``, ``self_attn``).
- Requires both the ``lora_unet_[llm_adapter_]blocks_N_`` prefix **and** a
- Cosmos DiT subcomponent name (cross_attn, self_attn, mlp, adaln_modulation)
- to avoid false-positives on other architectures that might also use bare
- ``blocks`` in their key paths.
+ For probe disambiguation between Anima and Wan, prefer
+ ``has_cosmos_dit_kohya_keys_strict``. This loose form is still useful
+ inside the Anima conversion utility, where the architecture is already
+ confirmed to be Anima and we just need to enumerate matching layers.
"""
return any(_KOHYA_ANIMA_RE.search(k) is not None for k in str_keys)
def has_cosmos_dit_peft_keys(str_keys: list[str]) -> bool:
- """Check for diffusers PEFT keys targeting Cosmos DiT blocks with specific subcomponents."""
+ """Loose PEFT-format detector — see ``has_cosmos_dit_kohya_keys`` docstring."""
return any(_PEFT_ANIMA_RE.search(k) is not None for k in str_keys)
+
+
+def has_cosmos_dit_kohya_keys_strict(str_keys: list[str]) -> bool:
+ """Strict Kohya detector requiring an Anima-exclusive submodule (``mlp``,
+ ``adaln_modulation``, or Cosmos's ``_proj``-suffixed attention names).
+
+ Mutually exclusive with the Wan LoRA probe — no Wan LoRA can satisfy this.
+ """
+ return any(_KOHYA_ANIMA_STRICT_RE.search(k) is not None for k in str_keys)
+
+
+def has_cosmos_dit_peft_keys_strict(str_keys: list[str]) -> bool:
+ """Strict PEFT detector. See ``has_cosmos_dit_kohya_keys_strict`` docstring."""
+ return any(_PEFT_ANIMA_STRICT_RE.search(k) is not None for k in str_keys)
diff --git a/invokeai/backend/patches/lora_conversions/wan_lora_constants.py b/invokeai/backend/patches/lora_conversions/wan_lora_constants.py
new file mode 100644
index 00000000000..c7a6859d6f0
--- /dev/null
+++ b/invokeai/backend/patches/lora_conversions/wan_lora_constants.py
@@ -0,0 +1,174 @@
+# Wan 2.2 LoRA prefix constants and key-shape detection helpers.
+#
+# Wan LoRAs come in three shapes in the wild:
+#
+# 1. **Diffusers PEFT** (HF naming), with or without a "transformer." prefix:
+# blocks.0.attn1.to_q.lora_A.weight
+# transformer.blocks.0.attn1.to_q.lora_A.weight
+#
+# 2. **Native upstream PEFT** (ComfyUI / Wan-AI checkpoint naming) with
+# "diffusion_model." or "transformer." prefix:
+# diffusion_model.blocks.0.self_attn.q.lora_A.weight
+# transformer.blocks.0.cross_attn.k.lora_A.weight
+#
+# 3. **Kohya**, with the standard ``lora_unet_blocks__`` shape,
+# in either diffusers naming (``attn1_to_q``) or native naming (``self_attn_q``):
+# lora_unet_blocks_0_attn1_to_q.lora_down.weight
+# lora_unet_blocks_0_self_attn_q.lora_down.weight
+#
+# The detection helpers below are shared with ``configs/lora.py`` so the probe
+# and the conversion code agree on what counts as a Wan LoRA. They keep this
+# file circular-import-free.
+
+import re
+
+from invokeai.backend.model_manager.taxonomy import WanLoRAVariantType
+
+# Prefix for Wan transformer LoRA layers in the ModelPatchRaw layer dict.
+# Same convention as Anima / QwenImage — the LayerPatcher uses this prefix to
+# resolve patches against the loaded transformer's parameter paths.
+WAN_LORA_TRANSFORMER_PREFIX = "lora_transformer-"
+
+
+# Diffusers Wan-specific submodules: attn1/attn2 (self/cross attention with
+# to_q/to_k/to_v/to_out.0 children) and ffn.net (gated FFN). These are unique
+# to WanTransformer3DModel — none of FLUX (double_blocks/single_blocks),
+# QwenImage (transformer_blocks.X.attn), Z-Image (diffusion_model.layers),
+# or Anima/Cosmos (mlp + adaln_modulation) produce this combination.
+_WAN_DIFFUSERS_SUBMODULES = r"(attn1\.|attn2\.|ffn\.net\.)"
+
+# Native upstream Wan submodules. self_attn / cross_attn collide with Anima's
+# Cosmos DiT naming, so we look for the bare ``.q``/``.k``/``.v``/``.o``
+# projection suffix (no ``_proj`` tail) AND/OR the ``ffn.`` MLP layout —
+# Anima uses ``mlp`` instead, so this is mutually exclusive.
+_WAN_NATIVE_SUBMODULES = r"(self_attn\.[qkvo](\.|$)|cross_attn\.[qkvo](\.|$)|ffn\.\d+\.)"
+
+# Anti-patterns: keys that would indicate Anima/Cosmos (mlp / adaln_modulation /
+# the ``q_proj`` projection naming Cosmos uses on its attention blocks),
+# QwenImage (transformer_blocks), Flux (double_blocks / single_blocks), or
+# Z-Image (diffusion_model.layers). If any of these are present, the LoRA is
+# NOT Wan.
+_ANIMA_ANTI_RE = re.compile(r"blocks[\._]\d+[\._](mlp|adaln_modulation)")
+# Anima Cosmos attention uses ``q_proj`` / ``k_proj`` / ``v_proj`` / ``output_proj``
+# under self_attn/cross_attn. Wan native uses just ``q``/``k``/``v``/``o`` — so
+# the ``_proj`` suffix on a self_attn/cross_attn child is a definitive Anima tell,
+# in both Kohya (``self_attn_q_proj``) and PEFT (``self_attn.q_proj``) forms.
+_ANIMA_ATTN_ANTI_RE = re.compile(r"(self_attn|cross_attn)[\._]([qkv]_proj|output_proj)")
+_QWEN_ANTI_RE = re.compile(r"(^|\.)transformer_blocks\.\d+\.")
+_FLUX_ANTI_RE = re.compile(r"(^|\.|_)(double_blocks|single_blocks|single_transformer_blocks)[\._]\d+")
+_Z_IMAGE_ANTI_RE = re.compile(r"diffusion_model\.layers\.\d+\.")
+
+
+# Kohya format: lora_unet_blocks__(attn1_to_X | ffn_N | (self|cross)_attn_X
+# where X is a single q/k/v/o letter). The strict alphabet on the attention
+# child keeps us from matching Anima's ``cross_attn_q_proj`` (which has an
+# additional ``_proj`` segment).
+_KOHYA_WAN_RE = re.compile(
+ r"lora_unet_blocks_\d+_"
+ r"(attn[12]_(to_[qkv]|to_out_0|norm_[qk])"
+ r"|(self_attn|cross_attn)_[qkvo](_|\.|$)"
+ r"|ffn_(\d+|net_\d+_proj|net_\d+))"
+)
+
+# PEFT format: .blocks..
+# Prefix may be empty, "transformer.", "diffusion_model.", or "base_model.model.transformer."
+_PEFT_WAN_DIFFUSERS_RE = re.compile(
+ r"(?:^|(?:diffusion_model|transformer|base_model\.model\.transformer)\.)blocks\.\d+\." + _WAN_DIFFUSERS_SUBMODULES
+)
+_PEFT_WAN_NATIVE_RE = re.compile(
+ r"(?:^|(?:diffusion_model|transformer|base_model\.model\.transformer)\.)blocks\.\d+\." + _WAN_NATIVE_SUBMODULES
+)
+
+
+def has_wan_kohya_keys(str_keys: list[str]) -> bool:
+ """Kohya-style keys naming Wan submodules (attn1/attn2/self_attn/cross_attn/ffn)."""
+ return any(_KOHYA_WAN_RE.search(k) is not None for k in str_keys)
+
+
+def has_wan_peft_keys(str_keys: list[str]) -> bool:
+ """Diffusers PEFT keys naming Wan submodules in either diffusers or native layout."""
+ for k in str_keys:
+ if _PEFT_WAN_DIFFUSERS_RE.search(k) is not None:
+ return True
+ if _PEFT_WAN_NATIVE_RE.search(k) is not None:
+ return True
+ return False
+
+
+def detect_wan_lora_variant(state_dict: dict) -> WanLoRAVariantType | None:
+ """Inspect a Wan LoRA state dict and guess which model family it targets.
+
+ A14B has inner_dim=5120; TI2V-5B has inner_dim=3072. Every transformer
+ block's ``attn1.to_q`` (or native ``self_attn.q``) LoRA pair has weights
+ shaped against the inner dim — ``lora_up.weight`` is ``[inner_dim, rank]``
+ and ``lora_down.weight`` is ``[rank, inner_dim]``. The larger dim of
+ either is the inner dim.
+
+ Returns:
+ ``WanLoRAVariantType.A14B`` if inner_dim == 5120,
+ ``WanLoRAVariantType.Wan5B`` if inner_dim == 3072,
+ ``None`` if no recognisable attn weight is found or inner_dim is
+ ambiguous (e.g. LoRA that only patches FFN at non-standard rank).
+ """
+ # Probe several common key shapes — diffusers PEFT (lora_A/lora_B),
+ # native Kohya naming (lora_up/lora_down), with or without a
+ # diffusion_model/transformer prefix, in diffusers or native attn
+ # naming. The first matching tensor is enough.
+ candidate_suffixes = (
+ # diffusers PEFT
+ ".attn1.to_q.lora_A.weight",
+ ".attn1.to_q.lora_B.weight",
+ ".self_attn.q.lora_A.weight",
+ ".self_attn.q.lora_B.weight",
+ # native (Kohya) PEFT
+ ".attn1.to_q.lora_up.weight",
+ ".attn1.to_q.lora_down.weight",
+ ".self_attn.q.lora_up.weight",
+ ".self_attn.q.lora_down.weight",
+ )
+ kohya_substrings = (
+ "_attn1_to_q.lora_up.weight",
+ "_attn1_to_q.lora_down.weight",
+ "_self_attn_q.lora_up.weight",
+ "_self_attn_q.lora_down.weight",
+ )
+
+ for key, tensor in state_dict.items():
+ if not isinstance(key, str):
+ continue
+ match_suffix = any(key.endswith(suffix) for suffix in candidate_suffixes)
+ match_kohya = any(needle in key for needle in kohya_substrings)
+ if not (match_suffix or match_kohya):
+ continue
+ shape = getattr(tensor, "shape", None)
+ if shape is None or len(shape) < 2:
+ continue
+ inner_dim = max(int(shape[0]), int(shape[1]))
+ if inner_dim == 5120:
+ return WanLoRAVariantType.A14B
+ if inner_dim == 3072:
+ return WanLoRAVariantType.Wan5B
+ # Any other inner_dim is uncharted — bail rather than guess.
+ return None
+
+ return None
+
+
+def has_non_wan_architecture_keys(str_keys: list[str]) -> bool:
+ """True if any key indicates a non-Wan architecture (Anima, Qwen, Flux, Z-Image).
+
+ Used as an exclusion guard — a Wan LoRA should never carry these patterns,
+ so finding them is grounds to reject the Wan probe.
+ """
+ for k in str_keys:
+ if _ANIMA_ANTI_RE.search(k) is not None:
+ return True
+ if _ANIMA_ATTN_ANTI_RE.search(k) is not None:
+ return True
+ if _QWEN_ANTI_RE.search(k) is not None:
+ return True
+ if _FLUX_ANTI_RE.search(k) is not None:
+ return True
+ if _Z_IMAGE_ANTI_RE.search(k) is not None:
+ return True
+ return False
diff --git a/invokeai/backend/patches/lora_conversions/wan_lora_conversion_utils.py b/invokeai/backend/patches/lora_conversions/wan_lora_conversion_utils.py
new file mode 100644
index 00000000000..5592572b246
--- /dev/null
+++ b/invokeai/backend/patches/lora_conversions/wan_lora_conversion_utils.py
@@ -0,0 +1,250 @@
+"""Wan 2.2 LoRA conversion utilities.
+
+Wan LoRAs target the ``WanTransformer3DModel`` attention and FFN layers. We
+normalise every supported source layout to the diffusers parameter-path naming
+the loaded model uses at runtime (``blocks..attn1.to_q``,
+``blocks..attn2.to_k``, ``blocks..ffn.net.0.proj``, etc.).
+
+Supported source layouts:
+
+- **Diffusers PEFT**: ``[transformer.|base_model.model.transformer.]blocks.X.attn1.to_q.lora_A.weight``
+- **Native PEFT** (ComfyUI / Wan-AI native naming, with diffusion_model or transformer prefix):
+ ``diffusion_model.blocks.X.self_attn.q.lora_A.weight``
+- **Kohya** in either naming: ``lora_unet_blocks_X_attn1_to_q.lora_down.weight``
+ or ``lora_unet_blocks_X_self_attn_q.lora_down.weight``
+"""
+
+import re
+from typing import Dict
+
+import torch
+
+from invokeai.backend.patches.layers.base_layer_patch import BaseLayerPatch
+from invokeai.backend.patches.layers.utils import any_lora_layer_from_state_dict
+from invokeai.backend.patches.lora_conversions.wan_lora_constants import (
+ WAN_LORA_TRANSFORMER_PREFIX,
+ has_wan_kohya_keys,
+)
+from invokeai.backend.patches.model_patch_raw import ModelPatchRaw
+
+# Kohya layer-name regex: lora_unet_blocks__
+_KOHYA_KEY_REGEX = re.compile(r"lora_unet_blocks_(\d+)_(.*)")
+
+
+# Kohya submodule name -> diffusers parameter-path tail.
+#
+# Longest-match-first ordering matters because some keys are prefixes of others
+# (e.g. ``attn1_to_q`` vs ``attn1_to_out_0``). The lookup is exact (not prefix),
+# so this is purely cosmetic, but kept consistent with QwenImage's convention.
+_KOHYA_SUBMODULE_MAP: list[tuple[str, str]] = [
+ # --- Diffusers naming ---
+ # Self-attention (attn1)
+ ("attn1_to_q", "attn1.to_q"),
+ ("attn1_to_k", "attn1.to_k"),
+ ("attn1_to_v", "attn1.to_v"),
+ ("attn1_to_out_0", "attn1.to_out.0"),
+ ("attn1_norm_q", "attn1.norm_q"),
+ ("attn1_norm_k", "attn1.norm_k"),
+ # Cross-attention (attn2)
+ ("attn2_to_q", "attn2.to_q"),
+ ("attn2_to_k", "attn2.to_k"),
+ ("attn2_to_v", "attn2.to_v"),
+ ("attn2_to_out_0", "attn2.to_out.0"),
+ ("attn2_norm_q", "attn2.norm_q"),
+ ("attn2_norm_k", "attn2.norm_k"),
+ # FFN diffusers
+ ("ffn_net_0_proj", "ffn.net.0.proj"),
+ ("ffn_net_2", "ffn.net.2"),
+ # --- Native naming (mapped onto diffusers paths) ---
+ # self_attn -> attn1
+ ("self_attn_q", "attn1.to_q"),
+ ("self_attn_k", "attn1.to_k"),
+ ("self_attn_v", "attn1.to_v"),
+ ("self_attn_o", "attn1.to_out.0"),
+ ("self_attn_norm_q", "attn1.norm_q"),
+ ("self_attn_norm_k", "attn1.norm_k"),
+ # cross_attn -> attn2
+ ("cross_attn_q", "attn2.to_q"),
+ ("cross_attn_k", "attn2.to_k"),
+ ("cross_attn_v", "attn2.to_v"),
+ ("cross_attn_o", "attn2.to_out.0"),
+ ("cross_attn_norm_q", "attn2.norm_q"),
+ ("cross_attn_norm_k", "attn2.norm_k"),
+ # FFN native
+ ("ffn_0", "ffn.net.0.proj"),
+ ("ffn_2", "ffn.net.2"),
+]
+
+
+# Layer-path rules used for PEFT-style keys: applied as substring replacements
+# to the *layer path* (everything between an optional prefix and the LoRA suffix).
+# Order matters — see ``convert_wan_transformer_to_diffusers`` in diffusers for
+# the equivalent state-dict-key rules. We use trailing-dot semantics so e.g.
+# ``.q.`` matches ``self_attn.q.something`` but not ``norm_q``.
+#
+# Paths are augmented with a sentinel trailing ``.`` before applying these
+# rules so that bare endings like ``blocks.0.self_attn.q`` get rewritten as
+# ``blocks.0.attn1.to_q``.
+_NATIVE_TO_DIFFUSERS_PATH_RULES: tuple[tuple[str, str], ...] = (
+ ("cross_attn.", "attn2."),
+ ("self_attn.", "attn1."),
+ (".o.", ".to_out.0."),
+ (".q.", ".to_q."),
+ (".k.", ".to_k."),
+ (".v.", ".to_v."),
+ ("ffn.0.", "ffn.net.0.proj."),
+ ("ffn.2.", "ffn.net.2."),
+)
+
+# Prefixes seen on PEFT-style Wan LoRA keys.
+_PEFT_PREFIXES_TO_STRIP: tuple[str, ...] = (
+ "base_model.model.transformer.",
+ "transformer.",
+ "diffusion_model.",
+)
+
+
+def lora_model_from_wan_state_dict(state_dict: Dict[str, torch.Tensor], alpha: float | None = None) -> ModelPatchRaw:
+ """Convert any supported Wan LoRA state dict into a ``ModelPatchRaw``.
+
+ Detects Kohya vs PEFT layouts and dispatches accordingly. Layer paths in
+ the returned patch use diffusers naming (``blocks.X.attn1.to_q``) prefixed
+ with ``WAN_LORA_TRANSFORMER_PREFIX`` so the runtime ``LayerPatcher`` can
+ match them against ``WanTransformer3DModel`` parameters.
+ """
+ str_keys = [k for k in state_dict.keys() if isinstance(k, str)]
+ if has_wan_kohya_keys(str_keys):
+ return _convert_kohya_format(state_dict, alpha)
+ return _convert_peft_format(state_dict, alpha)
+
+
+def _convert_kohya_format(state_dict: Dict[str, torch.Tensor], alpha: float | None) -> ModelPatchRaw:
+ """Convert a Kohya-format Wan LoRA state dict.
+
+ Keys look like ``lora_unet_blocks__.{lora_down,lora_up,alpha}.weight``.
+ Unrecognised submodules are silently skipped (logged at conversion debug level
+ by the layer factory if needed).
+ """
+ layers: dict[str, BaseLayerPatch] = {}
+ grouped = _group_by_layer(state_dict)
+
+ for kohya_layer, layer_dict in grouped.items():
+ path = _kohya_layer_to_diffusers_path(kohya_layer)
+ if path is None:
+ continue
+ values = _normalize_lora_param_names(layer_dict, alpha)
+ layers[f"{WAN_LORA_TRANSFORMER_PREFIX}{path}"] = any_lora_layer_from_state_dict(values)
+
+ return ModelPatchRaw(layers=layers)
+
+
+def _convert_peft_format(state_dict: Dict[str, torch.Tensor], alpha: float | None) -> ModelPatchRaw:
+ """Convert a Diffusers-PEFT or native-PEFT Wan LoRA state dict."""
+ layers: dict[str, BaseLayerPatch] = {}
+ grouped = _group_by_layer(state_dict)
+
+ for raw_layer_key, layer_dict in grouped.items():
+ stripped = _strip_peft_prefix(raw_layer_key)
+ path = _native_layer_path_to_diffusers(stripped)
+ if path is None:
+ continue
+ values = _normalize_lora_param_names(layer_dict, alpha)
+ layers[f"{WAN_LORA_TRANSFORMER_PREFIX}{path}"] = any_lora_layer_from_state_dict(values)
+
+ return ModelPatchRaw(layers=layers)
+
+
+def _kohya_layer_to_diffusers_path(kohya_layer: str) -> str | None:
+ """``lora_unet_blocks_0_self_attn_q`` -> ``blocks.0.attn1.to_q``."""
+ m = _KOHYA_KEY_REGEX.match(kohya_layer)
+ if not m:
+ return None
+ block_idx = m.group(1)
+ sub = m.group(2)
+ for kohya_sub, diffusers_sub in _KOHYA_SUBMODULE_MAP:
+ if sub == kohya_sub:
+ return f"blocks.{block_idx}.{diffusers_sub}"
+ return None
+
+
+def _strip_peft_prefix(layer_key: str) -> str:
+ """Strip ``transformer.``, ``diffusion_model.``, ``base_model.model.transformer.`` if present."""
+ for prefix in _PEFT_PREFIXES_TO_STRIP:
+ if layer_key.startswith(prefix):
+ return layer_key[len(prefix) :]
+ return layer_key
+
+
+def _native_layer_path_to_diffusers(path: str) -> str | None:
+ """Rewrite a stripped PEFT layer path to diffusers naming.
+
+ No-op if the path is already in diffusers form (contains attn1./attn2./ffn.net.).
+ Returns None only if the path can't be plausibly identified as Wan.
+ """
+ if not path.startswith("blocks."):
+ return None
+
+ if "attn1." in path or "attn2." in path or "ffn.net." in path:
+ return path
+
+ # Apply the native-to-diffusers replacements with a sentinel trailing dot
+ # so rules like ``.q.`` fire on a bare-ending ``...self_attn.q``.
+ augmented = path + "."
+ for needle, replacement in _NATIVE_TO_DIFFUSERS_PATH_RULES:
+ augmented = augmented.replace(needle, replacement)
+ return augmented.rstrip(".")
+
+
+def _normalize_lora_param_names(layer_dict: dict[str, torch.Tensor], alpha: float | None) -> dict[str, torch.Tensor]:
+ """Map PEFT-style ``lora_A``/``lora_B`` to ``lora_down``/``lora_up``.
+
+ Kohya-style ``lora_down``/``lora_up`` pass through unchanged.
+ """
+ if "lora_A.weight" in layer_dict:
+ values: dict[str, torch.Tensor] = {
+ "lora_down.weight": layer_dict["lora_A.weight"],
+ "lora_up.weight": layer_dict["lora_B.weight"],
+ }
+ if alpha is not None:
+ values["alpha"] = torch.tensor(alpha)
+ if "alpha" in layer_dict:
+ values["alpha"] = layer_dict["alpha"]
+ if "dora_scale" in layer_dict:
+ values["dora_scale"] = layer_dict["dora_scale"]
+ return values
+ return layer_dict
+
+
+def _group_by_layer(state_dict: Dict[str, torch.Tensor]) -> dict[str, dict[str, torch.Tensor]]:
+ """Group state-dict keys by their layer path (everything before the LoRA-suffix tail)."""
+ grouped: dict[str, dict[str, torch.Tensor]] = {}
+
+ known_suffixes = [
+ ".lora_A.weight",
+ ".lora_B.weight",
+ ".lora_down.weight",
+ ".lora_up.weight",
+ ".dora_scale",
+ ".alpha",
+ ]
+
+ for key in state_dict:
+ if not isinstance(key, str):
+ continue
+
+ layer_name = None
+ key_name = None
+ for suffix in known_suffixes:
+ if key.endswith(suffix):
+ layer_name = key[: -len(suffix)]
+ key_name = suffix[1:] # drop leading dot
+ break
+
+ if layer_name is None:
+ parts = key.rsplit(".", maxsplit=2)
+ layer_name = parts[0]
+ key_name = ".".join(parts[1:])
+
+ grouped.setdefault(layer_name, {})[key_name] = state_dict[key]
+
+ return grouped
diff --git a/invokeai/backend/stable_diffusion/diffusion/conditioning_data.py b/invokeai/backend/stable_diffusion/diffusion/conditioning_data.py
index 6a9959f1e87..2274b34890b 100644
--- a/invokeai/backend/stable_diffusion/diffusion/conditioning_data.py
+++ b/invokeai/backend/stable_diffusion/diffusion/conditioning_data.py
@@ -130,6 +130,27 @@ def to(self, device: torch.device | None = None, dtype: torch.dtype | None = Non
return self
+@dataclass
+class WanConditioningInfo:
+ """Wan 2.2 text conditioning information from the UMT5-XXL encoder.
+
+ The Wan transformer takes the encoder's last hidden state directly as
+ cross-attention context (``encoder_hidden_states``).
+ """
+
+ prompt_embeds: torch.Tensor
+ """UMT5-XXL hidden states. Shape: (seq_len, hidden_size) where hidden_size=4096."""
+
+ prompt_attention_mask: torch.Tensor | None = None
+ """Attention mask marking valid (non-padding) tokens. Shape: (seq_len,). 1 for valid, 0 for padding."""
+
+ def to(self, device: torch.device | None = None, dtype: torch.dtype | None = None):
+ self.prompt_embeds = self.prompt_embeds.to(device=device, dtype=dtype)
+ if self.prompt_attention_mask is not None:
+ self.prompt_attention_mask = self.prompt_attention_mask.to(device=device)
+ return self
+
+
@dataclass
class ConditioningFieldData:
# If you change this class, adding more types, you _must_ update the instantiation of ObjectSerializerDisk in
@@ -144,6 +165,7 @@ class ConditioningFieldData:
| List[ZImageConditioningInfo]
| List[QwenImageConditioningInfo]
| List[AnimaConditioningInfo]
+ | List[WanConditioningInfo]
)
diff --git a/invokeai/backend/util/vae_working_memory.py b/invokeai/backend/util/vae_working_memory.py
index 7a2fa1f381b..fbd1c0e1280 100644
--- a/invokeai/backend/util/vae_working_memory.py
+++ b/invokeai/backend/util/vae_working_memory.py
@@ -125,6 +125,45 @@ def estimate_vae_working_memory_anima(
return int(working_memory)
+def estimate_vae_working_memory_wan(
+ operation: Literal["encode", "decode"],
+ vae: AutoencoderKLWan,
+ pixel_height: int,
+ pixel_width: int,
+ pixel_frames: int,
+ tile_size: int | None = None,
+) -> int:
+ """Estimate the working memory required to encode or decode with a Wan VAE.
+
+ Generalizes the single-frame Wan 2.1 calibration (see
+ estimate_vae_working_memory_anima) to multi-frame clips and to the TI2V-5B VAE's
+ 16x spatial compression — callers pass *pixel-space* dimensions, so the VAE's
+ spatial scale factor is already applied. The Wan VAE processes the clip causally,
+ one latent frame at a time with cached features, so the conv working set scales
+ with a single frame's pixels; what grows with clip length is the full RGB clip,
+ which diffusers keeps resident on the execution device for the whole operation.
+ """
+ element_size = next(vae.parameters()).element_size()
+
+ # Per-frame conv working set: ~2900 bytes per output pixel per element byte for a
+ # full-frame decode, encode ~50% (calibrated empirically on a Wan 2.1 fp16 decode).
+ scaling_constant = 2900 if operation == "decode" else 1450
+ if tile_size is not None:
+ # Add 25% for tile overlap.
+ per_frame = tile_size * tile_size * element_size * scaling_constant * 1.25
+ else:
+ per_frame = pixel_height * pixel_width * element_size * scaling_constant
+
+ # The full RGB clip stays on the execution device regardless of tiling (decode
+ # output / encode input). Decode accumulates frames with torch.cat, whose final
+ # iterations transiently hold both the accumulated clip and its copy — ~2x the
+ # clip bytes at peak. Encode consumes the input clip without duplicating it.
+ clip_copies = 2 if operation == "decode" else 1
+ clip_bytes = clip_copies * 3 * pixel_frames * pixel_height * pixel_width * element_size
+
+ return int(per_frame + clip_bytes)
+
+
def estimate_vae_working_memory_qwen_image(
operation: Literal["encode", "decode"], image_tensor: torch.Tensor, vae: AutoencoderKLQwenImage
) -> int:
diff --git a/invokeai/backend/wan/__init__.py b/invokeai/backend/wan/__init__.py
new file mode 100644
index 00000000000..e69de29bb2d
diff --git a/invokeai/backend/wan/extensions/__init__.py b/invokeai/backend/wan/extensions/__init__.py
new file mode 100644
index 00000000000..e69de29bb2d
diff --git a/invokeai/backend/wan/extensions/wan_ref_image_extension.py b/invokeai/backend/wan/extensions/wan_ref_image_extension.py
new file mode 100644
index 00000000000..867325b6f10
--- /dev/null
+++ b/invokeai/backend/wan/extensions/wan_ref_image_extension.py
@@ -0,0 +1,219 @@
+"""Wan 2.2 I2V reference-image conditioning.
+
+Wan 2.2 I2V-A14B conditions on a reference image by **VAE-encoding** it and
+concatenating the resulting latents to the noise latents along the channel
+dim — its transformer has ``in_channels=36`` (16 noise + 16 ref-image latents
++ 4 first-frame mask) rather than 16.
+
+This module produces the 20-channel condition tensor ``[B, 20, T_lat, H_lat, W_lat]``
+that the denoise loop will concatenate to the 16-channel noise latents each
+step, yielding the 36-channel input the I2V transformer expects.
+
+Mirrors diffusers ``WanImageToVideoPipeline.prepare_latents`` lines 423–481
+with ``num_frames=1`` and ``expand_timesteps=False`` (the defaults for
+single-frame image generation).
+"""
+
+import torch
+import torchvision.transforms.functional as TF
+from diffusers.models.autoencoders import AutoencoderKLWan
+from PIL import Image
+
+# Wan 2.2 VAE temporal scale factor — single frame still consumes a 4-position
+# slice of the mask tensor, which is why the mask contributes 4 channels.
+_WAN_VAE_TEMPORAL_SCALE = 4
+
+
+def preprocess_reference_image(image: Image.Image, width: int, height: int) -> torch.Tensor:
+ """Resize a PIL image to (width, height) and return a normalised [-1, 1]
+ tensor of shape ``[1, 3, 1, height, width]`` ready for ``AutoencoderKLWan.encode``."""
+ if width % 8 != 0 or height % 8 != 0:
+ raise ValueError(f"Reference-image dimensions must be multiples of 8 (got {width}x{height}).")
+ resized = image.convert("RGB").resize((width, height), Image.LANCZOS)
+ # [0, 1] CHW float tensor.
+ pixel = TF.to_tensor(resized)
+ # Scale to [-1, 1] to match the Wan VAE's expected input range.
+ pixel = pixel * 2.0 - 1.0
+ # [3, H, W] -> [1, 3, 1, H, W]: add batch + temporal dims.
+ return pixel.unsqueeze(0).unsqueeze(2)
+
+
+def encode_reference_image_to_ti2v_condition(
+ image: Image.Image,
+ vae: AutoencoderKLWan,
+ width: int,
+ height: int,
+ device: torch.device,
+ dtype: torch.dtype,
+) -> torch.Tensor:
+ """Build the TI2V-5B-style reference condition tensor.
+
+ Returns shape ``[1, 48, 1, height // 16, width // 16]`` — single VAE-encoded
+ latent frame of the reference image, normalised against the VAE's
+ per-channel mean/std. TI2V-5B does **not** use the A14B 4-channel mask;
+ the mask is built inline in the denoise loop (``expand_timesteps`` path)
+ and used to blend this condition with the noisy latents at each step:
+ ``(1 - mask) * condition + mask * latents``.
+
+ Mirrors :class:`diffusers.WanImageToVideoPipeline.prepare_latents` lines
+ 423-466 with ``expand_timesteps=True``.
+
+ Wan 2.2-VAE has 16x spatial compression (vs A14B's 8x), so the latent
+ dims are ``height // 16`` and ``width // 16``.
+ """
+ vae_dtype = next(iter(vae.parameters())).dtype
+ pixel = preprocess_reference_image(image, width=width, height=height).to(device=device, dtype=vae_dtype)
+
+ with torch.inference_mode():
+ encoded = vae.encode(pixel, return_dict=False)[0]
+ latents = encoded.sample() # [1, 48, 1, H_lat, W_lat]
+
+ latents_mean = torch.tensor(vae.config.latents_mean).view(1, -1, 1, 1, 1).to(latents.device, latents.dtype)
+ latents_std = torch.tensor(vae.config.latents_std).view(1, -1, 1, 1, 1).to(latents.device, latents.dtype)
+ latent_condition = (latents - latents_mean) / latents_std
+
+ return latent_condition.to(dtype=dtype)
+
+
+def encode_reference_image_to_condition(
+ image: Image.Image,
+ vae: AutoencoderKLWan,
+ width: int,
+ height: int,
+ device: torch.device,
+ dtype: torch.dtype,
+) -> torch.Tensor:
+ """Build the 20-channel I2V condition tensor for a reference image.
+
+ Returns shape ``[1, 20, 1, height // 8, width // 8]`` (4-channel first-frame
+ mask concatenated with 16-channel VAE-encoded image latents along the
+ channel dim).
+
+ The output should later be concatenated with the 16-channel noise latents
+ inside the denoise loop to produce the 36-channel input the I2V transformer
+ expects.
+ """
+ vae_dtype = next(iter(vae.parameters())).dtype
+ pixel = preprocess_reference_image(image, width=width, height=height).to(device=device, dtype=vae_dtype)
+
+ with torch.inference_mode():
+ encoded = vae.encode(pixel, return_dict=False)[0]
+ latents = encoded.sample() # [1, 16, 1, H_lat, W_lat]
+
+ # Normalise against the VAE's per-channel mean/std, matching diffusers'
+ # ``WanImageToVideoPipeline.prepare_latents`` (lines 440-459). Note the
+ # multiplication by 1/std == division by std.
+ latents_mean = torch.tensor(vae.config.latents_mean).view(1, -1, 1, 1, 1).to(latents.device, latents.dtype)
+ latents_std = torch.tensor(vae.config.latents_std).view(1, -1, 1, 1, 1).to(latents.device, latents.dtype)
+ latent_condition = (latents - latents_mean) / latents_std
+
+ latent_condition = latent_condition.to(dtype=dtype)
+
+ # First-frame mask: at num_frames=1 every position is "the first frame"
+ # (i.e., conditioned). After the temporal-scale expansion the mask is
+ # 4 channels of ones at [1, T_lat=1, H_lat, W_lat].
+ _, _, t_lat, h_lat, w_lat = latent_condition.shape
+ mask = torch.ones(1, _WAN_VAE_TEMPORAL_SCALE, t_lat, h_lat, w_lat, device=device, dtype=dtype)
+
+ return torch.cat([mask, latent_condition], dim=1)
+
+
+def encode_reference_image_to_video_condition(
+ image: Image.Image,
+ vae: AutoencoderKLWan,
+ width: int,
+ height: int,
+ num_frames: int,
+ device: torch.device,
+ dtype: torch.dtype,
+ last_image: Image.Image | None = None,
+) -> torch.Tensor:
+ """Build the multi-frame I2V condition tensor for Wan 2.2 video generation.
+
+ Returns shape ``[1, 20, T_lat, height // 8, width // 8]`` where
+ ``T_lat = (num_frames - 1) // 4 + 1`` (the standard Wan VAE temporal
+ compression — e.g. 21 latent frames for 81 pixel frames). First 4 channels
+ are the rearranged first-frame mask, last 16 channels are the VAE-encoded
+ latents of the image+zero pseudo-video.
+
+ When ``last_image`` is given the function builds a **first-last-frame
+ (FLF2V)** condition instead: the end image is placed in the final temporal
+ slot and the mask anchors **both** the first and last latent frames, so the
+ model interpolates the motion between the two stills. With ``last_image=None``
+ it builds the standard single-reference I2V condition (frame 0 anchored,
+ the rest free).
+
+ Mirrors :class:`diffusers.WanImageToVideoPipeline.prepare_latents`
+ (``expand_timesteps=False``):
+
+ 1. The reference image is concatenated with zero pixel-frames to form a
+ ``[1, 3, num_frames, H, W]`` pseudo-video — frame 0 carries the start
+ image, frame ``num_frames - 1`` carries ``last_image`` (when given), and
+ the frames in between are zero. The model was trained against latents
+ produced this way; padding in latent space after a 1-frame VAE encode
+ would land different values.
+ 2. The VAE encodes that to ``[1, 16, T_lat, H_lat, W_lat]`` and we
+ normalise by the per-channel ``(mean, std)`` from ``vae.config``.
+ 3. The mask starts in pixel-frame space as ``[1, 1, num_frames, ...]``
+ with 1 at the anchored frame(s) and 0 elsewhere — frame 0 for plain I2V,
+ or frames 0 and ``num_frames - 1`` for FLF2V. The first frame is repeated
+ 4× then the whole thing is reshaped/transposed into ``[1, 4, T_lat, ...]``.
+
+ The denoise loop concatenates the result along the channel dim to the
+ 16-channel noise latents each step, yielding the 36-channel input the
+ Wan 2.2 I2V-A14B transformer expects.
+ """
+ if last_image is not None and num_frames <= 1:
+ raise ValueError("last_image (FLF2V) interpolation requires num_frames > 1.")
+
+ vae_dtype = next(iter(vae.parameters())).dtype
+ pixel = preprocess_reference_image(image, width=width, height=height).to(
+ device=device, dtype=vae_dtype
+ ) # [1, 3, 1, H, W]
+
+ # Pad the temporal dim with zero pixel-frames; the VAE handles temporal
+ # compression to T_lat. For FLF2V the end image takes the final slot and
+ # only the in-between frames are zero.
+ if num_frames > 1:
+ if last_image is not None:
+ last_pixel = preprocess_reference_image(last_image, width=width, height=height).to(
+ device=device, dtype=vae_dtype
+ )
+ middle_zeros = torch.zeros(1, 3, num_frames - 2, height, width, device=device, dtype=vae_dtype)
+ video_condition = torch.cat([pixel, middle_zeros, last_pixel], dim=2)
+ else:
+ zero_frames = torch.zeros(1, 3, num_frames - 1, height, width, device=device, dtype=vae_dtype)
+ video_condition = torch.cat([pixel, zero_frames], dim=2)
+ else:
+ video_condition = pixel
+
+ with torch.inference_mode():
+ encoded = vae.encode(video_condition, return_dict=False)[0]
+ latents = encoded.sample() # [1, 16, T_lat, H_lat, W_lat]
+
+ latents_mean = torch.tensor(vae.config.latents_mean).view(1, -1, 1, 1, 1).to(latents.device, latents.dtype)
+ latents_std = torch.tensor(vae.config.latents_std).view(1, -1, 1, 1, 1).to(latents.device, latents.dtype)
+ latent_condition = (latents - latents_mean) / latents_std
+
+ latent_condition = latent_condition.to(dtype=dtype)
+ _, _, t_lat, h_lat, w_lat = latent_condition.shape
+
+ # Build the mask in pixel-frame space then rearrange to the 4-channel
+ # latent-temporal form the transformer expects.
+ mask_pixel = torch.ones(1, 1, num_frames, h_lat, w_lat, device=device, dtype=dtype)
+ if num_frames > 1:
+ if last_image is not None:
+ # FLF2V: keep both the first and last frames anchored; zero the middle.
+ mask_pixel[:, :, 1:-1] = 0
+ else:
+ mask_pixel[:, :, 1:] = 0
+
+ first_frame_mask = mask_pixel[:, :, 0:1].repeat_interleave(repeats=_WAN_VAE_TEMPORAL_SCALE, dim=2)
+ mask = torch.cat([first_frame_mask, mask_pixel[:, :, 1:]], dim=2)
+ # mask is now [1, 1, _WAN_VAE_TEMPORAL_SCALE + (num_frames - 1), H_lat, W_lat]
+ # = [1, 1, num_frames + 3, H_lat, W_lat]. Total temporal positions
+ # (num_frames + 3) equals (t_lat * _WAN_VAE_TEMPORAL_SCALE) when
+ # (num_frames - 1) is divisible by 4 (the contract of num_latent_frames_for).
+ mask = mask.view(1, t_lat, _WAN_VAE_TEMPORAL_SCALE, h_lat, w_lat).transpose(1, 2)
+
+ return torch.cat([mask, latent_condition], dim=1)
diff --git a/invokeai/backend/wan/rocm_causal_conv3d.py b/invokeai/backend/wan/rocm_causal_conv3d.py
new file mode 100644
index 00000000000..77d30f4548e
--- /dev/null
+++ b/invokeai/backend/wan/rocm_causal_conv3d.py
@@ -0,0 +1,81 @@
+"""ROCm workaround: decompose WanCausalConv3d into per-temporal-tap conv2d calls.
+
+MIOpen (ROCm's cuDNN equivalent) has no implicit-GEMM 3D-convolution kernels for
+the shapes used by the Wan VAE on RDNA3 — it falls back to ``Im3d2Col``, which
+materializes every 3x3x3 patch into a matrix before a GEMM. Profiling a Wan 2.1
+VAE decode on a W7900 showed Im3d2Col consuming 61% of GPU time, and neither
+dtype changes nor ``cudnn.benchmark`` kernel search helped (all configs within
+±7%). MIOpen's *2D* convolutions are well optimized, and a stride-1 kT x kH x kW
+conv3d is exactly the sum of kT conv2d taps over shifted temporal slices, so this
+module rebinds ``WanCausalConv3d.forward`` to that decomposition.
+
+Measured on a W7900 (832x480, 3 latent frames, bf16): 81.6s -> 1.71s (~48x),
+matching NVIDIA wall-clock for the same decode. Numerics: identical math up to
+floating-point summation order — max abs error vs ``F.conv3d`` is ~1e-6 in fp32;
+a full bf16 VAE decode differs by at most ~3/255 in pixel space with 0.1% of
+pixels off by more than 1/255 (bf16 accumulation noise, visually imperceptible).
+
+The patch is class-level and idempotent, applied only when torch is a ROCm/HIP
+build. It covers every ``AutoencoderKLWan`` consumer (Wan decode/encode nodes,
+ref-image encoding, Anima's VAE) regardless of which loader constructed it.
+"""
+
+import torch
+import torch.nn.functional as F
+
+_SENTINEL = "_invokeai_rocm_conv2d_decomposition"
+
+
+def _decomposed_conv3d(module: torch.nn.Conv3d, x: torch.Tensor) -> torch.Tensor:
+ """``F.conv3d(x, module.weight, module.bias)`` for stride-1/dilation-1/groups-1
+ convs, computed as kT batched conv2d taps. ``x`` must already be padded."""
+ b, c, t, h, w = x.shape
+ k_t = module.weight.shape[2]
+ t_out = t - k_t + 1
+ out = None
+ for k in range(k_t):
+ xs = x[:, :, k : k + t_out].transpose(1, 2).reshape(b * t_out, c, h, w)
+ o = F.conv2d(xs, module.weight[:, :, k], None)
+ out = o if out is None else out + o
+ assert out is not None
+ if module.bias is not None:
+ out = out + module.bias.view(1, -1, 1, 1)
+ oh, ow = out.shape[-2:]
+ return out.reshape(b, t_out, -1, oh, ow).transpose(1, 2)
+
+
+def _decomposed_forward(self, x: torch.Tensor, cache_x: torch.Tensor | None = None) -> torch.Tensor:
+ # Causal-padding / feature-cache handling copied verbatim from
+ # diffusers.models.autoencoders.autoencoder_kl_wan.WanCausalConv3d.forward.
+ padding = list(self._padding)
+ if cache_x is not None and self._padding[4] > 0:
+ cache_x = cache_x.to(x.device)
+ x = torch.cat([cache_x, x], dim=2)
+ padding[4] -= cache_x.shape[2]
+ x = F.pad(x, padding)
+ if self.stride != (1, 1, 1) or self.dilation != (1, 1, 1) or self.groups != 1:
+ # Not worth decomposing (and stride couples the temporal taps) — these
+ # only occur on encoder downsample convs, which are a minority of calls.
+ return F.conv3d(x, self.weight, self.bias, self.stride, (0, 0, 0), self.dilation, self.groups)
+ return _decomposed_conv3d(self, x)
+
+
+def _patch_wan_causal_conv3d() -> None:
+ """Rebind WanCausalConv3d.forward to the conv2d decomposition (idempotent)."""
+ from diffusers.models.autoencoders.autoencoder_kl_wan import WanCausalConv3d
+
+ if getattr(WanCausalConv3d, _SENTINEL, False):
+ return
+ WanCausalConv3d.forward = _decomposed_forward
+ setattr(WanCausalConv3d, _SENTINEL, True)
+
+
+def patch_wan_causal_conv3d_for_rocm() -> None:
+ """Apply the conv2d decomposition on ROCm builds; no-op elsewhere.
+
+ Call from any loader that constructs an ``AutoencoderKLWan``. cuDNN has real
+ implicit-GEMM conv3d kernels, so CUDA builds keep the stock path.
+ """
+ if torch.version.hip is None:
+ return
+ _patch_wan_causal_conv3d()
diff --git a/invokeai/backend/wan/sampling_utils.py b/invokeai/backend/wan/sampling_utils.py
new file mode 100644
index 00000000000..f8e4b6f632a
--- /dev/null
+++ b/invokeai/backend/wan/sampling_utils.py
@@ -0,0 +1,82 @@
+"""Sampling utilities for Wan 2.2 image generation.
+
+Single-frame inference uses 5D ``[B, C, T=1, H, W]`` latent tensors. The
+scale factors are dictated by the model variant:
+
+* A14B — standard Wan VAE: spatial 8x, latent channels 16
+* TI2V-5B — Wan2.2-VAE: spatial 16x, latent channels 48
+"""
+
+from __future__ import annotations
+
+import torch
+
+from invokeai.backend.model_manager.taxonomy import WanVariantType
+
+
+def get_spatial_scale_factor(variant: WanVariantType) -> int:
+ """Return the VAE spatial downsampling factor for a Wan variant."""
+ if variant == WanVariantType.TI2V_5B:
+ return 16
+ return 8 # A14B and any future single-expert variant default to standard Wan VAE.
+
+
+def get_default_latent_channels(variant: WanVariantType) -> int:
+ """Return the default latent-channel count for a Wan variant.
+
+ Use the actual transformer ``in_channels`` from the loaded model when
+ possible; this helper is for cases where we need the count before the
+ transformer is on device (e.g. building the noise tensor before entering
+ the model-on-device context).
+ """
+ if variant == WanVariantType.TI2V_5B:
+ return 48
+ return 16
+
+
+def make_noise(
+ *,
+ batch_size: int,
+ latent_channels: int,
+ height: int,
+ width: int,
+ spatial_scale_factor: int,
+ device: torch.device,
+ dtype: torch.dtype,
+ seed: int,
+ num_latent_frames: int = 1,
+) -> torch.Tensor:
+ """Generate Wan-shaped noise: ``[B, C, T_lat, H/s, W/s]``.
+
+ For single-frame image generation the default ``num_latent_frames=1`` yields
+ a temporal dim of 1 (matching the original behaviour). Video generation
+ passes the latent-space frame count computed from the pixel-frame count via
+ :func:`num_latent_frames_for` (or directly).
+
+ Mirrors Anima's ``_get_noise``: noise is generated on CPU (deterministic
+ across CUDA / ROCm / MPS) and moved to ``device`` afterwards.
+ """
+ return torch.randn(
+ batch_size,
+ latent_channels,
+ num_latent_frames,
+ height // spatial_scale_factor,
+ width // spatial_scale_factor,
+ device="cpu",
+ dtype=torch.float32,
+ generator=torch.Generator(device="cpu").manual_seed(seed),
+ ).to(device=device, dtype=dtype)
+
+
+# Wan 2.2 VAE temporal compression ratio. 4 pixel-frames collapse to 1 latent-
+# temporal slice (this is also why the I2V conditioning mask has 4 channels).
+WAN_VAE_TEMPORAL_SCALE_FACTOR = 4
+
+
+def num_latent_frames_for(num_frames: int, vae_scale_factor_temporal: int = WAN_VAE_TEMPORAL_SCALE_FACTOR) -> int:
+ """Convert a pixel-frame count to latent-frame count for the Wan VAE.
+
+ Matches Diffusers ``WanPipeline.prepare_latents``: ``(num_frames - 1) // s + 1``
+ (e.g. ``81 -> 21`` for the standard Wan VAE).
+ """
+ return (num_frames - 1) // vae_scale_factor_temporal + 1
diff --git a/invokeai/frontend/web/openapi.json b/invokeai/frontend/web/openapi.json
index 22c6d43e0e3..ad64697dce0 100644
--- a/invokeai/frontend/web/openapi.json
+++ b/invokeai/frontend/web/openapi.json
@@ -92,6 +92,31 @@
]
}
},
+ "/api/v1/auth/media-cookie": {
+ "post": {
+ "tags": ["authentication"],
+ "summary": "Refresh Media Cookie",
+ "description": "Re-issue the media cookie from a valid Bearer token.\n\nThe media cookie is normally set at login, but a session can hold a valid JWT\nwithout it \u2014 the session may predate the cookie's introduction, or the cookie\nmay have been cleared while the JWT (in client storage) survived. Media\nelements can't send Authorization headers, so such sessions silently fail to\nload videos (black player) while every other API call works. The frontend\ncalls this on app load so an existing session self-heals without re-login.\n\nThe cookie's lifetime is clamped to the presented token's remaining validity \u2014\nit is the same JWT, so a longer-lived cookie would just yield 401s after\nexpiry anyway.\n\nReturns:\n MediaCookieResponse indicating the cookie was set. In single-user mode the\n media routes don't require authentication, so this is a successful no-op.\n\nRaises:\n HTTPException: 401 if the Bearer token is missing, invalid, or expired, or\n the user no longer exists or is inactive (raised by the auth dependency).",
+ "operationId": "refresh_media_cookie_api_v1_auth_media_cookie_post",
+ "responses": {
+ "200": {
+ "description": "Successful Response",
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/MediaCookieResponse"
+ }
+ }
+ }
+ }
+ },
+ "security": [
+ {
+ "HTTPBearer": []
+ }
+ ]
+ }
+ },
"/api/v1/auth/me": {
"get": {
"tags": ["authentication"],
@@ -817,333 +842,7 @@
"$ref": "#/components/schemas/Main_Diffusers_QwenImage_Config"
},
{
- "$ref": "#/components/schemas/Main_Diffusers_ZImage_Config"
- },
- {
- "$ref": "#/components/schemas/Main_Checkpoint_SD1_Config"
- },
- {
- "$ref": "#/components/schemas/Main_Checkpoint_SD2_Config"
- },
- {
- "$ref": "#/components/schemas/Main_Checkpoint_SDXL_Config"
- },
- {
- "$ref": "#/components/schemas/Main_Checkpoint_SDXLRefiner_Config"
- },
- {
- "$ref": "#/components/schemas/Main_Checkpoint_Flux2_Config"
- },
- {
- "$ref": "#/components/schemas/Main_Checkpoint_FLUX_Config"
- },
- {
- "$ref": "#/components/schemas/Main_Checkpoint_QwenImage_Config"
- },
- {
- "$ref": "#/components/schemas/Main_Checkpoint_ZImage_Config"
- },
- {
- "$ref": "#/components/schemas/Main_Checkpoint_Anima_Config"
- },
- {
- "$ref": "#/components/schemas/Main_BnBNF4_FLUX_Config"
- },
- {
- "$ref": "#/components/schemas/Main_GGUF_Flux2_Config"
- },
- {
- "$ref": "#/components/schemas/Main_GGUF_FLUX_Config"
- },
- {
- "$ref": "#/components/schemas/Main_GGUF_QwenImage_Config"
- },
- {
- "$ref": "#/components/schemas/Main_GGUF_ZImage_Config"
- },
- {
- "$ref": "#/components/schemas/VAE_Checkpoint_SD1_Config"
- },
- {
- "$ref": "#/components/schemas/VAE_Checkpoint_SD2_Config"
- },
- {
- "$ref": "#/components/schemas/VAE_Checkpoint_SDXL_Config"
- },
- {
- "$ref": "#/components/schemas/VAE_Checkpoint_FLUX_Config"
- },
- {
- "$ref": "#/components/schemas/VAE_Checkpoint_Flux2_Config"
- },
- {
- "$ref": "#/components/schemas/VAE_Checkpoint_QwenImage_Config"
- },
- {
- "$ref": "#/components/schemas/VAE_Checkpoint_Anima_Config"
- },
- {
- "$ref": "#/components/schemas/VAE_Diffusers_SD1_Config"
- },
- {
- "$ref": "#/components/schemas/VAE_Diffusers_SDXL_Config"
- },
- {
- "$ref": "#/components/schemas/VAE_Diffusers_Flux2_Config"
- },
- {
- "$ref": "#/components/schemas/ControlNet_Checkpoint_SD1_Config"
- },
- {
- "$ref": "#/components/schemas/ControlNet_Checkpoint_SD2_Config"
- },
- {
- "$ref": "#/components/schemas/ControlNet_Checkpoint_SDXL_Config"
- },
- {
- "$ref": "#/components/schemas/ControlNet_Checkpoint_FLUX_Config"
- },
- {
- "$ref": "#/components/schemas/ControlNet_Checkpoint_ZImage_Config"
- },
- {
- "$ref": "#/components/schemas/ControlNet_Checkpoint_Anima_Config"
- },
- {
- "$ref": "#/components/schemas/ControlNet_Diffusers_SD1_Config"
- },
- {
- "$ref": "#/components/schemas/ControlNet_Diffusers_SD2_Config"
- },
- {
- "$ref": "#/components/schemas/ControlNet_Diffusers_SDXL_Config"
- },
- {
- "$ref": "#/components/schemas/ControlNet_Diffusers_FLUX_Config"
- },
- {
- "$ref": "#/components/schemas/LoRA_LyCORIS_SD1_Config"
- },
- {
- "$ref": "#/components/schemas/LoRA_LyCORIS_SD2_Config"
- },
- {
- "$ref": "#/components/schemas/LoRA_LyCORIS_SDXL_Config"
- },
- {
- "$ref": "#/components/schemas/LoRA_LyCORIS_Flux2_Config"
- },
- {
- "$ref": "#/components/schemas/LoRA_LyCORIS_FLUX_Config"
- },
- {
- "$ref": "#/components/schemas/LoRA_LyCORIS_ZImage_Config"
- },
- {
- "$ref": "#/components/schemas/LoRA_LyCORIS_QwenImage_Config"
- },
- {
- "$ref": "#/components/schemas/LoRA_LyCORIS_Anima_Config"
- },
- {
- "$ref": "#/components/schemas/LoRA_OMI_SDXL_Config"
- },
- {
- "$ref": "#/components/schemas/LoRA_OMI_FLUX_Config"
- },
- {
- "$ref": "#/components/schemas/LoRA_Diffusers_SD1_Config"
- },
- {
- "$ref": "#/components/schemas/LoRA_Diffusers_SD2_Config"
- },
- {
- "$ref": "#/components/schemas/LoRA_Diffusers_SDXL_Config"
- },
- {
- "$ref": "#/components/schemas/LoRA_Diffusers_Flux2_Config"
- },
- {
- "$ref": "#/components/schemas/LoRA_Diffusers_FLUX_Config"
- },
- {
- "$ref": "#/components/schemas/LoRA_Diffusers_ZImage_Config"
- },
- {
- "$ref": "#/components/schemas/ControlLoRA_LyCORIS_FLUX_Config"
- },
- {
- "$ref": "#/components/schemas/T5Encoder_T5Encoder_Config"
- },
- {
- "$ref": "#/components/schemas/T5Encoder_BnBLLMint8_Config"
- },
- {
- "$ref": "#/components/schemas/Qwen3Encoder_Qwen3Encoder_Config"
- },
- {
- "$ref": "#/components/schemas/Qwen3Encoder_Checkpoint_Config"
- },
- {
- "$ref": "#/components/schemas/Qwen3Encoder_GGUF_Config"
- },
- {
- "$ref": "#/components/schemas/QwenVLEncoder_Diffusers_Config"
- },
- {
- "$ref": "#/components/schemas/QwenVLEncoder_Checkpoint_Config"
- },
- {
- "$ref": "#/components/schemas/TI_File_SD1_Config"
- },
- {
- "$ref": "#/components/schemas/TI_File_SD2_Config"
- },
- {
- "$ref": "#/components/schemas/TI_File_SDXL_Config"
- },
- {
- "$ref": "#/components/schemas/TI_Folder_SD1_Config"
- },
- {
- "$ref": "#/components/schemas/TI_Folder_SD2_Config"
- },
- {
- "$ref": "#/components/schemas/TI_Folder_SDXL_Config"
- },
- {
- "$ref": "#/components/schemas/IPAdapter_InvokeAI_SD1_Config"
- },
- {
- "$ref": "#/components/schemas/IPAdapter_InvokeAI_SD2_Config"
- },
- {
- "$ref": "#/components/schemas/IPAdapter_InvokeAI_SDXL_Config"
- },
- {
- "$ref": "#/components/schemas/IPAdapter_Checkpoint_SD1_Config"
- },
- {
- "$ref": "#/components/schemas/IPAdapter_Checkpoint_SD2_Config"
- },
- {
- "$ref": "#/components/schemas/IPAdapter_Checkpoint_SDXL_Config"
- },
- {
- "$ref": "#/components/schemas/IPAdapter_Checkpoint_FLUX_Config"
- },
- {
- "$ref": "#/components/schemas/T2IAdapter_Diffusers_SD1_Config"
- },
- {
- "$ref": "#/components/schemas/T2IAdapter_Diffusers_SDXL_Config"
- },
- {
- "$ref": "#/components/schemas/Spandrel_Checkpoint_Config"
- },
- {
- "$ref": "#/components/schemas/CLIPEmbed_Diffusers_G_Config"
- },
- {
- "$ref": "#/components/schemas/CLIPEmbed_Diffusers_L_Config"
- },
- {
- "$ref": "#/components/schemas/CLIPVision_Diffusers_Config"
- },
- {
- "$ref": "#/components/schemas/SigLIP_Diffusers_Config"
- },
- {
- "$ref": "#/components/schemas/FLUXRedux_Checkpoint_Config"
- },
- {
- "$ref": "#/components/schemas/LlavaOnevision_Diffusers_Config"
- },
- {
- "$ref": "#/components/schemas/TextLLM_Diffusers_Config"
- },
- {
- "$ref": "#/components/schemas/ExternalApiModelConfig"
- },
- {
- "$ref": "#/components/schemas/Unknown_Config"
- }
- ],
- "title": "Response Get Model Records By Attrs"
- }
- }
- }
- },
- "422": {
- "description": "Validation Error",
- "content": {
- "application/json": {
- "schema": {
- "$ref": "#/components/schemas/HTTPValidationError"
- }
- }
- }
- }
- }
- }
- },
- "/api/v2/models/get_by_hash": {
- "get": {
- "tags": ["model_manager"],
- "summary": "Get Model Records By Hash",
- "description": "Gets a model by its hash. This is useful for recalling models that were deleted and reinstalled,\nas the hash remains stable across reinstallations while the key (UUID) changes.",
- "operationId": "get_model_records_by_hash",
- "security": [
- {
- "HTTPBearer": []
- }
- ],
- "parameters": [
- {
- "name": "hash",
- "in": "query",
- "required": true,
- "schema": {
- "type": "string",
- "description": "The hash of the model",
- "title": "Hash"
- },
- "description": "The hash of the model"
- }
- ],
- "responses": {
- "200": {
- "description": "Successful Response",
- "content": {
- "application/json": {
- "schema": {
- "oneOf": [
- {
- "$ref": "#/components/schemas/Main_Diffusers_SD1_Config"
- },
- {
- "$ref": "#/components/schemas/Main_Diffusers_SD2_Config"
- },
- {
- "$ref": "#/components/schemas/Main_Diffusers_SDXL_Config"
- },
- {
- "$ref": "#/components/schemas/Main_Diffusers_SDXLRefiner_Config"
- },
- {
- "$ref": "#/components/schemas/Main_Diffusers_SD3_Config"
- },
- {
- "$ref": "#/components/schemas/Main_Diffusers_FLUX_Config"
- },
- {
- "$ref": "#/components/schemas/Main_Diffusers_Flux2_Config"
- },
- {
- "$ref": "#/components/schemas/Main_Diffusers_CogView4_Config"
- },
- {
- "$ref": "#/components/schemas/Main_Diffusers_QwenImage_Config"
+ "$ref": "#/components/schemas/Main_Diffusers_Wan_Config"
},
{
"$ref": "#/components/schemas/Main_Diffusers_ZImage_Config"
@@ -1187,6 +886,9 @@
{
"$ref": "#/components/schemas/Main_GGUF_QwenImage_Config"
},
+ {
+ "$ref": "#/components/schemas/Main_GGUF_Wan_Config"
+ },
{
"$ref": "#/components/schemas/Main_GGUF_ZImage_Config"
},
@@ -1205,6 +907,9 @@
{
"$ref": "#/components/schemas/VAE_Checkpoint_Flux2_Config"
},
+ {
+ "$ref": "#/components/schemas/VAE_Checkpoint_Wan_Config"
+ },
{
"$ref": "#/components/schemas/VAE_Checkpoint_QwenImage_Config"
},
@@ -1220,6 +925,9 @@
{
"$ref": "#/components/schemas/VAE_Diffusers_Flux2_Config"
},
+ {
+ "$ref": "#/components/schemas/VAE_Diffusers_Wan_Config"
+ },
{
"$ref": "#/components/schemas/ControlNet_Checkpoint_SD1_Config"
},
@@ -1271,6 +979,9 @@
{
"$ref": "#/components/schemas/LoRA_LyCORIS_QwenImage_Config"
},
+ {
+ "$ref": "#/components/schemas/LoRA_LyCORIS_Wan_Config"
+ },
{
"$ref": "#/components/schemas/LoRA_LyCORIS_Anima_Config"
},
@@ -1322,6 +1033,356 @@
{
"$ref": "#/components/schemas/QwenVLEncoder_Checkpoint_Config"
},
+ {
+ "$ref": "#/components/schemas/WanT5Encoder_WanT5Encoder_Config"
+ },
+ {
+ "$ref": "#/components/schemas/TI_File_SD1_Config"
+ },
+ {
+ "$ref": "#/components/schemas/TI_File_SD2_Config"
+ },
+ {
+ "$ref": "#/components/schemas/TI_File_SDXL_Config"
+ },
+ {
+ "$ref": "#/components/schemas/TI_Folder_SD1_Config"
+ },
+ {
+ "$ref": "#/components/schemas/TI_Folder_SD2_Config"
+ },
+ {
+ "$ref": "#/components/schemas/TI_Folder_SDXL_Config"
+ },
+ {
+ "$ref": "#/components/schemas/IPAdapter_InvokeAI_SD1_Config"
+ },
+ {
+ "$ref": "#/components/schemas/IPAdapter_InvokeAI_SD2_Config"
+ },
+ {
+ "$ref": "#/components/schemas/IPAdapter_InvokeAI_SDXL_Config"
+ },
+ {
+ "$ref": "#/components/schemas/IPAdapter_Checkpoint_SD1_Config"
+ },
+ {
+ "$ref": "#/components/schemas/IPAdapter_Checkpoint_SD2_Config"
+ },
+ {
+ "$ref": "#/components/schemas/IPAdapter_Checkpoint_SDXL_Config"
+ },
+ {
+ "$ref": "#/components/schemas/IPAdapter_Checkpoint_FLUX_Config"
+ },
+ {
+ "$ref": "#/components/schemas/T2IAdapter_Diffusers_SD1_Config"
+ },
+ {
+ "$ref": "#/components/schemas/T2IAdapter_Diffusers_SDXL_Config"
+ },
+ {
+ "$ref": "#/components/schemas/Spandrel_Checkpoint_Config"
+ },
+ {
+ "$ref": "#/components/schemas/CLIPEmbed_Diffusers_G_Config"
+ },
+ {
+ "$ref": "#/components/schemas/CLIPEmbed_Diffusers_L_Config"
+ },
+ {
+ "$ref": "#/components/schemas/CLIPVision_Diffusers_Config"
+ },
+ {
+ "$ref": "#/components/schemas/SigLIP_Diffusers_Config"
+ },
+ {
+ "$ref": "#/components/schemas/FLUXRedux_Checkpoint_Config"
+ },
+ {
+ "$ref": "#/components/schemas/LlavaOnevision_Diffusers_Config"
+ },
+ {
+ "$ref": "#/components/schemas/TextLLM_Diffusers_Config"
+ },
+ {
+ "$ref": "#/components/schemas/ExternalApiModelConfig"
+ },
+ {
+ "$ref": "#/components/schemas/Unknown_Config"
+ }
+ ],
+ "title": "Response Get Model Records By Attrs"
+ }
+ }
+ }
+ },
+ "422": {
+ "description": "Validation Error",
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/HTTPValidationError"
+ }
+ }
+ }
+ }
+ }
+ }
+ },
+ "/api/v2/models/get_by_hash": {
+ "get": {
+ "tags": ["model_manager"],
+ "summary": "Get Model Records By Hash",
+ "description": "Gets a model by its hash. This is useful for recalling models that were deleted and reinstalled,\nas the hash remains stable across reinstallations while the key (UUID) changes.",
+ "operationId": "get_model_records_by_hash",
+ "security": [
+ {
+ "HTTPBearer": []
+ }
+ ],
+ "parameters": [
+ {
+ "name": "hash",
+ "in": "query",
+ "required": true,
+ "schema": {
+ "type": "string",
+ "description": "The hash of the model",
+ "title": "Hash"
+ },
+ "description": "The hash of the model"
+ }
+ ],
+ "responses": {
+ "200": {
+ "description": "Successful Response",
+ "content": {
+ "application/json": {
+ "schema": {
+ "oneOf": [
+ {
+ "$ref": "#/components/schemas/Main_Diffusers_SD1_Config"
+ },
+ {
+ "$ref": "#/components/schemas/Main_Diffusers_SD2_Config"
+ },
+ {
+ "$ref": "#/components/schemas/Main_Diffusers_SDXL_Config"
+ },
+ {
+ "$ref": "#/components/schemas/Main_Diffusers_SDXLRefiner_Config"
+ },
+ {
+ "$ref": "#/components/schemas/Main_Diffusers_SD3_Config"
+ },
+ {
+ "$ref": "#/components/schemas/Main_Diffusers_FLUX_Config"
+ },
+ {
+ "$ref": "#/components/schemas/Main_Diffusers_Flux2_Config"
+ },
+ {
+ "$ref": "#/components/schemas/Main_Diffusers_CogView4_Config"
+ },
+ {
+ "$ref": "#/components/schemas/Main_Diffusers_QwenImage_Config"
+ },
+ {
+ "$ref": "#/components/schemas/Main_Diffusers_Wan_Config"
+ },
+ {
+ "$ref": "#/components/schemas/Main_Diffusers_ZImage_Config"
+ },
+ {
+ "$ref": "#/components/schemas/Main_Checkpoint_SD1_Config"
+ },
+ {
+ "$ref": "#/components/schemas/Main_Checkpoint_SD2_Config"
+ },
+ {
+ "$ref": "#/components/schemas/Main_Checkpoint_SDXL_Config"
+ },
+ {
+ "$ref": "#/components/schemas/Main_Checkpoint_SDXLRefiner_Config"
+ },
+ {
+ "$ref": "#/components/schemas/Main_Checkpoint_Flux2_Config"
+ },
+ {
+ "$ref": "#/components/schemas/Main_Checkpoint_FLUX_Config"
+ },
+ {
+ "$ref": "#/components/schemas/Main_Checkpoint_QwenImage_Config"
+ },
+ {
+ "$ref": "#/components/schemas/Main_Checkpoint_ZImage_Config"
+ },
+ {
+ "$ref": "#/components/schemas/Main_Checkpoint_Anima_Config"
+ },
+ {
+ "$ref": "#/components/schemas/Main_BnBNF4_FLUX_Config"
+ },
+ {
+ "$ref": "#/components/schemas/Main_GGUF_Flux2_Config"
+ },
+ {
+ "$ref": "#/components/schemas/Main_GGUF_FLUX_Config"
+ },
+ {
+ "$ref": "#/components/schemas/Main_GGUF_QwenImage_Config"
+ },
+ {
+ "$ref": "#/components/schemas/Main_GGUF_Wan_Config"
+ },
+ {
+ "$ref": "#/components/schemas/Main_GGUF_ZImage_Config"
+ },
+ {
+ "$ref": "#/components/schemas/VAE_Checkpoint_SD1_Config"
+ },
+ {
+ "$ref": "#/components/schemas/VAE_Checkpoint_SD2_Config"
+ },
+ {
+ "$ref": "#/components/schemas/VAE_Checkpoint_SDXL_Config"
+ },
+ {
+ "$ref": "#/components/schemas/VAE_Checkpoint_FLUX_Config"
+ },
+ {
+ "$ref": "#/components/schemas/VAE_Checkpoint_Flux2_Config"
+ },
+ {
+ "$ref": "#/components/schemas/VAE_Checkpoint_Wan_Config"
+ },
+ {
+ "$ref": "#/components/schemas/VAE_Checkpoint_QwenImage_Config"
+ },
+ {
+ "$ref": "#/components/schemas/VAE_Checkpoint_Anima_Config"
+ },
+ {
+ "$ref": "#/components/schemas/VAE_Diffusers_SD1_Config"
+ },
+ {
+ "$ref": "#/components/schemas/VAE_Diffusers_SDXL_Config"
+ },
+ {
+ "$ref": "#/components/schemas/VAE_Diffusers_Flux2_Config"
+ },
+ {
+ "$ref": "#/components/schemas/VAE_Diffusers_Wan_Config"
+ },
+ {
+ "$ref": "#/components/schemas/ControlNet_Checkpoint_SD1_Config"
+ },
+ {
+ "$ref": "#/components/schemas/ControlNet_Checkpoint_SD2_Config"
+ },
+ {
+ "$ref": "#/components/schemas/ControlNet_Checkpoint_SDXL_Config"
+ },
+ {
+ "$ref": "#/components/schemas/ControlNet_Checkpoint_FLUX_Config"
+ },
+ {
+ "$ref": "#/components/schemas/ControlNet_Checkpoint_ZImage_Config"
+ },
+ {
+ "$ref": "#/components/schemas/ControlNet_Checkpoint_Anima_Config"
+ },
+ {
+ "$ref": "#/components/schemas/ControlNet_Diffusers_SD1_Config"
+ },
+ {
+ "$ref": "#/components/schemas/ControlNet_Diffusers_SD2_Config"
+ },
+ {
+ "$ref": "#/components/schemas/ControlNet_Diffusers_SDXL_Config"
+ },
+ {
+ "$ref": "#/components/schemas/ControlNet_Diffusers_FLUX_Config"
+ },
+ {
+ "$ref": "#/components/schemas/LoRA_LyCORIS_SD1_Config"
+ },
+ {
+ "$ref": "#/components/schemas/LoRA_LyCORIS_SD2_Config"
+ },
+ {
+ "$ref": "#/components/schemas/LoRA_LyCORIS_SDXL_Config"
+ },
+ {
+ "$ref": "#/components/schemas/LoRA_LyCORIS_Flux2_Config"
+ },
+ {
+ "$ref": "#/components/schemas/LoRA_LyCORIS_FLUX_Config"
+ },
+ {
+ "$ref": "#/components/schemas/LoRA_LyCORIS_ZImage_Config"
+ },
+ {
+ "$ref": "#/components/schemas/LoRA_LyCORIS_QwenImage_Config"
+ },
+ {
+ "$ref": "#/components/schemas/LoRA_LyCORIS_Wan_Config"
+ },
+ {
+ "$ref": "#/components/schemas/LoRA_LyCORIS_Anima_Config"
+ },
+ {
+ "$ref": "#/components/schemas/LoRA_OMI_SDXL_Config"
+ },
+ {
+ "$ref": "#/components/schemas/LoRA_OMI_FLUX_Config"
+ },
+ {
+ "$ref": "#/components/schemas/LoRA_Diffusers_SD1_Config"
+ },
+ {
+ "$ref": "#/components/schemas/LoRA_Diffusers_SD2_Config"
+ },
+ {
+ "$ref": "#/components/schemas/LoRA_Diffusers_SDXL_Config"
+ },
+ {
+ "$ref": "#/components/schemas/LoRA_Diffusers_Flux2_Config"
+ },
+ {
+ "$ref": "#/components/schemas/LoRA_Diffusers_FLUX_Config"
+ },
+ {
+ "$ref": "#/components/schemas/LoRA_Diffusers_ZImage_Config"
+ },
+ {
+ "$ref": "#/components/schemas/ControlLoRA_LyCORIS_FLUX_Config"
+ },
+ {
+ "$ref": "#/components/schemas/T5Encoder_T5Encoder_Config"
+ },
+ {
+ "$ref": "#/components/schemas/T5Encoder_BnBLLMint8_Config"
+ },
+ {
+ "$ref": "#/components/schemas/Qwen3Encoder_Qwen3Encoder_Config"
+ },
+ {
+ "$ref": "#/components/schemas/Qwen3Encoder_Checkpoint_Config"
+ },
+ {
+ "$ref": "#/components/schemas/Qwen3Encoder_GGUF_Config"
+ },
+ {
+ "$ref": "#/components/schemas/QwenVLEncoder_Diffusers_Config"
+ },
+ {
+ "$ref": "#/components/schemas/QwenVLEncoder_Checkpoint_Config"
+ },
+ {
+ "$ref": "#/components/schemas/WanT5Encoder_WanT5Encoder_Config"
+ },
{
"$ref": "#/components/schemas/TI_File_SD1_Config"
},
@@ -1474,6 +1535,9 @@
{
"$ref": "#/components/schemas/Main_Diffusers_QwenImage_Config"
},
+ {
+ "$ref": "#/components/schemas/Main_Diffusers_Wan_Config"
+ },
{
"$ref": "#/components/schemas/Main_Diffusers_ZImage_Config"
},
@@ -1516,6 +1580,9 @@
{
"$ref": "#/components/schemas/Main_GGUF_QwenImage_Config"
},
+ {
+ "$ref": "#/components/schemas/Main_GGUF_Wan_Config"
+ },
{
"$ref": "#/components/schemas/Main_GGUF_ZImage_Config"
},
@@ -1534,6 +1601,9 @@
{
"$ref": "#/components/schemas/VAE_Checkpoint_Flux2_Config"
},
+ {
+ "$ref": "#/components/schemas/VAE_Checkpoint_Wan_Config"
+ },
{
"$ref": "#/components/schemas/VAE_Checkpoint_QwenImage_Config"
},
@@ -1549,6 +1619,9 @@
{
"$ref": "#/components/schemas/VAE_Diffusers_Flux2_Config"
},
+ {
+ "$ref": "#/components/schemas/VAE_Diffusers_Wan_Config"
+ },
{
"$ref": "#/components/schemas/ControlNet_Checkpoint_SD1_Config"
},
@@ -1600,6 +1673,9 @@
{
"$ref": "#/components/schemas/LoRA_LyCORIS_QwenImage_Config"
},
+ {
+ "$ref": "#/components/schemas/LoRA_LyCORIS_Wan_Config"
+ },
{
"$ref": "#/components/schemas/LoRA_LyCORIS_Anima_Config"
},
@@ -1651,6 +1727,9 @@
{
"$ref": "#/components/schemas/QwenVLEncoder_Checkpoint_Config"
},
+ {
+ "$ref": "#/components/schemas/WanT5Encoder_WanT5Encoder_Config"
+ },
{
"$ref": "#/components/schemas/TI_File_SD1_Config"
},
@@ -1848,6 +1927,9 @@
{
"$ref": "#/components/schemas/Main_Diffusers_QwenImage_Config"
},
+ {
+ "$ref": "#/components/schemas/Main_Diffusers_Wan_Config"
+ },
{
"$ref": "#/components/schemas/Main_Diffusers_ZImage_Config"
},
@@ -1890,6 +1972,9 @@
{
"$ref": "#/components/schemas/Main_GGUF_QwenImage_Config"
},
+ {
+ "$ref": "#/components/schemas/Main_GGUF_Wan_Config"
+ },
{
"$ref": "#/components/schemas/Main_GGUF_ZImage_Config"
},
@@ -1908,6 +1993,9 @@
{
"$ref": "#/components/schemas/VAE_Checkpoint_Flux2_Config"
},
+ {
+ "$ref": "#/components/schemas/VAE_Checkpoint_Wan_Config"
+ },
{
"$ref": "#/components/schemas/VAE_Checkpoint_QwenImage_Config"
},
@@ -1923,6 +2011,9 @@
{
"$ref": "#/components/schemas/VAE_Diffusers_Flux2_Config"
},
+ {
+ "$ref": "#/components/schemas/VAE_Diffusers_Wan_Config"
+ },
{
"$ref": "#/components/schemas/ControlNet_Checkpoint_SD1_Config"
},
@@ -1974,6 +2065,9 @@
{
"$ref": "#/components/schemas/LoRA_LyCORIS_QwenImage_Config"
},
+ {
+ "$ref": "#/components/schemas/LoRA_LyCORIS_Wan_Config"
+ },
{
"$ref": "#/components/schemas/LoRA_LyCORIS_Anima_Config"
},
@@ -2025,6 +2119,9 @@
{
"$ref": "#/components/schemas/QwenVLEncoder_Checkpoint_Config"
},
+ {
+ "$ref": "#/components/schemas/WanT5Encoder_WanT5Encoder_Config"
+ },
{
"$ref": "#/components/schemas/TI_File_SD1_Config"
},
@@ -2246,6 +2343,9 @@
{
"$ref": "#/components/schemas/Main_Diffusers_QwenImage_Config"
},
+ {
+ "$ref": "#/components/schemas/Main_Diffusers_Wan_Config"
+ },
{
"$ref": "#/components/schemas/Main_Diffusers_ZImage_Config"
},
@@ -2288,6 +2388,9 @@
{
"$ref": "#/components/schemas/Main_GGUF_QwenImage_Config"
},
+ {
+ "$ref": "#/components/schemas/Main_GGUF_Wan_Config"
+ },
{
"$ref": "#/components/schemas/Main_GGUF_ZImage_Config"
},
@@ -2306,6 +2409,9 @@
{
"$ref": "#/components/schemas/VAE_Checkpoint_Flux2_Config"
},
+ {
+ "$ref": "#/components/schemas/VAE_Checkpoint_Wan_Config"
+ },
{
"$ref": "#/components/schemas/VAE_Checkpoint_QwenImage_Config"
},
@@ -2321,6 +2427,9 @@
{
"$ref": "#/components/schemas/VAE_Diffusers_Flux2_Config"
},
+ {
+ "$ref": "#/components/schemas/VAE_Diffusers_Wan_Config"
+ },
{
"$ref": "#/components/schemas/ControlNet_Checkpoint_SD1_Config"
},
@@ -2372,6 +2481,9 @@
{
"$ref": "#/components/schemas/LoRA_LyCORIS_QwenImage_Config"
},
+ {
+ "$ref": "#/components/schemas/LoRA_LyCORIS_Wan_Config"
+ },
{
"$ref": "#/components/schemas/LoRA_LyCORIS_Anima_Config"
},
@@ -2423,6 +2535,9 @@
{
"$ref": "#/components/schemas/QwenVLEncoder_Checkpoint_Config"
},
+ {
+ "$ref": "#/components/schemas/WanT5Encoder_WanT5Encoder_Config"
+ },
{
"$ref": "#/components/schemas/TI_File_SD1_Config"
},
@@ -3474,6 +3589,9 @@
{
"$ref": "#/components/schemas/Main_Diffusers_QwenImage_Config"
},
+ {
+ "$ref": "#/components/schemas/Main_Diffusers_Wan_Config"
+ },
{
"$ref": "#/components/schemas/Main_Diffusers_ZImage_Config"
},
@@ -3516,6 +3634,9 @@
{
"$ref": "#/components/schemas/Main_GGUF_QwenImage_Config"
},
+ {
+ "$ref": "#/components/schemas/Main_GGUF_Wan_Config"
+ },
{
"$ref": "#/components/schemas/Main_GGUF_ZImage_Config"
},
@@ -3534,6 +3655,9 @@
{
"$ref": "#/components/schemas/VAE_Checkpoint_Flux2_Config"
},
+ {
+ "$ref": "#/components/schemas/VAE_Checkpoint_Wan_Config"
+ },
{
"$ref": "#/components/schemas/VAE_Checkpoint_QwenImage_Config"
},
@@ -3549,6 +3673,9 @@
{
"$ref": "#/components/schemas/VAE_Diffusers_Flux2_Config"
},
+ {
+ "$ref": "#/components/schemas/VAE_Diffusers_Wan_Config"
+ },
{
"$ref": "#/components/schemas/ControlNet_Checkpoint_SD1_Config"
},
@@ -3600,6 +3727,9 @@
{
"$ref": "#/components/schemas/LoRA_LyCORIS_QwenImage_Config"
},
+ {
+ "$ref": "#/components/schemas/LoRA_LyCORIS_Wan_Config"
+ },
{
"$ref": "#/components/schemas/LoRA_LyCORIS_Anima_Config"
},
@@ -3651,6 +3781,9 @@
{
"$ref": "#/components/schemas/QwenVLEncoder_Checkpoint_Config"
},
+ {
+ "$ref": "#/components/schemas/WanT5Encoder_WanT5Encoder_Config"
+ },
{
"$ref": "#/components/schemas/TI_File_SD1_Config"
},
@@ -4966,8 +5099,13 @@
"head": {
"tags": ["images"],
"summary": "Get Image Full",
- "description": "Gets a full-resolution image file.\n\nThis endpoint is intentionally unauthenticated because browsers load images\nvia tags which cannot send Bearer tokens. Image names are UUIDs,\nproviding security through unguessability. Returns 409 while image storage\nmaintenance is active.",
+ "description": "Gets a full-resolution image file.\n\nBrowser media requests authenticate with the path-scoped HttpOnly cookie set at login.\nReturns 409 while image storage maintenance is active.",
"operationId": "get_image_full_head",
+ "security": [
+ {
+ "HTTPBearer": []
+ }
+ ],
"parameters": [
{
"name": "image_name",
@@ -4979,6 +5117,22 @@
"title": "Image Name"
},
"description": "The name of full-resolution image file to get"
+ },
+ {
+ "name": "invokeai_media_token",
+ "in": "cookie",
+ "required": false,
+ "schema": {
+ "anyOf": [
+ {
+ "type": "string"
+ },
+ {
+ "type": "null"
+ }
+ ],
+ "title": "Invokeai Media Token"
+ }
}
],
"responses": {
@@ -5006,8 +5160,13 @@
"get": {
"tags": ["images"],
"summary": "Get Image Full",
- "description": "Gets a full-resolution image file.\n\nThis endpoint is intentionally unauthenticated because browsers load images\nvia tags which cannot send Bearer tokens. Image names are UUIDs,\nproviding security through unguessability. Returns 409 while image storage\nmaintenance is active.",
+ "description": "Gets a full-resolution image file.\n\nBrowser media requests authenticate with the path-scoped HttpOnly cookie set at login.\nReturns 409 while image storage maintenance is active.",
"operationId": "get_image_full",
+ "security": [
+ {
+ "HTTPBearer": []
+ }
+ ],
"parameters": [
{
"name": "image_name",
@@ -5019,6 +5178,22 @@
"title": "Image Name"
},
"description": "The name of full-resolution image file to get"
+ },
+ {
+ "name": "invokeai_media_token",
+ "in": "cookie",
+ "required": false,
+ "schema": {
+ "anyOf": [
+ {
+ "type": "string"
+ },
+ {
+ "type": "null"
+ }
+ ],
+ "title": "Invokeai Media Token"
+ }
}
],
"responses": {
@@ -5048,8 +5223,13 @@
"get": {
"tags": ["images"],
"summary": "Get Image Thumbnail",
- "description": "Gets a thumbnail image file.\n\nThis endpoint is intentionally unauthenticated because browsers load images\nvia tags which cannot send Bearer tokens. Image names are UUIDs,\nproviding security through unguessability. Returns 409 while image storage\nmaintenance is active.",
+ "description": "Gets a thumbnail image file.\n\nBrowser media requests authenticate with the path-scoped HttpOnly cookie set at login.\nReturns 409 while image storage maintenance is active.",
"operationId": "get_image_thumbnail",
+ "security": [
+ {
+ "HTTPBearer": []
+ }
+ ],
"parameters": [
{
"name": "image_name",
@@ -5061,6 +5241,22 @@
"title": "Image Name"
},
"description": "The name of thumbnail image file to get"
+ },
+ {
+ "name": "invokeai_media_token",
+ "in": "cookie",
+ "required": false,
+ "schema": {
+ "anyOf": [
+ {
+ "type": "string"
+ },
+ {
+ "type": "null"
+ }
+ ],
+ "title": "Invokeai Media Token"
+ }
}
],
"responses": {
@@ -5583,12 +5779,12 @@
]
}
},
- "/api/v1/boards/": {
+ "/api/v1/videos/upload": {
"post": {
- "tags": ["boards"],
- "summary": "Create Board",
- "description": "Creates a board for the current user",
- "operationId": "create_board",
+ "tags": ["videos"],
+ "summary": "Upload Video",
+ "description": "Uploads a video for the current user.",
+ "operationId": "upload_video",
"security": [
{
"HTTPBearer": []
@@ -5596,163 +5792,87 @@
],
"parameters": [
{
- "name": "board_name",
+ "name": "video_category",
"in": "query",
"required": true,
"schema": {
- "type": "string",
- "maxLength": 300,
- "description": "The name of the board to create",
- "title": "Board Name"
- },
- "description": "The name of the board to create"
- }
- ],
- "responses": {
- "201": {
- "description": "The board was created successfully",
- "content": {
- "application/json": {
- "schema": {
- "$ref": "#/components/schemas/BoardDTO"
- }
- }
- }
- },
- "422": {
- "description": "Validation Error",
- "content": {
- "application/json": {
- "schema": {
- "$ref": "#/components/schemas/HTTPValidationError"
- }
- }
- }
- }
- }
- },
- "get": {
- "tags": ["boards"],
- "summary": "List Boards",
- "description": "Gets a list of boards for the current user, including shared boards. Admin users see all boards.",
- "operationId": "list_boards",
- "security": [
- {
- "HTTPBearer": []
- }
- ],
- "parameters": [
- {
- "name": "order_by",
- "in": "query",
- "required": false,
- "schema": {
- "$ref": "#/components/schemas/BoardRecordOrderBy",
- "description": "The attribute to order by",
- "default": "created_at"
- },
- "description": "The attribute to order by"
- },
- {
- "name": "direction",
- "in": "query",
- "required": false,
- "schema": {
- "$ref": "#/components/schemas/SQLiteDirection",
- "description": "The direction to order by",
- "default": "DESC"
+ "$ref": "#/components/schemas/ImageCategory",
+ "description": "The category of the video"
},
- "description": "The direction to order by"
+ "description": "The category of the video"
},
{
- "name": "all",
+ "name": "is_intermediate",
"in": "query",
- "required": false,
+ "required": true,
"schema": {
- "anyOf": [
- {
- "type": "boolean"
- },
- {
- "type": "null"
- }
- ],
- "description": "Whether to list all boards",
- "title": "All"
+ "type": "boolean",
+ "description": "Whether this is an intermediate video",
+ "title": "Is Intermediate"
},
- "description": "Whether to list all boards"
+ "description": "Whether this is an intermediate video"
},
{
- "name": "offset",
+ "name": "board_id",
"in": "query",
"required": false,
"schema": {
"anyOf": [
{
- "type": "integer"
+ "type": "string"
},
{
"type": "null"
}
],
- "description": "The page offset",
- "title": "Offset"
+ "description": "The board to add this video to, if any",
+ "title": "Board Id"
},
- "description": "The page offset"
+ "description": "The board to add this video to, if any"
},
{
- "name": "limit",
+ "name": "session_id",
"in": "query",
"required": false,
"schema": {
"anyOf": [
{
- "type": "integer"
+ "type": "string"
},
{
"type": "null"
}
],
- "description": "The number of boards per page",
- "title": "Limit"
- },
- "description": "The number of boards per page"
- },
- {
- "name": "include_archived",
- "in": "query",
- "required": false,
- "schema": {
- "type": "boolean",
- "description": "Whether or not to include archived boards in list",
- "default": false,
- "title": "Include Archived"
+ "description": "The session ID associated with this upload, if any",
+ "title": "Session Id"
},
- "description": "Whether or not to include archived boards in list"
+ "description": "The session ID associated with this upload, if any"
}
],
+ "requestBody": {
+ "required": true,
+ "content": {
+ "multipart/form-data": {
+ "schema": {
+ "$ref": "#/components/schemas/Body_upload_video"
+ }
+ }
+ }
+ },
"responses": {
- "200": {
- "description": "Successful Response",
+ "201": {
+ "description": "The video was uploaded successfully",
"content": {
"application/json": {
"schema": {
- "anyOf": [
- {
- "$ref": "#/components/schemas/OffsetPaginatedResults_BoardDTO_"
- },
- {
- "type": "array",
- "items": {
- "$ref": "#/components/schemas/BoardDTO"
- }
- }
- ],
- "title": "Response List Boards"
+ "$ref": "#/components/schemas/VideoDTO"
}
}
}
},
+ "415": {
+ "description": "Video upload failed"
+ },
"422": {
"description": "Validation Error",
"content": {
@@ -5766,12 +5886,11 @@
}
}
},
- "/api/v1/boards/{board_id}": {
- "get": {
- "tags": ["boards"],
- "summary": "Get Board",
- "description": "Gets a board (user must have access to it)",
- "operationId": "get_board",
+ "/api/v1/videos/i/{video_name}": {
+ "delete": {
+ "tags": ["videos"],
+ "summary": "Delete Video",
+ "operationId": "delete_video",
"security": [
{
"HTTPBearer": []
@@ -5779,15 +5898,15 @@
],
"parameters": [
{
- "name": "board_id",
+ "name": "video_name",
"in": "path",
"required": true,
"schema": {
"type": "string",
- "description": "The id of board to get",
- "title": "Board Id"
+ "description": "The name of the video to delete",
+ "title": "Video Name"
},
- "description": "The id of board to get"
+ "description": "The name of the video to delete"
}
],
"responses": {
@@ -5796,7 +5915,7 @@
"content": {
"application/json": {
"schema": {
- "$ref": "#/components/schemas/BoardDTO"
+ "$ref": "#/components/schemas/DeleteVideosResult"
}
}
}
@@ -5814,10 +5933,9 @@
}
},
"patch": {
- "tags": ["boards"],
- "summary": "Update Board",
- "description": "Updates a board (user must have access to it)",
- "operationId": "update_board",
+ "tags": ["videos"],
+ "summary": "Update Video",
+ "operationId": "update_video",
"security": [
{
"HTTPBearer": []
@@ -5825,15 +5943,15 @@
],
"parameters": [
{
- "name": "board_id",
+ "name": "video_name",
"in": "path",
"required": true,
"schema": {
"type": "string",
- "description": "The id of board to update",
- "title": "Board Id"
+ "description": "The name of the video to update",
+ "title": "Video Name"
},
- "description": "The id of board to update"
+ "description": "The name of the video to update"
}
],
"requestBody": {
@@ -5841,84 +5959,19 @@
"content": {
"application/json": {
"schema": {
- "$ref": "#/components/schemas/BoardChanges",
- "description": "The changes to apply to the board"
+ "$ref": "#/components/schemas/VideoRecordChanges",
+ "description": "The changes to apply to the video"
}
}
}
},
- "responses": {
- "201": {
- "description": "The board was updated successfully",
- "content": {
- "application/json": {
- "schema": {
- "$ref": "#/components/schemas/BoardDTO"
- }
- }
- }
- },
- "422": {
- "description": "Validation Error",
- "content": {
- "application/json": {
- "schema": {
- "$ref": "#/components/schemas/HTTPValidationError"
- }
- }
- }
- }
- }
- },
- "delete": {
- "tags": ["boards"],
- "summary": "Delete Board",
- "description": "Deletes a board (user must have access to it)",
- "operationId": "delete_board",
- "security": [
- {
- "HTTPBearer": []
- }
- ],
- "parameters": [
- {
- "name": "board_id",
- "in": "path",
- "required": true,
- "schema": {
- "type": "string",
- "description": "The id of board to delete",
- "title": "Board Id"
- },
- "description": "The id of board to delete"
- },
- {
- "name": "include_images",
- "in": "query",
- "required": false,
- "schema": {
- "anyOf": [
- {
- "type": "boolean"
- },
- {
- "type": "null"
- }
- ],
- "description": "Permanently delete all images on the board",
- "default": false,
- "title": "Include Images"
- },
- "description": "Permanently delete all images on the board"
- }
- ],
"responses": {
"200": {
"description": "Successful Response",
"content": {
"application/json": {
"schema": {
- "$ref": "#/components/schemas/DeleteBoardResult"
+ "$ref": "#/components/schemas/VideoDTO"
}
}
}
@@ -5934,14 +5987,11 @@
}
}
}
- }
- },
- "/api/v1/boards/{board_id}/image_names": {
+ },
"get": {
- "tags": ["boards"],
- "summary": "List All Board Image Names",
- "description": "Gets a list of images for a board",
- "operationId": "list_all_board_image_names",
+ "tags": ["videos"],
+ "summary": "Get Video Dto",
+ "operationId": "get_video_dto",
"security": [
{
"HTTPBearer": []
@@ -5949,54 +5999,15 @@
],
"parameters": [
{
- "name": "board_id",
+ "name": "video_name",
"in": "path",
"required": true,
"schema": {
"type": "string",
- "description": "The id of the board or 'none' for uncategorized images",
- "title": "Board Id"
- },
- "description": "The id of the board or 'none' for uncategorized images"
- },
- {
- "name": "categories",
- "in": "query",
- "required": false,
- "schema": {
- "anyOf": [
- {
- "type": "array",
- "items": {
- "$ref": "#/components/schemas/ImageCategory"
- }
- },
- {
- "type": "null"
- }
- ],
- "description": "The categories of image to include.",
- "title": "Categories"
- },
- "description": "The categories of image to include."
- },
- {
- "name": "is_intermediate",
- "in": "query",
- "required": false,
- "schema": {
- "anyOf": [
- {
- "type": "boolean"
- },
- {
- "type": "null"
- }
- ],
- "description": "Whether to list intermediate images.",
- "title": "Is Intermediate"
+ "description": "The name of video to get",
+ "title": "Video Name"
},
- "description": "Whether to list intermediate images."
+ "description": "The name of video to get"
}
],
"responses": {
@@ -6005,11 +6016,7 @@
"content": {
"application/json": {
"schema": {
- "type": "array",
- "items": {
- "type": "string"
- },
- "title": "Response List All Board Image Names"
+ "$ref": "#/components/schemas/VideoDTO"
}
}
}
@@ -6027,29 +6034,28 @@
}
}
},
- "/api/v1/board_images/": {
+ "/api/v1/videos/delete": {
"post": {
- "tags": ["boards"],
- "summary": "Add Image To Board",
- "description": "Creates a board_image",
- "operationId": "add_image_to_board",
+ "tags": ["videos"],
+ "summary": "Delete Videos From List",
+ "operationId": "delete_videos_from_list",
"requestBody": {
"content": {
"application/json": {
"schema": {
- "$ref": "#/components/schemas/Body_add_image_to_board"
+ "$ref": "#/components/schemas/Body_delete_videos_from_list"
}
}
},
"required": true
},
"responses": {
- "201": {
- "description": "The image was added to a board successfully",
+ "200": {
+ "description": "Successful Response",
"content": {
"application/json": {
"schema": {
- "$ref": "#/components/schemas/AddImagesToBoardResult"
+ "$ref": "#/components/schemas/DeleteVideosResult"
}
}
}
@@ -6070,39 +6076,21 @@
"HTTPBearer": []
}
]
- },
+ }
+ },
+ "/api/v1/videos/uncategorized": {
"delete": {
- "tags": ["boards"],
- "summary": "Remove Image From Board",
- "description": "Removes an image from its board, if it had one",
- "operationId": "remove_image_from_board",
- "requestBody": {
- "content": {
- "application/json": {
- "schema": {
- "$ref": "#/components/schemas/Body_remove_image_from_board"
- }
- }
- },
- "required": true
- },
+ "tags": ["videos"],
+ "summary": "Delete Uncategorized Videos",
+ "description": "Deletes all uncategorized videos owned by the current user (or all if admin).\n\nMirrors ``delete_uncategorized_images`` so the \"Delete All Uncategorized\nImages/Videos\" board action covers both media kinds.",
+ "operationId": "delete_uncategorized_videos",
"responses": {
- "201": {
- "description": "The image was removed from the board successfully",
- "content": {
- "application/json": {
- "schema": {
- "$ref": "#/components/schemas/RemoveImagesFromBoardResult"
- }
- }
- }
- },
- "422": {
- "description": "Validation Error",
+ "200": {
+ "description": "Successful Response",
"content": {
"application/json": {
"schema": {
- "$ref": "#/components/schemas/HTTPValidationError"
+ "$ref": "#/components/schemas/DeleteVideosResult"
}
}
}
@@ -6115,29 +6103,44 @@
]
}
},
- "/api/v1/board_images/batch": {
- "post": {
- "tags": ["boards"],
- "summary": "Add Images To Board",
- "description": "Adds a list of images to a board",
- "operationId": "add_images_to_board",
- "requestBody": {
- "content": {
- "application/json": {
- "schema": {
- "$ref": "#/components/schemas/Body_add_images_to_board"
- }
- }
- },
- "required": true
- },
+ "/api/v1/videos/i/{video_name}/metadata": {
+ "get": {
+ "tags": ["videos"],
+ "summary": "Get Video Metadata",
+ "operationId": "get_video_metadata",
+ "security": [
+ {
+ "HTTPBearer": []
+ }
+ ],
+ "parameters": [
+ {
+ "name": "video_name",
+ "in": "path",
+ "required": true,
+ "schema": {
+ "type": "string",
+ "description": "The name of video to get",
+ "title": "Video Name"
+ },
+ "description": "The name of video to get"
+ }
+ ],
"responses": {
- "201": {
- "description": "Images were added to board successfully",
+ "200": {
+ "description": "Successful Response",
"content": {
"application/json": {
"schema": {
- "$ref": "#/components/schemas/AddImagesToBoardResult"
+ "anyOf": [
+ {
+ "$ref": "#/components/schemas/MetadataField"
+ },
+ {
+ "type": "null"
+ }
+ ],
+ "title": "Response Get Video Metadata"
}
}
}
@@ -6152,37 +6155,40 @@
}
}
}
- },
+ }
+ }
+ },
+ "/api/v1/videos/i/{video_name}/workflow": {
+ "get": {
+ "tags": ["videos"],
+ "summary": "Get Video Workflow",
+ "description": "Gets the workflow and graph saved with a generated video (mirrors the image route).",
+ "operationId": "get_video_workflow",
"security": [
{
"HTTPBearer": []
}
- ]
- }
- },
- "/api/v1/board_images/batch/delete": {
- "post": {
- "tags": ["boards"],
- "summary": "Remove Images From Board",
- "description": "Removes a list of images from their board, if they had one",
- "operationId": "remove_images_from_board",
- "requestBody": {
- "content": {
- "application/json": {
- "schema": {
- "$ref": "#/components/schemas/Body_remove_images_from_board"
- }
- }
- },
- "required": true
- },
+ ],
+ "parameters": [
+ {
+ "name": "video_name",
+ "in": "path",
+ "required": true,
+ "schema": {
+ "type": "string",
+ "description": "The name of video whose workflow to get",
+ "title": "Video Name"
+ },
+ "description": "The name of video whose workflow to get"
+ }
+ ],
"responses": {
- "201": {
- "description": "Images were removed from board successfully",
+ "200": {
+ "description": "Successful Response",
"content": {
"application/json": {
"schema": {
- "$ref": "#/components/schemas/RemoveImagesFromBoardResult"
+ "$ref": "#/components/schemas/WorkflowAndGraphResponse"
}
}
}
@@ -6197,49 +6203,145 @@
}
}
}
- },
+ }
+ }
+ },
+ "/api/v1/videos/i/{video_name}/full": {
+ "head": {
+ "tags": ["videos"],
+ "summary": "Get Video Full",
+ "description": "Serves the video file with HTTP Range support so HTML5 seek/scrub works.\n\nBrowser media requests authenticate with the path-scoped HttpOnly cookie set at login.",
+ "operationId": "get_video_full_head",
"security": [
{
"HTTPBearer": []
}
- ]
- }
- },
- "/api/v1/virtual_boards/by_date": {
- "get": {
- "tags": ["virtual_boards"],
- "summary": "List Virtual Boards By Date",
- "description": "Gets a list of virtual sub-boards grouped by date.",
- "operationId": "list_virtual_boards_by_date",
- "responses": {
- "200": {
- "description": "Successful Response",
- "content": {
- "application/json": {
- "schema": {
- "items": {
- "$ref": "#/components/schemas/VirtualSubBoardDTO"
- },
- "type": "array",
- "title": "Response List Virtual Boards By Date"
+ ],
+ "parameters": [
+ {
+ "name": "video_name",
+ "in": "path",
+ "required": true,
+ "schema": {
+ "type": "string",
+ "description": "The name of video file to get",
+ "title": "Video Name"
+ },
+ "description": "The name of video file to get"
+ },
+ {
+ "name": "invokeai_media_token",
+ "in": "cookie",
+ "required": false,
+ "schema": {
+ "anyOf": [
+ {
+ "type": "string"
+ },
+ {
+ "type": "null"
+ }
+ ],
+ "title": "Invokeai Media Token"
+ }
+ }
+ ],
+ "responses": {
+ "200": {
+ "description": "Return the full video file",
+ "content": {
+ "video/mp4": {}
+ }
+ },
+ "404": {
+ "description": "Video not found"
+ },
+ "422": {
+ "description": "Validation Error",
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/HTTPValidationError"
}
}
}
}
- },
+ }
+ },
+ "get": {
+ "tags": ["videos"],
+ "summary": "Get Video Full",
+ "description": "Serves the video file with HTTP Range support so HTML5 seek/scrub works.\n\nBrowser media requests authenticate with the path-scoped HttpOnly cookie set at login.",
+ "operationId": "get_video_full",
"security": [
{
"HTTPBearer": []
}
- ]
+ ],
+ "parameters": [
+ {
+ "name": "video_name",
+ "in": "path",
+ "required": true,
+ "schema": {
+ "type": "string",
+ "description": "The name of video file to get",
+ "title": "Video Name"
+ },
+ "description": "The name of video file to get"
+ },
+ {
+ "name": "invokeai_media_token",
+ "in": "cookie",
+ "required": false,
+ "schema": {
+ "anyOf": [
+ {
+ "type": "string"
+ },
+ {
+ "type": "null"
+ }
+ ],
+ "title": "Invokeai Media Token"
+ }
+ }
+ ],
+ "responses": {
+ "200": {
+ "description": "Return the full video file",
+ "content": {
+ "video/mp4": {}
+ }
+ },
+ "206": {
+ "description": "Return a byte-range of the video file",
+ "content": {
+ "video/mp4": {}
+ }
+ },
+ "404": {
+ "description": "Video not found"
+ },
+ "422": {
+ "description": "Validation Error",
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/HTTPValidationError"
+ }
+ }
+ }
+ }
+ }
}
},
- "/api/v1/virtual_boards/by_date/{date}/image_names": {
+ "/api/v1/videos/i/{video_name}/thumbnail": {
"get": {
- "tags": ["virtual_boards"],
- "summary": "List Virtual Board Image Names By Date",
- "description": "Gets ordered image names for a specific date.",
- "operationId": "list_virtual_board_image_names_by_date",
+ "tags": ["videos"],
+ "summary": "Get Video Thumbnail",
+ "description": "Returns the first-frame WebP thumbnail of an authorized video.",
+ "operationId": "get_video_thumbnail",
"security": [
{
"HTTPBearer": []
@@ -6247,38 +6349,132 @@
],
"parameters": [
{
- "name": "date",
+ "name": "video_name",
"in": "path",
"required": true,
"schema": {
"type": "string",
- "description": "The ISO date string, e.g. '2026-03-18'",
- "title": "Date"
+ "description": "The name of thumbnail file to get",
+ "title": "Video Name"
},
- "description": "The ISO date string, e.g. '2026-03-18'"
+ "description": "The name of thumbnail file to get"
},
{
- "name": "starred_first",
- "in": "query",
+ "name": "invokeai_media_token",
+ "in": "cookie",
"required": false,
"schema": {
- "type": "boolean",
- "description": "Whether to sort starred images first",
- "default": true,
- "title": "Starred First"
+ "anyOf": [
+ {
+ "type": "string"
+ },
+ {
+ "type": "null"
+ }
+ ],
+ "title": "Invokeai Media Token"
+ }
+ }
+ ],
+ "responses": {
+ "200": {
+ "description": "Return the video thumbnail",
+ "content": {
+ "image/webp": {}
+ }
+ },
+ "404": {
+ "description": "Video not found"
+ },
+ "422": {
+ "description": "Validation Error",
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/HTTPValidationError"
+ }
+ }
+ }
+ }
+ }
+ }
+ },
+ "/api/v1/videos/i/{video_name}/urls": {
+ "get": {
+ "tags": ["videos"],
+ "summary": "Get Video Urls",
+ "operationId": "get_video_urls",
+ "security": [
+ {
+ "HTTPBearer": []
+ }
+ ],
+ "parameters": [
+ {
+ "name": "video_name",
+ "in": "path",
+ "required": true,
+ "schema": {
+ "type": "string",
+ "description": "The name of the video whose URL to get",
+ "title": "Video Name"
},
- "description": "Whether to sort starred images first"
+ "description": "The name of the video whose URL to get"
+ }
+ ],
+ "responses": {
+ "200": {
+ "description": "Successful Response",
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/VideoUrlsDTO"
+ }
+ }
+ }
},
+ "422": {
+ "description": "Validation Error",
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/HTTPValidationError"
+ }
+ }
+ }
+ }
+ }
+ }
+ },
+ "/api/v1/videos/": {
+ "get": {
+ "tags": ["videos"],
+ "summary": "List Video Dtos",
+ "description": "Gets a list of video DTOs for the current user.",
+ "operationId": "list_video_dtos",
+ "security": [
{
- "name": "order_dir",
+ "HTTPBearer": []
+ }
+ ],
+ "parameters": [
+ {
+ "name": "video_origin",
"in": "query",
"required": false,
"schema": {
- "$ref": "#/components/schemas/SQLiteDirection",
- "description": "The sort direction",
- "default": "DESC"
+ "anyOf": [
+ {
+ "$ref": "#/components/schemas/ResourceOrigin"
+ },
+ {
+ "type": "null"
+ }
+ ],
+ "description": "The origin of videos to list.",
+ "title": "Video Origin"
},
- "description": "The sort direction"
+ "description": "The origin of videos to list."
},
{
"name": "categories",
@@ -6296,10 +6492,96 @@
"type": "null"
}
],
- "description": "The categories of images to include",
+ "description": "The categories of video to include.",
"title": "Categories"
},
- "description": "The categories of images to include"
+ "description": "The categories of video to include."
+ },
+ {
+ "name": "is_intermediate",
+ "in": "query",
+ "required": false,
+ "schema": {
+ "anyOf": [
+ {
+ "type": "boolean"
+ },
+ {
+ "type": "null"
+ }
+ ],
+ "description": "Whether to list intermediate videos.",
+ "title": "Is Intermediate"
+ },
+ "description": "Whether to list intermediate videos."
+ },
+ {
+ "name": "board_id",
+ "in": "query",
+ "required": false,
+ "schema": {
+ "anyOf": [
+ {
+ "type": "string"
+ },
+ {
+ "type": "null"
+ }
+ ],
+ "description": "The board id to filter by. Use 'none' to find videos without a board.",
+ "title": "Board Id"
+ },
+ "description": "The board id to filter by. Use 'none' to find videos without a board."
+ },
+ {
+ "name": "offset",
+ "in": "query",
+ "required": false,
+ "schema": {
+ "type": "integer",
+ "minimum": 0,
+ "description": "The page offset",
+ "default": 0,
+ "title": "Offset"
+ },
+ "description": "The page offset"
+ },
+ {
+ "name": "limit",
+ "in": "query",
+ "required": false,
+ "schema": {
+ "type": "integer",
+ "maximum": 1000,
+ "minimum": 0,
+ "description": "The number of videos per page",
+ "default": 10,
+ "title": "Limit"
+ },
+ "description": "The number of videos per page"
+ },
+ {
+ "name": "order_dir",
+ "in": "query",
+ "required": false,
+ "schema": {
+ "$ref": "#/components/schemas/SQLiteDirection",
+ "description": "The order of sort",
+ "default": "DESC"
+ },
+ "description": "The order of sort"
+ },
+ {
+ "name": "starred_first",
+ "in": "query",
+ "required": false,
+ "schema": {
+ "type": "boolean",
+ "description": "Whether to sort by starred videos first",
+ "default": true,
+ "title": "Starred First"
+ },
+ "description": "Whether to sort by starred videos first"
},
{
"name": "search_term",
@@ -6314,10 +6596,10 @@
"type": "null"
}
],
- "description": "Search term to filter images",
+ "description": "The term to search for",
"title": "Search Term"
},
- "description": "Search term to filter images"
+ "description": "The term to search for"
}
],
"responses": {
@@ -6326,7 +6608,7 @@
"content": {
"application/json": {
"schema": {
- "$ref": "#/components/schemas/ImageNamesResult"
+ "$ref": "#/components/schemas/OffsetPaginatedResults_VideoDTO_"
}
}
}
@@ -6344,12 +6626,12 @@
}
}
},
- "/api/v1/model_relationships/i/{model_key}": {
+ "/api/v1/videos/names": {
"get": {
- "tags": ["model_relationships"],
- "summary": "Get Related Models",
- "description": "Get a list of model keys related to a given model.",
- "operationId": "get_related_models",
+ "tags": ["videos"],
+ "summary": "Get Video Names",
+ "description": "Gets ordered list of video names with metadata for optimistic updates.",
+ "operationId": "get_video_names",
"security": [
{
"HTTPBearer": []
@@ -6357,78 +6639,181 @@
],
"parameters": [
{
- "name": "model_key",
- "in": "path",
- "required": true,
+ "name": "video_origin",
+ "in": "query",
+ "required": false,
"schema": {
- "type": "string",
- "description": "The key of the model to get relationships for",
- "title": "Model Key"
+ "anyOf": [
+ {
+ "$ref": "#/components/schemas/ResourceOrigin"
+ },
+ {
+ "type": "null"
+ }
+ ],
+ "description": "The origin of videos to list.",
+ "title": "Video Origin"
},
- "description": "The key of the model to get relationships for"
+ "description": "The origin of videos to list."
+ },
+ {
+ "name": "categories",
+ "in": "query",
+ "required": false,
+ "schema": {
+ "anyOf": [
+ {
+ "type": "array",
+ "items": {
+ "$ref": "#/components/schemas/ImageCategory"
+ }
+ },
+ {
+ "type": "null"
+ }
+ ],
+ "description": "The categories of video to include.",
+ "title": "Categories"
+ },
+ "description": "The categories of video to include."
+ },
+ {
+ "name": "is_intermediate",
+ "in": "query",
+ "required": false,
+ "schema": {
+ "anyOf": [
+ {
+ "type": "boolean"
+ },
+ {
+ "type": "null"
+ }
+ ],
+ "description": "Whether to list intermediate videos.",
+ "title": "Is Intermediate"
+ },
+ "description": "Whether to list intermediate videos."
+ },
+ {
+ "name": "board_id",
+ "in": "query",
+ "required": false,
+ "schema": {
+ "anyOf": [
+ {
+ "type": "string"
+ },
+ {
+ "type": "null"
+ }
+ ],
+ "description": "The board id to filter by. Use 'none' to find videos without a board.",
+ "title": "Board Id"
+ },
+ "description": "The board id to filter by. Use 'none' to find videos without a board."
+ },
+ {
+ "name": "order_dir",
+ "in": "query",
+ "required": false,
+ "schema": {
+ "$ref": "#/components/schemas/SQLiteDirection",
+ "description": "The order of sort",
+ "default": "DESC"
+ },
+ "description": "The order of sort"
+ },
+ {
+ "name": "starred_first",
+ "in": "query",
+ "required": false,
+ "schema": {
+ "type": "boolean",
+ "description": "Whether to sort by starred videos first",
+ "default": true,
+ "title": "Starred First"
+ },
+ "description": "Whether to sort by starred videos first"
+ },
+ {
+ "name": "search_term",
+ "in": "query",
+ "required": false,
+ "schema": {
+ "anyOf": [
+ {
+ "type": "string"
+ },
+ {
+ "type": "null"
+ }
+ ],
+ "description": "The term to search for",
+ "title": "Search Term"
+ },
+ "description": "The term to search for"
}
],
"responses": {
"200": {
- "description": "A list of related model keys was retrieved successfully",
+ "description": "Successful Response",
"content": {
"application/json": {
"schema": {
- "type": "array",
- "items": {
- "type": "string"
- },
- "title": "Response Get Related Models"
- },
- "example": [
- "15e9eb28-8cfe-47c9-b610-37907a79fc3c",
- "71272e82-0e5f-46d5-bca9-9a61f4bd8a82",
- "a5d7cd49-1b98-4534-a475-aeee4ccf5fa2"
- ]
+ "$ref": "#/components/schemas/VideoNamesResult"
+ }
}
}
},
- "404": {
- "description": "The specified model could not be found"
- },
"422": {
- "description": "Validation error"
+ "description": "Validation Error",
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/HTTPValidationError"
+ }
+ }
+ }
}
}
}
},
- "/api/v1/model_relationships/": {
+ "/api/v1/videos/star": {
"post": {
- "tags": ["model_relationships"],
- "summary": "Add Model Relationship",
- "description": "Creates a **bidirectional** relationship between two models, allowing each to reference the other as related.",
- "operationId": "add_model_relationship_api_v1_model_relationships__post",
+ "tags": ["videos"],
+ "summary": "Star Videos In List",
+ "operationId": "star_videos_in_list",
"requestBody": {
"content": {
"application/json": {
"schema": {
- "$ref": "#/components/schemas/ModelRelationshipCreateRequest",
- "description": "The model keys to relate"
+ "$ref": "#/components/schemas/Body_star_videos_in_list"
}
}
},
"required": true
},
"responses": {
- "204": {
- "description": "The relationship was successfully created"
- },
- "400": {
- "description": "Invalid model keys or self-referential relationship"
- },
- "409": {
- "description": "The relationship already exists"
+ "200": {
+ "description": "Successful Response",
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/StarredVideosResult"
+ }
+ }
+ }
},
"422": {
- "description": "Validation error"
- },
- "500": {
- "description": "Internal server error"
+ "description": "Validation Error",
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/HTTPValidationError"
+ }
+ }
+ }
}
},
"security": [
@@ -6436,38 +6821,43 @@
"HTTPBearer": []
}
]
- },
- "delete": {
- "tags": ["model_relationships"],
- "summary": "Remove Model Relationship",
- "description": "Removes a **bidirectional** relationship between two models. The relationship must already exist.",
- "operationId": "remove_model_relationship_api_v1_model_relationships__delete",
+ }
+ },
+ "/api/v1/videos/unstar": {
+ "post": {
+ "tags": ["videos"],
+ "summary": "Unstar Videos In List",
+ "operationId": "unstar_videos_in_list",
"requestBody": {
"content": {
"application/json": {
"schema": {
- "$ref": "#/components/schemas/ModelRelationshipCreateRequest",
- "description": "The model keys to disconnect"
+ "$ref": "#/components/schemas/Body_unstar_videos_in_list"
}
}
},
"required": true
},
"responses": {
- "204": {
- "description": "The relationship was successfully removed"
- },
- "400": {
- "description": "Invalid model keys or self-referential relationship"
- },
- "404": {
- "description": "The relationship does not exist"
+ "200": {
+ "description": "Successful Response",
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/UnstarredVideosResult"
+ }
+ }
+ }
},
"422": {
- "description": "Validation error"
- },
- "500": {
- "description": "Internal server error"
+ "description": "Validation Error",
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/HTTPValidationError"
+ }
+ }
+ }
}
},
"security": [
@@ -6477,18 +6867,16 @@
]
}
},
- "/api/v1/model_relationships/batch": {
+ "/api/v1/videos/board": {
"post": {
- "tags": ["model_relationships"],
- "summary": "Get Related Model Keys (Batch)",
- "description": "Retrieves all **unique related model keys** for a list of given models. This is useful for contextual suggestions or filtering.",
- "operationId": "get_related_models_batch",
+ "tags": ["videos"],
+ "summary": "Add Video To Board",
+ "operationId": "add_video_to_board",
"requestBody": {
"content": {
"application/json": {
"schema": {
- "$ref": "#/components/schemas/ModelRelationshipBatchRequest",
- "description": "Model keys to check for related connections"
+ "$ref": "#/components/schemas/VideoBoardArg"
}
}
},
@@ -6496,126 +6884,21 @@
},
"responses": {
"200": {
- "description": "Related model keys retrieved successfully",
+ "description": "Successful Response",
"content": {
"application/json": {
"schema": {
- "items": {
- "type": "string"
- },
- "type": "array",
- "title": "Response Get Related Models Batch"
- },
- "example": [
- "ca562b14-995e-4a42-90c1-9528f1a5921d",
- "cc0c2b8a-c62e-41d6-878e-cc74dde5ca8f",
- "18ca7649-6a9e-47d5-bc17-41ab1e8cec81",
- "7c12d1b2-0ef9-4bec-ba55-797b2d8f2ee1",
- "c382eaa3-0e28-4ab0-9446-408667699aeb",
- "71272e82-0e5f-46d5-bca9-9a61f4bd8a82",
- "a5d7cd49-1b98-4534-a475-aeee4ccf5fa2"
- ]
+ "$ref": "#/components/schemas/AddVideosToBoardResult"
+ }
}
}
},
"422": {
- "description": "Validation error"
- },
- "500": {
- "description": "Internal server error"
- }
- },
- "security": [
- {
- "HTTPBearer": []
- }
- ]
- }
- },
- "/api/v1/app/version": {
- "get": {
- "tags": ["app"],
- "summary": "Get Version",
- "operationId": "app_version",
- "responses": {
- "200": {
- "description": "Successful Response",
- "content": {
- "application/json": {
- "schema": {
- "$ref": "#/components/schemas/AppVersion"
- }
- }
- }
- }
- }
- }
- },
- "/api/v1/app/app_deps": {
- "get": {
- "tags": ["app"],
- "summary": "Get App Deps",
- "operationId": "get_app_deps",
- "responses": {
- "200": {
- "description": "Successful Response",
- "content": {
- "application/json": {
- "schema": {
- "additionalProperties": {
- "type": "string"
- },
- "type": "object",
- "title": "Response Get App Deps"
- }
- }
- }
- }
- },
- "security": [
- {
- "HTTPBearer": []
- }
- ]
- }
- },
- "/api/v1/app/patchmatch_status": {
- "get": {
- "tags": ["app"],
- "summary": "Get Patchmatch Status",
- "operationId": "get_patchmatch_status",
- "responses": {
- "200": {
- "description": "Successful Response",
- "content": {
- "application/json": {
- "schema": {
- "type": "boolean",
- "title": "Response Get Patchmatch Status"
- }
- }
- }
- }
- },
- "security": [
- {
- "HTTPBearer": []
- }
- ]
- }
- },
- "/api/v1/app/runtime_config": {
- "get": {
- "tags": ["app"],
- "summary": "Get Runtime Config",
- "operationId": "get_runtime_config",
- "responses": {
- "200": {
- "description": "Successful Response",
+ "description": "Validation Error",
"content": {
"application/json": {
"schema": {
- "$ref": "#/components/schemas/InvokeAIAppConfigWithSetFields"
+ "$ref": "#/components/schemas/HTTPValidationError"
}
}
}
@@ -6627,16 +6910,15 @@
}
]
},
- "patch": {
- "tags": ["app"],
- "summary": "Update Runtime Config",
- "operationId": "update_runtime_config",
+ "delete": {
+ "tags": ["videos"],
+ "summary": "Remove Video From Board",
+ "operationId": "remove_video_from_board",
"requestBody": {
"content": {
"application/json": {
"schema": {
- "$ref": "#/components/schemas/UpdateAppGenerationSettingsRequest",
- "description": "Writable runtime configuration changes"
+ "$ref": "#/components/schemas/Body_remove_video_from_board"
}
}
},
@@ -6648,7 +6930,7 @@
"content": {
"application/json": {
"schema": {
- "$ref": "#/components/schemas/InvokeAIAppConfigWithSetFields"
+ "$ref": "#/components/schemas/RemoveVideosFromBoardResult"
}
}
}
@@ -6671,103 +6953,169 @@
]
}
},
- "/api/v1/app/external_providers/status": {
+ "/api/v1/gallery/items/": {
"get": {
- "tags": ["app"],
- "summary": "Get External Provider Statuses",
- "operationId": "get_external_provider_statuses",
- "responses": {
- "200": {
- "description": "Successful Response",
- "content": {
- "application/json": {
- "schema": {
- "items": {
- "$ref": "#/components/schemas/ExternalProviderStatusModel"
- },
- "type": "array",
- "title": "Response Get External Provider Statuses"
- }
- }
- }
- }
- },
+ "tags": ["gallery"],
+ "summary": "List Gallery Items",
+ "description": "Returns a paginated, time-sorted stream of polymorphic gallery items (images + videos).",
+ "operationId": "list_gallery_items",
"security": [
{
"HTTPBearer": []
}
- ]
- }
- },
- "/api/v1/app/external_providers/config": {
- "get": {
- "tags": ["app"],
- "summary": "Get External Provider Configs",
- "operationId": "get_external_provider_configs",
- "responses": {
- "200": {
- "description": "Successful Response",
- "content": {
- "application/json": {
- "schema": {
- "items": {
- "$ref": "#/components/schemas/ExternalProviderConfigModel"
- },
+ ],
+ "parameters": [
+ {
+ "name": "origin",
+ "in": "query",
+ "required": false,
+ "schema": {
+ "anyOf": [
+ {
+ "$ref": "#/components/schemas/ResourceOrigin"
+ },
+ {
+ "type": "null"
+ }
+ ],
+ "description": "The origin of items to list.",
+ "title": "Origin"
+ },
+ "description": "The origin of items to list."
+ },
+ {
+ "name": "categories",
+ "in": "query",
+ "required": false,
+ "schema": {
+ "anyOf": [
+ {
"type": "array",
- "title": "Response Get External Provider Configs"
+ "items": {
+ "$ref": "#/components/schemas/ImageCategory"
+ }
+ },
+ {
+ "type": "null"
}
- }
- }
- }
- },
- "security": [
+ ],
+ "description": "The categories to include. Shared between images and videos.",
+ "title": "Categories"
+ },
+ "description": "The categories to include. Shared between images and videos."
+ },
{
- "HTTPBearer": []
- }
- ]
- }
- },
- "/api/v1/app/external_providers/config/{provider_id}": {
- "post": {
- "tags": ["app"],
- "summary": "Set External Provider Config",
- "operationId": "set_external_provider_config",
- "security": [
+ "name": "is_intermediate",
+ "in": "query",
+ "required": false,
+ "schema": {
+ "anyOf": [
+ {
+ "type": "boolean"
+ },
+ {
+ "type": "null"
+ }
+ ],
+ "description": "Whether to list intermediate items.",
+ "title": "Is Intermediate"
+ },
+ "description": "Whether to list intermediate items."
+ },
{
- "HTTPBearer": []
- }
- ],
- "parameters": [
+ "name": "board_id",
+ "in": "query",
+ "required": false,
+ "schema": {
+ "anyOf": [
+ {
+ "type": "string"
+ },
+ {
+ "type": "null"
+ }
+ ],
+ "description": "The board id to filter by. Use 'none' to find items without a board.",
+ "title": "Board Id"
+ },
+ "description": "The board id to filter by. Use 'none' to find items without a board."
+ },
{
- "name": "provider_id",
- "in": "path",
- "required": true,
+ "name": "offset",
+ "in": "query",
+ "required": false,
"schema": {
- "type": "string",
- "description": "The external provider identifier",
- "title": "Provider Id"
+ "type": "integer",
+ "minimum": 0,
+ "description": "The page offset",
+ "default": 0,
+ "title": "Offset"
},
- "description": "The external provider identifier"
+ "description": "The page offset"
+ },
+ {
+ "name": "limit",
+ "in": "query",
+ "required": false,
+ "schema": {
+ "type": "integer",
+ "maximum": 1000,
+ "minimum": 0,
+ "description": "The number of items per page",
+ "default": 10,
+ "title": "Limit"
+ },
+ "description": "The number of items per page"
+ },
+ {
+ "name": "order_dir",
+ "in": "query",
+ "required": false,
+ "schema": {
+ "$ref": "#/components/schemas/SQLiteDirection",
+ "description": "The order of sort",
+ "default": "DESC"
+ },
+ "description": "The order of sort"
+ },
+ {
+ "name": "starred_first",
+ "in": "query",
+ "required": false,
+ "schema": {
+ "type": "boolean",
+ "description": "Whether to sort by starred items first",
+ "default": true,
+ "title": "Starred First"
+ },
+ "description": "Whether to sort by starred items first"
+ },
+ {
+ "name": "search_term",
+ "in": "query",
+ "required": false,
+ "schema": {
+ "anyOf": [
+ {
+ "type": "string"
+ },
+ {
+ "type": "null"
+ }
+ ],
+ "description": "The term to search for",
+ "title": "Search Term"
+ },
+ "description": "The term to search for"
}
],
- "requestBody": {
- "required": true,
- "content": {
- "application/json": {
- "schema": {
- "$ref": "#/components/schemas/ExternalProviderConfigUpdate",
- "description": "External provider configuration settings"
- }
- }
- }
- },
"responses": {
"200": {
"description": "Successful Response",
"content": {
"application/json": {
"schema": {
- "$ref": "#/components/schemas/ExternalProviderConfigModel"
+ "$ref": "#/components/schemas/OffsetPaginatedResults_GalleryItem_"
}
}
}
@@ -6783,11 +7131,14 @@
}
}
}
- },
- "delete": {
- "tags": ["app"],
- "summary": "Reset External Provider Config",
- "operationId": "reset_external_provider_config",
+ }
+ },
+ "/api/v1/gallery/items/names": {
+ "get": {
+ "tags": ["gallery"],
+ "summary": "Get Gallery Item Names",
+ "description": "Returns an ordered (kind, name) list \u2014 used to drive virtualized gallery selection.",
+ "operationId": "get_gallery_item_names",
"security": [
{
"HTTPBearer": []
@@ -6795,15 +7146,120 @@
],
"parameters": [
{
- "name": "provider_id",
- "in": "path",
- "required": true,
+ "name": "origin",
+ "in": "query",
+ "required": false,
"schema": {
- "type": "string",
- "description": "The external provider identifier",
- "title": "Provider Id"
+ "anyOf": [
+ {
+ "$ref": "#/components/schemas/ResourceOrigin"
+ },
+ {
+ "type": "null"
+ }
+ ],
+ "description": "The origin of items to list.",
+ "title": "Origin"
},
- "description": "The external provider identifier"
+ "description": "The origin of items to list."
+ },
+ {
+ "name": "categories",
+ "in": "query",
+ "required": false,
+ "schema": {
+ "anyOf": [
+ {
+ "type": "array",
+ "items": {
+ "$ref": "#/components/schemas/ImageCategory"
+ }
+ },
+ {
+ "type": "null"
+ }
+ ],
+ "description": "The categories to include. Shared between images and videos.",
+ "title": "Categories"
+ },
+ "description": "The categories to include. Shared between images and videos."
+ },
+ {
+ "name": "is_intermediate",
+ "in": "query",
+ "required": false,
+ "schema": {
+ "anyOf": [
+ {
+ "type": "boolean"
+ },
+ {
+ "type": "null"
+ }
+ ],
+ "description": "Whether to list intermediate items.",
+ "title": "Is Intermediate"
+ },
+ "description": "Whether to list intermediate items."
+ },
+ {
+ "name": "board_id",
+ "in": "query",
+ "required": false,
+ "schema": {
+ "anyOf": [
+ {
+ "type": "string"
+ },
+ {
+ "type": "null"
+ }
+ ],
+ "description": "The board id to filter by. Use 'none' to find items without a board.",
+ "title": "Board Id"
+ },
+ "description": "The board id to filter by. Use 'none' to find items without a board."
+ },
+ {
+ "name": "order_dir",
+ "in": "query",
+ "required": false,
+ "schema": {
+ "$ref": "#/components/schemas/SQLiteDirection",
+ "description": "The order of sort",
+ "default": "DESC"
+ },
+ "description": "The order of sort"
+ },
+ {
+ "name": "starred_first",
+ "in": "query",
+ "required": false,
+ "schema": {
+ "type": "boolean",
+ "description": "Whether to sort by starred items first",
+ "default": true,
+ "title": "Starred First"
+ },
+ "description": "Whether to sort by starred items first"
+ },
+ {
+ "name": "search_term",
+ "in": "query",
+ "required": false,
+ "schema": {
+ "anyOf": [
+ {
+ "type": "string"
+ },
+ {
+ "type": "null"
+ }
+ ],
+ "description": "The term to search for",
+ "title": "Search Term"
+ },
+ "description": "The term to search for"
}
],
"responses": {
@@ -6812,7 +7268,7 @@
"content": {
"application/json": {
"schema": {
- "$ref": "#/components/schemas/ExternalProviderConfigModel"
+ "$ref": "#/components/schemas/GalleryItemNamesResult"
}
}
}
@@ -6830,53 +7286,38 @@
}
}
},
- "/api/v1/app/logging": {
- "get": {
- "tags": ["app"],
- "summary": "Get Log Level",
- "description": "Returns the log level",
- "operationId": "get_log_level",
- "responses": {
- "200": {
- "description": "The operation was successful",
- "content": {
- "application/json": {
- "schema": {
- "$ref": "#/components/schemas/LogLevel"
- }
- }
- }
- }
- },
+ "/api/v1/boards/": {
+ "post": {
+ "tags": ["boards"],
+ "summary": "Create Board",
+ "description": "Creates a board for the current user",
+ "operationId": "create_board",
"security": [
{
"HTTPBearer": []
}
- ]
- },
- "post": {
- "tags": ["app"],
- "summary": "Set Log Level",
- "description": "Sets the log verbosity level",
- "operationId": "set_log_level",
- "requestBody": {
- "content": {
- "application/json": {
- "schema": {
- "$ref": "#/components/schemas/LogLevel",
- "description": "New log verbosity level"
- }
- }
- },
- "required": true
- },
+ ],
+ "parameters": [
+ {
+ "name": "board_name",
+ "in": "query",
+ "required": true,
+ "schema": {
+ "type": "string",
+ "maxLength": 300,
+ "description": "The name of the board to create",
+ "title": "Board Name"
+ },
+ "description": "The name of the board to create"
+ }
+ ],
"responses": {
- "200": {
- "description": "The operation was successful",
+ "201": {
+ "description": "The board was created successfully",
"content": {
"application/json": {
"schema": {
- "$ref": "#/components/schemas/LogLevel"
+ "$ref": "#/components/schemas/BoardDTO"
}
}
}
@@ -6891,216 +7332,106 @@
}
}
}
- },
+ }
+ },
+ "get": {
+ "tags": ["boards"],
+ "summary": "List Boards",
+ "description": "Gets a list of boards for the current user, including shared boards. Admin users see all boards.",
+ "operationId": "list_boards",
"security": [
{
"HTTPBearer": []
}
- ]
- }
- },
- "/api/v1/app/invocation_cache": {
- "delete": {
- "tags": ["app"],
- "summary": "Clear Invocation Cache",
- "description": "Clears the invocation cache",
- "operationId": "clear_invocation_cache",
- "responses": {
- "200": {
- "description": "The operation was successful",
- "content": {
- "application/json": {
- "schema": {}
- }
- }
- }
- },
- "security": [
+ ],
+ "parameters": [
{
- "HTTPBearer": []
- }
- ]
- }
- },
- "/api/v1/app/invocation_cache/enable": {
- "put": {
- "tags": ["app"],
- "summary": "Enable Invocation Cache",
- "description": "Clears the invocation cache",
- "operationId": "enable_invocation_cache",
- "responses": {
- "200": {
- "description": "The operation was successful",
- "content": {
- "application/json": {
- "schema": {}
- }
- }
- }
- },
- "security": [
- {
- "HTTPBearer": []
- }
- ]
- }
- },
- "/api/v1/app/invocation_cache/disable": {
- "put": {
- "tags": ["app"],
- "summary": "Disable Invocation Cache",
- "description": "Clears the invocation cache",
- "operationId": "disable_invocation_cache",
- "responses": {
- "200": {
- "description": "The operation was successful",
- "content": {
- "application/json": {
- "schema": {}
- }
- }
- }
- },
- "security": [
- {
- "HTTPBearer": []
- }
- ]
- }
- },
- "/api/v1/app/invocation_cache/status": {
- "get": {
- "tags": ["app"],
- "summary": "Get Invocation Cache Status",
- "description": "Clears the invocation cache",
- "operationId": "get_invocation_cache_status",
- "responses": {
- "200": {
- "description": "Successful Response",
- "content": {
- "application/json": {
- "schema": {
- "$ref": "#/components/schemas/InvocationCacheStatus"
- }
- }
- }
- }
- },
- "security": [
- {
- "HTTPBearer": []
- }
- ]
- }
- },
- "/api/v1/queue/{queue_id}/enqueue_batch": {
- "post": {
- "tags": ["queue"],
- "summary": "Enqueue Batch",
- "description": "Processes a batch and enqueues the output graphs for execution for the current user.",
- "operationId": "enqueue_batch",
- "security": [
- {
- "HTTPBearer": []
- }
- ],
- "parameters": [
+ "name": "order_by",
+ "in": "query",
+ "required": false,
+ "schema": {
+ "$ref": "#/components/schemas/BoardRecordOrderBy",
+ "description": "The attribute to order by",
+ "default": "created_at"
+ },
+ "description": "The attribute to order by"
+ },
{
- "name": "queue_id",
- "in": "path",
- "required": true,
+ "name": "direction",
+ "in": "query",
+ "required": false,
"schema": {
- "type": "string",
- "description": "The queue id to perform this operation on",
- "title": "Queue Id"
+ "$ref": "#/components/schemas/SQLiteDirection",
+ "description": "The direction to order by",
+ "default": "DESC"
},
- "description": "The queue id to perform this operation on"
- }
- ],
- "requestBody": {
- "required": true,
- "content": {
- "application/json": {
- "schema": {
- "$ref": "#/components/schemas/Body_enqueue_batch"
- }
- }
- }
- },
- "responses": {
- "200": {
- "description": "Successful Response",
- "content": {
- "application/json": {
- "schema": {
- "$ref": "#/components/schemas/EnqueueBatchResult"
- }
- }
- }
+ "description": "The direction to order by"
},
- "201": {
- "content": {
- "application/json": {
- "schema": {
- "$ref": "#/components/schemas/EnqueueBatchResult"
+ {
+ "name": "all",
+ "in": "query",
+ "required": false,
+ "schema": {
+ "anyOf": [
+ {
+ "type": "boolean"
+ },
+ {
+ "type": "null"
}
- }
+ ],
+ "description": "Whether to list all boards",
+ "title": "All"
},
- "description": "Created"
+ "description": "Whether to list all boards"
},
- "422": {
- "description": "Validation Error",
- "content": {
- "application/json": {
- "schema": {
- "$ref": "#/components/schemas/HTTPValidationError"
- }
- }
- }
- }
- }
- }
- },
- "/api/v1/queue/{queue_id}/list_all": {
- "get": {
- "tags": ["queue"],
- "summary": "List All Queue Items",
- "description": "Gets all queue items",
- "operationId": "list_all_queue_items",
- "security": [
- {
- "HTTPBearer": []
- }
- ],
- "parameters": [
{
- "name": "queue_id",
- "in": "path",
- "required": true,
+ "name": "offset",
+ "in": "query",
+ "required": false,
"schema": {
- "type": "string",
- "description": "The queue id to perform this operation on",
- "title": "Queue Id"
+ "anyOf": [
+ {
+ "type": "integer"
+ },
+ {
+ "type": "null"
+ }
+ ],
+ "description": "The page offset",
+ "title": "Offset"
},
- "description": "The queue id to perform this operation on"
+ "description": "The page offset"
},
{
- "name": "destination",
+ "name": "limit",
"in": "query",
"required": false,
"schema": {
"anyOf": [
{
- "type": "string"
+ "type": "integer"
},
{
"type": "null"
}
],
- "description": "The destination of queue items to fetch",
- "title": "Destination"
+ "description": "The number of boards per page",
+ "title": "Limit"
},
- "description": "The destination of queue items to fetch"
+ "description": "The number of boards per page"
+ },
+ {
+ "name": "include_archived",
+ "in": "query",
+ "required": false,
+ "schema": {
+ "type": "boolean",
+ "description": "Whether or not to include archived boards in list",
+ "default": false,
+ "title": "Include Archived"
+ },
+ "description": "Whether or not to include archived boards in list"
}
],
"responses": {
@@ -7109,11 +7440,18 @@
"content": {
"application/json": {
"schema": {
- "type": "array",
- "items": {
- "$ref": "#/components/schemas/SessionQueueItem"
- },
- "title": "Response 200 List All Queue Items"
+ "anyOf": [
+ {
+ "$ref": "#/components/schemas/OffsetPaginatedResults_BoardDTO_"
+ },
+ {
+ "type": "array",
+ "items": {
+ "$ref": "#/components/schemas/BoardDTO"
+ }
+ }
+ ],
+ "title": "Response List Boards"
}
}
}
@@ -7131,12 +7469,12 @@
}
}
},
- "/api/v1/queue/{queue_id}/item_ids": {
+ "/api/v1/boards/{board_id}": {
"get": {
- "tags": ["queue"],
- "summary": "Get Queue Item Ids",
- "description": "Gets all queue item ids that match the given parameters.\n\nIDs for every user's items are returned (item ids carry no sensitive data on their own).\nWhen the corresponding items are hydrated via get_queue_items_by_item_ids, those belonging\nto other users are redacted by sanitize_queue_item_for_user. This lets a non-admin see\npartially-redacted entries for other users' jobs in the queue list, while still revealing\nonly timestamps and status for items they do not own.\n\ncurrent_user is required so the endpoint stays behind authentication in multiuser mode.",
- "operationId": "get_queue_item_ids",
+ "tags": ["boards"],
+ "summary": "Get Board",
+ "description": "Gets a board (user must have access to it)",
+ "operationId": "get_board",
"security": [
{
"HTTPBearer": []
@@ -7144,26 +7482,15 @@
],
"parameters": [
{
- "name": "queue_id",
+ "name": "board_id",
"in": "path",
"required": true,
"schema": {
"type": "string",
- "description": "The queue id to perform this operation on",
- "title": "Queue Id"
- },
- "description": "The queue id to perform this operation on"
- },
- {
- "name": "order_dir",
- "in": "query",
- "required": false,
- "schema": {
- "$ref": "#/components/schemas/SQLiteDirection",
- "description": "The order of sort",
- "default": "DESC"
+ "description": "The id of board to get",
+ "title": "Board Id"
},
- "description": "The order of sort"
+ "description": "The id of board to get"
}
],
"responses": {
@@ -7172,7 +7499,7 @@
"content": {
"application/json": {
"schema": {
- "$ref": "#/components/schemas/ItemIdsResult"
+ "$ref": "#/components/schemas/BoardDTO"
}
}
}
@@ -7188,14 +7515,12 @@
}
}
}
- }
- },
- "/api/v1/queue/{queue_id}/items_by_ids": {
- "post": {
- "tags": ["queue"],
- "summary": "Get Queue Items By Item Ids",
- "description": "Gets queue items for the specified queue item ids. Maintains order of item ids.",
- "operationId": "get_queue_items_by_item_ids",
+ },
+ "patch": {
+ "tags": ["boards"],
+ "summary": "Update Board",
+ "description": "Updates a board (user must have access to it)",
+ "operationId": "update_board",
"security": [
{
"HTTPBearer": []
@@ -7203,15 +7528,15 @@
],
"parameters": [
{
- "name": "queue_id",
+ "name": "board_id",
"in": "path",
"required": true,
"schema": {
"type": "string",
- "description": "The queue id to perform this operation on",
- "title": "Queue Id"
+ "description": "The id of board to update",
+ "title": "Board Id"
},
- "description": "The queue id to perform this operation on"
+ "description": "The id of board to update"
}
],
"requestBody": {
@@ -7219,22 +7544,19 @@
"content": {
"application/json": {
"schema": {
- "$ref": "#/components/schemas/Body_get_queue_items_by_item_ids"
+ "$ref": "#/components/schemas/BoardChanges",
+ "description": "The changes to apply to the board"
}
}
}
},
"responses": {
- "200": {
- "description": "Successful Response",
+ "201": {
+ "description": "The board was updated successfully",
"content": {
"application/json": {
"schema": {
- "type": "array",
- "items": {
- "$ref": "#/components/schemas/SessionQueueItem"
- },
- "title": "Response 200 Get Queue Items By Item Ids"
+ "$ref": "#/components/schemas/BoardDTO"
}
}
}
@@ -7250,14 +7572,12 @@
}
}
}
- }
- },
- "/api/v1/queue/{queue_id}/processor/resume": {
- "put": {
- "tags": ["queue"],
- "summary": "Resume",
- "description": "Resumes session processor. Admin only.",
- "operationId": "resume",
+ },
+ "delete": {
+ "tags": ["boards"],
+ "summary": "Delete Board",
+ "description": "Deletes a board (user must have access to it)",
+ "operationId": "delete_board",
"security": [
{
"HTTPBearer": []
@@ -7265,15 +7585,34 @@
],
"parameters": [
{
- "name": "queue_id",
+ "name": "board_id",
"in": "path",
"required": true,
"schema": {
"type": "string",
- "description": "The queue id to perform this operation on",
- "title": "Queue Id"
+ "description": "The id of board to delete",
+ "title": "Board Id"
},
- "description": "The queue id to perform this operation on"
+ "description": "The id of board to delete"
+ },
+ {
+ "name": "include_images",
+ "in": "query",
+ "required": false,
+ "schema": {
+ "anyOf": [
+ {
+ "type": "boolean"
+ },
+ {
+ "type": "null"
+ }
+ ],
+ "description": "Permanently delete all images and videos on the board",
+ "default": false,
+ "title": "Include Images"
+ },
+ "description": "Permanently delete all images and videos on the board"
}
],
"responses": {
@@ -7282,7 +7621,7 @@
"content": {
"application/json": {
"schema": {
- "$ref": "#/components/schemas/SessionProcessorStatus"
+ "$ref": "#/components/schemas/DeleteBoardResult"
}
}
}
@@ -7300,12 +7639,12 @@
}
}
},
- "/api/v1/queue/{queue_id}/processor/pause": {
- "put": {
- "tags": ["queue"],
- "summary": "Pause",
- "description": "Pauses session processor. Admin only.",
- "operationId": "pause",
+ "/api/v1/boards/{board_id}/image_names": {
+ "get": {
+ "tags": ["boards"],
+ "summary": "List All Board Image Names",
+ "description": "Gets a list of images for a board",
+ "operationId": "list_all_board_image_names",
"security": [
{
"HTTPBearer": []
@@ -7313,15 +7652,54 @@
],
"parameters": [
{
- "name": "queue_id",
+ "name": "board_id",
"in": "path",
"required": true,
"schema": {
"type": "string",
- "description": "The queue id to perform this operation on",
- "title": "Queue Id"
+ "description": "The id of the board or 'none' for uncategorized images",
+ "title": "Board Id"
},
- "description": "The queue id to perform this operation on"
+ "description": "The id of the board or 'none' for uncategorized images"
+ },
+ {
+ "name": "categories",
+ "in": "query",
+ "required": false,
+ "schema": {
+ "anyOf": [
+ {
+ "type": "array",
+ "items": {
+ "$ref": "#/components/schemas/ImageCategory"
+ }
+ },
+ {
+ "type": "null"
+ }
+ ],
+ "description": "The categories of image to include.",
+ "title": "Categories"
+ },
+ "description": "The categories of image to include."
+ },
+ {
+ "name": "is_intermediate",
+ "in": "query",
+ "required": false,
+ "schema": {
+ "anyOf": [
+ {
+ "type": "boolean"
+ },
+ {
+ "type": "null"
+ }
+ ],
+ "description": "Whether to list intermediate images.",
+ "title": "Is Intermediate"
+ },
+ "description": "Whether to list intermediate images."
}
],
"responses": {
@@ -7330,7 +7708,11 @@
"content": {
"application/json": {
"schema": {
- "$ref": "#/components/schemas/SessionProcessorStatus"
+ "type": "array",
+ "items": {
+ "type": "string"
+ },
+ "title": "Response List All Board Image Names"
}
}
}
@@ -7348,37 +7730,29 @@
}
}
},
- "/api/v1/queue/{queue_id}/cancel_all_except_current": {
- "put": {
- "tags": ["queue"],
- "summary": "Cancel All Except Current",
- "description": "Immediately cancels all queue items except in-processing items. Non-admin users can only cancel their own items.",
- "operationId": "cancel_all_except_current",
- "security": [
- {
- "HTTPBearer": []
- }
- ],
- "parameters": [
- {
- "name": "queue_id",
- "in": "path",
- "required": true,
- "schema": {
- "type": "string",
- "description": "The queue id to perform this operation on",
- "title": "Queue Id"
- },
- "description": "The queue id to perform this operation on"
- }
- ],
+ "/api/v1/board_images/": {
+ "post": {
+ "tags": ["boards"],
+ "summary": "Add Image To Board",
+ "description": "Creates a board_image",
+ "operationId": "add_image_to_board",
+ "requestBody": {
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/Body_add_image_to_board"
+ }
+ }
+ },
+ "required": true
+ },
"responses": {
- "200": {
- "description": "Successful Response",
+ "201": {
+ "description": "The image was added to a board successfully",
"content": {
"application/json": {
"schema": {
- "$ref": "#/components/schemas/CancelAllExceptCurrentResult"
+ "$ref": "#/components/schemas/AddImagesToBoardResult"
}
}
}
@@ -7393,40 +7767,35 @@
}
}
}
- }
- }
- },
- "/api/v1/queue/{queue_id}/delete_all_except_current": {
- "put": {
- "tags": ["queue"],
- "summary": "Delete All Except Current",
- "description": "Immediately deletes all queue items except in-processing items. Non-admin users can only delete their own items.",
- "operationId": "delete_all_except_current",
+ },
"security": [
{
"HTTPBearer": []
}
- ],
- "parameters": [
- {
- "name": "queue_id",
- "in": "path",
- "required": true,
- "schema": {
- "type": "string",
- "description": "The queue id to perform this operation on",
- "title": "Queue Id"
- },
- "description": "The queue id to perform this operation on"
- }
- ],
+ ]
+ },
+ "delete": {
+ "tags": ["boards"],
+ "summary": "Remove Image From Board",
+ "description": "Removes an image from its board, if it had one",
+ "operationId": "remove_image_from_board",
+ "requestBody": {
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/Body_remove_image_from_board"
+ }
+ }
+ },
+ "required": true
+ },
"responses": {
- "200": {
- "description": "Successful Response",
+ "201": {
+ "description": "The image was removed from the board successfully",
"content": {
"application/json": {
"schema": {
- "$ref": "#/components/schemas/DeleteAllExceptCurrentResult"
+ "$ref": "#/components/schemas/RemoveImagesFromBoardResult"
}
}
}
@@ -7441,50 +7810,37 @@
}
}
}
- }
+ },
+ "security": [
+ {
+ "HTTPBearer": []
+ }
+ ]
}
},
- "/api/v1/queue/{queue_id}/cancel_by_batch_ids": {
- "put": {
- "tags": ["queue"],
- "summary": "Cancel By Batch Ids",
- "description": "Immediately cancels all queue items from the given batch ids. Non-admin users can only cancel their own items.",
- "operationId": "cancel_by_batch_ids",
- "security": [
- {
- "HTTPBearer": []
- }
- ],
- "parameters": [
- {
- "name": "queue_id",
- "in": "path",
- "required": true,
- "schema": {
- "type": "string",
- "description": "The queue id to perform this operation on",
- "title": "Queue Id"
- },
- "description": "The queue id to perform this operation on"
- }
- ],
+ "/api/v1/board_images/batch": {
+ "post": {
+ "tags": ["boards"],
+ "summary": "Add Images To Board",
+ "description": "Adds a list of images to a board",
+ "operationId": "add_images_to_board",
"requestBody": {
- "required": true,
"content": {
"application/json": {
"schema": {
- "$ref": "#/components/schemas/Body_cancel_by_batch_ids"
+ "$ref": "#/components/schemas/Body_add_images_to_board"
}
}
- }
+ },
+ "required": true
},
"responses": {
- "200": {
- "description": "Successful Response",
+ "201": {
+ "description": "Images were added to board successfully",
"content": {
"application/json": {
"schema": {
- "$ref": "#/components/schemas/CancelByBatchIDsResult"
+ "$ref": "#/components/schemas/AddImagesToBoardResult"
}
}
}
@@ -7499,114 +7855,37 @@
}
}
}
- }
- }
- },
- "/api/v1/queue/{queue_id}/cancel_by_destination": {
- "put": {
- "tags": ["queue"],
- "summary": "Cancel By Destination",
- "description": "Immediately cancels all queue items with the given destination. Non-admin users can only cancel their own items.",
- "operationId": "cancel_by_destination",
+ },
"security": [
{
"HTTPBearer": []
}
- ],
- "parameters": [
- {
- "name": "queue_id",
- "in": "path",
- "required": true,
- "schema": {
- "type": "string",
- "description": "The queue id to perform this operation on",
- "title": "Queue Id"
- },
- "description": "The queue id to perform this operation on"
- },
- {
- "name": "destination",
- "in": "query",
- "required": true,
- "schema": {
- "type": "string",
- "description": "The destination to cancel all queue items for",
- "title": "Destination"
- },
- "description": "The destination to cancel all queue items for"
- }
- ],
- "responses": {
- "200": {
- "description": "Successful Response",
- "content": {
- "application/json": {
- "schema": {
- "$ref": "#/components/schemas/CancelByDestinationResult"
- }
- }
- }
- },
- "422": {
- "description": "Validation Error",
- "content": {
- "application/json": {
- "schema": {
- "$ref": "#/components/schemas/HTTPValidationError"
- }
- }
- }
- }
- }
+ ]
}
},
- "/api/v1/queue/{queue_id}/retry_items_by_id": {
- "put": {
- "tags": ["queue"],
- "summary": "Retry Items By Id",
- "description": "Retries the given queue items. Users can only retry their own items unless they are an admin.",
- "operationId": "retry_items_by_id",
- "security": [
- {
- "HTTPBearer": []
- }
- ],
- "parameters": [
- {
- "name": "queue_id",
- "in": "path",
- "required": true,
- "schema": {
- "type": "string",
- "description": "The queue id to perform this operation on",
- "title": "Queue Id"
- },
- "description": "The queue id to perform this operation on"
- }
- ],
+ "/api/v1/board_images/batch/delete": {
+ "post": {
+ "tags": ["boards"],
+ "summary": "Remove Images From Board",
+ "description": "Removes a list of images from their board, if they had one",
+ "operationId": "remove_images_from_board",
"requestBody": {
- "required": true,
"content": {
"application/json": {
"schema": {
- "type": "array",
- "items": {
- "type": "integer"
- },
- "description": "The queue item ids to retry",
- "title": "Item Ids"
+ "$ref": "#/components/schemas/Body_remove_images_from_board"
}
}
- }
+ },
+ "required": true
},
"responses": {
- "200": {
- "description": "Successful Response",
+ "201": {
+ "description": "Images were removed from board successfully",
"content": {
"application/json": {
"schema": {
- "$ref": "#/components/schemas/RetryItemsResult"
+ "$ref": "#/components/schemas/RemoveImagesFromBoardResult"
}
}
}
@@ -7621,111 +7900,49 @@
}
}
}
- }
- }
- },
- "/api/v1/queue/{queue_id}/clear": {
- "put": {
- "tags": ["queue"],
- "summary": "Clear",
- "description": "Clears the queue entirely. Admin users clear all items; non-admin users only clear their own items. If there's a currently-executing item, users can only cancel it if they own it or are an admin.",
- "operationId": "clear",
+ },
"security": [
{
"HTTPBearer": []
}
- ],
- "parameters": [
- {
- "name": "queue_id",
- "in": "path",
- "required": true,
- "schema": {
- "type": "string",
- "description": "The queue id to perform this operation on",
- "title": "Queue Id"
- },
- "description": "The queue id to perform this operation on"
- }
- ],
+ ]
+ }
+ },
+ "/api/v1/virtual_boards/by_date": {
+ "get": {
+ "tags": ["virtual_boards"],
+ "summary": "List Virtual Boards By Date",
+ "description": "Gets a list of virtual sub-boards grouped by date. Covers both images and videos.",
+ "operationId": "list_virtual_boards_by_date",
"responses": {
"200": {
"description": "Successful Response",
"content": {
"application/json": {
"schema": {
- "$ref": "#/components/schemas/ClearResult"
- }
- }
- }
- },
- "422": {
- "description": "Validation Error",
- "content": {
- "application/json": {
- "schema": {
- "$ref": "#/components/schemas/HTTPValidationError"
+ "items": {
+ "$ref": "#/components/schemas/VirtualSubBoardDTO"
+ },
+ "type": "array",
+ "title": "Response List Virtual Boards By Date"
}
}
}
}
- }
- }
- },
- "/api/v1/queue/{queue_id}/prune": {
- "put": {
- "tags": ["queue"],
- "summary": "Prune",
- "description": "Prunes all completed or errored queue items. Non-admin users can only prune their own items.",
- "operationId": "prune",
+ },
"security": [
{
"HTTPBearer": []
}
- ],
- "parameters": [
- {
- "name": "queue_id",
- "in": "path",
- "required": true,
- "schema": {
- "type": "string",
- "description": "The queue id to perform this operation on",
- "title": "Queue Id"
- },
- "description": "The queue id to perform this operation on"
- }
- ],
- "responses": {
- "200": {
- "description": "Successful Response",
- "content": {
- "application/json": {
- "schema": {
- "$ref": "#/components/schemas/PruneResult"
- }
- }
- }
- },
- "422": {
- "description": "Validation Error",
- "content": {
- "application/json": {
- "schema": {
- "$ref": "#/components/schemas/HTTPValidationError"
- }
- }
- }
- }
- }
+ ]
}
},
- "/api/v1/queue/{queue_id}/current": {
+ "/api/v1/virtual_boards/by_date/{date}/image_names": {
"get": {
- "tags": ["queue"],
- "summary": "Get Current Queue Item",
- "description": "Gets the currently execution queue item",
- "operationId": "get_current_queue_item",
+ "tags": ["virtual_boards"],
+ "summary": "List Virtual Board Image Names By Date",
+ "description": "Gets ordered image names for a specific date. Image-only; kept for API compatibility \u2014\nthe UI uses the polymorphic `/by_date/{date}/item_names` endpoint.",
+ "operationId": "list_virtual_board_image_names_by_date",
"security": [
{
"HTTPBearer": []
@@ -7733,15 +7950,77 @@
],
"parameters": [
{
- "name": "queue_id",
+ "name": "date",
"in": "path",
"required": true,
"schema": {
"type": "string",
- "description": "The queue id to perform this operation on",
- "title": "Queue Id"
+ "description": "The ISO date string, e.g. '2026-03-18'",
+ "title": "Date"
},
- "description": "The queue id to perform this operation on"
+ "description": "The ISO date string, e.g. '2026-03-18'"
+ },
+ {
+ "name": "starred_first",
+ "in": "query",
+ "required": false,
+ "schema": {
+ "type": "boolean",
+ "description": "Whether to sort starred images first",
+ "default": true,
+ "title": "Starred First"
+ },
+ "description": "Whether to sort starred images first"
+ },
+ {
+ "name": "order_dir",
+ "in": "query",
+ "required": false,
+ "schema": {
+ "$ref": "#/components/schemas/SQLiteDirection",
+ "description": "The sort direction",
+ "default": "DESC"
+ },
+ "description": "The sort direction"
+ },
+ {
+ "name": "categories",
+ "in": "query",
+ "required": false,
+ "schema": {
+ "anyOf": [
+ {
+ "type": "array",
+ "items": {
+ "$ref": "#/components/schemas/ImageCategory"
+ }
+ },
+ {
+ "type": "null"
+ }
+ ],
+ "description": "The categories of images to include",
+ "title": "Categories"
+ },
+ "description": "The categories of images to include"
+ },
+ {
+ "name": "search_term",
+ "in": "query",
+ "required": false,
+ "schema": {
+ "anyOf": [
+ {
+ "type": "string"
+ },
+ {
+ "type": "null"
+ }
+ ],
+ "description": "Search term to filter images",
+ "title": "Search Term"
+ },
+ "description": "Search term to filter images"
}
],
"responses": {
@@ -7750,21 +8029,7 @@
"content": {
"application/json": {
"schema": {
- "anyOf": [
- {
- "$ref": "#/components/schemas/SessionQueueItem"
- },
- {
- "type": "null"
- },
- {
- "$ref": "#/components/schemas/SessionQueueItem"
- },
- {
- "type": "null"
- }
- ],
- "title": "Response 200 Get Current Queue Item"
+ "$ref": "#/components/schemas/ImageNamesResult"
}
}
}
@@ -7782,12 +8047,12 @@
}
}
},
- "/api/v1/queue/{queue_id}/next": {
+ "/api/v1/virtual_boards/by_date/{date}/item_names": {
"get": {
- "tags": ["queue"],
- "summary": "Get Next Queue Item",
- "description": "Gets the next queue item, without executing it",
- "operationId": "get_next_queue_item",
+ "tags": ["virtual_boards"],
+ "summary": "List Virtual Board Item Names By Date",
+ "description": "Gets ordered polymorphic (image + video) item refs for a specific date.",
+ "operationId": "list_virtual_board_item_names_by_date",
"security": [
{
"HTTPBearer": []
@@ -7795,15 +8060,77 @@
],
"parameters": [
{
- "name": "queue_id",
+ "name": "date",
"in": "path",
"required": true,
"schema": {
"type": "string",
- "description": "The queue id to perform this operation on",
- "title": "Queue Id"
+ "description": "The ISO date string, e.g. '2026-03-18'",
+ "title": "Date"
},
- "description": "The queue id to perform this operation on"
+ "description": "The ISO date string, e.g. '2026-03-18'"
+ },
+ {
+ "name": "starred_first",
+ "in": "query",
+ "required": false,
+ "schema": {
+ "type": "boolean",
+ "description": "Whether to sort starred items first",
+ "default": true,
+ "title": "Starred First"
+ },
+ "description": "Whether to sort starred items first"
+ },
+ {
+ "name": "order_dir",
+ "in": "query",
+ "required": false,
+ "schema": {
+ "$ref": "#/components/schemas/SQLiteDirection",
+ "description": "The sort direction",
+ "default": "DESC"
+ },
+ "description": "The sort direction"
+ },
+ {
+ "name": "categories",
+ "in": "query",
+ "required": false,
+ "schema": {
+ "anyOf": [
+ {
+ "type": "array",
+ "items": {
+ "$ref": "#/components/schemas/ImageCategory"
+ }
+ },
+ {
+ "type": "null"
+ }
+ ],
+ "description": "The categories of items to include",
+ "title": "Categories"
+ },
+ "description": "The categories of items to include"
+ },
+ {
+ "name": "search_term",
+ "in": "query",
+ "required": false,
+ "schema": {
+ "anyOf": [
+ {
+ "type": "string"
+ },
+ {
+ "type": "null"
+ }
+ ],
+ "description": "Search term to filter items",
+ "title": "Search Term"
+ },
+ "description": "Search term to filter items"
}
],
"responses": {
@@ -7812,21 +8139,7 @@
"content": {
"application/json": {
"schema": {
- "anyOf": [
- {
- "$ref": "#/components/schemas/SessionQueueItem"
- },
- {
- "type": "null"
- },
- {
- "$ref": "#/components/schemas/SessionQueueItem"
- },
- {
- "type": "null"
- }
- ],
- "title": "Response 200 Get Next Queue Item"
+ "$ref": "#/components/schemas/GalleryItemNamesResult"
}
}
}
@@ -7844,12 +8157,12 @@
}
}
},
- "/api/v1/queue/{queue_id}/status": {
+ "/api/v1/model_relationships/i/{model_key}": {
"get": {
- "tags": ["queue"],
- "summary": "Get Queue Status",
- "description": "Gets the status of the session queue. Returns global counts; every user additionally gets\ntheir own pending/in_progress counts (so the UI can show an X/Y badge and scope personal UI\nlike the progress bar to the user's own activity). Non-admin users cannot see the current\nitem's identifiers unless they own it.",
- "operationId": "get_queue_status",
+ "tags": ["model_relationships"],
+ "summary": "Get Related Models",
+ "description": "Get a list of model keys related to a given model.",
+ "operationId": "get_related_models",
"security": [
{
"HTTPBearer": []
@@ -7857,93 +8170,193 @@
],
"parameters": [
{
- "name": "queue_id",
+ "name": "model_key",
"in": "path",
"required": true,
"schema": {
"type": "string",
- "description": "The queue id to perform this operation on",
- "title": "Queue Id"
+ "description": "The key of the model to get relationships for",
+ "title": "Model Key"
},
- "description": "The queue id to perform this operation on"
+ "description": "The key of the model to get relationships for"
}
],
"responses": {
"200": {
- "description": "Successful Response",
+ "description": "A list of related model keys was retrieved successfully",
"content": {
"application/json": {
"schema": {
- "$ref": "#/components/schemas/SessionQueueAndProcessorStatus"
- }
+ "type": "array",
+ "items": {
+ "type": "string"
+ },
+ "title": "Response Get Related Models"
+ },
+ "example": [
+ "15e9eb28-8cfe-47c9-b610-37907a79fc3c",
+ "71272e82-0e5f-46d5-bca9-9a61f4bd8a82",
+ "a5d7cd49-1b98-4534-a475-aeee4ccf5fa2"
+ ]
}
}
},
+ "404": {
+ "description": "The specified model could not be found"
+ },
"422": {
- "description": "Validation Error",
- "content": {
- "application/json": {
- "schema": {
- "$ref": "#/components/schemas/HTTPValidationError"
- }
- }
- }
+ "description": "Validation error"
}
}
}
},
- "/api/v1/queue/{queue_id}/b/{batch_id}/status": {
- "get": {
- "tags": ["queue"],
- "summary": "Get Batch Status",
- "description": "Gets the status of a batch. Non-admin users only see their own batches.",
- "operationId": "get_batch_status",
+ "/api/v1/model_relationships/": {
+ "post": {
+ "tags": ["model_relationships"],
+ "summary": "Add Model Relationship",
+ "description": "Creates a **bidirectional** relationship between two models, allowing each to reference the other as related.",
+ "operationId": "add_model_relationship_api_v1_model_relationships__post",
+ "requestBody": {
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/ModelRelationshipCreateRequest",
+ "description": "The model keys to relate"
+ }
+ }
+ },
+ "required": true
+ },
+ "responses": {
+ "204": {
+ "description": "The relationship was successfully created"
+ },
+ "400": {
+ "description": "Invalid model keys or self-referential relationship"
+ },
+ "409": {
+ "description": "The relationship already exists"
+ },
+ "422": {
+ "description": "Validation error"
+ },
+ "500": {
+ "description": "Internal server error"
+ }
+ },
"security": [
{
"HTTPBearer": []
}
- ],
- "parameters": [
- {
- "name": "queue_id",
- "in": "path",
- "required": true,
- "schema": {
- "type": "string",
- "description": "The queue id to perform this operation on",
- "title": "Queue Id"
- },
- "description": "The queue id to perform this operation on"
+ ]
+ },
+ "delete": {
+ "tags": ["model_relationships"],
+ "summary": "Remove Model Relationship",
+ "description": "Removes a **bidirectional** relationship between two models. The relationship must already exist.",
+ "operationId": "remove_model_relationship_api_v1_model_relationships__delete",
+ "requestBody": {
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/ModelRelationshipCreateRequest",
+ "description": "The model keys to disconnect"
+ }
+ }
+ },
+ "required": true
+ },
+ "responses": {
+ "204": {
+ "description": "The relationship was successfully removed"
+ },
+ "400": {
+ "description": "Invalid model keys or self-referential relationship"
+ },
+ "404": {
+ "description": "The relationship does not exist"
+ },
+ "422": {
+ "description": "Validation error"
},
+ "500": {
+ "description": "Internal server error"
+ }
+ },
+ "security": [
{
- "name": "batch_id",
- "in": "path",
- "required": true,
- "schema": {
- "type": "string",
- "description": "The batch to get the status of",
- "title": "Batch Id"
- },
- "description": "The batch to get the status of"
+ "HTTPBearer": []
}
- ],
+ ]
+ }
+ },
+ "/api/v1/model_relationships/batch": {
+ "post": {
+ "tags": ["model_relationships"],
+ "summary": "Get Related Model Keys (Batch)",
+ "description": "Retrieves all **unique related model keys** for a list of given models. This is useful for contextual suggestions or filtering.",
+ "operationId": "get_related_models_batch",
+ "requestBody": {
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/ModelRelationshipBatchRequest",
+ "description": "Model keys to check for related connections"
+ }
+ }
+ },
+ "required": true
+ },
"responses": {
"200": {
- "description": "Successful Response",
+ "description": "Related model keys retrieved successfully",
"content": {
"application/json": {
"schema": {
- "$ref": "#/components/schemas/BatchStatus"
- }
+ "items": {
+ "type": "string"
+ },
+ "type": "array",
+ "title": "Response Get Related Models Batch"
+ },
+ "example": [
+ "ca562b14-995e-4a42-90c1-9528f1a5921d",
+ "cc0c2b8a-c62e-41d6-878e-cc74dde5ca8f",
+ "18ca7649-6a9e-47d5-bc17-41ab1e8cec81",
+ "7c12d1b2-0ef9-4bec-ba55-797b2d8f2ee1",
+ "c382eaa3-0e28-4ab0-9446-408667699aeb",
+ "71272e82-0e5f-46d5-bca9-9a61f4bd8a82",
+ "a5d7cd49-1b98-4534-a475-aeee4ccf5fa2"
+ ]
}
}
},
"422": {
- "description": "Validation Error",
+ "description": "Validation error"
+ },
+ "500": {
+ "description": "Internal server error"
+ }
+ },
+ "security": [
+ {
+ "HTTPBearer": []
+ }
+ ]
+ }
+ },
+ "/api/v1/app/version": {
+ "get": {
+ "tags": ["app"],
+ "summary": "Get Version",
+ "operationId": "app_version",
+ "responses": {
+ "200": {
+ "description": "Successful Response",
"content": {
"application/json": {
"schema": {
- "$ref": "#/components/schemas/HTTPValidationError"
+ "$ref": "#/components/schemas/AppVersion"
}
}
}
@@ -7951,104 +8364,105 @@
}
}
},
- "/api/v1/queue/{queue_id}/i/{item_id}": {
+ "/api/v1/app/app_deps": {
"get": {
- "tags": ["queue"],
- "summary": "Get Queue Item",
- "description": "Gets a queue item",
- "operationId": "get_queue_item",
+ "tags": ["app"],
+ "summary": "Get App Deps",
+ "operationId": "get_app_deps",
+ "responses": {
+ "200": {
+ "description": "Successful Response",
+ "content": {
+ "application/json": {
+ "schema": {
+ "additionalProperties": {
+ "type": "string"
+ },
+ "type": "object",
+ "title": "Response Get App Deps"
+ }
+ }
+ }
+ }
+ },
"security": [
{
"HTTPBearer": []
}
- ],
- "parameters": [
- {
- "name": "queue_id",
- "in": "path",
- "required": true,
- "schema": {
- "type": "string",
- "description": "The queue id to perform this operation on",
- "title": "Queue Id"
- },
- "description": "The queue id to perform this operation on"
- },
- {
- "name": "item_id",
- "in": "path",
- "required": true,
- "schema": {
- "type": "integer",
- "description": "The queue item to get",
- "title": "Item Id"
- },
- "description": "The queue item to get"
- }
- ],
+ ]
+ }
+ },
+ "/api/v1/app/patchmatch_status": {
+ "get": {
+ "tags": ["app"],
+ "summary": "Get Patchmatch Status",
+ "operationId": "get_patchmatch_status",
"responses": {
"200": {
"description": "Successful Response",
"content": {
"application/json": {
"schema": {
- "$ref": "#/components/schemas/SessionQueueItem"
+ "type": "boolean",
+ "title": "Response Get Patchmatch Status"
}
}
}
- },
- "422": {
- "description": "Validation Error",
+ }
+ },
+ "security": [
+ {
+ "HTTPBearer": []
+ }
+ ]
+ }
+ },
+ "/api/v1/app/runtime_config": {
+ "get": {
+ "tags": ["app"],
+ "summary": "Get Runtime Config",
+ "operationId": "get_runtime_config",
+ "responses": {
+ "200": {
+ "description": "Successful Response",
"content": {
"application/json": {
"schema": {
- "$ref": "#/components/schemas/HTTPValidationError"
+ "$ref": "#/components/schemas/InvokeAIAppConfigWithSetFields"
}
}
}
}
- }
- },
- "delete": {
- "tags": ["queue"],
- "summary": "Delete Queue Item",
- "description": "Deletes a queue item. Users can only delete their own items unless they are an admin.",
- "operationId": "delete_queue_item",
+ },
"security": [
{
"HTTPBearer": []
}
- ],
- "parameters": [
- {
- "name": "queue_id",
- "in": "path",
- "required": true,
- "schema": {
- "type": "string",
- "description": "The queue id to perform this operation on",
- "title": "Queue Id"
- },
- "description": "The queue id to perform this operation on"
+ ]
+ },
+ "patch": {
+ "tags": ["app"],
+ "summary": "Update Runtime Config",
+ "operationId": "update_runtime_config",
+ "requestBody": {
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/UpdateAppGenerationSettingsRequest",
+ "description": "Writable runtime configuration changes"
+ }
+ }
},
- {
- "name": "item_id",
- "in": "path",
- "required": true,
- "schema": {
- "type": "integer",
- "description": "The queue item to delete",
- "title": "Item Id"
- },
- "description": "The queue item to delete"
- }
- ],
+ "required": true
+ },
"responses": {
"200": {
"description": "Successful Response",
"content": {
"application/json": {
- "schema": {}
+ "schema": {
+ "$ref": "#/components/schemas/InvokeAIAppConfigWithSetFields"
+ }
}
}
},
@@ -8062,74 +8476,75 @@
}
}
}
- }
- }
- },
- "/api/v1/queue/{queue_id}/i/{item_id}/cancel": {
- "put": {
- "tags": ["queue"],
- "summary": "Cancel Queue Item",
- "description": "Cancels a queue item. Users can only cancel their own items unless they are an admin.",
- "operationId": "cancel_queue_item",
+ },
"security": [
{
"HTTPBearer": []
}
- ],
- "parameters": [
- {
- "name": "queue_id",
- "in": "path",
- "required": true,
- "schema": {
- "type": "string",
- "description": "The queue id to perform this operation on",
- "title": "Queue Id"
- },
- "description": "The queue id to perform this operation on"
- },
- {
- "name": "item_id",
- "in": "path",
- "required": true,
- "schema": {
- "type": "integer",
- "description": "The queue item to cancel",
- "title": "Item Id"
- },
- "description": "The queue item to cancel"
- }
- ],
+ ]
+ }
+ },
+ "/api/v1/app/external_providers/status": {
+ "get": {
+ "tags": ["app"],
+ "summary": "Get External Provider Statuses",
+ "operationId": "get_external_provider_statuses",
"responses": {
"200": {
"description": "Successful Response",
"content": {
"application/json": {
"schema": {
- "$ref": "#/components/schemas/SessionQueueItem"
+ "items": {
+ "$ref": "#/components/schemas/ExternalProviderStatusModel"
+ },
+ "type": "array",
+ "title": "Response Get External Provider Statuses"
}
}
}
- },
- "422": {
- "description": "Validation Error",
+ }
+ },
+ "security": [
+ {
+ "HTTPBearer": []
+ }
+ ]
+ }
+ },
+ "/api/v1/app/external_providers/config": {
+ "get": {
+ "tags": ["app"],
+ "summary": "Get External Provider Configs",
+ "operationId": "get_external_provider_configs",
+ "responses": {
+ "200": {
+ "description": "Successful Response",
"content": {
"application/json": {
"schema": {
- "$ref": "#/components/schemas/HTTPValidationError"
+ "items": {
+ "$ref": "#/components/schemas/ExternalProviderConfigModel"
+ },
+ "type": "array",
+ "title": "Response Get External Provider Configs"
}
}
}
}
- }
+ },
+ "security": [
+ {
+ "HTTPBearer": []
+ }
+ ]
}
},
- "/api/v1/queue/{queue_id}/counts_by_destination": {
- "get": {
- "tags": ["queue"],
- "summary": "Counts By Destination",
- "description": "Gets the counts of queue items by destination. Non-admin users only see their own items.",
- "operationId": "counts_by_destination",
+ "/api/v1/app/external_providers/config/{provider_id}": {
+ "post": {
+ "tags": ["app"],
+ "summary": "Set External Provider Config",
+ "operationId": "set_external_provider_config",
"security": [
{
"HTTPBearer": []
@@ -8137,35 +8552,35 @@
],
"parameters": [
{
- "name": "queue_id",
+ "name": "provider_id",
"in": "path",
"required": true,
"schema": {
"type": "string",
- "description": "The queue id to query",
- "title": "Queue Id"
- },
- "description": "The queue id to query"
- },
- {
- "name": "destination",
- "in": "query",
- "required": true,
- "schema": {
- "type": "string",
- "description": "The destination to query",
- "title": "Destination"
+ "description": "The external provider identifier",
+ "title": "Provider Id"
},
- "description": "The destination to query"
+ "description": "The external provider identifier"
}
],
+ "requestBody": {
+ "required": true,
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/ExternalProviderConfigUpdate",
+ "description": "External provider configuration settings"
+ }
+ }
+ }
+ },
"responses": {
"200": {
"description": "Successful Response",
"content": {
"application/json": {
"schema": {
- "$ref": "#/components/schemas/SessionQueueCountsByDestination"
+ "$ref": "#/components/schemas/ExternalProviderConfigModel"
}
}
}
@@ -8181,14 +8596,11 @@
}
}
}
- }
- },
- "/api/v1/queue/{queue_id}/d/{destination}": {
+ },
"delete": {
- "tags": ["queue"],
- "summary": "Delete By Destination",
- "description": "Deletes all items with the given destination. Non-admin users can only delete their own items.",
- "operationId": "delete_by_destination",
+ "tags": ["app"],
+ "summary": "Reset External Provider Config",
+ "operationId": "reset_external_provider_config",
"security": [
{
"HTTPBearer": []
@@ -8196,26 +8608,15 @@
],
"parameters": [
{
- "name": "queue_id",
- "in": "path",
- "required": true,
- "schema": {
- "type": "string",
- "description": "The queue id to query",
- "title": "Queue Id"
- },
- "description": "The queue id to query"
- },
- {
- "name": "destination",
+ "name": "provider_id",
"in": "path",
"required": true,
"schema": {
"type": "string",
- "description": "The destination to query",
- "title": "Destination"
+ "description": "The external provider identifier",
+ "title": "Provider Id"
},
- "description": "The destination to query"
+ "description": "The external provider identifier"
}
],
"responses": {
@@ -8224,7 +8625,7 @@
"content": {
"application/json": {
"schema": {
- "$ref": "#/components/schemas/DeleteByDestinationResult"
+ "$ref": "#/components/schemas/ExternalProviderConfigModel"
}
}
}
@@ -8242,80 +8643,53 @@
}
}
},
- "/api/v1/workflows/i/{workflow_id}": {
+ "/api/v1/app/logging": {
"get": {
- "tags": ["workflows"],
- "summary": "Get Workflow",
- "description": "Gets a workflow",
- "operationId": "get_workflow",
- "security": [
- {
- "HTTPBearer": []
- }
- ],
- "parameters": [
- {
- "name": "workflow_id",
- "in": "path",
- "required": true,
- "schema": {
- "type": "string",
- "description": "The workflow to get",
- "title": "Workflow Id"
- },
- "description": "The workflow to get"
- }
- ],
+ "tags": ["app"],
+ "summary": "Get Log Level",
+ "description": "Returns the log level",
+ "operationId": "get_log_level",
"responses": {
"200": {
- "description": "Successful Response",
- "content": {
- "application/json": {
- "schema": {
- "$ref": "#/components/schemas/WorkflowRecordWithThumbnailDTO"
- }
- }
- }
- },
- "422": {
- "description": "Validation Error",
+ "description": "The operation was successful",
"content": {
"application/json": {
"schema": {
- "$ref": "#/components/schemas/HTTPValidationError"
+ "$ref": "#/components/schemas/LogLevel"
}
}
}
}
- }
- },
- "patch": {
- "tags": ["workflows"],
- "summary": "Update Workflow",
- "description": "Updates a workflow",
- "operationId": "update_workflow",
+ },
"security": [
{
"HTTPBearer": []
}
- ],
+ ]
+ },
+ "post": {
+ "tags": ["app"],
+ "summary": "Set Log Level",
+ "description": "Sets the log verbosity level",
+ "operationId": "set_log_level",
"requestBody": {
- "required": true,
"content": {
"application/json": {
"schema": {
- "$ref": "#/components/schemas/Body_update_workflow"
+ "$ref": "#/components/schemas/LogLevel",
+ "description": "New log verbosity level"
}
}
- }
+ },
+ "required": true
},
"responses": {
"200": {
- "description": "Successful Response",
+ "description": "The operation was successful",
"content": {
"application/json": {
"schema": {
- "$ref": "#/components/schemas/WorkflowRecordDTO"
+ "$ref": "#/components/schemas/LogLevel"
}
}
}
@@ -8330,70 +8704,138 @@
}
}
}
- }
- },
- "delete": {
- "tags": ["workflows"],
- "summary": "Delete Workflow",
- "description": "Deletes a workflow",
- "operationId": "delete_workflow",
+ },
"security": [
{
"HTTPBearer": []
}
- ],
- "parameters": [
- {
- "name": "workflow_id",
- "in": "path",
- "required": true,
- "schema": {
- "type": "string",
- "description": "The workflow to delete",
- "title": "Workflow Id"
- },
- "description": "The workflow to delete"
- }
- ],
+ ]
+ }
+ },
+ "/api/v1/app/invocation_cache": {
+ "delete": {
+ "tags": ["app"],
+ "summary": "Clear Invocation Cache",
+ "description": "Clears the invocation cache",
+ "operationId": "clear_invocation_cache",
"responses": {
"200": {
- "description": "Successful Response",
+ "description": "The operation was successful",
"content": {
"application/json": {
"schema": {}
}
}
- },
- "422": {
- "description": "Validation Error",
+ }
+ },
+ "security": [
+ {
+ "HTTPBearer": []
+ }
+ ]
+ }
+ },
+ "/api/v1/app/invocation_cache/enable": {
+ "put": {
+ "tags": ["app"],
+ "summary": "Enable Invocation Cache",
+ "description": "Clears the invocation cache",
+ "operationId": "enable_invocation_cache",
+ "responses": {
+ "200": {
+ "description": "The operation was successful",
+ "content": {
+ "application/json": {
+ "schema": {}
+ }
+ }
+ }
+ },
+ "security": [
+ {
+ "HTTPBearer": []
+ }
+ ]
+ }
+ },
+ "/api/v1/app/invocation_cache/disable": {
+ "put": {
+ "tags": ["app"],
+ "summary": "Disable Invocation Cache",
+ "description": "Clears the invocation cache",
+ "operationId": "disable_invocation_cache",
+ "responses": {
+ "200": {
+ "description": "The operation was successful",
+ "content": {
+ "application/json": {
+ "schema": {}
+ }
+ }
+ }
+ },
+ "security": [
+ {
+ "HTTPBearer": []
+ }
+ ]
+ }
+ },
+ "/api/v1/app/invocation_cache/status": {
+ "get": {
+ "tags": ["app"],
+ "summary": "Get Invocation Cache Status",
+ "description": "Clears the invocation cache",
+ "operationId": "get_invocation_cache_status",
+ "responses": {
+ "200": {
+ "description": "Successful Response",
"content": {
"application/json": {
"schema": {
- "$ref": "#/components/schemas/HTTPValidationError"
+ "$ref": "#/components/schemas/InvocationCacheStatus"
}
}
}
}
- }
+ },
+ "security": [
+ {
+ "HTTPBearer": []
+ }
+ ]
}
},
- "/api/v1/workflows/": {
+ "/api/v1/queue/{queue_id}/enqueue_batch": {
"post": {
- "tags": ["workflows"],
- "summary": "Create Workflow",
- "description": "Creates a workflow",
- "operationId": "create_workflow",
+ "tags": ["queue"],
+ "summary": "Enqueue Batch",
+ "description": "Processes a batch and enqueues the output graphs for execution for the current user.",
+ "operationId": "enqueue_batch",
"security": [
{
"HTTPBearer": []
}
],
+ "parameters": [
+ {
+ "name": "queue_id",
+ "in": "path",
+ "required": true,
+ "schema": {
+ "type": "string",
+ "description": "The queue id to perform this operation on",
+ "title": "Queue Id"
+ },
+ "description": "The queue id to perform this operation on"
+ }
+ ],
"requestBody": {
"required": true,
"content": {
"application/json": {
"schema": {
- "$ref": "#/components/schemas/Body_create_workflow"
+ "$ref": "#/components/schemas/Body_enqueue_batch"
}
}
}
@@ -8404,11 +8846,21 @@
"content": {
"application/json": {
"schema": {
- "$ref": "#/components/schemas/WorkflowRecordDTO"
+ "$ref": "#/components/schemas/EnqueueBatchResult"
}
}
}
},
+ "201": {
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/EnqueueBatchResult"
+ }
+ }
+ },
+ "description": "Created"
+ },
"422": {
"description": "Validation Error",
"content": {
@@ -8420,12 +8872,14 @@
}
}
}
- },
+ }
+ },
+ "/api/v1/queue/{queue_id}/list_all": {
"get": {
- "tags": ["workflows"],
- "summary": "List Workflows",
- "description": "Gets a page of workflows",
- "operationId": "list_workflows",
+ "tags": ["queue"],
+ "summary": "List All Queue Items",
+ "description": "Gets all queue items",
+ "operationId": "list_all_queue_items",
"security": [
{
"HTTPBearer": []
@@ -8433,170 +8887,96 @@
],
"parameters": [
{
- "name": "page",
- "in": "query",
- "required": false,
- "schema": {
- "type": "integer",
- "description": "The page to get",
- "default": 0,
- "title": "Page"
- },
- "description": "The page to get"
- },
- {
- "name": "per_page",
- "in": "query",
- "required": false,
- "schema": {
- "anyOf": [
- {
- "type": "integer"
- },
- {
- "type": "null"
- }
- ],
- "description": "The number of workflows per page",
- "title": "Per Page"
- },
- "description": "The number of workflows per page"
- },
- {
- "name": "order_by",
- "in": "query",
- "required": false,
- "schema": {
- "$ref": "#/components/schemas/WorkflowRecordOrderBy",
- "description": "The attribute to order by",
- "default": "name"
- },
- "description": "The attribute to order by"
- },
- {
- "name": "direction",
- "in": "query",
- "required": false,
+ "name": "queue_id",
+ "in": "path",
+ "required": true,
"schema": {
- "$ref": "#/components/schemas/SQLiteDirection",
- "description": "The direction to order by",
- "default": "ASC"
+ "type": "string",
+ "description": "The queue id to perform this operation on",
+ "title": "Queue Id"
},
- "description": "The direction to order by"
+ "description": "The queue id to perform this operation on"
},
{
- "name": "categories",
+ "name": "destination",
"in": "query",
"required": false,
"schema": {
"anyOf": [
{
- "type": "array",
- "items": {
- "$ref": "#/components/schemas/WorkflowCategory"
- }
+ "type": "string"
},
{
"type": "null"
}
],
- "description": "The categories of workflow to get",
- "title": "Categories"
+ "description": "The destination of queue items to fetch",
+ "title": "Destination"
},
- "description": "The categories of workflow to get"
- },
- {
- "name": "tags",
- "in": "query",
- "required": false,
- "schema": {
- "anyOf": [
- {
+ "description": "The destination of queue items to fetch"
+ }
+ ],
+ "responses": {
+ "200": {
+ "description": "Successful Response",
+ "content": {
+ "application/json": {
+ "schema": {
"type": "array",
"items": {
- "type": "string"
- }
- },
- {
- "type": "null"
+ "$ref": "#/components/schemas/SessionQueueItem"
+ },
+ "title": "Response 200 List All Queue Items"
}
- ],
- "description": "The tags of workflow to get",
- "title": "Tags"
- },
- "description": "The tags of workflow to get"
+ }
+ }
},
- {
- "name": "query",
- "in": "query",
- "required": false,
- "schema": {
- "anyOf": [
- {
- "type": "string"
- },
- {
- "type": "null"
+ "422": {
+ "description": "Validation Error",
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/HTTPValidationError"
}
- ],
- "description": "The text to query by (matches name and description)",
- "title": "Query"
- },
- "description": "The text to query by (matches name and description)"
- },
+ }
+ }
+ }
+ }
+ }
+ },
+ "/api/v1/queue/{queue_id}/item_ids": {
+ "get": {
+ "tags": ["queue"],
+ "summary": "Get Queue Item Ids",
+ "description": "Gets all queue item ids that match the given parameters.\n\nIDs for every user's items are returned (item ids carry no sensitive data on their own).\nWhen the corresponding items are hydrated via get_queue_items_by_item_ids, those belonging\nto other users are redacted by sanitize_queue_item_for_user. This lets a non-admin see\npartially-redacted entries for other users' jobs in the queue list, while still revealing\nonly timestamps and status for items they do not own.\n\ncurrent_user is required so the endpoint stays behind authentication in multiuser mode.",
+ "operationId": "get_queue_item_ids",
+ "security": [
{
- "name": "has_been_opened",
- "in": "query",
- "required": false,
- "schema": {
- "anyOf": [
- {
- "type": "boolean"
- },
- {
- "type": "null"
- }
- ],
- "description": "Whether to include/exclude recent workflows",
- "title": "Has Been Opened"
- },
- "description": "Whether to include/exclude recent workflows"
- },
+ "HTTPBearer": []
+ }
+ ],
+ "parameters": [
{
- "name": "is_public",
- "in": "query",
- "required": false,
+ "name": "queue_id",
+ "in": "path",
+ "required": true,
"schema": {
- "anyOf": [
- {
- "type": "boolean"
- },
- {
- "type": "null"
- }
- ],
- "description": "Filter by public/shared status",
- "title": "Is Public"
+ "type": "string",
+ "description": "The queue id to perform this operation on",
+ "title": "Queue Id"
},
- "description": "Filter by public/shared status"
+ "description": "The queue id to perform this operation on"
},
{
- "name": "callable",
+ "name": "order_dir",
"in": "query",
"required": false,
"schema": {
- "anyOf": [
- {
- "type": "boolean"
- },
- {
- "type": "null"
- }
- ],
- "description": "Filter by whether workflows are callable by call_saved_workflow",
- "title": "Callable"
+ "$ref": "#/components/schemas/SQLiteDirection",
+ "description": "The order of sort",
+ "default": "DESC"
},
- "description": "Filter by whether workflows are callable by call_saved_workflow"
+ "description": "The order of sort"
}
],
"responses": {
@@ -8605,7 +8985,7 @@
"content": {
"application/json": {
"schema": {
- "$ref": "#/components/schemas/PaginatedResults_WorkflowRecordListItemWithThumbnailDTO_"
+ "$ref": "#/components/schemas/ItemIdsResult"
}
}
}
@@ -8623,12 +9003,12 @@
}
}
},
- "/api/v1/workflows/i/{workflow_id}/thumbnail": {
- "put": {
- "tags": ["workflows"],
- "summary": "Set Workflow Thumbnail",
- "description": "Sets a workflow's thumbnail image",
- "operationId": "set_workflow_thumbnail",
+ "/api/v1/queue/{queue_id}/items_by_ids": {
+ "post": {
+ "tags": ["queue"],
+ "summary": "Get Queue Items By Item Ids",
+ "description": "Gets queue items for the specified queue item ids. Maintains order of item ids.",
+ "operationId": "get_queue_items_by_item_ids",
"security": [
{
"HTTPBearer": []
@@ -8636,23 +9016,23 @@
],
"parameters": [
{
- "name": "workflow_id",
+ "name": "queue_id",
"in": "path",
"required": true,
"schema": {
"type": "string",
- "description": "The workflow to update",
- "title": "Workflow Id"
+ "description": "The queue id to perform this operation on",
+ "title": "Queue Id"
},
- "description": "The workflow to update"
+ "description": "The queue id to perform this operation on"
}
],
"requestBody": {
"required": true,
"content": {
- "multipart/form-data": {
+ "application/json": {
"schema": {
- "$ref": "#/components/schemas/Body_set_workflow_thumbnail"
+ "$ref": "#/components/schemas/Body_get_queue_items_by_item_ids"
}
}
}
@@ -8663,7 +9043,11 @@
"content": {
"application/json": {
"schema": {
- "$ref": "#/components/schemas/WorkflowRecordDTO"
+ "type": "array",
+ "items": {
+ "$ref": "#/components/schemas/SessionQueueItem"
+ },
+ "title": "Response 200 Get Queue Items By Item Ids"
}
}
}
@@ -8679,12 +9063,14 @@
}
}
}
- },
- "delete": {
- "tags": ["workflows"],
- "summary": "Delete Workflow Thumbnail",
- "description": "Removes a workflow's thumbnail image",
- "operationId": "delete_workflow_thumbnail",
+ }
+ },
+ "/api/v1/queue/{queue_id}/processor/resume": {
+ "put": {
+ "tags": ["queue"],
+ "summary": "Resume",
+ "description": "Resumes session processor. Admin only.",
+ "operationId": "resume",
"security": [
{
"HTTPBearer": []
@@ -8692,15 +9078,15 @@
],
"parameters": [
{
- "name": "workflow_id",
+ "name": "queue_id",
"in": "path",
"required": true,
"schema": {
"type": "string",
- "description": "The workflow to update",
- "title": "Workflow Id"
+ "description": "The queue id to perform this operation on",
+ "title": "Queue Id"
},
- "description": "The workflow to update"
+ "description": "The queue id to perform this operation on"
}
],
"responses": {
@@ -8709,7 +9095,7 @@
"content": {
"application/json": {
"schema": {
- "$ref": "#/components/schemas/WorkflowRecordDTO"
+ "$ref": "#/components/schemas/SessionProcessorStatus"
}
}
}
@@ -8725,40 +9111,43 @@
}
}
}
- },
- "get": {
- "tags": ["workflows"],
- "summary": "Get Workflow Thumbnail",
- "description": "Gets a workflow's thumbnail image.\n\nThis endpoint is intentionally unauthenticated because browsers load images\nvia tags which cannot send Bearer tokens. Workflow IDs are UUIDs,\nproviding security through unguessability.",
- "operationId": "get_workflow_thumbnail",
+ }
+ },
+ "/api/v1/queue/{queue_id}/processor/pause": {
+ "put": {
+ "tags": ["queue"],
+ "summary": "Pause",
+ "description": "Pauses session processor. Admin only.",
+ "operationId": "pause",
+ "security": [
+ {
+ "HTTPBearer": []
+ }
+ ],
"parameters": [
{
- "name": "workflow_id",
+ "name": "queue_id",
"in": "path",
"required": true,
"schema": {
"type": "string",
- "description": "The id of the workflow thumbnail to get",
- "title": "Workflow Id"
+ "description": "The queue id to perform this operation on",
+ "title": "Queue Id"
},
- "description": "The id of the workflow thumbnail to get"
+ "description": "The queue id to perform this operation on"
}
],
"responses": {
"200": {
- "description": "The workflow thumbnail was fetched successfully",
+ "description": "Successful Response",
"content": {
"application/json": {
- "schema": {}
+ "schema": {
+ "$ref": "#/components/schemas/SessionProcessorStatus"
+ }
}
}
},
- "400": {
- "description": "Bad request"
- },
- "404": {
- "description": "The workflow thumbnail could not be found"
- },
"422": {
"description": "Validation Error",
"content": {
@@ -8772,12 +9161,12 @@
}
}
},
- "/api/v1/workflows/i/{workflow_id}/is_public": {
- "patch": {
- "tags": ["workflows"],
- "summary": "Update Workflow Is Public",
- "description": "Updates whether a workflow is shared publicly",
- "operationId": "update_workflow_is_public",
+ "/api/v1/queue/{queue_id}/cancel_all_except_current": {
+ "put": {
+ "tags": ["queue"],
+ "summary": "Cancel All Except Current",
+ "description": "Immediately cancels all queue items except in-processing items. Non-admin users can only cancel their own items.",
+ "operationId": "cancel_all_except_current",
"security": [
{
"HTTPBearer": []
@@ -8785,34 +9174,24 @@
],
"parameters": [
{
- "name": "workflow_id",
+ "name": "queue_id",
"in": "path",
"required": true,
"schema": {
"type": "string",
- "description": "The workflow to update",
- "title": "Workflow Id"
+ "description": "The queue id to perform this operation on",
+ "title": "Queue Id"
},
- "description": "The workflow to update"
+ "description": "The queue id to perform this operation on"
}
],
- "requestBody": {
- "required": true,
- "content": {
- "application/json": {
- "schema": {
- "$ref": "#/components/schemas/Body_update_workflow_is_public"
- }
- }
- }
- },
"responses": {
"200": {
"description": "Successful Response",
"content": {
"application/json": {
"schema": {
- "$ref": "#/components/schemas/WorkflowRecordDTO"
+ "$ref": "#/components/schemas/CancelAllExceptCurrentResult"
}
}
}
@@ -8830,12 +9209,12 @@
}
}
},
- "/api/v1/workflows/tags": {
- "get": {
- "tags": ["workflows"],
- "summary": "Get All Tags",
- "description": "Gets all unique tags from workflows",
- "operationId": "get_all_tags",
+ "/api/v1/queue/{queue_id}/delete_all_except_current": {
+ "put": {
+ "tags": ["queue"],
+ "summary": "Delete All Except Current",
+ "description": "Immediately deletes all queue items except in-processing items. Non-admin users can only delete their own items.",
+ "operationId": "delete_all_except_current",
"security": [
{
"HTTPBearer": []
@@ -8843,43 +9222,15 @@
],
"parameters": [
{
- "name": "categories",
- "in": "query",
- "required": false,
- "schema": {
- "anyOf": [
- {
- "type": "array",
- "items": {
- "$ref": "#/components/schemas/WorkflowCategory"
- }
- },
- {
- "type": "null"
- }
- ],
- "description": "The categories to include",
- "title": "Categories"
- },
- "description": "The categories to include"
- },
- {
- "name": "is_public",
- "in": "query",
- "required": false,
+ "name": "queue_id",
+ "in": "path",
+ "required": true,
"schema": {
- "anyOf": [
- {
- "type": "boolean"
- },
- {
- "type": "null"
- }
- ],
- "description": "Filter by public/shared status",
- "title": "Is Public"
+ "type": "string",
+ "description": "The queue id to perform this operation on",
+ "title": "Queue Id"
},
- "description": "Filter by public/shared status"
+ "description": "The queue id to perform this operation on"
}
],
"responses": {
@@ -8888,11 +9239,7 @@
"content": {
"application/json": {
"schema": {
- "type": "array",
- "items": {
- "type": "string"
- },
- "title": "Response Get All Tags"
+ "$ref": "#/components/schemas/DeleteAllExceptCurrentResult"
}
}
}
@@ -8910,101 +9257,47 @@
}
}
},
- "/api/v1/workflows/counts_by_tag": {
- "get": {
- "tags": ["workflows"],
- "summary": "Get Counts By Tag",
- "description": "Counts workflows by tag",
- "operationId": "get_counts_by_tag",
- "security": [
- {
- "HTTPBearer": []
+ "/api/v1/queue/{queue_id}/cancel_by_batch_ids": {
+ "put": {
+ "tags": ["queue"],
+ "summary": "Cancel By Batch Ids",
+ "description": "Immediately cancels all queue items from the given batch ids. Non-admin users can only cancel their own items.",
+ "operationId": "cancel_by_batch_ids",
+ "security": [
+ {
+ "HTTPBearer": []
}
],
"parameters": [
{
- "name": "tags",
- "in": "query",
+ "name": "queue_id",
+ "in": "path",
"required": true,
"schema": {
- "type": "array",
- "items": {
- "type": "string"
- },
- "description": "The tags to get counts for",
- "title": "Tags"
- },
- "description": "The tags to get counts for"
- },
- {
- "name": "categories",
- "in": "query",
- "required": false,
- "schema": {
- "anyOf": [
- {
- "type": "array",
- "items": {
- "$ref": "#/components/schemas/WorkflowCategory"
- }
- },
- {
- "type": "null"
- }
- ],
- "description": "The categories to include",
- "title": "Categories"
- },
- "description": "The categories to include"
- },
- {
- "name": "has_been_opened",
- "in": "query",
- "required": false,
- "schema": {
- "anyOf": [
- {
- "type": "boolean"
- },
- {
- "type": "null"
- }
- ],
- "description": "Whether to include/exclude recent workflows",
- "title": "Has Been Opened"
- },
- "description": "Whether to include/exclude recent workflows"
- },
- {
- "name": "is_public",
- "in": "query",
- "required": false,
- "schema": {
- "anyOf": [
- {
- "type": "boolean"
- },
- {
- "type": "null"
- }
- ],
- "description": "Filter by public/shared status",
- "title": "Is Public"
+ "type": "string",
+ "description": "The queue id to perform this operation on",
+ "title": "Queue Id"
},
- "description": "Filter by public/shared status"
+ "description": "The queue id to perform this operation on"
}
],
+ "requestBody": {
+ "required": true,
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/Body_cancel_by_batch_ids"
+ }
+ }
+ }
+ },
"responses": {
"200": {
"description": "Successful Response",
"content": {
"application/json": {
"schema": {
- "type": "object",
- "additionalProperties": {
- "type": "integer"
- },
- "title": "Response Get Counts By Tag"
+ "$ref": "#/components/schemas/CancelByBatchIDsResult"
}
}
}
@@ -9022,12 +9315,12 @@
}
}
},
- "/api/v1/workflows/counts_by_category": {
- "get": {
- "tags": ["workflows"],
- "summary": "Counts By Category",
- "description": "Counts workflows by category",
- "operationId": "counts_by_category",
+ "/api/v1/queue/{queue_id}/cancel_by_destination": {
+ "put": {
+ "tags": ["queue"],
+ "summary": "Cancel By Destination",
+ "description": "Immediately cancels all queue items with the given destination. Non-admin users can only cancel their own items.",
+ "operationId": "cancel_by_destination",
"security": [
{
"HTTPBearer": []
@@ -9035,54 +9328,26 @@
],
"parameters": [
{
- "name": "categories",
- "in": "query",
+ "name": "queue_id",
+ "in": "path",
"required": true,
"schema": {
- "type": "array",
- "items": {
- "$ref": "#/components/schemas/WorkflowCategory"
- },
- "description": "The categories to include",
- "title": "Categories"
- },
- "description": "The categories to include"
- },
- {
- "name": "has_been_opened",
- "in": "query",
- "required": false,
- "schema": {
- "anyOf": [
- {
- "type": "boolean"
- },
- {
- "type": "null"
- }
- ],
- "description": "Whether to include/exclude recent workflows",
- "title": "Has Been Opened"
+ "type": "string",
+ "description": "The queue id to perform this operation on",
+ "title": "Queue Id"
},
- "description": "Whether to include/exclude recent workflows"
+ "description": "The queue id to perform this operation on"
},
{
- "name": "is_public",
+ "name": "destination",
"in": "query",
- "required": false,
+ "required": true,
"schema": {
- "anyOf": [
- {
- "type": "boolean"
- },
- {
- "type": "null"
- }
- ],
- "description": "Filter by public/shared status",
- "title": "Is Public"
+ "type": "string",
+ "description": "The destination to cancel all queue items for",
+ "title": "Destination"
},
- "description": "Filter by public/shared status"
+ "description": "The destination to cancel all queue items for"
}
],
"responses": {
@@ -9091,11 +9356,7 @@
"content": {
"application/json": {
"schema": {
- "type": "object",
- "additionalProperties": {
- "type": "integer"
- },
- "title": "Response Counts By Category"
+ "$ref": "#/components/schemas/CancelByDestinationResult"
}
}
}
@@ -9113,12 +9374,12 @@
}
}
},
- "/api/v1/workflows/i/{workflow_id}/opened_at": {
+ "/api/v1/queue/{queue_id}/retry_items_by_id": {
"put": {
- "tags": ["workflows"],
- "summary": "Update Opened At",
- "description": "Updates the opened_at field of a workflow",
- "operationId": "update_opened_at",
+ "tags": ["queue"],
+ "summary": "Retry Items By Id",
+ "description": "Retries the given queue items. Users can only retry their own items unless they are an admin.",
+ "operationId": "retry_items_by_id",
"security": [
{
"HTTPBearer": []
@@ -9126,23 +9387,40 @@
],
"parameters": [
{
- "name": "workflow_id",
+ "name": "queue_id",
"in": "path",
"required": true,
"schema": {
"type": "string",
- "description": "The workflow to update",
- "title": "Workflow Id"
+ "description": "The queue id to perform this operation on",
+ "title": "Queue Id"
},
- "description": "The workflow to update"
+ "description": "The queue id to perform this operation on"
}
],
+ "requestBody": {
+ "required": true,
+ "content": {
+ "application/json": {
+ "schema": {
+ "type": "array",
+ "items": {
+ "type": "integer"
+ },
+ "description": "The queue item ids to retry",
+ "title": "Item Ids"
+ }
+ }
+ }
+ },
"responses": {
"200": {
"description": "Successful Response",
"content": {
"application/json": {
- "schema": {}
+ "schema": {
+ "$ref": "#/components/schemas/RetryItemsResult"
+ }
}
}
},
@@ -9159,12 +9437,12 @@
}
}
},
- "/api/v1/style_presets/i/{style_preset_id}": {
- "get": {
- "tags": ["style_presets"],
- "summary": "Get Style Preset",
- "description": "Gets a style preset",
- "operationId": "get_style_preset",
+ "/api/v1/queue/{queue_id}/clear": {
+ "put": {
+ "tags": ["queue"],
+ "summary": "Clear",
+ "description": "Clears the queue entirely. Admin users clear all items; non-admin users only clear their own items. If there's a currently-executing item, users can only cancel it if they own it or are an admin.",
+ "operationId": "clear",
"security": [
{
"HTTPBearer": []
@@ -9172,15 +9450,15 @@
],
"parameters": [
{
- "name": "style_preset_id",
+ "name": "queue_id",
"in": "path",
"required": true,
"schema": {
"type": "string",
- "description": "The style preset to get",
- "title": "Style Preset Id"
+ "description": "The queue id to perform this operation on",
+ "title": "Queue Id"
},
- "description": "The style preset to get"
+ "description": "The queue id to perform this operation on"
}
],
"responses": {
@@ -9189,7 +9467,7 @@
"content": {
"application/json": {
"schema": {
- "$ref": "#/components/schemas/StylePresetRecordWithImage"
+ "$ref": "#/components/schemas/ClearResult"
}
}
}
@@ -9205,12 +9483,14 @@
}
}
}
- },
- "patch": {
- "tags": ["style_presets"],
- "summary": "Update Style Preset",
- "description": "Updates a style preset",
- "operationId": "update_style_preset",
+ }
+ },
+ "/api/v1/queue/{queue_id}/prune": {
+ "put": {
+ "tags": ["queue"],
+ "summary": "Prune",
+ "description": "Prunes all completed or errored queue items. Non-admin users can only prune their own items.",
+ "operationId": "prune",
"security": [
{
"HTTPBearer": []
@@ -9218,34 +9498,24 @@
],
"parameters": [
{
- "name": "style_preset_id",
+ "name": "queue_id",
"in": "path",
"required": true,
"schema": {
"type": "string",
- "description": "The id of the style preset to update",
- "title": "Style Preset Id"
+ "description": "The queue id to perform this operation on",
+ "title": "Queue Id"
},
- "description": "The id of the style preset to update"
+ "description": "The queue id to perform this operation on"
}
],
- "requestBody": {
- "required": true,
- "content": {
- "multipart/form-data": {
- "schema": {
- "$ref": "#/components/schemas/Body_update_style_preset"
- }
- }
- }
- },
"responses": {
"200": {
"description": "Successful Response",
"content": {
"application/json": {
"schema": {
- "$ref": "#/components/schemas/StylePresetRecordWithImage"
+ "$ref": "#/components/schemas/PruneResult"
}
}
}
@@ -9261,12 +9531,14 @@
}
}
}
- },
- "delete": {
- "tags": ["style_presets"],
- "summary": "Delete Style Preset",
- "description": "Deletes a style preset",
- "operationId": "delete_style_preset",
+ }
+ },
+ "/api/v1/queue/{queue_id}/current": {
+ "get": {
+ "tags": ["queue"],
+ "summary": "Get Current Queue Item",
+ "description": "Gets the currently execution queue item",
+ "operationId": "get_current_queue_item",
"security": [
{
"HTTPBearer": []
@@ -9274,15 +9546,15 @@
],
"parameters": [
{
- "name": "style_preset_id",
+ "name": "queue_id",
"in": "path",
"required": true,
"schema": {
"type": "string",
- "description": "The style preset to delete",
- "title": "Style Preset Id"
+ "description": "The queue id to perform this operation on",
+ "title": "Queue Id"
},
- "description": "The style preset to delete"
+ "description": "The queue id to perform this operation on"
}
],
"responses": {
@@ -9290,7 +9562,23 @@
"description": "Successful Response",
"content": {
"application/json": {
- "schema": {}
+ "schema": {
+ "anyOf": [
+ {
+ "$ref": "#/components/schemas/SessionQueueItem"
+ },
+ {
+ "type": "null"
+ },
+ {
+ "$ref": "#/components/schemas/SessionQueueItem"
+ },
+ {
+ "type": "null"
+ }
+ ],
+ "title": "Response 200 Get Current Queue Item"
+ }
}
}
},
@@ -9307,56 +9595,51 @@
}
}
},
- "/api/v1/style_presets/": {
+ "/api/v1/queue/{queue_id}/next": {
"get": {
- "tags": ["style_presets"],
- "summary": "List Style Presets",
- "description": "Gets the style presets visible to the current user.",
- "operationId": "list_style_presets",
- "responses": {
- "200": {
- "description": "Successful Response",
- "content": {
- "application/json": {
- "schema": {
- "items": {
- "$ref": "#/components/schemas/StylePresetRecordWithImage"
- },
- "type": "array",
- "title": "Response 200 List Style Presets"
- }
- }
- }
- }
- },
+ "tags": ["queue"],
+ "summary": "Get Next Queue Item",
+ "description": "Gets the next queue item, without executing it",
+ "operationId": "get_next_queue_item",
"security": [
{
"HTTPBearer": []
}
- ]
- },
- "post": {
- "tags": ["style_presets"],
- "summary": "Create Style Preset",
- "description": "Creates a style preset",
- "operationId": "create_style_preset",
- "requestBody": {
- "content": {
- "multipart/form-data": {
- "schema": {
- "$ref": "#/components/schemas/Body_create_style_preset"
- }
- }
- },
- "required": true
- },
+ ],
+ "parameters": [
+ {
+ "name": "queue_id",
+ "in": "path",
+ "required": true,
+ "schema": {
+ "type": "string",
+ "description": "The queue id to perform this operation on",
+ "title": "Queue Id"
+ },
+ "description": "The queue id to perform this operation on"
+ }
+ ],
"responses": {
"200": {
"description": "Successful Response",
"content": {
"application/json": {
"schema": {
- "$ref": "#/components/schemas/StylePresetRecordWithImage"
+ "anyOf": [
+ {
+ "$ref": "#/components/schemas/SessionQueueItem"
+ },
+ {
+ "type": "null"
+ },
+ {
+ "$ref": "#/components/schemas/SessionQueueItem"
+ },
+ {
+ "type": "null"
+ }
+ ],
+ "title": "Response 200 Get Next Queue Item"
}
}
}
@@ -9371,20 +9654,15 @@
}
}
}
- },
- "security": [
- {
- "HTTPBearer": []
- }
- ]
+ }
}
},
- "/api/v1/style_presets/i/{style_preset_id}/image": {
+ "/api/v1/queue/{queue_id}/status": {
"get": {
- "tags": ["style_presets"],
- "summary": "Get Style Preset Image",
- "description": "Gets an image file that previews the model",
- "operationId": "get_style_preset_image",
+ "tags": ["queue"],
+ "summary": "Get Queue Status",
+ "description": "Gets the status of the session queue. Returns global counts; every user additionally gets\ntheir own pending/in_progress counts (so the UI can show an X/Y badge and scope personal UI\nlike the progress bar to the user's own activity). Non-admin users cannot see the current\nitem's identifiers unless they own it.",
+ "operationId": "get_queue_status",
"security": [
{
"HTTPBearer": []
@@ -9392,32 +9670,28 @@
],
"parameters": [
{
- "name": "style_preset_id",
+ "name": "queue_id",
"in": "path",
"required": true,
"schema": {
"type": "string",
- "description": "The id of the style preset image to get",
- "title": "Style Preset Id"
+ "description": "The queue id to perform this operation on",
+ "title": "Queue Id"
},
- "description": "The id of the style preset image to get"
+ "description": "The queue id to perform this operation on"
}
],
"responses": {
"200": {
- "description": "The style preset image was fetched successfully",
+ "description": "Successful Response",
"content": {
"application/json": {
- "schema": {}
+ "schema": {
+ "$ref": "#/components/schemas/SessionQueueAndProcessorStatus"
+ }
}
}
},
- "400": {
- "description": "Bad request"
- },
- "404": {
- "description": "The style preset image could not be found"
- },
"422": {
"description": "Validation Error",
"content": {
@@ -9431,50 +9705,49 @@
}
}
},
- "/api/v1/style_presets/export": {
+ "/api/v1/queue/{queue_id}/b/{batch_id}/status": {
"get": {
- "tags": ["style_presets"],
- "summary": "Export Style Presets",
- "operationId": "export_style_presets",
- "responses": {
- "200": {
- "description": "A CSV file with the requested data.",
- "content": {
- "application/json": {
- "schema": {}
- },
- "text/csv": {}
- }
- }
- },
+ "tags": ["queue"],
+ "summary": "Get Batch Status",
+ "description": "Gets the status of a batch. Non-admin users only see their own batches.",
+ "operationId": "get_batch_status",
"security": [
{
"HTTPBearer": []
}
- ]
- }
- },
- "/api/v1/style_presets/import": {
- "post": {
- "tags": ["style_presets"],
- "summary": "Import Style Presets",
- "operationId": "import_style_presets",
- "requestBody": {
- "content": {
- "multipart/form-data": {
- "schema": {
- "$ref": "#/components/schemas/Body_import_style_presets"
- }
- }
+ ],
+ "parameters": [
+ {
+ "name": "queue_id",
+ "in": "path",
+ "required": true,
+ "schema": {
+ "type": "string",
+ "description": "The queue id to perform this operation on",
+ "title": "Queue Id"
+ },
+ "description": "The queue id to perform this operation on"
},
- "required": true
- },
+ {
+ "name": "batch_id",
+ "in": "path",
+ "required": true,
+ "schema": {
+ "type": "string",
+ "description": "The batch to get the status of",
+ "title": "Batch Id"
+ },
+ "description": "The batch to get the status of"
+ }
+ ],
"responses": {
"200": {
"description": "Successful Response",
"content": {
"application/json": {
- "schema": {}
+ "schema": {
+ "$ref": "#/components/schemas/BatchStatus"
+ }
}
}
},
@@ -9488,20 +9761,15 @@
}
}
}
- },
- "security": [
- {
- "HTTPBearer": []
- }
- ]
+ }
}
},
- "/api/v1/client_state/{queue_id}/get_by_key": {
+ "/api/v1/queue/{queue_id}/i/{item_id}": {
"get": {
- "tags": ["client_state"],
- "summary": "Get Client State By Key",
- "description": "Gets the client state for the current user (or system user if not authenticated)",
- "operationId": "get_client_state_by_key",
+ "tags": ["queue"],
+ "summary": "Get Queue Item",
+ "description": "Gets a queue item",
+ "operationId": "get_queue_item",
"security": [
{
"HTTPBearer": []
@@ -9514,21 +9782,21 @@
"required": true,
"schema": {
"type": "string",
- "description": "The queue id (ignored, kept for backwards compatibility)",
+ "description": "The queue id to perform this operation on",
"title": "Queue Id"
},
- "description": "The queue id (ignored, kept for backwards compatibility)"
+ "description": "The queue id to perform this operation on"
},
{
- "name": "key",
- "in": "query",
+ "name": "item_id",
+ "in": "path",
"required": true,
"schema": {
- "type": "string",
- "description": "Key to get",
- "title": "Key"
+ "type": "integer",
+ "description": "The queue item to get",
+ "title": "Item Id"
},
- "description": "Key to get"
+ "description": "The queue item to get"
}
],
"responses": {
@@ -9537,15 +9805,7 @@
"content": {
"application/json": {
"schema": {
- "anyOf": [
- {
- "type": "string"
- },
- {
- "type": "null"
- }
- ],
- "title": "Response Get Client State By Key"
+ "$ref": "#/components/schemas/SessionQueueItem"
}
}
}
@@ -9561,14 +9821,12 @@
}
}
}
- }
- },
- "/api/v1/client_state/{queue_id}/set_by_key": {
- "post": {
- "tags": ["client_state"],
- "summary": "Set Client State",
- "description": "Sets the client state for the current user (or system user if not authenticated)",
- "operationId": "set_client_state",
+ },
+ "delete": {
+ "tags": ["queue"],
+ "summary": "Delete Queue Item",
+ "description": "Deletes a queue item. Users can only delete their own items unless they are an admin.",
+ "operationId": "delete_queue_item",
"security": [
{
"HTTPBearer": []
@@ -9581,44 +9839,29 @@
"required": true,
"schema": {
"type": "string",
- "description": "The queue id (ignored, kept for backwards compatibility)",
+ "description": "The queue id to perform this operation on",
"title": "Queue Id"
},
- "description": "The queue id (ignored, kept for backwards compatibility)"
+ "description": "The queue id to perform this operation on"
},
{
- "name": "key",
- "in": "query",
+ "name": "item_id",
+ "in": "path",
"required": true,
"schema": {
- "type": "string",
- "description": "Key to set",
- "title": "Key"
+ "type": "integer",
+ "description": "The queue item to delete",
+ "title": "Item Id"
},
- "description": "Key to set"
+ "description": "The queue item to delete"
}
],
- "requestBody": {
- "required": true,
- "content": {
- "application/json": {
- "schema": {
- "type": "string",
- "description": "Stringified value to set",
- "title": "Value"
- }
- }
- }
- },
"responses": {
"200": {
"description": "Successful Response",
"content": {
"application/json": {
- "schema": {
- "type": "string",
- "title": "Response Set Client State"
- }
+ "schema": {}
}
}
},
@@ -9635,12 +9878,12 @@
}
}
},
- "/api/v1/client_state/{queue_id}/get_keys_by_prefix": {
- "get": {
- "tags": ["client_state"],
- "summary": "Get Client State Keys By Prefix",
- "description": "Gets client state keys matching a prefix for the current user",
- "operationId": "get_client_state_keys_by_prefix",
+ "/api/v1/queue/{queue_id}/i/{item_id}/cancel": {
+ "put": {
+ "tags": ["queue"],
+ "summary": "Cancel Queue Item",
+ "description": "Cancels a queue item. Users can only cancel their own items unless they are an admin.",
+ "operationId": "cancel_queue_item",
"security": [
{
"HTTPBearer": []
@@ -9653,21 +9896,21 @@
"required": true,
"schema": {
"type": "string",
- "description": "The queue id (ignored, kept for backwards compatibility)",
+ "description": "The queue id to perform this operation on",
"title": "Queue Id"
},
- "description": "The queue id (ignored, kept for backwards compatibility)"
+ "description": "The queue id to perform this operation on"
},
{
- "name": "prefix",
- "in": "query",
+ "name": "item_id",
+ "in": "path",
"required": true,
"schema": {
- "type": "string",
- "description": "Prefix to filter keys by",
- "title": "Prefix"
+ "type": "integer",
+ "description": "The queue item to cancel",
+ "title": "Item Id"
},
- "description": "Prefix to filter keys by"
+ "description": "The queue item to cancel"
}
],
"responses": {
@@ -9676,11 +9919,7 @@
"content": {
"application/json": {
"schema": {
- "type": "array",
- "items": {
- "type": "string"
- },
- "title": "Response Get Client State Keys By Prefix"
+ "$ref": "#/components/schemas/SessionQueueItem"
}
}
}
@@ -9698,12 +9937,12 @@
}
}
},
- "/api/v1/client_state/{queue_id}/delete_by_key": {
- "post": {
- "tags": ["client_state"],
- "summary": "Delete Client State By Key",
- "description": "Deletes a specific client state key for the current user",
- "operationId": "delete_client_state_by_key",
+ "/api/v1/queue/{queue_id}/counts_by_destination": {
+ "get": {
+ "tags": ["queue"],
+ "summary": "Counts By Destination",
+ "description": "Gets the counts of queue items by destination. Non-admin users only see their own items.",
+ "operationId": "counts_by_destination",
"security": [
{
"HTTPBearer": []
@@ -9716,21 +9955,21 @@
"required": true,
"schema": {
"type": "string",
- "description": "The queue id (ignored, kept for backwards compatibility)",
+ "description": "The queue id to query",
"title": "Queue Id"
},
- "description": "The queue id (ignored, kept for backwards compatibility)"
+ "description": "The queue id to query"
},
{
- "name": "key",
+ "name": "destination",
"in": "query",
"required": true,
"schema": {
"type": "string",
- "description": "Key to delete",
- "title": "Key"
+ "description": "The destination to query",
+ "title": "Destination"
},
- "description": "Key to delete"
+ "description": "The destination to query"
}
],
"responses": {
@@ -9738,13 +9977,12 @@
"description": "Successful Response",
"content": {
"application/json": {
- "schema": {}
+ "schema": {
+ "$ref": "#/components/schemas/SessionQueueCountsByDestination"
+ }
}
}
},
- "204": {
- "description": "Client state key deleted"
- },
"422": {
"description": "Validation Error",
"content": {
@@ -9758,12 +9996,12 @@
}
}
},
- "/api/v1/client_state/{queue_id}/delete": {
- "post": {
- "tags": ["client_state"],
- "summary": "Delete Client State",
- "description": "Deletes the client state for the current user (or system user if not authenticated)",
- "operationId": "delete_client_state",
+ "/api/v1/queue/{queue_id}/d/{destination}": {
+ "delete": {
+ "tags": ["queue"],
+ "summary": "Delete By Destination",
+ "description": "Deletes all items with the given destination. Non-admin users can only delete their own items.",
+ "operationId": "delete_by_destination",
"security": [
{
"HTTPBearer": []
@@ -9776,10 +10014,21 @@
"required": true,
"schema": {
"type": "string",
- "description": "The queue id (ignored, kept for backwards compatibility)",
+ "description": "The queue id to query",
"title": "Queue Id"
},
- "description": "The queue id (ignored, kept for backwards compatibility)"
+ "description": "The queue id to query"
+ },
+ {
+ "name": "destination",
+ "in": "path",
+ "required": true,
+ "schema": {
+ "type": "string",
+ "description": "The destination to query",
+ "title": "Destination"
+ },
+ "description": "The destination to query"
}
],
"responses": {
@@ -9787,13 +10036,12 @@
"description": "Successful Response",
"content": {
"application/json": {
- "schema": {}
+ "schema": {
+ "$ref": "#/components/schemas/DeleteByDestinationResult"
+ }
}
}
},
- "204": {
- "description": "Client state deleted"
- },
"422": {
"description": "Validation Error",
"content": {
@@ -9807,12 +10055,12 @@
}
}
},
- "/api/v1/recall/{queue_id}": {
- "post": {
- "tags": ["recall"],
- "summary": "Update Recall Parameters",
- "description": "Update recallable parameters that can be recalled on the frontend.\n\nThis endpoint allows updating parameters such as prompt, model, steps, and other\ngeneration settings. These parameters are stored in client state and can be\naccessed by the frontend to populate UI elements.\n\nArgs:\n queue_id: The queue ID to associate these parameters with\n parameters: The RecallParameter object containing the parameters to update\n strict: When true, parameters not included in the request body are reset\n to their defaults (cleared on the frontend). Defaults to false,\n which preserves the existing behaviour of only updating the\n parameters that are explicitly provided.\n append: When true, recalled reference images (``ip_adapters`` and\n ``reference_images``) are appended to whatever reference images the\n frontend already has, instead of replacing the whole list. Mutually\n exclusive with ``strict`` (which clears omitted parameters).\n\nReturns:\n A dictionary containing the updated parameters and status\n\nExample:\n POST /api/v1/recall/{queue_id}?strict=true\n {\n \"positive_prompt\": \"a beautiful landscape\",\n \"model\": \"sd-1.5\",\n \"steps\": 20\n }\n # In strict mode, all other parameters (reference_images, loras, etc.)\n # are cleared. In non-strict mode (default) they would be left as-is.",
- "operationId": "update_recall_parameters",
+ "/api/v1/workflows/i/{workflow_id}": {
+ "get": {
+ "tags": ["workflows"],
+ "summary": "Get Workflow",
+ "description": "Gets a workflow",
+ "operationId": "get_workflow",
"security": [
{
"HTTPBearer": []
@@ -9820,39 +10068,48 @@
],
"parameters": [
{
- "name": "queue_id",
+ "name": "workflow_id",
"in": "path",
"required": true,
"schema": {
"type": "string",
- "description": "The queue id to perform this operation on",
- "title": "Queue Id"
- },
- "description": "The queue id to perform this operation on"
- },
- {
- "name": "strict",
- "in": "query",
- "required": false,
- "schema": {
- "type": "boolean",
- "description": "When true, parameters not included in the request are reset to their defaults (cleared).",
- "default": false,
- "title": "Strict"
+ "description": "The workflow to get",
+ "title": "Workflow Id"
},
- "description": "When true, parameters not included in the request are reset to their defaults (cleared)."
+ "description": "The workflow to get"
+ }
+ ],
+ "responses": {
+ "200": {
+ "description": "Successful Response",
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/WorkflowRecordWithThumbnailDTO"
+ }
+ }
+ }
},
+ "422": {
+ "description": "Validation Error",
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/HTTPValidationError"
+ }
+ }
+ }
+ }
+ }
+ },
+ "patch": {
+ "tags": ["workflows"],
+ "summary": "Update Workflow",
+ "description": "Updates a workflow",
+ "operationId": "update_workflow",
+ "security": [
{
- "name": "append",
- "in": "query",
- "required": false,
- "schema": {
- "type": "boolean",
- "description": "When true, recalled reference images (ip_adapters and reference_images) are appended to the frontend's existing reference-image list instead of replacing it. Mutually exclusive with strict.",
- "default": false,
- "title": "Append"
- },
- "description": "When true, recalled reference images (ip_adapters and reference_images) are appended to the frontend's existing reference-image list instead of replacing it. Mutually exclusive with strict."
+ "HTTPBearer": []
}
],
"requestBody": {
@@ -9860,8 +10117,7 @@
"content": {
"application/json": {
"schema": {
- "$ref": "#/components/schemas/RecallParameter",
- "description": "Recall parameters to update"
+ "$ref": "#/components/schemas/Body_update_workflow"
}
}
}
@@ -9872,9 +10128,7 @@
"content": {
"application/json": {
"schema": {
- "type": "object",
- "additionalProperties": true,
- "title": "Response Update Recall Parameters"
+ "$ref": "#/components/schemas/WorkflowRecordDTO"
}
}
}
@@ -9891,11 +10145,11 @@
}
}
},
- "get": {
- "tags": ["recall"],
- "summary": "Get Recall Parameters",
- "description": "Retrieve all stored recall parameters for a given queue.\n\nReturns a dictionary of all recall parameters that have been set for the queue.\n\nArgs:\n queue_id: The queue ID to retrieve parameters for\n\nReturns:\n A dictionary containing all stored recall parameters",
- "operationId": "get_recall_parameters",
+ "delete": {
+ "tags": ["workflows"],
+ "summary": "Delete Workflow",
+ "description": "Deletes a workflow",
+ "operationId": "delete_workflow",
"security": [
{
"HTTPBearer": []
@@ -9903,15 +10157,15 @@
],
"parameters": [
{
- "name": "queue_id",
+ "name": "workflow_id",
"in": "path",
"required": true,
"schema": {
"type": "string",
- "description": "The queue id to retrieve parameters for",
- "title": "Queue Id"
+ "description": "The workflow to delete",
+ "title": "Workflow Id"
},
- "description": "The queue id to retrieve parameters for"
+ "description": "The workflow to delete"
}
],
"responses": {
@@ -9919,11 +10173,7 @@
"description": "Successful Response",
"content": {
"application/json": {
- "schema": {
- "type": "object",
- "additionalProperties": true,
- "title": "Response Get Recall Parameters"
- }
+ "schema": {}
}
}
},
@@ -9940,47 +10190,26 @@
}
}
},
- "/api/v2/custom_nodes/": {
- "get": {
- "tags": ["custom_nodes"],
- "summary": "List Custom Node Packs",
- "description": "Lists all installed custom node packs.\n\nAdmin-only: the response includes absolute filesystem paths, and non-admins have no\nlegitimate use for pack management data (install/uninstall/reload are also admin-only).",
- "operationId": "list_custom_node_packs",
- "responses": {
- "200": {
- "description": "Successful Response",
- "content": {
- "application/json": {
- "schema": {
- "$ref": "#/components/schemas/NodePackListResponse"
- }
- }
- }
- }
- },
+ "/api/v1/workflows/": {
+ "post": {
+ "tags": ["workflows"],
+ "summary": "Create Workflow",
+ "description": "Creates a workflow",
+ "operationId": "create_workflow",
"security": [
{
"HTTPBearer": []
}
- ]
- }
- },
- "/api/v2/custom_nodes/install": {
- "post": {
- "tags": ["custom_nodes"],
- "summary": "Install Custom Node Pack",
- "description": "Installs a custom node pack from a git URL by cloning it into the nodes directory.",
- "operationId": "install_custom_node_pack",
+ ],
"requestBody": {
+ "required": true,
"content": {
"application/json": {
"schema": {
- "$ref": "#/components/schemas/InstallNodePackRequest",
- "description": "The source URL to install from."
+ "$ref": "#/components/schemas/Body_create_workflow"
}
}
- },
- "required": true
+ }
},
"responses": {
"200": {
@@ -9988,7 +10217,7 @@
"content": {
"application/json": {
"schema": {
- "$ref": "#/components/schemas/InstallNodePackResponse"
+ "$ref": "#/components/schemas/WorkflowRecordDTO"
}
}
}
@@ -10003,20 +10232,13 @@
}
}
}
- },
- "security": [
- {
- "HTTPBearer": []
- }
- ]
- }
- },
- "/api/v2/custom_nodes/{pack_name}": {
- "delete": {
- "tags": ["custom_nodes"],
- "summary": "Uninstall Custom Node Pack",
- "description": "Uninstalls a custom node pack by removing its directory.\n\nNote: A restart is required for the node removal to take full effect.\nInstalled nodes from the pack will remain registered until restart.",
- "operationId": "uninstall_custom_node_pack",
+ }
+ },
+ "get": {
+ "tags": ["workflows"],
+ "summary": "List Workflows",
+ "description": "Gets a page of workflows",
+ "operationId": "list_workflows",
"security": [
{
"HTTPBearer": []
@@ -10024,13 +10246,170 @@
],
"parameters": [
{
- "name": "pack_name",
- "in": "path",
- "required": true,
+ "name": "page",
+ "in": "query",
+ "required": false,
"schema": {
- "type": "string",
- "title": "Pack Name"
- }
+ "type": "integer",
+ "description": "The page to get",
+ "default": 0,
+ "title": "Page"
+ },
+ "description": "The page to get"
+ },
+ {
+ "name": "per_page",
+ "in": "query",
+ "required": false,
+ "schema": {
+ "anyOf": [
+ {
+ "type": "integer"
+ },
+ {
+ "type": "null"
+ }
+ ],
+ "description": "The number of workflows per page",
+ "title": "Per Page"
+ },
+ "description": "The number of workflows per page"
+ },
+ {
+ "name": "order_by",
+ "in": "query",
+ "required": false,
+ "schema": {
+ "$ref": "#/components/schemas/WorkflowRecordOrderBy",
+ "description": "The attribute to order by",
+ "default": "name"
+ },
+ "description": "The attribute to order by"
+ },
+ {
+ "name": "direction",
+ "in": "query",
+ "required": false,
+ "schema": {
+ "$ref": "#/components/schemas/SQLiteDirection",
+ "description": "The direction to order by",
+ "default": "ASC"
+ },
+ "description": "The direction to order by"
+ },
+ {
+ "name": "categories",
+ "in": "query",
+ "required": false,
+ "schema": {
+ "anyOf": [
+ {
+ "type": "array",
+ "items": {
+ "$ref": "#/components/schemas/WorkflowCategory"
+ }
+ },
+ {
+ "type": "null"
+ }
+ ],
+ "description": "The categories of workflow to get",
+ "title": "Categories"
+ },
+ "description": "The categories of workflow to get"
+ },
+ {
+ "name": "tags",
+ "in": "query",
+ "required": false,
+ "schema": {
+ "anyOf": [
+ {
+ "type": "array",
+ "items": {
+ "type": "string"
+ }
+ },
+ {
+ "type": "null"
+ }
+ ],
+ "description": "The tags of workflow to get",
+ "title": "Tags"
+ },
+ "description": "The tags of workflow to get"
+ },
+ {
+ "name": "query",
+ "in": "query",
+ "required": false,
+ "schema": {
+ "anyOf": [
+ {
+ "type": "string"
+ },
+ {
+ "type": "null"
+ }
+ ],
+ "description": "The text to query by (matches name and description)",
+ "title": "Query"
+ },
+ "description": "The text to query by (matches name and description)"
+ },
+ {
+ "name": "has_been_opened",
+ "in": "query",
+ "required": false,
+ "schema": {
+ "anyOf": [
+ {
+ "type": "boolean"
+ },
+ {
+ "type": "null"
+ }
+ ],
+ "description": "Whether to include/exclude recent workflows",
+ "title": "Has Been Opened"
+ },
+ "description": "Whether to include/exclude recent workflows"
+ },
+ {
+ "name": "is_public",
+ "in": "query",
+ "required": false,
+ "schema": {
+ "anyOf": [
+ {
+ "type": "boolean"
+ },
+ {
+ "type": "null"
+ }
+ ],
+ "description": "Filter by public/shared status",
+ "title": "Is Public"
+ },
+ "description": "Filter by public/shared status"
+ },
+ {
+ "name": "callable",
+ "in": "query",
+ "required": false,
+ "schema": {
+ "anyOf": [
+ {
+ "type": "boolean"
+ },
+ {
+ "type": "null"
+ }
+ ],
+ "description": "Filter by whether workflows are callable by call_saved_workflow",
+ "title": "Callable"
+ },
+ "description": "Filter by whether workflows are callable by call_saved_workflow"
}
],
"responses": {
@@ -10039,7 +10418,7 @@
"content": {
"application/json": {
"schema": {
- "$ref": "#/components/schemas/UninstallNodePackResponse"
+ "$ref": "#/components/schemas/PaginatedResults_WorkflowRecordListItemWithThumbnailDTO_"
}
}
}
@@ -10057,321 +10436,1778 @@
}
}
},
- "/api/v2/custom_nodes/reload": {
- "post": {
- "tags": ["custom_nodes"],
- "summary": "Reload Custom Nodes",
- "description": "Triggers a reload of all custom nodes.\n\nThis re-scans the nodes directory and loads any new node packs.\nAlready loaded packs are skipped.",
- "operationId": "reload_custom_nodes",
- "responses": {
- "200": {
- "description": "Successful Response",
- "content": {
- "application/json": {
- "schema": {
- "additionalProperties": {
- "type": "string"
- },
- "type": "object",
- "title": "Response Reload Custom Nodes"
- }
- }
- }
- }
- },
+ "/api/v1/workflows/i/{workflow_id}/thumbnail": {
+ "put": {
+ "tags": ["workflows"],
+ "summary": "Set Workflow Thumbnail",
+ "description": "Sets a workflow's thumbnail image",
+ "operationId": "set_workflow_thumbnail",
"security": [
{
"HTTPBearer": []
}
- ]
- }
- }
- },
- "components": {
- "schemas": {
- "AddImagesToBoardResult": {
- "properties": {
- "affected_boards": {
- "items": {
- "type": "string"
- },
- "type": "array",
- "title": "Affected Boards",
- "description": "The ids of boards affected by the delete operation"
- },
- "added_images": {
- "items": {
- "type": "string"
+ ],
+ "parameters": [
+ {
+ "name": "workflow_id",
+ "in": "path",
+ "required": true,
+ "schema": {
+ "type": "string",
+ "description": "The workflow to update",
+ "title": "Workflow Id"
},
- "type": "array",
- "title": "Added Images",
- "description": "The image names that were added to the board"
+ "description": "The workflow to update"
+ }
+ ],
+ "requestBody": {
+ "required": true,
+ "content": {
+ "multipart/form-data": {
+ "schema": {
+ "$ref": "#/components/schemas/Body_set_workflow_thumbnail"
+ }
+ }
}
},
- "type": "object",
- "required": ["affected_boards", "added_images"],
- "title": "AddImagesToBoardResult"
- },
- "AddInvocation": {
- "category": "math",
- "class": "invocation",
- "classification": "stable",
- "description": "Adds two numbers",
- "node_pack": "invokeai",
- "properties": {
- "id": {
- "description": "The id of this instance of an invocation. Must be unique among all instances of invocations.",
- "field_kind": "node_attribute",
- "title": "Id",
- "type": "string"
- },
- "is_intermediate": {
- "default": false,
- "description": "Whether or not this is an intermediate invocation.",
- "field_kind": "node_attribute",
- "input": "direct",
- "orig_required": true,
- "title": "Is Intermediate",
- "type": "boolean",
- "ui_hidden": false,
- "ui_type": "IsIntermediate"
- },
- "use_cache": {
- "default": true,
- "description": "Whether or not to use the cache",
- "field_kind": "node_attribute",
- "title": "Use Cache",
- "type": "boolean"
- },
- "a": {
- "default": 0,
- "description": "The first number",
- "field_kind": "input",
- "input": "any",
- "orig_default": 0,
- "orig_required": false,
- "title": "A",
- "type": "integer"
- },
- "b": {
- "default": 0,
- "description": "The second number",
- "field_kind": "input",
- "input": "any",
- "orig_default": 0,
- "orig_required": false,
- "title": "B",
- "type": "integer"
+ "responses": {
+ "200": {
+ "description": "Successful Response",
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/WorkflowRecordDTO"
+ }
+ }
+ }
},
- "type": {
- "const": "add",
- "default": "add",
- "field_kind": "node_attribute",
- "title": "type",
- "type": "string"
+ "422": {
+ "description": "Validation Error",
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/HTTPValidationError"
+ }
+ }
+ }
}
- },
- "required": ["type", "id"],
- "tags": ["math", "add"],
- "title": "Add Integers",
- "type": "object",
- "version": "1.0.1",
- "output": {
- "$ref": "#/components/schemas/IntegerOutput"
}
},
- "AdminUserCreateRequest": {
- "properties": {
- "email": {
- "type": "string",
- "title": "Email",
- "description": "User email address"
- },
- "display_name": {
- "anyOf": [
- {
- "type": "string"
- },
- {
- "type": "null"
+ "delete": {
+ "tags": ["workflows"],
+ "summary": "Delete Workflow Thumbnail",
+ "description": "Removes a workflow's thumbnail image",
+ "operationId": "delete_workflow_thumbnail",
+ "security": [
+ {
+ "HTTPBearer": []
+ }
+ ],
+ "parameters": [
+ {
+ "name": "workflow_id",
+ "in": "path",
+ "required": true,
+ "schema": {
+ "type": "string",
+ "description": "The workflow to update",
+ "title": "Workflow Id"
+ },
+ "description": "The workflow to update"
+ }
+ ],
+ "responses": {
+ "200": {
+ "description": "Successful Response",
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/WorkflowRecordDTO"
+ }
}
- ],
- "title": "Display Name",
- "description": "Display name"
- },
- "password": {
- "type": "string",
- "title": "Password",
- "description": "User password"
+ }
},
- "is_admin": {
- "type": "boolean",
- "title": "Is Admin",
- "description": "Whether user should have admin privileges",
- "default": false
+ "422": {
+ "description": "Validation Error",
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/HTTPValidationError"
+ }
+ }
+ }
}
- },
- "type": "object",
- "required": ["email", "password"],
- "title": "AdminUserCreateRequest",
- "description": "Request body for admin to create a new user."
+ }
},
- "AdminUserUpdateRequest": {
- "properties": {
- "display_name": {
- "anyOf": [
- {
- "type": "string"
- },
- {
- "type": "null"
+ "get": {
+ "tags": ["workflows"],
+ "summary": "Get Workflow Thumbnail",
+ "description": "Gets a workflow's thumbnail image.\n\nThis endpoint is intentionally unauthenticated because browsers load images\nvia tags which cannot send Bearer tokens. Workflow IDs are UUIDs,\nproviding security through unguessability.",
+ "operationId": "get_workflow_thumbnail",
+ "parameters": [
+ {
+ "name": "workflow_id",
+ "in": "path",
+ "required": true,
+ "schema": {
+ "type": "string",
+ "description": "The id of the workflow thumbnail to get",
+ "title": "Workflow Id"
+ },
+ "description": "The id of the workflow thumbnail to get"
+ }
+ ],
+ "responses": {
+ "200": {
+ "description": "The workflow thumbnail was fetched successfully",
+ "content": {
+ "application/json": {
+ "schema": {}
}
- ],
- "title": "Display Name",
- "description": "Display name"
+ }
},
- "password": {
- "anyOf": [
- {
- "type": "string"
- },
- {
- "type": "null"
- }
- ],
- "title": "Password",
- "description": "New password"
+ "400": {
+ "description": "Bad request"
},
- "is_admin": {
- "anyOf": [
- {
- "type": "boolean"
- },
- {
- "type": "null"
- }
- ],
- "title": "Is Admin",
- "description": "Whether user should have admin privileges"
+ "404": {
+ "description": "The workflow thumbnail could not be found"
},
- "is_active": {
- "anyOf": [
- {
- "type": "boolean"
- },
- {
- "type": "null"
+ "422": {
+ "description": "Validation Error",
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/HTTPValidationError"
+ }
}
- ],
- "title": "Is Active",
- "description": "Whether user account should be active"
+ }
+ }
+ }
+ }
+ },
+ "/api/v1/workflows/i/{workflow_id}/is_public": {
+ "patch": {
+ "tags": ["workflows"],
+ "summary": "Update Workflow Is Public",
+ "description": "Updates whether a workflow is shared publicly",
+ "operationId": "update_workflow_is_public",
+ "security": [
+ {
+ "HTTPBearer": []
+ }
+ ],
+ "parameters": [
+ {
+ "name": "workflow_id",
+ "in": "path",
+ "required": true,
+ "schema": {
+ "type": "string",
+ "description": "The workflow to update",
+ "title": "Workflow Id"
+ },
+ "description": "The workflow to update"
+ }
+ ],
+ "requestBody": {
+ "required": true,
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/Body_update_workflow_is_public"
+ }
+ }
}
},
- "type": "object",
- "title": "AdminUserUpdateRequest",
- "description": "Request body for admin to update any user."
- },
- "AlibabaCloudImageGenerationInvocation": {
- "category": "image",
- "class": "invocation",
- "classification": "stable",
- "description": "Generate images using an Alibaba Cloud DashScope external model.",
- "node_pack": "invokeai",
- "properties": {
- "board": {
- "anyOf": [
- {
- "$ref": "#/components/schemas/BoardField"
- },
- {
- "type": "null"
+ "responses": {
+ "200": {
+ "description": "Successful Response",
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/WorkflowRecordDTO"
+ }
}
- ],
- "default": null,
- "description": "The board to save the image to",
- "field_kind": "internal",
- "input": "direct",
- "orig_required": false,
- "ui_hidden": false
+ }
},
- "metadata": {
- "anyOf": [
- {
- "$ref": "#/components/schemas/MetadataField"
- },
- {
- "type": "null"
+ "422": {
+ "description": "Validation Error",
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/HTTPValidationError"
+ }
}
- ],
- "default": null,
- "description": "Optional metadata to be saved with the image",
- "field_kind": "internal",
- "input": "connection",
- "orig_required": false,
- "ui_hidden": false
- },
- "id": {
- "description": "The id of this instance of an invocation. Must be unique among all instances of invocations.",
- "field_kind": "node_attribute",
- "title": "Id",
- "type": "string"
- },
- "is_intermediate": {
- "default": false,
- "description": "Whether or not this is an intermediate invocation.",
- "field_kind": "node_attribute",
- "input": "direct",
- "orig_required": true,
- "title": "Is Intermediate",
- "type": "boolean",
- "ui_hidden": false,
- "ui_type": "IsIntermediate"
- },
- "use_cache": {
- "default": true,
- "description": "Whether or not to use the cache",
- "field_kind": "node_attribute",
- "title": "Use Cache",
- "type": "boolean"
+ }
+ }
+ }
+ }
+ },
+ "/api/v1/workflows/tags": {
+ "get": {
+ "tags": ["workflows"],
+ "summary": "Get All Tags",
+ "description": "Gets all unique tags from workflows",
+ "operationId": "get_all_tags",
+ "security": [
+ {
+ "HTTPBearer": []
+ }
+ ],
+ "parameters": [
+ {
+ "name": "categories",
+ "in": "query",
+ "required": false,
+ "schema": {
+ "anyOf": [
+ {
+ "type": "array",
+ "items": {
+ "$ref": "#/components/schemas/WorkflowCategory"
+ }
+ },
+ {
+ "type": "null"
+ }
+ ],
+ "description": "The categories to include",
+ "title": "Categories"
+ },
+ "description": "The categories to include"
},
- "model": {
- "anyOf": [
- {
- "$ref": "#/components/schemas/ModelIdentifierField"
- },
- {
- "type": "null"
+ {
+ "name": "is_public",
+ "in": "query",
+ "required": false,
+ "schema": {
+ "anyOf": [
+ {
+ "type": "boolean"
+ },
+ {
+ "type": "null"
+ }
+ ],
+ "description": "Filter by public/shared status",
+ "title": "Is Public"
+ },
+ "description": "Filter by public/shared status"
+ }
+ ],
+ "responses": {
+ "200": {
+ "description": "Successful Response",
+ "content": {
+ "application/json": {
+ "schema": {
+ "type": "array",
+ "items": {
+ "type": "string"
+ },
+ "title": "Response Get All Tags"
+ }
}
- ],
- "default": null,
- "description": "Main model (UNet, VAE, CLIP) to load",
- "field_kind": "input",
- "input": "any",
- "orig_required": true,
- "ui_model_base": ["external"],
- "ui_model_format": ["external_api"],
- "ui_model_provider_id": ["alibabacloud"],
- "ui_model_type": ["external_image_generator"]
- },
- "mode": {
- "default": "txt2img",
- "description": "Generation mode. Not all modes are supported by every model; unsupported modes raise at runtime.",
- "enum": ["txt2img", "img2img", "inpaint"],
- "field_kind": "input",
- "input": "any",
- "orig_default": "txt2img",
- "orig_required": false,
- "title": "Mode",
- "type": "string"
+ }
},
- "prompt": {
- "anyOf": [
- {
- "type": "string"
- },
- {
- "type": "null"
- }
+ "422": {
+ "description": "Validation Error",
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/HTTPValidationError"
+ }
+ }
+ }
+ }
+ }
+ }
+ },
+ "/api/v1/workflows/counts_by_tag": {
+ "get": {
+ "tags": ["workflows"],
+ "summary": "Get Counts By Tag",
+ "description": "Counts workflows by tag",
+ "operationId": "get_counts_by_tag",
+ "security": [
+ {
+ "HTTPBearer": []
+ }
+ ],
+ "parameters": [
+ {
+ "name": "tags",
+ "in": "query",
+ "required": true,
+ "schema": {
+ "type": "array",
+ "items": {
+ "type": "string"
+ },
+ "description": "The tags to get counts for",
+ "title": "Tags"
+ },
+ "description": "The tags to get counts for"
+ },
+ {
+ "name": "categories",
+ "in": "query",
+ "required": false,
+ "schema": {
+ "anyOf": [
+ {
+ "type": "array",
+ "items": {
+ "$ref": "#/components/schemas/WorkflowCategory"
+ }
+ },
+ {
+ "type": "null"
+ }
+ ],
+ "description": "The categories to include",
+ "title": "Categories"
+ },
+ "description": "The categories to include"
+ },
+ {
+ "name": "has_been_opened",
+ "in": "query",
+ "required": false,
+ "schema": {
+ "anyOf": [
+ {
+ "type": "boolean"
+ },
+ {
+ "type": "null"
+ }
+ ],
+ "description": "Whether to include/exclude recent workflows",
+ "title": "Has Been Opened"
+ },
+ "description": "Whether to include/exclude recent workflows"
+ },
+ {
+ "name": "is_public",
+ "in": "query",
+ "required": false,
+ "schema": {
+ "anyOf": [
+ {
+ "type": "boolean"
+ },
+ {
+ "type": "null"
+ }
+ ],
+ "description": "Filter by public/shared status",
+ "title": "Is Public"
+ },
+ "description": "Filter by public/shared status"
+ }
+ ],
+ "responses": {
+ "200": {
+ "description": "Successful Response",
+ "content": {
+ "application/json": {
+ "schema": {
+ "type": "object",
+ "additionalProperties": {
+ "type": "integer"
+ },
+ "title": "Response Get Counts By Tag"
+ }
+ }
+ }
+ },
+ "422": {
+ "description": "Validation Error",
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/HTTPValidationError"
+ }
+ }
+ }
+ }
+ }
+ }
+ },
+ "/api/v1/workflows/counts_by_category": {
+ "get": {
+ "tags": ["workflows"],
+ "summary": "Counts By Category",
+ "description": "Counts workflows by category",
+ "operationId": "counts_by_category",
+ "security": [
+ {
+ "HTTPBearer": []
+ }
+ ],
+ "parameters": [
+ {
+ "name": "categories",
+ "in": "query",
+ "required": true,
+ "schema": {
+ "type": "array",
+ "items": {
+ "$ref": "#/components/schemas/WorkflowCategory"
+ },
+ "description": "The categories to include",
+ "title": "Categories"
+ },
+ "description": "The categories to include"
+ },
+ {
+ "name": "has_been_opened",
+ "in": "query",
+ "required": false,
+ "schema": {
+ "anyOf": [
+ {
+ "type": "boolean"
+ },
+ {
+ "type": "null"
+ }
+ ],
+ "description": "Whether to include/exclude recent workflows",
+ "title": "Has Been Opened"
+ },
+ "description": "Whether to include/exclude recent workflows"
+ },
+ {
+ "name": "is_public",
+ "in": "query",
+ "required": false,
+ "schema": {
+ "anyOf": [
+ {
+ "type": "boolean"
+ },
+ {
+ "type": "null"
+ }
+ ],
+ "description": "Filter by public/shared status",
+ "title": "Is Public"
+ },
+ "description": "Filter by public/shared status"
+ }
+ ],
+ "responses": {
+ "200": {
+ "description": "Successful Response",
+ "content": {
+ "application/json": {
+ "schema": {
+ "type": "object",
+ "additionalProperties": {
+ "type": "integer"
+ },
+ "title": "Response Counts By Category"
+ }
+ }
+ }
+ },
+ "422": {
+ "description": "Validation Error",
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/HTTPValidationError"
+ }
+ }
+ }
+ }
+ }
+ }
+ },
+ "/api/v1/workflows/i/{workflow_id}/opened_at": {
+ "put": {
+ "tags": ["workflows"],
+ "summary": "Update Opened At",
+ "description": "Updates the opened_at field of a workflow",
+ "operationId": "update_opened_at",
+ "security": [
+ {
+ "HTTPBearer": []
+ }
+ ],
+ "parameters": [
+ {
+ "name": "workflow_id",
+ "in": "path",
+ "required": true,
+ "schema": {
+ "type": "string",
+ "description": "The workflow to update",
+ "title": "Workflow Id"
+ },
+ "description": "The workflow to update"
+ }
+ ],
+ "responses": {
+ "200": {
+ "description": "Successful Response",
+ "content": {
+ "application/json": {
+ "schema": {}
+ }
+ }
+ },
+ "422": {
+ "description": "Validation Error",
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/HTTPValidationError"
+ }
+ }
+ }
+ }
+ }
+ }
+ },
+ "/api/v1/style_presets/i/{style_preset_id}": {
+ "get": {
+ "tags": ["style_presets"],
+ "summary": "Get Style Preset",
+ "description": "Gets a style preset",
+ "operationId": "get_style_preset",
+ "security": [
+ {
+ "HTTPBearer": []
+ }
+ ],
+ "parameters": [
+ {
+ "name": "style_preset_id",
+ "in": "path",
+ "required": true,
+ "schema": {
+ "type": "string",
+ "description": "The style preset to get",
+ "title": "Style Preset Id"
+ },
+ "description": "The style preset to get"
+ }
+ ],
+ "responses": {
+ "200": {
+ "description": "Successful Response",
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/StylePresetRecordWithImage"
+ }
+ }
+ }
+ },
+ "422": {
+ "description": "Validation Error",
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/HTTPValidationError"
+ }
+ }
+ }
+ }
+ }
+ },
+ "patch": {
+ "tags": ["style_presets"],
+ "summary": "Update Style Preset",
+ "description": "Updates a style preset",
+ "operationId": "update_style_preset",
+ "security": [
+ {
+ "HTTPBearer": []
+ }
+ ],
+ "parameters": [
+ {
+ "name": "style_preset_id",
+ "in": "path",
+ "required": true,
+ "schema": {
+ "type": "string",
+ "description": "The id of the style preset to update",
+ "title": "Style Preset Id"
+ },
+ "description": "The id of the style preset to update"
+ }
+ ],
+ "requestBody": {
+ "required": true,
+ "content": {
+ "multipart/form-data": {
+ "schema": {
+ "$ref": "#/components/schemas/Body_update_style_preset"
+ }
+ }
+ }
+ },
+ "responses": {
+ "200": {
+ "description": "Successful Response",
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/StylePresetRecordWithImage"
+ }
+ }
+ }
+ },
+ "422": {
+ "description": "Validation Error",
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/HTTPValidationError"
+ }
+ }
+ }
+ }
+ }
+ },
+ "delete": {
+ "tags": ["style_presets"],
+ "summary": "Delete Style Preset",
+ "description": "Deletes a style preset",
+ "operationId": "delete_style_preset",
+ "security": [
+ {
+ "HTTPBearer": []
+ }
+ ],
+ "parameters": [
+ {
+ "name": "style_preset_id",
+ "in": "path",
+ "required": true,
+ "schema": {
+ "type": "string",
+ "description": "The style preset to delete",
+ "title": "Style Preset Id"
+ },
+ "description": "The style preset to delete"
+ }
+ ],
+ "responses": {
+ "200": {
+ "description": "Successful Response",
+ "content": {
+ "application/json": {
+ "schema": {}
+ }
+ }
+ },
+ "422": {
+ "description": "Validation Error",
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/HTTPValidationError"
+ }
+ }
+ }
+ }
+ }
+ }
+ },
+ "/api/v1/style_presets/": {
+ "get": {
+ "tags": ["style_presets"],
+ "summary": "List Style Presets",
+ "description": "Gets the style presets visible to the current user.",
+ "operationId": "list_style_presets",
+ "responses": {
+ "200": {
+ "description": "Successful Response",
+ "content": {
+ "application/json": {
+ "schema": {
+ "items": {
+ "$ref": "#/components/schemas/StylePresetRecordWithImage"
+ },
+ "type": "array",
+ "title": "Response 200 List Style Presets"
+ }
+ }
+ }
+ }
+ },
+ "security": [
+ {
+ "HTTPBearer": []
+ }
+ ]
+ },
+ "post": {
+ "tags": ["style_presets"],
+ "summary": "Create Style Preset",
+ "description": "Creates a style preset",
+ "operationId": "create_style_preset",
+ "requestBody": {
+ "content": {
+ "multipart/form-data": {
+ "schema": {
+ "$ref": "#/components/schemas/Body_create_style_preset"
+ }
+ }
+ },
+ "required": true
+ },
+ "responses": {
+ "200": {
+ "description": "Successful Response",
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/StylePresetRecordWithImage"
+ }
+ }
+ }
+ },
+ "422": {
+ "description": "Validation Error",
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/HTTPValidationError"
+ }
+ }
+ }
+ }
+ },
+ "security": [
+ {
+ "HTTPBearer": []
+ }
+ ]
+ }
+ },
+ "/api/v1/style_presets/i/{style_preset_id}/image": {
+ "get": {
+ "tags": ["style_presets"],
+ "summary": "Get Style Preset Image",
+ "description": "Gets an image file that previews the model",
+ "operationId": "get_style_preset_image",
+ "security": [
+ {
+ "HTTPBearer": []
+ }
+ ],
+ "parameters": [
+ {
+ "name": "style_preset_id",
+ "in": "path",
+ "required": true,
+ "schema": {
+ "type": "string",
+ "description": "The id of the style preset image to get",
+ "title": "Style Preset Id"
+ },
+ "description": "The id of the style preset image to get"
+ }
+ ],
+ "responses": {
+ "200": {
+ "description": "The style preset image was fetched successfully",
+ "content": {
+ "application/json": {
+ "schema": {}
+ }
+ }
+ },
+ "400": {
+ "description": "Bad request"
+ },
+ "404": {
+ "description": "The style preset image could not be found"
+ },
+ "422": {
+ "description": "Validation Error",
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/HTTPValidationError"
+ }
+ }
+ }
+ }
+ }
+ }
+ },
+ "/api/v1/style_presets/export": {
+ "get": {
+ "tags": ["style_presets"],
+ "summary": "Export Style Presets",
+ "operationId": "export_style_presets",
+ "responses": {
+ "200": {
+ "description": "A CSV file with the requested data.",
+ "content": {
+ "application/json": {
+ "schema": {}
+ },
+ "text/csv": {}
+ }
+ }
+ },
+ "security": [
+ {
+ "HTTPBearer": []
+ }
+ ]
+ }
+ },
+ "/api/v1/style_presets/import": {
+ "post": {
+ "tags": ["style_presets"],
+ "summary": "Import Style Presets",
+ "operationId": "import_style_presets",
+ "requestBody": {
+ "content": {
+ "multipart/form-data": {
+ "schema": {
+ "$ref": "#/components/schemas/Body_import_style_presets"
+ }
+ }
+ },
+ "required": true
+ },
+ "responses": {
+ "200": {
+ "description": "Successful Response",
+ "content": {
+ "application/json": {
+ "schema": {}
+ }
+ }
+ },
+ "422": {
+ "description": "Validation Error",
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/HTTPValidationError"
+ }
+ }
+ }
+ }
+ },
+ "security": [
+ {
+ "HTTPBearer": []
+ }
+ ]
+ }
+ },
+ "/api/v1/client_state/{queue_id}/get_by_key": {
+ "get": {
+ "tags": ["client_state"],
+ "summary": "Get Client State By Key",
+ "description": "Gets the client state for the current user (or system user if not authenticated)",
+ "operationId": "get_client_state_by_key",
+ "security": [
+ {
+ "HTTPBearer": []
+ }
+ ],
+ "parameters": [
+ {
+ "name": "queue_id",
+ "in": "path",
+ "required": true,
+ "schema": {
+ "type": "string",
+ "description": "The queue id (ignored, kept for backwards compatibility)",
+ "title": "Queue Id"
+ },
+ "description": "The queue id (ignored, kept for backwards compatibility)"
+ },
+ {
+ "name": "key",
+ "in": "query",
+ "required": true,
+ "schema": {
+ "type": "string",
+ "description": "Key to get",
+ "title": "Key"
+ },
+ "description": "Key to get"
+ }
+ ],
+ "responses": {
+ "200": {
+ "description": "Successful Response",
+ "content": {
+ "application/json": {
+ "schema": {
+ "anyOf": [
+ {
+ "type": "string"
+ },
+ {
+ "type": "null"
+ }
+ ],
+ "title": "Response Get Client State By Key"
+ }
+ }
+ }
+ },
+ "422": {
+ "description": "Validation Error",
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/HTTPValidationError"
+ }
+ }
+ }
+ }
+ }
+ }
+ },
+ "/api/v1/client_state/{queue_id}/set_by_key": {
+ "post": {
+ "tags": ["client_state"],
+ "summary": "Set Client State",
+ "description": "Sets the client state for the current user (or system user if not authenticated)",
+ "operationId": "set_client_state",
+ "security": [
+ {
+ "HTTPBearer": []
+ }
+ ],
+ "parameters": [
+ {
+ "name": "queue_id",
+ "in": "path",
+ "required": true,
+ "schema": {
+ "type": "string",
+ "description": "The queue id (ignored, kept for backwards compatibility)",
+ "title": "Queue Id"
+ },
+ "description": "The queue id (ignored, kept for backwards compatibility)"
+ },
+ {
+ "name": "key",
+ "in": "query",
+ "required": true,
+ "schema": {
+ "type": "string",
+ "description": "Key to set",
+ "title": "Key"
+ },
+ "description": "Key to set"
+ }
+ ],
+ "requestBody": {
+ "required": true,
+ "content": {
+ "application/json": {
+ "schema": {
+ "type": "string",
+ "description": "Stringified value to set",
+ "title": "Value"
+ }
+ }
+ }
+ },
+ "responses": {
+ "200": {
+ "description": "Successful Response",
+ "content": {
+ "application/json": {
+ "schema": {
+ "type": "string",
+ "title": "Response Set Client State"
+ }
+ }
+ }
+ },
+ "422": {
+ "description": "Validation Error",
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/HTTPValidationError"
+ }
+ }
+ }
+ }
+ }
+ }
+ },
+ "/api/v1/client_state/{queue_id}/get_keys_by_prefix": {
+ "get": {
+ "tags": ["client_state"],
+ "summary": "Get Client State Keys By Prefix",
+ "description": "Gets client state keys matching a prefix for the current user",
+ "operationId": "get_client_state_keys_by_prefix",
+ "security": [
+ {
+ "HTTPBearer": []
+ }
+ ],
+ "parameters": [
+ {
+ "name": "queue_id",
+ "in": "path",
+ "required": true,
+ "schema": {
+ "type": "string",
+ "description": "The queue id (ignored, kept for backwards compatibility)",
+ "title": "Queue Id"
+ },
+ "description": "The queue id (ignored, kept for backwards compatibility)"
+ },
+ {
+ "name": "prefix",
+ "in": "query",
+ "required": true,
+ "schema": {
+ "type": "string",
+ "description": "Prefix to filter keys by",
+ "title": "Prefix"
+ },
+ "description": "Prefix to filter keys by"
+ }
+ ],
+ "responses": {
+ "200": {
+ "description": "Successful Response",
+ "content": {
+ "application/json": {
+ "schema": {
+ "type": "array",
+ "items": {
+ "type": "string"
+ },
+ "title": "Response Get Client State Keys By Prefix"
+ }
+ }
+ }
+ },
+ "422": {
+ "description": "Validation Error",
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/HTTPValidationError"
+ }
+ }
+ }
+ }
+ }
+ }
+ },
+ "/api/v1/client_state/{queue_id}/delete_by_key": {
+ "post": {
+ "tags": ["client_state"],
+ "summary": "Delete Client State By Key",
+ "description": "Deletes a specific client state key for the current user",
+ "operationId": "delete_client_state_by_key",
+ "security": [
+ {
+ "HTTPBearer": []
+ }
+ ],
+ "parameters": [
+ {
+ "name": "queue_id",
+ "in": "path",
+ "required": true,
+ "schema": {
+ "type": "string",
+ "description": "The queue id (ignored, kept for backwards compatibility)",
+ "title": "Queue Id"
+ },
+ "description": "The queue id (ignored, kept for backwards compatibility)"
+ },
+ {
+ "name": "key",
+ "in": "query",
+ "required": true,
+ "schema": {
+ "type": "string",
+ "description": "Key to delete",
+ "title": "Key"
+ },
+ "description": "Key to delete"
+ }
+ ],
+ "responses": {
+ "200": {
+ "description": "Successful Response",
+ "content": {
+ "application/json": {
+ "schema": {}
+ }
+ }
+ },
+ "204": {
+ "description": "Client state key deleted"
+ },
+ "422": {
+ "description": "Validation Error",
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/HTTPValidationError"
+ }
+ }
+ }
+ }
+ }
+ }
+ },
+ "/api/v1/client_state/{queue_id}/delete": {
+ "post": {
+ "tags": ["client_state"],
+ "summary": "Delete Client State",
+ "description": "Deletes the client state for the current user (or system user if not authenticated)",
+ "operationId": "delete_client_state",
+ "security": [
+ {
+ "HTTPBearer": []
+ }
+ ],
+ "parameters": [
+ {
+ "name": "queue_id",
+ "in": "path",
+ "required": true,
+ "schema": {
+ "type": "string",
+ "description": "The queue id (ignored, kept for backwards compatibility)",
+ "title": "Queue Id"
+ },
+ "description": "The queue id (ignored, kept for backwards compatibility)"
+ }
+ ],
+ "responses": {
+ "200": {
+ "description": "Successful Response",
+ "content": {
+ "application/json": {
+ "schema": {}
+ }
+ }
+ },
+ "204": {
+ "description": "Client state deleted"
+ },
+ "422": {
+ "description": "Validation Error",
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/HTTPValidationError"
+ }
+ }
+ }
+ }
+ }
+ }
+ },
+ "/api/v1/recall/{queue_id}": {
+ "post": {
+ "tags": ["recall"],
+ "summary": "Update Recall Parameters",
+ "description": "Update recallable parameters that can be recalled on the frontend.\n\nThis endpoint allows updating parameters such as prompt, model, steps, and other\ngeneration settings. These parameters are stored in client state and can be\naccessed by the frontend to populate UI elements.\n\nArgs:\n queue_id: The queue ID to associate these parameters with\n parameters: The RecallParameter object containing the parameters to update\n strict: When true, parameters not included in the request body are reset\n to their defaults (cleared on the frontend). Defaults to false,\n which preserves the existing behaviour of only updating the\n parameters that are explicitly provided.\n append: When true, recalled reference images (``ip_adapters`` and\n ``reference_images``) are appended to whatever reference images the\n frontend already has, instead of replacing the whole list. Mutually\n exclusive with ``strict`` (which clears omitted parameters).\n\nReturns:\n A dictionary containing the updated parameters and status\n\nExample:\n POST /api/v1/recall/{queue_id}?strict=true\n {\n \"positive_prompt\": \"a beautiful landscape\",\n \"model\": \"sd-1.5\",\n \"steps\": 20\n }\n # In strict mode, all other parameters (reference_images, loras, etc.)\n # are cleared. In non-strict mode (default) they would be left as-is.",
+ "operationId": "update_recall_parameters",
+ "security": [
+ {
+ "HTTPBearer": []
+ }
+ ],
+ "parameters": [
+ {
+ "name": "queue_id",
+ "in": "path",
+ "required": true,
+ "schema": {
+ "type": "string",
+ "description": "The queue id to perform this operation on",
+ "title": "Queue Id"
+ },
+ "description": "The queue id to perform this operation on"
+ },
+ {
+ "name": "strict",
+ "in": "query",
+ "required": false,
+ "schema": {
+ "type": "boolean",
+ "description": "When true, parameters not included in the request are reset to their defaults (cleared).",
+ "default": false,
+ "title": "Strict"
+ },
+ "description": "When true, parameters not included in the request are reset to their defaults (cleared)."
+ },
+ {
+ "name": "append",
+ "in": "query",
+ "required": false,
+ "schema": {
+ "type": "boolean",
+ "description": "When true, recalled reference images (ip_adapters and reference_images) are appended to the frontend's existing reference-image list instead of replacing it. Mutually exclusive with strict.",
+ "default": false,
+ "title": "Append"
+ },
+ "description": "When true, recalled reference images (ip_adapters and reference_images) are appended to the frontend's existing reference-image list instead of replacing it. Mutually exclusive with strict."
+ }
+ ],
+ "requestBody": {
+ "required": true,
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/RecallParameter",
+ "description": "Recall parameters to update"
+ }
+ }
+ }
+ },
+ "responses": {
+ "200": {
+ "description": "Successful Response",
+ "content": {
+ "application/json": {
+ "schema": {
+ "type": "object",
+ "additionalProperties": true,
+ "title": "Response Update Recall Parameters"
+ }
+ }
+ }
+ },
+ "422": {
+ "description": "Validation Error",
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/HTTPValidationError"
+ }
+ }
+ }
+ }
+ }
+ },
+ "get": {
+ "tags": ["recall"],
+ "summary": "Get Recall Parameters",
+ "description": "Retrieve all stored recall parameters for a given queue.\n\nReturns a dictionary of all recall parameters that have been set for the queue.\n\nArgs:\n queue_id: The queue ID to retrieve parameters for\n\nReturns:\n A dictionary containing all stored recall parameters",
+ "operationId": "get_recall_parameters",
+ "security": [
+ {
+ "HTTPBearer": []
+ }
+ ],
+ "parameters": [
+ {
+ "name": "queue_id",
+ "in": "path",
+ "required": true,
+ "schema": {
+ "type": "string",
+ "description": "The queue id to retrieve parameters for",
+ "title": "Queue Id"
+ },
+ "description": "The queue id to retrieve parameters for"
+ }
+ ],
+ "responses": {
+ "200": {
+ "description": "Successful Response",
+ "content": {
+ "application/json": {
+ "schema": {
+ "type": "object",
+ "additionalProperties": true,
+ "title": "Response Get Recall Parameters"
+ }
+ }
+ }
+ },
+ "422": {
+ "description": "Validation Error",
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/HTTPValidationError"
+ }
+ }
+ }
+ }
+ }
+ }
+ },
+ "/api/v2/custom_nodes/": {
+ "get": {
+ "tags": ["custom_nodes"],
+ "summary": "List Custom Node Packs",
+ "description": "Lists all installed custom node packs.\n\nAdmin-only: the response includes absolute filesystem paths, and non-admins have no\nlegitimate use for pack management data (install/uninstall/reload are also admin-only).",
+ "operationId": "list_custom_node_packs",
+ "responses": {
+ "200": {
+ "description": "Successful Response",
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/NodePackListResponse"
+ }
+ }
+ }
+ }
+ },
+ "security": [
+ {
+ "HTTPBearer": []
+ }
+ ]
+ }
+ },
+ "/api/v2/custom_nodes/install": {
+ "post": {
+ "tags": ["custom_nodes"],
+ "summary": "Install Custom Node Pack",
+ "description": "Installs a custom node pack from a git URL by cloning it into the nodes directory.",
+ "operationId": "install_custom_node_pack",
+ "requestBody": {
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/InstallNodePackRequest",
+ "description": "The source URL to install from."
+ }
+ }
+ },
+ "required": true
+ },
+ "responses": {
+ "200": {
+ "description": "Successful Response",
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/InstallNodePackResponse"
+ }
+ }
+ }
+ },
+ "422": {
+ "description": "Validation Error",
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/HTTPValidationError"
+ }
+ }
+ }
+ }
+ },
+ "security": [
+ {
+ "HTTPBearer": []
+ }
+ ]
+ }
+ },
+ "/api/v2/custom_nodes/{pack_name}": {
+ "delete": {
+ "tags": ["custom_nodes"],
+ "summary": "Uninstall Custom Node Pack",
+ "description": "Uninstalls a custom node pack by removing its directory.\n\nNote: A restart is required for the node removal to take full effect.\nInstalled nodes from the pack will remain registered until restart.",
+ "operationId": "uninstall_custom_node_pack",
+ "security": [
+ {
+ "HTTPBearer": []
+ }
+ ],
+ "parameters": [
+ {
+ "name": "pack_name",
+ "in": "path",
+ "required": true,
+ "schema": {
+ "type": "string",
+ "title": "Pack Name"
+ }
+ }
+ ],
+ "responses": {
+ "200": {
+ "description": "Successful Response",
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/UninstallNodePackResponse"
+ }
+ }
+ }
+ },
+ "422": {
+ "description": "Validation Error",
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/HTTPValidationError"
+ }
+ }
+ }
+ }
+ }
+ }
+ },
+ "/api/v2/custom_nodes/reload": {
+ "post": {
+ "tags": ["custom_nodes"],
+ "summary": "Reload Custom Nodes",
+ "description": "Triggers a reload of all custom nodes.\n\nThis re-scans the nodes directory and loads any new node packs.\nAlready loaded packs are skipped.",
+ "operationId": "reload_custom_nodes",
+ "responses": {
+ "200": {
+ "description": "Successful Response",
+ "content": {
+ "application/json": {
+ "schema": {
+ "additionalProperties": {
+ "type": "string"
+ },
+ "type": "object",
+ "title": "Response Reload Custom Nodes"
+ }
+ }
+ }
+ }
+ },
+ "security": [
+ {
+ "HTTPBearer": []
+ }
+ ]
+ }
+ }
+ },
+ "components": {
+ "schemas": {
+ "AddImagesToBoardResult": {
+ "properties": {
+ "affected_boards": {
+ "items": {
+ "type": "string"
+ },
+ "type": "array",
+ "title": "Affected Boards",
+ "description": "The ids of boards affected by the delete operation"
+ },
+ "added_images": {
+ "items": {
+ "type": "string"
+ },
+ "type": "array",
+ "title": "Added Images",
+ "description": "The image names that were added to the board"
+ }
+ },
+ "type": "object",
+ "required": ["affected_boards", "added_images"],
+ "title": "AddImagesToBoardResult"
+ },
+ "AddInvocation": {
+ "category": "math",
+ "class": "invocation",
+ "classification": "stable",
+ "description": "Adds two numbers",
+ "node_pack": "invokeai",
+ "properties": {
+ "id": {
+ "description": "The id of this instance of an invocation. Must be unique among all instances of invocations.",
+ "field_kind": "node_attribute",
+ "title": "Id",
+ "type": "string"
+ },
+ "is_intermediate": {
+ "default": false,
+ "description": "Whether or not this is an intermediate invocation.",
+ "field_kind": "node_attribute",
+ "input": "direct",
+ "orig_required": true,
+ "title": "Is Intermediate",
+ "type": "boolean",
+ "ui_hidden": false,
+ "ui_type": "IsIntermediate"
+ },
+ "use_cache": {
+ "default": true,
+ "description": "Whether or not to use the cache",
+ "field_kind": "node_attribute",
+ "title": "Use Cache",
+ "type": "boolean"
+ },
+ "a": {
+ "default": 0,
+ "description": "The first number",
+ "field_kind": "input",
+ "input": "any",
+ "orig_default": 0,
+ "orig_required": false,
+ "title": "A",
+ "type": "integer"
+ },
+ "b": {
+ "default": 0,
+ "description": "The second number",
+ "field_kind": "input",
+ "input": "any",
+ "orig_default": 0,
+ "orig_required": false,
+ "title": "B",
+ "type": "integer"
+ },
+ "type": {
+ "const": "add",
+ "default": "add",
+ "field_kind": "node_attribute",
+ "title": "type",
+ "type": "string"
+ }
+ },
+ "required": ["type", "id"],
+ "tags": ["math", "add"],
+ "title": "Add Integers",
+ "type": "object",
+ "version": "1.0.1",
+ "output": {
+ "$ref": "#/components/schemas/IntegerOutput"
+ }
+ },
+ "AddVideosToBoardResult": {
+ "properties": {
+ "affected_boards": {
+ "items": {
+ "type": "string"
+ },
+ "type": "array",
+ "title": "Affected Boards",
+ "description": "The ids of boards affected by the operation"
+ },
+ "added_videos": {
+ "items": {
+ "type": "string"
+ },
+ "type": "array",
+ "title": "Added Videos",
+ "description": "The video names that were added to the board"
+ }
+ },
+ "type": "object",
+ "required": ["affected_boards", "added_videos"],
+ "title": "AddVideosToBoardResult"
+ },
+ "AdminUserCreateRequest": {
+ "properties": {
+ "email": {
+ "type": "string",
+ "title": "Email",
+ "description": "User email address"
+ },
+ "display_name": {
+ "anyOf": [
+ {
+ "type": "string"
+ },
+ {
+ "type": "null"
+ }
+ ],
+ "title": "Display Name",
+ "description": "Display name"
+ },
+ "password": {
+ "type": "string",
+ "title": "Password",
+ "description": "User password"
+ },
+ "is_admin": {
+ "type": "boolean",
+ "title": "Is Admin",
+ "description": "Whether user should have admin privileges",
+ "default": false
+ }
+ },
+ "type": "object",
+ "required": ["email", "password"],
+ "title": "AdminUserCreateRequest",
+ "description": "Request body for admin to create a new user."
+ },
+ "AdminUserUpdateRequest": {
+ "properties": {
+ "display_name": {
+ "anyOf": [
+ {
+ "type": "string"
+ },
+ {
+ "type": "null"
+ }
+ ],
+ "title": "Display Name",
+ "description": "Display name"
+ },
+ "password": {
+ "anyOf": [
+ {
+ "type": "string"
+ },
+ {
+ "type": "null"
+ }
+ ],
+ "title": "Password",
+ "description": "New password"
+ },
+ "is_admin": {
+ "anyOf": [
+ {
+ "type": "boolean"
+ },
+ {
+ "type": "null"
+ }
+ ],
+ "title": "Is Admin",
+ "description": "Whether user should have admin privileges"
+ },
+ "is_active": {
+ "anyOf": [
+ {
+ "type": "boolean"
+ },
+ {
+ "type": "null"
+ }
+ ],
+ "title": "Is Active",
+ "description": "Whether user account should be active"
+ }
+ },
+ "type": "object",
+ "title": "AdminUserUpdateRequest",
+ "description": "Request body for admin to update any user."
+ },
+ "AlibabaCloudImageGenerationInvocation": {
+ "category": "image",
+ "class": "invocation",
+ "classification": "stable",
+ "description": "Generate images using an Alibaba Cloud DashScope external model.",
+ "node_pack": "invokeai",
+ "properties": {
+ "board": {
+ "anyOf": [
+ {
+ "$ref": "#/components/schemas/BoardField"
+ },
+ {
+ "type": "null"
+ }
+ ],
+ "default": null,
+ "description": "The board to save the image to",
+ "field_kind": "internal",
+ "input": "direct",
+ "orig_required": false,
+ "ui_hidden": false
+ },
+ "metadata": {
+ "anyOf": [
+ {
+ "$ref": "#/components/schemas/MetadataField"
+ },
+ {
+ "type": "null"
+ }
+ ],
+ "default": null,
+ "description": "Optional metadata to be saved with the image",
+ "field_kind": "internal",
+ "input": "connection",
+ "orig_required": false,
+ "ui_hidden": false
+ },
+ "id": {
+ "description": "The id of this instance of an invocation. Must be unique among all instances of invocations.",
+ "field_kind": "node_attribute",
+ "title": "Id",
+ "type": "string"
+ },
+ "is_intermediate": {
+ "default": false,
+ "description": "Whether or not this is an intermediate invocation.",
+ "field_kind": "node_attribute",
+ "input": "direct",
+ "orig_required": true,
+ "title": "Is Intermediate",
+ "type": "boolean",
+ "ui_hidden": false,
+ "ui_type": "IsIntermediate"
+ },
+ "use_cache": {
+ "default": true,
+ "description": "Whether or not to use the cache",
+ "field_kind": "node_attribute",
+ "title": "Use Cache",
+ "type": "boolean"
+ },
+ "model": {
+ "anyOf": [
+ {
+ "$ref": "#/components/schemas/ModelIdentifierField"
+ },
+ {
+ "type": "null"
+ }
+ ],
+ "default": null,
+ "description": "Main model (UNet, VAE, CLIP) to load",
+ "field_kind": "input",
+ "input": "any",
+ "orig_required": true,
+ "ui_model_base": ["external"],
+ "ui_model_format": ["external_api"],
+ "ui_model_provider_id": ["alibabacloud"],
+ "ui_model_type": ["external_image_generator"]
+ },
+ "mode": {
+ "default": "txt2img",
+ "description": "Generation mode. Not all modes are supported by every model; unsupported modes raise at runtime.",
+ "enum": ["txt2img", "img2img", "inpaint"],
+ "field_kind": "input",
+ "input": "any",
+ "orig_default": "txt2img",
+ "orig_required": false,
+ "title": "Mode",
+ "type": "string"
+ },
+ "prompt": {
+ "anyOf": [
+ {
+ "type": "string"
+ },
+ {
+ "type": "null"
+ }
],
"default": null,
"description": "Prompt",
@@ -11847,6 +13683,9 @@
{
"$ref": "#/components/schemas/Main_Diffusers_QwenImage_Config"
},
+ {
+ "$ref": "#/components/schemas/Main_Diffusers_Wan_Config"
+ },
{
"$ref": "#/components/schemas/Main_Diffusers_ZImage_Config"
},
@@ -11889,6 +13728,9 @@
{
"$ref": "#/components/schemas/Main_GGUF_QwenImage_Config"
},
+ {
+ "$ref": "#/components/schemas/Main_GGUF_Wan_Config"
+ },
{
"$ref": "#/components/schemas/Main_GGUF_ZImage_Config"
},
@@ -11907,6 +13749,9 @@
{
"$ref": "#/components/schemas/VAE_Checkpoint_Flux2_Config"
},
+ {
+ "$ref": "#/components/schemas/VAE_Checkpoint_Wan_Config"
+ },
{
"$ref": "#/components/schemas/VAE_Checkpoint_QwenImage_Config"
},
@@ -11922,6 +13767,9 @@
{
"$ref": "#/components/schemas/VAE_Diffusers_Flux2_Config"
},
+ {
+ "$ref": "#/components/schemas/VAE_Diffusers_Wan_Config"
+ },
{
"$ref": "#/components/schemas/ControlNet_Checkpoint_SD1_Config"
},
@@ -11973,6 +13821,9 @@
{
"$ref": "#/components/schemas/LoRA_LyCORIS_QwenImage_Config"
},
+ {
+ "$ref": "#/components/schemas/LoRA_LyCORIS_Wan_Config"
+ },
{
"$ref": "#/components/schemas/LoRA_LyCORIS_Anima_Config"
},
@@ -12024,6 +13875,9 @@
{
"$ref": "#/components/schemas/QwenVLEncoder_Checkpoint_Config"
},
+ {
+ "$ref": "#/components/schemas/WanT5Encoder_WanT5Encoder_Config"
+ },
{
"$ref": "#/components/schemas/TI_File_SD1_Config"
},
@@ -12389,6 +14243,7 @@
"external",
"qwen-image",
"anima",
+ "wan",
"unknown"
],
"title": "BaseModelType",
@@ -12497,6 +14352,9 @@
{
"$ref": "#/components/schemas/ImageField"
},
+ {
+ "$ref": "#/components/schemas/VideoField"
+ },
{
"items": {
"anyOf": [
@@ -12511,6 +14369,9 @@
},
{
"$ref": "#/components/schemas/ImageField"
+ },
+ {
+ "$ref": "#/components/schemas/VideoField"
}
]
},
@@ -13043,11 +14904,29 @@
"description": "The visibility of the board.",
"default": "private"
},
+ "cover_video_name": {
+ "anyOf": [
+ {
+ "type": "string"
+ },
+ {
+ "type": "null"
+ }
+ ],
+ "title": "Cover Video Name",
+ "description": "The name of the board's cover video, when the most recent item is a video."
+ },
"image_count": {
"type": "integer",
"title": "Image Count",
"description": "The number of images in the board."
},
+ "video_count": {
+ "type": "integer",
+ "title": "Video Count",
+ "description": "The number of videos in the board.",
+ "default": 0
+ },
"asset_count": {
"type": "integer",
"title": "Asset Count",
@@ -13238,6 +15117,21 @@
"required": ["image_names"],
"title": "Body_delete_images_from_list"
},
+ "Body_delete_videos_from_list": {
+ "properties": {
+ "video_names": {
+ "items": {
+ "type": "string"
+ },
+ "type": "array",
+ "title": "Video Names",
+ "description": "The list of names of videos to delete"
+ }
+ },
+ "type": "object",
+ "required": ["video_names"],
+ "title": "Body_delete_videos_from_list"
+ },
"Body_do_hf_login": {
"properties": {
"token": {
@@ -13445,6 +15339,18 @@
"required": ["image_names"],
"title": "Body_remove_images_from_board"
},
+ "Body_remove_video_from_board": {
+ "properties": {
+ "video_name": {
+ "type": "string",
+ "title": "Video Name",
+ "description": "The name of the video to remove from its board"
+ }
+ },
+ "type": "object",
+ "required": ["video_name"],
+ "title": "Body_remove_video_from_board"
+ },
"Body_set_workflow_thumbnail": {
"properties": {
"image": {
@@ -13473,6 +15379,21 @@
"required": ["image_names"],
"title": "Body_star_images_in_list"
},
+ "Body_star_videos_in_list": {
+ "properties": {
+ "video_names": {
+ "items": {
+ "type": "string"
+ },
+ "type": "array",
+ "title": "Video Names",
+ "description": "The list of names of videos to star"
+ }
+ },
+ "type": "object",
+ "required": ["video_names"],
+ "title": "Body_star_videos_in_list"
+ },
"Body_unstar_images_in_list": {
"properties": {
"image_names": {
@@ -13488,6 +15409,21 @@
"required": ["image_names"],
"title": "Body_unstar_images_in_list"
},
+ "Body_unstar_videos_in_list": {
+ "properties": {
+ "video_names": {
+ "items": {
+ "type": "string"
+ },
+ "type": "array",
+ "title": "Video Names",
+ "description": "The list of names of videos to unstar"
+ }
+ },
+ "type": "object",
+ "required": ["video_names"],
+ "title": "Body_unstar_videos_in_list"
+ },
"Body_update_model_image": {
"properties": {
"image": {
@@ -13585,6 +15521,30 @@
"required": ["file"],
"title": "Body_upload_image"
},
+ "Body_upload_video": {
+ "properties": {
+ "file": {
+ "type": "string",
+ "format": "binary",
+ "title": "File"
+ },
+ "metadata": {
+ "anyOf": [
+ {
+ "type": "string"
+ },
+ {
+ "type": "null"
+ }
+ ],
+ "title": "Metadata",
+ "description": "The metadata to associate with the video, must be a stringified JSON dict"
+ }
+ },
+ "type": "object",
+ "required": ["file"],
+ "title": "Body_upload_video"
+ },
"BooleanCollectionInvocation": {
"category": "primitives",
"class": "invocation",
@@ -19557,7 +21517,12 @@
"anima_txt2img",
"anima_img2img",
"anima_inpaint",
- "anima_outpaint"
+ "anima_outpaint",
+ "wan_txt2img",
+ "wan_img2img",
+ "wan_inpaint",
+ "wan_outpaint",
+ "wan_i2v"
],
"type": "string"
},
@@ -21057,6 +23022,38 @@
"type": "array",
"title": "Deleted Images",
"description": "The names of the images that were deleted."
+ },
+ "deleted_board_videos": {
+ "items": {
+ "type": "string"
+ },
+ "type": "array",
+ "title": "Deleted Board Videos",
+ "description": "The video names of the board-videos relationships that were deleted."
+ },
+ "deleted_videos": {
+ "items": {
+ "type": "string"
+ },
+ "type": "array",
+ "title": "Deleted Videos",
+ "description": "The names of the videos that were deleted."
+ },
+ "failed_images": {
+ "items": {
+ "type": "string"
+ },
+ "type": "array",
+ "title": "Failed Images",
+ "description": "The names of images that could not be deleted and became uncategorized."
+ },
+ "failed_videos": {
+ "items": {
+ "type": "string"
+ },
+ "type": "array",
+ "title": "Failed Videos",
+ "description": "The names of videos that could not be deleted and became uncategorized."
}
},
"type": "object",
@@ -21093,6 +23090,14 @@
"type": "array",
"title": "Deleted Images",
"description": "The names of the images that were deleted"
+ },
+ "failed_images": {
+ "items": {
+ "type": "string"
+ },
+ "type": "array",
+ "title": "Failed Images",
+ "description": "The names of authorized images that could not be deleted"
}
},
"type": "object",
@@ -21139,6 +23144,37 @@
"title": "DeleteOrphanedModelsResponse",
"description": "Response from deleting orphaned models."
},
+ "DeleteVideosResult": {
+ "properties": {
+ "affected_boards": {
+ "items": {
+ "type": "string"
+ },
+ "type": "array",
+ "title": "Affected Boards",
+ "description": "The ids of boards affected by the operation"
+ },
+ "deleted_videos": {
+ "items": {
+ "type": "string"
+ },
+ "type": "array",
+ "title": "Deleted Videos",
+ "description": "The names of the videos that were deleted"
+ },
+ "failed_videos": {
+ "items": {
+ "type": "string"
+ },
+ "type": "array",
+ "title": "Failed Videos",
+ "description": "The names of videos that were not deleted"
+ }
+ },
+ "type": "object",
+ "required": ["affected_boards", "deleted_videos", "failed_videos"],
+ "title": "DeleteVideosResult"
+ },
"DenoiseLatentsInvocation": {
"category": "latents",
"class": "invocation",
@@ -23585,6 +25621,218 @@
"required": ["label", "aspect_ratio", "image_size", "width", "height"],
"title": "ExternalResolutionPreset"
},
+ "ExtractVideoRangeInvocation": {
+ "category": "video",
+ "class": "invocation",
+ "classification": "prototype",
+ "description": "Trim a video to a contiguous frame range and re-encode as MP4.\n\nBoth bounds are inclusive and 0-based \u2014 ``start_frame=10, end_frame=50``\nemits 41 frames. Negative indices count from the end (``end_frame=-1``\nis the final frame), matching ``video_frame_extract``. The output frame\nrate defaults to the source video's frame rate; set ``fps=0`` to inherit\nit (or 16 fps if the source rate can't be probed).\n\nThe resolved (positive) ``start_frame`` and ``end_frame`` are also emitted as\noutputs, so chained workflows can re-use the boundary indices \u2014 e.g. feeding\nthem into a downstream Frame from Video to extract the same boundary frame.",
+ "node_pack": "invokeai",
+ "properties": {
+ "board": {
+ "anyOf": [
+ {
+ "$ref": "#/components/schemas/BoardField"
+ },
+ {
+ "type": "null"
+ }
+ ],
+ "default": null,
+ "description": "The board to save the image to",
+ "field_kind": "internal",
+ "input": "direct",
+ "orig_required": false,
+ "ui_hidden": false
+ },
+ "metadata": {
+ "anyOf": [
+ {
+ "$ref": "#/components/schemas/MetadataField"
+ },
+ {
+ "type": "null"
+ }
+ ],
+ "default": null,
+ "description": "Optional metadata to be saved with the image",
+ "field_kind": "internal",
+ "input": "connection",
+ "orig_required": false,
+ "ui_hidden": false
+ },
+ "id": {
+ "description": "The id of this instance of an invocation. Must be unique among all instances of invocations.",
+ "field_kind": "node_attribute",
+ "title": "Id",
+ "type": "string"
+ },
+ "is_intermediate": {
+ "default": false,
+ "description": "Whether or not this is an intermediate invocation.",
+ "field_kind": "node_attribute",
+ "input": "direct",
+ "orig_required": true,
+ "title": "Is Intermediate",
+ "type": "boolean",
+ "ui_hidden": false,
+ "ui_type": "IsIntermediate"
+ },
+ "use_cache": {
+ "default": true,
+ "description": "Whether or not to use the cache",
+ "field_kind": "node_attribute",
+ "title": "Use Cache",
+ "type": "boolean"
+ },
+ "video": {
+ "anyOf": [
+ {
+ "$ref": "#/components/schemas/VideoField"
+ },
+ {
+ "type": "null"
+ }
+ ],
+ "default": null,
+ "description": "The video to extract a frame range from.",
+ "field_kind": "input",
+ "input": "any",
+ "orig_required": true
+ },
+ "start_frame": {
+ "default": 0,
+ "description": "First frame to keep, inclusive. 0 = first frame. Negative indices count from the end.",
+ "field_kind": "input",
+ "input": "any",
+ "orig_default": 0,
+ "orig_required": false,
+ "title": "Start Frame",
+ "type": "integer",
+ "ui_component": "video-frame-index"
+ },
+ "end_frame": {
+ "default": -1,
+ "description": "Last frame to keep, inclusive. -1 = last frame. Negative indices count from the end.",
+ "field_kind": "input",
+ "input": "any",
+ "orig_default": -1,
+ "orig_required": false,
+ "title": "End Frame",
+ "type": "integer",
+ "ui_component": "video-frame-index"
+ },
+ "fps": {
+ "default": 0,
+ "description": "Output frame rate. 0 = match the source video's frame rate (falls back to 16 fps if the source rate can't be probed).",
+ "field_kind": "input",
+ "input": "any",
+ "maximum": 120,
+ "minimum": 0,
+ "orig_default": 0,
+ "orig_required": false,
+ "title": "Fps",
+ "type": "integer"
+ },
+ "type": {
+ "const": "extract_video_range",
+ "default": "extract_video_range",
+ "field_kind": "node_attribute",
+ "title": "type",
+ "type": "string"
+ }
+ },
+ "required": ["type", "id"],
+ "tags": ["video", "trim", "range", "frames"],
+ "title": "Frame Range from Video",
+ "type": "object",
+ "version": "1.1.0",
+ "output": {
+ "$ref": "#/components/schemas/ExtractVideoRangeOutput"
+ }
+ },
+ "ExtractVideoRangeOutput": {
+ "class": "output",
+ "description": "Output of ``extract_video_range``: a trimmed video plus the resolved frame indices.\n\nMirrors ``VideoOutput`` so the video can be piped directly into Concatenate Videos or\nany other ``VideoField``-consuming node, and additionally exposes the resolved\n(positive, clamped) start and end indices so chained workflows can feed them back in\n\u2014 e.g. drive a downstream Frame from Video to pull the same boundary frame.",
+ "properties": {
+ "video": {
+ "$ref": "#/components/schemas/VideoField",
+ "description": "The trimmed video",
+ "field_kind": "output",
+ "ui_hidden": false
+ },
+ "width": {
+ "description": "The width of the video in pixels",
+ "field_kind": "output",
+ "title": "Width",
+ "type": "integer",
+ "ui_hidden": false
+ },
+ "height": {
+ "description": "The height of the video in pixels",
+ "field_kind": "output",
+ "title": "Height",
+ "type": "integer",
+ "ui_hidden": false
+ },
+ "num_frames": {
+ "description": "The number of frames in the trimmed video",
+ "field_kind": "output",
+ "title": "Num Frames",
+ "type": "integer",
+ "ui_hidden": false
+ },
+ "fps": {
+ "description": "The frames-per-second of the trimmed video",
+ "field_kind": "output",
+ "title": "Fps",
+ "type": "number",
+ "ui_hidden": false
+ },
+ "duration": {
+ "description": "The duration of the trimmed video in seconds",
+ "field_kind": "output",
+ "title": "Duration",
+ "type": "number",
+ "ui_hidden": false
+ },
+ "start_frame": {
+ "description": "The resolved (positive, 0-based) start frame index in the source video",
+ "field_kind": "output",
+ "title": "Start Frame",
+ "type": "integer",
+ "ui_hidden": false
+ },
+ "end_frame": {
+ "description": "The resolved (positive, 0-based) end frame index in the source video",
+ "field_kind": "output",
+ "title": "End Frame",
+ "type": "integer",
+ "ui_hidden": false
+ },
+ "type": {
+ "const": "extract_video_range_output",
+ "default": "extract_video_range_output",
+ "field_kind": "node_attribute",
+ "title": "type",
+ "type": "string"
+ }
+ },
+ "required": [
+ "output_meta",
+ "video",
+ "width",
+ "height",
+ "num_frames",
+ "fps",
+ "duration",
+ "start_frame",
+ "end_frame",
+ "type",
+ "type"
+ ],
+ "title": "ExtractVideoRangeOutput",
+ "type": "object"
+ },
"FLUXLoRACollectionLoader": {
"category": "model",
"class": "invocation",
@@ -28894,6 +31142,166 @@
"$ref": "#/components/schemas/UNetOutput"
}
},
+ "GalleryItem": {
+ "properties": {
+ "kind": {
+ "$ref": "#/components/schemas/GalleryItemKind",
+ "description": "Whether the item is an image or video."
+ },
+ "name": {
+ "type": "string",
+ "title": "Name",
+ "description": "The unique name of the image or video."
+ },
+ "full_url": {
+ "type": "string",
+ "title": "Full Url",
+ "description": "URL to the full-resolution image PNG or the full-quality video MP4."
+ },
+ "thumbnail_url": {
+ "type": "string",
+ "title": "Thumbnail Url",
+ "description": "URL to the static (WebP) thumbnail."
+ },
+ "width": {
+ "type": "integer",
+ "title": "Width",
+ "description": "The width of the item in pixels."
+ },
+ "height": {
+ "type": "integer",
+ "title": "Height",
+ "description": "The height of the item in pixels."
+ },
+ "category": {
+ "$ref": "#/components/schemas/ImageCategory",
+ "description": "The category of the item (images and videos share the same enum)."
+ },
+ "starred": {
+ "type": "boolean",
+ "title": "Starred",
+ "description": "Whether the item is starred."
+ },
+ "is_intermediate": {
+ "type": "boolean",
+ "title": "Is Intermediate",
+ "description": "Whether the item is an intermediate output."
+ },
+ "board_id": {
+ "anyOf": [
+ {
+ "type": "string"
+ },
+ {
+ "type": "null"
+ }
+ ],
+ "title": "Board Id",
+ "description": "Owning board id, if any."
+ },
+ "created_at": {
+ "anyOf": [
+ {
+ "type": "string",
+ "format": "date-time"
+ },
+ {
+ "type": "string"
+ }
+ ],
+ "title": "Created At",
+ "description": "The created timestamp of the item."
+ },
+ "duration": {
+ "anyOf": [
+ {
+ "type": "number"
+ },
+ {
+ "type": "null"
+ }
+ ],
+ "title": "Duration",
+ "description": "Video duration in seconds. None for images."
+ },
+ "fps": {
+ "anyOf": [
+ {
+ "type": "number"
+ },
+ {
+ "type": "null"
+ }
+ ],
+ "title": "Fps",
+ "description": "Video frames per second. None for images."
+ }
+ },
+ "type": "object",
+ "required": [
+ "kind",
+ "name",
+ "full_url",
+ "thumbnail_url",
+ "width",
+ "height",
+ "category",
+ "starred",
+ "is_intermediate",
+ "created_at"
+ ],
+ "title": "GalleryItem",
+ "description": "A gallery item \u2014 either an image or a video, with shared fields and a discriminator.\n\nFrontend code should dispatch on `kind` to render image- vs video-specific UI."
+ },
+ "GalleryItemKind": {
+ "type": "string",
+ "enum": ["image", "video"],
+ "title": "GalleryItemKind",
+ "description": "Discriminator for polymorphic gallery items."
+ },
+ "GalleryItemNamesResult": {
+ "properties": {
+ "items": {
+ "items": {
+ "$ref": "#/components/schemas/GalleryItemRef"
+ },
+ "type": "array",
+ "title": "Items",
+ "description": "Ordered list of (kind, name) references."
+ },
+ "starred_count": {
+ "type": "integer",
+ "title": "Starred Count",
+ "description": "Number of starred items (when starred_first=True)."
+ },
+ "total_count": {
+ "type": "integer",
+ "title": "Total Count",
+ "description": "Total number of items matching the query."
+ }
+ },
+ "type": "object",
+ "required": ["items", "starred_count", "total_count"],
+ "title": "GalleryItemNamesResult",
+ "description": "Ordered list of gallery item references plus counts for optimistic UI."
+ },
+ "GalleryItemRef": {
+ "properties": {
+ "kind": {
+ "$ref": "#/components/schemas/GalleryItemKind",
+ "description": "Whether the item is an image or video."
+ },
+ "name": {
+ "type": "string",
+ "title": "Name",
+ "description": "The unique name of the image or video."
+ }
+ },
+ "type": "object",
+ "required": ["kind", "name"],
+ "title": "GalleryItemRef",
+ "description": "A thin reference to a gallery item \u2014 used for ordered name lists."
+ },
"GeminiImageGenerationInvocation": {
"category": "image",
"class": "invocation",
@@ -29504,6 +31912,9 @@
{
"$ref": "#/components/schemas/ExpandMaskWithFadeInvocation"
},
+ {
+ "$ref": "#/components/schemas/ExtractVideoRangeInvocation"
+ },
{
"$ref": "#/components/schemas/FLUXLoRACollectionLoader"
},
@@ -30071,6 +32482,51 @@
{
"$ref": "#/components/schemas/VAELoaderInvocation"
},
+ {
+ "$ref": "#/components/schemas/VideoConcatInvocation"
+ },
+ {
+ "$ref": "#/components/schemas/VideoFrameExtractInvocation"
+ },
+ {
+ "$ref": "#/components/schemas/VideoInvocation"
+ },
+ {
+ "$ref": "#/components/schemas/WanDenoiseInvocation"
+ },
+ {
+ "$ref": "#/components/schemas/WanI2VIdealDimensionsInvocation"
+ },
+ {
+ "$ref": "#/components/schemas/WanImageToLatentsInvocation"
+ },
+ {
+ "$ref": "#/components/schemas/WanLatentsToImageInvocation"
+ },
+ {
+ "$ref": "#/components/schemas/WanLatentsToVideoInvocation"
+ },
+ {
+ "$ref": "#/components/schemas/WanLoRACollectionLoader"
+ },
+ {
+ "$ref": "#/components/schemas/WanLoRALoaderInvocation"
+ },
+ {
+ "$ref": "#/components/schemas/WanModelLoaderInvocation"
+ },
+ {
+ "$ref": "#/components/schemas/WanRefImageEncoderInvocation"
+ },
+ {
+ "$ref": "#/components/schemas/WanTI2VIdealDimensionsInvocation"
+ },
+ {
+ "$ref": "#/components/schemas/WanTextEncoderInvocation"
+ },
+ {
+ "$ref": "#/components/schemas/WanVideoDenoiseInvocation"
+ },
{
"$ref": "#/components/schemas/WorkflowReturnGetInvocation"
},
@@ -30224,6 +32680,9 @@
{
"$ref": "#/components/schemas/DenoiseMaskOutput"
},
+ {
+ "$ref": "#/components/schemas/ExtractVideoRangeOutput"
+ },
{
"$ref": "#/components/schemas/FaceMaskOutput"
},
@@ -30425,6 +32884,21 @@
{
"$ref": "#/components/schemas/VAEOutput"
},
+ {
+ "$ref": "#/components/schemas/VideoOutput"
+ },
+ {
+ "$ref": "#/components/schemas/WanConditioningOutput"
+ },
+ {
+ "$ref": "#/components/schemas/WanLoRALoaderOutput"
+ },
+ {
+ "$ref": "#/components/schemas/WanModelLoaderOutput"
+ },
+ {
+ "$ref": "#/components/schemas/WanRefImageOutput"
+ },
{
"$ref": "#/components/schemas/WorkflowReturnGetOutput"
},
@@ -37206,6 +39680,9 @@
{
"$ref": "#/components/schemas/ExpandMaskWithFadeInvocation"
},
+ {
+ "$ref": "#/components/schemas/ExtractVideoRangeInvocation"
+ },
{
"$ref": "#/components/schemas/FLUXLoRACollectionLoader"
},
@@ -37773,6 +40250,51 @@
{
"$ref": "#/components/schemas/VAELoaderInvocation"
},
+ {
+ "$ref": "#/components/schemas/VideoConcatInvocation"
+ },
+ {
+ "$ref": "#/components/schemas/VideoFrameExtractInvocation"
+ },
+ {
+ "$ref": "#/components/schemas/VideoInvocation"
+ },
+ {
+ "$ref": "#/components/schemas/WanDenoiseInvocation"
+ },
+ {
+ "$ref": "#/components/schemas/WanI2VIdealDimensionsInvocation"
+ },
+ {
+ "$ref": "#/components/schemas/WanImageToLatentsInvocation"
+ },
+ {
+ "$ref": "#/components/schemas/WanLatentsToImageInvocation"
+ },
+ {
+ "$ref": "#/components/schemas/WanLatentsToVideoInvocation"
+ },
+ {
+ "$ref": "#/components/schemas/WanLoRACollectionLoader"
+ },
+ {
+ "$ref": "#/components/schemas/WanLoRALoaderInvocation"
+ },
+ {
+ "$ref": "#/components/schemas/WanModelLoaderInvocation"
+ },
+ {
+ "$ref": "#/components/schemas/WanRefImageEncoderInvocation"
+ },
+ {
+ "$ref": "#/components/schemas/WanTI2VIdealDimensionsInvocation"
+ },
+ {
+ "$ref": "#/components/schemas/WanTextEncoderInvocation"
+ },
+ {
+ "$ref": "#/components/schemas/WanVideoDenoiseInvocation"
+ },
{
"$ref": "#/components/schemas/WorkflowReturnGetInvocation"
},
@@ -37883,6 +40405,9 @@
{
"$ref": "#/components/schemas/DenoiseMaskOutput"
},
+ {
+ "$ref": "#/components/schemas/ExtractVideoRangeOutput"
+ },
{
"$ref": "#/components/schemas/FaceMaskOutput"
},
@@ -38084,6 +40609,21 @@
{
"$ref": "#/components/schemas/VAEOutput"
},
+ {
+ "$ref": "#/components/schemas/VideoOutput"
+ },
+ {
+ "$ref": "#/components/schemas/WanConditioningOutput"
+ },
+ {
+ "$ref": "#/components/schemas/WanLoRALoaderOutput"
+ },
+ {
+ "$ref": "#/components/schemas/WanModelLoaderOutput"
+ },
+ {
+ "$ref": "#/components/schemas/WanRefImageOutput"
+ },
{
"$ref": "#/components/schemas/WorkflowReturnGetOutput"
},
@@ -38362,6 +40902,9 @@
{
"$ref": "#/components/schemas/ExpandMaskWithFadeInvocation"
},
+ {
+ "$ref": "#/components/schemas/ExtractVideoRangeInvocation"
+ },
{
"$ref": "#/components/schemas/FLUXLoRACollectionLoader"
},
@@ -38929,6 +41472,51 @@
{
"$ref": "#/components/schemas/VAELoaderInvocation"
},
+ {
+ "$ref": "#/components/schemas/VideoConcatInvocation"
+ },
+ {
+ "$ref": "#/components/schemas/VideoFrameExtractInvocation"
+ },
+ {
+ "$ref": "#/components/schemas/VideoInvocation"
+ },
+ {
+ "$ref": "#/components/schemas/WanDenoiseInvocation"
+ },
+ {
+ "$ref": "#/components/schemas/WanI2VIdealDimensionsInvocation"
+ },
+ {
+ "$ref": "#/components/schemas/WanImageToLatentsInvocation"
+ },
+ {
+ "$ref": "#/components/schemas/WanLatentsToImageInvocation"
+ },
+ {
+ "$ref": "#/components/schemas/WanLatentsToVideoInvocation"
+ },
+ {
+ "$ref": "#/components/schemas/WanLoRACollectionLoader"
+ },
+ {
+ "$ref": "#/components/schemas/WanLoRALoaderInvocation"
+ },
+ {
+ "$ref": "#/components/schemas/WanModelLoaderInvocation"
+ },
+ {
+ "$ref": "#/components/schemas/WanRefImageEncoderInvocation"
+ },
+ {
+ "$ref": "#/components/schemas/WanTI2VIdealDimensionsInvocation"
+ },
+ {
+ "$ref": "#/components/schemas/WanTextEncoderInvocation"
+ },
+ {
+ "$ref": "#/components/schemas/WanVideoDenoiseInvocation"
+ },
{
"$ref": "#/components/schemas/WorkflowReturnGetInvocation"
},
@@ -39178,6 +41766,9 @@
"expand_mask_with_fade": {
"$ref": "#/components/schemas/ImageOutput"
},
+ "extract_video_range": {
+ "$ref": "#/components/schemas/ExtractVideoRangeOutput"
+ },
"face_identifier": {
"$ref": "#/components/schemas/ImageOutput"
},
@@ -39754,6 +42345,51 @@
"vae_loader": {
"$ref": "#/components/schemas/VAEOutput"
},
+ "video": {
+ "$ref": "#/components/schemas/VideoOutput"
+ },
+ "video_concat": {
+ "$ref": "#/components/schemas/VideoOutput"
+ },
+ "video_frame_extract": {
+ "$ref": "#/components/schemas/ImageOutput"
+ },
+ "wan_denoise": {
+ "$ref": "#/components/schemas/LatentsOutput"
+ },
+ "wan_i2l": {
+ "$ref": "#/components/schemas/LatentsOutput"
+ },
+ "wan_i2v_ideal_dimensions": {
+ "$ref": "#/components/schemas/IdealSizeOutput"
+ },
+ "wan_l2i": {
+ "$ref": "#/components/schemas/ImageOutput"
+ },
+ "wan_l2v": {
+ "$ref": "#/components/schemas/VideoOutput"
+ },
+ "wan_lora_collection_loader": {
+ "$ref": "#/components/schemas/WanLoRALoaderOutput"
+ },
+ "wan_lora_loader": {
+ "$ref": "#/components/schemas/WanLoRALoaderOutput"
+ },
+ "wan_model_loader": {
+ "$ref": "#/components/schemas/WanModelLoaderOutput"
+ },
+ "wan_ref_image_encoder": {
+ "$ref": "#/components/schemas/WanRefImageOutput"
+ },
+ "wan_text_encoder": {
+ "$ref": "#/components/schemas/WanConditioningOutput"
+ },
+ "wan_ti2v_ideal_dimensions": {
+ "$ref": "#/components/schemas/IdealSizeOutput"
+ },
+ "wan_video_denoise": {
+ "$ref": "#/components/schemas/LatentsOutput"
+ },
"workflow_return": {
"$ref": "#/components/schemas/WorkflowReturnOutput"
},
@@ -39850,6 +42486,7 @@
"dynamic_prompt",
"esrgan",
"expand_mask_with_fade",
+ "extract_video_range",
"face_identifier",
"face_mask_detection",
"face_off",
@@ -40042,6 +42679,21 @@
"unsharp_mask",
"unsharp_mask_oklab",
"vae_loader",
+ "video",
+ "video_concat",
+ "video_frame_extract",
+ "wan_denoise",
+ "wan_i2l",
+ "wan_i2v_ideal_dimensions",
+ "wan_l2i",
+ "wan_l2v",
+ "wan_lora_collection_loader",
+ "wan_lora_loader",
+ "wan_model_loader",
+ "wan_ref_image_encoder",
+ "wan_text_encoder",
+ "wan_ti2v_ideal_dimensions",
+ "wan_video_denoise",
"workflow_return",
"workflow_return_get",
"workflow_return_value",
@@ -40294,6 +42946,9 @@
{
"$ref": "#/components/schemas/ExpandMaskWithFadeInvocation"
},
+ {
+ "$ref": "#/components/schemas/ExtractVideoRangeInvocation"
+ },
{
"$ref": "#/components/schemas/FLUXLoRACollectionLoader"
},
@@ -40861,6 +43516,51 @@
{
"$ref": "#/components/schemas/VAELoaderInvocation"
},
+ {
+ "$ref": "#/components/schemas/VideoConcatInvocation"
+ },
+ {
+ "$ref": "#/components/schemas/VideoFrameExtractInvocation"
+ },
+ {
+ "$ref": "#/components/schemas/VideoInvocation"
+ },
+ {
+ "$ref": "#/components/schemas/WanDenoiseInvocation"
+ },
+ {
+ "$ref": "#/components/schemas/WanI2VIdealDimensionsInvocation"
+ },
+ {
+ "$ref": "#/components/schemas/WanImageToLatentsInvocation"
+ },
+ {
+ "$ref": "#/components/schemas/WanLatentsToImageInvocation"
+ },
+ {
+ "$ref": "#/components/schemas/WanLatentsToVideoInvocation"
+ },
+ {
+ "$ref": "#/components/schemas/WanLoRACollectionLoader"
+ },
+ {
+ "$ref": "#/components/schemas/WanLoRALoaderInvocation"
+ },
+ {
+ "$ref": "#/components/schemas/WanModelLoaderInvocation"
+ },
+ {
+ "$ref": "#/components/schemas/WanRefImageEncoderInvocation"
+ },
+ {
+ "$ref": "#/components/schemas/WanTI2VIdealDimensionsInvocation"
+ },
+ {
+ "$ref": "#/components/schemas/WanTextEncoderInvocation"
+ },
+ {
+ "$ref": "#/components/schemas/WanVideoDenoiseInvocation"
+ },
{
"$ref": "#/components/schemas/WorkflowReturnGetInvocation"
},
@@ -41023,1056 +43723,4118 @@
"description": "The ID of the invocation",
"oneOf": [
{
- "$ref": "#/components/schemas/AddInvocation"
+ "$ref": "#/components/schemas/AddInvocation"
+ },
+ {
+ "$ref": "#/components/schemas/AlibabaCloudImageGenerationInvocation"
+ },
+ {
+ "$ref": "#/components/schemas/AlphaMaskToTensorInvocation"
+ },
+ {
+ "$ref": "#/components/schemas/AnimaDenoiseInvocation"
+ },
+ {
+ "$ref": "#/components/schemas/AnimaImageToLatentsInvocation"
+ },
+ {
+ "$ref": "#/components/schemas/AnimaLLLiteInvocation"
+ },
+ {
+ "$ref": "#/components/schemas/AnimaLatentsToImageInvocation"
+ },
+ {
+ "$ref": "#/components/schemas/AnimaLoRACollectionLoader"
+ },
+ {
+ "$ref": "#/components/schemas/AnimaLoRALoaderInvocation"
+ },
+ {
+ "$ref": "#/components/schemas/AnimaModelLoaderInvocation"
+ },
+ {
+ "$ref": "#/components/schemas/AnimaTextEncoderInvocation"
+ },
+ {
+ "$ref": "#/components/schemas/ApplyMaskTensorToImageInvocation"
+ },
+ {
+ "$ref": "#/components/schemas/ApplyMaskToImageInvocation"
+ },
+ {
+ "$ref": "#/components/schemas/BlankImageInvocation"
+ },
+ {
+ "$ref": "#/components/schemas/BlendLatentsInvocation"
+ },
+ {
+ "$ref": "#/components/schemas/BooleanCollectionInvocation"
+ },
+ {
+ "$ref": "#/components/schemas/BooleanInvocation"
+ },
+ {
+ "$ref": "#/components/schemas/BoundingBoxInvocation"
+ },
+ {
+ "$ref": "#/components/schemas/CLIPSkipInvocation"
+ },
+ {
+ "$ref": "#/components/schemas/CV2InfillInvocation"
+ },
+ {
+ "$ref": "#/components/schemas/CalculateImageTilesEvenSplitInvocation"
+ },
+ {
+ "$ref": "#/components/schemas/CalculateImageTilesInvocation"
+ },
+ {
+ "$ref": "#/components/schemas/CalculateImageTilesMinimumOverlapInvocation"
+ },
+ {
+ "$ref": "#/components/schemas/CallSavedWorkflowInvocation"
+ },
+ {
+ "$ref": "#/components/schemas/CannyEdgeDetectionInvocation"
+ },
+ {
+ "$ref": "#/components/schemas/CanvasOutputInvocation"
+ },
+ {
+ "$ref": "#/components/schemas/CanvasPasteBackInvocation"
+ },
+ {
+ "$ref": "#/components/schemas/CanvasV2MaskAndCropInvocation"
+ },
+ {
+ "$ref": "#/components/schemas/CenterPadCropInvocation"
+ },
+ {
+ "$ref": "#/components/schemas/CogView4DenoiseInvocation"
+ },
+ {
+ "$ref": "#/components/schemas/CogView4ImageToLatentsInvocation"
+ },
+ {
+ "$ref": "#/components/schemas/CogView4LatentsToImageInvocation"
+ },
+ {
+ "$ref": "#/components/schemas/CogView4ModelLoaderInvocation"
+ },
+ {
+ "$ref": "#/components/schemas/CogView4TextEncoderInvocation"
+ },
+ {
+ "$ref": "#/components/schemas/CollectInvocation"
+ },
+ {
+ "$ref": "#/components/schemas/ColorCorrectInvocation"
+ },
+ {
+ "$ref": "#/components/schemas/ColorInvocation"
+ },
+ {
+ "$ref": "#/components/schemas/ColorMapInvocation"
+ },
+ {
+ "$ref": "#/components/schemas/CompelInvocation"
+ },
+ {
+ "$ref": "#/components/schemas/ConditioningCollectionInvocation"
+ },
+ {
+ "$ref": "#/components/schemas/ConditioningInvocation"
+ },
+ {
+ "$ref": "#/components/schemas/ContentShuffleInvocation"
+ },
+ {
+ "$ref": "#/components/schemas/ControlNetInvocation"
+ },
+ {
+ "$ref": "#/components/schemas/CoreMetadataInvocation"
+ },
+ {
+ "$ref": "#/components/schemas/CreateDenoiseMaskInvocation"
+ },
+ {
+ "$ref": "#/components/schemas/CreateGradientMaskInvocation"
+ },
+ {
+ "$ref": "#/components/schemas/CropImageToBoundingBoxInvocation"
+ },
+ {
+ "$ref": "#/components/schemas/CropLatentsCoreInvocation"
+ },
+ {
+ "$ref": "#/components/schemas/CvInpaintInvocation"
+ },
+ {
+ "$ref": "#/components/schemas/DWOpenposeDetectionInvocation"
+ },
+ {
+ "$ref": "#/components/schemas/DecodeInvisibleWatermarkInvocation"
+ },
+ {
+ "$ref": "#/components/schemas/DenoiseLatentsInvocation"
+ },
+ {
+ "$ref": "#/components/schemas/DenoiseLatentsMetaInvocation"
+ },
+ {
+ "$ref": "#/components/schemas/DepthAnythingDepthEstimationInvocation"
+ },
+ {
+ "$ref": "#/components/schemas/DivideInvocation"
+ },
+ {
+ "$ref": "#/components/schemas/DynamicPromptInvocation"
+ },
+ {
+ "$ref": "#/components/schemas/ESRGANInvocation"
+ },
+ {
+ "$ref": "#/components/schemas/ExpandMaskWithFadeInvocation"
+ },
+ {
+ "$ref": "#/components/schemas/ExtractVideoRangeInvocation"
+ },
+ {
+ "$ref": "#/components/schemas/FLUXLoRACollectionLoader"
+ },
+ {
+ "$ref": "#/components/schemas/FaceIdentifierInvocation"
+ },
+ {
+ "$ref": "#/components/schemas/FaceMaskInvocation"
+ },
+ {
+ "$ref": "#/components/schemas/FaceOffInvocation"
+ },
+ {
+ "$ref": "#/components/schemas/FloatBatchInvocation"
+ },
+ {
+ "$ref": "#/components/schemas/FloatCollectionInvocation"
+ },
+ {
+ "$ref": "#/components/schemas/FloatGenerator"
+ },
+ {
+ "$ref": "#/components/schemas/FloatInvocation"
+ },
+ {
+ "$ref": "#/components/schemas/FloatLinearRangeInvocation"
+ },
+ {
+ "$ref": "#/components/schemas/FloatMathInvocation"
+ },
+ {
+ "$ref": "#/components/schemas/FloatToIntegerInvocation"
+ },
+ {
+ "$ref": "#/components/schemas/Flux2DenoiseInvocation"
+ },
+ {
+ "$ref": "#/components/schemas/Flux2KleinLoRACollectionLoader"
+ },
+ {
+ "$ref": "#/components/schemas/Flux2KleinLoRALoaderInvocation"
+ },
+ {
+ "$ref": "#/components/schemas/Flux2KleinModelLoaderInvocation"
+ },
+ {
+ "$ref": "#/components/schemas/Flux2KleinTextEncoderInvocation"
+ },
+ {
+ "$ref": "#/components/schemas/Flux2VaeDecodeInvocation"
+ },
+ {
+ "$ref": "#/components/schemas/Flux2VaeEncodeInvocation"
+ },
+ {
+ "$ref": "#/components/schemas/FluxControlLoRALoaderInvocation"
+ },
+ {
+ "$ref": "#/components/schemas/FluxControlNetInvocation"
+ },
+ {
+ "$ref": "#/components/schemas/FluxDenoiseInvocation"
+ },
+ {
+ "$ref": "#/components/schemas/FluxDenoiseLatentsMetaInvocation"
+ },
+ {
+ "$ref": "#/components/schemas/FluxFillInvocation"
+ },
+ {
+ "$ref": "#/components/schemas/FluxIPAdapterInvocation"
+ },
+ {
+ "$ref": "#/components/schemas/FluxKontextConcatenateImagesInvocation"
+ },
+ {
+ "$ref": "#/components/schemas/FluxKontextInvocation"
+ },
+ {
+ "$ref": "#/components/schemas/FluxLoRALoaderInvocation"
+ },
+ {
+ "$ref": "#/components/schemas/FluxModelLoaderInvocation"
+ },
+ {
+ "$ref": "#/components/schemas/FluxReduxInvocation"
+ },
+ {
+ "$ref": "#/components/schemas/FluxTextEncoderInvocation"
+ },
+ {
+ "$ref": "#/components/schemas/FluxVaeDecodeInvocation"
+ },
+ {
+ "$ref": "#/components/schemas/FluxVaeEncodeInvocation"
+ },
+ {
+ "$ref": "#/components/schemas/FreeUInvocation"
+ },
+ {
+ "$ref": "#/components/schemas/GeminiImageGenerationInvocation"
+ },
+ {
+ "$ref": "#/components/schemas/GetMaskBoundingBoxInvocation"
+ },
+ {
+ "$ref": "#/components/schemas/GroundingDinoInvocation"
+ },
+ {
+ "$ref": "#/components/schemas/HEDEdgeDetectionInvocation"
+ },
+ {
+ "$ref": "#/components/schemas/HeuristicResizeInvocation"
+ },
+ {
+ "$ref": "#/components/schemas/IPAdapterInvocation"
+ },
+ {
+ "$ref": "#/components/schemas/IdealSizeInvocation"
+ },
+ {
+ "$ref": "#/components/schemas/IfInvocation"
+ },
+ {
+ "$ref": "#/components/schemas/ImageBatchInvocation"
+ },
+ {
+ "$ref": "#/components/schemas/ImageBlurInvocation"
+ },
+ {
+ "$ref": "#/components/schemas/ImageChannelInvocation"
+ },
+ {
+ "$ref": "#/components/schemas/ImageChannelMultiplyInvocation"
+ },
+ {
+ "$ref": "#/components/schemas/ImageChannelOffsetInvocation"
+ },
+ {
+ "$ref": "#/components/schemas/ImageCollectionInvocation"
+ },
+ {
+ "$ref": "#/components/schemas/ImageConvertInvocation"
+ },
+ {
+ "$ref": "#/components/schemas/ImageCropInvocation"
+ },
+ {
+ "$ref": "#/components/schemas/ImageGenerator"
+ },
+ {
+ "$ref": "#/components/schemas/ImageHueAdjustmentInvocation"
+ },
+ {
+ "$ref": "#/components/schemas/ImageInverseLerpInvocation"
+ },
+ {
+ "$ref": "#/components/schemas/ImageInvocation"
+ },
+ {
+ "$ref": "#/components/schemas/ImageLerpInvocation"
+ },
+ {
+ "$ref": "#/components/schemas/ImageMaskToTensorInvocation"
+ },
+ {
+ "$ref": "#/components/schemas/ImageMultiplyInvocation"
+ },
+ {
+ "$ref": "#/components/schemas/ImageNSFWBlurInvocation"
+ },
+ {
+ "$ref": "#/components/schemas/ImageNoiseInvocation"
+ },
+ {
+ "$ref": "#/components/schemas/ImagePanelLayoutInvocation"
+ },
+ {
+ "$ref": "#/components/schemas/ImagePasteInvocation"
+ },
+ {
+ "$ref": "#/components/schemas/ImageResizeInvocation"
+ },
+ {
+ "$ref": "#/components/schemas/ImageScaleInvocation"
+ },
+ {
+ "$ref": "#/components/schemas/ImageToLatentsInvocation"
+ },
+ {
+ "$ref": "#/components/schemas/ImageWatermarkInvocation"
+ },
+ {
+ "$ref": "#/components/schemas/InfillColorInvocation"
+ },
+ {
+ "$ref": "#/components/schemas/InfillPatchMatchInvocation"
+ },
+ {
+ "$ref": "#/components/schemas/InfillTileInvocation"
+ },
+ {
+ "$ref": "#/components/schemas/IntegerBatchInvocation"
+ },
+ {
+ "$ref": "#/components/schemas/IntegerCollectionInvocation"
+ },
+ {
+ "$ref": "#/components/schemas/IntegerGenerator"
+ },
+ {
+ "$ref": "#/components/schemas/IntegerInvocation"
+ },
+ {
+ "$ref": "#/components/schemas/IntegerMathInvocation"
+ },
+ {
+ "$ref": "#/components/schemas/InvertTensorMaskInvocation"
+ },
+ {
+ "$ref": "#/components/schemas/InvokeAdjustImageHuePlusInvocation"
+ },
+ {
+ "$ref": "#/components/schemas/InvokeEquivalentAchromaticLightnessInvocation"
+ },
+ {
+ "$ref": "#/components/schemas/InvokeImageBlendInvocation"
+ },
+ {
+ "$ref": "#/components/schemas/InvokeImageCompositorInvocation"
+ },
+ {
+ "$ref": "#/components/schemas/InvokeImageDilateOrErodeInvocation"
+ },
+ {
+ "$ref": "#/components/schemas/InvokeImageEnhanceInvocation"
+ },
+ {
+ "$ref": "#/components/schemas/InvokeImageValueThresholdsInvocation"
+ },
+ {
+ "$ref": "#/components/schemas/IterateInvocation"
+ },
+ {
+ "$ref": "#/components/schemas/LaMaInfillInvocation"
+ },
+ {
+ "$ref": "#/components/schemas/LatentsCollectionInvocation"
+ },
+ {
+ "$ref": "#/components/schemas/LatentsInvocation"
+ },
+ {
+ "$ref": "#/components/schemas/LatentsToImageInvocation"
+ },
+ {
+ "$ref": "#/components/schemas/LineartAnimeEdgeDetectionInvocation"
+ },
+ {
+ "$ref": "#/components/schemas/LineartEdgeDetectionInvocation"
+ },
+ {
+ "$ref": "#/components/schemas/LlavaOnevisionVllmInvocation"
+ },
+ {
+ "$ref": "#/components/schemas/LoRACollectionLoader"
+ },
+ {
+ "$ref": "#/components/schemas/LoRALoaderInvocation"
+ },
+ {
+ "$ref": "#/components/schemas/LoRASelectorInvocation"
+ },
+ {
+ "$ref": "#/components/schemas/MLSDDetectionInvocation"
+ },
+ {
+ "$ref": "#/components/schemas/MainModelLoaderInvocation"
+ },
+ {
+ "$ref": "#/components/schemas/MaskCombineInvocation"
},
{
- "$ref": "#/components/schemas/AlibabaCloudImageGenerationInvocation"
+ "$ref": "#/components/schemas/MaskEdgeInvocation"
},
{
- "$ref": "#/components/schemas/AlphaMaskToTensorInvocation"
+ "$ref": "#/components/schemas/MaskFromAlphaInvocation"
},
{
- "$ref": "#/components/schemas/AnimaDenoiseInvocation"
+ "$ref": "#/components/schemas/MaskFromIDInvocation"
},
{
- "$ref": "#/components/schemas/AnimaImageToLatentsInvocation"
+ "$ref": "#/components/schemas/MaskTensorToImageInvocation"
},
{
- "$ref": "#/components/schemas/AnimaLLLiteInvocation"
+ "$ref": "#/components/schemas/MediaPipeFaceDetectionInvocation"
},
{
- "$ref": "#/components/schemas/AnimaLatentsToImageInvocation"
+ "$ref": "#/components/schemas/MergeMetadataInvocation"
},
{
- "$ref": "#/components/schemas/AnimaLoRACollectionLoader"
+ "$ref": "#/components/schemas/MergeTilesToImageInvocation"
},
{
- "$ref": "#/components/schemas/AnimaLoRALoaderInvocation"
+ "$ref": "#/components/schemas/MetadataFieldExtractorInvocation"
},
{
- "$ref": "#/components/schemas/AnimaModelLoaderInvocation"
+ "$ref": "#/components/schemas/MetadataFromImageInvocation"
},
{
- "$ref": "#/components/schemas/AnimaTextEncoderInvocation"
+ "$ref": "#/components/schemas/MetadataInvocation"
},
{
- "$ref": "#/components/schemas/ApplyMaskTensorToImageInvocation"
+ "$ref": "#/components/schemas/MetadataItemInvocation"
},
{
- "$ref": "#/components/schemas/ApplyMaskToImageInvocation"
+ "$ref": "#/components/schemas/MetadataItemLinkedInvocation"
},
{
- "$ref": "#/components/schemas/BlankImageInvocation"
+ "$ref": "#/components/schemas/MetadataToBoolCollectionInvocation"
},
{
- "$ref": "#/components/schemas/BlendLatentsInvocation"
+ "$ref": "#/components/schemas/MetadataToBoolInvocation"
},
{
- "$ref": "#/components/schemas/BooleanCollectionInvocation"
+ "$ref": "#/components/schemas/MetadataToControlnetsInvocation"
},
{
- "$ref": "#/components/schemas/BooleanInvocation"
+ "$ref": "#/components/schemas/MetadataToFloatCollectionInvocation"
},
{
- "$ref": "#/components/schemas/BoundingBoxInvocation"
+ "$ref": "#/components/schemas/MetadataToFloatInvocation"
},
{
- "$ref": "#/components/schemas/CLIPSkipInvocation"
+ "$ref": "#/components/schemas/MetadataToIPAdaptersInvocation"
},
{
- "$ref": "#/components/schemas/CV2InfillInvocation"
+ "$ref": "#/components/schemas/MetadataToIntegerCollectionInvocation"
},
{
- "$ref": "#/components/schemas/CalculateImageTilesEvenSplitInvocation"
+ "$ref": "#/components/schemas/MetadataToIntegerInvocation"
},
{
- "$ref": "#/components/schemas/CalculateImageTilesInvocation"
+ "$ref": "#/components/schemas/MetadataToLorasCollectionInvocation"
},
{
- "$ref": "#/components/schemas/CalculateImageTilesMinimumOverlapInvocation"
+ "$ref": "#/components/schemas/MetadataToLorasInvocation"
},
{
- "$ref": "#/components/schemas/CallSavedWorkflowInvocation"
+ "$ref": "#/components/schemas/MetadataToModelInvocation"
},
{
- "$ref": "#/components/schemas/CannyEdgeDetectionInvocation"
+ "$ref": "#/components/schemas/MetadataToSDXLLorasInvocation"
},
{
- "$ref": "#/components/schemas/CanvasOutputInvocation"
+ "$ref": "#/components/schemas/MetadataToSDXLModelInvocation"
},
{
- "$ref": "#/components/schemas/CanvasPasteBackInvocation"
+ "$ref": "#/components/schemas/MetadataToSchedulerInvocation"
},
{
- "$ref": "#/components/schemas/CanvasV2MaskAndCropInvocation"
+ "$ref": "#/components/schemas/MetadataToStringCollectionInvocation"
},
{
- "$ref": "#/components/schemas/CenterPadCropInvocation"
+ "$ref": "#/components/schemas/MetadataToStringInvocation"
},
{
- "$ref": "#/components/schemas/CogView4DenoiseInvocation"
+ "$ref": "#/components/schemas/MetadataToT2IAdaptersInvocation"
},
{
- "$ref": "#/components/schemas/CogView4ImageToLatentsInvocation"
+ "$ref": "#/components/schemas/MetadataToVAEInvocation"
},
{
- "$ref": "#/components/schemas/CogView4LatentsToImageInvocation"
+ "$ref": "#/components/schemas/ModelIdentifierInvocation"
},
{
- "$ref": "#/components/schemas/CogView4ModelLoaderInvocation"
+ "$ref": "#/components/schemas/MultiplyInvocation"
},
{
- "$ref": "#/components/schemas/CogView4TextEncoderInvocation"
+ "$ref": "#/components/schemas/NoiseInvocation"
},
{
- "$ref": "#/components/schemas/CollectInvocation"
+ "$ref": "#/components/schemas/NormalMapInvocation"
},
{
- "$ref": "#/components/schemas/ColorCorrectInvocation"
+ "$ref": "#/components/schemas/OklabUnsharpMaskInvocation"
},
{
- "$ref": "#/components/schemas/ColorInvocation"
+ "$ref": "#/components/schemas/OklchImageHueAdjustmentInvocation"
},
{
- "$ref": "#/components/schemas/ColorMapInvocation"
+ "$ref": "#/components/schemas/OpenAIImageGenerationInvocation"
},
{
- "$ref": "#/components/schemas/CompelInvocation"
+ "$ref": "#/components/schemas/PBRMapsInvocation"
},
{
- "$ref": "#/components/schemas/ConditioningCollectionInvocation"
+ "$ref": "#/components/schemas/PairTileImageInvocation"
},
{
- "$ref": "#/components/schemas/ConditioningInvocation"
+ "$ref": "#/components/schemas/PasteImageIntoBoundingBoxInvocation"
},
{
- "$ref": "#/components/schemas/ContentShuffleInvocation"
+ "$ref": "#/components/schemas/PiDiNetEdgeDetectionInvocation"
},
{
- "$ref": "#/components/schemas/ControlNetInvocation"
+ "$ref": "#/components/schemas/PromptTemplateInvocation"
},
{
- "$ref": "#/components/schemas/CoreMetadataInvocation"
+ "$ref": "#/components/schemas/PromptsFromFileInvocation"
},
{
- "$ref": "#/components/schemas/CreateDenoiseMaskInvocation"
+ "$ref": "#/components/schemas/QwenImageDenoiseInvocation"
},
{
- "$ref": "#/components/schemas/CreateGradientMaskInvocation"
+ "$ref": "#/components/schemas/QwenImageImageToLatentsInvocation"
},
{
- "$ref": "#/components/schemas/CropImageToBoundingBoxInvocation"
+ "$ref": "#/components/schemas/QwenImageLatentsToImageInvocation"
},
{
- "$ref": "#/components/schemas/CropLatentsCoreInvocation"
+ "$ref": "#/components/schemas/QwenImageLoRACollectionLoader"
},
{
- "$ref": "#/components/schemas/CvInpaintInvocation"
+ "$ref": "#/components/schemas/QwenImageLoRALoaderInvocation"
},
{
- "$ref": "#/components/schemas/DWOpenposeDetectionInvocation"
+ "$ref": "#/components/schemas/QwenImageModelLoaderInvocation"
},
{
- "$ref": "#/components/schemas/DecodeInvisibleWatermarkInvocation"
+ "$ref": "#/components/schemas/QwenImageTextEncoderInvocation"
},
{
- "$ref": "#/components/schemas/DenoiseLatentsInvocation"
+ "$ref": "#/components/schemas/RandomFloatInvocation"
},
{
- "$ref": "#/components/schemas/DenoiseLatentsMetaInvocation"
+ "$ref": "#/components/schemas/RandomIntInvocation"
},
{
- "$ref": "#/components/schemas/DepthAnythingDepthEstimationInvocation"
+ "$ref": "#/components/schemas/RandomRangeInvocation"
},
{
- "$ref": "#/components/schemas/DivideInvocation"
+ "$ref": "#/components/schemas/RangeInvocation"
},
{
- "$ref": "#/components/schemas/DynamicPromptInvocation"
+ "$ref": "#/components/schemas/RangeOfSizeInvocation"
},
{
- "$ref": "#/components/schemas/ESRGANInvocation"
+ "$ref": "#/components/schemas/RectangleMaskInvocation"
},
{
- "$ref": "#/components/schemas/ExpandMaskWithFadeInvocation"
+ "$ref": "#/components/schemas/ResizeLatentsInvocation"
},
{
- "$ref": "#/components/schemas/FLUXLoRACollectionLoader"
+ "$ref": "#/components/schemas/RoundInvocation"
},
{
- "$ref": "#/components/schemas/FaceIdentifierInvocation"
+ "$ref": "#/components/schemas/SD3DenoiseInvocation"
},
{
- "$ref": "#/components/schemas/FaceMaskInvocation"
+ "$ref": "#/components/schemas/SD3ImageToLatentsInvocation"
},
{
- "$ref": "#/components/schemas/FaceOffInvocation"
+ "$ref": "#/components/schemas/SD3LatentsToImageInvocation"
},
{
- "$ref": "#/components/schemas/FloatBatchInvocation"
+ "$ref": "#/components/schemas/SDXLCompelPromptInvocation"
},
{
- "$ref": "#/components/schemas/FloatCollectionInvocation"
+ "$ref": "#/components/schemas/SDXLLoRACollectionLoader"
},
{
- "$ref": "#/components/schemas/FloatGenerator"
+ "$ref": "#/components/schemas/SDXLLoRALoaderInvocation"
},
{
- "$ref": "#/components/schemas/FloatInvocation"
+ "$ref": "#/components/schemas/SDXLModelLoaderInvocation"
},
{
- "$ref": "#/components/schemas/FloatLinearRangeInvocation"
+ "$ref": "#/components/schemas/SDXLRefinerCompelPromptInvocation"
},
{
- "$ref": "#/components/schemas/FloatMathInvocation"
+ "$ref": "#/components/schemas/SDXLRefinerModelLoaderInvocation"
},
{
- "$ref": "#/components/schemas/FloatToIntegerInvocation"
+ "$ref": "#/components/schemas/SaveImageInvocation"
},
{
- "$ref": "#/components/schemas/Flux2DenoiseInvocation"
+ "$ref": "#/components/schemas/SaveImageToFileInvocation"
},
{
- "$ref": "#/components/schemas/Flux2KleinLoRACollectionLoader"
+ "$ref": "#/components/schemas/ScaleLatentsInvocation"
},
{
- "$ref": "#/components/schemas/Flux2KleinLoRALoaderInvocation"
+ "$ref": "#/components/schemas/SchedulerInvocation"
},
{
- "$ref": "#/components/schemas/Flux2KleinModelLoaderInvocation"
+ "$ref": "#/components/schemas/Sd3ModelLoaderInvocation"
},
{
- "$ref": "#/components/schemas/Flux2KleinTextEncoderInvocation"
+ "$ref": "#/components/schemas/Sd3TextEncoderInvocation"
},
{
- "$ref": "#/components/schemas/Flux2VaeDecodeInvocation"
+ "$ref": "#/components/schemas/SeamlessModeInvocation"
},
{
- "$ref": "#/components/schemas/Flux2VaeEncodeInvocation"
+ "$ref": "#/components/schemas/SeedreamImageGenerationInvocation"
},
{
- "$ref": "#/components/schemas/FluxControlLoRALoaderInvocation"
+ "$ref": "#/components/schemas/SegmentAnythingInvocation"
},
{
- "$ref": "#/components/schemas/FluxControlNetInvocation"
+ "$ref": "#/components/schemas/ShowImageInvocation"
},
{
- "$ref": "#/components/schemas/FluxDenoiseInvocation"
+ "$ref": "#/components/schemas/SpandrelImageToImageAutoscaleInvocation"
},
{
- "$ref": "#/components/schemas/FluxDenoiseLatentsMetaInvocation"
+ "$ref": "#/components/schemas/SpandrelImageToImageInvocation"
},
{
- "$ref": "#/components/schemas/FluxFillInvocation"
+ "$ref": "#/components/schemas/StringBatchInvocation"
},
{
- "$ref": "#/components/schemas/FluxIPAdapterInvocation"
+ "$ref": "#/components/schemas/StringCollectionInvocation"
},
{
- "$ref": "#/components/schemas/FluxKontextConcatenateImagesInvocation"
+ "$ref": "#/components/schemas/StringGenerator"
},
{
- "$ref": "#/components/schemas/FluxKontextInvocation"
+ "$ref": "#/components/schemas/StringInvocation"
},
{
- "$ref": "#/components/schemas/FluxLoRALoaderInvocation"
+ "$ref": "#/components/schemas/StringJoinInvocation"
},
{
- "$ref": "#/components/schemas/FluxModelLoaderInvocation"
+ "$ref": "#/components/schemas/StringJoinThreeInvocation"
},
{
- "$ref": "#/components/schemas/FluxReduxInvocation"
+ "$ref": "#/components/schemas/StringReplaceInvocation"
},
{
- "$ref": "#/components/schemas/FluxTextEncoderInvocation"
+ "$ref": "#/components/schemas/StringSplitInvocation"
},
{
- "$ref": "#/components/schemas/FluxVaeDecodeInvocation"
+ "$ref": "#/components/schemas/StringSplitNegInvocation"
},
{
- "$ref": "#/components/schemas/FluxVaeEncodeInvocation"
+ "$ref": "#/components/schemas/SubtractInvocation"
},
{
- "$ref": "#/components/schemas/FreeUInvocation"
+ "$ref": "#/components/schemas/T2IAdapterInvocation"
},
{
- "$ref": "#/components/schemas/GeminiImageGenerationInvocation"
+ "$ref": "#/components/schemas/TextLLMInvocation"
},
{
- "$ref": "#/components/schemas/GetMaskBoundingBoxInvocation"
+ "$ref": "#/components/schemas/TileToPropertiesInvocation"
},
{
- "$ref": "#/components/schemas/GroundingDinoInvocation"
+ "$ref": "#/components/schemas/TiledMultiDiffusionDenoiseLatents"
},
{
- "$ref": "#/components/schemas/HEDEdgeDetectionInvocation"
+ "$ref": "#/components/schemas/UnsharpMaskInvocation"
},
{
- "$ref": "#/components/schemas/HeuristicResizeInvocation"
+ "$ref": "#/components/schemas/VAELoaderInvocation"
},
{
- "$ref": "#/components/schemas/IPAdapterInvocation"
+ "$ref": "#/components/schemas/VideoConcatInvocation"
},
{
- "$ref": "#/components/schemas/IdealSizeInvocation"
+ "$ref": "#/components/schemas/VideoFrameExtractInvocation"
},
{
- "$ref": "#/components/schemas/IfInvocation"
+ "$ref": "#/components/schemas/VideoInvocation"
},
{
- "$ref": "#/components/schemas/ImageBatchInvocation"
+ "$ref": "#/components/schemas/WanDenoiseInvocation"
},
{
- "$ref": "#/components/schemas/ImageBlurInvocation"
+ "$ref": "#/components/schemas/WanI2VIdealDimensionsInvocation"
},
{
- "$ref": "#/components/schemas/ImageChannelInvocation"
+ "$ref": "#/components/schemas/WanImageToLatentsInvocation"
},
{
- "$ref": "#/components/schemas/ImageChannelMultiplyInvocation"
+ "$ref": "#/components/schemas/WanLatentsToImageInvocation"
},
{
- "$ref": "#/components/schemas/ImageChannelOffsetInvocation"
+ "$ref": "#/components/schemas/WanLatentsToVideoInvocation"
},
{
- "$ref": "#/components/schemas/ImageCollectionInvocation"
+ "$ref": "#/components/schemas/WanLoRACollectionLoader"
},
{
- "$ref": "#/components/schemas/ImageConvertInvocation"
+ "$ref": "#/components/schemas/WanLoRALoaderInvocation"
},
{
- "$ref": "#/components/schemas/ImageCropInvocation"
+ "$ref": "#/components/schemas/WanModelLoaderInvocation"
},
{
- "$ref": "#/components/schemas/ImageGenerator"
+ "$ref": "#/components/schemas/WanRefImageEncoderInvocation"
},
{
- "$ref": "#/components/schemas/ImageHueAdjustmentInvocation"
+ "$ref": "#/components/schemas/WanTI2VIdealDimensionsInvocation"
},
{
- "$ref": "#/components/schemas/ImageInverseLerpInvocation"
+ "$ref": "#/components/schemas/WanTextEncoderInvocation"
},
{
- "$ref": "#/components/schemas/ImageInvocation"
+ "$ref": "#/components/schemas/WanVideoDenoiseInvocation"
},
{
- "$ref": "#/components/schemas/ImageLerpInvocation"
+ "$ref": "#/components/schemas/WorkflowReturnGetInvocation"
},
{
- "$ref": "#/components/schemas/ImageMaskToTensorInvocation"
+ "$ref": "#/components/schemas/WorkflowReturnInvocation"
},
{
- "$ref": "#/components/schemas/ImageMultiplyInvocation"
+ "$ref": "#/components/schemas/WorkflowReturnValueInvocation"
},
{
- "$ref": "#/components/schemas/ImageNSFWBlurInvocation"
+ "$ref": "#/components/schemas/ZImageControlInvocation"
},
{
- "$ref": "#/components/schemas/ImageNoiseInvocation"
+ "$ref": "#/components/schemas/ZImageDenoiseInvocation"
},
{
- "$ref": "#/components/schemas/ImagePanelLayoutInvocation"
+ "$ref": "#/components/schemas/ZImageDenoiseMetaInvocation"
},
{
- "$ref": "#/components/schemas/ImagePasteInvocation"
+ "$ref": "#/components/schemas/ZImageImageToLatentsInvocation"
},
{
- "$ref": "#/components/schemas/ImageResizeInvocation"
+ "$ref": "#/components/schemas/ZImageLatentsToImageInvocation"
},
{
- "$ref": "#/components/schemas/ImageScaleInvocation"
+ "$ref": "#/components/schemas/ZImageLoRACollectionLoader"
},
{
- "$ref": "#/components/schemas/ImageToLatentsInvocation"
+ "$ref": "#/components/schemas/ZImageLoRALoaderInvocation"
},
{
- "$ref": "#/components/schemas/ImageWatermarkInvocation"
+ "$ref": "#/components/schemas/ZImageModelLoaderInvocation"
},
{
- "$ref": "#/components/schemas/InfillColorInvocation"
+ "$ref": "#/components/schemas/ZImageSeedVarianceEnhancerInvocation"
},
{
- "$ref": "#/components/schemas/InfillPatchMatchInvocation"
+ "$ref": "#/components/schemas/ZImageTextEncoderInvocation"
+ }
+ ],
+ "title": "Invocation"
+ },
+ "invocation_source_id": {
+ "description": "The ID of the prepared invocation's source node",
+ "title": "Invocation Source Id",
+ "type": "string"
+ }
+ },
+ "required": [
+ "timestamp",
+ "queue_id",
+ "item_id",
+ "batch_id",
+ "origin",
+ "destination",
+ "user_id",
+ "session_id",
+ "invocation",
+ "invocation_source_id"
+ ],
+ "title": "InvocationStartedEvent",
+ "type": "object"
+ },
+ "InvokeAIAppConfig": {
+ "properties": {
+ "schema_version": {
+ "type": "string",
+ "title": "Schema Version",
+ "description": "Schema version of the config file. This is not a user-configurable setting.",
+ "default": "4.0.3"
+ },
+ "legacy_models_yaml_path": {
+ "anyOf": [
+ {
+ "type": "string",
+ "format": "path"
},
{
- "$ref": "#/components/schemas/InfillTileInvocation"
+ "type": "null"
+ }
+ ],
+ "title": "Legacy Models Yaml Path",
+ "description": "Path to the legacy models.yaml file. This is not a user-configurable setting."
+ },
+ "host": {
+ "type": "string",
+ "title": "Host",
+ "description": "IP address to bind to. Use `0.0.0.0` to serve to your local network.",
+ "default": "127.0.0.1"
+ },
+ "port": {
+ "type": "integer",
+ "title": "Port",
+ "description": "Port to bind to.",
+ "default": 9090
+ },
+ "allow_origins": {
+ "items": {
+ "type": "string"
+ },
+ "type": "array",
+ "title": "Allow Origins",
+ "description": "Allowed CORS origins.",
+ "default": []
+ },
+ "allow_credentials": {
+ "type": "boolean",
+ "title": "Allow Credentials",
+ "description": "Allow CORS credentials.",
+ "default": true
+ },
+ "allow_methods": {
+ "items": {
+ "type": "string"
+ },
+ "type": "array",
+ "title": "Allow Methods",
+ "description": "Methods allowed for CORS.",
+ "default": ["*"]
+ },
+ "allow_headers": {
+ "items": {
+ "type": "string"
+ },
+ "type": "array",
+ "title": "Allow Headers",
+ "description": "Headers allowed for CORS.",
+ "default": ["*"]
+ },
+ "ssl_certfile": {
+ "anyOf": [
+ {
+ "type": "string",
+ "format": "path"
},
{
- "$ref": "#/components/schemas/IntegerBatchInvocation"
+ "type": "null"
+ }
+ ],
+ "title": "Ssl Certfile",
+ "description": "SSL certificate file for HTTPS. See https://www.uvicorn.dev/settings/#https."
+ },
+ "ssl_keyfile": {
+ "anyOf": [
+ {
+ "type": "string",
+ "format": "path"
},
{
- "$ref": "#/components/schemas/IntegerCollectionInvocation"
+ "type": "null"
+ }
+ ],
+ "title": "Ssl Keyfile",
+ "description": "SSL key file for HTTPS. See https://www.uvicorn.dev/settings/#https."
+ },
+ "base_url": {
+ "anyOf": [
+ {
+ "type": "string"
+ },
+ {
+ "type": "null"
+ }
+ ],
+ "title": "Base Url",
+ "description": "Public base path when running behind a reverse proxy under a sub-path, e.g. `/invoke`. Required when the proxy PRESERVES the sub-path (the backend receives `/invoke/api/...`); optional when the proxy strips it (set it anyway so openapi/docs URLs are correct). Leave unset when serving at the domain root. Normalized to a single leading slash with no trailing slash."
+ },
+ "forwarded_allow_ips": {
+ "type": "string",
+ "title": "Forwarded Allow Ips",
+ "description": "Comma-separated list of IPs (or `*`) allowed to set X-Forwarded-* headers. Set to the reverse proxy's IP. Only used when `base_url` is set.",
+ "default": "127.0.0.1"
+ },
+ "log_tokenization": {
+ "type": "boolean",
+ "title": "Log Tokenization",
+ "description": "Enable logging of parsed prompt tokens.",
+ "default": false
+ },
+ "patchmatch": {
+ "type": "boolean",
+ "title": "Patchmatch",
+ "description": "Enable patchmatch inpaint code.",
+ "default": true
+ },
+ "models_dir": {
+ "type": "string",
+ "format": "path",
+ "title": "Models Dir",
+ "description": "Path to the models directory.",
+ "default": "models"
+ },
+ "convert_cache_dir": {
+ "type": "string",
+ "format": "path",
+ "title": "Convert Cache Dir",
+ "description": "Path to the converted models cache directory (DEPRECATED, but do not delete because it is needed for migration from previous versions).",
+ "default": "models/.convert_cache"
+ },
+ "download_cache_dir": {
+ "type": "string",
+ "format": "path",
+ "title": "Download Cache Dir",
+ "description": "Path to the directory that contains dynamically downloaded models.",
+ "default": "models/.download_cache"
+ },
+ "legacy_conf_dir": {
+ "type": "string",
+ "format": "path",
+ "title": "Legacy Conf Dir",
+ "description": "Path to directory of legacy checkpoint config files.",
+ "default": "configs"
+ },
+ "db_dir": {
+ "type": "string",
+ "format": "path",
+ "title": "Db Dir",
+ "description": "Path to InvokeAI databases directory.",
+ "default": "databases"
+ },
+ "outputs_dir": {
+ "type": "string",
+ "format": "path",
+ "title": "Outputs Dir",
+ "description": "Path to directory for outputs.",
+ "default": "outputs"
+ },
+ "image_subfolder_strategy": {
+ "type": "string",
+ "enum": ["flat", "date", "type", "hash"],
+ "title": "Image Subfolder Strategy",
+ "description": "Strategy for organizing images into subfolders. 'flat' stores all images in a single folder. 'date' organizes by YYYY/MM/DD. 'type' organizes by image category. 'hash' uses first 2 characters of UUID for filesystem performance.",
+ "default": "flat"
+ },
+ "custom_nodes_dir": {
+ "type": "string",
+ "format": "path",
+ "title": "Custom Nodes Dir",
+ "description": "Path to directory for custom nodes.",
+ "default": "nodes"
+ },
+ "style_presets_dir": {
+ "type": "string",
+ "format": "path",
+ "title": "Style Presets Dir",
+ "description": "Path to directory for style presets.",
+ "default": "style_presets"
+ },
+ "workflow_thumbnails_dir": {
+ "type": "string",
+ "format": "path",
+ "title": "Workflow Thumbnails Dir",
+ "description": "Path to directory for workflow thumbnails.",
+ "default": "workflow_thumbnails"
+ },
+ "log_handlers": {
+ "items": {
+ "type": "string"
+ },
+ "type": "array",
+ "title": "Log Handlers",
+ "description": "Log handler. Valid options are \"console\", \"file=\", \"syslog=path|address:host:port\", \"http=\".",
+ "default": ["console"]
+ },
+ "log_format": {
+ "type": "string",
+ "enum": ["plain", "color", "syslog", "legacy"],
+ "title": "Log Format",
+ "description": "Log format. Use \"plain\" for text-only, \"color\" for colorized output, \"legacy\" for 2.3-style logging and \"syslog\" for syslog-style.",
+ "default": "color"
+ },
+ "log_level": {
+ "type": "string",
+ "enum": ["debug", "info", "warning", "error", "critical"],
+ "title": "Log Level",
+ "description": "Emit logging messages at this level or higher.",
+ "default": "info"
+ },
+ "log_sql": {
+ "type": "boolean",
+ "title": "Log Sql",
+ "description": "Log SQL queries. `log_level` must be `debug` for this to do anything. Extremely verbose.",
+ "default": false
+ },
+ "log_level_network": {
+ "type": "string",
+ "enum": ["debug", "info", "warning", "error", "critical"],
+ "title": "Log Level Network",
+ "description": "Log level for network-related messages. 'info' and 'debug' are very verbose.",
+ "default": "warning"
+ },
+ "use_memory_db": {
+ "type": "boolean",
+ "title": "Use Memory Db",
+ "description": "Use in-memory database. Useful for development.",
+ "default": false
+ },
+ "dev_reload": {
+ "type": "boolean",
+ "title": "Dev Reload",
+ "description": "Automatically reload when Python sources are changed. Does not reload node definitions.",
+ "default": false
+ },
+ "profile_graphs": {
+ "type": "boolean",
+ "title": "Profile Graphs",
+ "description": "Enable graph profiling using `cProfile`.",
+ "default": false
+ },
+ "profile_prefix": {
+ "anyOf": [
+ {
+ "type": "string"
},
{
- "$ref": "#/components/schemas/IntegerGenerator"
+ "type": "null"
+ }
+ ],
+ "title": "Profile Prefix",
+ "description": "An optional prefix for profile output files."
+ },
+ "profiles_dir": {
+ "type": "string",
+ "format": "path",
+ "title": "Profiles Dir",
+ "description": "Path to profiles output directory.",
+ "default": "profiles"
+ },
+ "max_cache_ram_gb": {
+ "anyOf": [
+ {
+ "type": "number",
+ "exclusiveMinimum": 0.0
},
{
- "$ref": "#/components/schemas/IntegerInvocation"
- },
+ "type": "null"
+ }
+ ],
+ "title": "Max Cache Ram Gb",
+ "description": "The maximum amount of CPU RAM to use for model caching in GB. If unset, the limit will be configured based on the available RAM. In most cases, it is recommended to leave this unset."
+ },
+ "max_cache_vram_gb": {
+ "anyOf": [
{
- "$ref": "#/components/schemas/IntegerMathInvocation"
+ "type": "number",
+ "minimum": 0.0
},
{
- "$ref": "#/components/schemas/InvertTensorMaskInvocation"
- },
+ "type": "null"
+ }
+ ],
+ "title": "Max Cache Vram Gb",
+ "description": "The amount of VRAM to use for model caching in GB. If unset, the limit will be configured based on the available VRAM and the device_working_mem_gb. In most cases, it is recommended to leave this unset."
+ },
+ "log_memory_usage": {
+ "type": "boolean",
+ "title": "Log Memory Usage",
+ "description": "If True, a memory snapshot will be captured before and after every model cache operation, and the result will be logged (at debug level). There is a time cost to capturing the memory snapshots, so it is recommended to only enable this feature if you are actively inspecting the model cache's behaviour.",
+ "default": false
+ },
+ "model_cache_keep_alive_min": {
+ "type": "number",
+ "minimum": 0.0,
+ "title": "Model Cache Keep Alive Min",
+ "description": "How long to keep models in cache after last use, in minutes. A value of 0 (the default) means models are kept in cache indefinitely. If no model generations occur within the timeout period, the model cache is cleared using the same logic as the 'Clear Model Cache' button.",
+ "default": 0
+ },
+ "device_working_mem_gb": {
+ "type": "number",
+ "title": "Device Working Mem Gb",
+ "description": "The amount of working memory to keep available on the compute device (in GB). Has no effect if running on CPU. If you are experiencing OOM errors, try increasing this value.",
+ "default": 3
+ },
+ "enable_partial_loading": {
+ "type": "boolean",
+ "title": "Enable Partial Loading",
+ "description": "Enable partial loading of models. This enables models to run with reduced VRAM requirements (at the cost of slower speed) by streaming the model from RAM to VRAM as its used. In some edge cases, partial loading can cause models to run more slowly if they were previously being fully loaded into VRAM.",
+ "default": true
+ },
+ "keep_ram_copy_of_weights": {
+ "type": "boolean",
+ "title": "Keep Ram Copy Of Weights",
+ "description": "Whether to keep a full RAM copy of a model's weights when the model is loaded in VRAM. Keeping a RAM copy increases average RAM usage, but speeds up model switching and LoRA patching (assuming there is sufficient RAM). Set this to False if RAM pressure is consistently high.",
+ "default": true
+ },
+ "ram": {
+ "anyOf": [
{
- "$ref": "#/components/schemas/InvokeAdjustImageHuePlusInvocation"
+ "type": "number",
+ "exclusiveMinimum": 0.0
},
{
- "$ref": "#/components/schemas/InvokeEquivalentAchromaticLightnessInvocation"
- },
+ "type": "null"
+ }
+ ],
+ "title": "Ram",
+ "description": "DEPRECATED: This setting is no longer used. It has been replaced by `max_cache_ram_gb`, but most users will not need to use this config since automatic cache size limits should work well in most cases. This config setting will be removed once the new model cache behavior is stable."
+ },
+ "vram": {
+ "anyOf": [
{
- "$ref": "#/components/schemas/InvokeImageBlendInvocation"
+ "type": "number",
+ "minimum": 0.0
},
{
- "$ref": "#/components/schemas/InvokeImageCompositorInvocation"
- },
+ "type": "null"
+ }
+ ],
+ "title": "Vram",
+ "description": "DEPRECATED: This setting is no longer used. It has been replaced by `max_cache_vram_gb`, but most users will not need to use this config since automatic cache size limits should work well in most cases. This config setting will be removed once the new model cache behavior is stable."
+ },
+ "lazy_offload": {
+ "type": "boolean",
+ "title": "Lazy Offload",
+ "description": "DEPRECATED: This setting is no longer used. Lazy-offloading is enabled by default. This config setting will be removed once the new model cache behavior is stable.",
+ "default": true
+ },
+ "pytorch_cuda_alloc_conf": {
+ "anyOf": [
{
- "$ref": "#/components/schemas/InvokeImageDilateOrErodeInvocation"
+ "type": "string"
},
{
- "$ref": "#/components/schemas/InvokeImageEnhanceInvocation"
- },
+ "type": "null"
+ }
+ ],
+ "title": "Pytorch Cuda Alloc Conf",
+ "description": "Configure the Torch CUDA memory allocator. This will impact peak reserved VRAM usage and performance. Setting to \"backend:cudaMallocAsync\" works well on many systems. The optimal configuration is highly dependent on the system configuration (device type, VRAM, CUDA driver version, etc.), so must be tuned experimentally."
+ },
+ "device": {
+ "type": "string",
+ "pattern": "^(auto|cpu|mps|cuda(:\\d+)?)$",
+ "title": "Device",
+ "description": "Preferred execution device. `auto` will choose the device depending on the hardware platform and the installed torch capabilities. Valid values: `auto`, `cpu`, `cuda`, `mps`, `cuda:N` (where N is a device number)",
+ "default": "auto"
+ },
+ "precision": {
+ "type": "string",
+ "enum": ["auto", "float16", "bfloat16", "float32"],
+ "title": "Precision",
+ "description": "Floating point precision. `float16` will consume half the memory of `float32` but produce slightly lower-quality images. The `auto` setting will guess the proper precision based on your video card and operating system.",
+ "default": "auto"
+ },
+ "sequential_guidance": {
+ "type": "boolean",
+ "title": "Sequential Guidance",
+ "description": "Whether to calculate guidance in serial instead of in parallel, lowering memory requirements.",
+ "default": false
+ },
+ "attention_type": {
+ "type": "string",
+ "enum": ["auto", "normal", "xformers", "sliced", "torch-sdp"],
+ "title": "Attention Type",
+ "description": "Attention type.",
+ "default": "auto"
+ },
+ "attention_slice_size": {
+ "enum": ["auto", "balanced", "max", 1, 2, 3, 4, 5, 6, 7, 8],
+ "title": "Attention Slice Size",
+ "description": "Slice size, valid when attention_type==\"sliced\".",
+ "default": "auto"
+ },
+ "force_tiled_decode": {
+ "type": "boolean",
+ "title": "Force Tiled Decode",
+ "description": "Whether to enable tiled VAE decode (reduces memory consumption with some performance penalty).",
+ "default": false
+ },
+ "pil_compress_level": {
+ "type": "integer",
+ "title": "Pil Compress Level",
+ "description": "The compress_level setting of PIL.Image.save(), used for PNG encoding. All settings are lossless. 0 = no compression, 1 = fastest with slightly larger filesize, 9 = slowest with smallest filesize. 1 is typically the best setting.",
+ "default": 1
+ },
+ "max_queue_size": {
+ "type": "integer",
+ "exclusiveMinimum": 0.0,
+ "title": "Max Queue Size",
+ "description": "Maximum number of items in the session queue.",
+ "default": 10000
+ },
+ "session_queue_mode": {
+ "type": "string",
+ "enum": ["FIFO", "round_robin"],
+ "title": "Session Queue Mode",
+ "description": "Session queue mode. Use 'FIFO' for traditional first-in-first-out, or 'round_robin' to serve each user's jobs in turn. In single-user mode, FIFO is always used regardless of this setting.",
+ "default": "round_robin"
+ },
+ "clear_queue_on_startup": {
+ "type": "boolean",
+ "title": "Clear Queue On Startup",
+ "description": "Empties session queue on startup. If true, disables `max_queue_history`.",
+ "default": false
+ },
+ "max_queue_history": {
+ "anyOf": [
{
- "$ref": "#/components/schemas/InvokeImageValueThresholdsInvocation"
+ "type": "integer",
+ "minimum": 0.0
},
{
- "$ref": "#/components/schemas/IterateInvocation"
- },
+ "type": "null"
+ }
+ ],
+ "title": "Max Queue History",
+ "description": "Keep the last N completed, failed, and canceled queue items. Older items are deleted on startup. Set to 0 to prune all terminal items. Ignored if `clear_queue_on_startup` is true."
+ },
+ "allow_nodes": {
+ "anyOf": [
{
- "$ref": "#/components/schemas/LaMaInfillInvocation"
+ "items": {
+ "type": "string"
+ },
+ "type": "array"
},
{
- "$ref": "#/components/schemas/LatentsCollectionInvocation"
- },
+ "type": "null"
+ }
+ ],
+ "title": "Allow Nodes",
+ "description": "List of nodes to allow. Omit to allow all."
+ },
+ "deny_nodes": {
+ "anyOf": [
{
- "$ref": "#/components/schemas/LatentsInvocation"
+ "items": {
+ "type": "string"
+ },
+ "type": "array"
},
{
- "$ref": "#/components/schemas/LatentsToImageInvocation"
- },
+ "type": "null"
+ }
+ ],
+ "title": "Deny Nodes",
+ "description": "List of nodes to deny. Omit to deny none."
+ },
+ "node_cache_size": {
+ "type": "integer",
+ "title": "Node Cache Size",
+ "description": "How many cached nodes to keep in memory.",
+ "default": 512
+ },
+ "hashing_algorithm": {
+ "type": "string",
+ "enum": [
+ "blake3_multi",
+ "blake3_single",
+ "random",
+ "md5",
+ "sha1",
+ "sha224",
+ "sha256",
+ "sha384",
+ "sha512",
+ "blake2b",
+ "blake2s",
+ "sha3_224",
+ "sha3_256",
+ "sha3_384",
+ "sha3_512",
+ "shake_128",
+ "shake_256"
+ ],
+ "title": "Hashing Algorithm",
+ "description": "Model hashing algorthim for model installs. 'blake3_multi' is best for SSDs. 'blake3_single' is best for spinning disk HDDs. 'random' disables hashing, instead assigning a UUID to models. Useful when using a memory db to reduce model installation time, or if you don't care about storing stable hashes for models. Alternatively, any other hashlib algorithm is accepted, though these are not nearly as performant as blake3.",
+ "default": "blake3_single"
+ },
+ "remote_api_tokens": {
+ "anyOf": [
{
- "$ref": "#/components/schemas/LineartAnimeEdgeDetectionInvocation"
+ "items": {
+ "$ref": "#/components/schemas/URLRegexTokenPair"
+ },
+ "type": "array"
},
{
- "$ref": "#/components/schemas/LineartEdgeDetectionInvocation"
- },
+ "type": "null"
+ }
+ ],
+ "title": "Remote Api Tokens",
+ "description": "List of regular expression and token pairs used when downloading models from URLs. The download URL is tested against the regex, and if it matches, the token is provided in as a Bearer token."
+ },
+ "scan_models_on_startup": {
+ "type": "boolean",
+ "title": "Scan Models On Startup",
+ "description": "Scan the models directory on startup, registering orphaned models. This is typically only used in conjunction with `use_memory_db` for testing purposes.",
+ "default": false
+ },
+ "unsafe_disable_picklescan": {
+ "type": "boolean",
+ "title": "Unsafe Disable Picklescan",
+ "description": "UNSAFE. Disable the picklescan security check during model installation. Recommended only for development and testing purposes. This will allow arbitrary code execution during model installation, so should never be used in production.",
+ "default": false
+ },
+ "allow_unknown_models": {
+ "type": "boolean",
+ "title": "Allow Unknown Models",
+ "description": "Allow installation of models that we are unable to identify. If enabled, models will be marked as `unknown` in the database, and will not have any metadata associated with them. If disabled, unknown models will be rejected during installation.",
+ "default": true
+ },
+ "multiuser": {
+ "type": "boolean",
+ "title": "Multiuser",
+ "description": "Enable multiuser support. When disabled, the application runs in single-user mode using a default system account with administrator privileges. When enabled, requires user authentication and authorization.",
+ "default": false
+ },
+ "strict_password_checking": {
+ "type": "boolean",
+ "title": "Strict Password Checking",
+ "description": "Enforce strict password requirements. When True, passwords must contain uppercase, lowercase, and numbers. When False (default), any password is accepted but its strength (weak/moderate/strong) is reported to the user.",
+ "default": false
+ },
+ "external_alibabacloud_api_key": {
+ "anyOf": [
{
- "$ref": "#/components/schemas/LlavaOnevisionVllmInvocation"
+ "type": "string"
},
{
- "$ref": "#/components/schemas/LoRACollectionLoader"
- },
+ "type": "null"
+ }
+ ],
+ "title": "External Alibabacloud Api Key",
+ "description": "API key for Alibaba Cloud DashScope image generation."
+ },
+ "external_alibabacloud_base_url": {
+ "anyOf": [
{
- "$ref": "#/components/schemas/LoRALoaderInvocation"
+ "type": "string"
},
{
- "$ref": "#/components/schemas/LoRASelectorInvocation"
- },
+ "type": "null"
+ }
+ ],
+ "title": "External Alibabacloud Base Url",
+ "description": "Base URL override for Alibaba Cloud DashScope image generation."
+ },
+ "external_gemini_api_key": {
+ "anyOf": [
{
- "$ref": "#/components/schemas/MLSDDetectionInvocation"
+ "type": "string"
},
{
- "$ref": "#/components/schemas/MainModelLoaderInvocation"
- },
+ "type": "null"
+ }
+ ],
+ "title": "External Gemini Api Key",
+ "description": "API key for Gemini image generation."
+ },
+ "external_openai_api_key": {
+ "anyOf": [
{
- "$ref": "#/components/schemas/MaskCombineInvocation"
+ "type": "string"
},
{
- "$ref": "#/components/schemas/MaskEdgeInvocation"
- },
+ "type": "null"
+ }
+ ],
+ "title": "External Openai Api Key",
+ "description": "API key for OpenAI image generation."
+ },
+ "external_gemini_base_url": {
+ "anyOf": [
{
- "$ref": "#/components/schemas/MaskFromAlphaInvocation"
+ "type": "string"
},
{
- "$ref": "#/components/schemas/MaskFromIDInvocation"
- },
+ "type": "null"
+ }
+ ],
+ "title": "External Gemini Base Url",
+ "description": "Base URL override for Gemini image generation."
+ },
+ "external_openai_base_url": {
+ "anyOf": [
{
- "$ref": "#/components/schemas/MaskTensorToImageInvocation"
+ "type": "string"
},
{
- "$ref": "#/components/schemas/MediaPipeFaceDetectionInvocation"
- },
+ "type": "null"
+ }
+ ],
+ "title": "External Openai Base Url",
+ "description": "Base URL override for OpenAI image generation."
+ },
+ "external_seedream_api_key": {
+ "anyOf": [
{
- "$ref": "#/components/schemas/MergeMetadataInvocation"
+ "type": "string"
},
{
- "$ref": "#/components/schemas/MergeTilesToImageInvocation"
- },
+ "type": "null"
+ }
+ ],
+ "title": "External Seedream Api Key",
+ "description": "API key for Seedream image generation."
+ },
+ "external_seedream_base_url": {
+ "anyOf": [
{
- "$ref": "#/components/schemas/MetadataFieldExtractorInvocation"
+ "type": "string"
},
{
- "$ref": "#/components/schemas/MetadataFromImageInvocation"
- },
+ "type": "null"
+ }
+ ],
+ "title": "External Seedream Base Url",
+ "description": "Base URL override for Seedream image generation."
+ }
+ },
+ "additionalProperties": false,
+ "type": "object",
+ "title": "InvokeAIAppConfig",
+ "description": "Invoke's global app configuration.\n\nTypically, you won't need to interact with this class directly. Instead, use the `get_config` function from `invokeai.app.services.config` to get a singleton config object.\n\nAttributes:\n host: IP address to bind to. Use `0.0.0.0` to serve to your local network.\n port: Port to bind to.\n allow_origins: Allowed CORS origins.\n allow_credentials: Allow CORS credentials.\n allow_methods: Methods allowed for CORS.\n allow_headers: Headers allowed for CORS.\n ssl_certfile: SSL certificate file for HTTPS. See https://www.uvicorn.dev/settings/#https.\n ssl_keyfile: SSL key file for HTTPS. See https://www.uvicorn.dev/settings/#https.\n log_tokenization: Enable logging of parsed prompt tokens.\n patchmatch: Enable patchmatch inpaint code.\n models_dir: Path to the models directory.\n convert_cache_dir: Path to the converted models cache directory (DEPRECATED, but do not delete because it is needed for migration from previous versions).\n download_cache_dir: Path to the directory that contains dynamically downloaded models.\n legacy_conf_dir: Path to directory of legacy checkpoint config files.\n db_dir: Path to InvokeAI databases directory.\n outputs_dir: Path to directory for outputs.\n image_subfolder_strategy: Strategy for organizing images into subfolders. 'flat' stores all images in a single folder. 'date' organizes by YYYY/MM/DD. 'type' organizes by image category. 'hash' uses first 2 characters of UUID for filesystem performance. Valid values: `flat`, `date`, `type`, `hash`\n custom_nodes_dir: Path to directory for custom nodes.\n style_presets_dir: Path to directory for style presets.\n workflow_thumbnails_dir: Path to directory for workflow thumbnails.\n log_handlers: Log handler. Valid options are \"console\", \"file=\", \"syslog=path|address:host:port\", \"http=\".\n log_format: Log format. Use \"plain\" for text-only, \"color\" for colorized output, \"legacy\" for 2.3-style logging and \"syslog\" for syslog-style. Valid values: `plain`, `color`, `syslog`, `legacy`\n log_level: Emit logging messages at this level or higher. Valid values: `debug`, `info`, `warning`, `error`, `critical`\n log_sql: Log SQL queries. `log_level` must be `debug` for this to do anything. Extremely verbose.\n log_level_network: Log level for network-related messages. 'info' and 'debug' are very verbose. Valid values: `debug`, `info`, `warning`, `error`, `critical`\n use_memory_db: Use in-memory database. Useful for development.\n dev_reload: Automatically reload when Python sources are changed. Does not reload node definitions.\n profile_graphs: Enable graph profiling using `cProfile`.\n profile_prefix: An optional prefix for profile output files.\n profiles_dir: Path to profiles output directory.\n max_cache_ram_gb: The maximum amount of CPU RAM to use for model caching in GB. If unset, the limit will be configured based on the available RAM. In most cases, it is recommended to leave this unset.\n max_cache_vram_gb: The amount of VRAM to use for model caching in GB. If unset, the limit will be configured based on the available VRAM and the device_working_mem_gb. In most cases, it is recommended to leave this unset.\n log_memory_usage: If True, a memory snapshot will be captured before and after every model cache operation, and the result will be logged (at debug level). There is a time cost to capturing the memory snapshots, so it is recommended to only enable this feature if you are actively inspecting the model cache's behaviour.\n model_cache_keep_alive_min: How long to keep models in cache after last use, in minutes. A value of 0 (the default) means models are kept in cache indefinitely. If no model generations occur within the timeout period, the model cache is cleared using the same logic as the 'Clear Model Cache' button.\n device_working_mem_gb: The amount of working memory to keep available on the compute device (in GB). Has no effect if running on CPU. If you are experiencing OOM errors, try increasing this value.\n enable_partial_loading: Enable partial loading of models. This enables models to run with reduced VRAM requirements (at the cost of slower speed) by streaming the model from RAM to VRAM as its used. In some edge cases, partial loading can cause models to run more slowly if they were previously being fully loaded into VRAM.\n keep_ram_copy_of_weights: Whether to keep a full RAM copy of a model's weights when the model is loaded in VRAM. Keeping a RAM copy increases average RAM usage, but speeds up model switching and LoRA patching (assuming there is sufficient RAM). Set this to False if RAM pressure is consistently high.\n ram: DEPRECATED: This setting is no longer used. It has been replaced by `max_cache_ram_gb`, but most users will not need to use this config since automatic cache size limits should work well in most cases. This config setting will be removed once the new model cache behavior is stable.\n vram: DEPRECATED: This setting is no longer used. It has been replaced by `max_cache_vram_gb`, but most users will not need to use this config since automatic cache size limits should work well in most cases. This config setting will be removed once the new model cache behavior is stable.\n lazy_offload: DEPRECATED: This setting is no longer used. Lazy-offloading is enabled by default. This config setting will be removed once the new model cache behavior is stable.\n pytorch_cuda_alloc_conf: Configure the Torch CUDA memory allocator. This will impact peak reserved VRAM usage and performance. Setting to \"backend:cudaMallocAsync\" works well on many systems. The optimal configuration is highly dependent on the system configuration (device type, VRAM, CUDA driver version, etc.), so must be tuned experimentally.\n device: Preferred execution device. `auto` will choose the device depending on the hardware platform and the installed torch capabilities. Valid values: `auto`, `cpu`, `cuda`, `mps`, `cuda:N` (where N is a device number)\n precision: Floating point precision. `float16` will consume half the memory of `float32` but produce slightly lower-quality images. The `auto` setting will guess the proper precision based on your video card and operating system. Valid values: `auto`, `float16`, `bfloat16`, `float32`\n sequential_guidance: Whether to calculate guidance in serial instead of in parallel, lowering memory requirements.\n attention_type: Attention type. Valid values: `auto`, `normal`, `xformers`, `sliced`, `torch-sdp`\n attention_slice_size: Slice size, valid when attention_type==\"sliced\". Valid values: `auto`, `balanced`, `max`, `1`, `2`, `3`, `4`, `5`, `6`, `7`, `8`\n force_tiled_decode: Whether to enable tiled VAE decode (reduces memory consumption with some performance penalty).\n pil_compress_level: The compress_level setting of PIL.Image.save(), used for PNG encoding. All settings are lossless. 0 = no compression, 1 = fastest with slightly larger filesize, 9 = slowest with smallest filesize. 1 is typically the best setting.\n max_queue_size: Maximum number of items in the session queue.\n session_queue_mode: Session queue mode. Use 'FIFO' for traditional first-in-first-out, or 'round_robin' to serve each user's jobs in turn. In single-user mode, FIFO is always used regardless of this setting. Valid values: `FIFO`, `round_robin`\n clear_queue_on_startup: Empties session queue on startup. If true, disables `max_queue_history`.\n max_queue_history: Keep the last N completed, failed, and canceled queue items. Older items are deleted on startup. Set to 0 to prune all terminal items. Ignored if `clear_queue_on_startup` is true.\n allow_nodes: List of nodes to allow. Omit to allow all.\n deny_nodes: List of nodes to deny. Omit to deny none.\n node_cache_size: How many cached nodes to keep in memory.\n hashing_algorithm: Model hashing algorthim for model installs. 'blake3_multi' is best for SSDs. 'blake3_single' is best for spinning disk HDDs. 'random' disables hashing, instead assigning a UUID to models. Useful when using a memory db to reduce model installation time, or if you don't care about storing stable hashes for models. Alternatively, any other hashlib algorithm is accepted, though these are not nearly as performant as blake3. Valid values: `blake3_multi`, `blake3_single`, `random`, `md5`, `sha1`, `sha224`, `sha256`, `sha384`, `sha512`, `blake2b`, `blake2s`, `sha3_224`, `sha3_256`, `sha3_384`, `sha3_512`, `shake_128`, `shake_256`\n remote_api_tokens: List of regular expression and token pairs used when downloading models from URLs. The download URL is tested against the regex, and if it matches, the token is provided in as a Bearer token.\n scan_models_on_startup: Scan the models directory on startup, registering orphaned models. This is typically only used in conjunction with `use_memory_db` for testing purposes.\n unsafe_disable_picklescan: UNSAFE. Disable the picklescan security check during model installation. Recommended only for development and testing purposes. This will allow arbitrary code execution during model installation, so should never be used in production.\n allow_unknown_models: Allow installation of models that we are unable to identify. If enabled, models will be marked as `unknown` in the database, and will not have any metadata associated with them. If disabled, unknown models will be rejected during installation.\n multiuser: Enable multiuser support. When disabled, the application runs in single-user mode using a default system account with administrator privileges. When enabled, requires user authentication and authorization.\n strict_password_checking: Enforce strict password requirements. When True, passwords must contain uppercase, lowercase, and numbers. When False (default), any password is accepted but its strength (weak/moderate/strong) is reported to the user.\n external_alibabacloud_api_key: API key for Alibaba Cloud DashScope image generation.\n external_alibabacloud_base_url: Base URL override for Alibaba Cloud DashScope image generation.\n external_gemini_api_key: API key for Gemini image generation.\n external_openai_api_key: API key for OpenAI image generation.\n external_gemini_base_url: Base URL override for Gemini image generation.\n external_openai_base_url: Base URL override for OpenAI image generation.\n external_seedream_api_key: API key for Seedream image generation.\n external_seedream_base_url: Base URL override for Seedream image generation.\n base_url: Public base path when running behind a reverse proxy under a sub-path, e.g. `/invoke`. Set only when the proxy PRESERVES the sub-path (the backend receives `/invoke/api/...`). Leave unset when the proxy strips the sub-path or when serving at the domain root.\n forwarded_allow_ips: Comma-separated list of IPs (or `*`) allowed to set X-Forwarded-* headers. Set to the reverse proxy's IP. Only used when `base_url` is set."
+ },
+ "InvokeAIAppConfigWithSetFields": {
+ "properties": {
+ "set_fields": {
+ "items": {
+ "type": "string"
+ },
+ "type": "array",
+ "uniqueItems": true,
+ "title": "Set Fields",
+ "description": "The set fields"
+ },
+ "config": {
+ "$ref": "#/components/schemas/InvokeAIAppConfig",
+ "description": "The InvokeAI App Config"
+ }
+ },
+ "type": "object",
+ "required": ["set_fields", "config"],
+ "title": "InvokeAIAppConfigWithSetFields",
+ "description": "InvokeAI App Config with model fields set"
+ },
+ "InvokeAdjustImageHuePlusInvocation": {
+ "category": "image",
+ "class": "invocation",
+ "classification": "stable",
+ "description": "Adjusts the Hue of an image by rotating it in the selected color space. Originally created by @dwringer",
+ "node_pack": "invokeai",
+ "properties": {
+ "board": {
+ "anyOf": [
{
- "$ref": "#/components/schemas/MetadataInvocation"
+ "$ref": "#/components/schemas/BoardField"
},
{
- "$ref": "#/components/schemas/MetadataItemInvocation"
- },
+ "type": "null"
+ }
+ ],
+ "default": null,
+ "description": "The board to save the image to",
+ "field_kind": "internal",
+ "input": "direct",
+ "orig_required": false,
+ "ui_hidden": false
+ },
+ "metadata": {
+ "anyOf": [
{
- "$ref": "#/components/schemas/MetadataItemLinkedInvocation"
+ "$ref": "#/components/schemas/MetadataField"
},
{
- "$ref": "#/components/schemas/MetadataToBoolCollectionInvocation"
- },
+ "type": "null"
+ }
+ ],
+ "default": null,
+ "description": "Optional metadata to be saved with the image",
+ "field_kind": "internal",
+ "input": "connection",
+ "orig_required": false,
+ "ui_hidden": false
+ },
+ "id": {
+ "description": "The id of this instance of an invocation. Must be unique among all instances of invocations.",
+ "field_kind": "node_attribute",
+ "title": "Id",
+ "type": "string"
+ },
+ "is_intermediate": {
+ "default": false,
+ "description": "Whether or not this is an intermediate invocation.",
+ "field_kind": "node_attribute",
+ "input": "direct",
+ "orig_required": true,
+ "title": "Is Intermediate",
+ "type": "boolean",
+ "ui_hidden": false,
+ "ui_type": "IsIntermediate"
+ },
+ "use_cache": {
+ "default": true,
+ "description": "Whether or not to use the cache",
+ "field_kind": "node_attribute",
+ "title": "Use Cache",
+ "type": "boolean"
+ },
+ "image": {
+ "anyOf": [
{
- "$ref": "#/components/schemas/MetadataToBoolInvocation"
+ "$ref": "#/components/schemas/ImageField"
},
{
- "$ref": "#/components/schemas/MetadataToControlnetsInvocation"
- },
+ "type": "null"
+ }
+ ],
+ "default": null,
+ "description": "The image to adjust",
+ "field_kind": "input",
+ "input": "any",
+ "orig_required": true
+ },
+ "space": {
+ "default": "HSV / HSL / RGB",
+ "description": "Color space in which to rotate hue by polar coords (*: non-invertible)",
+ "enum": [
+ "HSV / HSL / RGB",
+ "Okhsl",
+ "Okhsv",
+ "*Oklch / Oklab",
+ "*LCh / CIELab",
+ "*UPLab (w/CIELab_to_UPLab.icc)"
+ ],
+ "field_kind": "input",
+ "input": "any",
+ "orig_default": "HSV / HSL / RGB",
+ "orig_required": false,
+ "title": "Space",
+ "type": "string"
+ },
+ "degrees": {
+ "default": 0.0,
+ "description": "Degrees by which to rotate image hue",
+ "field_kind": "input",
+ "input": "any",
+ "orig_default": 0.0,
+ "orig_required": false,
+ "title": "Degrees",
+ "type": "number"
+ },
+ "preserve_lightness": {
+ "default": false,
+ "description": "Whether to preserve CIELAB lightness values",
+ "field_kind": "input",
+ "input": "any",
+ "orig_default": false,
+ "orig_required": false,
+ "title": "Preserve Lightness",
+ "type": "boolean"
+ },
+ "ok_adaptive_gamut": {
+ "default": 0.05,
+ "description": "Higher preserves chroma at the expense of lightness (Oklab)",
+ "field_kind": "input",
+ "input": "any",
+ "minimum": 0,
+ "orig_default": 0.05,
+ "orig_required": false,
+ "title": "Ok Adaptive Gamut",
+ "type": "number"
+ },
+ "ok_high_precision": {
+ "default": true,
+ "description": "Use more steps in computing gamut (Oklab/Okhsv/Okhsl)",
+ "field_kind": "input",
+ "input": "any",
+ "orig_default": true,
+ "orig_required": false,
+ "title": "Ok High Precision",
+ "type": "boolean"
+ },
+ "type": {
+ "const": "invokeai_img_hue_adjust_plus",
+ "default": "invokeai_img_hue_adjust_plus",
+ "field_kind": "node_attribute",
+ "title": "type",
+ "type": "string"
+ }
+ },
+ "required": ["type", "id"],
+ "tags": ["image", "hue", "oklab", "cielab", "uplab", "lch", "hsv", "hsl", "lab"],
+ "title": "Adjust Image Hue Plus",
+ "type": "object",
+ "version": "1.2.0",
+ "output": {
+ "$ref": "#/components/schemas/ImageOutput"
+ }
+ },
+ "InvokeEquivalentAchromaticLightnessInvocation": {
+ "category": "image",
+ "class": "invocation",
+ "classification": "stable",
+ "description": "Calculate Equivalent Achromatic Lightness from image. Originally created by @dwringer",
+ "node_pack": "invokeai",
+ "properties": {
+ "board": {
+ "anyOf": [
{
- "$ref": "#/components/schemas/MetadataToFloatCollectionInvocation"
+ "$ref": "#/components/schemas/BoardField"
},
{
- "$ref": "#/components/schemas/MetadataToFloatInvocation"
- },
+ "type": "null"
+ }
+ ],
+ "default": null,
+ "description": "The board to save the image to",
+ "field_kind": "internal",
+ "input": "direct",
+ "orig_required": false,
+ "ui_hidden": false
+ },
+ "metadata": {
+ "anyOf": [
{
- "$ref": "#/components/schemas/MetadataToIPAdaptersInvocation"
+ "$ref": "#/components/schemas/MetadataField"
},
{
- "$ref": "#/components/schemas/MetadataToIntegerCollectionInvocation"
- },
+ "type": "null"
+ }
+ ],
+ "default": null,
+ "description": "Optional metadata to be saved with the image",
+ "field_kind": "internal",
+ "input": "connection",
+ "orig_required": false,
+ "ui_hidden": false
+ },
+ "id": {
+ "description": "The id of this instance of an invocation. Must be unique among all instances of invocations.",
+ "field_kind": "node_attribute",
+ "title": "Id",
+ "type": "string"
+ },
+ "is_intermediate": {
+ "default": false,
+ "description": "Whether or not this is an intermediate invocation.",
+ "field_kind": "node_attribute",
+ "input": "direct",
+ "orig_required": true,
+ "title": "Is Intermediate",
+ "type": "boolean",
+ "ui_hidden": false,
+ "ui_type": "IsIntermediate"
+ },
+ "use_cache": {
+ "default": true,
+ "description": "Whether or not to use the cache",
+ "field_kind": "node_attribute",
+ "title": "Use Cache",
+ "type": "boolean"
+ },
+ "image": {
+ "anyOf": [
{
- "$ref": "#/components/schemas/MetadataToIntegerInvocation"
+ "$ref": "#/components/schemas/ImageField"
},
{
- "$ref": "#/components/schemas/MetadataToLorasCollectionInvocation"
- },
+ "type": "null"
+ }
+ ],
+ "default": null,
+ "description": "Image from which to get channel",
+ "field_kind": "input",
+ "input": "any",
+ "orig_required": true
+ },
+ "type": {
+ "const": "invokeai_ealightness",
+ "default": "invokeai_ealightness",
+ "field_kind": "node_attribute",
+ "title": "type",
+ "type": "string"
+ }
+ },
+ "required": ["type", "id"],
+ "tags": ["image", "channel", "mask", "cielab", "lab"],
+ "title": "Equivalent Achromatic Lightness",
+ "type": "object",
+ "version": "1.2.0",
+ "output": {
+ "$ref": "#/components/schemas/ImageOutput"
+ }
+ },
+ "InvokeImageBlendInvocation": {
+ "category": "image",
+ "class": "invocation",
+ "classification": "stable",
+ "description": "Blend two images together, with optional opacity, mask, and blend modes. Originally created by @dwringer",
+ "node_pack": "invokeai",
+ "properties": {
+ "board": {
+ "anyOf": [
{
- "$ref": "#/components/schemas/MetadataToLorasInvocation"
+ "$ref": "#/components/schemas/BoardField"
},
{
- "$ref": "#/components/schemas/MetadataToModelInvocation"
- },
+ "type": "null"
+ }
+ ],
+ "default": null,
+ "description": "The board to save the image to",
+ "field_kind": "internal",
+ "input": "direct",
+ "orig_required": false,
+ "ui_hidden": false
+ },
+ "metadata": {
+ "anyOf": [
{
- "$ref": "#/components/schemas/MetadataToSDXLLorasInvocation"
+ "$ref": "#/components/schemas/MetadataField"
},
{
- "$ref": "#/components/schemas/MetadataToSDXLModelInvocation"
- },
+ "type": "null"
+ }
+ ],
+ "default": null,
+ "description": "Optional metadata to be saved with the image",
+ "field_kind": "internal",
+ "input": "connection",
+ "orig_required": false,
+ "ui_hidden": false
+ },
+ "id": {
+ "description": "The id of this instance of an invocation. Must be unique among all instances of invocations.",
+ "field_kind": "node_attribute",
+ "title": "Id",
+ "type": "string"
+ },
+ "is_intermediate": {
+ "default": false,
+ "description": "Whether or not this is an intermediate invocation.",
+ "field_kind": "node_attribute",
+ "input": "direct",
+ "orig_required": true,
+ "title": "Is Intermediate",
+ "type": "boolean",
+ "ui_hidden": false,
+ "ui_type": "IsIntermediate"
+ },
+ "use_cache": {
+ "default": true,
+ "description": "Whether or not to use the cache",
+ "field_kind": "node_attribute",
+ "title": "Use Cache",
+ "type": "boolean"
+ },
+ "layer_upper": {
+ "anyOf": [
{
- "$ref": "#/components/schemas/MetadataToSchedulerInvocation"
+ "$ref": "#/components/schemas/ImageField"
},
{
- "$ref": "#/components/schemas/MetadataToStringCollectionInvocation"
- },
+ "type": "null"
+ }
+ ],
+ "default": null,
+ "description": "The top image to blend",
+ "field_kind": "input",
+ "input": "any",
+ "orig_required": true,
+ "ui_order": 1
+ },
+ "blend_mode": {
+ "default": "Normal",
+ "description": "Available blend modes",
+ "enum": [
+ "Normal",
+ "Lighten Only",
+ "Darken Only",
+ "Lighten Only (EAL)",
+ "Darken Only (EAL)",
+ "Hue",
+ "Saturation",
+ "Color",
+ "Luminosity",
+ "Linear Dodge (Add)",
+ "Subtract",
+ "Multiply",
+ "Divide",
+ "Screen",
+ "Overlay",
+ "Linear Burn",
+ "Difference",
+ "Hard Light",
+ "Soft Light",
+ "Vivid Light",
+ "Linear Light",
+ "Color Burn",
+ "Color Dodge"
+ ],
+ "field_kind": "input",
+ "input": "any",
+ "orig_default": "Normal",
+ "orig_required": false,
+ "title": "Blend Mode",
+ "type": "string",
+ "ui_order": 2
+ },
+ "opacity": {
+ "default": 1.0,
+ "description": "Desired opacity of the upper layer",
+ "field_kind": "input",
+ "input": "any",
+ "minimum": 0,
+ "orig_default": 1.0,
+ "orig_required": false,
+ "title": "Opacity",
+ "type": "number",
+ "ui_order": 3
+ },
+ "mask": {
+ "anyOf": [
{
- "$ref": "#/components/schemas/MetadataToStringInvocation"
+ "$ref": "#/components/schemas/ImageField"
},
{
- "$ref": "#/components/schemas/MetadataToT2IAdaptersInvocation"
- },
+ "type": "null"
+ }
+ ],
+ "default": null,
+ "description": "Optional mask, used to restrict areas from blending",
+ "field_kind": "input",
+ "input": "any",
+ "orig_default": null,
+ "orig_required": false,
+ "ui_order": 4
+ },
+ "fit_to_width": {
+ "default": false,
+ "description": "Scale upper layer to fit base width",
+ "field_kind": "input",
+ "input": "any",
+ "orig_default": false,
+ "orig_required": false,
+ "title": "Fit To Width",
+ "type": "boolean",
+ "ui_order": 5
+ },
+ "fit_to_height": {
+ "default": true,
+ "description": "Scale upper layer to fit base height",
+ "field_kind": "input",
+ "input": "any",
+ "orig_default": true,
+ "orig_required": false,
+ "title": "Fit To Height",
+ "type": "boolean",
+ "ui_order": 6
+ },
+ "layer_base": {
+ "anyOf": [
{
- "$ref": "#/components/schemas/MetadataToVAEInvocation"
+ "$ref": "#/components/schemas/ImageField"
},
{
- "$ref": "#/components/schemas/ModelIdentifierInvocation"
- },
+ "type": "null"
+ }
+ ],
+ "default": null,
+ "description": "The bottom image to blend",
+ "field_kind": "input",
+ "input": "any",
+ "orig_required": true,
+ "ui_order": 7
+ },
+ "color_space": {
+ "default": "RGB",
+ "description": "Available color spaces for blend computations",
+ "enum": ["RGB", "Linear RGB", "HSL (RGB)", "HSV (RGB)", "Okhsl", "Okhsv", "Oklch (Oklab)", "LCh (CIELab)"],
+ "field_kind": "input",
+ "input": "any",
+ "orig_default": "RGB",
+ "orig_required": false,
+ "title": "Color Space",
+ "type": "string",
+ "ui_order": 8
+ },
+ "adaptive_gamut": {
+ "default": 0.0,
+ "description": "Adaptive gamut clipping (0=off). Higher prioritizes chroma over lightness",
+ "field_kind": "input",
+ "input": "any",
+ "minimum": 0,
+ "orig_default": 0.0,
+ "orig_required": false,
+ "title": "Adaptive Gamut",
+ "type": "number",
+ "ui_order": 9
+ },
+ "high_precision": {
+ "default": true,
+ "description": "Use more steps in computing gamut when possible",
+ "field_kind": "input",
+ "input": "any",
+ "orig_default": true,
+ "orig_required": false,
+ "title": "High Precision",
+ "type": "boolean",
+ "ui_order": 10
+ },
+ "type": {
+ "const": "invokeai_img_blend",
+ "default": "invokeai_img_blend",
+ "field_kind": "node_attribute",
+ "title": "type",
+ "type": "string"
+ }
+ },
+ "required": ["type", "id"],
+ "tags": ["image", "blend", "layer", "alpha", "composite", "dodge", "burn"],
+ "title": "Image Layer Blend",
+ "type": "object",
+ "version": "1.2.0",
+ "output": {
+ "$ref": "#/components/schemas/ImageOutput"
+ }
+ },
+ "InvokeImageCompositorInvocation": {
+ "category": "image",
+ "class": "invocation",
+ "classification": "stable",
+ "description": "Removes backdrop from subject image then overlays subject on background image. Originally created by @dwringer",
+ "node_pack": "invokeai",
+ "properties": {
+ "board": {
+ "anyOf": [
{
- "$ref": "#/components/schemas/MultiplyInvocation"
+ "$ref": "#/components/schemas/BoardField"
},
{
- "$ref": "#/components/schemas/NoiseInvocation"
- },
+ "type": "null"
+ }
+ ],
+ "default": null,
+ "description": "The board to save the image to",
+ "field_kind": "internal",
+ "input": "direct",
+ "orig_required": false,
+ "ui_hidden": false
+ },
+ "metadata": {
+ "anyOf": [
{
- "$ref": "#/components/schemas/NormalMapInvocation"
+ "$ref": "#/components/schemas/MetadataField"
},
{
- "$ref": "#/components/schemas/OklabUnsharpMaskInvocation"
- },
+ "type": "null"
+ }
+ ],
+ "default": null,
+ "description": "Optional metadata to be saved with the image",
+ "field_kind": "internal",
+ "input": "connection",
+ "orig_required": false,
+ "ui_hidden": false
+ },
+ "id": {
+ "description": "The id of this instance of an invocation. Must be unique among all instances of invocations.",
+ "field_kind": "node_attribute",
+ "title": "Id",
+ "type": "string"
+ },
+ "is_intermediate": {
+ "default": false,
+ "description": "Whether or not this is an intermediate invocation.",
+ "field_kind": "node_attribute",
+ "input": "direct",
+ "orig_required": true,
+ "title": "Is Intermediate",
+ "type": "boolean",
+ "ui_hidden": false,
+ "ui_type": "IsIntermediate"
+ },
+ "use_cache": {
+ "default": true,
+ "description": "Whether or not to use the cache",
+ "field_kind": "node_attribute",
+ "title": "Use Cache",
+ "type": "boolean"
+ },
+ "image_subject": {
+ "anyOf": [
{
- "$ref": "#/components/schemas/OklchImageHueAdjustmentInvocation"
+ "$ref": "#/components/schemas/ImageField"
},
{
- "$ref": "#/components/schemas/OpenAIImageGenerationInvocation"
- },
+ "type": "null"
+ }
+ ],
+ "default": null,
+ "description": "Image of the subject on a plain monochrome background",
+ "field_kind": "input",
+ "input": "any",
+ "orig_required": true
+ },
+ "image_background": {
+ "anyOf": [
{
- "$ref": "#/components/schemas/PBRMapsInvocation"
+ "$ref": "#/components/schemas/ImageField"
},
{
- "$ref": "#/components/schemas/PairTileImageInvocation"
- },
+ "type": "null"
+ }
+ ],
+ "default": null,
+ "description": "Image of a background scene",
+ "field_kind": "input",
+ "input": "any",
+ "orig_required": true
+ },
+ "chroma_key": {
+ "default": "",
+ "description": "Can be empty for corner flood select, or CSS-3 color or tuple",
+ "field_kind": "input",
+ "input": "any",
+ "orig_default": "",
+ "orig_required": false,
+ "title": "Chroma Key",
+ "type": "string"
+ },
+ "threshold": {
+ "default": 50,
+ "description": "Subject isolation flood-fill threshold",
+ "field_kind": "input",
+ "input": "any",
+ "minimum": 0,
+ "orig_default": 50,
+ "orig_required": false,
+ "title": "Threshold",
+ "type": "integer"
+ },
+ "fill_x": {
+ "default": false,
+ "description": "Scale base subject image to fit background width",
+ "field_kind": "input",
+ "input": "any",
+ "orig_default": false,
+ "orig_required": false,
+ "title": "Fill X",
+ "type": "boolean"
+ },
+ "fill_y": {
+ "default": true,
+ "description": "Scale base subject image to fit background height",
+ "field_kind": "input",
+ "input": "any",
+ "orig_default": true,
+ "orig_required": false,
+ "title": "Fill Y",
+ "type": "boolean"
+ },
+ "x_offset": {
+ "default": 0,
+ "description": "x-offset for the subject",
+ "field_kind": "input",
+ "input": "any",
+ "orig_default": 0,
+ "orig_required": false,
+ "title": "X Offset",
+ "type": "integer"
+ },
+ "y_offset": {
+ "default": 0,
+ "description": "y-offset for the subject",
+ "field_kind": "input",
+ "input": "any",
+ "orig_default": 0,
+ "orig_required": false,
+ "title": "Y Offset",
+ "type": "integer"
+ },
+ "type": {
+ "const": "invokeai_img_composite",
+ "default": "invokeai_img_composite",
+ "field_kind": "node_attribute",
+ "title": "type",
+ "type": "string"
+ }
+ },
+ "required": ["type", "id"],
+ "tags": ["image", "compose", "chroma", "key"],
+ "title": "Image Compositor",
+ "type": "object",
+ "version": "1.2.0",
+ "output": {
+ "$ref": "#/components/schemas/ImageOutput"
+ }
+ },
+ "InvokeImageDilateOrErodeInvocation": {
+ "category": "image",
+ "class": "invocation",
+ "classification": "stable",
+ "description": "Dilate (expand) or erode (contract) an image. Originally created by @dwringer",
+ "node_pack": "invokeai",
+ "properties": {
+ "metadata": {
+ "anyOf": [
{
- "$ref": "#/components/schemas/PasteImageIntoBoundingBoxInvocation"
+ "$ref": "#/components/schemas/MetadataField"
},
{
- "$ref": "#/components/schemas/PiDiNetEdgeDetectionInvocation"
- },
+ "type": "null"
+ }
+ ],
+ "default": null,
+ "description": "Optional metadata to be saved with the image",
+ "field_kind": "internal",
+ "input": "connection",
+ "orig_required": false,
+ "ui_hidden": false
+ },
+ "id": {
+ "description": "The id of this instance of an invocation. Must be unique among all instances of invocations.",
+ "field_kind": "node_attribute",
+ "title": "Id",
+ "type": "string"
+ },
+ "is_intermediate": {
+ "default": false,
+ "description": "Whether or not this is an intermediate invocation.",
+ "field_kind": "node_attribute",
+ "input": "direct",
+ "orig_required": true,
+ "title": "Is Intermediate",
+ "type": "boolean",
+ "ui_hidden": false,
+ "ui_type": "IsIntermediate"
+ },
+ "use_cache": {
+ "default": true,
+ "description": "Whether or not to use the cache",
+ "field_kind": "node_attribute",
+ "title": "Use Cache",
+ "type": "boolean"
+ },
+ "image": {
+ "anyOf": [
{
- "$ref": "#/components/schemas/PromptTemplateInvocation"
+ "$ref": "#/components/schemas/ImageField"
},
{
- "$ref": "#/components/schemas/PromptsFromFileInvocation"
- },
+ "type": "null"
+ }
+ ],
+ "default": null,
+ "description": "The image from which to create a mask",
+ "field_kind": "input",
+ "input": "any",
+ "orig_required": true
+ },
+ "lightness_only": {
+ "default": false,
+ "description": "If true, only applies to image lightness (CIELa*b*)",
+ "field_kind": "input",
+ "input": "any",
+ "orig_default": false,
+ "orig_required": false,
+ "title": "Lightness Only",
+ "type": "boolean"
+ },
+ "radius_w": {
+ "default": 4,
+ "description": "Width (in pixels) by which to dilate(expand) or erode (contract) the image",
+ "field_kind": "input",
+ "input": "any",
+ "minimum": 0,
+ "orig_default": 4,
+ "orig_required": false,
+ "title": "Radius W",
+ "type": "integer"
+ },
+ "radius_h": {
+ "default": 4,
+ "description": "Height (in pixels) by which to dilate(expand) or erode (contract) the image",
+ "field_kind": "input",
+ "input": "any",
+ "minimum": 0,
+ "orig_default": 4,
+ "orig_required": false,
+ "title": "Radius H",
+ "type": "integer"
+ },
+ "mode": {
+ "default": "Dilate",
+ "description": "How to operate on the image",
+ "enum": ["Dilate", "Erode"],
+ "field_kind": "input",
+ "input": "any",
+ "orig_default": "Dilate",
+ "orig_required": false,
+ "title": "Mode",
+ "type": "string"
+ },
+ "type": {
+ "const": "invokeai_img_dilate_erode",
+ "default": "invokeai_img_dilate_erode",
+ "field_kind": "node_attribute",
+ "title": "type",
+ "type": "string"
+ }
+ },
+ "required": ["type", "id"],
+ "tags": ["image", "mask", "dilate", "erode", "expand", "contract", "mask"],
+ "title": "Image Dilate or Erode",
+ "type": "object",
+ "version": "1.3.0",
+ "output": {
+ "$ref": "#/components/schemas/ImageOutput"
+ }
+ },
+ "InvokeImageEnhanceInvocation": {
+ "category": "image",
+ "class": "invocation",
+ "classification": "stable",
+ "description": "Applies processing from PIL's ImageEnhance module. Originally created by @dwringer",
+ "node_pack": "invokeai",
+ "properties": {
+ "board": {
+ "anyOf": [
{
- "$ref": "#/components/schemas/QwenImageDenoiseInvocation"
+ "$ref": "#/components/schemas/BoardField"
},
{
- "$ref": "#/components/schemas/QwenImageImageToLatentsInvocation"
- },
+ "type": "null"
+ }
+ ],
+ "default": null,
+ "description": "The board to save the image to",
+ "field_kind": "internal",
+ "input": "direct",
+ "orig_required": false,
+ "ui_hidden": false
+ },
+ "metadata": {
+ "anyOf": [
{
- "$ref": "#/components/schemas/QwenImageLatentsToImageInvocation"
+ "$ref": "#/components/schemas/MetadataField"
},
{
- "$ref": "#/components/schemas/QwenImageLoRACollectionLoader"
- },
+ "type": "null"
+ }
+ ],
+ "default": null,
+ "description": "Optional metadata to be saved with the image",
+ "field_kind": "internal",
+ "input": "connection",
+ "orig_required": false,
+ "ui_hidden": false
+ },
+ "id": {
+ "description": "The id of this instance of an invocation. Must be unique among all instances of invocations.",
+ "field_kind": "node_attribute",
+ "title": "Id",
+ "type": "string"
+ },
+ "is_intermediate": {
+ "default": false,
+ "description": "Whether or not this is an intermediate invocation.",
+ "field_kind": "node_attribute",
+ "input": "direct",
+ "orig_required": true,
+ "title": "Is Intermediate",
+ "type": "boolean",
+ "ui_hidden": false,
+ "ui_type": "IsIntermediate"
+ },
+ "use_cache": {
+ "default": true,
+ "description": "Whether or not to use the cache",
+ "field_kind": "node_attribute",
+ "title": "Use Cache",
+ "type": "boolean"
+ },
+ "image": {
+ "anyOf": [
{
- "$ref": "#/components/schemas/QwenImageLoRALoaderInvocation"
+ "$ref": "#/components/schemas/ImageField"
},
{
- "$ref": "#/components/schemas/QwenImageModelLoaderInvocation"
- },
+ "type": "null"
+ }
+ ],
+ "default": null,
+ "description": "The image for which to apply processing",
+ "field_kind": "input",
+ "input": "any",
+ "orig_required": true
+ },
+ "invert": {
+ "default": false,
+ "description": "Whether to invert the image colors",
+ "field_kind": "input",
+ "input": "any",
+ "orig_default": false,
+ "orig_required": false,
+ "title": "Invert",
+ "type": "boolean"
+ },
+ "color": {
+ "default": 1.0,
+ "description": "Color enhancement factor",
+ "field_kind": "input",
+ "input": "any",
+ "minimum": 0,
+ "orig_default": 1.0,
+ "orig_required": false,
+ "title": "Color",
+ "type": "number"
+ },
+ "contrast": {
+ "default": 1.0,
+ "description": "Contrast enhancement factor",
+ "field_kind": "input",
+ "input": "any",
+ "minimum": 0,
+ "orig_default": 1.0,
+ "orig_required": false,
+ "title": "Contrast",
+ "type": "number"
+ },
+ "brightness": {
+ "default": 1.0,
+ "description": "Brightness enhancement factor",
+ "field_kind": "input",
+ "input": "any",
+ "minimum": 0,
+ "orig_default": 1.0,
+ "orig_required": false,
+ "title": "Brightness",
+ "type": "number"
+ },
+ "sharpness": {
+ "default": 1.0,
+ "description": "Sharpness enhancement factor",
+ "field_kind": "input",
+ "input": "any",
+ "minimum": 0,
+ "orig_default": 1.0,
+ "orig_required": false,
+ "title": "Sharpness",
+ "type": "number"
+ },
+ "type": {
+ "const": "invokeai_img_enhance",
+ "default": "invokeai_img_enhance",
+ "field_kind": "node_attribute",
+ "title": "type",
+ "type": "string"
+ }
+ },
+ "required": ["type", "id"],
+ "tags": ["enhance", "image"],
+ "title": "Enhance Image",
+ "type": "object",
+ "version": "1.2.1",
+ "output": {
+ "$ref": "#/components/schemas/ImageOutput"
+ }
+ },
+ "InvokeImageValueThresholdsInvocation": {
+ "category": "image",
+ "class": "invocation",
+ "classification": "stable",
+ "description": "Clip image to pure black/white past specified thresholds. Originally created by @dwringer",
+ "node_pack": "invokeai",
+ "properties": {
+ "board": {
+ "anyOf": [
{
- "$ref": "#/components/schemas/QwenImageTextEncoderInvocation"
+ "$ref": "#/components/schemas/BoardField"
},
{
- "$ref": "#/components/schemas/RandomFloatInvocation"
- },
+ "type": "null"
+ }
+ ],
+ "default": null,
+ "description": "The board to save the image to",
+ "field_kind": "internal",
+ "input": "direct",
+ "orig_required": false,
+ "ui_hidden": false
+ },
+ "metadata": {
+ "anyOf": [
{
- "$ref": "#/components/schemas/RandomIntInvocation"
+ "$ref": "#/components/schemas/MetadataField"
},
{
- "$ref": "#/components/schemas/RandomRangeInvocation"
- },
+ "type": "null"
+ }
+ ],
+ "default": null,
+ "description": "Optional metadata to be saved with the image",
+ "field_kind": "internal",
+ "input": "connection",
+ "orig_required": false,
+ "ui_hidden": false
+ },
+ "id": {
+ "description": "The id of this instance of an invocation. Must be unique among all instances of invocations.",
+ "field_kind": "node_attribute",
+ "title": "Id",
+ "type": "string"
+ },
+ "is_intermediate": {
+ "default": false,
+ "description": "Whether or not this is an intermediate invocation.",
+ "field_kind": "node_attribute",
+ "input": "direct",
+ "orig_required": true,
+ "title": "Is Intermediate",
+ "type": "boolean",
+ "ui_hidden": false,
+ "ui_type": "IsIntermediate"
+ },
+ "use_cache": {
+ "default": true,
+ "description": "Whether or not to use the cache",
+ "field_kind": "node_attribute",
+ "title": "Use Cache",
+ "type": "boolean"
+ },
+ "image": {
+ "anyOf": [
{
- "$ref": "#/components/schemas/RangeInvocation"
+ "$ref": "#/components/schemas/ImageField"
},
{
- "$ref": "#/components/schemas/RangeOfSizeInvocation"
- },
+ "type": "null"
+ }
+ ],
+ "default": null,
+ "description": "The image from which to create a mask",
+ "field_kind": "input",
+ "input": "any",
+ "orig_required": true
+ },
+ "invert_output": {
+ "default": false,
+ "description": "Make light areas dark and vice versa",
+ "field_kind": "input",
+ "input": "any",
+ "orig_default": false,
+ "orig_required": false,
+ "title": "Invert Output",
+ "type": "boolean"
+ },
+ "renormalize_values": {
+ "default": false,
+ "description": "Rescale remaining values from minimum to maximum",
+ "field_kind": "input",
+ "input": "any",
+ "orig_default": false,
+ "orig_required": false,
+ "title": "Renormalize Values",
+ "type": "boolean"
+ },
+ "lightness_only": {
+ "default": false,
+ "description": "If true, only applies to image lightness (CIELa*b*)",
+ "field_kind": "input",
+ "input": "any",
+ "orig_default": false,
+ "orig_required": false,
+ "title": "Lightness Only",
+ "type": "boolean"
+ },
+ "threshold_upper": {
+ "default": 0.5,
+ "description": "Threshold above which will be set to full value",
+ "field_kind": "input",
+ "input": "any",
+ "orig_default": 0.5,
+ "orig_required": false,
+ "title": "Threshold Upper",
+ "type": "number"
+ },
+ "threshold_lower": {
+ "default": 0.5,
+ "description": "Threshold below which will be set to minimum value",
+ "field_kind": "input",
+ "input": "any",
+ "orig_default": 0.5,
+ "orig_required": false,
+ "title": "Threshold Lower",
+ "type": "number"
+ },
+ "type": {
+ "const": "invokeai_img_val_thresholds",
+ "default": "invokeai_img_val_thresholds",
+ "field_kind": "node_attribute",
+ "title": "type",
+ "type": "string"
+ }
+ },
+ "required": ["type", "id"],
+ "tags": ["image", "mask", "value", "threshold"],
+ "title": "Image Value Thresholds",
+ "type": "object",
+ "version": "1.2.0",
+ "output": {
+ "$ref": "#/components/schemas/ImageOutput"
+ }
+ },
+ "ItemIdsResult": {
+ "properties": {
+ "item_ids": {
+ "items": {
+ "type": "integer"
+ },
+ "type": "array",
+ "title": "Item Ids",
+ "description": "Ordered list of item ids"
+ },
+ "total_count": {
+ "type": "integer",
+ "title": "Total Count",
+ "description": "Total number of queue items matching the query"
+ }
+ },
+ "type": "object",
+ "required": ["item_ids", "total_count"],
+ "title": "ItemIdsResult",
+ "description": "Response containing ordered item ids with metadata for optimistic updates."
+ },
+ "IterateInvocation": {
+ "class": "invocation",
+ "classification": "stable",
+ "description": "Iterates over a list of items",
+ "node_pack": "invokeai",
+ "properties": {
+ "id": {
+ "description": "The id of this instance of an invocation. Must be unique among all instances of invocations.",
+ "field_kind": "node_attribute",
+ "title": "Id",
+ "type": "string"
+ },
+ "is_intermediate": {
+ "default": false,
+ "description": "Whether or not this is an intermediate invocation.",
+ "field_kind": "node_attribute",
+ "input": "direct",
+ "orig_required": true,
+ "title": "Is Intermediate",
+ "type": "boolean",
+ "ui_hidden": false,
+ "ui_type": "IsIntermediate"
+ },
+ "use_cache": {
+ "default": true,
+ "description": "Whether or not to use the cache",
+ "field_kind": "node_attribute",
+ "title": "Use Cache",
+ "type": "boolean"
+ },
+ "collection": {
+ "default": [],
+ "description": "The list of items to iterate over",
+ "field_kind": "input",
+ "input": "any",
+ "items": {},
+ "orig_default": [],
+ "orig_required": false,
+ "title": "Collection",
+ "type": "array",
+ "ui_type": "CollectionField"
+ },
+ "index": {
+ "default": 0,
+ "description": "The index, will be provided on executed iterators",
+ "field_kind": "input",
+ "input": "any",
+ "orig_default": 0,
+ "orig_required": false,
+ "title": "Index",
+ "type": "integer",
+ "ui_hidden": true
+ },
+ "type": {
+ "const": "iterate",
+ "default": "iterate",
+ "field_kind": "node_attribute",
+ "title": "type",
+ "type": "string"
+ }
+ },
+ "required": ["type", "id"],
+ "title": "IterateInvocation",
+ "type": "object",
+ "version": "1.1.0",
+ "output": {
+ "$ref": "#/components/schemas/IterateInvocationOutput"
+ }
+ },
+ "IterateInvocationOutput": {
+ "class": "output",
+ "description": "Used to connect iteration outputs. Will be expanded to a specific output.",
+ "properties": {
+ "item": {
+ "description": "The item being iterated over",
+ "field_kind": "output",
+ "title": "Collection Item",
+ "ui_hidden": false,
+ "ui_type": "CollectionItemField"
+ },
+ "index": {
+ "description": "The index of the item",
+ "field_kind": "output",
+ "title": "Index",
+ "type": "integer",
+ "ui_hidden": false
+ },
+ "total": {
+ "description": "The total number of items",
+ "field_kind": "output",
+ "title": "Total",
+ "type": "integer",
+ "ui_hidden": false
+ },
+ "type": {
+ "const": "iterate_output",
+ "default": "iterate_output",
+ "field_kind": "node_attribute",
+ "title": "type",
+ "type": "string"
+ }
+ },
+ "required": ["output_meta", "item", "index", "total", "type", "type"],
+ "title": "IterateInvocationOutput",
+ "type": "object"
+ },
+ "JsonValue": {},
+ "LaMaInfillInvocation": {
+ "category": "inpaint",
+ "class": "invocation",
+ "classification": "stable",
+ "description": "Infills transparent areas of an image using the LaMa model",
+ "node_pack": "invokeai",
+ "properties": {
+ "board": {
+ "anyOf": [
{
- "$ref": "#/components/schemas/RectangleMaskInvocation"
+ "$ref": "#/components/schemas/BoardField"
},
{
- "$ref": "#/components/schemas/ResizeLatentsInvocation"
- },
+ "type": "null"
+ }
+ ],
+ "default": null,
+ "description": "The board to save the image to",
+ "field_kind": "internal",
+ "input": "direct",
+ "orig_required": false,
+ "ui_hidden": false
+ },
+ "metadata": {
+ "anyOf": [
{
- "$ref": "#/components/schemas/RoundInvocation"
+ "$ref": "#/components/schemas/MetadataField"
},
{
- "$ref": "#/components/schemas/SD3DenoiseInvocation"
- },
+ "type": "null"
+ }
+ ],
+ "default": null,
+ "description": "Optional metadata to be saved with the image",
+ "field_kind": "internal",
+ "input": "connection",
+ "orig_required": false,
+ "ui_hidden": false
+ },
+ "id": {
+ "description": "The id of this instance of an invocation. Must be unique among all instances of invocations.",
+ "field_kind": "node_attribute",
+ "title": "Id",
+ "type": "string"
+ },
+ "is_intermediate": {
+ "default": false,
+ "description": "Whether or not this is an intermediate invocation.",
+ "field_kind": "node_attribute",
+ "input": "direct",
+ "orig_required": true,
+ "title": "Is Intermediate",
+ "type": "boolean",
+ "ui_hidden": false,
+ "ui_type": "IsIntermediate"
+ },
+ "use_cache": {
+ "default": true,
+ "description": "Whether or not to use the cache",
+ "field_kind": "node_attribute",
+ "title": "Use Cache",
+ "type": "boolean"
+ },
+ "image": {
+ "anyOf": [
{
- "$ref": "#/components/schemas/SD3ImageToLatentsInvocation"
+ "$ref": "#/components/schemas/ImageField"
},
{
- "$ref": "#/components/schemas/SD3LatentsToImageInvocation"
- },
+ "type": "null"
+ }
+ ],
+ "default": null,
+ "description": "The image to process",
+ "field_kind": "input",
+ "input": "any",
+ "orig_required": true
+ },
+ "type": {
+ "const": "infill_lama",
+ "default": "infill_lama",
+ "field_kind": "node_attribute",
+ "title": "type",
+ "type": "string"
+ }
+ },
+ "required": ["type", "id"],
+ "tags": ["image", "inpaint"],
+ "title": "LaMa Infill",
+ "type": "object",
+ "version": "1.2.2",
+ "output": {
+ "$ref": "#/components/schemas/ImageOutput"
+ }
+ },
+ "LatentsCollectionInvocation": {
+ "category": "primitives",
+ "class": "invocation",
+ "classification": "stable",
+ "description": "A collection of latents tensor primitive values",
+ "node_pack": "invokeai",
+ "properties": {
+ "id": {
+ "description": "The id of this instance of an invocation. Must be unique among all instances of invocations.",
+ "field_kind": "node_attribute",
+ "title": "Id",
+ "type": "string"
+ },
+ "is_intermediate": {
+ "default": false,
+ "description": "Whether or not this is an intermediate invocation.",
+ "field_kind": "node_attribute",
+ "input": "direct",
+ "orig_required": true,
+ "title": "Is Intermediate",
+ "type": "boolean",
+ "ui_hidden": false,
+ "ui_type": "IsIntermediate"
+ },
+ "use_cache": {
+ "default": true,
+ "description": "Whether or not to use the cache",
+ "field_kind": "node_attribute",
+ "title": "Use Cache",
+ "type": "boolean"
+ },
+ "collection": {
+ "anyOf": [
{
- "$ref": "#/components/schemas/SDXLCompelPromptInvocation"
+ "items": {
+ "$ref": "#/components/schemas/LatentsField"
+ },
+ "type": "array"
},
{
- "$ref": "#/components/schemas/SDXLLoRACollectionLoader"
- },
+ "type": "null"
+ }
+ ],
+ "default": null,
+ "description": "The collection of latents tensors",
+ "field_kind": "input",
+ "input": "any",
+ "orig_required": true,
+ "title": "Collection"
+ },
+ "type": {
+ "const": "latents_collection",
+ "default": "latents_collection",
+ "field_kind": "node_attribute",
+ "title": "type",
+ "type": "string"
+ }
+ },
+ "required": ["type", "id"],
+ "tags": ["primitives", "latents", "collection"],
+ "title": "Latents Collection Primitive",
+ "type": "object",
+ "version": "1.0.1",
+ "output": {
+ "$ref": "#/components/schemas/LatentsCollectionOutput"
+ }
+ },
+ "LatentsCollectionOutput": {
+ "class": "output",
+ "description": "Base class for nodes that output a collection of latents tensors",
+ "properties": {
+ "collection": {
+ "description": "Latents tensor",
+ "field_kind": "output",
+ "items": {
+ "$ref": "#/components/schemas/LatentsField"
+ },
+ "title": "Collection",
+ "type": "array",
+ "ui_hidden": false
+ },
+ "type": {
+ "const": "latents_collection_output",
+ "default": "latents_collection_output",
+ "field_kind": "node_attribute",
+ "title": "type",
+ "type": "string"
+ }
+ },
+ "required": ["output_meta", "collection", "type", "type"],
+ "title": "LatentsCollectionOutput",
+ "type": "object"
+ },
+ "LatentsField": {
+ "description": "A latents tensor primitive field",
+ "properties": {
+ "latents_name": {
+ "description": "The name of the latents",
+ "title": "Latents Name",
+ "type": "string"
+ },
+ "seed": {
+ "anyOf": [
{
- "$ref": "#/components/schemas/SDXLLoRALoaderInvocation"
+ "type": "integer"
},
{
- "$ref": "#/components/schemas/SDXLModelLoaderInvocation"
- },
+ "type": "null"
+ }
+ ],
+ "default": null,
+ "description": "Seed used to generate this latents",
+ "title": "Seed"
+ }
+ },
+ "required": ["latents_name"],
+ "title": "LatentsField",
+ "type": "object"
+ },
+ "LatentsInvocation": {
+ "category": "primitives",
+ "class": "invocation",
+ "classification": "stable",
+ "description": "A latents tensor primitive value",
+ "node_pack": "invokeai",
+ "properties": {
+ "id": {
+ "description": "The id of this instance of an invocation. Must be unique among all instances of invocations.",
+ "field_kind": "node_attribute",
+ "title": "Id",
+ "type": "string"
+ },
+ "is_intermediate": {
+ "default": false,
+ "description": "Whether or not this is an intermediate invocation.",
+ "field_kind": "node_attribute",
+ "input": "direct",
+ "orig_required": true,
+ "title": "Is Intermediate",
+ "type": "boolean",
+ "ui_hidden": false,
+ "ui_type": "IsIntermediate"
+ },
+ "use_cache": {
+ "default": true,
+ "description": "Whether or not to use the cache",
+ "field_kind": "node_attribute",
+ "title": "Use Cache",
+ "type": "boolean"
+ },
+ "latents": {
+ "anyOf": [
{
- "$ref": "#/components/schemas/SDXLRefinerCompelPromptInvocation"
+ "$ref": "#/components/schemas/LatentsField"
},
{
- "$ref": "#/components/schemas/SDXLRefinerModelLoaderInvocation"
- },
+ "type": "null"
+ }
+ ],
+ "default": null,
+ "description": "The latents tensor",
+ "field_kind": "input",
+ "input": "connection",
+ "orig_required": true
+ },
+ "type": {
+ "const": "latents",
+ "default": "latents",
+ "field_kind": "node_attribute",
+ "title": "type",
+ "type": "string"
+ }
+ },
+ "required": ["type", "id"],
+ "tags": ["primitives", "latents"],
+ "title": "Latents Primitive",
+ "type": "object",
+ "version": "1.0.2",
+ "output": {
+ "$ref": "#/components/schemas/LatentsOutput"
+ }
+ },
+ "LatentsMetaOutput": {
+ "class": "output",
+ "description": "Latents + metadata",
+ "properties": {
+ "metadata": {
+ "$ref": "#/components/schemas/MetadataField",
+ "description": "Metadata Dict",
+ "field_kind": "output",
+ "ui_hidden": false
+ },
+ "type": {
+ "const": "latents_meta_output",
+ "default": "latents_meta_output",
+ "field_kind": "node_attribute",
+ "title": "type",
+ "type": "string"
+ },
+ "latents": {
+ "$ref": "#/components/schemas/LatentsField",
+ "description": "Latents tensor",
+ "field_kind": "output",
+ "ui_hidden": false
+ },
+ "width": {
+ "description": "Width of output (px)",
+ "field_kind": "output",
+ "title": "Width",
+ "type": "integer",
+ "ui_hidden": false
+ },
+ "height": {
+ "description": "Height of output (px)",
+ "field_kind": "output",
+ "title": "Height",
+ "type": "integer",
+ "ui_hidden": false
+ }
+ },
+ "required": ["output_meta", "metadata", "type", "latents", "width", "height", "type"],
+ "title": "LatentsMetaOutput",
+ "type": "object"
+ },
+ "LatentsOutput": {
+ "class": "output",
+ "description": "Base class for nodes that output a single latents tensor",
+ "properties": {
+ "latents": {
+ "$ref": "#/components/schemas/LatentsField",
+ "description": "Latents tensor",
+ "field_kind": "output",
+ "ui_hidden": false
+ },
+ "width": {
+ "description": "Width of output (px)",
+ "field_kind": "output",
+ "title": "Width",
+ "type": "integer",
+ "ui_hidden": false
+ },
+ "height": {
+ "description": "Height of output (px)",
+ "field_kind": "output",
+ "title": "Height",
+ "type": "integer",
+ "ui_hidden": false
+ },
+ "type": {
+ "const": "latents_output",
+ "default": "latents_output",
+ "field_kind": "node_attribute",
+ "title": "type",
+ "type": "string"
+ }
+ },
+ "required": ["output_meta", "latents", "width", "height", "type", "type"],
+ "title": "LatentsOutput",
+ "type": "object"
+ },
+ "LatentsToImageInvocation": {
+ "category": "latents",
+ "class": "invocation",
+ "classification": "stable",
+ "description": "Generates an image from latents.",
+ "node_pack": "invokeai",
+ "properties": {
+ "board": {
+ "anyOf": [
{
- "$ref": "#/components/schemas/SaveImageInvocation"
+ "$ref": "#/components/schemas/BoardField"
},
{
- "$ref": "#/components/schemas/SaveImageToFileInvocation"
- },
+ "type": "null"
+ }
+ ],
+ "default": null,
+ "description": "The board to save the image to",
+ "field_kind": "internal",
+ "input": "direct",
+ "orig_required": false,
+ "ui_hidden": false
+ },
+ "metadata": {
+ "anyOf": [
{
- "$ref": "#/components/schemas/ScaleLatentsInvocation"
+ "$ref": "#/components/schemas/MetadataField"
},
{
- "$ref": "#/components/schemas/SchedulerInvocation"
- },
+ "type": "null"
+ }
+ ],
+ "default": null,
+ "description": "Optional metadata to be saved with the image",
+ "field_kind": "internal",
+ "input": "connection",
+ "orig_required": false,
+ "ui_hidden": false
+ },
+ "id": {
+ "description": "The id of this instance of an invocation. Must be unique among all instances of invocations.",
+ "field_kind": "node_attribute",
+ "title": "Id",
+ "type": "string"
+ },
+ "is_intermediate": {
+ "default": false,
+ "description": "Whether or not this is an intermediate invocation.",
+ "field_kind": "node_attribute",
+ "input": "direct",
+ "orig_required": true,
+ "title": "Is Intermediate",
+ "type": "boolean",
+ "ui_hidden": false,
+ "ui_type": "IsIntermediate"
+ },
+ "use_cache": {
+ "default": true,
+ "description": "Whether or not to use the cache",
+ "field_kind": "node_attribute",
+ "title": "Use Cache",
+ "type": "boolean"
+ },
+ "latents": {
+ "anyOf": [
{
- "$ref": "#/components/schemas/Sd3ModelLoaderInvocation"
+ "$ref": "#/components/schemas/LatentsField"
},
{
- "$ref": "#/components/schemas/Sd3TextEncoderInvocation"
- },
+ "type": "null"
+ }
+ ],
+ "default": null,
+ "description": "Latents tensor",
+ "field_kind": "input",
+ "input": "connection",
+ "orig_required": true
+ },
+ "vae": {
+ "anyOf": [
{
- "$ref": "#/components/schemas/SeamlessModeInvocation"
+ "$ref": "#/components/schemas/VAEField"
},
{
- "$ref": "#/components/schemas/SeedreamImageGenerationInvocation"
- },
+ "type": "null"
+ }
+ ],
+ "default": null,
+ "description": "VAE",
+ "field_kind": "input",
+ "input": "connection",
+ "orig_required": true
+ },
+ "tiled": {
+ "default": false,
+ "description": "Processing using overlapping tiles (reduce memory consumption)",
+ "field_kind": "input",
+ "input": "any",
+ "orig_default": false,
+ "orig_required": false,
+ "title": "Tiled",
+ "type": "boolean"
+ },
+ "tile_size": {
+ "default": 0,
+ "description": "The tile size for VAE tiling in pixels (image space). If set to 0, the default tile size for the model will be used. Larger tile sizes generally produce better results at the cost of higher memory usage.",
+ "field_kind": "input",
+ "input": "any",
+ "multipleOf": 8,
+ "orig_default": 0,
+ "orig_required": false,
+ "title": "Tile Size",
+ "type": "integer"
+ },
+ "fp32": {
+ "default": false,
+ "description": "Whether or not to use full float32 precision",
+ "field_kind": "input",
+ "input": "any",
+ "orig_default": false,
+ "orig_required": false,
+ "title": "Fp32",
+ "type": "boolean"
+ },
+ "type": {
+ "const": "l2i",
+ "default": "l2i",
+ "field_kind": "node_attribute",
+ "title": "type",
+ "type": "string"
+ }
+ },
+ "required": ["type", "id"],
+ "tags": ["latents", "image", "vae", "l2i"],
+ "title": "Latents to Image - SD1.5, SDXL",
+ "type": "object",
+ "version": "1.3.2",
+ "output": {
+ "$ref": "#/components/schemas/ImageOutput"
+ }
+ },
+ "LineartAnimeEdgeDetectionInvocation": {
+ "category": "controlnet_preprocessors",
+ "class": "invocation",
+ "classification": "stable",
+ "description": "Geneartes an edge map using the Lineart model.",
+ "node_pack": "invokeai",
+ "properties": {
+ "board": {
+ "anyOf": [
{
- "$ref": "#/components/schemas/SegmentAnythingInvocation"
+ "$ref": "#/components/schemas/BoardField"
},
{
- "$ref": "#/components/schemas/ShowImageInvocation"
- },
+ "type": "null"
+ }
+ ],
+ "default": null,
+ "description": "The board to save the image to",
+ "field_kind": "internal",
+ "input": "direct",
+ "orig_required": false,
+ "ui_hidden": false
+ },
+ "metadata": {
+ "anyOf": [
{
- "$ref": "#/components/schemas/SpandrelImageToImageAutoscaleInvocation"
+ "$ref": "#/components/schemas/MetadataField"
},
{
- "$ref": "#/components/schemas/SpandrelImageToImageInvocation"
- },
+ "type": "null"
+ }
+ ],
+ "default": null,
+ "description": "Optional metadata to be saved with the image",
+ "field_kind": "internal",
+ "input": "connection",
+ "orig_required": false,
+ "ui_hidden": false
+ },
+ "id": {
+ "description": "The id of this instance of an invocation. Must be unique among all instances of invocations.",
+ "field_kind": "node_attribute",
+ "title": "Id",
+ "type": "string"
+ },
+ "is_intermediate": {
+ "default": false,
+ "description": "Whether or not this is an intermediate invocation.",
+ "field_kind": "node_attribute",
+ "input": "direct",
+ "orig_required": true,
+ "title": "Is Intermediate",
+ "type": "boolean",
+ "ui_hidden": false,
+ "ui_type": "IsIntermediate"
+ },
+ "use_cache": {
+ "default": true,
+ "description": "Whether or not to use the cache",
+ "field_kind": "node_attribute",
+ "title": "Use Cache",
+ "type": "boolean"
+ },
+ "image": {
+ "anyOf": [
{
- "$ref": "#/components/schemas/StringBatchInvocation"
+ "$ref": "#/components/schemas/ImageField"
},
{
- "$ref": "#/components/schemas/StringCollectionInvocation"
- },
+ "type": "null"
+ }
+ ],
+ "default": null,
+ "description": "The image to process",
+ "field_kind": "input",
+ "input": "any",
+ "orig_required": true
+ },
+ "type": {
+ "const": "lineart_anime_edge_detection",
+ "default": "lineart_anime_edge_detection",
+ "field_kind": "node_attribute",
+ "title": "type",
+ "type": "string"
+ }
+ },
+ "required": ["type", "id"],
+ "tags": ["controlnet", "lineart"],
+ "title": "Lineart Anime Edge Detection",
+ "type": "object",
+ "version": "1.0.0",
+ "output": {
+ "$ref": "#/components/schemas/ImageOutput"
+ }
+ },
+ "LineartEdgeDetectionInvocation": {
+ "category": "controlnet_preprocessors",
+ "class": "invocation",
+ "classification": "stable",
+ "description": "Generates an edge map using the Lineart model.",
+ "node_pack": "invokeai",
+ "properties": {
+ "board": {
+ "anyOf": [
{
- "$ref": "#/components/schemas/StringGenerator"
+ "$ref": "#/components/schemas/BoardField"
},
{
- "$ref": "#/components/schemas/StringInvocation"
- },
+ "type": "null"
+ }
+ ],
+ "default": null,
+ "description": "The board to save the image to",
+ "field_kind": "internal",
+ "input": "direct",
+ "orig_required": false,
+ "ui_hidden": false
+ },
+ "metadata": {
+ "anyOf": [
{
- "$ref": "#/components/schemas/StringJoinInvocation"
+ "$ref": "#/components/schemas/MetadataField"
},
{
- "$ref": "#/components/schemas/StringJoinThreeInvocation"
- },
+ "type": "null"
+ }
+ ],
+ "default": null,
+ "description": "Optional metadata to be saved with the image",
+ "field_kind": "internal",
+ "input": "connection",
+ "orig_required": false,
+ "ui_hidden": false
+ },
+ "id": {
+ "description": "The id of this instance of an invocation. Must be unique among all instances of invocations.",
+ "field_kind": "node_attribute",
+ "title": "Id",
+ "type": "string"
+ },
+ "is_intermediate": {
+ "default": false,
+ "description": "Whether or not this is an intermediate invocation.",
+ "field_kind": "node_attribute",
+ "input": "direct",
+ "orig_required": true,
+ "title": "Is Intermediate",
+ "type": "boolean",
+ "ui_hidden": false,
+ "ui_type": "IsIntermediate"
+ },
+ "use_cache": {
+ "default": true,
+ "description": "Whether or not to use the cache",
+ "field_kind": "node_attribute",
+ "title": "Use Cache",
+ "type": "boolean"
+ },
+ "image": {
+ "anyOf": [
{
- "$ref": "#/components/schemas/StringReplaceInvocation"
+ "$ref": "#/components/schemas/ImageField"
},
{
- "$ref": "#/components/schemas/StringSplitInvocation"
- },
+ "type": "null"
+ }
+ ],
+ "default": null,
+ "description": "The image to process",
+ "field_kind": "input",
+ "input": "any",
+ "orig_required": true
+ },
+ "coarse": {
+ "default": false,
+ "description": "Whether to use coarse mode",
+ "field_kind": "input",
+ "input": "any",
+ "orig_default": false,
+ "orig_required": false,
+ "title": "Coarse",
+ "type": "boolean"
+ },
+ "type": {
+ "const": "lineart_edge_detection",
+ "default": "lineart_edge_detection",
+ "field_kind": "node_attribute",
+ "title": "type",
+ "type": "string"
+ }
+ },
+ "required": ["type", "id"],
+ "tags": ["controlnet", "lineart"],
+ "title": "Lineart Edge Detection",
+ "type": "object",
+ "version": "1.0.0",
+ "output": {
+ "$ref": "#/components/schemas/ImageOutput"
+ }
+ },
+ "LlavaOnevisionVllmInvocation": {
+ "category": "multimodal",
+ "class": "invocation",
+ "classification": "beta",
+ "description": "Run a LLaVA OneVision VLLM model.",
+ "node_pack": "invokeai",
+ "properties": {
+ "id": {
+ "description": "The id of this instance of an invocation. Must be unique among all instances of invocations.",
+ "field_kind": "node_attribute",
+ "title": "Id",
+ "type": "string"
+ },
+ "is_intermediate": {
+ "default": false,
+ "description": "Whether or not this is an intermediate invocation.",
+ "field_kind": "node_attribute",
+ "input": "direct",
+ "orig_required": true,
+ "title": "Is Intermediate",
+ "type": "boolean",
+ "ui_hidden": false,
+ "ui_type": "IsIntermediate"
+ },
+ "use_cache": {
+ "default": true,
+ "description": "Whether or not to use the cache",
+ "field_kind": "node_attribute",
+ "title": "Use Cache",
+ "type": "boolean"
+ },
+ "images": {
+ "anyOf": [
{
- "$ref": "#/components/schemas/StringSplitNegInvocation"
+ "anyOf": [
+ {
+ "items": {
+ "$ref": "#/components/schemas/ImageField"
+ },
+ "type": "array"
+ },
+ {
+ "$ref": "#/components/schemas/ImageField"
+ }
+ ],
+ "maxLength": 3
},
{
- "$ref": "#/components/schemas/SubtractInvocation"
- },
+ "type": "null"
+ }
+ ],
+ "default": null,
+ "description": "Input image.",
+ "field_kind": "input",
+ "input": "any",
+ "orig_default": null,
+ "orig_required": false,
+ "title": "Images"
+ },
+ "prompt": {
+ "default": "",
+ "description": "Input text prompt.",
+ "field_kind": "input",
+ "input": "any",
+ "orig_default": "",
+ "orig_required": false,
+ "title": "Prompt",
+ "type": "string",
+ "ui_component": "textarea"
+ },
+ "vllm_model": {
+ "anyOf": [
{
- "$ref": "#/components/schemas/T2IAdapterInvocation"
+ "$ref": "#/components/schemas/ModelIdentifierField"
},
{
- "$ref": "#/components/schemas/TextLLMInvocation"
- },
+ "type": "null"
+ }
+ ],
+ "default": null,
+ "description": "The VLLM model to use",
+ "field_kind": "input",
+ "input": "any",
+ "orig_required": true,
+ "title": "LLaVA Model Type",
+ "ui_model_type": ["llava_onevision"]
+ },
+ "type": {
+ "const": "llava_onevision_vllm",
+ "default": "llava_onevision_vllm",
+ "field_kind": "node_attribute",
+ "title": "type",
+ "type": "string"
+ }
+ },
+ "required": ["type", "id"],
+ "tags": ["vllm"],
+ "title": "LLaVA OneVision VLLM",
+ "type": "object",
+ "version": "1.0.0",
+ "output": {
+ "$ref": "#/components/schemas/StringOutput"
+ }
+ },
+ "LlavaOnevision_Diffusers_Config": {
+ "properties": {
+ "key": {
+ "type": "string",
+ "title": "Key",
+ "description": "A unique key for this model."
+ },
+ "hash": {
+ "type": "string",
+ "title": "Hash",
+ "description": "The hash of the model file(s)."
+ },
+ "path": {
+ "type": "string",
+ "title": "Path",
+ "description": "Path to the model on the filesystem. Relative paths are relative to the Invoke root directory."
+ },
+ "file_size": {
+ "type": "integer",
+ "title": "File Size",
+ "description": "The size of the model in bytes."
+ },
+ "name": {
+ "type": "string",
+ "title": "Name",
+ "description": "Name of the model."
+ },
+ "description": {
+ "anyOf": [
{
- "$ref": "#/components/schemas/TileToPropertiesInvocation"
+ "type": "string"
},
{
- "$ref": "#/components/schemas/TiledMultiDiffusionDenoiseLatents"
- },
+ "type": "null"
+ }
+ ],
+ "title": "Description",
+ "description": "Model description"
+ },
+ "source": {
+ "type": "string",
+ "title": "Source",
+ "description": "The original source of the model (path, URL or repo_id)."
+ },
+ "source_type": {
+ "$ref": "#/components/schemas/ModelSourceType",
+ "description": "The type of source"
+ },
+ "source_api_response": {
+ "anyOf": [
{
- "$ref": "#/components/schemas/UnsharpMaskInvocation"
+ "type": "string"
},
{
- "$ref": "#/components/schemas/VAELoaderInvocation"
- },
+ "type": "null"
+ }
+ ],
+ "title": "Source Api Response",
+ "description": "The original API response from the source, as stringified JSON."
+ },
+ "source_url": {
+ "anyOf": [
{
- "$ref": "#/components/schemas/WorkflowReturnGetInvocation"
+ "type": "string"
},
{
- "$ref": "#/components/schemas/WorkflowReturnInvocation"
- },
+ "type": "null"
+ }
+ ],
+ "title": "Source Url",
+ "description": "Optional URL for the model (e.g. download page or model page)."
+ },
+ "cover_image": {
+ "anyOf": [
{
- "$ref": "#/components/schemas/WorkflowReturnValueInvocation"
+ "type": "string"
},
{
- "$ref": "#/components/schemas/ZImageControlInvocation"
- },
+ "type": "null"
+ }
+ ],
+ "title": "Cover Image",
+ "description": "Url for image to preview model"
+ },
+ "format": {
+ "type": "string",
+ "const": "diffusers",
+ "title": "Format",
+ "default": "diffusers"
+ },
+ "repo_variant": {
+ "$ref": "#/components/schemas/ModelRepoVariant",
+ "default": ""
+ },
+ "type": {
+ "type": "string",
+ "const": "llava_onevision",
+ "title": "Type",
+ "default": "llava_onevision"
+ },
+ "base": {
+ "type": "string",
+ "const": "any",
+ "title": "Base",
+ "default": "any"
+ },
+ "cpu_only": {
+ "anyOf": [
{
- "$ref": "#/components/schemas/ZImageDenoiseInvocation"
+ "type": "boolean"
},
{
- "$ref": "#/components/schemas/ZImageDenoiseMetaInvocation"
- },
+ "type": "null"
+ }
+ ],
+ "title": "Cpu Only",
+ "description": "Whether this model should run on CPU only"
+ }
+ },
+ "type": "object",
+ "required": [
+ "key",
+ "hash",
+ "path",
+ "file_size",
+ "name",
+ "description",
+ "source",
+ "source_type",
+ "source_api_response",
+ "source_url",
+ "cover_image",
+ "format",
+ "repo_variant",
+ "type",
+ "base",
+ "cpu_only"
+ ],
+ "title": "LlavaOnevision_Diffusers_Config",
+ "description": "Model config for Llava Onevision models."
+ },
+ "LoRACollectionLoader": {
+ "category": "model",
+ "class": "invocation",
+ "classification": "stable",
+ "description": "Applies a collection of LoRAs to the provided UNet and CLIP models.",
+ "node_pack": "invokeai",
+ "properties": {
+ "id": {
+ "description": "The id of this instance of an invocation. Must be unique among all instances of invocations.",
+ "field_kind": "node_attribute",
+ "title": "Id",
+ "type": "string"
+ },
+ "is_intermediate": {
+ "default": false,
+ "description": "Whether or not this is an intermediate invocation.",
+ "field_kind": "node_attribute",
+ "input": "direct",
+ "orig_required": true,
+ "title": "Is Intermediate",
+ "type": "boolean",
+ "ui_hidden": false,
+ "ui_type": "IsIntermediate"
+ },
+ "use_cache": {
+ "default": true,
+ "description": "Whether or not to use the cache",
+ "field_kind": "node_attribute",
+ "title": "Use Cache",
+ "type": "boolean"
+ },
+ "loras": {
+ "anyOf": [
{
- "$ref": "#/components/schemas/ZImageImageToLatentsInvocation"
+ "$ref": "#/components/schemas/LoRAField"
},
{
- "$ref": "#/components/schemas/ZImageLatentsToImageInvocation"
+ "items": {
+ "$ref": "#/components/schemas/LoRAField"
+ },
+ "type": "array"
},
{
- "$ref": "#/components/schemas/ZImageLoRACollectionLoader"
- },
+ "type": "null"
+ }
+ ],
+ "default": null,
+ "description": "LoRA models and weights. May be a single LoRA or collection.",
+ "field_kind": "input",
+ "input": "any",
+ "orig_default": null,
+ "orig_required": false,
+ "title": "LoRAs",
+ "ui_model_base": ["sd-1", "sd-2"],
+ "ui_model_type": ["lora"]
+ },
+ "unet": {
+ "anyOf": [
{
- "$ref": "#/components/schemas/ZImageLoRALoaderInvocation"
+ "$ref": "#/components/schemas/UNetField"
},
{
- "$ref": "#/components/schemas/ZImageModelLoaderInvocation"
- },
+ "type": "null"
+ }
+ ],
+ "default": null,
+ "description": "UNet (scheduler, LoRAs)",
+ "field_kind": "input",
+ "input": "connection",
+ "orig_default": null,
+ "orig_required": false,
+ "title": "UNet"
+ },
+ "clip": {
+ "anyOf": [
{
- "$ref": "#/components/schemas/ZImageSeedVarianceEnhancerInvocation"
+ "$ref": "#/components/schemas/CLIPField"
},
{
- "$ref": "#/components/schemas/ZImageTextEncoderInvocation"
+ "type": "null"
}
],
- "title": "Invocation"
+ "default": null,
+ "description": "CLIP (tokenizer, text encoder, LoRAs) and skipped layer count",
+ "field_kind": "input",
+ "input": "connection",
+ "orig_default": null,
+ "orig_required": false,
+ "title": "CLIP"
},
- "invocation_source_id": {
- "description": "The ID of the prepared invocation's source node",
- "title": "Invocation Source Id",
+ "type": {
+ "const": "lora_collection_loader",
+ "default": "lora_collection_loader",
+ "field_kind": "node_attribute",
+ "title": "type",
"type": "string"
}
},
- "required": [
- "timestamp",
- "queue_id",
- "item_id",
- "batch_id",
- "origin",
- "destination",
- "user_id",
- "session_id",
- "invocation",
- "invocation_source_id"
- ],
- "title": "InvocationStartedEvent",
+ "required": ["type", "id"],
+ "tags": ["model"],
+ "title": "Apply LoRA Collection - SD1.5",
+ "type": "object",
+ "version": "1.1.3",
+ "output": {
+ "$ref": "#/components/schemas/LoRALoaderOutput"
+ }
+ },
+ "LoRAField": {
+ "properties": {
+ "lora": {
+ "$ref": "#/components/schemas/ModelIdentifierField",
+ "description": "Info to load lora model"
+ },
+ "weight": {
+ "description": "Weight to apply to lora model",
+ "title": "Weight",
+ "type": "number"
+ }
+ },
+ "required": ["lora", "weight"],
+ "title": "LoRAField",
"type": "object"
},
- "InvokeAIAppConfig": {
+ "LoRALoaderInvocation": {
+ "category": "model",
+ "class": "invocation",
+ "classification": "stable",
+ "description": "Apply selected lora to unet and text_encoder.",
+ "node_pack": "invokeai",
"properties": {
- "schema_version": {
- "type": "string",
- "title": "Schema Version",
- "description": "Schema version of the config file. This is not a user-configurable setting.",
- "default": "4.0.3"
+ "id": {
+ "description": "The id of this instance of an invocation. Must be unique among all instances of invocations.",
+ "field_kind": "node_attribute",
+ "title": "Id",
+ "type": "string"
},
- "legacy_models_yaml_path": {
+ "is_intermediate": {
+ "default": false,
+ "description": "Whether or not this is an intermediate invocation.",
+ "field_kind": "node_attribute",
+ "input": "direct",
+ "orig_required": true,
+ "title": "Is Intermediate",
+ "type": "boolean",
+ "ui_hidden": false,
+ "ui_type": "IsIntermediate"
+ },
+ "use_cache": {
+ "default": true,
+ "description": "Whether or not to use the cache",
+ "field_kind": "node_attribute",
+ "title": "Use Cache",
+ "type": "boolean"
+ },
+ "lora": {
"anyOf": [
{
- "type": "string",
- "format": "path"
+ "$ref": "#/components/schemas/ModelIdentifierField"
},
{
"type": "null"
}
],
- "title": "Legacy Models Yaml Path",
- "description": "Path to the legacy models.yaml file. This is not a user-configurable setting."
- },
- "host": {
- "type": "string",
- "title": "Host",
- "description": "IP address to bind to. Use `0.0.0.0` to serve to your local network.",
- "default": "127.0.0.1"
- },
- "port": {
- "type": "integer",
- "title": "Port",
- "description": "Port to bind to.",
- "default": 9090
- },
- "allow_origins": {
- "items": {
- "type": "string"
- },
- "type": "array",
- "title": "Allow Origins",
- "description": "Allowed CORS origins.",
- "default": []
- },
- "allow_credentials": {
- "type": "boolean",
- "title": "Allow Credentials",
- "description": "Allow CORS credentials.",
- "default": true
+ "default": null,
+ "description": "LoRA model to load",
+ "field_kind": "input",
+ "input": "any",
+ "orig_required": true,
+ "title": "LoRA",
+ "ui_model_base": ["sd-1"],
+ "ui_model_type": ["lora"]
},
- "allow_methods": {
- "items": {
- "type": "string"
- },
- "type": "array",
- "title": "Allow Methods",
- "description": "Methods allowed for CORS.",
- "default": ["*"]
+ "weight": {
+ "default": 0.75,
+ "description": "The weight at which the LoRA is applied to each model",
+ "field_kind": "input",
+ "input": "any",
+ "orig_default": 0.75,
+ "orig_required": false,
+ "title": "Weight",
+ "type": "number"
},
- "allow_headers": {
- "items": {
- "type": "string"
- },
- "type": "array",
- "title": "Allow Headers",
- "description": "Headers allowed for CORS.",
- "default": ["*"]
+ "unet": {
+ "anyOf": [
+ {
+ "$ref": "#/components/schemas/UNetField"
+ },
+ {
+ "type": "null"
+ }
+ ],
+ "default": null,
+ "description": "UNet (scheduler, LoRAs)",
+ "field_kind": "input",
+ "input": "connection",
+ "orig_default": null,
+ "orig_required": false,
+ "title": "UNet"
},
- "ssl_certfile": {
+ "clip": {
"anyOf": [
{
- "type": "string",
- "format": "path"
+ "$ref": "#/components/schemas/CLIPField"
},
{
"type": "null"
}
],
- "title": "Ssl Certfile",
- "description": "SSL certificate file for HTTPS. See https://www.uvicorn.dev/settings/#https."
+ "default": null,
+ "description": "CLIP (tokenizer, text encoder, LoRAs) and skipped layer count",
+ "field_kind": "input",
+ "input": "connection",
+ "orig_default": null,
+ "orig_required": false,
+ "title": "CLIP"
},
- "ssl_keyfile": {
+ "type": {
+ "const": "lora_loader",
+ "default": "lora_loader",
+ "field_kind": "node_attribute",
+ "title": "type",
+ "type": "string"
+ }
+ },
+ "required": ["type", "id"],
+ "tags": ["model"],
+ "title": "Apply LoRA - SD1.5",
+ "type": "object",
+ "version": "1.0.4",
+ "output": {
+ "$ref": "#/components/schemas/LoRALoaderOutput"
+ }
+ },
+ "LoRALoaderOutput": {
+ "class": "output",
+ "description": "Model loader output",
+ "properties": {
+ "unet": {
"anyOf": [
{
- "type": "string",
- "format": "path"
+ "$ref": "#/components/schemas/UNetField"
},
{
"type": "null"
}
],
- "title": "Ssl Keyfile",
- "description": "SSL key file for HTTPS. See https://www.uvicorn.dev/settings/#https."
+ "default": null,
+ "description": "UNet (scheduler, LoRAs)",
+ "field_kind": "output",
+ "title": "UNet",
+ "ui_hidden": false
},
- "base_url": {
+ "clip": {
"anyOf": [
{
- "type": "string"
+ "$ref": "#/components/schemas/CLIPField"
},
{
"type": "null"
}
],
- "title": "Base Url",
- "description": "Public base path when running behind a reverse proxy under a sub-path, e.g. `/invoke`. Required when the proxy PRESERVES the sub-path (the backend receives `/invoke/api/...`); optional when the proxy strips it (set it anyway so openapi/docs URLs are correct). Leave unset when serving at the domain root. Normalized to a single leading slash with no trailing slash."
+ "default": null,
+ "description": "CLIP (tokenizer, text encoder, LoRAs) and skipped layer count",
+ "field_kind": "output",
+ "title": "CLIP",
+ "ui_hidden": false
},
- "forwarded_allow_ips": {
+ "type": {
+ "const": "lora_loader_output",
+ "default": "lora_loader_output",
+ "field_kind": "node_attribute",
+ "title": "type",
+ "type": "string"
+ }
+ },
+ "required": ["output_meta", "unet", "clip", "type", "type"],
+ "title": "LoRALoaderOutput",
+ "type": "object"
+ },
+ "LoRAMetadataField": {
+ "description": "LoRA Metadata Field",
+ "properties": {
+ "model": {
+ "$ref": "#/components/schemas/ModelIdentifierField",
+ "description": "LoRA model to load"
+ },
+ "weight": {
+ "description": "The weight at which the LoRA is applied to each model",
+ "title": "Weight",
+ "type": "number"
+ }
+ },
+ "required": ["model", "weight"],
+ "title": "LoRAMetadataField",
+ "type": "object"
+ },
+ "LoRARecallParameter": {
+ "properties": {
+ "model_name": {
"type": "string",
- "title": "Forwarded Allow Ips",
- "description": "Comma-separated list of IPs (or `*`) allowed to set X-Forwarded-* headers. Set to the reverse proxy's IP. Only used when `base_url` is set.",
- "default": "127.0.0.1"
+ "title": "Model Name",
+ "description": "The name of the LoRA model"
},
- "log_tokenization": {
- "type": "boolean",
- "title": "Log Tokenization",
- "description": "Enable logging of parsed prompt tokens.",
- "default": false
+ "weight": {
+ "type": "number",
+ "maximum": 10.0,
+ "minimum": -10.0,
+ "title": "Weight",
+ "description": "The weight for the LoRA",
+ "default": 0.75
},
- "patchmatch": {
+ "is_enabled": {
"type": "boolean",
- "title": "Patchmatch",
- "description": "Enable patchmatch inpaint code.",
+ "title": "Is Enabled",
+ "description": "Whether the LoRA is enabled",
"default": true
+ }
+ },
+ "type": "object",
+ "required": ["model_name"],
+ "title": "LoRARecallParameter",
+ "description": "LoRA configuration for recall"
+ },
+ "LoRASelectorInvocation": {
+ "category": "model",
+ "class": "invocation",
+ "classification": "stable",
+ "description": "Selects a LoRA model and weight.",
+ "node_pack": "invokeai",
+ "properties": {
+ "id": {
+ "description": "The id of this instance of an invocation. Must be unique among all instances of invocations.",
+ "field_kind": "node_attribute",
+ "title": "Id",
+ "type": "string"
},
- "models_dir": {
- "type": "string",
- "format": "path",
- "title": "Models Dir",
- "description": "Path to the models directory.",
- "default": "models"
- },
- "convert_cache_dir": {
- "type": "string",
- "format": "path",
- "title": "Convert Cache Dir",
- "description": "Path to the converted models cache directory (DEPRECATED, but do not delete because it is needed for migration from previous versions).",
- "default": "models/.convert_cache"
- },
- "download_cache_dir": {
- "type": "string",
- "format": "path",
- "title": "Download Cache Dir",
- "description": "Path to the directory that contains dynamically downloaded models.",
- "default": "models/.download_cache"
- },
- "legacy_conf_dir": {
- "type": "string",
- "format": "path",
- "title": "Legacy Conf Dir",
- "description": "Path to directory of legacy checkpoint config files.",
- "default": "configs"
- },
- "db_dir": {
- "type": "string",
- "format": "path",
- "title": "Db Dir",
- "description": "Path to InvokeAI databases directory.",
- "default": "databases"
- },
- "outputs_dir": {
- "type": "string",
- "format": "path",
- "title": "Outputs Dir",
- "description": "Path to directory for outputs.",
- "default": "outputs"
- },
- "image_subfolder_strategy": {
- "type": "string",
- "enum": ["flat", "date", "type", "hash"],
- "title": "Image Subfolder Strategy",
- "description": "Strategy for organizing images into subfolders. 'flat' stores all images in a single folder. 'date' organizes by YYYY/MM/DD. 'type' organizes by image category. 'hash' uses first 2 characters of UUID for filesystem performance.",
- "default": "flat"
+ "is_intermediate": {
+ "default": false,
+ "description": "Whether or not this is an intermediate invocation.",
+ "field_kind": "node_attribute",
+ "input": "direct",
+ "orig_required": true,
+ "title": "Is Intermediate",
+ "type": "boolean",
+ "ui_hidden": false,
+ "ui_type": "IsIntermediate"
},
- "custom_nodes_dir": {
- "type": "string",
- "format": "path",
- "title": "Custom Nodes Dir",
- "description": "Path to directory for custom nodes.",
- "default": "nodes"
+ "use_cache": {
+ "default": true,
+ "description": "Whether or not to use the cache",
+ "field_kind": "node_attribute",
+ "title": "Use Cache",
+ "type": "boolean"
},
- "style_presets_dir": {
- "type": "string",
- "format": "path",
- "title": "Style Presets Dir",
- "description": "Path to directory for style presets.",
- "default": "style_presets"
+ "lora": {
+ "anyOf": [
+ {
+ "$ref": "#/components/schemas/ModelIdentifierField"
+ },
+ {
+ "type": "null"
+ }
+ ],
+ "default": null,
+ "description": "LoRA model to load",
+ "field_kind": "input",
+ "input": "any",
+ "orig_required": true,
+ "title": "LoRA",
+ "ui_model_type": ["lora"]
},
- "workflow_thumbnails_dir": {
- "type": "string",
- "format": "path",
- "title": "Workflow Thumbnails Dir",
- "description": "Path to directory for workflow thumbnails.",
- "default": "workflow_thumbnails"
+ "weight": {
+ "default": 0.75,
+ "description": "The weight at which the LoRA is applied to each model",
+ "field_kind": "input",
+ "input": "any",
+ "orig_default": 0.75,
+ "orig_required": false,
+ "title": "Weight",
+ "type": "number"
},
- "log_handlers": {
- "items": {
- "type": "string"
- },
- "type": "array",
- "title": "Log Handlers",
- "description": "Log handler. Valid options are \"console\", \"file=\", \"syslog=path|address:host:port\", \"http=\".",
- "default": ["console"]
+ "type": {
+ "const": "lora_selector",
+ "default": "lora_selector",
+ "field_kind": "node_attribute",
+ "title": "type",
+ "type": "string"
+ }
+ },
+ "required": ["type", "id"],
+ "tags": ["model"],
+ "title": "Select LoRA",
+ "type": "object",
+ "version": "1.0.3",
+ "output": {
+ "$ref": "#/components/schemas/LoRASelectorOutput"
+ }
+ },
+ "LoRASelectorOutput": {
+ "class": "output",
+ "description": "Model loader output",
+ "properties": {
+ "lora": {
+ "$ref": "#/components/schemas/LoRAField",
+ "description": "LoRA model and weight",
+ "field_kind": "output",
+ "title": "LoRA",
+ "ui_hidden": false
},
- "log_format": {
+ "type": {
+ "const": "lora_selector_output",
+ "default": "lora_selector_output",
+ "field_kind": "node_attribute",
+ "title": "type",
+ "type": "string"
+ }
+ },
+ "required": ["output_meta", "lora", "type", "type"],
+ "title": "LoRASelectorOutput",
+ "type": "object"
+ },
+ "LoRA_Diffusers_FLUX_Config": {
+ "properties": {
+ "key": {
"type": "string",
- "enum": ["plain", "color", "syslog", "legacy"],
- "title": "Log Format",
- "description": "Log format. Use \"plain\" for text-only, \"color\" for colorized output, \"legacy\" for 2.3-style logging and \"syslog\" for syslog-style.",
- "default": "color"
+ "title": "Key",
+ "description": "A unique key for this model."
},
- "log_level": {
+ "hash": {
"type": "string",
- "enum": ["debug", "info", "warning", "error", "critical"],
- "title": "Log Level",
- "description": "Emit logging messages at this level or higher.",
- "default": "info"
- },
- "log_sql": {
- "type": "boolean",
- "title": "Log Sql",
- "description": "Log SQL queries. `log_level` must be `debug` for this to do anything. Extremely verbose.",
- "default": false
+ "title": "Hash",
+ "description": "The hash of the model file(s)."
},
- "log_level_network": {
+ "path": {
"type": "string",
- "enum": ["debug", "info", "warning", "error", "critical"],
- "title": "Log Level Network",
- "description": "Log level for network-related messages. 'info' and 'debug' are very verbose.",
- "default": "warning"
- },
- "use_memory_db": {
- "type": "boolean",
- "title": "Use Memory Db",
- "description": "Use in-memory database. Useful for development.",
- "default": false
+ "title": "Path",
+ "description": "Path to the model on the filesystem. Relative paths are relative to the Invoke root directory."
},
- "dev_reload": {
- "type": "boolean",
- "title": "Dev Reload",
- "description": "Automatically reload when Python sources are changed. Does not reload node definitions.",
- "default": false
+ "file_size": {
+ "type": "integer",
+ "title": "File Size",
+ "description": "The size of the model in bytes."
},
- "profile_graphs": {
- "type": "boolean",
- "title": "Profile Graphs",
- "description": "Enable graph profiling using `cProfile`.",
- "default": false
+ "name": {
+ "type": "string",
+ "title": "Name",
+ "description": "Name of the model."
},
- "profile_prefix": {
+ "description": {
"anyOf": [
{
"type": "string"
@@ -42081,302 +47843,345 @@
"type": "null"
}
],
- "title": "Profile Prefix",
- "description": "An optional prefix for profile output files."
+ "title": "Description",
+ "description": "Model description"
},
- "profiles_dir": {
+ "source": {
"type": "string",
- "format": "path",
- "title": "Profiles Dir",
- "description": "Path to profiles output directory.",
- "default": "profiles"
+ "title": "Source",
+ "description": "The original source of the model (path, URL or repo_id)."
},
- "max_cache_ram_gb": {
+ "source_type": {
+ "$ref": "#/components/schemas/ModelSourceType",
+ "description": "The type of source"
+ },
+ "source_api_response": {
"anyOf": [
{
- "type": "number",
- "exclusiveMinimum": 0.0
+ "type": "string"
},
{
"type": "null"
}
],
- "title": "Max Cache Ram Gb",
- "description": "The maximum amount of CPU RAM to use for model caching in GB. If unset, the limit will be configured based on the available RAM. In most cases, it is recommended to leave this unset."
+ "title": "Source Api Response",
+ "description": "The original API response from the source, as stringified JSON."
},
- "max_cache_vram_gb": {
+ "source_url": {
"anyOf": [
{
- "type": "number",
- "minimum": 0.0
+ "type": "string"
},
{
"type": "null"
}
],
- "title": "Max Cache Vram Gb",
- "description": "The amount of VRAM to use for model caching in GB. If unset, the limit will be configured based on the available VRAM and the device_working_mem_gb. In most cases, it is recommended to leave this unset."
- },
- "log_memory_usage": {
- "type": "boolean",
- "title": "Log Memory Usage",
- "description": "If True, a memory snapshot will be captured before and after every model cache operation, and the result will be logged (at debug level). There is a time cost to capturing the memory snapshots, so it is recommended to only enable this feature if you are actively inspecting the model cache's behaviour.",
- "default": false
- },
- "model_cache_keep_alive_min": {
- "type": "number",
- "minimum": 0.0,
- "title": "Model Cache Keep Alive Min",
- "description": "How long to keep models in cache after last use, in minutes. A value of 0 (the default) means models are kept in cache indefinitely. If no model generations occur within the timeout period, the model cache is cleared using the same logic as the 'Clear Model Cache' button.",
- "default": 0
- },
- "device_working_mem_gb": {
- "type": "number",
- "title": "Device Working Mem Gb",
- "description": "The amount of working memory to keep available on the compute device (in GB). Has no effect if running on CPU. If you are experiencing OOM errors, try increasing this value.",
- "default": 3
- },
- "enable_partial_loading": {
- "type": "boolean",
- "title": "Enable Partial Loading",
- "description": "Enable partial loading of models. This enables models to run with reduced VRAM requirements (at the cost of slower speed) by streaming the model from RAM to VRAM as its used. In some edge cases, partial loading can cause models to run more slowly if they were previously being fully loaded into VRAM.",
- "default": true
- },
- "keep_ram_copy_of_weights": {
- "type": "boolean",
- "title": "Keep Ram Copy Of Weights",
- "description": "Whether to keep a full RAM copy of a model's weights when the model is loaded in VRAM. Keeping a RAM copy increases average RAM usage, but speeds up model switching and LoRA patching (assuming there is sufficient RAM). Set this to False if RAM pressure is consistently high.",
- "default": true
+ "title": "Source Url",
+ "description": "Optional URL for the model (e.g. download page or model page)."
},
- "ram": {
+ "cover_image": {
"anyOf": [
{
- "type": "number",
- "exclusiveMinimum": 0.0
+ "type": "string"
},
{
"type": "null"
}
],
- "title": "Ram",
- "description": "DEPRECATED: This setting is no longer used. It has been replaced by `max_cache_ram_gb`, but most users will not need to use this config since automatic cache size limits should work well in most cases. This config setting will be removed once the new model cache behavior is stable."
+ "title": "Cover Image",
+ "description": "Url for image to preview model"
},
- "vram": {
+ "type": {
+ "type": "string",
+ "const": "lora",
+ "title": "Type",
+ "default": "lora"
+ },
+ "trigger_phrases": {
"anyOf": [
{
- "type": "number",
- "minimum": 0.0
+ "items": {
+ "type": "string"
+ },
+ "type": "array",
+ "uniqueItems": true
},
{
"type": "null"
}
],
- "title": "Vram",
- "description": "DEPRECATED: This setting is no longer used. It has been replaced by `max_cache_vram_gb`, but most users will not need to use this config since automatic cache size limits should work well in most cases. This config setting will be removed once the new model cache behavior is stable."
- },
- "lazy_offload": {
- "type": "boolean",
- "title": "Lazy Offload",
- "description": "DEPRECATED: This setting is no longer used. Lazy-offloading is enabled by default. This config setting will be removed once the new model cache behavior is stable.",
- "default": true
+ "title": "Trigger Phrases",
+ "description": "Set of trigger phrases for this model"
},
- "pytorch_cuda_alloc_conf": {
+ "default_settings": {
"anyOf": [
{
- "type": "string"
+ "$ref": "#/components/schemas/LoraModelDefaultSettings"
},
{
"type": "null"
}
],
- "title": "Pytorch Cuda Alloc Conf",
- "description": "Configure the Torch CUDA memory allocator. This will impact peak reserved VRAM usage and performance. Setting to \"backend:cudaMallocAsync\" works well on many systems. The optimal configuration is highly dependent on the system configuration (device type, VRAM, CUDA driver version, etc.), so must be tuned experimentally."
+ "description": "Default settings for this model"
},
- "device": {
+ "format": {
"type": "string",
- "pattern": "^(auto|cpu|mps|cuda(:\\d+)?)$",
- "title": "Device",
- "description": "Preferred execution device. `auto` will choose the device depending on the hardware platform and the installed torch capabilities. Valid values: `auto`, `cpu`, `cuda`, `mps`, `cuda:N` (where N is a device number)",
- "default": "auto"
+ "const": "diffusers",
+ "title": "Format",
+ "default": "diffusers"
},
- "precision": {
+ "base": {
"type": "string",
- "enum": ["auto", "float16", "bfloat16", "float32"],
- "title": "Precision",
- "description": "Floating point precision. `float16` will consume half the memory of `float32` but produce slightly lower-quality images. The `auto` setting will guess the proper precision based on your video card and operating system.",
- "default": "auto"
- },
- "sequential_guidance": {
- "type": "boolean",
- "title": "Sequential Guidance",
- "description": "Whether to calculate guidance in serial instead of in parallel, lowering memory requirements.",
- "default": false
- },
- "attention_type": {
+ "const": "flux",
+ "title": "Base",
+ "default": "flux"
+ }
+ },
+ "type": "object",
+ "required": [
+ "key",
+ "hash",
+ "path",
+ "file_size",
+ "name",
+ "description",
+ "source",
+ "source_type",
+ "source_api_response",
+ "source_url",
+ "cover_image",
+ "type",
+ "trigger_phrases",
+ "default_settings",
+ "format",
+ "base"
+ ],
+ "title": "LoRA_Diffusers_FLUX_Config"
+ },
+ "LoRA_Diffusers_Flux2_Config": {
+ "properties": {
+ "key": {
"type": "string",
- "enum": ["auto", "normal", "xformers", "sliced", "torch-sdp"],
- "title": "Attention Type",
- "description": "Attention type.",
- "default": "auto"
+ "title": "Key",
+ "description": "A unique key for this model."
},
- "attention_slice_size": {
- "enum": ["auto", "balanced", "max", 1, 2, 3, 4, 5, 6, 7, 8],
- "title": "Attention Slice Size",
- "description": "Slice size, valid when attention_type==\"sliced\".",
- "default": "auto"
+ "hash": {
+ "type": "string",
+ "title": "Hash",
+ "description": "The hash of the model file(s)."
},
- "force_tiled_decode": {
- "type": "boolean",
- "title": "Force Tiled Decode",
- "description": "Whether to enable tiled VAE decode (reduces memory consumption with some performance penalty).",
- "default": false
+ "path": {
+ "type": "string",
+ "title": "Path",
+ "description": "Path to the model on the filesystem. Relative paths are relative to the Invoke root directory."
},
- "pil_compress_level": {
+ "file_size": {
"type": "integer",
- "title": "Pil Compress Level",
- "description": "The compress_level setting of PIL.Image.save(), used for PNG encoding. All settings are lossless. 0 = no compression, 1 = fastest with slightly larger filesize, 9 = slowest with smallest filesize. 1 is typically the best setting.",
- "default": 1
+ "title": "File Size",
+ "description": "The size of the model in bytes."
},
- "max_queue_size": {
- "type": "integer",
- "exclusiveMinimum": 0.0,
- "title": "Max Queue Size",
- "description": "Maximum number of items in the session queue.",
- "default": 10000
+ "name": {
+ "type": "string",
+ "title": "Name",
+ "description": "Name of the model."
},
- "session_queue_mode": {
+ "description": {
+ "anyOf": [
+ {
+ "type": "string"
+ },
+ {
+ "type": "null"
+ }
+ ],
+ "title": "Description",
+ "description": "Model description"
+ },
+ "source": {
"type": "string",
- "enum": ["FIFO", "round_robin"],
- "title": "Session Queue Mode",
- "description": "Session queue mode. Use 'FIFO' for traditional first-in-first-out, or 'round_robin' to serve each user's jobs in turn. In single-user mode, FIFO is always used regardless of this setting.",
- "default": "round_robin"
+ "title": "Source",
+ "description": "The original source of the model (path, URL or repo_id)."
},
- "clear_queue_on_startup": {
- "type": "boolean",
- "title": "Clear Queue On Startup",
- "description": "Empties session queue on startup. If true, disables `max_queue_history`.",
- "default": false
+ "source_type": {
+ "$ref": "#/components/schemas/ModelSourceType",
+ "description": "The type of source"
},
- "max_queue_history": {
+ "source_api_response": {
"anyOf": [
{
- "type": "integer",
- "minimum": 0.0
+ "type": "string"
},
{
"type": "null"
}
],
- "title": "Max Queue History",
- "description": "Keep the last N completed, failed, and canceled queue items. Older items are deleted on startup. Set to 0 to prune all terminal items. Ignored if `clear_queue_on_startup` is true."
+ "title": "Source Api Response",
+ "description": "The original API response from the source, as stringified JSON."
},
- "allow_nodes": {
+ "source_url": {
"anyOf": [
{
- "items": {
- "type": "string"
- },
- "type": "array"
+ "type": "string"
},
{
"type": "null"
}
],
- "title": "Allow Nodes",
- "description": "List of nodes to allow. Omit to allow all."
+ "title": "Source Url",
+ "description": "Optional URL for the model (e.g. download page or model page)."
},
- "deny_nodes": {
+ "cover_image": {
+ "anyOf": [
+ {
+ "type": "string"
+ },
+ {
+ "type": "null"
+ }
+ ],
+ "title": "Cover Image",
+ "description": "Url for image to preview model"
+ },
+ "type": {
+ "type": "string",
+ "const": "lora",
+ "title": "Type",
+ "default": "lora"
+ },
+ "trigger_phrases": {
"anyOf": [
{
"items": {
"type": "string"
},
- "type": "array"
+ "type": "array",
+ "uniqueItems": true
},
{
"type": "null"
}
],
- "title": "Deny Nodes",
- "description": "List of nodes to deny. Omit to deny none."
+ "title": "Trigger Phrases",
+ "description": "Set of trigger phrases for this model"
},
- "node_cache_size": {
- "type": "integer",
- "title": "Node Cache Size",
- "description": "How many cached nodes to keep in memory.",
- "default": 512
+ "default_settings": {
+ "anyOf": [
+ {
+ "$ref": "#/components/schemas/LoraModelDefaultSettings"
+ },
+ {
+ "type": "null"
+ }
+ ],
+ "description": "Default settings for this model"
},
- "hashing_algorithm": {
+ "format": {
"type": "string",
- "enum": [
- "blake3_multi",
- "blake3_single",
- "random",
- "md5",
- "sha1",
- "sha224",
- "sha256",
- "sha384",
- "sha512",
- "blake2b",
- "blake2s",
- "sha3_224",
- "sha3_256",
- "sha3_384",
- "sha3_512",
- "shake_128",
- "shake_256"
- ],
- "title": "Hashing Algorithm",
- "description": "Model hashing algorthim for model installs. 'blake3_multi' is best for SSDs. 'blake3_single' is best for spinning disk HDDs. 'random' disables hashing, instead assigning a UUID to models. Useful when using a memory db to reduce model installation time, or if you don't care about storing stable hashes for models. Alternatively, any other hashlib algorithm is accepted, though these are not nearly as performant as blake3.",
- "default": "blake3_single"
+ "const": "diffusers",
+ "title": "Format",
+ "default": "diffusers"
},
- "remote_api_tokens": {
+ "base": {
+ "type": "string",
+ "const": "flux2",
+ "title": "Base",
+ "default": "flux2"
+ },
+ "variant": {
"anyOf": [
{
- "items": {
- "$ref": "#/components/schemas/URLRegexTokenPair"
- },
- "type": "array"
+ "$ref": "#/components/schemas/Flux2VariantType"
},
{
"type": "null"
}
- ],
- "title": "Remote Api Tokens",
- "description": "List of regular expression and token pairs used when downloading models from URLs. The download URL is tested against the regex, and if it matches, the token is provided in as a Bearer token."
+ ]
+ }
+ },
+ "type": "object",
+ "required": [
+ "key",
+ "hash",
+ "path",
+ "file_size",
+ "name",
+ "description",
+ "source",
+ "source_type",
+ "source_api_response",
+ "source_url",
+ "cover_image",
+ "type",
+ "trigger_phrases",
+ "default_settings",
+ "format",
+ "base",
+ "variant"
+ ],
+ "title": "LoRA_Diffusers_Flux2_Config",
+ "description": "Model config for FLUX.2 (Klein) LoRA models in Diffusers format."
+ },
+ "LoRA_Diffusers_SD1_Config": {
+ "properties": {
+ "key": {
+ "type": "string",
+ "title": "Key",
+ "description": "A unique key for this model."
},
- "scan_models_on_startup": {
- "type": "boolean",
- "title": "Scan Models On Startup",
- "description": "Scan the models directory on startup, registering orphaned models. This is typically only used in conjunction with `use_memory_db` for testing purposes.",
- "default": false
+ "hash": {
+ "type": "string",
+ "title": "Hash",
+ "description": "The hash of the model file(s)."
},
- "unsafe_disable_picklescan": {
- "type": "boolean",
- "title": "Unsafe Disable Picklescan",
- "description": "UNSAFE. Disable the picklescan security check during model installation. Recommended only for development and testing purposes. This will allow arbitrary code execution during model installation, so should never be used in production.",
- "default": false
+ "path": {
+ "type": "string",
+ "title": "Path",
+ "description": "Path to the model on the filesystem. Relative paths are relative to the Invoke root directory."
},
- "allow_unknown_models": {
- "type": "boolean",
- "title": "Allow Unknown Models",
- "description": "Allow installation of models that we are unable to identify. If enabled, models will be marked as `unknown` in the database, and will not have any metadata associated with them. If disabled, unknown models will be rejected during installation.",
- "default": true
+ "file_size": {
+ "type": "integer",
+ "title": "File Size",
+ "description": "The size of the model in bytes."
},
- "multiuser": {
- "type": "boolean",
- "title": "Multiuser",
- "description": "Enable multiuser support. When disabled, the application runs in single-user mode using a default system account with administrator privileges. When enabled, requires user authentication and authorization.",
- "default": false
+ "name": {
+ "type": "string",
+ "title": "Name",
+ "description": "Name of the model."
},
- "strict_password_checking": {
- "type": "boolean",
- "title": "Strict Password Checking",
- "description": "Enforce strict password requirements. When True, passwords must contain uppercase, lowercase, and numbers. When False (default), any password is accepted but its strength (weak/moderate/strong) is reported to the user.",
- "default": false
+ "description": {
+ "anyOf": [
+ {
+ "type": "string"
+ },
+ {
+ "type": "null"
+ }
+ ],
+ "title": "Description",
+ "description": "Model description"
+ },
+ "source": {
+ "type": "string",
+ "title": "Source",
+ "description": "The original source of the model (path, URL or repo_id)."
+ },
+ "source_type": {
+ "$ref": "#/components/schemas/ModelSourceType",
+ "description": "The type of source"
+ },
+ "source_api_response": {
+ "anyOf": [
+ {
+ "type": "string"
+ },
+ {
+ "type": "null"
+ }
+ ],
+ "title": "Source Api Response",
+ "description": "The original API response from the source, as stringified JSON."
},
- "external_alibabacloud_api_key": {
+ "source_url": {
"anyOf": [
{
"type": "string"
@@ -42385,10 +48190,10 @@
"type": "null"
}
],
- "title": "External Alibabacloud Api Key",
- "description": "API key for Alibaba Cloud DashScope image generation."
+ "title": "Source Url",
+ "description": "Optional URL for the model (e.g. download page or model page)."
},
- "external_alibabacloud_base_url": {
+ "cover_image": {
"anyOf": [
{
"type": "string"
@@ -42397,34 +48202,104 @@
"type": "null"
}
],
- "title": "External Alibabacloud Base Url",
- "description": "Base URL override for Alibaba Cloud DashScope image generation."
+ "title": "Cover Image",
+ "description": "Url for image to preview model"
},
- "external_gemini_api_key": {
+ "type": {
+ "type": "string",
+ "const": "lora",
+ "title": "Type",
+ "default": "lora"
+ },
+ "trigger_phrases": {
"anyOf": [
{
- "type": "string"
+ "items": {
+ "type": "string"
+ },
+ "type": "array",
+ "uniqueItems": true
},
{
"type": "null"
}
],
- "title": "External Gemini Api Key",
- "description": "API key for Gemini image generation."
+ "title": "Trigger Phrases",
+ "description": "Set of trigger phrases for this model"
},
- "external_openai_api_key": {
+ "default_settings": {
"anyOf": [
{
- "type": "string"
+ "$ref": "#/components/schemas/LoraModelDefaultSettings"
},
{
"type": "null"
}
],
- "title": "External Openai Api Key",
- "description": "API key for OpenAI image generation."
+ "description": "Default settings for this model"
},
- "external_gemini_base_url": {
+ "format": {
+ "type": "string",
+ "const": "diffusers",
+ "title": "Format",
+ "default": "diffusers"
+ },
+ "base": {
+ "type": "string",
+ "const": "sd-1",
+ "title": "Base",
+ "default": "sd-1"
+ }
+ },
+ "type": "object",
+ "required": [
+ "key",
+ "hash",
+ "path",
+ "file_size",
+ "name",
+ "description",
+ "source",
+ "source_type",
+ "source_api_response",
+ "source_url",
+ "cover_image",
+ "type",
+ "trigger_phrases",
+ "default_settings",
+ "format",
+ "base"
+ ],
+ "title": "LoRA_Diffusers_SD1_Config"
+ },
+ "LoRA_Diffusers_SD2_Config": {
+ "properties": {
+ "key": {
+ "type": "string",
+ "title": "Key",
+ "description": "A unique key for this model."
+ },
+ "hash": {
+ "type": "string",
+ "title": "Hash",
+ "description": "The hash of the model file(s)."
+ },
+ "path": {
+ "type": "string",
+ "title": "Path",
+ "description": "Path to the model on the filesystem. Relative paths are relative to the Invoke root directory."
+ },
+ "file_size": {
+ "type": "integer",
+ "title": "File Size",
+ "description": "The size of the model in bytes."
+ },
+ "name": {
+ "type": "string",
+ "title": "Name",
+ "description": "Name of the model."
+ },
+ "description": {
"anyOf": [
{
"type": "string"
@@ -42433,10 +48308,19 @@
"type": "null"
}
],
- "title": "External Gemini Base Url",
- "description": "Base URL override for Gemini image generation."
+ "title": "Description",
+ "description": "Model description"
},
- "external_openai_base_url": {
+ "source": {
+ "type": "string",
+ "title": "Source",
+ "description": "The original source of the model (path, URL or repo_id)."
+ },
+ "source_type": {
+ "$ref": "#/components/schemas/ModelSourceType",
+ "description": "The type of source"
+ },
+ "source_api_response": {
"anyOf": [
{
"type": "string"
@@ -42445,10 +48329,10 @@
"type": "null"
}
],
- "title": "External Openai Base Url",
- "description": "Base URL override for OpenAI image generation."
+ "title": "Source Api Response",
+ "description": "The original API response from the source, as stringified JSON."
},
- "external_seedream_api_key": {
+ "source_url": {
"anyOf": [
{
"type": "string"
@@ -42457,10 +48341,10 @@
"type": "null"
}
],
- "title": "External Seedream Api Key",
- "description": "API key for Seedream image generation."
+ "title": "Source Url",
+ "description": "Optional URL for the model (e.g. download page or model page)."
},
- "external_seedream_base_url": {
+ "cover_image": {
"anyOf": [
{
"type": "string"
@@ -42469,2035 +48353,1311 @@
"type": "null"
}
],
- "title": "External Seedream Base Url",
- "description": "Base URL override for Seedream image generation."
- }
- },
- "additionalProperties": false,
- "type": "object",
- "title": "InvokeAIAppConfig",
- "description": "Invoke's global app configuration.\n\nTypically, you won't need to interact with this class directly. Instead, use the `get_config` function from `invokeai.app.services.config` to get a singleton config object.\n\nAttributes:\n host: IP address to bind to. Use `0.0.0.0` to serve to your local network.\n port: Port to bind to.\n allow_origins: Allowed CORS origins.\n allow_credentials: Allow CORS credentials.\n allow_methods: Methods allowed for CORS.\n allow_headers: Headers allowed for CORS.\n ssl_certfile: SSL certificate file for HTTPS. See https://www.uvicorn.dev/settings/#https.\n ssl_keyfile: SSL key file for HTTPS. See https://www.uvicorn.dev/settings/#https.\n log_tokenization: Enable logging of parsed prompt tokens.\n patchmatch: Enable patchmatch inpaint code.\n models_dir: Path to the models directory.\n convert_cache_dir: Path to the converted models cache directory (DEPRECATED, but do not delete because it is needed for migration from previous versions).\n download_cache_dir: Path to the directory that contains dynamically downloaded models.\n legacy_conf_dir: Path to directory of legacy checkpoint config files.\n db_dir: Path to InvokeAI databases directory.\n outputs_dir: Path to directory for outputs.\n image_subfolder_strategy: Strategy for organizing images into subfolders. 'flat' stores all images in a single folder. 'date' organizes by YYYY/MM/DD. 'type' organizes by image category. 'hash' uses first 2 characters of UUID for filesystem performance. Valid values: `flat`, `date`, `type`, `hash`\n custom_nodes_dir: Path to directory for custom nodes.\n style_presets_dir: Path to directory for style presets.\n workflow_thumbnails_dir: Path to directory for workflow thumbnails.\n log_handlers: Log handler. Valid options are \"console\", \"file=\", \"syslog=path|address:host:port\", \"http=\".\n log_format: Log format. Use \"plain\" for text-only, \"color\" for colorized output, \"legacy\" for 2.3-style logging and \"syslog\" for syslog-style. Valid values: `plain`, `color`, `syslog`, `legacy`\n log_level: Emit logging messages at this level or higher. Valid values: `debug`, `info`, `warning`, `error`, `critical`\n log_sql: Log SQL queries. `log_level` must be `debug` for this to do anything. Extremely verbose.\n log_level_network: Log level for network-related messages. 'info' and 'debug' are very verbose. Valid values: `debug`, `info`, `warning`, `error`, `critical`\n use_memory_db: Use in-memory database. Useful for development.\n dev_reload: Automatically reload when Python sources are changed. Does not reload node definitions.\n profile_graphs: Enable graph profiling using `cProfile`.\n profile_prefix: An optional prefix for profile output files.\n profiles_dir: Path to profiles output directory.\n max_cache_ram_gb: The maximum amount of CPU RAM to use for model caching in GB. If unset, the limit will be configured based on the available RAM. In most cases, it is recommended to leave this unset.\n max_cache_vram_gb: The amount of VRAM to use for model caching in GB. If unset, the limit will be configured based on the available VRAM and the device_working_mem_gb. In most cases, it is recommended to leave this unset.\n log_memory_usage: If True, a memory snapshot will be captured before and after every model cache operation, and the result will be logged (at debug level). There is a time cost to capturing the memory snapshots, so it is recommended to only enable this feature if you are actively inspecting the model cache's behaviour.\n model_cache_keep_alive_min: How long to keep models in cache after last use, in minutes. A value of 0 (the default) means models are kept in cache indefinitely. If no model generations occur within the timeout period, the model cache is cleared using the same logic as the 'Clear Model Cache' button.\n device_working_mem_gb: The amount of working memory to keep available on the compute device (in GB). Has no effect if running on CPU. If you are experiencing OOM errors, try increasing this value.\n enable_partial_loading: Enable partial loading of models. This enables models to run with reduced VRAM requirements (at the cost of slower speed) by streaming the model from RAM to VRAM as its used. In some edge cases, partial loading can cause models to run more slowly if they were previously being fully loaded into VRAM.\n keep_ram_copy_of_weights: Whether to keep a full RAM copy of a model's weights when the model is loaded in VRAM. Keeping a RAM copy increases average RAM usage, but speeds up model switching and LoRA patching (assuming there is sufficient RAM). Set this to False if RAM pressure is consistently high.\n ram: DEPRECATED: This setting is no longer used. It has been replaced by `max_cache_ram_gb`, but most users will not need to use this config since automatic cache size limits should work well in most cases. This config setting will be removed once the new model cache behavior is stable.\n vram: DEPRECATED: This setting is no longer used. It has been replaced by `max_cache_vram_gb`, but most users will not need to use this config since automatic cache size limits should work well in most cases. This config setting will be removed once the new model cache behavior is stable.\n lazy_offload: DEPRECATED: This setting is no longer used. Lazy-offloading is enabled by default. This config setting will be removed once the new model cache behavior is stable.\n pytorch_cuda_alloc_conf: Configure the Torch CUDA memory allocator. This will impact peak reserved VRAM usage and performance. Setting to \"backend:cudaMallocAsync\" works well on many systems. The optimal configuration is highly dependent on the system configuration (device type, VRAM, CUDA driver version, etc.), so must be tuned experimentally.\n device: Preferred execution device. `auto` will choose the device depending on the hardware platform and the installed torch capabilities. Valid values: `auto`, `cpu`, `cuda`, `mps`, `cuda:N` (where N is a device number)\n precision: Floating point precision. `float16` will consume half the memory of `float32` but produce slightly lower-quality images. The `auto` setting will guess the proper precision based on your video card and operating system. Valid values: `auto`, `float16`, `bfloat16`, `float32`\n sequential_guidance: Whether to calculate guidance in serial instead of in parallel, lowering memory requirements.\n attention_type: Attention type. Valid values: `auto`, `normal`, `xformers`, `sliced`, `torch-sdp`\n attention_slice_size: Slice size, valid when attention_type==\"sliced\". Valid values: `auto`, `balanced`, `max`, `1`, `2`, `3`, `4`, `5`, `6`, `7`, `8`\n force_tiled_decode: Whether to enable tiled VAE decode (reduces memory consumption with some performance penalty).\n pil_compress_level: The compress_level setting of PIL.Image.save(), used for PNG encoding. All settings are lossless. 0 = no compression, 1 = fastest with slightly larger filesize, 9 = slowest with smallest filesize. 1 is typically the best setting.\n max_queue_size: Maximum number of items in the session queue.\n session_queue_mode: Session queue mode. Use 'FIFO' for traditional first-in-first-out, or 'round_robin' to serve each user's jobs in turn. In single-user mode, FIFO is always used regardless of this setting. Valid values: `FIFO`, `round_robin`\n clear_queue_on_startup: Empties session queue on startup. If true, disables `max_queue_history`.\n max_queue_history: Keep the last N completed, failed, and canceled queue items. Older items are deleted on startup. Set to 0 to prune all terminal items. Ignored if `clear_queue_on_startup` is true.\n allow_nodes: List of nodes to allow. Omit to allow all.\n deny_nodes: List of nodes to deny. Omit to deny none.\n node_cache_size: How many cached nodes to keep in memory.\n hashing_algorithm: Model hashing algorthim for model installs. 'blake3_multi' is best for SSDs. 'blake3_single' is best for spinning disk HDDs. 'random' disables hashing, instead assigning a UUID to models. Useful when using a memory db to reduce model installation time, or if you don't care about storing stable hashes for models. Alternatively, any other hashlib algorithm is accepted, though these are not nearly as performant as blake3. Valid values: `blake3_multi`, `blake3_single`, `random`, `md5`, `sha1`, `sha224`, `sha256`, `sha384`, `sha512`, `blake2b`, `blake2s`, `sha3_224`, `sha3_256`, `sha3_384`, `sha3_512`, `shake_128`, `shake_256`\n remote_api_tokens: List of regular expression and token pairs used when downloading models from URLs. The download URL is tested against the regex, and if it matches, the token is provided in as a Bearer token.\n scan_models_on_startup: Scan the models directory on startup, registering orphaned models. This is typically only used in conjunction with `use_memory_db` for testing purposes.\n unsafe_disable_picklescan: UNSAFE. Disable the picklescan security check during model installation. Recommended only for development and testing purposes. This will allow arbitrary code execution during model installation, so should never be used in production.\n allow_unknown_models: Allow installation of models that we are unable to identify. If enabled, models will be marked as `unknown` in the database, and will not have any metadata associated with them. If disabled, unknown models will be rejected during installation.\n multiuser: Enable multiuser support. When disabled, the application runs in single-user mode using a default system account with administrator privileges. When enabled, requires user authentication and authorization.\n strict_password_checking: Enforce strict password requirements. When True, passwords must contain uppercase, lowercase, and numbers. When False (default), any password is accepted but its strength (weak/moderate/strong) is reported to the user.\n external_alibabacloud_api_key: API key for Alibaba Cloud DashScope image generation.\n external_alibabacloud_base_url: Base URL override for Alibaba Cloud DashScope image generation.\n external_gemini_api_key: API key for Gemini image generation.\n external_openai_api_key: API key for OpenAI image generation.\n external_gemini_base_url: Base URL override for Gemini image generation.\n external_openai_base_url: Base URL override for OpenAI image generation.\n external_seedream_api_key: API key for Seedream image generation.\n external_seedream_base_url: Base URL override for Seedream image generation.\n base_url: Public base path when running behind a reverse proxy under a sub-path, e.g. `/invoke`. Set only when the proxy PRESERVES the sub-path (the backend receives `/invoke/api/...`). Leave unset when the proxy strips the sub-path or when serving at the domain root.\n forwarded_allow_ips: Comma-separated list of IPs (or `*`) allowed to set X-Forwarded-* headers. Set to the reverse proxy's IP. Only used when `base_url` is set."
- },
- "InvokeAIAppConfigWithSetFields": {
- "properties": {
- "set_fields": {
- "items": {
- "type": "string"
- },
- "type": "array",
- "uniqueItems": true,
- "title": "Set Fields",
- "description": "The set fields"
+ "title": "Cover Image",
+ "description": "Url for image to preview model"
},
- "config": {
- "$ref": "#/components/schemas/InvokeAIAppConfig",
- "description": "The InvokeAI App Config"
- }
- },
- "type": "object",
- "required": ["set_fields", "config"],
- "title": "InvokeAIAppConfigWithSetFields",
- "description": "InvokeAI App Config with model fields set"
- },
- "InvokeAdjustImageHuePlusInvocation": {
- "category": "image",
- "class": "invocation",
- "classification": "stable",
- "description": "Adjusts the Hue of an image by rotating it in the selected color space. Originally created by @dwringer",
- "node_pack": "invokeai",
- "properties": {
- "board": {
+ "type": {
+ "type": "string",
+ "const": "lora",
+ "title": "Type",
+ "default": "lora"
+ },
+ "trigger_phrases": {
"anyOf": [
{
- "$ref": "#/components/schemas/BoardField"
+ "items": {
+ "type": "string"
+ },
+ "type": "array",
+ "uniqueItems": true
},
{
"type": "null"
}
],
- "default": null,
- "description": "The board to save the image to",
- "field_kind": "internal",
- "input": "direct",
- "orig_required": false,
- "ui_hidden": false
+ "title": "Trigger Phrases",
+ "description": "Set of trigger phrases for this model"
},
- "metadata": {
+ "default_settings": {
"anyOf": [
{
- "$ref": "#/components/schemas/MetadataField"
+ "$ref": "#/components/schemas/LoraModelDefaultSettings"
},
{
"type": "null"
}
],
- "default": null,
- "description": "Optional metadata to be saved with the image",
- "field_kind": "internal",
- "input": "connection",
- "orig_required": false,
- "ui_hidden": false
+ "description": "Default settings for this model"
},
- "id": {
- "description": "The id of this instance of an invocation. Must be unique among all instances of invocations.",
- "field_kind": "node_attribute",
- "title": "Id",
- "type": "string"
+ "format": {
+ "type": "string",
+ "const": "diffusers",
+ "title": "Format",
+ "default": "diffusers"
},
- "is_intermediate": {
- "default": false,
- "description": "Whether or not this is an intermediate invocation.",
- "field_kind": "node_attribute",
- "input": "direct",
- "orig_required": true,
- "title": "Is Intermediate",
- "type": "boolean",
- "ui_hidden": false,
- "ui_type": "IsIntermediate"
+ "base": {
+ "type": "string",
+ "const": "sd-2",
+ "title": "Base",
+ "default": "sd-2"
+ }
+ },
+ "type": "object",
+ "required": [
+ "key",
+ "hash",
+ "path",
+ "file_size",
+ "name",
+ "description",
+ "source",
+ "source_type",
+ "source_api_response",
+ "source_url",
+ "cover_image",
+ "type",
+ "trigger_phrases",
+ "default_settings",
+ "format",
+ "base"
+ ],
+ "title": "LoRA_Diffusers_SD2_Config"
+ },
+ "LoRA_Diffusers_SDXL_Config": {
+ "properties": {
+ "key": {
+ "type": "string",
+ "title": "Key",
+ "description": "A unique key for this model."
},
- "use_cache": {
- "default": true,
- "description": "Whether or not to use the cache",
- "field_kind": "node_attribute",
- "title": "Use Cache",
- "type": "boolean"
+ "hash": {
+ "type": "string",
+ "title": "Hash",
+ "description": "The hash of the model file(s)."
},
- "image": {
+ "path": {
+ "type": "string",
+ "title": "Path",
+ "description": "Path to the model on the filesystem. Relative paths are relative to the Invoke root directory."
+ },
+ "file_size": {
+ "type": "integer",
+ "title": "File Size",
+ "description": "The size of the model in bytes."
+ },
+ "name": {
+ "type": "string",
+ "title": "Name",
+ "description": "Name of the model."
+ },
+ "description": {
"anyOf": [
{
- "$ref": "#/components/schemas/ImageField"
+ "type": "string"
},
{
"type": "null"
}
],
- "default": null,
- "description": "The image to adjust",
- "field_kind": "input",
- "input": "any",
- "orig_required": true
- },
- "space": {
- "default": "HSV / HSL / RGB",
- "description": "Color space in which to rotate hue by polar coords (*: non-invertible)",
- "enum": [
- "HSV / HSL / RGB",
- "Okhsl",
- "Okhsv",
- "*Oklch / Oklab",
- "*LCh / CIELab",
- "*UPLab (w/CIELab_to_UPLab.icc)"
- ],
- "field_kind": "input",
- "input": "any",
- "orig_default": "HSV / HSL / RGB",
- "orig_required": false,
- "title": "Space",
- "type": "string"
- },
- "degrees": {
- "default": 0.0,
- "description": "Degrees by which to rotate image hue",
- "field_kind": "input",
- "input": "any",
- "orig_default": 0.0,
- "orig_required": false,
- "title": "Degrees",
- "type": "number"
- },
- "preserve_lightness": {
- "default": false,
- "description": "Whether to preserve CIELAB lightness values",
- "field_kind": "input",
- "input": "any",
- "orig_default": false,
- "orig_required": false,
- "title": "Preserve Lightness",
- "type": "boolean"
+ "title": "Description",
+ "description": "Model description"
},
- "ok_adaptive_gamut": {
- "default": 0.05,
- "description": "Higher preserves chroma at the expense of lightness (Oklab)",
- "field_kind": "input",
- "input": "any",
- "minimum": 0,
- "orig_default": 0.05,
- "orig_required": false,
- "title": "Ok Adaptive Gamut",
- "type": "number"
+ "source": {
+ "type": "string",
+ "title": "Source",
+ "description": "The original source of the model (path, URL or repo_id)."
},
- "ok_high_precision": {
- "default": true,
- "description": "Use more steps in computing gamut (Oklab/Okhsv/Okhsl)",
- "field_kind": "input",
- "input": "any",
- "orig_default": true,
- "orig_required": false,
- "title": "Ok High Precision",
- "type": "boolean"
+ "source_type": {
+ "$ref": "#/components/schemas/ModelSourceType",
+ "description": "The type of source"
},
- "type": {
- "const": "invokeai_img_hue_adjust_plus",
- "default": "invokeai_img_hue_adjust_plus",
- "field_kind": "node_attribute",
- "title": "type",
- "type": "string"
- }
- },
- "required": ["type", "id"],
- "tags": ["image", "hue", "oklab", "cielab", "uplab", "lch", "hsv", "hsl", "lab"],
- "title": "Adjust Image Hue Plus",
- "type": "object",
- "version": "1.2.0",
- "output": {
- "$ref": "#/components/schemas/ImageOutput"
- }
- },
- "InvokeEquivalentAchromaticLightnessInvocation": {
- "category": "image",
- "class": "invocation",
- "classification": "stable",
- "description": "Calculate Equivalent Achromatic Lightness from image. Originally created by @dwringer",
- "node_pack": "invokeai",
- "properties": {
- "board": {
+ "source_api_response": {
"anyOf": [
{
- "$ref": "#/components/schemas/BoardField"
+ "type": "string"
},
{
"type": "null"
}
],
- "default": null,
- "description": "The board to save the image to",
- "field_kind": "internal",
- "input": "direct",
- "orig_required": false,
- "ui_hidden": false
+ "title": "Source Api Response",
+ "description": "The original API response from the source, as stringified JSON."
},
- "metadata": {
+ "source_url": {
"anyOf": [
{
- "$ref": "#/components/schemas/MetadataField"
+ "type": "string"
},
{
"type": "null"
}
],
- "default": null,
- "description": "Optional metadata to be saved with the image",
- "field_kind": "internal",
- "input": "connection",
- "orig_required": false,
- "ui_hidden": false
+ "title": "Source Url",
+ "description": "Optional URL for the model (e.g. download page or model page)."
},
- "id": {
- "description": "The id of this instance of an invocation. Must be unique among all instances of invocations.",
- "field_kind": "node_attribute",
- "title": "Id",
- "type": "string"
+ "cover_image": {
+ "anyOf": [
+ {
+ "type": "string"
+ },
+ {
+ "type": "null"
+ }
+ ],
+ "title": "Cover Image",
+ "description": "Url for image to preview model"
},
- "is_intermediate": {
- "default": false,
- "description": "Whether or not this is an intermediate invocation.",
- "field_kind": "node_attribute",
- "input": "direct",
- "orig_required": true,
- "title": "Is Intermediate",
- "type": "boolean",
- "ui_hidden": false,
- "ui_type": "IsIntermediate"
+ "type": {
+ "type": "string",
+ "const": "lora",
+ "title": "Type",
+ "default": "lora"
},
- "use_cache": {
- "default": true,
- "description": "Whether or not to use the cache",
- "field_kind": "node_attribute",
- "title": "Use Cache",
- "type": "boolean"
+ "trigger_phrases": {
+ "anyOf": [
+ {
+ "items": {
+ "type": "string"
+ },
+ "type": "array",
+ "uniqueItems": true
+ },
+ {
+ "type": "null"
+ }
+ ],
+ "title": "Trigger Phrases",
+ "description": "Set of trigger phrases for this model"
},
- "image": {
+ "default_settings": {
"anyOf": [
{
- "$ref": "#/components/schemas/ImageField"
+ "$ref": "#/components/schemas/LoraModelDefaultSettings"
},
{
"type": "null"
}
],
- "default": null,
- "description": "Image from which to get channel",
- "field_kind": "input",
- "input": "any",
- "orig_required": true
+ "description": "Default settings for this model"
},
- "type": {
- "const": "invokeai_ealightness",
- "default": "invokeai_ealightness",
- "field_kind": "node_attribute",
- "title": "type",
- "type": "string"
+ "format": {
+ "type": "string",
+ "const": "diffusers",
+ "title": "Format",
+ "default": "diffusers"
+ },
+ "base": {
+ "type": "string",
+ "const": "sdxl",
+ "title": "Base",
+ "default": "sdxl"
}
},
- "required": ["type", "id"],
- "tags": ["image", "channel", "mask", "cielab", "lab"],
- "title": "Equivalent Achromatic Lightness",
"type": "object",
- "version": "1.2.0",
- "output": {
- "$ref": "#/components/schemas/ImageOutput"
- }
+ "required": [
+ "key",
+ "hash",
+ "path",
+ "file_size",
+ "name",
+ "description",
+ "source",
+ "source_type",
+ "source_api_response",
+ "source_url",
+ "cover_image",
+ "type",
+ "trigger_phrases",
+ "default_settings",
+ "format",
+ "base"
+ ],
+ "title": "LoRA_Diffusers_SDXL_Config"
},
- "InvokeImageBlendInvocation": {
- "category": "image",
- "class": "invocation",
- "classification": "stable",
- "description": "Blend two images together, with optional opacity, mask, and blend modes. Originally created by @dwringer",
- "node_pack": "invokeai",
+ "LoRA_Diffusers_ZImage_Config": {
"properties": {
- "board": {
- "anyOf": [
- {
- "$ref": "#/components/schemas/BoardField"
- },
- {
- "type": "null"
- }
- ],
- "default": null,
- "description": "The board to save the image to",
- "field_kind": "internal",
- "input": "direct",
- "orig_required": false,
- "ui_hidden": false
+ "key": {
+ "type": "string",
+ "title": "Key",
+ "description": "A unique key for this model."
},
- "metadata": {
+ "hash": {
+ "type": "string",
+ "title": "Hash",
+ "description": "The hash of the model file(s)."
+ },
+ "path": {
+ "type": "string",
+ "title": "Path",
+ "description": "Path to the model on the filesystem. Relative paths are relative to the Invoke root directory."
+ },
+ "file_size": {
+ "type": "integer",
+ "title": "File Size",
+ "description": "The size of the model in bytes."
+ },
+ "name": {
+ "type": "string",
+ "title": "Name",
+ "description": "Name of the model."
+ },
+ "description": {
"anyOf": [
{
- "$ref": "#/components/schemas/MetadataField"
+ "type": "string"
},
{
"type": "null"
}
],
- "default": null,
- "description": "Optional metadata to be saved with the image",
- "field_kind": "internal",
- "input": "connection",
- "orig_required": false,
- "ui_hidden": false
- },
- "id": {
- "description": "The id of this instance of an invocation. Must be unique among all instances of invocations.",
- "field_kind": "node_attribute",
- "title": "Id",
- "type": "string"
+ "title": "Description",
+ "description": "Model description"
},
- "is_intermediate": {
- "default": false,
- "description": "Whether or not this is an intermediate invocation.",
- "field_kind": "node_attribute",
- "input": "direct",
- "orig_required": true,
- "title": "Is Intermediate",
- "type": "boolean",
- "ui_hidden": false,
- "ui_type": "IsIntermediate"
+ "source": {
+ "type": "string",
+ "title": "Source",
+ "description": "The original source of the model (path, URL or repo_id)."
},
- "use_cache": {
- "default": true,
- "description": "Whether or not to use the cache",
- "field_kind": "node_attribute",
- "title": "Use Cache",
- "type": "boolean"
+ "source_type": {
+ "$ref": "#/components/schemas/ModelSourceType",
+ "description": "The type of source"
},
- "layer_upper": {
+ "source_api_response": {
"anyOf": [
{
- "$ref": "#/components/schemas/ImageField"
+ "type": "string"
},
{
"type": "null"
}
],
- "default": null,
- "description": "The top image to blend",
- "field_kind": "input",
- "input": "any",
- "orig_required": true,
- "ui_order": 1
- },
- "blend_mode": {
- "default": "Normal",
- "description": "Available blend modes",
- "enum": [
- "Normal",
- "Lighten Only",
- "Darken Only",
- "Lighten Only (EAL)",
- "Darken Only (EAL)",
- "Hue",
- "Saturation",
- "Color",
- "Luminosity",
- "Linear Dodge (Add)",
- "Subtract",
- "Multiply",
- "Divide",
- "Screen",
- "Overlay",
- "Linear Burn",
- "Difference",
- "Hard Light",
- "Soft Light",
- "Vivid Light",
- "Linear Light",
- "Color Burn",
- "Color Dodge"
- ],
- "field_kind": "input",
- "input": "any",
- "orig_default": "Normal",
- "orig_required": false,
- "title": "Blend Mode",
- "type": "string",
- "ui_order": 2
- },
- "opacity": {
- "default": 1.0,
- "description": "Desired opacity of the upper layer",
- "field_kind": "input",
- "input": "any",
- "minimum": 0,
- "orig_default": 1.0,
- "orig_required": false,
- "title": "Opacity",
- "type": "number",
- "ui_order": 3
+ "title": "Source Api Response",
+ "description": "The original API response from the source, as stringified JSON."
},
- "mask": {
+ "source_url": {
"anyOf": [
{
- "$ref": "#/components/schemas/ImageField"
+ "type": "string"
},
{
"type": "null"
}
],
- "default": null,
- "description": "Optional mask, used to restrict areas from blending",
- "field_kind": "input",
- "input": "any",
- "orig_default": null,
- "orig_required": false,
- "ui_order": 4
- },
- "fit_to_width": {
- "default": false,
- "description": "Scale upper layer to fit base width",
- "field_kind": "input",
- "input": "any",
- "orig_default": false,
- "orig_required": false,
- "title": "Fit To Width",
- "type": "boolean",
- "ui_order": 5
- },
- "fit_to_height": {
- "default": true,
- "description": "Scale upper layer to fit base height",
- "field_kind": "input",
- "input": "any",
- "orig_default": true,
- "orig_required": false,
- "title": "Fit To Height",
- "type": "boolean",
- "ui_order": 6
+ "title": "Source Url",
+ "description": "Optional URL for the model (e.g. download page or model page)."
},
- "layer_base": {
+ "cover_image": {
"anyOf": [
{
- "$ref": "#/components/schemas/ImageField"
+ "type": "string"
},
{
"type": "null"
}
],
- "default": null,
- "description": "The bottom image to blend",
- "field_kind": "input",
- "input": "any",
- "orig_required": true,
- "ui_order": 7
+ "title": "Cover Image",
+ "description": "Url for image to preview model"
},
- "color_space": {
- "default": "RGB",
- "description": "Available color spaces for blend computations",
- "enum": ["RGB", "Linear RGB", "HSL (RGB)", "HSV (RGB)", "Okhsl", "Okhsv", "Oklch (Oklab)", "LCh (CIELab)"],
- "field_kind": "input",
- "input": "any",
- "orig_default": "RGB",
- "orig_required": false,
- "title": "Color Space",
+ "type": {
"type": "string",
- "ui_order": 8
- },
- "adaptive_gamut": {
- "default": 0.0,
- "description": "Adaptive gamut clipping (0=off). Higher prioritizes chroma over lightness",
- "field_kind": "input",
- "input": "any",
- "minimum": 0,
- "orig_default": 0.0,
- "orig_required": false,
- "title": "Adaptive Gamut",
- "type": "number",
- "ui_order": 9
- },
- "high_precision": {
- "default": true,
- "description": "Use more steps in computing gamut when possible",
- "field_kind": "input",
- "input": "any",
- "orig_default": true,
- "orig_required": false,
- "title": "High Precision",
- "type": "boolean",
- "ui_order": 10
+ "const": "lora",
+ "title": "Type",
+ "default": "lora"
},
- "type": {
- "const": "invokeai_img_blend",
- "default": "invokeai_img_blend",
- "field_kind": "node_attribute",
- "title": "type",
- "type": "string"
- }
- },
- "required": ["type", "id"],
- "tags": ["image", "blend", "layer", "alpha", "composite", "dodge", "burn"],
- "title": "Image Layer Blend",
- "type": "object",
- "version": "1.2.0",
- "output": {
- "$ref": "#/components/schemas/ImageOutput"
- }
- },
- "InvokeImageCompositorInvocation": {
- "category": "image",
- "class": "invocation",
- "classification": "stable",
- "description": "Removes backdrop from subject image then overlays subject on background image. Originally created by @dwringer",
- "node_pack": "invokeai",
- "properties": {
- "board": {
+ "trigger_phrases": {
"anyOf": [
{
- "$ref": "#/components/schemas/BoardField"
+ "items": {
+ "type": "string"
+ },
+ "type": "array",
+ "uniqueItems": true
},
{
"type": "null"
}
],
- "default": null,
- "description": "The board to save the image to",
- "field_kind": "internal",
- "input": "direct",
- "orig_required": false,
- "ui_hidden": false
+ "title": "Trigger Phrases",
+ "description": "Set of trigger phrases for this model"
},
- "metadata": {
+ "default_settings": {
"anyOf": [
{
- "$ref": "#/components/schemas/MetadataField"
+ "$ref": "#/components/schemas/LoraModelDefaultSettings"
},
{
"type": "null"
}
],
- "default": null,
- "description": "Optional metadata to be saved with the image",
- "field_kind": "internal",
- "input": "connection",
- "orig_required": false,
- "ui_hidden": false
- },
- "id": {
- "description": "The id of this instance of an invocation. Must be unique among all instances of invocations.",
- "field_kind": "node_attribute",
- "title": "Id",
- "type": "string"
+ "description": "Default settings for this model"
},
- "is_intermediate": {
- "default": false,
- "description": "Whether or not this is an intermediate invocation.",
- "field_kind": "node_attribute",
- "input": "direct",
- "orig_required": true,
- "title": "Is Intermediate",
- "type": "boolean",
- "ui_hidden": false,
- "ui_type": "IsIntermediate"
+ "format": {
+ "type": "string",
+ "const": "diffusers",
+ "title": "Format",
+ "default": "diffusers"
},
- "use_cache": {
- "default": true,
- "description": "Whether or not to use the cache",
- "field_kind": "node_attribute",
- "title": "Use Cache",
- "type": "boolean"
+ "base": {
+ "type": "string",
+ "const": "z-image",
+ "title": "Base",
+ "default": "z-image"
},
- "image_subject": {
+ "variant": {
"anyOf": [
{
- "$ref": "#/components/schemas/ImageField"
+ "$ref": "#/components/schemas/ZImageVariantType"
},
{
"type": "null"
}
- ],
- "default": null,
- "description": "Image of the subject on a plain monochrome background",
- "field_kind": "input",
- "input": "any",
- "orig_required": true
+ ]
+ }
+ },
+ "type": "object",
+ "required": [
+ "key",
+ "hash",
+ "path",
+ "file_size",
+ "name",
+ "description",
+ "source",
+ "source_type",
+ "source_api_response",
+ "source_url",
+ "cover_image",
+ "type",
+ "trigger_phrases",
+ "default_settings",
+ "format",
+ "base",
+ "variant"
+ ],
+ "title": "LoRA_Diffusers_ZImage_Config",
+ "description": "Model config for Z-Image LoRA models in Diffusers format."
+ },
+ "LoRA_LyCORIS_Anima_Config": {
+ "properties": {
+ "key": {
+ "type": "string",
+ "title": "Key",
+ "description": "A unique key for this model."
},
- "image_background": {
+ "hash": {
+ "type": "string",
+ "title": "Hash",
+ "description": "The hash of the model file(s)."
+ },
+ "path": {
+ "type": "string",
+ "title": "Path",
+ "description": "Path to the model on the filesystem. Relative paths are relative to the Invoke root directory."
+ },
+ "file_size": {
+ "type": "integer",
+ "title": "File Size",
+ "description": "The size of the model in bytes."
+ },
+ "name": {
+ "type": "string",
+ "title": "Name",
+ "description": "Name of the model."
+ },
+ "description": {
"anyOf": [
{
- "$ref": "#/components/schemas/ImageField"
+ "type": "string"
},
{
"type": "null"
}
],
- "default": null,
- "description": "Image of a background scene",
- "field_kind": "input",
- "input": "any",
- "orig_required": true
- },
- "chroma_key": {
- "default": "",
- "description": "Can be empty for corner flood select, or CSS-3 color or tuple",
- "field_kind": "input",
- "input": "any",
- "orig_default": "",
- "orig_required": false,
- "title": "Chroma Key",
- "type": "string"
- },
- "threshold": {
- "default": 50,
- "description": "Subject isolation flood-fill threshold",
- "field_kind": "input",
- "input": "any",
- "minimum": 0,
- "orig_default": 50,
- "orig_required": false,
- "title": "Threshold",
- "type": "integer"
- },
- "fill_x": {
- "default": false,
- "description": "Scale base subject image to fit background width",
- "field_kind": "input",
- "input": "any",
- "orig_default": false,
- "orig_required": false,
- "title": "Fill X",
- "type": "boolean"
- },
- "fill_y": {
- "default": true,
- "description": "Scale base subject image to fit background height",
- "field_kind": "input",
- "input": "any",
- "orig_default": true,
- "orig_required": false,
- "title": "Fill Y",
- "type": "boolean"
+ "title": "Description",
+ "description": "Model description"
},
- "x_offset": {
- "default": 0,
- "description": "x-offset for the subject",
- "field_kind": "input",
- "input": "any",
- "orig_default": 0,
- "orig_required": false,
- "title": "X Offset",
- "type": "integer"
+ "source": {
+ "type": "string",
+ "title": "Source",
+ "description": "The original source of the model (path, URL or repo_id)."
},
- "y_offset": {
- "default": 0,
- "description": "y-offset for the subject",
- "field_kind": "input",
- "input": "any",
- "orig_default": 0,
- "orig_required": false,
- "title": "Y Offset",
- "type": "integer"
+ "source_type": {
+ "$ref": "#/components/schemas/ModelSourceType",
+ "description": "The type of source"
},
- "type": {
- "const": "invokeai_img_composite",
- "default": "invokeai_img_composite",
- "field_kind": "node_attribute",
- "title": "type",
- "type": "string"
- }
- },
- "required": ["type", "id"],
- "tags": ["image", "compose", "chroma", "key"],
- "title": "Image Compositor",
- "type": "object",
- "version": "1.2.0",
- "output": {
- "$ref": "#/components/schemas/ImageOutput"
- }
- },
- "InvokeImageDilateOrErodeInvocation": {
- "category": "image",
- "class": "invocation",
- "classification": "stable",
- "description": "Dilate (expand) or erode (contract) an image. Originally created by @dwringer",
- "node_pack": "invokeai",
- "properties": {
- "metadata": {
+ "source_api_response": {
"anyOf": [
{
- "$ref": "#/components/schemas/MetadataField"
+ "type": "string"
},
{
"type": "null"
}
],
- "default": null,
- "description": "Optional metadata to be saved with the image",
- "field_kind": "internal",
- "input": "connection",
- "orig_required": false,
- "ui_hidden": false
- },
- "id": {
- "description": "The id of this instance of an invocation. Must be unique among all instances of invocations.",
- "field_kind": "node_attribute",
- "title": "Id",
- "type": "string"
- },
- "is_intermediate": {
- "default": false,
- "description": "Whether or not this is an intermediate invocation.",
- "field_kind": "node_attribute",
- "input": "direct",
- "orig_required": true,
- "title": "Is Intermediate",
- "type": "boolean",
- "ui_hidden": false,
- "ui_type": "IsIntermediate"
+ "title": "Source Api Response",
+ "description": "The original API response from the source, as stringified JSON."
},
- "use_cache": {
- "default": true,
- "description": "Whether or not to use the cache",
- "field_kind": "node_attribute",
- "title": "Use Cache",
- "type": "boolean"
+ "source_url": {
+ "anyOf": [
+ {
+ "type": "string"
+ },
+ {
+ "type": "null"
+ }
+ ],
+ "title": "Source Url",
+ "description": "Optional URL for the model (e.g. download page or model page)."
},
- "image": {
+ "cover_image": {
"anyOf": [
{
- "$ref": "#/components/schemas/ImageField"
+ "type": "string"
},
{
"type": "null"
}
],
- "default": null,
- "description": "The image from which to create a mask",
- "field_kind": "input",
- "input": "any",
- "orig_required": true
- },
- "lightness_only": {
- "default": false,
- "description": "If true, only applies to image lightness (CIELa*b*)",
- "field_kind": "input",
- "input": "any",
- "orig_default": false,
- "orig_required": false,
- "title": "Lightness Only",
- "type": "boolean"
- },
- "radius_w": {
- "default": 4,
- "description": "Width (in pixels) by which to dilate(expand) or erode (contract) the image",
- "field_kind": "input",
- "input": "any",
- "minimum": 0,
- "orig_default": 4,
- "orig_required": false,
- "title": "Radius W",
- "type": "integer"
- },
- "radius_h": {
- "default": 4,
- "description": "Height (in pixels) by which to dilate(expand) or erode (contract) the image",
- "field_kind": "input",
- "input": "any",
- "minimum": 0,
- "orig_default": 4,
- "orig_required": false,
- "title": "Radius H",
- "type": "integer"
- },
- "mode": {
- "default": "Dilate",
- "description": "How to operate on the image",
- "enum": ["Dilate", "Erode"],
- "field_kind": "input",
- "input": "any",
- "orig_default": "Dilate",
- "orig_required": false,
- "title": "Mode",
- "type": "string"
+ "title": "Cover Image",
+ "description": "Url for image to preview model"
},
"type": {
- "const": "invokeai_img_dilate_erode",
- "default": "invokeai_img_dilate_erode",
- "field_kind": "node_attribute",
- "title": "type",
- "type": "string"
- }
- },
- "required": ["type", "id"],
- "tags": ["image", "mask", "dilate", "erode", "expand", "contract", "mask"],
- "title": "Image Dilate or Erode",
- "type": "object",
- "version": "1.3.0",
- "output": {
- "$ref": "#/components/schemas/ImageOutput"
- }
- },
- "InvokeImageEnhanceInvocation": {
- "category": "image",
- "class": "invocation",
- "classification": "stable",
- "description": "Applies processing from PIL's ImageEnhance module. Originally created by @dwringer",
- "node_pack": "invokeai",
- "properties": {
- "board": {
+ "type": "string",
+ "const": "lora",
+ "title": "Type",
+ "default": "lora"
+ },
+ "trigger_phrases": {
"anyOf": [
{
- "$ref": "#/components/schemas/BoardField"
+ "items": {
+ "type": "string"
+ },
+ "type": "array",
+ "uniqueItems": true
},
{
"type": "null"
}
],
- "default": null,
- "description": "The board to save the image to",
- "field_kind": "internal",
- "input": "direct",
- "orig_required": false,
- "ui_hidden": false
+ "title": "Trigger Phrases",
+ "description": "Set of trigger phrases for this model"
},
- "metadata": {
+ "default_settings": {
"anyOf": [
{
- "$ref": "#/components/schemas/MetadataField"
+ "$ref": "#/components/schemas/LoraModelDefaultSettings"
},
{
"type": "null"
}
],
- "default": null,
- "description": "Optional metadata to be saved with the image",
- "field_kind": "internal",
- "input": "connection",
- "orig_required": false,
- "ui_hidden": false
+ "description": "Default settings for this model"
},
- "id": {
- "description": "The id of this instance of an invocation. Must be unique among all instances of invocations.",
- "field_kind": "node_attribute",
- "title": "Id",
- "type": "string"
+ "format": {
+ "type": "string",
+ "const": "lycoris",
+ "title": "Format",
+ "default": "lycoris"
},
- "is_intermediate": {
- "default": false,
- "description": "Whether or not this is an intermediate invocation.",
- "field_kind": "node_attribute",
- "input": "direct",
- "orig_required": true,
- "title": "Is Intermediate",
- "type": "boolean",
- "ui_hidden": false,
- "ui_type": "IsIntermediate"
+ "base": {
+ "type": "string",
+ "const": "anima",
+ "title": "Base",
+ "default": "anima"
+ }
+ },
+ "type": "object",
+ "required": [
+ "key",
+ "hash",
+ "path",
+ "file_size",
+ "name",
+ "description",
+ "source",
+ "source_type",
+ "source_api_response",
+ "source_url",
+ "cover_image",
+ "type",
+ "trigger_phrases",
+ "default_settings",
+ "format",
+ "base"
+ ],
+ "title": "LoRA_LyCORIS_Anima_Config",
+ "description": "Model config for Anima LoRA models in LyCORIS format."
+ },
+ "LoRA_LyCORIS_FLUX_Config": {
+ "properties": {
+ "key": {
+ "type": "string",
+ "title": "Key",
+ "description": "A unique key for this model."
},
- "use_cache": {
- "default": true,
- "description": "Whether or not to use the cache",
- "field_kind": "node_attribute",
- "title": "Use Cache",
- "type": "boolean"
+ "hash": {
+ "type": "string",
+ "title": "Hash",
+ "description": "The hash of the model file(s)."
},
- "image": {
+ "path": {
+ "type": "string",
+ "title": "Path",
+ "description": "Path to the model on the filesystem. Relative paths are relative to the Invoke root directory."
+ },
+ "file_size": {
+ "type": "integer",
+ "title": "File Size",
+ "description": "The size of the model in bytes."
+ },
+ "name": {
+ "type": "string",
+ "title": "Name",
+ "description": "Name of the model."
+ },
+ "description": {
"anyOf": [
{
- "$ref": "#/components/schemas/ImageField"
+ "type": "string"
},
{
"type": "null"
}
],
- "default": null,
- "description": "The image for which to apply processing",
- "field_kind": "input",
- "input": "any",
- "orig_required": true
- },
- "invert": {
- "default": false,
- "description": "Whether to invert the image colors",
- "field_kind": "input",
- "input": "any",
- "orig_default": false,
- "orig_required": false,
- "title": "Invert",
- "type": "boolean"
- },
- "color": {
- "default": 1.0,
- "description": "Color enhancement factor",
- "field_kind": "input",
- "input": "any",
- "minimum": 0,
- "orig_default": 1.0,
- "orig_required": false,
- "title": "Color",
- "type": "number"
- },
- "contrast": {
- "default": 1.0,
- "description": "Contrast enhancement factor",
- "field_kind": "input",
- "input": "any",
- "minimum": 0,
- "orig_default": 1.0,
- "orig_required": false,
- "title": "Contrast",
- "type": "number"
+ "title": "Description",
+ "description": "Model description"
},
- "brightness": {
- "default": 1.0,
- "description": "Brightness enhancement factor",
- "field_kind": "input",
- "input": "any",
- "minimum": 0,
- "orig_default": 1.0,
- "orig_required": false,
- "title": "Brightness",
- "type": "number"
+ "source": {
+ "type": "string",
+ "title": "Source",
+ "description": "The original source of the model (path, URL or repo_id)."
},
- "sharpness": {
- "default": 1.0,
- "description": "Sharpness enhancement factor",
- "field_kind": "input",
- "input": "any",
- "minimum": 0,
- "orig_default": 1.0,
- "orig_required": false,
- "title": "Sharpness",
- "type": "number"
+ "source_type": {
+ "$ref": "#/components/schemas/ModelSourceType",
+ "description": "The type of source"
},
- "type": {
- "const": "invokeai_img_enhance",
- "default": "invokeai_img_enhance",
- "field_kind": "node_attribute",
- "title": "type",
- "type": "string"
- }
- },
- "required": ["type", "id"],
- "tags": ["enhance", "image"],
- "title": "Enhance Image",
- "type": "object",
- "version": "1.2.1",
- "output": {
- "$ref": "#/components/schemas/ImageOutput"
- }
- },
- "InvokeImageValueThresholdsInvocation": {
- "category": "image",
- "class": "invocation",
- "classification": "stable",
- "description": "Clip image to pure black/white past specified thresholds. Originally created by @dwringer",
- "node_pack": "invokeai",
- "properties": {
- "board": {
+ "source_api_response": {
"anyOf": [
{
- "$ref": "#/components/schemas/BoardField"
+ "type": "string"
},
{
"type": "null"
}
],
- "default": null,
- "description": "The board to save the image to",
- "field_kind": "internal",
- "input": "direct",
- "orig_required": false,
- "ui_hidden": false
+ "title": "Source Api Response",
+ "description": "The original API response from the source, as stringified JSON."
},
- "metadata": {
+ "source_url": {
"anyOf": [
{
- "$ref": "#/components/schemas/MetadataField"
+ "type": "string"
},
{
"type": "null"
}
],
- "default": null,
- "description": "Optional metadata to be saved with the image",
- "field_kind": "internal",
- "input": "connection",
- "orig_required": false,
- "ui_hidden": false
- },
- "id": {
- "description": "The id of this instance of an invocation. Must be unique among all instances of invocations.",
- "field_kind": "node_attribute",
- "title": "Id",
- "type": "string"
- },
- "is_intermediate": {
- "default": false,
- "description": "Whether or not this is an intermediate invocation.",
- "field_kind": "node_attribute",
- "input": "direct",
- "orig_required": true,
- "title": "Is Intermediate",
- "type": "boolean",
- "ui_hidden": false,
- "ui_type": "IsIntermediate"
- },
- "use_cache": {
- "default": true,
- "description": "Whether or not to use the cache",
- "field_kind": "node_attribute",
- "title": "Use Cache",
- "type": "boolean"
+ "title": "Source Url",
+ "description": "Optional URL for the model (e.g. download page or model page)."
},
- "image": {
+ "cover_image": {
"anyOf": [
{
- "$ref": "#/components/schemas/ImageField"
+ "type": "string"
},
{
"type": "null"
}
],
- "default": null,
- "description": "The image from which to create a mask",
- "field_kind": "input",
- "input": "any",
- "orig_required": true
- },
- "invert_output": {
- "default": false,
- "description": "Make light areas dark and vice versa",
- "field_kind": "input",
- "input": "any",
- "orig_default": false,
- "orig_required": false,
- "title": "Invert Output",
- "type": "boolean"
- },
- "renormalize_values": {
- "default": false,
- "description": "Rescale remaining values from minimum to maximum",
- "field_kind": "input",
- "input": "any",
- "orig_default": false,
- "orig_required": false,
- "title": "Renormalize Values",
- "type": "boolean"
- },
- "lightness_only": {
- "default": false,
- "description": "If true, only applies to image lightness (CIELa*b*)",
- "field_kind": "input",
- "input": "any",
- "orig_default": false,
- "orig_required": false,
- "title": "Lightness Only",
- "type": "boolean"
- },
- "threshold_upper": {
- "default": 0.5,
- "description": "Threshold above which will be set to full value",
- "field_kind": "input",
- "input": "any",
- "orig_default": 0.5,
- "orig_required": false,
- "title": "Threshold Upper",
- "type": "number"
- },
- "threshold_lower": {
- "default": 0.5,
- "description": "Threshold below which will be set to minimum value",
- "field_kind": "input",
- "input": "any",
- "orig_default": 0.5,
- "orig_required": false,
- "title": "Threshold Lower",
- "type": "number"
+ "title": "Cover Image",
+ "description": "Url for image to preview model"
},
"type": {
- "const": "invokeai_img_val_thresholds",
- "default": "invokeai_img_val_thresholds",
- "field_kind": "node_attribute",
- "title": "type",
- "type": "string"
- }
- },
- "required": ["type", "id"],
- "tags": ["image", "mask", "value", "threshold"],
- "title": "Image Value Thresholds",
- "type": "object",
- "version": "1.2.0",
- "output": {
- "$ref": "#/components/schemas/ImageOutput"
- }
- },
- "ItemIdsResult": {
- "properties": {
- "item_ids": {
- "items": {
- "type": "integer"
- },
- "type": "array",
- "title": "Item Ids",
- "description": "Ordered list of item ids"
- },
- "total_count": {
- "type": "integer",
- "title": "Total Count",
- "description": "Total number of queue items matching the query"
- }
- },
- "type": "object",
- "required": ["item_ids", "total_count"],
- "title": "ItemIdsResult",
- "description": "Response containing ordered item ids with metadata for optimistic updates."
- },
- "IterateInvocation": {
- "class": "invocation",
- "classification": "stable",
- "description": "Iterates over a list of items",
- "node_pack": "invokeai",
- "properties": {
- "id": {
- "description": "The id of this instance of an invocation. Must be unique among all instances of invocations.",
- "field_kind": "node_attribute",
- "title": "Id",
- "type": "string"
- },
- "is_intermediate": {
- "default": false,
- "description": "Whether or not this is an intermediate invocation.",
- "field_kind": "node_attribute",
- "input": "direct",
- "orig_required": true,
- "title": "Is Intermediate",
- "type": "boolean",
- "ui_hidden": false,
- "ui_type": "IsIntermediate"
+ "type": "string",
+ "const": "lora",
+ "title": "Type",
+ "default": "lora"
},
- "use_cache": {
- "default": true,
- "description": "Whether or not to use the cache",
- "field_kind": "node_attribute",
- "title": "Use Cache",
- "type": "boolean"
+ "trigger_phrases": {
+ "anyOf": [
+ {
+ "items": {
+ "type": "string"
+ },
+ "type": "array",
+ "uniqueItems": true
+ },
+ {
+ "type": "null"
+ }
+ ],
+ "title": "Trigger Phrases",
+ "description": "Set of trigger phrases for this model"
},
- "collection": {
- "default": [],
- "description": "The list of items to iterate over",
- "field_kind": "input",
- "input": "any",
- "items": {},
- "orig_default": [],
- "orig_required": false,
- "title": "Collection",
- "type": "array",
- "ui_type": "CollectionField"
+ "default_settings": {
+ "anyOf": [
+ {
+ "$ref": "#/components/schemas/LoraModelDefaultSettings"
+ },
+ {
+ "type": "null"
+ }
+ ],
+ "description": "Default settings for this model"
},
- "index": {
- "default": 0,
- "description": "The index, will be provided on executed iterators",
- "field_kind": "input",
- "input": "any",
- "orig_default": 0,
- "orig_required": false,
- "title": "Index",
- "type": "integer",
- "ui_hidden": true
+ "format": {
+ "type": "string",
+ "const": "lycoris",
+ "title": "Format",
+ "default": "lycoris"
},
- "type": {
- "const": "iterate",
- "default": "iterate",
- "field_kind": "node_attribute",
- "title": "type",
- "type": "string"
+ "base": {
+ "type": "string",
+ "const": "flux",
+ "title": "Base",
+ "default": "flux"
}
},
- "required": ["type", "id"],
- "title": "IterateInvocation",
"type": "object",
- "version": "1.1.0",
- "output": {
- "$ref": "#/components/schemas/IterateInvocationOutput"
- }
+ "required": [
+ "key",
+ "hash",
+ "path",
+ "file_size",
+ "name",
+ "description",
+ "source",
+ "source_type",
+ "source_api_response",
+ "source_url",
+ "cover_image",
+ "type",
+ "trigger_phrases",
+ "default_settings",
+ "format",
+ "base"
+ ],
+ "title": "LoRA_LyCORIS_FLUX_Config"
},
- "IterateInvocationOutput": {
- "class": "output",
- "description": "Used to connect iteration outputs. Will be expanded to a specific output.",
+ "LoRA_LyCORIS_Flux2_Config": {
"properties": {
- "item": {
- "description": "The item being iterated over",
- "field_kind": "output",
- "title": "Collection Item",
- "ui_hidden": false,
- "ui_type": "CollectionItemField"
+ "key": {
+ "type": "string",
+ "title": "Key",
+ "description": "A unique key for this model."
},
- "index": {
- "description": "The index of the item",
- "field_kind": "output",
- "title": "Index",
- "type": "integer",
- "ui_hidden": false
+ "hash": {
+ "type": "string",
+ "title": "Hash",
+ "description": "The hash of the model file(s)."
},
- "total": {
- "description": "The total number of items",
- "field_kind": "output",
- "title": "Total",
+ "path": {
+ "type": "string",
+ "title": "Path",
+ "description": "Path to the model on the filesystem. Relative paths are relative to the Invoke root directory."
+ },
+ "file_size": {
"type": "integer",
- "ui_hidden": false
+ "title": "File Size",
+ "description": "The size of the model in bytes."
},
- "type": {
- "const": "iterate_output",
- "default": "iterate_output",
- "field_kind": "node_attribute",
- "title": "type",
- "type": "string"
- }
- },
- "required": ["output_meta", "item", "index", "total", "type", "type"],
- "title": "IterateInvocationOutput",
- "type": "object"
- },
- "JsonValue": {},
- "LaMaInfillInvocation": {
- "category": "inpaint",
- "class": "invocation",
- "classification": "stable",
- "description": "Infills transparent areas of an image using the LaMa model",
- "node_pack": "invokeai",
- "properties": {
- "board": {
+ "name": {
+ "type": "string",
+ "title": "Name",
+ "description": "Name of the model."
+ },
+ "description": {
"anyOf": [
{
- "$ref": "#/components/schemas/BoardField"
+ "type": "string"
},
{
"type": "null"
}
],
- "default": null,
- "description": "The board to save the image to",
- "field_kind": "internal",
- "input": "direct",
- "orig_required": false,
- "ui_hidden": false
+ "title": "Description",
+ "description": "Model description"
},
- "metadata": {
+ "source": {
+ "type": "string",
+ "title": "Source",
+ "description": "The original source of the model (path, URL or repo_id)."
+ },
+ "source_type": {
+ "$ref": "#/components/schemas/ModelSourceType",
+ "description": "The type of source"
+ },
+ "source_api_response": {
"anyOf": [
{
- "$ref": "#/components/schemas/MetadataField"
+ "type": "string"
},
{
"type": "null"
}
],
- "default": null,
- "description": "Optional metadata to be saved with the image",
- "field_kind": "internal",
- "input": "connection",
- "orig_required": false,
- "ui_hidden": false
- },
- "id": {
- "description": "The id of this instance of an invocation. Must be unique among all instances of invocations.",
- "field_kind": "node_attribute",
- "title": "Id",
- "type": "string"
- },
- "is_intermediate": {
- "default": false,
- "description": "Whether or not this is an intermediate invocation.",
- "field_kind": "node_attribute",
- "input": "direct",
- "orig_required": true,
- "title": "Is Intermediate",
- "type": "boolean",
- "ui_hidden": false,
- "ui_type": "IsIntermediate"
+ "title": "Source Api Response",
+ "description": "The original API response from the source, as stringified JSON."
},
- "use_cache": {
- "default": true,
- "description": "Whether or not to use the cache",
- "field_kind": "node_attribute",
- "title": "Use Cache",
- "type": "boolean"
+ "source_url": {
+ "anyOf": [
+ {
+ "type": "string"
+ },
+ {
+ "type": "null"
+ }
+ ],
+ "title": "Source Url",
+ "description": "Optional URL for the model (e.g. download page or model page)."
},
- "image": {
+ "cover_image": {
"anyOf": [
{
- "$ref": "#/components/schemas/ImageField"
+ "type": "string"
},
{
"type": "null"
}
],
- "default": null,
- "description": "The image to process",
- "field_kind": "input",
- "input": "any",
- "orig_required": true
+ "title": "Cover Image",
+ "description": "Url for image to preview model"
},
"type": {
- "const": "infill_lama",
- "default": "infill_lama",
- "field_kind": "node_attribute",
- "title": "type",
- "type": "string"
- }
- },
- "required": ["type", "id"],
- "tags": ["image", "inpaint"],
- "title": "LaMa Infill",
- "type": "object",
- "version": "1.2.2",
- "output": {
- "$ref": "#/components/schemas/ImageOutput"
- }
- },
- "LatentsCollectionInvocation": {
- "category": "primitives",
- "class": "invocation",
- "classification": "stable",
- "description": "A collection of latents tensor primitive values",
- "node_pack": "invokeai",
- "properties": {
- "id": {
- "description": "The id of this instance of an invocation. Must be unique among all instances of invocations.",
- "field_kind": "node_attribute",
- "title": "Id",
- "type": "string"
- },
- "is_intermediate": {
- "default": false,
- "description": "Whether or not this is an intermediate invocation.",
- "field_kind": "node_attribute",
- "input": "direct",
- "orig_required": true,
- "title": "Is Intermediate",
- "type": "boolean",
- "ui_hidden": false,
- "ui_type": "IsIntermediate"
- },
- "use_cache": {
- "default": true,
- "description": "Whether or not to use the cache",
- "field_kind": "node_attribute",
- "title": "Use Cache",
- "type": "boolean"
+ "type": "string",
+ "const": "lora",
+ "title": "Type",
+ "default": "lora"
},
- "collection": {
+ "trigger_phrases": {
"anyOf": [
{
"items": {
- "$ref": "#/components/schemas/LatentsField"
+ "type": "string"
},
- "type": "array"
+ "type": "array",
+ "uniqueItems": true
},
{
"type": "null"
}
],
- "default": null,
- "description": "The collection of latents tensors",
- "field_kind": "input",
- "input": "any",
- "orig_required": true,
- "title": "Collection"
- },
- "type": {
- "const": "latents_collection",
- "default": "latents_collection",
- "field_kind": "node_attribute",
- "title": "type",
- "type": "string"
- }
- },
- "required": ["type", "id"],
- "tags": ["primitives", "latents", "collection"],
- "title": "Latents Collection Primitive",
- "type": "object",
- "version": "1.0.1",
- "output": {
- "$ref": "#/components/schemas/LatentsCollectionOutput"
- }
- },
- "LatentsCollectionOutput": {
- "class": "output",
- "description": "Base class for nodes that output a collection of latents tensors",
- "properties": {
- "collection": {
- "description": "Latents tensor",
- "field_kind": "output",
- "items": {
- "$ref": "#/components/schemas/LatentsField"
- },
- "title": "Collection",
- "type": "array",
- "ui_hidden": false
- },
- "type": {
- "const": "latents_collection_output",
- "default": "latents_collection_output",
- "field_kind": "node_attribute",
- "title": "type",
- "type": "string"
- }
- },
- "required": ["output_meta", "collection", "type", "type"],
- "title": "LatentsCollectionOutput",
- "type": "object"
- },
- "LatentsField": {
- "description": "A latents tensor primitive field",
- "properties": {
- "latents_name": {
- "description": "The name of the latents",
- "title": "Latents Name",
- "type": "string"
+ "title": "Trigger Phrases",
+ "description": "Set of trigger phrases for this model"
},
- "seed": {
+ "default_settings": {
"anyOf": [
{
- "type": "integer"
+ "$ref": "#/components/schemas/LoraModelDefaultSettings"
},
{
"type": "null"
}
],
- "default": null,
- "description": "Seed used to generate this latents",
- "title": "Seed"
- }
- },
- "required": ["latents_name"],
- "title": "LatentsField",
- "type": "object"
- },
- "LatentsInvocation": {
- "category": "primitives",
- "class": "invocation",
- "classification": "stable",
- "description": "A latents tensor primitive value",
- "node_pack": "invokeai",
- "properties": {
- "id": {
- "description": "The id of this instance of an invocation. Must be unique among all instances of invocations.",
- "field_kind": "node_attribute",
- "title": "Id",
- "type": "string"
+ "description": "Default settings for this model"
},
- "is_intermediate": {
- "default": false,
- "description": "Whether or not this is an intermediate invocation.",
- "field_kind": "node_attribute",
- "input": "direct",
- "orig_required": true,
- "title": "Is Intermediate",
- "type": "boolean",
- "ui_hidden": false,
- "ui_type": "IsIntermediate"
+ "format": {
+ "type": "string",
+ "const": "lycoris",
+ "title": "Format",
+ "default": "lycoris"
},
- "use_cache": {
- "default": true,
- "description": "Whether or not to use the cache",
- "field_kind": "node_attribute",
- "title": "Use Cache",
- "type": "boolean"
+ "base": {
+ "type": "string",
+ "const": "flux2",
+ "title": "Base",
+ "default": "flux2"
},
- "latents": {
+ "variant": {
"anyOf": [
{
- "$ref": "#/components/schemas/LatentsField"
+ "$ref": "#/components/schemas/Flux2VariantType"
},
{
"type": "null"
}
- ],
- "default": null,
- "description": "The latents tensor",
- "field_kind": "input",
- "input": "connection",
- "orig_required": true
- },
- "type": {
- "const": "latents",
- "default": "latents",
- "field_kind": "node_attribute",
- "title": "type",
- "type": "string"
+ ]
}
},
- "required": ["type", "id"],
- "tags": ["primitives", "latents"],
- "title": "Latents Primitive",
"type": "object",
- "version": "1.0.2",
- "output": {
- "$ref": "#/components/schemas/LatentsOutput"
- }
+ "required": [
+ "key",
+ "hash",
+ "path",
+ "file_size",
+ "name",
+ "description",
+ "source",
+ "source_type",
+ "source_api_response",
+ "source_url",
+ "cover_image",
+ "type",
+ "trigger_phrases",
+ "default_settings",
+ "format",
+ "base",
+ "variant"
+ ],
+ "title": "LoRA_LyCORIS_Flux2_Config",
+ "description": "Model config for FLUX.2 (Klein) LoRA models in LyCORIS format."
},
- "LatentsMetaOutput": {
- "class": "output",
- "description": "Latents + metadata",
+ "LoRA_LyCORIS_QwenImage_Config": {
"properties": {
- "metadata": {
- "$ref": "#/components/schemas/MetadataField",
- "description": "Metadata Dict",
- "field_kind": "output",
- "ui_hidden": false
+ "key": {
+ "type": "string",
+ "title": "Key",
+ "description": "A unique key for this model."
},
- "type": {
- "const": "latents_meta_output",
- "default": "latents_meta_output",
- "field_kind": "node_attribute",
- "title": "type",
- "type": "string"
+ "hash": {
+ "type": "string",
+ "title": "Hash",
+ "description": "The hash of the model file(s)."
},
- "latents": {
- "$ref": "#/components/schemas/LatentsField",
- "description": "Latents tensor",
- "field_kind": "output",
- "ui_hidden": false
+ "path": {
+ "type": "string",
+ "title": "Path",
+ "description": "Path to the model on the filesystem. Relative paths are relative to the Invoke root directory."
},
- "width": {
- "description": "Width of output (px)",
- "field_kind": "output",
- "title": "Width",
+ "file_size": {
"type": "integer",
- "ui_hidden": false
+ "title": "File Size",
+ "description": "The size of the model in bytes."
},
- "height": {
- "description": "Height of output (px)",
- "field_kind": "output",
- "title": "Height",
- "type": "integer",
- "ui_hidden": false
- }
- },
- "required": ["output_meta", "metadata", "type", "latents", "width", "height", "type"],
- "title": "LatentsMetaOutput",
- "type": "object"
- },
- "LatentsOutput": {
- "class": "output",
- "description": "Base class for nodes that output a single latents tensor",
- "properties": {
- "latents": {
- "$ref": "#/components/schemas/LatentsField",
- "description": "Latents tensor",
- "field_kind": "output",
- "ui_hidden": false
+ "name": {
+ "type": "string",
+ "title": "Name",
+ "description": "Name of the model."
},
- "width": {
- "description": "Width of output (px)",
- "field_kind": "output",
- "title": "Width",
- "type": "integer",
- "ui_hidden": false
+ "description": {
+ "anyOf": [
+ {
+ "type": "string"
+ },
+ {
+ "type": "null"
+ }
+ ],
+ "title": "Description",
+ "description": "Model description"
},
- "height": {
- "description": "Height of output (px)",
- "field_kind": "output",
- "title": "Height",
- "type": "integer",
- "ui_hidden": false
+ "source": {
+ "type": "string",
+ "title": "Source",
+ "description": "The original source of the model (path, URL or repo_id)."
},
- "type": {
- "const": "latents_output",
- "default": "latents_output",
- "field_kind": "node_attribute",
- "title": "type",
- "type": "string"
- }
- },
- "required": ["output_meta", "latents", "width", "height", "type", "type"],
- "title": "LatentsOutput",
- "type": "object"
- },
- "LatentsToImageInvocation": {
- "category": "latents",
- "class": "invocation",
- "classification": "stable",
- "description": "Generates an image from latents.",
- "node_pack": "invokeai",
- "properties": {
- "board": {
+ "source_type": {
+ "$ref": "#/components/schemas/ModelSourceType",
+ "description": "The type of source"
+ },
+ "source_api_response": {
"anyOf": [
{
- "$ref": "#/components/schemas/BoardField"
+ "type": "string"
},
{
"type": "null"
}
],
- "default": null,
- "description": "The board to save the image to",
- "field_kind": "internal",
- "input": "direct",
- "orig_required": false,
- "ui_hidden": false
+ "title": "Source Api Response",
+ "description": "The original API response from the source, as stringified JSON."
},
- "metadata": {
+ "source_url": {
"anyOf": [
{
- "$ref": "#/components/schemas/MetadataField"
+ "type": "string"
},
{
"type": "null"
}
],
- "default": null,
- "description": "Optional metadata to be saved with the image",
- "field_kind": "internal",
- "input": "connection",
- "orig_required": false,
- "ui_hidden": false
- },
- "id": {
- "description": "The id of this instance of an invocation. Must be unique among all instances of invocations.",
- "field_kind": "node_attribute",
- "title": "Id",
- "type": "string"
+ "title": "Source Url",
+ "description": "Optional URL for the model (e.g. download page or model page)."
},
- "is_intermediate": {
- "default": false,
- "description": "Whether or not this is an intermediate invocation.",
- "field_kind": "node_attribute",
- "input": "direct",
- "orig_required": true,
- "title": "Is Intermediate",
- "type": "boolean",
- "ui_hidden": false,
- "ui_type": "IsIntermediate"
+ "cover_image": {
+ "anyOf": [
+ {
+ "type": "string"
+ },
+ {
+ "type": "null"
+ }
+ ],
+ "title": "Cover Image",
+ "description": "Url for image to preview model"
},
- "use_cache": {
- "default": true,
- "description": "Whether or not to use the cache",
- "field_kind": "node_attribute",
- "title": "Use Cache",
- "type": "boolean"
+ "type": {
+ "type": "string",
+ "const": "lora",
+ "title": "Type",
+ "default": "lora"
},
- "latents": {
+ "trigger_phrases": {
"anyOf": [
{
- "$ref": "#/components/schemas/LatentsField"
+ "items": {
+ "type": "string"
+ },
+ "type": "array",
+ "uniqueItems": true
},
{
"type": "null"
}
],
- "default": null,
- "description": "Latents tensor",
- "field_kind": "input",
- "input": "connection",
- "orig_required": true
+ "title": "Trigger Phrases",
+ "description": "Set of trigger phrases for this model"
},
- "vae": {
+ "default_settings": {
"anyOf": [
{
- "$ref": "#/components/schemas/VAEField"
+ "$ref": "#/components/schemas/LoraModelDefaultSettings"
},
{
"type": "null"
}
],
- "default": null,
- "description": "VAE",
- "field_kind": "input",
- "input": "connection",
- "orig_required": true
- },
- "tiled": {
- "default": false,
- "description": "Processing using overlapping tiles (reduce memory consumption)",
- "field_kind": "input",
- "input": "any",
- "orig_default": false,
- "orig_required": false,
- "title": "Tiled",
- "type": "boolean"
- },
- "tile_size": {
- "default": 0,
- "description": "The tile size for VAE tiling in pixels (image space). If set to 0, the default tile size for the model will be used. Larger tile sizes generally produce better results at the cost of higher memory usage.",
- "field_kind": "input",
- "input": "any",
- "multipleOf": 8,
- "orig_default": 0,
- "orig_required": false,
- "title": "Tile Size",
- "type": "integer"
+ "description": "Default settings for this model"
},
- "fp32": {
- "default": false,
- "description": "Whether or not to use full float32 precision",
- "field_kind": "input",
- "input": "any",
- "orig_default": false,
- "orig_required": false,
- "title": "Fp32",
- "type": "boolean"
+ "format": {
+ "type": "string",
+ "const": "lycoris",
+ "title": "Format",
+ "default": "lycoris"
},
- "type": {
- "const": "l2i",
- "default": "l2i",
- "field_kind": "node_attribute",
- "title": "type",
- "type": "string"
+ "base": {
+ "type": "string",
+ "const": "qwen-image",
+ "title": "Base",
+ "default": "qwen-image"
}
},
- "required": ["type", "id"],
- "tags": ["latents", "image", "vae", "l2i"],
- "title": "Latents to Image - SD1.5, SDXL",
"type": "object",
- "version": "1.3.2",
- "output": {
- "$ref": "#/components/schemas/ImageOutput"
- }
+ "required": [
+ "key",
+ "hash",
+ "path",
+ "file_size",
+ "name",
+ "description",
+ "source",
+ "source_type",
+ "source_api_response",
+ "source_url",
+ "cover_image",
+ "type",
+ "trigger_phrases",
+ "default_settings",
+ "format",
+ "base"
+ ],
+ "title": "LoRA_LyCORIS_QwenImage_Config",
+ "description": "Model config for Qwen Image Edit LoRA models in LyCORIS format."
},
- "LineartAnimeEdgeDetectionInvocation": {
- "category": "controlnet_preprocessors",
- "class": "invocation",
- "classification": "stable",
- "description": "Geneartes an edge map using the Lineart model.",
- "node_pack": "invokeai",
+ "LoRA_LyCORIS_SD1_Config": {
"properties": {
- "board": {
+ "key": {
+ "type": "string",
+ "title": "Key",
+ "description": "A unique key for this model."
+ },
+ "hash": {
+ "type": "string",
+ "title": "Hash",
+ "description": "The hash of the model file(s)."
+ },
+ "path": {
+ "type": "string",
+ "title": "Path",
+ "description": "Path to the model on the filesystem. Relative paths are relative to the Invoke root directory."
+ },
+ "file_size": {
+ "type": "integer",
+ "title": "File Size",
+ "description": "The size of the model in bytes."
+ },
+ "name": {
+ "type": "string",
+ "title": "Name",
+ "description": "Name of the model."
+ },
+ "description": {
"anyOf": [
{
- "$ref": "#/components/schemas/BoardField"
+ "type": "string"
},
{
"type": "null"
}
],
- "default": null,
- "description": "The board to save the image to",
- "field_kind": "internal",
- "input": "direct",
- "orig_required": false,
- "ui_hidden": false
+ "title": "Description",
+ "description": "Model description"
},
- "metadata": {
+ "source": {
+ "type": "string",
+ "title": "Source",
+ "description": "The original source of the model (path, URL or repo_id)."
+ },
+ "source_type": {
+ "$ref": "#/components/schemas/ModelSourceType",
+ "description": "The type of source"
+ },
+ "source_api_response": {
"anyOf": [
{
- "$ref": "#/components/schemas/MetadataField"
+ "type": "string"
},
{
"type": "null"
}
],
- "default": null,
- "description": "Optional metadata to be saved with the image",
- "field_kind": "internal",
- "input": "connection",
- "orig_required": false,
- "ui_hidden": false
+ "title": "Source Api Response",
+ "description": "The original API response from the source, as stringified JSON."
},
- "id": {
- "description": "The id of this instance of an invocation. Must be unique among all instances of invocations.",
- "field_kind": "node_attribute",
- "title": "Id",
- "type": "string"
+ "source_url": {
+ "anyOf": [
+ {
+ "type": "string"
+ },
+ {
+ "type": "null"
+ }
+ ],
+ "title": "Source Url",
+ "description": "Optional URL for the model (e.g. download page or model page)."
},
- "is_intermediate": {
- "default": false,
- "description": "Whether or not this is an intermediate invocation.",
- "field_kind": "node_attribute",
- "input": "direct",
- "orig_required": true,
- "title": "Is Intermediate",
- "type": "boolean",
- "ui_hidden": false,
- "ui_type": "IsIntermediate"
+ "cover_image": {
+ "anyOf": [
+ {
+ "type": "string"
+ },
+ {
+ "type": "null"
+ }
+ ],
+ "title": "Cover Image",
+ "description": "Url for image to preview model"
},
- "use_cache": {
- "default": true,
- "description": "Whether or not to use the cache",
- "field_kind": "node_attribute",
- "title": "Use Cache",
- "type": "boolean"
+ "type": {
+ "type": "string",
+ "const": "lora",
+ "title": "Type",
+ "default": "lora"
},
- "image": {
+ "trigger_phrases": {
"anyOf": [
{
- "$ref": "#/components/schemas/ImageField"
+ "items": {
+ "type": "string"
+ },
+ "type": "array",
+ "uniqueItems": true
},
{
"type": "null"
}
],
- "default": null,
- "description": "The image to process",
- "field_kind": "input",
- "input": "any",
- "orig_required": true
+ "title": "Trigger Phrases",
+ "description": "Set of trigger phrases for this model"
},
- "type": {
- "const": "lineart_anime_edge_detection",
- "default": "lineart_anime_edge_detection",
- "field_kind": "node_attribute",
- "title": "type",
- "type": "string"
+ "default_settings": {
+ "anyOf": [
+ {
+ "$ref": "#/components/schemas/LoraModelDefaultSettings"
+ },
+ {
+ "type": "null"
+ }
+ ],
+ "description": "Default settings for this model"
+ },
+ "format": {
+ "type": "string",
+ "const": "lycoris",
+ "title": "Format",
+ "default": "lycoris"
+ },
+ "base": {
+ "type": "string",
+ "const": "sd-1",
+ "title": "Base",
+ "default": "sd-1"
}
},
- "required": ["type", "id"],
- "tags": ["controlnet", "lineart"],
- "title": "Lineart Anime Edge Detection",
"type": "object",
- "version": "1.0.0",
- "output": {
- "$ref": "#/components/schemas/ImageOutput"
- }
+ "required": [
+ "key",
+ "hash",
+ "path",
+ "file_size",
+ "name",
+ "description",
+ "source",
+ "source_type",
+ "source_api_response",
+ "source_url",
+ "cover_image",
+ "type",
+ "trigger_phrases",
+ "default_settings",
+ "format",
+ "base"
+ ],
+ "title": "LoRA_LyCORIS_SD1_Config"
},
- "LineartEdgeDetectionInvocation": {
- "category": "controlnet_preprocessors",
- "class": "invocation",
- "classification": "stable",
- "description": "Generates an edge map using the Lineart model.",
- "node_pack": "invokeai",
+ "LoRA_LyCORIS_SD2_Config": {
"properties": {
- "board": {
+ "key": {
+ "type": "string",
+ "title": "Key",
+ "description": "A unique key for this model."
+ },
+ "hash": {
+ "type": "string",
+ "title": "Hash",
+ "description": "The hash of the model file(s)."
+ },
+ "path": {
+ "type": "string",
+ "title": "Path",
+ "description": "Path to the model on the filesystem. Relative paths are relative to the Invoke root directory."
+ },
+ "file_size": {
+ "type": "integer",
+ "title": "File Size",
+ "description": "The size of the model in bytes."
+ },
+ "name": {
+ "type": "string",
+ "title": "Name",
+ "description": "Name of the model."
+ },
+ "description": {
"anyOf": [
{
- "$ref": "#/components/schemas/BoardField"
+ "type": "string"
},
{
"type": "null"
}
],
- "default": null,
- "description": "The board to save the image to",
- "field_kind": "internal",
- "input": "direct",
- "orig_required": false,
- "ui_hidden": false
+ "title": "Description",
+ "description": "Model description"
},
- "metadata": {
+ "source": {
+ "type": "string",
+ "title": "Source",
+ "description": "The original source of the model (path, URL or repo_id)."
+ },
+ "source_type": {
+ "$ref": "#/components/schemas/ModelSourceType",
+ "description": "The type of source"
+ },
+ "source_api_response": {
"anyOf": [
{
- "$ref": "#/components/schemas/MetadataField"
+ "type": "string"
},
{
"type": "null"
}
],
- "default": null,
- "description": "Optional metadata to be saved with the image",
- "field_kind": "internal",
- "input": "connection",
- "orig_required": false,
- "ui_hidden": false
- },
- "id": {
- "description": "The id of this instance of an invocation. Must be unique among all instances of invocations.",
- "field_kind": "node_attribute",
- "title": "Id",
- "type": "string"
- },
- "is_intermediate": {
- "default": false,
- "description": "Whether or not this is an intermediate invocation.",
- "field_kind": "node_attribute",
- "input": "direct",
- "orig_required": true,
- "title": "Is Intermediate",
- "type": "boolean",
- "ui_hidden": false,
- "ui_type": "IsIntermediate"
- },
- "use_cache": {
- "default": true,
- "description": "Whether or not to use the cache",
- "field_kind": "node_attribute",
- "title": "Use Cache",
- "type": "boolean"
+ "title": "Source Api Response",
+ "description": "The original API response from the source, as stringified JSON."
},
- "image": {
+ "source_url": {
"anyOf": [
{
- "$ref": "#/components/schemas/ImageField"
+ "type": "string"
},
{
"type": "null"
}
],
- "default": null,
- "description": "The image to process",
- "field_kind": "input",
- "input": "any",
- "orig_required": true
+ "title": "Source Url",
+ "description": "Optional URL for the model (e.g. download page or model page)."
},
- "coarse": {
- "default": false,
- "description": "Whether to use coarse mode",
- "field_kind": "input",
- "input": "any",
- "orig_default": false,
- "orig_required": false,
- "title": "Coarse",
- "type": "boolean"
+ "cover_image": {
+ "anyOf": [
+ {
+ "type": "string"
+ },
+ {
+ "type": "null"
+ }
+ ],
+ "title": "Cover Image",
+ "description": "Url for image to preview model"
},
"type": {
- "const": "lineart_edge_detection",
- "default": "lineart_edge_detection",
- "field_kind": "node_attribute",
- "title": "type",
- "type": "string"
- }
- },
- "required": ["type", "id"],
- "tags": ["controlnet", "lineart"],
- "title": "Lineart Edge Detection",
- "type": "object",
- "version": "1.0.0",
- "output": {
- "$ref": "#/components/schemas/ImageOutput"
- }
- },
- "LlavaOnevisionVllmInvocation": {
- "category": "multimodal",
- "class": "invocation",
- "classification": "beta",
- "description": "Run a LLaVA OneVision VLLM model.",
- "node_pack": "invokeai",
- "properties": {
- "id": {
- "description": "The id of this instance of an invocation. Must be unique among all instances of invocations.",
- "field_kind": "node_attribute",
- "title": "Id",
- "type": "string"
- },
- "is_intermediate": {
- "default": false,
- "description": "Whether or not this is an intermediate invocation.",
- "field_kind": "node_attribute",
- "input": "direct",
- "orig_required": true,
- "title": "Is Intermediate",
- "type": "boolean",
- "ui_hidden": false,
- "ui_type": "IsIntermediate"
- },
- "use_cache": {
- "default": true,
- "description": "Whether or not to use the cache",
- "field_kind": "node_attribute",
- "title": "Use Cache",
- "type": "boolean"
+ "type": "string",
+ "const": "lora",
+ "title": "Type",
+ "default": "lora"
},
- "images": {
+ "trigger_phrases": {
"anyOf": [
{
- "anyOf": [
- {
- "items": {
- "$ref": "#/components/schemas/ImageField"
- },
- "type": "array"
- },
- {
- "$ref": "#/components/schemas/ImageField"
- }
- ],
- "maxLength": 3
+ "items": {
+ "type": "string"
+ },
+ "type": "array",
+ "uniqueItems": true
},
{
"type": "null"
}
],
- "default": null,
- "description": "Input image.",
- "field_kind": "input",
- "input": "any",
- "orig_default": null,
- "orig_required": false,
- "title": "Images"
- },
- "prompt": {
- "default": "",
- "description": "Input text prompt.",
- "field_kind": "input",
- "input": "any",
- "orig_default": "",
- "orig_required": false,
- "title": "Prompt",
- "type": "string",
- "ui_component": "textarea"
+ "title": "Trigger Phrases",
+ "description": "Set of trigger phrases for this model"
},
- "vllm_model": {
+ "default_settings": {
"anyOf": [
{
- "$ref": "#/components/schemas/ModelIdentifierField"
+ "$ref": "#/components/schemas/LoraModelDefaultSettings"
},
{
"type": "null"
}
],
- "default": null,
- "description": "The VLLM model to use",
- "field_kind": "input",
- "input": "any",
- "orig_required": true,
- "title": "LLaVA Model Type",
- "ui_model_type": ["llava_onevision"]
+ "description": "Default settings for this model"
},
- "type": {
- "const": "llava_onevision_vllm",
- "default": "llava_onevision_vllm",
- "field_kind": "node_attribute",
- "title": "type",
- "type": "string"
+ "format": {
+ "type": "string",
+ "const": "lycoris",
+ "title": "Format",
+ "default": "lycoris"
+ },
+ "base": {
+ "type": "string",
+ "const": "sd-2",
+ "title": "Base",
+ "default": "sd-2"
}
},
- "required": ["type", "id"],
- "tags": ["vllm"],
- "title": "LLaVA OneVision VLLM",
"type": "object",
- "version": "1.0.0",
- "output": {
- "$ref": "#/components/schemas/StringOutput"
- }
+ "required": [
+ "key",
+ "hash",
+ "path",
+ "file_size",
+ "name",
+ "description",
+ "source",
+ "source_type",
+ "source_api_response",
+ "source_url",
+ "cover_image",
+ "type",
+ "trigger_phrases",
+ "default_settings",
+ "format",
+ "base"
+ ],
+ "title": "LoRA_LyCORIS_SD2_Config"
},
- "LlavaOnevision_Diffusers_Config": {
+ "LoRA_LyCORIS_SDXL_Config": {
"properties": {
"key": {
"type": "string",
@@ -44581,39 +49741,50 @@
"title": "Cover Image",
"description": "Url for image to preview model"
},
- "format": {
- "type": "string",
- "const": "diffusers",
- "title": "Format",
- "default": "diffusers"
- },
- "repo_variant": {
- "$ref": "#/components/schemas/ModelRepoVariant",
- "default": ""
- },
"type": {
"type": "string",
- "const": "llava_onevision",
+ "const": "lora",
"title": "Type",
- "default": "llava_onevision"
+ "default": "lora"
},
- "base": {
- "type": "string",
- "const": "any",
- "title": "Base",
- "default": "any"
+ "trigger_phrases": {
+ "anyOf": [
+ {
+ "items": {
+ "type": "string"
+ },
+ "type": "array",
+ "uniqueItems": true
+ },
+ {
+ "type": "null"
+ }
+ ],
+ "title": "Trigger Phrases",
+ "description": "Set of trigger phrases for this model"
},
- "cpu_only": {
+ "default_settings": {
"anyOf": [
{
- "type": "boolean"
+ "$ref": "#/components/schemas/LoraModelDefaultSettings"
},
{
"type": "null"
}
],
- "title": "Cpu Only",
- "description": "Whether this model should run on CPU only"
+ "description": "Default settings for this model"
+ },
+ "format": {
+ "type": "string",
+ "const": "lycoris",
+ "title": "Format",
+ "default": "lycoris"
+ },
+ "base": {
+ "type": "string",
+ "const": "sdxl",
+ "title": "Base",
+ "default": "sdxl"
}
},
"type": "object",
@@ -44629,437 +49800,356 @@
"source_api_response",
"source_url",
"cover_image",
- "format",
- "repo_variant",
"type",
- "base",
- "cpu_only"
+ "trigger_phrases",
+ "default_settings",
+ "format",
+ "base"
],
- "title": "LlavaOnevision_Diffusers_Config",
- "description": "Model config for Llava Onevision models."
+ "title": "LoRA_LyCORIS_SDXL_Config"
},
- "LoRACollectionLoader": {
- "category": "model",
- "class": "invocation",
- "classification": "stable",
- "description": "Applies a collection of LoRAs to the provided UNet and CLIP models.",
- "node_pack": "invokeai",
+ "LoRA_LyCORIS_Wan_Config": {
"properties": {
- "id": {
- "description": "The id of this instance of an invocation. Must be unique among all instances of invocations.",
- "field_kind": "node_attribute",
- "title": "Id",
- "type": "string"
+ "key": {
+ "type": "string",
+ "title": "Key",
+ "description": "A unique key for this model."
},
- "is_intermediate": {
- "default": false,
- "description": "Whether or not this is an intermediate invocation.",
- "field_kind": "node_attribute",
- "input": "direct",
- "orig_required": true,
- "title": "Is Intermediate",
- "type": "boolean",
- "ui_hidden": false,
- "ui_type": "IsIntermediate"
+ "hash": {
+ "type": "string",
+ "title": "Hash",
+ "description": "The hash of the model file(s)."
},
- "use_cache": {
- "default": true,
- "description": "Whether or not to use the cache",
- "field_kind": "node_attribute",
- "title": "Use Cache",
- "type": "boolean"
+ "path": {
+ "type": "string",
+ "title": "Path",
+ "description": "Path to the model on the filesystem. Relative paths are relative to the Invoke root directory."
},
- "loras": {
+ "file_size": {
+ "type": "integer",
+ "title": "File Size",
+ "description": "The size of the model in bytes."
+ },
+ "name": {
+ "type": "string",
+ "title": "Name",
+ "description": "Name of the model."
+ },
+ "description": {
"anyOf": [
{
- "$ref": "#/components/schemas/LoRAField"
+ "type": "string"
+ },
+ {
+ "type": "null"
+ }
+ ],
+ "title": "Description",
+ "description": "Model description"
+ },
+ "source": {
+ "type": "string",
+ "title": "Source",
+ "description": "The original source of the model (path, URL or repo_id)."
+ },
+ "source_type": {
+ "$ref": "#/components/schemas/ModelSourceType",
+ "description": "The type of source"
+ },
+ "source_api_response": {
+ "anyOf": [
+ {
+ "type": "string"
+ },
+ {
+ "type": "null"
+ }
+ ],
+ "title": "Source Api Response",
+ "description": "The original API response from the source, as stringified JSON."
+ },
+ "source_url": {
+ "anyOf": [
+ {
+ "type": "string"
+ },
+ {
+ "type": "null"
+ }
+ ],
+ "title": "Source Url",
+ "description": "Optional URL for the model (e.g. download page or model page)."
+ },
+ "cover_image": {
+ "anyOf": [
+ {
+ "type": "string"
},
+ {
+ "type": "null"
+ }
+ ],
+ "title": "Cover Image",
+ "description": "Url for image to preview model"
+ },
+ "type": {
+ "type": "string",
+ "const": "lora",
+ "title": "Type",
+ "default": "lora"
+ },
+ "trigger_phrases": {
+ "anyOf": [
{
"items": {
- "$ref": "#/components/schemas/LoRAField"
+ "type": "string"
},
- "type": "array"
+ "type": "array",
+ "uniqueItems": true
},
{
"type": "null"
}
],
- "default": null,
- "description": "LoRA models and weights. May be a single LoRA or collection.",
- "field_kind": "input",
- "input": "any",
- "orig_default": null,
- "orig_required": false,
- "title": "LoRAs",
- "ui_model_base": ["sd-1", "sd-2"],
- "ui_model_type": ["lora"]
+ "title": "Trigger Phrases",
+ "description": "Set of trigger phrases for this model"
},
- "unet": {
+ "default_settings": {
"anyOf": [
{
- "$ref": "#/components/schemas/UNetField"
+ "$ref": "#/components/schemas/LoraModelDefaultSettings"
},
{
"type": "null"
}
],
- "default": null,
- "description": "UNet (scheduler, LoRAs)",
- "field_kind": "input",
- "input": "connection",
- "orig_default": null,
- "orig_required": false,
- "title": "UNet"
+ "description": "Default settings for this model"
},
- "clip": {
+ "format": {
+ "type": "string",
+ "const": "lycoris",
+ "title": "Format",
+ "default": "lycoris"
+ },
+ "base": {
+ "type": "string",
+ "const": "wan",
+ "title": "Base",
+ "default": "wan"
+ },
+ "expert": {
"anyOf": [
{
- "$ref": "#/components/schemas/CLIPField"
+ "type": "string",
+ "enum": ["high", "low"]
},
{
"type": "null"
}
],
- "default": null,
- "description": "CLIP (tokenizer, text encoder, LoRAs) and skipped layer count",
- "field_kind": "input",
- "input": "connection",
- "orig_default": null,
- "orig_required": false,
- "title": "CLIP"
+ "title": "Expert",
+ "description": "For Wan 2.2 A14B dual-expert LoRAs: 'high' targets the high-noise expert, 'low' targets the low-noise expert. None means the LoRA is expert-agnostic (TI2V-5B, or community LoRAs without explicit tagging) and is applied to both."
},
- "type": {
- "const": "lora_collection_loader",
- "default": "lora_collection_loader",
- "field_kind": "node_attribute",
- "title": "type",
- "type": "string"
+ "variant": {
+ "anyOf": [
+ {
+ "$ref": "#/components/schemas/WanLoRAVariantType"
+ },
+ {
+ "type": "null"
+ }
+ ],
+ "description": "The Wan model family this LoRA targets, detected from its inner-dim (5120 -> A14B, 3072 -> TI2V-5B). A14B LoRAs are incompatible with TI2V-5B mains (and vice versa) \u2014 they crash with a shape mismatch in the layer patcher. The linear-view graph builder filters LoRAs on variant when building the LoRA collection. None means the LoRA's inner-dim couldn't be identified."
}
},
- "required": ["type", "id"],
- "tags": ["model"],
- "title": "Apply LoRA Collection - SD1.5",
"type": "object",
- "version": "1.1.3",
- "output": {
- "$ref": "#/components/schemas/LoRALoaderOutput"
- }
+ "required": [
+ "key",
+ "hash",
+ "path",
+ "file_size",
+ "name",
+ "description",
+ "source",
+ "source_type",
+ "source_api_response",
+ "source_url",
+ "cover_image",
+ "type",
+ "trigger_phrases",
+ "default_settings",
+ "format",
+ "base",
+ "expert",
+ "variant"
+ ],
+ "title": "LoRA_LyCORIS_Wan_Config",
+ "description": "Model config for Wan 2.2 LoRA models in LyCORIS format.\n\nWan LoRAs target ``WanTransformer3DModel`` blocks. The Wan 2.2 A14B family\nis dual-expert (high-noise + low-noise) \u2014 LoRAs are typically trained\nagainst one expert. ``expert`` records which one so the model loader\ninvocation can wire it to the correct ``loras`` / ``loras_low_noise`` list.\nMany LoRAs are expert-agnostic (TI2V-5B family, or community LoRAs that\njust don't tag the expert) \u2014 these get ``expert=None`` and are applied to\nboth experts by default."
},
- "LoRAField": {
+ "LoRA_LyCORIS_ZImage_Config": {
"properties": {
- "lora": {
- "$ref": "#/components/schemas/ModelIdentifierField",
- "description": "Info to load lora model"
+ "key": {
+ "type": "string",
+ "title": "Key",
+ "description": "A unique key for this model."
},
- "weight": {
- "description": "Weight to apply to lora model",
- "title": "Weight",
- "type": "number"
- }
- },
- "required": ["lora", "weight"],
- "title": "LoRAField",
- "type": "object"
- },
- "LoRALoaderInvocation": {
- "category": "model",
- "class": "invocation",
- "classification": "stable",
- "description": "Apply selected lora to unet and text_encoder.",
- "node_pack": "invokeai",
- "properties": {
- "id": {
- "description": "The id of this instance of an invocation. Must be unique among all instances of invocations.",
- "field_kind": "node_attribute",
- "title": "Id",
- "type": "string"
+ "hash": {
+ "type": "string",
+ "title": "Hash",
+ "description": "The hash of the model file(s)."
},
- "is_intermediate": {
- "default": false,
- "description": "Whether or not this is an intermediate invocation.",
- "field_kind": "node_attribute",
- "input": "direct",
- "orig_required": true,
- "title": "Is Intermediate",
- "type": "boolean",
- "ui_hidden": false,
- "ui_type": "IsIntermediate"
+ "path": {
+ "type": "string",
+ "title": "Path",
+ "description": "Path to the model on the filesystem. Relative paths are relative to the Invoke root directory."
},
- "use_cache": {
- "default": true,
- "description": "Whether or not to use the cache",
- "field_kind": "node_attribute",
- "title": "Use Cache",
- "type": "boolean"
+ "file_size": {
+ "type": "integer",
+ "title": "File Size",
+ "description": "The size of the model in bytes."
},
- "lora": {
+ "name": {
+ "type": "string",
+ "title": "Name",
+ "description": "Name of the model."
+ },
+ "description": {
"anyOf": [
{
- "$ref": "#/components/schemas/ModelIdentifierField"
+ "type": "string"
},
{
"type": "null"
}
],
- "default": null,
- "description": "LoRA model to load",
- "field_kind": "input",
- "input": "any",
- "orig_required": true,
- "title": "LoRA",
- "ui_model_base": ["sd-1"],
- "ui_model_type": ["lora"]
+ "title": "Description",
+ "description": "Model description"
},
- "weight": {
- "default": 0.75,
- "description": "The weight at which the LoRA is applied to each model",
- "field_kind": "input",
- "input": "any",
- "orig_default": 0.75,
- "orig_required": false,
- "title": "Weight",
- "type": "number"
+ "source": {
+ "type": "string",
+ "title": "Source",
+ "description": "The original source of the model (path, URL or repo_id)."
},
- "unet": {
+ "source_type": {
+ "$ref": "#/components/schemas/ModelSourceType",
+ "description": "The type of source"
+ },
+ "source_api_response": {
"anyOf": [
{
- "$ref": "#/components/schemas/UNetField"
+ "type": "string"
},
{
"type": "null"
}
],
- "default": null,
- "description": "UNet (scheduler, LoRAs)",
- "field_kind": "input",
- "input": "connection",
- "orig_default": null,
- "orig_required": false,
- "title": "UNet"
+ "title": "Source Api Response",
+ "description": "The original API response from the source, as stringified JSON."
},
- "clip": {
+ "source_url": {
"anyOf": [
{
- "$ref": "#/components/schemas/CLIPField"
+ "type": "string"
},
{
"type": "null"
}
],
- "default": null,
- "description": "CLIP (tokenizer, text encoder, LoRAs) and skipped layer count",
- "field_kind": "input",
- "input": "connection",
- "orig_default": null,
- "orig_required": false,
- "title": "CLIP"
+ "title": "Source Url",
+ "description": "Optional URL for the model (e.g. download page or model page)."
},
- "type": {
- "const": "lora_loader",
- "default": "lora_loader",
- "field_kind": "node_attribute",
- "title": "type",
- "type": "string"
- }
- },
- "required": ["type", "id"],
- "tags": ["model"],
- "title": "Apply LoRA - SD1.5",
- "type": "object",
- "version": "1.0.4",
- "output": {
- "$ref": "#/components/schemas/LoRALoaderOutput"
- }
- },
- "LoRALoaderOutput": {
- "class": "output",
- "description": "Model loader output",
- "properties": {
- "unet": {
+ "cover_image": {
+ "anyOf": [
+ {
+ "type": "string"
+ },
+ {
+ "type": "null"
+ }
+ ],
+ "title": "Cover Image",
+ "description": "Url for image to preview model"
+ },
+ "type": {
+ "type": "string",
+ "const": "lora",
+ "title": "Type",
+ "default": "lora"
+ },
+ "trigger_phrases": {
"anyOf": [
{
- "$ref": "#/components/schemas/UNetField"
+ "items": {
+ "type": "string"
+ },
+ "type": "array",
+ "uniqueItems": true
},
{
"type": "null"
}
],
- "default": null,
- "description": "UNet (scheduler, LoRAs)",
- "field_kind": "output",
- "title": "UNet",
- "ui_hidden": false
+ "title": "Trigger Phrases",
+ "description": "Set of trigger phrases for this model"
},
- "clip": {
+ "default_settings": {
"anyOf": [
{
- "$ref": "#/components/schemas/CLIPField"
+ "$ref": "#/components/schemas/LoraModelDefaultSettings"
},
{
"type": "null"
}
],
- "default": null,
- "description": "CLIP (tokenizer, text encoder, LoRAs) and skipped layer count",
- "field_kind": "output",
- "title": "CLIP",
- "ui_hidden": false
- },
- "type": {
- "const": "lora_loader_output",
- "default": "lora_loader_output",
- "field_kind": "node_attribute",
- "title": "type",
- "type": "string"
- }
- },
- "required": ["output_meta", "unet", "clip", "type", "type"],
- "title": "LoRALoaderOutput",
- "type": "object"
- },
- "LoRAMetadataField": {
- "description": "LoRA Metadata Field",
- "properties": {
- "model": {
- "$ref": "#/components/schemas/ModelIdentifierField",
- "description": "LoRA model to load"
+ "description": "Default settings for this model"
},
- "weight": {
- "description": "The weight at which the LoRA is applied to each model",
- "title": "Weight",
- "type": "number"
- }
- },
- "required": ["model", "weight"],
- "title": "LoRAMetadataField",
- "type": "object"
- },
- "LoRARecallParameter": {
- "properties": {
- "model_name": {
+ "format": {
"type": "string",
- "title": "Model Name",
- "description": "The name of the LoRA model"
- },
- "weight": {
- "type": "number",
- "maximum": 10.0,
- "minimum": -10.0,
- "title": "Weight",
- "description": "The weight for the LoRA",
- "default": 0.75
- },
- "is_enabled": {
- "type": "boolean",
- "title": "Is Enabled",
- "description": "Whether the LoRA is enabled",
- "default": true
- }
- },
- "type": "object",
- "required": ["model_name"],
- "title": "LoRARecallParameter",
- "description": "LoRA configuration for recall"
- },
- "LoRASelectorInvocation": {
- "category": "model",
- "class": "invocation",
- "classification": "stable",
- "description": "Selects a LoRA model and weight.",
- "node_pack": "invokeai",
- "properties": {
- "id": {
- "description": "The id of this instance of an invocation. Must be unique among all instances of invocations.",
- "field_kind": "node_attribute",
- "title": "Id",
- "type": "string"
- },
- "is_intermediate": {
- "default": false,
- "description": "Whether or not this is an intermediate invocation.",
- "field_kind": "node_attribute",
- "input": "direct",
- "orig_required": true,
- "title": "Is Intermediate",
- "type": "boolean",
- "ui_hidden": false,
- "ui_type": "IsIntermediate"
+ "const": "lycoris",
+ "title": "Format",
+ "default": "lycoris"
},
- "use_cache": {
- "default": true,
- "description": "Whether or not to use the cache",
- "field_kind": "node_attribute",
- "title": "Use Cache",
- "type": "boolean"
+ "base": {
+ "type": "string",
+ "const": "z-image",
+ "title": "Base",
+ "default": "z-image"
},
- "lora": {
+ "variant": {
"anyOf": [
{
- "$ref": "#/components/schemas/ModelIdentifierField"
+ "$ref": "#/components/schemas/ZImageVariantType"
},
{
"type": "null"
}
- ],
- "default": null,
- "description": "LoRA model to load",
- "field_kind": "input",
- "input": "any",
- "orig_required": true,
- "title": "LoRA",
- "ui_model_type": ["lora"]
- },
- "weight": {
- "default": 0.75,
- "description": "The weight at which the LoRA is applied to each model",
- "field_kind": "input",
- "input": "any",
- "orig_default": 0.75,
- "orig_required": false,
- "title": "Weight",
- "type": "number"
- },
- "type": {
- "const": "lora_selector",
- "default": "lora_selector",
- "field_kind": "node_attribute",
- "title": "type",
- "type": "string"
+ ]
}
},
- "required": ["type", "id"],
- "tags": ["model"],
- "title": "Select LoRA",
"type": "object",
- "version": "1.0.3",
- "output": {
- "$ref": "#/components/schemas/LoRASelectorOutput"
- }
- },
- "LoRASelectorOutput": {
- "class": "output",
- "description": "Model loader output",
- "properties": {
- "lora": {
- "$ref": "#/components/schemas/LoRAField",
- "description": "LoRA model and weight",
- "field_kind": "output",
- "title": "LoRA",
- "ui_hidden": false
- },
- "type": {
- "const": "lora_selector_output",
- "default": "lora_selector_output",
- "field_kind": "node_attribute",
- "title": "type",
- "type": "string"
- }
- },
- "required": ["output_meta", "lora", "type", "type"],
- "title": "LoRASelectorOutput",
- "type": "object"
+ "required": [
+ "key",
+ "hash",
+ "path",
+ "file_size",
+ "name",
+ "description",
+ "source",
+ "source_type",
+ "source_api_response",
+ "source_url",
+ "cover_image",
+ "type",
+ "trigger_phrases",
+ "default_settings",
+ "format",
+ "base",
+ "variant"
+ ],
+ "title": "LoRA_LyCORIS_ZImage_Config",
+ "description": "Model config for Z-Image LoRA models in LyCORIS format."
},
- "LoRA_Diffusers_FLUX_Config": {
+ "LoRA_OMI_FLUX_Config": {
"properties": {
"key": {
"type": "string",
@@ -45178,9 +50268,9 @@
},
"format": {
"type": "string",
- "const": "diffusers",
+ "const": "omi",
"title": "Format",
- "default": "diffusers"
+ "default": "omi"
},
"base": {
"type": "string",
@@ -45208,9 +50298,9 @@
"format",
"base"
],
- "title": "LoRA_Diffusers_FLUX_Config"
+ "title": "LoRA_OMI_FLUX_Config"
},
- "LoRA_Diffusers_Flux2_Config": {
+ "LoRA_OMI_SDXL_Config": {
"properties": {
"key": {
"type": "string",
@@ -45285,122 +50375,452 @@
"cover_image": {
"anyOf": [
{
- "type": "string"
+ "type": "string"
+ },
+ {
+ "type": "null"
+ }
+ ],
+ "title": "Cover Image",
+ "description": "Url for image to preview model"
+ },
+ "type": {
+ "type": "string",
+ "const": "lora",
+ "title": "Type",
+ "default": "lora"
+ },
+ "trigger_phrases": {
+ "anyOf": [
+ {
+ "items": {
+ "type": "string"
+ },
+ "type": "array",
+ "uniqueItems": true
+ },
+ {
+ "type": "null"
+ }
+ ],
+ "title": "Trigger Phrases",
+ "description": "Set of trigger phrases for this model"
+ },
+ "default_settings": {
+ "anyOf": [
+ {
+ "$ref": "#/components/schemas/LoraModelDefaultSettings"
+ },
+ {
+ "type": "null"
+ }
+ ],
+ "description": "Default settings for this model"
+ },
+ "format": {
+ "type": "string",
+ "const": "omi",
+ "title": "Format",
+ "default": "omi"
+ },
+ "base": {
+ "type": "string",
+ "const": "sdxl",
+ "title": "Base",
+ "default": "sdxl"
+ }
+ },
+ "type": "object",
+ "required": [
+ "key",
+ "hash",
+ "path",
+ "file_size",
+ "name",
+ "description",
+ "source",
+ "source_type",
+ "source_api_response",
+ "source_url",
+ "cover_image",
+ "type",
+ "trigger_phrases",
+ "default_settings",
+ "format",
+ "base"
+ ],
+ "title": "LoRA_OMI_SDXL_Config"
+ },
+ "LocalModelSource": {
+ "properties": {
+ "path": {
+ "anyOf": [
+ {
+ "type": "string"
+ },
+ {
+ "type": "string",
+ "format": "path"
+ }
+ ],
+ "title": "Path"
+ },
+ "inplace": {
+ "anyOf": [
+ {
+ "type": "boolean"
+ },
+ {
+ "type": "null"
+ }
+ ],
+ "title": "Inplace",
+ "default": false
+ },
+ "type": {
+ "type": "string",
+ "const": "local",
+ "title": "Type",
+ "default": "local"
+ }
+ },
+ "type": "object",
+ "required": ["path"],
+ "title": "LocalModelSource",
+ "description": "A local file or directory path."
+ },
+ "LogLevel": {
+ "type": "integer",
+ "enum": [0, 10, 20, 30, 40, 50],
+ "title": "LogLevel"
+ },
+ "LoginRequest": {
+ "properties": {
+ "email": {
+ "type": "string",
+ "title": "Email",
+ "description": "User email address"
+ },
+ "password": {
+ "type": "string",
+ "title": "Password",
+ "description": "User password"
+ },
+ "remember_me": {
+ "type": "boolean",
+ "title": "Remember Me",
+ "description": "Whether to extend session duration",
+ "default": false
+ }
+ },
+ "type": "object",
+ "required": ["email", "password"],
+ "title": "LoginRequest",
+ "description": "Request body for user login."
+ },
+ "LoginResponse": {
+ "properties": {
+ "token": {
+ "type": "string",
+ "title": "Token",
+ "description": "JWT access token"
+ },
+ "user": {
+ "$ref": "#/components/schemas/UserDTO",
+ "description": "User information"
+ },
+ "expires_in": {
+ "type": "integer",
+ "title": "Expires In",
+ "description": "Token expiration time in seconds"
+ }
+ },
+ "type": "object",
+ "required": ["token", "user", "expires_in"],
+ "title": "LoginResponse",
+ "description": "Response from successful login."
+ },
+ "LogoutResponse": {
+ "properties": {
+ "success": {
+ "type": "boolean",
+ "title": "Success",
+ "description": "Whether logout was successful"
+ }
+ },
+ "type": "object",
+ "required": ["success"],
+ "title": "LogoutResponse",
+ "description": "Response from logout."
+ },
+ "LoraModelDefaultSettings": {
+ "properties": {
+ "weight": {
+ "anyOf": [
+ {
+ "type": "number"
+ },
+ {
+ "type": "null"
+ }
+ ],
+ "title": "Weight",
+ "description": "Default weight for this model"
+ },
+ "weight_min": {
+ "anyOf": [
+ {
+ "type": "number"
+ },
+ {
+ "type": "null"
+ }
+ ],
+ "title": "Weight Min",
+ "description": "Minimum weight slider value for this model"
+ },
+ "weight_max": {
+ "anyOf": [
+ {
+ "type": "number"
+ },
+ {
+ "type": "null"
+ }
+ ],
+ "title": "Weight Max",
+ "description": "Maximum weight slider value for this model"
+ }
+ },
+ "additionalProperties": false,
+ "type": "object",
+ "title": "LoraModelDefaultSettings"
+ },
+ "MDControlListOutput": {
+ "class": "output",
+ "properties": {
+ "control_list": {
+ "anyOf": [
+ {
+ "$ref": "#/components/schemas/ControlField"
+ },
+ {
+ "items": {
+ "$ref": "#/components/schemas/ControlField"
+ },
+ "type": "array"
+ },
+ {
+ "type": "null"
+ }
+ ],
+ "description": "ControlNet(s) to apply",
+ "field_kind": "output",
+ "title": "ControlNet-List",
+ "ui_hidden": false
+ },
+ "type": {
+ "const": "md_control_list_output",
+ "default": "md_control_list_output",
+ "field_kind": "node_attribute",
+ "title": "type",
+ "type": "string"
+ }
+ },
+ "required": ["output_meta", "control_list", "type", "type"],
+ "title": "MDControlListOutput",
+ "type": "object"
+ },
+ "MDIPAdapterListOutput": {
+ "class": "output",
+ "properties": {
+ "ip_adapter_list": {
+ "anyOf": [
+ {
+ "$ref": "#/components/schemas/IPAdapterField"
+ },
+ {
+ "items": {
+ "$ref": "#/components/schemas/IPAdapterField"
+ },
+ "type": "array"
+ },
+ {
+ "type": "null"
+ }
+ ],
+ "description": "IP-Adapter to apply",
+ "field_kind": "output",
+ "title": "IP-Adapter-List",
+ "ui_hidden": false
+ },
+ "type": {
+ "const": "md_ip_adapter_list_output",
+ "default": "md_ip_adapter_list_output",
+ "field_kind": "node_attribute",
+ "title": "type",
+ "type": "string"
+ }
+ },
+ "required": ["output_meta", "ip_adapter_list", "type", "type"],
+ "title": "MDIPAdapterListOutput",
+ "type": "object"
+ },
+ "MDT2IAdapterListOutput": {
+ "class": "output",
+ "properties": {
+ "t2i_adapter_list": {
+ "anyOf": [
+ {
+ "$ref": "#/components/schemas/T2IAdapterField"
+ },
+ {
+ "items": {
+ "$ref": "#/components/schemas/T2IAdapterField"
+ },
+ "type": "array"
},
{
"type": "null"
}
],
- "title": "Cover Image",
- "description": "Url for image to preview model"
+ "description": "T2I-Adapter(s) to apply",
+ "field_kind": "output",
+ "title": "T2I Adapter-List",
+ "ui_hidden": false
},
"type": {
- "type": "string",
- "const": "lora",
- "title": "Type",
- "default": "lora"
- },
- "trigger_phrases": {
+ "const": "md_ip_adapters_output",
+ "default": "md_ip_adapters_output",
+ "field_kind": "node_attribute",
+ "title": "type",
+ "type": "string"
+ }
+ },
+ "required": ["output_meta", "t2i_adapter_list", "type", "type"],
+ "title": "MDT2IAdapterListOutput",
+ "type": "object"
+ },
+ "MLSDDetectionInvocation": {
+ "category": "controlnet_preprocessors",
+ "class": "invocation",
+ "classification": "stable",
+ "description": "Generates an line segment map using MLSD.",
+ "node_pack": "invokeai",
+ "properties": {
+ "board": {
"anyOf": [
{
- "items": {
- "type": "string"
- },
- "type": "array",
- "uniqueItems": true
+ "$ref": "#/components/schemas/BoardField"
},
{
"type": "null"
}
],
- "title": "Trigger Phrases",
- "description": "Set of trigger phrases for this model"
+ "default": null,
+ "description": "The board to save the image to",
+ "field_kind": "internal",
+ "input": "direct",
+ "orig_required": false,
+ "ui_hidden": false
},
- "default_settings": {
+ "metadata": {
"anyOf": [
{
- "$ref": "#/components/schemas/LoraModelDefaultSettings"
+ "$ref": "#/components/schemas/MetadataField"
},
{
"type": "null"
}
],
- "description": "Default settings for this model"
+ "default": null,
+ "description": "Optional metadata to be saved with the image",
+ "field_kind": "internal",
+ "input": "connection",
+ "orig_required": false,
+ "ui_hidden": false
},
- "format": {
- "type": "string",
- "const": "diffusers",
- "title": "Format",
- "default": "diffusers"
+ "id": {
+ "description": "The id of this instance of an invocation. Must be unique among all instances of invocations.",
+ "field_kind": "node_attribute",
+ "title": "Id",
+ "type": "string"
},
- "base": {
- "type": "string",
- "const": "flux2",
- "title": "Base",
- "default": "flux2"
+ "is_intermediate": {
+ "default": false,
+ "description": "Whether or not this is an intermediate invocation.",
+ "field_kind": "node_attribute",
+ "input": "direct",
+ "orig_required": true,
+ "title": "Is Intermediate",
+ "type": "boolean",
+ "ui_hidden": false,
+ "ui_type": "IsIntermediate"
},
- "variant": {
+ "use_cache": {
+ "default": true,
+ "description": "Whether or not to use the cache",
+ "field_kind": "node_attribute",
+ "title": "Use Cache",
+ "type": "boolean"
+ },
+ "image": {
"anyOf": [
{
- "$ref": "#/components/schemas/Flux2VariantType"
+ "$ref": "#/components/schemas/ImageField"
},
{
"type": "null"
}
- ]
+ ],
+ "default": null,
+ "description": "The image to process",
+ "field_kind": "input",
+ "input": "any",
+ "orig_required": true
+ },
+ "score_threshold": {
+ "default": 0.1,
+ "description": "The threshold used to score points when determining line segments",
+ "field_kind": "input",
+ "input": "any",
+ "minimum": 0,
+ "orig_default": 0.1,
+ "orig_required": false,
+ "title": "Score Threshold",
+ "type": "number"
+ },
+ "distance_threshold": {
+ "default": 20.0,
+ "description": "Threshold for including a line segment - lines shorter than this distance will be discarded",
+ "field_kind": "input",
+ "input": "any",
+ "minimum": 0,
+ "orig_default": 20.0,
+ "orig_required": false,
+ "title": "Distance Threshold",
+ "type": "number"
+ },
+ "type": {
+ "const": "mlsd_detection",
+ "default": "mlsd_detection",
+ "field_kind": "node_attribute",
+ "title": "type",
+ "type": "string"
}
},
+ "required": ["type", "id"],
+ "tags": ["controlnet", "mlsd", "edge"],
+ "title": "MLSD Detection",
"type": "object",
- "required": [
- "key",
- "hash",
- "path",
- "file_size",
- "name",
- "description",
- "source",
- "source_type",
- "source_api_response",
- "source_url",
- "cover_image",
- "type",
- "trigger_phrases",
- "default_settings",
- "format",
- "base",
- "variant"
- ],
- "title": "LoRA_Diffusers_Flux2_Config",
- "description": "Model config for FLUX.2 (Klein) LoRA models in Diffusers format."
+ "version": "1.0.0",
+ "output": {
+ "$ref": "#/components/schemas/ImageOutput"
+ }
},
- "LoRA_Diffusers_SD1_Config": {
+ "MainModelDefaultSettings": {
"properties": {
- "key": {
- "type": "string",
- "title": "Key",
- "description": "A unique key for this model."
- },
- "hash": {
- "type": "string",
- "title": "Hash",
- "description": "The hash of the model file(s)."
- },
- "path": {
- "type": "string",
- "title": "Path",
- "description": "Path to the model on the filesystem. Relative paths are relative to the Invoke root directory."
- },
- "file_size": {
- "type": "integer",
- "title": "File Size",
- "description": "The size of the model in bytes."
- },
- "name": {
- "type": "string",
- "title": "Name",
- "description": "Name of the model."
- },
- "description": {
+ "vae": {
"anyOf": [
{
"type": "string"
@@ -45409,273 +50829,243 @@
"type": "null"
}
],
- "title": "Description",
- "description": "Model description"
- },
- "source": {
- "type": "string",
- "title": "Source",
- "description": "The original source of the model (path, URL or repo_id)."
- },
- "source_type": {
- "$ref": "#/components/schemas/ModelSourceType",
- "description": "The type of source"
+ "title": "Vae",
+ "description": "Default VAE for this model (model key)"
},
- "source_api_response": {
+ "vae_precision": {
"anyOf": [
{
- "type": "string"
+ "type": "string",
+ "enum": ["fp16", "fp32"]
},
{
"type": "null"
}
],
- "title": "Source Api Response",
- "description": "The original API response from the source, as stringified JSON."
+ "title": "Vae Precision",
+ "description": "Default VAE precision for this model"
},
- "source_url": {
+ "scheduler": {
"anyOf": [
{
- "type": "string"
+ "type": "string",
+ "enum": [
+ "ddim",
+ "ddpm",
+ "deis",
+ "deis_k",
+ "lms",
+ "lms_k",
+ "pndm",
+ "heun",
+ "heun_k",
+ "euler",
+ "euler_k",
+ "euler_a",
+ "kdpm_2",
+ "kdpm_2_k",
+ "kdpm_2_a",
+ "kdpm_2_a_k",
+ "dpmpp_2s",
+ "dpmpp_2s_k",
+ "dpmpp_2m",
+ "dpmpp_2m_k",
+ "dpmpp_2m_sde",
+ "dpmpp_2m_sde_k",
+ "dpmpp_3m",
+ "dpmpp_3m_k",
+ "dpmpp_sde",
+ "dpmpp_sde_k",
+ "er_sde",
+ "unipc",
+ "unipc_k",
+ "lcm",
+ "tcd"
+ ]
},
{
"type": "null"
}
],
- "title": "Source Url",
- "description": "Optional URL for the model (e.g. download page or model page)."
+ "title": "Scheduler",
+ "description": "Default scheduler for this model"
},
- "cover_image": {
+ "steps": {
"anyOf": [
{
- "type": "string"
+ "type": "integer",
+ "exclusiveMinimum": 0.0
},
{
"type": "null"
}
],
- "title": "Cover Image",
- "description": "Url for image to preview model"
- },
- "type": {
- "type": "string",
- "const": "lora",
- "title": "Type",
- "default": "lora"
+ "title": "Steps",
+ "description": "Default number of steps for this model"
},
- "trigger_phrases": {
+ "cfg_scale": {
"anyOf": [
{
- "items": {
- "type": "string"
- },
- "type": "array",
- "uniqueItems": true
+ "type": "number",
+ "minimum": 1.0
},
{
"type": "null"
}
],
- "title": "Trigger Phrases",
- "description": "Set of trigger phrases for this model"
+ "title": "Cfg Scale",
+ "description": "Default CFG Scale for this model"
},
- "default_settings": {
+ "cfg_rescale_multiplier": {
"anyOf": [
{
- "$ref": "#/components/schemas/LoraModelDefaultSettings"
+ "type": "number",
+ "exclusiveMaximum": 1.0,
+ "minimum": 0.0
},
{
"type": "null"
}
],
- "description": "Default settings for this model"
- },
- "format": {
- "type": "string",
- "const": "diffusers",
- "title": "Format",
- "default": "diffusers"
- },
- "base": {
- "type": "string",
- "const": "sd-1",
- "title": "Base",
- "default": "sd-1"
- }
- },
- "type": "object",
- "required": [
- "key",
- "hash",
- "path",
- "file_size",
- "name",
- "description",
- "source",
- "source_type",
- "source_api_response",
- "source_url",
- "cover_image",
- "type",
- "trigger_phrases",
- "default_settings",
- "format",
- "base"
- ],
- "title": "LoRA_Diffusers_SD1_Config"
- },
- "LoRA_Diffusers_SD2_Config": {
- "properties": {
- "key": {
- "type": "string",
- "title": "Key",
- "description": "A unique key for this model."
- },
- "hash": {
- "type": "string",
- "title": "Hash",
- "description": "The hash of the model file(s)."
- },
- "path": {
- "type": "string",
- "title": "Path",
- "description": "Path to the model on the filesystem. Relative paths are relative to the Invoke root directory."
- },
- "file_size": {
- "type": "integer",
- "title": "File Size",
- "description": "The size of the model in bytes."
- },
- "name": {
- "type": "string",
- "title": "Name",
- "description": "Name of the model."
+ "title": "Cfg Rescale Multiplier",
+ "description": "Default CFG Rescale Multiplier for this model"
},
- "description": {
+ "width": {
"anyOf": [
{
- "type": "string"
+ "type": "integer",
+ "multipleOf": 8.0,
+ "minimum": 64.0
},
{
"type": "null"
}
],
- "title": "Description",
- "description": "Model description"
- },
- "source": {
- "type": "string",
- "title": "Source",
- "description": "The original source of the model (path, URL or repo_id)."
- },
- "source_type": {
- "$ref": "#/components/schemas/ModelSourceType",
- "description": "The type of source"
+ "title": "Width",
+ "description": "Default width for this model"
},
- "source_api_response": {
+ "height": {
"anyOf": [
{
- "type": "string"
+ "type": "integer",
+ "multipleOf": 8.0,
+ "minimum": 64.0
},
{
"type": "null"
}
],
- "title": "Source Api Response",
- "description": "The original API response from the source, as stringified JSON."
+ "title": "Height",
+ "description": "Default height for this model"
},
- "source_url": {
+ "guidance": {
"anyOf": [
{
- "type": "string"
+ "type": "number",
+ "minimum": 1.0
},
{
"type": "null"
}
],
- "title": "Source Url",
- "description": "Optional URL for the model (e.g. download page or model page)."
+ "title": "Guidance",
+ "description": "Default Guidance for this model"
},
- "cover_image": {
+ "cpu_only": {
"anyOf": [
{
- "type": "string"
+ "type": "boolean"
},
{
"type": "null"
}
],
- "title": "Cover Image",
- "description": "Url for image to preview model"
- },
- "type": {
- "type": "string",
- "const": "lora",
- "title": "Type",
- "default": "lora"
+ "title": "Cpu Only",
+ "description": "Whether this model should run on CPU only"
},
- "trigger_phrases": {
+ "fp8_storage": {
"anyOf": [
{
- "items": {
- "type": "string"
- },
- "type": "array",
- "uniqueItems": true
+ "type": "boolean"
},
{
"type": "null"
}
],
- "title": "Trigger Phrases",
- "description": "Set of trigger phrases for this model"
+ "title": "Fp8 Storage",
+ "description": "Store weights in FP8 to reduce VRAM usage (~50% savings). Weights are cast to compute dtype during inference."
+ }
+ },
+ "additionalProperties": false,
+ "type": "object",
+ "title": "MainModelDefaultSettings"
+ },
+ "MainModelLoaderInvocation": {
+ "category": "model",
+ "class": "invocation",
+ "classification": "stable",
+ "description": "Loads a main model, outputting its submodels.",
+ "node_pack": "invokeai",
+ "properties": {
+ "id": {
+ "description": "The id of this instance of an invocation. Must be unique among all instances of invocations.",
+ "field_kind": "node_attribute",
+ "title": "Id",
+ "type": "string"
},
- "default_settings": {
+ "is_intermediate": {
+ "default": false,
+ "description": "Whether or not this is an intermediate invocation.",
+ "field_kind": "node_attribute",
+ "input": "direct",
+ "orig_required": true,
+ "title": "Is Intermediate",
+ "type": "boolean",
+ "ui_hidden": false,
+ "ui_type": "IsIntermediate"
+ },
+ "use_cache": {
+ "default": true,
+ "description": "Whether or not to use the cache",
+ "field_kind": "node_attribute",
+ "title": "Use Cache",
+ "type": "boolean"
+ },
+ "model": {
"anyOf": [
{
- "$ref": "#/components/schemas/LoraModelDefaultSettings"
+ "$ref": "#/components/schemas/ModelIdentifierField"
},
{
"type": "null"
}
],
- "description": "Default settings for this model"
- },
- "format": {
- "type": "string",
- "const": "diffusers",
- "title": "Format",
- "default": "diffusers"
+ "default": null,
+ "description": "Main model (UNet, VAE, CLIP) to load",
+ "field_kind": "input",
+ "input": "any",
+ "orig_required": true,
+ "ui_model_base": ["sd-1", "sd-2"],
+ "ui_model_type": ["main"]
},
- "base": {
- "type": "string",
- "const": "sd-2",
- "title": "Base",
- "default": "sd-2"
+ "type": {
+ "const": "main_model_loader",
+ "default": "main_model_loader",
+ "field_kind": "node_attribute",
+ "title": "type",
+ "type": "string"
}
},
+ "required": ["type", "id"],
+ "tags": ["model"],
+ "title": "Main Model - SD1.5, SD2",
"type": "object",
- "required": [
- "key",
- "hash",
- "path",
- "file_size",
- "name",
- "description",
- "source",
- "source_type",
- "source_api_response",
- "source_url",
- "cover_image",
- "type",
- "trigger_phrases",
- "default_settings",
- "format",
- "base"
- ],
- "title": "LoRA_Diffusers_SD2_Config"
+ "version": "1.0.4",
+ "output": {
+ "$ref": "#/components/schemas/ModelLoaderOutput"
+ }
},
- "LoRA_Diffusers_SDXL_Config": {
+ "Main_BnBNF4_FLUX_Config": {
"properties": {
"key": {
"type": "string",
@@ -45761,9 +51151,9 @@
},
"type": {
"type": "string",
- "const": "lora",
+ "const": "main",
"title": "Type",
- "default": "lora"
+ "default": "main"
},
"trigger_phrases": {
"anyOf": [
@@ -45784,7 +51174,7 @@
"default_settings": {
"anyOf": [
{
- "$ref": "#/components/schemas/LoraModelDefaultSettings"
+ "$ref": "#/components/schemas/MainModelDefaultSettings"
},
{
"type": "null"
@@ -45792,17 +51182,32 @@
],
"description": "Default settings for this model"
},
- "format": {
- "type": "string",
- "const": "diffusers",
- "title": "Format",
- "default": "diffusers"
+ "config_path": {
+ "anyOf": [
+ {
+ "type": "string"
+ },
+ {
+ "type": "null"
+ }
+ ],
+ "title": "Config Path",
+ "description": "Path to the config for this model, if any."
},
"base": {
"type": "string",
- "const": "sdxl",
+ "const": "flux",
"title": "Base",
- "default": "sdxl"
+ "default": "flux"
+ },
+ "format": {
+ "type": "string",
+ "const": "bnb_quantized_nf4b",
+ "title": "Format",
+ "default": "bnb_quantized_nf4b"
+ },
+ "variant": {
+ "$ref": "#/components/schemas/FluxVariantType"
}
},
"type": "object",
@@ -45821,12 +51226,15 @@
"type",
"trigger_phrases",
"default_settings",
+ "config_path",
+ "base",
"format",
- "base"
+ "variant"
],
- "title": "LoRA_Diffusers_SDXL_Config"
+ "title": "Main_BnBNF4_FLUX_Config",
+ "description": "Model config for main checkpoint models."
},
- "LoRA_Diffusers_ZImage_Config": {
+ "Main_Checkpoint_Anima_Config": {
"properties": {
"key": {
"type": "string",
@@ -45912,9 +51320,9 @@
},
"type": {
"type": "string",
- "const": "lora",
+ "const": "main",
"title": "Type",
- "default": "lora"
+ "default": "main"
},
"trigger_phrases": {
"anyOf": [
@@ -45935,7 +51343,7 @@
"default_settings": {
"anyOf": [
{
- "$ref": "#/components/schemas/LoraModelDefaultSettings"
+ "$ref": "#/components/schemas/MainModelDefaultSettings"
},
{
"type": "null"
@@ -45943,27 +51351,29 @@
],
"description": "Default settings for this model"
},
- "format": {
- "type": "string",
- "const": "diffusers",
- "title": "Format",
- "default": "diffusers"
- },
- "base": {
- "type": "string",
- "const": "z-image",
- "title": "Base",
- "default": "z-image"
- },
- "variant": {
+ "config_path": {
"anyOf": [
{
- "$ref": "#/components/schemas/ZImageVariantType"
+ "type": "string"
},
{
"type": "null"
}
- ]
+ ],
+ "title": "Config Path",
+ "description": "Path to the config for this model, if any."
+ },
+ "base": {
+ "type": "string",
+ "const": "anima",
+ "title": "Base",
+ "default": "anima"
+ },
+ "format": {
+ "type": "string",
+ "const": "checkpoint",
+ "title": "Format",
+ "default": "checkpoint"
}
},
"type": "object",
@@ -45982,14 +51392,14 @@
"type",
"trigger_phrases",
"default_settings",
- "format",
+ "config_path",
"base",
- "variant"
+ "format"
],
- "title": "LoRA_Diffusers_ZImage_Config",
- "description": "Model config for Z-Image LoRA models in Diffusers format."
+ "title": "Main_Checkpoint_Anima_Config",
+ "description": "Model config for Anima single-file checkpoint models (safetensors).\n\nAnima is built on NVIDIA Cosmos Predict2 DiT with a custom LLM Adapter\nthat bridges Qwen3 0.6B text encoder outputs to the DiT."
},
- "LoRA_LyCORIS_Anima_Config": {
+ "Main_Checkpoint_FLUX_Config": {
"properties": {
"key": {
"type": "string",
@@ -46075,9 +51485,9 @@
},
"type": {
"type": "string",
- "const": "lora",
+ "const": "main",
"title": "Type",
- "default": "lora"
+ "default": "main"
},
"trigger_phrases": {
"anyOf": [
@@ -46098,7 +51508,7 @@
"default_settings": {
"anyOf": [
{
- "$ref": "#/components/schemas/LoraModelDefaultSettings"
+ "$ref": "#/components/schemas/MainModelDefaultSettings"
},
{
"type": "null"
@@ -46106,17 +51516,32 @@
],
"description": "Default settings for this model"
},
+ "config_path": {
+ "anyOf": [
+ {
+ "type": "string"
+ },
+ {
+ "type": "null"
+ }
+ ],
+ "title": "Config Path",
+ "description": "Path to the config for this model, if any."
+ },
"format": {
"type": "string",
- "const": "lycoris",
+ "const": "checkpoint",
"title": "Format",
- "default": "lycoris"
+ "default": "checkpoint"
},
"base": {
"type": "string",
- "const": "anima",
+ "const": "flux",
"title": "Base",
- "default": "anima"
+ "default": "flux"
+ },
+ "variant": {
+ "$ref": "#/components/schemas/FluxVariantType"
}
},
"type": "object",
@@ -46135,13 +51560,15 @@
"type",
"trigger_phrases",
"default_settings",
+ "config_path",
"format",
- "base"
+ "base",
+ "variant"
],
- "title": "LoRA_LyCORIS_Anima_Config",
- "description": "Model config for Anima LoRA models in LyCORIS format."
+ "title": "Main_Checkpoint_FLUX_Config",
+ "description": "Model config for main checkpoint models."
},
- "LoRA_LyCORIS_FLUX_Config": {
+ "Main_Checkpoint_Flux2_Config": {
"properties": {
"key": {
"type": "string",
@@ -46227,9 +51654,9 @@
},
"type": {
"type": "string",
- "const": "lora",
+ "const": "main",
"title": "Type",
- "default": "lora"
+ "default": "main"
},
"trigger_phrases": {
"anyOf": [
@@ -46250,7 +51677,7 @@
"default_settings": {
"anyOf": [
{
- "$ref": "#/components/schemas/LoraModelDefaultSettings"
+ "$ref": "#/components/schemas/MainModelDefaultSettings"
},
{
"type": "null"
@@ -46258,17 +51685,32 @@
],
"description": "Default settings for this model"
},
+ "config_path": {
+ "anyOf": [
+ {
+ "type": "string"
+ },
+ {
+ "type": "null"
+ }
+ ],
+ "title": "Config Path",
+ "description": "Path to the config for this model, if any."
+ },
"format": {
"type": "string",
- "const": "lycoris",
+ "const": "checkpoint",
"title": "Format",
- "default": "lycoris"
+ "default": "checkpoint"
},
"base": {
"type": "string",
- "const": "flux",
+ "const": "flux2",
"title": "Base",
- "default": "flux"
+ "default": "flux2"
+ },
+ "variant": {
+ "$ref": "#/components/schemas/Flux2VariantType"
}
},
"type": "object",
@@ -46287,12 +51729,15 @@
"type",
"trigger_phrases",
"default_settings",
+ "config_path",
"format",
- "base"
+ "base",
+ "variant"
],
- "title": "LoRA_LyCORIS_FLUX_Config"
+ "title": "Main_Checkpoint_Flux2_Config",
+ "description": "Model config for FLUX.2 checkpoint models (e.g. Klein)."
},
- "LoRA_LyCORIS_Flux2_Config": {
+ "Main_Checkpoint_QwenImage_Config": {
"properties": {
"key": {
"type": "string",
@@ -46378,9 +51823,9 @@
},
"type": {
"type": "string",
- "const": "lora",
+ "const": "main",
"title": "Type",
- "default": "lora"
+ "default": "main"
},
"trigger_phrases": {
"anyOf": [
@@ -46401,7 +51846,7 @@
"default_settings": {
"anyOf": [
{
- "$ref": "#/components/schemas/LoraModelDefaultSettings"
+ "$ref": "#/components/schemas/MainModelDefaultSettings"
},
{
"type": "null"
@@ -46409,22 +51854,34 @@
],
"description": "Default settings for this model"
},
- "format": {
- "type": "string",
- "const": "lycoris",
- "title": "Format",
- "default": "lycoris"
+ "config_path": {
+ "anyOf": [
+ {
+ "type": "string"
+ },
+ {
+ "type": "null"
+ }
+ ],
+ "title": "Config Path",
+ "description": "Path to the config for this model, if any."
},
"base": {
"type": "string",
- "const": "flux2",
+ "const": "qwen-image",
"title": "Base",
- "default": "flux2"
+ "default": "qwen-image"
+ },
+ "format": {
+ "type": "string",
+ "const": "checkpoint",
+ "title": "Format",
+ "default": "checkpoint"
},
"variant": {
"anyOf": [
{
- "$ref": "#/components/schemas/Flux2VariantType"
+ "$ref": "#/components/schemas/QwenImageVariantType"
},
{
"type": "null"
@@ -46448,14 +51905,15 @@
"type",
"trigger_phrases",
"default_settings",
- "format",
+ "config_path",
"base",
+ "format",
"variant"
],
- "title": "LoRA_LyCORIS_Flux2_Config",
- "description": "Model config for FLUX.2 (Klein) LoRA models in LyCORIS format."
+ "title": "Main_Checkpoint_QwenImage_Config",
+ "description": "Model config for Qwen Image single-file checkpoint models (safetensors, etc).\n\nCovers both raw bf16/fp16 checkpoints and ComfyUI-style fp8_scaled checkpoints.\nThe loader dequantizes fp8 weights back to bf16 at load time; the\n`default_settings.fp8_storage` toggle can then optionally re-cast to fp8 for\nVRAM savings."
},
- "LoRA_LyCORIS_QwenImage_Config": {
+ "Main_Checkpoint_SD1_Config": {
"properties": {
"key": {
"type": "string",
@@ -46541,9 +51999,9 @@
},
"type": {
"type": "string",
- "const": "lora",
+ "const": "main",
"title": "Type",
- "default": "lora"
+ "default": "main"
},
"trigger_phrases": {
"anyOf": [
@@ -46564,7 +52022,7 @@
"default_settings": {
"anyOf": [
{
- "$ref": "#/components/schemas/LoraModelDefaultSettings"
+ "$ref": "#/components/schemas/MainModelDefaultSettings"
},
{
"type": "null"
@@ -46572,17 +52030,35 @@
],
"description": "Default settings for this model"
},
+ "config_path": {
+ "anyOf": [
+ {
+ "type": "string"
+ },
+ {
+ "type": "null"
+ }
+ ],
+ "title": "Config Path",
+ "description": "Path to the config for this model, if any."
+ },
"format": {
"type": "string",
- "const": "lycoris",
+ "const": "checkpoint",
"title": "Format",
- "default": "lycoris"
+ "default": "checkpoint"
+ },
+ "prediction_type": {
+ "$ref": "#/components/schemas/SchedulerPredictionType"
+ },
+ "variant": {
+ "$ref": "#/components/schemas/ModelVariantType"
},
"base": {
"type": "string",
- "const": "qwen-image",
+ "const": "sd-1",
"title": "Base",
- "default": "qwen-image"
+ "default": "sd-1"
}
},
"type": "object",
@@ -46601,13 +52077,15 @@
"type",
"trigger_phrases",
"default_settings",
+ "config_path",
"format",
+ "prediction_type",
+ "variant",
"base"
],
- "title": "LoRA_LyCORIS_QwenImage_Config",
- "description": "Model config for Qwen Image Edit LoRA models in LyCORIS format."
+ "title": "Main_Checkpoint_SD1_Config"
},
- "LoRA_LyCORIS_SD1_Config": {
+ "Main_Checkpoint_SD2_Config": {
"properties": {
"key": {
"type": "string",
@@ -46693,9 +52171,9 @@
},
"type": {
"type": "string",
- "const": "lora",
+ "const": "main",
"title": "Type",
- "default": "lora"
+ "default": "main"
},
"trigger_phrases": {
"anyOf": [
@@ -46716,7 +52194,7 @@
"default_settings": {
"anyOf": [
{
- "$ref": "#/components/schemas/LoraModelDefaultSettings"
+ "$ref": "#/components/schemas/MainModelDefaultSettings"
},
{
"type": "null"
@@ -46724,17 +52202,35 @@
],
"description": "Default settings for this model"
},
+ "config_path": {
+ "anyOf": [
+ {
+ "type": "string"
+ },
+ {
+ "type": "null"
+ }
+ ],
+ "title": "Config Path",
+ "description": "Path to the config for this model, if any."
+ },
"format": {
"type": "string",
- "const": "lycoris",
+ "const": "checkpoint",
"title": "Format",
- "default": "lycoris"
+ "default": "checkpoint"
+ },
+ "prediction_type": {
+ "$ref": "#/components/schemas/SchedulerPredictionType"
+ },
+ "variant": {
+ "$ref": "#/components/schemas/ModelVariantType"
},
"base": {
"type": "string",
- "const": "sd-1",
+ "const": "sd-2",
"title": "Base",
- "default": "sd-1"
+ "default": "sd-2"
}
},
"type": "object",
@@ -46753,12 +52249,15 @@
"type",
"trigger_phrases",
"default_settings",
+ "config_path",
"format",
+ "prediction_type",
+ "variant",
"base"
],
- "title": "LoRA_LyCORIS_SD1_Config"
+ "title": "Main_Checkpoint_SD2_Config"
},
- "LoRA_LyCORIS_SD2_Config": {
+ "Main_Checkpoint_SDXLRefiner_Config": {
"properties": {
"key": {
"type": "string",
@@ -46844,9 +52343,9 @@
},
"type": {
"type": "string",
- "const": "lora",
+ "const": "main",
"title": "Type",
- "default": "lora"
+ "default": "main"
},
"trigger_phrases": {
"anyOf": [
@@ -46867,7 +52366,7 @@
"default_settings": {
"anyOf": [
{
- "$ref": "#/components/schemas/LoraModelDefaultSettings"
+ "$ref": "#/components/schemas/MainModelDefaultSettings"
},
{
"type": "null"
@@ -46875,17 +52374,35 @@
],
"description": "Default settings for this model"
},
+ "config_path": {
+ "anyOf": [
+ {
+ "type": "string"
+ },
+ {
+ "type": "null"
+ }
+ ],
+ "title": "Config Path",
+ "description": "Path to the config for this model, if any."
+ },
"format": {
"type": "string",
- "const": "lycoris",
+ "const": "checkpoint",
"title": "Format",
- "default": "lycoris"
+ "default": "checkpoint"
+ },
+ "prediction_type": {
+ "$ref": "#/components/schemas/SchedulerPredictionType"
+ },
+ "variant": {
+ "$ref": "#/components/schemas/ModelVariantType"
},
"base": {
"type": "string",
- "const": "sd-2",
+ "const": "sdxl-refiner",
"title": "Base",
- "default": "sd-2"
+ "default": "sdxl-refiner"
}
},
"type": "object",
@@ -46904,12 +52421,15 @@
"type",
"trigger_phrases",
"default_settings",
+ "config_path",
"format",
+ "prediction_type",
+ "variant",
"base"
],
- "title": "LoRA_LyCORIS_SD2_Config"
+ "title": "Main_Checkpoint_SDXLRefiner_Config"
},
- "LoRA_LyCORIS_SDXL_Config": {
+ "Main_Checkpoint_SDXL_Config": {
"properties": {
"key": {
"type": "string",
@@ -46995,9 +52515,9 @@
},
"type": {
"type": "string",
- "const": "lora",
+ "const": "main",
"title": "Type",
- "default": "lora"
+ "default": "main"
},
"trigger_phrases": {
"anyOf": [
@@ -47018,7 +52538,7 @@
"default_settings": {
"anyOf": [
{
- "$ref": "#/components/schemas/LoraModelDefaultSettings"
+ "$ref": "#/components/schemas/MainModelDefaultSettings"
},
{
"type": "null"
@@ -47026,11 +52546,29 @@
],
"description": "Default settings for this model"
},
+ "config_path": {
+ "anyOf": [
+ {
+ "type": "string"
+ },
+ {
+ "type": "null"
+ }
+ ],
+ "title": "Config Path",
+ "description": "Path to the config for this model, if any."
+ },
"format": {
"type": "string",
- "const": "lycoris",
+ "const": "checkpoint",
"title": "Format",
- "default": "lycoris"
+ "default": "checkpoint"
+ },
+ "prediction_type": {
+ "$ref": "#/components/schemas/SchedulerPredictionType"
+ },
+ "variant": {
+ "$ref": "#/components/schemas/ModelVariantType"
},
"base": {
"type": "string",
@@ -47055,12 +52593,15 @@
"type",
"trigger_phrases",
"default_settings",
+ "config_path",
"format",
+ "prediction_type",
+ "variant",
"base"
],
- "title": "LoRA_LyCORIS_SDXL_Config"
+ "title": "Main_Checkpoint_SDXL_Config"
},
- "LoRA_LyCORIS_ZImage_Config": {
+ "Main_Checkpoint_ZImage_Config": {
"properties": {
"key": {
"type": "string",
@@ -47146,9 +52687,9 @@
},
"type": {
"type": "string",
- "const": "lora",
+ "const": "main",
"title": "Type",
- "default": "lora"
+ "default": "main"
},
"trigger_phrases": {
"anyOf": [
@@ -47169,7 +52710,7 @@
"default_settings": {
"anyOf": [
{
- "$ref": "#/components/schemas/LoraModelDefaultSettings"
+ "$ref": "#/components/schemas/MainModelDefaultSettings"
},
{
"type": "null"
@@ -47177,11 +52718,17 @@
],
"description": "Default settings for this model"
},
- "format": {
- "type": "string",
- "const": "lycoris",
- "title": "Format",
- "default": "lycoris"
+ "config_path": {
+ "anyOf": [
+ {
+ "type": "string"
+ },
+ {
+ "type": "null"
+ }
+ ],
+ "title": "Config Path",
+ "description": "Path to the config for this model, if any."
},
"base": {
"type": "string",
@@ -47189,15 +52736,14 @@
"title": "Base",
"default": "z-image"
},
+ "format": {
+ "type": "string",
+ "const": "checkpoint",
+ "title": "Format",
+ "default": "checkpoint"
+ },
"variant": {
- "anyOf": [
- {
- "$ref": "#/components/schemas/ZImageVariantType"
- },
- {
- "type": "null"
- }
- ]
+ "$ref": "#/components/schemas/ZImageVariantType"
}
},
"type": "object",
@@ -47216,14 +52762,15 @@
"type",
"trigger_phrases",
"default_settings",
- "format",
+ "config_path",
"base",
+ "format",
"variant"
],
- "title": "LoRA_LyCORIS_ZImage_Config",
- "description": "Model config for Z-Image LoRA models in LyCORIS format."
+ "title": "Main_Checkpoint_ZImage_Config",
+ "description": "Model config for Z-Image single-file checkpoint models (safetensors, etc)."
},
- "LoRA_OMI_FLUX_Config": {
+ "Main_Diffusers_CogView4_Config": {
"properties": {
"key": {
"type": "string",
@@ -47309,9 +52856,9 @@
},
"type": {
"type": "string",
- "const": "lora",
+ "const": "main",
"title": "Type",
- "default": "lora"
+ "default": "main"
},
"trigger_phrases": {
"anyOf": [
@@ -47332,7 +52879,7 @@
"default_settings": {
"anyOf": [
{
- "$ref": "#/components/schemas/LoraModelDefaultSettings"
+ "$ref": "#/components/schemas/MainModelDefaultSettings"
},
{
"type": "null"
@@ -47342,15 +52889,19 @@
},
"format": {
"type": "string",
- "const": "omi",
+ "const": "diffusers",
"title": "Format",
- "default": "omi"
+ "default": "diffusers"
+ },
+ "repo_variant": {
+ "$ref": "#/components/schemas/ModelRepoVariant",
+ "default": ""
},
"base": {
"type": "string",
- "const": "flux",
+ "const": "cogview4",
"title": "Base",
- "default": "flux"
+ "default": "cogview4"
}
},
"type": "object",
@@ -47370,11 +52921,12 @@
"trigger_phrases",
"default_settings",
"format",
+ "repo_variant",
"base"
],
- "title": "LoRA_OMI_FLUX_Config"
+ "title": "Main_Diffusers_CogView4_Config"
},
- "LoRA_OMI_SDXL_Config": {
+ "Main_Diffusers_FLUX_Config": {
"properties": {
"key": {
"type": "string",
@@ -47460,9 +53012,9 @@
},
"type": {
"type": "string",
- "const": "lora",
+ "const": "main",
"title": "Type",
- "default": "lora"
+ "default": "main"
},
"trigger_phrases": {
"anyOf": [
@@ -47483,7 +53035,7 @@
"default_settings": {
"anyOf": [
{
- "$ref": "#/components/schemas/LoraModelDefaultSettings"
+ "$ref": "#/components/schemas/MainModelDefaultSettings"
},
{
"type": "null"
@@ -47493,15 +53045,22 @@
},
"format": {
"type": "string",
- "const": "omi",
+ "const": "diffusers",
"title": "Format",
- "default": "omi"
+ "default": "diffusers"
+ },
+ "repo_variant": {
+ "$ref": "#/components/schemas/ModelRepoVariant",
+ "default": ""
},
"base": {
"type": "string",
- "const": "sdxl",
+ "const": "flux",
"title": "Base",
- "default": "sdxl"
+ "default": "flux"
+ },
+ "variant": {
+ "$ref": "#/components/schemas/FluxVariantType"
}
},
"type": "object",
@@ -47521,380 +53080,202 @@
"trigger_phrases",
"default_settings",
"format",
- "base"
+ "repo_variant",
+ "base",
+ "variant"
],
- "title": "LoRA_OMI_SDXL_Config"
- },
- "LocalModelSource": {
- "properties": {
- "path": {
- "anyOf": [
- {
- "type": "string"
- },
- {
- "type": "string",
- "format": "path"
- }
- ],
- "title": "Path"
- },
- "inplace": {
- "anyOf": [
- {
- "type": "boolean"
- },
- {
- "type": "null"
- }
- ],
- "title": "Inplace",
- "default": false
- },
- "type": {
- "type": "string",
- "const": "local",
- "title": "Type",
- "default": "local"
- }
- },
- "type": "object",
- "required": ["path"],
- "title": "LocalModelSource",
- "description": "A local file or directory path."
- },
- "LogLevel": {
- "type": "integer",
- "enum": [0, 10, 20, 30, 40, 50],
- "title": "LogLevel"
+ "title": "Main_Diffusers_FLUX_Config",
+ "description": "Model config for FLUX.1 models in diffusers format."
},
- "LoginRequest": {
+ "Main_Diffusers_Flux2_Config": {
"properties": {
- "email": {
+ "key": {
"type": "string",
- "title": "Email",
- "description": "User email address"
+ "title": "Key",
+ "description": "A unique key for this model."
},
- "password": {
+ "hash": {
"type": "string",
- "title": "Password",
- "description": "User password"
+ "title": "Hash",
+ "description": "The hash of the model file(s)."
},
- "remember_me": {
- "type": "boolean",
- "title": "Remember Me",
- "description": "Whether to extend session duration",
- "default": false
- }
- },
- "type": "object",
- "required": ["email", "password"],
- "title": "LoginRequest",
- "description": "Request body for user login."
- },
- "LoginResponse": {
- "properties": {
- "token": {
+ "path": {
"type": "string",
- "title": "Token",
- "description": "JWT access token"
- },
- "user": {
- "$ref": "#/components/schemas/UserDTO",
- "description": "User information"
+ "title": "Path",
+ "description": "Path to the model on the filesystem. Relative paths are relative to the Invoke root directory."
},
- "expires_in": {
+ "file_size": {
"type": "integer",
- "title": "Expires In",
- "description": "Token expiration time in seconds"
- }
- },
- "type": "object",
- "required": ["token", "user", "expires_in"],
- "title": "LoginResponse",
- "description": "Response from successful login."
- },
- "LogoutResponse": {
- "properties": {
- "success": {
- "type": "boolean",
- "title": "Success",
- "description": "Whether logout was successful"
- }
- },
- "type": "object",
- "required": ["success"],
- "title": "LogoutResponse",
- "description": "Response from logout."
- },
- "LoraModelDefaultSettings": {
- "properties": {
- "weight": {
- "anyOf": [
- {
- "type": "number"
- },
- {
- "type": "null"
- }
- ],
- "title": "Weight",
- "description": "Default weight for this model"
+ "title": "File Size",
+ "description": "The size of the model in bytes."
},
- "weight_min": {
- "anyOf": [
- {
- "type": "number"
- },
- {
- "type": "null"
- }
- ],
- "title": "Weight Min",
- "description": "Minimum weight slider value for this model"
+ "name": {
+ "type": "string",
+ "title": "Name",
+ "description": "Name of the model."
},
- "weight_max": {
+ "description": {
"anyOf": [
{
- "type": "number"
+ "type": "string"
},
{
"type": "null"
}
],
- "title": "Weight Max",
- "description": "Maximum weight slider value for this model"
- }
- },
- "additionalProperties": false,
- "type": "object",
- "title": "LoraModelDefaultSettings"
- },
- "MDControlListOutput": {
- "class": "output",
- "properties": {
- "control_list": {
+ "title": "Description",
+ "description": "Model description"
+ },
+ "source": {
+ "type": "string",
+ "title": "Source",
+ "description": "The original source of the model (path, URL or repo_id)."
+ },
+ "source_type": {
+ "$ref": "#/components/schemas/ModelSourceType",
+ "description": "The type of source"
+ },
+ "source_api_response": {
"anyOf": [
{
- "$ref": "#/components/schemas/ControlField"
- },
- {
- "items": {
- "$ref": "#/components/schemas/ControlField"
- },
- "type": "array"
+ "type": "string"
},
{
"type": "null"
}
],
- "description": "ControlNet(s) to apply",
- "field_kind": "output",
- "title": "ControlNet-List",
- "ui_hidden": false
+ "title": "Source Api Response",
+ "description": "The original API response from the source, as stringified JSON."
},
- "type": {
- "const": "md_control_list_output",
- "default": "md_control_list_output",
- "field_kind": "node_attribute",
- "title": "type",
- "type": "string"
- }
- },
- "required": ["output_meta", "control_list", "type", "type"],
- "title": "MDControlListOutput",
- "type": "object"
- },
- "MDIPAdapterListOutput": {
- "class": "output",
- "properties": {
- "ip_adapter_list": {
+ "source_url": {
"anyOf": [
{
- "$ref": "#/components/schemas/IPAdapterField"
- },
- {
- "items": {
- "$ref": "#/components/schemas/IPAdapterField"
- },
- "type": "array"
+ "type": "string"
},
{
"type": "null"
}
],
- "description": "IP-Adapter to apply",
- "field_kind": "output",
- "title": "IP-Adapter-List",
- "ui_hidden": false
+ "title": "Source Url",
+ "description": "Optional URL for the model (e.g. download page or model page)."
},
- "type": {
- "const": "md_ip_adapter_list_output",
- "default": "md_ip_adapter_list_output",
- "field_kind": "node_attribute",
- "title": "type",
- "type": "string"
- }
- },
- "required": ["output_meta", "ip_adapter_list", "type", "type"],
- "title": "MDIPAdapterListOutput",
- "type": "object"
- },
- "MDT2IAdapterListOutput": {
- "class": "output",
- "properties": {
- "t2i_adapter_list": {
+ "cover_image": {
"anyOf": [
{
- "$ref": "#/components/schemas/T2IAdapterField"
- },
- {
- "items": {
- "$ref": "#/components/schemas/T2IAdapterField"
- },
- "type": "array"
+ "type": "string"
},
{
"type": "null"
}
],
- "description": "T2I-Adapter(s) to apply",
- "field_kind": "output",
- "title": "T2I Adapter-List",
- "ui_hidden": false
+ "title": "Cover Image",
+ "description": "Url for image to preview model"
},
"type": {
- "const": "md_ip_adapters_output",
- "default": "md_ip_adapters_output",
- "field_kind": "node_attribute",
- "title": "type",
- "type": "string"
- }
- },
- "required": ["output_meta", "t2i_adapter_list", "type", "type"],
- "title": "MDT2IAdapterListOutput",
- "type": "object"
- },
- "MLSDDetectionInvocation": {
- "category": "controlnet_preprocessors",
- "class": "invocation",
- "classification": "stable",
- "description": "Generates an line segment map using MLSD.",
- "node_pack": "invokeai",
- "properties": {
- "board": {
- "anyOf": [
- {
- "$ref": "#/components/schemas/BoardField"
- },
- {
- "type": "null"
- }
- ],
- "default": null,
- "description": "The board to save the image to",
- "field_kind": "internal",
- "input": "direct",
- "orig_required": false,
- "ui_hidden": false
+ "type": "string",
+ "const": "main",
+ "title": "Type",
+ "default": "main"
},
- "metadata": {
+ "trigger_phrases": {
"anyOf": [
{
- "$ref": "#/components/schemas/MetadataField"
- },
- {
- "type": "null"
- }
- ],
- "default": null,
- "description": "Optional metadata to be saved with the image",
- "field_kind": "internal",
- "input": "connection",
- "orig_required": false,
- "ui_hidden": false
- },
- "id": {
- "description": "The id of this instance of an invocation. Must be unique among all instances of invocations.",
- "field_kind": "node_attribute",
- "title": "Id",
- "type": "string"
- },
- "is_intermediate": {
- "default": false,
- "description": "Whether or not this is an intermediate invocation.",
- "field_kind": "node_attribute",
- "input": "direct",
- "orig_required": true,
- "title": "Is Intermediate",
- "type": "boolean",
- "ui_hidden": false,
- "ui_type": "IsIntermediate"
- },
- "use_cache": {
- "default": true,
- "description": "Whether or not to use the cache",
- "field_kind": "node_attribute",
- "title": "Use Cache",
- "type": "boolean"
+ "items": {
+ "type": "string"
+ },
+ "type": "array",
+ "uniqueItems": true
+ },
+ {
+ "type": "null"
+ }
+ ],
+ "title": "Trigger Phrases",
+ "description": "Set of trigger phrases for this model"
},
- "image": {
+ "default_settings": {
"anyOf": [
{
- "$ref": "#/components/schemas/ImageField"
+ "$ref": "#/components/schemas/MainModelDefaultSettings"
},
{
"type": "null"
}
],
- "default": null,
- "description": "The image to process",
- "field_kind": "input",
- "input": "any",
- "orig_required": true
+ "description": "Default settings for this model"
},
- "score_threshold": {
- "default": 0.1,
- "description": "The threshold used to score points when determining line segments",
- "field_kind": "input",
- "input": "any",
- "minimum": 0,
- "orig_default": 0.1,
- "orig_required": false,
- "title": "Score Threshold",
- "type": "number"
+ "format": {
+ "type": "string",
+ "const": "diffusers",
+ "title": "Format",
+ "default": "diffusers"
},
- "distance_threshold": {
- "default": 20.0,
- "description": "Threshold for including a line segment - lines shorter than this distance will be discarded",
- "field_kind": "input",
- "input": "any",
- "minimum": 0,
- "orig_default": 20.0,
- "orig_required": false,
- "title": "Distance Threshold",
- "type": "number"
+ "repo_variant": {
+ "$ref": "#/components/schemas/ModelRepoVariant",
+ "default": ""
},
- "type": {
- "const": "mlsd_detection",
- "default": "mlsd_detection",
- "field_kind": "node_attribute",
- "title": "type",
- "type": "string"
+ "base": {
+ "type": "string",
+ "const": "flux2",
+ "title": "Base",
+ "default": "flux2"
+ },
+ "variant": {
+ "$ref": "#/components/schemas/Flux2VariantType"
}
},
- "required": ["type", "id"],
- "tags": ["controlnet", "mlsd", "edge"],
- "title": "MLSD Detection",
"type": "object",
- "version": "1.0.0",
- "output": {
- "$ref": "#/components/schemas/ImageOutput"
- }
+ "required": [
+ "key",
+ "hash",
+ "path",
+ "file_size",
+ "name",
+ "description",
+ "source",
+ "source_type",
+ "source_api_response",
+ "source_url",
+ "cover_image",
+ "type",
+ "trigger_phrases",
+ "default_settings",
+ "format",
+ "repo_variant",
+ "base",
+ "variant"
+ ],
+ "title": "Main_Diffusers_Flux2_Config",
+ "description": "Model config for FLUX.2 models in diffusers format (e.g. FLUX.2 Klein)."
},
- "MainModelDefaultSettings": {
+ "Main_Diffusers_QwenImage_Config": {
"properties": {
- "vae": {
+ "key": {
+ "type": "string",
+ "title": "Key",
+ "description": "A unique key for this model."
+ },
+ "hash": {
+ "type": "string",
+ "title": "Hash",
+ "description": "The hash of the model file(s)."
+ },
+ "path": {
+ "type": "string",
+ "title": "Path",
+ "description": "Path to the model on the filesystem. Relative paths are relative to the Invoke root directory."
+ },
+ "file_size": {
+ "type": "integer",
+ "title": "File Size",
+ "description": "The size of the model in bytes."
+ },
+ "name": {
+ "type": "string",
+ "title": "Name",
+ "description": "Name of the model."
+ },
+ "description": {
"anyOf": [
{
"type": "string"
@@ -47903,243 +53284,303 @@
"type": "null"
}
],
- "title": "Vae",
- "description": "Default VAE for this model (model key)"
+ "title": "Description",
+ "description": "Model description"
},
- "vae_precision": {
+ "source": {
+ "type": "string",
+ "title": "Source",
+ "description": "The original source of the model (path, URL or repo_id)."
+ },
+ "source_type": {
+ "$ref": "#/components/schemas/ModelSourceType",
+ "description": "The type of source"
+ },
+ "source_api_response": {
"anyOf": [
{
- "type": "string",
- "enum": ["fp16", "fp32"]
+ "type": "string"
},
{
"type": "null"
}
],
- "title": "Vae Precision",
- "description": "Default VAE precision for this model"
+ "title": "Source Api Response",
+ "description": "The original API response from the source, as stringified JSON."
},
- "scheduler": {
+ "source_url": {
"anyOf": [
{
- "type": "string",
- "enum": [
- "ddim",
- "ddpm",
- "deis",
- "deis_k",
- "lms",
- "lms_k",
- "pndm",
- "heun",
- "heun_k",
- "euler",
- "euler_k",
- "euler_a",
- "kdpm_2",
- "kdpm_2_k",
- "kdpm_2_a",
- "kdpm_2_a_k",
- "dpmpp_2s",
- "dpmpp_2s_k",
- "dpmpp_2m",
- "dpmpp_2m_k",
- "dpmpp_2m_sde",
- "dpmpp_2m_sde_k",
- "dpmpp_3m",
- "dpmpp_3m_k",
- "dpmpp_sde",
- "dpmpp_sde_k",
- "er_sde",
- "unipc",
- "unipc_k",
- "lcm",
- "tcd"
- ]
+ "type": "string"
},
{
"type": "null"
}
],
- "title": "Scheduler",
- "description": "Default scheduler for this model"
+ "title": "Source Url",
+ "description": "Optional URL for the model (e.g. download page or model page)."
},
- "steps": {
+ "cover_image": {
"anyOf": [
{
- "type": "integer",
- "exclusiveMinimum": 0.0
+ "type": "string"
},
{
"type": "null"
}
],
- "title": "Steps",
- "description": "Default number of steps for this model"
+ "title": "Cover Image",
+ "description": "Url for image to preview model"
},
- "cfg_scale": {
+ "type": {
+ "type": "string",
+ "const": "main",
+ "title": "Type",
+ "default": "main"
+ },
+ "trigger_phrases": {
"anyOf": [
{
- "type": "number",
- "minimum": 1.0
+ "items": {
+ "type": "string"
+ },
+ "type": "array",
+ "uniqueItems": true
},
{
"type": "null"
}
],
- "title": "Cfg Scale",
- "description": "Default CFG Scale for this model"
+ "title": "Trigger Phrases",
+ "description": "Set of trigger phrases for this model"
},
- "cfg_rescale_multiplier": {
+ "default_settings": {
"anyOf": [
{
- "type": "number",
- "exclusiveMaximum": 1.0,
- "minimum": 0.0
+ "$ref": "#/components/schemas/MainModelDefaultSettings"
},
{
"type": "null"
}
],
- "title": "Cfg Rescale Multiplier",
- "description": "Default CFG Rescale Multiplier for this model"
+ "description": "Default settings for this model"
},
- "width": {
+ "format": {
+ "type": "string",
+ "const": "diffusers",
+ "title": "Format",
+ "default": "diffusers"
+ },
+ "repo_variant": {
+ "$ref": "#/components/schemas/ModelRepoVariant",
+ "default": ""
+ },
+ "base": {
+ "type": "string",
+ "const": "qwen-image",
+ "title": "Base",
+ "default": "qwen-image"
+ },
+ "variant": {
"anyOf": [
{
- "type": "integer",
- "multipleOf": 8.0,
- "minimum": 64.0
+ "$ref": "#/components/schemas/QwenImageVariantType"
},
{
"type": "null"
}
- ],
- "title": "Width",
- "description": "Default width for this model"
+ ]
+ }
+ },
+ "type": "object",
+ "required": [
+ "key",
+ "hash",
+ "path",
+ "file_size",
+ "name",
+ "description",
+ "source",
+ "source_type",
+ "source_api_response",
+ "source_url",
+ "cover_image",
+ "type",
+ "trigger_phrases",
+ "default_settings",
+ "format",
+ "repo_variant",
+ "base",
+ "variant"
+ ],
+ "title": "Main_Diffusers_QwenImage_Config",
+ "description": "Model config for Qwen Image diffusers models (both txt2img and edit)."
+ },
+ "Main_Diffusers_SD1_Config": {
+ "properties": {
+ "key": {
+ "type": "string",
+ "title": "Key",
+ "description": "A unique key for this model."
},
- "height": {
+ "hash": {
+ "type": "string",
+ "title": "Hash",
+ "description": "The hash of the model file(s)."
+ },
+ "path": {
+ "type": "string",
+ "title": "Path",
+ "description": "Path to the model on the filesystem. Relative paths are relative to the Invoke root directory."
+ },
+ "file_size": {
+ "type": "integer",
+ "title": "File Size",
+ "description": "The size of the model in bytes."
+ },
+ "name": {
+ "type": "string",
+ "title": "Name",
+ "description": "Name of the model."
+ },
+ "description": {
"anyOf": [
{
- "type": "integer",
- "multipleOf": 8.0,
- "minimum": 64.0
+ "type": "string"
},
{
"type": "null"
}
],
- "title": "Height",
- "description": "Default height for this model"
+ "title": "Description",
+ "description": "Model description"
},
- "guidance": {
+ "source": {
+ "type": "string",
+ "title": "Source",
+ "description": "The original source of the model (path, URL or repo_id)."
+ },
+ "source_type": {
+ "$ref": "#/components/schemas/ModelSourceType",
+ "description": "The type of source"
+ },
+ "source_api_response": {
"anyOf": [
{
- "type": "number",
- "minimum": 1.0
+ "type": "string"
},
{
"type": "null"
}
],
- "title": "Guidance",
- "description": "Default Guidance for this model"
+ "title": "Source Api Response",
+ "description": "The original API response from the source, as stringified JSON."
},
- "cpu_only": {
+ "source_url": {
"anyOf": [
{
- "type": "boolean"
+ "type": "string"
},
{
"type": "null"
}
],
- "title": "Cpu Only",
- "description": "Whether this model should run on CPU only"
+ "title": "Source Url",
+ "description": "Optional URL for the model (e.g. download page or model page)."
},
- "fp8_storage": {
+ "cover_image": {
"anyOf": [
{
- "type": "boolean"
+ "type": "string"
},
{
"type": "null"
}
],
- "title": "Fp8 Storage",
- "description": "Store weights in FP8 to reduce VRAM usage (~50% savings). Weights are cast to compute dtype during inference."
- }
- },
- "additionalProperties": false,
- "type": "object",
- "title": "MainModelDefaultSettings"
- },
- "MainModelLoaderInvocation": {
- "category": "model",
- "class": "invocation",
- "classification": "stable",
- "description": "Loads a main model, outputting its submodels.",
- "node_pack": "invokeai",
- "properties": {
- "id": {
- "description": "The id of this instance of an invocation. Must be unique among all instances of invocations.",
- "field_kind": "node_attribute",
- "title": "Id",
- "type": "string"
+ "title": "Cover Image",
+ "description": "Url for image to preview model"
},
- "is_intermediate": {
- "default": false,
- "description": "Whether or not this is an intermediate invocation.",
- "field_kind": "node_attribute",
- "input": "direct",
- "orig_required": true,
- "title": "Is Intermediate",
- "type": "boolean",
- "ui_hidden": false,
- "ui_type": "IsIntermediate"
+ "type": {
+ "type": "string",
+ "const": "main",
+ "title": "Type",
+ "default": "main"
},
- "use_cache": {
- "default": true,
- "description": "Whether or not to use the cache",
- "field_kind": "node_attribute",
- "title": "Use Cache",
- "type": "boolean"
+ "trigger_phrases": {
+ "anyOf": [
+ {
+ "items": {
+ "type": "string"
+ },
+ "type": "array",
+ "uniqueItems": true
+ },
+ {
+ "type": "null"
+ }
+ ],
+ "title": "Trigger Phrases",
+ "description": "Set of trigger phrases for this model"
},
- "model": {
+ "default_settings": {
"anyOf": [
{
- "$ref": "#/components/schemas/ModelIdentifierField"
+ "$ref": "#/components/schemas/MainModelDefaultSettings"
},
{
"type": "null"
}
],
- "default": null,
- "description": "Main model (UNet, VAE, CLIP) to load",
- "field_kind": "input",
- "input": "any",
- "orig_required": true,
- "ui_model_base": ["sd-1", "sd-2"],
- "ui_model_type": ["main"]
+ "description": "Default settings for this model"
},
- "type": {
- "const": "main_model_loader",
- "default": "main_model_loader",
- "field_kind": "node_attribute",
- "title": "type",
- "type": "string"
+ "format": {
+ "type": "string",
+ "const": "diffusers",
+ "title": "Format",
+ "default": "diffusers"
+ },
+ "repo_variant": {
+ "$ref": "#/components/schemas/ModelRepoVariant",
+ "default": ""
+ },
+ "prediction_type": {
+ "$ref": "#/components/schemas/SchedulerPredictionType"
+ },
+ "variant": {
+ "$ref": "#/components/schemas/ModelVariantType"
+ },
+ "base": {
+ "type": "string",
+ "const": "sd-1",
+ "title": "Base",
+ "default": "sd-1"
}
},
- "required": ["type", "id"],
- "tags": ["model"],
- "title": "Main Model - SD1.5, SD2",
"type": "object",
- "version": "1.0.4",
- "output": {
- "$ref": "#/components/schemas/ModelLoaderOutput"
- }
+ "required": [
+ "key",
+ "hash",
+ "path",
+ "file_size",
+ "name",
+ "description",
+ "source",
+ "source_type",
+ "source_api_response",
+ "source_url",
+ "cover_image",
+ "type",
+ "trigger_phrases",
+ "default_settings",
+ "format",
+ "repo_variant",
+ "prediction_type",
+ "variant",
+ "base"
+ ],
+ "title": "Main_Diffusers_SD1_Config"
},
- "Main_BnBNF4_FLUX_Config": {
+ "Main_Diffusers_SD2_Config": {
"properties": {
"key": {
"type": "string",
@@ -48256,32 +53697,27 @@
],
"description": "Default settings for this model"
},
- "config_path": {
- "anyOf": [
- {
- "type": "string"
- },
- {
- "type": "null"
- }
- ],
- "title": "Config Path",
- "description": "Path to the config for this model, if any."
- },
- "base": {
- "type": "string",
- "const": "flux",
- "title": "Base",
- "default": "flux"
- },
"format": {
"type": "string",
- "const": "bnb_quantized_nf4b",
+ "const": "diffusers",
"title": "Format",
- "default": "bnb_quantized_nf4b"
+ "default": "diffusers"
+ },
+ "repo_variant": {
+ "$ref": "#/components/schemas/ModelRepoVariant",
+ "default": ""
+ },
+ "prediction_type": {
+ "$ref": "#/components/schemas/SchedulerPredictionType"
},
"variant": {
- "$ref": "#/components/schemas/FluxVariantType"
+ "$ref": "#/components/schemas/ModelVariantType"
+ },
+ "base": {
+ "type": "string",
+ "const": "sd-2",
+ "title": "Base",
+ "default": "sd-2"
}
},
"type": "object",
@@ -48300,15 +53736,15 @@
"type",
"trigger_phrases",
"default_settings",
- "config_path",
- "base",
"format",
- "variant"
+ "repo_variant",
+ "prediction_type",
+ "variant",
+ "base"
],
- "title": "Main_BnBNF4_FLUX_Config",
- "description": "Model config for main checkpoint models."
+ "title": "Main_Diffusers_SD2_Config"
},
- "Main_Checkpoint_Anima_Config": {
+ "Main_Diffusers_SD3_Config": {
"properties": {
"key": {
"type": "string",
@@ -48425,29 +53861,39 @@
],
"description": "Default settings for this model"
},
- "config_path": {
+ "format": {
+ "type": "string",
+ "const": "diffusers",
+ "title": "Format",
+ "default": "diffusers"
+ },
+ "repo_variant": {
+ "$ref": "#/components/schemas/ModelRepoVariant",
+ "default": ""
+ },
+ "base": {
+ "type": "string",
+ "const": "sd-3",
+ "title": "Base",
+ "default": "sd-3"
+ },
+ "submodels": {
"anyOf": [
{
- "type": "string"
+ "additionalProperties": {
+ "$ref": "#/components/schemas/SubmodelDefinition"
+ },
+ "propertyNames": {
+ "$ref": "#/components/schemas/SubModelType"
+ },
+ "type": "object"
},
{
"type": "null"
}
],
- "title": "Config Path",
- "description": "Path to the config for this model, if any."
- },
- "base": {
- "type": "string",
- "const": "anima",
- "title": "Base",
- "default": "anima"
- },
- "format": {
- "type": "string",
- "const": "checkpoint",
- "title": "Format",
- "default": "checkpoint"
+ "title": "Submodels",
+ "description": "Loadable submodels in this model"
}
},
"type": "object",
@@ -48466,14 +53912,14 @@
"type",
"trigger_phrases",
"default_settings",
- "config_path",
+ "format",
+ "repo_variant",
"base",
- "format"
+ "submodels"
],
- "title": "Main_Checkpoint_Anima_Config",
- "description": "Model config for Anima single-file checkpoint models (safetensors).\n\nAnima is built on NVIDIA Cosmos Predict2 DiT with a custom LLM Adapter\nthat bridges Qwen3 0.6B text encoder outputs to the DiT."
+ "title": "Main_Diffusers_SD3_Config"
},
- "Main_Checkpoint_FLUX_Config": {
+ "Main_Diffusers_SDXLRefiner_Config": {
"properties": {
"key": {
"type": "string",
@@ -48590,32 +54036,27 @@
],
"description": "Default settings for this model"
},
- "config_path": {
- "anyOf": [
- {
- "type": "string"
- },
- {
- "type": "null"
- }
- ],
- "title": "Config Path",
- "description": "Path to the config for this model, if any."
- },
"format": {
"type": "string",
- "const": "checkpoint",
+ "const": "diffusers",
"title": "Format",
- "default": "checkpoint"
+ "default": "diffusers"
+ },
+ "repo_variant": {
+ "$ref": "#/components/schemas/ModelRepoVariant",
+ "default": ""
+ },
+ "prediction_type": {
+ "$ref": "#/components/schemas/SchedulerPredictionType"
+ },
+ "variant": {
+ "$ref": "#/components/schemas/ModelVariantType"
},
"base": {
"type": "string",
- "const": "flux",
+ "const": "sdxl-refiner",
"title": "Base",
- "default": "flux"
- },
- "variant": {
- "$ref": "#/components/schemas/FluxVariantType"
+ "default": "sdxl-refiner"
}
},
"type": "object",
@@ -48634,15 +54075,15 @@
"type",
"trigger_phrases",
"default_settings",
- "config_path",
"format",
- "base",
- "variant"
+ "repo_variant",
+ "prediction_type",
+ "variant",
+ "base"
],
- "title": "Main_Checkpoint_FLUX_Config",
- "description": "Model config for main checkpoint models."
+ "title": "Main_Diffusers_SDXLRefiner_Config"
},
- "Main_Checkpoint_Flux2_Config": {
+ "Main_Diffusers_SDXL_Config": {
"properties": {
"key": {
"type": "string",
@@ -48759,32 +54200,27 @@
],
"description": "Default settings for this model"
},
- "config_path": {
- "anyOf": [
- {
- "type": "string"
- },
- {
- "type": "null"
- }
- ],
- "title": "Config Path",
- "description": "Path to the config for this model, if any."
- },
"format": {
"type": "string",
- "const": "checkpoint",
+ "const": "diffusers",
"title": "Format",
- "default": "checkpoint"
+ "default": "diffusers"
+ },
+ "repo_variant": {
+ "$ref": "#/components/schemas/ModelRepoVariant",
+ "default": ""
+ },
+ "prediction_type": {
+ "$ref": "#/components/schemas/SchedulerPredictionType"
+ },
+ "variant": {
+ "$ref": "#/components/schemas/ModelVariantType"
},
"base": {
"type": "string",
- "const": "flux2",
+ "const": "sdxl",
"title": "Base",
- "default": "flux2"
- },
- "variant": {
- "$ref": "#/components/schemas/Flux2VariantType"
+ "default": "sdxl"
}
},
"type": "object",
@@ -48803,15 +54239,15 @@
"type",
"trigger_phrases",
"default_settings",
- "config_path",
"format",
- "base",
- "variant"
+ "repo_variant",
+ "prediction_type",
+ "variant",
+ "base"
],
- "title": "Main_Checkpoint_Flux2_Config",
- "description": "Model config for FLUX.2 checkpoint models (e.g. Klein)."
+ "title": "Main_Diffusers_SDXL_Config"
},
- "Main_Checkpoint_QwenImage_Config": {
+ "Main_Diffusers_Wan_Config": {
"properties": {
"key": {
"type": "string",
@@ -48928,7 +54364,98 @@
],
"description": "Default settings for this model"
},
- "config_path": {
+ "format": {
+ "type": "string",
+ "const": "diffusers",
+ "title": "Format",
+ "default": "diffusers"
+ },
+ "repo_variant": {
+ "$ref": "#/components/schemas/ModelRepoVariant",
+ "default": ""
+ },
+ "base": {
+ "type": "string",
+ "const": "wan",
+ "title": "Base",
+ "default": "wan"
+ },
+ "variant": {
+ "$ref": "#/components/schemas/WanVariantType"
+ },
+ "has_dual_expert": {
+ "type": "boolean",
+ "title": "Has Dual Expert",
+ "description": "Whether this model ships two transformer experts (Wan 2.2 A14B MoE). False for TI2V-5B.",
+ "default": false
+ },
+ "boundary_ratio": {
+ "anyOf": [
+ {
+ "type": "number"
+ },
+ {
+ "type": "null"
+ }
+ ],
+ "title": "Boundary Ratio",
+ "description": "MoE expert switch point as a fraction of num_train_timesteps (typically 1000). None for single-transformer models. Read from model_index.json by Diffusers' WanPipeline."
+ }
+ },
+ "type": "object",
+ "required": [
+ "key",
+ "hash",
+ "path",
+ "file_size",
+ "name",
+ "description",
+ "source",
+ "source_type",
+ "source_api_response",
+ "source_url",
+ "cover_image",
+ "type",
+ "trigger_phrases",
+ "default_settings",
+ "format",
+ "repo_variant",
+ "base",
+ "variant",
+ "has_dual_expert",
+ "boundary_ratio"
+ ],
+ "title": "Main_Diffusers_Wan_Config",
+ "description": "Model config for Wan 2.2 diffusers models.\n\nCovers both the dual-expert T2V-A14B family and the single-transformer TI2V-5B\nfamily. Variant is detected from the on-disk transformer config (latent channel\ncount) plus the presence of a sibling ``transformer_2/`` directory."
+ },
+ "Main_Diffusers_ZImage_Config": {
+ "properties": {
+ "key": {
+ "type": "string",
+ "title": "Key",
+ "description": "A unique key for this model."
+ },
+ "hash": {
+ "type": "string",
+ "title": "Hash",
+ "description": "The hash of the model file(s)."
+ },
+ "path": {
+ "type": "string",
+ "title": "Path",
+ "description": "Path to the model on the filesystem. Relative paths are relative to the Invoke root directory."
+ },
+ "file_size": {
+ "type": "integer",
+ "title": "File Size",
+ "description": "The size of the model in bytes."
+ },
+ "name": {
+ "type": "string",
+ "title": "Name",
+ "description": "Name of the model."
+ },
+ "description": {
"anyOf": [
{
"type": "string"
@@ -48937,30 +54464,105 @@
"type": "null"
}
],
- "title": "Config Path",
- "description": "Path to the config for this model, if any."
+ "title": "Description",
+ "description": "Model description"
},
- "base": {
+ "source": {
"type": "string",
- "const": "qwen-image",
- "title": "Base",
- "default": "qwen-image"
+ "title": "Source",
+ "description": "The original source of the model (path, URL or repo_id)."
},
- "format": {
+ "source_type": {
+ "$ref": "#/components/schemas/ModelSourceType",
+ "description": "The type of source"
+ },
+ "source_api_response": {
+ "anyOf": [
+ {
+ "type": "string"
+ },
+ {
+ "type": "null"
+ }
+ ],
+ "title": "Source Api Response",
+ "description": "The original API response from the source, as stringified JSON."
+ },
+ "source_url": {
+ "anyOf": [
+ {
+ "type": "string"
+ },
+ {
+ "type": "null"
+ }
+ ],
+ "title": "Source Url",
+ "description": "Optional URL for the model (e.g. download page or model page)."
+ },
+ "cover_image": {
+ "anyOf": [
+ {
+ "type": "string"
+ },
+ {
+ "type": "null"
+ }
+ ],
+ "title": "Cover Image",
+ "description": "Url for image to preview model"
+ },
+ "type": {
"type": "string",
- "const": "checkpoint",
- "title": "Format",
- "default": "checkpoint"
+ "const": "main",
+ "title": "Type",
+ "default": "main"
},
- "variant": {
+ "trigger_phrases": {
"anyOf": [
{
- "$ref": "#/components/schemas/QwenImageVariantType"
+ "items": {
+ "type": "string"
+ },
+ "type": "array",
+ "uniqueItems": true
},
{
"type": "null"
}
- ]
+ ],
+ "title": "Trigger Phrases",
+ "description": "Set of trigger phrases for this model"
+ },
+ "default_settings": {
+ "anyOf": [
+ {
+ "$ref": "#/components/schemas/MainModelDefaultSettings"
+ },
+ {
+ "type": "null"
+ }
+ ],
+ "description": "Default settings for this model"
+ },
+ "format": {
+ "type": "string",
+ "const": "diffusers",
+ "title": "Format",
+ "default": "diffusers"
+ },
+ "repo_variant": {
+ "$ref": "#/components/schemas/ModelRepoVariant",
+ "default": ""
+ },
+ "base": {
+ "type": "string",
+ "const": "z-image",
+ "title": "Base",
+ "default": "z-image"
+ },
+ "variant": {
+ "$ref": "#/components/schemas/ZImageVariantType"
}
},
"type": "object",
@@ -48979,15 +54581,15 @@
"type",
"trigger_phrases",
"default_settings",
- "config_path",
- "base",
"format",
+ "repo_variant",
+ "base",
"variant"
],
- "title": "Main_Checkpoint_QwenImage_Config",
- "description": "Model config for Qwen Image single-file checkpoint models (safetensors, etc).\n\nCovers both raw bf16/fp16 checkpoints and ComfyUI-style fp8_scaled checkpoints.\nThe loader dequantizes fp8 weights back to bf16 at load time; the\n`default_settings.fp8_storage` toggle can then optionally re-cast to fp8 for\nVRAM savings."
+ "title": "Main_Diffusers_ZImage_Config",
+ "description": "Model config for Z-Image diffusers models (Z-Image-Turbo, Z-Image-Base)."
},
- "Main_Checkpoint_SD1_Config": {
+ "Main_GGUF_FLUX_Config": {
"properties": {
"key": {
"type": "string",
@@ -49116,23 +54718,20 @@
"title": "Config Path",
"description": "Path to the config for this model, if any."
},
+ "base": {
+ "type": "string",
+ "const": "flux",
+ "title": "Base",
+ "default": "flux"
+ },
"format": {
"type": "string",
- "const": "checkpoint",
+ "const": "gguf_quantized",
"title": "Format",
- "default": "checkpoint"
- },
- "prediction_type": {
- "$ref": "#/components/schemas/SchedulerPredictionType"
+ "default": "gguf_quantized"
},
"variant": {
- "$ref": "#/components/schemas/ModelVariantType"
- },
- "base": {
- "type": "string",
- "const": "sd-1",
- "title": "Base",
- "default": "sd-1"
+ "$ref": "#/components/schemas/FluxVariantType"
}
},
"type": "object",
@@ -49152,14 +54751,14 @@
"trigger_phrases",
"default_settings",
"config_path",
+ "base",
"format",
- "prediction_type",
- "variant",
- "base"
+ "variant"
],
- "title": "Main_Checkpoint_SD1_Config"
+ "title": "Main_GGUF_FLUX_Config",
+ "description": "Model config for main checkpoint models."
},
- "Main_Checkpoint_SD2_Config": {
+ "Main_GGUF_Flux2_Config": {
"properties": {
"key": {
"type": "string",
@@ -49288,23 +54887,20 @@
"title": "Config Path",
"description": "Path to the config for this model, if any."
},
+ "base": {
+ "type": "string",
+ "const": "flux2",
+ "title": "Base",
+ "default": "flux2"
+ },
"format": {
"type": "string",
- "const": "checkpoint",
+ "const": "gguf_quantized",
"title": "Format",
- "default": "checkpoint"
- },
- "prediction_type": {
- "$ref": "#/components/schemas/SchedulerPredictionType"
+ "default": "gguf_quantized"
},
"variant": {
- "$ref": "#/components/schemas/ModelVariantType"
- },
- "base": {
- "type": "string",
- "const": "sd-2",
- "title": "Base",
- "default": "sd-2"
+ "$ref": "#/components/schemas/Flux2VariantType"
}
},
"type": "object",
@@ -49324,14 +54920,14 @@
"trigger_phrases",
"default_settings",
"config_path",
+ "base",
"format",
- "prediction_type",
- "variant",
- "base"
+ "variant"
],
- "title": "Main_Checkpoint_SD2_Config"
+ "title": "Main_GGUF_Flux2_Config",
+ "description": "Model config for GGUF-quantized FLUX.2 checkpoint models (e.g. Klein)."
},
- "Main_Checkpoint_SDXLRefiner_Config": {
+ "Main_GGUF_QwenImage_Config": {
"properties": {
"key": {
"type": "string",
@@ -49460,23 +55056,27 @@
"title": "Config Path",
"description": "Path to the config for this model, if any."
},
+ "base": {
+ "type": "string",
+ "const": "qwen-image",
+ "title": "Base",
+ "default": "qwen-image"
+ },
"format": {
"type": "string",
- "const": "checkpoint",
+ "const": "gguf_quantized",
"title": "Format",
- "default": "checkpoint"
- },
- "prediction_type": {
- "$ref": "#/components/schemas/SchedulerPredictionType"
+ "default": "gguf_quantized"
},
"variant": {
- "$ref": "#/components/schemas/ModelVariantType"
- },
- "base": {
- "type": "string",
- "const": "sdxl-refiner",
- "title": "Base",
- "default": "sdxl-refiner"
+ "anyOf": [
+ {
+ "$ref": "#/components/schemas/QwenImageVariantType"
+ },
+ {
+ "type": "null"
+ }
+ ]
}
},
"type": "object",
@@ -49496,14 +55096,14 @@
"trigger_phrases",
"default_settings",
"config_path",
+ "base",
"format",
- "prediction_type",
- "variant",
- "base"
+ "variant"
],
- "title": "Main_Checkpoint_SDXLRefiner_Config"
+ "title": "Main_GGUF_QwenImage_Config",
+ "description": "Model config for GGUF-quantized Qwen Image transformer models."
},
- "Main_Checkpoint_SDXL_Config": {
+ "Main_GGUF_Wan_Config": {
"properties": {
"key": {
"type": "string",
@@ -49632,23 +55232,27 @@
"title": "Config Path",
"description": "Path to the config for this model, if any."
},
+ "base": {
+ "type": "string",
+ "const": "wan",
+ "title": "Base",
+ "default": "wan"
+ },
"format": {
"type": "string",
- "const": "checkpoint",
+ "const": "gguf_quantized",
"title": "Format",
- "default": "checkpoint"
- },
- "prediction_type": {
- "$ref": "#/components/schemas/SchedulerPredictionType"
+ "default": "gguf_quantized"
},
"variant": {
- "$ref": "#/components/schemas/ModelVariantType"
+ "$ref": "#/components/schemas/WanVariantType"
},
- "base": {
+ "expert": {
"type": "string",
- "const": "sdxl",
- "title": "Base",
- "default": "sdxl"
+ "enum": ["high", "low", "none"],
+ "title": "Expert",
+ "description": "For Wan 2.2 A14B's dual-expert MoE: 'high' for the high-noise expert, 'low' for the low-noise expert. 'none' for single-transformer models (TI2V-5B).",
+ "default": "none"
}
},
"type": "object",
@@ -49668,14 +55272,15 @@
"trigger_phrases",
"default_settings",
"config_path",
+ "base",
"format",
- "prediction_type",
"variant",
- "base"
+ "expert"
],
- "title": "Main_Checkpoint_SDXL_Config"
+ "title": "Main_GGUF_Wan_Config",
+ "description": "Model config for GGUF-quantized Wan 2.2 transformer models.\n\nA14B's MoE ships as two GGUF files (one per expert); ``expert`` records\nwhich one this is so the model loader invocation can pair them. TI2V-5B\nis a single-transformer model and stores ``expert='none'``."
},
- "Main_Checkpoint_ZImage_Config": {
+ "Main_GGUF_ZImage_Config": {
"properties": {
"key": {
"type": "string",
@@ -49812,9 +55417,9 @@
},
"format": {
"type": "string",
- "const": "checkpoint",
+ "const": "gguf_quantized",
"title": "Format",
- "default": "checkpoint"
+ "default": "gguf_quantized"
},
"variant": {
"$ref": "#/components/schemas/ZImageVariantType"
@@ -49841,704 +55446,1016 @@
"format",
"variant"
],
- "title": "Main_Checkpoint_ZImage_Config",
- "description": "Model config for Z-Image single-file checkpoint models (safetensors, etc)."
+ "title": "Main_GGUF_ZImage_Config",
+ "description": "Model config for GGUF-quantized Z-Image transformer models."
},
- "Main_Diffusers_CogView4_Config": {
+ "MaskCombineInvocation": {
+ "category": "mask",
+ "class": "invocation",
+ "classification": "stable",
+ "description": "Combine two masks together by multiplying them using `PIL.ImageChops.multiply()`.",
+ "node_pack": "invokeai",
"properties": {
- "key": {
- "type": "string",
- "title": "Key",
- "description": "A unique key for this model."
+ "board": {
+ "anyOf": [
+ {
+ "$ref": "#/components/schemas/BoardField"
+ },
+ {
+ "type": "null"
+ }
+ ],
+ "default": null,
+ "description": "The board to save the image to",
+ "field_kind": "internal",
+ "input": "direct",
+ "orig_required": false,
+ "ui_hidden": false
},
- "hash": {
- "type": "string",
- "title": "Hash",
- "description": "The hash of the model file(s)."
+ "metadata": {
+ "anyOf": [
+ {
+ "$ref": "#/components/schemas/MetadataField"
+ },
+ {
+ "type": "null"
+ }
+ ],
+ "default": null,
+ "description": "Optional metadata to be saved with the image",
+ "field_kind": "internal",
+ "input": "connection",
+ "orig_required": false,
+ "ui_hidden": false
},
- "path": {
- "type": "string",
- "title": "Path",
- "description": "Path to the model on the filesystem. Relative paths are relative to the Invoke root directory."
+ "id": {
+ "description": "The id of this instance of an invocation. Must be unique among all instances of invocations.",
+ "field_kind": "node_attribute",
+ "title": "Id",
+ "type": "string"
},
- "file_size": {
- "type": "integer",
- "title": "File Size",
- "description": "The size of the model in bytes."
+ "is_intermediate": {
+ "default": false,
+ "description": "Whether or not this is an intermediate invocation.",
+ "field_kind": "node_attribute",
+ "input": "direct",
+ "orig_required": true,
+ "title": "Is Intermediate",
+ "type": "boolean",
+ "ui_hidden": false,
+ "ui_type": "IsIntermediate"
},
- "name": {
- "type": "string",
- "title": "Name",
- "description": "Name of the model."
+ "use_cache": {
+ "default": true,
+ "description": "Whether or not to use the cache",
+ "field_kind": "node_attribute",
+ "title": "Use Cache",
+ "type": "boolean"
},
- "description": {
+ "mask1": {
"anyOf": [
{
- "type": "string"
+ "$ref": "#/components/schemas/ImageField"
},
{
"type": "null"
}
],
- "title": "Description",
- "description": "Model description"
+ "default": null,
+ "description": "The first mask to combine",
+ "field_kind": "input",
+ "input": "any",
+ "orig_required": true
},
- "source": {
- "type": "string",
- "title": "Source",
- "description": "The original source of the model (path, URL or repo_id)."
+ "mask2": {
+ "anyOf": [
+ {
+ "$ref": "#/components/schemas/ImageField"
+ },
+ {
+ "type": "null"
+ }
+ ],
+ "default": null,
+ "description": "The second image to combine",
+ "field_kind": "input",
+ "input": "any",
+ "orig_required": true
},
- "source_type": {
- "$ref": "#/components/schemas/ModelSourceType",
- "description": "The type of source"
+ "type": {
+ "const": "mask_combine",
+ "default": "mask_combine",
+ "field_kind": "node_attribute",
+ "title": "type",
+ "type": "string"
+ }
+ },
+ "required": ["type", "id"],
+ "tags": ["image", "mask", "multiply"],
+ "title": "Combine Masks",
+ "type": "object",
+ "version": "1.2.2",
+ "output": {
+ "$ref": "#/components/schemas/ImageOutput"
+ }
+ },
+ "MaskEdgeInvocation": {
+ "category": "mask",
+ "class": "invocation",
+ "classification": "stable",
+ "description": "Applies an edge mask to an image",
+ "node_pack": "invokeai",
+ "properties": {
+ "board": {
+ "anyOf": [
+ {
+ "$ref": "#/components/schemas/BoardField"
+ },
+ {
+ "type": "null"
+ }
+ ],
+ "default": null,
+ "description": "The board to save the image to",
+ "field_kind": "internal",
+ "input": "direct",
+ "orig_required": false,
+ "ui_hidden": false
},
- "source_api_response": {
+ "metadata": {
"anyOf": [
{
- "type": "string"
+ "$ref": "#/components/schemas/MetadataField"
},
{
"type": "null"
}
],
- "title": "Source Api Response",
- "description": "The original API response from the source, as stringified JSON."
+ "default": null,
+ "description": "Optional metadata to be saved with the image",
+ "field_kind": "internal",
+ "input": "connection",
+ "orig_required": false,
+ "ui_hidden": false
},
- "source_url": {
+ "id": {
+ "description": "The id of this instance of an invocation. Must be unique among all instances of invocations.",
+ "field_kind": "node_attribute",
+ "title": "Id",
+ "type": "string"
+ },
+ "is_intermediate": {
+ "default": false,
+ "description": "Whether or not this is an intermediate invocation.",
+ "field_kind": "node_attribute",
+ "input": "direct",
+ "orig_required": true,
+ "title": "Is Intermediate",
+ "type": "boolean",
+ "ui_hidden": false,
+ "ui_type": "IsIntermediate"
+ },
+ "use_cache": {
+ "default": true,
+ "description": "Whether or not to use the cache",
+ "field_kind": "node_attribute",
+ "title": "Use Cache",
+ "type": "boolean"
+ },
+ "image": {
"anyOf": [
{
- "type": "string"
+ "$ref": "#/components/schemas/ImageField"
},
{
"type": "null"
}
],
- "title": "Source Url",
- "description": "Optional URL for the model (e.g. download page or model page)."
+ "default": null,
+ "description": "The image to apply the mask to",
+ "field_kind": "input",
+ "input": "any",
+ "orig_required": true
},
- "cover_image": {
+ "edge_size": {
+ "anyOf": [
+ {
+ "type": "integer"
+ },
+ {
+ "type": "null"
+ }
+ ],
+ "default": null,
+ "description": "The size of the edge",
+ "field_kind": "input",
+ "input": "any",
+ "orig_required": true,
+ "title": "Edge Size"
+ },
+ "edge_blur": {
+ "anyOf": [
+ {
+ "type": "integer"
+ },
+ {
+ "type": "null"
+ }
+ ],
+ "default": null,
+ "description": "The amount of blur on the edge",
+ "field_kind": "input",
+ "input": "any",
+ "orig_required": true,
+ "title": "Edge Blur"
+ },
+ "low_threshold": {
+ "anyOf": [
+ {
+ "type": "integer"
+ },
+ {
+ "type": "null"
+ }
+ ],
+ "default": null,
+ "description": "First threshold for the hysteresis procedure in Canny edge detection",
+ "field_kind": "input",
+ "input": "any",
+ "orig_required": true,
+ "title": "Low Threshold"
+ },
+ "high_threshold": {
+ "anyOf": [
+ {
+ "type": "integer"
+ },
+ {
+ "type": "null"
+ }
+ ],
+ "default": null,
+ "description": "Second threshold for the hysteresis procedure in Canny edge detection",
+ "field_kind": "input",
+ "input": "any",
+ "orig_required": true,
+ "title": "High Threshold"
+ },
+ "type": {
+ "const": "mask_edge",
+ "default": "mask_edge",
+ "field_kind": "node_attribute",
+ "title": "type",
+ "type": "string"
+ }
+ },
+ "required": ["type", "id"],
+ "tags": ["image", "mask", "inpaint"],
+ "title": "Mask Edge",
+ "type": "object",
+ "version": "1.2.2",
+ "output": {
+ "$ref": "#/components/schemas/ImageOutput"
+ }
+ },
+ "MaskFromAlphaInvocation": {
+ "category": "mask",
+ "class": "invocation",
+ "classification": "stable",
+ "description": "Extracts the alpha channel of an image as a mask.",
+ "node_pack": "invokeai",
+ "properties": {
+ "board": {
"anyOf": [
{
- "type": "string"
+ "$ref": "#/components/schemas/BoardField"
},
{
"type": "null"
}
],
- "title": "Cover Image",
- "description": "Url for image to preview model"
- },
- "type": {
- "type": "string",
- "const": "main",
- "title": "Type",
- "default": "main"
+ "default": null,
+ "description": "The board to save the image to",
+ "field_kind": "internal",
+ "input": "direct",
+ "orig_required": false,
+ "ui_hidden": false
},
- "trigger_phrases": {
+ "metadata": {
"anyOf": [
{
- "items": {
- "type": "string"
- },
- "type": "array",
- "uniqueItems": true
+ "$ref": "#/components/schemas/MetadataField"
},
{
"type": "null"
}
],
- "title": "Trigger Phrases",
- "description": "Set of trigger phrases for this model"
+ "default": null,
+ "description": "Optional metadata to be saved with the image",
+ "field_kind": "internal",
+ "input": "connection",
+ "orig_required": false,
+ "ui_hidden": false
},
- "default_settings": {
+ "id": {
+ "description": "The id of this instance of an invocation. Must be unique among all instances of invocations.",
+ "field_kind": "node_attribute",
+ "title": "Id",
+ "type": "string"
+ },
+ "is_intermediate": {
+ "default": false,
+ "description": "Whether or not this is an intermediate invocation.",
+ "field_kind": "node_attribute",
+ "input": "direct",
+ "orig_required": true,
+ "title": "Is Intermediate",
+ "type": "boolean",
+ "ui_hidden": false,
+ "ui_type": "IsIntermediate"
+ },
+ "use_cache": {
+ "default": true,
+ "description": "Whether or not to use the cache",
+ "field_kind": "node_attribute",
+ "title": "Use Cache",
+ "type": "boolean"
+ },
+ "image": {
"anyOf": [
{
- "$ref": "#/components/schemas/MainModelDefaultSettings"
+ "$ref": "#/components/schemas/ImageField"
},
{
"type": "null"
}
],
- "description": "Default settings for this model"
- },
- "format": {
- "type": "string",
- "const": "diffusers",
- "title": "Format",
- "default": "diffusers"
+ "default": null,
+ "description": "The image to create the mask from",
+ "field_kind": "input",
+ "input": "any",
+ "orig_required": true
},
- "repo_variant": {
- "$ref": "#/components/schemas/ModelRepoVariant",
- "default": ""
+ "invert": {
+ "default": false,
+ "description": "Whether or not to invert the mask",
+ "field_kind": "input",
+ "input": "any",
+ "orig_default": false,
+ "orig_required": false,
+ "title": "Invert",
+ "type": "boolean"
},
- "base": {
- "type": "string",
- "const": "cogview4",
- "title": "Base",
- "default": "cogview4"
+ "type": {
+ "const": "tomask",
+ "default": "tomask",
+ "field_kind": "node_attribute",
+ "title": "type",
+ "type": "string"
}
},
+ "required": ["type", "id"],
+ "tags": ["image", "mask"],
+ "title": "Mask from Alpha",
"type": "object",
- "required": [
- "key",
- "hash",
- "path",
- "file_size",
- "name",
- "description",
- "source",
- "source_type",
- "source_api_response",
- "source_url",
- "cover_image",
- "type",
- "trigger_phrases",
- "default_settings",
- "format",
- "repo_variant",
- "base"
- ],
- "title": "Main_Diffusers_CogView4_Config"
+ "version": "1.2.2",
+ "output": {
+ "$ref": "#/components/schemas/ImageOutput"
+ }
},
- "Main_Diffusers_FLUX_Config": {
+ "MaskFromIDInvocation": {
+ "category": "mask",
+ "class": "invocation",
+ "classification": "stable",
+ "description": "Generate a mask for a particular color in an ID Map",
+ "node_pack": "invokeai",
"properties": {
- "key": {
- "type": "string",
- "title": "Key",
- "description": "A unique key for this model."
- },
- "hash": {
- "type": "string",
- "title": "Hash",
- "description": "The hash of the model file(s)."
- },
- "path": {
- "type": "string",
- "title": "Path",
- "description": "Path to the model on the filesystem. Relative paths are relative to the Invoke root directory."
- },
- "file_size": {
- "type": "integer",
- "title": "File Size",
- "description": "The size of the model in bytes."
- },
- "name": {
- "type": "string",
- "title": "Name",
- "description": "Name of the model."
- },
- "description": {
+ "board": {
"anyOf": [
{
- "type": "string"
+ "$ref": "#/components/schemas/BoardField"
},
{
"type": "null"
}
],
- "title": "Description",
- "description": "Model description"
- },
- "source": {
- "type": "string",
- "title": "Source",
- "description": "The original source of the model (path, URL or repo_id)."
- },
- "source_type": {
- "$ref": "#/components/schemas/ModelSourceType",
- "description": "The type of source"
+ "default": null,
+ "description": "The board to save the image to",
+ "field_kind": "internal",
+ "input": "direct",
+ "orig_required": false,
+ "ui_hidden": false
},
- "source_api_response": {
+ "metadata": {
"anyOf": [
{
- "type": "string"
+ "$ref": "#/components/schemas/MetadataField"
},
{
"type": "null"
}
],
- "title": "Source Api Response",
- "description": "The original API response from the source, as stringified JSON."
+ "default": null,
+ "description": "Optional metadata to be saved with the image",
+ "field_kind": "internal",
+ "input": "connection",
+ "orig_required": false,
+ "ui_hidden": false
},
- "source_url": {
- "anyOf": [
- {
- "type": "string"
- },
- {
- "type": "null"
- }
- ],
- "title": "Source Url",
- "description": "Optional URL for the model (e.g. download page or model page)."
+ "id": {
+ "description": "The id of this instance of an invocation. Must be unique among all instances of invocations.",
+ "field_kind": "node_attribute",
+ "title": "Id",
+ "type": "string"
},
- "cover_image": {
- "anyOf": [
- {
- "type": "string"
- },
- {
- "type": "null"
- }
- ],
- "title": "Cover Image",
- "description": "Url for image to preview model"
+ "is_intermediate": {
+ "default": false,
+ "description": "Whether or not this is an intermediate invocation.",
+ "field_kind": "node_attribute",
+ "input": "direct",
+ "orig_required": true,
+ "title": "Is Intermediate",
+ "type": "boolean",
+ "ui_hidden": false,
+ "ui_type": "IsIntermediate"
},
- "type": {
- "type": "string",
- "const": "main",
- "title": "Type",
- "default": "main"
+ "use_cache": {
+ "default": true,
+ "description": "Whether or not to use the cache",
+ "field_kind": "node_attribute",
+ "title": "Use Cache",
+ "type": "boolean"
},
- "trigger_phrases": {
+ "image": {
"anyOf": [
{
- "items": {
- "type": "string"
- },
- "type": "array",
- "uniqueItems": true
+ "$ref": "#/components/schemas/ImageField"
},
{
"type": "null"
}
],
- "title": "Trigger Phrases",
- "description": "Set of trigger phrases for this model"
+ "default": null,
+ "description": "The image to create the mask from",
+ "field_kind": "input",
+ "input": "any",
+ "orig_required": true
},
- "default_settings": {
+ "color": {
"anyOf": [
{
- "$ref": "#/components/schemas/MainModelDefaultSettings"
+ "$ref": "#/components/schemas/ColorField"
},
{
"type": "null"
}
],
- "description": "Default settings for this model"
- },
- "format": {
- "type": "string",
- "const": "diffusers",
- "title": "Format",
- "default": "diffusers"
+ "default": null,
+ "description": "ID color to mask",
+ "field_kind": "input",
+ "input": "any",
+ "orig_required": true
},
- "repo_variant": {
- "$ref": "#/components/schemas/ModelRepoVariant",
- "default": ""
+ "threshold": {
+ "default": 100,
+ "description": "Threshold for color detection",
+ "field_kind": "input",
+ "input": "any",
+ "orig_default": 100,
+ "orig_required": false,
+ "title": "Threshold",
+ "type": "integer"
},
- "base": {
- "type": "string",
- "const": "flux",
- "title": "Base",
- "default": "flux"
+ "invert": {
+ "default": false,
+ "description": "Whether or not to invert the mask",
+ "field_kind": "input",
+ "input": "any",
+ "orig_default": false,
+ "orig_required": false,
+ "title": "Invert",
+ "type": "boolean"
},
- "variant": {
- "$ref": "#/components/schemas/FluxVariantType"
+ "type": {
+ "const": "mask_from_id",
+ "default": "mask_from_id",
+ "field_kind": "node_attribute",
+ "title": "type",
+ "type": "string"
}
},
+ "required": ["type", "id"],
+ "tags": ["image", "mask", "id"],
+ "title": "Mask from Segmented Image",
"type": "object",
- "required": [
- "key",
- "hash",
- "path",
- "file_size",
- "name",
- "description",
- "source",
- "source_type",
- "source_api_response",
- "source_url",
- "cover_image",
- "type",
- "trigger_phrases",
- "default_settings",
- "format",
- "repo_variant",
- "base",
- "variant"
- ],
- "title": "Main_Diffusers_FLUX_Config",
- "description": "Model config for FLUX.1 models in diffusers format."
+ "version": "1.0.1",
+ "output": {
+ "$ref": "#/components/schemas/ImageOutput"
+ }
},
- "Main_Diffusers_Flux2_Config": {
+ "MaskOutput": {
+ "class": "output",
+ "description": "A torch mask tensor.",
"properties": {
- "key": {
- "type": "string",
- "title": "Key",
- "description": "A unique key for this model."
- },
- "hash": {
- "type": "string",
- "title": "Hash",
- "description": "The hash of the model file(s)."
- },
- "path": {
- "type": "string",
- "title": "Path",
- "description": "Path to the model on the filesystem. Relative paths are relative to the Invoke root directory."
+ "mask": {
+ "$ref": "#/components/schemas/TensorField",
+ "description": "The mask.",
+ "field_kind": "output",
+ "ui_hidden": false
},
- "file_size": {
+ "width": {
+ "description": "The width of the mask in pixels.",
+ "field_kind": "output",
+ "title": "Width",
"type": "integer",
- "title": "File Size",
- "description": "The size of the model in bytes."
+ "ui_hidden": false
},
- "name": {
- "type": "string",
- "title": "Name",
- "description": "Name of the model."
+ "height": {
+ "description": "The height of the mask in pixels.",
+ "field_kind": "output",
+ "title": "Height",
+ "type": "integer",
+ "ui_hidden": false
},
- "description": {
+ "type": {
+ "const": "mask_output",
+ "default": "mask_output",
+ "field_kind": "node_attribute",
+ "title": "type",
+ "type": "string"
+ }
+ },
+ "required": ["output_meta", "mask", "width", "height", "type", "type"],
+ "title": "MaskOutput",
+ "type": "object"
+ },
+ "MaskTensorToImageInvocation": {
+ "category": "mask",
+ "class": "invocation",
+ "classification": "stable",
+ "description": "Convert a mask tensor to an image.",
+ "node_pack": "invokeai",
+ "properties": {
+ "board": {
"anyOf": [
{
- "type": "string"
+ "$ref": "#/components/schemas/BoardField"
},
{
"type": "null"
}
],
- "title": "Description",
- "description": "Model description"
- },
- "source": {
- "type": "string",
- "title": "Source",
- "description": "The original source of the model (path, URL or repo_id)."
- },
- "source_type": {
- "$ref": "#/components/schemas/ModelSourceType",
- "description": "The type of source"
+ "default": null,
+ "description": "The board to save the image to",
+ "field_kind": "internal",
+ "input": "direct",
+ "orig_required": false,
+ "ui_hidden": false
},
- "source_api_response": {
+ "metadata": {
"anyOf": [
{
- "type": "string"
+ "$ref": "#/components/schemas/MetadataField"
},
{
"type": "null"
}
],
- "title": "Source Api Response",
- "description": "The original API response from the source, as stringified JSON."
+ "default": null,
+ "description": "Optional metadata to be saved with the image",
+ "field_kind": "internal",
+ "input": "connection",
+ "orig_required": false,
+ "ui_hidden": false
},
- "source_url": {
+ "id": {
+ "description": "The id of this instance of an invocation. Must be unique among all instances of invocations.",
+ "field_kind": "node_attribute",
+ "title": "Id",
+ "type": "string"
+ },
+ "is_intermediate": {
+ "default": false,
+ "description": "Whether or not this is an intermediate invocation.",
+ "field_kind": "node_attribute",
+ "input": "direct",
+ "orig_required": true,
+ "title": "Is Intermediate",
+ "type": "boolean",
+ "ui_hidden": false,
+ "ui_type": "IsIntermediate"
+ },
+ "use_cache": {
+ "default": true,
+ "description": "Whether or not to use the cache",
+ "field_kind": "node_attribute",
+ "title": "Use Cache",
+ "type": "boolean"
+ },
+ "mask": {
"anyOf": [
{
- "type": "string"
+ "$ref": "#/components/schemas/TensorField"
},
{
"type": "null"
}
],
- "title": "Source Url",
- "description": "Optional URL for the model (e.g. download page or model page)."
+ "default": null,
+ "description": "The mask tensor to convert.",
+ "field_kind": "input",
+ "input": "any",
+ "orig_required": true
},
- "cover_image": {
+ "type": {
+ "const": "tensor_mask_to_image",
+ "default": "tensor_mask_to_image",
+ "field_kind": "node_attribute",
+ "title": "type",
+ "type": "string"
+ }
+ },
+ "required": ["type", "id"],
+ "tags": ["mask"],
+ "title": "Tensor Mask to Image",
+ "type": "object",
+ "version": "1.1.0",
+ "output": {
+ "$ref": "#/components/schemas/ImageOutput"
+ }
+ },
+ "MediaCookieResponse": {
+ "properties": {
+ "success": {
+ "type": "boolean",
+ "title": "Success",
+ "description": "Whether the media cookie was set (always true in single-user mode)"
+ }
+ },
+ "type": "object",
+ "required": ["success"],
+ "title": "MediaCookieResponse",
+ "description": "Response from refreshing the media cookie."
+ },
+ "MediaPipeFaceDetectionInvocation": {
+ "category": "controlnet_preprocessors",
+ "class": "invocation",
+ "classification": "stable",
+ "description": "Detects faces using MediaPipe.",
+ "node_pack": "invokeai",
+ "properties": {
+ "board": {
"anyOf": [
{
- "type": "string"
+ "$ref": "#/components/schemas/BoardField"
},
{
"type": "null"
}
],
- "title": "Cover Image",
- "description": "Url for image to preview model"
- },
- "type": {
- "type": "string",
- "const": "main",
- "title": "Type",
- "default": "main"
+ "default": null,
+ "description": "The board to save the image to",
+ "field_kind": "internal",
+ "input": "direct",
+ "orig_required": false,
+ "ui_hidden": false
},
- "trigger_phrases": {
+ "metadata": {
"anyOf": [
{
- "items": {
- "type": "string"
- },
- "type": "array",
- "uniqueItems": true
+ "$ref": "#/components/schemas/MetadataField"
},
{
"type": "null"
}
],
- "title": "Trigger Phrases",
- "description": "Set of trigger phrases for this model"
+ "default": null,
+ "description": "Optional metadata to be saved with the image",
+ "field_kind": "internal",
+ "input": "connection",
+ "orig_required": false,
+ "ui_hidden": false
},
- "default_settings": {
+ "id": {
+ "description": "The id of this instance of an invocation. Must be unique among all instances of invocations.",
+ "field_kind": "node_attribute",
+ "title": "Id",
+ "type": "string"
+ },
+ "is_intermediate": {
+ "default": false,
+ "description": "Whether or not this is an intermediate invocation.",
+ "field_kind": "node_attribute",
+ "input": "direct",
+ "orig_required": true,
+ "title": "Is Intermediate",
+ "type": "boolean",
+ "ui_hidden": false,
+ "ui_type": "IsIntermediate"
+ },
+ "use_cache": {
+ "default": true,
+ "description": "Whether or not to use the cache",
+ "field_kind": "node_attribute",
+ "title": "Use Cache",
+ "type": "boolean"
+ },
+ "image": {
"anyOf": [
{
- "$ref": "#/components/schemas/MainModelDefaultSettings"
+ "$ref": "#/components/schemas/ImageField"
},
{
"type": "null"
}
],
- "description": "Default settings for this model"
- },
- "format": {
- "type": "string",
- "const": "diffusers",
- "title": "Format",
- "default": "diffusers"
+ "default": null,
+ "description": "The image to process",
+ "field_kind": "input",
+ "input": "any",
+ "orig_required": true
},
- "repo_variant": {
- "$ref": "#/components/schemas/ModelRepoVariant",
- "default": ""
+ "max_faces": {
+ "default": 1,
+ "description": "Maximum number of faces to detect",
+ "field_kind": "input",
+ "input": "any",
+ "minimum": 1,
+ "orig_default": 1,
+ "orig_required": false,
+ "title": "Max Faces",
+ "type": "integer"
},
- "base": {
- "type": "string",
- "const": "flux2",
- "title": "Base",
- "default": "flux2"
+ "min_confidence": {
+ "default": 0.5,
+ "description": "Minimum confidence for face detection",
+ "field_kind": "input",
+ "input": "any",
+ "maximum": 1,
+ "minimum": 0,
+ "orig_default": 0.5,
+ "orig_required": false,
+ "title": "Min Confidence",
+ "type": "number"
},
- "variant": {
- "$ref": "#/components/schemas/Flux2VariantType"
+ "type": {
+ "const": "mediapipe_face_detection",
+ "default": "mediapipe_face_detection",
+ "field_kind": "node_attribute",
+ "title": "type",
+ "type": "string"
}
},
+ "required": ["type", "id"],
+ "tags": ["controlnet", "face"],
+ "title": "MediaPipe Face Detection",
"type": "object",
- "required": [
- "key",
- "hash",
- "path",
- "file_size",
- "name",
- "description",
- "source",
- "source_type",
- "source_api_response",
- "source_url",
- "cover_image",
- "type",
- "trigger_phrases",
- "default_settings",
- "format",
- "repo_variant",
- "base",
- "variant"
- ],
- "title": "Main_Diffusers_Flux2_Config",
- "description": "Model config for FLUX.2 models in diffusers format (e.g. FLUX.2 Klein)."
+ "version": "1.0.0",
+ "output": {
+ "$ref": "#/components/schemas/ImageOutput"
+ }
},
- "Main_Diffusers_QwenImage_Config": {
+ "MergeMetadataInvocation": {
+ "category": "metadata",
+ "class": "invocation",
+ "classification": "stable",
+ "description": "Merged a collection of MetadataDict into a single MetadataDict.",
+ "node_pack": "invokeai",
"properties": {
- "key": {
- "type": "string",
- "title": "Key",
- "description": "A unique key for this model."
- },
- "hash": {
- "type": "string",
- "title": "Hash",
- "description": "The hash of the model file(s)."
- },
- "path": {
- "type": "string",
- "title": "Path",
- "description": "Path to the model on the filesystem. Relative paths are relative to the Invoke root directory."
+ "id": {
+ "description": "The id of this instance of an invocation. Must be unique among all instances of invocations.",
+ "field_kind": "node_attribute",
+ "title": "Id",
+ "type": "string"
},
- "file_size": {
- "type": "integer",
- "title": "File Size",
- "description": "The size of the model in bytes."
+ "is_intermediate": {
+ "default": false,
+ "description": "Whether or not this is an intermediate invocation.",
+ "field_kind": "node_attribute",
+ "input": "direct",
+ "orig_required": true,
+ "title": "Is Intermediate",
+ "type": "boolean",
+ "ui_hidden": false,
+ "ui_type": "IsIntermediate"
},
- "name": {
- "type": "string",
- "title": "Name",
- "description": "Name of the model."
+ "use_cache": {
+ "default": true,
+ "description": "Whether or not to use the cache",
+ "field_kind": "node_attribute",
+ "title": "Use Cache",
+ "type": "boolean"
},
- "description": {
+ "collection": {
"anyOf": [
{
- "type": "string"
+ "items": {
+ "$ref": "#/components/schemas/MetadataField"
+ },
+ "type": "array"
},
{
"type": "null"
}
],
- "title": "Description",
- "description": "Model description"
- },
- "source": {
- "type": "string",
- "title": "Source",
- "description": "The original source of the model (path, URL or repo_id)."
- },
- "source_type": {
- "$ref": "#/components/schemas/ModelSourceType",
- "description": "The type of source"
+ "default": null,
+ "description": "Collection of Metadata",
+ "field_kind": "input",
+ "input": "any",
+ "orig_required": true,
+ "title": "Collection"
},
- "source_api_response": {
+ "type": {
+ "const": "merge_metadata",
+ "default": "merge_metadata",
+ "field_kind": "node_attribute",
+ "title": "type",
+ "type": "string"
+ }
+ },
+ "required": ["type", "id"],
+ "tags": ["metadata"],
+ "title": "Metadata Merge",
+ "type": "object",
+ "version": "1.0.1",
+ "output": {
+ "$ref": "#/components/schemas/MetadataOutput"
+ }
+ },
+ "MergeTilesToImageInvocation": {
+ "category": "tiles",
+ "class": "invocation",
+ "classification": "stable",
+ "description": "Merge multiple tile images into a single image.",
+ "node_pack": "invokeai",
+ "properties": {
+ "board": {
"anyOf": [
{
- "type": "string"
+ "$ref": "#/components/schemas/BoardField"
},
{
"type": "null"
}
],
- "title": "Source Api Response",
- "description": "The original API response from the source, as stringified JSON."
+ "default": null,
+ "description": "The board to save the image to",
+ "field_kind": "internal",
+ "input": "direct",
+ "orig_required": false,
+ "ui_hidden": false
},
- "source_url": {
+ "metadata": {
"anyOf": [
{
- "type": "string"
+ "$ref": "#/components/schemas/MetadataField"
},
{
"type": "null"
}
],
- "title": "Source Url",
- "description": "Optional URL for the model (e.g. download page or model page)."
+ "default": null,
+ "description": "Optional metadata to be saved with the image",
+ "field_kind": "internal",
+ "input": "connection",
+ "orig_required": false,
+ "ui_hidden": false
},
- "cover_image": {
- "anyOf": [
- {
- "type": "string"
- },
- {
- "type": "null"
- }
- ],
- "title": "Cover Image",
- "description": "Url for image to preview model"
+ "id": {
+ "description": "The id of this instance of an invocation. Must be unique among all instances of invocations.",
+ "field_kind": "node_attribute",
+ "title": "Id",
+ "type": "string"
},
- "type": {
- "type": "string",
- "const": "main",
- "title": "Type",
- "default": "main"
+ "is_intermediate": {
+ "default": false,
+ "description": "Whether or not this is an intermediate invocation.",
+ "field_kind": "node_attribute",
+ "input": "direct",
+ "orig_required": true,
+ "title": "Is Intermediate",
+ "type": "boolean",
+ "ui_hidden": false,
+ "ui_type": "IsIntermediate"
},
- "trigger_phrases": {
+ "use_cache": {
+ "default": true,
+ "description": "Whether or not to use the cache",
+ "field_kind": "node_attribute",
+ "title": "Use Cache",
+ "type": "boolean"
+ },
+ "tiles_with_images": {
"anyOf": [
{
"items": {
- "type": "string"
+ "$ref": "#/components/schemas/TileWithImage"
},
- "type": "array",
- "uniqueItems": true
- },
- {
- "type": "null"
- }
- ],
- "title": "Trigger Phrases",
- "description": "Set of trigger phrases for this model"
- },
- "default_settings": {
- "anyOf": [
- {
- "$ref": "#/components/schemas/MainModelDefaultSettings"
+ "type": "array"
},
{
"type": "null"
}
],
- "description": "Default settings for this model"
- },
- "format": {
- "type": "string",
- "const": "diffusers",
- "title": "Format",
- "default": "diffusers"
+ "default": null,
+ "description": "A list of tile images with tile properties.",
+ "field_kind": "input",
+ "input": "any",
+ "orig_required": true,
+ "title": "Tiles With Images"
},
- "repo_variant": {
- "$ref": "#/components/schemas/ModelRepoVariant",
- "default": ""
+ "blend_mode": {
+ "default": "Seam",
+ "description": "blending type Linear or Seam",
+ "enum": ["Linear", "Seam"],
+ "field_kind": "input",
+ "input": "direct",
+ "orig_default": "Seam",
+ "orig_required": false,
+ "title": "Blend Mode",
+ "type": "string"
},
- "base": {
- "type": "string",
- "const": "qwen-image",
- "title": "Base",
- "default": "qwen-image"
+ "blend_amount": {
+ "default": 32,
+ "description": "The amount to blend adjacent tiles in pixels. Must be <= the amount of overlap between adjacent tiles.",
+ "field_kind": "input",
+ "input": "any",
+ "minimum": 0,
+ "orig_default": 32,
+ "orig_required": false,
+ "title": "Blend Amount",
+ "type": "integer"
},
- "variant": {
- "anyOf": [
- {
- "$ref": "#/components/schemas/QwenImageVariantType"
- },
- {
- "type": "null"
- }
- ]
+ "type": {
+ "const": "merge_tiles_to_image",
+ "default": "merge_tiles_to_image",
+ "field_kind": "node_attribute",
+ "title": "type",
+ "type": "string"
}
},
+ "required": ["type", "id"],
+ "tags": ["tiles"],
+ "title": "Merge Tiles to Image",
"type": "object",
- "required": [
- "key",
- "hash",
- "path",
- "file_size",
- "name",
- "description",
- "source",
- "source_type",
- "source_api_response",
- "source_url",
- "cover_image",
- "type",
- "trigger_phrases",
- "default_settings",
- "format",
- "repo_variant",
- "base",
- "variant"
- ],
- "title": "Main_Diffusers_QwenImage_Config",
- "description": "Model config for Qwen Image diffusers models (both txt2img and edit)."
+ "version": "1.1.1",
+ "output": {
+ "$ref": "#/components/schemas/ImageOutput"
+ }
},
- "Main_Diffusers_SD1_Config": {
+ "MetadataField": {
+ "additionalProperties": true,
+ "type": "object",
+ "title": "MetadataField",
+ "description": "Pydantic model for metadata with custom root of type dict[str, Any].\nMetadata is stored without a strict schema."
+ },
+ "MetadataFieldExtractorInvocation": {
+ "category": "metadata",
+ "class": "invocation",
+ "classification": "deprecated",
+ "description": "Extracts the text value from an image's metadata given a key.\nRaises an error if the image has no metadata or if the value is not a string (nesting not permitted).",
+ "node_pack": "invokeai",
"properties": {
- "key": {
- "type": "string",
- "title": "Key",
- "description": "A unique key for this model."
- },
- "hash": {
- "type": "string",
- "title": "Hash",
- "description": "The hash of the model file(s)."
- },
- "path": {
- "type": "string",
- "title": "Path",
- "description": "Path to the model on the filesystem. Relative paths are relative to the Invoke root directory."
+ "id": {
+ "description": "The id of this instance of an invocation. Must be unique among all instances of invocations.",
+ "field_kind": "node_attribute",
+ "title": "Id",
+ "type": "string"
},
- "file_size": {
- "type": "integer",
- "title": "File Size",
- "description": "The size of the model in bytes."
+ "is_intermediate": {
+ "default": false,
+ "description": "Whether or not this is an intermediate invocation.",
+ "field_kind": "node_attribute",
+ "input": "direct",
+ "orig_required": true,
+ "title": "Is Intermediate",
+ "type": "boolean",
+ "ui_hidden": false,
+ "ui_type": "IsIntermediate"
},
- "name": {
- "type": "string",
- "title": "Name",
- "description": "Name of the model."
+ "use_cache": {
+ "default": true,
+ "description": "Whether or not to use the cache",
+ "field_kind": "node_attribute",
+ "title": "Use Cache",
+ "type": "boolean"
},
- "description": {
+ "image": {
"anyOf": [
{
- "type": "string"
+ "$ref": "#/components/schemas/ImageField"
},
{
"type": "null"
}
],
- "title": "Description",
- "description": "Model description"
- },
- "source": {
- "type": "string",
- "title": "Source",
- "description": "The original source of the model (path, URL or repo_id)."
- },
- "source_type": {
- "$ref": "#/components/schemas/ModelSourceType",
- "description": "The type of source"
+ "default": null,
+ "description": "The image to extract metadata from",
+ "field_kind": "input",
+ "input": "any",
+ "orig_required": true
},
- "source_api_response": {
+ "key": {
"anyOf": [
{
"type": "string"
@@ -50547,141 +56464,211 @@
"type": "null"
}
],
- "title": "Source Api Response",
- "description": "The original API response from the source, as stringified JSON."
+ "default": null,
+ "description": "The key in the image's metadata to extract the value from",
+ "field_kind": "input",
+ "input": "any",
+ "orig_required": true,
+ "title": "Key"
+ },
+ "type": {
+ "const": "metadata_field_extractor",
+ "default": "metadata_field_extractor",
+ "field_kind": "node_attribute",
+ "title": "type",
+ "type": "string"
+ }
+ },
+ "required": ["type", "id"],
+ "tags": ["metadata"],
+ "title": "Metadata Field Extractor",
+ "type": "object",
+ "version": "1.0.0",
+ "output": {
+ "$ref": "#/components/schemas/StringOutput"
+ }
+ },
+ "MetadataFromImageInvocation": {
+ "category": "metadata",
+ "class": "invocation",
+ "classification": "beta",
+ "description": "Used to create a core metadata item then Add/Update it to the provided metadata",
+ "node_pack": "invokeai",
+ "properties": {
+ "id": {
+ "description": "The id of this instance of an invocation. Must be unique among all instances of invocations.",
+ "field_kind": "node_attribute",
+ "title": "Id",
+ "type": "string"
+ },
+ "is_intermediate": {
+ "default": false,
+ "description": "Whether or not this is an intermediate invocation.",
+ "field_kind": "node_attribute",
+ "input": "direct",
+ "orig_required": true,
+ "title": "Is Intermediate",
+ "type": "boolean",
+ "ui_hidden": false,
+ "ui_type": "IsIntermediate"
},
- "source_url": {
- "anyOf": [
- {
- "type": "string"
- },
- {
- "type": "null"
- }
- ],
- "title": "Source Url",
- "description": "Optional URL for the model (e.g. download page or model page)."
+ "use_cache": {
+ "default": true,
+ "description": "Whether or not to use the cache",
+ "field_kind": "node_attribute",
+ "title": "Use Cache",
+ "type": "boolean"
},
- "cover_image": {
+ "image": {
"anyOf": [
{
- "type": "string"
+ "$ref": "#/components/schemas/ImageField"
},
{
"type": "null"
}
],
- "title": "Cover Image",
- "description": "Url for image to preview model"
+ "default": null,
+ "description": "The image to process",
+ "field_kind": "input",
+ "input": "any",
+ "orig_required": true
},
"type": {
- "type": "string",
- "const": "main",
- "title": "Type",
- "default": "main"
+ "const": "metadata_from_image",
+ "default": "metadata_from_image",
+ "field_kind": "node_attribute",
+ "title": "type",
+ "type": "string"
+ }
+ },
+ "required": ["type", "id"],
+ "tags": ["metadata"],
+ "title": "Metadata From Image",
+ "type": "object",
+ "version": "1.0.1",
+ "output": {
+ "$ref": "#/components/schemas/MetadataOutput"
+ }
+ },
+ "MetadataInvocation": {
+ "category": "metadata",
+ "class": "invocation",
+ "classification": "stable",
+ "description": "Takes a MetadataItem or collection of MetadataItems and outputs a MetadataDict.",
+ "node_pack": "invokeai",
+ "properties": {
+ "id": {
+ "description": "The id of this instance of an invocation. Must be unique among all instances of invocations.",
+ "field_kind": "node_attribute",
+ "title": "Id",
+ "type": "string"
},
- "trigger_phrases": {
+ "is_intermediate": {
+ "default": false,
+ "description": "Whether or not this is an intermediate invocation.",
+ "field_kind": "node_attribute",
+ "input": "direct",
+ "orig_required": true,
+ "title": "Is Intermediate",
+ "type": "boolean",
+ "ui_hidden": false,
+ "ui_type": "IsIntermediate"
+ },
+ "use_cache": {
+ "default": true,
+ "description": "Whether or not to use the cache",
+ "field_kind": "node_attribute",
+ "title": "Use Cache",
+ "type": "boolean"
+ },
+ "items": {
"anyOf": [
{
"items": {
- "type": "string"
+ "$ref": "#/components/schemas/MetadataItemField"
},
- "type": "array",
- "uniqueItems": true
+ "type": "array"
},
{
- "type": "null"
- }
- ],
- "title": "Trigger Phrases",
- "description": "Set of trigger phrases for this model"
- },
- "default_settings": {
- "anyOf": [
- {
- "$ref": "#/components/schemas/MainModelDefaultSettings"
+ "$ref": "#/components/schemas/MetadataItemField"
},
{
"type": "null"
}
],
- "description": "Default settings for this model"
- },
- "format": {
- "type": "string",
- "const": "diffusers",
- "title": "Format",
- "default": "diffusers"
- },
- "repo_variant": {
- "$ref": "#/components/schemas/ModelRepoVariant",
- "default": ""
- },
- "prediction_type": {
- "$ref": "#/components/schemas/SchedulerPredictionType"
- },
- "variant": {
- "$ref": "#/components/schemas/ModelVariantType"
+ "default": null,
+ "description": "A single metadata item or collection of metadata items",
+ "field_kind": "input",
+ "input": "any",
+ "orig_required": true,
+ "title": "Items"
},
- "base": {
- "type": "string",
- "const": "sd-1",
- "title": "Base",
- "default": "sd-1"
+ "type": {
+ "const": "metadata",
+ "default": "metadata",
+ "field_kind": "node_attribute",
+ "title": "type",
+ "type": "string"
}
},
+ "required": ["type", "id"],
+ "tags": ["metadata"],
+ "title": "Metadata",
"type": "object",
- "required": [
- "key",
- "hash",
- "path",
- "file_size",
- "name",
- "description",
- "source",
- "source_type",
- "source_api_response",
- "source_url",
- "cover_image",
- "type",
- "trigger_phrases",
- "default_settings",
- "format",
- "repo_variant",
- "prediction_type",
- "variant",
- "base"
- ],
- "title": "Main_Diffusers_SD1_Config"
+ "version": "1.0.1",
+ "output": {
+ "$ref": "#/components/schemas/MetadataOutput"
+ }
},
- "Main_Diffusers_SD2_Config": {
+ "MetadataItemField": {
"properties": {
- "key": {
- "type": "string",
- "title": "Key",
- "description": "A unique key for this model."
- },
- "hash": {
- "type": "string",
- "title": "Hash",
- "description": "The hash of the model file(s)."
+ "label": {
+ "description": "Label for this metadata item",
+ "title": "Label",
+ "type": "string"
},
- "path": {
- "type": "string",
- "title": "Path",
- "description": "Path to the model on the filesystem. Relative paths are relative to the Invoke root directory."
+ "value": {
+ "description": "The value for this metadata item (may be any type)",
+ "title": "Value"
+ }
+ },
+ "required": ["label", "value"],
+ "title": "MetadataItemField",
+ "type": "object"
+ },
+ "MetadataItemInvocation": {
+ "category": "metadata",
+ "class": "invocation",
+ "classification": "stable",
+ "description": "Used to create an arbitrary metadata item. Provide \"label\" and make a connection to \"value\" to store that data as the value.",
+ "node_pack": "invokeai",
+ "properties": {
+ "id": {
+ "description": "The id of this instance of an invocation. Must be unique among all instances of invocations.",
+ "field_kind": "node_attribute",
+ "title": "Id",
+ "type": "string"
},
- "file_size": {
- "type": "integer",
- "title": "File Size",
- "description": "The size of the model in bytes."
+ "is_intermediate": {
+ "default": false,
+ "description": "Whether or not this is an intermediate invocation.",
+ "field_kind": "node_attribute",
+ "input": "direct",
+ "orig_required": true,
+ "title": "Is Intermediate",
+ "type": "boolean",
+ "ui_hidden": false,
+ "ui_type": "IsIntermediate"
},
- "name": {
- "type": "string",
- "title": "Name",
- "description": "Name of the model."
+ "use_cache": {
+ "default": true,
+ "description": "Whether or not to use the cache",
+ "field_kind": "node_attribute",
+ "title": "Use Cache",
+ "type": "boolean"
},
- "description": {
+ "label": {
"anyOf": [
{
"type": "string"
@@ -50690,207 +56677,275 @@
"type": "null"
}
],
- "title": "Description",
- "description": "Model description"
- },
- "source": {
- "type": "string",
- "title": "Source",
- "description": "The original source of the model (path, URL or repo_id)."
- },
- "source_type": {
- "$ref": "#/components/schemas/ModelSourceType",
- "description": "The type of source"
+ "default": null,
+ "description": "Label for this metadata item",
+ "field_kind": "input",
+ "input": "any",
+ "orig_required": true,
+ "title": "Label"
},
- "source_api_response": {
+ "value": {
"anyOf": [
- {
- "type": "string"
- },
+ {},
{
"type": "null"
}
],
- "title": "Source Api Response",
- "description": "The original API response from the source, as stringified JSON."
+ "default": null,
+ "description": "The value for this metadata item (may be any type)",
+ "field_kind": "input",
+ "input": "any",
+ "orig_required": true,
+ "title": "Value",
+ "ui_type": "AnyField"
},
- "source_url": {
+ "type": {
+ "const": "metadata_item",
+ "default": "metadata_item",
+ "field_kind": "node_attribute",
+ "title": "type",
+ "type": "string"
+ }
+ },
+ "required": ["type", "id"],
+ "tags": ["metadata"],
+ "title": "Metadata Item",
+ "type": "object",
+ "version": "1.0.1",
+ "output": {
+ "$ref": "#/components/schemas/MetadataItemOutput"
+ }
+ },
+ "MetadataItemLinkedInvocation": {
+ "category": "metadata",
+ "class": "invocation",
+ "classification": "beta",
+ "description": "Used to Create/Add/Update a value into a metadata label",
+ "node_pack": "invokeai",
+ "properties": {
+ "metadata": {
"anyOf": [
{
- "type": "string"
+ "$ref": "#/components/schemas/MetadataField"
},
{
"type": "null"
}
],
- "title": "Source Url",
- "description": "Optional URL for the model (e.g. download page or model page)."
+ "default": null,
+ "description": "Optional metadata to be saved with the image",
+ "field_kind": "internal",
+ "input": "connection",
+ "orig_required": false,
+ "ui_hidden": false
},
- "cover_image": {
- "anyOf": [
- {
- "type": "string"
- },
- {
- "type": "null"
- }
- ],
- "title": "Cover Image",
- "description": "Url for image to preview model"
+ "id": {
+ "description": "The id of this instance of an invocation. Must be unique among all instances of invocations.",
+ "field_kind": "node_attribute",
+ "title": "Id",
+ "type": "string"
},
- "type": {
- "type": "string",
- "const": "main",
- "title": "Type",
- "default": "main"
+ "is_intermediate": {
+ "default": false,
+ "description": "Whether or not this is an intermediate invocation.",
+ "field_kind": "node_attribute",
+ "input": "direct",
+ "orig_required": true,
+ "title": "Is Intermediate",
+ "type": "boolean",
+ "ui_hidden": false,
+ "ui_type": "IsIntermediate"
},
- "trigger_phrases": {
+ "use_cache": {
+ "default": true,
+ "description": "Whether or not to use the cache",
+ "field_kind": "node_attribute",
+ "title": "Use Cache",
+ "type": "boolean"
+ },
+ "label": {
+ "default": "* CUSTOM LABEL *",
+ "description": "Label for this metadata item",
+ "enum": [
+ "* CUSTOM LABEL *",
+ "positive_prompt",
+ "positive_style_prompt",
+ "negative_prompt",
+ "negative_style_prompt",
+ "width",
+ "height",
+ "seed",
+ "cfg_scale",
+ "cfg_rescale_multiplier",
+ "steps",
+ "scheduler",
+ "clip_skip",
+ "model",
+ "vae",
+ "seamless_x",
+ "seamless_y",
+ "guidance",
+ "cfg_scale_start_step",
+ "cfg_scale_end_step"
+ ],
+ "field_kind": "input",
+ "input": "direct",
+ "orig_default": "* CUSTOM LABEL *",
+ "orig_required": false,
+ "title": "Label",
+ "type": "string"
+ },
+ "custom_label": {
"anyOf": [
{
- "items": {
- "type": "string"
- },
- "type": "array",
- "uniqueItems": true
+ "type": "string"
},
{
"type": "null"
}
],
- "title": "Trigger Phrases",
- "description": "Set of trigger phrases for this model"
+ "default": null,
+ "description": "Label for this metadata item",
+ "field_kind": "input",
+ "input": "direct",
+ "orig_default": null,
+ "orig_required": false,
+ "title": "Custom Label"
},
- "default_settings": {
+ "value": {
"anyOf": [
- {
- "$ref": "#/components/schemas/MainModelDefaultSettings"
- },
+ {},
{
"type": "null"
}
],
- "description": "Default settings for this model"
- },
- "format": {
- "type": "string",
- "const": "diffusers",
- "title": "Format",
- "default": "diffusers"
- },
- "repo_variant": {
- "$ref": "#/components/schemas/ModelRepoVariant",
- "default": ""
- },
- "prediction_type": {
- "$ref": "#/components/schemas/SchedulerPredictionType"
- },
- "variant": {
- "$ref": "#/components/schemas/ModelVariantType"
+ "default": null,
+ "description": "The value for this metadata item (may be any type)",
+ "field_kind": "input",
+ "input": "any",
+ "orig_required": true,
+ "title": "Value",
+ "ui_type": "AnyField"
},
- "base": {
- "type": "string",
- "const": "sd-2",
- "title": "Base",
- "default": "sd-2"
+ "type": {
+ "const": "metadata_item_linked",
+ "default": "metadata_item_linked",
+ "field_kind": "node_attribute",
+ "title": "type",
+ "type": "string"
}
},
+ "required": ["type", "id"],
+ "tags": ["metadata"],
+ "title": "Metadata Item Linked",
"type": "object",
- "required": [
- "key",
- "hash",
- "path",
- "file_size",
- "name",
- "description",
- "source",
- "source_type",
- "source_api_response",
- "source_url",
- "cover_image",
- "type",
- "trigger_phrases",
- "default_settings",
- "format",
- "repo_variant",
- "prediction_type",
- "variant",
- "base"
- ],
- "title": "Main_Diffusers_SD2_Config"
+ "version": "1.0.1",
+ "output": {
+ "$ref": "#/components/schemas/MetadataOutput"
+ }
},
- "Main_Diffusers_SD3_Config": {
+ "MetadataItemOutput": {
+ "class": "output",
+ "description": "Metadata Item Output",
"properties": {
- "key": {
- "type": "string",
- "title": "Key",
- "description": "A unique key for this model."
- },
- "hash": {
- "type": "string",
- "title": "Hash",
- "description": "The hash of the model file(s)."
- },
- "path": {
- "type": "string",
- "title": "Path",
- "description": "Path to the model on the filesystem. Relative paths are relative to the Invoke root directory."
- },
- "file_size": {
- "type": "integer",
- "title": "File Size",
- "description": "The size of the model in bytes."
+ "item": {
+ "$ref": "#/components/schemas/MetadataItemField",
+ "description": "Metadata Item",
+ "field_kind": "output",
+ "ui_hidden": false
},
- "name": {
- "type": "string",
- "title": "Name",
- "description": "Name of the model."
+ "type": {
+ "const": "metadata_item_output",
+ "default": "metadata_item_output",
+ "field_kind": "node_attribute",
+ "title": "type",
+ "type": "string"
+ }
+ },
+ "required": ["output_meta", "item", "type", "type"],
+ "title": "MetadataItemOutput",
+ "type": "object"
+ },
+ "MetadataOutput": {
+ "class": "output",
+ "properties": {
+ "metadata": {
+ "$ref": "#/components/schemas/MetadataField",
+ "description": "Metadata Dict",
+ "field_kind": "output",
+ "ui_hidden": false
},
- "description": {
+ "type": {
+ "const": "metadata_output",
+ "default": "metadata_output",
+ "field_kind": "node_attribute",
+ "title": "type",
+ "type": "string"
+ }
+ },
+ "required": ["output_meta", "metadata", "type", "type"],
+ "title": "MetadataOutput",
+ "type": "object"
+ },
+ "MetadataToBoolCollectionInvocation": {
+ "category": "metadata",
+ "class": "invocation",
+ "classification": "beta",
+ "description": "Extracts a Boolean value Collection of a label from metadata",
+ "node_pack": "invokeai",
+ "properties": {
+ "metadata": {
"anyOf": [
{
- "type": "string"
+ "$ref": "#/components/schemas/MetadataField"
},
{
"type": "null"
}
],
- "title": "Description",
- "description": "Model description"
+ "default": null,
+ "description": "Optional metadata to be saved with the image",
+ "field_kind": "internal",
+ "input": "connection",
+ "orig_required": false,
+ "ui_hidden": false
},
- "source": {
- "type": "string",
- "title": "Source",
- "description": "The original source of the model (path, URL or repo_id)."
+ "id": {
+ "description": "The id of this instance of an invocation. Must be unique among all instances of invocations.",
+ "field_kind": "node_attribute",
+ "title": "Id",
+ "type": "string"
},
- "source_type": {
- "$ref": "#/components/schemas/ModelSourceType",
- "description": "The type of source"
+ "is_intermediate": {
+ "default": false,
+ "description": "Whether or not this is an intermediate invocation.",
+ "field_kind": "node_attribute",
+ "input": "direct",
+ "orig_required": true,
+ "title": "Is Intermediate",
+ "type": "boolean",
+ "ui_hidden": false,
+ "ui_type": "IsIntermediate"
},
- "source_api_response": {
- "anyOf": [
- {
- "type": "string"
- },
- {
- "type": "null"
- }
- ],
- "title": "Source Api Response",
- "description": "The original API response from the source, as stringified JSON."
+ "use_cache": {
+ "default": true,
+ "description": "Whether or not to use the cache",
+ "field_kind": "node_attribute",
+ "title": "Use Cache",
+ "type": "boolean"
},
- "source_url": {
- "anyOf": [
- {
- "type": "string"
- },
- {
- "type": "null"
- }
- ],
- "title": "Source Url",
- "description": "Optional URL for the model (e.g. download page or model page)."
+ "label": {
+ "default": "* CUSTOM LABEL *",
+ "description": "Label for this metadata item",
+ "enum": ["* CUSTOM LABEL *", "seamless_x", "seamless_y"],
+ "field_kind": "input",
+ "input": "direct",
+ "orig_default": "* CUSTOM LABEL *",
+ "orig_required": false,
+ "title": "Label",
+ "type": "string"
},
- "cover_image": {
+ "custom_label": {
"anyOf": [
{
"type": "string"
@@ -50899,149 +56954,109 @@
"type": "null"
}
],
- "title": "Cover Image",
- "description": "Url for image to preview model"
- },
- "type": {
- "type": "string",
- "const": "main",
- "title": "Type",
- "default": "main"
+ "default": null,
+ "description": "Label for this metadata item",
+ "field_kind": "input",
+ "input": "direct",
+ "orig_default": null,
+ "orig_required": false,
+ "title": "Custom Label"
},
- "trigger_phrases": {
+ "default_value": {
"anyOf": [
{
"items": {
- "type": "string"
+ "type": "boolean"
},
- "type": "array",
- "uniqueItems": true
- },
- {
- "type": "null"
- }
- ],
- "title": "Trigger Phrases",
- "description": "Set of trigger phrases for this model"
- },
- "default_settings": {
- "anyOf": [
- {
- "$ref": "#/components/schemas/MainModelDefaultSettings"
+ "type": "array"
},
{
"type": "null"
}
],
- "description": "Default settings for this model"
- },
- "format": {
- "type": "string",
- "const": "diffusers",
- "title": "Format",
- "default": "diffusers"
- },
- "repo_variant": {
- "$ref": "#/components/schemas/ModelRepoVariant",
- "default": ""
- },
- "base": {
- "type": "string",
- "const": "sd-3",
- "title": "Base",
- "default": "sd-3"
+ "default": null,
+ "description": "The default bool to use if not found in the metadata",
+ "field_kind": "input",
+ "input": "any",
+ "orig_required": true,
+ "title": "Default Value"
},
- "submodels": {
- "anyOf": [
- {
- "additionalProperties": {
- "$ref": "#/components/schemas/SubmodelDefinition"
- },
- "propertyNames": {
- "$ref": "#/components/schemas/SubModelType"
- },
- "type": "object"
- },
- {
- "type": "null"
- }
- ],
- "title": "Submodels",
- "description": "Loadable submodels in this model"
+ "type": {
+ "const": "metadata_to_bool_collection",
+ "default": "metadata_to_bool_collection",
+ "field_kind": "node_attribute",
+ "title": "type",
+ "type": "string"
}
},
+ "required": ["type", "id"],
+ "tags": ["metadata"],
+ "title": "Metadata To Bool Collection",
"type": "object",
- "required": [
- "key",
- "hash",
- "path",
- "file_size",
- "name",
- "description",
- "source",
- "source_type",
- "source_api_response",
- "source_url",
- "cover_image",
- "type",
- "trigger_phrases",
- "default_settings",
- "format",
- "repo_variant",
- "base",
- "submodels"
- ],
- "title": "Main_Diffusers_SD3_Config"
+ "version": "1.0.0",
+ "output": {
+ "$ref": "#/components/schemas/BooleanCollectionOutput"
+ }
},
- "Main_Diffusers_SDXLRefiner_Config": {
+ "MetadataToBoolInvocation": {
+ "category": "metadata",
+ "class": "invocation",
+ "classification": "beta",
+ "description": "Extracts a Boolean value of a label from metadata",
+ "node_pack": "invokeai",
"properties": {
- "key": {
- "type": "string",
- "title": "Key",
- "description": "A unique key for this model."
- },
- "hash": {
- "type": "string",
- "title": "Hash",
- "description": "The hash of the model file(s)."
- },
- "path": {
- "type": "string",
- "title": "Path",
- "description": "Path to the model on the filesystem. Relative paths are relative to the Invoke root directory."
- },
- "file_size": {
- "type": "integer",
- "title": "File Size",
- "description": "The size of the model in bytes."
- },
- "name": {
- "type": "string",
- "title": "Name",
- "description": "Name of the model."
- },
- "description": {
+ "metadata": {
"anyOf": [
{
- "type": "string"
+ "$ref": "#/components/schemas/MetadataField"
},
{
"type": "null"
}
],
- "title": "Description",
- "description": "Model description"
+ "default": null,
+ "description": "Optional metadata to be saved with the image",
+ "field_kind": "internal",
+ "input": "connection",
+ "orig_required": false,
+ "ui_hidden": false
},
- "source": {
- "type": "string",
- "title": "Source",
- "description": "The original source of the model (path, URL or repo_id)."
+ "id": {
+ "description": "The id of this instance of an invocation. Must be unique among all instances of invocations.",
+ "field_kind": "node_attribute",
+ "title": "Id",
+ "type": "string"
},
- "source_type": {
- "$ref": "#/components/schemas/ModelSourceType",
- "description": "The type of source"
+ "is_intermediate": {
+ "default": false,
+ "description": "Whether or not this is an intermediate invocation.",
+ "field_kind": "node_attribute",
+ "input": "direct",
+ "orig_required": true,
+ "title": "Is Intermediate",
+ "type": "boolean",
+ "ui_hidden": false,
+ "ui_type": "IsIntermediate"
},
- "source_api_response": {
+ "use_cache": {
+ "default": true,
+ "description": "Whether or not to use the cache",
+ "field_kind": "node_attribute",
+ "title": "Use Cache",
+ "type": "boolean"
+ },
+ "label": {
+ "default": "* CUSTOM LABEL *",
+ "description": "Label for this metadata item",
+ "enum": ["* CUSTOM LABEL *", "seamless_x", "seamless_y"],
+ "field_kind": "input",
+ "input": "direct",
+ "orig_default": "* CUSTOM LABEL *",
+ "orig_required": false,
+ "title": "Label",
+ "type": "string"
+ },
+ "custom_label": {
"anyOf": [
{
"type": "string"
@@ -51050,162 +57065,192 @@
"type": "null"
}
],
- "title": "Source Api Response",
- "description": "The original API response from the source, as stringified JSON."
+ "default": null,
+ "description": "Label for this metadata item",
+ "field_kind": "input",
+ "input": "direct",
+ "orig_default": null,
+ "orig_required": false,
+ "title": "Custom Label"
},
- "source_url": {
+ "default_value": {
"anyOf": [
{
- "type": "string"
+ "type": "boolean"
},
{
"type": "null"
}
],
- "title": "Source Url",
- "description": "Optional URL for the model (e.g. download page or model page)."
+ "default": null,
+ "description": "The default bool to use if not found in the metadata",
+ "field_kind": "input",
+ "input": "any",
+ "orig_required": true,
+ "title": "Default Value"
},
- "cover_image": {
+ "type": {
+ "const": "metadata_to_bool",
+ "default": "metadata_to_bool",
+ "field_kind": "node_attribute",
+ "title": "type",
+ "type": "string"
+ }
+ },
+ "required": ["type", "id"],
+ "tags": ["metadata"],
+ "title": "Metadata To Bool",
+ "type": "object",
+ "version": "1.0.0",
+ "output": {
+ "$ref": "#/components/schemas/BooleanOutput"
+ }
+ },
+ "MetadataToControlnetsInvocation": {
+ "category": "metadata",
+ "class": "invocation",
+ "classification": "beta",
+ "description": "Extracts a Controlnets value of a label from metadata",
+ "node_pack": "invokeai",
+ "properties": {
+ "metadata": {
"anyOf": [
{
- "type": "string"
+ "$ref": "#/components/schemas/MetadataField"
},
{
"type": "null"
}
],
- "title": "Cover Image",
- "description": "Url for image to preview model"
+ "default": null,
+ "description": "Optional metadata to be saved with the image",
+ "field_kind": "internal",
+ "input": "connection",
+ "orig_required": false,
+ "ui_hidden": false
},
- "type": {
- "type": "string",
- "const": "main",
- "title": "Type",
- "default": "main"
+ "id": {
+ "description": "The id of this instance of an invocation. Must be unique among all instances of invocations.",
+ "field_kind": "node_attribute",
+ "title": "Id",
+ "type": "string"
},
- "trigger_phrases": {
+ "is_intermediate": {
+ "default": false,
+ "description": "Whether or not this is an intermediate invocation.",
+ "field_kind": "node_attribute",
+ "input": "direct",
+ "orig_required": true,
+ "title": "Is Intermediate",
+ "type": "boolean",
+ "ui_hidden": false,
+ "ui_type": "IsIntermediate"
+ },
+ "use_cache": {
+ "default": true,
+ "description": "Whether or not to use the cache",
+ "field_kind": "node_attribute",
+ "title": "Use Cache",
+ "type": "boolean"
+ },
+ "control_list": {
"anyOf": [
{
- "items": {
- "type": "string"
- },
- "type": "array",
- "uniqueItems": true
+ "$ref": "#/components/schemas/ControlField"
},
{
- "type": "null"
- }
- ],
- "title": "Trigger Phrases",
- "description": "Set of trigger phrases for this model"
- },
- "default_settings": {
- "anyOf": [
- {
- "$ref": "#/components/schemas/MainModelDefaultSettings"
+ "items": {
+ "$ref": "#/components/schemas/ControlField"
+ },
+ "type": "array"
},
{
"type": "null"
}
],
- "description": "Default settings for this model"
- },
- "format": {
- "type": "string",
- "const": "diffusers",
- "title": "Format",
- "default": "diffusers"
- },
- "repo_variant": {
- "$ref": "#/components/schemas/ModelRepoVariant",
- "default": ""
- },
- "prediction_type": {
- "$ref": "#/components/schemas/SchedulerPredictionType"
- },
- "variant": {
- "$ref": "#/components/schemas/ModelVariantType"
+ "default": null,
+ "field_kind": "input",
+ "input": "connection",
+ "orig_default": null,
+ "orig_required": false,
+ "title": "ControlNet-List"
},
- "base": {
- "type": "string",
- "const": "sdxl-refiner",
- "title": "Base",
- "default": "sdxl-refiner"
+ "type": {
+ "const": "metadata_to_controlnets",
+ "default": "metadata_to_controlnets",
+ "field_kind": "node_attribute",
+ "title": "type",
+ "type": "string"
}
},
+ "required": ["type", "id"],
+ "tags": ["metadata"],
+ "title": "Metadata To ControlNets",
"type": "object",
- "required": [
- "key",
- "hash",
- "path",
- "file_size",
- "name",
- "description",
- "source",
- "source_type",
- "source_api_response",
- "source_url",
- "cover_image",
- "type",
- "trigger_phrases",
- "default_settings",
- "format",
- "repo_variant",
- "prediction_type",
- "variant",
- "base"
- ],
- "title": "Main_Diffusers_SDXLRefiner_Config"
+ "version": "1.2.0",
+ "output": {
+ "$ref": "#/components/schemas/MDControlListOutput"
+ }
},
- "Main_Diffusers_SDXL_Config": {
+ "MetadataToFloatCollectionInvocation": {
+ "category": "metadata",
+ "class": "invocation",
+ "classification": "beta",
+ "description": "Extracts a Float value Collection of a label from metadata",
+ "node_pack": "invokeai",
"properties": {
- "key": {
- "type": "string",
- "title": "Key",
- "description": "A unique key for this model."
- },
- "hash": {
- "type": "string",
- "title": "Hash",
- "description": "The hash of the model file(s)."
- },
- "path": {
- "type": "string",
- "title": "Path",
- "description": "Path to the model on the filesystem. Relative paths are relative to the Invoke root directory."
- },
- "file_size": {
- "type": "integer",
- "title": "File Size",
- "description": "The size of the model in bytes."
- },
- "name": {
- "type": "string",
- "title": "Name",
- "description": "Name of the model."
- },
- "description": {
+ "metadata": {
"anyOf": [
{
- "type": "string"
+ "$ref": "#/components/schemas/MetadataField"
},
{
"type": "null"
}
],
- "title": "Description",
- "description": "Model description"
+ "default": null,
+ "description": "Optional metadata to be saved with the image",
+ "field_kind": "internal",
+ "input": "connection",
+ "orig_required": false,
+ "ui_hidden": false
},
- "source": {
- "type": "string",
- "title": "Source",
- "description": "The original source of the model (path, URL or repo_id)."
+ "id": {
+ "description": "The id of this instance of an invocation. Must be unique among all instances of invocations.",
+ "field_kind": "node_attribute",
+ "title": "Id",
+ "type": "string"
+ },
+ "is_intermediate": {
+ "default": false,
+ "description": "Whether or not this is an intermediate invocation.",
+ "field_kind": "node_attribute",
+ "input": "direct",
+ "orig_required": true,
+ "title": "Is Intermediate",
+ "type": "boolean",
+ "ui_hidden": false,
+ "ui_type": "IsIntermediate"
},
- "source_type": {
- "$ref": "#/components/schemas/ModelSourceType",
- "description": "The type of source"
+ "use_cache": {
+ "default": true,
+ "description": "Whether or not to use the cache",
+ "field_kind": "node_attribute",
+ "title": "Use Cache",
+ "type": "boolean"
},
- "source_api_response": {
+ "label": {
+ "default": "* CUSTOM LABEL *",
+ "description": "Label for this metadata item",
+ "enum": ["* CUSTOM LABEL *", "cfg_scale", "cfg_rescale_multiplier", "guidance"],
+ "field_kind": "input",
+ "input": "direct",
+ "orig_default": "* CUSTOM LABEL *",
+ "orig_required": false,
+ "title": "Label",
+ "type": "string"
+ },
+ "custom_label": {
"anyOf": [
{
"type": "string"
@@ -51214,186 +57259,313 @@
"type": "null"
}
],
- "title": "Source Api Response",
- "description": "The original API response from the source, as stringified JSON."
+ "default": null,
+ "description": "Label for this metadata item",
+ "field_kind": "input",
+ "input": "direct",
+ "orig_default": null,
+ "orig_required": false,
+ "title": "Custom Label"
},
- "source_url": {
+ "default_value": {
"anyOf": [
{
- "type": "string"
+ "items": {
+ "type": "number"
+ },
+ "type": "array"
},
{
"type": "null"
}
],
- "title": "Source Url",
- "description": "Optional URL for the model (e.g. download page or model page)."
+ "default": null,
+ "description": "The default float to use if not found in the metadata",
+ "field_kind": "input",
+ "input": "any",
+ "orig_required": true,
+ "title": "Default Value"
},
- "cover_image": {
+ "type": {
+ "const": "metadata_to_float_collection",
+ "default": "metadata_to_float_collection",
+ "field_kind": "node_attribute",
+ "title": "type",
+ "type": "string"
+ }
+ },
+ "required": ["type", "id"],
+ "tags": ["metadata"],
+ "title": "Metadata To Float Collection",
+ "type": "object",
+ "version": "1.0.0",
+ "output": {
+ "$ref": "#/components/schemas/FloatCollectionOutput"
+ }
+ },
+ "MetadataToFloatInvocation": {
+ "category": "metadata",
+ "class": "invocation",
+ "classification": "beta",
+ "description": "Extracts a Float value of a label from metadata",
+ "node_pack": "invokeai",
+ "properties": {
+ "metadata": {
"anyOf": [
{
- "type": "string"
+ "$ref": "#/components/schemas/MetadataField"
},
{
"type": "null"
}
],
- "title": "Cover Image",
- "description": "Url for image to preview model"
+ "default": null,
+ "description": "Optional metadata to be saved with the image",
+ "field_kind": "internal",
+ "input": "connection",
+ "orig_required": false,
+ "ui_hidden": false
},
- "type": {
- "type": "string",
- "const": "main",
- "title": "Type",
- "default": "main"
+ "id": {
+ "description": "The id of this instance of an invocation. Must be unique among all instances of invocations.",
+ "field_kind": "node_attribute",
+ "title": "Id",
+ "type": "string"
},
- "trigger_phrases": {
+ "is_intermediate": {
+ "default": false,
+ "description": "Whether or not this is an intermediate invocation.",
+ "field_kind": "node_attribute",
+ "input": "direct",
+ "orig_required": true,
+ "title": "Is Intermediate",
+ "type": "boolean",
+ "ui_hidden": false,
+ "ui_type": "IsIntermediate"
+ },
+ "use_cache": {
+ "default": true,
+ "description": "Whether or not to use the cache",
+ "field_kind": "node_attribute",
+ "title": "Use Cache",
+ "type": "boolean"
+ },
+ "label": {
+ "default": "* CUSTOM LABEL *",
+ "description": "Label for this metadata item",
+ "enum": ["* CUSTOM LABEL *", "cfg_scale", "cfg_rescale_multiplier", "guidance"],
+ "field_kind": "input",
+ "input": "direct",
+ "orig_default": "* CUSTOM LABEL *",
+ "orig_required": false,
+ "title": "Label",
+ "type": "string"
+ },
+ "custom_label": {
"anyOf": [
{
- "items": {
- "type": "string"
- },
- "type": "array",
- "uniqueItems": true
+ "type": "string"
},
{
"type": "null"
}
],
- "title": "Trigger Phrases",
- "description": "Set of trigger phrases for this model"
+ "default": null,
+ "description": "Label for this metadata item",
+ "field_kind": "input",
+ "input": "direct",
+ "orig_default": null,
+ "orig_required": false,
+ "title": "Custom Label"
},
- "default_settings": {
+ "default_value": {
"anyOf": [
{
- "$ref": "#/components/schemas/MainModelDefaultSettings"
+ "type": "number"
},
{
"type": "null"
}
],
- "description": "Default settings for this model"
- },
- "format": {
- "type": "string",
- "const": "diffusers",
- "title": "Format",
- "default": "diffusers"
- },
- "repo_variant": {
- "$ref": "#/components/schemas/ModelRepoVariant",
- "default": ""
- },
- "prediction_type": {
- "$ref": "#/components/schemas/SchedulerPredictionType"
- },
- "variant": {
- "$ref": "#/components/schemas/ModelVariantType"
+ "default": null,
+ "description": "The default float to use if not found in the metadata",
+ "field_kind": "input",
+ "input": "any",
+ "orig_required": true,
+ "title": "Default Value"
},
- "base": {
- "type": "string",
- "const": "sdxl",
- "title": "Base",
- "default": "sdxl"
+ "type": {
+ "const": "metadata_to_float",
+ "default": "metadata_to_float",
+ "field_kind": "node_attribute",
+ "title": "type",
+ "type": "string"
}
},
+ "required": ["type", "id"],
+ "tags": ["metadata"],
+ "title": "Metadata To Float",
"type": "object",
- "required": [
- "key",
- "hash",
- "path",
- "file_size",
- "name",
- "description",
- "source",
- "source_type",
- "source_api_response",
- "source_url",
- "cover_image",
- "type",
- "trigger_phrases",
- "default_settings",
- "format",
- "repo_variant",
- "prediction_type",
- "variant",
- "base"
- ],
- "title": "Main_Diffusers_SDXL_Config"
+ "version": "1.1.0",
+ "output": {
+ "$ref": "#/components/schemas/FloatOutput"
+ }
},
- "Main_Diffusers_ZImage_Config": {
+ "MetadataToIPAdaptersInvocation": {
+ "category": "metadata",
+ "class": "invocation",
+ "classification": "beta",
+ "description": "Extracts a IP-Adapters value of a label from metadata",
+ "node_pack": "invokeai",
"properties": {
- "key": {
- "type": "string",
- "title": "Key",
- "description": "A unique key for this model."
- },
- "hash": {
- "type": "string",
- "title": "Hash",
- "description": "The hash of the model file(s)."
- },
- "path": {
- "type": "string",
- "title": "Path",
- "description": "Path to the model on the filesystem. Relative paths are relative to the Invoke root directory."
- },
- "file_size": {
- "type": "integer",
- "title": "File Size",
- "description": "The size of the model in bytes."
- },
- "name": {
- "type": "string",
- "title": "Name",
- "description": "Name of the model."
- },
- "description": {
+ "metadata": {
"anyOf": [
{
- "type": "string"
+ "$ref": "#/components/schemas/MetadataField"
},
{
"type": "null"
}
],
- "title": "Description",
- "description": "Model description"
+ "default": null,
+ "description": "Optional metadata to be saved with the image",
+ "field_kind": "internal",
+ "input": "connection",
+ "orig_required": false,
+ "ui_hidden": false
},
- "source": {
- "type": "string",
- "title": "Source",
- "description": "The original source of the model (path, URL or repo_id)."
+ "id": {
+ "description": "The id of this instance of an invocation. Must be unique among all instances of invocations.",
+ "field_kind": "node_attribute",
+ "title": "Id",
+ "type": "string"
},
- "source_type": {
- "$ref": "#/components/schemas/ModelSourceType",
- "description": "The type of source"
+ "is_intermediate": {
+ "default": false,
+ "description": "Whether or not this is an intermediate invocation.",
+ "field_kind": "node_attribute",
+ "input": "direct",
+ "orig_required": true,
+ "title": "Is Intermediate",
+ "type": "boolean",
+ "ui_hidden": false,
+ "ui_type": "IsIntermediate"
},
- "source_api_response": {
+ "use_cache": {
+ "default": true,
+ "description": "Whether or not to use the cache",
+ "field_kind": "node_attribute",
+ "title": "Use Cache",
+ "type": "boolean"
+ },
+ "ip_adapter_list": {
"anyOf": [
{
- "type": "string"
+ "$ref": "#/components/schemas/IPAdapterField"
+ },
+ {
+ "items": {
+ "$ref": "#/components/schemas/IPAdapterField"
+ },
+ "type": "array"
},
{
"type": "null"
}
],
- "title": "Source Api Response",
- "description": "The original API response from the source, as stringified JSON."
+ "default": null,
+ "description": "IP-Adapter to apply",
+ "field_kind": "input",
+ "input": "connection",
+ "orig_default": null,
+ "orig_required": false,
+ "title": "IP-Adapter-List"
},
- "source_url": {
+ "type": {
+ "const": "metadata_to_ip_adapters",
+ "default": "metadata_to_ip_adapters",
+ "field_kind": "node_attribute",
+ "title": "type",
+ "type": "string"
+ }
+ },
+ "required": ["type", "id"],
+ "tags": ["metadata"],
+ "title": "Metadata To IP-Adapters",
+ "type": "object",
+ "version": "1.2.0",
+ "output": {
+ "$ref": "#/components/schemas/MDIPAdapterListOutput"
+ }
+ },
+ "MetadataToIntegerCollectionInvocation": {
+ "category": "metadata",
+ "class": "invocation",
+ "classification": "beta",
+ "description": "Extracts an integer value Collection of a label from metadata",
+ "node_pack": "invokeai",
+ "properties": {
+ "metadata": {
"anyOf": [
{
- "type": "string"
+ "$ref": "#/components/schemas/MetadataField"
},
{
"type": "null"
}
],
- "title": "Source Url",
- "description": "Optional URL for the model (e.g. download page or model page)."
+ "default": null,
+ "description": "Optional metadata to be saved with the image",
+ "field_kind": "internal",
+ "input": "connection",
+ "orig_required": false,
+ "ui_hidden": false
},
- "cover_image": {
+ "id": {
+ "description": "The id of this instance of an invocation. Must be unique among all instances of invocations.",
+ "field_kind": "node_attribute",
+ "title": "Id",
+ "type": "string"
+ },
+ "is_intermediate": {
+ "default": false,
+ "description": "Whether or not this is an intermediate invocation.",
+ "field_kind": "node_attribute",
+ "input": "direct",
+ "orig_required": true,
+ "title": "Is Intermediate",
+ "type": "boolean",
+ "ui_hidden": false,
+ "ui_type": "IsIntermediate"
+ },
+ "use_cache": {
+ "default": true,
+ "description": "Whether or not to use the cache",
+ "field_kind": "node_attribute",
+ "title": "Use Cache",
+ "type": "boolean"
+ },
+ "label": {
+ "default": "* CUSTOM LABEL *",
+ "description": "Label for this metadata item",
+ "enum": [
+ "* CUSTOM LABEL *",
+ "width",
+ "height",
+ "seed",
+ "steps",
+ "clip_skip",
+ "cfg_scale_start_step",
+ "cfg_scale_end_step"
+ ],
+ "field_kind": "input",
+ "input": "direct",
+ "orig_default": "* CUSTOM LABEL *",
+ "orig_required": false,
+ "title": "Label",
+ "type": "string"
+ },
+ "custom_label": {
"anyOf": [
{
"type": "string"
@@ -51402,114 +57574,118 @@
"type": "null"
}
],
- "title": "Cover Image",
- "description": "Url for image to preview model"
- },
- "type": {
- "type": "string",
- "const": "main",
- "title": "Type",
- "default": "main"
+ "default": null,
+ "description": "Label for this metadata item",
+ "field_kind": "input",
+ "input": "direct",
+ "orig_default": null,
+ "orig_required": false,
+ "title": "Custom Label"
},
- "trigger_phrases": {
+ "default_value": {
"anyOf": [
{
"items": {
- "type": "string"
+ "type": "integer"
},
- "type": "array",
- "uniqueItems": true
+ "type": "array"
},
{
"type": "null"
}
],
- "title": "Trigger Phrases",
- "description": "Set of trigger phrases for this model"
+ "default": null,
+ "description": "The default integer to use if not found in the metadata",
+ "field_kind": "input",
+ "input": "any",
+ "orig_required": true,
+ "title": "Default Value"
},
- "default_settings": {
+ "type": {
+ "const": "metadata_to_integer_collection",
+ "default": "metadata_to_integer_collection",
+ "field_kind": "node_attribute",
+ "title": "type",
+ "type": "string"
+ }
+ },
+ "required": ["type", "id"],
+ "tags": ["metadata"],
+ "title": "Metadata To Integer Collection",
+ "type": "object",
+ "version": "1.0.0",
+ "output": {
+ "$ref": "#/components/schemas/IntegerCollectionOutput"
+ }
+ },
+ "MetadataToIntegerInvocation": {
+ "category": "metadata",
+ "class": "invocation",
+ "classification": "beta",
+ "description": "Extracts an integer value of a label from metadata",
+ "node_pack": "invokeai",
+ "properties": {
+ "metadata": {
"anyOf": [
{
- "$ref": "#/components/schemas/MainModelDefaultSettings"
+ "$ref": "#/components/schemas/MetadataField"
},
{
"type": "null"
}
],
- "description": "Default settings for this model"
- },
- "format": {
- "type": "string",
- "const": "diffusers",
- "title": "Format",
- "default": "diffusers"
- },
- "repo_variant": {
- "$ref": "#/components/schemas/ModelRepoVariant",
- "default": ""
- },
- "base": {
- "type": "string",
- "const": "z-image",
- "title": "Base",
- "default": "z-image"
- },
- "variant": {
- "$ref": "#/components/schemas/ZImageVariantType"
- }
- },
- "type": "object",
- "required": [
- "key",
- "hash",
- "path",
- "file_size",
- "name",
- "description",
- "source",
- "source_type",
- "source_api_response",
- "source_url",
- "cover_image",
- "type",
- "trigger_phrases",
- "default_settings",
- "format",
- "repo_variant",
- "base",
- "variant"
- ],
- "title": "Main_Diffusers_ZImage_Config",
- "description": "Model config for Z-Image diffusers models (Z-Image-Turbo, Z-Image-Base)."
- },
- "Main_GGUF_FLUX_Config": {
- "properties": {
- "key": {
- "type": "string",
- "title": "Key",
- "description": "A unique key for this model."
+ "default": null,
+ "description": "Optional metadata to be saved with the image",
+ "field_kind": "internal",
+ "input": "connection",
+ "orig_required": false,
+ "ui_hidden": false
},
- "hash": {
- "type": "string",
- "title": "Hash",
- "description": "The hash of the model file(s)."
+ "id": {
+ "description": "The id of this instance of an invocation. Must be unique among all instances of invocations.",
+ "field_kind": "node_attribute",
+ "title": "Id",
+ "type": "string"
},
- "path": {
- "type": "string",
- "title": "Path",
- "description": "Path to the model on the filesystem. Relative paths are relative to the Invoke root directory."
+ "is_intermediate": {
+ "default": false,
+ "description": "Whether or not this is an intermediate invocation.",
+ "field_kind": "node_attribute",
+ "input": "direct",
+ "orig_required": true,
+ "title": "Is Intermediate",
+ "type": "boolean",
+ "ui_hidden": false,
+ "ui_type": "IsIntermediate"
},
- "file_size": {
- "type": "integer",
- "title": "File Size",
- "description": "The size of the model in bytes."
+ "use_cache": {
+ "default": true,
+ "description": "Whether or not to use the cache",
+ "field_kind": "node_attribute",
+ "title": "Use Cache",
+ "type": "boolean"
},
- "name": {
- "type": "string",
- "title": "Name",
- "description": "Name of the model."
+ "label": {
+ "default": "* CUSTOM LABEL *",
+ "description": "Label for this metadata item",
+ "enum": [
+ "* CUSTOM LABEL *",
+ "width",
+ "height",
+ "seed",
+ "steps",
+ "clip_skip",
+ "cfg_scale_start_step",
+ "cfg_scale_end_step"
+ ],
+ "field_kind": "input",
+ "input": "direct",
+ "orig_default": "* CUSTOM LABEL *",
+ "orig_required": false,
+ "title": "Label",
+ "type": "string"
},
- "description": {
+ "custom_label": {
"anyOf": [
{
"type": "string"
@@ -51518,188 +57694,327 @@
"type": "null"
}
],
- "title": "Description",
- "description": "Model description"
- },
- "source": {
- "type": "string",
- "title": "Source",
- "description": "The original source of the model (path, URL or repo_id)."
- },
- "source_type": {
- "$ref": "#/components/schemas/ModelSourceType",
- "description": "The type of source"
+ "default": null,
+ "description": "Label for this metadata item",
+ "field_kind": "input",
+ "input": "direct",
+ "orig_default": null,
+ "orig_required": false,
+ "title": "Custom Label"
},
- "source_api_response": {
+ "default_value": {
"anyOf": [
{
- "type": "string"
+ "type": "integer"
},
{
"type": "null"
}
],
- "title": "Source Api Response",
- "description": "The original API response from the source, as stringified JSON."
+ "default": null,
+ "description": "The default integer to use if not found in the metadata",
+ "field_kind": "input",
+ "input": "any",
+ "orig_required": true,
+ "title": "Default Value"
},
- "source_url": {
+ "type": {
+ "const": "metadata_to_integer",
+ "default": "metadata_to_integer",
+ "field_kind": "node_attribute",
+ "title": "type",
+ "type": "string"
+ }
+ },
+ "required": ["type", "id"],
+ "tags": ["metadata"],
+ "title": "Metadata To Integer",
+ "type": "object",
+ "version": "1.0.0",
+ "output": {
+ "$ref": "#/components/schemas/IntegerOutput"
+ }
+ },
+ "MetadataToLorasCollectionInvocation": {
+ "category": "metadata",
+ "class": "invocation",
+ "classification": "beta",
+ "description": "Extracts Lora(s) from metadata into a collection",
+ "node_pack": "invokeai",
+ "properties": {
+ "metadata": {
"anyOf": [
{
- "type": "string"
+ "$ref": "#/components/schemas/MetadataField"
},
{
"type": "null"
}
],
- "title": "Source Url",
- "description": "Optional URL for the model (e.g. download page or model page)."
+ "default": null,
+ "description": "Optional metadata to be saved with the image",
+ "field_kind": "internal",
+ "input": "connection",
+ "orig_required": false,
+ "ui_hidden": false
},
- "cover_image": {
+ "id": {
+ "description": "The id of this instance of an invocation. Must be unique among all instances of invocations.",
+ "field_kind": "node_attribute",
+ "title": "Id",
+ "type": "string"
+ },
+ "is_intermediate": {
+ "default": false,
+ "description": "Whether or not this is an intermediate invocation.",
+ "field_kind": "node_attribute",
+ "input": "direct",
+ "orig_required": true,
+ "title": "Is Intermediate",
+ "type": "boolean",
+ "ui_hidden": false,
+ "ui_type": "IsIntermediate"
+ },
+ "use_cache": {
+ "default": true,
+ "description": "Whether or not to use the cache",
+ "field_kind": "node_attribute",
+ "title": "Use Cache",
+ "type": "boolean"
+ },
+ "custom_label": {
+ "default": "loras",
+ "description": "Label for this metadata item",
+ "field_kind": "input",
+ "input": "direct",
+ "orig_default": "loras",
+ "orig_required": false,
+ "title": "Custom Label",
+ "type": "string"
+ },
+ "loras": {
"anyOf": [
{
- "type": "string"
+ "$ref": "#/components/schemas/LoRAField"
+ },
+ {
+ "items": {
+ "$ref": "#/components/schemas/LoRAField"
+ },
+ "type": "array"
},
{
"type": "null"
}
],
- "title": "Cover Image",
- "description": "Url for image to preview model"
+ "default": [],
+ "description": "LoRA models and weights. May be a single LoRA or collection.",
+ "field_kind": "input",
+ "input": "any",
+ "orig_default": [],
+ "orig_required": false,
+ "title": "LoRAs"
},
"type": {
- "type": "string",
- "const": "main",
- "title": "Type",
- "default": "main"
+ "const": "metadata_to_lora_collection",
+ "default": "metadata_to_lora_collection",
+ "field_kind": "node_attribute",
+ "title": "type",
+ "type": "string"
+ }
+ },
+ "required": ["type", "id"],
+ "tags": ["metadata"],
+ "title": "Metadata To LoRA Collection",
+ "type": "object",
+ "version": "1.1.0",
+ "output": {
+ "$ref": "#/components/schemas/MetadataToLorasCollectionOutput"
+ }
+ },
+ "MetadataToLorasCollectionOutput": {
+ "class": "output",
+ "description": "Model loader output",
+ "properties": {
+ "lora": {
+ "description": "Collection of LoRA model and weights",
+ "field_kind": "output",
+ "items": {
+ "$ref": "#/components/schemas/LoRAField"
+ },
+ "title": "LoRAs",
+ "type": "array",
+ "ui_hidden": false
},
- "trigger_phrases": {
+ "type": {
+ "const": "metadata_to_lora_collection_output",
+ "default": "metadata_to_lora_collection_output",
+ "field_kind": "node_attribute",
+ "title": "type",
+ "type": "string"
+ }
+ },
+ "required": ["output_meta", "lora", "type", "type"],
+ "title": "MetadataToLorasCollectionOutput",
+ "type": "object"
+ },
+ "MetadataToLorasInvocation": {
+ "category": "metadata",
+ "class": "invocation",
+ "classification": "beta",
+ "description": "Extracts a Loras value of a label from metadata",
+ "node_pack": "invokeai",
+ "properties": {
+ "metadata": {
"anyOf": [
{
- "items": {
- "type": "string"
- },
- "type": "array",
- "uniqueItems": true
+ "$ref": "#/components/schemas/MetadataField"
},
{
"type": "null"
}
],
- "title": "Trigger Phrases",
- "description": "Set of trigger phrases for this model"
+ "default": null,
+ "description": "Optional metadata to be saved with the image",
+ "field_kind": "internal",
+ "input": "connection",
+ "orig_required": false,
+ "ui_hidden": false
},
- "default_settings": {
+ "id": {
+ "description": "The id of this instance of an invocation. Must be unique among all instances of invocations.",
+ "field_kind": "node_attribute",
+ "title": "Id",
+ "type": "string"
+ },
+ "is_intermediate": {
+ "default": false,
+ "description": "Whether or not this is an intermediate invocation.",
+ "field_kind": "node_attribute",
+ "input": "direct",
+ "orig_required": true,
+ "title": "Is Intermediate",
+ "type": "boolean",
+ "ui_hidden": false,
+ "ui_type": "IsIntermediate"
+ },
+ "use_cache": {
+ "default": true,
+ "description": "Whether or not to use the cache",
+ "field_kind": "node_attribute",
+ "title": "Use Cache",
+ "type": "boolean"
+ },
+ "unet": {
"anyOf": [
{
- "$ref": "#/components/schemas/MainModelDefaultSettings"
+ "$ref": "#/components/schemas/UNetField"
},
{
"type": "null"
}
],
- "description": "Default settings for this model"
+ "default": null,
+ "description": "UNet (scheduler, LoRAs)",
+ "field_kind": "input",
+ "input": "connection",
+ "orig_default": null,
+ "orig_required": false,
+ "title": "UNet"
},
- "config_path": {
+ "clip": {
"anyOf": [
{
- "type": "string"
+ "$ref": "#/components/schemas/CLIPField"
},
{
"type": "null"
}
],
- "title": "Config Path",
- "description": "Path to the config for this model, if any."
- },
- "base": {
- "type": "string",
- "const": "flux",
- "title": "Base",
- "default": "flux"
- },
- "format": {
- "type": "string",
- "const": "gguf_quantized",
- "title": "Format",
- "default": "gguf_quantized"
+ "default": null,
+ "description": "CLIP (tokenizer, text encoder, LoRAs) and skipped layer count",
+ "field_kind": "input",
+ "input": "connection",
+ "orig_default": null,
+ "orig_required": false,
+ "title": "CLIP"
},
- "variant": {
- "$ref": "#/components/schemas/FluxVariantType"
+ "type": {
+ "const": "metadata_to_loras",
+ "default": "metadata_to_loras",
+ "field_kind": "node_attribute",
+ "title": "type",
+ "type": "string"
}
},
+ "required": ["type", "id"],
+ "tags": ["metadata"],
+ "title": "Metadata To LoRAs",
"type": "object",
- "required": [
- "key",
- "hash",
- "path",
- "file_size",
- "name",
- "description",
- "source",
- "source_type",
- "source_api_response",
- "source_url",
- "cover_image",
- "type",
- "trigger_phrases",
- "default_settings",
- "config_path",
- "base",
- "format",
- "variant"
- ],
- "title": "Main_GGUF_FLUX_Config",
- "description": "Model config for main checkpoint models."
+ "version": "1.1.1",
+ "output": {
+ "$ref": "#/components/schemas/LoRALoaderOutput"
+ }
},
- "Main_GGUF_Flux2_Config": {
+ "MetadataToModelInvocation": {
+ "category": "metadata",
+ "class": "invocation",
+ "classification": "beta",
+ "description": "Extracts a Model value of a label from metadata",
+ "node_pack": "invokeai",
"properties": {
- "key": {
- "type": "string",
- "title": "Key",
- "description": "A unique key for this model."
- },
- "hash": {
- "type": "string",
- "title": "Hash",
- "description": "The hash of the model file(s)."
- },
- "path": {
- "type": "string",
- "title": "Path",
- "description": "Path to the model on the filesystem. Relative paths are relative to the Invoke root directory."
- },
- "file_size": {
- "type": "integer",
- "title": "File Size",
- "description": "The size of the model in bytes."
- },
- "name": {
- "type": "string",
- "title": "Name",
- "description": "Name of the model."
- },
- "description": {
+ "metadata": {
"anyOf": [
{
- "type": "string"
+ "$ref": "#/components/schemas/MetadataField"
},
{
"type": "null"
}
],
- "title": "Description",
- "description": "Model description"
+ "default": null,
+ "description": "Optional metadata to be saved with the image",
+ "field_kind": "internal",
+ "input": "connection",
+ "orig_required": false,
+ "ui_hidden": false
},
- "source": {
- "type": "string",
- "title": "Source",
- "description": "The original source of the model (path, URL or repo_id)."
+ "id": {
+ "description": "The id of this instance of an invocation. Must be unique among all instances of invocations.",
+ "field_kind": "node_attribute",
+ "title": "Id",
+ "type": "string"
},
- "source_type": {
- "$ref": "#/components/schemas/ModelSourceType",
- "description": "The type of source"
+ "is_intermediate": {
+ "default": false,
+ "description": "Whether or not this is an intermediate invocation.",
+ "field_kind": "node_attribute",
+ "input": "direct",
+ "orig_required": true,
+ "title": "Is Intermediate",
+ "type": "boolean",
+ "ui_hidden": false,
+ "ui_type": "IsIntermediate"
},
- "source_api_response": {
+ "use_cache": {
+ "default": true,
+ "description": "Whether or not to use the cache",
+ "field_kind": "node_attribute",
+ "title": "Use Cache",
+ "type": "boolean"
+ },
+ "label": {
+ "default": "model",
+ "description": "Label for this metadata item",
+ "enum": ["* CUSTOM LABEL *", "model"],
+ "field_kind": "input",
+ "input": "direct",
+ "orig_default": "model",
+ "orig_required": false,
+ "title": "Label",
+ "type": "string"
+ },
+ "custom_label": {
"anyOf": [
{
"type": "string"
@@ -51708,179 +58023,272 @@
"type": "null"
}
],
- "title": "Source Api Response",
- "description": "The original API response from the source, as stringified JSON."
+ "default": null,
+ "description": "Label for this metadata item",
+ "field_kind": "input",
+ "input": "direct",
+ "orig_default": null,
+ "orig_required": false,
+ "title": "Custom Label"
},
- "source_url": {
+ "default_value": {
"anyOf": [
{
- "type": "string"
+ "$ref": "#/components/schemas/ModelIdentifierField"
},
{
"type": "null"
}
],
- "title": "Source Url",
- "description": "Optional URL for the model (e.g. download page or model page)."
+ "default": null,
+ "description": "The default model to use if not found in the metadata",
+ "field_kind": "input",
+ "input": "any",
+ "orig_required": true,
+ "ui_model_type": ["main"]
},
- "cover_image": {
+ "type": {
+ "const": "metadata_to_model",
+ "default": "metadata_to_model",
+ "field_kind": "node_attribute",
+ "title": "type",
+ "type": "string"
+ }
+ },
+ "required": ["type", "id"],
+ "tags": ["metadata"],
+ "title": "Metadata To Model",
+ "type": "object",
+ "version": "1.3.0",
+ "output": {
+ "$ref": "#/components/schemas/MetadataToModelOutput"
+ }
+ },
+ "MetadataToModelOutput": {
+ "class": "output",
+ "description": "String to main model output",
+ "properties": {
+ "model": {
+ "$ref": "#/components/schemas/ModelIdentifierField",
+ "description": "Main model (UNet, VAE, CLIP) to load",
+ "field_kind": "output",
+ "title": "Model",
+ "ui_hidden": false
+ },
+ "name": {
+ "description": "Model Name",
+ "field_kind": "output",
+ "title": "Name",
+ "type": "string",
+ "ui_hidden": false
+ },
+ "unet": {
+ "$ref": "#/components/schemas/UNetField",
+ "description": "UNet (scheduler, LoRAs)",
+ "field_kind": "output",
+ "title": "UNet",
+ "ui_hidden": false
+ },
+ "vae": {
+ "$ref": "#/components/schemas/VAEField",
+ "description": "VAE",
+ "field_kind": "output",
+ "title": "VAE",
+ "ui_hidden": false
+ },
+ "clip": {
+ "$ref": "#/components/schemas/CLIPField",
+ "description": "CLIP (tokenizer, text encoder, LoRAs) and skipped layer count",
+ "field_kind": "output",
+ "title": "CLIP",
+ "ui_hidden": false
+ },
+ "type": {
+ "const": "metadata_to_model_output",
+ "default": "metadata_to_model_output",
+ "field_kind": "node_attribute",
+ "title": "type",
+ "type": "string"
+ }
+ },
+ "required": ["output_meta", "model", "name", "unet", "vae", "clip", "type", "type"],
+ "title": "MetadataToModelOutput",
+ "type": "object"
+ },
+ "MetadataToSDXLLorasInvocation": {
+ "category": "metadata",
+ "class": "invocation",
+ "classification": "beta",
+ "description": "Extracts a SDXL Loras value of a label from metadata",
+ "node_pack": "invokeai",
+ "properties": {
+ "metadata": {
"anyOf": [
{
- "type": "string"
+ "$ref": "#/components/schemas/MetadataField"
},
{
"type": "null"
}
],
- "title": "Cover Image",
- "description": "Url for image to preview model"
+ "default": null,
+ "description": "Optional metadata to be saved with the image",
+ "field_kind": "internal",
+ "input": "connection",
+ "orig_required": false,
+ "ui_hidden": false
},
- "type": {
- "type": "string",
- "const": "main",
- "title": "Type",
- "default": "main"
+ "id": {
+ "description": "The id of this instance of an invocation. Must be unique among all instances of invocations.",
+ "field_kind": "node_attribute",
+ "title": "Id",
+ "type": "string"
},
- "trigger_phrases": {
+ "is_intermediate": {
+ "default": false,
+ "description": "Whether or not this is an intermediate invocation.",
+ "field_kind": "node_attribute",
+ "input": "direct",
+ "orig_required": true,
+ "title": "Is Intermediate",
+ "type": "boolean",
+ "ui_hidden": false,
+ "ui_type": "IsIntermediate"
+ },
+ "use_cache": {
+ "default": true,
+ "description": "Whether or not to use the cache",
+ "field_kind": "node_attribute",
+ "title": "Use Cache",
+ "type": "boolean"
+ },
+ "unet": {
"anyOf": [
{
- "items": {
- "type": "string"
- },
- "type": "array",
- "uniqueItems": true
+ "$ref": "#/components/schemas/UNetField"
},
{
"type": "null"
}
],
- "title": "Trigger Phrases",
- "description": "Set of trigger phrases for this model"
+ "default": null,
+ "description": "UNet (scheduler, LoRAs)",
+ "field_kind": "input",
+ "input": "connection",
+ "orig_default": null,
+ "orig_required": false,
+ "title": "UNet"
},
- "default_settings": {
+ "clip": {
"anyOf": [
{
- "$ref": "#/components/schemas/MainModelDefaultSettings"
+ "$ref": "#/components/schemas/CLIPField"
},
{
"type": "null"
}
],
- "description": "Default settings for this model"
+ "default": null,
+ "description": "CLIP (tokenizer, text encoder, LoRAs) and skipped layer count",
+ "field_kind": "input",
+ "input": "connection",
+ "orig_default": null,
+ "orig_required": false,
+ "title": "CLIP 1"
},
- "config_path": {
+ "clip2": {
"anyOf": [
{
- "type": "string"
+ "$ref": "#/components/schemas/CLIPField"
},
{
"type": "null"
}
],
- "title": "Config Path",
- "description": "Path to the config for this model, if any."
- },
- "base": {
- "type": "string",
- "const": "flux2",
- "title": "Base",
- "default": "flux2"
- },
- "format": {
- "type": "string",
- "const": "gguf_quantized",
- "title": "Format",
- "default": "gguf_quantized"
+ "default": null,
+ "description": "CLIP (tokenizer, text encoder, LoRAs) and skipped layer count",
+ "field_kind": "input",
+ "input": "connection",
+ "orig_default": null,
+ "orig_required": false,
+ "title": "CLIP 2"
},
- "variant": {
- "$ref": "#/components/schemas/Flux2VariantType"
+ "type": {
+ "const": "metadata_to_sdlx_loras",
+ "default": "metadata_to_sdlx_loras",
+ "field_kind": "node_attribute",
+ "title": "type",
+ "type": "string"
}
},
+ "required": ["type", "id"],
+ "tags": ["metadata"],
+ "title": "Metadata To SDXL LoRAs",
"type": "object",
- "required": [
- "key",
- "hash",
- "path",
- "file_size",
- "name",
- "description",
- "source",
- "source_type",
- "source_api_response",
- "source_url",
- "cover_image",
- "type",
- "trigger_phrases",
- "default_settings",
- "config_path",
- "base",
- "format",
- "variant"
- ],
- "title": "Main_GGUF_Flux2_Config",
- "description": "Model config for GGUF-quantized FLUX.2 checkpoint models (e.g. Klein)."
+ "version": "1.1.1",
+ "output": {
+ "$ref": "#/components/schemas/SDXLLoRALoaderOutput"
+ }
},
- "Main_GGUF_QwenImage_Config": {
+ "MetadataToSDXLModelInvocation": {
+ "category": "metadata",
+ "class": "invocation",
+ "classification": "beta",
+ "description": "Extracts a SDXL Model value of a label from metadata",
+ "node_pack": "invokeai",
"properties": {
- "key": {
- "type": "string",
- "title": "Key",
- "description": "A unique key for this model."
- },
- "hash": {
- "type": "string",
- "title": "Hash",
- "description": "The hash of the model file(s)."
- },
- "path": {
- "type": "string",
- "title": "Path",
- "description": "Path to the model on the filesystem. Relative paths are relative to the Invoke root directory."
- },
- "file_size": {
- "type": "integer",
- "title": "File Size",
- "description": "The size of the model in bytes."
- },
- "name": {
- "type": "string",
- "title": "Name",
- "description": "Name of the model."
- },
- "description": {
+ "metadata": {
"anyOf": [
{
- "type": "string"
+ "$ref": "#/components/schemas/MetadataField"
},
{
"type": "null"
}
],
- "title": "Description",
- "description": "Model description"
+ "default": null,
+ "description": "Optional metadata to be saved with the image",
+ "field_kind": "internal",
+ "input": "connection",
+ "orig_required": false,
+ "ui_hidden": false
},
- "source": {
- "type": "string",
- "title": "Source",
- "description": "The original source of the model (path, URL or repo_id)."
+ "id": {
+ "description": "The id of this instance of an invocation. Must be unique among all instances of invocations.",
+ "field_kind": "node_attribute",
+ "title": "Id",
+ "type": "string"
},
- "source_type": {
- "$ref": "#/components/schemas/ModelSourceType",
- "description": "The type of source"
+ "is_intermediate": {
+ "default": false,
+ "description": "Whether or not this is an intermediate invocation.",
+ "field_kind": "node_attribute",
+ "input": "direct",
+ "orig_required": true,
+ "title": "Is Intermediate",
+ "type": "boolean",
+ "ui_hidden": false,
+ "ui_type": "IsIntermediate"
},
- "source_api_response": {
- "anyOf": [
- {
- "type": "string"
- },
- {
- "type": "null"
- }
- ],
- "title": "Source Api Response",
- "description": "The original API response from the source, as stringified JSON."
+ "use_cache": {
+ "default": true,
+ "description": "Whether or not to use the cache",
+ "field_kind": "node_attribute",
+ "title": "Use Cache",
+ "type": "boolean"
},
- "source_url": {
+ "label": {
+ "default": "model",
+ "description": "Label for this metadata item",
+ "enum": ["* CUSTOM LABEL *", "model"],
+ "field_kind": "input",
+ "input": "direct",
+ "orig_default": "model",
+ "orig_required": false,
+ "title": "Label",
+ "type": "string"
+ },
+ "custom_label": {
"anyOf": [
{
"type": "string"
@@ -51889,186 +58297,307 @@
"type": "null"
}
],
- "title": "Source Url",
- "description": "Optional URL for the model (e.g. download page or model page)."
+ "default": null,
+ "description": "Label for this metadata item",
+ "field_kind": "input",
+ "input": "direct",
+ "orig_default": null,
+ "orig_required": false,
+ "title": "Custom Label"
},
- "cover_image": {
+ "default_value": {
"anyOf": [
{
- "type": "string"
+ "$ref": "#/components/schemas/ModelIdentifierField"
},
{
"type": "null"
}
],
- "title": "Cover Image",
- "description": "Url for image to preview model"
+ "default": null,
+ "description": "The default SDXL Model to use if not found in the metadata",
+ "field_kind": "input",
+ "input": "any",
+ "orig_required": true,
+ "ui_model_base": ["sdxl"],
+ "ui_model_type": ["main"]
},
"type": {
+ "const": "metadata_to_sdxl_model",
+ "default": "metadata_to_sdxl_model",
+ "field_kind": "node_attribute",
+ "title": "type",
+ "type": "string"
+ }
+ },
+ "required": ["type", "id"],
+ "tags": ["metadata"],
+ "title": "Metadata To SDXL Model",
+ "type": "object",
+ "version": "1.3.0",
+ "output": {
+ "$ref": "#/components/schemas/MetadataToSDXLModelOutput"
+ }
+ },
+ "MetadataToSDXLModelOutput": {
+ "class": "output",
+ "description": "String to SDXL main model output",
+ "properties": {
+ "model": {
+ "$ref": "#/components/schemas/ModelIdentifierField",
+ "description": "Main model (UNet, VAE, CLIP) to load",
+ "field_kind": "output",
+ "title": "Model",
+ "ui_hidden": false
+ },
+ "name": {
+ "description": "Model Name",
+ "field_kind": "output",
+ "title": "Name",
"type": "string",
- "const": "main",
- "title": "Type",
- "default": "main"
+ "ui_hidden": false
},
- "trigger_phrases": {
- "anyOf": [
- {
- "items": {
- "type": "string"
- },
- "type": "array",
- "uniqueItems": true
- },
- {
- "type": "null"
- }
- ],
- "title": "Trigger Phrases",
- "description": "Set of trigger phrases for this model"
+ "unet": {
+ "$ref": "#/components/schemas/UNetField",
+ "description": "UNet (scheduler, LoRAs)",
+ "field_kind": "output",
+ "title": "UNet",
+ "ui_hidden": false
},
- "default_settings": {
- "anyOf": [
- {
- "$ref": "#/components/schemas/MainModelDefaultSettings"
- },
- {
- "type": "null"
- }
- ],
- "description": "Default settings for this model"
+ "clip": {
+ "$ref": "#/components/schemas/CLIPField",
+ "description": "CLIP (tokenizer, text encoder, LoRAs) and skipped layer count",
+ "field_kind": "output",
+ "title": "CLIP 1",
+ "ui_hidden": false
},
- "config_path": {
+ "clip2": {
+ "$ref": "#/components/schemas/CLIPField",
+ "description": "CLIP (tokenizer, text encoder, LoRAs) and skipped layer count",
+ "field_kind": "output",
+ "title": "CLIP 2",
+ "ui_hidden": false
+ },
+ "vae": {
+ "$ref": "#/components/schemas/VAEField",
+ "description": "VAE",
+ "field_kind": "output",
+ "title": "VAE",
+ "ui_hidden": false
+ },
+ "type": {
+ "const": "metadata_to_sdxl_model_output",
+ "default": "metadata_to_sdxl_model_output",
+ "field_kind": "node_attribute",
+ "title": "type",
+ "type": "string"
+ }
+ },
+ "required": ["output_meta", "model", "name", "unet", "clip", "clip2", "vae", "type", "type"],
+ "title": "MetadataToSDXLModelOutput",
+ "type": "object"
+ },
+ "MetadataToSchedulerInvocation": {
+ "category": "metadata",
+ "class": "invocation",
+ "classification": "beta",
+ "description": "Extracts a Scheduler value of a label from metadata",
+ "node_pack": "invokeai",
+ "properties": {
+ "metadata": {
"anyOf": [
{
- "type": "string"
+ "$ref": "#/components/schemas/MetadataField"
},
{
"type": "null"
}
],
- "title": "Config Path",
- "description": "Path to the config for this model, if any."
+ "default": null,
+ "description": "Optional metadata to be saved with the image",
+ "field_kind": "internal",
+ "input": "connection",
+ "orig_required": false,
+ "ui_hidden": false
},
- "base": {
- "type": "string",
- "const": "qwen-image",
- "title": "Base",
- "default": "qwen-image"
+ "id": {
+ "description": "The id of this instance of an invocation. Must be unique among all instances of invocations.",
+ "field_kind": "node_attribute",
+ "title": "Id",
+ "type": "string"
},
- "format": {
- "type": "string",
- "const": "gguf_quantized",
- "title": "Format",
- "default": "gguf_quantized"
+ "is_intermediate": {
+ "default": false,
+ "description": "Whether or not this is an intermediate invocation.",
+ "field_kind": "node_attribute",
+ "input": "direct",
+ "orig_required": true,
+ "title": "Is Intermediate",
+ "type": "boolean",
+ "ui_hidden": false,
+ "ui_type": "IsIntermediate"
},
- "variant": {
+ "use_cache": {
+ "default": true,
+ "description": "Whether or not to use the cache",
+ "field_kind": "node_attribute",
+ "title": "Use Cache",
+ "type": "boolean"
+ },
+ "label": {
+ "default": "scheduler",
+ "description": "Label for this metadata item",
+ "enum": ["* CUSTOM LABEL *", "scheduler"],
+ "field_kind": "input",
+ "input": "direct",
+ "orig_default": "scheduler",
+ "orig_required": false,
+ "title": "Label",
+ "type": "string"
+ },
+ "custom_label": {
"anyOf": [
{
- "$ref": "#/components/schemas/QwenImageVariantType"
+ "type": "string"
},
{
"type": "null"
}
- ]
- }
- },
- "type": "object",
- "required": [
- "key",
- "hash",
- "path",
- "file_size",
- "name",
- "description",
- "source",
- "source_type",
- "source_api_response",
- "source_url",
- "cover_image",
- "type",
- "trigger_phrases",
- "default_settings",
- "config_path",
- "base",
- "format",
- "variant"
- ],
- "title": "Main_GGUF_QwenImage_Config",
- "description": "Model config for GGUF-quantized Qwen Image transformer models."
- },
- "Main_GGUF_ZImage_Config": {
- "properties": {
- "key": {
- "type": "string",
- "title": "Key",
- "description": "A unique key for this model."
- },
- "hash": {
- "type": "string",
- "title": "Hash",
- "description": "The hash of the model file(s)."
- },
- "path": {
- "type": "string",
- "title": "Path",
- "description": "Path to the model on the filesystem. Relative paths are relative to the Invoke root directory."
- },
- "file_size": {
- "type": "integer",
- "title": "File Size",
- "description": "The size of the model in bytes."
+ ],
+ "default": null,
+ "description": "Label for this metadata item",
+ "field_kind": "input",
+ "input": "direct",
+ "orig_default": null,
+ "orig_required": false,
+ "title": "Custom Label"
},
- "name": {
+ "default_value": {
+ "default": "euler",
+ "description": "The default scheduler to use if not found in the metadata",
+ "enum": [
+ "ddim",
+ "ddpm",
+ "deis",
+ "deis_k",
+ "lms",
+ "lms_k",
+ "pndm",
+ "heun",
+ "heun_k",
+ "euler",
+ "euler_k",
+ "euler_a",
+ "kdpm_2",
+ "kdpm_2_k",
+ "kdpm_2_a",
+ "kdpm_2_a_k",
+ "dpmpp_2s",
+ "dpmpp_2s_k",
+ "dpmpp_2m",
+ "dpmpp_2m_k",
+ "dpmpp_2m_sde",
+ "dpmpp_2m_sde_k",
+ "dpmpp_3m",
+ "dpmpp_3m_k",
+ "dpmpp_sde",
+ "dpmpp_sde_k",
+ "er_sde",
+ "unipc",
+ "unipc_k",
+ "lcm",
+ "tcd"
+ ],
+ "field_kind": "input",
+ "input": "any",
+ "orig_default": "euler",
+ "orig_required": false,
+ "title": "Default Value",
"type": "string",
- "title": "Name",
- "description": "Name of the model."
+ "ui_type": "SchedulerField"
},
- "description": {
+ "type": {
+ "const": "metadata_to_scheduler",
+ "default": "metadata_to_scheduler",
+ "field_kind": "node_attribute",
+ "title": "type",
+ "type": "string"
+ }
+ },
+ "required": ["type", "id"],
+ "tags": ["metadata"],
+ "title": "Metadata To Scheduler",
+ "type": "object",
+ "version": "1.0.1",
+ "output": {
+ "$ref": "#/components/schemas/SchedulerOutput"
+ }
+ },
+ "MetadataToStringCollectionInvocation": {
+ "category": "metadata",
+ "class": "invocation",
+ "classification": "beta",
+ "description": "Extracts a string collection value of a label from metadata",
+ "node_pack": "invokeai",
+ "properties": {
+ "metadata": {
"anyOf": [
{
- "type": "string"
+ "$ref": "#/components/schemas/MetadataField"
},
{
"type": "null"
}
],
- "title": "Description",
- "description": "Model description"
+ "default": null,
+ "description": "Optional metadata to be saved with the image",
+ "field_kind": "internal",
+ "input": "connection",
+ "orig_required": false,
+ "ui_hidden": false
},
- "source": {
- "type": "string",
- "title": "Source",
- "description": "The original source of the model (path, URL or repo_id)."
+ "id": {
+ "description": "The id of this instance of an invocation. Must be unique among all instances of invocations.",
+ "field_kind": "node_attribute",
+ "title": "Id",
+ "type": "string"
},
- "source_type": {
- "$ref": "#/components/schemas/ModelSourceType",
- "description": "The type of source"
+ "is_intermediate": {
+ "default": false,
+ "description": "Whether or not this is an intermediate invocation.",
+ "field_kind": "node_attribute",
+ "input": "direct",
+ "orig_required": true,
+ "title": "Is Intermediate",
+ "type": "boolean",
+ "ui_hidden": false,
+ "ui_type": "IsIntermediate"
},
- "source_api_response": {
- "anyOf": [
- {
- "type": "string"
- },
- {
- "type": "null"
- }
- ],
- "title": "Source Api Response",
- "description": "The original API response from the source, as stringified JSON."
+ "use_cache": {
+ "default": true,
+ "description": "Whether or not to use the cache",
+ "field_kind": "node_attribute",
+ "title": "Use Cache",
+ "type": "boolean"
},
- "source_url": {
- "anyOf": [
- {
- "type": "string"
- },
- {
- "type": "null"
- }
+ "label": {
+ "default": "* CUSTOM LABEL *",
+ "description": "Label for this metadata item",
+ "enum": [
+ "* CUSTOM LABEL *",
+ "positive_prompt",
+ "positive_style_prompt",
+ "negative_prompt",
+ "negative_style_prompt"
],
- "title": "Source Url",
- "description": "Optional URL for the model (e.g. download page or model page)."
+ "field_kind": "input",
+ "input": "direct",
+ "orig_default": "* CUSTOM LABEL *",
+ "orig_required": false,
+ "title": "Label",
+ "type": "string"
},
- "cover_image": {
+ "custom_label": {
"anyOf": [
{
"type": "string"
@@ -52077,117 +58606,57 @@
"type": "null"
}
],
- "title": "Cover Image",
- "description": "Url for image to preview model"
- },
- "type": {
- "type": "string",
- "const": "main",
- "title": "Type",
- "default": "main"
+ "default": null,
+ "description": "Label for this metadata item",
+ "field_kind": "input",
+ "input": "direct",
+ "orig_default": null,
+ "orig_required": false,
+ "title": "Custom Label"
},
- "trigger_phrases": {
+ "default_value": {
"anyOf": [
{
"items": {
"type": "string"
},
- "type": "array",
- "uniqueItems": true
- },
- {
- "type": "null"
- }
- ],
- "title": "Trigger Phrases",
- "description": "Set of trigger phrases for this model"
- },
- "default_settings": {
- "anyOf": [
- {
- "$ref": "#/components/schemas/MainModelDefaultSettings"
- },
- {
- "type": "null"
- }
- ],
- "description": "Default settings for this model"
- },
- "config_path": {
- "anyOf": [
- {
- "type": "string"
+ "type": "array"
},
{
"type": "null"
}
],
- "title": "Config Path",
- "description": "Path to the config for this model, if any."
- },
- "base": {
- "type": "string",
- "const": "z-image",
- "title": "Base",
- "default": "z-image"
- },
- "format": {
- "type": "string",
- "const": "gguf_quantized",
- "title": "Format",
- "default": "gguf_quantized"
+ "default": null,
+ "description": "The default string collection to use if not found in the metadata",
+ "field_kind": "input",
+ "input": "any",
+ "orig_required": true,
+ "title": "Default Value"
},
- "variant": {
- "$ref": "#/components/schemas/ZImageVariantType"
+ "type": {
+ "const": "metadata_to_string_collection",
+ "default": "metadata_to_string_collection",
+ "field_kind": "node_attribute",
+ "title": "type",
+ "type": "string"
}
},
+ "required": ["type", "id"],
+ "tags": ["metadata"],
+ "title": "Metadata To String Collection",
"type": "object",
- "required": [
- "key",
- "hash",
- "path",
- "file_size",
- "name",
- "description",
- "source",
- "source_type",
- "source_api_response",
- "source_url",
- "cover_image",
- "type",
- "trigger_phrases",
- "default_settings",
- "config_path",
- "base",
- "format",
- "variant"
- ],
- "title": "Main_GGUF_ZImage_Config",
- "description": "Model config for GGUF-quantized Z-Image transformer models."
+ "version": "1.0.0",
+ "output": {
+ "$ref": "#/components/schemas/StringCollectionOutput"
+ }
},
- "MaskCombineInvocation": {
- "category": "mask",
+ "MetadataToStringInvocation": {
+ "category": "metadata",
"class": "invocation",
- "classification": "stable",
- "description": "Combine two masks together by multiplying them using `PIL.ImageChops.multiply()`.",
+ "classification": "beta",
+ "description": "Extracts a string value of a label from metadata",
"node_pack": "invokeai",
"properties": {
- "board": {
- "anyOf": [
- {
- "$ref": "#/components/schemas/BoardField"
- },
- {
- "type": "null"
- }
- ],
- "default": null,
- "description": "The board to save the image to",
- "field_kind": "internal",
- "input": "direct",
- "orig_required": false,
- "ui_hidden": false
- },
"metadata": {
"anyOf": [
{
@@ -52228,76 +58697,80 @@
"title": "Use Cache",
"type": "boolean"
},
- "mask1": {
+ "label": {
+ "default": "* CUSTOM LABEL *",
+ "description": "Label for this metadata item",
+ "enum": [
+ "* CUSTOM LABEL *",
+ "positive_prompt",
+ "positive_style_prompt",
+ "negative_prompt",
+ "negative_style_prompt"
+ ],
+ "field_kind": "input",
+ "input": "direct",
+ "orig_default": "* CUSTOM LABEL *",
+ "orig_required": false,
+ "title": "Label",
+ "type": "string"
+ },
+ "custom_label": {
"anyOf": [
{
- "$ref": "#/components/schemas/ImageField"
+ "type": "string"
},
{
"type": "null"
}
],
"default": null,
- "description": "The first mask to combine",
+ "description": "Label for this metadata item",
"field_kind": "input",
- "input": "any",
- "orig_required": true
+ "input": "direct",
+ "orig_default": null,
+ "orig_required": false,
+ "title": "Custom Label"
},
- "mask2": {
+ "default_value": {
"anyOf": [
{
- "$ref": "#/components/schemas/ImageField"
+ "type": "string"
},
{
"type": "null"
}
],
"default": null,
- "description": "The second image to combine",
+ "description": "The default string to use if not found in the metadata",
"field_kind": "input",
"input": "any",
- "orig_required": true
+ "orig_required": true,
+ "title": "Default Value"
},
"type": {
- "const": "mask_combine",
- "default": "mask_combine",
+ "const": "metadata_to_string",
+ "default": "metadata_to_string",
"field_kind": "node_attribute",
"title": "type",
"type": "string"
}
},
"required": ["type", "id"],
- "tags": ["image", "mask", "multiply"],
- "title": "Combine Masks",
+ "tags": ["metadata"],
+ "title": "Metadata To String",
"type": "object",
- "version": "1.2.2",
+ "version": "1.0.0",
"output": {
- "$ref": "#/components/schemas/ImageOutput"
+ "$ref": "#/components/schemas/StringOutput"
}
},
- "MaskEdgeInvocation": {
- "category": "mask",
+ "MetadataToT2IAdaptersInvocation": {
+ "category": "metadata",
"class": "invocation",
- "classification": "stable",
- "description": "Applies an edge mask to an image",
+ "classification": "beta",
+ "description": "Extracts a T2I-Adapters value of a label from metadata",
"node_pack": "invokeai",
"properties": {
- "board": {
- "anyOf": [
- {
- "$ref": "#/components/schemas/BoardField"
- },
- {
- "type": "null"
- }
- ],
- "default": null,
- "description": "The board to save the image to",
- "field_kind": "internal",
- "input": "direct",
- "orig_required": false,
- "ui_hidden": false
- },
"metadata": {
"anyOf": [
{
@@ -52338,141 +58811,227 @@
"title": "Use Cache",
"type": "boolean"
},
- "image": {
+ "t2i_adapter_list": {
"anyOf": [
{
- "$ref": "#/components/schemas/ImageField"
+ "$ref": "#/components/schemas/T2IAdapterField"
},
{
- "type": "null"
- }
- ],
- "default": null,
- "description": "The image to apply the mask to",
- "field_kind": "input",
- "input": "any",
- "orig_required": true
- },
- "edge_size": {
- "anyOf": [
- {
- "type": "integer"
+ "items": {
+ "$ref": "#/components/schemas/T2IAdapterField"
+ },
+ "type": "array"
},
{
"type": "null"
}
],
"default": null,
- "description": "The size of the edge",
+ "description": "IP-Adapter to apply",
"field_kind": "input",
- "input": "any",
- "orig_required": true,
- "title": "Edge Size"
+ "input": "connection",
+ "orig_default": null,
+ "orig_required": false,
+ "title": "T2I-Adapter"
},
- "edge_blur": {
+ "type": {
+ "const": "metadata_to_t2i_adapters",
+ "default": "metadata_to_t2i_adapters",
+ "field_kind": "node_attribute",
+ "title": "type",
+ "type": "string"
+ }
+ },
+ "required": ["type", "id"],
+ "tags": ["metadata"],
+ "title": "Metadata To T2I-Adapters",
+ "type": "object",
+ "version": "1.2.0",
+ "output": {
+ "$ref": "#/components/schemas/MDT2IAdapterListOutput"
+ }
+ },
+ "MetadataToVAEInvocation": {
+ "category": "metadata",
+ "class": "invocation",
+ "classification": "beta",
+ "description": "Extracts a VAE value of a label from metadata",
+ "node_pack": "invokeai",
+ "properties": {
+ "metadata": {
"anyOf": [
{
- "type": "integer"
+ "$ref": "#/components/schemas/MetadataField"
},
{
"type": "null"
}
],
"default": null,
- "description": "The amount of blur on the edge",
- "field_kind": "input",
- "input": "any",
+ "description": "Optional metadata to be saved with the image",
+ "field_kind": "internal",
+ "input": "connection",
+ "orig_required": false,
+ "ui_hidden": false
+ },
+ "id": {
+ "description": "The id of this instance of an invocation. Must be unique among all instances of invocations.",
+ "field_kind": "node_attribute",
+ "title": "Id",
+ "type": "string"
+ },
+ "is_intermediate": {
+ "default": false,
+ "description": "Whether or not this is an intermediate invocation.",
+ "field_kind": "node_attribute",
+ "input": "direct",
"orig_required": true,
- "title": "Edge Blur"
+ "title": "Is Intermediate",
+ "type": "boolean",
+ "ui_hidden": false,
+ "ui_type": "IsIntermediate"
},
- "low_threshold": {
+ "use_cache": {
+ "default": true,
+ "description": "Whether or not to use the cache",
+ "field_kind": "node_attribute",
+ "title": "Use Cache",
+ "type": "boolean"
+ },
+ "label": {
+ "default": "vae",
+ "description": "Label for this metadata item",
+ "enum": ["* CUSTOM LABEL *", "vae"],
+ "field_kind": "input",
+ "input": "direct",
+ "orig_default": "vae",
+ "orig_required": false,
+ "title": "Label",
+ "type": "string"
+ },
+ "custom_label": {
"anyOf": [
{
- "type": "integer"
+ "type": "string"
},
{
"type": "null"
}
],
"default": null,
- "description": "First threshold for the hysteresis procedure in Canny edge detection",
+ "description": "Label for this metadata item",
"field_kind": "input",
- "input": "any",
- "orig_required": true,
- "title": "Low Threshold"
+ "input": "direct",
+ "orig_default": null,
+ "orig_required": false,
+ "title": "Custom Label"
},
- "high_threshold": {
+ "default_value": {
"anyOf": [
{
- "type": "integer"
+ "$ref": "#/components/schemas/VAEField"
},
{
"type": "null"
}
],
"default": null,
- "description": "Second threshold for the hysteresis procedure in Canny edge detection",
+ "description": "The default VAE to use if not found in the metadata",
"field_kind": "input",
"input": "any",
- "orig_required": true,
- "title": "High Threshold"
+ "orig_required": true
},
"type": {
- "const": "mask_edge",
- "default": "mask_edge",
+ "const": "metadata_to_vae",
+ "default": "metadata_to_vae",
"field_kind": "node_attribute",
"title": "type",
"type": "string"
}
},
"required": ["type", "id"],
- "tags": ["image", "mask", "inpaint"],
- "title": "Mask Edge",
+ "tags": ["metadata"],
+ "title": "Metadata To VAE",
"type": "object",
- "version": "1.2.2",
+ "version": "1.2.1",
"output": {
- "$ref": "#/components/schemas/ImageOutput"
+ "$ref": "#/components/schemas/VAEOutput"
}
},
- "MaskFromAlphaInvocation": {
- "category": "mask",
- "class": "invocation",
- "classification": "stable",
- "description": "Extracts the alpha channel of an image as a mask.",
- "node_pack": "invokeai",
+ "ModelFormat": {
+ "type": "string",
+ "enum": [
+ "omi",
+ "diffusers",
+ "checkpoint",
+ "lycoris",
+ "onnx",
+ "olive",
+ "embedding_file",
+ "embedding_folder",
+ "invokeai",
+ "t5_encoder",
+ "qwen3_encoder",
+ "qwen_vl_encoder",
+ "wan_t5_encoder",
+ "bnb_quantized_int8b",
+ "bnb_quantized_nf4b",
+ "gguf_quantized",
+ "external_api",
+ "unknown"
+ ],
+ "title": "ModelFormat",
+ "description": "Storage format of model."
+ },
+ "ModelIdentifierField": {
"properties": {
- "board": {
- "anyOf": [
- {
- "$ref": "#/components/schemas/BoardField"
- },
- {
- "type": "null"
- }
- ],
- "default": null,
- "description": "The board to save the image to",
- "field_kind": "internal",
- "input": "direct",
- "orig_required": false,
- "ui_hidden": false
+ "key": {
+ "description": "The model's unique key",
+ "title": "Key",
+ "type": "string"
},
- "metadata": {
+ "hash": {
+ "description": "The model's BLAKE3 hash",
+ "title": "Hash",
+ "type": "string"
+ },
+ "name": {
+ "description": "The model's name",
+ "title": "Name",
+ "type": "string"
+ },
+ "base": {
+ "$ref": "#/components/schemas/BaseModelType",
+ "description": "The model's base model type"
+ },
+ "type": {
+ "$ref": "#/components/schemas/ModelType",
+ "description": "The model's type"
+ },
+ "submodel_type": {
"anyOf": [
{
- "$ref": "#/components/schemas/MetadataField"
+ "$ref": "#/components/schemas/SubModelType"
},
{
"type": "null"
}
],
- "default": null,
- "description": "Optional metadata to be saved with the image",
- "field_kind": "internal",
- "input": "connection",
- "orig_required": false,
- "ui_hidden": false
- },
+ "default": null,
+ "description": "The submodel to load, if this is a main model"
+ }
+ },
+ "required": ["key", "hash", "name", "base", "type"],
+ "title": "ModelIdentifierField",
+ "type": "object"
+ },
+ "ModelIdentifierInvocation": {
+ "category": "model",
+ "class": "invocation",
+ "classification": "stable",
+ "description": "Selects any model, outputting it its identifier. Be careful with this one! The identifier will be accepted as\ninput for any model, even if the model types don't match. If you connect this to a mismatched input, you'll get an\nerror.",
+ "node_pack": "invokeai",
+ "properties": {
"id": {
"description": "The id of this instance of an invocation. Must be unique among all instances of invocations.",
"field_kind": "node_attribute",
@@ -52497,668 +59056,1153 @@
"title": "Use Cache",
"type": "boolean"
},
- "image": {
+ "model": {
"anyOf": [
{
- "$ref": "#/components/schemas/ImageField"
+ "$ref": "#/components/schemas/ModelIdentifierField"
},
{
"type": "null"
}
],
"default": null,
- "description": "The image to create the mask from",
- "field_kind": "input",
- "input": "any",
- "orig_required": true
- },
- "invert": {
- "default": false,
- "description": "Whether or not to invert the mask",
+ "description": "The model to select",
"field_kind": "input",
"input": "any",
- "orig_default": false,
- "orig_required": false,
- "title": "Invert",
- "type": "boolean"
+ "orig_required": true,
+ "title": "Model"
},
"type": {
- "const": "tomask",
- "default": "tomask",
+ "const": "model_identifier",
+ "default": "model_identifier",
"field_kind": "node_attribute",
"title": "type",
"type": "string"
}
},
"required": ["type", "id"],
- "tags": ["image", "mask"],
- "title": "Mask from Alpha",
+ "tags": ["model"],
+ "title": "Any Model",
"type": "object",
- "version": "1.2.2",
+ "version": "1.0.1",
"output": {
- "$ref": "#/components/schemas/ImageOutput"
+ "$ref": "#/components/schemas/ModelIdentifierOutput"
}
},
- "MaskFromIDInvocation": {
- "category": "mask",
- "class": "invocation",
- "classification": "stable",
- "description": "Generate a mask for a particular color in an ID Map",
- "node_pack": "invokeai",
+ "ModelIdentifierOutput": {
+ "class": "output",
+ "description": "Model identifier output",
"properties": {
- "board": {
- "anyOf": [
+ "model": {
+ "$ref": "#/components/schemas/ModelIdentifierField",
+ "description": "Model identifier",
+ "field_kind": "output",
+ "title": "Model",
+ "ui_hidden": false
+ },
+ "type": {
+ "const": "model_identifier_output",
+ "default": "model_identifier_output",
+ "field_kind": "node_attribute",
+ "title": "type",
+ "type": "string"
+ }
+ },
+ "required": ["output_meta", "model", "type", "type"],
+ "title": "ModelIdentifierOutput",
+ "type": "object"
+ },
+ "ModelInstallCancelledEvent": {
+ "description": "Event model for model_install_cancelled",
+ "properties": {
+ "timestamp": {
+ "description": "The timestamp of the event",
+ "title": "Timestamp",
+ "type": "integer"
+ },
+ "id": {
+ "description": "The ID of the install job",
+ "title": "Id",
+ "type": "integer"
+ },
+ "source": {
+ "description": "Source of the model; local path, repo_id or url",
+ "discriminator": {
+ "mapping": {
+ "external": "#/components/schemas/ExternalModelSource",
+ "hf": "#/components/schemas/HFModelSource",
+ "local": "#/components/schemas/LocalModelSource",
+ "url": "#/components/schemas/URLModelSource"
+ },
+ "propertyName": "type"
+ },
+ "oneOf": [
{
- "$ref": "#/components/schemas/BoardField"
+ "$ref": "#/components/schemas/LocalModelSource"
},
{
- "type": "null"
+ "$ref": "#/components/schemas/HFModelSource"
+ },
+ {
+ "$ref": "#/components/schemas/URLModelSource"
+ },
+ {
+ "$ref": "#/components/schemas/ExternalModelSource"
}
],
- "default": null,
- "description": "The board to save the image to",
- "field_kind": "internal",
- "input": "direct",
- "orig_required": false,
- "ui_hidden": false
+ "title": "Source"
+ }
+ },
+ "required": ["timestamp", "id", "source"],
+ "title": "ModelInstallCancelledEvent",
+ "type": "object"
+ },
+ "ModelInstallCompleteEvent": {
+ "description": "Event model for model_install_complete",
+ "properties": {
+ "timestamp": {
+ "description": "The timestamp of the event",
+ "title": "Timestamp",
+ "type": "integer"
},
- "metadata": {
- "anyOf": [
+ "id": {
+ "description": "The ID of the install job",
+ "title": "Id",
+ "type": "integer"
+ },
+ "source": {
+ "description": "Source of the model; local path, repo_id or url",
+ "discriminator": {
+ "mapping": {
+ "external": "#/components/schemas/ExternalModelSource",
+ "hf": "#/components/schemas/HFModelSource",
+ "local": "#/components/schemas/LocalModelSource",
+ "url": "#/components/schemas/URLModelSource"
+ },
+ "propertyName": "type"
+ },
+ "oneOf": [
{
- "$ref": "#/components/schemas/MetadataField"
+ "$ref": "#/components/schemas/LocalModelSource"
},
{
- "type": "null"
+ "$ref": "#/components/schemas/HFModelSource"
+ },
+ {
+ "$ref": "#/components/schemas/URLModelSource"
+ },
+ {
+ "$ref": "#/components/schemas/ExternalModelSource"
}
],
- "default": null,
- "description": "Optional metadata to be saved with the image",
- "field_kind": "internal",
- "input": "connection",
- "orig_required": false,
- "ui_hidden": false
+ "title": "Source"
},
- "id": {
- "description": "The id of this instance of an invocation. Must be unique among all instances of invocations.",
- "field_kind": "node_attribute",
- "title": "Id",
+ "key": {
+ "description": "Model config record key",
+ "title": "Key",
"type": "string"
},
- "is_intermediate": {
- "default": false,
- "description": "Whether or not this is an intermediate invocation.",
- "field_kind": "node_attribute",
- "input": "direct",
- "orig_required": true,
- "title": "Is Intermediate",
- "type": "boolean",
- "ui_hidden": false,
- "ui_type": "IsIntermediate"
+ "total_bytes": {
+ "anyOf": [
+ {
+ "type": "integer"
+ },
+ {
+ "type": "null"
+ }
+ ],
+ "description": "Size of the model (may be None for installation of a local path)",
+ "title": "Total Bytes"
},
- "use_cache": {
- "default": true,
- "description": "Whether or not to use the cache",
- "field_kind": "node_attribute",
- "title": "Use Cache",
- "type": "boolean"
+ "config": {
+ "description": "The installed model's config",
+ "oneOf": [
+ {
+ "$ref": "#/components/schemas/Main_Diffusers_SD1_Config"
+ },
+ {
+ "$ref": "#/components/schemas/Main_Diffusers_SD2_Config"
+ },
+ {
+ "$ref": "#/components/schemas/Main_Diffusers_SDXL_Config"
+ },
+ {
+ "$ref": "#/components/schemas/Main_Diffusers_SDXLRefiner_Config"
+ },
+ {
+ "$ref": "#/components/schemas/Main_Diffusers_SD3_Config"
+ },
+ {
+ "$ref": "#/components/schemas/Main_Diffusers_FLUX_Config"
+ },
+ {
+ "$ref": "#/components/schemas/Main_Diffusers_Flux2_Config"
+ },
+ {
+ "$ref": "#/components/schemas/Main_Diffusers_CogView4_Config"
+ },
+ {
+ "$ref": "#/components/schemas/Main_Diffusers_QwenImage_Config"
+ },
+ {
+ "$ref": "#/components/schemas/Main_Diffusers_Wan_Config"
+ },
+ {
+ "$ref": "#/components/schemas/Main_Diffusers_ZImage_Config"
+ },
+ {
+ "$ref": "#/components/schemas/Main_Checkpoint_SD1_Config"
+ },
+ {
+ "$ref": "#/components/schemas/Main_Checkpoint_SD2_Config"
+ },
+ {
+ "$ref": "#/components/schemas/Main_Checkpoint_SDXL_Config"
+ },
+ {
+ "$ref": "#/components/schemas/Main_Checkpoint_SDXLRefiner_Config"
+ },
+ {
+ "$ref": "#/components/schemas/Main_Checkpoint_Flux2_Config"
+ },
+ {
+ "$ref": "#/components/schemas/Main_Checkpoint_FLUX_Config"
+ },
+ {
+ "$ref": "#/components/schemas/Main_Checkpoint_QwenImage_Config"
+ },
+ {
+ "$ref": "#/components/schemas/Main_Checkpoint_ZImage_Config"
+ },
+ {
+ "$ref": "#/components/schemas/Main_Checkpoint_Anima_Config"
+ },
+ {
+ "$ref": "#/components/schemas/Main_BnBNF4_FLUX_Config"
+ },
+ {
+ "$ref": "#/components/schemas/Main_GGUF_Flux2_Config"
+ },
+ {
+ "$ref": "#/components/schemas/Main_GGUF_FLUX_Config"
+ },
+ {
+ "$ref": "#/components/schemas/Main_GGUF_QwenImage_Config"
+ },
+ {
+ "$ref": "#/components/schemas/Main_GGUF_Wan_Config"
+ },
+ {
+ "$ref": "#/components/schemas/Main_GGUF_ZImage_Config"
+ },
+ {
+ "$ref": "#/components/schemas/VAE_Checkpoint_SD1_Config"
+ },
+ {
+ "$ref": "#/components/schemas/VAE_Checkpoint_SD2_Config"
+ },
+ {
+ "$ref": "#/components/schemas/VAE_Checkpoint_SDXL_Config"
+ },
+ {
+ "$ref": "#/components/schemas/VAE_Checkpoint_FLUX_Config"
+ },
+ {
+ "$ref": "#/components/schemas/VAE_Checkpoint_Flux2_Config"
+ },
+ {
+ "$ref": "#/components/schemas/VAE_Checkpoint_Wan_Config"
+ },
+ {
+ "$ref": "#/components/schemas/VAE_Checkpoint_QwenImage_Config"
+ },
+ {
+ "$ref": "#/components/schemas/VAE_Checkpoint_Anima_Config"
+ },
+ {
+ "$ref": "#/components/schemas/VAE_Diffusers_SD1_Config"
+ },
+ {
+ "$ref": "#/components/schemas/VAE_Diffusers_SDXL_Config"
+ },
+ {
+ "$ref": "#/components/schemas/VAE_Diffusers_Flux2_Config"
+ },
+ {
+ "$ref": "#/components/schemas/VAE_Diffusers_Wan_Config"
+ },
+ {
+ "$ref": "#/components/schemas/ControlNet_Checkpoint_SD1_Config"
+ },
+ {
+ "$ref": "#/components/schemas/ControlNet_Checkpoint_SD2_Config"
+ },
+ {
+ "$ref": "#/components/schemas/ControlNet_Checkpoint_SDXL_Config"
+ },
+ {
+ "$ref": "#/components/schemas/ControlNet_Checkpoint_FLUX_Config"
+ },
+ {
+ "$ref": "#/components/schemas/ControlNet_Checkpoint_ZImage_Config"
+ },
+ {
+ "$ref": "#/components/schemas/ControlNet_Checkpoint_Anima_Config"
+ },
+ {
+ "$ref": "#/components/schemas/ControlNet_Diffusers_SD1_Config"
+ },
+ {
+ "$ref": "#/components/schemas/ControlNet_Diffusers_SD2_Config"
+ },
+ {
+ "$ref": "#/components/schemas/ControlNet_Diffusers_SDXL_Config"
+ },
+ {
+ "$ref": "#/components/schemas/ControlNet_Diffusers_FLUX_Config"
+ },
+ {
+ "$ref": "#/components/schemas/LoRA_LyCORIS_SD1_Config"
+ },
+ {
+ "$ref": "#/components/schemas/LoRA_LyCORIS_SD2_Config"
+ },
+ {
+ "$ref": "#/components/schemas/LoRA_LyCORIS_SDXL_Config"
+ },
+ {
+ "$ref": "#/components/schemas/LoRA_LyCORIS_Flux2_Config"
+ },
+ {
+ "$ref": "#/components/schemas/LoRA_LyCORIS_FLUX_Config"
+ },
+ {
+ "$ref": "#/components/schemas/LoRA_LyCORIS_ZImage_Config"
+ },
+ {
+ "$ref": "#/components/schemas/LoRA_LyCORIS_QwenImage_Config"
+ },
+ {
+ "$ref": "#/components/schemas/LoRA_LyCORIS_Wan_Config"
+ },
+ {
+ "$ref": "#/components/schemas/LoRA_LyCORIS_Anima_Config"
+ },
+ {
+ "$ref": "#/components/schemas/LoRA_OMI_SDXL_Config"
+ },
+ {
+ "$ref": "#/components/schemas/LoRA_OMI_FLUX_Config"
+ },
+ {
+ "$ref": "#/components/schemas/LoRA_Diffusers_SD1_Config"
+ },
+ {
+ "$ref": "#/components/schemas/LoRA_Diffusers_SD2_Config"
+ },
+ {
+ "$ref": "#/components/schemas/LoRA_Diffusers_SDXL_Config"
+ },
+ {
+ "$ref": "#/components/schemas/LoRA_Diffusers_Flux2_Config"
+ },
+ {
+ "$ref": "#/components/schemas/LoRA_Diffusers_FLUX_Config"
+ },
+ {
+ "$ref": "#/components/schemas/LoRA_Diffusers_ZImage_Config"
+ },
+ {
+ "$ref": "#/components/schemas/ControlLoRA_LyCORIS_FLUX_Config"
+ },
+ {
+ "$ref": "#/components/schemas/T5Encoder_T5Encoder_Config"
+ },
+ {
+ "$ref": "#/components/schemas/T5Encoder_BnBLLMint8_Config"
+ },
+ {
+ "$ref": "#/components/schemas/Qwen3Encoder_Qwen3Encoder_Config"
+ },
+ {
+ "$ref": "#/components/schemas/Qwen3Encoder_Checkpoint_Config"
+ },
+ {
+ "$ref": "#/components/schemas/Qwen3Encoder_GGUF_Config"
+ },
+ {
+ "$ref": "#/components/schemas/QwenVLEncoder_Diffusers_Config"
+ },
+ {
+ "$ref": "#/components/schemas/QwenVLEncoder_Checkpoint_Config"
+ },
+ {
+ "$ref": "#/components/schemas/WanT5Encoder_WanT5Encoder_Config"
+ },
+ {
+ "$ref": "#/components/schemas/TI_File_SD1_Config"
+ },
+ {
+ "$ref": "#/components/schemas/TI_File_SD2_Config"
+ },
+ {
+ "$ref": "#/components/schemas/TI_File_SDXL_Config"
+ },
+ {
+ "$ref": "#/components/schemas/TI_Folder_SD1_Config"
+ },
+ {
+ "$ref": "#/components/schemas/TI_Folder_SD2_Config"
+ },
+ {
+ "$ref": "#/components/schemas/TI_Folder_SDXL_Config"
+ },
+ {
+ "$ref": "#/components/schemas/IPAdapter_InvokeAI_SD1_Config"
+ },
+ {
+ "$ref": "#/components/schemas/IPAdapter_InvokeAI_SD2_Config"
+ },
+ {
+ "$ref": "#/components/schemas/IPAdapter_InvokeAI_SDXL_Config"
+ },
+ {
+ "$ref": "#/components/schemas/IPAdapter_Checkpoint_SD1_Config"
+ },
+ {
+ "$ref": "#/components/schemas/IPAdapter_Checkpoint_SD2_Config"
+ },
+ {
+ "$ref": "#/components/schemas/IPAdapter_Checkpoint_SDXL_Config"
+ },
+ {
+ "$ref": "#/components/schemas/IPAdapter_Checkpoint_FLUX_Config"
+ },
+ {
+ "$ref": "#/components/schemas/T2IAdapter_Diffusers_SD1_Config"
+ },
+ {
+ "$ref": "#/components/schemas/T2IAdapter_Diffusers_SDXL_Config"
+ },
+ {
+ "$ref": "#/components/schemas/Spandrel_Checkpoint_Config"
+ },
+ {
+ "$ref": "#/components/schemas/CLIPEmbed_Diffusers_G_Config"
+ },
+ {
+ "$ref": "#/components/schemas/CLIPEmbed_Diffusers_L_Config"
+ },
+ {
+ "$ref": "#/components/schemas/CLIPVision_Diffusers_Config"
+ },
+ {
+ "$ref": "#/components/schemas/SigLIP_Diffusers_Config"
+ },
+ {
+ "$ref": "#/components/schemas/FLUXRedux_Checkpoint_Config"
+ },
+ {
+ "$ref": "#/components/schemas/LlavaOnevision_Diffusers_Config"
+ },
+ {
+ "$ref": "#/components/schemas/TextLLM_Diffusers_Config"
+ },
+ {
+ "$ref": "#/components/schemas/ExternalApiModelConfig"
+ },
+ {
+ "$ref": "#/components/schemas/Unknown_Config"
+ }
+ ],
+ "title": "Config"
+ }
+ },
+ "required": ["timestamp", "id", "source", "key", "total_bytes", "config"],
+ "title": "ModelInstallCompleteEvent",
+ "type": "object"
+ },
+ "ModelInstallDownloadProgressEvent": {
+ "description": "Event model for model_install_download_progress",
+ "properties": {
+ "timestamp": {
+ "description": "The timestamp of the event",
+ "title": "Timestamp",
+ "type": "integer"
+ },
+ "id": {
+ "description": "The ID of the install job",
+ "title": "Id",
+ "type": "integer"
},
- "image": {
- "anyOf": [
+ "source": {
+ "description": "Source of the model; local path, repo_id or url",
+ "discriminator": {
+ "mapping": {
+ "external": "#/components/schemas/ExternalModelSource",
+ "hf": "#/components/schemas/HFModelSource",
+ "local": "#/components/schemas/LocalModelSource",
+ "url": "#/components/schemas/URLModelSource"
+ },
+ "propertyName": "type"
+ },
+ "oneOf": [
{
- "$ref": "#/components/schemas/ImageField"
+ "$ref": "#/components/schemas/LocalModelSource"
},
{
- "type": "null"
- }
- ],
- "default": null,
- "description": "The image to create the mask from",
- "field_kind": "input",
- "input": "any",
- "orig_required": true
- },
- "color": {
- "anyOf": [
+ "$ref": "#/components/schemas/HFModelSource"
+ },
{
- "$ref": "#/components/schemas/ColorField"
+ "$ref": "#/components/schemas/URLModelSource"
},
{
- "type": "null"
+ "$ref": "#/components/schemas/ExternalModelSource"
}
],
- "default": null,
- "description": "ID color to mask",
- "field_kind": "input",
- "input": "any",
- "orig_required": true
- },
- "threshold": {
- "default": 100,
- "description": "Threshold for color detection",
- "field_kind": "input",
- "input": "any",
- "orig_default": 100,
- "orig_required": false,
- "title": "Threshold",
- "type": "integer"
- },
- "invert": {
- "default": false,
- "description": "Whether or not to invert the mask",
- "field_kind": "input",
- "input": "any",
- "orig_default": false,
- "orig_required": false,
- "title": "Invert",
- "type": "boolean"
+ "title": "Source"
},
- "type": {
- "const": "mask_from_id",
- "default": "mask_from_id",
- "field_kind": "node_attribute",
- "title": "type",
+ "local_path": {
+ "description": "Where model is downloading to",
+ "title": "Local Path",
"type": "string"
- }
- },
- "required": ["type", "id"],
- "tags": ["image", "mask", "id"],
- "title": "Mask from Segmented Image",
- "type": "object",
- "version": "1.0.1",
- "output": {
- "$ref": "#/components/schemas/ImageOutput"
- }
- },
- "MaskOutput": {
- "class": "output",
- "description": "A torch mask tensor.",
- "properties": {
- "mask": {
- "$ref": "#/components/schemas/TensorField",
- "description": "The mask.",
- "field_kind": "output",
- "ui_hidden": false
},
- "width": {
- "description": "The width of the mask in pixels.",
- "field_kind": "output",
- "title": "Width",
- "type": "integer",
- "ui_hidden": false
+ "bytes": {
+ "description": "Number of bytes downloaded so far",
+ "title": "Bytes",
+ "type": "integer"
},
- "height": {
- "description": "The height of the mask in pixels.",
- "field_kind": "output",
- "title": "Height",
- "type": "integer",
- "ui_hidden": false
+ "total_bytes": {
+ "description": "Total size of download, including all files",
+ "title": "Total Bytes",
+ "type": "integer"
},
- "type": {
- "const": "mask_output",
- "default": "mask_output",
- "field_kind": "node_attribute",
- "title": "type",
- "type": "string"
+ "parts": {
+ "description": "Progress of downloading URLs that comprise the model, if any",
+ "items": {
+ "additionalProperties": {
+ "anyOf": [
+ {
+ "type": "integer"
+ },
+ {
+ "type": "string"
+ }
+ ]
+ },
+ "type": "object"
+ },
+ "title": "Parts",
+ "type": "array"
}
},
- "required": ["output_meta", "mask", "width", "height", "type", "type"],
- "title": "MaskOutput",
+ "required": ["timestamp", "id", "source", "local_path", "bytes", "total_bytes", "parts"],
+ "title": "ModelInstallDownloadProgressEvent",
"type": "object"
},
- "MaskTensorToImageInvocation": {
- "category": "mask",
- "class": "invocation",
- "classification": "stable",
- "description": "Convert a mask tensor to an image.",
- "node_pack": "invokeai",
+ "ModelInstallDownloadStartedEvent": {
+ "description": "Event model for model_install_download_started",
"properties": {
- "board": {
- "anyOf": [
+ "timestamp": {
+ "description": "The timestamp of the event",
+ "title": "Timestamp",
+ "type": "integer"
+ },
+ "id": {
+ "description": "The ID of the install job",
+ "title": "Id",
+ "type": "integer"
+ },
+ "source": {
+ "description": "Source of the model; local path, repo_id or url",
+ "discriminator": {
+ "mapping": {
+ "external": "#/components/schemas/ExternalModelSource",
+ "hf": "#/components/schemas/HFModelSource",
+ "local": "#/components/schemas/LocalModelSource",
+ "url": "#/components/schemas/URLModelSource"
+ },
+ "propertyName": "type"
+ },
+ "oneOf": [
{
- "$ref": "#/components/schemas/BoardField"
+ "$ref": "#/components/schemas/LocalModelSource"
},
{
- "type": "null"
- }
- ],
- "default": null,
- "description": "The board to save the image to",
- "field_kind": "internal",
- "input": "direct",
- "orig_required": false,
- "ui_hidden": false
- },
- "metadata": {
- "anyOf": [
+ "$ref": "#/components/schemas/HFModelSource"
+ },
{
- "$ref": "#/components/schemas/MetadataField"
+ "$ref": "#/components/schemas/URLModelSource"
},
{
- "type": "null"
+ "$ref": "#/components/schemas/ExternalModelSource"
}
],
- "default": null,
- "description": "Optional metadata to be saved with the image",
- "field_kind": "internal",
- "input": "connection",
- "orig_required": false,
- "ui_hidden": false
+ "title": "Source"
},
- "id": {
- "description": "The id of this instance of an invocation. Must be unique among all instances of invocations.",
- "field_kind": "node_attribute",
- "title": "Id",
+ "local_path": {
+ "description": "Where model is downloading to",
+ "title": "Local Path",
"type": "string"
},
- "is_intermediate": {
- "default": false,
- "description": "Whether or not this is an intermediate invocation.",
- "field_kind": "node_attribute",
- "input": "direct",
- "orig_required": true,
- "title": "Is Intermediate",
- "type": "boolean",
- "ui_hidden": false,
- "ui_type": "IsIntermediate"
+ "bytes": {
+ "description": "Number of bytes downloaded so far",
+ "title": "Bytes",
+ "type": "integer"
},
- "use_cache": {
- "default": true,
- "description": "Whether or not to use the cache",
- "field_kind": "node_attribute",
- "title": "Use Cache",
- "type": "boolean"
+ "total_bytes": {
+ "description": "Total size of download, including all files",
+ "title": "Total Bytes",
+ "type": "integer"
},
- "mask": {
- "anyOf": [
- {
- "$ref": "#/components/schemas/TensorField"
+ "parts": {
+ "description": "Progress of downloading URLs that comprise the model, if any",
+ "items": {
+ "additionalProperties": {
+ "anyOf": [
+ {
+ "type": "integer"
+ },
+ {
+ "type": "string"
+ }
+ ]
},
- {
- "type": "null"
- }
- ],
- "default": null,
- "description": "The mask tensor to convert.",
- "field_kind": "input",
- "input": "any",
- "orig_required": true
- },
- "type": {
- "const": "tensor_mask_to_image",
- "default": "tensor_mask_to_image",
- "field_kind": "node_attribute",
- "title": "type",
- "type": "string"
+ "type": "object"
+ },
+ "title": "Parts",
+ "type": "array"
}
},
- "required": ["type", "id"],
- "tags": ["mask"],
- "title": "Tensor Mask to Image",
- "type": "object",
- "version": "1.1.0",
- "output": {
- "$ref": "#/components/schemas/ImageOutput"
- }
+ "required": ["timestamp", "id", "source", "local_path", "bytes", "total_bytes", "parts"],
+ "title": "ModelInstallDownloadStartedEvent",
+ "type": "object"
},
- "MediaPipeFaceDetectionInvocation": {
- "category": "controlnet_preprocessors",
- "class": "invocation",
- "classification": "stable",
- "description": "Detects faces using MediaPipe.",
- "node_pack": "invokeai",
+ "ModelInstallDownloadsCompleteEvent": {
+ "description": "Emitted once when an install job becomes active.",
"properties": {
- "board": {
- "anyOf": [
- {
- "$ref": "#/components/schemas/BoardField"
- },
- {
- "type": "null"
- }
- ],
- "default": null,
- "description": "The board to save the image to",
- "field_kind": "internal",
- "input": "direct",
- "orig_required": false,
- "ui_hidden": false
- },
- "metadata": {
- "anyOf": [
- {
- "$ref": "#/components/schemas/MetadataField"
- },
- {
- "type": "null"
- }
- ],
- "default": null,
- "description": "Optional metadata to be saved with the image",
- "field_kind": "internal",
- "input": "connection",
- "orig_required": false,
- "ui_hidden": false
+ "timestamp": {
+ "description": "The timestamp of the event",
+ "title": "Timestamp",
+ "type": "integer"
},
"id": {
- "description": "The id of this instance of an invocation. Must be unique among all instances of invocations.",
- "field_kind": "node_attribute",
+ "description": "The ID of the install job",
"title": "Id",
- "type": "string"
- },
- "is_intermediate": {
- "default": false,
- "description": "Whether or not this is an intermediate invocation.",
- "field_kind": "node_attribute",
- "input": "direct",
- "orig_required": true,
- "title": "Is Intermediate",
- "type": "boolean",
- "ui_hidden": false,
- "ui_type": "IsIntermediate"
- },
- "use_cache": {
- "default": true,
- "description": "Whether or not to use the cache",
- "field_kind": "node_attribute",
- "title": "Use Cache",
- "type": "boolean"
+ "type": "integer"
},
- "image": {
- "anyOf": [
+ "source": {
+ "description": "Source of the model; local path, repo_id or url",
+ "discriminator": {
+ "mapping": {
+ "external": "#/components/schemas/ExternalModelSource",
+ "hf": "#/components/schemas/HFModelSource",
+ "local": "#/components/schemas/LocalModelSource",
+ "url": "#/components/schemas/URLModelSource"
+ },
+ "propertyName": "type"
+ },
+ "oneOf": [
{
- "$ref": "#/components/schemas/ImageField"
+ "$ref": "#/components/schemas/LocalModelSource"
},
{
- "type": "null"
+ "$ref": "#/components/schemas/HFModelSource"
+ },
+ {
+ "$ref": "#/components/schemas/URLModelSource"
+ },
+ {
+ "$ref": "#/components/schemas/ExternalModelSource"
}
],
- "default": null,
- "description": "The image to process",
- "field_kind": "input",
- "input": "any",
- "orig_required": true
- },
- "max_faces": {
- "default": 1,
- "description": "Maximum number of faces to detect",
- "field_kind": "input",
- "input": "any",
- "minimum": 1,
- "orig_default": 1,
- "orig_required": false,
- "title": "Max Faces",
- "type": "integer"
- },
- "min_confidence": {
- "default": 0.5,
- "description": "Minimum confidence for face detection",
- "field_kind": "input",
- "input": "any",
- "maximum": 1,
- "minimum": 0,
- "orig_default": 0.5,
- "orig_required": false,
- "title": "Min Confidence",
- "type": "number"
- },
- "type": {
- "const": "mediapipe_face_detection",
- "default": "mediapipe_face_detection",
- "field_kind": "node_attribute",
- "title": "type",
- "type": "string"
+ "title": "Source"
}
},
- "required": ["type", "id"],
- "tags": ["controlnet", "face"],
- "title": "MediaPipe Face Detection",
- "type": "object",
- "version": "1.0.0",
- "output": {
- "$ref": "#/components/schemas/ImageOutput"
- }
+ "required": ["timestamp", "id", "source"],
+ "title": "ModelInstallDownloadsCompleteEvent",
+ "type": "object"
},
- "MergeMetadataInvocation": {
- "category": "metadata",
- "class": "invocation",
- "classification": "stable",
- "description": "Merged a collection of MetadataDict into a single MetadataDict.",
- "node_pack": "invokeai",
+ "ModelInstallErrorEvent": {
+ "description": "Event model for model_install_error",
"properties": {
+ "timestamp": {
+ "description": "The timestamp of the event",
+ "title": "Timestamp",
+ "type": "integer"
+ },
"id": {
- "description": "The id of this instance of an invocation. Must be unique among all instances of invocations.",
- "field_kind": "node_attribute",
+ "description": "The ID of the install job",
"title": "Id",
- "type": "string"
- },
- "is_intermediate": {
- "default": false,
- "description": "Whether or not this is an intermediate invocation.",
- "field_kind": "node_attribute",
- "input": "direct",
- "orig_required": true,
- "title": "Is Intermediate",
- "type": "boolean",
- "ui_hidden": false,
- "ui_type": "IsIntermediate"
- },
- "use_cache": {
- "default": true,
- "description": "Whether or not to use the cache",
- "field_kind": "node_attribute",
- "title": "Use Cache",
- "type": "boolean"
+ "type": "integer"
},
- "collection": {
- "anyOf": [
+ "source": {
+ "description": "Source of the model; local path, repo_id or url",
+ "discriminator": {
+ "mapping": {
+ "external": "#/components/schemas/ExternalModelSource",
+ "hf": "#/components/schemas/HFModelSource",
+ "local": "#/components/schemas/LocalModelSource",
+ "url": "#/components/schemas/URLModelSource"
+ },
+ "propertyName": "type"
+ },
+ "oneOf": [
{
- "items": {
- "$ref": "#/components/schemas/MetadataField"
- },
- "type": "array"
+ "$ref": "#/components/schemas/LocalModelSource"
},
{
- "type": "null"
+ "$ref": "#/components/schemas/HFModelSource"
+ },
+ {
+ "$ref": "#/components/schemas/URLModelSource"
+ },
+ {
+ "$ref": "#/components/schemas/ExternalModelSource"
}
],
- "default": null,
- "description": "Collection of Metadata",
- "field_kind": "input",
- "input": "any",
- "orig_required": true,
- "title": "Collection"
+ "title": "Source"
},
- "type": {
- "const": "merge_metadata",
- "default": "merge_metadata",
- "field_kind": "node_attribute",
- "title": "type",
+ "error_type": {
+ "description": "The name of the exception",
+ "title": "Error Type",
+ "type": "string"
+ },
+ "error": {
+ "description": "A text description of the exception",
+ "title": "Error",
"type": "string"
}
},
- "required": ["type", "id"],
- "tags": ["metadata"],
- "title": "Metadata Merge",
- "type": "object",
- "version": "1.0.1",
- "output": {
- "$ref": "#/components/schemas/MetadataOutput"
- }
- },
- "MergeTilesToImageInvocation": {
- "category": "tiles",
- "class": "invocation",
- "classification": "stable",
- "description": "Merge multiple tile images into a single image.",
- "node_pack": "invokeai",
+ "required": ["timestamp", "id", "source", "error_type", "error"],
+ "title": "ModelInstallErrorEvent",
+ "type": "object"
+ },
+ "ModelInstallJob": {
"properties": {
- "board": {
+ "id": {
+ "type": "integer",
+ "title": "Id",
+ "description": "Unique ID for this job"
+ },
+ "status": {
+ "$ref": "#/components/schemas/InstallStatus",
+ "description": "Current status of install process",
+ "default": "waiting"
+ },
+ "error_reason": {
"anyOf": [
{
- "$ref": "#/components/schemas/BoardField"
+ "type": "string"
},
{
"type": "null"
}
],
- "default": null,
- "description": "The board to save the image to",
- "field_kind": "internal",
- "input": "direct",
- "orig_required": false,
- "ui_hidden": false
+ "title": "Error Reason",
+ "description": "Information about why the job failed"
},
- "metadata": {
+ "config_in": {
+ "$ref": "#/components/schemas/ModelRecordChanges",
+ "description": "Configuration information (e.g. 'description') to apply to model."
+ },
+ "config_out": {
"anyOf": [
{
- "$ref": "#/components/schemas/MetadataField"
+ "oneOf": [
+ {
+ "$ref": "#/components/schemas/Main_Diffusers_SD1_Config"
+ },
+ {
+ "$ref": "#/components/schemas/Main_Diffusers_SD2_Config"
+ },
+ {
+ "$ref": "#/components/schemas/Main_Diffusers_SDXL_Config"
+ },
+ {
+ "$ref": "#/components/schemas/Main_Diffusers_SDXLRefiner_Config"
+ },
+ {
+ "$ref": "#/components/schemas/Main_Diffusers_SD3_Config"
+ },
+ {
+ "$ref": "#/components/schemas/Main_Diffusers_FLUX_Config"
+ },
+ {
+ "$ref": "#/components/schemas/Main_Diffusers_Flux2_Config"
+ },
+ {
+ "$ref": "#/components/schemas/Main_Diffusers_CogView4_Config"
+ },
+ {
+ "$ref": "#/components/schemas/Main_Diffusers_QwenImage_Config"
+ },
+ {
+ "$ref": "#/components/schemas/Main_Diffusers_Wan_Config"
+ },
+ {
+ "$ref": "#/components/schemas/Main_Diffusers_ZImage_Config"
+ },
+ {
+ "$ref": "#/components/schemas/Main_Checkpoint_SD1_Config"
+ },
+ {
+ "$ref": "#/components/schemas/Main_Checkpoint_SD2_Config"
+ },
+ {
+ "$ref": "#/components/schemas/Main_Checkpoint_SDXL_Config"
+ },
+ {
+ "$ref": "#/components/schemas/Main_Checkpoint_SDXLRefiner_Config"
+ },
+ {
+ "$ref": "#/components/schemas/Main_Checkpoint_Flux2_Config"
+ },
+ {
+ "$ref": "#/components/schemas/Main_Checkpoint_FLUX_Config"
+ },
+ {
+ "$ref": "#/components/schemas/Main_Checkpoint_QwenImage_Config"
+ },
+ {
+ "$ref": "#/components/schemas/Main_Checkpoint_ZImage_Config"
+ },
+ {
+ "$ref": "#/components/schemas/Main_Checkpoint_Anima_Config"
+ },
+ {
+ "$ref": "#/components/schemas/Main_BnBNF4_FLUX_Config"
+ },
+ {
+ "$ref": "#/components/schemas/Main_GGUF_Flux2_Config"
+ },
+ {
+ "$ref": "#/components/schemas/Main_GGUF_FLUX_Config"
+ },
+ {
+ "$ref": "#/components/schemas/Main_GGUF_QwenImage_Config"
+ },
+ {
+ "$ref": "#/components/schemas/Main_GGUF_Wan_Config"
+ },
+ {
+ "$ref": "#/components/schemas/Main_GGUF_ZImage_Config"
+ },
+ {
+ "$ref": "#/components/schemas/VAE_Checkpoint_SD1_Config"
+ },
+ {
+ "$ref": "#/components/schemas/VAE_Checkpoint_SD2_Config"
+ },
+ {
+ "$ref": "#/components/schemas/VAE_Checkpoint_SDXL_Config"
+ },
+ {
+ "$ref": "#/components/schemas/VAE_Checkpoint_FLUX_Config"
+ },
+ {
+ "$ref": "#/components/schemas/VAE_Checkpoint_Flux2_Config"
+ },
+ {
+ "$ref": "#/components/schemas/VAE_Checkpoint_Wan_Config"
+ },
+ {
+ "$ref": "#/components/schemas/VAE_Checkpoint_QwenImage_Config"
+ },
+ {
+ "$ref": "#/components/schemas/VAE_Checkpoint_Anima_Config"
+ },
+ {
+ "$ref": "#/components/schemas/VAE_Diffusers_SD1_Config"
+ },
+ {
+ "$ref": "#/components/schemas/VAE_Diffusers_SDXL_Config"
+ },
+ {
+ "$ref": "#/components/schemas/VAE_Diffusers_Flux2_Config"
+ },
+ {
+ "$ref": "#/components/schemas/VAE_Diffusers_Wan_Config"
+ },
+ {
+ "$ref": "#/components/schemas/ControlNet_Checkpoint_SD1_Config"
+ },
+ {
+ "$ref": "#/components/schemas/ControlNet_Checkpoint_SD2_Config"
+ },
+ {
+ "$ref": "#/components/schemas/ControlNet_Checkpoint_SDXL_Config"
+ },
+ {
+ "$ref": "#/components/schemas/ControlNet_Checkpoint_FLUX_Config"
+ },
+ {
+ "$ref": "#/components/schemas/ControlNet_Checkpoint_ZImage_Config"
+ },
+ {
+ "$ref": "#/components/schemas/ControlNet_Checkpoint_Anima_Config"
+ },
+ {
+ "$ref": "#/components/schemas/ControlNet_Diffusers_SD1_Config"
+ },
+ {
+ "$ref": "#/components/schemas/ControlNet_Diffusers_SD2_Config"
+ },
+ {
+ "$ref": "#/components/schemas/ControlNet_Diffusers_SDXL_Config"
+ },
+ {
+ "$ref": "#/components/schemas/ControlNet_Diffusers_FLUX_Config"
+ },
+ {
+ "$ref": "#/components/schemas/LoRA_LyCORIS_SD1_Config"
+ },
+ {
+ "$ref": "#/components/schemas/LoRA_LyCORIS_SD2_Config"
+ },
+ {
+ "$ref": "#/components/schemas/LoRA_LyCORIS_SDXL_Config"
+ },
+ {
+ "$ref": "#/components/schemas/LoRA_LyCORIS_Flux2_Config"
+ },
+ {
+ "$ref": "#/components/schemas/LoRA_LyCORIS_FLUX_Config"
+ },
+ {
+ "$ref": "#/components/schemas/LoRA_LyCORIS_ZImage_Config"
+ },
+ {
+ "$ref": "#/components/schemas/LoRA_LyCORIS_QwenImage_Config"
+ },
+ {
+ "$ref": "#/components/schemas/LoRA_LyCORIS_Wan_Config"
+ },
+ {
+ "$ref": "#/components/schemas/LoRA_LyCORIS_Anima_Config"
+ },
+ {
+ "$ref": "#/components/schemas/LoRA_OMI_SDXL_Config"
+ },
+ {
+ "$ref": "#/components/schemas/LoRA_OMI_FLUX_Config"
+ },
+ {
+ "$ref": "#/components/schemas/LoRA_Diffusers_SD1_Config"
+ },
+ {
+ "$ref": "#/components/schemas/LoRA_Diffusers_SD2_Config"
+ },
+ {
+ "$ref": "#/components/schemas/LoRA_Diffusers_SDXL_Config"
+ },
+ {
+ "$ref": "#/components/schemas/LoRA_Diffusers_Flux2_Config"
+ },
+ {
+ "$ref": "#/components/schemas/LoRA_Diffusers_FLUX_Config"
+ },
+ {
+ "$ref": "#/components/schemas/LoRA_Diffusers_ZImage_Config"
+ },
+ {
+ "$ref": "#/components/schemas/ControlLoRA_LyCORIS_FLUX_Config"
+ },
+ {
+ "$ref": "#/components/schemas/T5Encoder_T5Encoder_Config"
+ },
+ {
+ "$ref": "#/components/schemas/T5Encoder_BnBLLMint8_Config"
+ },
+ {
+ "$ref": "#/components/schemas/Qwen3Encoder_Qwen3Encoder_Config"
+ },
+ {
+ "$ref": "#/components/schemas/Qwen3Encoder_Checkpoint_Config"
+ },
+ {
+ "$ref": "#/components/schemas/Qwen3Encoder_GGUF_Config"
+ },
+ {
+ "$ref": "#/components/schemas/QwenVLEncoder_Diffusers_Config"
+ },
+ {
+ "$ref": "#/components/schemas/QwenVLEncoder_Checkpoint_Config"
+ },
+ {
+ "$ref": "#/components/schemas/WanT5Encoder_WanT5Encoder_Config"
+ },
+ {
+ "$ref": "#/components/schemas/TI_File_SD1_Config"
+ },
+ {
+ "$ref": "#/components/schemas/TI_File_SD2_Config"
+ },
+ {
+ "$ref": "#/components/schemas/TI_File_SDXL_Config"
+ },
+ {
+ "$ref": "#/components/schemas/TI_Folder_SD1_Config"
+ },
+ {
+ "$ref": "#/components/schemas/TI_Folder_SD2_Config"
+ },
+ {
+ "$ref": "#/components/schemas/TI_Folder_SDXL_Config"
+ },
+ {
+ "$ref": "#/components/schemas/IPAdapter_InvokeAI_SD1_Config"
+ },
+ {
+ "$ref": "#/components/schemas/IPAdapter_InvokeAI_SD2_Config"
+ },
+ {
+ "$ref": "#/components/schemas/IPAdapter_InvokeAI_SDXL_Config"
+ },
+ {
+ "$ref": "#/components/schemas/IPAdapter_Checkpoint_SD1_Config"
+ },
+ {
+ "$ref": "#/components/schemas/IPAdapter_Checkpoint_SD2_Config"
+ },
+ {
+ "$ref": "#/components/schemas/IPAdapter_Checkpoint_SDXL_Config"
+ },
+ {
+ "$ref": "#/components/schemas/IPAdapter_Checkpoint_FLUX_Config"
+ },
+ {
+ "$ref": "#/components/schemas/T2IAdapter_Diffusers_SD1_Config"
+ },
+ {
+ "$ref": "#/components/schemas/T2IAdapter_Diffusers_SDXL_Config"
+ },
+ {
+ "$ref": "#/components/schemas/Spandrel_Checkpoint_Config"
+ },
+ {
+ "$ref": "#/components/schemas/CLIPEmbed_Diffusers_G_Config"
+ },
+ {
+ "$ref": "#/components/schemas/CLIPEmbed_Diffusers_L_Config"
+ },
+ {
+ "$ref": "#/components/schemas/CLIPVision_Diffusers_Config"
+ },
+ {
+ "$ref": "#/components/schemas/SigLIP_Diffusers_Config"
+ },
+ {
+ "$ref": "#/components/schemas/FLUXRedux_Checkpoint_Config"
+ },
+ {
+ "$ref": "#/components/schemas/LlavaOnevision_Diffusers_Config"
+ },
+ {
+ "$ref": "#/components/schemas/TextLLM_Diffusers_Config"
+ },
+ {
+ "$ref": "#/components/schemas/ExternalApiModelConfig"
+ },
+ {
+ "$ref": "#/components/schemas/Unknown_Config"
+ }
+ ]
},
{
"type": "null"
}
],
- "default": null,
- "description": "Optional metadata to be saved with the image",
- "field_kind": "internal",
- "input": "connection",
- "orig_required": false,
- "ui_hidden": false
- },
- "id": {
- "description": "The id of this instance of an invocation. Must be unique among all instances of invocations.",
- "field_kind": "node_attribute",
- "title": "Id",
- "type": "string"
+ "title": "Config Out",
+ "description": "After successful installation, this will hold the configuration object."
},
- "is_intermediate": {
- "default": false,
- "description": "Whether or not this is an intermediate invocation.",
- "field_kind": "node_attribute",
- "input": "direct",
- "orig_required": true,
- "title": "Is Intermediate",
+ "inplace": {
"type": "boolean",
- "ui_hidden": false,
- "ui_type": "IsIntermediate"
- },
- "use_cache": {
- "default": true,
- "description": "Whether or not to use the cache",
- "field_kind": "node_attribute",
- "title": "Use Cache",
- "type": "boolean"
+ "title": "Inplace",
+ "description": "Leave model in its current location; otherwise install under models directory",
+ "default": false
},
- "tiles_with_images": {
- "anyOf": [
+ "source": {
+ "oneOf": [
{
- "items": {
- "$ref": "#/components/schemas/TileWithImage"
- },
- "type": "array"
+ "$ref": "#/components/schemas/LocalModelSource"
},
{
- "type": "null"
+ "$ref": "#/components/schemas/HFModelSource"
+ },
+ {
+ "$ref": "#/components/schemas/URLModelSource"
+ },
+ {
+ "$ref": "#/components/schemas/ExternalModelSource"
}
],
- "default": null,
- "description": "A list of tile images with tile properties.",
- "field_kind": "input",
- "input": "any",
- "orig_required": true,
- "title": "Tiles With Images"
- },
- "blend_mode": {
- "default": "Seam",
- "description": "blending type Linear or Seam",
- "enum": ["Linear", "Seam"],
- "field_kind": "input",
- "input": "direct",
- "orig_default": "Seam",
- "orig_required": false,
- "title": "Blend Mode",
- "type": "string"
- },
- "blend_amount": {
- "default": 32,
- "description": "The amount to blend adjacent tiles in pixels. Must be <= the amount of overlap between adjacent tiles.",
- "field_kind": "input",
- "input": "any",
- "minimum": 0,
- "orig_default": 32,
- "orig_required": false,
- "title": "Blend Amount",
- "type": "integer"
+ "title": "Source",
+ "description": "Source (URL, repo_id, or local path) of model",
+ "discriminator": {
+ "propertyName": "type",
+ "mapping": {
+ "external": "#/components/schemas/ExternalModelSource",
+ "hf": "#/components/schemas/HFModelSource",
+ "local": "#/components/schemas/LocalModelSource",
+ "url": "#/components/schemas/URLModelSource"
+ }
+ }
},
- "type": {
- "const": "merge_tiles_to_image",
- "default": "merge_tiles_to_image",
- "field_kind": "node_attribute",
- "title": "type",
- "type": "string"
- }
- },
- "required": ["type", "id"],
- "tags": ["tiles"],
- "title": "Merge Tiles to Image",
- "type": "object",
- "version": "1.1.1",
- "output": {
- "$ref": "#/components/schemas/ImageOutput"
- }
- },
- "MetadataField": {
- "additionalProperties": true,
- "type": "object",
- "title": "MetadataField",
- "description": "Pydantic model for metadata with custom root of type dict[str, Any].\nMetadata is stored without a strict schema."
- },
- "MetadataFieldExtractorInvocation": {
- "category": "metadata",
- "class": "invocation",
- "classification": "deprecated",
- "description": "Extracts the text value from an image's metadata given a key.\nRaises an error if the image has no metadata or if the value is not a string (nesting not permitted).",
- "node_pack": "invokeai",
- "properties": {
- "id": {
- "description": "The id of this instance of an invocation. Must be unique among all instances of invocations.",
- "field_kind": "node_attribute",
- "title": "Id",
- "type": "string"
+ "local_path": {
+ "type": "string",
+ "format": "path",
+ "title": "Local Path",
+ "description": "Path to locally-downloaded model; may be the same as the source"
},
- "is_intermediate": {
- "default": false,
- "description": "Whether or not this is an intermediate invocation.",
- "field_kind": "node_attribute",
- "input": "direct",
- "orig_required": true,
- "title": "Is Intermediate",
- "type": "boolean",
- "ui_hidden": false,
- "ui_type": "IsIntermediate"
+ "bytes": {
+ "type": "integer",
+ "title": "Bytes",
+ "description": "For a remote model, the number of bytes downloaded so far (may not be available)",
+ "default": 0
},
- "use_cache": {
- "default": true,
- "description": "Whether or not to use the cache",
- "field_kind": "node_attribute",
- "title": "Use Cache",
- "type": "boolean"
+ "total_bytes": {
+ "type": "integer",
+ "title": "Total Bytes",
+ "description": "Total size of the model to be installed",
+ "default": 0
},
- "image": {
+ "source_metadata": {
"anyOf": [
{
- "$ref": "#/components/schemas/ImageField"
+ "oneOf": [
+ {
+ "$ref": "#/components/schemas/BaseMetadata"
+ },
+ {
+ "$ref": "#/components/schemas/HuggingFaceMetadata"
+ }
+ ],
+ "discriminator": {
+ "propertyName": "type",
+ "mapping": {
+ "basemetadata": "#/components/schemas/BaseMetadata",
+ "huggingface": "#/components/schemas/HuggingFaceMetadata"
+ }
+ }
},
{
"type": "null"
}
],
- "default": null,
- "description": "The image to extract metadata from",
- "field_kind": "input",
- "input": "any",
- "orig_required": true
+ "title": "Source Metadata",
+ "description": "Metadata provided by the model source"
+ },
+ "download_parts": {
+ "items": {
+ "$ref": "#/components/schemas/DownloadJob"
+ },
+ "type": "array",
+ "uniqueItems": true,
+ "title": "Download Parts",
+ "description": "Download jobs contributing to this install"
},
- "key": {
+ "error": {
"anyOf": [
{
"type": "string"
@@ -53167,904 +60211,780 @@
"type": "null"
}
],
- "default": null,
- "description": "The key in the image's metadata to extract the value from",
- "field_kind": "input",
- "input": "any",
- "orig_required": true,
- "title": "Key"
- },
- "type": {
- "const": "metadata_field_extractor",
- "default": "metadata_field_extractor",
- "field_kind": "node_attribute",
- "title": "type",
- "type": "string"
- }
- },
- "required": ["type", "id"],
- "tags": ["metadata"],
- "title": "Metadata Field Extractor",
- "type": "object",
- "version": "1.0.0",
- "output": {
- "$ref": "#/components/schemas/StringOutput"
- }
- },
- "MetadataFromImageInvocation": {
- "category": "metadata",
- "class": "invocation",
- "classification": "beta",
- "description": "Used to create a core metadata item then Add/Update it to the provided metadata",
- "node_pack": "invokeai",
- "properties": {
- "id": {
- "description": "The id of this instance of an invocation. Must be unique among all instances of invocations.",
- "field_kind": "node_attribute",
- "title": "Id",
- "type": "string"
- },
- "is_intermediate": {
- "default": false,
- "description": "Whether or not this is an intermediate invocation.",
- "field_kind": "node_attribute",
- "input": "direct",
- "orig_required": true,
- "title": "Is Intermediate",
- "type": "boolean",
- "ui_hidden": false,
- "ui_type": "IsIntermediate"
- },
- "use_cache": {
- "default": true,
- "description": "Whether or not to use the cache",
- "field_kind": "node_attribute",
- "title": "Use Cache",
- "type": "boolean"
+ "title": "Error",
+ "description": "On an error condition, this field will contain the text of the exception"
},
- "image": {
+ "error_traceback": {
"anyOf": [
{
- "$ref": "#/components/schemas/ImageField"
+ "type": "string"
},
{
"type": "null"
}
],
- "default": null,
- "description": "The image to process",
- "field_kind": "input",
- "input": "any",
- "orig_required": true
- },
- "type": {
- "const": "metadata_from_image",
- "default": "metadata_from_image",
- "field_kind": "node_attribute",
- "title": "type",
- "type": "string"
+ "title": "Error Traceback",
+ "description": "On an error condition, this field will contain the exception traceback"
}
},
- "required": ["type", "id"],
- "tags": ["metadata"],
- "title": "Metadata From Image",
"type": "object",
- "version": "1.0.1",
- "output": {
- "$ref": "#/components/schemas/MetadataOutput"
- }
+ "required": ["id", "source", "local_path"],
+ "title": "ModelInstallJob",
+ "description": "Object that tracks the current status of an install request."
},
- "MetadataInvocation": {
- "category": "metadata",
- "class": "invocation",
- "classification": "stable",
- "description": "Takes a MetadataItem or collection of MetadataItems and outputs a MetadataDict.",
- "node_pack": "invokeai",
+ "ModelInstallStartedEvent": {
+ "description": "Event model for model_install_started",
"properties": {
+ "timestamp": {
+ "description": "The timestamp of the event",
+ "title": "Timestamp",
+ "type": "integer"
+ },
"id": {
- "description": "The id of this instance of an invocation. Must be unique among all instances of invocations.",
- "field_kind": "node_attribute",
+ "description": "The ID of the install job",
"title": "Id",
- "type": "string"
- },
- "is_intermediate": {
- "default": false,
- "description": "Whether or not this is an intermediate invocation.",
- "field_kind": "node_attribute",
- "input": "direct",
- "orig_required": true,
- "title": "Is Intermediate",
- "type": "boolean",
- "ui_hidden": false,
- "ui_type": "IsIntermediate"
- },
- "use_cache": {
- "default": true,
- "description": "Whether or not to use the cache",
- "field_kind": "node_attribute",
- "title": "Use Cache",
- "type": "boolean"
+ "type": "integer"
},
- "items": {
- "anyOf": [
+ "source": {
+ "description": "Source of the model; local path, repo_id or url",
+ "discriminator": {
+ "mapping": {
+ "external": "#/components/schemas/ExternalModelSource",
+ "hf": "#/components/schemas/HFModelSource",
+ "local": "#/components/schemas/LocalModelSource",
+ "url": "#/components/schemas/URLModelSource"
+ },
+ "propertyName": "type"
+ },
+ "oneOf": [
{
- "items": {
- "$ref": "#/components/schemas/MetadataItemField"
- },
- "type": "array"
+ "$ref": "#/components/schemas/LocalModelSource"
},
{
- "$ref": "#/components/schemas/MetadataItemField"
+ "$ref": "#/components/schemas/HFModelSource"
},
{
- "type": "null"
+ "$ref": "#/components/schemas/URLModelSource"
+ },
+ {
+ "$ref": "#/components/schemas/ExternalModelSource"
}
],
- "default": null,
- "description": "A single metadata item or collection of metadata items",
- "field_kind": "input",
- "input": "any",
- "orig_required": true,
- "title": "Items"
- },
- "type": {
- "const": "metadata",
- "default": "metadata",
- "field_kind": "node_attribute",
- "title": "type",
- "type": "string"
- }
- },
- "required": ["type", "id"],
- "tags": ["metadata"],
- "title": "Metadata",
- "type": "object",
- "version": "1.0.1",
- "output": {
- "$ref": "#/components/schemas/MetadataOutput"
- }
- },
- "MetadataItemField": {
- "properties": {
- "label": {
- "description": "Label for this metadata item",
- "title": "Label",
- "type": "string"
- },
- "value": {
- "description": "The value for this metadata item (may be any type)",
- "title": "Value"
+ "title": "Source"
}
},
- "required": ["label", "value"],
- "title": "MetadataItemField",
+ "required": ["timestamp", "id", "source"],
+ "title": "ModelInstallStartedEvent",
"type": "object"
},
- "MetadataItemInvocation": {
- "category": "metadata",
- "class": "invocation",
- "classification": "stable",
- "description": "Used to create an arbitrary metadata item. Provide \"label\" and make a connection to \"value\" to store that data as the value.",
- "node_pack": "invokeai",
+ "ModelLoadCompleteEvent": {
+ "description": "Event model for model_load_complete",
"properties": {
- "id": {
- "description": "The id of this instance of an invocation. Must be unique among all instances of invocations.",
- "field_kind": "node_attribute",
- "title": "Id",
- "type": "string"
- },
- "is_intermediate": {
- "default": false,
- "description": "Whether or not this is an intermediate invocation.",
- "field_kind": "node_attribute",
- "input": "direct",
- "orig_required": true,
- "title": "Is Intermediate",
- "type": "boolean",
- "ui_hidden": false,
- "ui_type": "IsIntermediate"
- },
- "use_cache": {
- "default": true,
- "description": "Whether or not to use the cache",
- "field_kind": "node_attribute",
- "title": "Use Cache",
- "type": "boolean"
+ "timestamp": {
+ "description": "The timestamp of the event",
+ "title": "Timestamp",
+ "type": "integer"
},
- "label": {
- "anyOf": [
+ "config": {
+ "description": "The model's config",
+ "oneOf": [
+ {
+ "$ref": "#/components/schemas/Main_Diffusers_SD1_Config"
+ },
+ {
+ "$ref": "#/components/schemas/Main_Diffusers_SD2_Config"
+ },
+ {
+ "$ref": "#/components/schemas/Main_Diffusers_SDXL_Config"
+ },
+ {
+ "$ref": "#/components/schemas/Main_Diffusers_SDXLRefiner_Config"
+ },
+ {
+ "$ref": "#/components/schemas/Main_Diffusers_SD3_Config"
+ },
+ {
+ "$ref": "#/components/schemas/Main_Diffusers_FLUX_Config"
+ },
+ {
+ "$ref": "#/components/schemas/Main_Diffusers_Flux2_Config"
+ },
+ {
+ "$ref": "#/components/schemas/Main_Diffusers_CogView4_Config"
+ },
+ {
+ "$ref": "#/components/schemas/Main_Diffusers_QwenImage_Config"
+ },
+ {
+ "$ref": "#/components/schemas/Main_Diffusers_Wan_Config"
+ },
+ {
+ "$ref": "#/components/schemas/Main_Diffusers_ZImage_Config"
+ },
+ {
+ "$ref": "#/components/schemas/Main_Checkpoint_SD1_Config"
+ },
+ {
+ "$ref": "#/components/schemas/Main_Checkpoint_SD2_Config"
+ },
+ {
+ "$ref": "#/components/schemas/Main_Checkpoint_SDXL_Config"
+ },
+ {
+ "$ref": "#/components/schemas/Main_Checkpoint_SDXLRefiner_Config"
+ },
+ {
+ "$ref": "#/components/schemas/Main_Checkpoint_Flux2_Config"
+ },
+ {
+ "$ref": "#/components/schemas/Main_Checkpoint_FLUX_Config"
+ },
+ {
+ "$ref": "#/components/schemas/Main_Checkpoint_QwenImage_Config"
+ },
+ {
+ "$ref": "#/components/schemas/Main_Checkpoint_ZImage_Config"
+ },
+ {
+ "$ref": "#/components/schemas/Main_Checkpoint_Anima_Config"
+ },
+ {
+ "$ref": "#/components/schemas/Main_BnBNF4_FLUX_Config"
+ },
+ {
+ "$ref": "#/components/schemas/Main_GGUF_Flux2_Config"
+ },
+ {
+ "$ref": "#/components/schemas/Main_GGUF_FLUX_Config"
+ },
+ {
+ "$ref": "#/components/schemas/Main_GGUF_QwenImage_Config"
+ },
+ {
+ "$ref": "#/components/schemas/Main_GGUF_Wan_Config"
+ },
+ {
+ "$ref": "#/components/schemas/Main_GGUF_ZImage_Config"
+ },
+ {
+ "$ref": "#/components/schemas/VAE_Checkpoint_SD1_Config"
+ },
+ {
+ "$ref": "#/components/schemas/VAE_Checkpoint_SD2_Config"
+ },
+ {
+ "$ref": "#/components/schemas/VAE_Checkpoint_SDXL_Config"
+ },
+ {
+ "$ref": "#/components/schemas/VAE_Checkpoint_FLUX_Config"
+ },
+ {
+ "$ref": "#/components/schemas/VAE_Checkpoint_Flux2_Config"
+ },
+ {
+ "$ref": "#/components/schemas/VAE_Checkpoint_Wan_Config"
+ },
+ {
+ "$ref": "#/components/schemas/VAE_Checkpoint_QwenImage_Config"
+ },
+ {
+ "$ref": "#/components/schemas/VAE_Checkpoint_Anima_Config"
+ },
+ {
+ "$ref": "#/components/schemas/VAE_Diffusers_SD1_Config"
+ },
+ {
+ "$ref": "#/components/schemas/VAE_Diffusers_SDXL_Config"
+ },
+ {
+ "$ref": "#/components/schemas/VAE_Diffusers_Flux2_Config"
+ },
+ {
+ "$ref": "#/components/schemas/VAE_Diffusers_Wan_Config"
+ },
+ {
+ "$ref": "#/components/schemas/ControlNet_Checkpoint_SD1_Config"
+ },
+ {
+ "$ref": "#/components/schemas/ControlNet_Checkpoint_SD2_Config"
+ },
+ {
+ "$ref": "#/components/schemas/ControlNet_Checkpoint_SDXL_Config"
+ },
+ {
+ "$ref": "#/components/schemas/ControlNet_Checkpoint_FLUX_Config"
+ },
+ {
+ "$ref": "#/components/schemas/ControlNet_Checkpoint_ZImage_Config"
+ },
+ {
+ "$ref": "#/components/schemas/ControlNet_Checkpoint_Anima_Config"
+ },
+ {
+ "$ref": "#/components/schemas/ControlNet_Diffusers_SD1_Config"
+ },
+ {
+ "$ref": "#/components/schemas/ControlNet_Diffusers_SD2_Config"
+ },
+ {
+ "$ref": "#/components/schemas/ControlNet_Diffusers_SDXL_Config"
+ },
+ {
+ "$ref": "#/components/schemas/ControlNet_Diffusers_FLUX_Config"
+ },
+ {
+ "$ref": "#/components/schemas/LoRA_LyCORIS_SD1_Config"
+ },
+ {
+ "$ref": "#/components/schemas/LoRA_LyCORIS_SD2_Config"
+ },
+ {
+ "$ref": "#/components/schemas/LoRA_LyCORIS_SDXL_Config"
+ },
+ {
+ "$ref": "#/components/schemas/LoRA_LyCORIS_Flux2_Config"
+ },
+ {
+ "$ref": "#/components/schemas/LoRA_LyCORIS_FLUX_Config"
+ },
+ {
+ "$ref": "#/components/schemas/LoRA_LyCORIS_ZImage_Config"
+ },
+ {
+ "$ref": "#/components/schemas/LoRA_LyCORIS_QwenImage_Config"
+ },
+ {
+ "$ref": "#/components/schemas/LoRA_LyCORIS_Wan_Config"
+ },
+ {
+ "$ref": "#/components/schemas/LoRA_LyCORIS_Anima_Config"
+ },
+ {
+ "$ref": "#/components/schemas/LoRA_OMI_SDXL_Config"
+ },
+ {
+ "$ref": "#/components/schemas/LoRA_OMI_FLUX_Config"
+ },
+ {
+ "$ref": "#/components/schemas/LoRA_Diffusers_SD1_Config"
+ },
+ {
+ "$ref": "#/components/schemas/LoRA_Diffusers_SD2_Config"
+ },
+ {
+ "$ref": "#/components/schemas/LoRA_Diffusers_SDXL_Config"
+ },
+ {
+ "$ref": "#/components/schemas/LoRA_Diffusers_Flux2_Config"
+ },
+ {
+ "$ref": "#/components/schemas/LoRA_Diffusers_FLUX_Config"
+ },
+ {
+ "$ref": "#/components/schemas/LoRA_Diffusers_ZImage_Config"
+ },
+ {
+ "$ref": "#/components/schemas/ControlLoRA_LyCORIS_FLUX_Config"
+ },
+ {
+ "$ref": "#/components/schemas/T5Encoder_T5Encoder_Config"
+ },
+ {
+ "$ref": "#/components/schemas/T5Encoder_BnBLLMint8_Config"
+ },
+ {
+ "$ref": "#/components/schemas/Qwen3Encoder_Qwen3Encoder_Config"
+ },
+ {
+ "$ref": "#/components/schemas/Qwen3Encoder_Checkpoint_Config"
+ },
+ {
+ "$ref": "#/components/schemas/Qwen3Encoder_GGUF_Config"
+ },
+ {
+ "$ref": "#/components/schemas/QwenVLEncoder_Diffusers_Config"
+ },
+ {
+ "$ref": "#/components/schemas/QwenVLEncoder_Checkpoint_Config"
+ },
+ {
+ "$ref": "#/components/schemas/WanT5Encoder_WanT5Encoder_Config"
+ },
+ {
+ "$ref": "#/components/schemas/TI_File_SD1_Config"
+ },
+ {
+ "$ref": "#/components/schemas/TI_File_SD2_Config"
+ },
+ {
+ "$ref": "#/components/schemas/TI_File_SDXL_Config"
+ },
+ {
+ "$ref": "#/components/schemas/TI_Folder_SD1_Config"
+ },
+ {
+ "$ref": "#/components/schemas/TI_Folder_SD2_Config"
+ },
+ {
+ "$ref": "#/components/schemas/TI_Folder_SDXL_Config"
+ },
+ {
+ "$ref": "#/components/schemas/IPAdapter_InvokeAI_SD1_Config"
+ },
+ {
+ "$ref": "#/components/schemas/IPAdapter_InvokeAI_SD2_Config"
+ },
+ {
+ "$ref": "#/components/schemas/IPAdapter_InvokeAI_SDXL_Config"
+ },
+ {
+ "$ref": "#/components/schemas/IPAdapter_Checkpoint_SD1_Config"
+ },
+ {
+ "$ref": "#/components/schemas/IPAdapter_Checkpoint_SD2_Config"
+ },
+ {
+ "$ref": "#/components/schemas/IPAdapter_Checkpoint_SDXL_Config"
+ },
+ {
+ "$ref": "#/components/schemas/IPAdapter_Checkpoint_FLUX_Config"
+ },
+ {
+ "$ref": "#/components/schemas/T2IAdapter_Diffusers_SD1_Config"
+ },
+ {
+ "$ref": "#/components/schemas/T2IAdapter_Diffusers_SDXL_Config"
+ },
+ {
+ "$ref": "#/components/schemas/Spandrel_Checkpoint_Config"
+ },
+ {
+ "$ref": "#/components/schemas/CLIPEmbed_Diffusers_G_Config"
+ },
+ {
+ "$ref": "#/components/schemas/CLIPEmbed_Diffusers_L_Config"
+ },
+ {
+ "$ref": "#/components/schemas/CLIPVision_Diffusers_Config"
+ },
+ {
+ "$ref": "#/components/schemas/SigLIP_Diffusers_Config"
+ },
+ {
+ "$ref": "#/components/schemas/FLUXRedux_Checkpoint_Config"
+ },
+ {
+ "$ref": "#/components/schemas/LlavaOnevision_Diffusers_Config"
+ },
+ {
+ "$ref": "#/components/schemas/TextLLM_Diffusers_Config"
+ },
{
- "type": "string"
+ "$ref": "#/components/schemas/ExternalApiModelConfig"
},
{
- "type": "null"
+ "$ref": "#/components/schemas/Unknown_Config"
}
],
- "default": null,
- "description": "Label for this metadata item",
- "field_kind": "input",
- "input": "any",
- "orig_required": true,
- "title": "Label"
+ "title": "Config"
},
- "value": {
+ "submodel_type": {
"anyOf": [
- {},
+ {
+ "$ref": "#/components/schemas/SubModelType"
+ },
{
"type": "null"
}
],
"default": null,
- "description": "The value for this metadata item (may be any type)",
- "field_kind": "input",
- "input": "any",
- "orig_required": true,
- "title": "Value",
- "ui_type": "AnyField"
+ "description": "The submodel type, if any"
},
- "type": {
- "const": "metadata_item",
- "default": "metadata_item",
- "field_kind": "node_attribute",
- "title": "type",
+ "user_id": {
+ "default": "system",
+ "description": "The ID of the user whose action triggered the load",
+ "title": "User Id",
"type": "string"
}
},
- "required": ["type", "id"],
- "tags": ["metadata"],
- "title": "Metadata Item",
- "type": "object",
- "version": "1.0.1",
- "output": {
- "$ref": "#/components/schemas/MetadataItemOutput"
- }
+ "required": ["timestamp", "config", "submodel_type", "user_id"],
+ "title": "ModelLoadCompleteEvent",
+ "type": "object"
},
- "MetadataItemLinkedInvocation": {
- "category": "metadata",
- "class": "invocation",
- "classification": "beta",
- "description": "Used to Create/Add/Update a value into a metadata label",
- "node_pack": "invokeai",
+ "ModelLoadStartedEvent": {
+ "description": "Event model for model_load_started",
"properties": {
- "metadata": {
- "anyOf": [
+ "timestamp": {
+ "description": "The timestamp of the event",
+ "title": "Timestamp",
+ "type": "integer"
+ },
+ "config": {
+ "description": "The model's config",
+ "oneOf": [
{
- "$ref": "#/components/schemas/MetadataField"
+ "$ref": "#/components/schemas/Main_Diffusers_SD1_Config"
},
{
- "type": "null"
- }
- ],
- "default": null,
- "description": "Optional metadata to be saved with the image",
- "field_kind": "internal",
- "input": "connection",
- "orig_required": false,
- "ui_hidden": false
- },
- "id": {
- "description": "The id of this instance of an invocation. Must be unique among all instances of invocations.",
- "field_kind": "node_attribute",
- "title": "Id",
- "type": "string"
- },
- "is_intermediate": {
- "default": false,
- "description": "Whether or not this is an intermediate invocation.",
- "field_kind": "node_attribute",
- "input": "direct",
- "orig_required": true,
- "title": "Is Intermediate",
- "type": "boolean",
- "ui_hidden": false,
- "ui_type": "IsIntermediate"
- },
- "use_cache": {
- "default": true,
- "description": "Whether or not to use the cache",
- "field_kind": "node_attribute",
- "title": "Use Cache",
- "type": "boolean"
- },
- "label": {
- "default": "* CUSTOM LABEL *",
- "description": "Label for this metadata item",
- "enum": [
- "* CUSTOM LABEL *",
- "positive_prompt",
- "positive_style_prompt",
- "negative_prompt",
- "negative_style_prompt",
- "width",
- "height",
- "seed",
- "cfg_scale",
- "cfg_rescale_multiplier",
- "steps",
- "scheduler",
- "clip_skip",
- "model",
- "vae",
- "seamless_x",
- "seamless_y",
- "guidance",
- "cfg_scale_start_step",
- "cfg_scale_end_step"
- ],
- "field_kind": "input",
- "input": "direct",
- "orig_default": "* CUSTOM LABEL *",
- "orig_required": false,
- "title": "Label",
- "type": "string"
- },
- "custom_label": {
- "anyOf": [
+ "$ref": "#/components/schemas/Main_Diffusers_SD2_Config"
+ },
{
- "type": "string"
+ "$ref": "#/components/schemas/Main_Diffusers_SDXL_Config"
},
{
- "type": "null"
- }
- ],
- "default": null,
- "description": "Label for this metadata item",
- "field_kind": "input",
- "input": "direct",
- "orig_default": null,
- "orig_required": false,
- "title": "Custom Label"
- },
- "value": {
- "anyOf": [
- {},
+ "$ref": "#/components/schemas/Main_Diffusers_SDXLRefiner_Config"
+ },
{
- "type": "null"
- }
- ],
- "default": null,
- "description": "The value for this metadata item (may be any type)",
- "field_kind": "input",
- "input": "any",
- "orig_required": true,
- "title": "Value",
- "ui_type": "AnyField"
- },
- "type": {
- "const": "metadata_item_linked",
- "default": "metadata_item_linked",
- "field_kind": "node_attribute",
- "title": "type",
- "type": "string"
- }
- },
- "required": ["type", "id"],
- "tags": ["metadata"],
- "title": "Metadata Item Linked",
- "type": "object",
- "version": "1.0.1",
- "output": {
- "$ref": "#/components/schemas/MetadataOutput"
- }
- },
- "MetadataItemOutput": {
- "class": "output",
- "description": "Metadata Item Output",
- "properties": {
- "item": {
- "$ref": "#/components/schemas/MetadataItemField",
- "description": "Metadata Item",
- "field_kind": "output",
- "ui_hidden": false
- },
- "type": {
- "const": "metadata_item_output",
- "default": "metadata_item_output",
- "field_kind": "node_attribute",
- "title": "type",
- "type": "string"
- }
- },
- "required": ["output_meta", "item", "type", "type"],
- "title": "MetadataItemOutput",
- "type": "object"
- },
- "MetadataOutput": {
- "class": "output",
- "properties": {
- "metadata": {
- "$ref": "#/components/schemas/MetadataField",
- "description": "Metadata Dict",
- "field_kind": "output",
- "ui_hidden": false
- },
- "type": {
- "const": "metadata_output",
- "default": "metadata_output",
- "field_kind": "node_attribute",
- "title": "type",
- "type": "string"
- }
- },
- "required": ["output_meta", "metadata", "type", "type"],
- "title": "MetadataOutput",
- "type": "object"
- },
- "MetadataToBoolCollectionInvocation": {
- "category": "metadata",
- "class": "invocation",
- "classification": "beta",
- "description": "Extracts a Boolean value Collection of a label from metadata",
- "node_pack": "invokeai",
- "properties": {
- "metadata": {
- "anyOf": [
+ "$ref": "#/components/schemas/Main_Diffusers_SD3_Config"
+ },
{
- "$ref": "#/components/schemas/MetadataField"
+ "$ref": "#/components/schemas/Main_Diffusers_FLUX_Config"
},
{
- "type": "null"
- }
- ],
- "default": null,
- "description": "Optional metadata to be saved with the image",
- "field_kind": "internal",
- "input": "connection",
- "orig_required": false,
- "ui_hidden": false
- },
- "id": {
- "description": "The id of this instance of an invocation. Must be unique among all instances of invocations.",
- "field_kind": "node_attribute",
- "title": "Id",
- "type": "string"
- },
- "is_intermediate": {
- "default": false,
- "description": "Whether or not this is an intermediate invocation.",
- "field_kind": "node_attribute",
- "input": "direct",
- "orig_required": true,
- "title": "Is Intermediate",
- "type": "boolean",
- "ui_hidden": false,
- "ui_type": "IsIntermediate"
- },
- "use_cache": {
- "default": true,
- "description": "Whether or not to use the cache",
- "field_kind": "node_attribute",
- "title": "Use Cache",
- "type": "boolean"
- },
- "label": {
- "default": "* CUSTOM LABEL *",
- "description": "Label for this metadata item",
- "enum": ["* CUSTOM LABEL *", "seamless_x", "seamless_y"],
- "field_kind": "input",
- "input": "direct",
- "orig_default": "* CUSTOM LABEL *",
- "orig_required": false,
- "title": "Label",
- "type": "string"
- },
- "custom_label": {
- "anyOf": [
+ "$ref": "#/components/schemas/Main_Diffusers_Flux2_Config"
+ },
{
- "type": "string"
+ "$ref": "#/components/schemas/Main_Diffusers_CogView4_Config"
},
{
- "type": "null"
- }
- ],
- "default": null,
- "description": "Label for this metadata item",
- "field_kind": "input",
- "input": "direct",
- "orig_default": null,
- "orig_required": false,
- "title": "Custom Label"
- },
- "default_value": {
- "anyOf": [
+ "$ref": "#/components/schemas/Main_Diffusers_QwenImage_Config"
+ },
+ {
+ "$ref": "#/components/schemas/Main_Diffusers_Wan_Config"
+ },
+ {
+ "$ref": "#/components/schemas/Main_Diffusers_ZImage_Config"
+ },
+ {
+ "$ref": "#/components/schemas/Main_Checkpoint_SD1_Config"
+ },
+ {
+ "$ref": "#/components/schemas/Main_Checkpoint_SD2_Config"
+ },
+ {
+ "$ref": "#/components/schemas/Main_Checkpoint_SDXL_Config"
+ },
+ {
+ "$ref": "#/components/schemas/Main_Checkpoint_SDXLRefiner_Config"
+ },
+ {
+ "$ref": "#/components/schemas/Main_Checkpoint_Flux2_Config"
+ },
+ {
+ "$ref": "#/components/schemas/Main_Checkpoint_FLUX_Config"
+ },
+ {
+ "$ref": "#/components/schemas/Main_Checkpoint_QwenImage_Config"
+ },
+ {
+ "$ref": "#/components/schemas/Main_Checkpoint_ZImage_Config"
+ },
+ {
+ "$ref": "#/components/schemas/Main_Checkpoint_Anima_Config"
+ },
+ {
+ "$ref": "#/components/schemas/Main_BnBNF4_FLUX_Config"
+ },
+ {
+ "$ref": "#/components/schemas/Main_GGUF_Flux2_Config"
+ },
+ {
+ "$ref": "#/components/schemas/Main_GGUF_FLUX_Config"
+ },
+ {
+ "$ref": "#/components/schemas/Main_GGUF_QwenImage_Config"
+ },
+ {
+ "$ref": "#/components/schemas/Main_GGUF_Wan_Config"
+ },
+ {
+ "$ref": "#/components/schemas/Main_GGUF_ZImage_Config"
+ },
+ {
+ "$ref": "#/components/schemas/VAE_Checkpoint_SD1_Config"
+ },
+ {
+ "$ref": "#/components/schemas/VAE_Checkpoint_SD2_Config"
+ },
+ {
+ "$ref": "#/components/schemas/VAE_Checkpoint_SDXL_Config"
+ },
+ {
+ "$ref": "#/components/schemas/VAE_Checkpoint_FLUX_Config"
+ },
+ {
+ "$ref": "#/components/schemas/VAE_Checkpoint_Flux2_Config"
+ },
+ {
+ "$ref": "#/components/schemas/VAE_Checkpoint_Wan_Config"
+ },
+ {
+ "$ref": "#/components/schemas/VAE_Checkpoint_QwenImage_Config"
+ },
+ {
+ "$ref": "#/components/schemas/VAE_Checkpoint_Anima_Config"
+ },
+ {
+ "$ref": "#/components/schemas/VAE_Diffusers_SD1_Config"
+ },
+ {
+ "$ref": "#/components/schemas/VAE_Diffusers_SDXL_Config"
+ },
+ {
+ "$ref": "#/components/schemas/VAE_Diffusers_Flux2_Config"
+ },
+ {
+ "$ref": "#/components/schemas/VAE_Diffusers_Wan_Config"
+ },
+ {
+ "$ref": "#/components/schemas/ControlNet_Checkpoint_SD1_Config"
+ },
+ {
+ "$ref": "#/components/schemas/ControlNet_Checkpoint_SD2_Config"
+ },
+ {
+ "$ref": "#/components/schemas/ControlNet_Checkpoint_SDXL_Config"
+ },
+ {
+ "$ref": "#/components/schemas/ControlNet_Checkpoint_FLUX_Config"
+ },
+ {
+ "$ref": "#/components/schemas/ControlNet_Checkpoint_ZImage_Config"
+ },
+ {
+ "$ref": "#/components/schemas/ControlNet_Checkpoint_Anima_Config"
+ },
+ {
+ "$ref": "#/components/schemas/ControlNet_Diffusers_SD1_Config"
+ },
+ {
+ "$ref": "#/components/schemas/ControlNet_Diffusers_SD2_Config"
+ },
+ {
+ "$ref": "#/components/schemas/ControlNet_Diffusers_SDXL_Config"
+ },
+ {
+ "$ref": "#/components/schemas/ControlNet_Diffusers_FLUX_Config"
+ },
+ {
+ "$ref": "#/components/schemas/LoRA_LyCORIS_SD1_Config"
+ },
+ {
+ "$ref": "#/components/schemas/LoRA_LyCORIS_SD2_Config"
+ },
+ {
+ "$ref": "#/components/schemas/LoRA_LyCORIS_SDXL_Config"
+ },
+ {
+ "$ref": "#/components/schemas/LoRA_LyCORIS_Flux2_Config"
+ },
+ {
+ "$ref": "#/components/schemas/LoRA_LyCORIS_FLUX_Config"
+ },
+ {
+ "$ref": "#/components/schemas/LoRA_LyCORIS_ZImage_Config"
+ },
+ {
+ "$ref": "#/components/schemas/LoRA_LyCORIS_QwenImage_Config"
+ },
+ {
+ "$ref": "#/components/schemas/LoRA_LyCORIS_Wan_Config"
+ },
+ {
+ "$ref": "#/components/schemas/LoRA_LyCORIS_Anima_Config"
+ },
+ {
+ "$ref": "#/components/schemas/LoRA_OMI_SDXL_Config"
+ },
+ {
+ "$ref": "#/components/schemas/LoRA_OMI_FLUX_Config"
+ },
+ {
+ "$ref": "#/components/schemas/LoRA_Diffusers_SD1_Config"
+ },
+ {
+ "$ref": "#/components/schemas/LoRA_Diffusers_SD2_Config"
+ },
+ {
+ "$ref": "#/components/schemas/LoRA_Diffusers_SDXL_Config"
+ },
+ {
+ "$ref": "#/components/schemas/LoRA_Diffusers_Flux2_Config"
+ },
+ {
+ "$ref": "#/components/schemas/LoRA_Diffusers_FLUX_Config"
+ },
+ {
+ "$ref": "#/components/schemas/LoRA_Diffusers_ZImage_Config"
+ },
+ {
+ "$ref": "#/components/schemas/ControlLoRA_LyCORIS_FLUX_Config"
+ },
+ {
+ "$ref": "#/components/schemas/T5Encoder_T5Encoder_Config"
+ },
+ {
+ "$ref": "#/components/schemas/T5Encoder_BnBLLMint8_Config"
+ },
+ {
+ "$ref": "#/components/schemas/Qwen3Encoder_Qwen3Encoder_Config"
+ },
+ {
+ "$ref": "#/components/schemas/Qwen3Encoder_Checkpoint_Config"
+ },
+ {
+ "$ref": "#/components/schemas/Qwen3Encoder_GGUF_Config"
+ },
+ {
+ "$ref": "#/components/schemas/QwenVLEncoder_Diffusers_Config"
+ },
+ {
+ "$ref": "#/components/schemas/QwenVLEncoder_Checkpoint_Config"
+ },
+ {
+ "$ref": "#/components/schemas/WanT5Encoder_WanT5Encoder_Config"
+ },
+ {
+ "$ref": "#/components/schemas/TI_File_SD1_Config"
+ },
+ {
+ "$ref": "#/components/schemas/TI_File_SD2_Config"
+ },
+ {
+ "$ref": "#/components/schemas/TI_File_SDXL_Config"
+ },
+ {
+ "$ref": "#/components/schemas/TI_Folder_SD1_Config"
+ },
+ {
+ "$ref": "#/components/schemas/TI_Folder_SD2_Config"
+ },
+ {
+ "$ref": "#/components/schemas/TI_Folder_SDXL_Config"
+ },
+ {
+ "$ref": "#/components/schemas/IPAdapter_InvokeAI_SD1_Config"
+ },
{
- "items": {
- "type": "boolean"
- },
- "type": "array"
+ "$ref": "#/components/schemas/IPAdapter_InvokeAI_SD2_Config"
},
{
- "type": "null"
- }
- ],
- "default": null,
- "description": "The default bool to use if not found in the metadata",
- "field_kind": "input",
- "input": "any",
- "orig_required": true,
- "title": "Default Value"
- },
- "type": {
- "const": "metadata_to_bool_collection",
- "default": "metadata_to_bool_collection",
- "field_kind": "node_attribute",
- "title": "type",
- "type": "string"
- }
- },
- "required": ["type", "id"],
- "tags": ["metadata"],
- "title": "Metadata To Bool Collection",
- "type": "object",
- "version": "1.0.0",
- "output": {
- "$ref": "#/components/schemas/BooleanCollectionOutput"
- }
- },
- "MetadataToBoolInvocation": {
- "category": "metadata",
- "class": "invocation",
- "classification": "beta",
- "description": "Extracts a Boolean value of a label from metadata",
- "node_pack": "invokeai",
- "properties": {
- "metadata": {
- "anyOf": [
+ "$ref": "#/components/schemas/IPAdapter_InvokeAI_SDXL_Config"
+ },
{
- "$ref": "#/components/schemas/MetadataField"
+ "$ref": "#/components/schemas/IPAdapter_Checkpoint_SD1_Config"
},
{
- "type": "null"
- }
- ],
- "default": null,
- "description": "Optional metadata to be saved with the image",
- "field_kind": "internal",
- "input": "connection",
- "orig_required": false,
- "ui_hidden": false
- },
- "id": {
- "description": "The id of this instance of an invocation. Must be unique among all instances of invocations.",
- "field_kind": "node_attribute",
- "title": "Id",
- "type": "string"
- },
- "is_intermediate": {
- "default": false,
- "description": "Whether or not this is an intermediate invocation.",
- "field_kind": "node_attribute",
- "input": "direct",
- "orig_required": true,
- "title": "Is Intermediate",
- "type": "boolean",
- "ui_hidden": false,
- "ui_type": "IsIntermediate"
- },
- "use_cache": {
- "default": true,
- "description": "Whether or not to use the cache",
- "field_kind": "node_attribute",
- "title": "Use Cache",
- "type": "boolean"
- },
- "label": {
- "default": "* CUSTOM LABEL *",
- "description": "Label for this metadata item",
- "enum": ["* CUSTOM LABEL *", "seamless_x", "seamless_y"],
- "field_kind": "input",
- "input": "direct",
- "orig_default": "* CUSTOM LABEL *",
- "orig_required": false,
- "title": "Label",
- "type": "string"
- },
- "custom_label": {
- "anyOf": [
+ "$ref": "#/components/schemas/IPAdapter_Checkpoint_SD2_Config"
+ },
{
- "type": "string"
+ "$ref": "#/components/schemas/IPAdapter_Checkpoint_SDXL_Config"
},
{
- "type": "null"
- }
- ],
- "default": null,
- "description": "Label for this metadata item",
- "field_kind": "input",
- "input": "direct",
- "orig_default": null,
- "orig_required": false,
- "title": "Custom Label"
- },
- "default_value": {
- "anyOf": [
+ "$ref": "#/components/schemas/IPAdapter_Checkpoint_FLUX_Config"
+ },
{
- "type": "boolean"
+ "$ref": "#/components/schemas/T2IAdapter_Diffusers_SD1_Config"
},
{
- "type": "null"
- }
- ],
- "default": null,
- "description": "The default bool to use if not found in the metadata",
- "field_kind": "input",
- "input": "any",
- "orig_required": true,
- "title": "Default Value"
- },
- "type": {
- "const": "metadata_to_bool",
- "default": "metadata_to_bool",
- "field_kind": "node_attribute",
- "title": "type",
- "type": "string"
- }
- },
- "required": ["type", "id"],
- "tags": ["metadata"],
- "title": "Metadata To Bool",
- "type": "object",
- "version": "1.0.0",
- "output": {
- "$ref": "#/components/schemas/BooleanOutput"
- }
- },
- "MetadataToControlnetsInvocation": {
- "category": "metadata",
- "class": "invocation",
- "classification": "beta",
- "description": "Extracts a Controlnets value of a label from metadata",
- "node_pack": "invokeai",
- "properties": {
- "metadata": {
- "anyOf": [
+ "$ref": "#/components/schemas/T2IAdapter_Diffusers_SDXL_Config"
+ },
{
- "$ref": "#/components/schemas/MetadataField"
+ "$ref": "#/components/schemas/Spandrel_Checkpoint_Config"
},
{
- "type": "null"
- }
- ],
- "default": null,
- "description": "Optional metadata to be saved with the image",
- "field_kind": "internal",
- "input": "connection",
- "orig_required": false,
- "ui_hidden": false
- },
- "id": {
- "description": "The id of this instance of an invocation. Must be unique among all instances of invocations.",
- "field_kind": "node_attribute",
- "title": "Id",
- "type": "string"
- },
- "is_intermediate": {
- "default": false,
- "description": "Whether or not this is an intermediate invocation.",
- "field_kind": "node_attribute",
- "input": "direct",
- "orig_required": true,
- "title": "Is Intermediate",
- "type": "boolean",
- "ui_hidden": false,
- "ui_type": "IsIntermediate"
- },
- "use_cache": {
- "default": true,
- "description": "Whether or not to use the cache",
- "field_kind": "node_attribute",
- "title": "Use Cache",
- "type": "boolean"
- },
- "control_list": {
- "anyOf": [
+ "$ref": "#/components/schemas/CLIPEmbed_Diffusers_G_Config"
+ },
{
- "$ref": "#/components/schemas/ControlField"
+ "$ref": "#/components/schemas/CLIPEmbed_Diffusers_L_Config"
},
{
- "items": {
- "$ref": "#/components/schemas/ControlField"
- },
- "type": "array"
+ "$ref": "#/components/schemas/CLIPVision_Diffusers_Config"
},
{
- "type": "null"
- }
- ],
- "default": null,
- "field_kind": "input",
- "input": "connection",
- "orig_default": null,
- "orig_required": false,
- "title": "ControlNet-List"
- },
- "type": {
- "const": "metadata_to_controlnets",
- "default": "metadata_to_controlnets",
- "field_kind": "node_attribute",
- "title": "type",
- "type": "string"
- }
- },
- "required": ["type", "id"],
- "tags": ["metadata"],
- "title": "Metadata To ControlNets",
- "type": "object",
- "version": "1.2.0",
- "output": {
- "$ref": "#/components/schemas/MDControlListOutput"
- }
- },
- "MetadataToFloatCollectionInvocation": {
- "category": "metadata",
- "class": "invocation",
- "classification": "beta",
- "description": "Extracts a Float value Collection of a label from metadata",
- "node_pack": "invokeai",
- "properties": {
- "metadata": {
- "anyOf": [
+ "$ref": "#/components/schemas/SigLIP_Diffusers_Config"
+ },
{
- "$ref": "#/components/schemas/MetadataField"
+ "$ref": "#/components/schemas/FLUXRedux_Checkpoint_Config"
},
{
- "type": "null"
- }
- ],
- "default": null,
- "description": "Optional metadata to be saved with the image",
- "field_kind": "internal",
- "input": "connection",
- "orig_required": false,
- "ui_hidden": false
- },
- "id": {
- "description": "The id of this instance of an invocation. Must be unique among all instances of invocations.",
- "field_kind": "node_attribute",
- "title": "Id",
- "type": "string"
- },
- "is_intermediate": {
- "default": false,
- "description": "Whether or not this is an intermediate invocation.",
- "field_kind": "node_attribute",
- "input": "direct",
- "orig_required": true,
- "title": "Is Intermediate",
- "type": "boolean",
- "ui_hidden": false,
- "ui_type": "IsIntermediate"
- },
- "use_cache": {
- "default": true,
- "description": "Whether or not to use the cache",
- "field_kind": "node_attribute",
- "title": "Use Cache",
- "type": "boolean"
- },
- "label": {
- "default": "* CUSTOM LABEL *",
- "description": "Label for this metadata item",
- "enum": ["* CUSTOM LABEL *", "cfg_scale", "cfg_rescale_multiplier", "guidance"],
- "field_kind": "input",
- "input": "direct",
- "orig_default": "* CUSTOM LABEL *",
- "orig_required": false,
- "title": "Label",
- "type": "string"
- },
- "custom_label": {
- "anyOf": [
+ "$ref": "#/components/schemas/LlavaOnevision_Diffusers_Config"
+ },
{
- "type": "string"
+ "$ref": "#/components/schemas/TextLLM_Diffusers_Config"
},
{
- "type": "null"
+ "$ref": "#/components/schemas/ExternalApiModelConfig"
+ },
+ {
+ "$ref": "#/components/schemas/Unknown_Config"
}
],
- "default": null,
- "description": "Label for this metadata item",
- "field_kind": "input",
- "input": "direct",
- "orig_default": null,
- "orig_required": false,
- "title": "Custom Label"
+ "title": "Config"
},
- "default_value": {
+ "submodel_type": {
"anyOf": [
{
- "items": {
- "type": "number"
- },
- "type": "array"
+ "$ref": "#/components/schemas/SubModelType"
},
{
"type": "null"
}
],
"default": null,
- "description": "The default float to use if not found in the metadata",
- "field_kind": "input",
- "input": "any",
- "orig_required": true,
- "title": "Default Value"
+ "description": "The submodel type, if any"
},
- "type": {
- "const": "metadata_to_float_collection",
- "default": "metadata_to_float_collection",
- "field_kind": "node_attribute",
- "title": "type",
+ "user_id": {
+ "default": "system",
+ "description": "The ID of the user whose action triggered the load",
+ "title": "User Id",
"type": "string"
}
},
- "required": ["type", "id"],
- "tags": ["metadata"],
- "title": "Metadata To Float Collection",
- "type": "object",
- "version": "1.0.0",
- "output": {
- "$ref": "#/components/schemas/FloatCollectionOutput"
- }
+ "required": ["timestamp", "config", "submodel_type", "user_id"],
+ "title": "ModelLoadStartedEvent",
+ "type": "object"
},
- "MetadataToFloatInvocation": {
- "category": "metadata",
- "class": "invocation",
- "classification": "beta",
- "description": "Extracts a Float value of a label from metadata",
- "node_pack": "invokeai",
+ "ModelLoaderOutput": {
+ "class": "output",
+ "description": "Model loader output",
"properties": {
- "metadata": {
- "anyOf": [
- {
- "$ref": "#/components/schemas/MetadataField"
- },
- {
- "type": "null"
- }
- ],
- "default": null,
- "description": "Optional metadata to be saved with the image",
- "field_kind": "internal",
- "input": "connection",
- "orig_required": false,
+ "vae": {
+ "$ref": "#/components/schemas/VAEField",
+ "description": "VAE",
+ "field_kind": "output",
+ "title": "VAE",
"ui_hidden": false
},
- "id": {
- "description": "The id of this instance of an invocation. Must be unique among all instances of invocations.",
+ "type": {
+ "const": "model_loader_output",
+ "default": "model_loader_output",
"field_kind": "node_attribute",
- "title": "Id",
+ "title": "type",
"type": "string"
},
- "is_intermediate": {
- "default": false,
- "description": "Whether or not this is an intermediate invocation.",
- "field_kind": "node_attribute",
- "input": "direct",
- "orig_required": true,
- "title": "Is Intermediate",
- "type": "boolean",
- "ui_hidden": false,
- "ui_type": "IsIntermediate"
- },
- "use_cache": {
- "default": true,
- "description": "Whether or not to use the cache",
- "field_kind": "node_attribute",
- "title": "Use Cache",
- "type": "boolean"
- },
- "label": {
- "default": "* CUSTOM LABEL *",
- "description": "Label for this metadata item",
- "enum": ["* CUSTOM LABEL *", "cfg_scale", "cfg_rescale_multiplier", "guidance"],
- "field_kind": "input",
- "input": "direct",
- "orig_default": "* CUSTOM LABEL *",
- "orig_required": false,
- "title": "Label",
- "type": "string"
+ "clip": {
+ "$ref": "#/components/schemas/CLIPField",
+ "description": "CLIP (tokenizer, text encoder, LoRAs) and skipped layer count",
+ "field_kind": "output",
+ "title": "CLIP",
+ "ui_hidden": false
},
- "custom_label": {
+ "unet": {
+ "$ref": "#/components/schemas/UNetField",
+ "description": "UNet (scheduler, LoRAs)",
+ "field_kind": "output",
+ "title": "UNet",
+ "ui_hidden": false
+ }
+ },
+ "required": ["output_meta", "vae", "type", "clip", "unet", "type"],
+ "title": "ModelLoaderOutput",
+ "type": "object"
+ },
+ "ModelRecordChanges": {
+ "properties": {
+ "source": {
"anyOf": [
{
"type": "string"
@@ -54073,202 +60993,69 @@
"type": "null"
}
],
- "default": null,
- "description": "Label for this metadata item",
- "field_kind": "input",
- "input": "direct",
- "orig_default": null,
- "orig_required": false,
- "title": "Custom Label"
+ "title": "Source",
+ "description": "original source of the model"
},
- "default_value": {
+ "source_type": {
"anyOf": [
{
- "type": "number"
+ "$ref": "#/components/schemas/ModelSourceType"
},
{
"type": "null"
}
],
- "default": null,
- "description": "The default float to use if not found in the metadata",
- "field_kind": "input",
- "input": "any",
- "orig_required": true,
- "title": "Default Value"
+ "description": "type of model source"
},
- "type": {
- "const": "metadata_to_float",
- "default": "metadata_to_float",
- "field_kind": "node_attribute",
- "title": "type",
- "type": "string"
- }
- },
- "required": ["type", "id"],
- "tags": ["metadata"],
- "title": "Metadata To Float",
- "type": "object",
- "version": "1.1.0",
- "output": {
- "$ref": "#/components/schemas/FloatOutput"
- }
- },
- "MetadataToIPAdaptersInvocation": {
- "category": "metadata",
- "class": "invocation",
- "classification": "beta",
- "description": "Extracts a IP-Adapters value of a label from metadata",
- "node_pack": "invokeai",
- "properties": {
- "metadata": {
+ "source_api_response": {
"anyOf": [
{
- "$ref": "#/components/schemas/MetadataField"
+ "type": "string"
},
{
"type": "null"
}
],
- "default": null,
- "description": "Optional metadata to be saved with the image",
- "field_kind": "internal",
- "input": "connection",
- "orig_required": false,
- "ui_hidden": false
- },
- "id": {
- "description": "The id of this instance of an invocation. Must be unique among all instances of invocations.",
- "field_kind": "node_attribute",
- "title": "Id",
- "type": "string"
- },
- "is_intermediate": {
- "default": false,
- "description": "Whether or not this is an intermediate invocation.",
- "field_kind": "node_attribute",
- "input": "direct",
- "orig_required": true,
- "title": "Is Intermediate",
- "type": "boolean",
- "ui_hidden": false,
- "ui_type": "IsIntermediate"
- },
- "use_cache": {
- "default": true,
- "description": "Whether or not to use the cache",
- "field_kind": "node_attribute",
- "title": "Use Cache",
- "type": "boolean"
+ "title": "Source Api Response",
+ "description": "metadata from remote source"
},
- "ip_adapter_list": {
+ "source_url": {
"anyOf": [
{
- "$ref": "#/components/schemas/IPAdapterField"
- },
- {
- "items": {
- "$ref": "#/components/schemas/IPAdapterField"
- },
- "type": "array"
+ "type": "string"
},
{
"type": "null"
}
],
- "default": null,
- "description": "IP-Adapter to apply",
- "field_kind": "input",
- "input": "connection",
- "orig_default": null,
- "orig_required": false,
- "title": "IP-Adapter-List"
+ "title": "Source Url",
+ "description": "Optional URL for the model (e.g. download page)"
},
- "type": {
- "const": "metadata_to_ip_adapters",
- "default": "metadata_to_ip_adapters",
- "field_kind": "node_attribute",
- "title": "type",
- "type": "string"
- }
- },
- "required": ["type", "id"],
- "tags": ["metadata"],
- "title": "Metadata To IP-Adapters",
- "type": "object",
- "version": "1.2.0",
- "output": {
- "$ref": "#/components/schemas/MDIPAdapterListOutput"
- }
- },
- "MetadataToIntegerCollectionInvocation": {
- "category": "metadata",
- "class": "invocation",
- "classification": "beta",
- "description": "Extracts an integer value Collection of a label from metadata",
- "node_pack": "invokeai",
- "properties": {
- "metadata": {
+ "name": {
"anyOf": [
{
- "$ref": "#/components/schemas/MetadataField"
+ "type": "string"
},
{
"type": "null"
}
],
- "default": null,
- "description": "Optional metadata to be saved with the image",
- "field_kind": "internal",
- "input": "connection",
- "orig_required": false,
- "ui_hidden": false
- },
- "id": {
- "description": "The id of this instance of an invocation. Must be unique among all instances of invocations.",
- "field_kind": "node_attribute",
- "title": "Id",
- "type": "string"
- },
- "is_intermediate": {
- "default": false,
- "description": "Whether or not this is an intermediate invocation.",
- "field_kind": "node_attribute",
- "input": "direct",
- "orig_required": true,
- "title": "Is Intermediate",
- "type": "boolean",
- "ui_hidden": false,
- "ui_type": "IsIntermediate"
- },
- "use_cache": {
- "default": true,
- "description": "Whether or not to use the cache",
- "field_kind": "node_attribute",
- "title": "Use Cache",
- "type": "boolean"
+ "title": "Name",
+ "description": "Name of the model."
},
- "label": {
- "default": "* CUSTOM LABEL *",
- "description": "Label for this metadata item",
- "enum": [
- "* CUSTOM LABEL *",
- "width",
- "height",
- "seed",
- "steps",
- "clip_skip",
- "cfg_scale_start_step",
- "cfg_scale_end_step"
+ "path": {
+ "anyOf": [
+ {
+ "type": "string"
+ },
+ {
+ "type": "null"
+ }
],
- "field_kind": "input",
- "input": "direct",
- "orig_default": "* CUSTOM LABEL *",
- "orig_required": false,
- "title": "Label",
- "type": "string"
+ "title": "Path",
+ "description": "Path to the model."
},
- "custom_label": {
+ "description": {
"anyOf": [
{
"type": "string"
@@ -54277,118 +61064,44 @@
"type": "null"
}
],
- "default": null,
- "description": "Label for this metadata item",
- "field_kind": "input",
- "input": "direct",
- "orig_default": null,
- "orig_required": false,
- "title": "Custom Label"
+ "title": "Description",
+ "description": "Model description"
},
- "default_value": {
+ "base": {
"anyOf": [
{
- "items": {
- "type": "integer"
- },
- "type": "array"
+ "$ref": "#/components/schemas/BaseModelType"
},
{
"type": "null"
}
],
- "default": null,
- "description": "The default integer to use if not found in the metadata",
- "field_kind": "input",
- "input": "any",
- "orig_required": true,
- "title": "Default Value"
+ "description": "The base model."
},
"type": {
- "const": "metadata_to_integer_collection",
- "default": "metadata_to_integer_collection",
- "field_kind": "node_attribute",
- "title": "type",
- "type": "string"
- }
- },
- "required": ["type", "id"],
- "tags": ["metadata"],
- "title": "Metadata To Integer Collection",
- "type": "object",
- "version": "1.0.0",
- "output": {
- "$ref": "#/components/schemas/IntegerCollectionOutput"
- }
- },
- "MetadataToIntegerInvocation": {
- "category": "metadata",
- "class": "invocation",
- "classification": "beta",
- "description": "Extracts an integer value of a label from metadata",
- "node_pack": "invokeai",
- "properties": {
- "metadata": {
"anyOf": [
{
- "$ref": "#/components/schemas/MetadataField"
+ "$ref": "#/components/schemas/ModelType"
},
{
"type": "null"
}
],
- "default": null,
- "description": "Optional metadata to be saved with the image",
- "field_kind": "internal",
- "input": "connection",
- "orig_required": false,
- "ui_hidden": false
- },
- "id": {
- "description": "The id of this instance of an invocation. Must be unique among all instances of invocations.",
- "field_kind": "node_attribute",
- "title": "Id",
- "type": "string"
- },
- "is_intermediate": {
- "default": false,
- "description": "Whether or not this is an intermediate invocation.",
- "field_kind": "node_attribute",
- "input": "direct",
- "orig_required": true,
- "title": "Is Intermediate",
- "type": "boolean",
- "ui_hidden": false,
- "ui_type": "IsIntermediate"
- },
- "use_cache": {
- "default": true,
- "description": "Whether or not to use the cache",
- "field_kind": "node_attribute",
- "title": "Use Cache",
- "type": "boolean"
+ "description": "Type of model"
},
- "label": {
- "default": "* CUSTOM LABEL *",
- "description": "Label for this metadata item",
- "enum": [
- "* CUSTOM LABEL *",
- "width",
- "height",
- "seed",
- "steps",
- "clip_skip",
- "cfg_scale_start_step",
- "cfg_scale_end_step"
+ "key": {
+ "anyOf": [
+ {
+ "type": "string"
+ },
+ {
+ "type": "null"
+ }
],
- "field_kind": "input",
- "input": "direct",
- "orig_default": "* CUSTOM LABEL *",
- "orig_required": false,
- "title": "Label",
- "type": "string"
+ "title": "Key",
+ "description": "Database ID for this model"
},
- "custom_label": {
+ "hash": {
"anyOf": [
{
"type": "string"
@@ -54397,15 +61110,10 @@
"type": "null"
}
],
- "default": null,
- "description": "Label for this metadata item",
- "field_kind": "input",
- "input": "direct",
- "orig_default": null,
- "orig_required": false,
- "title": "Custom Label"
+ "title": "Hash",
+ "description": "hash of model file"
},
- "default_value": {
+ "file_size": {
"anyOf": [
{
"type": "integer"
@@ -54414,433 +61122,611 @@
"type": "null"
}
],
- "default": null,
- "description": "The default integer to use if not found in the metadata",
- "field_kind": "input",
- "input": "any",
- "orig_required": true,
- "title": "Default Value"
+ "title": "File Size",
+ "description": "Size of model file"
},
- "type": {
- "const": "metadata_to_integer",
- "default": "metadata_to_integer",
- "field_kind": "node_attribute",
- "title": "type",
- "type": "string"
- }
- },
- "required": ["type", "id"],
- "tags": ["metadata"],
- "title": "Metadata To Integer",
- "type": "object",
- "version": "1.0.0",
- "output": {
- "$ref": "#/components/schemas/IntegerOutput"
- }
- },
- "MetadataToLorasCollectionInvocation": {
- "category": "metadata",
- "class": "invocation",
- "classification": "beta",
- "description": "Extracts Lora(s) from metadata into a collection",
- "node_pack": "invokeai",
- "properties": {
- "metadata": {
+ "format": {
"anyOf": [
{
- "$ref": "#/components/schemas/MetadataField"
+ "type": "string"
},
{
"type": "null"
}
],
- "default": null,
- "description": "Optional metadata to be saved with the image",
- "field_kind": "internal",
- "input": "connection",
- "orig_required": false,
- "ui_hidden": false
- },
- "id": {
- "description": "The id of this instance of an invocation. Must be unique among all instances of invocations.",
- "field_kind": "node_attribute",
- "title": "Id",
- "type": "string"
- },
- "is_intermediate": {
- "default": false,
- "description": "Whether or not this is an intermediate invocation.",
- "field_kind": "node_attribute",
- "input": "direct",
- "orig_required": true,
- "title": "Is Intermediate",
- "type": "boolean",
- "ui_hidden": false,
- "ui_type": "IsIntermediate"
- },
- "use_cache": {
- "default": true,
- "description": "Whether or not to use the cache",
- "field_kind": "node_attribute",
- "title": "Use Cache",
- "type": "boolean"
- },
- "custom_label": {
- "default": "loras",
- "description": "Label for this metadata item",
- "field_kind": "input",
- "input": "direct",
- "orig_default": "loras",
- "orig_required": false,
- "title": "Custom Label",
- "type": "string"
+ "title": "Format",
+ "description": "format of model file"
},
- "loras": {
+ "trigger_phrases": {
"anyOf": [
- {
- "$ref": "#/components/schemas/LoRAField"
- },
{
"items": {
- "$ref": "#/components/schemas/LoRAField"
+ "type": "string"
},
- "type": "array"
+ "type": "array",
+ "uniqueItems": true
},
{
"type": "null"
}
],
- "default": [],
- "description": "LoRA models and weights. May be a single LoRA or collection.",
- "field_kind": "input",
- "input": "any",
- "orig_default": [],
- "orig_required": false,
- "title": "LoRAs"
- },
- "type": {
- "const": "metadata_to_lora_collection",
- "default": "metadata_to_lora_collection",
- "field_kind": "node_attribute",
- "title": "type",
- "type": "string"
- }
- },
- "required": ["type", "id"],
- "tags": ["metadata"],
- "title": "Metadata To LoRA Collection",
- "type": "object",
- "version": "1.1.0",
- "output": {
- "$ref": "#/components/schemas/MetadataToLorasCollectionOutput"
- }
- },
- "MetadataToLorasCollectionOutput": {
- "class": "output",
- "description": "Model loader output",
- "properties": {
- "lora": {
- "description": "Collection of LoRA model and weights",
- "field_kind": "output",
- "items": {
- "$ref": "#/components/schemas/LoRAField"
- },
- "title": "LoRAs",
- "type": "array",
- "ui_hidden": false
+ "title": "Trigger Phrases",
+ "description": "Set of trigger phrases for this model"
},
- "type": {
- "const": "metadata_to_lora_collection_output",
- "default": "metadata_to_lora_collection_output",
- "field_kind": "node_attribute",
- "title": "type",
- "type": "string"
- }
- },
- "required": ["output_meta", "lora", "type", "type"],
- "title": "MetadataToLorasCollectionOutput",
- "type": "object"
- },
- "MetadataToLorasInvocation": {
- "category": "metadata",
- "class": "invocation",
- "classification": "beta",
- "description": "Extracts a Loras value of a label from metadata",
- "node_pack": "invokeai",
- "properties": {
- "metadata": {
+ "default_settings": {
"anyOf": [
{
- "$ref": "#/components/schemas/MetadataField"
+ "$ref": "#/components/schemas/MainModelDefaultSettings"
+ },
+ {
+ "$ref": "#/components/schemas/LoraModelDefaultSettings"
+ },
+ {
+ "$ref": "#/components/schemas/ControlAdapterDefaultSettings"
+ },
+ {
+ "$ref": "#/components/schemas/ExternalApiModelDefaultSettings"
},
{
"type": "null"
}
],
- "default": null,
- "description": "Optional metadata to be saved with the image",
- "field_kind": "internal",
- "input": "connection",
- "orig_required": false,
- "ui_hidden": false
- },
- "id": {
- "description": "The id of this instance of an invocation. Must be unique among all instances of invocations.",
- "field_kind": "node_attribute",
- "title": "Id",
- "type": "string"
+ "title": "Default Settings",
+ "description": "Default settings for this model"
},
- "is_intermediate": {
- "default": false,
- "description": "Whether or not this is an intermediate invocation.",
- "field_kind": "node_attribute",
- "input": "direct",
- "orig_required": true,
- "title": "Is Intermediate",
- "type": "boolean",
- "ui_hidden": false,
- "ui_type": "IsIntermediate"
+ "provider_id": {
+ "anyOf": [
+ {
+ "type": "string"
+ },
+ {
+ "type": "null"
+ }
+ ],
+ "title": "Provider Id",
+ "description": "External provider identifier"
},
- "use_cache": {
- "default": true,
- "description": "Whether or not to use the cache",
- "field_kind": "node_attribute",
- "title": "Use Cache",
- "type": "boolean"
+ "provider_model_id": {
+ "anyOf": [
+ {
+ "type": "string"
+ },
+ {
+ "type": "null"
+ }
+ ],
+ "title": "Provider Model Id",
+ "description": "External provider model identifier"
},
- "unet": {
+ "capabilities": {
"anyOf": [
{
- "$ref": "#/components/schemas/UNetField"
+ "$ref": "#/components/schemas/ExternalModelCapabilities"
},
{
"type": "null"
}
],
- "default": null,
- "description": "UNet (scheduler, LoRAs)",
- "field_kind": "input",
- "input": "connection",
- "orig_default": null,
- "orig_required": false,
- "title": "UNet"
+ "description": "External model capabilities"
},
- "clip": {
+ "cpu_only": {
"anyOf": [
{
- "$ref": "#/components/schemas/CLIPField"
+ "type": "boolean"
},
{
"type": "null"
}
],
- "default": null,
- "description": "CLIP (tokenizer, text encoder, LoRAs) and skipped layer count",
- "field_kind": "input",
- "input": "connection",
- "orig_default": null,
- "orig_required": false,
- "title": "CLIP"
+ "title": "Cpu Only",
+ "description": "Whether this model should run on CPU only"
},
- "type": {
- "const": "metadata_to_loras",
- "default": "metadata_to_loras",
- "field_kind": "node_attribute",
- "title": "type",
- "type": "string"
- }
- },
- "required": ["type", "id"],
- "tags": ["metadata"],
- "title": "Metadata To LoRAs",
- "type": "object",
- "version": "1.1.1",
- "output": {
- "$ref": "#/components/schemas/LoRALoaderOutput"
- }
- },
- "MetadataToModelInvocation": {
- "category": "metadata",
- "class": "invocation",
- "classification": "beta",
- "description": "Extracts a Model value of a label from metadata",
- "node_pack": "invokeai",
- "properties": {
- "metadata": {
+ "variant": {
+ "anyOf": [
+ {
+ "$ref": "#/components/schemas/ModelVariantType"
+ },
+ {
+ "$ref": "#/components/schemas/ClipVariantType"
+ },
+ {
+ "$ref": "#/components/schemas/FluxVariantType"
+ },
+ {
+ "$ref": "#/components/schemas/Flux2VariantType"
+ },
+ {
+ "$ref": "#/components/schemas/ZImageVariantType"
+ },
+ {
+ "$ref": "#/components/schemas/QwenImageVariantType"
+ },
+ {
+ "$ref": "#/components/schemas/WanVariantType"
+ },
+ {
+ "$ref": "#/components/schemas/WanLoRAVariantType"
+ },
+ {
+ "$ref": "#/components/schemas/Qwen3VariantType"
+ },
+ {
+ "type": "null"
+ }
+ ],
+ "title": "Variant",
+ "description": "The variant of the model."
+ },
+ "prediction_type": {
"anyOf": [
{
- "$ref": "#/components/schemas/MetadataField"
+ "$ref": "#/components/schemas/SchedulerPredictionType"
},
{
"type": "null"
}
],
- "default": null,
- "description": "Optional metadata to be saved with the image",
- "field_kind": "internal",
- "input": "connection",
- "orig_required": false,
- "ui_hidden": false
- },
- "id": {
- "description": "The id of this instance of an invocation. Must be unique among all instances of invocations.",
- "field_kind": "node_attribute",
- "title": "Id",
- "type": "string"
- },
- "is_intermediate": {
- "default": false,
- "description": "Whether or not this is an intermediate invocation.",
- "field_kind": "node_attribute",
- "input": "direct",
- "orig_required": true,
- "title": "Is Intermediate",
- "type": "boolean",
- "ui_hidden": false,
- "ui_type": "IsIntermediate"
- },
- "use_cache": {
- "default": true,
- "description": "Whether or not to use the cache",
- "field_kind": "node_attribute",
- "title": "Use Cache",
- "type": "boolean"
- },
- "label": {
- "default": "model",
- "description": "Label for this metadata item",
- "enum": ["* CUSTOM LABEL *", "model"],
- "field_kind": "input",
- "input": "direct",
- "orig_default": "model",
- "orig_required": false,
- "title": "Label",
- "type": "string"
+ "description": "The prediction type of the model."
},
- "custom_label": {
+ "upcast_attention": {
"anyOf": [
{
- "type": "string"
+ "type": "boolean"
},
{
"type": "null"
}
],
- "default": null,
- "description": "Label for this metadata item",
- "field_kind": "input",
- "input": "direct",
- "orig_default": null,
- "orig_required": false,
- "title": "Custom Label"
+ "title": "Upcast Attention",
+ "description": "Whether to upcast attention."
},
- "default_value": {
+ "config_path": {
"anyOf": [
{
- "$ref": "#/components/schemas/ModelIdentifierField"
+ "type": "string"
},
{
"type": "null"
}
],
- "default": null,
- "description": "The default model to use if not found in the metadata",
- "field_kind": "input",
- "input": "any",
- "orig_required": true,
- "ui_model_type": ["main"]
- },
- "type": {
- "const": "metadata_to_model",
- "default": "metadata_to_model",
- "field_kind": "node_attribute",
- "title": "type",
- "type": "string"
+ "title": "Config Path",
+ "description": "Path to config file for model"
}
},
- "required": ["type", "id"],
- "tags": ["metadata"],
- "title": "Metadata To Model",
"type": "object",
- "version": "1.3.0",
- "output": {
- "$ref": "#/components/schemas/MetadataToModelOutput"
- }
+ "title": "ModelRecordChanges",
+ "description": "A set of changes to apply to a model."
},
- "MetadataToModelOutput": {
- "class": "output",
- "description": "String to main model output",
+ "ModelRecordOrderBy": {
+ "type": "string",
+ "enum": ["default", "type", "base", "name", "format", "size", "created_at", "updated_at", "path"],
+ "title": "ModelRecordOrderBy",
+ "description": "The order in which to return model summaries."
+ },
+ "ModelRelationshipBatchRequest": {
"properties": {
- "model": {
- "$ref": "#/components/schemas/ModelIdentifierField",
- "description": "Main model (UNet, VAE, CLIP) to load",
- "field_kind": "output",
- "title": "Model",
- "ui_hidden": false
- },
- "name": {
- "description": "Model Name",
- "field_kind": "output",
- "title": "Name",
+ "model_keys": {
+ "items": {
+ "type": "string"
+ },
+ "type": "array",
+ "title": "Model Keys",
+ "description": "List of model keys to fetch related models for",
+ "examples": [
+ ["aa3b247f-90c9-4416-bfcd-aeaa57a5339e", "ac32b914-10ab-496e-a24a-3068724b9c35"],
+ [
+ "b1c2d3e4-f5a6-7890-abcd-ef1234567890",
+ "12345678-90ab-cdef-1234-567890abcdef",
+ "fedcba98-7654-3210-fedc-ba9876543210"
+ ],
+ ["3bb7c0eb-b6c8-469c-ad8c-4d69c06075e4"]
+ ]
+ }
+ },
+ "type": "object",
+ "required": ["model_keys"],
+ "title": "ModelRelationshipBatchRequest"
+ },
+ "ModelRelationshipCreateRequest": {
+ "properties": {
+ "model_key_1": {
"type": "string",
- "ui_hidden": false
- },
- "unet": {
- "$ref": "#/components/schemas/UNetField",
- "description": "UNet (scheduler, LoRAs)",
- "field_kind": "output",
- "title": "UNet",
- "ui_hidden": false
- },
- "vae": {
- "$ref": "#/components/schemas/VAEField",
- "description": "VAE",
- "field_kind": "output",
- "title": "VAE",
- "ui_hidden": false
- },
- "clip": {
- "$ref": "#/components/schemas/CLIPField",
- "description": "CLIP (tokenizer, text encoder, LoRAs) and skipped layer count",
- "field_kind": "output",
- "title": "CLIP",
- "ui_hidden": false
+ "title": "Model Key 1",
+ "description": "The key of the first model in the relationship",
+ "examples": [
+ "aa3b247f-90c9-4416-bfcd-aeaa57a5339e",
+ "ac32b914-10ab-496e-a24a-3068724b9c35",
+ "d944abfd-c7c3-42e2-a4ff-da640b29b8b4",
+ "b1c2d3e4-f5a6-7890-abcd-ef1234567890",
+ "12345678-90ab-cdef-1234-567890abcdef",
+ "fedcba98-7654-3210-fedc-ba9876543210"
+ ]
},
- "type": {
- "const": "metadata_to_model_output",
- "default": "metadata_to_model_output",
- "field_kind": "node_attribute",
- "title": "type",
- "type": "string"
+ "model_key_2": {
+ "type": "string",
+ "title": "Model Key 2",
+ "description": "The key of the second model in the relationship",
+ "examples": [
+ "3bb7c0eb-b6c8-469c-ad8c-4d69c06075e4",
+ "f0c3da4e-d9ff-42b5-a45c-23be75c887c9",
+ "38170dd8-f1e5-431e-866c-2c81f1277fcc",
+ "c57fea2d-7646-424c-b9ad-c0ba60fc68be",
+ "10f7807b-ab54-46a9-ab03-600e88c630a1",
+ "f6c1d267-cf87-4ee0-bee0-37e791eacab7"
+ ]
+ }
+ },
+ "type": "object",
+ "required": ["model_key_1", "model_key_2"],
+ "title": "ModelRelationshipCreateRequest"
+ },
+ "ModelRepoVariant": {
+ "type": "string",
+ "enum": ["", "fp16", "fp32", "onnx", "openvino", "flax"],
+ "title": "ModelRepoVariant",
+ "description": "Various hugging face variants on the diffusers format."
+ },
+ "ModelSourceType": {
+ "type": "string",
+ "enum": ["path", "url", "hf_repo_id", "external"],
+ "title": "ModelSourceType",
+ "description": "Model source type."
+ },
+ "ModelType": {
+ "type": "string",
+ "enum": [
+ "onnx",
+ "main",
+ "vae",
+ "lora",
+ "control_lora",
+ "controlnet",
+ "embedding",
+ "ip_adapter",
+ "clip_vision",
+ "clip_embed",
+ "t2i_adapter",
+ "t5_encoder",
+ "qwen3_encoder",
+ "qwen_vl_encoder",
+ "wan_t5_encoder",
+ "spandrel_image_to_image",
+ "siglip",
+ "flux_redux",
+ "llava_onevision",
+ "text_llm",
+ "external_image_generator",
+ "unknown"
+ ],
+ "title": "ModelType",
+ "description": "Model type."
+ },
+ "ModelVariantType": {
+ "type": "string",
+ "enum": ["normal", "inpaint", "depth"],
+ "title": "ModelVariantType",
+ "description": "Variant type."
+ },
+ "ModelsList": {
+ "properties": {
+ "models": {
+ "items": {
+ "oneOf": [
+ {
+ "$ref": "#/components/schemas/Main_Diffusers_SD1_Config"
+ },
+ {
+ "$ref": "#/components/schemas/Main_Diffusers_SD2_Config"
+ },
+ {
+ "$ref": "#/components/schemas/Main_Diffusers_SDXL_Config"
+ },
+ {
+ "$ref": "#/components/schemas/Main_Diffusers_SDXLRefiner_Config"
+ },
+ {
+ "$ref": "#/components/schemas/Main_Diffusers_SD3_Config"
+ },
+ {
+ "$ref": "#/components/schemas/Main_Diffusers_FLUX_Config"
+ },
+ {
+ "$ref": "#/components/schemas/Main_Diffusers_Flux2_Config"
+ },
+ {
+ "$ref": "#/components/schemas/Main_Diffusers_CogView4_Config"
+ },
+ {
+ "$ref": "#/components/schemas/Main_Diffusers_QwenImage_Config"
+ },
+ {
+ "$ref": "#/components/schemas/Main_Diffusers_Wan_Config"
+ },
+ {
+ "$ref": "#/components/schemas/Main_Diffusers_ZImage_Config"
+ },
+ {
+ "$ref": "#/components/schemas/Main_Checkpoint_SD1_Config"
+ },
+ {
+ "$ref": "#/components/schemas/Main_Checkpoint_SD2_Config"
+ },
+ {
+ "$ref": "#/components/schemas/Main_Checkpoint_SDXL_Config"
+ },
+ {
+ "$ref": "#/components/schemas/Main_Checkpoint_SDXLRefiner_Config"
+ },
+ {
+ "$ref": "#/components/schemas/Main_Checkpoint_Flux2_Config"
+ },
+ {
+ "$ref": "#/components/schemas/Main_Checkpoint_FLUX_Config"
+ },
+ {
+ "$ref": "#/components/schemas/Main_Checkpoint_QwenImage_Config"
+ },
+ {
+ "$ref": "#/components/schemas/Main_Checkpoint_ZImage_Config"
+ },
+ {
+ "$ref": "#/components/schemas/Main_Checkpoint_Anima_Config"
+ },
+ {
+ "$ref": "#/components/schemas/Main_BnBNF4_FLUX_Config"
+ },
+ {
+ "$ref": "#/components/schemas/Main_GGUF_Flux2_Config"
+ },
+ {
+ "$ref": "#/components/schemas/Main_GGUF_FLUX_Config"
+ },
+ {
+ "$ref": "#/components/schemas/Main_GGUF_QwenImage_Config"
+ },
+ {
+ "$ref": "#/components/schemas/Main_GGUF_Wan_Config"
+ },
+ {
+ "$ref": "#/components/schemas/Main_GGUF_ZImage_Config"
+ },
+ {
+ "$ref": "#/components/schemas/VAE_Checkpoint_SD1_Config"
+ },
+ {
+ "$ref": "#/components/schemas/VAE_Checkpoint_SD2_Config"
+ },
+ {
+ "$ref": "#/components/schemas/VAE_Checkpoint_SDXL_Config"
+ },
+ {
+ "$ref": "#/components/schemas/VAE_Checkpoint_FLUX_Config"
+ },
+ {
+ "$ref": "#/components/schemas/VAE_Checkpoint_Flux2_Config"
+ },
+ {
+ "$ref": "#/components/schemas/VAE_Checkpoint_Wan_Config"
+ },
+ {
+ "$ref": "#/components/schemas/VAE_Checkpoint_QwenImage_Config"
+ },
+ {
+ "$ref": "#/components/schemas/VAE_Checkpoint_Anima_Config"
+ },
+ {
+ "$ref": "#/components/schemas/VAE_Diffusers_SD1_Config"
+ },
+ {
+ "$ref": "#/components/schemas/VAE_Diffusers_SDXL_Config"
+ },
+ {
+ "$ref": "#/components/schemas/VAE_Diffusers_Flux2_Config"
+ },
+ {
+ "$ref": "#/components/schemas/VAE_Diffusers_Wan_Config"
+ },
+ {
+ "$ref": "#/components/schemas/ControlNet_Checkpoint_SD1_Config"
+ },
+ {
+ "$ref": "#/components/schemas/ControlNet_Checkpoint_SD2_Config"
+ },
+ {
+ "$ref": "#/components/schemas/ControlNet_Checkpoint_SDXL_Config"
+ },
+ {
+ "$ref": "#/components/schemas/ControlNet_Checkpoint_FLUX_Config"
+ },
+ {
+ "$ref": "#/components/schemas/ControlNet_Checkpoint_ZImage_Config"
+ },
+ {
+ "$ref": "#/components/schemas/ControlNet_Checkpoint_Anima_Config"
+ },
+ {
+ "$ref": "#/components/schemas/ControlNet_Diffusers_SD1_Config"
+ },
+ {
+ "$ref": "#/components/schemas/ControlNet_Diffusers_SD2_Config"
+ },
+ {
+ "$ref": "#/components/schemas/ControlNet_Diffusers_SDXL_Config"
+ },
+ {
+ "$ref": "#/components/schemas/ControlNet_Diffusers_FLUX_Config"
+ },
+ {
+ "$ref": "#/components/schemas/LoRA_LyCORIS_SD1_Config"
+ },
+ {
+ "$ref": "#/components/schemas/LoRA_LyCORIS_SD2_Config"
+ },
+ {
+ "$ref": "#/components/schemas/LoRA_LyCORIS_SDXL_Config"
+ },
+ {
+ "$ref": "#/components/schemas/LoRA_LyCORIS_Flux2_Config"
+ },
+ {
+ "$ref": "#/components/schemas/LoRA_LyCORIS_FLUX_Config"
+ },
+ {
+ "$ref": "#/components/schemas/LoRA_LyCORIS_ZImage_Config"
+ },
+ {
+ "$ref": "#/components/schemas/LoRA_LyCORIS_QwenImage_Config"
+ },
+ {
+ "$ref": "#/components/schemas/LoRA_LyCORIS_Wan_Config"
+ },
+ {
+ "$ref": "#/components/schemas/LoRA_LyCORIS_Anima_Config"
+ },
+ {
+ "$ref": "#/components/schemas/LoRA_OMI_SDXL_Config"
+ },
+ {
+ "$ref": "#/components/schemas/LoRA_OMI_FLUX_Config"
+ },
+ {
+ "$ref": "#/components/schemas/LoRA_Diffusers_SD1_Config"
+ },
+ {
+ "$ref": "#/components/schemas/LoRA_Diffusers_SD2_Config"
+ },
+ {
+ "$ref": "#/components/schemas/LoRA_Diffusers_SDXL_Config"
+ },
+ {
+ "$ref": "#/components/schemas/LoRA_Diffusers_Flux2_Config"
+ },
+ {
+ "$ref": "#/components/schemas/LoRA_Diffusers_FLUX_Config"
+ },
+ {
+ "$ref": "#/components/schemas/LoRA_Diffusers_ZImage_Config"
+ },
+ {
+ "$ref": "#/components/schemas/ControlLoRA_LyCORIS_FLUX_Config"
+ },
+ {
+ "$ref": "#/components/schemas/T5Encoder_T5Encoder_Config"
+ },
+ {
+ "$ref": "#/components/schemas/T5Encoder_BnBLLMint8_Config"
+ },
+ {
+ "$ref": "#/components/schemas/Qwen3Encoder_Qwen3Encoder_Config"
+ },
+ {
+ "$ref": "#/components/schemas/Qwen3Encoder_Checkpoint_Config"
+ },
+ {
+ "$ref": "#/components/schemas/Qwen3Encoder_GGUF_Config"
+ },
+ {
+ "$ref": "#/components/schemas/QwenVLEncoder_Diffusers_Config"
+ },
+ {
+ "$ref": "#/components/schemas/QwenVLEncoder_Checkpoint_Config"
+ },
+ {
+ "$ref": "#/components/schemas/WanT5Encoder_WanT5Encoder_Config"
+ },
+ {
+ "$ref": "#/components/schemas/TI_File_SD1_Config"
+ },
+ {
+ "$ref": "#/components/schemas/TI_File_SD2_Config"
+ },
+ {
+ "$ref": "#/components/schemas/TI_File_SDXL_Config"
+ },
+ {
+ "$ref": "#/components/schemas/TI_Folder_SD1_Config"
+ },
+ {
+ "$ref": "#/components/schemas/TI_Folder_SD2_Config"
+ },
+ {
+ "$ref": "#/components/schemas/TI_Folder_SDXL_Config"
+ },
+ {
+ "$ref": "#/components/schemas/IPAdapter_InvokeAI_SD1_Config"
+ },
+ {
+ "$ref": "#/components/schemas/IPAdapter_InvokeAI_SD2_Config"
+ },
+ {
+ "$ref": "#/components/schemas/IPAdapter_InvokeAI_SDXL_Config"
+ },
+ {
+ "$ref": "#/components/schemas/IPAdapter_Checkpoint_SD1_Config"
+ },
+ {
+ "$ref": "#/components/schemas/IPAdapter_Checkpoint_SD2_Config"
+ },
+ {
+ "$ref": "#/components/schemas/IPAdapter_Checkpoint_SDXL_Config"
+ },
+ {
+ "$ref": "#/components/schemas/IPAdapter_Checkpoint_FLUX_Config"
+ },
+ {
+ "$ref": "#/components/schemas/T2IAdapter_Diffusers_SD1_Config"
+ },
+ {
+ "$ref": "#/components/schemas/T2IAdapter_Diffusers_SDXL_Config"
+ },
+ {
+ "$ref": "#/components/schemas/Spandrel_Checkpoint_Config"
+ },
+ {
+ "$ref": "#/components/schemas/CLIPEmbed_Diffusers_G_Config"
+ },
+ {
+ "$ref": "#/components/schemas/CLIPEmbed_Diffusers_L_Config"
+ },
+ {
+ "$ref": "#/components/schemas/CLIPVision_Diffusers_Config"
+ },
+ {
+ "$ref": "#/components/schemas/SigLIP_Diffusers_Config"
+ },
+ {
+ "$ref": "#/components/schemas/FLUXRedux_Checkpoint_Config"
+ },
+ {
+ "$ref": "#/components/schemas/LlavaOnevision_Diffusers_Config"
+ },
+ {
+ "$ref": "#/components/schemas/TextLLM_Diffusers_Config"
+ },
+ {
+ "$ref": "#/components/schemas/ExternalApiModelConfig"
+ },
+ {
+ "$ref": "#/components/schemas/Unknown_Config"
+ }
+ ]
+ },
+ "type": "array",
+ "title": "Models"
}
},
- "required": ["output_meta", "model", "name", "unet", "vae", "clip", "type", "type"],
- "title": "MetadataToModelOutput",
- "type": "object"
+ "type": "object",
+ "required": ["models"],
+ "title": "ModelsList",
+ "description": "Return list of configs."
},
- "MetadataToSDXLLorasInvocation": {
- "category": "metadata",
+ "MultiplyInvocation": {
+ "category": "math",
"class": "invocation",
- "classification": "beta",
- "description": "Extracts a SDXL Loras value of a label from metadata",
+ "classification": "stable",
+ "description": "Multiplies two numbers",
"node_pack": "invokeai",
"properties": {
- "metadata": {
- "anyOf": [
- {
- "$ref": "#/components/schemas/MetadataField"
- },
- {
- "type": "null"
- }
- ],
- "default": null,
- "description": "Optional metadata to be saved with the image",
- "field_kind": "internal",
- "input": "connection",
- "orig_required": false,
- "ui_hidden": false
- },
"id": {
"description": "The id of this instance of an invocation. Must be unique among all instances of invocations.",
"field_kind": "node_attribute",
@@ -54865,264 +61751,162 @@
"title": "Use Cache",
"type": "boolean"
},
- "unet": {
- "anyOf": [
- {
- "$ref": "#/components/schemas/UNetField"
- },
- {
- "type": "null"
- }
- ],
- "default": null,
- "description": "UNet (scheduler, LoRAs)",
- "field_kind": "input",
- "input": "connection",
- "orig_default": null,
- "orig_required": false,
- "title": "UNet"
- },
- "clip": {
- "anyOf": [
- {
- "$ref": "#/components/schemas/CLIPField"
- },
- {
- "type": "null"
- }
- ],
- "default": null,
- "description": "CLIP (tokenizer, text encoder, LoRAs) and skipped layer count",
+ "a": {
+ "default": 0,
+ "description": "The first number",
"field_kind": "input",
- "input": "connection",
- "orig_default": null,
+ "input": "any",
+ "orig_default": 0,
"orig_required": false,
- "title": "CLIP 1"
+ "title": "A",
+ "type": "integer"
},
- "clip2": {
- "anyOf": [
- {
- "$ref": "#/components/schemas/CLIPField"
- },
- {
- "type": "null"
- }
- ],
- "default": null,
- "description": "CLIP (tokenizer, text encoder, LoRAs) and skipped layer count",
+ "b": {
+ "default": 0,
+ "description": "The second number",
"field_kind": "input",
- "input": "connection",
- "orig_default": null,
+ "input": "any",
+ "orig_default": 0,
"orig_required": false,
- "title": "CLIP 2"
+ "title": "B",
+ "type": "integer"
},
"type": {
- "const": "metadata_to_sdlx_loras",
- "default": "metadata_to_sdlx_loras",
+ "const": "mul",
+ "default": "mul",
"field_kind": "node_attribute",
"title": "type",
"type": "string"
}
},
"required": ["type", "id"],
- "tags": ["metadata"],
- "title": "Metadata To SDXL LoRAs",
+ "tags": ["math", "multiply"],
+ "title": "Multiply Integers",
"type": "object",
- "version": "1.1.1",
+ "version": "1.0.1",
"output": {
- "$ref": "#/components/schemas/SDXLLoRALoaderOutput"
+ "$ref": "#/components/schemas/IntegerOutput"
}
},
- "MetadataToSDXLModelInvocation": {
- "category": "metadata",
- "class": "invocation",
- "classification": "beta",
- "description": "Extracts a SDXL Model value of a label from metadata",
- "node_pack": "invokeai",
+ "NodeFieldValue": {
"properties": {
- "metadata": {
- "anyOf": [
- {
- "$ref": "#/components/schemas/MetadataField"
- },
- {
- "type": "null"
- }
- ],
- "default": null,
- "description": "Optional metadata to be saved with the image",
- "field_kind": "internal",
- "input": "connection",
- "orig_required": false,
- "ui_hidden": false
- },
- "id": {
- "description": "The id of this instance of an invocation. Must be unique among all instances of invocations.",
- "field_kind": "node_attribute",
- "title": "Id",
- "type": "string"
- },
- "is_intermediate": {
- "default": false,
- "description": "Whether or not this is an intermediate invocation.",
- "field_kind": "node_attribute",
- "input": "direct",
- "orig_required": true,
- "title": "Is Intermediate",
- "type": "boolean",
- "ui_hidden": false,
- "ui_type": "IsIntermediate"
- },
- "use_cache": {
- "default": true,
- "description": "Whether or not to use the cache",
- "field_kind": "node_attribute",
- "title": "Use Cache",
- "type": "boolean"
+ "node_path": {
+ "type": "string",
+ "title": "Node Path",
+ "description": "The node into which this batch data item will be substituted."
},
- "label": {
- "default": "model",
- "description": "Label for this metadata item",
- "enum": ["* CUSTOM LABEL *", "model"],
- "field_kind": "input",
- "input": "direct",
- "orig_default": "model",
- "orig_required": false,
- "title": "Label",
- "type": "string"
+ "field_name": {
+ "type": "string",
+ "title": "Field Name",
+ "description": "The field into which this batch data item will be substituted."
},
- "custom_label": {
+ "value": {
"anyOf": [
{
"type": "string"
},
{
- "type": "null"
- }
- ],
- "default": null,
- "description": "Label for this metadata item",
- "field_kind": "input",
- "input": "direct",
- "orig_default": null,
- "orig_required": false,
- "title": "Custom Label"
- },
- "default_value": {
- "anyOf": [
+ "type": "number"
+ },
{
- "$ref": "#/components/schemas/ModelIdentifierField"
+ "type": "integer"
},
{
- "type": "null"
+ "$ref": "#/components/schemas/ImageField"
+ },
+ {
+ "$ref": "#/components/schemas/VideoField"
+ },
+ {
+ "items": {
+ "anyOf": [
+ {
+ "type": "string"
+ },
+ {
+ "type": "number"
+ },
+ {
+ "type": "integer"
+ },
+ {
+ "$ref": "#/components/schemas/ImageField"
+ },
+ {
+ "$ref": "#/components/schemas/VideoField"
+ }
+ ]
+ },
+ "type": "array"
}
],
- "default": null,
- "description": "The default SDXL Model to use if not found in the metadata",
- "field_kind": "input",
- "input": "any",
- "orig_required": true,
- "ui_model_base": ["sdxl"],
- "ui_model_type": ["main"]
- },
- "type": {
- "const": "metadata_to_sdxl_model",
- "default": "metadata_to_sdxl_model",
- "field_kind": "node_attribute",
- "title": "type",
- "type": "string"
+ "title": "Value",
+ "description": "The value to substitute into the node/field."
}
},
- "required": ["type", "id"],
- "tags": ["metadata"],
- "title": "Metadata To SDXL Model",
"type": "object",
- "version": "1.3.0",
- "output": {
- "$ref": "#/components/schemas/MetadataToSDXLModelOutput"
- }
+ "required": ["node_path", "field_name", "value"],
+ "title": "NodeFieldValue"
},
- "MetadataToSDXLModelOutput": {
- "class": "output",
- "description": "String to SDXL main model output",
+ "NodePackInfo": {
"properties": {
- "model": {
- "$ref": "#/components/schemas/ModelIdentifierField",
- "description": "Main model (UNet, VAE, CLIP) to load",
- "field_kind": "output",
- "title": "Model",
- "ui_hidden": false
- },
"name": {
- "description": "Model Name",
- "field_kind": "output",
- "title": "Name",
"type": "string",
- "ui_hidden": false
- },
- "unet": {
- "$ref": "#/components/schemas/UNetField",
- "description": "UNet (scheduler, LoRAs)",
- "field_kind": "output",
- "title": "UNet",
- "ui_hidden": false
+ "title": "Name",
+ "description": "The name of the node pack."
},
- "clip": {
- "$ref": "#/components/schemas/CLIPField",
- "description": "CLIP (tokenizer, text encoder, LoRAs) and skipped layer count",
- "field_kind": "output",
- "title": "CLIP 1",
- "ui_hidden": false
+ "path": {
+ "type": "string",
+ "title": "Path",
+ "description": "The path to the node pack directory."
},
- "clip2": {
- "$ref": "#/components/schemas/CLIPField",
- "description": "CLIP (tokenizer, text encoder, LoRAs) and skipped layer count",
- "field_kind": "output",
- "title": "CLIP 2",
- "ui_hidden": false
+ "node_count": {
+ "type": "integer",
+ "title": "Node Count",
+ "description": "The number of nodes in the pack."
},
- "vae": {
- "$ref": "#/components/schemas/VAEField",
- "description": "VAE",
- "field_kind": "output",
- "title": "VAE",
- "ui_hidden": false
+ "node_types": {
+ "items": {
+ "type": "string"
+ },
+ "type": "array",
+ "title": "Node Types",
+ "description": "The invocation types provided by this node pack."
+ }
+ },
+ "type": "object",
+ "required": ["name", "path", "node_count", "node_types"],
+ "title": "NodePackInfo",
+ "description": "Information about an installed node pack."
+ },
+ "NodePackListResponse": {
+ "properties": {
+ "node_packs": {
+ "items": {
+ "$ref": "#/components/schemas/NodePackInfo"
+ },
+ "type": "array",
+ "title": "Node Packs",
+ "description": "List of installed node packs."
},
- "type": {
- "const": "metadata_to_sdxl_model_output",
- "default": "metadata_to_sdxl_model_output",
- "field_kind": "node_attribute",
- "title": "type",
- "type": "string"
+ "custom_nodes_path": {
+ "type": "string",
+ "title": "Custom Nodes Path",
+ "description": "The configured custom nodes directory path."
}
},
- "required": ["output_meta", "model", "name", "unet", "clip", "clip2", "vae", "type", "type"],
- "title": "MetadataToSDXLModelOutput",
- "type": "object"
+ "type": "object",
+ "required": ["node_packs", "custom_nodes_path"],
+ "title": "NodePackListResponse",
+ "description": "Response for listing installed node packs."
},
- "MetadataToSchedulerInvocation": {
- "category": "metadata",
+ "NoiseInvocation": {
+ "category": "latents",
"class": "invocation",
- "classification": "beta",
- "description": "Extracts a Scheduler value of a label from metadata",
+ "classification": "stable",
+ "description": "Generates latent noise for supported denoiser architectures.",
"node_pack": "invokeai",
"properties": {
- "metadata": {
- "anyOf": [
- {
- "$ref": "#/components/schemas/MetadataField"
- },
- {
- "type": "null"
- }
- ],
- "default": null,
- "description": "Optional metadata to be saved with the image",
- "field_kind": "internal",
- "input": "connection",
- "orig_required": false,
- "ui_hidden": false
- },
"id": {
"description": "The id of this instance of an invocation. Must be unique among all instances of invocations.",
"field_kind": "node_attribute",
@@ -55147,102 +61931,139 @@
"title": "Use Cache",
"type": "boolean"
},
- "label": {
- "default": "scheduler",
- "description": "Label for this metadata item",
- "enum": ["* CUSTOM LABEL *", "scheduler"],
+ "noise_type": {
+ "default": "SD",
+ "description": "Architecture-specific noise type.",
+ "enum": ["SD", "FLUX", "FLUX.2", "SD3", "CogView4", "Z-Image", "Anima"],
"field_kind": "input",
- "input": "direct",
- "orig_default": "scheduler",
+ "input": "any",
+ "orig_default": "SD",
"orig_required": false,
- "title": "Label",
+ "title": "Noise Type",
"type": "string"
},
- "custom_label": {
- "anyOf": [
- {
- "type": "string"
- },
- {
- "type": "null"
- }
- ],
- "default": null,
- "description": "Label for this metadata item",
+ "seed": {
+ "default": 0,
+ "description": "Seed for random number generation",
"field_kind": "input",
- "input": "direct",
- "orig_default": null,
+ "input": "any",
+ "maximum": 4294967295,
+ "minimum": 0,
+ "orig_default": 0,
"orig_required": false,
- "title": "Custom Label"
+ "title": "Seed",
+ "type": "integer"
},
- "default_value": {
- "default": "euler",
- "description": "The default scheduler to use if not found in the metadata",
- "enum": [
- "ddim",
- "ddpm",
- "deis",
- "deis_k",
- "lms",
- "lms_k",
- "pndm",
- "heun",
- "heun_k",
- "euler",
- "euler_k",
- "euler_a",
- "kdpm_2",
- "kdpm_2_k",
- "kdpm_2_a",
- "kdpm_2_a_k",
- "dpmpp_2s",
- "dpmpp_2s_k",
- "dpmpp_2m",
- "dpmpp_2m_k",
- "dpmpp_2m_sde",
- "dpmpp_2m_sde_k",
- "dpmpp_3m",
- "dpmpp_3m_k",
- "dpmpp_sde",
- "dpmpp_sde_k",
- "er_sde",
- "unipc",
- "unipc_k",
- "lcm",
- "tcd"
- ],
+ "width": {
+ "default": 512,
+ "description": "Width of output (px)",
+ "exclusiveMinimum": 0,
"field_kind": "input",
"input": "any",
- "orig_default": "euler",
+ "multipleOf": 8,
+ "orig_default": 512,
"orig_required": false,
- "title": "Default Value",
- "type": "string",
- "ui_type": "SchedulerField"
+ "title": "Width",
+ "type": "integer"
+ },
+ "height": {
+ "default": 512,
+ "description": "Height of output (px)",
+ "exclusiveMinimum": 0,
+ "field_kind": "input",
+ "input": "any",
+ "multipleOf": 8,
+ "orig_default": 512,
+ "orig_required": false,
+ "title": "Height",
+ "type": "integer"
+ },
+ "use_cpu": {
+ "default": true,
+ "description": "Use CPU for noise generation (for reproducible results across platforms)",
+ "field_kind": "input",
+ "input": "any",
+ "orig_default": true,
+ "orig_required": false,
+ "title": "Use Cpu",
+ "type": "boolean"
+ },
+ "type": {
+ "const": "noise",
+ "default": "noise",
+ "field_kind": "node_attribute",
+ "title": "type",
+ "type": "string"
+ }
+ },
+ "required": ["type", "id"],
+ "tags": ["latents", "noise"],
+ "title": "Create Latent Noise",
+ "type": "object",
+ "version": "1.1.0",
+ "output": {
+ "$ref": "#/components/schemas/NoiseOutput"
+ }
+ },
+ "NoiseOutput": {
+ "class": "output",
+ "description": "Invocation noise output",
+ "properties": {
+ "noise": {
+ "$ref": "#/components/schemas/LatentsField",
+ "description": "Noise tensor",
+ "field_kind": "output",
+ "ui_hidden": false
+ },
+ "width": {
+ "description": "Width of output (px)",
+ "field_kind": "output",
+ "title": "Width",
+ "type": "integer",
+ "ui_hidden": false
+ },
+ "height": {
+ "description": "Height of output (px)",
+ "field_kind": "output",
+ "title": "Height",
+ "type": "integer",
+ "ui_hidden": false
},
"type": {
- "const": "metadata_to_scheduler",
- "default": "metadata_to_scheduler",
+ "const": "noise_output",
+ "default": "noise_output",
"field_kind": "node_attribute",
"title": "type",
"type": "string"
}
},
- "required": ["type", "id"],
- "tags": ["metadata"],
- "title": "Metadata To Scheduler",
- "type": "object",
- "version": "1.0.1",
- "output": {
- "$ref": "#/components/schemas/SchedulerOutput"
- }
+ "required": ["output_meta", "noise", "width", "height", "type", "type"],
+ "title": "NoiseOutput",
+ "type": "object"
},
- "MetadataToStringCollectionInvocation": {
- "category": "metadata",
+ "NormalMapInvocation": {
+ "category": "controlnet_preprocessors",
"class": "invocation",
- "classification": "beta",
- "description": "Extracts a string collection value of a label from metadata",
+ "classification": "stable",
+ "description": "Generates a normal map.",
"node_pack": "invokeai",
"properties": {
+ "board": {
+ "anyOf": [
+ {
+ "$ref": "#/components/schemas/BoardField"
+ },
+ {
+ "type": "null"
+ }
+ ],
+ "default": null,
+ "description": "The board to save the image to",
+ "field_kind": "internal",
+ "input": "direct",
+ "orig_required": false,
+ "ui_hidden": false
+ },
"metadata": {
"anyOf": [
{
@@ -55283,83 +62104,181 @@
"title": "Use Cache",
"type": "boolean"
},
- "label": {
- "default": "* CUSTOM LABEL *",
- "description": "Label for this metadata item",
- "enum": [
- "* CUSTOM LABEL *",
- "positive_prompt",
- "positive_style_prompt",
- "negative_prompt",
- "negative_style_prompt"
- ],
- "field_kind": "input",
- "input": "direct",
- "orig_default": "* CUSTOM LABEL *",
- "orig_required": false,
- "title": "Label",
- "type": "string"
- },
- "custom_label": {
- "anyOf": [
- {
- "type": "string"
- },
- {
- "type": "null"
- }
- ],
- "default": null,
- "description": "Label for this metadata item",
- "field_kind": "input",
- "input": "direct",
- "orig_default": null,
- "orig_required": false,
- "title": "Custom Label"
- },
- "default_value": {
+ "image": {
"anyOf": [
{
- "items": {
- "type": "string"
- },
- "type": "array"
+ "$ref": "#/components/schemas/ImageField"
},
{
"type": "null"
}
],
"default": null,
- "description": "The default string collection to use if not found in the metadata",
+ "description": "The image to process",
"field_kind": "input",
"input": "any",
- "orig_required": true,
- "title": "Default Value"
+ "orig_required": true
},
"type": {
- "const": "metadata_to_string_collection",
- "default": "metadata_to_string_collection",
+ "const": "normal_map",
+ "default": "normal_map",
"field_kind": "node_attribute",
"title": "type",
"type": "string"
}
},
"required": ["type", "id"],
- "tags": ["metadata"],
- "title": "Metadata To String Collection",
+ "tags": ["controlnet", "normal"],
+ "title": "Normal Map",
"type": "object",
"version": "1.0.0",
"output": {
- "$ref": "#/components/schemas/StringCollectionOutput"
+ "$ref": "#/components/schemas/ImageOutput"
}
},
- "MetadataToStringInvocation": {
- "category": "metadata",
+ "OffsetPaginatedResults_BoardDTO_": {
+ "properties": {
+ "limit": {
+ "type": "integer",
+ "title": "Limit",
+ "description": "Limit of items to get"
+ },
+ "offset": {
+ "type": "integer",
+ "title": "Offset",
+ "description": "Offset from which to retrieve items"
+ },
+ "total": {
+ "type": "integer",
+ "title": "Total",
+ "description": "Total number of items in result"
+ },
+ "items": {
+ "items": {
+ "$ref": "#/components/schemas/BoardDTO"
+ },
+ "type": "array",
+ "title": "Items",
+ "description": "Items"
+ }
+ },
+ "type": "object",
+ "required": ["limit", "offset", "total", "items"],
+ "title": "OffsetPaginatedResults[BoardDTO]"
+ },
+ "OffsetPaginatedResults_GalleryItem_": {
+ "properties": {
+ "limit": {
+ "type": "integer",
+ "title": "Limit",
+ "description": "Limit of items to get"
+ },
+ "offset": {
+ "type": "integer",
+ "title": "Offset",
+ "description": "Offset from which to retrieve items"
+ },
+ "total": {
+ "type": "integer",
+ "title": "Total",
+ "description": "Total number of items in result"
+ },
+ "items": {
+ "items": {
+ "$ref": "#/components/schemas/GalleryItem"
+ },
+ "type": "array",
+ "title": "Items",
+ "description": "Items"
+ }
+ },
+ "type": "object",
+ "required": ["limit", "offset", "total", "items"],
+ "title": "OffsetPaginatedResults[GalleryItem]"
+ },
+ "OffsetPaginatedResults_ImageDTO_": {
+ "properties": {
+ "limit": {
+ "type": "integer",
+ "title": "Limit",
+ "description": "Limit of items to get"
+ },
+ "offset": {
+ "type": "integer",
+ "title": "Offset",
+ "description": "Offset from which to retrieve items"
+ },
+ "total": {
+ "type": "integer",
+ "title": "Total",
+ "description": "Total number of items in result"
+ },
+ "items": {
+ "items": {
+ "$ref": "#/components/schemas/ImageDTO"
+ },
+ "type": "array",
+ "title": "Items",
+ "description": "Items"
+ }
+ },
+ "type": "object",
+ "required": ["limit", "offset", "total", "items"],
+ "title": "OffsetPaginatedResults[ImageDTO]"
+ },
+ "OffsetPaginatedResults_VideoDTO_": {
+ "properties": {
+ "limit": {
+ "type": "integer",
+ "title": "Limit",
+ "description": "Limit of items to get"
+ },
+ "offset": {
+ "type": "integer",
+ "title": "Offset",
+ "description": "Offset from which to retrieve items"
+ },
+ "total": {
+ "type": "integer",
+ "title": "Total",
+ "description": "Total number of items in result"
+ },
+ "items": {
+ "items": {
+ "$ref": "#/components/schemas/VideoDTO"
+ },
+ "type": "array",
+ "title": "Items",
+ "description": "Items"
+ }
+ },
+ "type": "object",
+ "required": ["limit", "offset", "total", "items"],
+ "title": "OffsetPaginatedResults[VideoDTO]"
+ },
+ "OklabUnsharpMaskInvocation": {
+ "category": "image",
"class": "invocation",
- "classification": "beta",
- "description": "Extracts a string value of a label from metadata",
+ "classification": "stable",
+ "description": "Applies an unsharp mask filter to an image in the Oklab color space",
"node_pack": "invokeai",
"properties": {
+ "board": {
+ "anyOf": [
+ {
+ "$ref": "#/components/schemas/BoardField"
+ },
+ {
+ "type": "null"
+ }
+ ],
+ "default": null,
+ "description": "The board to save the image to",
+ "field_kind": "internal",
+ "input": "direct",
+ "orig_required": false,
+ "ui_hidden": false
+ },
"metadata": {
"anyOf": [
{
@@ -55400,167 +62319,83 @@
"title": "Use Cache",
"type": "boolean"
},
- "label": {
- "default": "* CUSTOM LABEL *",
- "description": "Label for this metadata item",
- "enum": [
- "* CUSTOM LABEL *",
- "positive_prompt",
- "positive_style_prompt",
- "negative_prompt",
- "negative_style_prompt"
- ],
- "field_kind": "input",
- "input": "direct",
- "orig_default": "* CUSTOM LABEL *",
- "orig_required": false,
- "title": "Label",
- "type": "string"
- },
- "custom_label": {
+ "image": {
"anyOf": [
{
- "type": "string"
+ "$ref": "#/components/schemas/ImageField"
},
{
"type": "null"
}
],
"default": null,
- "description": "Label for this metadata item",
+ "description": "The image to use",
"field_kind": "input",
- "input": "direct",
- "orig_default": null,
+ "input": "any",
+ "orig_required": true
+ },
+ "radius": {
+ "default": 2,
+ "description": "Unsharp mask radius",
+ "exclusiveMinimum": 0,
+ "field_kind": "input",
+ "input": "any",
+ "orig_default": 2,
"orig_required": false,
- "title": "Custom Label"
+ "title": "Radius",
+ "type": "number"
},
- "default_value": {
- "anyOf": [
- {
- "type": "string"
- },
- {
- "type": "null"
- }
- ],
- "default": null,
- "description": "The default string to use if not found in the metadata",
+ "strength": {
+ "default": 50,
+ "description": "Unsharp mask strength",
"field_kind": "input",
"input": "any",
- "orig_required": true,
- "title": "Default Value"
+ "minimum": 0,
+ "orig_default": 50,
+ "orig_required": false,
+ "title": "Strength",
+ "type": "number"
},
"type": {
- "const": "metadata_to_string",
- "default": "metadata_to_string",
+ "const": "unsharp_mask_oklab",
+ "default": "unsharp_mask_oklab",
"field_kind": "node_attribute",
"title": "type",
"type": "string"
}
},
"required": ["type", "id"],
- "tags": ["metadata"],
- "title": "Metadata To String",
+ "tags": ["image", "unsharp_mask", "oklab"],
+ "title": "Unsharp Mask (Oklab)",
"type": "object",
"version": "1.0.0",
"output": {
- "$ref": "#/components/schemas/StringOutput"
+ "$ref": "#/components/schemas/ImageOutput"
}
},
- "MetadataToT2IAdaptersInvocation": {
- "category": "metadata",
+ "OklchImageHueAdjustmentInvocation": {
+ "category": "image",
"class": "invocation",
- "classification": "beta",
- "description": "Extracts a T2I-Adapters value of a label from metadata",
+ "classification": "stable",
+ "description": "Adjusts the hue of an image in Oklch space.",
"node_pack": "invokeai",
"properties": {
- "metadata": {
+ "board": {
"anyOf": [
{
- "$ref": "#/components/schemas/MetadataField"
+ "$ref": "#/components/schemas/BoardField"
},
{
"type": "null"
}
],
"default": null,
- "description": "Optional metadata to be saved with the image",
+ "description": "The board to save the image to",
"field_kind": "internal",
- "input": "connection",
- "orig_required": false,
- "ui_hidden": false
- },
- "id": {
- "description": "The id of this instance of an invocation. Must be unique among all instances of invocations.",
- "field_kind": "node_attribute",
- "title": "Id",
- "type": "string"
- },
- "is_intermediate": {
- "default": false,
- "description": "Whether or not this is an intermediate invocation.",
- "field_kind": "node_attribute",
"input": "direct",
- "orig_required": true,
- "title": "Is Intermediate",
- "type": "boolean",
- "ui_hidden": false,
- "ui_type": "IsIntermediate"
- },
- "use_cache": {
- "default": true,
- "description": "Whether or not to use the cache",
- "field_kind": "node_attribute",
- "title": "Use Cache",
- "type": "boolean"
- },
- "t2i_adapter_list": {
- "anyOf": [
- {
- "$ref": "#/components/schemas/T2IAdapterField"
- },
- {
- "items": {
- "$ref": "#/components/schemas/T2IAdapterField"
- },
- "type": "array"
- },
- {
- "type": "null"
- }
- ],
- "default": null,
- "description": "IP-Adapter to apply",
- "field_kind": "input",
- "input": "connection",
- "orig_default": null,
"orig_required": false,
- "title": "T2I-Adapter"
+ "ui_hidden": false
},
- "type": {
- "const": "metadata_to_t2i_adapters",
- "default": "metadata_to_t2i_adapters",
- "field_kind": "node_attribute",
- "title": "type",
- "type": "string"
- }
- },
- "required": ["type", "id"],
- "tags": ["metadata"],
- "title": "Metadata To T2I-Adapters",
- "type": "object",
- "version": "1.2.0",
- "output": {
- "$ref": "#/components/schemas/MDT2IAdapterListOutput"
- }
- },
- "MetadataToVAEInvocation": {
- "category": "metadata",
- "class": "invocation",
- "classification": "beta",
- "description": "Extracts a VAE value of a label from metadata",
- "node_pack": "invokeai",
- "properties": {
"metadata": {
"anyOf": [
{
@@ -55601,139 +62436,87 @@
"title": "Use Cache",
"type": "boolean"
},
- "label": {
- "default": "vae",
- "description": "Label for this metadata item",
- "enum": ["* CUSTOM LABEL *", "vae"],
- "field_kind": "input",
- "input": "direct",
- "orig_default": "vae",
- "orig_required": false,
- "title": "Label",
- "type": "string"
- },
- "custom_label": {
+ "image": {
"anyOf": [
{
- "type": "string"
+ "$ref": "#/components/schemas/ImageField"
},
{
"type": "null"
}
],
"default": null,
- "description": "Label for this metadata item",
+ "description": "The image to adjust",
"field_kind": "input",
- "input": "direct",
- "orig_default": null,
- "orig_required": false,
- "title": "Custom Label"
+ "input": "any",
+ "orig_required": true
},
- "default_value": {
- "anyOf": [
- {
- "$ref": "#/components/schemas/VAEField"
- },
- {
- "type": "null"
- }
- ],
- "default": null,
- "description": "The default VAE to use if not found in the metadata",
+ "hue": {
+ "default": 0,
+ "description": "The degrees by which to rotate the hue, 0-360",
"field_kind": "input",
"input": "any",
- "orig_required": true
+ "orig_default": 0,
+ "orig_required": false,
+ "title": "Hue",
+ "type": "integer"
},
"type": {
- "const": "metadata_to_vae",
- "default": "metadata_to_vae",
+ "const": "img_hue_adjust_oklch",
+ "default": "img_hue_adjust_oklch",
"field_kind": "node_attribute",
"title": "type",
"type": "string"
}
},
"required": ["type", "id"],
- "tags": ["metadata"],
- "title": "Metadata To VAE",
+ "tags": ["image", "hue", "oklch"],
+ "title": "Adjust Image Hue (Oklch)",
"type": "object",
- "version": "1.2.1",
+ "version": "1.0.0",
"output": {
- "$ref": "#/components/schemas/VAEOutput"
+ "$ref": "#/components/schemas/ImageOutput"
}
},
- "ModelFormat": {
- "type": "string",
- "enum": [
- "omi",
- "diffusers",
- "checkpoint",
- "lycoris",
- "onnx",
- "olive",
- "embedding_file",
- "embedding_folder",
- "invokeai",
- "t5_encoder",
- "qwen3_encoder",
- "qwen_vl_encoder",
- "bnb_quantized_int8b",
- "bnb_quantized_nf4b",
- "gguf_quantized",
- "external_api",
- "unknown"
- ],
- "title": "ModelFormat",
- "description": "Storage format of model."
- },
- "ModelIdentifierField": {
+ "OpenAIImageGenerationInvocation": {
+ "category": "image",
+ "class": "invocation",
+ "classification": "stable",
+ "description": "Generate images using an OpenAI-hosted external model.",
+ "node_pack": "invokeai",
"properties": {
- "key": {
- "description": "The model's unique key",
- "title": "Key",
- "type": "string"
- },
- "hash": {
- "description": "The model's BLAKE3 hash",
- "title": "Hash",
- "type": "string"
- },
- "name": {
- "description": "The model's name",
- "title": "Name",
- "type": "string"
- },
- "base": {
- "$ref": "#/components/schemas/BaseModelType",
- "description": "The model's base model type"
- },
- "type": {
- "$ref": "#/components/schemas/ModelType",
- "description": "The model's type"
+ "board": {
+ "anyOf": [
+ {
+ "$ref": "#/components/schemas/BoardField"
+ },
+ {
+ "type": "null"
+ }
+ ],
+ "default": null,
+ "description": "The board to save the image to",
+ "field_kind": "internal",
+ "input": "direct",
+ "orig_required": false,
+ "ui_hidden": false
},
- "submodel_type": {
+ "metadata": {
"anyOf": [
{
- "$ref": "#/components/schemas/SubModelType"
+ "$ref": "#/components/schemas/MetadataField"
},
{
"type": "null"
}
],
"default": null,
- "description": "The submodel to load, if this is a main model"
- }
- },
- "required": ["key", "hash", "name", "base", "type"],
- "title": "ModelIdentifierField",
- "type": "object"
- },
- "ModelIdentifierInvocation": {
- "category": "model",
- "class": "invocation",
- "classification": "stable",
- "description": "Selects any model, outputting it its identifier. Be careful with this one! The identifier will be accepted as\ninput for any model, even if the model types don't match. If you connect this to a mismatched input, you'll get an\nerror.",
- "node_pack": "invokeai",
- "properties": {
+ "description": "Optional metadata to be saved with the image",
+ "field_kind": "internal",
+ "input": "connection",
+ "orig_required": false,
+ "ui_hidden": false
+ },
"id": {
"description": "The id of this instance of an invocation. Must be unique among all instances of invocations.",
"field_kind": "node_attribute",
@@ -55768,1994 +62551,2861 @@
}
],
"default": null,
- "description": "The model to select",
+ "description": "Main model (UNet, VAE, CLIP) to load",
"field_kind": "input",
"input": "any",
"orig_required": true,
- "title": "Model"
- },
- "type": {
- "const": "model_identifier",
- "default": "model_identifier",
- "field_kind": "node_attribute",
- "title": "type",
- "type": "string"
- }
- },
- "required": ["type", "id"],
- "tags": ["model"],
- "title": "Any Model",
- "type": "object",
- "version": "1.0.1",
- "output": {
- "$ref": "#/components/schemas/ModelIdentifierOutput"
- }
- },
- "ModelIdentifierOutput": {
- "class": "output",
- "description": "Model identifier output",
- "properties": {
- "model": {
- "$ref": "#/components/schemas/ModelIdentifierField",
- "description": "Model identifier",
- "field_kind": "output",
- "title": "Model",
- "ui_hidden": false
- },
- "type": {
- "const": "model_identifier_output",
- "default": "model_identifier_output",
- "field_kind": "node_attribute",
- "title": "type",
- "type": "string"
- }
- },
- "required": ["output_meta", "model", "type", "type"],
- "title": "ModelIdentifierOutput",
- "type": "object"
- },
- "ModelInstallCancelledEvent": {
- "description": "Event model for model_install_cancelled",
- "properties": {
- "timestamp": {
- "description": "The timestamp of the event",
- "title": "Timestamp",
- "type": "integer"
+ "ui_model_base": ["external"],
+ "ui_model_format": ["external_api"],
+ "ui_model_provider_id": ["openai"],
+ "ui_model_type": ["external_image_generator"]
},
- "id": {
- "description": "The ID of the install job",
- "title": "Id",
- "type": "integer"
+ "mode": {
+ "default": "txt2img",
+ "description": "Generation mode.",
+ "enum": ["txt2img", "img2img", "inpaint"],
+ "field_kind": "input",
+ "input": "any",
+ "orig_default": "txt2img",
+ "orig_required": false,
+ "title": "Mode",
+ "type": "string",
+ "ui_hidden": true
},
- "source": {
- "description": "Source of the model; local path, repo_id or url",
- "discriminator": {
- "mapping": {
- "external": "#/components/schemas/ExternalModelSource",
- "hf": "#/components/schemas/HFModelSource",
- "local": "#/components/schemas/LocalModelSource",
- "url": "#/components/schemas/URLModelSource"
- },
- "propertyName": "type"
- },
- "oneOf": [
+ "prompt": {
+ "anyOf": [
{
- "$ref": "#/components/schemas/LocalModelSource"
+ "type": "string"
},
{
- "$ref": "#/components/schemas/HFModelSource"
- },
+ "type": "null"
+ }
+ ],
+ "default": null,
+ "description": "Prompt",
+ "field_kind": "input",
+ "input": "any",
+ "orig_required": true,
+ "title": "Prompt"
+ },
+ "seed": {
+ "anyOf": [
{
- "$ref": "#/components/schemas/URLModelSource"
+ "type": "integer"
},
{
- "$ref": "#/components/schemas/ExternalModelSource"
+ "type": "null"
}
],
- "title": "Source"
- }
- },
- "required": ["timestamp", "id", "source"],
- "title": "ModelInstallCancelledEvent",
- "type": "object"
- },
- "ModelInstallCompleteEvent": {
- "description": "Event model for model_install_complete",
- "properties": {
- "timestamp": {
- "description": "The timestamp of the event",
- "title": "Timestamp",
+ "default": null,
+ "description": "Seed for random number generation",
+ "field_kind": "input",
+ "input": "any",
+ "orig_default": null,
+ "orig_required": false,
+ "title": "Seed"
+ },
+ "num_images": {
+ "default": 1,
+ "description": "Number of images to generate",
+ "exclusiveMinimum": 0,
+ "field_kind": "input",
+ "input": "any",
+ "orig_default": 1,
+ "orig_required": false,
+ "title": "Num Images",
"type": "integer"
},
- "id": {
- "description": "The ID of the install job",
- "title": "Id",
+ "width": {
+ "default": 1024,
+ "description": "Width of output (px)",
+ "exclusiveMinimum": 0,
+ "field_kind": "input",
+ "input": "any",
+ "orig_default": 1024,
+ "orig_required": false,
+ "title": "Width",
"type": "integer"
},
- "source": {
- "description": "Source of the model; local path, repo_id or url",
- "discriminator": {
- "mapping": {
- "external": "#/components/schemas/ExternalModelSource",
- "hf": "#/components/schemas/HFModelSource",
- "local": "#/components/schemas/LocalModelSource",
- "url": "#/components/schemas/URLModelSource"
- },
- "propertyName": "type"
- },
- "oneOf": [
- {
- "$ref": "#/components/schemas/LocalModelSource"
- },
- {
- "$ref": "#/components/schemas/HFModelSource"
- },
+ "height": {
+ "default": 1024,
+ "description": "Height of output (px)",
+ "exclusiveMinimum": 0,
+ "field_kind": "input",
+ "input": "any",
+ "orig_default": 1024,
+ "orig_required": false,
+ "title": "Height",
+ "type": "integer"
+ },
+ "image_size": {
+ "anyOf": [
{
- "$ref": "#/components/schemas/URLModelSource"
+ "type": "string"
},
{
- "$ref": "#/components/schemas/ExternalModelSource"
+ "type": "null"
}
],
- "title": "Source"
- },
- "key": {
- "description": "Model config record key",
- "title": "Key",
- "type": "string"
+ "default": null,
+ "description": "Image size preset (e.g. 1K, 2K, 4K)",
+ "field_kind": "input",
+ "input": "any",
+ "orig_default": null,
+ "orig_required": false,
+ "title": "Image Size"
},
- "total_bytes": {
+ "init_image": {
"anyOf": [
{
- "type": "integer"
+ "$ref": "#/components/schemas/ImageField"
},
{
"type": "null"
}
],
- "description": "Size of the model (may be None for installation of a local path)",
- "title": "Total Bytes"
+ "default": null,
+ "description": "Init image (use reference_images instead)",
+ "field_kind": "input",
+ "input": "any",
+ "orig_default": null,
+ "orig_required": false,
+ "ui_hidden": true
},
- "config": {
- "description": "The installed model's config",
- "oneOf": [
- {
- "$ref": "#/components/schemas/Main_Diffusers_SD1_Config"
- },
- {
- "$ref": "#/components/schemas/Main_Diffusers_SD2_Config"
- },
- {
- "$ref": "#/components/schemas/Main_Diffusers_SDXL_Config"
- },
- {
- "$ref": "#/components/schemas/Main_Diffusers_SDXLRefiner_Config"
- },
- {
- "$ref": "#/components/schemas/Main_Diffusers_SD3_Config"
- },
- {
- "$ref": "#/components/schemas/Main_Diffusers_FLUX_Config"
- },
- {
- "$ref": "#/components/schemas/Main_Diffusers_Flux2_Config"
- },
- {
- "$ref": "#/components/schemas/Main_Diffusers_CogView4_Config"
- },
- {
- "$ref": "#/components/schemas/Main_Diffusers_QwenImage_Config"
- },
- {
- "$ref": "#/components/schemas/Main_Diffusers_ZImage_Config"
- },
- {
- "$ref": "#/components/schemas/Main_Checkpoint_SD1_Config"
- },
- {
- "$ref": "#/components/schemas/Main_Checkpoint_SD2_Config"
- },
- {
- "$ref": "#/components/schemas/Main_Checkpoint_SDXL_Config"
- },
- {
- "$ref": "#/components/schemas/Main_Checkpoint_SDXLRefiner_Config"
- },
- {
- "$ref": "#/components/schemas/Main_Checkpoint_Flux2_Config"
- },
- {
- "$ref": "#/components/schemas/Main_Checkpoint_FLUX_Config"
- },
- {
- "$ref": "#/components/schemas/Main_Checkpoint_QwenImage_Config"
- },
- {
- "$ref": "#/components/schemas/Main_Checkpoint_ZImage_Config"
- },
- {
- "$ref": "#/components/schemas/Main_Checkpoint_Anima_Config"
- },
- {
- "$ref": "#/components/schemas/Main_BnBNF4_FLUX_Config"
- },
- {
- "$ref": "#/components/schemas/Main_GGUF_Flux2_Config"
- },
- {
- "$ref": "#/components/schemas/Main_GGUF_FLUX_Config"
- },
- {
- "$ref": "#/components/schemas/Main_GGUF_QwenImage_Config"
- },
- {
- "$ref": "#/components/schemas/Main_GGUF_ZImage_Config"
- },
- {
- "$ref": "#/components/schemas/VAE_Checkpoint_SD1_Config"
- },
- {
- "$ref": "#/components/schemas/VAE_Checkpoint_SD2_Config"
- },
- {
- "$ref": "#/components/schemas/VAE_Checkpoint_SDXL_Config"
- },
- {
- "$ref": "#/components/schemas/VAE_Checkpoint_FLUX_Config"
- },
- {
- "$ref": "#/components/schemas/VAE_Checkpoint_Flux2_Config"
- },
- {
- "$ref": "#/components/schemas/VAE_Checkpoint_QwenImage_Config"
- },
- {
- "$ref": "#/components/schemas/VAE_Checkpoint_Anima_Config"
- },
- {
- "$ref": "#/components/schemas/VAE_Diffusers_SD1_Config"
- },
- {
- "$ref": "#/components/schemas/VAE_Diffusers_SDXL_Config"
- },
- {
- "$ref": "#/components/schemas/VAE_Diffusers_Flux2_Config"
- },
- {
- "$ref": "#/components/schemas/ControlNet_Checkpoint_SD1_Config"
- },
- {
- "$ref": "#/components/schemas/ControlNet_Checkpoint_SD2_Config"
- },
- {
- "$ref": "#/components/schemas/ControlNet_Checkpoint_SDXL_Config"
- },
- {
- "$ref": "#/components/schemas/ControlNet_Checkpoint_FLUX_Config"
- },
- {
- "$ref": "#/components/schemas/ControlNet_Checkpoint_ZImage_Config"
- },
- {
- "$ref": "#/components/schemas/ControlNet_Checkpoint_Anima_Config"
- },
- {
- "$ref": "#/components/schemas/ControlNet_Diffusers_SD1_Config"
- },
- {
- "$ref": "#/components/schemas/ControlNet_Diffusers_SD2_Config"
- },
- {
- "$ref": "#/components/schemas/ControlNet_Diffusers_SDXL_Config"
- },
- {
- "$ref": "#/components/schemas/ControlNet_Diffusers_FLUX_Config"
- },
- {
- "$ref": "#/components/schemas/LoRA_LyCORIS_SD1_Config"
- },
- {
- "$ref": "#/components/schemas/LoRA_LyCORIS_SD2_Config"
- },
- {
- "$ref": "#/components/schemas/LoRA_LyCORIS_SDXL_Config"
- },
- {
- "$ref": "#/components/schemas/LoRA_LyCORIS_Flux2_Config"
- },
- {
- "$ref": "#/components/schemas/LoRA_LyCORIS_FLUX_Config"
- },
- {
- "$ref": "#/components/schemas/LoRA_LyCORIS_ZImage_Config"
- },
- {
- "$ref": "#/components/schemas/LoRA_LyCORIS_QwenImage_Config"
- },
- {
- "$ref": "#/components/schemas/LoRA_LyCORIS_Anima_Config"
- },
- {
- "$ref": "#/components/schemas/LoRA_OMI_SDXL_Config"
- },
- {
- "$ref": "#/components/schemas/LoRA_OMI_FLUX_Config"
- },
- {
- "$ref": "#/components/schemas/LoRA_Diffusers_SD1_Config"
- },
- {
- "$ref": "#/components/schemas/LoRA_Diffusers_SD2_Config"
- },
- {
- "$ref": "#/components/schemas/LoRA_Diffusers_SDXL_Config"
- },
- {
- "$ref": "#/components/schemas/LoRA_Diffusers_Flux2_Config"
- },
- {
- "$ref": "#/components/schemas/LoRA_Diffusers_FLUX_Config"
- },
- {
- "$ref": "#/components/schemas/LoRA_Diffusers_ZImage_Config"
- },
- {
- "$ref": "#/components/schemas/ControlLoRA_LyCORIS_FLUX_Config"
- },
- {
- "$ref": "#/components/schemas/T5Encoder_T5Encoder_Config"
- },
- {
- "$ref": "#/components/schemas/T5Encoder_BnBLLMint8_Config"
- },
- {
- "$ref": "#/components/schemas/Qwen3Encoder_Qwen3Encoder_Config"
- },
- {
- "$ref": "#/components/schemas/Qwen3Encoder_Checkpoint_Config"
- },
- {
- "$ref": "#/components/schemas/Qwen3Encoder_GGUF_Config"
- },
- {
- "$ref": "#/components/schemas/QwenVLEncoder_Diffusers_Config"
- },
- {
- "$ref": "#/components/schemas/QwenVLEncoder_Checkpoint_Config"
- },
- {
- "$ref": "#/components/schemas/TI_File_SD1_Config"
- },
- {
- "$ref": "#/components/schemas/TI_File_SD2_Config"
- },
- {
- "$ref": "#/components/schemas/TI_File_SDXL_Config"
- },
- {
- "$ref": "#/components/schemas/TI_Folder_SD1_Config"
- },
- {
- "$ref": "#/components/schemas/TI_Folder_SD2_Config"
- },
- {
- "$ref": "#/components/schemas/TI_Folder_SDXL_Config"
- },
- {
- "$ref": "#/components/schemas/IPAdapter_InvokeAI_SD1_Config"
- },
- {
- "$ref": "#/components/schemas/IPAdapter_InvokeAI_SD2_Config"
- },
- {
- "$ref": "#/components/schemas/IPAdapter_InvokeAI_SDXL_Config"
- },
- {
- "$ref": "#/components/schemas/IPAdapter_Checkpoint_SD1_Config"
- },
- {
- "$ref": "#/components/schemas/IPAdapter_Checkpoint_SD2_Config"
- },
- {
- "$ref": "#/components/schemas/IPAdapter_Checkpoint_SDXL_Config"
- },
- {
- "$ref": "#/components/schemas/IPAdapter_Checkpoint_FLUX_Config"
- },
- {
- "$ref": "#/components/schemas/T2IAdapter_Diffusers_SD1_Config"
- },
- {
- "$ref": "#/components/schemas/T2IAdapter_Diffusers_SDXL_Config"
- },
- {
- "$ref": "#/components/schemas/Spandrel_Checkpoint_Config"
- },
- {
- "$ref": "#/components/schemas/CLIPEmbed_Diffusers_G_Config"
- },
- {
- "$ref": "#/components/schemas/CLIPEmbed_Diffusers_L_Config"
- },
- {
- "$ref": "#/components/schemas/CLIPVision_Diffusers_Config"
- },
- {
- "$ref": "#/components/schemas/SigLIP_Diffusers_Config"
- },
- {
- "$ref": "#/components/schemas/FLUXRedux_Checkpoint_Config"
- },
+ "mask_image": {
+ "anyOf": [
{
- "$ref": "#/components/schemas/LlavaOnevision_Diffusers_Config"
+ "$ref": "#/components/schemas/ImageField"
},
{
- "$ref": "#/components/schemas/TextLLM_Diffusers_Config"
- },
+ "type": "null"
+ }
+ ],
+ "default": null,
+ "description": "Mask image for inpaint",
+ "field_kind": "input",
+ "input": "any",
+ "orig_default": null,
+ "orig_required": false,
+ "ui_hidden": true
+ },
+ "reference_images": {
+ "default": [],
+ "description": "Reference images",
+ "field_kind": "input",
+ "input": "any",
+ "items": {
+ "$ref": "#/components/schemas/ImageField"
+ },
+ "orig_default": [],
+ "orig_required": false,
+ "title": "Reference Images",
+ "type": "array"
+ },
+ "quality": {
+ "default": "auto",
+ "description": "Output image quality",
+ "enum": ["auto", "high", "medium", "low"],
+ "field_kind": "input",
+ "input": "any",
+ "orig_default": "auto",
+ "orig_required": false,
+ "title": "Quality",
+ "type": "string"
+ },
+ "background": {
+ "default": "auto",
+ "description": "Background transparency handling",
+ "enum": ["auto", "transparent", "opaque"],
+ "field_kind": "input",
+ "input": "any",
+ "orig_default": "auto",
+ "orig_required": false,
+ "title": "Background",
+ "type": "string"
+ },
+ "input_fidelity": {
+ "anyOf": [
{
- "$ref": "#/components/schemas/ExternalApiModelConfig"
+ "enum": ["low", "high"],
+ "type": "string"
},
{
- "$ref": "#/components/schemas/Unknown_Config"
+ "type": "null"
}
],
- "title": "Config"
+ "default": null,
+ "description": "Fidelity to source images (edits only)",
+ "field_kind": "input",
+ "input": "any",
+ "orig_default": null,
+ "orig_required": false,
+ "title": "Input Fidelity"
+ },
+ "type": {
+ "const": "openai_image_generation",
+ "default": "openai_image_generation",
+ "field_kind": "node_attribute",
+ "title": "type",
+ "type": "string"
}
},
- "required": ["timestamp", "id", "source", "key", "total_bytes", "config"],
- "title": "ModelInstallCompleteEvent",
- "type": "object"
+ "required": ["type", "id"],
+ "tags": ["external", "generation", "openai"],
+ "title": "OpenAI Image Generation",
+ "type": "object",
+ "version": "1.0.0",
+ "output": {
+ "$ref": "#/components/schemas/ImageCollectionOutput"
+ }
},
- "ModelInstallDownloadProgressEvent": {
- "description": "Event model for model_install_download_progress",
+ "OrphanedModelInfo": {
"properties": {
- "timestamp": {
- "description": "The timestamp of the event",
- "title": "Timestamp",
- "type": "integer"
+ "path": {
+ "type": "string",
+ "title": "Path",
+ "description": "Relative path to the orphaned directory from models root"
},
- "id": {
- "description": "The ID of the install job",
- "title": "Id",
- "type": "integer"
+ "absolute_path": {
+ "type": "string",
+ "title": "Absolute Path",
+ "description": "Absolute path to the orphaned directory"
},
- "source": {
- "description": "Source of the model; local path, repo_id or url",
- "discriminator": {
- "mapping": {
- "external": "#/components/schemas/ExternalModelSource",
- "hf": "#/components/schemas/HFModelSource",
- "local": "#/components/schemas/LocalModelSource",
- "url": "#/components/schemas/URLModelSource"
- },
- "propertyName": "type"
+ "files": {
+ "items": {
+ "type": "string"
},
- "oneOf": [
+ "type": "array",
+ "title": "Files",
+ "description": "List of model files in this directory"
+ },
+ "size_bytes": {
+ "type": "integer",
+ "title": "Size Bytes",
+ "description": "Total size of all files in bytes"
+ }
+ },
+ "type": "object",
+ "required": ["path", "absolute_path", "files", "size_bytes"],
+ "title": "OrphanedModelInfo",
+ "description": "Information about an orphaned model directory."
+ },
+ "OutputFieldJSONSchemaExtra": {
+ "description": "Extra attributes to be added to input fields and their OpenAPI schema. Used by the workflow editor\nduring schema parsing and UI rendering.",
+ "properties": {
+ "field_kind": {
+ "$ref": "#/components/schemas/FieldKind"
+ },
+ "ui_hidden": {
+ "default": false,
+ "title": "Ui Hidden",
+ "type": "boolean"
+ },
+ "ui_order": {
+ "anyOf": [
{
- "$ref": "#/components/schemas/LocalModelSource"
+ "type": "integer"
},
{
- "$ref": "#/components/schemas/HFModelSource"
- },
+ "type": "null"
+ }
+ ],
+ "default": null,
+ "title": "Ui Order"
+ },
+ "ui_type": {
+ "anyOf": [
{
- "$ref": "#/components/schemas/URLModelSource"
+ "$ref": "#/components/schemas/UIType"
},
{
- "$ref": "#/components/schemas/ExternalModelSource"
+ "type": "null"
}
],
- "title": "Source"
- },
- "local_path": {
- "description": "Where model is downloading to",
- "title": "Local Path",
- "type": "string"
- },
- "bytes": {
- "description": "Number of bytes downloaded so far",
- "title": "Bytes",
- "type": "integer"
- },
- "total_bytes": {
- "description": "Total size of download, including all files",
- "title": "Total Bytes",
- "type": "integer"
- },
- "parts": {
- "description": "Progress of downloading URLs that comprise the model, if any",
- "items": {
- "additionalProperties": {
- "anyOf": [
- {
- "type": "integer"
- },
- {
- "type": "string"
- }
- ]
- },
- "type": "object"
- },
- "title": "Parts",
- "type": "array"
+ "default": null
}
},
- "required": ["timestamp", "id", "source", "local_path", "bytes", "total_bytes", "parts"],
- "title": "ModelInstallDownloadProgressEvent",
+ "required": ["field_kind", "ui_hidden", "ui_order", "ui_type"],
+ "title": "OutputFieldJSONSchemaExtra",
"type": "object"
},
- "ModelInstallDownloadStartedEvent": {
- "description": "Event model for model_install_download_started",
+ "PBRMapsInvocation": {
+ "category": "controlnet_preprocessors",
+ "class": "invocation",
+ "classification": "stable",
+ "description": "Generate Normal, Displacement and Roughness Map from a given image",
+ "node_pack": "invokeai",
"properties": {
- "timestamp": {
- "description": "The timestamp of the event",
- "title": "Timestamp",
- "type": "integer"
- },
- "id": {
- "description": "The ID of the install job",
- "title": "Id",
- "type": "integer"
- },
- "source": {
- "description": "Source of the model; local path, repo_id or url",
- "discriminator": {
- "mapping": {
- "external": "#/components/schemas/ExternalModelSource",
- "hf": "#/components/schemas/HFModelSource",
- "local": "#/components/schemas/LocalModelSource",
- "url": "#/components/schemas/URLModelSource"
- },
- "propertyName": "type"
- },
- "oneOf": [
+ "board": {
+ "anyOf": [
{
- "$ref": "#/components/schemas/LocalModelSource"
+ "$ref": "#/components/schemas/BoardField"
},
{
- "$ref": "#/components/schemas/HFModelSource"
- },
+ "type": "null"
+ }
+ ],
+ "default": null,
+ "description": "The board to save the image to",
+ "field_kind": "internal",
+ "input": "direct",
+ "orig_required": false,
+ "ui_hidden": false
+ },
+ "metadata": {
+ "anyOf": [
{
- "$ref": "#/components/schemas/URLModelSource"
+ "$ref": "#/components/schemas/MetadataField"
},
{
- "$ref": "#/components/schemas/ExternalModelSource"
+ "type": "null"
}
],
- "title": "Source"
+ "default": null,
+ "description": "Optional metadata to be saved with the image",
+ "field_kind": "internal",
+ "input": "connection",
+ "orig_required": false,
+ "ui_hidden": false
},
- "local_path": {
- "description": "Where model is downloading to",
- "title": "Local Path",
+ "id": {
+ "description": "The id of this instance of an invocation. Must be unique among all instances of invocations.",
+ "field_kind": "node_attribute",
+ "title": "Id",
"type": "string"
},
- "bytes": {
- "description": "Number of bytes downloaded so far",
- "title": "Bytes",
- "type": "integer"
+ "is_intermediate": {
+ "default": false,
+ "description": "Whether or not this is an intermediate invocation.",
+ "field_kind": "node_attribute",
+ "input": "direct",
+ "orig_required": true,
+ "title": "Is Intermediate",
+ "type": "boolean",
+ "ui_hidden": false,
+ "ui_type": "IsIntermediate"
},
- "total_bytes": {
- "description": "Total size of download, including all files",
- "title": "Total Bytes",
- "type": "integer"
+ "use_cache": {
+ "default": true,
+ "description": "Whether or not to use the cache",
+ "field_kind": "node_attribute",
+ "title": "Use Cache",
+ "type": "boolean"
},
- "parts": {
- "description": "Progress of downloading URLs that comprise the model, if any",
- "items": {
- "additionalProperties": {
- "anyOf": [
- {
- "type": "integer"
- },
- {
- "type": "string"
- }
- ]
+ "image": {
+ "anyOf": [
+ {
+ "$ref": "#/components/schemas/ImageField"
},
- "type": "object"
- },
- "title": "Parts",
- "type": "array"
+ {
+ "type": "null"
+ }
+ ],
+ "default": null,
+ "description": "Input image",
+ "field_kind": "input",
+ "input": "any",
+ "orig_required": true
+ },
+ "tile_size": {
+ "default": 512,
+ "description": "Tile size",
+ "field_kind": "input",
+ "input": "any",
+ "orig_default": 512,
+ "orig_required": false,
+ "title": "Tile Size",
+ "type": "integer"
+ },
+ "border_mode": {
+ "default": "none",
+ "description": "Border mode to apply to eliminate any artifacts or seams",
+ "enum": ["none", "seamless", "mirror", "replicate"],
+ "field_kind": "input",
+ "input": "any",
+ "orig_default": "none",
+ "orig_required": false,
+ "title": "Border Mode",
+ "type": "string"
+ },
+ "type": {
+ "const": "pbr_maps",
+ "default": "pbr_maps",
+ "field_kind": "node_attribute",
+ "title": "type",
+ "type": "string"
}
},
- "required": ["timestamp", "id", "source", "local_path", "bytes", "total_bytes", "parts"],
- "title": "ModelInstallDownloadStartedEvent",
- "type": "object"
+ "required": ["type", "id"],
+ "tags": ["image", "material"],
+ "title": "PBR Maps",
+ "type": "object",
+ "version": "1.0.0",
+ "output": {
+ "$ref": "#/components/schemas/PBRMapsOutput"
+ }
},
- "ModelInstallDownloadsCompleteEvent": {
- "description": "Emitted once when an install job becomes active.",
+ "PBRMapsOutput": {
+ "class": "output",
"properties": {
- "timestamp": {
- "description": "The timestamp of the event",
- "title": "Timestamp",
- "type": "integer"
+ "normal_map": {
+ "$ref": "#/components/schemas/ImageField",
+ "default": null,
+ "description": "The generated normal map",
+ "field_kind": "output",
+ "ui_hidden": false
},
- "id": {
- "description": "The ID of the install job",
- "title": "Id",
- "type": "integer"
+ "roughness_map": {
+ "$ref": "#/components/schemas/ImageField",
+ "default": null,
+ "description": "The generated roughness map",
+ "field_kind": "output",
+ "ui_hidden": false
},
- "source": {
- "description": "Source of the model; local path, repo_id or url",
- "discriminator": {
- "mapping": {
- "external": "#/components/schemas/ExternalModelSource",
- "hf": "#/components/schemas/HFModelSource",
- "local": "#/components/schemas/LocalModelSource",
- "url": "#/components/schemas/URLModelSource"
- },
- "propertyName": "type"
- },
- "oneOf": [
- {
- "$ref": "#/components/schemas/LocalModelSource"
- },
- {
- "$ref": "#/components/schemas/HFModelSource"
- },
- {
- "$ref": "#/components/schemas/URLModelSource"
- },
- {
- "$ref": "#/components/schemas/ExternalModelSource"
- }
- ],
- "title": "Source"
+ "displacement_map": {
+ "$ref": "#/components/schemas/ImageField",
+ "default": null,
+ "description": "The generated displacement map",
+ "field_kind": "output",
+ "ui_hidden": false
+ },
+ "type": {
+ "const": "pbr_maps-output",
+ "default": "pbr_maps-output",
+ "field_kind": "node_attribute",
+ "title": "type",
+ "type": "string"
}
},
- "required": ["timestamp", "id", "source"],
- "title": "ModelInstallDownloadsCompleteEvent",
+ "required": ["output_meta", "normal_map", "roughness_map", "displacement_map", "type", "type"],
+ "title": "PBRMapsOutput",
"type": "object"
},
- "ModelInstallErrorEvent": {
- "description": "Event model for model_install_error",
+ "PaginatedResults_WorkflowRecordListItemWithThumbnailDTO_": {
"properties": {
- "timestamp": {
- "description": "The timestamp of the event",
- "title": "Timestamp",
- "type": "integer"
+ "page": {
+ "type": "integer",
+ "title": "Page",
+ "description": "Current Page"
+ },
+ "pages": {
+ "type": "integer",
+ "title": "Pages",
+ "description": "Total number of pages"
+ },
+ "per_page": {
+ "type": "integer",
+ "title": "Per Page",
+ "description": "Number of items per page"
+ },
+ "total": {
+ "type": "integer",
+ "title": "Total",
+ "description": "Total number of items in result"
},
+ "items": {
+ "items": {
+ "$ref": "#/components/schemas/WorkflowRecordListItemWithThumbnailDTO"
+ },
+ "type": "array",
+ "title": "Items",
+ "description": "Items"
+ }
+ },
+ "type": "object",
+ "required": ["page", "pages", "per_page", "total", "items"],
+ "title": "PaginatedResults[WorkflowRecordListItemWithThumbnailDTO]"
+ },
+ "PairTileImageInvocation": {
+ "category": "tiles",
+ "class": "invocation",
+ "classification": "stable",
+ "description": "Pair an image with its tile properties.",
+ "node_pack": "invokeai",
+ "properties": {
"id": {
- "description": "The ID of the install job",
+ "description": "The id of this instance of an invocation. Must be unique among all instances of invocations.",
+ "field_kind": "node_attribute",
"title": "Id",
- "type": "integer"
+ "type": "string"
},
- "source": {
- "description": "Source of the model; local path, repo_id or url",
- "discriminator": {
- "mapping": {
- "external": "#/components/schemas/ExternalModelSource",
- "hf": "#/components/schemas/HFModelSource",
- "local": "#/components/schemas/LocalModelSource",
- "url": "#/components/schemas/URLModelSource"
- },
- "propertyName": "type"
- },
- "oneOf": [
+ "is_intermediate": {
+ "default": false,
+ "description": "Whether or not this is an intermediate invocation.",
+ "field_kind": "node_attribute",
+ "input": "direct",
+ "orig_required": true,
+ "title": "Is Intermediate",
+ "type": "boolean",
+ "ui_hidden": false,
+ "ui_type": "IsIntermediate"
+ },
+ "use_cache": {
+ "default": true,
+ "description": "Whether or not to use the cache",
+ "field_kind": "node_attribute",
+ "title": "Use Cache",
+ "type": "boolean"
+ },
+ "image": {
+ "anyOf": [
{
- "$ref": "#/components/schemas/LocalModelSource"
+ "$ref": "#/components/schemas/ImageField"
},
{
- "$ref": "#/components/schemas/HFModelSource"
- },
+ "type": "null"
+ }
+ ],
+ "default": null,
+ "description": "The tile image.",
+ "field_kind": "input",
+ "input": "any",
+ "orig_required": true
+ },
+ "tile": {
+ "anyOf": [
{
- "$ref": "#/components/schemas/URLModelSource"
+ "$ref": "#/components/schemas/Tile"
},
{
- "$ref": "#/components/schemas/ExternalModelSource"
+ "type": "null"
}
],
- "title": "Source"
+ "default": null,
+ "description": "The tile properties.",
+ "field_kind": "input",
+ "input": "any",
+ "orig_required": true
},
- "error_type": {
- "description": "The name of the exception",
- "title": "Error Type",
+ "type": {
+ "const": "pair_tile_image",
+ "default": "pair_tile_image",
+ "field_kind": "node_attribute",
+ "title": "type",
"type": "string"
+ }
+ },
+ "required": ["type", "id"],
+ "tags": ["tiles"],
+ "title": "Pair Tile with Image",
+ "type": "object",
+ "version": "1.0.1",
+ "output": {
+ "$ref": "#/components/schemas/PairTileImageOutput"
+ }
+ },
+ "PairTileImageOutput": {
+ "class": "output",
+ "properties": {
+ "tile_with_image": {
+ "$ref": "#/components/schemas/TileWithImage",
+ "description": "A tile description with its corresponding image.",
+ "field_kind": "output",
+ "ui_hidden": false
},
- "error": {
- "description": "A text description of the exception",
- "title": "Error",
+ "type": {
+ "const": "pair_tile_image_output",
+ "default": "pair_tile_image_output",
+ "field_kind": "node_attribute",
+ "title": "type",
"type": "string"
}
},
- "required": ["timestamp", "id", "source", "error_type", "error"],
- "title": "ModelInstallErrorEvent",
+ "required": ["output_meta", "tile_with_image", "type", "type"],
+ "title": "PairTileImageOutput",
"type": "object"
},
- "ModelInstallJob": {
+ "PasteImageIntoBoundingBoxInvocation": {
+ "category": "image",
+ "class": "invocation",
+ "classification": "stable",
+ "description": "Paste the source image into the target image at the given bounding box.\n\nThe source image must be the same size as the bounding box, and the bounding box must fit within the target image.",
+ "node_pack": "invokeai",
"properties": {
- "id": {
- "type": "integer",
- "title": "Id",
- "description": "Unique ID for this job"
- },
- "status": {
- "$ref": "#/components/schemas/InstallStatus",
- "description": "Current status of install process",
- "default": "waiting"
- },
- "error_reason": {
+ "board": {
"anyOf": [
{
- "type": "string"
+ "$ref": "#/components/schemas/BoardField"
},
{
"type": "null"
}
],
- "title": "Error Reason",
- "description": "Information about why the job failed"
- },
- "config_in": {
- "$ref": "#/components/schemas/ModelRecordChanges",
- "description": "Configuration information (e.g. 'description') to apply to model."
+ "default": null,
+ "description": "The board to save the image to",
+ "field_kind": "internal",
+ "input": "direct",
+ "orig_required": false,
+ "ui_hidden": false
},
- "config_out": {
+ "metadata": {
"anyOf": [
{
- "oneOf": [
- {
- "$ref": "#/components/schemas/Main_Diffusers_SD1_Config"
- },
- {
- "$ref": "#/components/schemas/Main_Diffusers_SD2_Config"
- },
- {
- "$ref": "#/components/schemas/Main_Diffusers_SDXL_Config"
- },
- {
- "$ref": "#/components/schemas/Main_Diffusers_SDXLRefiner_Config"
- },
- {
- "$ref": "#/components/schemas/Main_Diffusers_SD3_Config"
- },
- {
- "$ref": "#/components/schemas/Main_Diffusers_FLUX_Config"
- },
- {
- "$ref": "#/components/schemas/Main_Diffusers_Flux2_Config"
- },
- {
- "$ref": "#/components/schemas/Main_Diffusers_CogView4_Config"
- },
- {
- "$ref": "#/components/schemas/Main_Diffusers_QwenImage_Config"
- },
- {
- "$ref": "#/components/schemas/Main_Diffusers_ZImage_Config"
- },
- {
- "$ref": "#/components/schemas/Main_Checkpoint_SD1_Config"
- },
- {
- "$ref": "#/components/schemas/Main_Checkpoint_SD2_Config"
- },
- {
- "$ref": "#/components/schemas/Main_Checkpoint_SDXL_Config"
- },
- {
- "$ref": "#/components/schemas/Main_Checkpoint_SDXLRefiner_Config"
- },
- {
- "$ref": "#/components/schemas/Main_Checkpoint_Flux2_Config"
- },
- {
- "$ref": "#/components/schemas/Main_Checkpoint_FLUX_Config"
- },
- {
- "$ref": "#/components/schemas/Main_Checkpoint_QwenImage_Config"
- },
- {
- "$ref": "#/components/schemas/Main_Checkpoint_ZImage_Config"
- },
- {
- "$ref": "#/components/schemas/Main_Checkpoint_Anima_Config"
- },
- {
- "$ref": "#/components/schemas/Main_BnBNF4_FLUX_Config"
- },
- {
- "$ref": "#/components/schemas/Main_GGUF_Flux2_Config"
- },
- {
- "$ref": "#/components/schemas/Main_GGUF_FLUX_Config"
- },
- {
- "$ref": "#/components/schemas/Main_GGUF_QwenImage_Config"
- },
- {
- "$ref": "#/components/schemas/Main_GGUF_ZImage_Config"
- },
- {
- "$ref": "#/components/schemas/VAE_Checkpoint_SD1_Config"
- },
- {
- "$ref": "#/components/schemas/VAE_Checkpoint_SD2_Config"
- },
- {
- "$ref": "#/components/schemas/VAE_Checkpoint_SDXL_Config"
- },
- {
- "$ref": "#/components/schemas/VAE_Checkpoint_FLUX_Config"
- },
- {
- "$ref": "#/components/schemas/VAE_Checkpoint_Flux2_Config"
- },
- {
- "$ref": "#/components/schemas/VAE_Checkpoint_QwenImage_Config"
- },
- {
- "$ref": "#/components/schemas/VAE_Checkpoint_Anima_Config"
- },
- {
- "$ref": "#/components/schemas/VAE_Diffusers_SD1_Config"
- },
- {
- "$ref": "#/components/schemas/VAE_Diffusers_SDXL_Config"
- },
- {
- "$ref": "#/components/schemas/VAE_Diffusers_Flux2_Config"
- },
- {
- "$ref": "#/components/schemas/ControlNet_Checkpoint_SD1_Config"
- },
- {
- "$ref": "#/components/schemas/ControlNet_Checkpoint_SD2_Config"
- },
- {
- "$ref": "#/components/schemas/ControlNet_Checkpoint_SDXL_Config"
- },
- {
- "$ref": "#/components/schemas/ControlNet_Checkpoint_FLUX_Config"
- },
- {
- "$ref": "#/components/schemas/ControlNet_Checkpoint_ZImage_Config"
- },
- {
- "$ref": "#/components/schemas/ControlNet_Checkpoint_Anima_Config"
- },
- {
- "$ref": "#/components/schemas/ControlNet_Diffusers_SD1_Config"
- },
- {
- "$ref": "#/components/schemas/ControlNet_Diffusers_SD2_Config"
- },
- {
- "$ref": "#/components/schemas/ControlNet_Diffusers_SDXL_Config"
- },
- {
- "$ref": "#/components/schemas/ControlNet_Diffusers_FLUX_Config"
- },
- {
- "$ref": "#/components/schemas/LoRA_LyCORIS_SD1_Config"
- },
- {
- "$ref": "#/components/schemas/LoRA_LyCORIS_SD2_Config"
- },
- {
- "$ref": "#/components/schemas/LoRA_LyCORIS_SDXL_Config"
- },
- {
- "$ref": "#/components/schemas/LoRA_LyCORIS_Flux2_Config"
- },
- {
- "$ref": "#/components/schemas/LoRA_LyCORIS_FLUX_Config"
- },
- {
- "$ref": "#/components/schemas/LoRA_LyCORIS_ZImage_Config"
- },
- {
- "$ref": "#/components/schemas/LoRA_LyCORIS_QwenImage_Config"
- },
- {
- "$ref": "#/components/schemas/LoRA_LyCORIS_Anima_Config"
- },
- {
- "$ref": "#/components/schemas/LoRA_OMI_SDXL_Config"
- },
- {
- "$ref": "#/components/schemas/LoRA_OMI_FLUX_Config"
- },
- {
- "$ref": "#/components/schemas/LoRA_Diffusers_SD1_Config"
- },
- {
- "$ref": "#/components/schemas/LoRA_Diffusers_SD2_Config"
- },
- {
- "$ref": "#/components/schemas/LoRA_Diffusers_SDXL_Config"
- },
- {
- "$ref": "#/components/schemas/LoRA_Diffusers_Flux2_Config"
- },
- {
- "$ref": "#/components/schemas/LoRA_Diffusers_FLUX_Config"
- },
- {
- "$ref": "#/components/schemas/LoRA_Diffusers_ZImage_Config"
- },
- {
- "$ref": "#/components/schemas/ControlLoRA_LyCORIS_FLUX_Config"
- },
- {
- "$ref": "#/components/schemas/T5Encoder_T5Encoder_Config"
- },
- {
- "$ref": "#/components/schemas/T5Encoder_BnBLLMint8_Config"
- },
- {
- "$ref": "#/components/schemas/Qwen3Encoder_Qwen3Encoder_Config"
- },
- {
- "$ref": "#/components/schemas/Qwen3Encoder_Checkpoint_Config"
- },
- {
- "$ref": "#/components/schemas/Qwen3Encoder_GGUF_Config"
- },
- {
- "$ref": "#/components/schemas/QwenVLEncoder_Diffusers_Config"
- },
- {
- "$ref": "#/components/schemas/QwenVLEncoder_Checkpoint_Config"
- },
- {
- "$ref": "#/components/schemas/TI_File_SD1_Config"
- },
- {
- "$ref": "#/components/schemas/TI_File_SD2_Config"
- },
- {
- "$ref": "#/components/schemas/TI_File_SDXL_Config"
- },
- {
- "$ref": "#/components/schemas/TI_Folder_SD1_Config"
- },
- {
- "$ref": "#/components/schemas/TI_Folder_SD2_Config"
- },
- {
- "$ref": "#/components/schemas/TI_Folder_SDXL_Config"
- },
- {
- "$ref": "#/components/schemas/IPAdapter_InvokeAI_SD1_Config"
- },
- {
- "$ref": "#/components/schemas/IPAdapter_InvokeAI_SD2_Config"
- },
- {
- "$ref": "#/components/schemas/IPAdapter_InvokeAI_SDXL_Config"
- },
- {
- "$ref": "#/components/schemas/IPAdapter_Checkpoint_SD1_Config"
- },
- {
- "$ref": "#/components/schemas/IPAdapter_Checkpoint_SD2_Config"
- },
- {
- "$ref": "#/components/schemas/IPAdapter_Checkpoint_SDXL_Config"
- },
- {
- "$ref": "#/components/schemas/IPAdapter_Checkpoint_FLUX_Config"
- },
- {
- "$ref": "#/components/schemas/T2IAdapter_Diffusers_SD1_Config"
- },
- {
- "$ref": "#/components/schemas/T2IAdapter_Diffusers_SDXL_Config"
- },
- {
- "$ref": "#/components/schemas/Spandrel_Checkpoint_Config"
- },
- {
- "$ref": "#/components/schemas/CLIPEmbed_Diffusers_G_Config"
- },
- {
- "$ref": "#/components/schemas/CLIPEmbed_Diffusers_L_Config"
- },
- {
- "$ref": "#/components/schemas/CLIPVision_Diffusers_Config"
- },
- {
- "$ref": "#/components/schemas/SigLIP_Diffusers_Config"
- },
- {
- "$ref": "#/components/schemas/FLUXRedux_Checkpoint_Config"
- },
- {
- "$ref": "#/components/schemas/LlavaOnevision_Diffusers_Config"
- },
- {
- "$ref": "#/components/schemas/TextLLM_Diffusers_Config"
- },
- {
- "$ref": "#/components/schemas/ExternalApiModelConfig"
- },
- {
- "$ref": "#/components/schemas/Unknown_Config"
- }
- ]
+ "$ref": "#/components/schemas/MetadataField"
},
{
"type": "null"
}
],
- "title": "Config Out",
- "description": "After successful installation, this will hold the configuration object."
+ "default": null,
+ "description": "Optional metadata to be saved with the image",
+ "field_kind": "internal",
+ "input": "connection",
+ "orig_required": false,
+ "ui_hidden": false
},
- "inplace": {
+ "id": {
+ "description": "The id of this instance of an invocation. Must be unique among all instances of invocations.",
+ "field_kind": "node_attribute",
+ "title": "Id",
+ "type": "string"
+ },
+ "is_intermediate": {
+ "default": false,
+ "description": "Whether or not this is an intermediate invocation.",
+ "field_kind": "node_attribute",
+ "input": "direct",
+ "orig_required": true,
+ "title": "Is Intermediate",
"type": "boolean",
- "title": "Inplace",
- "description": "Leave model in its current location; otherwise install under models directory",
- "default": false
+ "ui_hidden": false,
+ "ui_type": "IsIntermediate"
},
- "source": {
- "oneOf": [
+ "use_cache": {
+ "default": true,
+ "description": "Whether or not to use the cache",
+ "field_kind": "node_attribute",
+ "title": "Use Cache",
+ "type": "boolean"
+ },
+ "source_image": {
+ "anyOf": [
{
- "$ref": "#/components/schemas/LocalModelSource"
+ "$ref": "#/components/schemas/ImageField"
},
{
- "$ref": "#/components/schemas/HFModelSource"
- },
+ "type": "null"
+ }
+ ],
+ "default": null,
+ "description": "The image to paste",
+ "field_kind": "input",
+ "input": "any",
+ "orig_required": true
+ },
+ "target_image": {
+ "anyOf": [
{
- "$ref": "#/components/schemas/URLModelSource"
+ "$ref": "#/components/schemas/ImageField"
},
{
- "$ref": "#/components/schemas/ExternalModelSource"
+ "type": "null"
}
],
- "title": "Source",
- "description": "Source (URL, repo_id, or local path) of model",
- "discriminator": {
- "propertyName": "type",
- "mapping": {
- "external": "#/components/schemas/ExternalModelSource",
- "hf": "#/components/schemas/HFModelSource",
- "local": "#/components/schemas/LocalModelSource",
- "url": "#/components/schemas/URLModelSource"
- }
- }
- },
- "local_path": {
- "type": "string",
- "format": "path",
- "title": "Local Path",
- "description": "Path to locally-downloaded model; may be the same as the source"
- },
- "bytes": {
- "type": "integer",
- "title": "Bytes",
- "description": "For a remote model, the number of bytes downloaded so far (may not be available)",
- "default": 0
- },
- "total_bytes": {
- "type": "integer",
- "title": "Total Bytes",
- "description": "Total size of the model to be installed",
- "default": 0
+ "default": null,
+ "description": "The image to paste into",
+ "field_kind": "input",
+ "input": "any",
+ "orig_required": true
},
- "source_metadata": {
+ "bounding_box": {
"anyOf": [
{
- "oneOf": [
- {
- "$ref": "#/components/schemas/BaseMetadata"
- },
- {
- "$ref": "#/components/schemas/HuggingFaceMetadata"
- }
- ],
- "discriminator": {
- "propertyName": "type",
- "mapping": {
- "basemetadata": "#/components/schemas/BaseMetadata",
- "huggingface": "#/components/schemas/HuggingFaceMetadata"
- }
- }
+ "$ref": "#/components/schemas/BoundingBoxField"
+ },
+ {
+ "type": "null"
+ }
+ ],
+ "default": null,
+ "description": "The bounding box to paste the image into",
+ "field_kind": "input",
+ "input": "any",
+ "orig_required": true
+ },
+ "type": {
+ "const": "paste_image_into_bounding_box",
+ "default": "paste_image_into_bounding_box",
+ "field_kind": "node_attribute",
+ "title": "type",
+ "type": "string"
+ }
+ },
+ "required": ["type", "id"],
+ "tags": ["image", "crop"],
+ "title": "Paste Image into Bounding Box",
+ "type": "object",
+ "version": "1.0.0",
+ "output": {
+ "$ref": "#/components/schemas/ImageOutput"
+ }
+ },
+ "PiDiNetEdgeDetectionInvocation": {
+ "category": "controlnet_preprocessors",
+ "class": "invocation",
+ "classification": "stable",
+ "description": "Generates an edge map using PiDiNet.",
+ "node_pack": "invokeai",
+ "properties": {
+ "board": {
+ "anyOf": [
+ {
+ "$ref": "#/components/schemas/BoardField"
},
{
"type": "null"
}
],
- "title": "Source Metadata",
- "description": "Metadata provided by the model source"
- },
- "download_parts": {
- "items": {
- "$ref": "#/components/schemas/DownloadJob"
- },
- "type": "array",
- "uniqueItems": true,
- "title": "Download Parts",
- "description": "Download jobs contributing to this install"
+ "default": null,
+ "description": "The board to save the image to",
+ "field_kind": "internal",
+ "input": "direct",
+ "orig_required": false,
+ "ui_hidden": false
},
- "error": {
+ "metadata": {
"anyOf": [
{
- "type": "string"
+ "$ref": "#/components/schemas/MetadataField"
},
{
"type": "null"
}
],
- "title": "Error",
- "description": "On an error condition, this field will contain the text of the exception"
+ "default": null,
+ "description": "Optional metadata to be saved with the image",
+ "field_kind": "internal",
+ "input": "connection",
+ "orig_required": false,
+ "ui_hidden": false
},
- "error_traceback": {
+ "id": {
+ "description": "The id of this instance of an invocation. Must be unique among all instances of invocations.",
+ "field_kind": "node_attribute",
+ "title": "Id",
+ "type": "string"
+ },
+ "is_intermediate": {
+ "default": false,
+ "description": "Whether or not this is an intermediate invocation.",
+ "field_kind": "node_attribute",
+ "input": "direct",
+ "orig_required": true,
+ "title": "Is Intermediate",
+ "type": "boolean",
+ "ui_hidden": false,
+ "ui_type": "IsIntermediate"
+ },
+ "use_cache": {
+ "default": true,
+ "description": "Whether or not to use the cache",
+ "field_kind": "node_attribute",
+ "title": "Use Cache",
+ "type": "boolean"
+ },
+ "image": {
"anyOf": [
{
- "type": "string"
+ "$ref": "#/components/schemas/ImageField"
},
{
"type": "null"
}
],
- "title": "Error Traceback",
- "description": "On an error condition, this field will contain the exception traceback"
+ "default": null,
+ "description": "The image to process",
+ "field_kind": "input",
+ "input": "any",
+ "orig_required": true
+ },
+ "quantize_edges": {
+ "default": false,
+ "description": "Whether or not to use safe mode",
+ "field_kind": "input",
+ "input": "any",
+ "orig_default": false,
+ "orig_required": false,
+ "title": "Quantize Edges",
+ "type": "boolean"
+ },
+ "scribble": {
+ "default": false,
+ "description": "Whether or not to use scribble mode",
+ "field_kind": "input",
+ "input": "any",
+ "orig_default": false,
+ "orig_required": false,
+ "title": "Scribble",
+ "type": "boolean"
+ },
+ "type": {
+ "const": "pidi_edge_detection",
+ "default": "pidi_edge_detection",
+ "field_kind": "node_attribute",
+ "title": "type",
+ "type": "string"
}
},
+ "required": ["type", "id"],
+ "tags": ["controlnet", "edge"],
+ "title": "PiDiNet Edge Detection",
"type": "object",
- "required": ["id", "source", "local_path"],
- "title": "ModelInstallJob",
- "description": "Object that tracks the current status of an install request."
+ "version": "1.0.0",
+ "output": {
+ "$ref": "#/components/schemas/ImageOutput"
+ }
},
- "ModelInstallStartedEvent": {
- "description": "Event model for model_install_started",
+ "PresetData": {
"properties": {
- "timestamp": {
- "description": "The timestamp of the event",
- "title": "Timestamp",
+ "positive_prompt": {
+ "type": "string",
+ "title": "Positive Prompt",
+ "description": "Positive prompt"
+ },
+ "negative_prompt": {
+ "type": "string",
+ "title": "Negative Prompt",
+ "description": "Negative prompt"
+ }
+ },
+ "additionalProperties": false,
+ "type": "object",
+ "required": ["positive_prompt", "negative_prompt"],
+ "title": "PresetData"
+ },
+ "PresetType": {
+ "type": "string",
+ "enum": ["user", "default"],
+ "title": "PresetType"
+ },
+ "ProgressImage": {
+ "description": "The progress image sent intermittently during processing",
+ "properties": {
+ "width": {
+ "description": "The effective width of the image in pixels",
+ "minimum": 1,
+ "title": "Width",
+ "type": "integer"
+ },
+ "height": {
+ "description": "The effective height of the image in pixels",
+ "minimum": 1,
+ "title": "Height",
"type": "integer"
},
+ "dataURL": {
+ "description": "The image data as a b64 data URL",
+ "title": "Dataurl",
+ "type": "string"
+ }
+ },
+ "required": ["width", "height", "dataURL"],
+ "title": "ProgressImage",
+ "type": "object"
+ },
+ "PromptTemplateInvocation": {
+ "category": "prompt",
+ "class": "invocation",
+ "classification": "stable",
+ "description": "Applies a Style Preset template to positive and negative prompts.\n\nSelect a Style Preset and provide positive/negative prompts. The node replaces\n{prompt} placeholders in the template with your input prompts.",
+ "node_pack": "invokeai",
+ "properties": {
"id": {
- "description": "The ID of the install job",
+ "description": "The id of this instance of an invocation. Must be unique among all instances of invocations.",
+ "field_kind": "node_attribute",
"title": "Id",
- "type": "integer"
+ "type": "string"
},
- "source": {
- "description": "Source of the model; local path, repo_id or url",
- "discriminator": {
- "mapping": {
- "external": "#/components/schemas/ExternalModelSource",
- "hf": "#/components/schemas/HFModelSource",
- "local": "#/components/schemas/LocalModelSource",
- "url": "#/components/schemas/URLModelSource"
- },
- "propertyName": "type"
- },
- "oneOf": [
- {
- "$ref": "#/components/schemas/LocalModelSource"
- },
- {
- "$ref": "#/components/schemas/HFModelSource"
- },
+ "is_intermediate": {
+ "default": false,
+ "description": "Whether or not this is an intermediate invocation.",
+ "field_kind": "node_attribute",
+ "input": "direct",
+ "orig_required": true,
+ "title": "Is Intermediate",
+ "type": "boolean",
+ "ui_hidden": false,
+ "ui_type": "IsIntermediate"
+ },
+ "use_cache": {
+ "default": true,
+ "description": "Whether or not to use the cache",
+ "field_kind": "node_attribute",
+ "title": "Use Cache",
+ "type": "boolean"
+ },
+ "style_preset": {
+ "anyOf": [
{
- "$ref": "#/components/schemas/URLModelSource"
+ "$ref": "#/components/schemas/StylePresetField"
},
{
- "$ref": "#/components/schemas/ExternalModelSource"
+ "type": "null"
}
],
- "title": "Source"
+ "default": null,
+ "description": "The Style Preset to use as a template",
+ "field_kind": "input",
+ "input": "any",
+ "orig_required": true
+ },
+ "positive_prompt": {
+ "default": "",
+ "description": "The positive prompt to insert into the template's {prompt} placeholder",
+ "field_kind": "input",
+ "input": "any",
+ "orig_default": "",
+ "orig_required": false,
+ "title": "Positive Prompt",
+ "type": "string",
+ "ui_component": "textarea"
+ },
+ "negative_prompt": {
+ "default": "",
+ "description": "The negative prompt to insert into the template's {prompt} placeholder",
+ "field_kind": "input",
+ "input": "any",
+ "orig_default": "",
+ "orig_required": false,
+ "title": "Negative Prompt",
+ "type": "string",
+ "ui_component": "textarea"
+ },
+ "type": {
+ "const": "prompt_template",
+ "default": "prompt_template",
+ "field_kind": "node_attribute",
+ "title": "type",
+ "type": "string"
}
},
- "required": ["timestamp", "id", "source"],
- "title": "ModelInstallStartedEvent",
- "type": "object"
+ "required": ["type", "id"],
+ "tags": ["prompt", "template", "style", "preset"],
+ "title": "Prompt Template",
+ "type": "object",
+ "version": "1.0.0",
+ "output": {
+ "$ref": "#/components/schemas/PromptTemplateOutput"
+ }
},
- "ModelLoadCompleteEvent": {
- "description": "Event model for model_load_complete",
+ "PromptTemplateOutput": {
+ "class": "output",
+ "description": "Output for the Prompt Template node",
"properties": {
- "timestamp": {
- "description": "The timestamp of the event",
- "title": "Timestamp",
- "type": "integer"
+ "positive_prompt": {
+ "description": "The positive prompt with the template applied",
+ "field_kind": "output",
+ "title": "Positive Prompt",
+ "type": "string",
+ "ui_hidden": false
},
- "config": {
- "description": "The model's config",
- "oneOf": [
- {
- "$ref": "#/components/schemas/Main_Diffusers_SD1_Config"
- },
- {
- "$ref": "#/components/schemas/Main_Diffusers_SD2_Config"
- },
- {
- "$ref": "#/components/schemas/Main_Diffusers_SDXL_Config"
- },
- {
- "$ref": "#/components/schemas/Main_Diffusers_SDXLRefiner_Config"
- },
- {
- "$ref": "#/components/schemas/Main_Diffusers_SD3_Config"
- },
- {
- "$ref": "#/components/schemas/Main_Diffusers_FLUX_Config"
- },
- {
- "$ref": "#/components/schemas/Main_Diffusers_Flux2_Config"
- },
- {
- "$ref": "#/components/schemas/Main_Diffusers_CogView4_Config"
- },
- {
- "$ref": "#/components/schemas/Main_Diffusers_QwenImage_Config"
- },
- {
- "$ref": "#/components/schemas/Main_Diffusers_ZImage_Config"
- },
- {
- "$ref": "#/components/schemas/Main_Checkpoint_SD1_Config"
- },
- {
- "$ref": "#/components/schemas/Main_Checkpoint_SD2_Config"
- },
- {
- "$ref": "#/components/schemas/Main_Checkpoint_SDXL_Config"
- },
- {
- "$ref": "#/components/schemas/Main_Checkpoint_SDXLRefiner_Config"
- },
- {
- "$ref": "#/components/schemas/Main_Checkpoint_Flux2_Config"
- },
- {
- "$ref": "#/components/schemas/Main_Checkpoint_FLUX_Config"
- },
- {
- "$ref": "#/components/schemas/Main_Checkpoint_QwenImage_Config"
- },
- {
- "$ref": "#/components/schemas/Main_Checkpoint_ZImage_Config"
- },
- {
- "$ref": "#/components/schemas/Main_Checkpoint_Anima_Config"
- },
- {
- "$ref": "#/components/schemas/Main_BnBNF4_FLUX_Config"
- },
- {
- "$ref": "#/components/schemas/Main_GGUF_Flux2_Config"
- },
- {
- "$ref": "#/components/schemas/Main_GGUF_FLUX_Config"
- },
- {
- "$ref": "#/components/schemas/Main_GGUF_QwenImage_Config"
- },
- {
- "$ref": "#/components/schemas/Main_GGUF_ZImage_Config"
- },
- {
- "$ref": "#/components/schemas/VAE_Checkpoint_SD1_Config"
- },
- {
- "$ref": "#/components/schemas/VAE_Checkpoint_SD2_Config"
- },
- {
- "$ref": "#/components/schemas/VAE_Checkpoint_SDXL_Config"
- },
- {
- "$ref": "#/components/schemas/VAE_Checkpoint_FLUX_Config"
- },
- {
- "$ref": "#/components/schemas/VAE_Checkpoint_Flux2_Config"
- },
- {
- "$ref": "#/components/schemas/VAE_Checkpoint_QwenImage_Config"
- },
- {
- "$ref": "#/components/schemas/VAE_Checkpoint_Anima_Config"
- },
- {
- "$ref": "#/components/schemas/VAE_Diffusers_SD1_Config"
- },
- {
- "$ref": "#/components/schemas/VAE_Diffusers_SDXL_Config"
- },
- {
- "$ref": "#/components/schemas/VAE_Diffusers_Flux2_Config"
- },
- {
- "$ref": "#/components/schemas/ControlNet_Checkpoint_SD1_Config"
- },
- {
- "$ref": "#/components/schemas/ControlNet_Checkpoint_SD2_Config"
- },
- {
- "$ref": "#/components/schemas/ControlNet_Checkpoint_SDXL_Config"
- },
- {
- "$ref": "#/components/schemas/ControlNet_Checkpoint_FLUX_Config"
- },
- {
- "$ref": "#/components/schemas/ControlNet_Checkpoint_ZImage_Config"
- },
- {
- "$ref": "#/components/schemas/ControlNet_Checkpoint_Anima_Config"
- },
- {
- "$ref": "#/components/schemas/ControlNet_Diffusers_SD1_Config"
- },
- {
- "$ref": "#/components/schemas/ControlNet_Diffusers_SD2_Config"
- },
- {
- "$ref": "#/components/schemas/ControlNet_Diffusers_SDXL_Config"
- },
- {
- "$ref": "#/components/schemas/ControlNet_Diffusers_FLUX_Config"
- },
- {
- "$ref": "#/components/schemas/LoRA_LyCORIS_SD1_Config"
- },
- {
- "$ref": "#/components/schemas/LoRA_LyCORIS_SD2_Config"
- },
- {
- "$ref": "#/components/schemas/LoRA_LyCORIS_SDXL_Config"
- },
- {
- "$ref": "#/components/schemas/LoRA_LyCORIS_Flux2_Config"
- },
- {
- "$ref": "#/components/schemas/LoRA_LyCORIS_FLUX_Config"
- },
- {
- "$ref": "#/components/schemas/LoRA_LyCORIS_ZImage_Config"
- },
- {
- "$ref": "#/components/schemas/LoRA_LyCORIS_QwenImage_Config"
- },
- {
- "$ref": "#/components/schemas/LoRA_LyCORIS_Anima_Config"
- },
- {
- "$ref": "#/components/schemas/LoRA_OMI_SDXL_Config"
- },
- {
- "$ref": "#/components/schemas/LoRA_OMI_FLUX_Config"
- },
- {
- "$ref": "#/components/schemas/LoRA_Diffusers_SD1_Config"
- },
- {
- "$ref": "#/components/schemas/LoRA_Diffusers_SD2_Config"
- },
- {
- "$ref": "#/components/schemas/LoRA_Diffusers_SDXL_Config"
- },
- {
- "$ref": "#/components/schemas/LoRA_Diffusers_Flux2_Config"
- },
- {
- "$ref": "#/components/schemas/LoRA_Diffusers_FLUX_Config"
- },
- {
- "$ref": "#/components/schemas/LoRA_Diffusers_ZImage_Config"
- },
- {
- "$ref": "#/components/schemas/ControlLoRA_LyCORIS_FLUX_Config"
- },
- {
- "$ref": "#/components/schemas/T5Encoder_T5Encoder_Config"
- },
- {
- "$ref": "#/components/schemas/T5Encoder_BnBLLMint8_Config"
- },
- {
- "$ref": "#/components/schemas/Qwen3Encoder_Qwen3Encoder_Config"
- },
- {
- "$ref": "#/components/schemas/Qwen3Encoder_Checkpoint_Config"
- },
- {
- "$ref": "#/components/schemas/Qwen3Encoder_GGUF_Config"
- },
- {
- "$ref": "#/components/schemas/QwenVLEncoder_Diffusers_Config"
- },
- {
- "$ref": "#/components/schemas/QwenVLEncoder_Checkpoint_Config"
- },
- {
- "$ref": "#/components/schemas/TI_File_SD1_Config"
- },
- {
- "$ref": "#/components/schemas/TI_File_SD2_Config"
- },
- {
- "$ref": "#/components/schemas/TI_File_SDXL_Config"
- },
+ "negative_prompt": {
+ "description": "The negative prompt with the template applied",
+ "field_kind": "output",
+ "title": "Negative Prompt",
+ "type": "string",
+ "ui_hidden": false
+ },
+ "type": {
+ "const": "prompt_template_output",
+ "default": "prompt_template_output",
+ "field_kind": "node_attribute",
+ "title": "type",
+ "type": "string"
+ }
+ },
+ "required": ["output_meta", "positive_prompt", "negative_prompt", "type", "type"],
+ "title": "PromptTemplateOutput",
+ "type": "object"
+ },
+ "PromptsFromFileInvocation": {
+ "category": "prompt",
+ "class": "invocation",
+ "classification": "stable",
+ "description": "Loads prompts from a text file",
+ "node_pack": "invokeai",
+ "properties": {
+ "id": {
+ "description": "The id of this instance of an invocation. Must be unique among all instances of invocations.",
+ "field_kind": "node_attribute",
+ "title": "Id",
+ "type": "string"
+ },
+ "is_intermediate": {
+ "default": false,
+ "description": "Whether or not this is an intermediate invocation.",
+ "field_kind": "node_attribute",
+ "input": "direct",
+ "orig_required": true,
+ "title": "Is Intermediate",
+ "type": "boolean",
+ "ui_hidden": false,
+ "ui_type": "IsIntermediate"
+ },
+ "use_cache": {
+ "default": true,
+ "description": "Whether or not to use the cache",
+ "field_kind": "node_attribute",
+ "title": "Use Cache",
+ "type": "boolean"
+ },
+ "file_path": {
+ "anyOf": [
{
- "$ref": "#/components/schemas/TI_Folder_SD1_Config"
+ "type": "string"
},
{
- "$ref": "#/components/schemas/TI_Folder_SD2_Config"
- },
+ "type": "null"
+ }
+ ],
+ "default": null,
+ "description": "Path to prompt text file",
+ "field_kind": "input",
+ "input": "any",
+ "orig_required": true,
+ "title": "File Path"
+ },
+ "pre_prompt": {
+ "anyOf": [
{
- "$ref": "#/components/schemas/TI_Folder_SDXL_Config"
+ "type": "string"
},
{
- "$ref": "#/components/schemas/IPAdapter_InvokeAI_SD1_Config"
- },
+ "type": "null"
+ }
+ ],
+ "default": null,
+ "description": "String to prepend to each prompt",
+ "field_kind": "input",
+ "input": "any",
+ "orig_default": null,
+ "orig_required": false,
+ "title": "Pre Prompt",
+ "ui_component": "textarea"
+ },
+ "post_prompt": {
+ "anyOf": [
{
- "$ref": "#/components/schemas/IPAdapter_InvokeAI_SD2_Config"
+ "type": "string"
},
{
- "$ref": "#/components/schemas/IPAdapter_InvokeAI_SDXL_Config"
- },
+ "type": "null"
+ }
+ ],
+ "default": null,
+ "description": "String to append to each prompt",
+ "field_kind": "input",
+ "input": "any",
+ "orig_default": null,
+ "orig_required": false,
+ "title": "Post Prompt",
+ "ui_component": "textarea"
+ },
+ "start_line": {
+ "default": 1,
+ "description": "Line in the file to start start from",
+ "field_kind": "input",
+ "input": "any",
+ "minimum": 1,
+ "orig_default": 1,
+ "orig_required": false,
+ "title": "Start Line",
+ "type": "integer"
+ },
+ "max_prompts": {
+ "default": 1,
+ "description": "Max lines to read from file (0=all)",
+ "field_kind": "input",
+ "input": "any",
+ "minimum": 0,
+ "orig_default": 1,
+ "orig_required": false,
+ "title": "Max Prompts",
+ "type": "integer"
+ },
+ "type": {
+ "const": "prompt_from_file",
+ "default": "prompt_from_file",
+ "field_kind": "node_attribute",
+ "title": "type",
+ "type": "string"
+ }
+ },
+ "required": ["type", "id"],
+ "tags": ["prompt", "file"],
+ "title": "Prompts from File",
+ "type": "object",
+ "version": "1.0.2",
+ "output": {
+ "$ref": "#/components/schemas/StringCollectionOutput"
+ }
+ },
+ "PruneResult": {
+ "properties": {
+ "deleted": {
+ "type": "integer",
+ "title": "Deleted",
+ "description": "Number of queue items deleted"
+ }
+ },
+ "type": "object",
+ "required": ["deleted"],
+ "title": "PruneResult",
+ "description": "Result of pruning the session queue"
+ },
+ "QueueClearedEvent": {
+ "description": "Event model for queue_cleared",
+ "properties": {
+ "timestamp": {
+ "description": "The timestamp of the event",
+ "title": "Timestamp",
+ "type": "integer"
+ },
+ "queue_id": {
+ "description": "The ID of the queue",
+ "title": "Queue Id",
+ "type": "string"
+ },
+ "user_id": {
+ "anyOf": [
{
- "$ref": "#/components/schemas/IPAdapter_Checkpoint_SD1_Config"
+ "type": "string"
},
{
- "$ref": "#/components/schemas/IPAdapter_Checkpoint_SD2_Config"
- },
+ "type": "null"
+ }
+ ],
+ "default": null,
+ "description": "The ID of the user whose queue items were cleared, or None if all users' items were cleared",
+ "title": "User Id"
+ }
+ },
+ "required": ["timestamp", "queue_id", "user_id"],
+ "title": "QueueClearedEvent",
+ "type": "object"
+ },
+ "QueueItemStatusChangedEvent": {
+ "description": "Event model for queue_item_status_changed",
+ "properties": {
+ "timestamp": {
+ "description": "The timestamp of the event",
+ "title": "Timestamp",
+ "type": "integer"
+ },
+ "queue_id": {
+ "description": "The ID of the queue",
+ "title": "Queue Id",
+ "type": "string"
+ },
+ "item_id": {
+ "description": "The ID of the queue item",
+ "title": "Item Id",
+ "type": "integer"
+ },
+ "batch_id": {
+ "description": "The ID of the queue batch",
+ "title": "Batch Id",
+ "type": "string"
+ },
+ "origin": {
+ "anyOf": [
{
- "$ref": "#/components/schemas/IPAdapter_Checkpoint_SDXL_Config"
+ "type": "string"
},
{
- "$ref": "#/components/schemas/IPAdapter_Checkpoint_FLUX_Config"
- },
+ "type": "null"
+ }
+ ],
+ "default": null,
+ "description": "The origin of the queue item",
+ "title": "Origin"
+ },
+ "destination": {
+ "anyOf": [
{
- "$ref": "#/components/schemas/T2IAdapter_Diffusers_SD1_Config"
+ "type": "string"
},
{
- "$ref": "#/components/schemas/T2IAdapter_Diffusers_SDXL_Config"
- },
+ "type": "null"
+ }
+ ],
+ "default": null,
+ "description": "The destination of the queue item",
+ "title": "Destination"
+ },
+ "user_id": {
+ "default": "system",
+ "description": "The ID of the user who created the queue item",
+ "title": "User Id",
+ "type": "string"
+ },
+ "status": {
+ "description": "The new status of the queue item",
+ "enum": ["pending", "in_progress", "waiting", "completed", "failed", "canceled"],
+ "title": "Status",
+ "type": "string"
+ },
+ "status_sequence": {
+ "anyOf": [
{
- "$ref": "#/components/schemas/Spandrel_Checkpoint_Config"
+ "type": "integer"
},
{
- "$ref": "#/components/schemas/CLIPEmbed_Diffusers_G_Config"
- },
+ "type": "null"
+ }
+ ],
+ "default": null,
+ "description": "A monotonically increasing version for this queue item's visible status lifecycle",
+ "title": "Status Sequence"
+ },
+ "error_type": {
+ "anyOf": [
{
- "$ref": "#/components/schemas/CLIPEmbed_Diffusers_L_Config"
+ "type": "string"
},
{
- "$ref": "#/components/schemas/CLIPVision_Diffusers_Config"
- },
+ "type": "null"
+ }
+ ],
+ "default": null,
+ "description": "The error type, if any",
+ "title": "Error Type"
+ },
+ "error_message": {
+ "anyOf": [
{
- "$ref": "#/components/schemas/SigLIP_Diffusers_Config"
+ "type": "string"
},
{
- "$ref": "#/components/schemas/FLUXRedux_Checkpoint_Config"
- },
+ "type": "null"
+ }
+ ],
+ "default": null,
+ "description": "The error message, if any",
+ "title": "Error Message"
+ },
+ "error_traceback": {
+ "anyOf": [
{
- "$ref": "#/components/schemas/LlavaOnevision_Diffusers_Config"
+ "type": "string"
},
{
- "$ref": "#/components/schemas/TextLLM_Diffusers_Config"
- },
+ "type": "null"
+ }
+ ],
+ "default": null,
+ "description": "The error traceback, if any",
+ "title": "Error Traceback"
+ },
+ "created_at": {
+ "description": "The timestamp when the queue item was created",
+ "title": "Created At",
+ "type": "string"
+ },
+ "updated_at": {
+ "description": "The timestamp when the queue item was last updated",
+ "title": "Updated At",
+ "type": "string"
+ },
+ "started_at": {
+ "anyOf": [
{
- "$ref": "#/components/schemas/ExternalApiModelConfig"
+ "type": "string"
},
{
- "$ref": "#/components/schemas/Unknown_Config"
+ "type": "null"
}
],
- "title": "Config"
+ "default": null,
+ "description": "The timestamp when the queue item was started",
+ "title": "Started At"
},
- "submodel_type": {
+ "completed_at": {
"anyOf": [
{
- "$ref": "#/components/schemas/SubModelType"
+ "type": "string"
},
{
"type": "null"
}
],
"default": null,
- "description": "The submodel type, if any"
+ "description": "The timestamp when the queue item was completed",
+ "title": "Completed At"
},
- "user_id": {
- "default": "system",
- "description": "The ID of the user whose action triggered the load",
- "title": "User Id",
+ "batch_status": {
+ "$ref": "#/components/schemas/BatchStatus",
+ "description": "The status of the batch"
+ },
+ "queue_status": {
+ "$ref": "#/components/schemas/SessionQueueStatus",
+ "description": "The status of the queue"
+ },
+ "session_id": {
+ "description": "The ID of the session (aka graph execution state)",
+ "title": "Session Id",
"type": "string"
}
},
- "required": ["timestamp", "config", "submodel_type", "user_id"],
- "title": "ModelLoadCompleteEvent",
+ "required": [
+ "timestamp",
+ "queue_id",
+ "item_id",
+ "batch_id",
+ "origin",
+ "destination",
+ "user_id",
+ "status",
+ "status_sequence",
+ "error_type",
+ "error_message",
+ "error_traceback",
+ "created_at",
+ "updated_at",
+ "started_at",
+ "completed_at",
+ "batch_status",
+ "queue_status",
+ "session_id"
+ ],
+ "title": "QueueItemStatusChangedEvent",
"type": "object"
},
- "ModelLoadStartedEvent": {
- "description": "Event model for model_load_started",
+ "QueueItemsCanceledEvent": {
+ "description": "Event model for queue_items_canceled. Emitted when queue items are canceled or deleted in\nbulk (e.g. cancel/delete-all-except-current) without per-item status change events.",
"properties": {
"timestamp": {
"description": "The timestamp of the event",
"title": "Timestamp",
"type": "integer"
},
- "config": {
- "description": "The model's config",
- "oneOf": [
- {
- "$ref": "#/components/schemas/Main_Diffusers_SD1_Config"
- },
- {
- "$ref": "#/components/schemas/Main_Diffusers_SD2_Config"
- },
- {
- "$ref": "#/components/schemas/Main_Diffusers_SDXL_Config"
- },
- {
- "$ref": "#/components/schemas/Main_Diffusers_SDXLRefiner_Config"
- },
- {
- "$ref": "#/components/schemas/Main_Diffusers_SD3_Config"
- },
- {
- "$ref": "#/components/schemas/Main_Diffusers_FLUX_Config"
- },
- {
- "$ref": "#/components/schemas/Main_Diffusers_Flux2_Config"
- },
- {
- "$ref": "#/components/schemas/Main_Diffusers_CogView4_Config"
- },
- {
- "$ref": "#/components/schemas/Main_Diffusers_QwenImage_Config"
- },
- {
- "$ref": "#/components/schemas/Main_Diffusers_ZImage_Config"
- },
- {
- "$ref": "#/components/schemas/Main_Checkpoint_SD1_Config"
- },
- {
- "$ref": "#/components/schemas/Main_Checkpoint_SD2_Config"
- },
- {
- "$ref": "#/components/schemas/Main_Checkpoint_SDXL_Config"
- },
- {
- "$ref": "#/components/schemas/Main_Checkpoint_SDXLRefiner_Config"
- },
- {
- "$ref": "#/components/schemas/Main_Checkpoint_Flux2_Config"
- },
- {
- "$ref": "#/components/schemas/Main_Checkpoint_FLUX_Config"
- },
- {
- "$ref": "#/components/schemas/Main_Checkpoint_QwenImage_Config"
- },
- {
- "$ref": "#/components/schemas/Main_Checkpoint_ZImage_Config"
- },
- {
- "$ref": "#/components/schemas/Main_Checkpoint_Anima_Config"
- },
- {
- "$ref": "#/components/schemas/Main_BnBNF4_FLUX_Config"
- },
- {
- "$ref": "#/components/schemas/Main_GGUF_Flux2_Config"
- },
- {
- "$ref": "#/components/schemas/Main_GGUF_FLUX_Config"
- },
- {
- "$ref": "#/components/schemas/Main_GGUF_QwenImage_Config"
- },
- {
- "$ref": "#/components/schemas/Main_GGUF_ZImage_Config"
- },
- {
- "$ref": "#/components/schemas/VAE_Checkpoint_SD1_Config"
- },
- {
- "$ref": "#/components/schemas/VAE_Checkpoint_SD2_Config"
- },
- {
- "$ref": "#/components/schemas/VAE_Checkpoint_SDXL_Config"
- },
- {
- "$ref": "#/components/schemas/VAE_Checkpoint_FLUX_Config"
+ "queue_id": {
+ "description": "The ID of the queue",
+ "title": "Queue Id",
+ "type": "string"
+ },
+ "canceled_item_ids": {
+ "description": "The IDs of the queue items that were canceled or deleted",
+ "items": {
+ "type": "integer"
+ },
+ "title": "Canceled Item Ids",
+ "type": "array"
+ },
+ "user_ids": {
+ "description": "The IDs of the users who own the canceled queue items",
+ "items": {
+ "type": "string"
+ },
+ "title": "User Ids",
+ "type": "array"
+ },
+ "canceled_item_ids_by_user": {
+ "additionalProperties": {
+ "items": {
+ "type": "integer"
},
- {
- "$ref": "#/components/schemas/VAE_Checkpoint_Flux2_Config"
+ "type": "array"
+ },
+ "description": "The canceled queue item IDs keyed by owner user ID.",
+ "title": "Canceled Item Ids By User",
+ "type": "object"
+ }
+ },
+ "required": ["timestamp", "queue_id", "canceled_item_ids", "user_ids", "canceled_item_ids_by_user"],
+ "title": "QueueItemsCanceledEvent",
+ "type": "object"
+ },
+ "QueueItemsRetriedEvent": {
+ "description": "Event model for queue_items_retried",
+ "properties": {
+ "timestamp": {
+ "description": "The timestamp of the event",
+ "title": "Timestamp",
+ "type": "integer"
+ },
+ "queue_id": {
+ "description": "The ID of the queue",
+ "title": "Queue Id",
+ "type": "string"
+ },
+ "retried_item_ids": {
+ "description": "The IDs of the queue items that were retried",
+ "items": {
+ "type": "integer"
+ },
+ "title": "Retried Item Ids",
+ "type": "array"
+ },
+ "user_ids": {
+ "description": "The IDs of the users who own the retried root queue items",
+ "items": {
+ "type": "string"
+ },
+ "title": "User Ids",
+ "type": "array"
+ },
+ "retried_item_ids_by_user": {
+ "additionalProperties": {
+ "items": {
+ "type": "integer"
},
+ "type": "array"
+ },
+ "description": "The retried root queue item IDs keyed by owner user ID.",
+ "title": "Retried Item Ids By User",
+ "type": "object"
+ }
+ },
+ "required": ["timestamp", "queue_id", "retried_item_ids", "user_ids", "retried_item_ids_by_user"],
+ "title": "QueueItemsRetriedEvent",
+ "type": "object"
+ },
+ "Qwen3EncoderField": {
+ "description": "Field for Qwen3 text encoder used by Z-Image models.",
+ "properties": {
+ "tokenizer": {
+ "$ref": "#/components/schemas/ModelIdentifierField",
+ "description": "Info to load tokenizer submodel"
+ },
+ "text_encoder": {
+ "$ref": "#/components/schemas/ModelIdentifierField",
+ "description": "Info to load text_encoder submodel"
+ },
+ "loras": {
+ "description": "LoRAs to apply on model loading",
+ "items": {
+ "$ref": "#/components/schemas/LoRAField"
+ },
+ "title": "Loras",
+ "type": "array"
+ }
+ },
+ "required": ["tokenizer", "text_encoder"],
+ "title": "Qwen3EncoderField",
+ "type": "object"
+ },
+ "Qwen3Encoder_Checkpoint_Config": {
+ "properties": {
+ "key": {
+ "type": "string",
+ "title": "Key",
+ "description": "A unique key for this model."
+ },
+ "hash": {
+ "type": "string",
+ "title": "Hash",
+ "description": "The hash of the model file(s)."
+ },
+ "path": {
+ "type": "string",
+ "title": "Path",
+ "description": "Path to the model on the filesystem. Relative paths are relative to the Invoke root directory."
+ },
+ "file_size": {
+ "type": "integer",
+ "title": "File Size",
+ "description": "The size of the model in bytes."
+ },
+ "name": {
+ "type": "string",
+ "title": "Name",
+ "description": "Name of the model."
+ },
+ "description": {
+ "anyOf": [
{
- "$ref": "#/components/schemas/VAE_Checkpoint_QwenImage_Config"
+ "type": "string"
},
{
- "$ref": "#/components/schemas/VAE_Checkpoint_Anima_Config"
- },
+ "type": "null"
+ }
+ ],
+ "title": "Description",
+ "description": "Model description"
+ },
+ "source": {
+ "type": "string",
+ "title": "Source",
+ "description": "The original source of the model (path, URL or repo_id)."
+ },
+ "source_type": {
+ "$ref": "#/components/schemas/ModelSourceType",
+ "description": "The type of source"
+ },
+ "source_api_response": {
+ "anyOf": [
{
- "$ref": "#/components/schemas/VAE_Diffusers_SD1_Config"
+ "type": "string"
},
{
- "$ref": "#/components/schemas/VAE_Diffusers_SDXL_Config"
- },
+ "type": "null"
+ }
+ ],
+ "title": "Source Api Response",
+ "description": "The original API response from the source, as stringified JSON."
+ },
+ "source_url": {
+ "anyOf": [
{
- "$ref": "#/components/schemas/VAE_Diffusers_Flux2_Config"
+ "type": "string"
},
{
- "$ref": "#/components/schemas/ControlNet_Checkpoint_SD1_Config"
- },
+ "type": "null"
+ }
+ ],
+ "title": "Source Url",
+ "description": "Optional URL for the model (e.g. download page or model page)."
+ },
+ "cover_image": {
+ "anyOf": [
{
- "$ref": "#/components/schemas/ControlNet_Checkpoint_SD2_Config"
+ "type": "string"
},
{
- "$ref": "#/components/schemas/ControlNet_Checkpoint_SDXL_Config"
- },
+ "type": "null"
+ }
+ ],
+ "title": "Cover Image",
+ "description": "Url for image to preview model"
+ },
+ "config_path": {
+ "anyOf": [
{
- "$ref": "#/components/schemas/ControlNet_Checkpoint_FLUX_Config"
+ "type": "string"
},
{
- "$ref": "#/components/schemas/ControlNet_Checkpoint_ZImage_Config"
- },
+ "type": "null"
+ }
+ ],
+ "title": "Config Path",
+ "description": "Path to the config for this model, if any."
+ },
+ "base": {
+ "type": "string",
+ "const": "any",
+ "title": "Base",
+ "default": "any"
+ },
+ "type": {
+ "type": "string",
+ "const": "qwen3_encoder",
+ "title": "Type",
+ "default": "qwen3_encoder"
+ },
+ "format": {
+ "type": "string",
+ "const": "checkpoint",
+ "title": "Format",
+ "default": "checkpoint"
+ },
+ "cpu_only": {
+ "anyOf": [
{
- "$ref": "#/components/schemas/ControlNet_Checkpoint_Anima_Config"
+ "type": "boolean"
},
{
- "$ref": "#/components/schemas/ControlNet_Diffusers_SD1_Config"
- },
+ "type": "null"
+ }
+ ],
+ "title": "Cpu Only",
+ "description": "Whether this model should run on CPU only"
+ },
+ "variant": {
+ "$ref": "#/components/schemas/Qwen3VariantType",
+ "description": "Qwen3 model size variant (4B or 8B)"
+ }
+ },
+ "type": "object",
+ "required": [
+ "key",
+ "hash",
+ "path",
+ "file_size",
+ "name",
+ "description",
+ "source",
+ "source_type",
+ "source_api_response",
+ "source_url",
+ "cover_image",
+ "config_path",
+ "base",
+ "type",
+ "format",
+ "cpu_only",
+ "variant"
+ ],
+ "title": "Qwen3Encoder_Checkpoint_Config",
+ "description": "Configuration for single-file Qwen3 Encoder models (safetensors)."
+ },
+ "Qwen3Encoder_GGUF_Config": {
+ "properties": {
+ "key": {
+ "type": "string",
+ "title": "Key",
+ "description": "A unique key for this model."
+ },
+ "hash": {
+ "type": "string",
+ "title": "Hash",
+ "description": "The hash of the model file(s)."
+ },
+ "path": {
+ "type": "string",
+ "title": "Path",
+ "description": "Path to the model on the filesystem. Relative paths are relative to the Invoke root directory."
+ },
+ "file_size": {
+ "type": "integer",
+ "title": "File Size",
+ "description": "The size of the model in bytes."
+ },
+ "name": {
+ "type": "string",
+ "title": "Name",
+ "description": "Name of the model."
+ },
+ "description": {
+ "anyOf": [
{
- "$ref": "#/components/schemas/ControlNet_Diffusers_SD2_Config"
+ "type": "string"
},
{
- "$ref": "#/components/schemas/ControlNet_Diffusers_SDXL_Config"
- },
+ "type": "null"
+ }
+ ],
+ "title": "Description",
+ "description": "Model description"
+ },
+ "source": {
+ "type": "string",
+ "title": "Source",
+ "description": "The original source of the model (path, URL or repo_id)."
+ },
+ "source_type": {
+ "$ref": "#/components/schemas/ModelSourceType",
+ "description": "The type of source"
+ },
+ "source_api_response": {
+ "anyOf": [
{
- "$ref": "#/components/schemas/ControlNet_Diffusers_FLUX_Config"
+ "type": "string"
},
{
- "$ref": "#/components/schemas/LoRA_LyCORIS_SD1_Config"
- },
+ "type": "null"
+ }
+ ],
+ "title": "Source Api Response",
+ "description": "The original API response from the source, as stringified JSON."
+ },
+ "source_url": {
+ "anyOf": [
{
- "$ref": "#/components/schemas/LoRA_LyCORIS_SD2_Config"
+ "type": "string"
},
{
- "$ref": "#/components/schemas/LoRA_LyCORIS_SDXL_Config"
- },
+ "type": "null"
+ }
+ ],
+ "title": "Source Url",
+ "description": "Optional URL for the model (e.g. download page or model page)."
+ },
+ "cover_image": {
+ "anyOf": [
{
- "$ref": "#/components/schemas/LoRA_LyCORIS_Flux2_Config"
+ "type": "string"
},
{
- "$ref": "#/components/schemas/LoRA_LyCORIS_FLUX_Config"
- },
+ "type": "null"
+ }
+ ],
+ "title": "Cover Image",
+ "description": "Url for image to preview model"
+ },
+ "config_path": {
+ "anyOf": [
{
- "$ref": "#/components/schemas/LoRA_LyCORIS_ZImage_Config"
+ "type": "string"
},
{
- "$ref": "#/components/schemas/LoRA_LyCORIS_QwenImage_Config"
- },
+ "type": "null"
+ }
+ ],
+ "title": "Config Path",
+ "description": "Path to the config for this model, if any."
+ },
+ "base": {
+ "type": "string",
+ "const": "any",
+ "title": "Base",
+ "default": "any"
+ },
+ "type": {
+ "type": "string",
+ "const": "qwen3_encoder",
+ "title": "Type",
+ "default": "qwen3_encoder"
+ },
+ "format": {
+ "type": "string",
+ "const": "gguf_quantized",
+ "title": "Format",
+ "default": "gguf_quantized"
+ },
+ "cpu_only": {
+ "anyOf": [
{
- "$ref": "#/components/schemas/LoRA_LyCORIS_Anima_Config"
+ "type": "boolean"
},
{
- "$ref": "#/components/schemas/LoRA_OMI_SDXL_Config"
- },
+ "type": "null"
+ }
+ ],
+ "title": "Cpu Only",
+ "description": "Whether this model should run on CPU only"
+ },
+ "variant": {
+ "$ref": "#/components/schemas/Qwen3VariantType",
+ "description": "Qwen3 model size variant (4B or 8B)"
+ }
+ },
+ "type": "object",
+ "required": [
+ "key",
+ "hash",
+ "path",
+ "file_size",
+ "name",
+ "description",
+ "source",
+ "source_type",
+ "source_api_response",
+ "source_url",
+ "cover_image",
+ "config_path",
+ "base",
+ "type",
+ "format",
+ "cpu_only",
+ "variant"
+ ],
+ "title": "Qwen3Encoder_GGUF_Config",
+ "description": "Configuration for GGUF-quantized Qwen3 Encoder models."
+ },
+ "Qwen3Encoder_Qwen3Encoder_Config": {
+ "properties": {
+ "key": {
+ "type": "string",
+ "title": "Key",
+ "description": "A unique key for this model."
+ },
+ "hash": {
+ "type": "string",
+ "title": "Hash",
+ "description": "The hash of the model file(s)."
+ },
+ "path": {
+ "type": "string",
+ "title": "Path",
+ "description": "Path to the model on the filesystem. Relative paths are relative to the Invoke root directory."
+ },
+ "file_size": {
+ "type": "integer",
+ "title": "File Size",
+ "description": "The size of the model in bytes."
+ },
+ "name": {
+ "type": "string",
+ "title": "Name",
+ "description": "Name of the model."
+ },
+ "description": {
+ "anyOf": [
{
- "$ref": "#/components/schemas/LoRA_OMI_FLUX_Config"
+ "type": "string"
},
{
- "$ref": "#/components/schemas/LoRA_Diffusers_SD1_Config"
- },
+ "type": "null"
+ }
+ ],
+ "title": "Description",
+ "description": "Model description"
+ },
+ "source": {
+ "type": "string",
+ "title": "Source",
+ "description": "The original source of the model (path, URL or repo_id)."
+ },
+ "source_type": {
+ "$ref": "#/components/schemas/ModelSourceType",
+ "description": "The type of source"
+ },
+ "source_api_response": {
+ "anyOf": [
{
- "$ref": "#/components/schemas/LoRA_Diffusers_SD2_Config"
+ "type": "string"
},
{
- "$ref": "#/components/schemas/LoRA_Diffusers_SDXL_Config"
- },
+ "type": "null"
+ }
+ ],
+ "title": "Source Api Response",
+ "description": "The original API response from the source, as stringified JSON."
+ },
+ "source_url": {
+ "anyOf": [
{
- "$ref": "#/components/schemas/LoRA_Diffusers_Flux2_Config"
+ "type": "string"
},
{
- "$ref": "#/components/schemas/LoRA_Diffusers_FLUX_Config"
- },
+ "type": "null"
+ }
+ ],
+ "title": "Source Url",
+ "description": "Optional URL for the model (e.g. download page or model page)."
+ },
+ "cover_image": {
+ "anyOf": [
{
- "$ref": "#/components/schemas/LoRA_Diffusers_ZImage_Config"
+ "type": "string"
},
{
- "$ref": "#/components/schemas/ControlLoRA_LyCORIS_FLUX_Config"
- },
+ "type": "null"
+ }
+ ],
+ "title": "Cover Image",
+ "description": "Url for image to preview model"
+ },
+ "base": {
+ "type": "string",
+ "const": "any",
+ "title": "Base",
+ "default": "any"
+ },
+ "type": {
+ "type": "string",
+ "const": "qwen3_encoder",
+ "title": "Type",
+ "default": "qwen3_encoder"
+ },
+ "format": {
+ "type": "string",
+ "const": "qwen3_encoder",
+ "title": "Format",
+ "default": "qwen3_encoder"
+ },
+ "cpu_only": {
+ "anyOf": [
{
- "$ref": "#/components/schemas/T5Encoder_T5Encoder_Config"
+ "type": "boolean"
},
{
- "$ref": "#/components/schemas/T5Encoder_BnBLLMint8_Config"
- },
+ "type": "null"
+ }
+ ],
+ "title": "Cpu Only",
+ "description": "Whether this model should run on CPU only"
+ },
+ "variant": {
+ "$ref": "#/components/schemas/Qwen3VariantType",
+ "description": "Qwen3 model size variant (4B or 8B)"
+ }
+ },
+ "type": "object",
+ "required": [
+ "key",
+ "hash",
+ "path",
+ "file_size",
+ "name",
+ "description",
+ "source",
+ "source_type",
+ "source_api_response",
+ "source_url",
+ "cover_image",
+ "base",
+ "type",
+ "format",
+ "cpu_only",
+ "variant"
+ ],
+ "title": "Qwen3Encoder_Qwen3Encoder_Config",
+ "description": "Configuration for Qwen3 Encoder models in a diffusers-like format.\n\nThe model weights are expected to be in a folder called text_encoder inside the model directory,\ncompatible with Qwen2VLForConditionalGeneration or similar architectures used by Z-Image."
+ },
+ "Qwen3VariantType": {
+ "type": "string",
+ "enum": ["qwen3_4b", "qwen3_8b", "qwen3_06b"],
+ "title": "Qwen3VariantType",
+ "description": "Qwen3 text encoder variants based on model size."
+ },
+ "QwenImageConditioningField": {
+ "description": "A Qwen Image Edit conditioning tensor primitive value",
+ "properties": {
+ "conditioning_name": {
+ "description": "The name of conditioning tensor",
+ "title": "Conditioning Name",
+ "type": "string"
+ }
+ },
+ "required": ["conditioning_name"],
+ "title": "QwenImageConditioningField",
+ "type": "object"
+ },
+ "QwenImageConditioningOutput": {
+ "class": "output",
+ "description": "Base class for nodes that output a Qwen Image Edit conditioning tensor.",
+ "properties": {
+ "conditioning": {
+ "$ref": "#/components/schemas/QwenImageConditioningField",
+ "description": "Conditioning tensor",
+ "field_kind": "output",
+ "ui_hidden": false
+ },
+ "type": {
+ "const": "qwen_image_conditioning_output",
+ "default": "qwen_image_conditioning_output",
+ "field_kind": "node_attribute",
+ "title": "type",
+ "type": "string"
+ }
+ },
+ "required": ["output_meta", "conditioning", "type", "type"],
+ "title": "QwenImageConditioningOutput",
+ "type": "object"
+ },
+ "QwenImageDenoiseInvocation": {
+ "category": "image",
+ "class": "invocation",
+ "classification": "prototype",
+ "description": "Run the denoising process with a Qwen Image model.",
+ "node_pack": "invokeai",
+ "properties": {
+ "board": {
+ "anyOf": [
{
- "$ref": "#/components/schemas/Qwen3Encoder_Qwen3Encoder_Config"
+ "$ref": "#/components/schemas/BoardField"
},
{
- "$ref": "#/components/schemas/Qwen3Encoder_Checkpoint_Config"
- },
+ "type": "null"
+ }
+ ],
+ "default": null,
+ "description": "The board to save the image to",
+ "field_kind": "internal",
+ "input": "direct",
+ "orig_required": false,
+ "ui_hidden": false
+ },
+ "metadata": {
+ "anyOf": [
{
- "$ref": "#/components/schemas/Qwen3Encoder_GGUF_Config"
+ "$ref": "#/components/schemas/MetadataField"
},
{
- "$ref": "#/components/schemas/QwenVLEncoder_Diffusers_Config"
- },
+ "type": "null"
+ }
+ ],
+ "default": null,
+ "description": "Optional metadata to be saved with the image",
+ "field_kind": "internal",
+ "input": "connection",
+ "orig_required": false,
+ "ui_hidden": false
+ },
+ "id": {
+ "description": "The id of this instance of an invocation. Must be unique among all instances of invocations.",
+ "field_kind": "node_attribute",
+ "title": "Id",
+ "type": "string"
+ },
+ "is_intermediate": {
+ "default": false,
+ "description": "Whether or not this is an intermediate invocation.",
+ "field_kind": "node_attribute",
+ "input": "direct",
+ "orig_required": true,
+ "title": "Is Intermediate",
+ "type": "boolean",
+ "ui_hidden": false,
+ "ui_type": "IsIntermediate"
+ },
+ "use_cache": {
+ "default": true,
+ "description": "Whether or not to use the cache",
+ "field_kind": "node_attribute",
+ "title": "Use Cache",
+ "type": "boolean"
+ },
+ "latents": {
+ "anyOf": [
{
- "$ref": "#/components/schemas/QwenVLEncoder_Checkpoint_Config"
+ "$ref": "#/components/schemas/LatentsField"
},
{
- "$ref": "#/components/schemas/TI_File_SD1_Config"
- },
+ "type": "null"
+ }
+ ],
+ "default": null,
+ "description": "Latents tensor",
+ "field_kind": "input",
+ "input": "connection",
+ "orig_default": null,
+ "orig_required": false
+ },
+ "reference_latents": {
+ "anyOf": [
{
- "$ref": "#/components/schemas/TI_File_SD2_Config"
+ "$ref": "#/components/schemas/LatentsField"
},
{
- "$ref": "#/components/schemas/TI_File_SDXL_Config"
- },
+ "type": "null"
+ }
+ ],
+ "default": null,
+ "description": "Reference image latents to guide generation. Encoded through the VAE.",
+ "field_kind": "input",
+ "input": "connection",
+ "orig_default": null,
+ "orig_required": false
+ },
+ "denoise_mask": {
+ "anyOf": [
{
- "$ref": "#/components/schemas/TI_Folder_SD1_Config"
+ "$ref": "#/components/schemas/DenoiseMaskField"
},
{
- "$ref": "#/components/schemas/TI_Folder_SD2_Config"
- },
+ "type": "null"
+ }
+ ],
+ "default": null,
+ "description": "A mask of the region to apply the denoising process to. Values of 0.0 represent the regions to be fully denoised, and 1.0 represent the regions to be preserved.",
+ "field_kind": "input",
+ "input": "connection",
+ "orig_default": null,
+ "orig_required": false
+ },
+ "denoising_start": {
+ "default": 0.0,
+ "description": "When to start denoising, expressed a percentage of total steps",
+ "field_kind": "input",
+ "input": "any",
+ "maximum": 1,
+ "minimum": 0,
+ "orig_default": 0.0,
+ "orig_required": false,
+ "title": "Denoising Start",
+ "type": "number"
+ },
+ "denoising_end": {
+ "default": 1.0,
+ "description": "When to stop denoising, expressed a percentage of total steps",
+ "field_kind": "input",
+ "input": "any",
+ "maximum": 1,
+ "minimum": 0,
+ "orig_default": 1.0,
+ "orig_required": false,
+ "title": "Denoising End",
+ "type": "number"
+ },
+ "transformer": {
+ "anyOf": [
{
- "$ref": "#/components/schemas/TI_Folder_SDXL_Config"
+ "$ref": "#/components/schemas/TransformerField"
},
{
- "$ref": "#/components/schemas/IPAdapter_InvokeAI_SD1_Config"
- },
+ "type": "null"
+ }
+ ],
+ "default": null,
+ "description": "Qwen Image Edit model (Transformer) to load",
+ "field_kind": "input",
+ "input": "connection",
+ "orig_required": true,
+ "title": "Transformer"
+ },
+ "positive_conditioning": {
+ "anyOf": [
{
- "$ref": "#/components/schemas/IPAdapter_InvokeAI_SD2_Config"
+ "$ref": "#/components/schemas/QwenImageConditioningField"
},
{
- "$ref": "#/components/schemas/IPAdapter_InvokeAI_SDXL_Config"
- },
+ "type": "null"
+ }
+ ],
+ "default": null,
+ "description": "Positive conditioning tensor",
+ "field_kind": "input",
+ "input": "connection",
+ "orig_required": true
+ },
+ "negative_conditioning": {
+ "anyOf": [
{
- "$ref": "#/components/schemas/IPAdapter_Checkpoint_SD1_Config"
+ "$ref": "#/components/schemas/QwenImageConditioningField"
},
{
- "$ref": "#/components/schemas/IPAdapter_Checkpoint_SD2_Config"
- },
+ "type": "null"
+ }
+ ],
+ "default": null,
+ "description": "Negative conditioning tensor",
+ "field_kind": "input",
+ "input": "connection",
+ "orig_default": null,
+ "orig_required": false
+ },
+ "cfg_scale": {
+ "anyOf": [
{
- "$ref": "#/components/schemas/IPAdapter_Checkpoint_SDXL_Config"
+ "type": "number"
},
{
- "$ref": "#/components/schemas/IPAdapter_Checkpoint_FLUX_Config"
- },
+ "items": {
+ "type": "number"
+ },
+ "type": "array"
+ }
+ ],
+ "default": 4.0,
+ "description": "Classifier-Free Guidance scale",
+ "field_kind": "input",
+ "input": "any",
+ "orig_default": 4.0,
+ "orig_required": false,
+ "title": "CFG Scale"
+ },
+ "width": {
+ "default": 1024,
+ "description": "Width of the generated image.",
+ "field_kind": "input",
+ "input": "any",
+ "multipleOf": 16,
+ "orig_default": 1024,
+ "orig_required": false,
+ "title": "Width",
+ "type": "integer"
+ },
+ "height": {
+ "default": 1024,
+ "description": "Height of the generated image.",
+ "field_kind": "input",
+ "input": "any",
+ "multipleOf": 16,
+ "orig_default": 1024,
+ "orig_required": false,
+ "title": "Height",
+ "type": "integer"
+ },
+ "steps": {
+ "default": 40,
+ "description": "Number of steps to run",
+ "exclusiveMinimum": 0,
+ "field_kind": "input",
+ "input": "any",
+ "orig_default": 40,
+ "orig_required": false,
+ "title": "Steps",
+ "type": "integer"
+ },
+ "seed": {
+ "default": 0,
+ "description": "Randomness seed for reproducibility.",
+ "field_kind": "input",
+ "input": "any",
+ "orig_default": 0,
+ "orig_required": false,
+ "title": "Seed",
+ "type": "integer"
+ },
+ "shift": {
+ "anyOf": [
{
- "$ref": "#/components/schemas/T2IAdapter_Diffusers_SD1_Config"
+ "type": "number"
},
{
- "$ref": "#/components/schemas/T2IAdapter_Diffusers_SDXL_Config"
- },
+ "type": "null"
+ }
+ ],
+ "default": null,
+ "description": "Override the sigma schedule shift. When set, uses a fixed shift (e.g. 3.0 for Lightning LoRAs) instead of the default dynamic shifting. Leave unset for the base model's default schedule.",
+ "field_kind": "input",
+ "input": "any",
+ "orig_default": null,
+ "orig_required": false,
+ "title": "Shift"
+ },
+ "type": {
+ "const": "qwen_image_denoise",
+ "default": "qwen_image_denoise",
+ "field_kind": "node_attribute",
+ "title": "type",
+ "type": "string"
+ }
+ },
+ "required": ["type", "id"],
+ "tags": ["image", "qwen_image"],
+ "title": "Denoise - Qwen Image",
+ "type": "object",
+ "version": "1.0.0",
+ "output": {
+ "$ref": "#/components/schemas/LatentsOutput"
+ }
+ },
+ "QwenImageImageToLatentsInvocation": {
+ "category": "image",
+ "class": "invocation",
+ "classification": "prototype",
+ "description": "Generates latents from an image using the Qwen Image VAE.",
+ "node_pack": "invokeai",
+ "properties": {
+ "board": {
+ "anyOf": [
{
- "$ref": "#/components/schemas/Spandrel_Checkpoint_Config"
+ "$ref": "#/components/schemas/BoardField"
},
{
- "$ref": "#/components/schemas/CLIPEmbed_Diffusers_G_Config"
- },
+ "type": "null"
+ }
+ ],
+ "default": null,
+ "description": "The board to save the image to",
+ "field_kind": "internal",
+ "input": "direct",
+ "orig_required": false,
+ "ui_hidden": false
+ },
+ "metadata": {
+ "anyOf": [
{
- "$ref": "#/components/schemas/CLIPEmbed_Diffusers_L_Config"
+ "$ref": "#/components/schemas/MetadataField"
},
{
- "$ref": "#/components/schemas/CLIPVision_Diffusers_Config"
- },
+ "type": "null"
+ }
+ ],
+ "default": null,
+ "description": "Optional metadata to be saved with the image",
+ "field_kind": "internal",
+ "input": "connection",
+ "orig_required": false,
+ "ui_hidden": false
+ },
+ "id": {
+ "description": "The id of this instance of an invocation. Must be unique among all instances of invocations.",
+ "field_kind": "node_attribute",
+ "title": "Id",
+ "type": "string"
+ },
+ "is_intermediate": {
+ "default": false,
+ "description": "Whether or not this is an intermediate invocation.",
+ "field_kind": "node_attribute",
+ "input": "direct",
+ "orig_required": true,
+ "title": "Is Intermediate",
+ "type": "boolean",
+ "ui_hidden": false,
+ "ui_type": "IsIntermediate"
+ },
+ "use_cache": {
+ "default": true,
+ "description": "Whether or not to use the cache",
+ "field_kind": "node_attribute",
+ "title": "Use Cache",
+ "type": "boolean"
+ },
+ "image": {
+ "anyOf": [
{
- "$ref": "#/components/schemas/SigLIP_Diffusers_Config"
+ "$ref": "#/components/schemas/ImageField"
},
{
- "$ref": "#/components/schemas/FLUXRedux_Checkpoint_Config"
- },
+ "type": "null"
+ }
+ ],
+ "default": null,
+ "description": "The image to encode.",
+ "field_kind": "input",
+ "input": "any",
+ "orig_required": true
+ },
+ "vae": {
+ "anyOf": [
{
- "$ref": "#/components/schemas/LlavaOnevision_Diffusers_Config"
+ "$ref": "#/components/schemas/VAEField"
},
{
- "$ref": "#/components/schemas/TextLLM_Diffusers_Config"
- },
+ "type": "null"
+ }
+ ],
+ "default": null,
+ "description": "VAE",
+ "field_kind": "input",
+ "input": "connection",
+ "orig_required": true
+ },
+ "width": {
+ "anyOf": [
{
- "$ref": "#/components/schemas/ExternalApiModelConfig"
+ "type": "integer"
},
{
- "$ref": "#/components/schemas/Unknown_Config"
+ "type": "null"
}
],
- "title": "Config"
+ "default": null,
+ "description": "Resize the image to this width before encoding. If not set, encodes at the image's original size.",
+ "field_kind": "input",
+ "input": "any",
+ "orig_default": null,
+ "orig_required": false,
+ "title": "Width"
},
- "submodel_type": {
+ "height": {
"anyOf": [
{
- "$ref": "#/components/schemas/SubModelType"
+ "type": "integer"
},
{
"type": "null"
}
],
"default": null,
- "description": "The submodel type, if any"
- },
- "user_id": {
- "default": "system",
- "description": "The ID of the user whose action triggered the load",
- "title": "User Id",
- "type": "string"
- }
- },
- "required": ["timestamp", "config", "submodel_type", "user_id"],
- "title": "ModelLoadStartedEvent",
- "type": "object"
- },
- "ModelLoaderOutput": {
- "class": "output",
- "description": "Model loader output",
- "properties": {
- "vae": {
- "$ref": "#/components/schemas/VAEField",
- "description": "VAE",
- "field_kind": "output",
- "title": "VAE",
- "ui_hidden": false
+ "description": "Resize the image to this height before encoding. If not set, encodes at the image's original size.",
+ "field_kind": "input",
+ "input": "any",
+ "orig_default": null,
+ "orig_required": false,
+ "title": "Height"
},
"type": {
- "const": "model_loader_output",
- "default": "model_loader_output",
+ "const": "qwen_image_i2l",
+ "default": "qwen_image_i2l",
"field_kind": "node_attribute",
"title": "type",
"type": "string"
- },
- "clip": {
- "$ref": "#/components/schemas/CLIPField",
- "description": "CLIP (tokenizer, text encoder, LoRAs) and skipped layer count",
- "field_kind": "output",
- "title": "CLIP",
- "ui_hidden": false
- },
- "unet": {
- "$ref": "#/components/schemas/UNetField",
- "description": "UNet (scheduler, LoRAs)",
- "field_kind": "output",
- "title": "UNet",
- "ui_hidden": false
}
},
- "required": ["output_meta", "vae", "type", "clip", "unet", "type"],
- "title": "ModelLoaderOutput",
- "type": "object"
+ "required": ["type", "id"],
+ "tags": ["image", "latents", "vae", "i2l", "qwen_image"],
+ "title": "Image to Latents - Qwen Image",
+ "type": "object",
+ "version": "1.0.0",
+ "output": {
+ "$ref": "#/components/schemas/LatentsOutput"
+ }
},
- "ModelRecordChanges": {
+ "QwenImageLatentsToImageInvocation": {
+ "category": "latents",
+ "class": "invocation",
+ "classification": "prototype",
+ "description": "Generates an image from latents using the Qwen Image VAE.",
+ "node_pack": "invokeai",
"properties": {
- "source": {
+ "board": {
"anyOf": [
{
- "type": "string"
+ "$ref": "#/components/schemas/BoardField"
},
{
"type": "null"
}
],
- "title": "Source",
- "description": "original source of the model"
+ "default": null,
+ "description": "The board to save the image to",
+ "field_kind": "internal",
+ "input": "direct",
+ "orig_required": false,
+ "ui_hidden": false
},
- "source_type": {
+ "metadata": {
"anyOf": [
{
- "$ref": "#/components/schemas/ModelSourceType"
+ "$ref": "#/components/schemas/MetadataField"
},
{
"type": "null"
}
],
- "description": "type of model source"
+ "default": null,
+ "description": "Optional metadata to be saved with the image",
+ "field_kind": "internal",
+ "input": "connection",
+ "orig_required": false,
+ "ui_hidden": false
},
- "source_api_response": {
+ "id": {
+ "description": "The id of this instance of an invocation. Must be unique among all instances of invocations.",
+ "field_kind": "node_attribute",
+ "title": "Id",
+ "type": "string"
+ },
+ "is_intermediate": {
+ "default": false,
+ "description": "Whether or not this is an intermediate invocation.",
+ "field_kind": "node_attribute",
+ "input": "direct",
+ "orig_required": true,
+ "title": "Is Intermediate",
+ "type": "boolean",
+ "ui_hidden": false,
+ "ui_type": "IsIntermediate"
+ },
+ "use_cache": {
+ "default": true,
+ "description": "Whether or not to use the cache",
+ "field_kind": "node_attribute",
+ "title": "Use Cache",
+ "type": "boolean"
+ },
+ "latents": {
"anyOf": [
{
- "type": "string"
+ "$ref": "#/components/schemas/LatentsField"
},
{
"type": "null"
}
],
- "title": "Source Api Response",
- "description": "metadata from remote source"
+ "default": null,
+ "description": "Latents tensor",
+ "field_kind": "input",
+ "input": "connection",
+ "orig_required": true
},
- "source_url": {
+ "vae": {
"anyOf": [
{
- "type": "string"
+ "$ref": "#/components/schemas/VAEField"
},
{
"type": "null"
}
],
- "title": "Source Url",
- "description": "Optional URL for the model (e.g. download page)"
+ "default": null,
+ "description": "VAE",
+ "field_kind": "input",
+ "input": "connection",
+ "orig_required": true
},
- "name": {
+ "type": {
+ "const": "qwen_image_l2i",
+ "default": "qwen_image_l2i",
+ "field_kind": "node_attribute",
+ "title": "type",
+ "type": "string"
+ }
+ },
+ "required": ["type", "id"],
+ "tags": ["latents", "image", "vae", "l2i", "qwen_image"],
+ "title": "Latents to Image - Qwen Image",
+ "type": "object",
+ "version": "1.0.0",
+ "output": {
+ "$ref": "#/components/schemas/ImageOutput"
+ }
+ },
+ "QwenImageLoRACollectionLoader": {
+ "category": "model",
+ "class": "invocation",
+ "classification": "prototype",
+ "description": "Applies a collection of LoRAs to a Qwen Image transformer.",
+ "node_pack": "invokeai",
+ "properties": {
+ "id": {
+ "description": "The id of this instance of an invocation. Must be unique among all instances of invocations.",
+ "field_kind": "node_attribute",
+ "title": "Id",
+ "type": "string"
+ },
+ "is_intermediate": {
+ "default": false,
+ "description": "Whether or not this is an intermediate invocation.",
+ "field_kind": "node_attribute",
+ "input": "direct",
+ "orig_required": true,
+ "title": "Is Intermediate",
+ "type": "boolean",
+ "ui_hidden": false,
+ "ui_type": "IsIntermediate"
+ },
+ "use_cache": {
+ "default": true,
+ "description": "Whether or not to use the cache",
+ "field_kind": "node_attribute",
+ "title": "Use Cache",
+ "type": "boolean"
+ },
+ "loras": {
"anyOf": [
{
- "type": "string"
+ "$ref": "#/components/schemas/LoRAField"
+ },
+ {
+ "items": {
+ "$ref": "#/components/schemas/LoRAField"
+ },
+ "type": "array"
},
{
"type": "null"
}
],
- "title": "Name",
- "description": "Name of the model."
+ "default": null,
+ "description": "LoRA models and weights. May be a single LoRA or collection.",
+ "field_kind": "input",
+ "input": "any",
+ "orig_default": null,
+ "orig_required": false,
+ "title": "LoRAs",
+ "ui_model_base": ["qwen-image"],
+ "ui_model_type": ["lora"]
},
- "path": {
+ "transformer": {
"anyOf": [
{
- "type": "string"
+ "$ref": "#/components/schemas/TransformerField"
},
{
"type": "null"
}
],
- "title": "Path",
- "description": "Path to the model."
+ "default": null,
+ "description": "Transformer",
+ "field_kind": "input",
+ "input": "connection",
+ "orig_default": null,
+ "orig_required": false,
+ "title": "Transformer"
},
- "description": {
+ "type": {
+ "const": "qwen_image_lora_collection_loader",
+ "default": "qwen_image_lora_collection_loader",
+ "field_kind": "node_attribute",
+ "title": "type",
+ "type": "string"
+ }
+ },
+ "required": ["type", "id"],
+ "tags": ["lora", "model", "qwen_image"],
+ "title": "Apply LoRA Collection - Qwen Image",
+ "type": "object",
+ "version": "1.0.1",
+ "output": {
+ "$ref": "#/components/schemas/QwenImageLoRALoaderOutput"
+ }
+ },
+ "QwenImageLoRALoaderInvocation": {
+ "category": "model",
+ "class": "invocation",
+ "classification": "prototype",
+ "description": "Apply a LoRA model to a Qwen Image transformer.",
+ "node_pack": "invokeai",
+ "properties": {
+ "id": {
+ "description": "The id of this instance of an invocation. Must be unique among all instances of invocations.",
+ "field_kind": "node_attribute",
+ "title": "Id",
+ "type": "string"
+ },
+ "is_intermediate": {
+ "default": false,
+ "description": "Whether or not this is an intermediate invocation.",
+ "field_kind": "node_attribute",
+ "input": "direct",
+ "orig_required": true,
+ "title": "Is Intermediate",
+ "type": "boolean",
+ "ui_hidden": false,
+ "ui_type": "IsIntermediate"
+ },
+ "use_cache": {
+ "default": true,
+ "description": "Whether or not to use the cache",
+ "field_kind": "node_attribute",
+ "title": "Use Cache",
+ "type": "boolean"
+ },
+ "lora": {
"anyOf": [
{
- "type": "string"
+ "$ref": "#/components/schemas/ModelIdentifierField"
},
{
"type": "null"
}
],
- "title": "Description",
- "description": "Model description"
+ "default": null,
+ "description": "LoRA model to load",
+ "field_kind": "input",
+ "input": "any",
+ "orig_required": true,
+ "title": "LoRA",
+ "ui_model_base": ["qwen-image"],
+ "ui_model_type": ["lora"]
},
- "base": {
+ "weight": {
+ "default": 1.0,
+ "description": "The weight at which the LoRA is applied to each model",
+ "field_kind": "input",
+ "input": "any",
+ "orig_default": 1.0,
+ "orig_required": false,
+ "title": "Weight",
+ "type": "number"
+ },
+ "transformer": {
"anyOf": [
{
- "$ref": "#/components/schemas/BaseModelType"
+ "$ref": "#/components/schemas/TransformerField"
},
{
"type": "null"
}
],
- "description": "The base model."
+ "default": null,
+ "description": "Transformer",
+ "field_kind": "input",
+ "input": "connection",
+ "orig_default": null,
+ "orig_required": false,
+ "title": "Transformer"
},
"type": {
+ "const": "qwen_image_lora_loader",
+ "default": "qwen_image_lora_loader",
+ "field_kind": "node_attribute",
+ "title": "type",
+ "type": "string"
+ }
+ },
+ "required": ["type", "id"],
+ "tags": ["lora", "model", "qwen_image"],
+ "title": "Apply LoRA - Qwen Image",
+ "type": "object",
+ "version": "1.0.0",
+ "output": {
+ "$ref": "#/components/schemas/QwenImageLoRALoaderOutput"
+ }
+ },
+ "QwenImageLoRALoaderOutput": {
+ "class": "output",
+ "description": "Qwen Image LoRA Loader Output",
+ "properties": {
+ "transformer": {
"anyOf": [
{
- "$ref": "#/components/schemas/ModelType"
+ "$ref": "#/components/schemas/TransformerField"
},
{
"type": "null"
}
],
- "description": "Type of model"
+ "default": null,
+ "description": "Transformer",
+ "field_kind": "output",
+ "title": "Transformer",
+ "ui_hidden": false
},
- "key": {
+ "type": {
+ "const": "qwen_image_lora_loader_output",
+ "default": "qwen_image_lora_loader_output",
+ "field_kind": "node_attribute",
+ "title": "type",
+ "type": "string"
+ }
+ },
+ "required": ["output_meta", "transformer", "type", "type"],
+ "title": "QwenImageLoRALoaderOutput",
+ "type": "object"
+ },
+ "QwenImageModelLoaderInvocation": {
+ "category": "model",
+ "class": "invocation",
+ "classification": "prototype",
+ "description": "Loads a Qwen Image model, outputting its submodels.\n\nThe transformer is always loaded from the main model (Diffusers or GGUF).\n\nComponents can be mixed and matched:\n- VAE: standalone Qwen Image VAE checkpoint, the Component Source (Diffusers),\n or the main model if it's Diffusers.\n- Qwen VL Encoder: standalone Qwen2.5-VL encoder, the Component Source\n (Diffusers), or the main model if it's Diffusers.\n\nTogether, the standalone VAE and standalone encoder allow running a GGUF\ntransformer without ever downloading the full ~40 GB Diffusers pipeline.",
+ "node_pack": "invokeai",
+ "properties": {
+ "id": {
+ "description": "The id of this instance of an invocation. Must be unique among all instances of invocations.",
+ "field_kind": "node_attribute",
+ "title": "Id",
+ "type": "string"
+ },
+ "is_intermediate": {
+ "default": false,
+ "description": "Whether or not this is an intermediate invocation.",
+ "field_kind": "node_attribute",
+ "input": "direct",
+ "orig_required": true,
+ "title": "Is Intermediate",
+ "type": "boolean",
+ "ui_hidden": false,
+ "ui_type": "IsIntermediate"
+ },
+ "use_cache": {
+ "default": true,
+ "description": "Whether or not to use the cache",
+ "field_kind": "node_attribute",
+ "title": "Use Cache",
+ "type": "boolean"
+ },
+ "model": {
+ "$ref": "#/components/schemas/ModelIdentifierField",
+ "description": "Qwen Image Edit model (Transformer) to load",
+ "field_kind": "input",
+ "input": "direct",
+ "orig_required": true,
+ "title": "Transformer",
+ "ui_model_base": ["qwen-image"],
+ "ui_model_type": ["main"]
+ },
+ "vae_model": {
"anyOf": [
{
- "type": "string"
+ "$ref": "#/components/schemas/ModelIdentifierField"
},
{
"type": "null"
}
],
- "title": "Key",
- "description": "Database ID for this model"
+ "default": null,
+ "description": "Standalone Qwen Image VAE model. If not provided, VAE will be loaded from the Component Source (or from the main model if it is Diffusers).",
+ "field_kind": "input",
+ "input": "direct",
+ "orig_default": null,
+ "orig_required": false,
+ "title": "VAE",
+ "ui_model_base": ["qwen-image"],
+ "ui_model_type": ["vae"]
},
- "hash": {
+ "qwen_vl_encoder_model": {
"anyOf": [
{
- "type": "string"
+ "$ref": "#/components/schemas/ModelIdentifierField"
},
{
"type": "null"
}
],
- "title": "Hash",
- "description": "hash of model file"
+ "default": null,
+ "description": "Standalone Qwen2.5-VL encoder model. If not provided, the encoder will be loaded from the Component Source (or from the main model if it is Diffusers).",
+ "field_kind": "input",
+ "input": "direct",
+ "orig_default": null,
+ "orig_required": false,
+ "title": "Qwen VL Encoder",
+ "ui_model_type": ["qwen_vl_encoder"]
},
- "file_size": {
+ "component_source": {
"anyOf": [
{
- "type": "integer"
+ "$ref": "#/components/schemas/ModelIdentifierField"
},
{
"type": "null"
}
],
- "title": "File Size",
- "description": "Size of model file"
+ "default": null,
+ "description": "Diffusers Qwen Image model to extract VAE and/or Qwen VL encoder from. Use this if you don't have separate VAE/encoder models. Ignored for any submodel that is provided separately.",
+ "field_kind": "input",
+ "input": "direct",
+ "orig_default": null,
+ "orig_required": false,
+ "title": "Component Source (Diffusers)",
+ "ui_model_base": ["qwen-image"],
+ "ui_model_format": ["diffusers"],
+ "ui_model_type": ["main"]
},
- "format": {
+ "type": {
+ "const": "qwen_image_model_loader",
+ "default": "qwen_image_model_loader",
+ "field_kind": "node_attribute",
+ "title": "type",
+ "type": "string"
+ }
+ },
+ "required": ["model", "type", "id"],
+ "tags": ["model", "qwen_image"],
+ "title": "Main Model - Qwen Image",
+ "type": "object",
+ "version": "1.2.0",
+ "output": {
+ "$ref": "#/components/schemas/QwenImageModelLoaderOutput"
+ }
+ },
+ "QwenImageModelLoaderOutput": {
+ "class": "output",
+ "description": "Qwen Image model loader output.",
+ "properties": {
+ "transformer": {
+ "$ref": "#/components/schemas/TransformerField",
+ "description": "Transformer",
+ "field_kind": "output",
+ "title": "Transformer",
+ "ui_hidden": false
+ },
+ "qwen_vl_encoder": {
+ "$ref": "#/components/schemas/QwenVLEncoderField",
+ "description": "Qwen2.5-VL tokenizer, processor and text/vision encoder",
+ "field_kind": "output",
+ "title": "Qwen VL Encoder",
+ "ui_hidden": false
+ },
+ "vae": {
+ "$ref": "#/components/schemas/VAEField",
+ "description": "VAE",
+ "field_kind": "output",
+ "title": "VAE",
+ "ui_hidden": false
+ },
+ "type": {
+ "const": "qwen_image_model_loader_output",
+ "default": "qwen_image_model_loader_output",
+ "field_kind": "node_attribute",
+ "title": "type",
+ "type": "string"
+ }
+ },
+ "required": ["output_meta", "transformer", "qwen_vl_encoder", "vae", "type", "type"],
+ "title": "QwenImageModelLoaderOutput",
+ "type": "object"
+ },
+ "QwenImageTextEncoderInvocation": {
+ "category": "conditioning",
+ "class": "invocation",
+ "classification": "prototype",
+ "description": "Encodes text and reference images for Qwen Image using Qwen2.5-VL.",
+ "node_pack": "invokeai",
+ "properties": {
+ "id": {
+ "description": "The id of this instance of an invocation. Must be unique among all instances of invocations.",
+ "field_kind": "node_attribute",
+ "title": "Id",
+ "type": "string"
+ },
+ "is_intermediate": {
+ "default": false,
+ "description": "Whether or not this is an intermediate invocation.",
+ "field_kind": "node_attribute",
+ "input": "direct",
+ "orig_required": true,
+ "title": "Is Intermediate",
+ "type": "boolean",
+ "ui_hidden": false,
+ "ui_type": "IsIntermediate"
+ },
+ "use_cache": {
+ "default": true,
+ "description": "Whether or not to use the cache",
+ "field_kind": "node_attribute",
+ "title": "Use Cache",
+ "type": "boolean"
+ },
+ "prompt": {
"anyOf": [
{
"type": "string"
@@ -57764,59 +65414,121 @@
"type": "null"
}
],
- "title": "Format",
- "description": "format of model file"
+ "default": null,
+ "description": "Text prompt describing the desired edit.",
+ "field_kind": "input",
+ "input": "any",
+ "orig_required": true,
+ "title": "Prompt",
+ "ui_component": "textarea"
},
- "trigger_phrases": {
- "anyOf": [
- {
- "items": {
- "type": "string"
- },
- "type": "array",
- "uniqueItems": true
- },
- {
- "type": "null"
- }
- ],
- "title": "Trigger Phrases",
- "description": "Set of trigger phrases for this model"
+ "reference_images": {
+ "default": [],
+ "description": "Reference images to guide the edit. The model can use multiple reference images.",
+ "field_kind": "input",
+ "input": "any",
+ "items": {
+ "$ref": "#/components/schemas/ImageField"
+ },
+ "orig_default": [],
+ "orig_required": false,
+ "title": "Reference Images",
+ "type": "array"
},
- "default_settings": {
+ "qwen_vl_encoder": {
"anyOf": [
{
- "$ref": "#/components/schemas/MainModelDefaultSettings"
- },
- {
- "$ref": "#/components/schemas/LoraModelDefaultSettings"
- },
- {
- "$ref": "#/components/schemas/ControlAdapterDefaultSettings"
- },
- {
- "$ref": "#/components/schemas/ExternalApiModelDefaultSettings"
+ "$ref": "#/components/schemas/QwenVLEncoderField"
},
{
"type": "null"
}
],
- "title": "Default Settings",
- "description": "Default settings for this model"
+ "default": null,
+ "description": "Qwen2.5-VL tokenizer, processor and text/vision encoder",
+ "field_kind": "input",
+ "input": "connection",
+ "orig_required": true,
+ "title": "Qwen VL Encoder"
+ },
+ "quantization": {
+ "default": "none",
+ "description": "Quantize the Qwen VL encoder to reduce VRAM usage. 'nf4' (4-bit) saves the most memory, 'int8' (8-bit) is a middle ground.",
+ "enum": ["none", "int8", "nf4"],
+ "field_kind": "input",
+ "input": "any",
+ "orig_default": "none",
+ "orig_required": false,
+ "title": "Quantization",
+ "type": "string"
+ },
+ "type": {
+ "const": "qwen_image_text_encoder",
+ "default": "qwen_image_text_encoder",
+ "field_kind": "node_attribute",
+ "title": "type",
+ "type": "string"
+ }
+ },
+ "required": ["type", "id"],
+ "tags": ["prompt", "conditioning", "qwen_image"],
+ "title": "Prompt - Qwen Image",
+ "type": "object",
+ "version": "1.2.0",
+ "output": {
+ "$ref": "#/components/schemas/QwenImageConditioningOutput"
+ }
+ },
+ "QwenImageVariantType": {
+ "type": "string",
+ "enum": ["generate", "edit"],
+ "title": "QwenImageVariantType",
+ "description": "Qwen Image model variants."
+ },
+ "QwenVLEncoderField": {
+ "description": "Field for Qwen2.5-VL encoder used by Qwen Image Edit models.",
+ "properties": {
+ "tokenizer": {
+ "$ref": "#/components/schemas/ModelIdentifierField",
+ "description": "Info to load tokenizer submodel"
+ },
+ "text_encoder": {
+ "$ref": "#/components/schemas/ModelIdentifierField",
+ "description": "Info to load text_encoder submodel"
+ }
+ },
+ "required": ["tokenizer", "text_encoder"],
+ "title": "QwenVLEncoderField",
+ "type": "object"
+ },
+ "QwenVLEncoder_Checkpoint_Config": {
+ "properties": {
+ "key": {
+ "type": "string",
+ "title": "Key",
+ "description": "A unique key for this model."
},
- "provider_id": {
- "anyOf": [
- {
- "type": "string"
- },
- {
- "type": "null"
- }
- ],
- "title": "Provider Id",
- "description": "External provider identifier"
+ "hash": {
+ "type": "string",
+ "title": "Hash",
+ "description": "The hash of the model file(s)."
},
- "provider_model_id": {
+ "path": {
+ "type": "string",
+ "title": "Path",
+ "description": "Path to the model on the filesystem. Relative paths are relative to the Invoke root directory."
+ },
+ "file_size": {
+ "type": "integer",
+ "title": "File Size",
+ "description": "The size of the model in bytes."
+ },
+ "name": {
+ "type": "string",
+ "title": "Name",
+ "description": "Name of the model."
+ },
+ "description": {
"anyOf": [
{
"type": "string"
@@ -57825,84 +65537,53 @@
"type": "null"
}
],
- "title": "Provider Model Id",
- "description": "External provider model identifier"
+ "title": "Description",
+ "description": "Model description"
},
- "capabilities": {
- "anyOf": [
- {
- "$ref": "#/components/schemas/ExternalModelCapabilities"
- },
- {
- "type": "null"
- }
- ],
- "description": "External model capabilities"
+ "source": {
+ "type": "string",
+ "title": "Source",
+ "description": "The original source of the model (path, URL or repo_id)."
},
- "cpu_only": {
- "anyOf": [
- {
- "type": "boolean"
- },
- {
- "type": "null"
- }
- ],
- "title": "Cpu Only",
- "description": "Whether this model should run on CPU only"
+ "source_type": {
+ "$ref": "#/components/schemas/ModelSourceType",
+ "description": "The type of source"
},
- "variant": {
+ "source_api_response": {
"anyOf": [
{
- "$ref": "#/components/schemas/ModelVariantType"
- },
- {
- "$ref": "#/components/schemas/ClipVariantType"
- },
- {
- "$ref": "#/components/schemas/FluxVariantType"
- },
- {
- "$ref": "#/components/schemas/Flux2VariantType"
- },
- {
- "$ref": "#/components/schemas/ZImageVariantType"
- },
- {
- "$ref": "#/components/schemas/QwenImageVariantType"
- },
- {
- "$ref": "#/components/schemas/Qwen3VariantType"
+ "type": "string"
},
{
"type": "null"
}
],
- "title": "Variant",
- "description": "The variant of the model."
+ "title": "Source Api Response",
+ "description": "The original API response from the source, as stringified JSON."
},
- "prediction_type": {
+ "source_url": {
"anyOf": [
{
- "$ref": "#/components/schemas/SchedulerPredictionType"
+ "type": "string"
},
{
"type": "null"
}
],
- "description": "The prediction type of the model."
+ "title": "Source Url",
+ "description": "Optional URL for the model (e.g. download page or model page)."
},
- "upcast_attention": {
+ "cover_image": {
"anyOf": [
{
- "type": "boolean"
+ "type": "string"
},
{
"type": "null"
}
],
- "title": "Upcast Attention",
- "description": "Whether to upcast attention."
+ "title": "Cover Image",
+ "description": "Url for image to preview model"
},
"config_path": {
"anyOf": [
@@ -57914,422 +65595,176 @@
}
],
"title": "Config Path",
- "description": "Path to config file for model"
- }
- },
- "type": "object",
- "title": "ModelRecordChanges",
- "description": "A set of changes to apply to a model."
- },
- "ModelRecordOrderBy": {
- "type": "string",
- "enum": ["default", "type", "base", "name", "format", "size", "created_at", "updated_at", "path"],
- "title": "ModelRecordOrderBy",
- "description": "The order in which to return model summaries."
- },
- "ModelRelationshipBatchRequest": {
- "properties": {
- "model_keys": {
- "items": {
- "type": "string"
- },
- "type": "array",
- "title": "Model Keys",
- "description": "List of model keys to fetch related models for",
- "examples": [
- ["aa3b247f-90c9-4416-bfcd-aeaa57a5339e", "ac32b914-10ab-496e-a24a-3068724b9c35"],
- [
- "b1c2d3e4-f5a6-7890-abcd-ef1234567890",
- "12345678-90ab-cdef-1234-567890abcdef",
- "fedcba98-7654-3210-fedc-ba9876543210"
- ],
- ["3bb7c0eb-b6c8-469c-ad8c-4d69c06075e4"]
- ]
- }
- },
- "type": "object",
- "required": ["model_keys"],
- "title": "ModelRelationshipBatchRequest"
- },
- "ModelRelationshipCreateRequest": {
- "properties": {
- "model_key_1": {
- "type": "string",
- "title": "Model Key 1",
- "description": "The key of the first model in the relationship",
- "examples": [
- "aa3b247f-90c9-4416-bfcd-aeaa57a5339e",
- "ac32b914-10ab-496e-a24a-3068724b9c35",
- "d944abfd-c7c3-42e2-a4ff-da640b29b8b4",
- "b1c2d3e4-f5a6-7890-abcd-ef1234567890",
- "12345678-90ab-cdef-1234-567890abcdef",
- "fedcba98-7654-3210-fedc-ba9876543210"
- ]
+ "description": "Path to the config for this model, if any."
},
- "model_key_2": {
- "type": "string",
- "title": "Model Key 2",
- "description": "The key of the second model in the relationship",
- "examples": [
- "3bb7c0eb-b6c8-469c-ad8c-4d69c06075e4",
- "f0c3da4e-d9ff-42b5-a45c-23be75c887c9",
- "38170dd8-f1e5-431e-866c-2c81f1277fcc",
- "c57fea2d-7646-424c-b9ad-c0ba60fc68be",
- "10f7807b-ab54-46a9-ab03-600e88c630a1",
- "f6c1d267-cf87-4ee0-bee0-37e791eacab7"
- ]
- }
- },
- "type": "object",
- "required": ["model_key_1", "model_key_2"],
- "title": "ModelRelationshipCreateRequest"
- },
- "ModelRepoVariant": {
- "type": "string",
- "enum": ["", "fp16", "fp32", "onnx", "openvino", "flax"],
- "title": "ModelRepoVariant",
- "description": "Various hugging face variants on the diffusers format."
- },
- "ModelSourceType": {
- "type": "string",
- "enum": ["path", "url", "hf_repo_id", "external"],
- "title": "ModelSourceType",
- "description": "Model source type."
- },
- "ModelType": {
- "type": "string",
- "enum": [
- "onnx",
- "main",
- "vae",
- "lora",
- "control_lora",
- "controlnet",
- "embedding",
- "ip_adapter",
- "clip_vision",
- "clip_embed",
- "t2i_adapter",
- "t5_encoder",
- "qwen3_encoder",
- "qwen_vl_encoder",
- "spandrel_image_to_image",
- "siglip",
- "flux_redux",
- "llava_onevision",
- "text_llm",
- "external_image_generator",
- "unknown"
- ],
- "title": "ModelType",
- "description": "Model type."
- },
- "ModelVariantType": {
- "type": "string",
- "enum": ["normal", "inpaint", "depth"],
- "title": "ModelVariantType",
- "description": "Variant type."
- },
- "ModelsList": {
- "properties": {
- "models": {
- "items": {
- "oneOf": [
- {
- "$ref": "#/components/schemas/Main_Diffusers_SD1_Config"
- },
- {
- "$ref": "#/components/schemas/Main_Diffusers_SD2_Config"
- },
- {
- "$ref": "#/components/schemas/Main_Diffusers_SDXL_Config"
- },
- {
- "$ref": "#/components/schemas/Main_Diffusers_SDXLRefiner_Config"
- },
- {
- "$ref": "#/components/schemas/Main_Diffusers_SD3_Config"
- },
- {
- "$ref": "#/components/schemas/Main_Diffusers_FLUX_Config"
- },
- {
- "$ref": "#/components/schemas/Main_Diffusers_Flux2_Config"
- },
- {
- "$ref": "#/components/schemas/Main_Diffusers_CogView4_Config"
- },
- {
- "$ref": "#/components/schemas/Main_Diffusers_QwenImage_Config"
- },
- {
- "$ref": "#/components/schemas/Main_Diffusers_ZImage_Config"
- },
- {
- "$ref": "#/components/schemas/Main_Checkpoint_SD1_Config"
- },
- {
- "$ref": "#/components/schemas/Main_Checkpoint_SD2_Config"
- },
- {
- "$ref": "#/components/schemas/Main_Checkpoint_SDXL_Config"
- },
- {
- "$ref": "#/components/schemas/Main_Checkpoint_SDXLRefiner_Config"
- },
- {
- "$ref": "#/components/schemas/Main_Checkpoint_Flux2_Config"
- },
- {
- "$ref": "#/components/schemas/Main_Checkpoint_FLUX_Config"
- },
- {
- "$ref": "#/components/schemas/Main_Checkpoint_QwenImage_Config"
- },
- {
- "$ref": "#/components/schemas/Main_Checkpoint_ZImage_Config"
- },
- {
- "$ref": "#/components/schemas/Main_Checkpoint_Anima_Config"
- },
- {
- "$ref": "#/components/schemas/Main_BnBNF4_FLUX_Config"
- },
- {
- "$ref": "#/components/schemas/Main_GGUF_Flux2_Config"
- },
- {
- "$ref": "#/components/schemas/Main_GGUF_FLUX_Config"
- },
- {
- "$ref": "#/components/schemas/Main_GGUF_QwenImage_Config"
- },
- {
- "$ref": "#/components/schemas/Main_GGUF_ZImage_Config"
- },
- {
- "$ref": "#/components/schemas/VAE_Checkpoint_SD1_Config"
- },
- {
- "$ref": "#/components/schemas/VAE_Checkpoint_SD2_Config"
- },
- {
- "$ref": "#/components/schemas/VAE_Checkpoint_SDXL_Config"
- },
- {
- "$ref": "#/components/schemas/VAE_Checkpoint_FLUX_Config"
- },
- {
- "$ref": "#/components/schemas/VAE_Checkpoint_Flux2_Config"
- },
- {
- "$ref": "#/components/schemas/VAE_Checkpoint_QwenImage_Config"
- },
- {
- "$ref": "#/components/schemas/VAE_Checkpoint_Anima_Config"
- },
- {
- "$ref": "#/components/schemas/VAE_Diffusers_SD1_Config"
- },
- {
- "$ref": "#/components/schemas/VAE_Diffusers_SDXL_Config"
- },
- {
- "$ref": "#/components/schemas/VAE_Diffusers_Flux2_Config"
- },
- {
- "$ref": "#/components/schemas/ControlNet_Checkpoint_SD1_Config"
- },
- {
- "$ref": "#/components/schemas/ControlNet_Checkpoint_SD2_Config"
- },
- {
- "$ref": "#/components/schemas/ControlNet_Checkpoint_SDXL_Config"
- },
- {
- "$ref": "#/components/schemas/ControlNet_Checkpoint_FLUX_Config"
- },
- {
- "$ref": "#/components/schemas/ControlNet_Checkpoint_ZImage_Config"
- },
- {
- "$ref": "#/components/schemas/ControlNet_Checkpoint_Anima_Config"
- },
- {
- "$ref": "#/components/schemas/ControlNet_Diffusers_SD1_Config"
- },
- {
- "$ref": "#/components/schemas/ControlNet_Diffusers_SD2_Config"
- },
- {
- "$ref": "#/components/schemas/ControlNet_Diffusers_SDXL_Config"
- },
- {
- "$ref": "#/components/schemas/ControlNet_Diffusers_FLUX_Config"
- },
- {
- "$ref": "#/components/schemas/LoRA_LyCORIS_SD1_Config"
- },
- {
- "$ref": "#/components/schemas/LoRA_LyCORIS_SD2_Config"
- },
- {
- "$ref": "#/components/schemas/LoRA_LyCORIS_SDXL_Config"
- },
- {
- "$ref": "#/components/schemas/LoRA_LyCORIS_Flux2_Config"
- },
- {
- "$ref": "#/components/schemas/LoRA_LyCORIS_FLUX_Config"
- },
- {
- "$ref": "#/components/schemas/LoRA_LyCORIS_ZImage_Config"
- },
- {
- "$ref": "#/components/schemas/LoRA_LyCORIS_QwenImage_Config"
- },
- {
- "$ref": "#/components/schemas/LoRA_LyCORIS_Anima_Config"
- },
- {
- "$ref": "#/components/schemas/LoRA_OMI_SDXL_Config"
- },
- {
- "$ref": "#/components/schemas/LoRA_OMI_FLUX_Config"
- },
- {
- "$ref": "#/components/schemas/LoRA_Diffusers_SD1_Config"
- },
- {
- "$ref": "#/components/schemas/LoRA_Diffusers_SD2_Config"
- },
- {
- "$ref": "#/components/schemas/LoRA_Diffusers_SDXL_Config"
- },
- {
- "$ref": "#/components/schemas/LoRA_Diffusers_Flux2_Config"
- },
- {
- "$ref": "#/components/schemas/LoRA_Diffusers_FLUX_Config"
- },
- {
- "$ref": "#/components/schemas/LoRA_Diffusers_ZImage_Config"
- },
- {
- "$ref": "#/components/schemas/ControlLoRA_LyCORIS_FLUX_Config"
- },
- {
- "$ref": "#/components/schemas/T5Encoder_T5Encoder_Config"
- },
- {
- "$ref": "#/components/schemas/T5Encoder_BnBLLMint8_Config"
- },
- {
- "$ref": "#/components/schemas/Qwen3Encoder_Qwen3Encoder_Config"
- },
- {
- "$ref": "#/components/schemas/Qwen3Encoder_Checkpoint_Config"
- },
- {
- "$ref": "#/components/schemas/Qwen3Encoder_GGUF_Config"
- },
- {
- "$ref": "#/components/schemas/QwenVLEncoder_Diffusers_Config"
- },
- {
- "$ref": "#/components/schemas/QwenVLEncoder_Checkpoint_Config"
- },
- {
- "$ref": "#/components/schemas/TI_File_SD1_Config"
- },
- {
- "$ref": "#/components/schemas/TI_File_SD2_Config"
- },
- {
- "$ref": "#/components/schemas/TI_File_SDXL_Config"
- },
- {
- "$ref": "#/components/schemas/TI_Folder_SD1_Config"
- },
- {
- "$ref": "#/components/schemas/TI_Folder_SD2_Config"
- },
- {
- "$ref": "#/components/schemas/TI_Folder_SDXL_Config"
- },
- {
- "$ref": "#/components/schemas/IPAdapter_InvokeAI_SD1_Config"
- },
- {
- "$ref": "#/components/schemas/IPAdapter_InvokeAI_SD2_Config"
- },
- {
- "$ref": "#/components/schemas/IPAdapter_InvokeAI_SDXL_Config"
- },
- {
- "$ref": "#/components/schemas/IPAdapter_Checkpoint_SD1_Config"
- },
- {
- "$ref": "#/components/schemas/IPAdapter_Checkpoint_SD2_Config"
- },
- {
- "$ref": "#/components/schemas/IPAdapter_Checkpoint_SDXL_Config"
- },
- {
- "$ref": "#/components/schemas/IPAdapter_Checkpoint_FLUX_Config"
- },
- {
- "$ref": "#/components/schemas/T2IAdapter_Diffusers_SD1_Config"
- },
- {
- "$ref": "#/components/schemas/T2IAdapter_Diffusers_SDXL_Config"
- },
- {
- "$ref": "#/components/schemas/Spandrel_Checkpoint_Config"
- },
- {
- "$ref": "#/components/schemas/CLIPEmbed_Diffusers_G_Config"
- },
- {
- "$ref": "#/components/schemas/CLIPEmbed_Diffusers_L_Config"
- },
- {
- "$ref": "#/components/schemas/CLIPVision_Diffusers_Config"
- },
- {
- "$ref": "#/components/schemas/SigLIP_Diffusers_Config"
- },
- {
- "$ref": "#/components/schemas/FLUXRedux_Checkpoint_Config"
- },
- {
- "$ref": "#/components/schemas/LlavaOnevision_Diffusers_Config"
- },
- {
- "$ref": "#/components/schemas/TextLLM_Diffusers_Config"
- },
- {
- "$ref": "#/components/schemas/ExternalApiModelConfig"
- },
- {
- "$ref": "#/components/schemas/Unknown_Config"
- }
- ]
- },
- "type": "array",
- "title": "Models"
+ "base": {
+ "type": "string",
+ "const": "any",
+ "title": "Base",
+ "default": "any"
+ },
+ "type": {
+ "type": "string",
+ "const": "qwen_vl_encoder",
+ "title": "Type",
+ "default": "qwen_vl_encoder"
+ },
+ "format": {
+ "type": "string",
+ "const": "checkpoint",
+ "title": "Format",
+ "default": "checkpoint"
}
},
"type": "object",
- "required": ["models"],
- "title": "ModelsList",
- "description": "Return list of configs."
+ "required": [
+ "key",
+ "hash",
+ "path",
+ "file_size",
+ "name",
+ "description",
+ "source",
+ "source_type",
+ "source_api_response",
+ "source_url",
+ "cover_image",
+ "config_path",
+ "base",
+ "type",
+ "format"
+ ],
+ "title": "QwenVLEncoder_Checkpoint_Config",
+ "description": "Configuration for single-file Qwen2.5-VL encoder checkpoints (safetensors).\n\nThis matches ComfyUI-style consolidated single-file encoders such as\n`qwen_2.5_vl_7b_fp8_scaled.safetensors`, which bundle the language model\nand the visual tower into one file (typically with FP8 + per-tensor\n`weight_scale` ComfyUI quantization).\n\nThe matching tokenizer + processor are pulled from HuggingFace\n(`Qwen/Qwen2.5-VL-7B-Instruct`) on first use and cached for offline use."
},
- "MultiplyInvocation": {
+ "QwenVLEncoder_Diffusers_Config": {
+ "properties": {
+ "key": {
+ "type": "string",
+ "title": "Key",
+ "description": "A unique key for this model."
+ },
+ "hash": {
+ "type": "string",
+ "title": "Hash",
+ "description": "The hash of the model file(s)."
+ },
+ "path": {
+ "type": "string",
+ "title": "Path",
+ "description": "Path to the model on the filesystem. Relative paths are relative to the Invoke root directory."
+ },
+ "file_size": {
+ "type": "integer",
+ "title": "File Size",
+ "description": "The size of the model in bytes."
+ },
+ "name": {
+ "type": "string",
+ "title": "Name",
+ "description": "Name of the model."
+ },
+ "description": {
+ "anyOf": [
+ {
+ "type": "string"
+ },
+ {
+ "type": "null"
+ }
+ ],
+ "title": "Description",
+ "description": "Model description"
+ },
+ "source": {
+ "type": "string",
+ "title": "Source",
+ "description": "The original source of the model (path, URL or repo_id)."
+ },
+ "source_type": {
+ "$ref": "#/components/schemas/ModelSourceType",
+ "description": "The type of source"
+ },
+ "source_api_response": {
+ "anyOf": [
+ {
+ "type": "string"
+ },
+ {
+ "type": "null"
+ }
+ ],
+ "title": "Source Api Response",
+ "description": "The original API response from the source, as stringified JSON."
+ },
+ "source_url": {
+ "anyOf": [
+ {
+ "type": "string"
+ },
+ {
+ "type": "null"
+ }
+ ],
+ "title": "Source Url",
+ "description": "Optional URL for the model (e.g. download page or model page)."
+ },
+ "cover_image": {
+ "anyOf": [
+ {
+ "type": "string"
+ },
+ {
+ "type": "null"
+ }
+ ],
+ "title": "Cover Image",
+ "description": "Url for image to preview model"
+ },
+ "base": {
+ "type": "string",
+ "const": "any",
+ "title": "Base",
+ "default": "any"
+ },
+ "type": {
+ "type": "string",
+ "const": "qwen_vl_encoder",
+ "title": "Type",
+ "default": "qwen_vl_encoder"
+ },
+ "format": {
+ "type": "string",
+ "const": "qwen_vl_encoder",
+ "title": "Format",
+ "default": "qwen_vl_encoder"
+ }
+ },
+ "type": "object",
+ "required": [
+ "key",
+ "hash",
+ "path",
+ "file_size",
+ "name",
+ "description",
+ "source",
+ "source_type",
+ "source_api_response",
+ "source_url",
+ "cover_image",
+ "base",
+ "type",
+ "format"
+ ],
+ "title": "QwenVLEncoder_Diffusers_Config",
+ "description": "Configuration for standalone Qwen2.5-VL encoder models in diffusers-style folder layout.\n\nExpected structure:\n /\n text_encoder/\n config.json (with `_class_name` or `architectures` listing\n `Qwen2_5_VLForConditionalGeneration`)\n model.safetensors\n tokenizer/\n tokenizer_config.json\n ...\n processor/ (optional, for vision preprocessing)\n preprocessor_config.json\n\nThis lets users avoid downloading the full ~40 GB Qwen Image diffusers pipeline\nwhen they only need the Qwen2.5-VL encoder for use with a GGUF transformer."
+ },
+ "RandomFloatInvocation": {
"category": "math",
"class": "invocation",
"classification": "stable",
- "description": "Multiplies two numbers",
+ "description": "Outputs a single random float",
"node_pack": "invokeai",
"properties": {
"id": {
@@ -58350,160 +65785,64 @@
"ui_type": "IsIntermediate"
},
"use_cache": {
- "default": true,
+ "default": false,
"description": "Whether or not to use the cache",
"field_kind": "node_attribute",
"title": "Use Cache",
"type": "boolean"
},
- "a": {
- "default": 0,
- "description": "The first number",
+ "low": {
+ "default": 0.0,
+ "description": "The inclusive low value",
"field_kind": "input",
"input": "any",
- "orig_default": 0,
+ "orig_default": 0.0,
"orig_required": false,
- "title": "A",
- "type": "integer"
+ "title": "Low",
+ "type": "number"
},
- "b": {
- "default": 0,
- "description": "The second number",
+ "high": {
+ "default": 1.0,
+ "description": "The exclusive high value",
"field_kind": "input",
"input": "any",
- "orig_default": 0,
+ "orig_default": 1.0,
"orig_required": false,
- "title": "B",
+ "title": "High",
+ "type": "number"
+ },
+ "decimals": {
+ "default": 2,
+ "description": "The number of decimal places to round to",
+ "field_kind": "input",
+ "input": "any",
+ "orig_default": 2,
+ "orig_required": false,
+ "title": "Decimals",
"type": "integer"
},
"type": {
- "const": "mul",
- "default": "mul",
+ "const": "rand_float",
+ "default": "rand_float",
"field_kind": "node_attribute",
"title": "type",
"type": "string"
}
},
"required": ["type", "id"],
- "tags": ["math", "multiply"],
- "title": "Multiply Integers",
+ "tags": ["math", "float", "random"],
+ "title": "Random Float",
"type": "object",
"version": "1.0.1",
"output": {
- "$ref": "#/components/schemas/IntegerOutput"
+ "$ref": "#/components/schemas/FloatOutput"
}
},
- "NodeFieldValue": {
- "properties": {
- "node_path": {
- "type": "string",
- "title": "Node Path",
- "description": "The node into which this batch data item will be substituted."
- },
- "field_name": {
- "type": "string",
- "title": "Field Name",
- "description": "The field into which this batch data item will be substituted."
- },
- "value": {
- "anyOf": [
- {
- "type": "string"
- },
- {
- "type": "number"
- },
- {
- "type": "integer"
- },
- {
- "$ref": "#/components/schemas/ImageField"
- },
- {
- "items": {
- "anyOf": [
- {
- "type": "string"
- },
- {
- "type": "number"
- },
- {
- "type": "integer"
- },
- {
- "$ref": "#/components/schemas/ImageField"
- }
- ]
- },
- "type": "array"
- }
- ],
- "title": "Value",
- "description": "The value to substitute into the node/field."
- }
- },
- "type": "object",
- "required": ["node_path", "field_name", "value"],
- "title": "NodeFieldValue"
- },
- "NodePackInfo": {
- "properties": {
- "name": {
- "type": "string",
- "title": "Name",
- "description": "The name of the node pack."
- },
- "path": {
- "type": "string",
- "title": "Path",
- "description": "The path to the node pack directory."
- },
- "node_count": {
- "type": "integer",
- "title": "Node Count",
- "description": "The number of nodes in the pack."
- },
- "node_types": {
- "items": {
- "type": "string"
- },
- "type": "array",
- "title": "Node Types",
- "description": "The invocation types provided by this node pack."
- }
- },
- "type": "object",
- "required": ["name", "path", "node_count", "node_types"],
- "title": "NodePackInfo",
- "description": "Information about an installed node pack."
- },
- "NodePackListResponse": {
- "properties": {
- "node_packs": {
- "items": {
- "$ref": "#/components/schemas/NodePackInfo"
- },
- "type": "array",
- "title": "Node Packs",
- "description": "List of installed node packs."
- },
- "custom_nodes_path": {
- "type": "string",
- "title": "Custom Nodes Path",
- "description": "The configured custom nodes directory path."
- }
- },
- "type": "object",
- "required": ["node_packs", "custom_nodes_path"],
- "title": "NodePackListResponse",
- "description": "Response for listing installed node packs."
- },
- "NoiseInvocation": {
- "category": "latents",
+ "RandomIntInvocation": {
+ "category": "math",
"class": "invocation",
"classification": "stable",
- "description": "Generates latent noise for supported denoiser architectures.",
+ "description": "Outputs a single random integer.",
"node_pack": "invokeai",
"properties": {
"id": {
@@ -58524,161 +65863,146 @@
"ui_type": "IsIntermediate"
},
"use_cache": {
- "default": true,
+ "default": false,
"description": "Whether or not to use the cache",
"field_kind": "node_attribute",
"title": "Use Cache",
"type": "boolean"
},
- "noise_type": {
- "default": "SD",
- "description": "Architecture-specific noise type.",
- "enum": ["SD", "FLUX", "FLUX.2", "SD3", "CogView4", "Z-Image", "Anima"],
- "field_kind": "input",
- "input": "any",
- "orig_default": "SD",
- "orig_required": false,
- "title": "Noise Type",
- "type": "string"
- },
- "seed": {
+ "low": {
"default": 0,
- "description": "Seed for random number generation",
+ "description": "The inclusive low value",
"field_kind": "input",
"input": "any",
- "maximum": 4294967295,
- "minimum": 0,
"orig_default": 0,
"orig_required": false,
- "title": "Seed",
- "type": "integer"
- },
- "width": {
- "default": 512,
- "description": "Width of output (px)",
- "exclusiveMinimum": 0,
- "field_kind": "input",
- "input": "any",
- "multipleOf": 8,
- "orig_default": 512,
- "orig_required": false,
- "title": "Width",
+ "title": "Low",
"type": "integer"
},
- "height": {
- "default": 512,
- "description": "Height of output (px)",
- "exclusiveMinimum": 0,
+ "high": {
+ "default": 2147483647,
+ "description": "The exclusive high value",
"field_kind": "input",
"input": "any",
- "multipleOf": 8,
- "orig_default": 512,
+ "orig_default": 2147483647,
"orig_required": false,
- "title": "Height",
+ "title": "High",
"type": "integer"
},
- "use_cpu": {
- "default": true,
- "description": "Use CPU for noise generation (for reproducible results across platforms)",
- "field_kind": "input",
- "input": "any",
- "orig_default": true,
- "orig_required": false,
- "title": "Use Cpu",
- "type": "boolean"
- },
"type": {
- "const": "noise",
- "default": "noise",
+ "const": "rand_int",
+ "default": "rand_int",
"field_kind": "node_attribute",
"title": "type",
"type": "string"
}
},
"required": ["type", "id"],
- "tags": ["latents", "noise"],
- "title": "Create Latent Noise",
+ "tags": ["math", "random"],
+ "title": "Random Integer",
"type": "object",
- "version": "1.1.0",
+ "version": "1.0.1",
"output": {
- "$ref": "#/components/schemas/NoiseOutput"
+ "$ref": "#/components/schemas/IntegerOutput"
}
},
- "NoiseOutput": {
- "class": "output",
- "description": "Invocation noise output",
- "properties": {
- "noise": {
- "$ref": "#/components/schemas/LatentsField",
- "description": "Noise tensor",
- "field_kind": "output",
- "ui_hidden": false
- },
- "width": {
- "description": "Width of output (px)",
- "field_kind": "output",
- "title": "Width",
- "type": "integer",
- "ui_hidden": false
- },
- "height": {
- "description": "Height of output (px)",
- "field_kind": "output",
- "title": "Height",
- "type": "integer",
- "ui_hidden": false
- },
- "type": {
- "const": "noise_output",
- "default": "noise_output",
- "field_kind": "node_attribute",
- "title": "type",
- "type": "string"
- }
- },
- "required": ["output_meta", "noise", "width", "height", "type", "type"],
- "title": "NoiseOutput",
- "type": "object"
- },
- "NormalMapInvocation": {
- "category": "controlnet_preprocessors",
+ "RandomRangeInvocation": {
+ "category": "batch",
"class": "invocation",
"classification": "stable",
- "description": "Generates a normal map.",
+ "description": "Creates a collection of random numbers",
"node_pack": "invokeai",
"properties": {
- "board": {
- "anyOf": [
- {
- "$ref": "#/components/schemas/BoardField"
- },
- {
- "type": "null"
- }
- ],
- "default": null,
- "description": "The board to save the image to",
- "field_kind": "internal",
+ "id": {
+ "description": "The id of this instance of an invocation. Must be unique among all instances of invocations.",
+ "field_kind": "node_attribute",
+ "title": "Id",
+ "type": "string"
+ },
+ "is_intermediate": {
+ "default": false,
+ "description": "Whether or not this is an intermediate invocation.",
+ "field_kind": "node_attribute",
"input": "direct",
+ "orig_required": true,
+ "title": "Is Intermediate",
+ "type": "boolean",
+ "ui_hidden": false,
+ "ui_type": "IsIntermediate"
+ },
+ "use_cache": {
+ "default": false,
+ "description": "Whether or not to use the cache",
+ "field_kind": "node_attribute",
+ "title": "Use Cache",
+ "type": "boolean"
+ },
+ "low": {
+ "default": 0,
+ "description": "The inclusive low value",
+ "field_kind": "input",
+ "input": "any",
+ "orig_default": 0,
"orig_required": false,
- "ui_hidden": false
+ "title": "Low",
+ "type": "integer"
},
- "metadata": {
- "anyOf": [
- {
- "$ref": "#/components/schemas/MetadataField"
- },
- {
- "type": "null"
- }
- ],
- "default": null,
- "description": "Optional metadata to be saved with the image",
- "field_kind": "internal",
- "input": "connection",
+ "high": {
+ "default": 2147483647,
+ "description": "The exclusive high value",
+ "field_kind": "input",
+ "input": "any",
+ "orig_default": 2147483647,
+ "orig_required": false,
+ "title": "High",
+ "type": "integer"
+ },
+ "size": {
+ "default": 1,
+ "description": "The number of values to generate",
+ "field_kind": "input",
+ "input": "any",
+ "orig_default": 1,
+ "orig_required": false,
+ "title": "Size",
+ "type": "integer"
+ },
+ "seed": {
+ "default": 0,
+ "description": "The seed for the RNG (omit for random)",
+ "field_kind": "input",
+ "input": "any",
+ "maximum": 4294967295,
+ "minimum": 0,
+ "orig_default": 0,
"orig_required": false,
- "ui_hidden": false
+ "title": "Seed",
+ "type": "integer"
},
+ "type": {
+ "const": "random_range",
+ "default": "random_range",
+ "field_kind": "node_attribute",
+ "title": "type",
+ "type": "string"
+ }
+ },
+ "required": ["type", "id"],
+ "tags": ["range", "integer", "random", "collection"],
+ "title": "Random Range",
+ "type": "object",
+ "version": "1.0.1",
+ "output": {
+ "$ref": "#/components/schemas/IntegerCollectionOutput"
+ }
+ },
+ "RangeInvocation": {
+ "category": "batch",
+ "class": "invocation",
+ "classification": "stable",
+ "description": "Creates a range of numbers from start to stop with step",
+ "node_pack": "invokeai",
+ "properties": {
"id": {
"description": "The id of this instance of an invocation. Must be unique among all instances of invocations.",
"field_kind": "node_attribute",
@@ -58703,137 +66027,60 @@
"title": "Use Cache",
"type": "boolean"
},
- "image": {
- "anyOf": [
- {
- "$ref": "#/components/schemas/ImageField"
- },
- {
- "type": "null"
- }
- ],
- "default": null,
- "description": "The image to process",
+ "start": {
+ "default": 0,
+ "description": "The start of the range",
"field_kind": "input",
"input": "any",
- "orig_required": true
+ "orig_default": 0,
+ "orig_required": false,
+ "title": "Start",
+ "type": "integer"
+ },
+ "stop": {
+ "default": 10,
+ "description": "The stop of the range",
+ "field_kind": "input",
+ "input": "any",
+ "orig_default": 10,
+ "orig_required": false,
+ "title": "Stop",
+ "type": "integer"
+ },
+ "step": {
+ "default": 1,
+ "description": "The step of the range",
+ "field_kind": "input",
+ "input": "any",
+ "orig_default": 1,
+ "orig_required": false,
+ "title": "Step",
+ "type": "integer"
},
"type": {
- "const": "normal_map",
- "default": "normal_map",
+ "const": "range",
+ "default": "range",
"field_kind": "node_attribute",
"title": "type",
"type": "string"
}
},
"required": ["type", "id"],
- "tags": ["controlnet", "normal"],
- "title": "Normal Map",
+ "tags": ["collection", "integer", "range"],
+ "title": "Integer Range",
"type": "object",
"version": "1.0.0",
"output": {
- "$ref": "#/components/schemas/ImageOutput"
+ "$ref": "#/components/schemas/IntegerCollectionOutput"
}
},
- "OffsetPaginatedResults_BoardDTO_": {
- "properties": {
- "limit": {
- "type": "integer",
- "title": "Limit",
- "description": "Limit of items to get"
- },
- "offset": {
- "type": "integer",
- "title": "Offset",
- "description": "Offset from which to retrieve items"
- },
- "total": {
- "type": "integer",
- "title": "Total",
- "description": "Total number of items in result"
- },
- "items": {
- "items": {
- "$ref": "#/components/schemas/BoardDTO"
- },
- "type": "array",
- "title": "Items",
- "description": "Items"
- }
- },
- "type": "object",
- "required": ["limit", "offset", "total", "items"],
- "title": "OffsetPaginatedResults[BoardDTO]"
- },
- "OffsetPaginatedResults_ImageDTO_": {
- "properties": {
- "limit": {
- "type": "integer",
- "title": "Limit",
- "description": "Limit of items to get"
- },
- "offset": {
- "type": "integer",
- "title": "Offset",
- "description": "Offset from which to retrieve items"
- },
- "total": {
- "type": "integer",
- "title": "Total",
- "description": "Total number of items in result"
- },
- "items": {
- "items": {
- "$ref": "#/components/schemas/ImageDTO"
- },
- "type": "array",
- "title": "Items",
- "description": "Items"
- }
- },
- "type": "object",
- "required": ["limit", "offset", "total", "items"],
- "title": "OffsetPaginatedResults[ImageDTO]"
- },
- "OklabUnsharpMaskInvocation": {
- "category": "image",
+ "RangeOfSizeInvocation": {
+ "category": "batch",
"class": "invocation",
"classification": "stable",
- "description": "Applies an unsharp mask filter to an image in the Oklab color space",
+ "description": "Creates a range from start to start + (size * step) incremented by step",
"node_pack": "invokeai",
"properties": {
- "board": {
- "anyOf": [
- {
- "$ref": "#/components/schemas/BoardField"
- },
- {
- "type": "null"
- }
- ],
- "default": null,
- "description": "The board to save the image to",
- "field_kind": "internal",
- "input": "direct",
- "orig_required": false,
- "ui_hidden": false
- },
- "metadata": {
- "anyOf": [
- {
- "$ref": "#/components/schemas/MetadataField"
- },
- {
- "type": "null"
- }
- ],
- "default": null,
- "description": "Optional metadata to be saved with the image",
- "field_kind": "internal",
- "input": "connection",
- "orig_required": false,
- "ui_hidden": false
- },
"id": {
"description": "The id of this instance of an invocation. Must be unique among all instances of invocations.",
"field_kind": "node_attribute",
@@ -58858,825 +66105,432 @@
"title": "Use Cache",
"type": "boolean"
},
- "image": {
- "anyOf": [
- {
- "$ref": "#/components/schemas/ImageField"
- },
- {
- "type": "null"
- }
- ],
- "default": null,
- "description": "The image to use",
+ "start": {
+ "default": 0,
+ "description": "The start of the range",
"field_kind": "input",
"input": "any",
- "orig_required": true
+ "orig_default": 0,
+ "orig_required": false,
+ "title": "Start",
+ "type": "integer"
},
- "radius": {
- "default": 2,
- "description": "Unsharp mask radius",
+ "size": {
+ "default": 1,
+ "description": "The number of values",
"exclusiveMinimum": 0,
"field_kind": "input",
"input": "any",
- "orig_default": 2,
+ "orig_default": 1,
"orig_required": false,
- "title": "Radius",
- "type": "number"
+ "title": "Size",
+ "type": "integer"
},
- "strength": {
- "default": 50,
- "description": "Unsharp mask strength",
+ "step": {
+ "default": 1,
+ "description": "The step of the range",
"field_kind": "input",
"input": "any",
- "minimum": 0,
- "orig_default": 50,
+ "orig_default": 1,
"orig_required": false,
- "title": "Strength",
- "type": "number"
+ "title": "Step",
+ "type": "integer"
},
"type": {
- "const": "unsharp_mask_oklab",
- "default": "unsharp_mask_oklab",
+ "const": "range_of_size",
+ "default": "range_of_size",
"field_kind": "node_attribute",
"title": "type",
"type": "string"
}
},
"required": ["type", "id"],
- "tags": ["image", "unsharp_mask", "oklab"],
- "title": "Unsharp Mask (Oklab)",
+ "tags": ["collection", "integer", "size", "range"],
+ "title": "Integer Range of Size",
"type": "object",
"version": "1.0.0",
"output": {
- "$ref": "#/components/schemas/ImageOutput"
+ "$ref": "#/components/schemas/IntegerCollectionOutput"
}
},
- "OklchImageHueAdjustmentInvocation": {
- "category": "image",
- "class": "invocation",
- "classification": "stable",
- "description": "Adjusts the hue of an image in Oklch space.",
- "node_pack": "invokeai",
+ "RecallParameter": {
"properties": {
- "board": {
+ "positive_prompt": {
"anyOf": [
{
- "$ref": "#/components/schemas/BoardField"
+ "type": "string"
},
{
"type": "null"
}
],
- "default": null,
- "description": "The board to save the image to",
- "field_kind": "internal",
- "input": "direct",
- "orig_required": false,
- "ui_hidden": false
+ "title": "Positive Prompt",
+ "description": "Positive prompt text"
},
- "metadata": {
+ "negative_prompt": {
"anyOf": [
{
- "$ref": "#/components/schemas/MetadataField"
+ "type": "string"
},
{
"type": "null"
}
],
- "default": null,
- "description": "Optional metadata to be saved with the image",
- "field_kind": "internal",
- "input": "connection",
- "orig_required": false,
- "ui_hidden": false
- },
- "id": {
- "description": "The id of this instance of an invocation. Must be unique among all instances of invocations.",
- "field_kind": "node_attribute",
- "title": "Id",
- "type": "string"
- },
- "is_intermediate": {
- "default": false,
- "description": "Whether or not this is an intermediate invocation.",
- "field_kind": "node_attribute",
- "input": "direct",
- "orig_required": true,
- "title": "Is Intermediate",
- "type": "boolean",
- "ui_hidden": false,
- "ui_type": "IsIntermediate"
- },
- "use_cache": {
- "default": true,
- "description": "Whether or not to use the cache",
- "field_kind": "node_attribute",
- "title": "Use Cache",
- "type": "boolean"
+ "title": "Negative Prompt",
+ "description": "Negative prompt text"
},
- "image": {
+ "model": {
"anyOf": [
{
- "$ref": "#/components/schemas/ImageField"
+ "type": "string"
},
{
"type": "null"
}
],
- "default": null,
- "description": "The image to adjust",
- "field_kind": "input",
- "input": "any",
- "orig_required": true
- },
- "hue": {
- "default": 0,
- "description": "The degrees by which to rotate the hue, 0-360",
- "field_kind": "input",
- "input": "any",
- "orig_default": 0,
- "orig_required": false,
- "title": "Hue",
- "type": "integer"
+ "title": "Model",
+ "description": "Main model name/identifier"
},
- "type": {
- "const": "img_hue_adjust_oklch",
- "default": "img_hue_adjust_oklch",
- "field_kind": "node_attribute",
- "title": "type",
- "type": "string"
- }
- },
- "required": ["type", "id"],
- "tags": ["image", "hue", "oklch"],
- "title": "Adjust Image Hue (Oklch)",
- "type": "object",
- "version": "1.0.0",
- "output": {
- "$ref": "#/components/schemas/ImageOutput"
- }
- },
- "OpenAIImageGenerationInvocation": {
- "category": "image",
- "class": "invocation",
- "classification": "stable",
- "description": "Generate images using an OpenAI-hosted external model.",
- "node_pack": "invokeai",
- "properties": {
- "board": {
+ "refiner_model": {
"anyOf": [
{
- "$ref": "#/components/schemas/BoardField"
+ "type": "string"
},
{
"type": "null"
}
],
- "default": null,
- "description": "The board to save the image to",
- "field_kind": "internal",
- "input": "direct",
- "orig_required": false,
- "ui_hidden": false
+ "title": "Refiner Model",
+ "description": "Refiner model name/identifier"
},
- "metadata": {
+ "vae_model": {
"anyOf": [
{
- "$ref": "#/components/schemas/MetadataField"
+ "type": "string"
},
{
"type": "null"
}
],
- "default": null,
- "description": "Optional metadata to be saved with the image",
- "field_kind": "internal",
- "input": "connection",
- "orig_required": false,
- "ui_hidden": false
- },
- "id": {
- "description": "The id of this instance of an invocation. Must be unique among all instances of invocations.",
- "field_kind": "node_attribute",
- "title": "Id",
- "type": "string"
+ "title": "Vae Model",
+ "description": "VAE model name/identifier"
},
- "is_intermediate": {
- "default": false,
- "description": "Whether or not this is an intermediate invocation.",
- "field_kind": "node_attribute",
- "input": "direct",
- "orig_required": true,
- "title": "Is Intermediate",
- "type": "boolean",
- "ui_hidden": false,
- "ui_type": "IsIntermediate"
+ "scheduler": {
+ "anyOf": [
+ {
+ "type": "string"
+ },
+ {
+ "type": "null"
+ }
+ ],
+ "title": "Scheduler",
+ "description": "Scheduler name"
},
- "use_cache": {
- "default": true,
- "description": "Whether or not to use the cache",
- "field_kind": "node_attribute",
- "title": "Use Cache",
- "type": "boolean"
+ "steps": {
+ "anyOf": [
+ {
+ "type": "integer",
+ "minimum": 1.0
+ },
+ {
+ "type": "null"
+ }
+ ],
+ "title": "Steps",
+ "description": "Number of generation steps"
},
- "model": {
+ "refiner_steps": {
"anyOf": [
{
- "$ref": "#/components/schemas/ModelIdentifierField"
+ "type": "integer",
+ "minimum": 0.0
},
{
"type": "null"
}
],
- "default": null,
- "description": "Main model (UNet, VAE, CLIP) to load",
- "field_kind": "input",
- "input": "any",
- "orig_required": true,
- "ui_model_base": ["external"],
- "ui_model_format": ["external_api"],
- "ui_model_provider_id": ["openai"],
- "ui_model_type": ["external_image_generator"]
+ "title": "Refiner Steps",
+ "description": "Number of refiner steps"
},
- "mode": {
- "default": "txt2img",
- "description": "Generation mode.",
- "enum": ["txt2img", "img2img", "inpaint"],
- "field_kind": "input",
- "input": "any",
- "orig_default": "txt2img",
- "orig_required": false,
- "title": "Mode",
- "type": "string",
- "ui_hidden": true
+ "cfg_scale": {
+ "anyOf": [
+ {
+ "type": "number"
+ },
+ {
+ "type": "null"
+ }
+ ],
+ "title": "Cfg Scale",
+ "description": "CFG scale for guidance"
},
- "prompt": {
+ "cfg_rescale_multiplier": {
"anyOf": [
{
- "type": "string"
+ "type": "number"
},
{
"type": "null"
}
],
- "default": null,
- "description": "Prompt",
- "field_kind": "input",
- "input": "any",
- "orig_required": true,
- "title": "Prompt"
+ "title": "Cfg Rescale Multiplier",
+ "description": "CFG rescale multiplier"
},
- "seed": {
+ "refiner_cfg_scale": {
"anyOf": [
{
- "type": "integer"
+ "type": "number"
},
{
"type": "null"
}
],
- "default": null,
- "description": "Seed for random number generation",
- "field_kind": "input",
- "input": "any",
- "orig_default": null,
- "orig_required": false,
- "title": "Seed"
+ "title": "Refiner Cfg Scale",
+ "description": "Refiner CFG scale"
},
- "num_images": {
- "default": 1,
- "description": "Number of images to generate",
- "exclusiveMinimum": 0,
- "field_kind": "input",
- "input": "any",
- "orig_default": 1,
- "orig_required": false,
- "title": "Num Images",
- "type": "integer"
+ "guidance": {
+ "anyOf": [
+ {
+ "type": "number"
+ },
+ {
+ "type": "null"
+ }
+ ],
+ "title": "Guidance",
+ "description": "Guidance scale"
},
"width": {
- "default": 1024,
- "description": "Width of output (px)",
- "exclusiveMinimum": 0,
- "field_kind": "input",
- "input": "any",
- "orig_default": 1024,
- "orig_required": false,
+ "anyOf": [
+ {
+ "type": "integer",
+ "minimum": 64.0
+ },
+ {
+ "type": "null"
+ }
+ ],
"title": "Width",
- "type": "integer"
+ "description": "Image width in pixels"
},
"height": {
- "default": 1024,
- "description": "Height of output (px)",
- "exclusiveMinimum": 0,
- "field_kind": "input",
- "input": "any",
- "orig_default": 1024,
- "orig_required": false,
+ "anyOf": [
+ {
+ "type": "integer",
+ "minimum": 64.0
+ },
+ {
+ "type": "null"
+ }
+ ],
"title": "Height",
- "type": "integer"
+ "description": "Image height in pixels"
},
- "image_size": {
+ "seed": {
"anyOf": [
{
- "type": "string"
+ "type": "integer",
+ "minimum": 0.0
},
{
"type": "null"
}
],
- "default": null,
- "description": "Image size preset (e.g. 1K, 2K, 4K)",
- "field_kind": "input",
- "input": "any",
- "orig_default": null,
- "orig_required": false,
- "title": "Image Size"
+ "title": "Seed",
+ "description": "Random seed"
},
- "init_image": {
+ "denoise_strength": {
"anyOf": [
{
- "$ref": "#/components/schemas/ImageField"
+ "type": "number",
+ "maximum": 1.0,
+ "minimum": 0.0
},
{
"type": "null"
}
],
- "default": null,
- "description": "Init image (use reference_images instead)",
- "field_kind": "input",
- "input": "any",
- "orig_default": null,
- "orig_required": false,
- "ui_hidden": true
+ "title": "Denoise Strength",
+ "description": "Denoising strength"
},
- "mask_image": {
+ "refiner_denoise_start": {
"anyOf": [
{
- "$ref": "#/components/schemas/ImageField"
+ "type": "number",
+ "maximum": 1.0,
+ "minimum": 0.0
},
{
"type": "null"
}
],
- "default": null,
- "description": "Mask image for inpaint",
- "field_kind": "input",
- "input": "any",
- "orig_default": null,
- "orig_required": false,
- "ui_hidden": true
- },
- "reference_images": {
- "default": [],
- "description": "Reference images",
- "field_kind": "input",
- "input": "any",
- "items": {
- "$ref": "#/components/schemas/ImageField"
- },
- "orig_default": [],
- "orig_required": false,
- "title": "Reference Images",
- "type": "array"
- },
- "quality": {
- "default": "auto",
- "description": "Output image quality",
- "enum": ["auto", "high", "medium", "low"],
- "field_kind": "input",
- "input": "any",
- "orig_default": "auto",
- "orig_required": false,
- "title": "Quality",
- "type": "string"
- },
- "background": {
- "default": "auto",
- "description": "Background transparency handling",
- "enum": ["auto", "transparent", "opaque"],
- "field_kind": "input",
- "input": "any",
- "orig_default": "auto",
- "orig_required": false,
- "title": "Background",
- "type": "string"
+ "title": "Refiner Denoise Start",
+ "description": "Refiner denoising start"
},
- "input_fidelity": {
+ "clip_skip": {
"anyOf": [
{
- "enum": ["low", "high"],
- "type": "string"
+ "type": "integer",
+ "minimum": 0.0
},
{
"type": "null"
}
],
- "default": null,
- "description": "Fidelity to source images (edits only)",
- "field_kind": "input",
- "input": "any",
- "orig_default": null,
- "orig_required": false,
- "title": "Input Fidelity"
- },
- "type": {
- "const": "openai_image_generation",
- "default": "openai_image_generation",
- "field_kind": "node_attribute",
- "title": "type",
- "type": "string"
- }
- },
- "required": ["type", "id"],
- "tags": ["external", "generation", "openai"],
- "title": "OpenAI Image Generation",
- "type": "object",
- "version": "1.0.0",
- "output": {
- "$ref": "#/components/schemas/ImageCollectionOutput"
- }
- },
- "OrphanedModelInfo": {
- "properties": {
- "path": {
- "type": "string",
- "title": "Path",
- "description": "Relative path to the orphaned directory from models root"
- },
- "absolute_path": {
- "type": "string",
- "title": "Absolute Path",
- "description": "Absolute path to the orphaned directory"
- },
- "files": {
- "items": {
- "type": "string"
- },
- "type": "array",
- "title": "Files",
- "description": "List of model files in this directory"
- },
- "size_bytes": {
- "type": "integer",
- "title": "Size Bytes",
- "description": "Total size of all files in bytes"
- }
- },
- "type": "object",
- "required": ["path", "absolute_path", "files", "size_bytes"],
- "title": "OrphanedModelInfo",
- "description": "Information about an orphaned model directory."
- },
- "OutputFieldJSONSchemaExtra": {
- "description": "Extra attributes to be added to input fields and their OpenAPI schema. Used by the workflow editor\nduring schema parsing and UI rendering.",
- "properties": {
- "field_kind": {
- "$ref": "#/components/schemas/FieldKind"
- },
- "ui_hidden": {
- "default": false,
- "title": "Ui Hidden",
- "type": "boolean"
+ "title": "Clip Skip",
+ "description": "CLIP skip layers"
},
- "ui_order": {
+ "seamless_x": {
"anyOf": [
{
- "type": "integer"
+ "type": "boolean"
},
{
"type": "null"
}
],
- "default": null,
- "title": "Ui Order"
+ "title": "Seamless X",
+ "description": "Enable seamless X tiling"
},
- "ui_type": {
+ "seamless_y": {
"anyOf": [
{
- "$ref": "#/components/schemas/UIType"
+ "type": "boolean"
},
{
"type": "null"
}
],
- "default": null
- }
- },
- "required": ["field_kind", "ui_hidden", "ui_order", "ui_type"],
- "title": "OutputFieldJSONSchemaExtra",
- "type": "object"
- },
- "PBRMapsInvocation": {
- "category": "controlnet_preprocessors",
- "class": "invocation",
- "classification": "stable",
- "description": "Generate Normal, Displacement and Roughness Map from a given image",
- "node_pack": "invokeai",
- "properties": {
- "board": {
+ "title": "Seamless Y",
+ "description": "Enable seamless Y tiling"
+ },
+ "refiner_positive_aesthetic_score": {
"anyOf": [
{
- "$ref": "#/components/schemas/BoardField"
+ "type": "number"
},
{
"type": "null"
}
],
- "default": null,
- "description": "The board to save the image to",
- "field_kind": "internal",
- "input": "direct",
- "orig_required": false,
- "ui_hidden": false
+ "title": "Refiner Positive Aesthetic Score",
+ "description": "Refiner positive aesthetic score"
},
- "metadata": {
+ "refiner_negative_aesthetic_score": {
"anyOf": [
{
- "$ref": "#/components/schemas/MetadataField"
+ "type": "number"
},
{
"type": "null"
}
],
- "default": null,
- "description": "Optional metadata to be saved with the image",
- "field_kind": "internal",
- "input": "connection",
- "orig_required": false,
- "ui_hidden": false
- },
- "id": {
- "description": "The id of this instance of an invocation. Must be unique among all instances of invocations.",
- "field_kind": "node_attribute",
- "title": "Id",
- "type": "string"
- },
- "is_intermediate": {
- "default": false,
- "description": "Whether or not this is an intermediate invocation.",
- "field_kind": "node_attribute",
- "input": "direct",
- "orig_required": true,
- "title": "Is Intermediate",
- "type": "boolean",
- "ui_hidden": false,
- "ui_type": "IsIntermediate"
- },
- "use_cache": {
- "default": true,
- "description": "Whether or not to use the cache",
- "field_kind": "node_attribute",
- "title": "Use Cache",
- "type": "boolean"
+ "title": "Refiner Negative Aesthetic Score",
+ "description": "Refiner negative aesthetic score"
},
- "image": {
+ "loras": {
"anyOf": [
{
- "$ref": "#/components/schemas/ImageField"
+ "items": {
+ "$ref": "#/components/schemas/LoRARecallParameter"
+ },
+ "type": "array"
},
{
"type": "null"
}
],
- "default": null,
- "description": "Input image",
- "field_kind": "input",
- "input": "any",
- "orig_required": true
- },
- "tile_size": {
- "default": 512,
- "description": "Tile size",
- "field_kind": "input",
- "input": "any",
- "orig_default": 512,
- "orig_required": false,
- "title": "Tile Size",
- "type": "integer"
- },
- "border_mode": {
- "default": "none",
- "description": "Border mode to apply to eliminate any artifacts or seams",
- "enum": ["none", "seamless", "mirror", "replicate"],
- "field_kind": "input",
- "input": "any",
- "orig_default": "none",
- "orig_required": false,
- "title": "Border Mode",
- "type": "string"
- },
- "type": {
- "const": "pbr_maps",
- "default": "pbr_maps",
- "field_kind": "node_attribute",
- "title": "type",
- "type": "string"
- }
- },
- "required": ["type", "id"],
- "tags": ["image", "material"],
- "title": "PBR Maps",
- "type": "object",
- "version": "1.0.0",
- "output": {
- "$ref": "#/components/schemas/PBRMapsOutput"
- }
- },
- "PBRMapsOutput": {
- "class": "output",
- "properties": {
- "normal_map": {
- "$ref": "#/components/schemas/ImageField",
- "default": null,
- "description": "The generated normal map",
- "field_kind": "output",
- "ui_hidden": false
- },
- "roughness_map": {
- "$ref": "#/components/schemas/ImageField",
- "default": null,
- "description": "The generated roughness map",
- "field_kind": "output",
- "ui_hidden": false
- },
- "displacement_map": {
- "$ref": "#/components/schemas/ImageField",
- "default": null,
- "description": "The generated displacement map",
- "field_kind": "output",
- "ui_hidden": false
- },
- "type": {
- "const": "pbr_maps-output",
- "default": "pbr_maps-output",
- "field_kind": "node_attribute",
- "title": "type",
- "type": "string"
- }
- },
- "required": ["output_meta", "normal_map", "roughness_map", "displacement_map", "type", "type"],
- "title": "PBRMapsOutput",
- "type": "object"
- },
- "PaginatedResults_WorkflowRecordListItemWithThumbnailDTO_": {
- "properties": {
- "page": {
- "type": "integer",
- "title": "Page",
- "description": "Current Page"
- },
- "pages": {
- "type": "integer",
- "title": "Pages",
- "description": "Total number of pages"
- },
- "per_page": {
- "type": "integer",
- "title": "Per Page",
- "description": "Number of items per page"
- },
- "total": {
- "type": "integer",
- "title": "Total",
- "description": "Total number of items in result"
- },
- "items": {
- "items": {
- "$ref": "#/components/schemas/WorkflowRecordListItemWithThumbnailDTO"
- },
- "type": "array",
- "title": "Items",
- "description": "Items"
- }
- },
- "type": "object",
- "required": ["page", "pages", "per_page", "total", "items"],
- "title": "PaginatedResults[WorkflowRecordListItemWithThumbnailDTO]"
- },
- "PairTileImageInvocation": {
- "category": "tiles",
- "class": "invocation",
- "classification": "stable",
- "description": "Pair an image with its tile properties.",
- "node_pack": "invokeai",
- "properties": {
- "id": {
- "description": "The id of this instance of an invocation. Must be unique among all instances of invocations.",
- "field_kind": "node_attribute",
- "title": "Id",
- "type": "string"
- },
- "is_intermediate": {
- "default": false,
- "description": "Whether or not this is an intermediate invocation.",
- "field_kind": "node_attribute",
- "input": "direct",
- "orig_required": true,
- "title": "Is Intermediate",
- "type": "boolean",
- "ui_hidden": false,
- "ui_type": "IsIntermediate"
- },
- "use_cache": {
- "default": true,
- "description": "Whether or not to use the cache",
- "field_kind": "node_attribute",
- "title": "Use Cache",
- "type": "boolean"
+ "title": "Loras",
+ "description": "List of LoRAs with their weights"
},
- "image": {
+ "control_layers": {
"anyOf": [
{
- "$ref": "#/components/schemas/ImageField"
+ "items": {
+ "$ref": "#/components/schemas/ControlNetRecallParameter"
+ },
+ "type": "array"
},
{
"type": "null"
}
],
- "default": null,
- "description": "The tile image.",
- "field_kind": "input",
- "input": "any",
- "orig_required": true
+ "title": "Control Layers",
+ "description": "List of control adapters (ControlNet, T2I Adapter, Control LoRA) with their settings"
},
- "tile": {
+ "ip_adapters": {
"anyOf": [
{
- "$ref": "#/components/schemas/Tile"
+ "items": {
+ "$ref": "#/components/schemas/IPAdapterRecallParameter"
+ },
+ "type": "array"
},
{
"type": "null"
}
],
- "default": null,
- "description": "The tile properties.",
- "field_kind": "input",
- "input": "any",
- "orig_required": true
+ "title": "Ip Adapters",
+ "description": "List of IP Adapters with their settings"
},
- "type": {
- "const": "pair_tile_image",
- "default": "pair_tile_image",
- "field_kind": "node_attribute",
- "title": "type",
- "type": "string"
+ "reference_images": {
+ "anyOf": [
+ {
+ "items": {
+ "$ref": "#/components/schemas/ReferenceImageRecallParameter"
+ },
+ "type": "array"
+ },
+ {
+ "type": "null"
+ }
+ ],
+ "title": "Reference Images",
+ "description": "List of model-free reference images for architectures that consume reference images directly (FLUX.2 Klein, FLUX Kontext, Qwen Image Edit). The frontend picks the correct config type based on the currently-selected main model."
}
},
- "required": ["type", "id"],
- "tags": ["tiles"],
- "title": "Pair Tile with Image",
+ "additionalProperties": false,
"type": "object",
- "version": "1.0.1",
- "output": {
- "$ref": "#/components/schemas/PairTileImageOutput"
- }
+ "title": "RecallParameter",
+ "description": "Request model for updating recallable parameters."
},
- "PairTileImageOutput": {
- "class": "output",
+ "RecallParametersUpdatedEvent": {
+ "description": "Event model for recall_parameters_updated",
"properties": {
- "tile_with_image": {
- "$ref": "#/components/schemas/TileWithImage",
- "description": "A tile description with its corresponding image.",
- "field_kind": "output",
- "ui_hidden": false
+ "timestamp": {
+ "description": "The timestamp of the event",
+ "title": "Timestamp",
+ "type": "integer"
},
- "type": {
- "const": "pair_tile_image_output",
- "default": "pair_tile_image_output",
- "field_kind": "node_attribute",
- "title": "type",
+ "queue_id": {
+ "description": "The ID of the queue",
+ "title": "Queue Id",
"type": "string"
+ },
+ "user_id": {
+ "description": "The ID of the user whose recall parameters were updated",
+ "title": "User Id",
+ "type": "string"
+ },
+ "parameters": {
+ "additionalProperties": true,
+ "description": "The recall parameters that were updated",
+ "title": "Parameters",
+ "type": "object"
}
},
- "required": ["output_meta", "tile_with_image", "type", "type"],
- "title": "PairTileImageOutput",
+ "required": ["timestamp", "queue_id", "user_id", "parameters"],
+ "title": "RecallParametersUpdatedEvent",
"type": "object"
},
- "PasteImageIntoBoundingBoxInvocation": {
- "category": "image",
- "class": "invocation",
- "classification": "stable",
- "description": "Paste the source image into the target image at the given bounding box.\n\nThe source image must be the same size as the bounding box, and the bounding box must fit within the target image.",
- "node_pack": "invokeai",
- "properties": {
- "board": {
- "anyOf": [
- {
- "$ref": "#/components/schemas/BoardField"
- },
- {
- "type": "null"
- }
- ],
- "default": null,
- "description": "The board to save the image to",
- "field_kind": "internal",
- "input": "direct",
- "orig_required": false,
- "ui_hidden": false
- },
+ "RectangleMaskInvocation": {
+ "category": "mask",
+ "class": "invocation",
+ "classification": "stable",
+ "description": "Create a rectangular mask.",
+ "node_pack": "invokeai",
+ "properties": {
"metadata": {
"anyOf": [
{
@@ -59717,107 +66571,231 @@
"title": "Use Cache",
"type": "boolean"
},
- "source_image": {
+ "width": {
"anyOf": [
{
- "$ref": "#/components/schemas/ImageField"
+ "type": "integer"
},
{
"type": "null"
}
],
"default": null,
- "description": "The image to paste",
+ "description": "The width of the entire mask.",
"field_kind": "input",
"input": "any",
- "orig_required": true
+ "orig_required": true,
+ "title": "Width"
},
- "target_image": {
+ "height": {
"anyOf": [
{
- "$ref": "#/components/schemas/ImageField"
+ "type": "integer"
},
{
"type": "null"
}
],
"default": null,
- "description": "The image to paste into",
+ "description": "The height of the entire mask.",
"field_kind": "input",
"input": "any",
- "orig_required": true
+ "orig_required": true,
+ "title": "Height"
},
- "bounding_box": {
+ "x_left": {
"anyOf": [
{
- "$ref": "#/components/schemas/BoundingBoxField"
+ "type": "integer"
},
{
"type": "null"
}
],
"default": null,
- "description": "The bounding box to paste the image into",
+ "description": "The left x-coordinate of the rectangular masked region (inclusive).",
"field_kind": "input",
"input": "any",
- "orig_required": true
+ "orig_required": true,
+ "title": "X Left"
+ },
+ "y_top": {
+ "anyOf": [
+ {
+ "type": "integer"
+ },
+ {
+ "type": "null"
+ }
+ ],
+ "default": null,
+ "description": "The top y-coordinate of the rectangular masked region (inclusive).",
+ "field_kind": "input",
+ "input": "any",
+ "orig_required": true,
+ "title": "Y Top"
+ },
+ "rectangle_width": {
+ "anyOf": [
+ {
+ "type": "integer"
+ },
+ {
+ "type": "null"
+ }
+ ],
+ "default": null,
+ "description": "The width of the rectangular masked region.",
+ "field_kind": "input",
+ "input": "any",
+ "orig_required": true,
+ "title": "Rectangle Width"
+ },
+ "rectangle_height": {
+ "anyOf": [
+ {
+ "type": "integer"
+ },
+ {
+ "type": "null"
+ }
+ ],
+ "default": null,
+ "description": "The height of the rectangular masked region.",
+ "field_kind": "input",
+ "input": "any",
+ "orig_required": true,
+ "title": "Rectangle Height"
},
"type": {
- "const": "paste_image_into_bounding_box",
- "default": "paste_image_into_bounding_box",
+ "const": "rectangle_mask",
+ "default": "rectangle_mask",
"field_kind": "node_attribute",
"title": "type",
"type": "string"
}
},
"required": ["type", "id"],
- "tags": ["image", "crop"],
- "title": "Paste Image into Bounding Box",
+ "tags": ["conditioning"],
+ "title": "Create Rectangle Mask",
"type": "object",
- "version": "1.0.0",
+ "version": "1.0.1",
"output": {
- "$ref": "#/components/schemas/ImageOutput"
+ "$ref": "#/components/schemas/MaskOutput"
}
},
- "PiDiNetEdgeDetectionInvocation": {
- "category": "controlnet_preprocessors",
- "class": "invocation",
- "classification": "stable",
- "description": "Generates an edge map using PiDiNet.",
- "node_pack": "invokeai",
+ "ReferenceImageRecallParameter": {
"properties": {
- "board": {
+ "image_name": {
+ "type": "string",
+ "title": "Image Name",
+ "description": "The filename of the reference image in outputs/images"
+ }
+ },
+ "type": "object",
+ "required": ["image_name"],
+ "title": "ReferenceImageRecallParameter",
+ "description": "Global reference-image configuration for recall.\n\nUsed for reference images that feed directly into the main model rather\nthan through a separate IP-Adapter / ControlNet model \u2014 for example\nFLUX.2 Klein, FLUX Kontext, and Qwen Image Edit. The receiving frontend\npicks the correct config type (``flux2_reference_image`` /\n``qwen_image_reference_image`` / ``flux_kontext_reference_image``) based\non the currently-selected main model."
+ },
+ "RemoteModelFile": {
+ "properties": {
+ "url": {
+ "type": "string",
+ "minLength": 1,
+ "format": "uri",
+ "title": "Url",
+ "description": "The url to download this model file"
+ },
+ "path": {
+ "type": "string",
+ "format": "path",
+ "title": "Path",
+ "description": "The path to the file, relative to the model root"
+ },
+ "size": {
"anyOf": [
{
- "$ref": "#/components/schemas/BoardField"
+ "type": "integer"
},
{
"type": "null"
}
],
- "default": null,
- "description": "The board to save the image to",
- "field_kind": "internal",
- "input": "direct",
- "orig_required": false,
- "ui_hidden": false
+ "title": "Size",
+ "description": "The size of this file, in bytes",
+ "default": 0
},
- "metadata": {
+ "sha256": {
"anyOf": [
{
- "$ref": "#/components/schemas/MetadataField"
+ "type": "string"
},
{
"type": "null"
}
],
- "default": null,
- "description": "Optional metadata to be saved with the image",
- "field_kind": "internal",
- "input": "connection",
- "orig_required": false,
- "ui_hidden": false
+ "title": "Sha256",
+ "description": "SHA256 hash of this model (not always available)"
+ }
+ },
+ "type": "object",
+ "required": ["url", "path"],
+ "title": "RemoteModelFile",
+ "description": "Information about a downloadable file that forms part of a model."
+ },
+ "RemoveImagesFromBoardResult": {
+ "properties": {
+ "affected_boards": {
+ "items": {
+ "type": "string"
+ },
+ "type": "array",
+ "title": "Affected Boards",
+ "description": "The ids of boards affected by the delete operation"
+ },
+ "removed_images": {
+ "items": {
+ "type": "string"
+ },
+ "type": "array",
+ "title": "Removed Images",
+ "description": "The image names that were removed from their board"
+ }
+ },
+ "type": "object",
+ "required": ["affected_boards", "removed_images"],
+ "title": "RemoveImagesFromBoardResult"
+ },
+ "RemoveVideosFromBoardResult": {
+ "properties": {
+ "affected_boards": {
+ "items": {
+ "type": "string"
+ },
+ "type": "array",
+ "title": "Affected Boards",
+ "description": "The ids of boards affected by the operation"
},
+ "removed_videos": {
+ "items": {
+ "type": "string"
+ },
+ "type": "array",
+ "title": "Removed Videos",
+ "description": "The video names that were removed from their board"
+ }
+ },
+ "type": "object",
+ "required": ["affected_boards", "removed_videos"],
+ "title": "RemoveVideosFromBoardResult"
+ },
+ "ResizeLatentsInvocation": {
+ "category": "latents",
+ "class": "invocation",
+ "classification": "stable",
+ "description": "Resizes latents to explicit width/height (in pixels). Provided dimensions are floor-divided by 8.",
+ "node_pack": "invokeai",
+ "properties": {
"id": {
"description": "The id of this instance of an invocation. Must be unique among all instances of invocations.",
"field_kind": "node_attribute",
@@ -59842,111 +66820,126 @@
"title": "Use Cache",
"type": "boolean"
},
- "image": {
+ "latents": {
"anyOf": [
{
- "$ref": "#/components/schemas/ImageField"
+ "$ref": "#/components/schemas/LatentsField"
},
{
"type": "null"
}
],
"default": null,
- "description": "The image to process",
+ "description": "Latents tensor",
"field_kind": "input",
- "input": "any",
+ "input": "connection",
"orig_required": true
},
- "quantize_edges": {
- "default": false,
- "description": "Whether or not to use safe mode",
+ "width": {
+ "anyOf": [
+ {
+ "minimum": 64,
+ "multipleOf": 8,
+ "type": "integer"
+ },
+ {
+ "type": "null"
+ }
+ ],
+ "default": null,
+ "description": "Width of output (px)",
"field_kind": "input",
"input": "any",
- "orig_default": false,
+ "orig_required": true,
+ "title": "Width"
+ },
+ "height": {
+ "anyOf": [
+ {
+ "minimum": 64,
+ "multipleOf": 8,
+ "type": "integer"
+ },
+ {
+ "type": "null"
+ }
+ ],
+ "default": null,
+ "description": "Width of output (px)",
+ "field_kind": "input",
+ "input": "any",
+ "orig_required": true,
+ "title": "Height"
+ },
+ "mode": {
+ "default": "bilinear",
+ "description": "Interpolation mode",
+ "enum": ["nearest", "linear", "bilinear", "bicubic", "trilinear", "area", "nearest-exact"],
+ "field_kind": "input",
+ "input": "any",
+ "orig_default": "bilinear",
"orig_required": false,
- "title": "Quantize Edges",
- "type": "boolean"
+ "title": "Mode",
+ "type": "string"
},
- "scribble": {
+ "antialias": {
"default": false,
- "description": "Whether or not to use scribble mode",
+ "description": "Whether or not to apply antialiasing (bilinear or bicubic only)",
"field_kind": "input",
"input": "any",
"orig_default": false,
"orig_required": false,
- "title": "Scribble",
+ "title": "Antialias",
"type": "boolean"
},
"type": {
- "const": "pidi_edge_detection",
- "default": "pidi_edge_detection",
+ "const": "lresize",
+ "default": "lresize",
"field_kind": "node_attribute",
"title": "type",
"type": "string"
}
},
"required": ["type", "id"],
- "tags": ["controlnet", "edge"],
- "title": "PiDiNet Edge Detection",
+ "tags": ["latents", "resize"],
+ "title": "Resize Latents",
"type": "object",
- "version": "1.0.0",
+ "version": "1.0.2",
"output": {
- "$ref": "#/components/schemas/ImageOutput"
+ "$ref": "#/components/schemas/LatentsOutput"
}
},
- "PresetData": {
- "properties": {
- "positive_prompt": {
- "type": "string",
- "title": "Positive Prompt",
- "description": "Positive prompt"
- },
- "negative_prompt": {
- "type": "string",
- "title": "Negative Prompt",
- "description": "Negative prompt"
- }
- },
- "additionalProperties": false,
- "type": "object",
- "required": ["positive_prompt", "negative_prompt"],
- "title": "PresetData"
- },
- "PresetType": {
+ "ResourceOrigin": {
"type": "string",
- "enum": ["user", "default"],
- "title": "PresetType"
+ "enum": ["internal", "external"],
+ "title": "ResourceOrigin",
+ "description": "The origin of a resource (eg image).\n\n- INTERNAL: The resource was created by the application.\n- EXTERNAL: The resource was not created by the application.\nThis may be a user-initiated upload, or an internal application upload (eg Canvas init image)."
},
- "ProgressImage": {
- "description": "The progress image sent intermittently during processing",
+ "RetryItemsResult": {
"properties": {
- "width": {
- "description": "The effective width of the image in pixels",
- "minimum": 1,
- "title": "Width",
- "type": "integer"
- },
- "height": {
- "description": "The effective height of the image in pixels",
- "minimum": 1,
- "title": "Height",
- "type": "integer"
+ "queue_id": {
+ "type": "string",
+ "title": "Queue Id",
+ "description": "The ID of the queue"
},
- "dataURL": {
- "description": "The image data as a b64 data URL",
- "title": "Dataurl",
- "type": "string"
+ "retried_item_ids": {
+ "items": {
+ "type": "integer"
+ },
+ "type": "array",
+ "title": "Retried Item Ids",
+ "description": "The IDs of the queue items that were retried"
}
},
- "required": ["width", "height", "dataURL"],
- "title": "ProgressImage",
- "type": "object"
+ "type": "object",
+ "required": ["queue_id", "retried_item_ids"],
+ "title": "RetryItemsResult"
},
- "PromptTemplateInvocation": {
- "category": "prompt",
+ "RoundInvocation": {
+ "category": "math",
"class": "invocation",
"classification": "stable",
- "description": "Applies a Style Preset template to positive and negative prompts.\n\nSelect a Style Preset and provide positive/negative prompts. The node replaces\n{prompt} placeholders in the template with your input prompts.",
+ "description": "Rounds a float to a specified number of decimal places.",
"node_pack": "invokeai",
"properties": {
"id": {
@@ -59973,97 +66966,159 @@
"title": "Use Cache",
"type": "boolean"
},
- "style_preset": {
- "anyOf": [
- {
- "$ref": "#/components/schemas/StylePresetField"
- },
- {
- "type": "null"
- }
- ],
- "default": null,
- "description": "The Style Preset to use as a template",
- "field_kind": "input",
- "input": "any",
- "orig_required": true
- },
- "positive_prompt": {
- "default": "",
- "description": "The positive prompt to insert into the template's {prompt} placeholder",
+ "value": {
+ "default": 0,
+ "description": "The float value",
"field_kind": "input",
"input": "any",
- "orig_default": "",
+ "orig_default": 0,
"orig_required": false,
- "title": "Positive Prompt",
- "type": "string",
- "ui_component": "textarea"
+ "title": "Value",
+ "type": "number"
},
- "negative_prompt": {
- "default": "",
- "description": "The negative prompt to insert into the template's {prompt} placeholder",
+ "decimals": {
+ "default": 0,
+ "description": "The number of decimal places",
"field_kind": "input",
"input": "any",
- "orig_default": "",
+ "orig_default": 0,
"orig_required": false,
- "title": "Negative Prompt",
- "type": "string",
- "ui_component": "textarea"
+ "title": "Decimals",
+ "type": "integer"
},
"type": {
- "const": "prompt_template",
- "default": "prompt_template",
+ "const": "round_float",
+ "default": "round_float",
"field_kind": "node_attribute",
"title": "type",
"type": "string"
}
},
"required": ["type", "id"],
- "tags": ["prompt", "template", "style", "preset"],
- "title": "Prompt Template",
+ "tags": ["math", "round"],
+ "title": "Round Float",
"type": "object",
- "version": "1.0.0",
+ "version": "1.0.1",
"output": {
- "$ref": "#/components/schemas/PromptTemplateOutput"
+ "$ref": "#/components/schemas/FloatOutput"
}
},
- "PromptTemplateOutput": {
+ "SAMPoint": {
+ "properties": {
+ "x": {
+ "description": "The x-coordinate of the point",
+ "title": "X",
+ "type": "integer"
+ },
+ "y": {
+ "description": "The y-coordinate of the point",
+ "title": "Y",
+ "type": "integer"
+ },
+ "label": {
+ "$ref": "#/components/schemas/SAMPointLabel",
+ "description": "The label of the point"
+ }
+ },
+ "required": ["x", "y", "label"],
+ "title": "SAMPoint",
+ "type": "object"
+ },
+ "SAMPointLabel": {
+ "enum": [-1, 0, 1],
+ "title": "SAMPointLabel",
+ "type": "integer"
+ },
+ "SAMPointsField": {
+ "properties": {
+ "points": {
+ "description": "The points of the object",
+ "items": {
+ "$ref": "#/components/schemas/SAMPoint"
+ },
+ "minItems": 1,
+ "title": "Points",
+ "type": "array"
+ }
+ },
+ "required": ["points"],
+ "title": "SAMPointsField",
+ "type": "object"
+ },
+ "SD3ConditioningField": {
+ "description": "A conditioning tensor primitive value",
+ "properties": {
+ "conditioning_name": {
+ "description": "The name of conditioning tensor",
+ "title": "Conditioning Name",
+ "type": "string"
+ }
+ },
+ "required": ["conditioning_name"],
+ "title": "SD3ConditioningField",
+ "type": "object"
+ },
+ "SD3ConditioningOutput": {
"class": "output",
- "description": "Output for the Prompt Template node",
+ "description": "Base class for nodes that output a single SD3 conditioning tensor",
"properties": {
- "positive_prompt": {
- "description": "The positive prompt with the template applied",
+ "conditioning": {
+ "$ref": "#/components/schemas/SD3ConditioningField",
+ "description": "Conditioning tensor",
"field_kind": "output",
- "title": "Positive Prompt",
- "type": "string",
"ui_hidden": false
},
- "negative_prompt": {
- "description": "The negative prompt with the template applied",
- "field_kind": "output",
- "title": "Negative Prompt",
- "type": "string",
+ "type": {
+ "const": "sd3_conditioning_output",
+ "default": "sd3_conditioning_output",
+ "field_kind": "node_attribute",
+ "title": "type",
+ "type": "string"
+ }
+ },
+ "required": ["output_meta", "conditioning", "type", "type"],
+ "title": "SD3ConditioningOutput",
+ "type": "object"
+ },
+ "SD3DenoiseInvocation": {
+ "category": "latents",
+ "class": "invocation",
+ "classification": "stable",
+ "description": "Run denoising process with a SD3 model.",
+ "node_pack": "invokeai",
+ "properties": {
+ "board": {
+ "anyOf": [
+ {
+ "$ref": "#/components/schemas/BoardField"
+ },
+ {
+ "type": "null"
+ }
+ ],
+ "default": null,
+ "description": "The board to save the image to",
+ "field_kind": "internal",
+ "input": "direct",
+ "orig_required": false,
+ "ui_hidden": false
+ },
+ "metadata": {
+ "anyOf": [
+ {
+ "$ref": "#/components/schemas/MetadataField"
+ },
+ {
+ "type": "null"
+ }
+ ],
+ "default": null,
+ "description": "Optional metadata to be saved with the image",
+ "field_kind": "internal",
+ "input": "connection",
+ "orig_required": false,
"ui_hidden": false
},
- "type": {
- "const": "prompt_template_output",
- "default": "prompt_template_output",
- "field_kind": "node_attribute",
- "title": "type",
- "type": "string"
- }
- },
- "required": ["output_meta", "positive_prompt", "negative_prompt", "type", "type"],
- "title": "PromptTemplateOutput",
- "type": "object"
- },
- "PromptsFromFileInvocation": {
- "category": "prompt",
- "class": "invocation",
- "classification": "stable",
- "description": "Loads prompts from a text file",
- "node_pack": "invokeai",
- "properties": {
"id": {
"description": "The id of this instance of an invocation. Must be unique among all instances of invocations.",
"field_kind": "node_attribute",
@@ -60088,971 +67143,733 @@
"title": "Use Cache",
"type": "boolean"
},
- "file_path": {
+ "latents": {
"anyOf": [
{
- "type": "string"
+ "$ref": "#/components/schemas/LatentsField"
},
{
"type": "null"
}
],
"default": null,
- "description": "Path to prompt text file",
+ "description": "Latents tensor",
"field_kind": "input",
- "input": "any",
- "orig_required": true,
- "title": "File Path"
+ "input": "connection",
+ "orig_default": null,
+ "orig_required": false
},
- "pre_prompt": {
+ "noise": {
"anyOf": [
{
- "type": "string"
+ "$ref": "#/components/schemas/LatentsField"
},
{
"type": "null"
}
],
"default": null,
- "description": "String to prepend to each prompt",
+ "description": "Noise tensor",
"field_kind": "input",
- "input": "any",
+ "input": "connection",
"orig_default": null,
- "orig_required": false,
- "title": "Pre Prompt",
- "ui_component": "textarea"
+ "orig_required": false
},
- "post_prompt": {
+ "denoise_mask": {
"anyOf": [
{
- "type": "string"
+ "$ref": "#/components/schemas/DenoiseMaskField"
},
{
"type": "null"
}
],
"default": null,
- "description": "String to append to each prompt",
+ "description": "A mask of the region to apply the denoising process to. Values of 0.0 represent the regions to be fully denoised, and 1.0 represent the regions to be preserved.",
"field_kind": "input",
- "input": "any",
+ "input": "connection",
"orig_default": null,
- "orig_required": false,
- "title": "Post Prompt",
- "ui_component": "textarea"
+ "orig_required": false
},
- "start_line": {
- "default": 1,
- "description": "Line in the file to start start from",
+ "denoising_start": {
+ "default": 0.0,
+ "description": "When to start denoising, expressed a percentage of total steps",
"field_kind": "input",
"input": "any",
- "minimum": 1,
- "orig_default": 1,
+ "maximum": 1,
+ "minimum": 0,
+ "orig_default": 0.0,
"orig_required": false,
- "title": "Start Line",
- "type": "integer"
+ "title": "Denoising Start",
+ "type": "number"
},
- "max_prompts": {
- "default": 1,
- "description": "Max lines to read from file (0=all)",
+ "denoising_end": {
+ "default": 1.0,
+ "description": "When to stop denoising, expressed a percentage of total steps",
"field_kind": "input",
"input": "any",
+ "maximum": 1,
"minimum": 0,
- "orig_default": 1,
+ "orig_default": 1.0,
"orig_required": false,
- "title": "Max Prompts",
- "type": "integer"
- },
- "type": {
- "const": "prompt_from_file",
- "default": "prompt_from_file",
- "field_kind": "node_attribute",
- "title": "type",
- "type": "string"
- }
- },
- "required": ["type", "id"],
- "tags": ["prompt", "file"],
- "title": "Prompts from File",
- "type": "object",
- "version": "1.0.2",
- "output": {
- "$ref": "#/components/schemas/StringCollectionOutput"
- }
- },
- "PruneResult": {
- "properties": {
- "deleted": {
- "type": "integer",
- "title": "Deleted",
- "description": "Number of queue items deleted"
- }
- },
- "type": "object",
- "required": ["deleted"],
- "title": "PruneResult",
- "description": "Result of pruning the session queue"
- },
- "QueueClearedEvent": {
- "description": "Event model for queue_cleared",
- "properties": {
- "timestamp": {
- "description": "The timestamp of the event",
- "title": "Timestamp",
- "type": "integer"
- },
- "queue_id": {
- "description": "The ID of the queue",
- "title": "Queue Id",
- "type": "string"
- },
- "user_id": {
- "anyOf": [
- {
- "type": "string"
- },
- {
- "type": "null"
- }
- ],
- "default": null,
- "description": "The ID of the user whose queue items were cleared, or None if all users' items were cleared",
- "title": "User Id"
- }
- },
- "required": ["timestamp", "queue_id", "user_id"],
- "title": "QueueClearedEvent",
- "type": "object"
- },
- "QueueItemStatusChangedEvent": {
- "description": "Event model for queue_item_status_changed",
- "properties": {
- "timestamp": {
- "description": "The timestamp of the event",
- "title": "Timestamp",
- "type": "integer"
- },
- "queue_id": {
- "description": "The ID of the queue",
- "title": "Queue Id",
- "type": "string"
- },
- "item_id": {
- "description": "The ID of the queue item",
- "title": "Item Id",
- "type": "integer"
- },
- "batch_id": {
- "description": "The ID of the queue batch",
- "title": "Batch Id",
- "type": "string"
- },
- "origin": {
- "anyOf": [
- {
- "type": "string"
- },
- {
- "type": "null"
- }
- ],
- "default": null,
- "description": "The origin of the queue item",
- "title": "Origin"
- },
- "destination": {
- "anyOf": [
- {
- "type": "string"
- },
- {
- "type": "null"
- }
- ],
- "default": null,
- "description": "The destination of the queue item",
- "title": "Destination"
- },
- "user_id": {
- "default": "system",
- "description": "The ID of the user who created the queue item",
- "title": "User Id",
- "type": "string"
- },
- "status": {
- "description": "The new status of the queue item",
- "enum": ["pending", "in_progress", "waiting", "completed", "failed", "canceled"],
- "title": "Status",
- "type": "string"
- },
- "status_sequence": {
- "anyOf": [
- {
- "type": "integer"
- },
- {
- "type": "null"
- }
- ],
- "default": null,
- "description": "A monotonically increasing version for this queue item's visible status lifecycle",
- "title": "Status Sequence"
- },
- "error_type": {
- "anyOf": [
- {
- "type": "string"
- },
- {
- "type": "null"
- }
- ],
- "default": null,
- "description": "The error type, if any",
- "title": "Error Type"
+ "title": "Denoising End",
+ "type": "number"
},
- "error_message": {
+ "transformer": {
"anyOf": [
{
- "type": "string"
+ "$ref": "#/components/schemas/TransformerField"
},
{
"type": "null"
}
],
"default": null,
- "description": "The error message, if any",
- "title": "Error Message"
+ "description": "SD3 model (MMDiTX) to load",
+ "field_kind": "input",
+ "input": "connection",
+ "orig_required": true,
+ "title": "Transformer"
},
- "error_traceback": {
+ "positive_conditioning": {
"anyOf": [
{
- "type": "string"
+ "$ref": "#/components/schemas/SD3ConditioningField"
},
{
"type": "null"
}
],
"default": null,
- "description": "The error traceback, if any",
- "title": "Error Traceback"
- },
- "created_at": {
- "description": "The timestamp when the queue item was created",
- "title": "Created At",
- "type": "string"
- },
- "updated_at": {
- "description": "The timestamp when the queue item was last updated",
- "title": "Updated At",
- "type": "string"
+ "description": "Positive conditioning tensor",
+ "field_kind": "input",
+ "input": "connection",
+ "orig_required": true
},
- "started_at": {
+ "negative_conditioning": {
"anyOf": [
{
- "type": "string"
+ "$ref": "#/components/schemas/SD3ConditioningField"
},
{
"type": "null"
}
],
"default": null,
- "description": "The timestamp when the queue item was started",
- "title": "Started At"
+ "description": "Negative conditioning tensor",
+ "field_kind": "input",
+ "input": "connection",
+ "orig_required": true
},
- "completed_at": {
+ "cfg_scale": {
"anyOf": [
{
- "type": "string"
+ "type": "number"
},
{
- "type": "null"
+ "items": {
+ "type": "number"
+ },
+ "type": "array"
}
],
- "default": null,
- "description": "The timestamp when the queue item was completed",
- "title": "Completed At"
- },
- "batch_status": {
- "$ref": "#/components/schemas/BatchStatus",
- "description": "The status of the batch"
- },
- "queue_status": {
- "$ref": "#/components/schemas/SessionQueueStatus",
- "description": "The status of the queue"
+ "default": 3.5,
+ "description": "Classifier-Free Guidance scale",
+ "field_kind": "input",
+ "input": "any",
+ "orig_default": 3.5,
+ "orig_required": false,
+ "title": "CFG Scale"
},
- "session_id": {
- "description": "The ID of the session (aka graph execution state)",
- "title": "Session Id",
- "type": "string"
- }
- },
- "required": [
- "timestamp",
- "queue_id",
- "item_id",
- "batch_id",
- "origin",
- "destination",
- "user_id",
- "status",
- "status_sequence",
- "error_type",
- "error_message",
- "error_traceback",
- "created_at",
- "updated_at",
- "started_at",
- "completed_at",
- "batch_status",
- "queue_status",
- "session_id"
- ],
- "title": "QueueItemStatusChangedEvent",
- "type": "object"
- },
- "QueueItemsCanceledEvent": {
- "description": "Event model for queue_items_canceled. Emitted when queue items are canceled or deleted in\nbulk (e.g. cancel/delete-all-except-current) without per-item status change events.",
- "properties": {
- "timestamp": {
- "description": "The timestamp of the event",
- "title": "Timestamp",
+ "width": {
+ "default": 1024,
+ "description": "Width of the generated image.",
+ "field_kind": "input",
+ "input": "any",
+ "multipleOf": 16,
+ "orig_default": 1024,
+ "orig_required": false,
+ "title": "Width",
"type": "integer"
},
- "queue_id": {
- "description": "The ID of the queue",
- "title": "Queue Id",
- "type": "string"
- },
- "canceled_item_ids": {
- "description": "The IDs of the queue items that were canceled or deleted",
- "items": {
- "type": "integer"
- },
- "title": "Canceled Item Ids",
- "type": "array"
- },
- "user_ids": {
- "description": "The IDs of the users who own the canceled queue items",
- "items": {
- "type": "string"
- },
- "title": "User Ids",
- "type": "array"
- },
- "canceled_item_ids_by_user": {
- "additionalProperties": {
- "items": {
- "type": "integer"
- },
- "type": "array"
- },
- "description": "The canceled queue item IDs keyed by owner user ID.",
- "title": "Canceled Item Ids By User",
- "type": "object"
- }
- },
- "required": ["timestamp", "queue_id", "canceled_item_ids", "user_ids", "canceled_item_ids_by_user"],
- "title": "QueueItemsCanceledEvent",
- "type": "object"
- },
- "QueueItemsRetriedEvent": {
- "description": "Event model for queue_items_retried",
- "properties": {
- "timestamp": {
- "description": "The timestamp of the event",
- "title": "Timestamp",
+ "height": {
+ "default": 1024,
+ "description": "Height of the generated image.",
+ "field_kind": "input",
+ "input": "any",
+ "multipleOf": 16,
+ "orig_default": 1024,
+ "orig_required": false,
+ "title": "Height",
"type": "integer"
},
- "queue_id": {
- "description": "The ID of the queue",
- "title": "Queue Id",
- "type": "string"
- },
- "retried_item_ids": {
- "description": "The IDs of the queue items that were retried",
- "items": {
- "type": "integer"
- },
- "title": "Retried Item Ids",
- "type": "array"
- },
- "user_ids": {
- "description": "The IDs of the users who own the retried root queue items",
- "items": {
- "type": "string"
- },
- "title": "User Ids",
- "type": "array"
- },
- "retried_item_ids_by_user": {
- "additionalProperties": {
- "items": {
- "type": "integer"
- },
- "type": "array"
- },
- "description": "The retried root queue item IDs keyed by owner user ID.",
- "title": "Retried Item Ids By User",
- "type": "object"
- }
- },
- "required": ["timestamp", "queue_id", "retried_item_ids", "user_ids", "retried_item_ids_by_user"],
- "title": "QueueItemsRetriedEvent",
- "type": "object"
- },
- "Qwen3EncoderField": {
- "description": "Field for Qwen3 text encoder used by Z-Image models.",
- "properties": {
- "tokenizer": {
- "$ref": "#/components/schemas/ModelIdentifierField",
- "description": "Info to load tokenizer submodel"
- },
- "text_encoder": {
- "$ref": "#/components/schemas/ModelIdentifierField",
- "description": "Info to load text_encoder submodel"
- },
- "loras": {
- "description": "LoRAs to apply on model loading",
- "items": {
- "$ref": "#/components/schemas/LoRAField"
- },
- "title": "Loras",
- "type": "array"
- }
- },
- "required": ["tokenizer", "text_encoder"],
- "title": "Qwen3EncoderField",
- "type": "object"
- },
- "Qwen3Encoder_Checkpoint_Config": {
- "properties": {
- "key": {
- "type": "string",
- "title": "Key",
- "description": "A unique key for this model."
- },
- "hash": {
- "type": "string",
- "title": "Hash",
- "description": "The hash of the model file(s)."
- },
- "path": {
- "type": "string",
- "title": "Path",
- "description": "Path to the model on the filesystem. Relative paths are relative to the Invoke root directory."
- },
- "file_size": {
- "type": "integer",
- "title": "File Size",
- "description": "The size of the model in bytes."
- },
- "name": {
- "type": "string",
- "title": "Name",
- "description": "Name of the model."
- },
- "description": {
- "anyOf": [
- {
- "type": "string"
- },
- {
- "type": "null"
- }
- ],
- "title": "Description",
- "description": "Model description"
- },
- "source": {
- "type": "string",
- "title": "Source",
- "description": "The original source of the model (path, URL or repo_id)."
+ "steps": {
+ "default": 10,
+ "description": "Number of steps to run",
+ "exclusiveMinimum": 0,
+ "field_kind": "input",
+ "input": "any",
+ "orig_default": 10,
+ "orig_required": false,
+ "title": "Steps",
+ "type": "integer"
},
- "source_type": {
- "$ref": "#/components/schemas/ModelSourceType",
- "description": "The type of source"
+ "seed": {
+ "default": 0,
+ "description": "Randomness seed for reproducibility.",
+ "field_kind": "input",
+ "input": "any",
+ "orig_default": 0,
+ "orig_required": false,
+ "title": "Seed",
+ "type": "integer"
},
- "source_api_response": {
+ "type": {
+ "const": "sd3_denoise",
+ "default": "sd3_denoise",
+ "field_kind": "node_attribute",
+ "title": "type",
+ "type": "string"
+ }
+ },
+ "required": ["type", "id"],
+ "tags": ["image", "sd3"],
+ "title": "Denoise - SD3",
+ "type": "object",
+ "version": "1.2.0",
+ "output": {
+ "$ref": "#/components/schemas/LatentsOutput"
+ }
+ },
+ "SD3ImageToLatentsInvocation": {
+ "category": "latents",
+ "class": "invocation",
+ "classification": "stable",
+ "description": "Generates latents from an image.",
+ "node_pack": "invokeai",
+ "properties": {
+ "board": {
"anyOf": [
{
- "type": "string"
+ "$ref": "#/components/schemas/BoardField"
},
{
"type": "null"
}
],
- "title": "Source Api Response",
- "description": "The original API response from the source, as stringified JSON."
+ "default": null,
+ "description": "The board to save the image to",
+ "field_kind": "internal",
+ "input": "direct",
+ "orig_required": false,
+ "ui_hidden": false
},
- "source_url": {
+ "metadata": {
"anyOf": [
{
- "type": "string"
+ "$ref": "#/components/schemas/MetadataField"
},
{
"type": "null"
}
],
- "title": "Source Url",
- "description": "Optional URL for the model (e.g. download page or model page)."
+ "default": null,
+ "description": "Optional metadata to be saved with the image",
+ "field_kind": "internal",
+ "input": "connection",
+ "orig_required": false,
+ "ui_hidden": false
},
- "cover_image": {
- "anyOf": [
- {
- "type": "string"
- },
- {
- "type": "null"
- }
- ],
- "title": "Cover Image",
- "description": "Url for image to preview model"
+ "id": {
+ "description": "The id of this instance of an invocation. Must be unique among all instances of invocations.",
+ "field_kind": "node_attribute",
+ "title": "Id",
+ "type": "string"
},
- "config_path": {
+ "is_intermediate": {
+ "default": false,
+ "description": "Whether or not this is an intermediate invocation.",
+ "field_kind": "node_attribute",
+ "input": "direct",
+ "orig_required": true,
+ "title": "Is Intermediate",
+ "type": "boolean",
+ "ui_hidden": false,
+ "ui_type": "IsIntermediate"
+ },
+ "use_cache": {
+ "default": true,
+ "description": "Whether or not to use the cache",
+ "field_kind": "node_attribute",
+ "title": "Use Cache",
+ "type": "boolean"
+ },
+ "image": {
"anyOf": [
{
- "type": "string"
+ "$ref": "#/components/schemas/ImageField"
},
{
"type": "null"
}
],
- "title": "Config Path",
- "description": "Path to the config for this model, if any."
- },
- "base": {
- "type": "string",
- "const": "any",
- "title": "Base",
- "default": "any"
- },
- "type": {
- "type": "string",
- "const": "qwen3_encoder",
- "title": "Type",
- "default": "qwen3_encoder"
- },
- "format": {
- "type": "string",
- "const": "checkpoint",
- "title": "Format",
- "default": "checkpoint"
+ "default": null,
+ "description": "The image to encode",
+ "field_kind": "input",
+ "input": "any",
+ "orig_required": true
},
- "cpu_only": {
+ "vae": {
"anyOf": [
{
- "type": "boolean"
+ "$ref": "#/components/schemas/VAEField"
},
{
"type": "null"
}
],
- "title": "Cpu Only",
- "description": "Whether this model should run on CPU only"
+ "default": null,
+ "description": "VAE",
+ "field_kind": "input",
+ "input": "connection",
+ "orig_required": true
},
- "variant": {
- "$ref": "#/components/schemas/Qwen3VariantType",
- "description": "Qwen3 model size variant (4B or 8B)"
+ "type": {
+ "const": "sd3_i2l",
+ "default": "sd3_i2l",
+ "field_kind": "node_attribute",
+ "title": "type",
+ "type": "string"
}
},
+ "required": ["type", "id"],
+ "tags": ["image", "latents", "vae", "i2l", "sd3"],
+ "title": "Image to Latents - SD3",
"type": "object",
- "required": [
- "key",
- "hash",
- "path",
- "file_size",
- "name",
- "description",
- "source",
- "source_type",
- "source_api_response",
- "source_url",
- "cover_image",
- "config_path",
- "base",
- "type",
- "format",
- "cpu_only",
- "variant"
- ],
- "title": "Qwen3Encoder_Checkpoint_Config",
- "description": "Configuration for single-file Qwen3 Encoder models (safetensors)."
+ "version": "1.0.1",
+ "output": {
+ "$ref": "#/components/schemas/LatentsOutput"
+ }
},
- "Qwen3Encoder_GGUF_Config": {
+ "SD3LatentsToImageInvocation": {
+ "category": "latents",
+ "class": "invocation",
+ "classification": "stable",
+ "description": "Generates an image from latents.",
+ "node_pack": "invokeai",
"properties": {
- "key": {
- "type": "string",
- "title": "Key",
- "description": "A unique key for this model."
- },
- "hash": {
- "type": "string",
- "title": "Hash",
- "description": "The hash of the model file(s)."
- },
- "path": {
- "type": "string",
- "title": "Path",
- "description": "Path to the model on the filesystem. Relative paths are relative to the Invoke root directory."
- },
- "file_size": {
- "type": "integer",
- "title": "File Size",
- "description": "The size of the model in bytes."
- },
- "name": {
- "type": "string",
- "title": "Name",
- "description": "Name of the model."
- },
- "description": {
+ "board": {
"anyOf": [
{
- "type": "string"
+ "$ref": "#/components/schemas/BoardField"
},
{
"type": "null"
}
],
- "title": "Description",
- "description": "Model description"
- },
- "source": {
- "type": "string",
- "title": "Source",
- "description": "The original source of the model (path, URL or repo_id)."
- },
- "source_type": {
- "$ref": "#/components/schemas/ModelSourceType",
- "description": "The type of source"
+ "default": null,
+ "description": "The board to save the image to",
+ "field_kind": "internal",
+ "input": "direct",
+ "orig_required": false,
+ "ui_hidden": false
},
- "source_api_response": {
+ "metadata": {
"anyOf": [
{
- "type": "string"
+ "$ref": "#/components/schemas/MetadataField"
},
{
"type": "null"
}
],
- "title": "Source Api Response",
- "description": "The original API response from the source, as stringified JSON."
+ "default": null,
+ "description": "Optional metadata to be saved with the image",
+ "field_kind": "internal",
+ "input": "connection",
+ "orig_required": false,
+ "ui_hidden": false
},
- "source_url": {
- "anyOf": [
- {
- "type": "string"
- },
- {
- "type": "null"
- }
- ],
- "title": "Source Url",
- "description": "Optional URL for the model (e.g. download page or model page)."
+ "id": {
+ "description": "The id of this instance of an invocation. Must be unique among all instances of invocations.",
+ "field_kind": "node_attribute",
+ "title": "Id",
+ "type": "string"
},
- "cover_image": {
- "anyOf": [
- {
- "type": "string"
- },
- {
- "type": "null"
- }
- ],
- "title": "Cover Image",
- "description": "Url for image to preview model"
+ "is_intermediate": {
+ "default": false,
+ "description": "Whether or not this is an intermediate invocation.",
+ "field_kind": "node_attribute",
+ "input": "direct",
+ "orig_required": true,
+ "title": "Is Intermediate",
+ "type": "boolean",
+ "ui_hidden": false,
+ "ui_type": "IsIntermediate"
},
- "config_path": {
+ "use_cache": {
+ "default": true,
+ "description": "Whether or not to use the cache",
+ "field_kind": "node_attribute",
+ "title": "Use Cache",
+ "type": "boolean"
+ },
+ "latents": {
"anyOf": [
{
- "type": "string"
+ "$ref": "#/components/schemas/LatentsField"
},
{
"type": "null"
}
],
- "title": "Config Path",
- "description": "Path to the config for this model, if any."
- },
- "base": {
- "type": "string",
- "const": "any",
- "title": "Base",
- "default": "any"
- },
- "type": {
- "type": "string",
- "const": "qwen3_encoder",
- "title": "Type",
- "default": "qwen3_encoder"
- },
- "format": {
- "type": "string",
- "const": "gguf_quantized",
- "title": "Format",
- "default": "gguf_quantized"
+ "default": null,
+ "description": "Latents tensor",
+ "field_kind": "input",
+ "input": "connection",
+ "orig_required": true
},
- "cpu_only": {
+ "vae": {
"anyOf": [
{
- "type": "boolean"
+ "$ref": "#/components/schemas/VAEField"
},
{
"type": "null"
}
],
- "title": "Cpu Only",
- "description": "Whether this model should run on CPU only"
+ "default": null,
+ "description": "VAE",
+ "field_kind": "input",
+ "input": "connection",
+ "orig_required": true
},
- "variant": {
- "$ref": "#/components/schemas/Qwen3VariantType",
- "description": "Qwen3 model size variant (4B or 8B)"
+ "type": {
+ "const": "sd3_l2i",
+ "default": "sd3_l2i",
+ "field_kind": "node_attribute",
+ "title": "type",
+ "type": "string"
}
},
+ "required": ["type", "id"],
+ "tags": ["latents", "image", "vae", "l2i", "sd3"],
+ "title": "Latents to Image - SD3",
"type": "object",
- "required": [
- "key",
- "hash",
- "path",
- "file_size",
- "name",
- "description",
- "source",
- "source_type",
- "source_api_response",
- "source_url",
- "cover_image",
- "config_path",
- "base",
- "type",
- "format",
- "cpu_only",
- "variant"
- ],
- "title": "Qwen3Encoder_GGUF_Config",
- "description": "Configuration for GGUF-quantized Qwen3 Encoder models."
+ "version": "1.3.2",
+ "output": {
+ "$ref": "#/components/schemas/ImageOutput"
+ }
},
- "Qwen3Encoder_Qwen3Encoder_Config": {
+ "SDXLCompelPromptInvocation": {
+ "category": "prompt",
+ "class": "invocation",
+ "classification": "stable",
+ "description": "Parse prompt using compel package to conditioning.",
+ "node_pack": "invokeai",
"properties": {
- "key": {
- "type": "string",
- "title": "Key",
- "description": "A unique key for this model."
+ "id": {
+ "description": "The id of this instance of an invocation. Must be unique among all instances of invocations.",
+ "field_kind": "node_attribute",
+ "title": "Id",
+ "type": "string"
},
- "hash": {
+ "is_intermediate": {
+ "default": false,
+ "description": "Whether or not this is an intermediate invocation.",
+ "field_kind": "node_attribute",
+ "input": "direct",
+ "orig_required": true,
+ "title": "Is Intermediate",
+ "type": "boolean",
+ "ui_hidden": false,
+ "ui_type": "IsIntermediate"
+ },
+ "use_cache": {
+ "default": true,
+ "description": "Whether or not to use the cache",
+ "field_kind": "node_attribute",
+ "title": "Use Cache",
+ "type": "boolean"
+ },
+ "prompt": {
+ "default": "",
+ "description": "Prompt to be parsed by Compel to create a conditioning tensor",
+ "field_kind": "input",
+ "input": "any",
+ "orig_default": "",
+ "orig_required": false,
+ "title": "Prompt",
"type": "string",
- "title": "Hash",
- "description": "The hash of the model file(s)."
+ "ui_component": "textarea"
},
- "path": {
+ "style": {
+ "default": "",
+ "description": "Prompt to be parsed by Compel to create a conditioning tensor",
+ "field_kind": "input",
+ "input": "any",
+ "orig_default": "",
+ "orig_required": false,
+ "title": "Style",
"type": "string",
- "title": "Path",
- "description": "Path to the model on the filesystem. Relative paths are relative to the Invoke root directory."
+ "ui_component": "textarea"
},
- "file_size": {
- "type": "integer",
- "title": "File Size",
- "description": "The size of the model in bytes."
+ "original_width": {
+ "default": 1024,
+ "description": "",
+ "field_kind": "input",
+ "input": "any",
+ "orig_default": 1024,
+ "orig_required": false,
+ "title": "Original Width",
+ "type": "integer"
},
- "name": {
- "type": "string",
- "title": "Name",
- "description": "Name of the model."
+ "original_height": {
+ "default": 1024,
+ "description": "",
+ "field_kind": "input",
+ "input": "any",
+ "orig_default": 1024,
+ "orig_required": false,
+ "title": "Original Height",
+ "type": "integer"
},
- "description": {
- "anyOf": [
- {
- "type": "string"
- },
- {
- "type": "null"
- }
- ],
- "title": "Description",
- "description": "Model description"
+ "crop_top": {
+ "default": 0,
+ "description": "",
+ "field_kind": "input",
+ "input": "any",
+ "orig_default": 0,
+ "orig_required": false,
+ "title": "Crop Top",
+ "type": "integer"
},
- "source": {
- "type": "string",
- "title": "Source",
- "description": "The original source of the model (path, URL or repo_id)."
+ "crop_left": {
+ "default": 0,
+ "description": "",
+ "field_kind": "input",
+ "input": "any",
+ "orig_default": 0,
+ "orig_required": false,
+ "title": "Crop Left",
+ "type": "integer"
},
- "source_type": {
- "$ref": "#/components/schemas/ModelSourceType",
- "description": "The type of source"
+ "target_width": {
+ "default": 1024,
+ "description": "",
+ "field_kind": "input",
+ "input": "any",
+ "orig_default": 1024,
+ "orig_required": false,
+ "title": "Target Width",
+ "type": "integer"
},
- "source_api_response": {
+ "target_height": {
+ "default": 1024,
+ "description": "",
+ "field_kind": "input",
+ "input": "any",
+ "orig_default": 1024,
+ "orig_required": false,
+ "title": "Target Height",
+ "type": "integer"
+ },
+ "clip": {
"anyOf": [
{
- "type": "string"
+ "$ref": "#/components/schemas/CLIPField"
},
{
"type": "null"
}
],
- "title": "Source Api Response",
- "description": "The original API response from the source, as stringified JSON."
+ "default": null,
+ "description": "CLIP (tokenizer, text encoder, LoRAs) and skipped layer count",
+ "field_kind": "input",
+ "input": "connection",
+ "orig_required": true,
+ "title": "CLIP 1"
},
- "source_url": {
+ "clip2": {
"anyOf": [
{
- "type": "string"
+ "$ref": "#/components/schemas/CLIPField"
},
{
"type": "null"
}
],
- "title": "Source Url",
- "description": "Optional URL for the model (e.g. download page or model page)."
+ "default": null,
+ "description": "CLIP (tokenizer, text encoder, LoRAs) and skipped layer count",
+ "field_kind": "input",
+ "input": "connection",
+ "orig_required": true,
+ "title": "CLIP 2"
},
- "cover_image": {
+ "mask": {
"anyOf": [
{
- "type": "string"
+ "$ref": "#/components/schemas/TensorField"
},
{
"type": "null"
}
],
- "title": "Cover Image",
- "description": "Url for image to preview model"
- },
- "base": {
- "type": "string",
- "const": "any",
- "title": "Base",
- "default": "any"
+ "default": null,
+ "description": "A mask defining the region that this conditioning prompt applies to.",
+ "field_kind": "input",
+ "input": "any",
+ "orig_default": null,
+ "orig_required": false
},
"type": {
- "type": "string",
- "const": "qwen3_encoder",
- "title": "Type",
- "default": "qwen3_encoder"
+ "const": "sdxl_compel_prompt",
+ "default": "sdxl_compel_prompt",
+ "field_kind": "node_attribute",
+ "title": "type",
+ "type": "string"
+ }
+ },
+ "required": ["type", "id"],
+ "tags": ["sdxl", "compel", "prompt"],
+ "title": "Prompt - SDXL",
+ "type": "object",
+ "version": "1.2.1",
+ "output": {
+ "$ref": "#/components/schemas/ConditioningOutput"
+ }
+ },
+ "SDXLLoRACollectionLoader": {
+ "category": "model",
+ "class": "invocation",
+ "classification": "stable",
+ "description": "Applies a collection of SDXL LoRAs to the provided UNet and CLIP models.",
+ "node_pack": "invokeai",
+ "properties": {
+ "id": {
+ "description": "The id of this instance of an invocation. Must be unique among all instances of invocations.",
+ "field_kind": "node_attribute",
+ "title": "Id",
+ "type": "string"
},
- "format": {
- "type": "string",
- "const": "qwen3_encoder",
- "title": "Format",
- "default": "qwen3_encoder"
+ "is_intermediate": {
+ "default": false,
+ "description": "Whether or not this is an intermediate invocation.",
+ "field_kind": "node_attribute",
+ "input": "direct",
+ "orig_required": true,
+ "title": "Is Intermediate",
+ "type": "boolean",
+ "ui_hidden": false,
+ "ui_type": "IsIntermediate"
},
- "cpu_only": {
+ "use_cache": {
+ "default": true,
+ "description": "Whether or not to use the cache",
+ "field_kind": "node_attribute",
+ "title": "Use Cache",
+ "type": "boolean"
+ },
+ "loras": {
"anyOf": [
{
- "type": "boolean"
+ "$ref": "#/components/schemas/LoRAField"
+ },
+ {
+ "items": {
+ "$ref": "#/components/schemas/LoRAField"
+ },
+ "type": "array"
},
{
"type": "null"
}
],
- "title": "Cpu Only",
- "description": "Whether this model should run on CPU only"
+ "default": null,
+ "description": "LoRA models and weights. May be a single LoRA or collection.",
+ "field_kind": "input",
+ "input": "any",
+ "orig_default": null,
+ "orig_required": false,
+ "title": "LoRAs",
+ "ui_model_base": ["sdxl"],
+ "ui_model_type": ["lora"]
},
- "variant": {
- "$ref": "#/components/schemas/Qwen3VariantType",
- "description": "Qwen3 model size variant (4B or 8B)"
- }
- },
- "type": "object",
- "required": [
- "key",
- "hash",
- "path",
- "file_size",
- "name",
- "description",
- "source",
- "source_type",
- "source_api_response",
- "source_url",
- "cover_image",
- "base",
- "type",
- "format",
- "cpu_only",
- "variant"
- ],
- "title": "Qwen3Encoder_Qwen3Encoder_Config",
- "description": "Configuration for Qwen3 Encoder models in a diffusers-like format.\n\nThe model weights are expected to be in a folder called text_encoder inside the model directory,\ncompatible with Qwen2VLForConditionalGeneration or similar architectures used by Z-Image."
- },
- "Qwen3VariantType": {
- "type": "string",
- "enum": ["qwen3_4b", "qwen3_8b", "qwen3_06b"],
- "title": "Qwen3VariantType",
- "description": "Qwen3 text encoder variants based on model size."
- },
- "QwenImageConditioningField": {
- "description": "A Qwen Image Edit conditioning tensor primitive value",
- "properties": {
- "conditioning_name": {
- "description": "The name of conditioning tensor",
- "title": "Conditioning Name",
- "type": "string"
- }
- },
- "required": ["conditioning_name"],
- "title": "QwenImageConditioningField",
- "type": "object"
- },
- "QwenImageConditioningOutput": {
- "class": "output",
- "description": "Base class for nodes that output a Qwen Image Edit conditioning tensor.",
- "properties": {
- "conditioning": {
- "$ref": "#/components/schemas/QwenImageConditioningField",
- "description": "Conditioning tensor",
- "field_kind": "output",
- "ui_hidden": false
+ "unet": {
+ "anyOf": [
+ {
+ "$ref": "#/components/schemas/UNetField"
+ },
+ {
+ "type": "null"
+ }
+ ],
+ "default": null,
+ "description": "UNet (scheduler, LoRAs)",
+ "field_kind": "input",
+ "input": "connection",
+ "orig_default": null,
+ "orig_required": false,
+ "title": "UNet"
},
- "type": {
- "const": "qwen_image_conditioning_output",
- "default": "qwen_image_conditioning_output",
- "field_kind": "node_attribute",
- "title": "type",
- "type": "string"
- }
- },
- "required": ["output_meta", "conditioning", "type", "type"],
- "title": "QwenImageConditioningOutput",
- "type": "object"
- },
- "QwenImageDenoiseInvocation": {
- "category": "image",
- "class": "invocation",
- "classification": "prototype",
- "description": "Run the denoising process with a Qwen Image model.",
- "node_pack": "invokeai",
- "properties": {
- "board": {
+ "clip": {
"anyOf": [
{
- "$ref": "#/components/schemas/BoardField"
+ "$ref": "#/components/schemas/CLIPField"
},
{
"type": "null"
}
],
"default": null,
- "description": "The board to save the image to",
- "field_kind": "internal",
- "input": "direct",
+ "description": "CLIP (tokenizer, text encoder, LoRAs) and skipped layer count",
+ "field_kind": "input",
+ "input": "connection",
+ "orig_default": null,
"orig_required": false,
- "ui_hidden": false
+ "title": "CLIP"
},
- "metadata": {
+ "clip2": {
"anyOf": [
{
- "$ref": "#/components/schemas/MetadataField"
+ "$ref": "#/components/schemas/CLIPField"
},
{
"type": "null"
}
],
"default": null,
- "description": "Optional metadata to be saved with the image",
- "field_kind": "internal",
+ "description": "CLIP (tokenizer, text encoder, LoRAs) and skipped layer count",
+ "field_kind": "input",
"input": "connection",
+ "orig_default": null,
"orig_required": false,
- "ui_hidden": false
+ "title": "CLIP 2"
},
+ "type": {
+ "const": "sdxl_lora_collection_loader",
+ "default": "sdxl_lora_collection_loader",
+ "field_kind": "node_attribute",
+ "title": "type",
+ "type": "string"
+ }
+ },
+ "required": ["type", "id"],
+ "tags": ["model"],
+ "title": "Apply LoRA Collection - SDXL",
+ "type": "object",
+ "version": "1.1.3",
+ "output": {
+ "$ref": "#/components/schemas/SDXLLoRALoaderOutput"
+ }
+ },
+ "SDXLLoRALoaderInvocation": {
+ "category": "model",
+ "class": "invocation",
+ "classification": "stable",
+ "description": "Apply selected lora to unet and text_encoder.",
+ "node_pack": "invokeai",
+ "properties": {
"id": {
"description": "The id of this instance of an invocation. Must be unique among all instances of invocations.",
"field_kind": "node_attribute",
@@ -61077,261 +67894,403 @@
"title": "Use Cache",
"type": "boolean"
},
- "latents": {
+ "lora": {
"anyOf": [
{
- "$ref": "#/components/schemas/LatentsField"
+ "$ref": "#/components/schemas/ModelIdentifierField"
},
{
"type": "null"
}
],
"default": null,
- "description": "Latents tensor",
+ "description": "LoRA model to load",
"field_kind": "input",
- "input": "connection",
- "orig_default": null,
- "orig_required": false
+ "input": "any",
+ "orig_required": true,
+ "title": "LoRA",
+ "ui_model_base": ["sdxl"],
+ "ui_model_type": ["lora"]
},
- "reference_latents": {
+ "weight": {
+ "default": 0.75,
+ "description": "The weight at which the LoRA is applied to each model",
+ "field_kind": "input",
+ "input": "any",
+ "orig_default": 0.75,
+ "orig_required": false,
+ "title": "Weight",
+ "type": "number"
+ },
+ "unet": {
"anyOf": [
{
- "$ref": "#/components/schemas/LatentsField"
+ "$ref": "#/components/schemas/UNetField"
},
{
"type": "null"
}
],
"default": null,
- "description": "Reference image latents to guide generation. Encoded through the VAE.",
+ "description": "UNet (scheduler, LoRAs)",
"field_kind": "input",
"input": "connection",
"orig_default": null,
- "orig_required": false
+ "orig_required": false,
+ "title": "UNet"
},
- "denoise_mask": {
+ "clip": {
"anyOf": [
{
- "$ref": "#/components/schemas/DenoiseMaskField"
+ "$ref": "#/components/schemas/CLIPField"
},
{
"type": "null"
}
],
"default": null,
- "description": "A mask of the region to apply the denoising process to. Values of 0.0 represent the regions to be fully denoised, and 1.0 represent the regions to be preserved.",
+ "description": "CLIP (tokenizer, text encoder, LoRAs) and skipped layer count",
"field_kind": "input",
"input": "connection",
"orig_default": null,
- "orig_required": false
- },
- "denoising_start": {
- "default": 0.0,
- "description": "When to start denoising, expressed a percentage of total steps",
- "field_kind": "input",
- "input": "any",
- "maximum": 1,
- "minimum": 0,
- "orig_default": 0.0,
"orig_required": false,
- "title": "Denoising Start",
- "type": "number"
+ "title": "CLIP 1"
},
- "denoising_end": {
- "default": 1.0,
- "description": "When to stop denoising, expressed a percentage of total steps",
+ "clip2": {
+ "anyOf": [
+ {
+ "$ref": "#/components/schemas/CLIPField"
+ },
+ {
+ "type": "null"
+ }
+ ],
+ "default": null,
+ "description": "CLIP (tokenizer, text encoder, LoRAs) and skipped layer count",
"field_kind": "input",
- "input": "any",
- "maximum": 1,
- "minimum": 0,
- "orig_default": 1.0,
+ "input": "connection",
+ "orig_default": null,
"orig_required": false,
- "title": "Denoising End",
- "type": "number"
+ "title": "CLIP 2"
},
- "transformer": {
+ "type": {
+ "const": "sdxl_lora_loader",
+ "default": "sdxl_lora_loader",
+ "field_kind": "node_attribute",
+ "title": "type",
+ "type": "string"
+ }
+ },
+ "required": ["type", "id"],
+ "tags": ["lora", "model"],
+ "title": "Apply LoRA - SDXL",
+ "type": "object",
+ "version": "1.0.5",
+ "output": {
+ "$ref": "#/components/schemas/SDXLLoRALoaderOutput"
+ }
+ },
+ "SDXLLoRALoaderOutput": {
+ "class": "output",
+ "description": "SDXL LoRA Loader Output",
+ "properties": {
+ "unet": {
"anyOf": [
{
- "$ref": "#/components/schemas/TransformerField"
+ "$ref": "#/components/schemas/UNetField"
},
{
"type": "null"
}
],
"default": null,
- "description": "Qwen Image Edit model (Transformer) to load",
- "field_kind": "input",
- "input": "connection",
- "orig_required": true,
- "title": "Transformer"
+ "description": "UNet (scheduler, LoRAs)",
+ "field_kind": "output",
+ "title": "UNet",
+ "ui_hidden": false
},
- "positive_conditioning": {
+ "clip": {
"anyOf": [
{
- "$ref": "#/components/schemas/QwenImageConditioningField"
+ "$ref": "#/components/schemas/CLIPField"
},
{
"type": "null"
}
],
"default": null,
- "description": "Positive conditioning tensor",
- "field_kind": "input",
- "input": "connection",
- "orig_required": true
+ "description": "CLIP (tokenizer, text encoder, LoRAs) and skipped layer count",
+ "field_kind": "output",
+ "title": "CLIP 1",
+ "ui_hidden": false
},
- "negative_conditioning": {
+ "clip2": {
"anyOf": [
{
- "$ref": "#/components/schemas/QwenImageConditioningField"
+ "$ref": "#/components/schemas/CLIPField"
},
{
"type": "null"
}
],
"default": null,
- "description": "Negative conditioning tensor",
+ "description": "CLIP (tokenizer, text encoder, LoRAs) and skipped layer count",
+ "field_kind": "output",
+ "title": "CLIP 2",
+ "ui_hidden": false
+ },
+ "type": {
+ "const": "sdxl_lora_loader_output",
+ "default": "sdxl_lora_loader_output",
+ "field_kind": "node_attribute",
+ "title": "type",
+ "type": "string"
+ }
+ },
+ "required": ["output_meta", "unet", "clip", "clip2", "type", "type"],
+ "title": "SDXLLoRALoaderOutput",
+ "type": "object"
+ },
+ "SDXLModelLoaderInvocation": {
+ "category": "model",
+ "class": "invocation",
+ "classification": "stable",
+ "description": "Loads an sdxl base model, outputting its submodels.",
+ "node_pack": "invokeai",
+ "properties": {
+ "id": {
+ "description": "The id of this instance of an invocation. Must be unique among all instances of invocations.",
+ "field_kind": "node_attribute",
+ "title": "Id",
+ "type": "string"
+ },
+ "is_intermediate": {
+ "default": false,
+ "description": "Whether or not this is an intermediate invocation.",
+ "field_kind": "node_attribute",
+ "input": "direct",
+ "orig_required": true,
+ "title": "Is Intermediate",
+ "type": "boolean",
+ "ui_hidden": false,
+ "ui_type": "IsIntermediate"
+ },
+ "use_cache": {
+ "default": true,
+ "description": "Whether or not to use the cache",
+ "field_kind": "node_attribute",
+ "title": "Use Cache",
+ "type": "boolean"
+ },
+ "model": {
+ "anyOf": [
+ {
+ "$ref": "#/components/schemas/ModelIdentifierField"
+ },
+ {
+ "type": "null"
+ }
+ ],
+ "default": null,
+ "description": "SDXL Main model (UNet, VAE, CLIP1, CLIP2) to load",
"field_kind": "input",
- "input": "connection",
- "orig_default": null,
- "orig_required": false
+ "input": "any",
+ "orig_required": true,
+ "ui_model_base": ["sdxl"],
+ "ui_model_type": ["main"]
+ },
+ "type": {
+ "const": "sdxl_model_loader",
+ "default": "sdxl_model_loader",
+ "field_kind": "node_attribute",
+ "title": "type",
+ "type": "string"
+ }
+ },
+ "required": ["type", "id"],
+ "tags": ["model", "sdxl"],
+ "title": "Main Model - SDXL",
+ "type": "object",
+ "version": "1.0.4",
+ "output": {
+ "$ref": "#/components/schemas/SDXLModelLoaderOutput"
+ }
+ },
+ "SDXLModelLoaderOutput": {
+ "class": "output",
+ "description": "SDXL base model loader output",
+ "properties": {
+ "unet": {
+ "$ref": "#/components/schemas/UNetField",
+ "description": "UNet (scheduler, LoRAs)",
+ "field_kind": "output",
+ "title": "UNet",
+ "ui_hidden": false
+ },
+ "clip": {
+ "$ref": "#/components/schemas/CLIPField",
+ "description": "CLIP (tokenizer, text encoder, LoRAs) and skipped layer count",
+ "field_kind": "output",
+ "title": "CLIP 1",
+ "ui_hidden": false
+ },
+ "clip2": {
+ "$ref": "#/components/schemas/CLIPField",
+ "description": "CLIP (tokenizer, text encoder, LoRAs) and skipped layer count",
+ "field_kind": "output",
+ "title": "CLIP 2",
+ "ui_hidden": false
+ },
+ "vae": {
+ "$ref": "#/components/schemas/VAEField",
+ "description": "VAE",
+ "field_kind": "output",
+ "title": "VAE",
+ "ui_hidden": false
+ },
+ "type": {
+ "const": "sdxl_model_loader_output",
+ "default": "sdxl_model_loader_output",
+ "field_kind": "node_attribute",
+ "title": "type",
+ "type": "string"
+ }
+ },
+ "required": ["output_meta", "unet", "clip", "clip2", "vae", "type", "type"],
+ "title": "SDXLModelLoaderOutput",
+ "type": "object"
+ },
+ "SDXLRefinerCompelPromptInvocation": {
+ "category": "prompt",
+ "class": "invocation",
+ "classification": "stable",
+ "description": "Parse prompt using compel package to conditioning.",
+ "node_pack": "invokeai",
+ "properties": {
+ "id": {
+ "description": "The id of this instance of an invocation. Must be unique among all instances of invocations.",
+ "field_kind": "node_attribute",
+ "title": "Id",
+ "type": "string"
+ },
+ "is_intermediate": {
+ "default": false,
+ "description": "Whether or not this is an intermediate invocation.",
+ "field_kind": "node_attribute",
+ "input": "direct",
+ "orig_required": true,
+ "title": "Is Intermediate",
+ "type": "boolean",
+ "ui_hidden": false,
+ "ui_type": "IsIntermediate"
},
- "cfg_scale": {
- "anyOf": [
- {
- "type": "number"
- },
- {
- "items": {
- "type": "number"
- },
- "type": "array"
- }
- ],
- "default": 4.0,
- "description": "Classifier-Free Guidance scale",
+ "use_cache": {
+ "default": true,
+ "description": "Whether or not to use the cache",
+ "field_kind": "node_attribute",
+ "title": "Use Cache",
+ "type": "boolean"
+ },
+ "style": {
+ "default": "",
+ "description": "Prompt to be parsed by Compel to create a conditioning tensor",
"field_kind": "input",
"input": "any",
- "orig_default": 4.0,
+ "orig_default": "",
"orig_required": false,
- "title": "CFG Scale"
+ "title": "Style",
+ "type": "string",
+ "ui_component": "textarea"
},
- "width": {
+ "original_width": {
"default": 1024,
- "description": "Width of the generated image.",
+ "description": "",
"field_kind": "input",
"input": "any",
- "multipleOf": 16,
"orig_default": 1024,
"orig_required": false,
- "title": "Width",
+ "title": "Original Width",
"type": "integer"
},
- "height": {
+ "original_height": {
"default": 1024,
- "description": "Height of the generated image.",
+ "description": "",
"field_kind": "input",
"input": "any",
- "multipleOf": 16,
"orig_default": 1024,
"orig_required": false,
- "title": "Height",
+ "title": "Original Height",
"type": "integer"
},
- "steps": {
- "default": 40,
- "description": "Number of steps to run",
- "exclusiveMinimum": 0,
+ "crop_top": {
+ "default": 0,
+ "description": "",
"field_kind": "input",
"input": "any",
- "orig_default": 40,
+ "orig_default": 0,
"orig_required": false,
- "title": "Steps",
+ "title": "Crop Top",
"type": "integer"
},
- "seed": {
+ "crop_left": {
"default": 0,
- "description": "Randomness seed for reproducibility.",
+ "description": "",
"field_kind": "input",
"input": "any",
"orig_default": 0,
"orig_required": false,
- "title": "Seed",
+ "title": "Crop Left",
"type": "integer"
},
- "shift": {
+ "aesthetic_score": {
+ "default": 6.0,
+ "description": "The aesthetic score to apply to the conditioning tensor",
+ "field_kind": "input",
+ "input": "any",
+ "orig_default": 6.0,
+ "orig_required": false,
+ "title": "Aesthetic Score",
+ "type": "number"
+ },
+ "clip2": {
"anyOf": [
{
- "type": "number"
+ "$ref": "#/components/schemas/CLIPField"
},
{
"type": "null"
}
],
"default": null,
- "description": "Override the sigma schedule shift. When set, uses a fixed shift (e.g. 3.0 for Lightning LoRAs) instead of the default dynamic shifting. Leave unset for the base model's default schedule.",
+ "description": "CLIP (tokenizer, text encoder, LoRAs) and skipped layer count",
"field_kind": "input",
- "input": "any",
- "orig_default": null,
- "orig_required": false,
- "title": "Shift"
+ "input": "connection",
+ "orig_required": true
},
"type": {
- "const": "qwen_image_denoise",
- "default": "qwen_image_denoise",
+ "const": "sdxl_refiner_compel_prompt",
+ "default": "sdxl_refiner_compel_prompt",
"field_kind": "node_attribute",
"title": "type",
"type": "string"
}
},
"required": ["type", "id"],
- "tags": ["image", "qwen_image"],
- "title": "Denoise - Qwen Image",
+ "tags": ["sdxl", "compel", "prompt"],
+ "title": "Prompt - SDXL Refiner",
"type": "object",
- "version": "1.0.0",
+ "version": "1.1.2",
"output": {
- "$ref": "#/components/schemas/LatentsOutput"
+ "$ref": "#/components/schemas/ConditioningOutput"
}
},
- "QwenImageImageToLatentsInvocation": {
- "category": "image",
+ "SDXLRefinerModelLoaderInvocation": {
+ "category": "model",
"class": "invocation",
- "classification": "prototype",
- "description": "Generates latents from an image using the Qwen Image VAE.",
+ "classification": "stable",
+ "description": "Loads an sdxl refiner model, outputting its submodels.",
"node_pack": "invokeai",
"properties": {
- "board": {
- "anyOf": [
- {
- "$ref": "#/components/schemas/BoardField"
- },
- {
- "type": "null"
- }
- ],
- "default": null,
- "description": "The board to save the image to",
- "field_kind": "internal",
- "input": "direct",
- "orig_required": false,
- "ui_hidden": false
- },
- "metadata": {
- "anyOf": [
- {
- "$ref": "#/components/schemas/MetadataField"
- },
- {
- "type": "null"
- }
- ],
- "default": null,
- "description": "Optional metadata to be saved with the image",
- "field_kind": "internal",
- "input": "connection",
- "orig_required": false,
- "ui_hidden": false
- },
"id": {
"description": "The id of this instance of an invocation. Must be unique among all instances of invocations.",
"field_kind": "node_attribute",
@@ -61356,92 +68315,87 @@
"title": "Use Cache",
"type": "boolean"
},
- "image": {
- "anyOf": [
- {
- "$ref": "#/components/schemas/ImageField"
- },
- {
- "type": "null"
- }
- ],
- "default": null,
- "description": "The image to encode.",
- "field_kind": "input",
- "input": "any",
- "orig_required": true
- },
- "vae": {
- "anyOf": [
- {
- "$ref": "#/components/schemas/VAEField"
- },
- {
- "type": "null"
- }
- ],
- "default": null,
- "description": "VAE",
- "field_kind": "input",
- "input": "connection",
- "orig_required": true
- },
- "width": {
- "anyOf": [
- {
- "type": "integer"
- },
- {
- "type": "null"
- }
- ],
- "default": null,
- "description": "Resize the image to this width before encoding. If not set, encodes at the image's original size.",
- "field_kind": "input",
- "input": "any",
- "orig_default": null,
- "orig_required": false,
- "title": "Width"
- },
- "height": {
+ "model": {
"anyOf": [
{
- "type": "integer"
+ "$ref": "#/components/schemas/ModelIdentifierField"
},
{
"type": "null"
}
],
"default": null,
- "description": "Resize the image to this height before encoding. If not set, encodes at the image's original size.",
+ "description": "SDXL Refiner Main Modde (UNet, VAE, CLIP2) to load",
"field_kind": "input",
"input": "any",
- "orig_default": null,
- "orig_required": false,
- "title": "Height"
+ "orig_required": true,
+ "ui_model_base": ["sdxl-refiner"],
+ "ui_model_type": ["main"]
},
"type": {
- "const": "qwen_image_i2l",
- "default": "qwen_image_i2l",
+ "const": "sdxl_refiner_model_loader",
+ "default": "sdxl_refiner_model_loader",
"field_kind": "node_attribute",
"title": "type",
"type": "string"
}
},
"required": ["type", "id"],
- "tags": ["image", "latents", "vae", "i2l", "qwen_image"],
- "title": "Image to Latents - Qwen Image",
+ "tags": ["model", "sdxl", "refiner"],
+ "title": "Refiner Model - SDXL",
"type": "object",
- "version": "1.0.0",
+ "version": "1.0.4",
"output": {
- "$ref": "#/components/schemas/LatentsOutput"
+ "$ref": "#/components/schemas/SDXLRefinerModelLoaderOutput"
}
},
- "QwenImageLatentsToImageInvocation": {
- "category": "latents",
+ "SDXLRefinerModelLoaderOutput": {
+ "class": "output",
+ "description": "SDXL refiner model loader output",
+ "properties": {
+ "unet": {
+ "$ref": "#/components/schemas/UNetField",
+ "description": "UNet (scheduler, LoRAs)",
+ "field_kind": "output",
+ "title": "UNet",
+ "ui_hidden": false
+ },
+ "clip2": {
+ "$ref": "#/components/schemas/CLIPField",
+ "description": "CLIP (tokenizer, text encoder, LoRAs) and skipped layer count",
+ "field_kind": "output",
+ "title": "CLIP 2",
+ "ui_hidden": false
+ },
+ "vae": {
+ "$ref": "#/components/schemas/VAEField",
+ "description": "VAE",
+ "field_kind": "output",
+ "title": "VAE",
+ "ui_hidden": false
+ },
+ "type": {
+ "const": "sdxl_refiner_model_loader_output",
+ "default": "sdxl_refiner_model_loader_output",
+ "field_kind": "node_attribute",
+ "title": "type",
+ "type": "string"
+ }
+ },
+ "required": ["output_meta", "unet", "clip2", "vae", "type", "type"],
+ "title": "SDXLRefinerModelLoaderOutput",
+ "type": "object"
+ },
+ "SQLiteDirection": {
+ "type": "string",
+ "enum": ["ASC", "DESC"],
+ "title": "SQLiteDirection"
+ },
+ "SaveImageInvocation": {
+ "category": "image",
"class": "invocation",
- "classification": "prototype",
- "description": "Generates an image from latents using the Qwen Image VAE.",
+ "classification": "stable",
+ "description": "Saves an image. Unlike an image primitive, this invocation stores a copy of the image.",
"node_pack": "invokeai",
"properties": {
"board": {
@@ -61494,156 +68448,83 @@
"ui_type": "IsIntermediate"
},
"use_cache": {
- "default": true,
+ "default": false,
"description": "Whether or not to use the cache",
"field_kind": "node_attribute",
"title": "Use Cache",
"type": "boolean"
},
- "latents": {
- "anyOf": [
- {
- "$ref": "#/components/schemas/LatentsField"
- },
- {
- "type": "null"
- }
- ],
- "default": null,
- "description": "Latents tensor",
- "field_kind": "input",
- "input": "connection",
- "orig_required": true
- },
- "vae": {
+ "image": {
"anyOf": [
{
- "$ref": "#/components/schemas/VAEField"
+ "$ref": "#/components/schemas/ImageField"
},
{
"type": "null"
}
],
"default": null,
- "description": "VAE",
+ "description": "The image to process",
"field_kind": "input",
- "input": "connection",
+ "input": "any",
"orig_required": true
},
"type": {
- "const": "qwen_image_l2i",
- "default": "qwen_image_l2i",
+ "const": "save_image",
+ "default": "save_image",
"field_kind": "node_attribute",
"title": "type",
"type": "string"
}
},
"required": ["type", "id"],
- "tags": ["latents", "image", "vae", "l2i", "qwen_image"],
- "title": "Latents to Image - Qwen Image",
+ "tags": ["primitives", "image"],
+ "title": "Save Image",
"type": "object",
- "version": "1.0.0",
+ "version": "1.2.2",
"output": {
"$ref": "#/components/schemas/ImageOutput"
}
},
- "QwenImageLoRACollectionLoader": {
- "category": "model",
+ "SaveImageToFileInvocation": {
+ "category": "image",
"class": "invocation",
- "classification": "prototype",
- "description": "Applies a collection of LoRAs to a Qwen Image transformer.",
+ "classification": "stable",
+ "description": "Saves an image to the gallery (like the standard Save Image node) AND additionally exports a copy\nto the filesystem with a custom filename.\n\nFilename pattern: {prefix}{uuid}{suffix}.{file_format}\n- The UUID is the same UUID used for the gallery entry, so the exported file can be matched to the gallery item.\n- The gallery entry itself always uses the plain UUID (prefix/suffix apply only to the exported file on disk).\n- Board and Metadata inputs behave exactly like the standard Save Image node.\n- The export target is restricted to (subfolders of) the InvokeAI outputs folder \u2014 absolute paths are rejected.\n\nExample: prefix=\"hero_\", suffix=\"_final\", file_format=\"png\" \u2192 \"hero__final.png\"",
"node_pack": "invokeai",
"properties": {
- "id": {
- "description": "The id of this instance of an invocation. Must be unique among all instances of invocations.",
- "field_kind": "node_attribute",
- "title": "Id",
- "type": "string"
- },
- "is_intermediate": {
- "default": false,
- "description": "Whether or not this is an intermediate invocation.",
- "field_kind": "node_attribute",
- "input": "direct",
- "orig_required": true,
- "title": "Is Intermediate",
- "type": "boolean",
- "ui_hidden": false,
- "ui_type": "IsIntermediate"
- },
- "use_cache": {
- "default": true,
- "description": "Whether or not to use the cache",
- "field_kind": "node_attribute",
- "title": "Use Cache",
- "type": "boolean"
- },
- "loras": {
+ "board": {
"anyOf": [
{
- "$ref": "#/components/schemas/LoRAField"
- },
- {
- "items": {
- "$ref": "#/components/schemas/LoRAField"
- },
- "type": "array"
+ "$ref": "#/components/schemas/BoardField"
},
{
"type": "null"
}
],
"default": null,
- "description": "LoRA models and weights. May be a single LoRA or collection.",
- "field_kind": "input",
- "input": "any",
- "orig_default": null,
+ "description": "The board to save the image to",
+ "field_kind": "internal",
+ "input": "direct",
"orig_required": false,
- "title": "LoRAs",
- "ui_model_base": ["qwen-image"],
- "ui_model_type": ["lora"]
+ "ui_hidden": false
},
- "transformer": {
+ "metadata": {
"anyOf": [
{
- "$ref": "#/components/schemas/TransformerField"
+ "$ref": "#/components/schemas/MetadataField"
},
{
"type": "null"
}
],
"default": null,
- "description": "Transformer",
- "field_kind": "input",
+ "description": "Optional metadata to be saved with the image",
+ "field_kind": "internal",
"input": "connection",
- "orig_default": null,
"orig_required": false,
- "title": "Transformer"
+ "ui_hidden": false
},
- "type": {
- "const": "qwen_image_lora_collection_loader",
- "default": "qwen_image_lora_collection_loader",
- "field_kind": "node_attribute",
- "title": "type",
- "type": "string"
- }
- },
- "required": ["type", "id"],
- "tags": ["lora", "model", "qwen_image"],
- "title": "Apply LoRA Collection - Qwen Image",
- "type": "object",
- "version": "1.0.1",
- "output": {
- "$ref": "#/components/schemas/QwenImageLoRALoaderOutput"
- }
- },
- "QwenImageLoRALoaderInvocation": {
- "category": "model",
- "class": "invocation",
- "classification": "prototype",
- "description": "Apply a LoRA model to a Qwen Image transformer.",
- "node_pack": "invokeai",
- "properties": {
"id": {
"description": "The id of this instance of an invocation. Must be unique among all instances of invocations.",
"field_kind": "node_attribute",
@@ -61662,110 +68543,102 @@
"ui_type": "IsIntermediate"
},
"use_cache": {
- "default": true,
+ "default": false,
"description": "Whether or not to use the cache",
"field_kind": "node_attribute",
"title": "Use Cache",
"type": "boolean"
},
- "lora": {
+ "image": {
"anyOf": [
{
- "$ref": "#/components/schemas/ModelIdentifierField"
+ "$ref": "#/components/schemas/ImageField"
},
{
"type": "null"
}
],
"default": null,
- "description": "LoRA model to load",
+ "description": "The image to save and export",
"field_kind": "input",
"input": "any",
- "orig_required": true,
- "title": "LoRA",
- "ui_model_base": ["qwen-image"],
- "ui_model_type": ["lora"]
+ "orig_required": true
},
- "weight": {
- "default": 1.0,
- "description": "The weight at which the LoRA is applied to each model",
+ "output_directory": {
+ "default": "",
+ "description": "Target subdirectory (relative to the configured InvokeAI outputs folder) for the exported file. Leave empty to use the outputs folder directly. Example: 'my-exports' \u2192 /my-exports/. Nested paths like 'exports/2026' are allowed. Absolute paths and path traversal ('..') are not allowed for security reasons. The directory is created automatically if it doesn't exist.",
"field_kind": "input",
"input": "any",
- "orig_default": 1.0,
+ "orig_default": "",
"orig_required": false,
- "title": "Weight",
- "type": "number"
+ "title": "Output Directory",
+ "type": "string"
},
- "transformer": {
- "anyOf": [
- {
- "$ref": "#/components/schemas/TransformerField"
- },
- {
- "type": "null"
- }
- ],
- "default": null,
- "description": "Transformer",
+ "prefix": {
+ "default": "",
+ "description": "Text prepended to the UUID in the exported filename. Example: 'portrait_' \u2192 'portrait_.png'",
"field_kind": "input",
- "input": "connection",
- "orig_default": null,
+ "input": "any",
+ "orig_default": "",
"orig_required": false,
- "title": "Transformer"
+ "title": "Prefix",
+ "type": "string"
+ },
+ "suffix": {
+ "default": "",
+ "description": "Text appended to the UUID (before the extension). Example: '_v2' \u2192 '_v2.png'",
+ "field_kind": "input",
+ "input": "any",
+ "orig_default": "",
+ "orig_required": false,
+ "title": "Suffix",
+ "type": "string"
+ },
+ "file_format": {
+ "default": "png",
+ "description": "File format for the exported file. PNG is lossless; JPG/WEBP are lossy and respect 'quality'.",
+ "enum": ["png", "jpg", "webp"],
+ "field_kind": "input",
+ "input": "any",
+ "orig_default": "png",
+ "orig_required": false,
+ "title": "File Format",
+ "type": "string"
+ },
+ "quality": {
+ "default": 95,
+ "description": "Compression quality for JPG and WEBP (1-100, higher = better quality, larger file). Ignored for PNG.",
+ "field_kind": "input",
+ "input": "any",
+ "maximum": 100,
+ "minimum": 1,
+ "orig_default": 95,
+ "orig_required": false,
+ "title": "Quality",
+ "type": "integer"
},
"type": {
- "const": "qwen_image_lora_loader",
- "default": "qwen_image_lora_loader",
+ "const": "save_image_to_file",
+ "default": "save_image_to_file",
"field_kind": "node_attribute",
"title": "type",
"type": "string"
}
},
"required": ["type", "id"],
- "tags": ["lora", "model", "qwen_image"],
- "title": "Apply LoRA - Qwen Image",
+ "tags": ["image", "export", "file", "save"],
+ "title": "Save Image (Gallery + File Export)",
"type": "object",
"version": "1.0.0",
"output": {
- "$ref": "#/components/schemas/QwenImageLoRALoaderOutput"
+ "$ref": "#/components/schemas/ImageOutput"
}
},
- "QwenImageLoRALoaderOutput": {
- "class": "output",
- "description": "Qwen Image LoRA Loader Output",
- "properties": {
- "transformer": {
- "anyOf": [
- {
- "$ref": "#/components/schemas/TransformerField"
- },
- {
- "type": "null"
- }
- ],
- "default": null,
- "description": "Transformer",
- "field_kind": "output",
- "title": "Transformer",
- "ui_hidden": false
- },
- "type": {
- "const": "qwen_image_lora_loader_output",
- "default": "qwen_image_lora_loader_output",
- "field_kind": "node_attribute",
- "title": "type",
- "type": "string"
- }
- },
- "required": ["output_meta", "transformer", "type", "type"],
- "title": "QwenImageLoRALoaderOutput",
- "type": "object"
- },
- "QwenImageModelLoaderInvocation": {
- "category": "model",
+ "ScaleLatentsInvocation": {
+ "category": "latents",
"class": "invocation",
- "classification": "prototype",
- "description": "Loads a Qwen Image model, outputting its submodels.\n\nThe transformer is always loaded from the main model (Diffusers or GGUF).\n\nComponents can be mixed and matched:\n- VAE: standalone Qwen Image VAE checkpoint, the Component Source (Diffusers),\n or the main model if it's Diffusers.\n- Qwen VL Encoder: standalone Qwen2.5-VL encoder, the Component Source\n (Diffusers), or the main model if it's Diffusers.\n\nTogether, the standalone VAE and standalone encoder allow running a GGUF\ntransformer without ever downloading the full ~40 GB Diffusers pipeline.",
+ "classification": "stable",
+ "description": "Scales latents by a given factor.",
"node_pack": "invokeai",
"properties": {
"id": {
@@ -61792,132 +68665,81 @@
"title": "Use Cache",
"type": "boolean"
},
- "model": {
- "$ref": "#/components/schemas/ModelIdentifierField",
- "description": "Qwen Image Edit model (Transformer) to load",
- "field_kind": "input",
- "input": "direct",
- "orig_required": true,
- "title": "Transformer",
- "ui_model_base": ["qwen-image"],
- "ui_model_type": ["main"]
- },
- "vae_model": {
+ "latents": {
"anyOf": [
{
- "$ref": "#/components/schemas/ModelIdentifierField"
+ "$ref": "#/components/schemas/LatentsField"
},
{
"type": "null"
}
],
"default": null,
- "description": "Standalone Qwen Image VAE model. If not provided, VAE will be loaded from the Component Source (or from the main model if it is Diffusers).",
+ "description": "Latents tensor",
"field_kind": "input",
- "input": "direct",
- "orig_default": null,
- "orig_required": false,
- "title": "VAE",
- "ui_model_base": ["qwen-image"],
- "ui_model_type": ["vae"]
+ "input": "connection",
+ "orig_required": true
},
- "qwen_vl_encoder_model": {
+ "scale_factor": {
"anyOf": [
{
- "$ref": "#/components/schemas/ModelIdentifierField"
+ "exclusiveMinimum": 0,
+ "type": "number"
},
{
"type": "null"
}
],
"default": null,
- "description": "Standalone Qwen2.5-VL encoder model. If not provided, the encoder will be loaded from the Component Source (or from the main model if it is Diffusers).",
+ "description": "The factor by which to scale",
"field_kind": "input",
- "input": "direct",
- "orig_default": null,
+ "input": "any",
+ "orig_required": true,
+ "title": "Scale Factor"
+ },
+ "mode": {
+ "default": "bilinear",
+ "description": "Interpolation mode",
+ "enum": ["nearest", "linear", "bilinear", "bicubic", "trilinear", "area", "nearest-exact"],
+ "field_kind": "input",
+ "input": "any",
+ "orig_default": "bilinear",
"orig_required": false,
- "title": "Qwen VL Encoder",
- "ui_model_type": ["qwen_vl_encoder"]
+ "title": "Mode",
+ "type": "string"
},
- "component_source": {
- "anyOf": [
- {
- "$ref": "#/components/schemas/ModelIdentifierField"
- },
- {
- "type": "null"
- }
- ],
- "default": null,
- "description": "Diffusers Qwen Image model to extract VAE and/or Qwen VL encoder from. Use this if you don't have separate VAE/encoder models. Ignored for any submodel that is provided separately.",
+ "antialias": {
+ "default": false,
+ "description": "Whether or not to apply antialiasing (bilinear or bicubic only)",
"field_kind": "input",
- "input": "direct",
- "orig_default": null,
+ "input": "any",
+ "orig_default": false,
"orig_required": false,
- "title": "Component Source (Diffusers)",
- "ui_model_base": ["qwen-image"],
- "ui_model_format": ["diffusers"],
- "ui_model_type": ["main"]
+ "title": "Antialias",
+ "type": "boolean"
},
"type": {
- "const": "qwen_image_model_loader",
- "default": "qwen_image_model_loader",
+ "const": "lscale",
+ "default": "lscale",
"field_kind": "node_attribute",
"title": "type",
"type": "string"
}
},
- "required": ["model", "type", "id"],
- "tags": ["model", "qwen_image"],
- "title": "Main Model - Qwen Image",
+ "required": ["type", "id"],
+ "tags": ["latents", "resize"],
+ "title": "Scale Latents",
"type": "object",
- "version": "1.2.0",
+ "version": "1.0.2",
"output": {
- "$ref": "#/components/schemas/QwenImageModelLoaderOutput"
+ "$ref": "#/components/schemas/LatentsOutput"
}
},
- "QwenImageModelLoaderOutput": {
- "class": "output",
- "description": "Qwen Image model loader output.",
- "properties": {
- "transformer": {
- "$ref": "#/components/schemas/TransformerField",
- "description": "Transformer",
- "field_kind": "output",
- "title": "Transformer",
- "ui_hidden": false
- },
- "qwen_vl_encoder": {
- "$ref": "#/components/schemas/QwenVLEncoderField",
- "description": "Qwen2.5-VL tokenizer, processor and text/vision encoder",
- "field_kind": "output",
- "title": "Qwen VL Encoder",
- "ui_hidden": false
- },
- "vae": {
- "$ref": "#/components/schemas/VAEField",
- "description": "VAE",
- "field_kind": "output",
- "title": "VAE",
- "ui_hidden": false
- },
- "type": {
- "const": "qwen_image_model_loader_output",
- "default": "qwen_image_model_loader_output",
- "field_kind": "node_attribute",
- "title": "type",
- "type": "string"
- }
- },
- "required": ["output_meta", "transformer", "qwen_vl_encoder", "vae", "type", "type"],
- "title": "QwenImageModelLoaderOutput",
- "type": "object"
- },
- "QwenImageTextEncoderInvocation": {
- "category": "conditioning",
+ "SchedulerInvocation": {
+ "category": "latents",
"class": "invocation",
- "classification": "prototype",
- "description": "Encodes text and reference images for Qwen Image using Qwen2.5-VL.",
+ "classification": "stable",
+ "description": "Selects a scheduler.",
"node_pack": "invokeai",
"properties": {
"id": {
@@ -61944,366 +68766,134 @@
"title": "Use Cache",
"type": "boolean"
},
- "prompt": {
- "anyOf": [
- {
- "type": "string"
- },
- {
- "type": "null"
- }
- ],
- "default": null,
- "description": "Text prompt describing the desired edit.",
- "field_kind": "input",
- "input": "any",
- "orig_required": true,
- "title": "Prompt",
- "ui_component": "textarea"
- },
- "reference_images": {
- "default": [],
- "description": "Reference images to guide the edit. The model can use multiple reference images.",
- "field_kind": "input",
- "input": "any",
- "items": {
- "$ref": "#/components/schemas/ImageField"
- },
- "orig_default": [],
- "orig_required": false,
- "title": "Reference Images",
- "type": "array"
- },
- "qwen_vl_encoder": {
- "anyOf": [
- {
- "$ref": "#/components/schemas/QwenVLEncoderField"
- },
- {
- "type": "null"
- }
+ "scheduler": {
+ "default": "euler",
+ "description": "Scheduler to use during inference",
+ "enum": [
+ "ddim",
+ "ddpm",
+ "deis",
+ "deis_k",
+ "lms",
+ "lms_k",
+ "pndm",
+ "heun",
+ "heun_k",
+ "euler",
+ "euler_k",
+ "euler_a",
+ "kdpm_2",
+ "kdpm_2_k",
+ "kdpm_2_a",
+ "kdpm_2_a_k",
+ "dpmpp_2s",
+ "dpmpp_2s_k",
+ "dpmpp_2m",
+ "dpmpp_2m_k",
+ "dpmpp_2m_sde",
+ "dpmpp_2m_sde_k",
+ "dpmpp_3m",
+ "dpmpp_3m_k",
+ "dpmpp_sde",
+ "dpmpp_sde_k",
+ "er_sde",
+ "unipc",
+ "unipc_k",
+ "lcm",
+ "tcd"
],
- "default": null,
- "description": "Qwen2.5-VL tokenizer, processor and text/vision encoder",
- "field_kind": "input",
- "input": "connection",
- "orig_required": true,
- "title": "Qwen VL Encoder"
- },
- "quantization": {
- "default": "none",
- "description": "Quantize the Qwen VL encoder to reduce VRAM usage. 'nf4' (4-bit) saves the most memory, 'int8' (8-bit) is a middle ground.",
- "enum": ["none", "int8", "nf4"],
"field_kind": "input",
"input": "any",
- "orig_default": "none",
+ "orig_default": "euler",
"orig_required": false,
- "title": "Quantization",
- "type": "string"
+ "title": "Scheduler",
+ "type": "string",
+ "ui_type": "SchedulerField"
},
"type": {
- "const": "qwen_image_text_encoder",
- "default": "qwen_image_text_encoder",
+ "const": "scheduler",
+ "default": "scheduler",
"field_kind": "node_attribute",
"title": "type",
"type": "string"
}
},
"required": ["type", "id"],
- "tags": ["prompt", "conditioning", "qwen_image"],
- "title": "Prompt - Qwen Image",
+ "tags": ["scheduler"],
+ "title": "Scheduler",
"type": "object",
- "version": "1.2.0",
+ "version": "1.0.0",
"output": {
- "$ref": "#/components/schemas/QwenImageConditioningOutput"
+ "$ref": "#/components/schemas/SchedulerOutput"
}
},
- "QwenImageVariantType": {
- "type": "string",
- "enum": ["generate", "edit"],
- "title": "QwenImageVariantType",
- "description": "Qwen Image model variants."
- },
- "QwenVLEncoderField": {
- "description": "Field for Qwen2.5-VL encoder used by Qwen Image Edit models.",
- "properties": {
- "tokenizer": {
- "$ref": "#/components/schemas/ModelIdentifierField",
- "description": "Info to load tokenizer submodel"
- },
- "text_encoder": {
- "$ref": "#/components/schemas/ModelIdentifierField",
- "description": "Info to load text_encoder submodel"
- }
- },
- "required": ["tokenizer", "text_encoder"],
- "title": "QwenVLEncoderField",
- "type": "object"
- },
- "QwenVLEncoder_Checkpoint_Config": {
- "properties": {
- "key": {
- "type": "string",
- "title": "Key",
- "description": "A unique key for this model."
- },
- "hash": {
- "type": "string",
- "title": "Hash",
- "description": "The hash of the model file(s)."
- },
- "path": {
- "type": "string",
- "title": "Path",
- "description": "Path to the model on the filesystem. Relative paths are relative to the Invoke root directory."
- },
- "file_size": {
- "type": "integer",
- "title": "File Size",
- "description": "The size of the model in bytes."
- },
- "name": {
- "type": "string",
- "title": "Name",
- "description": "Name of the model."
- },
- "description": {
- "anyOf": [
- {
- "type": "string"
- },
- {
- "type": "null"
- }
- ],
- "title": "Description",
- "description": "Model description"
- },
- "source": {
- "type": "string",
- "title": "Source",
- "description": "The original source of the model (path, URL or repo_id)."
- },
- "source_type": {
- "$ref": "#/components/schemas/ModelSourceType",
- "description": "The type of source"
- },
- "source_api_response": {
- "anyOf": [
- {
- "type": "string"
- },
- {
- "type": "null"
- }
- ],
- "title": "Source Api Response",
- "description": "The original API response from the source, as stringified JSON."
- },
- "source_url": {
- "anyOf": [
- {
- "type": "string"
- },
- {
- "type": "null"
- }
- ],
- "title": "Source Url",
- "description": "Optional URL for the model (e.g. download page or model page)."
- },
- "cover_image": {
- "anyOf": [
- {
- "type": "string"
- },
- {
- "type": "null"
- }
- ],
- "title": "Cover Image",
- "description": "Url for image to preview model"
- },
- "config_path": {
- "anyOf": [
- {
- "type": "string"
- },
- {
- "type": "null"
- }
- ],
- "title": "Config Path",
- "description": "Path to the config for this model, if any."
- },
- "base": {
- "type": "string",
- "const": "any",
- "title": "Base",
- "default": "any"
- },
- "type": {
- "type": "string",
- "const": "qwen_vl_encoder",
- "title": "Type",
- "default": "qwen_vl_encoder"
- },
- "format": {
- "type": "string",
- "const": "checkpoint",
- "title": "Format",
- "default": "checkpoint"
- }
- },
- "type": "object",
- "required": [
- "key",
- "hash",
- "path",
- "file_size",
- "name",
- "description",
- "source",
- "source_type",
- "source_api_response",
- "source_url",
- "cover_image",
- "config_path",
- "base",
- "type",
- "format"
- ],
- "title": "QwenVLEncoder_Checkpoint_Config",
- "description": "Configuration for single-file Qwen2.5-VL encoder checkpoints (safetensors).\n\nThis matches ComfyUI-style consolidated single-file encoders such as\n`qwen_2.5_vl_7b_fp8_scaled.safetensors`, which bundle the language model\nand the visual tower into one file (typically with FP8 + per-tensor\n`weight_scale` ComfyUI quantization).\n\nThe matching tokenizer + processor are pulled from HuggingFace\n(`Qwen/Qwen2.5-VL-7B-Instruct`) on first use and cached for offline use."
- },
- "QwenVLEncoder_Diffusers_Config": {
+ "SchedulerOutput": {
+ "class": "output",
"properties": {
- "key": {
- "type": "string",
- "title": "Key",
- "description": "A unique key for this model."
- },
- "hash": {
- "type": "string",
- "title": "Hash",
- "description": "The hash of the model file(s)."
- },
- "path": {
- "type": "string",
- "title": "Path",
- "description": "Path to the model on the filesystem. Relative paths are relative to the Invoke root directory."
- },
- "file_size": {
- "type": "integer",
- "title": "File Size",
- "description": "The size of the model in bytes."
- },
- "name": {
- "type": "string",
- "title": "Name",
- "description": "Name of the model."
- },
- "description": {
- "anyOf": [
- {
- "type": "string"
- },
- {
- "type": "null"
- }
- ],
- "title": "Description",
- "description": "Model description"
- },
- "source": {
- "type": "string",
- "title": "Source",
- "description": "The original source of the model (path, URL or repo_id)."
- },
- "source_type": {
- "$ref": "#/components/schemas/ModelSourceType",
- "description": "The type of source"
- },
- "source_api_response": {
- "anyOf": [
- {
- "type": "string"
- },
- {
- "type": "null"
- }
- ],
- "title": "Source Api Response",
- "description": "The original API response from the source, as stringified JSON."
- },
- "source_url": {
- "anyOf": [
- {
- "type": "string"
- },
- {
- "type": "null"
- }
- ],
- "title": "Source Url",
- "description": "Optional URL for the model (e.g. download page or model page)."
- },
- "cover_image": {
- "anyOf": [
- {
- "type": "string"
- },
- {
- "type": "null"
- }
- ],
- "title": "Cover Image",
- "description": "Url for image to preview model"
- },
- "base": {
+ "scheduler": {
+ "description": "Scheduler to use during inference",
+ "enum": [
+ "ddim",
+ "ddpm",
+ "deis",
+ "deis_k",
+ "lms",
+ "lms_k",
+ "pndm",
+ "heun",
+ "heun_k",
+ "euler",
+ "euler_k",
+ "euler_a",
+ "kdpm_2",
+ "kdpm_2_k",
+ "kdpm_2_a",
+ "kdpm_2_a_k",
+ "dpmpp_2s",
+ "dpmpp_2s_k",
+ "dpmpp_2m",
+ "dpmpp_2m_k",
+ "dpmpp_2m_sde",
+ "dpmpp_2m_sde_k",
+ "dpmpp_3m",
+ "dpmpp_3m_k",
+ "dpmpp_sde",
+ "dpmpp_sde_k",
+ "er_sde",
+ "unipc",
+ "unipc_k",
+ "lcm",
+ "tcd"
+ ],
+ "field_kind": "output",
+ "title": "Scheduler",
"type": "string",
- "const": "any",
- "title": "Base",
- "default": "any"
+ "ui_hidden": false,
+ "ui_type": "SchedulerField"
},
"type": {
- "type": "string",
- "const": "qwen_vl_encoder",
- "title": "Type",
- "default": "qwen_vl_encoder"
- },
- "format": {
- "type": "string",
- "const": "qwen_vl_encoder",
- "title": "Format",
- "default": "qwen_vl_encoder"
+ "const": "scheduler_output",
+ "default": "scheduler_output",
+ "field_kind": "node_attribute",
+ "title": "type",
+ "type": "string"
}
},
- "type": "object",
- "required": [
- "key",
- "hash",
- "path",
- "file_size",
- "name",
- "description",
- "source",
- "source_type",
- "source_api_response",
- "source_url",
- "cover_image",
- "base",
- "type",
- "format"
- ],
- "title": "QwenVLEncoder_Diffusers_Config",
- "description": "Configuration for standalone Qwen2.5-VL encoder models in diffusers-style folder layout.\n\nExpected structure:\n /\n text_encoder/\n config.json (with `_class_name` or `architectures` listing\n `Qwen2_5_VLForConditionalGeneration`)\n model.safetensors\n tokenizer/\n tokenizer_config.json\n ...\n processor/ (optional, for vision preprocessing)\n preprocessor_config.json\n\nThis lets users avoid downloading the full ~40 GB Qwen Image diffusers pipeline\nwhen they only need the Qwen2.5-VL encoder for use with a GGUF transformer."
+ "required": ["output_meta", "scheduler", "type", "type"],
+ "title": "SchedulerOutput",
+ "type": "object"
},
- "RandomFloatInvocation": {
- "category": "math",
+ "SchedulerPredictionType": {
+ "type": "string",
+ "enum": ["epsilon", "v_prediction", "sample"],
+ "title": "SchedulerPredictionType",
+ "description": "Scheduler prediction type."
+ },
+ "Sd3ModelLoaderInvocation": {
+ "category": "model",
"class": "invocation",
"classification": "stable",
- "description": "Outputs a single random float",
+ "description": "Loads a SD3 base model, outputting its submodels.",
"node_pack": "invokeai",
"properties": {
"id": {
@@ -62324,132 +68914,169 @@
"ui_type": "IsIntermediate"
},
"use_cache": {
- "default": false,
+ "default": true,
"description": "Whether or not to use the cache",
"field_kind": "node_attribute",
"title": "Use Cache",
"type": "boolean"
},
- "low": {
- "default": 0.0,
- "description": "The inclusive low value",
+ "model": {
+ "$ref": "#/components/schemas/ModelIdentifierField",
+ "description": "SD3 model (MMDiTX) to load",
"field_kind": "input",
- "input": "any",
- "orig_default": 0.0,
+ "input": "direct",
+ "orig_required": true,
+ "ui_model_base": ["sd-3"],
+ "ui_model_type": ["main"]
+ },
+ "t5_encoder_model": {
+ "anyOf": [
+ {
+ "$ref": "#/components/schemas/ModelIdentifierField"
+ },
+ {
+ "type": "null"
+ }
+ ],
+ "default": null,
+ "description": "T5 tokenizer and text encoder",
+ "field_kind": "input",
+ "input": "direct",
+ "orig_default": null,
"orig_required": false,
- "title": "Low",
- "type": "number"
+ "title": "T5 Encoder",
+ "ui_model_type": ["t5_encoder"]
},
- "high": {
- "default": 1.0,
- "description": "The exclusive high value",
+ "clip_l_model": {
+ "anyOf": [
+ {
+ "$ref": "#/components/schemas/ModelIdentifierField"
+ },
+ {
+ "type": "null"
+ }
+ ],
+ "default": null,
+ "description": "CLIP Embed loader",
"field_kind": "input",
- "input": "any",
- "orig_default": 1.0,
+ "input": "direct",
+ "orig_default": null,
"orig_required": false,
- "title": "High",
- "type": "number"
+ "title": "CLIP L Encoder",
+ "ui_model_type": ["clip_embed"],
+ "ui_model_variant": ["large"]
},
- "decimals": {
- "default": 2,
- "description": "The number of decimal places to round to",
+ "clip_g_model": {
+ "anyOf": [
+ {
+ "$ref": "#/components/schemas/ModelIdentifierField"
+ },
+ {
+ "type": "null"
+ }
+ ],
+ "default": null,
+ "description": "CLIP-G Embed loader",
+ "field_kind": "input",
+ "input": "direct",
+ "orig_default": null,
+ "orig_required": false,
+ "title": "CLIP G Encoder",
+ "ui_model_type": ["clip_embed"],
+ "ui_model_variant": ["gigantic"]
+ },
+ "vae_model": {
+ "anyOf": [
+ {
+ "$ref": "#/components/schemas/ModelIdentifierField"
+ },
+ {
+ "type": "null"
+ }
+ ],
+ "default": null,
+ "description": "VAE model to load",
"field_kind": "input",
"input": "any",
- "orig_default": 2,
+ "orig_default": null,
"orig_required": false,
- "title": "Decimals",
- "type": "integer"
+ "title": "VAE",
+ "ui_model_base": ["sd-3"],
+ "ui_model_type": ["vae"]
},
"type": {
- "const": "rand_float",
- "default": "rand_float",
+ "const": "sd3_model_loader",
+ "default": "sd3_model_loader",
"field_kind": "node_attribute",
"title": "type",
"type": "string"
}
},
- "required": ["type", "id"],
- "tags": ["math", "float", "random"],
- "title": "Random Float",
+ "required": ["model", "type", "id"],
+ "tags": ["model", "sd3"],
+ "title": "Main Model - SD3",
"type": "object",
"version": "1.0.1",
"output": {
- "$ref": "#/components/schemas/FloatOutput"
+ "$ref": "#/components/schemas/Sd3ModelLoaderOutput"
}
},
- "RandomIntInvocation": {
- "category": "math",
- "class": "invocation",
- "classification": "stable",
- "description": "Outputs a single random integer.",
- "node_pack": "invokeai",
+ "Sd3ModelLoaderOutput": {
+ "class": "output",
+ "description": "SD3 base model loader output.",
"properties": {
- "id": {
- "description": "The id of this instance of an invocation. Must be unique among all instances of invocations.",
- "field_kind": "node_attribute",
- "title": "Id",
- "type": "string"
+ "transformer": {
+ "$ref": "#/components/schemas/TransformerField",
+ "description": "Transformer",
+ "field_kind": "output",
+ "title": "Transformer",
+ "ui_hidden": false
},
- "is_intermediate": {
- "default": false,
- "description": "Whether or not this is an intermediate invocation.",
- "field_kind": "node_attribute",
- "input": "direct",
- "orig_required": true,
- "title": "Is Intermediate",
- "type": "boolean",
- "ui_hidden": false,
- "ui_type": "IsIntermediate"
+ "clip_l": {
+ "$ref": "#/components/schemas/CLIPField",
+ "description": "CLIP (tokenizer, text encoder, LoRAs) and skipped layer count",
+ "field_kind": "output",
+ "title": "CLIP L",
+ "ui_hidden": false
},
- "use_cache": {
- "default": false,
- "description": "Whether or not to use the cache",
- "field_kind": "node_attribute",
- "title": "Use Cache",
- "type": "boolean"
+ "clip_g": {
+ "$ref": "#/components/schemas/CLIPField",
+ "description": "CLIP (tokenizer, text encoder, LoRAs) and skipped layer count",
+ "field_kind": "output",
+ "title": "CLIP G",
+ "ui_hidden": false
},
- "low": {
- "default": 0,
- "description": "The inclusive low value",
- "field_kind": "input",
- "input": "any",
- "orig_default": 0,
- "orig_required": false,
- "title": "Low",
- "type": "integer"
+ "t5_encoder": {
+ "$ref": "#/components/schemas/T5EncoderField",
+ "description": "T5 tokenizer and text encoder",
+ "field_kind": "output",
+ "title": "T5 Encoder",
+ "ui_hidden": false
},
- "high": {
- "default": 2147483647,
- "description": "The exclusive high value",
- "field_kind": "input",
- "input": "any",
- "orig_default": 2147483647,
- "orig_required": false,
- "title": "High",
- "type": "integer"
+ "vae": {
+ "$ref": "#/components/schemas/VAEField",
+ "description": "VAE",
+ "field_kind": "output",
+ "title": "VAE",
+ "ui_hidden": false
},
"type": {
- "const": "rand_int",
- "default": "rand_int",
+ "const": "sd3_model_loader_output",
+ "default": "sd3_model_loader_output",
"field_kind": "node_attribute",
"title": "type",
"type": "string"
}
},
- "required": ["type", "id"],
- "tags": ["math", "random"],
- "title": "Random Integer",
- "type": "object",
- "version": "1.0.1",
- "output": {
- "$ref": "#/components/schemas/IntegerOutput"
- }
+ "required": ["output_meta", "transformer", "clip_l", "clip_g", "t5_encoder", "vae", "type", "type"],
+ "title": "Sd3ModelLoaderOutput",
+ "type": "object"
},
- "RandomRangeInvocation": {
- "category": "batch",
+ "Sd3TextEncoderInvocation": {
+ "category": "prompt",
"class": "invocation",
"classification": "stable",
- "description": "Creates a collection of random numbers",
+ "description": "Encodes and preps a prompt for a SD3 image.",
"node_pack": "invokeai",
"properties": {
"id": {
@@ -62470,76 +69097,99 @@
"ui_type": "IsIntermediate"
},
"use_cache": {
- "default": false,
+ "default": true,
"description": "Whether or not to use the cache",
"field_kind": "node_attribute",
"title": "Use Cache",
"type": "boolean"
},
- "low": {
- "default": 0,
- "description": "The inclusive low value",
+ "clip_l": {
+ "anyOf": [
+ {
+ "$ref": "#/components/schemas/CLIPField"
+ },
+ {
+ "type": "null"
+ }
+ ],
+ "default": null,
+ "description": "CLIP (tokenizer, text encoder, LoRAs) and skipped layer count",
"field_kind": "input",
- "input": "any",
- "orig_default": 0,
- "orig_required": false,
- "title": "Low",
- "type": "integer"
+ "input": "connection",
+ "orig_required": true,
+ "title": "CLIP L"
},
- "high": {
- "default": 2147483647,
- "description": "The exclusive high value",
+ "clip_g": {
+ "anyOf": [
+ {
+ "$ref": "#/components/schemas/CLIPField"
+ },
+ {
+ "type": "null"
+ }
+ ],
+ "default": null,
+ "description": "CLIP (tokenizer, text encoder, LoRAs) and skipped layer count",
"field_kind": "input",
- "input": "any",
- "orig_default": 2147483647,
- "orig_required": false,
- "title": "High",
- "type": "integer"
+ "input": "connection",
+ "orig_required": true,
+ "title": "CLIP G"
},
- "size": {
- "default": 1,
- "description": "The number of values to generate",
+ "t5_encoder": {
+ "anyOf": [
+ {
+ "$ref": "#/components/schemas/T5EncoderField"
+ },
+ {
+ "type": "null"
+ }
+ ],
+ "default": null,
+ "description": "T5 tokenizer and text encoder",
"field_kind": "input",
- "input": "any",
- "orig_default": 1,
+ "input": "connection",
+ "orig_default": null,
"orig_required": false,
- "title": "Size",
- "type": "integer"
+ "title": "T5Encoder"
},
- "seed": {
- "default": 0,
- "description": "The seed for the RNG (omit for random)",
+ "prompt": {
+ "anyOf": [
+ {
+ "type": "string"
+ },
+ {
+ "type": "null"
+ }
+ ],
+ "default": null,
+ "description": "Text prompt to encode.",
"field_kind": "input",
"input": "any",
- "maximum": 4294967295,
- "minimum": 0,
- "orig_default": 0,
- "orig_required": false,
- "title": "Seed",
- "type": "integer"
+ "orig_required": true,
+ "title": "Prompt"
},
"type": {
- "const": "random_range",
- "default": "random_range",
+ "const": "sd3_text_encoder",
+ "default": "sd3_text_encoder",
"field_kind": "node_attribute",
"title": "type",
"type": "string"
}
},
"required": ["type", "id"],
- "tags": ["range", "integer", "random", "collection"],
- "title": "Random Range",
+ "tags": ["prompt", "conditioning", "sd3"],
+ "title": "Prompt - SD3",
"type": "object",
"version": "1.0.1",
"output": {
- "$ref": "#/components/schemas/IntegerCollectionOutput"
+ "$ref": "#/components/schemas/SD3ConditioningOutput"
}
},
- "RangeInvocation": {
- "category": "batch",
+ "SeamlessModeInvocation": {
+ "category": "model",
"class": "invocation",
"classification": "stable",
- "description": "Creates a range of numbers from start to stop with step",
+ "description": "Applies the seamless transformation to the Model UNet and VAE.",
"node_pack": "invokeai",
"properties": {
"id": {
@@ -62566,60 +69216,162 @@
"title": "Use Cache",
"type": "boolean"
},
- "start": {
- "default": 0,
- "description": "The start of the range",
+ "unet": {
+ "anyOf": [
+ {
+ "$ref": "#/components/schemas/UNetField"
+ },
+ {
+ "type": "null"
+ }
+ ],
+ "default": null,
+ "description": "UNet (scheduler, LoRAs)",
"field_kind": "input",
- "input": "any",
- "orig_default": 0,
+ "input": "connection",
+ "orig_default": null,
"orig_required": false,
- "title": "Start",
- "type": "integer"
+ "title": "UNet"
},
- "stop": {
- "default": 10,
- "description": "The stop of the range",
+ "vae": {
+ "anyOf": [
+ {
+ "$ref": "#/components/schemas/VAEField"
+ },
+ {
+ "type": "null"
+ }
+ ],
+ "default": null,
+ "description": "VAE model to load",
+ "field_kind": "input",
+ "input": "connection",
+ "orig_default": null,
+ "orig_required": false,
+ "title": "VAE"
+ },
+ "seamless_y": {
+ "default": true,
+ "description": "Specify whether Y axis is seamless",
"field_kind": "input",
"input": "any",
- "orig_default": 10,
+ "orig_default": true,
"orig_required": false,
- "title": "Stop",
- "type": "integer"
+ "title": "Seamless Y",
+ "type": "boolean"
},
- "step": {
- "default": 1,
- "description": "The step of the range",
+ "seamless_x": {
+ "default": true,
+ "description": "Specify whether X axis is seamless",
"field_kind": "input",
"input": "any",
- "orig_default": 1,
+ "orig_default": true,
"orig_required": false,
- "title": "Step",
- "type": "integer"
+ "title": "Seamless X",
+ "type": "boolean"
},
"type": {
- "const": "range",
- "default": "range",
+ "const": "seamless",
+ "default": "seamless",
"field_kind": "node_attribute",
"title": "type",
"type": "string"
}
},
"required": ["type", "id"],
- "tags": ["collection", "integer", "range"],
- "title": "Integer Range",
+ "tags": ["seamless", "model"],
+ "title": "Apply Seamless - SD1.5, SDXL",
"type": "object",
- "version": "1.0.0",
+ "version": "1.0.2",
"output": {
- "$ref": "#/components/schemas/IntegerCollectionOutput"
+ "$ref": "#/components/schemas/SeamlessModeOutput"
}
},
- "RangeOfSizeInvocation": {
- "category": "batch",
+ "SeamlessModeOutput": {
+ "class": "output",
+ "description": "Modified Seamless Model output",
+ "properties": {
+ "unet": {
+ "anyOf": [
+ {
+ "$ref": "#/components/schemas/UNetField"
+ },
+ {
+ "type": "null"
+ }
+ ],
+ "default": null,
+ "description": "UNet (scheduler, LoRAs)",
+ "field_kind": "output",
+ "title": "UNet",
+ "ui_hidden": false
+ },
+ "vae": {
+ "anyOf": [
+ {
+ "$ref": "#/components/schemas/VAEField"
+ },
+ {
+ "type": "null"
+ }
+ ],
+ "default": null,
+ "description": "VAE",
+ "field_kind": "output",
+ "title": "VAE",
+ "ui_hidden": false
+ },
+ "type": {
+ "const": "seamless_output",
+ "default": "seamless_output",
+ "field_kind": "node_attribute",
+ "title": "type",
+ "type": "string"
+ }
+ },
+ "required": ["output_meta", "unet", "vae", "type", "type"],
+ "title": "SeamlessModeOutput",
+ "type": "object"
+ },
+ "SeedreamImageGenerationInvocation": {
+ "category": "image",
"class": "invocation",
"classification": "stable",
- "description": "Creates a range from start to start + (size * step) incremented by step",
+ "description": "Generate images using a BytePlus Seedream model.",
"node_pack": "invokeai",
"properties": {
+ "board": {
+ "anyOf": [
+ {
+ "$ref": "#/components/schemas/BoardField"
+ },
+ {
+ "type": "null"
+ }
+ ],
+ "default": null,
+ "description": "The board to save the image to",
+ "field_kind": "internal",
+ "input": "direct",
+ "orig_required": false,
+ "ui_hidden": false
+ },
+ "metadata": {
+ "anyOf": [
+ {
+ "$ref": "#/components/schemas/MetadataField"
+ },
+ {
+ "type": "null"
+ }
+ ],
+ "default": null,
+ "description": "Optional metadata to be saved with the image",
+ "field_kind": "internal",
+ "input": "connection",
+ "orig_required": false,
+ "ui_hidden": false
+ },
"id": {
"description": "The id of this instance of an invocation. Must be unique among all instances of invocations.",
"field_kind": "node_attribute",
@@ -62644,57 +69396,38 @@
"title": "Use Cache",
"type": "boolean"
},
- "start": {
- "default": 0,
- "description": "The start of the range",
- "field_kind": "input",
- "input": "any",
- "orig_default": 0,
- "orig_required": false,
- "title": "Start",
- "type": "integer"
- },
- "size": {
- "default": 1,
- "description": "The number of values",
- "exclusiveMinimum": 0,
+ "model": {
+ "anyOf": [
+ {
+ "$ref": "#/components/schemas/ModelIdentifierField"
+ },
+ {
+ "type": "null"
+ }
+ ],
+ "default": null,
+ "description": "Main model (UNet, VAE, CLIP) to load",
"field_kind": "input",
"input": "any",
- "orig_default": 1,
- "orig_required": false,
- "title": "Size",
- "type": "integer"
+ "orig_required": true,
+ "ui_model_base": ["external"],
+ "ui_model_format": ["external_api"],
+ "ui_model_provider_id": ["seedream"],
+ "ui_model_type": ["external_image_generator"]
},
- "step": {
- "default": 1,
- "description": "The step of the range",
+ "mode": {
+ "default": "txt2img",
+ "description": "Generation mode.",
+ "enum": ["txt2img", "img2img", "inpaint"],
"field_kind": "input",
"input": "any",
- "orig_default": 1,
+ "orig_default": "txt2img",
"orig_required": false,
- "title": "Step",
- "type": "integer"
+ "title": "Mode",
+ "type": "string",
+ "ui_hidden": true
},
- "type": {
- "const": "range_of_size",
- "default": "range_of_size",
- "field_kind": "node_attribute",
- "title": "type",
- "type": "string"
- }
- },
- "required": ["type", "id"],
- "tags": ["collection", "integer", "size", "range"],
- "title": "Integer Range of Size",
- "type": "object",
- "version": "1.0.0",
- "output": {
- "$ref": "#/components/schemas/IntegerCollectionOutput"
- }
- },
- "RecallParameter": {
- "properties": {
- "positive_prompt": {
+ "prompt": {
"anyOf": [
{
"type": "string"
@@ -62703,22 +69436,64 @@
"type": "null"
}
],
- "title": "Positive Prompt",
- "description": "Positive prompt text"
+ "default": null,
+ "description": "Prompt",
+ "field_kind": "input",
+ "input": "any",
+ "orig_required": true,
+ "title": "Prompt"
},
- "negative_prompt": {
+ "seed": {
"anyOf": [
{
- "type": "string"
+ "type": "integer"
},
{
"type": "null"
}
],
- "title": "Negative Prompt",
- "description": "Negative prompt text"
+ "default": null,
+ "description": "Seed for random number generation",
+ "field_kind": "input",
+ "input": "any",
+ "orig_default": null,
+ "orig_required": false,
+ "title": "Seed"
},
- "model": {
+ "num_images": {
+ "default": 1,
+ "description": "Number of images to generate",
+ "exclusiveMinimum": 0,
+ "field_kind": "input",
+ "input": "any",
+ "orig_default": 1,
+ "orig_required": false,
+ "title": "Num Images",
+ "type": "integer"
+ },
+ "width": {
+ "default": 1024,
+ "description": "Width of output (px)",
+ "exclusiveMinimum": 0,
+ "field_kind": "input",
+ "input": "any",
+ "orig_default": 1024,
+ "orig_required": false,
+ "title": "Width",
+ "type": "integer"
+ },
+ "height": {
+ "default": 1024,
+ "description": "Height of output (px)",
+ "exclusiveMinimum": 0,
+ "field_kind": "input",
+ "input": "any",
+ "orig_default": 1024,
+ "orig_required": false,
+ "title": "Height",
+ "type": "integer"
+ },
+ "image_size": {
"anyOf": [
{
"type": "string"
@@ -62727,297 +69502,539 @@
"type": "null"
}
],
- "title": "Model",
- "description": "Main model name/identifier"
+ "default": null,
+ "description": "Image size preset (e.g. 1K, 2K, 4K)",
+ "field_kind": "input",
+ "input": "any",
+ "orig_default": null,
+ "orig_required": false,
+ "title": "Image Size"
},
- "refiner_model": {
+ "init_image": {
"anyOf": [
{
- "type": "string"
+ "$ref": "#/components/schemas/ImageField"
},
{
"type": "null"
}
],
- "title": "Refiner Model",
- "description": "Refiner model name/identifier"
+ "default": null,
+ "description": "Init image for img2img/inpaint",
+ "field_kind": "input",
+ "input": "any",
+ "orig_default": null,
+ "orig_required": false
},
- "vae_model": {
+ "mask_image": {
"anyOf": [
{
- "type": "string"
+ "$ref": "#/components/schemas/ImageField"
},
{
"type": "null"
}
],
- "title": "Vae Model",
- "description": "VAE model name/identifier"
+ "default": null,
+ "description": "Mask image for inpaint",
+ "field_kind": "input",
+ "input": "any",
+ "orig_default": null,
+ "orig_required": false,
+ "ui_hidden": true
},
- "scheduler": {
+ "reference_images": {
+ "default": [],
+ "description": "Reference images",
+ "field_kind": "input",
+ "input": "any",
+ "items": {
+ "$ref": "#/components/schemas/ImageField"
+ },
+ "orig_default": [],
+ "orig_required": false,
+ "title": "Reference Images",
+ "type": "array"
+ },
+ "watermark": {
+ "default": false,
+ "description": "Add watermark to generated images",
+ "field_kind": "input",
+ "input": "any",
+ "orig_default": false,
+ "orig_required": false,
+ "title": "Watermark",
+ "type": "boolean"
+ },
+ "optimize_prompt": {
+ "default": false,
+ "description": "Let the model optimize the prompt before generation",
+ "field_kind": "input",
+ "input": "any",
+ "orig_default": false,
+ "orig_required": false,
+ "title": "Optimize Prompt",
+ "type": "boolean"
+ },
+ "type": {
+ "const": "seedream_image_generation",
+ "default": "seedream_image_generation",
+ "field_kind": "node_attribute",
+ "title": "type",
+ "type": "string"
+ }
+ },
+ "required": ["type", "id"],
+ "tags": ["external", "generation", "seedream"],
+ "title": "Seedream Image Generation",
+ "type": "object",
+ "version": "1.1.0",
+ "output": {
+ "$ref": "#/components/schemas/ImageCollectionOutput"
+ }
+ },
+ "SegmentAnythingInvocation": {
+ "category": "segmentation",
+ "class": "invocation",
+ "classification": "stable",
+ "description": "Runs a Segment Anything Model (SAM or SAM2).",
+ "node_pack": "invokeai",
+ "properties": {
+ "id": {
+ "description": "The id of this instance of an invocation. Must be unique among all instances of invocations.",
+ "field_kind": "node_attribute",
+ "title": "Id",
+ "type": "string"
+ },
+ "is_intermediate": {
+ "default": false,
+ "description": "Whether or not this is an intermediate invocation.",
+ "field_kind": "node_attribute",
+ "input": "direct",
+ "orig_required": true,
+ "title": "Is Intermediate",
+ "type": "boolean",
+ "ui_hidden": false,
+ "ui_type": "IsIntermediate"
+ },
+ "use_cache": {
+ "default": true,
+ "description": "Whether or not to use the cache",
+ "field_kind": "node_attribute",
+ "title": "Use Cache",
+ "type": "boolean"
+ },
+ "model": {
"anyOf": [
{
+ "enum": [
+ "segment-anything-base",
+ "segment-anything-large",
+ "segment-anything-huge",
+ "segment-anything-2-tiny",
+ "segment-anything-2-small",
+ "segment-anything-2-base",
+ "segment-anything-2-large"
+ ],
"type": "string"
},
{
"type": "null"
}
],
- "title": "Scheduler",
- "description": "Scheduler name"
+ "default": null,
+ "description": "The Segment Anything model to use (SAM or SAM2).",
+ "field_kind": "input",
+ "input": "any",
+ "orig_required": true,
+ "title": "Model"
},
- "steps": {
+ "image": {
"anyOf": [
{
- "type": "integer",
- "minimum": 1.0
+ "$ref": "#/components/schemas/ImageField"
},
{
"type": "null"
}
],
- "title": "Steps",
- "description": "Number of generation steps"
+ "default": null,
+ "description": "The image to segment.",
+ "field_kind": "input",
+ "input": "any",
+ "orig_required": true
},
- "refiner_steps": {
+ "bounding_boxes": {
"anyOf": [
{
- "type": "integer",
- "minimum": 0.0
+ "items": {
+ "$ref": "#/components/schemas/BoundingBoxField"
+ },
+ "type": "array"
},
{
"type": "null"
}
],
- "title": "Refiner Steps",
- "description": "Number of refiner steps"
+ "default": null,
+ "description": "The bounding boxes to prompt the model with.",
+ "field_kind": "input",
+ "input": "any",
+ "orig_default": null,
+ "orig_required": false,
+ "title": "Bounding Boxes"
},
- "cfg_scale": {
+ "point_lists": {
"anyOf": [
{
- "type": "number"
+ "items": {
+ "$ref": "#/components/schemas/SAMPointsField"
+ },
+ "type": "array"
},
{
"type": "null"
}
],
- "title": "Cfg Scale",
- "description": "CFG scale for guidance"
+ "default": null,
+ "description": "The list of point lists to prompt the model with. Each list of points represents a single object.",
+ "field_kind": "input",
+ "input": "any",
+ "orig_default": null,
+ "orig_required": false,
+ "title": "Point Lists"
},
- "cfg_rescale_multiplier": {
- "anyOf": [
- {
- "type": "number"
- },
- {
- "type": "null"
- }
- ],
- "title": "Cfg Rescale Multiplier",
- "description": "CFG rescale multiplier"
+ "apply_polygon_refinement": {
+ "default": true,
+ "description": "Whether to apply polygon refinement to the masks. This will smooth the edges of the masks slightly and ensure that each mask consists of a single closed polygon (before merging).",
+ "field_kind": "input",
+ "input": "any",
+ "orig_default": true,
+ "orig_required": false,
+ "title": "Apply Polygon Refinement",
+ "type": "boolean"
},
- "refiner_cfg_scale": {
- "anyOf": [
- {
- "type": "number"
- },
- {
- "type": "null"
- }
- ],
- "title": "Refiner Cfg Scale",
- "description": "Refiner CFG scale"
+ "mask_filter": {
+ "default": "all",
+ "description": "The filtering to apply to the detected masks before merging them into a final output.",
+ "enum": ["all", "largest", "highest_box_score"],
+ "field_kind": "input",
+ "input": "any",
+ "orig_default": "all",
+ "orig_required": false,
+ "title": "Mask Filter",
+ "type": "string"
},
- "guidance": {
- "anyOf": [
- {
- "type": "number"
- },
- {
- "type": "null"
- }
- ],
- "title": "Guidance",
- "description": "Guidance scale"
+ "type": {
+ "const": "segment_anything",
+ "default": "segment_anything",
+ "field_kind": "node_attribute",
+ "title": "type",
+ "type": "string"
+ }
+ },
+ "required": ["type", "id"],
+ "tags": ["prompt", "segmentation", "sam", "sam2"],
+ "title": "Segment Anything",
+ "type": "object",
+ "version": "1.3.0",
+ "output": {
+ "$ref": "#/components/schemas/MaskOutput"
+ }
+ },
+ "SessionProcessorStatus": {
+ "properties": {
+ "is_started": {
+ "type": "boolean",
+ "title": "Is Started",
+ "description": "Whether the session processor is started"
},
- "width": {
+ "is_processing": {
+ "type": "boolean",
+ "title": "Is Processing",
+ "description": "Whether a session is being processed"
+ }
+ },
+ "type": "object",
+ "required": ["is_started", "is_processing"],
+ "title": "SessionProcessorStatus"
+ },
+ "SessionQueueAndProcessorStatus": {
+ "properties": {
+ "queue": {
+ "$ref": "#/components/schemas/SessionQueueStatus"
+ },
+ "processor": {
+ "$ref": "#/components/schemas/SessionProcessorStatus"
+ }
+ },
+ "type": "object",
+ "required": ["queue", "processor"],
+ "title": "SessionQueueAndProcessorStatus",
+ "description": "The overall status of session queue and processor"
+ },
+ "SessionQueueCountsByDestination": {
+ "properties": {
+ "queue_id": {
+ "type": "string",
+ "title": "Queue Id",
+ "description": "The ID of the queue"
+ },
+ "destination": {
+ "type": "string",
+ "title": "Destination",
+ "description": "The destination of queue items included in this status"
+ },
+ "pending": {
+ "type": "integer",
+ "title": "Pending",
+ "description": "Number of queue items with status 'pending' for the destination"
+ },
+ "in_progress": {
+ "type": "integer",
+ "title": "In Progress",
+ "description": "Number of queue items with status 'in_progress' for the destination"
+ },
+ "waiting": {
+ "type": "integer",
+ "title": "Waiting",
+ "description": "Number of queue items with status 'waiting' for the destination"
+ },
+ "completed": {
+ "type": "integer",
+ "title": "Completed",
+ "description": "Number of queue items with status 'complete' for the destination"
+ },
+ "failed": {
+ "type": "integer",
+ "title": "Failed",
+ "description": "Number of queue items with status 'error' for the destination"
+ },
+ "canceled": {
+ "type": "integer",
+ "title": "Canceled",
+ "description": "Number of queue items with status 'canceled' for the destination"
+ },
+ "total": {
+ "type": "integer",
+ "title": "Total",
+ "description": "Total number of queue items for the destination"
+ }
+ },
+ "type": "object",
+ "required": [
+ "queue_id",
+ "destination",
+ "pending",
+ "in_progress",
+ "waiting",
+ "completed",
+ "failed",
+ "canceled",
+ "total"
+ ],
+ "title": "SessionQueueCountsByDestination"
+ },
+ "SessionQueueItem": {
+ "properties": {
+ "item_id": {
+ "type": "integer",
+ "title": "Item Id",
+ "description": "The identifier of the session queue item"
+ },
+ "status": {
+ "type": "string",
+ "enum": ["pending", "in_progress", "waiting", "completed", "failed", "canceled"],
+ "title": "Status",
+ "description": "The status of this queue item",
+ "default": "pending"
+ },
+ "status_sequence": {
"anyOf": [
{
- "type": "integer",
- "minimum": 64.0
+ "type": "integer"
},
{
"type": "null"
}
],
- "title": "Width",
- "description": "Image width in pixels"
+ "title": "Status Sequence",
+ "description": "A monotonically increasing version for this queue item's visible status lifecycle"
+ },
+ "priority": {
+ "type": "integer",
+ "title": "Priority",
+ "description": "The priority of this queue item",
+ "default": 0
+ },
+ "batch_id": {
+ "type": "string",
+ "title": "Batch Id",
+ "description": "The ID of the batch associated with this queue item"
},
- "height": {
+ "origin": {
"anyOf": [
{
- "type": "integer",
- "minimum": 64.0
+ "type": "string"
},
{
"type": "null"
}
],
- "title": "Height",
- "description": "Image height in pixels"
+ "title": "Origin",
+ "description": "The origin of this queue item. This data is used by the frontend to determine how to handle results."
},
- "seed": {
+ "destination": {
"anyOf": [
{
- "type": "integer",
- "minimum": 0.0
+ "type": "string"
},
{
"type": "null"
}
],
- "title": "Seed",
- "description": "Random seed"
+ "title": "Destination",
+ "description": "The origin of this queue item. This data is used by the frontend to determine how to handle results"
},
- "denoise_strength": {
+ "session_id": {
+ "type": "string",
+ "title": "Session Id",
+ "description": "The ID of the session associated with this queue item. The session doesn't exist in graph_executions until the queue item is executed."
+ },
+ "error_type": {
"anyOf": [
{
- "type": "number",
- "maximum": 1.0,
- "minimum": 0.0
+ "type": "string"
},
{
"type": "null"
}
],
- "title": "Denoise Strength",
- "description": "Denoising strength"
+ "title": "Error Type",
+ "description": "The error type if this queue item errored"
},
- "refiner_denoise_start": {
+ "error_message": {
"anyOf": [
{
- "type": "number",
- "maximum": 1.0,
- "minimum": 0.0
+ "type": "string"
},
{
"type": "null"
}
],
- "title": "Refiner Denoise Start",
- "description": "Refiner denoising start"
+ "title": "Error Message",
+ "description": "The error message if this queue item errored"
},
- "clip_skip": {
+ "error_traceback": {
"anyOf": [
{
- "type": "integer",
- "minimum": 0.0
+ "type": "string"
},
{
"type": "null"
}
],
- "title": "Clip Skip",
- "description": "CLIP skip layers"
+ "title": "Error Traceback",
+ "description": "The error traceback if this queue item errored"
},
- "seamless_x": {
+ "created_at": {
"anyOf": [
{
- "type": "boolean"
+ "type": "string",
+ "format": "date-time"
},
{
- "type": "null"
+ "type": "string"
}
],
- "title": "Seamless X",
- "description": "Enable seamless X tiling"
+ "title": "Created At",
+ "description": "When this queue item was created"
},
- "seamless_y": {
+ "updated_at": {
"anyOf": [
{
- "type": "boolean"
+ "type": "string",
+ "format": "date-time"
},
{
- "type": "null"
+ "type": "string"
}
],
- "title": "Seamless Y",
- "description": "Enable seamless Y tiling"
+ "title": "Updated At",
+ "description": "When this queue item was updated"
},
- "refiner_positive_aesthetic_score": {
+ "started_at": {
"anyOf": [
{
- "type": "number"
+ "type": "string",
+ "format": "date-time"
},
{
- "type": "null"
- }
- ],
- "title": "Refiner Positive Aesthetic Score",
- "description": "Refiner positive aesthetic score"
- },
- "refiner_negative_aesthetic_score": {
- "anyOf": [
- {
- "type": "number"
+ "type": "string"
},
{
"type": "null"
}
],
- "title": "Refiner Negative Aesthetic Score",
- "description": "Refiner negative aesthetic score"
+ "title": "Started At",
+ "description": "When this queue item was started"
},
- "loras": {
+ "completed_at": {
"anyOf": [
{
- "items": {
- "$ref": "#/components/schemas/LoRARecallParameter"
- },
- "type": "array"
+ "type": "string",
+ "format": "date-time"
+ },
+ {
+ "type": "string"
},
{
"type": "null"
}
],
- "title": "Loras",
- "description": "List of LoRAs with their weights"
+ "title": "Completed At",
+ "description": "When this queue item was completed"
},
- "control_layers": {
+ "queue_id": {
+ "type": "string",
+ "title": "Queue Id",
+ "description": "The id of the queue with which this item is associated"
+ },
+ "user_id": {
+ "type": "string",
+ "title": "User Id",
+ "description": "The id of the user who created this queue item",
+ "default": "system"
+ },
+ "user_display_name": {
"anyOf": [
{
- "items": {
- "$ref": "#/components/schemas/ControlNetRecallParameter"
- },
- "type": "array"
+ "type": "string"
},
{
"type": "null"
}
],
- "title": "Control Layers",
- "description": "List of control adapters (ControlNet, T2I Adapter, Control LoRA) with their settings"
+ "title": "User Display Name",
+ "description": "The display name of the user who created this queue item, if available"
},
- "ip_adapters": {
+ "user_email": {
"anyOf": [
{
- "items": {
- "$ref": "#/components/schemas/IPAdapterRecallParameter"
- },
- "type": "array"
+ "type": "string"
},
{
"type": "null"
}
],
- "title": "Ip Adapters",
- "description": "List of IP Adapters with their settings"
+ "title": "User Email",
+ "description": "The email of the user who created this queue item, if available"
},
- "reference_images": {
+ "field_values": {
"anyOf": [
{
"items": {
- "$ref": "#/components/schemas/ReferenceImageRecallParameter"
+ "$ref": "#/components/schemas/NodeFieldValue"
},
"type": "array"
},
@@ -63025,108 +70042,34 @@
"type": "null"
}
],
- "title": "Reference Images",
- "description": "List of model-free reference images for architectures that consume reference images directly (FLUX.2 Klein, FLUX Kontext, Qwen Image Edit). The frontend picks the correct config type based on the currently-selected main model."
- }
- },
- "additionalProperties": false,
- "type": "object",
- "title": "RecallParameter",
- "description": "Request model for updating recallable parameters."
- },
- "RecallParametersUpdatedEvent": {
- "description": "Event model for recall_parameters_updated",
- "properties": {
- "timestamp": {
- "description": "The timestamp of the event",
- "title": "Timestamp",
- "type": "integer"
- },
- "queue_id": {
- "description": "The ID of the queue",
- "title": "Queue Id",
- "type": "string"
- },
- "user_id": {
- "description": "The ID of the user whose recall parameters were updated",
- "title": "User Id",
- "type": "string"
+ "title": "Field Values",
+ "description": "The field values that were used for this queue item"
},
- "parameters": {
- "additionalProperties": true,
- "description": "The recall parameters that were updated",
- "title": "Parameters",
- "type": "object"
- }
- },
- "required": ["timestamp", "queue_id", "user_id", "parameters"],
- "title": "RecallParametersUpdatedEvent",
- "type": "object"
- },
- "RectangleMaskInvocation": {
- "category": "mask",
- "class": "invocation",
- "classification": "stable",
- "description": "Create a rectangular mask.",
- "node_pack": "invokeai",
- "properties": {
- "metadata": {
+ "retried_from_item_id": {
"anyOf": [
{
- "$ref": "#/components/schemas/MetadataField"
+ "type": "integer"
},
{
"type": "null"
}
],
- "default": null,
- "description": "Optional metadata to be saved with the image",
- "field_kind": "internal",
- "input": "connection",
- "orig_required": false,
- "ui_hidden": false
- },
- "id": {
- "description": "The id of this instance of an invocation. Must be unique among all instances of invocations.",
- "field_kind": "node_attribute",
- "title": "Id",
- "type": "string"
- },
- "is_intermediate": {
- "default": false,
- "description": "Whether or not this is an intermediate invocation.",
- "field_kind": "node_attribute",
- "input": "direct",
- "orig_required": true,
- "title": "Is Intermediate",
- "type": "boolean",
- "ui_hidden": false,
- "ui_type": "IsIntermediate"
- },
- "use_cache": {
- "default": true,
- "description": "Whether or not to use the cache",
- "field_kind": "node_attribute",
- "title": "Use Cache",
- "type": "boolean"
+ "title": "Retried From Item Id",
+ "description": "The item_id of the queue item that this item was retried from"
},
- "width": {
+ "workflow_call_id": {
"anyOf": [
{
- "type": "integer"
+ "type": "string"
},
{
"type": "null"
}
],
- "default": null,
- "description": "The width of the entire mask.",
- "field_kind": "input",
- "input": "any",
- "orig_required": true,
- "title": "Width"
+ "title": "Workflow Call Id",
+ "description": "The active workflow-call relationship id when this queue item is a child execution."
},
- "height": {
+ "parent_item_id": {
"anyOf": [
{
"type": "integer"
@@ -63135,30 +70078,22 @@
"type": "null"
}
],
- "default": null,
- "description": "The height of the entire mask.",
- "field_kind": "input",
- "input": "any",
- "orig_required": true,
- "title": "Height"
+ "title": "Parent Item Id",
+ "description": "The parent queue item id when this queue item is a child workflow execution."
},
- "x_left": {
+ "parent_session_id": {
"anyOf": [
{
- "type": "integer"
+ "type": "string"
},
{
"type": "null"
}
],
- "default": null,
- "description": "The left x-coordinate of the rectangular masked region (inclusive).",
- "field_kind": "input",
- "input": "any",
- "orig_required": true,
- "title": "X Left"
+ "title": "Parent Session Id",
+ "description": "The parent session id when this queue item is a child workflow execution."
},
- "y_top": {
+ "root_item_id": {
"anyOf": [
{
"type": "integer"
@@ -63167,14 +70102,10 @@
"type": "null"
}
],
- "default": null,
- "description": "The top y-coordinate of the rectangular masked region (inclusive).",
- "field_kind": "input",
- "input": "any",
- "orig_required": true,
- "title": "Y Top"
+ "title": "Root Item Id",
+ "description": "The root queue item id for this workflow call chain, if any."
},
- "rectangle_width": {
+ "workflow_call_depth": {
"anyOf": [
{
"type": "integer"
@@ -63183,75 +70114,49 @@
"type": "null"
}
],
- "default": null,
- "description": "The width of the rectangular masked region.",
- "field_kind": "input",
- "input": "any",
- "orig_required": true,
- "title": "Rectangle Width"
+ "title": "Workflow Call Depth",
+ "description": "The 1-based workflow-call depth for this queue item when it is a child execution."
},
- "rectangle_height": {
+ "session": {
+ "$ref": "#/components/schemas/GraphExecutionState",
+ "description": "The fully-populated session to be executed"
+ },
+ "workflow": {
"anyOf": [
{
- "type": "integer"
+ "$ref": "#/components/schemas/WorkflowWithoutID"
},
{
"type": "null"
}
],
- "default": null,
- "description": "The height of the rectangular masked region.",
- "field_kind": "input",
- "input": "any",
- "orig_required": true,
- "title": "Rectangle Height"
- },
- "type": {
- "const": "rectangle_mask",
- "default": "rectangle_mask",
- "field_kind": "node_attribute",
- "title": "type",
- "type": "string"
- }
- },
- "required": ["type", "id"],
- "tags": ["conditioning"],
- "title": "Create Rectangle Mask",
- "type": "object",
- "version": "1.0.1",
- "output": {
- "$ref": "#/components/schemas/MaskOutput"
- }
- },
- "ReferenceImageRecallParameter": {
- "properties": {
- "image_name": {
- "type": "string",
- "title": "Image Name",
- "description": "The filename of the reference image in outputs/images"
+ "description": "The workflow associated with this queue item"
}
},
"type": "object",
- "required": ["image_name"],
- "title": "ReferenceImageRecallParameter",
- "description": "Global reference-image configuration for recall.\n\nUsed for reference images that feed directly into the main model rather\nthan through a separate IP-Adapter / ControlNet model \u2014 for example\nFLUX.2 Klein, FLUX Kontext, and Qwen Image Edit. The receiving frontend\npicks the correct config type (``flux2_reference_image`` /\n``qwen_image_reference_image`` / ``flux_kontext_reference_image``) based\non the currently-selected main model."
+ "required": [
+ "item_id",
+ "status",
+ "batch_id",
+ "queue_id",
+ "session_id",
+ "session",
+ "priority",
+ "session_id",
+ "created_at",
+ "updated_at"
+ ],
+ "title": "SessionQueueItem",
+ "description": "Session queue item without the full graph. Used for serialization."
},
- "RemoteModelFile": {
+ "SessionQueueStatus": {
"properties": {
- "url": {
- "type": "string",
- "minLength": 1,
- "format": "uri",
- "title": "Url",
- "description": "The url to download this model file"
- },
- "path": {
+ "queue_id": {
"type": "string",
- "format": "path",
- "title": "Path",
- "description": "The path to the file, relative to the model root"
+ "title": "Queue Id",
+ "description": "The ID of the queue"
},
- "size": {
+ "item_id": {
"anyOf": [
{
"type": "integer"
@@ -63260,11 +70165,10 @@
"type": "null"
}
],
- "title": "Size",
- "description": "The size of this file, in bytes",
- "default": 0
+ "title": "Item Id",
+ "description": "The current queue item id"
},
- "sha256": {
+ "batch_id": {
"anyOf": [
{
"type": "string"
@@ -63273,189 +70177,184 @@
"type": "null"
}
],
- "title": "Sha256",
- "description": "SHA256 hash of this model (not always available)"
- }
- },
- "type": "object",
- "required": ["url", "path"],
- "title": "RemoteModelFile",
- "description": "Information about a downloadable file that forms part of a model."
- },
- "RemoveImagesFromBoardResult": {
- "properties": {
- "affected_boards": {
- "items": {
- "type": "string"
- },
- "type": "array",
- "title": "Affected Boards",
- "description": "The ids of boards affected by the delete operation"
+ "title": "Batch Id",
+ "description": "The current queue item's batch id"
},
- "removed_images": {
- "items": {
- "type": "string"
- },
- "type": "array",
- "title": "Removed Images",
- "description": "The image names that were removed from their board"
- }
- },
- "type": "object",
- "required": ["affected_boards", "removed_images"],
- "title": "RemoveImagesFromBoardResult"
- },
- "ResizeLatentsInvocation": {
- "category": "latents",
- "class": "invocation",
- "classification": "stable",
- "description": "Resizes latents to explicit width/height (in pixels). Provided dimensions are floor-divided by 8.",
- "node_pack": "invokeai",
- "properties": {
- "id": {
- "description": "The id of this instance of an invocation. Must be unique among all instances of invocations.",
- "field_kind": "node_attribute",
- "title": "Id",
- "type": "string"
+ "session_id": {
+ "anyOf": [
+ {
+ "type": "string"
+ },
+ {
+ "type": "null"
+ }
+ ],
+ "title": "Session Id",
+ "description": "The current queue item's session id"
},
- "is_intermediate": {
- "default": false,
- "description": "Whether or not this is an intermediate invocation.",
- "field_kind": "node_attribute",
- "input": "direct",
- "orig_required": true,
- "title": "Is Intermediate",
- "type": "boolean",
- "ui_hidden": false,
- "ui_type": "IsIntermediate"
+ "pending": {
+ "type": "integer",
+ "title": "Pending",
+ "description": "Number of queue items with status 'pending'"
},
- "use_cache": {
- "default": true,
- "description": "Whether or not to use the cache",
- "field_kind": "node_attribute",
- "title": "Use Cache",
- "type": "boolean"
+ "in_progress": {
+ "type": "integer",
+ "title": "In Progress",
+ "description": "Number of queue items with status 'in_progress'"
},
- "latents": {
+ "waiting": {
+ "type": "integer",
+ "title": "Waiting",
+ "description": "Number of queue items with status 'waiting'"
+ },
+ "completed": {
+ "type": "integer",
+ "title": "Completed",
+ "description": "Number of queue items with status 'complete'"
+ },
+ "failed": {
+ "type": "integer",
+ "title": "Failed",
+ "description": "Number of queue items with status 'error'"
+ },
+ "canceled": {
+ "type": "integer",
+ "title": "Canceled",
+ "description": "Number of queue items with status 'canceled'"
+ },
+ "total": {
+ "type": "integer",
+ "title": "Total",
+ "description": "Total number of queue items"
+ },
+ "user_pending": {
"anyOf": [
{
- "$ref": "#/components/schemas/LatentsField"
+ "type": "integer"
},
{
"type": "null"
}
],
- "default": null,
- "description": "Latents tensor",
- "field_kind": "input",
- "input": "connection",
- "orig_required": true
+ "title": "User Pending",
+ "description": "Number of the requesting user's queue items with status 'pending' (None for admins/global callers)"
},
- "width": {
+ "user_in_progress": {
"anyOf": [
{
- "minimum": 64,
- "multipleOf": 8,
"type": "integer"
},
{
"type": "null"
}
],
- "default": null,
- "description": "Width of output (px)",
- "field_kind": "input",
- "input": "any",
- "orig_required": true,
- "title": "Width"
+ "title": "User In Progress",
+ "description": "Number of the requesting user's queue items with status 'in_progress' (None for admins/global callers)"
+ }
+ },
+ "type": "object",
+ "required": [
+ "queue_id",
+ "item_id",
+ "batch_id",
+ "session_id",
+ "pending",
+ "in_progress",
+ "waiting",
+ "completed",
+ "failed",
+ "canceled",
+ "total"
+ ],
+ "title": "SessionQueueStatus"
+ },
+ "SetupRequest": {
+ "properties": {
+ "email": {
+ "type": "string",
+ "title": "Email",
+ "description": "Admin email address"
},
- "height": {
+ "display_name": {
"anyOf": [
{
- "minimum": 64,
- "multipleOf": 8,
- "type": "integer"
+ "type": "string"
},
{
"type": "null"
}
],
- "default": null,
- "description": "Width of output (px)",
- "field_kind": "input",
- "input": "any",
- "orig_required": true,
- "title": "Height"
- },
- "mode": {
- "default": "bilinear",
- "description": "Interpolation mode",
- "enum": ["nearest", "linear", "bilinear", "bicubic", "trilinear", "area", "nearest-exact"],
- "field_kind": "input",
- "input": "any",
- "orig_default": "bilinear",
- "orig_required": false,
- "title": "Mode",
- "type": "string"
- },
- "antialias": {
- "default": false,
- "description": "Whether or not to apply antialiasing (bilinear or bicubic only)",
- "field_kind": "input",
- "input": "any",
- "orig_default": false,
- "orig_required": false,
- "title": "Antialias",
- "type": "boolean"
+ "title": "Display Name",
+ "description": "Admin display name"
},
- "type": {
- "const": "lresize",
- "default": "lresize",
- "field_kind": "node_attribute",
- "title": "type",
- "type": "string"
+ "password": {
+ "type": "string",
+ "title": "Password",
+ "description": "Admin password"
}
},
- "required": ["type", "id"],
- "tags": ["latents", "resize"],
- "title": "Resize Latents",
"type": "object",
- "version": "1.0.2",
- "output": {
- "$ref": "#/components/schemas/LatentsOutput"
- }
+ "required": ["email", "password"],
+ "title": "SetupRequest",
+ "description": "Request body for initial admin setup."
},
- "ResourceOrigin": {
- "type": "string",
- "enum": ["internal", "external"],
- "title": "ResourceOrigin",
- "description": "The origin of a resource (eg image).\n\n- INTERNAL: The resource was created by the application.\n- EXTERNAL: The resource was not created by the application.\nThis may be a user-initiated upload, or an internal application upload (eg Canvas init image)."
+ "SetupResponse": {
+ "properties": {
+ "success": {
+ "type": "boolean",
+ "title": "Success",
+ "description": "Whether setup was successful"
+ },
+ "user": {
+ "$ref": "#/components/schemas/UserDTO",
+ "description": "Created admin user information"
+ }
+ },
+ "type": "object",
+ "required": ["success", "user"],
+ "title": "SetupResponse",
+ "description": "Response from successful admin setup."
},
- "RetryItemsResult": {
+ "SetupStatusResponse": {
"properties": {
- "queue_id": {
- "type": "string",
- "title": "Queue Id",
- "description": "The ID of the queue"
+ "setup_required": {
+ "type": "boolean",
+ "title": "Setup Required",
+ "description": "Whether initial setup is required"
},
- "retried_item_ids": {
- "items": {
- "type": "integer"
- },
- "type": "array",
- "title": "Retried Item Ids",
- "description": "The IDs of the queue items that were retried"
+ "multiuser_enabled": {
+ "type": "boolean",
+ "title": "Multiuser Enabled",
+ "description": "Whether multiuser mode is enabled"
+ },
+ "strict_password_checking": {
+ "type": "boolean",
+ "title": "Strict Password Checking",
+ "description": "Whether strict password requirements are enforced"
+ },
+ "admin_email": {
+ "anyOf": [
+ {
+ "type": "string"
+ },
+ {
+ "type": "null"
+ }
+ ],
+ "title": "Admin Email",
+ "description": "Email of the first active admin user, if any"
}
},
"type": "object",
- "required": ["queue_id", "retried_item_ids"],
- "title": "RetryItemsResult"
+ "required": ["setup_required", "multiuser_enabled", "strict_password_checking"],
+ "title": "SetupStatusResponse",
+ "description": "Response for setup status check."
},
- "RoundInvocation": {
- "category": "math",
+ "ShowImageInvocation": {
+ "category": "image",
"class": "invocation",
"classification": "stable",
- "description": "Rounds a float to a specified number of decimal places.",
+ "description": "Displays a provided image using the OS image viewer, and passes it forward in the pipeline.",
"node_pack": "invokeai",
"properties": {
"id": {
@@ -63482,386 +70381,184 @@
"title": "Use Cache",
"type": "boolean"
},
- "value": {
- "default": 0,
- "description": "The float value",
- "field_kind": "input",
- "input": "any",
- "orig_default": 0,
- "orig_required": false,
- "title": "Value",
- "type": "number"
- },
- "decimals": {
- "default": 0,
- "description": "The number of decimal places",
+ "image": {
+ "anyOf": [
+ {
+ "$ref": "#/components/schemas/ImageField"
+ },
+ {
+ "type": "null"
+ }
+ ],
+ "default": null,
+ "description": "The image to show",
"field_kind": "input",
"input": "any",
- "orig_default": 0,
- "orig_required": false,
- "title": "Decimals",
- "type": "integer"
+ "orig_required": true
},
"type": {
- "const": "round_float",
- "default": "round_float",
+ "const": "show_image",
+ "default": "show_image",
"field_kind": "node_attribute",
"title": "type",
"type": "string"
}
},
"required": ["type", "id"],
- "tags": ["math", "round"],
- "title": "Round Float",
+ "tags": ["image"],
+ "title": "Show Image",
"type": "object",
"version": "1.0.1",
"output": {
- "$ref": "#/components/schemas/FloatOutput"
+ "$ref": "#/components/schemas/ImageOutput"
}
},
- "SAMPoint": {
+ "SigLIP_Diffusers_Config": {
"properties": {
- "x": {
- "description": "The x-coordinate of the point",
- "title": "X",
- "type": "integer"
+ "key": {
+ "type": "string",
+ "title": "Key",
+ "description": "A unique key for this model."
},
- "y": {
- "description": "The y-coordinate of the point",
- "title": "Y",
- "type": "integer"
+ "hash": {
+ "type": "string",
+ "title": "Hash",
+ "description": "The hash of the model file(s)."
},
- "label": {
- "$ref": "#/components/schemas/SAMPointLabel",
- "description": "The label of the point"
- }
- },
- "required": ["x", "y", "label"],
- "title": "SAMPoint",
- "type": "object"
- },
- "SAMPointLabel": {
- "enum": [-1, 0, 1],
- "title": "SAMPointLabel",
- "type": "integer"
- },
- "SAMPointsField": {
- "properties": {
- "points": {
- "description": "The points of the object",
- "items": {
- "$ref": "#/components/schemas/SAMPoint"
- },
- "minItems": 1,
- "title": "Points",
- "type": "array"
- }
- },
- "required": ["points"],
- "title": "SAMPointsField",
- "type": "object"
- },
- "SD3ConditioningField": {
- "description": "A conditioning tensor primitive value",
- "properties": {
- "conditioning_name": {
- "description": "The name of conditioning tensor",
- "title": "Conditioning Name",
- "type": "string"
- }
- },
- "required": ["conditioning_name"],
- "title": "SD3ConditioningField",
- "type": "object"
- },
- "SD3ConditioningOutput": {
- "class": "output",
- "description": "Base class for nodes that output a single SD3 conditioning tensor",
- "properties": {
- "conditioning": {
- "$ref": "#/components/schemas/SD3ConditioningField",
- "description": "Conditioning tensor",
- "field_kind": "output",
- "ui_hidden": false
+ "path": {
+ "type": "string",
+ "title": "Path",
+ "description": "Path to the model on the filesystem. Relative paths are relative to the Invoke root directory."
},
- "type": {
- "const": "sd3_conditioning_output",
- "default": "sd3_conditioning_output",
- "field_kind": "node_attribute",
- "title": "type",
- "type": "string"
- }
- },
- "required": ["output_meta", "conditioning", "type", "type"],
- "title": "SD3ConditioningOutput",
- "type": "object"
- },
- "SD3DenoiseInvocation": {
- "category": "latents",
- "class": "invocation",
- "classification": "stable",
- "description": "Run denoising process with a SD3 model.",
- "node_pack": "invokeai",
- "properties": {
- "board": {
- "anyOf": [
- {
- "$ref": "#/components/schemas/BoardField"
- },
- {
- "type": "null"
- }
- ],
- "default": null,
- "description": "The board to save the image to",
- "field_kind": "internal",
- "input": "direct",
- "orig_required": false,
- "ui_hidden": false
+ "file_size": {
+ "type": "integer",
+ "title": "File Size",
+ "description": "The size of the model in bytes."
},
- "metadata": {
+ "name": {
+ "type": "string",
+ "title": "Name",
+ "description": "Name of the model."
+ },
+ "description": {
"anyOf": [
{
- "$ref": "#/components/schemas/MetadataField"
+ "type": "string"
},
{
"type": "null"
}
],
- "default": null,
- "description": "Optional metadata to be saved with the image",
- "field_kind": "internal",
- "input": "connection",
- "orig_required": false,
- "ui_hidden": false
- },
- "id": {
- "description": "The id of this instance of an invocation. Must be unique among all instances of invocations.",
- "field_kind": "node_attribute",
- "title": "Id",
- "type": "string"
+ "title": "Description",
+ "description": "Model description"
},
- "is_intermediate": {
- "default": false,
- "description": "Whether or not this is an intermediate invocation.",
- "field_kind": "node_attribute",
- "input": "direct",
- "orig_required": true,
- "title": "Is Intermediate",
- "type": "boolean",
- "ui_hidden": false,
- "ui_type": "IsIntermediate"
+ "source": {
+ "type": "string",
+ "title": "Source",
+ "description": "The original source of the model (path, URL or repo_id)."
},
- "use_cache": {
- "default": true,
- "description": "Whether or not to use the cache",
- "field_kind": "node_attribute",
- "title": "Use Cache",
- "type": "boolean"
+ "source_type": {
+ "$ref": "#/components/schemas/ModelSourceType",
+ "description": "The type of source"
},
- "latents": {
+ "source_api_response": {
"anyOf": [
{
- "$ref": "#/components/schemas/LatentsField"
+ "type": "string"
},
{
"type": "null"
}
],
- "default": null,
- "description": "Latents tensor",
- "field_kind": "input",
- "input": "connection",
- "orig_default": null,
- "orig_required": false
+ "title": "Source Api Response",
+ "description": "The original API response from the source, as stringified JSON."
},
- "noise": {
+ "source_url": {
"anyOf": [
{
- "$ref": "#/components/schemas/LatentsField"
+ "type": "string"
},
{
"type": "null"
}
],
- "default": null,
- "description": "Noise tensor",
- "field_kind": "input",
- "input": "connection",
- "orig_default": null,
- "orig_required": false
+ "title": "Source Url",
+ "description": "Optional URL for the model (e.g. download page or model page)."
},
- "denoise_mask": {
+ "cover_image": {
"anyOf": [
{
- "$ref": "#/components/schemas/DenoiseMaskField"
+ "type": "string"
},
{
"type": "null"
}
],
- "default": null,
- "description": "A mask of the region to apply the denoising process to. Values of 0.0 represent the regions to be fully denoised, and 1.0 represent the regions to be preserved.",
- "field_kind": "input",
- "input": "connection",
- "orig_default": null,
- "orig_required": false
+ "title": "Cover Image",
+ "description": "Url for image to preview model"
},
- "denoising_start": {
- "default": 0.0,
- "description": "When to start denoising, expressed a percentage of total steps",
- "field_kind": "input",
- "input": "any",
- "maximum": 1,
- "minimum": 0,
- "orig_default": 0.0,
- "orig_required": false,
- "title": "Denoising Start",
- "type": "number"
+ "format": {
+ "type": "string",
+ "const": "diffusers",
+ "title": "Format",
+ "default": "diffusers"
},
- "denoising_end": {
- "default": 1.0,
- "description": "When to stop denoising, expressed a percentage of total steps",
- "field_kind": "input",
- "input": "any",
- "maximum": 1,
- "minimum": 0,
- "orig_default": 1.0,
- "orig_required": false,
- "title": "Denoising End",
- "type": "number"
+ "repo_variant": {
+ "$ref": "#/components/schemas/ModelRepoVariant",
+ "default": ""
},
- "transformer": {
- "anyOf": [
- {
- "$ref": "#/components/schemas/TransformerField"
- },
- {
- "type": "null"
- }
- ],
- "default": null,
- "description": "SD3 model (MMDiTX) to load",
- "field_kind": "input",
- "input": "connection",
- "orig_required": true,
- "title": "Transformer"
+ "type": {
+ "type": "string",
+ "const": "siglip",
+ "title": "Type",
+ "default": "siglip"
},
- "positive_conditioning": {
- "anyOf": [
- {
- "$ref": "#/components/schemas/SD3ConditioningField"
- },
- {
- "type": "null"
- }
- ],
- "default": null,
- "description": "Positive conditioning tensor",
- "field_kind": "input",
- "input": "connection",
- "orig_required": true
+ "base": {
+ "type": "string",
+ "const": "any",
+ "title": "Base",
+ "default": "any"
},
- "negative_conditioning": {
+ "cpu_only": {
"anyOf": [
{
- "$ref": "#/components/schemas/SD3ConditioningField"
+ "type": "boolean"
},
{
"type": "null"
}
],
- "default": null,
- "description": "Negative conditioning tensor",
- "field_kind": "input",
- "input": "connection",
- "orig_required": true
- },
- "cfg_scale": {
- "anyOf": [
- {
- "type": "number"
- },
- {
- "items": {
- "type": "number"
- },
- "type": "array"
- }
- ],
- "default": 3.5,
- "description": "Classifier-Free Guidance scale",
- "field_kind": "input",
- "input": "any",
- "orig_default": 3.5,
- "orig_required": false,
- "title": "CFG Scale"
- },
- "width": {
- "default": 1024,
- "description": "Width of the generated image.",
- "field_kind": "input",
- "input": "any",
- "multipleOf": 16,
- "orig_default": 1024,
- "orig_required": false,
- "title": "Width",
- "type": "integer"
- },
- "height": {
- "default": 1024,
- "description": "Height of the generated image.",
- "field_kind": "input",
- "input": "any",
- "multipleOf": 16,
- "orig_default": 1024,
- "orig_required": false,
- "title": "Height",
- "type": "integer"
- },
- "steps": {
- "default": 10,
- "description": "Number of steps to run",
- "exclusiveMinimum": 0,
- "field_kind": "input",
- "input": "any",
- "orig_default": 10,
- "orig_required": false,
- "title": "Steps",
- "type": "integer"
- },
- "seed": {
- "default": 0,
- "description": "Randomness seed for reproducibility.",
- "field_kind": "input",
- "input": "any",
- "orig_default": 0,
- "orig_required": false,
- "title": "Seed",
- "type": "integer"
- },
- "type": {
- "const": "sd3_denoise",
- "default": "sd3_denoise",
- "field_kind": "node_attribute",
- "title": "type",
- "type": "string"
+ "title": "Cpu Only",
+ "description": "Whether this model should run on CPU only"
}
},
- "required": ["type", "id"],
- "tags": ["image", "sd3"],
- "title": "Denoise - SD3",
"type": "object",
- "version": "1.2.0",
- "output": {
- "$ref": "#/components/schemas/LatentsOutput"
- }
+ "required": [
+ "key",
+ "hash",
+ "path",
+ "file_size",
+ "name",
+ "description",
+ "source",
+ "source_type",
+ "source_api_response",
+ "source_url",
+ "cover_image",
+ "format",
+ "repo_variant",
+ "type",
+ "base",
+ "cpu_only"
+ ],
+ "title": "SigLIP_Diffusers_Config",
+ "description": "Model config for SigLIP."
},
- "SD3ImageToLatentsInvocation": {
- "category": "latents",
+ "SpandrelImageToImageAutoscaleInvocation": {
+ "category": "upscale",
"class": "invocation",
"classification": "stable",
- "description": "Generates latents from an image.",
+ "description": "Run any spandrel image-to-image model (https://github.com/chaiNNer-org/spandrel) until the target scale is reached.",
"node_pack": "invokeai",
"properties": {
"board": {
@@ -63930,48 +70627,82 @@
}
],
"default": null,
- "description": "The image to encode",
+ "description": "The input image",
"field_kind": "input",
"input": "any",
"orig_required": true
},
- "vae": {
+ "image_to_image_model": {
"anyOf": [
{
- "$ref": "#/components/schemas/VAEField"
+ "$ref": "#/components/schemas/ModelIdentifierField"
},
{
"type": "null"
}
],
"default": null,
- "description": "VAE",
+ "description": "Image-to-Image model",
"field_kind": "input",
- "input": "connection",
- "orig_required": true
+ "input": "any",
+ "orig_required": true,
+ "title": "Image-to-Image Model",
+ "ui_model_type": ["spandrel_image_to_image"]
+ },
+ "tile_size": {
+ "default": 512,
+ "description": "The tile size for tiled image-to-image. Set to 0 to disable tiling.",
+ "field_kind": "input",
+ "input": "any",
+ "orig_default": 512,
+ "orig_required": false,
+ "title": "Tile Size",
+ "type": "integer"
},
"type": {
- "const": "sd3_i2l",
- "default": "sd3_i2l",
+ "const": "spandrel_image_to_image_autoscale",
+ "default": "spandrel_image_to_image_autoscale",
"field_kind": "node_attribute",
"title": "type",
"type": "string"
+ },
+ "scale": {
+ "default": 4.0,
+ "description": "The final scale of the output image. If the model does not upscale the image, this will be ignored.",
+ "exclusiveMinimum": 0.0,
+ "field_kind": "input",
+ "input": "any",
+ "maximum": 16.0,
+ "orig_default": 4.0,
+ "orig_required": false,
+ "title": "Scale",
+ "type": "number"
+ },
+ "fit_to_multiple_of_8": {
+ "default": false,
+ "description": "If true, the output image will be resized to the nearest multiple of 8 in both dimensions.",
+ "field_kind": "input",
+ "input": "any",
+ "orig_default": false,
+ "orig_required": false,
+ "title": "Fit To Multiple Of 8",
+ "type": "boolean"
}
},
"required": ["type", "id"],
- "tags": ["image", "latents", "vae", "i2l", "sd3"],
- "title": "Image to Latents - SD3",
+ "tags": ["upscale"],
+ "title": "Image-to-Image (Autoscale)",
"type": "object",
- "version": "1.0.1",
+ "version": "1.0.0",
"output": {
- "$ref": "#/components/schemas/LatentsOutput"
+ "$ref": "#/components/schemas/ImageOutput"
}
},
- "SD3LatentsToImageInvocation": {
- "category": "latents",
+ "SpandrelImageToImageInvocation": {
+ "category": "upscale",
"class": "invocation",
"classification": "stable",
- "description": "Generates an image from latents.",
+ "description": "Run any spandrel image-to-image model (https://github.com/chaiNNer-org/spandrel).",
"node_pack": "invokeai",
"properties": {
"board": {
@@ -64030,548 +70761,548 @@
"title": "Use Cache",
"type": "boolean"
},
- "latents": {
+ "image": {
"anyOf": [
{
- "$ref": "#/components/schemas/LatentsField"
+ "$ref": "#/components/schemas/ImageField"
},
{
"type": "null"
}
],
"default": null,
- "description": "Latents tensor",
+ "description": "The input image",
"field_kind": "input",
- "input": "connection",
+ "input": "any",
"orig_required": true
},
- "vae": {
+ "image_to_image_model": {
"anyOf": [
{
- "$ref": "#/components/schemas/VAEField"
+ "$ref": "#/components/schemas/ModelIdentifierField"
},
{
"type": "null"
}
],
"default": null,
- "description": "VAE",
+ "description": "Image-to-Image model",
"field_kind": "input",
- "input": "connection",
- "orig_required": true
+ "input": "any",
+ "orig_required": true,
+ "title": "Image-to-Image Model",
+ "ui_model_type": ["spandrel_image_to_image"]
+ },
+ "tile_size": {
+ "default": 512,
+ "description": "The tile size for tiled image-to-image. Set to 0 to disable tiling.",
+ "field_kind": "input",
+ "input": "any",
+ "orig_default": 512,
+ "orig_required": false,
+ "title": "Tile Size",
+ "type": "integer"
},
"type": {
- "const": "sd3_l2i",
- "default": "sd3_l2i",
+ "const": "spandrel_image_to_image",
+ "default": "spandrel_image_to_image",
"field_kind": "node_attribute",
"title": "type",
"type": "string"
}
},
"required": ["type", "id"],
- "tags": ["latents", "image", "vae", "l2i", "sd3"],
- "title": "Latents to Image - SD3",
+ "tags": ["upscale"],
+ "title": "Image-to-Image",
"type": "object",
- "version": "1.3.2",
+ "version": "1.3.0",
"output": {
"$ref": "#/components/schemas/ImageOutput"
}
},
- "SDXLCompelPromptInvocation": {
- "category": "prompt",
- "class": "invocation",
- "classification": "stable",
- "description": "Parse prompt using compel package to conditioning.",
- "node_pack": "invokeai",
+ "Spandrel_Checkpoint_Config": {
"properties": {
- "id": {
- "description": "The id of this instance of an invocation. Must be unique among all instances of invocations.",
- "field_kind": "node_attribute",
- "title": "Id",
- "type": "string"
- },
- "is_intermediate": {
- "default": false,
- "description": "Whether or not this is an intermediate invocation.",
- "field_kind": "node_attribute",
- "input": "direct",
- "orig_required": true,
- "title": "Is Intermediate",
- "type": "boolean",
- "ui_hidden": false,
- "ui_type": "IsIntermediate"
- },
- "use_cache": {
- "default": true,
- "description": "Whether or not to use the cache",
- "field_kind": "node_attribute",
- "title": "Use Cache",
- "type": "boolean"
- },
- "prompt": {
- "default": "",
- "description": "Prompt to be parsed by Compel to create a conditioning tensor",
- "field_kind": "input",
- "input": "any",
- "orig_default": "",
- "orig_required": false,
- "title": "Prompt",
+ "key": {
"type": "string",
- "ui_component": "textarea"
+ "title": "Key",
+ "description": "A unique key for this model."
},
- "style": {
- "default": "",
- "description": "Prompt to be parsed by Compel to create a conditioning tensor",
- "field_kind": "input",
- "input": "any",
- "orig_default": "",
- "orig_required": false,
- "title": "Style",
+ "hash": {
"type": "string",
- "ui_component": "textarea"
+ "title": "Hash",
+ "description": "The hash of the model file(s)."
},
- "original_width": {
- "default": 1024,
- "description": "",
- "field_kind": "input",
- "input": "any",
- "orig_default": 1024,
- "orig_required": false,
- "title": "Original Width",
- "type": "integer"
+ "path": {
+ "type": "string",
+ "title": "Path",
+ "description": "Path to the model on the filesystem. Relative paths are relative to the Invoke root directory."
},
- "original_height": {
- "default": 1024,
- "description": "",
- "field_kind": "input",
- "input": "any",
- "orig_default": 1024,
- "orig_required": false,
- "title": "Original Height",
- "type": "integer"
+ "file_size": {
+ "type": "integer",
+ "title": "File Size",
+ "description": "The size of the model in bytes."
},
- "crop_top": {
- "default": 0,
- "description": "",
- "field_kind": "input",
- "input": "any",
- "orig_default": 0,
- "orig_required": false,
- "title": "Crop Top",
- "type": "integer"
+ "name": {
+ "type": "string",
+ "title": "Name",
+ "description": "Name of the model."
},
- "crop_left": {
- "default": 0,
- "description": "",
- "field_kind": "input",
- "input": "any",
- "orig_default": 0,
- "orig_required": false,
- "title": "Crop Left",
- "type": "integer"
+ "description": {
+ "anyOf": [
+ {
+ "type": "string"
+ },
+ {
+ "type": "null"
+ }
+ ],
+ "title": "Description",
+ "description": "Model description"
},
- "target_width": {
- "default": 1024,
- "description": "",
- "field_kind": "input",
- "input": "any",
- "orig_default": 1024,
- "orig_required": false,
- "title": "Target Width",
- "type": "integer"
+ "source": {
+ "type": "string",
+ "title": "Source",
+ "description": "The original source of the model (path, URL or repo_id)."
},
- "target_height": {
- "default": 1024,
- "description": "",
- "field_kind": "input",
- "input": "any",
- "orig_default": 1024,
- "orig_required": false,
- "title": "Target Height",
- "type": "integer"
+ "source_type": {
+ "$ref": "#/components/schemas/ModelSourceType",
+ "description": "The type of source"
},
- "clip": {
+ "source_api_response": {
"anyOf": [
{
- "$ref": "#/components/schemas/CLIPField"
+ "type": "string"
},
{
"type": "null"
}
],
- "default": null,
- "description": "CLIP (tokenizer, text encoder, LoRAs) and skipped layer count",
- "field_kind": "input",
- "input": "connection",
- "orig_required": true,
- "title": "CLIP 1"
+ "title": "Source Api Response",
+ "description": "The original API response from the source, as stringified JSON."
},
- "clip2": {
+ "source_url": {
"anyOf": [
{
- "$ref": "#/components/schemas/CLIPField"
+ "type": "string"
},
{
"type": "null"
}
],
- "default": null,
- "description": "CLIP (tokenizer, text encoder, LoRAs) and skipped layer count",
- "field_kind": "input",
- "input": "connection",
- "orig_required": true,
- "title": "CLIP 2"
+ "title": "Source Url",
+ "description": "Optional URL for the model (e.g. download page or model page)."
},
- "mask": {
+ "cover_image": {
"anyOf": [
{
- "$ref": "#/components/schemas/TensorField"
+ "type": "string"
},
{
"type": "null"
}
],
- "default": null,
- "description": "A mask defining the region that this conditioning prompt applies to.",
- "field_kind": "input",
- "input": "any",
- "orig_default": null,
- "orig_required": false
+ "title": "Cover Image",
+ "description": "Url for image to preview model"
+ },
+ "base": {
+ "type": "string",
+ "const": "any",
+ "title": "Base",
+ "default": "any"
},
"type": {
- "const": "sdxl_compel_prompt",
- "default": "sdxl_compel_prompt",
- "field_kind": "node_attribute",
- "title": "type",
- "type": "string"
+ "type": "string",
+ "const": "spandrel_image_to_image",
+ "title": "Type",
+ "default": "spandrel_image_to_image"
+ },
+ "format": {
+ "type": "string",
+ "const": "checkpoint",
+ "title": "Format",
+ "default": "checkpoint"
}
},
- "required": ["type", "id"],
- "tags": ["sdxl", "compel", "prompt"],
- "title": "Prompt - SDXL",
"type": "object",
- "version": "1.2.1",
- "output": {
- "$ref": "#/components/schemas/ConditioningOutput"
- }
+ "required": [
+ "key",
+ "hash",
+ "path",
+ "file_size",
+ "name",
+ "description",
+ "source",
+ "source_type",
+ "source_api_response",
+ "source_url",
+ "cover_image",
+ "base",
+ "type",
+ "format"
+ ],
+ "title": "Spandrel_Checkpoint_Config",
+ "description": "Model config for Spandrel Image to Image models."
},
- "SDXLLoRACollectionLoader": {
- "category": "model",
- "class": "invocation",
- "classification": "stable",
- "description": "Applies a collection of SDXL LoRAs to the provided UNet and CLIP models.",
- "node_pack": "invokeai",
+ "StarredImagesResult": {
"properties": {
- "id": {
- "description": "The id of this instance of an invocation. Must be unique among all instances of invocations.",
- "field_kind": "node_attribute",
- "title": "Id",
- "type": "string"
+ "affected_boards": {
+ "items": {
+ "type": "string"
+ },
+ "type": "array",
+ "title": "Affected Boards",
+ "description": "The ids of boards affected by the delete operation"
},
- "is_intermediate": {
- "default": false,
- "description": "Whether or not this is an intermediate invocation.",
- "field_kind": "node_attribute",
- "input": "direct",
- "orig_required": true,
- "title": "Is Intermediate",
- "type": "boolean",
- "ui_hidden": false,
- "ui_type": "IsIntermediate"
+ "starred_images": {
+ "items": {
+ "type": "string"
+ },
+ "type": "array",
+ "title": "Starred Images",
+ "description": "The names of the images that were starred"
+ }
+ },
+ "type": "object",
+ "required": ["affected_boards", "starred_images"],
+ "title": "StarredImagesResult"
+ },
+ "StarredVideosResult": {
+ "properties": {
+ "affected_boards": {
+ "items": {
+ "type": "string"
+ },
+ "type": "array",
+ "title": "Affected Boards",
+ "description": "The ids of boards affected by the operation"
},
- "use_cache": {
- "default": true,
- "description": "Whether or not to use the cache",
- "field_kind": "node_attribute",
- "title": "Use Cache",
- "type": "boolean"
+ "starred_videos": {
+ "items": {
+ "type": "string"
+ },
+ "type": "array",
+ "title": "Starred Videos",
+ "description": "The names of the videos that were starred"
+ }
+ },
+ "type": "object",
+ "required": ["affected_boards", "starred_videos"],
+ "title": "StarredVideosResult"
+ },
+ "StarterModel": {
+ "properties": {
+ "description": {
+ "type": "string",
+ "title": "Description"
},
- "loras": {
+ "source": {
+ "type": "string",
+ "title": "Source"
+ },
+ "name": {
+ "type": "string",
+ "title": "Name"
+ },
+ "base": {
+ "$ref": "#/components/schemas/BaseModelType"
+ },
+ "type": {
+ "$ref": "#/components/schemas/ModelType"
+ },
+ "format": {
"anyOf": [
{
- "$ref": "#/components/schemas/LoRAField"
+ "$ref": "#/components/schemas/ModelFormat"
},
{
- "items": {
- "$ref": "#/components/schemas/LoRAField"
- },
- "type": "array"
+ "type": "null"
+ }
+ ]
+ },
+ "variant": {
+ "anyOf": [
+ {
+ "$ref": "#/components/schemas/ModelVariantType"
+ },
+ {
+ "$ref": "#/components/schemas/ClipVariantType"
+ },
+ {
+ "$ref": "#/components/schemas/FluxVariantType"
+ },
+ {
+ "$ref": "#/components/schemas/Flux2VariantType"
+ },
+ {
+ "$ref": "#/components/schemas/ZImageVariantType"
+ },
+ {
+ "$ref": "#/components/schemas/QwenImageVariantType"
+ },
+ {
+ "$ref": "#/components/schemas/WanVariantType"
+ },
+ {
+ "$ref": "#/components/schemas/WanLoRAVariantType"
+ },
+ {
+ "$ref": "#/components/schemas/Qwen3VariantType"
},
{
"type": "null"
}
],
- "default": null,
- "description": "LoRA models and weights. May be a single LoRA or collection.",
- "field_kind": "input",
- "input": "any",
- "orig_default": null,
- "orig_required": false,
- "title": "LoRAs",
- "ui_model_base": ["sdxl"],
- "ui_model_type": ["lora"]
+ "title": "Variant"
},
- "unet": {
+ "is_installed": {
+ "type": "boolean",
+ "title": "Is Installed",
+ "default": false
+ },
+ "capabilities": {
"anyOf": [
{
- "$ref": "#/components/schemas/UNetField"
+ "$ref": "#/components/schemas/ExternalModelCapabilities"
},
{
"type": "null"
}
- ],
- "default": null,
- "description": "UNet (scheduler, LoRAs)",
- "field_kind": "input",
- "input": "connection",
- "orig_default": null,
- "orig_required": false,
- "title": "UNet"
+ ]
+ },
+ "default_settings": {
+ "anyOf": [
+ {
+ "$ref": "#/components/schemas/ExternalApiModelDefaultSettings"
+ },
+ {
+ "type": "null"
+ }
+ ]
},
- "clip": {
+ "panel_schema": {
"anyOf": [
{
- "$ref": "#/components/schemas/CLIPField"
+ "$ref": "#/components/schemas/ExternalModelPanelSchema"
},
{
"type": "null"
}
- ],
- "default": null,
- "description": "CLIP (tokenizer, text encoder, LoRAs) and skipped layer count",
- "field_kind": "input",
- "input": "connection",
- "orig_default": null,
- "orig_required": false,
- "title": "CLIP"
+ ]
},
- "clip2": {
+ "previous_names": {
+ "items": {
+ "type": "string"
+ },
+ "type": "array",
+ "title": "Previous Names",
+ "default": []
+ },
+ "dependencies": {
"anyOf": [
{
- "$ref": "#/components/schemas/CLIPField"
+ "items": {
+ "$ref": "#/components/schemas/StarterModelWithoutDependencies"
+ },
+ "type": "array"
},
{
"type": "null"
}
],
- "default": null,
- "description": "CLIP (tokenizer, text encoder, LoRAs) and skipped layer count",
- "field_kind": "input",
- "input": "connection",
- "orig_default": null,
- "orig_required": false,
- "title": "CLIP 2"
+ "title": "Dependencies"
+ }
+ },
+ "type": "object",
+ "required": ["description", "source", "name", "base", "type"],
+ "title": "StarterModel"
+ },
+ "StarterModelBundle": {
+ "properties": {
+ "name": {
+ "type": "string",
+ "title": "Name"
},
- "type": {
- "const": "sdxl_lora_collection_loader",
- "default": "sdxl_lora_collection_loader",
- "field_kind": "node_attribute",
- "title": "type",
- "type": "string"
+ "models": {
+ "items": {
+ "$ref": "#/components/schemas/StarterModel"
+ },
+ "type": "array",
+ "title": "Models"
}
},
- "required": ["type", "id"],
- "tags": ["model"],
- "title": "Apply LoRA Collection - SDXL",
"type": "object",
- "version": "1.1.3",
- "output": {
- "$ref": "#/components/schemas/SDXLLoRALoaderOutput"
- }
+ "required": ["name", "models"],
+ "title": "StarterModelBundle"
},
- "SDXLLoRALoaderInvocation": {
- "category": "model",
- "class": "invocation",
- "classification": "stable",
- "description": "Apply selected lora to unet and text_encoder.",
- "node_pack": "invokeai",
+ "StarterModelResponse": {
"properties": {
- "id": {
- "description": "The id of this instance of an invocation. Must be unique among all instances of invocations.",
- "field_kind": "node_attribute",
- "title": "Id",
- "type": "string"
+ "starter_models": {
+ "items": {
+ "$ref": "#/components/schemas/StarterModel"
+ },
+ "type": "array",
+ "title": "Starter Models"
},
- "is_intermediate": {
- "default": false,
- "description": "Whether or not this is an intermediate invocation.",
- "field_kind": "node_attribute",
- "input": "direct",
- "orig_required": true,
- "title": "Is Intermediate",
- "type": "boolean",
- "ui_hidden": false,
- "ui_type": "IsIntermediate"
+ "starter_bundles": {
+ "additionalProperties": {
+ "$ref": "#/components/schemas/StarterModelBundle"
+ },
+ "type": "object",
+ "title": "Starter Bundles"
+ }
+ },
+ "type": "object",
+ "required": ["starter_models", "starter_bundles"],
+ "title": "StarterModelResponse"
+ },
+ "StarterModelWithoutDependencies": {
+ "properties": {
+ "description": {
+ "type": "string",
+ "title": "Description"
},
- "use_cache": {
- "default": true,
- "description": "Whether or not to use the cache",
- "field_kind": "node_attribute",
- "title": "Use Cache",
- "type": "boolean"
+ "source": {
+ "type": "string",
+ "title": "Source"
},
- "lora": {
+ "name": {
+ "type": "string",
+ "title": "Name"
+ },
+ "base": {
+ "$ref": "#/components/schemas/BaseModelType"
+ },
+ "type": {
+ "$ref": "#/components/schemas/ModelType"
+ },
+ "format": {
"anyOf": [
{
- "$ref": "#/components/schemas/ModelIdentifierField"
+ "$ref": "#/components/schemas/ModelFormat"
},
{
"type": "null"
}
- ],
- "default": null,
- "description": "LoRA model to load",
- "field_kind": "input",
- "input": "any",
- "orig_required": true,
- "title": "LoRA",
- "ui_model_base": ["sdxl"],
- "ui_model_type": ["lora"]
- },
- "weight": {
- "default": 0.75,
- "description": "The weight at which the LoRA is applied to each model",
- "field_kind": "input",
- "input": "any",
- "orig_default": 0.75,
- "orig_required": false,
- "title": "Weight",
- "type": "number"
+ ]
},
- "unet": {
+ "variant": {
"anyOf": [
{
- "$ref": "#/components/schemas/UNetField"
+ "$ref": "#/components/schemas/ModelVariantType"
},
{
- "type": "null"
- }
- ],
- "default": null,
- "description": "UNet (scheduler, LoRAs)",
- "field_kind": "input",
- "input": "connection",
- "orig_default": null,
- "orig_required": false,
- "title": "UNet"
- },
- "clip": {
- "anyOf": [
+ "$ref": "#/components/schemas/ClipVariantType"
+ },
{
- "$ref": "#/components/schemas/CLIPField"
+ "$ref": "#/components/schemas/FluxVariantType"
},
{
- "type": "null"
- }
- ],
- "default": null,
- "description": "CLIP (tokenizer, text encoder, LoRAs) and skipped layer count",
- "field_kind": "input",
- "input": "connection",
- "orig_default": null,
- "orig_required": false,
- "title": "CLIP 1"
- },
- "clip2": {
- "anyOf": [
+ "$ref": "#/components/schemas/Flux2VariantType"
+ },
{
- "$ref": "#/components/schemas/CLIPField"
+ "$ref": "#/components/schemas/ZImageVariantType"
+ },
+ {
+ "$ref": "#/components/schemas/QwenImageVariantType"
+ },
+ {
+ "$ref": "#/components/schemas/WanVariantType"
+ },
+ {
+ "$ref": "#/components/schemas/WanLoRAVariantType"
+ },
+ {
+ "$ref": "#/components/schemas/Qwen3VariantType"
},
{
"type": "null"
}
],
- "default": null,
- "description": "CLIP (tokenizer, text encoder, LoRAs) and skipped layer count",
- "field_kind": "input",
- "input": "connection",
- "orig_default": null,
- "orig_required": false,
- "title": "CLIP 2"
+ "title": "Variant"
},
- "type": {
- "const": "sdxl_lora_loader",
- "default": "sdxl_lora_loader",
- "field_kind": "node_attribute",
- "title": "type",
- "type": "string"
- }
- },
- "required": ["type", "id"],
- "tags": ["lora", "model"],
- "title": "Apply LoRA - SDXL",
- "type": "object",
- "version": "1.0.5",
- "output": {
- "$ref": "#/components/schemas/SDXLLoRALoaderOutput"
- }
- },
- "SDXLLoRALoaderOutput": {
- "class": "output",
- "description": "SDXL LoRA Loader Output",
- "properties": {
- "unet": {
+ "is_installed": {
+ "type": "boolean",
+ "title": "Is Installed",
+ "default": false
+ },
+ "capabilities": {
"anyOf": [
{
- "$ref": "#/components/schemas/UNetField"
+ "$ref": "#/components/schemas/ExternalModelCapabilities"
},
{
"type": "null"
}
- ],
- "default": null,
- "description": "UNet (scheduler, LoRAs)",
- "field_kind": "output",
- "title": "UNet",
- "ui_hidden": false
+ ]
},
- "clip": {
+ "default_settings": {
"anyOf": [
{
- "$ref": "#/components/schemas/CLIPField"
+ "$ref": "#/components/schemas/ExternalApiModelDefaultSettings"
},
{
"type": "null"
}
- ],
- "default": null,
- "description": "CLIP (tokenizer, text encoder, LoRAs) and skipped layer count",
- "field_kind": "output",
- "title": "CLIP 1",
- "ui_hidden": false
+ ]
},
- "clip2": {
+ "panel_schema": {
"anyOf": [
{
- "$ref": "#/components/schemas/CLIPField"
+ "$ref": "#/components/schemas/ExternalModelPanelSchema"
},
{
"type": "null"
}
- ],
- "default": null,
- "description": "CLIP (tokenizer, text encoder, LoRAs) and skipped layer count",
+ ]
+ },
+ "previous_names": {
+ "items": {
+ "type": "string"
+ },
+ "type": "array",
+ "title": "Previous Names",
+ "default": []
+ }
+ },
+ "type": "object",
+ "required": ["description", "source", "name", "base", "type"],
+ "title": "StarterModelWithoutDependencies"
+ },
+ "String2Output": {
+ "class": "output",
+ "description": "Base class for invocations that output two strings",
+ "properties": {
+ "string_1": {
+ "description": "string 1",
"field_kind": "output",
- "title": "CLIP 2",
+ "title": "String 1",
+ "type": "string",
+ "ui_hidden": false
+ },
+ "string_2": {
+ "description": "string 2",
+ "field_kind": "output",
+ "title": "String 2",
+ "type": "string",
"ui_hidden": false
},
"type": {
- "const": "sdxl_lora_loader_output",
- "default": "sdxl_lora_loader_output",
+ "const": "string_2_output",
+ "default": "string_2_output",
"field_kind": "node_attribute",
"title": "type",
"type": "string"
}
},
- "required": ["output_meta", "unet", "clip", "clip2", "type", "type"],
- "title": "SDXLLoRALoaderOutput",
+ "required": ["output_meta", "string_1", "string_2", "type", "type"],
+ "title": "String2Output",
"type": "object"
},
- "SDXLModelLoaderInvocation": {
- "category": "model",
+ "StringBatchInvocation": {
+ "category": "batch",
"class": "invocation",
- "classification": "stable",
- "description": "Loads an sdxl base model, outputting its submodels.",
+ "classification": "special",
+ "description": "Create a batched generation, where the workflow is executed once for each string in the batch.",
"node_pack": "invokeai",
"properties": {
"id": {
@@ -64598,89 +71329,59 @@
"title": "Use Cache",
"type": "boolean"
},
- "model": {
+ "batch_group_id": {
+ "default": "None",
+ "description": "The ID of this batch node's group. If provided, all batch nodes in with the same ID will be 'zipped' before execution, and all nodes' collections must be of the same size.",
+ "enum": ["None", "Group 1", "Group 2", "Group 3", "Group 4", "Group 5"],
+ "field_kind": "input",
+ "input": "direct",
+ "orig_default": "None",
+ "orig_required": false,
+ "title": "Batch Group",
+ "type": "string"
+ },
+ "strings": {
"anyOf": [
{
- "$ref": "#/components/schemas/ModelIdentifierField"
+ "items": {
+ "type": "string"
+ },
+ "minItems": 1,
+ "type": "array"
},
{
"type": "null"
}
],
"default": null,
- "description": "SDXL Main model (UNet, VAE, CLIP1, CLIP2) to load",
+ "description": "The strings to batch over",
"field_kind": "input",
"input": "any",
"orig_required": true,
- "ui_model_base": ["sdxl"],
- "ui_model_type": ["main"]
+ "title": "Strings"
},
"type": {
- "const": "sdxl_model_loader",
- "default": "sdxl_model_loader",
+ "const": "string_batch",
+ "default": "string_batch",
"field_kind": "node_attribute",
"title": "type",
"type": "string"
}
},
"required": ["type", "id"],
- "tags": ["model", "sdxl"],
- "title": "Main Model - SDXL",
+ "tags": ["primitives", "string", "batch", "special"],
+ "title": "String Batch",
"type": "object",
- "version": "1.0.4",
+ "version": "1.0.0",
"output": {
- "$ref": "#/components/schemas/SDXLModelLoaderOutput"
+ "$ref": "#/components/schemas/StringOutput"
}
},
- "SDXLModelLoaderOutput": {
- "class": "output",
- "description": "SDXL base model loader output",
- "properties": {
- "unet": {
- "$ref": "#/components/schemas/UNetField",
- "description": "UNet (scheduler, LoRAs)",
- "field_kind": "output",
- "title": "UNet",
- "ui_hidden": false
- },
- "clip": {
- "$ref": "#/components/schemas/CLIPField",
- "description": "CLIP (tokenizer, text encoder, LoRAs) and skipped layer count",
- "field_kind": "output",
- "title": "CLIP 1",
- "ui_hidden": false
- },
- "clip2": {
- "$ref": "#/components/schemas/CLIPField",
- "description": "CLIP (tokenizer, text encoder, LoRAs) and skipped layer count",
- "field_kind": "output",
- "title": "CLIP 2",
- "ui_hidden": false
- },
- "vae": {
- "$ref": "#/components/schemas/VAEField",
- "description": "VAE",
- "field_kind": "output",
- "title": "VAE",
- "ui_hidden": false
- },
- "type": {
- "const": "sdxl_model_loader_output",
- "default": "sdxl_model_loader_output",
- "field_kind": "node_attribute",
- "title": "type",
- "type": "string"
- }
- },
- "required": ["output_meta", "unet", "clip", "clip2", "vae", "type", "type"],
- "title": "SDXLModelLoaderOutput",
- "type": "object"
- },
- "SDXLRefinerCompelPromptInvocation": {
- "category": "prompt",
+ "StringCollectionInvocation": {
+ "category": "primitives",
"class": "invocation",
"classification": "stable",
- "description": "Parse prompt using compel package to conditioning.",
+ "description": "A collection of string primitive values",
"node_pack": "invokeai",
"properties": {
"id": {
@@ -64707,104 +71408,67 @@
"title": "Use Cache",
"type": "boolean"
},
- "style": {
- "default": "",
- "description": "Prompt to be parsed by Compel to create a conditioning tensor",
- "field_kind": "input",
- "input": "any",
- "orig_default": "",
- "orig_required": false,
- "title": "Style",
- "type": "string",
- "ui_component": "textarea"
- },
- "original_width": {
- "default": 1024,
- "description": "",
- "field_kind": "input",
- "input": "any",
- "orig_default": 1024,
- "orig_required": false,
- "title": "Original Width",
- "type": "integer"
- },
- "original_height": {
- "default": 1024,
- "description": "",
- "field_kind": "input",
- "input": "any",
- "orig_default": 1024,
- "orig_required": false,
- "title": "Original Height",
- "type": "integer"
- },
- "crop_top": {
- "default": 0,
- "description": "",
- "field_kind": "input",
- "input": "any",
- "orig_default": 0,
- "orig_required": false,
- "title": "Crop Top",
- "type": "integer"
- },
- "crop_left": {
- "default": 0,
- "description": "",
- "field_kind": "input",
- "input": "any",
- "orig_default": 0,
- "orig_required": false,
- "title": "Crop Left",
- "type": "integer"
- },
- "aesthetic_score": {
- "default": 6.0,
- "description": "The aesthetic score to apply to the conditioning tensor",
+ "collection": {
+ "default": [],
+ "description": "The collection of string values",
"field_kind": "input",
"input": "any",
- "orig_default": 6.0,
+ "items": {
+ "type": "string"
+ },
+ "orig_default": [],
"orig_required": false,
- "title": "Aesthetic Score",
- "type": "number"
+ "title": "Collection",
+ "type": "array"
},
- "clip2": {
- "anyOf": [
- {
- "$ref": "#/components/schemas/CLIPField"
- },
- {
- "type": "null"
- }
- ],
- "default": null,
- "description": "CLIP (tokenizer, text encoder, LoRAs) and skipped layer count",
- "field_kind": "input",
- "input": "connection",
- "orig_required": true
+ "type": {
+ "const": "string_collection",
+ "default": "string_collection",
+ "field_kind": "node_attribute",
+ "title": "type",
+ "type": "string"
+ }
+ },
+ "required": ["type", "id"],
+ "tags": ["primitives", "string", "collection"],
+ "title": "String Collection Primitive",
+ "type": "object",
+ "version": "1.0.2",
+ "output": {
+ "$ref": "#/components/schemas/StringCollectionOutput"
+ }
+ },
+ "StringCollectionOutput": {
+ "class": "output",
+ "description": "Base class for nodes that output a collection of strings",
+ "properties": {
+ "collection": {
+ "description": "The output strings",
+ "field_kind": "output",
+ "items": {
+ "type": "string"
+ },
+ "title": "Collection",
+ "type": "array",
+ "ui_hidden": false
},
"type": {
- "const": "sdxl_refiner_compel_prompt",
- "default": "sdxl_refiner_compel_prompt",
+ "const": "string_collection_output",
+ "default": "string_collection_output",
"field_kind": "node_attribute",
"title": "type",
"type": "string"
}
},
- "required": ["type", "id"],
- "tags": ["sdxl", "compel", "prompt"],
- "title": "Prompt - SDXL Refiner",
- "type": "object",
- "version": "1.1.2",
- "output": {
- "$ref": "#/components/schemas/ConditioningOutput"
- }
+ "required": ["output_meta", "collection", "type", "type"],
+ "title": "StringCollectionOutput",
+ "type": "object"
},
- "SDXLRefinerModelLoaderInvocation": {
- "category": "model",
+ "StringGenerator": {
+ "category": "batch",
"class": "invocation",
- "classification": "stable",
- "description": "Loads an sdxl refiner model, outputting its submodels.",
+ "classification": "special",
+ "description": "Generated a range of strings for use in a batched generation",
"node_pack": "invokeai",
"properties": {
"id": {
@@ -64831,121 +71495,69 @@
"title": "Use Cache",
"type": "boolean"
},
- "model": {
- "anyOf": [
- {
- "$ref": "#/components/schemas/ModelIdentifierField"
- },
- {
- "type": "null"
- }
- ],
- "default": null,
- "description": "SDXL Refiner Main Modde (UNet, VAE, CLIP2) to load",
+ "generator": {
+ "$ref": "#/components/schemas/StringGeneratorField",
+ "description": "The string generator.",
"field_kind": "input",
- "input": "any",
+ "input": "direct",
"orig_required": true,
- "ui_model_base": ["sdxl-refiner"],
- "ui_model_type": ["main"]
+ "title": "Generator Type"
},
"type": {
- "const": "sdxl_refiner_model_loader",
- "default": "sdxl_refiner_model_loader",
+ "const": "string_generator",
+ "default": "string_generator",
"field_kind": "node_attribute",
"title": "type",
"type": "string"
}
},
- "required": ["type", "id"],
- "tags": ["model", "sdxl", "refiner"],
- "title": "Refiner Model - SDXL",
+ "required": ["generator", "type", "id"],
+ "tags": ["primitives", "string", "number", "batch", "special"],
+ "title": "String Generator",
"type": "object",
- "version": "1.0.4",
+ "version": "1.0.0",
"output": {
- "$ref": "#/components/schemas/SDXLRefinerModelLoaderOutput"
+ "$ref": "#/components/schemas/StringGeneratorOutput"
}
},
- "SDXLRefinerModelLoaderOutput": {
+ "StringGeneratorField": {
+ "properties": {},
+ "title": "StringGeneratorField",
+ "type": "object"
+ },
+ "StringGeneratorOutput": {
"class": "output",
- "description": "SDXL refiner model loader output",
+ "description": "Base class for nodes that output a collection of strings",
"properties": {
- "unet": {
- "$ref": "#/components/schemas/UNetField",
- "description": "UNet (scheduler, LoRAs)",
- "field_kind": "output",
- "title": "UNet",
- "ui_hidden": false
- },
- "clip2": {
- "$ref": "#/components/schemas/CLIPField",
- "description": "CLIP (tokenizer, text encoder, LoRAs) and skipped layer count",
- "field_kind": "output",
- "title": "CLIP 2",
- "ui_hidden": false
- },
- "vae": {
- "$ref": "#/components/schemas/VAEField",
- "description": "VAE",
+ "strings": {
+ "description": "The generated strings",
"field_kind": "output",
- "title": "VAE",
+ "items": {
+ "type": "string"
+ },
+ "title": "Strings",
+ "type": "array",
"ui_hidden": false
},
"type": {
- "const": "sdxl_refiner_model_loader_output",
- "default": "sdxl_refiner_model_loader_output",
+ "const": "string_generator_output",
+ "default": "string_generator_output",
"field_kind": "node_attribute",
"title": "type",
"type": "string"
}
},
- "required": ["output_meta", "unet", "clip2", "vae", "type", "type"],
- "title": "SDXLRefinerModelLoaderOutput",
+ "required": ["output_meta", "strings", "type", "type"],
+ "title": "StringGeneratorOutput",
"type": "object"
},
- "SQLiteDirection": {
- "type": "string",
- "enum": ["ASC", "DESC"],
- "title": "SQLiteDirection"
- },
- "SaveImageInvocation": {
- "category": "image",
+ "StringInvocation": {
+ "category": "primitives",
"class": "invocation",
"classification": "stable",
- "description": "Saves an image. Unlike an image primitive, this invocation stores a copy of the image.",
+ "description": "A string primitive value",
"node_pack": "invokeai",
"properties": {
- "board": {
- "anyOf": [
- {
- "$ref": "#/components/schemas/BoardField"
- },
- {
- "type": "null"
- }
- ],
- "default": null,
- "description": "The board to save the image to",
- "field_kind": "internal",
- "input": "direct",
- "orig_required": false,
- "ui_hidden": false
- },
- "metadata": {
- "anyOf": [
- {
- "$ref": "#/components/schemas/MetadataField"
- },
- {
- "type": "null"
- }
- ],
- "default": null,
- "description": "Optional metadata to be saved with the image",
- "field_kind": "internal",
- "input": "connection",
- "orig_required": false,
- "ui_hidden": false
- },
"id": {
"description": "The id of this instance of an invocation. Must be unique among all instances of invocations.",
"field_kind": "node_attribute",
@@ -64964,83 +71576,47 @@
"ui_type": "IsIntermediate"
},
"use_cache": {
- "default": false,
+ "default": true,
"description": "Whether or not to use the cache",
"field_kind": "node_attribute",
"title": "Use Cache",
"type": "boolean"
},
- "image": {
- "anyOf": [
- {
- "$ref": "#/components/schemas/ImageField"
- },
- {
- "type": "null"
- }
- ],
- "default": null,
- "description": "The image to process",
+ "value": {
+ "default": "",
+ "description": "The string value",
"field_kind": "input",
"input": "any",
- "orig_required": true
+ "orig_default": "",
+ "orig_required": false,
+ "title": "Value",
+ "type": "string",
+ "ui_component": "textarea"
},
"type": {
- "const": "save_image",
- "default": "save_image",
+ "const": "string",
+ "default": "string",
"field_kind": "node_attribute",
"title": "type",
"type": "string"
}
},
"required": ["type", "id"],
- "tags": ["primitives", "image"],
- "title": "Save Image",
+ "tags": ["primitives", "string"],
+ "title": "String Primitive",
"type": "object",
- "version": "1.2.2",
+ "version": "1.0.1",
"output": {
- "$ref": "#/components/schemas/ImageOutput"
+ "$ref": "#/components/schemas/StringOutput"
}
},
- "SaveImageToFileInvocation": {
- "category": "image",
+ "StringJoinInvocation": {
+ "category": "strings",
"class": "invocation",
"classification": "stable",
- "description": "Saves an image to the gallery (like the standard Save Image node) AND additionally exports a copy\nto the filesystem with a custom filename.\n\nFilename pattern: {prefix}{uuid}{suffix}.{file_format}\n- The UUID is the same UUID used for the gallery entry, so the exported file can be matched to the gallery item.\n- The gallery entry itself always uses the plain UUID (prefix/suffix apply only to the exported file on disk).\n- Board and Metadata inputs behave exactly like the standard Save Image node.\n- The export target is restricted to (subfolders of) the InvokeAI outputs folder \u2014 absolute paths are rejected.\n\nExample: prefix=\"hero_\", suffix=\"_final\", file_format=\"png\" \u2192 \"hero__final.png\"",
+ "description": "Joins string left to string right",
"node_pack": "invokeai",
"properties": {
- "board": {
- "anyOf": [
- {
- "$ref": "#/components/schemas/BoardField"
- },
- {
- "type": "null"
- }
- ],
- "default": null,
- "description": "The board to save the image to",
- "field_kind": "internal",
- "input": "direct",
- "orig_required": false,
- "ui_hidden": false
- },
- "metadata": {
- "anyOf": [
- {
- "$ref": "#/components/schemas/MetadataField"
- },
- {
- "type": "null"
- }
- ],
- "default": null,
- "description": "Optional metadata to be saved with the image",
- "field_kind": "internal",
- "input": "connection",
- "orig_required": false,
- "ui_hidden": false
- },
"id": {
"description": "The id of this instance of an invocation. Must be unique among all instances of invocations.",
"field_kind": "node_attribute",
@@ -65059,102 +71635,56 @@
"ui_type": "IsIntermediate"
},
"use_cache": {
- "default": false,
+ "default": true,
"description": "Whether or not to use the cache",
"field_kind": "node_attribute",
"title": "Use Cache",
"type": "boolean"
},
- "image": {
- "anyOf": [
- {
- "$ref": "#/components/schemas/ImageField"
- },
- {
- "type": "null"
- }
- ],
- "default": null,
- "description": "The image to save and export",
- "field_kind": "input",
- "input": "any",
- "orig_required": true
- },
- "output_directory": {
- "default": "",
- "description": "Target subdirectory (relative to the configured InvokeAI outputs folder) for the exported file. Leave empty to use the outputs folder directly. Example: 'my-exports' \u2192 /my-exports/. Nested paths like 'exports/2026' are allowed. Absolute paths and path traversal ('..') are not allowed for security reasons. The directory is created automatically if it doesn't exist.",
- "field_kind": "input",
- "input": "any",
- "orig_default": "",
- "orig_required": false,
- "title": "Output Directory",
- "type": "string"
- },
- "prefix": {
+ "string_left": {
"default": "",
- "description": "Text prepended to the UUID in the exported filename. Example: 'portrait_' \u2192 'portrait_.png'",
+ "description": "String Left",
"field_kind": "input",
"input": "any",
"orig_default": "",
"orig_required": false,
- "title": "Prefix",
- "type": "string"
+ "title": "String Left",
+ "type": "string",
+ "ui_component": "textarea"
},
- "suffix": {
+ "string_right": {
"default": "",
- "description": "Text appended to the UUID (before the extension). Example: '_v2' \u2192 '_v2.png'",
+ "description": "String Right",
"field_kind": "input",
"input": "any",
"orig_default": "",
"orig_required": false,
- "title": "Suffix",
- "type": "string"
- },
- "file_format": {
- "default": "png",
- "description": "File format for the exported file. PNG is lossless; JPG/WEBP are lossy and respect 'quality'.",
- "enum": ["png", "jpg", "webp"],
- "field_kind": "input",
- "input": "any",
- "orig_default": "png",
- "orig_required": false,
- "title": "File Format",
- "type": "string"
- },
- "quality": {
- "default": 95,
- "description": "Compression quality for JPG and WEBP (1-100, higher = better quality, larger file). Ignored for PNG.",
- "field_kind": "input",
- "input": "any",
- "maximum": 100,
- "minimum": 1,
- "orig_default": 95,
- "orig_required": false,
- "title": "Quality",
- "type": "integer"
+ "title": "String Right",
+ "type": "string",
+ "ui_component": "textarea"
},
"type": {
- "const": "save_image_to_file",
- "default": "save_image_to_file",
+ "const": "string_join",
+ "default": "string_join",
"field_kind": "node_attribute",
"title": "type",
"type": "string"
}
},
"required": ["type", "id"],
- "tags": ["image", "export", "file", "save"],
- "title": "Save Image (Gallery + File Export)",
+ "tags": ["string", "join"],
+ "title": "String Join",
"type": "object",
- "version": "1.0.0",
+ "version": "1.0.1",
"output": {
- "$ref": "#/components/schemas/ImageOutput"
+ "$ref": "#/components/schemas/StringOutput"
}
},
- "ScaleLatentsInvocation": {
- "category": "latents",
+ "StringJoinThreeInvocation": {
+ "category": "strings",
"class": "invocation",
"classification": "stable",
- "description": "Scales latents by a given factor.",
+ "description": "Joins string left to string middle to string right",
"node_pack": "invokeai",
"properties": {
"id": {
@@ -65181,235 +71711,114 @@
"title": "Use Cache",
"type": "boolean"
},
- "latents": {
- "anyOf": [
- {
- "$ref": "#/components/schemas/LatentsField"
- },
- {
- "type": "null"
- }
- ],
- "default": null,
- "description": "Latents tensor",
- "field_kind": "input",
- "input": "connection",
- "orig_required": true
- },
- "scale_factor": {
- "anyOf": [
- {
- "exclusiveMinimum": 0,
- "type": "number"
- },
- {
- "type": "null"
- }
- ],
- "default": null,
- "description": "The factor by which to scale",
+ "string_left": {
+ "default": "",
+ "description": "String Left",
"field_kind": "input",
"input": "any",
- "orig_required": true,
- "title": "Scale Factor"
+ "orig_default": "",
+ "orig_required": false,
+ "title": "String Left",
+ "type": "string",
+ "ui_component": "textarea"
},
- "mode": {
- "default": "bilinear",
- "description": "Interpolation mode",
- "enum": ["nearest", "linear", "bilinear", "bicubic", "trilinear", "area", "nearest-exact"],
+ "string_middle": {
+ "default": "",
+ "description": "String Middle",
"field_kind": "input",
"input": "any",
- "orig_default": "bilinear",
+ "orig_default": "",
"orig_required": false,
- "title": "Mode",
- "type": "string"
+ "title": "String Middle",
+ "type": "string",
+ "ui_component": "textarea"
},
- "antialias": {
- "default": false,
- "description": "Whether or not to apply antialiasing (bilinear or bicubic only)",
+ "string_right": {
+ "default": "",
+ "description": "String Right",
"field_kind": "input",
"input": "any",
- "orig_default": false,
+ "orig_default": "",
"orig_required": false,
- "title": "Antialias",
- "type": "boolean"
+ "title": "String Right",
+ "type": "string",
+ "ui_component": "textarea"
},
"type": {
- "const": "lscale",
- "default": "lscale",
+ "const": "string_join_three",
+ "default": "string_join_three",
"field_kind": "node_attribute",
"title": "type",
"type": "string"
}
},
"required": ["type", "id"],
- "tags": ["latents", "resize"],
- "title": "Scale Latents",
+ "tags": ["string", "join"],
+ "title": "String Join Three",
"type": "object",
- "version": "1.0.2",
+ "version": "1.0.1",
"output": {
- "$ref": "#/components/schemas/LatentsOutput"
+ "$ref": "#/components/schemas/StringOutput"
}
},
- "SchedulerInvocation": {
- "category": "latents",
- "class": "invocation",
- "classification": "stable",
- "description": "Selects a scheduler.",
- "node_pack": "invokeai",
+ "StringOutput": {
+ "class": "output",
+ "description": "Base class for nodes that output a single string",
"properties": {
- "id": {
- "description": "The id of this instance of an invocation. Must be unique among all instances of invocations.",
- "field_kind": "node_attribute",
- "title": "Id",
- "type": "string"
- },
- "is_intermediate": {
- "default": false,
- "description": "Whether or not this is an intermediate invocation.",
- "field_kind": "node_attribute",
- "input": "direct",
- "orig_required": true,
- "title": "Is Intermediate",
- "type": "boolean",
- "ui_hidden": false,
- "ui_type": "IsIntermediate"
- },
- "use_cache": {
- "default": true,
- "description": "Whether or not to use the cache",
- "field_kind": "node_attribute",
- "title": "Use Cache",
- "type": "boolean"
- },
- "scheduler": {
- "default": "euler",
- "description": "Scheduler to use during inference",
- "enum": [
- "ddim",
- "ddpm",
- "deis",
- "deis_k",
- "lms",
- "lms_k",
- "pndm",
- "heun",
- "heun_k",
- "euler",
- "euler_k",
- "euler_a",
- "kdpm_2",
- "kdpm_2_k",
- "kdpm_2_a",
- "kdpm_2_a_k",
- "dpmpp_2s",
- "dpmpp_2s_k",
- "dpmpp_2m",
- "dpmpp_2m_k",
- "dpmpp_2m_sde",
- "dpmpp_2m_sde_k",
- "dpmpp_3m",
- "dpmpp_3m_k",
- "dpmpp_sde",
- "dpmpp_sde_k",
- "er_sde",
- "unipc",
- "unipc_k",
- "lcm",
- "tcd"
- ],
- "field_kind": "input",
- "input": "any",
- "orig_default": "euler",
- "orig_required": false,
- "title": "Scheduler",
+ "value": {
+ "description": "The output string",
+ "field_kind": "output",
+ "title": "Value",
"type": "string",
- "ui_type": "SchedulerField"
+ "ui_hidden": false
},
"type": {
- "const": "scheduler",
- "default": "scheduler",
+ "const": "string_output",
+ "default": "string_output",
"field_kind": "node_attribute",
"title": "type",
"type": "string"
}
},
- "required": ["type", "id"],
- "tags": ["scheduler"],
- "title": "Scheduler",
- "type": "object",
- "version": "1.0.0",
- "output": {
- "$ref": "#/components/schemas/SchedulerOutput"
- }
+ "required": ["output_meta", "value", "type", "type"],
+ "title": "StringOutput",
+ "type": "object"
},
- "SchedulerOutput": {
+ "StringPosNegOutput": {
"class": "output",
+ "description": "Base class for invocations that output a positive and negative string",
"properties": {
- "scheduler": {
- "description": "Scheduler to use during inference",
- "enum": [
- "ddim",
- "ddpm",
- "deis",
- "deis_k",
- "lms",
- "lms_k",
- "pndm",
- "heun",
- "heun_k",
- "euler",
- "euler_k",
- "euler_a",
- "kdpm_2",
- "kdpm_2_k",
- "kdpm_2_a",
- "kdpm_2_a_k",
- "dpmpp_2s",
- "dpmpp_2s_k",
- "dpmpp_2m",
- "dpmpp_2m_k",
- "dpmpp_2m_sde",
- "dpmpp_2m_sde_k",
- "dpmpp_3m",
- "dpmpp_3m_k",
- "dpmpp_sde",
- "dpmpp_sde_k",
- "er_sde",
- "unipc",
- "unipc_k",
- "lcm",
- "tcd"
- ],
+ "positive_string": {
+ "description": "Positive string",
"field_kind": "output",
- "title": "Scheduler",
+ "title": "Positive String",
"type": "string",
- "ui_hidden": false,
- "ui_type": "SchedulerField"
+ "ui_hidden": false
+ },
+ "negative_string": {
+ "description": "Negative string",
+ "field_kind": "output",
+ "title": "Negative String",
+ "type": "string",
+ "ui_hidden": false
},
"type": {
- "const": "scheduler_output",
- "default": "scheduler_output",
+ "const": "string_pos_neg_output",
+ "default": "string_pos_neg_output",
"field_kind": "node_attribute",
"title": "type",
"type": "string"
}
},
- "required": ["output_meta", "scheduler", "type", "type"],
- "title": "SchedulerOutput",
+ "required": ["output_meta", "positive_string", "negative_string", "type", "type"],
+ "title": "StringPosNegOutput",
"type": "object"
},
- "SchedulerPredictionType": {
- "type": "string",
- "enum": ["epsilon", "v_prediction", "sample"],
- "title": "SchedulerPredictionType",
- "description": "Scheduler prediction type."
- },
- "Sd3ModelLoaderInvocation": {
- "category": "model",
+ "StringReplaceInvocation": {
+ "category": "strings",
"class": "invocation",
"classification": "stable",
- "description": "Loads a SD3 base model, outputting its submodels.",
+ "description": "Replaces the search string with the replace string",
"node_pack": "invokeai",
"properties": {
"id": {
@@ -65436,163 +71845,71 @@
"title": "Use Cache",
"type": "boolean"
},
- "model": {
- "$ref": "#/components/schemas/ModelIdentifierField",
- "description": "SD3 model (MMDiTX) to load",
- "field_kind": "input",
- "input": "direct",
- "orig_required": true,
- "ui_model_base": ["sd-3"],
- "ui_model_type": ["main"]
- },
- "t5_encoder_model": {
- "anyOf": [
- {
- "$ref": "#/components/schemas/ModelIdentifierField"
- },
- {
- "type": "null"
- }
- ],
- "default": null,
- "description": "T5 tokenizer and text encoder",
+ "string": {
+ "default": "",
+ "description": "String to work on",
"field_kind": "input",
- "input": "direct",
- "orig_default": null,
+ "input": "any",
+ "orig_default": "",
"orig_required": false,
- "title": "T5 Encoder",
- "ui_model_type": ["t5_encoder"]
+ "title": "String",
+ "type": "string",
+ "ui_component": "textarea"
},
- "clip_l_model": {
- "anyOf": [
- {
- "$ref": "#/components/schemas/ModelIdentifierField"
- },
- {
- "type": "null"
- }
- ],
- "default": null,
- "description": "CLIP Embed loader",
+ "search_string": {
+ "default": "",
+ "description": "String to search for",
"field_kind": "input",
- "input": "direct",
- "orig_default": null,
+ "input": "any",
+ "orig_default": "",
"orig_required": false,
- "title": "CLIP L Encoder",
- "ui_model_type": ["clip_embed"],
- "ui_model_variant": ["large"]
+ "title": "Search String",
+ "type": "string",
+ "ui_component": "textarea"
},
- "clip_g_model": {
- "anyOf": [
- {
- "$ref": "#/components/schemas/ModelIdentifierField"
- },
- {
- "type": "null"
- }
- ],
- "default": null,
- "description": "CLIP-G Embed loader",
+ "replace_string": {
+ "default": "",
+ "description": "String to replace the search",
"field_kind": "input",
- "input": "direct",
- "orig_default": null,
+ "input": "any",
+ "orig_default": "",
"orig_required": false,
- "title": "CLIP G Encoder",
- "ui_model_type": ["clip_embed"],
- "ui_model_variant": ["gigantic"]
+ "title": "Replace String",
+ "type": "string",
+ "ui_component": "textarea"
},
- "vae_model": {
- "anyOf": [
- {
- "$ref": "#/components/schemas/ModelIdentifierField"
- },
- {
- "type": "null"
- }
- ],
- "default": null,
- "description": "VAE model to load",
+ "use_regex": {
+ "default": false,
+ "description": "Use search string as a regex expression (non regex is case insensitive)",
"field_kind": "input",
"input": "any",
- "orig_default": null,
+ "orig_default": false,
"orig_required": false,
- "title": "VAE",
- "ui_model_base": ["sd-3"],
- "ui_model_type": ["vae"]
+ "title": "Use Regex",
+ "type": "boolean"
},
"type": {
- "const": "sd3_model_loader",
- "default": "sd3_model_loader",
+ "const": "string_replace",
+ "default": "string_replace",
"field_kind": "node_attribute",
"title": "type",
"type": "string"
}
},
- "required": ["model", "type", "id"],
- "tags": ["model", "sd3"],
- "title": "Main Model - SD3",
+ "required": ["type", "id"],
+ "tags": ["string", "replace", "regex"],
+ "title": "String Replace",
"type": "object",
"version": "1.0.1",
"output": {
- "$ref": "#/components/schemas/Sd3ModelLoaderOutput"
+ "$ref": "#/components/schemas/StringOutput"
}
},
- "Sd3ModelLoaderOutput": {
- "class": "output",
- "description": "SD3 base model loader output.",
- "properties": {
- "transformer": {
- "$ref": "#/components/schemas/TransformerField",
- "description": "Transformer",
- "field_kind": "output",
- "title": "Transformer",
- "ui_hidden": false
- },
- "clip_l": {
- "$ref": "#/components/schemas/CLIPField",
- "description": "CLIP (tokenizer, text encoder, LoRAs) and skipped layer count",
- "field_kind": "output",
- "title": "CLIP L",
- "ui_hidden": false
- },
- "clip_g": {
- "$ref": "#/components/schemas/CLIPField",
- "description": "CLIP (tokenizer, text encoder, LoRAs) and skipped layer count",
- "field_kind": "output",
- "title": "CLIP G",
- "ui_hidden": false
- },
- "t5_encoder": {
- "$ref": "#/components/schemas/T5EncoderField",
- "description": "T5 tokenizer and text encoder",
- "field_kind": "output",
- "title": "T5 Encoder",
- "ui_hidden": false
- },
- "vae": {
- "$ref": "#/components/schemas/VAEField",
- "description": "VAE",
- "field_kind": "output",
- "title": "VAE",
- "ui_hidden": false
- },
- "type": {
- "const": "sd3_model_loader_output",
- "default": "sd3_model_loader_output",
- "field_kind": "node_attribute",
- "title": "type",
- "type": "string"
- }
- },
- "required": ["output_meta", "transformer", "clip_l", "clip_g", "t5_encoder", "vae", "type", "type"],
- "title": "Sd3ModelLoaderOutput",
- "type": "object"
- },
- "Sd3TextEncoderInvocation": {
- "category": "prompt",
+ "StringSplitInvocation": {
+ "category": "strings",
"class": "invocation",
"classification": "stable",
- "description": "Encodes and preps a prompt for a SD3 image.",
+ "description": "Splits string into two strings, based on the first occurance of the delimiter. The delimiter will be removed from the string",
"node_pack": "invokeai",
"properties": {
"id": {
@@ -65619,93 +71936,49 @@
"title": "Use Cache",
"type": "boolean"
},
- "clip_l": {
- "anyOf": [
- {
- "$ref": "#/components/schemas/CLIPField"
- },
- {
- "type": "null"
- }
- ],
- "default": null,
- "description": "CLIP (tokenizer, text encoder, LoRAs) and skipped layer count",
- "field_kind": "input",
- "input": "connection",
- "orig_required": true,
- "title": "CLIP L"
- },
- "clip_g": {
- "anyOf": [
- {
- "$ref": "#/components/schemas/CLIPField"
- },
- {
- "type": "null"
- }
- ],
- "default": null,
- "description": "CLIP (tokenizer, text encoder, LoRAs) and skipped layer count",
- "field_kind": "input",
- "input": "connection",
- "orig_required": true,
- "title": "CLIP G"
- },
- "t5_encoder": {
- "anyOf": [
- {
- "$ref": "#/components/schemas/T5EncoderField"
- },
- {
- "type": "null"
- }
- ],
- "default": null,
- "description": "T5 tokenizer and text encoder",
+ "string": {
+ "default": "",
+ "description": "String to split",
"field_kind": "input",
- "input": "connection",
- "orig_default": null,
+ "input": "any",
+ "orig_default": "",
"orig_required": false,
- "title": "T5Encoder"
+ "title": "String",
+ "type": "string",
+ "ui_component": "textarea"
},
- "prompt": {
- "anyOf": [
- {
- "type": "string"
- },
- {
- "type": "null"
- }
- ],
- "default": null,
- "description": "Text prompt to encode.",
+ "delimiter": {
+ "default": "",
+ "description": "Delimiter to spilt with. blank will split on the first whitespace",
"field_kind": "input",
"input": "any",
- "orig_required": true,
- "title": "Prompt"
+ "orig_default": "",
+ "orig_required": false,
+ "title": "Delimiter",
+ "type": "string"
},
"type": {
- "const": "sd3_text_encoder",
- "default": "sd3_text_encoder",
+ "const": "string_split",
+ "default": "string_split",
"field_kind": "node_attribute",
"title": "type",
"type": "string"
}
},
"required": ["type", "id"],
- "tags": ["prompt", "conditioning", "sd3"],
- "title": "Prompt - SD3",
+ "tags": ["string", "split"],
+ "title": "String Split",
"type": "object",
"version": "1.0.1",
"output": {
- "$ref": "#/components/schemas/SD3ConditioningOutput"
+ "$ref": "#/components/schemas/String2Output"
}
},
- "SeamlessModeInvocation": {
- "category": "model",
+ "StringSplitNegInvocation": {
+ "category": "strings",
"class": "invocation",
"classification": "stable",
- "description": "Applies the seamless transformation to the Model UNet and VAE.",
+ "description": "Splits string into two strings, inside [] goes into negative string everthing else goes into positive string. Each [ and ] character is replaced with a space",
"node_pack": "invokeai",
"properties": {
"id": {
@@ -65732,162 +72005,172 @@
"title": "Use Cache",
"type": "boolean"
},
- "unet": {
- "anyOf": [
- {
- "$ref": "#/components/schemas/UNetField"
- },
- {
- "type": "null"
- }
- ],
- "default": null,
- "description": "UNet (scheduler, LoRAs)",
- "field_kind": "input",
- "input": "connection",
- "orig_default": null,
- "orig_required": false,
- "title": "UNet"
- },
- "vae": {
- "anyOf": [
- {
- "$ref": "#/components/schemas/VAEField"
- },
- {
- "type": "null"
- }
- ],
- "default": null,
- "description": "VAE model to load",
- "field_kind": "input",
- "input": "connection",
- "orig_default": null,
- "orig_required": false,
- "title": "VAE"
- },
- "seamless_y": {
- "default": true,
- "description": "Specify whether Y axis is seamless",
- "field_kind": "input",
- "input": "any",
- "orig_default": true,
- "orig_required": false,
- "title": "Seamless Y",
- "type": "boolean"
- },
- "seamless_x": {
- "default": true,
- "description": "Specify whether X axis is seamless",
+ "string": {
+ "default": "",
+ "description": "String to split",
"field_kind": "input",
"input": "any",
- "orig_default": true,
+ "orig_default": "",
"orig_required": false,
- "title": "Seamless X",
- "type": "boolean"
+ "title": "String",
+ "type": "string",
+ "ui_component": "textarea"
},
"type": {
- "const": "seamless",
- "default": "seamless",
+ "const": "string_split_neg",
+ "default": "string_split_neg",
"field_kind": "node_attribute",
"title": "type",
"type": "string"
}
},
"required": ["type", "id"],
- "tags": ["seamless", "model"],
- "title": "Apply Seamless - SD1.5, SDXL",
+ "tags": ["string", "split", "negative"],
+ "title": "String Split Negative",
"type": "object",
- "version": "1.0.2",
+ "version": "1.0.1",
"output": {
- "$ref": "#/components/schemas/SeamlessModeOutput"
+ "$ref": "#/components/schemas/StringPosNegOutput"
}
},
- "SeamlessModeOutput": {
- "class": "output",
- "description": "Modified Seamless Model output",
+ "StylePresetField": {
+ "description": "A style preset primitive field",
"properties": {
- "unet": {
- "anyOf": [
- {
- "$ref": "#/components/schemas/UNetField"
- },
- {
- "type": "null"
- }
- ],
- "default": null,
- "description": "UNet (scheduler, LoRAs)",
- "field_kind": "output",
- "title": "UNet",
- "ui_hidden": false
+ "style_preset_id": {
+ "description": "The id of the style preset",
+ "title": "Style Preset Id",
+ "type": "string"
+ }
+ },
+ "required": ["style_preset_id"],
+ "title": "StylePresetField",
+ "type": "object"
+ },
+ "StylePresetRecordWithImage": {
+ "properties": {
+ "name": {
+ "type": "string",
+ "title": "Name",
+ "description": "The name of the style preset."
},
- "vae": {
+ "preset_data": {
+ "$ref": "#/components/schemas/PresetData",
+ "description": "The preset data"
+ },
+ "type": {
+ "$ref": "#/components/schemas/PresetType",
+ "description": "The type of style preset"
+ },
+ "is_public": {
+ "type": "boolean",
+ "title": "Is Public",
+ "description": "Whether the preset is visible to other users.",
+ "default": false
+ },
+ "id": {
+ "type": "string",
+ "title": "Id",
+ "description": "The style preset ID."
+ },
+ "user_id": {
+ "type": "string",
+ "title": "User Id",
+ "description": "The user who owns this style preset."
+ },
+ "image": {
"anyOf": [
{
- "$ref": "#/components/schemas/VAEField"
+ "type": "string"
},
{
"type": "null"
}
],
- "default": null,
- "description": "VAE",
- "field_kind": "output",
- "title": "VAE",
- "ui_hidden": false
- },
- "type": {
- "const": "seamless_output",
- "default": "seamless_output",
- "field_kind": "node_attribute",
- "title": "type",
- "type": "string"
+ "title": "Image",
+ "description": "The path for image"
}
},
- "required": ["output_meta", "unet", "vae", "type", "type"],
- "title": "SeamlessModeOutput",
- "type": "object"
+ "type": "object",
+ "required": ["name", "preset_data", "type", "id", "user_id", "image"],
+ "title": "StylePresetRecordWithImage"
},
- "SeedreamImageGenerationInvocation": {
- "category": "image",
- "class": "invocation",
- "classification": "stable",
- "description": "Generate images using a BytePlus Seedream model.",
- "node_pack": "invokeai",
+ "SubModelType": {
+ "type": "string",
+ "enum": [
+ "unet",
+ "transformer",
+ "transformer_2",
+ "text_encoder",
+ "text_encoder_2",
+ "text_encoder_3",
+ "tokenizer",
+ "tokenizer_2",
+ "tokenizer_3",
+ "vae",
+ "vae_decoder",
+ "vae_encoder",
+ "scheduler",
+ "safety_checker"
+ ],
+ "title": "SubModelType",
+ "description": "Submodel type."
+ },
+ "SubmodelDefinition": {
"properties": {
- "board": {
+ "path_or_prefix": {
+ "type": "string",
+ "title": "Path Or Prefix"
+ },
+ "model_type": {
+ "$ref": "#/components/schemas/ModelType"
+ },
+ "variant": {
"anyOf": [
{
- "$ref": "#/components/schemas/BoardField"
+ "$ref": "#/components/schemas/ModelVariantType"
},
{
- "type": "null"
- }
- ],
- "default": null,
- "description": "The board to save the image to",
- "field_kind": "internal",
- "input": "direct",
- "orig_required": false,
- "ui_hidden": false
- },
- "metadata": {
- "anyOf": [
+ "$ref": "#/components/schemas/ClipVariantType"
+ },
+ {
+ "$ref": "#/components/schemas/FluxVariantType"
+ },
+ {
+ "$ref": "#/components/schemas/Flux2VariantType"
+ },
+ {
+ "$ref": "#/components/schemas/ZImageVariantType"
+ },
+ {
+ "$ref": "#/components/schemas/QwenImageVariantType"
+ },
+ {
+ "$ref": "#/components/schemas/WanVariantType"
+ },
{
- "$ref": "#/components/schemas/MetadataField"
+ "$ref": "#/components/schemas/WanLoRAVariantType"
+ },
+ {
+ "$ref": "#/components/schemas/Qwen3VariantType"
},
{
"type": "null"
}
],
- "default": null,
- "description": "Optional metadata to be saved with the image",
- "field_kind": "internal",
- "input": "connection",
- "orig_required": false,
- "ui_hidden": false
- },
+ "title": "Variant"
+ }
+ },
+ "type": "object",
+ "required": ["path_or_prefix", "model_type"],
+ "title": "SubmodelDefinition"
+ },
+ "SubtractInvocation": {
+ "category": "math",
+ "class": "invocation",
+ "classification": "stable",
+ "description": "Subtracts two numbers",
+ "node_pack": "invokeai",
+ "properties": {
"id": {
"description": "The id of this instance of an invocation. Must be unique among all instances of invocations.",
"field_kind": "node_attribute",
@@ -65912,208 +72195,102 @@
"title": "Use Cache",
"type": "boolean"
},
- "model": {
- "anyOf": [
- {
- "$ref": "#/components/schemas/ModelIdentifierField"
- },
- {
- "type": "null"
- }
- ],
- "default": null,
- "description": "Main model (UNet, VAE, CLIP) to load",
- "field_kind": "input",
- "input": "any",
- "orig_required": true,
- "ui_model_base": ["external"],
- "ui_model_format": ["external_api"],
- "ui_model_provider_id": ["seedream"],
- "ui_model_type": ["external_image_generator"]
- },
- "mode": {
- "default": "txt2img",
- "description": "Generation mode.",
- "enum": ["txt2img", "img2img", "inpaint"],
- "field_kind": "input",
- "input": "any",
- "orig_default": "txt2img",
- "orig_required": false,
- "title": "Mode",
- "type": "string",
- "ui_hidden": true
- },
- "prompt": {
- "anyOf": [
- {
- "type": "string"
- },
- {
- "type": "null"
- }
- ],
- "default": null,
- "description": "Prompt",
- "field_kind": "input",
- "input": "any",
- "orig_required": true,
- "title": "Prompt"
- },
- "seed": {
- "anyOf": [
- {
- "type": "integer"
- },
- {
- "type": "null"
- }
- ],
- "default": null,
- "description": "Seed for random number generation",
- "field_kind": "input",
- "input": "any",
- "orig_default": null,
- "orig_required": false,
- "title": "Seed"
- },
- "num_images": {
- "default": 1,
- "description": "Number of images to generate",
- "exclusiveMinimum": 0,
- "field_kind": "input",
- "input": "any",
- "orig_default": 1,
- "orig_required": false,
- "title": "Num Images",
- "type": "integer"
- },
- "width": {
- "default": 1024,
- "description": "Width of output (px)",
- "exclusiveMinimum": 0,
+ "a": {
+ "default": 0,
+ "description": "The first number",
"field_kind": "input",
"input": "any",
- "orig_default": 1024,
+ "orig_default": 0,
"orig_required": false,
- "title": "Width",
+ "title": "A",
"type": "integer"
},
- "height": {
- "default": 1024,
- "description": "Height of output (px)",
- "exclusiveMinimum": 0,
+ "b": {
+ "default": 0,
+ "description": "The second number",
"field_kind": "input",
"input": "any",
- "orig_default": 1024,
+ "orig_default": 0,
"orig_required": false,
- "title": "Height",
+ "title": "B",
"type": "integer"
},
- "image_size": {
- "anyOf": [
- {
- "type": "string"
- },
- {
- "type": "null"
- }
- ],
- "default": null,
- "description": "Image size preset (e.g. 1K, 2K, 4K)",
- "field_kind": "input",
- "input": "any",
- "orig_default": null,
- "orig_required": false,
- "title": "Image Size"
+ "type": {
+ "const": "sub",
+ "default": "sub",
+ "field_kind": "node_attribute",
+ "title": "type",
+ "type": "string"
+ }
+ },
+ "required": ["type", "id"],
+ "tags": ["math", "subtract"],
+ "title": "Subtract Integers",
+ "type": "object",
+ "version": "1.0.1",
+ "output": {
+ "$ref": "#/components/schemas/IntegerOutput"
+ }
+ },
+ "T2IAdapterField": {
+ "properties": {
+ "image": {
+ "$ref": "#/components/schemas/ImageField",
+ "description": "The T2I-Adapter image prompt."
},
- "init_image": {
- "anyOf": [
- {
- "$ref": "#/components/schemas/ImageField"
- },
- {
- "type": "null"
- }
- ],
- "default": null,
- "description": "Init image for img2img/inpaint",
- "field_kind": "input",
- "input": "any",
- "orig_default": null,
- "orig_required": false
+ "t2i_adapter_model": {
+ "$ref": "#/components/schemas/ModelIdentifierField",
+ "description": "The T2I-Adapter model to use."
},
- "mask_image": {
+ "weight": {
"anyOf": [
{
- "$ref": "#/components/schemas/ImageField"
+ "type": "number"
},
{
- "type": "null"
+ "items": {
+ "type": "number"
+ },
+ "type": "array"
}
],
- "default": null,
- "description": "Mask image for inpaint",
- "field_kind": "input",
- "input": "any",
- "orig_default": null,
- "orig_required": false,
- "ui_hidden": true
- },
- "reference_images": {
- "default": [],
- "description": "Reference images",
- "field_kind": "input",
- "input": "any",
- "items": {
- "$ref": "#/components/schemas/ImageField"
- },
- "orig_default": [],
- "orig_required": false,
- "title": "Reference Images",
- "type": "array"
+ "default": 1,
+ "description": "The weight given to the T2I-Adapter",
+ "title": "Weight"
},
- "watermark": {
- "default": false,
- "description": "Add watermark to generated images",
- "field_kind": "input",
- "input": "any",
- "orig_default": false,
- "orig_required": false,
- "title": "Watermark",
- "type": "boolean"
+ "begin_step_percent": {
+ "default": 0,
+ "description": "When the T2I-Adapter is first applied (% of total steps)",
+ "maximum": 1,
+ "minimum": 0,
+ "title": "Begin Step Percent",
+ "type": "number"
},
- "optimize_prompt": {
- "default": false,
- "description": "Let the model optimize the prompt before generation",
- "field_kind": "input",
- "input": "any",
- "orig_default": false,
- "orig_required": false,
- "title": "Optimize Prompt",
- "type": "boolean"
+ "end_step_percent": {
+ "default": 1,
+ "description": "When the T2I-Adapter is last applied (% of total steps)",
+ "maximum": 1,
+ "minimum": 0,
+ "title": "End Step Percent",
+ "type": "number"
},
- "type": {
- "const": "seedream_image_generation",
- "default": "seedream_image_generation",
- "field_kind": "node_attribute",
- "title": "type",
+ "resize_mode": {
+ "default": "just_resize",
+ "description": "The resize mode to use",
+ "enum": ["just_resize", "crop_resize", "fill_resize", "just_resize_simple"],
+ "title": "Resize Mode",
"type": "string"
}
},
- "required": ["type", "id"],
- "tags": ["external", "generation", "seedream"],
- "title": "Seedream Image Generation",
- "type": "object",
- "version": "1.1.0",
- "output": {
- "$ref": "#/components/schemas/ImageCollectionOutput"
- }
+ "required": ["image", "t2i_adapter_model"],
+ "title": "T2IAdapterField",
+ "type": "object"
},
- "SegmentAnythingInvocation": {
- "category": "segmentation",
+ "T2IAdapterInvocation": {
+ "category": "conditioning",
"class": "invocation",
"classification": "stable",
- "description": "Runs a Segment Anything Model (SAM or SAM2).",
+ "description": "Collects T2I-Adapter info to pass to other nodes.",
"node_pack": "invokeai",
"properties": {
"id": {
@@ -66140,267 +72317,229 @@
"title": "Use Cache",
"type": "boolean"
},
- "model": {
+ "image": {
"anyOf": [
{
- "enum": [
- "segment-anything-base",
- "segment-anything-large",
- "segment-anything-huge",
- "segment-anything-2-tiny",
- "segment-anything-2-small",
- "segment-anything-2-base",
- "segment-anything-2-large"
- ],
- "type": "string"
+ "$ref": "#/components/schemas/ImageField"
},
{
"type": "null"
}
],
"default": null,
- "description": "The Segment Anything model to use (SAM or SAM2).",
+ "description": "The IP-Adapter image prompt.",
"field_kind": "input",
"input": "any",
- "orig_required": true,
- "title": "Model"
+ "orig_required": true
},
- "image": {
+ "t2i_adapter_model": {
"anyOf": [
{
- "$ref": "#/components/schemas/ImageField"
+ "$ref": "#/components/schemas/ModelIdentifierField"
},
{
"type": "null"
}
],
"default": null,
- "description": "The image to segment.",
+ "description": "The T2I-Adapter model.",
"field_kind": "input",
"input": "any",
- "orig_required": true
+ "orig_required": true,
+ "title": "T2I-Adapter Model",
+ "ui_model_base": ["sd-1", "sdxl"],
+ "ui_model_type": ["t2i_adapter"],
+ "ui_order": -1
},
- "bounding_boxes": {
+ "weight": {
"anyOf": [
+ {
+ "type": "number"
+ },
{
"items": {
- "$ref": "#/components/schemas/BoundingBoxField"
+ "type": "number"
},
"type": "array"
- },
- {
- "type": "null"
}
],
- "default": null,
- "description": "The bounding boxes to prompt the model with.",
+ "default": 1,
+ "description": "The weight given to the T2I-Adapter",
"field_kind": "input",
+ "ge": 0,
"input": "any",
- "orig_default": null,
+ "orig_default": 1,
"orig_required": false,
- "title": "Bounding Boxes"
+ "title": "Weight"
},
- "point_lists": {
- "anyOf": [
- {
- "items": {
- "$ref": "#/components/schemas/SAMPointsField"
- },
- "type": "array"
- },
- {
- "type": "null"
- }
- ],
- "default": null,
- "description": "The list of point lists to prompt the model with. Each list of points represents a single object.",
+ "begin_step_percent": {
+ "default": 0,
+ "description": "When the T2I-Adapter is first applied (% of total steps)",
"field_kind": "input",
"input": "any",
- "orig_default": null,
+ "maximum": 1,
+ "minimum": 0,
+ "orig_default": 0,
"orig_required": false,
- "title": "Point Lists"
+ "title": "Begin Step Percent",
+ "type": "number"
},
- "apply_polygon_refinement": {
- "default": true,
- "description": "Whether to apply polygon refinement to the masks. This will smooth the edges of the masks slightly and ensure that each mask consists of a single closed polygon (before merging).",
+ "end_step_percent": {
+ "default": 1,
+ "description": "When the T2I-Adapter is last applied (% of total steps)",
"field_kind": "input",
"input": "any",
- "orig_default": true,
+ "maximum": 1,
+ "minimum": 0,
+ "orig_default": 1,
"orig_required": false,
- "title": "Apply Polygon Refinement",
- "type": "boolean"
+ "title": "End Step Percent",
+ "type": "number"
},
- "mask_filter": {
- "default": "all",
- "description": "The filtering to apply to the detected masks before merging them into a final output.",
- "enum": ["all", "largest", "highest_box_score"],
+ "resize_mode": {
+ "default": "just_resize",
+ "description": "The resize mode applied to the T2I-Adapter input image so that it matches the target output size.",
+ "enum": ["just_resize", "crop_resize", "fill_resize", "just_resize_simple"],
"field_kind": "input",
"input": "any",
- "orig_default": "all",
+ "orig_default": "just_resize",
"orig_required": false,
- "title": "Mask Filter",
+ "title": "Resize Mode",
"type": "string"
},
"type": {
- "const": "segment_anything",
- "default": "segment_anything",
+ "const": "t2i_adapter",
+ "default": "t2i_adapter",
"field_kind": "node_attribute",
"title": "type",
"type": "string"
}
},
"required": ["type", "id"],
- "tags": ["prompt", "segmentation", "sam", "sam2"],
- "title": "Segment Anything",
+ "tags": ["t2i_adapter", "control"],
+ "title": "T2I-Adapter - SD1.5, SDXL",
"type": "object",
- "version": "1.3.0",
+ "version": "1.0.4",
"output": {
- "$ref": "#/components/schemas/MaskOutput"
+ "$ref": "#/components/schemas/T2IAdapterOutput"
}
},
- "SessionProcessorStatus": {
- "properties": {
- "is_started": {
- "type": "boolean",
- "title": "Is Started",
- "description": "Whether the session processor is started"
- },
- "is_processing": {
- "type": "boolean",
- "title": "Is Processing",
- "description": "Whether a session is being processed"
- }
- },
- "type": "object",
- "required": ["is_started", "is_processing"],
- "title": "SessionProcessorStatus"
- },
- "SessionQueueAndProcessorStatus": {
- "properties": {
- "queue": {
- "$ref": "#/components/schemas/SessionQueueStatus"
- },
- "processor": {
- "$ref": "#/components/schemas/SessionProcessorStatus"
- }
- },
- "type": "object",
- "required": ["queue", "processor"],
- "title": "SessionQueueAndProcessorStatus",
- "description": "The overall status of session queue and processor"
- },
- "SessionQueueCountsByDestination": {
+ "T2IAdapterMetadataField": {
"properties": {
- "queue_id": {
- "type": "string",
- "title": "Queue Id",
- "description": "The ID of the queue"
- },
- "destination": {
- "type": "string",
- "title": "Destination",
- "description": "The destination of queue items included in this status"
+ "image": {
+ "$ref": "#/components/schemas/ImageField",
+ "description": "The control image."
},
- "pending": {
- "type": "integer",
- "title": "Pending",
- "description": "Number of queue items with status 'pending' for the destination"
+ "processed_image": {
+ "anyOf": [
+ {
+ "$ref": "#/components/schemas/ImageField"
+ },
+ {
+ "type": "null"
+ }
+ ],
+ "default": null,
+ "description": "The control image, after processing."
},
- "in_progress": {
- "type": "integer",
- "title": "In Progress",
- "description": "Number of queue items with status 'in_progress' for the destination"
+ "t2i_adapter_model": {
+ "$ref": "#/components/schemas/ModelIdentifierField",
+ "description": "The T2I-Adapter model to use."
},
- "waiting": {
- "type": "integer",
- "title": "Waiting",
- "description": "Number of queue items with status 'waiting' for the destination"
+ "weight": {
+ "anyOf": [
+ {
+ "type": "number"
+ },
+ {
+ "items": {
+ "type": "number"
+ },
+ "type": "array"
+ }
+ ],
+ "default": 1,
+ "description": "The weight given to the T2I-Adapter",
+ "title": "Weight"
},
- "completed": {
- "type": "integer",
- "title": "Completed",
- "description": "Number of queue items with status 'complete' for the destination"
+ "begin_step_percent": {
+ "default": 0,
+ "description": "When the T2I-Adapter is first applied (% of total steps)",
+ "maximum": 1,
+ "minimum": 0,
+ "title": "Begin Step Percent",
+ "type": "number"
},
- "failed": {
- "type": "integer",
- "title": "Failed",
- "description": "Number of queue items with status 'error' for the destination"
+ "end_step_percent": {
+ "default": 1,
+ "description": "When the T2I-Adapter is last applied (% of total steps)",
+ "maximum": 1,
+ "minimum": 0,
+ "title": "End Step Percent",
+ "type": "number"
},
- "canceled": {
- "type": "integer",
- "title": "Canceled",
- "description": "Number of queue items with status 'canceled' for the destination"
+ "resize_mode": {
+ "default": "just_resize",
+ "description": "The resize mode to use",
+ "enum": ["just_resize", "crop_resize", "fill_resize", "just_resize_simple"],
+ "title": "Resize Mode",
+ "type": "string"
+ }
+ },
+ "required": ["image", "t2i_adapter_model"],
+ "title": "T2IAdapterMetadataField",
+ "type": "object"
+ },
+ "T2IAdapterOutput": {
+ "class": "output",
+ "properties": {
+ "t2i_adapter": {
+ "$ref": "#/components/schemas/T2IAdapterField",
+ "description": "T2I-Adapter(s) to apply",
+ "field_kind": "output",
+ "title": "T2I Adapter",
+ "ui_hidden": false
},
- "total": {
- "type": "integer",
- "title": "Total",
- "description": "Total number of queue items for the destination"
+ "type": {
+ "const": "t2i_adapter_output",
+ "default": "t2i_adapter_output",
+ "field_kind": "node_attribute",
+ "title": "type",
+ "type": "string"
}
},
- "type": "object",
- "required": [
- "queue_id",
- "destination",
- "pending",
- "in_progress",
- "waiting",
- "completed",
- "failed",
- "canceled",
- "total"
- ],
- "title": "SessionQueueCountsByDestination"
+ "required": ["output_meta", "t2i_adapter", "type", "type"],
+ "title": "T2IAdapterOutput",
+ "type": "object"
},
- "SessionQueueItem": {
+ "T2IAdapter_Diffusers_SD1_Config": {
"properties": {
- "item_id": {
- "type": "integer",
- "title": "Item Id",
- "description": "The identifier of the session queue item"
+ "key": {
+ "type": "string",
+ "title": "Key",
+ "description": "A unique key for this model."
},
- "status": {
+ "hash": {
"type": "string",
- "enum": ["pending", "in_progress", "waiting", "completed", "failed", "canceled"],
- "title": "Status",
- "description": "The status of this queue item",
- "default": "pending"
+ "title": "Hash",
+ "description": "The hash of the model file(s)."
},
- "status_sequence": {
- "anyOf": [
- {
- "type": "integer"
- },
- {
- "type": "null"
- }
- ],
- "title": "Status Sequence",
- "description": "A monotonically increasing version for this queue item's visible status lifecycle"
+ "path": {
+ "type": "string",
+ "title": "Path",
+ "description": "Path to the model on the filesystem. Relative paths are relative to the Invoke root directory."
},
- "priority": {
+ "file_size": {
"type": "integer",
- "title": "Priority",
- "description": "The priority of this queue item",
- "default": 0
+ "title": "File Size",
+ "description": "The size of the model in bytes."
},
- "batch_id": {
+ "name": {
"type": "string",
- "title": "Batch Id",
- "description": "The ID of the batch associated with this queue item"
- },
- "origin": {
- "anyOf": [
- {
- "type": "string"
- },
- {
- "type": "null"
- }
- ],
- "title": "Origin",
- "description": "The origin of this queue item. This data is used by the frontend to determine how to handle results."
+ "title": "Name",
+ "description": "Name of the model."
},
- "destination": {
+ "description": {
"anyOf": [
{
"type": "string"
@@ -66409,15 +72548,19 @@
"type": "null"
}
],
- "title": "Destination",
- "description": "The origin of this queue item. This data is used by the frontend to determine how to handle results"
+ "title": "Description",
+ "description": "Model description"
},
- "session_id": {
+ "source": {
"type": "string",
- "title": "Session Id",
- "description": "The ID of the session associated with this queue item. The session doesn't exist in graph_executions until the queue item is executed."
+ "title": "Source",
+ "description": "The original source of the model (path, URL or repo_id)."
},
- "error_type": {
+ "source_type": {
+ "$ref": "#/components/schemas/ModelSourceType",
+ "description": "The type of source"
+ },
+ "source_api_response": {
"anyOf": [
{
"type": "string"
@@ -66426,10 +72569,10 @@
"type": "null"
}
],
- "title": "Error Type",
- "description": "The error type if this queue item errored"
+ "title": "Source Api Response",
+ "description": "The original API response from the source, as stringified JSON."
},
- "error_message": {
+ "source_url": {
"anyOf": [
{
"type": "string"
@@ -66438,10 +72581,10 @@
"type": "null"
}
],
- "title": "Error Message",
- "description": "The error message if this queue item errored"
+ "title": "Source Url",
+ "description": "Optional URL for the model (e.g. download page or model page)."
},
- "error_traceback": {
+ "cover_image": {
"anyOf": [
{
"type": "string"
@@ -66450,57 +72593,92 @@
"type": "null"
}
],
- "title": "Error Traceback",
- "description": "The error traceback if this queue item errored"
+ "title": "Cover Image",
+ "description": "Url for image to preview model"
},
- "created_at": {
- "anyOf": [
- {
- "type": "string",
- "format": "date-time"
- },
- {
- "type": "string"
- }
- ],
- "title": "Created At",
- "description": "When this queue item was created"
+ "format": {
+ "type": "string",
+ "const": "diffusers",
+ "title": "Format",
+ "default": "diffusers"
},
- "updated_at": {
- "anyOf": [
- {
- "type": "string",
- "format": "date-time"
- },
- {
- "type": "string"
- }
- ],
- "title": "Updated At",
- "description": "When this queue item was updated"
+ "repo_variant": {
+ "$ref": "#/components/schemas/ModelRepoVariant",
+ "default": ""
},
- "started_at": {
+ "type": {
+ "type": "string",
+ "const": "t2i_adapter",
+ "title": "Type",
+ "default": "t2i_adapter"
+ },
+ "default_settings": {
"anyOf": [
{
- "type": "string",
- "format": "date-time"
- },
- {
- "type": "string"
+ "$ref": "#/components/schemas/ControlAdapterDefaultSettings"
},
{
"type": "null"
}
- ],
- "title": "Started At",
- "description": "When this queue item was started"
+ ]
},
- "completed_at": {
+ "base": {
+ "type": "string",
+ "const": "sd-1",
+ "title": "Base",
+ "default": "sd-1"
+ }
+ },
+ "type": "object",
+ "required": [
+ "key",
+ "hash",
+ "path",
+ "file_size",
+ "name",
+ "description",
+ "source",
+ "source_type",
+ "source_api_response",
+ "source_url",
+ "cover_image",
+ "format",
+ "repo_variant",
+ "type",
+ "default_settings",
+ "base"
+ ],
+ "title": "T2IAdapter_Diffusers_SD1_Config"
+ },
+ "T2IAdapter_Diffusers_SDXL_Config": {
+ "properties": {
+ "key": {
+ "type": "string",
+ "title": "Key",
+ "description": "A unique key for this model."
+ },
+ "hash": {
+ "type": "string",
+ "title": "Hash",
+ "description": "The hash of the model file(s)."
+ },
+ "path": {
+ "type": "string",
+ "title": "Path",
+ "description": "Path to the model on the filesystem. Relative paths are relative to the Invoke root directory."
+ },
+ "file_size": {
+ "type": "integer",
+ "title": "File Size",
+ "description": "The size of the model in bytes."
+ },
+ "name": {
+ "type": "string",
+ "title": "Name",
+ "description": "Name of the model."
+ },
+ "description": {
"anyOf": [
- {
- "type": "string",
- "format": "date-time"
- },
{
"type": "string"
},
@@ -66508,21 +72686,19 @@
"type": "null"
}
],
- "title": "Completed At",
- "description": "When this queue item was completed"
+ "title": "Description",
+ "description": "Model description"
},
- "queue_id": {
+ "source": {
"type": "string",
- "title": "Queue Id",
- "description": "The id of the queue with which this item is associated"
+ "title": "Source",
+ "description": "The original source of the model (path, URL or repo_id)."
},
- "user_id": {
- "type": "string",
- "title": "User Id",
- "description": "The id of the user who created this queue item",
- "default": "system"
+ "source_type": {
+ "$ref": "#/components/schemas/ModelSourceType",
+ "description": "The type of source"
},
- "user_display_name": {
+ "source_api_response": {
"anyOf": [
{
"type": "string"
@@ -66531,10 +72707,10 @@
"type": "null"
}
],
- "title": "User Display Name",
- "description": "The display name of the user who created this queue item, if available"
+ "title": "Source Api Response",
+ "description": "The original API response from the source, as stringified JSON."
},
- "user_email": {
+ "source_url": {
"anyOf": [
{
"type": "string"
@@ -66543,37 +72719,126 @@
"type": "null"
}
],
- "title": "User Email",
- "description": "The email of the user who created this queue item, if available"
+ "title": "Source Url",
+ "description": "Optional URL for the model (e.g. download page or model page)."
},
- "field_values": {
+ "cover_image": {
"anyOf": [
{
- "items": {
- "$ref": "#/components/schemas/NodeFieldValue"
- },
- "type": "array"
+ "type": "string"
},
{
"type": "null"
}
],
- "title": "Field Values",
- "description": "The field values that were used for this queue item"
+ "title": "Cover Image",
+ "description": "Url for image to preview model"
},
- "retried_from_item_id": {
+ "format": {
+ "type": "string",
+ "const": "diffusers",
+ "title": "Format",
+ "default": "diffusers"
+ },
+ "repo_variant": {
+ "$ref": "#/components/schemas/ModelRepoVariant",
+ "default": ""
+ },
+ "type": {
+ "type": "string",
+ "const": "t2i_adapter",
+ "title": "Type",
+ "default": "t2i_adapter"
+ },
+ "default_settings": {
"anyOf": [
{
- "type": "integer"
+ "$ref": "#/components/schemas/ControlAdapterDefaultSettings"
},
{
"type": "null"
}
- ],
- "title": "Retried From Item Id",
- "description": "The item_id of the queue item that this item was retried from"
+ ]
},
- "workflow_call_id": {
+ "base": {
+ "type": "string",
+ "const": "sdxl",
+ "title": "Base",
+ "default": "sdxl"
+ }
+ },
+ "type": "object",
+ "required": [
+ "key",
+ "hash",
+ "path",
+ "file_size",
+ "name",
+ "description",
+ "source",
+ "source_type",
+ "source_api_response",
+ "source_url",
+ "cover_image",
+ "format",
+ "repo_variant",
+ "type",
+ "default_settings",
+ "base"
+ ],
+ "title": "T2IAdapter_Diffusers_SDXL_Config"
+ },
+ "T5EncoderField": {
+ "properties": {
+ "tokenizer": {
+ "$ref": "#/components/schemas/ModelIdentifierField",
+ "description": "Info to load tokenizer submodel"
+ },
+ "text_encoder": {
+ "$ref": "#/components/schemas/ModelIdentifierField",
+ "description": "Info to load text_encoder submodel"
+ },
+ "loras": {
+ "description": "LoRAs to apply on model loading",
+ "items": {
+ "$ref": "#/components/schemas/LoRAField"
+ },
+ "title": "Loras",
+ "type": "array"
+ }
+ },
+ "required": ["tokenizer", "text_encoder", "loras"],
+ "title": "T5EncoderField",
+ "type": "object"
+ },
+ "T5Encoder_BnBLLMint8_Config": {
+ "properties": {
+ "key": {
+ "type": "string",
+ "title": "Key",
+ "description": "A unique key for this model."
+ },
+ "hash": {
+ "type": "string",
+ "title": "Hash",
+ "description": "The hash of the model file(s)."
+ },
+ "path": {
+ "type": "string",
+ "title": "Path",
+ "description": "Path to the model on the filesystem. Relative paths are relative to the Invoke root directory."
+ },
+ "file_size": {
+ "type": "integer",
+ "title": "File Size",
+ "description": "The size of the model in bytes."
+ },
+ "name": {
+ "type": "string",
+ "title": "Name",
+ "description": "Name of the model."
+ },
+ "description": {
"anyOf": [
{
"type": "string"
@@ -66582,22 +72847,19 @@
"type": "null"
}
],
- "title": "Workflow Call Id",
- "description": "The active workflow-call relationship id when this queue item is a child execution."
+ "title": "Description",
+ "description": "Model description"
},
- "parent_item_id": {
- "anyOf": [
- {
- "type": "integer"
- },
- {
- "type": "null"
- }
- ],
- "title": "Parent Item Id",
- "description": "The parent queue item id when this queue item is a child workflow execution."
+ "source": {
+ "type": "string",
+ "title": "Source",
+ "description": "The original source of the model (path, URL or repo_id)."
},
- "parent_session_id": {
+ "source_type": {
+ "$ref": "#/components/schemas/ModelSourceType",
+ "description": "The type of source"
+ },
+ "source_api_response": {
"anyOf": [
{
"type": "string"
@@ -66606,97 +72868,113 @@
"type": "null"
}
],
- "title": "Parent Session Id",
- "description": "The parent session id when this queue item is a child workflow execution."
+ "title": "Source Api Response",
+ "description": "The original API response from the source, as stringified JSON."
},
- "root_item_id": {
+ "source_url": {
"anyOf": [
{
- "type": "integer"
+ "type": "string"
},
{
"type": "null"
}
],
- "title": "Root Item Id",
- "description": "The root queue item id for this workflow call chain, if any."
+ "title": "Source Url",
+ "description": "Optional URL for the model (e.g. download page or model page)."
},
- "workflow_call_depth": {
+ "cover_image": {
"anyOf": [
{
- "type": "integer"
+ "type": "string"
},
{
"type": "null"
}
],
- "title": "Workflow Call Depth",
- "description": "The 1-based workflow-call depth for this queue item when it is a child execution."
+ "title": "Cover Image",
+ "description": "Url for image to preview model"
},
- "session": {
- "$ref": "#/components/schemas/GraphExecutionState",
- "description": "The fully-populated session to be executed"
+ "base": {
+ "type": "string",
+ "const": "any",
+ "title": "Base",
+ "default": "any"
},
- "workflow": {
+ "type": {
+ "type": "string",
+ "const": "t5_encoder",
+ "title": "Type",
+ "default": "t5_encoder"
+ },
+ "format": {
+ "type": "string",
+ "const": "bnb_quantized_int8b",
+ "title": "Format",
+ "default": "bnb_quantized_int8b"
+ },
+ "cpu_only": {
"anyOf": [
{
- "$ref": "#/components/schemas/WorkflowWithoutID"
+ "type": "boolean"
},
{
"type": "null"
}
],
- "description": "The workflow associated with this queue item"
+ "title": "Cpu Only",
+ "description": "Whether this model should run on CPU only"
}
},
"type": "object",
"required": [
- "item_id",
- "status",
- "batch_id",
- "queue_id",
- "session_id",
- "session",
- "priority",
- "session_id",
- "created_at",
- "updated_at"
+ "key",
+ "hash",
+ "path",
+ "file_size",
+ "name",
+ "description",
+ "source",
+ "source_type",
+ "source_api_response",
+ "source_url",
+ "cover_image",
+ "base",
+ "type",
+ "format",
+ "cpu_only"
],
- "title": "SessionQueueItem",
- "description": "Session queue item without the full graph. Used for serialization."
+ "title": "T5Encoder_BnBLLMint8_Config",
+ "description": "Configuration for T5 Encoder models quantized by bitsandbytes' LLM.int8."
},
- "SessionQueueStatus": {
+ "T5Encoder_T5Encoder_Config": {
"properties": {
- "queue_id": {
+ "key": {
"type": "string",
- "title": "Queue Id",
- "description": "The ID of the queue"
+ "title": "Key",
+ "description": "A unique key for this model."
},
- "item_id": {
- "anyOf": [
- {
- "type": "integer"
- },
- {
- "type": "null"
- }
- ],
- "title": "Item Id",
- "description": "The current queue item id"
+ "hash": {
+ "type": "string",
+ "title": "Hash",
+ "description": "The hash of the model file(s)."
},
- "batch_id": {
- "anyOf": [
- {
- "type": "string"
- },
- {
- "type": "null"
- }
- ],
- "title": "Batch Id",
- "description": "The current queue item's batch id"
+ "path": {
+ "type": "string",
+ "title": "Path",
+ "description": "Path to the model on the filesystem. Relative paths are relative to the Invoke root directory."
},
- "session_id": {
+ "file_size": {
+ "type": "integer",
+ "title": "File Size",
+ "description": "The size of the model in bytes."
+ },
+ "name": {
+ "type": "string",
+ "title": "Name",
+ "description": "Name of the model."
+ },
+ "description": {
"anyOf": [
{
"type": "string"
@@ -66705,93 +72983,43 @@
"type": "null"
}
],
- "title": "Session Id",
- "description": "The current queue item's session id"
- },
- "pending": {
- "type": "integer",
- "title": "Pending",
- "description": "Number of queue items with status 'pending'"
- },
- "in_progress": {
- "type": "integer",
- "title": "In Progress",
- "description": "Number of queue items with status 'in_progress'"
- },
- "waiting": {
- "type": "integer",
- "title": "Waiting",
- "description": "Number of queue items with status 'waiting'"
- },
- "completed": {
- "type": "integer",
- "title": "Completed",
- "description": "Number of queue items with status 'complete'"
- },
- "failed": {
- "type": "integer",
- "title": "Failed",
- "description": "Number of queue items with status 'error'"
+ "title": "Description",
+ "description": "Model description"
},
- "canceled": {
- "type": "integer",
- "title": "Canceled",
- "description": "Number of queue items with status 'canceled'"
+ "source": {
+ "type": "string",
+ "title": "Source",
+ "description": "The original source of the model (path, URL or repo_id)."
},
- "total": {
- "type": "integer",
- "title": "Total",
- "description": "Total number of queue items"
+ "source_type": {
+ "$ref": "#/components/schemas/ModelSourceType",
+ "description": "The type of source"
},
- "user_pending": {
+ "source_api_response": {
"anyOf": [
{
- "type": "integer"
+ "type": "string"
},
{
"type": "null"
}
],
- "title": "User Pending",
- "description": "Number of the requesting user's queue items with status 'pending' (None for admins/global callers)"
+ "title": "Source Api Response",
+ "description": "The original API response from the source, as stringified JSON."
},
- "user_in_progress": {
+ "source_url": {
"anyOf": [
{
- "type": "integer"
+ "type": "string"
},
{
"type": "null"
}
],
- "title": "User In Progress",
- "description": "Number of the requesting user's queue items with status 'in_progress' (None for admins/global callers)"
- }
- },
- "type": "object",
- "required": [
- "queue_id",
- "item_id",
- "batch_id",
- "session_id",
- "pending",
- "in_progress",
- "waiting",
- "completed",
- "failed",
- "canceled",
- "total"
- ],
- "title": "SessionQueueStatus"
- },
- "SetupRequest": {
- "properties": {
- "email": {
- "type": "string",
- "title": "Email",
- "description": "Admin email address"
+ "title": "Source Url",
+ "description": "Optional URL for the model (e.g. download page or model page)."
},
- "display_name": {
+ "cover_image": {
"anyOf": [
{
"type": "string"
@@ -66800,136 +73028,85 @@
"type": "null"
}
],
- "title": "Display Name",
- "description": "Admin display name"
+ "title": "Cover Image",
+ "description": "Url for image to preview model"
},
- "password": {
+ "base": {
"type": "string",
- "title": "Password",
- "description": "Admin password"
- }
- },
- "type": "object",
- "required": ["email", "password"],
- "title": "SetupRequest",
- "description": "Request body for initial admin setup."
- },
- "SetupResponse": {
- "properties": {
- "success": {
- "type": "boolean",
- "title": "Success",
- "description": "Whether setup was successful"
- },
- "user": {
- "$ref": "#/components/schemas/UserDTO",
- "description": "Created admin user information"
- }
- },
- "type": "object",
- "required": ["success", "user"],
- "title": "SetupResponse",
- "description": "Response from successful admin setup."
- },
- "SetupStatusResponse": {
- "properties": {
- "setup_required": {
- "type": "boolean",
- "title": "Setup Required",
- "description": "Whether initial setup is required"
+ "const": "any",
+ "title": "Base",
+ "default": "any"
},
- "multiuser_enabled": {
- "type": "boolean",
- "title": "Multiuser Enabled",
- "description": "Whether multiuser mode is enabled"
+ "type": {
+ "type": "string",
+ "const": "t5_encoder",
+ "title": "Type",
+ "default": "t5_encoder"
},
- "strict_password_checking": {
- "type": "boolean",
- "title": "Strict Password Checking",
- "description": "Whether strict password requirements are enforced"
+ "format": {
+ "type": "string",
+ "const": "t5_encoder",
+ "title": "Format",
+ "default": "t5_encoder"
},
- "admin_email": {
+ "cpu_only": {
"anyOf": [
{
- "type": "string"
+ "type": "boolean"
},
{
"type": "null"
}
],
- "title": "Admin Email",
- "description": "Email of the first active admin user, if any"
+ "title": "Cpu Only",
+ "description": "Whether this model should run on CPU only"
}
},
"type": "object",
- "required": ["setup_required", "multiuser_enabled", "strict_password_checking"],
- "title": "SetupStatusResponse",
- "description": "Response for setup status check."
+ "required": [
+ "key",
+ "hash",
+ "path",
+ "file_size",
+ "name",
+ "description",
+ "source",
+ "source_type",
+ "source_api_response",
+ "source_url",
+ "cover_image",
+ "base",
+ "type",
+ "format",
+ "cpu_only"
+ ],
+ "title": "T5Encoder_T5Encoder_Config",
+ "description": "Configuration for T5 Encoder models in a bespoke, diffusers-like format. The model weights are expected to be in\na folder called text_encoder_2 inside the model directory, with a config file named model.safetensors.index.json."
},
- "ShowImageInvocation": {
- "category": "image",
- "class": "invocation",
- "classification": "stable",
- "description": "Displays a provided image using the OS image viewer, and passes it forward in the pipeline.",
- "node_pack": "invokeai",
+ "TBLR": {
"properties": {
- "id": {
- "description": "The id of this instance of an invocation. Must be unique among all instances of invocations.",
- "field_kind": "node_attribute",
- "title": "Id",
- "type": "string"
- },
- "is_intermediate": {
- "default": false,
- "description": "Whether or not this is an intermediate invocation.",
- "field_kind": "node_attribute",
- "input": "direct",
- "orig_required": true,
- "title": "Is Intermediate",
- "type": "boolean",
- "ui_hidden": false,
- "ui_type": "IsIntermediate"
+ "top": {
+ "title": "Top",
+ "type": "integer"
},
- "use_cache": {
- "default": true,
- "description": "Whether or not to use the cache",
- "field_kind": "node_attribute",
- "title": "Use Cache",
- "type": "boolean"
+ "bottom": {
+ "title": "Bottom",
+ "type": "integer"
},
- "image": {
- "anyOf": [
- {
- "$ref": "#/components/schemas/ImageField"
- },
- {
- "type": "null"
- }
- ],
- "default": null,
- "description": "The image to show",
- "field_kind": "input",
- "input": "any",
- "orig_required": true
+ "left": {
+ "title": "Left",
+ "type": "integer"
},
- "type": {
- "const": "show_image",
- "default": "show_image",
- "field_kind": "node_attribute",
- "title": "type",
- "type": "string"
+ "right": {
+ "title": "Right",
+ "type": "integer"
}
},
- "required": ["type", "id"],
- "tags": ["image"],
- "title": "Show Image",
- "type": "object",
- "version": "1.0.1",
- "output": {
- "$ref": "#/components/schemas/ImageOutput"
- }
+ "required": ["top", "bottom", "left", "right"],
+ "title": "TBLR",
+ "type": "object"
},
- "SigLIP_Diffusers_Config": {
+ "TI_File_SD1_Config": {
"properties": {
"key": {
"type": "string",
@@ -67013,39 +73190,23 @@
"title": "Cover Image",
"description": "Url for image to preview model"
},
- "format": {
- "type": "string",
- "const": "diffusers",
- "title": "Format",
- "default": "diffusers"
- },
- "repo_variant": {
- "$ref": "#/components/schemas/ModelRepoVariant",
- "default": ""
- },
"type": {
"type": "string",
- "const": "siglip",
+ "const": "embedding",
"title": "Type",
- "default": "siglip"
+ "default": "embedding"
+ },
+ "format": {
+ "type": "string",
+ "const": "embedding_file",
+ "title": "Format",
+ "default": "embedding_file"
},
"base": {
"type": "string",
- "const": "any",
+ "const": "sd-1",
"title": "Base",
- "default": "any"
- },
- "cpu_only": {
- "anyOf": [
- {
- "type": "boolean"
- },
- {
- "type": "null"
- }
- ],
- "title": "Cpu Only",
- "description": "Whether this model should run on CPU only"
+ "default": "sd-1"
}
},
"type": "object",
@@ -67061,282 +73222,257 @@
"source_api_response",
"source_url",
"cover_image",
- "format",
- "repo_variant",
"type",
- "base",
- "cpu_only"
+ "format",
+ "base"
],
- "title": "SigLIP_Diffusers_Config",
- "description": "Model config for SigLIP."
+ "title": "TI_File_SD1_Config"
},
- "SpandrelImageToImageAutoscaleInvocation": {
- "category": "upscale",
- "class": "invocation",
- "classification": "stable",
- "description": "Run any spandrel image-to-image model (https://github.com/chaiNNer-org/spandrel) until the target scale is reached.",
- "node_pack": "invokeai",
+ "TI_File_SD2_Config": {
"properties": {
- "board": {
+ "key": {
+ "type": "string",
+ "title": "Key",
+ "description": "A unique key for this model."
+ },
+ "hash": {
+ "type": "string",
+ "title": "Hash",
+ "description": "The hash of the model file(s)."
+ },
+ "path": {
+ "type": "string",
+ "title": "Path",
+ "description": "Path to the model on the filesystem. Relative paths are relative to the Invoke root directory."
+ },
+ "file_size": {
+ "type": "integer",
+ "title": "File Size",
+ "description": "The size of the model in bytes."
+ },
+ "name": {
+ "type": "string",
+ "title": "Name",
+ "description": "Name of the model."
+ },
+ "description": {
"anyOf": [
{
- "$ref": "#/components/schemas/BoardField"
+ "type": "string"
},
{
"type": "null"
}
],
- "default": null,
- "description": "The board to save the image to",
- "field_kind": "internal",
- "input": "direct",
- "orig_required": false,
- "ui_hidden": false
+ "title": "Description",
+ "description": "Model description"
},
- "metadata": {
+ "source": {
+ "type": "string",
+ "title": "Source",
+ "description": "The original source of the model (path, URL or repo_id)."
+ },
+ "source_type": {
+ "$ref": "#/components/schemas/ModelSourceType",
+ "description": "The type of source"
+ },
+ "source_api_response": {
"anyOf": [
{
- "$ref": "#/components/schemas/MetadataField"
+ "type": "string"
},
{
"type": "null"
}
],
- "default": null,
- "description": "Optional metadata to be saved with the image",
- "field_kind": "internal",
- "input": "connection",
- "orig_required": false,
- "ui_hidden": false
- },
- "id": {
- "description": "The id of this instance of an invocation. Must be unique among all instances of invocations.",
- "field_kind": "node_attribute",
- "title": "Id",
- "type": "string"
- },
- "is_intermediate": {
- "default": false,
- "description": "Whether or not this is an intermediate invocation.",
- "field_kind": "node_attribute",
- "input": "direct",
- "orig_required": true,
- "title": "Is Intermediate",
- "type": "boolean",
- "ui_hidden": false,
- "ui_type": "IsIntermediate"
- },
- "use_cache": {
- "default": true,
- "description": "Whether or not to use the cache",
- "field_kind": "node_attribute",
- "title": "Use Cache",
- "type": "boolean"
+ "title": "Source Api Response",
+ "description": "The original API response from the source, as stringified JSON."
},
- "image": {
+ "source_url": {
"anyOf": [
{
- "$ref": "#/components/schemas/ImageField"
+ "type": "string"
},
{
"type": "null"
}
],
- "default": null,
- "description": "The input image",
- "field_kind": "input",
- "input": "any",
- "orig_required": true
+ "title": "Source Url",
+ "description": "Optional URL for the model (e.g. download page or model page)."
},
- "image_to_image_model": {
+ "cover_image": {
"anyOf": [
{
- "$ref": "#/components/schemas/ModelIdentifierField"
+ "type": "string"
},
{
"type": "null"
}
],
- "default": null,
- "description": "Image-to-Image model",
- "field_kind": "input",
- "input": "any",
- "orig_required": true,
- "title": "Image-to-Image Model",
- "ui_model_type": ["spandrel_image_to_image"]
- },
- "tile_size": {
- "default": 512,
- "description": "The tile size for tiled image-to-image. Set to 0 to disable tiling.",
- "field_kind": "input",
- "input": "any",
- "orig_default": 512,
- "orig_required": false,
- "title": "Tile Size",
- "type": "integer"
+ "title": "Cover Image",
+ "description": "Url for image to preview model"
},
"type": {
- "const": "spandrel_image_to_image_autoscale",
- "default": "spandrel_image_to_image_autoscale",
- "field_kind": "node_attribute",
- "title": "type",
- "type": "string"
+ "type": "string",
+ "const": "embedding",
+ "title": "Type",
+ "default": "embedding"
},
- "scale": {
- "default": 4.0,
- "description": "The final scale of the output image. If the model does not upscale the image, this will be ignored.",
- "exclusiveMinimum": 0.0,
- "field_kind": "input",
- "input": "any",
- "maximum": 16.0,
- "orig_default": 4.0,
- "orig_required": false,
- "title": "Scale",
- "type": "number"
+ "format": {
+ "type": "string",
+ "const": "embedding_file",
+ "title": "Format",
+ "default": "embedding_file"
},
- "fit_to_multiple_of_8": {
- "default": false,
- "description": "If true, the output image will be resized to the nearest multiple of 8 in both dimensions.",
- "field_kind": "input",
- "input": "any",
- "orig_default": false,
- "orig_required": false,
- "title": "Fit To Multiple Of 8",
- "type": "boolean"
+ "base": {
+ "type": "string",
+ "const": "sd-2",
+ "title": "Base",
+ "default": "sd-2"
}
},
- "required": ["type", "id"],
- "tags": ["upscale"],
- "title": "Image-to-Image (Autoscale)",
"type": "object",
- "version": "1.0.0",
- "output": {
- "$ref": "#/components/schemas/ImageOutput"
- }
+ "required": [
+ "key",
+ "hash",
+ "path",
+ "file_size",
+ "name",
+ "description",
+ "source",
+ "source_type",
+ "source_api_response",
+ "source_url",
+ "cover_image",
+ "type",
+ "format",
+ "base"
+ ],
+ "title": "TI_File_SD2_Config"
},
- "SpandrelImageToImageInvocation": {
- "category": "upscale",
- "class": "invocation",
- "classification": "stable",
- "description": "Run any spandrel image-to-image model (https://github.com/chaiNNer-org/spandrel).",
- "node_pack": "invokeai",
+ "TI_File_SDXL_Config": {
"properties": {
- "board": {
+ "key": {
+ "type": "string",
+ "title": "Key",
+ "description": "A unique key for this model."
+ },
+ "hash": {
+ "type": "string",
+ "title": "Hash",
+ "description": "The hash of the model file(s)."
+ },
+ "path": {
+ "type": "string",
+ "title": "Path",
+ "description": "Path to the model on the filesystem. Relative paths are relative to the Invoke root directory."
+ },
+ "file_size": {
+ "type": "integer",
+ "title": "File Size",
+ "description": "The size of the model in bytes."
+ },
+ "name": {
+ "type": "string",
+ "title": "Name",
+ "description": "Name of the model."
+ },
+ "description": {
"anyOf": [
{
- "$ref": "#/components/schemas/BoardField"
+ "type": "string"
},
{
"type": "null"
}
],
- "default": null,
- "description": "The board to save the image to",
- "field_kind": "internal",
- "input": "direct",
- "orig_required": false,
- "ui_hidden": false
+ "title": "Description",
+ "description": "Model description"
},
- "metadata": {
+ "source": {
+ "type": "string",
+ "title": "Source",
+ "description": "The original source of the model (path, URL or repo_id)."
+ },
+ "source_type": {
+ "$ref": "#/components/schemas/ModelSourceType",
+ "description": "The type of source"
+ },
+ "source_api_response": {
"anyOf": [
{
- "$ref": "#/components/schemas/MetadataField"
+ "type": "string"
},
{
"type": "null"
}
],
- "default": null,
- "description": "Optional metadata to be saved with the image",
- "field_kind": "internal",
- "input": "connection",
- "orig_required": false,
- "ui_hidden": false
- },
- "id": {
- "description": "The id of this instance of an invocation. Must be unique among all instances of invocations.",
- "field_kind": "node_attribute",
- "title": "Id",
- "type": "string"
- },
- "is_intermediate": {
- "default": false,
- "description": "Whether or not this is an intermediate invocation.",
- "field_kind": "node_attribute",
- "input": "direct",
- "orig_required": true,
- "title": "Is Intermediate",
- "type": "boolean",
- "ui_hidden": false,
- "ui_type": "IsIntermediate"
- },
- "use_cache": {
- "default": true,
- "description": "Whether or not to use the cache",
- "field_kind": "node_attribute",
- "title": "Use Cache",
- "type": "boolean"
+ "title": "Source Api Response",
+ "description": "The original API response from the source, as stringified JSON."
},
- "image": {
+ "source_url": {
"anyOf": [
{
- "$ref": "#/components/schemas/ImageField"
+ "type": "string"
},
{
"type": "null"
}
],
- "default": null,
- "description": "The input image",
- "field_kind": "input",
- "input": "any",
- "orig_required": true
+ "title": "Source Url",
+ "description": "Optional URL for the model (e.g. download page or model page)."
},
- "image_to_image_model": {
+ "cover_image": {
"anyOf": [
{
- "$ref": "#/components/schemas/ModelIdentifierField"
+ "type": "string"
},
{
"type": "null"
}
],
- "default": null,
- "description": "Image-to-Image model",
- "field_kind": "input",
- "input": "any",
- "orig_required": true,
- "title": "Image-to-Image Model",
- "ui_model_type": ["spandrel_image_to_image"]
- },
- "tile_size": {
- "default": 512,
- "description": "The tile size for tiled image-to-image. Set to 0 to disable tiling.",
- "field_kind": "input",
- "input": "any",
- "orig_default": 512,
- "orig_required": false,
- "title": "Tile Size",
- "type": "integer"
+ "title": "Cover Image",
+ "description": "Url for image to preview model"
},
"type": {
- "const": "spandrel_image_to_image",
- "default": "spandrel_image_to_image",
- "field_kind": "node_attribute",
- "title": "type",
- "type": "string"
+ "type": "string",
+ "const": "embedding",
+ "title": "Type",
+ "default": "embedding"
+ },
+ "format": {
+ "type": "string",
+ "const": "embedding_file",
+ "title": "Format",
+ "default": "embedding_file"
+ },
+ "base": {
+ "type": "string",
+ "const": "sdxl",
+ "title": "Base",
+ "default": "sdxl"
}
},
- "required": ["type", "id"],
- "tags": ["upscale"],
- "title": "Image-to-Image",
"type": "object",
- "version": "1.3.0",
- "output": {
- "$ref": "#/components/schemas/ImageOutput"
- }
+ "required": [
+ "key",
+ "hash",
+ "path",
+ "file_size",
+ "name",
+ "description",
+ "source",
+ "source_type",
+ "source_api_response",
+ "source_url",
+ "cover_image",
+ "type",
+ "format",
+ "base"
+ ],
+ "title": "TI_File_SDXL_Config"
},
- "Spandrel_Checkpoint_Config": {
+ "TI_Folder_SD1_Config": {
"properties": {
"key": {
"type": "string",
@@ -67420,23 +73556,23 @@
"title": "Cover Image",
"description": "Url for image to preview model"
},
- "base": {
- "type": "string",
- "const": "any",
- "title": "Base",
- "default": "any"
- },
"type": {
"type": "string",
- "const": "spandrel_image_to_image",
+ "const": "embedding",
"title": "Type",
- "default": "spandrel_image_to_image"
+ "default": "embedding"
},
"format": {
"type": "string",
- "const": "checkpoint",
+ "const": "embedding_folder",
"title": "Format",
- "default": "checkpoint"
+ "default": "embedding_folder"
+ },
+ "base": {
+ "type": "string",
+ "const": "sd-1",
+ "title": "Base",
+ "default": "sd-1"
}
},
"type": "object",
@@ -67452,338 +73588,274 @@
"source_api_response",
"source_url",
"cover_image",
- "base",
"type",
- "format"
+ "format",
+ "base"
],
- "title": "Spandrel_Checkpoint_Config",
- "description": "Model config for Spandrel Image to Image models."
- },
- "StarredImagesResult": {
- "properties": {
- "affected_boards": {
- "items": {
- "type": "string"
- },
- "type": "array",
- "title": "Affected Boards",
- "description": "The ids of boards affected by the delete operation"
- },
- "starred_images": {
- "items": {
- "type": "string"
- },
- "type": "array",
- "title": "Starred Images",
- "description": "The names of the images that were starred"
- }
- },
- "type": "object",
- "required": ["affected_boards", "starred_images"],
- "title": "StarredImagesResult"
+ "title": "TI_Folder_SD1_Config"
},
- "StarterModel": {
+ "TI_Folder_SD2_Config": {
"properties": {
- "description": {
+ "key": {
"type": "string",
- "title": "Description"
+ "title": "Key",
+ "description": "A unique key for this model."
},
- "source": {
+ "hash": {
"type": "string",
- "title": "Source"
+ "title": "Hash",
+ "description": "The hash of the model file(s)."
},
- "name": {
+ "path": {
"type": "string",
- "title": "Name"
- },
- "base": {
- "$ref": "#/components/schemas/BaseModelType"
+ "title": "Path",
+ "description": "Path to the model on the filesystem. Relative paths are relative to the Invoke root directory."
},
- "type": {
- "$ref": "#/components/schemas/ModelType"
+ "file_size": {
+ "type": "integer",
+ "title": "File Size",
+ "description": "The size of the model in bytes."
},
- "format": {
- "anyOf": [
- {
- "$ref": "#/components/schemas/ModelFormat"
- },
- {
- "type": "null"
- }
- ]
+ "name": {
+ "type": "string",
+ "title": "Name",
+ "description": "Name of the model."
},
- "variant": {
+ "description": {
"anyOf": [
{
- "$ref": "#/components/schemas/ModelVariantType"
- },
- {
- "$ref": "#/components/schemas/ClipVariantType"
- },
- {
- "$ref": "#/components/schemas/FluxVariantType"
- },
- {
- "$ref": "#/components/schemas/Flux2VariantType"
- },
- {
- "$ref": "#/components/schemas/ZImageVariantType"
- },
- {
- "$ref": "#/components/schemas/QwenImageVariantType"
- },
- {
- "$ref": "#/components/schemas/Qwen3VariantType"
+ "type": "string"
},
{
"type": "null"
}
],
- "title": "Variant"
+ "title": "Description",
+ "description": "Model description"
},
- "is_installed": {
- "type": "boolean",
- "title": "Is Installed",
- "default": false
+ "source": {
+ "type": "string",
+ "title": "Source",
+ "description": "The original source of the model (path, URL or repo_id)."
},
- "capabilities": {
- "anyOf": [
- {
- "$ref": "#/components/schemas/ExternalModelCapabilities"
- },
- {
- "type": "null"
- }
- ]
+ "source_type": {
+ "$ref": "#/components/schemas/ModelSourceType",
+ "description": "The type of source"
},
- "default_settings": {
+ "source_api_response": {
"anyOf": [
{
- "$ref": "#/components/schemas/ExternalApiModelDefaultSettings"
+ "type": "string"
},
{
"type": "null"
}
- ]
+ ],
+ "title": "Source Api Response",
+ "description": "The original API response from the source, as stringified JSON."
},
- "panel_schema": {
+ "source_url": {
"anyOf": [
{
- "$ref": "#/components/schemas/ExternalModelPanelSchema"
+ "type": "string"
},
{
"type": "null"
}
- ]
- },
- "previous_names": {
- "items": {
- "type": "string"
- },
- "type": "array",
- "title": "Previous Names",
- "default": []
+ ],
+ "title": "Source Url",
+ "description": "Optional URL for the model (e.g. download page or model page)."
},
- "dependencies": {
+ "cover_image": {
"anyOf": [
{
- "items": {
- "$ref": "#/components/schemas/StarterModelWithoutDependencies"
- },
- "type": "array"
+ "type": "string"
},
{
"type": "null"
}
],
- "title": "Dependencies"
- }
- },
- "type": "object",
- "required": ["description", "source", "name", "base", "type"],
- "title": "StarterModel"
- },
- "StarterModelBundle": {
- "properties": {
- "name": {
+ "title": "Cover Image",
+ "description": "Url for image to preview model"
+ },
+ "type": {
"type": "string",
- "title": "Name"
+ "const": "embedding",
+ "title": "Type",
+ "default": "embedding"
},
- "models": {
- "items": {
- "$ref": "#/components/schemas/StarterModel"
- },
- "type": "array",
- "title": "Models"
- }
- },
- "type": "object",
- "required": ["name", "models"],
- "title": "StarterModelBundle"
- },
- "StarterModelResponse": {
- "properties": {
- "starter_models": {
- "items": {
- "$ref": "#/components/schemas/StarterModel"
- },
- "type": "array",
- "title": "Starter Models"
+ "format": {
+ "type": "string",
+ "const": "embedding_folder",
+ "title": "Format",
+ "default": "embedding_folder"
},
- "starter_bundles": {
- "additionalProperties": {
- "$ref": "#/components/schemas/StarterModelBundle"
- },
- "type": "object",
- "title": "Starter Bundles"
+ "base": {
+ "type": "string",
+ "const": "sd-2",
+ "title": "Base",
+ "default": "sd-2"
}
},
"type": "object",
- "required": ["starter_models", "starter_bundles"],
- "title": "StarterModelResponse"
+ "required": [
+ "key",
+ "hash",
+ "path",
+ "file_size",
+ "name",
+ "description",
+ "source",
+ "source_type",
+ "source_api_response",
+ "source_url",
+ "cover_image",
+ "type",
+ "format",
+ "base"
+ ],
+ "title": "TI_Folder_SD2_Config"
},
- "StarterModelWithoutDependencies": {
+ "TI_Folder_SDXL_Config": {
"properties": {
- "description": {
+ "key": {
"type": "string",
- "title": "Description"
+ "title": "Key",
+ "description": "A unique key for this model."
},
- "source": {
+ "hash": {
"type": "string",
- "title": "Source"
+ "title": "Hash",
+ "description": "The hash of the model file(s)."
},
- "name": {
+ "path": {
"type": "string",
- "title": "Name"
- },
- "base": {
- "$ref": "#/components/schemas/BaseModelType"
+ "title": "Path",
+ "description": "Path to the model on the filesystem. Relative paths are relative to the Invoke root directory."
},
- "type": {
- "$ref": "#/components/schemas/ModelType"
+ "file_size": {
+ "type": "integer",
+ "title": "File Size",
+ "description": "The size of the model in bytes."
},
- "format": {
- "anyOf": [
- {
- "$ref": "#/components/schemas/ModelFormat"
- },
- {
- "type": "null"
- }
- ]
+ "name": {
+ "type": "string",
+ "title": "Name",
+ "description": "Name of the model."
},
- "variant": {
+ "description": {
"anyOf": [
{
- "$ref": "#/components/schemas/ModelVariantType"
- },
- {
- "$ref": "#/components/schemas/ClipVariantType"
- },
- {
- "$ref": "#/components/schemas/FluxVariantType"
- },
- {
- "$ref": "#/components/schemas/Flux2VariantType"
- },
- {
- "$ref": "#/components/schemas/ZImageVariantType"
- },
- {
- "$ref": "#/components/schemas/QwenImageVariantType"
- },
- {
- "$ref": "#/components/schemas/Qwen3VariantType"
+ "type": "string"
},
{
"type": "null"
}
],
- "title": "Variant"
+ "title": "Description",
+ "description": "Model description"
},
- "is_installed": {
- "type": "boolean",
- "title": "Is Installed",
- "default": false
+ "source": {
+ "type": "string",
+ "title": "Source",
+ "description": "The original source of the model (path, URL or repo_id)."
},
- "capabilities": {
+ "source_type": {
+ "$ref": "#/components/schemas/ModelSourceType",
+ "description": "The type of source"
+ },
+ "source_api_response": {
"anyOf": [
{
- "$ref": "#/components/schemas/ExternalModelCapabilities"
+ "type": "string"
},
{
"type": "null"
}
- ]
+ ],
+ "title": "Source Api Response",
+ "description": "The original API response from the source, as stringified JSON."
},
- "default_settings": {
+ "source_url": {
"anyOf": [
{
- "$ref": "#/components/schemas/ExternalApiModelDefaultSettings"
+ "type": "string"
},
{
"type": "null"
}
- ]
+ ],
+ "title": "Source Url",
+ "description": "Optional URL for the model (e.g. download page or model page)."
},
- "panel_schema": {
+ "cover_image": {
"anyOf": [
{
- "$ref": "#/components/schemas/ExternalModelPanelSchema"
+ "type": "string"
},
{
"type": "null"
}
- ]
+ ],
+ "title": "Cover Image",
+ "description": "Url for image to preview model"
},
- "previous_names": {
- "items": {
- "type": "string"
- },
- "type": "array",
- "title": "Previous Names",
- "default": []
+ "type": {
+ "type": "string",
+ "const": "embedding",
+ "title": "Type",
+ "default": "embedding"
+ },
+ "format": {
+ "type": "string",
+ "const": "embedding_folder",
+ "title": "Format",
+ "default": "embedding_folder"
+ },
+ "base": {
+ "type": "string",
+ "const": "sdxl",
+ "title": "Base",
+ "default": "sdxl"
}
},
"type": "object",
- "required": ["description", "source", "name", "base", "type"],
- "title": "StarterModelWithoutDependencies"
+ "required": [
+ "key",
+ "hash",
+ "path",
+ "file_size",
+ "name",
+ "description",
+ "source",
+ "source_type",
+ "source_api_response",
+ "source_url",
+ "cover_image",
+ "type",
+ "format",
+ "base"
+ ],
+ "title": "TI_Folder_SDXL_Config"
},
- "String2Output": {
- "class": "output",
- "description": "Base class for invocations that output two strings",
+ "TensorField": {
+ "description": "A tensor primitive field.",
"properties": {
- "string_1": {
- "description": "string 1",
- "field_kind": "output",
- "title": "String 1",
- "type": "string",
- "ui_hidden": false
- },
- "string_2": {
- "description": "string 2",
- "field_kind": "output",
- "title": "String 2",
- "type": "string",
- "ui_hidden": false
- },
- "type": {
- "const": "string_2_output",
- "default": "string_2_output",
- "field_kind": "node_attribute",
- "title": "type",
+ "tensor_name": {
+ "description": "The name of a tensor.",
+ "title": "Tensor Name",
"type": "string"
}
},
- "required": ["output_meta", "string_1", "string_2", "type", "type"],
- "title": "String2Output",
+ "required": ["tensor_name"],
+ "title": "TensorField",
"type": "object"
},
- "StringBatchInvocation": {
- "category": "batch",
+ "TextLLMInvocation": {
+ "category": "llm",
"class": "invocation",
- "classification": "special",
- "description": "Create a batched generation, where the workflow is executed once for each string in the batch.",
+ "classification": "beta",
+ "description": "Run a text language model to generate or expand text (e.g. for prompt expansion).",
"node_pack": "invokeai",
"properties": {
"id": {
@@ -67810,146 +73882,235 @@
"title": "Use Cache",
"type": "boolean"
},
- "batch_group_id": {
- "default": "None",
- "description": "The ID of this batch node's group. If provided, all batch nodes in with the same ID will be 'zipped' before execution, and all nodes' collections must be of the same size.",
- "enum": ["None", "Group 1", "Group 2", "Group 3", "Group 4", "Group 5"],
+ "prompt": {
+ "default": "",
+ "description": "Input text prompt.",
"field_kind": "input",
- "input": "direct",
- "orig_default": "None",
+ "input": "any",
+ "orig_default": "",
"orig_required": false,
- "title": "Batch Group",
- "type": "string"
+ "title": "Prompt",
+ "type": "string",
+ "ui_component": "textarea"
},
- "strings": {
+ "system_prompt": {
+ "default": "You are an expert prompt writer for AI image generation. Given a brief description, expand it into a detailed, vivid prompt suitable for generating high-quality images. Only output the expanded prompt, nothing else.",
+ "description": "System prompt that guides the model's behavior.",
+ "field_kind": "input",
+ "input": "any",
+ "orig_default": "You are an expert prompt writer for AI image generation. Given a brief description, expand it into a detailed, vivid prompt suitable for generating high-quality images. Only output the expanded prompt, nothing else.",
+ "orig_required": false,
+ "title": "System Prompt",
+ "type": "string",
+ "ui_component": "textarea"
+ },
+ "text_llm_model": {
"anyOf": [
{
- "items": {
- "type": "string"
- },
- "minItems": 1,
- "type": "array"
+ "$ref": "#/components/schemas/ModelIdentifierField"
},
{
"type": "null"
}
],
"default": null,
- "description": "The strings to batch over",
+ "description": "The text language model to use for text generation",
"field_kind": "input",
"input": "any",
"orig_required": true,
- "title": "Strings"
+ "title": "Text LLM Model",
+ "ui_model_type": ["text_llm"]
+ },
+ "max_tokens": {
+ "default": 300,
+ "description": "Maximum number of tokens to generate.",
+ "field_kind": "input",
+ "input": "any",
+ "maximum": 2048,
+ "minimum": 1,
+ "orig_default": 300,
+ "orig_required": false,
+ "title": "Max Tokens",
+ "type": "integer"
},
"type": {
- "const": "string_batch",
- "default": "string_batch",
+ "const": "text_llm",
+ "default": "text_llm",
"field_kind": "node_attribute",
"title": "type",
"type": "string"
}
},
"required": ["type", "id"],
- "tags": ["primitives", "string", "batch", "special"],
- "title": "String Batch",
+ "tags": ["llm", "text", "prompt"],
+ "title": "Text LLM",
"type": "object",
"version": "1.0.0",
"output": {
"$ref": "#/components/schemas/StringOutput"
}
},
- "StringCollectionInvocation": {
- "category": "primitives",
- "class": "invocation",
- "classification": "stable",
- "description": "A collection of string primitive values",
- "node_pack": "invokeai",
+ "TextLLM_Diffusers_Config": {
"properties": {
- "id": {
- "description": "The id of this instance of an invocation. Must be unique among all instances of invocations.",
- "field_kind": "node_attribute",
- "title": "Id",
- "type": "string"
+ "key": {
+ "type": "string",
+ "title": "Key",
+ "description": "A unique key for this model."
},
- "is_intermediate": {
- "default": false,
- "description": "Whether or not this is an intermediate invocation.",
- "field_kind": "node_attribute",
- "input": "direct",
- "orig_required": true,
- "title": "Is Intermediate",
- "type": "boolean",
- "ui_hidden": false,
- "ui_type": "IsIntermediate"
+ "hash": {
+ "type": "string",
+ "title": "Hash",
+ "description": "The hash of the model file(s)."
},
- "use_cache": {
- "default": true,
- "description": "Whether or not to use the cache",
- "field_kind": "node_attribute",
- "title": "Use Cache",
- "type": "boolean"
+ "path": {
+ "type": "string",
+ "title": "Path",
+ "description": "Path to the model on the filesystem. Relative paths are relative to the Invoke root directory."
},
- "collection": {
- "default": [],
- "description": "The collection of string values",
- "field_kind": "input",
- "input": "any",
- "items": {
- "type": "string"
- },
- "orig_default": [],
- "orig_required": false,
- "title": "Collection",
- "type": "array"
+ "file_size": {
+ "type": "integer",
+ "title": "File Size",
+ "description": "The size of the model in bytes."
+ },
+ "name": {
+ "type": "string",
+ "title": "Name",
+ "description": "Name of the model."
+ },
+ "description": {
+ "anyOf": [
+ {
+ "type": "string"
+ },
+ {
+ "type": "null"
+ }
+ ],
+ "title": "Description",
+ "description": "Model description"
+ },
+ "source": {
+ "type": "string",
+ "title": "Source",
+ "description": "The original source of the model (path, URL or repo_id)."
+ },
+ "source_type": {
+ "$ref": "#/components/schemas/ModelSourceType",
+ "description": "The type of source"
+ },
+ "source_api_response": {
+ "anyOf": [
+ {
+ "type": "string"
+ },
+ {
+ "type": "null"
+ }
+ ],
+ "title": "Source Api Response",
+ "description": "The original API response from the source, as stringified JSON."
+ },
+ "source_url": {
+ "anyOf": [
+ {
+ "type": "string"
+ },
+ {
+ "type": "null"
+ }
+ ],
+ "title": "Source Url",
+ "description": "Optional URL for the model (e.g. download page or model page)."
+ },
+ "cover_image": {
+ "anyOf": [
+ {
+ "type": "string"
+ },
+ {
+ "type": "null"
+ }
+ ],
+ "title": "Cover Image",
+ "description": "Url for image to preview model"
+ },
+ "format": {
+ "type": "string",
+ "const": "diffusers",
+ "title": "Format",
+ "default": "diffusers"
+ },
+ "repo_variant": {
+ "$ref": "#/components/schemas/ModelRepoVariant",
+ "default": ""
},
"type": {
- "const": "string_collection",
- "default": "string_collection",
- "field_kind": "node_attribute",
- "title": "type",
- "type": "string"
+ "type": "string",
+ "const": "text_llm",
+ "title": "Type",
+ "default": "text_llm"
+ },
+ "base": {
+ "type": "string",
+ "const": "any",
+ "title": "Base",
+ "default": "any"
+ },
+ "cpu_only": {
+ "anyOf": [
+ {
+ "type": "boolean"
+ },
+ {
+ "type": "null"
+ }
+ ],
+ "title": "Cpu Only",
+ "description": "Whether this model should run on CPU only"
}
},
- "required": ["type", "id"],
- "tags": ["primitives", "string", "collection"],
- "title": "String Collection Primitive",
"type": "object",
- "version": "1.0.2",
- "output": {
- "$ref": "#/components/schemas/StringCollectionOutput"
- }
+ "required": [
+ "key",
+ "hash",
+ "path",
+ "file_size",
+ "name",
+ "description",
+ "source",
+ "source_type",
+ "source_api_response",
+ "source_url",
+ "cover_image",
+ "format",
+ "repo_variant",
+ "type",
+ "base",
+ "cpu_only"
+ ],
+ "title": "TextLLM_Diffusers_Config",
+ "description": "Model config for text-only causal language models (e.g. Llama, Phi, Qwen, Mistral)."
},
- "StringCollectionOutput": {
- "class": "output",
- "description": "Base class for nodes that output a collection of strings",
+ "Tile": {
"properties": {
- "collection": {
- "description": "The output strings",
- "field_kind": "output",
- "items": {
- "type": "string"
- },
- "title": "Collection",
- "type": "array",
- "ui_hidden": false
+ "coords": {
+ "$ref": "#/components/schemas/TBLR",
+ "description": "The coordinates of this tile relative to its parent image."
},
- "type": {
- "const": "string_collection_output",
- "default": "string_collection_output",
- "field_kind": "node_attribute",
- "title": "type",
- "type": "string"
+ "overlap": {
+ "$ref": "#/components/schemas/TBLR",
+ "description": "The amount of overlap with adjacent tiles on each side of this tile."
}
},
- "required": ["output_meta", "collection", "type", "type"],
- "title": "StringCollectionOutput",
+ "required": ["coords", "overlap"],
+ "title": "Tile",
"type": "object"
},
- "StringGenerator": {
- "category": "batch",
+ "TileToPropertiesInvocation": {
+ "category": "tiles",
"class": "invocation",
- "classification": "special",
- "description": "Generated a range of strings for use in a batched generation",
+ "classification": "stable",
+ "description": "Split a Tile into its individual properties.",
"node_pack": "invokeai",
"properties": {
"id": {
@@ -67976,126 +74137,155 @@
"title": "Use Cache",
"type": "boolean"
},
- "generator": {
- "$ref": "#/components/schemas/StringGeneratorField",
- "description": "The string generator.",
+ "tile": {
+ "anyOf": [
+ {
+ "$ref": "#/components/schemas/Tile"
+ },
+ {
+ "type": "null"
+ }
+ ],
+ "default": null,
+ "description": "The tile to split into properties.",
"field_kind": "input",
- "input": "direct",
- "orig_required": true,
- "title": "Generator Type"
+ "input": "any",
+ "orig_required": true
},
"type": {
- "const": "string_generator",
- "default": "string_generator",
+ "const": "tile_to_properties",
+ "default": "tile_to_properties",
"field_kind": "node_attribute",
"title": "type",
"type": "string"
}
},
- "required": ["generator", "type", "id"],
- "tags": ["primitives", "string", "number", "batch", "special"],
- "title": "String Generator",
+ "required": ["type", "id"],
+ "tags": ["tiles"],
+ "title": "Tile to Properties",
"type": "object",
- "version": "1.0.0",
+ "version": "1.0.1",
"output": {
- "$ref": "#/components/schemas/StringGeneratorOutput"
+ "$ref": "#/components/schemas/TileToPropertiesOutput"
}
},
- "StringGeneratorField": {
- "properties": {},
- "title": "StringGeneratorField",
- "type": "object"
- },
- "StringGeneratorOutput": {
+ "TileToPropertiesOutput": {
"class": "output",
- "description": "Base class for nodes that output a collection of strings",
"properties": {
- "strings": {
- "description": "The generated strings",
+ "coords_left": {
+ "description": "Left coordinate of the tile relative to its parent image.",
"field_kind": "output",
- "items": {
- "type": "string"
- },
- "title": "Strings",
- "type": "array",
+ "title": "Coords Left",
+ "type": "integer",
+ "ui_hidden": false
+ },
+ "coords_right": {
+ "description": "Right coordinate of the tile relative to its parent image.",
+ "field_kind": "output",
+ "title": "Coords Right",
+ "type": "integer",
+ "ui_hidden": false
+ },
+ "coords_top": {
+ "description": "Top coordinate of the tile relative to its parent image.",
+ "field_kind": "output",
+ "title": "Coords Top",
+ "type": "integer",
+ "ui_hidden": false
+ },
+ "coords_bottom": {
+ "description": "Bottom coordinate of the tile relative to its parent image.",
+ "field_kind": "output",
+ "title": "Coords Bottom",
+ "type": "integer",
+ "ui_hidden": false
+ },
+ "width": {
+ "description": "The width of the tile. Equal to coords_right - coords_left.",
+ "field_kind": "output",
+ "title": "Width",
+ "type": "integer",
+ "ui_hidden": false
+ },
+ "height": {
+ "description": "The height of the tile. Equal to coords_bottom - coords_top.",
+ "field_kind": "output",
+ "title": "Height",
+ "type": "integer",
+ "ui_hidden": false
+ },
+ "overlap_top": {
+ "description": "Overlap between this tile and its top neighbor.",
+ "field_kind": "output",
+ "title": "Overlap Top",
+ "type": "integer",
+ "ui_hidden": false
+ },
+ "overlap_bottom": {
+ "description": "Overlap between this tile and its bottom neighbor.",
+ "field_kind": "output",
+ "title": "Overlap Bottom",
+ "type": "integer",
+ "ui_hidden": false
+ },
+ "overlap_left": {
+ "description": "Overlap between this tile and its left neighbor.",
+ "field_kind": "output",
+ "title": "Overlap Left",
+ "type": "integer",
+ "ui_hidden": false
+ },
+ "overlap_right": {
+ "description": "Overlap between this tile and its right neighbor.",
+ "field_kind": "output",
+ "title": "Overlap Right",
+ "type": "integer",
"ui_hidden": false
},
"type": {
- "const": "string_generator_output",
- "default": "string_generator_output",
+ "const": "tile_to_properties_output",
+ "default": "tile_to_properties_output",
"field_kind": "node_attribute",
"title": "type",
"type": "string"
}
},
- "required": ["output_meta", "strings", "type", "type"],
- "title": "StringGeneratorOutput",
- "type": "object"
- },
- "StringInvocation": {
- "category": "primitives",
- "class": "invocation",
- "classification": "stable",
- "description": "A string primitive value",
- "node_pack": "invokeai",
- "properties": {
- "id": {
- "description": "The id of this instance of an invocation. Must be unique among all instances of invocations.",
- "field_kind": "node_attribute",
- "title": "Id",
- "type": "string"
- },
- "is_intermediate": {
- "default": false,
- "description": "Whether or not this is an intermediate invocation.",
- "field_kind": "node_attribute",
- "input": "direct",
- "orig_required": true,
- "title": "Is Intermediate",
- "type": "boolean",
- "ui_hidden": false,
- "ui_type": "IsIntermediate"
- },
- "use_cache": {
- "default": true,
- "description": "Whether or not to use the cache",
- "field_kind": "node_attribute",
- "title": "Use Cache",
- "type": "boolean"
- },
- "value": {
- "default": "",
- "description": "The string value",
- "field_kind": "input",
- "input": "any",
- "orig_default": "",
- "orig_required": false,
- "title": "Value",
- "type": "string",
- "ui_component": "textarea"
+ "required": [
+ "output_meta",
+ "coords_left",
+ "coords_right",
+ "coords_top",
+ "coords_bottom",
+ "width",
+ "height",
+ "overlap_top",
+ "overlap_bottom",
+ "overlap_left",
+ "overlap_right",
+ "type",
+ "type"
+ ],
+ "title": "TileToPropertiesOutput",
+ "type": "object"
+ },
+ "TileWithImage": {
+ "properties": {
+ "tile": {
+ "$ref": "#/components/schemas/Tile"
},
- "type": {
- "const": "string",
- "default": "string",
- "field_kind": "node_attribute",
- "title": "type",
- "type": "string"
+ "image": {
+ "$ref": "#/components/schemas/ImageField"
}
},
- "required": ["type", "id"],
- "tags": ["primitives", "string"],
- "title": "String Primitive",
- "type": "object",
- "version": "1.0.1",
- "output": {
- "$ref": "#/components/schemas/StringOutput"
- }
+ "required": ["tile", "image"],
+ "title": "TileWithImage",
+ "type": "object"
},
- "StringJoinInvocation": {
- "category": "strings",
+ "TiledMultiDiffusionDenoiseLatents": {
+ "category": "latents",
"class": "invocation",
"classification": "stable",
- "description": "Joins string left to string right",
+ "description": "Tiled Multi-Diffusion denoising.\n\nThis node handles automatically tiling the input image, and is primarily intended for global refinement of images\nin tiled upscaling workflows. Future Multi-Diffusion nodes should allow the user to specify custom regions with\ndifferent parameters for each region to harness the full power of Multi-Diffusion.\n\nThis node has a similar interface to the `DenoiseLatents` node, but it has a reduced feature set (no IP-Adapter,\nT2I-Adapter, masking, etc.).",
"node_pack": "invokeai",
"properties": {
"id": {
@@ -68122,443 +74312,651 @@
"title": "Use Cache",
"type": "boolean"
},
- "string_left": {
- "default": "",
- "description": "String Left",
+ "positive_conditioning": {
+ "anyOf": [
+ {
+ "$ref": "#/components/schemas/ConditioningField"
+ },
+ {
+ "type": "null"
+ }
+ ],
+ "default": null,
+ "description": "Positive conditioning tensor",
+ "field_kind": "input",
+ "input": "connection",
+ "orig_required": true
+ },
+ "negative_conditioning": {
+ "anyOf": [
+ {
+ "$ref": "#/components/schemas/ConditioningField"
+ },
+ {
+ "type": "null"
+ }
+ ],
+ "default": null,
+ "description": "Negative conditioning tensor",
+ "field_kind": "input",
+ "input": "connection",
+ "orig_required": true
+ },
+ "noise": {
+ "anyOf": [
+ {
+ "$ref": "#/components/schemas/LatentsField"
+ },
+ {
+ "type": "null"
+ }
+ ],
+ "default": null,
+ "description": "Noise tensor",
+ "field_kind": "input",
+ "input": "connection",
+ "orig_default": null,
+ "orig_required": false
+ },
+ "latents": {
+ "anyOf": [
+ {
+ "$ref": "#/components/schemas/LatentsField"
+ },
+ {
+ "type": "null"
+ }
+ ],
+ "default": null,
+ "description": "Latents tensor",
+ "field_kind": "input",
+ "input": "connection",
+ "orig_default": null,
+ "orig_required": false
+ },
+ "tile_height": {
+ "default": 1024,
+ "description": "Height of the tiles in image space.",
+ "exclusiveMinimum": 0,
"field_kind": "input",
"input": "any",
- "orig_default": "",
+ "multipleOf": 8,
+ "orig_default": 1024,
"orig_required": false,
- "title": "String Left",
- "type": "string",
- "ui_component": "textarea"
+ "title": "Tile Height",
+ "type": "integer"
},
- "string_right": {
- "default": "",
- "description": "String Right",
+ "tile_width": {
+ "default": 1024,
+ "description": "Width of the tiles in image space.",
+ "exclusiveMinimum": 0,
"field_kind": "input",
"input": "any",
- "orig_default": "",
+ "multipleOf": 8,
+ "orig_default": 1024,
"orig_required": false,
- "title": "String Right",
+ "title": "Tile Width",
+ "type": "integer"
+ },
+ "tile_overlap": {
+ "default": 32,
+ "description": "The overlap between adjacent tiles in pixel space. (Of course, tile merging is applied in latent space.) Tiles will be cropped during merging (if necessary) to ensure that they overlap by exactly this amount.",
+ "exclusiveMinimum": 0,
+ "field_kind": "input",
+ "input": "any",
+ "multipleOf": 8,
+ "orig_default": 32,
+ "orig_required": false,
+ "title": "Tile Overlap",
+ "type": "integer"
+ },
+ "steps": {
+ "default": 18,
+ "description": "Number of steps to run",
+ "exclusiveMinimum": 0,
+ "field_kind": "input",
+ "input": "any",
+ "orig_default": 18,
+ "orig_required": false,
+ "title": "Steps",
+ "type": "integer"
+ },
+ "cfg_scale": {
+ "anyOf": [
+ {
+ "type": "number"
+ },
+ {
+ "items": {
+ "type": "number"
+ },
+ "type": "array"
+ }
+ ],
+ "default": 6.0,
+ "description": "Classifier-Free Guidance scale",
+ "field_kind": "input",
+ "input": "any",
+ "orig_default": 6.0,
+ "orig_required": false,
+ "title": "CFG Scale"
+ },
+ "denoising_start": {
+ "default": 0.0,
+ "description": "When to start denoising, expressed a percentage of total steps",
+ "field_kind": "input",
+ "input": "any",
+ "maximum": 1,
+ "minimum": 0,
+ "orig_default": 0.0,
+ "orig_required": false,
+ "title": "Denoising Start",
+ "type": "number"
+ },
+ "denoising_end": {
+ "default": 1.0,
+ "description": "When to stop denoising, expressed a percentage of total steps",
+ "field_kind": "input",
+ "input": "any",
+ "maximum": 1,
+ "minimum": 0,
+ "orig_default": 1.0,
+ "orig_required": false,
+ "title": "Denoising End",
+ "type": "number"
+ },
+ "scheduler": {
+ "default": "euler",
+ "description": "Scheduler to use during inference",
+ "enum": [
+ "ddim",
+ "ddpm",
+ "deis",
+ "deis_k",
+ "lms",
+ "lms_k",
+ "pndm",
+ "heun",
+ "heun_k",
+ "euler",
+ "euler_k",
+ "euler_a",
+ "kdpm_2",
+ "kdpm_2_k",
+ "kdpm_2_a",
+ "kdpm_2_a_k",
+ "dpmpp_2s",
+ "dpmpp_2s_k",
+ "dpmpp_2m",
+ "dpmpp_2m_k",
+ "dpmpp_2m_sde",
+ "dpmpp_2m_sde_k",
+ "dpmpp_3m",
+ "dpmpp_3m_k",
+ "dpmpp_sde",
+ "dpmpp_sde_k",
+ "er_sde",
+ "unipc",
+ "unipc_k",
+ "lcm",
+ "tcd"
+ ],
+ "field_kind": "input",
+ "input": "any",
+ "orig_default": "euler",
+ "orig_required": false,
+ "title": "Scheduler",
"type": "string",
- "ui_component": "textarea"
+ "ui_type": "SchedulerField"
+ },
+ "unet": {
+ "anyOf": [
+ {
+ "$ref": "#/components/schemas/UNetField"
+ },
+ {
+ "type": "null"
+ }
+ ],
+ "default": null,
+ "description": "UNet (scheduler, LoRAs)",
+ "field_kind": "input",
+ "input": "connection",
+ "orig_required": true,
+ "title": "UNet"
+ },
+ "cfg_rescale_multiplier": {
+ "default": 0,
+ "description": "Rescale multiplier for CFG guidance, used for models trained with zero-terminal SNR",
+ "exclusiveMaximum": 1,
+ "field_kind": "input",
+ "input": "any",
+ "minimum": 0,
+ "orig_default": 0,
+ "orig_required": false,
+ "title": "CFG Rescale Multiplier",
+ "type": "number"
+ },
+ "control": {
+ "anyOf": [
+ {
+ "$ref": "#/components/schemas/ControlField"
+ },
+ {
+ "items": {
+ "$ref": "#/components/schemas/ControlField"
+ },
+ "type": "array"
+ },
+ {
+ "type": "null"
+ }
+ ],
+ "default": null,
+ "field_kind": "input",
+ "input": "connection",
+ "orig_default": null,
+ "orig_required": false,
+ "title": "Control"
},
"type": {
- "const": "string_join",
- "default": "string_join",
+ "const": "tiled_multi_diffusion_denoise_latents",
+ "default": "tiled_multi_diffusion_denoise_latents",
"field_kind": "node_attribute",
"title": "type",
"type": "string"
}
},
"required": ["type", "id"],
- "tags": ["string", "join"],
- "title": "String Join",
+ "tags": ["upscale", "denoise"],
+ "title": "Tiled Multi-Diffusion Denoise - SD1.5, SDXL",
"type": "object",
"version": "1.0.1",
"output": {
- "$ref": "#/components/schemas/StringOutput"
+ "$ref": "#/components/schemas/LatentsOutput"
}
},
- "StringJoinThreeInvocation": {
- "category": "strings",
- "class": "invocation",
- "classification": "stable",
- "description": "Joins string left to string middle to string right",
- "node_pack": "invokeai",
+ "TransformerField": {
"properties": {
- "id": {
- "description": "The id of this instance of an invocation. Must be unique among all instances of invocations.",
- "field_kind": "node_attribute",
- "title": "Id",
- "type": "string"
+ "transformer": {
+ "$ref": "#/components/schemas/ModelIdentifierField",
+ "description": "Info to load Transformer submodel"
},
- "is_intermediate": {
- "default": false,
- "description": "Whether or not this is an intermediate invocation.",
- "field_kind": "node_attribute",
- "input": "direct",
- "orig_required": true,
- "title": "Is Intermediate",
- "type": "boolean",
- "ui_hidden": false,
- "ui_type": "IsIntermediate"
+ "loras": {
+ "description": "LoRAs to apply on model loading",
+ "items": {
+ "$ref": "#/components/schemas/LoRAField"
+ },
+ "title": "Loras",
+ "type": "array"
+ }
+ },
+ "required": ["transformer", "loras"],
+ "title": "TransformerField",
+ "type": "object"
+ },
+ "UIComponent": {
+ "description": "The type of UI component to use for a field, used to override the default components, which are\ninferred from the field type.",
+ "enum": ["none", "textarea", "slider", "video-frame-index"],
+ "title": "UIComponent",
+ "type": "string"
+ },
+ "UIConfigBase": {
+ "description": "Provides additional node configuration to the UI.\nThis is used internally by the @invocation decorator logic. Do not use this directly.",
+ "properties": {
+ "tags": {
+ "anyOf": [
+ {
+ "items": {
+ "type": "string"
+ },
+ "type": "array"
+ },
+ {
+ "type": "null"
+ }
+ ],
+ "default": null,
+ "description": "The node's tags",
+ "title": "Tags"
},
- "use_cache": {
- "default": true,
- "description": "Whether or not to use the cache",
- "field_kind": "node_attribute",
- "title": "Use Cache",
- "type": "boolean"
+ "title": {
+ "anyOf": [
+ {
+ "type": "string"
+ },
+ {
+ "type": "null"
+ }
+ ],
+ "default": null,
+ "description": "The node's display name",
+ "title": "Title"
},
- "string_left": {
- "default": "",
- "description": "String Left",
- "field_kind": "input",
- "input": "any",
- "orig_default": "",
- "orig_required": false,
- "title": "String Left",
- "type": "string",
- "ui_component": "textarea"
+ "category": {
+ "anyOf": [
+ {
+ "type": "string"
+ },
+ {
+ "type": "null"
+ }
+ ],
+ "default": null,
+ "description": "The node's category",
+ "title": "Category"
},
- "string_middle": {
- "default": "",
- "description": "String Middle",
- "field_kind": "input",
- "input": "any",
- "orig_default": "",
- "orig_required": false,
- "title": "String Middle",
- "type": "string",
- "ui_component": "textarea"
+ "version": {
+ "description": "The node's version. Should be a valid semver string e.g. \"1.0.0\" or \"3.8.13\".",
+ "title": "Version",
+ "type": "string"
+ },
+ "node_pack": {
+ "description": "The node pack that this node belongs to, will be 'invokeai' for built-in nodes",
+ "title": "Node Pack",
+ "type": "string"
+ },
+ "classification": {
+ "$ref": "#/components/schemas/Classification",
+ "default": "stable",
+ "description": "The node's classification"
+ }
+ },
+ "required": ["tags", "title", "category", "version", "node_pack", "classification"],
+ "title": "UIConfigBase",
+ "type": "object"
+ },
+ "UIType": {
+ "description": "Type hints for the UI for situations in which the field type is not enough to infer the correct UI type.\n\n- Model Fields\nThe most common node-author-facing use will be for model fields. Internally, there is no difference\nbetween SD-1, SD-2 and SDXL model fields - they all use the class `MainModelField`. To ensure the\nbase-model-specific UI is rendered, use e.g. `ui_type=UIType.SDXLMainModelField` to indicate that\nthe field is an SDXL main model field.\n\n- Any Field\nWe cannot infer the usage of `typing.Any` via schema parsing, so you *must* use `ui_type=UIType.Any` to\nindicate that the field accepts any type. Use with caution. This cannot be used on outputs.\n\n- Scheduler Field\nSpecial handling in the UI is needed for this field, which otherwise would be parsed as a plain enum field.\n\n- Internal Fields\nSimilar to the Any Field, the `collect` and `iterate` nodes make use of `typing.Any`. To facilitate\nhandling these types in the client, we use `UIType._Collection` and `UIType._CollectionItem`. These\nshould not be used by node authors.\n\n- DEPRECATED Fields\nThese types are deprecated and should not be used by node authors. A warning will be logged if one is\nused, and the type will be ignored. They are included here for backwards compatibility.",
+ "enum": [
+ "SchedulerField",
+ "AnyField",
+ "SavedWorkflowField",
+ "CollectionField",
+ "CollectionItemField",
+ "IsIntermediate",
+ "DEPRECATED_Boolean",
+ "DEPRECATED_Color",
+ "DEPRECATED_Conditioning",
+ "DEPRECATED_Control",
+ "DEPRECATED_Float",
+ "DEPRECATED_Image",
+ "DEPRECATED_Integer",
+ "DEPRECATED_Latents",
+ "DEPRECATED_String",
+ "DEPRECATED_BooleanCollection",
+ "DEPRECATED_ColorCollection",
+ "DEPRECATED_ConditioningCollection",
+ "DEPRECATED_ControlCollection",
+ "DEPRECATED_FloatCollection",
+ "DEPRECATED_ImageCollection",
+ "DEPRECATED_IntegerCollection",
+ "DEPRECATED_LatentsCollection",
+ "DEPRECATED_StringCollection",
+ "DEPRECATED_BooleanPolymorphic",
+ "DEPRECATED_ColorPolymorphic",
+ "DEPRECATED_ConditioningPolymorphic",
+ "DEPRECATED_ControlPolymorphic",
+ "DEPRECATED_FloatPolymorphic",
+ "DEPRECATED_ImagePolymorphic",
+ "DEPRECATED_IntegerPolymorphic",
+ "DEPRECATED_LatentsPolymorphic",
+ "DEPRECATED_StringPolymorphic",
+ "DEPRECATED_UNet",
+ "DEPRECATED_Vae",
+ "DEPRECATED_CLIP",
+ "DEPRECATED_Collection",
+ "DEPRECATED_CollectionItem",
+ "DEPRECATED_Enum",
+ "DEPRECATED_WorkflowField",
+ "DEPRECATED_BoardField",
+ "DEPRECATED_MetadataItem",
+ "DEPRECATED_MetadataItemCollection",
+ "DEPRECATED_MetadataItemPolymorphic",
+ "DEPRECATED_MetadataDict",
+ "DEPRECATED_MainModelField",
+ "DEPRECATED_CogView4MainModelField",
+ "DEPRECATED_FluxMainModelField",
+ "DEPRECATED_SD3MainModelField",
+ "DEPRECATED_SDXLMainModelField",
+ "DEPRECATED_SDXLRefinerModelField",
+ "DEPRECATED_ONNXModelField",
+ "DEPRECATED_VAEModelField",
+ "DEPRECATED_FluxVAEModelField",
+ "DEPRECATED_LoRAModelField",
+ "DEPRECATED_ControlNetModelField",
+ "DEPRECATED_IPAdapterModelField",
+ "DEPRECATED_T2IAdapterModelField",
+ "DEPRECATED_T5EncoderModelField",
+ "DEPRECATED_CLIPEmbedModelField",
+ "DEPRECATED_CLIPLEmbedModelField",
+ "DEPRECATED_CLIPGEmbedModelField",
+ "DEPRECATED_SpandrelImageToImageModelField",
+ "DEPRECATED_ControlLoRAModelField",
+ "DEPRECATED_SigLipModelField",
+ "DEPRECATED_FluxReduxModelField",
+ "DEPRECATED_LLaVAModelField",
+ "DEPRECATED_Imagen3ModelField",
+ "DEPRECATED_Imagen4ModelField",
+ "DEPRECATED_ChatGPT4oModelField",
+ "DEPRECATED_Gemini2_5ModelField",
+ "DEPRECATED_FluxKontextModelField",
+ "DEPRECATED_Veo3ModelField",
+ "DEPRECATED_RunwayModelField"
+ ],
+ "title": "UIType",
+ "type": "string"
+ },
+ "UNetField": {
+ "properties": {
+ "unet": {
+ "$ref": "#/components/schemas/ModelIdentifierField",
+ "description": "Info to load unet submodel"
},
- "string_right": {
- "default": "",
- "description": "String Right",
- "field_kind": "input",
- "input": "any",
- "orig_default": "",
- "orig_required": false,
- "title": "String Right",
- "type": "string",
- "ui_component": "textarea"
+ "scheduler": {
+ "$ref": "#/components/schemas/ModelIdentifierField",
+ "description": "Info to load scheduler submodel"
},
- "type": {
- "const": "string_join_three",
- "default": "string_join_three",
- "field_kind": "node_attribute",
- "title": "type",
- "type": "string"
+ "loras": {
+ "description": "LoRAs to apply on model loading",
+ "items": {
+ "$ref": "#/components/schemas/LoRAField"
+ },
+ "title": "Loras",
+ "type": "array"
+ },
+ "seamless_axes": {
+ "description": "Axes(\"x\" and \"y\") to which apply seamless",
+ "items": {
+ "type": "string"
+ },
+ "title": "Seamless Axes",
+ "type": "array"
+ },
+ "freeu_config": {
+ "anyOf": [
+ {
+ "$ref": "#/components/schemas/FreeUConfig"
+ },
+ {
+ "type": "null"
+ }
+ ],
+ "default": null,
+ "description": "FreeU configuration"
}
},
- "required": ["type", "id"],
- "tags": ["string", "join"],
- "title": "String Join Three",
- "type": "object",
- "version": "1.0.1",
- "output": {
- "$ref": "#/components/schemas/StringOutput"
- }
+ "required": ["unet", "scheduler", "loras"],
+ "title": "UNetField",
+ "type": "object"
},
- "StringOutput": {
+ "UNetOutput": {
"class": "output",
- "description": "Base class for nodes that output a single string",
+ "description": "Base class for invocations that output a UNet field.",
"properties": {
- "value": {
- "description": "The output string",
+ "unet": {
+ "$ref": "#/components/schemas/UNetField",
+ "description": "UNet (scheduler, LoRAs)",
"field_kind": "output",
- "title": "Value",
- "type": "string",
+ "title": "UNet",
"ui_hidden": false
},
"type": {
- "const": "string_output",
- "default": "string_output",
+ "const": "unet_output",
+ "default": "unet_output",
"field_kind": "node_attribute",
"title": "type",
"type": "string"
}
},
- "required": ["output_meta", "value", "type", "type"],
- "title": "StringOutput",
+ "required": ["output_meta", "unet", "type", "type"],
+ "title": "UNetOutput",
"type": "object"
},
- "StringPosNegOutput": {
- "class": "output",
- "description": "Base class for invocations that output a positive and negative string",
+ "URLModelSource": {
"properties": {
- "positive_string": {
- "description": "Positive string",
- "field_kind": "output",
- "title": "Positive String",
+ "url": {
"type": "string",
- "ui_hidden": false
+ "minLength": 1,
+ "format": "uri",
+ "title": "Url"
},
- "negative_string": {
- "description": "Negative string",
- "field_kind": "output",
- "title": "Negative String",
- "type": "string",
- "ui_hidden": false
+ "access_token": {
+ "anyOf": [
+ {
+ "type": "string"
+ },
+ {
+ "type": "null"
+ }
+ ],
+ "title": "Access Token"
},
"type": {
- "const": "string_pos_neg_output",
- "default": "string_pos_neg_output",
- "field_kind": "node_attribute",
- "title": "type",
- "type": "string"
+ "type": "string",
+ "const": "url",
+ "title": "Type",
+ "default": "url"
}
},
- "required": ["output_meta", "positive_string", "negative_string", "type", "type"],
- "title": "StringPosNegOutput",
- "type": "object"
+ "type": "object",
+ "required": ["url"],
+ "title": "URLModelSource",
+ "description": "A generic URL point to a checkpoint file."
},
- "StringReplaceInvocation": {
- "category": "strings",
- "class": "invocation",
- "classification": "stable",
- "description": "Replaces the search string with the replace string",
- "node_pack": "invokeai",
+ "URLRegexTokenPair": {
"properties": {
- "id": {
- "description": "The id of this instance of an invocation. Must be unique among all instances of invocations.",
- "field_kind": "node_attribute",
- "title": "Id",
- "type": "string"
- },
- "is_intermediate": {
- "default": false,
- "description": "Whether or not this is an intermediate invocation.",
- "field_kind": "node_attribute",
- "input": "direct",
- "orig_required": true,
- "title": "Is Intermediate",
- "type": "boolean",
- "ui_hidden": false,
- "ui_type": "IsIntermediate"
- },
- "use_cache": {
- "default": true,
- "description": "Whether or not to use the cache",
- "field_kind": "node_attribute",
- "title": "Use Cache",
- "type": "boolean"
- },
- "string": {
- "default": "",
- "description": "String to work on",
- "field_kind": "input",
- "input": "any",
- "orig_default": "",
- "orig_required": false,
- "title": "String",
- "type": "string",
- "ui_component": "textarea"
- },
- "search_string": {
- "default": "",
- "description": "String to search for",
- "field_kind": "input",
- "input": "any",
- "orig_default": "",
- "orig_required": false,
- "title": "Search String",
+ "url_regex": {
"type": "string",
- "ui_component": "textarea"
+ "title": "Url Regex",
+ "description": "Regular expression to match against the URL"
},
- "replace_string": {
- "default": "",
- "description": "String to replace the search",
- "field_kind": "input",
- "input": "any",
- "orig_default": "",
- "orig_required": false,
- "title": "Replace String",
+ "token": {
"type": "string",
- "ui_component": "textarea"
- },
- "use_regex": {
- "default": false,
- "description": "Use search string as a regex expression (non regex is case insensitive)",
- "field_kind": "input",
- "input": "any",
- "orig_default": false,
- "orig_required": false,
- "title": "Use Regex",
- "type": "boolean"
- },
- "type": {
- "const": "string_replace",
- "default": "string_replace",
- "field_kind": "node_attribute",
- "title": "type",
- "type": "string"
+ "title": "Token",
+ "description": "Token to use when the URL matches the regex"
}
},
- "required": ["type", "id"],
- "tags": ["string", "replace", "regex"],
- "title": "String Replace",
"type": "object",
- "version": "1.0.1",
- "output": {
- "$ref": "#/components/schemas/StringOutput"
- }
+ "required": ["url_regex", "token"],
+ "title": "URLRegexTokenPair"
},
- "StringSplitInvocation": {
- "category": "strings",
- "class": "invocation",
- "classification": "stable",
- "description": "Splits string into two strings, based on the first occurance of the delimiter. The delimiter will be removed from the string",
- "node_pack": "invokeai",
+ "UninstallNodePackResponse": {
"properties": {
- "id": {
- "description": "The id of this instance of an invocation. Must be unique among all instances of invocations.",
- "field_kind": "node_attribute",
- "title": "Id",
- "type": "string"
+ "name": {
+ "type": "string",
+ "title": "Name",
+ "description": "The name of the uninstalled node pack."
},
- "is_intermediate": {
- "default": false,
- "description": "Whether or not this is an intermediate invocation.",
- "field_kind": "node_attribute",
- "input": "direct",
- "orig_required": true,
- "title": "Is Intermediate",
+ "success": {
"type": "boolean",
- "ui_hidden": false,
- "ui_type": "IsIntermediate"
- },
- "use_cache": {
- "default": true,
- "description": "Whether or not to use the cache",
- "field_kind": "node_attribute",
- "title": "Use Cache",
- "type": "boolean"
+ "title": "Success",
+ "description": "Whether the uninstall was successful."
},
- "string": {
- "default": "",
- "description": "String to split",
- "field_kind": "input",
- "input": "any",
- "orig_default": "",
- "orig_required": false,
- "title": "String",
+ "message": {
"type": "string",
- "ui_component": "textarea"
- },
- "delimiter": {
- "default": "",
- "description": "Delimiter to spilt with. blank will split on the first whitespace",
- "field_kind": "input",
- "input": "any",
- "orig_default": "",
- "orig_required": false,
- "title": "Delimiter",
- "type": "string"
- },
- "type": {
- "const": "string_split",
- "default": "string_split",
- "field_kind": "node_attribute",
- "title": "type",
- "type": "string"
+ "title": "Message",
+ "description": "Status message."
}
},
- "required": ["type", "id"],
- "tags": ["string", "split"],
- "title": "String Split",
"type": "object",
- "version": "1.0.1",
- "output": {
- "$ref": "#/components/schemas/String2Output"
- }
+ "required": ["name", "success", "message"],
+ "title": "UninstallNodePackResponse",
+ "description": "Response after uninstalling a node pack."
},
- "StringSplitNegInvocation": {
- "category": "strings",
- "class": "invocation",
- "classification": "stable",
- "description": "Splits string into two strings, inside [] goes into negative string everthing else goes into positive string. Each [ and ] character is replaced with a space",
- "node_pack": "invokeai",
+ "Unknown_Config": {
"properties": {
- "id": {
- "description": "The id of this instance of an invocation. Must be unique among all instances of invocations.",
- "field_kind": "node_attribute",
- "title": "Id",
- "type": "string"
- },
- "is_intermediate": {
- "default": false,
- "description": "Whether or not this is an intermediate invocation.",
- "field_kind": "node_attribute",
- "input": "direct",
- "orig_required": true,
- "title": "Is Intermediate",
- "type": "boolean",
- "ui_hidden": false,
- "ui_type": "IsIntermediate"
+ "key": {
+ "type": "string",
+ "title": "Key",
+ "description": "A unique key for this model."
},
- "use_cache": {
- "default": true,
- "description": "Whether or not to use the cache",
- "field_kind": "node_attribute",
- "title": "Use Cache",
- "type": "boolean"
+ "hash": {
+ "type": "string",
+ "title": "Hash",
+ "description": "The hash of the model file(s)."
},
- "string": {
- "default": "",
- "description": "String to split",
- "field_kind": "input",
- "input": "any",
- "orig_default": "",
- "orig_required": false,
- "title": "String",
+ "path": {
"type": "string",
- "ui_component": "textarea"
+ "title": "Path",
+ "description": "Path to the model on the filesystem. Relative paths are relative to the Invoke root directory."
+ },
+ "file_size": {
+ "type": "integer",
+ "title": "File Size",
+ "description": "The size of the model in bytes."
},
- "type": {
- "const": "string_split_neg",
- "default": "string_split_neg",
- "field_kind": "node_attribute",
- "title": "type",
- "type": "string"
- }
- },
- "required": ["type", "id"],
- "tags": ["string", "split", "negative"],
- "title": "String Split Negative",
- "type": "object",
- "version": "1.0.1",
- "output": {
- "$ref": "#/components/schemas/StringPosNegOutput"
- }
- },
- "StylePresetField": {
- "description": "A style preset primitive field",
- "properties": {
- "style_preset_id": {
- "description": "The id of the style preset",
- "title": "Style Preset Id",
- "type": "string"
- }
- },
- "required": ["style_preset_id"],
- "title": "StylePresetField",
- "type": "object"
- },
- "StylePresetRecordWithImage": {
- "properties": {
"name": {
"type": "string",
"title": "Name",
- "description": "The name of the style preset."
+ "description": "Name of the model."
},
- "preset_data": {
- "$ref": "#/components/schemas/PresetData",
- "description": "The preset data"
+ "description": {
+ "anyOf": [
+ {
+ "type": "string"
+ },
+ {
+ "type": "null"
+ }
+ ],
+ "title": "Description",
+ "description": "Model description"
},
- "type": {
- "$ref": "#/components/schemas/PresetType",
- "description": "The type of style preset"
+ "source": {
+ "type": "string",
+ "title": "Source",
+ "description": "The original source of the model (path, URL or repo_id)."
},
- "is_public": {
- "type": "boolean",
- "title": "Is Public",
- "description": "Whether the preset is visible to other users.",
- "default": false
+ "source_type": {
+ "$ref": "#/components/schemas/ModelSourceType",
+ "description": "The type of source"
},
- "id": {
- "type": "string",
- "title": "Id",
- "description": "The style preset ID."
+ "source_api_response": {
+ "anyOf": [
+ {
+ "type": "string"
+ },
+ {
+ "type": "null"
+ }
+ ],
+ "title": "Source Api Response",
+ "description": "The original API response from the source, as stringified JSON."
},
- "user_id": {
- "type": "string",
- "title": "User Id",
- "description": "The user who owns this style preset."
+ "source_url": {
+ "anyOf": [
+ {
+ "type": "string"
+ },
+ {
+ "type": "null"
+ }
+ ],
+ "title": "Source Url",
+ "description": "Optional URL for the model (e.g. download page or model page)."
},
- "image": {
+ "cover_image": {
"anyOf": [
{
"type": "string"
@@ -68567,84 +74965,87 @@
"type": "null"
}
],
- "title": "Image",
- "description": "The path for image"
+ "title": "Cover Image",
+ "description": "Url for image to preview model"
+ },
+ "base": {
+ "type": "string",
+ "const": "unknown",
+ "title": "Base",
+ "default": "unknown"
+ },
+ "type": {
+ "type": "string",
+ "const": "unknown",
+ "title": "Type",
+ "default": "unknown"
+ },
+ "format": {
+ "type": "string",
+ "const": "unknown",
+ "title": "Format",
+ "default": "unknown"
}
},
"type": "object",
- "required": ["name", "preset_data", "type", "id", "user_id", "image"],
- "title": "StylePresetRecordWithImage"
- },
- "SubModelType": {
- "type": "string",
- "enum": [
- "unet",
- "transformer",
- "text_encoder",
- "text_encoder_2",
- "text_encoder_3",
- "tokenizer",
- "tokenizer_2",
- "tokenizer_3",
- "vae",
- "vae_decoder",
- "vae_encoder",
- "scheduler",
- "safety_checker"
+ "required": [
+ "key",
+ "hash",
+ "path",
+ "file_size",
+ "name",
+ "description",
+ "source",
+ "source_type",
+ "source_api_response",
+ "source_url",
+ "cover_image",
+ "base",
+ "type",
+ "format"
],
- "title": "SubModelType",
- "description": "Submodel type."
+ "title": "Unknown_Config",
+ "description": "Model config for unknown models, used as a fallback when we cannot positively identify a model."
},
- "SubmodelDefinition": {
- "properties": {
- "path_or_prefix": {
- "type": "string",
- "title": "Path Or Prefix"
- },
- "model_type": {
- "$ref": "#/components/schemas/ModelType"
- },
- "variant": {
+ "UnsharpMaskInvocation": {
+ "category": "image",
+ "class": "invocation",
+ "classification": "stable",
+ "description": "Applies an unsharp mask filter to an image",
+ "node_pack": "invokeai",
+ "properties": {
+ "board": {
"anyOf": [
{
- "$ref": "#/components/schemas/ModelVariantType"
- },
- {
- "$ref": "#/components/schemas/ClipVariantType"
- },
- {
- "$ref": "#/components/schemas/FluxVariantType"
- },
- {
- "$ref": "#/components/schemas/Flux2VariantType"
- },
- {
- "$ref": "#/components/schemas/ZImageVariantType"
+ "$ref": "#/components/schemas/BoardField"
},
{
- "$ref": "#/components/schemas/QwenImageVariantType"
- },
+ "type": "null"
+ }
+ ],
+ "default": null,
+ "description": "The board to save the image to",
+ "field_kind": "internal",
+ "input": "direct",
+ "orig_required": false,
+ "ui_hidden": false
+ },
+ "metadata": {
+ "anyOf": [
{
- "$ref": "#/components/schemas/Qwen3VariantType"
+ "$ref": "#/components/schemas/MetadataField"
},
{
"type": "null"
}
],
- "title": "Variant"
- }
- },
- "type": "object",
- "required": ["path_or_prefix", "model_type"],
- "title": "SubmodelDefinition"
- },
- "SubtractInvocation": {
- "category": "math",
- "class": "invocation",
- "classification": "stable",
- "description": "Subtracts two numbers",
- "node_pack": "invokeai",
- "properties": {
+ "default": null,
+ "description": "Optional metadata to be saved with the image",
+ "field_kind": "internal",
+ "input": "connection",
+ "orig_required": false,
+ "ui_hidden": false
+ },
"id": {
"description": "The id of this instance of an invocation. Must be unique among all instances of invocations.",
"field_kind": "node_attribute",
@@ -68669,102 +75070,266 @@
"title": "Use Cache",
"type": "boolean"
},
- "a": {
- "default": 0,
- "description": "The first number",
+ "image": {
+ "anyOf": [
+ {
+ "$ref": "#/components/schemas/ImageField"
+ },
+ {
+ "type": "null"
+ }
+ ],
+ "default": null,
+ "description": "The image to use",
"field_kind": "input",
"input": "any",
- "orig_default": 0,
+ "orig_required": true
+ },
+ "radius": {
+ "default": 2,
+ "description": "Unsharp mask radius",
+ "exclusiveMinimum": 0,
+ "field_kind": "input",
+ "input": "any",
+ "orig_default": 2,
"orig_required": false,
- "title": "A",
- "type": "integer"
+ "title": "Radius",
+ "type": "number"
},
- "b": {
- "default": 0,
- "description": "The second number",
+ "strength": {
+ "default": 50,
+ "description": "Unsharp mask strength",
"field_kind": "input",
"input": "any",
- "orig_default": 0,
+ "minimum": 0,
+ "orig_default": 50,
"orig_required": false,
- "title": "B",
- "type": "integer"
+ "title": "Strength",
+ "type": "number"
},
"type": {
- "const": "sub",
- "default": "sub",
+ "const": "unsharp_mask",
+ "default": "unsharp_mask",
"field_kind": "node_attribute",
"title": "type",
"type": "string"
}
},
"required": ["type", "id"],
- "tags": ["math", "subtract"],
- "title": "Subtract Integers",
+ "tags": ["image", "unsharp_mask"],
+ "title": "Unsharp Mask",
"type": "object",
- "version": "1.0.1",
+ "version": "1.2.2",
"output": {
- "$ref": "#/components/schemas/IntegerOutput"
+ "$ref": "#/components/schemas/ImageOutput"
}
},
- "T2IAdapterField": {
+ "UnstarredImagesResult": {
"properties": {
- "image": {
- "$ref": "#/components/schemas/ImageField",
- "description": "The T2I-Adapter image prompt."
+ "affected_boards": {
+ "items": {
+ "type": "string"
+ },
+ "type": "array",
+ "title": "Affected Boards",
+ "description": "The ids of boards affected by the delete operation"
},
- "t2i_adapter_model": {
- "$ref": "#/components/schemas/ModelIdentifierField",
- "description": "The T2I-Adapter model to use."
+ "unstarred_images": {
+ "items": {
+ "type": "string"
+ },
+ "type": "array",
+ "title": "Unstarred Images",
+ "description": "The names of the images that were unstarred"
+ }
+ },
+ "type": "object",
+ "required": ["affected_boards", "unstarred_images"],
+ "title": "UnstarredImagesResult"
+ },
+ "UnstarredVideosResult": {
+ "properties": {
+ "affected_boards": {
+ "items": {
+ "type": "string"
+ },
+ "type": "array",
+ "title": "Affected Boards",
+ "description": "The ids of boards affected by the operation"
},
- "weight": {
+ "unstarred_videos": {
+ "items": {
+ "type": "string"
+ },
+ "type": "array",
+ "title": "Unstarred Videos",
+ "description": "The names of the videos that were unstarred"
+ }
+ },
+ "type": "object",
+ "required": ["affected_boards", "unstarred_videos"],
+ "title": "UnstarredVideosResult"
+ },
+ "UpdateAppGenerationSettingsRequest": {
+ "properties": {
+ "image_subfolder_strategy": {
+ "type": "string",
+ "enum": ["flat", "date", "type", "hash"],
+ "title": "Image Subfolder Strategy",
+ "description": "Strategy for organizing images into subfolders."
+ },
+ "max_queue_history": {
"anyOf": [
{
- "type": "number"
+ "type": "integer",
+ "minimum": 0.0
},
{
- "items": {
- "type": "number"
- },
- "type": "array"
+ "type": "null"
}
],
- "default": 1,
- "description": "The weight given to the T2I-Adapter",
- "title": "Weight"
+ "title": "Max Queue History",
+ "description": "Keep the last N completed, failed, and canceled queue items on startup. Set to 0 to prune all terminal items."
+ }
+ },
+ "type": "object",
+ "title": "UpdateAppGenerationSettingsRequest",
+ "description": "Writable generation-related app settings."
+ },
+ "UserDTO": {
+ "properties": {
+ "user_id": {
+ "type": "string",
+ "title": "User Id",
+ "description": "Unique user identifier"
},
- "begin_step_percent": {
- "default": 0,
- "description": "When the T2I-Adapter is first applied (% of total steps)",
- "maximum": 1,
- "minimum": 0,
- "title": "Begin Step Percent",
- "type": "number"
+ "email": {
+ "type": "string",
+ "title": "Email",
+ "description": "User email address"
},
- "end_step_percent": {
- "default": 1,
- "description": "When the T2I-Adapter is last applied (% of total steps)",
- "maximum": 1,
- "minimum": 0,
- "title": "End Step Percent",
- "type": "number"
+ "display_name": {
+ "anyOf": [
+ {
+ "type": "string"
+ },
+ {
+ "type": "null"
+ }
+ ],
+ "title": "Display Name",
+ "description": "Display name"
},
- "resize_mode": {
- "default": "just_resize",
- "description": "The resize mode to use",
- "enum": ["just_resize", "crop_resize", "fill_resize", "just_resize_simple"],
- "title": "Resize Mode",
- "type": "string"
+ "is_admin": {
+ "type": "boolean",
+ "title": "Is Admin",
+ "description": "Whether user has admin privileges",
+ "default": false
+ },
+ "is_active": {
+ "type": "boolean",
+ "title": "Is Active",
+ "description": "Whether user account is active",
+ "default": true
+ },
+ "created_at": {
+ "type": "string",
+ "format": "date-time",
+ "title": "Created At",
+ "description": "When the user was created"
+ },
+ "updated_at": {
+ "type": "string",
+ "format": "date-time",
+ "title": "Updated At",
+ "description": "When the user was last updated"
+ },
+ "last_login_at": {
+ "anyOf": [
+ {
+ "type": "string",
+ "format": "date-time"
+ },
+ {
+ "type": "null"
+ }
+ ],
+ "title": "Last Login At",
+ "description": "When user last logged in"
}
},
- "required": ["image", "t2i_adapter_model"],
- "title": "T2IAdapterField",
+ "type": "object",
+ "required": ["user_id", "email", "created_at", "updated_at"],
+ "title": "UserDTO",
+ "description": "User data transfer object."
+ },
+ "UserProfileUpdateRequest": {
+ "properties": {
+ "display_name": {
+ "anyOf": [
+ {
+ "type": "string"
+ },
+ {
+ "type": "null"
+ }
+ ],
+ "title": "Display Name",
+ "description": "New display name"
+ },
+ "current_password": {
+ "anyOf": [
+ {
+ "type": "string"
+ },
+ {
+ "type": "null"
+ }
+ ],
+ "title": "Current Password",
+ "description": "Current password (required when changing password)"
+ },
+ "new_password": {
+ "anyOf": [
+ {
+ "type": "string"
+ },
+ {
+ "type": "null"
+ }
+ ],
+ "title": "New Password",
+ "description": "New password"
+ }
+ },
+ "type": "object",
+ "title": "UserProfileUpdateRequest",
+ "description": "Request body for a user to update their own profile."
+ },
+ "VAEField": {
+ "properties": {
+ "vae": {
+ "$ref": "#/components/schemas/ModelIdentifierField",
+ "description": "Info to load vae submodel"
+ },
+ "seamless_axes": {
+ "description": "Axes(\"x\" and \"y\") to which apply seamless",
+ "items": {
+ "type": "string"
+ },
+ "title": "Seamless Axes",
+ "type": "array"
+ }
+ },
+ "required": ["vae"],
+ "title": "VAEField",
"type": "object"
},
- "T2IAdapterInvocation": {
- "category": "conditioning",
+ "VAELoaderInvocation": {
+ "category": "model",
"class": "invocation",
"classification": "stable",
- "description": "Collects T2I-Adapter info to pass to other nodes.",
+ "description": "Loads a VAE model, outputting a VaeLoaderOutput",
"node_pack": "invokeai",
"properties": {
"id": {
@@ -68791,22 +75356,7 @@
"title": "Use Cache",
"type": "boolean"
},
- "image": {
- "anyOf": [
- {
- "$ref": "#/components/schemas/ImageField"
- },
- {
- "type": "null"
- }
- ],
- "default": null,
- "description": "The IP-Adapter image prompt.",
- "field_kind": "input",
- "input": "any",
- "orig_required": true
- },
- "t2i_adapter_model": {
+ "vae_model": {
"anyOf": [
{
"$ref": "#/components/schemas/ModelIdentifierField"
@@ -68816,177 +75366,55 @@
}
],
"default": null,
- "description": "The T2I-Adapter model.",
+ "description": "VAE model to load",
"field_kind": "input",
"input": "any",
"orig_required": true,
- "title": "T2I-Adapter Model",
- "ui_model_base": ["sd-1", "sdxl"],
- "ui_model_type": ["t2i_adapter"],
- "ui_order": -1
- },
- "weight": {
- "anyOf": [
- {
- "type": "number"
- },
- {
- "items": {
- "type": "number"
- },
- "type": "array"
- }
- ],
- "default": 1,
- "description": "The weight given to the T2I-Adapter",
- "field_kind": "input",
- "ge": 0,
- "input": "any",
- "orig_default": 1,
- "orig_required": false,
- "title": "Weight"
- },
- "begin_step_percent": {
- "default": 0,
- "description": "When the T2I-Adapter is first applied (% of total steps)",
- "field_kind": "input",
- "input": "any",
- "maximum": 1,
- "minimum": 0,
- "orig_default": 0,
- "orig_required": false,
- "title": "Begin Step Percent",
- "type": "number"
- },
- "end_step_percent": {
- "default": 1,
- "description": "When the T2I-Adapter is last applied (% of total steps)",
- "field_kind": "input",
- "input": "any",
- "maximum": 1,
- "minimum": 0,
- "orig_default": 1,
- "orig_required": false,
- "title": "End Step Percent",
- "type": "number"
- },
- "resize_mode": {
- "default": "just_resize",
- "description": "The resize mode applied to the T2I-Adapter input image so that it matches the target output size.",
- "enum": ["just_resize", "crop_resize", "fill_resize", "just_resize_simple"],
- "field_kind": "input",
- "input": "any",
- "orig_default": "just_resize",
- "orig_required": false,
- "title": "Resize Mode",
- "type": "string"
+ "title": "VAE",
+ "ui_model_base": ["sd-1", "sd-2", "sdxl", "sd-3", "flux", "flux2"],
+ "ui_model_type": ["vae"]
},
"type": {
- "const": "t2i_adapter",
- "default": "t2i_adapter",
+ "const": "vae_loader",
+ "default": "vae_loader",
"field_kind": "node_attribute",
"title": "type",
"type": "string"
}
},
"required": ["type", "id"],
- "tags": ["t2i_adapter", "control"],
- "title": "T2I-Adapter - SD1.5, SDXL",
+ "tags": ["vae", "model"],
+ "title": "VAE Model - SD1.5, SD2, SDXL, SD3, FLUX",
"type": "object",
"version": "1.0.4",
"output": {
- "$ref": "#/components/schemas/T2IAdapterOutput"
+ "$ref": "#/components/schemas/VAEOutput"
}
},
- "T2IAdapterMetadataField": {
- "properties": {
- "image": {
- "$ref": "#/components/schemas/ImageField",
- "description": "The control image."
- },
- "processed_image": {
- "anyOf": [
- {
- "$ref": "#/components/schemas/ImageField"
- },
- {
- "type": "null"
- }
- ],
- "default": null,
- "description": "The control image, after processing."
- },
- "t2i_adapter_model": {
- "$ref": "#/components/schemas/ModelIdentifierField",
- "description": "The T2I-Adapter model to use."
- },
- "weight": {
- "anyOf": [
- {
- "type": "number"
- },
- {
- "items": {
- "type": "number"
- },
- "type": "array"
- }
- ],
- "default": 1,
- "description": "The weight given to the T2I-Adapter",
- "title": "Weight"
- },
- "begin_step_percent": {
- "default": 0,
- "description": "When the T2I-Adapter is first applied (% of total steps)",
- "maximum": 1,
- "minimum": 0,
- "title": "Begin Step Percent",
- "type": "number"
- },
- "end_step_percent": {
- "default": 1,
- "description": "When the T2I-Adapter is last applied (% of total steps)",
- "maximum": 1,
- "minimum": 0,
- "title": "End Step Percent",
- "type": "number"
- },
- "resize_mode": {
- "default": "just_resize",
- "description": "The resize mode to use",
- "enum": ["just_resize", "crop_resize", "fill_resize", "just_resize_simple"],
- "title": "Resize Mode",
- "type": "string"
- }
- },
- "required": ["image", "t2i_adapter_model"],
- "title": "T2IAdapterMetadataField",
- "type": "object"
- },
- "T2IAdapterOutput": {
+ "VAEOutput": {
"class": "output",
+ "description": "Base class for invocations that output a VAE field",
"properties": {
- "t2i_adapter": {
- "$ref": "#/components/schemas/T2IAdapterField",
- "description": "T2I-Adapter(s) to apply",
+ "vae": {
+ "$ref": "#/components/schemas/VAEField",
+ "description": "VAE",
"field_kind": "output",
- "title": "T2I Adapter",
+ "title": "VAE",
"ui_hidden": false
},
"type": {
- "const": "t2i_adapter_output",
- "default": "t2i_adapter_output",
+ "const": "vae_output",
+ "default": "vae_output",
"field_kind": "node_attribute",
"title": "type",
"type": "string"
}
},
- "required": ["output_meta", "t2i_adapter", "type", "type"],
- "title": "T2IAdapterOutput",
+ "required": ["output_meta", "vae", "type", "type"],
+ "title": "VAEOutput",
"type": "object"
},
- "T2IAdapter_Diffusers_SD1_Config": {
+ "VAE_Checkpoint_Anima_Config": {
"properties": {
"key": {
"type": "string",
@@ -69070,37 +75498,47 @@
"title": "Cover Image",
"description": "Url for image to preview model"
},
+ "config_path": {
+ "anyOf": [
+ {
+ "type": "string"
+ },
+ {
+ "type": "null"
+ }
+ ],
+ "title": "Config Path",
+ "description": "Path to the config for this model, if any."
+ },
+ "type": {
+ "type": "string",
+ "const": "vae",
+ "title": "Type",
+ "default": "vae"
+ },
"format": {
"type": "string",
- "const": "diffusers",
+ "const": "checkpoint",
"title": "Format",
- "default": "diffusers"
- },
- "repo_variant": {
- "$ref": "#/components/schemas/ModelRepoVariant",
- "default": ""
+ "default": "checkpoint"
},
- "type": {
+ "base": {
"type": "string",
- "const": "t2i_adapter",
- "title": "Type",
- "default": "t2i_adapter"
+ "const": "anima",
+ "title": "Base",
+ "default": "anima"
},
- "default_settings": {
+ "cpu_only": {
"anyOf": [
{
- "$ref": "#/components/schemas/ControlAdapterDefaultSettings"
+ "type": "boolean"
},
{
"type": "null"
}
- ]
- },
- "base": {
- "type": "string",
- "const": "sd-1",
- "title": "Base",
- "default": "sd-1"
+ ],
+ "title": "Cpu Only",
+ "description": "Whether this model should run on CPU only"
}
},
"type": "object",
@@ -69116,15 +75554,16 @@
"source_api_response",
"source_url",
"cover_image",
- "format",
- "repo_variant",
+ "config_path",
"type",
- "default_settings",
- "base"
+ "format",
+ "base",
+ "cpu_only"
],
- "title": "T2IAdapter_Diffusers_SD1_Config"
+ "title": "VAE_Checkpoint_Anima_Config",
+ "description": "Model config for Anima QwenImage VAE checkpoint models (AutoencoderKLQwenImage)."
},
- "T2IAdapter_Diffusers_SDXL_Config": {
+ "VAE_Checkpoint_FLUX_Config": {
"properties": {
"key": {
"type": "string",
@@ -69208,37 +75647,47 @@
"title": "Cover Image",
"description": "Url for image to preview model"
},
- "format": {
- "type": "string",
- "const": "diffusers",
- "title": "Format",
- "default": "diffusers"
- },
- "repo_variant": {
- "$ref": "#/components/schemas/ModelRepoVariant",
- "default": ""
+ "config_path": {
+ "anyOf": [
+ {
+ "type": "string"
+ },
+ {
+ "type": "null"
+ }
+ ],
+ "title": "Config Path",
+ "description": "Path to the config for this model, if any."
},
"type": {
"type": "string",
- "const": "t2i_adapter",
+ "const": "vae",
"title": "Type",
- "default": "t2i_adapter"
+ "default": "vae"
},
- "default_settings": {
+ "format": {
+ "type": "string",
+ "const": "checkpoint",
+ "title": "Format",
+ "default": "checkpoint"
+ },
+ "cpu_only": {
"anyOf": [
{
- "$ref": "#/components/schemas/ControlAdapterDefaultSettings"
+ "type": "boolean"
},
{
"type": "null"
}
- ]
+ ],
+ "title": "Cpu Only",
+ "description": "Whether this model should run on CPU only"
},
"base": {
"type": "string",
- "const": "sdxl",
+ "const": "flux",
"title": "Base",
- "default": "sdxl"
+ "default": "flux"
}
},
"type": "object",
@@ -69254,38 +75703,15 @@
"source_api_response",
"source_url",
"cover_image",
- "format",
- "repo_variant",
+ "config_path",
"type",
- "default_settings",
+ "format",
+ "cpu_only",
"base"
],
- "title": "T2IAdapter_Diffusers_SDXL_Config"
- },
- "T5EncoderField": {
- "properties": {
- "tokenizer": {
- "$ref": "#/components/schemas/ModelIdentifierField",
- "description": "Info to load tokenizer submodel"
- },
- "text_encoder": {
- "$ref": "#/components/schemas/ModelIdentifierField",
- "description": "Info to load text_encoder submodel"
- },
- "loras": {
- "description": "LoRAs to apply on model loading",
- "items": {
- "$ref": "#/components/schemas/LoRAField"
- },
- "title": "Loras",
- "type": "array"
- }
- },
- "required": ["tokenizer", "text_encoder", "loras"],
- "title": "T5EncoderField",
- "type": "object"
+ "title": "VAE_Checkpoint_FLUX_Config"
},
- "T5Encoder_BnBLLMint8_Config": {
+ "VAE_Checkpoint_Flux2_Config": {
"properties": {
"key": {
"type": "string",
@@ -69369,23 +75795,35 @@
"title": "Cover Image",
"description": "Url for image to preview model"
},
- "base": {
- "type": "string",
- "const": "any",
- "title": "Base",
- "default": "any"
+ "config_path": {
+ "anyOf": [
+ {
+ "type": "string"
+ },
+ {
+ "type": "null"
+ }
+ ],
+ "title": "Config Path",
+ "description": "Path to the config for this model, if any."
},
"type": {
"type": "string",
- "const": "t5_encoder",
+ "const": "vae",
"title": "Type",
- "default": "t5_encoder"
+ "default": "vae"
},
"format": {
"type": "string",
- "const": "bnb_quantized_int8b",
+ "const": "checkpoint",
"title": "Format",
- "default": "bnb_quantized_int8b"
+ "default": "checkpoint"
+ },
+ "base": {
+ "type": "string",
+ "const": "flux2",
+ "title": "Base",
+ "default": "flux2"
},
"cpu_only": {
"anyOf": [
@@ -69413,15 +75851,16 @@
"source_api_response",
"source_url",
"cover_image",
- "base",
+ "config_path",
"type",
"format",
+ "base",
"cpu_only"
],
- "title": "T5Encoder_BnBLLMint8_Config",
- "description": "Configuration for T5 Encoder models quantized by bitsandbytes' LLM.int8."
+ "title": "VAE_Checkpoint_Flux2_Config",
+ "description": "Model config for FLUX.2 VAE checkpoint models (AutoencoderKLFlux2)."
},
- "T5Encoder_T5Encoder_Config": {
+ "VAE_Checkpoint_QwenImage_Config": {
"properties": {
"key": {
"type": "string",
@@ -69505,23 +75944,35 @@
"title": "Cover Image",
"description": "Url for image to preview model"
},
- "base": {
- "type": "string",
- "const": "any",
- "title": "Base",
- "default": "any"
+ "config_path": {
+ "anyOf": [
+ {
+ "type": "string"
+ },
+ {
+ "type": "null"
+ }
+ ],
+ "title": "Config Path",
+ "description": "Path to the config for this model, if any."
},
"type": {
"type": "string",
- "const": "t5_encoder",
+ "const": "vae",
"title": "Type",
- "default": "t5_encoder"
+ "default": "vae"
},
"format": {
"type": "string",
- "const": "t5_encoder",
+ "const": "checkpoint",
"title": "Format",
- "default": "t5_encoder"
+ "default": "checkpoint"
+ },
+ "base": {
+ "type": "string",
+ "const": "qwen-image",
+ "title": "Base",
+ "default": "qwen-image"
},
"cpu_only": {
"anyOf": [
@@ -69549,38 +76000,16 @@
"source_api_response",
"source_url",
"cover_image",
- "base",
+ "config_path",
"type",
"format",
+ "base",
"cpu_only"
],
- "title": "T5Encoder_T5Encoder_Config",
- "description": "Configuration for T5 Encoder models in a bespoke, diffusers-like format. The model weights are expected to be in\na folder called text_encoder_2 inside the model directory, with a config file named model.safetensors.index.json."
- },
- "TBLR": {
- "properties": {
- "top": {
- "title": "Top",
- "type": "integer"
- },
- "bottom": {
- "title": "Bottom",
- "type": "integer"
- },
- "left": {
- "title": "Left",
- "type": "integer"
- },
- "right": {
- "title": "Right",
- "type": "integer"
- }
- },
- "required": ["top", "bottom", "left", "right"],
- "title": "TBLR",
- "type": "object"
+ "title": "VAE_Checkpoint_QwenImage_Config",
+ "description": "Model config for Qwen Image VAE checkpoint models (AutoencoderKLQwenImage)."
},
- "TI_File_SD1_Config": {
+ "VAE_Checkpoint_SD1_Config": {
"properties": {
"key": {
"type": "string",
@@ -69664,17 +76093,41 @@
"title": "Cover Image",
"description": "Url for image to preview model"
},
+ "config_path": {
+ "anyOf": [
+ {
+ "type": "string"
+ },
+ {
+ "type": "null"
+ }
+ ],
+ "title": "Config Path",
+ "description": "Path to the config for this model, if any."
+ },
"type": {
"type": "string",
- "const": "embedding",
+ "const": "vae",
"title": "Type",
- "default": "embedding"
+ "default": "vae"
},
"format": {
"type": "string",
- "const": "embedding_file",
+ "const": "checkpoint",
"title": "Format",
- "default": "embedding_file"
+ "default": "checkpoint"
+ },
+ "cpu_only": {
+ "anyOf": [
+ {
+ "type": "boolean"
+ },
+ {
+ "type": "null"
+ }
+ ],
+ "title": "Cpu Only",
+ "description": "Whether this model should run on CPU only"
},
"base": {
"type": "string",
@@ -69696,13 +76149,15 @@
"source_api_response",
"source_url",
"cover_image",
+ "config_path",
"type",
"format",
+ "cpu_only",
"base"
],
- "title": "TI_File_SD1_Config"
+ "title": "VAE_Checkpoint_SD1_Config"
},
- "TI_File_SD2_Config": {
+ "VAE_Checkpoint_SD2_Config": {
"properties": {
"key": {
"type": "string",
@@ -69786,17 +76241,41 @@
"title": "Cover Image",
"description": "Url for image to preview model"
},
+ "config_path": {
+ "anyOf": [
+ {
+ "type": "string"
+ },
+ {
+ "type": "null"
+ }
+ ],
+ "title": "Config Path",
+ "description": "Path to the config for this model, if any."
+ },
"type": {
"type": "string",
- "const": "embedding",
+ "const": "vae",
"title": "Type",
- "default": "embedding"
+ "default": "vae"
},
"format": {
"type": "string",
- "const": "embedding_file",
+ "const": "checkpoint",
"title": "Format",
- "default": "embedding_file"
+ "default": "checkpoint"
+ },
+ "cpu_only": {
+ "anyOf": [
+ {
+ "type": "boolean"
+ },
+ {
+ "type": "null"
+ }
+ ],
+ "title": "Cpu Only",
+ "description": "Whether this model should run on CPU only"
},
"base": {
"type": "string",
@@ -69818,13 +76297,15 @@
"source_api_response",
"source_url",
"cover_image",
+ "config_path",
"type",
"format",
+ "cpu_only",
"base"
],
- "title": "TI_File_SD2_Config"
+ "title": "VAE_Checkpoint_SD2_Config"
},
- "TI_File_SDXL_Config": {
+ "VAE_Checkpoint_SDXL_Config": {
"properties": {
"key": {
"type": "string",
@@ -69908,17 +76389,41 @@
"title": "Cover Image",
"description": "Url for image to preview model"
},
+ "config_path": {
+ "anyOf": [
+ {
+ "type": "string"
+ },
+ {
+ "type": "null"
+ }
+ ],
+ "title": "Config Path",
+ "description": "Path to the config for this model, if any."
+ },
"type": {
"type": "string",
- "const": "embedding",
+ "const": "vae",
"title": "Type",
- "default": "embedding"
+ "default": "vae"
},
"format": {
"type": "string",
- "const": "embedding_file",
+ "const": "checkpoint",
"title": "Format",
- "default": "embedding_file"
+ "default": "checkpoint"
+ },
+ "cpu_only": {
+ "anyOf": [
+ {
+ "type": "boolean"
+ },
+ {
+ "type": "null"
+ }
+ ],
+ "title": "Cpu Only",
+ "description": "Whether this model should run on CPU only"
},
"base": {
"type": "string",
@@ -69940,13 +76445,15 @@
"source_api_response",
"source_url",
"cover_image",
+ "config_path",
"type",
"format",
+ "cpu_only",
"base"
],
- "title": "TI_File_SDXL_Config"
+ "title": "VAE_Checkpoint_SDXL_Config"
},
- "TI_Folder_SD1_Config": {
+ "VAE_Checkpoint_Wan_Config": {
"properties": {
"key": {
"type": "string",
@@ -70030,23 +76537,53 @@
"title": "Cover Image",
"description": "Url for image to preview model"
},
+ "config_path": {
+ "anyOf": [
+ {
+ "type": "string"
+ },
+ {
+ "type": "null"
+ }
+ ],
+ "title": "Config Path",
+ "description": "Path to the config for this model, if any."
+ },
"type": {
"type": "string",
- "const": "embedding",
+ "const": "vae",
"title": "Type",
- "default": "embedding"
+ "default": "vae"
},
"format": {
"type": "string",
- "const": "embedding_folder",
+ "const": "checkpoint",
"title": "Format",
- "default": "embedding_folder"
+ "default": "checkpoint"
},
"base": {
"type": "string",
- "const": "sd-1",
+ "const": "wan",
"title": "Base",
- "default": "sd-1"
+ "default": "wan"
+ },
+ "cpu_only": {
+ "anyOf": [
+ {
+ "type": "boolean"
+ },
+ {
+ "type": "null"
+ }
+ ],
+ "title": "Cpu Only",
+ "description": "Whether this model should run on CPU only"
+ },
+ "latent_channels": {
+ "type": "integer",
+ "enum": [16, 48],
+ "title": "Latent Channels",
+ "description": "VAE latent channel count: 16 for A14B (standard Wan VAE) or 48 for TI2V-5B (Wan2.2-VAE)."
}
},
"type": "object",
@@ -70062,13 +76599,17 @@
"source_api_response",
"source_url",
"cover_image",
+ "config_path",
"type",
"format",
- "base"
+ "base",
+ "cpu_only",
+ "latent_channels"
],
- "title": "TI_Folder_SD1_Config"
+ "title": "VAE_Checkpoint_Wan_Config",
+ "description": "Model config for Wan 2.2 VAE checkpoint models (AutoencoderKLWan).\n\nDistinguishes A14B (z_dim=16, standard Wan VAE) from TI2V-5B (z_dim=48,\nWan2.2-VAE) via the input channel count of ``decoder.conv_in.weight``."
},
- "TI_Folder_SD2_Config": {
+ "VAE_Diffusers_Flux2_Config": {
"properties": {
"key": {
"type": "string",
@@ -70152,23 +76693,39 @@
"title": "Cover Image",
"description": "Url for image to preview model"
},
- "type": {
- "type": "string",
- "const": "embedding",
- "title": "Type",
- "default": "embedding"
- },
"format": {
"type": "string",
- "const": "embedding_folder",
+ "const": "diffusers",
"title": "Format",
- "default": "embedding_folder"
+ "default": "diffusers"
+ },
+ "repo_variant": {
+ "$ref": "#/components/schemas/ModelRepoVariant",
+ "default": ""
+ },
+ "type": {
+ "type": "string",
+ "const": "vae",
+ "title": "Type",
+ "default": "vae"
},
"base": {
"type": "string",
- "const": "sd-2",
+ "const": "flux2",
"title": "Base",
- "default": "sd-2"
+ "default": "flux2"
+ },
+ "cpu_only": {
+ "anyOf": [
+ {
+ "type": "boolean"
+ },
+ {
+ "type": "null"
+ }
+ ],
+ "title": "Cpu Only",
+ "description": "Whether this model should run on CPU only"
}
},
"type": "object",
@@ -70184,13 +76741,16 @@
"source_api_response",
"source_url",
"cover_image",
- "type",
"format",
- "base"
+ "repo_variant",
+ "type",
+ "base",
+ "cpu_only"
],
- "title": "TI_Folder_SD2_Config"
+ "title": "VAE_Diffusers_Flux2_Config",
+ "description": "Model config for FLUX.2 VAE models in diffusers format (AutoencoderKLFlux2)."
},
- "TI_Folder_SDXL_Config": {
+ "VAE_Diffusers_SD1_Config": {
"properties": {
"key": {
"type": "string",
@@ -70274,157 +76834,63 @@
"title": "Cover Image",
"description": "Url for image to preview model"
},
- "type": {
- "type": "string",
- "const": "embedding",
- "title": "Type",
- "default": "embedding"
- },
"format": {
"type": "string",
- "const": "embedding_folder",
+ "const": "diffusers",
"title": "Format",
- "default": "embedding_folder"
- },
- "base": {
- "type": "string",
- "const": "sdxl",
- "title": "Base",
- "default": "sdxl"
- }
- },
- "type": "object",
- "required": [
- "key",
- "hash",
- "path",
- "file_size",
- "name",
- "description",
- "source",
- "source_type",
- "source_api_response",
- "source_url",
- "cover_image",
- "type",
- "format",
- "base"
- ],
- "title": "TI_Folder_SDXL_Config"
- },
- "TensorField": {
- "description": "A tensor primitive field.",
- "properties": {
- "tensor_name": {
- "description": "The name of a tensor.",
- "title": "Tensor Name",
- "type": "string"
- }
- },
- "required": ["tensor_name"],
- "title": "TensorField",
- "type": "object"
- },
- "TextLLMInvocation": {
- "category": "llm",
- "class": "invocation",
- "classification": "beta",
- "description": "Run a text language model to generate or expand text (e.g. for prompt expansion).",
- "node_pack": "invokeai",
- "properties": {
- "id": {
- "description": "The id of this instance of an invocation. Must be unique among all instances of invocations.",
- "field_kind": "node_attribute",
- "title": "Id",
- "type": "string"
- },
- "is_intermediate": {
- "default": false,
- "description": "Whether or not this is an intermediate invocation.",
- "field_kind": "node_attribute",
- "input": "direct",
- "orig_required": true,
- "title": "Is Intermediate",
- "type": "boolean",
- "ui_hidden": false,
- "ui_type": "IsIntermediate"
- },
- "use_cache": {
- "default": true,
- "description": "Whether or not to use the cache",
- "field_kind": "node_attribute",
- "title": "Use Cache",
- "type": "boolean"
+ "default": "diffusers"
},
- "prompt": {
- "default": "",
- "description": "Input text prompt.",
- "field_kind": "input",
- "input": "any",
- "orig_default": "",
- "orig_required": false,
- "title": "Prompt",
- "type": "string",
- "ui_component": "textarea"
+ "repo_variant": {
+ "$ref": "#/components/schemas/ModelRepoVariant",
+ "default": ""
},
- "system_prompt": {
- "default": "You are an expert prompt writer for AI image generation. Given a brief description, expand it into a detailed, vivid prompt suitable for generating high-quality images. Only output the expanded prompt, nothing else.",
- "description": "System prompt that guides the model's behavior.",
- "field_kind": "input",
- "input": "any",
- "orig_default": "You are an expert prompt writer for AI image generation. Given a brief description, expand it into a detailed, vivid prompt suitable for generating high-quality images. Only output the expanded prompt, nothing else.",
- "orig_required": false,
- "title": "System Prompt",
+ "type": {
"type": "string",
- "ui_component": "textarea"
+ "const": "vae",
+ "title": "Type",
+ "default": "vae"
},
- "text_llm_model": {
+ "cpu_only": {
"anyOf": [
{
- "$ref": "#/components/schemas/ModelIdentifierField"
+ "type": "boolean"
},
{
"type": "null"
}
],
- "default": null,
- "description": "The text language model to use for text generation",
- "field_kind": "input",
- "input": "any",
- "orig_required": true,
- "title": "Text LLM Model",
- "ui_model_type": ["text_llm"]
- },
- "max_tokens": {
- "default": 300,
- "description": "Maximum number of tokens to generate.",
- "field_kind": "input",
- "input": "any",
- "maximum": 2048,
- "minimum": 1,
- "orig_default": 300,
- "orig_required": false,
- "title": "Max Tokens",
- "type": "integer"
+ "title": "Cpu Only",
+ "description": "Whether this model should run on CPU only"
},
- "type": {
- "const": "text_llm",
- "default": "text_llm",
- "field_kind": "node_attribute",
- "title": "type",
- "type": "string"
+ "base": {
+ "type": "string",
+ "const": "sd-1",
+ "title": "Base",
+ "default": "sd-1"
}
},
- "required": ["type", "id"],
- "tags": ["llm", "text", "prompt"],
- "title": "Text LLM",
"type": "object",
- "version": "1.0.0",
- "output": {
- "$ref": "#/components/schemas/StringOutput"
- }
+ "required": [
+ "key",
+ "hash",
+ "path",
+ "file_size",
+ "name",
+ "description",
+ "source",
+ "source_type",
+ "source_api_response",
+ "source_url",
+ "cover_image",
+ "format",
+ "repo_variant",
+ "type",
+ "cpu_only",
+ "base"
+ ],
+ "title": "VAE_Diffusers_SD1_Config"
},
- "TextLLM_Diffusers_Config": {
+ "VAE_Diffusers_SDXL_Config": {
"properties": {
"key": {
"type": "string",
@@ -70520,15 +76986,9 @@
},
"type": {
"type": "string",
- "const": "text_llm",
+ "const": "vae",
"title": "Type",
- "default": "text_llm"
- },
- "base": {
- "type": "string",
- "const": "any",
- "title": "Base",
- "default": "any"
+ "default": "vae"
},
"cpu_only": {
"anyOf": [
@@ -70541,6 +77001,12 @@
],
"title": "Cpu Only",
"description": "Whether this model should run on CPU only"
+ },
+ "base": {
+ "type": "string",
+ "const": "sdxl",
+ "title": "Base",
+ "default": "sdxl"
}
},
"type": "object",
@@ -70559,855 +77025,432 @@
"format",
"repo_variant",
"type",
- "base",
- "cpu_only"
+ "cpu_only",
+ "base"
],
- "title": "TextLLM_Diffusers_Config",
- "description": "Model config for text-only causal language models (e.g. Llama, Phi, Qwen, Mistral)."
- },
- "Tile": {
- "properties": {
- "coords": {
- "$ref": "#/components/schemas/TBLR",
- "description": "The coordinates of this tile relative to its parent image."
- },
- "overlap": {
- "$ref": "#/components/schemas/TBLR",
- "description": "The amount of overlap with adjacent tiles on each side of this tile."
- }
- },
- "required": ["coords", "overlap"],
- "title": "Tile",
- "type": "object"
- },
- "TileToPropertiesInvocation": {
- "category": "tiles",
- "class": "invocation",
- "classification": "stable",
- "description": "Split a Tile into its individual properties.",
- "node_pack": "invokeai",
- "properties": {
- "id": {
- "description": "The id of this instance of an invocation. Must be unique among all instances of invocations.",
- "field_kind": "node_attribute",
- "title": "Id",
- "type": "string"
- },
- "is_intermediate": {
- "default": false,
- "description": "Whether or not this is an intermediate invocation.",
- "field_kind": "node_attribute",
- "input": "direct",
- "orig_required": true,
- "title": "Is Intermediate",
- "type": "boolean",
- "ui_hidden": false,
- "ui_type": "IsIntermediate"
- },
- "use_cache": {
- "default": true,
- "description": "Whether or not to use the cache",
- "field_kind": "node_attribute",
- "title": "Use Cache",
- "type": "boolean"
- },
- "tile": {
- "anyOf": [
- {
- "$ref": "#/components/schemas/Tile"
- },
- {
- "type": "null"
- }
- ],
- "default": null,
- "description": "The tile to split into properties.",
- "field_kind": "input",
- "input": "any",
- "orig_required": true
- },
- "type": {
- "const": "tile_to_properties",
- "default": "tile_to_properties",
- "field_kind": "node_attribute",
- "title": "type",
- "type": "string"
- }
- },
- "required": ["type", "id"],
- "tags": ["tiles"],
- "title": "Tile to Properties",
- "type": "object",
- "version": "1.0.1",
- "output": {
- "$ref": "#/components/schemas/TileToPropertiesOutput"
- }
+ "title": "VAE_Diffusers_SDXL_Config"
},
- "TileToPropertiesOutput": {
- "class": "output",
+ "VAE_Diffusers_Wan_Config": {
"properties": {
- "coords_left": {
- "description": "Left coordinate of the tile relative to its parent image.",
- "field_kind": "output",
- "title": "Coords Left",
- "type": "integer",
- "ui_hidden": false
- },
- "coords_right": {
- "description": "Right coordinate of the tile relative to its parent image.",
- "field_kind": "output",
- "title": "Coords Right",
- "type": "integer",
- "ui_hidden": false
- },
- "coords_top": {
- "description": "Top coordinate of the tile relative to its parent image.",
- "field_kind": "output",
- "title": "Coords Top",
- "type": "integer",
- "ui_hidden": false
- },
- "coords_bottom": {
- "description": "Bottom coordinate of the tile relative to its parent image.",
- "field_kind": "output",
- "title": "Coords Bottom",
- "type": "integer",
- "ui_hidden": false
- },
- "width": {
- "description": "The width of the tile. Equal to coords_right - coords_left.",
- "field_kind": "output",
- "title": "Width",
- "type": "integer",
- "ui_hidden": false
- },
- "height": {
- "description": "The height of the tile. Equal to coords_bottom - coords_top.",
- "field_kind": "output",
- "title": "Height",
- "type": "integer",
- "ui_hidden": false
- },
- "overlap_top": {
- "description": "Overlap between this tile and its top neighbor.",
- "field_kind": "output",
- "title": "Overlap Top",
- "type": "integer",
- "ui_hidden": false
+ "key": {
+ "type": "string",
+ "title": "Key",
+ "description": "A unique key for this model."
},
- "overlap_bottom": {
- "description": "Overlap between this tile and its bottom neighbor.",
- "field_kind": "output",
- "title": "Overlap Bottom",
- "type": "integer",
- "ui_hidden": false
+ "hash": {
+ "type": "string",
+ "title": "Hash",
+ "description": "The hash of the model file(s)."
},
- "overlap_left": {
- "description": "Overlap between this tile and its left neighbor.",
- "field_kind": "output",
- "title": "Overlap Left",
- "type": "integer",
- "ui_hidden": false
+ "path": {
+ "type": "string",
+ "title": "Path",
+ "description": "Path to the model on the filesystem. Relative paths are relative to the Invoke root directory."
},
- "overlap_right": {
- "description": "Overlap between this tile and its right neighbor.",
- "field_kind": "output",
- "title": "Overlap Right",
+ "file_size": {
"type": "integer",
- "ui_hidden": false
- },
- "type": {
- "const": "tile_to_properties_output",
- "default": "tile_to_properties_output",
- "field_kind": "node_attribute",
- "title": "type",
- "type": "string"
- }
- },
- "required": [
- "output_meta",
- "coords_left",
- "coords_right",
- "coords_top",
- "coords_bottom",
- "width",
- "height",
- "overlap_top",
- "overlap_bottom",
- "overlap_left",
- "overlap_right",
- "type",
- "type"
- ],
- "title": "TileToPropertiesOutput",
- "type": "object"
- },
- "TileWithImage": {
- "properties": {
- "tile": {
- "$ref": "#/components/schemas/Tile"
- },
- "image": {
- "$ref": "#/components/schemas/ImageField"
- }
- },
- "required": ["tile", "image"],
- "title": "TileWithImage",
- "type": "object"
- },
- "TiledMultiDiffusionDenoiseLatents": {
- "category": "latents",
- "class": "invocation",
- "classification": "stable",
- "description": "Tiled Multi-Diffusion denoising.\n\nThis node handles automatically tiling the input image, and is primarily intended for global refinement of images\nin tiled upscaling workflows. Future Multi-Diffusion nodes should allow the user to specify custom regions with\ndifferent parameters for each region to harness the full power of Multi-Diffusion.\n\nThis node has a similar interface to the `DenoiseLatents` node, but it has a reduced feature set (no IP-Adapter,\nT2I-Adapter, masking, etc.).",
- "node_pack": "invokeai",
- "properties": {
- "id": {
- "description": "The id of this instance of an invocation. Must be unique among all instances of invocations.",
- "field_kind": "node_attribute",
- "title": "Id",
- "type": "string"
- },
- "is_intermediate": {
- "default": false,
- "description": "Whether or not this is an intermediate invocation.",
- "field_kind": "node_attribute",
- "input": "direct",
- "orig_required": true,
- "title": "Is Intermediate",
- "type": "boolean",
- "ui_hidden": false,
- "ui_type": "IsIntermediate"
+ "title": "File Size",
+ "description": "The size of the model in bytes."
},
- "use_cache": {
- "default": true,
- "description": "Whether or not to use the cache",
- "field_kind": "node_attribute",
- "title": "Use Cache",
- "type": "boolean"
+ "name": {
+ "type": "string",
+ "title": "Name",
+ "description": "Name of the model."
},
- "positive_conditioning": {
+ "description": {
"anyOf": [
{
- "$ref": "#/components/schemas/ConditioningField"
+ "type": "string"
},
{
"type": "null"
}
],
- "default": null,
- "description": "Positive conditioning tensor",
- "field_kind": "input",
- "input": "connection",
- "orig_required": true
+ "title": "Description",
+ "description": "Model description"
},
- "negative_conditioning": {
- "anyOf": [
- {
- "$ref": "#/components/schemas/ConditioningField"
- },
- {
- "type": "null"
- }
- ],
- "default": null,
- "description": "Negative conditioning tensor",
- "field_kind": "input",
- "input": "connection",
- "orig_required": true
+ "source": {
+ "type": "string",
+ "title": "Source",
+ "description": "The original source of the model (path, URL or repo_id)."
},
- "noise": {
- "anyOf": [
- {
- "$ref": "#/components/schemas/LatentsField"
- },
- {
- "type": "null"
- }
- ],
- "default": null,
- "description": "Noise tensor",
- "field_kind": "input",
- "input": "connection",
- "orig_default": null,
- "orig_required": false
+ "source_type": {
+ "$ref": "#/components/schemas/ModelSourceType",
+ "description": "The type of source"
},
- "latents": {
+ "source_api_response": {
"anyOf": [
{
- "$ref": "#/components/schemas/LatentsField"
+ "type": "string"
},
{
"type": "null"
}
],
- "default": null,
- "description": "Latents tensor",
- "field_kind": "input",
- "input": "connection",
- "orig_default": null,
- "orig_required": false
- },
- "tile_height": {
- "default": 1024,
- "description": "Height of the tiles in image space.",
- "exclusiveMinimum": 0,
- "field_kind": "input",
- "input": "any",
- "multipleOf": 8,
- "orig_default": 1024,
- "orig_required": false,
- "title": "Tile Height",
- "type": "integer"
- },
- "tile_width": {
- "default": 1024,
- "description": "Width of the tiles in image space.",
- "exclusiveMinimum": 0,
- "field_kind": "input",
- "input": "any",
- "multipleOf": 8,
- "orig_default": 1024,
- "orig_required": false,
- "title": "Tile Width",
- "type": "integer"
- },
- "tile_overlap": {
- "default": 32,
- "description": "The overlap between adjacent tiles in pixel space. (Of course, tile merging is applied in latent space.) Tiles will be cropped during merging (if necessary) to ensure that they overlap by exactly this amount.",
- "exclusiveMinimum": 0,
- "field_kind": "input",
- "input": "any",
- "multipleOf": 8,
- "orig_default": 32,
- "orig_required": false,
- "title": "Tile Overlap",
- "type": "integer"
- },
- "steps": {
- "default": 18,
- "description": "Number of steps to run",
- "exclusiveMinimum": 0,
- "field_kind": "input",
- "input": "any",
- "orig_default": 18,
- "orig_required": false,
- "title": "Steps",
- "type": "integer"
+ "title": "Source Api Response",
+ "description": "The original API response from the source, as stringified JSON."
},
- "cfg_scale": {
+ "source_url": {
"anyOf": [
{
- "type": "number"
+ "type": "string"
},
- {
- "items": {
- "type": "number"
- },
- "type": "array"
- }
- ],
- "default": 6.0,
- "description": "Classifier-Free Guidance scale",
- "field_kind": "input",
- "input": "any",
- "orig_default": 6.0,
- "orig_required": false,
- "title": "CFG Scale"
- },
- "denoising_start": {
- "default": 0.0,
- "description": "When to start denoising, expressed a percentage of total steps",
- "field_kind": "input",
- "input": "any",
- "maximum": 1,
- "minimum": 0,
- "orig_default": 0.0,
- "orig_required": false,
- "title": "Denoising Start",
- "type": "number"
- },
- "denoising_end": {
- "default": 1.0,
- "description": "When to stop denoising, expressed a percentage of total steps",
- "field_kind": "input",
- "input": "any",
- "maximum": 1,
- "minimum": 0,
- "orig_default": 1.0,
- "orig_required": false,
- "title": "Denoising End",
- "type": "number"
- },
- "scheduler": {
- "default": "euler",
- "description": "Scheduler to use during inference",
- "enum": [
- "ddim",
- "ddpm",
- "deis",
- "deis_k",
- "lms",
- "lms_k",
- "pndm",
- "heun",
- "heun_k",
- "euler",
- "euler_k",
- "euler_a",
- "kdpm_2",
- "kdpm_2_k",
- "kdpm_2_a",
- "kdpm_2_a_k",
- "dpmpp_2s",
- "dpmpp_2s_k",
- "dpmpp_2m",
- "dpmpp_2m_k",
- "dpmpp_2m_sde",
- "dpmpp_2m_sde_k",
- "dpmpp_3m",
- "dpmpp_3m_k",
- "dpmpp_sde",
- "dpmpp_sde_k",
- "er_sde",
- "unipc",
- "unipc_k",
- "lcm",
- "tcd"
- ],
- "field_kind": "input",
- "input": "any",
- "orig_default": "euler",
- "orig_required": false,
- "title": "Scheduler",
- "type": "string",
- "ui_type": "SchedulerField"
+ {
+ "type": "null"
+ }
+ ],
+ "title": "Source Url",
+ "description": "Optional URL for the model (e.g. download page or model page)."
},
- "unet": {
+ "cover_image": {
"anyOf": [
{
- "$ref": "#/components/schemas/UNetField"
+ "type": "string"
},
{
"type": "null"
}
],
- "default": null,
- "description": "UNet (scheduler, LoRAs)",
- "field_kind": "input",
- "input": "connection",
- "orig_required": true,
- "title": "UNet"
+ "title": "Cover Image",
+ "description": "Url for image to preview model"
},
- "cfg_rescale_multiplier": {
- "default": 0,
- "description": "Rescale multiplier for CFG guidance, used for models trained with zero-terminal SNR",
- "exclusiveMaximum": 1,
- "field_kind": "input",
- "input": "any",
- "minimum": 0,
- "orig_default": 0,
- "orig_required": false,
- "title": "CFG Rescale Multiplier",
- "type": "number"
+ "format": {
+ "type": "string",
+ "const": "diffusers",
+ "title": "Format",
+ "default": "diffusers"
},
- "control": {
+ "repo_variant": {
+ "$ref": "#/components/schemas/ModelRepoVariant",
+ "default": ""
+ },
+ "type": {
+ "type": "string",
+ "const": "vae",
+ "title": "Type",
+ "default": "vae"
+ },
+ "base": {
+ "type": "string",
+ "const": "wan",
+ "title": "Base",
+ "default": "wan"
+ },
+ "cpu_only": {
"anyOf": [
{
- "$ref": "#/components/schemas/ControlField"
- },
- {
- "items": {
- "$ref": "#/components/schemas/ControlField"
- },
- "type": "array"
+ "type": "boolean"
},
{
"type": "null"
}
],
- "default": null,
- "field_kind": "input",
- "input": "connection",
- "orig_default": null,
- "orig_required": false,
- "title": "Control"
+ "title": "Cpu Only",
+ "description": "Whether this model should run on CPU only"
},
- "type": {
- "const": "tiled_multi_diffusion_denoise_latents",
- "default": "tiled_multi_diffusion_denoise_latents",
- "field_kind": "node_attribute",
- "title": "type",
- "type": "string"
+ "latent_channels": {
+ "type": "integer",
+ "enum": [16, 48],
+ "title": "Latent Channels",
+ "description": "VAE latent channel count: 16 for A14B or 48 for TI2V-5B's Wan2.2-VAE.",
+ "default": 16
}
},
- "required": ["type", "id"],
- "tags": ["upscale", "denoise"],
- "title": "Tiled Multi-Diffusion Denoise - SD1.5, SDXL",
"type": "object",
- "version": "1.0.1",
- "output": {
- "$ref": "#/components/schemas/LatentsOutput"
- }
+ "required": [
+ "key",
+ "hash",
+ "path",
+ "file_size",
+ "name",
+ "description",
+ "source",
+ "source_type",
+ "source_api_response",
+ "source_url",
+ "cover_image",
+ "format",
+ "repo_variant",
+ "type",
+ "base",
+ "cpu_only",
+ "latent_channels"
+ ],
+ "title": "VAE_Diffusers_Wan_Config",
+ "description": "Model config for Wan 2.2 VAE in diffusers folder layout (AutoencoderKLWan)."
},
- "TransformerField": {
+ "ValidationError": {
"properties": {
- "transformer": {
- "$ref": "#/components/schemas/ModelIdentifierField",
- "description": "Info to load Transformer submodel"
- },
- "loras": {
- "description": "LoRAs to apply on model loading",
+ "loc": {
"items": {
- "$ref": "#/components/schemas/LoRAField"
+ "anyOf": [
+ {
+ "type": "string"
+ },
+ {
+ "type": "integer"
+ }
+ ]
},
- "title": "Loras",
- "type": "array"
+ "type": "array",
+ "title": "Location"
+ },
+ "msg": {
+ "type": "string",
+ "title": "Message"
+ },
+ "type": {
+ "type": "string",
+ "title": "Error Type"
}
},
- "required": ["transformer", "loras"],
- "title": "TransformerField",
- "type": "object"
+ "type": "object",
+ "required": ["loc", "msg", "type"],
+ "title": "ValidationError"
},
- "UIComponent": {
- "description": "The type of UI component to use for a field, used to override the default components, which are\ninferred from the field type.",
- "enum": ["none", "textarea", "slider"],
- "title": "UIComponent",
- "type": "string"
+ "VideoBoardArg": {
+ "properties": {
+ "board_id": {
+ "type": "string",
+ "title": "Board Id",
+ "description": "The id of the board to add or remove the video from"
+ },
+ "video_name": {
+ "type": "string",
+ "title": "Video Name",
+ "description": "The name of the video to add to / remove from the board"
+ }
+ },
+ "type": "object",
+ "required": ["board_id", "video_name"],
+ "title": "VideoBoardArg"
},
- "UIConfigBase": {
- "description": "Provides additional node configuration to the UI.\nThis is used internally by the @invocation decorator logic. Do not use this directly.",
+ "VideoConcatInvocation": {
+ "category": "video",
+ "class": "invocation",
+ "classification": "prototype",
+ "description": "Join two or more videos into a single MP4.\n\nTransitions:\n\n* ``cut`` \u2014 hard splice, no blending. Fastest; total length is the sum of inputs.\n* ``crossfade`` \u2014 linear A\u2192B cross-dissolve over ``transition_frames``. Each boundary\n consumes ``transition_frames`` from both adjacent clips, so total length is\n ``sum(inputs) - transition_frames * (n - 1)``.\n* ``fade_through_black`` \u2014 A fades to black, then B fades in from black. Each boundary\n consumes ``transition_frames // 2`` frames from the preceding clip's tail and the\n remainder (``transition_frames - transition_frames // 2``) from the next clip's head,\n so the total emitted is exactly ``transition_frames`` per boundary \u2014 even for odd\n ``transition_frames`` \u2014 and the overall length equals the sum of inputs.\n\nAll inputs must share the same pixel dimensions. Output frame rate defaults to the\nfirst input's fps; override with ``fps`` to force a specific rate (the frames are not\nresampled, only the container is encoded at the new rate).",
+ "node_pack": "invokeai",
"properties": {
- "tags": {
+ "board": {
"anyOf": [
{
- "items": {
- "type": "string"
- },
- "type": "array"
+ "$ref": "#/components/schemas/BoardField"
},
{
"type": "null"
}
],
"default": null,
- "description": "The node's tags",
- "title": "Tags"
+ "description": "The board to save the image to",
+ "field_kind": "internal",
+ "input": "direct",
+ "orig_required": false,
+ "ui_hidden": false
},
- "title": {
+ "metadata": {
"anyOf": [
{
- "type": "string"
+ "$ref": "#/components/schemas/MetadataField"
},
{
"type": "null"
}
],
"default": null,
- "description": "The node's display name",
- "title": "Title"
+ "description": "Optional metadata to be saved with the image",
+ "field_kind": "internal",
+ "input": "connection",
+ "orig_required": false,
+ "ui_hidden": false
},
- "category": {
+ "id": {
+ "description": "The id of this instance of an invocation. Must be unique among all instances of invocations.",
+ "field_kind": "node_attribute",
+ "title": "Id",
+ "type": "string"
+ },
+ "is_intermediate": {
+ "default": false,
+ "description": "Whether or not this is an intermediate invocation.",
+ "field_kind": "node_attribute",
+ "input": "direct",
+ "orig_required": true,
+ "title": "Is Intermediate",
+ "type": "boolean",
+ "ui_hidden": false,
+ "ui_type": "IsIntermediate"
+ },
+ "use_cache": {
+ "default": true,
+ "description": "Whether or not to use the cache",
+ "field_kind": "node_attribute",
+ "title": "Use Cache",
+ "type": "boolean"
+ },
+ "videos": {
"anyOf": [
{
- "type": "string"
+ "items": {
+ "$ref": "#/components/schemas/VideoField"
+ },
+ "minItems": 2,
+ "type": "array"
},
{
"type": "null"
}
],
"default": null,
- "description": "The node's category",
- "title": "Category"
- },
- "version": {
- "description": "The node's version. Should be a valid semver string e.g. \"1.0.0\" or \"3.8.13\".",
- "title": "Version",
- "type": "string"
+ "description": "Videos to concatenate, in order. At least two are required.",
+ "field_kind": "input",
+ "input": "any",
+ "orig_required": true,
+ "title": "Videos"
},
- "node_pack": {
- "description": "The node pack that this node belongs to, will be 'invokeai' for built-in nodes",
- "title": "Node Pack",
+ "transition": {
+ "default": "cut",
+ "description": "Transition between consecutive clips.",
+ "enum": ["cut", "crossfade", "fade_through_black"],
+ "field_kind": "input",
+ "input": "any",
+ "orig_default": "cut",
+ "orig_required": false,
+ "title": "Transition",
"type": "string"
},
- "classification": {
- "$ref": "#/components/schemas/Classification",
- "default": "stable",
- "description": "The node's classification"
- }
- },
- "required": ["tags", "title", "category", "version", "node_pack", "classification"],
- "title": "UIConfigBase",
- "type": "object"
- },
- "UIType": {
- "description": "Type hints for the UI for situations in which the field type is not enough to infer the correct UI type.\n\n- Model Fields\nThe most common node-author-facing use will be for model fields. Internally, there is no difference\nbetween SD-1, SD-2 and SDXL model fields - they all use the class `MainModelField`. To ensure the\nbase-model-specific UI is rendered, use e.g. `ui_type=UIType.SDXLMainModelField` to indicate that\nthe field is an SDXL main model field.\n\n- Any Field\nWe cannot infer the usage of `typing.Any` via schema parsing, so you *must* use `ui_type=UIType.Any` to\nindicate that the field accepts any type. Use with caution. This cannot be used on outputs.\n\n- Scheduler Field\nSpecial handling in the UI is needed for this field, which otherwise would be parsed as a plain enum field.\n\n- Internal Fields\nSimilar to the Any Field, the `collect` and `iterate` nodes make use of `typing.Any`. To facilitate\nhandling these types in the client, we use `UIType._Collection` and `UIType._CollectionItem`. These\nshould not be used by node authors.\n\n- DEPRECATED Fields\nThese types are deprecated and should not be used by node authors. A warning will be logged if one is\nused, and the type will be ignored. They are included here for backwards compatibility.",
- "enum": [
- "SchedulerField",
- "AnyField",
- "SavedWorkflowField",
- "CollectionField",
- "CollectionItemField",
- "IsIntermediate",
- "DEPRECATED_Boolean",
- "DEPRECATED_Color",
- "DEPRECATED_Conditioning",
- "DEPRECATED_Control",
- "DEPRECATED_Float",
- "DEPRECATED_Image",
- "DEPRECATED_Integer",
- "DEPRECATED_Latents",
- "DEPRECATED_String",
- "DEPRECATED_BooleanCollection",
- "DEPRECATED_ColorCollection",
- "DEPRECATED_ConditioningCollection",
- "DEPRECATED_ControlCollection",
- "DEPRECATED_FloatCollection",
- "DEPRECATED_ImageCollection",
- "DEPRECATED_IntegerCollection",
- "DEPRECATED_LatentsCollection",
- "DEPRECATED_StringCollection",
- "DEPRECATED_BooleanPolymorphic",
- "DEPRECATED_ColorPolymorphic",
- "DEPRECATED_ConditioningPolymorphic",
- "DEPRECATED_ControlPolymorphic",
- "DEPRECATED_FloatPolymorphic",
- "DEPRECATED_ImagePolymorphic",
- "DEPRECATED_IntegerPolymorphic",
- "DEPRECATED_LatentsPolymorphic",
- "DEPRECATED_StringPolymorphic",
- "DEPRECATED_UNet",
- "DEPRECATED_Vae",
- "DEPRECATED_CLIP",
- "DEPRECATED_Collection",
- "DEPRECATED_CollectionItem",
- "DEPRECATED_Enum",
- "DEPRECATED_WorkflowField",
- "DEPRECATED_BoardField",
- "DEPRECATED_MetadataItem",
- "DEPRECATED_MetadataItemCollection",
- "DEPRECATED_MetadataItemPolymorphic",
- "DEPRECATED_MetadataDict",
- "DEPRECATED_MainModelField",
- "DEPRECATED_CogView4MainModelField",
- "DEPRECATED_FluxMainModelField",
- "DEPRECATED_SD3MainModelField",
- "DEPRECATED_SDXLMainModelField",
- "DEPRECATED_SDXLRefinerModelField",
- "DEPRECATED_ONNXModelField",
- "DEPRECATED_VAEModelField",
- "DEPRECATED_FluxVAEModelField",
- "DEPRECATED_LoRAModelField",
- "DEPRECATED_ControlNetModelField",
- "DEPRECATED_IPAdapterModelField",
- "DEPRECATED_T2IAdapterModelField",
- "DEPRECATED_T5EncoderModelField",
- "DEPRECATED_CLIPEmbedModelField",
- "DEPRECATED_CLIPLEmbedModelField",
- "DEPRECATED_CLIPGEmbedModelField",
- "DEPRECATED_SpandrelImageToImageModelField",
- "DEPRECATED_ControlLoRAModelField",
- "DEPRECATED_SigLipModelField",
- "DEPRECATED_FluxReduxModelField",
- "DEPRECATED_LLaVAModelField",
- "DEPRECATED_Imagen3ModelField",
- "DEPRECATED_Imagen4ModelField",
- "DEPRECATED_ChatGPT4oModelField",
- "DEPRECATED_Gemini2_5ModelField",
- "DEPRECATED_FluxKontextModelField",
- "DEPRECATED_Veo3ModelField",
- "DEPRECATED_RunwayModelField"
- ],
- "title": "UIType",
- "type": "string"
- },
- "UNetField": {
- "properties": {
- "unet": {
- "$ref": "#/components/schemas/ModelIdentifierField",
- "description": "Info to load unet submodel"
- },
- "scheduler": {
- "$ref": "#/components/schemas/ModelIdentifierField",
- "description": "Info to load scheduler submodel"
- },
- "loras": {
- "description": "LoRAs to apply on model loading",
- "items": {
- "$ref": "#/components/schemas/LoRAField"
- },
- "title": "Loras",
- "type": "array"
- },
- "seamless_axes": {
- "description": "Axes(\"x\" and \"y\") to which apply seamless",
- "items": {
- "type": "string"
- },
- "title": "Seamless Axes",
- "type": "array"
+ "transition_frames": {
+ "default": 8,
+ "description": "Length of each transition in frames. Ignored when transition is 'cut'.",
+ "field_kind": "input",
+ "input": "any",
+ "maximum": 240,
+ "minimum": 0,
+ "orig_default": 8,
+ "orig_required": false,
+ "title": "Transition Frames",
+ "type": "integer"
},
- "freeu_config": {
+ "fps": {
"anyOf": [
{
- "$ref": "#/components/schemas/FreeUConfig"
+ "maximum": 120,
+ "minimum": 1,
+ "type": "integer"
},
{
"type": "null"
}
],
"default": null,
- "description": "FreeU configuration"
- }
- },
- "required": ["unet", "scheduler", "loras"],
- "title": "UNetField",
- "type": "object"
- },
- "UNetOutput": {
- "class": "output",
- "description": "Base class for invocations that output a UNet field.",
- "properties": {
- "unet": {
- "$ref": "#/components/schemas/UNetField",
- "description": "UNet (scheduler, LoRAs)",
- "field_kind": "output",
- "title": "UNet",
- "ui_hidden": false
+ "description": "Output frame rate. Defaults to the first input's fps.",
+ "field_kind": "input",
+ "input": "any",
+ "orig_default": null,
+ "orig_required": false,
+ "title": "Fps"
},
"type": {
- "const": "unet_output",
- "default": "unet_output",
+ "const": "video_concat",
+ "default": "video_concat",
"field_kind": "node_attribute",
"title": "type",
"type": "string"
}
},
- "required": ["output_meta", "unet", "type", "type"],
- "title": "UNetOutput",
- "type": "object"
- },
- "URLModelSource": {
- "properties": {
- "url": {
- "type": "string",
- "minLength": 1,
- "format": "uri",
- "title": "Url"
- },
- "access_token": {
- "anyOf": [
- {
- "type": "string"
- },
- {
- "type": "null"
- }
- ],
- "title": "Access Token"
- },
- "type": {
- "type": "string",
- "const": "url",
- "title": "Type",
- "default": "url"
- }
- },
+ "required": ["type", "id"],
+ "tags": ["video", "concat", "transition"],
+ "title": "Concatenate Videos",
"type": "object",
- "required": ["url"],
- "title": "URLModelSource",
- "description": "A generic URL point to a checkpoint file."
+ "version": "1.0.0",
+ "output": {
+ "$ref": "#/components/schemas/VideoOutput"
+ }
},
- "URLRegexTokenPair": {
+ "VideoDTO": {
"properties": {
- "url_regex": {
+ "video_name": {
"type": "string",
- "title": "Url Regex",
- "description": "Regular expression to match against the URL"
+ "title": "Video Name",
+ "description": "The unique name of the video."
},
- "token": {
- "type": "string",
- "title": "Token",
- "description": "Token to use when the URL matches the regex"
- }
- },
- "type": "object",
- "required": ["url_regex", "token"],
- "title": "URLRegexTokenPair"
- },
- "UninstallNodePackResponse": {
- "properties": {
- "name": {
+ "video_url": {
"type": "string",
- "title": "Name",
- "description": "The name of the uninstalled node pack."
- },
- "success": {
- "type": "boolean",
- "title": "Success",
- "description": "Whether the uninstall was successful."
+ "title": "Video Url",
+ "description": "The URL of the video file (MP4)."
},
- "message": {
- "type": "string",
- "title": "Message",
- "description": "Status message."
- }
- },
- "type": "object",
- "required": ["name", "success", "message"],
- "title": "UninstallNodePackResponse",
- "description": "Response after uninstalling a node pack."
- },
- "Unknown_Config": {
- "properties": {
- "key": {
+ "thumbnail_url": {
"type": "string",
- "title": "Key",
- "description": "A unique key for this model."
+ "title": "Thumbnail Url",
+ "description": "The URL of the video's first-frame thumbnail (WebP)."
},
- "hash": {
- "type": "string",
- "title": "Hash",
- "description": "The hash of the model file(s)."
+ "video_origin": {
+ "$ref": "#/components/schemas/ResourceOrigin",
+ "description": "The origin of the video."
},
- "path": {
- "type": "string",
- "title": "Path",
- "description": "Path to the model on the filesystem. Relative paths are relative to the Invoke root directory."
+ "video_category": {
+ "$ref": "#/components/schemas/ImageCategory",
+ "description": "The category of the video (reuses ImageCategory)."
},
- "file_size": {
+ "width": {
"type": "integer",
- "title": "File Size",
- "description": "The size of the model in bytes."
+ "title": "Width",
+ "description": "The pixel width of the video."
},
- "name": {
- "type": "string",
- "title": "Name",
- "description": "Name of the model."
+ "height": {
+ "type": "integer",
+ "title": "Height",
+ "description": "The pixel height of the video."
},
- "description": {
+ "duration": {
+ "type": "number",
+ "title": "Duration",
+ "description": "The duration of the video in seconds."
+ },
+ "fps": {
"anyOf": [
{
- "type": "string"
+ "type": "number"
},
{
"type": "null"
}
],
- "title": "Description",
- "description": "Model description"
+ "title": "Fps",
+ "description": "The frames-per-second of the video, if known."
},
- "source": {
- "type": "string",
- "title": "Source",
- "description": "The original source of the model (path, URL or repo_id)."
+ "created_at": {
+ "anyOf": [
+ {
+ "type": "string",
+ "format": "date-time"
+ },
+ {
+ "type": "string"
+ }
+ ],
+ "title": "Created At",
+ "description": "The created timestamp of the video."
},
- "source_type": {
- "$ref": "#/components/schemas/ModelSourceType",
- "description": "The type of source"
+ "updated_at": {
+ "anyOf": [
+ {
+ "type": "string",
+ "format": "date-time"
+ },
+ {
+ "type": "string"
+ }
+ ],
+ "title": "Updated At",
+ "description": "The updated timestamp of the video."
},
- "source_api_response": {
+ "deleted_at": {
"anyOf": [
+ {
+ "type": "string",
+ "format": "date-time"
+ },
{
"type": "string"
},
@@ -71415,10 +77458,15 @@
"type": "null"
}
],
- "title": "Source Api Response",
- "description": "The original API response from the source, as stringified JSON."
+ "title": "Deleted At",
+ "description": "The deleted timestamp of the video."
},
- "source_url": {
+ "is_intermediate": {
+ "type": "boolean",
+ "title": "Is Intermediate",
+ "description": "Whether this is an intermediate video."
+ },
+ "session_id": {
"anyOf": [
{
"type": "string"
@@ -71427,10 +77475,10 @@
"type": "null"
}
],
- "title": "Source Url",
- "description": "Optional URL for the model (e.g. download page or model page)."
+ "title": "Session Id",
+ "description": "The session ID that produced this video, if any."
},
- "cover_image": {
+ "node_id": {
"anyOf": [
{
"type": "string"
@@ -71439,53 +77487,75 @@
"type": "null"
}
],
- "title": "Cover Image",
- "description": "Url for image to preview model"
+ "title": "Node Id",
+ "description": "The node ID that produced this video, if any."
},
- "base": {
- "type": "string",
- "const": "unknown",
- "title": "Base",
- "default": "unknown"
+ "starred": {
+ "type": "boolean",
+ "title": "Starred",
+ "description": "Whether this video is starred."
},
- "type": {
- "type": "string",
- "const": "unknown",
- "title": "Type",
- "default": "unknown"
+ "has_workflow": {
+ "type": "boolean",
+ "title": "Has Workflow",
+ "description": "Whether this video has a workflow associated."
},
- "format": {
+ "video_subfolder": {
"type": "string",
- "const": "unknown",
- "title": "Format",
- "default": "unknown"
+ "title": "Video Subfolder",
+ "description": "The subfolder where the video is stored on disk.",
+ "default": ""
+ },
+ "board_id": {
+ "anyOf": [
+ {
+ "type": "string"
+ },
+ {
+ "type": "null"
+ }
+ ],
+ "title": "Board Id",
+ "description": "The id of the board the video belongs to, if one exists."
}
},
"type": "object",
"required": [
- "key",
- "hash",
- "path",
- "file_size",
- "name",
- "description",
- "source",
- "source_type",
- "source_api_response",
- "source_url",
- "cover_image",
- "base",
- "type",
- "format"
+ "video_name",
+ "video_url",
+ "thumbnail_url",
+ "video_origin",
+ "video_category",
+ "width",
+ "height",
+ "duration",
+ "created_at",
+ "updated_at",
+ "is_intermediate",
+ "starred",
+ "has_workflow"
],
- "title": "Unknown_Config",
- "description": "Model config for unknown models, used as a fallback when we cannot positively identify a model."
+ "title": "VideoDTO",
+ "description": "Deserialized video record, enriched for the frontend."
},
- "UnsharpMaskInvocation": {
+ "VideoField": {
+ "properties": {
+ "video_name": {
+ "type": "string",
+ "title": "Video Name",
+ "description": "The name of the video"
+ }
+ },
+ "type": "object",
+ "required": ["video_name"],
+ "title": "VideoField",
+ "description": "A video primitive field"
+ },
+ "VideoFrameExtractInvocation": {
"category": "image",
"class": "invocation",
- "classification": "stable",
- "description": "Applies an unsharp mask filter to an image",
+ "classification": "prototype",
+ "description": "Extract a single frame from a video and save it as an image.\n\n``frame_index`` is 0-based. Negative indices count from the end, so the\ndefault of -1 returns the final frame \u2014 the typical setup for chaining\nI2V clips into a longer sequence.",
"node_pack": "invokeai",
"properties": {
"board": {
@@ -71544,243 +77614,54 @@
"title": "Use Cache",
"type": "boolean"
},
- "image": {
+ "video": {
"anyOf": [
{
- "$ref": "#/components/schemas/ImageField"
+ "$ref": "#/components/schemas/VideoField"
},
{
"type": "null"
}
],
"default": null,
- "description": "The image to use",
+ "description": "The video to extract a frame from.",
"field_kind": "input",
"input": "any",
"orig_required": true
},
- "radius": {
- "default": 2,
- "description": "Unsharp mask radius",
- "exclusiveMinimum": 0,
- "field_kind": "input",
- "input": "any",
- "orig_default": 2,
- "orig_required": false,
- "title": "Radius",
- "type": "number"
- },
- "strength": {
- "default": 50,
- "description": "Unsharp mask strength",
+ "frame_index": {
+ "default": -1,
+ "description": "Index of the frame to extract. 0 = first frame, -1 = last frame, -2 = second-to-last, etc.",
"field_kind": "input",
"input": "any",
- "minimum": 0,
- "orig_default": 50,
+ "orig_default": -1,
"orig_required": false,
- "title": "Strength",
- "type": "number"
+ "title": "Frame Index",
+ "type": "integer",
+ "ui_component": "video-frame-index"
},
"type": {
- "const": "unsharp_mask",
- "default": "unsharp_mask",
+ "const": "video_frame_extract",
+ "default": "video_frame_extract",
"field_kind": "node_attribute",
"title": "type",
"type": "string"
}
},
"required": ["type", "id"],
- "tags": ["image", "unsharp_mask"],
- "title": "Unsharp Mask",
+ "tags": ["video", "image", "frame"],
+ "title": "Frame from Video",
"type": "object",
- "version": "1.2.2",
+ "version": "1.1.0",
"output": {
"$ref": "#/components/schemas/ImageOutput"
}
},
- "UnstarredImagesResult": {
- "properties": {
- "affected_boards": {
- "items": {
- "type": "string"
- },
- "type": "array",
- "title": "Affected Boards",
- "description": "The ids of boards affected by the delete operation"
- },
- "unstarred_images": {
- "items": {
- "type": "string"
- },
- "type": "array",
- "title": "Unstarred Images",
- "description": "The names of the images that were unstarred"
- }
- },
- "type": "object",
- "required": ["affected_boards", "unstarred_images"],
- "title": "UnstarredImagesResult"
- },
- "UpdateAppGenerationSettingsRequest": {
- "properties": {
- "image_subfolder_strategy": {
- "type": "string",
- "enum": ["flat", "date", "type", "hash"],
- "title": "Image Subfolder Strategy",
- "description": "Strategy for organizing images into subfolders."
- },
- "max_queue_history": {
- "anyOf": [
- {
- "type": "integer",
- "minimum": 0.0
- },
- {
- "type": "null"
- }
- ],
- "title": "Max Queue History",
- "description": "Keep the last N completed, failed, and canceled queue items on startup. Set to 0 to prune all terminal items."
- }
- },
- "type": "object",
- "title": "UpdateAppGenerationSettingsRequest",
- "description": "Writable generation-related app settings."
- },
- "UserDTO": {
- "properties": {
- "user_id": {
- "type": "string",
- "title": "User Id",
- "description": "Unique user identifier"
- },
- "email": {
- "type": "string",
- "title": "Email",
- "description": "User email address"
- },
- "display_name": {
- "anyOf": [
- {
- "type": "string"
- },
- {
- "type": "null"
- }
- ],
- "title": "Display Name",
- "description": "Display name"
- },
- "is_admin": {
- "type": "boolean",
- "title": "Is Admin",
- "description": "Whether user has admin privileges",
- "default": false
- },
- "is_active": {
- "type": "boolean",
- "title": "Is Active",
- "description": "Whether user account is active",
- "default": true
- },
- "created_at": {
- "type": "string",
- "format": "date-time",
- "title": "Created At",
- "description": "When the user was created"
- },
- "updated_at": {
- "type": "string",
- "format": "date-time",
- "title": "Updated At",
- "description": "When the user was last updated"
- },
- "last_login_at": {
- "anyOf": [
- {
- "type": "string",
- "format": "date-time"
- },
- {
- "type": "null"
- }
- ],
- "title": "Last Login At",
- "description": "When user last logged in"
- }
- },
- "type": "object",
- "required": ["user_id", "email", "created_at", "updated_at"],
- "title": "UserDTO",
- "description": "User data transfer object."
- },
- "UserProfileUpdateRequest": {
- "properties": {
- "display_name": {
- "anyOf": [
- {
- "type": "string"
- },
- {
- "type": "null"
- }
- ],
- "title": "Display Name",
- "description": "New display name"
- },
- "current_password": {
- "anyOf": [
- {
- "type": "string"
- },
- {
- "type": "null"
- }
- ],
- "title": "Current Password",
- "description": "Current password (required when changing password)"
- },
- "new_password": {
- "anyOf": [
- {
- "type": "string"
- },
- {
- "type": "null"
- }
- ],
- "title": "New Password",
- "description": "New password"
- }
- },
- "type": "object",
- "title": "UserProfileUpdateRequest",
- "description": "Request body for a user to update their own profile."
- },
- "VAEField": {
- "properties": {
- "vae": {
- "$ref": "#/components/schemas/ModelIdentifierField",
- "description": "Info to load vae submodel"
- },
- "seamless_axes": {
- "description": "Axes(\"x\" and \"y\") to which apply seamless",
- "items": {
- "type": "string"
- },
- "title": "Seamless Axes",
- "type": "array"
- }
- },
- "required": ["vae"],
- "title": "VAEField",
- "type": "object"
- },
- "VAELoaderInvocation": {
- "category": "model",
+ "VideoInvocation": {
+ "category": "primitives",
"class": "invocation",
"classification": "stable",
- "description": "Loads a VAE model, outputting a VaeLoaderOutput",
+ "description": "A video primitive value. Drop a video onto the field to make it available as an input\nto downstream nodes (e.g. Frame from Video, Concatenate Videos).",
"node_pack": "invokeai",
"properties": {
"id": {
@@ -71807,262 +77688,135 @@
"title": "Use Cache",
"type": "boolean"
},
- "vae_model": {
+ "video": {
"anyOf": [
{
- "$ref": "#/components/schemas/ModelIdentifierField"
+ "$ref": "#/components/schemas/VideoField"
},
{
"type": "null"
}
],
"default": null,
- "description": "VAE model to load",
+ "description": "The video to load",
"field_kind": "input",
"input": "any",
- "orig_required": true,
- "title": "VAE",
- "ui_model_base": ["sd-1", "sd-2", "sdxl", "sd-3", "flux", "flux2"],
- "ui_model_type": ["vae"]
+ "orig_required": true
},
"type": {
- "const": "vae_loader",
- "default": "vae_loader",
+ "const": "video",
+ "default": "video",
"field_kind": "node_attribute",
"title": "type",
"type": "string"
}
},
"required": ["type", "id"],
- "tags": ["vae", "model"],
- "title": "VAE Model - SD1.5, SD2, SDXL, SD3, FLUX",
+ "tags": ["primitives", "video"],
+ "title": "Video Primitive",
"type": "object",
- "version": "1.0.4",
+ "version": "1.0.0",
"output": {
- "$ref": "#/components/schemas/VAEOutput"
+ "$ref": "#/components/schemas/VideoOutput"
}
},
- "VAEOutput": {
- "class": "output",
- "description": "Base class for invocations that output a VAE field",
- "properties": {
- "vae": {
- "$ref": "#/components/schemas/VAEField",
- "description": "VAE",
- "field_kind": "output",
- "title": "VAE",
- "ui_hidden": false
- },
- "type": {
- "const": "vae_output",
- "default": "vae_output",
- "field_kind": "node_attribute",
- "title": "type",
- "type": "string"
- }
- },
- "required": ["output_meta", "vae", "type", "type"],
- "title": "VAEOutput",
- "type": "object"
- },
- "VAE_Checkpoint_Anima_Config": {
+ "VideoNamesResult": {
"properties": {
- "key": {
- "type": "string",
- "title": "Key",
- "description": "A unique key for this model."
- },
- "hash": {
- "type": "string",
- "title": "Hash",
- "description": "The hash of the model file(s)."
- },
- "path": {
- "type": "string",
- "title": "Path",
- "description": "Path to the model on the filesystem. Relative paths are relative to the Invoke root directory."
+ "video_names": {
+ "items": {
+ "type": "string"
+ },
+ "type": "array",
+ "title": "Video Names",
+ "description": "Ordered list of video names"
},
- "file_size": {
+ "starred_count": {
"type": "integer",
- "title": "File Size",
- "description": "The size of the model in bytes."
- },
- "name": {
- "type": "string",
- "title": "Name",
- "description": "Name of the model."
- },
- "description": {
- "anyOf": [
- {
- "type": "string"
- },
- {
- "type": "null"
- }
- ],
- "title": "Description",
- "description": "Model description"
- },
- "source": {
- "type": "string",
- "title": "Source",
- "description": "The original source of the model (path, URL or repo_id)."
- },
- "source_type": {
- "$ref": "#/components/schemas/ModelSourceType",
- "description": "The type of source"
- },
- "source_api_response": {
- "anyOf": [
- {
- "type": "string"
- },
- {
- "type": "null"
- }
- ],
- "title": "Source Api Response",
- "description": "The original API response from the source, as stringified JSON."
- },
- "source_url": {
- "anyOf": [
- {
- "type": "string"
- },
- {
- "type": "null"
- }
- ],
- "title": "Source Url",
- "description": "Optional URL for the model (e.g. download page or model page)."
- },
- "cover_image": {
- "anyOf": [
- {
- "type": "string"
- },
- {
- "type": "null"
- }
- ],
- "title": "Cover Image",
- "description": "Url for image to preview model"
- },
- "config_path": {
- "anyOf": [
- {
- "type": "string"
- },
- {
- "type": "null"
- }
- ],
- "title": "Config Path",
- "description": "Path to the config for this model, if any."
- },
- "type": {
- "type": "string",
- "const": "vae",
- "title": "Type",
- "default": "vae"
- },
- "format": {
- "type": "string",
- "const": "checkpoint",
- "title": "Format",
- "default": "checkpoint"
- },
- "base": {
- "type": "string",
- "const": "anima",
- "title": "Base",
- "default": "anima"
+ "title": "Starred Count",
+ "description": "Number of starred videos (when starred_first=True)"
},
- "cpu_only": {
- "anyOf": [
- {
- "type": "boolean"
- },
- {
- "type": "null"
- }
- ],
- "title": "Cpu Only",
- "description": "Whether this model should run on CPU only"
+ "total_count": {
+ "type": "integer",
+ "title": "Total Count",
+ "description": "Total number of videos matching the query"
}
},
"type": "object",
- "required": [
- "key",
- "hash",
- "path",
- "file_size",
- "name",
- "description",
- "source",
- "source_type",
- "source_api_response",
- "source_url",
- "cover_image",
- "config_path",
- "type",
- "format",
- "base",
- "cpu_only"
- ],
- "title": "VAE_Checkpoint_Anima_Config",
- "description": "Model config for Anima QwenImage VAE checkpoint models (AutoencoderKLQwenImage)."
+ "required": ["video_names", "starred_count", "total_count"],
+ "title": "VideoNamesResult",
+ "description": "Response containing ordered video names with metadata for optimistic updates."
},
- "VAE_Checkpoint_FLUX_Config": {
+ "VideoOutput": {
+ "class": "output",
+ "description": "Output of a node that produces a video file (e.g. Wan 2.2 latents-to-video).",
"properties": {
- "key": {
- "type": "string",
- "title": "Key",
- "description": "A unique key for this model."
+ "video": {
+ "$ref": "#/components/schemas/VideoField",
+ "description": "The output video",
+ "field_kind": "output",
+ "ui_hidden": false
},
- "hash": {
- "type": "string",
- "title": "Hash",
- "description": "The hash of the model file(s)."
+ "width": {
+ "description": "The width of the video in pixels",
+ "field_kind": "output",
+ "title": "Width",
+ "type": "integer",
+ "ui_hidden": false
},
- "path": {
- "type": "string",
- "title": "Path",
- "description": "Path to the model on the filesystem. Relative paths are relative to the Invoke root directory."
+ "height": {
+ "description": "The height of the video in pixels",
+ "field_kind": "output",
+ "title": "Height",
+ "type": "integer",
+ "ui_hidden": false
},
- "file_size": {
+ "num_frames": {
+ "description": "The number of frames in the video",
+ "field_kind": "output",
+ "title": "Num Frames",
"type": "integer",
- "title": "File Size",
- "description": "The size of the model in bytes."
+ "ui_hidden": false
},
- "name": {
- "type": "string",
- "title": "Name",
- "description": "Name of the model."
+ "fps": {
+ "description": "The frames-per-second of the video",
+ "field_kind": "output",
+ "title": "Fps",
+ "type": "number",
+ "ui_hidden": false
},
- "description": {
+ "duration": {
+ "description": "The duration of the video in seconds",
+ "field_kind": "output",
+ "title": "Duration",
+ "type": "number",
+ "ui_hidden": false
+ },
+ "type": {
+ "const": "video_output",
+ "default": "video_output",
+ "field_kind": "node_attribute",
+ "title": "type",
+ "type": "string"
+ }
+ },
+ "required": ["output_meta", "video", "width", "height", "num_frames", "fps", "duration", "type", "type"],
+ "title": "VideoOutput",
+ "type": "object"
+ },
+ "VideoRecordChanges": {
+ "properties": {
+ "video_category": {
"anyOf": [
{
- "type": "string"
+ "$ref": "#/components/schemas/ImageCategory"
},
{
"type": "null"
}
],
- "title": "Description",
- "description": "Model description"
- },
- "source": {
- "type": "string",
- "title": "Source",
- "description": "The original source of the model (path, URL or repo_id)."
- },
- "source_type": {
- "$ref": "#/components/schemas/ModelSourceType",
- "description": "The type of source"
+ "description": "The video's new category."
},
- "source_api_response": {
+ "session_id": {
"anyOf": [
{
"type": "string"
@@ -72071,34 +77825,96 @@
"type": "null"
}
],
- "title": "Source Api Response",
- "description": "The original API response from the source, as stringified JSON."
+ "title": "Session Id",
+ "description": "The video's new session ID."
},
- "source_url": {
+ "is_intermediate": {
"anyOf": [
{
- "type": "string"
+ "type": "boolean"
},
{
"type": "null"
}
],
- "title": "Source Url",
- "description": "Optional URL for the model (e.g. download page or model page)."
+ "title": "Is Intermediate",
+ "description": "The video's new `is_intermediate` flag."
},
- "cover_image": {
+ "starred": {
"anyOf": [
{
- "type": "string"
+ "type": "boolean"
},
{
"type": "null"
}
],
- "title": "Cover Image",
- "description": "Url for image to preview model"
+ "title": "Starred",
+ "description": "The video's new `starred` state."
+ }
+ },
+ "additionalProperties": true,
+ "type": "object",
+ "title": "VideoRecordChanges",
+ "description": "Allowed mutations on a video record."
+ },
+ "VideoUrlsDTO": {
+ "properties": {
+ "video_name": {
+ "type": "string",
+ "title": "Video Name",
+ "description": "The unique name of the video."
},
- "config_path": {
+ "video_url": {
+ "type": "string",
+ "title": "Video Url",
+ "description": "The URL of the video file (MP4)."
+ },
+ "thumbnail_url": {
+ "type": "string",
+ "title": "Thumbnail Url",
+ "description": "The URL of the video's first-frame thumbnail (WebP)."
+ }
+ },
+ "type": "object",
+ "required": ["video_name", "video_url", "thumbnail_url"],
+ "title": "VideoUrlsDTO",
+ "description": "The URLs for a video and its thumbnail."
+ },
+ "VirtualSubBoardDTO": {
+ "properties": {
+ "virtual_board_id": {
+ "type": "string",
+ "title": "Virtual Board Id",
+ "description": "The virtual board ID, e.g. 'by_date:2026-03-18'."
+ },
+ "board_name": {
+ "type": "string",
+ "title": "Board Name",
+ "description": "The display name of the virtual sub-board, e.g. '2026-03-18'."
+ },
+ "date": {
+ "type": "string",
+ "title": "Date",
+ "description": "The ISO date string, e.g. '2026-03-18'."
+ },
+ "image_count": {
+ "type": "integer",
+ "title": "Image Count",
+ "description": "The number of general images for this date."
+ },
+ "asset_count": {
+ "type": "integer",
+ "title": "Asset Count",
+ "description": "The number of asset images for this date."
+ },
+ "video_count": {
+ "type": "integer",
+ "title": "Video Count",
+ "description": "The number of videos for this date.",
+ "default": 0
+ },
+ "cover_image_name": {
"anyOf": [
{
"type": "string"
@@ -72107,945 +77923,1360 @@
"type": "null"
}
],
- "title": "Config Path",
- "description": "Path to the config for this model, if any."
- },
- "type": {
- "type": "string",
- "const": "vae",
- "title": "Type",
- "default": "vae"
- },
- "format": {
- "type": "string",
- "const": "checkpoint",
- "title": "Format",
- "default": "checkpoint"
+ "title": "Cover Image Name",
+ "description": "The most recent image name for this date."
},
- "cpu_only": {
+ "cover_video_name": {
"anyOf": [
{
- "type": "boolean"
+ "type": "string"
},
{
"type": "null"
}
],
- "title": "Cpu Only",
- "description": "Whether this model should run on CPU only"
- },
- "base": {
- "type": "string",
- "const": "flux",
- "title": "Base",
- "default": "flux"
+ "title": "Cover Video Name",
+ "description": "The most recent video name for this date. Set instead of cover_image_name when the newest item for the date is a video."
}
},
"type": "object",
- "required": [
- "key",
- "hash",
- "path",
- "file_size",
- "name",
- "description",
- "source",
- "source_type",
- "source_api_response",
- "source_url",
- "cover_image",
- "config_path",
- "type",
- "format",
- "cpu_only",
- "base"
- ],
- "title": "VAE_Checkpoint_FLUX_Config"
+ "required": ["virtual_board_id", "board_name", "date", "image_count", "asset_count"],
+ "title": "VirtualSubBoardDTO",
+ "description": "A virtual sub-board computed from image/video metadata, not stored in the database."
},
- "VAE_Checkpoint_Flux2_Config": {
+ "WanConditioningField": {
+ "description": "A Wan 2.2 conditioning tensor primitive value.\n\nWan conditioning is the UMT5-XXL hidden state for the prompt plus an attention\nmask marking valid (non-padding) tokens.",
"properties": {
- "key": {
- "type": "string",
- "title": "Key",
- "description": "A unique key for this model."
- },
- "hash": {
- "type": "string",
- "title": "Hash",
- "description": "The hash of the model file(s)."
+ "conditioning_name": {
+ "description": "The name of conditioning tensor",
+ "title": "Conditioning Name",
+ "type": "string"
+ }
+ },
+ "required": ["conditioning_name"],
+ "title": "WanConditioningField",
+ "type": "object"
+ },
+ "WanConditioningOutput": {
+ "class": "output",
+ "description": "Base class for nodes that output a Wan 2.2 text conditioning tensor.",
+ "properties": {
+ "conditioning": {
+ "$ref": "#/components/schemas/WanConditioningField",
+ "description": "Conditioning tensor",
+ "field_kind": "output",
+ "ui_hidden": false
},
- "path": {
- "type": "string",
- "title": "Path",
- "description": "Path to the model on the filesystem. Relative paths are relative to the Invoke root directory."
+ "type": {
+ "const": "wan_conditioning_output",
+ "default": "wan_conditioning_output",
+ "field_kind": "node_attribute",
+ "title": "type",
+ "type": "string"
+ }
+ },
+ "required": ["output_meta", "conditioning", "type", "type"],
+ "title": "WanConditioningOutput",
+ "type": "object"
+ },
+ "WanDenoiseInvocation": {
+ "category": "image",
+ "class": "invocation",
+ "classification": "prototype",
+ "description": "Run the denoising process with a Wan 2.2 model.\n\nDrives the flow-matching schedule the model ships with (UniPC with flow\nsigmas for the Wan 2.2 reference repos; see ``_build_scheduler``). CFG is\nsupported when negative conditioning is provided and ``guidance_scale != 1.0``.\n\nFor Wan 2.2 A14B the high-noise expert handles timesteps at and above\n``boundary_ratio * num_train_timesteps``; the low-noise expert handles\ntimesteps below. Both experts share the model cache; only the active one is\nGPU-resident at any time.",
+ "node_pack": "invokeai",
+ "properties": {
+ "id": {
+ "description": "The id of this instance of an invocation. Must be unique among all instances of invocations.",
+ "field_kind": "node_attribute",
+ "title": "Id",
+ "type": "string"
},
- "file_size": {
- "type": "integer",
- "title": "File Size",
- "description": "The size of the model in bytes."
+ "is_intermediate": {
+ "default": false,
+ "description": "Whether or not this is an intermediate invocation.",
+ "field_kind": "node_attribute",
+ "input": "direct",
+ "orig_required": true,
+ "title": "Is Intermediate",
+ "type": "boolean",
+ "ui_hidden": false,
+ "ui_type": "IsIntermediate"
},
- "name": {
- "type": "string",
- "title": "Name",
- "description": "Name of the model."
+ "use_cache": {
+ "default": true,
+ "description": "Whether or not to use the cache",
+ "field_kind": "node_attribute",
+ "title": "Use Cache",
+ "type": "boolean"
},
- "description": {
+ "transformer": {
"anyOf": [
{
- "type": "string"
+ "$ref": "#/components/schemas/WanTransformerField"
},
{
"type": "null"
}
],
- "title": "Description",
- "description": "Model description"
- },
- "source": {
- "type": "string",
- "title": "Source",
- "description": "The original source of the model (path, URL or repo_id)."
+ "default": null,
+ "description": "Wan transformer field (transformer + optional dual-expert metadata).",
+ "field_kind": "input",
+ "input": "connection",
+ "orig_required": true,
+ "title": "Transformer"
},
- "source_type": {
- "$ref": "#/components/schemas/ModelSourceType",
- "description": "The type of source"
+ "positive_conditioning": {
+ "anyOf": [
+ {
+ "$ref": "#/components/schemas/WanConditioningField"
+ },
+ {
+ "type": "null"
+ }
+ ],
+ "default": null,
+ "description": "Positive conditioning tensor",
+ "field_kind": "input",
+ "input": "connection",
+ "orig_required": true
},
- "source_api_response": {
+ "negative_conditioning": {
"anyOf": [
{
- "type": "string"
+ "$ref": "#/components/schemas/WanConditioningField"
},
{
"type": "null"
}
],
- "title": "Source Api Response",
- "description": "The original API response from the source, as stringified JSON."
+ "default": null,
+ "description": "Negative conditioning tensor",
+ "field_kind": "input",
+ "input": "connection",
+ "orig_default": null,
+ "orig_required": false
},
- "source_url": {
+ "ref_image": {
"anyOf": [
{
- "type": "string"
+ "$ref": "#/components/schemas/WanRefImageConditioningField"
},
{
"type": "null"
}
],
- "title": "Source Url",
- "description": "Optional URL for the model (e.g. download page or model page)."
+ "default": null,
+ "description": "Reference-image (VAE-latent) conditioning for Wan 2.2 I2V.",
+ "field_kind": "input",
+ "input": "connection",
+ "orig_default": null,
+ "orig_required": false,
+ "title": "Reference Image"
},
- "cover_image": {
+ "latents": {
"anyOf": [
{
- "type": "string"
+ "$ref": "#/components/schemas/LatentsField"
},
{
"type": "null"
}
],
- "title": "Cover Image",
- "description": "Url for image to preview model"
+ "default": null,
+ "description": "Latents tensor",
+ "field_kind": "input",
+ "input": "connection",
+ "orig_default": null,
+ "orig_required": false
},
- "config_path": {
+ "denoise_mask": {
"anyOf": [
{
- "type": "string"
+ "$ref": "#/components/schemas/DenoiseMaskField"
},
{
"type": "null"
}
],
- "title": "Config Path",
- "description": "Path to the config for this model, if any."
+ "default": null,
+ "description": "A mask of the region to apply the denoising process to. Values of 0.0 represent the regions to be fully denoised, and 1.0 represent the regions to be preserved.",
+ "field_kind": "input",
+ "input": "connection",
+ "orig_default": null,
+ "orig_required": false
},
- "type": {
- "type": "string",
- "const": "vae",
- "title": "Type",
- "default": "vae"
+ "denoising_start": {
+ "default": 0.0,
+ "description": "When to start denoising, expressed a percentage of total steps",
+ "field_kind": "input",
+ "input": "any",
+ "maximum": 1,
+ "minimum": 0,
+ "orig_default": 0.0,
+ "orig_required": false,
+ "title": "Denoising Start",
+ "type": "number"
},
- "format": {
- "type": "string",
- "const": "checkpoint",
- "title": "Format",
- "default": "checkpoint"
+ "denoising_end": {
+ "default": 1.0,
+ "description": "When to stop denoising, expressed a percentage of total steps",
+ "field_kind": "input",
+ "input": "any",
+ "maximum": 1,
+ "minimum": 0,
+ "orig_default": 1.0,
+ "orig_required": false,
+ "title": "Denoising End",
+ "type": "number"
},
- "base": {
- "type": "string",
- "const": "flux2",
- "title": "Base",
- "default": "flux2"
+ "add_noise": {
+ "default": true,
+ "description": "Add noise based on denoising start.",
+ "field_kind": "input",
+ "input": "any",
+ "orig_default": true,
+ "orig_required": false,
+ "title": "Add Noise",
+ "type": "boolean"
},
- "cpu_only": {
+ "guidance_scale": {
+ "default": 4.0,
+ "description": "Classifier-free guidance scale. 4.0 is the Wan 2.2 default for A14B; TI2V-5B can tolerate higher values up to ~5.5.",
+ "field_kind": "input",
+ "input": "any",
+ "minimum": 1.0,
+ "orig_default": 4.0,
+ "orig_required": false,
+ "title": "Guidance Scale",
+ "type": "number"
+ },
+ "guidance_scale_low_noise": {
"anyOf": [
{
- "type": "boolean"
+ "minimum": 0.0,
+ "type": "number"
},
{
"type": "null"
}
],
- "title": "Cpu Only",
- "description": "Whether this model should run on CPU only"
+ "default": null,
+ "description": "Optional separate CFG scale for the low-noise expert (Wan 2.2 A14B only). Values below 1.0 (including 0) fall back to the primary 'Guidance Scale'. Ignored for TI2V-5B.",
+ "field_kind": "input",
+ "input": "any",
+ "orig_default": null,
+ "orig_required": false,
+ "title": "Guidance Scale (Low Noise)"
+ },
+ "width": {
+ "default": 1024,
+ "description": "Width of the generated image.",
+ "field_kind": "input",
+ "input": "any",
+ "multipleOf": 16,
+ "orig_default": 1024,
+ "orig_required": false,
+ "title": "Width",
+ "type": "integer"
+ },
+ "height": {
+ "default": 1024,
+ "description": "Height of the generated image.",
+ "field_kind": "input",
+ "input": "any",
+ "multipleOf": 16,
+ "orig_default": 1024,
+ "orig_required": false,
+ "title": "Height",
+ "type": "integer"
+ },
+ "steps": {
+ "default": 40,
+ "description": "Number of denoising steps.",
+ "exclusiveMinimum": 0,
+ "field_kind": "input",
+ "input": "any",
+ "orig_default": 40,
+ "orig_required": false,
+ "title": "Steps",
+ "type": "integer"
+ },
+ "seed": {
+ "default": 0,
+ "description": "Randomness seed for reproducibility.",
+ "field_kind": "input",
+ "input": "any",
+ "orig_default": 0,
+ "orig_required": false,
+ "title": "Seed",
+ "type": "integer"
+ },
+ "type": {
+ "const": "wan_denoise",
+ "default": "wan_denoise",
+ "field_kind": "node_attribute",
+ "title": "type",
+ "type": "string"
}
},
+ "required": ["type", "id"],
+ "tags": ["image", "wan"],
+ "title": "Denoise - Wan 2.2",
"type": "object",
- "required": [
- "key",
- "hash",
- "path",
- "file_size",
- "name",
- "description",
- "source",
- "source_type",
- "source_api_response",
- "source_url",
- "cover_image",
- "config_path",
- "type",
- "format",
- "base",
- "cpu_only"
- ],
- "title": "VAE_Checkpoint_Flux2_Config",
- "description": "Model config for FLUX.2 VAE checkpoint models (AutoencoderKLFlux2)."
+ "version": "1.0.0",
+ "output": {
+ "$ref": "#/components/schemas/LatentsOutput"
+ }
},
- "VAE_Checkpoint_QwenImage_Config": {
+ "WanI2VIdealDimensionsInvocation": {
+ "category": "video",
+ "class": "invocation",
+ "classification": "prototype",
+ "description": "Ideal dimensions for the Wan 2.2 A14B models (I2V-A14B and T2V-A14B).\n\nUse this node for the A14B family. For the TI2V-5B model use \"Wan 2.2 TI2V\nIdeal Dimensions\" instead \u2014 TI2V-5B requires multiples of 32, and feeding it\nthese multiples-of-16 dims fails the patchify step.\n\nScales the input W\u00d7H so the shorter side equals the chosen preset (480 / 720 /\n1080 px), then snaps each dimension to a multiple of 16 (the A14B pixel-grid\nconstraint). Wire from ``Image Primitive``'s width/height outputs and into\n``wan_ref_image_encoder`` / ``wan_denoise``.",
+ "node_pack": "invokeai",
"properties": {
- "key": {
- "type": "string",
- "title": "Key",
- "description": "A unique key for this model."
+ "id": {
+ "description": "The id of this instance of an invocation. Must be unique among all instances of invocations.",
+ "field_kind": "node_attribute",
+ "title": "Id",
+ "type": "string"
},
- "hash": {
- "type": "string",
- "title": "Hash",
- "description": "The hash of the model file(s)."
+ "is_intermediate": {
+ "default": false,
+ "description": "Whether or not this is an intermediate invocation.",
+ "field_kind": "node_attribute",
+ "input": "direct",
+ "orig_required": true,
+ "title": "Is Intermediate",
+ "type": "boolean",
+ "ui_hidden": false,
+ "ui_type": "IsIntermediate"
},
- "path": {
- "type": "string",
- "title": "Path",
- "description": "Path to the model on the filesystem. Relative paths are relative to the Invoke root directory."
+ "use_cache": {
+ "default": true,
+ "description": "Whether or not to use the cache",
+ "field_kind": "node_attribute",
+ "title": "Use Cache",
+ "type": "boolean"
},
- "file_size": {
- "type": "integer",
- "title": "File Size",
- "description": "The size of the model in bytes."
+ "width": {
+ "default": 1024,
+ "description": "Source image width in pixels.",
+ "exclusiveMinimum": 0,
+ "field_kind": "input",
+ "input": "any",
+ "orig_default": 1024,
+ "orig_required": false,
+ "title": "Width",
+ "type": "integer"
},
- "name": {
+ "height": {
+ "default": 1024,
+ "description": "Source image height in pixels.",
+ "exclusiveMinimum": 0,
+ "field_kind": "input",
+ "input": "any",
+ "orig_default": 1024,
+ "orig_required": false,
+ "title": "Height",
+ "type": "integer"
+ },
+ "target_resolution": {
+ "default": "720p",
+ "description": "Short-side resolution preset. 480p and 720p are Wan 2.2's native training resolutions; 1080p works but is extrapolation and costs ~2.25x the memory of 720p.",
+ "enum": ["480p", "720p", "1080p"],
+ "field_kind": "input",
+ "input": "any",
+ "orig_default": "720p",
+ "orig_required": false,
+ "title": "Target Resolution",
"type": "string",
- "title": "Name",
- "description": "Name of the model."
+ "ui_choice_labels": {
+ "1080p": "1080p (extrapolated \u2014 not a Wan training size)",
+ "480p": "480p (Wan native)",
+ "720p": "720p (Wan native, default)"
+ }
+ },
+ "rounding": {
+ "default": "nearest",
+ "description": "How to snap each dimension to a multiple of 16. 'floor' rounds down \u2014 safest for VRAM, guaranteed not to exceed the unsnapped target. 'ceiling' rounds up. 'nearest' minimizes aspect-ratio drift (default).",
+ "enum": ["nearest", "floor", "ceiling"],
+ "field_kind": "input",
+ "input": "any",
+ "orig_default": "nearest",
+ "orig_required": false,
+ "title": "Rounding",
+ "type": "string"
},
- "description": {
+ "type": {
+ "const": "wan_i2v_ideal_dimensions",
+ "default": "wan_i2v_ideal_dimensions",
+ "field_kind": "node_attribute",
+ "title": "type",
+ "type": "string"
+ }
+ },
+ "required": ["type", "id"],
+ "tags": ["wan", "video", "dimensions", "math"],
+ "title": "Wan 2.2 I2V Ideal Dimensions (A14B)",
+ "type": "object",
+ "version": "1.1.0",
+ "output": {
+ "$ref": "#/components/schemas/IdealSizeOutput"
+ }
+ },
+ "WanImageToLatentsInvocation": {
+ "category": "image",
+ "class": "invocation",
+ "classification": "prototype",
+ "description": "Encodes an image with the Wan VAE (AutoencoderKLWan).\n\nThe output latents have the temporal dimension squeezed out, so downstream\nnodes see 4D ``[B, C, H, W]``. The denoise loop re-adds ``T=1`` before\nfeeding the transformer.",
+ "node_pack": "invokeai",
+ "properties": {
+ "board": {
"anyOf": [
{
- "type": "string"
+ "$ref": "#/components/schemas/BoardField"
},
{
"type": "null"
}
],
- "title": "Description",
- "description": "Model description"
- },
- "source": {
- "type": "string",
- "title": "Source",
- "description": "The original source of the model (path, URL or repo_id)."
- },
- "source_type": {
- "$ref": "#/components/schemas/ModelSourceType",
- "description": "The type of source"
+ "default": null,
+ "description": "The board to save the image to",
+ "field_kind": "internal",
+ "input": "direct",
+ "orig_required": false,
+ "ui_hidden": false
},
- "source_api_response": {
+ "metadata": {
"anyOf": [
{
- "type": "string"
+ "$ref": "#/components/schemas/MetadataField"
},
{
"type": "null"
}
],
- "title": "Source Api Response",
- "description": "The original API response from the source, as stringified JSON."
+ "default": null,
+ "description": "Optional metadata to be saved with the image",
+ "field_kind": "internal",
+ "input": "connection",
+ "orig_required": false,
+ "ui_hidden": false
},
- "source_url": {
+ "id": {
+ "description": "The id of this instance of an invocation. Must be unique among all instances of invocations.",
+ "field_kind": "node_attribute",
+ "title": "Id",
+ "type": "string"
+ },
+ "is_intermediate": {
+ "default": false,
+ "description": "Whether or not this is an intermediate invocation.",
+ "field_kind": "node_attribute",
+ "input": "direct",
+ "orig_required": true,
+ "title": "Is Intermediate",
+ "type": "boolean",
+ "ui_hidden": false,
+ "ui_type": "IsIntermediate"
+ },
+ "use_cache": {
+ "default": true,
+ "description": "Whether or not to use the cache",
+ "field_kind": "node_attribute",
+ "title": "Use Cache",
+ "type": "boolean"
+ },
+ "image": {
"anyOf": [
{
- "type": "string"
+ "$ref": "#/components/schemas/ImageField"
},
{
"type": "null"
}
],
- "title": "Source Url",
- "description": "Optional URL for the model (e.g. download page or model page)."
+ "default": null,
+ "description": "The image to encode.",
+ "field_kind": "input",
+ "input": "any",
+ "orig_required": true
},
- "cover_image": {
+ "vae": {
"anyOf": [
{
- "type": "string"
+ "$ref": "#/components/schemas/VAEField"
},
{
"type": "null"
}
],
- "title": "Cover Image",
- "description": "Url for image to preview model"
+ "default": null,
+ "description": "VAE",
+ "field_kind": "input",
+ "input": "connection",
+ "orig_required": true
},
- "config_path": {
+ "type": {
+ "const": "wan_i2l",
+ "default": "wan_i2l",
+ "field_kind": "node_attribute",
+ "title": "type",
+ "type": "string"
+ }
+ },
+ "required": ["type", "id"],
+ "tags": ["image", "latents", "vae", "i2l", "wan"],
+ "title": "Image to Latents - Wan 2.2",
+ "type": "object",
+ "version": "1.0.0",
+ "output": {
+ "$ref": "#/components/schemas/LatentsOutput"
+ }
+ },
+ "WanLatentsToImageInvocation": {
+ "category": "latents",
+ "class": "invocation",
+ "classification": "prototype",
+ "description": "Decodes Wan latents back to RGB.",
+ "node_pack": "invokeai",
+ "properties": {
+ "board": {
"anyOf": [
{
- "type": "string"
+ "$ref": "#/components/schemas/BoardField"
},
{
"type": "null"
}
],
- "title": "Config Path",
- "description": "Path to the config for this model, if any."
- },
- "type": {
- "type": "string",
- "const": "vae",
- "title": "Type",
- "default": "vae"
- },
- "format": {
- "type": "string",
- "const": "checkpoint",
- "title": "Format",
- "default": "checkpoint"
- },
- "base": {
- "type": "string",
- "const": "qwen-image",
- "title": "Base",
- "default": "qwen-image"
+ "default": null,
+ "description": "The board to save the image to",
+ "field_kind": "internal",
+ "input": "direct",
+ "orig_required": false,
+ "ui_hidden": false
},
- "cpu_only": {
+ "metadata": {
"anyOf": [
{
- "type": "boolean"
+ "$ref": "#/components/schemas/MetadataField"
},
{
"type": "null"
}
],
- "title": "Cpu Only",
- "description": "Whether this model should run on CPU only"
- }
- },
- "type": "object",
- "required": [
- "key",
- "hash",
- "path",
- "file_size",
- "name",
- "description",
- "source",
- "source_type",
- "source_api_response",
- "source_url",
- "cover_image",
- "config_path",
- "type",
- "format",
- "base",
- "cpu_only"
- ],
- "title": "VAE_Checkpoint_QwenImage_Config",
- "description": "Model config for Qwen Image VAE checkpoint models (AutoencoderKLQwenImage)."
- },
- "VAE_Checkpoint_SD1_Config": {
- "properties": {
- "key": {
- "type": "string",
- "title": "Key",
- "description": "A unique key for this model."
- },
- "hash": {
- "type": "string",
- "title": "Hash",
- "description": "The hash of the model file(s)."
+ "default": null,
+ "description": "Optional metadata to be saved with the image",
+ "field_kind": "internal",
+ "input": "connection",
+ "orig_required": false,
+ "ui_hidden": false
},
- "path": {
- "type": "string",
- "title": "Path",
- "description": "Path to the model on the filesystem. Relative paths are relative to the Invoke root directory."
+ "id": {
+ "description": "The id of this instance of an invocation. Must be unique among all instances of invocations.",
+ "field_kind": "node_attribute",
+ "title": "Id",
+ "type": "string"
},
- "file_size": {
- "type": "integer",
- "title": "File Size",
- "description": "The size of the model in bytes."
+ "is_intermediate": {
+ "default": false,
+ "description": "Whether or not this is an intermediate invocation.",
+ "field_kind": "node_attribute",
+ "input": "direct",
+ "orig_required": true,
+ "title": "Is Intermediate",
+ "type": "boolean",
+ "ui_hidden": false,
+ "ui_type": "IsIntermediate"
},
- "name": {
- "type": "string",
- "title": "Name",
- "description": "Name of the model."
+ "use_cache": {
+ "default": true,
+ "description": "Whether or not to use the cache",
+ "field_kind": "node_attribute",
+ "title": "Use Cache",
+ "type": "boolean"
},
- "description": {
+ "latents": {
"anyOf": [
{
- "type": "string"
+ "$ref": "#/components/schemas/LatentsField"
},
{
"type": "null"
}
],
- "title": "Description",
- "description": "Model description"
- },
- "source": {
- "type": "string",
- "title": "Source",
- "description": "The original source of the model (path, URL or repo_id)."
- },
- "source_type": {
- "$ref": "#/components/schemas/ModelSourceType",
- "description": "The type of source"
+ "default": null,
+ "description": "Latents tensor",
+ "field_kind": "input",
+ "input": "connection",
+ "orig_required": true
},
- "source_api_response": {
+ "vae": {
"anyOf": [
{
- "type": "string"
+ "$ref": "#/components/schemas/VAEField"
},
{
"type": "null"
}
],
- "title": "Source Api Response",
- "description": "The original API response from the source, as stringified JSON."
+ "default": null,
+ "description": "VAE",
+ "field_kind": "input",
+ "input": "connection",
+ "orig_required": true
},
- "source_url": {
+ "type": {
+ "const": "wan_l2i",
+ "default": "wan_l2i",
+ "field_kind": "node_attribute",
+ "title": "type",
+ "type": "string"
+ }
+ },
+ "required": ["type", "id"],
+ "tags": ["latents", "image", "vae", "l2i", "wan"],
+ "title": "Latents to Image - Wan 2.2",
+ "type": "object",
+ "version": "1.0.0",
+ "output": {
+ "$ref": "#/components/schemas/ImageOutput"
+ }
+ },
+ "WanLatentsToVideoInvocation": {
+ "category": "latents",
+ "class": "invocation",
+ "classification": "prototype",
+ "description": "Decode 5D Wan latents to RGB frames and encode an MP4.",
+ "node_pack": "invokeai",
+ "properties": {
+ "board": {
"anyOf": [
{
- "type": "string"
+ "$ref": "#/components/schemas/BoardField"
},
{
"type": "null"
}
],
- "title": "Source Url",
- "description": "Optional URL for the model (e.g. download page or model page)."
+ "default": null,
+ "description": "The board to save the image to",
+ "field_kind": "internal",
+ "input": "direct",
+ "orig_required": false,
+ "ui_hidden": false
},
- "cover_image": {
+ "metadata": {
"anyOf": [
{
- "type": "string"
+ "$ref": "#/components/schemas/MetadataField"
},
{
"type": "null"
}
],
- "title": "Cover Image",
- "description": "Url for image to preview model"
+ "default": null,
+ "description": "Optional metadata to be saved with the image",
+ "field_kind": "internal",
+ "input": "connection",
+ "orig_required": false,
+ "ui_hidden": false
},
- "config_path": {
+ "id": {
+ "description": "The id of this instance of an invocation. Must be unique among all instances of invocations.",
+ "field_kind": "node_attribute",
+ "title": "Id",
+ "type": "string"
+ },
+ "is_intermediate": {
+ "default": false,
+ "description": "Whether or not this is an intermediate invocation.",
+ "field_kind": "node_attribute",
+ "input": "direct",
+ "orig_required": true,
+ "title": "Is Intermediate",
+ "type": "boolean",
+ "ui_hidden": false,
+ "ui_type": "IsIntermediate"
+ },
+ "use_cache": {
+ "default": true,
+ "description": "Whether or not to use the cache",
+ "field_kind": "node_attribute",
+ "title": "Use Cache",
+ "type": "boolean"
+ },
+ "latents": {
"anyOf": [
{
- "type": "string"
+ "$ref": "#/components/schemas/LatentsField"
},
{
"type": "null"
}
],
- "title": "Config Path",
- "description": "Path to the config for this model, if any."
- },
- "type": {
- "type": "string",
- "const": "vae",
- "title": "Type",
- "default": "vae"
- },
- "format": {
- "type": "string",
- "const": "checkpoint",
- "title": "Format",
- "default": "checkpoint"
+ "default": null,
+ "description": "Latents tensor",
+ "field_kind": "input",
+ "input": "connection",
+ "orig_required": true
},
- "cpu_only": {
+ "vae": {
"anyOf": [
{
- "type": "boolean"
+ "$ref": "#/components/schemas/VAEField"
},
{
"type": "null"
}
],
- "title": "Cpu Only",
- "description": "Whether this model should run on CPU only"
+ "default": null,
+ "description": "VAE",
+ "field_kind": "input",
+ "input": "connection",
+ "orig_required": true
},
- "base": {
- "type": "string",
- "const": "sd-1",
- "title": "Base",
- "default": "sd-1"
+ "fps": {
+ "default": 16,
+ "description": "Frames-per-second for the encoded MP4. Wan 2.2 was trained at 16 FPS.",
+ "field_kind": "input",
+ "input": "any",
+ "maximum": 120,
+ "minimum": 1,
+ "orig_default": 16,
+ "orig_required": false,
+ "title": "Fps",
+ "type": "integer"
+ },
+ "type": {
+ "const": "wan_l2v",
+ "default": "wan_l2v",
+ "field_kind": "node_attribute",
+ "title": "type",
+ "type": "string"
}
},
+ "required": ["type", "id"],
+ "tags": ["latents", "video", "vae", "l2v", "wan"],
+ "title": "Latents to Video - Wan 2.2",
"type": "object",
- "required": [
- "key",
- "hash",
- "path",
- "file_size",
- "name",
- "description",
- "source",
- "source_type",
- "source_api_response",
- "source_url",
- "cover_image",
- "config_path",
- "type",
- "format",
- "cpu_only",
- "base"
- ],
- "title": "VAE_Checkpoint_SD1_Config"
+ "version": "1.0.0",
+ "output": {
+ "$ref": "#/components/schemas/VideoOutput"
+ }
},
- "VAE_Checkpoint_SD2_Config": {
+ "WanLoRACollectionLoader": {
+ "category": "model",
+ "class": "invocation",
+ "classification": "prototype",
+ "description": "Apply a collection of LoRAs to the Wan 2.2 transformer(s).\n\nEach LoRA is routed to the primary and/or low-noise list based on its\nrecorded ``expert`` tag (set by the probe from the filename). Untagged\nLoRAs go to both lists.",
+ "node_pack": "invokeai",
"properties": {
- "key": {
- "type": "string",
- "title": "Key",
- "description": "A unique key for this model."
- },
- "hash": {
- "type": "string",
- "title": "Hash",
- "description": "The hash of the model file(s)."
+ "id": {
+ "description": "The id of this instance of an invocation. Must be unique among all instances of invocations.",
+ "field_kind": "node_attribute",
+ "title": "Id",
+ "type": "string"
},
- "path": {
- "type": "string",
- "title": "Path",
- "description": "Path to the model on the filesystem. Relative paths are relative to the Invoke root directory."
+ "is_intermediate": {
+ "default": false,
+ "description": "Whether or not this is an intermediate invocation.",
+ "field_kind": "node_attribute",
+ "input": "direct",
+ "orig_required": true,
+ "title": "Is Intermediate",
+ "type": "boolean",
+ "ui_hidden": false,
+ "ui_type": "IsIntermediate"
},
- "file_size": {
- "type": "integer",
- "title": "File Size",
- "description": "The size of the model in bytes."
+ "use_cache": {
+ "default": true,
+ "description": "Whether or not to use the cache",
+ "field_kind": "node_attribute",
+ "title": "Use Cache",
+ "type": "boolean"
},
- "name": {
- "type": "string",
- "title": "Name",
- "description": "Name of the model."
+ "loras": {
+ "anyOf": [
+ {
+ "$ref": "#/components/schemas/LoRAField"
+ },
+ {
+ "items": {
+ "$ref": "#/components/schemas/LoRAField"
+ },
+ "type": "array"
+ },
+ {
+ "type": "null"
+ }
+ ],
+ "default": null,
+ "description": "LoRAs to apply. May be a single LoRA or a collection.",
+ "field_kind": "input",
+ "input": "any",
+ "orig_default": null,
+ "orig_required": false,
+ "title": "LoRAs",
+ "ui_model_base": ["wan"],
+ "ui_model_type": ["lora"]
},
- "description": {
+ "transformer": {
"anyOf": [
{
- "type": "string"
+ "$ref": "#/components/schemas/WanTransformerField"
},
{
"type": "null"
}
],
- "title": "Description",
- "description": "Model description"
+ "default": null,
+ "description": "Transformer",
+ "field_kind": "input",
+ "input": "connection",
+ "orig_default": null,
+ "orig_required": false,
+ "title": "Wan Transformer"
},
- "source": {
- "type": "string",
- "title": "Source",
- "description": "The original source of the model (path, URL or repo_id)."
+ "type": {
+ "const": "wan_lora_collection_loader",
+ "default": "wan_lora_collection_loader",
+ "field_kind": "node_attribute",
+ "title": "type",
+ "type": "string"
+ }
+ },
+ "required": ["type", "id"],
+ "tags": ["lora", "model", "wan"],
+ "title": "Apply LoRA Collection - Wan 2.2",
+ "type": "object",
+ "version": "1.0.1",
+ "output": {
+ "$ref": "#/components/schemas/WanLoRALoaderOutput"
+ }
+ },
+ "WanLoRALoaderInvocation": {
+ "category": "model",
+ "class": "invocation",
+ "classification": "prototype",
+ "description": "Apply a LoRA to the Wan 2.2 transformer(s).\n\nFor A14B (dual expert) the LoRA's recorded ``expert`` field determines\nwhich expert list it lands in: ``\"high\"`` -> primary list, ``\"low\"`` ->\nlow-noise list, ``None`` (untagged) -> both lists. Use the ``target``\nfield to override.\n\nFor TI2V-5B (single transformer) only the primary list is used at denoise\ntime; a LoRA routed only to the low-noise list would be inert, so that\nrouting logs a warning.",
+ "node_pack": "invokeai",
+ "properties": {
+ "id": {
+ "description": "The id of this instance of an invocation. Must be unique among all instances of invocations.",
+ "field_kind": "node_attribute",
+ "title": "Id",
+ "type": "string"
+ },
+ "is_intermediate": {
+ "default": false,
+ "description": "Whether or not this is an intermediate invocation.",
+ "field_kind": "node_attribute",
+ "input": "direct",
+ "orig_required": true,
+ "title": "Is Intermediate",
+ "type": "boolean",
+ "ui_hidden": false,
+ "ui_type": "IsIntermediate"
},
- "source_type": {
- "$ref": "#/components/schemas/ModelSourceType",
- "description": "The type of source"
+ "use_cache": {
+ "default": true,
+ "description": "Whether or not to use the cache",
+ "field_kind": "node_attribute",
+ "title": "Use Cache",
+ "type": "boolean"
},
- "source_api_response": {
+ "lora": {
"anyOf": [
{
- "type": "string"
+ "$ref": "#/components/schemas/ModelIdentifierField"
},
{
"type": "null"
}
],
- "title": "Source Api Response",
- "description": "The original API response from the source, as stringified JSON."
+ "default": null,
+ "description": "LoRA model to load",
+ "field_kind": "input",
+ "input": "any",
+ "orig_required": true,
+ "title": "LoRA",
+ "ui_model_base": ["wan"],
+ "ui_model_type": ["lora"]
},
- "source_url": {
- "anyOf": [
- {
- "type": "string"
- },
- {
- "type": "null"
- }
- ],
- "title": "Source Url",
- "description": "Optional URL for the model (e.g. download page or model page)."
+ "weight": {
+ "default": 0.75,
+ "description": "The weight at which the LoRA is applied to each model",
+ "field_kind": "input",
+ "input": "any",
+ "orig_default": 0.75,
+ "orig_required": false,
+ "title": "Weight",
+ "type": "number"
},
- "cover_image": {
- "anyOf": [
- {
- "type": "string"
- },
- {
- "type": "null"
- }
- ],
- "title": "Cover Image",
- "description": "Url for image to preview model"
+ "target": {
+ "default": "auto",
+ "description": "Which expert(s) to apply this LoRA to. 'auto' uses the LoRA's recorded expert tag (or both if untagged); 'both'/'high'/'low' override it.",
+ "enum": ["auto", "both", "high", "low"],
+ "field_kind": "input",
+ "input": "any",
+ "orig_default": "auto",
+ "orig_required": false,
+ "title": "Target",
+ "type": "string"
},
- "config_path": {
+ "transformer": {
"anyOf": [
{
- "type": "string"
+ "$ref": "#/components/schemas/WanTransformerField"
},
{
"type": "null"
}
],
- "title": "Config Path",
- "description": "Path to the config for this model, if any."
+ "default": null,
+ "description": "Transformer",
+ "field_kind": "input",
+ "input": "connection",
+ "orig_default": null,
+ "orig_required": false,
+ "title": "Wan Transformer"
},
"type": {
- "type": "string",
- "const": "vae",
- "title": "Type",
- "default": "vae"
- },
- "format": {
- "type": "string",
- "const": "checkpoint",
- "title": "Format",
- "default": "checkpoint"
- },
- "cpu_only": {
+ "const": "wan_lora_loader",
+ "default": "wan_lora_loader",
+ "field_kind": "node_attribute",
+ "title": "type",
+ "type": "string"
+ }
+ },
+ "required": ["type", "id"],
+ "tags": ["lora", "model", "wan"],
+ "title": "Apply LoRA - Wan 2.2",
+ "type": "object",
+ "version": "1.0.1",
+ "output": {
+ "$ref": "#/components/schemas/WanLoRALoaderOutput"
+ }
+ },
+ "WanLoRALoaderOutput": {
+ "class": "output",
+ "description": "Wan 2.2 LoRA loader output.",
+ "properties": {
+ "transformer": {
"anyOf": [
{
- "type": "boolean"
+ "$ref": "#/components/schemas/WanTransformerField"
},
{
"type": "null"
}
],
- "title": "Cpu Only",
- "description": "Whether this model should run on CPU only"
+ "default": null,
+ "description": "Transformer",
+ "field_kind": "output",
+ "title": "Wan Transformer",
+ "ui_hidden": false
},
- "base": {
- "type": "string",
- "const": "sd-2",
- "title": "Base",
- "default": "sd-2"
+ "type": {
+ "const": "wan_lora_loader_output",
+ "default": "wan_lora_loader_output",
+ "field_kind": "node_attribute",
+ "title": "type",
+ "type": "string"
}
},
- "type": "object",
- "required": [
- "key",
- "hash",
- "path",
- "file_size",
- "name",
- "description",
- "source",
- "source_type",
- "source_api_response",
- "source_url",
- "cover_image",
- "config_path",
- "type",
- "format",
- "cpu_only",
- "base"
- ],
- "title": "VAE_Checkpoint_SD2_Config"
+ "required": ["output_meta", "transformer", "type", "type"],
+ "title": "WanLoRALoaderOutput",
+ "type": "object"
},
- "VAE_Checkpoint_SDXL_Config": {
+ "WanLoRAVariantType": {
+ "type": "string",
+ "enum": ["a14b", "5b"],
+ "title": "WanLoRAVariantType",
+ "description": "Wan 2.2 LoRA variants, identifying which model family a LoRA targets.\n\nDetected from the LoRA's inner attention dim: A14B has ``inner_dim=5120``,\nTI2V-5B has ``inner_dim=3072``. A14B and 5B LoRAs are NOT interchangeable \u2014\napplying one against the wrong main model crashes in the layer patcher\nwith a tensor-shape error."
+ },
+ "WanModelLoaderInvocation": {
+ "category": "model",
+ "class": "invocation",
+ "classification": "prototype",
+ "description": "Loads a Wan 2.2 model, outputting its submodels.\n\nComponents can be mixed and matched, mirroring the Qwen Image loader pattern:\n\n- Transformer(s):\n * Diffusers main: emits ``transformer/`` and (for A14B) ``transformer_2/``\n from the same model record.\n * GGUF main: emits the single GGUF as the primary transformer; for A14B\n the second-expert GGUF must be wired to ``Transformer (Low Noise)``.\n- VAE: standalone Wan VAE > main (if Diffusers) > Component Source (Diffusers).\n- UMT5-XXL encoder: standalone Wan T5 encoder > main (if Diffusers) >\n Component Source (Diffusers).\n\nThe Component Source slot lets users supply a Diffusers Wan main model purely\nfor VAE / encoder extraction when the actual transformer is in a single-file\nformat. Together, the standalone VAE + standalone encoder let a GGUF\ntransformer run without a full ~30 GB Diffusers install.",
+ "node_pack": "invokeai",
"properties": {
- "key": {
- "type": "string",
- "title": "Key",
- "description": "A unique key for this model."
- },
- "hash": {
- "type": "string",
- "title": "Hash",
- "description": "The hash of the model file(s)."
- },
- "path": {
- "type": "string",
- "title": "Path",
- "description": "Path to the model on the filesystem. Relative paths are relative to the Invoke root directory."
- },
- "file_size": {
- "type": "integer",
- "title": "File Size",
- "description": "The size of the model in bytes."
- },
- "name": {
- "type": "string",
- "title": "Name",
- "description": "Name of the model."
+ "id": {
+ "description": "The id of this instance of an invocation. Must be unique among all instances of invocations.",
+ "field_kind": "node_attribute",
+ "title": "Id",
+ "type": "string"
},
- "description": {
- "anyOf": [
- {
- "type": "string"
- },
- {
- "type": "null"
- }
- ],
- "title": "Description",
- "description": "Model description"
+ "is_intermediate": {
+ "default": false,
+ "description": "Whether or not this is an intermediate invocation.",
+ "field_kind": "node_attribute",
+ "input": "direct",
+ "orig_required": true,
+ "title": "Is Intermediate",
+ "type": "boolean",
+ "ui_hidden": false,
+ "ui_type": "IsIntermediate"
},
- "source": {
- "type": "string",
- "title": "Source",
- "description": "The original source of the model (path, URL or repo_id)."
+ "use_cache": {
+ "default": true,
+ "description": "Whether or not to use the cache",
+ "field_kind": "node_attribute",
+ "title": "Use Cache",
+ "type": "boolean"
},
- "source_type": {
- "$ref": "#/components/schemas/ModelSourceType",
- "description": "The type of source"
+ "model": {
+ "$ref": "#/components/schemas/ModelIdentifierField",
+ "description": "Wan 2.2 model (Transformer) to load",
+ "field_kind": "input",
+ "input": "direct",
+ "orig_required": true,
+ "title": "Transformer",
+ "ui_model_base": ["wan"],
+ "ui_model_type": ["main"]
},
- "source_api_response": {
+ "transformer_low_noise_model": {
"anyOf": [
{
- "type": "string"
+ "$ref": "#/components/schemas/ModelIdentifierField"
},
{
"type": "null"
}
],
- "title": "Source Api Response",
- "description": "The original API response from the source, as stringified JSON."
+ "default": null,
+ "description": "Optional second GGUF transformer for the A14B low-noise expert. Only relevant when the main model is a single-file GGUF and the variant is A14B; ignored when the main is a Diffusers A14B (both experts are pulled from transformer/ and transformer_2/ already) or when the variant is TI2V-5B.",
+ "field_kind": "input",
+ "input": "direct",
+ "orig_default": null,
+ "orig_required": false,
+ "title": "Transformer (Low Noise)",
+ "ui_model_base": ["wan"],
+ "ui_model_format": ["gguf_quantized"],
+ "ui_model_type": ["main"]
},
- "source_url": {
+ "vae_model": {
"anyOf": [
{
- "type": "string"
+ "$ref": "#/components/schemas/ModelIdentifierField"
},
{
"type": "null"
}
],
- "title": "Source Url",
- "description": "Optional URL for the model (e.g. download page or model page)."
+ "default": null,
+ "description": "Standalone Wan VAE model. If not set, the VAE is loaded from the main model (when in Diffusers format) or from the Component Source.",
+ "field_kind": "input",
+ "input": "direct",
+ "orig_default": null,
+ "orig_required": false,
+ "title": "VAE",
+ "ui_model_base": ["wan"],
+ "ui_model_type": ["vae"]
},
- "cover_image": {
+ "wan_t5_encoder_model": {
"anyOf": [
{
- "type": "string"
+ "$ref": "#/components/schemas/ModelIdentifierField"
},
{
"type": "null"
}
],
- "title": "Cover Image",
- "description": "Url for image to preview model"
+ "default": null,
+ "description": "Standalone Wan UMT5-XXL encoder. If not set, the encoder is loaded from the main model (when in Diffusers format) or from the Component Source.",
+ "field_kind": "input",
+ "input": "direct",
+ "orig_default": null,
+ "orig_required": false,
+ "title": "Wan T5 Encoder",
+ "ui_model_type": ["wan_t5_encoder"]
},
- "config_path": {
+ "component_source": {
"anyOf": [
{
- "type": "string"
+ "$ref": "#/components/schemas/ModelIdentifierField"
},
{
"type": "null"
}
],
- "title": "Config Path",
- "description": "Path to the config for this model, if any."
+ "default": null,
+ "description": "Diffusers Wan main model to extract VAE and/or encoder from. Use this if you don't have separate VAE/encoder models. Ignored for any submodel that is provided separately.",
+ "field_kind": "input",
+ "input": "direct",
+ "orig_default": null,
+ "orig_required": false,
+ "title": "Component Source (Diffusers)",
+ "ui_model_base": ["wan"],
+ "ui_model_format": ["diffusers"],
+ "ui_model_type": ["main"]
},
"type": {
- "type": "string",
- "const": "vae",
- "title": "Type",
- "default": "vae"
- },
- "format": {
- "type": "string",
- "const": "checkpoint",
- "title": "Format",
- "default": "checkpoint"
- },
- "cpu_only": {
- "anyOf": [
- {
- "type": "boolean"
- },
- {
- "type": "null"
- }
- ],
- "title": "Cpu Only",
- "description": "Whether this model should run on CPU only"
- },
- "base": {
- "type": "string",
- "const": "sdxl",
- "title": "Base",
- "default": "sdxl"
+ "const": "wan_model_loader",
+ "default": "wan_model_loader",
+ "field_kind": "node_attribute",
+ "title": "type",
+ "type": "string"
}
},
+ "required": ["model", "type", "id"],
+ "tags": ["model", "wan"],
+ "title": "Main Model - Wan 2.2",
"type": "object",
- "required": [
- "key",
- "hash",
- "path",
- "file_size",
- "name",
- "description",
- "source",
- "source_type",
- "source_api_response",
- "source_url",
- "cover_image",
- "config_path",
- "type",
- "format",
- "cpu_only",
- "base"
- ],
- "title": "VAE_Checkpoint_SDXL_Config"
+ "version": "1.0.1",
+ "output": {
+ "$ref": "#/components/schemas/WanModelLoaderOutput"
+ }
},
- "VAE_Diffusers_Flux2_Config": {
+ "WanModelLoaderOutput": {
+ "class": "output",
+ "description": "Wan 2.2 model loader output.",
"properties": {
- "key": {
- "type": "string",
- "title": "Key",
- "description": "A unique key for this model."
+ "transformer": {
+ "$ref": "#/components/schemas/WanTransformerField",
+ "description": "Wan transformer (one or two experts depending on the variant)",
+ "field_kind": "output",
+ "title": "Transformer",
+ "ui_hidden": false
},
- "hash": {
- "type": "string",
- "title": "Hash",
- "description": "The hash of the model file(s)."
+ "wan_t5_encoder": {
+ "$ref": "#/components/schemas/WanT5EncoderField",
+ "description": "UMT5-XXL tokenizer and text encoder for Wan 2.2",
+ "field_kind": "output",
+ "title": "UMT5-XXL Encoder",
+ "ui_hidden": false
},
- "path": {
- "type": "string",
- "title": "Path",
- "description": "Path to the model on the filesystem. Relative paths are relative to the Invoke root directory."
+ "vae": {
+ "$ref": "#/components/schemas/VAEField",
+ "description": "VAE",
+ "field_kind": "output",
+ "title": "VAE",
+ "ui_hidden": false
},
- "file_size": {
- "type": "integer",
- "title": "File Size",
- "description": "The size of the model in bytes."
+ "type": {
+ "const": "wan_model_loader_output",
+ "default": "wan_model_loader_output",
+ "field_kind": "node_attribute",
+ "title": "type",
+ "type": "string"
+ }
+ },
+ "required": ["output_meta", "transformer", "wan_t5_encoder", "vae", "type", "type"],
+ "title": "WanModelLoaderOutput",
+ "type": "object"
+ },
+ "WanRefImageConditioningField": {
+ "description": "Reference-image conditioning for Wan 2.2 I2V.\n\nCarries the 20-channel VAE-latent condition tensor (4-channel first-frame\nmask + 16-channel ref-image latents). The denoise loop concatenates this\nto the 16-channel noise latents along the channel dim each step, producing\nthe 36-channel input the I2V-A14B transformer expects.\n\nAlso carries the spatial dims and frame count used to encode the image so\nthe denoise node can sanity-check the user's width/height/num_frames \u2014 a\nlatent temporal-dim mismatch is hard to debug from the downstream error.",
+ "properties": {
+ "condition_tensor_name": {
+ "description": "Name of the saved [1, 20, T_lat, H/8, W/8] condition tensor.",
+ "title": "Condition Tensor Name",
+ "type": "string"
},
- "name": {
- "type": "string",
- "title": "Name",
- "description": "Name of the model."
+ "width": {
+ "description": "Image width used during VAE encoding (matches denoise width).",
+ "title": "Width",
+ "type": "integer"
},
- "description": {
- "anyOf": [
- {
- "type": "string"
- },
- {
- "type": "null"
- }
- ],
- "title": "Description",
- "description": "Model description"
+ "height": {
+ "description": "Image height used during VAE encoding (matches denoise height).",
+ "title": "Height",
+ "type": "integer"
},
- "source": {
- "type": "string",
- "title": "Source",
- "description": "The original source of the model (path, URL or repo_id)."
+ "num_frames": {
+ "default": 1,
+ "description": "Pixel-frame count the condition was built for. 1 for single-frame I2V (image output), 81+ for video.",
+ "title": "Num Frames",
+ "type": "integer"
+ }
+ },
+ "required": ["condition_tensor_name", "width", "height"],
+ "title": "WanRefImageConditioningField",
+ "type": "object"
+ },
+ "WanRefImageEncoderInvocation": {
+ "category": "conditioning",
+ "class": "invocation",
+ "classification": "prototype",
+ "description": "VAE-encode a reference image into Wan 2.2 I2V conditioning.\n\nThe wired VAE selects the output shape. With the 16-channel A14B VAE the\noutput is a ``[1, 20, T_lat, height // 8, width // 8]`` condition (4-ch\nfirst-frame mask + 16-ch image latents) that the denoise loop concatenates\nto the noise latents each step, producing the 36-channel input the\nI2V-A14B transformer expects. With the 48-channel TI2V-5B VAE the output\nis a ``[1, 48, 1, height // 16, width // 16]`` condition that the denoise\nloop blends into the noisy latents at every step.\n\nFor image (single-frame) I2V leave ``num_frames=1`` (T_lat=1). For A14B\nvideo I2V set ``num_frames`` to match the value on the video-denoise node\n(e.g. 81 for the Wan 2.2 reference defaults); TI2V-5B always uses the\nsingle-frame condition.\n\nSupply an optional ``end_image`` for **first-last-frame interpolation\n(FLF2V)** \u2014 the model then interpolates the motion from ``image`` (first\nframe) to ``end_image`` (final frame). FLF2V is I2V-A14B video only\n(``num_frames > 1``); it is not supported for TI2V-5B or single-frame I2V.\n\nFor T2V (no reference image), omit this node entirely.",
+ "node_pack": "invokeai",
+ "properties": {
+ "id": {
+ "description": "The id of this instance of an invocation. Must be unique among all instances of invocations.",
+ "field_kind": "node_attribute",
+ "title": "Id",
+ "type": "string"
},
- "source_type": {
- "$ref": "#/components/schemas/ModelSourceType",
- "description": "The type of source"
+ "is_intermediate": {
+ "default": false,
+ "description": "Whether or not this is an intermediate invocation.",
+ "field_kind": "node_attribute",
+ "input": "direct",
+ "orig_required": true,
+ "title": "Is Intermediate",
+ "type": "boolean",
+ "ui_hidden": false,
+ "ui_type": "IsIntermediate"
},
- "source_api_response": {
+ "use_cache": {
+ "default": true,
+ "description": "Whether or not to use the cache",
+ "field_kind": "node_attribute",
+ "title": "Use Cache",
+ "type": "boolean"
+ },
+ "image": {
"anyOf": [
{
- "type": "string"
+ "$ref": "#/components/schemas/ImageField"
},
{
"type": "null"
}
],
- "title": "Source Api Response",
- "description": "The original API response from the source, as stringified JSON."
+ "default": null,
+ "description": "Reference image to condition on (the first frame).",
+ "field_kind": "input",
+ "input": "any",
+ "orig_required": true
},
- "source_url": {
+ "vae": {
"anyOf": [
{
- "type": "string"
+ "$ref": "#/components/schemas/VAEField"
},
{
"type": "null"
}
],
- "title": "Source Url",
- "description": "Optional URL for the model (e.g. download page or model page)."
+ "default": null,
+ "description": "VAE",
+ "field_kind": "input",
+ "input": "connection",
+ "orig_required": true,
+ "title": "VAE"
},
- "cover_image": {
+ "width": {
+ "default": 1024,
+ "description": "Width to resize the reference image to (must match denoise width).",
+ "field_kind": "input",
+ "input": "any",
+ "multipleOf": 16,
+ "orig_default": 1024,
+ "orig_required": false,
+ "title": "Width",
+ "type": "integer"
+ },
+ "height": {
+ "default": 1024,
+ "description": "Height to resize the reference image to (must match denoise height).",
+ "field_kind": "input",
+ "input": "any",
+ "multipleOf": 16,
+ "orig_default": 1024,
+ "orig_required": false,
+ "title": "Height",
+ "type": "integer"
+ },
+ "num_frames": {
+ "default": 1,
+ "description": "Pixel-frame count to build the condition for. Use 1 for single-frame image I2V. For video I2V, set this to match the video-denoise node's num_frames (and ensure (num_frames - 1) %% 4 == 0, e.g. 81).",
+ "field_kind": "input",
+ "input": "any",
+ "minimum": 1,
+ "orig_default": 1,
+ "orig_required": false,
+ "title": "Number of Frames",
+ "type": "integer"
+ },
+ "end_image": {
"anyOf": [
{
- "type": "string"
+ "$ref": "#/components/schemas/ImageField"
},
{
"type": "null"
}
],
- "title": "Cover Image",
- "description": "Url for image to preview model"
- },
- "format": {
- "type": "string",
- "const": "diffusers",
- "title": "Format",
- "default": "diffusers"
+ "default": null,
+ "description": "Optional end frame for first-last-frame interpolation (FLF2V). When set, the video interpolates from the reference image (first frame) to this image (final frame). I2V-A14B video only (num_frames > 1); not supported for TI2V-5B or single-frame I2V.",
+ "field_kind": "input",
+ "input": "any",
+ "orig_default": null,
+ "orig_required": false,
+ "title": "End Image (FLF2V)"
},
- "repo_variant": {
- "$ref": "#/components/schemas/ModelRepoVariant",
- "default": ""
+ "type": {
+ "const": "wan_ref_image_encoder",
+ "default": "wan_ref_image_encoder",
+ "field_kind": "node_attribute",
+ "title": "type",
+ "type": "string"
+ }
+ },
+ "required": ["type", "id"],
+ "tags": ["image", "conditioning", "wan", "i2v"],
+ "title": "Reference Image - Wan 2.2",
+ "type": "object",
+ "version": "1.2.0",
+ "output": {
+ "$ref": "#/components/schemas/WanRefImageOutput"
+ }
+ },
+ "WanRefImageOutput": {
+ "class": "output",
+ "description": "Output of a Wan 2.2 reference-image VAE-encoder.",
+ "properties": {
+ "ref_image": {
+ "$ref": "#/components/schemas/WanRefImageConditioningField",
+ "description": "VAE-latent reference-image conditioning for Wan 2.2 I2V.",
+ "field_kind": "output",
+ "title": "Reference Image",
+ "ui_hidden": false
},
"type": {
- "type": "string",
- "const": "vae",
- "title": "Type",
- "default": "vae"
+ "const": "wan_ref_image_output",
+ "default": "wan_ref_image_output",
+ "field_kind": "node_attribute",
+ "title": "type",
+ "type": "string"
+ }
+ },
+ "required": ["output_meta", "ref_image", "type", "type"],
+ "title": "WanRefImageOutput",
+ "type": "object"
+ },
+ "WanT5EncoderField": {
+ "description": "Field for the UMT5-XXL text encoder used by Wan 2.2 models.",
+ "properties": {
+ "tokenizer": {
+ "$ref": "#/components/schemas/ModelIdentifierField",
+ "description": "Info to load tokenizer submodel"
},
- "base": {
- "type": "string",
- "const": "flux2",
- "title": "Base",
- "default": "flux2"
+ "text_encoder": {
+ "$ref": "#/components/schemas/ModelIdentifierField",
+ "description": "Info to load text_encoder submodel"
},
- "cpu_only": {
- "anyOf": [
- {
- "type": "boolean"
- },
- {
- "type": "null"
- }
- ],
- "title": "Cpu Only",
- "description": "Whether this model should run on CPU only"
+ "loras": {
+ "description": "LoRAs to apply on model loading",
+ "items": {
+ "$ref": "#/components/schemas/LoRAField"
+ },
+ "title": "Loras",
+ "type": "array"
}
},
- "type": "object",
- "required": [
- "key",
- "hash",
- "path",
- "file_size",
- "name",
- "description",
- "source",
- "source_type",
- "source_api_response",
- "source_url",
- "cover_image",
- "format",
- "repo_variant",
- "type",
- "base",
- "cpu_only"
- ],
- "title": "VAE_Diffusers_Flux2_Config",
- "description": "Model config for FLUX.2 VAE models in diffusers format (AutoencoderKLFlux2)."
+ "required": ["tokenizer", "text_encoder"],
+ "title": "WanT5EncoderField",
+ "type": "object"
},
- "VAE_Diffusers_SD1_Config": {
+ "WanT5Encoder_WanT5Encoder_Config": {
"properties": {
"key": {
"type": "string",
@@ -73129,21 +79360,23 @@
"title": "Cover Image",
"description": "Url for image to preview model"
},
- "format": {
+ "base": {
"type": "string",
- "const": "diffusers",
- "title": "Format",
- "default": "diffusers"
- },
- "repo_variant": {
- "$ref": "#/components/schemas/ModelRepoVariant",
- "default": ""
+ "const": "any",
+ "title": "Base",
+ "default": "any"
},
"type": {
"type": "string",
- "const": "vae",
+ "const": "wan_t5_encoder",
"title": "Type",
- "default": "vae"
+ "default": "wan_t5_encoder"
+ },
+ "format": {
+ "type": "string",
+ "const": "wan_t5_encoder",
+ "title": "Format",
+ "default": "wan_t5_encoder"
},
"cpu_only": {
"anyOf": [
@@ -73156,12 +79389,6 @@
],
"title": "Cpu Only",
"description": "Whether this model should run on CPU only"
- },
- "base": {
- "type": "string",
- "const": "sd-1",
- "title": "Base",
- "default": "sd-1"
}
},
"type": "object",
@@ -73177,42 +79404,143 @@
"source_api_response",
"source_url",
"cover_image",
- "format",
- "repo_variant",
+ "base",
"type",
- "cpu_only",
- "base"
+ "format",
+ "cpu_only"
],
- "title": "VAE_Diffusers_SD1_Config"
+ "title": "WanT5Encoder_WanT5Encoder_Config",
+ "description": "UMT5-XXL encoder in diffusers folder layout.\n\nAccepts either:\n- A directory containing ``text_encoder/`` (and typically ``tokenizer/``) \u2500 the\n shape produced by ``Wan-AI/Wan2.2-T2V-A14B::text_encoder+tokenizer``.\n- A bare ``text_encoder/`` directory whose own ``config.json`` declares\n ``model_type: umt5``."
},
- "VAE_Diffusers_SDXL_Config": {
+ "WanTI2VIdealDimensionsInvocation": {
+ "category": "video",
+ "class": "invocation",
+ "classification": "prototype",
+ "description": "Ideal dimensions for the Wan 2.2 TI2V-5B model.\n\nUse this node for TI2V-5B only. For the A14B models (I2V-A14B / T2V-A14B) use\n\"Wan 2.2 I2V Ideal Dimensions\" instead \u2014 those need multiples of 16, and this\nnode's multiples-of-32 dims would overshoot their pixel grid.\n\nIdentical to the A14B node but snaps each dimension to a multiple of 32 instead\nof 16: the Wan 2.2-VAE used by TI2V-5B applies 16x spatial compression and the\ntransformer adds a 2x patch on top, so pixel dims must divide by 32 for the\npatchify step. Wire from ``Image Primitive``'s width/height outputs and into\nthe matching ``wan_denoise`` inputs.",
+ "node_pack": "invokeai",
"properties": {
- "key": {
- "type": "string",
- "title": "Key",
- "description": "A unique key for this model."
+ "id": {
+ "description": "The id of this instance of an invocation. Must be unique among all instances of invocations.",
+ "field_kind": "node_attribute",
+ "title": "Id",
+ "type": "string"
},
- "hash": {
- "type": "string",
- "title": "Hash",
- "description": "The hash of the model file(s)."
+ "is_intermediate": {
+ "default": false,
+ "description": "Whether or not this is an intermediate invocation.",
+ "field_kind": "node_attribute",
+ "input": "direct",
+ "orig_required": true,
+ "title": "Is Intermediate",
+ "type": "boolean",
+ "ui_hidden": false,
+ "ui_type": "IsIntermediate"
},
- "path": {
- "type": "string",
- "title": "Path",
- "description": "Path to the model on the filesystem. Relative paths are relative to the Invoke root directory."
+ "use_cache": {
+ "default": true,
+ "description": "Whether or not to use the cache",
+ "field_kind": "node_attribute",
+ "title": "Use Cache",
+ "type": "boolean"
},
- "file_size": {
- "type": "integer",
- "title": "File Size",
- "description": "The size of the model in bytes."
+ "width": {
+ "default": 1024,
+ "description": "Source image width in pixels.",
+ "exclusiveMinimum": 0,
+ "field_kind": "input",
+ "input": "any",
+ "orig_default": 1024,
+ "orig_required": false,
+ "title": "Width",
+ "type": "integer"
},
- "name": {
+ "height": {
+ "default": 1024,
+ "description": "Source image height in pixels.",
+ "exclusiveMinimum": 0,
+ "field_kind": "input",
+ "input": "any",
+ "orig_default": 1024,
+ "orig_required": false,
+ "title": "Height",
+ "type": "integer"
+ },
+ "target_resolution": {
+ "default": "720p",
+ "description": "Short-side resolution preset. 480p and 720p are Wan 2.2's native training resolutions; 1080p works but is extrapolation and costs ~2.25x the memory of 720p.",
+ "enum": ["480p", "720p", "1080p"],
+ "field_kind": "input",
+ "input": "any",
+ "orig_default": "720p",
+ "orig_required": false,
+ "title": "Target Resolution",
"type": "string",
- "title": "Name",
- "description": "Name of the model."
+ "ui_choice_labels": {
+ "1080p": "1080p (extrapolated \u2014 not a Wan training size)",
+ "480p": "480p (Wan native)",
+ "720p": "720p (Wan native, default)"
+ }
},
- "description": {
+ "rounding": {
+ "default": "nearest",
+ "description": "How to snap each dimension to a multiple of 32. 'floor' rounds down \u2014 safest for VRAM, guaranteed not to exceed the unsnapped target. 'ceiling' rounds up. 'nearest' minimizes aspect-ratio drift (default).",
+ "enum": ["nearest", "floor", "ceiling"],
+ "field_kind": "input",
+ "input": "any",
+ "orig_default": "nearest",
+ "orig_required": false,
+ "title": "Rounding",
+ "type": "string"
+ },
+ "type": {
+ "const": "wan_ti2v_ideal_dimensions",
+ "default": "wan_ti2v_ideal_dimensions",
+ "field_kind": "node_attribute",
+ "title": "type",
+ "type": "string"
+ }
+ },
+ "required": ["type", "id"],
+ "tags": ["wan", "video", "dimensions", "math"],
+ "title": "Wan 2.2 TI2V Ideal Dimensions (5B)",
+ "type": "object",
+ "version": "1.0.0",
+ "output": {
+ "$ref": "#/components/schemas/IdealSizeOutput"
+ }
+ },
+ "WanTextEncoderInvocation": {
+ "category": "conditioning",
+ "class": "invocation",
+ "classification": "prototype",
+ "description": "Encodes a text prompt for Wan 2.2 using the UMT5-XXL encoder.\n\nOutput is the encoder's last hidden state (shape: [seq_len=512, 4096]) plus\nan attention mask marking valid (non-padding) tokens. The Wan transformer\nconsumes these directly as ``encoder_hidden_states``.",
+ "node_pack": "invokeai",
+ "properties": {
+ "id": {
+ "description": "The id of this instance of an invocation. Must be unique among all instances of invocations.",
+ "field_kind": "node_attribute",
+ "title": "Id",
+ "type": "string"
+ },
+ "is_intermediate": {
+ "default": false,
+ "description": "Whether or not this is an intermediate invocation.",
+ "field_kind": "node_attribute",
+ "input": "direct",
+ "orig_required": true,
+ "title": "Is Intermediate",
+ "type": "boolean",
+ "ui_hidden": false,
+ "ui_type": "IsIntermediate"
+ },
+ "use_cache": {
+ "default": true,
+ "description": "Whether or not to use the cache",
+ "field_kind": "node_attribute",
+ "title": "Use Cache",
+ "type": "boolean"
+ },
+ "prompt": {
"anyOf": [
{
"type": "string"
@@ -73221,183 +79549,295 @@
"type": "null"
}
],
- "title": "Description",
- "description": "Model description"
+ "default": null,
+ "description": "Text prompt for Wan 2.2.",
+ "field_kind": "input",
+ "input": "any",
+ "orig_required": true,
+ "title": "Prompt",
+ "ui_component": "textarea"
},
- "source": {
- "type": "string",
- "title": "Source",
- "description": "The original source of the model (path, URL or repo_id)."
+ "wan_t5_encoder": {
+ "anyOf": [
+ {
+ "$ref": "#/components/schemas/WanT5EncoderField"
+ },
+ {
+ "type": "null"
+ }
+ ],
+ "default": null,
+ "description": "UMT5-XXL tokenizer and text encoder for Wan 2.2",
+ "field_kind": "input",
+ "input": "connection",
+ "orig_required": true,
+ "title": "UMT5-XXL Encoder"
},
- "source_type": {
- "$ref": "#/components/schemas/ModelSourceType",
- "description": "The type of source"
+ "type": {
+ "const": "wan_text_encoder",
+ "default": "wan_text_encoder",
+ "field_kind": "node_attribute",
+ "title": "type",
+ "type": "string"
+ }
+ },
+ "required": ["type", "id"],
+ "tags": ["prompt", "conditioning", "wan"],
+ "title": "Prompt - Wan 2.2",
+ "type": "object",
+ "version": "1.0.0",
+ "output": {
+ "$ref": "#/components/schemas/WanConditioningOutput"
+ }
+ },
+ "WanTransformerField": {
+ "description": "Transformer field for Wan 2.2 models.\n\nWan 2.2 A14B is a Mixture-of-Experts model with two transformer experts:\na high-noise expert (active at large timesteps) and a low-noise expert\n(active at small timesteps). TI2V-5B is a single-transformer model and only\npopulates ``transformer``.\n\n``boundary_ratio`` matches Diffusers' ``WanPipeline`` semantics: it's the\nboundary timestep as a fraction of ``num_train_timesteps`` (typically 1000),\nso ``boundary_ratio=0.875`` means the high-noise expert handles t >= 875 and\nthe low-noise expert handles t < 875.",
+ "properties": {
+ "transformer": {
+ "$ref": "#/components/schemas/ModelIdentifierField",
+ "description": "Primary transformer submodel. For A14B this is the high-noise expert."
},
- "source_api_response": {
+ "transformer_low_noise": {
"anyOf": [
{
- "type": "string"
+ "$ref": "#/components/schemas/ModelIdentifierField"
},
{
"type": "null"
}
],
- "title": "Source Api Response",
- "description": "The original API response from the source, as stringified JSON."
+ "default": null,
+ "description": "Low-noise transformer expert (Wan 2.2 A14B only). None for TI2V-5B."
},
- "source_url": {
+ "loras": {
+ "description": "LoRAs to apply to the primary transformer. For A14B applied to the high-noise expert.",
+ "items": {
+ "$ref": "#/components/schemas/LoRAField"
+ },
+ "title": "Loras",
+ "type": "array"
+ },
+ "loras_low_noise": {
+ "description": "LoRAs to apply to the low-noise expert (Wan 2.2 A14B). The Wan LoRA loader routes 'both'- and 'low'-targeted LoRAs here; if empty, no LoRAs are applied to the low-noise expert (a 'high'-targeted LoRA must not leak onto it).",
+ "items": {
+ "$ref": "#/components/schemas/LoRAField"
+ },
+ "title": "Loras Low Noise",
+ "type": "array"
+ },
+ "boundary_ratio": {
+ "default": 0.875,
+ "description": "Boundary timestep as a fraction of num_train_timesteps (Wan 2.2 A14B only). High-noise expert: t >= boundary_ratio * num_train_timesteps. Low-noise expert: t below. Ignored for TI2V-5B.",
+ "maximum": 1.0,
+ "minimum": 0.0,
+ "title": "Boundary Ratio",
+ "type": "number"
+ }
+ },
+ "required": ["transformer"],
+ "title": "WanTransformerField",
+ "type": "object"
+ },
+ "WanVariantType": {
+ "type": "string",
+ "enum": ["t2v_a14b", "i2v_a14b", "ti2v_5b"],
+ "title": "WanVariantType",
+ "description": "Wan 2.2 model variants.\n\nAll variants are used for image generation at num_frames=1. The A14B family\nis a Mixture-of-Experts (high-noise + low-noise) totalling ~28B params; the\nT2V sub-variant takes text only, while the I2V sub-variant additionally\nconditions on a reference image (encoded by the VAE and concatenated to the\nnoise latents along the channel dim \u2014 its transformer has ``in_channels=36``\ninstead of ``16``). TI2V-5B is a single ~5B transformer with a\nhigher-compression VAE (z_dim=48)."
+ },
+ "WanVideoDenoiseInvocation": {
+ "category": "latents",
+ "class": "invocation",
+ "classification": "prototype",
+ "description": "Run the Wan 2.2 denoising loop on a multi-frame latent tensor.\n\nThe output is a 5D ``[1, C, T_lat, H/8, W/8]`` latent tensor ready for\n:class:`WanLatentsToVideoInvocation` to VAE-decode and encode as MP4.\n\nMirrors :class:`WanDenoiseInvocation` for the per-step logic (CFG, MoE\nexpert swap at the boundary timestep, LoRA patching, scheduler selection).\nDifferences from the image denoise:\n\n* The noise tensor has a real temporal dim built from ``num_frames``.\n* The I2V condition is built across all latent frames (frame 0\n conditioned, rest zero) via\n :func:`encode_reference_image_to_video_condition` upstream \u2014 the\n ``ref_image`` field on this node carries a tensor of shape\n ``[1, 20, T_lat, H_lat, W_lat]`` instead of ``[1, 20, 1, ...]``.\n* No ``denoising_start`` / ``denoising_end`` / initial-latents inputs.\n The image denoise node uses those for img2img (noise injection on an\n existing latent), but image-conditioned video generation flows through\n the reference-frame conditioning mechanism instead \u2014 the first frame\n drives subsequent frames at every step, so a partial-schedule run from\n an initial latent has no analogue here. Run the schedule from t=1\n to t=0 every time. The base ``WanDenoiseInvocation`` still handles\n the img2img case for stills.",
+ "node_pack": "invokeai",
+ "properties": {
+ "id": {
+ "description": "The id of this instance of an invocation. Must be unique among all instances of invocations.",
+ "field_kind": "node_attribute",
+ "title": "Id",
+ "type": "string"
+ },
+ "is_intermediate": {
+ "default": false,
+ "description": "Whether or not this is an intermediate invocation.",
+ "field_kind": "node_attribute",
+ "input": "direct",
+ "orig_required": true,
+ "title": "Is Intermediate",
+ "type": "boolean",
+ "ui_hidden": false,
+ "ui_type": "IsIntermediate"
+ },
+ "use_cache": {
+ "default": true,
+ "description": "Whether or not to use the cache",
+ "field_kind": "node_attribute",
+ "title": "Use Cache",
+ "type": "boolean"
+ },
+ "transformer": {
"anyOf": [
{
- "type": "string"
+ "$ref": "#/components/schemas/WanTransformerField"
},
{
"type": "null"
}
],
- "title": "Source Url",
- "description": "Optional URL for the model (e.g. download page or model page)."
+ "default": null,
+ "description": "Wan transformer field. Supported: T2V-A14B / I2V-A14B (dual-expert) and TI2V-5B (single-expert, handles both T2V and I2V). All three accept a Reference Image input for image-to-video; A14B uses the 36-channel concat scheme while TI2V-5B uses the expand_timesteps first-frame-mask blend.",
+ "field_kind": "input",
+ "input": "connection",
+ "orig_required": true,
+ "title": "Transformer"
},
- "cover_image": {
+ "positive_conditioning": {
"anyOf": [
{
- "type": "string"
+ "$ref": "#/components/schemas/WanConditioningField"
},
{
"type": "null"
}
],
- "title": "Cover Image",
- "description": "Url for image to preview model"
- },
- "format": {
- "type": "string",
- "const": "diffusers",
- "title": "Format",
- "default": "diffusers"
- },
- "repo_variant": {
- "$ref": "#/components/schemas/ModelRepoVariant",
- "default": ""
- },
- "type": {
- "type": "string",
- "const": "vae",
- "title": "Type",
- "default": "vae"
+ "default": null,
+ "description": "Positive conditioning tensor",
+ "field_kind": "input",
+ "input": "connection",
+ "orig_required": true
},
- "cpu_only": {
+ "negative_conditioning": {
"anyOf": [
{
- "type": "boolean"
+ "$ref": "#/components/schemas/WanConditioningField"
},
{
"type": "null"
}
],
- "title": "Cpu Only",
- "description": "Whether this model should run on CPU only"
- },
- "base": {
- "type": "string",
- "const": "sdxl",
- "title": "Base",
- "default": "sdxl"
- }
- },
- "type": "object",
- "required": [
- "key",
- "hash",
- "path",
- "file_size",
- "name",
- "description",
- "source",
- "source_type",
- "source_api_response",
- "source_url",
- "cover_image",
- "format",
- "repo_variant",
- "type",
- "cpu_only",
- "base"
- ],
- "title": "VAE_Diffusers_SDXL_Config"
- },
- "ValidationError": {
- "properties": {
- "loc": {
- "items": {
- "anyOf": [
- {
- "type": "string"
- },
- {
- "type": "integer"
- }
- ]
- },
- "type": "array",
- "title": "Location"
- },
- "msg": {
- "type": "string",
- "title": "Message"
- },
- "type": {
- "type": "string",
- "title": "Error Type"
- }
- },
- "type": "object",
- "required": ["loc", "msg", "type"],
- "title": "ValidationError"
- },
- "VirtualSubBoardDTO": {
- "properties": {
- "virtual_board_id": {
- "type": "string",
- "title": "Virtual Board Id",
- "description": "The virtual board ID, e.g. 'by_date:2026-03-18'."
- },
- "board_name": {
- "type": "string",
- "title": "Board Name",
- "description": "The display name of the virtual sub-board, e.g. '2026-03-18'."
- },
- "date": {
- "type": "string",
- "title": "Date",
- "description": "The ISO date string, e.g. '2026-03-18'."
+ "default": null,
+ "description": "Negative conditioning tensor",
+ "field_kind": "input",
+ "input": "connection",
+ "orig_default": null,
+ "orig_required": false
},
- "image_count": {
- "type": "integer",
- "title": "Image Count",
- "description": "The number of general images for this date."
+ "ref_image": {
+ "anyOf": [
+ {
+ "$ref": "#/components/schemas/WanRefImageConditioningField"
+ },
+ {
+ "type": "null"
+ }
+ ],
+ "default": null,
+ "description": "Reference-image (VAE-latent) conditioning for Wan 2.2 I2V.",
+ "field_kind": "input",
+ "input": "connection",
+ "orig_default": null,
+ "orig_required": false,
+ "title": "Reference Image"
},
- "asset_count": {
- "type": "integer",
- "title": "Asset Count",
- "description": "The number of asset images for this date."
+ "guidance_scale": {
+ "default": 5.0,
+ "description": "Classifier-free guidance scale. Wan 2.2 video reference uses 5.0 for the high-noise expert and 4.0 for the low-noise expert.",
+ "field_kind": "input",
+ "input": "any",
+ "minimum": 1.0,
+ "orig_default": 5.0,
+ "orig_required": false,
+ "title": "Guidance Scale",
+ "type": "number"
},
- "cover_image_name": {
+ "guidance_scale_low_noise": {
"anyOf": [
{
- "type": "string"
+ "minimum": 0.0,
+ "type": "number"
},
{
"type": "null"
}
],
- "title": "Cover Image Name",
- "description": "The most recent image name for this date."
+ "default": 4.0,
+ "description": "Optional separate CFG scale for the low-noise expert (Wan 2.2 A14B only). Values below 1.0 fall back to the primary 'Guidance Scale'.",
+ "field_kind": "input",
+ "input": "any",
+ "orig_default": 4.0,
+ "orig_required": false,
+ "title": "Guidance Scale (Low Noise)"
+ },
+ "width": {
+ "default": 832,
+ "description": "Width of the generated video.",
+ "field_kind": "input",
+ "input": "any",
+ "multipleOf": 16,
+ "orig_default": 832,
+ "orig_required": false,
+ "title": "Width",
+ "type": "integer"
+ },
+ "height": {
+ "default": 480,
+ "description": "Height of the generated video.",
+ "field_kind": "input",
+ "input": "any",
+ "multipleOf": 16,
+ "orig_default": 480,
+ "orig_required": false,
+ "title": "Height",
+ "type": "integer"
+ },
+ "num_frames": {
+ "default": 81,
+ "description": "Number of output frames. Must satisfy (num_frames - 1) %% 4 == 0 so the latent temporal dim divides cleanly. Wan 2.2 was trained at 81 frames @ 16 FPS (~5 s).",
+ "field_kind": "input",
+ "input": "any",
+ "minimum": 5,
+ "orig_default": 81,
+ "orig_required": false,
+ "title": "Number of Frames",
+ "type": "integer"
+ },
+ "steps": {
+ "default": 40,
+ "description": "Number of denoising steps.",
+ "exclusiveMinimum": 0,
+ "field_kind": "input",
+ "input": "any",
+ "orig_default": 40,
+ "orig_required": false,
+ "title": "Steps",
+ "type": "integer"
+ },
+ "seed": {
+ "default": 0,
+ "description": "Randomness seed for reproducibility.",
+ "field_kind": "input",
+ "input": "any",
+ "orig_default": 0,
+ "orig_required": false,
+ "title": "Seed",
+ "type": "integer"
+ },
+ "type": {
+ "const": "wan_video_denoise",
+ "default": "wan_video_denoise",
+ "field_kind": "node_attribute",
+ "title": "type",
+ "type": "string"
}
},
+ "required": ["type", "id"],
+ "tags": ["video", "wan"],
+ "title": "Denoise Video - Wan 2.2",
"type": "object",
- "required": ["virtual_board_id", "board_name", "date", "image_count", "asset_count"],
- "title": "VirtualSubBoardDTO",
- "description": "A virtual sub-board computed from image metadata, not stored in the database."
+ "version": "1.0.0",
+ "output": {
+ "$ref": "#/components/schemas/LatentsOutput"
+ }
},
"Workflow": {
"properties": {
diff --git a/invokeai/frontend/web/public/locales/en.json b/invokeai/frontend/web/public/locales/en.json
index 363bf0da7e0..53b4d9c59b6 100644
--- a/invokeai/frontend/web/public/locales/en.json
+++ b/invokeai/frontend/web/public/locales/en.json
@@ -13,7 +13,8 @@
"toggleRightPanel": "Toggle Right Panel (G)",
"toggleLeftPanel": "Toggle Left Panel (T)",
"uploadImage": "Upload Image",
- "uploadImages": "Upload Images"
+ "uploadImages": "Upload Images",
+ "uploadMedia": "Upload Media"
},
"auth": {
"login": {
@@ -53,6 +54,8 @@
"userMenu": "User Menu",
"admin": "Admin",
"logout": "Logout",
+ "logoutFailed": "Logout Failed",
+ "logoutFailedDesc": "Could not reach the server to log out. You are still signed in.",
"adminOnlyFeature": "This feature is only available to administrators.",
"profile": {
"menuItem": "My Profile",
@@ -119,8 +122,9 @@
"archived": "Archived",
"autoAddBoard": "Auto-Add Board",
"boards": "Boards",
+ "byDate": "By Date",
"selectedForAutoAdd": "Selected for Auto-Add",
- "bottomMessage": "Deleting images will reset any features currently using them.",
+ "bottomMessage": "Deleting images/videos will reset any features currently using them.",
"cancel": "Cancel",
"pause": "Pause",
"resume": "Resume",
@@ -129,22 +133,27 @@
"restartRequired": "Restart required",
"resumeRefused": "Resume refused by server. Restart required.",
"changeBoard": "Change Board",
+ "changeBoardImage_one": "Move Image to Board",
+ "changeBoardImage_other": "Move {{count}} Images to Board",
+ "changeBoardVideo_one": "Move Video to Board",
+ "changeBoardVideo_other": "Move {{count}} Videos to Board",
"clearSearch": "Clear Search",
"deleteBoard": "Delete Board",
"deleteBoardAndImages": "Delete Board and Images",
+ "deleteBoardAndAssets": "Delete Board, Images & Videos",
"deleteBoardOnly": "Delete Board Only",
- "deletedBoardsCannotbeRestored": "Deleted boards and images cannot be restored. Selecting 'Delete Board Only' will move images to an uncategorized state.",
- "deletedPrivateBoardsCannotbeRestored": "Deleted boards and images cannot be restored. Selecting 'Delete Board Only' will move images to a private uncategorized state for the image's creator.",
- "uncategorizedImages": "Uncategorized Images",
- "deleteAllUncategorizedImages": "Delete All Uncategorized Images",
+ "deletedBoardsCannotbeRestored": "Deleted boards, images, and videos cannot be restored. Selecting 'Delete Board Only' will move images and videos to an uncategorized state.",
+ "deletedPrivateBoardsCannotbeRestored": "Deleted boards, images, and videos cannot be restored. Selecting 'Delete Board Only' will move images and videos to a private uncategorized state for the original creator.",
+ "uncategorizedImages": "Uncategorized Images/Videos",
+ "deleteAllUncategorizedImages": "Delete All Uncategorized Images/Videos",
"deletedImagesCannotBeRestored": "Deleted images cannot be restored.",
"hideBoards": "Hide Boards",
"loading": "Loading...",
"locateInGalery": "Locate in Gallery",
"menuItemAutoAdd": "Auto-add to this Board",
"move": "Move",
- "movingImagesToBoard_one": "Moving {{count}} image to board:",
- "movingImagesToBoard_other": "Moving {{count}} images to board:",
+ "movingImagesToBoard_one": "Moving {{count}} image/video to board:",
+ "movingImagesToBoard_other": "Moving {{count}} images/videos to board:",
"myBoard": "My Board",
"noBoards": "No {{boardType}} Boards",
"noMatching": "No matching Boards",
@@ -152,15 +161,17 @@
"searchBoard": "Search Boards...",
"selectBoard": "Select a Board",
"shared": "Shared Boards",
- "topMessage": "This selection contains images used in the following features:",
+ "topMessage": "This selection contains images/videos used in the following features:",
"unarchiveBoard": "Unarchive Board",
"uncategorized": "Uncategorized",
"viewBoards": "View Boards",
- "downloadBoard": "Download Board",
+ "downloadBoard": "Download Board Images",
"imagesWithCount_one": "{{count}} image",
"imagesWithCount_other": "{{count}} images",
"assetsWithCount_one": "{{count}} asset",
"assetsWithCount_other": "{{count}} assets",
+ "videosWithCount_one": "{{count}} video",
+ "videosWithCount_other": "{{count}} videos",
"updateBoardError": "Error updating board",
"setBoardVisibility": "Set Board Visibility",
"setVisibilityPrivate": "Set Private",
@@ -212,7 +223,9 @@
"copy": "Copy",
"copyError": "$t(gallery.copy) Error",
"clipboard": "Clipboard",
+ "collapse": "Collapse",
"collapseAll": "Collapse All",
+ "expand": "Expand",
"crop": "Crop",
"on": "On",
"off": "Off",
@@ -522,7 +535,7 @@
"alwaysShowImageSizeBadge": "Always Show Image Size Badge",
"assetsTab": "Files you've uploaded for use in your projects.",
"autoAssignBoardOnClick": "Auto-Assign Board",
- "autoSwitchNewImages": "Auto-Switch to New Images",
+ "autoSwitchNewImages": "Auto-Switch to New Images/Videos",
"boardsSettings": "Boards Settings",
"copy": "Copy",
"currentlyInUse": "This image is currently in use in the following features:",
@@ -532,32 +545,50 @@
"deleteImage_one": "Delete Image",
"deleteImage_other": "Delete {{count}} Images",
"deleteImagePermanent": "Deleted images cannot be restored.",
+ "deleteVideo_one": "Delete Video",
+ "deleteVideo_other": "Delete {{count}} Videos",
+ "deleteVideoPermanent": "Deleted videos cannot be restored.",
+ "playVideo": "Play video",
+ "closeVideoPlayer": "Close video player",
+ "copyVideoFrame": "Copy Frame",
"displayBoardSearch": "Board Search",
- "displaySearch": "Image Search",
+ "displaySearch": "Gallery Search",
"download": "Download",
"exitBoardSearch": "Exit Board Search",
- "exitSearch": "Exit Image Search",
+ "exitSearch": "Exit Gallery Search",
"featuresWillReset": "If you delete this image, those features will immediately be reset.",
"galleryImageSize": "Image Size",
"gallerySettings": "Gallery Settings",
"go": "Go",
"image": "image",
- "imagesTab": "Images you've created and saved within Invoke.",
- "imagesSettings": "Gallery Images Settings",
+ "imagesTab": "Images/Videos you've created and saved within Invoke.",
+ "imagesSettings": "Gallery Images/Videos Settings",
"jump": "Jump",
"loading": "Loading",
"loadingGallery": "Loading gallery...",
"loadingMetadata": "Loading metadata...",
"newestFirst": "Newest First",
- "noImagesFound": "No images found",
+ "noImagesFound": "No images/videos found",
"oldestFirst": "Oldest First",
"sortDirection": "Sort Direction",
- "showStarredImagesFirst": "Show Starred Images First",
+ "showStarredImagesFirst": "Show Starred Images/Videos First",
"usePagedGalleryView": "Use Paged Gallery View",
"noImageSelected": "No Image Selected",
"noImagesInGallery": "No Images to Display",
"starImage": "Star",
+ "starImage_one": "Star Image",
+ "starImage_other": "Star {{count}} Images",
"unstarImage": "Unstar",
+ "unstarImage_one": "Unstar Image",
+ "unstarImage_other": "Unstar {{count}} Images",
+ "starVideo_one": "Star Video",
+ "starVideo_other": "Star {{count}} Videos",
+ "unstarVideo_one": "Unstar Video",
+ "unstarVideo_other": "Unstar {{count}} Videos",
+ "downloadImage_one": "Download Image",
+ "downloadImage_other": "Download {{count}} Images",
+ "downloadVideo_one": "Download Video",
+ "downloadVideo_other": "Download {{count}} Videos",
"unableToLoad": "Unable to load Gallery",
"deleteSelection": "Delete Selection",
"downloadSelection": "Download Selection",
@@ -588,7 +619,8 @@
"openViewer": "Open Viewer",
"closeViewer": "Close Viewer",
"move": "Move",
- "useForPromptGeneration": "Use for Prompt Generation"
+ "useForPromptGeneration": "Use for Prompt Generation",
+ "deleteMediaPermanent": "Deleted images/videos cannot be restored."
},
"hotkeys": {
"hotkeys": "Hotkeys",
@@ -957,7 +989,7 @@
"title": "Gallery",
"selectAllOnPage": {
"title": "Select All On Page",
- "desc": "Select all images on the current page."
+ "desc": "Select all images/videos on the current page."
},
"clearSelection": {
"title": "Clear Selection",
@@ -997,11 +1029,11 @@
},
"deleteSelection": {
"title": "Delete",
- "desc": "Delete all selected images. By default, you will be prompted to confirm deletion. If the images are currently in use in the app, you will be warned."
+ "desc": "Delete all selected images/videos. By default, you will be prompted to confirm deletion. If the images/videos are currently in use in the app, you will be warned."
},
"starImage": {
- "title": "Star/Unstar Image",
- "desc": "Star or unstar the selected image."
+ "title": "Star/Unstar Image/Video",
+ "desc": "Star or unstar the selected image or video."
}
}
},
@@ -1388,6 +1420,14 @@
"qwenImageQuantizationNone": "None (bf16)",
"qwenImageQuantizationInt8": "8-bit (int8)",
"qwenImageQuantizationNf4": "4-bit (nf4)",
+ "wanT5Encoder": "Wan2.2 T5 Encoder",
+ "wanT5EncoderPlaceholder": "From VAE/Encoder Source",
+ "wanVae": "VAE",
+ "wanVaePlaceholder": "From VAE/Encoder Source",
+ "wanComponentSource": "VAE/Encoder Source (Diffusers)",
+ "wanComponentSourcePlaceholder": "GGUF Wan models require a Diffusers Wan source for VAE + UMT5-XXL",
+ "wanTransformerLowNoise": "Transformer (Low Noise)",
+ "wanTransformerLowNoisePlaceholder": "Add for full detail",
"upcastAttention": "Upcast Attention",
"uploadImage": "Upload Image",
"urlOrLocalPath": "URL or Local Path",
@@ -1427,6 +1467,10 @@
"noCompatibleLoRAs": "No Compatible LoRAs"
},
"nodes": {
+ "extractVideoRange": {
+ "dropVideoPrompt": "Connect a video to preview the selected frame.",
+ "missingFps": "Cannot preview frames: input video has no fps metadata."
+ },
"arithmeticSequence": "Arithmetic Sequence",
"linearDistribution": "Linear Distribution",
"uniformRandomDistribution": "Uniform Random Distribution",
@@ -1606,6 +1650,7 @@
"internalDesc": "This invocation is used internally by Invoke. It may have breaking changes during app updates and may be removed at any time.",
"specialDesc": "This invocation some special handling in the app. For example, Batch nodes are used to queue multiple graphs from a single workflow.",
"imageAccessError": "Unable to find image {{image_name}}, resetting to default",
+ "videoAccessError": "Unable to find video {{video_name}}, resetting to default",
"boardAccessError": "Unable to find board {{board_id}}, resetting to default",
"modelAccessError": "Unable to find model {{key}}, resetting to default",
"saveToGallery": "Save To Gallery",
@@ -1657,14 +1702,14 @@
"guidance": "Guidance",
"height": "Height",
"imageFit": "Fit Initial Image To Output Size",
- "images": "Images",
+ "images": "Images/Videos",
"images_withCount_one": "Image",
- "images_withCount_other": "Images",
+ "images_withCount_other": "Images/Videos",
"infillMethod": "Infill Method",
"infillColorValue": "Fill Color",
"info": "Info",
"invoke": {
- "addingImagesTo": "Adding images to",
+ "addingImagesTo": "Adding images/videos to",
"boardNotWritable": "You do not have write access to board \"{{boardName}}\". Select a board you own or switch to Uncategorized.",
"modelDisabledForTrial": "Generating with {{modelName}} is not available on trial accounts. Visit your account settings to upgrade.",
"invoke": "Invoke",
@@ -1695,6 +1740,7 @@
"noFlux2KleinVaeModelSelected": "No VAE selected. Non-diffusers FLUX.2 Klein models require a standalone VAE",
"noFlux2KleinQwen3EncoderModelSelected": "No Qwen3 Encoder selected. Non-diffusers FLUX.2 Klein models require a standalone Qwen3 Encoder",
"noQwenImageComponentSourceSelected": "GGUF Qwen Image models require a Diffusers Component Source for VAE/encoder",
+ "noWanComponentSourceSelected": "GGUF Wan 2.2 models require a Diffusers Component Source for VAE/encoder",
"noZImageVaeSourceSelected": "No VAE source: Select VAE (FLUX) or Qwen3 Source model",
"noZImageQwen3EncoderSourceSelected": "No Qwen3 Encoder source: Select Qwen3 Encoder or Qwen3 Source model",
"noAnimaVaeModelSelected": "No Anima VAE model selected",
@@ -1749,6 +1795,7 @@
"showOptionsPanel": "Show Side Panel (O or T)",
"shift": "Shift",
"shuffle": "Shuffle Seed",
+ "wanGuidanceScaleLowNoise": "CFG (Low)",
"steps": "Steps",
"strength": "Strength",
"symmetry": "Symmetry",
@@ -1847,7 +1894,7 @@
"informationalPopoversDisabledDesc": "Informational popovers have been disabled. Enable them in Settings.",
"enableModelDescriptions": "Enable Model Descriptions in Dropdowns",
"enableHighlightFocusedRegions": "Highlight Focused Regions",
- "middleClickOpenInNewTab": "Use Middle Click to Open Images in New Tab",
+ "middleClickOpenInNewTab": "Use Middle Click to Open Images/Videos in New Tab",
"modelDescriptionsDisabled": "Model Descriptions in Dropdowns Disabled",
"modelDescriptionsDisabledDesc": "Model descriptions in dropdowns have been disabled. Enable them in Settings.",
"enableInvisibleWatermark": "Enable Invisible Watermark",
@@ -1888,8 +1935,8 @@
"prompt": "Prompt",
"resetComplete": "Web UI has been reset.",
"resetWebUI": "Reset Web UI",
- "resetWebUIDesc1": "Resetting the web UI only resets the browser's local cache of your images and remembered settings. It does not delete any images from disk.",
- "resetWebUIDesc2": "If images aren't showing up in the gallery or something else isn't working, please try resetting before submitting an issue on GitHub.",
+ "resetWebUIDesc1": "Resetting the web UI only resets the browser's local cache of your images/videos and remembered settings. It does not delete any images/videos from disk.",
+ "resetWebUIDesc2": "If images/videos aren't showing up in the gallery or something else isn't working, please try resetting before submitting an issue on GitHub.",
"showDetailedInvocationProgress": "Show Progress Details",
"showProgressInViewer": "Show Progress Images in Viewer",
"ui": "User Interface",
@@ -1937,6 +1984,18 @@
"imageSavingFailed": "Image Saving Failed",
"imageUploaded": "Image Uploaded",
"imageUploadFailed": "Image Upload Failed",
+ "videoUploaded": "Video Uploaded",
+ "videoUploadFailed": "Video Upload Failed",
+ "videoPlaybackFailed": "Unable to Play Video",
+ "videoPlaybackFailedDesc": "The video may use an unsupported codec or may no longer be available.",
+ "videoDeleteFailed": "Failed to Delete Videos",
+ "videoDeleteFailedDesc": "The delete request failed; no videos were deleted.",
+ "videoDeletePartial": "{{count}} video could not be deleted.",
+ "videoDeletePartial_other": "{{count}} videos could not be deleted.",
+ "mediaDeleteFailed": "Failed to Delete Media",
+ "mediaDeleteFailedDesc": "A delete request failed. Some media may not have been deleted.",
+ "mediaDeletePartial": "{{count}} media item could not be deleted.",
+ "mediaDeletePartial_other": "{{count}} media items could not be deleted.",
"importFailed": "Import Failed",
"importSuccessful": "Import Successful",
"invalidUpload": "Invalid Upload",
@@ -1988,10 +2047,11 @@
"setNodeField": "Set as node field",
"somethingWentWrong": "Something Went Wrong",
"uploadFailed": "Upload failed",
- "imagesWillBeAddedTo": "Uploaded images will be added to board {{boardName}}'s assets.",
+ "itemsWillBeAddedTo": "Uploaded items will be added to board {{boardName}}'s assets.",
"uploadFailedInvalidUploadDesc_withCount_one": "Must be maximum of 1 PNG, JPEG or WEBP image.",
"uploadFailedInvalidUploadDesc_withCount_other": "Must be maximum of {{count}} PNG, JPEG or WEBP images.",
"uploadFailedInvalidUploadDesc": "Must be PNG, JPEG or WEBP images.",
+ "uploadFailedInvalidMediaUploadDesc": "Must be PNG, JPEG or WEBP images, or MP4 videos.",
"workflowLoaded": "Workflow Loaded",
"problemRetrievingWorkflow": "Problem Retrieving Workflow",
"workflowDeleted": "Workflow Deleted",
@@ -3513,10 +3573,10 @@
}
},
"newUserExperience": {
- "toGetStartedLocal": "To get started, make sure to download or import models needed to run Invoke. Then, enter a prompt in the box and click Invoke to generate your first image. Select a prompt template to improve results. You can choose to save your images directly to the Gallery or edit them to the Canvas .",
- "toGetStarted": "To get started, enter a prompt in the box and click Invoke to generate your first image. Select a prompt template to improve results. You can choose to save your images directly to the Gallery or edit them to the Canvas .",
+ "toGetStartedLocal": "To get started, make sure to download or import models needed to run Invoke. Then, enter a prompt in the box and click Invoke to generate your first image. Select a prompt template to improve results. You can choose to save your images/videos directly to the Gallery or edit them to the Canvas .",
+ "toGetStarted": "To get started, enter a prompt in the box and click Invoke to generate your first image. Select a prompt template to improve results. You can choose to save your images/videos directly to the Gallery or edit them to the Canvas .",
"toGetStartedWorkflow": "To get started, fill in the fields on the left and press Invoke to generate your image. Want to explore more workflows? Click the folder icon next to the workflow title to see a list of other templates you can try.",
- "toGetStartedNonAdmin": "To get started, ask your InvokeAI administrator ( ) to install the AI models needed to run Invoke. Then, enter a prompt in the box and click Invoke to generate your first image. Select a prompt template to improve results. You can choose to save your images directly to the Gallery or edit them to the Canvas .",
+ "toGetStartedNonAdmin": "To get started, ask your InvokeAI administrator ( ) to install the AI models needed to run Invoke. Then, enter a prompt in the box and click Invoke to generate your first image. Select a prompt template to improve results. You can choose to save your images/videos directly to the Gallery or edit them to the Canvas .",
"gettingStartedSeries": "Want more guidance? Check out our Getting Started Series for tips on unlocking the full potential of the Invoke Studio.",
"lowVRAMMode": "For best performance, follow our Low VRAM guide .",
"noModelsInstalled": "It looks like you don't have any models installed! You can download a starter model bundle or import models .",
diff --git a/invokeai/frontend/web/src/app/components/GlobalHookIsolator.tsx b/invokeai/frontend/web/src/app/components/GlobalHookIsolator.tsx
index c2cdde228d3..aa3d0c968bd 100644
--- a/invokeai/frontend/web/src/app/components/GlobalHookIsolator.tsx
+++ b/invokeai/frontend/web/src/app/components/GlobalHookIsolator.tsx
@@ -9,6 +9,7 @@ import { useFocusRegionWatcher } from 'common/hooks/focus';
import { useCloseChakraTooltipsOnDragFix } from 'common/hooks/useCloseChakraTooltipsOnDragFix';
import { useGlobalHotkeys } from 'common/hooks/useGlobalHotkeys';
import { useTouchDeviceClass } from 'common/hooks/useTouchDeviceClass';
+import { useMediaCookieRefresh } from 'features/auth/hooks/useMediaCookieRefresh';
import { useDndMonitor } from 'features/dnd/useDndMonitor';
import { useDynamicPromptsWatcher } from 'features/dynamicPrompts/hooks/useDynamicPromptsWatcher';
import { useStarterModelsToast } from 'features/modelManagerV2/hooks/useStarterModelsToast';
@@ -47,6 +48,7 @@ export const GlobalHookIsolator = memo(() => {
useDndMonitor();
useSyncNodeErrors();
useSyncLangDirection();
+ useMediaCookieRefresh();
// Persistent subscription to the queue counts query - canvas relies on this to know if there are pending
// and/or in progress canvas sessions.
diff --git a/invokeai/frontend/web/src/app/components/GlobalImageHotkeys.tsx b/invokeai/frontend/web/src/app/components/GlobalImageHotkeys.tsx
index dd1595bdd74..f1bcf0c1088 100644
--- a/invokeai/frontend/web/src/app/components/GlobalImageHotkeys.tsx
+++ b/invokeai/frontend/web/src/app/components/GlobalImageHotkeys.tsx
@@ -8,6 +8,7 @@ import { useRecallPrompts } from 'features/gallery/hooks/useRecallPrompts';
import { useRecallRemix } from 'features/gallery/hooks/useRecallRemix';
import { useRecallSeed } from 'features/gallery/hooks/useRecallSeed';
import { selectLastSelectedItem } from 'features/gallery/store/gallerySelectors';
+import { isVideoName } from 'features/gallery/store/types';
import { useRegisteredHotkeys } from 'features/system/components/HotkeysModal/useHotkeyData';
import { memo } from 'react';
import { useImageDTO } from 'services/api/endpoints/images';
@@ -16,7 +17,11 @@ import type { ImageDTO } from 'services/api/types';
export const GlobalImageHotkeys = memo(() => {
useAssertSingleton('GlobalImageHotkeys');
const lastSelectedItem = useAppSelector(selectLastSelectedItem);
- const imageDTO = useImageDTO(lastSelectedItem ?? null);
+ // Recall-hotkeys are image-only; passing a video name through to useImageDTO fires a 404
+ // against /api/v1/images/i/.mp4 and emits a noisy "Image record not found" backend
+ // log on every video selection.
+ const imageName = lastSelectedItem && !isVideoName(lastSelectedItem) ? lastSelectedItem : null;
+ const imageDTO = useImageDTO(imageName);
if (!imageDTO) {
return null;
diff --git a/invokeai/frontend/web/src/app/components/GlobalModalIsolator.tsx b/invokeai/frontend/web/src/app/components/GlobalModalIsolator.tsx
index e5ec5ccc565..a3284e06166 100644
--- a/invokeai/frontend/web/src/app/components/GlobalModalIsolator.tsx
+++ b/invokeai/frontend/web/src/app/components/GlobalModalIsolator.tsx
@@ -7,10 +7,12 @@ import { SaveCanvasProjectDialog } from 'features/controlLayers/components/SaveC
import { CanvasManagerProviderGate } from 'features/controlLayers/contexts/CanvasManagerProviderGate';
import { CropImageModal } from 'features/cropper/components/CropImageModal';
import { DeleteImageModal } from 'features/deleteImageModal/components/DeleteImageModal';
+import { DeleteVideoModal } from 'features/deleteVideoModal/components/DeleteVideoModal';
import { FullscreenDropzone } from 'features/dnd/FullscreenDropzone';
import { DynamicPromptsModal } from 'features/dynamicPrompts/components/DynamicPromptsPreviewModal';
import DeleteBoardModal from 'features/gallery/components/Boards/DeleteBoardModal';
import { ImageContextMenu } from 'features/gallery/components/ContextMenu/ImageContextMenu';
+import { VideoContextMenu } from 'features/gallery/components/ContextMenu/VideoContextMenu';
import { WorkflowLibraryModal } from 'features/nodes/components/sidePanel/workflow/WorkflowLibrary/WorkflowLibraryModal';
import { CancelAllExceptCurrentQueueItemConfirmationAlertDialog } from 'features/queue/components/CancelAllExceptCurrentQueueItemConfirmationAlertDialog';
import { ClearQueueConfirmationsAlertDialog } from 'features/queue/components/ClearQueueConfirmationAlertDialog';
@@ -34,6 +36,7 @@ export const GlobalModalIsolator = memo(() => {
return (
<>
+
@@ -49,6 +52,7 @@ export const GlobalModalIsolator = memo(() => {
+
diff --git a/invokeai/frontend/web/src/app/store/enhancers/reduxRemember/driver.ts b/invokeai/frontend/web/src/app/store/enhancers/reduxRemember/driver.ts
index fdb25b37d2c..217718cb848 100644
--- a/invokeai/frontend/web/src/app/store/enhancers/reduxRemember/driver.ts
+++ b/invokeai/frontend/web/src/app/store/enhancers/reduxRemember/driver.ts
@@ -1,10 +1,12 @@
import { logger } from 'app/logging/logger';
import { StorageError } from 'app/store/enhancers/reduxRemember/errors';
+import { $store } from 'app/store/nanostores/store';
+import { captureAuthGeneration } from 'features/auth/store/authTokenRefresh';
import type { UseStore } from 'idb-keyval';
import { createStore as idbCreateStore, del as idbDel, get as idbGet } from 'idb-keyval';
import type { Driver } from 'redux-remember';
import { serializeError } from 'serialize-error';
-import { buildV1Url, getBaseUrl } from 'services/api';
+import { acceptRefreshedToken, buildV1Url, getBaseUrl } from 'services/api';
import type { JsonObject } from 'type-fest';
const log = logger('system');
@@ -68,17 +70,32 @@ const getIdbKey = (key: string) => {
return `${IDB_STORAGE_PREFIX}${key}`;
};
-// Helper to get auth headers for client_state requests
-const getAuthHeaders = (): Record => {
- const headers: Record = {};
- // Safe access to localStorage (not available in Node.js test environment)
+// Safe access to localStorage (not available in Node.js test environment)
+const getAuthToken = (): string | null => {
if (typeof window !== 'undefined' && window.localStorage) {
- const token = localStorage.getItem('auth_token');
- if (token) {
- headers['Authorization'] = `Bearer ${token}`;
- }
+ return localStorage.getItem('auth_token');
+ }
+ return null;
+};
+
+// Helper to get auth headers for client_state requests
+const getAuthHeaders = (token: string | null): Record => {
+ return token ? { Authorization: `Bearer ${token}` } : {};
+};
+
+// The sliding-window middleware mints X-Refreshed-Token on these POSTs like any other
+// mutation, but this driver bypasses RTK Query (raw fetch), so dynamicBaseQuery's
+// acceptance path never sees the header. Without committing it here, a session whose
+// only server traffic is client-state persistence (canvas/settings tweaks) would
+// never slide its expiry and would hard-expire mid-activity. Fire-and-forget: the
+// cookie sync + commit must not block or fail persistence.
+const commitRefreshedToken = (res: Response, requestToken: string | null, requestGeneration: number): void => {
+ const refreshedToken = res.headers.get('X-Refreshed-Token');
+ const dispatch = $store.get()?.dispatch;
+ if (!refreshedToken || !requestToken || !dispatch) {
+ return;
}
- return headers;
+ void acceptRefreshedToken(refreshedToken, requestToken, requestGeneration, dispatch);
};
const getItem = async (key: string) => {
@@ -86,7 +103,7 @@ const getItem = async (key: string) => {
const url = getUrl('get_by_key', key);
const res = await fetch(url, {
method: 'GET',
- headers: getAuthHeaders(),
+ headers: getAuthHeaders(getAuthToken()),
});
if (!res.ok) {
throw new Error(`Response status: ${res.status}`);
@@ -146,14 +163,17 @@ const setItem = async (key: string, value: string) => {
}
log.trace({ key, last: lastPersistedState.get(key), next: value }, `Persisting state for ${key}`);
const url = getUrl('set_by_key', key);
+ const requestToken = getAuthToken();
+ const requestGeneration = requestToken !== null ? captureAuthGeneration() : 0;
const res = await fetch(url, {
method: 'POST',
body: value,
- headers: getAuthHeaders(),
+ headers: getAuthHeaders(requestToken),
});
if (!res.ok) {
throw new Error(`Response status: ${res.status}`);
}
+ commitRefreshedToken(res, requestToken, requestGeneration);
const resultValue = await res.json();
lastPersistedState.set(key, resultValue);
return resultValue;
@@ -178,13 +198,16 @@ export const clearStorage = async () => {
try {
persistRefCount++;
const url = getUrl('delete');
+ const requestToken = getAuthToken();
+ const requestGeneration = requestToken !== null ? captureAuthGeneration() : 0;
const res = await fetch(url, {
method: 'POST',
- headers: getAuthHeaders(),
+ headers: getAuthHeaders(requestToken),
});
if (!res.ok) {
throw new Error(`Response status: ${res.status}`);
}
+ commitRefreshedToken(res, requestToken, requestGeneration);
} catch {
log.error('Failed to reset client state');
} finally {
diff --git a/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/appStarted.ts b/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/appStarted.ts
index b1d60edc2dc..a5503d3286b 100644
--- a/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/appStarted.ts
+++ b/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/appStarted.ts
@@ -5,7 +5,7 @@ import { setInfillMethod } from 'features/controlLayers/store/paramsSlice';
import { selectLastSelectedItem } from 'features/gallery/store/gallerySelectors';
import { imageSelected } from 'features/gallery/store/gallerySlice';
import { appInfoApi } from 'services/api/endpoints/appInfo';
-import { imagesApi } from 'services/api/endpoints/images';
+import { galleryApi } from 'services/api/endpoints/gallery';
export const appStarted = createAction('app/appStarted');
@@ -29,22 +29,27 @@ export const addAppStartedListener = (startAppListening: AppStartListening) => {
})
.catch(noop);
- // ensure an image is selected when we load the first board.
+ // Ensure a gallery item is selected when we load the first board. The grid is fed by the
+ // polymorphic `getGalleryItemNames` endpoint (image + video names interleaved by date),
+ // so that's what we wait on — the older `getImageNames` is no longer dispatched and would
+ // time out forever.
+ //
// The effect must be async and await take() so that RTK keeps the listener's AbortController
// alive until the query resolves; a synchronous effect causes the controller to be aborted
// immediately after the effect returns, before any network response arrives.
- const firstImageLoad = await take(imagesApi.endpoints.getImageNames.matchFulfilled, 5000);
- if (firstImageLoad === null) {
+ const firstLoad = await take(galleryApi.endpoints.getGalleryItemNames.matchFulfilled, 5000);
+ if (firstLoad === null) {
// timeout or cancelled
return;
}
- const [{ payload }] = firstImageLoad;
- const selectedImage = selectLastSelectedItem(getState());
- if (selectedImage) {
+ const [{ payload }] = firstLoad;
+ const selectedItem = selectLastSelectedItem(getState());
+ if (selectedItem) {
return;
}
- if (payload.image_names[0]) {
- dispatch(imageSelected(payload.image_names[0]));
+ const firstItem = payload.items[0];
+ if (firstItem) {
+ dispatch(imageSelected(firstItem.name));
}
},
});
diff --git a/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/boardAndImagesDeleted.test.ts b/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/boardAndImagesDeleted.test.ts
new file mode 100644
index 00000000000..05464eb7a92
--- /dev/null
+++ b/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/boardAndImagesDeleted.test.ts
@@ -0,0 +1,43 @@
+import { describe, expect, it } from 'vitest';
+
+import { getDeletedImagesFromDeleteBoardAction, getDeletedVideosFromDeleteBoardAction } from './boardAndImagesDeleted';
+
+describe('getDeletedImagesFromDeleteBoardAction', () => {
+ it('reads deleted images from a successful response', () => {
+ expect(getDeletedImagesFromDeleteBoardAction({ deleted_images: ['a.png'] })).toEqual(['a.png']);
+ });
+
+ it('reads deleted images from a partial-failure response', () => {
+ expect(
+ getDeletedImagesFromDeleteBoardAction({
+ status: 500,
+ data: { detail: { deleted_images: ['a.png'], deleted_videos: [], board_deleted: false } },
+ })
+ ).toEqual(['a.png']);
+ });
+
+ it('rejects malformed partial-failure data', () => {
+ expect(getDeletedImagesFromDeleteBoardAction({ data: { detail: { deleted_images: [123] } } })).toEqual([]);
+ expect(getDeletedImagesFromDeleteBoardAction(undefined)).toEqual([]);
+ });
+});
+
+describe('getDeletedVideosFromDeleteBoardAction', () => {
+ it('reads deleted videos from a successful response', () => {
+ expect(getDeletedVideosFromDeleteBoardAction({ deleted_videos: ['a.mp4'] })).toEqual(['a.mp4']);
+ });
+
+ it('reads deleted videos from a partial-failure response', () => {
+ expect(
+ getDeletedVideosFromDeleteBoardAction({
+ status: 500,
+ data: { detail: { deleted_images: [], deleted_videos: ['a.mp4'], board_deleted: false } },
+ })
+ ).toEqual(['a.mp4']);
+ });
+
+ it('rejects malformed partial-failure data', () => {
+ expect(getDeletedVideosFromDeleteBoardAction({ data: { detail: { deleted_videos: [123] } } })).toEqual([]);
+ expect(getDeletedVideosFromDeleteBoardAction(undefined)).toEqual([]);
+ });
+});
diff --git a/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/boardAndImagesDeleted.ts b/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/boardAndImagesDeleted.ts
index d185a03f220..a4b5b92ca0b 100644
--- a/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/boardAndImagesDeleted.ts
+++ b/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/boardAndImagesDeleted.ts
@@ -1,17 +1,63 @@
+import { isAnyOf } from '@reduxjs/toolkit';
import type { AppStartListening } from 'app/store/store';
import { selectRefImagesSlice } from 'features/controlLayers/store/refImagesSlice';
import { selectCanvasSlice } from 'features/controlLayers/store/selectors';
import { getImageUsage } from 'features/deleteImageModal/store/state';
+import { clearNodesVideoFields } from 'features/deleteVideoModal/store/state';
import { nodeEditorReset } from 'features/nodes/store/nodesSlice';
import { selectNodesSlice } from 'features/nodes/store/selectors';
import { selectUpscaleSlice } from 'features/parameters/store/upscaleSlice';
+import { api } from 'services/api';
import { imagesApi } from 'services/api/endpoints/images';
+import { videosApi } from 'services/api/endpoints/videos';
+import {
+ getTagsToInvalidateForImageMutation,
+ getTagsToInvalidateForVideoMutation,
+} from 'services/api/util/tagInvalidation';
+
+const getDeletedNamesFromDeleteBoardAction = (payload: unknown, key: 'deleted_images' | 'deleted_videos'): string[] => {
+ if (!payload || typeof payload !== 'object') {
+ return [];
+ }
+ const response = payload as Record & { data?: unknown };
+ let deletedNames = response[key];
+ if (deletedNames === undefined && response.data && typeof response.data === 'object') {
+ const detail = (response.data as { detail?: unknown }).detail;
+ if (detail && typeof detail === 'object') {
+ deletedNames = (detail as Record)[key];
+ }
+ }
+ return Array.isArray(deletedNames) && deletedNames.every((name) => typeof name === 'string') ? deletedNames : [];
+};
+
+export const getDeletedImagesFromDeleteBoardAction = (payload: unknown): string[] =>
+ getDeletedNamesFromDeleteBoardAction(payload, 'deleted_images');
+
+export const getDeletedVideosFromDeleteBoardAction = (payload: unknown): string[] =>
+ getDeletedNamesFromDeleteBoardAction(payload, 'deleted_videos');
export const addDeleteBoardAndImagesFulfilledListener = (startAppListening: AppStartListening) => {
startAppListening({
- matcher: imagesApi.endpoints.deleteBoardAndImages.matchFulfilled,
+ matcher: isAnyOf(
+ imagesApi.endpoints.deleteBoardAndImages.matchFulfilled,
+ imagesApi.endpoints.deleteBoardAndImages.matchRejected
+ ),
effect: (action, { dispatch, getState }) => {
- const { deleted_images } = action.payload;
+ const deletedImages = getDeletedImagesFromDeleteBoardAction(action.payload);
+ const deletedVideos = getDeletedVideosFromDeleteBoardAction(action.payload);
+
+ // On a *partial* board deletion the mutation rejects, so its invalidatesTags run
+ // with no result and only the static list tags fire — the per-item caches
+ // (Image/ImageMetadata/ImageWorkflow, Video/...) of the confirmed-deleted media
+ // would otherwise stay readable. We already parsed the confirmed names out of the
+ // 500's detail above, so invalidate them here (harmlessly redundant on success).
+ const itemTags = [
+ ...getTagsToInvalidateForImageMutation(deletedImages),
+ ...getTagsToInvalidateForVideoMutation(deletedVideos),
+ ];
+ if (itemTags.length > 0) {
+ dispatch(api.util.invalidateTags(itemTags));
+ }
// Remove all deleted images from the UI
@@ -23,7 +69,7 @@ export const addDeleteBoardAndImagesFulfilledListener = (startAppListening: AppS
const upscale = selectUpscaleSlice(state);
const refImages = selectRefImagesSlice(state);
- deleted_images.forEach((image_name) => {
+ deletedImages.forEach((image_name) => {
const imageUsage = getImageUsage(nodes, canvas, upscale, refImages, image_name);
if (imageUsage.isNodesImage && !wasNodeEditorReset) {
@@ -31,6 +77,26 @@ export const addDeleteBoardAndImagesFulfilledListener = (startAppListening: AppS
wasNodeEditorReset = true;
}
});
+
+ // Clear workflow-node VideoField inputs that referenced a cascade-deleted video —
+ // same cleanup the direct video-delete flow performs. Skipped if the image sweep
+ // already reset the node editor.
+ if (!wasNodeEditorReset) {
+ deletedVideos.forEach((video_name) => {
+ clearNodesVideoFields(getState(), dispatch, video_name);
+ });
+ }
+ },
+ });
+
+ // "Delete All Uncategorized Images/Videos" deletes videos through this dedicated
+ // endpoint — its confirmed deletions need the same VideoField cleanup.
+ startAppListening({
+ matcher: videosApi.endpoints.deleteUncategorizedVideos.matchFulfilled,
+ effect: (action, { dispatch, getState }) => {
+ action.payload.deleted_videos.forEach((video_name) => {
+ clearNodesVideoFields(getState(), dispatch, video_name);
+ });
},
});
};
diff --git a/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/boardIdSelected.ts b/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/boardIdSelected.ts
index 9fd777fb29b..e37da159527 100644
--- a/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/boardIdSelected.ts
+++ b/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/boardIdSelected.ts
@@ -2,13 +2,13 @@ import { isAnyOf } from '@reduxjs/toolkit';
import type { AppStartListening } from 'app/store/store';
import { selectGetImageNamesQueryArgs, selectSelectedBoardId } from 'features/gallery/store/gallerySelectors';
import { boardIdSelected, galleryViewChanged, imageSelected } from 'features/gallery/store/gallerySlice';
-import { imagesApi } from 'services/api/endpoints/images';
+import { galleryApi } from 'services/api/endpoints/gallery';
export const addBoardIdSelectedListener = (startAppListening: AppStartListening) => {
startAppListening({
matcher: isAnyOf(boardIdSelected, galleryViewChanged),
effect: async (action, { getState, dispatch, condition, cancelActiveListeners }) => {
- // Cancel any in-progress instances of this listener, we don't want to select an image from a previous board
+ // Cancel any in-progress instances of this listener, we don't want to select an item from a previous board
cancelActiveListeners();
if (boardIdSelected.match(action) && action.payload.select) {
@@ -20,11 +20,14 @@ export const addBoardIdSelectedListener = (startAppListening: AppStartListening)
const board_id = selectSelectedBoardId(state);
+ // The grid is now backed by the polymorphic getGalleryItemNames endpoint (the legacy
+ // getImageNames query is no longer dispatched), so the auto-select probe must read its
+ // cache or it will time out and clear the user's selection on every board switch.
const queryArgs = { ...selectGetImageNamesQueryArgs(state), board_id };
- // wait until the board has some images - maybe it already has some from a previous fetch
+ // wait until the board has some items - maybe it already has some from a previous fetch
// must use getState() to ensure we do not have stale state
const isSuccess = await condition(
- () => imagesApi.endpoints.getImageNames.select(queryArgs)(getState()).isSuccess,
+ () => galleryApi.endpoints.getGalleryItemNames.select(queryArgs)(getState()).isSuccess,
5000
);
@@ -33,12 +36,12 @@ export const addBoardIdSelectedListener = (startAppListening: AppStartListening)
return;
}
- // the board was just changed - we can select the first image
- const imageNames = imagesApi.endpoints.getImageNames.select(queryArgs)(getState()).data?.image_names;
+ // the board was just changed - we can select the first gallery item (image or video)
+ const items = galleryApi.endpoints.getGalleryItemNames.select(queryArgs)(getState()).data?.items;
- const imageToSelect = imageNames && imageNames.length > 0 ? imageNames[0] : null;
+ const itemToSelect = items && items.length > 0 ? (items[0]?.name ?? null) : null;
- dispatch(imageSelected(imageToSelect ?? null));
+ dispatch(imageSelected(itemToSelect));
},
});
};
diff --git a/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/modelSelected.ts b/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/modelSelected.ts
index 7fc992f8364..34df519b69c 100644
--- a/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/modelSelected.ts
+++ b/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/modelSelected.ts
@@ -17,6 +17,9 @@ import {
setZImageScheduler,
syncedToOptimalDimension,
vaeSelected,
+ wanComponentSourceSelected,
+ wanT5EncoderModelSelected,
+ wanVaeModelSelected,
zImageQwen3EncoderModelSelected,
zImageQwen3SourceModelSelected,
zImageVaeModelSelected,
@@ -36,6 +39,7 @@ import {
isAspectRatioID,
isFlux2ReferenceImageConfig,
isQwenImageReferenceImageConfig,
+ isWanReferenceImageConfig,
} from 'features/controlLayers/store/types';
import {
initialFlux2ReferenceImage,
@@ -43,6 +47,7 @@ import {
initialFLUXRedux,
initialIPAdapter,
initialQwenImageReferenceImage,
+ initialWanReferenceImage,
} from 'features/controlLayers/store/util';
import { SUPPORTS_REF_IMAGES_BASE_MODELS } from 'features/modelManagerV2/models';
import { zModelIdentifierField } from 'features/nodes/types/common';
@@ -61,6 +66,9 @@ import {
selectQwenImageVAEModels,
selectQwenVLEncoderModels,
selectRegionalRefImageModels,
+ selectWanDiffusersModels,
+ selectWanT5EncoderModels,
+ selectWanVAEModels,
selectZImageDiffusersModels,
} from 'services/api/hooks/modelsByType';
import type { FLUXKontextModelConfig, FLUXReduxModelConfig, IPAdapterModelConfig } from 'services/api/types';
@@ -315,6 +323,29 @@ export const addModelSelectedListener = (startAppListening: AppStartListening) =
}
}
+ // handle Wan 2.2 component source / standalone VAE / standalone T5 encoder -
+ // clear when switching away. (Auto-default happens unconditionally outside
+ // this block so it fires when switching between Wan variants too.)
+ const {
+ wanComponentSource: wanComponentSourceOnLeave,
+ wanVaeModel: wanVaeModelOnLeave,
+ wanT5EncoderModel: wanT5EncoderModelOnLeave,
+ } = state.params;
+ if (newBase !== 'wan') {
+ if (wanComponentSourceOnLeave) {
+ dispatch(wanComponentSourceSelected(null));
+ modelsUpdatedDisabledOrCleared += 1;
+ }
+ if (wanVaeModelOnLeave) {
+ dispatch(wanVaeModelSelected(null));
+ modelsUpdatedDisabledOrCleared += 1;
+ }
+ if (wanT5EncoderModelOnLeave) {
+ dispatch(wanT5EncoderModelSelected(null));
+ modelsUpdatedDisabledOrCleared += 1;
+ }
+ }
+
if (newModel.base !== 'external' && SUPPORTS_REF_IMAGES_BASE_MODELS.includes(newModel.base)) {
// Handle incompatible reference image models - switch to first compatible model, with some smart logic
// to choose the best available model based on the new main model.
@@ -371,6 +402,22 @@ export const addModelSelectedListener = (startAppListening: AppStartListening) =
continue;
}
+ if (newBase === 'wan') {
+ // Switching TO Wan - convert any non-wan configs to wan_reference_image.
+ // The Wan I2V graph builder consumes the first enabled ref image; T2V /
+ // TI2V variants ignore ref images entirely (matches Qwen-generate behavior).
+ if (!isWanReferenceImageConfig(entity.config)) {
+ dispatch(
+ refImageConfigChanged({
+ id: entity.id,
+ config: { ...initialWanReferenceImage },
+ })
+ );
+ modelsUpdatedDisabledOrCleared += 1;
+ }
+ continue;
+ }
+
if (isFlux2ReferenceImageConfig(entity.config)) {
// Switching AWAY from FLUX.2 - convert flux2_reference_image to the appropriate config type
let newConfig;
@@ -419,6 +466,29 @@ export const addModelSelectedListener = (startAppListening: AppStartListening) =
continue;
}
+ if (isWanReferenceImageConfig(entity.config)) {
+ // Switching AWAY from Wan - convert to the appropriate config type for the new base.
+ let newConfig;
+ if (newGlobalRefImageModel) {
+ const parsedModel = zModelIdentifierField.parse(newGlobalRefImageModel);
+ if (newModel.base === 'flux' && newModel.name.toLowerCase().includes('kontext')) {
+ newConfig = { ...initialFluxKontextReferenceImage, model: parsedModel };
+ } else if (newGlobalRefImageModel.type === 'flux_redux') {
+ newConfig = { ...initialFLUXRedux, model: parsedModel };
+ } else {
+ newConfig = { ...initialIPAdapter, model: parsedModel };
+ if (parsedModel.base === 'flux') {
+ newConfig.clipVisionModel = 'ViT-L';
+ }
+ }
+ } else {
+ newConfig = { ...initialIPAdapter };
+ }
+ dispatch(refImageConfigChanged({ id: entity.id, config: newConfig }));
+ modelsUpdatedDisabledOrCleared += 1;
+ continue;
+ }
+
// Standard handling for non-flux2 configs
const shouldUpdateModel =
(entity.config.model && entity.config.model.base !== newBase) ||
@@ -474,6 +544,54 @@ export const addModelSelectedListener = (startAppListening: AppStartListening) =
}
}
+ // Wan 2.2: auto-default Component Source / standalone VAE / standalone T5 encoder
+ // when the new model is Wan. Runs on every Wan selection (including same-base
+ // switches like Diffusers Wan → GGUF Wan) so the user doesn't have to dig into
+ // Advanced when picking a GGUF main. Only sets fields that are currently empty
+ // and only does it for GGUF mains — Diffusers mains carry everything themselves.
+ if (newBase === 'wan') {
+ const modelConfigsResult = selectModelConfigsQuery(state);
+ const newModelConfig = modelConfigsResult.data
+ ? modelConfigsAdapterSelectors.selectById(modelConfigsResult.data, newModel.key)
+ : null;
+ const isNewModelGGUF = newModelConfig?.type === 'main' && newModelConfig.format === 'gguf_quantized';
+ if (isNewModelGGUF) {
+ const { wanComponentSource, wanVaeModel, wanT5EncoderModel } = state.params;
+ // Match component source by variant family — A14B (t2v_a14b/i2v_a14b) and
+ // TI2V-5B use different VAEs (16-ch vs 48-ch); a mismatched component source
+ // would silently load the wrong VAE and produce broken images. The standalone
+ // VAE / encoder configs don't carry variant info, so those still go first-match.
+ const newVariant =
+ newModelConfig && 'variant' in newModelConfig && typeof newModelConfig.variant === 'string'
+ ? newModelConfig.variant
+ : null;
+ const a14bFamily = newVariant === 't2v_a14b' || newVariant === 'i2v_a14b';
+ if (!wanComponentSource) {
+ const availableWanDiffusers = selectWanDiffusersModels(state);
+ const matchingFamily = availableWanDiffusers.find((m) => {
+ const v = 'variant' in m && typeof m.variant === 'string' ? m.variant : null;
+ return a14bFamily ? v === 't2v_a14b' || v === 'i2v_a14b' : v === newVariant;
+ });
+ const diffusersModel = matchingFamily ?? availableWanDiffusers[0];
+ if (diffusersModel) {
+ dispatch(wanComponentSourceSelected(zModelIdentifierField.parse(diffusersModel)));
+ }
+ }
+ if (!wanVaeModel) {
+ const vae = selectWanVAEModels(state)[0];
+ if (vae) {
+ dispatch(wanVaeModelSelected(zModelIdentifierField.parse(vae)));
+ }
+ }
+ if (!wanT5EncoderModel) {
+ const encoder = selectWanT5EncoderModels(state)[0];
+ if (encoder) {
+ dispatch(wanT5EncoderModelSelected(zModelIdentifierField.parse(encoder)));
+ }
+ }
+ }
+ }
+
// Handle FLUX.2 Klein model changes within the same base (different variants need different encoders)
// Clear the Qwen3 encoder only when switching between different Klein variants
// (e.g., klein_4b needs qwen3_4b, klein_9b needs qwen3_8b)
diff --git a/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/videoUploadFailedDescription.test.ts b/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/videoUploadFailedDescription.test.ts
new file mode 100644
index 00000000000..589ea5e3384
--- /dev/null
+++ b/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/videoUploadFailedDescription.test.ts
@@ -0,0 +1,32 @@
+/**
+ * Regression test for silent video upload failures (PR #9163 review).
+ *
+ * The bug: uploadVideos() aggregated with Promise.allSettled and dropped rejections, and
+ * no uploadVideo.matchRejected listener existed — so a rejected MP4 vanished with no
+ * error feedback, and a partially successful batch gave no indication of which files
+ * failed. The rejected listener now toasts per failed mutation; these tests pin that the
+ * failed file's name reaches the user-visible description.
+ */
+import { describe, expect, it } from 'vitest';
+
+import { getVideoUploadFailedDescription } from './videoUploadFailedDescription';
+
+describe('getVideoUploadFailedDescription', () => {
+ it('names the failed file so mixed-outcome batches are attributable', () => {
+ expect(getVideoUploadFailedDescription('clip.mp4', 'Request failed with status code 500')).toBe(
+ 'clip.mp4: Request failed with status code 500'
+ );
+ });
+
+ it('still names the file when no error message is available', () => {
+ expect(getVideoUploadFailedDescription('clip.mp4', undefined)).toBe('clip.mp4');
+ });
+
+ it('falls back to the error message alone for pasted blobs without a name', () => {
+ expect(getVideoUploadFailedDescription(undefined, 'Network Error')).toBe('Network Error');
+ });
+
+ it('returns undefined when there is nothing to show (toast falls back to its title)', () => {
+ expect(getVideoUploadFailedDescription(undefined, undefined)).toBeUndefined();
+ });
+});
diff --git a/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/videoUploadFailedDescription.ts b/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/videoUploadFailedDescription.ts
new file mode 100644
index 00000000000..d1e1c0cea84
--- /dev/null
+++ b/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/videoUploadFailedDescription.ts
@@ -0,0 +1,18 @@
+/**
+ * Builds the description for the video-upload-failed toast (PR #9163 review).
+ *
+ * Batch video uploads are aggregated with Promise.allSettled, so a partially failed
+ * batch resolves "successfully" at the call site — the per-mutation rejected listener is
+ * where each failure surfaces, and it must name the file so the user can tell which of
+ * their videos disappeared from a mixed-outcome batch.
+ *
+ * Kept in its own module (rather than inline in the listener) so it can be unit tested:
+ * the listener itself needs a live store and is exercised only at runtime.
+ */
+export const getVideoUploadFailedDescription = (
+ fileName: string | undefined,
+ errorMessage: string | undefined
+): string | undefined => {
+ const parts = [fileName, errorMessage].filter(Boolean);
+ return parts.length > 0 ? parts.join(': ') : undefined;
+};
diff --git a/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/videoUploaded.ts b/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/videoUploaded.ts
new file mode 100644
index 00000000000..30ed978e55a
--- /dev/null
+++ b/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/videoUploaded.ts
@@ -0,0 +1,99 @@
+import { logger } from 'app/logging/logger';
+import type { AppStartListening, RootState } from 'app/store/store';
+import { omit } from 'es-toolkit/compat';
+import { selectListBoardsQueryArgs } from 'features/gallery/store/gallerySelectors';
+import { boardIdSelected, galleryViewChanged } from 'features/gallery/store/gallerySlice';
+import { toast } from 'features/toast/toast';
+import { t } from 'i18next';
+import { boardsApi } from 'services/api/endpoints/boards';
+import { videosApi } from 'services/api/endpoints/videos';
+import type { VideoDTO } from 'services/api/types';
+
+import { getVideoUploadFailedDescription } from './videoUploadFailedDescription';
+
+const log = logger('gallery');
+
+/**
+ * Mirrors getUploadedToastDescription in imageUploaded.ts: names the board the video
+ * landed on so the user knows where to find it.
+ */
+const getUploadedToastDescription = (boardId: string, state: RootState) => {
+ if (boardId === 'none') {
+ return t('toast.addedToUncategorized');
+ }
+ const queryArgs = selectListBoardsQueryArgs(state);
+ const { data } = boardsApi.endpoints.listAllBoards.select(queryArgs)(state);
+ const board = data?.find((b) => b.board_id === boardId);
+
+ return t('toast.addedToBoard', { name: board?.board_name ?? boardId });
+};
+
+let lastUploadedToastTimeout: number | null = null;
+
+/**
+ * Success and failure feedback for video uploads, mirroring the image upload listeners.
+ * Because the batch helpers (uploadVideos) aggregate with Promise.allSettled, this
+ * per-mutation listener is the layer where an individual rejected MP4 becomes visible
+ * to the user — including which file failed.
+ */
+export const addVideoUploadedListeners = (startAppListening: AppStartListening) => {
+ startAppListening({
+ matcher: videosApi.endpoints.uploadVideo.matchFulfilled,
+ effect: (action, { dispatch, getState }) => {
+ const videoDTO: VideoDTO = action.payload;
+ const silent = action.meta.arg.originalArgs.silent;
+ const isFirstUploadOfBatch = action.meta.arg.originalArgs.isFirstUploadOfBatch ?? true;
+
+ if (silent || videoDTO.is_intermediate) {
+ // If the video is silent or intermediate, we don't want to show a toast
+ return;
+ }
+
+ const state = getState();
+
+ log.debug({ videoDTO }, 'Video uploaded');
+
+ const boardId = videoDTO.board_id ?? 'none';
+
+ if (lastUploadedToastTimeout !== null) {
+ window.clearTimeout(lastUploadedToastTimeout);
+ }
+ const toastApi = toast({
+ id: 'VIDEO_UPLOADED',
+ title: t('toast.videoUploaded'),
+ description: getUploadedToastDescription(boardId, state),
+ status: 'success',
+ duration: null, // we will close the toast manually
+ });
+ lastUploadedToastTimeout = window.setTimeout(() => {
+ toastApi.close();
+ }, 3000);
+
+ // Only navigate the gallery on the first upload of a batch — see the matching
+ // comment in imageUploaded.ts for the board-hijacking failure mode this avoids.
+ if (isFirstUploadOfBatch) {
+ dispatch(boardIdSelected({ boardId }));
+ dispatch(galleryViewChanged('assets'));
+ }
+ },
+ });
+
+ startAppListening({
+ matcher: videosApi.endpoints.uploadVideo.matchRejected,
+ effect: (action) => {
+ const fileName = action.meta.arg.originalArgs.file.name;
+ const sanitizedData = {
+ arg: {
+ ...omit(action.meta.arg.originalArgs, ['file']),
+ file: ``,
+ },
+ };
+ log.error({ ...sanitizedData }, 'Video upload failed');
+ toast({
+ title: t('toast.videoUploadFailed'),
+ description: getVideoUploadFailedDescription(fileName, action.error.message),
+ status: 'error',
+ });
+ },
+ });
+};
diff --git a/invokeai/frontend/web/src/app/store/store.ts b/invokeai/frontend/web/src/app/store/store.ts
index 854eaba0f16..87ec489eeac 100644
--- a/invokeai/frontend/web/src/app/store/store.ts
+++ b/invokeai/frontend/web/src/app/store/store.ts
@@ -63,6 +63,7 @@ import { stateSanitizer } from './middleware/devtools/stateSanitizer';
import { addArchivedOrDeletedBoardListener } from './middleware/listenerMiddleware/listeners/addArchivedOrDeletedBoardListener';
import { addPBRFilterListener } from './middleware/listenerMiddleware/listeners/addPBRFilterListener';
import { addImageUploadedFulfilledListener } from './middleware/listenerMiddleware/listeners/imageUploaded';
+import { addVideoUploadedListeners } from './middleware/listenerMiddleware/listeners/videoUploaded';
const listenerMiddleware = createListenerMiddleware();
@@ -272,6 +273,7 @@ startAppListening({
});
addImageUploadedFulfilledListener(startAppListening);
+addVideoUploadedListeners(startAppListening);
// Image deleted
addDeleteBoardAndImagesFulfilledListener(startAppListening);
diff --git a/invokeai/frontend/web/src/common/hooks/useDownloadImage.ts b/invokeai/frontend/web/src/common/hooks/useDownloadImage.ts
index 33b90e1d7fe..17d5cbc983e 100644
--- a/invokeai/frontend/web/src/common/hooks/useDownloadImage.ts
+++ b/invokeai/frontend/web/src/common/hooks/useDownloadImage.ts
@@ -8,7 +8,13 @@ export const useDownloadItem = () => {
const downloadItem = useCallback(
async (item_url: string, item_id: string) => {
try {
- const blob = await fetch(item_url).then((resp) => resp.blob());
+ const resp = await fetch(item_url);
+ if (!resp.ok) {
+ // Without this check an error response body (e.g. a 401 after the media cookie
+ // expired) would be saved as the media file itself.
+ throw new Error(`Server returned ${resp.status}`);
+ }
+ const blob = await resp.blob();
if (!blob) {
throw new Error('Unable to create Blob');
}
diff --git a/invokeai/frontend/web/src/common/hooks/useGalleryItemDTO.tsx b/invokeai/frontend/web/src/common/hooks/useGalleryItemDTO.tsx
new file mode 100644
index 00000000000..05dfdf056bb
--- /dev/null
+++ b/invokeai/frontend/web/src/common/hooks/useGalleryItemDTO.tsx
@@ -0,0 +1,32 @@
+import { isVideoName } from 'features/gallery/store/types';
+import { useImageDTO } from 'services/api/endpoints/images';
+import { useVideoDTO } from 'services/api/endpoints/videos';
+import type { ImageDTO, VideoDTO } from 'services/api/types';
+
+/**
+ * Resolves either an ImageDTO or a VideoDTO based on a polymorphic name. The kind is derived
+ * from the filename extension — the backend names images `.png` and videos `.mp4`,
+ * so we can dispatch without an extra fetch.
+ *
+ * Both underlying RTK Query hooks are always called (React rule-of-hooks), but only the relevant
+ * one is given a real name; the other receives `null` and short-circuits via `skipToken`.
+ */
+/** @knipignore Re-exported for callers that destructure the hook return into named locals. */
+export type GalleryItemDTO = { kind: 'image'; dto: ImageDTO } | { kind: 'video'; dto: VideoDTO };
+
+export const useGalleryItemDTO = (name: string | null | undefined): GalleryItemDTO | null => {
+ const isVideo = name ? isVideoName(name) : false;
+ const imageName = name && !isVideo ? name : null;
+ const videoName = name && isVideo ? name : null;
+
+ const imageDTO = useImageDTO(imageName);
+ const videoDTO = useVideoDTO(videoName);
+
+ if (!name) {
+ return null;
+ }
+ if (isVideo) {
+ return videoDTO ? { kind: 'video', dto: videoDTO } : null;
+ }
+ return imageDTO ? { kind: 'image', dto: imageDTO } : null;
+};
diff --git a/invokeai/frontend/web/src/common/hooks/useGlobalHotkeys.ts b/invokeai/frontend/web/src/common/hooks/useGlobalHotkeys.ts
index 070a4866a50..47c9256a811 100644
--- a/invokeai/frontend/web/src/common/hooks/useGlobalHotkeys.ts
+++ b/invokeai/frontend/web/src/common/hooks/useGlobalHotkeys.ts
@@ -1,6 +1,8 @@
import { useAppStore } from 'app/store/storeHooks';
import { useDeleteImageModalApi } from 'features/deleteImageModal/store/state';
+import { useDeleteVideoModalApi } from 'features/deleteVideoModal/store/state';
import { selectSelection } from 'features/gallery/store/gallerySelectors';
+import { isVideoName } from 'features/gallery/store/types';
import { useCancelCurrentQueueItem } from 'features/queue/hooks/useCancelCurrentQueueItem';
import { useClearQueue } from 'features/queue/hooks/useClearQueue';
import { useInvoke } from 'features/queue/hooks/useInvoke';
@@ -120,11 +122,12 @@ export const useGlobalHotkeys = () => {
});
const deleteImageModalApi = useDeleteImageModalApi();
+ const deleteVideoModalApi = useDeleteVideoModalApi();
useRegisteredHotkeys({
id: 'deleteSelection',
category: 'gallery',
- callback: () => {
+ callback: async () => {
const focusedRegion = getFocusedRegion();
if (focusedRegion !== 'gallery' && focusedRegion !== 'viewer') {
return;
@@ -133,9 +136,27 @@ export const useGlobalHotkeys = () => {
if (!selection.length) {
return;
}
- deleteImageModalApi.delete(selection);
+ // The gallery selection is polymorphic — route each kind to its own delete flow.
+ // Sequential so the two confirmation dialogs don't stack; canceling one flow
+ // (rejected promise) still lets the other run.
+ const imageNames = selection.filter((name) => !isVideoName(name));
+ const videoNames = selection.filter(isVideoName);
+ if (imageNames.length) {
+ try {
+ await deleteImageModalApi.delete(imageNames);
+ } catch {
+ // User canceled the image deletion — the video flow is independent.
+ }
+ }
+ if (videoNames.length) {
+ try {
+ await deleteVideoModalApi.delete(videoNames);
+ } catch {
+ // User canceled the video deletion.
+ }
+ }
},
- dependencies: [getState, deleteImageModalApi],
+ dependencies: [getState, deleteImageModalApi, deleteVideoModalApi],
});
useRegisteredHotkeys({
diff --git a/invokeai/frontend/web/src/common/hooks/useImageUploadButton.tsx b/invokeai/frontend/web/src/common/hooks/useImageUploadButton.tsx
index fc173de979f..cc119103967 100644
--- a/invokeai/frontend/web/src/common/hooks/useImageUploadButton.tsx
+++ b/invokeai/frontend/web/src/common/hooks/useImageUploadButton.tsx
@@ -2,42 +2,44 @@ import type { ButtonProps, IconButtonProps, SystemStyleObject } from '@invoke-ai
import { Button, IconButton } from '@invoke-ai/ui-library';
import { logger } from 'app/logging/logger';
import { useAppSelector } from 'app/store/storeHooks';
+import { trackAsyncTask } from 'common/util/trackAsyncTask';
+import { getUploadDropzoneAccept, partitionUploadFiles } from 'common/util/uploadMediaAccept';
import { selectAutoAddBoardId } from 'features/gallery/store/gallerySelectors';
import { toast } from 'features/toast/toast';
-import { memo, useCallback } from 'react';
-import type { Accept, FileRejection } from 'react-dropzone';
+import { memo, useCallback, useRef, useState } from 'react';
+import type { FileRejection } from 'react-dropzone';
import { useDropzone } from 'react-dropzone';
import { useTranslation } from 'react-i18next';
import { PiUploadBold } from 'react-icons/pi';
import { uploadImages, useUploadImageMutation } from 'services/api/endpoints/images';
-import type { ImageDTO } from 'services/api/types';
+import { uploadVideos, useUploadVideoMutation } from 'services/api/endpoints/videos';
+import type { ImageDTO, VideoDTO } from 'services/api/types';
import { assert } from 'tsafe';
import type { SetOptional } from 'type-fest';
-const addUpperCaseReducer = (acc: string[], ext: string) => {
- acc.push(ext);
- acc.push(ext.toUpperCase());
- return acc;
-};
-
-export const dropzoneAccept: Accept = {
- 'image/png': ['.png'].reduce(addUpperCaseReducer, [] as string[]),
- 'image/jpeg': ['.jpg', '.jpeg', '.png'].reduce(addUpperCaseReducer, [] as string[]),
- 'image/webp': ['.webp'].reduce(addUpperCaseReducer, [] as string[]),
-};
-
type UseImageUploadButtonArgs =
| {
isDisabled?: boolean;
allowMultiple: false;
+ /**
+ * Opt-in for video uploads. The backend only stores videos in the gallery, so a
+ * consumer that wants an image (board covers, ref images, etc.) must NOT set this —
+ * otherwise a selected MP4 would be uploaded to the gallery and its DTO silently
+ * discarded while the requested image action goes nowhere.
+ */
+ allowVideos?: boolean;
onUpload?: (imageDTO: ImageDTO) => void;
+ /** Called when a single dropped file is a video (parallel to onUpload for images). */
+ onUploadVideo?: (videoDTO: VideoDTO) => void;
onUploadStarted?: (files: File) => void;
onError?: (error: unknown) => void;
}
| {
isDisabled?: boolean;
allowMultiple: true;
+ allowVideos?: boolean;
onUpload?: (imageDTOs: ImageDTO[]) => void;
+ onUploadVideo?: (videoDTOs: VideoDTO[]) => void;
onUploadStarted?: (files: File[]) => void;
onError?: (error: unknown) => void;
};
@@ -65,17 +67,39 @@ const log = logger('gallery');
*/
export const useImageUploadButton = ({
onUpload,
+ onUploadVideo,
isDisabled,
allowMultiple,
+ allowVideos = false,
onUploadStarted,
onError,
}: UseImageUploadButtonArgs) => {
const autoAddBoardId = useAppSelector(selectAutoAddBoardId);
- const [uploadImage, request] = useUploadImageMutation();
+ const [uploadImage, imageRequest] = useUploadImageMutation();
+ const [uploadVideo, videoRequest] = useUploadVideoMutation();
+ const [isBatchUploading, setIsBatchUploading] = useState(false);
+ const pendingBatchUploads = useRef(0);
const { t } = useTranslation();
+ const onBatchLoadingChanged = useCallback((isLoading: boolean) => {
+ pendingBatchUploads.current += isLoading ? 1 : -1;
+ setIsBatchUploading(pendingBatchUploads.current > 0);
+ }, []);
const onDropAccepted = useCallback(
async (files: File[]) => {
+ // The accept map already excludes videos for image-only consumers, but the file
+ // dialog can bypass it (e.g. "All Files"), so partition again at runtime.
+ const { imageFiles, videoFiles, rejectedFiles } = partitionUploadFiles(files, allowVideos);
+ if (rejectedFiles.length > 0) {
+ log.error({ files: rejectedFiles.map((f) => f.name) }, 'Videos are not accepted by this upload field');
+ toast({
+ id: 'UPLOAD_FAILED',
+ title: t('toast.uploadFailed'),
+ description: t('toast.uploadFailedInvalidUploadDesc'),
+ status: 'error',
+ });
+ return;
+ }
try {
if (!allowMultiple) {
if (files.length > 1) {
@@ -90,44 +114,96 @@ export const useImageUploadButton = ({
const file = files[0];
assert(file !== undefined); // should never happen
onUploadStarted?.(file);
- const imageDTO = await uploadImage({
- file,
- image_category: 'user',
- is_intermediate: false,
- board_id: autoAddBoardId === 'none' ? undefined : autoAddBoardId,
- silent: true,
- }).unwrap();
- if (onUpload) {
- onUpload(imageDTO);
- }
- } else {
- onUploadStarted?.(files);
- let imageDTOs: ImageDTO[] = [];
- imageDTOs = await uploadImages(
- files.map((file, i) => ({
+ if (videoFiles.length > 0) {
+ const videoDTO = await uploadVideo({
+ file,
+ video_category: 'user',
+ is_intermediate: false,
+ board_id: autoAddBoardId === 'none' ? undefined : autoAddBoardId,
+ silent: true,
+ }).unwrap();
+ // Cast: TS narrows onUploadVideo by the allowMultiple discriminator above.
+ (onUploadVideo as ((dto: VideoDTO) => void) | undefined)?.(videoDTO);
+ } else {
+ const imageDTO = await uploadImage({
file,
image_category: 'user',
is_intermediate: false,
board_id: autoAddBoardId === 'none' ? undefined : autoAddBoardId,
- silent: false,
- isFirstUploadOfBatch: i === 0,
- }))
- );
- if (onUpload) {
- onUpload(imageDTOs);
+ silent: true,
+ }).unwrap();
+ (onUpload as ((dto: ImageDTO) => void) | undefined)?.(imageDTO);
}
+ } else {
+ onUploadStarted?.(files);
+ await trackAsyncTask(async () => {
+ let imageDTOs: ImageDTO[] = [];
+ if (imageFiles.length > 0) {
+ imageDTOs = await uploadImages(
+ imageFiles.map((file, i) => ({
+ file,
+ image_category: 'user',
+ is_intermediate: false,
+ board_id: autoAddBoardId === 'none' ? undefined : autoAddBoardId,
+ silent: false,
+ isFirstUploadOfBatch: i === 0,
+ }))
+ );
+ }
+
+ let videoDTOs: VideoDTO[] = [];
+ if (videoFiles.length > 0) {
+ videoDTOs = await uploadVideos(
+ videoFiles.map((file, i) => ({
+ file,
+ video_category: 'user',
+ is_intermediate: false,
+ board_id: autoAddBoardId === 'none' ? undefined : autoAddBoardId,
+ silent: false,
+ isFirstUploadOfBatch: i === 0,
+ }))
+ );
+ }
+
+ if (imageDTOs.length > 0) {
+ (onUpload as ((dtos: ImageDTO[]) => void) | undefined)?.(imageDTOs);
+ }
+ if (videoDTOs.length > 0) {
+ (onUploadVideo as ((dtos: VideoDTO[]) => void) | undefined)?.(videoDTOs);
+ }
+ }, onBatchLoadingChanged);
}
} catch (error) {
onError?.(error);
+ // Name the media that actually failed — a failed MP4 upload should not claim an
+ // image upload failed. Mixed batches get the media-neutral title.
+ const title =
+ videoFiles.length > 0 && imageFiles.length > 0
+ ? t('toast.uploadFailed')
+ : videoFiles.length > 0
+ ? t('toast.videoUploadFailed')
+ : t('toast.imageUploadFailed');
toast({
id: 'UPLOAD_FAILED',
- title: t('toast.imageUploadFailed'),
+ title,
status: 'error',
});
}
},
- [allowMultiple, onUploadStarted, uploadImage, autoAddBoardId, onUpload, onError, t]
+ [
+ allowMultiple,
+ allowVideos,
+ onUploadStarted,
+ uploadImage,
+ uploadVideo,
+ autoAddBoardId,
+ onUpload,
+ onUploadVideo,
+ onError,
+ onBatchLoadingChanged,
+ t,
+ ]
);
const onDropRejected = useCallback(
@@ -158,7 +234,7 @@ export const useImageUploadButton = ({
getInputProps: getUploadInputProps,
open: openUploader,
} = useDropzone({
- accept: dropzoneAccept,
+ accept: getUploadDropzoneAccept(allowVideos),
onDropAccepted,
onDropRejected,
disabled: isDisabled,
@@ -166,7 +242,11 @@ export const useImageUploadButton = ({
multiple: allowMultiple,
});
- return { getUploadButtonProps, getUploadInputProps, openUploader, request };
+ // Uploads run through separate image and video mutations; loading state must cover both
+ // or an in-flight MP4 upload would show idle controls and allow double submission.
+ const isUploading = imageRequest.isLoading || videoRequest.isLoading || isBatchUploading;
+
+ return { getUploadButtonProps, getUploadInputProps, openUploader, isUploading };
};
const sx = {
@@ -197,7 +277,7 @@ export const UploadImageIconButton = memo(
sx={sx}
data-error={isError}
icon={ }
- isLoading={uploadApi.request.isLoading}
+ isLoading={uploadApi.isUploading}
{...rest}
{...uploadApi.getUploadButtonProps()}
/>
@@ -225,7 +305,7 @@ const UploadImageButton = memo((props: UploadImageButtonProps) => {
sx={sx}
data-error={isError}
rightIcon={ }
- isLoading={uploadApi.request.isLoading}
+ isLoading={uploadApi.isUploading}
{...rest}
{...uploadApi.getUploadButtonProps()}
>
@@ -256,7 +336,7 @@ export const UploadMultipleImageButton = ({
sx={sx}
data-error={isError}
icon={ }
- isLoading={uploadApi.request.isLoading}
+ isLoading={uploadApi.isUploading}
{...rest}
{...uploadApi.getUploadButtonProps()}
/>
diff --git a/invokeai/frontend/web/src/common/util/trackAsyncTask.test.ts b/invokeai/frontend/web/src/common/util/trackAsyncTask.test.ts
new file mode 100644
index 00000000000..2b09bf4e005
--- /dev/null
+++ b/invokeai/frontend/web/src/common/util/trackAsyncTask.test.ts
@@ -0,0 +1,27 @@
+import { describe, expect, it, vi } from 'vitest';
+
+import { trackAsyncTask } from './trackAsyncTask';
+
+describe('trackAsyncTask', () => {
+ it('reports loading until the tracked task settles', async () => {
+ let resolveTask: (() => void) | undefined;
+ const task = new Promise((resolve) => {
+ resolveTask = resolve;
+ });
+ const onLoadingChanged = vi.fn();
+
+ const tracked = trackAsyncTask(() => task, onLoadingChanged);
+ expect(onLoadingChanged).toHaveBeenCalledWith(true);
+ expect(onLoadingChanged).not.toHaveBeenCalledWith(false);
+
+ resolveTask?.();
+ await tracked;
+ expect(onLoadingChanged).toHaveBeenLastCalledWith(false);
+ });
+
+ it('clears loading when the task rejects', async () => {
+ const onLoadingChanged = vi.fn();
+ await expect(trackAsyncTask(() => Promise.reject(new Error('failed')), onLoadingChanged)).rejects.toThrow('failed');
+ expect(onLoadingChanged.mock.calls).toEqual([[true], [false]]);
+ });
+});
diff --git a/invokeai/frontend/web/src/common/util/trackAsyncTask.ts b/invokeai/frontend/web/src/common/util/trackAsyncTask.ts
new file mode 100644
index 00000000000..090f3375142
--- /dev/null
+++ b/invokeai/frontend/web/src/common/util/trackAsyncTask.ts
@@ -0,0 +1,8 @@
+export const trackAsyncTask = async (task: () => Promise, onLoadingChanged: (isLoading: boolean) => void) => {
+ onLoadingChanged(true);
+ try {
+ return await task();
+ } finally {
+ onLoadingChanged(false);
+ }
+};
diff --git a/invokeai/frontend/web/src/common/util/uploadMediaAccept.test.ts b/invokeai/frontend/web/src/common/util/uploadMediaAccept.test.ts
new file mode 100644
index 00000000000..de07dc833aa
--- /dev/null
+++ b/invokeai/frontend/web/src/common/util/uploadMediaAccept.test.ts
@@ -0,0 +1,102 @@
+/**
+ * Regression tests for client-side upload acceptance (PR #9163 review).
+ *
+ * The bug: the dropzone accept map advertised `.webm`/`.mov` and `isVideoFile` treated
+ * `.webm`/`.mov`/`.mkv` as uploadable videos, but the video upload router accepts MP4 only
+ * (`invokeai/app/api/routers/videos.py`: `ACCEPTED_VIDEO_MIME_PREFIXES = ("video/mp4",)`,
+ * `ACCEPTED_VIDEO_EXTENSIONS = (".mp4",)`). Dropping a WebM or QuickTime file started an
+ * upload the server was guaranteed to reject with 415.
+ *
+ * These tests pin the client lists to the backend contract: if a new container is ever
+ * accepted server-side, update `uploadMediaAccept.ts` and these expectations together.
+ */
+import { describe, expect, it } from 'vitest';
+
+import {
+ ACCEPTED_VIDEO_EXTENSIONS,
+ ACCEPTED_VIDEO_TYPES,
+ getUploadDropzoneAccept,
+ imageAndVideoDropzoneAccept,
+ imageDropzoneAccept,
+ isAcceptedUploadFile,
+ isImageFile,
+ isVideoFile,
+ partitionUploadFiles,
+} from './uploadMediaAccept';
+
+// `isVideoFile` only reads `type` and `name`, so a plain object stands in for a DOM File.
+const fakeFile = (name: string, type: string): File => ({ name, type }) as File;
+
+describe('video acceptance matches the backend (MP4 only)', () => {
+ it('accepts only MIME types and extensions the video upload router supports', () => {
+ expect(ACCEPTED_VIDEO_TYPES).toEqual(['video/mp4']);
+ expect(ACCEPTED_VIDEO_EXTENSIONS).toEqual(['.mp4']);
+ });
+
+ it('advertises only backend-supported video entries in the gallery dropzone accept map', () => {
+ const videoEntries = Object.entries(imageAndVideoDropzoneAccept).filter(([mime]) => mime.startsWith('video/'));
+ expect(videoEntries).toEqual([['video/mp4', ['.mp4', '.MP4']]]);
+ });
+
+ it('does not advertise any video entries for image-only upload fields', () => {
+ expect(Object.keys(imageDropzoneAccept).some((mime) => mime.startsWith('video/'))).toBe(false);
+ });
+});
+
+describe('isVideoFile', () => {
+ it('recognizes MP4 by MIME type and by extension', () => {
+ expect(isVideoFile(fakeFile('clip.mp4', 'video/mp4'))).toBe(true);
+ expect(isVideoFile(fakeFile('CLIP.MP4', ''))).toBe(true);
+ });
+
+ it('rejects video containers the backend does not accept', () => {
+ expect(isVideoFile(fakeFile('clip.webm', 'video/webm'))).toBe(false);
+ expect(isVideoFile(fakeFile('clip.mov', 'video/quicktime'))).toBe(false);
+ expect(isVideoFile(fakeFile('clip.mkv', 'video/x-matroska'))).toBe(false);
+ });
+
+ it('rejects images', () => {
+ expect(isVideoFile(fakeFile('image.png', 'image/png'))).toBe(false);
+ });
+});
+
+describe('isImageFile / isAcceptedUploadFile', () => {
+ it('accepts a file when either the MIME type or the extension is recognized', () => {
+ // Browsers sometimes supply an empty File.type — the extension must suffice on its own.
+ expect(isAcceptedUploadFile(fakeFile('clip.mp4', ''))).toBe(true);
+ expect(isAcceptedUploadFile(fakeFile('image.png', ''))).toBe(true);
+ // And a recognized MIME type must suffice without a matching extension.
+ expect(isAcceptedUploadFile(fakeFile('pasted-blob', 'image/png'))).toBe(true);
+ expect(isAcceptedUploadFile(fakeFile('pasted-blob', 'video/mp4'))).toBe(true);
+ });
+
+ it('rejects unsupported media', () => {
+ expect(isImageFile(fakeFile('vector.svg', 'image/svg+xml'))).toBe(false);
+ expect(isAcceptedUploadFile(fakeFile('clip.webm', 'video/webm'))).toBe(false);
+ expect(isAcceptedUploadFile(fakeFile('notes.txt', 'text/plain'))).toBe(false);
+ });
+});
+
+describe('video uploads are opt-in per consumer', () => {
+ const mp4 = fakeFile('clip.mp4', 'video/mp4');
+ const png = fakeFile('image.png', 'image/png');
+
+ it('image-only consumers do not advertise video MIME types', () => {
+ expect(Object.keys(getUploadDropzoneAccept(false)).some((mime) => mime.startsWith('video/'))).toBe(false);
+ expect(getUploadDropzoneAccept(true)).toBe(imageAndVideoDropzoneAccept);
+ });
+
+ it('rejects an MP4 submitted to an image-only consumer instead of uploading it', () => {
+ const { imageFiles, videoFiles, rejectedFiles } = partitionUploadFiles([png, mp4], false);
+ expect(imageFiles).toEqual([png]);
+ expect(videoFiles).toEqual([]); // nothing to hand to the video uploader
+ expect(rejectedFiles).toEqual([mp4]);
+ });
+
+ it('routes an MP4 to the video batch when the consumer opted in', () => {
+ const { imageFiles, videoFiles, rejectedFiles } = partitionUploadFiles([png, mp4], true);
+ expect(imageFiles).toEqual([png]);
+ expect(videoFiles).toEqual([mp4]);
+ expect(rejectedFiles).toEqual([]);
+ });
+});
diff --git a/invokeai/frontend/web/src/common/util/uploadMediaAccept.ts b/invokeai/frontend/web/src/common/util/uploadMediaAccept.ts
new file mode 100644
index 00000000000..5b186f31b88
--- /dev/null
+++ b/invokeai/frontend/web/src/common/util/uploadMediaAccept.ts
@@ -0,0 +1,92 @@
+import type { Accept } from 'react-dropzone';
+
+/**
+ * Single source of truth for which media files the client accepts for upload.
+ *
+ * The video lists must stay in sync with the video upload router
+ * (`invokeai/app/api/routers/videos.py`: `ACCEPTED_VIDEO_MIME_PREFIXES` /
+ * `ACCEPTED_VIDEO_EXTENSIONS`). The backend accepts MP4 only — it stores uploads under a
+ * `.mp4` name without transcoding — so advertising other containers client-side just
+ * produces a guaranteed 415 after the bytes have been uploaded.
+ */
+const ACCEPTED_IMAGE_TYPES = ['image/png', 'image/jpg', 'image/jpeg', 'image/webp'];
+const ACCEPTED_IMAGE_EXTENSIONS = ['.png', '.jpg', '.jpeg', '.webp'];
+export const ACCEPTED_VIDEO_TYPES = ['video/mp4'];
+export const ACCEPTED_VIDEO_EXTENSIONS = ['.mp4'];
+
+const addUpperCaseReducer = (acc: string[], ext: string) => {
+ acc.push(ext);
+ acc.push(ext.toUpperCase());
+ return acc;
+};
+
+/** react-dropzone accept map for image-only upload fields (board covers, style presets, etc). */
+export const imageDropzoneAccept: Accept = {
+ 'image/png': ['.png'].reduce(addUpperCaseReducer, [] as string[]),
+ 'image/jpeg': ['.jpg', '.jpeg', '.png'].reduce(addUpperCaseReducer, [] as string[]),
+ 'image/webp': ['.webp'].reduce(addUpperCaseReducer, [] as string[]),
+};
+
+/** react-dropzone accept map for the gallery uploader, which also takes videos. */
+export const imageAndVideoDropzoneAccept: Accept = {
+ ...imageDropzoneAccept,
+ 'video/mp4': ['.mp4'].reduce(addUpperCaseReducer, [] as string[]),
+};
+
+/** Returns true when the file is an uploadable video (by MIME or by extension). */
+export const isVideoFile = (file: File): boolean => {
+ if (file.type && ACCEPTED_VIDEO_TYPES.includes(file.type.toLowerCase())) {
+ return true;
+ }
+ const lower = file.name.toLowerCase();
+ return ACCEPTED_VIDEO_EXTENSIONS.some((ext) => lower.endsWith(ext));
+};
+
+/** Returns true when the file is an uploadable image (by MIME or by extension). */
+export const isImageFile = (file: File): boolean => {
+ if (file.type && ACCEPTED_IMAGE_TYPES.includes(file.type.toLowerCase())) {
+ return true;
+ }
+ const lower = file.name.toLowerCase();
+ return ACCEPTED_IMAGE_EXTENSIONS.some((ext) => lower.endsWith(ext));
+};
+
+/**
+ * Returns true when the file is uploadable media of either kind. MIME and extension each
+ * suffice on their own: browsers sometimes supply an empty or generic `File.type` (e.g. for
+ * a pasted or drag-dropped MP4), and the backend upload routes likewise accept either signal.
+ */
+export const isAcceptedUploadFile = (file: File): boolean => isImageFile(file) || isVideoFile(file);
+
+/** The dropzone accept map for an uploader: videos are opt-in, images always accepted. */
+export const getUploadDropzoneAccept = (allowVideos: boolean): Accept =>
+ allowVideos ? imageAndVideoDropzoneAccept : imageDropzoneAccept;
+
+type UploadPartition = {
+ imageFiles: File[];
+ videoFiles: File[];
+ /** Videos submitted to an uploader that did not opt into videos. */
+ rejectedFiles: File[];
+};
+
+/**
+ * Splits dropped files into per-media upload batches. Videos are only uploadable when the
+ * consumer explicitly opted in — an image-only consumer (e.g. a board cover or ref-image
+ * field) must reject an MP4 outright rather than upload it to the gallery and silently
+ * discard the resulting VideoDTO.
+ */
+export const partitionUploadFiles = (files: File[], allowVideos: boolean): UploadPartition => {
+ const imageFiles: File[] = [];
+ const videoFiles: File[] = [];
+ const rejectedFiles: File[] = [];
+ for (const file of files) {
+ if (!isVideoFile(file)) {
+ imageFiles.push(file);
+ } else if (allowVideos) {
+ videoFiles.push(file);
+ } else {
+ rejectedFiles.push(file);
+ }
+ }
+ return { imageFiles, videoFiles, rejectedFiles };
+};
diff --git a/invokeai/frontend/web/src/features/auth/components/UserMenu.tsx b/invokeai/frontend/web/src/features/auth/components/UserMenu.tsx
index d8f598f996b..3fae3879dd7 100644
--- a/invokeai/frontend/web/src/features/auth/components/UserMenu.tsx
+++ b/invokeai/frontend/web/src/features/auth/components/UserMenu.tsx
@@ -1,6 +1,9 @@
import { Badge, Flex, IconButton, Menu, MenuButton, MenuItem, MenuList, Text, Tooltip } from '@invoke-ai/ui-library';
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
+import { pauseMediaCookieRefreshForLogout } from 'features/auth/hooks/useMediaCookieRefresh';
import { logout, selectCurrentUser } from 'features/auth/store/authSlice';
+import { logoutAfterServerConfirmation } from 'features/auth/store/logoutAfterServerConfirmation';
+import { toast } from 'features/toast/toast';
import { memo, useCallback } from 'react';
import { useTranslation } from 'react-i18next';
import { PiGearBold, PiSignOutBold, PiUserBold, PiUsersBold } from 'react-icons/pi';
@@ -15,18 +18,24 @@ export const UserMenu = memo(() => {
const [logoutMutation] = useLogoutMutation();
const handleLogout = useCallback(() => {
- // Call backend logout endpoint
- logoutMutation()
- .unwrap()
- .catch(() => {
- // Ignore errors - we'll log out locally anyway
- })
- .finally(() => {
- // Clear local state regardless of backend response
+ void logoutAfterServerConfirmation(
+ () => logoutMutation().unwrap(),
+ () => {
dispatch(logout());
navigate('/login');
+ },
+ pauseMediaCookieRefreshForLogout
+ ).catch(() => {
+ // Local auth state is deliberately kept when the server can't confirm the logout
+ // (the media cookie may still be live) — but silently doing nothing leaves the
+ // user stuck with a dead button, so say why.
+ toast({
+ status: 'error',
+ title: t('auth.logoutFailed'),
+ description: t('auth.logoutFailedDesc'),
});
- }, [dispatch, navigate, logoutMutation]);
+ });
+ }, [dispatch, navigate, logoutMutation, t]);
const handleProfile = useCallback(() => {
navigate('/profile');
diff --git a/invokeai/frontend/web/src/features/auth/components/UserProfile.tsx b/invokeai/frontend/web/src/features/auth/components/UserProfile.tsx
index 02d25b6de98..028a87c1a9d 100644
--- a/invokeai/frontend/web/src/features/auth/components/UserProfile.tsx
+++ b/invokeai/frontend/web/src/features/auth/components/UserProfile.tsx
@@ -20,7 +20,7 @@ import {
VStack,
} from '@invoke-ai/ui-library';
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
-import { selectAuthToken, selectCurrentUser, setCredentials } from 'features/auth/store/authSlice';
+import { currentUserUpdated, selectCurrentUser } from 'features/auth/store/authSlice';
import { validatePasswordField } from 'features/auth/util/passwordUtils';
import type { ChangeEvent, FormEvent } from 'react';
import { memo, useCallback, useState } from 'react';
@@ -38,7 +38,6 @@ const PASSWORD_GRID_COLUMNS = '180px 1fr';
export const UserProfile = memo(() => {
const { t } = useTranslation();
const currentUser = useAppSelector(selectCurrentUser);
- const currentToken = useAppSelector(selectAuthToken);
const dispatch = useAppDispatch();
const navigate = useNavigate();
@@ -127,20 +126,15 @@ export const UserProfile = memo(() => {
const updatedUser = await updateCurrentUser(updatePayload).unwrap();
// Refresh the stored user info so the header reflects the new display name
- if (currentToken) {
- dispatch(
- setCredentials({
- token: currentToken,
- user: {
- user_id: updatedUser.user_id,
- email: updatedUser.email,
- display_name: updatedUser.display_name ?? null,
- is_admin: updatedUser.is_admin ?? false,
- is_active: updatedUser.is_active ?? true,
- },
- })
- );
- }
+ dispatch(
+ currentUserUpdated({
+ user_id: updatedUser.user_id,
+ email: updatedUser.email,
+ display_name: updatedUser.display_name ?? null,
+ is_admin: updatedUser.is_admin ?? false,
+ is_active: updatedUser.is_active ?? true,
+ })
+ );
// Navigate back after successful save
navigate(-1);
@@ -152,17 +146,7 @@ export const UserProfile = memo(() => {
setErrorMessage(detail);
}
},
- [
- displayName,
- currentPassword,
- newPassword,
- isPasswordChangeValid,
- updateCurrentUser,
- currentToken,
- dispatch,
- navigate,
- t,
- ]
+ [displayName, currentPassword, newPassword, isPasswordChangeValid, updateCurrentUser, dispatch, navigate, t]
);
if (!currentUser) {
diff --git a/invokeai/frontend/web/src/features/auth/hooks/useMediaCookieRefresh.test.ts b/invokeai/frontend/web/src/features/auth/hooks/useMediaCookieRefresh.test.ts
new file mode 100644
index 00000000000..e74bb780de5
--- /dev/null
+++ b/invokeai/frontend/web/src/features/auth/hooks/useMediaCookieRefresh.test.ts
@@ -0,0 +1,22 @@
+import { describe, expect, it, vi } from 'vitest';
+
+import { abortAndWaitForPendingRefreshes } from './useMediaCookieRefresh';
+
+describe('media cookie refresh shutdown', () => {
+ it('aborts a stalled request instead of blocking logout indefinitely', async () => {
+ let settle: (() => void) | undefined;
+ const promise = new Promise((resolve) => {
+ settle = resolve;
+ });
+ const abort = vi.fn(() => settle?.());
+ const pending = new Set([{ promise, abort }]);
+
+ await abortAndWaitForPendingRefreshes(pending);
+
+ expect(abort).toHaveBeenCalledOnce();
+ });
+
+ it('is a no-op when no refresh is pending', async () => {
+ await expect(abortAndWaitForPendingRefreshes(new Set())).resolves.toBeUndefined();
+ });
+});
diff --git a/invokeai/frontend/web/src/features/auth/hooks/useMediaCookieRefresh.ts b/invokeai/frontend/web/src/features/auth/hooks/useMediaCookieRefresh.ts
new file mode 100644
index 00000000000..2bc2ad2c1dc
--- /dev/null
+++ b/invokeai/frontend/web/src/features/auth/hooks/useMediaCookieRefresh.ts
@@ -0,0 +1,151 @@
+import { useAppSelector } from 'app/store/storeHooks';
+import { selectIsAuthenticated } from 'features/auth/store/authSlice';
+import { notifyMediaCookieRefreshed } from 'features/auth/store/mediaCookieRefresh';
+import { useEffect, useRef } from 'react';
+import { useRefreshMediaCookieMutation } from 'services/api/endpoints/auth';
+
+// Bounded backoff for transient failures (network hiccup, server 5xx during app
+// load). Two retries is enough to ride out a restart; anything longer-lived needs a
+// reload anyway.
+const RETRY_DELAYS_MS = [2_000, 10_000];
+
+let pauseRefreshHandler: (() => Promise<() => void>) | null = null;
+type PendingRefresh = { promise: Promise; abort: () => void };
+const pendingRefreshes = new Set();
+
+// True while the self-heal (initial attempt, retries, or a paused retry) is still
+// expected to re-mint the cookie. Media consumers use this to treat a 401-style load
+// error as transient rather than surfacing an error to the user.
+let selfHealPending = false;
+export const isMediaCookieSelfHealPending = () => selfHealPending;
+
+export const abortAndWaitForPendingRefreshes = async (pending: Set) => {
+ for (const refresh of pending) {
+ refresh.abort();
+ }
+ await Promise.all([...pending].map((refresh) => refresh.promise));
+};
+
+export const pauseMediaCookieRefreshForLogout = (): Promise<() => void> =>
+ pauseRefreshHandler?.() ?? Promise.resolve(() => undefined);
+
+/**
+ * Self-heal the media cookie for restored sessions.
+ *
+ * Video playback authenticates via an HttpOnly cookie (media elements can't send
+ * Authorization headers) that is only set at login. A session restored from
+ * localStorage can hold a valid JWT without the cookie — every API call works but
+ * each `` request 401s and the player renders black with 0:00 duration.
+ * Re-issuing the cookie from the Bearer token on app load closes that gap.
+ *
+ * Runs when an authenticated session exists (fresh logins get the cookie from the
+ * login response too — the repeat is a harmless no-op). In single-user mode
+ * `isAuthenticated` is never set, and media routes don't require the cookie anyway.
+ *
+ * Transient failures retry on a short bounded backoff — a network error or 5xx
+ * during app load must not leave an otherwise-valid session unable to load media
+ * until a full reload. A 401 is not retried: the session itself is expired, which
+ * the global 401 handling already deals with, and retrying would loop.
+ */
+export const useMediaCookieRefresh = () => {
+ const isAuthenticated = useAppSelector(selectIsAuthenticated);
+ const [refreshMediaCookie] = useRefreshMediaCookieMutation();
+ const hasSucceeded = useRef(false);
+
+ useEffect(() => {
+ if (!isAuthenticated || hasSucceeded.current) {
+ return;
+ }
+
+ let canceled = false;
+ let paused = false;
+ let timeoutId: ReturnType | undefined;
+ let nextAttemptIndex: number | null = null;
+
+ const attempt = (attemptIndex: number) => {
+ if (canceled || paused) {
+ return;
+ }
+ nextAttemptIndex = null;
+ const request = refreshMediaCookie();
+ const refreshPromise = request
+ .unwrap()
+ .then(() => {
+ if (canceled) {
+ return;
+ }
+ hasSucceeded.current = true;
+ selfHealPending = false;
+ notifyMediaCookieRefreshed();
+ })
+ .catch((error: unknown) => {
+ if (canceled) {
+ return;
+ }
+ if (paused) {
+ // Deliberate abort from a logout pause, not a real failure: resume must
+ // retry this same attempt rather than consuming a retry slot — and on the
+ // final attempt there is no "next" index, which would otherwise leave
+ // resume with nothing to run.
+ nextAttemptIndex = attemptIndex;
+ return;
+ }
+ const status = (error as { status?: unknown } | null)?.status;
+ if (status === 401) {
+ selfHealPending = false;
+ return;
+ }
+ const delay = RETRY_DELAYS_MS[attemptIndex];
+ if (delay === undefined) {
+ selfHealPending = false;
+ return;
+ }
+ nextAttemptIndex = attemptIndex + 1;
+ timeoutId = setTimeout(() => attempt(attemptIndex + 1), delay);
+ });
+ const pendingRefresh = { promise: refreshPromise, abort: request.abort };
+ pendingRefreshes.add(pendingRefresh);
+ void refreshPromise.finally(() => pendingRefreshes.delete(pendingRefresh));
+ };
+
+ const pause = async () => {
+ paused = true;
+ if (timeoutId !== undefined) {
+ clearTimeout(timeoutId);
+ timeoutId = undefined;
+ }
+ await abortAndWaitForPendingRefreshes(pendingRefreshes);
+ return () => {
+ if (canceled || hasSucceeded.current) {
+ return;
+ }
+ paused = false;
+ if (nextAttemptIndex !== null) {
+ attempt(nextAttemptIndex);
+ }
+ };
+ };
+
+ pauseRefreshHandler = pause;
+ selfHealPending = true;
+ attempt(0);
+
+ return () => {
+ canceled = true;
+ selfHealPending = false;
+ if (pauseRefreshHandler === pause) {
+ pauseRefreshHandler = null;
+ }
+ if (timeoutId !== undefined) {
+ clearTimeout(timeoutId);
+ }
+ // Every logout path flips isAuthenticated and lands here — abort any in-flight
+ // refresh so its response can't re-mint the HttpOnly cookie after auth state is
+ // cleared. (UserMenu's logout additionally pauses and awaits these before the
+ // server call; this is the backstop for sessionExpiredLogout and direct logout.)
+ for (const refresh of pendingRefreshes) {
+ refresh.abort();
+ }
+ };
+ }, [isAuthenticated, refreshMediaCookie]);
+};
diff --git a/invokeai/frontend/web/src/features/auth/store/authSlice.ts b/invokeai/frontend/web/src/features/auth/store/authSlice.ts
index d933c57ed34..296c6474cf1 100644
--- a/invokeai/frontend/web/src/features/auth/store/authSlice.ts
+++ b/invokeai/frontend/web/src/features/auth/store/authSlice.ts
@@ -53,6 +53,15 @@ const authSlice = createSlice({
localStorage.setItem('auth_token', action.payload.token);
}
},
+ tokenRefreshed: (state, action: PayloadAction) => {
+ state.token = action.payload;
+ if (typeof window !== 'undefined' && window.localStorage) {
+ localStorage.setItem('auth_token', action.payload);
+ }
+ },
+ currentUserUpdated: (state, action: PayloadAction) => {
+ state.user = action.payload;
+ },
logout: (state) => {
state.token = null;
state.user = null;
@@ -77,7 +86,8 @@ const authSlice = createSlice({
},
});
-export const { setCredentials, logout, sessionExpiredLogout, setLoading } = authSlice.actions;
+export const { setCredentials, tokenRefreshed, currentUserUpdated, logout, sessionExpiredLogout, setLoading } =
+ authSlice.actions;
export const authSliceConfig: SliceConfig = {
slice: authSlice,
diff --git a/invokeai/frontend/web/src/features/auth/store/authTokenRefresh.test.ts b/invokeai/frontend/web/src/features/auth/store/authTokenRefresh.test.ts
new file mode 100644
index 00000000000..ed8530ba217
--- /dev/null
+++ b/invokeai/frontend/web/src/features/auth/store/authTokenRefresh.test.ts
@@ -0,0 +1,119 @@
+import { beforeAll, beforeEach, describe, expect, it, vi } from 'vitest';
+
+import {
+ beginAuthTransition,
+ captureAuthGeneration,
+ createMediaAuthLock,
+ runWithMediaAuthLock,
+ shouldAcceptRefreshedToken,
+} from './authTokenRefresh';
+
+describe('refreshed token acceptance', () => {
+ beforeAll(() => {
+ const values = new Map();
+ vi.stubGlobal('localStorage', {
+ clear: () => values.clear(),
+ getItem: (key: string) => values.get(key) ?? null,
+ key: (index: number) => [...values.keys()][index] ?? null,
+ get length() {
+ return values.size;
+ },
+ setItem: (key: string, value: string) => values.set(key, value),
+ removeItem: (key: string) => values.delete(key),
+ });
+ });
+
+ beforeEach(() => {
+ localStorage.clear();
+ });
+
+ it('accepts a refresh for the unchanged authentication session', () => {
+ localStorage.setItem('auth_token', 'token-a');
+ const generation = captureAuthGeneration();
+
+ expect(shouldAcceptRefreshedToken('token-a', generation)).toBe(true);
+ });
+
+ it('rejects a delayed refresh after logout or another login', () => {
+ localStorage.setItem('auth_token', 'token-a');
+ const generation = captureAuthGeneration();
+
+ beginAuthTransition();
+ localStorage.setItem('auth_token', 'token-b');
+
+ expect(shouldAcceptRefreshedToken('token-a', generation)).toBe(false);
+ });
+
+ it('rejects a response superseded by a newer refresh', () => {
+ localStorage.setItem('auth_token', 'token-a');
+ const generation = captureAuthGeneration();
+ localStorage.setItem('auth_token', 'token-newer');
+
+ expect(shouldAcceptRefreshedToken('token-a', generation)).toBe(false);
+ });
+
+ it('recovers from a malformed stored generation', () => {
+ localStorage.setItem('auth_generation', 'not-a-number');
+
+ expect(captureAuthGeneration()).toBe(0);
+ expect(beginAuthTransition()).toBe(1);
+ });
+
+ it('serializes media-cookie writes', async () => {
+ const calls: string[] = [];
+ let releaseFirst: (() => void) | undefined;
+ const first = runWithMediaAuthLock(
+ () =>
+ new Promise((resolve) => {
+ calls.push('first-start');
+ releaseFirst = () => {
+ calls.push('first-end');
+ resolve();
+ };
+ })
+ );
+ const second = runWithMediaAuthLock(() => {
+ calls.push('second');
+ });
+
+ await Promise.resolve();
+ expect(calls).toEqual(['first-start']);
+ releaseFirst?.();
+ await Promise.all([first, second]);
+ expect(calls).toEqual(['first-start', 'first-end', 'second']);
+ });
+
+ it('serializes fallback media-cookie writes across tabs', async () => {
+ vi.stubGlobal('navigator', {});
+ const firstTabLock = createMediaAuthLock('tab-a');
+ const secondTabLock = createMediaAuthLock('tab-b');
+ const calls: string[] = [];
+ let releaseFirst: (() => void) | undefined;
+
+ const first = firstTabLock(
+ () =>
+ new Promise((resolve) => {
+ calls.push('first-start');
+ releaseFirst = () => {
+ calls.push('first-end');
+ resolve();
+ };
+ })
+ );
+ const second = secondTabLock(() => {
+ calls.push('second');
+ });
+
+ await vi.waitFor(() => expect(calls).toEqual(['first-start']));
+ releaseFirst?.();
+ await Promise.all([first, second]);
+ expect(calls).toEqual(['first-start', 'first-end', 'second']);
+ });
+
+ it('releases the fallback media lock when a write fails', async () => {
+ const lock = createMediaAuthLock('tab-a');
+
+ await expect(lock(() => Promise.reject(new Error('write failed')))).rejects.toThrow('write failed');
+ await expect(lock(() => 'next write')).resolves.toBe('next write');
+ });
+});
diff --git a/invokeai/frontend/web/src/features/auth/store/authTokenRefresh.ts b/invokeai/frontend/web/src/features/auth/store/authTokenRefresh.ts
new file mode 100644
index 00000000000..ba23a73f9c8
--- /dev/null
+++ b/invokeai/frontend/web/src/features/auth/store/authTokenRefresh.ts
@@ -0,0 +1,142 @@
+const AUTH_GENERATION_KEY = 'auth_generation';
+const MEDIA_AUTH_LOCK = 'invokeai-media-auth';
+const FALLBACK_LOCK_PREFIX = `${MEDIA_AUTH_LOCK}:`;
+const FALLBACK_LOCK_LEASE_MS = 30_000;
+const FALLBACK_LOCK_POLL_MS = 10;
+
+// Bound on the media-cookie sync fetch made while holding the media-auth lock.
+export const MEDIA_COOKIE_SYNC_TIMEOUT_MS = 10_000;
+
+// The middleware mints a refreshed token on every successful mutating request, but
+// token lifetimes are measured in days — re-committing (and re-syncing the media
+// cookie over the network) once a minute is plenty, and it keeps bulk operations from
+// paying one serialized cookie round trip per request.
+const TOKEN_REFRESH_THROTTLE_MS = 60_000;
+let lastTokenRefreshAcceptedAt = 0;
+
+export const isTokenRefreshThrottled = () => Date.now() - lastTokenRefreshAcceptedAt < TOKEN_REFRESH_THROTTLE_MS;
+
+export const markTokenRefreshAccepted = () => {
+ lastTokenRefreshAcceptedAt = Date.now();
+};
+
+type FallbackLockTicket = {
+ choosing: boolean;
+ expiresAt: number;
+ owner: string;
+ ticket: number;
+};
+
+const getAuthGeneration = () => {
+ const value = Number(localStorage.getItem(AUTH_GENERATION_KEY) ?? 0);
+ return Number.isSafeInteger(value) && value >= 0 ? value : 0;
+};
+
+export const captureAuthGeneration = () => getAuthGeneration();
+
+export const beginAuthTransition = () => {
+ const next = getAuthGeneration() + 1;
+ localStorage.setItem(AUTH_GENERATION_KEY, String(next));
+ return next;
+};
+
+export const shouldAcceptRefreshedToken = (requestToken: string, requestGeneration: number) =>
+ getAuthGeneration() === requestGeneration && localStorage.getItem('auth_token') === requestToken;
+
+const getFallbackLockTickets = (): FallbackLockTicket[] => {
+ const tickets: FallbackLockTicket[] = [];
+ const now = Date.now();
+ for (let index = 0; index < localStorage.length; index++) {
+ const key = localStorage.key(index);
+ if (!key?.startsWith(FALLBACK_LOCK_PREFIX)) {
+ continue;
+ }
+ try {
+ const ticket = JSON.parse(localStorage.getItem(key) ?? '') as FallbackLockTicket;
+ if (
+ ticket.owner &&
+ Number.isSafeInteger(ticket.ticket) &&
+ ticket.ticket >= 0 &&
+ Number.isFinite(ticket.expiresAt) &&
+ ticket.expiresAt > now
+ ) {
+ tickets.push(ticket);
+ }
+ } catch {
+ // Ignore malformed or stale lock records.
+ }
+ }
+ return tickets;
+};
+
+const delay = (milliseconds: number) =>
+ new Promise((resolve) => {
+ setTimeout(resolve, milliseconds);
+ });
+
+export const createMediaAuthLock = (owner: string) => {
+ const key = `${FALLBACK_LOCK_PREFIX}${owner}`;
+ let localQueue = Promise.resolve();
+
+ const run = async (callback: () => T | PromiseLike): Promise => {
+ const writeTicket = (ticket: number, choosing: boolean) => {
+ localStorage.setItem(
+ key,
+ JSON.stringify({ choosing, expiresAt: Date.now() + FALLBACK_LOCK_LEASE_MS, owner, ticket })
+ );
+ };
+
+ writeTicket(0, true);
+ const ticket = Math.max(0, ...getFallbackLockTickets().map((entry) => entry.ticket)) + 1;
+ writeTicket(ticket, false);
+
+ let lastRenewedAt = Date.now();
+ while (
+ getFallbackLockTickets().some(
+ (entry) =>
+ entry.owner !== owner &&
+ (entry.choosing || entry.ticket < ticket || (entry.ticket === ticket && entry.owner < owner))
+ )
+ ) {
+ await delay(FALLBACK_LOCK_POLL_MS);
+ // Renew the waiter's own lease while queueing: tickets are stamped with a 30s
+ // expiry at acquisition, and a waiter that outlives it vanishes from other tabs'
+ // views — a later-ticket tab would then enter the critical section alongside it.
+ if (Date.now() - lastRenewedAt >= FALLBACK_LOCK_LEASE_MS / 3) {
+ writeTicket(ticket, false);
+ lastRenewedAt = Date.now();
+ }
+ }
+
+ const heartbeat = setInterval(() => writeTicket(ticket, false), FALLBACK_LOCK_LEASE_MS / 3);
+ try {
+ return await callback();
+ } finally {
+ clearInterval(heartbeat);
+ localStorage.removeItem(key);
+ }
+ };
+
+ return (callback: () => T | PromiseLike): Promise => {
+ const result = localQueue.then(
+ () => run(callback),
+ () => run(callback)
+ );
+ localQueue = result.then(
+ () => undefined,
+ () => undefined
+ );
+ return result;
+ };
+};
+
+const fallbackMediaAuthLock = createMediaAuthLock(
+ globalThis.crypto?.randomUUID?.() ?? `${Date.now()}-${Math.random()}`
+);
+
+export const runWithMediaAuthLock = (callback: () => T | PromiseLike): Promise => {
+ if (typeof navigator !== 'undefined' && navigator.locks) {
+ return navigator.locks.request(MEDIA_AUTH_LOCK, callback) as Promise;
+ }
+ return fallbackMediaAuthLock(callback);
+};
diff --git a/invokeai/frontend/web/src/features/auth/store/logoutAfterServerConfirmation.test.ts b/invokeai/frontend/web/src/features/auth/store/logoutAfterServerConfirmation.test.ts
new file mode 100644
index 00000000000..b1ec213208d
--- /dev/null
+++ b/invokeai/frontend/web/src/features/auth/store/logoutAfterServerConfirmation.test.ts
@@ -0,0 +1,60 @@
+import { describe, expect, it, vi } from 'vitest';
+
+import { logoutAfterServerConfirmation } from './logoutAfterServerConfirmation';
+
+describe('logoutAfterServerConfirmation', () => {
+ it('clears local authentication after the server confirms logout', async () => {
+ const clearLocalAuthentication = vi.fn();
+
+ await logoutAfterServerConfirmation(() => Promise.resolve(), clearLocalAuthentication);
+
+ expect(clearLocalAuthentication).toHaveBeenCalledOnce();
+ });
+
+ it('retains local authentication when server logout fails', async () => {
+ const clearLocalAuthentication = vi.fn();
+
+ await expect(
+ logoutAfterServerConfirmation(() => Promise.reject(new Error('network error')), clearLocalAuthentication)
+ ).rejects.toThrow('network error');
+ expect(clearLocalAuthentication).not.toHaveBeenCalled();
+ });
+
+ it('waits for media-cookie refreshes before logging out', async () => {
+ const resumeMediaCookieRefresh = vi.fn();
+ let settleRefresh: (() => void) | undefined;
+ const pauseMediaCookieRefresh = vi.fn(
+ () =>
+ new Promise<() => void>((resolve) => {
+ settleRefresh = () => resolve(resumeMediaCookieRefresh);
+ })
+ );
+ const logoutOnServer = vi.fn(() => Promise.resolve());
+ const clearLocalAuthentication = vi.fn();
+
+ const logoutPromise = logoutAfterServerConfirmation(
+ logoutOnServer,
+ clearLocalAuthentication,
+ pauseMediaCookieRefresh
+ );
+
+ expect(logoutOnServer).not.toHaveBeenCalled();
+ settleRefresh?.();
+ await logoutPromise;
+
+ expect(logoutOnServer).toHaveBeenCalledOnce();
+ expect(clearLocalAuthentication).toHaveBeenCalledOnce();
+ expect(resumeMediaCookieRefresh).not.toHaveBeenCalled();
+ });
+
+ it('resumes media-cookie refreshes when server logout fails', async () => {
+ const resumeMediaCookieRefresh = vi.fn();
+ const pauseMediaCookieRefresh = vi.fn(() => Promise.resolve(resumeMediaCookieRefresh));
+
+ await expect(
+ logoutAfterServerConfirmation(() => Promise.reject(new Error('network error')), vi.fn(), pauseMediaCookieRefresh)
+ ).rejects.toThrow('network error');
+
+ expect(resumeMediaCookieRefresh).toHaveBeenCalledOnce();
+ });
+});
diff --git a/invokeai/frontend/web/src/features/auth/store/logoutAfterServerConfirmation.ts b/invokeai/frontend/web/src/features/auth/store/logoutAfterServerConfirmation.ts
new file mode 100644
index 00000000000..4eeb6b775ed
--- /dev/null
+++ b/invokeai/frontend/web/src/features/auth/store/logoutAfterServerConfirmation.ts
@@ -0,0 +1,14 @@
+export const logoutAfterServerConfirmation = async (
+ logoutOnServer: () => Promise,
+ clearLocalAuthentication: () => void,
+ pauseMediaCookieRefresh: () => Promise<() => void> = () => Promise.resolve(() => undefined)
+) => {
+ const resumeMediaCookieRefresh = await pauseMediaCookieRefresh();
+ try {
+ await logoutOnServer();
+ } catch (error) {
+ resumeMediaCookieRefresh();
+ throw error;
+ }
+ clearLocalAuthentication();
+};
diff --git a/invokeai/frontend/web/src/features/auth/store/mediaCookieRefresh.test.ts b/invokeai/frontend/web/src/features/auth/store/mediaCookieRefresh.test.ts
new file mode 100644
index 00000000000..d08638a6e20
--- /dev/null
+++ b/invokeai/frontend/web/src/features/auth/store/mediaCookieRefresh.test.ts
@@ -0,0 +1,25 @@
+import { describe, expect, it } from 'vitest';
+
+import { getMediaUrl } from './mediaCookieRefresh';
+
+describe('getMediaUrl', () => {
+ it('leaves the URL unchanged before the media cookie is refreshed', () => {
+ expect(getMediaUrl('/api/v1/images/i/image.png/full', 0)).toBe('/api/v1/images/i/image.png/full');
+ });
+
+ it('changes the URL after the media cookie is refreshed', () => {
+ expect(getMediaUrl('/api/v1/images/i/image.png/full', 1)).not.toBe(
+ getMediaUrl('/api/v1/images/i/image.png/full', 2)
+ );
+ });
+
+ it('preserves existing query parameters and fragments', () => {
+ expect(getMediaUrl('/media/video.mp4?download=false#preview', 3)).toBe(
+ '/media/video.mp4?download=false&media_cookie_version=3#preview'
+ );
+ });
+
+ it('preserves an empty URL', () => {
+ expect(getMediaUrl(undefined, 1)).toBeUndefined();
+ });
+});
diff --git a/invokeai/frontend/web/src/features/auth/store/mediaCookieRefresh.ts b/invokeai/frontend/web/src/features/auth/store/mediaCookieRefresh.ts
new file mode 100644
index 00000000000..f6003f68c81
--- /dev/null
+++ b/invokeai/frontend/web/src/features/auth/store/mediaCookieRefresh.ts
@@ -0,0 +1,31 @@
+import { useSyncExternalStore } from 'react';
+
+let version = 0;
+const listeners = new Set<() => void>();
+
+export const notifyMediaCookieRefreshed = () => {
+ version += 1;
+ listeners.forEach((listener) => listener());
+};
+
+const subscribe = (listener: () => void) => {
+ listeners.add(listener);
+ return () => listeners.delete(listener);
+};
+
+const getSnapshot = () => version;
+
+export const useMediaCookieRefreshVersion = () => useSyncExternalStore(subscribe, getSnapshot, getSnapshot);
+
+export const getMediaUrl = (url: string | undefined, mediaCookieVersion: number): string | undefined => {
+ if (!url || mediaCookieVersion === 0) {
+ return url;
+ }
+ const hashIndex = url.indexOf('#');
+ const base = hashIndex === -1 ? url : url.slice(0, hashIndex);
+ const hash = hashIndex === -1 ? '' : url.slice(hashIndex);
+ const separator = base.includes('?') ? '&' : '?';
+ return `${base}${separator}media_cookie_version=${mediaCookieVersion}${hash}`;
+};
+
+export const useMediaUrl = (url: string | undefined) => getMediaUrl(url, useMediaCookieRefreshVersion());
diff --git a/invokeai/frontend/web/src/features/auth/store/protectedMediaConsumers.test.ts b/invokeai/frontend/web/src/features/auth/store/protectedMediaConsumers.test.ts
new file mode 100644
index 00000000000..8a7933e670e
--- /dev/null
+++ b/invokeai/frontend/web/src/features/auth/store/protectedMediaConsumers.test.ts
@@ -0,0 +1,33 @@
+import { readFileSync } from 'node:fs';
+import { fileURLToPath } from 'node:url';
+
+import { describe, expect, it } from 'vitest';
+
+const readConsumer = (relativePath: string) =>
+ readFileSync(fileURLToPath(new URL(relativePath, import.meta.url)), 'utf8');
+
+describe('protected media consumers', () => {
+ it.each([
+ [
+ 'video field thumbnails',
+ '../../nodes/components/flow/nodes/Invocation/fields/inputs/VideoFieldInputComponent.tsx',
+ 'src={videoDTO.thumbnail_url}',
+ ],
+ [
+ 'video frame scrubbers',
+ '../../nodes/components/flow/nodes/Invocation/fields/IntegerField/VideoFrameIndexFieldInput.tsx',
+ 'videoUrl={videoDTO.video_url}',
+ ],
+ [
+ 'reference image previews',
+ '../../controlLayers/components/RefImage/RefImagePreview.tsx',
+ 'src={imageDTO.image_url}',
+ ],
+ ])('%s refreshes its URL after the media cookie changes', (_name, relativePath, rawUsage) => {
+ const source = readConsumer(relativePath);
+
+ expect(source).toContain("from 'features/auth/store/mediaCookieRefresh'");
+ expect(source).toContain('useMediaUrl(');
+ expect(source).not.toContain(rawUsage);
+ });
+});
diff --git a/invokeai/frontend/web/src/features/auth/store/refreshedTokenConsumers.test.ts b/invokeai/frontend/web/src/features/auth/store/refreshedTokenConsumers.test.ts
new file mode 100644
index 00000000000..6388c8f784f
--- /dev/null
+++ b/invokeai/frontend/web/src/features/auth/store/refreshedTokenConsumers.test.ts
@@ -0,0 +1,56 @@
+import { readFileSync } from 'node:fs';
+import { fileURLToPath } from 'node:url';
+
+import { describe, expect, it } from 'vitest';
+
+import { authSliceConfig, currentUserUpdated, setCredentials, tokenRefreshed } from './authSlice';
+
+const user = {
+ user_id: 'user',
+ email: 'user@example.com',
+ display_name: null,
+ is_admin: false,
+ is_active: true,
+};
+
+describe('refreshed token consumers', () => {
+ it('updates the Redux token used by socket reconnects', () => {
+ let state = authSliceConfig.slice.reducer(undefined, setCredentials({ token: 'old', user }));
+ state = authSliceConfig.slice.reducer(state, tokenRefreshed('new'));
+
+ expect(state.token).toBe('new');
+ });
+
+ it('updates profile data without replacing the refreshed token', () => {
+ let state = authSliceConfig.slice.reducer(undefined, setCredentials({ token: 'new', user }));
+ state = authSliceConfig.slice.reducer(state, currentUserUpdated({ ...user, display_name: 'Updated' }));
+
+ expect(state.token).toBe('new');
+ expect(state.user?.display_name).toBe('Updated');
+
+ const profileSource = readFileSync(
+ fileURLToPath(new URL('../components/UserProfile.tsx', import.meta.url)),
+ 'utf8'
+ );
+ expect(profileSource).toContain('currentUserUpdated({');
+ expect(profileSource).not.toContain('setCredentials({');
+ });
+
+ it('attempts the media-cookie sync before committing a refreshed bearer token', () => {
+ const apiSource = readFileSync(fileURLToPath(new URL('../../../services/api/index.ts', import.meta.url)), 'utf8');
+ const cookieWrite = apiSource.indexOf('const mediaCookieResponse = await fetch');
+ const tokenCommit = apiSource.indexOf('dispatch(tokenRefreshed(refreshedToken))');
+
+ expect(cookieWrite).toBeGreaterThan(-1);
+ expect(tokenCommit).toBeGreaterThan(cookieWrite);
+ expect(apiSource.slice(cookieWrite, tokenCommit)).toContain('/api/v1/auth/media-cookie');
+ // The commit is refused only when the server rejects the refreshed token itself
+ // (401/403); a 5xx or network failure must NOT discard the token — dropping it
+ // would hard-expire an active session over a transient cookie-endpoint problem,
+ // while the media cookie itself self-heals on later refreshes.
+ expect(apiSource.slice(cookieWrite, tokenCommit)).toContain('mediaCookieResponse.status === 401');
+ // The sync fetch is time-bounded so the exclusive cross-tab media-auth lock
+ // (shared with login/logout) can never be held indefinitely by a stalled request.
+ expect(apiSource).toContain('AbortSignal.timeout(MEDIA_COOKIE_SYNC_TIMEOUT_MS)');
+ });
+});
diff --git a/invokeai/frontend/web/src/features/changeBoardModal/components/ChangeBoardModal.tsx b/invokeai/frontend/web/src/features/changeBoardModal/components/ChangeBoardModal.tsx
index 5ac6ffcb7c9..62cb8eacbf6 100644
--- a/invokeai/frontend/web/src/features/changeBoardModal/components/ChangeBoardModal.tsx
+++ b/invokeai/frontend/web/src/features/changeBoardModal/components/ChangeBoardModal.tsx
@@ -14,6 +14,7 @@ import { memo, useCallback, useMemo, useState } from 'react';
import { useTranslation } from 'react-i18next';
import { useListAllBoardsQuery } from 'services/api/endpoints/boards';
import { useAddImagesToBoardMutation, useRemoveImagesFromBoardMutation } from 'services/api/endpoints/images';
+import { useAddVideoToBoardMutation, useRemoveVideoFromBoardMutation } from 'services/api/endpoints/videos';
import type { BoardDTO } from 'services/api/types';
const selectImagesToChange = createSelector(
@@ -21,6 +22,11 @@ const selectImagesToChange = createSelector(
(changeBoardModal) => changeBoardModal.image_names
);
+const selectVideosToChange = createSelector(
+ selectChangeBoardModalSlice,
+ (changeBoardModal) => changeBoardModal.video_names
+);
+
const selectIsModalOpen = createSelector(
selectChangeBoardModalSlice,
(changeBoardModal) => changeBoardModal.isModalOpen
@@ -35,8 +41,11 @@ const ChangeBoardModal = () => {
const { data: boards, isFetching } = useListAllBoardsQuery({ include_archived: true });
const isModalOpen = useAppSelector(selectIsModalOpen);
const imagesToChange = useAppSelector(selectImagesToChange);
+ const videosToChange = useAppSelector(selectVideosToChange);
const [addImagesToBoard] = useAddImagesToBoardMutation();
const [removeImagesFromBoard] = useRemoveImagesFromBoardMutation();
+ const [addVideoToBoard] = useAddVideoToBoardMutation();
+ const [removeVideoFromBoard] = useRemoveVideoFromBoardMutation();
const { t } = useTranslation();
// Returns true if the current user can write images to the given board.
@@ -70,7 +79,7 @@ const ChangeBoardModal = () => {
}, [dispatch]);
const handleChangeBoard = useCallback(() => {
- if (!selectedBoardId || imagesToChange.length === 0) {
+ if (!selectedBoardId || (imagesToChange.length === 0 && videosToChange.length === 0)) {
return;
}
@@ -84,8 +93,30 @@ const ChangeBoardModal = () => {
});
}
}
+
+ if (videosToChange.length) {
+ // The video board endpoints take one video at a time; the context menu acts on a single
+ // selection, so this is normally a one-iteration loop.
+ for (const video_name of videosToChange) {
+ if (selectedBoardId === 'none') {
+ removeVideoFromBoard({ video_name });
+ } else {
+ addVideoToBoard({ board_id: selectedBoardId, video_name });
+ }
+ }
+ }
+
dispatch(changeBoardReset());
- }, [addImagesToBoard, dispatch, imagesToChange, removeImagesFromBoard, selectedBoardId]);
+ }, [
+ addImagesToBoard,
+ addVideoToBoard,
+ dispatch,
+ imagesToChange,
+ removeImagesFromBoard,
+ removeVideoFromBoard,
+ selectedBoardId,
+ videosToChange,
+ ]);
const onChange = useCallback((v) => {
if (!v) {
@@ -107,7 +138,7 @@ const ChangeBoardModal = () => {
{t('boards.movingImagesToBoard', {
- count: imagesToChange.length,
+ count: imagesToChange.length + videosToChange.length,
})}
diff --git a/invokeai/frontend/web/src/features/changeBoardModal/store/slice.ts b/invokeai/frontend/web/src/features/changeBoardModal/store/slice.ts
index 3f72720a420..dadf7a43b36 100644
--- a/invokeai/frontend/web/src/features/changeBoardModal/store/slice.ts
+++ b/invokeai/frontend/web/src/features/changeBoardModal/store/slice.ts
@@ -7,6 +7,7 @@ import z from 'zod';
const zChangeBoardModalState = z.object({
isModalOpen: z.boolean().default(false),
image_names: z.array(z.string()).default(() => []),
+ video_names: z.array(z.string()).default(() => []),
});
type ChangeBoardModalState = z.infer;
@@ -21,15 +22,21 @@ const slice = createSlice({
},
imagesToChangeSelected: (state, action: PayloadAction) => {
state.image_names = action.payload;
+ state.video_names = [];
+ },
+ videosToChangeSelected: (state, action: PayloadAction) => {
+ state.video_names = action.payload;
+ state.image_names = [];
},
changeBoardReset: (state) => {
state.image_names = [];
+ state.video_names = [];
state.isModalOpen = false;
},
},
});
-export const { isModalOpenChanged, imagesToChangeSelected, changeBoardReset } = slice.actions;
+export const { isModalOpenChanged, imagesToChangeSelected, videosToChangeSelected, changeBoardReset } = slice.actions;
export const selectChangeBoardModalSlice = (state: RootState) => state.changeBoardModal;
diff --git a/invokeai/frontend/web/src/features/controlLayers/components/RefImage/RefImagePreview.tsx b/invokeai/frontend/web/src/features/controlLayers/components/RefImage/RefImagePreview.tsx
index becf7e4c4d1..95ed74a3145 100644
--- a/invokeai/frontend/web/src/features/controlLayers/components/RefImage/RefImagePreview.tsx
+++ b/invokeai/frontend/web/src/features/controlLayers/components/RefImage/RefImagePreview.tsx
@@ -2,6 +2,7 @@ import type { SystemStyleObject } from '@invoke-ai/ui-library';
import { Box, Flex, Icon, IconButton, Skeleton, Text, Tooltip } from '@invoke-ai/ui-library';
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
import { round } from 'es-toolkit/compat';
+import { useMediaUrl } from 'features/auth/store/mediaCookieRefresh';
import { useRefImageDnd } from 'features/controlLayers/components/RefImage/useRefImageDnd';
import { useRefImageEntity } from 'features/controlLayers/components/RefImage/useRefImageEntity';
import { useRefImageIdContext } from 'features/controlLayers/contexts/RefImageIdContext';
@@ -88,6 +89,7 @@ export const RefImagePreview = memo(() => {
const [dndListState, isDragging] = useRefImageDnd(dndRef, id);
const imageDTO = useImageDTOFromCroppableImage(entity.config.image);
+ const imageUrl = useMediaUrl(imageDTO?.image_url);
const sx = useMemo(() => {
if (!isIPAdapterConfig(entity.config) || isExternalModel) {
@@ -184,7 +186,7 @@ export const RefImagePreview = memo(() => {
>
{imageDTO ? (
{
const isFLUX = useAppSelector(selectIsFLUX);
const isExternalModel = !!mainModelConfig && isExternalApiModelConfig(mainModelConfig);
- // FLUX.2 Klein, Qwen Image Edit and external API models do not require a ref image model selection.
+ // FLUX.2 Klein, Qwen Image Edit, Wan 2.2 and external API models do not require a ref image model selection.
const showModelSelector =
- !isFlux2ReferenceImageConfig(config) && !isQwenImageReferenceImageConfig(config) && !isExternalModel;
+ !isFlux2ReferenceImageConfig(config) &&
+ !isQwenImageReferenceImageConfig(config) &&
+ !isWanReferenceImageConfig(config) &&
+ !isExternalModel;
return (
diff --git a/invokeai/frontend/web/src/features/controlLayers/hooks/addLayerHooks.ts b/invokeai/frontend/web/src/features/controlLayers/hooks/addLayerHooks.ts
index 2027ff41741..10603191df6 100644
--- a/invokeai/frontend/web/src/features/controlLayers/hooks/addLayerHooks.ts
+++ b/invokeai/frontend/web/src/features/controlLayers/hooks/addLayerHooks.ts
@@ -32,6 +32,7 @@ import type {
QwenImageReferenceImageConfig,
RegionalGuidanceIPAdapterConfig,
T2IAdapterConfig,
+ WanReferenceImageConfig,
} from 'features/controlLayers/store/types';
import {
initialControlNet,
@@ -41,6 +42,7 @@ import {
initialQwenImageReferenceImage,
initialRegionalGuidanceIPAdapter,
initialT2IAdapter,
+ initialWanReferenceImage,
} from 'features/controlLayers/store/util';
import { zModelIdentifierField } from 'features/nodes/types/common';
import { useCallback } from 'react';
@@ -80,7 +82,12 @@ export const selectDefaultControlAdapter = createSelector(
export const getDefaultRefImageConfig = (
getState: AppGetState
-): IPAdapterConfig | FluxKontextReferenceImageConfig | Flux2ReferenceImageConfig | QwenImageReferenceImageConfig => {
+):
+ | IPAdapterConfig
+ | FluxKontextReferenceImageConfig
+ | Flux2ReferenceImageConfig
+ | QwenImageReferenceImageConfig
+ | WanReferenceImageConfig => {
const state = getState();
const mainModelConfig = selectMainModelConfig(state);
@@ -98,6 +105,11 @@ export const getDefaultRefImageConfig = (
return deepClone(initialQwenImageReferenceImage);
}
+ // Wan 2.2 I2V uses the main model's own VAE - no adapter model needed
+ if (base === 'wan') {
+ return deepClone(initialWanReferenceImage);
+ }
+
if (base === 'flux' && mainModelConfig?.name?.toLowerCase().includes('kontext')) {
const config = deepClone(initialFluxKontextReferenceImage);
config.model = zModelIdentifierField.parse(mainModelConfig);
diff --git a/invokeai/frontend/web/src/features/controlLayers/store/paramsSlice.ts b/invokeai/frontend/web/src/features/controlLayers/store/paramsSlice.ts
index b7b1a6ef1c0..1797949356a 100644
--- a/invokeai/frontend/web/src/features/controlLayers/store/paramsSlice.ts
+++ b/invokeai/frontend/web/src/features/controlLayers/store/paramsSlice.ts
@@ -304,6 +304,37 @@ const slice = createSlice({
qwenImageShiftChanged: (state, action: PayloadAction) => {
state.qwenImageShift = action.payload;
},
+ wanTransformerLowNoiseSelected: (state, action: PayloadAction) => {
+ const result = zParamsState.shape.wanTransformerLowNoise.safeParse(action.payload);
+ if (!result.success) {
+ return;
+ }
+ state.wanTransformerLowNoise = result.data;
+ },
+ wanComponentSourceSelected: (state, action: PayloadAction) => {
+ const result = zParamsState.shape.wanComponentSource.safeParse(action.payload);
+ if (!result.success) {
+ return;
+ }
+ state.wanComponentSource = result.data;
+ },
+ wanVaeModelSelected: (state, action: PayloadAction) => {
+ const result = zParamsState.shape.wanVaeModel.safeParse(action.payload);
+ if (!result.success) {
+ return;
+ }
+ state.wanVaeModel = result.data;
+ },
+ wanT5EncoderModelSelected: (state, action: PayloadAction<{ key: string; name: string; base: string } | null>) => {
+ const result = zParamsState.shape.wanT5EncoderModel.safeParse(action.payload);
+ if (!result.success) {
+ return;
+ }
+ state.wanT5EncoderModel = result.data;
+ },
+ wanGuidanceScaleLowNoiseChanged: (state, action: PayloadAction) => {
+ state.wanGuidanceScaleLowNoise = action.payload;
+ },
vaePrecisionChanged: (state, action: PayloadAction) => {
state.vaePrecision = action.payload;
},
@@ -636,6 +667,11 @@ const resetState = (state: ParamsState): ParamsState => {
newState.qwenImageQwenVLEncoderModel = oldState.qwenImageQwenVLEncoderModel;
newState.qwenImageQuantization = oldState.qwenImageQuantization;
newState.qwenImageShift = oldState.qwenImageShift;
+ newState.wanTransformerLowNoise = oldState.wanTransformerLowNoise;
+ newState.wanComponentSource = oldState.wanComponentSource;
+ newState.wanVaeModel = oldState.wanVaeModel;
+ newState.wanT5EncoderModel = oldState.wanT5EncoderModel;
+ newState.wanGuidanceScaleLowNoise = oldState.wanGuidanceScaleLowNoise;
return newState;
};
@@ -688,6 +724,11 @@ export const {
qwenImageQwenVLEncoderModelSelected,
qwenImageQuantizationChanged,
qwenImageShiftChanged,
+ wanTransformerLowNoiseSelected,
+ wanComponentSourceSelected,
+ wanVaeModelSelected,
+ wanT5EncoderModelSelected,
+ wanGuidanceScaleLowNoiseChanged,
setClipSkip,
shouldUseCpuNoiseChanged,
setColorCompensation,
@@ -779,6 +820,7 @@ export const selectIsAnima = createParamsSelector((params) => params.model?.base
export const selectIsFlux2 = createParamsSelector((params) => params.model?.base === 'flux2');
export const selectIsExternal = createParamsSelector((params) => params.model?.base === 'external');
export const selectIsQwenImage = createParamsSelector((params) => params.model?.base === 'qwen-image');
+export const selectIsWan = createParamsSelector((params) => params.model?.base === 'wan');
export const selectIsFluxKontext = createParamsSelector((params) => {
if (params.model?.base === 'flux' && params.model?.name.toLowerCase().includes('kontext')) {
return true;
@@ -811,6 +853,11 @@ export const selectQwenImageVaeModel = createParamsSelector((params) => params.q
export const selectQwenImageQwenVLEncoderModel = createParamsSelector((params) => params.qwenImageQwenVLEncoderModel);
export const selectQwenImageQuantization = createParamsSelector((params) => params.qwenImageQuantization);
export const selectQwenImageShift = createParamsSelector((params) => params.qwenImageShift);
+export const selectWanTransformerLowNoise = createParamsSelector((params) => params.wanTransformerLowNoise);
+export const selectWanComponentSource = createParamsSelector((params) => params.wanComponentSource);
+export const selectWanVaeModel = createParamsSelector((params) => params.wanVaeModel);
+export const selectWanT5EncoderModel = createParamsSelector((params) => params.wanT5EncoderModel);
+export const selectWanGuidanceScaleLowNoise = createParamsSelector((params) => params.wanGuidanceScaleLowNoise);
export const selectCFGScale = createParamsSelector((params) => params.cfgScale);
export const selectGuidance = createParamsSelector((params) => params.guidance);
@@ -870,7 +917,16 @@ export const selectModelSupportsRefImages = createSelector(selectModel, selectMo
if (model.base === 'external') {
return false;
}
- return SUPPORTS_REF_IMAGES_BASE_MODELS.includes(model.base);
+ if (!SUPPORTS_REF_IMAGES_BASE_MODELS.includes(model.base)) {
+ return false;
+ }
+ // Wan: only the I2V variant of A14B consumes a reference image. T2V and
+ // TI2V-5B ignore ref images, so hide the panel for those.
+ if (model.base === 'wan') {
+ const variant = modelConfig && 'variant' in modelConfig ? modelConfig.variant : null;
+ return variant === 'i2v_a14b';
+ }
+ return true;
});
export const selectModelSupportsOptimizedDenoising = createSelector(
selectModel,
diff --git a/invokeai/frontend/web/src/features/controlLayers/store/refImagesSlice.ts b/invokeai/frontend/web/src/features/controlLayers/store/refImagesSlice.ts
index b7026b586a8..6c364e51e88 100644
--- a/invokeai/frontend/web/src/features/controlLayers/store/refImagesSlice.ts
+++ b/invokeai/frontend/web/src/features/controlLayers/store/refImagesSlice.ts
@@ -23,6 +23,7 @@ import {
isFLUXReduxConfig,
isIPAdapterConfig,
isQwenImageReferenceImageConfig,
+ isWanReferenceImageConfig,
zRefImagesState,
} from './types';
import { getReferenceImageState, initialFluxKontextReferenceImage, initialFLUXRedux, initialIPAdapter } from './util';
@@ -144,8 +145,12 @@ const slice = createSlice({
return;
}
- // FLUX.2 and Qwen Image Edit reference images don't have a model field - they use built-in support
- if (isFlux2ReferenceImageConfig(entity.config) || isQwenImageReferenceImageConfig(entity.config)) {
+ // FLUX.2, Qwen Image Edit and Wan reference images don't have a model field - they use built-in support
+ if (
+ isFlux2ReferenceImageConfig(entity.config) ||
+ isQwenImageReferenceImageConfig(entity.config) ||
+ isWanReferenceImageConfig(entity.config)
+ ) {
return;
}
diff --git a/invokeai/frontend/web/src/features/controlLayers/store/types.ts b/invokeai/frontend/web/src/features/controlLayers/store/types.ts
index 04897189693..d82c1b2765a 100644
--- a/invokeai/frontend/web/src/features/controlLayers/store/types.ts
+++ b/invokeai/frontend/web/src/features/controlLayers/store/types.ts
@@ -424,6 +424,15 @@ const zQwenImageReferenceImageConfig = z.object({
});
export type QwenImageReferenceImageConfig = z.infer;
+// Wan 2.2 I2V uses the model's own VAE to encode a single reference image -
+// no separate adapter model needed. Only consumed by the I2V variant of Wan
+// 2.2 (A14B). T2V / TI2V variants ignore the ref image at graph build time.
+const zWanReferenceImageConfig = z.object({
+ type: z.literal('wan_reference_image'),
+ image: zCroppableImageWithDims.nullable(),
+});
+export type WanReferenceImageConfig = z.infer;
+
const zCanvasEntityBase = z.object({
id: zId,
name: zName,
@@ -440,6 +449,7 @@ export const zRefImageState = z.object({
zFluxKontextReferenceImageConfig,
zFlux2ReferenceImageConfig,
zQwenImageReferenceImageConfig,
+ zWanReferenceImageConfig,
]),
});
export type RefImageState = z.infer;
@@ -461,6 +471,9 @@ export const isQwenImageReferenceImageConfig = (
config: RefImageState['config']
): config is QwenImageReferenceImageConfig => config.type === 'qwen_image_reference_image';
+export const isWanReferenceImageConfig = (config: RefImageState['config']): config is WanReferenceImageConfig =>
+ config.type === 'wan_reference_image';
+
const zFillStyle = z.enum(['solid', 'grid', 'crosshatch', 'diagonal', 'horizontal', 'vertical']);
export type FillStyle = z.infer;
export const isFillStyle = (v: unknown): v is FillStyle => zFillStyle.safeParse(v).success;
@@ -867,6 +880,13 @@ export const zParamsState = z.object({
qwenImageQwenVLEncoderModel: zModelIdentifierField.nullable(), // Optional: Standalone Qwen2.5-VL encoder
qwenImageQuantization: z.enum(['none', 'int8', 'nf4']), // BitsAndBytes quantization for Qwen VL encoder
qwenImageShift: z.number().nullable(), // Sigma schedule shift override (e.g. 3.0 for Lightning LoRAs)
+ // Wan 2.2 model components — A14B GGUF needs a paired second-expert transformer
+ // plus a Diffusers source for VAE/T5 unless standalone VAE/encoder models are wired.
+ wanTransformerLowNoise: zParameterModel.nullable(), // A14B GGUF only: second-expert transformer
+ wanComponentSource: zParameterModel.nullable(), // Diffusers Wan model providing VAE + UMT5-XXL
+ wanVaeModel: zParameterVAEModel.nullable(), // Optional: Standalone Wan VAE checkpoint
+ wanT5EncoderModel: zModelIdentifierField.nullable(), // Optional: Standalone UMT5-XXL encoder
+ wanGuidanceScaleLowNoise: z.number().nullable(), // Optional: separate CFG for low-noise expert (A14B). null = same as primary
// Z-Image Seed Variance Enhancer settings
zImageSeedVarianceEnabled: z.boolean(),
zImageSeedVarianceStrength: z.number().min(0).max(2),
@@ -953,6 +973,11 @@ export const getInitialParamsState = (): ParamsState => ({
qwenImageQwenVLEncoderModel: null,
qwenImageQuantization: 'none' as const,
qwenImageShift: null,
+ wanTransformerLowNoise: null,
+ wanComponentSource: null,
+ wanVaeModel: null,
+ wanT5EncoderModel: null,
+ wanGuidanceScaleLowNoise: null,
zImageSeedVarianceEnabled: false,
zImageSeedVarianceStrength: 0.1,
zImageSeedVarianceRandomizePercent: 50,
diff --git a/invokeai/frontend/web/src/features/controlLayers/store/util.ts b/invokeai/frontend/web/src/features/controlLayers/store/util.ts
index b28d74d7b54..a0dae2145d0 100644
--- a/invokeai/frontend/web/src/features/controlLayers/store/util.ts
+++ b/invokeai/frontend/web/src/features/controlLayers/store/util.ts
@@ -22,6 +22,7 @@ import type {
RegionalGuidanceIPAdapterConfig,
RgbColor,
T2IAdapterConfig,
+ WanReferenceImageConfig,
ZImageControlConfig,
} from 'features/controlLayers/store/types';
import type { ImageDTO } from 'services/api/types';
@@ -123,6 +124,10 @@ export const initialQwenImageReferenceImage: QwenImageReferenceImageConfig = {
type: 'qwen_image_reference_image',
image: null,
};
+export const initialWanReferenceImage: WanReferenceImageConfig = {
+ type: 'wan_reference_image',
+ image: null,
+};
export const initialT2IAdapter: T2IAdapterConfig = {
type: 't2i_adapter',
model: null,
diff --git a/invokeai/frontend/web/src/features/controlLayers/store/validators.ts b/invokeai/frontend/web/src/features/controlLayers/store/validators.ts
index a497a38a5d7..fc6cf235bb6 100644
--- a/invokeai/frontend/web/src/features/controlLayers/store/validators.ts
+++ b/invokeai/frontend/web/src/features/controlLayers/store/validators.ts
@@ -163,8 +163,12 @@ export const getGlobalReferenceImageWarnings = (
const { config } = entity;
- // FLUX.2 and Qwen Image Edit reference images don't require a model - it's built-in
- if (config.type !== 'flux2_reference_image' && config.type !== 'qwen_image_reference_image') {
+ // FLUX.2, Qwen Image Edit and Wan reference images don't require a model - it's built-in
+ if (
+ config.type !== 'flux2_reference_image' &&
+ config.type !== 'qwen_image_reference_image' &&
+ config.type !== 'wan_reference_image'
+ ) {
if (!('model' in config) || !config.model) {
// No model selected
warnings.push(WARNINGS.IP_ADAPTER_NO_MODEL_SELECTED);
@@ -175,8 +179,10 @@ export const getGlobalReferenceImageWarnings = (
}
if (!entity.config.image) {
- // No image selected - for Qwen Image Edit, an image is optional (txt2img works without one)
- if (config.type !== 'qwen_image_reference_image') {
+ // No image selected - for Qwen Image Edit and Wan, an image is optional at the
+ // entity level. Wan I2V *requires* one but enforcement happens at graph-build
+ // time so the warning doesn't fire on T2V/TI2V variants that ignore ref images.
+ if (config.type !== 'qwen_image_reference_image' && config.type !== 'wan_reference_image') {
warnings.push(WARNINGS.IP_ADAPTER_NO_IMAGE_SELECTED);
}
}
diff --git a/invokeai/frontend/web/src/features/deleteImageModal/store/state.test.ts b/invokeai/frontend/web/src/features/deleteImageModal/store/state.test.ts
new file mode 100644
index 00000000000..385482447cc
--- /dev/null
+++ b/invokeai/frontend/web/src/features/deleteImageModal/store/state.test.ts
@@ -0,0 +1,147 @@
+/**
+ * Regression tests for the post-delete selection logic in the image delete flow.
+ *
+ * The bug (PR #9163 review): ``handleDeletions`` cleared the viewer selection
+ * (``imageSelected(null)``) whenever the deletion intersected the multi-selection but the
+ * *displayed* item was not among the deleted names — e.g. a video displayed while only
+ * images were deleted, or a hover-delete of a non-displayed selected image. It also treated
+ * every *requested* name as deleted, ignoring the server's ``deleted_images`` response, so a
+ * partial failure could jump the selection away from (or past) an image that still exists.
+ *
+ * The fix mirrors deleteVideoModal/store/state.ts: only server-confirmed deletions count,
+ * and a surviving displayed item stays selected.
+ */
+import { beforeEach, describe, expect, it, vi } from 'vitest';
+
+vi.mock('services/api/endpoints/images', () => ({
+ imagesApi: {
+ endpoints: {
+ deleteImages: {
+ initiate: vi.fn((arg: { image_names: string[] }) => ({
+ type: 'imagesApi/deleteImages',
+ image_names: arg.image_names,
+ })),
+ },
+ },
+ },
+}));
+
+vi.mock('features/gallery/store/gallerySelectors', () => ({
+ selectLastSelectedItem: vi.fn(),
+}));
+
+vi.mock('features/gallery/store/gallerySlice', () => ({
+ imageSelected: vi.fn((payload: string | null) => ({ type: 'gallery/imageSelected', payload })),
+}));
+
+vi.mock('features/system/store/systemSlice', () => ({
+ selectSystemShouldConfirmOnDelete: vi.fn(() => false),
+}));
+
+// The canvas/ref-image usage sweeps aren't under test — give them empty state.
+vi.mock('features/controlLayers/store/selectors', () => ({
+ selectCanvasSlice: vi.fn(() => ({ controlLayers: { entities: [] }, rasterLayers: { entities: [] } })),
+}));
+
+vi.mock('features/controlLayers/store/refImagesSlice', () => ({
+ refImageImageChanged: vi.fn(),
+ selectReferenceImageEntities: vi.fn(() => []),
+ selectRefImagesSlice: vi.fn(() => ({ entities: [] })),
+}));
+
+// Keep the real pickSelectionAfterDelete (its neighbour-picking is part of the behavior under
+// test) but stub the cache selector, which would otherwise need a live RTK Query store.
+vi.mock('features/gallery/store/selectCachedGalleryItemNames', async (importOriginal) => {
+ const actual = await importOriginal();
+ return { ...actual, selectCachedGalleryItemNames: vi.fn() };
+});
+
+import type { AppStore } from 'app/store/store';
+import { selectLastSelectedItem } from 'features/gallery/store/gallerySelectors';
+import { selectCachedGalleryItemNames } from 'features/gallery/store/selectCachedGalleryItemNames';
+
+import { handleDeletions } from './state';
+
+const buildStore = (selection: string[], failingNames: Set) => {
+ const dispatched: unknown[] = [];
+ const dispatch = vi.fn((action: unknown) => {
+ dispatched.push(action);
+ const typed = action as { type?: string; image_names?: string[] };
+ if (typed?.type === 'imagesApi/deleteImages') {
+ return {
+ unwrap: () =>
+ Promise.resolve({
+ deleted_images: (typed.image_names ?? []).filter((name) => !failingNames.has(name)),
+ affected_boards: [],
+ }),
+ };
+ }
+ return action;
+ });
+ const getState = vi.fn(() => ({ gallery: { selection }, nodes: { present: { nodes: [] } } }));
+ return { store: { dispatch, getState } as unknown as AppStore, dispatched };
+};
+
+const getSelectionChange = (dispatched: unknown[]) =>
+ dispatched.find(
+ (action): action is { type: string; payload: string | null } =>
+ !!action && typeof action === 'object' && (action as { type?: string }).type === 'gallery/imageSelected'
+ );
+
+describe('handleDeletions selection behavior', () => {
+ beforeEach(() => {
+ vi.clearAllMocks();
+ vi.mocked(selectCachedGalleryItemNames).mockReturnValue(['a.png', 'b.png', 'c.mp4']);
+ });
+
+ it('keeps viewing a displayed video when only images from a mixed selection are deleted', async () => {
+ // Video c.mp4 is displayed; the user deletes image a.png from the mixed selection.
+ vi.mocked(selectLastSelectedItem).mockReturnValue('c.mp4');
+ const { store, dispatched } = buildStore(['a.png', 'c.mp4'], new Set());
+
+ await handleDeletions(['a.png'], store);
+
+ // Before the fix this dispatched imageSelected(null) and dropped the viewer to its
+ // empty state even though the displayed video still exists.
+ expect(getSelectionChange(dispatched)?.payload).toBe('c.mp4');
+ });
+
+ it('keeps viewing the displayed image on hover-delete of another selected image', async () => {
+ // Multi-selection [a, b] with b displayed; the hover delete button deletes only a.
+ vi.mocked(selectLastSelectedItem).mockReturnValue('b.png');
+ const { store, dispatched } = buildStore(['a.png', 'b.png'], new Set());
+
+ await handleDeletions(['a.png'], store);
+
+ expect(getSelectionChange(dispatched)?.payload).toBe('b.png');
+ });
+
+ it('does not move the selection when the displayed image fails to delete', async () => {
+ vi.mocked(selectLastSelectedItem).mockReturnValue('a.png');
+ const { store, dispatched } = buildStore(['a.png'], new Set(['a.png']));
+
+ await handleDeletions(['a.png'], store);
+
+ expect(getSelectionChange(dispatched), 'a failed delete must not advance the selection').toBeUndefined();
+ });
+
+ it('keeps a surviving (failed-delete) neighbour as the replacement candidate', async () => {
+ vi.mocked(selectLastSelectedItem).mockReturnValue('a.png');
+ // Batch delete of a + b: a (the displayed item) deletes fine, b fails and still exists.
+ const { store, dispatched } = buildStore(['a.png'], new Set(['b.png']));
+
+ await handleDeletions(['a.png', 'b.png'], store);
+
+ // If b.png were treated as deleted, the selection would skip to c.mp4.
+ expect(getSelectionChange(dispatched)?.payload).toBe('b.png');
+ });
+
+ it('advances to the nearest surviving neighbour when everything requested is deleted', async () => {
+ vi.mocked(selectLastSelectedItem).mockReturnValue('b.png');
+ const { store, dispatched } = buildStore(['b.png'], new Set());
+
+ await handleDeletions(['b.png'], store);
+
+ expect(getSelectionChange(dispatched)?.payload).toBe('a.png');
+ });
+});
diff --git a/invokeai/frontend/web/src/features/deleteImageModal/store/state.ts b/invokeai/frontend/web/src/features/deleteImageModal/store/state.ts
index c50aa9465f5..8169a54bafc 100644
--- a/invokeai/frontend/web/src/features/deleteImageModal/store/state.ts
+++ b/invokeai/frontend/web/src/features/deleteImageModal/store/state.ts
@@ -11,8 +11,12 @@ import {
import { selectCanvasSlice } from 'features/controlLayers/store/selectors';
import type { CanvasState, RefImagesState } from 'features/controlLayers/store/types';
import type { ImageUsage } from 'features/deleteImageModal/store/types';
-import { selectGetImageNamesQueryArgs } from 'features/gallery/store/gallerySelectors';
+import { selectLastSelectedItem } from 'features/gallery/store/gallerySelectors';
import { imageSelected } from 'features/gallery/store/gallerySlice';
+import {
+ pickSelectionAfterDelete,
+ selectCachedGalleryItemNames,
+} from 'features/gallery/store/selectCachedGalleryItemNames';
import { fieldImageCollectionValueChanged, fieldImageValueChanged } from 'features/nodes/store/nodesSlice';
import { selectNodesSlice } from 'features/nodes/store/selectors';
import type { NodesState } from 'features/nodes/store/types';
@@ -76,30 +80,45 @@ const deleteImagesWithDialog = async (image_names: string[], store: AppStore): P
});
};
-const handleDeletions = async (image_names: string[], store: AppStore) => {
+export const handleDeletions = async (image_names: string[], store: AppStore) => {
try {
const { dispatch, getState } = store;
const state = getState();
- const { data } = imagesApi.endpoints.getImageNames.select(selectGetImageNamesQueryArgs(state))(state);
- const index = data?.image_names.findIndex((name) => name === image_names[0]);
- const { deleted_images } = await dispatch(
- imagesApi.endpoints.deleteImages.initiate({ image_names }, { track: false })
- ).unwrap();
- const newImageNames = data?.image_names.filter((name) => !deleted_images.includes(name)) || [];
- const newSelectedImage = newImageNames[index ?? 0] || null;
+ // Snapshot the polymorphic gallery list (images + videos, in display order) and the
+ // currently-displayed item *before* the delete fires. Once the request resolves the
+ // cache will have shifted, so the index computed afterwards would be wrong.
+ const galleryItemNames = selectCachedGalleryItemNames(state);
+ const lastSelected = selectLastSelectedItem(state);
+ const lastSelectedIndex =
+ lastSelected && image_names.includes(lastSelected) ? galleryItemNames.indexOf(lastSelected) : -1;
- if (intersection(state.gallery.selection, image_names).length > 0) {
- if (newSelectedImage) {
- // Some selected images were deleted, clear selection
- dispatch(imageSelected(newSelectedImage));
+ const result = await dispatch(
+ imagesApi.endpoints.deleteImages.initiate({ image_names }, { track: false })
+ ).unwrap();
+ // Only the images the server confirmed deleted count: a partial failure means the
+ // survivor still exists, so the selection must not jump away from it and it remains
+ // a valid replacement candidate. Mirrors deleteVideoModal/store/state.ts.
+ const deletedNames = new Set(result.deleted_images);
+
+ if (intersection(getState().gallery.selection, [...deletedNames]).length > 0) {
+ if (lastSelected && !deletedNames.has(lastSelected)) {
+ // The displayed item survived — either it wasn't part of this deletion (e.g. a
+ // video displayed while images were deleted, or a hover-delete of another item)
+ // or its own delete failed. Keep viewing it and just prune the deleted items
+ // from the multi-selection.
+ dispatch(imageSelected(lastSelected));
} else {
- dispatch(imageSelected(null));
+ // Advance to a still-living neighbour (prev > next) so the Viewer keeps a real
+ // selection. May pick a video — the polymorphic list intentionally allows that.
+ const replacement =
+ lastSelectedIndex >= 0 ? pickSelectionAfterDelete(galleryItemNames, lastSelectedIndex, deletedNames) : null;
+ dispatch(imageSelected(replacement));
}
}
// We need to reset the features where the image is in use - none of these work if their image(s) don't exist
- for (const image_name of image_names) {
+ for (const image_name of deletedNames) {
deleteNodesImages(state, dispatch, image_name);
deleteControlLayerImages(state, dispatch, image_name);
deleteReferenceImages(state, dispatch, image_name);
diff --git a/invokeai/frontend/web/src/features/deleteVideoModal/components/DeleteVideoModal.tsx b/invokeai/frontend/web/src/features/deleteVideoModal/components/DeleteVideoModal.tsx
new file mode 100644
index 00000000000..47030d95e9e
--- /dev/null
+++ b/invokeai/frontend/web/src/features/deleteVideoModal/components/DeleteVideoModal.tsx
@@ -0,0 +1,49 @@
+import { ConfirmationAlertDialog, Flex, FormControl, FormLabel, Switch, Text } from '@invoke-ai/ui-library';
+import { useAppSelector, useAppStore } from 'app/store/storeHooks';
+import { useDeleteVideoModalApi, useDeleteVideoModalState } from 'features/deleteVideoModal/store/state';
+import { selectSystemShouldConfirmOnDelete, setShouldConfirmOnDelete } from 'features/system/store/systemSlice';
+import type { ChangeEvent } from 'react';
+import { memo, useCallback } from 'react';
+import { useTranslation } from 'react-i18next';
+
+/**
+ * Confirmation dialog for deleting videos. Mirrors DeleteImageModal so the experience is
+ * consistent: same "don't ask me again" toggle (shared system setting), same accept/cancel
+ * buttons, same destructive styling. Skips the image dialog's usage analysis because
+ * videos don't show up as canvas layers / node fields / ref images.
+ */
+export const DeleteVideoModal = memo(() => {
+ const state = useDeleteVideoModalState();
+ const api = useDeleteVideoModalApi();
+ const { dispatch } = useAppStore();
+ const { t } = useTranslation();
+ const shouldConfirmOnDelete = useAppSelector(selectSystemShouldConfirmOnDelete);
+
+ const handleChangeShouldConfirmOnDelete = useCallback(
+ (e: ChangeEvent) => dispatch(setShouldConfirmOnDelete(!e.target.checked)),
+ [dispatch]
+ );
+
+ return (
+
+
+ {t('gallery.deleteVideoPermanent')}
+ {t('common.areYouSure')}
+
+ {t('common.dontAskMeAgain')}
+
+
+
+
+ );
+});
+DeleteVideoModal.displayName = 'DeleteVideoModal';
diff --git a/invokeai/frontend/web/src/features/deleteVideoModal/store/state.test.ts b/invokeai/frontend/web/src/features/deleteVideoModal/store/state.test.ts
new file mode 100644
index 00000000000..82fdfeb3ff2
--- /dev/null
+++ b/invokeai/frontend/web/src/features/deleteVideoModal/store/state.test.ts
@@ -0,0 +1,272 @@
+/**
+ * Regression tests for the video delete flow.
+ *
+ * Selection (PR #9163 review): ``handleDeletions`` must only treat server-confirmed
+ * deletions as deleted — the Viewer must not jump away from a video whose delete failed
+ * (403/500), and a surviving neighbour remains a valid replacement candidate.
+ *
+ * Batching (PR #9163 review): deletion goes through the batch ``deleteVideos`` endpoint —
+ * one request per invocation, not one per video — and its ``deleted_videos`` result is the
+ * source of truth for partial failures.
+ *
+ * Node references (PR #9163 review): workflow nodes take VideoField inputs; references to
+ * confirmed-deleted videos are cleared, references to surviving videos are preserved.
+ */
+import { readFileSync } from 'node:fs';
+import { fileURLToPath } from 'node:url';
+
+import { beforeEach, describe, expect, it, vi } from 'vitest';
+
+vi.mock('services/api/endpoints/videos', () => ({
+ videosApi: {
+ endpoints: {
+ deleteVideos: {
+ initiate: vi.fn((arg: { video_names: string[] }) => ({
+ type: 'videosApi/deleteVideos',
+ video_names: arg.video_names,
+ })),
+ },
+ },
+ },
+}));
+
+vi.mock('features/gallery/store/gallerySelectors', () => ({
+ selectLastSelectedItem: vi.fn(),
+}));
+
+vi.mock('features/gallery/store/gallerySlice', () => ({
+ imageSelected: vi.fn((payload: string | null) => ({ type: 'gallery/imageSelected', payload })),
+}));
+
+vi.mock('features/nodes/store/nodesSlice', () => ({
+ fieldVideoValueChanged: vi.fn((payload: unknown) => ({ type: 'nodes/fieldVideoValueChanged', payload })),
+}));
+
+vi.mock('features/system/store/systemSlice', () => ({
+ selectSystemShouldConfirmOnDelete: vi.fn(() => false),
+}));
+
+vi.mock('features/toast/toast', () => ({ toast: vi.fn() }));
+
+// Keep the real pickSelectionAfterDelete (its neighbour-picking is part of the behavior under
+// test) but stub the cache selector, which would otherwise need a live RTK Query store.
+vi.mock('features/gallery/store/selectCachedGalleryItemNames', async (importOriginal) => {
+ const actual = await importOriginal();
+ return { ...actual, selectCachedGalleryItemNames: vi.fn() };
+});
+
+import type { AppStore } from 'app/store/store';
+import { selectLastSelectedItem } from 'features/gallery/store/gallerySelectors';
+import { imageSelected } from 'features/gallery/store/gallerySlice';
+import { selectCachedGalleryItemNames } from 'features/gallery/store/selectCachedGalleryItemNames';
+import { selectSystemShouldConfirmOnDelete } from 'features/system/store/systemSlice';
+import { toast } from 'features/toast/toast';
+import { videosApi } from 'services/api/endpoints/videos';
+
+import { cancelDeletion, confirmDeletion, deleteVideosWithDialog, handleDeletions } from './state';
+
+const buildVideoFieldNode = (nodeId: string, videoName: string) => ({
+ type: 'invocation',
+ data: {
+ id: nodeId,
+ inputs: {
+ video: { name: 'video', label: '', description: '', value: { video_name: videoName } },
+ },
+ },
+});
+
+const buildStore = (selection: string[], failingNames: Set, nodes: unknown[] = [], rejectAll = false) => {
+ const dispatched: unknown[] = [];
+ const dispatch = vi.fn((action: unknown) => {
+ dispatched.push(action);
+ const typed = action as { type?: string; video_names?: string[] };
+ if (typed?.type === 'videosApi/deleteVideos') {
+ return {
+ unwrap: () =>
+ rejectAll
+ ? Promise.reject(new Error('delete failed'))
+ : Promise.resolve({
+ deleted_videos: (typed.video_names ?? []).filter((name) => !failingNames.has(name)),
+ failed_videos: (typed.video_names ?? []).filter((name) => failingNames.has(name)),
+ affected_boards: ['none'],
+ }),
+ };
+ }
+ return action;
+ });
+ const getState = vi.fn(() => ({ gallery: { selection }, nodes: { present: { nodes } } }));
+ return { store: { dispatch, getState } as unknown as AppStore, dispatched };
+};
+
+const getSelectionChange = (dispatched: unknown[]) =>
+ dispatched.find(
+ (action): action is { type: string; payload: string | null } =>
+ !!action && typeof action === 'object' && (action as { type?: string }).type === 'gallery/imageSelected'
+ );
+
+const getVideoFieldChanges = (dispatched: unknown[]) =>
+ dispatched.filter(
+ (action): action is { type: string; payload: { nodeId: string; fieldName: string; value: unknown } } =>
+ !!action && typeof action === 'object' && (action as { type?: string }).type === 'nodes/fieldVideoValueChanged'
+ );
+
+describe('handleDeletions batching', () => {
+ beforeEach(() => {
+ vi.clearAllMocks();
+ vi.mocked(selectCachedGalleryItemNames).mockReturnValue(['a.mp4', 'b.mp4', 'c.png']);
+ vi.mocked(selectLastSelectedItem).mockReturnValue(undefined);
+ });
+
+ it('issues a single batch request for a multi-video deletion', async () => {
+ const { store } = buildStore([], new Set());
+
+ await handleDeletions(['a.mp4', 'b.mp4'], store);
+
+ expect(videosApi.endpoints.deleteVideos.initiate).toHaveBeenCalledTimes(1);
+ expect(videosApi.endpoints.deleteVideos.initiate).toHaveBeenCalledWith(
+ { video_names: ['a.mp4', 'b.mp4'] },
+ { track: false }
+ );
+ expect(toast).not.toHaveBeenCalled();
+ });
+});
+
+describe('handleDeletions selection behavior on partial failure', () => {
+ beforeEach(() => {
+ vi.clearAllMocks();
+ vi.mocked(selectCachedGalleryItemNames).mockReturnValue(['a.mp4', 'b.mp4', 'c.png']);
+ });
+
+ it('does not move the selection when the displayed video fails to delete', async () => {
+ vi.mocked(selectLastSelectedItem).mockReturnValue('a.mp4');
+ const { store, dispatched } = buildStore(['a.mp4'], new Set(['a.mp4']));
+
+ await handleDeletions(['a.mp4'], store);
+
+ expect(getSelectionChange(dispatched), 'a failed delete must not advance the selection').toBeUndefined();
+ });
+
+ it('does not move the selection when the whole batch request fails', async () => {
+ vi.mocked(selectLastSelectedItem).mockReturnValue('a.mp4');
+ const { store, dispatched } = buildStore(['a.mp4'], new Set(), [], true);
+
+ await handleDeletions(['a.mp4'], store);
+
+ expect(getSelectionChange(dispatched)).toBeUndefined();
+ });
+
+ it('keeps a surviving (failed-delete) neighbour as the replacement candidate', async () => {
+ vi.mocked(selectLastSelectedItem).mockReturnValue('a.mp4');
+ // Batch delete of a + b: a (the displayed item) deletes fine, b fails and still exists.
+ const { store, dispatched } = buildStore(['a.mp4'], new Set(['b.mp4']));
+
+ await handleDeletions(['a.mp4', 'b.mp4'], store);
+
+ // Before the fix, b.mp4 was excluded as "deleted" and the selection skipped to c.png.
+ expect(getSelectionChange(dispatched)?.payload).toBe('b.mp4');
+ expect(toast).toHaveBeenCalledWith(expect.objectContaining({ status: 'warning' }));
+ });
+
+ it('keeps viewing the displayed video when its delete fails but another selected video was deleted', async () => {
+ vi.mocked(selectLastSelectedItem).mockReturnValue('a.mp4');
+ const { store, dispatched } = buildStore(['a.mp4', 'b.mp4'], new Set(['a.mp4']));
+
+ await handleDeletions(['a.mp4', 'b.mp4'], store);
+
+ // The multi-selection contained a deleted item (b), so the selection is pruned — but it
+ // must land on the still-existing displayed video, not jump to a neighbour.
+ expect(getSelectionChange(dispatched)?.payload).toBe('a.mp4');
+ });
+
+ it('advances to the nearest surviving neighbour when everything requested is deleted', async () => {
+ vi.mocked(selectLastSelectedItem).mockReturnValue('b.mp4');
+ const { store, dispatched } = buildStore(['b.mp4'], new Set());
+
+ await handleDeletions(['b.mp4'], store);
+
+ expect(getSelectionChange(dispatched)?.payload).toBe('a.mp4');
+ expect(imageSelected).toHaveBeenCalledWith('a.mp4');
+ });
+});
+
+describe('handleDeletions node VideoField cleanup', () => {
+ beforeEach(() => {
+ vi.clearAllMocks();
+ vi.mocked(selectCachedGalleryItemNames).mockReturnValue(['a.mp4', 'b.mp4']);
+ vi.mocked(selectLastSelectedItem).mockReturnValue(undefined);
+ });
+
+ it('clears VideoField inputs that reference a confirmed-deleted video', async () => {
+ const nodes = [buildVideoFieldNode('n1', 'a.mp4'), buildVideoFieldNode('n2', 'b.mp4')];
+ const { store, dispatched } = buildStore([], new Set(), nodes);
+
+ await handleDeletions(['a.mp4'], store);
+
+ const changes = getVideoFieldChanges(dispatched);
+ expect(changes).toHaveLength(1);
+ expect(changes[0]?.payload).toEqual({ nodeId: 'n1', fieldName: 'video', value: undefined });
+ });
+
+ it('preserves VideoField inputs for videos whose deletion failed', async () => {
+ const nodes = [buildVideoFieldNode('n1', 'a.mp4'), buildVideoFieldNode('n2', 'b.mp4')];
+ const { store, dispatched } = buildStore([], new Set(['b.mp4']), nodes);
+
+ await handleDeletions(['a.mp4', 'b.mp4'], store);
+
+ const changes = getVideoFieldChanges(dispatched);
+ expect(changes).toHaveLength(1);
+ expect(changes[0]?.payload.nodeId).toBe('n1');
+ });
+
+ it('preserves all VideoField inputs when the whole batch request fails', async () => {
+ const nodes = [buildVideoFieldNode('n1', 'a.mp4')];
+ const { store, dispatched } = buildStore([], new Set(), nodes, true);
+
+ await handleDeletions(['a.mp4'], store);
+
+ expect(getVideoFieldChanges(dispatched)).toHaveLength(0);
+ });
+});
+
+describe('delete dialog dismissal', () => {
+ beforeEach(() => {
+ vi.clearAllMocks();
+ vi.mocked(selectCachedGalleryItemNames).mockReturnValue(['a.mp4']);
+ vi.mocked(selectLastSelectedItem).mockReturnValue(undefined);
+ });
+
+ it('routes generic dialog closure through cancellation so callers settle', () => {
+ const source = readFileSync(fileURLToPath(new URL('../components/DeleteVideoModal.tsx', import.meta.url)), 'utf8');
+
+ expect(source).toContain('onClose={api.cancel}');
+ expect(source).not.toContain('onClose={api.close}');
+ });
+
+ it('rejects the caller when the dialog is dismissed without confirming', async () => {
+ vi.mocked(selectSystemShouldConfirmOnDelete).mockReturnValue(true);
+ const { store } = buildStore([], new Set());
+
+ const promise = deleteVideosWithDialog(['a.mp4'], store);
+ cancelDeletion();
+
+ await expect(promise).rejects.toBe('User canceled');
+ expect(videosApi.endpoints.deleteVideos.initiate).not.toHaveBeenCalled();
+ });
+
+ it('resolves a confirmed deletion even though the accept path also fires onClose (cancel)', async () => {
+ // ConfirmationAlertDialog's accept button invokes acceptCallback() and then
+ // onClose() synchronously, without awaiting the callback. With onClose bound to
+ // cancel, that cancel must be a no-op after confirm — not a rejection of the
+ // deletion the user just accepted.
+ vi.mocked(selectSystemShouldConfirmOnDelete).mockReturnValue(true);
+ const { store } = buildStore([], new Set());
+
+ const promise = deleteVideosWithDialog(['a.mp4'], store);
+ const confirming = confirmDeletion(store);
+ cancelDeletion();
+ await confirming;
+
+ await expect(promise).resolves.toBeUndefined();
+ expect(videosApi.endpoints.deleteVideos.initiate).toHaveBeenCalledTimes(1);
+ });
+});
diff --git a/invokeai/frontend/web/src/features/deleteVideoModal/store/state.ts b/invokeai/frontend/web/src/features/deleteVideoModal/store/state.ts
new file mode 100644
index 00000000000..9821267088e
--- /dev/null
+++ b/invokeai/frontend/web/src/features/deleteVideoModal/store/state.ts
@@ -0,0 +1,173 @@
+import { useStore } from '@nanostores/react';
+import type { AppDispatch, AppStore, RootState } from 'app/store/store';
+import { useAppStore } from 'app/store/storeHooks';
+import { intersection } from 'es-toolkit/compat';
+import { selectLastSelectedItem } from 'features/gallery/store/gallerySelectors';
+import { imageSelected } from 'features/gallery/store/gallerySlice';
+import {
+ pickSelectionAfterDelete,
+ selectCachedGalleryItemNames,
+} from 'features/gallery/store/selectCachedGalleryItemNames';
+import { fieldVideoValueChanged } from 'features/nodes/store/nodesSlice';
+import { isVideoFieldInputInstance } from 'features/nodes/types/field';
+import { isInvocationNode } from 'features/nodes/types/invocation';
+import { selectSystemShouldConfirmOnDelete } from 'features/system/store/systemSlice';
+import { toast } from 'features/toast/toast';
+import { t } from 'i18next';
+import { atom } from 'nanostores';
+import { useMemo } from 'react';
+import { videosApi } from 'services/api/endpoints/videos';
+
+// Parallel of features/deleteImageModal/store/state.ts but trimmed: videos don't show up
+// on canvas layers, ref-image entities, or upscale inputs the way images do, so there is
+// no "usage" analysis to compute and the dialog is a straight confirm. Workflow nodes DO
+// take VideoField inputs, though — confirmed deletions must clear those references.
+
+type DeleteVideosModalState = {
+ video_names: string[];
+ isOpen: boolean;
+ resolve?: () => void;
+ reject?: (reason?: string) => void;
+};
+
+const getInitialState = (): DeleteVideosModalState => ({
+ video_names: [],
+ isOpen: false,
+});
+
+const $deleteModalState = atom(getInitialState());
+
+export const deleteVideosWithDialog = async (video_names: string[], store: AppStore): Promise => {
+ const { getState } = store;
+ const shouldConfirmOnDelete = selectSystemShouldConfirmOnDelete(getState());
+
+ if (!shouldConfirmOnDelete) {
+ await handleDeletions(video_names, store);
+ return;
+ }
+
+ return new Promise((resolve, reject) => {
+ $deleteModalState.set({
+ video_names,
+ isOpen: true,
+ resolve,
+ reject,
+ });
+ });
+};
+
+export const clearNodesVideoFields = (state: RootState, dispatch: AppDispatch, video_name: string) => {
+ state.nodes.present.nodes.forEach((node) => {
+ if (!isInvocationNode(node)) {
+ return;
+ }
+ for (const input of Object.values(node.data.inputs)) {
+ if (isVideoFieldInputInstance(input) && input.value?.video_name === video_name) {
+ dispatch(
+ fieldVideoValueChanged({
+ nodeId: node.data.id,
+ fieldName: input.name,
+ value: undefined,
+ })
+ );
+ }
+ }
+ });
+};
+
+export const handleDeletions = async (video_names: string[], store: AppStore) => {
+ const { dispatch, getState } = store;
+
+ // Snapshot the polymorphic gallery list and the currently-displayed item *before* the
+ // delete fires; once the network call resolves the cache will already have shifted.
+ const stateBefore = getState();
+ const galleryItemNames = selectCachedGalleryItemNames(stateBefore);
+ const lastSelected = selectLastSelectedItem(stateBefore);
+ const lastSelectedIndex =
+ lastSelected && video_names.includes(lastSelected) ? galleryItemNames.indexOf(lastSelected) : -1;
+
+ // One batch request; the backend skips per-video failures and reports what actually
+ // got deleted, so partial failures don't discard the successes.
+ let deletedNames = new Set();
+ try {
+ const result = await dispatch(
+ videosApi.endpoints.deleteVideos.initiate({ video_names }, { track: false })
+ ).unwrap();
+ deletedNames = new Set(result.deleted_videos);
+ if (result.failed_videos.length > 0) {
+ toast({
+ status: 'warning',
+ title: t('toast.videoDeleteFailed'),
+ description: t('toast.videoDeletePartial', { count: result.failed_videos.length }),
+ });
+ }
+ } catch {
+ // The whole request failed — nothing was confirmed deleted, so leave selection and
+ // node references untouched. The mutation is untracked, so this toast is the only
+ // user-visible signal that the delete didn't happen.
+ toast({
+ status: 'error',
+ title: t('toast.videoDeleteFailed'),
+ description: t('toast.videoDeleteFailedDesc'),
+ });
+ }
+
+ // Clear workflow-node VideoField inputs that referenced a now-deleted video. Failed
+ // deletions keep their references — those videos still exist.
+ for (const video_name of deletedNames) {
+ clearNodesVideoFields(getState(), dispatch, video_name);
+ }
+
+ // If anything in the active selection was actually deleted, advance to a still-living
+ // neighbour (prev > next) so the Viewer doesn't drop to its empty-state placeholder.
+ // Failed deletions are excluded: those videos still exist, so the selection must not
+ // jump away from them, and they remain valid replacement candidates.
+ const stateAfter = getState();
+ if (intersection(stateAfter.gallery.selection, [...deletedNames]).length > 0) {
+ if (lastSelected && !deletedNames.has(lastSelected)) {
+ // The displayed item survived (its delete failed) — keep viewing it and just prune
+ // the deleted items from the multi-selection.
+ dispatch(imageSelected(lastSelected));
+ } else {
+ const replacement =
+ lastSelectedIndex >= 0 ? pickSelectionAfterDelete(galleryItemNames, lastSelectedIndex, deletedNames) : null;
+ dispatch(imageSelected(replacement));
+ }
+ }
+};
+
+export const confirmDeletion = async (store: AppStore) => {
+ const state = $deleteModalState.get();
+ // Detach the promise callbacks before any async work: ConfirmationAlertDialog's accept
+ // button invokes acceptCallback() and then onClose() (bound to cancel) synchronously,
+ // and that cancel must not reject a deletion the user just confirmed. Dismissal still
+ // rejects — cancelDeletion only finds a live `reject` when no confirm has run.
+ closeSilently();
+ await handleDeletions(state.video_names, store);
+ state.resolve?.();
+};
+
+export const cancelDeletion = () => {
+ const state = $deleteModalState.get();
+ state.reject?.('User canceled');
+ closeSilently();
+};
+
+const closeSilently = () => {
+ $deleteModalState.set(getInitialState());
+};
+
+export const useDeleteVideoModalState = () => useStore($deleteModalState);
+
+export const useDeleteVideoModalApi = () => {
+ const store = useAppStore();
+ return useMemo(
+ () => ({
+ delete: (video_names: string[]) => deleteVideosWithDialog(video_names, store),
+ confirm: () => confirmDeletion(store),
+ cancel: cancelDeletion,
+ close: closeSilently,
+ }),
+ [store]
+ );
+};
diff --git a/invokeai/frontend/web/src/features/dnd/DndDragPreviewMultipleImage.tsx b/invokeai/frontend/web/src/features/dnd/DndDragPreviewMultipleImage.tsx
index 056ce114112..2dd7d89a6c3 100644
--- a/invokeai/frontend/web/src/features/dnd/DndDragPreviewMultipleImage.tsx
+++ b/invokeai/frontend/web/src/features/dnd/DndDragPreviewMultipleImage.tsx
@@ -8,23 +8,28 @@ import { createPortal } from 'react-dom';
import { useTranslation } from 'react-i18next';
import type { Param0 } from 'tsafe';
-const DndDragPreviewMultipleImage = memo(({ image_names }: { image_names: string[] }) => {
- const { t } = useTranslation();
- return (
-
- {image_names.length}
- {t('parameters.images_withCount', { count: image_names.length })}
-
- );
-});
+const DndDragPreviewMultipleImage = memo(
+ ({ image_names, video_names }: { image_names: string[]; video_names: string[] }) => {
+ const { t } = useTranslation();
+ // The whole mixed selection moves on drop (the board drop handler dispatches both
+ // mutations), so the preview counts both kinds to match the "Images/Videos" label.
+ const count = image_names.length + video_names.length;
+ return (
+
+ {count}
+ {t('parameters.images_withCount', { count })}
+
+ );
+ }
+);
DndDragPreviewMultipleImage.displayName = 'DndDragPreviewMultipleImage';
@@ -32,10 +37,14 @@ export type DndDragPreviewMultipleImageState = {
type: 'multiple-image';
container: HTMLElement;
image_names: string[];
+ video_names: string[];
};
export const createMultipleImageDragPreview = (arg: DndDragPreviewMultipleImageState) =>
- createPortal( , arg.container);
+ createPortal(
+ ,
+ arg.container
+ );
type SetMultipleDragPreviewArg = {
multipleImageDndData: MultipleImageDndSourceData;
@@ -51,7 +60,12 @@ export const setMultipleImageDragPreview = ({
const { nativeSetDragImage, source, location } = onGenerateDragPreviewArgs;
setCustomNativeDragPreview({
render({ container }) {
- setDragPreviewState({ type: 'multiple-image', container, image_names: multipleImageDndData.payload.image_names });
+ setDragPreviewState({
+ type: 'multiple-image',
+ container,
+ image_names: multipleImageDndData.payload.image_names,
+ video_names: multipleImageDndData.payload.video_names,
+ });
return () => setDragPreviewState(null);
},
nativeSetDragImage,
diff --git a/invokeai/frontend/web/src/features/dnd/DndImage.tsx b/invokeai/frontend/web/src/features/dnd/DndImage.tsx
index ecc11ee5975..5d70e43434c 100644
--- a/invokeai/frontend/web/src/features/dnd/DndImage.tsx
+++ b/invokeai/frontend/web/src/features/dnd/DndImage.tsx
@@ -3,6 +3,7 @@ import { draggable } from '@atlaskit/pragmatic-drag-and-drop/element/adapter';
import type { ImageProps, SystemStyleObject } from '@invoke-ai/ui-library';
import { Image } from '@invoke-ai/ui-library';
import { useMiddleClickOpenInNewTab } from 'common/hooks/useMiddleClickOpenInNewTab';
+import { useMediaUrl } from 'features/auth/store/mediaCookieRefresh';
import { singleImageDndSource } from 'features/dnd/dnd';
import type { DndDragPreviewSingleImageState } from 'features/dnd/DndDragPreviewSingleImage';
import { createSingleImageDragPreview, setSingleImageDragPreview } from 'features/dnd/DndDragPreviewSingleImage';
@@ -31,6 +32,8 @@ export const DndImage = memo(
const ref = useRef(null);
useImperativeHandle(forwardedRef, () => ref.current!, []);
const [dragPreviewState, setDragPreviewState] = useState(null);
+ const imageUrl = useMediaUrl(asThumbnail ? imageDTO.thumbnail_url : imageDTO.image_url);
+ const fallbackUrl = useMediaUrl(asThumbnail ? undefined : imageDTO.thumbnail_url);
useMiddleClickOpenInNewTab(ref, imageDTO.image_url);
@@ -70,8 +73,8 @@ export const DndImage = memo(
{
-// const result = sizeInBytes / (1024 * 1024);
-// return +result.toFixed(decimalsNum);
-// };
-
-const zUploadFile = z
- .custom()
- // .refine(
- // (file) => {
- // return sizeInMB(file.size) <= MAX_IMAGE_SIZE;
- // },
- // () => ({ message: `The maximum image size is ${MAX_IMAGE_SIZE}MB` })
- // )
- .refine(
- (file) => {
- return ACCEPTED_IMAGE_TYPES.includes(file.type.toLowerCase());
- },
- { message: `File type is not supported` }
- )
- .refine(
- (file) => {
- return ACCEPTED_FILE_EXTENSIONS.some((ext) => file.name.toLowerCase().endsWith(ext));
- },
- { message: `File extension is not supported` }
- );
-
const sx = {
position: 'absolute',
top: 2,
@@ -73,7 +46,9 @@ export const FullscreenDropzone = memo(() => {
const parseResult = z.array(zUploadFile).safeParse(files);
if (!parseResult.success) {
- const description = t('toast.uploadFailedInvalidUploadDesc');
+ // The fullscreen surface accepts videos too, so its invalid-upload message must
+ // mention MP4 (unlike image-only upload fields, which keep the image-only text).
+ const description = t('toast.uploadFailedInvalidMediaUploadDesc');
toast({
id: 'UPLOAD_FAILED',
@@ -86,25 +61,49 @@ export const FullscreenDropzone = memo(() => {
const focusedRegion = getFocusedRegion();
- // While on the canvas tab and when pasting a single image, canvas may want to create a new layer. Let it handle
- // the paste event.
- const [firstImageFile] = files;
- if (focusedRegion === 'canvas' && activeTab === 'canvas' && files.length === 1 && firstImageFile) {
- setFileToPaste(firstImageFile);
+ // While on the canvas tab and when pasting a single image (not a video — the canvas can't
+ // host videos), canvas may want to create a new layer. Let it handle the paste event.
+ const [firstFile] = files;
+ if (
+ focusedRegion === 'canvas' &&
+ activeTab === 'canvas' &&
+ files.length === 1 &&
+ firstFile &&
+ !isVideoFile(firstFile)
+ ) {
+ setFileToPaste(firstFile);
return;
}
const autoAddBoardId = selectAutoAddBoardId(getState());
+ const boardId = autoAddBoardId === 'none' ? undefined : autoAddBoardId;
+
+ // Split files by media type so each batch goes through its own uploader. Image and video
+ // uploaders are independent — they each update their own RTK cache + invalidate the gallery.
+ const imageFiles = files.filter((f) => !isVideoFile(f));
+ const videoFiles = files.filter((f) => isVideoFile(f));
+
+ if (imageFiles.length > 0) {
+ const imageUploadArgs: UploadImageArg[] = imageFiles.map((file, i) => ({
+ file,
+ image_category: 'user',
+ is_intermediate: false,
+ board_id: boardId,
+ isFirstUploadOfBatch: i === 0,
+ }));
+ uploadImages(imageUploadArgs);
+ }
- const uploadArgs: UploadImageArg[] = files.map((file, i) => ({
- file,
- image_category: 'user',
- is_intermediate: false,
- board_id: autoAddBoardId === 'none' ? undefined : autoAddBoardId,
- isFirstUploadOfBatch: i === 0,
- }));
-
- uploadImages(uploadArgs);
+ if (videoFiles.length > 0) {
+ const videoUploadArgs: UploadVideoArg[] = videoFiles.map((file, i) => ({
+ file,
+ video_category: 'user',
+ is_intermediate: false,
+ board_id: boardId,
+ isFirstUploadOfBatch: i === 0,
+ }));
+ uploadVideos(videoUploadArgs);
+ }
},
[activeTab, t]
);
@@ -180,7 +179,7 @@ const DropLabel = memo(() => {
return (
{t('gallery.dropToUpload')}
- {t('toast.imagesWillBeAddedTo', { boardName })}
+ {t('toast.itemsWillBeAddedTo', { boardName })}
);
});
diff --git a/invokeai/frontend/web/src/features/dnd/dnd.ts b/invokeai/frontend/web/src/features/dnd/dnd.ts
index 8ed60799407..d5494592694 100644
--- a/invokeai/frontend/web/src/features/dnd/dnd.ts
+++ b/invokeai/frontend/web/src/features/dnd/dnd.ts
@@ -9,20 +9,25 @@ import { selectComparisonImages } from 'features/gallery/components/ImageViewer/
import type { BoardId } from 'features/gallery/store/types';
import {
addImagesToBoard,
+ addVideosToBoard,
+ addVideoToBoard,
createNewCanvasEntityFromImage,
newCanvasFromImage,
removeImagesFromBoard,
+ removeVideoFromBoard,
+ removeVideosFromBoard,
replaceCanvasEntityObjectsWithImage,
setComparisonImage,
setGlobalReferenceImage,
setNodeImageFieldImage,
+ setNodeVideoFieldVideo,
setRegionalGuidanceReferenceImage,
setUpscaleInitialImage,
} from 'features/imageActions/actions';
import { fieldImageCollectionValueChanged } from 'features/nodes/store/nodesSlice';
import { selectFieldInputInstanceSafe, selectNodesSlice } from 'features/nodes/store/selectors';
import { type FieldIdentifier, isImageFieldCollectionInputInstance } from 'features/nodes/types/field';
-import type { ImageDTO } from 'services/api/types';
+import type { ImageDTO, VideoDTO } from 'services/api/types';
import type { JsonObject } from 'type-fest';
const log = logger('dnd');
@@ -83,12 +88,26 @@ export const singleImageDndSource: DndSource = {
};
//#endregion
+//#region Single Video
+const _singleVideo = buildTypeAndKey('single-video');
+type SingleVideoDndSourceData = DndData;
+export const singleVideoDndSource: DndSource = {
+ ..._singleVideo,
+ typeGuard: buildTypeGuard(_singleVideo.key),
+ getData: buildGetData(_singleVideo.key, _singleVideo.type),
+};
+//#endregion
+
//#region Multiple Image
+// `image_names` is the primary payload (used by the drag preview heading and the image-field
+// collection drop target). `video_names` rides along so that mixed selections dragged from an
+// image thumbnail still move the videos to the board — the board drop handler dispatches both
+// mutations when both arrays are populated.
const _multipleImage = buildTypeAndKey('multiple-image');
export type MultipleImageDndSourceData = DndData<
typeof _multipleImage.type,
typeof _multipleImage.key,
- { image_names: string[]; board_id: BoardId }
+ { image_names: string[]; video_names: string[]; board_id: BoardId }
>;
export const multipleImageDndSource: DndSource = {
..._multipleImage,
@@ -97,6 +116,22 @@ export const multipleImageDndSource: DndSource = {
};
//#endregion
+//#region Multiple Video
+// Symmetric to MultipleImageDndSourceData: `video_names` is the primary payload, `image_names`
+// rides along for mixed selections dragged from a video thumbnail.
+const _multipleVideo = buildTypeAndKey('multiple-video');
+type MultipleVideoDndSourceData = DndData<
+ typeof _multipleVideo.type,
+ typeof _multipleVideo.key,
+ { video_names: string[]; image_names: string[]; board_id: BoardId }
+>;
+export const multipleVideoDndSource: DndSource = {
+ ..._multipleVideo,
+ typeGuard: buildTypeGuard(_multipleVideo.key),
+ getData: buildGetData(_multipleVideo.key, _multipleVideo.type),
+};
+//#endregion
+
//#region Single Reference Image (reorder)
const _singleRefImage = buildTypeAndKey('single-ref-image');
type SingleRefImageDndSourceData = DndData;
@@ -273,6 +308,32 @@ export const setNodeImageFieldImageDndTarget: DndTarget;
+export const setNodeVideoFieldVideoDndTarget: DndTarget =
+ {
+ ..._setNodeVideoFieldVideo,
+ typeGuard: buildTypeGuard(_setNodeVideoFieldVideo.key),
+ getData: buildGetData(_setNodeVideoFieldVideo.key, _setNodeVideoFieldVideo.type),
+ isValid: ({ sourceData }) => {
+ if (singleVideoDndSource.typeGuard(sourceData)) {
+ return true;
+ }
+ return false;
+ },
+ handler: ({ sourceData, targetData, dispatch }) => {
+ const { videoDTO } = sourceData.payload;
+ const { fieldIdentifier } = targetData.payload;
+ setNodeVideoFieldVideo({ fieldIdentifier, videoDTO, dispatch });
+ },
+ };
+//#endregion
+
//#region Add Images to Image Collection Node Field
const _addImagesToNodeImageFieldCollection = buildTypeAndKey('add-images-to-image-collection-node-field');
export type AddImagesToNodeImageFieldCollection = DndData<
@@ -495,7 +556,7 @@ export type AddImageToBoardDndTargetData = DndData<
>;
export const addImageToBoardDndTarget: DndTarget<
AddImageToBoardDndTargetData,
- SingleImageDndSourceData | MultipleImageDndSourceData
+ SingleImageDndSourceData | MultipleImageDndSourceData | SingleVideoDndSourceData | MultipleVideoDndSourceData
> = {
..._addToBoard,
typeGuard: buildTypeGuard(_addToBoard.key),
@@ -518,6 +579,26 @@ export const addImageToBoardDndTarget: DndTarget<
}
return canMoveFromSourceBoard(currentBoard, getState);
}
+ if (singleVideoDndSource.typeGuard(sourceData)) {
+ const currentBoard = sourceData.payload.videoDTO.board_id ?? 'none';
+ const destinationBoard = targetData.payload.boardId;
+ if (currentBoard === destinationBoard) {
+ return false;
+ }
+ // Same source-board permission check as images. Backend additionally
+ // enforces _assert_video_direct_owner — a stricter check the client can't
+ // perform without each video's owner, so we let those failures bubble up
+ // through the mutation rather than blocking the drop preemptively.
+ return canMoveFromSourceBoard(currentBoard, getState);
+ }
+ if (multipleVideoDndSource.typeGuard(sourceData)) {
+ const currentBoard = sourceData.payload.board_id;
+ const destinationBoard = targetData.payload.boardId;
+ if (currentBoard === destinationBoard) {
+ return false;
+ }
+ return canMoveFromSourceBoard(currentBoard, getState);
+ }
return false;
},
handler: ({ sourceData, targetData, dispatch }) => {
@@ -528,9 +609,31 @@ export const addImageToBoardDndTarget: DndTarget<
}
if (multipleImageDndSource.typeGuard(sourceData)) {
- const { image_names } = sourceData.payload;
+ const { image_names, video_names } = sourceData.payload;
const { boardId } = targetData.payload;
- addImagesToBoard({ image_names, boardId, dispatch });
+ if (image_names.length > 0) {
+ addImagesToBoard({ image_names, boardId, dispatch });
+ }
+ if (video_names.length > 0) {
+ addVideosToBoard({ video_names, boardId, dispatch });
+ }
+ }
+
+ if (singleVideoDndSource.typeGuard(sourceData)) {
+ const { videoDTO } = sourceData.payload;
+ const { boardId } = targetData.payload;
+ addVideoToBoard({ video_name: videoDTO.video_name, boardId, dispatch });
+ }
+
+ if (multipleVideoDndSource.typeGuard(sourceData)) {
+ const { video_names, image_names } = sourceData.payload;
+ const { boardId } = targetData.payload;
+ if (video_names.length > 0) {
+ addVideosToBoard({ video_names, boardId, dispatch });
+ }
+ if (image_names.length > 0) {
+ addImagesToBoard({ image_names, boardId, dispatch });
+ }
}
},
};
@@ -546,7 +649,7 @@ export type RemoveImageFromBoardDndTargetData = DndData<
>;
export const removeImageFromBoardDndTarget: DndTarget<
RemoveImageFromBoardDndTargetData,
- SingleImageDndSourceData | MultipleImageDndSourceData
+ SingleImageDndSourceData | MultipleImageDndSourceData | SingleVideoDndSourceData | MultipleVideoDndSourceData
> = {
..._removeFromBoard,
typeGuard: buildTypeGuard(_removeFromBoard.key),
@@ -569,6 +672,22 @@ export const removeImageFromBoardDndTarget: DndTarget<
return canMoveFromSourceBoard(currentBoard, getState);
}
+ if (singleVideoDndSource.typeGuard(sourceData)) {
+ const currentBoard = sourceData.payload.videoDTO.board_id ?? 'none';
+ if (currentBoard === 'none') {
+ return false;
+ }
+ return canMoveFromSourceBoard(currentBoard, getState);
+ }
+
+ if (multipleVideoDndSource.typeGuard(sourceData)) {
+ const currentBoard = sourceData.payload.board_id;
+ if (currentBoard === 'none') {
+ return false;
+ }
+ return canMoveFromSourceBoard(currentBoard, getState);
+ }
+
return false;
},
handler: ({ sourceData, dispatch }) => {
@@ -578,8 +697,28 @@ export const removeImageFromBoardDndTarget: DndTarget<
}
if (multipleImageDndSource.typeGuard(sourceData)) {
- const { image_names } = sourceData.payload;
- removeImagesFromBoard({ image_names, dispatch });
+ const { image_names, video_names } = sourceData.payload;
+ if (image_names.length > 0) {
+ removeImagesFromBoard({ image_names, dispatch });
+ }
+ if (video_names.length > 0) {
+ removeVideosFromBoard({ video_names, dispatch });
+ }
+ }
+
+ if (singleVideoDndSource.typeGuard(sourceData)) {
+ const { videoDTO } = sourceData.payload;
+ removeVideoFromBoard({ video_name: videoDTO.video_name, dispatch });
+ }
+
+ if (multipleVideoDndSource.typeGuard(sourceData)) {
+ const { video_names, image_names } = sourceData.payload;
+ if (video_names.length > 0) {
+ removeVideosFromBoard({ video_names, dispatch });
+ }
+ if (image_names.length > 0) {
+ removeImagesFromBoard({ image_names, dispatch });
+ }
}
},
};
@@ -592,6 +731,7 @@ export const dndTargets = [
setRegionalGuidanceReferenceImageDndTarget,
setUpscaleInitialImageDndTarget,
setNodeImageFieldImageDndTarget,
+ setNodeVideoFieldVideoDndTarget,
addImagesToNodeImageFieldCollectionDndTarget,
setComparisonImageDndTarget,
newCanvasEntityFromImageDndTarget,
diff --git a/invokeai/frontend/web/src/features/dnd/fullscreenDropzoneAccept.test.ts b/invokeai/frontend/web/src/features/dnd/fullscreenDropzoneAccept.test.ts
new file mode 100644
index 00000000000..ec6723a94ba
--- /dev/null
+++ b/invokeai/frontend/web/src/features/dnd/fullscreenDropzoneAccept.test.ts
@@ -0,0 +1,34 @@
+/**
+ * Regression tests for the fullscreen drag-drop / paste validator (PR #9163 review).
+ *
+ * The bug: the validator required BOTH an accepted MIME type AND an accepted extension, so a
+ * valid `clip.mp4` whose browser supplied an empty or generic `File.type` was rejected before
+ * upload even though the backend accepts MP4 by extension alone.
+ */
+import { describe, expect, it } from 'vitest';
+
+import { zUploadFile } from './fullscreenDropzoneAccept';
+
+// The validator only reads `type` and `name`, so a plain object stands in for a DOM File.
+const fakeFile = (name: string, type: string): File => ({ name, type }) as File;
+
+describe('zUploadFile', () => {
+ it('accepts an MP4 with an empty MIME type by its extension', () => {
+ expect(zUploadFile.safeParse(fakeFile('clip.mp4', '')).success).toBe(true);
+ });
+
+ it('accepts supported media by MIME type alone', () => {
+ expect(zUploadFile.safeParse(fakeFile('pasted-blob', 'image/png')).success).toBe(true);
+ expect(zUploadFile.safeParse(fakeFile('pasted-blob', 'video/mp4')).success).toBe(true);
+ });
+
+ it('rejects video containers the backend does not accept', () => {
+ expect(zUploadFile.safeParse(fakeFile('clip.webm', 'video/webm')).success).toBe(false);
+ expect(zUploadFile.safeParse(fakeFile('clip.mov', 'video/quicktime')).success).toBe(false);
+ expect(zUploadFile.safeParse(fakeFile('clip.mkv', 'video/x-matroska')).success).toBe(false);
+ });
+
+ it('rejects non-media files', () => {
+ expect(zUploadFile.safeParse(fakeFile('notes.txt', 'text/plain')).success).toBe(false);
+ });
+});
diff --git a/invokeai/frontend/web/src/features/dnd/fullscreenDropzoneAccept.ts b/invokeai/frontend/web/src/features/dnd/fullscreenDropzoneAccept.ts
new file mode 100644
index 00000000000..0587b9dd1a0
--- /dev/null
+++ b/invokeai/frontend/web/src/features/dnd/fullscreenDropzoneAccept.ts
@@ -0,0 +1,12 @@
+import { isAcceptedUploadFile } from 'common/util/uploadMediaAccept';
+import { z } from 'zod';
+
+/**
+ * Validates files entering via the fullscreen drag-drop / paste path.
+ *
+ * MIME type and filename extension each suffice on their own (`isAcceptedUploadFile`):
+ * browsers sometimes supply an empty or generic `File.type` — e.g. a `clip.mp4` dragged
+ * from some file managers — and requiring both signals used to reject files the backend
+ * upload routes would happily accept.
+ */
+export const zUploadFile = z.custom().refine(isAcceptedUploadFile, { message: 'File type is not supported' });
diff --git a/invokeai/frontend/web/src/features/dnd/useDndMonitor.ts b/invokeai/frontend/web/src/features/dnd/useDndMonitor.ts
index 24d6bea1680..94a875bfe35 100644
--- a/invokeai/frontend/web/src/features/dnd/useDndMonitor.ts
+++ b/invokeai/frontend/web/src/features/dnd/useDndMonitor.ts
@@ -4,7 +4,13 @@ import { logger } from 'app/logging/logger';
import { getStore } from 'app/store/nanostores/store';
import { useAssertSingleton } from 'common/hooks/useAssertSingleton';
import { parseify } from 'common/util/serialize';
-import { dndTargets, multipleImageDndSource, singleImageDndSource } from 'features/dnd/dnd';
+import {
+ dndTargets,
+ multipleImageDndSource,
+ multipleVideoDndSource,
+ singleImageDndSource,
+ singleVideoDndSource,
+} from 'features/dnd/dnd';
import { useEffect } from 'react';
const log = logger('dnd');
@@ -18,8 +24,15 @@ export const useDndMonitor = () => {
canMonitor: ({ source }) => {
const sourceData = source.data;
- // Check for allowed sources
- if (!singleImageDndSource.typeGuard(sourceData) && !multipleImageDndSource.typeGuard(sourceData)) {
+ // Check for allowed sources. Without multipleVideoDndSource here, multi-video
+ // drags would be ignored at the global monitor level — the drop would silently
+ // do nothing because the handler never fires.
+ if (
+ !singleImageDndSource.typeGuard(sourceData) &&
+ !multipleImageDndSource.typeGuard(sourceData) &&
+ !singleVideoDndSource.typeGuard(sourceData) &&
+ !multipleVideoDndSource.typeGuard(sourceData)
+ ) {
return false;
}
diff --git a/invokeai/frontend/web/src/features/gallery/components/Boards/BoardsList/BoardTooltip.tsx b/invokeai/frontend/web/src/features/gallery/components/Boards/BoardsList/BoardTooltip.tsx
index 8877b22612f..1960eb7f07e 100644
--- a/invokeai/frontend/web/src/features/gallery/components/Boards/BoardsList/BoardTooltip.tsx
+++ b/invokeai/frontend/web/src/features/gallery/components/Boards/BoardsList/BoardTooltip.tsx
@@ -1,13 +1,17 @@
-import { Flex, Image, Text } from '@invoke-ai/ui-library';
+import { Flex, Icon, Image, Text } from '@invoke-ai/ui-library';
import { skipToken } from '@reduxjs/toolkit/query';
+import { useMediaUrl } from 'features/auth/store/mediaCookieRefresh';
import { useTranslation } from 'react-i18next';
+import { PiImageSquare } from 'react-icons/pi';
import { useGetImageDTOQuery } from 'services/api/endpoints/images';
+import { useGetVideoDTOQuery } from 'services/api/endpoints/videos';
import type { BoardDTO } from 'services/api/types';
type Props = {
board: BoardDTO | null;
boardCounts: {
image_count: number;
+ video_count: number;
asset_count: number;
};
};
@@ -15,25 +19,40 @@ type Props = {
export const BoardTooltip = ({ board, boardCounts }: Props) => {
const { t } = useTranslation();
- const { currentData: coverImage } = useGetImageDTOQuery(board?.cover_image_name ?? skipToken);
+ // Backend picks a single cover — either an image or a video. Prefer the video when set.
+ const { currentData: coverVideo } = useGetVideoDTOQuery(board?.cover_video_name ?? skipToken);
+ const { currentData: coverImage } = useGetImageDTOQuery(
+ board?.cover_video_name ? skipToken : (board?.cover_image_name ?? skipToken)
+ );
+ const thumbnailUrl = coverVideo?.thumbnail_url ?? coverImage?.thumbnail_url;
+ const refreshedThumbnailUrl = useMediaUrl(thumbnailUrl);
return (
- {coverImage && (
+ {refreshedThumbnailUrl && (
+
+
+ }
/>
)}
{board && {board.board_name} }
{t('boards.imagesWithCount', { count: boardCounts.image_count })},{' '}
+ {t('boards.videosWithCount', { count: boardCounts.video_count })},{' '}
{t('boards.assetsWithCount', { count: boardCounts.asset_count })}
{board?.archived && ({t('boards.archived')}) }
diff --git a/invokeai/frontend/web/src/features/gallery/components/Boards/BoardsList/GalleryBoard.tsx b/invokeai/frontend/web/src/features/gallery/components/Boards/BoardsList/GalleryBoard.tsx
index 10fbe618322..24c4f457d24 100644
--- a/invokeai/frontend/web/src/features/gallery/components/Boards/BoardsList/GalleryBoard.tsx
+++ b/invokeai/frontend/web/src/features/gallery/components/Boards/BoardsList/GalleryBoard.tsx
@@ -3,6 +3,7 @@ import { Box, Flex, Icon, Image, Text, Tooltip } from '@invoke-ai/ui-library';
import { skipToken } from '@reduxjs/toolkit/query';
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
import { selectCurrentUser } from 'features/auth/store/authSlice';
+import { useMediaUrl } from 'features/auth/store/mediaCookieRefresh';
import type { AddImageToBoardDndTargetData } from 'features/dnd/dnd';
import { addImageToBoardDndTarget } from 'features/dnd/dnd';
import { DndDropTarget } from 'features/dnd/DndDropTarget';
@@ -20,6 +21,7 @@ import { memo, useCallback, useMemo } from 'react';
import { useTranslation } from 'react-i18next';
import { PiArchiveBold, PiGlobeBold, PiImageSquare, PiShareNetworkBold } from 'react-icons/pi';
import { useGetImageDTOQuery } from 'services/api/endpoints/images';
+import { useGetVideoDTOQuery } from 'services/api/endpoints/videos';
import { useBoardAccess } from 'services/api/hooks/useBoardAccess';
import type { BoardDTO } from 'services/api/types';
@@ -53,9 +55,13 @@ const GalleryBoard = ({ board, isSelected }: GalleryBoardProps) => {
[board.board_id]
);
+ // The tooltip gets split counts so videos aren't mislabeled as images; the headline
+ // number below stays combined (images + videos) so a video-only board doesn't read
+ // as empty in the gallery list.
const boardCounts = useMemo(
() => ({
image_count: board.image_count,
+ video_count: board.video_count ?? 0,
asset_count: board.asset_count,
}),
[board]
@@ -118,7 +124,7 @@ const GalleryBoard = ({ board, isSelected }: GalleryBoardProps) => {
)}
- {board.image_count} | {board.asset_count}
+ {board.image_count + (board.video_count ?? 0)} | {board.asset_count}
@@ -138,25 +144,40 @@ const GalleryBoard = ({ board, isSelected }: GalleryBoardProps) => {
export default memo(GalleryBoard);
const CoverImage = ({ board }: { board: BoardDTO }) => {
- const { currentData: coverImage } = useGetImageDTOQuery(board.cover_image_name ?? skipToken);
+ // Backend picks a single cover — either an image or a video — so at most one of these
+ // queries fires (the other is `skipToken`). The video case takes precedence when set.
+ const { currentData: coverVideo } = useGetVideoDTOQuery(board.cover_video_name ?? skipToken);
+ const { currentData: coverImage } = useGetImageDTOQuery(
+ board.cover_video_name ? skipToken : (board.cover_image_name ?? skipToken)
+ );
+
+ const thumbnailUrl = coverVideo?.thumbnail_url ?? coverImage?.thumbnail_url;
+ const refreshedThumbnailUrl = useMediaUrl(thumbnailUrl);
+
+ // Also used as the Image's error fallback: thumbnail generation is best-effort on the
+ // backend (a video can exist whose thumbnail 404s), so a broken URL must degrade to
+ // the same icon as a missing one.
+ const fallbackIcon = (
+
+
+
+ );
- if (coverImage) {
+ if (refreshedThumbnailUrl) {
return (
);
}
- return (
-
-
-
- );
+ return fallbackIcon;
};
diff --git a/invokeai/frontend/web/src/features/gallery/components/Boards/BoardsList/NoBoardBoard.tsx b/invokeai/frontend/web/src/features/gallery/components/Boards/BoardsList/NoBoardBoard.tsx
index 900799f8ed7..49032db7605 100644
--- a/invokeai/frontend/web/src/features/gallery/components/Boards/BoardsList/NoBoardBoard.tsx
+++ b/invokeai/frontend/web/src/features/gallery/components/Boards/BoardsList/NoBoardBoard.tsx
@@ -15,7 +15,11 @@ import {
import { autoAddBoardIdChanged, boardIdSelected } from 'features/gallery/store/gallerySlice';
import { memo, useCallback, useMemo } from 'react';
import { useTranslation } from 'react-i18next';
-import { useGetBoardAssetsTotalQuery, useGetBoardImagesTotalQuery } from 'services/api/endpoints/boards';
+import {
+ useGetBoardAssetsTotalQuery,
+ useGetBoardImagesTotalQuery,
+ useGetBoardVideosTotalQuery,
+} from 'services/api/endpoints/boards';
import { useBoardName } from 'services/api/hooks/useBoardName';
interface Props {
@@ -33,11 +37,18 @@ const NoBoardBoard = memo(({ isSelected }: Props) => {
return { imagesTotal: data?.total ?? 0 };
},
});
+ const { videosTotal } = useGetBoardVideosTotalQuery('none', {
+ selectFromResult: ({ data }) => {
+ return { videosTotal: data?.total ?? 0 };
+ },
+ });
const { assetsTotal } = useGetBoardAssetsTotalQuery('none', {
selectFromResult: ({ data }) => {
return { assetsTotal: data?.total ?? 0 };
},
});
+ // Videos share the "images" headline count so a board with only videos doesn't read as empty.
+ const imagesAndVideosTotal = imagesTotal + videosTotal;
const autoAddBoardId = useAppSelector(selectAutoAddBoardId);
const autoAssignBoardOnClick = useAppSelector(selectAutoAssignBoardOnClick);
const boardSearchText = useAppSelector(selectBoardSearchText);
@@ -62,7 +73,12 @@ const NoBoardBoard = memo(({ isSelected }: Props) => {
{(ref) => (
}
+ label={
+
+ }
openDelay={1000}
placement="right"
closeOnScroll
@@ -103,7 +119,7 @@ const NoBoardBoard = memo(({ isSelected }: Props) => {
{autoAddBoardId === 'none' && }
- {imagesTotal} | {assetsTotal}
+ {imagesAndVideosTotal} | {assetsTotal}
diff --git a/invokeai/frontend/web/src/features/gallery/components/Boards/BoardsList/VirtualBoardItem.tsx b/invokeai/frontend/web/src/features/gallery/components/Boards/BoardsList/VirtualBoardItem.tsx
index d85c90f7dc1..8f5c94b607c 100644
--- a/invokeai/frontend/web/src/features/gallery/components/Boards/BoardsList/VirtualBoardItem.tsx
+++ b/invokeai/frontend/web/src/features/gallery/components/Boards/BoardsList/VirtualBoardItem.tsx
@@ -2,11 +2,14 @@ import type { SystemStyleObject } from '@invoke-ai/ui-library';
import { Box, Flex, Icon, Image, Text, Tooltip } from '@invoke-ai/ui-library';
import { skipToken } from '@reduxjs/toolkit/query';
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
+import { useMediaUrl } from 'features/auth/store/mediaCookieRefresh';
import { selectSelectedBoardId } from 'features/gallery/store/gallerySelectors';
import { boardIdSelected } from 'features/gallery/store/gallerySlice';
import { memo, useCallback } from 'react';
+import { useTranslation } from 'react-i18next';
import { PiCalendarBold, PiImageSquare } from 'react-icons/pi';
import { useGetImageDTOQuery } from 'services/api/endpoints/images';
+import { useGetVideoDTOQuery } from 'services/api/endpoints/videos';
import type { VirtualSubBoard } from 'services/api/endpoints/virtual_boards';
const _hover: SystemStyleObject = {
@@ -19,6 +22,7 @@ interface VirtualBoardItemProps {
const VirtualBoardItem = ({ board }: VirtualBoardItemProps) => {
const dispatch = useAppDispatch();
+ const { t } = useTranslation();
const selectedBoardId = useAppSelector(selectSelectedBoardId);
const isSelected = selectedBoardId === board.virtual_board_id;
@@ -28,15 +32,14 @@ const VirtualBoardItem = ({ board }: VirtualBoardItemProps) => {
}
}, [selectedBoardId, board.virtual_board_id, dispatch]);
+ const tooltip = `${board.date} — ${t('boards.imagesWithCount', { count: board.image_count })}, ${t(
+ 'boards.videosWithCount',
+ { count: board.video_count }
+ )}, ${t('boards.assetsWithCount', { count: board.asset_count })}`;
+
return (
-
+
{
w="full"
h="full"
>
-
+
{board.board_name}
@@ -60,7 +63,7 @@ const VirtualBoardItem = ({ board }: VirtualBoardItemProps) => {
- {board.image_count} | {board.asset_count}
+ {board.image_count + board.video_count} | {board.asset_count}
@@ -71,26 +74,45 @@ const VirtualBoardItem = ({ board }: VirtualBoardItemProps) => {
export default memo(VirtualBoardItem);
-const CoverImage = ({ coverImageName }: { coverImageName: string | null }) => {
- const { currentData: coverImage } = useGetImageDTOQuery(coverImageName ?? skipToken);
+const CoverImage = ({
+ coverImageName,
+ coverVideoName,
+}: {
+ coverImageName: string | null;
+ coverVideoName: string | null;
+}) => {
+ // Backend picks a single cover — the newest item of the date, image or video — so at most
+ // one of these queries fires (the other is `skipToken`).
+ const { currentData: coverVideo } = useGetVideoDTOQuery(coverVideoName ?? skipToken);
+ const { currentData: coverImage } = useGetImageDTOQuery(coverVideoName ? skipToken : (coverImageName ?? skipToken));
+
+ const thumbnailUrl = coverVideo?.thumbnail_url ?? coverImage?.thumbnail_url;
+ const refreshedThumbnailUrl = useMediaUrl(thumbnailUrl);
+
+ // Also used as the Image's error fallback: thumbnail generation is best-effort on the
+ // backend (a video can exist whose thumbnail 404s), so a broken URL must degrade to
+ // the same icon as a missing one.
+ const fallbackIcon = (
+
+
+
+ );
- if (coverImage) {
+ if (refreshedThumbnailUrl) {
return (
);
}
- return (
-
-
-
- );
+ return fallbackIcon;
};
diff --git a/invokeai/frontend/web/src/features/gallery/components/Boards/BoardsList/VirtualBoardSection.tsx b/invokeai/frontend/web/src/features/gallery/components/Boards/BoardsList/VirtualBoardSection.tsx
index bdadaf77fda..78d2c842639 100644
--- a/invokeai/frontend/web/src/features/gallery/components/Boards/BoardsList/VirtualBoardSection.tsx
+++ b/invokeai/frontend/web/src/features/gallery/components/Boards/BoardsList/VirtualBoardSection.tsx
@@ -3,6 +3,7 @@ import { createSelector } from '@reduxjs/toolkit';
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
import { selectGallerySlice, virtualBoardsSectionOpenChanged } from 'features/gallery/store/gallerySlice';
import { memo, useCallback } from 'react';
+import { useTranslation } from 'react-i18next';
import { PiCalendarBold, PiCaretDownBold, PiCaretRightBold } from 'react-icons/pi';
import { useListVirtualBoardsByDateQuery } from 'services/api/endpoints/virtual_boards';
@@ -15,6 +16,7 @@ const selectVirtualBoardsSectionOpen = createSelector(
);
export const VirtualBoardSection = memo(() => {
+ const { t } = useTranslation();
const dispatch = useAppDispatch();
const showVirtualBoards = useAppSelector(selectShowVirtualBoards);
const isOpen = useAppSelector(selectVirtualBoardsSectionOpen);
@@ -37,13 +39,13 @@ export const VirtualBoardSection = memo(() => {
- By Date
+ {t('boards.byDate')}
: }
onClick={toggleOpen}
/>
diff --git a/invokeai/frontend/web/src/features/gallery/components/Boards/DeleteBoardModal.tsx b/invokeai/frontend/web/src/features/gallery/components/Boards/DeleteBoardModal.tsx
index b7d99301e7a..fd1480defd0 100644
--- a/invokeai/frontend/web/src/features/gallery/components/Boards/DeleteBoardModal.tsx
+++ b/invokeai/frontend/web/src/features/gallery/components/Boards/DeleteBoardModal.tsx
@@ -23,6 +23,7 @@ import { getImageUsage } from 'features/deleteImageModal/store/state';
import type { ImageUsage } from 'features/deleteImageModal/store/types';
import { selectNodesSlice } from 'features/nodes/store/selectors';
import { selectUpscaleSlice } from 'features/parameters/store/upscaleSlice';
+import { toast } from 'features/toast/toast';
import { atom } from 'nanostores';
import { memo, useCallback, useMemo, useRef } from 'react';
import { useTranslation } from 'react-i18next';
@@ -32,8 +33,11 @@ import {
useDeleteBoardMutation,
useDeleteUncategorizedImagesMutation,
} from 'services/api/endpoints/images';
+import { useDeleteUncategorizedVideosMutation } from 'services/api/endpoints/videos';
import type { BoardDTO } from 'services/api/types';
+import { getMediaDeletionSummary } from './getMediaDeletionSummary';
+
export const $boardToDelete = atom(null);
const DeleteBoardModal = () => {
@@ -85,6 +89,9 @@ const DeleteBoardModal = () => {
const [deleteUncategorizedImages, { isLoading: isDeleteUncategorizedImagesLoading }] =
useDeleteUncategorizedImagesMutation();
+ const [deleteUncategorizedVideos, { isLoading: isDeleteUncategorizedVideosLoading }] =
+ useDeleteUncategorizedVideosMutation();
+
const imageUsageSummary = useAppSelector(selectImageUsageSummary);
const handleDeleteBoardOnly = useCallback(() => {
@@ -95,21 +102,47 @@ const DeleteBoardModal = () => {
$boardToDelete.set(null);
}, [boardToDelete, deleteBoardOnly]);
- const handleDeleteBoardAndImages = useCallback(() => {
+ const handleDeleteBoardAndImages = useCallback(async () => {
if (!boardToDelete || boardToDelete === 'none') {
return;
}
- deleteBoardAndImages({ board_id: boardToDelete.board_id });
+ const result = await Promise.allSettled([deleteBoardAndImages({ board_id: boardToDelete.board_id }).unwrap()]);
+ const summary = getMediaDeletionSummary(result);
+ if (summary.requestFailed || summary.failedCount > 0) {
+ toast({
+ status: summary.requestFailed ? 'error' : 'warning',
+ title: t('toast.mediaDeleteFailed'),
+ description: summary.requestFailed
+ ? t('toast.mediaDeleteFailedDesc')
+ : t('toast.mediaDeletePartial', { count: summary.failedCount }),
+ });
+ }
$boardToDelete.set(null);
- }, [boardToDelete, deleteBoardAndImages]);
+ }, [boardToDelete, deleteBoardAndImages, t]);
- const handleDeleteUncategorizedImages = useCallback(() => {
+ const handleDeleteUncategorizedMedia = useCallback(async () => {
if (!boardToDelete || boardToDelete !== 'none') {
return;
}
- deleteUncategorizedImages();
+ // The uncategorized bucket is polymorphic (the button says "Images/Videos"), so both
+ // media kinds are deleted. The mutations are independent — a failure in one doesn't
+ // block the other.
+ const results = await Promise.allSettled([
+ deleteUncategorizedImages().unwrap(),
+ deleteUncategorizedVideos().unwrap(),
+ ]);
+ const summary = getMediaDeletionSummary(results);
+ if (summary.requestFailed || summary.failedCount > 0) {
+ toast({
+ status: summary.requestFailed ? 'error' : 'warning',
+ title: t('toast.mediaDeleteFailed'),
+ description: summary.requestFailed
+ ? t('toast.mediaDeleteFailedDesc')
+ : t('toast.mediaDeletePartial', { count: summary.failedCount }),
+ });
+ }
$boardToDelete.set(null);
- }, [boardToDelete, deleteUncategorizedImages]);
+ }, [boardToDelete, deleteUncategorizedImages, deleteUncategorizedVideos, t]);
const handleClose = useCallback(() => {
$boardToDelete.set(null);
@@ -122,8 +155,15 @@ const DeleteBoardModal = () => {
isDeleteBoardAndImagesLoading ||
isDeleteBoardOnlyLoading ||
isFetchingBoardNames ||
+ isDeleteUncategorizedImagesLoading ||
+ isDeleteUncategorizedVideosLoading,
+ [
+ isDeleteBoardAndImagesLoading,
+ isDeleteBoardOnlyLoading,
+ isFetchingBoardNames,
isDeleteUncategorizedImagesLoading,
- [isDeleteBoardAndImagesLoading, isDeleteBoardOnlyLoading, isFetchingBoardNames, isDeleteUncategorizedImagesLoading]
+ isDeleteUncategorizedVideosLoading,
+ ]
);
if (!boardToDelete) {
@@ -151,8 +191,11 @@ const DeleteBoardModal = () => {
bottomMessage={t('boards.bottomMessage')}
/>
)}
- {boardToDelete !== 'none' && {t('boards.deletedBoardsCannotbeRestored')} }
- {t('gallery.deleteImagePermanent')}
+ {boardToDelete !== 'none' ? (
+ {t('boards.deletedBoardsCannotbeRestored')}
+ ) : (
+ {t('gallery.deleteMediaPermanent')}
+ )}
@@ -167,11 +210,11 @@ const DeleteBoardModal = () => {
)}
{boardToDelete !== 'none' && (
- {t('boards.deleteBoardAndImages')}
+ {t('boards.deleteBoardAndAssets')}
)}
{boardToDelete === 'none' && (
-
+
{t('boards.deleteAllUncategorizedImages')}
)}
diff --git a/invokeai/frontend/web/src/features/gallery/components/Boards/getMediaDeletionSummary.test.ts b/invokeai/frontend/web/src/features/gallery/components/Boards/getMediaDeletionSummary.test.ts
new file mode 100644
index 00000000000..4714b263c66
--- /dev/null
+++ b/invokeai/frontend/web/src/features/gallery/components/Boards/getMediaDeletionSummary.test.ts
@@ -0,0 +1,28 @@
+import { describe, expect, it } from 'vitest';
+
+import { getMediaDeletionSummary } from './getMediaDeletionSummary';
+
+describe('getMediaDeletionSummary', () => {
+ it('reports a complete deletion as successful', () => {
+ expect(getMediaDeletionSummary([{ status: 'fulfilled', value: { failed_images: [], failed_videos: [] } }])).toEqual(
+ { failedCount: 0, requestFailed: false }
+ );
+ });
+
+ it('counts image and video deletion failures', () => {
+ expect(
+ getMediaDeletionSummary([
+ { status: 'fulfilled', value: { failed_images: ['image.png'], failed_videos: ['video.mp4'] } },
+ ])
+ ).toEqual({ failedCount: 2, requestFailed: false });
+ });
+
+ it('reports a rejected image deletion request', () => {
+ expect(
+ getMediaDeletionSummary([
+ { status: 'rejected', reason: new Error('image delete failed') },
+ { status: 'fulfilled', value: { failed_videos: [] } },
+ ])
+ ).toEqual({ failedCount: 0, requestFailed: true });
+ });
+});
diff --git a/invokeai/frontend/web/src/features/gallery/components/Boards/getMediaDeletionSummary.ts b/invokeai/frontend/web/src/features/gallery/components/Boards/getMediaDeletionSummary.ts
new file mode 100644
index 00000000000..ef02fc28922
--- /dev/null
+++ b/invokeai/frontend/web/src/features/gallery/components/Boards/getMediaDeletionSummary.ts
@@ -0,0 +1,15 @@
+type MediaDeletionResult = {
+ failed_images?: readonly string[];
+ failed_videos?: readonly string[];
+};
+
+export const getMediaDeletionSummary = (results: PromiseSettledResult[]) => ({
+ failedCount: results.reduce(
+ (count, result) =>
+ result.status === 'fulfilled'
+ ? count + (result.value.failed_images?.length ?? 0) + (result.value.failed_videos?.length ?? 0)
+ : count,
+ 0
+ ),
+ requestFailed: results.some((result) => result.status === 'rejected'),
+});
diff --git a/invokeai/frontend/web/src/features/gallery/components/ContextMenu/ImageContextMenu.tsx b/invokeai/frontend/web/src/features/gallery/components/ContextMenu/ImageContextMenu.tsx
index d696d97e779..a175dc869b3 100644
--- a/invokeai/frontend/web/src/features/gallery/components/ContextMenu/ImageContextMenu.tsx
+++ b/invokeai/frontend/web/src/features/gallery/components/ContextMenu/ImageContextMenu.tsx
@@ -5,7 +5,7 @@ import { useAppSelector } from 'app/store/storeHooks';
import { useAssertSingleton } from 'common/hooks/useAssertSingleton';
import MultipleSelectionMenuItems from 'features/gallery/components/ContextMenu/MultipleSelectionMenuItems';
import SingleSelectionMenuItems from 'features/gallery/components/ContextMenu/SingleSelectionMenuItems';
-import { selectSelectionCount } from 'features/gallery/store/gallerySelectors';
+import { selectSelection } from 'features/gallery/store/gallerySelectors';
import { map } from 'nanostores';
import type { RefObject } from 'react';
import { memo, useCallback, useEffect, useRef } from 'react';
@@ -254,14 +254,18 @@ ImageContextMenuEventLogical.displayName = 'ImageContextMenuEventLogical';
// The content of the context menu, which changes based on the selection count. Split out and memoized to avoid
// re-rendering the whole context menu too often.
const MenuContent = memo(() => {
- const selectionCount = useAppSelector(selectSelectionCount);
+ const selection = useAppSelector(selectSelection);
const state = useStore($imageContextMenuState);
if (!state.imageDTO) {
return null;
}
- if (selectionCount > 1) {
+ // Only show the multi-selection menu when the clicked image is part of the selection —
+ // right-clicking an item outside the selection acts on that item alone. Without this,
+ // right-clicking an image while 2+ videos were selected showed a menu whose every
+ // action was disabled (the multi menu filters the selection down to images).
+ if (selection.length > 1 && selection.includes(state.imageDTO.image_name)) {
return (
diff --git a/invokeai/frontend/web/src/features/gallery/components/ContextMenu/MenuItems/ContextMenuItemChangeBoardVideo.tsx b/invokeai/frontend/web/src/features/gallery/components/ContextMenu/MenuItems/ContextMenuItemChangeBoardVideo.tsx
new file mode 100644
index 00000000000..b7298a87431
--- /dev/null
+++ b/invokeai/frontend/web/src/features/gallery/components/ContextMenu/MenuItems/ContextMenuItemChangeBoardVideo.tsx
@@ -0,0 +1,26 @@
+import { MenuItem } from '@invoke-ai/ui-library';
+import { useAppDispatch } from 'app/store/storeHooks';
+import { isModalOpenChanged, videosToChangeSelected } from 'features/changeBoardModal/store/slice';
+import { useVideoDTOContext } from 'features/gallery/contexts/VideoDTOContext';
+import { memo, useCallback } from 'react';
+import { useTranslation } from 'react-i18next';
+import { PiFoldersBold } from 'react-icons/pi';
+
+export const ContextMenuItemChangeBoardVideo = memo(() => {
+ const { t } = useTranslation();
+ const dispatch = useAppDispatch();
+ const videoDTO = useVideoDTOContext();
+
+ const onClick = useCallback(() => {
+ dispatch(videosToChangeSelected([videoDTO.video_name]));
+ dispatch(isModalOpenChanged(true));
+ }, [dispatch, videoDTO.video_name]);
+
+ return (
+ } onClickCapture={onClick}>
+ {t('boards.changeBoard')}
+
+ );
+});
+
+ContextMenuItemChangeBoardVideo.displayName = 'ContextMenuItemChangeBoardVideo';
diff --git a/invokeai/frontend/web/src/features/gallery/components/ContextMenu/MenuItems/ContextMenuItemDeleteVideo.tsx b/invokeai/frontend/web/src/features/gallery/components/ContextMenu/MenuItems/ContextMenuItemDeleteVideo.tsx
new file mode 100644
index 00000000000..15339e07473
--- /dev/null
+++ b/invokeai/frontend/web/src/features/gallery/components/ContextMenu/MenuItems/ContextMenuItemDeleteVideo.tsx
@@ -0,0 +1,44 @@
+import { MenuItem } from '@invoke-ai/ui-library';
+import { useAppSelector } from 'app/store/storeHooks';
+import { useDeleteVideoModalApi } from 'features/deleteVideoModal/store/state';
+import { useVideoDTOContext } from 'features/gallery/contexts/VideoDTOContext';
+import { selectSelection } from 'features/gallery/store/gallerySelectors';
+import { isVideoName } from 'features/gallery/store/types';
+import { memo, useCallback, useMemo } from 'react';
+import { useTranslation } from 'react-i18next';
+import { PiTrashSimpleBold } from 'react-icons/pi';
+
+export const ContextMenuItemDeleteVideo = memo(() => {
+ const { t } = useTranslation();
+ const videoDTO = useVideoDTOContext();
+ const deleteVideoModal = useDeleteVideoModalApi();
+ const selection = useAppSelector(selectSelection);
+
+ // When the right-clicked video is part of an active multi-selection, delete every
+ // selected video in one shot. Image names mixed into the selection are skipped —
+ // right-clicking an image surfaces ImageContextMenu, which owns that flow.
+ const targetVideoNames = useMemo(() => {
+ if (selection.length > 1 && selection.includes(videoDTO.video_name)) {
+ return selection.filter(isVideoName);
+ }
+ return [videoDTO.video_name];
+ }, [selection, videoDTO.video_name]);
+
+ const label = t('gallery.deleteVideo', { count: targetVideoNames.length });
+
+ const onClick = useCallback(async () => {
+ try {
+ await deleteVideoModal.delete(targetVideoNames);
+ } catch {
+ // noop — user canceled the confirm dialog.
+ }
+ }, [deleteVideoModal, targetVideoNames]);
+
+ return (
+ } onClickCapture={onClick}>
+ {label}
+
+ );
+});
+
+ContextMenuItemDeleteVideo.displayName = 'ContextMenuItemDeleteVideo';
diff --git a/invokeai/frontend/web/src/features/gallery/components/ContextMenu/MenuItems/ContextMenuItemDownloadVideo.tsx b/invokeai/frontend/web/src/features/gallery/components/ContextMenu/MenuItems/ContextMenuItemDownloadVideo.tsx
new file mode 100644
index 00000000000..5a667b3e96f
--- /dev/null
+++ b/invokeai/frontend/web/src/features/gallery/components/ContextMenu/MenuItems/ContextMenuItemDownloadVideo.tsx
@@ -0,0 +1,24 @@
+import { MenuItem } from '@invoke-ai/ui-library';
+import { useDownloadItem } from 'common/hooks/useDownloadImage';
+import { useVideoDTOContext } from 'features/gallery/contexts/VideoDTOContext';
+import { memo, useCallback } from 'react';
+import { useTranslation } from 'react-i18next';
+import { PiDownloadSimpleBold } from 'react-icons/pi';
+
+export const ContextMenuItemDownloadVideo = memo(() => {
+ const { t } = useTranslation();
+ const videoDTO = useVideoDTOContext();
+ const { downloadItem } = useDownloadItem();
+
+ const onClick = useCallback(() => {
+ downloadItem(videoDTO.video_url, videoDTO.video_name);
+ }, [downloadItem, videoDTO]);
+
+ return (
+ } onClickCapture={onClick}>
+ {t('gallery.download')}
+
+ );
+});
+
+ContextMenuItemDownloadVideo.displayName = 'ContextMenuItemDownloadVideo';
diff --git a/invokeai/frontend/web/src/features/gallery/components/ContextMenu/MenuItems/ContextMenuItemOpenInNewTabVideo.tsx b/invokeai/frontend/web/src/features/gallery/components/ContextMenu/MenuItems/ContextMenuItemOpenInNewTabVideo.tsx
new file mode 100644
index 00000000000..aae84d0e3f0
--- /dev/null
+++ b/invokeai/frontend/web/src/features/gallery/components/ContextMenu/MenuItems/ContextMenuItemOpenInNewTabVideo.tsx
@@ -0,0 +1,21 @@
+import { MenuItem } from '@invoke-ai/ui-library';
+import { useVideoDTOContext } from 'features/gallery/contexts/VideoDTOContext';
+import { memo, useCallback } from 'react';
+import { useTranslation } from 'react-i18next';
+import { PiArrowSquareOutBold } from 'react-icons/pi';
+
+export const ContextMenuItemOpenInNewTabVideo = memo(() => {
+ const { t } = useTranslation();
+ const videoDTO = useVideoDTOContext();
+ const onClick = useCallback(() => {
+ window.open(videoDTO.video_url, '_blank', 'noopener,noreferrer');
+ }, [videoDTO.video_url]);
+
+ return (
+ } onClickCapture={onClick}>
+ {t('common.openInNewTab')}
+
+ );
+});
+
+ContextMenuItemOpenInNewTabVideo.displayName = 'ContextMenuItemOpenInNewTabVideo';
diff --git a/invokeai/frontend/web/src/features/gallery/components/ContextMenu/MultipleSelectionMenuItems.tsx b/invokeai/frontend/web/src/features/gallery/components/ContextMenu/MultipleSelectionMenuItems.tsx
index ee3c8e4e985..a2e7392c564 100644
--- a/invokeai/frontend/web/src/features/gallery/components/ContextMenu/MultipleSelectionMenuItems.tsx
+++ b/invokeai/frontend/web/src/features/gallery/components/ContextMenu/MultipleSelectionMenuItems.tsx
@@ -2,7 +2,9 @@ import { MenuDivider, MenuItem } from '@invoke-ai/ui-library';
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
import { imagesToChangeSelected, isModalOpenChanged } from 'features/changeBoardModal/store/slice';
import { useDeleteImageModalApi } from 'features/deleteImageModal/store/state';
-import { memo, useCallback } from 'react';
+import { selectSelection } from 'features/gallery/store/gallerySelectors';
+import { isVideoName } from 'features/gallery/store/types';
+import { memo, useCallback, useMemo } from 'react';
import { useTranslation } from 'react-i18next';
import { PiDownloadSimpleBold, PiFoldersBold, PiStarBold, PiStarFill, PiTrashSimpleBold } from 'react-icons/pi';
import {
@@ -16,7 +18,7 @@ import { useSelectedBoard } from 'services/api/hooks/useSelectedBoard';
const MultipleSelectionMenuItems = () => {
const { t } = useTranslation();
const dispatch = useAppDispatch();
- const selection = useAppSelector((s) => s.gallery.selection);
+ const selection = useAppSelector(selectSelection);
const deleteImageModal = useDeleteImageModalApi();
const selectedBoard = useSelectedBoard();
const { canWriteImages } = useBoardAccess(selectedBoard);
@@ -25,49 +27,56 @@ const MultipleSelectionMenuItems = () => {
const [unstarImages] = useUnstarImagesMutation();
const [bulkDownload] = useBulkDownloadImagesMutation();
+ // The gallery selection can contain mixed image+video names. Each menu only acts on its
+ // own kind so the action is unambiguous; right-clicking a video surfaces the video
+ // equivalent of this menu.
+ const imageNames = useMemo(() => selection.filter((name) => !isVideoName(name)), [selection]);
+ const count = imageNames.length;
+ const hasImages = count > 0;
+
const handleChangeBoard = useCallback(() => {
- dispatch(imagesToChangeSelected(selection));
+ dispatch(imagesToChangeSelected(imageNames));
dispatch(isModalOpenChanged(true));
- }, [dispatch, selection]);
+ }, [dispatch, imageNames]);
const handleDeleteSelection = useCallback(() => {
- deleteImageModal.delete(selection);
- }, [deleteImageModal, selection]);
+ deleteImageModal.delete(imageNames);
+ }, [deleteImageModal, imageNames]);
const handleStarSelection = useCallback(() => {
- starImages({ image_names: selection });
- }, [starImages, selection]);
+ starImages({ image_names: imageNames });
+ }, [starImages, imageNames]);
const handleUnstarSelection = useCallback(() => {
- unstarImages({ image_names: selection });
- }, [unstarImages, selection]);
+ unstarImages({ image_names: imageNames });
+ }, [unstarImages, imageNames]);
const handleBulkDownload = useCallback(() => {
- bulkDownload({ image_names: selection });
- }, [selection, bulkDownload]);
+ bulkDownload({ image_names: imageNames });
+ }, [imageNames, bulkDownload]);
return (
<>
- } onClickCapture={handleUnstarSelection}>
- Unstar All
+ } onClickCapture={handleUnstarSelection} isDisabled={!hasImages}>
+ {t('gallery.unstarImage', { count })}
- } onClickCapture={handleStarSelection}>
- Star All
+ } onClickCapture={handleStarSelection} isDisabled={!hasImages}>
+ {t('gallery.starImage', { count })}
- } onClickCapture={handleBulkDownload}>
- {t('gallery.downloadSelection')}
+ } onClickCapture={handleBulkDownload} isDisabled={!hasImages}>
+ {t('gallery.downloadImage', { count })}
- } onClickCapture={handleChangeBoard} isDisabled={!canWriteImages}>
- {t('boards.changeBoard')}
+ } onClickCapture={handleChangeBoard} isDisabled={!hasImages || !canWriteImages}>
+ {t('boards.changeBoardImage', { count })}
}
onClickCapture={handleDeleteSelection}
- isDisabled={!canWriteImages}
+ isDisabled={!hasImages || !canWriteImages}
>
- {t('gallery.deleteSelection')}
+ {t('gallery.deleteImage', { count })}
>
);
diff --git a/invokeai/frontend/web/src/features/gallery/components/ContextMenu/MultipleSelectionMenuItemsVideos.tsx b/invokeai/frontend/web/src/features/gallery/components/ContextMenu/MultipleSelectionMenuItemsVideos.tsx
new file mode 100644
index 00000000000..45018b72f89
--- /dev/null
+++ b/invokeai/frontend/web/src/features/gallery/components/ContextMenu/MultipleSelectionMenuItemsVideos.tsx
@@ -0,0 +1,97 @@
+import { MenuDivider, MenuItem } from '@invoke-ai/ui-library';
+import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
+import { useDownloadItem } from 'common/hooks/useDownloadImage';
+import { isModalOpenChanged, videosToChangeSelected } from 'features/changeBoardModal/store/slice';
+import { useDeleteVideoModalApi } from 'features/deleteVideoModal/store/state';
+import { selectSelection } from 'features/gallery/store/gallerySelectors';
+import { isVideoName } from 'features/gallery/store/types';
+import { memo, useCallback, useMemo } from 'react';
+import { useTranslation } from 'react-i18next';
+import { PiDownloadSimpleBold, PiFoldersBold, PiStarBold, PiStarFill, PiTrashSimpleBold } from 'react-icons/pi';
+import { getVideoDTOSafe, useStarVideosMutation, useUnstarVideosMutation } from 'services/api/endpoints/videos';
+import { useBoardAccess } from 'services/api/hooks/useBoardAccess';
+import { useSelectedBoard } from 'services/api/hooks/useSelectedBoard';
+
+/**
+ * Multi-selection menu surfaced by `VideoContextMenu` when the gallery selection has more
+ * than one item. Mirrors `MultipleSelectionMenuItems` (the image equivalent) feature-for-feature
+ * where the video API supports it. Filters the polymorphic gallery selection down to videos —
+ * mixed selections that also contain images are handled by the image-side menu when the user
+ * right-clicks an image.
+ */
+const MultipleSelectionMenuItemsVideos = () => {
+ const { t } = useTranslation();
+ const dispatch = useAppDispatch();
+ const selection = useAppSelector(selectSelection);
+ const deleteVideoModal = useDeleteVideoModalApi();
+ const selectedBoard = useSelectedBoard();
+ // Boards use one write permission for both kinds — videos inherit from `canWriteImages`.
+ const { canWriteImages } = useBoardAccess(selectedBoard);
+
+ const [starVideos] = useStarVideosMutation();
+ const [unstarVideos] = useUnstarVideosMutation();
+ const { downloadItem } = useDownloadItem();
+
+ const videoNames = useMemo(() => selection.filter(isVideoName), [selection]);
+ const count = videoNames.length;
+ const hasVideos = count > 0;
+
+ const handleChangeBoard = useCallback(() => {
+ dispatch(videosToChangeSelected(videoNames));
+ dispatch(isModalOpenChanged(true));
+ }, [dispatch, videoNames]);
+
+ const handleDeleteSelection = useCallback(() => {
+ deleteVideoModal.delete(videoNames).catch(() => {
+ // user cancelled the confirmation dialog
+ });
+ }, [deleteVideoModal, videoNames]);
+
+ const handleStarSelection = useCallback(() => {
+ starVideos({ video_names: videoNames });
+ }, [starVideos, videoNames]);
+
+ const handleUnstarSelection = useCallback(() => {
+ unstarVideos({ video_names: videoNames });
+ }, [unstarVideos, videoNames]);
+
+ const handleBulkDownload = useCallback(async () => {
+ // No zip-bundle endpoint exists for videos, so we loop the per-video download helper.
+ // Modern browsers prompt once for "allow multiple file downloads", then proceed silently.
+ for (const video_name of videoNames) {
+ const dto = await getVideoDTOSafe(video_name);
+ if (!dto) {
+ continue;
+ }
+ await downloadItem(dto.video_url, dto.video_name);
+ }
+ }, [downloadItem, videoNames]);
+
+ return (
+ <>
+ } onClickCapture={handleUnstarSelection} isDisabled={!hasVideos}>
+ {t('gallery.unstarVideo', { count })}
+
+ } onClickCapture={handleStarSelection} isDisabled={!hasVideos}>
+ {t('gallery.starVideo', { count })}
+
+ } onClickCapture={handleBulkDownload} isDisabled={!hasVideos}>
+ {t('gallery.downloadVideo', { count })}
+
+ } onClickCapture={handleChangeBoard} isDisabled={!hasVideos || !canWriteImages}>
+ {t('boards.changeBoardVideo', { count })}
+
+
+ }
+ onClickCapture={handleDeleteSelection}
+ isDisabled={!hasVideos || !canWriteImages}
+ >
+ {t('gallery.deleteVideo', { count })}
+
+ >
+ );
+};
+
+export default memo(MultipleSelectionMenuItemsVideos);
diff --git a/invokeai/frontend/web/src/features/gallery/components/ContextMenu/VideoContextMenu.tsx b/invokeai/frontend/web/src/features/gallery/components/ContextMenu/VideoContextMenu.tsx
new file mode 100644
index 00000000000..7502df33555
--- /dev/null
+++ b/invokeai/frontend/web/src/features/gallery/components/ContextMenu/VideoContextMenu.tsx
@@ -0,0 +1,242 @@
+import type { ChakraProps } from '@invoke-ai/ui-library';
+import { Menu, MenuButton, MenuDivider, MenuList, Portal, useGlobalMenuClose } from '@invoke-ai/ui-library';
+import { useStore } from '@nanostores/react';
+import { useAppSelector } from 'app/store/storeHooks';
+import { useAssertSingleton } from 'common/hooks/useAssertSingleton';
+import { ContextMenuItemChangeBoardVideo } from 'features/gallery/components/ContextMenu/MenuItems/ContextMenuItemChangeBoardVideo';
+import { ContextMenuItemDeleteVideo } from 'features/gallery/components/ContextMenu/MenuItems/ContextMenuItemDeleteVideo';
+import { ContextMenuItemDownloadVideo } from 'features/gallery/components/ContextMenu/MenuItems/ContextMenuItemDownloadVideo';
+import { ContextMenuItemOpenInNewTabVideo } from 'features/gallery/components/ContextMenu/MenuItems/ContextMenuItemOpenInNewTabVideo';
+import MultipleSelectionMenuItemsVideos from 'features/gallery/components/ContextMenu/MultipleSelectionMenuItemsVideos';
+import { VideoDTOContextProvider } from 'features/gallery/contexts/VideoDTOContext';
+import { selectSelection } from 'features/gallery/store/gallerySelectors';
+import { map } from 'nanostores';
+import type { RefObject } from 'react';
+import { memo, useCallback, useEffect, useRef } from 'react';
+import type { VideoDTO } from 'services/api/types';
+
+// Mirror of ImageContextMenu, but pared down to the three actions the video item supports today:
+// delete, change-board, download. Long-press on touch devices opens the menu the same way.
+
+const LONGPRESS_DELAY_MS = 500;
+const LONGPRESS_MOVE_THRESHOLD_PX = 10;
+
+const $videoContextMenuState = map<{
+ isOpen: boolean;
+ videoDTO: VideoDTO | null;
+ position: { x: number; y: number };
+}>({
+ isOpen: false,
+ videoDTO: null,
+ position: { x: -1, y: -1 },
+});
+
+const onClose = () => {
+ $videoContextMenuState.setKey('isOpen', false);
+};
+
+const elToVideoMap = new Map();
+
+const getVideoDTOFromMap = (target: Node): VideoDTO | undefined => {
+ const entry = Array.from(elToVideoMap.entries()).find((entry) => entry[0].contains(target));
+ return entry?.[1];
+};
+
+/**
+ * Register a context menu for a video DTO on a target element. Mirrors useImageContextMenu.
+ */
+export const useVideoContextMenu = (videoDTO: VideoDTO, ref: RefObject | (HTMLElement | null)) => {
+ useEffect(() => {
+ if (ref === null) {
+ return;
+ }
+ const el = ref instanceof HTMLElement ? ref : ref.current;
+ if (!el) {
+ return;
+ }
+ elToVideoMap.set(el, videoDTO);
+ return () => {
+ elToVideoMap.delete(el);
+ };
+ }, [videoDTO, ref]);
+};
+
+const _hover: ChakraProps['_hover'] = { bg: 'transparent' };
+
+export const VideoContextMenu = memo(() => {
+ useAssertSingleton('VideoContextMenu');
+ const state = useStore($videoContextMenuState);
+ useGlobalMenuClose(onClose);
+
+ return (
+
+
+
+
+
+
+
+ );
+});
+
+VideoContextMenu.displayName = 'VideoContextMenu';
+
+const MenuContent = memo(() => {
+ const state = useStore($videoContextMenuState);
+ const selection = useAppSelector(selectSelection);
+ if (!state.videoDTO) {
+ return null;
+ }
+ // Only show the multi-selection menu when the clicked video is part of the selection —
+ // right-clicking an item outside the selection acts on that item alone. Without this,
+ // right-clicking a video while 2+ images are selected showed a menu whose every action
+ // was disabled (the multi menu filters the selection down to videos and finds none).
+ if (selection.length > 1 && selection.includes(state.videoDTO.video_name)) {
+ return (
+
+
+
+ );
+ }
+ return (
+
+
+
+
+
+
+
+
+
+ );
+});
+
+MenuContent.displayName = 'VideoContextMenuContent';
+
+/**
+ * Logical component that listens for context-menu events and dispatches to the singleton's state.
+ * Split out from the visible menu to keep re-renders cheap.
+ */
+const VideoContextMenuEventLogical = memo(() => {
+ const lastPositionRef = useRef<{ x: number; y: number }>({ x: -1, y: -1 });
+ const longPressTimeoutRef = useRef(0);
+ const animationTimeoutRef = useRef(0);
+
+ const onContextMenu = useCallback((e: MouseEvent | PointerEvent) => {
+ if (e.shiftKey) {
+ // shift+right-click opens the native context menu
+ onClose();
+ return;
+ }
+
+ const videoDTO = getVideoDTOFromMap(e.target as Node);
+ if (!videoDTO) {
+ // Not over a registered video item — let ImageContextMenu handle it (or close).
+ onClose();
+ return;
+ }
+
+ window.clearTimeout(animationTimeoutRef.current);
+ e.preventDefault();
+
+ if (lastPositionRef.current.x !== e.pageX || lastPositionRef.current.y !== e.pageY) {
+ if ($videoContextMenuState.get().isOpen) {
+ onClose();
+ }
+ animationTimeoutRef.current = window.setTimeout(() => {
+ $videoContextMenuState.set({
+ isOpen: true,
+ position: { x: e.pageX, y: e.pageY },
+ videoDTO,
+ });
+ }, 100);
+ } else {
+ $videoContextMenuState.set({
+ isOpen: true,
+ position: { x: e.pageX, y: e.pageY },
+ videoDTO,
+ });
+ }
+
+ lastPositionRef.current = { x: e.pageX, y: e.pageY };
+ }, []);
+
+ const onPointerDown = useCallback(
+ (e: PointerEvent) => {
+ if (e.pointerType === 'mouse') {
+ return;
+ }
+ longPressTimeoutRef.current = window.setTimeout(() => {
+ onContextMenu(e);
+ }, LONGPRESS_DELAY_MS);
+ lastPositionRef.current = { x: e.pageX, y: e.pageY };
+ },
+ [onContextMenu]
+ );
+
+ const onPointerMove = useCallback((e: PointerEvent) => {
+ if (e.pointerType === 'mouse') {
+ return;
+ }
+ if (longPressTimeoutRef.current === null) {
+ return;
+ }
+ const distance = Math.hypot(e.pageX - lastPositionRef.current.x, e.pageY - lastPositionRef.current.y);
+ if (distance > LONGPRESS_MOVE_THRESHOLD_PX) {
+ clearTimeout(longPressTimeoutRef.current);
+ }
+ }, []);
+
+ const onPointerUp = useCallback((e: PointerEvent) => {
+ if (e.pointerType === 'mouse') {
+ return;
+ }
+ if (longPressTimeoutRef.current) {
+ clearTimeout(longPressTimeoutRef.current);
+ }
+ }, []);
+
+ const onPointerCancel = useCallback((e: PointerEvent) => {
+ if (e.pointerType === 'mouse') {
+ return;
+ }
+ if (longPressTimeoutRef.current) {
+ clearTimeout(longPressTimeoutRef.current);
+ }
+ }, []);
+
+ useEffect(() => {
+ const controller = new AbortController();
+ window.addEventListener('contextmenu', onContextMenu, { signal: controller.signal });
+ window.addEventListener('pointerdown', onPointerDown, { signal: controller.signal });
+ window.addEventListener('pointerup', onPointerUp, { signal: controller.signal });
+ window.addEventListener('pointercancel', onPointerCancel, { signal: controller.signal });
+ window.addEventListener('pointermove', onPointerMove, { signal: controller.signal });
+ return () => {
+ controller.abort();
+ };
+ }, [onContextMenu, onPointerCancel, onPointerDown, onPointerMove, onPointerUp]);
+
+ useEffect(
+ () => () => {
+ window.clearTimeout(animationTimeoutRef.current);
+ window.clearTimeout(longPressTimeoutRef.current);
+ },
+ []
+ );
+
+ return null;
+});
+
+VideoContextMenuEventLogical.displayName = 'VideoContextMenuEventLogical';
diff --git a/invokeai/frontend/web/src/features/gallery/components/GalleryImageGrid.tsx b/invokeai/frontend/web/src/features/gallery/components/GalleryImageGrid.tsx
index 43e7d2aca92..b62a8b01c8d 100644
--- a/invokeai/frontend/web/src/features/gallery/components/GalleryImageGrid.tsx
+++ b/invokeai/frontend/web/src/features/gallery/components/GalleryImageGrid.tsx
@@ -1,8 +1,8 @@
-import { Box, Flex, forwardRef, Grid, GridItem, Spinner, Text } from '@invoke-ai/ui-library';
+import { Box, Flex, forwardRef, Grid, GridItem, IconButton, Spinner, Text } from '@invoke-ai/ui-library';
import { createSelector } from '@reduxjs/toolkit';
import { useAppSelector, useAppStore } from 'app/store/storeHooks';
import { getFocusedRegion, useIsRegionFocused } from 'common/hooks/focus';
-import { useRangeBasedImageFetching } from 'features/gallery/hooks/useRangeBasedImageFetching';
+import { getVideoPrefetchOptions, useRangeBasedImageFetching } from 'features/gallery/hooks/useRangeBasedImageFetching';
import type { selectGetImageNamesQueryArgs } from 'features/gallery/store/gallerySelectors';
import {
selectGalleryImageMinimumWidth,
@@ -12,12 +12,14 @@ import {
selectSelectionCount,
} from 'features/gallery/store/gallerySelectors';
import { imageToCompareChanged, selectionChanged } from 'features/gallery/store/gallerySlice';
+import { isVideoName } from 'features/gallery/store/types';
import { useRegisteredHotkeys } from 'features/system/components/HotkeysModal/useHotkeyData';
import { navigationApi } from 'features/ui/layouts/navigation-api';
import { VIEWER_PANEL_ID } from 'features/ui/layouts/shared';
import { selectActiveTab } from 'features/ui/store/uiSelectors';
import React, { memo, useCallback, useEffect, useMemo, useRef } from 'react';
import { useTranslation } from 'react-i18next';
+import { PiArrowCounterClockwiseBold } from 'react-icons/pi';
import type {
GridComponents,
GridComputeItemKey,
@@ -28,12 +30,14 @@ import type {
} from 'react-virtuoso';
import { VirtuosoGrid } from 'react-virtuoso';
import { imagesApi, useImageDTO, useStarImagesMutation, useUnstarImagesMutation } from 'services/api/endpoints/images';
+import { useStarVideosMutation, useUnstarVideosMutation, useVideoDTO, videosApi } from 'services/api/endpoints/videos';
import { useDebounce } from 'use-debounce';
import { getItemIndex } from './getItemIndex';
import { getItemsPerRow } from './getItemsPerRow';
import { GalleryImage, GalleryImagePlaceholder } from './ImageGrid/GalleryImage';
import { GallerySelectionCountTag } from './ImageGrid/GallerySelectionCountTag';
+import { GalleryVideoItem } from './ImageGrid/GalleryVideoItem';
import { scrollIntoView } from './scrollIntoView';
import { useGalleryImageNames } from './use-gallery-image-names';
import { useScrollableGallery } from './useScrollableGallery';
@@ -46,35 +50,60 @@ type GridContext = {
};
/**
- * Wraps an image - either the placeholder as it is being loaded or the loaded image
+ * Wraps a gallery item — either the placeholder as it is being loaded, or the loaded image / video.
+ *
+ * Names are polymorphic: image names end in `.png`, video names in `.mp4` (see SimpleNameService).
+ * `isVideoName` discriminates so we can subscribe to the right query and render the right component.
+ *
+ * We rely on `useRangeBasedImageFetching` to fetch all DTOs into the RTK Query cache. Here we just
+ * consume the cache — `useQuerySubscription` with `skip: isUninitialized` subscribes only after the
+ * fetch has populated data (see https://github.com/reduxjs/redux-toolkit/discussions/4213).
*/
const ImageAtPosition = memo(({ imageName }: { index: number; imageName: string }) => {
- /*
- * We rely on the useRangeBasedImageFetching to fetch all image DTOs, caching them with RTK Query.
- *
- * In this component, we just want to consume that cache. Unforutnately, RTK Query does not provide a way to
- * subscribe to a query without triggering a new fetch.
- *
- * There is a hack, though:
- * - https://github.com/reduxjs/redux-toolkit/discussions/4213
- *
- * This essentially means "subscribe to the query once it has some data".
- *
- * One issue with this approach. When an item DTO is already cached - for example, because it is selected and
- * rendered in the viewer - it will show up in the grid before the other items have loaded. This is most
- * noticeable when first loading a board. The first item in the board is selected and rendered immediately in
- * the viewer, caching the DTO. The gallery grid renders, and that first item displays as a thumbnail while the
- * others are still placeholders. After a moment, the rest of the items load up and display as thumbnails.
- */
-
- // Use `currentData` instead of `data` to prevent a flash of previous image rendered at this index
- const { currentData: imageDTO, isUninitialized } = imagesApi.endpoints.getImageDTO.useQueryState(imageName);
- imagesApi.endpoints.getImageDTO.useQuerySubscription(imageName, { skip: isUninitialized });
+ const isVideo = isVideoName(imageName);
+ const store = useAppStore();
+ const { t } = useTranslation();
+ const retryVideo = useCallback(() => {
+ store.dispatch(videosApi.endpoints.getVideoDTO.initiate(imageName, getVideoPrefetchOptions()));
+ }, [imageName, store]);
+
+ // Always call both hooks (React rules of hooks) — the irrelevant one is just a no-op subscription.
+ const imageState = imagesApi.endpoints.getImageDTO.useQueryState(isVideo ? '' : imageName);
+ imagesApi.endpoints.getImageDTO.useQuerySubscription(isVideo ? '' : imageName, {
+ skip: isVideo || imageState.isUninitialized,
+ });
+ const videoState = videosApi.endpoints.getVideoDTO.useQueryState(isVideo ? imageName : '');
+ videosApi.endpoints.getVideoDTO.useQuerySubscription(isVideo ? imageName : '', {
+ skip: !isVideo || videoState.isUninitialized,
+ });
+
+ if (isVideo) {
+ if (videoState.isError) {
+ return (
+
+ } />
+
+ );
+ }
+ const videoDTO = videoState.currentData;
+ if (!videoDTO) {
+ return ;
+ }
+ return ;
+ }
+ const imageDTO = imageState.currentData;
if (!imageDTO) {
return ;
}
-
return ;
});
ImageAtPosition.displayName = 'ImageAtPosition';
@@ -309,30 +338,47 @@ const useStarImageHotkey = () => {
const lastSelectedItem = useAppSelector(selectLastSelectedItem);
const selectionCount = useAppSelector(selectSelectionCount);
const isGalleryFocused = useIsRegionFocused('gallery');
- const imageDTO = useImageDTO(lastSelectedItem);
+ const isVideo = lastSelectedItem ? isVideoName(lastSelectedItem) : false;
+ const imageDTO = useImageDTO(isVideo ? null : lastSelectedItem);
+ const videoDTO = useVideoDTO(isVideo ? lastSelectedItem : null);
const [starImages] = useStarImagesMutation();
const [unstarImages] = useUnstarImagesMutation();
+ const [starVideos] = useStarVideosMutation();
+ const [unstarVideos] = useUnstarVideosMutation();
+
+ const dto = isVideo ? videoDTO : imageDTO;
const handleStarHotkey = useCallback(() => {
- if (!imageDTO) {
- return;
- }
if (!isGalleryFocused) {
return;
}
- if (imageDTO.starred) {
- unstarImages({ image_names: [imageDTO.image_name] });
+ if (isVideo) {
+ if (!videoDTO) {
+ return;
+ }
+ if (videoDTO.starred) {
+ unstarVideos({ video_names: [videoDTO.video_name] });
+ } else {
+ starVideos({ video_names: [videoDTO.video_name] });
+ }
} else {
- starImages({ image_names: [imageDTO.image_name] });
+ if (!imageDTO) {
+ return;
+ }
+ if (imageDTO.starred) {
+ unstarImages({ image_names: [imageDTO.image_name] });
+ } else {
+ starImages({ image_names: [imageDTO.image_name] });
+ }
}
- }, [imageDTO, isGalleryFocused, starImages, unstarImages]);
+ }, [isGalleryFocused, isVideo, imageDTO, videoDTO, starImages, unstarImages, starVideos, unstarVideos]);
useRegisteredHotkeys({
id: 'starImage',
category: 'gallery',
callback: handleStarHotkey,
- options: { enabled: !!imageDTO && selectionCount === 1 && isGalleryFocused },
- dependencies: [imageDTO, selectionCount, isGalleryFocused, handleStarHotkey],
+ options: { enabled: !!dto && selectionCount === 1 && isGalleryFocused },
+ dependencies: [dto, selectionCount, isGalleryFocused, handleStarHotkey],
});
};
diff --git a/invokeai/frontend/web/src/features/gallery/components/GalleryUploadButton.tsx b/invokeai/frontend/web/src/features/gallery/components/GalleryUploadButton.tsx
index af3b980947e..5134a883e7b 100644
--- a/invokeai/frontend/web/src/features/gallery/components/GalleryUploadButton.tsx
+++ b/invokeai/frontend/web/src/features/gallery/components/GalleryUploadButton.tsx
@@ -4,7 +4,8 @@ import { t } from 'i18next';
import { memo } from 'react';
import { PiUploadBold } from 'react-icons/pi';
-const UPLOAD_OPTIONS: Parameters[0] = { allowMultiple: true };
+// The gallery uploader is the one place videos are accepted — every other consumer wants an image.
+const UPLOAD_OPTIONS: Parameters[0] = { allowMultiple: true, allowVideos: true };
export const GalleryUploadButton = memo(() => {
const uploadApi = useImageUploadButton(UPLOAD_OPTIONS);
@@ -14,9 +15,10 @@ export const GalleryUploadButton = memo(() => {
size="sm"
alignSelf="stretch"
variant="link"
- aria-label={t('accessibility.uploadImages')}
- tooltip={t('accessibility.uploadImages')}
+ aria-label={t('accessibility.uploadMedia')}
+ tooltip={t('accessibility.uploadMedia')}
icon={ }
+ isLoading={uploadApi.isUploading}
{...uploadApi.getUploadButtonProps()}
/>
diff --git a/invokeai/frontend/web/src/features/gallery/components/ImageGrid/GalleryImage.tsx b/invokeai/frontend/web/src/features/gallery/components/ImageGrid/GalleryImage.tsx
index d03a884a094..b16f0056c57 100644
--- a/invokeai/frontend/web/src/features/gallery/components/ImageGrid/GalleryImage.tsx
+++ b/invokeai/frontend/web/src/features/gallery/components/ImageGrid/GalleryImage.tsx
@@ -7,6 +7,7 @@ import type { AppDispatch, AppGetState } from 'app/store/store';
import { useAppSelector, useAppStore } from 'app/store/storeHooks';
import { useMiddleClickOpenInNewTab } from 'common/hooks/useMiddleClickOpenInNewTab';
import { uniq } from 'es-toolkit';
+import { useMediaUrl } from 'features/auth/store/mediaCookieRefresh';
import { multipleImageDndSource, singleImageDndSource } from 'features/dnd/dnd';
import type { DndDragPreviewMultipleImageState } from 'features/dnd/DndDragPreviewMultipleImage';
import { createMultipleImageDragPreview, setMultipleImageDragPreview } from 'features/dnd/DndDragPreviewMultipleImage';
@@ -15,18 +16,15 @@ import { createSingleImageDragPreview, setSingleImageDragPreview } from 'feature
import { firefoxDndFix } from 'features/dnd/util';
import { useImageContextMenu } from 'features/gallery/components/ContextMenu/ImageContextMenu';
import { GalleryItemHoverIcons } from 'features/gallery/components/ImageGrid/GalleryItemHoverIcons';
-import {
- selectGetImageNamesQueryArgs,
- selectSelectedBoardId,
- selectSelection,
-} from 'features/gallery/store/gallerySelectors';
+import { selectSelectedBoardId, selectSelection } from 'features/gallery/store/gallerySelectors';
import { imageToCompareChanged, selectGallerySlice, selectionChanged } from 'features/gallery/store/gallerySlice';
+import { selectCachedGalleryItemNames } from 'features/gallery/store/selectCachedGalleryItemNames';
+import { isVideoName } from 'features/gallery/store/types';
import { navigationApi } from 'features/ui/layouts/navigation-api';
import { VIEWER_PANEL_ID } from 'features/ui/layouts/shared';
import type { MouseEvent, MouseEventHandler } from 'react';
import { memo, useCallback, useEffect, useMemo, useRef, useState } from 'react';
import { PiImageBold } from 'react-icons/pi';
-import { imagesApi } from 'services/api/endpoints/images';
import type { ImageDTO } from 'services/api/types';
import { galleryItemContainerSX } from './galleryItemContainerSX';
@@ -39,13 +37,14 @@ const buildOnClick =
(imageName: string, dispatch: AppDispatch, getState: AppGetState) => (e: MouseEvent) => {
const { shiftKey, ctrlKey, metaKey, altKey } = e;
const state = getState();
- const queryArgs = selectGetImageNamesQueryArgs(state);
- const imageNames = imagesApi.endpoints.getImageNames.select(queryArgs)(state).data?.image_names ?? [];
-
- // If we don't have the image names cached, we can't perform selection operations
- // This can happen if the user clicks on an image before the names are loaded
- if (imageNames.length === 0) {
- // For basic click without modifiers, we can still set selection
+ // Read from the polymorphic getGalleryItemNames cache (the source of truth for grid order)
+ // rather than the legacy image-only getImageNames cache, which is no longer populated for
+ // the grid. Without this, shift- and ctrl-click would silently no-op because the legacy
+ // list comes back empty.
+ const itemNames = selectCachedGalleryItemNames(state);
+
+ if (itemNames.length === 0) {
+ // Without an ordered list we can still honor a plain single-click.
if (!shiftKey && !ctrlKey && !metaKey && !altKey) {
dispatch(selectionChanged([imageName]));
}
@@ -63,13 +62,12 @@ const buildOnClick =
} else if (shiftKey) {
const rangeEndImageName = imageName;
const lastSelectedImage = selection.at(-1);
- const lastClickedIndex = imageNames.findIndex((name) => name === lastSelectedImage);
- const currentClickedIndex = imageNames.findIndex((name) => name === rangeEndImageName);
+ const lastClickedIndex = itemNames.findIndex((name) => name === lastSelectedImage);
+ const currentClickedIndex = itemNames.findIndex((name) => name === rangeEndImageName);
if (lastClickedIndex > -1 && currentClickedIndex > -1) {
- // We have a valid range!
const start = Math.min(lastClickedIndex, currentClickedIndex);
const end = Math.max(lastClickedIndex, currentClickedIndex);
- const imagesToSelect = imageNames.slice(start, end + 1);
+ const imagesToSelect = itemNames.slice(start, end + 1);
if (currentClickedIndex < lastClickedIndex) {
imagesToSelect.reverse();
}
@@ -87,6 +85,7 @@ const buildOnClick =
};
export const GalleryImage = memo(({ imageDTO }: Props) => {
+ const thumbnailUrl = useMediaUrl(imageDTO.thumbnail_url);
const store = useAppStore();
const [isDragging, setIsDragging] = useState(false);
const [dragPreviewState, setDragPreviewState] = useState<
@@ -136,11 +135,17 @@ export const GalleryImage = memo(({ imageDTO }: Props) => {
const selection = selectSelection(store.getState());
const boardId = selectSelectedBoardId(store.getState());
- // When we have multiple images selected, and the dragged image is part of the selection, initiate a
- // multi-image drag.
+ // When we have multiple items selected, and the dragged image is part of the
+ // selection, initiate a multi-drag. Mixed selections (images + videos) ride along in
+ // the same payload: the board drop handler splits them and dispatches both mutations.
+ // Without filtering, video names would land in `image_names` and the image router
+ // would 404 on each one.
if (selection.length > 1 && selection.some((n) => n === imageDTO.image_name)) {
+ const image_names = selection.filter((n) => !isVideoName(n));
+ const video_names = selection.filter(isVideoName);
return multipleImageDndSource.getData({
- image_names: selection,
+ image_names,
+ video_names,
board_id: boardId,
});
}
@@ -216,7 +221,7 @@ export const GalleryImage = memo(({ imageDTO }: Props) => {
>
}
objectFit="contain"
diff --git a/invokeai/frontend/web/src/features/gallery/components/ImageGrid/GalleryItemPlayBadge.tsx b/invokeai/frontend/web/src/features/gallery/components/ImageGrid/GalleryItemPlayBadge.tsx
new file mode 100644
index 00000000000..3ea9b00ad50
--- /dev/null
+++ b/invokeai/frontend/web/src/features/gallery/components/ImageGrid/GalleryItemPlayBadge.tsx
@@ -0,0 +1,34 @@
+import { Flex, Icon } from '@invoke-ai/ui-library';
+import { memo } from 'react';
+import { PiPlayFill } from 'react-icons/pi';
+
+/**
+ * Centered play-button badge laid over a video thumbnail in the gallery grid. Purely visual —
+ * the gallery item itself owns click selection; the play action lives in the viewer.
+ */
+export const GalleryItemPlayBadge = memo(() => {
+ return (
+
+
+
+ );
+});
+
+GalleryItemPlayBadge.displayName = 'GalleryItemPlayBadge';
diff --git a/invokeai/frontend/web/src/features/gallery/components/ImageGrid/GalleryItemStarIconButton.tsx b/invokeai/frontend/web/src/features/gallery/components/ImageGrid/GalleryItemStarIconButton.tsx
index 71607ff3ecb..cca87de2da6 100644
--- a/invokeai/frontend/web/src/features/gallery/components/ImageGrid/GalleryItemStarIconButton.tsx
+++ b/invokeai/frontend/web/src/features/gallery/components/ImageGrid/GalleryItemStarIconButton.tsx
@@ -1,5 +1,6 @@
import { DndImageIcon } from 'features/dnd/DndImageIcon';
import { memo, useCallback } from 'react';
+import { useTranslation } from 'react-i18next';
import { PiStarBold, PiStarFill } from 'react-icons/pi';
import { useStarImagesMutation, useUnstarImagesMutation } from 'services/api/endpoints/images';
import type { ImageDTO } from 'services/api/types';
@@ -9,6 +10,7 @@ type Props = {
};
export const GalleryItemStarIconButton = memo(({ imageDTO }: Props) => {
+ const { t } = useTranslation();
const [starImages] = useStarImagesMutation();
const [unstarImages] = useUnstarImagesMutation();
@@ -24,7 +26,7 @@ export const GalleryItemStarIconButton = memo(({ imageDTO }: Props) => {
: }
- tooltip={imageDTO.starred ? 'Unstar' : 'Star'}
+ tooltip={imageDTO.starred ? t('gallery.unstarImage') : t('gallery.starImage')}
position="absolute"
top={2}
insetInlineEnd={2}
diff --git a/invokeai/frontend/web/src/features/gallery/components/ImageGrid/GalleryItemVideoStarIconButton.tsx b/invokeai/frontend/web/src/features/gallery/components/ImageGrid/GalleryItemVideoStarIconButton.tsx
new file mode 100644
index 00000000000..c9894f53656
--- /dev/null
+++ b/invokeai/frontend/web/src/features/gallery/components/ImageGrid/GalleryItemVideoStarIconButton.tsx
@@ -0,0 +1,37 @@
+import { DndImageIcon } from 'features/dnd/DndImageIcon';
+import { memo, useCallback } from 'react';
+import { useTranslation } from 'react-i18next';
+import { PiStarBold, PiStarFill } from 'react-icons/pi';
+import { useStarVideosMutation, useUnstarVideosMutation } from 'services/api/endpoints/videos';
+import type { VideoDTO } from 'services/api/types';
+
+type Props = {
+ videoDTO: VideoDTO;
+};
+
+export const GalleryItemVideoStarIconButton = memo(({ videoDTO }: Props) => {
+ const { t } = useTranslation();
+ const [starVideos] = useStarVideosMutation();
+ const [unstarVideos] = useUnstarVideosMutation();
+
+ const toggleStarredState = useCallback(() => {
+ if (videoDTO.starred) {
+ unstarVideos({ video_names: [videoDTO.video_name] });
+ } else {
+ starVideos({ video_names: [videoDTO.video_name] });
+ }
+ }, [starVideos, unstarVideos, videoDTO]);
+
+ return (
+ : }
+ tooltip={videoDTO.starred ? t('gallery.unstarVideo', { count: 1 }) : t('gallery.starVideo', { count: 1 })}
+ position="absolute"
+ top={2}
+ insetInlineEnd={2}
+ />
+ );
+});
+
+GalleryItemVideoStarIconButton.displayName = 'GalleryItemVideoStarIconButton';
diff --git a/invokeai/frontend/web/src/features/gallery/components/ImageGrid/GalleryVideoItem.tsx b/invokeai/frontend/web/src/features/gallery/components/ImageGrid/GalleryVideoItem.tsx
new file mode 100644
index 00000000000..76aa7b8ae81
--- /dev/null
+++ b/invokeai/frontend/web/src/features/gallery/components/ImageGrid/GalleryVideoItem.tsx
@@ -0,0 +1,182 @@
+import { combine } from '@atlaskit/pragmatic-drag-and-drop/combine';
+import { draggable } from '@atlaskit/pragmatic-drag-and-drop/element/adapter';
+import { Flex } from '@invoke-ai/ui-library';
+import { createSelector } from '@reduxjs/toolkit';
+import type { AppDispatch, AppGetState } from 'app/store/store';
+import { useAppSelector, useAppStore } from 'app/store/storeHooks';
+import { useMiddleClickOpenInNewTab } from 'common/hooks/useMiddleClickOpenInNewTab';
+import { uniq } from 'es-toolkit';
+import { multipleVideoDndSource, singleVideoDndSource } from 'features/dnd/dnd';
+import { firefoxDndFix } from 'features/dnd/util';
+import { useVideoContextMenu } from 'features/gallery/components/ContextMenu/VideoContextMenu';
+import {
+ selectAlwaysShouldImageSizeBadge,
+ selectSelectedBoardId,
+ selectSelection,
+} from 'features/gallery/store/gallerySelectors';
+import { selectGallerySlice, selectionChanged } from 'features/gallery/store/gallerySlice';
+import { selectCachedGalleryItemNames } from 'features/gallery/store/selectCachedGalleryItemNames';
+import { isVideoName } from 'features/gallery/store/types';
+import { navigationApi } from 'features/ui/layouts/navigation-api';
+import { VIEWER_PANEL_ID } from 'features/ui/layouts/shared';
+import type { MouseEvent, MouseEventHandler } from 'react';
+import { memo, useCallback, useEffect, useMemo, useRef, useState } from 'react';
+import type { VideoDTO } from 'services/api/types';
+
+import { galleryItemContainerSX } from './galleryItemContainerSX';
+import { GalleryItemPlayBadge } from './GalleryItemPlayBadge';
+import { GalleryItemSizeBadge } from './GalleryItemSizeBadge';
+import { GalleryItemVideoStarIconButton } from './GalleryItemVideoStarIconButton';
+import { GalleryVideoThumbnail } from './GalleryVideoThumbnail';
+
+interface Props {
+ videoDTO: VideoDTO;
+}
+
+/**
+ * Click handler for selection. Mirrors the image grid's logic but reads the polymorphic
+ * /gallery/items/names cache to know the full ordered list (since a shift-range across a
+ * mixed image+video gallery has to include both kinds).
+ *
+ * Video items do not participate in alt-click comparison (comparison is image-only).
+ */
+const buildOnClick =
+ (videoName: string, dispatch: AppDispatch, getState: AppGetState) => (e: MouseEvent) => {
+ const { shiftKey, ctrlKey, metaKey, altKey } = e;
+ // We need the same query args the gallery grid used to fetch its name list. The grid
+ // calls `useGalleryItemNames` which forwards the args to the polymorphic gallery endpoint.
+ // Pull the most recent cached entry to recover the ordering.
+ const state = getState();
+ const itemNames = selectCachedGalleryItemNames(state);
+
+ if (itemNames.length === 0) {
+ // Without an ordered list, only basic single-click selection is possible.
+ if (!shiftKey && !ctrlKey && !metaKey && !altKey) {
+ dispatch(selectionChanged([videoName]));
+ }
+ return;
+ }
+
+ const selection = state.gallery.selection;
+
+ if (altKey) {
+ // Alt-click is image-only (comparison view). Quietly treat as a normal click for videos.
+ dispatch(selectionChanged([videoName]));
+ } else if (shiftKey) {
+ const lastSelectedItem = selection.at(-1);
+ const lastClickedIndex = itemNames.findIndex((name) => name === lastSelectedItem);
+ const currentClickedIndex = itemNames.findIndex((name) => name === videoName);
+ if (lastClickedIndex > -1 && currentClickedIndex > -1) {
+ const start = Math.min(lastClickedIndex, currentClickedIndex);
+ const end = Math.max(lastClickedIndex, currentClickedIndex);
+ const itemsToSelect = itemNames.slice(start, end + 1);
+ if (currentClickedIndex < lastClickedIndex) {
+ itemsToSelect.reverse();
+ }
+ dispatch(selectionChanged(uniq(selection.concat(itemsToSelect))));
+ }
+ } else if (ctrlKey || metaKey) {
+ if (selection.some((n) => n === videoName) && selection.length > 1) {
+ dispatch(selectionChanged(uniq(selection.filter((n) => n !== videoName))));
+ } else {
+ dispatch(selectionChanged(uniq(selection.concat(videoName))));
+ }
+ } else {
+ dispatch(selectionChanged([videoName]));
+ }
+ };
+
+export const GalleryVideoItem = memo(({ videoDTO }: Props) => {
+ const store = useAppStore();
+ const ref = useRef(null);
+ const [isHovered, setIsHovered] = useState(false);
+ const alwaysShowSizeBadge = useAppSelector(selectAlwaysShouldImageSizeBadge);
+
+ const selectIsSelected = useMemo(
+ () => createSelector(selectGallerySlice, (gallery) => gallery.selection.some((n) => n === videoDTO.video_name)),
+ [videoDTO.video_name]
+ );
+ const isSelected = useAppSelector(selectIsSelected);
+
+ const onMouseOver = useCallback(() => setIsHovered(true), []);
+ const onMouseOut = useCallback(() => setIsHovered(false), []);
+
+ const onClick = useMemo(() => buildOnClick(videoDTO.video_name, store.dispatch, store.getState), [videoDTO, store]);
+
+ const onDoubleClick = useCallback>(() => {
+ navigationApi.focusPanelInActiveTab(VIEWER_PANEL_ID);
+ }, []);
+
+ // Reuse the image item's size-badge component — its only inputs are width/height.
+ const sizeBadgeImageStandIn = useMemo(
+ () => ({ width: videoDTO.width, height: videoDTO.height }),
+ [videoDTO.width, videoDTO.height]
+ );
+
+ // Right-click / long-press context menu (delete, change board, download).
+ useVideoContextMenu(videoDTO, ref);
+
+ // The "middle click opens in new tab" setting covers videos too. Unlike GalleryImage
+ // (whose inner DndImage registers its own handler and the wrapper only catches the
+ // padding), the thumbnail here registers nothing — so catch clicks anywhere in the item.
+ useMiddleClickOpenInNewTab(ref, videoDTO.video_url);
+
+ // Register the item as a drag source so users can drop videos onto node fields,
+ // ref-image inputs, etc. — mirrors DndImage for image gallery items.
+ useEffect(() => {
+ const element = ref.current;
+ if (!element) {
+ return;
+ }
+ return combine(
+ firefoxDndFix(element),
+ draggable({
+ element,
+ getInitialData: () => {
+ // When the dragged video is part of a multi-selection, send the whole selection so a
+ // bulk move-to-board fires for every selected item. Mixed selections (videos + images)
+ // ride along in the same payload: the board drop handler splits them and dispatches
+ // both mutations. Without this, only the single dragged video would move.
+ const state = store.getState();
+ const selection = selectSelection(state);
+ const boardId = selectSelectedBoardId(state);
+ if (selection.length > 1 && selection.includes(videoDTO.video_name)) {
+ const video_names = selection.filter(isVideoName);
+ const image_names = selection.filter((n) => !isVideoName(n));
+ return multipleVideoDndSource.getData({
+ video_names,
+ image_names,
+ board_id: boardId,
+ });
+ }
+ return singleVideoDndSource.getData({ videoDTO }, videoDTO.video_name);
+ },
+ })
+ );
+ }, [videoDTO, store]);
+
+ return (
+
+
+
+ {(isHovered || alwaysShowSizeBadge) && (
+ [0]['imageDTO']}
+ />
+ )}
+ {(isHovered || videoDTO.starred) && }
+
+ );
+});
+
+GalleryVideoItem.displayName = 'GalleryVideoItem';
diff --git a/invokeai/frontend/web/src/features/gallery/components/ImageGrid/GalleryVideoThumbnail.test.ts b/invokeai/frontend/web/src/features/gallery/components/ImageGrid/GalleryVideoThumbnail.test.ts
new file mode 100644
index 00000000000..3c58300f19f
--- /dev/null
+++ b/invokeai/frontend/web/src/features/gallery/components/ImageGrid/GalleryVideoThumbnail.test.ts
@@ -0,0 +1,31 @@
+import { describe, expect, it } from 'vitest';
+
+import { getVideoThumbnailKey, getVideoThumbnailMode } from './GalleryVideoThumbnail';
+
+describe('getVideoThumbnailMode', () => {
+ it('uses the video after the current thumbnail fails', () => {
+ expect(getVideoThumbnailMode('thumb.webp', { url: 'thumb.webp', mediaCookieVersion: 1 }, 1)).toBe('video');
+ });
+
+ it('retries the image when the thumbnail URL changes', () => {
+ expect(getVideoThumbnailMode('new-thumb.webp', { url: 'old-thumb.webp', mediaCookieVersion: 1 }, 1)).toBe('image');
+ });
+
+ it('retries the image after the media cookie refreshes', () => {
+ expect(getVideoThumbnailMode('thumb.webp', { url: 'thumb.webp', mediaCookieVersion: 1 }, 2)).toBe('image');
+ });
+
+ it('returns to the video when the post-refresh image retry fails', () => {
+ expect(getVideoThumbnailMode('thumb.webp', { url: 'thumb.webp', mediaCookieVersion: 2 }, 2)).toBe('video');
+ });
+});
+
+describe('getVideoThumbnailKey', () => {
+ it('changes after the media cookie refreshes', () => {
+ expect(getVideoThumbnailKey('thumb.webp', 2)).not.toBe(getVideoThumbnailKey('thumb.webp', 1));
+ });
+
+ it('is stable when neither the URL nor cookie version changes', () => {
+ expect(getVideoThumbnailKey('thumb.webp', 1)).toBe(getVideoThumbnailKey('thumb.webp', 1));
+ });
+});
diff --git a/invokeai/frontend/web/src/features/gallery/components/ImageGrid/GalleryVideoThumbnail.tsx b/invokeai/frontend/web/src/features/gallery/components/ImageGrid/GalleryVideoThumbnail.tsx
new file mode 100644
index 00000000000..ad1630f9294
--- /dev/null
+++ b/invokeai/frontend/web/src/features/gallery/components/ImageGrid/GalleryVideoThumbnail.tsx
@@ -0,0 +1,84 @@
+import { Box, Image } from '@invoke-ai/ui-library';
+import { getMediaUrl, useMediaCookieRefreshVersion } from 'features/auth/store/mediaCookieRefresh';
+import type { SyntheticEvent } from 'react';
+import { memo, useCallback, useState } from 'react';
+
+type Props = {
+ thumbnailUrl: string;
+ videoUrl: string;
+};
+
+type FailedThumbnail = {
+ url: string;
+ mediaCookieVersion: number;
+};
+
+export const getVideoThumbnailMode = (
+ thumbnailUrl: string,
+ failedThumbnail: FailedThumbnail | null,
+ mediaCookieVersion: number
+): 'image' | 'video' =>
+ thumbnailUrl === failedThumbnail?.url && mediaCookieVersion === failedThumbnail.mediaCookieVersion
+ ? 'video'
+ : 'image';
+
+export const getVideoThumbnailKey = (thumbnailUrl: string, mediaCookieVersion: number) =>
+ `${mediaCookieVersion}:${thumbnailUrl}`;
+
+export const GalleryVideoThumbnail = memo(({ thumbnailUrl, videoUrl }: Props) => {
+ const mediaCookieVersion = useMediaCookieRefreshVersion();
+ const refreshedThumbnailUrl = getMediaUrl(thumbnailUrl, mediaCookieVersion);
+ const refreshedVideoUrl = getMediaUrl(videoUrl, mediaCookieVersion);
+ const [failedThumbnail, setFailedThumbnail] = useState(null);
+ const onError = useCallback(
+ () => setFailedThumbnail({ url: thumbnailUrl, mediaCookieVersion }),
+ [mediaCookieVersion, thumbnailUrl]
+ );
+
+ // With preload="metadata" some browsers populate dimensions/duration but don't decode
+ // and paint the first frame until playback or a seek — the tile would just show its
+ // black background. Mirror CurrentVideoPreview's near-zero seek to nudge the decoder.
+ const onLoadedMetadata = useCallback((e: SyntheticEvent) => {
+ const el = e.currentTarget as HTMLVideoElement;
+ if (el.currentTime === 0) {
+ try {
+ el.currentTime = 0.0001;
+ } catch {
+ // Some browsers throw if metadata isn't fully ready yet; harmless.
+ }
+ }
+ }, []);
+
+ if (getVideoThumbnailMode(thumbnailUrl, failedThumbnail, mediaCookieVersion) === 'video') {
+ return (
+
+ );
+ }
+
+ return (
+
+ );
+});
+
+GalleryVideoThumbnail.displayName = 'GalleryVideoThumbnail';
diff --git a/invokeai/frontend/web/src/features/gallery/components/ImageMetadataViewer/VideoMetadataViewer.tsx b/invokeai/frontend/web/src/features/gallery/components/ImageMetadataViewer/VideoMetadataViewer.tsx
new file mode 100644
index 00000000000..a1aace15616
--- /dev/null
+++ b/invokeai/frontend/web/src/features/gallery/components/ImageMetadataViewer/VideoMetadataViewer.tsx
@@ -0,0 +1,88 @@
+import { ExternalLink, Flex, Tab, TabList, TabPanel, TabPanels, Tabs, Text } from '@invoke-ai/ui-library';
+import { IAINoContentFallback } from 'common/components/IAIImageFallback';
+import { memo } from 'react';
+import { useTranslation } from 'react-i18next';
+import { useGetVideoMetadataQuery, useGetVideoWorkflowQuery } from 'services/api/endpoints/videos';
+import type { VideoDTO } from 'services/api/types';
+
+import DataViewer from './DataViewer';
+
+type Props = {
+ video: VideoDTO;
+};
+
+/**
+ * Details overlay for a selected video. Counterpart to ImageMetadataViewer, trimmed to what
+ * videos carry: the raw generation metadata and the saved workflow/graph. (Per-field recall
+ * is an image-only concept for now.)
+ */
+const VideoMetadataViewer = ({ video }: Props) => {
+ const { t } = useTranslation();
+ const { data: metadata } = useGetVideoMetadataQuery(video.video_name);
+ const { data: workflowAndGraph } = useGetVideoWorkflowQuery(video.video_name, { skip: !video.has_workflow });
+
+ return (
+
+
+
+
+
+ {t('metadata.metadata')}
+ {t('metadata.workflow')}
+ {t('nodes.graph')}
+
+
+
+
+ {metadata ? (
+
+ ) : (
+
+ )}
+
+
+ {workflowAndGraph?.workflow ? (
+
+ ) : (
+
+ )}
+
+
+ {workflowAndGraph?.graph ? (
+
+ ) : (
+
+ )}
+
+
+
+
+ {video.video_name}
+
+
+ );
+};
+
+export default memo(VideoMetadataViewer);
diff --git a/invokeai/frontend/web/src/features/gallery/components/ImageViewer/CurrentVideoButtons.tsx b/invokeai/frontend/web/src/features/gallery/components/ImageViewer/CurrentVideoButtons.tsx
new file mode 100644
index 00000000000..e370655898e
--- /dev/null
+++ b/invokeai/frontend/web/src/features/gallery/components/ImageViewer/CurrentVideoButtons.tsx
@@ -0,0 +1,42 @@
+import { IconButton } from '@invoke-ai/ui-library';
+import { useStore } from '@nanostores/react';
+import { $hasTemplates } from 'features/nodes/store/nodesSlice';
+import { useLoadWorkflowWithDialog } from 'features/workflowLibrary/components/LoadWorkflowConfirmationAlertDialog';
+import { memo, useCallback, useMemo } from 'react';
+import { useTranslation } from 'react-i18next';
+import { PiFlowArrowBold } from 'react-icons/pi';
+import type { VideoDTO } from 'services/api/types';
+
+/**
+ * Viewer toolbar actions for a selected video. Counterpart to CurrentImageButtons, trimmed
+ * to what videos support: loading the saved workflow/graph into the editor. (Metadata
+ * recall and canvas sends are image-only concepts.)
+ */
+export const CurrentVideoButtons = memo(({ videoDTO }: { videoDTO: VideoDTO }) => {
+ const { t } = useTranslation();
+ const hasTemplates = useStore($hasTemplates);
+ const loadWorkflowWithDialog = useLoadWorkflowWithDialog();
+
+ const isEnabled = useMemo(() => videoDTO.has_workflow && hasTemplates, [hasTemplates, videoDTO]);
+
+ const load = useCallback(() => {
+ if (!isEnabled) {
+ return;
+ }
+ loadWorkflowWithDialog({ type: 'video', data: videoDTO.video_name });
+ }, [isEnabled, loadWorkflowWithDialog, videoDTO.video_name]);
+
+ return (
+ }
+ tooltip={t('nodes.loadWorkflow')}
+ aria-label={t('nodes.loadWorkflow')}
+ isDisabled={!isEnabled}
+ variant="link"
+ alignSelf="stretch"
+ onClick={load}
+ />
+ );
+});
+
+CurrentVideoButtons.displayName = 'CurrentVideoButtons';
diff --git a/invokeai/frontend/web/src/features/gallery/components/ImageViewer/CurrentVideoPreview.test.ts b/invokeai/frontend/web/src/features/gallery/components/ImageViewer/CurrentVideoPreview.test.ts
new file mode 100644
index 00000000000..9a22e8cc321
--- /dev/null
+++ b/invokeai/frontend/web/src/features/gallery/components/ImageViewer/CurrentVideoPreview.test.ts
@@ -0,0 +1,13 @@
+import { readFileSync } from 'node:fs';
+import { fileURLToPath } from 'node:url';
+
+import { describe, expect, it } from 'vitest';
+
+describe('CurrentVideoPreview playback errors', () => {
+ it('shows a persistent user-facing error instead of only restoring the retry button', () => {
+ const source = readFileSync(fileURLToPath(new URL('./CurrentVideoPreview.tsx', import.meta.url)), 'utf8');
+
+ expect(source).toContain("t('toast.videoPlaybackFailed')");
+ expect(source).toContain('onError={handleVideoError}');
+ });
+});
diff --git a/invokeai/frontend/web/src/features/gallery/components/ImageViewer/CurrentVideoPreview.tsx b/invokeai/frontend/web/src/features/gallery/components/ImageViewer/CurrentVideoPreview.tsx
new file mode 100644
index 00000000000..ccbb1c57a3b
--- /dev/null
+++ b/invokeai/frontend/web/src/features/gallery/components/ImageViewer/CurrentVideoPreview.tsx
@@ -0,0 +1,443 @@
+import { combine } from '@atlaskit/pragmatic-drag-and-drop/combine';
+import { draggable } from '@atlaskit/pragmatic-drag-and-drop/element/adapter';
+import { Box, Button, Flex, IconButton } from '@invoke-ai/ui-library';
+import { useStore } from '@nanostores/react';
+import { useAppSelector, useAppStore } from 'app/store/storeHooks';
+import { useClipboard } from 'common/hooks/useClipboard';
+import { useDownloadItem } from 'common/hooks/useDownloadImage';
+import { isMediaCookieSelfHealPending } from 'features/auth/hooks/useMediaCookieRefresh';
+import { useMediaUrl } from 'features/auth/store/mediaCookieRefresh';
+import { useDeleteVideoModalApi } from 'features/deleteVideoModal/store/state';
+import { multipleVideoDndSource, singleVideoDndSource } from 'features/dnd/dnd';
+import { firefoxDndFix } from 'features/dnd/util';
+import VideoMetadataViewer from 'features/gallery/components/ImageMetadataViewer/VideoMetadataViewer';
+import NextPrevItemButtons from 'features/gallery/components/NextPrevItemButtons';
+import { useNextPrevItemNavigation } from 'features/gallery/components/useNextPrevItemNavigation';
+import { selectSelectedBoardId, selectSelection } from 'features/gallery/store/gallerySelectors';
+import { isVideoName } from 'features/gallery/store/types';
+import { useRegisteredHotkeys } from 'features/system/components/HotkeysModal/useHotkeyData';
+import { toast } from 'features/toast/toast';
+import { navigationApi } from 'features/ui/layouts/navigation-api';
+import {
+ selectActiveTab,
+ selectShouldShowItemDetails,
+ selectShouldShowProgressInViewer,
+} from 'features/ui/store/uiSelectors';
+import type { AnimationProps } from 'framer-motion';
+import { AnimatePresence, motion } from 'framer-motion';
+import { memo, useCallback, useEffect, useRef, useState } from 'react';
+import { useTranslation } from 'react-i18next';
+import { PiArrowSquareOutBold, PiCopyBold, PiDownloadSimpleBold, PiTrashSimpleBold, PiXBold } from 'react-icons/pi';
+import type { VideoDTO } from 'services/api/types';
+
+import { useImageViewerContext } from './context';
+import { NoContentForViewer } from './NoContentForViewer';
+import { ProgressImage } from './ProgressImage2';
+import { ProgressIndicator } from './ProgressIndicator2';
+import { VideoPlayButtonOverlay } from './VideoPlayButtonOverlay';
+
+type Props = {
+ videoDTO: VideoDTO | null;
+};
+
+/**
+ * Counterpart to CurrentImagePreview for videos. A single element spans both states:
+ *
+ * - **idle**: muted, no controls. Without a `poster` attribute the browser decodes and
+ * displays the video's actual first frame at full resolution (much sharper than the
+ * small WebP gallery thumbnail upscaled to fit the viewer). A centered play button
+ * overlay sits on top.
+ * - **playing**: native HTML5 controls + audio. The element is the same DOM node, so the
+ * decoded buffer carries over — no reload when the user hits play.
+ *
+ * Changing the selected video swaps the element via `key={videoName}`, which discards the
+ * old playback state cleanly.
+ *
+ * Mirrors CurrentImagePreview's progress overlay so denoise previews from a new render
+ * appear on top of the previously-loaded video. Without this, a freshly generated render's
+ * progress images had nowhere to display whenever a video was the last-selected gallery
+ * item (and the user only saw the static first-frame still until the new video finished).
+ */
+export const CurrentVideoPreview = memo(({ videoDTO }: Props) => {
+ const videoUrl = useMediaUrl(videoDTO?.video_url);
+ const { t } = useTranslation();
+ const store = useAppStore();
+ const videoName = videoDTO?.video_name ?? null;
+ const videoRef = useRef(null);
+ const [isPlaying, setIsPlaying] = useState(false);
+ const shouldShowProgressInViewer = useAppSelector(selectShouldShowProgressInViewer);
+ const shouldShowItemDetails = useAppSelector(selectShouldShowItemDetails);
+ const activeTab = useAppSelector(selectActiveTab);
+ const deleteVideoModal = useDeleteVideoModalApi();
+ const { downloadItem } = useDownloadItem();
+ const clipboard = useClipboard();
+ const { $progressEvent, $progressImage, onLoadImage } = useImageViewerContext();
+ const progressEvent = useStore($progressEvent);
+ const progressImage = useStore($progressImage);
+ const withProgress = shouldShowProgressInViewer && progressImage !== null;
+ const { goToPreviousImage, goToNextImage, isFetching } = useNextPrevItemNavigation();
+
+ // Whenever the selected video changes, drop back to the idle still + play overlay.
+ useEffect(() => {
+ setIsPlaying(false);
+ }, [videoName]);
+
+ // Register the viewer's as a drag source so users can drag the currently-displayed
+ // video onto node fields (e.g. a Video Primitive's "Starting Video" input) directly from
+ // the viewer, just like they can from the gallery thumbnail. Mirrors GalleryVideoItem's
+ // setup. Without this, the bare element has no drag handler and the drop target
+ // sees nothing it can accept.
+ useEffect(() => {
+ const element = videoRef.current;
+ if (!element || !videoDTO) {
+ return;
+ }
+ return combine(
+ firefoxDndFix(element),
+ draggable({
+ element,
+ getInitialData: () => {
+ // Honor any active gallery multi-selection so dropping onto a board moves the whole
+ // batch, matching the gallery thumbnail's behavior.
+ const state = store.getState();
+ const selection = selectSelection(state);
+ const boardId = selectSelectedBoardId(state);
+ if (selection.length > 1 && selection.includes(videoDTO.video_name)) {
+ const video_names = selection.filter(isVideoName);
+ const image_names = selection.filter((n) => !isVideoName(n));
+ return multipleVideoDndSource.getData({
+ video_names,
+ image_names,
+ board_id: boardId,
+ });
+ }
+ return singleVideoDndSource.getData({ videoDTO }, videoDTO.video_name);
+ },
+ })
+ );
+ }, [videoDTO, store]);
+
+ const handleVideoError = useCallback(() => {
+ setIsPlaying(false);
+ // A restored session's request can 401 before the media-cookie self-heal
+ // completes; the URL version bumps and the element reloads once the cookie lands.
+ // Surfacing an error toast for that transient window would be a false alarm.
+ if (isMediaCookieSelfHealPending()) {
+ return;
+ }
+ toast({
+ id: 'VIDEO_PLAYBACK_FAILED',
+ status: 'error',
+ title: t('toast.videoPlaybackFailed'),
+ description: t('toast.videoPlaybackFailedDesc'),
+ });
+ }, [t]);
+
+ const handlePlay = useCallback(() => {
+ setIsPlaying(true);
+ // The ref points at the same element we'll re-render with controls/audio; calling
+ // play() here keeps the user gesture wired to playback without waiting for React.
+ // A rejected play() (blocked autoplay, codec error, missing media cookie) must roll
+ // the state back — otherwise the overlay stays hidden over a dead element and the
+ // rejection surfaces as an unhandled promise. An AbortError is benign — pause(),
+ // close, or navigating away interrupted a pending play() — so roll back silently.
+ videoRef.current?.play().catch((error: unknown) => {
+ if (error instanceof DOMException && error.name === 'AbortError') {
+ setIsPlaying(false);
+ return;
+ }
+ handleVideoError();
+ });
+ }, [handleVideoError]);
+
+ // Close: stop playback and drop back to the first-frame preview + play overlay. We
+ // explicitly pause() because toggling React's `controls` prop hides the chrome but does
+ // not stop playback. Seeking back to ~0 nudges the decoder to re-paint the first frame
+ // (mirroring handleLoadedMetadata's near-zero seek trick).
+ const handleClose = useCallback(() => {
+ const el = videoRef.current;
+ if (el) {
+ el.pause();
+ try {
+ el.currentTime = 0.0001;
+ } catch {
+ // Some browsers throw if metadata isn't fully ready yet; harmless.
+ }
+ }
+ setIsPlaying(false);
+ }, []);
+
+ const handleDelete = useCallback(async () => {
+ if (!videoDTO) {
+ return;
+ }
+ try {
+ await deleteVideoModal.delete([videoDTO.video_name]);
+ } catch {
+ // user canceled the confirmation dialog
+ }
+ }, [deleteVideoModal, videoDTO]);
+
+ const handleDownload = useCallback(() => {
+ if (!videoDTO) {
+ return;
+ }
+ void downloadItem(videoDTO.video_url, videoDTO.video_name);
+ }, [downloadItem, videoDTO]);
+
+ const handleOpenInNewTab = useCallback(() => {
+ if (!videoDTO) {
+ return;
+ }
+ window.open(videoDTO.video_url, '_blank', 'noopener,noreferrer');
+ }, [videoDTO]);
+
+ // Cross-browser clipboard support for raw `video/*` MIME types doesn't really exist — Chrome
+ // and Firefox both reject anything outside a small allow-list (image/png, image/jpeg, text).
+ // So instead we grab the currently-displayed frame off the element via a canvas and
+ // hand the resulting PNG to the standard image-clipboard path. The video is same-origin so
+ // the canvas doesn't taint.
+ const handleCopyFrame = useCallback(async () => {
+ const el = videoRef.current;
+ if (!el || !el.videoWidth || !el.videoHeight) {
+ return;
+ }
+ try {
+ const canvas = document.createElement('canvas');
+ canvas.width = el.videoWidth;
+ canvas.height = el.videoHeight;
+ const ctx = canvas.getContext('2d');
+ if (!ctx) {
+ throw new Error('Unable to acquire 2D canvas context');
+ }
+ ctx.drawImage(el, 0, 0);
+ const blob = await new Promise((resolve) => {
+ canvas.toBlob((b) => resolve(b), 'image/png');
+ });
+ if (!blob) {
+ throw new Error('Unable to encode frame as PNG');
+ }
+ clipboard.writeImage(blob, () => {
+ toast({
+ id: 'IMAGE_COPIED',
+ title: t('toast.imageCopied'),
+ status: 'success',
+ });
+ });
+ } catch (err) {
+ toast({
+ id: 'PROBLEM_COPYING_IMAGE',
+ title: t('toast.problemCopyingImage'),
+ description: String(err),
+ status: 'error',
+ });
+ }
+ }, [clipboard, t]);
+
+ // Mirror CurrentImagePreview's hover-driven next/prev gating so the arrows only intrude
+ // while the user is interacting with the viewer.
+ const [shouldShowNextPrevButtons, setShouldShowNextPrevButtons] = useState(false);
+ const timeoutId = useRef(0);
+ const onMouseOver = useCallback(() => {
+ setShouldShowNextPrevButtons(true);
+ window.clearTimeout(timeoutId.current);
+ }, []);
+ const onMouseOut = useCallback(() => {
+ timeoutId.current = window.setTimeout(() => {
+ setShouldShowNextPrevButtons(false);
+ }, 500);
+ }, []);
+
+ const handleViewerArrowNavigation = useCallback(
+ (event: KeyboardEvent, navigate: () => void) => {
+ if (!navigationApi.isViewerArrowNavigationMode(activeTab) || !videoDTO || isFetching) {
+ return;
+ }
+ if (event.target instanceof HTMLInputElement || event.target instanceof HTMLTextAreaElement) {
+ return;
+ }
+ event.preventDefault();
+ navigate();
+ },
+ [activeTab, videoDTO, isFetching]
+ );
+
+ const onHotkeyPrevImage = useCallback(
+ (event: KeyboardEvent) => {
+ handleViewerArrowNavigation(event, goToPreviousImage);
+ },
+ [goToPreviousImage, handleViewerArrowNavigation]
+ );
+
+ const onHotkeyNextImage = useCallback(
+ (event: KeyboardEvent) => {
+ handleViewerArrowNavigation(event, goToNextImage);
+ },
+ [goToNextImage, handleViewerArrowNavigation]
+ );
+
+ useRegisteredHotkeys({
+ id: 'galleryNavLeft',
+ category: 'gallery',
+ callback: onHotkeyPrevImage,
+ options: { preventDefault: true },
+ dependencies: [onHotkeyPrevImage],
+ });
+
+ useRegisteredHotkeys({
+ id: 'galleryNavRight',
+ category: 'gallery',
+ callback: onHotkeyNextImage,
+ options: { preventDefault: true },
+ dependencies: [onHotkeyNextImage],
+ });
+
+ // Analogous to in the image viewer: clear any stale
+ // denoise progress overlay once the new video's metadata is in. Without this, the
+ // ImageViewerContext atom stays set after a video render (there's no image load to
+ // trigger its clear), so the overlay sticks over the freshly-selected video forever.
+ //
+ // Also force a first-frame paint via a near-zero seek. With preload="metadata" some
+ // browsers populate dimensions/duration but don't actually decode and display the first
+ // video frame until playback or a seek — the element just shows its black background.
+ // Setting currentTime to 0.0001 nudges the decoder to paint without measurably advancing.
+ const handleLoadedMetadata = useCallback(() => {
+ onLoadImage();
+ const el = videoRef.current;
+ if (el && !isPlaying && el.currentTime === 0) {
+ try {
+ el.currentTime = 0.0001;
+ } catch {
+ // Some browsers throw if metadata isn't fully ready yet; harmless.
+ }
+ }
+ }, [isPlaying, onLoadImage]);
+
+ if (!videoDTO) {
+ return ;
+ }
+
+ return (
+
+
+ {!isPlaying && !withProgress && }
+ {shouldShowItemDetails && !withProgress && (
+
+
+
+ )}
+ {withProgress && (
+
+
+ {progressEvent && (
+
+ )}
+
+ )}
+ {/* Top action bar, right-aligned. Auto-sized Flex anchored to insetInlineEnd leaves the
+ rest of the viewer click-through so clicking the video still pauses native playback.
+ Order: open in new tab, copy frame, download, delete, then the labelled close button
+ farthest right (only while the player is active). */}
+
+ }
+ onClick={handleOpenInNewTab}
+ variant="solid"
+ size="sm"
+ />
+ }
+ onClick={handleCopyFrame}
+ variant="solid"
+ size="sm"
+ />
+ }
+ onClick={handleDownload}
+ variant="solid"
+ size="sm"
+ />
+ }
+ onClick={handleDelete}
+ colorScheme="error"
+ variant="solid"
+ size="sm"
+ />
+ {isPlaying && (
+ } onClick={handleClose} variant="solid" size="sm">
+ {t('gallery.closeVideoPlayer')}
+
+ )}
+
+
+ {shouldShowNextPrevButtons && (
+
+
+
+ )}
+
+
+ );
+});
+
+const initial: AnimationProps['initial'] = {
+ opacity: 0,
+};
+const animateArrows: AnimationProps['animate'] = {
+ opacity: 1,
+ transition: { duration: 0.07 },
+};
+const exit: AnimationProps['exit'] = {
+ opacity: 0,
+ transition: { duration: 0.07 },
+};
+
+CurrentVideoPreview.displayName = 'CurrentVideoPreview';
diff --git a/invokeai/frontend/web/src/features/gallery/components/ImageViewer/ImageComparisonHover.tsx b/invokeai/frontend/web/src/features/gallery/components/ImageViewer/ImageComparisonHover.tsx
index edfada8bc2d..6d336bacc52 100644
--- a/invokeai/frontend/web/src/features/gallery/components/ImageViewer/ImageComparisonHover.tsx
+++ b/invokeai/frontend/web/src/features/gallery/components/ImageViewer/ImageComparisonHover.tsx
@@ -2,6 +2,7 @@ import { Box, Flex, Image } from '@invoke-ai/ui-library';
import { useAppSelector } from 'app/store/storeHooks';
import { useBoolean } from 'common/hooks/useBoolean';
import { preventDefault } from 'common/util/stopPropagation';
+import { useMediaUrl } from 'features/auth/store/mediaCookieRefresh';
import { TRANSPARENCY_CHECKERBOARD_PATTERN_DARK_DATAURL } from 'features/controlLayers/konva/patterns/transparency-checkerboard-pattern';
import type { Dimensions } from 'features/controlLayers/store/types';
import { ImageComparisonLabel } from 'features/gallery/components/ImageViewer/ImageComparisonLabel';
@@ -15,6 +16,10 @@ export const ImageComparisonHover = memo(({ firstImage, secondImage, rect }: Com
const comparisonFit = useAppSelector(selectComparisonFit);
const imageContainerRef = useRef(null);
const mouseOver = useBoolean(false);
+ const firstImageUrl = useMediaUrl(firstImage.image_url);
+ const firstThumbnailUrl = useMediaUrl(firstImage.thumbnail_url);
+ const secondImageUrl = useMediaUrl(secondImage.image_url);
+ const secondThumbnailUrl = useMediaUrl(secondImage.thumbnail_url);
const fittedDims = useMemo(() => {
if (!rect) {
return { width: 0, height: 0 };
@@ -51,8 +56,8 @@ export const ImageComparisonHover = memo(({ firstImage, secondImage, rect }: Com
>
{
+ const imageUrl = useMediaUrl(imageDTO.image_url);
+ const thumbnailUrl = useMediaUrl(imageDTO.thumbnail_url);
return (
@@ -50,8 +53,8 @@ const SideBySideImage = memo(({ imageDTO, type }: { imageDTO: ImageDTO; type: 'f
h={imageDTO.height}
maxW="full"
maxH="full"
- src={imageDTO.image_url}
- fallbackSrc={imageDTO.thumbnail_url}
+ src={imageUrl}
+ fallbackSrc={thumbnailUrl}
objectFit="contain"
borderRadius="base"
/>
diff --git a/invokeai/frontend/web/src/features/gallery/components/ImageViewer/ImageComparisonSlider.tsx b/invokeai/frontend/web/src/features/gallery/components/ImageViewer/ImageComparisonSlider.tsx
index 1f0d64aafeb..154e550f68d 100644
--- a/invokeai/frontend/web/src/features/gallery/components/ImageViewer/ImageComparisonSlider.tsx
+++ b/invokeai/frontend/web/src/features/gallery/components/ImageViewer/ImageComparisonSlider.tsx
@@ -1,6 +1,7 @@
import { Box, Flex, Icon, Image } from '@invoke-ai/ui-library';
import { useAppSelector } from 'app/store/storeHooks';
import { preventDefault } from 'common/util/stopPropagation';
+import { useMediaUrl } from 'features/auth/store/mediaCookieRefresh';
import { TRANSPARENCY_CHECKERBOARD_PATTERN_DARK_DATAURL } from 'features/controlLayers/konva/patterns/transparency-checkerboard-pattern';
import type { Dimensions } from 'features/controlLayers/store/types';
import { ImageComparisonLabel } from 'features/gallery/components/ImageViewer/ImageComparisonLabel';
@@ -21,6 +22,10 @@ const HANDLE_LEFT_INITIAL_PX = `calc(${INITIAL_POS} - ${HANDLE_HITBOX / 2}px)`;
export const ImageComparisonSlider = memo(({ firstImage, secondImage, rect }: ComparisonProps) => {
const comparisonFit = useAppSelector(selectComparisonFit);
+ const firstImageUrl = useMediaUrl(firstImage.image_url);
+ const firstThumbnailUrl = useMediaUrl(firstImage.thumbnail_url);
+ const secondImageUrl = useMediaUrl(secondImage.image_url);
+ const secondThumbnailUrl = useMediaUrl(secondImage.thumbnail_url);
// How far the handle is from the left - this will be a CSS calculation that takes into account the handle width
const [left, setLeft] = useState(HANDLE_LEFT_INITIAL_PX);
@@ -130,8 +135,8 @@ export const ImageComparisonSlider = memo(({ firstImage, secondImage, rect }: Co
{
const { t } = useTranslation();
const lastSelectedItem = useAppSelector(selectLastSelectedItem);
- const lastSelectedImageDTO = useImageDTO(lastSelectedItem ?? null);
+ const galleryItem = useGalleryItemDTO(lastSelectedItem);
+
+ // Polymorphic preview: videos render the play-overlay/HTML5 video; images render the existing
+ // DndImage-based preview with progress / metadata / next-prev affordances.
+ let preview;
+ if (galleryItem?.kind === 'video') {
+ preview = ;
+ } else {
+ preview = ;
+ }
+
return (
-
-
+ {preview}
+ {galleryItem?.kind !== 'video' && (
+
+ )}
);
diff --git a/invokeai/frontend/web/src/features/gallery/components/ImageViewer/ImageViewerToolbar.tsx b/invokeai/frontend/web/src/features/gallery/components/ImageViewer/ImageViewerToolbar.tsx
index b963f5a80d6..1b04201f3c4 100644
--- a/invokeai/frontend/web/src/features/gallery/components/ImageViewer/ImageViewerToolbar.tsx
+++ b/invokeai/frontend/web/src/features/gallery/components/ImageViewer/ImageViewerToolbar.tsx
@@ -1,24 +1,31 @@
import { Flex, Spacer } from '@invoke-ai/ui-library';
import { useAppSelector } from 'app/store/storeHooks';
+import { useGalleryItemDTO } from 'common/hooks/useGalleryItemDTO';
import { ToggleMetadataViewerButton } from 'features/gallery/components/ImageViewer/ToggleMetadataViewerButton';
import { selectLastSelectedItem } from 'features/gallery/store/gallerySelectors';
import { memo } from 'react';
-import { useImageDTO } from 'services/api/endpoints/images';
import { CurrentImageButtons } from './CurrentImageButtons';
+import { CurrentVideoButtons } from './CurrentVideoButtons';
import { ToggleProgressButton } from './ToggleProgressButton';
export const ImageViewerToolbar = memo(() => {
const lastSelectedItem = useAppSelector(selectLastSelectedItem);
- const imageDTO = useImageDTO(lastSelectedItem);
+ const galleryItem = useGalleryItemDTO(lastSelectedItem);
+
+ // Images get the full action row; videos get their own trimmed row (load workflow). The
+ // metadata viewer toggle works for both kinds, and the progress button is media-agnostic.
+ const showImageActions = galleryItem?.kind === 'image';
+ const showVideoActions = galleryItem?.kind === 'video';
return (
- {imageDTO && }
+ {showImageActions && }
+ {showVideoActions && }
- {imageDTO && }
+ {(showImageActions || showVideoActions) && }
);
});
diff --git a/invokeai/frontend/web/src/features/gallery/components/ImageViewer/VideoPlayButtonOverlay.tsx b/invokeai/frontend/web/src/features/gallery/components/ImageViewer/VideoPlayButtonOverlay.tsx
new file mode 100644
index 00000000000..39777bdd90a
--- /dev/null
+++ b/invokeai/frontend/web/src/features/gallery/components/ImageViewer/VideoPlayButtonOverlay.tsx
@@ -0,0 +1,40 @@
+import { Flex, Icon } from '@invoke-ai/ui-library';
+import { memo } from 'react';
+import { useTranslation } from 'react-i18next';
+import { PiPlayFill } from 'react-icons/pi';
+
+type Props = {
+ onClick: () => void;
+};
+
+/**
+ * Large centered play button shown over the still thumbnail in the video viewer. Clicking it
+ * swaps the preview into HTML5 video playback (see CurrentVideoPreview).
+ */
+export const VideoPlayButtonOverlay = memo(({ onClick }: Props) => {
+ const { t } = useTranslation();
+ return (
+
+
+
+ );
+});
+
+VideoPlayButtonOverlay.displayName = 'VideoPlayButtonOverlay';
diff --git a/invokeai/frontend/web/src/features/gallery/components/use-gallery-image-names.test.ts b/invokeai/frontend/web/src/features/gallery/components/use-gallery-image-names.test.ts
new file mode 100644
index 00000000000..eff9123cc61
--- /dev/null
+++ b/invokeai/frontend/web/src/features/gallery/components/use-gallery-image-names.test.ts
@@ -0,0 +1,34 @@
+/**
+ * Pins the polymorphic name-flattening used by `useGalleryImageNames` for both regular boards
+ * and date-based virtual boards.
+ *
+ * The bug (PR #9163 review): virtual boards were image-only — selecting a virtual date fetched
+ * from the legacy image_names endpoint, so videos created on that date never appeared. The hook
+ * now consumes the by-date item_names endpoint, which returns the same (kind, name) refs as the
+ * regular gallery names endpoint, and this shared mapper must keep video refs in the flat list.
+ * (The server-side guarantee that a date query returns video refs is pinned by
+ * tests/app/routers/test_virtual_boards.py.)
+ */
+import type { GalleryItemRef } from 'services/api/types';
+import { describe, expect, it } from 'vitest';
+
+import { itemRefsToNames } from './use-gallery-image-names';
+
+describe('itemRefsToNames', () => {
+ it('keeps video refs interleaved with images, preserving order', () => {
+ const items: GalleryItemRef[] = [
+ { kind: 'image', name: 'newest.png' },
+ { kind: 'video', name: 'middle.mp4' },
+ { kind: 'image', name: 'oldest.png' },
+ ];
+ expect(itemRefsToNames(items)).toEqual(['newest.png', 'middle.mp4', 'oldest.png']);
+ });
+
+ it('handles a video-only list (video-only virtual date)', () => {
+ const items: GalleryItemRef[] = [
+ { kind: 'video', name: 'a.mp4' },
+ { kind: 'video', name: 'b.mp4' },
+ ];
+ expect(itemRefsToNames(items)).toEqual(['a.mp4', 'b.mp4']);
+ });
+});
diff --git a/invokeai/frontend/web/src/features/gallery/components/use-gallery-image-names.ts b/invokeai/frontend/web/src/features/gallery/components/use-gallery-image-names.ts
index 487c5609062..4749dc9de8b 100644
--- a/invokeai/frontend/web/src/features/gallery/components/use-gallery-image-names.ts
+++ b/invokeai/frontend/web/src/features/gallery/components/use-gallery-image-names.ts
@@ -3,59 +3,81 @@ import { EMPTY_ARRAY } from 'app/store/constants';
import { useAppSelector } from 'app/store/storeHooks';
import { selectGetImageNamesQueryArgs, selectSelectedBoardId } from 'features/gallery/store/gallerySelectors';
import { getDateFromVirtualBoardId, isVirtualBoardId } from 'features/gallery/store/types';
-import { useGetImageNamesQuery } from 'services/api/endpoints/images';
-import { useGetVirtualBoardImageNamesByDateQuery } from 'services/api/endpoints/virtual_boards';
+import { useMemo } from 'react';
+import { useGetGalleryItemNamesQuery } from 'services/api/endpoints/gallery';
+import { useGetVirtualBoardItemNamesByDateQuery } from 'services/api/endpoints/virtual_boards';
+import type { GalleryItemRef } from 'services/api/types';
import { useDebounce } from 'use-debounce';
-const selectFromResult = ({
+const selectFromGalleryItemNamesResult = ({
currentData,
isLoading,
isFetching,
}: {
- currentData?: { image_names: string[] };
+ currentData?: { items: GalleryItemRef[] };
isLoading: boolean;
isFetching: boolean;
}) => ({
- imageNames: currentData?.image_names ?? EMPTY_ARRAY,
+ items: currentData?.items ?? (EMPTY_ARRAY as GalleryItemRef[]),
isLoading,
isFetching,
});
-const queryOptions = {
+const galleryQueryOptions = {
refetchOnReconnect: true,
- selectFromResult,
+ selectFromResult: selectFromGalleryItemNamesResult,
};
+/**
+ * Flattens polymorphic (kind, name) refs into the ordered name list consumed by the gallery
+ * grid and navigation hotkeys. Video refs must pass through untouched — regular boards and
+ * date-based virtual boards both contain them. Exported for tests.
+ */
+export const itemRefsToNames = (items: GalleryItemRef[]): string[] => items.map((ref) => ref.name);
+
+/**
+ * Returns the ordered flat list of gallery item names. Names are polymorphic — both image and
+ * video names appear in the same list, interleaved by created_at. Callers that need to know the
+ * kind of a particular name use `isVideoName` from `features/gallery/store/types`.
+ *
+ * Virtual boards (date-based) go through their own by-date endpoint, which returns the same
+ * polymorphic (kind, name) refs as the regular gallery names endpoint.
+ */
export const useGalleryImageNames = () => {
const selectedBoardId = useAppSelector(selectSelectedBoardId);
- const _queryArgs = useAppSelector(selectGetImageNamesQueryArgs);
- const [queryArgs] = useDebounce(_queryArgs, 300);
+ const _imageQueryArgs = useAppSelector(selectGetImageNamesQueryArgs);
+ const [imageQueryArgs] = useDebounce(_imageQueryArgs, 300);
const isVirtual = isVirtualBoardId(selectedBoardId);
- // Regular board query
- const regularResult = useGetImageNamesQuery(isVirtual ? skipToken : queryArgs, queryOptions);
+ // The polymorphic gallery names endpoint shares the same filter args as the image names
+ // endpoint (board_id, categories, search_term, order_dir, starred_first, is_intermediate).
+ const galleryResult = useGetGalleryItemNamesQuery(isVirtual ? skipToken : imageQueryArgs, galleryQueryOptions);
- // Virtual board query
const date = isVirtual ? getDateFromVirtualBoardId(selectedBoardId) : '';
- const virtualResult = useGetVirtualBoardImageNamesByDateQuery(
+ const virtualResult = useGetVirtualBoardItemNamesByDateQuery(
isVirtual
? {
date,
- categories: queryArgs.categories ?? undefined,
- search_term: queryArgs.search_term || undefined,
- order_dir: queryArgs.order_dir,
- starred_first: queryArgs.starred_first,
+ categories: imageQueryArgs.categories ?? undefined,
+ search_term: imageQueryArgs.search_term || undefined,
+ order_dir: imageQueryArgs.order_dir,
+ starred_first: imageQueryArgs.starred_first,
}
: skipToken,
- queryOptions
+ galleryQueryOptions
);
- const result = isVirtual ? virtualResult : regularResult;
+ // Flat names + isLoading exposed for backward compatibility with the existing callers (paged
+ // grid, search, navigation hotkeys). The kind is recoverable from the filename extension.
+ const imageNames = useMemo(() => {
+ const items = isVirtual ? virtualResult.items : galleryResult.items;
+ return itemRefsToNames(items);
+ }, [isVirtual, virtualResult.items, galleryResult.items]);
return {
- imageNames: result.imageNames,
- isLoading: result.isLoading,
- isFetching: result.isFetching,
- queryArgs,
+ imageNames,
+ isLoading: isVirtual ? virtualResult.isLoading : galleryResult.isLoading,
+ isFetching: isVirtual ? virtualResult.isFetching : galleryResult.isFetching,
+ queryArgs: imageQueryArgs,
};
};
diff --git a/invokeai/frontend/web/src/features/gallery/contexts/VideoDTOContext.ts b/invokeai/frontend/web/src/features/gallery/contexts/VideoDTOContext.ts
new file mode 100644
index 00000000000..69b822fb1eb
--- /dev/null
+++ b/invokeai/frontend/web/src/features/gallery/contexts/VideoDTOContext.ts
@@ -0,0 +1,13 @@
+import { createContext, useContext } from 'react';
+import type { VideoDTO } from 'services/api/types';
+import { assert } from 'tsafe';
+
+const VideoDTOContext = createContext(null);
+
+export const VideoDTOContextProvider = VideoDTOContext.Provider;
+
+export const useVideoDTOContext = () => {
+ const dto = useContext(VideoDTOContext);
+ assert(dto !== null, 'useVideoDTOContext must be used within VideoDTOContextProvider');
+ return dto;
+};
diff --git a/invokeai/frontend/web/src/features/gallery/hooks/useHasImages.test.ts b/invokeai/frontend/web/src/features/gallery/hooks/useHasImages.test.ts
new file mode 100644
index 00000000000..8553399abf2
--- /dev/null
+++ b/invokeai/frontend/web/src/features/gallery/hooks/useHasImages.test.ts
@@ -0,0 +1,33 @@
+/**
+ * Regression test for the gallery-has-content decision (PR #9163 review).
+ *
+ * The bug: `useHasImages` decided emptiness from boards + the uncategorized *image* total
+ * only. A gallery whose sole content was one uncategorized video was treated as empty, so
+ * `NoContentForViewer` showed the new-user/get-started view instead of the normal
+ * no-selection state.
+ */
+import { describe, expect, it } from 'vitest';
+
+import { getHasGalleryContent } from './useHasImages';
+
+describe('getHasGalleryContent', () => {
+ it('counts a single uncategorized video as content (zero boards, zero uncategorized images)', () => {
+ expect(getHasGalleryContent([], 0, 1)).toBe(true);
+ });
+
+ it('is empty with zero boards, zero uncategorized images and zero uncategorized videos', () => {
+ expect(getHasGalleryContent([], 0, 0)).toBe(false);
+ });
+
+ it('counts a video-only board as content', () => {
+ expect(getHasGalleryContent([{ image_count: 0, video_count: 2 }], 0, 0)).toBe(true);
+ });
+
+ it('counts uncategorized images as content', () => {
+ expect(getHasGalleryContent([], 3, 0)).toBe(true);
+ });
+
+ it('defaults to content when no totals have arrived yet', () => {
+ expect(getHasGalleryContent(undefined, undefined, undefined)).toBe(true);
+ });
+});
diff --git a/invokeai/frontend/web/src/features/gallery/hooks/useHasImages.ts b/invokeai/frontend/web/src/features/gallery/hooks/useHasImages.ts
index bf94c465b95..8df053d7846 100644
--- a/invokeai/frontend/web/src/features/gallery/hooks/useHasImages.ts
+++ b/invokeai/frontend/web/src/features/gallery/hooks/useHasImages.ts
@@ -1,9 +1,30 @@
import { useMemo } from 'react';
-import { useListAllBoardsQuery } from 'services/api/endpoints/boards';
+import { useGetBoardVideosTotalQuery, useListAllBoardsQuery } from 'services/api/endpoints/boards';
import { useListImagesQuery } from 'services/api/endpoints/images';
export const LOADING_SYMBOL = Symbol('LOADING');
+/**
+ * Decides whether the gallery has any content. A gallery containing only uncategorized
+ * videos must count as non-empty — the video totals are not derivable from the boards
+ * list (which only covers real boards) or the uncategorized *image* total. Exported for
+ * tests.
+ */
+export const getHasGalleryContent = (
+ boardList: { image_count: number; video_count?: number }[] | undefined,
+ uncategorizedImagesTotal: number | undefined,
+ uncategorizedVideosTotal: number | undefined
+): boolean => {
+ if (boardList && boardList.some((board) => board.image_count + (board.video_count ?? 0) > 0)) {
+ return true;
+ }
+ if (uncategorizedImagesTotal === undefined && uncategorizedVideosTotal === undefined) {
+ // No data at all — default to "has content" so we don't flash the new-user view.
+ return true;
+ }
+ return (uncategorizedImagesTotal ?? 0) > 0 || (uncategorizedVideosTotal ?? 0) > 0;
+};
+
export const useHasImages = () => {
const { data: boardList, isLoading: loadingBoards } = useListAllBoardsQuery({ include_archived: true });
const { data: uncategorizedImages, isLoading: loadingImages } = useListImagesQuery({
@@ -12,22 +33,16 @@ export const useHasImages = () => {
limit: 0,
is_intermediate: false,
});
+ const { data: uncategorizedVideos, isLoading: loadingVideos } = useGetBoardVideosTotalQuery('none');
const hasImages = useMemo(() => {
// default to true
- if (loadingBoards || loadingImages) {
+ if (loadingBoards || loadingImages || loadingVideos) {
return LOADING_SYMBOL;
}
- const hasBoards = boardList && boardList.length > 0;
-
- if (hasBoards) {
- if (boardList.filter((board) => board.image_count > 0).length > 0) {
- return true;
- }
- }
- return uncategorizedImages ? uncategorizedImages.total > 0 : true;
- }, [boardList, uncategorizedImages, loadingBoards, loadingImages]);
+ return getHasGalleryContent(boardList, uncategorizedImages?.total, uncategorizedVideos?.total);
+ }, [boardList, uncategorizedImages, uncategorizedVideos, loadingBoards, loadingImages, loadingVideos]);
return hasImages;
};
diff --git a/invokeai/frontend/web/src/features/gallery/hooks/useRangeBasedImageFetching.test.ts b/invokeai/frontend/web/src/features/gallery/hooks/useRangeBasedImageFetching.test.ts
new file mode 100644
index 00000000000..6cec16aa043
--- /dev/null
+++ b/invokeai/frontend/web/src/features/gallery/hooks/useRangeBasedImageFetching.test.ts
@@ -0,0 +1,14 @@
+import { describe, expect, it } from 'vitest';
+
+import { getVideoPrefetchOptions, hasCachedVideoDTO } from './useRangeBasedImageFetching';
+
+describe('video range prefetch', () => {
+ it('does not retain an RTK Query subscription', () => {
+ expect(getVideoPrefetchOptions()).toEqual({ subscribe: false, forceRefetch: true });
+ });
+
+ it('only treats fulfilled DTO queries as cached', () => {
+ expect(hasCachedVideoDTO({ data: { video_name: 'video.mp4' } })).toBe(true);
+ expect(hasCachedVideoDTO({ isError: true })).toBe(false);
+ });
+});
diff --git a/invokeai/frontend/web/src/features/gallery/hooks/useRangeBasedImageFetching.ts b/invokeai/frontend/web/src/features/gallery/hooks/useRangeBasedImageFetching.ts
index 8ea8e9023e3..eab38776e5b 100644
--- a/invokeai/frontend/web/src/features/gallery/hooks/useRangeBasedImageFetching.ts
+++ b/invokeai/frontend/web/src/features/gallery/hooks/useRangeBasedImageFetching.ts
@@ -1,7 +1,9 @@
import { useAppStore } from 'app/store/storeHooks';
+import { isVideoName } from 'features/gallery/store/types';
import { useCallback, useEffect, useState } from 'react';
import type { ListRange } from 'react-virtuoso';
import { imagesApi, useGetImageDTOsByNamesMutation } from 'services/api/endpoints/images';
+import { videosApi } from 'services/api/endpoints/videos';
import { useThrottledCallback } from 'use-debounce';
interface UseRangeBasedImageFetchingArgs {
@@ -13,6 +15,11 @@ interface UseRangeBasedImageFetchingReturn {
onRangeChanged: (range: ListRange) => void;
}
+export const getVideoPrefetchOptions = () => ({ subscribe: false, forceRefetch: true }) as const;
+
+export const hasCachedVideoDTO = (queryState: { data?: unknown; isError?: boolean }): boolean =>
+ queryState.data !== undefined;
+
const getUncachedNames = (imageNames: string[], cachedImageNames: string[], ranges: ListRange[]): string[] => {
const uncachedNamesSet = new Set();
const cachedImageNamesSet = new Set(cachedImageNames);
@@ -30,9 +37,12 @@ const getUncachedNames = (imageNames: string[], cachedImageNames: string[], rang
};
/**
- * Hook for bulk fetching image DTOs based on the visible range from virtuoso.
- * Individual image components should use `useGetImageDTOQuery(imageName)` to get their specific DTO.
- * This hook ensures DTOs are bulk fetched and cached efficiently.
+ * Hook for bulk fetching gallery item DTOs based on the visible range from virtuoso.
+ *
+ * Names are polymorphic — image names go through the bulk `getImageDTOsByNames` mutation while
+ * video names dispatch individual `getVideoDTO` queries (the videos API doesn't have a batch
+ * endpoint yet; per-item is fine while video counts are low). Individual components still call
+ * `useGetImageDTOQuery` / `useGetVideoDTOQuery` to subscribe — this hook only triggers fetches.
*/
export const useRangeBasedImageFetching = ({
imageNames,
@@ -43,23 +53,36 @@ export const useRangeBasedImageFetching = ({
const [lastRange, setLastRange] = useState(null);
const [pendingRanges, setPendingRanges] = useState([]);
- const fetchImages = useCallback(
- (ranges: ListRange[], imageNames: string[]) => {
+ const fetchItems = useCallback(
+ (ranges: ListRange[], allNames: string[]) => {
if (!enabled) {
return;
}
- const cachedImageNames = imagesApi.util.selectCachedArgsForQuery(store.getState(), 'getImageDTO');
- const uncachedNames = getUncachedNames(imageNames, cachedImageNames, ranges);
- if (uncachedNames.length === 0) {
- return;
+ const state = store.getState();
+
+ // Images — bulk fetch via the existing batch endpoint.
+ const cachedImageNames = imagesApi.util.selectCachedArgsForQuery(state, 'getImageDTO');
+ const uncachedImageNames = getUncachedNames(allNames, cachedImageNames, ranges).filter((n) => !isVideoName(n));
+ if (uncachedImageNames.length > 0) {
+ getImageDTOsByNames({ image_names: uncachedImageNames });
}
- getImageDTOsByNames({ image_names: uncachedNames });
+
+ // Videos — fetch one at a time (no batch endpoint yet). Each `initiate()` is a no-op for
+ // already-cached entries, so this is safe to call repeatedly while scrolling.
+ const cachedVideoNames = videosApi.util
+ .selectCachedArgsForQuery(state, 'getVideoDTO')
+ .filter((videoName) => hasCachedVideoDTO(videosApi.endpoints.getVideoDTO.select(videoName)(state)));
+ const uncachedVideoNames = getUncachedNames(allNames, cachedVideoNames, ranges).filter((n) => isVideoName(n));
+ for (const videoName of uncachedVideoNames) {
+ store.dispatch(videosApi.endpoints.getVideoDTO.initiate(videoName, getVideoPrefetchOptions()));
+ }
+
setPendingRanges([]);
},
[enabled, getImageDTOsByNames, store]
);
- const throttledFetchImages = useThrottledCallback(fetchImages, 500);
+ const throttledFetchItems = useThrottledCallback(fetchItems, 500);
const onRangeChanged = useCallback((range: ListRange) => {
setLastRange(range);
@@ -68,8 +91,8 @@ export const useRangeBasedImageFetching = ({
useEffect(() => {
const combinedRanges = lastRange ? [...pendingRanges, lastRange] : pendingRanges;
- throttledFetchImages(combinedRanges, imageNames);
- }, [imageNames, lastRange, pendingRanges, throttledFetchImages]);
+ throttledFetchItems(combinedRanges, imageNames);
+ }, [imageNames, lastRange, pendingRanges, throttledFetchItems]);
return {
onRangeChanged,
diff --git a/invokeai/frontend/web/src/features/gallery/store/gallerySlice.test.ts b/invokeai/frontend/web/src/features/gallery/store/gallerySlice.test.ts
new file mode 100644
index 00000000000..202921424e4
--- /dev/null
+++ b/invokeai/frontend/web/src/features/gallery/store/gallerySlice.test.ts
@@ -0,0 +1,37 @@
+import { describe, expect, it } from 'vitest';
+
+import { gallerySliceConfig, imageSelected, imageToCompareChanged, selectionChanged } from './gallerySlice';
+
+const reducer = gallerySliceConfig.slice.reducer;
+
+describe('gallery comparison state', () => {
+ it('keeps comparison active when another image is selected', () => {
+ let state = reducer(undefined, imageToCompareChanged('comparison.png'));
+
+ state = reducer(state, imageSelected('selected.png'));
+
+ expect(state.imageToCompare).toBe('comparison.png');
+ });
+
+ it('clears comparison when a video is selected', () => {
+ let state = reducer(undefined, imageToCompareChanged('comparison.png'));
+
+ state = reducer(state, imageSelected('selected.mp4'));
+
+ expect(state.imageToCompare).toBeNull();
+ });
+
+ it('clears comparison when a multi-selection contains a video', () => {
+ let state = reducer(undefined, imageToCompareChanged('comparison.png'));
+
+ state = reducer(state, selectionChanged(['selected.png', 'selected.mp4']));
+
+ expect(state.imageToCompare).toBeNull();
+ });
+
+ it('rejects a video as the comparison item', () => {
+ const state = reducer(undefined, imageToCompareChanged('comparison.mp4'));
+
+ expect(state.imageToCompare).toBeNull();
+ });
+});
diff --git a/invokeai/frontend/web/src/features/gallery/store/gallerySlice.ts b/invokeai/frontend/web/src/features/gallery/store/gallerySlice.ts
index e4894b60766..2f07eff0dc5 100644
--- a/invokeai/frontend/web/src/features/gallery/store/gallerySlice.ts
+++ b/invokeai/frontend/web/src/features/gallery/store/gallerySlice.ts
@@ -12,6 +12,7 @@ import {
type ComparisonMode,
type GalleryState,
type GalleryView,
+ isVideoName,
isVirtualBoardId,
type OrderDir,
zGalleryState,
@@ -52,12 +53,18 @@ const slice = createSlice({
} else {
state.selection = [selectedItem];
}
+ if (selectedItem && isVideoName(selectedItem)) {
+ state.imageToCompare = null;
+ }
},
selectionChanged: (state, action: PayloadAction) => {
state.selection = uniq(action.payload);
+ if (state.selection.some(isVideoName)) {
+ state.imageToCompare = null;
+ }
},
imageToCompareChanged: (state, action: PayloadAction) => {
- state.imageToCompare = action.payload;
+ state.imageToCompare = action.payload && !isVideoName(action.payload) ? action.payload : null;
},
comparisonModeChanged: (state, action: PayloadAction) => {
state.comparisonMode = action.payload;
@@ -162,11 +169,15 @@ const slice = createSlice({
},
},
extraReducers(builder) {
- // Clear board-related state on logout to prevent stale data when switching users
+ // Clear board-related state on logout to prevent stale data when switching users.
+ // `selection` holds only item-name strings (never persisted, and the DTOs behind
+ // them are wiped by the store-level resetApiState on logout), but clear it anyway
+ // so the next user never sees the previous user's selection highlighted.
builder.addCase(logout, (state) => {
state.selectedBoardId = 'none';
state.autoAddBoardId = 'none';
state.boardSearchText = '';
+ state.selection = [];
});
},
});
diff --git a/invokeai/frontend/web/src/features/gallery/store/selectCachedGalleryItemNames.ts b/invokeai/frontend/web/src/features/gallery/store/selectCachedGalleryItemNames.ts
new file mode 100644
index 00000000000..a4cdbb0557f
--- /dev/null
+++ b/invokeai/frontend/web/src/features/gallery/store/selectCachedGalleryItemNames.ts
@@ -0,0 +1,81 @@
+import type { AppGetState } from 'app/store/store';
+import { galleryApi } from 'services/api/endpoints/gallery';
+import type { GetGalleryItemNamesArgs } from 'services/api/types';
+
+import { selectGetImageNamesQueryArgs } from './gallerySelectors';
+
+/**
+ * Returns the names (in display order) of the currently-cached gallery item list.
+ *
+ * The grid renders via the polymorphic ``getGalleryItemNames`` endpoint, which returns a
+ * mixed image+video list. Range-selection click handlers (shift-click for ranges, ctrl-click
+ * for discontiguous selection) need that ordered list to compute the items between two
+ * clicks.
+ *
+ * We look up the cache entry whose args match the gallery's current query args. RTK Query
+ * keeps recently-used entries warm (60s default ``keepUnusedDataFor``), so after a board
+ * switch the cache contains entries for every board the user has visited this session.
+ * Falling back to "the first invalidated entry" — the previous behaviour — silently picked
+ * a stale board's list, which manifested as shift-click range selection failing at random
+ * until the user did anything that invalidated ``GalleryItemNameList`` (move, delete) and
+ * forced a refetch.
+ */
+export const selectCachedGalleryItemNames = (state: ReturnType): string[] => {
+ const args = selectGetImageNamesQueryArgs(state);
+ // Exact match: the entry the grid is actively subscribed to. This is the common case.
+ const exact = galleryApi.endpoints.getGalleryItemNames.select(args)(state).data;
+ if (exact) {
+ return exact.items.map((ref) => ref.name);
+ }
+ // Debounce window: the grid hook debounces its args by ~300ms, so for a moment after the
+ // user changes a filter the cache key may not match Redux yet. Best-effort fallback to any
+ // cached entry on the same board so range selection still feels responsive — but do not
+ // silently fall back to an unrelated board's entry, which was the bug.
+ const entries = galleryApi.util.selectInvalidatedBy(state, ['GalleryItemNameList']);
+ for (const entry of entries) {
+ if (entry.endpointName !== 'getGalleryItemNames') {
+ continue;
+ }
+ const entryArgs = entry.originalArgs as GetGalleryItemNamesArgs | undefined;
+ if (!entryArgs || entryArgs.board_id !== args.board_id) {
+ continue;
+ }
+ const data = galleryApi.endpoints.getGalleryItemNames.select(entryArgs)(state).data;
+ if (data) {
+ return data.items.map((ref) => ref.name);
+ }
+ }
+ return [];
+};
+
+/**
+ * Given the ordered gallery list, the index of the currently-displayed item in that list,
+ * and the set of names being deleted, return the name that should be selected after the
+ * deletion completes — preferring the immediate predecessor, falling back to the immediate
+ * successor, and returning null if every remaining item was also deleted.
+ *
+ * Used by the image and video delete flows so that clearing the displayed item from the
+ * Viewer lands on an adjacent gallery item instead of the empty-state placeholder.
+ */
+export const pickSelectionAfterDelete = (
+ galleryItemNames: string[],
+ deletedIndex: number,
+ deletedNames: Set
+): string | null => {
+ if (deletedIndex < 0) {
+ return null;
+ }
+ for (let i = deletedIndex - 1; i >= 0; i--) {
+ const name = galleryItemNames[i];
+ if (name && !deletedNames.has(name)) {
+ return name;
+ }
+ }
+ for (let i = deletedIndex + 1; i < galleryItemNames.length; i++) {
+ const name = galleryItemNames[i];
+ if (name && !deletedNames.has(name)) {
+ return name;
+ }
+ }
+ return null;
+};
diff --git a/invokeai/frontend/web/src/features/gallery/store/types.ts b/invokeai/frontend/web/src/features/gallery/store/types.ts
index c040e5834d7..c844aead1c7 100644
--- a/invokeai/frontend/web/src/features/gallery/store/types.ts
+++ b/invokeai/frontend/web/src/features/gallery/store/types.ts
@@ -48,3 +48,11 @@ const VIRTUAL_BOARD_ID_PREFIX = 'by_date:';
export const isVirtualBoardId = (id: string): boolean => id.startsWith(VIRTUAL_BOARD_ID_PREFIX);
export const getDateFromVirtualBoardId = (id: string): string => id.replace(VIRTUAL_BOARD_ID_PREFIX, '');
+
+/**
+ * The polymorphic gallery treats selection as `string[]` of names. The kind is recoverable from
+ * the filename extension since the backend names images with `.png` and videos with `.mp4` (see
+ * SimpleNameService). Centralizing the discriminator here so callers don't have to know about
+ * the extension contract.
+ */
+export const isVideoName = (name: string): boolean => name.toLowerCase().endsWith('.mp4');
diff --git a/invokeai/frontend/web/src/features/imageActions/actions.ts b/invokeai/frontend/web/src/features/imageActions/actions.ts
index 2c9293127b4..8b454e4af01 100644
--- a/invokeai/frontend/web/src/features/imageActions/actions.ts
+++ b/invokeai/frontend/web/src/features/imageActions/actions.ts
@@ -35,14 +35,15 @@ import {
import { calculateNewSize } from 'features/controlLayers/util/getScaledBoundingBoxDimensions';
import { imageToCompareChanged, selectionChanged } from 'features/gallery/store/gallerySlice';
import type { BoardId } from 'features/gallery/store/types';
-import { fieldImageValueChanged } from 'features/nodes/store/nodesSlice';
+import { fieldImageValueChanged, fieldVideoValueChanged } from 'features/nodes/store/nodesSlice';
import type { FieldIdentifier } from 'features/nodes/types/field';
import { upscaleInitialImageChanged } from 'features/parameters/store/upscaleSlice';
import { getOptimalDimension } from 'features/parameters/util/optimalDimension';
import { navigationApi } from 'features/ui/layouts/navigation-api';
import { WORKSPACE_PANEL_ID } from 'features/ui/layouts/shared';
import { imageDTOToFile, imagesApi, uploadImage } from 'services/api/endpoints/images';
-import type { ImageDTO } from 'services/api/types';
+import { videosApi } from 'services/api/endpoints/videos';
+import type { ImageDTO, VideoDTO } from 'services/api/types';
import type { Equals } from 'tsafe';
import { assert } from 'tsafe';
@@ -75,6 +76,15 @@ export const setNodeImageFieldImage = (arg: {
dispatch(fieldImageValueChanged({ ...fieldIdentifier, value: imageDTO }));
};
+export const setNodeVideoFieldVideo = (arg: {
+ videoDTO: VideoDTO;
+ fieldIdentifier: FieldIdentifier;
+ dispatch: AppDispatch;
+}) => {
+ const { videoDTO, fieldIdentifier, dispatch } = arg;
+ dispatch(fieldVideoValueChanged({ ...fieldIdentifier, value: videoDTO }));
+};
+
export const setComparisonImage = (arg: { image_name: string; dispatch: AppDispatch }) => {
const { image_name, dispatch } = arg;
dispatch(imageToCompareChanged(image_name));
@@ -323,3 +333,40 @@ export const removeImagesFromBoard = (arg: { image_names: string[]; dispatch: Ap
dispatch(imagesApi.endpoints.removeImagesFromBoard.initiate({ image_names }, { track: false }));
dispatch(selectionChanged([]));
};
+
+// Single-video counterparts to addImagesToBoard / removeImagesFromBoard. The video router
+// only exposes single-video endpoints today (POST/DELETE /api/v1/videos/board), so the
+// callers loop per video. Backend permissions: add requires _assert_board_write_access
+// (admin/owner/public dest) AND _assert_video_direct_owner; remove requires
+// _assert_video_direct_owner plus write access on the *source* board.
+export const addVideoToBoard = (arg: { video_name: string; boardId: BoardId; dispatch: AppDispatch }) => {
+ const { video_name, boardId, dispatch } = arg;
+ dispatch(videosApi.endpoints.addVideoToBoard.initiate({ video_name, board_id: boardId }, { track: false }));
+ dispatch(selectionChanged([]));
+};
+
+export const removeVideoFromBoard = (arg: { video_name: string; dispatch: AppDispatch }) => {
+ const { video_name, dispatch } = arg;
+ dispatch(videosApi.endpoints.removeVideoFromBoard.initiate({ video_name }, { track: false }));
+ dispatch(selectionChanged([]));
+};
+
+// Bulk helpers. No batch endpoint exists for the video router yet, so we fan out per-video over
+// the existing singular mutation — same pattern the change-board modal already uses. Callers
+// (drag-and-drop bulk move-to-board, future bulk actions) get to share the selection-clear and
+// keep their wiring symmetrical with the image-side helpers.
+export const addVideosToBoard = (arg: { video_names: string[]; boardId: BoardId; dispatch: AppDispatch }) => {
+ const { video_names, boardId, dispatch } = arg;
+ for (const video_name of video_names) {
+ dispatch(videosApi.endpoints.addVideoToBoard.initiate({ video_name, board_id: boardId }, { track: false }));
+ }
+ dispatch(selectionChanged([]));
+};
+
+export const removeVideosFromBoard = (arg: { video_names: string[]; dispatch: AppDispatch }) => {
+ const { video_names, dispatch } = arg;
+ for (const video_name of video_names) {
+ dispatch(videosApi.endpoints.removeVideoFromBoard.initiate({ video_name }, { track: false }));
+ }
+ dispatch(selectionChanged([]));
+};
diff --git a/invokeai/frontend/web/src/features/lora/components/LoRASelect.tsx b/invokeai/frontend/web/src/features/lora/components/LoRASelect.tsx
index 2d043c9c816..5394bb67a65 100644
--- a/invokeai/frontend/web/src/features/lora/components/LoRASelect.tsx
+++ b/invokeai/frontend/web/src/features/lora/components/LoRASelect.tsx
@@ -44,6 +44,25 @@ const LoRASelect = () => {
) {
return model.variant === currentMainModelConfig.variant;
}
+ // For Wan: A14B (t2v_a14b/i2v_a14b) and TI2V-5B have different inner
+ // dims (5120 vs 3072) — applying the wrong variant crashes the layer
+ // patcher. LoRAs whose variant couldn't be detected (null) are kept
+ // so we don't silently hide ambiguous ones.
+ if (
+ currentMainModelConfig?.base === 'wan' &&
+ 'variant' in currentMainModelConfig &&
+ currentMainModelConfig.variant &&
+ 'variant' in model &&
+ model.variant
+ ) {
+ const expected =
+ currentMainModelConfig.variant === 't2v_a14b' || currentMainModelConfig.variant === 'i2v_a14b'
+ ? 'a14b'
+ : currentMainModelConfig.variant === 'ti2v_5b'
+ ? '5b'
+ : null;
+ return expected === null || model.variant === expected;
+ }
return true;
});
}, [modelConfigs, currentBaseModel, currentMainModelConfig]);
diff --git a/invokeai/frontend/web/src/features/metadata/parsing.tsx b/invokeai/frontend/web/src/features/metadata/parsing.tsx
index bc2623045e1..793e595303b 100644
--- a/invokeai/frontend/web/src/features/metadata/parsing.tsx
+++ b/invokeai/frontend/web/src/features/metadata/parsing.tsx
@@ -57,6 +57,11 @@ import {
setZImageSeedVarianceStrength,
setZImageShift,
vaeSelected,
+ wanComponentSourceSelected,
+ wanGuidanceScaleLowNoiseChanged,
+ wanT5EncoderModelSelected,
+ wanTransformerLowNoiseSelected,
+ wanVaeModelSelected,
widthChanged,
zImageQwen3EncoderModelSelected,
zImageQwen3SourceModelSelected,
@@ -858,6 +863,133 @@ const QwenImageShift: SingleMetadataHandler = {
};
//#endregion QwenImageShift
+//#region WanTransformerLowNoise
+const WanTransformerLowNoise: SingleMetadataHandler = {
+ [SingleMetadataKey]: true,
+ type: 'WanTransformerLowNoise',
+ parse: (metadata, _store) => {
+ const raw = getProperty(metadata, 'wan_transformer_low_noise');
+ // Reject when the key is absent so the handler is not rendered for non-Wan images
+ if (raw === undefined) {
+ return Promise.reject();
+ }
+ if (raw === null) {
+ return Promise.resolve(null);
+ }
+ return Promise.resolve(zModelIdentifierField.parse(raw));
+ },
+ recall: (value, store) => {
+ store.dispatch(wanTransformerLowNoiseSelected(value));
+ },
+ i18nKey: 'modelManager.wanTransformerLowNoise',
+ LabelComponent: MetadataLabel,
+ ValueComponent: ({ value }: SingleMetadataValueProps) => (
+
+ ),
+};
+//#endregion WanTransformerLowNoise
+
+//#region WanComponentSource
+const WanComponentSource: SingleMetadataHandler = {
+ [SingleMetadataKey]: true,
+ type: 'WanComponentSource',
+ parse: (metadata, _store) => {
+ const raw = getProperty(metadata, 'wan_component_source');
+ if (raw === undefined) {
+ return Promise.reject();
+ }
+ if (raw === null) {
+ return Promise.resolve(null);
+ }
+ return Promise.resolve(zModelIdentifierField.parse(raw));
+ },
+ recall: (value, store) => {
+ store.dispatch(wanComponentSourceSelected(value));
+ },
+ i18nKey: 'modelManager.wanComponentSource',
+ LabelComponent: MetadataLabel,
+ ValueComponent: ({ value }: SingleMetadataValueProps) => (
+
+ ),
+};
+//#endregion WanComponentSource
+
+//#region WanVaeModel
+const WanVaeModel: SingleMetadataHandler = {
+ [SingleMetadataKey]: true,
+ type: 'WanVaeModel',
+ parse: (metadata, _store) => {
+ const raw = getProperty(metadata, 'wan_vae_model');
+ if (raw === undefined) {
+ return Promise.reject();
+ }
+ if (raw === null) {
+ return Promise.resolve(null);
+ }
+ return Promise.resolve(zModelIdentifierField.parse(raw));
+ },
+ recall: (value, store) => {
+ store.dispatch(wanVaeModelSelected(value));
+ },
+ i18nKey: 'modelManager.wanVae',
+ LabelComponent: MetadataLabel,
+ ValueComponent: ({ value }: SingleMetadataValueProps) => (
+
+ ),
+};
+//#endregion WanVaeModel
+
+//#region WanT5EncoderModel
+const WanT5EncoderModel: SingleMetadataHandler = {
+ [SingleMetadataKey]: true,
+ type: 'WanT5EncoderModel',
+ parse: (metadata, _store) => {
+ const raw = getProperty(metadata, 'wan_t5_encoder_model');
+ if (raw === undefined) {
+ return Promise.reject();
+ }
+ if (raw === null) {
+ return Promise.resolve(null);
+ }
+ return Promise.resolve(zModelIdentifierField.parse(raw));
+ },
+ recall: (value, store) => {
+ store.dispatch(wanT5EncoderModelSelected(value));
+ },
+ i18nKey: 'modelManager.wanT5Encoder',
+ LabelComponent: MetadataLabel,
+ ValueComponent: ({ value }: SingleMetadataValueProps) => (
+
+ ),
+};
+//#endregion WanT5EncoderModel
+
+//#region WanGuidanceScaleLowNoise
+const WanGuidanceScaleLowNoise: SingleMetadataHandler = {
+ [SingleMetadataKey]: true,
+ type: 'WanGuidanceScaleLowNoise',
+ parse: (metadata, _store) => {
+ const raw = getProperty(metadata, 'wan_guidance_scale_low_noise');
+ if (raw === undefined) {
+ return Promise.reject();
+ }
+ if (raw === null) {
+ return Promise.resolve(null);
+ }
+ const parsed = z.number().parse(raw);
+ return Promise.resolve(parsed);
+ },
+ recall: (value, store) => {
+ store.dispatch(wanGuidanceScaleLowNoiseChanged(value));
+ },
+ i18nKey: 'parameters.wanGuidanceScaleLowNoise',
+ LabelComponent: MetadataLabel,
+ ValueComponent: ({ value }: SingleMetadataValueProps) => (
+
+ ),
+};
+//#endregion WanGuidanceScaleLowNoise
+
//#region ZImageShift
const ZImageShift: SingleMetadataHandler = {
[SingleMetadataKey]: true,
@@ -1654,6 +1786,11 @@ export const ImageMetadataHandlers = {
QwenImageQwenVLEncoderModel,
QwenImageQuantization,
QwenImageShift,
+ WanTransformerLowNoise,
+ WanComponentSource,
+ WanVaeModel,
+ WanT5EncoderModel,
+ WanGuidanceScaleLowNoise,
ZImageShift,
LoRAs,
CanvasLayers,
diff --git a/invokeai/frontend/web/src/features/modelManagerV2/models.ts b/invokeai/frontend/web/src/features/modelManagerV2/models.ts
index cf295c9af6a..48db55da321 100644
--- a/invokeai/frontend/web/src/features/modelManagerV2/models.ts
+++ b/invokeai/frontend/web/src/features/modelManagerV2/models.ts
@@ -23,6 +23,7 @@ import {
isTIModelConfig,
isUnknownModelConfig,
isVAEModelConfig,
+ isWanT5EncoderModelConfig,
} from 'services/api/types';
import { objectEntries } from 'tsafe';
@@ -85,6 +86,11 @@ const MODEL_CATEGORIES: Record = {
i18nKey: 'modelManager.qwenVLEncoder',
filter: isQwenVLEncoderModelConfig,
},
+ wan_t5_encoder: {
+ category: 'wan_t5_encoder',
+ i18nKey: 'modelManager.wanT5Encoder',
+ filter: isWanT5EncoderModelConfig,
+ },
control_lora: {
category: 'control_lora',
i18nKey: 'modelManager.controlLora',
@@ -165,6 +171,7 @@ export const MODEL_BASE_TO_COLOR: Record = {
'z-image': 'cyan',
external: 'orange',
anima: 'invokePurple',
+ wan: 'cyan',
unknown: 'red',
};
@@ -187,6 +194,7 @@ export const MODEL_TYPE_TO_LONG_NAME: Record = {
t5_encoder: 'T5 Encoder',
qwen3_encoder: 'Qwen3 Encoder',
qwen_vl_encoder: 'Qwen2.5-VL Encoder',
+ wan_t5_encoder: 'Wan T5 Encoder',
clip_embed: 'CLIP Embed',
siglip: 'SigLIP',
flux_redux: 'FLUX Redux',
@@ -212,6 +220,7 @@ export const MODEL_BASE_TO_LONG_NAME: Record = {
'z-image': 'Z-Image',
external: 'External',
anima: 'Anima',
+ wan: 'Wan 2.2',
unknown: 'Unknown',
};
@@ -232,6 +241,7 @@ export const MODEL_BASE_TO_SHORT_NAME: Record = {
'z-image': 'Z-Image',
external: 'External',
anima: 'Anima',
+ wan: 'Wan',
unknown: 'Unknown',
};
@@ -252,6 +262,11 @@ export const MODEL_VARIANT_TO_LONG_NAME: Record = {
gigantic: 'CLIP G',
generate: 'Qwen Image',
edit: 'Qwen Image Edit',
+ t2v_a14b: 'Wan 2.2 T2V A14B',
+ i2v_a14b: 'Wan 2.2 I2V A14B',
+ ti2v_5b: 'Wan 2.2 TI2V 5B',
+ a14b: 'Wan 2.2 A14B LoRA',
+ '5b': 'Wan 2.2 5B LoRA',
qwen3_4b: 'Qwen3 4B',
qwen3_8b: 'Qwen3 8B',
qwen3_06b: 'Qwen3 0.6B',
@@ -271,6 +286,7 @@ export const MODEL_FORMAT_TO_LONG_NAME: Record = {
t5_encoder: 'T5 Encoder',
qwen3_encoder: 'Qwen3 Encoder',
qwen_vl_encoder: 'Qwen2.5-VL Encoder',
+ wan_t5_encoder: 'Wan T5 Encoder (UMT5-XXL)',
bnb_quantized_int8b: 'BNB Quantized (int8b)',
bnb_quantized_nf4b: 'BNB Quantized (nf4b)',
gguf_quantized: 'GGUF Quantized',
@@ -279,7 +295,7 @@ export const MODEL_FORMAT_TO_LONG_NAME: Record = {
export const SUPPORTS_OPTIMIZED_DENOISING_BASE_MODELS: BaseModelType[] = ['flux', 'sd-3'];
-export const SUPPORTS_REF_IMAGES_BASE_MODELS: BaseModelType[] = ['sd-1', 'sdxl', 'flux', 'flux2', 'qwen-image'];
+export const SUPPORTS_REF_IMAGES_BASE_MODELS: BaseModelType[] = ['sd-1', 'sdxl', 'flux', 'flux2', 'qwen-image', 'wan'];
export const SUPPORTS_NEGATIVE_PROMPT_BASE_MODELS: BaseModelType[] = [
'sd-1',
@@ -290,4 +306,5 @@ export const SUPPORTS_NEGATIVE_PROMPT_BASE_MODELS: BaseModelType[] = [
'sd-3',
'z-image',
'anima',
+ 'wan',
];
diff --git a/invokeai/frontend/web/src/features/modelManagerV2/subpanels/ModelManagerPanel/ModelFormatBadge.tsx b/invokeai/frontend/web/src/features/modelManagerV2/subpanels/ModelManagerPanel/ModelFormatBadge.tsx
index 71d2efe0e45..1473e6dd076 100644
--- a/invokeai/frontend/web/src/features/modelManagerV2/subpanels/ModelManagerPanel/ModelFormatBadge.tsx
+++ b/invokeai/frontend/web/src/features/modelManagerV2/subpanels/ModelManagerPanel/ModelFormatBadge.tsx
@@ -16,6 +16,7 @@ const FORMAT_NAME_MAP: Record = {
t5_encoder: 't5_encoder',
qwen3_encoder: 'qwen3_encoder',
qwen_vl_encoder: 'qwen_vl_encoder',
+ wan_t5_encoder: 'wan_t5_encoder',
bnb_quantized_int8b: 'bnb_quantized_int8b',
bnb_quantized_nf4b: 'quantized',
gguf_quantized: 'gguf',
@@ -37,6 +38,7 @@ const FORMAT_COLOR_MAP: Record = {
t5_encoder: 'base',
qwen3_encoder: 'base',
qwen_vl_encoder: 'base',
+ wan_t5_encoder: 'base',
bnb_quantized_int8b: 'base',
bnb_quantized_nf4b: 'base',
gguf_quantized: 'base',
diff --git a/invokeai/frontend/web/src/features/modelManagerV2/subpanels/ModelPanel/EncoderModelSettings/EncoderModelSettings.tsx b/invokeai/frontend/web/src/features/modelManagerV2/subpanels/ModelPanel/EncoderModelSettings/EncoderModelSettings.tsx
index 455df7673a9..daa85ac4904 100644
--- a/invokeai/frontend/web/src/features/modelManagerV2/subpanels/ModelPanel/EncoderModelSettings/EncoderModelSettings.tsx
+++ b/invokeai/frontend/web/src/features/modelManagerV2/subpanels/ModelPanel/EncoderModelSettings/EncoderModelSettings.tsx
@@ -8,6 +8,7 @@ import type {
SigLIPModelConfig,
T5EncoderModelConfig,
TextLLMModelConfig,
+ WanT5EncoderModelConfig,
} from 'services/api/types';
type EncoderModelConfig =
@@ -17,7 +18,8 @@ type EncoderModelConfig =
| CLIPVisionModelConfig
| SigLIPModelConfig
| LlavaOnevisionModelConfig
- | TextLLMModelConfig;
+ | TextLLMModelConfig
+ | WanT5EncoderModelConfig;
type Props = {
modelConfig: EncoderModelConfig;
diff --git a/invokeai/frontend/web/src/features/modelManagerV2/subpanels/ModelPanel/Fields/ModelImageUpload.tsx b/invokeai/frontend/web/src/features/modelManagerV2/subpanels/ModelPanel/Fields/ModelImageUpload.tsx
index 929d16285df..ffd7388070f 100644
--- a/invokeai/frontend/web/src/features/modelManagerV2/subpanels/ModelPanel/Fields/ModelImageUpload.tsx
+++ b/invokeai/frontend/web/src/features/modelManagerV2/subpanels/ModelPanel/Fields/ModelImageUpload.tsx
@@ -1,7 +1,7 @@
import type { SystemStyleObject } from '@invoke-ai/ui-library';
import { Box, IconButton, Image } from '@invoke-ai/ui-library';
-import { dropzoneAccept } from 'common/hooks/useImageUploadButton';
import { typedMemo } from 'common/util/typedMemo';
+import { imageDropzoneAccept } from 'common/util/uploadMediaAccept';
import { toast } from 'features/toast/toast';
import { useCallback, useState } from 'react';
import { useDropzone } from 'react-dropzone';
@@ -96,7 +96,7 @@ const ModelImageUpload = ({ model_key, model_image }: Props) => {
}, [model_key, t, deleteModelImage]);
const { getInputProps, getRootProps } = useDropzone({
- accept: dropzoneAccept,
+ accept: imageDropzoneAccept,
onDropAccepted,
noDrag: true,
multiple: false,
diff --git a/invokeai/frontend/web/src/features/modelManagerV2/subpanels/ModelPanel/ModelView.tsx b/invokeai/frontend/web/src/features/modelManagerV2/subpanels/ModelPanel/ModelView.tsx
index e666ebfd1b4..d9cba9a47ef 100644
--- a/invokeai/frontend/web/src/features/modelManagerV2/subpanels/ModelPanel/ModelView.tsx
+++ b/invokeai/frontend/web/src/features/modelManagerV2/subpanels/ModelPanel/ModelView.tsx
@@ -23,6 +23,7 @@ import {
type SigLIPModelConfig,
type T5EncoderModelConfig,
type TextLLMModelConfig,
+ type WanT5EncoderModelConfig,
} from 'services/api/types';
import { isExternalModel } from './isExternalModel';
@@ -40,7 +41,8 @@ type EncoderModelConfig =
| CLIPVisionModelConfig
| SigLIPModelConfig
| LlavaOnevisionModelConfig
- | TextLLMModelConfig;
+ | TextLLMModelConfig
+ | WanT5EncoderModelConfig;
const isEncoderModel = (modelConfig: AnyModelConfigWithExternal): modelConfig is EncoderModelConfig => {
return (
@@ -50,7 +52,8 @@ const isEncoderModel = (modelConfig: AnyModelConfigWithExternal): modelConfig is
modelConfig.type === 'clip_vision' ||
modelConfig.type === 'siglip' ||
modelConfig.type === 'llava_onevision' ||
- modelConfig.type === 'text_llm'
+ modelConfig.type === 'text_llm' ||
+ modelConfig.type === 'wan_t5_encoder'
);
};
diff --git a/invokeai/frontend/web/src/features/nodes/components/flow/nodes/CurrentImage/CurrentImageNode.tsx b/invokeai/frontend/web/src/features/nodes/components/flow/nodes/CurrentImage/CurrentImageNode.tsx
index c8423a8fe4e..20ee4b21c04 100644
--- a/invokeai/frontend/web/src/features/nodes/components/flow/nodes/CurrentImage/CurrentImageNode.tsx
+++ b/invokeai/frontend/web/src/features/nodes/components/flow/nodes/CurrentImage/CurrentImageNode.tsx
@@ -6,6 +6,7 @@ import { IAINoContentFallback } from 'common/components/IAIImageFallback';
import { DndImage } from 'features/dnd/DndImage';
import NextPrevItemButtons from 'features/gallery/components/NextPrevItemButtons';
import { selectLastSelectedItem } from 'features/gallery/store/gallerySelectors';
+import { isVideoName } from 'features/gallery/store/types';
import NonInvocationNodeWrapper from 'features/nodes/components/flow/nodes/common/NonInvocationNodeWrapper';
import { DRAG_HANDLE_CLASSNAME } from 'features/nodes/types/constants';
import type { AnimationProps } from 'framer-motion';
@@ -19,7 +20,12 @@ import { $lastProgressEvent } from 'services/events/stores';
const CurrentImageNode = (props: NodeProps) => {
const lastSelectedItem = useAppSelector(selectLastSelectedItem);
const lastProgressEvent = useStore($lastProgressEvent);
- const imageDTO = useImageDTO(lastSelectedItem);
+ // Pass a real name only when the selection is an image. Videos use the polymorphic
+ // gallery and would otherwise trigger GET /api/v1/images/i/.mp4 — which 404s
+ // and emits a noisy "Image record not found" backend log every time a video is
+ // clicked in the gallery while a Current Image node is in the workflow.
+ const imageName = lastSelectedItem && !isVideoName(lastSelectedItem) ? lastSelectedItem : null;
+ const imageDTO = useImageDTO(imageName);
if (lastProgressEvent?.image) {
return (
diff --git a/invokeai/frontend/web/src/features/nodes/components/flow/nodes/Invocation/InvocationNodeFooter.tsx b/invokeai/frontend/web/src/features/nodes/components/flow/nodes/Invocation/InvocationNodeFooter.tsx
index 890666b0c4e..e025fe39fe2 100644
--- a/invokeai/frontend/web/src/features/nodes/components/flow/nodes/Invocation/InvocationNodeFooter.tsx
+++ b/invokeai/frontend/web/src/features/nodes/components/flow/nodes/Invocation/InvocationNodeFooter.tsx
@@ -1,7 +1,7 @@
import type { ChakraProps } from '@invoke-ai/ui-library';
import { Flex, FormControlGroup } from '@invoke-ai/ui-library';
import { useIsExecutableNode } from 'features/nodes/hooks/useIsBatchNode';
-import { useNodeHasImageOutput } from 'features/nodes/hooks/useNodeHasImageOutput';
+import { useNodeHasGalleryOutput } from 'features/nodes/hooks/useNodeHasGalleryOutput';
import { DRAG_HANDLE_CLASSNAME } from 'features/nodes/types/constants';
import { memo } from 'react';
@@ -15,7 +15,7 @@ type Props = {
const props: ChakraProps = { w: 'unset' };
const InvocationNodeFooter = ({ nodeId }: Props) => {
- const hasImageOutput = useNodeHasImageOutput();
+ const hasGalleryOutput = useNodeHasGalleryOutput();
const isExecutableNode = useIsExecutableNode();
return (
{
>
{isExecutableNode && }
- {isExecutableNode && hasImageOutput && }
+ {isExecutableNode && hasGalleryOutput && }
);
diff --git a/invokeai/frontend/web/src/features/nodes/components/flow/nodes/Invocation/SaveToGalleryCheckbox.tsx b/invokeai/frontend/web/src/features/nodes/components/flow/nodes/Invocation/SaveToGalleryCheckbox.tsx
index 7d05ba63da6..4e9912a87be 100644
--- a/invokeai/frontend/web/src/features/nodes/components/flow/nodes/Invocation/SaveToGalleryCheckbox.tsx
+++ b/invokeai/frontend/web/src/features/nodes/components/flow/nodes/Invocation/SaveToGalleryCheckbox.tsx
@@ -1,6 +1,6 @@
import { Checkbox, FormControl, FormLabel } from '@invoke-ai/ui-library';
import { useAppDispatch } from 'app/store/storeHooks';
-import { useNodeHasImageOutput } from 'features/nodes/hooks/useNodeHasImageOutput';
+import { useNodeHasGalleryOutput } from 'features/nodes/hooks/useNodeHasGalleryOutput';
import { useNodeIsIntermediate } from 'features/nodes/hooks/useNodeIsIntermediate';
import { nodeIsIntermediateChanged } from 'features/nodes/store/nodesSlice';
import { NO_PAN_CLASS } from 'features/nodes/types/constants';
@@ -11,7 +11,7 @@ import { useTranslation } from 'react-i18next';
const SaveToGalleryCheckbox = ({ nodeId }: { nodeId: string }) => {
const { t } = useTranslation();
const dispatch = useAppDispatch();
- const hasImageOutput = useNodeHasImageOutput();
+ const hasGalleryOutput = useNodeHasGalleryOutput();
const isIntermediate = useNodeIsIntermediate();
const handleChange = useCallback(
(e: ChangeEvent) => {
@@ -25,7 +25,7 @@ const SaveToGalleryCheckbox = ({ nodeId }: { nodeId: string }) => {
[dispatch, nodeId]
);
- if (!hasImageOutput) {
+ if (!hasGalleryOutput) {
return null;
}
diff --git a/invokeai/frontend/web/src/features/nodes/components/flow/nodes/Invocation/fields/InputFieldRenderer.tsx b/invokeai/frontend/web/src/features/nodes/components/flow/nodes/Invocation/fields/InputFieldRenderer.tsx
index 053f8a09985..d73a840f6bc 100644
--- a/invokeai/frontend/web/src/features/nodes/components/flow/nodes/Invocation/fields/InputFieldRenderer.tsx
+++ b/invokeai/frontend/web/src/features/nodes/components/flow/nodes/Invocation/fields/InputFieldRenderer.tsx
@@ -15,6 +15,7 @@ import { StringGeneratorFieldInputComponent } from 'features/nodes/components/fl
import { IntegerFieldInput } from 'features/nodes/components/flow/nodes/Invocation/fields/IntegerField/IntegerFieldInput';
import { IntegerFieldInputAndSlider } from 'features/nodes/components/flow/nodes/Invocation/fields/IntegerField/IntegerFieldInputAndSlider';
import { IntegerFieldSlider } from 'features/nodes/components/flow/nodes/Invocation/fields/IntegerField/IntegerFieldSlider';
+import { VideoFrameIndexFieldInput } from 'features/nodes/components/flow/nodes/Invocation/fields/IntegerField/VideoFrameIndexFieldInput';
import { StringFieldDropdown } from 'features/nodes/components/flow/nodes/Invocation/fields/StringField/StringFieldDropdown';
import { StringFieldInput } from 'features/nodes/components/flow/nodes/Invocation/fields/StringField/StringFieldInput';
import { StringFieldTextarea } from 'features/nodes/components/flow/nodes/Invocation/fields/StringField/StringFieldTextarea';
@@ -63,6 +64,8 @@ import {
isStringGeneratorFieldInputTemplate,
isStylePresetFieldInputInstance,
isStylePresetFieldInputTemplate,
+ isVideoFieldInputInstance,
+ isVideoFieldInputTemplate,
} from 'features/nodes/types/field';
import type { NodeFieldElement } from 'features/nodes/types/workflow';
import { memo } from 'react';
@@ -76,6 +79,7 @@ import EnumFieldInputComponent from './inputs/EnumFieldInputComponent';
import ImageFieldInputComponent from './inputs/ImageFieldInputComponent';
import SchedulerFieldInputComponent from './inputs/SchedulerFieldInputComponent';
import StylePresetFieldInputComponent from './inputs/StylePresetFieldInputComponent';
+import VideoFieldInputComponent from './inputs/VideoFieldInputComponent';
type Props = {
nodeId: string;
@@ -137,6 +141,13 @@ export const InputFieldRenderer = memo(({ nodeId, fieldName, settings }: Props)
if (!isIntegerFieldInputInstance(field)) {
return null;
}
+ // The ``video-frame-index`` ui_component bolts a frame thumbnail + scrubber slider
+ // onto the standard integer input. It's a per-field widget rather than a node-body
+ // widget so it works in both the workflow editor and the Form Builder (both of
+ // which dispatch through this same renderer).
+ if (template.ui_component === 'video-frame-index') {
+ return ;
+ }
if (!settings || settings.type !== 'integer-field-config') {
return ;
}
@@ -215,6 +226,13 @@ export const InputFieldRenderer = memo(({ nodeId, fieldName, settings }: Props)
return ;
}
+ if (isVideoFieldInputTemplate(template)) {
+ if (!isVideoFieldInputInstance(field)) {
+ return null;
+ }
+ return ;
+ }
+
if (isBoardFieldInputTemplate(template)) {
if (!isBoardFieldInputInstance(field)) {
return null;
diff --git a/invokeai/frontend/web/src/features/nodes/components/flow/nodes/Invocation/fields/IntegerField/VideoFrameIndexFieldInput.tsx b/invokeai/frontend/web/src/features/nodes/components/flow/nodes/Invocation/fields/IntegerField/VideoFrameIndexFieldInput.tsx
new file mode 100644
index 00000000000..0f3b3eefb23
--- /dev/null
+++ b/invokeai/frontend/web/src/features/nodes/components/flow/nodes/Invocation/fields/IntegerField/VideoFrameIndexFieldInput.tsx
@@ -0,0 +1,201 @@
+import { CompositeNumberInput, CompositeSlider, Flex, Text } from '@invoke-ai/ui-library';
+import { createSelector } from '@reduxjs/toolkit';
+import { skipToken } from '@reduxjs/toolkit/query';
+import { useAppSelector } from 'app/store/storeHooks';
+import { useMediaUrl } from 'features/auth/store/mediaCookieRefresh';
+import type { FieldComponentProps } from 'features/nodes/components/flow/nodes/Invocation/fields/inputs/types';
+import { useIntegerField } from 'features/nodes/components/flow/nodes/Invocation/fields/IntegerField/useIntegerField';
+import { selectFieldInputInstanceSafe, selectNodesSlice } from 'features/nodes/store/selectors';
+import { NO_DRAG_CLASS } from 'features/nodes/types/constants';
+import type { IntegerFieldInputInstance, IntegerFieldInputTemplate } from 'features/nodes/types/field';
+import { isVideoFieldInputInstance } from 'features/nodes/types/field';
+import { memo, useEffect, useMemo, useRef } from 'react';
+import { useTranslation } from 'react-i18next';
+import { useGetVideoDTOQuery } from 'services/api/endpoints/videos';
+
+/**
+ * Integer-field renderer used by ``start_frame`` and ``end_frame`` on the
+ * ``extract_video_range`` invocation. Renders the standard number input plus a
+ * live frame thumbnail and a scrubber slider, all writing to the same Redux
+ * integer field. The thumbnail is a ```` element seeked to
+ * ``frame / fps``; browsers display the frame natively without a canvas
+ * roundtrip.
+ *
+ * The widget looks up its companion ``VideoField`` directly via Redux on the
+ * same node, so it works wherever ``InputFieldRenderer`` is used — the
+ * workflow editor's node body AND the Form Builder's view/edit modes.
+ *
+ * Convention: the source video is expected to live on a sibling field named
+ * ``video`` on the same node. If that field is missing or unset, the widget
+ * gracefully falls back to a plain number input.
+ */
+const COMPANION_VIDEO_FIELD_NAME = 'video';
+
+export const VideoFrameIndexFieldInput = memo(
+ (props: FieldComponentProps) => {
+ const { t } = useTranslation();
+ const { nodeId, field, fieldTemplate } = props;
+ const { defaultValue, onChange, min, max, step, fineStep, constrainValue } = useIntegerField(
+ nodeId,
+ field.name,
+ fieldTemplate
+ );
+
+ const selectVideoName = useMemo(
+ () =>
+ createSelector(selectNodesSlice, (nodes) => {
+ const sibling = selectFieldInputInstanceSafe(nodes, nodeId, COMPANION_VIDEO_FIELD_NAME);
+ if (!sibling || !isVideoFieldInputInstance(sibling)) {
+ return undefined;
+ }
+ return sibling.value?.video_name;
+ }),
+ [nodeId]
+ );
+ const videoName = useAppSelector(selectVideoName);
+ const { currentData: videoDTO } = useGetVideoDTOQuery(videoName ?? skipToken);
+ const videoUrl = useMediaUrl(videoDTO?.video_url);
+
+ // Frame count is the slider's upper bound. duration*fps can be off-by-one for VFR
+ // containers, but that's tolerable here — the slider is for visual scrubbing, not
+ // for the authoritative range check (which the backend re-validates at invoke time).
+ const fps = videoDTO?.fps ?? null;
+ const frameCount =
+ videoDTO && fps && videoDTO.duration > 0 ? Math.max(1, Math.round(videoDTO.duration * fps)) : null;
+
+ // Resolve negative indices (e.g. -1 = last frame) for display only — the underlying
+ // field value is preserved verbatim so users can still type "-1" and have the
+ // backend resolve it at invoke time against the authoritative decoder frame count.
+ const resolvedIndex = useMemo(() => {
+ if (frameCount === null || field.value === undefined) {
+ return 0;
+ }
+ const candidate = field.value < 0 ? frameCount + field.value : field.value;
+ if (Number.isNaN(candidate)) {
+ return 0;
+ }
+ return Math.max(0, Math.min(frameCount - 1, candidate));
+ }, [field.value, frameCount]);
+
+ return (
+
+
+ {videoDTO && videoUrl && frameCount && fps ? (
+
+ ) : (
+
+
+ {videoName ? t('nodes.extractVideoRange.missingFps') : t('nodes.extractVideoRange.dropVideoPrompt')}
+
+
+ )}
+
+ );
+ }
+);
+
+VideoFrameIndexFieldInput.displayName = 'VideoFrameIndexFieldInput';
+
+type FrameScrubberProps = {
+ videoUrl: string;
+ resolvedIndex: number;
+ fps: number;
+ frameCount: number;
+ onChange: (value: number) => void;
+};
+
+const FrameScrubber = memo(({ videoUrl, resolvedIndex, fps, frameCount, onChange }: FrameScrubberProps) => {
+ const videoRef = useRef(null);
+
+ // Seek the video element whenever the resolved index changes. We nudge currentTime
+ // by half a frame so the seek lands inside the frame's display window — some codecs
+ // decode the boundary as black on first paint without this offset.
+ useEffect(() => {
+ const el = videoRef.current;
+ if (!el) {
+ return;
+ }
+ const targetTime = (resolvedIndex + 0.5) / fps;
+ const setTime = () => {
+ try {
+ el.currentTime = targetTime;
+ } catch {
+ // Seeking before metadata is available throws on some browsers — the
+ // loadedmetadata listener below retries when the element is ready.
+ }
+ };
+ if (el.readyState >= 1) {
+ setTime();
+ } else {
+ el.addEventListener('loadedmetadata', setTime, { once: true });
+ return () => el.removeEventListener('loadedmetadata', setTime);
+ }
+ }, [resolvedIndex, fps, videoUrl]);
+
+ return (
+
+
+
+
+ {`${resolvedIndex} / ${frameCount - 1}`}
+
+
+
+
+ );
+});
+
+FrameScrubber.displayName = 'FrameScrubber';
diff --git a/invokeai/frontend/web/src/features/nodes/components/flow/nodes/Invocation/fields/inputs/VideoFieldInputComponent.tsx b/invokeai/frontend/web/src/features/nodes/components/flow/nodes/Invocation/fields/inputs/VideoFieldInputComponent.tsx
new file mode 100644
index 00000000000..cb3cdf6b0fa
--- /dev/null
+++ b/invokeai/frontend/web/src/features/nodes/components/flow/nodes/Invocation/fields/inputs/VideoFieldInputComponent.tsx
@@ -0,0 +1,119 @@
+import { Flex, Icon, Image, Text } from '@invoke-ai/ui-library';
+import { useStore } from '@nanostores/react';
+import { skipToken } from '@reduxjs/toolkit/query';
+import { useAppDispatch } from 'app/store/storeHooks';
+import { useMediaUrl } from 'features/auth/store/mediaCookieRefresh';
+import type { SetNodeVideoFieldVideoDndTargetData } from 'features/dnd/dnd';
+import { setNodeVideoFieldVideoDndTarget } from 'features/dnd/dnd';
+import { DndDropTarget } from 'features/dnd/DndDropTarget';
+import { fieldVideoValueChanged } from 'features/nodes/store/nodesSlice';
+import { NO_DRAG_CLASS } from 'features/nodes/types/constants';
+import type { VideoFieldInputInstance, VideoFieldInputTemplate } from 'features/nodes/types/field';
+import { memo, useCallback, useEffect, useMemo } from 'react';
+import { useTranslation } from 'react-i18next';
+import { PiVideo } from 'react-icons/pi';
+import { useGetVideoDTOQuery } from 'services/api/endpoints/videos';
+import { $isConnected } from 'services/events/stores';
+
+import type { FieldComponentProps } from './types';
+import { isVideoMissingError } from './videoFieldErrors';
+
+/**
+ * Counterpart to ImageFieldInputComponent for VideoField inputs. Shows the video's WebP
+ * thumbnail (the first decoded frame at the gallery-default size — same image the gallery
+ * grid uses), with a small dimensions badge in the corner. Users drop a video from the
+ * gallery onto the field; the drop is handled by setNodeVideoFieldVideoDndTarget.
+ */
+const VideoFieldInputComponent = (props: FieldComponentProps) => {
+ const { t } = useTranslation();
+ const { nodeId, field } = props;
+ const dispatch = useAppDispatch();
+ const isConnected = useStore($isConnected);
+
+ const { currentData: videoDTO, error } = useGetVideoDTOQuery(field.value?.video_name ?? skipToken);
+ const thumbnailUrl = useMediaUrl(videoDTO?.thumbnail_url);
+
+ const handleReset = useCallback(() => {
+ dispatch(
+ fieldVideoValueChanged({
+ nodeId,
+ fieldName: field.name,
+ value: undefined,
+ })
+ );
+ }, [dispatch, field.name, nodeId]);
+
+ const dndTargetData = useMemo(
+ () => setNodeVideoFieldVideoDndTarget.getData({ fieldIdentifier: { nodeId, fieldName: field.name } }),
+ [field.name, nodeId]
+ );
+
+ // If the referenced video was deleted while disconnected, drop the stale reference once
+ // we reconnect. Only a confirmed 404 means the video is gone — transient network, auth
+ // (401/403), or server (5xx) failures must not silently clear the user's input.
+ useEffect(() => {
+ if (isConnected && isVideoMissingError(error)) {
+ handleReset();
+ }
+ }, [handleReset, isConnected, error]);
+
+ return (
+
+ {!videoDTO && (
+
+
+ {t('gallery.drop')}
+
+
+ )}
+ {videoDTO && (
+ <>
+
+
+
+
+ }
+ />
+
+ {`${videoDTO.width}x${videoDTO.height}`}
+ >
+ )}
+
+
+ );
+};
+
+export default memo(VideoFieldInputComponent);
diff --git a/invokeai/frontend/web/src/features/nodes/components/flow/nodes/Invocation/fields/inputs/videoFieldErrors.test.ts b/invokeai/frontend/web/src/features/nodes/components/flow/nodes/Invocation/fields/inputs/videoFieldErrors.test.ts
new file mode 100644
index 00000000000..d574e17ffd8
--- /dev/null
+++ b/invokeai/frontend/web/src/features/nodes/components/flow/nodes/Invocation/fields/inputs/videoFieldErrors.test.ts
@@ -0,0 +1,20 @@
+import { describe, expect, it } from 'vitest';
+
+import { isVideoMissingError } from './videoFieldErrors';
+
+describe('isVideoMissingError', () => {
+ it('is true for a 404 — the video is confirmed gone', () => {
+ expect(isVideoMissingError({ status: 404, data: { detail: 'not found' } })).toBe(true);
+ });
+
+ it.each([
+ ['auth (401)', { status: 401, data: {} }],
+ ['forbidden (403)', { status: 403, data: {} }],
+ ['server error (500)', { status: 500, data: {} }],
+ ['network failure', { status: 'FETCH_ERROR', error: 'TypeError: Failed to fetch' }],
+ ['parsing failure', { status: 'PARSING_ERROR', originalStatus: 200, data: '', error: 'oops' }],
+ ['no error', undefined],
+ ])('is false for %s — the field value must be preserved', (_label, error) => {
+ expect(isVideoMissingError(error)).toBe(false);
+ });
+});
diff --git a/invokeai/frontend/web/src/features/nodes/components/flow/nodes/Invocation/fields/inputs/videoFieldErrors.ts b/invokeai/frontend/web/src/features/nodes/components/flow/nodes/Invocation/fields/inputs/videoFieldErrors.ts
new file mode 100644
index 00000000000..c55f160b96c
--- /dev/null
+++ b/invokeai/frontend/web/src/features/nodes/components/flow/nodes/Invocation/fields/inputs/videoFieldErrors.ts
@@ -0,0 +1,9 @@
+/**
+ * True only for a confirmed "video does not exist" (HTTP 404) query error.
+ *
+ * VideoFieldInputComponent clears its field value when the referenced video is gone, but
+ * a transient network error (`FETCH_ERROR`), auth failure (401/403), or server error (5xx)
+ * must not silently discard the user's input — only a 404 proves the video was deleted.
+ */
+export const isVideoMissingError = (error: unknown): boolean =>
+ error instanceof Object && 'status' in error && error.status === 404;
diff --git a/invokeai/frontend/web/src/features/nodes/components/sidePanel/workflow/WorkflowThumbnail/WorkflowThumbnailField.tsx b/invokeai/frontend/web/src/features/nodes/components/sidePanel/workflow/WorkflowThumbnail/WorkflowThumbnailField.tsx
index b902c75c726..17e78bc019d 100644
--- a/invokeai/frontend/web/src/features/nodes/components/sidePanel/workflow/WorkflowThumbnail/WorkflowThumbnailField.tsx
+++ b/invokeai/frontend/web/src/features/nodes/components/sidePanel/workflow/WorkflowThumbnail/WorkflowThumbnailField.tsx
@@ -1,6 +1,6 @@
import { Box, Button, Flex, Icon, IconButton, Image, Tooltip } from '@invoke-ai/ui-library';
-import { dropzoneAccept } from 'common/hooks/useImageUploadButton';
import { convertImageUrlToBlob } from 'common/util/convertImageUrlToBlob';
+import { imageDropzoneAccept } from 'common/util/uploadMediaAccept';
import { useCallback, useEffect, useState } from 'react';
import { useDropzone } from 'react-dropzone';
import { useTranslation } from 'react-i18next';
@@ -67,7 +67,7 @@ export const WorkflowThumbnailField = ({
}, [onChange]);
const { getInputProps, getRootProps } = useDropzone({
- accept: dropzoneAccept,
+ accept: imageDropzoneAccept,
onDropAccepted,
noDrag: true,
multiple: false,
diff --git a/invokeai/frontend/web/src/features/nodes/hooks/useNodeHasGalleryOutput.ts b/invokeai/frontend/web/src/features/nodes/hooks/useNodeHasGalleryOutput.ts
new file mode 100644
index 00000000000..d1e028d2b5f
--- /dev/null
+++ b/invokeai/frontend/web/src/features/nodes/hooks/useNodeHasGalleryOutput.ts
@@ -0,0 +1,27 @@
+import { some } from 'es-toolkit/compat';
+import { useMemo } from 'react';
+
+import { useNodeTemplateSafe } from './useNodeTemplateSafe';
+
+/**
+ * True when the node produces an output that lands in the gallery — currently ImageField or
+ * VideoField. Used to gate the "Save in gallery" checkbox and the footer that contains it.
+ *
+ * The `image` and `video` primitive nodes are excluded because they pass through an existing
+ * asset without saving a new copy.
+ */
+export const useNodeHasGalleryOutput = (): boolean => {
+ const template = useNodeTemplateSafe();
+ const hasGalleryOutput = useMemo(
+ () =>
+ some(
+ template?.outputs,
+ (output) =>
+ (output.type.name === 'ImageField' && template?.type !== 'image') ||
+ (output.type.name === 'VideoField' && template?.type !== 'video')
+ ),
+ [template]
+ );
+
+ return hasGalleryOutput;
+};
diff --git a/invokeai/frontend/web/src/features/nodes/hooks/useNodeHasImageOutput.ts b/invokeai/frontend/web/src/features/nodes/hooks/useNodeHasImageOutput.ts
deleted file mode 100644
index 523f48919fb..00000000000
--- a/invokeai/frontend/web/src/features/nodes/hooks/useNodeHasImageOutput.ts
+++ /dev/null
@@ -1,21 +0,0 @@
-import { some } from 'es-toolkit/compat';
-import { useMemo } from 'react';
-
-import { useNodeTemplateSafe } from './useNodeTemplateSafe';
-
-export const useNodeHasImageOutput = (): boolean => {
- const template = useNodeTemplateSafe();
- const hasImageOutput = useMemo(
- () =>
- some(
- template?.outputs,
- (output) =>
- output.type.name === 'ImageField' &&
- // the image primitive node (node type "image") does not actually save the image, do not show the image-saving checkboxes
- template?.type !== 'image'
- ),
- [template]
- );
-
- return hasImageOutput;
-};
diff --git a/invokeai/frontend/web/src/features/nodes/hooks/useWithFooter.ts b/invokeai/frontend/web/src/features/nodes/hooks/useWithFooter.ts
index b140295801b..14383affd82 100644
--- a/invokeai/frontend/web/src/features/nodes/hooks/useWithFooter.ts
+++ b/invokeai/frontend/web/src/features/nodes/hooks/useWithFooter.ts
@@ -1,9 +1,9 @@
import { useIsExecutableNode } from 'features/nodes/hooks/useIsBatchNode';
-import { useNodeHasImageOutput } from './useNodeHasImageOutput';
+import { useNodeHasGalleryOutput } from './useNodeHasGalleryOutput';
export const useWithFooter = () => {
- const hasImageOutput = useNodeHasImageOutput();
+ const hasGalleryOutput = useNodeHasGalleryOutput();
const isExecutableNode = useIsExecutableNode();
- return isExecutableNode && hasImageOutput;
+ return isExecutableNode && hasGalleryOutput;
};
diff --git a/invokeai/frontend/web/src/features/nodes/store/nodesSlice.ts b/invokeai/frontend/web/src/features/nodes/store/nodesSlice.ts
index 61511b46627..85c378d5f80 100644
--- a/invokeai/frontend/web/src/features/nodes/store/nodesSlice.ts
+++ b/invokeai/frontend/web/src/features/nodes/store/nodesSlice.ts
@@ -50,6 +50,7 @@ import type {
StringFieldValue,
StringGeneratorFieldValue,
StylePresetFieldValue,
+ VideoFieldValue,
} from 'features/nodes/types/field';
import {
zBoardFieldValue,
@@ -73,6 +74,7 @@ import {
zStringFieldValue,
zStringGeneratorFieldValue,
zStylePresetFieldValue,
+ zVideoFieldValue,
} from 'features/nodes/types/field';
import type { AnyEdge, AnyNode, ConnectorNode } from 'features/nodes/types/invocation';
import { isConnectorNode, isInvocationNode, isNotesNode } from 'features/nodes/types/invocation';
@@ -586,6 +588,9 @@ const slice = createSlice({
fieldImageCollectionValueChanged: (state, action: FieldValueAction) => {
fieldValueReducer(state, action, zImageFieldCollectionValue);
},
+ fieldVideoValueChanged: (state, action: FieldValueAction) => {
+ fieldValueReducer(state, action, zVideoFieldValue);
+ },
fieldLoRACollectionValueChanged: (state, action: FieldValueAction) => {
fieldValueReducer(state, action, zLoRAFieldCollectionValue);
},
@@ -803,6 +808,7 @@ export const {
fieldEnumModelValueChanged,
fieldImageValueChanged,
fieldImageCollectionValueChanged,
+ fieldVideoValueChanged,
fieldLabelChanged,
fieldLoRACollectionValueChanged,
fieldModelIdentifierValueChanged,
diff --git a/invokeai/frontend/web/src/features/nodes/store/workflowLibrarySlice.ts b/invokeai/frontend/web/src/features/nodes/store/workflowLibrarySlice.ts
index 1d5d8554aeb..3c53bb611b0 100644
--- a/invokeai/frontend/web/src/features/nodes/store/workflowLibrarySlice.ts
+++ b/invokeai/frontend/web/src/features/nodes/store/workflowLibrarySlice.ts
@@ -114,7 +114,14 @@ export const WORKFLOW_LIBRARY_TAG_CATEGORIES: WorkflowTagCategory[] = [
},
{
categoryTKey: 'Common Tasks',
- tags: [{ label: 'Upscaling' }, { label: 'Text to Image' }, { label: 'Image to Image' }],
+ tags: [
+ { label: 'Upscaling' },
+ { label: 'Text to Image' },
+ { label: 'Image to Image' },
+ { label: 'Text to Video' },
+ { label: 'Image to Video' },
+ { label: 'Video to Video' },
+ ],
},
{
categoryTKey: 'Model Architecture',
diff --git a/invokeai/frontend/web/src/features/nodes/types/common.ts b/invokeai/frontend/web/src/features/nodes/types/common.ts
index fb2a1ce946a..f23c34ab170 100644
--- a/invokeai/frontend/web/src/features/nodes/types/common.ts
+++ b/invokeai/frontend/web/src/features/nodes/types/common.ts
@@ -11,6 +11,12 @@ type ImageFieldCollection = z.infer;
export const isImageFieldCollection = (field: unknown): field is ImageFieldCollection =>
zImageFieldCollection.safeParse(field).success;
+export const zVideoField = z.object({
+ video_name: z.string().trim().min(1),
+});
+type VideoField = z.infer;
+export const isVideoField = (field: unknown): field is VideoField => zVideoField.safeParse(field).success;
+
export const zBoardField = z.object({
board_id: z.string().trim().min(1),
});
@@ -100,6 +106,7 @@ export const zBaseModelType = z.enum([
'z-image',
'external',
'anima',
+ 'wan',
'unknown',
]);
export type BaseModelType = z.infer;
@@ -114,6 +121,7 @@ export const zMainModelBase = z.enum([
'qwen-image',
'z-image',
'anima',
+ 'wan',
]);
type MainModelBase = z.infer;
export const isMainModelBase = (base: unknown): base is MainModelBase => zMainModelBase.safeParse(base).success;
@@ -134,6 +142,7 @@ export const zModelType = z.enum([
't5_encoder',
'qwen3_encoder',
'qwen_vl_encoder',
+ 'wan_t5_encoder',
'clip_embed',
'siglip',
'flux_redux',
@@ -144,6 +153,7 @@ export type ModelType = z.infer;
export const zSubModelType = z.enum([
'unet',
'transformer',
+ 'transformer_2',
'text_encoder',
'text_encoder_2',
'text_encoder_3',
@@ -163,6 +173,10 @@ export const zFluxVariantType = z.enum(['dev', 'dev_fill', 'schnell']);
export const zFlux2VariantType = z.enum(['klein_4b', 'klein_4b_base', 'klein_9b', 'klein_9b_base']);
export const zZImageVariantType = z.enum(['turbo', 'zbase']);
const zQwenImageVariantType = z.enum(['generate', 'edit']);
+const zWanVariantType = z.enum(['t2v_a14b', 'i2v_a14b', 'ti2v_5b']);
+/** Wan LoRA variant — identifies which model FAMILY (inner_dim) a LoRA
+ * targets. A14B = inner_dim 5120 (both T2V and I2V), 5B = inner_dim 3072. */
+const zWanLoRAVariantType = z.enum(['a14b', '5b']);
export const zQwen3VariantType = z.enum(['qwen3_4b', 'qwen3_8b', 'qwen3_06b']);
export const zAnyModelVariant = z.union([
zModelVariantType,
@@ -171,6 +185,8 @@ export const zAnyModelVariant = z.union([
zFlux2VariantType,
zZImageVariantType,
zQwenImageVariantType,
+ zWanVariantType,
+ zWanLoRAVariantType,
zQwen3VariantType,
]);
export type AnyModelVariant = z.infer;
@@ -187,6 +203,7 @@ export const zModelFormat = z.enum([
't5_encoder',
'qwen3_encoder',
'qwen_vl_encoder',
+ 'wan_t5_encoder',
'bnb_quantized_int8b',
'bnb_quantized_nf4b',
'gguf_quantized',
diff --git a/invokeai/frontend/web/src/features/nodes/types/constants.ts b/invokeai/frontend/web/src/features/nodes/types/constants.ts
index 9da499ab91c..7383629eb84 100644
--- a/invokeai/frontend/web/src/features/nodes/types/constants.ts
+++ b/invokeai/frontend/web/src/features/nodes/types/constants.ts
@@ -57,6 +57,7 @@ export const FIELD_COLORS: { [key: string]: string } = {
CogView4MainModelField: 'teal.500',
ZImageMainModelField: 'teal.500',
AnimaMainModelField: 'teal.500',
+ WanMainModelField: 'teal.500',
SDXLMainModelField: 'teal.500',
SDXLRefinerModelField: 'teal.500',
SpandrelImageToImageModelField: 'teal.500',
diff --git a/invokeai/frontend/web/src/features/nodes/types/field.ts b/invokeai/frontend/web/src/features/nodes/types/field.ts
index e3829b0cc8d..8fabf408976 100644
--- a/invokeai/frontend/web/src/features/nodes/types/field.ts
+++ b/invokeai/frontend/web/src/features/nodes/types/field.ts
@@ -20,6 +20,7 @@ import {
zModelType,
zSchedulerField,
zStylePresetField,
+ zVideoField,
} from './common';
/**
@@ -50,7 +51,7 @@ import {
// #region Base schemas & misc
const zFieldInput = z.enum(['connection', 'direct', 'any']);
-const zFieldUIComponent = z.enum(['none', 'textarea', 'slider']);
+const zFieldUIComponent = z.enum(['none', 'textarea', 'slider', 'video-frame-index']);
const zFieldInputInstanceBase = z.object({
name: z.string().trim().min(1),
label: z.string().catch(''),
@@ -156,6 +157,10 @@ const zImageFieldType = zFieldTypeBase.extend({
name: z.literal('ImageField'),
originalType: zStatelessFieldType.optional(),
});
+const zVideoFieldType = zFieldTypeBase.extend({
+ name: z.literal('VideoField'),
+ originalType: zStatelessFieldType.optional(),
+});
const zImageCollectionFieldType = zFieldTypeBase.extend({
name: z.literal('ImageField'),
cardinality: z.literal(COLLECTION),
@@ -219,6 +224,7 @@ const zStatefulFieldType = z.union([
zBooleanFieldType,
zEnumFieldType,
zImageFieldType,
+ zVideoFieldType,
zBoardFieldType,
zStylePresetFieldType,
zModelIdentifierFieldType,
@@ -546,6 +552,26 @@ export const isEnumFieldInputInstance = buildInstanceTypeGuard(zEnumFieldInputIn
export const isEnumFieldInputTemplate = buildTemplateTypeGuard('EnumField');
// #endregion
+// #region VideoField
+export const zVideoFieldValue = zVideoField.optional();
+const zVideoFieldInputInstance = zFieldInputInstanceBase.extend({
+ value: zVideoFieldValue,
+});
+const zVideoFieldInputTemplate = zFieldInputTemplateBase.extend({
+ type: zVideoFieldType,
+ originalType: zFieldType.optional(),
+ default: zVideoFieldValue,
+});
+const zVideoFieldOutputTemplate = zFieldOutputTemplateBase.extend({
+ type: zVideoFieldType,
+});
+export type VideoFieldValue = z.infer;
+export type VideoFieldInputInstance = z.infer;
+export type VideoFieldInputTemplate = z.infer;
+export const isVideoFieldInputInstance = buildInstanceTypeGuard(zVideoFieldInputInstance);
+export const isVideoFieldInputTemplate = buildTemplateTypeGuard('VideoField', ['SINGLE']);
+// #endregion
+
// #region ImageField
export const zImageFieldValue = zImageField.optional();
const zImageFieldInputInstance = zFieldInputInstanceBase.extend({
@@ -1367,6 +1393,7 @@ export const zStatefulFieldValue = z.union([
zEnumFieldValue,
zImageFieldValue,
zImageFieldCollectionValue,
+ zVideoFieldValue,
zBoardFieldValue,
zStylePresetFieldValue,
zModelIdentifierFieldValue,
@@ -1397,6 +1424,7 @@ const zStatefulFieldInputInstance = z.union([
zEnumFieldInputInstance,
zImageFieldInputInstance,
zImageFieldCollectionInputInstance,
+ zVideoFieldInputInstance,
zBoardFieldInputInstance,
zStylePresetFieldInputInstance,
zModelIdentifierFieldInputInstance,
@@ -1426,6 +1454,7 @@ const zStatefulFieldInputTemplate = z.union([
zEnumFieldInputTemplate,
zImageFieldInputTemplate,
zImageFieldCollectionInputTemplate,
+ zVideoFieldInputTemplate,
zBoardFieldInputTemplate,
zStylePresetFieldInputTemplate,
zModelIdentifierFieldInputTemplate,
@@ -1455,6 +1484,7 @@ const zStatefulFieldOutputTemplate = z.union([
zEnumFieldOutputTemplate,
zImageFieldOutputTemplate,
zImageFieldCollectionOutputTemplate,
+ zVideoFieldOutputTemplate,
zBoardFieldOutputTemplate,
zStylePresetFieldOutputTemplate,
zModelIdentifierFieldOutputTemplate,
diff --git a/invokeai/frontend/web/src/features/nodes/util/graph/generation/addImageToImage.ts b/invokeai/frontend/web/src/features/nodes/util/graph/generation/addImageToImage.ts
index f17ff970f27..103c139b723 100644
--- a/invokeai/frontend/web/src/features/nodes/util/graph/generation/addImageToImage.ts
+++ b/invokeai/frontend/web/src/features/nodes/util/graph/generation/addImageToImage.ts
@@ -30,6 +30,7 @@ type AddImageToImageArg = {
| 'qwen_image_i2l'
| 'z_image_i2l'
| 'anima_i2l'
+ | 'wan_i2l'
>;
noise?: Invocation<'noise'>;
denoise: Invocation;
@@ -56,6 +57,7 @@ export const addImageToImage = async ({
| 'qwen_image_l2i'
| 'z_image_l2i'
| 'anima_l2i'
+ | 'wan_l2i'
>
> => {
const { denoising_start, denoising_end } = getDenoisingStartAndEnd(state);
@@ -71,7 +73,8 @@ export const addImageToImage = async ({
denoise.type === 'flux2_denoise' ||
denoise.type === 'sd3_denoise' ||
denoise.type === 'z_image_denoise' ||
- denoise.type === 'anima_denoise'
+ denoise.type === 'anima_denoise' ||
+ denoise.type === 'wan_denoise'
) {
denoise.width = scaledSize.width;
denoise.height = scaledSize.height;
diff --git a/invokeai/frontend/web/src/features/nodes/util/graph/generation/addInpaint.ts b/invokeai/frontend/web/src/features/nodes/util/graph/generation/addInpaint.ts
index 51ac7c8d6b3..2f47577366b 100644
--- a/invokeai/frontend/web/src/features/nodes/util/graph/generation/addInpaint.ts
+++ b/invokeai/frontend/web/src/features/nodes/util/graph/generation/addInpaint.ts
@@ -33,6 +33,7 @@ type AddInpaintArg = {
| 'qwen_image_i2l'
| 'z_image_i2l'
| 'anima_i2l'
+ | 'wan_i2l'
>;
noise?: Invocation<'noise'>;
denoise: Invocation;
@@ -72,7 +73,8 @@ export const addInpaint = async ({
denoise.type === 'flux2_denoise' ||
denoise.type === 'sd3_denoise' ||
denoise.type === 'z_image_denoise' ||
- denoise.type === 'anima_denoise'
+ denoise.type === 'anima_denoise' ||
+ denoise.type === 'wan_denoise'
) {
denoise.width = scaledSize.width;
denoise.height = scaledSize.height;
diff --git a/invokeai/frontend/web/src/features/nodes/util/graph/generation/addOutpaint.ts b/invokeai/frontend/web/src/features/nodes/util/graph/generation/addOutpaint.ts
index fa362fb095e..4ced0744afd 100644
--- a/invokeai/frontend/web/src/features/nodes/util/graph/generation/addOutpaint.ts
+++ b/invokeai/frontend/web/src/features/nodes/util/graph/generation/addOutpaint.ts
@@ -65,7 +65,8 @@ export const addOutpaint = async ({
denoise.type === 'flux2_denoise' ||
denoise.type === 'sd3_denoise' ||
denoise.type === 'z_image_denoise' ||
- denoise.type === 'anima_denoise'
+ denoise.type === 'anima_denoise' ||
+ denoise.type === 'wan_denoise'
) {
denoise.width = scaledSize.width;
denoise.height = scaledSize.height;
diff --git a/invokeai/frontend/web/src/features/nodes/util/graph/generation/addTextToImage.ts b/invokeai/frontend/web/src/features/nodes/util/graph/generation/addTextToImage.ts
index 06ece522da5..9e5d8aec82e 100644
--- a/invokeai/frontend/web/src/features/nodes/util/graph/generation/addTextToImage.ts
+++ b/invokeai/frontend/web/src/features/nodes/util/graph/generation/addTextToImage.ts
@@ -31,6 +31,7 @@ export const addTextToImage = ({
| 'qwen_image_l2i'
| 'z_image_l2i'
| 'anima_l2i'
+ | 'wan_l2i'
> => {
denoise.denoising_start = 0;
denoise.denoising_end = 1;
@@ -44,7 +45,8 @@ export const addTextToImage = ({
denoise.type === 'flux2_denoise' ||
denoise.type === 'sd3_denoise' ||
denoise.type === 'z_image_denoise' ||
- denoise.type === 'anima_denoise'
+ denoise.type === 'anima_denoise' ||
+ denoise.type === 'wan_denoise'
) {
denoise.width = scaledSize.width;
denoise.height = scaledSize.height;
diff --git a/invokeai/frontend/web/src/features/nodes/util/graph/generation/addWanLoRAs.ts b/invokeai/frontend/web/src/features/nodes/util/graph/generation/addWanLoRAs.ts
new file mode 100644
index 00000000000..9b7bacccff5
--- /dev/null
+++ b/invokeai/frontend/web/src/features/nodes/util/graph/generation/addWanLoRAs.ts
@@ -0,0 +1,132 @@
+import { logger } from 'app/logging/logger';
+import type { RootState } from 'app/store/store';
+import { getPrefixedId } from 'features/controlLayers/konva/util';
+import { fetchModelConfigWithTypeGuard } from 'features/metadata/util/modelFetchingHelpers';
+import { zModelIdentifierField } from 'features/nodes/types/common';
+import type { Graph } from 'features/nodes/util/graph/generation/Graph';
+import type { Invocation, MainModelConfig, S } from 'services/api/types';
+import { isWanLoRAModelConfig } from 'services/api/types';
+
+const log = logger('system');
+
+/** Map a Wan main-model variant onto the LoRA-variant string used by the
+ * probe. A14B (both T2V and I2V) uses inner_dim=5120 → "a14b". TI2V-5B
+ * uses inner_dim=3072 → "5b". */
+const mainVariantToLoRAVariant = (mainVariant: string | null | undefined): 'a14b' | '5b' | null => {
+ if (mainVariant === 't2v_a14b' || mainVariant === 'i2v_a14b') {
+ return 'a14b';
+ }
+ if (mainVariant === 'ti2v_5b') {
+ return '5b';
+ }
+ return null;
+};
+
+/**
+ * Add Wan 2.2 LoRA wiring to the graph between the model loader and the
+ * denoise node.
+ *
+ * Each enabled Wan LoRA becomes a ``lora_selector`` feeding a ``collect``
+ * node, which fans into a ``wan_lora_collection_loader``. The collection
+ * loader rewrites the model loader's transformer output into a
+ * ``WanTransformerField`` with the appropriate ``loras`` /
+ * ``loras_low_noise`` lists populated based on each LoRA's recorded
+ * ``expert`` tag — high-noise LoRAs land on the primary list, low-noise
+ * LoRAs on ``loras_low_noise``, and untagged LoRAs are applied to both
+ * experts. The dual-expert routing happens entirely on the backend; the
+ * FE just hands the loader the bag of LoRAs.
+ *
+ * Variant filter: each LoRA's full config carries a ``variant`` field
+ * (``"a14b"`` / ``"5b"`` / null) set by the backend probe from the LoRA's
+ * inner-dim. A14B LoRAs have 5120-dim weights and can't be reshaped to
+ * fit a TI2V-5B main (3072-dim) — the layer patcher would crash with a
+ * tensor-size error. We fetch each LoRA's config and skip mismatches,
+ * logging a warning so the user can tell why a LoRA they enabled didn't
+ * take effect.
+ */
+export const addWanLoRAs = async (
+ state: RootState,
+ g: Graph,
+ denoise: Invocation<'wan_denoise'>,
+ modelLoader: Invocation<'wan_model_loader'>,
+ mainConfig: MainModelConfig
+): Promise => {
+ // MainModelConfig is the union of all main-config schemas; ``variant`` is
+ // only present on the discriminated members (Wan, FLUX, ZImage, etc.).
+ // Read it defensively rather than relying on TypeScript narrowing through
+ // a typed parameter.
+ const mainVariant = 'variant' in mainConfig ? ((mainConfig as { variant?: string | null }).variant ?? null) : null;
+ const expectedLoRAVariant = mainVariantToLoRAVariant(mainVariant);
+ const candidateLoRAs = state.loras.loras.filter((l) => l.isEnabled && l.model.base === 'wan');
+
+ if (candidateLoRAs.length === 0) {
+ return;
+ }
+
+ // Fetch each LoRA's config and filter by variant compatibility. LoRAs
+ // with ``variant === null`` are kept (the probe couldn't identify them;
+ // best to try rather than silently drop).
+ const compatibleLoRAs: typeof candidateLoRAs = [];
+ for (const lora of candidateLoRAs) {
+ try {
+ const cfg = await fetchModelConfigWithTypeGuard(lora.model.key, isWanLoRAModelConfig);
+ const loraVariant = cfg.variant ?? null;
+ if (loraVariant !== null && expectedLoRAVariant !== null && loraVariant !== expectedLoRAVariant) {
+ log.warn(
+ { lora: lora.model.name, loraVariant, mainVariant },
+ `Skipping Wan LoRA "${lora.model.name}" — its variant (${loraVariant}) is incompatible with ` +
+ `the selected main model variant (${mainVariant}). ` +
+ `A14B and TI2V-5B have different inner dims and LoRA weights aren't interchangeable.`
+ );
+ continue;
+ }
+ compatibleLoRAs.push(lora);
+ } catch (e) {
+ // If the config can't be fetched, fall back to including the LoRA —
+ // the backend will produce a clearer error if it really doesn't fit.
+ log.warn({ lora: lora.model.name, error: String(e) }, `Failed to read variant for Wan LoRA "${lora.model.name}"`);
+ compatibleLoRAs.push(lora);
+ }
+ }
+
+ if (compatibleLoRAs.length === 0) {
+ return;
+ }
+
+ const loraMetadata: S['LoRAMetadataField'][] = [];
+
+ const loraCollector = g.addNode({
+ id: getPrefixedId('lora_collector'),
+ type: 'collect',
+ });
+ const loraCollectionLoader = g.addNode({
+ type: 'wan_lora_collection_loader',
+ id: getPrefixedId('wan_lora_collection_loader'),
+ });
+
+ g.addEdge(loraCollector, 'collection', loraCollectionLoader, 'loras');
+ g.addEdge(modelLoader, 'transformer', loraCollectionLoader, 'transformer');
+ g.deleteEdgesTo(denoise, ['transformer']);
+ g.addEdge(loraCollectionLoader, 'transformer', denoise, 'transformer');
+
+ for (const lora of compatibleLoRAs) {
+ const { weight } = lora;
+ const parsedModel = zModelIdentifierField.parse(lora.model);
+
+ const loraSelector = g.addNode({
+ type: 'lora_selector',
+ id: getPrefixedId('lora_selector'),
+ lora: parsedModel,
+ weight,
+ });
+
+ loraMetadata.push({
+ model: parsedModel,
+ weight,
+ });
+
+ g.addEdge(loraSelector, 'lora', loraCollector, 'item');
+ }
+
+ g.upsertMetadata({ loras: loraMetadata });
+};
diff --git a/invokeai/frontend/web/src/features/nodes/util/graph/generation/buildWanGraph.ts b/invokeai/frontend/web/src/features/nodes/util/graph/generation/buildWanGraph.ts
new file mode 100644
index 00000000000..74b1da57571
--- /dev/null
+++ b/invokeai/frontend/web/src/features/nodes/util/graph/generation/buildWanGraph.ts
@@ -0,0 +1,277 @@
+import { logger } from 'app/logging/logger';
+import { getPrefixedId } from 'features/controlLayers/konva/util';
+import { selectMainModelConfig, selectParamsSlice } from 'features/controlLayers/store/paramsSlice';
+import { selectRefImagesSlice } from 'features/controlLayers/store/refImagesSlice';
+import { selectCanvasMetadata } from 'features/controlLayers/store/selectors';
+import { isWanReferenceImageConfig } from 'features/controlLayers/store/types';
+import { getGlobalReferenceImageWarnings } from 'features/controlLayers/store/validators';
+import { fetchModelConfigWithTypeGuard } from 'features/metadata/util/modelFetchingHelpers';
+import { zImageField } from 'features/nodes/types/common';
+import { addImageToImage } from 'features/nodes/util/graph/generation/addImageToImage';
+import { addInpaint } from 'features/nodes/util/graph/generation/addInpaint';
+import { addNSFWChecker } from 'features/nodes/util/graph/generation/addNSFWChecker';
+import { addOutpaint } from 'features/nodes/util/graph/generation/addOutpaint';
+import { addTextToImage } from 'features/nodes/util/graph/generation/addTextToImage';
+import { addWanLoRAs } from 'features/nodes/util/graph/generation/addWanLoRAs';
+import { addWatermarker } from 'features/nodes/util/graph/generation/addWatermarker';
+import { Graph } from 'features/nodes/util/graph/generation/Graph';
+import { selectCanvasOutputFields, selectPresetModifiedPrompts } from 'features/nodes/util/graph/graphBuilderUtils';
+import type { GraphBuilderArg, GraphBuilderReturn, ImageOutputNodes } from 'features/nodes/util/graph/types';
+import { selectActiveTab } from 'features/ui/store/uiSelectors';
+import type { Invocation } from 'services/api/types';
+import { isNonRefinerMainModelConfig } from 'services/api/types';
+import type { Equals } from 'tsafe';
+import { assert } from 'tsafe';
+
+const log = logger('system');
+
+/**
+ * Build a graph for Wan 2.2 image generation.
+ *
+ * Phase 9 piece #1: text-to-image only, Diffusers main model with all
+ * components (transformer, VAE, UMT5-XXL encoder) resolved from the main
+ * model itself. Subsequent pieces will add:
+ * - img2img (Latents input + Image-to-Latents wiring + denoising_start)
+ * - I2V (ref-image encoder, A14B I2V variant gate)
+ * - LoRAs (single + collection)
+ * - Inpaint (mask handling)
+ * - Standalone VAE / T5 / GGUF low-noise-expert wiring via params slice
+ */
+export const buildWanGraph = async (arg: GraphBuilderArg): Promise => {
+ const { generationMode, state, manager } = arg;
+
+ log.debug({ generationMode, manager: manager?.id }, 'Building Wan 2.2 graph');
+
+ const model = selectMainModelConfig(state);
+ assert(model, 'No model selected');
+ assert(model.base === 'wan', 'Selected model is not a Wan model');
+
+ // Fetch the full config early so we can branch on variant. I2V flows
+ // route the raster image through wan_ref_image_encoder instead of
+ // wan_i2l, so the variant has to be known before we choose a graph
+ // shape — not after.
+ const modelConfig = await fetchModelConfigWithTypeGuard(model.key, isNonRefinerMainModelConfig);
+ assert(modelConfig.base === 'wan');
+ const isI2V = modelConfig.variant === 'i2v_a14b';
+
+ const params = selectParamsSlice(state);
+ const { cfgScale: cfg_scale, steps } = params;
+ const prompts = selectPresetModifiedPrompts(state);
+
+ const g = new Graph(getPrefixedId('wan_graph'));
+
+ const modelLoader = g.addNode({
+ type: 'wan_model_loader',
+ id: getPrefixedId('wan_model_loader'),
+ model,
+ transformer_low_noise_model: params.wanTransformerLowNoise ?? undefined,
+ component_source: params.wanComponentSource ?? undefined,
+ vae_model: params.wanVaeModel ?? undefined,
+ wan_t5_encoder_model: params.wanT5EncoderModel ?? undefined,
+ });
+
+ const positivePrompt = g.addNode({
+ id: getPrefixedId('positive_prompt'),
+ type: 'string',
+ });
+ const posCond = g.addNode({
+ type: 'wan_text_encoder',
+ id: getPrefixedId('pos_prompt'),
+ });
+
+ // CFG is mathematically inactive at scale 1.0 — skip the negative branch
+ // entirely so each step runs only one forward pass.
+ const useCfg = cfg_scale > 1;
+ const negCond = useCfg
+ ? g.addNode({
+ type: 'wan_text_encoder',
+ id: getPrefixedId('neg_prompt'),
+ prompt: prompts.negative || ' ',
+ })
+ : null;
+
+ const seed = g.addNode({
+ id: getPrefixedId('seed'),
+ type: 'integer',
+ });
+
+ const denoise = g.addNode({
+ type: 'wan_denoise',
+ id: getPrefixedId('denoise_latents'),
+ guidance_scale: cfg_scale,
+ // The denoise node treats values < 1.0 (including the FE's default 0) as
+ // "fall back to the primary guidance_scale". Forward null/undefined when
+ // the user hasn't set an explicit low-noise CFG so the backend handles it.
+ guidance_scale_low_noise: params.wanGuidanceScaleLowNoise ?? undefined,
+ steps,
+ });
+
+ const l2i = g.addNode({
+ type: 'wan_l2i',
+ id: getPrefixedId('l2i'),
+ });
+
+ g.addEdge(modelLoader, 'transformer', denoise, 'transformer');
+ g.addEdge(modelLoader, 'wan_t5_encoder', posCond, 'wan_t5_encoder');
+ g.addEdge(modelLoader, 'vae', l2i, 'vae');
+
+ g.addEdge(positivePrompt, 'value', posCond, 'prompt');
+ g.addEdge(posCond, 'conditioning', denoise, 'positive_conditioning');
+
+ if (negCond) {
+ g.addEdge(modelLoader, 'wan_t5_encoder', negCond, 'wan_t5_encoder');
+ g.addEdge(negCond, 'conditioning', denoise, 'negative_conditioning');
+ }
+
+ g.addEdge(seed, 'value', denoise, 'seed');
+ g.addEdge(denoise, 'latents', l2i, 'latents');
+
+ // Wan LoRAs (high-noise, low-noise, and untagged). The collection loader
+ // is inserted between modelLoader and denoise; both expert routing and
+ // dual-list population happen on the backend based on each LoRA's
+ // recorded ``expert`` tag. The helper also filters out variant-incompatible
+ // LoRAs (e.g. A14B Lightning on a TI2V-5B main) so the layer patcher
+ // doesn't crash on a shape mismatch.
+ await addWanLoRAs(state, g, denoise, modelLoader, modelConfig);
+
+ g.upsertMetadata({
+ cfg_scale,
+ negative_prompt: prompts.negative,
+ model: Graph.getModelMetadataField(modelConfig),
+ steps,
+ wan_transformer_low_noise: params.wanTransformerLowNoise,
+ wan_component_source: params.wanComponentSource,
+ wan_vae_model: params.wanVaeModel,
+ wan_t5_encoder_model: params.wanT5EncoderModel,
+ wan_guidance_scale_low_noise: params.wanGuidanceScaleLowNoise,
+ });
+ g.addEdgeToMetadata(seed, 'value', 'seed');
+ g.addEdgeToMetadata(positivePrompt, 'value', 'positive_prompt');
+
+ let canvasOutput: Invocation = l2i;
+
+ // I2V variants take a reference image from the global Reference Images
+ // panel (same UX as Qwen Image Edit / FLUX.2 Klein). The image is encoded
+ // by the model's own VAE and concatenated to the noise latents along the
+ // channel dim each step (transformer in_channels=36 on I2V). Canvas modes
+ // (img2img/inpaint/outpaint) don't apply to I2V — the ref image fully
+ // replaces what a raster layer used to provide.
+ if (isI2V) {
+ assert(
+ generationMode === 'txt2img',
+ 'Wan 2.2 I2V only supports text-to-image with a reference image. ' +
+ 'Use a T2V or TI2V model for canvas img2img / inpaint / outpaint.'
+ );
+
+ const wanRefEntity = selectRefImagesSlice(state).entities.find(
+ (entity) =>
+ entity.isEnabled &&
+ isWanReferenceImageConfig(entity.config) &&
+ entity.config.image !== null &&
+ getGlobalReferenceImageWarnings(entity, modelConfig).length === 0
+ );
+ assert(
+ wanRefEntity && isWanReferenceImageConfig(wanRefEntity.config) && wanRefEntity.config.image,
+ 'Wan 2.2 I2V requires a reference image. Add one in the Reference Images panel.'
+ );
+
+ canvasOutput = addTextToImage({ g, state, denoise, l2i });
+ const refImageField = zImageField.parse(
+ wanRefEntity.config.image.crop?.image ?? wanRefEntity.config.image.original.image
+ );
+ const refEncoder = g.addNode({
+ type: 'wan_ref_image_encoder',
+ id: getPrefixedId('wan_ref_encoder'),
+ image: refImageField,
+ width: denoise.width,
+ height: denoise.height,
+ });
+ g.addEdge(modelLoader, 'vae', refEncoder, 'vae');
+ g.addEdge(refEncoder, 'ref_image', denoise, 'ref_image');
+
+ g.upsertMetadata({ generation_mode: 'wan_i2v' });
+ } else if (generationMode === 'txt2img') {
+ canvasOutput = addTextToImage({
+ g,
+ state,
+ denoise,
+ l2i,
+ });
+ g.upsertMetadata({ generation_mode: 'wan_txt2img' });
+ } else if (generationMode === 'img2img') {
+ assert(manager !== null);
+ const i2l = g.addNode({
+ type: 'wan_i2l',
+ id: getPrefixedId('wan_i2l'),
+ });
+ canvasOutput = await addImageToImage({
+ g,
+ state,
+ manager,
+ denoise,
+ l2i,
+ i2l,
+ vaeSource: modelLoader,
+ });
+ g.upsertMetadata({ generation_mode: 'wan_img2img' });
+ } else if (generationMode === 'inpaint') {
+ assert(manager !== null);
+ const i2l = g.addNode({
+ type: 'wan_i2l',
+ id: getPrefixedId('wan_i2l'),
+ });
+ canvasOutput = await addInpaint({
+ g,
+ state,
+ manager,
+ l2i,
+ i2l,
+ denoise,
+ vaeSource: modelLoader,
+ modelLoader,
+ seed,
+ });
+ g.upsertMetadata({ generation_mode: 'wan_inpaint' });
+ } else if (generationMode === 'outpaint') {
+ assert(manager !== null);
+ const i2l = g.addNode({
+ type: 'wan_i2l',
+ id: getPrefixedId('wan_i2l'),
+ });
+ canvasOutput = await addOutpaint({
+ g,
+ state,
+ manager,
+ l2i,
+ i2l,
+ denoise,
+ vaeSource: modelLoader,
+ modelLoader,
+ seed,
+ });
+ g.upsertMetadata({ generation_mode: 'wan_outpaint' });
+ } else {
+ assert>(false);
+ }
+
+ if (state.system.shouldUseNSFWChecker) {
+ canvasOutput = addNSFWChecker(g, canvasOutput);
+ }
+ if (state.system.shouldUseWatermarker) {
+ canvasOutput = addWatermarker(g, canvasOutput);
+ }
+
+ g.updateNode(canvasOutput, selectCanvasOutputFields(state));
+
+ if (selectActiveTab(state) === 'canvas') {
+ g.upsertMetadata(selectCanvasMetadata(state));
+ }
+
+ g.setMetadataReceivingNode(canvasOutput);
+
+ return {
+ g,
+ seed,
+ positivePrompt,
+ };
+};
diff --git a/invokeai/frontend/web/src/features/nodes/util/graph/graphBuilderUtils.ts b/invokeai/frontend/web/src/features/nodes/util/graph/graphBuilderUtils.ts
index 28aa74db5ec..9d5f165ef78 100644
--- a/invokeai/frontend/web/src/features/nodes/util/graph/graphBuilderUtils.ts
+++ b/invokeai/frontend/web/src/features/nodes/util/graph/graphBuilderUtils.ts
@@ -217,7 +217,8 @@ export const isMainModelWithoutUnet = (modelLoader: Invocation =
ColorField: { r: 0, g: 0, b: 0, a: 1 },
FloatField: 0,
ImageField: undefined,
+ VideoField: undefined,
IntegerField: 0,
ModelIdentifierField: undefined,
LoRAField: [],
diff --git a/invokeai/frontend/web/src/features/nodes/util/schema/buildFieldInputTemplate.ts b/invokeai/frontend/web/src/features/nodes/util/schema/buildFieldInputTemplate.ts
index 7751390e9da..e4b527178cb 100644
--- a/invokeai/frontend/web/src/features/nodes/util/schema/buildFieldInputTemplate.ts
+++ b/invokeai/frontend/web/src/features/nodes/util/schema/buildFieldInputTemplate.ts
@@ -26,6 +26,7 @@ import type {
StringFieldInputTemplate,
StringGeneratorFieldInputTemplate,
StylePresetFieldInputTemplate,
+ VideoFieldInputTemplate,
} from 'features/nodes/types/field';
import {
getFloatGeneratorArithmeticSequenceDefaults,
@@ -321,6 +322,20 @@ const buildImageFieldInputTemplate: FieldInputTemplateBuilder = ({
+ schemaObject,
+ baseField,
+ fieldType,
+}) => {
+ const template: VideoFieldInputTemplate = {
+ ...baseField,
+ type: fieldType,
+ default: schemaObject.default ?? undefined,
+ };
+
+ return template;
+};
+
const buildImageFieldCollectionInputTemplate: FieldInputTemplateBuilder = ({
schemaObject,
baseField,
@@ -520,6 +535,7 @@ const TEMPLATE_BUILDER_MAP: Record {
workflow: getWorkflow(),
templates: { img_resize, main_model_loader },
checkImageAccess: resolveFalse,
+ checkVideoAccess: resolveTrue,
checkBoardAccess: resolveTrue,
checkModelAccess: resolveTrue,
});
@@ -219,6 +220,7 @@ describe('validateWorkflow', () => {
workflow: getWorkflow(),
templates: { img_resize, main_model_loader },
checkImageAccess: resolveTrue,
+ checkVideoAccess: resolveTrue,
checkBoardAccess: resolveFalse,
checkModelAccess: resolveTrue,
});
@@ -230,6 +232,7 @@ describe('validateWorkflow', () => {
workflow: getWorkflow(),
templates: { img_resize, main_model_loader },
checkImageAccess: resolveTrue,
+ checkVideoAccess: resolveTrue,
checkBoardAccess: resolveTrue,
checkModelAccess: resolveFalse,
});
@@ -271,6 +274,7 @@ describe('validateWorkflow', () => {
},
templates: { img_resize, main_model_loader, workflow_return },
checkImageAccess: resolveTrue,
+ checkVideoAccess: resolveTrue,
checkBoardAccess: resolveTrue,
checkModelAccess: resolveTrue,
})
@@ -293,6 +297,7 @@ describe('validateWorkflow', () => {
workflow,
templates: { img_resize, main_model_loader },
checkImageAccess: resolveTrue,
+ checkVideoAccess: resolveTrue,
checkBoardAccess: resolveTrue,
checkModelAccess: resolveTrue,
});
@@ -317,6 +322,7 @@ describe('validateWorkflow', () => {
workflow,
templates: { img_resize, main_model_loader },
checkImageAccess: resolveTrue,
+ checkVideoAccess: resolveTrue,
checkBoardAccess: resolveTrue,
checkModelAccess: resolveTrue,
});
@@ -353,6 +359,7 @@ describe('validateWorkflow', () => {
workflow,
templates: { img_resize, main_model_loader },
checkImageAccess: resolveTrue,
+ checkVideoAccess: resolveTrue,
checkBoardAccess: resolveTrue,
checkModelAccess: resolveTrue,
});
@@ -378,6 +385,7 @@ describe('validateWorkflow', () => {
workflow,
templates: { img_resize, main_model_loader },
checkImageAccess: resolveTrue,
+ checkVideoAccess: resolveTrue,
checkBoardAccess: resolveTrue,
checkModelAccess: resolveTrue,
});
@@ -403,6 +411,7 @@ describe('validateWorkflow', () => {
workflow,
templates: { img_resize, main_model_loader },
checkImageAccess: resolveTrue,
+ checkVideoAccess: resolveTrue,
checkBoardAccess: resolveTrue,
checkModelAccess: resolveTrue,
});
@@ -428,6 +437,7 @@ describe('validateWorkflow', () => {
workflow,
templates: { img_resize, main_model_loader, image_collection: imageCollectionTemplate },
checkImageAccess: resolveTrue,
+ checkVideoAccess: resolveTrue,
checkBoardAccess: resolveTrue,
checkModelAccess: resolveTrue,
});
@@ -455,6 +465,7 @@ describe('validateWorkflow', () => {
workflow,
templates: { img_resize, main_model_loader, image_collection: imageCollectionTemplate },
checkImageAccess: resolveTrue,
+ checkVideoAccess: resolveTrue,
checkBoardAccess: resolveTrue,
checkModelAccess: resolveTrue,
});
@@ -480,6 +491,7 @@ describe('validateWorkflow', () => {
workflow,
templates: { img_resize, main_model_loader, image_collection: imageCollectionTemplate },
checkImageAccess: resolveTrue,
+ checkVideoAccess: resolveTrue,
checkBoardAccess: resolveTrue,
checkModelAccess: resolveTrue,
});
@@ -519,6 +531,7 @@ describe('validateWorkflow', () => {
workflow,
templates: { add, call_saved_workflow },
checkImageAccess: resolveTrue,
+ checkVideoAccess: resolveTrue,
checkBoardAccess: resolveTrue,
checkModelAccess: resolveTrue,
getWorkflow: (workflowId) => {
diff --git a/invokeai/frontend/web/src/features/nodes/util/workflow/validateWorkflow.ts b/invokeai/frontend/web/src/features/nodes/util/workflow/validateWorkflow.ts
index 3b1752635bf..045a99120b2 100644
--- a/invokeai/frontend/web/src/features/nodes/util/workflow/validateWorkflow.ts
+++ b/invokeai/frontend/web/src/features/nodes/util/workflow/validateWorkflow.ts
@@ -10,6 +10,7 @@ import {
isImageFieldInputInstance,
isModelFieldType,
isModelIdentifierFieldInputInstance,
+ isVideoFieldInputInstance,
} from 'features/nodes/types/field';
import { getInvocationNodeInputTemplate } from 'features/nodes/types/invocation';
import type { WorkflowV3 } from 'features/nodes/types/workflow';
@@ -41,6 +42,7 @@ type ValidateWorkflowArgs = {
workflow: unknown;
templates: Templates;
checkImageAccess: (name: string) => Promise;
+ checkVideoAccess: (name: string) => Promise;
checkBoardAccess: (id: string) => Promise;
checkModelAccess: (key: string) => Promise;
getWorkflow?: (workflowId: string) => Promise[0]>>;
@@ -135,7 +137,8 @@ const refreshCallSavedWorkflowDynamicInputs = async ({
* @throws {z.ZodError} If there is a validation error.
*/
export const validateWorkflow = async (args: ValidateWorkflowArgs): Promise => {
- const { workflow, templates, checkImageAccess, checkBoardAccess, checkModelAccess, getWorkflow } = args;
+ const { workflow, templates, checkImageAccess, checkVideoAccess, checkBoardAccess, checkModelAccess, getWorkflow } =
+ args;
// Parse the raw workflow data & migrate it to the latest version
const _workflow = parseAndMigrateWorkflow(workflow);
@@ -311,6 +314,14 @@ export const validateWorkflow = async (args: ValidateWorkflowArgs): Promise {
+ const dispatch = useAppDispatch();
+ const { t } = useTranslation();
+ const value = useAppSelector(selectWanTransformerLowNoise);
+ const [modelConfigs, { isLoading }] = useWanGGUFLowNoiseModels();
+
+ const _onChange = useCallback(
+ (model: MainModelConfig | null) => {
+ if (model) {
+ dispatch(wanTransformerLowNoiseSelected(zModelIdentifierField.parse(model)));
+ } else {
+ dispatch(wanTransformerLowNoiseSelected(null));
+ }
+ },
+ [dispatch]
+ );
+
+ const {
+ options,
+ value: comboValue,
+ onChange,
+ noOptionsMessage,
+ } = useModelCombobox({
+ modelConfigs,
+ onChange: _onChange,
+ selectedModel: value,
+ isLoading,
+ });
+
+ return (
+
+ {t('modelManager.wanTransformerLowNoise')}
+
+
+ );
+});
+
+ParamWanTransformerLowNoiseSelect.displayName = 'ParamWanTransformerLowNoiseSelect';
+
+/**
+ * Wan 2.2 Component Source Select
+ *
+ * Picks a Diffusers Wan model whose VAE and UMT5-XXL encoder will be extracted
+ * for the workflow. Required when the main Wan model is a GGUF (since GGUF
+ * mains are transformer-only). Ignored for Diffusers mains, which carry their
+ * own VAE and encoder.
+ */
+const ParamWanComponentSourceSelect = memo(() => {
+ const dispatch = useAppDispatch();
+ const { t } = useTranslation();
+ const value = useAppSelector(selectWanComponentSource);
+ const [modelConfigs, { isLoading }] = useWanDiffusersModels();
+
+ const _onChange = useCallback(
+ (model: MainModelConfig | null) => {
+ if (model) {
+ dispatch(wanComponentSourceSelected(zModelIdentifierField.parse(model)));
+ } else {
+ dispatch(wanComponentSourceSelected(null));
+ }
+ },
+ [dispatch]
+ );
+
+ const {
+ options,
+ value: comboValue,
+ onChange,
+ noOptionsMessage,
+ } = useModelCombobox({
+ modelConfigs,
+ onChange: _onChange,
+ selectedModel: value,
+ isLoading,
+ });
+
+ return (
+
+ {t('modelManager.wanComponentSource')}
+
+
+ );
+});
+
+ParamWanComponentSourceSelect.displayName = 'ParamWanComponentSourceSelect';
+
+/**
+ * Wan 2.2 Standalone VAE Select
+ *
+ * Selects a standalone Wan VAE checkpoint. When set, this overrides the VAE
+ * provided by the Component Source (or the main Diffusers model).
+ */
+const ParamWanVaeModelSelect = memo(() => {
+ const dispatch = useAppDispatch();
+ const { t } = useTranslation();
+ const vaeModel = useAppSelector(selectWanVaeModel);
+ const [modelConfigs, { isLoading }] = useWanVAEModels();
+
+ const _onChange = useCallback(
+ (model: VAEModelConfig | null) => {
+ if (model) {
+ dispatch(wanVaeModelSelected(zModelIdentifierField.parse(model)));
+ } else {
+ dispatch(wanVaeModelSelected(null));
+ }
+ },
+ [dispatch]
+ );
+
+ const { options, value, onChange, noOptionsMessage } = useModelCombobox({
+ modelConfigs,
+ onChange: _onChange,
+ selectedModel: vaeModel,
+ isLoading,
+ });
+
+ return (
+
+ {t('modelManager.wanVae')}
+
+
+ );
+});
+
+ParamWanVaeModelSelect.displayName = 'ParamWanVaeModelSelect';
+
+/**
+ * Wan 2.2 Standalone UMT5-XXL Encoder Select
+ *
+ * Selects a standalone UMT5-XXL encoder. When set, this overrides the encoder
+ * provided by the Component Source (or the main Diffusers model).
+ */
+const ParamWanT5EncoderModelSelect = memo(() => {
+ const dispatch = useAppDispatch();
+ const { t } = useTranslation();
+ const encoderModel = useAppSelector(selectWanT5EncoderModel);
+ const [modelConfigs, { isLoading }] = useWanT5EncoderModels();
+
+ const _onChange = useCallback(
+ (model: WanT5EncoderModelConfig | null) => {
+ if (model) {
+ dispatch(wanT5EncoderModelSelected(zModelIdentifierField.parse(model)));
+ } else {
+ dispatch(wanT5EncoderModelSelected(null));
+ }
+ },
+ [dispatch]
+ );
+
+ const { options, value, onChange, noOptionsMessage } = useModelCombobox({
+ modelConfigs,
+ onChange: _onChange,
+ selectedModel: encoderModel,
+ isLoading,
+ });
+
+ return (
+
+ {t('modelManager.wanT5Encoder')}
+
+
+ );
+});
+
+ParamWanT5EncoderModelSelect.displayName = 'ParamWanT5EncoderModelSelect';
+
+/**
+ * Combined Wan 2.2 component selectors (low-noise transformer + standalone
+ * VAE + standalone T5 encoder + Component Source).
+ *
+ * Only relevant for GGUF workflows. Diffusers Wan mains have everything
+ * built in; TI2V-5B is a single-expert model with no low-noise pair. Showing
+ * these always is fine since they're optional — but the AdvancedSettingsAccordion
+ * still gates the render on `isWan` so they don't pollute other tabs.
+ */
+const ParamWanModelSelects = () => {
+ return (
+ <>
+
+
+
+
+ >
+ );
+};
+
+export default memo(ParamWanModelSelects);
diff --git a/invokeai/frontend/web/src/features/parameters/components/Core/ParamWanGuidanceScaleLowNoise.tsx b/invokeai/frontend/web/src/features/parameters/components/Core/ParamWanGuidanceScaleLowNoise.tsx
new file mode 100644
index 00000000000..e336a770f9a
--- /dev/null
+++ b/invokeai/frontend/web/src/features/parameters/components/Core/ParamWanGuidanceScaleLowNoise.tsx
@@ -0,0 +1,98 @@
+import { CompositeNumberInput, CompositeSlider, FormControl, FormLabel, IconButton } from '@invoke-ai/ui-library';
+import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
+import {
+ selectCFGScale,
+ selectWanGuidanceScaleLowNoise,
+ wanGuidanceScaleLowNoiseChanged,
+} from 'features/controlLayers/store/paramsSlice';
+import type React from 'react';
+import { memo, useCallback } from 'react';
+import { useTranslation } from 'react-i18next';
+import { PiXBold } from 'react-icons/pi';
+
+// Match the primary ParamCFGScale's range so the slider thumb position is
+// visually comparable between the two CFG sliders at the same numeric value
+// (e.g. CFG=5 and CFG-Low=3 should look correct relative to each other).
+const CONSTRAINTS = {
+ initial: 3.5,
+ sliderMin: 1,
+ sliderMax: 20,
+ numberInputMin: 1,
+ numberInputMax: 200,
+ fineStep: 0.1,
+ coarseStep: 0.5,
+};
+
+const MARKS = [CONSTRAINTS.sliderMin, Math.floor(CONSTRAINTS.sliderMax / 2), CONSTRAINTS.sliderMax];
+
+/**
+ * Wan 2.2 Guidance Scale (Low Noise)
+ *
+ * Optional separate CFG for the A14B low-noise expert. When null (cleared),
+ * the denoise node falls back to the primary guidance_scale. Ignored for
+ * TI2V-5B (single-expert).
+ *
+ * Diffusers reference defaults for A14B: primary 4.0 / low-noise 3.0 — i.e.
+ * a slightly lower CFG on the detail-pass expert produces less over-sharpened
+ * output.
+ */
+const ParamWanGuidanceScaleLowNoise = () => {
+ const { t } = useTranslation();
+ const value = useAppSelector(selectWanGuidanceScaleLowNoise);
+ const primaryCfgScale = useAppSelector(selectCFGScale);
+ const dispatch = useAppDispatch();
+
+ const onChange = useCallback((v: number) => dispatch(wanGuidanceScaleLowNoiseChanged(v)), [dispatch]);
+ const onReset = useCallback(
+ (e: React.MouseEvent) => {
+ e.preventDefault();
+ e.stopPropagation();
+ dispatch(wanGuidanceScaleLowNoiseChanged(null));
+ },
+ [dispatch]
+ );
+
+ // When unset, the backend falls back to the *primary* guidance scale — show that
+ // effective value rather than a constant the run won't actually use.
+ const displayValue = value ?? primaryCfgScale;
+
+ return (
+
+
+ {t('parameters.wanGuidanceScaleLowNoise')}{' '}
+ {value !== null && (
+ }
+ onClick={onReset}
+ minW={4}
+ h={4}
+ />
+ )}
+
+
+
+
+ );
+};
+
+export default memo(ParamWanGuidanceScaleLowNoise);
diff --git a/invokeai/frontend/web/src/features/parameters/types/constants.ts b/invokeai/frontend/web/src/features/parameters/types/constants.ts
index a3ffa24cc64..1674c16009e 100644
--- a/invokeai/frontend/web/src/features/parameters/types/constants.ts
+++ b/invokeai/frontend/web/src/features/parameters/types/constants.ts
@@ -49,6 +49,10 @@ export const CLIP_SKIP_MAP: { [key in BaseModelType]?: { maxClip: number; marker
maxClip: 0,
markers: [],
},
+ wan: {
+ maxClip: 0,
+ markers: [],
+ },
};
/**
diff --git a/invokeai/frontend/web/src/features/parameters/util/optimalDimension.ts b/invokeai/frontend/web/src/features/parameters/util/optimalDimension.ts
index 2ac59a32e2b..4b2263db2f4 100644
--- a/invokeai/frontend/web/src/features/parameters/util/optimalDimension.ts
+++ b/invokeai/frontend/web/src/features/parameters/util/optimalDimension.ts
@@ -63,9 +63,16 @@ export const isInSDXLTrainingDimensions = (width: number, height: number): boole
/**
* Gets the grid size for a given base model. For Flux, the grid size is 16, otherwise it is 8.
* - sd-1, sd-2, sdxl, anima: 8
- * - flux, sd-3, qwen-image, z-image: 16
+ * - flux, sd-3, qwen-image, z-image, wan: 16
* - cogview4: 32
* - default: 8
+ *
+ * Wan 2.2's transformer has ``patch_size=(1, 2, 2)``: it patch-embeds with
+ * stride 2 then un-patches by 2. Combined with the VAE's 8x spatial scale,
+ * canvas H/W must be a multiple of ``8 * 2 = 16``; otherwise the patch
+ * round-trip produces an off-by-one and the scheduler step fails with a
+ * spatial-dim mismatch between latents and noise prediction.
+ *
* @param base The base model
* @returns The grid size for the model, defaulting to 8
*/
@@ -77,6 +84,7 @@ export const getGridSize = (base?: BaseModelType | null): number => {
case 'flux2':
case 'sd-3':
case 'qwen-image':
+ case 'wan':
case 'z-image':
return 16;
case 'sd-1':
diff --git a/invokeai/frontend/web/src/features/queue/hooks/useEnqueueCanvas.ts b/invokeai/frontend/web/src/features/queue/hooks/useEnqueueCanvas.ts
index 1229371b6e8..7eef621819b 100644
--- a/invokeai/frontend/web/src/features/queue/hooks/useEnqueueCanvas.ts
+++ b/invokeai/frontend/web/src/features/queue/hooks/useEnqueueCanvas.ts
@@ -21,6 +21,7 @@ import { buildQwenImageGraph } from 'features/nodes/util/graph/generation/buildQ
import { buildSD1Graph } from 'features/nodes/util/graph/generation/buildSD1Graph';
import { buildSD3Graph } from 'features/nodes/util/graph/generation/buildSD3Graph';
import { buildSDXLGraph } from 'features/nodes/util/graph/generation/buildSDXLGraph';
+import { buildWanGraph } from 'features/nodes/util/graph/generation/buildWanGraph';
import { buildZImageGraph } from 'features/nodes/util/graph/generation/buildZImageGraph';
import { selectCanvasDestination } from 'features/nodes/util/graph/graphBuilderUtils';
import type { GraphBuilderArg } from 'features/nodes/util/graph/types';
@@ -73,6 +74,8 @@ const enqueueCanvas = async (store: AppStore, canvasManager: CanvasManager, prep
return await buildExternalGraph(graphBuilderArg);
case 'anima':
return await buildAnimaGraph(graphBuilderArg);
+ case 'wan':
+ return await buildWanGraph(graphBuilderArg);
default:
assert(false, `No graph builders for base ${base}`);
}
diff --git a/invokeai/frontend/web/src/features/queue/hooks/useEnqueueGenerate.ts b/invokeai/frontend/web/src/features/queue/hooks/useEnqueueGenerate.ts
index 8b0c30d924f..5650905576a 100644
--- a/invokeai/frontend/web/src/features/queue/hooks/useEnqueueGenerate.ts
+++ b/invokeai/frontend/web/src/features/queue/hooks/useEnqueueGenerate.ts
@@ -19,6 +19,7 @@ import { buildQwenImageGraph } from 'features/nodes/util/graph/generation/buildQ
import { buildSD1Graph } from 'features/nodes/util/graph/generation/buildSD1Graph';
import { buildSD3Graph } from 'features/nodes/util/graph/generation/buildSD3Graph';
import { buildSDXLGraph } from 'features/nodes/util/graph/generation/buildSDXLGraph';
+import { buildWanGraph } from 'features/nodes/util/graph/generation/buildWanGraph';
import { buildZImageGraph } from 'features/nodes/util/graph/generation/buildZImageGraph';
import type { GraphBuilderArg } from 'features/nodes/util/graph/types';
import { UnsupportedGenerationModeError } from 'features/nodes/util/graph/types';
@@ -66,6 +67,8 @@ const enqueueGenerate = async (store: AppStore, prepend: boolean) => {
return await buildExternalGraph(graphBuilderArg);
case 'anima':
return await buildAnimaGraph(graphBuilderArg);
+ case 'wan':
+ return await buildWanGraph(graphBuilderArg);
default:
assert(false, `No graph builders for base ${base}`);
}
diff --git a/invokeai/frontend/web/src/features/queue/store/readiness.ts b/invokeai/frontend/web/src/features/queue/store/readiness.ts
index 1e40cc6ce18..d6958ff750b 100644
--- a/invokeai/frontend/web/src/features/queue/store/readiness.ts
+++ b/invokeai/frontend/web/src/features/queue/store/readiness.ts
@@ -311,6 +311,19 @@ export const getReasonsWhyCannotEnqueueGenerateTab = (arg: {
}
}
+ if (model?.base === 'wan' && model.format === 'gguf_quantized') {
+ // GGUF Wan mains carry only the transformer; VAE + UMT5-XXL encoder must
+ // come from either standalone models or the Component Source (Diffusers).
+ // The low-noise A14B partner expert is optional — if omitted, the loader
+ // will use the high-noise expert for the whole schedule (lower quality
+ // but still produces an image).
+ const hasVaeSource = params.wanVaeModel !== null || params.wanComponentSource !== null;
+ const hasEncoderSource = params.wanT5EncoderModel !== null || params.wanComponentSource !== null;
+ if (!hasVaeSource || !hasEncoderSource) {
+ reasons.push({ content: i18n.t('parameters.invoke.noWanComponentSourceSelected') });
+ }
+ }
+
if (model?.base === 'z-image') {
// Check if VAE source is available (either separate VAE or Qwen3 Source)
const hasVaeSource = params.zImageVaeModel !== null || params.zImageQwen3SourceModel !== null;
@@ -771,6 +784,19 @@ export const getReasonsWhyCannotEnqueueCanvasTab = (arg: {
}
}
+ if (model?.base === 'wan' && model.format === 'gguf_quantized') {
+ // GGUF Wan mains carry only the transformer; VAE + UMT5-XXL encoder must
+ // come from either standalone models or the Component Source (Diffusers).
+ // The low-noise A14B partner expert is optional — if omitted, the loader
+ // will use the high-noise expert for the whole schedule (lower quality
+ // but still produces an image).
+ const hasVaeSource = params.wanVaeModel !== null || params.wanComponentSource !== null;
+ const hasEncoderSource = params.wanT5EncoderModel !== null || params.wanComponentSource !== null;
+ if (!hasVaeSource || !hasEncoderSource) {
+ reasons.push({ content: i18n.t('parameters.invoke.noWanComponentSourceSelected') });
+ }
+ }
+
if (model?.base === 'z-image') {
// Check if VAE source is available (either separate VAE or Qwen3 Source)
const hasVaeSource = params.zImageVaeModel !== null || params.zImageQwen3SourceModel !== null;
diff --git a/invokeai/frontend/web/src/features/settingsAccordions/components/AdvancedSettingsAccordion/AdvancedSettingsAccordion.tsx b/invokeai/frontend/web/src/features/settingsAccordions/components/AdvancedSettingsAccordion/AdvancedSettingsAccordion.tsx
index bfb69b945c8..312c9b71df9 100644
--- a/invokeai/frontend/web/src/features/settingsAccordions/components/AdvancedSettingsAccordion/AdvancedSettingsAccordion.tsx
+++ b/invokeai/frontend/web/src/features/settingsAccordions/components/AdvancedSettingsAccordion/AdvancedSettingsAccordion.tsx
@@ -10,6 +10,7 @@ import {
selectIsFlux2,
selectIsQwenImage,
selectIsSD3,
+ selectIsWan,
selectIsZImage,
selectParamsSlice,
selectVAEKey,
@@ -24,6 +25,7 @@ import ParamFlux2KleinModelSelect from 'features/parameters/components/Advanced/
import ParamQwenImageComponentSourceSelect from 'features/parameters/components/Advanced/ParamQwenImageComponentSourceSelect';
import ParamQwenImageQuantization from 'features/parameters/components/Advanced/ParamQwenImageQuantization';
import ParamT5EncoderModelSelect from 'features/parameters/components/Advanced/ParamT5EncoderModelSelect';
+import ParamWanModelSelects from 'features/parameters/components/Advanced/ParamWanModelSelects';
import ParamZImageQwen3VaeModelSelect from 'features/parameters/components/Advanced/ParamZImageQwen3VaeModelSelect';
import ParamSeamlessXAxis from 'features/parameters/components/Seamless/ParamSeamlessXAxis';
import ParamSeamlessYAxis from 'features/parameters/components/Seamless/ParamSeamlessYAxis';
@@ -54,6 +56,7 @@ export const AdvancedSettingsAccordion = memo(() => {
const isExternal = useAppSelector(selectIsExternal);
const isQwenImage = useAppSelector(selectIsQwenImage);
const isAnima = useAppSelector(selectIsAnima);
+ const isWan = useAppSelector(selectIsWan);
const selectBadges = useMemo(
() =>
@@ -107,13 +110,13 @@ export const AdvancedSettingsAccordion = memo(() => {
return (
- {!isZImage && !isAnima && !isFlux2 && !isQwenImage && (
+ {!isZImage && !isAnima && !isFlux2 && !isQwenImage && !isWan && (
{isFLUX ? : }
{!isFLUX && !isSD3 && }
)}
- {!isFLUX && !isFlux2 && !isSD3 && !isZImage && !isQwenImage && !isAnima && (
+ {!isFLUX && !isFlux2 && !isSD3 && !isZImage && !isQwenImage && !isAnima && !isWan && (
<>
@@ -166,6 +169,11 @@ export const AdvancedSettingsAccordion = memo(() => {
)}
+ {isWan && (
+
+
+
+ )}
);
diff --git a/invokeai/frontend/web/src/features/settingsAccordions/components/GenerationSettingsAccordion/GenerationSettingsAccordion.tsx b/invokeai/frontend/web/src/features/settingsAccordions/components/GenerationSettingsAccordion/GenerationSettingsAccordion.tsx
index 220008a38b0..2ec05cd46d8 100644
--- a/invokeai/frontend/web/src/features/settingsAccordions/components/GenerationSettingsAccordion/GenerationSettingsAccordion.tsx
+++ b/invokeai/frontend/web/src/features/settingsAccordions/components/GenerationSettingsAccordion/GenerationSettingsAccordion.tsx
@@ -13,6 +13,7 @@ import {
selectIsFlux2,
selectIsQwenImage,
selectIsSD3,
+ selectIsWan,
selectIsZImage,
selectModelSupportsGuidance,
selectModelSupportsSteps,
@@ -29,6 +30,7 @@ import ParamGuidance from 'features/parameters/components/Core/ParamGuidance';
import ParamQwenImageShift from 'features/parameters/components/Core/ParamQwenImageShift';
import ParamScheduler from 'features/parameters/components/Core/ParamScheduler';
import ParamSteps from 'features/parameters/components/Core/ParamSteps';
+import ParamWanGuidanceScaleLowNoise from 'features/parameters/components/Core/ParamWanGuidanceScaleLowNoise';
import ParamZImageScheduler from 'features/parameters/components/Core/ParamZImageScheduler';
import ParamZImageShift from 'features/parameters/components/Core/ParamZImageShift';
import ParamZImageSeedVarianceSettings from 'features/parameters/components/SeedVariance/ParamZImageSeedVarianceSettings';
@@ -55,6 +57,7 @@ export const GenerationSettingsAccordion = memo(() => {
const isExternal = useAppSelector(selectIsExternal);
const isQwenImage = useAppSelector(selectIsQwenImage);
const isAnima = useAppSelector(selectIsAnima);
+ const isWan = useAppSelector(selectIsWan);
const fluxDypePreset = useAppSelector(selectFluxDypePreset);
const modelSupportsGuidance = useAppSelector(selectModelSupportsGuidance);
const modelSupportsSteps = useAppSelector(selectModelSupportsSteps);
@@ -104,7 +107,8 @@ export const GenerationSettingsAccordion = memo(() => {
!isCogView4 &&
!isZImage &&
!isQwenImage &&
- !isAnima && }
+ !isAnima &&
+ !isWan && }
{!isExternal && (isFLUX || isFlux2) && }
{!isExternal && isZImage && }
{!isExternal && isAnima && }
@@ -114,6 +118,7 @@ export const GenerationSettingsAccordion = memo(() => {
)}
{!isExternal && !isFLUX && !isFlux2 && }
+ {!isExternal && isWan && }
{!isExternal && isZImage && }
{!isExternal && isQwenImage && }
{!isExternal && isFLUX && }
diff --git a/invokeai/frontend/web/src/features/settingsAccordions/components/GenerationSettingsAccordion/MainModelPicker.tsx b/invokeai/frontend/web/src/features/settingsAccordions/components/GenerationSettingsAccordion/MainModelPicker.tsx
index 66f76dcd153..134ab5f1e62 100644
--- a/invokeai/frontend/web/src/features/settingsAccordions/components/GenerationSettingsAccordion/MainModelPicker.tsx
+++ b/invokeai/frontend/web/src/features/settingsAccordions/components/GenerationSettingsAccordion/MainModelPicker.tsx
@@ -17,7 +17,26 @@ export const MainModelPicker = memo(() => {
const { t } = useTranslation();
const dispatch = useAppDispatch();
const activeTab = useAppSelector(selectActiveTab);
- const [modelConfigs] = useMainModels();
+ const [allModelConfigs] = useMainModels();
+ // Low-noise Wan GGUFs belong in the Transformer (Low Noise) slot of the
+ // Wan advanced section, not as a primary main. Filter them out of the main
+ // model dropdown so users can't accidentally wire them backwards.
+ const modelConfigs = useMemo(
+ () =>
+ allModelConfigs.filter((c) => {
+ if (
+ c.type === 'main' &&
+ c.base === 'wan' &&
+ c.format === 'gguf_quantized' &&
+ 'expert' in c &&
+ c.expert === 'low'
+ ) {
+ return false;
+ }
+ return true;
+ }),
+ [allModelConfigs]
+ );
const selectedModelConfig = useSelectedModelConfig();
const onChange = useCallback(
(modelConfig: AnyModelConfigWithExternal) => {
diff --git a/invokeai/frontend/web/src/features/stylePresets/components/StylePresetForm/StylePresetImageField.tsx b/invokeai/frontend/web/src/features/stylePresets/components/StylePresetForm/StylePresetImageField.tsx
index 089293d4e7a..e1b642b9ff0 100644
--- a/invokeai/frontend/web/src/features/stylePresets/components/StylePresetForm/StylePresetImageField.tsx
+++ b/invokeai/frontend/web/src/features/stylePresets/components/StylePresetForm/StylePresetImageField.tsx
@@ -1,5 +1,5 @@
import { Box, Button, Flex, Icon, IconButton, Image, Tooltip } from '@invoke-ai/ui-library';
-import { dropzoneAccept } from 'common/hooks/useImageUploadButton';
+import { imageDropzoneAccept } from 'common/util/uploadMediaAccept';
import { useCallback } from 'react';
import { useDropzone } from 'react-dropzone';
import type { UseControllerProps } from 'react-hook-form';
@@ -27,7 +27,7 @@ export const StylePresetImageField = (props: UseControllerProps(null);
const cleanup = () => $dialogState.set(null);
@@ -56,6 +63,7 @@ const useLoadImmediate = () => {
const loadWorkflowFromLibrary = useLoadWorkflowFromLibrary();
const loadWorkflowFromFile = useLoadWorkflowFromFile();
const loadWorkflowFromImage = useLoadWorkflowFromImage();
+ const loadWorkflowFromVideo = useLoadWorkflowFromVideo();
const loadWorkflowFromObject = useLoadWorkflowFromObject();
const loadImmediate = useCallback(async () => {
@@ -77,12 +85,15 @@ const useLoadImmediate = () => {
await loadWorkflowFromLibrary(data, options);
} else if (type === 'image') {
await loadWorkflowFromImage(data, options);
+ } else if (type === 'video') {
+ await loadWorkflowFromVideo(data, options);
}
cleanup();
workflowLibraryModal.close();
}, [
loadWorkflowFromFile,
loadWorkflowFromImage,
+ loadWorkflowFromVideo,
loadWorkflowFromLibrary,
loadWorkflowFromObject,
workflowLibraryModal,
@@ -113,7 +124,12 @@ export const useLoadWorkflowWithDialog = () => {
* @param data.onCompleted - A callback to call when the loading process is completed (both success and error).
*/
(
- data: LoadLibraryWorkflowData | LoadWorkflowFromObjectData | LoadWorkflowFromFileData | LoadWorkflowFromImageData
+ data:
+ | LoadLibraryWorkflowData
+ | LoadWorkflowFromObjectData
+ | LoadWorkflowFromFileData
+ | LoadWorkflowFromImageData
+ | LoadWorkflowFromVideoData
) => {
if (!doesWorkflowHaveUnsavedChanges) {
$dialogState.set({ ...data, isOpen: false });
diff --git a/invokeai/frontend/web/src/features/workflowLibrary/hooks/useLoadWorkflowFromVideo.ts b/invokeai/frontend/web/src/features/workflowLibrary/hooks/useLoadWorkflowFromVideo.ts
new file mode 100644
index 00000000000..0bfd156fea3
--- /dev/null
+++ b/invokeai/frontend/web/src/features/workflowLibrary/hooks/useLoadWorkflowFromVideo.ts
@@ -0,0 +1,66 @@
+import type { WorkflowV3 } from 'features/nodes/types/workflow';
+import { graphToWorkflow } from 'features/nodes/util/workflow/graphToWorkflow';
+import { toast } from 'features/toast/toast';
+import { useValidateAndLoadWorkflow } from 'features/workflowLibrary/hooks/useValidateAndLoadWorkflow';
+import { t } from 'i18next';
+import { useCallback } from 'react';
+import { useLazyGetVideoWorkflowQuery } from 'services/api/endpoints/videos';
+import type { NonNullableGraph } from 'services/api/types';
+import { assert } from 'tsafe';
+
+/**
+ * Loads a workflow from a generated video. Mirrors `useLoadWorkflowFromImage`.
+ *
+ * You probably should instead use `useLoadWorkflowWithDialog`, which opens a dialog to prevent loss of unsaved changes
+ * and handles the loading process.
+ */
+export const useLoadWorkflowFromVideo = () => {
+ const [getWorkflowAndGraphFromVideo] = useLazyGetVideoWorkflowQuery();
+ const validateAndLoadWorkflow = useValidateAndLoadWorkflow();
+ const loadWorkflowFromVideo = useCallback(
+ async (
+ videoName: string,
+ options: {
+ onSuccess?: (workflow: WorkflowV3) => void;
+ onError?: () => void;
+ onCompleted?: () => void;
+ } = {}
+ ) => {
+ const { onSuccess, onError, onCompleted } = options;
+ try {
+ const { workflow, graph } = await getWorkflowAndGraphFromVideo(videoName).unwrap();
+
+ // Videos may have a workflow and/or a graph. We can load either into the workflow editor, but we prefer the
+ // workflow.
+ const unvalidatedWorkflow = workflow
+ ? JSON.parse(workflow)
+ : graph
+ ? graphToWorkflow(JSON.parse(graph) as NonNullableGraph, true)
+ : null;
+
+ assert(unvalidatedWorkflow !== null, 'No workflow or graph provided');
+
+ const validatedWorkflow = await validateAndLoadWorkflow(unvalidatedWorkflow, 'image');
+
+ if (!validatedWorkflow) {
+ onError?.();
+ return;
+ }
+
+ onSuccess?.(validatedWorkflow);
+ } catch {
+ toast({
+ id: 'PROBLEM_RETRIEVING_WORKFLOW',
+ title: t('toast.problemRetrievingWorkflow'),
+ status: 'error',
+ });
+ onError?.();
+ } finally {
+ onCompleted?.();
+ }
+ },
+ [getWorkflowAndGraphFromVideo, validateAndLoadWorkflow]
+ );
+
+ return loadWorkflowFromVideo;
+};
diff --git a/invokeai/frontend/web/src/features/workflowLibrary/hooks/useValidateAndLoadWorkflow.ts b/invokeai/frontend/web/src/features/workflowLibrary/hooks/useValidateAndLoadWorkflow.ts
index 7fcfe26b4bd..a6f5ace0f8f 100644
--- a/invokeai/frontend/web/src/features/workflowLibrary/hooks/useValidateAndLoadWorkflow.ts
+++ b/invokeai/frontend/web/src/features/workflowLibrary/hooks/useValidateAndLoadWorkflow.ts
@@ -15,7 +15,12 @@ import { t } from 'i18next';
import { useCallback } from 'react';
import { serializeError } from 'serialize-error';
import { workflowsApi } from 'services/api/endpoints/workflows';
-import { checkBoardAccess, checkImageAccess, checkModelAccess } from 'services/api/hooks/accessChecks';
+import {
+ checkBoardAccess,
+ checkImageAccess,
+ checkModelAccess,
+ checkVideoAccess,
+} from 'services/api/hooks/accessChecks';
import { z } from 'zod';
import { fromZodError } from 'zod-validation-error';
@@ -56,6 +61,7 @@ export const useValidateAndLoadWorkflow = () => {
workflow: unvalidatedWorkflow,
templates,
checkImageAccess,
+ checkVideoAccess,
checkBoardAccess,
checkModelAccess,
getWorkflow: (workflowId) =>
diff --git a/invokeai/frontend/web/src/services/api/endpoints/auth.ts b/invokeai/frontend/web/src/services/api/endpoints/auth.ts
index ae7bfa7426f..dbbdf4b68f8 100644
--- a/invokeai/frontend/web/src/services/api/endpoints/auth.ts
+++ b/invokeai/frontend/web/src/services/api/endpoints/auth.ts
@@ -30,6 +30,10 @@ type LogoutResponse = {
success: boolean;
};
+type MediaCookieResponse = {
+ success: boolean;
+};
+
type SetupStatusResponse = {
setup_required: boolean;
multiuser_enabled: boolean;
@@ -79,9 +83,26 @@ export const authApi = api.injectEndpoints({
url: 'api/v1/auth/logout',
method: 'POST',
}),
- // Invalidate boards and images cache on logout to clear stale data
+ // NOTE: cross-user cache clearing does NOT rely on this list. A store-level
+ // listener (see store.ts) dispatches api.util.resetApiState() on the logout /
+ // sessionExpiredLogout actions, dropping EVERY cached query — video, gallery,
+ // and any future tag types included. These tags are a redundant belt-and-braces
+ // for the brief window before that action fires.
invalidatesTags: ['Board', 'Image', 'ImageList', 'ImageNameList', 'ImageCollection', 'ImageMetadata'],
}),
+ /**
+ * Re-issues the HttpOnly media cookie from the session's Bearer token. Media elements
+ * (``) can't send Authorization headers, so video routes authenticate via a
+ * cookie that is normally set at login — a restored session can hold a valid JWT without
+ * it, making every video render as a black player. Called once on app load (see
+ * useMediaCookieRefresh) so such sessions self-heal without re-login.
+ */
+ refreshMediaCookie: build.mutation({
+ query: () => ({
+ url: 'api/v1/auth/media-cookie',
+ method: 'POST',
+ }),
+ }),
getCurrentUser: build.query({
query: () => 'api/v1/auth/me',
}),
@@ -142,6 +163,7 @@ export const authApi = api.injectEndpoints({
export const {
useLoginMutation,
useLogoutMutation,
+ useRefreshMediaCookieMutation,
useGetCurrentUserQuery,
useSetupMutation,
useGetSetupStatusQuery,
diff --git a/invokeai/frontend/web/src/services/api/endpoints/boards.ts b/invokeai/frontend/web/src/services/api/endpoints/boards.ts
index 9b7a4f2ad8a..70d1cfda002 100644
--- a/invokeai/frontend/web/src/services/api/endpoints/boards.ts
+++ b/invokeai/frontend/web/src/services/api/endpoints/boards.ts
@@ -6,9 +6,10 @@ import type {
ImageCategory,
ListBoardsArgs,
OffsetPaginatedResults_ImageDTO_,
+ OffsetPaginatedResults_VideoDTO_,
UpdateBoardArg,
} from 'services/api/types';
-import { getListImagesUrl } from 'services/api/util';
+import { getListImagesUrl, getListVideosUrl } from 'services/api/util';
import type { ApiTagDescription } from '..';
import { api, buildV1Url, LIST_TAG } from '..';
@@ -95,6 +96,22 @@ export const boardsApi = api.injectEndpoints({
},
}),
+ getBoardVideosTotal: build.query<{ total: number }, string | undefined>({
+ query: (board_id) => ({
+ url: getListVideosUrl({
+ board_id: board_id ?? 'none',
+ is_intermediate: false,
+ limit: 0,
+ offset: 0,
+ }),
+ method: 'GET',
+ }),
+ providesTags: (result, error, arg) => [{ type: 'BoardVideosTotal', id: arg ?? 'none' }, 'FetchOnReconnect'],
+ transformResponse: (response: OffsetPaginatedResults_VideoDTO_) => {
+ return { total: response.total };
+ },
+ }),
+
/**
* Boards Mutations
*/
@@ -132,6 +149,7 @@ export const {
useListAllBoardsQuery,
useGetBoardImagesTotalQuery,
useGetBoardAssetsTotalQuery,
+ useGetBoardVideosTotalQuery,
useCreateBoardMutation,
useUpdateBoardMutation,
useListAllImageNamesForBoardQuery,
diff --git a/invokeai/frontend/web/src/services/api/endpoints/gallery.ts b/invokeai/frontend/web/src/services/api/endpoints/gallery.ts
new file mode 100644
index 00000000000..5b3dd85493e
--- /dev/null
+++ b/invokeai/frontend/web/src/services/api/endpoints/gallery.ts
@@ -0,0 +1,58 @@
+import type {
+ GetGalleryItemNamesArgs,
+ GetGalleryItemNamesResult,
+ ListGalleryItemsArgs,
+ ListGalleryItemsResponse,
+} from 'services/api/types';
+import { getListGalleryItemsUrl } from 'services/api/util';
+import stableHash from 'stable-hash';
+
+import { api, buildV1Url } from '..';
+
+/**
+ * Builds an endpoint URL for the gallery router.
+ * @example
+ * buildGalleryUrl('items/') // 'api/v1/gallery/items/'
+ */
+const buildGalleryUrl = (path: string = '', query?: Parameters[1]) =>
+ buildV1Url(`gallery/${path}`, query);
+
+export const galleryApi = api.injectEndpoints({
+ endpoints: (build) => ({
+ /** Paginated polymorphic stream of images + videos, sorted by created_at. */
+ listGalleryItems: build.query({
+ query: (queryArgs) => ({
+ url: getListGalleryItemsUrl(queryArgs),
+ method: 'GET',
+ }),
+ providesTags: (result, error, queryArgs) => [
+ 'GalleryItemList',
+ 'FetchOnReconnect',
+ { type: 'GalleryItemList', id: stableHash(queryArgs) },
+ { type: 'Board', id: queryArgs.board_id ?? 'none' },
+ ],
+ }),
+
+ /**
+ * Ordered (kind, name) refs for virtualized selection. The gallery grid's name list and
+ * keyboard navigation use this — the flat string list is derived by mapping items to `name`.
+ */
+ getGalleryItemNames: build.query({
+ query: (queryArgs) => ({
+ url: buildGalleryUrl('items/names', queryArgs),
+ method: 'GET',
+ }),
+ providesTags: (result, error, queryArgs) => [
+ 'GalleryItemNameList',
+ 'FetchOnReconnect',
+ { type: 'GalleryItemNameList', id: stableHash(queryArgs) },
+ ],
+ }),
+ }),
+});
+
+// useGetGalleryItemNamesQuery is consumed by use-gallery-image-names.ts.
+export const { useGetGalleryItemNamesQuery } = galleryApi;
+
+/** @knipignore Lands with the paged gallery view / future bulk-DTO consumers; not used today. */
+export const { useListGalleryItemsQuery } = galleryApi;
diff --git a/invokeai/frontend/web/src/services/api/endpoints/images.ts b/invokeai/frontend/web/src/services/api/endpoints/images.ts
index 7b150ac3572..67cf8499547 100644
--- a/invokeai/frontend/web/src/services/api/endpoints/images.ts
+++ b/invokeai/frontend/web/src/services/api/endpoints/images.ts
@@ -18,6 +18,7 @@ import { getListImagesUrl } from 'services/api/util';
import {
getTagsToInvalidateForBoardAffectingMutation,
getTagsToInvalidateForImageMutation,
+ getTagsToInvalidateForVideoMutation,
} from 'services/api/util/tagInvalidation';
import stableHash from 'stable-hash';
import type { Param0 } from 'tsafe';
@@ -294,9 +295,11 @@ export const imagesApi = api.injectEndpoints({
paths['/api/v1/boards/{board_id}']['delete']['parameters']['path']
>({
query: ({ board_id }) => ({ url: buildBoardsUrl(board_id), method: 'DELETE' }),
- invalidatesTags: () => [
+ invalidatesTags: (result) => [
{ type: 'Board', id: LIST_TAG },
- // invalidate the 'No Board' cache
+ // Both images and videos on the board cascade to the 'No Board' bucket on the
+ // backend side; invalidate the 'none' caches for both kinds so the polymorphic
+ // gallery surfaces them. The Gallery* tags refresh the unified gallery list view.
{
type: 'ImageList',
id: getListImagesUrl({
@@ -311,6 +314,14 @@ export const imagesApi = api.injectEndpoints({
categories: ASSETS_CATEGORIES,
}),
},
+ { type: 'VideoList', id: LIST_TAG },
+ 'VideoNameList',
+ 'GalleryItemList',
+ 'GalleryItemNameList',
+ // The orphaned media keep cached DTOs whose board_id still points at the deleted
+ // board; refetch them so drag/drop and context menus see the new 'none' board.
+ ...getTagsToInvalidateForImageMutation(result?.deleted_board_images ?? []),
+ ...getTagsToInvalidateForVideoMutation(result?.deleted_board_videos ?? []),
],
}),
@@ -323,7 +334,22 @@ export const imagesApi = api.injectEndpoints({
method: 'DELETE',
params: { include_images: true },
}),
- invalidatesTags: () => [{ type: 'Board', id: LIST_TAG }],
+ // The backend now also cascade-deletes videos on the board, so the unified gallery
+ // and the video list both need invalidation in addition to the board tag.
+ invalidatesTags: (result) => [
+ { type: 'Board', id: LIST_TAG },
+ { type: 'VideoList', id: LIST_TAG },
+ 'VideoNameList',
+ 'GalleryItemList',
+ 'GalleryItemNameList',
+ // Deleted media must drop out of their per-item caches (so DTO queries become
+ // 404s instead of serving stale entries, e.g. to node inputs still referencing
+ // them). Only server-confirmed deletions are listed in the result.
+ ...getTagsToInvalidateForImageMutation(result?.deleted_images ?? []),
+ ...getTagsToInvalidateForVideoMutation(result?.deleted_videos ?? []),
+ ...getTagsToInvalidateForImageMutation(result?.failed_images ?? []),
+ ...getTagsToInvalidateForVideoMutation(result?.failed_videos ?? []),
+ ],
}),
addImageToBoard: build.mutation<
paths['/api/v1/board_images/']['post']['responses']['201']['content']['application/json'],
@@ -484,7 +510,6 @@ export const {
useStarImagesMutation,
useUnstarImagesMutation,
useBulkDownloadImagesMutation,
- useGetImageNamesQuery,
useGetImageDTOsByNamesMutation,
} = imagesApi;
diff --git a/invokeai/frontend/web/src/services/api/endpoints/videos.ts b/invokeai/frontend/web/src/services/api/endpoints/videos.ts
new file mode 100644
index 00000000000..bd1e67d319c
--- /dev/null
+++ b/invokeai/frontend/web/src/services/api/endpoints/videos.ts
@@ -0,0 +1,395 @@
+import { skipToken } from '@reduxjs/toolkit/query';
+import { getStore } from 'app/store/nanostores/store';
+import type { paths } from 'services/api/schema';
+import type {
+ GetVideoNamesArgs,
+ GetVideoNamesResult,
+ ListVideosArgs,
+ ListVideosResponse,
+ UploadVideoArg,
+ VideoDTO,
+} from 'services/api/types';
+import { getListVideosUrl } from 'services/api/util';
+import {
+ getTagsToInvalidateForBoardAffectingMutation,
+ getTagsToInvalidateForVideoMutation,
+} from 'services/api/util/tagInvalidation';
+import stableHash from 'stable-hash';
+import type { Param0 } from 'tsafe';
+import type { JsonObject } from 'type-fest';
+
+import { api, buildV1Url, LIST_TAG } from '..';
+
+/**
+ * Builds an endpoint URL for the videos router.
+ * @example
+ * buildVideosUrl('some-path') // 'api/v1/videos/some-path'
+ */
+const buildVideosUrl = (path: string = '', query?: Parameters[1]) =>
+ buildV1Url(`videos/${path}`, query);
+
+/**
+ * Video RTK Query endpoints — parallel to imagesApi. Used by the gallery (Phase 4) and the
+ * viewer / linear flows that land in later phases.
+ */
+export const videosApi = api.injectEndpoints({
+ endpoints: (build) => ({
+ /**
+ * List videos (paginated). Used directly when a video-only view is needed; the gallery
+ * itself uses the polymorphic /gallery/items/ endpoint.
+ */
+ listVideos: build.query({
+ query: (queryArgs) => ({
+ url: getListVideosUrl(queryArgs),
+ method: 'GET',
+ }),
+ providesTags: (result, error, queryArgs) => [
+ { type: 'VideoList', id: stableHash(queryArgs) },
+ // The LIST_TAG-scoped tag is what mutations invalidate when they can't know which
+ // query-arg-specific lists changed (star/unstar, board cascades). Without
+ // providing it here those invalidations match nothing and the lists go stale.
+ { type: 'VideoList', id: LIST_TAG },
+ { type: 'Board', id: queryArgs.board_id ?? 'none' },
+ 'FetchOnReconnect',
+ ],
+ async onQueryStarted(_, { dispatch, queryFulfilled }) {
+ // Pre-populate the per-video getVideoDTO cache so selection feels snappy.
+ const res = await queryFulfilled;
+ const videoDTOs = res.data.items;
+ const updates: Param0 = [];
+ for (const videoDTO of videoDTOs) {
+ updates.push({
+ endpointName: 'getVideoDTO',
+ arg: videoDTO.video_name,
+ value: videoDTO,
+ });
+ }
+ dispatch(videosApi.util.upsertQueryEntries(updates));
+ },
+ }),
+
+ getVideoDTO: build.query({
+ query: (video_name) => ({ url: buildVideosUrl(`i/${video_name}`) }),
+ providesTags: (result, error, video_name) => [{ type: 'Video', id: video_name }],
+ }),
+
+ getVideoMetadata: build.query({
+ query: (video_name) => ({ url: buildVideosUrl(`i/${video_name}/metadata`) }),
+ providesTags: (result, error, video_name) => [{ type: 'VideoMetadata', id: video_name }],
+ }),
+
+ getVideoWorkflow: build.query<
+ paths['/api/v1/videos/i/{video_name}/workflow']['get']['responses']['200']['content']['application/json'],
+ string
+ >({
+ query: (video_name) => ({ url: buildVideosUrl(`i/${video_name}/workflow`) }),
+ providesTags: (result, error, video_name) => [{ type: 'VideoWorkflow', id: video_name }],
+ }),
+
+ getVideoNames: build.query({
+ query: (queryArgs) => ({
+ url: buildVideosUrl('names', queryArgs),
+ method: 'GET',
+ }),
+ providesTags: (result, error, queryArgs) => [
+ 'VideoNameList',
+ 'FetchOnReconnect',
+ { type: 'VideoNameList', id: stableHash(queryArgs) },
+ ],
+ }),
+
+ deleteVideo: build.mutation<
+ paths['/api/v1/videos/i/{video_name}']['delete']['responses']['200']['content']['application/json'],
+ paths['/api/v1/videos/i/{video_name}']['delete']['parameters']['path']
+ >({
+ query: ({ video_name }) => ({
+ url: buildVideosUrl(`i/${video_name}`),
+ method: 'DELETE',
+ }),
+ invalidatesTags: (result) => {
+ if (!result) {
+ return [];
+ }
+ // Per-video tags too, so the deleted video's DTO/metadata caches refetch and 404
+ // instead of serving a stale entry (e.g. to node inputs still referencing it).
+ return [
+ ...getTagsToInvalidateForVideoMutation(result.deleted_videos),
+ ...getTagsToInvalidateForBoardAffectingMutation(result.affected_boards),
+ { type: 'VideoList', id: LIST_TAG },
+ ];
+ },
+ }),
+
+ deleteVideos: build.mutation<
+ paths['/api/v1/videos/delete']['post']['responses']['200']['content']['application/json'],
+ paths['/api/v1/videos/delete']['post']['requestBody']['content']['application/json']
+ >({
+ query: (body) => ({
+ url: buildVideosUrl('delete'),
+ method: 'POST',
+ body,
+ }),
+ invalidatesTags: (result) => {
+ if (!result) {
+ return [];
+ }
+ // Only the server-confirmed deletions — videos that failed to delete keep their
+ // live cache entries.
+ return [
+ ...getTagsToInvalidateForVideoMutation(result.deleted_videos),
+ ...getTagsToInvalidateForBoardAffectingMutation(result.affected_boards),
+ { type: 'VideoList', id: LIST_TAG },
+ ];
+ },
+ }),
+
+ /** Companion to deleteUncategorizedImages: the "Delete All Uncategorized Images/Videos"
+ * board action fires both so the polymorphic uncategorized bucket is fully cleared. */
+ deleteUncategorizedVideos: build.mutation<
+ paths['/api/v1/videos/uncategorized']['delete']['responses']['200']['content']['application/json'],
+ void
+ >({
+ query: () => ({
+ url: buildVideosUrl('uncategorized'),
+ method: 'DELETE',
+ }),
+ invalidatesTags: (result) => {
+ if (!result) {
+ return [];
+ }
+ return [
+ ...getTagsToInvalidateForVideoMutation(result.deleted_videos),
+ ...getTagsToInvalidateForBoardAffectingMutation(result.affected_boards),
+ { type: 'VideoList', id: LIST_TAG },
+ ];
+ },
+ }),
+
+ /** Toggle a video's is_intermediate flag. */
+ changeVideoIsIntermediate: build.mutation<
+ paths['/api/v1/videos/i/{video_name}']['patch']['responses']['200']['content']['application/json'],
+ { video_name: string; is_intermediate: boolean }
+ >({
+ query: ({ video_name, is_intermediate }) => ({
+ url: buildVideosUrl(`i/${video_name}`),
+ method: 'PATCH',
+ body: { is_intermediate },
+ }),
+ invalidatesTags: (result) => {
+ if (!result) {
+ return [];
+ }
+ return [
+ ...getTagsToInvalidateForVideoMutation([result.video_name]),
+ ...getTagsToInvalidateForBoardAffectingMutation([result.board_id ?? 'none']),
+ // Board-scoped lists are covered by the Board tag above, but a listVideos
+ // query with board_id omitted (all boards) provides Board:'none' and would
+ // miss it — the LIST_TAG closes that gap for any future caller.
+ { type: 'VideoList', id: LIST_TAG },
+ ];
+ },
+ }),
+
+ starVideos: build.mutation<
+ paths['/api/v1/videos/star']['post']['responses']['200']['content']['application/json'],
+ paths['/api/v1/videos/star']['post']['requestBody']['content']['application/json']
+ >({
+ query: (body) => ({
+ url: buildVideosUrl('star'),
+ method: 'POST',
+ body,
+ }),
+ invalidatesTags: (result) => {
+ if (!result) {
+ return [];
+ }
+ // Starring reorders every video list (starred_first), but this mutation can't
+ // know which query-arg-specific lists are cached — invalidate the LIST_TAG-scoped
+ // tag that all listVideos queries provide.
+ return [
+ ...getTagsToInvalidateForVideoMutation(result.starred_videos),
+ ...getTagsToInvalidateForBoardAffectingMutation(result.affected_boards),
+ { type: 'VideoList', id: LIST_TAG },
+ ];
+ },
+ }),
+
+ unstarVideos: build.mutation<
+ paths['/api/v1/videos/unstar']['post']['responses']['200']['content']['application/json'],
+ paths['/api/v1/videos/unstar']['post']['requestBody']['content']['application/json']
+ >({
+ query: (body) => ({
+ url: buildVideosUrl('unstar'),
+ method: 'POST',
+ body,
+ }),
+ invalidatesTags: (result) => {
+ if (!result) {
+ return [];
+ }
+ return [
+ ...getTagsToInvalidateForVideoMutation(result.unstarred_videos),
+ ...getTagsToInvalidateForBoardAffectingMutation(result.affected_boards),
+ { type: 'VideoList', id: LIST_TAG },
+ ];
+ },
+ }),
+
+ uploadVideo: build.mutation<
+ paths['/api/v1/videos/upload']['post']['responses']['201']['content']['application/json'],
+ UploadVideoArg
+ >({
+ query: ({ file, video_category, is_intermediate, session_id, board_id, metadata }) => {
+ const formData = new FormData();
+ formData.append('file', file);
+ if (metadata) {
+ formData.append('metadata', JSON.stringify(metadata));
+ }
+ return {
+ url: buildVideosUrl('upload'),
+ method: 'POST',
+ body: formData,
+ params: {
+ video_category,
+ is_intermediate,
+ session_id,
+ board_id: board_id === 'none' ? undefined : board_id,
+ },
+ };
+ },
+ invalidatesTags: (result) => {
+ if (!result || result.is_intermediate) {
+ return [];
+ }
+ const boardId = result.board_id ?? 'none';
+ return [
+ ...getTagsToInvalidateForVideoMutation([result.video_name]),
+ ...getTagsToInvalidateForBoardAffectingMutation([boardId]),
+ { type: 'VideoList', id: LIST_TAG },
+ ];
+ },
+ }),
+
+ addVideoToBoard: build.mutation<
+ paths['/api/v1/videos/board']['post']['responses']['200']['content']['application/json'],
+ paths['/api/v1/videos/board']['post']['requestBody']['content']['application/json']
+ >({
+ query: (body) => ({
+ url: buildVideosUrl('board'),
+ method: 'POST',
+ body,
+ }),
+ invalidatesTags: (result) => {
+ if (!result) {
+ return [];
+ }
+ return [
+ ...getTagsToInvalidateForVideoMutation(result.added_videos),
+ ...getTagsToInvalidateForBoardAffectingMutation(result.affected_boards),
+ ];
+ },
+ }),
+
+ removeVideoFromBoard: build.mutation<
+ paths['/api/v1/videos/board']['delete']['responses']['200']['content']['application/json'],
+ paths['/api/v1/videos/board']['delete']['requestBody']['content']['application/json']
+ >({
+ query: (body) => ({
+ url: buildVideosUrl('board'),
+ method: 'DELETE',
+ body,
+ }),
+ invalidatesTags: (result) => {
+ if (!result) {
+ return [];
+ }
+ return [
+ ...getTagsToInvalidateForVideoMutation(result.removed_videos),
+ ...getTagsToInvalidateForBoardAffectingMutation(result.affected_boards),
+ ];
+ },
+ }),
+ }),
+});
+
+export const {
+ useUploadVideoMutation,
+ useGetVideoDTOQuery,
+ useStarVideosMutation,
+ useUnstarVideosMutation,
+ useAddVideoToBoardMutation,
+ useRemoveVideoFromBoardMutation,
+ useDeleteUncategorizedVideosMutation,
+} = videosApi;
+
+/** @knipignore Reserved for follow-up phases (bulk delete / intermediate toggle / video-only views).
+ * useDeleteVideoMutation is here because the only call site uses videosApi.endpoints.deleteVideo.initiate
+ * via the delete-video modal, but a future bulk/multi-select flow may want the React hook form. */
+export const {
+ useListVideosQuery,
+ useGetVideoMetadataQuery,
+ useGetVideoWorkflowQuery,
+ useLazyGetVideoWorkflowQuery,
+ useGetVideoNamesQuery,
+ useDeleteVideoMutation,
+ useDeleteVideosMutation,
+ useChangeVideoIsIntermediateMutation,
+} = videosApi;
+
+/**
+ * Imperative helper to fetch a VideoDTO. Mirrors `getImageDTOSafe`.
+ */
+export const getVideoDTOSafe = async (
+ video_name: string,
+ options?: Parameters[1]
+): Promise => {
+ const _options = { subscribe: false, ...options };
+ const req = getStore().dispatch(videosApi.endpoints.getVideoDTO.initiate(video_name, _options));
+ try {
+ return await req.unwrap();
+ } catch {
+ return null;
+ }
+};
+
+/** @knipignore Multi-phase rollout; consumed by Phase 5 viewer code. */
+export const getVideoDTO = (
+ video_name: string,
+ options?: Parameters[1]
+): Promise => {
+ const _options = { subscribe: false, ...options };
+ const req = getStore().dispatch(videosApi.endpoints.getVideoDTO.initiate(video_name, _options));
+ return req.unwrap();
+};
+
+/** @knipignore Multi-phase rollout; imperative form consumed by Phase 6 invocations. */
+export const uploadVideo = (arg: UploadVideoArg): Promise => {
+ const { dispatch } = getStore();
+ const req = dispatch(videosApi.endpoints.uploadVideo.initiate(arg, { track: false }));
+ return req.unwrap();
+};
+
+/**
+ * Uploads a batch of videos and resolves with the DTOs that succeeded.
+ *
+ * Rejections are NOT re-thrown, mirroring `uploadImages`: per-file failure feedback
+ * (an error toast naming the failed file) is handled by the `uploadVideo.matchRejected`
+ * listener in `videoUploaded.ts`, which fires for every rejected mutation regardless of
+ * how the caller aggregates the promises. Callers should treat the resolved array as
+ * "what actually made it" and must not assume it matches the request 1:1.
+ */
+export const uploadVideos = async (args: UploadVideoArg[]): Promise => {
+ const { dispatch } = getStore();
+ const results = await Promise.allSettled(
+ args.map((arg) => {
+ const req = dispatch(videosApi.endpoints.uploadVideo.initiate(arg, { track: false }));
+ return req.unwrap();
+ })
+ );
+ return results.filter((r): r is PromiseFulfilledResult => r.status === 'fulfilled').map((r) => r.value);
+};
+
+export const useVideoDTO = (videoName: string | null | undefined) => {
+ const { currentData: videoDTO } = useGetVideoDTOQuery(videoName ?? skipToken);
+ return videoDTO ?? null;
+};
diff --git a/invokeai/frontend/web/src/services/api/endpoints/virtual_boards.ts b/invokeai/frontend/web/src/services/api/endpoints/virtual_boards.ts
index b450bf84436..cb0e015547d 100644
--- a/invokeai/frontend/web/src/services/api/endpoints/virtual_boards.ts
+++ b/invokeai/frontend/web/src/services/api/endpoints/virtual_boards.ts
@@ -1,5 +1,5 @@
import queryString from 'query-string';
-import type { ImageCategory } from 'services/api/types';
+import type { GetGalleryItemNamesResult, ImageCategory } from 'services/api/types';
import type { ApiTagDescription } from '..';
import { api, buildV1Url } from '..';
@@ -10,13 +10,9 @@ export type VirtualSubBoard = {
date: string;
image_count: number;
asset_count: number;
+ video_count: number;
cover_image_name: string | null;
-};
-
-type ImageNamesResult = {
- image_names: string[];
- starred_count: number;
- total_count: number;
+ cover_video_name: string | null;
};
const buildVirtualBoardsUrl = (path: string = '') => buildV1Url(`virtual_boards/${path}`);
@@ -30,8 +26,12 @@ const virtualBoardsApi = api.injectEndpoints({
providesTags: (): ApiTagDescription[] => ['VirtualBoards', 'FetchOnReconnect'],
}),
- getVirtualBoardImageNamesByDate: build.query<
- ImageNamesResult,
+ /**
+ * Polymorphic (image + video) refs for a virtual date board. Same result shape as the
+ * gallery's `getGalleryItemNames`, so the gallery grid can consume either transparently.
+ */
+ getVirtualBoardItemNamesByDate: build.query<
+ GetGalleryItemNamesResult,
{
date: string;
starred_first?: boolean;
@@ -42,15 +42,19 @@ const virtualBoardsApi = api.injectEndpoints({
>({
query: ({ date, ...params }) => ({
url: buildVirtualBoardsUrl(
- `by_date/${date}/image_names?${queryString.stringify(params, { arrayFormat: 'none', skipNull: true, skipEmptyString: true })}`
+ `by_date/${date}/item_names?${queryString.stringify(params, { arrayFormat: 'none', skipNull: true, skipEmptyString: true })}`
),
}),
+ // Both image and video mutations must refetch a virtual date's contents, so this
+ // provides the name-list tag of each kind plus the polymorphic one.
providesTags: (_result, _error, arg): ApiTagDescription[] => [
{ type: 'ImageNameList', id: `virtual_${arg.date}` },
+ { type: 'VideoNameList', id: `virtual_${arg.date}` },
+ 'GalleryItemNameList',
'FetchOnReconnect',
],
}),
}),
});
-export const { useListVirtualBoardsByDateQuery, useGetVirtualBoardImageNamesByDateQuery } = virtualBoardsApi;
+export const { useListVirtualBoardsByDateQuery, useGetVirtualBoardItemNamesByDateQuery } = virtualBoardsApi;
diff --git a/invokeai/frontend/web/src/services/api/hooks/accessChecks.ts b/invokeai/frontend/web/src/services/api/hooks/accessChecks.ts
index 54d06ae9086..93336d0d08d 100644
--- a/invokeai/frontend/web/src/services/api/hooks/accessChecks.ts
+++ b/invokeai/frontend/web/src/services/api/hooks/accessChecks.ts
@@ -2,6 +2,7 @@ import { getStore } from 'app/store/nanostores/store';
import { boardsApi } from 'services/api/endpoints/boards';
import { imagesApi } from 'services/api/endpoints/images';
import { modelsApi } from 'services/api/endpoints/models';
+import { videosApi } from 'services/api/endpoints/videos';
/**
* Checks if the client has access to a model.
@@ -35,6 +36,22 @@ export const checkImageAccess = async (name: string): Promise => {
}
};
+/**
+ * Checks if the client has access to a video.
+ * @param name The video name.
+ * @returns A promise that resolves to true if the client has access, else false.
+ */
+export const checkVideoAccess = async (name: string): Promise => {
+ const { dispatch } = getStore();
+ try {
+ const req = dispatch(videosApi.endpoints.getVideoDTO.initiate(name, { forceRefetch: true, subscribe: false }));
+ const result = await req.unwrap();
+ return Boolean(result);
+ } catch {
+ return false;
+ }
+};
+
/**
* Checks if the client has access to a board.
* @param id The board id.
diff --git a/invokeai/frontend/web/src/services/api/hooks/modelsByType.ts b/invokeai/frontend/web/src/services/api/hooks/modelsByType.ts
index 07e89a305d4..8de39061518 100644
--- a/invokeai/frontend/web/src/services/api/hooks/modelsByType.ts
+++ b/invokeai/frontend/web/src/services/api/hooks/modelsByType.ts
@@ -38,6 +38,10 @@ import {
isTextLLMModelConfig,
isTIModelConfig,
isVAEModelConfigOrSubmodel,
+ isWanDiffusersMainModelConfig,
+ isWanGGUFLowNoiseMainModelConfig,
+ isWanT5EncoderModelConfig,
+ isWanVAEModelConfig,
isZImageDiffusersMainModelConfig,
} from 'services/api/types';
@@ -113,6 +117,10 @@ export const useQwenImageDiffusersModels = () => buildModelsHook(isQwenImageDiff
export const useQwenImageVAEModels = () => buildModelsHook(isQwenImageVAEModelConfig)();
export const useQwenVLEncoderModels = () => buildModelsHook(isQwenVLEncoderModelConfig)();
export const useQwen3EncoderModels = () => buildModelsHook(isQwen3EncoderModelConfig)();
+export const useWanDiffusersModels = () => buildModelsHook(isWanDiffusersMainModelConfig)();
+export const useWanGGUFLowNoiseModels = () => buildModelsHook(isWanGGUFLowNoiseMainModelConfig)();
+export const useWanVAEModels = () => buildModelsHook(isWanVAEModelConfig)();
+export const useWanT5EncoderModels = () => buildModelsHook(isWanT5EncoderModelConfig)();
export const useGlobalReferenceImageModels = buildModelsHook(
(config) => isIPAdapterModelConfig(config) || isFluxReduxModelConfig(config) || isFluxKontextModelConfig(config)
);
@@ -155,5 +163,8 @@ export const selectZImageDiffusersModels = buildModelsSelector(isZImageDiffusers
export const selectFlux2DiffusersModels = buildModelsSelector(isFlux2DiffusersMainModelConfig);
export const selectFluxVAEModels = buildModelsSelector(isFluxVAEModelConfig);
export const selectAnimaVAEModels = buildModelsSelector(isAnimaVAEModelConfig);
+export const selectWanDiffusersModels = buildModelsSelector(isWanDiffusersMainModelConfig);
+export const selectWanVAEModels = buildModelsSelector(isWanVAEModelConfig);
+export const selectWanT5EncoderModels = buildModelsSelector(isWanT5EncoderModelConfig);
export const useTextLLMModels = () => buildModelsHook(isTextLLMModelConfig)();
export const useLlavaModels = () => buildModelsHook(isLLaVAModelConfig)();
diff --git a/invokeai/frontend/web/src/services/api/index.ts b/invokeai/frontend/web/src/services/api/index.ts
index c02871bbbf3..e48e7bad8bb 100644
--- a/invokeai/frontend/web/src/services/api/index.ts
+++ b/invokeai/frontend/web/src/services/api/index.ts
@@ -8,7 +8,16 @@ import type {
} from '@reduxjs/toolkit/query/react';
import { buildCreateApi, coreModule, fetchBaseQuery, reactHooksModule } from '@reduxjs/toolkit/query/react';
import { getDeploymentBaseUrl } from 'common/util/baseUrl';
-import { sessionExpiredLogout } from 'features/auth/store/authSlice';
+import { sessionExpiredLogout, tokenRefreshed } from 'features/auth/store/authSlice';
+import {
+ beginAuthTransition,
+ captureAuthGeneration,
+ isTokenRefreshThrottled,
+ markTokenRefreshAccepted,
+ MEDIA_COOKIE_SYNC_TIMEOUT_MS,
+ runWithMediaAuthLock,
+ shouldAcceptRefreshedToken,
+} from 'features/auth/store/authTokenRefresh';
import queryString from 'query-string';
import stableHash from 'stable-hash';
@@ -64,6 +73,16 @@ const tagTypes = [
'UserList',
'CustomNodePacks',
'VirtualBoards',
+ // Video tags (parallel to Image tags).
+ 'Video',
+ 'VideoList',
+ 'VideoMetadata',
+ 'VideoWorkflow',
+ 'VideoNameList',
+ 'BoardVideosTotal',
+ // Polymorphic gallery list (images + videos interleaved by created_at).
+ 'GalleryItemList',
+ 'GalleryItemNameList',
] as const;
export type ApiTagDescription = TagDescription<(typeof tagTypes)[number]>;
export const LIST_TAG = 'LIST';
@@ -89,6 +108,13 @@ const dynamicBaseQuery: BaseQueryFn rawBaseQuery(args, api, extraOptions);
+ const result = changesMediaCookie ? await runWithMediaAuthLock(execute) : await execute();
// If we sent an auth token but got 401, the token is invalid/expired.
// Only trigger session expiry when we actually sent a token — unauthenticated
@@ -121,14 +148,63 @@ const dynamicBaseQuery: BaseQueryFn) => unknown
+): Promise => {
+ if (isTokenRefreshThrottled() || !shouldAcceptRefreshedToken(requestToken, requestGeneration)) {
+ return;
+ }
+ await runWithMediaAuthLock(async () => {
+ if (isTokenRefreshThrottled() || !shouldAcceptRefreshedToken(requestToken, requestGeneration)) {
+ return;
+ }
+ try {
+ const mediaCookieResponse = await fetch(`${getBaseUrl()}/api/v1/auth/media-cookie`, {
+ method: 'POST',
+ credentials: 'same-origin',
+ headers: { Authorization: `Bearer ${refreshedToken}` },
+ // Bound the time this exclusive cross-tab lock is held: login and logout
+ // serialize through the same lock, so a black-holed request here must not
+ // wedge auth transitions in every tab.
+ signal: AbortSignal.timeout(MEDIA_COOKIE_SYNC_TIMEOUT_MS),
+ });
+ if (mediaCookieResponse.status === 401 || mediaCookieResponse.status === 403) {
+ // The server rejected the refreshed token itself — don't commit it.
+ return;
+ }
+ } catch {
+ // Cookie sync is best-effort. The token commit below must not depend on it:
+ // tokens expire, cookies self-heal (useMediaCookieRefresh, plus the retry on
+ // the next refreshed-token header), so dropping the token on a 5xx or network
+ // failure would trade a transiently stale media cookie for a hard session
+ // expiry mid-activity.
+ }
+ if (shouldAcceptRefreshedToken(requestToken, requestGeneration)) {
+ markTokenRefreshAccepted();
+ dispatch(tokenRefreshed(refreshedToken));
+ }
+ });
+};
+
const createLruSelector = createSelectorCreator({
memoize: lruMemoize,
argsMemoize: lruMemoize,
diff --git a/invokeai/frontend/web/src/services/api/schema.ts b/invokeai/frontend/web/src/services/api/schema.ts
index 20241ff0da4..2206b9efd3e 100644
--- a/invokeai/frontend/web/src/services/api/schema.ts
+++ b/invokeai/frontend/web/src/services/api/schema.ts
@@ -85,6 +85,45 @@ export type paths = {
patch?: never;
trace?: never;
};
+ "/api/v1/auth/media-cookie": {
+ parameters: {
+ query?: never;
+ header?: never;
+ path?: never;
+ cookie?: never;
+ };
+ get?: never;
+ put?: never;
+ /**
+ * Refresh Media Cookie
+ * @description Re-issue the media cookie from a valid Bearer token.
+ *
+ * The media cookie is normally set at login, but a session can hold a valid JWT
+ * without it — the session may predate the cookie's introduction, or the cookie
+ * may have been cleared while the JWT (in client storage) survived. Media
+ * elements can't send Authorization headers, so such sessions silently fail to
+ * load videos (black player) while every other API call works. The frontend
+ * calls this on app load so an existing session self-heals without re-login.
+ *
+ * The cookie's lifetime is clamped to the presented token's remaining validity —
+ * it is the same JWT, so a longer-lived cookie would just yield 401s after
+ * expiry anyway.
+ *
+ * Returns:
+ * MediaCookieResponse indicating the cookie was set. In single-user mode the
+ * media routes don't require authentication, so this is a successful no-op.
+ *
+ * Raises:
+ * HTTPException: 401 if the Bearer token is missing, invalid, or expired, or
+ * the user no longer exists or is inactive (raised by the auth dependency).
+ */
+ post: operations["refresh_media_cookie_api_v1_auth_media_cookie_post"];
+ delete?: never;
+ options?: never;
+ head?: never;
+ patch?: never;
+ trace?: never;
+ };
"/api/v1/auth/me": {
parameters: {
query?: never;
@@ -1199,10 +1238,8 @@ export type paths = {
* Get Image Full
* @description Gets a full-resolution image file.
*
- * This endpoint is intentionally unauthenticated because browsers load images
- * via tags which cannot send Bearer tokens. Image names are UUIDs,
- * providing security through unguessability. Returns 409 while image storage
- * maintenance is active.
+ * Browser media requests authenticate with the path-scoped HttpOnly cookie set at login.
+ * Returns 409 while image storage maintenance is active.
*/
get: operations["get_image_full"];
put?: never;
@@ -1213,10 +1250,8 @@ export type paths = {
* Get Image Full
* @description Gets a full-resolution image file.
*
- * This endpoint is intentionally unauthenticated because browsers load images
- * via tags which cannot send Bearer tokens. Image names are UUIDs,
- * providing security through unguessability. Returns 409 while image storage
- * maintenance is active.
+ * Browser media requests authenticate with the path-scoped HttpOnly cookie set at login.
+ * Returns 409 while image storage maintenance is active.
*/
head: operations["get_image_full_head"];
patch?: never;
@@ -1233,10 +1268,8 @@ export type paths = {
* Get Image Thumbnail
* @description Gets a thumbnail image file.
*
- * This endpoint is intentionally unauthenticated because browsers load images
- * via tags which cannot send Bearer tokens. Image names are UUIDs,
- * providing security through unguessability. Returns 409 while image storage
- * maintenance is active.
+ * Browser media requests authenticate with the path-scoped HttpOnly cookie set at login.
+ * Returns 409 while image storage maintenance is active.
*/
get: operations["get_image_thumbnail"];
put?: never;
@@ -1418,6 +1451,319 @@ export type paths = {
patch?: never;
trace?: never;
};
+ "/api/v1/videos/upload": {
+ parameters: {
+ query?: never;
+ header?: never;
+ path?: never;
+ cookie?: never;
+ };
+ get?: never;
+ put?: never;
+ /**
+ * Upload Video
+ * @description Uploads a video for the current user.
+ */
+ post: operations["upload_video"];
+ delete?: never;
+ options?: never;
+ head?: never;
+ patch?: never;
+ trace?: never;
+ };
+ "/api/v1/videos/i/{video_name}": {
+ parameters: {
+ query?: never;
+ header?: never;
+ path?: never;
+ cookie?: never;
+ };
+ /** Get Video Dto */
+ get: operations["get_video_dto"];
+ put?: never;
+ post?: never;
+ /** Delete Video */
+ delete: operations["delete_video"];
+ options?: never;
+ head?: never;
+ /** Update Video */
+ patch: operations["update_video"];
+ trace?: never;
+ };
+ "/api/v1/videos/delete": {
+ parameters: {
+ query?: never;
+ header?: never;
+ path?: never;
+ cookie?: never;
+ };
+ get?: never;
+ put?: never;
+ /** Delete Videos From List */
+ post: operations["delete_videos_from_list"];
+ delete?: never;
+ options?: never;
+ head?: never;
+ patch?: never;
+ trace?: never;
+ };
+ "/api/v1/videos/uncategorized": {
+ parameters: {
+ query?: never;
+ header?: never;
+ path?: never;
+ cookie?: never;
+ };
+ get?: never;
+ put?: never;
+ post?: never;
+ /**
+ * Delete Uncategorized Videos
+ * @description Deletes all uncategorized videos owned by the current user (or all if admin).
+ *
+ * Mirrors ``delete_uncategorized_images`` so the "Delete All Uncategorized
+ * Images/Videos" board action covers both media kinds.
+ */
+ delete: operations["delete_uncategorized_videos"];
+ options?: never;
+ head?: never;
+ patch?: never;
+ trace?: never;
+ };
+ "/api/v1/videos/i/{video_name}/metadata": {
+ parameters: {
+ query?: never;
+ header?: never;
+ path?: never;
+ cookie?: never;
+ };
+ /** Get Video Metadata */
+ get: operations["get_video_metadata"];
+ put?: never;
+ post?: never;
+ delete?: never;
+ options?: never;
+ head?: never;
+ patch?: never;
+ trace?: never;
+ };
+ "/api/v1/videos/i/{video_name}/workflow": {
+ parameters: {
+ query?: never;
+ header?: never;
+ path?: never;
+ cookie?: never;
+ };
+ /**
+ * Get Video Workflow
+ * @description Gets the workflow and graph saved with a generated video (mirrors the image route).
+ */
+ get: operations["get_video_workflow"];
+ put?: never;
+ post?: never;
+ delete?: never;
+ options?: never;
+ head?: never;
+ patch?: never;
+ trace?: never;
+ };
+ "/api/v1/videos/i/{video_name}/full": {
+ parameters: {
+ query?: never;
+ header?: never;
+ path?: never;
+ cookie?: never;
+ };
+ /**
+ * Get Video Full
+ * @description Serves the video file with HTTP Range support so HTML5 seek/scrub works.
+ *
+ * Browser media requests authenticate with the path-scoped HttpOnly cookie set at login.
+ */
+ get: operations["get_video_full"];
+ put?: never;
+ post?: never;
+ delete?: never;
+ options?: never;
+ /**
+ * Get Video Full
+ * @description Serves the video file with HTTP Range support so HTML5 seek/scrub works.
+ *
+ * Browser media requests authenticate with the path-scoped HttpOnly cookie set at login.
+ */
+ head: operations["get_video_full_head"];
+ patch?: never;
+ trace?: never;
+ };
+ "/api/v1/videos/i/{video_name}/thumbnail": {
+ parameters: {
+ query?: never;
+ header?: never;
+ path?: never;
+ cookie?: never;
+ };
+ /**
+ * Get Video Thumbnail
+ * @description Returns the first-frame WebP thumbnail of an authorized video.
+ */
+ get: operations["get_video_thumbnail"];
+ put?: never;
+ post?: never;
+ delete?: never;
+ options?: never;
+ head?: never;
+ patch?: never;
+ trace?: never;
+ };
+ "/api/v1/videos/i/{video_name}/urls": {
+ parameters: {
+ query?: never;
+ header?: never;
+ path?: never;
+ cookie?: never;
+ };
+ /** Get Video Urls */
+ get: operations["get_video_urls"];
+ put?: never;
+ post?: never;
+ delete?: never;
+ options?: never;
+ head?: never;
+ patch?: never;
+ trace?: never;
+ };
+ "/api/v1/videos/": {
+ parameters: {
+ query?: never;
+ header?: never;
+ path?: never;
+ cookie?: never;
+ };
+ /**
+ * List Video Dtos
+ * @description Gets a list of video DTOs for the current user.
+ */
+ get: operations["list_video_dtos"];
+ put?: never;
+ post?: never;
+ delete?: never;
+ options?: never;
+ head?: never;
+ patch?: never;
+ trace?: never;
+ };
+ "/api/v1/videos/names": {
+ parameters: {
+ query?: never;
+ header?: never;
+ path?: never;
+ cookie?: never;
+ };
+ /**
+ * Get Video Names
+ * @description Gets ordered list of video names with metadata for optimistic updates.
+ */
+ get: operations["get_video_names"];
+ put?: never;
+ post?: never;
+ delete?: never;
+ options?: never;
+ head?: never;
+ patch?: never;
+ trace?: never;
+ };
+ "/api/v1/videos/star": {
+ parameters: {
+ query?: never;
+ header?: never;
+ path?: never;
+ cookie?: never;
+ };
+ get?: never;
+ put?: never;
+ /** Star Videos In List */
+ post: operations["star_videos_in_list"];
+ delete?: never;
+ options?: never;
+ head?: never;
+ patch?: never;
+ trace?: never;
+ };
+ "/api/v1/videos/unstar": {
+ parameters: {
+ query?: never;
+ header?: never;
+ path?: never;
+ cookie?: never;
+ };
+ get?: never;
+ put?: never;
+ /** Unstar Videos In List */
+ post: operations["unstar_videos_in_list"];
+ delete?: never;
+ options?: never;
+ head?: never;
+ patch?: never;
+ trace?: never;
+ };
+ "/api/v1/videos/board": {
+ parameters: {
+ query?: never;
+ header?: never;
+ path?: never;
+ cookie?: never;
+ };
+ get?: never;
+ put?: never;
+ /** Add Video To Board */
+ post: operations["add_video_to_board"];
+ /** Remove Video From Board */
+ delete: operations["remove_video_from_board"];
+ options?: never;
+ head?: never;
+ patch?: never;
+ trace?: never;
+ };
+ "/api/v1/gallery/items/": {
+ parameters: {
+ query?: never;
+ header?: never;
+ path?: never;
+ cookie?: never;
+ };
+ /**
+ * List Gallery Items
+ * @description Returns a paginated, time-sorted stream of polymorphic gallery items (images + videos).
+ */
+ get: operations["list_gallery_items"];
+ put?: never;
+ post?: never;
+ delete?: never;
+ options?: never;
+ head?: never;
+ patch?: never;
+ trace?: never;
+ };
+ "/api/v1/gallery/items/names": {
+ parameters: {
+ query?: never;
+ header?: never;
+ path?: never;
+ cookie?: never;
+ };
+ /**
+ * Get Gallery Item Names
+ * @description Returns an ordered (kind, name) list — used to drive virtualized gallery selection.
+ */
+ get: operations["get_gallery_item_names"];
+ put?: never;
+ post?: never;
+ delete?: never;
+ options?: never;
+ head?: never;
+ patch?: never;
+ trace?: never;
+ };
"/api/v1/boards/": {
parameters: {
query?: never;
@@ -1563,7 +1909,7 @@ export type paths = {
};
/**
* List Virtual Boards By Date
- * @description Gets a list of virtual sub-boards grouped by date.
+ * @description Gets a list of virtual sub-boards grouped by date. Covers both images and videos.
*/
get: operations["list_virtual_boards_by_date"];
put?: never;
@@ -1583,7 +1929,8 @@ export type paths = {
};
/**
* List Virtual Board Image Names By Date
- * @description Gets ordered image names for a specific date.
+ * @description Gets ordered image names for a specific date. Image-only; kept for API compatibility —
+ * the UI uses the polymorphic `/by_date/{date}/item_names` endpoint.
*/
get: operations["list_virtual_board_image_names_by_date"];
put?: never;
@@ -1594,6 +1941,26 @@ export type paths = {
patch?: never;
trace?: never;
};
+ "/api/v1/virtual_boards/by_date/{date}/item_names": {
+ parameters: {
+ query?: never;
+ header?: never;
+ path?: never;
+ cookie?: never;
+ };
+ /**
+ * List Virtual Board Item Names By Date
+ * @description Gets ordered polymorphic (image + video) item refs for a specific date.
+ */
+ get: operations["list_virtual_board_item_names_by_date"];
+ put?: never;
+ post?: never;
+ delete?: never;
+ options?: never;
+ head?: never;
+ patch?: never;
+ trace?: never;
+ };
"/api/v1/model_relationships/i/{model_key}": {
parameters: {
query?: never;
@@ -2916,6 +3283,19 @@ export type components = {
*/
type: "add";
};
+ /** AddVideosToBoardResult */
+ AddVideosToBoardResult: {
+ /**
+ * Affected Boards
+ * @description The ids of boards affected by the operation
+ */
+ affected_boards: string[];
+ /**
+ * Added Videos
+ * @description The video names that were added to the board
+ */
+ added_videos: string[];
+ };
/**
* AdminUserCreateRequest
* @description Request body for admin to create a new user.
@@ -3745,7 +4125,7 @@ export type components = {
*/
type: "anima_text_encoder";
};
- AnyModelConfig: components["schemas"]["Main_Diffusers_SD1_Config"] | components["schemas"]["Main_Diffusers_SD2_Config"] | components["schemas"]["Main_Diffusers_SDXL_Config"] | components["schemas"]["Main_Diffusers_SDXLRefiner_Config"] | components["schemas"]["Main_Diffusers_SD3_Config"] | components["schemas"]["Main_Diffusers_FLUX_Config"] | components["schemas"]["Main_Diffusers_Flux2_Config"] | components["schemas"]["Main_Diffusers_CogView4_Config"] | components["schemas"]["Main_Diffusers_QwenImage_Config"] | components["schemas"]["Main_Diffusers_ZImage_Config"] | components["schemas"]["Main_Checkpoint_SD1_Config"] | components["schemas"]["Main_Checkpoint_SD2_Config"] | components["schemas"]["Main_Checkpoint_SDXL_Config"] | components["schemas"]["Main_Checkpoint_SDXLRefiner_Config"] | components["schemas"]["Main_Checkpoint_Flux2_Config"] | components["schemas"]["Main_Checkpoint_FLUX_Config"] | components["schemas"]["Main_Checkpoint_QwenImage_Config"] | components["schemas"]["Main_Checkpoint_ZImage_Config"] | components["schemas"]["Main_Checkpoint_Anima_Config"] | components["schemas"]["Main_BnBNF4_FLUX_Config"] | components["schemas"]["Main_GGUF_Flux2_Config"] | components["schemas"]["Main_GGUF_FLUX_Config"] | components["schemas"]["Main_GGUF_QwenImage_Config"] | components["schemas"]["Main_GGUF_ZImage_Config"] | components["schemas"]["VAE_Checkpoint_SD1_Config"] | components["schemas"]["VAE_Checkpoint_SD2_Config"] | components["schemas"]["VAE_Checkpoint_SDXL_Config"] | components["schemas"]["VAE_Checkpoint_FLUX_Config"] | components["schemas"]["VAE_Checkpoint_Flux2_Config"] | components["schemas"]["VAE_Checkpoint_QwenImage_Config"] | components["schemas"]["VAE_Checkpoint_Anima_Config"] | components["schemas"]["VAE_Diffusers_SD1_Config"] | components["schemas"]["VAE_Diffusers_SDXL_Config"] | components["schemas"]["VAE_Diffusers_Flux2_Config"] | components["schemas"]["ControlNet_Checkpoint_SD1_Config"] | components["schemas"]["ControlNet_Checkpoint_SD2_Config"] | components["schemas"]["ControlNet_Checkpoint_SDXL_Config"] | components["schemas"]["ControlNet_Checkpoint_FLUX_Config"] | components["schemas"]["ControlNet_Checkpoint_ZImage_Config"] | components["schemas"]["ControlNet_Checkpoint_Anima_Config"] | components["schemas"]["ControlNet_Diffusers_SD1_Config"] | components["schemas"]["ControlNet_Diffusers_SD2_Config"] | components["schemas"]["ControlNet_Diffusers_SDXL_Config"] | components["schemas"]["ControlNet_Diffusers_FLUX_Config"] | components["schemas"]["LoRA_LyCORIS_SD1_Config"] | components["schemas"]["LoRA_LyCORIS_SD2_Config"] | components["schemas"]["LoRA_LyCORIS_SDXL_Config"] | components["schemas"]["LoRA_LyCORIS_Flux2_Config"] | components["schemas"]["LoRA_LyCORIS_FLUX_Config"] | components["schemas"]["LoRA_LyCORIS_ZImage_Config"] | components["schemas"]["LoRA_LyCORIS_QwenImage_Config"] | components["schemas"]["LoRA_LyCORIS_Anima_Config"] | components["schemas"]["LoRA_OMI_SDXL_Config"] | components["schemas"]["LoRA_OMI_FLUX_Config"] | components["schemas"]["LoRA_Diffusers_SD1_Config"] | components["schemas"]["LoRA_Diffusers_SD2_Config"] | components["schemas"]["LoRA_Diffusers_SDXL_Config"] | components["schemas"]["LoRA_Diffusers_Flux2_Config"] | components["schemas"]["LoRA_Diffusers_FLUX_Config"] | components["schemas"]["LoRA_Diffusers_ZImage_Config"] | components["schemas"]["ControlLoRA_LyCORIS_FLUX_Config"] | components["schemas"]["T5Encoder_T5Encoder_Config"] | components["schemas"]["T5Encoder_BnBLLMint8_Config"] | components["schemas"]["Qwen3Encoder_Qwen3Encoder_Config"] | components["schemas"]["Qwen3Encoder_Checkpoint_Config"] | components["schemas"]["Qwen3Encoder_GGUF_Config"] | components["schemas"]["QwenVLEncoder_Diffusers_Config"] | components["schemas"]["QwenVLEncoder_Checkpoint_Config"] | components["schemas"]["TI_File_SD1_Config"] | components["schemas"]["TI_File_SD2_Config"] | components["schemas"]["TI_File_SDXL_Config"] | components["schemas"]["TI_Folder_SD1_Config"] | components["schemas"]["TI_Folder_SD2_Config"] | components["schemas"]["TI_Folder_SDXL_Config"] | components["schemas"]["IPAdapter_InvokeAI_SD1_Config"] | components["schemas"]["IPAdapter_InvokeAI_SD2_Config"] | components["schemas"]["IPAdapter_InvokeAI_SDXL_Config"] | components["schemas"]["IPAdapter_Checkpoint_SD1_Config"] | components["schemas"]["IPAdapter_Checkpoint_SD2_Config"] | components["schemas"]["IPAdapter_Checkpoint_SDXL_Config"] | components["schemas"]["IPAdapter_Checkpoint_FLUX_Config"] | components["schemas"]["T2IAdapter_Diffusers_SD1_Config"] | components["schemas"]["T2IAdapter_Diffusers_SDXL_Config"] | components["schemas"]["Spandrel_Checkpoint_Config"] | components["schemas"]["CLIPEmbed_Diffusers_G_Config"] | components["schemas"]["CLIPEmbed_Diffusers_L_Config"] | components["schemas"]["CLIPVision_Diffusers_Config"] | components["schemas"]["SigLIP_Diffusers_Config"] | components["schemas"]["FLUXRedux_Checkpoint_Config"] | components["schemas"]["LlavaOnevision_Diffusers_Config"] | components["schemas"]["TextLLM_Diffusers_Config"] | components["schemas"]["ExternalApiModelConfig"] | components["schemas"]["Unknown_Config"];
+ AnyModelConfig: components["schemas"]["Main_Diffusers_SD1_Config"] | components["schemas"]["Main_Diffusers_SD2_Config"] | components["schemas"]["Main_Diffusers_SDXL_Config"] | components["schemas"]["Main_Diffusers_SDXLRefiner_Config"] | components["schemas"]["Main_Diffusers_SD3_Config"] | components["schemas"]["Main_Diffusers_FLUX_Config"] | components["schemas"]["Main_Diffusers_Flux2_Config"] | components["schemas"]["Main_Diffusers_CogView4_Config"] | components["schemas"]["Main_Diffusers_QwenImage_Config"] | components["schemas"]["Main_Diffusers_Wan_Config"] | components["schemas"]["Main_Diffusers_ZImage_Config"] | components["schemas"]["Main_Checkpoint_SD1_Config"] | components["schemas"]["Main_Checkpoint_SD2_Config"] | components["schemas"]["Main_Checkpoint_SDXL_Config"] | components["schemas"]["Main_Checkpoint_SDXLRefiner_Config"] | components["schemas"]["Main_Checkpoint_Flux2_Config"] | components["schemas"]["Main_Checkpoint_FLUX_Config"] | components["schemas"]["Main_Checkpoint_QwenImage_Config"] | components["schemas"]["Main_Checkpoint_ZImage_Config"] | components["schemas"]["Main_Checkpoint_Anima_Config"] | components["schemas"]["Main_BnBNF4_FLUX_Config"] | components["schemas"]["Main_GGUF_Flux2_Config"] | components["schemas"]["Main_GGUF_FLUX_Config"] | components["schemas"]["Main_GGUF_QwenImage_Config"] | components["schemas"]["Main_GGUF_Wan_Config"] | components["schemas"]["Main_GGUF_ZImage_Config"] | components["schemas"]["VAE_Checkpoint_SD1_Config"] | components["schemas"]["VAE_Checkpoint_SD2_Config"] | components["schemas"]["VAE_Checkpoint_SDXL_Config"] | components["schemas"]["VAE_Checkpoint_FLUX_Config"] | components["schemas"]["VAE_Checkpoint_Flux2_Config"] | components["schemas"]["VAE_Checkpoint_Wan_Config"] | components["schemas"]["VAE_Checkpoint_QwenImage_Config"] | components["schemas"]["VAE_Checkpoint_Anima_Config"] | components["schemas"]["VAE_Diffusers_SD1_Config"] | components["schemas"]["VAE_Diffusers_SDXL_Config"] | components["schemas"]["VAE_Diffusers_Flux2_Config"] | components["schemas"]["VAE_Diffusers_Wan_Config"] | components["schemas"]["ControlNet_Checkpoint_SD1_Config"] | components["schemas"]["ControlNet_Checkpoint_SD2_Config"] | components["schemas"]["ControlNet_Checkpoint_SDXL_Config"] | components["schemas"]["ControlNet_Checkpoint_FLUX_Config"] | components["schemas"]["ControlNet_Checkpoint_ZImage_Config"] | components["schemas"]["ControlNet_Checkpoint_Anima_Config"] | components["schemas"]["ControlNet_Diffusers_SD1_Config"] | components["schemas"]["ControlNet_Diffusers_SD2_Config"] | components["schemas"]["ControlNet_Diffusers_SDXL_Config"] | components["schemas"]["ControlNet_Diffusers_FLUX_Config"] | components["schemas"]["LoRA_LyCORIS_SD1_Config"] | components["schemas"]["LoRA_LyCORIS_SD2_Config"] | components["schemas"]["LoRA_LyCORIS_SDXL_Config"] | components["schemas"]["LoRA_LyCORIS_Flux2_Config"] | components["schemas"]["LoRA_LyCORIS_FLUX_Config"] | components["schemas"]["LoRA_LyCORIS_ZImage_Config"] | components["schemas"]["LoRA_LyCORIS_QwenImage_Config"] | components["schemas"]["LoRA_LyCORIS_Wan_Config"] | components["schemas"]["LoRA_LyCORIS_Anima_Config"] | components["schemas"]["LoRA_OMI_SDXL_Config"] | components["schemas"]["LoRA_OMI_FLUX_Config"] | components["schemas"]["LoRA_Diffusers_SD1_Config"] | components["schemas"]["LoRA_Diffusers_SD2_Config"] | components["schemas"]["LoRA_Diffusers_SDXL_Config"] | components["schemas"]["LoRA_Diffusers_Flux2_Config"] | components["schemas"]["LoRA_Diffusers_FLUX_Config"] | components["schemas"]["LoRA_Diffusers_ZImage_Config"] | components["schemas"]["ControlLoRA_LyCORIS_FLUX_Config"] | components["schemas"]["T5Encoder_T5Encoder_Config"] | components["schemas"]["T5Encoder_BnBLLMint8_Config"] | components["schemas"]["Qwen3Encoder_Qwen3Encoder_Config"] | components["schemas"]["Qwen3Encoder_Checkpoint_Config"] | components["schemas"]["Qwen3Encoder_GGUF_Config"] | components["schemas"]["QwenVLEncoder_Diffusers_Config"] | components["schemas"]["QwenVLEncoder_Checkpoint_Config"] | components["schemas"]["WanT5Encoder_WanT5Encoder_Config"] | components["schemas"]["TI_File_SD1_Config"] | components["schemas"]["TI_File_SD2_Config"] | components["schemas"]["TI_File_SDXL_Config"] | components["schemas"]["TI_Folder_SD1_Config"] | components["schemas"]["TI_Folder_SD2_Config"] | components["schemas"]["TI_Folder_SDXL_Config"] | components["schemas"]["IPAdapter_InvokeAI_SD1_Config"] | components["schemas"]["IPAdapter_InvokeAI_SD2_Config"] | components["schemas"]["IPAdapter_InvokeAI_SDXL_Config"] | components["schemas"]["IPAdapter_Checkpoint_SD1_Config"] | components["schemas"]["IPAdapter_Checkpoint_SD2_Config"] | components["schemas"]["IPAdapter_Checkpoint_SDXL_Config"] | components["schemas"]["IPAdapter_Checkpoint_FLUX_Config"] | components["schemas"]["T2IAdapter_Diffusers_SD1_Config"] | components["schemas"]["T2IAdapter_Diffusers_SDXL_Config"] | components["schemas"]["Spandrel_Checkpoint_Config"] | components["schemas"]["CLIPEmbed_Diffusers_G_Config"] | components["schemas"]["CLIPEmbed_Diffusers_L_Config"] | components["schemas"]["CLIPVision_Diffusers_Config"] | components["schemas"]["SigLIP_Diffusers_Config"] | components["schemas"]["FLUXRedux_Checkpoint_Config"] | components["schemas"]["LlavaOnevision_Diffusers_Config"] | components["schemas"]["TextLLM_Diffusers_Config"] | components["schemas"]["ExternalApiModelConfig"] | components["schemas"]["Unknown_Config"];
/**
* AppVersion
* @description App Version Response
@@ -3897,7 +4277,7 @@ export type components = {
* fallback/null value `BaseModelType.Any` for these models, instead of making the model base optional.
* @enum {string}
*/
- BaseModelType: "any" | "sd-1" | "sd-2" | "sd-3" | "sdxl" | "sdxl-refiner" | "flux" | "flux2" | "cogview4" | "z-image" | "external" | "qwen-image" | "anima" | "unknown";
+ BaseModelType: "any" | "sd-1" | "sd-2" | "sd-3" | "sdxl" | "sdxl-refiner" | "flux" | "flux2" | "cogview4" | "z-image" | "external" | "qwen-image" | "anima" | "wan" | "unknown";
/** Batch */
Batch: {
/**
@@ -3947,7 +4327,7 @@ export type components = {
* Items
* @description The list of items to substitute into the node/field.
*/
- items?: (string | number | components["schemas"]["ImageField"] | (string | number | components["schemas"]["ImageField"])[])[];
+ items?: (string | number | components["schemas"]["ImageField"] | components["schemas"]["VideoField"] | (string | number | components["schemas"]["ImageField"] | components["schemas"]["VideoField"])[])[];
};
/**
* BatchEnqueuedEvent
@@ -4244,11 +4624,22 @@ export type components = {
* @default private
*/
board_visibility?: components["schemas"]["BoardVisibility"];
+ /**
+ * Cover Video Name
+ * @description The name of the board's cover video, when the most recent item is a video.
+ */
+ cover_video_name?: string | null;
/**
* Image Count
* @description The number of images in the board.
*/
image_count: number;
+ /**
+ * Video Count
+ * @description The number of videos in the board.
+ * @default 0
+ */
+ video_count?: number;
/**
* Asset Count
* @description The number of assets in the board.
@@ -4361,6 +4752,14 @@ export type components = {
*/
image_names: string[];
};
+ /** Body_delete_videos_from_list */
+ Body_delete_videos_from_list: {
+ /**
+ * Video Names
+ * @description The list of names of videos to delete
+ */
+ video_names: string[];
+ };
/** Body_do_hf_login */
Body_do_hf_login: {
/**
@@ -4484,6 +4883,14 @@ export type components = {
*/
image_names: string[];
};
+ /** Body_remove_video_from_board */
+ Body_remove_video_from_board: {
+ /**
+ * Video Name
+ * @description The name of the video to remove from its board
+ */
+ video_name: string;
+ };
/** Body_set_workflow_thumbnail */
Body_set_workflow_thumbnail: {
/**
@@ -4501,6 +4908,14 @@ export type components = {
*/
image_names: string[];
};
+ /** Body_star_videos_in_list */
+ Body_star_videos_in_list: {
+ /**
+ * Video Names
+ * @description The list of names of videos to star
+ */
+ video_names: string[];
+ };
/** Body_unstar_images_in_list */
Body_unstar_images_in_list: {
/**
@@ -4509,6 +4924,14 @@ export type components = {
*/
image_names: string[];
};
+ /** Body_unstar_videos_in_list */
+ Body_unstar_videos_in_list: {
+ /**
+ * Video Names
+ * @description The list of names of videos to unstar
+ */
+ video_names: string[];
+ };
/** Body_update_model_image */
Body_update_model_image: {
/**
@@ -4562,6 +4985,19 @@ export type components = {
*/
metadata?: string | null;
};
+ /** Body_upload_video */
+ Body_upload_video: {
+ /**
+ * File
+ * Format: binary
+ */
+ file: Blob;
+ /**
+ * Metadata
+ * @description The metadata to associate with the video, must be a stringified JSON dict
+ */
+ metadata?: string | null;
+ };
/**
* Boolean Collection Primitive
* @description A collection of boolean primitive values
@@ -7883,7 +8319,7 @@ export type components = {
* @description The generation mode that output this image
* @default null
*/
- generation_mode?: ("txt2img" | "img2img" | "inpaint" | "outpaint" | "sdxl_txt2img" | "sdxl_img2img" | "sdxl_inpaint" | "sdxl_outpaint" | "flux_txt2img" | "flux_img2img" | "flux_inpaint" | "flux_outpaint" | "flux2_txt2img" | "flux2_img2img" | "flux2_inpaint" | "flux2_outpaint" | "sd3_txt2img" | "sd3_img2img" | "sd3_inpaint" | "sd3_outpaint" | "cogview4_txt2img" | "cogview4_img2img" | "cogview4_inpaint" | "cogview4_outpaint" | "z_image_txt2img" | "z_image_img2img" | "z_image_inpaint" | "z_image_outpaint" | "qwen_image_txt2img" | "qwen_image_img2img" | "qwen_image_inpaint" | "qwen_image_outpaint" | "anima_txt2img" | "anima_img2img" | "anima_inpaint" | "anima_outpaint") | null;
+ generation_mode?: ("txt2img" | "img2img" | "inpaint" | "outpaint" | "sdxl_txt2img" | "sdxl_img2img" | "sdxl_inpaint" | "sdxl_outpaint" | "flux_txt2img" | "flux_img2img" | "flux_inpaint" | "flux_outpaint" | "flux2_txt2img" | "flux2_img2img" | "flux2_inpaint" | "flux2_outpaint" | "sd3_txt2img" | "sd3_img2img" | "sd3_inpaint" | "sd3_outpaint" | "cogview4_txt2img" | "cogview4_img2img" | "cogview4_inpaint" | "cogview4_outpaint" | "z_image_txt2img" | "z_image_img2img" | "z_image_inpaint" | "z_image_outpaint" | "qwen_image_txt2img" | "qwen_image_img2img" | "qwen_image_inpaint" | "qwen_image_outpaint" | "anima_txt2img" | "anima_img2img" | "anima_inpaint" | "anima_outpaint" | "wan_txt2img" | "wan_img2img" | "wan_inpaint" | "wan_outpaint" | "wan_i2v") | null;
/**
* Positive Prompt
* @description The positive prompt parameter
@@ -8515,6 +8951,26 @@ export type components = {
* @description The names of the images that were deleted.
*/
deleted_images: string[];
+ /**
+ * Deleted Board Videos
+ * @description The video names of the board-videos relationships that were deleted.
+ */
+ deleted_board_videos?: string[];
+ /**
+ * Deleted Videos
+ * @description The names of the videos that were deleted.
+ */
+ deleted_videos?: string[];
+ /**
+ * Failed Images
+ * @description The names of images that could not be deleted and became uncategorized.
+ */
+ failed_images?: string[];
+ /**
+ * Failed Videos
+ * @description The names of videos that could not be deleted and became uncategorized.
+ */
+ failed_videos?: string[];
};
/**
* DeleteByDestinationResult
@@ -8539,6 +8995,11 @@ export type components = {
* @description The names of the images that were deleted
*/
deleted_images: string[];
+ /**
+ * Failed Images
+ * @description The names of authorized images that could not be deleted
+ */
+ failed_images?: string[];
};
/**
* DeleteOrphanedModelsRequest
@@ -8569,6 +9030,24 @@ export type components = {
[key: string]: string;
};
};
+ /** DeleteVideosResult */
+ DeleteVideosResult: {
+ /**
+ * Affected Boards
+ * @description The ids of boards affected by the operation
+ */
+ affected_boards: string[];
+ /**
+ * Deleted Videos
+ * @description The names of the videos that were deleted
+ */
+ deleted_videos: string[];
+ /**
+ * Failed Videos
+ * @description The names of videos that were not deleted
+ */
+ failed_videos: string[];
+ };
/**
* Denoise - SD1.5, SDXL
* @description Denoises noisy latents to decodable images
@@ -9724,6 +10203,132 @@ export type components = {
/** Height */
height: number;
};
+ /**
+ * Frame Range from Video
+ * @description Trim a video to a contiguous frame range and re-encode as MP4.
+ *
+ * Both bounds are inclusive and 0-based — ``start_frame=10, end_frame=50``
+ * emits 41 frames. Negative indices count from the end (``end_frame=-1``
+ * is the final frame), matching ``video_frame_extract``. The output frame
+ * rate defaults to the source video's frame rate; set ``fps=0`` to inherit
+ * it (or 16 fps if the source rate can't be probed).
+ *
+ * The resolved (positive) ``start_frame`` and ``end_frame`` are also emitted as
+ * outputs, so chained workflows can re-use the boundary indices — e.g. feeding
+ * them into a downstream Frame from Video to extract the same boundary frame.
+ */
+ ExtractVideoRangeInvocation: {
+ /**
+ * @description The board to save the image to
+ * @default null
+ */
+ board?: components["schemas"]["BoardField"] | null;
+ /**
+ * @description Optional metadata to be saved with the image
+ * @default null
+ */
+ metadata?: components["schemas"]["MetadataField"] | null;
+ /**
+ * Id
+ * @description The id of this instance of an invocation. Must be unique among all instances of invocations.
+ */
+ id: string;
+ /**
+ * Is Intermediate
+ * @description Whether or not this is an intermediate invocation.
+ * @default false
+ */
+ is_intermediate?: boolean;
+ /**
+ * Use Cache
+ * @description Whether or not to use the cache
+ * @default true
+ */
+ use_cache?: boolean;
+ /**
+ * @description The video to extract a frame range from.
+ * @default null
+ */
+ video?: components["schemas"]["VideoField"] | null;
+ /**
+ * Start Frame
+ * @description First frame to keep, inclusive. 0 = first frame. Negative indices count from the end.
+ * @default 0
+ */
+ start_frame?: number;
+ /**
+ * End Frame
+ * @description Last frame to keep, inclusive. -1 = last frame. Negative indices count from the end.
+ * @default -1
+ */
+ end_frame?: number;
+ /**
+ * Fps
+ * @description Output frame rate. 0 = match the source video's frame rate (falls back to 16 fps if the source rate can't be probed).
+ * @default 0
+ */
+ fps?: number;
+ /**
+ * type
+ * @default extract_video_range
+ * @constant
+ */
+ type: "extract_video_range";
+ };
+ /**
+ * ExtractVideoRangeOutput
+ * @description Output of ``extract_video_range``: a trimmed video plus the resolved frame indices.
+ *
+ * Mirrors ``VideoOutput`` so the video can be piped directly into Concatenate Videos or
+ * any other ``VideoField``-consuming node, and additionally exposes the resolved
+ * (positive, clamped) start and end indices so chained workflows can feed them back in
+ * — e.g. drive a downstream Frame from Video to pull the same boundary frame.
+ */
+ ExtractVideoRangeOutput: {
+ /** @description The trimmed video */
+ video: components["schemas"]["VideoField"];
+ /**
+ * Width
+ * @description The width of the video in pixels
+ */
+ width: number;
+ /**
+ * Height
+ * @description The height of the video in pixels
+ */
+ height: number;
+ /**
+ * Num Frames
+ * @description The number of frames in the trimmed video
+ */
+ num_frames: number;
+ /**
+ * Fps
+ * @description The frames-per-second of the trimmed video
+ */
+ fps: number;
+ /**
+ * Duration
+ * @description The duration of the trimmed video in seconds
+ */
+ duration: number;
+ /**
+ * Start Frame
+ * @description The resolved (positive, 0-based) start frame index in the source video
+ */
+ start_frame: number;
+ /**
+ * End Frame
+ * @description The resolved (positive, 0-based) end frame index in the source video
+ */
+ end_frame: number;
+ /**
+ * type
+ * @default extract_video_range_output
+ * @constant
+ */
+ type: "extract_video_range_output";
+ };
/**
* Apply LoRA Collection - FLUX
* @description Applies a collection of LoRAs to a FLUX transformer.
@@ -12392,6 +12997,113 @@ export type components = {
*/
type: "freeu";
};
+ /**
+ * GalleryItem
+ * @description A gallery item — either an image or a video, with shared fields and a discriminator.
+ *
+ * Frontend code should dispatch on `kind` to render image- vs video-specific UI.
+ */
+ GalleryItem: {
+ /** @description Whether the item is an image or video. */
+ kind: components["schemas"]["GalleryItemKind"];
+ /**
+ * Name
+ * @description The unique name of the image or video.
+ */
+ name: string;
+ /**
+ * Full Url
+ * @description URL to the full-resolution image PNG or the full-quality video MP4.
+ */
+ full_url: string;
+ /**
+ * Thumbnail Url
+ * @description URL to the static (WebP) thumbnail.
+ */
+ thumbnail_url: string;
+ /**
+ * Width
+ * @description The width of the item in pixels.
+ */
+ width: number;
+ /**
+ * Height
+ * @description The height of the item in pixels.
+ */
+ height: number;
+ /** @description The category of the item (images and videos share the same enum). */
+ category: components["schemas"]["ImageCategory"];
+ /**
+ * Starred
+ * @description Whether the item is starred.
+ */
+ starred: boolean;
+ /**
+ * Is Intermediate
+ * @description Whether the item is an intermediate output.
+ */
+ is_intermediate: boolean;
+ /**
+ * Board Id
+ * @description Owning board id, if any.
+ */
+ board_id?: string | null;
+ /**
+ * Created At
+ * @description The created timestamp of the item.
+ */
+ created_at: string;
+ /**
+ * Duration
+ * @description Video duration in seconds. None for images.
+ */
+ duration?: number | null;
+ /**
+ * Fps
+ * @description Video frames per second. None for images.
+ */
+ fps?: number | null;
+ };
+ /**
+ * GalleryItemKind
+ * @description Discriminator for polymorphic gallery items.
+ * @enum {string}
+ */
+ GalleryItemKind: "image" | "video";
+ /**
+ * GalleryItemNamesResult
+ * @description Ordered list of gallery item references plus counts for optimistic UI.
+ */
+ GalleryItemNamesResult: {
+ /**
+ * Items
+ * @description Ordered list of (kind, name) references.
+ */
+ items: components["schemas"]["GalleryItemRef"][];
+ /**
+ * Starred Count
+ * @description Number of starred items (when starred_first=True).
+ */
+ starred_count: number;
+ /**
+ * Total Count
+ * @description Total number of items matching the query.
+ */
+ total_count: number;
+ };
+ /**
+ * GalleryItemRef
+ * @description A thin reference to a gallery item — used for ordered name lists.
+ */
+ GalleryItemRef: {
+ /** @description Whether the item is an image or video. */
+ kind: components["schemas"]["GalleryItemKind"];
+ /**
+ * Name
+ * @description The unique name of the image or video.
+ */
+ name: string;
+ };
/**
* Gemini Image Generation
* @description Generate images using a Gemini-hosted external model.
@@ -12606,7 +13318,7 @@ export type components = {
* @description The nodes in this graph
*/
nodes?: {
- [key: string]: components["schemas"]["AddInvocation"] | components["schemas"]["AlibabaCloudImageGenerationInvocation"] | components["schemas"]["AlphaMaskToTensorInvocation"] | components["schemas"]["AnimaDenoiseInvocation"] | components["schemas"]["AnimaImageToLatentsInvocation"] | components["schemas"]["AnimaLLLiteInvocation"] | components["schemas"]["AnimaLatentsToImageInvocation"] | components["schemas"]["AnimaLoRACollectionLoader"] | components["schemas"]["AnimaLoRALoaderInvocation"] | components["schemas"]["AnimaModelLoaderInvocation"] | components["schemas"]["AnimaTextEncoderInvocation"] | components["schemas"]["ApplyMaskTensorToImageInvocation"] | components["schemas"]["ApplyMaskToImageInvocation"] | components["schemas"]["BlankImageInvocation"] | components["schemas"]["BlendLatentsInvocation"] | components["schemas"]["BooleanCollectionInvocation"] | components["schemas"]["BooleanInvocation"] | components["schemas"]["BoundingBoxInvocation"] | components["schemas"]["CLIPSkipInvocation"] | components["schemas"]["CV2InfillInvocation"] | components["schemas"]["CalculateImageTilesEvenSplitInvocation"] | components["schemas"]["CalculateImageTilesInvocation"] | components["schemas"]["CalculateImageTilesMinimumOverlapInvocation"] | components["schemas"]["CallSavedWorkflowInvocation"] | components["schemas"]["CannyEdgeDetectionInvocation"] | components["schemas"]["CanvasOutputInvocation"] | components["schemas"]["CanvasPasteBackInvocation"] | components["schemas"]["CanvasV2MaskAndCropInvocation"] | components["schemas"]["CenterPadCropInvocation"] | components["schemas"]["CogView4DenoiseInvocation"] | components["schemas"]["CogView4ImageToLatentsInvocation"] | components["schemas"]["CogView4LatentsToImageInvocation"] | components["schemas"]["CogView4ModelLoaderInvocation"] | components["schemas"]["CogView4TextEncoderInvocation"] | components["schemas"]["CollectInvocation"] | components["schemas"]["ColorCorrectInvocation"] | components["schemas"]["ColorInvocation"] | components["schemas"]["ColorMapInvocation"] | components["schemas"]["CompelInvocation"] | components["schemas"]["ConditioningCollectionInvocation"] | components["schemas"]["ConditioningInvocation"] | components["schemas"]["ContentShuffleInvocation"] | components["schemas"]["ControlNetInvocation"] | components["schemas"]["CoreMetadataInvocation"] | components["schemas"]["CreateDenoiseMaskInvocation"] | components["schemas"]["CreateGradientMaskInvocation"] | components["schemas"]["CropImageToBoundingBoxInvocation"] | components["schemas"]["CropLatentsCoreInvocation"] | components["schemas"]["CvInpaintInvocation"] | components["schemas"]["DWOpenposeDetectionInvocation"] | components["schemas"]["DecodeInvisibleWatermarkInvocation"] | components["schemas"]["DenoiseLatentsInvocation"] | components["schemas"]["DenoiseLatentsMetaInvocation"] | components["schemas"]["DepthAnythingDepthEstimationInvocation"] | components["schemas"]["DivideInvocation"] | components["schemas"]["DynamicPromptInvocation"] | components["schemas"]["ESRGANInvocation"] | components["schemas"]["ExpandMaskWithFadeInvocation"] | components["schemas"]["FLUXLoRACollectionLoader"] | components["schemas"]["FaceIdentifierInvocation"] | components["schemas"]["FaceMaskInvocation"] | components["schemas"]["FaceOffInvocation"] | components["schemas"]["FloatBatchInvocation"] | components["schemas"]["FloatCollectionInvocation"] | components["schemas"]["FloatGenerator"] | components["schemas"]["FloatInvocation"] | components["schemas"]["FloatLinearRangeInvocation"] | components["schemas"]["FloatMathInvocation"] | components["schemas"]["FloatToIntegerInvocation"] | components["schemas"]["Flux2DenoiseInvocation"] | components["schemas"]["Flux2KleinLoRACollectionLoader"] | components["schemas"]["Flux2KleinLoRALoaderInvocation"] | components["schemas"]["Flux2KleinModelLoaderInvocation"] | components["schemas"]["Flux2KleinTextEncoderInvocation"] | components["schemas"]["Flux2VaeDecodeInvocation"] | components["schemas"]["Flux2VaeEncodeInvocation"] | components["schemas"]["FluxControlLoRALoaderInvocation"] | components["schemas"]["FluxControlNetInvocation"] | components["schemas"]["FluxDenoiseInvocation"] | components["schemas"]["FluxDenoiseLatentsMetaInvocation"] | components["schemas"]["FluxFillInvocation"] | components["schemas"]["FluxIPAdapterInvocation"] | components["schemas"]["FluxKontextConcatenateImagesInvocation"] | components["schemas"]["FluxKontextInvocation"] | components["schemas"]["FluxLoRALoaderInvocation"] | components["schemas"]["FluxModelLoaderInvocation"] | components["schemas"]["FluxReduxInvocation"] | components["schemas"]["FluxTextEncoderInvocation"] | components["schemas"]["FluxVaeDecodeInvocation"] | components["schemas"]["FluxVaeEncodeInvocation"] | components["schemas"]["FreeUInvocation"] | components["schemas"]["GeminiImageGenerationInvocation"] | components["schemas"]["GetMaskBoundingBoxInvocation"] | components["schemas"]["GroundingDinoInvocation"] | components["schemas"]["HEDEdgeDetectionInvocation"] | components["schemas"]["HeuristicResizeInvocation"] | components["schemas"]["IPAdapterInvocation"] | components["schemas"]["IdealSizeInvocation"] | components["schemas"]["IfInvocation"] | components["schemas"]["ImageBatchInvocation"] | components["schemas"]["ImageBlurInvocation"] | components["schemas"]["ImageChannelInvocation"] | components["schemas"]["ImageChannelMultiplyInvocation"] | components["schemas"]["ImageChannelOffsetInvocation"] | components["schemas"]["ImageCollectionInvocation"] | components["schemas"]["ImageConvertInvocation"] | components["schemas"]["ImageCropInvocation"] | components["schemas"]["ImageGenerator"] | components["schemas"]["ImageHueAdjustmentInvocation"] | components["schemas"]["ImageInverseLerpInvocation"] | components["schemas"]["ImageInvocation"] | components["schemas"]["ImageLerpInvocation"] | components["schemas"]["ImageMaskToTensorInvocation"] | components["schemas"]["ImageMultiplyInvocation"] | components["schemas"]["ImageNSFWBlurInvocation"] | components["schemas"]["ImageNoiseInvocation"] | components["schemas"]["ImagePanelLayoutInvocation"] | components["schemas"]["ImagePasteInvocation"] | components["schemas"]["ImageResizeInvocation"] | components["schemas"]["ImageScaleInvocation"] | components["schemas"]["ImageToLatentsInvocation"] | components["schemas"]["ImageWatermarkInvocation"] | components["schemas"]["InfillColorInvocation"] | components["schemas"]["InfillPatchMatchInvocation"] | components["schemas"]["InfillTileInvocation"] | components["schemas"]["IntegerBatchInvocation"] | components["schemas"]["IntegerCollectionInvocation"] | components["schemas"]["IntegerGenerator"] | components["schemas"]["IntegerInvocation"] | components["schemas"]["IntegerMathInvocation"] | components["schemas"]["InvertTensorMaskInvocation"] | components["schemas"]["InvokeAdjustImageHuePlusInvocation"] | components["schemas"]["InvokeEquivalentAchromaticLightnessInvocation"] | components["schemas"]["InvokeImageBlendInvocation"] | components["schemas"]["InvokeImageCompositorInvocation"] | components["schemas"]["InvokeImageDilateOrErodeInvocation"] | components["schemas"]["InvokeImageEnhanceInvocation"] | components["schemas"]["InvokeImageValueThresholdsInvocation"] | components["schemas"]["IterateInvocation"] | components["schemas"]["LaMaInfillInvocation"] | components["schemas"]["LatentsCollectionInvocation"] | components["schemas"]["LatentsInvocation"] | components["schemas"]["LatentsToImageInvocation"] | components["schemas"]["LineartAnimeEdgeDetectionInvocation"] | components["schemas"]["LineartEdgeDetectionInvocation"] | components["schemas"]["LlavaOnevisionVllmInvocation"] | components["schemas"]["LoRACollectionLoader"] | components["schemas"]["LoRALoaderInvocation"] | components["schemas"]["LoRASelectorInvocation"] | components["schemas"]["MLSDDetectionInvocation"] | components["schemas"]["MainModelLoaderInvocation"] | components["schemas"]["MaskCombineInvocation"] | components["schemas"]["MaskEdgeInvocation"] | components["schemas"]["MaskFromAlphaInvocation"] | components["schemas"]["MaskFromIDInvocation"] | components["schemas"]["MaskTensorToImageInvocation"] | components["schemas"]["MediaPipeFaceDetectionInvocation"] | components["schemas"]["MergeMetadataInvocation"] | components["schemas"]["MergeTilesToImageInvocation"] | components["schemas"]["MetadataFieldExtractorInvocation"] | components["schemas"]["MetadataFromImageInvocation"] | components["schemas"]["MetadataInvocation"] | components["schemas"]["MetadataItemInvocation"] | components["schemas"]["MetadataItemLinkedInvocation"] | components["schemas"]["MetadataToBoolCollectionInvocation"] | components["schemas"]["MetadataToBoolInvocation"] | components["schemas"]["MetadataToControlnetsInvocation"] | components["schemas"]["MetadataToFloatCollectionInvocation"] | components["schemas"]["MetadataToFloatInvocation"] | components["schemas"]["MetadataToIPAdaptersInvocation"] | components["schemas"]["MetadataToIntegerCollectionInvocation"] | components["schemas"]["MetadataToIntegerInvocation"] | components["schemas"]["MetadataToLorasCollectionInvocation"] | components["schemas"]["MetadataToLorasInvocation"] | components["schemas"]["MetadataToModelInvocation"] | components["schemas"]["MetadataToSDXLLorasInvocation"] | components["schemas"]["MetadataToSDXLModelInvocation"] | components["schemas"]["MetadataToSchedulerInvocation"] | components["schemas"]["MetadataToStringCollectionInvocation"] | components["schemas"]["MetadataToStringInvocation"] | components["schemas"]["MetadataToT2IAdaptersInvocation"] | components["schemas"]["MetadataToVAEInvocation"] | components["schemas"]["ModelIdentifierInvocation"] | components["schemas"]["MultiplyInvocation"] | components["schemas"]["NoiseInvocation"] | components["schemas"]["NormalMapInvocation"] | components["schemas"]["OklabUnsharpMaskInvocation"] | components["schemas"]["OklchImageHueAdjustmentInvocation"] | components["schemas"]["OpenAIImageGenerationInvocation"] | components["schemas"]["PBRMapsInvocation"] | components["schemas"]["PairTileImageInvocation"] | components["schemas"]["PasteImageIntoBoundingBoxInvocation"] | components["schemas"]["PiDiNetEdgeDetectionInvocation"] | components["schemas"]["PromptTemplateInvocation"] | components["schemas"]["PromptsFromFileInvocation"] | components["schemas"]["QwenImageDenoiseInvocation"] | components["schemas"]["QwenImageImageToLatentsInvocation"] | components["schemas"]["QwenImageLatentsToImageInvocation"] | components["schemas"]["QwenImageLoRACollectionLoader"] | components["schemas"]["QwenImageLoRALoaderInvocation"] | components["schemas"]["QwenImageModelLoaderInvocation"] | components["schemas"]["QwenImageTextEncoderInvocation"] | components["schemas"]["RandomFloatInvocation"] | components["schemas"]["RandomIntInvocation"] | components["schemas"]["RandomRangeInvocation"] | components["schemas"]["RangeInvocation"] | components["schemas"]["RangeOfSizeInvocation"] | components["schemas"]["RectangleMaskInvocation"] | components["schemas"]["ResizeLatentsInvocation"] | components["schemas"]["RoundInvocation"] | components["schemas"]["SD3DenoiseInvocation"] | components["schemas"]["SD3ImageToLatentsInvocation"] | components["schemas"]["SD3LatentsToImageInvocation"] | components["schemas"]["SDXLCompelPromptInvocation"] | components["schemas"]["SDXLLoRACollectionLoader"] | components["schemas"]["SDXLLoRALoaderInvocation"] | components["schemas"]["SDXLModelLoaderInvocation"] | components["schemas"]["SDXLRefinerCompelPromptInvocation"] | components["schemas"]["SDXLRefinerModelLoaderInvocation"] | components["schemas"]["SaveImageInvocation"] | components["schemas"]["SaveImageToFileInvocation"] | components["schemas"]["ScaleLatentsInvocation"] | components["schemas"]["SchedulerInvocation"] | components["schemas"]["Sd3ModelLoaderInvocation"] | components["schemas"]["Sd3TextEncoderInvocation"] | components["schemas"]["SeamlessModeInvocation"] | components["schemas"]["SeedreamImageGenerationInvocation"] | components["schemas"]["SegmentAnythingInvocation"] | components["schemas"]["ShowImageInvocation"] | components["schemas"]["SpandrelImageToImageAutoscaleInvocation"] | components["schemas"]["SpandrelImageToImageInvocation"] | components["schemas"]["StringBatchInvocation"] | components["schemas"]["StringCollectionInvocation"] | components["schemas"]["StringGenerator"] | components["schemas"]["StringInvocation"] | components["schemas"]["StringJoinInvocation"] | components["schemas"]["StringJoinThreeInvocation"] | components["schemas"]["StringReplaceInvocation"] | components["schemas"]["StringSplitInvocation"] | components["schemas"]["StringSplitNegInvocation"] | components["schemas"]["SubtractInvocation"] | components["schemas"]["T2IAdapterInvocation"] | components["schemas"]["TextLLMInvocation"] | components["schemas"]["TileToPropertiesInvocation"] | components["schemas"]["TiledMultiDiffusionDenoiseLatents"] | components["schemas"]["UnsharpMaskInvocation"] | components["schemas"]["VAELoaderInvocation"] | components["schemas"]["WorkflowReturnGetInvocation"] | components["schemas"]["WorkflowReturnInvocation"] | components["schemas"]["WorkflowReturnValueInvocation"] | components["schemas"]["ZImageControlInvocation"] | components["schemas"]["ZImageDenoiseInvocation"] | components["schemas"]["ZImageDenoiseMetaInvocation"] | components["schemas"]["ZImageImageToLatentsInvocation"] | components["schemas"]["ZImageLatentsToImageInvocation"] | components["schemas"]["ZImageLoRACollectionLoader"] | components["schemas"]["ZImageLoRALoaderInvocation"] | components["schemas"]["ZImageModelLoaderInvocation"] | components["schemas"]["ZImageSeedVarianceEnhancerInvocation"] | components["schemas"]["ZImageTextEncoderInvocation"];
+ [key: string]: components["schemas"]["AddInvocation"] | components["schemas"]["AlibabaCloudImageGenerationInvocation"] | components["schemas"]["AlphaMaskToTensorInvocation"] | components["schemas"]["AnimaDenoiseInvocation"] | components["schemas"]["AnimaImageToLatentsInvocation"] | components["schemas"]["AnimaLLLiteInvocation"] | components["schemas"]["AnimaLatentsToImageInvocation"] | components["schemas"]["AnimaLoRACollectionLoader"] | components["schemas"]["AnimaLoRALoaderInvocation"] | components["schemas"]["AnimaModelLoaderInvocation"] | components["schemas"]["AnimaTextEncoderInvocation"] | components["schemas"]["ApplyMaskTensorToImageInvocation"] | components["schemas"]["ApplyMaskToImageInvocation"] | components["schemas"]["BlankImageInvocation"] | components["schemas"]["BlendLatentsInvocation"] | components["schemas"]["BooleanCollectionInvocation"] | components["schemas"]["BooleanInvocation"] | components["schemas"]["BoundingBoxInvocation"] | components["schemas"]["CLIPSkipInvocation"] | components["schemas"]["CV2InfillInvocation"] | components["schemas"]["CalculateImageTilesEvenSplitInvocation"] | components["schemas"]["CalculateImageTilesInvocation"] | components["schemas"]["CalculateImageTilesMinimumOverlapInvocation"] | components["schemas"]["CallSavedWorkflowInvocation"] | components["schemas"]["CannyEdgeDetectionInvocation"] | components["schemas"]["CanvasOutputInvocation"] | components["schemas"]["CanvasPasteBackInvocation"] | components["schemas"]["CanvasV2MaskAndCropInvocation"] | components["schemas"]["CenterPadCropInvocation"] | components["schemas"]["CogView4DenoiseInvocation"] | components["schemas"]["CogView4ImageToLatentsInvocation"] | components["schemas"]["CogView4LatentsToImageInvocation"] | components["schemas"]["CogView4ModelLoaderInvocation"] | components["schemas"]["CogView4TextEncoderInvocation"] | components["schemas"]["CollectInvocation"] | components["schemas"]["ColorCorrectInvocation"] | components["schemas"]["ColorInvocation"] | components["schemas"]["ColorMapInvocation"] | components["schemas"]["CompelInvocation"] | components["schemas"]["ConditioningCollectionInvocation"] | components["schemas"]["ConditioningInvocation"] | components["schemas"]["ContentShuffleInvocation"] | components["schemas"]["ControlNetInvocation"] | components["schemas"]["CoreMetadataInvocation"] | components["schemas"]["CreateDenoiseMaskInvocation"] | components["schemas"]["CreateGradientMaskInvocation"] | components["schemas"]["CropImageToBoundingBoxInvocation"] | components["schemas"]["CropLatentsCoreInvocation"] | components["schemas"]["CvInpaintInvocation"] | components["schemas"]["DWOpenposeDetectionInvocation"] | components["schemas"]["DecodeInvisibleWatermarkInvocation"] | components["schemas"]["DenoiseLatentsInvocation"] | components["schemas"]["DenoiseLatentsMetaInvocation"] | components["schemas"]["DepthAnythingDepthEstimationInvocation"] | components["schemas"]["DivideInvocation"] | components["schemas"]["DynamicPromptInvocation"] | components["schemas"]["ESRGANInvocation"] | components["schemas"]["ExpandMaskWithFadeInvocation"] | components["schemas"]["ExtractVideoRangeInvocation"] | components["schemas"]["FLUXLoRACollectionLoader"] | components["schemas"]["FaceIdentifierInvocation"] | components["schemas"]["FaceMaskInvocation"] | components["schemas"]["FaceOffInvocation"] | components["schemas"]["FloatBatchInvocation"] | components["schemas"]["FloatCollectionInvocation"] | components["schemas"]["FloatGenerator"] | components["schemas"]["FloatInvocation"] | components["schemas"]["FloatLinearRangeInvocation"] | components["schemas"]["FloatMathInvocation"] | components["schemas"]["FloatToIntegerInvocation"] | components["schemas"]["Flux2DenoiseInvocation"] | components["schemas"]["Flux2KleinLoRACollectionLoader"] | components["schemas"]["Flux2KleinLoRALoaderInvocation"] | components["schemas"]["Flux2KleinModelLoaderInvocation"] | components["schemas"]["Flux2KleinTextEncoderInvocation"] | components["schemas"]["Flux2VaeDecodeInvocation"] | components["schemas"]["Flux2VaeEncodeInvocation"] | components["schemas"]["FluxControlLoRALoaderInvocation"] | components["schemas"]["FluxControlNetInvocation"] | components["schemas"]["FluxDenoiseInvocation"] | components["schemas"]["FluxDenoiseLatentsMetaInvocation"] | components["schemas"]["FluxFillInvocation"] | components["schemas"]["FluxIPAdapterInvocation"] | components["schemas"]["FluxKontextConcatenateImagesInvocation"] | components["schemas"]["FluxKontextInvocation"] | components["schemas"]["FluxLoRALoaderInvocation"] | components["schemas"]["FluxModelLoaderInvocation"] | components["schemas"]["FluxReduxInvocation"] | components["schemas"]["FluxTextEncoderInvocation"] | components["schemas"]["FluxVaeDecodeInvocation"] | components["schemas"]["FluxVaeEncodeInvocation"] | components["schemas"]["FreeUInvocation"] | components["schemas"]["GeminiImageGenerationInvocation"] | components["schemas"]["GetMaskBoundingBoxInvocation"] | components["schemas"]["GroundingDinoInvocation"] | components["schemas"]["HEDEdgeDetectionInvocation"] | components["schemas"]["HeuristicResizeInvocation"] | components["schemas"]["IPAdapterInvocation"] | components["schemas"]["IdealSizeInvocation"] | components["schemas"]["IfInvocation"] | components["schemas"]["ImageBatchInvocation"] | components["schemas"]["ImageBlurInvocation"] | components["schemas"]["ImageChannelInvocation"] | components["schemas"]["ImageChannelMultiplyInvocation"] | components["schemas"]["ImageChannelOffsetInvocation"] | components["schemas"]["ImageCollectionInvocation"] | components["schemas"]["ImageConvertInvocation"] | components["schemas"]["ImageCropInvocation"] | components["schemas"]["ImageGenerator"] | components["schemas"]["ImageHueAdjustmentInvocation"] | components["schemas"]["ImageInverseLerpInvocation"] | components["schemas"]["ImageInvocation"] | components["schemas"]["ImageLerpInvocation"] | components["schemas"]["ImageMaskToTensorInvocation"] | components["schemas"]["ImageMultiplyInvocation"] | components["schemas"]["ImageNSFWBlurInvocation"] | components["schemas"]["ImageNoiseInvocation"] | components["schemas"]["ImagePanelLayoutInvocation"] | components["schemas"]["ImagePasteInvocation"] | components["schemas"]["ImageResizeInvocation"] | components["schemas"]["ImageScaleInvocation"] | components["schemas"]["ImageToLatentsInvocation"] | components["schemas"]["ImageWatermarkInvocation"] | components["schemas"]["InfillColorInvocation"] | components["schemas"]["InfillPatchMatchInvocation"] | components["schemas"]["InfillTileInvocation"] | components["schemas"]["IntegerBatchInvocation"] | components["schemas"]["IntegerCollectionInvocation"] | components["schemas"]["IntegerGenerator"] | components["schemas"]["IntegerInvocation"] | components["schemas"]["IntegerMathInvocation"] | components["schemas"]["InvertTensorMaskInvocation"] | components["schemas"]["InvokeAdjustImageHuePlusInvocation"] | components["schemas"]["InvokeEquivalentAchromaticLightnessInvocation"] | components["schemas"]["InvokeImageBlendInvocation"] | components["schemas"]["InvokeImageCompositorInvocation"] | components["schemas"]["InvokeImageDilateOrErodeInvocation"] | components["schemas"]["InvokeImageEnhanceInvocation"] | components["schemas"]["InvokeImageValueThresholdsInvocation"] | components["schemas"]["IterateInvocation"] | components["schemas"]["LaMaInfillInvocation"] | components["schemas"]["LatentsCollectionInvocation"] | components["schemas"]["LatentsInvocation"] | components["schemas"]["LatentsToImageInvocation"] | components["schemas"]["LineartAnimeEdgeDetectionInvocation"] | components["schemas"]["LineartEdgeDetectionInvocation"] | components["schemas"]["LlavaOnevisionVllmInvocation"] | components["schemas"]["LoRACollectionLoader"] | components["schemas"]["LoRALoaderInvocation"] | components["schemas"]["LoRASelectorInvocation"] | components["schemas"]["MLSDDetectionInvocation"] | components["schemas"]["MainModelLoaderInvocation"] | components["schemas"]["MaskCombineInvocation"] | components["schemas"]["MaskEdgeInvocation"] | components["schemas"]["MaskFromAlphaInvocation"] | components["schemas"]["MaskFromIDInvocation"] | components["schemas"]["MaskTensorToImageInvocation"] | components["schemas"]["MediaPipeFaceDetectionInvocation"] | components["schemas"]["MergeMetadataInvocation"] | components["schemas"]["MergeTilesToImageInvocation"] | components["schemas"]["MetadataFieldExtractorInvocation"] | components["schemas"]["MetadataFromImageInvocation"] | components["schemas"]["MetadataInvocation"] | components["schemas"]["MetadataItemInvocation"] | components["schemas"]["MetadataItemLinkedInvocation"] | components["schemas"]["MetadataToBoolCollectionInvocation"] | components["schemas"]["MetadataToBoolInvocation"] | components["schemas"]["MetadataToControlnetsInvocation"] | components["schemas"]["MetadataToFloatCollectionInvocation"] | components["schemas"]["MetadataToFloatInvocation"] | components["schemas"]["MetadataToIPAdaptersInvocation"] | components["schemas"]["MetadataToIntegerCollectionInvocation"] | components["schemas"]["MetadataToIntegerInvocation"] | components["schemas"]["MetadataToLorasCollectionInvocation"] | components["schemas"]["MetadataToLorasInvocation"] | components["schemas"]["MetadataToModelInvocation"] | components["schemas"]["MetadataToSDXLLorasInvocation"] | components["schemas"]["MetadataToSDXLModelInvocation"] | components["schemas"]["MetadataToSchedulerInvocation"] | components["schemas"]["MetadataToStringCollectionInvocation"] | components["schemas"]["MetadataToStringInvocation"] | components["schemas"]["MetadataToT2IAdaptersInvocation"] | components["schemas"]["MetadataToVAEInvocation"] | components["schemas"]["ModelIdentifierInvocation"] | components["schemas"]["MultiplyInvocation"] | components["schemas"]["NoiseInvocation"] | components["schemas"]["NormalMapInvocation"] | components["schemas"]["OklabUnsharpMaskInvocation"] | components["schemas"]["OklchImageHueAdjustmentInvocation"] | components["schemas"]["OpenAIImageGenerationInvocation"] | components["schemas"]["PBRMapsInvocation"] | components["schemas"]["PairTileImageInvocation"] | components["schemas"]["PasteImageIntoBoundingBoxInvocation"] | components["schemas"]["PiDiNetEdgeDetectionInvocation"] | components["schemas"]["PromptTemplateInvocation"] | components["schemas"]["PromptsFromFileInvocation"] | components["schemas"]["QwenImageDenoiseInvocation"] | components["schemas"]["QwenImageImageToLatentsInvocation"] | components["schemas"]["QwenImageLatentsToImageInvocation"] | components["schemas"]["QwenImageLoRACollectionLoader"] | components["schemas"]["QwenImageLoRALoaderInvocation"] | components["schemas"]["QwenImageModelLoaderInvocation"] | components["schemas"]["QwenImageTextEncoderInvocation"] | components["schemas"]["RandomFloatInvocation"] | components["schemas"]["RandomIntInvocation"] | components["schemas"]["RandomRangeInvocation"] | components["schemas"]["RangeInvocation"] | components["schemas"]["RangeOfSizeInvocation"] | components["schemas"]["RectangleMaskInvocation"] | components["schemas"]["ResizeLatentsInvocation"] | components["schemas"]["RoundInvocation"] | components["schemas"]["SD3DenoiseInvocation"] | components["schemas"]["SD3ImageToLatentsInvocation"] | components["schemas"]["SD3LatentsToImageInvocation"] | components["schemas"]["SDXLCompelPromptInvocation"] | components["schemas"]["SDXLLoRACollectionLoader"] | components["schemas"]["SDXLLoRALoaderInvocation"] | components["schemas"]["SDXLModelLoaderInvocation"] | components["schemas"]["SDXLRefinerCompelPromptInvocation"] | components["schemas"]["SDXLRefinerModelLoaderInvocation"] | components["schemas"]["SaveImageInvocation"] | components["schemas"]["SaveImageToFileInvocation"] | components["schemas"]["ScaleLatentsInvocation"] | components["schemas"]["SchedulerInvocation"] | components["schemas"]["Sd3ModelLoaderInvocation"] | components["schemas"]["Sd3TextEncoderInvocation"] | components["schemas"]["SeamlessModeInvocation"] | components["schemas"]["SeedreamImageGenerationInvocation"] | components["schemas"]["SegmentAnythingInvocation"] | components["schemas"]["ShowImageInvocation"] | components["schemas"]["SpandrelImageToImageAutoscaleInvocation"] | components["schemas"]["SpandrelImageToImageInvocation"] | components["schemas"]["StringBatchInvocation"] | components["schemas"]["StringCollectionInvocation"] | components["schemas"]["StringGenerator"] | components["schemas"]["StringInvocation"] | components["schemas"]["StringJoinInvocation"] | components["schemas"]["StringJoinThreeInvocation"] | components["schemas"]["StringReplaceInvocation"] | components["schemas"]["StringSplitInvocation"] | components["schemas"]["StringSplitNegInvocation"] | components["schemas"]["SubtractInvocation"] | components["schemas"]["T2IAdapterInvocation"] | components["schemas"]["TextLLMInvocation"] | components["schemas"]["TileToPropertiesInvocation"] | components["schemas"]["TiledMultiDiffusionDenoiseLatents"] | components["schemas"]["UnsharpMaskInvocation"] | components["schemas"]["VAELoaderInvocation"] | components["schemas"]["VideoConcatInvocation"] | components["schemas"]["VideoFrameExtractInvocation"] | components["schemas"]["VideoInvocation"] | components["schemas"]["WanDenoiseInvocation"] | components["schemas"]["WanI2VIdealDimensionsInvocation"] | components["schemas"]["WanImageToLatentsInvocation"] | components["schemas"]["WanLatentsToImageInvocation"] | components["schemas"]["WanLatentsToVideoInvocation"] | components["schemas"]["WanLoRACollectionLoader"] | components["schemas"]["WanLoRALoaderInvocation"] | components["schemas"]["WanModelLoaderInvocation"] | components["schemas"]["WanRefImageEncoderInvocation"] | components["schemas"]["WanTI2VIdealDimensionsInvocation"] | components["schemas"]["WanTextEncoderInvocation"] | components["schemas"]["WanVideoDenoiseInvocation"] | components["schemas"]["WorkflowReturnGetInvocation"] | components["schemas"]["WorkflowReturnInvocation"] | components["schemas"]["WorkflowReturnValueInvocation"] | components["schemas"]["ZImageControlInvocation"] | components["schemas"]["ZImageDenoiseInvocation"] | components["schemas"]["ZImageDenoiseMetaInvocation"] | components["schemas"]["ZImageImageToLatentsInvocation"] | components["schemas"]["ZImageLatentsToImageInvocation"] | components["schemas"]["ZImageLoRACollectionLoader"] | components["schemas"]["ZImageLoRALoaderInvocation"] | components["schemas"]["ZImageModelLoaderInvocation"] | components["schemas"]["ZImageSeedVarianceEnhancerInvocation"] | components["schemas"]["ZImageTextEncoderInvocation"];
};
/**
* Edges
@@ -12643,7 +13355,7 @@ export type components = {
* @description The results of node executions
*/
results: {
- [key: string]: components["schemas"]["AnimaConditioningOutput"] | components["schemas"]["AnimaLLLiteOutput"] | components["schemas"]["AnimaLoRALoaderOutput"] | components["schemas"]["AnimaModelLoaderOutput"] | components["schemas"]["BooleanCollectionOutput"] | components["schemas"]["BooleanOutput"] | components["schemas"]["BoundingBoxCollectionOutput"] | components["schemas"]["BoundingBoxOutput"] | components["schemas"]["CLIPOutput"] | components["schemas"]["CLIPSkipInvocationOutput"] | components["schemas"]["CalculateImageTilesOutput"] | components["schemas"]["CogView4ConditioningOutput"] | components["schemas"]["CogView4ModelLoaderOutput"] | components["schemas"]["CollectInvocationOutput"] | components["schemas"]["ColorCollectionOutput"] | components["schemas"]["ColorOutput"] | components["schemas"]["ConditioningCollectionOutput"] | components["schemas"]["ConditioningOutput"] | components["schemas"]["ControlOutput"] | components["schemas"]["DenoiseMaskOutput"] | components["schemas"]["FaceMaskOutput"] | components["schemas"]["FaceOffOutput"] | components["schemas"]["FloatCollectionOutput"] | components["schemas"]["FloatGeneratorOutput"] | components["schemas"]["FloatOutput"] | components["schemas"]["Flux2KleinLoRALoaderOutput"] | components["schemas"]["Flux2KleinModelLoaderOutput"] | components["schemas"]["FluxConditioningCollectionOutput"] | components["schemas"]["FluxConditioningOutput"] | components["schemas"]["FluxControlLoRALoaderOutput"] | components["schemas"]["FluxControlNetOutput"] | components["schemas"]["FluxFillOutput"] | components["schemas"]["FluxKontextOutput"] | components["schemas"]["FluxLoRALoaderOutput"] | components["schemas"]["FluxModelLoaderOutput"] | components["schemas"]["FluxReduxOutput"] | components["schemas"]["GradientMaskOutput"] | components["schemas"]["IPAdapterOutput"] | components["schemas"]["IdealSizeOutput"] | components["schemas"]["IfInvocationOutput"] | components["schemas"]["ImageCollectionOutput"] | components["schemas"]["ImageGeneratorOutput"] | components["schemas"]["ImageOutput"] | components["schemas"]["ImagePanelCoordinateOutput"] | components["schemas"]["IntegerCollectionOutput"] | components["schemas"]["IntegerGeneratorOutput"] | components["schemas"]["IntegerOutput"] | components["schemas"]["IterateInvocationOutput"] | components["schemas"]["LatentsCollectionOutput"] | components["schemas"]["LatentsMetaOutput"] | components["schemas"]["LatentsOutput"] | components["schemas"]["LoRALoaderOutput"] | components["schemas"]["LoRASelectorOutput"] | components["schemas"]["MDControlListOutput"] | components["schemas"]["MDIPAdapterListOutput"] | components["schemas"]["MDT2IAdapterListOutput"] | components["schemas"]["MaskOutput"] | components["schemas"]["MetadataItemOutput"] | components["schemas"]["MetadataOutput"] | components["schemas"]["MetadataToLorasCollectionOutput"] | components["schemas"]["MetadataToModelOutput"] | components["schemas"]["MetadataToSDXLModelOutput"] | components["schemas"]["ModelIdentifierOutput"] | components["schemas"]["ModelLoaderOutput"] | components["schemas"]["NoiseOutput"] | components["schemas"]["PBRMapsOutput"] | components["schemas"]["PairTileImageOutput"] | components["schemas"]["PromptTemplateOutput"] | components["schemas"]["QwenImageConditioningOutput"] | components["schemas"]["QwenImageLoRALoaderOutput"] | components["schemas"]["QwenImageModelLoaderOutput"] | components["schemas"]["SD3ConditioningOutput"] | components["schemas"]["SDXLLoRALoaderOutput"] | components["schemas"]["SDXLModelLoaderOutput"] | components["schemas"]["SDXLRefinerModelLoaderOutput"] | components["schemas"]["SchedulerOutput"] | components["schemas"]["Sd3ModelLoaderOutput"] | components["schemas"]["SeamlessModeOutput"] | components["schemas"]["String2Output"] | components["schemas"]["StringCollectionOutput"] | components["schemas"]["StringGeneratorOutput"] | components["schemas"]["StringOutput"] | components["schemas"]["StringPosNegOutput"] | components["schemas"]["T2IAdapterOutput"] | components["schemas"]["TileToPropertiesOutput"] | components["schemas"]["UNetOutput"] | components["schemas"]["VAEOutput"] | components["schemas"]["WorkflowReturnGetOutput"] | components["schemas"]["WorkflowReturnOutput"] | components["schemas"]["WorkflowReturnValueOutput"] | components["schemas"]["ZImageConditioningOutput"] | components["schemas"]["ZImageControlOutput"] | components["schemas"]["ZImageLoRALoaderOutput"] | components["schemas"]["ZImageModelLoaderOutput"];
+ [key: string]: components["schemas"]["AnimaConditioningOutput"] | components["schemas"]["AnimaLLLiteOutput"] | components["schemas"]["AnimaLoRALoaderOutput"] | components["schemas"]["AnimaModelLoaderOutput"] | components["schemas"]["BooleanCollectionOutput"] | components["schemas"]["BooleanOutput"] | components["schemas"]["BoundingBoxCollectionOutput"] | components["schemas"]["BoundingBoxOutput"] | components["schemas"]["CLIPOutput"] | components["schemas"]["CLIPSkipInvocationOutput"] | components["schemas"]["CalculateImageTilesOutput"] | components["schemas"]["CogView4ConditioningOutput"] | components["schemas"]["CogView4ModelLoaderOutput"] | components["schemas"]["CollectInvocationOutput"] | components["schemas"]["ColorCollectionOutput"] | components["schemas"]["ColorOutput"] | components["schemas"]["ConditioningCollectionOutput"] | components["schemas"]["ConditioningOutput"] | components["schemas"]["ControlOutput"] | components["schemas"]["DenoiseMaskOutput"] | components["schemas"]["ExtractVideoRangeOutput"] | components["schemas"]["FaceMaskOutput"] | components["schemas"]["FaceOffOutput"] | components["schemas"]["FloatCollectionOutput"] | components["schemas"]["FloatGeneratorOutput"] | components["schemas"]["FloatOutput"] | components["schemas"]["Flux2KleinLoRALoaderOutput"] | components["schemas"]["Flux2KleinModelLoaderOutput"] | components["schemas"]["FluxConditioningCollectionOutput"] | components["schemas"]["FluxConditioningOutput"] | components["schemas"]["FluxControlLoRALoaderOutput"] | components["schemas"]["FluxControlNetOutput"] | components["schemas"]["FluxFillOutput"] | components["schemas"]["FluxKontextOutput"] | components["schemas"]["FluxLoRALoaderOutput"] | components["schemas"]["FluxModelLoaderOutput"] | components["schemas"]["FluxReduxOutput"] | components["schemas"]["GradientMaskOutput"] | components["schemas"]["IPAdapterOutput"] | components["schemas"]["IdealSizeOutput"] | components["schemas"]["IfInvocationOutput"] | components["schemas"]["ImageCollectionOutput"] | components["schemas"]["ImageGeneratorOutput"] | components["schemas"]["ImageOutput"] | components["schemas"]["ImagePanelCoordinateOutput"] | components["schemas"]["IntegerCollectionOutput"] | components["schemas"]["IntegerGeneratorOutput"] | components["schemas"]["IntegerOutput"] | components["schemas"]["IterateInvocationOutput"] | components["schemas"]["LatentsCollectionOutput"] | components["schemas"]["LatentsMetaOutput"] | components["schemas"]["LatentsOutput"] | components["schemas"]["LoRALoaderOutput"] | components["schemas"]["LoRASelectorOutput"] | components["schemas"]["MDControlListOutput"] | components["schemas"]["MDIPAdapterListOutput"] | components["schemas"]["MDT2IAdapterListOutput"] | components["schemas"]["MaskOutput"] | components["schemas"]["MetadataItemOutput"] | components["schemas"]["MetadataOutput"] | components["schemas"]["MetadataToLorasCollectionOutput"] | components["schemas"]["MetadataToModelOutput"] | components["schemas"]["MetadataToSDXLModelOutput"] | components["schemas"]["ModelIdentifierOutput"] | components["schemas"]["ModelLoaderOutput"] | components["schemas"]["NoiseOutput"] | components["schemas"]["PBRMapsOutput"] | components["schemas"]["PairTileImageOutput"] | components["schemas"]["PromptTemplateOutput"] | components["schemas"]["QwenImageConditioningOutput"] | components["schemas"]["QwenImageLoRALoaderOutput"] | components["schemas"]["QwenImageModelLoaderOutput"] | components["schemas"]["SD3ConditioningOutput"] | components["schemas"]["SDXLLoRALoaderOutput"] | components["schemas"]["SDXLModelLoaderOutput"] | components["schemas"]["SDXLRefinerModelLoaderOutput"] | components["schemas"]["SchedulerOutput"] | components["schemas"]["Sd3ModelLoaderOutput"] | components["schemas"]["SeamlessModeOutput"] | components["schemas"]["String2Output"] | components["schemas"]["StringCollectionOutput"] | components["schemas"]["StringGeneratorOutput"] | components["schemas"]["StringOutput"] | components["schemas"]["StringPosNegOutput"] | components["schemas"]["T2IAdapterOutput"] | components["schemas"]["TileToPropertiesOutput"] | components["schemas"]["UNetOutput"] | components["schemas"]["VAEOutput"] | components["schemas"]["VideoOutput"] | components["schemas"]["WanConditioningOutput"] | components["schemas"]["WanLoRALoaderOutput"] | components["schemas"]["WanModelLoaderOutput"] | components["schemas"]["WanRefImageOutput"] | components["schemas"]["WorkflowReturnGetOutput"] | components["schemas"]["WorkflowReturnOutput"] | components["schemas"]["WorkflowReturnValueOutput"] | components["schemas"]["ZImageConditioningOutput"] | components["schemas"]["ZImageControlOutput"] | components["schemas"]["ZImageLoRALoaderOutput"] | components["schemas"]["ZImageModelLoaderOutput"];
};
/**
* Errors
@@ -16083,7 +16795,7 @@ export type components = {
* Invocation
* @description The ID of the invocation
*/
- invocation: components["schemas"]["AddInvocation"] | components["schemas"]["AlibabaCloudImageGenerationInvocation"] | components["schemas"]["AlphaMaskToTensorInvocation"] | components["schemas"]["AnimaDenoiseInvocation"] | components["schemas"]["AnimaImageToLatentsInvocation"] | components["schemas"]["AnimaLLLiteInvocation"] | components["schemas"]["AnimaLatentsToImageInvocation"] | components["schemas"]["AnimaLoRACollectionLoader"] | components["schemas"]["AnimaLoRALoaderInvocation"] | components["schemas"]["AnimaModelLoaderInvocation"] | components["schemas"]["AnimaTextEncoderInvocation"] | components["schemas"]["ApplyMaskTensorToImageInvocation"] | components["schemas"]["ApplyMaskToImageInvocation"] | components["schemas"]["BlankImageInvocation"] | components["schemas"]["BlendLatentsInvocation"] | components["schemas"]["BooleanCollectionInvocation"] | components["schemas"]["BooleanInvocation"] | components["schemas"]["BoundingBoxInvocation"] | components["schemas"]["CLIPSkipInvocation"] | components["schemas"]["CV2InfillInvocation"] | components["schemas"]["CalculateImageTilesEvenSplitInvocation"] | components["schemas"]["CalculateImageTilesInvocation"] | components["schemas"]["CalculateImageTilesMinimumOverlapInvocation"] | components["schemas"]["CallSavedWorkflowInvocation"] | components["schemas"]["CannyEdgeDetectionInvocation"] | components["schemas"]["CanvasOutputInvocation"] | components["schemas"]["CanvasPasteBackInvocation"] | components["schemas"]["CanvasV2MaskAndCropInvocation"] | components["schemas"]["CenterPadCropInvocation"] | components["schemas"]["CogView4DenoiseInvocation"] | components["schemas"]["CogView4ImageToLatentsInvocation"] | components["schemas"]["CogView4LatentsToImageInvocation"] | components["schemas"]["CogView4ModelLoaderInvocation"] | components["schemas"]["CogView4TextEncoderInvocation"] | components["schemas"]["CollectInvocation"] | components["schemas"]["ColorCorrectInvocation"] | components["schemas"]["ColorInvocation"] | components["schemas"]["ColorMapInvocation"] | components["schemas"]["CompelInvocation"] | components["schemas"]["ConditioningCollectionInvocation"] | components["schemas"]["ConditioningInvocation"] | components["schemas"]["ContentShuffleInvocation"] | components["schemas"]["ControlNetInvocation"] | components["schemas"]["CoreMetadataInvocation"] | components["schemas"]["CreateDenoiseMaskInvocation"] | components["schemas"]["CreateGradientMaskInvocation"] | components["schemas"]["CropImageToBoundingBoxInvocation"] | components["schemas"]["CropLatentsCoreInvocation"] | components["schemas"]["CvInpaintInvocation"] | components["schemas"]["DWOpenposeDetectionInvocation"] | components["schemas"]["DecodeInvisibleWatermarkInvocation"] | components["schemas"]["DenoiseLatentsInvocation"] | components["schemas"]["DenoiseLatentsMetaInvocation"] | components["schemas"]["DepthAnythingDepthEstimationInvocation"] | components["schemas"]["DivideInvocation"] | components["schemas"]["DynamicPromptInvocation"] | components["schemas"]["ESRGANInvocation"] | components["schemas"]["ExpandMaskWithFadeInvocation"] | components["schemas"]["FLUXLoRACollectionLoader"] | components["schemas"]["FaceIdentifierInvocation"] | components["schemas"]["FaceMaskInvocation"] | components["schemas"]["FaceOffInvocation"] | components["schemas"]["FloatBatchInvocation"] | components["schemas"]["FloatCollectionInvocation"] | components["schemas"]["FloatGenerator"] | components["schemas"]["FloatInvocation"] | components["schemas"]["FloatLinearRangeInvocation"] | components["schemas"]["FloatMathInvocation"] | components["schemas"]["FloatToIntegerInvocation"] | components["schemas"]["Flux2DenoiseInvocation"] | components["schemas"]["Flux2KleinLoRACollectionLoader"] | components["schemas"]["Flux2KleinLoRALoaderInvocation"] | components["schemas"]["Flux2KleinModelLoaderInvocation"] | components["schemas"]["Flux2KleinTextEncoderInvocation"] | components["schemas"]["Flux2VaeDecodeInvocation"] | components["schemas"]["Flux2VaeEncodeInvocation"] | components["schemas"]["FluxControlLoRALoaderInvocation"] | components["schemas"]["FluxControlNetInvocation"] | components["schemas"]["FluxDenoiseInvocation"] | components["schemas"]["FluxDenoiseLatentsMetaInvocation"] | components["schemas"]["FluxFillInvocation"] | components["schemas"]["FluxIPAdapterInvocation"] | components["schemas"]["FluxKontextConcatenateImagesInvocation"] | components["schemas"]["FluxKontextInvocation"] | components["schemas"]["FluxLoRALoaderInvocation"] | components["schemas"]["FluxModelLoaderInvocation"] | components["schemas"]["FluxReduxInvocation"] | components["schemas"]["FluxTextEncoderInvocation"] | components["schemas"]["FluxVaeDecodeInvocation"] | components["schemas"]["FluxVaeEncodeInvocation"] | components["schemas"]["FreeUInvocation"] | components["schemas"]["GeminiImageGenerationInvocation"] | components["schemas"]["GetMaskBoundingBoxInvocation"] | components["schemas"]["GroundingDinoInvocation"] | components["schemas"]["HEDEdgeDetectionInvocation"] | components["schemas"]["HeuristicResizeInvocation"] | components["schemas"]["IPAdapterInvocation"] | components["schemas"]["IdealSizeInvocation"] | components["schemas"]["IfInvocation"] | components["schemas"]["ImageBatchInvocation"] | components["schemas"]["ImageBlurInvocation"] | components["schemas"]["ImageChannelInvocation"] | components["schemas"]["ImageChannelMultiplyInvocation"] | components["schemas"]["ImageChannelOffsetInvocation"] | components["schemas"]["ImageCollectionInvocation"] | components["schemas"]["ImageConvertInvocation"] | components["schemas"]["ImageCropInvocation"] | components["schemas"]["ImageGenerator"] | components["schemas"]["ImageHueAdjustmentInvocation"] | components["schemas"]["ImageInverseLerpInvocation"] | components["schemas"]["ImageInvocation"] | components["schemas"]["ImageLerpInvocation"] | components["schemas"]["ImageMaskToTensorInvocation"] | components["schemas"]["ImageMultiplyInvocation"] | components["schemas"]["ImageNSFWBlurInvocation"] | components["schemas"]["ImageNoiseInvocation"] | components["schemas"]["ImagePanelLayoutInvocation"] | components["schemas"]["ImagePasteInvocation"] | components["schemas"]["ImageResizeInvocation"] | components["schemas"]["ImageScaleInvocation"] | components["schemas"]["ImageToLatentsInvocation"] | components["schemas"]["ImageWatermarkInvocation"] | components["schemas"]["InfillColorInvocation"] | components["schemas"]["InfillPatchMatchInvocation"] | components["schemas"]["InfillTileInvocation"] | components["schemas"]["IntegerBatchInvocation"] | components["schemas"]["IntegerCollectionInvocation"] | components["schemas"]["IntegerGenerator"] | components["schemas"]["IntegerInvocation"] | components["schemas"]["IntegerMathInvocation"] | components["schemas"]["InvertTensorMaskInvocation"] | components["schemas"]["InvokeAdjustImageHuePlusInvocation"] | components["schemas"]["InvokeEquivalentAchromaticLightnessInvocation"] | components["schemas"]["InvokeImageBlendInvocation"] | components["schemas"]["InvokeImageCompositorInvocation"] | components["schemas"]["InvokeImageDilateOrErodeInvocation"] | components["schemas"]["InvokeImageEnhanceInvocation"] | components["schemas"]["InvokeImageValueThresholdsInvocation"] | components["schemas"]["IterateInvocation"] | components["schemas"]["LaMaInfillInvocation"] | components["schemas"]["LatentsCollectionInvocation"] | components["schemas"]["LatentsInvocation"] | components["schemas"]["LatentsToImageInvocation"] | components["schemas"]["LineartAnimeEdgeDetectionInvocation"] | components["schemas"]["LineartEdgeDetectionInvocation"] | components["schemas"]["LlavaOnevisionVllmInvocation"] | components["schemas"]["LoRACollectionLoader"] | components["schemas"]["LoRALoaderInvocation"] | components["schemas"]["LoRASelectorInvocation"] | components["schemas"]["MLSDDetectionInvocation"] | components["schemas"]["MainModelLoaderInvocation"] | components["schemas"]["MaskCombineInvocation"] | components["schemas"]["MaskEdgeInvocation"] | components["schemas"]["MaskFromAlphaInvocation"] | components["schemas"]["MaskFromIDInvocation"] | components["schemas"]["MaskTensorToImageInvocation"] | components["schemas"]["MediaPipeFaceDetectionInvocation"] | components["schemas"]["MergeMetadataInvocation"] | components["schemas"]["MergeTilesToImageInvocation"] | components["schemas"]["MetadataFieldExtractorInvocation"] | components["schemas"]["MetadataFromImageInvocation"] | components["schemas"]["MetadataInvocation"] | components["schemas"]["MetadataItemInvocation"] | components["schemas"]["MetadataItemLinkedInvocation"] | components["schemas"]["MetadataToBoolCollectionInvocation"] | components["schemas"]["MetadataToBoolInvocation"] | components["schemas"]["MetadataToControlnetsInvocation"] | components["schemas"]["MetadataToFloatCollectionInvocation"] | components["schemas"]["MetadataToFloatInvocation"] | components["schemas"]["MetadataToIPAdaptersInvocation"] | components["schemas"]["MetadataToIntegerCollectionInvocation"] | components["schemas"]["MetadataToIntegerInvocation"] | components["schemas"]["MetadataToLorasCollectionInvocation"] | components["schemas"]["MetadataToLorasInvocation"] | components["schemas"]["MetadataToModelInvocation"] | components["schemas"]["MetadataToSDXLLorasInvocation"] | components["schemas"]["MetadataToSDXLModelInvocation"] | components["schemas"]["MetadataToSchedulerInvocation"] | components["schemas"]["MetadataToStringCollectionInvocation"] | components["schemas"]["MetadataToStringInvocation"] | components["schemas"]["MetadataToT2IAdaptersInvocation"] | components["schemas"]["MetadataToVAEInvocation"] | components["schemas"]["ModelIdentifierInvocation"] | components["schemas"]["MultiplyInvocation"] | components["schemas"]["NoiseInvocation"] | components["schemas"]["NormalMapInvocation"] | components["schemas"]["OklabUnsharpMaskInvocation"] | components["schemas"]["OklchImageHueAdjustmentInvocation"] | components["schemas"]["OpenAIImageGenerationInvocation"] | components["schemas"]["PBRMapsInvocation"] | components["schemas"]["PairTileImageInvocation"] | components["schemas"]["PasteImageIntoBoundingBoxInvocation"] | components["schemas"]["PiDiNetEdgeDetectionInvocation"] | components["schemas"]["PromptTemplateInvocation"] | components["schemas"]["PromptsFromFileInvocation"] | components["schemas"]["QwenImageDenoiseInvocation"] | components["schemas"]["QwenImageImageToLatentsInvocation"] | components["schemas"]["QwenImageLatentsToImageInvocation"] | components["schemas"]["QwenImageLoRACollectionLoader"] | components["schemas"]["QwenImageLoRALoaderInvocation"] | components["schemas"]["QwenImageModelLoaderInvocation"] | components["schemas"]["QwenImageTextEncoderInvocation"] | components["schemas"]["RandomFloatInvocation"] | components["schemas"]["RandomIntInvocation"] | components["schemas"]["RandomRangeInvocation"] | components["schemas"]["RangeInvocation"] | components["schemas"]["RangeOfSizeInvocation"] | components["schemas"]["RectangleMaskInvocation"] | components["schemas"]["ResizeLatentsInvocation"] | components["schemas"]["RoundInvocation"] | components["schemas"]["SD3DenoiseInvocation"] | components["schemas"]["SD3ImageToLatentsInvocation"] | components["schemas"]["SD3LatentsToImageInvocation"] | components["schemas"]["SDXLCompelPromptInvocation"] | components["schemas"]["SDXLLoRACollectionLoader"] | components["schemas"]["SDXLLoRALoaderInvocation"] | components["schemas"]["SDXLModelLoaderInvocation"] | components["schemas"]["SDXLRefinerCompelPromptInvocation"] | components["schemas"]["SDXLRefinerModelLoaderInvocation"] | components["schemas"]["SaveImageInvocation"] | components["schemas"]["SaveImageToFileInvocation"] | components["schemas"]["ScaleLatentsInvocation"] | components["schemas"]["SchedulerInvocation"] | components["schemas"]["Sd3ModelLoaderInvocation"] | components["schemas"]["Sd3TextEncoderInvocation"] | components["schemas"]["SeamlessModeInvocation"] | components["schemas"]["SeedreamImageGenerationInvocation"] | components["schemas"]["SegmentAnythingInvocation"] | components["schemas"]["ShowImageInvocation"] | components["schemas"]["SpandrelImageToImageAutoscaleInvocation"] | components["schemas"]["SpandrelImageToImageInvocation"] | components["schemas"]["StringBatchInvocation"] | components["schemas"]["StringCollectionInvocation"] | components["schemas"]["StringGenerator"] | components["schemas"]["StringInvocation"] | components["schemas"]["StringJoinInvocation"] | components["schemas"]["StringJoinThreeInvocation"] | components["schemas"]["StringReplaceInvocation"] | components["schemas"]["StringSplitInvocation"] | components["schemas"]["StringSplitNegInvocation"] | components["schemas"]["SubtractInvocation"] | components["schemas"]["T2IAdapterInvocation"] | components["schemas"]["TextLLMInvocation"] | components["schemas"]["TileToPropertiesInvocation"] | components["schemas"]["TiledMultiDiffusionDenoiseLatents"] | components["schemas"]["UnsharpMaskInvocation"] | components["schemas"]["VAELoaderInvocation"] | components["schemas"]["WorkflowReturnGetInvocation"] | components["schemas"]["WorkflowReturnInvocation"] | components["schemas"]["WorkflowReturnValueInvocation"] | components["schemas"]["ZImageControlInvocation"] | components["schemas"]["ZImageDenoiseInvocation"] | components["schemas"]["ZImageDenoiseMetaInvocation"] | components["schemas"]["ZImageImageToLatentsInvocation"] | components["schemas"]["ZImageLatentsToImageInvocation"] | components["schemas"]["ZImageLoRACollectionLoader"] | components["schemas"]["ZImageLoRALoaderInvocation"] | components["schemas"]["ZImageModelLoaderInvocation"] | components["schemas"]["ZImageSeedVarianceEnhancerInvocation"] | components["schemas"]["ZImageTextEncoderInvocation"];
+ invocation: components["schemas"]["AddInvocation"] | components["schemas"]["AlibabaCloudImageGenerationInvocation"] | components["schemas"]["AlphaMaskToTensorInvocation"] | components["schemas"]["AnimaDenoiseInvocation"] | components["schemas"]["AnimaImageToLatentsInvocation"] | components["schemas"]["AnimaLLLiteInvocation"] | components["schemas"]["AnimaLatentsToImageInvocation"] | components["schemas"]["AnimaLoRACollectionLoader"] | components["schemas"]["AnimaLoRALoaderInvocation"] | components["schemas"]["AnimaModelLoaderInvocation"] | components["schemas"]["AnimaTextEncoderInvocation"] | components["schemas"]["ApplyMaskTensorToImageInvocation"] | components["schemas"]["ApplyMaskToImageInvocation"] | components["schemas"]["BlankImageInvocation"] | components["schemas"]["BlendLatentsInvocation"] | components["schemas"]["BooleanCollectionInvocation"] | components["schemas"]["BooleanInvocation"] | components["schemas"]["BoundingBoxInvocation"] | components["schemas"]["CLIPSkipInvocation"] | components["schemas"]["CV2InfillInvocation"] | components["schemas"]["CalculateImageTilesEvenSplitInvocation"] | components["schemas"]["CalculateImageTilesInvocation"] | components["schemas"]["CalculateImageTilesMinimumOverlapInvocation"] | components["schemas"]["CallSavedWorkflowInvocation"] | components["schemas"]["CannyEdgeDetectionInvocation"] | components["schemas"]["CanvasOutputInvocation"] | components["schemas"]["CanvasPasteBackInvocation"] | components["schemas"]["CanvasV2MaskAndCropInvocation"] | components["schemas"]["CenterPadCropInvocation"] | components["schemas"]["CogView4DenoiseInvocation"] | components["schemas"]["CogView4ImageToLatentsInvocation"] | components["schemas"]["CogView4LatentsToImageInvocation"] | components["schemas"]["CogView4ModelLoaderInvocation"] | components["schemas"]["CogView4TextEncoderInvocation"] | components["schemas"]["CollectInvocation"] | components["schemas"]["ColorCorrectInvocation"] | components["schemas"]["ColorInvocation"] | components["schemas"]["ColorMapInvocation"] | components["schemas"]["CompelInvocation"] | components["schemas"]["ConditioningCollectionInvocation"] | components["schemas"]["ConditioningInvocation"] | components["schemas"]["ContentShuffleInvocation"] | components["schemas"]["ControlNetInvocation"] | components["schemas"]["CoreMetadataInvocation"] | components["schemas"]["CreateDenoiseMaskInvocation"] | components["schemas"]["CreateGradientMaskInvocation"] | components["schemas"]["CropImageToBoundingBoxInvocation"] | components["schemas"]["CropLatentsCoreInvocation"] | components["schemas"]["CvInpaintInvocation"] | components["schemas"]["DWOpenposeDetectionInvocation"] | components["schemas"]["DecodeInvisibleWatermarkInvocation"] | components["schemas"]["DenoiseLatentsInvocation"] | components["schemas"]["DenoiseLatentsMetaInvocation"] | components["schemas"]["DepthAnythingDepthEstimationInvocation"] | components["schemas"]["DivideInvocation"] | components["schemas"]["DynamicPromptInvocation"] | components["schemas"]["ESRGANInvocation"] | components["schemas"]["ExpandMaskWithFadeInvocation"] | components["schemas"]["ExtractVideoRangeInvocation"] | components["schemas"]["FLUXLoRACollectionLoader"] | components["schemas"]["FaceIdentifierInvocation"] | components["schemas"]["FaceMaskInvocation"] | components["schemas"]["FaceOffInvocation"] | components["schemas"]["FloatBatchInvocation"] | components["schemas"]["FloatCollectionInvocation"] | components["schemas"]["FloatGenerator"] | components["schemas"]["FloatInvocation"] | components["schemas"]["FloatLinearRangeInvocation"] | components["schemas"]["FloatMathInvocation"] | components["schemas"]["FloatToIntegerInvocation"] | components["schemas"]["Flux2DenoiseInvocation"] | components["schemas"]["Flux2KleinLoRACollectionLoader"] | components["schemas"]["Flux2KleinLoRALoaderInvocation"] | components["schemas"]["Flux2KleinModelLoaderInvocation"] | components["schemas"]["Flux2KleinTextEncoderInvocation"] | components["schemas"]["Flux2VaeDecodeInvocation"] | components["schemas"]["Flux2VaeEncodeInvocation"] | components["schemas"]["FluxControlLoRALoaderInvocation"] | components["schemas"]["FluxControlNetInvocation"] | components["schemas"]["FluxDenoiseInvocation"] | components["schemas"]["FluxDenoiseLatentsMetaInvocation"] | components["schemas"]["FluxFillInvocation"] | components["schemas"]["FluxIPAdapterInvocation"] | components["schemas"]["FluxKontextConcatenateImagesInvocation"] | components["schemas"]["FluxKontextInvocation"] | components["schemas"]["FluxLoRALoaderInvocation"] | components["schemas"]["FluxModelLoaderInvocation"] | components["schemas"]["FluxReduxInvocation"] | components["schemas"]["FluxTextEncoderInvocation"] | components["schemas"]["FluxVaeDecodeInvocation"] | components["schemas"]["FluxVaeEncodeInvocation"] | components["schemas"]["FreeUInvocation"] | components["schemas"]["GeminiImageGenerationInvocation"] | components["schemas"]["GetMaskBoundingBoxInvocation"] | components["schemas"]["GroundingDinoInvocation"] | components["schemas"]["HEDEdgeDetectionInvocation"] | components["schemas"]["HeuristicResizeInvocation"] | components["schemas"]["IPAdapterInvocation"] | components["schemas"]["IdealSizeInvocation"] | components["schemas"]["IfInvocation"] | components["schemas"]["ImageBatchInvocation"] | components["schemas"]["ImageBlurInvocation"] | components["schemas"]["ImageChannelInvocation"] | components["schemas"]["ImageChannelMultiplyInvocation"] | components["schemas"]["ImageChannelOffsetInvocation"] | components["schemas"]["ImageCollectionInvocation"] | components["schemas"]["ImageConvertInvocation"] | components["schemas"]["ImageCropInvocation"] | components["schemas"]["ImageGenerator"] | components["schemas"]["ImageHueAdjustmentInvocation"] | components["schemas"]["ImageInverseLerpInvocation"] | components["schemas"]["ImageInvocation"] | components["schemas"]["ImageLerpInvocation"] | components["schemas"]["ImageMaskToTensorInvocation"] | components["schemas"]["ImageMultiplyInvocation"] | components["schemas"]["ImageNSFWBlurInvocation"] | components["schemas"]["ImageNoiseInvocation"] | components["schemas"]["ImagePanelLayoutInvocation"] | components["schemas"]["ImagePasteInvocation"] | components["schemas"]["ImageResizeInvocation"] | components["schemas"]["ImageScaleInvocation"] | components["schemas"]["ImageToLatentsInvocation"] | components["schemas"]["ImageWatermarkInvocation"] | components["schemas"]["InfillColorInvocation"] | components["schemas"]["InfillPatchMatchInvocation"] | components["schemas"]["InfillTileInvocation"] | components["schemas"]["IntegerBatchInvocation"] | components["schemas"]["IntegerCollectionInvocation"] | components["schemas"]["IntegerGenerator"] | components["schemas"]["IntegerInvocation"] | components["schemas"]["IntegerMathInvocation"] | components["schemas"]["InvertTensorMaskInvocation"] | components["schemas"]["InvokeAdjustImageHuePlusInvocation"] | components["schemas"]["InvokeEquivalentAchromaticLightnessInvocation"] | components["schemas"]["InvokeImageBlendInvocation"] | components["schemas"]["InvokeImageCompositorInvocation"] | components["schemas"]["InvokeImageDilateOrErodeInvocation"] | components["schemas"]["InvokeImageEnhanceInvocation"] | components["schemas"]["InvokeImageValueThresholdsInvocation"] | components["schemas"]["IterateInvocation"] | components["schemas"]["LaMaInfillInvocation"] | components["schemas"]["LatentsCollectionInvocation"] | components["schemas"]["LatentsInvocation"] | components["schemas"]["LatentsToImageInvocation"] | components["schemas"]["LineartAnimeEdgeDetectionInvocation"] | components["schemas"]["LineartEdgeDetectionInvocation"] | components["schemas"]["LlavaOnevisionVllmInvocation"] | components["schemas"]["LoRACollectionLoader"] | components["schemas"]["LoRALoaderInvocation"] | components["schemas"]["LoRASelectorInvocation"] | components["schemas"]["MLSDDetectionInvocation"] | components["schemas"]["MainModelLoaderInvocation"] | components["schemas"]["MaskCombineInvocation"] | components["schemas"]["MaskEdgeInvocation"] | components["schemas"]["MaskFromAlphaInvocation"] | components["schemas"]["MaskFromIDInvocation"] | components["schemas"]["MaskTensorToImageInvocation"] | components["schemas"]["MediaPipeFaceDetectionInvocation"] | components["schemas"]["MergeMetadataInvocation"] | components["schemas"]["MergeTilesToImageInvocation"] | components["schemas"]["MetadataFieldExtractorInvocation"] | components["schemas"]["MetadataFromImageInvocation"] | components["schemas"]["MetadataInvocation"] | components["schemas"]["MetadataItemInvocation"] | components["schemas"]["MetadataItemLinkedInvocation"] | components["schemas"]["MetadataToBoolCollectionInvocation"] | components["schemas"]["MetadataToBoolInvocation"] | components["schemas"]["MetadataToControlnetsInvocation"] | components["schemas"]["MetadataToFloatCollectionInvocation"] | components["schemas"]["MetadataToFloatInvocation"] | components["schemas"]["MetadataToIPAdaptersInvocation"] | components["schemas"]["MetadataToIntegerCollectionInvocation"] | components["schemas"]["MetadataToIntegerInvocation"] | components["schemas"]["MetadataToLorasCollectionInvocation"] | components["schemas"]["MetadataToLorasInvocation"] | components["schemas"]["MetadataToModelInvocation"] | components["schemas"]["MetadataToSDXLLorasInvocation"] | components["schemas"]["MetadataToSDXLModelInvocation"] | components["schemas"]["MetadataToSchedulerInvocation"] | components["schemas"]["MetadataToStringCollectionInvocation"] | components["schemas"]["MetadataToStringInvocation"] | components["schemas"]["MetadataToT2IAdaptersInvocation"] | components["schemas"]["MetadataToVAEInvocation"] | components["schemas"]["ModelIdentifierInvocation"] | components["schemas"]["MultiplyInvocation"] | components["schemas"]["NoiseInvocation"] | components["schemas"]["NormalMapInvocation"] | components["schemas"]["OklabUnsharpMaskInvocation"] | components["schemas"]["OklchImageHueAdjustmentInvocation"] | components["schemas"]["OpenAIImageGenerationInvocation"] | components["schemas"]["PBRMapsInvocation"] | components["schemas"]["PairTileImageInvocation"] | components["schemas"]["PasteImageIntoBoundingBoxInvocation"] | components["schemas"]["PiDiNetEdgeDetectionInvocation"] | components["schemas"]["PromptTemplateInvocation"] | components["schemas"]["PromptsFromFileInvocation"] | components["schemas"]["QwenImageDenoiseInvocation"] | components["schemas"]["QwenImageImageToLatentsInvocation"] | components["schemas"]["QwenImageLatentsToImageInvocation"] | components["schemas"]["QwenImageLoRACollectionLoader"] | components["schemas"]["QwenImageLoRALoaderInvocation"] | components["schemas"]["QwenImageModelLoaderInvocation"] | components["schemas"]["QwenImageTextEncoderInvocation"] | components["schemas"]["RandomFloatInvocation"] | components["schemas"]["RandomIntInvocation"] | components["schemas"]["RandomRangeInvocation"] | components["schemas"]["RangeInvocation"] | components["schemas"]["RangeOfSizeInvocation"] | components["schemas"]["RectangleMaskInvocation"] | components["schemas"]["ResizeLatentsInvocation"] | components["schemas"]["RoundInvocation"] | components["schemas"]["SD3DenoiseInvocation"] | components["schemas"]["SD3ImageToLatentsInvocation"] | components["schemas"]["SD3LatentsToImageInvocation"] | components["schemas"]["SDXLCompelPromptInvocation"] | components["schemas"]["SDXLLoRACollectionLoader"] | components["schemas"]["SDXLLoRALoaderInvocation"] | components["schemas"]["SDXLModelLoaderInvocation"] | components["schemas"]["SDXLRefinerCompelPromptInvocation"] | components["schemas"]["SDXLRefinerModelLoaderInvocation"] | components["schemas"]["SaveImageInvocation"] | components["schemas"]["SaveImageToFileInvocation"] | components["schemas"]["ScaleLatentsInvocation"] | components["schemas"]["SchedulerInvocation"] | components["schemas"]["Sd3ModelLoaderInvocation"] | components["schemas"]["Sd3TextEncoderInvocation"] | components["schemas"]["SeamlessModeInvocation"] | components["schemas"]["SeedreamImageGenerationInvocation"] | components["schemas"]["SegmentAnythingInvocation"] | components["schemas"]["ShowImageInvocation"] | components["schemas"]["SpandrelImageToImageAutoscaleInvocation"] | components["schemas"]["SpandrelImageToImageInvocation"] | components["schemas"]["StringBatchInvocation"] | components["schemas"]["StringCollectionInvocation"] | components["schemas"]["StringGenerator"] | components["schemas"]["StringInvocation"] | components["schemas"]["StringJoinInvocation"] | components["schemas"]["StringJoinThreeInvocation"] | components["schemas"]["StringReplaceInvocation"] | components["schemas"]["StringSplitInvocation"] | components["schemas"]["StringSplitNegInvocation"] | components["schemas"]["SubtractInvocation"] | components["schemas"]["T2IAdapterInvocation"] | components["schemas"]["TextLLMInvocation"] | components["schemas"]["TileToPropertiesInvocation"] | components["schemas"]["TiledMultiDiffusionDenoiseLatents"] | components["schemas"]["UnsharpMaskInvocation"] | components["schemas"]["VAELoaderInvocation"] | components["schemas"]["VideoConcatInvocation"] | components["schemas"]["VideoFrameExtractInvocation"] | components["schemas"]["VideoInvocation"] | components["schemas"]["WanDenoiseInvocation"] | components["schemas"]["WanI2VIdealDimensionsInvocation"] | components["schemas"]["WanImageToLatentsInvocation"] | components["schemas"]["WanLatentsToImageInvocation"] | components["schemas"]["WanLatentsToVideoInvocation"] | components["schemas"]["WanLoRACollectionLoader"] | components["schemas"]["WanLoRALoaderInvocation"] | components["schemas"]["WanModelLoaderInvocation"] | components["schemas"]["WanRefImageEncoderInvocation"] | components["schemas"]["WanTI2VIdealDimensionsInvocation"] | components["schemas"]["WanTextEncoderInvocation"] | components["schemas"]["WanVideoDenoiseInvocation"] | components["schemas"]["WorkflowReturnGetInvocation"] | components["schemas"]["WorkflowReturnInvocation"] | components["schemas"]["WorkflowReturnValueInvocation"] | components["schemas"]["ZImageControlInvocation"] | components["schemas"]["ZImageDenoiseInvocation"] | components["schemas"]["ZImageDenoiseMetaInvocation"] | components["schemas"]["ZImageImageToLatentsInvocation"] | components["schemas"]["ZImageLatentsToImageInvocation"] | components["schemas"]["ZImageLoRACollectionLoader"] | components["schemas"]["ZImageLoRALoaderInvocation"] | components["schemas"]["ZImageModelLoaderInvocation"] | components["schemas"]["ZImageSeedVarianceEnhancerInvocation"] | components["schemas"]["ZImageTextEncoderInvocation"];
/**
* Invocation Source Id
* @description The ID of the prepared invocation's source node
@@ -16093,7 +16805,7 @@ export type components = {
* Result
* @description The result of the invocation
*/
- result: components["schemas"]["AnimaConditioningOutput"] | components["schemas"]["AnimaLLLiteOutput"] | components["schemas"]["AnimaLoRALoaderOutput"] | components["schemas"]["AnimaModelLoaderOutput"] | components["schemas"]["BooleanCollectionOutput"] | components["schemas"]["BooleanOutput"] | components["schemas"]["BoundingBoxCollectionOutput"] | components["schemas"]["BoundingBoxOutput"] | components["schemas"]["CLIPOutput"] | components["schemas"]["CLIPSkipInvocationOutput"] | components["schemas"]["CalculateImageTilesOutput"] | components["schemas"]["CogView4ConditioningOutput"] | components["schemas"]["CogView4ModelLoaderOutput"] | components["schemas"]["CollectInvocationOutput"] | components["schemas"]["ColorCollectionOutput"] | components["schemas"]["ColorOutput"] | components["schemas"]["ConditioningCollectionOutput"] | components["schemas"]["ConditioningOutput"] | components["schemas"]["ControlOutput"] | components["schemas"]["DenoiseMaskOutput"] | components["schemas"]["FaceMaskOutput"] | components["schemas"]["FaceOffOutput"] | components["schemas"]["FloatCollectionOutput"] | components["schemas"]["FloatGeneratorOutput"] | components["schemas"]["FloatOutput"] | components["schemas"]["Flux2KleinLoRALoaderOutput"] | components["schemas"]["Flux2KleinModelLoaderOutput"] | components["schemas"]["FluxConditioningCollectionOutput"] | components["schemas"]["FluxConditioningOutput"] | components["schemas"]["FluxControlLoRALoaderOutput"] | components["schemas"]["FluxControlNetOutput"] | components["schemas"]["FluxFillOutput"] | components["schemas"]["FluxKontextOutput"] | components["schemas"]["FluxLoRALoaderOutput"] | components["schemas"]["FluxModelLoaderOutput"] | components["schemas"]["FluxReduxOutput"] | components["schemas"]["GradientMaskOutput"] | components["schemas"]["IPAdapterOutput"] | components["schemas"]["IdealSizeOutput"] | components["schemas"]["IfInvocationOutput"] | components["schemas"]["ImageCollectionOutput"] | components["schemas"]["ImageGeneratorOutput"] | components["schemas"]["ImageOutput"] | components["schemas"]["ImagePanelCoordinateOutput"] | components["schemas"]["IntegerCollectionOutput"] | components["schemas"]["IntegerGeneratorOutput"] | components["schemas"]["IntegerOutput"] | components["schemas"]["IterateInvocationOutput"] | components["schemas"]["LatentsCollectionOutput"] | components["schemas"]["LatentsMetaOutput"] | components["schemas"]["LatentsOutput"] | components["schemas"]["LoRALoaderOutput"] | components["schemas"]["LoRASelectorOutput"] | components["schemas"]["MDControlListOutput"] | components["schemas"]["MDIPAdapterListOutput"] | components["schemas"]["MDT2IAdapterListOutput"] | components["schemas"]["MaskOutput"] | components["schemas"]["MetadataItemOutput"] | components["schemas"]["MetadataOutput"] | components["schemas"]["MetadataToLorasCollectionOutput"] | components["schemas"]["MetadataToModelOutput"] | components["schemas"]["MetadataToSDXLModelOutput"] | components["schemas"]["ModelIdentifierOutput"] | components["schemas"]["ModelLoaderOutput"] | components["schemas"]["NoiseOutput"] | components["schemas"]["PBRMapsOutput"] | components["schemas"]["PairTileImageOutput"] | components["schemas"]["PromptTemplateOutput"] | components["schemas"]["QwenImageConditioningOutput"] | components["schemas"]["QwenImageLoRALoaderOutput"] | components["schemas"]["QwenImageModelLoaderOutput"] | components["schemas"]["SD3ConditioningOutput"] | components["schemas"]["SDXLLoRALoaderOutput"] | components["schemas"]["SDXLModelLoaderOutput"] | components["schemas"]["SDXLRefinerModelLoaderOutput"] | components["schemas"]["SchedulerOutput"] | components["schemas"]["Sd3ModelLoaderOutput"] | components["schemas"]["SeamlessModeOutput"] | components["schemas"]["String2Output"] | components["schemas"]["StringCollectionOutput"] | components["schemas"]["StringGeneratorOutput"] | components["schemas"]["StringOutput"] | components["schemas"]["StringPosNegOutput"] | components["schemas"]["T2IAdapterOutput"] | components["schemas"]["TileToPropertiesOutput"] | components["schemas"]["UNetOutput"] | components["schemas"]["VAEOutput"] | components["schemas"]["WorkflowReturnGetOutput"] | components["schemas"]["WorkflowReturnOutput"] | components["schemas"]["WorkflowReturnValueOutput"] | components["schemas"]["ZImageConditioningOutput"] | components["schemas"]["ZImageControlOutput"] | components["schemas"]["ZImageLoRALoaderOutput"] | components["schemas"]["ZImageModelLoaderOutput"];
+ result: components["schemas"]["AnimaConditioningOutput"] | components["schemas"]["AnimaLLLiteOutput"] | components["schemas"]["AnimaLoRALoaderOutput"] | components["schemas"]["AnimaModelLoaderOutput"] | components["schemas"]["BooleanCollectionOutput"] | components["schemas"]["BooleanOutput"] | components["schemas"]["BoundingBoxCollectionOutput"] | components["schemas"]["BoundingBoxOutput"] | components["schemas"]["CLIPOutput"] | components["schemas"]["CLIPSkipInvocationOutput"] | components["schemas"]["CalculateImageTilesOutput"] | components["schemas"]["CogView4ConditioningOutput"] | components["schemas"]["CogView4ModelLoaderOutput"] | components["schemas"]["CollectInvocationOutput"] | components["schemas"]["ColorCollectionOutput"] | components["schemas"]["ColorOutput"] | components["schemas"]["ConditioningCollectionOutput"] | components["schemas"]["ConditioningOutput"] | components["schemas"]["ControlOutput"] | components["schemas"]["DenoiseMaskOutput"] | components["schemas"]["ExtractVideoRangeOutput"] | components["schemas"]["FaceMaskOutput"] | components["schemas"]["FaceOffOutput"] | components["schemas"]["FloatCollectionOutput"] | components["schemas"]["FloatGeneratorOutput"] | components["schemas"]["FloatOutput"] | components["schemas"]["Flux2KleinLoRALoaderOutput"] | components["schemas"]["Flux2KleinModelLoaderOutput"] | components["schemas"]["FluxConditioningCollectionOutput"] | components["schemas"]["FluxConditioningOutput"] | components["schemas"]["FluxControlLoRALoaderOutput"] | components["schemas"]["FluxControlNetOutput"] | components["schemas"]["FluxFillOutput"] | components["schemas"]["FluxKontextOutput"] | components["schemas"]["FluxLoRALoaderOutput"] | components["schemas"]["FluxModelLoaderOutput"] | components["schemas"]["FluxReduxOutput"] | components["schemas"]["GradientMaskOutput"] | components["schemas"]["IPAdapterOutput"] | components["schemas"]["IdealSizeOutput"] | components["schemas"]["IfInvocationOutput"] | components["schemas"]["ImageCollectionOutput"] | components["schemas"]["ImageGeneratorOutput"] | components["schemas"]["ImageOutput"] | components["schemas"]["ImagePanelCoordinateOutput"] | components["schemas"]["IntegerCollectionOutput"] | components["schemas"]["IntegerGeneratorOutput"] | components["schemas"]["IntegerOutput"] | components["schemas"]["IterateInvocationOutput"] | components["schemas"]["LatentsCollectionOutput"] | components["schemas"]["LatentsMetaOutput"] | components["schemas"]["LatentsOutput"] | components["schemas"]["LoRALoaderOutput"] | components["schemas"]["LoRASelectorOutput"] | components["schemas"]["MDControlListOutput"] | components["schemas"]["MDIPAdapterListOutput"] | components["schemas"]["MDT2IAdapterListOutput"] | components["schemas"]["MaskOutput"] | components["schemas"]["MetadataItemOutput"] | components["schemas"]["MetadataOutput"] | components["schemas"]["MetadataToLorasCollectionOutput"] | components["schemas"]["MetadataToModelOutput"] | components["schemas"]["MetadataToSDXLModelOutput"] | components["schemas"]["ModelIdentifierOutput"] | components["schemas"]["ModelLoaderOutput"] | components["schemas"]["NoiseOutput"] | components["schemas"]["PBRMapsOutput"] | components["schemas"]["PairTileImageOutput"] | components["schemas"]["PromptTemplateOutput"] | components["schemas"]["QwenImageConditioningOutput"] | components["schemas"]["QwenImageLoRALoaderOutput"] | components["schemas"]["QwenImageModelLoaderOutput"] | components["schemas"]["SD3ConditioningOutput"] | components["schemas"]["SDXLLoRALoaderOutput"] | components["schemas"]["SDXLModelLoaderOutput"] | components["schemas"]["SDXLRefinerModelLoaderOutput"] | components["schemas"]["SchedulerOutput"] | components["schemas"]["Sd3ModelLoaderOutput"] | components["schemas"]["SeamlessModeOutput"] | components["schemas"]["String2Output"] | components["schemas"]["StringCollectionOutput"] | components["schemas"]["StringGeneratorOutput"] | components["schemas"]["StringOutput"] | components["schemas"]["StringPosNegOutput"] | components["schemas"]["T2IAdapterOutput"] | components["schemas"]["TileToPropertiesOutput"] | components["schemas"]["UNetOutput"] | components["schemas"]["VAEOutput"] | components["schemas"]["VideoOutput"] | components["schemas"]["WanConditioningOutput"] | components["schemas"]["WanLoRALoaderOutput"] | components["schemas"]["WanModelLoaderOutput"] | components["schemas"]["WanRefImageOutput"] | components["schemas"]["WorkflowReturnGetOutput"] | components["schemas"]["WorkflowReturnOutput"] | components["schemas"]["WorkflowReturnValueOutput"] | components["schemas"]["ZImageConditioningOutput"] | components["schemas"]["ZImageControlOutput"] | components["schemas"]["ZImageLoRALoaderOutput"] | components["schemas"]["ZImageModelLoaderOutput"];
};
/**
* InvocationErrorEvent
@@ -16147,7 +16859,7 @@ export type components = {
* Invocation
* @description The ID of the invocation
*/
- invocation: components["schemas"]["AddInvocation"] | components["schemas"]["AlibabaCloudImageGenerationInvocation"] | components["schemas"]["AlphaMaskToTensorInvocation"] | components["schemas"]["AnimaDenoiseInvocation"] | components["schemas"]["AnimaImageToLatentsInvocation"] | components["schemas"]["AnimaLLLiteInvocation"] | components["schemas"]["AnimaLatentsToImageInvocation"] | components["schemas"]["AnimaLoRACollectionLoader"] | components["schemas"]["AnimaLoRALoaderInvocation"] | components["schemas"]["AnimaModelLoaderInvocation"] | components["schemas"]["AnimaTextEncoderInvocation"] | components["schemas"]["ApplyMaskTensorToImageInvocation"] | components["schemas"]["ApplyMaskToImageInvocation"] | components["schemas"]["BlankImageInvocation"] | components["schemas"]["BlendLatentsInvocation"] | components["schemas"]["BooleanCollectionInvocation"] | components["schemas"]["BooleanInvocation"] | components["schemas"]["BoundingBoxInvocation"] | components["schemas"]["CLIPSkipInvocation"] | components["schemas"]["CV2InfillInvocation"] | components["schemas"]["CalculateImageTilesEvenSplitInvocation"] | components["schemas"]["CalculateImageTilesInvocation"] | components["schemas"]["CalculateImageTilesMinimumOverlapInvocation"] | components["schemas"]["CallSavedWorkflowInvocation"] | components["schemas"]["CannyEdgeDetectionInvocation"] | components["schemas"]["CanvasOutputInvocation"] | components["schemas"]["CanvasPasteBackInvocation"] | components["schemas"]["CanvasV2MaskAndCropInvocation"] | components["schemas"]["CenterPadCropInvocation"] | components["schemas"]["CogView4DenoiseInvocation"] | components["schemas"]["CogView4ImageToLatentsInvocation"] | components["schemas"]["CogView4LatentsToImageInvocation"] | components["schemas"]["CogView4ModelLoaderInvocation"] | components["schemas"]["CogView4TextEncoderInvocation"] | components["schemas"]["CollectInvocation"] | components["schemas"]["ColorCorrectInvocation"] | components["schemas"]["ColorInvocation"] | components["schemas"]["ColorMapInvocation"] | components["schemas"]["CompelInvocation"] | components["schemas"]["ConditioningCollectionInvocation"] | components["schemas"]["ConditioningInvocation"] | components["schemas"]["ContentShuffleInvocation"] | components["schemas"]["ControlNetInvocation"] | components["schemas"]["CoreMetadataInvocation"] | components["schemas"]["CreateDenoiseMaskInvocation"] | components["schemas"]["CreateGradientMaskInvocation"] | components["schemas"]["CropImageToBoundingBoxInvocation"] | components["schemas"]["CropLatentsCoreInvocation"] | components["schemas"]["CvInpaintInvocation"] | components["schemas"]["DWOpenposeDetectionInvocation"] | components["schemas"]["DecodeInvisibleWatermarkInvocation"] | components["schemas"]["DenoiseLatentsInvocation"] | components["schemas"]["DenoiseLatentsMetaInvocation"] | components["schemas"]["DepthAnythingDepthEstimationInvocation"] | components["schemas"]["DivideInvocation"] | components["schemas"]["DynamicPromptInvocation"] | components["schemas"]["ESRGANInvocation"] | components["schemas"]["ExpandMaskWithFadeInvocation"] | components["schemas"]["FLUXLoRACollectionLoader"] | components["schemas"]["FaceIdentifierInvocation"] | components["schemas"]["FaceMaskInvocation"] | components["schemas"]["FaceOffInvocation"] | components["schemas"]["FloatBatchInvocation"] | components["schemas"]["FloatCollectionInvocation"] | components["schemas"]["FloatGenerator"] | components["schemas"]["FloatInvocation"] | components["schemas"]["FloatLinearRangeInvocation"] | components["schemas"]["FloatMathInvocation"] | components["schemas"]["FloatToIntegerInvocation"] | components["schemas"]["Flux2DenoiseInvocation"] | components["schemas"]["Flux2KleinLoRACollectionLoader"] | components["schemas"]["Flux2KleinLoRALoaderInvocation"] | components["schemas"]["Flux2KleinModelLoaderInvocation"] | components["schemas"]["Flux2KleinTextEncoderInvocation"] | components["schemas"]["Flux2VaeDecodeInvocation"] | components["schemas"]["Flux2VaeEncodeInvocation"] | components["schemas"]["FluxControlLoRALoaderInvocation"] | components["schemas"]["FluxControlNetInvocation"] | components["schemas"]["FluxDenoiseInvocation"] | components["schemas"]["FluxDenoiseLatentsMetaInvocation"] | components["schemas"]["FluxFillInvocation"] | components["schemas"]["FluxIPAdapterInvocation"] | components["schemas"]["FluxKontextConcatenateImagesInvocation"] | components["schemas"]["FluxKontextInvocation"] | components["schemas"]["FluxLoRALoaderInvocation"] | components["schemas"]["FluxModelLoaderInvocation"] | components["schemas"]["FluxReduxInvocation"] | components["schemas"]["FluxTextEncoderInvocation"] | components["schemas"]["FluxVaeDecodeInvocation"] | components["schemas"]["FluxVaeEncodeInvocation"] | components["schemas"]["FreeUInvocation"] | components["schemas"]["GeminiImageGenerationInvocation"] | components["schemas"]["GetMaskBoundingBoxInvocation"] | components["schemas"]["GroundingDinoInvocation"] | components["schemas"]["HEDEdgeDetectionInvocation"] | components["schemas"]["HeuristicResizeInvocation"] | components["schemas"]["IPAdapterInvocation"] | components["schemas"]["IdealSizeInvocation"] | components["schemas"]["IfInvocation"] | components["schemas"]["ImageBatchInvocation"] | components["schemas"]["ImageBlurInvocation"] | components["schemas"]["ImageChannelInvocation"] | components["schemas"]["ImageChannelMultiplyInvocation"] | components["schemas"]["ImageChannelOffsetInvocation"] | components["schemas"]["ImageCollectionInvocation"] | components["schemas"]["ImageConvertInvocation"] | components["schemas"]["ImageCropInvocation"] | components["schemas"]["ImageGenerator"] | components["schemas"]["ImageHueAdjustmentInvocation"] | components["schemas"]["ImageInverseLerpInvocation"] | components["schemas"]["ImageInvocation"] | components["schemas"]["ImageLerpInvocation"] | components["schemas"]["ImageMaskToTensorInvocation"] | components["schemas"]["ImageMultiplyInvocation"] | components["schemas"]["ImageNSFWBlurInvocation"] | components["schemas"]["ImageNoiseInvocation"] | components["schemas"]["ImagePanelLayoutInvocation"] | components["schemas"]["ImagePasteInvocation"] | components["schemas"]["ImageResizeInvocation"] | components["schemas"]["ImageScaleInvocation"] | components["schemas"]["ImageToLatentsInvocation"] | components["schemas"]["ImageWatermarkInvocation"] | components["schemas"]["InfillColorInvocation"] | components["schemas"]["InfillPatchMatchInvocation"] | components["schemas"]["InfillTileInvocation"] | components["schemas"]["IntegerBatchInvocation"] | components["schemas"]["IntegerCollectionInvocation"] | components["schemas"]["IntegerGenerator"] | components["schemas"]["IntegerInvocation"] | components["schemas"]["IntegerMathInvocation"] | components["schemas"]["InvertTensorMaskInvocation"] | components["schemas"]["InvokeAdjustImageHuePlusInvocation"] | components["schemas"]["InvokeEquivalentAchromaticLightnessInvocation"] | components["schemas"]["InvokeImageBlendInvocation"] | components["schemas"]["InvokeImageCompositorInvocation"] | components["schemas"]["InvokeImageDilateOrErodeInvocation"] | components["schemas"]["InvokeImageEnhanceInvocation"] | components["schemas"]["InvokeImageValueThresholdsInvocation"] | components["schemas"]["IterateInvocation"] | components["schemas"]["LaMaInfillInvocation"] | components["schemas"]["LatentsCollectionInvocation"] | components["schemas"]["LatentsInvocation"] | components["schemas"]["LatentsToImageInvocation"] | components["schemas"]["LineartAnimeEdgeDetectionInvocation"] | components["schemas"]["LineartEdgeDetectionInvocation"] | components["schemas"]["LlavaOnevisionVllmInvocation"] | components["schemas"]["LoRACollectionLoader"] | components["schemas"]["LoRALoaderInvocation"] | components["schemas"]["LoRASelectorInvocation"] | components["schemas"]["MLSDDetectionInvocation"] | components["schemas"]["MainModelLoaderInvocation"] | components["schemas"]["MaskCombineInvocation"] | components["schemas"]["MaskEdgeInvocation"] | components["schemas"]["MaskFromAlphaInvocation"] | components["schemas"]["MaskFromIDInvocation"] | components["schemas"]["MaskTensorToImageInvocation"] | components["schemas"]["MediaPipeFaceDetectionInvocation"] | components["schemas"]["MergeMetadataInvocation"] | components["schemas"]["MergeTilesToImageInvocation"] | components["schemas"]["MetadataFieldExtractorInvocation"] | components["schemas"]["MetadataFromImageInvocation"] | components["schemas"]["MetadataInvocation"] | components["schemas"]["MetadataItemInvocation"] | components["schemas"]["MetadataItemLinkedInvocation"] | components["schemas"]["MetadataToBoolCollectionInvocation"] | components["schemas"]["MetadataToBoolInvocation"] | components["schemas"]["MetadataToControlnetsInvocation"] | components["schemas"]["MetadataToFloatCollectionInvocation"] | components["schemas"]["MetadataToFloatInvocation"] | components["schemas"]["MetadataToIPAdaptersInvocation"] | components["schemas"]["MetadataToIntegerCollectionInvocation"] | components["schemas"]["MetadataToIntegerInvocation"] | components["schemas"]["MetadataToLorasCollectionInvocation"] | components["schemas"]["MetadataToLorasInvocation"] | components["schemas"]["MetadataToModelInvocation"] | components["schemas"]["MetadataToSDXLLorasInvocation"] | components["schemas"]["MetadataToSDXLModelInvocation"] | components["schemas"]["MetadataToSchedulerInvocation"] | components["schemas"]["MetadataToStringCollectionInvocation"] | components["schemas"]["MetadataToStringInvocation"] | components["schemas"]["MetadataToT2IAdaptersInvocation"] | components["schemas"]["MetadataToVAEInvocation"] | components["schemas"]["ModelIdentifierInvocation"] | components["schemas"]["MultiplyInvocation"] | components["schemas"]["NoiseInvocation"] | components["schemas"]["NormalMapInvocation"] | components["schemas"]["OklabUnsharpMaskInvocation"] | components["schemas"]["OklchImageHueAdjustmentInvocation"] | components["schemas"]["OpenAIImageGenerationInvocation"] | components["schemas"]["PBRMapsInvocation"] | components["schemas"]["PairTileImageInvocation"] | components["schemas"]["PasteImageIntoBoundingBoxInvocation"] | components["schemas"]["PiDiNetEdgeDetectionInvocation"] | components["schemas"]["PromptTemplateInvocation"] | components["schemas"]["PromptsFromFileInvocation"] | components["schemas"]["QwenImageDenoiseInvocation"] | components["schemas"]["QwenImageImageToLatentsInvocation"] | components["schemas"]["QwenImageLatentsToImageInvocation"] | components["schemas"]["QwenImageLoRACollectionLoader"] | components["schemas"]["QwenImageLoRALoaderInvocation"] | components["schemas"]["QwenImageModelLoaderInvocation"] | components["schemas"]["QwenImageTextEncoderInvocation"] | components["schemas"]["RandomFloatInvocation"] | components["schemas"]["RandomIntInvocation"] | components["schemas"]["RandomRangeInvocation"] | components["schemas"]["RangeInvocation"] | components["schemas"]["RangeOfSizeInvocation"] | components["schemas"]["RectangleMaskInvocation"] | components["schemas"]["ResizeLatentsInvocation"] | components["schemas"]["RoundInvocation"] | components["schemas"]["SD3DenoiseInvocation"] | components["schemas"]["SD3ImageToLatentsInvocation"] | components["schemas"]["SD3LatentsToImageInvocation"] | components["schemas"]["SDXLCompelPromptInvocation"] | components["schemas"]["SDXLLoRACollectionLoader"] | components["schemas"]["SDXLLoRALoaderInvocation"] | components["schemas"]["SDXLModelLoaderInvocation"] | components["schemas"]["SDXLRefinerCompelPromptInvocation"] | components["schemas"]["SDXLRefinerModelLoaderInvocation"] | components["schemas"]["SaveImageInvocation"] | components["schemas"]["SaveImageToFileInvocation"] | components["schemas"]["ScaleLatentsInvocation"] | components["schemas"]["SchedulerInvocation"] | components["schemas"]["Sd3ModelLoaderInvocation"] | components["schemas"]["Sd3TextEncoderInvocation"] | components["schemas"]["SeamlessModeInvocation"] | components["schemas"]["SeedreamImageGenerationInvocation"] | components["schemas"]["SegmentAnythingInvocation"] | components["schemas"]["ShowImageInvocation"] | components["schemas"]["SpandrelImageToImageAutoscaleInvocation"] | components["schemas"]["SpandrelImageToImageInvocation"] | components["schemas"]["StringBatchInvocation"] | components["schemas"]["StringCollectionInvocation"] | components["schemas"]["StringGenerator"] | components["schemas"]["StringInvocation"] | components["schemas"]["StringJoinInvocation"] | components["schemas"]["StringJoinThreeInvocation"] | components["schemas"]["StringReplaceInvocation"] | components["schemas"]["StringSplitInvocation"] | components["schemas"]["StringSplitNegInvocation"] | components["schemas"]["SubtractInvocation"] | components["schemas"]["T2IAdapterInvocation"] | components["schemas"]["TextLLMInvocation"] | components["schemas"]["TileToPropertiesInvocation"] | components["schemas"]["TiledMultiDiffusionDenoiseLatents"] | components["schemas"]["UnsharpMaskInvocation"] | components["schemas"]["VAELoaderInvocation"] | components["schemas"]["WorkflowReturnGetInvocation"] | components["schemas"]["WorkflowReturnInvocation"] | components["schemas"]["WorkflowReturnValueInvocation"] | components["schemas"]["ZImageControlInvocation"] | components["schemas"]["ZImageDenoiseInvocation"] | components["schemas"]["ZImageDenoiseMetaInvocation"] | components["schemas"]["ZImageImageToLatentsInvocation"] | components["schemas"]["ZImageLatentsToImageInvocation"] | components["schemas"]["ZImageLoRACollectionLoader"] | components["schemas"]["ZImageLoRALoaderInvocation"] | components["schemas"]["ZImageModelLoaderInvocation"] | components["schemas"]["ZImageSeedVarianceEnhancerInvocation"] | components["schemas"]["ZImageTextEncoderInvocation"];
+ invocation: components["schemas"]["AddInvocation"] | components["schemas"]["AlibabaCloudImageGenerationInvocation"] | components["schemas"]["AlphaMaskToTensorInvocation"] | components["schemas"]["AnimaDenoiseInvocation"] | components["schemas"]["AnimaImageToLatentsInvocation"] | components["schemas"]["AnimaLLLiteInvocation"] | components["schemas"]["AnimaLatentsToImageInvocation"] | components["schemas"]["AnimaLoRACollectionLoader"] | components["schemas"]["AnimaLoRALoaderInvocation"] | components["schemas"]["AnimaModelLoaderInvocation"] | components["schemas"]["AnimaTextEncoderInvocation"] | components["schemas"]["ApplyMaskTensorToImageInvocation"] | components["schemas"]["ApplyMaskToImageInvocation"] | components["schemas"]["BlankImageInvocation"] | components["schemas"]["BlendLatentsInvocation"] | components["schemas"]["BooleanCollectionInvocation"] | components["schemas"]["BooleanInvocation"] | components["schemas"]["BoundingBoxInvocation"] | components["schemas"]["CLIPSkipInvocation"] | components["schemas"]["CV2InfillInvocation"] | components["schemas"]["CalculateImageTilesEvenSplitInvocation"] | components["schemas"]["CalculateImageTilesInvocation"] | components["schemas"]["CalculateImageTilesMinimumOverlapInvocation"] | components["schemas"]["CallSavedWorkflowInvocation"] | components["schemas"]["CannyEdgeDetectionInvocation"] | components["schemas"]["CanvasOutputInvocation"] | components["schemas"]["CanvasPasteBackInvocation"] | components["schemas"]["CanvasV2MaskAndCropInvocation"] | components["schemas"]["CenterPadCropInvocation"] | components["schemas"]["CogView4DenoiseInvocation"] | components["schemas"]["CogView4ImageToLatentsInvocation"] | components["schemas"]["CogView4LatentsToImageInvocation"] | components["schemas"]["CogView4ModelLoaderInvocation"] | components["schemas"]["CogView4TextEncoderInvocation"] | components["schemas"]["CollectInvocation"] | components["schemas"]["ColorCorrectInvocation"] | components["schemas"]["ColorInvocation"] | components["schemas"]["ColorMapInvocation"] | components["schemas"]["CompelInvocation"] | components["schemas"]["ConditioningCollectionInvocation"] | components["schemas"]["ConditioningInvocation"] | components["schemas"]["ContentShuffleInvocation"] | components["schemas"]["ControlNetInvocation"] | components["schemas"]["CoreMetadataInvocation"] | components["schemas"]["CreateDenoiseMaskInvocation"] | components["schemas"]["CreateGradientMaskInvocation"] | components["schemas"]["CropImageToBoundingBoxInvocation"] | components["schemas"]["CropLatentsCoreInvocation"] | components["schemas"]["CvInpaintInvocation"] | components["schemas"]["DWOpenposeDetectionInvocation"] | components["schemas"]["DecodeInvisibleWatermarkInvocation"] | components["schemas"]["DenoiseLatentsInvocation"] | components["schemas"]["DenoiseLatentsMetaInvocation"] | components["schemas"]["DepthAnythingDepthEstimationInvocation"] | components["schemas"]["DivideInvocation"] | components["schemas"]["DynamicPromptInvocation"] | components["schemas"]["ESRGANInvocation"] | components["schemas"]["ExpandMaskWithFadeInvocation"] | components["schemas"]["ExtractVideoRangeInvocation"] | components["schemas"]["FLUXLoRACollectionLoader"] | components["schemas"]["FaceIdentifierInvocation"] | components["schemas"]["FaceMaskInvocation"] | components["schemas"]["FaceOffInvocation"] | components["schemas"]["FloatBatchInvocation"] | components["schemas"]["FloatCollectionInvocation"] | components["schemas"]["FloatGenerator"] | components["schemas"]["FloatInvocation"] | components["schemas"]["FloatLinearRangeInvocation"] | components["schemas"]["FloatMathInvocation"] | components["schemas"]["FloatToIntegerInvocation"] | components["schemas"]["Flux2DenoiseInvocation"] | components["schemas"]["Flux2KleinLoRACollectionLoader"] | components["schemas"]["Flux2KleinLoRALoaderInvocation"] | components["schemas"]["Flux2KleinModelLoaderInvocation"] | components["schemas"]["Flux2KleinTextEncoderInvocation"] | components["schemas"]["Flux2VaeDecodeInvocation"] | components["schemas"]["Flux2VaeEncodeInvocation"] | components["schemas"]["FluxControlLoRALoaderInvocation"] | components["schemas"]["FluxControlNetInvocation"] | components["schemas"]["FluxDenoiseInvocation"] | components["schemas"]["FluxDenoiseLatentsMetaInvocation"] | components["schemas"]["FluxFillInvocation"] | components["schemas"]["FluxIPAdapterInvocation"] | components["schemas"]["FluxKontextConcatenateImagesInvocation"] | components["schemas"]["FluxKontextInvocation"] | components["schemas"]["FluxLoRALoaderInvocation"] | components["schemas"]["FluxModelLoaderInvocation"] | components["schemas"]["FluxReduxInvocation"] | components["schemas"]["FluxTextEncoderInvocation"] | components["schemas"]["FluxVaeDecodeInvocation"] | components["schemas"]["FluxVaeEncodeInvocation"] | components["schemas"]["FreeUInvocation"] | components["schemas"]["GeminiImageGenerationInvocation"] | components["schemas"]["GetMaskBoundingBoxInvocation"] | components["schemas"]["GroundingDinoInvocation"] | components["schemas"]["HEDEdgeDetectionInvocation"] | components["schemas"]["HeuristicResizeInvocation"] | components["schemas"]["IPAdapterInvocation"] | components["schemas"]["IdealSizeInvocation"] | components["schemas"]["IfInvocation"] | components["schemas"]["ImageBatchInvocation"] | components["schemas"]["ImageBlurInvocation"] | components["schemas"]["ImageChannelInvocation"] | components["schemas"]["ImageChannelMultiplyInvocation"] | components["schemas"]["ImageChannelOffsetInvocation"] | components["schemas"]["ImageCollectionInvocation"] | components["schemas"]["ImageConvertInvocation"] | components["schemas"]["ImageCropInvocation"] | components["schemas"]["ImageGenerator"] | components["schemas"]["ImageHueAdjustmentInvocation"] | components["schemas"]["ImageInverseLerpInvocation"] | components["schemas"]["ImageInvocation"] | components["schemas"]["ImageLerpInvocation"] | components["schemas"]["ImageMaskToTensorInvocation"] | components["schemas"]["ImageMultiplyInvocation"] | components["schemas"]["ImageNSFWBlurInvocation"] | components["schemas"]["ImageNoiseInvocation"] | components["schemas"]["ImagePanelLayoutInvocation"] | components["schemas"]["ImagePasteInvocation"] | components["schemas"]["ImageResizeInvocation"] | components["schemas"]["ImageScaleInvocation"] | components["schemas"]["ImageToLatentsInvocation"] | components["schemas"]["ImageWatermarkInvocation"] | components["schemas"]["InfillColorInvocation"] | components["schemas"]["InfillPatchMatchInvocation"] | components["schemas"]["InfillTileInvocation"] | components["schemas"]["IntegerBatchInvocation"] | components["schemas"]["IntegerCollectionInvocation"] | components["schemas"]["IntegerGenerator"] | components["schemas"]["IntegerInvocation"] | components["schemas"]["IntegerMathInvocation"] | components["schemas"]["InvertTensorMaskInvocation"] | components["schemas"]["InvokeAdjustImageHuePlusInvocation"] | components["schemas"]["InvokeEquivalentAchromaticLightnessInvocation"] | components["schemas"]["InvokeImageBlendInvocation"] | components["schemas"]["InvokeImageCompositorInvocation"] | components["schemas"]["InvokeImageDilateOrErodeInvocation"] | components["schemas"]["InvokeImageEnhanceInvocation"] | components["schemas"]["InvokeImageValueThresholdsInvocation"] | components["schemas"]["IterateInvocation"] | components["schemas"]["LaMaInfillInvocation"] | components["schemas"]["LatentsCollectionInvocation"] | components["schemas"]["LatentsInvocation"] | components["schemas"]["LatentsToImageInvocation"] | components["schemas"]["LineartAnimeEdgeDetectionInvocation"] | components["schemas"]["LineartEdgeDetectionInvocation"] | components["schemas"]["LlavaOnevisionVllmInvocation"] | components["schemas"]["LoRACollectionLoader"] | components["schemas"]["LoRALoaderInvocation"] | components["schemas"]["LoRASelectorInvocation"] | components["schemas"]["MLSDDetectionInvocation"] | components["schemas"]["MainModelLoaderInvocation"] | components["schemas"]["MaskCombineInvocation"] | components["schemas"]["MaskEdgeInvocation"] | components["schemas"]["MaskFromAlphaInvocation"] | components["schemas"]["MaskFromIDInvocation"] | components["schemas"]["MaskTensorToImageInvocation"] | components["schemas"]["MediaPipeFaceDetectionInvocation"] | components["schemas"]["MergeMetadataInvocation"] | components["schemas"]["MergeTilesToImageInvocation"] | components["schemas"]["MetadataFieldExtractorInvocation"] | components["schemas"]["MetadataFromImageInvocation"] | components["schemas"]["MetadataInvocation"] | components["schemas"]["MetadataItemInvocation"] | components["schemas"]["MetadataItemLinkedInvocation"] | components["schemas"]["MetadataToBoolCollectionInvocation"] | components["schemas"]["MetadataToBoolInvocation"] | components["schemas"]["MetadataToControlnetsInvocation"] | components["schemas"]["MetadataToFloatCollectionInvocation"] | components["schemas"]["MetadataToFloatInvocation"] | components["schemas"]["MetadataToIPAdaptersInvocation"] | components["schemas"]["MetadataToIntegerCollectionInvocation"] | components["schemas"]["MetadataToIntegerInvocation"] | components["schemas"]["MetadataToLorasCollectionInvocation"] | components["schemas"]["MetadataToLorasInvocation"] | components["schemas"]["MetadataToModelInvocation"] | components["schemas"]["MetadataToSDXLLorasInvocation"] | components["schemas"]["MetadataToSDXLModelInvocation"] | components["schemas"]["MetadataToSchedulerInvocation"] | components["schemas"]["MetadataToStringCollectionInvocation"] | components["schemas"]["MetadataToStringInvocation"] | components["schemas"]["MetadataToT2IAdaptersInvocation"] | components["schemas"]["MetadataToVAEInvocation"] | components["schemas"]["ModelIdentifierInvocation"] | components["schemas"]["MultiplyInvocation"] | components["schemas"]["NoiseInvocation"] | components["schemas"]["NormalMapInvocation"] | components["schemas"]["OklabUnsharpMaskInvocation"] | components["schemas"]["OklchImageHueAdjustmentInvocation"] | components["schemas"]["OpenAIImageGenerationInvocation"] | components["schemas"]["PBRMapsInvocation"] | components["schemas"]["PairTileImageInvocation"] | components["schemas"]["PasteImageIntoBoundingBoxInvocation"] | components["schemas"]["PiDiNetEdgeDetectionInvocation"] | components["schemas"]["PromptTemplateInvocation"] | components["schemas"]["PromptsFromFileInvocation"] | components["schemas"]["QwenImageDenoiseInvocation"] | components["schemas"]["QwenImageImageToLatentsInvocation"] | components["schemas"]["QwenImageLatentsToImageInvocation"] | components["schemas"]["QwenImageLoRACollectionLoader"] | components["schemas"]["QwenImageLoRALoaderInvocation"] | components["schemas"]["QwenImageModelLoaderInvocation"] | components["schemas"]["QwenImageTextEncoderInvocation"] | components["schemas"]["RandomFloatInvocation"] | components["schemas"]["RandomIntInvocation"] | components["schemas"]["RandomRangeInvocation"] | components["schemas"]["RangeInvocation"] | components["schemas"]["RangeOfSizeInvocation"] | components["schemas"]["RectangleMaskInvocation"] | components["schemas"]["ResizeLatentsInvocation"] | components["schemas"]["RoundInvocation"] | components["schemas"]["SD3DenoiseInvocation"] | components["schemas"]["SD3ImageToLatentsInvocation"] | components["schemas"]["SD3LatentsToImageInvocation"] | components["schemas"]["SDXLCompelPromptInvocation"] | components["schemas"]["SDXLLoRACollectionLoader"] | components["schemas"]["SDXLLoRALoaderInvocation"] | components["schemas"]["SDXLModelLoaderInvocation"] | components["schemas"]["SDXLRefinerCompelPromptInvocation"] | components["schemas"]["SDXLRefinerModelLoaderInvocation"] | components["schemas"]["SaveImageInvocation"] | components["schemas"]["SaveImageToFileInvocation"] | components["schemas"]["ScaleLatentsInvocation"] | components["schemas"]["SchedulerInvocation"] | components["schemas"]["Sd3ModelLoaderInvocation"] | components["schemas"]["Sd3TextEncoderInvocation"] | components["schemas"]["SeamlessModeInvocation"] | components["schemas"]["SeedreamImageGenerationInvocation"] | components["schemas"]["SegmentAnythingInvocation"] | components["schemas"]["ShowImageInvocation"] | components["schemas"]["SpandrelImageToImageAutoscaleInvocation"] | components["schemas"]["SpandrelImageToImageInvocation"] | components["schemas"]["StringBatchInvocation"] | components["schemas"]["StringCollectionInvocation"] | components["schemas"]["StringGenerator"] | components["schemas"]["StringInvocation"] | components["schemas"]["StringJoinInvocation"] | components["schemas"]["StringJoinThreeInvocation"] | components["schemas"]["StringReplaceInvocation"] | components["schemas"]["StringSplitInvocation"] | components["schemas"]["StringSplitNegInvocation"] | components["schemas"]["SubtractInvocation"] | components["schemas"]["T2IAdapterInvocation"] | components["schemas"]["TextLLMInvocation"] | components["schemas"]["TileToPropertiesInvocation"] | components["schemas"]["TiledMultiDiffusionDenoiseLatents"] | components["schemas"]["UnsharpMaskInvocation"] | components["schemas"]["VAELoaderInvocation"] | components["schemas"]["VideoConcatInvocation"] | components["schemas"]["VideoFrameExtractInvocation"] | components["schemas"]["VideoInvocation"] | components["schemas"]["WanDenoiseInvocation"] | components["schemas"]["WanI2VIdealDimensionsInvocation"] | components["schemas"]["WanImageToLatentsInvocation"] | components["schemas"]["WanLatentsToImageInvocation"] | components["schemas"]["WanLatentsToVideoInvocation"] | components["schemas"]["WanLoRACollectionLoader"] | components["schemas"]["WanLoRALoaderInvocation"] | components["schemas"]["WanModelLoaderInvocation"] | components["schemas"]["WanRefImageEncoderInvocation"] | components["schemas"]["WanTI2VIdealDimensionsInvocation"] | components["schemas"]["WanTextEncoderInvocation"] | components["schemas"]["WanVideoDenoiseInvocation"] | components["schemas"]["WorkflowReturnGetInvocation"] | components["schemas"]["WorkflowReturnInvocation"] | components["schemas"]["WorkflowReturnValueInvocation"] | components["schemas"]["ZImageControlInvocation"] | components["schemas"]["ZImageDenoiseInvocation"] | components["schemas"]["ZImageDenoiseMetaInvocation"] | components["schemas"]["ZImageImageToLatentsInvocation"] | components["schemas"]["ZImageLatentsToImageInvocation"] | components["schemas"]["ZImageLoRACollectionLoader"] | components["schemas"]["ZImageLoRALoaderInvocation"] | components["schemas"]["ZImageModelLoaderInvocation"] | components["schemas"]["ZImageSeedVarianceEnhancerInvocation"] | components["schemas"]["ZImageTextEncoderInvocation"];
/**
* Invocation Source Id
* @description The ID of the prepared invocation's source node
@@ -16225,6 +16937,7 @@ export type components = {
dynamic_prompt: components["schemas"]["StringCollectionOutput"];
esrgan: components["schemas"]["ImageOutput"];
expand_mask_with_fade: components["schemas"]["ImageOutput"];
+ extract_video_range: components["schemas"]["ExtractVideoRangeOutput"];
face_identifier: components["schemas"]["ImageOutput"];
face_mask_detection: components["schemas"]["FaceMaskOutput"];
face_off: components["schemas"]["FaceOffOutput"];
@@ -16417,6 +17130,21 @@ export type components = {
unsharp_mask: components["schemas"]["ImageOutput"];
unsharp_mask_oklab: components["schemas"]["ImageOutput"];
vae_loader: components["schemas"]["VAEOutput"];
+ video: components["schemas"]["VideoOutput"];
+ video_concat: components["schemas"]["VideoOutput"];
+ video_frame_extract: components["schemas"]["ImageOutput"];
+ wan_denoise: components["schemas"]["LatentsOutput"];
+ wan_i2l: components["schemas"]["LatentsOutput"];
+ wan_i2v_ideal_dimensions: components["schemas"]["IdealSizeOutput"];
+ wan_l2i: components["schemas"]["ImageOutput"];
+ wan_l2v: components["schemas"]["VideoOutput"];
+ wan_lora_collection_loader: components["schemas"]["WanLoRALoaderOutput"];
+ wan_lora_loader: components["schemas"]["WanLoRALoaderOutput"];
+ wan_model_loader: components["schemas"]["WanModelLoaderOutput"];
+ wan_ref_image_encoder: components["schemas"]["WanRefImageOutput"];
+ wan_text_encoder: components["schemas"]["WanConditioningOutput"];
+ wan_ti2v_ideal_dimensions: components["schemas"]["IdealSizeOutput"];
+ wan_video_denoise: components["schemas"]["LatentsOutput"];
workflow_return: components["schemas"]["WorkflowReturnOutput"];
workflow_return_get: components["schemas"]["WorkflowReturnGetOutput"];
workflow_return_value: components["schemas"]["WorkflowReturnValueOutput"];
@@ -16483,7 +17211,7 @@ export type components = {
* Invocation
* @description The ID of the invocation
*/
- invocation: components["schemas"]["AddInvocation"] | components["schemas"]["AlibabaCloudImageGenerationInvocation"] | components["schemas"]["AlphaMaskToTensorInvocation"] | components["schemas"]["AnimaDenoiseInvocation"] | components["schemas"]["AnimaImageToLatentsInvocation"] | components["schemas"]["AnimaLLLiteInvocation"] | components["schemas"]["AnimaLatentsToImageInvocation"] | components["schemas"]["AnimaLoRACollectionLoader"] | components["schemas"]["AnimaLoRALoaderInvocation"] | components["schemas"]["AnimaModelLoaderInvocation"] | components["schemas"]["AnimaTextEncoderInvocation"] | components["schemas"]["ApplyMaskTensorToImageInvocation"] | components["schemas"]["ApplyMaskToImageInvocation"] | components["schemas"]["BlankImageInvocation"] | components["schemas"]["BlendLatentsInvocation"] | components["schemas"]["BooleanCollectionInvocation"] | components["schemas"]["BooleanInvocation"] | components["schemas"]["BoundingBoxInvocation"] | components["schemas"]["CLIPSkipInvocation"] | components["schemas"]["CV2InfillInvocation"] | components["schemas"]["CalculateImageTilesEvenSplitInvocation"] | components["schemas"]["CalculateImageTilesInvocation"] | components["schemas"]["CalculateImageTilesMinimumOverlapInvocation"] | components["schemas"]["CallSavedWorkflowInvocation"] | components["schemas"]["CannyEdgeDetectionInvocation"] | components["schemas"]["CanvasOutputInvocation"] | components["schemas"]["CanvasPasteBackInvocation"] | components["schemas"]["CanvasV2MaskAndCropInvocation"] | components["schemas"]["CenterPadCropInvocation"] | components["schemas"]["CogView4DenoiseInvocation"] | components["schemas"]["CogView4ImageToLatentsInvocation"] | components["schemas"]["CogView4LatentsToImageInvocation"] | components["schemas"]["CogView4ModelLoaderInvocation"] | components["schemas"]["CogView4TextEncoderInvocation"] | components["schemas"]["CollectInvocation"] | components["schemas"]["ColorCorrectInvocation"] | components["schemas"]["ColorInvocation"] | components["schemas"]["ColorMapInvocation"] | components["schemas"]["CompelInvocation"] | components["schemas"]["ConditioningCollectionInvocation"] | components["schemas"]["ConditioningInvocation"] | components["schemas"]["ContentShuffleInvocation"] | components["schemas"]["ControlNetInvocation"] | components["schemas"]["CoreMetadataInvocation"] | components["schemas"]["CreateDenoiseMaskInvocation"] | components["schemas"]["CreateGradientMaskInvocation"] | components["schemas"]["CropImageToBoundingBoxInvocation"] | components["schemas"]["CropLatentsCoreInvocation"] | components["schemas"]["CvInpaintInvocation"] | components["schemas"]["DWOpenposeDetectionInvocation"] | components["schemas"]["DecodeInvisibleWatermarkInvocation"] | components["schemas"]["DenoiseLatentsInvocation"] | components["schemas"]["DenoiseLatentsMetaInvocation"] | components["schemas"]["DepthAnythingDepthEstimationInvocation"] | components["schemas"]["DivideInvocation"] | components["schemas"]["DynamicPromptInvocation"] | components["schemas"]["ESRGANInvocation"] | components["schemas"]["ExpandMaskWithFadeInvocation"] | components["schemas"]["FLUXLoRACollectionLoader"] | components["schemas"]["FaceIdentifierInvocation"] | components["schemas"]["FaceMaskInvocation"] | components["schemas"]["FaceOffInvocation"] | components["schemas"]["FloatBatchInvocation"] | components["schemas"]["FloatCollectionInvocation"] | components["schemas"]["FloatGenerator"] | components["schemas"]["FloatInvocation"] | components["schemas"]["FloatLinearRangeInvocation"] | components["schemas"]["FloatMathInvocation"] | components["schemas"]["FloatToIntegerInvocation"] | components["schemas"]["Flux2DenoiseInvocation"] | components["schemas"]["Flux2KleinLoRACollectionLoader"] | components["schemas"]["Flux2KleinLoRALoaderInvocation"] | components["schemas"]["Flux2KleinModelLoaderInvocation"] | components["schemas"]["Flux2KleinTextEncoderInvocation"] | components["schemas"]["Flux2VaeDecodeInvocation"] | components["schemas"]["Flux2VaeEncodeInvocation"] | components["schemas"]["FluxControlLoRALoaderInvocation"] | components["schemas"]["FluxControlNetInvocation"] | components["schemas"]["FluxDenoiseInvocation"] | components["schemas"]["FluxDenoiseLatentsMetaInvocation"] | components["schemas"]["FluxFillInvocation"] | components["schemas"]["FluxIPAdapterInvocation"] | components["schemas"]["FluxKontextConcatenateImagesInvocation"] | components["schemas"]["FluxKontextInvocation"] | components["schemas"]["FluxLoRALoaderInvocation"] | components["schemas"]["FluxModelLoaderInvocation"] | components["schemas"]["FluxReduxInvocation"] | components["schemas"]["FluxTextEncoderInvocation"] | components["schemas"]["FluxVaeDecodeInvocation"] | components["schemas"]["FluxVaeEncodeInvocation"] | components["schemas"]["FreeUInvocation"] | components["schemas"]["GeminiImageGenerationInvocation"] | components["schemas"]["GetMaskBoundingBoxInvocation"] | components["schemas"]["GroundingDinoInvocation"] | components["schemas"]["HEDEdgeDetectionInvocation"] | components["schemas"]["HeuristicResizeInvocation"] | components["schemas"]["IPAdapterInvocation"] | components["schemas"]["IdealSizeInvocation"] | components["schemas"]["IfInvocation"] | components["schemas"]["ImageBatchInvocation"] | components["schemas"]["ImageBlurInvocation"] | components["schemas"]["ImageChannelInvocation"] | components["schemas"]["ImageChannelMultiplyInvocation"] | components["schemas"]["ImageChannelOffsetInvocation"] | components["schemas"]["ImageCollectionInvocation"] | components["schemas"]["ImageConvertInvocation"] | components["schemas"]["ImageCropInvocation"] | components["schemas"]["ImageGenerator"] | components["schemas"]["ImageHueAdjustmentInvocation"] | components["schemas"]["ImageInverseLerpInvocation"] | components["schemas"]["ImageInvocation"] | components["schemas"]["ImageLerpInvocation"] | components["schemas"]["ImageMaskToTensorInvocation"] | components["schemas"]["ImageMultiplyInvocation"] | components["schemas"]["ImageNSFWBlurInvocation"] | components["schemas"]["ImageNoiseInvocation"] | components["schemas"]["ImagePanelLayoutInvocation"] | components["schemas"]["ImagePasteInvocation"] | components["schemas"]["ImageResizeInvocation"] | components["schemas"]["ImageScaleInvocation"] | components["schemas"]["ImageToLatentsInvocation"] | components["schemas"]["ImageWatermarkInvocation"] | components["schemas"]["InfillColorInvocation"] | components["schemas"]["InfillPatchMatchInvocation"] | components["schemas"]["InfillTileInvocation"] | components["schemas"]["IntegerBatchInvocation"] | components["schemas"]["IntegerCollectionInvocation"] | components["schemas"]["IntegerGenerator"] | components["schemas"]["IntegerInvocation"] | components["schemas"]["IntegerMathInvocation"] | components["schemas"]["InvertTensorMaskInvocation"] | components["schemas"]["InvokeAdjustImageHuePlusInvocation"] | components["schemas"]["InvokeEquivalentAchromaticLightnessInvocation"] | components["schemas"]["InvokeImageBlendInvocation"] | components["schemas"]["InvokeImageCompositorInvocation"] | components["schemas"]["InvokeImageDilateOrErodeInvocation"] | components["schemas"]["InvokeImageEnhanceInvocation"] | components["schemas"]["InvokeImageValueThresholdsInvocation"] | components["schemas"]["IterateInvocation"] | components["schemas"]["LaMaInfillInvocation"] | components["schemas"]["LatentsCollectionInvocation"] | components["schemas"]["LatentsInvocation"] | components["schemas"]["LatentsToImageInvocation"] | components["schemas"]["LineartAnimeEdgeDetectionInvocation"] | components["schemas"]["LineartEdgeDetectionInvocation"] | components["schemas"]["LlavaOnevisionVllmInvocation"] | components["schemas"]["LoRACollectionLoader"] | components["schemas"]["LoRALoaderInvocation"] | components["schemas"]["LoRASelectorInvocation"] | components["schemas"]["MLSDDetectionInvocation"] | components["schemas"]["MainModelLoaderInvocation"] | components["schemas"]["MaskCombineInvocation"] | components["schemas"]["MaskEdgeInvocation"] | components["schemas"]["MaskFromAlphaInvocation"] | components["schemas"]["MaskFromIDInvocation"] | components["schemas"]["MaskTensorToImageInvocation"] | components["schemas"]["MediaPipeFaceDetectionInvocation"] | components["schemas"]["MergeMetadataInvocation"] | components["schemas"]["MergeTilesToImageInvocation"] | components["schemas"]["MetadataFieldExtractorInvocation"] | components["schemas"]["MetadataFromImageInvocation"] | components["schemas"]["MetadataInvocation"] | components["schemas"]["MetadataItemInvocation"] | components["schemas"]["MetadataItemLinkedInvocation"] | components["schemas"]["MetadataToBoolCollectionInvocation"] | components["schemas"]["MetadataToBoolInvocation"] | components["schemas"]["MetadataToControlnetsInvocation"] | components["schemas"]["MetadataToFloatCollectionInvocation"] | components["schemas"]["MetadataToFloatInvocation"] | components["schemas"]["MetadataToIPAdaptersInvocation"] | components["schemas"]["MetadataToIntegerCollectionInvocation"] | components["schemas"]["MetadataToIntegerInvocation"] | components["schemas"]["MetadataToLorasCollectionInvocation"] | components["schemas"]["MetadataToLorasInvocation"] | components["schemas"]["MetadataToModelInvocation"] | components["schemas"]["MetadataToSDXLLorasInvocation"] | components["schemas"]["MetadataToSDXLModelInvocation"] | components["schemas"]["MetadataToSchedulerInvocation"] | components["schemas"]["MetadataToStringCollectionInvocation"] | components["schemas"]["MetadataToStringInvocation"] | components["schemas"]["MetadataToT2IAdaptersInvocation"] | components["schemas"]["MetadataToVAEInvocation"] | components["schemas"]["ModelIdentifierInvocation"] | components["schemas"]["MultiplyInvocation"] | components["schemas"]["NoiseInvocation"] | components["schemas"]["NormalMapInvocation"] | components["schemas"]["OklabUnsharpMaskInvocation"] | components["schemas"]["OklchImageHueAdjustmentInvocation"] | components["schemas"]["OpenAIImageGenerationInvocation"] | components["schemas"]["PBRMapsInvocation"] | components["schemas"]["PairTileImageInvocation"] | components["schemas"]["PasteImageIntoBoundingBoxInvocation"] | components["schemas"]["PiDiNetEdgeDetectionInvocation"] | components["schemas"]["PromptTemplateInvocation"] | components["schemas"]["PromptsFromFileInvocation"] | components["schemas"]["QwenImageDenoiseInvocation"] | components["schemas"]["QwenImageImageToLatentsInvocation"] | components["schemas"]["QwenImageLatentsToImageInvocation"] | components["schemas"]["QwenImageLoRACollectionLoader"] | components["schemas"]["QwenImageLoRALoaderInvocation"] | components["schemas"]["QwenImageModelLoaderInvocation"] | components["schemas"]["QwenImageTextEncoderInvocation"] | components["schemas"]["RandomFloatInvocation"] | components["schemas"]["RandomIntInvocation"] | components["schemas"]["RandomRangeInvocation"] | components["schemas"]["RangeInvocation"] | components["schemas"]["RangeOfSizeInvocation"] | components["schemas"]["RectangleMaskInvocation"] | components["schemas"]["ResizeLatentsInvocation"] | components["schemas"]["RoundInvocation"] | components["schemas"]["SD3DenoiseInvocation"] | components["schemas"]["SD3ImageToLatentsInvocation"] | components["schemas"]["SD3LatentsToImageInvocation"] | components["schemas"]["SDXLCompelPromptInvocation"] | components["schemas"]["SDXLLoRACollectionLoader"] | components["schemas"]["SDXLLoRALoaderInvocation"] | components["schemas"]["SDXLModelLoaderInvocation"] | components["schemas"]["SDXLRefinerCompelPromptInvocation"] | components["schemas"]["SDXLRefinerModelLoaderInvocation"] | components["schemas"]["SaveImageInvocation"] | components["schemas"]["SaveImageToFileInvocation"] | components["schemas"]["ScaleLatentsInvocation"] | components["schemas"]["SchedulerInvocation"] | components["schemas"]["Sd3ModelLoaderInvocation"] | components["schemas"]["Sd3TextEncoderInvocation"] | components["schemas"]["SeamlessModeInvocation"] | components["schemas"]["SeedreamImageGenerationInvocation"] | components["schemas"]["SegmentAnythingInvocation"] | components["schemas"]["ShowImageInvocation"] | components["schemas"]["SpandrelImageToImageAutoscaleInvocation"] | components["schemas"]["SpandrelImageToImageInvocation"] | components["schemas"]["StringBatchInvocation"] | components["schemas"]["StringCollectionInvocation"] | components["schemas"]["StringGenerator"] | components["schemas"]["StringInvocation"] | components["schemas"]["StringJoinInvocation"] | components["schemas"]["StringJoinThreeInvocation"] | components["schemas"]["StringReplaceInvocation"] | components["schemas"]["StringSplitInvocation"] | components["schemas"]["StringSplitNegInvocation"] | components["schemas"]["SubtractInvocation"] | components["schemas"]["T2IAdapterInvocation"] | components["schemas"]["TextLLMInvocation"] | components["schemas"]["TileToPropertiesInvocation"] | components["schemas"]["TiledMultiDiffusionDenoiseLatents"] | components["schemas"]["UnsharpMaskInvocation"] | components["schemas"]["VAELoaderInvocation"] | components["schemas"]["WorkflowReturnGetInvocation"] | components["schemas"]["WorkflowReturnInvocation"] | components["schemas"]["WorkflowReturnValueInvocation"] | components["schemas"]["ZImageControlInvocation"] | components["schemas"]["ZImageDenoiseInvocation"] | components["schemas"]["ZImageDenoiseMetaInvocation"] | components["schemas"]["ZImageImageToLatentsInvocation"] | components["schemas"]["ZImageLatentsToImageInvocation"] | components["schemas"]["ZImageLoRACollectionLoader"] | components["schemas"]["ZImageLoRALoaderInvocation"] | components["schemas"]["ZImageModelLoaderInvocation"] | components["schemas"]["ZImageSeedVarianceEnhancerInvocation"] | components["schemas"]["ZImageTextEncoderInvocation"];
+ invocation: components["schemas"]["AddInvocation"] | components["schemas"]["AlibabaCloudImageGenerationInvocation"] | components["schemas"]["AlphaMaskToTensorInvocation"] | components["schemas"]["AnimaDenoiseInvocation"] | components["schemas"]["AnimaImageToLatentsInvocation"] | components["schemas"]["AnimaLLLiteInvocation"] | components["schemas"]["AnimaLatentsToImageInvocation"] | components["schemas"]["AnimaLoRACollectionLoader"] | components["schemas"]["AnimaLoRALoaderInvocation"] | components["schemas"]["AnimaModelLoaderInvocation"] | components["schemas"]["AnimaTextEncoderInvocation"] | components["schemas"]["ApplyMaskTensorToImageInvocation"] | components["schemas"]["ApplyMaskToImageInvocation"] | components["schemas"]["BlankImageInvocation"] | components["schemas"]["BlendLatentsInvocation"] | components["schemas"]["BooleanCollectionInvocation"] | components["schemas"]["BooleanInvocation"] | components["schemas"]["BoundingBoxInvocation"] | components["schemas"]["CLIPSkipInvocation"] | components["schemas"]["CV2InfillInvocation"] | components["schemas"]["CalculateImageTilesEvenSplitInvocation"] | components["schemas"]["CalculateImageTilesInvocation"] | components["schemas"]["CalculateImageTilesMinimumOverlapInvocation"] | components["schemas"]["CallSavedWorkflowInvocation"] | components["schemas"]["CannyEdgeDetectionInvocation"] | components["schemas"]["CanvasOutputInvocation"] | components["schemas"]["CanvasPasteBackInvocation"] | components["schemas"]["CanvasV2MaskAndCropInvocation"] | components["schemas"]["CenterPadCropInvocation"] | components["schemas"]["CogView4DenoiseInvocation"] | components["schemas"]["CogView4ImageToLatentsInvocation"] | components["schemas"]["CogView4LatentsToImageInvocation"] | components["schemas"]["CogView4ModelLoaderInvocation"] | components["schemas"]["CogView4TextEncoderInvocation"] | components["schemas"]["CollectInvocation"] | components["schemas"]["ColorCorrectInvocation"] | components["schemas"]["ColorInvocation"] | components["schemas"]["ColorMapInvocation"] | components["schemas"]["CompelInvocation"] | components["schemas"]["ConditioningCollectionInvocation"] | components["schemas"]["ConditioningInvocation"] | components["schemas"]["ContentShuffleInvocation"] | components["schemas"]["ControlNetInvocation"] | components["schemas"]["CoreMetadataInvocation"] | components["schemas"]["CreateDenoiseMaskInvocation"] | components["schemas"]["CreateGradientMaskInvocation"] | components["schemas"]["CropImageToBoundingBoxInvocation"] | components["schemas"]["CropLatentsCoreInvocation"] | components["schemas"]["CvInpaintInvocation"] | components["schemas"]["DWOpenposeDetectionInvocation"] | components["schemas"]["DecodeInvisibleWatermarkInvocation"] | components["schemas"]["DenoiseLatentsInvocation"] | components["schemas"]["DenoiseLatentsMetaInvocation"] | components["schemas"]["DepthAnythingDepthEstimationInvocation"] | components["schemas"]["DivideInvocation"] | components["schemas"]["DynamicPromptInvocation"] | components["schemas"]["ESRGANInvocation"] | components["schemas"]["ExpandMaskWithFadeInvocation"] | components["schemas"]["ExtractVideoRangeInvocation"] | components["schemas"]["FLUXLoRACollectionLoader"] | components["schemas"]["FaceIdentifierInvocation"] | components["schemas"]["FaceMaskInvocation"] | components["schemas"]["FaceOffInvocation"] | components["schemas"]["FloatBatchInvocation"] | components["schemas"]["FloatCollectionInvocation"] | components["schemas"]["FloatGenerator"] | components["schemas"]["FloatInvocation"] | components["schemas"]["FloatLinearRangeInvocation"] | components["schemas"]["FloatMathInvocation"] | components["schemas"]["FloatToIntegerInvocation"] | components["schemas"]["Flux2DenoiseInvocation"] | components["schemas"]["Flux2KleinLoRACollectionLoader"] | components["schemas"]["Flux2KleinLoRALoaderInvocation"] | components["schemas"]["Flux2KleinModelLoaderInvocation"] | components["schemas"]["Flux2KleinTextEncoderInvocation"] | components["schemas"]["Flux2VaeDecodeInvocation"] | components["schemas"]["Flux2VaeEncodeInvocation"] | components["schemas"]["FluxControlLoRALoaderInvocation"] | components["schemas"]["FluxControlNetInvocation"] | components["schemas"]["FluxDenoiseInvocation"] | components["schemas"]["FluxDenoiseLatentsMetaInvocation"] | components["schemas"]["FluxFillInvocation"] | components["schemas"]["FluxIPAdapterInvocation"] | components["schemas"]["FluxKontextConcatenateImagesInvocation"] | components["schemas"]["FluxKontextInvocation"] | components["schemas"]["FluxLoRALoaderInvocation"] | components["schemas"]["FluxModelLoaderInvocation"] | components["schemas"]["FluxReduxInvocation"] | components["schemas"]["FluxTextEncoderInvocation"] | components["schemas"]["FluxVaeDecodeInvocation"] | components["schemas"]["FluxVaeEncodeInvocation"] | components["schemas"]["FreeUInvocation"] | components["schemas"]["GeminiImageGenerationInvocation"] | components["schemas"]["GetMaskBoundingBoxInvocation"] | components["schemas"]["GroundingDinoInvocation"] | components["schemas"]["HEDEdgeDetectionInvocation"] | components["schemas"]["HeuristicResizeInvocation"] | components["schemas"]["IPAdapterInvocation"] | components["schemas"]["IdealSizeInvocation"] | components["schemas"]["IfInvocation"] | components["schemas"]["ImageBatchInvocation"] | components["schemas"]["ImageBlurInvocation"] | components["schemas"]["ImageChannelInvocation"] | components["schemas"]["ImageChannelMultiplyInvocation"] | components["schemas"]["ImageChannelOffsetInvocation"] | components["schemas"]["ImageCollectionInvocation"] | components["schemas"]["ImageConvertInvocation"] | components["schemas"]["ImageCropInvocation"] | components["schemas"]["ImageGenerator"] | components["schemas"]["ImageHueAdjustmentInvocation"] | components["schemas"]["ImageInverseLerpInvocation"] | components["schemas"]["ImageInvocation"] | components["schemas"]["ImageLerpInvocation"] | components["schemas"]["ImageMaskToTensorInvocation"] | components["schemas"]["ImageMultiplyInvocation"] | components["schemas"]["ImageNSFWBlurInvocation"] | components["schemas"]["ImageNoiseInvocation"] | components["schemas"]["ImagePanelLayoutInvocation"] | components["schemas"]["ImagePasteInvocation"] | components["schemas"]["ImageResizeInvocation"] | components["schemas"]["ImageScaleInvocation"] | components["schemas"]["ImageToLatentsInvocation"] | components["schemas"]["ImageWatermarkInvocation"] | components["schemas"]["InfillColorInvocation"] | components["schemas"]["InfillPatchMatchInvocation"] | components["schemas"]["InfillTileInvocation"] | components["schemas"]["IntegerBatchInvocation"] | components["schemas"]["IntegerCollectionInvocation"] | components["schemas"]["IntegerGenerator"] | components["schemas"]["IntegerInvocation"] | components["schemas"]["IntegerMathInvocation"] | components["schemas"]["InvertTensorMaskInvocation"] | components["schemas"]["InvokeAdjustImageHuePlusInvocation"] | components["schemas"]["InvokeEquivalentAchromaticLightnessInvocation"] | components["schemas"]["InvokeImageBlendInvocation"] | components["schemas"]["InvokeImageCompositorInvocation"] | components["schemas"]["InvokeImageDilateOrErodeInvocation"] | components["schemas"]["InvokeImageEnhanceInvocation"] | components["schemas"]["InvokeImageValueThresholdsInvocation"] | components["schemas"]["IterateInvocation"] | components["schemas"]["LaMaInfillInvocation"] | components["schemas"]["LatentsCollectionInvocation"] | components["schemas"]["LatentsInvocation"] | components["schemas"]["LatentsToImageInvocation"] | components["schemas"]["LineartAnimeEdgeDetectionInvocation"] | components["schemas"]["LineartEdgeDetectionInvocation"] | components["schemas"]["LlavaOnevisionVllmInvocation"] | components["schemas"]["LoRACollectionLoader"] | components["schemas"]["LoRALoaderInvocation"] | components["schemas"]["LoRASelectorInvocation"] | components["schemas"]["MLSDDetectionInvocation"] | components["schemas"]["MainModelLoaderInvocation"] | components["schemas"]["MaskCombineInvocation"] | components["schemas"]["MaskEdgeInvocation"] | components["schemas"]["MaskFromAlphaInvocation"] | components["schemas"]["MaskFromIDInvocation"] | components["schemas"]["MaskTensorToImageInvocation"] | components["schemas"]["MediaPipeFaceDetectionInvocation"] | components["schemas"]["MergeMetadataInvocation"] | components["schemas"]["MergeTilesToImageInvocation"] | components["schemas"]["MetadataFieldExtractorInvocation"] | components["schemas"]["MetadataFromImageInvocation"] | components["schemas"]["MetadataInvocation"] | components["schemas"]["MetadataItemInvocation"] | components["schemas"]["MetadataItemLinkedInvocation"] | components["schemas"]["MetadataToBoolCollectionInvocation"] | components["schemas"]["MetadataToBoolInvocation"] | components["schemas"]["MetadataToControlnetsInvocation"] | components["schemas"]["MetadataToFloatCollectionInvocation"] | components["schemas"]["MetadataToFloatInvocation"] | components["schemas"]["MetadataToIPAdaptersInvocation"] | components["schemas"]["MetadataToIntegerCollectionInvocation"] | components["schemas"]["MetadataToIntegerInvocation"] | components["schemas"]["MetadataToLorasCollectionInvocation"] | components["schemas"]["MetadataToLorasInvocation"] | components["schemas"]["MetadataToModelInvocation"] | components["schemas"]["MetadataToSDXLLorasInvocation"] | components["schemas"]["MetadataToSDXLModelInvocation"] | components["schemas"]["MetadataToSchedulerInvocation"] | components["schemas"]["MetadataToStringCollectionInvocation"] | components["schemas"]["MetadataToStringInvocation"] | components["schemas"]["MetadataToT2IAdaptersInvocation"] | components["schemas"]["MetadataToVAEInvocation"] | components["schemas"]["ModelIdentifierInvocation"] | components["schemas"]["MultiplyInvocation"] | components["schemas"]["NoiseInvocation"] | components["schemas"]["NormalMapInvocation"] | components["schemas"]["OklabUnsharpMaskInvocation"] | components["schemas"]["OklchImageHueAdjustmentInvocation"] | components["schemas"]["OpenAIImageGenerationInvocation"] | components["schemas"]["PBRMapsInvocation"] | components["schemas"]["PairTileImageInvocation"] | components["schemas"]["PasteImageIntoBoundingBoxInvocation"] | components["schemas"]["PiDiNetEdgeDetectionInvocation"] | components["schemas"]["PromptTemplateInvocation"] | components["schemas"]["PromptsFromFileInvocation"] | components["schemas"]["QwenImageDenoiseInvocation"] | components["schemas"]["QwenImageImageToLatentsInvocation"] | components["schemas"]["QwenImageLatentsToImageInvocation"] | components["schemas"]["QwenImageLoRACollectionLoader"] | components["schemas"]["QwenImageLoRALoaderInvocation"] | components["schemas"]["QwenImageModelLoaderInvocation"] | components["schemas"]["QwenImageTextEncoderInvocation"] | components["schemas"]["RandomFloatInvocation"] | components["schemas"]["RandomIntInvocation"] | components["schemas"]["RandomRangeInvocation"] | components["schemas"]["RangeInvocation"] | components["schemas"]["RangeOfSizeInvocation"] | components["schemas"]["RectangleMaskInvocation"] | components["schemas"]["ResizeLatentsInvocation"] | components["schemas"]["RoundInvocation"] | components["schemas"]["SD3DenoiseInvocation"] | components["schemas"]["SD3ImageToLatentsInvocation"] | components["schemas"]["SD3LatentsToImageInvocation"] | components["schemas"]["SDXLCompelPromptInvocation"] | components["schemas"]["SDXLLoRACollectionLoader"] | components["schemas"]["SDXLLoRALoaderInvocation"] | components["schemas"]["SDXLModelLoaderInvocation"] | components["schemas"]["SDXLRefinerCompelPromptInvocation"] | components["schemas"]["SDXLRefinerModelLoaderInvocation"] | components["schemas"]["SaveImageInvocation"] | components["schemas"]["SaveImageToFileInvocation"] | components["schemas"]["ScaleLatentsInvocation"] | components["schemas"]["SchedulerInvocation"] | components["schemas"]["Sd3ModelLoaderInvocation"] | components["schemas"]["Sd3TextEncoderInvocation"] | components["schemas"]["SeamlessModeInvocation"] | components["schemas"]["SeedreamImageGenerationInvocation"] | components["schemas"]["SegmentAnythingInvocation"] | components["schemas"]["ShowImageInvocation"] | components["schemas"]["SpandrelImageToImageAutoscaleInvocation"] | components["schemas"]["SpandrelImageToImageInvocation"] | components["schemas"]["StringBatchInvocation"] | components["schemas"]["StringCollectionInvocation"] | components["schemas"]["StringGenerator"] | components["schemas"]["StringInvocation"] | components["schemas"]["StringJoinInvocation"] | components["schemas"]["StringJoinThreeInvocation"] | components["schemas"]["StringReplaceInvocation"] | components["schemas"]["StringSplitInvocation"] | components["schemas"]["StringSplitNegInvocation"] | components["schemas"]["SubtractInvocation"] | components["schemas"]["T2IAdapterInvocation"] | components["schemas"]["TextLLMInvocation"] | components["schemas"]["TileToPropertiesInvocation"] | components["schemas"]["TiledMultiDiffusionDenoiseLatents"] | components["schemas"]["UnsharpMaskInvocation"] | components["schemas"]["VAELoaderInvocation"] | components["schemas"]["VideoConcatInvocation"] | components["schemas"]["VideoFrameExtractInvocation"] | components["schemas"]["VideoInvocation"] | components["schemas"]["WanDenoiseInvocation"] | components["schemas"]["WanI2VIdealDimensionsInvocation"] | components["schemas"]["WanImageToLatentsInvocation"] | components["schemas"]["WanLatentsToImageInvocation"] | components["schemas"]["WanLatentsToVideoInvocation"] | components["schemas"]["WanLoRACollectionLoader"] | components["schemas"]["WanLoRALoaderInvocation"] | components["schemas"]["WanModelLoaderInvocation"] | components["schemas"]["WanRefImageEncoderInvocation"] | components["schemas"]["WanTI2VIdealDimensionsInvocation"] | components["schemas"]["WanTextEncoderInvocation"] | components["schemas"]["WanVideoDenoiseInvocation"] | components["schemas"]["WorkflowReturnGetInvocation"] | components["schemas"]["WorkflowReturnInvocation"] | components["schemas"]["WorkflowReturnValueInvocation"] | components["schemas"]["ZImageControlInvocation"] | components["schemas"]["ZImageDenoiseInvocation"] | components["schemas"]["ZImageDenoiseMetaInvocation"] | components["schemas"]["ZImageImageToLatentsInvocation"] | components["schemas"]["ZImageLatentsToImageInvocation"] | components["schemas"]["ZImageLoRACollectionLoader"] | components["schemas"]["ZImageLoRALoaderInvocation"] | components["schemas"]["ZImageModelLoaderInvocation"] | components["schemas"]["ZImageSeedVarianceEnhancerInvocation"] | components["schemas"]["ZImageTextEncoderInvocation"];
/**
* Invocation Source Id
* @description The ID of the prepared invocation's source node
@@ -16558,7 +17286,7 @@ export type components = {
* Invocation
* @description The ID of the invocation
*/
- invocation: components["schemas"]["AddInvocation"] | components["schemas"]["AlibabaCloudImageGenerationInvocation"] | components["schemas"]["AlphaMaskToTensorInvocation"] | components["schemas"]["AnimaDenoiseInvocation"] | components["schemas"]["AnimaImageToLatentsInvocation"] | components["schemas"]["AnimaLLLiteInvocation"] | components["schemas"]["AnimaLatentsToImageInvocation"] | components["schemas"]["AnimaLoRACollectionLoader"] | components["schemas"]["AnimaLoRALoaderInvocation"] | components["schemas"]["AnimaModelLoaderInvocation"] | components["schemas"]["AnimaTextEncoderInvocation"] | components["schemas"]["ApplyMaskTensorToImageInvocation"] | components["schemas"]["ApplyMaskToImageInvocation"] | components["schemas"]["BlankImageInvocation"] | components["schemas"]["BlendLatentsInvocation"] | components["schemas"]["BooleanCollectionInvocation"] | components["schemas"]["BooleanInvocation"] | components["schemas"]["BoundingBoxInvocation"] | components["schemas"]["CLIPSkipInvocation"] | components["schemas"]["CV2InfillInvocation"] | components["schemas"]["CalculateImageTilesEvenSplitInvocation"] | components["schemas"]["CalculateImageTilesInvocation"] | components["schemas"]["CalculateImageTilesMinimumOverlapInvocation"] | components["schemas"]["CallSavedWorkflowInvocation"] | components["schemas"]["CannyEdgeDetectionInvocation"] | components["schemas"]["CanvasOutputInvocation"] | components["schemas"]["CanvasPasteBackInvocation"] | components["schemas"]["CanvasV2MaskAndCropInvocation"] | components["schemas"]["CenterPadCropInvocation"] | components["schemas"]["CogView4DenoiseInvocation"] | components["schemas"]["CogView4ImageToLatentsInvocation"] | components["schemas"]["CogView4LatentsToImageInvocation"] | components["schemas"]["CogView4ModelLoaderInvocation"] | components["schemas"]["CogView4TextEncoderInvocation"] | components["schemas"]["CollectInvocation"] | components["schemas"]["ColorCorrectInvocation"] | components["schemas"]["ColorInvocation"] | components["schemas"]["ColorMapInvocation"] | components["schemas"]["CompelInvocation"] | components["schemas"]["ConditioningCollectionInvocation"] | components["schemas"]["ConditioningInvocation"] | components["schemas"]["ContentShuffleInvocation"] | components["schemas"]["ControlNetInvocation"] | components["schemas"]["CoreMetadataInvocation"] | components["schemas"]["CreateDenoiseMaskInvocation"] | components["schemas"]["CreateGradientMaskInvocation"] | components["schemas"]["CropImageToBoundingBoxInvocation"] | components["schemas"]["CropLatentsCoreInvocation"] | components["schemas"]["CvInpaintInvocation"] | components["schemas"]["DWOpenposeDetectionInvocation"] | components["schemas"]["DecodeInvisibleWatermarkInvocation"] | components["schemas"]["DenoiseLatentsInvocation"] | components["schemas"]["DenoiseLatentsMetaInvocation"] | components["schemas"]["DepthAnythingDepthEstimationInvocation"] | components["schemas"]["DivideInvocation"] | components["schemas"]["DynamicPromptInvocation"] | components["schemas"]["ESRGANInvocation"] | components["schemas"]["ExpandMaskWithFadeInvocation"] | components["schemas"]["FLUXLoRACollectionLoader"] | components["schemas"]["FaceIdentifierInvocation"] | components["schemas"]["FaceMaskInvocation"] | components["schemas"]["FaceOffInvocation"] | components["schemas"]["FloatBatchInvocation"] | components["schemas"]["FloatCollectionInvocation"] | components["schemas"]["FloatGenerator"] | components["schemas"]["FloatInvocation"] | components["schemas"]["FloatLinearRangeInvocation"] | components["schemas"]["FloatMathInvocation"] | components["schemas"]["FloatToIntegerInvocation"] | components["schemas"]["Flux2DenoiseInvocation"] | components["schemas"]["Flux2KleinLoRACollectionLoader"] | components["schemas"]["Flux2KleinLoRALoaderInvocation"] | components["schemas"]["Flux2KleinModelLoaderInvocation"] | components["schemas"]["Flux2KleinTextEncoderInvocation"] | components["schemas"]["Flux2VaeDecodeInvocation"] | components["schemas"]["Flux2VaeEncodeInvocation"] | components["schemas"]["FluxControlLoRALoaderInvocation"] | components["schemas"]["FluxControlNetInvocation"] | components["schemas"]["FluxDenoiseInvocation"] | components["schemas"]["FluxDenoiseLatentsMetaInvocation"] | components["schemas"]["FluxFillInvocation"] | components["schemas"]["FluxIPAdapterInvocation"] | components["schemas"]["FluxKontextConcatenateImagesInvocation"] | components["schemas"]["FluxKontextInvocation"] | components["schemas"]["FluxLoRALoaderInvocation"] | components["schemas"]["FluxModelLoaderInvocation"] | components["schemas"]["FluxReduxInvocation"] | components["schemas"]["FluxTextEncoderInvocation"] | components["schemas"]["FluxVaeDecodeInvocation"] | components["schemas"]["FluxVaeEncodeInvocation"] | components["schemas"]["FreeUInvocation"] | components["schemas"]["GeminiImageGenerationInvocation"] | components["schemas"]["GetMaskBoundingBoxInvocation"] | components["schemas"]["GroundingDinoInvocation"] | components["schemas"]["HEDEdgeDetectionInvocation"] | components["schemas"]["HeuristicResizeInvocation"] | components["schemas"]["IPAdapterInvocation"] | components["schemas"]["IdealSizeInvocation"] | components["schemas"]["IfInvocation"] | components["schemas"]["ImageBatchInvocation"] | components["schemas"]["ImageBlurInvocation"] | components["schemas"]["ImageChannelInvocation"] | components["schemas"]["ImageChannelMultiplyInvocation"] | components["schemas"]["ImageChannelOffsetInvocation"] | components["schemas"]["ImageCollectionInvocation"] | components["schemas"]["ImageConvertInvocation"] | components["schemas"]["ImageCropInvocation"] | components["schemas"]["ImageGenerator"] | components["schemas"]["ImageHueAdjustmentInvocation"] | components["schemas"]["ImageInverseLerpInvocation"] | components["schemas"]["ImageInvocation"] | components["schemas"]["ImageLerpInvocation"] | components["schemas"]["ImageMaskToTensorInvocation"] | components["schemas"]["ImageMultiplyInvocation"] | components["schemas"]["ImageNSFWBlurInvocation"] | components["schemas"]["ImageNoiseInvocation"] | components["schemas"]["ImagePanelLayoutInvocation"] | components["schemas"]["ImagePasteInvocation"] | components["schemas"]["ImageResizeInvocation"] | components["schemas"]["ImageScaleInvocation"] | components["schemas"]["ImageToLatentsInvocation"] | components["schemas"]["ImageWatermarkInvocation"] | components["schemas"]["InfillColorInvocation"] | components["schemas"]["InfillPatchMatchInvocation"] | components["schemas"]["InfillTileInvocation"] | components["schemas"]["IntegerBatchInvocation"] | components["schemas"]["IntegerCollectionInvocation"] | components["schemas"]["IntegerGenerator"] | components["schemas"]["IntegerInvocation"] | components["schemas"]["IntegerMathInvocation"] | components["schemas"]["InvertTensorMaskInvocation"] | components["schemas"]["InvokeAdjustImageHuePlusInvocation"] | components["schemas"]["InvokeEquivalentAchromaticLightnessInvocation"] | components["schemas"]["InvokeImageBlendInvocation"] | components["schemas"]["InvokeImageCompositorInvocation"] | components["schemas"]["InvokeImageDilateOrErodeInvocation"] | components["schemas"]["InvokeImageEnhanceInvocation"] | components["schemas"]["InvokeImageValueThresholdsInvocation"] | components["schemas"]["IterateInvocation"] | components["schemas"]["LaMaInfillInvocation"] | components["schemas"]["LatentsCollectionInvocation"] | components["schemas"]["LatentsInvocation"] | components["schemas"]["LatentsToImageInvocation"] | components["schemas"]["LineartAnimeEdgeDetectionInvocation"] | components["schemas"]["LineartEdgeDetectionInvocation"] | components["schemas"]["LlavaOnevisionVllmInvocation"] | components["schemas"]["LoRACollectionLoader"] | components["schemas"]["LoRALoaderInvocation"] | components["schemas"]["LoRASelectorInvocation"] | components["schemas"]["MLSDDetectionInvocation"] | components["schemas"]["MainModelLoaderInvocation"] | components["schemas"]["MaskCombineInvocation"] | components["schemas"]["MaskEdgeInvocation"] | components["schemas"]["MaskFromAlphaInvocation"] | components["schemas"]["MaskFromIDInvocation"] | components["schemas"]["MaskTensorToImageInvocation"] | components["schemas"]["MediaPipeFaceDetectionInvocation"] | components["schemas"]["MergeMetadataInvocation"] | components["schemas"]["MergeTilesToImageInvocation"] | components["schemas"]["MetadataFieldExtractorInvocation"] | components["schemas"]["MetadataFromImageInvocation"] | components["schemas"]["MetadataInvocation"] | components["schemas"]["MetadataItemInvocation"] | components["schemas"]["MetadataItemLinkedInvocation"] | components["schemas"]["MetadataToBoolCollectionInvocation"] | components["schemas"]["MetadataToBoolInvocation"] | components["schemas"]["MetadataToControlnetsInvocation"] | components["schemas"]["MetadataToFloatCollectionInvocation"] | components["schemas"]["MetadataToFloatInvocation"] | components["schemas"]["MetadataToIPAdaptersInvocation"] | components["schemas"]["MetadataToIntegerCollectionInvocation"] | components["schemas"]["MetadataToIntegerInvocation"] | components["schemas"]["MetadataToLorasCollectionInvocation"] | components["schemas"]["MetadataToLorasInvocation"] | components["schemas"]["MetadataToModelInvocation"] | components["schemas"]["MetadataToSDXLLorasInvocation"] | components["schemas"]["MetadataToSDXLModelInvocation"] | components["schemas"]["MetadataToSchedulerInvocation"] | components["schemas"]["MetadataToStringCollectionInvocation"] | components["schemas"]["MetadataToStringInvocation"] | components["schemas"]["MetadataToT2IAdaptersInvocation"] | components["schemas"]["MetadataToVAEInvocation"] | components["schemas"]["ModelIdentifierInvocation"] | components["schemas"]["MultiplyInvocation"] | components["schemas"]["NoiseInvocation"] | components["schemas"]["NormalMapInvocation"] | components["schemas"]["OklabUnsharpMaskInvocation"] | components["schemas"]["OklchImageHueAdjustmentInvocation"] | components["schemas"]["OpenAIImageGenerationInvocation"] | components["schemas"]["PBRMapsInvocation"] | components["schemas"]["PairTileImageInvocation"] | components["schemas"]["PasteImageIntoBoundingBoxInvocation"] | components["schemas"]["PiDiNetEdgeDetectionInvocation"] | components["schemas"]["PromptTemplateInvocation"] | components["schemas"]["PromptsFromFileInvocation"] | components["schemas"]["QwenImageDenoiseInvocation"] | components["schemas"]["QwenImageImageToLatentsInvocation"] | components["schemas"]["QwenImageLatentsToImageInvocation"] | components["schemas"]["QwenImageLoRACollectionLoader"] | components["schemas"]["QwenImageLoRALoaderInvocation"] | components["schemas"]["QwenImageModelLoaderInvocation"] | components["schemas"]["QwenImageTextEncoderInvocation"] | components["schemas"]["RandomFloatInvocation"] | components["schemas"]["RandomIntInvocation"] | components["schemas"]["RandomRangeInvocation"] | components["schemas"]["RangeInvocation"] | components["schemas"]["RangeOfSizeInvocation"] | components["schemas"]["RectangleMaskInvocation"] | components["schemas"]["ResizeLatentsInvocation"] | components["schemas"]["RoundInvocation"] | components["schemas"]["SD3DenoiseInvocation"] | components["schemas"]["SD3ImageToLatentsInvocation"] | components["schemas"]["SD3LatentsToImageInvocation"] | components["schemas"]["SDXLCompelPromptInvocation"] | components["schemas"]["SDXLLoRACollectionLoader"] | components["schemas"]["SDXLLoRALoaderInvocation"] | components["schemas"]["SDXLModelLoaderInvocation"] | components["schemas"]["SDXLRefinerCompelPromptInvocation"] | components["schemas"]["SDXLRefinerModelLoaderInvocation"] | components["schemas"]["SaveImageInvocation"] | components["schemas"]["SaveImageToFileInvocation"] | components["schemas"]["ScaleLatentsInvocation"] | components["schemas"]["SchedulerInvocation"] | components["schemas"]["Sd3ModelLoaderInvocation"] | components["schemas"]["Sd3TextEncoderInvocation"] | components["schemas"]["SeamlessModeInvocation"] | components["schemas"]["SeedreamImageGenerationInvocation"] | components["schemas"]["SegmentAnythingInvocation"] | components["schemas"]["ShowImageInvocation"] | components["schemas"]["SpandrelImageToImageAutoscaleInvocation"] | components["schemas"]["SpandrelImageToImageInvocation"] | components["schemas"]["StringBatchInvocation"] | components["schemas"]["StringCollectionInvocation"] | components["schemas"]["StringGenerator"] | components["schemas"]["StringInvocation"] | components["schemas"]["StringJoinInvocation"] | components["schemas"]["StringJoinThreeInvocation"] | components["schemas"]["StringReplaceInvocation"] | components["schemas"]["StringSplitInvocation"] | components["schemas"]["StringSplitNegInvocation"] | components["schemas"]["SubtractInvocation"] | components["schemas"]["T2IAdapterInvocation"] | components["schemas"]["TextLLMInvocation"] | components["schemas"]["TileToPropertiesInvocation"] | components["schemas"]["TiledMultiDiffusionDenoiseLatents"] | components["schemas"]["UnsharpMaskInvocation"] | components["schemas"]["VAELoaderInvocation"] | components["schemas"]["WorkflowReturnGetInvocation"] | components["schemas"]["WorkflowReturnInvocation"] | components["schemas"]["WorkflowReturnValueInvocation"] | components["schemas"]["ZImageControlInvocation"] | components["schemas"]["ZImageDenoiseInvocation"] | components["schemas"]["ZImageDenoiseMetaInvocation"] | components["schemas"]["ZImageImageToLatentsInvocation"] | components["schemas"]["ZImageLatentsToImageInvocation"] | components["schemas"]["ZImageLoRACollectionLoader"] | components["schemas"]["ZImageLoRALoaderInvocation"] | components["schemas"]["ZImageModelLoaderInvocation"] | components["schemas"]["ZImageSeedVarianceEnhancerInvocation"] | components["schemas"]["ZImageTextEncoderInvocation"];
+ invocation: components["schemas"]["AddInvocation"] | components["schemas"]["AlibabaCloudImageGenerationInvocation"] | components["schemas"]["AlphaMaskToTensorInvocation"] | components["schemas"]["AnimaDenoiseInvocation"] | components["schemas"]["AnimaImageToLatentsInvocation"] | components["schemas"]["AnimaLLLiteInvocation"] | components["schemas"]["AnimaLatentsToImageInvocation"] | components["schemas"]["AnimaLoRACollectionLoader"] | components["schemas"]["AnimaLoRALoaderInvocation"] | components["schemas"]["AnimaModelLoaderInvocation"] | components["schemas"]["AnimaTextEncoderInvocation"] | components["schemas"]["ApplyMaskTensorToImageInvocation"] | components["schemas"]["ApplyMaskToImageInvocation"] | components["schemas"]["BlankImageInvocation"] | components["schemas"]["BlendLatentsInvocation"] | components["schemas"]["BooleanCollectionInvocation"] | components["schemas"]["BooleanInvocation"] | components["schemas"]["BoundingBoxInvocation"] | components["schemas"]["CLIPSkipInvocation"] | components["schemas"]["CV2InfillInvocation"] | components["schemas"]["CalculateImageTilesEvenSplitInvocation"] | components["schemas"]["CalculateImageTilesInvocation"] | components["schemas"]["CalculateImageTilesMinimumOverlapInvocation"] | components["schemas"]["CallSavedWorkflowInvocation"] | components["schemas"]["CannyEdgeDetectionInvocation"] | components["schemas"]["CanvasOutputInvocation"] | components["schemas"]["CanvasPasteBackInvocation"] | components["schemas"]["CanvasV2MaskAndCropInvocation"] | components["schemas"]["CenterPadCropInvocation"] | components["schemas"]["CogView4DenoiseInvocation"] | components["schemas"]["CogView4ImageToLatentsInvocation"] | components["schemas"]["CogView4LatentsToImageInvocation"] | components["schemas"]["CogView4ModelLoaderInvocation"] | components["schemas"]["CogView4TextEncoderInvocation"] | components["schemas"]["CollectInvocation"] | components["schemas"]["ColorCorrectInvocation"] | components["schemas"]["ColorInvocation"] | components["schemas"]["ColorMapInvocation"] | components["schemas"]["CompelInvocation"] | components["schemas"]["ConditioningCollectionInvocation"] | components["schemas"]["ConditioningInvocation"] | components["schemas"]["ContentShuffleInvocation"] | components["schemas"]["ControlNetInvocation"] | components["schemas"]["CoreMetadataInvocation"] | components["schemas"]["CreateDenoiseMaskInvocation"] | components["schemas"]["CreateGradientMaskInvocation"] | components["schemas"]["CropImageToBoundingBoxInvocation"] | components["schemas"]["CropLatentsCoreInvocation"] | components["schemas"]["CvInpaintInvocation"] | components["schemas"]["DWOpenposeDetectionInvocation"] | components["schemas"]["DecodeInvisibleWatermarkInvocation"] | components["schemas"]["DenoiseLatentsInvocation"] | components["schemas"]["DenoiseLatentsMetaInvocation"] | components["schemas"]["DepthAnythingDepthEstimationInvocation"] | components["schemas"]["DivideInvocation"] | components["schemas"]["DynamicPromptInvocation"] | components["schemas"]["ESRGANInvocation"] | components["schemas"]["ExpandMaskWithFadeInvocation"] | components["schemas"]["ExtractVideoRangeInvocation"] | components["schemas"]["FLUXLoRACollectionLoader"] | components["schemas"]["FaceIdentifierInvocation"] | components["schemas"]["FaceMaskInvocation"] | components["schemas"]["FaceOffInvocation"] | components["schemas"]["FloatBatchInvocation"] | components["schemas"]["FloatCollectionInvocation"] | components["schemas"]["FloatGenerator"] | components["schemas"]["FloatInvocation"] | components["schemas"]["FloatLinearRangeInvocation"] | components["schemas"]["FloatMathInvocation"] | components["schemas"]["FloatToIntegerInvocation"] | components["schemas"]["Flux2DenoiseInvocation"] | components["schemas"]["Flux2KleinLoRACollectionLoader"] | components["schemas"]["Flux2KleinLoRALoaderInvocation"] | components["schemas"]["Flux2KleinModelLoaderInvocation"] | components["schemas"]["Flux2KleinTextEncoderInvocation"] | components["schemas"]["Flux2VaeDecodeInvocation"] | components["schemas"]["Flux2VaeEncodeInvocation"] | components["schemas"]["FluxControlLoRALoaderInvocation"] | components["schemas"]["FluxControlNetInvocation"] | components["schemas"]["FluxDenoiseInvocation"] | components["schemas"]["FluxDenoiseLatentsMetaInvocation"] | components["schemas"]["FluxFillInvocation"] | components["schemas"]["FluxIPAdapterInvocation"] | components["schemas"]["FluxKontextConcatenateImagesInvocation"] | components["schemas"]["FluxKontextInvocation"] | components["schemas"]["FluxLoRALoaderInvocation"] | components["schemas"]["FluxModelLoaderInvocation"] | components["schemas"]["FluxReduxInvocation"] | components["schemas"]["FluxTextEncoderInvocation"] | components["schemas"]["FluxVaeDecodeInvocation"] | components["schemas"]["FluxVaeEncodeInvocation"] | components["schemas"]["FreeUInvocation"] | components["schemas"]["GeminiImageGenerationInvocation"] | components["schemas"]["GetMaskBoundingBoxInvocation"] | components["schemas"]["GroundingDinoInvocation"] | components["schemas"]["HEDEdgeDetectionInvocation"] | components["schemas"]["HeuristicResizeInvocation"] | components["schemas"]["IPAdapterInvocation"] | components["schemas"]["IdealSizeInvocation"] | components["schemas"]["IfInvocation"] | components["schemas"]["ImageBatchInvocation"] | components["schemas"]["ImageBlurInvocation"] | components["schemas"]["ImageChannelInvocation"] | components["schemas"]["ImageChannelMultiplyInvocation"] | components["schemas"]["ImageChannelOffsetInvocation"] | components["schemas"]["ImageCollectionInvocation"] | components["schemas"]["ImageConvertInvocation"] | components["schemas"]["ImageCropInvocation"] | components["schemas"]["ImageGenerator"] | components["schemas"]["ImageHueAdjustmentInvocation"] | components["schemas"]["ImageInverseLerpInvocation"] | components["schemas"]["ImageInvocation"] | components["schemas"]["ImageLerpInvocation"] | components["schemas"]["ImageMaskToTensorInvocation"] | components["schemas"]["ImageMultiplyInvocation"] | components["schemas"]["ImageNSFWBlurInvocation"] | components["schemas"]["ImageNoiseInvocation"] | components["schemas"]["ImagePanelLayoutInvocation"] | components["schemas"]["ImagePasteInvocation"] | components["schemas"]["ImageResizeInvocation"] | components["schemas"]["ImageScaleInvocation"] | components["schemas"]["ImageToLatentsInvocation"] | components["schemas"]["ImageWatermarkInvocation"] | components["schemas"]["InfillColorInvocation"] | components["schemas"]["InfillPatchMatchInvocation"] | components["schemas"]["InfillTileInvocation"] | components["schemas"]["IntegerBatchInvocation"] | components["schemas"]["IntegerCollectionInvocation"] | components["schemas"]["IntegerGenerator"] | components["schemas"]["IntegerInvocation"] | components["schemas"]["IntegerMathInvocation"] | components["schemas"]["InvertTensorMaskInvocation"] | components["schemas"]["InvokeAdjustImageHuePlusInvocation"] | components["schemas"]["InvokeEquivalentAchromaticLightnessInvocation"] | components["schemas"]["InvokeImageBlendInvocation"] | components["schemas"]["InvokeImageCompositorInvocation"] | components["schemas"]["InvokeImageDilateOrErodeInvocation"] | components["schemas"]["InvokeImageEnhanceInvocation"] | components["schemas"]["InvokeImageValueThresholdsInvocation"] | components["schemas"]["IterateInvocation"] | components["schemas"]["LaMaInfillInvocation"] | components["schemas"]["LatentsCollectionInvocation"] | components["schemas"]["LatentsInvocation"] | components["schemas"]["LatentsToImageInvocation"] | components["schemas"]["LineartAnimeEdgeDetectionInvocation"] | components["schemas"]["LineartEdgeDetectionInvocation"] | components["schemas"]["LlavaOnevisionVllmInvocation"] | components["schemas"]["LoRACollectionLoader"] | components["schemas"]["LoRALoaderInvocation"] | components["schemas"]["LoRASelectorInvocation"] | components["schemas"]["MLSDDetectionInvocation"] | components["schemas"]["MainModelLoaderInvocation"] | components["schemas"]["MaskCombineInvocation"] | components["schemas"]["MaskEdgeInvocation"] | components["schemas"]["MaskFromAlphaInvocation"] | components["schemas"]["MaskFromIDInvocation"] | components["schemas"]["MaskTensorToImageInvocation"] | components["schemas"]["MediaPipeFaceDetectionInvocation"] | components["schemas"]["MergeMetadataInvocation"] | components["schemas"]["MergeTilesToImageInvocation"] | components["schemas"]["MetadataFieldExtractorInvocation"] | components["schemas"]["MetadataFromImageInvocation"] | components["schemas"]["MetadataInvocation"] | components["schemas"]["MetadataItemInvocation"] | components["schemas"]["MetadataItemLinkedInvocation"] | components["schemas"]["MetadataToBoolCollectionInvocation"] | components["schemas"]["MetadataToBoolInvocation"] | components["schemas"]["MetadataToControlnetsInvocation"] | components["schemas"]["MetadataToFloatCollectionInvocation"] | components["schemas"]["MetadataToFloatInvocation"] | components["schemas"]["MetadataToIPAdaptersInvocation"] | components["schemas"]["MetadataToIntegerCollectionInvocation"] | components["schemas"]["MetadataToIntegerInvocation"] | components["schemas"]["MetadataToLorasCollectionInvocation"] | components["schemas"]["MetadataToLorasInvocation"] | components["schemas"]["MetadataToModelInvocation"] | components["schemas"]["MetadataToSDXLLorasInvocation"] | components["schemas"]["MetadataToSDXLModelInvocation"] | components["schemas"]["MetadataToSchedulerInvocation"] | components["schemas"]["MetadataToStringCollectionInvocation"] | components["schemas"]["MetadataToStringInvocation"] | components["schemas"]["MetadataToT2IAdaptersInvocation"] | components["schemas"]["MetadataToVAEInvocation"] | components["schemas"]["ModelIdentifierInvocation"] | components["schemas"]["MultiplyInvocation"] | components["schemas"]["NoiseInvocation"] | components["schemas"]["NormalMapInvocation"] | components["schemas"]["OklabUnsharpMaskInvocation"] | components["schemas"]["OklchImageHueAdjustmentInvocation"] | components["schemas"]["OpenAIImageGenerationInvocation"] | components["schemas"]["PBRMapsInvocation"] | components["schemas"]["PairTileImageInvocation"] | components["schemas"]["PasteImageIntoBoundingBoxInvocation"] | components["schemas"]["PiDiNetEdgeDetectionInvocation"] | components["schemas"]["PromptTemplateInvocation"] | components["schemas"]["PromptsFromFileInvocation"] | components["schemas"]["QwenImageDenoiseInvocation"] | components["schemas"]["QwenImageImageToLatentsInvocation"] | components["schemas"]["QwenImageLatentsToImageInvocation"] | components["schemas"]["QwenImageLoRACollectionLoader"] | components["schemas"]["QwenImageLoRALoaderInvocation"] | components["schemas"]["QwenImageModelLoaderInvocation"] | components["schemas"]["QwenImageTextEncoderInvocation"] | components["schemas"]["RandomFloatInvocation"] | components["schemas"]["RandomIntInvocation"] | components["schemas"]["RandomRangeInvocation"] | components["schemas"]["RangeInvocation"] | components["schemas"]["RangeOfSizeInvocation"] | components["schemas"]["RectangleMaskInvocation"] | components["schemas"]["ResizeLatentsInvocation"] | components["schemas"]["RoundInvocation"] | components["schemas"]["SD3DenoiseInvocation"] | components["schemas"]["SD3ImageToLatentsInvocation"] | components["schemas"]["SD3LatentsToImageInvocation"] | components["schemas"]["SDXLCompelPromptInvocation"] | components["schemas"]["SDXLLoRACollectionLoader"] | components["schemas"]["SDXLLoRALoaderInvocation"] | components["schemas"]["SDXLModelLoaderInvocation"] | components["schemas"]["SDXLRefinerCompelPromptInvocation"] | components["schemas"]["SDXLRefinerModelLoaderInvocation"] | components["schemas"]["SaveImageInvocation"] | components["schemas"]["SaveImageToFileInvocation"] | components["schemas"]["ScaleLatentsInvocation"] | components["schemas"]["SchedulerInvocation"] | components["schemas"]["Sd3ModelLoaderInvocation"] | components["schemas"]["Sd3TextEncoderInvocation"] | components["schemas"]["SeamlessModeInvocation"] | components["schemas"]["SeedreamImageGenerationInvocation"] | components["schemas"]["SegmentAnythingInvocation"] | components["schemas"]["ShowImageInvocation"] | components["schemas"]["SpandrelImageToImageAutoscaleInvocation"] | components["schemas"]["SpandrelImageToImageInvocation"] | components["schemas"]["StringBatchInvocation"] | components["schemas"]["StringCollectionInvocation"] | components["schemas"]["StringGenerator"] | components["schemas"]["StringInvocation"] | components["schemas"]["StringJoinInvocation"] | components["schemas"]["StringJoinThreeInvocation"] | components["schemas"]["StringReplaceInvocation"] | components["schemas"]["StringSplitInvocation"] | components["schemas"]["StringSplitNegInvocation"] | components["schemas"]["SubtractInvocation"] | components["schemas"]["T2IAdapterInvocation"] | components["schemas"]["TextLLMInvocation"] | components["schemas"]["TileToPropertiesInvocation"] | components["schemas"]["TiledMultiDiffusionDenoiseLatents"] | components["schemas"]["UnsharpMaskInvocation"] | components["schemas"]["VAELoaderInvocation"] | components["schemas"]["VideoConcatInvocation"] | components["schemas"]["VideoFrameExtractInvocation"] | components["schemas"]["VideoInvocation"] | components["schemas"]["WanDenoiseInvocation"] | components["schemas"]["WanI2VIdealDimensionsInvocation"] | components["schemas"]["WanImageToLatentsInvocation"] | components["schemas"]["WanLatentsToImageInvocation"] | components["schemas"]["WanLatentsToVideoInvocation"] | components["schemas"]["WanLoRACollectionLoader"] | components["schemas"]["WanLoRALoaderInvocation"] | components["schemas"]["WanModelLoaderInvocation"] | components["schemas"]["WanRefImageEncoderInvocation"] | components["schemas"]["WanTI2VIdealDimensionsInvocation"] | components["schemas"]["WanTextEncoderInvocation"] | components["schemas"]["WanVideoDenoiseInvocation"] | components["schemas"]["WorkflowReturnGetInvocation"] | components["schemas"]["WorkflowReturnInvocation"] | components["schemas"]["WorkflowReturnValueInvocation"] | components["schemas"]["ZImageControlInvocation"] | components["schemas"]["ZImageDenoiseInvocation"] | components["schemas"]["ZImageDenoiseMetaInvocation"] | components["schemas"]["ZImageImageToLatentsInvocation"] | components["schemas"]["ZImageLatentsToImageInvocation"] | components["schemas"]["ZImageLoRACollectionLoader"] | components["schemas"]["ZImageLoRALoaderInvocation"] | components["schemas"]["ZImageModelLoaderInvocation"] | components["schemas"]["ZImageSeedVarianceEnhancerInvocation"] | components["schemas"]["ZImageTextEncoderInvocation"];
/**
* Invocation Source Id
* @description The ID of the prepared invocation's source node
@@ -19480,6 +20208,104 @@ export type components = {
*/
base: "sdxl";
};
+ /**
+ * LoRA_LyCORIS_Wan_Config
+ * @description Model config for Wan 2.2 LoRA models in LyCORIS format.
+ *
+ * Wan LoRAs target ``WanTransformer3DModel`` blocks. The Wan 2.2 A14B family
+ * is dual-expert (high-noise + low-noise) — LoRAs are typically trained
+ * against one expert. ``expert`` records which one so the model loader
+ * invocation can wire it to the correct ``loras`` / ``loras_low_noise`` list.
+ * Many LoRAs are expert-agnostic (TI2V-5B family, or community LoRAs that
+ * just don't tag the expert) — these get ``expert=None`` and are applied to
+ * both experts by default.
+ */
+ LoRA_LyCORIS_Wan_Config: {
+ /**
+ * Key
+ * @description A unique key for this model.
+ */
+ key: string;
+ /**
+ * Hash
+ * @description The hash of the model file(s).
+ */
+ hash: string;
+ /**
+ * Path
+ * @description Path to the model on the filesystem. Relative paths are relative to the Invoke root directory.
+ */
+ path: string;
+ /**
+ * File Size
+ * @description The size of the model in bytes.
+ */
+ file_size: number;
+ /**
+ * Name
+ * @description Name of the model.
+ */
+ name: string;
+ /**
+ * Description
+ * @description Model description
+ */
+ description: string | null;
+ /**
+ * Source
+ * @description The original source of the model (path, URL or repo_id).
+ */
+ source: string;
+ /** @description The type of source */
+ source_type: components["schemas"]["ModelSourceType"];
+ /**
+ * Source Api Response
+ * @description The original API response from the source, as stringified JSON.
+ */
+ source_api_response: string | null;
+ /**
+ * Source Url
+ * @description Optional URL for the model (e.g. download page or model page).
+ */
+ source_url: string | null;
+ /**
+ * Cover Image
+ * @description Url for image to preview model
+ */
+ cover_image: string | null;
+ /**
+ * Type
+ * @default lora
+ * @constant
+ */
+ type: "lora";
+ /**
+ * Trigger Phrases
+ * @description Set of trigger phrases for this model
+ */
+ trigger_phrases: string[] | null;
+ /** @description Default settings for this model */
+ default_settings: components["schemas"]["LoraModelDefaultSettings"] | null;
+ /**
+ * Format
+ * @default lycoris
+ * @constant
+ */
+ format: "lycoris";
+ /**
+ * Base
+ * @default wan
+ * @constant
+ */
+ base: "wan";
+ /**
+ * Expert
+ * @description For Wan 2.2 A14B dual-expert LoRAs: 'high' targets the high-noise expert, 'low' targets the low-noise expert. None means the LoRA is expert-agnostic (TI2V-5B, or community LoRAs without explicit tagging) and is applied to both.
+ */
+ expert: ("high" | "low") | null;
+ /** @description The Wan model family this LoRA targets, detected from its inner-dim (5120 -> A14B, 3072 -> TI2V-5B). A14B LoRAs are incompatible with TI2V-5B mains (and vice versa) — they crash with a shape mismatch in the layer patcher. The linear-view graph builder filters LoRAs on variant when building the LoRA collection. None means the LoRA's inner-dim couldn't be identified. */
+ variant: components["schemas"]["WanLoRAVariantType"] | null;
+ };
/**
* LoRA_LyCORIS_ZImage_Config
* @description Model config for Z-Image LoRA models in LyCORIS format.
@@ -21660,6 +22486,107 @@ export type components = {
*/
base: "sdxl";
};
+ /**
+ * Main_Diffusers_Wan_Config
+ * @description Model config for Wan 2.2 diffusers models.
+ *
+ * Covers both the dual-expert T2V-A14B family and the single-transformer TI2V-5B
+ * family. Variant is detected from the on-disk transformer config (latent channel
+ * count) plus the presence of a sibling ``transformer_2/`` directory.
+ */
+ Main_Diffusers_Wan_Config: {
+ /**
+ * Key
+ * @description A unique key for this model.
+ */
+ key: string;
+ /**
+ * Hash
+ * @description The hash of the model file(s).
+ */
+ hash: string;
+ /**
+ * Path
+ * @description Path to the model on the filesystem. Relative paths are relative to the Invoke root directory.
+ */
+ path: string;
+ /**
+ * File Size
+ * @description The size of the model in bytes.
+ */
+ file_size: number;
+ /**
+ * Name
+ * @description Name of the model.
+ */
+ name: string;
+ /**
+ * Description
+ * @description Model description
+ */
+ description: string | null;
+ /**
+ * Source
+ * @description The original source of the model (path, URL or repo_id).
+ */
+ source: string;
+ /** @description The type of source */
+ source_type: components["schemas"]["ModelSourceType"];
+ /**
+ * Source Api Response
+ * @description The original API response from the source, as stringified JSON.
+ */
+ source_api_response: string | null;
+ /**
+ * Source Url
+ * @description Optional URL for the model (e.g. download page or model page).
+ */
+ source_url: string | null;
+ /**
+ * Cover Image
+ * @description Url for image to preview model
+ */
+ cover_image: string | null;
+ /**
+ * Type
+ * @default main
+ * @constant
+ */
+ type: "main";
+ /**
+ * Trigger Phrases
+ * @description Set of trigger phrases for this model
+ */
+ trigger_phrases: string[] | null;
+ /** @description Default settings for this model */
+ default_settings: components["schemas"]["MainModelDefaultSettings"] | null;
+ /**
+ * Format
+ * @default diffusers
+ * @constant
+ */
+ format: "diffusers";
+ /** @default */
+ repo_variant: components["schemas"]["ModelRepoVariant"];
+ /**
+ * Base
+ * @default wan
+ * @constant
+ */
+ base: "wan";
+ variant: components["schemas"]["WanVariantType"];
+ /**
+ * Has Dual Expert
+ * @description Whether this model ships two transformer experts (Wan 2.2 A14B MoE). False for TI2V-5B.
+ * @default false
+ */
+ has_dual_expert: boolean;
+ /**
+ * Boundary Ratio
+ * @description MoE expert switch point as a fraction of num_train_timesteps (typically 1000). None for single-transformer models. Read from model_index.json by Diffusers' WanPipeline.
+ */
+ boundary_ratio: number | null;
+ };
/**
* Main_Diffusers_ZImage_Config
* @description Model config for Z-Image diffusers models (Z-Image-Turbo, Z-Image-Base).
@@ -22014,10 +22941,14 @@ export type components = {
variant: components["schemas"]["QwenImageVariantType"] | null;
};
/**
- * Main_GGUF_ZImage_Config
- * @description Model config for GGUF-quantized Z-Image transformer models.
+ * Main_GGUF_Wan_Config
+ * @description Model config for GGUF-quantized Wan 2.2 transformer models.
+ *
+ * A14B's MoE ships as two GGUF files (one per expert); ``expert`` records
+ * which one this is so the model loader invocation can pair them. TI2V-5B
+ * is a single-transformer model and stores ``expert='none'``.
*/
- Main_GGUF_ZImage_Config: {
+ Main_GGUF_Wan_Config: {
/**
* Key
* @description A unique key for this model.
@@ -22090,140 +23021,119 @@ export type components = {
config_path: string | null;
/**
* Base
- * @default z-image
+ * @default wan
* @constant
*/
- base: "z-image";
+ base: "wan";
/**
* Format
* @default gguf_quantized
* @constant
*/
format: "gguf_quantized";
- variant: components["schemas"]["ZImageVariantType"];
+ variant: components["schemas"]["WanVariantType"];
+ /**
+ * Expert
+ * @description For Wan 2.2 A14B's dual-expert MoE: 'high' for the high-noise expert, 'low' for the low-noise expert. 'none' for single-transformer models (TI2V-5B).
+ * @default none
+ * @enum {string}
+ */
+ expert: "high" | "low" | "none";
};
/**
- * Combine Masks
- * @description Combine two masks together by multiplying them using `PIL.ImageChops.multiply()`.
+ * Main_GGUF_ZImage_Config
+ * @description Model config for GGUF-quantized Z-Image transformer models.
*/
- MaskCombineInvocation: {
- /**
- * @description The board to save the image to
- * @default null
- */
- board?: components["schemas"]["BoardField"] | null;
- /**
- * @description Optional metadata to be saved with the image
- * @default null
- */
- metadata?: components["schemas"]["MetadataField"] | null;
- /**
- * Id
- * @description The id of this instance of an invocation. Must be unique among all instances of invocations.
- */
- id: string;
- /**
- * Is Intermediate
- * @description Whether or not this is an intermediate invocation.
- * @default false
- */
- is_intermediate?: boolean;
+ Main_GGUF_ZImage_Config: {
/**
- * Use Cache
- * @description Whether or not to use the cache
- * @default true
+ * Key
+ * @description A unique key for this model.
*/
- use_cache?: boolean;
+ key: string;
/**
- * @description The first mask to combine
- * @default null
+ * Hash
+ * @description The hash of the model file(s).
*/
- mask1?: components["schemas"]["ImageField"] | null;
+ hash: string;
/**
- * @description The second image to combine
- * @default null
+ * Path
+ * @description Path to the model on the filesystem. Relative paths are relative to the Invoke root directory.
*/
- mask2?: components["schemas"]["ImageField"] | null;
+ path: string;
/**
- * type
- * @default mask_combine
- * @constant
+ * File Size
+ * @description The size of the model in bytes.
*/
- type: "mask_combine";
- };
- /**
- * Mask Edge
- * @description Applies an edge mask to an image
- */
- MaskEdgeInvocation: {
+ file_size: number;
/**
- * @description The board to save the image to
- * @default null
+ * Name
+ * @description Name of the model.
*/
- board?: components["schemas"]["BoardField"] | null;
+ name: string;
/**
- * @description Optional metadata to be saved with the image
- * @default null
+ * Description
+ * @description Model description
*/
- metadata?: components["schemas"]["MetadataField"] | null;
+ description: string | null;
/**
- * Id
- * @description The id of this instance of an invocation. Must be unique among all instances of invocations.
+ * Source
+ * @description The original source of the model (path, URL or repo_id).
*/
- id: string;
+ source: string;
+ /** @description The type of source */
+ source_type: components["schemas"]["ModelSourceType"];
/**
- * Is Intermediate
- * @description Whether or not this is an intermediate invocation.
- * @default false
+ * Source Api Response
+ * @description The original API response from the source, as stringified JSON.
*/
- is_intermediate?: boolean;
+ source_api_response: string | null;
/**
- * Use Cache
- * @description Whether or not to use the cache
- * @default true
+ * Source Url
+ * @description Optional URL for the model (e.g. download page or model page).
*/
- use_cache?: boolean;
+ source_url: string | null;
/**
- * @description The image to apply the mask to
- * @default null
+ * Cover Image
+ * @description Url for image to preview model
*/
- image?: components["schemas"]["ImageField"] | null;
+ cover_image: string | null;
/**
- * Edge Size
- * @description The size of the edge
- * @default null
+ * Type
+ * @default main
+ * @constant
*/
- edge_size?: number | null;
+ type: "main";
/**
- * Edge Blur
- * @description The amount of blur on the edge
- * @default null
+ * Trigger Phrases
+ * @description Set of trigger phrases for this model
*/
- edge_blur?: number | null;
+ trigger_phrases: string[] | null;
+ /** @description Default settings for this model */
+ default_settings: components["schemas"]["MainModelDefaultSettings"] | null;
/**
- * Low Threshold
- * @description First threshold for the hysteresis procedure in Canny edge detection
- * @default null
+ * Config Path
+ * @description Path to the config for this model, if any.
*/
- low_threshold?: number | null;
+ config_path: string | null;
/**
- * High Threshold
- * @description Second threshold for the hysteresis procedure in Canny edge detection
- * @default null
+ * Base
+ * @default z-image
+ * @constant
*/
- high_threshold?: number | null;
+ base: "z-image";
/**
- * type
- * @default mask_edge
+ * Format
+ * @default gguf_quantized
* @constant
*/
- type: "mask_edge";
+ format: "gguf_quantized";
+ variant: components["schemas"]["ZImageVariantType"];
};
/**
- * Mask from Alpha
- * @description Extracts the alpha channel of an image as a mask.
+ * Combine Masks
+ * @description Combine two masks together by multiplying them using `PIL.ImageChops.multiply()`.
*/
- MaskFromAlphaInvocation: {
+ MaskCombineInvocation: {
/**
* @description The board to save the image to
* @default null
@@ -22252,28 +23162,145 @@ export type components = {
*/
use_cache?: boolean;
/**
- * @description The image to create the mask from
+ * @description The first mask to combine
* @default null
*/
- image?: components["schemas"]["ImageField"] | null;
+ mask1?: components["schemas"]["ImageField"] | null;
/**
- * Invert
- * @description Whether or not to invert the mask
- * @default false
+ * @description The second image to combine
+ * @default null
*/
- invert?: boolean;
+ mask2?: components["schemas"]["ImageField"] | null;
/**
* type
- * @default tomask
+ * @default mask_combine
* @constant
*/
- type: "tomask";
+ type: "mask_combine";
};
/**
- * Mask from Segmented Image
- * @description Generate a mask for a particular color in an ID Map
+ * Mask Edge
+ * @description Applies an edge mask to an image
*/
- MaskFromIDInvocation: {
+ MaskEdgeInvocation: {
+ /**
+ * @description The board to save the image to
+ * @default null
+ */
+ board?: components["schemas"]["BoardField"] | null;
+ /**
+ * @description Optional metadata to be saved with the image
+ * @default null
+ */
+ metadata?: components["schemas"]["MetadataField"] | null;
+ /**
+ * Id
+ * @description The id of this instance of an invocation. Must be unique among all instances of invocations.
+ */
+ id: string;
+ /**
+ * Is Intermediate
+ * @description Whether or not this is an intermediate invocation.
+ * @default false
+ */
+ is_intermediate?: boolean;
+ /**
+ * Use Cache
+ * @description Whether or not to use the cache
+ * @default true
+ */
+ use_cache?: boolean;
+ /**
+ * @description The image to apply the mask to
+ * @default null
+ */
+ image?: components["schemas"]["ImageField"] | null;
+ /**
+ * Edge Size
+ * @description The size of the edge
+ * @default null
+ */
+ edge_size?: number | null;
+ /**
+ * Edge Blur
+ * @description The amount of blur on the edge
+ * @default null
+ */
+ edge_blur?: number | null;
+ /**
+ * Low Threshold
+ * @description First threshold for the hysteresis procedure in Canny edge detection
+ * @default null
+ */
+ low_threshold?: number | null;
+ /**
+ * High Threshold
+ * @description Second threshold for the hysteresis procedure in Canny edge detection
+ * @default null
+ */
+ high_threshold?: number | null;
+ /**
+ * type
+ * @default mask_edge
+ * @constant
+ */
+ type: "mask_edge";
+ };
+ /**
+ * Mask from Alpha
+ * @description Extracts the alpha channel of an image as a mask.
+ */
+ MaskFromAlphaInvocation: {
+ /**
+ * @description The board to save the image to
+ * @default null
+ */
+ board?: components["schemas"]["BoardField"] | null;
+ /**
+ * @description Optional metadata to be saved with the image
+ * @default null
+ */
+ metadata?: components["schemas"]["MetadataField"] | null;
+ /**
+ * Id
+ * @description The id of this instance of an invocation. Must be unique among all instances of invocations.
+ */
+ id: string;
+ /**
+ * Is Intermediate
+ * @description Whether or not this is an intermediate invocation.
+ * @default false
+ */
+ is_intermediate?: boolean;
+ /**
+ * Use Cache
+ * @description Whether or not to use the cache
+ * @default true
+ */
+ use_cache?: boolean;
+ /**
+ * @description The image to create the mask from
+ * @default null
+ */
+ image?: components["schemas"]["ImageField"] | null;
+ /**
+ * Invert
+ * @description Whether or not to invert the mask
+ * @default false
+ */
+ invert?: boolean;
+ /**
+ * type
+ * @default tomask
+ * @constant
+ */
+ type: "tomask";
+ };
+ /**
+ * Mask from Segmented Image
+ * @description Generate a mask for a particular color in an ID Map
+ */
+ MaskFromIDInvocation: {
/**
* @description The board to save the image to
* @default null
@@ -22398,6 +23425,17 @@ export type components = {
*/
type: "tensor_mask_to_image";
};
+ /**
+ * MediaCookieResponse
+ * @description Response from refreshing the media cookie.
+ */
+ MediaCookieResponse: {
+ /**
+ * Success
+ * @description Whether the media cookie was set (always true in single-user mode)
+ */
+ success: boolean;
+ };
/**
* MediaPipe Face Detection
* @description Detects faces using MediaPipe.
@@ -23793,7 +24831,7 @@ export type components = {
* @description Storage format of model.
* @enum {string}
*/
- ModelFormat: "omi" | "diffusers" | "checkpoint" | "lycoris" | "onnx" | "olive" | "embedding_file" | "embedding_folder" | "invokeai" | "t5_encoder" | "qwen3_encoder" | "qwen_vl_encoder" | "bnb_quantized_int8b" | "bnb_quantized_nf4b" | "gguf_quantized" | "external_api" | "unknown";
+ ModelFormat: "omi" | "diffusers" | "checkpoint" | "lycoris" | "onnx" | "olive" | "embedding_file" | "embedding_folder" | "invokeai" | "t5_encoder" | "qwen3_encoder" | "qwen_vl_encoder" | "wan_t5_encoder" | "bnb_quantized_int8b" | "bnb_quantized_nf4b" | "gguf_quantized" | "external_api" | "unknown";
/** ModelIdentifierField */
ModelIdentifierField: {
/**
@@ -23930,7 +24968,7 @@ export type components = {
* Config
* @description The installed model's config
*/
- config: components["schemas"]["Main_Diffusers_SD1_Config"] | components["schemas"]["Main_Diffusers_SD2_Config"] | components["schemas"]["Main_Diffusers_SDXL_Config"] | components["schemas"]["Main_Diffusers_SDXLRefiner_Config"] | components["schemas"]["Main_Diffusers_SD3_Config"] | components["schemas"]["Main_Diffusers_FLUX_Config"] | components["schemas"]["Main_Diffusers_Flux2_Config"] | components["schemas"]["Main_Diffusers_CogView4_Config"] | components["schemas"]["Main_Diffusers_QwenImage_Config"] | components["schemas"]["Main_Diffusers_ZImage_Config"] | components["schemas"]["Main_Checkpoint_SD1_Config"] | components["schemas"]["Main_Checkpoint_SD2_Config"] | components["schemas"]["Main_Checkpoint_SDXL_Config"] | components["schemas"]["Main_Checkpoint_SDXLRefiner_Config"] | components["schemas"]["Main_Checkpoint_Flux2_Config"] | components["schemas"]["Main_Checkpoint_FLUX_Config"] | components["schemas"]["Main_Checkpoint_QwenImage_Config"] | components["schemas"]["Main_Checkpoint_ZImage_Config"] | components["schemas"]["Main_Checkpoint_Anima_Config"] | components["schemas"]["Main_BnBNF4_FLUX_Config"] | components["schemas"]["Main_GGUF_Flux2_Config"] | components["schemas"]["Main_GGUF_FLUX_Config"] | components["schemas"]["Main_GGUF_QwenImage_Config"] | components["schemas"]["Main_GGUF_ZImage_Config"] | components["schemas"]["VAE_Checkpoint_SD1_Config"] | components["schemas"]["VAE_Checkpoint_SD2_Config"] | components["schemas"]["VAE_Checkpoint_SDXL_Config"] | components["schemas"]["VAE_Checkpoint_FLUX_Config"] | components["schemas"]["VAE_Checkpoint_Flux2_Config"] | components["schemas"]["VAE_Checkpoint_QwenImage_Config"] | components["schemas"]["VAE_Checkpoint_Anima_Config"] | components["schemas"]["VAE_Diffusers_SD1_Config"] | components["schemas"]["VAE_Diffusers_SDXL_Config"] | components["schemas"]["VAE_Diffusers_Flux2_Config"] | components["schemas"]["ControlNet_Checkpoint_SD1_Config"] | components["schemas"]["ControlNet_Checkpoint_SD2_Config"] | components["schemas"]["ControlNet_Checkpoint_SDXL_Config"] | components["schemas"]["ControlNet_Checkpoint_FLUX_Config"] | components["schemas"]["ControlNet_Checkpoint_ZImage_Config"] | components["schemas"]["ControlNet_Checkpoint_Anima_Config"] | components["schemas"]["ControlNet_Diffusers_SD1_Config"] | components["schemas"]["ControlNet_Diffusers_SD2_Config"] | components["schemas"]["ControlNet_Diffusers_SDXL_Config"] | components["schemas"]["ControlNet_Diffusers_FLUX_Config"] | components["schemas"]["LoRA_LyCORIS_SD1_Config"] | components["schemas"]["LoRA_LyCORIS_SD2_Config"] | components["schemas"]["LoRA_LyCORIS_SDXL_Config"] | components["schemas"]["LoRA_LyCORIS_Flux2_Config"] | components["schemas"]["LoRA_LyCORIS_FLUX_Config"] | components["schemas"]["LoRA_LyCORIS_ZImage_Config"] | components["schemas"]["LoRA_LyCORIS_QwenImage_Config"] | components["schemas"]["LoRA_LyCORIS_Anima_Config"] | components["schemas"]["LoRA_OMI_SDXL_Config"] | components["schemas"]["LoRA_OMI_FLUX_Config"] | components["schemas"]["LoRA_Diffusers_SD1_Config"] | components["schemas"]["LoRA_Diffusers_SD2_Config"] | components["schemas"]["LoRA_Diffusers_SDXL_Config"] | components["schemas"]["LoRA_Diffusers_Flux2_Config"] | components["schemas"]["LoRA_Diffusers_FLUX_Config"] | components["schemas"]["LoRA_Diffusers_ZImage_Config"] | components["schemas"]["ControlLoRA_LyCORIS_FLUX_Config"] | components["schemas"]["T5Encoder_T5Encoder_Config"] | components["schemas"]["T5Encoder_BnBLLMint8_Config"] | components["schemas"]["Qwen3Encoder_Qwen3Encoder_Config"] | components["schemas"]["Qwen3Encoder_Checkpoint_Config"] | components["schemas"]["Qwen3Encoder_GGUF_Config"] | components["schemas"]["QwenVLEncoder_Diffusers_Config"] | components["schemas"]["QwenVLEncoder_Checkpoint_Config"] | components["schemas"]["TI_File_SD1_Config"] | components["schemas"]["TI_File_SD2_Config"] | components["schemas"]["TI_File_SDXL_Config"] | components["schemas"]["TI_Folder_SD1_Config"] | components["schemas"]["TI_Folder_SD2_Config"] | components["schemas"]["TI_Folder_SDXL_Config"] | components["schemas"]["IPAdapter_InvokeAI_SD1_Config"] | components["schemas"]["IPAdapter_InvokeAI_SD2_Config"] | components["schemas"]["IPAdapter_InvokeAI_SDXL_Config"] | components["schemas"]["IPAdapter_Checkpoint_SD1_Config"] | components["schemas"]["IPAdapter_Checkpoint_SD2_Config"] | components["schemas"]["IPAdapter_Checkpoint_SDXL_Config"] | components["schemas"]["IPAdapter_Checkpoint_FLUX_Config"] | components["schemas"]["T2IAdapter_Diffusers_SD1_Config"] | components["schemas"]["T2IAdapter_Diffusers_SDXL_Config"] | components["schemas"]["Spandrel_Checkpoint_Config"] | components["schemas"]["CLIPEmbed_Diffusers_G_Config"] | components["schemas"]["CLIPEmbed_Diffusers_L_Config"] | components["schemas"]["CLIPVision_Diffusers_Config"] | components["schemas"]["SigLIP_Diffusers_Config"] | components["schemas"]["FLUXRedux_Checkpoint_Config"] | components["schemas"]["LlavaOnevision_Diffusers_Config"] | components["schemas"]["TextLLM_Diffusers_Config"] | components["schemas"]["ExternalApiModelConfig"] | components["schemas"]["Unknown_Config"];
+ config: components["schemas"]["Main_Diffusers_SD1_Config"] | components["schemas"]["Main_Diffusers_SD2_Config"] | components["schemas"]["Main_Diffusers_SDXL_Config"] | components["schemas"]["Main_Diffusers_SDXLRefiner_Config"] | components["schemas"]["Main_Diffusers_SD3_Config"] | components["schemas"]["Main_Diffusers_FLUX_Config"] | components["schemas"]["Main_Diffusers_Flux2_Config"] | components["schemas"]["Main_Diffusers_CogView4_Config"] | components["schemas"]["Main_Diffusers_QwenImage_Config"] | components["schemas"]["Main_Diffusers_Wan_Config"] | components["schemas"]["Main_Diffusers_ZImage_Config"] | components["schemas"]["Main_Checkpoint_SD1_Config"] | components["schemas"]["Main_Checkpoint_SD2_Config"] | components["schemas"]["Main_Checkpoint_SDXL_Config"] | components["schemas"]["Main_Checkpoint_SDXLRefiner_Config"] | components["schemas"]["Main_Checkpoint_Flux2_Config"] | components["schemas"]["Main_Checkpoint_FLUX_Config"] | components["schemas"]["Main_Checkpoint_QwenImage_Config"] | components["schemas"]["Main_Checkpoint_ZImage_Config"] | components["schemas"]["Main_Checkpoint_Anima_Config"] | components["schemas"]["Main_BnBNF4_FLUX_Config"] | components["schemas"]["Main_GGUF_Flux2_Config"] | components["schemas"]["Main_GGUF_FLUX_Config"] | components["schemas"]["Main_GGUF_QwenImage_Config"] | components["schemas"]["Main_GGUF_Wan_Config"] | components["schemas"]["Main_GGUF_ZImage_Config"] | components["schemas"]["VAE_Checkpoint_SD1_Config"] | components["schemas"]["VAE_Checkpoint_SD2_Config"] | components["schemas"]["VAE_Checkpoint_SDXL_Config"] | components["schemas"]["VAE_Checkpoint_FLUX_Config"] | components["schemas"]["VAE_Checkpoint_Flux2_Config"] | components["schemas"]["VAE_Checkpoint_Wan_Config"] | components["schemas"]["VAE_Checkpoint_QwenImage_Config"] | components["schemas"]["VAE_Checkpoint_Anima_Config"] | components["schemas"]["VAE_Diffusers_SD1_Config"] | components["schemas"]["VAE_Diffusers_SDXL_Config"] | components["schemas"]["VAE_Diffusers_Flux2_Config"] | components["schemas"]["VAE_Diffusers_Wan_Config"] | components["schemas"]["ControlNet_Checkpoint_SD1_Config"] | components["schemas"]["ControlNet_Checkpoint_SD2_Config"] | components["schemas"]["ControlNet_Checkpoint_SDXL_Config"] | components["schemas"]["ControlNet_Checkpoint_FLUX_Config"] | components["schemas"]["ControlNet_Checkpoint_ZImage_Config"] | components["schemas"]["ControlNet_Checkpoint_Anima_Config"] | components["schemas"]["ControlNet_Diffusers_SD1_Config"] | components["schemas"]["ControlNet_Diffusers_SD2_Config"] | components["schemas"]["ControlNet_Diffusers_SDXL_Config"] | components["schemas"]["ControlNet_Diffusers_FLUX_Config"] | components["schemas"]["LoRA_LyCORIS_SD1_Config"] | components["schemas"]["LoRA_LyCORIS_SD2_Config"] | components["schemas"]["LoRA_LyCORIS_SDXL_Config"] | components["schemas"]["LoRA_LyCORIS_Flux2_Config"] | components["schemas"]["LoRA_LyCORIS_FLUX_Config"] | components["schemas"]["LoRA_LyCORIS_ZImage_Config"] | components["schemas"]["LoRA_LyCORIS_QwenImage_Config"] | components["schemas"]["LoRA_LyCORIS_Wan_Config"] | components["schemas"]["LoRA_LyCORIS_Anima_Config"] | components["schemas"]["LoRA_OMI_SDXL_Config"] | components["schemas"]["LoRA_OMI_FLUX_Config"] | components["schemas"]["LoRA_Diffusers_SD1_Config"] | components["schemas"]["LoRA_Diffusers_SD2_Config"] | components["schemas"]["LoRA_Diffusers_SDXL_Config"] | components["schemas"]["LoRA_Diffusers_Flux2_Config"] | components["schemas"]["LoRA_Diffusers_FLUX_Config"] | components["schemas"]["LoRA_Diffusers_ZImage_Config"] | components["schemas"]["ControlLoRA_LyCORIS_FLUX_Config"] | components["schemas"]["T5Encoder_T5Encoder_Config"] | components["schemas"]["T5Encoder_BnBLLMint8_Config"] | components["schemas"]["Qwen3Encoder_Qwen3Encoder_Config"] | components["schemas"]["Qwen3Encoder_Checkpoint_Config"] | components["schemas"]["Qwen3Encoder_GGUF_Config"] | components["schemas"]["QwenVLEncoder_Diffusers_Config"] | components["schemas"]["QwenVLEncoder_Checkpoint_Config"] | components["schemas"]["WanT5Encoder_WanT5Encoder_Config"] | components["schemas"]["TI_File_SD1_Config"] | components["schemas"]["TI_File_SD2_Config"] | components["schemas"]["TI_File_SDXL_Config"] | components["schemas"]["TI_Folder_SD1_Config"] | components["schemas"]["TI_Folder_SD2_Config"] | components["schemas"]["TI_Folder_SDXL_Config"] | components["schemas"]["IPAdapter_InvokeAI_SD1_Config"] | components["schemas"]["IPAdapter_InvokeAI_SD2_Config"] | components["schemas"]["IPAdapter_InvokeAI_SDXL_Config"] | components["schemas"]["IPAdapter_Checkpoint_SD1_Config"] | components["schemas"]["IPAdapter_Checkpoint_SD2_Config"] | components["schemas"]["IPAdapter_Checkpoint_SDXL_Config"] | components["schemas"]["IPAdapter_Checkpoint_FLUX_Config"] | components["schemas"]["T2IAdapter_Diffusers_SD1_Config"] | components["schemas"]["T2IAdapter_Diffusers_SDXL_Config"] | components["schemas"]["Spandrel_Checkpoint_Config"] | components["schemas"]["CLIPEmbed_Diffusers_G_Config"] | components["schemas"]["CLIPEmbed_Diffusers_L_Config"] | components["schemas"]["CLIPVision_Diffusers_Config"] | components["schemas"]["SigLIP_Diffusers_Config"] | components["schemas"]["FLUXRedux_Checkpoint_Config"] | components["schemas"]["LlavaOnevision_Diffusers_Config"] | components["schemas"]["TextLLM_Diffusers_Config"] | components["schemas"]["ExternalApiModelConfig"] | components["schemas"]["Unknown_Config"];
};
/**
* ModelInstallDownloadProgressEvent
@@ -24096,7 +25134,7 @@ export type components = {
* Config Out
* @description After successful installation, this will hold the configuration object.
*/
- config_out?: (components["schemas"]["Main_Diffusers_SD1_Config"] | components["schemas"]["Main_Diffusers_SD2_Config"] | components["schemas"]["Main_Diffusers_SDXL_Config"] | components["schemas"]["Main_Diffusers_SDXLRefiner_Config"] | components["schemas"]["Main_Diffusers_SD3_Config"] | components["schemas"]["Main_Diffusers_FLUX_Config"] | components["schemas"]["Main_Diffusers_Flux2_Config"] | components["schemas"]["Main_Diffusers_CogView4_Config"] | components["schemas"]["Main_Diffusers_QwenImage_Config"] | components["schemas"]["Main_Diffusers_ZImage_Config"] | components["schemas"]["Main_Checkpoint_SD1_Config"] | components["schemas"]["Main_Checkpoint_SD2_Config"] | components["schemas"]["Main_Checkpoint_SDXL_Config"] | components["schemas"]["Main_Checkpoint_SDXLRefiner_Config"] | components["schemas"]["Main_Checkpoint_Flux2_Config"] | components["schemas"]["Main_Checkpoint_FLUX_Config"] | components["schemas"]["Main_Checkpoint_QwenImage_Config"] | components["schemas"]["Main_Checkpoint_ZImage_Config"] | components["schemas"]["Main_Checkpoint_Anima_Config"] | components["schemas"]["Main_BnBNF4_FLUX_Config"] | components["schemas"]["Main_GGUF_Flux2_Config"] | components["schemas"]["Main_GGUF_FLUX_Config"] | components["schemas"]["Main_GGUF_QwenImage_Config"] | components["schemas"]["Main_GGUF_ZImage_Config"] | components["schemas"]["VAE_Checkpoint_SD1_Config"] | components["schemas"]["VAE_Checkpoint_SD2_Config"] | components["schemas"]["VAE_Checkpoint_SDXL_Config"] | components["schemas"]["VAE_Checkpoint_FLUX_Config"] | components["schemas"]["VAE_Checkpoint_Flux2_Config"] | components["schemas"]["VAE_Checkpoint_QwenImage_Config"] | components["schemas"]["VAE_Checkpoint_Anima_Config"] | components["schemas"]["VAE_Diffusers_SD1_Config"] | components["schemas"]["VAE_Diffusers_SDXL_Config"] | components["schemas"]["VAE_Diffusers_Flux2_Config"] | components["schemas"]["ControlNet_Checkpoint_SD1_Config"] | components["schemas"]["ControlNet_Checkpoint_SD2_Config"] | components["schemas"]["ControlNet_Checkpoint_SDXL_Config"] | components["schemas"]["ControlNet_Checkpoint_FLUX_Config"] | components["schemas"]["ControlNet_Checkpoint_ZImage_Config"] | components["schemas"]["ControlNet_Checkpoint_Anima_Config"] | components["schemas"]["ControlNet_Diffusers_SD1_Config"] | components["schemas"]["ControlNet_Diffusers_SD2_Config"] | components["schemas"]["ControlNet_Diffusers_SDXL_Config"] | components["schemas"]["ControlNet_Diffusers_FLUX_Config"] | components["schemas"]["LoRA_LyCORIS_SD1_Config"] | components["schemas"]["LoRA_LyCORIS_SD2_Config"] | components["schemas"]["LoRA_LyCORIS_SDXL_Config"] | components["schemas"]["LoRA_LyCORIS_Flux2_Config"] | components["schemas"]["LoRA_LyCORIS_FLUX_Config"] | components["schemas"]["LoRA_LyCORIS_ZImage_Config"] | components["schemas"]["LoRA_LyCORIS_QwenImage_Config"] | components["schemas"]["LoRA_LyCORIS_Anima_Config"] | components["schemas"]["LoRA_OMI_SDXL_Config"] | components["schemas"]["LoRA_OMI_FLUX_Config"] | components["schemas"]["LoRA_Diffusers_SD1_Config"] | components["schemas"]["LoRA_Diffusers_SD2_Config"] | components["schemas"]["LoRA_Diffusers_SDXL_Config"] | components["schemas"]["LoRA_Diffusers_Flux2_Config"] | components["schemas"]["LoRA_Diffusers_FLUX_Config"] | components["schemas"]["LoRA_Diffusers_ZImage_Config"] | components["schemas"]["ControlLoRA_LyCORIS_FLUX_Config"] | components["schemas"]["T5Encoder_T5Encoder_Config"] | components["schemas"]["T5Encoder_BnBLLMint8_Config"] | components["schemas"]["Qwen3Encoder_Qwen3Encoder_Config"] | components["schemas"]["Qwen3Encoder_Checkpoint_Config"] | components["schemas"]["Qwen3Encoder_GGUF_Config"] | components["schemas"]["QwenVLEncoder_Diffusers_Config"] | components["schemas"]["QwenVLEncoder_Checkpoint_Config"] | components["schemas"]["TI_File_SD1_Config"] | components["schemas"]["TI_File_SD2_Config"] | components["schemas"]["TI_File_SDXL_Config"] | components["schemas"]["TI_Folder_SD1_Config"] | components["schemas"]["TI_Folder_SD2_Config"] | components["schemas"]["TI_Folder_SDXL_Config"] | components["schemas"]["IPAdapter_InvokeAI_SD1_Config"] | components["schemas"]["IPAdapter_InvokeAI_SD2_Config"] | components["schemas"]["IPAdapter_InvokeAI_SDXL_Config"] | components["schemas"]["IPAdapter_Checkpoint_SD1_Config"] | components["schemas"]["IPAdapter_Checkpoint_SD2_Config"] | components["schemas"]["IPAdapter_Checkpoint_SDXL_Config"] | components["schemas"]["IPAdapter_Checkpoint_FLUX_Config"] | components["schemas"]["T2IAdapter_Diffusers_SD1_Config"] | components["schemas"]["T2IAdapter_Diffusers_SDXL_Config"] | components["schemas"]["Spandrel_Checkpoint_Config"] | components["schemas"]["CLIPEmbed_Diffusers_G_Config"] | components["schemas"]["CLIPEmbed_Diffusers_L_Config"] | components["schemas"]["CLIPVision_Diffusers_Config"] | components["schemas"]["SigLIP_Diffusers_Config"] | components["schemas"]["FLUXRedux_Checkpoint_Config"] | components["schemas"]["LlavaOnevision_Diffusers_Config"] | components["schemas"]["TextLLM_Diffusers_Config"] | components["schemas"]["ExternalApiModelConfig"] | components["schemas"]["Unknown_Config"]) | null;
+ config_out?: (components["schemas"]["Main_Diffusers_SD1_Config"] | components["schemas"]["Main_Diffusers_SD2_Config"] | components["schemas"]["Main_Diffusers_SDXL_Config"] | components["schemas"]["Main_Diffusers_SDXLRefiner_Config"] | components["schemas"]["Main_Diffusers_SD3_Config"] | components["schemas"]["Main_Diffusers_FLUX_Config"] | components["schemas"]["Main_Diffusers_Flux2_Config"] | components["schemas"]["Main_Diffusers_CogView4_Config"] | components["schemas"]["Main_Diffusers_QwenImage_Config"] | components["schemas"]["Main_Diffusers_Wan_Config"] | components["schemas"]["Main_Diffusers_ZImage_Config"] | components["schemas"]["Main_Checkpoint_SD1_Config"] | components["schemas"]["Main_Checkpoint_SD2_Config"] | components["schemas"]["Main_Checkpoint_SDXL_Config"] | components["schemas"]["Main_Checkpoint_SDXLRefiner_Config"] | components["schemas"]["Main_Checkpoint_Flux2_Config"] | components["schemas"]["Main_Checkpoint_FLUX_Config"] | components["schemas"]["Main_Checkpoint_QwenImage_Config"] | components["schemas"]["Main_Checkpoint_ZImage_Config"] | components["schemas"]["Main_Checkpoint_Anima_Config"] | components["schemas"]["Main_BnBNF4_FLUX_Config"] | components["schemas"]["Main_GGUF_Flux2_Config"] | components["schemas"]["Main_GGUF_FLUX_Config"] | components["schemas"]["Main_GGUF_QwenImage_Config"] | components["schemas"]["Main_GGUF_Wan_Config"] | components["schemas"]["Main_GGUF_ZImage_Config"] | components["schemas"]["VAE_Checkpoint_SD1_Config"] | components["schemas"]["VAE_Checkpoint_SD2_Config"] | components["schemas"]["VAE_Checkpoint_SDXL_Config"] | components["schemas"]["VAE_Checkpoint_FLUX_Config"] | components["schemas"]["VAE_Checkpoint_Flux2_Config"] | components["schemas"]["VAE_Checkpoint_Wan_Config"] | components["schemas"]["VAE_Checkpoint_QwenImage_Config"] | components["schemas"]["VAE_Checkpoint_Anima_Config"] | components["schemas"]["VAE_Diffusers_SD1_Config"] | components["schemas"]["VAE_Diffusers_SDXL_Config"] | components["schemas"]["VAE_Diffusers_Flux2_Config"] | components["schemas"]["VAE_Diffusers_Wan_Config"] | components["schemas"]["ControlNet_Checkpoint_SD1_Config"] | components["schemas"]["ControlNet_Checkpoint_SD2_Config"] | components["schemas"]["ControlNet_Checkpoint_SDXL_Config"] | components["schemas"]["ControlNet_Checkpoint_FLUX_Config"] | components["schemas"]["ControlNet_Checkpoint_ZImage_Config"] | components["schemas"]["ControlNet_Checkpoint_Anima_Config"] | components["schemas"]["ControlNet_Diffusers_SD1_Config"] | components["schemas"]["ControlNet_Diffusers_SD2_Config"] | components["schemas"]["ControlNet_Diffusers_SDXL_Config"] | components["schemas"]["ControlNet_Diffusers_FLUX_Config"] | components["schemas"]["LoRA_LyCORIS_SD1_Config"] | components["schemas"]["LoRA_LyCORIS_SD2_Config"] | components["schemas"]["LoRA_LyCORIS_SDXL_Config"] | components["schemas"]["LoRA_LyCORIS_Flux2_Config"] | components["schemas"]["LoRA_LyCORIS_FLUX_Config"] | components["schemas"]["LoRA_LyCORIS_ZImage_Config"] | components["schemas"]["LoRA_LyCORIS_QwenImage_Config"] | components["schemas"]["LoRA_LyCORIS_Wan_Config"] | components["schemas"]["LoRA_LyCORIS_Anima_Config"] | components["schemas"]["LoRA_OMI_SDXL_Config"] | components["schemas"]["LoRA_OMI_FLUX_Config"] | components["schemas"]["LoRA_Diffusers_SD1_Config"] | components["schemas"]["LoRA_Diffusers_SD2_Config"] | components["schemas"]["LoRA_Diffusers_SDXL_Config"] | components["schemas"]["LoRA_Diffusers_Flux2_Config"] | components["schemas"]["LoRA_Diffusers_FLUX_Config"] | components["schemas"]["LoRA_Diffusers_ZImage_Config"] | components["schemas"]["ControlLoRA_LyCORIS_FLUX_Config"] | components["schemas"]["T5Encoder_T5Encoder_Config"] | components["schemas"]["T5Encoder_BnBLLMint8_Config"] | components["schemas"]["Qwen3Encoder_Qwen3Encoder_Config"] | components["schemas"]["Qwen3Encoder_Checkpoint_Config"] | components["schemas"]["Qwen3Encoder_GGUF_Config"] | components["schemas"]["QwenVLEncoder_Diffusers_Config"] | components["schemas"]["QwenVLEncoder_Checkpoint_Config"] | components["schemas"]["WanT5Encoder_WanT5Encoder_Config"] | components["schemas"]["TI_File_SD1_Config"] | components["schemas"]["TI_File_SD2_Config"] | components["schemas"]["TI_File_SDXL_Config"] | components["schemas"]["TI_Folder_SD1_Config"] | components["schemas"]["TI_Folder_SD2_Config"] | components["schemas"]["TI_Folder_SDXL_Config"] | components["schemas"]["IPAdapter_InvokeAI_SD1_Config"] | components["schemas"]["IPAdapter_InvokeAI_SD2_Config"] | components["schemas"]["IPAdapter_InvokeAI_SDXL_Config"] | components["schemas"]["IPAdapter_Checkpoint_SD1_Config"] | components["schemas"]["IPAdapter_Checkpoint_SD2_Config"] | components["schemas"]["IPAdapter_Checkpoint_SDXL_Config"] | components["schemas"]["IPAdapter_Checkpoint_FLUX_Config"] | components["schemas"]["T2IAdapter_Diffusers_SD1_Config"] | components["schemas"]["T2IAdapter_Diffusers_SDXL_Config"] | components["schemas"]["Spandrel_Checkpoint_Config"] | components["schemas"]["CLIPEmbed_Diffusers_G_Config"] | components["schemas"]["CLIPEmbed_Diffusers_L_Config"] | components["schemas"]["CLIPVision_Diffusers_Config"] | components["schemas"]["SigLIP_Diffusers_Config"] | components["schemas"]["FLUXRedux_Checkpoint_Config"] | components["schemas"]["LlavaOnevision_Diffusers_Config"] | components["schemas"]["TextLLM_Diffusers_Config"] | components["schemas"]["ExternalApiModelConfig"] | components["schemas"]["Unknown_Config"]) | null;
/**
* Inplace
* @description Leave model in its current location; otherwise install under models directory
@@ -24182,7 +25220,7 @@ export type components = {
* Config
* @description The model's config
*/
- config: components["schemas"]["Main_Diffusers_SD1_Config"] | components["schemas"]["Main_Diffusers_SD2_Config"] | components["schemas"]["Main_Diffusers_SDXL_Config"] | components["schemas"]["Main_Diffusers_SDXLRefiner_Config"] | components["schemas"]["Main_Diffusers_SD3_Config"] | components["schemas"]["Main_Diffusers_FLUX_Config"] | components["schemas"]["Main_Diffusers_Flux2_Config"] | components["schemas"]["Main_Diffusers_CogView4_Config"] | components["schemas"]["Main_Diffusers_QwenImage_Config"] | components["schemas"]["Main_Diffusers_ZImage_Config"] | components["schemas"]["Main_Checkpoint_SD1_Config"] | components["schemas"]["Main_Checkpoint_SD2_Config"] | components["schemas"]["Main_Checkpoint_SDXL_Config"] | components["schemas"]["Main_Checkpoint_SDXLRefiner_Config"] | components["schemas"]["Main_Checkpoint_Flux2_Config"] | components["schemas"]["Main_Checkpoint_FLUX_Config"] | components["schemas"]["Main_Checkpoint_QwenImage_Config"] | components["schemas"]["Main_Checkpoint_ZImage_Config"] | components["schemas"]["Main_Checkpoint_Anima_Config"] | components["schemas"]["Main_BnBNF4_FLUX_Config"] | components["schemas"]["Main_GGUF_Flux2_Config"] | components["schemas"]["Main_GGUF_FLUX_Config"] | components["schemas"]["Main_GGUF_QwenImage_Config"] | components["schemas"]["Main_GGUF_ZImage_Config"] | components["schemas"]["VAE_Checkpoint_SD1_Config"] | components["schemas"]["VAE_Checkpoint_SD2_Config"] | components["schemas"]["VAE_Checkpoint_SDXL_Config"] | components["schemas"]["VAE_Checkpoint_FLUX_Config"] | components["schemas"]["VAE_Checkpoint_Flux2_Config"] | components["schemas"]["VAE_Checkpoint_QwenImage_Config"] | components["schemas"]["VAE_Checkpoint_Anima_Config"] | components["schemas"]["VAE_Diffusers_SD1_Config"] | components["schemas"]["VAE_Diffusers_SDXL_Config"] | components["schemas"]["VAE_Diffusers_Flux2_Config"] | components["schemas"]["ControlNet_Checkpoint_SD1_Config"] | components["schemas"]["ControlNet_Checkpoint_SD2_Config"] | components["schemas"]["ControlNet_Checkpoint_SDXL_Config"] | components["schemas"]["ControlNet_Checkpoint_FLUX_Config"] | components["schemas"]["ControlNet_Checkpoint_ZImage_Config"] | components["schemas"]["ControlNet_Checkpoint_Anima_Config"] | components["schemas"]["ControlNet_Diffusers_SD1_Config"] | components["schemas"]["ControlNet_Diffusers_SD2_Config"] | components["schemas"]["ControlNet_Diffusers_SDXL_Config"] | components["schemas"]["ControlNet_Diffusers_FLUX_Config"] | components["schemas"]["LoRA_LyCORIS_SD1_Config"] | components["schemas"]["LoRA_LyCORIS_SD2_Config"] | components["schemas"]["LoRA_LyCORIS_SDXL_Config"] | components["schemas"]["LoRA_LyCORIS_Flux2_Config"] | components["schemas"]["LoRA_LyCORIS_FLUX_Config"] | components["schemas"]["LoRA_LyCORIS_ZImage_Config"] | components["schemas"]["LoRA_LyCORIS_QwenImage_Config"] | components["schemas"]["LoRA_LyCORIS_Anima_Config"] | components["schemas"]["LoRA_OMI_SDXL_Config"] | components["schemas"]["LoRA_OMI_FLUX_Config"] | components["schemas"]["LoRA_Diffusers_SD1_Config"] | components["schemas"]["LoRA_Diffusers_SD2_Config"] | components["schemas"]["LoRA_Diffusers_SDXL_Config"] | components["schemas"]["LoRA_Diffusers_Flux2_Config"] | components["schemas"]["LoRA_Diffusers_FLUX_Config"] | components["schemas"]["LoRA_Diffusers_ZImage_Config"] | components["schemas"]["ControlLoRA_LyCORIS_FLUX_Config"] | components["schemas"]["T5Encoder_T5Encoder_Config"] | components["schemas"]["T5Encoder_BnBLLMint8_Config"] | components["schemas"]["Qwen3Encoder_Qwen3Encoder_Config"] | components["schemas"]["Qwen3Encoder_Checkpoint_Config"] | components["schemas"]["Qwen3Encoder_GGUF_Config"] | components["schemas"]["QwenVLEncoder_Diffusers_Config"] | components["schemas"]["QwenVLEncoder_Checkpoint_Config"] | components["schemas"]["TI_File_SD1_Config"] | components["schemas"]["TI_File_SD2_Config"] | components["schemas"]["TI_File_SDXL_Config"] | components["schemas"]["TI_Folder_SD1_Config"] | components["schemas"]["TI_Folder_SD2_Config"] | components["schemas"]["TI_Folder_SDXL_Config"] | components["schemas"]["IPAdapter_InvokeAI_SD1_Config"] | components["schemas"]["IPAdapter_InvokeAI_SD2_Config"] | components["schemas"]["IPAdapter_InvokeAI_SDXL_Config"] | components["schemas"]["IPAdapter_Checkpoint_SD1_Config"] | components["schemas"]["IPAdapter_Checkpoint_SD2_Config"] | components["schemas"]["IPAdapter_Checkpoint_SDXL_Config"] | components["schemas"]["IPAdapter_Checkpoint_FLUX_Config"] | components["schemas"]["T2IAdapter_Diffusers_SD1_Config"] | components["schemas"]["T2IAdapter_Diffusers_SDXL_Config"] | components["schemas"]["Spandrel_Checkpoint_Config"] | components["schemas"]["CLIPEmbed_Diffusers_G_Config"] | components["schemas"]["CLIPEmbed_Diffusers_L_Config"] | components["schemas"]["CLIPVision_Diffusers_Config"] | components["schemas"]["SigLIP_Diffusers_Config"] | components["schemas"]["FLUXRedux_Checkpoint_Config"] | components["schemas"]["LlavaOnevision_Diffusers_Config"] | components["schemas"]["TextLLM_Diffusers_Config"] | components["schemas"]["ExternalApiModelConfig"] | components["schemas"]["Unknown_Config"];
+ config: components["schemas"]["Main_Diffusers_SD1_Config"] | components["schemas"]["Main_Diffusers_SD2_Config"] | components["schemas"]["Main_Diffusers_SDXL_Config"] | components["schemas"]["Main_Diffusers_SDXLRefiner_Config"] | components["schemas"]["Main_Diffusers_SD3_Config"] | components["schemas"]["Main_Diffusers_FLUX_Config"] | components["schemas"]["Main_Diffusers_Flux2_Config"] | components["schemas"]["Main_Diffusers_CogView4_Config"] | components["schemas"]["Main_Diffusers_QwenImage_Config"] | components["schemas"]["Main_Diffusers_Wan_Config"] | components["schemas"]["Main_Diffusers_ZImage_Config"] | components["schemas"]["Main_Checkpoint_SD1_Config"] | components["schemas"]["Main_Checkpoint_SD2_Config"] | components["schemas"]["Main_Checkpoint_SDXL_Config"] | components["schemas"]["Main_Checkpoint_SDXLRefiner_Config"] | components["schemas"]["Main_Checkpoint_Flux2_Config"] | components["schemas"]["Main_Checkpoint_FLUX_Config"] | components["schemas"]["Main_Checkpoint_QwenImage_Config"] | components["schemas"]["Main_Checkpoint_ZImage_Config"] | components["schemas"]["Main_Checkpoint_Anima_Config"] | components["schemas"]["Main_BnBNF4_FLUX_Config"] | components["schemas"]["Main_GGUF_Flux2_Config"] | components["schemas"]["Main_GGUF_FLUX_Config"] | components["schemas"]["Main_GGUF_QwenImage_Config"] | components["schemas"]["Main_GGUF_Wan_Config"] | components["schemas"]["Main_GGUF_ZImage_Config"] | components["schemas"]["VAE_Checkpoint_SD1_Config"] | components["schemas"]["VAE_Checkpoint_SD2_Config"] | components["schemas"]["VAE_Checkpoint_SDXL_Config"] | components["schemas"]["VAE_Checkpoint_FLUX_Config"] | components["schemas"]["VAE_Checkpoint_Flux2_Config"] | components["schemas"]["VAE_Checkpoint_Wan_Config"] | components["schemas"]["VAE_Checkpoint_QwenImage_Config"] | components["schemas"]["VAE_Checkpoint_Anima_Config"] | components["schemas"]["VAE_Diffusers_SD1_Config"] | components["schemas"]["VAE_Diffusers_SDXL_Config"] | components["schemas"]["VAE_Diffusers_Flux2_Config"] | components["schemas"]["VAE_Diffusers_Wan_Config"] | components["schemas"]["ControlNet_Checkpoint_SD1_Config"] | components["schemas"]["ControlNet_Checkpoint_SD2_Config"] | components["schemas"]["ControlNet_Checkpoint_SDXL_Config"] | components["schemas"]["ControlNet_Checkpoint_FLUX_Config"] | components["schemas"]["ControlNet_Checkpoint_ZImage_Config"] | components["schemas"]["ControlNet_Checkpoint_Anima_Config"] | components["schemas"]["ControlNet_Diffusers_SD1_Config"] | components["schemas"]["ControlNet_Diffusers_SD2_Config"] | components["schemas"]["ControlNet_Diffusers_SDXL_Config"] | components["schemas"]["ControlNet_Diffusers_FLUX_Config"] | components["schemas"]["LoRA_LyCORIS_SD1_Config"] | components["schemas"]["LoRA_LyCORIS_SD2_Config"] | components["schemas"]["LoRA_LyCORIS_SDXL_Config"] | components["schemas"]["LoRA_LyCORIS_Flux2_Config"] | components["schemas"]["LoRA_LyCORIS_FLUX_Config"] | components["schemas"]["LoRA_LyCORIS_ZImage_Config"] | components["schemas"]["LoRA_LyCORIS_QwenImage_Config"] | components["schemas"]["LoRA_LyCORIS_Wan_Config"] | components["schemas"]["LoRA_LyCORIS_Anima_Config"] | components["schemas"]["LoRA_OMI_SDXL_Config"] | components["schemas"]["LoRA_OMI_FLUX_Config"] | components["schemas"]["LoRA_Diffusers_SD1_Config"] | components["schemas"]["LoRA_Diffusers_SD2_Config"] | components["schemas"]["LoRA_Diffusers_SDXL_Config"] | components["schemas"]["LoRA_Diffusers_Flux2_Config"] | components["schemas"]["LoRA_Diffusers_FLUX_Config"] | components["schemas"]["LoRA_Diffusers_ZImage_Config"] | components["schemas"]["ControlLoRA_LyCORIS_FLUX_Config"] | components["schemas"]["T5Encoder_T5Encoder_Config"] | components["schemas"]["T5Encoder_BnBLLMint8_Config"] | components["schemas"]["Qwen3Encoder_Qwen3Encoder_Config"] | components["schemas"]["Qwen3Encoder_Checkpoint_Config"] | components["schemas"]["Qwen3Encoder_GGUF_Config"] | components["schemas"]["QwenVLEncoder_Diffusers_Config"] | components["schemas"]["QwenVLEncoder_Checkpoint_Config"] | components["schemas"]["WanT5Encoder_WanT5Encoder_Config"] | components["schemas"]["TI_File_SD1_Config"] | components["schemas"]["TI_File_SD2_Config"] | components["schemas"]["TI_File_SDXL_Config"] | components["schemas"]["TI_Folder_SD1_Config"] | components["schemas"]["TI_Folder_SD2_Config"] | components["schemas"]["TI_Folder_SDXL_Config"] | components["schemas"]["IPAdapter_InvokeAI_SD1_Config"] | components["schemas"]["IPAdapter_InvokeAI_SD2_Config"] | components["schemas"]["IPAdapter_InvokeAI_SDXL_Config"] | components["schemas"]["IPAdapter_Checkpoint_SD1_Config"] | components["schemas"]["IPAdapter_Checkpoint_SD2_Config"] | components["schemas"]["IPAdapter_Checkpoint_SDXL_Config"] | components["schemas"]["IPAdapter_Checkpoint_FLUX_Config"] | components["schemas"]["T2IAdapter_Diffusers_SD1_Config"] | components["schemas"]["T2IAdapter_Diffusers_SDXL_Config"] | components["schemas"]["Spandrel_Checkpoint_Config"] | components["schemas"]["CLIPEmbed_Diffusers_G_Config"] | components["schemas"]["CLIPEmbed_Diffusers_L_Config"] | components["schemas"]["CLIPVision_Diffusers_Config"] | components["schemas"]["SigLIP_Diffusers_Config"] | components["schemas"]["FLUXRedux_Checkpoint_Config"] | components["schemas"]["LlavaOnevision_Diffusers_Config"] | components["schemas"]["TextLLM_Diffusers_Config"] | components["schemas"]["ExternalApiModelConfig"] | components["schemas"]["Unknown_Config"];
/**
* @description The submodel type, if any
* @default null
@@ -24209,7 +25247,7 @@ export type components = {
* Config
* @description The model's config
*/
- config: components["schemas"]["Main_Diffusers_SD1_Config"] | components["schemas"]["Main_Diffusers_SD2_Config"] | components["schemas"]["Main_Diffusers_SDXL_Config"] | components["schemas"]["Main_Diffusers_SDXLRefiner_Config"] | components["schemas"]["Main_Diffusers_SD3_Config"] | components["schemas"]["Main_Diffusers_FLUX_Config"] | components["schemas"]["Main_Diffusers_Flux2_Config"] | components["schemas"]["Main_Diffusers_CogView4_Config"] | components["schemas"]["Main_Diffusers_QwenImage_Config"] | components["schemas"]["Main_Diffusers_ZImage_Config"] | components["schemas"]["Main_Checkpoint_SD1_Config"] | components["schemas"]["Main_Checkpoint_SD2_Config"] | components["schemas"]["Main_Checkpoint_SDXL_Config"] | components["schemas"]["Main_Checkpoint_SDXLRefiner_Config"] | components["schemas"]["Main_Checkpoint_Flux2_Config"] | components["schemas"]["Main_Checkpoint_FLUX_Config"] | components["schemas"]["Main_Checkpoint_QwenImage_Config"] | components["schemas"]["Main_Checkpoint_ZImage_Config"] | components["schemas"]["Main_Checkpoint_Anima_Config"] | components["schemas"]["Main_BnBNF4_FLUX_Config"] | components["schemas"]["Main_GGUF_Flux2_Config"] | components["schemas"]["Main_GGUF_FLUX_Config"] | components["schemas"]["Main_GGUF_QwenImage_Config"] | components["schemas"]["Main_GGUF_ZImage_Config"] | components["schemas"]["VAE_Checkpoint_SD1_Config"] | components["schemas"]["VAE_Checkpoint_SD2_Config"] | components["schemas"]["VAE_Checkpoint_SDXL_Config"] | components["schemas"]["VAE_Checkpoint_FLUX_Config"] | components["schemas"]["VAE_Checkpoint_Flux2_Config"] | components["schemas"]["VAE_Checkpoint_QwenImage_Config"] | components["schemas"]["VAE_Checkpoint_Anima_Config"] | components["schemas"]["VAE_Diffusers_SD1_Config"] | components["schemas"]["VAE_Diffusers_SDXL_Config"] | components["schemas"]["VAE_Diffusers_Flux2_Config"] | components["schemas"]["ControlNet_Checkpoint_SD1_Config"] | components["schemas"]["ControlNet_Checkpoint_SD2_Config"] | components["schemas"]["ControlNet_Checkpoint_SDXL_Config"] | components["schemas"]["ControlNet_Checkpoint_FLUX_Config"] | components["schemas"]["ControlNet_Checkpoint_ZImage_Config"] | components["schemas"]["ControlNet_Checkpoint_Anima_Config"] | components["schemas"]["ControlNet_Diffusers_SD1_Config"] | components["schemas"]["ControlNet_Diffusers_SD2_Config"] | components["schemas"]["ControlNet_Diffusers_SDXL_Config"] | components["schemas"]["ControlNet_Diffusers_FLUX_Config"] | components["schemas"]["LoRA_LyCORIS_SD1_Config"] | components["schemas"]["LoRA_LyCORIS_SD2_Config"] | components["schemas"]["LoRA_LyCORIS_SDXL_Config"] | components["schemas"]["LoRA_LyCORIS_Flux2_Config"] | components["schemas"]["LoRA_LyCORIS_FLUX_Config"] | components["schemas"]["LoRA_LyCORIS_ZImage_Config"] | components["schemas"]["LoRA_LyCORIS_QwenImage_Config"] | components["schemas"]["LoRA_LyCORIS_Anima_Config"] | components["schemas"]["LoRA_OMI_SDXL_Config"] | components["schemas"]["LoRA_OMI_FLUX_Config"] | components["schemas"]["LoRA_Diffusers_SD1_Config"] | components["schemas"]["LoRA_Diffusers_SD2_Config"] | components["schemas"]["LoRA_Diffusers_SDXL_Config"] | components["schemas"]["LoRA_Diffusers_Flux2_Config"] | components["schemas"]["LoRA_Diffusers_FLUX_Config"] | components["schemas"]["LoRA_Diffusers_ZImage_Config"] | components["schemas"]["ControlLoRA_LyCORIS_FLUX_Config"] | components["schemas"]["T5Encoder_T5Encoder_Config"] | components["schemas"]["T5Encoder_BnBLLMint8_Config"] | components["schemas"]["Qwen3Encoder_Qwen3Encoder_Config"] | components["schemas"]["Qwen3Encoder_Checkpoint_Config"] | components["schemas"]["Qwen3Encoder_GGUF_Config"] | components["schemas"]["QwenVLEncoder_Diffusers_Config"] | components["schemas"]["QwenVLEncoder_Checkpoint_Config"] | components["schemas"]["TI_File_SD1_Config"] | components["schemas"]["TI_File_SD2_Config"] | components["schemas"]["TI_File_SDXL_Config"] | components["schemas"]["TI_Folder_SD1_Config"] | components["schemas"]["TI_Folder_SD2_Config"] | components["schemas"]["TI_Folder_SDXL_Config"] | components["schemas"]["IPAdapter_InvokeAI_SD1_Config"] | components["schemas"]["IPAdapter_InvokeAI_SD2_Config"] | components["schemas"]["IPAdapter_InvokeAI_SDXL_Config"] | components["schemas"]["IPAdapter_Checkpoint_SD1_Config"] | components["schemas"]["IPAdapter_Checkpoint_SD2_Config"] | components["schemas"]["IPAdapter_Checkpoint_SDXL_Config"] | components["schemas"]["IPAdapter_Checkpoint_FLUX_Config"] | components["schemas"]["T2IAdapter_Diffusers_SD1_Config"] | components["schemas"]["T2IAdapter_Diffusers_SDXL_Config"] | components["schemas"]["Spandrel_Checkpoint_Config"] | components["schemas"]["CLIPEmbed_Diffusers_G_Config"] | components["schemas"]["CLIPEmbed_Diffusers_L_Config"] | components["schemas"]["CLIPVision_Diffusers_Config"] | components["schemas"]["SigLIP_Diffusers_Config"] | components["schemas"]["FLUXRedux_Checkpoint_Config"] | components["schemas"]["LlavaOnevision_Diffusers_Config"] | components["schemas"]["TextLLM_Diffusers_Config"] | components["schemas"]["ExternalApiModelConfig"] | components["schemas"]["Unknown_Config"];
+ config: components["schemas"]["Main_Diffusers_SD1_Config"] | components["schemas"]["Main_Diffusers_SD2_Config"] | components["schemas"]["Main_Diffusers_SDXL_Config"] | components["schemas"]["Main_Diffusers_SDXLRefiner_Config"] | components["schemas"]["Main_Diffusers_SD3_Config"] | components["schemas"]["Main_Diffusers_FLUX_Config"] | components["schemas"]["Main_Diffusers_Flux2_Config"] | components["schemas"]["Main_Diffusers_CogView4_Config"] | components["schemas"]["Main_Diffusers_QwenImage_Config"] | components["schemas"]["Main_Diffusers_Wan_Config"] | components["schemas"]["Main_Diffusers_ZImage_Config"] | components["schemas"]["Main_Checkpoint_SD1_Config"] | components["schemas"]["Main_Checkpoint_SD2_Config"] | components["schemas"]["Main_Checkpoint_SDXL_Config"] | components["schemas"]["Main_Checkpoint_SDXLRefiner_Config"] | components["schemas"]["Main_Checkpoint_Flux2_Config"] | components["schemas"]["Main_Checkpoint_FLUX_Config"] | components["schemas"]["Main_Checkpoint_QwenImage_Config"] | components["schemas"]["Main_Checkpoint_ZImage_Config"] | components["schemas"]["Main_Checkpoint_Anima_Config"] | components["schemas"]["Main_BnBNF4_FLUX_Config"] | components["schemas"]["Main_GGUF_Flux2_Config"] | components["schemas"]["Main_GGUF_FLUX_Config"] | components["schemas"]["Main_GGUF_QwenImage_Config"] | components["schemas"]["Main_GGUF_Wan_Config"] | components["schemas"]["Main_GGUF_ZImage_Config"] | components["schemas"]["VAE_Checkpoint_SD1_Config"] | components["schemas"]["VAE_Checkpoint_SD2_Config"] | components["schemas"]["VAE_Checkpoint_SDXL_Config"] | components["schemas"]["VAE_Checkpoint_FLUX_Config"] | components["schemas"]["VAE_Checkpoint_Flux2_Config"] | components["schemas"]["VAE_Checkpoint_Wan_Config"] | components["schemas"]["VAE_Checkpoint_QwenImage_Config"] | components["schemas"]["VAE_Checkpoint_Anima_Config"] | components["schemas"]["VAE_Diffusers_SD1_Config"] | components["schemas"]["VAE_Diffusers_SDXL_Config"] | components["schemas"]["VAE_Diffusers_Flux2_Config"] | components["schemas"]["VAE_Diffusers_Wan_Config"] | components["schemas"]["ControlNet_Checkpoint_SD1_Config"] | components["schemas"]["ControlNet_Checkpoint_SD2_Config"] | components["schemas"]["ControlNet_Checkpoint_SDXL_Config"] | components["schemas"]["ControlNet_Checkpoint_FLUX_Config"] | components["schemas"]["ControlNet_Checkpoint_ZImage_Config"] | components["schemas"]["ControlNet_Checkpoint_Anima_Config"] | components["schemas"]["ControlNet_Diffusers_SD1_Config"] | components["schemas"]["ControlNet_Diffusers_SD2_Config"] | components["schemas"]["ControlNet_Diffusers_SDXL_Config"] | components["schemas"]["ControlNet_Diffusers_FLUX_Config"] | components["schemas"]["LoRA_LyCORIS_SD1_Config"] | components["schemas"]["LoRA_LyCORIS_SD2_Config"] | components["schemas"]["LoRA_LyCORIS_SDXL_Config"] | components["schemas"]["LoRA_LyCORIS_Flux2_Config"] | components["schemas"]["LoRA_LyCORIS_FLUX_Config"] | components["schemas"]["LoRA_LyCORIS_ZImage_Config"] | components["schemas"]["LoRA_LyCORIS_QwenImage_Config"] | components["schemas"]["LoRA_LyCORIS_Wan_Config"] | components["schemas"]["LoRA_LyCORIS_Anima_Config"] | components["schemas"]["LoRA_OMI_SDXL_Config"] | components["schemas"]["LoRA_OMI_FLUX_Config"] | components["schemas"]["LoRA_Diffusers_SD1_Config"] | components["schemas"]["LoRA_Diffusers_SD2_Config"] | components["schemas"]["LoRA_Diffusers_SDXL_Config"] | components["schemas"]["LoRA_Diffusers_Flux2_Config"] | components["schemas"]["LoRA_Diffusers_FLUX_Config"] | components["schemas"]["LoRA_Diffusers_ZImage_Config"] | components["schemas"]["ControlLoRA_LyCORIS_FLUX_Config"] | components["schemas"]["T5Encoder_T5Encoder_Config"] | components["schemas"]["T5Encoder_BnBLLMint8_Config"] | components["schemas"]["Qwen3Encoder_Qwen3Encoder_Config"] | components["schemas"]["Qwen3Encoder_Checkpoint_Config"] | components["schemas"]["Qwen3Encoder_GGUF_Config"] | components["schemas"]["QwenVLEncoder_Diffusers_Config"] | components["schemas"]["QwenVLEncoder_Checkpoint_Config"] | components["schemas"]["WanT5Encoder_WanT5Encoder_Config"] | components["schemas"]["TI_File_SD1_Config"] | components["schemas"]["TI_File_SD2_Config"] | components["schemas"]["TI_File_SDXL_Config"] | components["schemas"]["TI_Folder_SD1_Config"] | components["schemas"]["TI_Folder_SD2_Config"] | components["schemas"]["TI_Folder_SDXL_Config"] | components["schemas"]["IPAdapter_InvokeAI_SD1_Config"] | components["schemas"]["IPAdapter_InvokeAI_SD2_Config"] | components["schemas"]["IPAdapter_InvokeAI_SDXL_Config"] | components["schemas"]["IPAdapter_Checkpoint_SD1_Config"] | components["schemas"]["IPAdapter_Checkpoint_SD2_Config"] | components["schemas"]["IPAdapter_Checkpoint_SDXL_Config"] | components["schemas"]["IPAdapter_Checkpoint_FLUX_Config"] | components["schemas"]["T2IAdapter_Diffusers_SD1_Config"] | components["schemas"]["T2IAdapter_Diffusers_SDXL_Config"] | components["schemas"]["Spandrel_Checkpoint_Config"] | components["schemas"]["CLIPEmbed_Diffusers_G_Config"] | components["schemas"]["CLIPEmbed_Diffusers_L_Config"] | components["schemas"]["CLIPVision_Diffusers_Config"] | components["schemas"]["SigLIP_Diffusers_Config"] | components["schemas"]["FLUXRedux_Checkpoint_Config"] | components["schemas"]["LlavaOnevision_Diffusers_Config"] | components["schemas"]["TextLLM_Diffusers_Config"] | components["schemas"]["ExternalApiModelConfig"] | components["schemas"]["Unknown_Config"];
/**
* @description The submodel type, if any
* @default null
@@ -24341,7 +25379,7 @@ export type components = {
* Variant
* @description The variant of the model.
*/
- variant?: components["schemas"]["ModelVariantType"] | components["schemas"]["ClipVariantType"] | components["schemas"]["FluxVariantType"] | components["schemas"]["Flux2VariantType"] | components["schemas"]["ZImageVariantType"] | components["schemas"]["QwenImageVariantType"] | components["schemas"]["Qwen3VariantType"] | null;
+ variant?: components["schemas"]["ModelVariantType"] | components["schemas"]["ClipVariantType"] | components["schemas"]["FluxVariantType"] | components["schemas"]["Flux2VariantType"] | components["schemas"]["ZImageVariantType"] | components["schemas"]["QwenImageVariantType"] | components["schemas"]["WanVariantType"] | components["schemas"]["WanLoRAVariantType"] | components["schemas"]["Qwen3VariantType"] | null;
/** @description The prediction type of the model. */
prediction_type?: components["schemas"]["SchedulerPredictionType"] | null;
/**
@@ -24423,7 +25461,7 @@ export type components = {
* @description Model type.
* @enum {string}
*/
- ModelType: "onnx" | "main" | "vae" | "lora" | "control_lora" | "controlnet" | "embedding" | "ip_adapter" | "clip_vision" | "clip_embed" | "t2i_adapter" | "t5_encoder" | "qwen3_encoder" | "qwen_vl_encoder" | "spandrel_image_to_image" | "siglip" | "flux_redux" | "llava_onevision" | "text_llm" | "external_image_generator" | "unknown";
+ ModelType: "onnx" | "main" | "vae" | "lora" | "control_lora" | "controlnet" | "embedding" | "ip_adapter" | "clip_vision" | "clip_embed" | "t2i_adapter" | "t5_encoder" | "qwen3_encoder" | "qwen_vl_encoder" | "wan_t5_encoder" | "spandrel_image_to_image" | "siglip" | "flux_redux" | "llava_onevision" | "text_llm" | "external_image_generator" | "unknown";
/**
* ModelVariantType
* @description Variant type.
@@ -24436,7 +25474,7 @@ export type components = {
*/
ModelsList: {
/** Models */
- models: (components["schemas"]["Main_Diffusers_SD1_Config"] | components["schemas"]["Main_Diffusers_SD2_Config"] | components["schemas"]["Main_Diffusers_SDXL_Config"] | components["schemas"]["Main_Diffusers_SDXLRefiner_Config"] | components["schemas"]["Main_Diffusers_SD3_Config"] | components["schemas"]["Main_Diffusers_FLUX_Config"] | components["schemas"]["Main_Diffusers_Flux2_Config"] | components["schemas"]["Main_Diffusers_CogView4_Config"] | components["schemas"]["Main_Diffusers_QwenImage_Config"] | components["schemas"]["Main_Diffusers_ZImage_Config"] | components["schemas"]["Main_Checkpoint_SD1_Config"] | components["schemas"]["Main_Checkpoint_SD2_Config"] | components["schemas"]["Main_Checkpoint_SDXL_Config"] | components["schemas"]["Main_Checkpoint_SDXLRefiner_Config"] | components["schemas"]["Main_Checkpoint_Flux2_Config"] | components["schemas"]["Main_Checkpoint_FLUX_Config"] | components["schemas"]["Main_Checkpoint_QwenImage_Config"] | components["schemas"]["Main_Checkpoint_ZImage_Config"] | components["schemas"]["Main_Checkpoint_Anima_Config"] | components["schemas"]["Main_BnBNF4_FLUX_Config"] | components["schemas"]["Main_GGUF_Flux2_Config"] | components["schemas"]["Main_GGUF_FLUX_Config"] | components["schemas"]["Main_GGUF_QwenImage_Config"] | components["schemas"]["Main_GGUF_ZImage_Config"] | components["schemas"]["VAE_Checkpoint_SD1_Config"] | components["schemas"]["VAE_Checkpoint_SD2_Config"] | components["schemas"]["VAE_Checkpoint_SDXL_Config"] | components["schemas"]["VAE_Checkpoint_FLUX_Config"] | components["schemas"]["VAE_Checkpoint_Flux2_Config"] | components["schemas"]["VAE_Checkpoint_QwenImage_Config"] | components["schemas"]["VAE_Checkpoint_Anima_Config"] | components["schemas"]["VAE_Diffusers_SD1_Config"] | components["schemas"]["VAE_Diffusers_SDXL_Config"] | components["schemas"]["VAE_Diffusers_Flux2_Config"] | components["schemas"]["ControlNet_Checkpoint_SD1_Config"] | components["schemas"]["ControlNet_Checkpoint_SD2_Config"] | components["schemas"]["ControlNet_Checkpoint_SDXL_Config"] | components["schemas"]["ControlNet_Checkpoint_FLUX_Config"] | components["schemas"]["ControlNet_Checkpoint_ZImage_Config"] | components["schemas"]["ControlNet_Checkpoint_Anima_Config"] | components["schemas"]["ControlNet_Diffusers_SD1_Config"] | components["schemas"]["ControlNet_Diffusers_SD2_Config"] | components["schemas"]["ControlNet_Diffusers_SDXL_Config"] | components["schemas"]["ControlNet_Diffusers_FLUX_Config"] | components["schemas"]["LoRA_LyCORIS_SD1_Config"] | components["schemas"]["LoRA_LyCORIS_SD2_Config"] | components["schemas"]["LoRA_LyCORIS_SDXL_Config"] | components["schemas"]["LoRA_LyCORIS_Flux2_Config"] | components["schemas"]["LoRA_LyCORIS_FLUX_Config"] | components["schemas"]["LoRA_LyCORIS_ZImage_Config"] | components["schemas"]["LoRA_LyCORIS_QwenImage_Config"] | components["schemas"]["LoRA_LyCORIS_Anima_Config"] | components["schemas"]["LoRA_OMI_SDXL_Config"] | components["schemas"]["LoRA_OMI_FLUX_Config"] | components["schemas"]["LoRA_Diffusers_SD1_Config"] | components["schemas"]["LoRA_Diffusers_SD2_Config"] | components["schemas"]["LoRA_Diffusers_SDXL_Config"] | components["schemas"]["LoRA_Diffusers_Flux2_Config"] | components["schemas"]["LoRA_Diffusers_FLUX_Config"] | components["schemas"]["LoRA_Diffusers_ZImage_Config"] | components["schemas"]["ControlLoRA_LyCORIS_FLUX_Config"] | components["schemas"]["T5Encoder_T5Encoder_Config"] | components["schemas"]["T5Encoder_BnBLLMint8_Config"] | components["schemas"]["Qwen3Encoder_Qwen3Encoder_Config"] | components["schemas"]["Qwen3Encoder_Checkpoint_Config"] | components["schemas"]["Qwen3Encoder_GGUF_Config"] | components["schemas"]["QwenVLEncoder_Diffusers_Config"] | components["schemas"]["QwenVLEncoder_Checkpoint_Config"] | components["schemas"]["TI_File_SD1_Config"] | components["schemas"]["TI_File_SD2_Config"] | components["schemas"]["TI_File_SDXL_Config"] | components["schemas"]["TI_Folder_SD1_Config"] | components["schemas"]["TI_Folder_SD2_Config"] | components["schemas"]["TI_Folder_SDXL_Config"] | components["schemas"]["IPAdapter_InvokeAI_SD1_Config"] | components["schemas"]["IPAdapter_InvokeAI_SD2_Config"] | components["schemas"]["IPAdapter_InvokeAI_SDXL_Config"] | components["schemas"]["IPAdapter_Checkpoint_SD1_Config"] | components["schemas"]["IPAdapter_Checkpoint_SD2_Config"] | components["schemas"]["IPAdapter_Checkpoint_SDXL_Config"] | components["schemas"]["IPAdapter_Checkpoint_FLUX_Config"] | components["schemas"]["T2IAdapter_Diffusers_SD1_Config"] | components["schemas"]["T2IAdapter_Diffusers_SDXL_Config"] | components["schemas"]["Spandrel_Checkpoint_Config"] | components["schemas"]["CLIPEmbed_Diffusers_G_Config"] | components["schemas"]["CLIPEmbed_Diffusers_L_Config"] | components["schemas"]["CLIPVision_Diffusers_Config"] | components["schemas"]["SigLIP_Diffusers_Config"] | components["schemas"]["FLUXRedux_Checkpoint_Config"] | components["schemas"]["LlavaOnevision_Diffusers_Config"] | components["schemas"]["TextLLM_Diffusers_Config"] | components["schemas"]["ExternalApiModelConfig"] | components["schemas"]["Unknown_Config"])[];
+ models: (components["schemas"]["Main_Diffusers_SD1_Config"] | components["schemas"]["Main_Diffusers_SD2_Config"] | components["schemas"]["Main_Diffusers_SDXL_Config"] | components["schemas"]["Main_Diffusers_SDXLRefiner_Config"] | components["schemas"]["Main_Diffusers_SD3_Config"] | components["schemas"]["Main_Diffusers_FLUX_Config"] | components["schemas"]["Main_Diffusers_Flux2_Config"] | components["schemas"]["Main_Diffusers_CogView4_Config"] | components["schemas"]["Main_Diffusers_QwenImage_Config"] | components["schemas"]["Main_Diffusers_Wan_Config"] | components["schemas"]["Main_Diffusers_ZImage_Config"] | components["schemas"]["Main_Checkpoint_SD1_Config"] | components["schemas"]["Main_Checkpoint_SD2_Config"] | components["schemas"]["Main_Checkpoint_SDXL_Config"] | components["schemas"]["Main_Checkpoint_SDXLRefiner_Config"] | components["schemas"]["Main_Checkpoint_Flux2_Config"] | components["schemas"]["Main_Checkpoint_FLUX_Config"] | components["schemas"]["Main_Checkpoint_QwenImage_Config"] | components["schemas"]["Main_Checkpoint_ZImage_Config"] | components["schemas"]["Main_Checkpoint_Anima_Config"] | components["schemas"]["Main_BnBNF4_FLUX_Config"] | components["schemas"]["Main_GGUF_Flux2_Config"] | components["schemas"]["Main_GGUF_FLUX_Config"] | components["schemas"]["Main_GGUF_QwenImage_Config"] | components["schemas"]["Main_GGUF_Wan_Config"] | components["schemas"]["Main_GGUF_ZImage_Config"] | components["schemas"]["VAE_Checkpoint_SD1_Config"] | components["schemas"]["VAE_Checkpoint_SD2_Config"] | components["schemas"]["VAE_Checkpoint_SDXL_Config"] | components["schemas"]["VAE_Checkpoint_FLUX_Config"] | components["schemas"]["VAE_Checkpoint_Flux2_Config"] | components["schemas"]["VAE_Checkpoint_Wan_Config"] | components["schemas"]["VAE_Checkpoint_QwenImage_Config"] | components["schemas"]["VAE_Checkpoint_Anima_Config"] | components["schemas"]["VAE_Diffusers_SD1_Config"] | components["schemas"]["VAE_Diffusers_SDXL_Config"] | components["schemas"]["VAE_Diffusers_Flux2_Config"] | components["schemas"]["VAE_Diffusers_Wan_Config"] | components["schemas"]["ControlNet_Checkpoint_SD1_Config"] | components["schemas"]["ControlNet_Checkpoint_SD2_Config"] | components["schemas"]["ControlNet_Checkpoint_SDXL_Config"] | components["schemas"]["ControlNet_Checkpoint_FLUX_Config"] | components["schemas"]["ControlNet_Checkpoint_ZImage_Config"] | components["schemas"]["ControlNet_Checkpoint_Anima_Config"] | components["schemas"]["ControlNet_Diffusers_SD1_Config"] | components["schemas"]["ControlNet_Diffusers_SD2_Config"] | components["schemas"]["ControlNet_Diffusers_SDXL_Config"] | components["schemas"]["ControlNet_Diffusers_FLUX_Config"] | components["schemas"]["LoRA_LyCORIS_SD1_Config"] | components["schemas"]["LoRA_LyCORIS_SD2_Config"] | components["schemas"]["LoRA_LyCORIS_SDXL_Config"] | components["schemas"]["LoRA_LyCORIS_Flux2_Config"] | components["schemas"]["LoRA_LyCORIS_FLUX_Config"] | components["schemas"]["LoRA_LyCORIS_ZImage_Config"] | components["schemas"]["LoRA_LyCORIS_QwenImage_Config"] | components["schemas"]["LoRA_LyCORIS_Wan_Config"] | components["schemas"]["LoRA_LyCORIS_Anima_Config"] | components["schemas"]["LoRA_OMI_SDXL_Config"] | components["schemas"]["LoRA_OMI_FLUX_Config"] | components["schemas"]["LoRA_Diffusers_SD1_Config"] | components["schemas"]["LoRA_Diffusers_SD2_Config"] | components["schemas"]["LoRA_Diffusers_SDXL_Config"] | components["schemas"]["LoRA_Diffusers_Flux2_Config"] | components["schemas"]["LoRA_Diffusers_FLUX_Config"] | components["schemas"]["LoRA_Diffusers_ZImage_Config"] | components["schemas"]["ControlLoRA_LyCORIS_FLUX_Config"] | components["schemas"]["T5Encoder_T5Encoder_Config"] | components["schemas"]["T5Encoder_BnBLLMint8_Config"] | components["schemas"]["Qwen3Encoder_Qwen3Encoder_Config"] | components["schemas"]["Qwen3Encoder_Checkpoint_Config"] | components["schemas"]["Qwen3Encoder_GGUF_Config"] | components["schemas"]["QwenVLEncoder_Diffusers_Config"] | components["schemas"]["QwenVLEncoder_Checkpoint_Config"] | components["schemas"]["WanT5Encoder_WanT5Encoder_Config"] | components["schemas"]["TI_File_SD1_Config"] | components["schemas"]["TI_File_SD2_Config"] | components["schemas"]["TI_File_SDXL_Config"] | components["schemas"]["TI_Folder_SD1_Config"] | components["schemas"]["TI_Folder_SD2_Config"] | components["schemas"]["TI_Folder_SDXL_Config"] | components["schemas"]["IPAdapter_InvokeAI_SD1_Config"] | components["schemas"]["IPAdapter_InvokeAI_SD2_Config"] | components["schemas"]["IPAdapter_InvokeAI_SDXL_Config"] | components["schemas"]["IPAdapter_Checkpoint_SD1_Config"] | components["schemas"]["IPAdapter_Checkpoint_SD2_Config"] | components["schemas"]["IPAdapter_Checkpoint_SDXL_Config"] | components["schemas"]["IPAdapter_Checkpoint_FLUX_Config"] | components["schemas"]["T2IAdapter_Diffusers_SD1_Config"] | components["schemas"]["T2IAdapter_Diffusers_SDXL_Config"] | components["schemas"]["Spandrel_Checkpoint_Config"] | components["schemas"]["CLIPEmbed_Diffusers_G_Config"] | components["schemas"]["CLIPEmbed_Diffusers_L_Config"] | components["schemas"]["CLIPVision_Diffusers_Config"] | components["schemas"]["SigLIP_Diffusers_Config"] | components["schemas"]["FLUXRedux_Checkpoint_Config"] | components["schemas"]["LlavaOnevision_Diffusers_Config"] | components["schemas"]["TextLLM_Diffusers_Config"] | components["schemas"]["ExternalApiModelConfig"] | components["schemas"]["Unknown_Config"])[];
};
/**
* Multiply Integers
@@ -24495,7 +25533,7 @@ export type components = {
* Value
* @description The value to substitute into the node/field.
*/
- value: string | number | components["schemas"]["ImageField"] | (string | number | components["schemas"]["ImageField"])[];
+ value: string | number | components["schemas"]["ImageField"] | components["schemas"]["VideoField"] | (string | number | components["schemas"]["ImageField"] | components["schemas"]["VideoField"])[];
};
/**
* NodePackInfo
@@ -24690,6 +25728,29 @@ export type components = {
*/
items: components["schemas"]["BoardDTO"][];
};
+ /** OffsetPaginatedResults[GalleryItem] */
+ OffsetPaginatedResults_GalleryItem_: {
+ /**
+ * Limit
+ * @description Limit of items to get
+ */
+ limit: number;
+ /**
+ * Offset
+ * @description Offset from which to retrieve items
+ */
+ offset: number;
+ /**
+ * Total
+ * @description Total number of items in result
+ */
+ total: number;
+ /**
+ * Items
+ * @description Items
+ */
+ items: components["schemas"]["GalleryItem"][];
+ };
/** OffsetPaginatedResults[ImageDTO] */
OffsetPaginatedResults_ImageDTO_: {
/**
@@ -24713,6 +25774,29 @@ export type components = {
*/
items: components["schemas"]["ImageDTO"][];
};
+ /** OffsetPaginatedResults[VideoDTO] */
+ OffsetPaginatedResults_VideoDTO_: {
+ /**
+ * Limit
+ * @description Limit of items to get
+ */
+ limit: number;
+ /**
+ * Offset
+ * @description Offset from which to retrieve items
+ */
+ offset: number;
+ /**
+ * Total
+ * @description Total number of items in result
+ */
+ total: number;
+ /**
+ * Items
+ * @description Items
+ */
+ items: components["schemas"]["VideoDTO"][];
+ };
/**
* Unsharp Mask (Oklab)
* @description Applies an unsharp mask filter to an image in the Oklab color space
@@ -27144,6 +28228,19 @@ export type components = {
*/
removed_images: string[];
};
+ /** RemoveVideosFromBoardResult */
+ RemoveVideosFromBoardResult: {
+ /**
+ * Affected Boards
+ * @description The ids of boards affected by the operation
+ */
+ affected_boards: string[];
+ /**
+ * Removed Videos
+ * @description The video names that were removed from their board
+ */
+ removed_videos: string[];
+ };
/**
* Resize Latents
* @description Resizes latents to explicit width/height (in pixels). Provided dimensions are floor-divided by 8.
@@ -29269,6 +30366,19 @@ export type components = {
*/
starred_images: string[];
};
+ /** StarredVideosResult */
+ StarredVideosResult: {
+ /**
+ * Affected Boards
+ * @description The ids of boards affected by the operation
+ */
+ affected_boards: string[];
+ /**
+ * Starred Videos
+ * @description The names of the videos that were starred
+ */
+ starred_videos: string[];
+ };
/** StarterModel */
StarterModel: {
/** Description */
@@ -29281,7 +30391,7 @@ export type components = {
type: components["schemas"]["ModelType"];
format?: components["schemas"]["ModelFormat"] | null;
/** Variant */
- variant?: components["schemas"]["ModelVariantType"] | components["schemas"]["ClipVariantType"] | components["schemas"]["FluxVariantType"] | components["schemas"]["Flux2VariantType"] | components["schemas"]["ZImageVariantType"] | components["schemas"]["QwenImageVariantType"] | components["schemas"]["Qwen3VariantType"] | null;
+ variant?: components["schemas"]["ModelVariantType"] | components["schemas"]["ClipVariantType"] | components["schemas"]["FluxVariantType"] | components["schemas"]["Flux2VariantType"] | components["schemas"]["ZImageVariantType"] | components["schemas"]["QwenImageVariantType"] | components["schemas"]["WanVariantType"] | components["schemas"]["WanLoRAVariantType"] | components["schemas"]["Qwen3VariantType"] | null;
/**
* Is Installed
* @default false
@@ -29326,7 +30436,7 @@ export type components = {
type: components["schemas"]["ModelType"];
format?: components["schemas"]["ModelFormat"] | null;
/** Variant */
- variant?: components["schemas"]["ModelVariantType"] | components["schemas"]["ClipVariantType"] | components["schemas"]["FluxVariantType"] | components["schemas"]["Flux2VariantType"] | components["schemas"]["ZImageVariantType"] | components["schemas"]["QwenImageVariantType"] | components["schemas"]["Qwen3VariantType"] | null;
+ variant?: components["schemas"]["ModelVariantType"] | components["schemas"]["ClipVariantType"] | components["schemas"]["FluxVariantType"] | components["schemas"]["Flux2VariantType"] | components["schemas"]["ZImageVariantType"] | components["schemas"]["QwenImageVariantType"] | components["schemas"]["WanVariantType"] | components["schemas"]["WanLoRAVariantType"] | components["schemas"]["Qwen3VariantType"] | null;
/**
* Is Installed
* @default false
@@ -29850,14 +30960,14 @@ export type components = {
* @description Submodel type.
* @enum {string}
*/
- SubModelType: "unet" | "transformer" | "text_encoder" | "text_encoder_2" | "text_encoder_3" | "tokenizer" | "tokenizer_2" | "tokenizer_3" | "vae" | "vae_decoder" | "vae_encoder" | "scheduler" | "safety_checker";
+ SubModelType: "unet" | "transformer" | "transformer_2" | "text_encoder" | "text_encoder_2" | "text_encoder_3" | "tokenizer" | "tokenizer_2" | "tokenizer_3" | "vae" | "vae_decoder" | "vae_encoder" | "scheduler" | "safety_checker";
/** SubmodelDefinition */
SubmodelDefinition: {
/** Path Or Prefix */
path_or_prefix: string;
model_type: components["schemas"]["ModelType"];
/** Variant */
- variant?: components["schemas"]["ModelVariantType"] | components["schemas"]["ClipVariantType"] | components["schemas"]["FluxVariantType"] | components["schemas"]["Flux2VariantType"] | components["schemas"]["ZImageVariantType"] | components["schemas"]["QwenImageVariantType"] | components["schemas"]["Qwen3VariantType"] | null;
+ variant?: components["schemas"]["ModelVariantType"] | components["schemas"]["ClipVariantType"] | components["schemas"]["FluxVariantType"] | components["schemas"]["Flux2VariantType"] | components["schemas"]["ZImageVariantType"] | components["schemas"]["QwenImageVariantType"] | components["schemas"]["WanVariantType"] | components["schemas"]["WanLoRAVariantType"] | components["schemas"]["Qwen3VariantType"] | null;
};
/**
* Subtract Integers
@@ -31214,7 +32324,7 @@ export type components = {
* inferred from the field type.
* @enum {string}
*/
- UIComponent: "none" | "textarea" | "slider";
+ UIComponent: "none" | "textarea" | "slider" | "video-frame-index";
/**
* UIConfigBase
* @description Provides additional node configuration to the UI.
@@ -31519,6 +32629,19 @@ export type components = {
*/
unstarred_images: string[];
};
+ /** UnstarredVideosResult */
+ UnstarredVideosResult: {
+ /**
+ * Affected Boards
+ * @description The ids of boards affected by the operation
+ */
+ affected_boards: string[];
+ /**
+ * Unstarred Videos
+ * @description The names of the videos that were unstarred
+ */
+ unstarred_videos: string[];
+ };
/**
* UpdateAppGenerationSettingsRequest
* @description Writable generation-related app settings.
@@ -32259,6 +33382,101 @@ export type components = {
*/
base: "sdxl";
};
+ /**
+ * VAE_Checkpoint_Wan_Config
+ * @description Model config for Wan 2.2 VAE checkpoint models (AutoencoderKLWan).
+ *
+ * Distinguishes A14B (z_dim=16, standard Wan VAE) from TI2V-5B (z_dim=48,
+ * Wan2.2-VAE) via the input channel count of ``decoder.conv_in.weight``.
+ */
+ VAE_Checkpoint_Wan_Config: {
+ /**
+ * Key
+ * @description A unique key for this model.
+ */
+ key: string;
+ /**
+ * Hash
+ * @description The hash of the model file(s).
+ */
+ hash: string;
+ /**
+ * Path
+ * @description Path to the model on the filesystem. Relative paths are relative to the Invoke root directory.
+ */
+ path: string;
+ /**
+ * File Size
+ * @description The size of the model in bytes.
+ */
+ file_size: number;
+ /**
+ * Name
+ * @description Name of the model.
+ */
+ name: string;
+ /**
+ * Description
+ * @description Model description
+ */
+ description: string | null;
+ /**
+ * Source
+ * @description The original source of the model (path, URL or repo_id).
+ */
+ source: string;
+ /** @description The type of source */
+ source_type: components["schemas"]["ModelSourceType"];
+ /**
+ * Source Api Response
+ * @description The original API response from the source, as stringified JSON.
+ */
+ source_api_response: string | null;
+ /**
+ * Source Url
+ * @description Optional URL for the model (e.g. download page or model page).
+ */
+ source_url: string | null;
+ /**
+ * Cover Image
+ * @description Url for image to preview model
+ */
+ cover_image: string | null;
+ /**
+ * Config Path
+ * @description Path to the config for this model, if any.
+ */
+ config_path: string | null;
+ /**
+ * Type
+ * @default vae
+ * @constant
+ */
+ type: "vae";
+ /**
+ * Format
+ * @default checkpoint
+ * @constant
+ */
+ format: "checkpoint";
+ /**
+ * Base
+ * @default wan
+ * @constant
+ */
+ base: "wan";
+ /**
+ * Cpu Only
+ * @description Whether this model should run on CPU only
+ */
+ cpu_only: boolean | null;
+ /**
+ * Latent Channels
+ * @description VAE latent channel count: 16 for A14B (standard Wan VAE) or 48 for TI2V-5B (Wan2.2-VAE).
+ * @enum {integer}
+ */
+ latent_channels: 16 | 48;
+ };
/**
* VAE_Diffusers_Flux2_Config
* @description Model config for FLUX.2 VAE models in diffusers format (AutoencoderKLFlux2).
@@ -32502,549 +33720,589 @@ export type components = {
*/
base: "sdxl";
};
- /** ValidationError */
- ValidationError: {
- /** Location */
- loc: (string | number)[];
- /** Message */
- msg: string;
- /** Error Type */
- type: string;
- };
/**
- * VirtualSubBoardDTO
- * @description A virtual sub-board computed from image metadata, not stored in the database.
+ * VAE_Diffusers_Wan_Config
+ * @description Model config for Wan 2.2 VAE in diffusers folder layout (AutoencoderKLWan).
*/
- VirtualSubBoardDTO: {
+ VAE_Diffusers_Wan_Config: {
/**
- * Virtual Board Id
- * @description The virtual board ID, e.g. 'by_date:2026-03-18'.
+ * Key
+ * @description A unique key for this model.
*/
- virtual_board_id: string;
+ key: string;
/**
- * Board Name
- * @description The display name of the virtual sub-board, e.g. '2026-03-18'.
+ * Hash
+ * @description The hash of the model file(s).
*/
- board_name: string;
+ hash: string;
/**
- * Date
- * @description The ISO date string, e.g. '2026-03-18'.
+ * Path
+ * @description Path to the model on the filesystem. Relative paths are relative to the Invoke root directory.
*/
- date: string;
+ path: string;
/**
- * Image Count
- * @description The number of general images for this date.
+ * File Size
+ * @description The size of the model in bytes.
*/
- image_count: number;
+ file_size: number;
/**
- * Asset Count
- * @description The number of asset images for this date.
+ * Name
+ * @description Name of the model.
*/
- asset_count: number;
+ name: string;
/**
- * Cover Image Name
- * @description The most recent image name for this date.
+ * Description
+ * @description Model description
*/
- cover_image_name?: string | null;
- };
- /** Workflow */
- Workflow: {
+ description: string | null;
/**
- * Name
- * @description The name of the workflow.
+ * Source
+ * @description The original source of the model (path, URL or repo_id).
*/
- name: string;
+ source: string;
+ /** @description The type of source */
+ source_type: components["schemas"]["ModelSourceType"];
/**
- * Author
- * @description The author of the workflow.
+ * Source Api Response
+ * @description The original API response from the source, as stringified JSON.
*/
- author: string;
+ source_api_response: string | null;
/**
- * Description
- * @description The description of the workflow.
+ * Source Url
+ * @description Optional URL for the model (e.g. download page or model page).
*/
- description: string;
+ source_url: string | null;
/**
- * Version
- * @description The version of the workflow.
+ * Cover Image
+ * @description Url for image to preview model
*/
- version: string;
+ cover_image: string | null;
/**
- * Contact
- * @description The contact of the workflow.
+ * Format
+ * @default diffusers
+ * @constant
*/
- contact: string;
+ format: "diffusers";
+ /** @default */
+ repo_variant: components["schemas"]["ModelRepoVariant"];
/**
- * Tags
- * @description The tags of the workflow.
+ * Type
+ * @default vae
+ * @constant
*/
- tags: string;
+ type: "vae";
/**
- * Notes
- * @description The notes of the workflow.
+ * Base
+ * @default wan
+ * @constant
*/
- notes: string;
+ base: "wan";
/**
- * Exposedfields
- * @description The exposed fields of the workflow.
+ * Cpu Only
+ * @description Whether this model should run on CPU only
*/
- exposedFields: components["schemas"]["ExposedField"][];
- /** @description The meta of the workflow. */
- meta: components["schemas"]["WorkflowMeta"];
+ cpu_only: boolean | null;
/**
- * Nodes
- * @description The nodes of the workflow.
+ * Latent Channels
+ * @description VAE latent channel count: 16 for A14B or 48 for TI2V-5B's Wan2.2-VAE.
+ * @default 16
+ * @enum {integer}
*/
- nodes: {
- [key: string]: components["schemas"]["JsonValue"];
- }[];
+ latent_channels: 16 | 48;
+ };
+ /** ValidationError */
+ ValidationError: {
+ /** Location */
+ loc: (string | number)[];
+ /** Message */
+ msg: string;
+ /** Error Type */
+ type: string;
+ };
+ /** VideoBoardArg */
+ VideoBoardArg: {
/**
- * Edges
- * @description The edges of the workflow.
+ * Board Id
+ * @description The id of the board to add or remove the video from
*/
- edges: {
- [key: string]: components["schemas"]["JsonValue"];
- }[];
+ board_id: string;
/**
- * Form
- * @description The form of the workflow.
+ * Video Name
+ * @description The name of the video to add to / remove from the board
*/
- form?: {
- [key: string]: components["schemas"]["JsonValue"];
- } | null;
+ video_name: string;
+ };
+ /**
+ * Concatenate Videos
+ * @description Join two or more videos into a single MP4.
+ *
+ * Transitions:
+ *
+ * * ``cut`` — hard splice, no blending. Fastest; total length is the sum of inputs.
+ * * ``crossfade`` — linear A→B cross-dissolve over ``transition_frames``. Each boundary
+ * consumes ``transition_frames`` from both adjacent clips, so total length is
+ * ``sum(inputs) - transition_frames * (n - 1)``.
+ * * ``fade_through_black`` — A fades to black, then B fades in from black. Each boundary
+ * consumes ``transition_frames // 2`` frames from the preceding clip's tail and the
+ * remainder (``transition_frames - transition_frames // 2``) from the next clip's head,
+ * so the total emitted is exactly ``transition_frames`` per boundary — even for odd
+ * ``transition_frames`` — and the overall length equals the sum of inputs.
+ *
+ * All inputs must share the same pixel dimensions. Output frame rate defaults to the
+ * first input's fps; override with ``fps`` to force a specific rate (the frames are not
+ * resampled, only the container is encoded at the new rate).
+ */
+ VideoConcatInvocation: {
+ /**
+ * @description The board to save the image to
+ * @default null
+ */
+ board?: components["schemas"]["BoardField"] | null;
+ /**
+ * @description Optional metadata to be saved with the image
+ * @default null
+ */
+ metadata?: components["schemas"]["MetadataField"] | null;
/**
* Id
- * @description The id of the workflow.
+ * @description The id of this instance of an invocation. Must be unique among all instances of invocations.
*/
id: string;
- };
- /**
- * WorkflowAccessRevokedEvent
- * @description Event model for workflow_access_revoked.
- */
- WorkflowAccessRevokedEvent: {
/**
- * Timestamp
- * @description The timestamp of the event
+ * Is Intermediate
+ * @description Whether or not this is an intermediate invocation.
+ * @default false
*/
- timestamp: number;
+ is_intermediate?: boolean;
/**
- * Workflow Id
- * @description The ID of the workflow
+ * Use Cache
+ * @description Whether or not to use the cache
+ * @default true
*/
- workflow_id: string;
+ use_cache?: boolean;
/**
- * User Id
- * @description The owner of the workflow
+ * Videos
+ * @description Videos to concatenate, in order. At least two are required.
+ * @default null
*/
- user_id: string;
- };
- /** WorkflowAndGraphResponse */
- WorkflowAndGraphResponse: {
+ videos?: components["schemas"]["VideoField"][] | null;
/**
- * Workflow
- * @description The workflow used to generate the image, as stringified JSON
+ * Transition
+ * @description Transition between consecutive clips.
+ * @default cut
+ * @enum {string}
*/
- workflow: string | null;
+ transition?: "cut" | "crossfade" | "fade_through_black";
/**
- * Graph
- * @description The graph used to generate the image, as stringified JSON
+ * Transition Frames
+ * @description Length of each transition in frames. Ignored when transition is 'cut'.
+ * @default 8
*/
- graph: string | null;
- };
- /** WorkflowCallCompatibility */
- WorkflowCallCompatibility: {
+ transition_frames?: number;
/**
- * Is Callable
- * @description Whether the workflow can currently be executed by call_saved_workflow.
+ * Fps
+ * @description Output frame rate. Defaults to the first input's fps.
+ * @default null
*/
- is_callable: boolean;
- /** @description Structured compatibility result. */
- reason: components["schemas"]["WorkflowCallCompatibilityReason"];
+ fps?: number | null;
/**
- * Message
- * @description Human-readable compatibility detail when unavailable.
+ * type
+ * @default video_concat
+ * @constant
*/
- message?: string | null;
+ type: "video_concat";
};
/**
- * WorkflowCallCompatibilityReason
- * @enum {string}
- */
- WorkflowCallCompatibilityReason: "ok" | "missing_workflow_return" | "multiple_workflow_return" | "unsupported_node" | "unsupported_batch_input" | "invalid_graph" | "invalid_inputs" | "exceeds_capacity" | "unknown";
- /**
- * WorkflowCallExecution
- * @description Tracks one parent/child workflow-call relationship and its lifecycle.
+ * VideoDTO
+ * @description Deserialized video record, enriched for the frontend.
*/
- WorkflowCallExecution: {
- /**
- * Id
- * @description The workflow-call execution id.
- */
- id?: string;
+ VideoDTO: {
/**
- * Parent Session Id
- * @description The parent graph execution state id.
+ * Video Name
+ * @description The unique name of the video.
*/
- parent_session_id: string;
+ video_name: string;
/**
- * Child Session Id
- * @description The child graph execution state id, if any.
+ * Video Url
+ * @description The URL of the video file (MP4).
*/
- child_session_id?: string | null;
+ video_url: string;
/**
- * Prepared Call Node Id
- * @description The prepared exec node id for the parent call site.
+ * Thumbnail Url
+ * @description The URL of the video's first-frame thumbnail (WebP).
*/
- prepared_call_node_id: string;
+ thumbnail_url: string;
+ /** @description The origin of the video. */
+ video_origin: components["schemas"]["ResourceOrigin"];
+ /** @description The category of the video (reuses ImageCategory). */
+ video_category: components["schemas"]["ImageCategory"];
/**
- * Source Call Node Id
- * @description The source graph node id for the parent call site.
+ * Width
+ * @description The pixel width of the video.
*/
- source_call_node_id: string;
+ width: number;
/**
- * Workflow Id
- * @description The saved workflow being called.
+ * Height
+ * @description The pixel height of the video.
*/
- workflow_id: string;
+ height: number;
/**
- * Depth
- * @description The 1-based depth of this call frame.
+ * Duration
+ * @description The duration of the video in seconds.
*/
- depth: number;
+ duration: number;
/**
- * Status
- * @description The current workflow-call lifecycle state.
- * @enum {string}
+ * Fps
+ * @description The frames-per-second of the video, if known.
*/
- status: "waiting_for_child" | "running_child" | "completed" | "failed";
+ fps?: number | null;
/**
- * Error Message
- * @description Failure reason, if the call failed.
+ * Created At
+ * @description The created timestamp of the video.
*/
- error_message?: string | null;
+ created_at: string;
/**
- * Child Session Ids
- * @description All child graph execution state ids.
+ * Updated At
+ * @description The updated timestamp of the video.
*/
- child_session_ids?: string[];
+ updated_at: string;
/**
- * Child Item Ids
- * @description Child queue item ids in enqueue order.
+ * Deleted At
+ * @description The deleted timestamp of the video.
*/
- child_item_ids?: number[];
+ deleted_at?: string | null;
/**
- * Expected Child Count
- * @description The number of child executions for this call.
- * @default 1
+ * Is Intermediate
+ * @description Whether this is an intermediate video.
*/
- expected_child_count?: number;
+ is_intermediate: boolean;
/**
- * Completed Child Item Ids
- * @description The child queue item ids whose workflow_return outputs have been aggregated.
+ * Session Id
+ * @description The session ID that produced this video, if any.
*/
- completed_child_item_ids?: number[];
+ session_id?: string | null;
/**
- * Aggregated Values
- * @description The aggregated workflow_return values accumulated from child executions.
+ * Node Id
+ * @description The node ID that produced this video, if any.
*/
- aggregated_values?: {
- [key: string]: unknown[];
- };
+ node_id?: string | null;
/**
- * Child Outputs
- * @description Workflow return values keyed by child queue item id.
+ * Starred
+ * @description Whether this video is starred.
*/
- child_outputs?: {
- [key: string]: {
- [key: string]: unknown;
- };
- };
- };
- /**
- * WorkflowCallFrame
- * @description Represents one workflow-call frame in a nested call chain.
- */
- WorkflowCallFrame: {
+ starred: boolean;
/**
- * Prepared Call Node Id
- * @description The prepared exec node id for the call site.
+ * Has Workflow
+ * @description Whether this video has a workflow associated.
*/
- prepared_call_node_id: string;
+ has_workflow: boolean;
/**
- * Source Call Node Id
- * @description The source graph node id for the call site.
+ * Video Subfolder
+ * @description The subfolder where the video is stored on disk.
+ * @default
*/
- source_call_node_id: string;
+ video_subfolder?: string;
/**
- * Workflow Id
- * @description The saved workflow being called.
+ * Board Id
+ * @description The id of the board the video belongs to, if one exists.
*/
- workflow_id: string;
+ board_id?: string | null;
+ };
+ /**
+ * VideoField
+ * @description A video primitive field
+ */
+ VideoField: {
/**
- * Depth
- * @description The 1-based depth of this call frame.
+ * Video Name
+ * @description The name of the video
*/
- depth: number;
+ video_name: string;
};
/**
- * WorkflowCallParentRef
- * @description Reference from a child execution state back to its parent workflow-call relationship.
+ * Frame from Video
+ * @description Extract a single frame from a video and save it as an image.
+ *
+ * ``frame_index`` is 0-based. Negative indices count from the end, so the
+ * default of -1 returns the final frame — the typical setup for chaining
+ * I2V clips into a longer sequence.
*/
- WorkflowCallParentRef: {
+ VideoFrameExtractInvocation: {
/**
- * Workflow Call Id
- * @description The workflow-call execution id.
+ * @description The board to save the image to
+ * @default null
*/
- workflow_call_id: string;
+ board?: components["schemas"]["BoardField"] | null;
/**
- * Parent Session Id
- * @description The parent graph execution state id.
+ * @description Optional metadata to be saved with the image
+ * @default null
*/
- parent_session_id: string;
+ metadata?: components["schemas"]["MetadataField"] | null;
/**
- * Prepared Call Node Id
- * @description The prepared exec node id for the parent call site.
+ * Id
+ * @description The id of this instance of an invocation. Must be unique among all instances of invocations.
*/
- prepared_call_node_id: string;
+ id: string;
/**
- * Source Call Node Id
- * @description The source graph node id for the parent call site.
+ * Is Intermediate
+ * @description Whether or not this is an intermediate invocation.
+ * @default false
*/
- source_call_node_id: string;
+ is_intermediate?: boolean;
/**
- * Workflow Id
- * @description The saved workflow being called.
+ * Use Cache
+ * @description Whether or not to use the cache
+ * @default true
*/
- workflow_id: string;
+ use_cache?: boolean;
/**
- * Depth
- * @description The 1-based depth of this call frame.
+ * @description The video to extract a frame from.
+ * @default null
*/
- depth: number;
+ video?: components["schemas"]["VideoField"] | null;
+ /**
+ * Frame Index
+ * @description Index of the frame to extract. 0 = first frame, -1 = last frame, -2 = second-to-last, etc.
+ * @default -1
+ */
+ frame_index?: number;
+ /**
+ * type
+ * @default video_frame_extract
+ * @constant
+ */
+ type: "video_frame_extract";
};
/**
- * WorkflowCategory
- * @enum {string}
- */
- WorkflowCategory: "user" | "default";
- /**
- * WorkflowCreatedEvent
- * @description Event model for workflow_created
+ * Video Primitive
+ * @description A video primitive value. Drop a video onto the field to make it available as an input
+ * to downstream nodes (e.g. Frame from Video, Concatenate Videos).
*/
- WorkflowCreatedEvent: {
+ VideoInvocation: {
/**
- * Timestamp
- * @description The timestamp of the event
+ * Id
+ * @description The id of this instance of an invocation. Must be unique among all instances of invocations.
*/
- timestamp: number;
+ id: string;
/**
- * Workflow Id
- * @description The ID of the workflow
+ * Is Intermediate
+ * @description Whether or not this is an intermediate invocation.
+ * @default false
*/
- workflow_id: string;
+ is_intermediate?: boolean;
/**
- * User Id
- * @description The owner of the workflow
+ * Use Cache
+ * @description Whether or not to use the cache
+ * @default true
*/
- user_id: string;
+ use_cache?: boolean;
/**
- * Is Public
- * @description Whether the workflow is shared with all users
+ * @description The video to load
+ * @default null
*/
- is_public: boolean;
+ video?: components["schemas"]["VideoField"] | null;
+ /**
+ * type
+ * @default video
+ * @constant
+ */
+ type: "video";
};
/**
- * WorkflowDeletedEvent
- * @description Event model for workflow_deleted
+ * VideoNamesResult
+ * @description Response containing ordered video names with metadata for optimistic updates.
*/
- WorkflowDeletedEvent: {
+ VideoNamesResult: {
/**
- * Timestamp
- * @description The timestamp of the event
+ * Video Names
+ * @description Ordered list of video names
*/
- timestamp: number;
+ video_names: string[];
/**
- * Workflow Id
- * @description The ID of the workflow
+ * Starred Count
+ * @description Number of starred videos (when starred_first=True)
*/
- workflow_id: string;
+ starred_count: number;
/**
- * User Id
- * @description The owner of the workflow
+ * Total Count
+ * @description Total number of videos matching the query
*/
- user_id: string;
+ total_count: number;
+ };
+ /**
+ * VideoOutput
+ * @description Output of a node that produces a video file (e.g. Wan 2.2 latents-to-video).
+ */
+ VideoOutput: {
+ /** @description The output video */
+ video: components["schemas"]["VideoField"];
/**
- * Is Public
- * @description Whether the workflow was shared when it was deleted
+ * Width
+ * @description The width of the video in pixels
*/
- is_public: boolean;
- };
- /** WorkflowMeta */
- WorkflowMeta: {
+ width: number;
/**
- * Version
- * @description The version of the workflow schema.
+ * Height
+ * @description The height of the video in pixels
*/
- version: string;
- /** @description The category of the workflow (user or default). */
- category: components["schemas"]["WorkflowCategory"];
- };
- /** WorkflowRecordDTO */
- WorkflowRecordDTO: {
+ height: number;
/**
- * Workflow Id
- * @description The id of the workflow.
+ * Num Frames
+ * @description The number of frames in the video
*/
- workflow_id: string;
+ num_frames: number;
/**
- * Name
- * @description The name of the workflow.
+ * Fps
+ * @description The frames-per-second of the video
*/
- name: string;
+ fps: number;
/**
- * Created At
- * @description The created timestamp of the workflow.
+ * Duration
+ * @description The duration of the video in seconds
*/
- created_at: string;
+ duration: number;
/**
- * Updated At
- * @description The updated timestamp of the workflow.
+ * type
+ * @default video_output
+ * @constant
*/
- updated_at: string;
+ type: "video_output";
+ };
+ /**
+ * VideoRecordChanges
+ * @description Allowed mutations on a video record.
+ */
+ VideoRecordChanges: {
+ /** @description The video's new category. */
+ video_category?: components["schemas"]["ImageCategory"] | null;
/**
- * Opened At
- * @description The opened timestamp of the workflow.
+ * Session Id
+ * @description The video's new session ID.
*/
- opened_at?: string | null;
+ session_id?: string | null;
/**
- * User Id
- * @description The id of the user who owns this workflow.
+ * Is Intermediate
+ * @description The video's new `is_intermediate` flag.
*/
- user_id: string;
+ is_intermediate?: boolean | null;
/**
- * Is Public
- * @description Whether this workflow is shared with all users.
+ * Starred
+ * @description The video's new `starred` state.
*/
- is_public: boolean;
- /** @description The workflow. */
- workflow: components["schemas"]["Workflow"];
+ starred?: boolean | null;
+ } & {
+ [key: string]: unknown;
};
- /** WorkflowRecordListItemWithThumbnailDTO */
- WorkflowRecordListItemWithThumbnailDTO: {
+ /**
+ * VideoUrlsDTO
+ * @description The URLs for a video and its thumbnail.
+ */
+ VideoUrlsDTO: {
/**
- * Workflow Id
- * @description The id of the workflow.
+ * Video Name
+ * @description The unique name of the video.
*/
- workflow_id: string;
+ video_name: string;
/**
- * Name
- * @description The name of the workflow.
+ * Video Url
+ * @description The URL of the video file (MP4).
*/
- name: string;
+ video_url: string;
/**
- * Created At
- * @description The created timestamp of the workflow.
+ * Thumbnail Url
+ * @description The URL of the video's first-frame thumbnail (WebP).
*/
- created_at: string;
+ thumbnail_url: string;
+ };
+ /**
+ * VirtualSubBoardDTO
+ * @description A virtual sub-board computed from image/video metadata, not stored in the database.
+ */
+ VirtualSubBoardDTO: {
/**
- * Updated At
- * @description The updated timestamp of the workflow.
+ * Virtual Board Id
+ * @description The virtual board ID, e.g. 'by_date:2026-03-18'.
*/
- updated_at: string;
+ virtual_board_id: string;
/**
- * Opened At
- * @description The opened timestamp of the workflow.
+ * Board Name
+ * @description The display name of the virtual sub-board, e.g. '2026-03-18'.
*/
- opened_at?: string | null;
+ board_name: string;
/**
- * User Id
- * @description The id of the user who owns this workflow.
+ * Date
+ * @description The ISO date string, e.g. '2026-03-18'.
*/
- user_id: string;
+ date: string;
/**
- * Is Public
- * @description Whether this workflow is shared with all users.
+ * Image Count
+ * @description The number of general images for this date.
*/
- is_public: boolean;
+ image_count: number;
/**
- * Description
- * @description The description of the workflow.
+ * Asset Count
+ * @description The number of asset images for this date.
*/
- description: string;
- /** @description The description of the workflow. */
- category: components["schemas"]["WorkflowCategory"];
+ asset_count: number;
/**
- * Tags
- * @description The tags of the workflow.
+ * Video Count
+ * @description The number of videos for this date.
+ * @default 0
*/
- tags: string;
+ video_count?: number;
/**
- * Thumbnail Url
- * @description The URL of the workflow thumbnail.
+ * Cover Image Name
+ * @description The most recent image name for this date.
*/
- thumbnail_url?: string | null;
- /** @description Whether this workflow is currently callable by call_saved_workflow. */
- call_saved_workflow_compatibility?: components["schemas"]["WorkflowCallCompatibility"] | null;
+ cover_image_name?: string | null;
+ /**
+ * Cover Video Name
+ * @description The most recent video name for this date. Set instead of cover_image_name when the newest item for the date is a video.
+ */
+ cover_video_name?: string | null;
};
/**
- * WorkflowRecordOrderBy
- * @description The order by options for workflow records
- * @enum {string}
+ * WanConditioningField
+ * @description A Wan 2.2 conditioning tensor primitive value.
+ *
+ * Wan conditioning is the UMT5-XXL hidden state for the prompt plus an attention
+ * mask marking valid (non-padding) tokens.
*/
- WorkflowRecordOrderBy: "created_at" | "updated_at" | "opened_at" | "name" | "is_public";
- /** WorkflowRecordWithThumbnailDTO */
- WorkflowRecordWithThumbnailDTO: {
- /**
- * Workflow Id
- * @description The id of the workflow.
- */
- workflow_id: string;
+ WanConditioningField: {
/**
- * Name
- * @description The name of the workflow.
- */
- name: string;
- /**
- * Created At
- * @description The created timestamp of the workflow.
- */
- created_at: string;
- /**
- * Updated At
- * @description The updated timestamp of the workflow.
- */
- updated_at: string;
- /**
- * Opened At
- * @description The opened timestamp of the workflow.
- */
- opened_at?: string | null;
- /**
- * User Id
- * @description The id of the user who owns this workflow.
- */
- user_id: string;
- /**
- * Is Public
- * @description Whether this workflow is shared with all users.
+ * Conditioning Name
+ * @description The name of conditioning tensor
*/
- is_public: boolean;
- /** @description The workflow. */
- workflow: components["schemas"]["Workflow"];
+ conditioning_name: string;
+ };
+ /**
+ * WanConditioningOutput
+ * @description Base class for nodes that output a Wan 2.2 text conditioning tensor.
+ */
+ WanConditioningOutput: {
+ /** @description Conditioning tensor */
+ conditioning: components["schemas"]["WanConditioningField"];
/**
- * Thumbnail Url
- * @description The URL of the workflow thumbnail.
+ * type
+ * @default wan_conditioning_output
+ * @constant
*/
- thumbnail_url?: string | null;
- /** @description Whether this workflow is currently callable by call_saved_workflow. */
- call_saved_workflow_compatibility?: components["schemas"]["WorkflowCallCompatibility"] | null;
+ type: "wan_conditioning_output";
};
/**
- * Get Workflow Return Value
- * @description Extracts one named value from a callable workflow return.
+ * Denoise - Wan 2.2
+ * @description Run the denoising process with a Wan 2.2 model.
+ *
+ * Drives the flow-matching schedule the model ships with (UniPC with flow
+ * sigmas for the Wan 2.2 reference repos; see ``_build_scheduler``). CFG is
+ * supported when negative conditioning is provided and ``guidance_scale != 1.0``.
+ *
+ * For Wan 2.2 A14B the high-noise expert handles timesteps at and above
+ * ``boundary_ratio * num_train_timesteps``; the low-noise expert handles
+ * timesteps below. Both experts share the model cache; only the active one is
+ * GPU-resident at any time.
*/
- WorkflowReturnGetInvocation: {
+ WanDenoiseInvocation: {
/**
* Id
* @description The id of this instance of an invocation. Must be unique among all instances of invocations.
@@ -33059,52 +34317,116 @@ export type components = {
/**
* Use Cache
* @description Whether or not to use the cache
- * @default false
+ * @default true
*/
use_cache?: boolean;
/**
- * Values
- * @description The named workflow return values.
- * @default {}
+ * Transformer
+ * @description Wan transformer field (transformer + optional dual-expert metadata).
+ * @default null
*/
- values?: {
- [key: string]: unknown;
- };
+ transformer?: components["schemas"]["WanTransformerField"] | null;
/**
- * Key
- * @description The return key to extract.
- * @default
+ * @description Positive conditioning tensor
+ * @default null
*/
- key?: string;
+ positive_conditioning?: components["schemas"]["WanConditioningField"] | null;
/**
- * type
- * @default workflow_return_get
- * @constant
+ * @description Negative conditioning tensor
+ * @default null
*/
- type: "workflow_return_get";
- };
- /**
- * WorkflowReturnGetOutput
- * @description A value extracted from named workflow return values.
- */
- WorkflowReturnGetOutput: {
+ negative_conditioning?: components["schemas"]["WanConditioningField"] | null;
/**
- * Value
- * @description The extracted workflow return value.
+ * Reference Image
+ * @description Reference-image (VAE-latent) conditioning for Wan 2.2 I2V.
+ * @default null
*/
- value: unknown;
+ ref_image?: components["schemas"]["WanRefImageConditioningField"] | null;
+ /**
+ * @description Latents tensor
+ * @default null
+ */
+ latents?: components["schemas"]["LatentsField"] | null;
+ /**
+ * @description A mask of the region to apply the denoising process to. Values of 0.0 represent the regions to be fully denoised, and 1.0 represent the regions to be preserved.
+ * @default null
+ */
+ denoise_mask?: components["schemas"]["DenoiseMaskField"] | null;
+ /**
+ * Denoising Start
+ * @description When to start denoising, expressed a percentage of total steps
+ * @default 0
+ */
+ denoising_start?: number;
+ /**
+ * Denoising End
+ * @description When to stop denoising, expressed a percentage of total steps
+ * @default 1
+ */
+ denoising_end?: number;
+ /**
+ * Add Noise
+ * @description Add noise based on denoising start.
+ * @default true
+ */
+ add_noise?: boolean;
+ /**
+ * Guidance Scale
+ * @description Classifier-free guidance scale. 4.0 is the Wan 2.2 default for A14B; TI2V-5B can tolerate higher values up to ~5.5.
+ * @default 4
+ */
+ guidance_scale?: number;
+ /**
+ * Guidance Scale (Low Noise)
+ * @description Optional separate CFG scale for the low-noise expert (Wan 2.2 A14B only). Values below 1.0 (including 0) fall back to the primary 'Guidance Scale'. Ignored for TI2V-5B.
+ * @default null
+ */
+ guidance_scale_low_noise?: number | null;
+ /**
+ * Width
+ * @description Width of the generated image.
+ * @default 1024
+ */
+ width?: number;
+ /**
+ * Height
+ * @description Height of the generated image.
+ * @default 1024
+ */
+ height?: number;
+ /**
+ * Steps
+ * @description Number of denoising steps.
+ * @default 40
+ */
+ steps?: number;
+ /**
+ * Seed
+ * @description Randomness seed for reproducibility.
+ * @default 0
+ */
+ seed?: number;
/**
* type
- * @default workflow_return_get_output
+ * @default wan_denoise
* @constant
*/
- type: "workflow_return_get_output";
+ type: "wan_denoise";
};
/**
- * Workflow Return
- * @description Defines the explicit named result returned by a callable workflow.
+ * Wan 2.2 I2V Ideal Dimensions (A14B)
+ * @description Ideal dimensions for the Wan 2.2 A14B models (I2V-A14B and T2V-A14B).
+ *
+ * Use this node for the A14B family. For the TI2V-5B model use "Wan 2.2 TI2V
+ * Ideal Dimensions" instead — TI2V-5B requires multiples of 32, and feeding it
+ * these multiples-of-16 dims fails the patchify step.
+ *
+ * Scales the input W×H so the shorter side equals the chosen preset (480 / 720 /
+ * 1080 px), then snaps each dimension to a multiple of 16 (the A14B pixel-grid
+ * constraint). Wire from ``Image Primitive``'s width/height outputs and into
+ * ``wan_ref_image_encoder`` / ``wan_denoise``.
*/
- WorkflowReturnInvocation: {
+ WanI2VIdealDimensionsInvocation: {
/**
* Id
* @description The id of this instance of an invocation. Must be unique among all instances of invocations.
@@ -33119,64 +34441,61 @@ export type components = {
/**
* Use Cache
* @description Whether or not to use the cache
- * @default false
+ * @default true
*/
use_cache?: boolean;
/**
- * Values
- * @description The named values returned to a calling workflow.
- * @default []
+ * Width
+ * @description Source image width in pixels.
+ * @default 1024
*/
- values?: components["schemas"]["WorkflowReturnValueField"] | components["schemas"]["WorkflowReturnValueField"][];
+ width?: number;
/**
- * type
- * @default workflow_return
- * @constant
+ * Height
+ * @description Source image height in pixels.
+ * @default 1024
*/
- type: "workflow_return";
- };
- /**
- * WorkflowReturnOutput
- * @description The explicit named values returned from a callable workflow.
- */
- WorkflowReturnOutput: {
+ height?: number;
/**
- * Values
- * @description The workflow return values, keyed by return name.
- * @default {}
+ * Target Resolution
+ * @description Short-side resolution preset. 480p and 720p are Wan 2.2's native training resolutions; 1080p works but is extrapolation and costs ~2.25x the memory of 720p.
+ * @default 720p
+ * @enum {string}
*/
- values: {
- [key: string]: unknown;
- };
+ target_resolution?: "480p" | "720p" | "1080p";
+ /**
+ * Rounding
+ * @description How to snap each dimension to a multiple of 16. 'floor' rounds down — safest for VRAM, guaranteed not to exceed the unsnapped target. 'ceiling' rounds up. 'nearest' minimizes aspect-ratio drift (default).
+ * @default nearest
+ * @enum {string}
+ */
+ rounding?: "nearest" | "floor" | "ceiling";
/**
* type
- * @default workflow_return_output
+ * @default wan_i2v_ideal_dimensions
* @constant
*/
- type: "workflow_return_output";
+ type: "wan_i2v_ideal_dimensions";
};
/**
- * WorkflowReturnValueField
- * @description One named workflow return value.
+ * Image to Latents - Wan 2.2
+ * @description Encodes an image with the Wan VAE (AutoencoderKLWan).
+ *
+ * The output latents have the temporal dimension squeezed out, so downstream
+ * nodes see 4D ``[B, C, H, W]``. The denoise loop re-adds ``T=1`` before
+ * feeding the transformer.
*/
- WorkflowReturnValueField: {
+ WanImageToLatentsInvocation: {
/**
- * Key
- * @description The workflow return key.
+ * @description The board to save the image to
+ * @default null
*/
- key: string;
+ board?: components["schemas"]["BoardField"] | null;
/**
- * Value
- * @description The workflow return value.
+ * @description Optional metadata to be saved with the image
* @default null
*/
- value?: unknown;
- };
- /**
- * Workflow Return Value
- * @description Creates one named value for a callable workflow return.
- */
- WorkflowReturnValueInvocation: {
+ metadata?: components["schemas"]["MetadataField"] | null;
/**
* Id
* @description The id of this instance of an invocation. Must be unique among all instances of invocations.
@@ -33191,214 +34510,189 @@ export type components = {
/**
* Use Cache
* @description Whether or not to use the cache
- * @default false
+ * @default true
*/
use_cache?: boolean;
/**
- * Key
- * @description The return key.
- * @default
+ * @description The image to encode.
+ * @default null
*/
- key?: string;
+ image?: components["schemas"]["ImageField"] | null;
/**
- * Value
- * @description The value returned under this key.
+ * @description VAE
* @default null
*/
- value?: unknown | null;
+ vae?: components["schemas"]["VAEField"] | null;
/**
* type
- * @default workflow_return_value
+ * @default wan_i2l
* @constant
*/
- type: "workflow_return_value";
+ type: "wan_i2l";
};
/**
- * WorkflowReturnValueOutput
- * @description A named workflow return value.
+ * Latents to Image - Wan 2.2
+ * @description Decodes Wan latents back to RGB.
*/
- WorkflowReturnValueOutput: {
- /**
- * Return Value
- * @description The named workflow return value.
- */
- value: components["schemas"]["WorkflowReturnValueField"];
+ WanLatentsToImageInvocation: {
/**
- * type
- * @default workflow_return_value_output
- * @constant
+ * @description The board to save the image to
+ * @default null
*/
- type: "workflow_return_value_output";
- };
- /**
- * WorkflowUpdatedEvent
- * @description Event model for workflow_updated
- */
- WorkflowUpdatedEvent: {
+ board?: components["schemas"]["BoardField"] | null;
/**
- * Timestamp
- * @description The timestamp of the event
+ * @description Optional metadata to be saved with the image
+ * @default null
*/
- timestamp: number;
+ metadata?: components["schemas"]["MetadataField"] | null;
/**
- * Workflow Id
- * @description The ID of the workflow
+ * Id
+ * @description The id of this instance of an invocation. Must be unique among all instances of invocations.
*/
- workflow_id: string;
+ id: string;
/**
- * User Id
- * @description The owner of the workflow
+ * Is Intermediate
+ * @description Whether or not this is an intermediate invocation.
+ * @default false
*/
- user_id: string;
+ is_intermediate?: boolean;
/**
- * Old Is Public
- * @description Whether the workflow was shared before the update
+ * Use Cache
+ * @description Whether or not to use the cache
+ * @default true
*/
- old_is_public: boolean;
+ use_cache?: boolean;
/**
- * New Is Public
- * @description Whether the workflow is shared after the update
+ * @description Latents tensor
+ * @default null
*/
- new_is_public: boolean;
- };
- /** WorkflowWithoutID */
- WorkflowWithoutID: {
+ latents?: components["schemas"]["LatentsField"] | null;
/**
- * Name
- * @description The name of the workflow.
+ * @description VAE
+ * @default null
*/
- name: string;
+ vae?: components["schemas"]["VAEField"] | null;
/**
- * Author
- * @description The author of the workflow.
+ * type
+ * @default wan_l2i
+ * @constant
*/
- author: string;
+ type: "wan_l2i";
+ };
+ /**
+ * Latents to Video - Wan 2.2
+ * @description Decode 5D Wan latents to RGB frames and encode an MP4.
+ */
+ WanLatentsToVideoInvocation: {
/**
- * Description
- * @description The description of the workflow.
+ * @description The board to save the image to
+ * @default null
*/
- description: string;
+ board?: components["schemas"]["BoardField"] | null;
/**
- * Version
- * @description The version of the workflow.
+ * @description Optional metadata to be saved with the image
+ * @default null
*/
- version: string;
+ metadata?: components["schemas"]["MetadataField"] | null;
/**
- * Contact
- * @description The contact of the workflow.
+ * Id
+ * @description The id of this instance of an invocation. Must be unique among all instances of invocations.
*/
- contact: string;
+ id: string;
/**
- * Tags
- * @description The tags of the workflow.
+ * Is Intermediate
+ * @description Whether or not this is an intermediate invocation.
+ * @default false
*/
- tags: string;
+ is_intermediate?: boolean;
/**
- * Notes
- * @description The notes of the workflow.
+ * Use Cache
+ * @description Whether or not to use the cache
+ * @default true
*/
- notes: string;
+ use_cache?: boolean;
/**
- * Exposedfields
- * @description The exposed fields of the workflow.
+ * @description Latents tensor
+ * @default null
*/
- exposedFields: components["schemas"]["ExposedField"][];
- /** @description The meta of the workflow. */
- meta: components["schemas"]["WorkflowMeta"];
+ latents?: components["schemas"]["LatentsField"] | null;
/**
- * Nodes
- * @description The nodes of the workflow.
+ * @description VAE
+ * @default null
*/
- nodes: {
- [key: string]: components["schemas"]["JsonValue"];
- }[];
+ vae?: components["schemas"]["VAEField"] | null;
/**
- * Edges
- * @description The edges of the workflow.
+ * Fps
+ * @description Frames-per-second for the encoded MP4. Wan 2.2 was trained at 16 FPS.
+ * @default 16
*/
- edges: {
- [key: string]: components["schemas"]["JsonValue"];
- }[];
+ fps?: number;
/**
- * Form
- * @description The form of the workflow.
+ * type
+ * @default wan_l2v
+ * @constant
*/
- form?: {
- [key: string]: components["schemas"]["JsonValue"];
- } | null;
+ type: "wan_l2v";
};
/**
- * ZImageConditioningField
- * @description A Z-Image conditioning tensor primitive value
+ * Apply LoRA Collection - Wan 2.2
+ * @description Apply a collection of LoRAs to the Wan 2.2 transformer(s).
+ *
+ * Each LoRA is routed to the primary and/or low-noise list based on its
+ * recorded ``expert`` tag (set by the probe from the filename). Untagged
+ * LoRAs go to both lists.
*/
- ZImageConditioningField: {
- /**
- * Conditioning Name
- * @description The name of conditioning tensor
- */
- conditioning_name: string;
+ WanLoRACollectionLoader: {
/**
- * @description The mask associated with this conditioning tensor for regional prompting. Excluded regions should be set to False, included regions should be set to True.
- * @default null
+ * Id
+ * @description The id of this instance of an invocation. Must be unique among all instances of invocations.
*/
- mask?: components["schemas"]["TensorField"] | null;
- };
- /**
- * ZImageConditioningOutput
- * @description Base class for nodes that output a Z-Image text conditioning tensor.
- */
- ZImageConditioningOutput: {
- /** @description Conditioning tensor */
- conditioning: components["schemas"]["ZImageConditioningField"];
+ id: string;
/**
- * type
- * @default z_image_conditioning_output
- * @constant
+ * Is Intermediate
+ * @description Whether or not this is an intermediate invocation.
+ * @default false
*/
- type: "z_image_conditioning_output";
- };
- /**
- * ZImageControlField
- * @description A Z-Image control conditioning field for spatial control (Canny, HED, Depth, Pose, MLSD).
- */
- ZImageControlField: {
+ is_intermediate?: boolean;
/**
- * Image Name
- * @description The name of the preprocessed control image
+ * Use Cache
+ * @description Whether or not to use the cache
+ * @default true
*/
- image_name: string;
- /** @description The Z-Image ControlNet adapter model */
- control_model: components["schemas"]["ModelIdentifierField"];
+ use_cache?: boolean;
/**
- * Control Context Scale
- * @description The strength of the control signal. Recommended range: 0.65-0.80.
- * @default 0.75
+ * LoRAs
+ * @description LoRAs to apply. May be a single LoRA or a collection.
+ * @default null
*/
- control_context_scale?: number;
+ loras?: components["schemas"]["LoRAField"] | components["schemas"]["LoRAField"][] | null;
/**
- * Begin Step Percent
- * @description When the control is first applied (% of total steps)
- * @default 0
+ * Wan Transformer
+ * @description Transformer
+ * @default null
*/
- begin_step_percent?: number;
+ transformer?: components["schemas"]["WanTransformerField"] | null;
/**
- * End Step Percent
- * @description When the control is last applied (% of total steps)
- * @default 1
+ * type
+ * @default wan_lora_collection_loader
+ * @constant
*/
- end_step_percent?: number;
+ type: "wan_lora_collection_loader";
};
/**
- * Z-Image ControlNet
- * @description Configure Z-Image ControlNet for spatial conditioning.
+ * Apply LoRA - Wan 2.2
+ * @description Apply a LoRA to the Wan 2.2 transformer(s).
*
- * Takes a preprocessed control image (e.g., Canny edges, depth map, pose)
- * and a Z-Image ControlNet adapter model to enable spatial control.
+ * For A14B (dual expert) the LoRA's recorded ``expert`` field determines
+ * which expert list it lands in: ``"high"`` -> primary list, ``"low"`` ->
+ * low-noise list, ``None`` (untagged) -> both lists. Use the ``target``
+ * field to override.
*
- * Supports 5 control modes: Canny, HED, Depth, Pose, MLSD.
- * Recommended control_context_scale: 0.65-0.80.
+ * For TI2V-5B (single transformer) only the primary list is used at denoise
+ * time; a LoRA routed only to the low-noise list would be inert, so that
+ * routing logs a warning.
*/
- ZImageControlInvocation: {
+ WanLoRALoaderInvocation: {
/**
* Id
* @description The id of this instance of an invocation. Must be unique among all instances of invocations.
@@ -33417,62 +34711,87 @@ export type components = {
*/
use_cache?: boolean;
/**
- * @description The preprocessed control image (Canny, HED, Depth, Pose, or MLSD)
- * @default null
- */
- image?: components["schemas"]["ImageField"] | null;
- /**
- * Control Model
- * @description ControlNet model to load
+ * LoRA
+ * @description LoRA model to load
* @default null
*/
- control_model?: components["schemas"]["ModelIdentifierField"] | null;
+ lora?: components["schemas"]["ModelIdentifierField"] | null;
/**
- * Control Scale
- * @description Strength of the control signal. Recommended range: 0.65-0.80.
+ * Weight
+ * @description The weight at which the LoRA is applied to each model
* @default 0.75
*/
- control_context_scale?: number;
+ weight?: number;
/**
- * Begin Step Percent
- * @description When the control is first applied (% of total steps)
- * @default 0
+ * Target
+ * @description Which expert(s) to apply this LoRA to. 'auto' uses the LoRA's recorded expert tag (or both if untagged); 'both'/'high'/'low' override it.
+ * @default auto
+ * @enum {string}
*/
- begin_step_percent?: number;
+ target?: "auto" | "both" | "high" | "low";
/**
- * End Step Percent
- * @description When the control is last applied (% of total steps)
- * @default 1
+ * Wan Transformer
+ * @description Transformer
+ * @default null
*/
- end_step_percent?: number;
+ transformer?: components["schemas"]["WanTransformerField"] | null;
/**
* type
- * @default z_image_control
+ * @default wan_lora_loader
* @constant
*/
- type: "z_image_control";
+ type: "wan_lora_loader";
};
/**
- * ZImageControlOutput
- * @description Z-Image Control output containing control configuration.
+ * WanLoRALoaderOutput
+ * @description Wan 2.2 LoRA loader output.
*/
- ZImageControlOutput: {
- /** @description Z-Image control conditioning */
- control: components["schemas"]["ZImageControlField"];
+ WanLoRALoaderOutput: {
+ /**
+ * Wan Transformer
+ * @description Transformer
+ * @default null
+ */
+ transformer: components["schemas"]["WanTransformerField"] | null;
/**
* type
- * @default z_image_control_output
+ * @default wan_lora_loader_output
* @constant
*/
- type: "z_image_control_output";
+ type: "wan_lora_loader_output";
};
/**
- * Denoise - Z-Image
- * @description Run the denoising process with a Z-Image model.
+ * WanLoRAVariantType
+ * @description Wan 2.2 LoRA variants, identifying which model family a LoRA targets.
*
- * Supports regional prompting by connecting multiple conditioning inputs with masks.
+ * Detected from the LoRA's inner attention dim: A14B has ``inner_dim=5120``,
+ * TI2V-5B has ``inner_dim=3072``. A14B and 5B LoRAs are NOT interchangeable —
+ * applying one against the wrong main model crashes in the layer patcher
+ * with a tensor-shape error.
+ * @enum {string}
*/
- ZImageDenoiseInvocation: {
+ WanLoRAVariantType: "a14b" | "5b";
+ /**
+ * Main Model - Wan 2.2
+ * @description Loads a Wan 2.2 model, outputting its submodels.
+ *
+ * Components can be mixed and matched, mirroring the Qwen Image loader pattern:
+ *
+ * - Transformer(s):
+ * * Diffusers main: emits ``transformer/`` and (for A14B) ``transformer_2/``
+ * from the same model record.
+ * * GGUF main: emits the single GGUF as the primary transformer; for A14B
+ * the second-expert GGUF must be wired to ``Transformer (Low Noise)``.
+ * - VAE: standalone Wan VAE > main (if Diffusers) > Component Source (Diffusers).
+ * - UMT5-XXL encoder: standalone Wan T5 encoder > main (if Diffusers) >
+ * Component Source (Diffusers).
+ *
+ * The Component Source slot lets users supply a Diffusers Wan main model purely
+ * for VAE / encoder extraction when the actual transformer is in a single-file
+ * format. Together, the standalone VAE + standalone encoder let a GGUF
+ * transformer run without a full ~30 GB Diffusers install.
+ */
+ WanModelLoaderInvocation: {
/**
* Id
* @description The id of this instance of an invocation. Must be unique among all instances of invocations.
@@ -33491,126 +34810,129 @@ export type components = {
*/
use_cache?: boolean;
/**
- * @description Latents tensor
- * @default null
+ * Transformer
+ * @description Wan 2.2 model (Transformer) to load
*/
- latents?: components["schemas"]["LatentsField"] | null;
+ model: components["schemas"]["ModelIdentifierField"];
/**
- * @description Noise tensor
+ * Transformer (Low Noise)
+ * @description Optional second GGUF transformer for the A14B low-noise expert. Only relevant when the main model is a single-file GGUF and the variant is A14B; ignored when the main is a Diffusers A14B (both experts are pulled from transformer/ and transformer_2/ already) or when the variant is TI2V-5B.
* @default null
*/
- noise?: components["schemas"]["LatentsField"] | null;
+ transformer_low_noise_model?: components["schemas"]["ModelIdentifierField"] | null;
/**
- * @description A mask of the region to apply the denoising process to. Values of 0.0 represent the regions to be fully denoised, and 1.0 represent the regions to be preserved.
- * @default null
- */
- denoise_mask?: components["schemas"]["DenoiseMaskField"] | null;
- /**
- * Denoising Start
- * @description When to start denoising, expressed a percentage of total steps
- * @default 0
- */
- denoising_start?: number;
- /**
- * Denoising End
- * @description When to stop denoising, expressed a percentage of total steps
- * @default 1
- */
- denoising_end?: number;
- /**
- * Add Noise
- * @description Add noise based on denoising start.
- * @default true
- */
- add_noise?: boolean;
- /**
- * Transformer
- * @description Z-Image model (Transformer) to load
+ * VAE
+ * @description Standalone Wan VAE model. If not set, the VAE is loaded from the main model (when in Diffusers format) or from the Component Source.
* @default null
*/
- transformer?: components["schemas"]["TransformerField"] | null;
+ vae_model?: components["schemas"]["ModelIdentifierField"] | null;
/**
- * Positive Conditioning
- * @description Positive conditioning tensor
+ * Wan T5 Encoder
+ * @description Standalone Wan UMT5-XXL encoder. If not set, the encoder is loaded from the main model (when in Diffusers format) or from the Component Source.
* @default null
*/
- positive_conditioning?: components["schemas"]["ZImageConditioningField"] | components["schemas"]["ZImageConditioningField"][] | null;
+ wan_t5_encoder_model?: components["schemas"]["ModelIdentifierField"] | null;
/**
- * Negative Conditioning
- * @description Negative conditioning tensor
+ * Component Source (Diffusers)
+ * @description Diffusers Wan main model to extract VAE and/or encoder from. Use this if you don't have separate VAE/encoder models. Ignored for any submodel that is provided separately.
* @default null
*/
- negative_conditioning?: components["schemas"]["ZImageConditioningField"] | components["schemas"]["ZImageConditioningField"][] | null;
- /**
- * Guidance Scale
- * @description Guidance scale for classifier-free guidance. 1.0 = no CFG (recommended for Z-Image-Turbo). Values > 1.0 amplify guidance.
- * @default 1
- */
- guidance_scale?: number;
+ component_source?: components["schemas"]["ModelIdentifierField"] | null;
/**
- * Width
- * @description Width of the generated image.
- * @default 1024
+ * type
+ * @default wan_model_loader
+ * @constant
*/
- width?: number;
+ type: "wan_model_loader";
+ };
+ /**
+ * WanModelLoaderOutput
+ * @description Wan 2.2 model loader output.
+ */
+ WanModelLoaderOutput: {
/**
- * Height
- * @description Height of the generated image.
- * @default 1024
+ * Transformer
+ * @description Wan transformer (one or two experts depending on the variant)
*/
- height?: number;
+ transformer: components["schemas"]["WanTransformerField"];
/**
- * Steps
- * @description Number of denoising steps. 8 recommended for Z-Image-Turbo.
- * @default 8
+ * UMT5-XXL Encoder
+ * @description UMT5-XXL tokenizer and text encoder for Wan 2.2
*/
- steps?: number;
+ wan_t5_encoder: components["schemas"]["WanT5EncoderField"];
/**
- * Seed
- * @description Randomness seed for reproducibility.
- * @default 0
+ * VAE
+ * @description VAE
*/
- seed?: number;
+ vae: components["schemas"]["VAEField"];
/**
- * @description Z-Image control conditioning for spatial control (Canny, HED, Depth, Pose, MLSD).
- * @default null
+ * type
+ * @default wan_model_loader_output
+ * @constant
*/
- control?: components["schemas"]["ZImageControlField"] | null;
+ type: "wan_model_loader_output";
+ };
+ /**
+ * WanRefImageConditioningField
+ * @description Reference-image conditioning for Wan 2.2 I2V.
+ *
+ * Carries the 20-channel VAE-latent condition tensor (4-channel first-frame
+ * mask + 16-channel ref-image latents). The denoise loop concatenates this
+ * to the 16-channel noise latents along the channel dim each step, producing
+ * the 36-channel input the I2V-A14B transformer expects.
+ *
+ * Also carries the spatial dims and frame count used to encode the image so
+ * the denoise node can sanity-check the user's width/height/num_frames — a
+ * latent temporal-dim mismatch is hard to debug from the downstream error.
+ */
+ WanRefImageConditioningField: {
/**
- * @description VAE Required for control conditioning.
- * @default null
+ * Condition Tensor Name
+ * @description Name of the saved [1, 20, T_lat, H/8, W/8] condition tensor.
*/
- vae?: components["schemas"]["VAEField"] | null;
+ condition_tensor_name: string;
/**
- * Shift
- * @description Override the timestep shift (mu) for the sigma schedule. Leave blank to auto-calculate based on image dimensions (recommended). Lower values (~0.5) produce less noise shifting, higher values (~1.15) produce more.
- * @default null
+ * Width
+ * @description Image width used during VAE encoding (matches denoise width).
*/
- shift?: number | null;
+ width: number;
/**
- * Scheduler
- * @description Scheduler (sampler) for the denoising process. Euler is the default and recommended. Heun is 2nd-order (better quality, 2x slower). LCM works with Turbo only (not Base).
- * @default euler
- * @enum {string}
+ * Height
+ * @description Image height used during VAE encoding (matches denoise height).
*/
- scheduler?: "euler" | "heun" | "lcm";
+ height: number;
/**
- * type
- * @default z_image_denoise
- * @constant
+ * Num Frames
+ * @description Pixel-frame count the condition was built for. 1 for single-frame I2V (image output), 81+ for video.
+ * @default 1
*/
- type: "z_image_denoise";
+ num_frames?: number;
};
/**
- * Denoise - Z-Image + Metadata
- * @description Run denoising process with a Z-Image transformer model + metadata.
+ * Reference Image - Wan 2.2
+ * @description VAE-encode a reference image into Wan 2.2 I2V conditioning.
+ *
+ * The wired VAE selects the output shape. With the 16-channel A14B VAE the
+ * output is a ``[1, 20, T_lat, height // 8, width // 8]`` condition (4-ch
+ * first-frame mask + 16-ch image latents) that the denoise loop concatenates
+ * to the noise latents each step, producing the 36-channel input the
+ * I2V-A14B transformer expects. With the 48-channel TI2V-5B VAE the output
+ * is a ``[1, 48, 1, height // 16, width // 16]`` condition that the denoise
+ * loop blends into the noisy latents at every step.
+ *
+ * For image (single-frame) I2V leave ``num_frames=1`` (T_lat=1). For A14B
+ * video I2V set ``num_frames`` to match the value on the video-denoise node
+ * (e.g. 81 for the Wan 2.2 reference defaults); TI2V-5B always uses the
+ * single-frame condition.
+ *
+ * Supply an optional ``end_image`` for **first-last-frame interpolation
+ * (FLF2V)** — the model then interpolates the motion from ``image`` (first
+ * frame) to ``end_image`` (final frame). FLF2V is I2V-A14B video only
+ * (``num_frames > 1``); it is not supported for TI2V-5B or single-frame I2V.
+ *
+ * For T2V (no reference image), omit this node entirely.
*/
- ZImageDenoiseMetaInvocation: {
- /**
- * @description Optional metadata to be saved with the image
- * @default null
- */
- metadata?: components["schemas"]["MetadataField"] | null;
+ WanRefImageEncoderInvocation: {
/**
* Id
* @description The id of this instance of an invocation. Must be unique among all instances of invocations.
@@ -33629,219 +34951,181 @@ export type components = {
*/
use_cache?: boolean;
/**
- * @description Latents tensor
- * @default null
- */
- latents?: components["schemas"]["LatentsField"] | null;
- /**
- * @description Noise tensor
- * @default null
- */
- noise?: components["schemas"]["LatentsField"] | null;
- /**
- * @description A mask of the region to apply the denoising process to. Values of 0.0 represent the regions to be fully denoised, and 1.0 represent the regions to be preserved.
- * @default null
- */
- denoise_mask?: components["schemas"]["DenoiseMaskField"] | null;
- /**
- * Denoising Start
- * @description When to start denoising, expressed a percentage of total steps
- * @default 0
- */
- denoising_start?: number;
- /**
- * Denoising End
- * @description When to stop denoising, expressed a percentage of total steps
- * @default 1
- */
- denoising_end?: number;
- /**
- * Add Noise
- * @description Add noise based on denoising start.
- * @default true
- */
- add_noise?: boolean;
- /**
- * Transformer
- * @description Z-Image model (Transformer) to load
- * @default null
- */
- transformer?: components["schemas"]["TransformerField"] | null;
- /**
- * Positive Conditioning
- * @description Positive conditioning tensor
+ * @description Reference image to condition on (the first frame).
* @default null
*/
- positive_conditioning?: components["schemas"]["ZImageConditioningField"] | components["schemas"]["ZImageConditioningField"][] | null;
+ image?: components["schemas"]["ImageField"] | null;
/**
- * Negative Conditioning
- * @description Negative conditioning tensor
+ * VAE
+ * @description VAE
* @default null
*/
- negative_conditioning?: components["schemas"]["ZImageConditioningField"] | components["schemas"]["ZImageConditioningField"][] | null;
- /**
- * Guidance Scale
- * @description Guidance scale for classifier-free guidance. 1.0 = no CFG (recommended for Z-Image-Turbo). Values > 1.0 amplify guidance.
- * @default 1
- */
- guidance_scale?: number;
+ vae?: components["schemas"]["VAEField"] | null;
/**
* Width
- * @description Width of the generated image.
+ * @description Width to resize the reference image to (must match denoise width).
* @default 1024
*/
width?: number;
/**
* Height
- * @description Height of the generated image.
+ * @description Height to resize the reference image to (must match denoise height).
* @default 1024
*/
height?: number;
/**
- * Steps
- * @description Number of denoising steps. 8 recommended for Z-Image-Turbo.
- * @default 8
- */
- steps?: number;
- /**
- * Seed
- * @description Randomness seed for reproducibility.
- * @default 0
- */
- seed?: number;
- /**
- * @description Z-Image control conditioning for spatial control (Canny, HED, Depth, Pose, MLSD).
- * @default null
+ * Number of Frames
+ * @description Pixel-frame count to build the condition for. Use 1 for single-frame image I2V. For video I2V, set this to match the video-denoise node's num_frames (and ensure (num_frames - 1) %% 4 == 0, e.g. 81).
+ * @default 1
*/
- control?: components["schemas"]["ZImageControlField"] | null;
+ num_frames?: number;
/**
- * @description VAE Required for control conditioning.
+ * End Image (FLF2V)
+ * @description Optional end frame for first-last-frame interpolation (FLF2V). When set, the video interpolates from the reference image (first frame) to this image (final frame). I2V-A14B video only (num_frames > 1); not supported for TI2V-5B or single-frame I2V.
* @default null
*/
- vae?: components["schemas"]["VAEField"] | null;
+ end_image?: components["schemas"]["ImageField"] | null;
/**
- * Shift
- * @description Override the timestep shift (mu) for the sigma schedule. Leave blank to auto-calculate based on image dimensions (recommended). Lower values (~0.5) produce less noise shifting, higher values (~1.15) produce more.
- * @default null
+ * type
+ * @default wan_ref_image_encoder
+ * @constant
*/
- shift?: number | null;
+ type: "wan_ref_image_encoder";
+ };
+ /**
+ * WanRefImageOutput
+ * @description Output of a Wan 2.2 reference-image VAE-encoder.
+ */
+ WanRefImageOutput: {
/**
- * Scheduler
- * @description Scheduler (sampler) for the denoising process. Euler is the default and recommended. Heun is 2nd-order (better quality, 2x slower). LCM works with Turbo only (not Base).
- * @default euler
- * @enum {string}
+ * Reference Image
+ * @description VAE-latent reference-image conditioning for Wan 2.2 I2V.
*/
- scheduler?: "euler" | "heun" | "lcm";
+ ref_image: components["schemas"]["WanRefImageConditioningField"];
/**
* type
- * @default z_image_denoise_meta
+ * @default wan_ref_image_output
* @constant
*/
- type: "z_image_denoise_meta";
+ type: "wan_ref_image_output";
};
/**
- * Image to Latents - Z-Image
- * @description Generates latents from an image using Z-Image VAE (supports both Diffusers and FLUX VAE).
+ * WanT5EncoderField
+ * @description Field for the UMT5-XXL text encoder used by Wan 2.2 models.
*/
- ZImageImageToLatentsInvocation: {
- /**
- * @description The board to save the image to
- * @default null
- */
- board?: components["schemas"]["BoardField"] | null;
+ WanT5EncoderField: {
+ /** @description Info to load tokenizer submodel */
+ tokenizer: components["schemas"]["ModelIdentifierField"];
+ /** @description Info to load text_encoder submodel */
+ text_encoder: components["schemas"]["ModelIdentifierField"];
/**
- * @description Optional metadata to be saved with the image
- * @default null
+ * Loras
+ * @description LoRAs to apply on model loading
*/
- metadata?: components["schemas"]["MetadataField"] | null;
+ loras?: components["schemas"]["LoRAField"][];
+ };
+ /**
+ * WanT5Encoder_WanT5Encoder_Config
+ * @description UMT5-XXL encoder in diffusers folder layout.
+ *
+ * Accepts either:
+ * - A directory containing ``text_encoder/`` (and typically ``tokenizer/``) ─ the
+ * shape produced by ``Wan-AI/Wan2.2-T2V-A14B::text_encoder+tokenizer``.
+ * - A bare ``text_encoder/`` directory whose own ``config.json`` declares
+ * ``model_type: umt5``.
+ */
+ WanT5Encoder_WanT5Encoder_Config: {
/**
- * Id
- * @description The id of this instance of an invocation. Must be unique among all instances of invocations.
+ * Key
+ * @description A unique key for this model.
*/
- id: string;
+ key: string;
/**
- * Is Intermediate
- * @description Whether or not this is an intermediate invocation.
- * @default false
+ * Hash
+ * @description The hash of the model file(s).
*/
- is_intermediate?: boolean;
+ hash: string;
/**
- * Use Cache
- * @description Whether or not to use the cache
- * @default true
+ * Path
+ * @description Path to the model on the filesystem. Relative paths are relative to the Invoke root directory.
*/
- use_cache?: boolean;
+ path: string;
/**
- * @description The image to encode.
- * @default null
+ * File Size
+ * @description The size of the model in bytes.
*/
- image?: components["schemas"]["ImageField"] | null;
+ file_size: number;
/**
- * @description VAE
- * @default null
+ * Name
+ * @description Name of the model.
*/
- vae?: components["schemas"]["VAEField"] | null;
+ name: string;
/**
- * type
- * @default z_image_i2l
- * @constant
+ * Description
+ * @description Model description
*/
- type: "z_image_i2l";
- };
- /**
- * Latents to Image - Z-Image
- * @description Generates an image from latents using Z-Image VAE (supports both Diffusers and FLUX VAE).
- */
- ZImageLatentsToImageInvocation: {
+ description: string | null;
/**
- * @description The board to save the image to
- * @default null
+ * Source
+ * @description The original source of the model (path, URL or repo_id).
*/
- board?: components["schemas"]["BoardField"] | null;
+ source: string;
+ /** @description The type of source */
+ source_type: components["schemas"]["ModelSourceType"];
/**
- * @description Optional metadata to be saved with the image
- * @default null
+ * Source Api Response
+ * @description The original API response from the source, as stringified JSON.
*/
- metadata?: components["schemas"]["MetadataField"] | null;
+ source_api_response: string | null;
/**
- * Id
- * @description The id of this instance of an invocation. Must be unique among all instances of invocations.
+ * Source Url
+ * @description Optional URL for the model (e.g. download page or model page).
*/
- id: string;
+ source_url: string | null;
/**
- * Is Intermediate
- * @description Whether or not this is an intermediate invocation.
- * @default false
+ * Cover Image
+ * @description Url for image to preview model
*/
- is_intermediate?: boolean;
+ cover_image: string | null;
/**
- * Use Cache
- * @description Whether or not to use the cache
- * @default true
+ * Base
+ * @default any
+ * @constant
*/
- use_cache?: boolean;
+ base: "any";
/**
- * @description Latents tensor
- * @default null
+ * Type
+ * @default wan_t5_encoder
+ * @constant
*/
- latents?: components["schemas"]["LatentsField"] | null;
+ type: "wan_t5_encoder";
/**
- * @description VAE
- * @default null
+ * Format
+ * @default wan_t5_encoder
+ * @constant
*/
- vae?: components["schemas"]["VAEField"] | null;
+ format: "wan_t5_encoder";
/**
- * type
- * @default z_image_l2i
- * @constant
+ * Cpu Only
+ * @description Whether this model should run on CPU only
*/
- type: "z_image_l2i";
+ cpu_only: boolean | null;
};
/**
- * Apply LoRA Collection - Z-Image
- * @description Applies a collection of LoRAs to a Z-Image transformer.
+ * Wan 2.2 TI2V Ideal Dimensions (5B)
+ * @description Ideal dimensions for the Wan 2.2 TI2V-5B model.
+ *
+ * Use this node for TI2V-5B only. For the A14B models (I2V-A14B / T2V-A14B) use
+ * "Wan 2.2 I2V Ideal Dimensions" instead — those need multiples of 16, and this
+ * node's multiples-of-32 dims would overshoot their pixel grid.
+ *
+ * Identical to the A14B node but snaps each dimension to a multiple of 32 instead
+ * of 16: the Wan 2.2-VAE used by TI2V-5B applies 16x spatial compression and the
+ * transformer adds a 2x patch on top, so pixel dims must divide by 32 for the
+ * patchify step. Wire from ``Image Primitive``'s width/height outputs and into
+ * the matching ``wan_denoise`` inputs.
*/
- ZImageLoRACollectionLoader: {
+ WanTI2VIdealDimensionsInvocation: {
/**
* Id
* @description The id of this instance of an invocation. Must be unique among all instances of invocations.
@@ -33860,35 +35144,47 @@ export type components = {
*/
use_cache?: boolean;
/**
- * LoRAs
- * @description LoRA models and weights. May be a single LoRA or collection.
- * @default null
+ * Width
+ * @description Source image width in pixels.
+ * @default 1024
*/
- loras?: components["schemas"]["LoRAField"] | components["schemas"]["LoRAField"][] | null;
+ width?: number;
/**
- * Transformer
- * @description Transformer
- * @default null
+ * Height
+ * @description Source image height in pixels.
+ * @default 1024
*/
- transformer?: components["schemas"]["TransformerField"] | null;
+ height?: number;
/**
- * Qwen3 Encoder
- * @description Qwen3 tokenizer and text encoder
- * @default null
+ * Target Resolution
+ * @description Short-side resolution preset. 480p and 720p are Wan 2.2's native training resolutions; 1080p works but is extrapolation and costs ~2.25x the memory of 720p.
+ * @default 720p
+ * @enum {string}
*/
- qwen3_encoder?: components["schemas"]["Qwen3EncoderField"] | null;
+ target_resolution?: "480p" | "720p" | "1080p";
+ /**
+ * Rounding
+ * @description How to snap each dimension to a multiple of 32. 'floor' rounds down — safest for VRAM, guaranteed not to exceed the unsnapped target. 'ceiling' rounds up. 'nearest' minimizes aspect-ratio drift (default).
+ * @default nearest
+ * @enum {string}
+ */
+ rounding?: "nearest" | "floor" | "ceiling";
/**
* type
- * @default z_image_lora_collection_loader
+ * @default wan_ti2v_ideal_dimensions
* @constant
*/
- type: "z_image_lora_collection_loader";
+ type: "wan_ti2v_ideal_dimensions";
};
/**
- * Apply LoRA - Z-Image
- * @description Apply a LoRA model to a Z-Image transformer and/or Qwen3 text encoder.
+ * Prompt - Wan 2.2
+ * @description Encodes a text prompt for Wan 2.2 using the UMT5-XXL encoder.
+ *
+ * Output is the encoder's last hidden state (shape: [seq_len=512, 4096]) plus
+ * an attention mask marking valid (non-padding) tokens. The Wan transformer
+ * consumes these directly as ``encoder_hidden_states``.
*/
- ZImageLoRALoaderInvocation: {
+ WanTextEncoderInvocation: {
/**
* Id
* @description The id of this instance of an invocation. Must be unique among all instances of invocations.
@@ -33907,70 +35203,104 @@ export type components = {
*/
use_cache?: boolean;
/**
- * LoRA
- * @description LoRA model to load
- * @default null
- */
- lora?: components["schemas"]["ModelIdentifierField"] | null;
- /**
- * Weight
- * @description The weight at which the LoRA is applied to each model
- * @default 0.75
- */
- weight?: number;
- /**
- * Z-Image Transformer
- * @description Transformer
+ * Prompt
+ * @description Text prompt for Wan 2.2.
* @default null
*/
- transformer?: components["schemas"]["TransformerField"] | null;
+ prompt?: string | null;
/**
- * Qwen3 Encoder
- * @description Qwen3 tokenizer and text encoder
+ * UMT5-XXL Encoder
+ * @description UMT5-XXL tokenizer and text encoder for Wan 2.2
* @default null
*/
- qwen3_encoder?: components["schemas"]["Qwen3EncoderField"] | null;
+ wan_t5_encoder?: components["schemas"]["WanT5EncoderField"] | null;
/**
* type
- * @default z_image_lora_loader
+ * @default wan_text_encoder
* @constant
*/
- type: "z_image_lora_loader";
+ type: "wan_text_encoder";
};
/**
- * ZImageLoRALoaderOutput
- * @description Z-Image LoRA Loader Output
+ * WanTransformerField
+ * @description Transformer field for Wan 2.2 models.
+ *
+ * Wan 2.2 A14B is a Mixture-of-Experts model with two transformer experts:
+ * a high-noise expert (active at large timesteps) and a low-noise expert
+ * (active at small timesteps). TI2V-5B is a single-transformer model and only
+ * populates ``transformer``.
+ *
+ * ``boundary_ratio`` matches Diffusers' ``WanPipeline`` semantics: it's the
+ * boundary timestep as a fraction of ``num_train_timesteps`` (typically 1000),
+ * so ``boundary_ratio=0.875`` means the high-noise expert handles t >= 875 and
+ * the low-noise expert handles t < 875.
*/
- ZImageLoRALoaderOutput: {
+ WanTransformerField: {
+ /** @description Primary transformer submodel. For A14B this is the high-noise expert. */
+ transformer: components["schemas"]["ModelIdentifierField"];
/**
- * Z-Image Transformer
- * @description Transformer
+ * @description Low-noise transformer expert (Wan 2.2 A14B only). None for TI2V-5B.
* @default null
*/
- transformer: components["schemas"]["TransformerField"] | null;
+ transformer_low_noise?: components["schemas"]["ModelIdentifierField"] | null;
/**
- * Qwen3 Encoder
- * @description Qwen3 tokenizer and text encoder
- * @default null
+ * Loras
+ * @description LoRAs to apply to the primary transformer. For A14B applied to the high-noise expert.
*/
- qwen3_encoder: components["schemas"]["Qwen3EncoderField"] | null;
+ loras?: components["schemas"]["LoRAField"][];
/**
- * type
- * @default z_image_lora_loader_output
- * @constant
+ * Loras Low Noise
+ * @description LoRAs to apply to the low-noise expert (Wan 2.2 A14B). The Wan LoRA loader routes 'both'- and 'low'-targeted LoRAs here; if empty, no LoRAs are applied to the low-noise expert (a 'high'-targeted LoRA must not leak onto it).
*/
- type: "z_image_lora_loader_output";
+ loras_low_noise?: components["schemas"]["LoRAField"][];
+ /**
+ * Boundary Ratio
+ * @description Boundary timestep as a fraction of num_train_timesteps (Wan 2.2 A14B only). High-noise expert: t >= boundary_ratio * num_train_timesteps. Low-noise expert: t below. Ignored for TI2V-5B.
+ * @default 0.875
+ */
+ boundary_ratio?: number;
};
/**
- * Main Model - Z-Image
- * @description Loads a Z-Image model, outputting its submodels.
+ * WanVariantType
+ * @description Wan 2.2 model variants.
*
- * Similar to FLUX, you can mix and match components:
- * - Transformer: From Z-Image main model (GGUF quantized or Diffusers format)
- * - VAE: Separate FLUX VAE (shared with FLUX models) or from a Diffusers Z-Image model
- * - Qwen3 Encoder: Separate Qwen3Encoder model or from a Diffusers Z-Image model
+ * All variants are used for image generation at num_frames=1. The A14B family
+ * is a Mixture-of-Experts (high-noise + low-noise) totalling ~28B params; the
+ * T2V sub-variant takes text only, while the I2V sub-variant additionally
+ * conditions on a reference image (encoded by the VAE and concatenated to the
+ * noise latents along the channel dim — its transformer has ``in_channels=36``
+ * instead of ``16``). TI2V-5B is a single ~5B transformer with a
+ * higher-compression VAE (z_dim=48).
+ * @enum {string}
*/
- ZImageModelLoaderInvocation: {
+ WanVariantType: "t2v_a14b" | "i2v_a14b" | "ti2v_5b";
+ /**
+ * Denoise Video - Wan 2.2
+ * @description Run the Wan 2.2 denoising loop on a multi-frame latent tensor.
+ *
+ * The output is a 5D ``[1, C, T_lat, H/8, W/8]`` latent tensor ready for
+ * :class:`WanLatentsToVideoInvocation` to VAE-decode and encode as MP4.
+ *
+ * Mirrors :class:`WanDenoiseInvocation` for the per-step logic (CFG, MoE
+ * expert swap at the boundary timestep, LoRA patching, scheduler selection).
+ * Differences from the image denoise:
+ *
+ * * The noise tensor has a real temporal dim built from ``num_frames``.
+ * * The I2V condition is built across all latent frames (frame 0
+ * conditioned, rest zero) via
+ * :func:`encode_reference_image_to_video_condition` upstream — the
+ * ``ref_image`` field on this node carries a tensor of shape
+ * ``[1, 20, T_lat, H_lat, W_lat]`` instead of ``[1, 20, 1, ...]``.
+ * * No ``denoising_start`` / ``denoising_end`` / initial-latents inputs.
+ * The image denoise node uses those for img2img (noise injection on an
+ * existing latent), but image-conditioned video generation flows through
+ * the reference-frame conditioning mechanism instead — the first frame
+ * drives subsequent frames at every step, so a partial-schedule run from
+ * an initial latent has no analogue here. Run the schedule from t=1
+ * to t=0 every time. The base ``WanDenoiseInvocation`` still handles
+ * the img2img case for stills.
+ */
+ WanVideoDenoiseInvocation: {
/**
* Id
* @description The id of this instance of an invocation. Must be unique among all instances of invocations.
@@ -33990,109 +35320,1621 @@ export type components = {
use_cache?: boolean;
/**
* Transformer
- * @description Z-Image model (Transformer) to load
+ * @description Wan transformer field. Supported: T2V-A14B / I2V-A14B (dual-expert) and TI2V-5B (single-expert, handles both T2V and I2V). All three accept a Reference Image input for image-to-video; A14B uses the 36-channel concat scheme while TI2V-5B uses the expand_timesteps first-frame-mask blend.
+ * @default null
*/
- model: components["schemas"]["ModelIdentifierField"];
+ transformer?: components["schemas"]["WanTransformerField"] | null;
/**
- * VAE
- * @description Standalone VAE model. Z-Image uses the same VAE as FLUX (16-channel). If not provided, VAE will be loaded from the Qwen3 Source model.
+ * @description Positive conditioning tensor
* @default null
*/
- vae_model?: components["schemas"]["ModelIdentifierField"] | null;
+ positive_conditioning?: components["schemas"]["WanConditioningField"] | null;
/**
- * Qwen3 Encoder
- * @description Standalone Qwen3 Encoder model. If not provided, encoder will be loaded from the Qwen3 Source model.
+ * @description Negative conditioning tensor
* @default null
*/
- qwen3_encoder_model?: components["schemas"]["ModelIdentifierField"] | null;
+ negative_conditioning?: components["schemas"]["WanConditioningField"] | null;
/**
- * Qwen3 Source (Diffusers)
- * @description Diffusers Z-Image model to extract VAE and/or Qwen3 encoder from. Use this if you don't have separate VAE/Qwen3 models. Ignored if both VAE and Qwen3 Encoder are provided separately.
+ * Reference Image
+ * @description Reference-image (VAE-latent) conditioning for Wan 2.2 I2V.
* @default null
*/
- qwen3_source_model?: components["schemas"]["ModelIdentifierField"] | null;
+ ref_image?: components["schemas"]["WanRefImageConditioningField"] | null;
/**
- * type
- * @default z_image_model_loader
- * @constant
+ * Guidance Scale
+ * @description Classifier-free guidance scale. Wan 2.2 video reference uses 5.0 for the high-noise expert and 4.0 for the low-noise expert.
+ * @default 5
*/
- type: "z_image_model_loader";
- };
- /**
- * ZImageModelLoaderOutput
- * @description Z-Image base model loader output.
- */
- ZImageModelLoaderOutput: {
+ guidance_scale?: number;
/**
- * Transformer
- * @description Transformer
+ * Guidance Scale (Low Noise)
+ * @description Optional separate CFG scale for the low-noise expert (Wan 2.2 A14B only). Values below 1.0 fall back to the primary 'Guidance Scale'.
+ * @default 4
*/
- transformer: components["schemas"]["TransformerField"];
+ guidance_scale_low_noise?: number | null;
/**
- * Qwen3 Encoder
- * @description Qwen3 tokenizer and text encoder
+ * Width
+ * @description Width of the generated video.
+ * @default 832
*/
- qwen3_encoder: components["schemas"]["Qwen3EncoderField"];
+ width?: number;
/**
- * VAE
- * @description VAE
+ * Height
+ * @description Height of the generated video.
+ * @default 480
*/
- vae: components["schemas"]["VAEField"];
+ height?: number;
/**
- * type
- * @default z_image_model_loader_output
- * @constant
+ * Number of Frames
+ * @description Number of output frames. Must satisfy (num_frames - 1) %% 4 == 0 so the latent temporal dim divides cleanly. Wan 2.2 was trained at 81 frames @ 16 FPS (~5 s).
+ * @default 81
*/
- type: "z_image_model_loader_output";
- };
- /**
- * Seed Variance Enhancer - Z-Image
- * @description Adds seed-based noise to Z-Image conditioning to increase variance between seeds.
- *
- * Z-Image-Turbo can produce relatively similar images with different seeds,
- * making it harder to explore variations of a prompt. This node implements
- * reproducible, seed-based noise injection into text embeddings to increase
- * visual variation while maintaining reproducibility.
- *
- * The noise strength is auto-calibrated relative to the embedding's standard
- * deviation, ensuring consistent results across different prompts.
- */
- ZImageSeedVarianceEnhancerInvocation: {
+ num_frames?: number;
/**
- * Id
- * @description The id of this instance of an invocation. Must be unique among all instances of invocations.
+ * Steps
+ * @description Number of denoising steps.
+ * @default 40
*/
- id: string;
+ steps?: number;
/**
- * Is Intermediate
- * @description Whether or not this is an intermediate invocation.
- * @default false
+ * Seed
+ * @description Randomness seed for reproducibility.
+ * @default 0
*/
- is_intermediate?: boolean;
+ seed?: number;
/**
- * Use Cache
- * @description Whether or not to use the cache
- * @default true
+ * type
+ * @default wan_video_denoise
+ * @constant
*/
- use_cache?: boolean;
+ type: "wan_video_denoise";
+ };
+ /** Workflow */
+ Workflow: {
/**
- * Conditioning
- * @description Conditioning tensor
- * @default null
+ * Name
+ * @description The name of the workflow.
*/
- conditioning?: components["schemas"]["ZImageConditioningField"] | null;
+ name: string;
/**
- * Seed
- * @description Seed for reproducible noise generation. Different seeds produce different noise patterns.
- * @default 0
+ * Author
+ * @description The author of the workflow.
*/
- seed?: number;
+ author: string;
/**
- * Strength
- * @description Noise strength as multiplier of embedding std. 0=off, 0.1=subtle, 0.5=strong.
- * @default 0.1
+ * Description
+ * @description The description of the workflow.
*/
- strength?: number;
+ description: string;
+ /**
+ * Version
+ * @description The version of the workflow.
+ */
+ version: string;
+ /**
+ * Contact
+ * @description The contact of the workflow.
+ */
+ contact: string;
+ /**
+ * Tags
+ * @description The tags of the workflow.
+ */
+ tags: string;
+ /**
+ * Notes
+ * @description The notes of the workflow.
+ */
+ notes: string;
+ /**
+ * Exposedfields
+ * @description The exposed fields of the workflow.
+ */
+ exposedFields: components["schemas"]["ExposedField"][];
+ /** @description The meta of the workflow. */
+ meta: components["schemas"]["WorkflowMeta"];
+ /**
+ * Nodes
+ * @description The nodes of the workflow.
+ */
+ nodes: {
+ [key: string]: components["schemas"]["JsonValue"];
+ }[];
+ /**
+ * Edges
+ * @description The edges of the workflow.
+ */
+ edges: {
+ [key: string]: components["schemas"]["JsonValue"];
+ }[];
+ /**
+ * Form
+ * @description The form of the workflow.
+ */
+ form?: {
+ [key: string]: components["schemas"]["JsonValue"];
+ } | null;
+ /**
+ * Id
+ * @description The id of the workflow.
+ */
+ id: string;
+ };
+ /**
+ * WorkflowAccessRevokedEvent
+ * @description Event model for workflow_access_revoked.
+ */
+ WorkflowAccessRevokedEvent: {
+ /**
+ * Timestamp
+ * @description The timestamp of the event
+ */
+ timestamp: number;
+ /**
+ * Workflow Id
+ * @description The ID of the workflow
+ */
+ workflow_id: string;
+ /**
+ * User Id
+ * @description The owner of the workflow
+ */
+ user_id: string;
+ };
+ /** WorkflowAndGraphResponse */
+ WorkflowAndGraphResponse: {
+ /**
+ * Workflow
+ * @description The workflow used to generate the image, as stringified JSON
+ */
+ workflow: string | null;
+ /**
+ * Graph
+ * @description The graph used to generate the image, as stringified JSON
+ */
+ graph: string | null;
+ };
+ /** WorkflowCallCompatibility */
+ WorkflowCallCompatibility: {
+ /**
+ * Is Callable
+ * @description Whether the workflow can currently be executed by call_saved_workflow.
+ */
+ is_callable: boolean;
+ /** @description Structured compatibility result. */
+ reason: components["schemas"]["WorkflowCallCompatibilityReason"];
+ /**
+ * Message
+ * @description Human-readable compatibility detail when unavailable.
+ */
+ message?: string | null;
+ };
+ /**
+ * WorkflowCallCompatibilityReason
+ * @enum {string}
+ */
+ WorkflowCallCompatibilityReason: "ok" | "missing_workflow_return" | "multiple_workflow_return" | "unsupported_node" | "unsupported_batch_input" | "invalid_graph" | "invalid_inputs" | "exceeds_capacity" | "unknown";
+ /**
+ * WorkflowCallExecution
+ * @description Tracks one parent/child workflow-call relationship and its lifecycle.
+ */
+ WorkflowCallExecution: {
+ /**
+ * Id
+ * @description The workflow-call execution id.
+ */
+ id?: string;
+ /**
+ * Parent Session Id
+ * @description The parent graph execution state id.
+ */
+ parent_session_id: string;
+ /**
+ * Child Session Id
+ * @description The child graph execution state id, if any.
+ */
+ child_session_id?: string | null;
+ /**
+ * Prepared Call Node Id
+ * @description The prepared exec node id for the parent call site.
+ */
+ prepared_call_node_id: string;
+ /**
+ * Source Call Node Id
+ * @description The source graph node id for the parent call site.
+ */
+ source_call_node_id: string;
+ /**
+ * Workflow Id
+ * @description The saved workflow being called.
+ */
+ workflow_id: string;
+ /**
+ * Depth
+ * @description The 1-based depth of this call frame.
+ */
+ depth: number;
+ /**
+ * Status
+ * @description The current workflow-call lifecycle state.
+ * @enum {string}
+ */
+ status: "waiting_for_child" | "running_child" | "completed" | "failed";
+ /**
+ * Error Message
+ * @description Failure reason, if the call failed.
+ */
+ error_message?: string | null;
+ /**
+ * Child Session Ids
+ * @description All child graph execution state ids.
+ */
+ child_session_ids?: string[];
+ /**
+ * Child Item Ids
+ * @description Child queue item ids in enqueue order.
+ */
+ child_item_ids?: number[];
+ /**
+ * Expected Child Count
+ * @description The number of child executions for this call.
+ * @default 1
+ */
+ expected_child_count?: number;
+ /**
+ * Completed Child Item Ids
+ * @description The child queue item ids whose workflow_return outputs have been aggregated.
+ */
+ completed_child_item_ids?: number[];
+ /**
+ * Aggregated Values
+ * @description The aggregated workflow_return values accumulated from child executions.
+ */
+ aggregated_values?: {
+ [key: string]: unknown[];
+ };
+ /**
+ * Child Outputs
+ * @description Workflow return values keyed by child queue item id.
+ */
+ child_outputs?: {
+ [key: string]: {
+ [key: string]: unknown;
+ };
+ };
+ };
+ /**
+ * WorkflowCallFrame
+ * @description Represents one workflow-call frame in a nested call chain.
+ */
+ WorkflowCallFrame: {
+ /**
+ * Prepared Call Node Id
+ * @description The prepared exec node id for the call site.
+ */
+ prepared_call_node_id: string;
+ /**
+ * Source Call Node Id
+ * @description The source graph node id for the call site.
+ */
+ source_call_node_id: string;
+ /**
+ * Workflow Id
+ * @description The saved workflow being called.
+ */
+ workflow_id: string;
+ /**
+ * Depth
+ * @description The 1-based depth of this call frame.
+ */
+ depth: number;
+ };
+ /**
+ * WorkflowCallParentRef
+ * @description Reference from a child execution state back to its parent workflow-call relationship.
+ */
+ WorkflowCallParentRef: {
+ /**
+ * Workflow Call Id
+ * @description The workflow-call execution id.
+ */
+ workflow_call_id: string;
+ /**
+ * Parent Session Id
+ * @description The parent graph execution state id.
+ */
+ parent_session_id: string;
+ /**
+ * Prepared Call Node Id
+ * @description The prepared exec node id for the parent call site.
+ */
+ prepared_call_node_id: string;
+ /**
+ * Source Call Node Id
+ * @description The source graph node id for the parent call site.
+ */
+ source_call_node_id: string;
+ /**
+ * Workflow Id
+ * @description The saved workflow being called.
+ */
+ workflow_id: string;
+ /**
+ * Depth
+ * @description The 1-based depth of this call frame.
+ */
+ depth: number;
+ };
+ /**
+ * WorkflowCategory
+ * @enum {string}
+ */
+ WorkflowCategory: "user" | "default";
+ /**
+ * WorkflowCreatedEvent
+ * @description Event model for workflow_created
+ */
+ WorkflowCreatedEvent: {
+ /**
+ * Timestamp
+ * @description The timestamp of the event
+ */
+ timestamp: number;
+ /**
+ * Workflow Id
+ * @description The ID of the workflow
+ */
+ workflow_id: string;
+ /**
+ * User Id
+ * @description The owner of the workflow
+ */
+ user_id: string;
+ /**
+ * Is Public
+ * @description Whether the workflow is shared with all users
+ */
+ is_public: boolean;
+ };
+ /**
+ * WorkflowDeletedEvent
+ * @description Event model for workflow_deleted
+ */
+ WorkflowDeletedEvent: {
+ /**
+ * Timestamp
+ * @description The timestamp of the event
+ */
+ timestamp: number;
+ /**
+ * Workflow Id
+ * @description The ID of the workflow
+ */
+ workflow_id: string;
+ /**
+ * User Id
+ * @description The owner of the workflow
+ */
+ user_id: string;
+ /**
+ * Is Public
+ * @description Whether the workflow was shared when it was deleted
+ */
+ is_public: boolean;
+ };
+ /** WorkflowMeta */
+ WorkflowMeta: {
+ /**
+ * Version
+ * @description The version of the workflow schema.
+ */
+ version: string;
+ /** @description The category of the workflow (user or default). */
+ category: components["schemas"]["WorkflowCategory"];
+ };
+ /** WorkflowRecordDTO */
+ WorkflowRecordDTO: {
+ /**
+ * Workflow Id
+ * @description The id of the workflow.
+ */
+ workflow_id: string;
+ /**
+ * Name
+ * @description The name of the workflow.
+ */
+ name: string;
+ /**
+ * Created At
+ * @description The created timestamp of the workflow.
+ */
+ created_at: string;
+ /**
+ * Updated At
+ * @description The updated timestamp of the workflow.
+ */
+ updated_at: string;
+ /**
+ * Opened At
+ * @description The opened timestamp of the workflow.
+ */
+ opened_at?: string | null;
+ /**
+ * User Id
+ * @description The id of the user who owns this workflow.
+ */
+ user_id: string;
+ /**
+ * Is Public
+ * @description Whether this workflow is shared with all users.
+ */
+ is_public: boolean;
+ /** @description The workflow. */
+ workflow: components["schemas"]["Workflow"];
+ };
+ /** WorkflowRecordListItemWithThumbnailDTO */
+ WorkflowRecordListItemWithThumbnailDTO: {
+ /**
+ * Workflow Id
+ * @description The id of the workflow.
+ */
+ workflow_id: string;
+ /**
+ * Name
+ * @description The name of the workflow.
+ */
+ name: string;
+ /**
+ * Created At
+ * @description The created timestamp of the workflow.
+ */
+ created_at: string;
+ /**
+ * Updated At
+ * @description The updated timestamp of the workflow.
+ */
+ updated_at: string;
+ /**
+ * Opened At
+ * @description The opened timestamp of the workflow.
+ */
+ opened_at?: string | null;
+ /**
+ * User Id
+ * @description The id of the user who owns this workflow.
+ */
+ user_id: string;
+ /**
+ * Is Public
+ * @description Whether this workflow is shared with all users.
+ */
+ is_public: boolean;
+ /**
+ * Description
+ * @description The description of the workflow.
+ */
+ description: string;
+ /** @description The description of the workflow. */
+ category: components["schemas"]["WorkflowCategory"];
+ /**
+ * Tags
+ * @description The tags of the workflow.
+ */
+ tags: string;
+ /**
+ * Thumbnail Url
+ * @description The URL of the workflow thumbnail.
+ */
+ thumbnail_url?: string | null;
+ /** @description Whether this workflow is currently callable by call_saved_workflow. */
+ call_saved_workflow_compatibility?: components["schemas"]["WorkflowCallCompatibility"] | null;
+ };
+ /**
+ * WorkflowRecordOrderBy
+ * @description The order by options for workflow records
+ * @enum {string}
+ */
+ WorkflowRecordOrderBy: "created_at" | "updated_at" | "opened_at" | "name" | "is_public";
+ /** WorkflowRecordWithThumbnailDTO */
+ WorkflowRecordWithThumbnailDTO: {
+ /**
+ * Workflow Id
+ * @description The id of the workflow.
+ */
+ workflow_id: string;
+ /**
+ * Name
+ * @description The name of the workflow.
+ */
+ name: string;
+ /**
+ * Created At
+ * @description The created timestamp of the workflow.
+ */
+ created_at: string;
+ /**
+ * Updated At
+ * @description The updated timestamp of the workflow.
+ */
+ updated_at: string;
+ /**
+ * Opened At
+ * @description The opened timestamp of the workflow.
+ */
+ opened_at?: string | null;
+ /**
+ * User Id
+ * @description The id of the user who owns this workflow.
+ */
+ user_id: string;
+ /**
+ * Is Public
+ * @description Whether this workflow is shared with all users.
+ */
+ is_public: boolean;
+ /** @description The workflow. */
+ workflow: components["schemas"]["Workflow"];
+ /**
+ * Thumbnail Url
+ * @description The URL of the workflow thumbnail.
+ */
+ thumbnail_url?: string | null;
+ /** @description Whether this workflow is currently callable by call_saved_workflow. */
+ call_saved_workflow_compatibility?: components["schemas"]["WorkflowCallCompatibility"] | null;
+ };
+ /**
+ * Get Workflow Return Value
+ * @description Extracts one named value from a callable workflow return.
+ */
+ WorkflowReturnGetInvocation: {
+ /**
+ * Id
+ * @description The id of this instance of an invocation. Must be unique among all instances of invocations.
+ */
+ id: string;
+ /**
+ * Is Intermediate
+ * @description Whether or not this is an intermediate invocation.
+ * @default false
+ */
+ is_intermediate?: boolean;
+ /**
+ * Use Cache
+ * @description Whether or not to use the cache
+ * @default false
+ */
+ use_cache?: boolean;
+ /**
+ * Values
+ * @description The named workflow return values.
+ * @default {}
+ */
+ values?: {
+ [key: string]: unknown;
+ };
+ /**
+ * Key
+ * @description The return key to extract.
+ * @default
+ */
+ key?: string;
+ /**
+ * type
+ * @default workflow_return_get
+ * @constant
+ */
+ type: "workflow_return_get";
+ };
+ /**
+ * WorkflowReturnGetOutput
+ * @description A value extracted from named workflow return values.
+ */
+ WorkflowReturnGetOutput: {
+ /**
+ * Value
+ * @description The extracted workflow return value.
+ */
+ value: unknown;
+ /**
+ * type
+ * @default workflow_return_get_output
+ * @constant
+ */
+ type: "workflow_return_get_output";
+ };
+ /**
+ * Workflow Return
+ * @description Defines the explicit named result returned by a callable workflow.
+ */
+ WorkflowReturnInvocation: {
+ /**
+ * Id
+ * @description The id of this instance of an invocation. Must be unique among all instances of invocations.
+ */
+ id: string;
+ /**
+ * Is Intermediate
+ * @description Whether or not this is an intermediate invocation.
+ * @default false
+ */
+ is_intermediate?: boolean;
+ /**
+ * Use Cache
+ * @description Whether or not to use the cache
+ * @default false
+ */
+ use_cache?: boolean;
+ /**
+ * Values
+ * @description The named values returned to a calling workflow.
+ * @default []
+ */
+ values?: components["schemas"]["WorkflowReturnValueField"] | components["schemas"]["WorkflowReturnValueField"][];
+ /**
+ * type
+ * @default workflow_return
+ * @constant
+ */
+ type: "workflow_return";
+ };
+ /**
+ * WorkflowReturnOutput
+ * @description The explicit named values returned from a callable workflow.
+ */
+ WorkflowReturnOutput: {
+ /**
+ * Values
+ * @description The workflow return values, keyed by return name.
+ * @default {}
+ */
+ values: {
+ [key: string]: unknown;
+ };
+ /**
+ * type
+ * @default workflow_return_output
+ * @constant
+ */
+ type: "workflow_return_output";
+ };
+ /**
+ * WorkflowReturnValueField
+ * @description One named workflow return value.
+ */
+ WorkflowReturnValueField: {
+ /**
+ * Key
+ * @description The workflow return key.
+ */
+ key: string;
+ /**
+ * Value
+ * @description The workflow return value.
+ * @default null
+ */
+ value?: unknown;
+ };
+ /**
+ * Workflow Return Value
+ * @description Creates one named value for a callable workflow return.
+ */
+ WorkflowReturnValueInvocation: {
+ /**
+ * Id
+ * @description The id of this instance of an invocation. Must be unique among all instances of invocations.
+ */
+ id: string;
+ /**
+ * Is Intermediate
+ * @description Whether or not this is an intermediate invocation.
+ * @default false
+ */
+ is_intermediate?: boolean;
+ /**
+ * Use Cache
+ * @description Whether or not to use the cache
+ * @default false
+ */
+ use_cache?: boolean;
+ /**
+ * Key
+ * @description The return key.
+ * @default
+ */
+ key?: string;
+ /**
+ * Value
+ * @description The value returned under this key.
+ * @default null
+ */
+ value?: unknown | null;
+ /**
+ * type
+ * @default workflow_return_value
+ * @constant
+ */
+ type: "workflow_return_value";
+ };
+ /**
+ * WorkflowReturnValueOutput
+ * @description A named workflow return value.
+ */
+ WorkflowReturnValueOutput: {
+ /**
+ * Return Value
+ * @description The named workflow return value.
+ */
+ value: components["schemas"]["WorkflowReturnValueField"];
+ /**
+ * type
+ * @default workflow_return_value_output
+ * @constant
+ */
+ type: "workflow_return_value_output";
+ };
+ /**
+ * WorkflowUpdatedEvent
+ * @description Event model for workflow_updated
+ */
+ WorkflowUpdatedEvent: {
+ /**
+ * Timestamp
+ * @description The timestamp of the event
+ */
+ timestamp: number;
+ /**
+ * Workflow Id
+ * @description The ID of the workflow
+ */
+ workflow_id: string;
+ /**
+ * User Id
+ * @description The owner of the workflow
+ */
+ user_id: string;
+ /**
+ * Old Is Public
+ * @description Whether the workflow was shared before the update
+ */
+ old_is_public: boolean;
+ /**
+ * New Is Public
+ * @description Whether the workflow is shared after the update
+ */
+ new_is_public: boolean;
+ };
+ /** WorkflowWithoutID */
+ WorkflowWithoutID: {
+ /**
+ * Name
+ * @description The name of the workflow.
+ */
+ name: string;
+ /**
+ * Author
+ * @description The author of the workflow.
+ */
+ author: string;
+ /**
+ * Description
+ * @description The description of the workflow.
+ */
+ description: string;
+ /**
+ * Version
+ * @description The version of the workflow.
+ */
+ version: string;
+ /**
+ * Contact
+ * @description The contact of the workflow.
+ */
+ contact: string;
+ /**
+ * Tags
+ * @description The tags of the workflow.
+ */
+ tags: string;
+ /**
+ * Notes
+ * @description The notes of the workflow.
+ */
+ notes: string;
+ /**
+ * Exposedfields
+ * @description The exposed fields of the workflow.
+ */
+ exposedFields: components["schemas"]["ExposedField"][];
+ /** @description The meta of the workflow. */
+ meta: components["schemas"]["WorkflowMeta"];
+ /**
+ * Nodes
+ * @description The nodes of the workflow.
+ */
+ nodes: {
+ [key: string]: components["schemas"]["JsonValue"];
+ }[];
+ /**
+ * Edges
+ * @description The edges of the workflow.
+ */
+ edges: {
+ [key: string]: components["schemas"]["JsonValue"];
+ }[];
+ /**
+ * Form
+ * @description The form of the workflow.
+ */
+ form?: {
+ [key: string]: components["schemas"]["JsonValue"];
+ } | null;
+ };
+ /**
+ * ZImageConditioningField
+ * @description A Z-Image conditioning tensor primitive value
+ */
+ ZImageConditioningField: {
+ /**
+ * Conditioning Name
+ * @description The name of conditioning tensor
+ */
+ conditioning_name: string;
+ /**
+ * @description The mask associated with this conditioning tensor for regional prompting. Excluded regions should be set to False, included regions should be set to True.
+ * @default null
+ */
+ mask?: components["schemas"]["TensorField"] | null;
+ };
+ /**
+ * ZImageConditioningOutput
+ * @description Base class for nodes that output a Z-Image text conditioning tensor.
+ */
+ ZImageConditioningOutput: {
+ /** @description Conditioning tensor */
+ conditioning: components["schemas"]["ZImageConditioningField"];
+ /**
+ * type
+ * @default z_image_conditioning_output
+ * @constant
+ */
+ type: "z_image_conditioning_output";
+ };
+ /**
+ * ZImageControlField
+ * @description A Z-Image control conditioning field for spatial control (Canny, HED, Depth, Pose, MLSD).
+ */
+ ZImageControlField: {
+ /**
+ * Image Name
+ * @description The name of the preprocessed control image
+ */
+ image_name: string;
+ /** @description The Z-Image ControlNet adapter model */
+ control_model: components["schemas"]["ModelIdentifierField"];
+ /**
+ * Control Context Scale
+ * @description The strength of the control signal. Recommended range: 0.65-0.80.
+ * @default 0.75
+ */
+ control_context_scale?: number;
+ /**
+ * Begin Step Percent
+ * @description When the control is first applied (% of total steps)
+ * @default 0
+ */
+ begin_step_percent?: number;
+ /**
+ * End Step Percent
+ * @description When the control is last applied (% of total steps)
+ * @default 1
+ */
+ end_step_percent?: number;
+ };
+ /**
+ * Z-Image ControlNet
+ * @description Configure Z-Image ControlNet for spatial conditioning.
+ *
+ * Takes a preprocessed control image (e.g., Canny edges, depth map, pose)
+ * and a Z-Image ControlNet adapter model to enable spatial control.
+ *
+ * Supports 5 control modes: Canny, HED, Depth, Pose, MLSD.
+ * Recommended control_context_scale: 0.65-0.80.
+ */
+ ZImageControlInvocation: {
+ /**
+ * Id
+ * @description The id of this instance of an invocation. Must be unique among all instances of invocations.
+ */
+ id: string;
+ /**
+ * Is Intermediate
+ * @description Whether or not this is an intermediate invocation.
+ * @default false
+ */
+ is_intermediate?: boolean;
+ /**
+ * Use Cache
+ * @description Whether or not to use the cache
+ * @default true
+ */
+ use_cache?: boolean;
+ /**
+ * @description The preprocessed control image (Canny, HED, Depth, Pose, or MLSD)
+ * @default null
+ */
+ image?: components["schemas"]["ImageField"] | null;
+ /**
+ * Control Model
+ * @description ControlNet model to load
+ * @default null
+ */
+ control_model?: components["schemas"]["ModelIdentifierField"] | null;
+ /**
+ * Control Scale
+ * @description Strength of the control signal. Recommended range: 0.65-0.80.
+ * @default 0.75
+ */
+ control_context_scale?: number;
+ /**
+ * Begin Step Percent
+ * @description When the control is first applied (% of total steps)
+ * @default 0
+ */
+ begin_step_percent?: number;
+ /**
+ * End Step Percent
+ * @description When the control is last applied (% of total steps)
+ * @default 1
+ */
+ end_step_percent?: number;
+ /**
+ * type
+ * @default z_image_control
+ * @constant
+ */
+ type: "z_image_control";
+ };
+ /**
+ * ZImageControlOutput
+ * @description Z-Image Control output containing control configuration.
+ */
+ ZImageControlOutput: {
+ /** @description Z-Image control conditioning */
+ control: components["schemas"]["ZImageControlField"];
+ /**
+ * type
+ * @default z_image_control_output
+ * @constant
+ */
+ type: "z_image_control_output";
+ };
+ /**
+ * Denoise - Z-Image
+ * @description Run the denoising process with a Z-Image model.
+ *
+ * Supports regional prompting by connecting multiple conditioning inputs with masks.
+ */
+ ZImageDenoiseInvocation: {
+ /**
+ * Id
+ * @description The id of this instance of an invocation. Must be unique among all instances of invocations.
+ */
+ id: string;
+ /**
+ * Is Intermediate
+ * @description Whether or not this is an intermediate invocation.
+ * @default false
+ */
+ is_intermediate?: boolean;
+ /**
+ * Use Cache
+ * @description Whether or not to use the cache
+ * @default true
+ */
+ use_cache?: boolean;
+ /**
+ * @description Latents tensor
+ * @default null
+ */
+ latents?: components["schemas"]["LatentsField"] | null;
+ /**
+ * @description Noise tensor
+ * @default null
+ */
+ noise?: components["schemas"]["LatentsField"] | null;
+ /**
+ * @description A mask of the region to apply the denoising process to. Values of 0.0 represent the regions to be fully denoised, and 1.0 represent the regions to be preserved.
+ * @default null
+ */
+ denoise_mask?: components["schemas"]["DenoiseMaskField"] | null;
+ /**
+ * Denoising Start
+ * @description When to start denoising, expressed a percentage of total steps
+ * @default 0
+ */
+ denoising_start?: number;
+ /**
+ * Denoising End
+ * @description When to stop denoising, expressed a percentage of total steps
+ * @default 1
+ */
+ denoising_end?: number;
+ /**
+ * Add Noise
+ * @description Add noise based on denoising start.
+ * @default true
+ */
+ add_noise?: boolean;
+ /**
+ * Transformer
+ * @description Z-Image model (Transformer) to load
+ * @default null
+ */
+ transformer?: components["schemas"]["TransformerField"] | null;
+ /**
+ * Positive Conditioning
+ * @description Positive conditioning tensor
+ * @default null
+ */
+ positive_conditioning?: components["schemas"]["ZImageConditioningField"] | components["schemas"]["ZImageConditioningField"][] | null;
+ /**
+ * Negative Conditioning
+ * @description Negative conditioning tensor
+ * @default null
+ */
+ negative_conditioning?: components["schemas"]["ZImageConditioningField"] | components["schemas"]["ZImageConditioningField"][] | null;
+ /**
+ * Guidance Scale
+ * @description Guidance scale for classifier-free guidance. 1.0 = no CFG (recommended for Z-Image-Turbo). Values > 1.0 amplify guidance.
+ * @default 1
+ */
+ guidance_scale?: number;
+ /**
+ * Width
+ * @description Width of the generated image.
+ * @default 1024
+ */
+ width?: number;
+ /**
+ * Height
+ * @description Height of the generated image.
+ * @default 1024
+ */
+ height?: number;
+ /**
+ * Steps
+ * @description Number of denoising steps. 8 recommended for Z-Image-Turbo.
+ * @default 8
+ */
+ steps?: number;
+ /**
+ * Seed
+ * @description Randomness seed for reproducibility.
+ * @default 0
+ */
+ seed?: number;
+ /**
+ * @description Z-Image control conditioning for spatial control (Canny, HED, Depth, Pose, MLSD).
+ * @default null
+ */
+ control?: components["schemas"]["ZImageControlField"] | null;
+ /**
+ * @description VAE Required for control conditioning.
+ * @default null
+ */
+ vae?: components["schemas"]["VAEField"] | null;
+ /**
+ * Shift
+ * @description Override the timestep shift (mu) for the sigma schedule. Leave blank to auto-calculate based on image dimensions (recommended). Lower values (~0.5) produce less noise shifting, higher values (~1.15) produce more.
+ * @default null
+ */
+ shift?: number | null;
+ /**
+ * Scheduler
+ * @description Scheduler (sampler) for the denoising process. Euler is the default and recommended. Heun is 2nd-order (better quality, 2x slower). LCM works with Turbo only (not Base).
+ * @default euler
+ * @enum {string}
+ */
+ scheduler?: "euler" | "heun" | "lcm";
+ /**
+ * type
+ * @default z_image_denoise
+ * @constant
+ */
+ type: "z_image_denoise";
+ };
+ /**
+ * Denoise - Z-Image + Metadata
+ * @description Run denoising process with a Z-Image transformer model + metadata.
+ */
+ ZImageDenoiseMetaInvocation: {
+ /**
+ * @description Optional metadata to be saved with the image
+ * @default null
+ */
+ metadata?: components["schemas"]["MetadataField"] | null;
+ /**
+ * Id
+ * @description The id of this instance of an invocation. Must be unique among all instances of invocations.
+ */
+ id: string;
+ /**
+ * Is Intermediate
+ * @description Whether or not this is an intermediate invocation.
+ * @default false
+ */
+ is_intermediate?: boolean;
+ /**
+ * Use Cache
+ * @description Whether or not to use the cache
+ * @default true
+ */
+ use_cache?: boolean;
+ /**
+ * @description Latents tensor
+ * @default null
+ */
+ latents?: components["schemas"]["LatentsField"] | null;
+ /**
+ * @description Noise tensor
+ * @default null
+ */
+ noise?: components["schemas"]["LatentsField"] | null;
+ /**
+ * @description A mask of the region to apply the denoising process to. Values of 0.0 represent the regions to be fully denoised, and 1.0 represent the regions to be preserved.
+ * @default null
+ */
+ denoise_mask?: components["schemas"]["DenoiseMaskField"] | null;
+ /**
+ * Denoising Start
+ * @description When to start denoising, expressed a percentage of total steps
+ * @default 0
+ */
+ denoising_start?: number;
+ /**
+ * Denoising End
+ * @description When to stop denoising, expressed a percentage of total steps
+ * @default 1
+ */
+ denoising_end?: number;
+ /**
+ * Add Noise
+ * @description Add noise based on denoising start.
+ * @default true
+ */
+ add_noise?: boolean;
+ /**
+ * Transformer
+ * @description Z-Image model (Transformer) to load
+ * @default null
+ */
+ transformer?: components["schemas"]["TransformerField"] | null;
+ /**
+ * Positive Conditioning
+ * @description Positive conditioning tensor
+ * @default null
+ */
+ positive_conditioning?: components["schemas"]["ZImageConditioningField"] | components["schemas"]["ZImageConditioningField"][] | null;
+ /**
+ * Negative Conditioning
+ * @description Negative conditioning tensor
+ * @default null
+ */
+ negative_conditioning?: components["schemas"]["ZImageConditioningField"] | components["schemas"]["ZImageConditioningField"][] | null;
+ /**
+ * Guidance Scale
+ * @description Guidance scale for classifier-free guidance. 1.0 = no CFG (recommended for Z-Image-Turbo). Values > 1.0 amplify guidance.
+ * @default 1
+ */
+ guidance_scale?: number;
+ /**
+ * Width
+ * @description Width of the generated image.
+ * @default 1024
+ */
+ width?: number;
+ /**
+ * Height
+ * @description Height of the generated image.
+ * @default 1024
+ */
+ height?: number;
+ /**
+ * Steps
+ * @description Number of denoising steps. 8 recommended for Z-Image-Turbo.
+ * @default 8
+ */
+ steps?: number;
+ /**
+ * Seed
+ * @description Randomness seed for reproducibility.
+ * @default 0
+ */
+ seed?: number;
+ /**
+ * @description Z-Image control conditioning for spatial control (Canny, HED, Depth, Pose, MLSD).
+ * @default null
+ */
+ control?: components["schemas"]["ZImageControlField"] | null;
+ /**
+ * @description VAE Required for control conditioning.
+ * @default null
+ */
+ vae?: components["schemas"]["VAEField"] | null;
+ /**
+ * Shift
+ * @description Override the timestep shift (mu) for the sigma schedule. Leave blank to auto-calculate based on image dimensions (recommended). Lower values (~0.5) produce less noise shifting, higher values (~1.15) produce more.
+ * @default null
+ */
+ shift?: number | null;
+ /**
+ * Scheduler
+ * @description Scheduler (sampler) for the denoising process. Euler is the default and recommended. Heun is 2nd-order (better quality, 2x slower). LCM works with Turbo only (not Base).
+ * @default euler
+ * @enum {string}
+ */
+ scheduler?: "euler" | "heun" | "lcm";
+ /**
+ * type
+ * @default z_image_denoise_meta
+ * @constant
+ */
+ type: "z_image_denoise_meta";
+ };
+ /**
+ * Image to Latents - Z-Image
+ * @description Generates latents from an image using Z-Image VAE (supports both Diffusers and FLUX VAE).
+ */
+ ZImageImageToLatentsInvocation: {
+ /**
+ * @description The board to save the image to
+ * @default null
+ */
+ board?: components["schemas"]["BoardField"] | null;
+ /**
+ * @description Optional metadata to be saved with the image
+ * @default null
+ */
+ metadata?: components["schemas"]["MetadataField"] | null;
+ /**
+ * Id
+ * @description The id of this instance of an invocation. Must be unique among all instances of invocations.
+ */
+ id: string;
+ /**
+ * Is Intermediate
+ * @description Whether or not this is an intermediate invocation.
+ * @default false
+ */
+ is_intermediate?: boolean;
+ /**
+ * Use Cache
+ * @description Whether or not to use the cache
+ * @default true
+ */
+ use_cache?: boolean;
+ /**
+ * @description The image to encode.
+ * @default null
+ */
+ image?: components["schemas"]["ImageField"] | null;
+ /**
+ * @description VAE
+ * @default null
+ */
+ vae?: components["schemas"]["VAEField"] | null;
+ /**
+ * type
+ * @default z_image_i2l
+ * @constant
+ */
+ type: "z_image_i2l";
+ };
+ /**
+ * Latents to Image - Z-Image
+ * @description Generates an image from latents using Z-Image VAE (supports both Diffusers and FLUX VAE).
+ */
+ ZImageLatentsToImageInvocation: {
+ /**
+ * @description The board to save the image to
+ * @default null
+ */
+ board?: components["schemas"]["BoardField"] | null;
+ /**
+ * @description Optional metadata to be saved with the image
+ * @default null
+ */
+ metadata?: components["schemas"]["MetadataField"] | null;
+ /**
+ * Id
+ * @description The id of this instance of an invocation. Must be unique among all instances of invocations.
+ */
+ id: string;
+ /**
+ * Is Intermediate
+ * @description Whether or not this is an intermediate invocation.
+ * @default false
+ */
+ is_intermediate?: boolean;
+ /**
+ * Use Cache
+ * @description Whether or not to use the cache
+ * @default true
+ */
+ use_cache?: boolean;
+ /**
+ * @description Latents tensor
+ * @default null
+ */
+ latents?: components["schemas"]["LatentsField"] | null;
+ /**
+ * @description VAE
+ * @default null
+ */
+ vae?: components["schemas"]["VAEField"] | null;
+ /**
+ * type
+ * @default z_image_l2i
+ * @constant
+ */
+ type: "z_image_l2i";
+ };
+ /**
+ * Apply LoRA Collection - Z-Image
+ * @description Applies a collection of LoRAs to a Z-Image transformer.
+ */
+ ZImageLoRACollectionLoader: {
+ /**
+ * Id
+ * @description The id of this instance of an invocation. Must be unique among all instances of invocations.
+ */
+ id: string;
+ /**
+ * Is Intermediate
+ * @description Whether or not this is an intermediate invocation.
+ * @default false
+ */
+ is_intermediate?: boolean;
+ /**
+ * Use Cache
+ * @description Whether or not to use the cache
+ * @default true
+ */
+ use_cache?: boolean;
+ /**
+ * LoRAs
+ * @description LoRA models and weights. May be a single LoRA or collection.
+ * @default null
+ */
+ loras?: components["schemas"]["LoRAField"] | components["schemas"]["LoRAField"][] | null;
+ /**
+ * Transformer
+ * @description Transformer
+ * @default null
+ */
+ transformer?: components["schemas"]["TransformerField"] | null;
+ /**
+ * Qwen3 Encoder
+ * @description Qwen3 tokenizer and text encoder
+ * @default null
+ */
+ qwen3_encoder?: components["schemas"]["Qwen3EncoderField"] | null;
+ /**
+ * type
+ * @default z_image_lora_collection_loader
+ * @constant
+ */
+ type: "z_image_lora_collection_loader";
+ };
+ /**
+ * Apply LoRA - Z-Image
+ * @description Apply a LoRA model to a Z-Image transformer and/or Qwen3 text encoder.
+ */
+ ZImageLoRALoaderInvocation: {
+ /**
+ * Id
+ * @description The id of this instance of an invocation. Must be unique among all instances of invocations.
+ */
+ id: string;
+ /**
+ * Is Intermediate
+ * @description Whether or not this is an intermediate invocation.
+ * @default false
+ */
+ is_intermediate?: boolean;
+ /**
+ * Use Cache
+ * @description Whether or not to use the cache
+ * @default true
+ */
+ use_cache?: boolean;
+ /**
+ * LoRA
+ * @description LoRA model to load
+ * @default null
+ */
+ lora?: components["schemas"]["ModelIdentifierField"] | null;
+ /**
+ * Weight
+ * @description The weight at which the LoRA is applied to each model
+ * @default 0.75
+ */
+ weight?: number;
+ /**
+ * Z-Image Transformer
+ * @description Transformer
+ * @default null
+ */
+ transformer?: components["schemas"]["TransformerField"] | null;
+ /**
+ * Qwen3 Encoder
+ * @description Qwen3 tokenizer and text encoder
+ * @default null
+ */
+ qwen3_encoder?: components["schemas"]["Qwen3EncoderField"] | null;
+ /**
+ * type
+ * @default z_image_lora_loader
+ * @constant
+ */
+ type: "z_image_lora_loader";
+ };
+ /**
+ * ZImageLoRALoaderOutput
+ * @description Z-Image LoRA Loader Output
+ */
+ ZImageLoRALoaderOutput: {
+ /**
+ * Z-Image Transformer
+ * @description Transformer
+ * @default null
+ */
+ transformer: components["schemas"]["TransformerField"] | null;
+ /**
+ * Qwen3 Encoder
+ * @description Qwen3 tokenizer and text encoder
+ * @default null
+ */
+ qwen3_encoder: components["schemas"]["Qwen3EncoderField"] | null;
+ /**
+ * type
+ * @default z_image_lora_loader_output
+ * @constant
+ */
+ type: "z_image_lora_loader_output";
+ };
+ /**
+ * Main Model - Z-Image
+ * @description Loads a Z-Image model, outputting its submodels.
+ *
+ * Similar to FLUX, you can mix and match components:
+ * - Transformer: From Z-Image main model (GGUF quantized or Diffusers format)
+ * - VAE: Separate FLUX VAE (shared with FLUX models) or from a Diffusers Z-Image model
+ * - Qwen3 Encoder: Separate Qwen3Encoder model or from a Diffusers Z-Image model
+ */
+ ZImageModelLoaderInvocation: {
+ /**
+ * Id
+ * @description The id of this instance of an invocation. Must be unique among all instances of invocations.
+ */
+ id: string;
+ /**
+ * Is Intermediate
+ * @description Whether or not this is an intermediate invocation.
+ * @default false
+ */
+ is_intermediate?: boolean;
+ /**
+ * Use Cache
+ * @description Whether or not to use the cache
+ * @default true
+ */
+ use_cache?: boolean;
+ /**
+ * Transformer
+ * @description Z-Image model (Transformer) to load
+ */
+ model: components["schemas"]["ModelIdentifierField"];
+ /**
+ * VAE
+ * @description Standalone VAE model. Z-Image uses the same VAE as FLUX (16-channel). If not provided, VAE will be loaded from the Qwen3 Source model.
+ * @default null
+ */
+ vae_model?: components["schemas"]["ModelIdentifierField"] | null;
+ /**
+ * Qwen3 Encoder
+ * @description Standalone Qwen3 Encoder model. If not provided, encoder will be loaded from the Qwen3 Source model.
+ * @default null
+ */
+ qwen3_encoder_model?: components["schemas"]["ModelIdentifierField"] | null;
+ /**
+ * Qwen3 Source (Diffusers)
+ * @description Diffusers Z-Image model to extract VAE and/or Qwen3 encoder from. Use this if you don't have separate VAE/Qwen3 models. Ignored if both VAE and Qwen3 Encoder are provided separately.
+ * @default null
+ */
+ qwen3_source_model?: components["schemas"]["ModelIdentifierField"] | null;
+ /**
+ * type
+ * @default z_image_model_loader
+ * @constant
+ */
+ type: "z_image_model_loader";
+ };
+ /**
+ * ZImageModelLoaderOutput
+ * @description Z-Image base model loader output.
+ */
+ ZImageModelLoaderOutput: {
+ /**
+ * Transformer
+ * @description Transformer
+ */
+ transformer: components["schemas"]["TransformerField"];
+ /**
+ * Qwen3 Encoder
+ * @description Qwen3 tokenizer and text encoder
+ */
+ qwen3_encoder: components["schemas"]["Qwen3EncoderField"];
+ /**
+ * VAE
+ * @description VAE
+ */
+ vae: components["schemas"]["VAEField"];
+ /**
+ * type
+ * @default z_image_model_loader_output
+ * @constant
+ */
+ type: "z_image_model_loader_output";
+ };
+ /**
+ * Seed Variance Enhancer - Z-Image
+ * @description Adds seed-based noise to Z-Image conditioning to increase variance between seeds.
+ *
+ * Z-Image-Turbo can produce relatively similar images with different seeds,
+ * making it harder to explore variations of a prompt. This node implements
+ * reproducible, seed-based noise injection into text embeddings to increase
+ * visual variation while maintaining reproducibility.
+ *
+ * The noise strength is auto-calibrated relative to the embedding's standard
+ * deviation, ensuring consistent results across different prompts.
+ */
+ ZImageSeedVarianceEnhancerInvocation: {
+ /**
+ * Id
+ * @description The id of this instance of an invocation. Must be unique among all instances of invocations.
+ */
+ id: string;
+ /**
+ * Is Intermediate
+ * @description Whether or not this is an intermediate invocation.
+ * @default false
+ */
+ is_intermediate?: boolean;
+ /**
+ * Use Cache
+ * @description Whether or not to use the cache
+ * @default true
+ */
+ use_cache?: boolean;
+ /**
+ * Conditioning
+ * @description Conditioning tensor
+ * @default null
+ */
+ conditioning?: components["schemas"]["ZImageConditioningField"] | null;
+ /**
+ * Seed
+ * @description Seed for reproducible noise generation. Different seeds produce different noise patterns.
+ * @default 0
+ */
+ seed?: number;
+ /**
+ * Strength
+ * @description Noise strength as multiplier of embedding std. 0=off, 0.1=subtle, 0.5=strong.
+ * @default 0.1
+ */
+ strength?: number;
/**
* Randomize Percent
* @description Percentage of embedding values to add noise to (1-100). Lower values create more selective noise patterns.
@@ -34106,77 +36948,431 @@ export type components = {
*/
type: "z_image_seed_variance_enhancer";
};
- /**
- * Prompt - Z-Image
- * @description Encodes and preps a prompt for a Z-Image image.
- *
- * Supports regional prompting by connecting a mask input.
- */
- ZImageTextEncoderInvocation: {
- /**
- * Id
- * @description The id of this instance of an invocation. Must be unique among all instances of invocations.
- */
- id: string;
- /**
- * Is Intermediate
- * @description Whether or not this is an intermediate invocation.
- * @default false
- */
- is_intermediate?: boolean;
- /**
- * Use Cache
- * @description Whether or not to use the cache
- * @default true
- */
- use_cache?: boolean;
- /**
- * Prompt
- * @description Text prompt to encode.
- * @default null
- */
- prompt?: string | null;
- /**
- * Qwen3 Encoder
- * @description Qwen3 tokenizer and text encoder
- * @default null
- */
- qwen3_encoder?: components["schemas"]["Qwen3EncoderField"] | null;
- /**
- * @description A mask defining the region that this conditioning prompt applies to.
- * @default null
- */
- mask?: components["schemas"]["TensorField"] | null;
- /**
- * type
- * @default z_image_text_encoder
- * @constant
- */
- type: "z_image_text_encoder";
+ /**
+ * Prompt - Z-Image
+ * @description Encodes and preps a prompt for a Z-Image image.
+ *
+ * Supports regional prompting by connecting a mask input.
+ */
+ ZImageTextEncoderInvocation: {
+ /**
+ * Id
+ * @description The id of this instance of an invocation. Must be unique among all instances of invocations.
+ */
+ id: string;
+ /**
+ * Is Intermediate
+ * @description Whether or not this is an intermediate invocation.
+ * @default false
+ */
+ is_intermediate?: boolean;
+ /**
+ * Use Cache
+ * @description Whether or not to use the cache
+ * @default true
+ */
+ use_cache?: boolean;
+ /**
+ * Prompt
+ * @description Text prompt to encode.
+ * @default null
+ */
+ prompt?: string | null;
+ /**
+ * Qwen3 Encoder
+ * @description Qwen3 tokenizer and text encoder
+ * @default null
+ */
+ qwen3_encoder?: components["schemas"]["Qwen3EncoderField"] | null;
+ /**
+ * @description A mask defining the region that this conditioning prompt applies to.
+ * @default null
+ */
+ mask?: components["schemas"]["TensorField"] | null;
+ /**
+ * type
+ * @default z_image_text_encoder
+ * @constant
+ */
+ type: "z_image_text_encoder";
+ };
+ /**
+ * ZImageVariantType
+ * @description Z-Image model variants.
+ * @enum {string}
+ */
+ ZImageVariantType: "turbo" | "zbase";
+ };
+ responses: never;
+ parameters: never;
+ requestBodies: never;
+ headers: never;
+ pathItems: never;
+};
+export type $defs = Record;
+export interface operations {
+ get_setup_status_api_v1_auth_status_get: {
+ parameters: {
+ query?: never;
+ header?: never;
+ path?: never;
+ cookie?: never;
+ };
+ requestBody?: never;
+ responses: {
+ /** @description Successful Response */
+ 200: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/json": components["schemas"]["SetupStatusResponse"];
+ };
+ };
+ };
+ };
+ login_api_v1_auth_login_post: {
+ parameters: {
+ query?: never;
+ header?: never;
+ path?: never;
+ cookie?: never;
+ };
+ requestBody: {
+ content: {
+ "application/json": components["schemas"]["LoginRequest"];
+ };
+ };
+ responses: {
+ /** @description Successful Response */
+ 200: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/json": components["schemas"]["LoginResponse"];
+ };
+ };
+ /** @description Validation Error */
+ 422: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/json": components["schemas"]["HTTPValidationError"];
+ };
+ };
+ };
+ };
+ logout_api_v1_auth_logout_post: {
+ parameters: {
+ query?: never;
+ header?: never;
+ path?: never;
+ cookie?: never;
+ };
+ requestBody?: never;
+ responses: {
+ /** @description Successful Response */
+ 200: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/json": components["schemas"]["LogoutResponse"];
+ };
+ };
+ };
+ };
+ refresh_media_cookie_api_v1_auth_media_cookie_post: {
+ parameters: {
+ query?: never;
+ header?: never;
+ path?: never;
+ cookie?: never;
+ };
+ requestBody?: never;
+ responses: {
+ /** @description Successful Response */
+ 200: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/json": components["schemas"]["MediaCookieResponse"];
+ };
+ };
+ };
+ };
+ get_current_user_info_api_v1_auth_me_get: {
+ parameters: {
+ query?: never;
+ header?: never;
+ path?: never;
+ cookie?: never;
+ };
+ requestBody?: never;
+ responses: {
+ /** @description Successful Response */
+ 200: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/json": components["schemas"]["UserDTO"];
+ };
+ };
+ };
+ };
+ update_current_user_api_v1_auth_me_patch: {
+ parameters: {
+ query?: never;
+ header?: never;
+ path?: never;
+ cookie?: never;
+ };
+ requestBody: {
+ content: {
+ "application/json": components["schemas"]["UserProfileUpdateRequest"];
+ };
+ };
+ responses: {
+ /** @description Successful Response */
+ 200: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/json": components["schemas"]["UserDTO"];
+ };
+ };
+ /** @description Validation Error */
+ 422: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/json": components["schemas"]["HTTPValidationError"];
+ };
+ };
+ };
+ };
+ setup_admin_api_v1_auth_setup_post: {
+ parameters: {
+ query?: never;
+ header?: never;
+ path?: never;
+ cookie?: never;
+ };
+ requestBody: {
+ content: {
+ "application/json": components["schemas"]["SetupRequest"];
+ };
+ };
+ responses: {
+ /** @description Successful Response */
+ 200: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/json": components["schemas"]["SetupResponse"];
+ };
+ };
+ /** @description Validation Error */
+ 422: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/json": components["schemas"]["HTTPValidationError"];
+ };
+ };
+ };
+ };
+ generate_password_api_v1_auth_generate_password_get: {
+ parameters: {
+ query?: never;
+ header?: never;
+ path?: never;
+ cookie?: never;
+ };
+ requestBody?: never;
+ responses: {
+ /** @description Successful Response */
+ 200: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/json": components["schemas"]["GeneratePasswordResponse"];
+ };
+ };
+ };
+ };
+ list_users_api_v1_auth_users_get: {
+ parameters: {
+ query?: never;
+ header?: never;
+ path?: never;
+ cookie?: never;
+ };
+ requestBody?: never;
+ responses: {
+ /** @description Successful Response */
+ 200: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/json": components["schemas"]["UserDTO"][];
+ };
+ };
+ };
+ };
+ create_user_api_v1_auth_users_post: {
+ parameters: {
+ query?: never;
+ header?: never;
+ path?: never;
+ cookie?: never;
+ };
+ requestBody: {
+ content: {
+ "application/json": components["schemas"]["AdminUserCreateRequest"];
+ };
+ };
+ responses: {
+ /** @description Successful Response */
+ 201: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/json": components["schemas"]["UserDTO"];
+ };
+ };
+ /** @description Validation Error */
+ 422: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/json": components["schemas"]["HTTPValidationError"];
+ };
+ };
+ };
+ };
+ get_user_api_v1_auth_users__user_id__get: {
+ parameters: {
+ query?: never;
+ header?: never;
+ path: {
+ /** @description User ID */
+ user_id: string;
+ };
+ cookie?: never;
+ };
+ requestBody?: never;
+ responses: {
+ /** @description Successful Response */
+ 200: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/json": components["schemas"]["UserDTO"];
+ };
+ };
+ /** @description Validation Error */
+ 422: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/json": components["schemas"]["HTTPValidationError"];
+ };
+ };
+ };
+ };
+ delete_user_api_v1_auth_users__user_id__delete: {
+ parameters: {
+ query?: never;
+ header?: never;
+ path: {
+ /** @description User ID */
+ user_id: string;
+ };
+ cookie?: never;
+ };
+ requestBody?: never;
+ responses: {
+ /** @description Successful Response */
+ 204: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content?: never;
+ };
+ /** @description Validation Error */
+ 422: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/json": components["schemas"]["HTTPValidationError"];
+ };
+ };
};
- /**
- * ZImageVariantType
- * @description Z-Image model variants.
- * @enum {string}
- */
- ZImageVariantType: "turbo" | "zbase";
};
- responses: never;
- parameters: never;
- requestBodies: never;
- headers: never;
- pathItems: never;
-};
-export type $defs = Record;
-export interface operations {
- get_setup_status_api_v1_auth_status_get: {
+ update_user_api_v1_auth_users__user_id__patch: {
+ parameters: {
+ query?: never;
+ header?: never;
+ path: {
+ /** @description User ID */
+ user_id: string;
+ };
+ cookie?: never;
+ };
+ requestBody: {
+ content: {
+ "application/json": components["schemas"]["AdminUserUpdateRequest"];
+ };
+ };
+ responses: {
+ /** @description Successful Response */
+ 200: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/json": components["schemas"]["UserDTO"];
+ };
+ };
+ /** @description Validation Error */
+ 422: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/json": components["schemas"]["HTTPValidationError"];
+ };
+ };
+ };
+ };
+ parse_dynamicprompts: {
parameters: {
query?: never;
header?: never;
path?: never;
cookie?: never;
};
- requestBody?: never;
+ requestBody: {
+ content: {
+ "application/json": components["schemas"]["Body_parse_dynamicprompts"];
+ };
+ };
responses: {
/** @description Successful Response */
200: {
@@ -34184,12 +37380,21 @@ export interface operations {
[name: string]: unknown;
};
content: {
- "application/json": components["schemas"]["SetupStatusResponse"];
+ "application/json": components["schemas"]["DynamicPromptsResponse"];
+ };
+ };
+ /** @description Validation Error */
+ 422: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/json": components["schemas"]["HTTPValidationError"];
};
};
};
};
- login_api_v1_auth_login_post: {
+ expand_prompt: {
parameters: {
query?: never;
header?: never;
@@ -34198,7 +37403,7 @@ export interface operations {
};
requestBody: {
content: {
- "application/json": components["schemas"]["LoginRequest"];
+ "application/json": components["schemas"]["ExpandPromptRequest"];
};
};
responses: {
@@ -34208,7 +37413,7 @@ export interface operations {
[name: string]: unknown;
};
content: {
- "application/json": components["schemas"]["LoginResponse"];
+ "application/json": components["schemas"]["ExpandPromptResponse"];
};
};
/** @description Validation Error */
@@ -34222,13 +37427,59 @@ export interface operations {
};
};
};
- logout_api_v1_auth_logout_post: {
+ image_to_prompt: {
parameters: {
query?: never;
header?: never;
path?: never;
cookie?: never;
};
+ requestBody: {
+ content: {
+ "application/json": components["schemas"]["ImageToPromptRequest"];
+ };
+ };
+ responses: {
+ /** @description Successful Response */
+ 200: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/json": components["schemas"]["ImageToPromptResponse"];
+ };
+ };
+ /** @description Validation Error */
+ 422: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/json": components["schemas"]["HTTPValidationError"];
+ };
+ };
+ };
+ };
+ list_model_records: {
+ parameters: {
+ query?: {
+ /** @description Base models to include */
+ base_models?: components["schemas"]["BaseModelType"][] | null;
+ /** @description The type of model to get */
+ model_type?: components["schemas"]["ModelType"] | null;
+ /** @description Exact match on the name of the model */
+ model_name?: string | null;
+ /** @description Exact match on the format of the model (e.g. 'diffusers') */
+ model_format?: components["schemas"]["ModelFormat"] | null;
+ /** @description The field to order by */
+ order_by?: components["schemas"]["ModelRecordOrderBy"];
+ /** @description The direction to order by */
+ direction?: components["schemas"]["SQLiteDirection"];
+ };
+ header?: never;
+ path?: never;
+ cookie?: never;
+ };
requestBody?: never;
responses: {
/** @description Successful Response */
@@ -34237,12 +37488,21 @@ export interface operations {
[name: string]: unknown;
};
content: {
- "application/json": components["schemas"]["LogoutResponse"];
+ "application/json": components["schemas"]["ModelsList"];
+ };
+ };
+ /** @description Validation Error */
+ 422: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/json": components["schemas"]["HTTPValidationError"];
};
};
};
};
- get_current_user_info_api_v1_auth_me_get: {
+ list_missing_models: {
parameters: {
query?: never;
header?: never;
@@ -34250,6 +37510,33 @@ export interface operations {
cookie?: never;
};
requestBody?: never;
+ responses: {
+ /** @description List of models with missing files */
+ 200: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/json": components["schemas"]["ModelsList"];
+ };
+ };
+ };
+ };
+ get_model_records_by_attrs: {
+ parameters: {
+ query: {
+ /** @description The name of the model */
+ name: string;
+ /** @description The type of the model */
+ type: components["schemas"]["ModelType"];
+ /** @description The base model of the model */
+ base: components["schemas"]["BaseModelType"];
+ };
+ header?: never;
+ path?: never;
+ cookie?: never;
+ };
+ requestBody?: never;
responses: {
/** @description Successful Response */
200: {
@@ -34257,32 +37544,286 @@ export interface operations {
[name: string]: unknown;
};
content: {
- "application/json": components["schemas"]["UserDTO"];
+ "application/json": components["schemas"]["Main_Diffusers_SD1_Config"] | components["schemas"]["Main_Diffusers_SD2_Config"] | components["schemas"]["Main_Diffusers_SDXL_Config"] | components["schemas"]["Main_Diffusers_SDXLRefiner_Config"] | components["schemas"]["Main_Diffusers_SD3_Config"] | components["schemas"]["Main_Diffusers_FLUX_Config"] | components["schemas"]["Main_Diffusers_Flux2_Config"] | components["schemas"]["Main_Diffusers_CogView4_Config"] | components["schemas"]["Main_Diffusers_QwenImage_Config"] | components["schemas"]["Main_Diffusers_Wan_Config"] | components["schemas"]["Main_Diffusers_ZImage_Config"] | components["schemas"]["Main_Checkpoint_SD1_Config"] | components["schemas"]["Main_Checkpoint_SD2_Config"] | components["schemas"]["Main_Checkpoint_SDXL_Config"] | components["schemas"]["Main_Checkpoint_SDXLRefiner_Config"] | components["schemas"]["Main_Checkpoint_Flux2_Config"] | components["schemas"]["Main_Checkpoint_FLUX_Config"] | components["schemas"]["Main_Checkpoint_QwenImage_Config"] | components["schemas"]["Main_Checkpoint_ZImage_Config"] | components["schemas"]["Main_Checkpoint_Anima_Config"] | components["schemas"]["Main_BnBNF4_FLUX_Config"] | components["schemas"]["Main_GGUF_Flux2_Config"] | components["schemas"]["Main_GGUF_FLUX_Config"] | components["schemas"]["Main_GGUF_QwenImage_Config"] | components["schemas"]["Main_GGUF_Wan_Config"] | components["schemas"]["Main_GGUF_ZImage_Config"] | components["schemas"]["VAE_Checkpoint_SD1_Config"] | components["schemas"]["VAE_Checkpoint_SD2_Config"] | components["schemas"]["VAE_Checkpoint_SDXL_Config"] | components["schemas"]["VAE_Checkpoint_FLUX_Config"] | components["schemas"]["VAE_Checkpoint_Flux2_Config"] | components["schemas"]["VAE_Checkpoint_Wan_Config"] | components["schemas"]["VAE_Checkpoint_QwenImage_Config"] | components["schemas"]["VAE_Checkpoint_Anima_Config"] | components["schemas"]["VAE_Diffusers_SD1_Config"] | components["schemas"]["VAE_Diffusers_SDXL_Config"] | components["schemas"]["VAE_Diffusers_Flux2_Config"] | components["schemas"]["VAE_Diffusers_Wan_Config"] | components["schemas"]["ControlNet_Checkpoint_SD1_Config"] | components["schemas"]["ControlNet_Checkpoint_SD2_Config"] | components["schemas"]["ControlNet_Checkpoint_SDXL_Config"] | components["schemas"]["ControlNet_Checkpoint_FLUX_Config"] | components["schemas"]["ControlNet_Checkpoint_ZImage_Config"] | components["schemas"]["ControlNet_Checkpoint_Anima_Config"] | components["schemas"]["ControlNet_Diffusers_SD1_Config"] | components["schemas"]["ControlNet_Diffusers_SD2_Config"] | components["schemas"]["ControlNet_Diffusers_SDXL_Config"] | components["schemas"]["ControlNet_Diffusers_FLUX_Config"] | components["schemas"]["LoRA_LyCORIS_SD1_Config"] | components["schemas"]["LoRA_LyCORIS_SD2_Config"] | components["schemas"]["LoRA_LyCORIS_SDXL_Config"] | components["schemas"]["LoRA_LyCORIS_Flux2_Config"] | components["schemas"]["LoRA_LyCORIS_FLUX_Config"] | components["schemas"]["LoRA_LyCORIS_ZImage_Config"] | components["schemas"]["LoRA_LyCORIS_QwenImage_Config"] | components["schemas"]["LoRA_LyCORIS_Wan_Config"] | components["schemas"]["LoRA_LyCORIS_Anima_Config"] | components["schemas"]["LoRA_OMI_SDXL_Config"] | components["schemas"]["LoRA_OMI_FLUX_Config"] | components["schemas"]["LoRA_Diffusers_SD1_Config"] | components["schemas"]["LoRA_Diffusers_SD2_Config"] | components["schemas"]["LoRA_Diffusers_SDXL_Config"] | components["schemas"]["LoRA_Diffusers_Flux2_Config"] | components["schemas"]["LoRA_Diffusers_FLUX_Config"] | components["schemas"]["LoRA_Diffusers_ZImage_Config"] | components["schemas"]["ControlLoRA_LyCORIS_FLUX_Config"] | components["schemas"]["T5Encoder_T5Encoder_Config"] | components["schemas"]["T5Encoder_BnBLLMint8_Config"] | components["schemas"]["Qwen3Encoder_Qwen3Encoder_Config"] | components["schemas"]["Qwen3Encoder_Checkpoint_Config"] | components["schemas"]["Qwen3Encoder_GGUF_Config"] | components["schemas"]["QwenVLEncoder_Diffusers_Config"] | components["schemas"]["QwenVLEncoder_Checkpoint_Config"] | components["schemas"]["WanT5Encoder_WanT5Encoder_Config"] | components["schemas"]["TI_File_SD1_Config"] | components["schemas"]["TI_File_SD2_Config"] | components["schemas"]["TI_File_SDXL_Config"] | components["schemas"]["TI_Folder_SD1_Config"] | components["schemas"]["TI_Folder_SD2_Config"] | components["schemas"]["TI_Folder_SDXL_Config"] | components["schemas"]["IPAdapter_InvokeAI_SD1_Config"] | components["schemas"]["IPAdapter_InvokeAI_SD2_Config"] | components["schemas"]["IPAdapter_InvokeAI_SDXL_Config"] | components["schemas"]["IPAdapter_Checkpoint_SD1_Config"] | components["schemas"]["IPAdapter_Checkpoint_SD2_Config"] | components["schemas"]["IPAdapter_Checkpoint_SDXL_Config"] | components["schemas"]["IPAdapter_Checkpoint_FLUX_Config"] | components["schemas"]["T2IAdapter_Diffusers_SD1_Config"] | components["schemas"]["T2IAdapter_Diffusers_SDXL_Config"] | components["schemas"]["Spandrel_Checkpoint_Config"] | components["schemas"]["CLIPEmbed_Diffusers_G_Config"] | components["schemas"]["CLIPEmbed_Diffusers_L_Config"] | components["schemas"]["CLIPVision_Diffusers_Config"] | components["schemas"]["SigLIP_Diffusers_Config"] | components["schemas"]["FLUXRedux_Checkpoint_Config"] | components["schemas"]["LlavaOnevision_Diffusers_Config"] | components["schemas"]["TextLLM_Diffusers_Config"] | components["schemas"]["ExternalApiModelConfig"] | components["schemas"]["Unknown_Config"];
+ };
+ };
+ /** @description Validation Error */
+ 422: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/json": components["schemas"]["HTTPValidationError"];
};
};
};
};
- update_current_user_api_v1_auth_me_patch: {
+ get_model_records_by_hash: {
parameters: {
- query?: never;
+ query: {
+ /** @description The hash of the model */
+ hash: string;
+ };
header?: never;
path?: never;
cookie?: never;
};
+ requestBody?: never;
+ responses: {
+ /** @description Successful Response */
+ 200: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/json": components["schemas"]["Main_Diffusers_SD1_Config"] | components["schemas"]["Main_Diffusers_SD2_Config"] | components["schemas"]["Main_Diffusers_SDXL_Config"] | components["schemas"]["Main_Diffusers_SDXLRefiner_Config"] | components["schemas"]["Main_Diffusers_SD3_Config"] | components["schemas"]["Main_Diffusers_FLUX_Config"] | components["schemas"]["Main_Diffusers_Flux2_Config"] | components["schemas"]["Main_Diffusers_CogView4_Config"] | components["schemas"]["Main_Diffusers_QwenImage_Config"] | components["schemas"]["Main_Diffusers_Wan_Config"] | components["schemas"]["Main_Diffusers_ZImage_Config"] | components["schemas"]["Main_Checkpoint_SD1_Config"] | components["schemas"]["Main_Checkpoint_SD2_Config"] | components["schemas"]["Main_Checkpoint_SDXL_Config"] | components["schemas"]["Main_Checkpoint_SDXLRefiner_Config"] | components["schemas"]["Main_Checkpoint_Flux2_Config"] | components["schemas"]["Main_Checkpoint_FLUX_Config"] | components["schemas"]["Main_Checkpoint_QwenImage_Config"] | components["schemas"]["Main_Checkpoint_ZImage_Config"] | components["schemas"]["Main_Checkpoint_Anima_Config"] | components["schemas"]["Main_BnBNF4_FLUX_Config"] | components["schemas"]["Main_GGUF_Flux2_Config"] | components["schemas"]["Main_GGUF_FLUX_Config"] | components["schemas"]["Main_GGUF_QwenImage_Config"] | components["schemas"]["Main_GGUF_Wan_Config"] | components["schemas"]["Main_GGUF_ZImage_Config"] | components["schemas"]["VAE_Checkpoint_SD1_Config"] | components["schemas"]["VAE_Checkpoint_SD2_Config"] | components["schemas"]["VAE_Checkpoint_SDXL_Config"] | components["schemas"]["VAE_Checkpoint_FLUX_Config"] | components["schemas"]["VAE_Checkpoint_Flux2_Config"] | components["schemas"]["VAE_Checkpoint_Wan_Config"] | components["schemas"]["VAE_Checkpoint_QwenImage_Config"] | components["schemas"]["VAE_Checkpoint_Anima_Config"] | components["schemas"]["VAE_Diffusers_SD1_Config"] | components["schemas"]["VAE_Diffusers_SDXL_Config"] | components["schemas"]["VAE_Diffusers_Flux2_Config"] | components["schemas"]["VAE_Diffusers_Wan_Config"] | components["schemas"]["ControlNet_Checkpoint_SD1_Config"] | components["schemas"]["ControlNet_Checkpoint_SD2_Config"] | components["schemas"]["ControlNet_Checkpoint_SDXL_Config"] | components["schemas"]["ControlNet_Checkpoint_FLUX_Config"] | components["schemas"]["ControlNet_Checkpoint_ZImage_Config"] | components["schemas"]["ControlNet_Checkpoint_Anima_Config"] | components["schemas"]["ControlNet_Diffusers_SD1_Config"] | components["schemas"]["ControlNet_Diffusers_SD2_Config"] | components["schemas"]["ControlNet_Diffusers_SDXL_Config"] | components["schemas"]["ControlNet_Diffusers_FLUX_Config"] | components["schemas"]["LoRA_LyCORIS_SD1_Config"] | components["schemas"]["LoRA_LyCORIS_SD2_Config"] | components["schemas"]["LoRA_LyCORIS_SDXL_Config"] | components["schemas"]["LoRA_LyCORIS_Flux2_Config"] | components["schemas"]["LoRA_LyCORIS_FLUX_Config"] | components["schemas"]["LoRA_LyCORIS_ZImage_Config"] | components["schemas"]["LoRA_LyCORIS_QwenImage_Config"] | components["schemas"]["LoRA_LyCORIS_Wan_Config"] | components["schemas"]["LoRA_LyCORIS_Anima_Config"] | components["schemas"]["LoRA_OMI_SDXL_Config"] | components["schemas"]["LoRA_OMI_FLUX_Config"] | components["schemas"]["LoRA_Diffusers_SD1_Config"] | components["schemas"]["LoRA_Diffusers_SD2_Config"] | components["schemas"]["LoRA_Diffusers_SDXL_Config"] | components["schemas"]["LoRA_Diffusers_Flux2_Config"] | components["schemas"]["LoRA_Diffusers_FLUX_Config"] | components["schemas"]["LoRA_Diffusers_ZImage_Config"] | components["schemas"]["ControlLoRA_LyCORIS_FLUX_Config"] | components["schemas"]["T5Encoder_T5Encoder_Config"] | components["schemas"]["T5Encoder_BnBLLMint8_Config"] | components["schemas"]["Qwen3Encoder_Qwen3Encoder_Config"] | components["schemas"]["Qwen3Encoder_Checkpoint_Config"] | components["schemas"]["Qwen3Encoder_GGUF_Config"] | components["schemas"]["QwenVLEncoder_Diffusers_Config"] | components["schemas"]["QwenVLEncoder_Checkpoint_Config"] | components["schemas"]["WanT5Encoder_WanT5Encoder_Config"] | components["schemas"]["TI_File_SD1_Config"] | components["schemas"]["TI_File_SD2_Config"] | components["schemas"]["TI_File_SDXL_Config"] | components["schemas"]["TI_Folder_SD1_Config"] | components["schemas"]["TI_Folder_SD2_Config"] | components["schemas"]["TI_Folder_SDXL_Config"] | components["schemas"]["IPAdapter_InvokeAI_SD1_Config"] | components["schemas"]["IPAdapter_InvokeAI_SD2_Config"] | components["schemas"]["IPAdapter_InvokeAI_SDXL_Config"] | components["schemas"]["IPAdapter_Checkpoint_SD1_Config"] | components["schemas"]["IPAdapter_Checkpoint_SD2_Config"] | components["schemas"]["IPAdapter_Checkpoint_SDXL_Config"] | components["schemas"]["IPAdapter_Checkpoint_FLUX_Config"] | components["schemas"]["T2IAdapter_Diffusers_SD1_Config"] | components["schemas"]["T2IAdapter_Diffusers_SDXL_Config"] | components["schemas"]["Spandrel_Checkpoint_Config"] | components["schemas"]["CLIPEmbed_Diffusers_G_Config"] | components["schemas"]["CLIPEmbed_Diffusers_L_Config"] | components["schemas"]["CLIPVision_Diffusers_Config"] | components["schemas"]["SigLIP_Diffusers_Config"] | components["schemas"]["FLUXRedux_Checkpoint_Config"] | components["schemas"]["LlavaOnevision_Diffusers_Config"] | components["schemas"]["TextLLM_Diffusers_Config"] | components["schemas"]["ExternalApiModelConfig"] | components["schemas"]["Unknown_Config"];
+ };
+ };
+ /** @description Validation Error */
+ 422: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/json": components["schemas"]["HTTPValidationError"];
+ };
+ };
+ };
+ };
+ get_model_record: {
+ parameters: {
+ query?: never;
+ header?: never;
+ path: {
+ /** @description Key of the model record to fetch. */
+ key: string;
+ };
+ cookie?: never;
+ };
+ requestBody?: never;
+ responses: {
+ /** @description The model configuration was retrieved successfully */
+ 200: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ /**
+ * @example {
+ * "path": "string",
+ * "name": "string",
+ * "base": "sd-1",
+ * "type": "main",
+ * "format": "checkpoint",
+ * "config_path": "string",
+ * "key": "string",
+ * "hash": "string",
+ * "file_size": 1,
+ * "description": "string",
+ * "source": "string",
+ * "converted_at": 0,
+ * "variant": "normal",
+ * "prediction_type": "epsilon",
+ * "repo_variant": "fp16",
+ * "upcast_attention": false
+ * }
+ */
+ "application/json": components["schemas"]["Main_Diffusers_SD1_Config"] | components["schemas"]["Main_Diffusers_SD2_Config"] | components["schemas"]["Main_Diffusers_SDXL_Config"] | components["schemas"]["Main_Diffusers_SDXLRefiner_Config"] | components["schemas"]["Main_Diffusers_SD3_Config"] | components["schemas"]["Main_Diffusers_FLUX_Config"] | components["schemas"]["Main_Diffusers_Flux2_Config"] | components["schemas"]["Main_Diffusers_CogView4_Config"] | components["schemas"]["Main_Diffusers_QwenImage_Config"] | components["schemas"]["Main_Diffusers_Wan_Config"] | components["schemas"]["Main_Diffusers_ZImage_Config"] | components["schemas"]["Main_Checkpoint_SD1_Config"] | components["schemas"]["Main_Checkpoint_SD2_Config"] | components["schemas"]["Main_Checkpoint_SDXL_Config"] | components["schemas"]["Main_Checkpoint_SDXLRefiner_Config"] | components["schemas"]["Main_Checkpoint_Flux2_Config"] | components["schemas"]["Main_Checkpoint_FLUX_Config"] | components["schemas"]["Main_Checkpoint_QwenImage_Config"] | components["schemas"]["Main_Checkpoint_ZImage_Config"] | components["schemas"]["Main_Checkpoint_Anima_Config"] | components["schemas"]["Main_BnBNF4_FLUX_Config"] | components["schemas"]["Main_GGUF_Flux2_Config"] | components["schemas"]["Main_GGUF_FLUX_Config"] | components["schemas"]["Main_GGUF_QwenImage_Config"] | components["schemas"]["Main_GGUF_Wan_Config"] | components["schemas"]["Main_GGUF_ZImage_Config"] | components["schemas"]["VAE_Checkpoint_SD1_Config"] | components["schemas"]["VAE_Checkpoint_SD2_Config"] | components["schemas"]["VAE_Checkpoint_SDXL_Config"] | components["schemas"]["VAE_Checkpoint_FLUX_Config"] | components["schemas"]["VAE_Checkpoint_Flux2_Config"] | components["schemas"]["VAE_Checkpoint_Wan_Config"] | components["schemas"]["VAE_Checkpoint_QwenImage_Config"] | components["schemas"]["VAE_Checkpoint_Anima_Config"] | components["schemas"]["VAE_Diffusers_SD1_Config"] | components["schemas"]["VAE_Diffusers_SDXL_Config"] | components["schemas"]["VAE_Diffusers_Flux2_Config"] | components["schemas"]["VAE_Diffusers_Wan_Config"] | components["schemas"]["ControlNet_Checkpoint_SD1_Config"] | components["schemas"]["ControlNet_Checkpoint_SD2_Config"] | components["schemas"]["ControlNet_Checkpoint_SDXL_Config"] | components["schemas"]["ControlNet_Checkpoint_FLUX_Config"] | components["schemas"]["ControlNet_Checkpoint_ZImage_Config"] | components["schemas"]["ControlNet_Checkpoint_Anima_Config"] | components["schemas"]["ControlNet_Diffusers_SD1_Config"] | components["schemas"]["ControlNet_Diffusers_SD2_Config"] | components["schemas"]["ControlNet_Diffusers_SDXL_Config"] | components["schemas"]["ControlNet_Diffusers_FLUX_Config"] | components["schemas"]["LoRA_LyCORIS_SD1_Config"] | components["schemas"]["LoRA_LyCORIS_SD2_Config"] | components["schemas"]["LoRA_LyCORIS_SDXL_Config"] | components["schemas"]["LoRA_LyCORIS_Flux2_Config"] | components["schemas"]["LoRA_LyCORIS_FLUX_Config"] | components["schemas"]["LoRA_LyCORIS_ZImage_Config"] | components["schemas"]["LoRA_LyCORIS_QwenImage_Config"] | components["schemas"]["LoRA_LyCORIS_Wan_Config"] | components["schemas"]["LoRA_LyCORIS_Anima_Config"] | components["schemas"]["LoRA_OMI_SDXL_Config"] | components["schemas"]["LoRA_OMI_FLUX_Config"] | components["schemas"]["LoRA_Diffusers_SD1_Config"] | components["schemas"]["LoRA_Diffusers_SD2_Config"] | components["schemas"]["LoRA_Diffusers_SDXL_Config"] | components["schemas"]["LoRA_Diffusers_Flux2_Config"] | components["schemas"]["LoRA_Diffusers_FLUX_Config"] | components["schemas"]["LoRA_Diffusers_ZImage_Config"] | components["schemas"]["ControlLoRA_LyCORIS_FLUX_Config"] | components["schemas"]["T5Encoder_T5Encoder_Config"] | components["schemas"]["T5Encoder_BnBLLMint8_Config"] | components["schemas"]["Qwen3Encoder_Qwen3Encoder_Config"] | components["schemas"]["Qwen3Encoder_Checkpoint_Config"] | components["schemas"]["Qwen3Encoder_GGUF_Config"] | components["schemas"]["QwenVLEncoder_Diffusers_Config"] | components["schemas"]["QwenVLEncoder_Checkpoint_Config"] | components["schemas"]["WanT5Encoder_WanT5Encoder_Config"] | components["schemas"]["TI_File_SD1_Config"] | components["schemas"]["TI_File_SD2_Config"] | components["schemas"]["TI_File_SDXL_Config"] | components["schemas"]["TI_Folder_SD1_Config"] | components["schemas"]["TI_Folder_SD2_Config"] | components["schemas"]["TI_Folder_SDXL_Config"] | components["schemas"]["IPAdapter_InvokeAI_SD1_Config"] | components["schemas"]["IPAdapter_InvokeAI_SD2_Config"] | components["schemas"]["IPAdapter_InvokeAI_SDXL_Config"] | components["schemas"]["IPAdapter_Checkpoint_SD1_Config"] | components["schemas"]["IPAdapter_Checkpoint_SD2_Config"] | components["schemas"]["IPAdapter_Checkpoint_SDXL_Config"] | components["schemas"]["IPAdapter_Checkpoint_FLUX_Config"] | components["schemas"]["T2IAdapter_Diffusers_SD1_Config"] | components["schemas"]["T2IAdapter_Diffusers_SDXL_Config"] | components["schemas"]["Spandrel_Checkpoint_Config"] | components["schemas"]["CLIPEmbed_Diffusers_G_Config"] | components["schemas"]["CLIPEmbed_Diffusers_L_Config"] | components["schemas"]["CLIPVision_Diffusers_Config"] | components["schemas"]["SigLIP_Diffusers_Config"] | components["schemas"]["FLUXRedux_Checkpoint_Config"] | components["schemas"]["LlavaOnevision_Diffusers_Config"] | components["schemas"]["TextLLM_Diffusers_Config"] | components["schemas"]["ExternalApiModelConfig"] | components["schemas"]["Unknown_Config"];
+ };
+ };
+ /** @description Bad request */
+ 400: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content?: never;
+ };
+ /** @description The model could not be found */
+ 404: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content?: never;
+ };
+ /** @description Validation Error */
+ 422: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/json": components["schemas"]["HTTPValidationError"];
+ };
+ };
+ };
+ };
+ delete_model: {
+ parameters: {
+ query?: never;
+ header?: never;
+ path: {
+ /** @description Unique key of model to remove from model registry. */
+ key: string;
+ };
+ cookie?: never;
+ };
+ requestBody?: never;
+ responses: {
+ /** @description Model deleted successfully */
+ 204: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content?: never;
+ };
+ /** @description Model not found */
+ 404: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content?: never;
+ };
+ /** @description Validation Error */
+ 422: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/json": components["schemas"]["HTTPValidationError"];
+ };
+ };
+ };
+ };
+ update_model_record: {
+ parameters: {
+ query?: never;
+ header?: never;
+ path: {
+ /** @description Unique key of model */
+ key: string;
+ };
+ cookie?: never;
+ };
requestBody: {
content: {
- "application/json": components["schemas"]["UserProfileUpdateRequest"];
+ "application/json": components["schemas"]["ModelRecordChanges"];
};
};
responses: {
- /** @description Successful Response */
+ /** @description The model was updated successfully */
200: {
headers: {
[name: string]: unknown;
};
content: {
- "application/json": components["schemas"]["UserDTO"];
+ /**
+ * @example {
+ * "path": "string",
+ * "name": "string",
+ * "base": "sd-1",
+ * "type": "main",
+ * "format": "checkpoint",
+ * "config_path": "string",
+ * "key": "string",
+ * "hash": "string",
+ * "file_size": 1,
+ * "description": "string",
+ * "source": "string",
+ * "converted_at": 0,
+ * "variant": "normal",
+ * "prediction_type": "epsilon",
+ * "repo_variant": "fp16",
+ * "upcast_attention": false
+ * }
+ */
+ "application/json": components["schemas"]["Main_Diffusers_SD1_Config"] | components["schemas"]["Main_Diffusers_SD2_Config"] | components["schemas"]["Main_Diffusers_SDXL_Config"] | components["schemas"]["Main_Diffusers_SDXLRefiner_Config"] | components["schemas"]["Main_Diffusers_SD3_Config"] | components["schemas"]["Main_Diffusers_FLUX_Config"] | components["schemas"]["Main_Diffusers_Flux2_Config"] | components["schemas"]["Main_Diffusers_CogView4_Config"] | components["schemas"]["Main_Diffusers_QwenImage_Config"] | components["schemas"]["Main_Diffusers_Wan_Config"] | components["schemas"]["Main_Diffusers_ZImage_Config"] | components["schemas"]["Main_Checkpoint_SD1_Config"] | components["schemas"]["Main_Checkpoint_SD2_Config"] | components["schemas"]["Main_Checkpoint_SDXL_Config"] | components["schemas"]["Main_Checkpoint_SDXLRefiner_Config"] | components["schemas"]["Main_Checkpoint_Flux2_Config"] | components["schemas"]["Main_Checkpoint_FLUX_Config"] | components["schemas"]["Main_Checkpoint_QwenImage_Config"] | components["schemas"]["Main_Checkpoint_ZImage_Config"] | components["schemas"]["Main_Checkpoint_Anima_Config"] | components["schemas"]["Main_BnBNF4_FLUX_Config"] | components["schemas"]["Main_GGUF_Flux2_Config"] | components["schemas"]["Main_GGUF_FLUX_Config"] | components["schemas"]["Main_GGUF_QwenImage_Config"] | components["schemas"]["Main_GGUF_Wan_Config"] | components["schemas"]["Main_GGUF_ZImage_Config"] | components["schemas"]["VAE_Checkpoint_SD1_Config"] | components["schemas"]["VAE_Checkpoint_SD2_Config"] | components["schemas"]["VAE_Checkpoint_SDXL_Config"] | components["schemas"]["VAE_Checkpoint_FLUX_Config"] | components["schemas"]["VAE_Checkpoint_Flux2_Config"] | components["schemas"]["VAE_Checkpoint_Wan_Config"] | components["schemas"]["VAE_Checkpoint_QwenImage_Config"] | components["schemas"]["VAE_Checkpoint_Anima_Config"] | components["schemas"]["VAE_Diffusers_SD1_Config"] | components["schemas"]["VAE_Diffusers_SDXL_Config"] | components["schemas"]["VAE_Diffusers_Flux2_Config"] | components["schemas"]["VAE_Diffusers_Wan_Config"] | components["schemas"]["ControlNet_Checkpoint_SD1_Config"] | components["schemas"]["ControlNet_Checkpoint_SD2_Config"] | components["schemas"]["ControlNet_Checkpoint_SDXL_Config"] | components["schemas"]["ControlNet_Checkpoint_FLUX_Config"] | components["schemas"]["ControlNet_Checkpoint_ZImage_Config"] | components["schemas"]["ControlNet_Checkpoint_Anima_Config"] | components["schemas"]["ControlNet_Diffusers_SD1_Config"] | components["schemas"]["ControlNet_Diffusers_SD2_Config"] | components["schemas"]["ControlNet_Diffusers_SDXL_Config"] | components["schemas"]["ControlNet_Diffusers_FLUX_Config"] | components["schemas"]["LoRA_LyCORIS_SD1_Config"] | components["schemas"]["LoRA_LyCORIS_SD2_Config"] | components["schemas"]["LoRA_LyCORIS_SDXL_Config"] | components["schemas"]["LoRA_LyCORIS_Flux2_Config"] | components["schemas"]["LoRA_LyCORIS_FLUX_Config"] | components["schemas"]["LoRA_LyCORIS_ZImage_Config"] | components["schemas"]["LoRA_LyCORIS_QwenImage_Config"] | components["schemas"]["LoRA_LyCORIS_Wan_Config"] | components["schemas"]["LoRA_LyCORIS_Anima_Config"] | components["schemas"]["LoRA_OMI_SDXL_Config"] | components["schemas"]["LoRA_OMI_FLUX_Config"] | components["schemas"]["LoRA_Diffusers_SD1_Config"] | components["schemas"]["LoRA_Diffusers_SD2_Config"] | components["schemas"]["LoRA_Diffusers_SDXL_Config"] | components["schemas"]["LoRA_Diffusers_Flux2_Config"] | components["schemas"]["LoRA_Diffusers_FLUX_Config"] | components["schemas"]["LoRA_Diffusers_ZImage_Config"] | components["schemas"]["ControlLoRA_LyCORIS_FLUX_Config"] | components["schemas"]["T5Encoder_T5Encoder_Config"] | components["schemas"]["T5Encoder_BnBLLMint8_Config"] | components["schemas"]["Qwen3Encoder_Qwen3Encoder_Config"] | components["schemas"]["Qwen3Encoder_Checkpoint_Config"] | components["schemas"]["Qwen3Encoder_GGUF_Config"] | components["schemas"]["QwenVLEncoder_Diffusers_Config"] | components["schemas"]["QwenVLEncoder_Checkpoint_Config"] | components["schemas"]["WanT5Encoder_WanT5Encoder_Config"] | components["schemas"]["TI_File_SD1_Config"] | components["schemas"]["TI_File_SD2_Config"] | components["schemas"]["TI_File_SDXL_Config"] | components["schemas"]["TI_Folder_SD1_Config"] | components["schemas"]["TI_Folder_SD2_Config"] | components["schemas"]["TI_Folder_SDXL_Config"] | components["schemas"]["IPAdapter_InvokeAI_SD1_Config"] | components["schemas"]["IPAdapter_InvokeAI_SD2_Config"] | components["schemas"]["IPAdapter_InvokeAI_SDXL_Config"] | components["schemas"]["IPAdapter_Checkpoint_SD1_Config"] | components["schemas"]["IPAdapter_Checkpoint_SD2_Config"] | components["schemas"]["IPAdapter_Checkpoint_SDXL_Config"] | components["schemas"]["IPAdapter_Checkpoint_FLUX_Config"] | components["schemas"]["T2IAdapter_Diffusers_SD1_Config"] | components["schemas"]["T2IAdapter_Diffusers_SDXL_Config"] | components["schemas"]["Spandrel_Checkpoint_Config"] | components["schemas"]["CLIPEmbed_Diffusers_G_Config"] | components["schemas"]["CLIPEmbed_Diffusers_L_Config"] | components["schemas"]["CLIPVision_Diffusers_Config"] | components["schemas"]["SigLIP_Diffusers_Config"] | components["schemas"]["FLUXRedux_Checkpoint_Config"] | components["schemas"]["LlavaOnevision_Diffusers_Config"] | components["schemas"]["TextLLM_Diffusers_Config"] | components["schemas"]["ExternalApiModelConfig"] | components["schemas"]["Unknown_Config"];
+ };
+ };
+ /** @description Bad request */
+ 400: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content?: never;
+ };
+ /** @description The model could not be found */
+ 404: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content?: never;
+ };
+ /** @description There is already a model corresponding to the new name */
+ 409: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content?: never;
+ };
+ /** @description Validation Error */
+ 422: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/json": components["schemas"]["HTTPValidationError"];
+ };
+ };
+ };
+ };
+ reidentify_model: {
+ parameters: {
+ query?: never;
+ header?: never;
+ path: {
+ /** @description Key of the model to reidentify. */
+ key: string;
+ };
+ cookie?: never;
+ };
+ requestBody?: never;
+ responses: {
+ /** @description The model configuration was retrieved successfully */
+ 200: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ /**
+ * @example {
+ * "path": "string",
+ * "name": "string",
+ * "base": "sd-1",
+ * "type": "main",
+ * "format": "checkpoint",
+ * "config_path": "string",
+ * "key": "string",
+ * "hash": "string",
+ * "file_size": 1,
+ * "description": "string",
+ * "source": "string",
+ * "converted_at": 0,
+ * "variant": "normal",
+ * "prediction_type": "epsilon",
+ * "repo_variant": "fp16",
+ * "upcast_attention": false
+ * }
+ */
+ "application/json": components["schemas"]["Main_Diffusers_SD1_Config"] | components["schemas"]["Main_Diffusers_SD2_Config"] | components["schemas"]["Main_Diffusers_SDXL_Config"] | components["schemas"]["Main_Diffusers_SDXLRefiner_Config"] | components["schemas"]["Main_Diffusers_SD3_Config"] | components["schemas"]["Main_Diffusers_FLUX_Config"] | components["schemas"]["Main_Diffusers_Flux2_Config"] | components["schemas"]["Main_Diffusers_CogView4_Config"] | components["schemas"]["Main_Diffusers_QwenImage_Config"] | components["schemas"]["Main_Diffusers_Wan_Config"] | components["schemas"]["Main_Diffusers_ZImage_Config"] | components["schemas"]["Main_Checkpoint_SD1_Config"] | components["schemas"]["Main_Checkpoint_SD2_Config"] | components["schemas"]["Main_Checkpoint_SDXL_Config"] | components["schemas"]["Main_Checkpoint_SDXLRefiner_Config"] | components["schemas"]["Main_Checkpoint_Flux2_Config"] | components["schemas"]["Main_Checkpoint_FLUX_Config"] | components["schemas"]["Main_Checkpoint_QwenImage_Config"] | components["schemas"]["Main_Checkpoint_ZImage_Config"] | components["schemas"]["Main_Checkpoint_Anima_Config"] | components["schemas"]["Main_BnBNF4_FLUX_Config"] | components["schemas"]["Main_GGUF_Flux2_Config"] | components["schemas"]["Main_GGUF_FLUX_Config"] | components["schemas"]["Main_GGUF_QwenImage_Config"] | components["schemas"]["Main_GGUF_Wan_Config"] | components["schemas"]["Main_GGUF_ZImage_Config"] | components["schemas"]["VAE_Checkpoint_SD1_Config"] | components["schemas"]["VAE_Checkpoint_SD2_Config"] | components["schemas"]["VAE_Checkpoint_SDXL_Config"] | components["schemas"]["VAE_Checkpoint_FLUX_Config"] | components["schemas"]["VAE_Checkpoint_Flux2_Config"] | components["schemas"]["VAE_Checkpoint_Wan_Config"] | components["schemas"]["VAE_Checkpoint_QwenImage_Config"] | components["schemas"]["VAE_Checkpoint_Anima_Config"] | components["schemas"]["VAE_Diffusers_SD1_Config"] | components["schemas"]["VAE_Diffusers_SDXL_Config"] | components["schemas"]["VAE_Diffusers_Flux2_Config"] | components["schemas"]["VAE_Diffusers_Wan_Config"] | components["schemas"]["ControlNet_Checkpoint_SD1_Config"] | components["schemas"]["ControlNet_Checkpoint_SD2_Config"] | components["schemas"]["ControlNet_Checkpoint_SDXL_Config"] | components["schemas"]["ControlNet_Checkpoint_FLUX_Config"] | components["schemas"]["ControlNet_Checkpoint_ZImage_Config"] | components["schemas"]["ControlNet_Checkpoint_Anima_Config"] | components["schemas"]["ControlNet_Diffusers_SD1_Config"] | components["schemas"]["ControlNet_Diffusers_SD2_Config"] | components["schemas"]["ControlNet_Diffusers_SDXL_Config"] | components["schemas"]["ControlNet_Diffusers_FLUX_Config"] | components["schemas"]["LoRA_LyCORIS_SD1_Config"] | components["schemas"]["LoRA_LyCORIS_SD2_Config"] | components["schemas"]["LoRA_LyCORIS_SDXL_Config"] | components["schemas"]["LoRA_LyCORIS_Flux2_Config"] | components["schemas"]["LoRA_LyCORIS_FLUX_Config"] | components["schemas"]["LoRA_LyCORIS_ZImage_Config"] | components["schemas"]["LoRA_LyCORIS_QwenImage_Config"] | components["schemas"]["LoRA_LyCORIS_Wan_Config"] | components["schemas"]["LoRA_LyCORIS_Anima_Config"] | components["schemas"]["LoRA_OMI_SDXL_Config"] | components["schemas"]["LoRA_OMI_FLUX_Config"] | components["schemas"]["LoRA_Diffusers_SD1_Config"] | components["schemas"]["LoRA_Diffusers_SD2_Config"] | components["schemas"]["LoRA_Diffusers_SDXL_Config"] | components["schemas"]["LoRA_Diffusers_Flux2_Config"] | components["schemas"]["LoRA_Diffusers_FLUX_Config"] | components["schemas"]["LoRA_Diffusers_ZImage_Config"] | components["schemas"]["ControlLoRA_LyCORIS_FLUX_Config"] | components["schemas"]["T5Encoder_T5Encoder_Config"] | components["schemas"]["T5Encoder_BnBLLMint8_Config"] | components["schemas"]["Qwen3Encoder_Qwen3Encoder_Config"] | components["schemas"]["Qwen3Encoder_Checkpoint_Config"] | components["schemas"]["Qwen3Encoder_GGUF_Config"] | components["schemas"]["QwenVLEncoder_Diffusers_Config"] | components["schemas"]["QwenVLEncoder_Checkpoint_Config"] | components["schemas"]["WanT5Encoder_WanT5Encoder_Config"] | components["schemas"]["TI_File_SD1_Config"] | components["schemas"]["TI_File_SD2_Config"] | components["schemas"]["TI_File_SDXL_Config"] | components["schemas"]["TI_Folder_SD1_Config"] | components["schemas"]["TI_Folder_SD2_Config"] | components["schemas"]["TI_Folder_SDXL_Config"] | components["schemas"]["IPAdapter_InvokeAI_SD1_Config"] | components["schemas"]["IPAdapter_InvokeAI_SD2_Config"] | components["schemas"]["IPAdapter_InvokeAI_SDXL_Config"] | components["schemas"]["IPAdapter_Checkpoint_SD1_Config"] | components["schemas"]["IPAdapter_Checkpoint_SD2_Config"] | components["schemas"]["IPAdapter_Checkpoint_SDXL_Config"] | components["schemas"]["IPAdapter_Checkpoint_FLUX_Config"] | components["schemas"]["T2IAdapter_Diffusers_SD1_Config"] | components["schemas"]["T2IAdapter_Diffusers_SDXL_Config"] | components["schemas"]["Spandrel_Checkpoint_Config"] | components["schemas"]["CLIPEmbed_Diffusers_G_Config"] | components["schemas"]["CLIPEmbed_Diffusers_L_Config"] | components["schemas"]["CLIPVision_Diffusers_Config"] | components["schemas"]["SigLIP_Diffusers_Config"] | components["schemas"]["FLUXRedux_Checkpoint_Config"] | components["schemas"]["LlavaOnevision_Diffusers_Config"] | components["schemas"]["TextLLM_Diffusers_Config"] | components["schemas"]["ExternalApiModelConfig"] | components["schemas"]["Unknown_Config"];
+ };
+ };
+ /** @description Bad request */
+ 400: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content?: never;
+ };
+ /** @description The model could not be found */
+ 404: {
+ headers: {
+ [name: string]: unknown;
};
+ content?: never;
};
/** @description Validation Error */
422: {
@@ -34295,100 +37836,72 @@ export interface operations {
};
};
};
- setup_admin_api_v1_auth_setup_post: {
+ scan_for_models: {
parameters: {
- query?: never;
+ query?: {
+ /** @description Directory path to search for models */
+ scan_path?: string;
+ };
header?: never;
path?: never;
cookie?: never;
};
- requestBody: {
- content: {
- "application/json": components["schemas"]["SetupRequest"];
- };
- };
+ requestBody?: never;
responses: {
- /** @description Successful Response */
+ /** @description Directory scanned successfully */
200: {
headers: {
[name: string]: unknown;
};
content: {
- "application/json": components["schemas"]["SetupResponse"];
+ "application/json": components["schemas"]["FoundModel"][];
};
};
- /** @description Validation Error */
- 422: {
+ /** @description Invalid directory path */
+ 400: {
headers: {
[name: string]: unknown;
};
- content: {
- "application/json": components["schemas"]["HTTPValidationError"];
- };
+ content?: never;
};
- };
- };
- generate_password_api_v1_auth_generate_password_get: {
- parameters: {
- query?: never;
- header?: never;
- path?: never;
- cookie?: never;
- };
- requestBody?: never;
- responses: {
- /** @description Successful Response */
- 200: {
+ /** @description Validation Error */
+ 422: {
headers: {
[name: string]: unknown;
};
content: {
- "application/json": components["schemas"]["GeneratePasswordResponse"];
+ "application/json": components["schemas"]["HTTPValidationError"];
};
};
};
};
- list_users_api_v1_auth_users_get: {
+ get_hugging_face_models: {
parameters: {
- query?: never;
+ query?: {
+ /** @description Hugging face repo to search for models */
+ hugging_face_repo?: string;
+ };
header?: never;
path?: never;
cookie?: never;
};
requestBody?: never;
responses: {
- /** @description Successful Response */
+ /** @description Hugging Face repo scanned successfully */
200: {
headers: {
[name: string]: unknown;
};
content: {
- "application/json": components["schemas"]["UserDTO"][];
+ "application/json": components["schemas"]["HuggingFaceModels"];
};
};
- };
- };
- create_user_api_v1_auth_users_post: {
- parameters: {
- query?: never;
- header?: never;
- path?: never;
- cookie?: never;
- };
- requestBody: {
- content: {
- "application/json": components["schemas"]["AdminUserCreateRequest"];
- };
- };
- responses: {
- /** @description Successful Response */
- 201: {
+ /** @description Invalid hugging face repo */
+ 400: {
headers: {
[name: string]: unknown;
};
- content: {
- "application/json": components["schemas"]["UserDTO"];
- };
+ content?: never;
};
/** @description Validation Error */
422: {
@@ -34401,26 +37914,40 @@ export interface operations {
};
};
};
- get_user_api_v1_auth_users__user_id__get: {
+ get_model_image: {
parameters: {
query?: never;
header?: never;
path: {
- /** @description User ID */
- user_id: string;
+ /** @description The name of model image file to get */
+ key: string;
};
cookie?: never;
};
requestBody?: never;
responses: {
- /** @description Successful Response */
+ /** @description The model image was fetched successfully */
200: {
headers: {
[name: string]: unknown;
};
content: {
- "application/json": components["schemas"]["UserDTO"];
+ "application/json": unknown;
+ };
+ };
+ /** @description Bad request */
+ 400: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content?: never;
+ };
+ /** @description The model image could not be found */
+ 404: {
+ headers: {
+ [name: string]: unknown;
};
+ content?: never;
};
/** @description Validation Error */
422: {
@@ -34433,25 +37960,32 @@ export interface operations {
};
};
};
- delete_user_api_v1_auth_users__user_id__delete: {
+ delete_model_image: {
parameters: {
query?: never;
header?: never;
path: {
- /** @description User ID */
- user_id: string;
+ /** @description Unique key of model image to remove from model_images directory. */
+ key: string;
};
cookie?: never;
};
requestBody?: never;
responses: {
- /** @description Successful Response */
+ /** @description Model image deleted successfully */
204: {
headers: {
[name: string]: unknown;
};
content?: never;
};
+ /** @description Model image not found */
+ 404: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content?: never;
+ };
/** @description Validation Error */
422: {
headers: {
@@ -34463,31 +37997,38 @@ export interface operations {
};
};
};
- update_user_api_v1_auth_users__user_id__patch: {
+ update_model_image: {
parameters: {
query?: never;
header?: never;
path: {
- /** @description User ID */
- user_id: string;
+ /** @description Unique key of model */
+ key: string;
};
cookie?: never;
};
requestBody: {
content: {
- "application/json": components["schemas"]["AdminUserUpdateRequest"];
+ "multipart/form-data": components["schemas"]["Body_update_model_image"];
};
};
responses: {
- /** @description Successful Response */
+ /** @description The model image was updated successfully */
200: {
headers: {
[name: string]: unknown;
};
content: {
- "application/json": components["schemas"]["UserDTO"];
+ "application/json": unknown;
};
};
+ /** @description Bad request */
+ 400: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content?: never;
+ };
/** @description Validation Error */
422: {
headers: {
@@ -34499,7 +38040,7 @@ export interface operations {
};
};
};
- parse_dynamicprompts: {
+ bulk_delete_models: {
parameters: {
query?: never;
header?: never;
@@ -34508,17 +38049,17 @@ export interface operations {
};
requestBody: {
content: {
- "application/json": components["schemas"]["Body_parse_dynamicprompts"];
+ "application/json": components["schemas"]["BulkDeleteModelsRequest"];
};
};
responses: {
- /** @description Successful Response */
+ /** @description Models deleted (possibly with some failures) */
200: {
headers: {
[name: string]: unknown;
};
content: {
- "application/json": components["schemas"]["DynamicPromptsResponse"];
+ "application/json": components["schemas"]["BulkDeleteModelsResponse"];
};
};
/** @description Validation Error */
@@ -34532,7 +38073,7 @@ export interface operations {
};
};
};
- expand_prompt: {
+ bulk_reidentify_models: {
parameters: {
query?: never;
header?: never;
@@ -34541,17 +38082,17 @@ export interface operations {
};
requestBody: {
content: {
- "application/json": components["schemas"]["ExpandPromptRequest"];
+ "application/json": components["schemas"]["BulkReidentifyModelsRequest"];
};
};
responses: {
- /** @description Successful Response */
+ /** @description Models reidentified (possibly with some failures) */
200: {
headers: {
[name: string]: unknown;
};
content: {
- "application/json": components["schemas"]["ExpandPromptResponse"];
+ "application/json": components["schemas"]["BulkReidentifyModelsResponse"];
};
};
/** @description Validation Error */
@@ -34565,18 +38106,14 @@ export interface operations {
};
};
};
- image_to_prompt: {
+ list_model_installs: {
parameters: {
query?: never;
header?: never;
path?: never;
cookie?: never;
};
- requestBody: {
- content: {
- "application/json": components["schemas"]["ImageToPromptRequest"];
- };
- };
+ requestBody?: never;
responses: {
/** @description Successful Response */
200: {
@@ -34584,50 +38121,53 @@ export interface operations {
[name: string]: unknown;
};
content: {
- "application/json": components["schemas"]["ImageToPromptResponse"];
- };
- };
- /** @description Validation Error */
- 422: {
- headers: {
- [name: string]: unknown;
- };
- content: {
- "application/json": components["schemas"]["HTTPValidationError"];
+ "application/json": components["schemas"]["ModelInstallJob"][];
};
};
};
};
- list_model_records: {
+ install_model: {
parameters: {
- query?: {
- /** @description Base models to include */
- base_models?: components["schemas"]["BaseModelType"][] | null;
- /** @description The type of model to get */
- model_type?: components["schemas"]["ModelType"] | null;
- /** @description Exact match on the name of the model */
- model_name?: string | null;
- /** @description Exact match on the format of the model (e.g. 'diffusers') */
- model_format?: components["schemas"]["ModelFormat"] | null;
- /** @description The field to order by */
- order_by?: components["schemas"]["ModelRecordOrderBy"];
- /** @description The direction to order by */
- direction?: components["schemas"]["SQLiteDirection"];
+ query: {
+ /** @description Model source to install, can be a local path, repo_id, or remote URL */
+ source: string;
+ /** @description Whether or not to install a local model in place */
+ inplace?: boolean | null;
+ /** @description access token for the remote resource */
+ access_token?: string | null;
};
header?: never;
path?: never;
cookie?: never;
};
- requestBody?: never;
+ requestBody: {
+ content: {
+ "application/json": components["schemas"]["ModelRecordChanges"];
+ };
+ };
responses: {
- /** @description Successful Response */
- 200: {
+ /** @description The model imported successfully */
+ 201: {
headers: {
[name: string]: unknown;
};
content: {
- "application/json": components["schemas"]["ModelsList"];
+ "application/json": components["schemas"]["ModelInstallJob"];
+ };
+ };
+ /** @description There is already a model corresponding to this path or repo_id */
+ 409: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content?: never;
+ };
+ /** @description Unrecognized file/folder format */
+ 415: {
+ headers: {
+ [name: string]: unknown;
};
+ content?: never;
};
/** @description Validation Error */
422: {
@@ -34638,38 +38178,18 @@ export interface operations {
"application/json": components["schemas"]["HTTPValidationError"];
};
};
- };
- };
- list_missing_models: {
- parameters: {
- query?: never;
- header?: never;
- path?: never;
- cookie?: never;
- };
- requestBody?: never;
- responses: {
- /** @description List of models with missing files */
- 200: {
+ /** @description The model appeared to import successfully, but could not be found in the model manager */
+ 424: {
headers: {
[name: string]: unknown;
};
- content: {
- "application/json": components["schemas"]["ModelsList"];
- };
+ content?: never;
};
};
};
- get_model_records_by_attrs: {
+ prune_model_install_jobs: {
parameters: {
- query: {
- /** @description The name of the model */
- name: string;
- /** @description The type of the model */
- type: components["schemas"]["ModelType"];
- /** @description The base model of the model */
- base: components["schemas"]["BaseModelType"];
- };
+ query?: never;
header?: never;
path?: never;
cookie?: never;
@@ -34682,25 +38202,30 @@ export interface operations {
[name: string]: unknown;
};
content: {
- "application/json": components["schemas"]["Main_Diffusers_SD1_Config"] | components["schemas"]["Main_Diffusers_SD2_Config"] | components["schemas"]["Main_Diffusers_SDXL_Config"] | components["schemas"]["Main_Diffusers_SDXLRefiner_Config"] | components["schemas"]["Main_Diffusers_SD3_Config"] | components["schemas"]["Main_Diffusers_FLUX_Config"] | components["schemas"]["Main_Diffusers_Flux2_Config"] | components["schemas"]["Main_Diffusers_CogView4_Config"] | components["schemas"]["Main_Diffusers_QwenImage_Config"] | components["schemas"]["Main_Diffusers_ZImage_Config"] | components["schemas"]["Main_Checkpoint_SD1_Config"] | components["schemas"]["Main_Checkpoint_SD2_Config"] | components["schemas"]["Main_Checkpoint_SDXL_Config"] | components["schemas"]["Main_Checkpoint_SDXLRefiner_Config"] | components["schemas"]["Main_Checkpoint_Flux2_Config"] | components["schemas"]["Main_Checkpoint_FLUX_Config"] | components["schemas"]["Main_Checkpoint_QwenImage_Config"] | components["schemas"]["Main_Checkpoint_ZImage_Config"] | components["schemas"]["Main_Checkpoint_Anima_Config"] | components["schemas"]["Main_BnBNF4_FLUX_Config"] | components["schemas"]["Main_GGUF_Flux2_Config"] | components["schemas"]["Main_GGUF_FLUX_Config"] | components["schemas"]["Main_GGUF_QwenImage_Config"] | components["schemas"]["Main_GGUF_ZImage_Config"] | components["schemas"]["VAE_Checkpoint_SD1_Config"] | components["schemas"]["VAE_Checkpoint_SD2_Config"] | components["schemas"]["VAE_Checkpoint_SDXL_Config"] | components["schemas"]["VAE_Checkpoint_FLUX_Config"] | components["schemas"]["VAE_Checkpoint_Flux2_Config"] | components["schemas"]["VAE_Checkpoint_QwenImage_Config"] | components["schemas"]["VAE_Checkpoint_Anima_Config"] | components["schemas"]["VAE_Diffusers_SD1_Config"] | components["schemas"]["VAE_Diffusers_SDXL_Config"] | components["schemas"]["VAE_Diffusers_Flux2_Config"] | components["schemas"]["ControlNet_Checkpoint_SD1_Config"] | components["schemas"]["ControlNet_Checkpoint_SD2_Config"] | components["schemas"]["ControlNet_Checkpoint_SDXL_Config"] | components["schemas"]["ControlNet_Checkpoint_FLUX_Config"] | components["schemas"]["ControlNet_Checkpoint_ZImage_Config"] | components["schemas"]["ControlNet_Checkpoint_Anima_Config"] | components["schemas"]["ControlNet_Diffusers_SD1_Config"] | components["schemas"]["ControlNet_Diffusers_SD2_Config"] | components["schemas"]["ControlNet_Diffusers_SDXL_Config"] | components["schemas"]["ControlNet_Diffusers_FLUX_Config"] | components["schemas"]["LoRA_LyCORIS_SD1_Config"] | components["schemas"]["LoRA_LyCORIS_SD2_Config"] | components["schemas"]["LoRA_LyCORIS_SDXL_Config"] | components["schemas"]["LoRA_LyCORIS_Flux2_Config"] | components["schemas"]["LoRA_LyCORIS_FLUX_Config"] | components["schemas"]["LoRA_LyCORIS_ZImage_Config"] | components["schemas"]["LoRA_LyCORIS_QwenImage_Config"] | components["schemas"]["LoRA_LyCORIS_Anima_Config"] | components["schemas"]["LoRA_OMI_SDXL_Config"] | components["schemas"]["LoRA_OMI_FLUX_Config"] | components["schemas"]["LoRA_Diffusers_SD1_Config"] | components["schemas"]["LoRA_Diffusers_SD2_Config"] | components["schemas"]["LoRA_Diffusers_SDXL_Config"] | components["schemas"]["LoRA_Diffusers_Flux2_Config"] | components["schemas"]["LoRA_Diffusers_FLUX_Config"] | components["schemas"]["LoRA_Diffusers_ZImage_Config"] | components["schemas"]["ControlLoRA_LyCORIS_FLUX_Config"] | components["schemas"]["T5Encoder_T5Encoder_Config"] | components["schemas"]["T5Encoder_BnBLLMint8_Config"] | components["schemas"]["Qwen3Encoder_Qwen3Encoder_Config"] | components["schemas"]["Qwen3Encoder_Checkpoint_Config"] | components["schemas"]["Qwen3Encoder_GGUF_Config"] | components["schemas"]["QwenVLEncoder_Diffusers_Config"] | components["schemas"]["QwenVLEncoder_Checkpoint_Config"] | components["schemas"]["TI_File_SD1_Config"] | components["schemas"]["TI_File_SD2_Config"] | components["schemas"]["TI_File_SDXL_Config"] | components["schemas"]["TI_Folder_SD1_Config"] | components["schemas"]["TI_Folder_SD2_Config"] | components["schemas"]["TI_Folder_SDXL_Config"] | components["schemas"]["IPAdapter_InvokeAI_SD1_Config"] | components["schemas"]["IPAdapter_InvokeAI_SD2_Config"] | components["schemas"]["IPAdapter_InvokeAI_SDXL_Config"] | components["schemas"]["IPAdapter_Checkpoint_SD1_Config"] | components["schemas"]["IPAdapter_Checkpoint_SD2_Config"] | components["schemas"]["IPAdapter_Checkpoint_SDXL_Config"] | components["schemas"]["IPAdapter_Checkpoint_FLUX_Config"] | components["schemas"]["T2IAdapter_Diffusers_SD1_Config"] | components["schemas"]["T2IAdapter_Diffusers_SDXL_Config"] | components["schemas"]["Spandrel_Checkpoint_Config"] | components["schemas"]["CLIPEmbed_Diffusers_G_Config"] | components["schemas"]["CLIPEmbed_Diffusers_L_Config"] | components["schemas"]["CLIPVision_Diffusers_Config"] | components["schemas"]["SigLIP_Diffusers_Config"] | components["schemas"]["FLUXRedux_Checkpoint_Config"] | components["schemas"]["LlavaOnevision_Diffusers_Config"] | components["schemas"]["TextLLM_Diffusers_Config"] | components["schemas"]["ExternalApiModelConfig"] | components["schemas"]["Unknown_Config"];
+ "application/json": unknown;
};
};
- /** @description Validation Error */
- 422: {
+ /** @description All completed and errored jobs have been pruned */
+ 204: {
headers: {
[name: string]: unknown;
};
- content: {
- "application/json": components["schemas"]["HTTPValidationError"];
+ content?: never;
+ };
+ /** @description Bad request */
+ 400: {
+ headers: {
+ [name: string]: unknown;
};
+ content?: never;
};
};
};
- get_model_records_by_hash: {
+ install_hugging_face_model: {
parameters: {
query: {
- /** @description The hash of the model */
- hash: string;
+ /** @description HuggingFace repo_id to install */
+ source: string;
};
header?: never;
path?: never;
@@ -34708,14 +38233,28 @@ export interface operations {
};
requestBody?: never;
responses: {
- /** @description Successful Response */
- 200: {
+ /** @description The model is being installed */
+ 201: {
headers: {
[name: string]: unknown;
};
content: {
- "application/json": components["schemas"]["Main_Diffusers_SD1_Config"] | components["schemas"]["Main_Diffusers_SD2_Config"] | components["schemas"]["Main_Diffusers_SDXL_Config"] | components["schemas"]["Main_Diffusers_SDXLRefiner_Config"] | components["schemas"]["Main_Diffusers_SD3_Config"] | components["schemas"]["Main_Diffusers_FLUX_Config"] | components["schemas"]["Main_Diffusers_Flux2_Config"] | components["schemas"]["Main_Diffusers_CogView4_Config"] | components["schemas"]["Main_Diffusers_QwenImage_Config"] | components["schemas"]["Main_Diffusers_ZImage_Config"] | components["schemas"]["Main_Checkpoint_SD1_Config"] | components["schemas"]["Main_Checkpoint_SD2_Config"] | components["schemas"]["Main_Checkpoint_SDXL_Config"] | components["schemas"]["Main_Checkpoint_SDXLRefiner_Config"] | components["schemas"]["Main_Checkpoint_Flux2_Config"] | components["schemas"]["Main_Checkpoint_FLUX_Config"] | components["schemas"]["Main_Checkpoint_QwenImage_Config"] | components["schemas"]["Main_Checkpoint_ZImage_Config"] | components["schemas"]["Main_Checkpoint_Anima_Config"] | components["schemas"]["Main_BnBNF4_FLUX_Config"] | components["schemas"]["Main_GGUF_Flux2_Config"] | components["schemas"]["Main_GGUF_FLUX_Config"] | components["schemas"]["Main_GGUF_QwenImage_Config"] | components["schemas"]["Main_GGUF_ZImage_Config"] | components["schemas"]["VAE_Checkpoint_SD1_Config"] | components["schemas"]["VAE_Checkpoint_SD2_Config"] | components["schemas"]["VAE_Checkpoint_SDXL_Config"] | components["schemas"]["VAE_Checkpoint_FLUX_Config"] | components["schemas"]["VAE_Checkpoint_Flux2_Config"] | components["schemas"]["VAE_Checkpoint_QwenImage_Config"] | components["schemas"]["VAE_Checkpoint_Anima_Config"] | components["schemas"]["VAE_Diffusers_SD1_Config"] | components["schemas"]["VAE_Diffusers_SDXL_Config"] | components["schemas"]["VAE_Diffusers_Flux2_Config"] | components["schemas"]["ControlNet_Checkpoint_SD1_Config"] | components["schemas"]["ControlNet_Checkpoint_SD2_Config"] | components["schemas"]["ControlNet_Checkpoint_SDXL_Config"] | components["schemas"]["ControlNet_Checkpoint_FLUX_Config"] | components["schemas"]["ControlNet_Checkpoint_ZImage_Config"] | components["schemas"]["ControlNet_Checkpoint_Anima_Config"] | components["schemas"]["ControlNet_Diffusers_SD1_Config"] | components["schemas"]["ControlNet_Diffusers_SD2_Config"] | components["schemas"]["ControlNet_Diffusers_SDXL_Config"] | components["schemas"]["ControlNet_Diffusers_FLUX_Config"] | components["schemas"]["LoRA_LyCORIS_SD1_Config"] | components["schemas"]["LoRA_LyCORIS_SD2_Config"] | components["schemas"]["LoRA_LyCORIS_SDXL_Config"] | components["schemas"]["LoRA_LyCORIS_Flux2_Config"] | components["schemas"]["LoRA_LyCORIS_FLUX_Config"] | components["schemas"]["LoRA_LyCORIS_ZImage_Config"] | components["schemas"]["LoRA_LyCORIS_QwenImage_Config"] | components["schemas"]["LoRA_LyCORIS_Anima_Config"] | components["schemas"]["LoRA_OMI_SDXL_Config"] | components["schemas"]["LoRA_OMI_FLUX_Config"] | components["schemas"]["LoRA_Diffusers_SD1_Config"] | components["schemas"]["LoRA_Diffusers_SD2_Config"] | components["schemas"]["LoRA_Diffusers_SDXL_Config"] | components["schemas"]["LoRA_Diffusers_Flux2_Config"] | components["schemas"]["LoRA_Diffusers_FLUX_Config"] | components["schemas"]["LoRA_Diffusers_ZImage_Config"] | components["schemas"]["ControlLoRA_LyCORIS_FLUX_Config"] | components["schemas"]["T5Encoder_T5Encoder_Config"] | components["schemas"]["T5Encoder_BnBLLMint8_Config"] | components["schemas"]["Qwen3Encoder_Qwen3Encoder_Config"] | components["schemas"]["Qwen3Encoder_Checkpoint_Config"] | components["schemas"]["Qwen3Encoder_GGUF_Config"] | components["schemas"]["QwenVLEncoder_Diffusers_Config"] | components["schemas"]["QwenVLEncoder_Checkpoint_Config"] | components["schemas"]["TI_File_SD1_Config"] | components["schemas"]["TI_File_SD2_Config"] | components["schemas"]["TI_File_SDXL_Config"] | components["schemas"]["TI_Folder_SD1_Config"] | components["schemas"]["TI_Folder_SD2_Config"] | components["schemas"]["TI_Folder_SDXL_Config"] | components["schemas"]["IPAdapter_InvokeAI_SD1_Config"] | components["schemas"]["IPAdapter_InvokeAI_SD2_Config"] | components["schemas"]["IPAdapter_InvokeAI_SDXL_Config"] | components["schemas"]["IPAdapter_Checkpoint_SD1_Config"] | components["schemas"]["IPAdapter_Checkpoint_SD2_Config"] | components["schemas"]["IPAdapter_Checkpoint_SDXL_Config"] | components["schemas"]["IPAdapter_Checkpoint_FLUX_Config"] | components["schemas"]["T2IAdapter_Diffusers_SD1_Config"] | components["schemas"]["T2IAdapter_Diffusers_SDXL_Config"] | components["schemas"]["Spandrel_Checkpoint_Config"] | components["schemas"]["CLIPEmbed_Diffusers_G_Config"] | components["schemas"]["CLIPEmbed_Diffusers_L_Config"] | components["schemas"]["CLIPVision_Diffusers_Config"] | components["schemas"]["SigLIP_Diffusers_Config"] | components["schemas"]["FLUXRedux_Checkpoint_Config"] | components["schemas"]["LlavaOnevision_Diffusers_Config"] | components["schemas"]["TextLLM_Diffusers_Config"] | components["schemas"]["ExternalApiModelConfig"] | components["schemas"]["Unknown_Config"];
+ "text/html": string;
+ };
+ };
+ /** @description Bad request */
+ 400: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content?: never;
+ };
+ /** @description There is already a model corresponding to this path or repo_id */
+ 409: {
+ headers: {
+ [name: string]: unknown;
};
+ content?: never;
};
/** @description Validation Error */
422: {
@@ -34728,55 +38267,28 @@ export interface operations {
};
};
};
- get_model_record: {
+ get_model_install_job: {
parameters: {
query?: never;
header?: never;
path: {
- /** @description Key of the model record to fetch. */
- key: string;
+ /** @description Model install id */
+ id: number;
};
cookie?: never;
};
requestBody?: never;
responses: {
- /** @description The model configuration was retrieved successfully */
+ /** @description Success */
200: {
headers: {
[name: string]: unknown;
};
content: {
- /**
- * @example {
- * "path": "string",
- * "name": "string",
- * "base": "sd-1",
- * "type": "main",
- * "format": "checkpoint",
- * "config_path": "string",
- * "key": "string",
- * "hash": "string",
- * "file_size": 1,
- * "description": "string",
- * "source": "string",
- * "converted_at": 0,
- * "variant": "normal",
- * "prediction_type": "epsilon",
- * "repo_variant": "fp16",
- * "upcast_attention": false
- * }
- */
- "application/json": components["schemas"]["Main_Diffusers_SD1_Config"] | components["schemas"]["Main_Diffusers_SD2_Config"] | components["schemas"]["Main_Diffusers_SDXL_Config"] | components["schemas"]["Main_Diffusers_SDXLRefiner_Config"] | components["schemas"]["Main_Diffusers_SD3_Config"] | components["schemas"]["Main_Diffusers_FLUX_Config"] | components["schemas"]["Main_Diffusers_Flux2_Config"] | components["schemas"]["Main_Diffusers_CogView4_Config"] | components["schemas"]["Main_Diffusers_QwenImage_Config"] | components["schemas"]["Main_Diffusers_ZImage_Config"] | components["schemas"]["Main_Checkpoint_SD1_Config"] | components["schemas"]["Main_Checkpoint_SD2_Config"] | components["schemas"]["Main_Checkpoint_SDXL_Config"] | components["schemas"]["Main_Checkpoint_SDXLRefiner_Config"] | components["schemas"]["Main_Checkpoint_Flux2_Config"] | components["schemas"]["Main_Checkpoint_FLUX_Config"] | components["schemas"]["Main_Checkpoint_QwenImage_Config"] | components["schemas"]["Main_Checkpoint_ZImage_Config"] | components["schemas"]["Main_Checkpoint_Anima_Config"] | components["schemas"]["Main_BnBNF4_FLUX_Config"] | components["schemas"]["Main_GGUF_Flux2_Config"] | components["schemas"]["Main_GGUF_FLUX_Config"] | components["schemas"]["Main_GGUF_QwenImage_Config"] | components["schemas"]["Main_GGUF_ZImage_Config"] | components["schemas"]["VAE_Checkpoint_SD1_Config"] | components["schemas"]["VAE_Checkpoint_SD2_Config"] | components["schemas"]["VAE_Checkpoint_SDXL_Config"] | components["schemas"]["VAE_Checkpoint_FLUX_Config"] | components["schemas"]["VAE_Checkpoint_Flux2_Config"] | components["schemas"]["VAE_Checkpoint_QwenImage_Config"] | components["schemas"]["VAE_Checkpoint_Anima_Config"] | components["schemas"]["VAE_Diffusers_SD1_Config"] | components["schemas"]["VAE_Diffusers_SDXL_Config"] | components["schemas"]["VAE_Diffusers_Flux2_Config"] | components["schemas"]["ControlNet_Checkpoint_SD1_Config"] | components["schemas"]["ControlNet_Checkpoint_SD2_Config"] | components["schemas"]["ControlNet_Checkpoint_SDXL_Config"] | components["schemas"]["ControlNet_Checkpoint_FLUX_Config"] | components["schemas"]["ControlNet_Checkpoint_ZImage_Config"] | components["schemas"]["ControlNet_Checkpoint_Anima_Config"] | components["schemas"]["ControlNet_Diffusers_SD1_Config"] | components["schemas"]["ControlNet_Diffusers_SD2_Config"] | components["schemas"]["ControlNet_Diffusers_SDXL_Config"] | components["schemas"]["ControlNet_Diffusers_FLUX_Config"] | components["schemas"]["LoRA_LyCORIS_SD1_Config"] | components["schemas"]["LoRA_LyCORIS_SD2_Config"] | components["schemas"]["LoRA_LyCORIS_SDXL_Config"] | components["schemas"]["LoRA_LyCORIS_Flux2_Config"] | components["schemas"]["LoRA_LyCORIS_FLUX_Config"] | components["schemas"]["LoRA_LyCORIS_ZImage_Config"] | components["schemas"]["LoRA_LyCORIS_QwenImage_Config"] | components["schemas"]["LoRA_LyCORIS_Anima_Config"] | components["schemas"]["LoRA_OMI_SDXL_Config"] | components["schemas"]["LoRA_OMI_FLUX_Config"] | components["schemas"]["LoRA_Diffusers_SD1_Config"] | components["schemas"]["LoRA_Diffusers_SD2_Config"] | components["schemas"]["LoRA_Diffusers_SDXL_Config"] | components["schemas"]["LoRA_Diffusers_Flux2_Config"] | components["schemas"]["LoRA_Diffusers_FLUX_Config"] | components["schemas"]["LoRA_Diffusers_ZImage_Config"] | components["schemas"]["ControlLoRA_LyCORIS_FLUX_Config"] | components["schemas"]["T5Encoder_T5Encoder_Config"] | components["schemas"]["T5Encoder_BnBLLMint8_Config"] | components["schemas"]["Qwen3Encoder_Qwen3Encoder_Config"] | components["schemas"]["Qwen3Encoder_Checkpoint_Config"] | components["schemas"]["Qwen3Encoder_GGUF_Config"] | components["schemas"]["QwenVLEncoder_Diffusers_Config"] | components["schemas"]["QwenVLEncoder_Checkpoint_Config"] | components["schemas"]["TI_File_SD1_Config"] | components["schemas"]["TI_File_SD2_Config"] | components["schemas"]["TI_File_SDXL_Config"] | components["schemas"]["TI_Folder_SD1_Config"] | components["schemas"]["TI_Folder_SD2_Config"] | components["schemas"]["TI_Folder_SDXL_Config"] | components["schemas"]["IPAdapter_InvokeAI_SD1_Config"] | components["schemas"]["IPAdapter_InvokeAI_SD2_Config"] | components["schemas"]["IPAdapter_InvokeAI_SDXL_Config"] | components["schemas"]["IPAdapter_Checkpoint_SD1_Config"] | components["schemas"]["IPAdapter_Checkpoint_SD2_Config"] | components["schemas"]["IPAdapter_Checkpoint_SDXL_Config"] | components["schemas"]["IPAdapter_Checkpoint_FLUX_Config"] | components["schemas"]["T2IAdapter_Diffusers_SD1_Config"] | components["schemas"]["T2IAdapter_Diffusers_SDXL_Config"] | components["schemas"]["Spandrel_Checkpoint_Config"] | components["schemas"]["CLIPEmbed_Diffusers_G_Config"] | components["schemas"]["CLIPEmbed_Diffusers_L_Config"] | components["schemas"]["CLIPVision_Diffusers_Config"] | components["schemas"]["SigLIP_Diffusers_Config"] | components["schemas"]["FLUXRedux_Checkpoint_Config"] | components["schemas"]["LlavaOnevision_Diffusers_Config"] | components["schemas"]["TextLLM_Diffusers_Config"] | components["schemas"]["ExternalApiModelConfig"] | components["schemas"]["Unknown_Config"];
- };
- };
- /** @description Bad request */
- 400: {
- headers: {
- [name: string]: unknown;
+ "application/json": components["schemas"]["ModelInstallJob"];
};
- content?: never;
};
- /** @description The model could not be found */
+ /** @description No such job */
404: {
headers: {
[name: string]: unknown;
@@ -34794,27 +38306,29 @@ export interface operations {
};
};
};
- delete_model: {
+ cancel_model_install_job: {
parameters: {
query?: never;
header?: never;
path: {
- /** @description Unique key of model to remove from model registry. */
- key: string;
+ /** @description Model install job ID */
+ id: number;
};
cookie?: never;
};
requestBody?: never;
responses: {
- /** @description Model deleted successfully */
- 204: {
+ /** @description The job was cancelled successfully */
+ 201: {
headers: {
[name: string]: unknown;
};
- content?: never;
+ content: {
+ "application/json": unknown;
+ };
};
- /** @description Model not found */
- 404: {
+ /** @description No such job */
+ 415: {
headers: {
[name: string]: unknown;
};
@@ -34831,67 +38345,29 @@ export interface operations {
};
};
};
- update_model_record: {
+ pause_model_install_job: {
parameters: {
query?: never;
header?: never;
path: {
- /** @description Unique key of model */
- key: string;
+ /** @description Model install job ID */
+ id: number;
};
cookie?: never;
};
- requestBody: {
- content: {
- "application/json": components["schemas"]["ModelRecordChanges"];
- };
- };
+ requestBody?: never;
responses: {
- /** @description The model was updated successfully */
- 200: {
+ /** @description The job was paused successfully */
+ 201: {
headers: {
[name: string]: unknown;
};
content: {
- /**
- * @example {
- * "path": "string",
- * "name": "string",
- * "base": "sd-1",
- * "type": "main",
- * "format": "checkpoint",
- * "config_path": "string",
- * "key": "string",
- * "hash": "string",
- * "file_size": 1,
- * "description": "string",
- * "source": "string",
- * "converted_at": 0,
- * "variant": "normal",
- * "prediction_type": "epsilon",
- * "repo_variant": "fp16",
- * "upcast_attention": false
- * }
- */
- "application/json": components["schemas"]["Main_Diffusers_SD1_Config"] | components["schemas"]["Main_Diffusers_SD2_Config"] | components["schemas"]["Main_Diffusers_SDXL_Config"] | components["schemas"]["Main_Diffusers_SDXLRefiner_Config"] | components["schemas"]["Main_Diffusers_SD3_Config"] | components["schemas"]["Main_Diffusers_FLUX_Config"] | components["schemas"]["Main_Diffusers_Flux2_Config"] | components["schemas"]["Main_Diffusers_CogView4_Config"] | components["schemas"]["Main_Diffusers_QwenImage_Config"] | components["schemas"]["Main_Diffusers_ZImage_Config"] | components["schemas"]["Main_Checkpoint_SD1_Config"] | components["schemas"]["Main_Checkpoint_SD2_Config"] | components["schemas"]["Main_Checkpoint_SDXL_Config"] | components["schemas"]["Main_Checkpoint_SDXLRefiner_Config"] | components["schemas"]["Main_Checkpoint_Flux2_Config"] | components["schemas"]["Main_Checkpoint_FLUX_Config"] | components["schemas"]["Main_Checkpoint_QwenImage_Config"] | components["schemas"]["Main_Checkpoint_ZImage_Config"] | components["schemas"]["Main_Checkpoint_Anima_Config"] | components["schemas"]["Main_BnBNF4_FLUX_Config"] | components["schemas"]["Main_GGUF_Flux2_Config"] | components["schemas"]["Main_GGUF_FLUX_Config"] | components["schemas"]["Main_GGUF_QwenImage_Config"] | components["schemas"]["Main_GGUF_ZImage_Config"] | components["schemas"]["VAE_Checkpoint_SD1_Config"] | components["schemas"]["VAE_Checkpoint_SD2_Config"] | components["schemas"]["VAE_Checkpoint_SDXL_Config"] | components["schemas"]["VAE_Checkpoint_FLUX_Config"] | components["schemas"]["VAE_Checkpoint_Flux2_Config"] | components["schemas"]["VAE_Checkpoint_QwenImage_Config"] | components["schemas"]["VAE_Checkpoint_Anima_Config"] | components["schemas"]["VAE_Diffusers_SD1_Config"] | components["schemas"]["VAE_Diffusers_SDXL_Config"] | components["schemas"]["VAE_Diffusers_Flux2_Config"] | components["schemas"]["ControlNet_Checkpoint_SD1_Config"] | components["schemas"]["ControlNet_Checkpoint_SD2_Config"] | components["schemas"]["ControlNet_Checkpoint_SDXL_Config"] | components["schemas"]["ControlNet_Checkpoint_FLUX_Config"] | components["schemas"]["ControlNet_Checkpoint_ZImage_Config"] | components["schemas"]["ControlNet_Checkpoint_Anima_Config"] | components["schemas"]["ControlNet_Diffusers_SD1_Config"] | components["schemas"]["ControlNet_Diffusers_SD2_Config"] | components["schemas"]["ControlNet_Diffusers_SDXL_Config"] | components["schemas"]["ControlNet_Diffusers_FLUX_Config"] | components["schemas"]["LoRA_LyCORIS_SD1_Config"] | components["schemas"]["LoRA_LyCORIS_SD2_Config"] | components["schemas"]["LoRA_LyCORIS_SDXL_Config"] | components["schemas"]["LoRA_LyCORIS_Flux2_Config"] | components["schemas"]["LoRA_LyCORIS_FLUX_Config"] | components["schemas"]["LoRA_LyCORIS_ZImage_Config"] | components["schemas"]["LoRA_LyCORIS_QwenImage_Config"] | components["schemas"]["LoRA_LyCORIS_Anima_Config"] | components["schemas"]["LoRA_OMI_SDXL_Config"] | components["schemas"]["LoRA_OMI_FLUX_Config"] | components["schemas"]["LoRA_Diffusers_SD1_Config"] | components["schemas"]["LoRA_Diffusers_SD2_Config"] | components["schemas"]["LoRA_Diffusers_SDXL_Config"] | components["schemas"]["LoRA_Diffusers_Flux2_Config"] | components["schemas"]["LoRA_Diffusers_FLUX_Config"] | components["schemas"]["LoRA_Diffusers_ZImage_Config"] | components["schemas"]["ControlLoRA_LyCORIS_FLUX_Config"] | components["schemas"]["T5Encoder_T5Encoder_Config"] | components["schemas"]["T5Encoder_BnBLLMint8_Config"] | components["schemas"]["Qwen3Encoder_Qwen3Encoder_Config"] | components["schemas"]["Qwen3Encoder_Checkpoint_Config"] | components["schemas"]["Qwen3Encoder_GGUF_Config"] | components["schemas"]["QwenVLEncoder_Diffusers_Config"] | components["schemas"]["QwenVLEncoder_Checkpoint_Config"] | components["schemas"]["TI_File_SD1_Config"] | components["schemas"]["TI_File_SD2_Config"] | components["schemas"]["TI_File_SDXL_Config"] | components["schemas"]["TI_Folder_SD1_Config"] | components["schemas"]["TI_Folder_SD2_Config"] | components["schemas"]["TI_Folder_SDXL_Config"] | components["schemas"]["IPAdapter_InvokeAI_SD1_Config"] | components["schemas"]["IPAdapter_InvokeAI_SD2_Config"] | components["schemas"]["IPAdapter_InvokeAI_SDXL_Config"] | components["schemas"]["IPAdapter_Checkpoint_SD1_Config"] | components["schemas"]["IPAdapter_Checkpoint_SD2_Config"] | components["schemas"]["IPAdapter_Checkpoint_SDXL_Config"] | components["schemas"]["IPAdapter_Checkpoint_FLUX_Config"] | components["schemas"]["T2IAdapter_Diffusers_SD1_Config"] | components["schemas"]["T2IAdapter_Diffusers_SDXL_Config"] | components["schemas"]["Spandrel_Checkpoint_Config"] | components["schemas"]["CLIPEmbed_Diffusers_G_Config"] | components["schemas"]["CLIPEmbed_Diffusers_L_Config"] | components["schemas"]["CLIPVision_Diffusers_Config"] | components["schemas"]["SigLIP_Diffusers_Config"] | components["schemas"]["FLUXRedux_Checkpoint_Config"] | components["schemas"]["LlavaOnevision_Diffusers_Config"] | components["schemas"]["TextLLM_Diffusers_Config"] | components["schemas"]["ExternalApiModelConfig"] | components["schemas"]["Unknown_Config"];
- };
- };
- /** @description Bad request */
- 400: {
- headers: {
- [name: string]: unknown;
- };
- content?: never;
- };
- /** @description The model could not be found */
- 404: {
- headers: {
- [name: string]: unknown;
+ "application/json": components["schemas"]["ModelInstallJob"];
};
- content?: never;
};
- /** @description There is already a model corresponding to the new name */
- 409: {
+ /** @description No such job */
+ 415: {
headers: {
[name: string]: unknown;
};
@@ -34908,56 +38384,29 @@ export interface operations {
};
};
};
- reidentify_model: {
+ resume_model_install_job: {
parameters: {
query?: never;
header?: never;
path: {
- /** @description Key of the model to reidentify. */
- key: string;
+ /** @description Model install job ID */
+ id: number;
};
cookie?: never;
};
requestBody?: never;
responses: {
- /** @description The model configuration was retrieved successfully */
- 200: {
+ /** @description The job was resumed successfully */
+ 201: {
headers: {
[name: string]: unknown;
};
content: {
- /**
- * @example {
- * "path": "string",
- * "name": "string",
- * "base": "sd-1",
- * "type": "main",
- * "format": "checkpoint",
- * "config_path": "string",
- * "key": "string",
- * "hash": "string",
- * "file_size": 1,
- * "description": "string",
- * "source": "string",
- * "converted_at": 0,
- * "variant": "normal",
- * "prediction_type": "epsilon",
- * "repo_variant": "fp16",
- * "upcast_attention": false
- * }
- */
- "application/json": components["schemas"]["Main_Diffusers_SD1_Config"] | components["schemas"]["Main_Diffusers_SD2_Config"] | components["schemas"]["Main_Diffusers_SDXL_Config"] | components["schemas"]["Main_Diffusers_SDXLRefiner_Config"] | components["schemas"]["Main_Diffusers_SD3_Config"] | components["schemas"]["Main_Diffusers_FLUX_Config"] | components["schemas"]["Main_Diffusers_Flux2_Config"] | components["schemas"]["Main_Diffusers_CogView4_Config"] | components["schemas"]["Main_Diffusers_QwenImage_Config"] | components["schemas"]["Main_Diffusers_ZImage_Config"] | components["schemas"]["Main_Checkpoint_SD1_Config"] | components["schemas"]["Main_Checkpoint_SD2_Config"] | components["schemas"]["Main_Checkpoint_SDXL_Config"] | components["schemas"]["Main_Checkpoint_SDXLRefiner_Config"] | components["schemas"]["Main_Checkpoint_Flux2_Config"] | components["schemas"]["Main_Checkpoint_FLUX_Config"] | components["schemas"]["Main_Checkpoint_QwenImage_Config"] | components["schemas"]["Main_Checkpoint_ZImage_Config"] | components["schemas"]["Main_Checkpoint_Anima_Config"] | components["schemas"]["Main_BnBNF4_FLUX_Config"] | components["schemas"]["Main_GGUF_Flux2_Config"] | components["schemas"]["Main_GGUF_FLUX_Config"] | components["schemas"]["Main_GGUF_QwenImage_Config"] | components["schemas"]["Main_GGUF_ZImage_Config"] | components["schemas"]["VAE_Checkpoint_SD1_Config"] | components["schemas"]["VAE_Checkpoint_SD2_Config"] | components["schemas"]["VAE_Checkpoint_SDXL_Config"] | components["schemas"]["VAE_Checkpoint_FLUX_Config"] | components["schemas"]["VAE_Checkpoint_Flux2_Config"] | components["schemas"]["VAE_Checkpoint_QwenImage_Config"] | components["schemas"]["VAE_Checkpoint_Anima_Config"] | components["schemas"]["VAE_Diffusers_SD1_Config"] | components["schemas"]["VAE_Diffusers_SDXL_Config"] | components["schemas"]["VAE_Diffusers_Flux2_Config"] | components["schemas"]["ControlNet_Checkpoint_SD1_Config"] | components["schemas"]["ControlNet_Checkpoint_SD2_Config"] | components["schemas"]["ControlNet_Checkpoint_SDXL_Config"] | components["schemas"]["ControlNet_Checkpoint_FLUX_Config"] | components["schemas"]["ControlNet_Checkpoint_ZImage_Config"] | components["schemas"]["ControlNet_Checkpoint_Anima_Config"] | components["schemas"]["ControlNet_Diffusers_SD1_Config"] | components["schemas"]["ControlNet_Diffusers_SD2_Config"] | components["schemas"]["ControlNet_Diffusers_SDXL_Config"] | components["schemas"]["ControlNet_Diffusers_FLUX_Config"] | components["schemas"]["LoRA_LyCORIS_SD1_Config"] | components["schemas"]["LoRA_LyCORIS_SD2_Config"] | components["schemas"]["LoRA_LyCORIS_SDXL_Config"] | components["schemas"]["LoRA_LyCORIS_Flux2_Config"] | components["schemas"]["LoRA_LyCORIS_FLUX_Config"] | components["schemas"]["LoRA_LyCORIS_ZImage_Config"] | components["schemas"]["LoRA_LyCORIS_QwenImage_Config"] | components["schemas"]["LoRA_LyCORIS_Anima_Config"] | components["schemas"]["LoRA_OMI_SDXL_Config"] | components["schemas"]["LoRA_OMI_FLUX_Config"] | components["schemas"]["LoRA_Diffusers_SD1_Config"] | components["schemas"]["LoRA_Diffusers_SD2_Config"] | components["schemas"]["LoRA_Diffusers_SDXL_Config"] | components["schemas"]["LoRA_Diffusers_Flux2_Config"] | components["schemas"]["LoRA_Diffusers_FLUX_Config"] | components["schemas"]["LoRA_Diffusers_ZImage_Config"] | components["schemas"]["ControlLoRA_LyCORIS_FLUX_Config"] | components["schemas"]["T5Encoder_T5Encoder_Config"] | components["schemas"]["T5Encoder_BnBLLMint8_Config"] | components["schemas"]["Qwen3Encoder_Qwen3Encoder_Config"] | components["schemas"]["Qwen3Encoder_Checkpoint_Config"] | components["schemas"]["Qwen3Encoder_GGUF_Config"] | components["schemas"]["QwenVLEncoder_Diffusers_Config"] | components["schemas"]["QwenVLEncoder_Checkpoint_Config"] | components["schemas"]["TI_File_SD1_Config"] | components["schemas"]["TI_File_SD2_Config"] | components["schemas"]["TI_File_SDXL_Config"] | components["schemas"]["TI_Folder_SD1_Config"] | components["schemas"]["TI_Folder_SD2_Config"] | components["schemas"]["TI_Folder_SDXL_Config"] | components["schemas"]["IPAdapter_InvokeAI_SD1_Config"] | components["schemas"]["IPAdapter_InvokeAI_SD2_Config"] | components["schemas"]["IPAdapter_InvokeAI_SDXL_Config"] | components["schemas"]["IPAdapter_Checkpoint_SD1_Config"] | components["schemas"]["IPAdapter_Checkpoint_SD2_Config"] | components["schemas"]["IPAdapter_Checkpoint_SDXL_Config"] | components["schemas"]["IPAdapter_Checkpoint_FLUX_Config"] | components["schemas"]["T2IAdapter_Diffusers_SD1_Config"] | components["schemas"]["T2IAdapter_Diffusers_SDXL_Config"] | components["schemas"]["Spandrel_Checkpoint_Config"] | components["schemas"]["CLIPEmbed_Diffusers_G_Config"] | components["schemas"]["CLIPEmbed_Diffusers_L_Config"] | components["schemas"]["CLIPVision_Diffusers_Config"] | components["schemas"]["SigLIP_Diffusers_Config"] | components["schemas"]["FLUXRedux_Checkpoint_Config"] | components["schemas"]["LlavaOnevision_Diffusers_Config"] | components["schemas"]["TextLLM_Diffusers_Config"] | components["schemas"]["ExternalApiModelConfig"] | components["schemas"]["Unknown_Config"];
- };
- };
- /** @description Bad request */
- 400: {
- headers: {
- [name: string]: unknown;
+ "application/json": components["schemas"]["ModelInstallJob"];
};
- content?: never;
};
- /** @description The model could not be found */
- 404: {
+ /** @description No such job */
+ 415: {
headers: {
[name: string]: unknown;
};
@@ -34974,29 +38423,29 @@ export interface operations {
};
};
};
- scan_for_models: {
+ restart_failed_model_install_job: {
parameters: {
- query?: {
- /** @description Directory path to search for models */
- scan_path?: string;
- };
+ query?: never;
header?: never;
- path?: never;
+ path: {
+ /** @description Model install job ID */
+ id: number;
+ };
cookie?: never;
};
requestBody?: never;
responses: {
- /** @description Directory scanned successfully */
- 200: {
+ /** @description Failed files restarted successfully */
+ 201: {
headers: {
[name: string]: unknown;
};
content: {
- "application/json": components["schemas"]["FoundModel"][];
+ "application/json": components["schemas"]["ModelInstallJob"];
};
};
- /** @description Invalid directory path */
- 400: {
+ /** @description No such job */
+ 415: {
headers: {
[name: string]: unknown;
};
@@ -35013,29 +38462,33 @@ export interface operations {
};
};
};
- get_hugging_face_models: {
+ restart_model_install_file: {
parameters: {
- query?: {
- /** @description Hugging face repo to search for models */
- hugging_face_repo?: string;
- };
+ query?: never;
header?: never;
- path?: never;
+ path: {
+ /** @description Model install job ID */
+ id: number;
+ };
cookie?: never;
};
- requestBody?: never;
+ requestBody: {
+ content: {
+ "application/json": string;
+ };
+ };
responses: {
- /** @description Hugging Face repo scanned successfully */
- 200: {
+ /** @description File restarted successfully */
+ 201: {
headers: {
[name: string]: unknown;
};
content: {
- "application/json": components["schemas"]["HuggingFaceModels"];
+ "application/json": components["schemas"]["ModelInstallJob"];
};
};
- /** @description Invalid hugging face repo */
- 400: {
+ /** @description No such job */
+ 415: {
headers: {
[name: string]: unknown;
};
@@ -35052,25 +38505,45 @@ export interface operations {
};
};
};
- get_model_image: {
+ convert_model: {
parameters: {
query?: never;
header?: never;
path: {
- /** @description The name of model image file to get */
+ /** @description Unique key of the safetensors main model to convert to diffusers format. */
key: string;
};
cookie?: never;
};
requestBody?: never;
responses: {
- /** @description The model image was fetched successfully */
+ /** @description Model converted successfully */
200: {
headers: {
[name: string]: unknown;
};
content: {
- "application/json": unknown;
+ /**
+ * @example {
+ * "path": "string",
+ * "name": "string",
+ * "base": "sd-1",
+ * "type": "main",
+ * "format": "checkpoint",
+ * "config_path": "string",
+ * "key": "string",
+ * "hash": "string",
+ * "file_size": 1,
+ * "description": "string",
+ * "source": "string",
+ * "converted_at": 0,
+ * "variant": "normal",
+ * "prediction_type": "epsilon",
+ * "repo_variant": "fp16",
+ * "upcast_attention": false
+ * }
+ */
+ "application/json": components["schemas"]["Main_Diffusers_SD1_Config"] | components["schemas"]["Main_Diffusers_SD2_Config"] | components["schemas"]["Main_Diffusers_SDXL_Config"] | components["schemas"]["Main_Diffusers_SDXLRefiner_Config"] | components["schemas"]["Main_Diffusers_SD3_Config"] | components["schemas"]["Main_Diffusers_FLUX_Config"] | components["schemas"]["Main_Diffusers_Flux2_Config"] | components["schemas"]["Main_Diffusers_CogView4_Config"] | components["schemas"]["Main_Diffusers_QwenImage_Config"] | components["schemas"]["Main_Diffusers_Wan_Config"] | components["schemas"]["Main_Diffusers_ZImage_Config"] | components["schemas"]["Main_Checkpoint_SD1_Config"] | components["schemas"]["Main_Checkpoint_SD2_Config"] | components["schemas"]["Main_Checkpoint_SDXL_Config"] | components["schemas"]["Main_Checkpoint_SDXLRefiner_Config"] | components["schemas"]["Main_Checkpoint_Flux2_Config"] | components["schemas"]["Main_Checkpoint_FLUX_Config"] | components["schemas"]["Main_Checkpoint_QwenImage_Config"] | components["schemas"]["Main_Checkpoint_ZImage_Config"] | components["schemas"]["Main_Checkpoint_Anima_Config"] | components["schemas"]["Main_BnBNF4_FLUX_Config"] | components["schemas"]["Main_GGUF_Flux2_Config"] | components["schemas"]["Main_GGUF_FLUX_Config"] | components["schemas"]["Main_GGUF_QwenImage_Config"] | components["schemas"]["Main_GGUF_Wan_Config"] | components["schemas"]["Main_GGUF_ZImage_Config"] | components["schemas"]["VAE_Checkpoint_SD1_Config"] | components["schemas"]["VAE_Checkpoint_SD2_Config"] | components["schemas"]["VAE_Checkpoint_SDXL_Config"] | components["schemas"]["VAE_Checkpoint_FLUX_Config"] | components["schemas"]["VAE_Checkpoint_Flux2_Config"] | components["schemas"]["VAE_Checkpoint_Wan_Config"] | components["schemas"]["VAE_Checkpoint_QwenImage_Config"] | components["schemas"]["VAE_Checkpoint_Anima_Config"] | components["schemas"]["VAE_Diffusers_SD1_Config"] | components["schemas"]["VAE_Diffusers_SDXL_Config"] | components["schemas"]["VAE_Diffusers_Flux2_Config"] | components["schemas"]["VAE_Diffusers_Wan_Config"] | components["schemas"]["ControlNet_Checkpoint_SD1_Config"] | components["schemas"]["ControlNet_Checkpoint_SD2_Config"] | components["schemas"]["ControlNet_Checkpoint_SDXL_Config"] | components["schemas"]["ControlNet_Checkpoint_FLUX_Config"] | components["schemas"]["ControlNet_Checkpoint_ZImage_Config"] | components["schemas"]["ControlNet_Checkpoint_Anima_Config"] | components["schemas"]["ControlNet_Diffusers_SD1_Config"] | components["schemas"]["ControlNet_Diffusers_SD2_Config"] | components["schemas"]["ControlNet_Diffusers_SDXL_Config"] | components["schemas"]["ControlNet_Diffusers_FLUX_Config"] | components["schemas"]["LoRA_LyCORIS_SD1_Config"] | components["schemas"]["LoRA_LyCORIS_SD2_Config"] | components["schemas"]["LoRA_LyCORIS_SDXL_Config"] | components["schemas"]["LoRA_LyCORIS_Flux2_Config"] | components["schemas"]["LoRA_LyCORIS_FLUX_Config"] | components["schemas"]["LoRA_LyCORIS_ZImage_Config"] | components["schemas"]["LoRA_LyCORIS_QwenImage_Config"] | components["schemas"]["LoRA_LyCORIS_Wan_Config"] | components["schemas"]["LoRA_LyCORIS_Anima_Config"] | components["schemas"]["LoRA_OMI_SDXL_Config"] | components["schemas"]["LoRA_OMI_FLUX_Config"] | components["schemas"]["LoRA_Diffusers_SD1_Config"] | components["schemas"]["LoRA_Diffusers_SD2_Config"] | components["schemas"]["LoRA_Diffusers_SDXL_Config"] | components["schemas"]["LoRA_Diffusers_Flux2_Config"] | components["schemas"]["LoRA_Diffusers_FLUX_Config"] | components["schemas"]["LoRA_Diffusers_ZImage_Config"] | components["schemas"]["ControlLoRA_LyCORIS_FLUX_Config"] | components["schemas"]["T5Encoder_T5Encoder_Config"] | components["schemas"]["T5Encoder_BnBLLMint8_Config"] | components["schemas"]["Qwen3Encoder_Qwen3Encoder_Config"] | components["schemas"]["Qwen3Encoder_Checkpoint_Config"] | components["schemas"]["Qwen3Encoder_GGUF_Config"] | components["schemas"]["QwenVLEncoder_Diffusers_Config"] | components["schemas"]["QwenVLEncoder_Checkpoint_Config"] | components["schemas"]["WanT5Encoder_WanT5Encoder_Config"] | components["schemas"]["TI_File_SD1_Config"] | components["schemas"]["TI_File_SD2_Config"] | components["schemas"]["TI_File_SDXL_Config"] | components["schemas"]["TI_Folder_SD1_Config"] | components["schemas"]["TI_Folder_SD2_Config"] | components["schemas"]["TI_Folder_SDXL_Config"] | components["schemas"]["IPAdapter_InvokeAI_SD1_Config"] | components["schemas"]["IPAdapter_InvokeAI_SD2_Config"] | components["schemas"]["IPAdapter_InvokeAI_SDXL_Config"] | components["schemas"]["IPAdapter_Checkpoint_SD1_Config"] | components["schemas"]["IPAdapter_Checkpoint_SD2_Config"] | components["schemas"]["IPAdapter_Checkpoint_SDXL_Config"] | components["schemas"]["IPAdapter_Checkpoint_FLUX_Config"] | components["schemas"]["T2IAdapter_Diffusers_SD1_Config"] | components["schemas"]["T2IAdapter_Diffusers_SDXL_Config"] | components["schemas"]["Spandrel_Checkpoint_Config"] | components["schemas"]["CLIPEmbed_Diffusers_G_Config"] | components["schemas"]["CLIPEmbed_Diffusers_L_Config"] | components["schemas"]["CLIPVision_Diffusers_Config"] | components["schemas"]["SigLIP_Diffusers_Config"] | components["schemas"]["FLUXRedux_Checkpoint_Config"] | components["schemas"]["LlavaOnevision_Diffusers_Config"] | components["schemas"]["TextLLM_Diffusers_Config"] | components["schemas"]["ExternalApiModelConfig"] | components["schemas"]["Unknown_Config"];
};
};
/** @description Bad request */
@@ -35080,13 +38553,20 @@ export interface operations {
};
content?: never;
};
- /** @description The model image could not be found */
+ /** @description Model not found */
404: {
headers: {
[name: string]: unknown;
};
content?: never;
};
+ /** @description There is already a model registered at this location */
+ 409: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content?: never;
+ };
/** @description Validation Error */
422: {
headers: {
@@ -35098,60 +38578,56 @@ export interface operations {
};
};
};
- delete_model_image: {
+ get_starter_models: {
parameters: {
query?: never;
header?: never;
- path: {
- /** @description Unique key of model image to remove from model_images directory. */
- key: string;
- };
+ path?: never;
cookie?: never;
};
requestBody?: never;
responses: {
- /** @description Model image deleted successfully */
- 204: {
+ /** @description Successful Response */
+ 200: {
headers: {
[name: string]: unknown;
};
- content?: never;
- };
- /** @description Model image not found */
- 404: {
- headers: {
- [name: string]: unknown;
+ content: {
+ "application/json": components["schemas"]["StarterModelResponse"];
};
- content?: never;
};
- /** @description Validation Error */
- 422: {
+ };
+ };
+ get_stats: {
+ parameters: {
+ query?: never;
+ header?: never;
+ path?: never;
+ cookie?: never;
+ };
+ requestBody?: never;
+ responses: {
+ /** @description Successful Response */
+ 200: {
headers: {
[name: string]: unknown;
};
content: {
- "application/json": components["schemas"]["HTTPValidationError"];
+ "application/json": components["schemas"]["CacheStats"] | null;
};
};
};
};
- update_model_image: {
+ empty_model_cache: {
parameters: {
query?: never;
header?: never;
- path: {
- /** @description Unique key of model */
- key: string;
- };
+ path?: never;
cookie?: never;
};
- requestBody: {
- content: {
- "multipart/form-data": components["schemas"]["Body_update_model_image"];
- };
- };
+ requestBody?: never;
responses: {
- /** @description The model image was updated successfully */
+ /** @description Successful Response */
200: {
headers: {
[name: string]: unknown;
@@ -35160,25 +38636,29 @@ export interface operations {
"application/json": unknown;
};
};
- /** @description Bad request */
- 400: {
- headers: {
- [name: string]: unknown;
- };
- content?: never;
- };
- /** @description Validation Error */
- 422: {
+ };
+ };
+ get_hf_login_status: {
+ parameters: {
+ query?: never;
+ header?: never;
+ path?: never;
+ cookie?: never;
+ };
+ requestBody?: never;
+ responses: {
+ /** @description Successful Response */
+ 200: {
headers: {
[name: string]: unknown;
};
content: {
- "application/json": components["schemas"]["HTTPValidationError"];
+ "application/json": components["schemas"]["HFTokenStatus"];
};
};
};
};
- bulk_delete_models: {
+ do_hf_login: {
parameters: {
query?: never;
header?: never;
@@ -35187,17 +38667,17 @@ export interface operations {
};
requestBody: {
content: {
- "application/json": components["schemas"]["BulkDeleteModelsRequest"];
+ "application/json": components["schemas"]["Body_do_hf_login"];
};
};
responses: {
- /** @description Models deleted (possibly with some failures) */
+ /** @description Successful Response */
200: {
headers: {
[name: string]: unknown;
};
content: {
- "application/json": components["schemas"]["BulkDeleteModelsResponse"];
+ "application/json": components["schemas"]["HFTokenStatus"];
};
};
/** @description Validation Error */
@@ -35211,40 +38691,27 @@ export interface operations {
};
};
};
- bulk_reidentify_models: {
+ reset_hf_token: {
parameters: {
query?: never;
header?: never;
path?: never;
cookie?: never;
};
- requestBody: {
- content: {
- "application/json": components["schemas"]["BulkReidentifyModelsRequest"];
- };
- };
+ requestBody?: never;
responses: {
- /** @description Models reidentified (possibly with some failures) */
+ /** @description Successful Response */
200: {
headers: {
[name: string]: unknown;
};
content: {
- "application/json": components["schemas"]["BulkReidentifyModelsResponse"];
- };
- };
- /** @description Validation Error */
- 422: {
- headers: {
- [name: string]: unknown;
- };
- content: {
- "application/json": components["schemas"]["HTTPValidationError"];
+ "application/json": components["schemas"]["HFTokenStatus"];
};
};
};
};
- list_model_installs: {
+ get_orphaned_models: {
parameters: {
query?: never;
header?: never;
@@ -35259,53 +38726,32 @@ export interface operations {
[name: string]: unknown;
};
content: {
- "application/json": components["schemas"]["ModelInstallJob"][];
+ "application/json": components["schemas"]["OrphanedModelInfo"][];
};
};
};
};
- install_model: {
+ delete_orphaned_models: {
parameters: {
- query: {
- /** @description Model source to install, can be a local path, repo_id, or remote URL */
- source: string;
- /** @description Whether or not to install a local model in place */
- inplace?: boolean | null;
- /** @description access token for the remote resource */
- access_token?: string | null;
- };
+ query?: never;
header?: never;
path?: never;
cookie?: never;
};
requestBody: {
content: {
- "application/json": components["schemas"]["ModelRecordChanges"];
+ "application/json": components["schemas"]["DeleteOrphanedModelsRequest"];
};
};
responses: {
- /** @description The model imported successfully */
- 201: {
+ /** @description Successful Response */
+ 200: {
headers: {
[name: string]: unknown;
};
content: {
- "application/json": components["schemas"]["ModelInstallJob"];
- };
- };
- /** @description There is already a model corresponding to this path or repo_id */
- 409: {
- headers: {
- [name: string]: unknown;
- };
- content?: never;
- };
- /** @description Unrecognized file/folder format */
- 415: {
- headers: {
- [name: string]: unknown;
+ "application/json": components["schemas"]["DeleteOrphanedModelsResponse"];
};
- content?: never;
};
/** @description Validation Error */
422: {
@@ -35316,16 +38762,29 @@ export interface operations {
"application/json": components["schemas"]["HTTPValidationError"];
};
};
- /** @description The model appeared to import successfully, but could not be found in the model manager */
- 424: {
+ };
+ };
+ list_downloads: {
+ parameters: {
+ query?: never;
+ header?: never;
+ path?: never;
+ cookie?: never;
+ };
+ requestBody?: never;
+ responses: {
+ /** @description Successful Response */
+ 200: {
headers: {
[name: string]: unknown;
};
- content?: never;
+ content: {
+ "application/json": components["schemas"]["DownloadJob"][];
+ };
};
};
};
- prune_model_install_jobs: {
+ prune_downloads: {
parameters: {
query?: never;
header?: never;
@@ -35343,7 +38802,7 @@ export interface operations {
"application/json": unknown;
};
};
- /** @description All completed and errored jobs have been pruned */
+ /** @description All completed jobs have been pruned */
204: {
headers: {
[name: string]: unknown;
@@ -35359,40 +38818,27 @@ export interface operations {
};
};
};
- install_hugging_face_model: {
+ download: {
parameters: {
- query: {
- /** @description HuggingFace repo_id to install */
- source: string;
- };
+ query?: never;
header?: never;
path?: never;
cookie?: never;
};
- requestBody?: never;
+ requestBody: {
+ content: {
+ "application/json": components["schemas"]["Body_download"];
+ };
+ };
responses: {
- /** @description The model is being installed */
- 201: {
+ /** @description Successful Response */
+ 200: {
headers: {
[name: string]: unknown;
};
content: {
- "text/html": string;
- };
- };
- /** @description Bad request */
- 400: {
- headers: {
- [name: string]: unknown;
- };
- content?: never;
- };
- /** @description There is already a model corresponding to this path or repo_id */
- 409: {
- headers: {
- [name: string]: unknown;
+ "application/json": components["schemas"]["DownloadJob"];
};
- content?: never;
};
/** @description Validation Error */
422: {
@@ -35405,12 +38851,12 @@ export interface operations {
};
};
};
- get_model_install_job: {
+ get_download_job: {
parameters: {
query?: never;
header?: never;
path: {
- /** @description Model install id */
+ /** @description ID of the download job to fetch. */
id: number;
};
cookie?: never;
@@ -35423,10 +38869,10 @@ export interface operations {
[name: string]: unknown;
};
content: {
- "application/json": components["schemas"]["ModelInstallJob"];
+ "application/json": components["schemas"]["DownloadJob"];
};
};
- /** @description No such job */
+ /** @description The requested download JobID could not be found */
404: {
headers: {
[name: string]: unknown;
@@ -35444,20 +38890,20 @@ export interface operations {
};
};
};
- cancel_model_install_job: {
+ cancel_download_job: {
parameters: {
query?: never;
header?: never;
path: {
- /** @description Model install job ID */
+ /** @description ID of the download job to cancel. */
id: number;
};
cookie?: never;
};
requestBody?: never;
responses: {
- /** @description The job was cancelled successfully */
- 201: {
+ /** @description Successful Response */
+ 200: {
headers: {
[name: string]: unknown;
};
@@ -35465,8 +38911,15 @@ export interface operations {
"application/json": unknown;
};
};
- /** @description No such job */
- 415: {
+ /** @description Job has been cancelled */
+ 204: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content?: never;
+ };
+ /** @description The requested download JobID could not be found */
+ 404: {
headers: {
[name: string]: unknown;
};
@@ -35483,149 +38936,127 @@ export interface operations {
};
};
};
- pause_model_install_job: {
+ cancel_all_download_jobs: {
parameters: {
query?: never;
header?: never;
- path: {
- /** @description Model install job ID */
- id: number;
- };
+ path?: never;
cookie?: never;
};
requestBody?: never;
responses: {
- /** @description The job was paused successfully */
- 201: {
+ /** @description Successful Response */
+ 200: {
headers: {
[name: string]: unknown;
};
content: {
- "application/json": components["schemas"]["ModelInstallJob"];
+ "application/json": unknown;
};
};
- /** @description No such job */
- 415: {
+ /** @description Download jobs have been cancelled */
+ 204: {
headers: {
[name: string]: unknown;
};
content?: never;
};
- /** @description Validation Error */
- 422: {
- headers: {
- [name: string]: unknown;
- };
- content: {
- "application/json": components["schemas"]["HTTPValidationError"];
- };
- };
};
};
- resume_model_install_job: {
+ start_image_move: {
parameters: {
query?: never;
header?: never;
- path: {
- /** @description Model install job ID */
- id: number;
- };
+ path?: never;
cookie?: never;
};
requestBody?: never;
responses: {
- /** @description The job was resumed successfully */
- 201: {
- headers: {
- [name: string]: unknown;
- };
- content: {
- "application/json": components["schemas"]["ModelInstallJob"];
- };
- };
- /** @description No such job */
- 415: {
- headers: {
- [name: string]: unknown;
- };
- content?: never;
- };
- /** @description Validation Error */
- 422: {
+ /** @description Successful Response */
+ 202: {
headers: {
[name: string]: unknown;
};
content: {
- "application/json": components["schemas"]["HTTPValidationError"];
+ "application/json": components["schemas"]["ImageMoveStatusResponse"];
};
};
};
};
- restart_failed_model_install_job: {
+ start_image_move_recovery: {
parameters: {
query?: never;
header?: never;
- path: {
- /** @description Model install job ID */
- id: number;
- };
+ path?: never;
cookie?: never;
};
requestBody?: never;
responses: {
- /** @description Failed files restarted successfully */
- 201: {
+ /** @description Successful Response */
+ 202: {
headers: {
[name: string]: unknown;
};
content: {
- "application/json": components["schemas"]["ModelInstallJob"];
- };
- };
- /** @description No such job */
- 415: {
- headers: {
- [name: string]: unknown;
+ "application/json": components["schemas"]["ImageMoveStatusResponse"];
};
- content?: never;
};
- /** @description Validation Error */
- 422: {
+ };
+ };
+ get_image_move_status: {
+ parameters: {
+ query?: never;
+ header?: never;
+ path?: never;
+ cookie?: never;
+ };
+ requestBody?: never;
+ responses: {
+ /** @description Successful Response */
+ 200: {
headers: {
[name: string]: unknown;
};
content: {
- "application/json": components["schemas"]["HTTPValidationError"];
+ "application/json": components["schemas"]["ImageMoveStatusResponse"];
};
};
};
};
- restart_model_install_file: {
+ upload_image: {
parameters: {
- query?: never;
- header?: never;
- path: {
- /** @description Model install job ID */
- id: number;
+ query: {
+ /** @description The category of the image */
+ image_category: components["schemas"]["ImageCategory"];
+ /** @description Whether this is an intermediate image */
+ is_intermediate: boolean;
+ /** @description The board to add this image to, if any */
+ board_id?: string | null;
+ /** @description The session ID associated with this upload, if any */
+ session_id?: string | null;
+ /** @description Whether to crop the image */
+ crop_visible?: boolean | null;
};
+ header?: never;
+ path?: never;
cookie?: never;
};
requestBody: {
content: {
- "application/json": string;
+ "multipart/form-data": components["schemas"]["Body_upload_image"];
};
};
responses: {
- /** @description File restarted successfully */
+ /** @description The image was uploaded successfully */
201: {
headers: {
[name: string]: unknown;
};
content: {
- "application/json": components["schemas"]["ModelInstallJob"];
+ "application/json": components["schemas"]["ImageDTO"];
};
};
- /** @description No such job */
+ /** @description Image upload failed */
415: {
headers: {
[name: string]: unknown;
@@ -35643,67 +39074,42 @@ export interface operations {
};
};
};
- convert_model: {
+ list_image_dtos: {
parameters: {
- query?: never;
- header?: never;
- path: {
- /** @description Unique key of the safetensors main model to convert to diffusers format. */
- key: string;
+ query?: {
+ /** @description The origin of images to list. */
+ image_origin?: components["schemas"]["ResourceOrigin"] | null;
+ /** @description The categories of image to include. */
+ categories?: components["schemas"]["ImageCategory"][] | null;
+ /** @description Whether to list intermediate images. */
+ is_intermediate?: boolean | null;
+ /** @description The board id to filter by. Use 'none' to find images without a board. */
+ board_id?: string | null;
+ /** @description The page offset */
+ offset?: number;
+ /** @description The number of images per page */
+ limit?: number;
+ /** @description The order of sort */
+ order_dir?: components["schemas"]["SQLiteDirection"];
+ /** @description Whether to sort by starred images first */
+ starred_first?: boolean;
+ /** @description The term to search for */
+ search_term?: string | null;
};
+ header?: never;
+ path?: never;
cookie?: never;
};
requestBody?: never;
responses: {
- /** @description Model converted successfully */
+ /** @description Successful Response */
200: {
headers: {
[name: string]: unknown;
};
content: {
- /**
- * @example {
- * "path": "string",
- * "name": "string",
- * "base": "sd-1",
- * "type": "main",
- * "format": "checkpoint",
- * "config_path": "string",
- * "key": "string",
- * "hash": "string",
- * "file_size": 1,
- * "description": "string",
- * "source": "string",
- * "converted_at": 0,
- * "variant": "normal",
- * "prediction_type": "epsilon",
- * "repo_variant": "fp16",
- * "upcast_attention": false
- * }
- */
- "application/json": components["schemas"]["Main_Diffusers_SD1_Config"] | components["schemas"]["Main_Diffusers_SD2_Config"] | components["schemas"]["Main_Diffusers_SDXL_Config"] | components["schemas"]["Main_Diffusers_SDXLRefiner_Config"] | components["schemas"]["Main_Diffusers_SD3_Config"] | components["schemas"]["Main_Diffusers_FLUX_Config"] | components["schemas"]["Main_Diffusers_Flux2_Config"] | components["schemas"]["Main_Diffusers_CogView4_Config"] | components["schemas"]["Main_Diffusers_QwenImage_Config"] | components["schemas"]["Main_Diffusers_ZImage_Config"] | components["schemas"]["Main_Checkpoint_SD1_Config"] | components["schemas"]["Main_Checkpoint_SD2_Config"] | components["schemas"]["Main_Checkpoint_SDXL_Config"] | components["schemas"]["Main_Checkpoint_SDXLRefiner_Config"] | components["schemas"]["Main_Checkpoint_Flux2_Config"] | components["schemas"]["Main_Checkpoint_FLUX_Config"] | components["schemas"]["Main_Checkpoint_QwenImage_Config"] | components["schemas"]["Main_Checkpoint_ZImage_Config"] | components["schemas"]["Main_Checkpoint_Anima_Config"] | components["schemas"]["Main_BnBNF4_FLUX_Config"] | components["schemas"]["Main_GGUF_Flux2_Config"] | components["schemas"]["Main_GGUF_FLUX_Config"] | components["schemas"]["Main_GGUF_QwenImage_Config"] | components["schemas"]["Main_GGUF_ZImage_Config"] | components["schemas"]["VAE_Checkpoint_SD1_Config"] | components["schemas"]["VAE_Checkpoint_SD2_Config"] | components["schemas"]["VAE_Checkpoint_SDXL_Config"] | components["schemas"]["VAE_Checkpoint_FLUX_Config"] | components["schemas"]["VAE_Checkpoint_Flux2_Config"] | components["schemas"]["VAE_Checkpoint_QwenImage_Config"] | components["schemas"]["VAE_Checkpoint_Anima_Config"] | components["schemas"]["VAE_Diffusers_SD1_Config"] | components["schemas"]["VAE_Diffusers_SDXL_Config"] | components["schemas"]["VAE_Diffusers_Flux2_Config"] | components["schemas"]["ControlNet_Checkpoint_SD1_Config"] | components["schemas"]["ControlNet_Checkpoint_SD2_Config"] | components["schemas"]["ControlNet_Checkpoint_SDXL_Config"] | components["schemas"]["ControlNet_Checkpoint_FLUX_Config"] | components["schemas"]["ControlNet_Checkpoint_ZImage_Config"] | components["schemas"]["ControlNet_Checkpoint_Anima_Config"] | components["schemas"]["ControlNet_Diffusers_SD1_Config"] | components["schemas"]["ControlNet_Diffusers_SD2_Config"] | components["schemas"]["ControlNet_Diffusers_SDXL_Config"] | components["schemas"]["ControlNet_Diffusers_FLUX_Config"] | components["schemas"]["LoRA_LyCORIS_SD1_Config"] | components["schemas"]["LoRA_LyCORIS_SD2_Config"] | components["schemas"]["LoRA_LyCORIS_SDXL_Config"] | components["schemas"]["LoRA_LyCORIS_Flux2_Config"] | components["schemas"]["LoRA_LyCORIS_FLUX_Config"] | components["schemas"]["LoRA_LyCORIS_ZImage_Config"] | components["schemas"]["LoRA_LyCORIS_QwenImage_Config"] | components["schemas"]["LoRA_LyCORIS_Anima_Config"] | components["schemas"]["LoRA_OMI_SDXL_Config"] | components["schemas"]["LoRA_OMI_FLUX_Config"] | components["schemas"]["LoRA_Diffusers_SD1_Config"] | components["schemas"]["LoRA_Diffusers_SD2_Config"] | components["schemas"]["LoRA_Diffusers_SDXL_Config"] | components["schemas"]["LoRA_Diffusers_Flux2_Config"] | components["schemas"]["LoRA_Diffusers_FLUX_Config"] | components["schemas"]["LoRA_Diffusers_ZImage_Config"] | components["schemas"]["ControlLoRA_LyCORIS_FLUX_Config"] | components["schemas"]["T5Encoder_T5Encoder_Config"] | components["schemas"]["T5Encoder_BnBLLMint8_Config"] | components["schemas"]["Qwen3Encoder_Qwen3Encoder_Config"] | components["schemas"]["Qwen3Encoder_Checkpoint_Config"] | components["schemas"]["Qwen3Encoder_GGUF_Config"] | components["schemas"]["QwenVLEncoder_Diffusers_Config"] | components["schemas"]["QwenVLEncoder_Checkpoint_Config"] | components["schemas"]["TI_File_SD1_Config"] | components["schemas"]["TI_File_SD2_Config"] | components["schemas"]["TI_File_SDXL_Config"] | components["schemas"]["TI_Folder_SD1_Config"] | components["schemas"]["TI_Folder_SD2_Config"] | components["schemas"]["TI_Folder_SDXL_Config"] | components["schemas"]["IPAdapter_InvokeAI_SD1_Config"] | components["schemas"]["IPAdapter_InvokeAI_SD2_Config"] | components["schemas"]["IPAdapter_InvokeAI_SDXL_Config"] | components["schemas"]["IPAdapter_Checkpoint_SD1_Config"] | components["schemas"]["IPAdapter_Checkpoint_SD2_Config"] | components["schemas"]["IPAdapter_Checkpoint_SDXL_Config"] | components["schemas"]["IPAdapter_Checkpoint_FLUX_Config"] | components["schemas"]["T2IAdapter_Diffusers_SD1_Config"] | components["schemas"]["T2IAdapter_Diffusers_SDXL_Config"] | components["schemas"]["Spandrel_Checkpoint_Config"] | components["schemas"]["CLIPEmbed_Diffusers_G_Config"] | components["schemas"]["CLIPEmbed_Diffusers_L_Config"] | components["schemas"]["CLIPVision_Diffusers_Config"] | components["schemas"]["SigLIP_Diffusers_Config"] | components["schemas"]["FLUXRedux_Checkpoint_Config"] | components["schemas"]["LlavaOnevision_Diffusers_Config"] | components["schemas"]["TextLLM_Diffusers_Config"] | components["schemas"]["ExternalApiModelConfig"] | components["schemas"]["Unknown_Config"];
- };
- };
- /** @description Bad request */
- 400: {
- headers: {
- [name: string]: unknown;
- };
- content?: never;
- };
- /** @description Model not found */
- 404: {
- headers: {
- [name: string]: unknown;
- };
- content?: never;
- };
- /** @description There is already a model registered at this location */
- 409: {
- headers: {
- [name: string]: unknown;
+ "application/json": components["schemas"]["OffsetPaginatedResults_ImageDTO_"];
};
- content?: never;
};
/** @description Validation Error */
422: {
@@ -35716,14 +39122,18 @@ export interface operations {
};
};
};
- get_starter_models: {
+ create_image_upload_entry: {
parameters: {
query?: never;
header?: never;
path?: never;
cookie?: never;
};
- requestBody?: never;
+ requestBody: {
+ content: {
+ "application/json": components["schemas"]["Body_create_image_upload_entry"];
+ };
+ };
responses: {
/** @description Successful Response */
200: {
@@ -35731,36 +39141,28 @@ export interface operations {
[name: string]: unknown;
};
content: {
- "application/json": components["schemas"]["StarterModelResponse"];
+ "application/json": components["schemas"]["ImageUploadEntry"];
};
};
- };
- };
- get_stats: {
- parameters: {
- query?: never;
- header?: never;
- path?: never;
- cookie?: never;
- };
- requestBody?: never;
- responses: {
- /** @description Successful Response */
- 200: {
+ /** @description Validation Error */
+ 422: {
headers: {
[name: string]: unknown;
};
content: {
- "application/json": components["schemas"]["CacheStats"] | null;
+ "application/json": components["schemas"]["HTTPValidationError"];
};
};
};
};
- empty_model_cache: {
+ get_image_dto: {
parameters: {
query?: never;
header?: never;
- path?: never;
+ path: {
+ /** @description The name of image to get */
+ image_name: string;
+ };
cookie?: never;
};
requestBody?: never;
@@ -35771,16 +39173,28 @@ export interface operations {
[name: string]: unknown;
};
content: {
- "application/json": unknown;
+ "application/json": components["schemas"]["ImageDTO"];
+ };
+ };
+ /** @description Validation Error */
+ 422: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/json": components["schemas"]["HTTPValidationError"];
};
};
};
};
- get_hf_login_status: {
+ delete_image: {
parameters: {
query?: never;
header?: never;
- path?: never;
+ path: {
+ /** @description The name of the image to delete */
+ image_name: string;
+ };
cookie?: never;
};
requestBody?: never;
@@ -35791,21 +39205,33 @@ export interface operations {
[name: string]: unknown;
};
content: {
- "application/json": components["schemas"]["HFTokenStatus"];
+ "application/json": components["schemas"]["DeleteImagesResult"];
+ };
+ };
+ /** @description Validation Error */
+ 422: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/json": components["schemas"]["HTTPValidationError"];
};
};
};
};
- do_hf_login: {
+ update_image: {
parameters: {
query?: never;
header?: never;
- path?: never;
+ path: {
+ /** @description The name of the image to update */
+ image_name: string;
+ };
cookie?: never;
};
requestBody: {
content: {
- "application/json": components["schemas"]["Body_do_hf_login"];
+ "application/json": components["schemas"]["ImageRecordChanges"];
};
};
responses: {
@@ -35815,7 +39241,7 @@ export interface operations {
[name: string]: unknown;
};
content: {
- "application/json": components["schemas"]["HFTokenStatus"];
+ "application/json": components["schemas"]["ImageDTO"];
};
};
/** @description Validation Error */
@@ -35829,7 +39255,7 @@ export interface operations {
};
};
};
- reset_hf_token: {
+ get_intermediates_count: {
parameters: {
query?: never;
header?: never;
@@ -35844,12 +39270,12 @@ export interface operations {
[name: string]: unknown;
};
content: {
- "application/json": components["schemas"]["HFTokenStatus"];
+ "application/json": number;
};
};
};
};
- get_orphaned_models: {
+ clear_intermediates: {
parameters: {
query?: never;
header?: never;
@@ -35864,23 +39290,22 @@ export interface operations {
[name: string]: unknown;
};
content: {
- "application/json": components["schemas"]["OrphanedModelInfo"][];
+ "application/json": number;
};
};
};
};
- delete_orphaned_models: {
+ get_image_metadata: {
parameters: {
query?: never;
header?: never;
- path?: never;
- cookie?: never;
- };
- requestBody: {
- content: {
- "application/json": components["schemas"]["DeleteOrphanedModelsRequest"];
+ path: {
+ /** @description The name of image to get */
+ image_name: string;
};
+ cookie?: never;
};
+ requestBody?: never;
responses: {
/** @description Successful Response */
200: {
@@ -35888,7 +39313,7 @@ export interface operations {
[name: string]: unknown;
};
content: {
- "application/json": components["schemas"]["DeleteOrphanedModelsResponse"];
+ "application/json": components["schemas"]["MetadataField"] | null;
};
};
/** @description Validation Error */
@@ -35902,11 +39327,14 @@ export interface operations {
};
};
};
- list_downloads: {
+ get_image_workflow: {
parameters: {
query?: never;
header?: never;
- path?: never;
+ path: {
+ /** @description The name of image whose workflow to get */
+ image_name: string;
+ };
cookie?: never;
};
requestBody?: never;
@@ -35917,66 +39345,90 @@ export interface operations {
[name: string]: unknown;
};
content: {
- "application/json": components["schemas"]["DownloadJob"][];
+ "application/json": components["schemas"]["WorkflowAndGraphResponse"];
+ };
+ };
+ /** @description Validation Error */
+ 422: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/json": components["schemas"]["HTTPValidationError"];
};
};
};
};
- prune_downloads: {
+ get_image_full: {
parameters: {
query?: never;
header?: never;
- path?: never;
- cookie?: never;
+ path: {
+ /** @description The name of full-resolution image file to get */
+ image_name: string;
+ };
+ cookie?: {
+ invokeai_media_token?: string | null;
+ };
};
requestBody?: never;
responses: {
- /** @description Successful Response */
+ /** @description Return the full-resolution image */
200: {
headers: {
[name: string]: unknown;
};
content: {
- "application/json": unknown;
+ "image/png": unknown;
};
};
- /** @description All completed jobs have been pruned */
- 204: {
+ /** @description Image not found */
+ 404: {
headers: {
[name: string]: unknown;
};
content?: never;
};
- /** @description Bad request */
- 400: {
+ /** @description Validation Error */
+ 422: {
headers: {
[name: string]: unknown;
};
- content?: never;
+ content: {
+ "application/json": components["schemas"]["HTTPValidationError"];
+ };
};
};
};
- download: {
+ get_image_full_head: {
parameters: {
query?: never;
header?: never;
- path?: never;
- cookie?: never;
- };
- requestBody: {
- content: {
- "application/json": components["schemas"]["Body_download"];
+ path: {
+ /** @description The name of full-resolution image file to get */
+ image_name: string;
+ };
+ cookie?: {
+ invokeai_media_token?: string | null;
};
};
+ requestBody?: never;
responses: {
- /** @description Successful Response */
+ /** @description Return the full-resolution image */
200: {
headers: {
[name: string]: unknown;
};
content: {
- "application/json": components["schemas"]["DownloadJob"];
+ "image/png": unknown;
+ };
+ };
+ /** @description Image not found */
+ 404: {
+ headers: {
+ [name: string]: unknown;
};
+ content?: never;
};
/** @description Validation Error */
422: {
@@ -35989,28 +39441,30 @@ export interface operations {
};
};
};
- get_download_job: {
+ get_image_thumbnail: {
parameters: {
query?: never;
header?: never;
path: {
- /** @description ID of the download job to fetch. */
- id: number;
+ /** @description The name of thumbnail image file to get */
+ image_name: string;
+ };
+ cookie?: {
+ invokeai_media_token?: string | null;
};
- cookie?: never;
};
requestBody?: never;
responses: {
- /** @description Success */
+ /** @description Return the image thumbnail */
200: {
headers: {
[name: string]: unknown;
};
content: {
- "application/json": components["schemas"]["DownloadJob"];
+ "image/webp": unknown;
};
};
- /** @description The requested download JobID could not be found */
+ /** @description Image not found */
404: {
headers: {
[name: string]: unknown;
@@ -36028,13 +39482,13 @@ export interface operations {
};
};
};
- cancel_download_job: {
+ get_image_urls: {
parameters: {
query?: never;
header?: never;
path: {
- /** @description ID of the download job to cancel. */
- id: number;
+ /** @description The name of the image whose URL to get */
+ image_name: string;
};
cookie?: never;
};
@@ -36046,22 +39500,8 @@ export interface operations {
[name: string]: unknown;
};
content: {
- "application/json": unknown;
- };
- };
- /** @description Job has been cancelled */
- 204: {
- headers: {
- [name: string]: unknown;
- };
- content?: never;
- };
- /** @description The requested download JobID could not be found */
- 404: {
- headers: {
- [name: string]: unknown;
+ "application/json": components["schemas"]["ImageUrlsDTO"];
};
- content?: never;
};
/** @description Validation Error */
422: {
@@ -36074,14 +39514,18 @@ export interface operations {
};
};
};
- cancel_all_download_jobs: {
+ delete_images_from_list: {
parameters: {
query?: never;
header?: never;
path?: never;
cookie?: never;
};
- requestBody?: never;
+ requestBody: {
+ content: {
+ "application/json": components["schemas"]["Body_delete_images_from_list"];
+ };
+ };
responses: {
/** @description Successful Response */
200: {
@@ -36089,19 +39533,21 @@ export interface operations {
[name: string]: unknown;
};
content: {
- "application/json": unknown;
+ "application/json": components["schemas"]["DeleteImagesResult"];
};
};
- /** @description Download jobs have been cancelled */
- 204: {
+ /** @description Validation Error */
+ 422: {
headers: {
[name: string]: unknown;
};
- content?: never;
+ content: {
+ "application/json": components["schemas"]["HTTPValidationError"];
+ };
};
};
};
- start_image_move: {
+ delete_uncategorized_images: {
parameters: {
query?: never;
header?: never;
@@ -36111,44 +39557,61 @@ export interface operations {
requestBody?: never;
responses: {
/** @description Successful Response */
- 202: {
+ 200: {
headers: {
[name: string]: unknown;
};
content: {
- "application/json": components["schemas"]["ImageMoveStatusResponse"];
+ "application/json": components["schemas"]["DeleteImagesResult"];
};
};
};
};
- start_image_move_recovery: {
+ star_images_in_list: {
parameters: {
query?: never;
header?: never;
path?: never;
cookie?: never;
};
- requestBody?: never;
+ requestBody: {
+ content: {
+ "application/json": components["schemas"]["Body_star_images_in_list"];
+ };
+ };
responses: {
/** @description Successful Response */
- 202: {
+ 200: {
headers: {
[name: string]: unknown;
};
content: {
- "application/json": components["schemas"]["ImageMoveStatusResponse"];
+ "application/json": components["schemas"]["StarredImagesResult"];
+ };
+ };
+ /** @description Validation Error */
+ 422: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/json": components["schemas"]["HTTPValidationError"];
};
};
};
};
- get_image_move_status: {
+ unstar_images_in_list: {
parameters: {
query?: never;
header?: never;
path?: never;
cookie?: never;
};
- requestBody?: never;
+ requestBody: {
+ content: {
+ "application/json": components["schemas"]["Body_unstar_images_in_list"];
+ };
+ };
responses: {
/** @description Successful Response */
200: {
@@ -36156,46 +39619,76 @@ export interface operations {
[name: string]: unknown;
};
content: {
- "application/json": components["schemas"]["ImageMoveStatusResponse"];
+ "application/json": components["schemas"]["UnstarredImagesResult"];
+ };
+ };
+ /** @description Validation Error */
+ 422: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/json": components["schemas"]["HTTPValidationError"];
};
};
};
};
- upload_image: {
+ download_images_from_list: {
parameters: {
- query: {
- /** @description The category of the image */
- image_category: components["schemas"]["ImageCategory"];
- /** @description Whether this is an intermediate image */
- is_intermediate: boolean;
- /** @description The board to add this image to, if any */
- board_id?: string | null;
- /** @description The session ID associated with this upload, if any */
- session_id?: string | null;
- /** @description Whether to crop the image */
- crop_visible?: boolean | null;
- };
+ query?: never;
header?: never;
path?: never;
cookie?: never;
};
- requestBody: {
+ requestBody?: {
content: {
- "multipart/form-data": components["schemas"]["Body_upload_image"];
+ "application/json": components["schemas"]["Body_download_images_from_list"];
};
};
responses: {
- /** @description The image was uploaded successfully */
- 201: {
+ /** @description Successful Response */
+ 202: {
headers: {
[name: string]: unknown;
};
content: {
- "application/json": components["schemas"]["ImageDTO"];
+ "application/json": components["schemas"]["ImagesDownloaded"];
};
};
- /** @description Image upload failed */
- 415: {
+ /** @description Validation Error */
+ 422: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/json": components["schemas"]["HTTPValidationError"];
+ };
+ };
+ };
+ };
+ get_bulk_download_item: {
+ parameters: {
+ query?: never;
+ header?: never;
+ path: {
+ /** @description The bulk_download_item_name of the bulk download item to get */
+ bulk_download_item_name: string;
+ };
+ cookie?: never;
+ };
+ requestBody?: never;
+ responses: {
+ /** @description Return the complete bulk download item */
+ 200: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/zip": unknown;
+ };
+ };
+ /** @description Image not found */
+ 404: {
headers: {
[name: string]: unknown;
};
@@ -36212,7 +39705,7 @@ export interface operations {
};
};
};
- list_image_dtos: {
+ get_image_names: {
parameters: {
query?: {
/** @description The origin of images to list. */
@@ -36223,10 +39716,6 @@ export interface operations {
is_intermediate?: boolean | null;
/** @description The board id to filter by. Use 'none' to find images without a board. */
board_id?: string | null;
- /** @description The page offset */
- offset?: number;
- /** @description The number of images per page */
- limit?: number;
/** @description The order of sort */
order_dir?: components["schemas"]["SQLiteDirection"];
/** @description Whether to sort by starred images first */
@@ -36246,7 +39735,7 @@ export interface operations {
[name: string]: unknown;
};
content: {
- "application/json": components["schemas"]["OffsetPaginatedResults_ImageDTO_"];
+ "application/json": components["schemas"]["ImageNamesResult"];
};
};
/** @description Validation Error */
@@ -36260,7 +39749,7 @@ export interface operations {
};
};
};
- create_image_upload_entry: {
+ get_images_by_names: {
parameters: {
query?: never;
header?: never;
@@ -36269,7 +39758,7 @@ export interface operations {
};
requestBody: {
content: {
- "application/json": components["schemas"]["Body_create_image_upload_entry"];
+ "application/json": components["schemas"]["Body_get_images_by_names"];
};
};
responses: {
@@ -36279,7 +39768,7 @@ export interface operations {
[name: string]: unknown;
};
content: {
- "application/json": components["schemas"]["ImageUploadEntry"];
+ "application/json": components["schemas"]["ImageDTO"][];
};
};
/** @description Validation Error */
@@ -36293,13 +39782,62 @@ export interface operations {
};
};
};
- get_image_dto: {
+ upload_video: {
+ parameters: {
+ query: {
+ /** @description The category of the video */
+ video_category: components["schemas"]["ImageCategory"];
+ /** @description Whether this is an intermediate video */
+ is_intermediate: boolean;
+ /** @description The board to add this video to, if any */
+ board_id?: string | null;
+ /** @description The session ID associated with this upload, if any */
+ session_id?: string | null;
+ };
+ header?: never;
+ path?: never;
+ cookie?: never;
+ };
+ requestBody: {
+ content: {
+ "multipart/form-data": components["schemas"]["Body_upload_video"];
+ };
+ };
+ responses: {
+ /** @description The video was uploaded successfully */
+ 201: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/json": components["schemas"]["VideoDTO"];
+ };
+ };
+ /** @description Video upload failed */
+ 415: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content?: never;
+ };
+ /** @description Validation Error */
+ 422: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/json": components["schemas"]["HTTPValidationError"];
+ };
+ };
+ };
+ };
+ get_video_dto: {
parameters: {
query?: never;
header?: never;
path: {
- /** @description The name of image to get */
- image_name: string;
+ /** @description The name of video to get */
+ video_name: string;
};
cookie?: never;
};
@@ -36311,7 +39849,7 @@ export interface operations {
[name: string]: unknown;
};
content: {
- "application/json": components["schemas"]["ImageDTO"];
+ "application/json": components["schemas"]["VideoDTO"];
};
};
/** @description Validation Error */
@@ -36325,13 +39863,13 @@ export interface operations {
};
};
};
- delete_image: {
+ delete_video: {
parameters: {
query?: never;
header?: never;
path: {
- /** @description The name of the image to delete */
- image_name: string;
+ /** @description The name of the video to delete */
+ video_name: string;
};
cookie?: never;
};
@@ -36343,7 +39881,7 @@ export interface operations {
[name: string]: unknown;
};
content: {
- "application/json": components["schemas"]["DeleteImagesResult"];
+ "application/json": components["schemas"]["DeleteVideosResult"];
};
};
/** @description Validation Error */
@@ -36357,19 +39895,19 @@ export interface operations {
};
};
};
- update_image: {
+ update_video: {
parameters: {
query?: never;
header?: never;
path: {
- /** @description The name of the image to update */
- image_name: string;
+ /** @description The name of the video to update */
+ video_name: string;
};
cookie?: never;
};
requestBody: {
content: {
- "application/json": components["schemas"]["ImageRecordChanges"];
+ "application/json": components["schemas"]["VideoRecordChanges"];
};
};
responses: {
@@ -36379,7 +39917,7 @@ export interface operations {
[name: string]: unknown;
};
content: {
- "application/json": components["schemas"]["ImageDTO"];
+ "application/json": components["schemas"]["VideoDTO"];
};
};
/** @description Validation Error */
@@ -36393,14 +39931,18 @@ export interface operations {
};
};
};
- get_intermediates_count: {
+ delete_videos_from_list: {
parameters: {
query?: never;
header?: never;
path?: never;
cookie?: never;
};
- requestBody?: never;
+ requestBody: {
+ content: {
+ "application/json": components["schemas"]["Body_delete_videos_from_list"];
+ };
+ };
responses: {
/** @description Successful Response */
200: {
@@ -36408,12 +39950,21 @@ export interface operations {
[name: string]: unknown;
};
content: {
- "application/json": number;
+ "application/json": components["schemas"]["DeleteVideosResult"];
+ };
+ };
+ /** @description Validation Error */
+ 422: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/json": components["schemas"]["HTTPValidationError"];
};
};
};
};
- clear_intermediates: {
+ delete_uncategorized_videos: {
parameters: {
query?: never;
header?: never;
@@ -36428,18 +39979,18 @@ export interface operations {
[name: string]: unknown;
};
content: {
- "application/json": number;
+ "application/json": components["schemas"]["DeleteVideosResult"];
};
};
};
};
- get_image_metadata: {
+ get_video_metadata: {
parameters: {
query?: never;
header?: never;
path: {
- /** @description The name of image to get */
- image_name: string;
+ /** @description The name of video to get */
+ video_name: string;
};
cookie?: never;
};
@@ -36465,13 +40016,13 @@ export interface operations {
};
};
};
- get_image_workflow: {
+ get_video_workflow: {
parameters: {
query?: never;
header?: never;
path: {
- /** @description The name of image whose workflow to get */
- image_name: string;
+ /** @description The name of video whose workflow to get */
+ video_name: string;
};
cookie?: never;
};
@@ -36497,28 +40048,39 @@ export interface operations {
};
};
};
- get_image_full: {
+ get_video_full: {
parameters: {
query?: never;
header?: never;
path: {
- /** @description The name of full-resolution image file to get */
- image_name: string;
+ /** @description The name of video file to get */
+ video_name: string;
+ };
+ cookie?: {
+ invokeai_media_token?: string | null;
};
- cookie?: never;
};
requestBody?: never;
responses: {
- /** @description Return the full-resolution image */
+ /** @description Return the full video file */
200: {
headers: {
[name: string]: unknown;
};
content: {
- "image/png": unknown;
+ "video/mp4": unknown;
};
};
- /** @description Image not found */
+ /** @description Return a byte-range of the video file */
+ 206: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "video/mp4": unknown;
+ };
+ };
+ /** @description Video not found */
404: {
headers: {
[name: string]: unknown;
@@ -36536,28 +40098,30 @@ export interface operations {
};
};
};
- get_image_full_head: {
+ get_video_full_head: {
parameters: {
query?: never;
header?: never;
path: {
- /** @description The name of full-resolution image file to get */
- image_name: string;
+ /** @description The name of video file to get */
+ video_name: string;
+ };
+ cookie?: {
+ invokeai_media_token?: string | null;
};
- cookie?: never;
};
requestBody?: never;
responses: {
- /** @description Return the full-resolution image */
+ /** @description Return the full video file */
200: {
headers: {
[name: string]: unknown;
};
content: {
- "image/png": unknown;
+ "video/mp4": unknown;
};
};
- /** @description Image not found */
+ /** @description Video not found */
404: {
headers: {
[name: string]: unknown;
@@ -36575,19 +40139,21 @@ export interface operations {
};
};
};
- get_image_thumbnail: {
+ get_video_thumbnail: {
parameters: {
query?: never;
header?: never;
path: {
- /** @description The name of thumbnail image file to get */
- image_name: string;
+ /** @description The name of thumbnail file to get */
+ video_name: string;
+ };
+ cookie?: {
+ invokeai_media_token?: string | null;
};
- cookie?: never;
};
requestBody?: never;
responses: {
- /** @description Return the image thumbnail */
+ /** @description Return the video thumbnail */
200: {
headers: {
[name: string]: unknown;
@@ -36596,7 +40162,7 @@ export interface operations {
"image/webp": unknown;
};
};
- /** @description Image not found */
+ /** @description Video not found */
404: {
headers: {
[name: string]: unknown;
@@ -36614,13 +40180,13 @@ export interface operations {
};
};
};
- get_image_urls: {
+ get_video_urls: {
parameters: {
query?: never;
header?: never;
path: {
- /** @description The name of the image whose URL to get */
- image_name: string;
+ /** @description The name of the video whose URL to get */
+ video_name: string;
};
cookie?: never;
};
@@ -36632,7 +40198,7 @@ export interface operations {
[name: string]: unknown;
};
content: {
- "application/json": components["schemas"]["ImageUrlsDTO"];
+ "application/json": components["schemas"]["VideoUrlsDTO"];
};
};
/** @description Validation Error */
@@ -36646,18 +40212,33 @@ export interface operations {
};
};
};
- delete_images_from_list: {
+ list_video_dtos: {
parameters: {
- query?: never;
+ query?: {
+ /** @description The origin of videos to list. */
+ video_origin?: components["schemas"]["ResourceOrigin"] | null;
+ /** @description The categories of video to include. */
+ categories?: components["schemas"]["ImageCategory"][] | null;
+ /** @description Whether to list intermediate videos. */
+ is_intermediate?: boolean | null;
+ /** @description The board id to filter by. Use 'none' to find videos without a board. */
+ board_id?: string | null;
+ /** @description The page offset */
+ offset?: number;
+ /** @description The number of videos per page */
+ limit?: number;
+ /** @description The order of sort */
+ order_dir?: components["schemas"]["SQLiteDirection"];
+ /** @description Whether to sort by starred videos first */
+ starred_first?: boolean;
+ /** @description The term to search for */
+ search_term?: string | null;
+ };
header?: never;
path?: never;
cookie?: never;
};
- requestBody: {
- content: {
- "application/json": components["schemas"]["Body_delete_images_from_list"];
- };
- };
+ requestBody?: never;
responses: {
/** @description Successful Response */
200: {
@@ -36665,7 +40246,7 @@ export interface operations {
[name: string]: unknown;
};
content: {
- "application/json": components["schemas"]["DeleteImagesResult"];
+ "application/json": components["schemas"]["OffsetPaginatedResults_VideoDTO_"];
};
};
/** @description Validation Error */
@@ -36679,9 +40260,24 @@ export interface operations {
};
};
};
- delete_uncategorized_images: {
+ get_video_names: {
parameters: {
- query?: never;
+ query?: {
+ /** @description The origin of videos to list. */
+ video_origin?: components["schemas"]["ResourceOrigin"] | null;
+ /** @description The categories of video to include. */
+ categories?: components["schemas"]["ImageCategory"][] | null;
+ /** @description Whether to list intermediate videos. */
+ is_intermediate?: boolean | null;
+ /** @description The board id to filter by. Use 'none' to find videos without a board. */
+ board_id?: string | null;
+ /** @description The order of sort */
+ order_dir?: components["schemas"]["SQLiteDirection"];
+ /** @description Whether to sort by starred videos first */
+ starred_first?: boolean;
+ /** @description The term to search for */
+ search_term?: string | null;
+ };
header?: never;
path?: never;
cookie?: never;
@@ -36694,12 +40290,21 @@ export interface operations {
[name: string]: unknown;
};
content: {
- "application/json": components["schemas"]["DeleteImagesResult"];
+ "application/json": components["schemas"]["VideoNamesResult"];
+ };
+ };
+ /** @description Validation Error */
+ 422: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/json": components["schemas"]["HTTPValidationError"];
};
};
};
};
- star_images_in_list: {
+ star_videos_in_list: {
parameters: {
query?: never;
header?: never;
@@ -36708,7 +40313,7 @@ export interface operations {
};
requestBody: {
content: {
- "application/json": components["schemas"]["Body_star_images_in_list"];
+ "application/json": components["schemas"]["Body_star_videos_in_list"];
};
};
responses: {
@@ -36718,7 +40323,7 @@ export interface operations {
[name: string]: unknown;
};
content: {
- "application/json": components["schemas"]["StarredImagesResult"];
+ "application/json": components["schemas"]["StarredVideosResult"];
};
};
/** @description Validation Error */
@@ -36732,7 +40337,7 @@ export interface operations {
};
};
};
- unstar_images_in_list: {
+ unstar_videos_in_list: {
parameters: {
query?: never;
header?: never;
@@ -36741,7 +40346,7 @@ export interface operations {
};
requestBody: {
content: {
- "application/json": components["schemas"]["Body_unstar_images_in_list"];
+ "application/json": components["schemas"]["Body_unstar_videos_in_list"];
};
};
responses: {
@@ -36751,7 +40356,7 @@ export interface operations {
[name: string]: unknown;
};
content: {
- "application/json": components["schemas"]["UnstarredImagesResult"];
+ "application/json": components["schemas"]["UnstarredVideosResult"];
};
};
/** @description Validation Error */
@@ -36765,26 +40370,26 @@ export interface operations {
};
};
};
- download_images_from_list: {
+ add_video_to_board: {
parameters: {
query?: never;
header?: never;
path?: never;
cookie?: never;
};
- requestBody?: {
+ requestBody: {
content: {
- "application/json": components["schemas"]["Body_download_images_from_list"];
+ "application/json": components["schemas"]["VideoBoardArg"];
};
};
responses: {
/** @description Successful Response */
- 202: {
+ 200: {
headers: {
[name: string]: unknown;
};
content: {
- "application/json": components["schemas"]["ImagesDownloaded"];
+ "application/json": components["schemas"]["AddVideosToBoardResult"];
};
};
/** @description Validation Error */
@@ -36798,33 +40403,27 @@ export interface operations {
};
};
};
- get_bulk_download_item: {
+ remove_video_from_board: {
parameters: {
query?: never;
header?: never;
- path: {
- /** @description The bulk_download_item_name of the bulk download item to get */
- bulk_download_item_name: string;
- };
+ path?: never;
cookie?: never;
};
- requestBody?: never;
+ requestBody: {
+ content: {
+ "application/json": components["schemas"]["Body_remove_video_from_board"];
+ };
+ };
responses: {
- /** @description Return the complete bulk download item */
+ /** @description Successful Response */
200: {
headers: {
[name: string]: unknown;
};
content: {
- "application/zip": unknown;
- };
- };
- /** @description Image not found */
- 404: {
- headers: {
- [name: string]: unknown;
+ "application/json": components["schemas"]["RemoveVideosFromBoardResult"];
};
- content?: never;
};
/** @description Validation Error */
422: {
@@ -36837,20 +40436,24 @@ export interface operations {
};
};
};
- get_image_names: {
+ list_gallery_items: {
parameters: {
query?: {
- /** @description The origin of images to list. */
- image_origin?: components["schemas"]["ResourceOrigin"] | null;
- /** @description The categories of image to include. */
+ /** @description The origin of items to list. */
+ origin?: components["schemas"]["ResourceOrigin"] | null;
+ /** @description The categories to include. Shared between images and videos. */
categories?: components["schemas"]["ImageCategory"][] | null;
- /** @description Whether to list intermediate images. */
+ /** @description Whether to list intermediate items. */
is_intermediate?: boolean | null;
- /** @description The board id to filter by. Use 'none' to find images without a board. */
+ /** @description The board id to filter by. Use 'none' to find items without a board. */
board_id?: string | null;
+ /** @description The page offset */
+ offset?: number;
+ /** @description The number of items per page */
+ limit?: number;
/** @description The order of sort */
order_dir?: components["schemas"]["SQLiteDirection"];
- /** @description Whether to sort by starred images first */
+ /** @description Whether to sort by starred items first */
starred_first?: boolean;
/** @description The term to search for */
search_term?: string | null;
@@ -36867,7 +40470,7 @@ export interface operations {
[name: string]: unknown;
};
content: {
- "application/json": components["schemas"]["ImageNamesResult"];
+ "application/json": components["schemas"]["OffsetPaginatedResults_GalleryItem_"];
};
};
/** @description Validation Error */
@@ -36881,18 +40484,29 @@ export interface operations {
};
};
};
- get_images_by_names: {
+ get_gallery_item_names: {
parameters: {
- query?: never;
+ query?: {
+ /** @description The origin of items to list. */
+ origin?: components["schemas"]["ResourceOrigin"] | null;
+ /** @description The categories to include. Shared between images and videos. */
+ categories?: components["schemas"]["ImageCategory"][] | null;
+ /** @description Whether to list intermediate items. */
+ is_intermediate?: boolean | null;
+ /** @description The board id to filter by. Use 'none' to find items without a board. */
+ board_id?: string | null;
+ /** @description The order of sort */
+ order_dir?: components["schemas"]["SQLiteDirection"];
+ /** @description Whether to sort by starred items first */
+ starred_first?: boolean;
+ /** @description The term to search for */
+ search_term?: string | null;
+ };
header?: never;
path?: never;
cookie?: never;
};
- requestBody: {
- content: {
- "application/json": components["schemas"]["Body_get_images_by_names"];
- };
- };
+ requestBody?: never;
responses: {
/** @description Successful Response */
200: {
@@ -36900,7 +40514,7 @@ export interface operations {
[name: string]: unknown;
};
content: {
- "application/json": components["schemas"]["ImageDTO"][];
+ "application/json": components["schemas"]["GalleryItemNamesResult"];
};
};
/** @description Validation Error */
@@ -37023,7 +40637,7 @@ export interface operations {
delete_board: {
parameters: {
query?: {
- /** @description Permanently delete all images on the board */
+ /** @description Permanently delete all images and videos on the board */
include_images?: boolean | null;
};
header?: never;
@@ -37321,6 +40935,47 @@ export interface operations {
};
};
};
+ list_virtual_board_item_names_by_date: {
+ parameters: {
+ query?: {
+ /** @description Whether to sort starred items first */
+ starred_first?: boolean;
+ /** @description The sort direction */
+ order_dir?: components["schemas"]["SQLiteDirection"];
+ /** @description The categories of items to include */
+ categories?: components["schemas"]["ImageCategory"][] | null;
+ /** @description Search term to filter items */
+ search_term?: string | null;
+ };
+ header?: never;
+ path: {
+ /** @description The ISO date string, e.g. '2026-03-18' */
+ date: string;
+ };
+ cookie?: never;
+ };
+ requestBody?: never;
+ responses: {
+ /** @description Successful Response */
+ 200: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/json": components["schemas"]["GalleryItemNamesResult"];
+ };
+ };
+ /** @description Validation Error */
+ 422: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/json": components["schemas"]["HTTPValidationError"];
+ };
+ };
+ };
+ };
get_related_models: {
parameters: {
query?: never;
diff --git a/invokeai/frontend/web/src/services/api/types.ts b/invokeai/frontend/web/src/services/api/types.ts
index 34d0470db45..1b2bd7ac960 100644
--- a/invokeai/frontend/web/src/services/api/types.ts
+++ b/invokeai/frontend/web/src/services/api/types.ts
@@ -101,6 +101,7 @@ type FLUX2ModelConfig = Extract;
export type LoRAModelConfig = Extract;
+type WanLoRAModelConfig = Extract;
export type VAEModelConfig = Extract;
export type ControlNetModelConfig = Extract;
type AnimaControlNetModelConfig = Extract;
@@ -118,6 +119,7 @@ export type T5EncoderBnbQuantizedLlmInt8bModelConfig = Extract<
>;
export type Qwen3EncoderModelConfig = Extract;
export type QwenVLEncoderModelConfig = Extract;
+export type WanT5EncoderModelConfig = Extract;
export type SpandrelImageToImageModelConfig = Extract;
export type CheckpointModelConfig = Extract;
export type CLIPVisionModelConfig = Extract;
@@ -322,6 +324,13 @@ export const isQwenImageVAEModelConfig = (
);
};
+export const isWanVAEModelConfig = (config: AnyModelConfig, excludeSubmodels?: boolean): config is VAEModelConfig => {
+ return (
+ (config.type === 'vae' || (!excludeSubmodels && config.type === 'main' && checkSubmodels(['vae'], config))) &&
+ config.base === 'wan'
+ );
+};
+
export const isControlNetModelConfig = (config: AnyModelConfig): config is ControlNetModelConfig => {
return config.type === 'controlnet';
};
@@ -397,6 +406,10 @@ export const isQwenVLEncoderModelConfig = (config: AnyModelConfig): config is Qw
return config.type === 'qwen_vl_encoder';
};
+export const isWanT5EncoderModelConfig = (config: AnyModelConfig): config is WanT5EncoderModelConfig => {
+ return config.type === 'wan_t5_encoder';
+};
+
export const isCLIPEmbedModelConfigOrSubmodel = (
config: AnyModelConfig,
excludeSubmodels?: boolean
@@ -504,6 +517,23 @@ export const isQwenImageDiffusersMainModelConfig = (config: AnyModelConfig): con
return config.type === 'main' && config.base === 'qwen-image' && config.format === 'diffusers';
};
+export const isWanDiffusersMainModelConfig = (config: AnyModelConfig): config is MainModelConfig => {
+ return config.type === 'main' && config.base === 'wan' && config.format === 'diffusers';
+};
+
+/** Wan GGUF main models marked as the low-noise expert (the second half
+ * of the A14B MoE pair). Suitable for the Transformer (Low Noise) picker;
+ * also used to filter low-noise GGUFs out of the primary main dropdown. */
+export const isWanGGUFLowNoiseMainModelConfig = (config: AnyModelConfig): config is MainModelConfig => {
+ return (
+ config.type === 'main' && config.base === 'wan' && config.format === 'gguf_quantized' && config.expert === 'low'
+ );
+};
+
+export const isWanLoRAModelConfig = (config: AnyModelConfig): config is WanLoRAModelConfig => {
+ return config.type === 'lora' && config.base === 'wan';
+};
+
export const isTIModelConfig = (config: AnyModelConfig): config is MainModelConfig => {
return config.type === 'embedding';
};
@@ -630,3 +660,51 @@ export type UploadImageArg = {
export type ImageUploadEntryResponse = S['ImageUploadEntry'];
export type ImageUploadEntryRequest = paths['/api/v1/images/']['post']['requestBody']['content']['application/json'];
+
+// Videos
+export type VideoDTO = S['VideoDTO'];
+/** @knipignore Used by Phase 4+ video gallery mutations. */
+export type VideoRecordChanges = S['VideoRecordChanges'];
+export type OffsetPaginatedResults_VideoDTO_ = S['OffsetPaginatedResults_VideoDTO_'];
+export type ListVideosArgs = NonNullable;
+export type ListVideosResponse = paths['/api/v1/videos/']['get']['responses']['200']['content']['application/json'];
+export type GetVideoNamesArgs = NonNullable;
+export type GetVideoNamesResult =
+ paths['/api/v1/videos/names']['get']['responses']['200']['content']['application/json'];
+
+export type UploadVideoArg = {
+ /** The MP4 (or other accepted video) file to upload. */
+ file: File;
+ /** The category of video to upload. Reuses the image category enum. */
+ video_category: ImageCategory;
+ /** Whether the uploaded video is an intermediate (intermediates are not shown in the gallery). */
+ is_intermediate: boolean;
+ /** The session with which to associate the uploaded video, if any. */
+ session_id?: string;
+ /** The board to add the video to, if any. */
+ board_id?: string;
+ /** Metadata JSON to attach to the video record. */
+ metadata?: JsonObject;
+ /** Suppress the upload toast / gallery navigation side effects. */
+ silent?: boolean;
+ /** Whether this is the first upload of a batch (used by toast logic). */
+ isFirstUploadOfBatch?: boolean;
+};
+
+// Polymorphic gallery items (images + videos). Consumed by the gallery wiring in Phase 4.
+/** @knipignore Consumed by gallery wiring in Phase 4. */
+export type GalleryItem = S['GalleryItem'];
+/** @knipignore Consumed by gallery wiring in Phase 4. */
+export type GalleryItemKind = S['GalleryItemKind'];
+/** @knipignore Consumed by gallery wiring in Phase 4. */
+export type GalleryItemRef = S['GalleryItemRef'];
+/** @knipignore Consumed by gallery wiring in Phase 4. */
+export type GalleryItemNamesResult = S['GalleryItemNamesResult'];
+/** @knipignore Consumed by gallery wiring in Phase 4. */
+export type OffsetPaginatedResults_GalleryItem_ = S['OffsetPaginatedResults_GalleryItem_'];
+export type ListGalleryItemsArgs = NonNullable;
+export type ListGalleryItemsResponse =
+ paths['/api/v1/gallery/items/']['get']['responses']['200']['content']['application/json'];
+export type GetGalleryItemNamesArgs = NonNullable;
+export type GetGalleryItemNamesResult =
+ paths['/api/v1/gallery/items/names']['get']['responses']['200']['content']['application/json'];
diff --git a/invokeai/frontend/web/src/services/api/util.ts b/invokeai/frontend/web/src/services/api/util.ts
index 3d92923f2c5..0f2a03407e0 100644
--- a/invokeai/frontend/web/src/services/api/util.ts
+++ b/invokeai/frontend/web/src/services/api/util.ts
@@ -2,7 +2,7 @@ import { ASSETS_CATEGORIES, IMAGE_CATEGORIES } from 'features/gallery/store/type
import queryString from 'query-string';
import { buildV1Url } from 'services/api';
-import type { ImageDTO, ListImagesArgs } from './types';
+import type { ImageDTO, ListGalleryItemsArgs, ListImagesArgs, ListVideosArgs } from './types';
export const getCategories = (imageDTO: ImageDTO) => {
if (IMAGE_CATEGORIES.includes(imageDTO.image_category)) {
@@ -14,3 +14,11 @@ export const getCategories = (imageDTO: ImageDTO) => {
// Helper to create the url for the listImages endpoint. Also we use it to create the cache key.
export const getListImagesUrl = (queryArgs: ListImagesArgs) =>
buildV1Url(`images/?${queryString.stringify(queryArgs, { arrayFormat: 'none' })}`);
+
+// Helper to create the url for the listVideos endpoint. Also we use it to create the cache key.
+export const getListVideosUrl = (queryArgs: ListVideosArgs) =>
+ buildV1Url(`videos/?${queryString.stringify(queryArgs, { arrayFormat: 'none' })}`);
+
+// Helper to create the url for the polymorphic listGalleryItems endpoint.
+export const getListGalleryItemsUrl = (queryArgs: ListGalleryItemsArgs) =>
+ buildV1Url(`gallery/items/?${queryString.stringify(queryArgs, { arrayFormat: 'none' })}`);
diff --git a/invokeai/frontend/web/src/services/api/util/tagInvalidation.ts b/invokeai/frontend/web/src/services/api/util/tagInvalidation.ts
index bac3130d312..3950cbea986 100644
--- a/invokeai/frontend/web/src/services/api/util/tagInvalidation.ts
+++ b/invokeai/frontend/web/src/services/api/util/tagInvalidation.ts
@@ -4,7 +4,16 @@ import { getListImagesUrl } from 'services/api/util';
import type { ApiTagDescription } from '..';
export const getTagsToInvalidateForBoardAffectingMutation = (affected_boards: string[]): ApiTagDescription[] => {
- const tags: ApiTagDescription[] = ['ImageNameList', 'VirtualBoards'];
+ // Whenever an image or video mutation changes a board's contents we also have to refresh
+ // the polymorphic gallery list (and its names companion) since that is what the gallery UI
+ // actually subscribes to once Phase 4 lands.
+ const tags: ApiTagDescription[] = [
+ 'ImageNameList',
+ 'VirtualBoards',
+ 'VideoNameList',
+ 'GalleryItemList',
+ 'GalleryItemNameList',
+ ];
for (const board_id of affected_boards) {
tags.push({
@@ -32,6 +41,23 @@ export const getTagsToInvalidateForBoardAffectingMutation = (affected_boards: st
type: 'BoardImagesTotal',
id: board_id,
});
+
+ tags.push({
+ type: 'BoardVideosTotal',
+ id: board_id,
+ });
+ }
+
+ return tags;
+};
+
+export const getTagsToInvalidateForVideoMutation = (video_names: string[]): ApiTagDescription[] => {
+ const tags: ApiTagDescription[] = [];
+
+ for (const video_name of video_names) {
+ tags.push({ type: 'Video', id: video_name });
+ tags.push({ type: 'VideoMetadata', id: video_name });
+ tags.push({ type: 'VideoWorkflow', id: video_name });
}
return tags;
diff --git a/invokeai/frontend/web/src/services/events/onInvocationComplete.test.ts b/invokeai/frontend/web/src/services/events/onInvocationComplete.test.ts
index b9578ca1a29..0a811360ae0 100644
--- a/invokeai/frontend/web/src/services/events/onInvocationComplete.test.ts
+++ b/invokeai/frontend/web/src/services/events/onInvocationComplete.test.ts
@@ -1,56 +1,63 @@
-import { boardIdSelected, imageSelected } from 'features/gallery/store/gallerySlice';
-import { api } from 'services/api';
-import { getImageDTOSafe } from 'services/api/endpoints/images';
-import type { ImageDTO, S } from 'services/api/types';
-import { $lastProgressEvent } from 'services/events/stores';
+/**
+ * Regression test for the polymorphic gallery cache invalidation in
+ * ``addImagesToGallery``.
+ *
+ * The bug: ``onInvocationComplete`` only updated the image-only
+ * ``getImageNames`` RTK Query cache via an optimistic insert, but the gallery
+ * grid actually reads from the polymorphic ``getGalleryItemNames`` cache. So a
+ * freshly-generated image never appeared until the user reloaded the browser,
+ * even though it landed in board totals and the per-DTO cache correctly.
+ *
+ * The fix is a single line that dispatches
+ * ``galleryApi.util.invalidateTags(['GalleryItemNameList', 'GalleryItemList'])``
+ * after image outputs are processed. This test pins that behavior so a future
+ * refactor of the complete handler doesn't silently drop the invalidation.
+ */
+import type { S } from 'services/api/types';
import { beforeEach, describe, expect, it, vi } from 'vitest';
-import {
- buildOnForeignInvocationComplete,
- buildOnInvocationComplete,
- FOREIGN_GALLERY_REFRESH_TAGS,
-} from './onInvocationComplete';
-
-vi.mock('app/logging/logger', () => ({
- logger: () => ({
- debug: vi.fn(),
- trace: vi.fn(),
- info: vi.fn(),
- warn: vi.fn(),
- error: vi.fn(),
- }),
-}));
-
-vi.mock('services/api', () => ({
- LIST_TAG: 'LIST',
- LIST_ALL_TAG: 'LIST_ALL',
- api: {
- util: {
- invalidateTags: vi.fn((tags) => ({ type: 'api/invalidateTags', payload: tags })),
- },
- },
-}));
-
+// Mock the modules that have heavy side effects on import or do real network work
+// when their selectors fire. The mocks return shape-compatible no-ops; we only care
+// about the dispatch trace.
vi.mock('services/api/endpoints/images', () => ({
- getImageDTOSafe: vi.fn(),
imagesApi: {
util: {
- updateQueryData: vi.fn(() => ({ type: 'imagesApi/updateQueryData' })),
- invalidateTags: vi.fn(() => ({ type: 'imagesApi/invalidateTags' })),
+ updateQueryData: vi.fn(() => ({ type: 'mock/imagesApi/updateQueryData' })),
+ invalidateTags: vi.fn((tags: unknown[]) => ({ type: 'imagesApi/invalidateTags', payload: tags })),
+ },
+ endpoints: {
+ getImageNames: { select: vi.fn(() => () => ({ data: { image_names: [] } })) },
},
},
+ getImageDTOSafe: vi.fn((image_name: string) =>
+ Promise.resolve({
+ image_name,
+ image_url: `mock://${image_name}`,
+ thumbnail_url: `mock://thumb/${image_name}`,
+ width: 1024,
+ height: 1024,
+ is_intermediate: false,
+ is_starred: false,
+ image_category: 'general',
+ image_origin: 'internal',
+ has_workflow: false,
+ board_id: null,
+ created_at: '2026-01-01',
+ updated_at: '2026-01-01',
+ session_id: 'test-session',
+ node_id: 'test-node',
+ })
+ ),
}));
vi.mock('services/api/endpoints/boards', () => ({
boardsApi: {
- endpoints: {
- getBoardImagesTotal: {
- select: vi.fn(() => () => ({ data: undefined })),
- },
- },
util: {
- upsertQueryEntries: vi.fn(() => ({ type: 'boardsApi/upsertQueryEntries' })),
- updateQueryData: vi.fn(() => ({ type: 'boardsApi/updateQueryData' })),
+ upsertQueryEntries: vi.fn(() => ({ type: 'mock/boardsApi/upsertQueryEntries' })),
+ updateQueryData: vi.fn(() => ({ type: 'mock/boardsApi/updateQueryData' })),
+ },
+ endpoints: {
+ getBoardImagesTotal: { select: vi.fn(() => () => ({ data: undefined })) },
},
},
}));
@@ -58,147 +65,316 @@ vi.mock('services/api/endpoints/boards', () => ({
vi.mock('services/api/endpoints/queue', () => ({
queueApi: {
util: {
- invalidateTags: vi.fn(() => ({ type: 'queueApi/invalidateTags' })),
+ invalidateTags: vi.fn((tags: unknown[]) => ({ type: 'queueApi/invalidateTags', payload: tags })),
},
},
}));
+vi.mock('services/api/endpoints/videos', () => ({
+ getVideoDTOSafe: vi.fn(() => Promise.resolve(null)),
+}));
+
vi.mock('features/gallery/store/gallerySelectors', () => ({
- selectAutoSwitch: () => true,
- selectGalleryView: () => 'images',
- selectGetImageNamesQueryArgs: () => ({ search_term: '', starred_first: true, order_dir: 'DESC' }),
- selectListBoardsQueryArgs: () => ({}),
- selectSelectedBoardId: () => 'other-board',
+ selectAutoSwitch: vi.fn(() => false),
+ selectGalleryView: vi.fn(() => 'images'),
+ selectGetImageNamesQueryArgs: vi.fn(() => ({
+ board_id: 'none',
+ categories: ['general'],
+ search_term: '',
+ order_dir: 'DESC',
+ starred_first: true,
+ is_intermediate: false,
+ })),
+ selectListBoardsQueryArgs: vi.fn(() => ({
+ order_by: 'created_at',
+ direction: 'DESC',
+ })),
+ selectSelectedBoardId: vi.fn(() => 'none'),
}));
-vi.mock('features/nodes/hooks/useNodeExecutionState', () => ({
- $nodeExecutionStates: { get: () => ({}) },
- upsertExecutionState: vi.fn(),
+vi.mock('features/gallery/store/gallerySlice', () => ({
+ boardIdSelected: vi.fn(() => ({ type: 'mock/boardIdSelected' })),
+ galleryViewChanged: vi.fn(() => ({ type: 'mock/galleryViewChanged' })),
+ imageSelected: vi.fn(() => ({ type: 'mock/imageSelected' })),
}));
vi.mock('features/controlLayers/store/canvasWorkflowIntegrationSlice', () => ({
- canvasWorkflowIntegrationProcessingCompleted: vi.fn(() => ({
- type: 'canvasWorkflowIntegration/processingCompleted',
- })),
+ canvasWorkflowIntegrationProcessingCompleted: vi.fn(() => ({ type: 'mock/canvasComplete' })),
+}));
+
+vi.mock('features/nodes/hooks/useNodeExecutionState', () => ({
+ $nodeExecutionStates: { get: vi.fn(() => ({})) },
+ upsertExecutionState: vi.fn(),
}));
vi.mock('services/events/nodeExecutionState', () => ({
- getUpdatedNodeExecutionStateOnInvocationComplete: vi.fn(() => undefined),
+ getUpdatedNodeExecutionStateOnInvocationComplete: vi.fn(() => null),
}));
vi.mock('services/events/stores', () => ({
$lastProgressEvent: { set: vi.fn() },
}));
-const imageDTO = {
- image_name: 'img-1.png',
- board_id: 'user-board',
- is_intermediate: false,
- image_category: 'general',
-} as ImageDTO;
+// Import AFTER the mocks above are declared (vi.mock is hoisted; explicit ordering here
+// is for the human reader).
+import { getImageDTOSafe } from 'services/api/endpoints/images';
+import { getVideoDTOSafe } from 'services/api/endpoints/videos';
+
+import {
+ buildOnForeignInvocationComplete,
+ buildOnInvocationComplete,
+ FOREIGN_GALLERY_REFRESH_TAGS,
+} from './onInvocationComplete';
-const buildEvent = (overrides: Record = {}): S['InvocationCompleteEvent'] =>
+// Build a synthetic InvocationCompleteEvent whose result contains a single ImageField output.
+// The runtime ``isImageField`` discriminator matches on ``type === 'image_output'``.
+const buildImageCompleteEvent = (): S['InvocationCompleteEvent'] =>
({
+ queue_id: 'default',
item_id: 1,
batch_id: 'batch-1',
- session_id: 'session-1',
- queue_id: 'default',
+ origin: 'workflows',
+ destination: 'gallery',
user_id: 'user-1',
- origin: null,
- destination: null,
- invocation: { id: 'node-1', type: 'l2i' },
+ session_id: 'session-1',
invocation_source_id: 'node-1',
- result: { image: { image_name: imageDTO.image_name } },
- ...overrides,
+ invocation: {
+ id: 'prepared-node-1',
+ // Not in nodeTypeDenylist (which contains 'load_image', 'image') — so the handler
+ // will proceed to extract image DTOs.
+ type: 'add',
+ },
+ // ``result`` is the node's OutputType serialized as a flat key→value map.
+ // ``isImageField`` accepts any object with a non-empty ``image_name`` string,
+ // which is what the ``image`` output field unwraps to.
+ result: {
+ image: { image_name: 'fresh-image.png' },
+ width: 1024,
+ height: 1024,
+ },
}) as unknown as S['InvocationCompleteEvent'];
-const dispatchedActionTypes = (dispatch: ReturnType) =>
- dispatch.mock.calls.map(([action]) => (action as { type?: string }).type);
+describe('onInvocationComplete polymorphic gallery cache', () => {
+ beforeEach(() => {
+ vi.clearAllMocks();
+ });
-describe('buildOnInvocationComplete gallery auto-switch', () => {
- const buildHandler = () => {
- const dispatch = vi.fn();
+ it('invalidates GalleryItemNameList + GalleryItemList when an image output completes', async () => {
+ const dispatched: unknown[] = [];
+ const dispatch = vi.fn((action: unknown) => {
+ dispatched.push(action);
+ // RTK Query thunks return unsubscribe promises; the handler does not chain on the
+ // return value of the invalidate dispatch, so we can synchronously return a stub.
+ return { unwrap: () => Promise.resolve(undefined) };
+ });
const getState = vi.fn(() => ({}));
- const handler = buildOnInvocationComplete(getState as never, dispatch as never, new Map());
- return { handler, dispatch };
- };
- beforeEach(() => {
- vi.clearAllMocks();
- vi.mocked(getImageDTOSafe).mockResolvedValue(imageDTO);
+ const handler = buildOnInvocationComplete(
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
+ getState as any,
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
+ dispatch as any,
+ new Map()
+ );
+
+ await handler(buildImageCompleteEvent());
+
+ // The handler emits many actions; the regression-critical one is the polymorphic
+ // gallery tag invalidation. We identify it by its payload — the real
+ // ``galleryApi.util.invalidateTags`` produces an action with this exact payload.
+ const galleryInvalidation = dispatched.find((action): action is { type: string; payload: string[] } => {
+ if (!action || typeof action !== 'object') {
+ return false;
+ }
+ const payload = (action as { payload?: unknown }).payload;
+ if (!Array.isArray(payload)) {
+ return false;
+ }
+ return payload.includes('GalleryItemNameList') && payload.includes('GalleryItemList');
+ });
+
+ expect(galleryInvalidation, 'addImagesToGallery must invalidate the polymorphic gallery cache').toBeDefined();
+ });
+
+ it('does not invalidate the polymorphic gallery cache for denylisted node types', async () => {
+ const dispatched: unknown[] = [];
+ const dispatch = vi.fn((action: unknown) => {
+ dispatched.push(action);
+ return { unwrap: () => Promise.resolve(undefined) };
+ });
+ const getState = vi.fn(() => ({}));
+
+ const handler = buildOnInvocationComplete(
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
+ getState as any,
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
+ dispatch as any,
+ new Map()
+ );
+
+ // ``image`` is in the nodeTypeDenylist (passthrough node — doesn't add to gallery).
+ const denylisted = buildImageCompleteEvent();
+ denylisted.invocation.type = 'image';
+
+ await handler(denylisted);
+
+ const galleryInvalidation = dispatched.find((action): action is { type: string; payload: string[] } => {
+ if (!action || typeof action !== 'object') {
+ return false;
+ }
+ const payload = (action as { payload?: unknown }).payload;
+ return Array.isArray(payload) && payload.includes('GalleryItemNameList');
+ });
+ expect(galleryInvalidation, 'denylisted passthrough nodes must not trigger a gallery refetch').toBeUndefined();
});
- // The socket listener routes only the current user's own events here (single-user mode included),
- // so the handler runs unconditionally — ownership is decided upstream via getEventScope.
- it('switches boards and clears the progress indicator when a generation completes', async () => {
- const { handler, dispatch } = buildHandler();
+ it('invalidates board tags/totals in addition to the gallery cache when a video output completes', async () => {
+ // A generated video landing on a board must also refresh that board's DTO (video_count,
+ // cover_video_name via the ``Board`` tag), its ``BoardVideosTotal``, and the virtual board
+ // groupings — otherwise the boards list shows stale counts/covers until some unrelated
+ // mutation happens to refetch them.
+ vi.mocked(getVideoDTOSafe).mockResolvedValueOnce({
+ video_name: 'fresh-video.mp4',
+ video_url: 'mock://fresh-video.mp4',
+ thumbnail_url: 'mock://thumb/fresh-video.mp4',
+ width: 832,
+ height: 480,
+ duration_seconds: 3.4,
+ frame_count: 81,
+ is_intermediate: false,
+ is_starred: false,
+ board_id: 'board-123',
+ created_at: '2026-01-01',
+ updated_at: '2026-01-01',
+ session_id: 'test-session',
+ node_id: 'test-node',
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
+ } as any);
+
+ const dispatched: unknown[] = [];
+ const dispatch = vi.fn((action: unknown) => {
+ dispatched.push(action);
+ return { unwrap: () => Promise.resolve(undefined) };
+ });
+ const getState = vi.fn(() => ({}));
- await handler(buildEvent());
+ const handler = buildOnInvocationComplete(
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
+ getState as any,
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
+ dispatch as any,
+ new Map()
+ );
- expect(dispatchedActionTypes(dispatch)).toContain(boardIdSelected.type);
- expect($lastProgressEvent.set).toHaveBeenCalledWith(null);
+ const videoEvent = buildImageCompleteEvent();
+ videoEvent.invocation.type = 'wan_l2v';
+ // ``isVideoField`` accepts any object with a non-empty ``video_name`` string.
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
+ (videoEvent as any).result = { video: { video_name: 'fresh-video.mp4' } };
+
+ await handler(videoEvent);
+
+ const galleryInvalidation = dispatched.find((action): action is { type: string; payload: unknown[] } => {
+ if (!action || typeof action !== 'object') {
+ return false;
+ }
+ const payload = (action as { payload?: unknown }).payload;
+ return Array.isArray(payload) && payload.includes('GalleryItemNameList');
+ });
+
+ expect(galleryInvalidation, 'video completion must invalidate the polymorphic gallery cache').toBeDefined();
+ // The same invalidation must cover the board caches for the video's board.
+ expect(galleryInvalidation?.payload).toContainEqual({ type: 'Board', id: 'board-123' });
+ expect(galleryInvalidation?.payload).toContainEqual({ type: 'BoardVideosTotal', id: 'board-123' });
+ expect(galleryInvalidation?.payload).toContain('VirtualBoards');
});
});
describe('buildOnForeignInvocationComplete', () => {
beforeEach(() => {
vi.clearAllMocks();
- vi.mocked(getImageDTOSafe).mockResolvedValue(imageDTO);
});
- it('refreshes gallery caches via tag invalidation only', () => {
- const dispatch = vi.fn();
+ const buildForeignHandler = () => {
+ const dispatched: unknown[] = [];
+ const dispatch = vi.fn((action: unknown) => {
+ dispatched.push(action);
+ });
const handler = buildOnForeignInvocationComplete(dispatch as never);
+ return { handler, dispatch, dispatched };
+ };
+
+ it('refreshes gallery caches via tag invalidation only — no DTO fetches, no selection changes', () => {
+ const { handler, dispatch, dispatched } = buildForeignHandler();
- handler(buildEvent());
+ handler(buildImageCompleteEvent());
// Exactly one dispatch: the tag invalidation. No optimistic cache edits, no board/image
// selection, no progress clear.
expect(dispatch).toHaveBeenCalledTimes(1);
- expect(api.util.invalidateTags).toHaveBeenCalledWith(FOREIGN_GALLERY_REFRESH_TAGS);
- expect(dispatch).toHaveBeenCalledWith({ type: 'api/invalidateTags', payload: FOREIGN_GALLERY_REFRESH_TAGS });
+ const action = dispatched[0] as { payload?: unknown };
+ expect(action.payload).toEqual(FOREIGN_GALLERY_REFRESH_TAGS);
+ // The WAN gallery reads polymorphic (image+video) caches, so the foreign refresh must
+ // cover both media types plus board totals.
expect(FOREIGN_GALLERY_REFRESH_TAGS).toEqual(
- expect.arrayContaining(['ImageNameList', 'BoardImagesTotal', 'VirtualBoards'])
+ expect.arrayContaining([
+ 'ImageNameList',
+ 'BoardImagesTotal',
+ 'VideoNameList',
+ 'BoardVideosTotal',
+ 'GalleryItemList',
+ 'GalleryItemNameList',
+ 'VirtualBoards',
+ ])
);
-
- const types = dispatchedActionTypes(dispatch);
- expect(types).not.toContain(boardIdSelected.type);
- expect(types).not.toContain(imageSelected.type);
- expect($lastProgressEvent.set).not.toHaveBeenCalled();
+ expect(getImageDTOSafe).not.toHaveBeenCalled();
+ expect(getVideoDTOSafe).not.toHaveBeenCalled();
});
- it('never fetches image DTOs', () => {
- const dispatch = vi.fn();
- const handler = buildOnForeignInvocationComplete(dispatch as never);
+ it('invalidates gallery caches for foreign video outputs', () => {
+ const { handler, dispatch } = buildForeignHandler();
- handler(buildEvent());
+ const videoEvent = buildImageCompleteEvent();
+ videoEvent.invocation.type = 'wan_l2v';
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
+ (videoEvent as any).result = { video: { video_name: 'foreign-video.mp4' } };
- expect(getImageDTOSafe).not.toHaveBeenCalled();
+ handler(videoEvent);
+
+ expect(dispatch).toHaveBeenCalledTimes(1);
});
- it('does nothing for results without image outputs', () => {
- const dispatch = vi.fn();
- const handler = buildOnForeignInvocationComplete(dispatch as never);
+ it('does nothing for results without gallery outputs', () => {
+ const { handler, dispatch } = buildForeignHandler();
+
+ const latentsEvent = buildImageCompleteEvent();
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
+ (latentsEvent as any).result = { latents: { latents_name: 'latents-1' } };
- handler(buildEvent({ result: { latents: { latents_name: 'latents-1' } } }));
+ handler(latentsEvent);
expect(dispatch).not.toHaveBeenCalled();
});
it('does nothing for denylisted passthrough node types', () => {
- const dispatch = vi.fn();
- const handler = buildOnForeignInvocationComplete(dispatch as never);
+ const { handler, dispatch } = buildForeignHandler();
+
+ const denylisted = buildImageCompleteEvent();
+ denylisted.invocation.type = 'image';
- handler(buildEvent({ invocation: { id: 'node-1', type: 'image' } }));
+ handler(denylisted);
expect(dispatch).not.toHaveBeenCalled();
});
- it('does nothing for intermediate image outputs, which never appear in the gallery', () => {
- const dispatch = vi.fn();
- const handler = buildOnForeignInvocationComplete(dispatch as never);
+ it('does nothing for intermediate outputs, which never appear in the gallery', () => {
+ const { handler, dispatch } = buildForeignHandler();
+
+ const intermediate = buildImageCompleteEvent();
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
+ (intermediate.invocation as any).is_intermediate = true;
- handler(buildEvent({ invocation: { id: 'node-1', type: 'l2i', is_intermediate: true } }));
+ handler(intermediate);
expect(dispatch).not.toHaveBeenCalled();
});
diff --git a/invokeai/frontend/web/src/services/events/onInvocationComplete.tsx b/invokeai/frontend/web/src/services/events/onInvocationComplete.tsx
index 13233a0c04f..5318043749d 100644
--- a/invokeai/frontend/web/src/services/events/onInvocationComplete.tsx
+++ b/invokeai/frontend/web/src/services/events/onInvocationComplete.tsx
@@ -10,15 +10,18 @@ import {
} from 'features/gallery/store/gallerySelectors';
import { boardIdSelected, galleryViewChanged, imageSelected } from 'features/gallery/store/gallerySlice';
import { $nodeExecutionStates, upsertExecutionState } from 'features/nodes/hooks/useNodeExecutionState';
-import { isImageField, isImageFieldCollection } from 'features/nodes/types/common';
+import { isImageField, isImageFieldCollection, isVideoField } from 'features/nodes/types/common';
import type { ApiTagDescription } from 'services/api';
import { api, LIST_ALL_TAG, LIST_TAG } from 'services/api';
import { boardsApi } from 'services/api/endpoints/boards';
+import { galleryApi } from 'services/api/endpoints/gallery';
import { getImageDTOSafe, imagesApi } from 'services/api/endpoints/images';
import { queueApi } from 'services/api/endpoints/queue';
-import type { ImageDTO, S } from 'services/api/types';
+import { getVideoDTOSafe } from 'services/api/endpoints/videos';
+import type { ImageDTO, S, VideoDTO } from 'services/api/types';
import { getCategories } from 'services/api/util';
import { insertImageIntoNamesResult } from 'services/api/util/optimisticUpdates';
+import { getTagsToInvalidateForBoardAffectingMutation } from 'services/api/util/tagInvalidation';
import { getUpdatedNodeExecutionStateOnInvocationComplete } from 'services/events/nodeExecutionState';
import { $lastProgressEvent } from 'services/events/stores';
import stableHash from 'stable-hash';
@@ -28,8 +31,11 @@ import type { JsonObject } from 'type-fest';
const log = logger('events');
-// These nodes are passthrough nodes. They do not add images to the gallery, so we must skip that handling for them.
-const nodeTypeDenylist = ['load_image', 'image'];
+// These nodes are passthrough nodes. They do not add images/videos to the gallery — their
+// outputs reference an existing asset — so we must skip the gallery handling for them.
+// Without 'video' here, a Video Primitive completing mid-run would invalidate the gallery
+// caches and auto-switch the user's selection to the node's *input* video.
+const nodeTypeDenylist = ['load_image', 'image', 'video'];
/**
* Builds the socket event handler for the current user's own invocation complete events. Adds
@@ -163,6 +169,17 @@ export const buildOnInvocationComplete = (
dispatch(imagesApi.util.invalidateTags(['VirtualBoards']));
}
+ // The optimistic updates above only touch the image-only ``getImageNames`` cache. The
+ // gallery grid actually subscribes to the polymorphic ``getGalleryItemNames`` endpoint
+ // (which interleaves images and videos by created_at), so without invalidating its tag
+ // a freshly generated image never appears in the grid until the user reloads — even
+ // though it lands correctly in board totals, image DTO cache, etc. Mirrors the same
+ // invalidation in addVideosToGallery below. A future optimization could insert into the
+ // polymorphic cache shape directly, but the refetch cost is a single HTTP round-trip.
+ if (imageDTOs.length > 0) {
+ dispatch(galleryApi.util.invalidateTags(['GalleryItemNameList', 'GalleryItemList']));
+ }
+
const autoSwitch = selectAutoSwitch(getState());
if (!autoSwitch) {
@@ -226,6 +243,92 @@ export const buildOnInvocationComplete = (
return imageDTOs;
};
+ const getResultVideoDTOs = async (data: S['InvocationCompleteEvent']): Promise => {
+ const { result } = data;
+ const videoDTOs: VideoDTO[] = [];
+ for (const [_name, value] of objectEntries(result)) {
+ if (isVideoField(value)) {
+ const videoDTO = await getVideoDTOSafe(value.video_name);
+ if (videoDTO) {
+ videoDTOs.push(videoDTO);
+ }
+ }
+ }
+ return videoDTOs;
+ };
+
+ // Counterpart to addImagesToGallery for VideoField outputs (e.g. Wan 2.2 latents-to-video).
+ // Two key differences from the image path:
+ // 1. The gallery view uses the polymorphic getGalleryItemNames endpoint and we have no
+ // cheap optimistic-insert here, so we invalidate the GalleryItemNameList/GalleryItemList
+ // tags to force a refetch.
+ // 2. The ImageViewerContext's local $progressEvent/$progressImage atoms expect onLoadImage
+ // (DndImage onLoad) to clear them. When auto-switching to a video, the viewer swaps
+ // CurrentImagePreview for CurrentVideoPreview, which unmounts the stale progress overlay
+ // so the stuck "Saving video" spinner goes away on its own.
+ const addVideosToGallery = async (data: S['InvocationCompleteEvent']) => {
+ if (nodeTypeDenylist.includes(data.invocation.type)) {
+ return;
+ }
+
+ const videoDTOs = await getResultVideoDTOs(data);
+ if (videoDTOs.length === 0) {
+ return;
+ }
+
+ const nonIntermediate = videoDTOs.filter((v) => !v.is_intermediate);
+ if (nonIntermediate.length === 0) {
+ return;
+ }
+
+ // Force the polymorphic gallery list to refetch so the new video shows up. Note: this is
+ // a tag invalidation, not an optimistic insert (the image path has a `insertImageIntoNamesResult`
+ // helper, but the polymorphic `GetGalleryItemNamesResult` has a different shape and we don't
+ // have an equivalent yet). The viewer selection below applies immediately, so the user sees
+ // their video right away; the *gallery grid* scroll-to-selection is delayed by one refetch
+ // because `useKeepSelectedImageInView` re-runs when `imageNames` updates and only then can
+ // it find the new name in the list. Worth a follow-up if the scroll lag becomes noticeable.
+ //
+ // The board-affecting helper also invalidates each board's `Board` tag (listAllBoards →
+ // video_count and cover_video_name), `BoardVideosTotal`, and `VirtualBoards`, so board
+ // counts and cover thumbnails don't lag behind the gallery grid until the next mutation.
+ const affectedBoards = [...new Set(nonIntermediate.map((v) => v.board_id ?? 'none'))];
+ dispatch(galleryApi.util.invalidateTags(getTagsToInvalidateForBoardAffectingMutation(affectedBoards)));
+
+ const autoSwitch = selectAutoSwitch(getState());
+ if (!autoSwitch) {
+ return;
+ }
+
+ const lastVideoDTO = nonIntermediate.at(-1);
+ if (!lastVideoDTO) {
+ return;
+ }
+
+ const { video_name } = lastVideoDTO;
+ const board_id = lastVideoDTO.board_id ?? 'none';
+
+ // Selection is a polymorphic string[]; useGalleryItemDTO discriminates by filename extension.
+ const selectedBoardId = selectSelectedBoardId(getState());
+ if (board_id !== selectedBoardId) {
+ dispatch(
+ boardIdSelected({
+ boardId: board_id,
+ select: {
+ selection: [video_name],
+ galleryView: 'images',
+ },
+ })
+ );
+ } else {
+ const galleryView = selectGalleryView(getState());
+ if (galleryView !== 'images') {
+ dispatch(galleryViewChanged('images'));
+ }
+ dispatch(imageSelected(video_name));
+ }
+ };
+
const clearCanvasWorkflowIntegrationProcessing = (data: S['InvocationCompleteEvent']) => {
// Check if this is a canvas workflow integration result
// Results go to staging area automatically via destination = canvasSessionId
@@ -273,6 +376,7 @@ export const buildOnInvocationComplete = (
// Add images to gallery (canvas workflow integration results go to staging area automatically)
await addImagesToGallery(data);
+ await addVideosToGallery(data);
$lastProgressEvent.set(null);
};
@@ -287,6 +391,10 @@ export const buildOnInvocationComplete = (
export const FOREIGN_GALLERY_REFRESH_TAGS: ApiTagDescription[] = [
'ImageNameList',
'BoardImagesTotal',
+ 'VideoNameList',
+ 'BoardVideosTotal',
+ 'GalleryItemList',
+ 'GalleryItemNameList',
{ type: 'Board', id: LIST_TAG },
'VirtualBoards',
];
@@ -312,10 +420,10 @@ export const buildOnForeignInvocationComplete = (dispatch: AppDispatch) => {
return;
}
- const hasImageOutput = objectEntries(data.result).some(
- ([_name, value]) => isImageField(value) || isImageFieldCollection(value)
+ const hasGalleryOutput = objectEntries(data.result).some(
+ ([_name, value]) => isImageField(value) || isImageFieldCollection(value) || isVideoField(value)
);
- if (!hasImageOutput) {
+ if (!hasGalleryOutput) {
return;
}
diff --git a/pyproject.toml b/pyproject.toml
index ea577e22c13..93701b8aeb7 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -43,6 +43,8 @@ dependencies = [
"onnx==1.16.1",
"onnxruntime==1.19.2",
"opencv-contrib-python",
+ "imageio[ffmpeg]>=2.37", # video encode (for Wan 2.2 T2V/I2V output); encode behavior (macro_block_size) is version-sensitive
+ "psutil>=6", # video decode worker process-tree termination
"safetensors",
"sentencepiece==0.2.0", # 0.2.1 coredumps windows when loading t5 tokenizer
"spandrel",
diff --git a/scripts/generate_openapi_schema.py b/scripts/generate_openapi_schema.py
index 70baa194dc1..4f59f1897b3 100644
--- a/scripts/generate_openapi_schema.py
+++ b/scripts/generate_openapi_schema.py
@@ -5,7 +5,16 @@
def main():
# Change working directory to the repo root
- os.chdir(os.path.abspath(os.path.join(os.path.dirname(__file__), "..")))
+ repo_root = os.path.abspath(os.path.join(os.path.dirname(__file__), ".."))
+ os.chdir(repo_root)
+
+ # When invoked as a script, sys.path[0] is this script's directory rather than the repo
+ # root, so ``import invokeai`` would resolve via the venv install — which on systems with
+ # multi-worktree editable installs can pick up an `invokeai` namespace package missing
+ # some of this worktree's invocation modules. Prepending the repo root ensures we always
+ # import the local sources and so register every invocation declared here.
+ if sys.path[0] != repo_root:
+ sys.path.insert(0, repo_root)
from invokeai.app.api_app import app
from invokeai.app.util.custom_openapi import get_openapi_func
diff --git a/scripts/wan_diffusers_reference.py b/scripts/wan_diffusers_reference.py
new file mode 100644
index 00000000000..0e67ceac225
--- /dev/null
+++ b/scripts/wan_diffusers_reference.py
@@ -0,0 +1,85 @@
+"""Run TI2V-5B (or any Wan 2.2 Diffusers checkpoint) via the upstream
+WanPipeline directly, with the same arguments InvokeAI's wan_denoise uses.
+
+Use to A/B against InvokeAI output when image quality is questionable.
+Generates one image and saves it next to this script.
+
+Example:
+ python scripts/wan_diffusers_reference.py \
+ --model-path /home/lstein/invokeai-delete/models/ \
+ --prompt "a photograph of a young redheaded woman sitting on a three-legged stool next to a potted fern" \
+ --seed 42 --steps 40 --cfg 4.0 --width 1024 --height 1024
+"""
+
+import argparse
+from pathlib import Path
+
+import torch
+from diffusers import WanPipeline
+
+
+def main() -> None:
+ p = argparse.ArgumentParser()
+ p.add_argument("--model-path", required=True, help="Path to a Diffusers Wan model directory.")
+ p.add_argument("--prompt", required=True)
+ p.add_argument(
+ "--negative",
+ default="",
+ help="Negative prompt (default empty string — matches WanPipeline.encode_prompt behaviour).",
+ )
+ p.add_argument("--seed", type=int, default=42)
+ p.add_argument("--steps", type=int, default=40)
+ p.add_argument("--cfg", type=float, default=4.0)
+ p.add_argument("--width", type=int, default=1024)
+ p.add_argument("--height", type=int, default=1024)
+ p.add_argument("--output", default="wan_diffusers_reference.png")
+ p.add_argument(
+ "--offload",
+ choices=["model", "sequential", "none"],
+ default="model",
+ help="VRAM-saving strategy. 'model' (default) keeps one component on GPU at a time — fits TI2V-5B "
+ "in ~16 GB. 'sequential' is even more aggressive (per-module offload) and slower. "
+ "'none' loads everything to GPU at once (~24 GB+).",
+ )
+ args = p.parse_args()
+
+ print(f"Loading WanPipeline from {args.model_path} ...")
+ pipe = WanPipeline.from_pretrained(args.model_path, torch_dtype=torch.bfloat16)
+
+ if args.offload == "model":
+ # enable_model_cpu_offload puts each component (transformer, vae, text_encoder)
+ # on GPU only while it's actively running; the rest sit on CPU. Adds a little
+ # latency between stages but cuts peak VRAM dramatically.
+ pipe.enable_model_cpu_offload()
+ elif args.offload == "sequential":
+ pipe.enable_sequential_cpu_offload()
+ else:
+ pipe.to("cuda")
+
+ generator = torch.Generator(device="cuda").manual_seed(args.seed)
+
+ print(
+ f"Generating: prompt={args.prompt!r}\n"
+ f" steps={args.steps}, cfg={args.cfg}, size={args.width}x{args.height}, seed={args.seed}"
+ )
+ # num_frames=1 → image generation
+ result = pipe(
+ prompt=args.prompt,
+ negative_prompt=args.negative,
+ height=args.height,
+ width=args.width,
+ num_frames=1,
+ num_inference_steps=args.steps,
+ guidance_scale=args.cfg,
+ generator=generator,
+ output_type="pil",
+ )
+ # WanPipelineOutput.frames is a list of [PIL.Image] sequences (one per video).
+ image = result.frames[0][0]
+ out = Path(args.output)
+ image.save(out)
+ print(f"Saved {out.resolve()}")
+
+
+if __name__ == "__main__":
+ main()
diff --git a/tests/app/api/test_sliding_window_token.py b/tests/app/api/test_sliding_window_token.py
index 9a5e14f63e2..409992a65d2 100644
--- a/tests/app/api/test_sliding_window_token.py
+++ b/tests/app/api/test_sliding_window_token.py
@@ -1,18 +1,35 @@
"""Tests for SlidingWindowTokenMiddleware and token refresh behavior."""
from datetime import timedelta
+from types import SimpleNamespace
import pytest
-from fastapi import FastAPI
+from fastapi import FastAPI, Request, Response
from fastapi.testclient import TestClient
from invokeai.app.services.auth.token_service import TokenData, create_access_token, set_jwt_secret
@pytest.fixture(autouse=True)
-def _setup_jwt_secret():
+def _setup_jwt_secret(monkeypatch: pytest.MonkeyPatch):
"""Ensure JWT secret is set for all tests."""
+ from invokeai.app.api.dependencies import ApiDependencies
+
set_jwt_secret("test-secret-key-for-sliding-window-tests")
+ user = SimpleNamespace(
+ user_id="test-user",
+ email="test@test.com",
+ is_admin=False,
+ is_active=True,
+ )
+ users = SimpleNamespace(get=lambda user_id: user if user_id == user.user_id else None)
+ monkeypatch.setattr(
+ ApiDependencies,
+ "invoker",
+ SimpleNamespace(services=SimpleNamespace(users=users)),
+ raising=False,
+ )
+ return user
def _create_test_app() -> FastAPI:
@@ -38,6 +55,23 @@ async def put_endpoint():
async def delete_endpoint():
return {"ok": True}
+ @test_app.post("/api/v1/auth/logout")
+ async def logout_endpoint(response: Response):
+ response.delete_cookie("invokeai_media_token", path="/api/v1")
+ return {"ok": True}
+
+ @test_app.post("/api/v1/auth/media-cookie")
+ async def media_cookie_endpoint(request: Request, response: Response):
+ from invokeai.app.api.routers.auth import _set_media_cookie
+
+ _set_media_cookie(request, response, request.headers["authorization"][7:], 300)
+ return {"ok": True}
+
+ @test_app.post("/api/v1/auth/login")
+ async def login_endpoint(response: Response):
+ response.set_cookie("invokeai_media_token", "login-user-token", path="/api/v1")
+ return {"token": "login-user-token"}
+
return test_app
@@ -75,6 +109,43 @@ def test_get_request_does_not_return_refreshed_token(self):
response = client.get("/test", headers={"Authorization": f"Bearer {token}"})
assert response.status_code == 200
assert "X-Refreshed-Token" not in response.headers
+ assert "set-cookie" not in response.headers
+
+ def test_mutating_request_does_not_write_media_cookie_directly(self):
+ app = _create_test_app()
+ client = TestClient(app)
+ token = _make_token(expires_delta=timedelta(minutes=5))
+
+ response = client.post("/test", headers={"Authorization": f"Bearer {token}"})
+
+ assert "X-Refreshed-Token" in response.headers
+ assert "set-cookie" not in response.headers
+
+ @pytest.mark.parametrize("path", ["/api/v1/auth/logout", "/api/v1/auth/media-cookie"])
+ def test_cookie_management_endpoints_do_not_refresh_token(self, path: str):
+ app = _create_test_app()
+ client = TestClient(app)
+ token = _make_token(expires_delta=timedelta(minutes=5))
+
+ response = client.post(path, headers={"Authorization": f"Bearer {token}"})
+
+ assert "X-Refreshed-Token" not in response.headers
+ if path.endswith("logout"):
+ assert response.cookies.get("invokeai_media_token") is None
+ assert 'invokeai_media_token=""' in response.headers["set-cookie"]
+ else:
+ assert response.cookies.get("invokeai_media_token") == token
+
+ def test_login_response_is_not_overwritten_by_existing_bearer_token(self):
+ app = _create_test_app()
+ client = TestClient(app)
+ existing_token = _make_token(expires_delta=timedelta(minutes=5))
+
+ response = client.post("/api/v1/auth/login", headers={"Authorization": f"Bearer {existing_token}"})
+
+ assert "X-Refreshed-Token" not in response.headers
+ assert response.cookies.get("invokeai_media_token") == "login-user-token"
+ assert len(response.headers.get_list("set-cookie")) == 1
def test_unauthenticated_request_does_not_return_refreshed_token(self):
"""Requests without a token do NOT return X-Refreshed-Token."""
@@ -166,3 +237,76 @@ def test_remember_me_claim_preserved_in_refreshed_token(self):
refreshed_data = verify_token(response.headers["X-Refreshed-Token"])
assert refreshed_data is not None
assert refreshed_data.remember_me is False
+
+ def test_refresh_uses_current_user_role_and_email(self, _setup_jwt_secret):
+ from invokeai.app.services.auth.token_service import verify_token
+
+ user = _setup_jwt_secret
+ stale_token = create_access_token(
+ TokenData(
+ user_id=user.user_id,
+ email="old@test.com",
+ is_admin=True,
+ remember_me=True,
+ )
+ )
+ user.email = "new@test.com"
+ user.is_admin = False
+
+ response = TestClient(_create_test_app()).post("/test", headers={"Authorization": f"Bearer {stale_token}"})
+
+ refreshed = verify_token(response.headers["X-Refreshed-Token"])
+ assert refreshed is not None
+ assert refreshed.email == "new@test.com"
+ assert refreshed.is_admin is False
+ assert refreshed.remember_me is True
+
+ def test_inactive_or_deleted_user_is_not_refreshed(self, _setup_jwt_secret):
+ user = _setup_jwt_secret
+ token = _make_token()
+ user.is_active = False
+
+ response = TestClient(_create_test_app()).post("/test", headers={"Authorization": f"Bearer {token}"})
+
+ assert "X-Refreshed-Token" not in response.headers
+ assert "set-cookie" not in response.headers
+
+
+class TestSlidingWindowBehindSubPathProxy:
+ """Behind a sub-path proxy the middleware sees prefixed public paths; the refreshed
+ media cookie must be scoped to the prefixed path and the auth-route exclusions must
+ still match."""
+
+ BASE_PATH = "/invoke"
+
+ def _proxied_client(self, preserve: bool) -> TestClient:
+ from invokeai.app.api_app import SubPathASGIMiddleware
+
+ wrapped = SubPathASGIMiddleware(_create_test_app(), self.BASE_PATH)
+ root = f"http://testserver{self.BASE_PATH}" if preserve else "http://testserver"
+ return TestClient(wrapped, base_url=root)
+
+ @pytest.mark.parametrize("preserve", [True, False], ids=["preserve", "strip"])
+ def test_media_cookie_refresh_is_scoped_to_public_prefix(self, preserve: bool):
+ client = self._proxied_client(preserve)
+ token = _make_token(expires_delta=timedelta(minutes=5))
+
+ mutation_response = client.post("/test", headers={"Authorization": f"Bearer {token}"})
+ refreshed_token = mutation_response.headers["X-Refreshed-Token"]
+ response = client.post("/api/v1/auth/media-cookie", headers={"Authorization": f"Bearer {refreshed_token}"})
+
+ assert response.cookies.get("invokeai_media_token") == refreshed_token
+ assert f"Path={self.BASE_PATH}/api/v1" in response.headers["set-cookie"]
+
+ @pytest.mark.parametrize("preserve", [True, False], ids=["preserve", "strip"])
+ @pytest.mark.parametrize("path", ["/api/v1/auth/login", "/api/v1/auth/logout", "/api/v1/auth/media-cookie"])
+ def test_auth_route_exclusions_match_under_prefix(self, preserve: bool, path: str):
+ """The exclusion list is unprefixed; the middleware must strip root_path before
+ matching or a proxied logout would mint a replacement token/cookie."""
+ client = self._proxied_client(preserve)
+ token = _make_token(expires_delta=timedelta(minutes=5))
+
+ response = client.post(path, headers={"Authorization": f"Bearer {token}"})
+
+ assert response.status_code == 200
+ assert "X-Refreshed-Token" not in response.headers
diff --git a/tests/app/api/test_video_upload_limits.py b/tests/app/api/test_video_upload_limits.py
new file mode 100644
index 00000000000..24824582c32
--- /dev/null
+++ b/tests/app/api/test_video_upload_limits.py
@@ -0,0 +1,486 @@
+"""Tests for VideoUploadLimitASGIMiddleware (PR #9163 review fix).
+
+The upload route's MAX_UPLOAD_SIZE check runs only after FastAPI has parsed (and spooled)
+the entire multipart body, so oversized/chunked/concurrent requests could exhaust temp
+storage before rejection. The middleware bounds ingress before the parser runs.
+"""
+
+import asyncio
+import tempfile
+from pathlib import Path
+from types import SimpleNamespace
+from typing import Any
+from unittest.mock import AsyncMock, MagicMock, patch
+
+import pytest
+from fastapi import FastAPI, Response, UploadFile
+from fastapi.testclient import TestClient
+from starlette.datastructures import Headers
+
+from invokeai.app import api_app
+from invokeai.app.api.routers import videos
+from invokeai.app.api.routers.videos import upload_video
+from invokeai.app.api_app import (
+ SubPathASGIMiddleware,
+ VideoUploadLimitASGIMiddleware,
+ _identify_video_upload_user,
+)
+from invokeai.app.services.auth.token_service import TokenData, create_access_token, set_jwt_secret
+from invokeai.app.services.image_records.image_records_common import ImageCategory
+from invokeai.app.util.video_thumbnails import VideoDecodeTimeoutError
+
+MAX_BODY = 1024 # tiny cap for tests
+MAX_CONCURRENT = 2
+
+
+def test_upload_probe_requires_a_decodable_frame(monkeypatch: pytest.MonkeyPatch):
+ monkeypatch.setattr(videos, "probe_video", lambda path: (48, 32, 1.0, 8.0))
+ monkeypatch.setattr(videos, "extract_video_frame", lambda path, frame_index=0, raise_on_timeout=False: None)
+
+ with pytest.raises(ValueError, match="decodable frame"):
+ videos._probe_decodable_video(Path("metadata-only.mp4"))
+
+
+def test_upload_probe_accepts_valid_metadata_and_frame(monkeypatch: pytest.MonkeyPatch):
+ frame = MagicMock()
+ monkeypatch.setattr(videos, "probe_video", lambda path: (48, 32, 1.0, 8.0))
+ monkeypatch.setattr(videos, "extract_video_frame", lambda path, frame_index=0, raise_on_timeout=False: frame)
+
+ assert videos._probe_decodable_video(Path("valid.mp4")) == ((48, 32, 1.0, 8.0), frame)
+
+
+def test_upload_probe_timeout_is_inconclusive_not_a_rejection(monkeypatch: pytest.MonkeyPatch):
+ """A decode-worker timeout is server contention, not evidence the video is bad — the
+ upload must proceed (without a pre-extracted frame) rather than 415."""
+
+ def _timeout(path, frame_index=0, raise_on_timeout=False):
+ raise VideoDecodeTimeoutError("decode worker timed out")
+
+ monkeypatch.setattr(videos, "probe_video", lambda path: (48, 32, 1.0, 8.0))
+ monkeypatch.setattr(videos, "extract_video_frame", _timeout)
+
+ assert videos._probe_decodable_video(Path("busy-server.mp4")) == ((48, 32, 1.0, 8.0), None)
+
+
+def _build_app() -> tuple[FastAPI, list[str]]:
+ app = FastAPI()
+ calls: list[str] = []
+
+ @app.post("/api/v1/videos/upload")
+ async def upload() -> dict[str, bool]:
+ calls.append("upload")
+ return {"ok": True}
+
+ @app.post("/api/v1/images/upload")
+ async def other() -> dict[str, bool]:
+ calls.append("other")
+ return {"ok": True}
+
+ return app, calls
+
+
+def _call_asgi(app: VideoUploadLimitASGIMiddleware, path: str, authorization: str | None = None) -> int:
+ headers = [(b"content-length", b"1")]
+ if authorization is not None:
+ headers.append((b"authorization", authorization.encode()))
+ scope = {
+ "type": "http",
+ "asgi": {"version": "3.0"},
+ "http_version": "1.1",
+ "method": "POST",
+ "scheme": "http",
+ "path": path,
+ "raw_path": path.encode(),
+ "query_string": b"",
+ "headers": headers,
+ "client": ("testclient", 50000),
+ "server": ("testserver", 80),
+ "root_path": "",
+ }
+ messages: list[dict[str, Any]] = []
+
+ async def receive() -> dict[str, Any]:
+ return {"type": "http.request", "body": b"x", "more_body": False}
+
+ async def send(message: dict[str, Any]) -> None:
+ messages.append(message)
+
+ asyncio.run(app(scope, receive, send)) # type: ignore[arg-type]
+ return next(message["status"] for message in messages if message["type"] == "http.response.start")
+
+
+def _client(base_path: str | None = None) -> tuple[TestClient, list[str]]:
+ app, calls = _build_app()
+ wrapped = VideoUploadLimitASGIMiddleware(app, max_body_bytes=MAX_BODY, max_concurrent=MAX_CONCURRENT)
+ if base_path is not None:
+ wrapped = SubPathASGIMiddleware(wrapped, base_path)
+ return TestClient(wrapped, base_url=f"http://testserver{base_path}"), calls
+ return TestClient(wrapped), calls
+
+
+def test_oversized_content_length_rejected_before_route_runs():
+ client, calls = _client()
+ response = client.post("/api/v1/videos/upload", content=b"x" * (MAX_BODY + 1))
+ assert response.status_code == 413
+ assert calls == []
+
+
+def test_within_limit_passes_through():
+ client, calls = _client()
+ response = client.post("/api/v1/videos/upload", content=b"x" * 10)
+ assert response.status_code == 200
+ assert calls == ["upload"]
+
+
+def test_other_routes_unaffected():
+ client, calls = _client()
+ response = client.post("/api/v1/images/upload", content=b"x" * (MAX_BODY + 1))
+ assert response.status_code == 200
+ assert calls == ["other"]
+
+
+def test_concurrency_bound_returns_429():
+ app, calls = _build_app()
+ middleware = VideoUploadLimitASGIMiddleware(app, max_body_bytes=MAX_BODY, max_concurrent=MAX_CONCURRENT)
+ middleware._active = MAX_CONCURRENT # simulate saturated uploads
+ client = TestClient(middleware)
+
+ response = client.post("/api/v1/videos/upload", content=b"x")
+
+ assert response.status_code == 429
+ assert response.headers.get("retry-after") is not None
+ assert calls == []
+
+
+def test_active_count_released_after_request():
+ app, _calls = _build_app()
+ middleware = VideoUploadLimitASGIMiddleware(app, max_body_bytes=MAX_BODY, max_concurrent=MAX_CONCURRENT)
+ client = TestClient(middleware)
+
+ client.post("/api/v1/videos/upload", content=b"x")
+
+ assert middleware._active == 0
+
+
+def test_idle_chunked_upload_releases_capacity():
+ messages: list[dict[str, Any]] = []
+ receive_calls = 0
+
+ async def app(scope, receive, send):
+ while (message := await receive())["type"] == "http.request" and message.get("more_body"):
+ pass
+ await Response(status_code=400)(scope, receive, send)
+
+ async def receive() -> dict[str, Any]:
+ nonlocal receive_calls
+ receive_calls += 1
+ if receive_calls == 1:
+ return {"type": "http.request", "body": b"x", "more_body": True}
+ await asyncio.Event().wait()
+ raise AssertionError("unreachable")
+
+ async def send(message: dict[str, Any]) -> None:
+ messages.append(message)
+
+ scope = {
+ "type": "http",
+ "method": "POST",
+ "path": "/api/v1/videos/upload",
+ "root_path": "",
+ "headers": [],
+ }
+ middleware = VideoUploadLimitASGIMiddleware(
+ app,
+ max_body_bytes=MAX_BODY,
+ max_concurrent=1,
+ idle_timeout_seconds=0.01,
+ )
+
+ asyncio.run(asyncio.wait_for(middleware(scope, receive, send), timeout=1)) # type: ignore[arg-type]
+
+ assert middleware._active == 0
+ assert any(message.get("status") == 400 for message in messages)
+
+
+def test_drip_feed_upload_cannot_hold_capacity_past_absolute_timeout():
+ receive_calls = 0
+
+ async def app(scope, receive, send):
+ while (await receive())["type"] == "http.request":
+ pass
+ await Response(status_code=400)(scope, receive, send)
+
+ async def receive() -> dict[str, Any]:
+ nonlocal receive_calls
+ receive_calls += 1
+ await asyncio.sleep(0.005)
+ return {"type": "http.request", "body": b"x", "more_body": True}
+
+ async def send(_message: dict[str, Any]) -> None:
+ pass
+
+ scope = {
+ "type": "http",
+ "method": "POST",
+ "path": "/api/v1/videos/upload",
+ "root_path": "",
+ "headers": [],
+ }
+ # The absolute deadline must dwarf the per-chunk delay: Windows event-loop timers
+ # have ~15.6 ms granularity, so a 20 ms deadline could expire before the first
+ # "5 ms" chunk was ever delivered, ending the request at receive_calls == 1 and
+ # proving nothing about the drip-feed case.
+ middleware = VideoUploadLimitASGIMiddleware(
+ app,
+ max_body_bytes=MAX_BODY,
+ max_concurrent=1,
+ idle_timeout_seconds=1.0,
+ max_upload_duration_seconds=0.25,
+ )
+
+ asyncio.run(asyncio.wait_for(middleware(scope, receive, send), timeout=5)) # type: ignore[arg-type]
+
+ assert receive_calls > 1
+ assert middleware._active == 0
+
+
+def _identify_by_bearer(scope: Any) -> tuple[bool, str | None]:
+ """Test identify_user: 'Bearer ' authenticates as user ."""
+ authorization = Headers(scope=scope).get("authorization")
+ if authorization is None or not authorization.startswith("Bearer "):
+ return False, None
+ return True, authorization.removeprefix("Bearer ")
+
+
+def test_unauthenticated_upload_is_rejected_without_consuming_capacity():
+ app, calls = _build_app()
+ middleware = VideoUploadLimitASGIMiddleware(
+ app,
+ max_body_bytes=MAX_BODY,
+ max_concurrent=MAX_CONCURRENT,
+ identify_user=_identify_by_bearer,
+ )
+ middleware._active = MAX_CONCURRENT
+
+ status_code = _call_asgi(middleware, "/api/v1/videos/upload")
+
+ assert status_code == 401
+ assert middleware._active == MAX_CONCURRENT
+ assert middleware._active_by_user == {}
+ assert calls == []
+
+
+def test_authenticated_upload_still_obeys_concurrency_bound():
+ app, calls = _build_app()
+ middleware = VideoUploadLimitASGIMiddleware(
+ app,
+ max_body_bytes=MAX_BODY,
+ max_concurrent=MAX_CONCURRENT,
+ identify_user=_identify_by_bearer,
+ )
+ middleware._active = MAX_CONCURRENT
+
+ status_code = _call_asgi(middleware, "/api/v1/videos/upload", authorization="Bearer valid")
+
+ assert status_code == 429
+ assert calls == []
+
+
+def test_per_user_quota_blocks_only_the_saturating_user():
+ """One tenant at their per-user cap gets 429 while another tenant still uploads,
+ even though global capacity remains."""
+ app, calls = _build_app()
+ middleware = VideoUploadLimitASGIMiddleware(
+ app,
+ max_body_bytes=MAX_BODY,
+ max_concurrent=4,
+ max_concurrent_per_user=2,
+ identify_user=_identify_by_bearer,
+ )
+ middleware._active = 2
+ middleware._active_by_user = {"alice": 2}
+
+ status_a = _call_asgi(middleware, "/api/v1/videos/upload", authorization="Bearer alice")
+ status_b = _call_asgi(middleware, "/api/v1/videos/upload", authorization="Bearer bob")
+
+ assert status_a == 429
+ assert status_b == 200
+ assert calls == ["upload"]
+
+
+def test_per_user_count_released_after_request():
+ app, _calls = _build_app()
+ middleware = VideoUploadLimitASGIMiddleware(
+ app,
+ max_body_bytes=MAX_BODY,
+ max_concurrent=4,
+ max_concurrent_per_user=2,
+ identify_user=_identify_by_bearer,
+ )
+
+ status = _call_asgi(middleware, "/api/v1/videos/upload", authorization="Bearer alice")
+
+ assert status == 200
+ assert middleware._active == 0
+ assert middleware._active_by_user == {}
+
+
+def test_single_user_mode_has_no_per_user_quota():
+ """identify_user returning (True, None) — single-user mode — must get the whole
+ global capacity, not be clipped by the per-user cap."""
+ app, calls = _build_app()
+ middleware = VideoUploadLimitASGIMiddleware(
+ app,
+ max_body_bytes=MAX_BODY,
+ max_concurrent=4,
+ max_concurrent_per_user=2,
+ identify_user=lambda scope: (True, None),
+ )
+ middleware._active = 3 # above the per-user cap, below the global cap
+
+ status = _call_asgi(middleware, "/api/v1/videos/upload")
+
+ assert status == 200
+ assert calls == ["upload"]
+
+
+@pytest.mark.parametrize(
+ "multiuser,send_token,user_active,expected",
+ [
+ (False, False, False, (True, None)),
+ (True, False, True, (False, None)),
+ (True, True, False, (False, None)),
+ (True, True, True, (True, "user")),
+ ],
+)
+def test_production_upload_authentication(
+ monkeypatch: pytest.MonkeyPatch,
+ multiuser: bool,
+ send_token: bool,
+ user_active: bool,
+ expected: tuple[bool, str | None],
+) -> None:
+ set_jwt_secret("test-secret-key-for-unit-tests-only-do-not-use-in-production")
+ token_data = TokenData(user_id="user", email="user@example.com", is_admin=False)
+ user = SimpleNamespace(is_active=user_active)
+ invoker = SimpleNamespace(
+ services=SimpleNamespace(
+ configuration=SimpleNamespace(multiuser=multiuser),
+ users=SimpleNamespace(get=lambda _user_id: user),
+ )
+ )
+ monkeypatch.setattr(api_app.ApiDependencies, "invoker", invoker, raising=False)
+ headers = []
+ if send_token:
+ headers.append((b"authorization", f"Bearer {create_access_token(token_data)}".encode()))
+
+ assert _identify_video_upload_user({"headers": headers}) == expected # type: ignore[arg-type]
+
+
+def test_upload_video_closes_tmp_handle_when_stream_copy_fails():
+ captured_handles: list[Any] = []
+ real_named_tmp = tempfile.NamedTemporaryFile
+
+ async def run_immediately(func: Any, *args: Any):
+ return func(*args)
+
+ def failing_named_tmp(*args: Any, **kwargs: Any):
+ handle = real_named_tmp(*args, **kwargs)
+ handle.write = MagicMock(side_effect=OSError("disk full"))
+ captured_handles.append(handle)
+ return handle
+
+ upload = MagicMock(spec=UploadFile)
+ upload.filename = "video.mp4"
+ upload.content_type = "video/mp4"
+ upload.read = AsyncMock(side_effect=[b"not-empty", b""])
+
+ try:
+ with (
+ patch("invokeai.app.api.routers.videos.tempfile.NamedTemporaryFile", side_effect=failing_named_tmp),
+ patch("invokeai.app.api.routers.videos.run_in_threadpool", side_effect=run_immediately),
+ pytest.raises(OSError, match="disk full"),
+ ):
+ asyncio.run(
+ upload_video(
+ current_user=TokenData(user_id="user", email="user@example.com", is_admin=False),
+ file=upload,
+ request=MagicMock(),
+ response=Response(),
+ video_category=ImageCategory.GENERAL,
+ is_intermediate=False,
+ board_id=None,
+ session_id=None,
+ metadata=None,
+ )
+ )
+
+ assert len(captured_handles) == 1
+ assert captured_handles[0].closed
+ assert not Path(captured_handles[0].name).exists()
+ finally:
+ for handle in captured_handles:
+ handle.close()
+ Path(handle.name).unlink(missing_ok=True)
+
+
+@pytest.mark.parametrize("preserve", [True, False], ids=["preserve", "strip"])
+def test_route_matching_is_root_path_aware(preserve: bool):
+ """Behind a sub-path proxy the public path carries the prefix; the size cap must still
+ apply to the proxied upload URL. SubPathASGIMiddleware wraps this middleware in
+ run_app (it restores the prefix and advertises root_path before this middleware
+ runs), so path matching must strip root_path for both proxy styles."""
+ app, calls = _build_app()
+ limited = VideoUploadLimitASGIMiddleware(app, max_body_bytes=MAX_BODY, max_concurrent=MAX_CONCURRENT)
+ wrapped = SubPathASGIMiddleware(limited, "/invoke")
+ root = "http://testserver/invoke" if preserve else "http://testserver"
+ client = TestClient(wrapped, base_url=root)
+
+ response = client.post("/api/v1/videos/upload", content=b"x" * (MAX_BODY + 1))
+
+ assert response.status_code == 413
+ assert calls == []
+
+
+def test_chunked_body_aborted_once_over_cap():
+ """Without a Content-Length header the counting backstop must stop feeding the app
+ once the cap is exceeded — the app sees a disconnect instead of an unbounded body."""
+ import asyncio
+
+ seen: list[dict] = []
+
+ async def app(scope, receive, send):
+ while True:
+ message = await receive()
+ seen.append(message)
+ if message["type"] != "http.request" or not message.get("more_body"):
+ break
+ await send({"type": "http.response.start", "status": 200, "headers": []})
+ await send({"type": "http.response.body", "body": b""})
+
+ middleware = VideoUploadLimitASGIMiddleware(app, max_body_bytes=MAX_BODY, max_concurrent=MAX_CONCURRENT)
+
+ scope = {
+ "type": "http",
+ "method": "POST",
+ "path": "/api/v1/videos/upload",
+ "root_path": "",
+ "headers": [], # no content-length -> chunked path
+ }
+
+ chunks = [b"x" * 600, b"x" * 600] # 1200 > MAX_BODY
+
+ async def receive():
+ if chunks:
+ return {"type": "http.request", "body": chunks.pop(0), "more_body": bool(chunks)}
+ return {"type": "http.disconnect"}
+
+ async def send(message):
+ pass
+
+ asyncio.run(middleware(scope, receive, send))
+
+ assert seen[-1]["type"] == "http.disconnect"
+ # Only the first chunk (under the cap) reached the app as a body message.
+ body_bytes = sum(len(m.get("body", b"")) for m in seen if m["type"] == "http.request")
+ assert body_bytes == 600
diff --git a/tests/app/invocations/test_video_concat.py b/tests/app/invocations/test_video_concat.py
new file mode 100644
index 00000000000..03c77155690
--- /dev/null
+++ b/tests/app/invocations/test_video_concat.py
@@ -0,0 +1,230 @@
+"""Regression tests for VideoConcatInvocation._iter_joined_frames.
+
+Covers two JPPhoto code-review findings (PR #9163):
+
+1. ``fade_through_black`` claimed to emit ``transition_frames`` frames per boundary but
+ used a symmetric ``tf // 2`` split, dropping one frame for odd ``tf``. The fix splits
+ asymmetrically: ``tail_half = tf // 2`` consumed from the previous clip's tail,
+ ``head_half = tf - tail_half`` from the next clip's head, so the emitted total is
+ exactly ``tf`` for both parities.
+
+2. The node fully decoded every input into RAM before encoding, so a long upload (the
+ API admits 1 GB compressed) could expand to tens of gigabytes of frames. Joining is
+ now a generator that pulls frames lazily and buffers only the transition windows —
+ the streaming tests pin that output begins before the inputs are exhausted and that
+ look-ahead stays bounded by ``transition_frames``.
+
+We exercise ``_iter_joined_frames`` directly because it is the pure transformation
+implementing the contract (the surrounding ``invoke()`` deals with imageio
+encode/decode plumbing that isn't germane to the math).
+"""
+
+from typing import Iterator
+
+import numpy as np
+import pytest
+
+from invokeai.app.invocations.fields import VideoField
+from invokeai.app.invocations.video_concat import MAX_TRANSITION_MEMORY_BYTES, VideoConcatInvocation, _crossfade
+from invokeai.app.services.session_processor.session_processor_common import CanceledException
+
+
+def _invocation(transition: str, transition_frames: int) -> VideoConcatInvocation:
+ # ``videos`` requires min_length=2 to construct; values are unused by ``_iter_joined_frames``.
+ return VideoConcatInvocation(
+ videos=[VideoField(video_name="a"), VideoField(video_name="b")],
+ transition=transition, # type: ignore[arg-type]
+ transition_frames=transition_frames,
+ )
+
+
+def _clip(value: int, n: int) -> list[np.ndarray]:
+ return [np.full((4, 4, 3), value, dtype=np.uint8) for _ in range(n)]
+
+
+class TestFadeThroughBlackOddTf:
+ """fade_through_black must emit exactly tf frames per boundary, even for odd tf."""
+
+ @pytest.mark.parametrize("tf", [1, 2, 3, 4, 5, 7, 8])
+ def test_total_length_preserved(self, tf: int) -> None:
+ clip_a = _clip(200, 10)
+ clip_b = _clip(100, 10)
+ v = _invocation("fade_through_black", tf)
+ out = list(v._iter_joined_frames([clip_a, clip_b]))
+ # fade_through_black: each boundary consumes tf frames (tail_half from A + head_half from B)
+ # and emits exactly tf frames in their place, so the total length is preserved.
+ assert len(out) == len(clip_a) + len(clip_b)
+
+ def test_three_clip_chain(self) -> None:
+ clip_a = _clip(200, 10)
+ clip_b = _clip(150, 10)
+ clip_c = _clip(100, 10)
+ v = _invocation("fade_through_black", 3)
+ out = list(v._iter_joined_frames([clip_a, clip_b, clip_c]))
+ # 30 input frames - 2 boundaries each consuming 3 + emitting 3 = 30 output frames.
+ assert len(out) == 30
+
+
+class TestFadeThroughBlackTransitionFrames:
+ """Validation should accept odd tf when both halves fit within their adjacent clips."""
+
+ def test_odd_tf_validation_uses_correct_halves(self) -> None:
+ # tf=5 → tail_half=2, head_half=3. A 5-frame clip would fail with the previous
+ # symmetric split (2+2 ≤ 5 was fine, but the math now needs head_half=3 from
+ # clip[1] head + tail_half=2 from clip[1] tail = 5 ≤ 5 → still fits).
+ clip_a = _clip(200, 5)
+ clip_b = _clip(100, 5)
+ clip_c = _clip(50, 5)
+ v = _invocation("fade_through_black", 5)
+ out = list(v._iter_joined_frames([clip_a, clip_b, clip_c]))
+ assert len(out) == 15
+
+
+class TestCrossfadeTransitionLength:
+ """Crossfade behaviour is unchanged; pin its length for safety."""
+
+ def test_crossfade_shortens_by_tf_per_boundary(self) -> None:
+ clip_a = _clip(200, 10)
+ clip_b = _clip(100, 10)
+ v = _invocation("crossfade", 5)
+ out = list(v._iter_joined_frames([clip_a, clip_b]))
+ # 20 input - 5 frames consumed from each side (10 total) + 5 blended emitted = 15.
+ assert len(out) == 15
+
+
+class TestCutNoTransitionFrames:
+ """transition='cut' or tf=0 returns the raw concatenation."""
+
+ def test_cut_concatenates_directly(self) -> None:
+ clip_a = _clip(200, 4)
+ clip_b = _clip(100, 6)
+ v = _invocation("cut", 0)
+ out = list(v._iter_joined_frames([clip_a, clip_b]))
+ assert len(out) == 10
+
+ def test_fade_through_black_tf_zero(self) -> None:
+ clip_a = _clip(200, 4)
+ clip_b = _clip(100, 6)
+ v = _invocation("fade_through_black", 0)
+ out = list(v._iter_joined_frames([clip_a, clip_b]))
+ assert len(out) == 10
+
+
+class TestStreamingJoin:
+ """Joining must stream: output frames are yielded before the inputs are exhausted,
+ and look-ahead into the decoders stays bounded by the transition window."""
+
+ def _tracked_clip(self, value: int, n: int, pulled: list[int]) -> Iterator[np.ndarray]:
+ def gen() -> Iterator[np.ndarray]:
+ for _ in range(n):
+ pulled[0] += 1
+ yield np.full((4, 4, 3), value, dtype=np.uint8)
+
+ return gen()
+
+ def test_cut_emits_first_frame_after_single_pull(self) -> None:
+ pulled = [0]
+ v = _invocation("cut", 0)
+ gen = v._iter_joined_frames([self._tracked_clip(200, 100, pulled), self._tracked_clip(100, 100, pulled)])
+ next(gen)
+ # Producing the first output frame must not decode the rest of either clip.
+ assert pulled[0] == 1
+
+ @pytest.mark.parametrize("transition", ["crossfade", "fade_through_black"])
+ def test_lookahead_bounded_by_transition_window(self, transition: str) -> None:
+ tf = 5
+ pulled = [0]
+ v = _invocation(transition, tf)
+ gen = v._iter_joined_frames([self._tracked_clip(200, 60, pulled), self._tracked_clip(100, 60, pulled)])
+ emitted = 0
+ max_lookahead = 0
+ for _ in gen:
+ emitted += 1
+ max_lookahead = max(max_lookahead, pulled[0] - emitted)
+ # At any moment the generator holds at most the previous clip's tail window plus
+ # the next clip's head window — never a whole decoded clip.
+ assert max_lookahead <= 2 * tf + 1
+ assert pulled[0] == 120 # every input frame was still consumed exactly once
+
+ def test_streamed_output_matches_eager_reference(self) -> None:
+ # The join must be frame-for-frame identical whether clips arrive as one-shot
+ # iterators (the decoder) or materialized lists — guarding against any accidental
+ # reliance on len()/indexing sneaking back into the streaming path.
+ clip_a = [np.full((4, 4, 3), 20 + i, dtype=np.uint8) for i in range(12)]
+ clip_b = [np.full((4, 4, 3), 120 + i, dtype=np.uint8) for i in range(12)]
+ for transition, tf in [("cut", 0), ("crossfade", 4), ("fade_through_black", 5)]:
+ v = _invocation(transition, tf)
+ streamed = list(v._iter_joined_frames([iter(clip_a), iter(clip_b)]))
+ eager = list(v._iter_joined_frames([clip_a, clip_b]))
+ assert len(streamed) == len(eager)
+ for s, e in zip(streamed, eager, strict=True):
+ assert np.array_equal(s, e)
+
+ def test_short_clip_still_raises(self) -> None:
+ v = _invocation("crossfade", 8)
+ with pytest.raises(ValueError, match="transitions need"):
+ list(v._iter_joined_frames([iter(_clip(200, 4)), iter(_clip(100, 20))]))
+
+ def test_crossfade_yields_without_materializing_all_blends(self) -> None:
+ a = _clip(200, 8)
+ b = _clip(100, 8)
+ blended = _crossfade(a, b)
+ assert isinstance(blended, Iterator)
+ assert next(blended).shape == (4, 4, 3)
+
+
+class TestResourceBounds:
+ def test_rejects_transition_window_over_memory_budget(self) -> None:
+ v = _invocation("crossfade", 240)
+ with pytest.raises(ValueError, match="memory budget"):
+ v._validate_transition_memory(width=8192, height=8192)
+
+ def test_accepts_small_transition_window(self) -> None:
+ v = _invocation("crossfade", 8)
+ assert v._estimate_transition_memory(width=512, height=512) < MAX_TRANSITION_MEMORY_BYTES
+ v._validate_transition_memory(width=512, height=512)
+
+ def test_consumed_crossfade_windows_are_released_before_buffering_the_next_tail(self) -> None:
+ v = _invocation("crossfade", 4)
+ joined = v._iter_joined_frames([iter(_clip(200, 12)), iter(_clip(100, 12)), iter(_clip(50, 12))])
+
+ for _ in range(13):
+ next(joined)
+
+ assert joined.gi_frame is not None
+ assert joined.gi_frame.f_locals["a_tail"] == []
+ assert joined.gi_frame.f_locals["b_head"] == []
+
+ def test_cancellation_stops_join(self) -> None:
+ v = _invocation("cut", 0)
+ with pytest.raises(CanceledException):
+ next(v._iter_joined_frames([iter(_clip(200, 10)), iter(_clip(100, 10))], is_canceled=lambda: True))
+
+
+class TestFrameRateValidation:
+ def test_matching_source_rates_use_first_rate(self) -> None:
+ invocation = _invocation("cut", 0)
+ assert invocation._resolve_output_fps([16.0, 16.0]) == 16.0
+
+ def test_mismatched_source_rates_require_explicit_override(self) -> None:
+ invocation = _invocation("cut", 0)
+ with pytest.raises(ValueError, match="frame rates"):
+ invocation._resolve_output_fps([16.0, 24.0])
+
+ def test_unknown_rate_mixed_with_agreeing_known_rates_uses_known_rate(self) -> None:
+ # probe_video reports None for metadata-poor containers; that must not fail a
+ # concat whose known rates agree — only disagreement between known rates errors.
+ invocation = _invocation("cut", 0)
+ assert invocation._resolve_output_fps([16.0, None, 16.0]) == 16.0
+
+ def test_all_unknown_rates_fall_back_to_default(self) -> None:
+ invocation = _invocation("cut", 0)
+ assert invocation._resolve_output_fps([None, None]) == 16.0
+
+ def test_explicit_output_rate_allows_retiming(self) -> None:
+ invocation = VideoConcatInvocation(
+ videos=[VideoField(video_name="a"), VideoField(video_name="b")],
+ transition="cut",
+ fps=20,
+ )
+ assert invocation._resolve_output_fps([16.0, 24.0]) == 20.0
diff --git a/tests/app/invocations/test_video_frame_extract.py b/tests/app/invocations/test_video_frame_extract.py
new file mode 100644
index 00000000000..7c40b2a74f6
--- /dev/null
+++ b/tests/app/invocations/test_video_frame_extract.py
@@ -0,0 +1,48 @@
+"""Regression tests for VideoFrameExtractInvocation negative-index resolution.
+
+Covers JPPhoto's code-review finding (PR #9163): the old code computed
+``n_frames = round(duration * fps)`` to resolve ``frame_index=-1``. For uploads
+with inexact metadata that can overshoot the decoded frame count, requesting
+the last frame would fail. The fix queries ``iio.improps(...).shape[0]`` for
+the exact decoder count.
+
+We exercise the ``decoder_frame_count`` helper (now subprocess-backed) with a real synthetic
+MP4 so the iio integration is actually validated.
+"""
+
+import imageio.v3 as iio
+import numpy as np
+import pytest
+
+from invokeai.app.util.video_thumbnails import decoder_frame_count
+
+
+def _write_mp4(tmp_path, n_frames: int):
+ """Encode a tiny synthetic MP4 with exactly ``n_frames`` frames at 8 fps."""
+ path = tmp_path / "synth.mp4"
+ frames = [np.full((32, 32, 3), 64 + i * 8, dtype=np.uint8) for i in range(n_frames)]
+ iio.imwrite(path, frames, plugin="FFMPEG", codec="libx264", fps=8.0, macro_block_size=1)
+ return path
+
+
+class TestDecoderFrameCountExact:
+ """decoder_frame_count returns the actual decoded count from the container."""
+
+ @pytest.mark.parametrize("n", [1, 5, 16, 33])
+ def test_matches_encoded_frame_count(self, tmp_path, n: int) -> None:
+ path = _write_mp4(tmp_path, n)
+ assert decoder_frame_count(path) == n
+
+
+class TestDecoderFrameCountGracefulFallback:
+ """decoder_frame_count returns None on unreadable inputs so the caller can fall back."""
+
+ def test_missing_path_returns_none(self, tmp_path) -> None:
+ bogus = tmp_path / "does_not_exist.mp4"
+ # Either iio raises (caught) or returns props without shape — both must yield None.
+ assert decoder_frame_count(bogus) is None
+
+ def test_non_video_file_returns_none(self, tmp_path) -> None:
+ not_a_video = tmp_path / "junk.mp4"
+ not_a_video.write_bytes(b"not actually an mp4")
+ assert decoder_frame_count(not_a_video) is None
diff --git a/tests/app/invocations/test_video_frame_extract_range.py b/tests/app/invocations/test_video_frame_extract_range.py
new file mode 100644
index 00000000000..52945ca7d1a
--- /dev/null
+++ b/tests/app/invocations/test_video_frame_extract_range.py
@@ -0,0 +1,113 @@
+"""Regression tests for ExtractVideoRangeInvocation streaming (PR #9163 review).
+
+The bug: the node collected every selected frame into a list before encoding, so the
+default ``start_frame=0, end_frame=-1`` materialized the whole source in RAM — and the
+upload API admits 1 GB compressed files whose decoded frames can run to tens of
+gigabytes. ``_write_frame_range`` now streams each frame straight into the encoder and
+stops decoding as soon as the range is written.
+"""
+
+from pathlib import Path
+from typing import Iterator
+from unittest.mock import MagicMock, patch
+
+import numpy as np
+import pytest
+
+from invokeai.app.invocations.fields import VideoField
+from invokeai.app.invocations.video_frame_extract_range import ExtractVideoRangeInvocation, _write_frame_range
+from invokeai.app.services.session_processor.session_processor_common import CanceledException
+
+
+class RecordingWriter:
+ """Stands in for the imageio writer; records what was appended and when."""
+
+ def __init__(self) -> None:
+ self.frames: list[np.ndarray] = []
+ self.pulled_at_first_append: int | None = None
+ self._pulled_ref: list[int] | None = None
+
+ def watch(self, pulled: list[int]) -> "RecordingWriter":
+ self._pulled_ref = pulled
+ return self
+
+ def append_data(self, frame: np.ndarray) -> None:
+ if self.pulled_at_first_append is None and self._pulled_ref is not None:
+ self.pulled_at_first_append = self._pulled_ref[0]
+ self.frames.append(frame)
+
+
+def _lazy_frames(n: int, pulled: list[int]) -> Iterator[np.ndarray]:
+ for i in range(n):
+ pulled[0] += 1
+ yield np.full((4, 4, 3), i % 255, dtype=np.uint8)
+
+
+def _fail_after(n: int) -> Iterator[np.ndarray]:
+ yield from _lazy_frames(n, [0])
+ raise RuntimeError("decoder advanced past requested range")
+
+
+class TestWriteFrameRangeStreams:
+ def test_encoding_begins_without_materializing_the_iterator(self) -> None:
+ pulled = [0]
+ writer = RecordingWriter().watch(pulled)
+ written = _write_frame_range(_lazy_frames(1000, pulled), writer, start=0, end=99)
+ assert written == 100
+ # The first frame must reach the encoder after a single decode, not after the
+ # whole range (let alone the whole file) has been buffered.
+ assert writer.pulled_at_first_append == 1
+
+ def test_decoding_stops_after_the_range(self) -> None:
+ pulled = [0]
+ writer = RecordingWriter().watch(pulled)
+ written = _write_frame_range(_lazy_frames(1000, pulled), writer, start=5, end=9)
+ assert written == 5
+ assert pulled[0] == 10
+
+ def test_does_not_pull_the_frame_after_end(self) -> None:
+ writer = RecordingWriter()
+ assert _write_frame_range(_fail_after(10), writer, start=5, end=9) == 5
+
+ def test_range_to_final_frame_consumes_input_exactly_once(self) -> None:
+ pulled = [0]
+ writer = RecordingWriter().watch(pulled)
+ written = _write_frame_range(_lazy_frames(24, pulled), writer, start=0, end=23)
+ assert written == 24
+ assert pulled[0] == 24
+ assert [int(f[0, 0, 0]) for f in writer.frames] == list(range(24))
+
+ def test_cancellation_stops_before_writing_more_frames(self) -> None:
+ pulled = [0]
+ writer = RecordingWriter()
+ with pytest.raises(CanceledException):
+ _write_frame_range(_lazy_frames(24, pulled), writer, start=0, end=23, is_canceled=lambda: pulled[0] >= 2)
+ assert len(writer.frames) == 1
+ assert pulled[0] == 2
+
+
+@pytest.mark.parametrize("written,should_raise", [(5, False), (3, True)])
+def test_invocation_only_saves_complete_requested_range(written: int, should_raise: bool) -> None:
+ invocation = ExtractVideoRangeInvocation(video=VideoField(video_name="input.mp4"), start_frame=0, end_frame=4)
+ context = MagicMock()
+ context.videos.get_path.return_value = Path("input.mp4")
+ context.util.is_canceled.return_value = False
+ base_output = MagicMock(
+ video=VideoField(video_name="output.mp4"), width=32, height=32, num_frames=5, fps=8.0, duration=0.625
+ )
+
+ with (
+ patch("invokeai.app.invocations.video_frame_extract_range.probe_video", return_value=(32, 32, 0.625, 8.0)),
+ patch("invokeai.app.invocations.video_frame_extract_range.decoder_frame_count", return_value=5),
+ patch("invokeai.app.invocations.video_frame_extract_range.make_mp4_writer", return_value=MagicMock()),
+ patch("invokeai.app.invocations.video_frame_extract_range._write_frame_range", return_value=written),
+ patch("invokeai.app.invocations.video_frame_extract_range.VideoOutput.build", return_value=base_output),
+ ):
+ if should_raise:
+ with pytest.raises(ValueError, match="Decoded only 3 of 5 requested frames"):
+ invocation.invoke(context)
+ context.videos.save.assert_not_called()
+ else:
+ output = invocation.invoke(context)
+ assert output.end_frame == 4
+ context.videos.save.assert_called_once()
diff --git a/tests/app/invocations/test_video_primitive.py b/tests/app/invocations/test_video_primitive.py
new file mode 100644
index 00000000000..1da7947d4ca
--- /dev/null
+++ b/tests/app/invocations/test_video_primitive.py
@@ -0,0 +1,19 @@
+from unittest.mock import MagicMock, patch
+
+import pytest
+
+from invokeai.app.invocations.fields import VideoField
+from invokeai.app.invocations.primitives import VideoInvocation
+
+
+@pytest.mark.parametrize("decoded_count,expected", [(7, 7), (None, 8)])
+def test_video_primitive_prefers_exact_decoder_frame_count(decoded_count: int | None, expected: int) -> None:
+ invocation = VideoInvocation(video=VideoField(video_name="input.mp4"))
+ context = MagicMock()
+ context.videos.get_dto.return_value = MagicMock(video_name="input.mp4", width=64, height=64, duration=1.0, fps=8.0)
+ context.videos.get_path.return_value = "input.mp4"
+
+ with patch("invokeai.app.util.video_thumbnails.decoder_frame_count", return_value=decoded_count):
+ output = invocation.invoke(context)
+
+ assert output.num_frames == expected
diff --git a/tests/app/invocations/test_wan_denoise.py b/tests/app/invocations/test_wan_denoise.py
new file mode 100644
index 00000000000..63d55c82416
--- /dev/null
+++ b/tests/app/invocations/test_wan_denoise.py
@@ -0,0 +1,1185 @@
+"""CPU-only integration tests for ``WanDenoiseInvocation``.
+
+These tests substitute a synthetic transformer (no weights) for the real
+``WanTransformer3DModel`` so the denoise loop's shape-handling, scheduler
+integration, CFG branch, and step-callback wiring can be exercised on a CPU
+runner. End-to-end tests against real Wan checkpoints are gated behind
+``INVOKEAI_HEAVY_TESTS=1`` and require a working CUDA install.
+"""
+
+from __future__ import annotations
+
+import os
+from collections import Counter
+from contextlib import contextmanager
+from pathlib import Path
+from tempfile import TemporaryDirectory
+from unittest.mock import MagicMock
+
+import pytest
+import torch
+import torch.nn as nn
+
+from invokeai.app.invocations.fields import WanConditioningField, WanRefImageConditioningField
+from invokeai.app.invocations.model import WanTransformerField
+from invokeai.app.invocations.wan_denoise import WanDenoiseInvocation
+from invokeai.app.invocations.wan_video_denoise import WanVideoDenoiseInvocation
+from invokeai.backend.model_manager.taxonomy import WanVariantType
+from invokeai.backend.stable_diffusion.diffusion.conditioning_data import (
+ ConditioningFieldData,
+ WanConditioningInfo,
+)
+
+
+class _ZeroTransformer(nn.Module):
+ """Stand-in for ``WanTransformer3DModel``.
+
+ Returns ``torch.zeros_like(hidden_states)`` so the flow-matching scheduler
+ treats every step as a no-op velocity. After N steps the latents equal the
+ initial noise — a useful invariant for shape correctness.
+
+ ``label`` lets dual-expert tests record which expert was invoked.
+ """
+
+ def __init__(self, label: str = "single") -> None:
+ super().__init__()
+ self.dtype = torch.float32
+ self.label = label
+ self.calls: list[tuple[int, ...]] = []
+ self.timesteps_seen: list[float] = []
+
+ def forward( # noqa: D401 — match diffusers signature
+ self,
+ hidden_states: torch.Tensor,
+ timestep: torch.Tensor,
+ encoder_hidden_states: torch.Tensor,
+ attention_kwargs=None,
+ return_dict: bool = True,
+ ):
+ # Record the call so assertions can verify shape contracts.
+ self.calls.append(
+ (
+ tuple(hidden_states.shape),
+ tuple(timestep.shape),
+ tuple(encoder_hidden_states.shape),
+ )
+ )
+ # Record the timestep (t.expand(B) → take first element).
+ self.timesteps_seen.append(float(timestep.flatten()[0].item()))
+ # Real Wan I2V transformer has in_channels=36 (16 noise + 20 ref-image
+ # condition) but out_channels=16. T2V is 16/16 and TI2V-5B is 48/48 —
+ # both have matching in/out. Mirror that by only collapsing the I2V
+ # input width back to 16 channels.
+ out_shape = list(hidden_states.shape)
+ if out_shape[1] == 36:
+ out_shape[1] = 16
+ out = torch.zeros(out_shape, dtype=hidden_states.dtype, device=hidden_states.device)
+ if return_dict:
+ return type("Out", (), {"sample": out})
+ return (out,)
+
+
+@contextmanager
+def _model_on_device_ctx(model: nn.Module):
+ yield (None, model)
+
+
+def _make_loaded_model(model: nn.Module) -> MagicMock:
+ """Mock ``LoadedModel`` exposing only the methods the denoise loop touches."""
+ loaded = MagicMock()
+ loaded.model_on_device = lambda: _model_on_device_ctx(model)
+ return loaded
+
+
+def _build_context(
+ transformer: nn.Module,
+ *,
+ variant: WanVariantType,
+ model_root: Path,
+ pos_cond: WanConditioningInfo,
+ neg_cond: WanConditioningInfo | None,
+ transformer_low: nn.Module | None = None,
+) -> MagicMock:
+ """Build a MagicMock InvocationContext sufficient for ``_run_diffusion``.
+
+ When ``transformer_low`` is provided, ``context.models.load`` routes the
+ request based on the ``ModelIdentifierField.submodel_type`` so dual-expert
+ code paths see two distinct loaded models.
+ """
+ config = MagicMock()
+ config.variant = variant
+ config.format = "diffusers"
+
+ context = MagicMock()
+ context.models.get_config.return_value = config
+ context.models.get_absolute_path.return_value = model_root
+
+ def _load(model_id) -> MagicMock:
+ submodel_type = getattr(model_id, "submodel_type", None)
+ if transformer_low is not None and str(submodel_type) == "SubModelType.Transformer2":
+ return _make_loaded_model(transformer_low)
+ return _make_loaded_model(transformer)
+
+ context.models.load.side_effect = _load
+
+ def _load_conditioning(name: str) -> ConditioningFieldData:
+ if name == "pos":
+ return ConditioningFieldData(conditionings=[pos_cond])
+ if name == "neg" and neg_cond is not None:
+ return ConditioningFieldData(conditionings=[neg_cond])
+ raise KeyError(name)
+
+ context.conditioning.load.side_effect = _load_conditioning
+ context.util.signal_progress = MagicMock()
+ context.util.sd_step_callback = MagicMock()
+ context.logger = MagicMock()
+ return context
+
+
+def _make_conditioning(seq_len: int = 226, hidden: int = 4096) -> WanConditioningInfo:
+ return WanConditioningInfo(
+ prompt_embeds=torch.zeros(seq_len, hidden),
+ prompt_attention_mask=None,
+ )
+
+
+def _make_invocation(
+ transformer_field: WanTransformerField,
+ pos_field: WanConditioningField,
+ neg_field: WanConditioningField | None,
+ *,
+ width: int,
+ height: int,
+ steps: int,
+ guidance_scale: float,
+ guidance_scale_low_noise: float | None = None,
+) -> WanDenoiseInvocation:
+ return WanDenoiseInvocation(
+ id="test",
+ transformer=transformer_field,
+ positive_conditioning=pos_field,
+ negative_conditioning=neg_field,
+ width=width,
+ height=height,
+ steps=steps,
+ guidance_scale=guidance_scale,
+ guidance_scale_low_noise=guidance_scale_low_noise,
+ seed=42,
+ )
+
+
+@pytest.fixture
+def fake_model_root():
+ """A directory layout the denoise helpers can read.
+
+ No ``scheduler/`` subfolder, so the scheduler falls back to defaults — that
+ keeps the test self-contained.
+ """
+ with TemporaryDirectory() as tmp:
+ yield Path(tmp)
+
+
+@pytest.fixture(autouse=True)
+def _force_cpu(monkeypatch):
+ """Pin TorchDevice to CPU + float32 for deterministic, GPU-free tests."""
+ from invokeai.backend.util.devices import TorchDevice
+
+ monkeypatch.setattr(TorchDevice, "choose_torch_device", classmethod(lambda cls: torch.device("cpu")))
+ monkeypatch.setattr(TorchDevice, "choose_bfloat16_safe_dtype", classmethod(lambda cls, device=None: torch.float32))
+
+
+def _wan_transformer_field(*, dual: bool = False, boundary_ratio: float = 0.875) -> WanTransformerField:
+ """Build a WanTransformerField. With ``dual=True`` a low-noise expert slot
+ is also populated so the denoise loop exercises the MoE swap path."""
+ base_id = {
+ "key": "wan-test",
+ "name": "wan-test",
+ "base": "wan",
+ "type": "main",
+ "hash": "h",
+ }
+ field_kwargs: dict = {
+ "transformer": {**base_id, "submodel_type": "transformer"},
+ "boundary_ratio": boundary_ratio,
+ }
+ if dual:
+ field_kwargs["transformer_low_noise"] = {**base_id, "submodel_type": "transformer_2"}
+ return WanTransformerField(**field_kwargs)
+
+
+class TestWanDenoiseShapes:
+ """Verify the denoise loop runs end-to-end on CPU for both variants."""
+
+ @pytest.mark.parametrize(
+ "variant,latent_channels,scale,height,width",
+ [
+ (WanVariantType.T2V_A14B, 16, 8, 64, 64),
+ (WanVariantType.TI2V_5B, 48, 16, 64, 64),
+ ],
+ )
+ def test_run_diffusion_returns_4d_finite(
+ self, variant, latent_channels, scale, height, width, fake_model_root
+ ) -> None:
+ transformer = _ZeroTransformer()
+ pos = _make_conditioning()
+ ctx = _build_context(
+ transformer,
+ variant=variant,
+ model_root=fake_model_root,
+ pos_cond=pos,
+ neg_cond=None,
+ )
+
+ inv = _make_invocation(
+ transformer_field=_wan_transformer_field(),
+ pos_field=WanConditioningField(conditioning_name="pos"),
+ neg_field=None,
+ width=width,
+ height=height,
+ steps=4,
+ guidance_scale=1.0, # disables CFG, so neg conditioning isn't required
+ )
+
+ latents = inv._run_diffusion(ctx)
+
+ # Output is 4D [B, C, H/scale, W/scale] — temporal dim squeezed.
+ assert latents.ndim == 4
+ assert latents.shape == (1, latent_channels, height // scale, width // scale)
+ assert torch.isfinite(latents).all()
+
+ # Transformer should have been called exactly steps times.
+ assert len(transformer.calls) == 4
+ # Hidden states are 5D with T=1.
+ h_shape, t_shape, ctx_shape = transformer.calls[0]
+ assert h_shape == (1, latent_channels, 1, height // scale, width // scale)
+ assert t_shape == (1,)
+ assert ctx_shape == (1, 226, 4096)
+
+ # Step callback invoked once per step.
+ assert ctx.util.sd_step_callback.call_count == 4
+
+ def test_cfg_doubles_transformer_calls(self, fake_model_root) -> None:
+ """With cfg_scale != 1.0 and a negative prompt, each step runs the model twice."""
+ transformer = _ZeroTransformer()
+ pos = _make_conditioning()
+ neg = _make_conditioning()
+ ctx = _build_context(
+ transformer,
+ variant=WanVariantType.T2V_A14B,
+ model_root=fake_model_root,
+ pos_cond=pos,
+ neg_cond=neg,
+ )
+
+ inv = _make_invocation(
+ transformer_field=_wan_transformer_field(),
+ pos_field=WanConditioningField(conditioning_name="pos"),
+ neg_field=WanConditioningField(conditioning_name="neg"),
+ width=64,
+ height=64,
+ steps=3,
+ guidance_scale=4.0,
+ )
+
+ inv._run_diffusion(ctx)
+ # 3 steps × 2 (cond + uncond) = 6 forward calls.
+ assert len(transformer.calls) == 6
+
+ def test_ti2v_image_rejects_dimensions_not_divisible_by_32(self, fake_model_root: Path) -> None:
+ context = _build_context(
+ _ZeroTransformer(),
+ variant=WanVariantType.TI2V_5B,
+ model_root=fake_model_root,
+ pos_cond=_make_conditioning(),
+ neg_cond=None,
+ )
+ invocation = _make_invocation(
+ transformer_field=_wan_transformer_field(),
+ pos_field=WanConditioningField(conditioning_name="pos"),
+ neg_field=None,
+ width=48,
+ height=64,
+ steps=2,
+ guidance_scale=1.0,
+ )
+
+ with pytest.raises(ValueError, match="multiples of 32"):
+ invocation._run_diffusion(context)
+
+ def test_ti2v_video_without_reference_rejects_dimensions_not_divisible_by_32(self, fake_model_root: Path) -> None:
+ context = _build_context(
+ _ZeroTransformer(),
+ variant=WanVariantType.TI2V_5B,
+ model_root=fake_model_root,
+ pos_cond=_make_conditioning(),
+ neg_cond=None,
+ )
+ invocation = WanVideoDenoiseInvocation(
+ id="test",
+ transformer=_wan_transformer_field(),
+ positive_conditioning=WanConditioningField(conditioning_name="pos"),
+ negative_conditioning=None,
+ ref_image=None,
+ width=48,
+ height=64,
+ num_frames=5,
+ steps=2,
+ guidance_scale=1.0,
+ seed=42,
+ )
+
+ with pytest.raises(ValueError, match="multiples of 32"):
+ invocation._run_diffusion(context)
+
+ def test_zero_velocity_preserves_initial_noise(self, fake_model_root) -> None:
+ """A zero-output transformer means the flow-match step never updates latents."""
+ transformer = _ZeroTransformer()
+ pos = _make_conditioning()
+ ctx = _build_context(
+ transformer,
+ variant=WanVariantType.T2V_A14B,
+ model_root=fake_model_root,
+ pos_cond=pos,
+ neg_cond=None,
+ )
+
+ inv = _make_invocation(
+ transformer_field=_wan_transformer_field(),
+ pos_field=WanConditioningField(conditioning_name="pos"),
+ neg_field=None,
+ width=64,
+ height=64,
+ steps=4,
+ guidance_scale=1.0,
+ )
+
+ latents = inv._run_diffusion(ctx)
+
+ # Reproduce the same noise the loop would have generated and compare.
+ from invokeai.backend.wan.sampling_utils import make_noise
+
+ expected = make_noise(
+ batch_size=1,
+ latent_channels=16,
+ height=64,
+ width=64,
+ spatial_scale_factor=8,
+ device=torch.device("cpu"),
+ dtype=torch.float32,
+ seed=42,
+ ).squeeze(2)
+
+ assert torch.allclose(latents, expected, atol=1e-5)
+
+
+class TestWanDenoiseDualExpert:
+ """Verify the A14B dual-expert MoE swap behaves correctly."""
+
+ def test_swap_fires_at_boundary(self, fake_model_root) -> None:
+ """High expert handles t >= boundary_timestep, low expert handles t < boundary_timestep."""
+ high = _ZeroTransformer(label="high")
+ low = _ZeroTransformer(label="low")
+ pos = _make_conditioning()
+ ctx = _build_context(
+ high,
+ transformer_low=low,
+ variant=WanVariantType.T2V_A14B,
+ model_root=fake_model_root,
+ pos_cond=pos,
+ neg_cond=None,
+ )
+
+ # boundary_ratio=0.5 → boundary_timestep=500 (default num_train_timesteps=1000).
+ inv = _make_invocation(
+ transformer_field=_wan_transformer_field(dual=True, boundary_ratio=0.5),
+ pos_field=WanConditioningField(conditioning_name="pos"),
+ neg_field=None,
+ width=64,
+ height=64,
+ steps=10,
+ guidance_scale=1.0,
+ )
+
+ inv._run_diffusion(ctx)
+
+ # Both experts called.
+ assert len(high.timesteps_seen) > 0, "high-noise expert never invoked"
+ assert len(low.timesteps_seen) > 0, "low-noise expert never invoked"
+
+ # Every high-noise timestep is >= 500; every low-noise timestep is < 500.
+ for t in high.timesteps_seen:
+ assert t >= 500.0, f"high-noise expert saw t={t}, should be >= 500"
+ for t in low.timesteps_seen:
+ assert t < 500.0, f"low-noise expert saw t={t}, should be < 500"
+
+ # Total steps adds up.
+ assert len(high.timesteps_seen) + len(low.timesteps_seen) == 10
+
+ def test_no_swap_when_boundary_skipped(self, fake_model_root) -> None:
+ """boundary_ratio=0.0 → boundary_timestep=0 → all timesteps go to high-noise expert."""
+ high = _ZeroTransformer(label="high")
+ low = _ZeroTransformer(label="low")
+ pos = _make_conditioning()
+ ctx = _build_context(
+ high,
+ transformer_low=low,
+ variant=WanVariantType.T2V_A14B,
+ model_root=fake_model_root,
+ pos_cond=pos,
+ neg_cond=None,
+ )
+
+ inv = _make_invocation(
+ transformer_field=_wan_transformer_field(dual=True, boundary_ratio=0.0),
+ pos_field=WanConditioningField(conditioning_name="pos"),
+ neg_field=None,
+ width=64,
+ height=64,
+ steps=4,
+ guidance_scale=1.0,
+ )
+
+ inv._run_diffusion(ctx)
+
+ # boundary_timestep=0 → t >= 0 always → high-noise expert handles every step.
+ assert len(high.timesteps_seen) == 4
+ assert len(low.timesteps_seen) == 0
+
+ def test_full_low_noise_when_boundary_at_max(self, fake_model_root) -> None:
+ """boundary_ratio=1.0 → boundary_timestep=1000 → every step goes to the low-noise expert.
+
+ The UniPC flow schedule's first timestep is slightly below 1000 (flow sigmas
+ don't start exactly at 1.0), so with the boundary at the maximum no timestep
+ satisfies ``t >= 1000`` and the high-noise expert is never invoked.
+ """
+ high = _ZeroTransformer(label="high")
+ low = _ZeroTransformer(label="low")
+ pos = _make_conditioning()
+ ctx = _build_context(
+ high,
+ transformer_low=low,
+ variant=WanVariantType.T2V_A14B,
+ model_root=fake_model_root,
+ pos_cond=pos,
+ neg_cond=None,
+ )
+
+ inv = _make_invocation(
+ transformer_field=_wan_transformer_field(dual=True, boundary_ratio=1.0),
+ pos_field=WanConditioningField(conditioning_name="pos"),
+ neg_field=None,
+ width=64,
+ height=64,
+ steps=4,
+ guidance_scale=1.0,
+ )
+
+ inv._run_diffusion(ctx)
+
+ # All steps are < 1000 → low expert handles the whole schedule.
+ assert len(high.timesteps_seen) == 0
+ assert len(low.timesteps_seen) == 4
+
+ def test_cfg_with_dual_experts_doubles_calls_per_step(self, fake_model_root) -> None:
+ """With negative conditioning + cfg_scale != 1, every step runs the active expert twice."""
+ high = _ZeroTransformer(label="high")
+ low = _ZeroTransformer(label="low")
+ pos = _make_conditioning()
+ neg = _make_conditioning()
+ ctx = _build_context(
+ high,
+ transformer_low=low,
+ variant=WanVariantType.T2V_A14B,
+ model_root=fake_model_root,
+ pos_cond=pos,
+ neg_cond=neg,
+ )
+
+ inv = _make_invocation(
+ transformer_field=_wan_transformer_field(dual=True, boundary_ratio=0.5),
+ pos_field=WanConditioningField(conditioning_name="pos"),
+ neg_field=WanConditioningField(conditioning_name="neg"),
+ width=64,
+ height=64,
+ steps=6,
+ guidance_scale=4.0,
+ guidance_scale_low_noise=2.0, # Field accepted by the invocation; effect is implicit.
+ )
+
+ inv._run_diffusion(ctx)
+
+ # Total transformer invocations: 6 steps × 2 (cond + uncond) = 12, split across experts.
+ total = len(high.timesteps_seen) + len(low.timesteps_seen)
+ assert total == 12
+
+ # Each unique timestep appears twice (cond + uncond) on the same expert.
+ high_counts = Counter(high.timesteps_seen)
+ low_counts = Counter(low.timesteps_seen)
+ assert all(v == 2 for v in high_counts.values()), high_counts
+ assert all(v == 2 for v in low_counts.values()), low_counts
+
+ # And the swap actually happened — both experts saw work.
+ assert len(high_counts) > 0 and len(low_counts) > 0
+
+ def test_low_noise_cfg_runs_when_primary_scale_is_one(self, fake_model_root) -> None:
+ high = _ZeroTransformer(label="high")
+ low = _ZeroTransformer(label="low")
+ ctx = _build_context(
+ high,
+ transformer_low=low,
+ variant=WanVariantType.T2V_A14B,
+ model_root=fake_model_root,
+ pos_cond=_make_conditioning(),
+ neg_cond=_make_conditioning(),
+ )
+ inv = _make_invocation(
+ transformer_field=_wan_transformer_field(dual=True, boundary_ratio=0.5),
+ pos_field=WanConditioningField(conditioning_name="pos"),
+ neg_field=WanConditioningField(conditioning_name="neg"),
+ width=64,
+ height=64,
+ steps=6,
+ guidance_scale=1.0,
+ guidance_scale_low_noise=2.0,
+ )
+
+ inv._run_diffusion(ctx)
+
+ assert len(high.timesteps_seen) > 0
+ assert len(low.timesteps_seen) > 0
+ assert all(count == 1 for count in Counter(high.timesteps_seen).values())
+ assert all(count == 2 for count in Counter(low.timesteps_seen).values())
+
+
+@pytest.mark.skipif(
+ os.environ.get("INVOKEAI_HEAVY_TESTS") != "1",
+ reason="End-to-end test requires real Wan weights and CUDA; opt in with INVOKEAI_HEAVY_TESTS=1",
+)
+class TestWanDenoiseHeavy:
+ """Placeholder for a real-weights smoke test once CUDA is available."""
+
+ def test_real_ti2v_5b_runs(self) -> None:
+ pytest.skip("Heavy test stub — implement once a TI2V-5B checkpoint is installable.")
+
+
+class TestWanDenoiseRefImage:
+ """Phase 7: VAE-latent reference-image conditioning for I2V-A14B.
+
+ The denoise loop must concatenate the 20-channel condition tensor to the
+ 16-channel noise latents at every transformer call, producing 36-channel
+ input. Variant gate must fast-fail when ref_image is wired to a non-I2V
+ transformer."""
+
+ def _build_ctx_with_condition(
+ self,
+ transformer: _ZeroTransformer,
+ variant: WanVariantType,
+ model_root: Path,
+ condition_tensor: torch.Tensor | None,
+ ) -> MagicMock:
+ ctx = _build_context(
+ transformer,
+ variant=variant,
+ model_root=model_root,
+ pos_cond=_make_conditioning(),
+ neg_cond=None,
+ )
+ if condition_tensor is not None:
+ ctx.tensors.load.return_value = condition_tensor
+ return ctx
+
+ def _make_inv_with_ref(
+ self,
+ ref_field: "WanRefImageConditioningField | None",
+ *,
+ width: int = 64,
+ height: int = 64,
+ ) -> WanDenoiseInvocation:
+ return WanDenoiseInvocation(
+ id="test",
+ transformer=_wan_transformer_field(dual=True),
+ positive_conditioning=WanConditioningField(conditioning_name="pos"),
+ negative_conditioning=None,
+ ref_image=ref_field,
+ width=width,
+ height=height,
+ steps=3,
+ guidance_scale=1.0,
+ seed=42,
+ )
+
+ def test_ref_image_concatenated_to_36_channels(self, fake_model_root: Path) -> None:
+ """I2V_A14B + ref_image → transformer sees [B, 36, T, H/8, W/8]."""
+ transformer = _ZeroTransformer()
+ # Build the 20-channel condition tensor the encoder would have saved:
+ # 4-ch first-frame mask + 16-ch VAE-encoded image latents.
+ # At 64x64 → 8x8 latent spatial dims.
+ condition = torch.zeros(1, 20, 1, 8, 8)
+ ctx = self._build_ctx_with_condition(transformer, WanVariantType.I2V_A14B, fake_model_root, condition)
+
+ ref_field = WanRefImageConditioningField(condition_tensor_name="condition", width=64, height=64)
+ inv = self._make_inv_with_ref(ref_field)
+ inv._run_diffusion(ctx)
+
+ assert len(transformer.calls) == 3
+ # Every call's hidden_states must have 36 channels (16 noise + 20 condition).
+ for h_shape, *_ in transformer.calls:
+ assert h_shape == (1, 36, 1, 8, 8), f"expected 36-channel input, got {h_shape}"
+
+ def test_no_ref_image_keeps_16_channels(self, fake_model_root: Path) -> None:
+ """Without ref_image → transformer sees [B, 16, T, H/8, W/8] as before."""
+ transformer = _ZeroTransformer()
+ ctx = self._build_ctx_with_condition(
+ transformer, WanVariantType.I2V_A14B, fake_model_root, condition_tensor=None
+ )
+
+ inv = self._make_inv_with_ref(ref_field=None)
+ inv._run_diffusion(ctx)
+
+ for h_shape, *_ in transformer.calls:
+ assert h_shape == (1, 16, 1, 8, 8), f"expected unchanged 16-channel input, got {h_shape}"
+
+ def test_variant_gate_rejects_ref_image_on_t2v(self, fake_model_root: Path) -> None:
+ """T2V_A14B + ref_image must raise — fast-fail before doing any work."""
+ transformer = _ZeroTransformer()
+ condition = torch.zeros(1, 20, 1, 8, 8)
+ ctx = self._build_ctx_with_condition(transformer, WanVariantType.T2V_A14B, fake_model_root, condition)
+
+ ref_field = WanRefImageConditioningField(condition_tensor_name="condition", width=64, height=64)
+ inv = self._make_inv_with_ref(ref_field)
+ with pytest.raises(ValueError, match="only supported by the Wan 2.2 I2V variant"):
+ inv._run_diffusion(ctx)
+
+ def test_variant_gate_rejects_ref_image_on_ti2v(self, fake_model_root: Path) -> None:
+ """TI2V-5B + ref_image must raise — TI2V uses a different image path."""
+ transformer = _ZeroTransformer()
+ condition = torch.zeros(1, 20, 1, 8, 8)
+ ctx = self._build_ctx_with_condition(transformer, WanVariantType.TI2V_5B, fake_model_root, condition)
+
+ ref_field = WanRefImageConditioningField(condition_tensor_name="condition", width=64, height=64)
+ inv = self._make_inv_with_ref(ref_field)
+ with pytest.raises(ValueError, match="only supported by the Wan 2.2 I2V variant"):
+ inv._run_diffusion(ctx)
+
+ def test_dim_mismatch_raises(self, fake_model_root: Path) -> None:
+ """If the encoder's width/height differ from denoise's, fail clearly."""
+ transformer = _ZeroTransformer()
+ condition = torch.zeros(1, 20, 1, 8, 8)
+ ctx = self._build_ctx_with_condition(transformer, WanVariantType.I2V_A14B, fake_model_root, condition)
+
+ ref_field = WanRefImageConditioningField(condition_tensor_name="condition", width=512, height=512)
+ inv = self._make_inv_with_ref(ref_field, width=64, height=64)
+ with pytest.raises(ValueError, match="must match denoise dimensions"):
+ inv._run_diffusion(ctx)
+
+
+class TestWanDenoiseInpaint:
+ """Phase 8: ``denoise_mask`` (inpaint) wiring via ``RectifiedFlowInpaintExtension``.
+
+ User-side mask convention (matches Anima / Flux): 1.0 = preserve,
+ 0.0 = regenerate. After ``_prep_inpaint_mask`` inverts, the extension
+ sees: 0.0 = preserve, 1.0 = regenerate.
+
+ With the synthetic zero-output transformer, the scheduler step is a
+ no-op (noise_pred=0 → latents unchanged). The init latents are placed
+ into the preserved regions at every step via the extension's merge
+ function; the regenerated regions stay as the original noise tensor
+ because the model never updates them.
+ """
+
+ def _build_inpaint_context(
+ self,
+ transformer: _ZeroTransformer,
+ variant: WanVariantType,
+ model_root: Path,
+ init_latents: torch.Tensor,
+ mask: torch.Tensor,
+ ) -> MagicMock:
+ ctx = _build_context(
+ transformer,
+ variant=variant,
+ model_root=model_root,
+ pos_cond=_make_conditioning(),
+ neg_cond=None,
+ )
+
+ # tensors.load needs to return different tensors for the init-latents
+ # and the mask, dispatched by the name field.
+ def _load_tensor(name: str) -> torch.Tensor:
+ if name == "init":
+ return init_latents
+ if name == "mask":
+ return mask
+ raise KeyError(name)
+
+ ctx.tensors.load.side_effect = _load_tensor
+ return ctx
+
+ def test_preserved_region_matches_init_exactly(self, fake_model_root: Path) -> None:
+ from invokeai.app.invocations.fields import DenoiseMaskField, LatentsField
+
+ transformer = _ZeroTransformer()
+ # 64x64 image -> 8x8 latents at scale 8 (T2V-A14B family).
+ # Init latents: fixed value 0.5 so the preserved region is detectable.
+ init_latents = torch.full((1, 16, 8, 8), 0.5)
+ # Mask: 8x8 spatial mask, half-1 (preserve left), half-0 (regenerate right).
+ # User-side convention: 1 = preserve, 0 = regenerate.
+ mask = torch.zeros(1, 1, 8, 8)
+ mask[..., :, :4] = 1.0 # left half preserved
+
+ ctx = self._build_inpaint_context(
+ transformer,
+ variant=WanVariantType.T2V_A14B,
+ model_root=fake_model_root,
+ init_latents=init_latents,
+ mask=mask,
+ )
+
+ inv = WanDenoiseInvocation(
+ id="test",
+ transformer=_wan_transformer_field(),
+ positive_conditioning=WanConditioningField(conditioning_name="pos"),
+ negative_conditioning=None,
+ latents=LatentsField(latents_name="init"),
+ denoise_mask=DenoiseMaskField(mask_name="mask", masked_latents_name=None, gradient=False),
+ width=64,
+ height=64,
+ steps=4,
+ guidance_scale=1.0,
+ denoising_start=0.0,
+ denoising_end=1.0,
+ seed=42,
+ )
+
+ out = inv._run_diffusion(ctx) # [B, C, H_lat, W_lat]
+ assert out.shape == (1, 16, 8, 8)
+
+ # Preserved (left) half: must exactly match the init latents at t_prev=0
+ # (final step's merge produces noised_init = noise*0 + 1*init = init).
+ assert torch.allclose(out[..., :, :4], torch.full_like(out[..., :, :4], 0.5)), (
+ "Preserved region must equal init latents at the end of denoise"
+ )
+
+ # Regenerated (right) half: model never changed anything (zero transformer)
+ # so this region stays equal to the original noise, NOT to init.
+ # Assert it's *not* equal to init — concrete proof the regions are
+ # being handled separately.
+ assert not torch.allclose(out[..., :, 4:], torch.full_like(out[..., :, 4:], 0.5)), (
+ "Regenerated region should NOT equal init — extension must route it through the model path"
+ )
+
+ def test_inpaint_requires_init_latents(self, fake_model_root: Path) -> None:
+ """Providing a mask without init latents must raise — there's nothing
+ to merge back into the preserved regions."""
+ from invokeai.app.invocations.fields import DenoiseMaskField
+
+ transformer = _ZeroTransformer()
+ mask = torch.ones(1, 1, 8, 8)
+ ctx = self._build_inpaint_context(
+ transformer,
+ variant=WanVariantType.T2V_A14B,
+ model_root=fake_model_root,
+ init_latents=torch.zeros(1, 16, 8, 8), # unused
+ mask=mask,
+ )
+
+ inv = WanDenoiseInvocation(
+ id="test",
+ transformer=_wan_transformer_field(),
+ positive_conditioning=WanConditioningField(conditioning_name="pos"),
+ negative_conditioning=None,
+ latents=None, # missing — error
+ denoise_mask=DenoiseMaskField(mask_name="mask", masked_latents_name=None, gradient=False),
+ width=64,
+ height=64,
+ steps=2,
+ guidance_scale=1.0,
+ seed=42,
+ )
+
+ with pytest.raises(ValueError, match="img2img inpainting"):
+ inv._run_diffusion(ctx)
+
+ def test_no_mask_path_is_unchanged(self, fake_model_root: Path) -> None:
+ """Without a denoise_mask, the loop behaves as before — sanity check
+ that adding the inpaint extension didn't introduce a regression on
+ the non-inpaint codepath."""
+ from invokeai.app.invocations.fields import LatentsField
+
+ transformer = _ZeroTransformer()
+ init_latents = torch.full((1, 16, 8, 8), 0.3)
+ ctx = self._build_inpaint_context(
+ transformer,
+ variant=WanVariantType.T2V_A14B,
+ model_root=fake_model_root,
+ init_latents=init_latents,
+ mask=torch.zeros(1, 1, 8, 8), # unused — no mask wired
+ )
+
+ inv = WanDenoiseInvocation(
+ id="test",
+ transformer=_wan_transformer_field(),
+ positive_conditioning=WanConditioningField(conditioning_name="pos"),
+ negative_conditioning=None,
+ latents=LatentsField(latents_name="init"),
+ denoise_mask=None, # no mask
+ width=64,
+ height=64,
+ steps=4,
+ guidance_scale=1.0,
+ denoising_start=0.5, # img2img-style partial denoise
+ denoising_end=1.0,
+ seed=42,
+ )
+
+ out = inv._run_diffusion(ctx)
+ assert out.shape == (1, 16, 8, 8)
+ assert torch.isfinite(out).all()
+
+
+class TestLoRAExpertWiring:
+ def test_high_only_loras_do_not_leak_to_low_expert(self, fake_model_root) -> None:
+ """A LoRA routed only to the primary (high-noise) list must not be applied to
+ the low-noise expert (PR #9163 review): the old ``loras_low_noise or loras``
+ fallback silently reused the primary list for the low expert, so an
+ expert-tagged LoRA (e.g. a Lightning high-noise distill) was misapplied and
+ high-only targeting was impossible."""
+ from unittest.mock import patch
+
+ from invokeai.app.invocations.model import LoRAField
+ from invokeai.app.invocations.wan_denoise import _ExpertSwapper # noqa: F401 (documents the patch target)
+
+ captured: dict = {}
+
+ class _RecordingSwapper:
+ HIGH = "high"
+ LOW = "low"
+
+ def __init__(self, **kwargs) -> None:
+ captured.update(kwargs)
+ self._model = _ZeroTransformer(label="either")
+
+ def get(self, label: str) -> nn.Module:
+ return self._model
+
+ def close(self) -> None:
+ pass
+
+ field = _wan_transformer_field(dual=True)
+ field.loras.append(
+ LoRAField(
+ lora={"key": "l", "hash": "h", "name": "lightning-high", "base": "wan", "type": "lora"},
+ weight=1.0,
+ )
+ )
+ assert field.loras_low_noise == []
+
+ ctx = _build_context(
+ _ZeroTransformer(label="high"),
+ transformer_low=_ZeroTransformer(label="low"),
+ variant=WanVariantType.T2V_A14B,
+ model_root=fake_model_root,
+ pos_cond=_make_conditioning(),
+ neg_cond=None,
+ )
+ inv = _make_invocation(
+ transformer_field=field,
+ pos_field=WanConditioningField(conditioning_name="pos"),
+ neg_field=None,
+ width=64,
+ height=64,
+ steps=2,
+ guidance_scale=1.0,
+ )
+ with patch("invokeai.app.invocations.wan_denoise._ExpertSwapper", _RecordingSwapper):
+ inv._run_diffusion(ctx)
+
+ assert captured["high_lora_factory"] is not None
+ assert captured["low_lora_factory"] is None
+
+
+class TestDefaultSchedulerForVariant:
+ """``_default_scheduler_for_variant`` returns the right class + config when no
+ on-disk ``scheduler/`` directory exists (the standalone GGUF / single-file case).
+ """
+
+ def test_ti2v_5b_returns_unipc_with_flow_config(self) -> None:
+ from diffusers import UniPCMultistepScheduler
+
+ from invokeai.app.invocations.wan_denoise import _default_scheduler_for_variant
+
+ s = _default_scheduler_for_variant(WanVariantType.TI2V_5B)
+ assert isinstance(s, UniPCMultistepScheduler)
+ # The combination below is what makes this a "Wan flow" UniPC rather than a
+ # generic UniPC schedule — wrong values here drift on TI2V-5B samples.
+ assert s.config.flow_shift == 5.0
+ assert s.config.prediction_type == "flow_prediction"
+ assert s.config.use_flow_sigmas is True
+ assert s.config.solver_type == "bh2"
+
+ def test_a14b_variants_return_unipc_with_flow_shift_3(self) -> None:
+ """Both A14B reference repos (Wan-AI/Wan2.2-{T2V,I2V}-A14B-Diffusers) ship
+ UniPCMultistepScheduler with flow_shift=3.0 — NOT FlowMatchEuler (PR #9163
+ review): an unshifted Euler fallback silently degrades every A14B GGUF render
+ and skews how many steps land above the MoE expert boundary."""
+ from diffusers import UniPCMultistepScheduler
+
+ from invokeai.app.invocations.wan_denoise import _default_scheduler_for_variant
+
+ for v in (WanVariantType.T2V_A14B, WanVariantType.I2V_A14B):
+ s = _default_scheduler_for_variant(v)
+ assert isinstance(s, UniPCMultistepScheduler)
+ assert s.config.flow_shift == 3.0
+ assert s.config.prediction_type == "flow_prediction"
+ assert s.config.use_flow_sigmas is True
+ assert s.config.solver_type == "bh2"
+
+
+class _TimestepRecordingTransformer(_ZeroTransformer):
+ """_ZeroTransformer that additionally keeps the full per-token timestep tensor and
+ the frame-0 slice of each hidden_states input — needed to verify the TI2V-5B
+ expand-timesteps blend, which _ZeroTransformer's scalar recording can't observe."""
+
+ def __init__(self, label: str = "single") -> None:
+ super().__init__(label)
+ self.full_timesteps: list[torch.Tensor] = []
+ self.frame0_inputs: list[torch.Tensor] = []
+
+ def forward(self, hidden_states, timestep, encoder_hidden_states, attention_kwargs=None, return_dict=True):
+ self.full_timesteps.append(timestep.detach().clone())
+ self.frame0_inputs.append(hidden_states[:, :, 0].detach().clone())
+ return super().forward(hidden_states, timestep, encoder_hidden_states, attention_kwargs, return_dict)
+
+
+def _make_video_invocation(
+ *,
+ ref_field: WanRefImageConditioningField | None = None,
+ width: int = 64,
+ height: int = 64,
+ num_frames: int = 17,
+ steps: int = 3,
+ guidance_scale: float = 1.0,
+ transformer_field: WanTransformerField | None = None,
+) -> WanVideoDenoiseInvocation:
+ return WanVideoDenoiseInvocation(
+ id="test",
+ transformer=transformer_field or _wan_transformer_field(),
+ positive_conditioning=WanConditioningField(conditioning_name="pos"),
+ negative_conditioning=None,
+ ref_image=ref_field,
+ width=width,
+ height=height,
+ num_frames=num_frames,
+ steps=steps,
+ guidance_scale=guidance_scale,
+ seed=42,
+ )
+
+
+class TestWanVideoDenoiseMultiFrame:
+ """The multi-frame (T_lat > 1) video denoise loop — the core video feature.
+
+ Uses the same synthetic zero-velocity transformer as the single-frame tests:
+ latents shapes, per-variant conditioning mechanisms (A14B channel-concat vs
+ TI2V-5B expand-timesteps mask blend), and the zero-velocity noise invariant
+ are all checkable on CPU without weights.
+ """
+
+ def test_low_noise_cfg_runs_when_primary_scale_is_one(self, fake_model_root: Path) -> None:
+ high = _ZeroTransformer(label="high")
+ low = _ZeroTransformer(label="low")
+ ctx = _build_context(
+ high,
+ transformer_low=low,
+ variant=WanVariantType.T2V_A14B,
+ model_root=fake_model_root,
+ pos_cond=_make_conditioning(),
+ neg_cond=_make_conditioning(),
+ )
+ inv = WanVideoDenoiseInvocation(
+ id="test",
+ transformer=_wan_transformer_field(dual=True, boundary_ratio=0.5),
+ positive_conditioning=WanConditioningField(conditioning_name="pos"),
+ negative_conditioning=WanConditioningField(conditioning_name="neg"),
+ ref_image=None,
+ width=64,
+ height=64,
+ num_frames=5,
+ steps=6,
+ guidance_scale=1.0,
+ guidance_scale_low_noise=2.0,
+ seed=42,
+ )
+
+ inv._run_diffusion(ctx)
+
+ assert all(count == 1 for count in Counter(high.timesteps_seen).values())
+ assert all(count == 2 for count in Counter(low.timesteps_seen).values())
+
+ @pytest.mark.parametrize(
+ "variant,latent_channels,scale",
+ [
+ (WanVariantType.T2V_A14B, 16, 8),
+ (WanVariantType.TI2V_5B, 48, 16),
+ ],
+ )
+ def test_t2v_multi_frame_returns_5d_finite(self, variant, latent_channels, scale, fake_model_root) -> None:
+ transformer = _ZeroTransformer()
+ ctx = _build_context(
+ transformer,
+ variant=variant,
+ model_root=fake_model_root,
+ pos_cond=_make_conditioning(),
+ neg_cond=None,
+ )
+ inv = _make_video_invocation(num_frames=17, steps=3)
+
+ latents = inv._run_diffusion(ctx)
+
+ # 17 pixel frames -> (17 - 1) / 4 + 1 = 5 latent frames.
+ h_lat, w_lat = 64 // scale, 64 // scale
+ assert latents.shape == (1, latent_channels, 5, h_lat, w_lat)
+ assert torch.isfinite(latents).all()
+ assert len(transformer.calls) == 3
+ for h_shape, t_shape, _ctx_shape in transformer.calls:
+ assert h_shape == (1, latent_channels, 5, h_lat, w_lat)
+ assert t_shape == (1,) # T2V: scalar timestep per batch item
+
+ def test_zero_velocity_preserves_initial_noise_video(self, fake_model_root) -> None:
+ transformer = _ZeroTransformer()
+ ctx = _build_context(
+ transformer,
+ variant=WanVariantType.T2V_A14B,
+ model_root=fake_model_root,
+ pos_cond=_make_conditioning(),
+ neg_cond=None,
+ )
+ inv = _make_video_invocation(num_frames=17, steps=4)
+
+ latents = inv._run_diffusion(ctx)
+
+ from invokeai.backend.wan.sampling_utils import make_noise
+
+ expected = make_noise(
+ batch_size=1,
+ latent_channels=16,
+ height=64,
+ width=64,
+ spatial_scale_factor=8,
+ device=torch.device("cpu"),
+ dtype=torch.float32,
+ seed=42,
+ num_latent_frames=5,
+ )
+ assert torch.allclose(latents, expected, atol=1e-5)
+
+ def test_a14b_i2v_video_concatenates_condition_across_frames(self, fake_model_root) -> None:
+ """I2V-A14B video: the [1, 20, T_lat, ...] condition concats to 36 channels at
+ every step, with T_lat matching the noise latents."""
+ transformer = _ZeroTransformer()
+ ctx = _build_context(
+ transformer,
+ variant=WanVariantType.I2V_A14B,
+ model_root=fake_model_root,
+ pos_cond=_make_conditioning(),
+ neg_cond=None,
+ )
+ ctx.tensors.load.return_value = torch.zeros(1, 20, 5, 8, 8)
+ ref = WanRefImageConditioningField(condition_tensor_name="condition", width=64, height=64, num_frames=17)
+ inv = _make_video_invocation(ref_field=ref, num_frames=17, steps=3)
+
+ inv._run_diffusion(ctx)
+
+ assert len(transformer.calls) == 3
+ for h_shape, t_shape, _ctx_shape in transformer.calls:
+ assert h_shape == (1, 36, 5, 8, 8)
+ assert t_shape == (1,) # concat path keeps the scalar timestep
+
+ def test_a14b_i2v_video_field_frame_count_mismatch_raises(self, fake_model_root) -> None:
+ ctx = _build_context(
+ _ZeroTransformer(),
+ variant=WanVariantType.I2V_A14B,
+ model_root=fake_model_root,
+ pos_cond=_make_conditioning(),
+ neg_cond=None,
+ )
+ ctx.tensors.load.return_value = torch.zeros(1, 20, 2, 8, 8)
+ ref = WanRefImageConditioningField(condition_tensor_name="condition", width=64, height=64, num_frames=5)
+ inv = _make_video_invocation(ref_field=ref, num_frames=17, steps=3)
+
+ with pytest.raises(ValueError, match="must match denoise\\s+num_frames"):
+ inv._run_diffusion(ctx)
+
+ def test_a14b_i2v_video_condition_tensor_frame_mismatch_raises(self, fake_model_root) -> None:
+ """Field metadata matches but the saved tensor's T_lat doesn't — fail clearly."""
+ ctx = _build_context(
+ _ZeroTransformer(),
+ variant=WanVariantType.I2V_A14B,
+ model_root=fake_model_root,
+ pos_cond=_make_conditioning(),
+ neg_cond=None,
+ )
+ ctx.tensors.load.return_value = torch.zeros(1, 20, 2, 8, 8)
+ ref = WanRefImageConditioningField(condition_tensor_name="condition", width=64, height=64, num_frames=17)
+ inv = _make_video_invocation(ref_field=ref, num_frames=17, steps=3)
+
+ with pytest.raises(ValueError, match="latent frames"):
+ inv._run_diffusion(ctx)
+
+ def test_ti2v_expand_timesteps_blend(self, fake_model_root) -> None:
+ """TI2V-5B I2V: frame 0 is locked to the condition via the first-frame mask.
+
+ Checks the three parts of the expand-timesteps mechanism:
+ 1. The model input's frame 0 equals the condition (blend), other frames don't.
+ 2. The per-token timestep tensor is 0 at frame-0 token positions and `t`
+ elsewhere (shape [1, T_lat * H_lat/2 * W_lat/2] from the ::2 stride).
+ 3. After the loop, frame 0 is restored to the clean condition while later
+ frames keep the initial noise (zero-velocity invariant).
+ """
+ transformer = _TimestepRecordingTransformer()
+ ctx = _build_context(
+ transformer,
+ variant=WanVariantType.TI2V_5B,
+ model_root=fake_model_root,
+ pos_cond=_make_conditioning(),
+ neg_cond=None,
+ )
+ # 64x64 at 16x spatial scale -> 4x4 latents; condition is a single latent
+ # frame of 48 channels with a distinctive constant value.
+ condition = torch.full((1, 48, 1, 4, 4), 0.7)
+ ctx.tensors.load.return_value = condition
+ ref = WanRefImageConditioningField(condition_tensor_name="condition", width=64, height=64, num_frames=1)
+ inv = _make_video_invocation(ref_field=ref, num_frames=17, steps=3)
+
+ latents = inv._run_diffusion(ctx)
+
+ t_lat, h_lat, w_lat = 5, 4, 4
+ tokens_per_frame = (h_lat // 2) * (w_lat // 2)
+ assert len(transformer.calls) == 3
+ for step_idx, (h_shape, t_shape, _ctx_shape) in enumerate(transformer.calls):
+ assert h_shape == (1, 48, t_lat, h_lat, w_lat)
+ assert t_shape == (1, t_lat * tokens_per_frame)
+ full_ts = transformer.full_timesteps[step_idx]
+ # Frame-0 tokens are gated to timestep 0; all others carry the step's t.
+ assert torch.all(full_ts[0, :tokens_per_frame] == 0)
+ assert torch.all(full_ts[0, tokens_per_frame:] != 0)
+ # The blended input's frame 0 is exactly the condition.
+ assert torch.allclose(transformer.frame0_inputs[step_idx], condition[:, :, 0], atol=1e-6)
+
+ # Final restore: frame 0 == clean condition; frames 1+ == the initial noise.
+ assert torch.allclose(latents[:, :, 0], condition[:, :, 0].to(latents.dtype), atol=1e-6)
+
+ from invokeai.backend.wan.sampling_utils import make_noise
+
+ expected_noise = make_noise(
+ batch_size=1,
+ latent_channels=48,
+ height=64,
+ width=64,
+ spatial_scale_factor=16,
+ device=torch.device("cpu"),
+ dtype=torch.float32,
+ seed=42,
+ num_latent_frames=t_lat,
+ )
+ assert torch.allclose(latents[:, :, 1:], expected_noise[:, :, 1:], atol=1e-5)
diff --git a/tests/app/invocations/test_wan_expert_swapper.py b/tests/app/invocations/test_wan_expert_swapper.py
new file mode 100644
index 00000000000..b5f910a9ac2
--- /dev/null
+++ b/tests/app/invocations/test_wan_expert_swapper.py
@@ -0,0 +1,654 @@
+"""Tests for ``_ExpertSwapper``'s LoRA-context lifecycle.
+
+The swapper is responsible for entering and exiting both the
+``model_on_device`` context and the ``LayerPatcher.apply_smart_model_patches``
+context in the right order across an expert swap:
+
+ enter HIGH: enter device(HIGH) -> enter lora(HIGH)
+ swap: exit lora(HIGH) -> exit device(HIGH)
+ enter device(LOW) -> enter lora(LOW)
+ close: exit lora(LOW) -> exit device(LOW)
+
+These tests use a tiny ``nn.Linear`` standing in for each transformer expert
+so we can verify the swapper hands back the right model and routes the right
+LoRA factory at each step.
+"""
+
+from typing import Iterable, Tuple
+from unittest.mock import patch
+
+import pytest
+import torch
+import torch.nn as nn
+
+from invokeai.app.invocations.wan_denoise import _ExpertSwapper
+from invokeai.backend.patches.model_patch_raw import ModelPatchRaw
+
+
+class _FakeModelOnDevice:
+ """Minimal stand-in for the model-cache record's ``model_on_device`` context.
+
+ Tracks enter/exit to verify the swapper's lifecycle invariants."""
+
+ def __init__(self, label: str, model: nn.Module, log: list[str]) -> None:
+ self._label = label
+ self._model = model
+ self._log = log
+
+ def __enter__(self):
+ self._log.append(f"device-enter:{self._label}")
+ # Return shape mirrors the real model cache: (cached_weights, model).
+ return (None, self._model)
+
+ def __exit__(self, exc_type, exc_val, exc_tb):
+ self._log.append(f"device-exit:{self._label}")
+ return False
+
+
+class _FakeCachedModel:
+ """Stand-in for ``CachedModelWithPartialLoad``: records full_unload_from_vram calls."""
+
+ def __init__(self, label: str, log: list[str]) -> None:
+ self._label = label
+ self._log = log
+ self.unload_calls = 0
+
+ def full_unload_from_vram(self) -> int:
+ self._log.append(f"full-unload:{self._label}")
+ self.unload_calls += 1
+ return 0
+
+
+class _FakeCacheRecord:
+ def __init__(self, cached_model: _FakeCachedModel) -> None:
+ self.cached_model = cached_model
+
+
+class _FakeInfo:
+ """Mirrors the runtime ``LoadedModel`` enough for the swapper to reach
+ ``info._cache_record.cached_model.full_unload_from_vram()`` on swap."""
+
+ def __init__(self, label: str, model: nn.Module, log: list[str]) -> None:
+ self._label = label
+ self._model = model
+ self._log = log
+ self._cache_record = _FakeCacheRecord(_FakeCachedModel(label, log))
+
+ def model_on_device(self):
+ return _FakeModelOnDevice(self._label, self._model, self._log)
+
+
+class _FakeContext:
+ """Mocks ``InvocationContext.models.load`` returning a fresh ``_FakeInfo``
+ for each call — mirrors the real behaviour where the swapper expects a
+ fresh handle per ``get()``."""
+
+ def __init__(self, infos_by_model_id: dict[str, _FakeInfo], log: list[str]) -> None:
+ self._infos = infos_by_model_id
+ self._log = log
+ # Track how many times each model id was loaded — the lazy-load fix
+ # depends on this count being 1 per swap, not 1 upfront.
+
+ class _Models:
+ def __init__(self, outer):
+ self._outer = outer
+ self.load_calls: list[str] = []
+
+ def load(self, model_id):
+ self.load_calls.append(model_id)
+ self._outer._log.append(f"models.load:{model_id}")
+ return self._outer._infos[model_id]
+
+ self.models = _Models(self)
+
+
+def _make_factory(log: list[str], label: str) -> "callable":
+ """Build a LoRAIteratorFactory that records each invocation in ``log``."""
+
+ def factory() -> Iterable[Tuple[ModelPatchRaw, float]]:
+ log.append(f"lora-factory-call:{label}")
+ return iter([])
+
+ return factory
+
+
+def _stub_lora_context_manager(log: list[str]):
+ """Patch ``LayerPatcher.apply_smart_model_patches`` to a stub that records
+ enter/exit in ``log`` and returns a no-op context manager.
+
+ The stub introspects its arguments so we can verify the swapper passes
+ the correct ``model``, ``patches`` factory output, and prefix.
+ """
+ calls: list[dict] = []
+
+ class _Stub:
+ def __init__(self, model, patches, prefix, dtype, cached_weights, force_sidecar_patching):
+ self.model = model
+ self.patches = patches
+ self.prefix = prefix
+ self.dtype = dtype
+ self.cached_weights = cached_weights
+ self.force_sidecar_patching = force_sidecar_patching
+ calls.append(
+ {
+ "model": model,
+ "prefix": prefix,
+ "dtype": dtype,
+ "force_sidecar_patching": force_sidecar_patching,
+ }
+ )
+
+ def __enter__(self):
+ log.append("lora-enter")
+ # Force the factory's iterator to evaluate so we can assert it was
+ # consumed (mirrors the real LayerPatcher behavior).
+ list(self.patches)
+ return self
+
+ def __exit__(self, exc_type, exc_val, exc_tb):
+ log.append("lora-exit")
+ return False
+
+ def factory(model, patches, prefix, dtype, cached_weights, force_sidecar_patching=False):
+ return _Stub(model, patches, prefix, dtype, cached_weights, force_sidecar_patching)
+
+ return factory, calls
+
+
+def test_lifecycle_high_only():
+ """Single-expert (TI2V-5B / A14B with only high loaded): enter HIGH, close."""
+ log: list[str] = []
+ high_nn = nn.Linear(1, 1)
+ ctx = _FakeContext({"high": _FakeInfo("HIGH", high_nn, log)}, log)
+
+ stub, calls = _stub_lora_context_manager(log)
+ with patch(
+ "invokeai.app.invocations.wan_denoise.LayerPatcher.apply_smart_model_patches",
+ side_effect=stub,
+ ):
+ swapper = _ExpertSwapper(
+ context=ctx,
+ high_model="high",
+ low_model=None,
+ inference_dtype=torch.bfloat16,
+ high_lora_factory=_make_factory(log, "HIGH"),
+ low_lora_factory=None,
+ )
+ model = swapper.get(_ExpertSwapper.HIGH)
+ assert model is high_nn
+ swapper.close()
+
+ assert log == [
+ "models.load:high",
+ "device-enter:HIGH",
+ "lora-factory-call:HIGH",
+ "lora-enter",
+ "lora-exit",
+ "device-exit:HIGH",
+ ]
+ assert len(calls) == 1
+ assert calls[0]["model"] is high_nn
+ assert calls[0]["prefix"] == "lora_transformer-"
+
+
+def test_lifecycle_dual_expert_swap():
+ """A14B: HIGH first, then LOW. Each LoRA context opens/closes with its expert."""
+ log: list[str] = []
+ high_nn = nn.Linear(1, 1)
+ low_nn = nn.Linear(1, 1)
+ ctx = _FakeContext(
+ {"high": _FakeInfo("HIGH", high_nn, log), "low": _FakeInfo("LOW", low_nn, log)},
+ log,
+ )
+
+ stub, calls = _stub_lora_context_manager(log)
+ with patch(
+ "invokeai.app.invocations.wan_denoise.LayerPatcher.apply_smart_model_patches",
+ side_effect=stub,
+ ):
+ swapper = _ExpertSwapper(
+ context=ctx,
+ high_model="high",
+ low_model="low",
+ inference_dtype=torch.bfloat16,
+ high_lora_factory=_make_factory(log, "HIGH"),
+ low_lora_factory=_make_factory(log, "LOW"),
+ )
+ first = swapper.get(_ExpertSwapper.HIGH)
+ assert first is high_nn
+
+ second = swapper.get(_ExpertSwapper.LOW)
+ assert second is low_nn
+
+ swapper.close()
+
+ expected = [
+ # enter HIGH (models.load first, then device, then lora)
+ "models.load:high",
+ "device-enter:HIGH",
+ "lora-factory-call:HIGH",
+ "lora-enter",
+ # swap to LOW: LoRA out -> device out -> force-unload HIGH -> models.load LOW
+ # -> device in -> LoRA in. The full-unload step shoves HIGH's weights off GPU
+ # before the cache decides how much room LOW gets.
+ "lora-exit",
+ "device-exit:HIGH",
+ "full-unload:HIGH",
+ "models.load:low",
+ "device-enter:LOW",
+ "lora-factory-call:LOW",
+ "lora-enter",
+ # close
+ "lora-exit",
+ "device-exit:LOW",
+ ]
+ assert log == expected
+ # Two patcher invocations, each bound to the expected model.
+ assert len(calls) == 2
+ assert calls[0]["model"] is high_nn
+ assert calls[1]["model"] is low_nn
+
+
+def test_quantized_flag_forwards_to_sidecar():
+ """GGUF (quantized) experts must request sidecar patching."""
+ log: list[str] = []
+ high_nn = nn.Linear(1, 1)
+ ctx = _FakeContext({"high": _FakeInfo("HIGH", high_nn, log)}, log)
+
+ stub, calls = _stub_lora_context_manager(log)
+ with patch(
+ "invokeai.app.invocations.wan_denoise.LayerPatcher.apply_smart_model_patches",
+ side_effect=stub,
+ ):
+ swapper = _ExpertSwapper(
+ context=ctx,
+ high_model="high",
+ low_model=None,
+ inference_dtype=torch.bfloat16,
+ high_lora_factory=_make_factory(log, "HIGH"),
+ high_is_quantized=True,
+ )
+ swapper.get(_ExpertSwapper.HIGH)
+ swapper.close()
+
+ assert calls[0]["force_sidecar_patching"] is True
+
+
+def test_no_lora_factory_skips_lora_context():
+ """When no LoRAs are wired, the swapper doesn't enter the LoRA context."""
+ log: list[str] = []
+ high_nn = nn.Linear(1, 1)
+ ctx = _FakeContext({"high": _FakeInfo("HIGH", high_nn, log)}, log)
+
+ stub, calls = _stub_lora_context_manager(log)
+ with patch(
+ "invokeai.app.invocations.wan_denoise.LayerPatcher.apply_smart_model_patches",
+ side_effect=stub,
+ ):
+ swapper = _ExpertSwapper(
+ context=ctx,
+ high_model="high",
+ low_model=None,
+ inference_dtype=torch.bfloat16,
+ high_lora_factory=None, # no LoRAs
+ low_lora_factory=None,
+ )
+ swapper.get(_ExpertSwapper.HIGH)
+ swapper.close()
+
+ # No "lora-enter" / "lora-exit" entries — LayerPatcher was never invoked.
+ assert "lora-enter" not in log
+ assert "lora-exit" not in log
+ assert len(calls) == 0
+
+
+def test_repeat_get_same_label_is_a_no_op():
+ """Calling get(HIGH) twice in a row must not re-enter the contexts.
+
+ Critically, ``models.load`` must only be called once per actual swap —
+ not on every ``get()``. Caching the loaded model on first entry, and
+ short-circuiting re-entry, prevents per-step cache thrash."""
+ log: list[str] = []
+ high_nn = nn.Linear(1, 1)
+ ctx = _FakeContext({"high": _FakeInfo("HIGH", high_nn, log)}, log)
+
+ stub, calls = _stub_lora_context_manager(log)
+ with patch(
+ "invokeai.app.invocations.wan_denoise.LayerPatcher.apply_smart_model_patches",
+ side_effect=stub,
+ ):
+ swapper = _ExpertSwapper(
+ context=ctx,
+ high_model="high",
+ low_model=None,
+ inference_dtype=torch.bfloat16,
+ high_lora_factory=_make_factory(log, "HIGH"),
+ )
+ swapper.get(_ExpertSwapper.HIGH)
+ swapper.get(_ExpertSwapper.HIGH) # should be a no-op
+ swapper.close()
+
+ # device-enter + lora-enter happen exactly once, and crucially
+ # models.load is called only once — repeat get() must short-circuit
+ # so the cache isn't re-touched every step of the denoise loop.
+ assert log.count("models.load:high") == 1
+ assert log.count("device-enter:HIGH") == 1
+ assert log.count("lora-enter") == 1
+ assert log.count("lora-exit") == 1
+ assert log.count("device-exit:HIGH") == 1
+
+
+def test_lazy_load_per_swap_not_upfront():
+ """Regression for the cache-eviction warning that triggered this fix.
+
+ ``models.load`` must NOT be called at swapper construction. It is called
+ only on the first ``get()`` for each expert. This keeps the per-handle
+ cache window small enough that the LRU policy doesn't drop one expert
+ while the other is being used."""
+ log: list[str] = []
+ high_nn = nn.Linear(1, 1)
+ low_nn = nn.Linear(1, 1)
+ ctx = _FakeContext(
+ {"high": _FakeInfo("HIGH", high_nn, log), "low": _FakeInfo("LOW", low_nn, log)},
+ log,
+ )
+
+ stub, _ = _stub_lora_context_manager(log)
+ with patch(
+ "invokeai.app.invocations.wan_denoise.LayerPatcher.apply_smart_model_patches",
+ side_effect=stub,
+ ):
+ # Construction alone must not trigger any models.load call.
+ swapper = _ExpertSwapper(
+ context=ctx,
+ high_model="high",
+ low_model="low",
+ inference_dtype=torch.bfloat16,
+ high_lora_factory=_make_factory(log, "HIGH"),
+ low_lora_factory=_make_factory(log, "LOW"),
+ )
+ assert ctx.models.load_calls == [], (
+ "Swapper must not call models.load until get() is invoked — see issue #7513 for cache-eviction rationale."
+ )
+
+ # First get(HIGH): loads HIGH only.
+ swapper.get(_ExpertSwapper.HIGH)
+ assert ctx.models.load_calls == ["high"]
+
+ # Swap to LOW: loads LOW only. HIGH is NOT re-loaded — its handle
+ # was used and released, the next call to it (if any) will re-load.
+ swapper.get(_ExpertSwapper.LOW)
+ assert ctx.models.load_calls == ["high", "low"]
+
+ # Back to HIGH: a fresh load (the previous handle is gone). This is
+ # the right behaviour — each swap gets a guaranteed-fresh handle
+ # rather than a stale reference into the cache.
+ swapper.get(_ExpertSwapper.HIGH)
+ assert ctx.models.load_calls == ["high", "low", "high"]
+
+ swapper.close()
+
+
+def test_empty_cache_called_on_swap():
+ """Regression: each expert swap must trigger ``TorchDevice.empty_cache()`` so
+ the next ``partial_load_to_vram`` sees an un-fragmented allocator.
+
+ A14B users reported the low-noise expert ending up far more CPU-resident than
+ the high-noise one — the previous expert's freed blocks stayed pinned in the
+ PyTorch caching allocator across the swap, and partial_load decided there
+ wasn't room for as much of the incoming expert as there actually was."""
+ log: list[str] = []
+ high_nn = nn.Linear(1, 1)
+ low_nn = nn.Linear(1, 1)
+ ctx = _FakeContext(
+ {"high": _FakeInfo("HIGH", high_nn, log), "low": _FakeInfo("LOW", low_nn, log)},
+ log,
+ )
+
+ stub, _ = _stub_lora_context_manager(log)
+ with (
+ patch(
+ "invokeai.app.invocations.wan_denoise.LayerPatcher.apply_smart_model_patches",
+ side_effect=stub,
+ ),
+ patch("invokeai.app.invocations.wan_denoise.TorchDevice.empty_cache") as empty_cache_mock,
+ ):
+ swapper = _ExpertSwapper(
+ context=ctx,
+ high_model="high",
+ low_model="low",
+ inference_dtype=torch.bfloat16,
+ high_lora_factory=_make_factory(log, "HIGH"),
+ low_lora_factory=_make_factory(log, "LOW"),
+ )
+ swapper.get(_ExpertSwapper.HIGH)
+ first_call_count = empty_cache_mock.call_count
+ assert first_call_count >= 1, "empty_cache should run on the initial expert load too"
+
+ swapper.get(_ExpertSwapper.LOW)
+ assert empty_cache_mock.call_count > first_call_count, (
+ "empty_cache must be called on each HIGH→LOW (or LOW→HIGH) swap"
+ )
+
+ # Same-label re-get is a no-op; empty_cache must NOT be re-invoked.
+ before_no_op = empty_cache_mock.call_count
+ swapper.get(_ExpertSwapper.LOW)
+ assert empty_cache_mock.call_count == before_no_op, (
+ "Re-getting the active expert must short-circuit before empty_cache."
+ )
+
+ swapper.close()
+
+
+def test_outgoing_expert_force_unloaded_from_vram():
+ """Regression: on swap, the previous expert's weights must be explicitly forced
+ off VRAM via ``cached_model.full_unload_from_vram()``.
+
+ A14B users observed the high-noise transformer continuing to occupy ~9 GB of
+ VRAM during the low-noise step, because the cache's automatic offload heuristic
+ underestimated how much room the new expert needed when workspace memory from
+ the previous denoise step was still allocated. The swapper sidesteps that by
+ invoking full_unload_from_vram on the outgoing expert directly."""
+ log: list[str] = []
+ high_info = _FakeInfo("HIGH", nn.Linear(1, 1), log)
+ low_info = _FakeInfo("LOW", nn.Linear(1, 1), log)
+ ctx = _FakeContext({"high": high_info, "low": low_info}, log)
+
+ stub, _ = _stub_lora_context_manager(log)
+ with patch(
+ "invokeai.app.invocations.wan_denoise.LayerPatcher.apply_smart_model_patches",
+ side_effect=stub,
+ ):
+ swapper = _ExpertSwapper(
+ context=ctx,
+ high_model="high",
+ low_model="low",
+ inference_dtype=torch.bfloat16,
+ high_lora_factory=_make_factory(log, "HIGH"),
+ low_lora_factory=_make_factory(log, "LOW"),
+ )
+ # Initial load: nothing to unload yet.
+ swapper.get(_ExpertSwapper.HIGH)
+ assert high_info._cache_record.cached_model.unload_calls == 0
+ assert low_info._cache_record.cached_model.unload_calls == 0
+
+ # Swap to LOW: HIGH must be force-unloaded; LOW is the incoming expert and
+ # must not be unloaded.
+ swapper.get(_ExpertSwapper.LOW)
+ assert high_info._cache_record.cached_model.unload_calls == 1
+ assert low_info._cache_record.cached_model.unload_calls == 0
+
+ # Swap back to HIGH: LOW must now be force-unloaded.
+ swapper.get(_ExpertSwapper.HIGH)
+ assert low_info._cache_record.cached_model.unload_calls == 1
+
+ swapper.close()
+
+
+def test_device_context_released_when_lora_enter_raises():
+ """Regression: if the LoRA patcher's ``__enter__`` raises, the device context
+ must still be released on the next swap or close.
+
+ Earlier shape stashed ``self._active_device_ctx`` only after the LoRA enter
+ succeeded, so an exception there left the device context entered but
+ unreachable — ``_release`` saw ``None`` and walked away, leaving 8–9 GB of
+ GGUF expert weights pinned to GPU until the model cache LRU evicted them."""
+ log: list[str] = []
+ high_nn = nn.Linear(1, 1)
+ ctx = _FakeContext({"high": _FakeInfo("HIGH", high_nn, log)}, log)
+
+ class _RaisingLoraStub:
+ def __init__(self, *args, **kwargs) -> None:
+ pass
+
+ def __enter__(self):
+ raise RuntimeError("LoRA patcher blew up")
+
+ def __exit__(self, *_args):
+ return False
+
+ with patch(
+ "invokeai.app.invocations.wan_denoise.LayerPatcher.apply_smart_model_patches",
+ side_effect=lambda **_kwargs: _RaisingLoraStub(),
+ ):
+ swapper = _ExpertSwapper(
+ context=ctx,
+ high_model="high",
+ low_model=None,
+ inference_dtype=torch.bfloat16,
+ high_lora_factory=_make_factory(log, "HIGH"),
+ low_lora_factory=None,
+ )
+ with pytest.raises(RuntimeError, match="LoRA patcher blew up"):
+ swapper.get(_ExpertSwapper.HIGH)
+ # close() must succeed and must call the device context's __exit__ so
+ # the model leaves GPU. If the device context were unreachable,
+ # device-exit:HIGH would be missing from the log.
+ swapper.close()
+
+
+def test_device_context_released_when_lora_exit_raises():
+ """Counterpart to the enter-raises regression: if LoRA weight-restore (``__exit__``)
+ raises — e.g. OOM restoring original weights on a nearly full card — the device
+ context must still exit so the cache record unlocks and the expert leaves GPU."""
+ log: list[str] = []
+ high_nn = nn.Linear(1, 1)
+ ctx = _FakeContext({"high": _FakeInfo("HIGH", high_nn, log)}, log)
+
+ class _ExitRaisingLoraStub:
+ def __init__(self, *args, **kwargs) -> None:
+ pass
+
+ def __enter__(self):
+ return self
+
+ def __exit__(self, *_args):
+ raise RuntimeError("LoRA weight restore blew up")
+
+ with patch(
+ "invokeai.app.invocations.wan_denoise.LayerPatcher.apply_smart_model_patches",
+ side_effect=lambda **_kwargs: _ExitRaisingLoraStub(),
+ ):
+ swapper = _ExpertSwapper(
+ context=ctx,
+ high_model="high",
+ low_model=None,
+ inference_dtype=torch.bfloat16,
+ high_lora_factory=_make_factory(log, "HIGH"),
+ low_lora_factory=None,
+ )
+ swapper.get(_ExpertSwapper.HIGH)
+ with pytest.raises(RuntimeError, match="LoRA weight restore blew up"):
+ swapper.close()
+ assert "device-exit:HIGH" in log
+ # The swapper must also have cleared its slots so a later close() is a no-op.
+ assert swapper._active_device_ctx is None
+ assert swapper._active_lora_ctx is None
+
+ assert "device-exit:HIGH" in log, "device context must be exited even if LoRA enter raised"
+
+
+def test_force_unload_failure_does_not_break_swap():
+ """If full_unload_from_vram raises (e.g. cache evicted the entry between unlock
+ and now), the swap must still succeed. Reaching into a private attribute is the
+ pragmatic choice today; this test pins the defensive try/except so a future
+ refactor of LoadedModel doesn't break swap reliability."""
+ log: list[str] = []
+
+ class _RaisingCachedModel:
+ def full_unload_from_vram(self):
+ raise RuntimeError("cache evicted me between unlock and unload")
+
+ raising_high = _FakeInfo("HIGH", nn.Linear(1, 1), log)
+ raising_high._cache_record = _FakeCacheRecord(_RaisingCachedModel())
+ low_info = _FakeInfo("LOW", nn.Linear(1, 1), log)
+ ctx = _FakeContext({"high": raising_high, "low": low_info}, log)
+
+ stub, _ = _stub_lora_context_manager(log)
+ with patch(
+ "invokeai.app.invocations.wan_denoise.LayerPatcher.apply_smart_model_patches",
+ side_effect=stub,
+ ):
+ swapper = _ExpertSwapper(
+ context=ctx,
+ high_model="high",
+ low_model="low",
+ inference_dtype=torch.bfloat16,
+ high_lora_factory=_make_factory(log, "HIGH"),
+ low_lora_factory=_make_factory(log, "LOW"),
+ )
+ swapper.get(_ExpertSwapper.HIGH)
+ # Should not raise even though the outgoing expert's full_unload throws.
+ model = swapper.get(_ExpertSwapper.LOW)
+ assert model is low_info._model
+ swapper.close()
+
+
+def test_slots_cleared_when_device_exit_raises():
+ """Counterpart to the LoRA-exit regression (JPPhoto review 2026-07-21): if the
+ *device* context's ``__exit__`` raises, the swapper must still clear every active
+ slot — otherwise a later ``close()`` double-exits the already-exited LoRA context
+ and ``get()`` can hand back a stale model."""
+ log: list[str] = []
+ high_nn = nn.Linear(1, 1)
+
+ class _ExitRaisingDeviceCtx(_FakeModelOnDevice):
+ def __exit__(self, exc_type, exc_val, exc_tb):
+ self._log.append(f"device-exit:{self._label}")
+ raise RuntimeError("device teardown blew up")
+
+ class _ExitRaisingInfo(_FakeInfo):
+ def model_on_device(self):
+ return _ExitRaisingDeviceCtx(self._label, self._model, self._log)
+
+ ctx = _FakeContext({"high": _ExitRaisingInfo("HIGH", high_nn, log)}, log)
+ stub, _calls = _stub_lora_context_manager(log)
+ with patch(
+ "invokeai.app.invocations.wan_denoise.LayerPatcher.apply_smart_model_patches",
+ side_effect=stub,
+ ):
+ swapper = _ExpertSwapper(
+ context=ctx,
+ high_model="high",
+ low_model=None,
+ inference_dtype=torch.bfloat16,
+ high_lora_factory=_make_factory(log, "HIGH"),
+ low_lora_factory=None,
+ )
+ swapper.get(_ExpertSwapper.HIGH)
+
+ with pytest.raises(RuntimeError, match="device teardown blew up"):
+ swapper.close()
+
+ # Every slot must be cleared despite the device-exit failure...
+ assert swapper._active_device_ctx is None
+ assert swapper._active_lora_ctx is None
+ assert swapper._active_model is None
+ assert swapper._active_label is None
+
+ # ...so a second close is a true no-op: no double LoRA exit, no second
+ # device exit attempt.
+ swapper.close()
+
+ assert log.count("lora-exit") == 1
+ assert log.count("device-exit:HIGH") == 1
diff --git a/tests/app/invocations/test_wan_ideal_dimensions.py b/tests/app/invocations/test_wan_ideal_dimensions.py
new file mode 100644
index 00000000000..e2944043848
--- /dev/null
+++ b/tests/app/invocations/test_wan_ideal_dimensions.py
@@ -0,0 +1,144 @@
+"""Unit tests for WanI2VIdealDimensionsInvocation.
+
+The node is a pure math transform — no context dependencies — so we can call
+``invoke`` with ``None`` directly.
+"""
+
+import pytest
+
+from invokeai.app.invocations.wan_ideal_dimensions import (
+ WAN_TARGET_RESOLUTION_PX,
+ WanI2VIdealDimensionsInvocation,
+)
+
+
+def _resolve(w: int, h: int, target: str = "720p", rounding: str = "nearest") -> tuple[int, int]:
+ inv = WanI2VIdealDimensionsInvocation(
+ width=w,
+ height=h,
+ target_resolution=target, # type: ignore[arg-type]
+ rounding=rounding, # type: ignore[arg-type]
+ )
+ out = inv.invoke(None) # type: ignore[arg-type]
+ return out.width, out.height
+
+
+class TestCommonResolutions:
+ """The output table from the docs."""
+
+ @pytest.mark.parametrize(
+ "w, h, target, expected",
+ [
+ (1920, 1080, "720p", (1280, 720)),
+ (1080, 1920, "720p", (720, 1280)),
+ (832, 480, "720p", (1248, 720)),
+ (4032, 3024, "720p", (960, 720)),
+ (3840, 2160, "720p", (1280, 720)),
+ (1024, 1024, "720p", (720, 720)),
+ (1920, 1080, "480p", (848, 480)),
+ (1920, 1080, "1080p", (1920, 1088)), # 1080 → snaps to 1088 (next multiple of 16)
+ ],
+ )
+ def test_nearest(self, w: int, h: int, target: str, expected: tuple[int, int]) -> None:
+ assert _resolve(w, h, target=target) == expected
+
+
+class TestRoundingModes:
+ """Floor / ceiling produce the expected over- or under-shoot vs. nearest."""
+
+ def test_floor_never_exceeds_raw(self) -> None:
+ # 1920x1080 → 480p has raw_w = 853.33; floor → 848, ceil → 864
+ assert _resolve(1920, 1080, target="480p", rounding="floor") == (848, 480)
+ assert _resolve(1920, 1080, target="480p", rounding="ceiling") == (864, 480)
+
+ def test_floor_and_ceiling_diverge_for_non_grid_aspect(self) -> None:
+ # 21:9-ish: 2048x858, raw_w = 1718.27 → floor 1712, ceil 1728
+ assert _resolve(2048, 858, target="720p", rounding="floor") == (1712, 720)
+ assert _resolve(2048, 858, target="720p", rounding="ceiling") == (1728, 720)
+
+
+class TestPostconditions:
+ """Output invariants that must always hold."""
+
+ @pytest.mark.parametrize(
+ "w, h, target",
+ [
+ (1920, 1080, "480p"),
+ (1920, 1080, "720p"),
+ (1080, 1920, "720p"),
+ (832, 480, "720p"),
+ (2048, 858, "720p"),
+ (4032, 3024, "480p"),
+ (17, 17, "720p"), # tiny input
+ ],
+ )
+ @pytest.mark.parametrize("rounding", ["nearest", "floor", "ceiling"])
+ def test_output_dims_are_multiples_of_16(self, w: int, h: int, target: str, rounding: str) -> None:
+ ow, oh = _resolve(w, h, target=target, rounding=rounding)
+ assert ow % 16 == 0
+ assert oh % 16 == 0
+
+ @pytest.mark.parametrize(
+ "w, h, target",
+ [
+ (1920, 1080, "720p"),
+ (1080, 1920, "720p"),
+ (832, 480, "720p"),
+ ],
+ )
+ def test_output_aspect_ratio_within_1_percent(self, w: int, h: int, target: str) -> None:
+ ow, oh = _resolve(w, h, target=target)
+ input_aspect = w / h
+ output_aspect = ow / oh
+ # 16-grid snap can shift aspect by at most half a 16-step on the long axis,
+ # which is ~1.1% at 720 short.
+ assert abs(output_aspect - input_aspect) / input_aspect < 0.012
+
+ def test_smallest_valid_input_still_snaps_to_16_grid(self) -> None:
+ # 16×16 is the minimum input the guard accepts. The downstream clamp ensures
+ # the output is at least 16×16 even when the floor rounding would zero it.
+ ow, oh = _resolve(16, 16, target="480p", rounding="floor")
+ assert ow >= 16
+ assert oh >= 16
+
+
+class TestResolutionPresetTable:
+ """The dropdown values must map to the documented short-side pixel counts."""
+
+ def test_presets_cover_canonical_video_sizes(self) -> None:
+ assert WAN_TARGET_RESOLUTION_PX == {"480p": 480, "720p": 720, "1080p": 1080}
+
+
+class TestInputValidation:
+ """Reject obviously bad inputs at the schema layer."""
+
+ def test_zero_width_rejected(self) -> None:
+ from pydantic import ValidationError
+
+ with pytest.raises(ValidationError):
+ WanI2VIdealDimensionsInvocation(width=0, height=720)
+
+ def test_negative_height_rejected(self) -> None:
+ from pydantic import ValidationError
+
+ with pytest.raises(ValidationError):
+ WanI2VIdealDimensionsInvocation(width=720, height=-1)
+
+ def test_input_smaller_than_pixel_grid_rejected(self) -> None:
+ # If the longer side is below the 16-px Wan grid, the floor-rounding output
+ # would silently disconnect from the requested aspect ratio (clamped to
+ # 16×16 regardless of the source's actual shape). Fail fast instead.
+ with pytest.raises(ValueError, match="smaller than the Wan pixel grid"):
+ _resolve(8, 8, target="480p", rounding="floor")
+ with pytest.raises(ValueError, match="smaller than the Wan pixel grid"):
+ _resolve(15, 15, target="720p", rounding="nearest")
+
+ def test_unknown_resolution_rejected(self) -> None:
+ from pydantic import ValidationError
+
+ with pytest.raises(ValidationError):
+ WanI2VIdealDimensionsInvocation(
+ width=1920,
+ height=1080,
+ target_resolution="2160p", # type: ignore[arg-type]
+ )
diff --git a/tests/app/invocations/test_wan_latents_to_image.py b/tests/app/invocations/test_wan_latents_to_image.py
new file mode 100644
index 00000000000..549273a6f4c
--- /dev/null
+++ b/tests/app/invocations/test_wan_latents_to_image.py
@@ -0,0 +1,74 @@
+"""Tests for ``WanLatentsToImageInvocation`` input validation (JPPhoto review 2026-07-21).
+
+The bug: the node accepted any 5D latent tensor. Multi-frame video latents ran the full
+multi-frame VAE decode (under a working-memory estimate that assumed one frame) and then
+died in an opaque einops rank error at the final rearrange. The node must reject T>1
+latents early — before the VAE is even loaded — and point users at the video decode node.
+"""
+
+from unittest.mock import MagicMock
+
+import pytest
+import torch
+
+from invokeai.app.invocations.fields import LatentsField
+from invokeai.app.invocations.model import ModelIdentifierField, VAEField
+from invokeai.app.invocations.wan_latents_to_image import WanLatentsToImageInvocation
+from invokeai.backend.model_manager.taxonomy import BaseModelType, ModelType
+
+
+def _make_invocation() -> WanLatentsToImageInvocation:
+ vae_id = ModelIdentifierField(key="wan-vae", hash="h", name="wan-vae", base=BaseModelType.Wan, type=ModelType.VAE)
+ return WanLatentsToImageInvocation(
+ id="inv-1",
+ latents=LatentsField(latents_name="latents-1"),
+ vae=VAEField(vae=vae_id),
+ )
+
+
+def _make_context(latents: torch.Tensor) -> MagicMock:
+ ctx = MagicMock()
+ ctx.tensors.load.return_value = latents
+ return ctx
+
+
+@pytest.mark.parametrize("num_frames", [2, 5, 21])
+def test_multi_frame_latents_rejected_before_vae_load(num_frames: int) -> None:
+ ctx = _make_context(torch.zeros(1, 16, num_frames, 8, 8))
+
+ with pytest.raises(ValueError, match="frames of video"):
+ _make_invocation().invoke(ctx)
+
+ # The rejection must be cheap: no model load, no decode.
+ ctx.models.load.assert_not_called()
+
+
+def test_error_points_at_the_video_decode_node() -> None:
+ ctx = _make_context(torch.zeros(1, 16, 5, 8, 8))
+ with pytest.raises(ValueError, match="wan_l2v"):
+ _make_invocation().invoke(ctx)
+
+
+@pytest.mark.parametrize("shape", [(16, 8, 8), (1, 1, 16, 1, 8, 8)], ids=["3d", "6d"])
+def test_invalid_rank_rejected_before_vae_load(shape: tuple[int, ...]) -> None:
+ ctx = _make_context(torch.zeros(*shape))
+
+ with pytest.raises(ValueError, match="expects a 4D or 5D latent tensor"):
+ _make_invocation().invoke(ctx)
+
+ ctx.models.load.assert_not_called()
+
+
+@pytest.mark.parametrize(
+ "shape",
+ [(1, 16, 8, 8), (1, 16, 1, 8, 8)],
+ ids=["4d", "5d-single-frame"],
+)
+def test_valid_shapes_pass_validation(shape: tuple[int, ...]) -> None:
+ """4D and single-frame 5D latents proceed past the temporal check. The mocked model
+ load then fails the AutoencoderKLWan type check, proving execution got there."""
+ ctx = _make_context(torch.zeros(*shape))
+ ctx.models.load.return_value = MagicMock(model=object())
+
+ with pytest.raises(TypeError, match="Expected AutoencoderKLWan"):
+ _make_invocation().invoke(ctx)
diff --git a/tests/app/invocations/test_wan_latents_to_video_encoding.py b/tests/app/invocations/test_wan_latents_to_video_encoding.py
new file mode 100644
index 00000000000..d2ea3671f3a
--- /dev/null
+++ b/tests/app/invocations/test_wan_latents_to_video_encoding.py
@@ -0,0 +1,61 @@
+from unittest.mock import MagicMock
+
+import numpy as np
+import pytest
+import torch
+
+from invokeai.app.invocations.wan_latents_to_video import (
+ _iter_decoded_frames,
+ _validate_video_latent_batch,
+ _write_video_frames,
+)
+from invokeai.app.services.session_processor.session_processor_common import CanceledException
+
+
+def test_video_encoding_stops_when_canceled() -> None:
+ writer = MagicMock()
+ yielded: list[int] = []
+
+ def frames():
+ for index in range(5):
+ yielded.append(index)
+ yield np.zeros((4, 4, 3), dtype=np.uint8)
+
+ checks = iter([False, False, True])
+
+ with pytest.raises(CanceledException):
+ _write_video_frames(writer, frames(), lambda: next(checks))
+
+ assert writer.append_data.call_count == 2
+ assert yielded == [0, 1]
+
+
+def test_decoded_frames_are_converted_lazily_in_temporal_order() -> None:
+ decoded = torch.tensor([-2.0, 0.0, 0.0, 2.0]).view(1, 4, 1, 1).expand(3, -1, -1, -1).clone()
+ frames = _iter_decoded_frames(decoded)
+
+ first = next(frames)
+ decoded[:, 1] = 1.0
+ second = next(frames)
+ third = next(frames)
+ fourth = next(frames)
+
+ assert first.shape == (1, 1, 3)
+ assert first.dtype == np.uint8
+ assert first.tolist() == [[[0, 0, 0]]]
+ assert second.tolist() == [[[255, 255, 255]]]
+ assert third.tolist() == [[[128, 128, 128]]]
+ assert fourth.tolist() == [[[255, 255, 255]]]
+ with pytest.raises(StopIteration):
+ next(frames)
+
+
+@pytest.mark.parametrize("shape", [(1, 16, 2, 4, 4), (1, 16, 4, 4)])
+def test_single_video_latent_batch_is_accepted(shape: tuple[int, ...]) -> None:
+ _validate_video_latent_batch(torch.zeros(shape))
+
+
+@pytest.mark.parametrize("shape", [(2, 16, 2, 4, 4), (2, 16, 4, 4)])
+def test_multiple_video_latent_batches_are_rejected(shape: tuple[int, ...]) -> None:
+ with pytest.raises(ValueError, match="batch size 1"):
+ _validate_video_latent_batch(torch.zeros(shape))
diff --git a/tests/app/invocations/test_wan_lora_loader.py b/tests/app/invocations/test_wan_lora_loader.py
new file mode 100644
index 00000000000..12851c88883
--- /dev/null
+++ b/tests/app/invocations/test_wan_lora_loader.py
@@ -0,0 +1,462 @@
+"""Tests for ``WanLoRALoaderInvocation`` target resolution and routing."""
+
+from unittest.mock import MagicMock
+
+import pytest
+
+from invokeai.app.invocations.model import LoRAField, ModelIdentifierField, WanTransformerField
+from invokeai.app.invocations.wan_lora_loader import (
+ WanLoRACollectionLoader,
+ WanLoRALoaderInvocation,
+ _resolve_target,
+)
+from invokeai.backend.model_manager.taxonomy import (
+ BaseModelType,
+ ModelType,
+ WanLoRAVariantType,
+ WanVariantType,
+)
+
+# --------------------------------------------------------------------------
+# _resolve_target — pure function, no mocks needed.
+# --------------------------------------------------------------------------
+
+
+class TestResolveTarget:
+ @pytest.mark.parametrize(
+ "target, expert, expected",
+ [
+ ("auto", None, (True, True)),
+ ("auto", "high", (True, False)),
+ ("auto", "low", (False, True)),
+ ("both", None, (True, True)),
+ ("both", "high", (True, True)),
+ ("both", "low", (True, True)),
+ ("high", None, (True, False)),
+ ("high", "low", (True, False)), # explicit target overrides config
+ ("low", None, (False, True)),
+ ("low", "high", (False, True)),
+ ],
+ )
+ def test_target_resolution(self, target, expert, expected):
+ assert _resolve_target(target, expert) == expected
+
+
+# --------------------------------------------------------------------------
+# WanLoRALoaderInvocation — routing into primary vs low-noise lists.
+# --------------------------------------------------------------------------
+
+
+def _make_lora_field(key: str = "lora-1") -> ModelIdentifierField:
+ return ModelIdentifierField(
+ key=key,
+ hash=f"hash-{key}",
+ name=f"name-{key}",
+ base=BaseModelType.Wan,
+ type=ModelType.LoRA,
+ )
+
+
+def _make_transformer_field() -> WanTransformerField:
+ transformer_id = ModelIdentifierField(
+ key="wan-main",
+ hash="wan-main-hash",
+ name="wan-main",
+ base=BaseModelType.Wan,
+ type=ModelType.Main,
+ )
+ return WanTransformerField(transformer=transformer_id)
+
+
+def _make_lora_config(
+ expert: str | None = None,
+ variant=None,
+ base: BaseModelType = BaseModelType.Wan,
+ model_type: ModelType = ModelType.LoRA,
+) -> MagicMock:
+ """A resolved LoRA config: the loaders validate type/base on the *config*, not the
+ client-supplied identifier, so mocks must carry real taxonomy values."""
+ config = MagicMock()
+ config.expert = expert
+ config.variant = variant
+ config.base = base
+ config.type = model_type
+ return config
+
+
+def _make_main_config(variant=None) -> MagicMock:
+ config = MagicMock()
+ config.variant = variant
+ config.base = BaseModelType.Wan
+ config.type = ModelType.Main
+ return config
+
+
+def _make_context(
+ lora_expert: str | None,
+ lora_config: MagicMock | None = None,
+ main_variant=None,
+) -> MagicMock:
+ """Mock context resolving LoRA keys to a Wan LoRA config (with the given expert
+ tag) and the 'wan-main' transformer key to a Wan main config."""
+ ctx = MagicMock()
+ ctx.models.exists.return_value = True
+ resolved_lora = lora_config if lora_config is not None else _make_lora_config(expert=lora_expert)
+ main_config = _make_main_config(variant=main_variant)
+
+ def _get_config(model_id: ModelIdentifierField) -> MagicMock:
+ return main_config if model_id.key == "wan-main" else resolved_lora
+
+ ctx.models.get_config.side_effect = _get_config
+ return ctx
+
+
+class TestSingleLoaderRouting:
+ def test_auto_untagged_goes_to_both(self):
+ inv = WanLoRALoaderInvocation(id="inv-1", lora=_make_lora_field(), transformer=_make_transformer_field())
+ out = inv.invoke(_make_context(lora_expert=None))
+ assert out.transformer is not None
+ assert len(out.transformer.loras) == 1
+ assert len(out.transformer.loras_low_noise) == 1
+
+ def test_auto_high_tag_goes_to_primary_only(self):
+ inv = WanLoRALoaderInvocation(id="inv-1", lora=_make_lora_field(), transformer=_make_transformer_field())
+ out = inv.invoke(_make_context(lora_expert="high"))
+ assert out.transformer is not None
+ assert len(out.transformer.loras) == 1
+ assert len(out.transformer.loras_low_noise) == 0
+
+ def test_auto_low_tag_goes_to_low_only(self):
+ inv = WanLoRALoaderInvocation(id="inv-1", lora=_make_lora_field(), transformer=_make_transformer_field())
+ out = inv.invoke(_make_context(lora_expert="low"))
+ assert out.transformer is not None
+ assert len(out.transformer.loras) == 0
+ assert len(out.transformer.loras_low_noise) == 1
+
+ def test_explicit_target_overrides_tag(self):
+ inv = WanLoRALoaderInvocation(
+ id="inv-1",
+ lora=_make_lora_field(),
+ target="high",
+ transformer=_make_transformer_field(),
+ )
+ out = inv.invoke(_make_context(lora_expert="low"))
+ assert out.transformer is not None
+ assert len(out.transformer.loras) == 1
+ assert len(out.transformer.loras_low_noise) == 0
+
+ def test_weight_propagates(self):
+ inv = WanLoRALoaderInvocation(
+ id="inv-1",
+ lora=_make_lora_field(),
+ weight=0.42,
+ transformer=_make_transformer_field(),
+ )
+ out = inv.invoke(_make_context(lora_expert=None))
+ assert out.transformer is not None
+ assert out.transformer.loras[0].weight == pytest.approx(0.42)
+
+ def test_unknown_lora_raises(self):
+ ctx = _make_context(lora_expert=None)
+ ctx.models.exists.return_value = False
+ inv = WanLoRALoaderInvocation(id="inv-1", lora=_make_lora_field(), transformer=_make_transformer_field())
+ with pytest.raises(ValueError, match="Unknown lora"):
+ inv.invoke(ctx)
+
+ def test_duplicate_on_primary_raises(self):
+ existing = LoRAField(lora=_make_lora_field(key="dup"), weight=0.5)
+ transformer = _make_transformer_field()
+ transformer.loras.append(existing)
+
+ inv = WanLoRALoaderInvocation(id="inv-1", lora=_make_lora_field(key="dup"), transformer=transformer)
+ with pytest.raises(ValueError, match="already applied to primary"):
+ inv.invoke(_make_context(lora_expert="high"))
+
+ def test_duplicate_on_low_noise_raises(self):
+ existing = LoRAField(lora=_make_lora_field(key="dup"), weight=0.5)
+ transformer = _make_transformer_field()
+ transformer.loras_low_noise.append(existing)
+
+ inv = WanLoRALoaderInvocation(id="inv-1", lora=_make_lora_field(key="dup"), transformer=transformer)
+ with pytest.raises(ValueError, match="already applied to low-noise"):
+ inv.invoke(_make_context(lora_expert="low"))
+
+ def test_no_transformer_returns_empty_output(self):
+ inv = WanLoRALoaderInvocation(id="inv-1", lora=_make_lora_field(), transformer=None)
+ out = inv.invoke(_make_context(lora_expert=None))
+ assert out.transformer is None
+
+
+# --------------------------------------------------------------------------
+# Collection loader — list routing + base validation.
+# --------------------------------------------------------------------------
+
+
+class TestCollectionLoaderRouting:
+ def test_routes_mixed_tagged_loras(self):
+ """A collection of three LoRAs (high, low, untagged) routes correctly."""
+ high_lora = LoRAField(lora=_make_lora_field(key="lora-high"), weight=0.5)
+ low_lora = LoRAField(lora=_make_lora_field(key="lora-low"), weight=0.6)
+ untagged_lora = LoRAField(lora=_make_lora_field(key="lora-any"), weight=0.7)
+
+ inv = WanLoRACollectionLoader(
+ id="inv-1",
+ loras=[high_lora, low_lora, untagged_lora],
+ transformer=_make_transformer_field(),
+ )
+
+ # The collection loader queries each LoRA's config separately. Mock
+ # get_config to return different expert tags by lora key.
+ expert_by_key = {"lora-high": "high", "lora-low": "low", "lora-any": None}
+ ctx = MagicMock()
+ ctx.models.exists.return_value = True
+
+ def get_config(field: ModelIdentifierField) -> MagicMock:
+ if field.key == "wan-main":
+ return _make_main_config()
+ return _make_lora_config(expert=expert_by_key[field.key])
+
+ ctx.models.get_config.side_effect = get_config
+ out = inv.invoke(ctx)
+ assert out.transformer is not None
+
+ primary_keys = {item.lora.key for item in out.transformer.loras}
+ low_keys = {item.lora.key for item in out.transformer.loras_low_noise}
+ # high -> primary only; low -> low only; untagged -> both
+ assert primary_keys == {"lora-high", "lora-any"}
+ assert low_keys == {"lora-low", "lora-any"}
+
+ def test_rejects_non_wan_base(self):
+ """The identifier claims Wan, but the key resolves to a Flux LoRA — the
+ resolved config is authoritative."""
+ mislabeled_lora = LoRAField(lora=_make_lora_field(key="not-wan"), weight=0.5)
+ inv = WanLoRACollectionLoader(id="inv-1", loras=[mislabeled_lora], transformer=_make_transformer_field())
+ ctx = _make_context(lora_expert=None, lora_config=_make_lora_config(base=BaseModelType.Flux))
+ with pytest.raises(ValueError, match="not a Wan LoRA"):
+ inv.invoke(ctx)
+
+ def test_skips_duplicates(self):
+ dup_lora = LoRAField(lora=_make_lora_field(key="dup"), weight=0.5)
+ inv = WanLoRACollectionLoader(
+ id="inv-1",
+ loras=[dup_lora, dup_lora],
+ transformer=_make_transformer_field(),
+ )
+ out = inv.invoke(_make_context(lora_expert=None))
+ assert out.transformer is not None
+ assert len(out.transformer.loras) == 1
+
+ def test_no_loras_returns_clean_copy(self):
+ inv = WanLoRACollectionLoader(id="inv-1", loras=None, transformer=_make_transformer_field())
+ out = inv.invoke(MagicMock())
+ assert out.transformer is not None
+ assert len(out.transformer.loras) == 0
+ assert len(out.transformer.loras_low_noise) == 0
+
+
+# --------------------------------------------------------------------------
+# LoRA-variant vs transformer-variant validation (A14B vs 5B).
+# --------------------------------------------------------------------------
+
+
+def _make_variant_context(lora_variant, main_variant) -> MagicMock:
+ """Context whose get_config returns a LoRA config (with variant + expert) for LoRA
+ keys and a main config (with variant) for the transformer key."""
+ return _make_context(
+ lora_expert=None,
+ lora_config=_make_lora_config(variant=lora_variant),
+ main_variant=main_variant,
+ )
+
+
+class TestVariantValidation:
+ """An A14B LoRA against a 5B main (or vice versa) crashes deep in the layer patcher
+ with an opaque shape error — the loaders must reject the mismatch up front."""
+
+ @pytest.mark.parametrize(
+ "lora_variant, main_variant",
+ [
+ (WanLoRAVariantType.A14B, WanVariantType.TI2V_5B),
+ (WanLoRAVariantType.Wan5B, WanVariantType.T2V_A14B),
+ (WanLoRAVariantType.Wan5B, WanVariantType.I2V_A14B),
+ ],
+ )
+ def test_single_loader_rejects_mismatch(self, lora_variant, main_variant):
+ inv = WanLoRALoaderInvocation(id="inv-1", lora=_make_lora_field(), transformer=_make_transformer_field())
+ with pytest.raises(ValueError, match="not interchangeable"):
+ inv.invoke(_make_variant_context(lora_variant, main_variant))
+
+ @pytest.mark.parametrize(
+ "lora_variant, main_variant",
+ [
+ (WanLoRAVariantType.A14B, WanVariantType.T2V_A14B),
+ (WanLoRAVariantType.A14B, WanVariantType.I2V_A14B),
+ (WanLoRAVariantType.Wan5B, WanVariantType.TI2V_5B),
+ (None, WanVariantType.T2V_A14B), # unrecorded LoRA variant -> skip check
+ (WanLoRAVariantType.A14B, None), # unrecorded main variant -> skip check
+ ],
+ )
+ def test_single_loader_accepts_match_or_unknown(self, lora_variant, main_variant):
+ inv = WanLoRALoaderInvocation(id="inv-1", lora=_make_lora_field(), transformer=_make_transformer_field())
+ out = inv.invoke(_make_variant_context(lora_variant, main_variant))
+ assert out.transformer is not None
+ assert len(out.transformer.loras) == 1
+
+ def test_collection_loader_rejects_mismatch(self):
+ lora = LoRAField(lora=_make_lora_field(), weight=1.0)
+ inv = WanLoRACollectionLoader(id="inv-1", loras=[lora], transformer=_make_transformer_field())
+ with pytest.raises(ValueError, match="not interchangeable"):
+ inv.invoke(_make_variant_context(WanLoRAVariantType.Wan5B, WanVariantType.T2V_A14B))
+
+ def test_collection_loader_accepts_match(self):
+ lora = LoRAField(lora=_make_lora_field(), weight=1.0)
+ inv = WanLoRACollectionLoader(id="inv-1", loras=[lora], transformer=_make_transformer_field())
+ out = inv.invoke(_make_variant_context(WanLoRAVariantType.A14B, WanVariantType.T2V_A14B))
+ assert out.transformer is not None
+ assert len(out.transformer.loras) == 1
+
+
+# --------------------------------------------------------------------------
+# Resolved-config validation — the identifier's base/type are client-supplied
+# and untrusted; only the config the key resolves to counts (JPPhoto review
+# 2026-07-21).
+# --------------------------------------------------------------------------
+
+
+class TestResolvedConfigValidation:
+ @pytest.mark.parametrize(
+ "bad_config",
+ [
+ _make_lora_config(base=BaseModelType.Flux), # Flux LoRA labeled as Wan
+ _make_lora_config(base=BaseModelType.StableDiffusionXL), # SDXL LoRA labeled as Wan
+ _make_lora_config(model_type=ModelType.Main), # a main-model key labeled as a LoRA
+ ],
+ ids=["flux-lora", "sdxl-lora", "main-model"],
+ )
+ def test_single_loader_rejects_mislabeled_key(self, bad_config):
+ inv = WanLoRALoaderInvocation(id="inv-1", lora=_make_lora_field(), transformer=_make_transformer_field())
+ with pytest.raises(ValueError, match="not a Wan LoRA"):
+ inv.invoke(_make_context(lora_expert=None, lora_config=bad_config))
+
+ def test_single_loader_rejects_mislabeled_key_even_without_transformer(self):
+ """The check must not depend on a transformer being wired."""
+ inv = WanLoRALoaderInvocation(id="inv-1", lora=_make_lora_field(), transformer=None)
+ with pytest.raises(ValueError, match="not a Wan LoRA"):
+ inv.invoke(_make_context(lora_expert=None, lora_config=_make_lora_config(model_type=ModelType.Main)))
+
+ def test_collection_loader_rejects_mislabeled_key(self):
+ lora = LoRAField(lora=_make_lora_field(), weight=1.0)
+ inv = WanLoRACollectionLoader(id="inv-1", loras=[lora], transformer=_make_transformer_field())
+ with pytest.raises(ValueError, match="not a Wan LoRA"):
+ inv.invoke(_make_context(lora_expert=None, lora_config=_make_lora_config(model_type=ModelType.Main)))
+
+ def test_real_wan_lora_still_accepted(self):
+ inv = WanLoRALoaderInvocation(id="inv-1", lora=_make_lora_field(), transformer=_make_transformer_field())
+ out = inv.invoke(_make_context(lora_expert=None))
+ assert out.transformer is not None
+ assert len(out.transformer.loras) == 1
+
+
+# --------------------------------------------------------------------------
+# Collection loader vs already-applied LoRAs — chaining loaders must not
+# silently double a LoRA's effective weight (JPPhoto review 2026-07-21).
+# --------------------------------------------------------------------------
+
+
+class TestCollectionLoaderUpstreamDuplicates:
+ def test_rejects_lora_already_on_primary_list(self):
+ transformer = _make_transformer_field()
+ transformer.loras.append(LoRAField(lora=_make_lora_field(key="dup"), weight=0.5))
+
+ inv = WanLoRACollectionLoader(
+ id="inv-1",
+ loras=[LoRAField(lora=_make_lora_field(key="dup"), weight=0.5)],
+ transformer=transformer,
+ )
+ with pytest.raises(ValueError, match="already applied to primary"):
+ inv.invoke(_make_context(lora_expert=None))
+
+ def test_rejects_lora_already_on_low_noise_list(self):
+ transformer = _make_transformer_field()
+ transformer.loras_low_noise.append(LoRAField(lora=_make_lora_field(key="dup"), weight=0.5))
+
+ inv = WanLoRACollectionLoader(
+ id="inv-1",
+ loras=[LoRAField(lora=_make_lora_field(key="dup"), weight=0.5)],
+ transformer=transformer,
+ )
+ with pytest.raises(ValueError, match="already applied to low-noise"):
+ inv.invoke(_make_context(lora_expert="low"))
+
+ def test_new_key_appends_alongside_existing(self):
+ transformer = _make_transformer_field()
+ transformer.loras.append(LoRAField(lora=_make_lora_field(key="existing"), weight=0.5))
+
+ inv = WanLoRACollectionLoader(
+ id="inv-1",
+ loras=[LoRAField(lora=_make_lora_field(key="new"), weight=0.5)],
+ transformer=transformer,
+ )
+ out = inv.invoke(_make_context(lora_expert="high"))
+ assert out.transformer is not None
+ assert [item.lora.key for item in out.transformer.loras] == ["existing", "new"]
+
+
+# --------------------------------------------------------------------------
+# TI2V-5B inert low routing — the single-transformer path never consumes the
+# low-noise list, so low-only routing must warn (JPPhoto review 2026-07-21).
+# --------------------------------------------------------------------------
+
+
+class TestInertLowRoutingWarning:
+ def test_single_loader_warns_for_low_only_routing_on_ti2v(self):
+ inv = WanLoRALoaderInvocation(
+ id="inv-1", lora=_make_lora_field(), target="low", transformer=_make_transformer_field()
+ )
+ ctx = _make_context(
+ lora_expert=None,
+ lora_config=_make_lora_config(variant=WanLoRAVariantType.Wan5B),
+ main_variant=WanVariantType.TI2V_5B,
+ )
+ out = inv.invoke(ctx)
+ assert out.transformer is not None
+ ctx.logger.warning.assert_called_once()
+ assert "no effect" in ctx.logger.warning.call_args.args[0]
+
+ def test_collection_loader_warns_for_low_tagged_lora_on_ti2v(self):
+ lora = LoRAField(lora=_make_lora_field(), weight=1.0)
+ inv = WanLoRACollectionLoader(id="inv-1", loras=[lora], transformer=_make_transformer_field())
+ ctx = _make_context(
+ lora_expert="low",
+ lora_config=_make_lora_config(expert="low", variant=WanLoRAVariantType.Wan5B),
+ main_variant=WanVariantType.TI2V_5B,
+ )
+ out = inv.invoke(ctx)
+ assert out.transformer is not None
+ ctx.logger.warning.assert_called_once()
+
+ @pytest.mark.parametrize("target", ["auto", "both", "high"])
+ def test_no_warning_when_primary_list_is_reached(self, target):
+ """Routing that touches the primary list is consumed on TI2V — no warning."""
+ inv = WanLoRALoaderInvocation(
+ id="inv-1", lora=_make_lora_field(), target=target, transformer=_make_transformer_field()
+ )
+ ctx = _make_context(
+ lora_expert=None,
+ lora_config=_make_lora_config(variant=WanLoRAVariantType.Wan5B),
+ main_variant=WanVariantType.TI2V_5B,
+ )
+ inv.invoke(ctx)
+ ctx.logger.warning.assert_not_called()
+
+ def test_no_warning_for_low_routing_on_a14b(self):
+ inv = WanLoRALoaderInvocation(
+ id="inv-1", lora=_make_lora_field(), target="low", transformer=_make_transformer_field()
+ )
+ ctx = _make_context(
+ lora_expert=None,
+ lora_config=_make_lora_config(variant=WanLoRAVariantType.A14B),
+ main_variant=WanVariantType.T2V_A14B,
+ )
+ inv.invoke(ctx)
+ ctx.logger.warning.assert_not_called()
diff --git a/tests/app/invocations/test_wan_model_loader.py b/tests/app/invocations/test_wan_model_loader.py
new file mode 100644
index 00000000000..9af818f0702
--- /dev/null
+++ b/tests/app/invocations/test_wan_model_loader.py
@@ -0,0 +1,237 @@
+from types import SimpleNamespace
+from unittest.mock import MagicMock
+
+import pytest
+
+from invokeai.app.invocations.model import ModelIdentifierField
+from invokeai.app.invocations.wan_model_loader import WanModelLoaderInvocation
+from invokeai.backend.model_manager.taxonomy import ModelFormat, WanVariantType
+
+
+def _model(key: str) -> ModelIdentifierField:
+ return ModelIdentifierField(key=key, hash="hash", name=key, base="wan", type="main")
+
+
+def _config(
+ name: str,
+ variant: WanVariantType,
+ expert: str,
+ *,
+ format: ModelFormat = ModelFormat.GGUFQuantized,
+ has_dual_expert: bool = False,
+ boundary_ratio: float | None = None,
+) -> SimpleNamespace:
+ return SimpleNamespace(
+ name=name,
+ format=format,
+ variant=variant,
+ expert=expert,
+ has_dual_expert=has_dual_expert,
+ boundary_ratio=boundary_ratio,
+ )
+
+
+def _invoke(
+ main_config: SimpleNamespace,
+ low_config: SimpleNamespace | None = None,
+ component_config: SimpleNamespace | None = None,
+ *,
+ use_component_vae: bool = False,
+ vae_latent_channels: int | None = None,
+):
+ main = _model("main")
+ low = _model("low") if low_config is not None else None
+ context = MagicMock()
+ configs = {"main": main_config}
+ if low_config is not None:
+ configs["low"] = low_config
+ component = _model("component") if component_config is not None else None
+ if component_config is not None:
+ configs["component"] = component_config
+ if not use_component_vae:
+ if vae_latent_channels is None:
+ vae_latent_channels = 48 if main_config.variant == WanVariantType.TI2V_5B else 16
+ configs["vae"] = SimpleNamespace(name="vae", latent_channels=vae_latent_channels)
+ context.models.get_config.side_effect = lambda model: configs[model.key]
+ invocation = WanModelLoaderInvocation(
+ id="test",
+ model=main,
+ transformer_low_noise_model=low,
+ vae_model=None if use_component_vae else _model("vae"),
+ wan_t5_encoder_model=_model("t5"),
+ component_source=component,
+ )
+ return invocation.invoke(context)
+
+
+@pytest.mark.parametrize("variant", [WanVariantType.T2V_A14B, WanVariantType.I2V_A14B])
+@pytest.mark.parametrize("main_expert,low_expert", [("high", "low"), ("low", "high")])
+def test_gguf_loader_accepts_valid_expert_pair_in_either_order(
+ variant: WanVariantType, main_expert: str, low_expert: str
+) -> None:
+ output = _invoke(
+ _config("main", variant, main_expert),
+ _config("low", variant, low_expert),
+ )
+
+ assert output.transformer.transformer.key == ("main" if main_expert == "high" else "low")
+ assert output.transformer.transformer_low_noise is not None
+ assert output.transformer.transformer_low_noise.key == ("low" if low_expert == "low" else "main")
+
+
+@pytest.mark.parametrize(
+ "main_config,low_config",
+ [
+ (
+ _config("main", WanVariantType.T2V_A14B, "high"),
+ _config("low", WanVariantType.I2V_A14B, "low"),
+ ),
+ (
+ _config("main", WanVariantType.T2V_A14B, "high"),
+ _config("low", WanVariantType.T2V_A14B, "high"),
+ ),
+ (
+ _config("main", WanVariantType.T2V_A14B, "high"),
+ _config("low", WanVariantType.T2V_A14B, "none"),
+ ),
+ ],
+)
+def test_gguf_loader_rejects_invalid_expert_pair(main_config: SimpleNamespace, low_config: SimpleNamespace) -> None:
+ with pytest.raises(ValueError, match="expert|variant"):
+ _invoke(main_config, low_config)
+
+
+@pytest.mark.parametrize("low_variant", [WanVariantType.TI2V_5B, WanVariantType.T2V_A14B])
+def test_ti2v_5b_main_ignores_wired_low_noise_model(low_variant: WanVariantType) -> None:
+ """The field docs promise 'Transformer (Low Noise)' is ignored for the single-expert
+ TI2V-5B — e.g. a wire left over from an A14B session must not abort the run."""
+ output = _invoke(
+ _config("main", WanVariantType.TI2V_5B, "none"),
+ _config("low", low_variant, "low"),
+ )
+
+ assert output.transformer.transformer.key == "main"
+ assert output.transformer.transformer_low_noise is None
+
+
+@pytest.mark.parametrize("expert", ["low", "none"])
+def test_gguf_loader_rejects_non_high_primary_without_pair(expert: str) -> None:
+ with pytest.raises(ValueError, match="high-noise"):
+ _invoke(_config("main", WanVariantType.T2V_A14B, expert))
+
+
+@pytest.mark.parametrize(
+ "variant,expected",
+ [(WanVariantType.T2V_A14B, 0.875), (WanVariantType.I2V_A14B, 0.9)],
+)
+def test_gguf_loader_uses_variant_boundary_default(variant: WanVariantType, expected: float) -> None:
+ output = _invoke(_config("main", variant, "high"))
+
+ assert output.transformer.boundary_ratio == expected
+
+
+def test_diffusers_i2v_loader_uses_variant_boundary_default_when_metadata_missing() -> None:
+ output = _invoke(
+ _config(
+ "main",
+ WanVariantType.I2V_A14B,
+ "none",
+ format=ModelFormat.Diffusers,
+ has_dual_expert=True,
+ )
+ )
+
+ assert output.transformer.boundary_ratio == 0.9
+
+
+def test_gguf_loader_uses_matching_component_source_boundary() -> None:
+ output = _invoke(
+ _config("main", WanVariantType.I2V_A14B, "high"),
+ component_config=_config(
+ "component", WanVariantType.I2V_A14B, "none", format=ModelFormat.Diffusers, boundary_ratio=0.91
+ ),
+ )
+
+ assert output.transformer.boundary_ratio == 0.91
+
+
+def test_gguf_loader_ignores_mismatched_component_source_boundary() -> None:
+ output = _invoke(
+ _config("main", WanVariantType.I2V_A14B, "high"),
+ component_config=_config(
+ "component", WanVariantType.T2V_A14B, "none", format=ModelFormat.Diffusers, boundary_ratio=0.875
+ ),
+ )
+
+ assert output.transformer.boundary_ratio == 0.9
+
+
+@pytest.mark.parametrize(
+ "main_variant,component_variant",
+ [
+ (WanVariantType.TI2V_5B, WanVariantType.T2V_A14B),
+ (WanVariantType.T2V_A14B, WanVariantType.TI2V_5B),
+ ],
+)
+def test_gguf_loader_rejects_component_source_with_incompatible_vae_family(
+ main_variant: WanVariantType, component_variant: WanVariantType
+) -> None:
+ with pytest.raises(ValueError, match="VAE"):
+ _invoke(
+ _config("main", main_variant, "none" if main_variant == WanVariantType.TI2V_5B else "high"),
+ component_config=_config("component", component_variant, "none", format=ModelFormat.Diffusers),
+ use_component_vae=True,
+ )
+
+
+@pytest.mark.parametrize(
+ "main_variant,component_variant",
+ [
+ (WanVariantType.TI2V_5B, WanVariantType.TI2V_5B),
+ (WanVariantType.T2V_A14B, WanVariantType.I2V_A14B),
+ (WanVariantType.I2V_A14B, WanVariantType.T2V_A14B),
+ ],
+)
+def test_gguf_loader_accepts_component_source_with_compatible_vae_family(
+ main_variant: WanVariantType, component_variant: WanVariantType
+) -> None:
+ output = _invoke(
+ _config("main", main_variant, "none" if main_variant == WanVariantType.TI2V_5B else "high"),
+ component_config=_config("component", component_variant, "none", format=ModelFormat.Diffusers),
+ use_component_vae=True,
+ )
+
+ assert output.vae.vae.key == "component"
+
+
+@pytest.mark.parametrize(
+ "main_variant,vae_latent_channels",
+ [
+ (WanVariantType.TI2V_5B, 16),
+ (WanVariantType.T2V_A14B, 48),
+ (WanVariantType.I2V_A14B, 48),
+ ],
+)
+def test_loader_rejects_incompatible_standalone_vae(main_variant: WanVariantType, vae_latent_channels: int) -> None:
+ with pytest.raises(ValueError, match="VAE"):
+ _invoke(
+ _config("main", main_variant, "none" if main_variant == WanVariantType.TI2V_5B else "high"),
+ vae_latent_channels=vae_latent_channels,
+ )
+
+
+@pytest.mark.parametrize(
+ "main_variant,vae_latent_channels",
+ [
+ (WanVariantType.TI2V_5B, 48),
+ (WanVariantType.T2V_A14B, 16),
+ (WanVariantType.I2V_A14B, 16),
+ ],
+)
+def test_loader_accepts_compatible_standalone_vae(main_variant: WanVariantType, vae_latent_channels: int) -> None:
+ output = _invoke(
+ _config("main", main_variant, "none" if main_variant == WanVariantType.TI2V_5B else "high"),
+ vae_latent_channels=vae_latent_channels,
+ )
+
+ assert output.vae.vae.key == "vae"
diff --git a/tests/app/invocations/test_wan_ti2v_ideal_dimensions.py b/tests/app/invocations/test_wan_ti2v_ideal_dimensions.py
new file mode 100644
index 00000000000..80a91b3e967
--- /dev/null
+++ b/tests/app/invocations/test_wan_ti2v_ideal_dimensions.py
@@ -0,0 +1,154 @@
+"""Unit tests for WanTI2VIdealDimensionsInvocation.
+
+Mirrors ``test_wan_ideal_dimensions.py`` but for the TI2V-5B variant, which
+snaps to a multiple of 32 (16x Wan 2.2-VAE × 2x transformer patch) instead of
+16. The node is a pure math transform — no context dependencies — so we can
+call ``invoke`` with ``None`` directly.
+"""
+
+import pytest
+
+from invokeai.app.invocations.wan_ideal_dimensions import (
+ WAN_TARGET_RESOLUTION_PX,
+ WAN_TI2V_PIXEL_MULTIPLE,
+ WanTI2VIdealDimensionsInvocation,
+)
+
+
+def _resolve(w: int, h: int, target: str = "720p", rounding: str = "nearest") -> tuple[int, int]:
+ inv = WanTI2VIdealDimensionsInvocation(
+ width=w,
+ height=h,
+ target_resolution=target, # type: ignore[arg-type]
+ rounding=rounding, # type: ignore[arg-type]
+ )
+ out = inv.invoke(None) # type: ignore[arg-type]
+ return out.width, out.height
+
+
+class TestCommonResolutions:
+ """Verified output table for the 32-px grid."""
+
+ @pytest.mark.parametrize(
+ "w, h, target, expected",
+ [
+ (1920, 1080, "720p", (1280, 704)),
+ (1080, 1920, "720p", (704, 1280)),
+ (832, 480, "720p", (1248, 704)),
+ (4032, 3024, "720p", (960, 704)),
+ (3840, 2160, "720p", (1280, 704)),
+ (1024, 1024, "720p", (704, 704)),
+ (1920, 1080, "480p", (864, 480)),
+ (1920, 1080, "1080p", (1920, 1088)), # 1080 → 1088 (next multiple of 32)
+ # The reported failing case: 720x480 is not a multiple of 32 (720 % 32 != 0);
+ # the node snaps it to valid dims at both presets.
+ (720, 480, "480p", (704, 480)),
+ (720, 480, "720p", (1088, 704)),
+ ],
+ )
+ def test_nearest(self, w: int, h: int, target: str, expected: tuple[int, int]) -> None:
+ assert _resolve(w, h, target=target) == expected
+
+
+class TestRoundingModes:
+ """Floor / ceiling produce the expected over- or under-shoot vs. nearest."""
+
+ def test_floor_never_exceeds_raw(self) -> None:
+ # 1920x1080 → 480p has raw_w = 853.33; floor → 832, ceil → 864
+ assert _resolve(1920, 1080, target="480p", rounding="floor") == (832, 480)
+ assert _resolve(1920, 1080, target="480p", rounding="ceiling") == (864, 480)
+
+ def test_floor_and_ceiling_diverge_for_non_grid_aspect(self) -> None:
+ # 2048x858, raw_w = 1718.6 → floor 1696, ceil 1728
+ assert _resolve(2048, 858, target="720p", rounding="floor") == (1696, 704)
+ assert _resolve(2048, 858, target="720p", rounding="ceiling") == (1728, 736)
+
+
+class TestPostconditions:
+ """Output invariants that must always hold."""
+
+ @pytest.mark.parametrize(
+ "w, h, target",
+ [
+ (1920, 1080, "480p"),
+ (1920, 1080, "720p"),
+ (1080, 1920, "720p"),
+ (832, 480, "720p"),
+ (2048, 858, "720p"),
+ (4032, 3024, "480p"),
+ (720, 480, "720p"),
+ (33, 33, "720p"), # tiny input
+ ],
+ )
+ @pytest.mark.parametrize("rounding", ["nearest", "floor", "ceiling"])
+ def test_output_dims_are_multiples_of_32(self, w: int, h: int, target: str, rounding: str) -> None:
+ ow, oh = _resolve(w, h, target=target, rounding=rounding)
+ assert ow % 32 == 0
+ assert oh % 32 == 0
+
+ @pytest.mark.parametrize(
+ "w, h, target",
+ [
+ (1920, 1080, "720p"),
+ (1080, 1920, "720p"),
+ (832, 480, "720p"),
+ ],
+ )
+ def test_output_aspect_ratio_within_2_percent(self, w: int, h: int, target: str) -> None:
+ ow, oh = _resolve(w, h, target=target)
+ input_aspect = w / h
+ output_aspect = ow / oh
+ # 32-grid snap can shift aspect by at most half a 32-step on the long axis,
+ # which is ~2.2% at 704 short — looser than the I2V node's 16-grid tolerance.
+ assert abs(output_aspect - input_aspect) / input_aspect < 0.023
+
+ def test_smallest_valid_input_still_snaps_to_32_grid(self) -> None:
+ # 32×32 is the minimum input the guard accepts. The downstream clamp ensures
+ # the output is at least 32×32 even when floor rounding would zero it.
+ ow, oh = _resolve(32, 32, target="480p", rounding="floor")
+ assert ow >= 32
+ assert oh >= 32
+
+
+class TestResolutionPresetTable:
+ """The dropdown values must map to the documented short-side pixel counts."""
+
+ def test_presets_cover_canonical_video_sizes(self) -> None:
+ assert WAN_TARGET_RESOLUTION_PX == {"480p": 480, "720p": 720, "1080p": 1080}
+
+ def test_pixel_multiple_is_32(self) -> None:
+ assert WAN_TI2V_PIXEL_MULTIPLE == 32
+
+
+class TestInputValidation:
+ """Reject obviously bad inputs at the schema layer."""
+
+ def test_zero_width_rejected(self) -> None:
+ from pydantic import ValidationError
+
+ with pytest.raises(ValidationError):
+ WanTI2VIdealDimensionsInvocation(width=0, height=720)
+
+ def test_negative_height_rejected(self) -> None:
+ from pydantic import ValidationError
+
+ with pytest.raises(ValidationError):
+ WanTI2VIdealDimensionsInvocation(width=720, height=-1)
+
+ def test_input_smaller_than_pixel_grid_rejected(self) -> None:
+ # If the longer side is below the 32-px TI2V grid, the floor-rounding output
+ # would silently disconnect from the requested aspect ratio. Fail fast instead.
+ with pytest.raises(ValueError, match="smaller than the Wan pixel grid"):
+ _resolve(16, 16, target="480p", rounding="floor")
+ with pytest.raises(ValueError, match="smaller than the Wan pixel grid"):
+ _resolve(31, 31, target="720p", rounding="nearest")
+
+ def test_unknown_resolution_rejected(self) -> None:
+ from pydantic import ValidationError
+
+ with pytest.raises(ValidationError):
+ WanTI2VIdealDimensionsInvocation(
+ width=1920,
+ height=1080,
+ target_resolution="2160p", # type: ignore[arg-type]
+ )
diff --git a/tests/app/invocations/test_wan_working_memory.py b/tests/app/invocations/test_wan_working_memory.py
new file mode 100644
index 00000000000..5a84df10d6f
--- /dev/null
+++ b/tests/app/invocations/test_wan_working_memory.py
@@ -0,0 +1,319 @@
+"""Wan VAE invocations: working-memory estimation and cpu_only device handling."""
+
+from unittest.mock import MagicMock, patch
+
+import PIL.Image
+import torch
+from diffusers.models.autoencoders import AutoencoderKLWan
+
+from invokeai.app.invocations.wan_image_to_latents import WanImageToLatentsInvocation
+from invokeai.app.invocations.wan_latents_to_image import WanLatentsToImageInvocation
+from invokeai.app.invocations.wan_latents_to_video import WanLatentsToVideoInvocation
+from invokeai.app.invocations.wan_ref_image_encoder import WanRefImageEncoderInvocation
+from invokeai.backend.util.devices import TorchDevice
+from invokeai.backend.util.vae_working_memory import estimate_vae_working_memory_wan
+
+
+def _mock_wan_vae(
+ z_dim: int = 16, spatial_scale: int = 8, temporal_scale: int = 4, dtype: torch.dtype = torch.float16
+) -> MagicMock:
+ vae = MagicMock(spec=AutoencoderKLWan)
+ param = torch.zeros(1, dtype=dtype)
+ # parameters() is consumed several times (estimator, dtype probe, effective device).
+ vae.parameters.side_effect = lambda: iter([param])
+ vae.config = MagicMock()
+ vae.config.z_dim = z_dim
+ vae.config.scale_factor_spatial = spatial_scale
+ vae.config.scale_factor_temporal = temporal_scale
+ vae.config.latents_mean = [0.0] * z_dim
+ vae.config.latents_std = [1.0] * z_dim
+ vae.tile_sample_min_height = 256
+ return vae
+
+
+def _mock_vae_info(vae: MagicMock, cpu_only: bool | None = None) -> MagicMock:
+ vae_info = MagicMock()
+ vae_info.model = vae
+ vae_info.config = MagicMock()
+ vae_info.config.cpu_only = cpu_only
+ cm = MagicMock()
+ cm.__enter__ = MagicMock(return_value=(None, vae))
+ cm.__exit__ = MagicMock(return_value=None)
+ vae_info.model_on_device = MagicMock(return_value=cm)
+ return vae_info
+
+
+class TestEstimateVaeWorkingMemoryWan:
+ """Lock in the generalized Wan estimator: per-frame conv working set plus the full
+ RGB clip that stays resident on the execution device."""
+
+ def test_decode_single_frame(self):
+ vae = _mock_wan_vae()
+ est = estimate_vae_working_memory_wan(
+ operation="decode", vae=vae, pixel_height=256, pixel_width=256, pixel_frames=1
+ )
+ area_bytes = 256 * 256 * 2
+ # Decode counts the clip twice: torch.cat frame accumulation transiently holds
+ # the clip and its copy at peak.
+ assert est == area_bytes * 2900 + 2 * 3 * 1 * area_bytes
+
+ def test_encode_uses_half_the_decode_constant(self):
+ vae = _mock_wan_vae()
+ est = estimate_vae_working_memory_wan(
+ operation="encode", vae=vae, pixel_height=256, pixel_width=256, pixel_frames=1
+ )
+ area_bytes = 256 * 256 * 2
+ # Encode consumes the input clip without duplicating it — one copy only.
+ assert est == area_bytes * 1450 + 3 * 1 * area_bytes
+
+ def test_additional_frames_add_only_clip_bytes(self):
+ """The Wan VAE is causal (frame-at-a-time with cached features), so extra frames
+ grow the resident clip (2 copies at decode peak), not the conv working set."""
+ vae = _mock_wan_vae()
+ one = estimate_vae_working_memory_wan(
+ operation="decode", vae=vae, pixel_height=128, pixel_width=128, pixel_frames=1
+ )
+ many = estimate_vae_working_memory_wan(
+ operation="decode", vae=vae, pixel_height=128, pixel_width=128, pixel_frames=81
+ )
+ assert many - one == 2 * 3 * 80 * 128 * 128 * 2
+
+ def test_tile_size_bounds_the_per_frame_term(self):
+ vae = _mock_wan_vae()
+ tiled = estimate_vae_working_memory_wan(
+ operation="decode", vae=vae, pixel_height=1920, pixel_width=1080, pixel_frames=17, tile_size=256
+ )
+ expected = int(256 * 256 * 2 * 2900 * 1.25 + 2 * 3 * 17 * 1920 * 1080 * 2)
+ assert tiled == expected
+ full = estimate_vae_working_memory_wan(
+ operation="decode", vae=vae, pixel_height=1920, pixel_width=1080, pixel_frames=17
+ )
+ assert tiled < full
+
+
+class TestWanInvocationsRequestWorkingMemory:
+ """Every Wan VAE path must reserve its estimated working memory with the model cache."""
+
+ def test_latents_to_image_requests_decode_memory(self):
+ vae = _mock_wan_vae()
+ vae_info = _mock_vae_info(vae)
+ mock_context = MagicMock()
+ mock_context.models.load.return_value = vae_info
+ mock_context.tensors.load.return_value = torch.zeros(1, 16, 64, 64)
+
+ with patch("invokeai.app.invocations.wan_latents_to_image.estimate_vae_working_memory_wan") as mock_estimate:
+ mock_estimate.return_value = 1234
+ invocation = WanLatentsToImageInvocation.model_construct(
+ latents=MagicMock(latents_name="l"), vae=MagicMock(vae=MagicMock())
+ )
+ try:
+ invocation.invoke(mock_context)
+ except Exception:
+ pass # Downstream image math fails under mocking; the reservation is what matters.
+
+ assert mock_estimate.call_args.kwargs["operation"] == "decode"
+ # Latent 64x64 at the standard 8x spatial scale -> 512x512 pixels.
+ assert mock_estimate.call_args.kwargs["pixel_height"] == 512
+ assert mock_estimate.call_args.kwargs["pixel_width"] == 512
+ vae_info.model_on_device.assert_called_once_with(working_mem_bytes=1234)
+
+ def test_latents_to_image_uses_config_spatial_scale_for_ti2v(self):
+ vae = _mock_wan_vae(z_dim=48, spatial_scale=16)
+ vae_info = _mock_vae_info(vae)
+ mock_context = MagicMock()
+ mock_context.models.load.return_value = vae_info
+ mock_context.tensors.load.return_value = torch.zeros(1, 48, 32, 32)
+
+ with patch("invokeai.app.invocations.wan_latents_to_image.estimate_vae_working_memory_wan") as mock_estimate:
+ mock_estimate.return_value = 1
+ invocation = WanLatentsToImageInvocation.model_construct(
+ latents=MagicMock(latents_name="l"), vae=MagicMock(vae=MagicMock())
+ )
+ try:
+ invocation.invoke(mock_context)
+ except Exception:
+ pass
+
+ # TI2V-5B VAE compresses 16x spatially -> 32 latent px = 512 pixel px.
+ assert mock_estimate.call_args.kwargs["pixel_height"] == 512
+
+ def test_image_to_latents_requests_encode_memory(self):
+ vae = _mock_wan_vae()
+ vae_info = _mock_vae_info(vae)
+
+ with patch("invokeai.app.invocations.wan_image_to_latents.estimate_vae_working_memory_wan") as mock_estimate:
+ mock_estimate.return_value = 4321
+ try:
+ WanImageToLatentsInvocation.vae_encode(vae_info, torch.zeros(1, 3, 512, 512))
+ except Exception:
+ pass
+
+ assert mock_estimate.call_args.kwargs["operation"] == "encode"
+ assert mock_estimate.call_args.kwargs["pixel_frames"] == 1
+ vae_info.model_on_device.assert_called_once_with(working_mem_bytes=4321)
+
+ def test_ref_image_encoder_requests_encode_memory(self):
+ vae = _mock_wan_vae()
+ vae_info = _mock_vae_info(vae)
+ mock_context = MagicMock()
+ mock_context.models.load.return_value = vae_info
+ mock_context.images.get_pil.return_value = PIL.Image.new("RGB", (32, 32))
+ mock_context.tensors.save.return_value = "t"
+
+ with (
+ patch("invokeai.app.invocations.wan_ref_image_encoder.estimate_vae_working_memory_wan") as mock_estimate,
+ patch(
+ "invokeai.app.invocations.wan_ref_image_encoder.encode_reference_image_to_condition",
+ return_value=torch.zeros(1, 20, 1, 4, 4),
+ ),
+ ):
+ mock_estimate.return_value = 999
+ invocation = WanRefImageEncoderInvocation.model_construct(
+ image=MagicMock(image_name="i"),
+ vae=MagicMock(vae=MagicMock()),
+ width=32,
+ height=32,
+ num_frames=1,
+ end_image=None,
+ )
+ invocation.invoke(mock_context)
+
+ assert mock_estimate.call_args.kwargs["operation"] == "encode"
+ assert mock_estimate.call_args.kwargs["pixel_frames"] == 1
+ vae_info.model_on_device.assert_called_once_with(working_mem_bytes=999)
+
+ def _video_context(self, vae_info: MagicMock, t_lat: int = 5) -> MagicMock:
+ mock_context = MagicMock()
+ mock_context.models.load.return_value = vae_info
+ mock_context.tensors.load.return_value = torch.zeros(1, 16, t_lat, 32, 32)
+ mock_context.util.is_canceled.return_value = False
+ return mock_context
+
+ def test_latents_to_video_requests_decode_memory_for_all_frames(self):
+ vae = _mock_wan_vae()
+ vae_info = _mock_vae_info(vae)
+ mock_context = self._video_context(vae_info)
+
+ with patch("invokeai.app.invocations.wan_latents_to_video.estimate_vae_working_memory_wan") as mock_estimate:
+ mock_estimate.return_value = 5678
+ invocation = WanLatentsToVideoInvocation.model_construct(
+ latents=MagicMock(latents_name="l"), vae=MagicMock(vae=MagicMock()), fps=16
+ )
+ try:
+ invocation.invoke(mock_context)
+ except Exception:
+ pass
+
+ assert mock_estimate.call_args.kwargs["operation"] == "decode"
+ # 5 latent frames -> (5-1)*4+1 = 17 pixel frames at 32*8 = 256px.
+ assert mock_estimate.call_args.kwargs["pixel_frames"] == 17
+ assert mock_estimate.call_args.kwargs["pixel_height"] == 256
+ vae_info.model_on_device.assert_called_once_with(working_mem_bytes=5678)
+
+ def test_latents_to_video_falls_back_to_tiling_when_estimate_exceeds_vram(self):
+ vae = _mock_wan_vae()
+ vae_info = _mock_vae_info(vae)
+ mock_context = self._video_context(vae_info)
+
+ with (
+ patch(
+ "invokeai.app.invocations.wan_latents_to_video.estimate_vae_working_memory_wan",
+ side_effect=[100 * 2**30, 4 * 2**30],
+ ) as mock_estimate,
+ patch.object(TorchDevice, "choose_torch_device", return_value=torch.device("cuda")),
+ patch.object(TorchDevice, "empty_cache"),
+ patch("torch.cuda.get_device_properties", return_value=MagicMock(total_memory=8 * 2**30)),
+ ):
+ invocation = WanLatentsToVideoInvocation.model_construct(
+ latents=MagicMock(latents_name="l"), vae=MagicMock(vae=MagicMock()), fps=16
+ )
+ try:
+ invocation.invoke(mock_context)
+ except Exception:
+ pass
+
+ assert mock_estimate.call_count == 2
+ assert mock_estimate.call_args_list[1].kwargs["tile_size"] == 256
+ vae.enable_tiling.assert_called_once()
+ vae.disable_tiling.assert_called_once()
+ vae_info.model_on_device.assert_called_once_with(working_mem_bytes=4 * 2**30)
+
+ def test_latents_to_video_skips_tiling_for_cpu_only_vae(self):
+ """A cpu_only VAE runs in system RAM; VRAM-based tiling must not kick in."""
+ vae = _mock_wan_vae()
+ vae_info = _mock_vae_info(vae, cpu_only=True)
+ mock_context = self._video_context(vae_info)
+
+ with (
+ patch(
+ "invokeai.app.invocations.wan_latents_to_video.estimate_vae_working_memory_wan",
+ return_value=100 * 2**30,
+ ) as mock_estimate,
+ patch.object(TorchDevice, "choose_torch_device", return_value=torch.device("cuda")),
+ patch.object(TorchDevice, "empty_cache"),
+ ):
+ invocation = WanLatentsToVideoInvocation.model_construct(
+ latents=MagicMock(latents_name="l"), vae=MagicMock(vae=MagicMock()), fps=16
+ )
+ try:
+ invocation.invoke(mock_context)
+ except Exception:
+ pass
+
+ assert mock_estimate.call_count == 1
+ vae.enable_tiling.assert_not_called()
+
+
+class TestWanCpuOnlyDeviceHandling:
+ """With a cpu_only VAE, inputs must go to the VAE's device — not the globally
+ selected accelerator (which would crash every Wan VAE invocation on GPU hosts)."""
+
+ def test_latents_to_image_moves_latents_to_vae_device(self):
+ vae = _mock_wan_vae(dtype=torch.float32)
+ seen_devices: list[torch.device] = []
+
+ def _decode(latents, return_dict):
+ seen_devices.append(latents.device)
+ return (torch.zeros(1, 3, 1, 8, 8),)
+
+ vae.decode.side_effect = _decode
+ vae_info = _mock_vae_info(vae, cpu_only=True)
+ mock_context = MagicMock()
+ mock_context.models.load.return_value = vae_info
+ mock_context.tensors.load.return_value = torch.zeros(1, 16, 1, 1)
+
+ # Poison the global device: if the invocation still routed through
+ # choose_torch_device, the .to() below would produce a meta tensor and fail
+ # the device assertion.
+ with (
+ patch.object(TorchDevice, "choose_torch_device", return_value=torch.device("meta")),
+ patch.object(TorchDevice, "empty_cache"),
+ ):
+ invocation = WanLatentsToImageInvocation.model_construct(
+ latents=MagicMock(latents_name="l"), vae=MagicMock(vae=MagicMock())
+ )
+ try:
+ invocation.invoke(mock_context)
+ except Exception:
+ pass
+
+ assert seen_devices == [torch.device("cpu")]
+
+ def test_image_to_latents_moves_image_to_vae_device(self):
+ vae = _mock_wan_vae(dtype=torch.float32)
+ seen_devices: list[torch.device] = []
+
+ def _encode(pixel, return_dict):
+ seen_devices.append(pixel.device)
+ raise RuntimeError("stop here")
+
+ vae.encode.side_effect = _encode
+ vae_info = _mock_vae_info(vae, cpu_only=True)
+
+ with patch.object(TorchDevice, "choose_torch_device", return_value=torch.device("meta")):
+ try:
+ WanImageToLatentsInvocation.vae_encode(vae_info, torch.zeros(1, 3, 64, 64))
+ except Exception:
+ pass
+
+ assert seen_devices == [torch.device("cpu")]
diff --git a/tests/app/routers/conftest.py b/tests/app/routers/conftest.py
index fe3e2bba14c..e5ca28e16f1 100644
--- a/tests/app/routers/conftest.py
+++ b/tests/app/routers/conftest.py
@@ -47,6 +47,7 @@ def __init__(self, invoker: Invoker) -> None:
# so the in-test invoker is reachable. Add new routers / shared helpers to this
# list when they start touching ApiDependencies.
_PATCHED_API_DEPENDENCIES_MODULES = (
+ "invokeai.app.api_app",
"invokeai.app.api.auth_dependencies",
"invokeai.app.api.routers.auth",
"invokeai.app.api.routers.download_queue",
diff --git a/tests/app/routers/test_auth.py b/tests/app/routers/test_auth.py
index 5362bd775ff..328c42b7c31 100644
--- a/tests/app/routers/test_auth.py
+++ b/tests/app/routers/test_auth.py
@@ -97,6 +97,9 @@ def test_login_success(monkeypatch: Any, mock_invoker: Invoker, client: TestClie
assert "expires_in" in json_response
assert json_response["user"]["email"] == "test@example.com"
assert json_response["user"]["is_admin"] is False
+ assert response.cookies.get("invokeai_media_token") == json_response["token"]
+ assert "HttpOnly" in response.headers["set-cookie"]
+ assert "Path=/api/v1" in response.headers["set-cookie"]
def test_login_with_remember_me(monkeypatch: Any, mock_invoker: Invoker, client: TestClient) -> None:
@@ -354,3 +357,163 @@ def test_admin_user_token_has_admin_flag(monkeypatch: Any, mock_invoker: Invoker
assert response.status_code == 200
json_response = response.json()
assert json_response["user"]["is_admin"] is True
+
+
+class TestRefreshMediaCookie:
+ """POST /api/v1/auth/media-cookie — self-healing for sessions that hold a valid
+ JWT but no media cookie (session predates the cookie, or the cookie was cleared).
+ Without it, every API call works but requests 401 and the player renders
+ black (PR #9163 functional testing)."""
+
+ def _login(self, monkeypatch: Any, mock_invoker: Invoker, client: TestClient, email: str) -> str:
+ monkeypatch.setattr("invokeai.app.api.routers.auth.ApiDependencies", MockApiDependencies(mock_invoker))
+ # The media-cookie route authenticates via CurrentUserOrDefault, which reads
+ # ApiDependencies from the auth_dependencies module.
+ monkeypatch.setattr("invokeai.app.api.auth_dependencies.ApiDependencies", MockApiDependencies(mock_invoker))
+ setup_test_user(mock_invoker, email, "TestPass123")
+ response = client.post(
+ "/api/v1/auth/login",
+ json={"email": email, "password": "TestPass123", "remember_me": False},
+ )
+ assert response.status_code == 200
+ return response.json()["token"]
+
+ def test_reissues_cookie_from_bearer_token(
+ self, monkeypatch: Any, mock_invoker: Invoker, client: TestClient
+ ) -> None:
+ token = self._login(monkeypatch, mock_invoker, client, "mediacookie@example.com")
+
+ # Simulate the stale-session state: valid JWT in hand, no cookie in the jar.
+ client.cookies.clear()
+
+ response = client.post("/api/v1/auth/media-cookie", headers={"Authorization": f"Bearer {token}"})
+
+ assert response.status_code == 200
+ assert response.json()["success"] is True
+ assert response.cookies.get("invokeai_media_token") == token
+ set_cookie = response.headers["set-cookie"]
+ assert "HttpOnly" in set_cookie
+ assert "Path=/api/v1" in set_cookie
+ # Cookie must not outlive the token: max-age is clamped to the remaining
+ # validity (1-day login here, minus the instants since).
+ max_age = int(set_cookie.split("Max-Age=")[1].split(";")[0])
+ assert 0 < max_age <= 24 * 3600
+
+ def test_requires_bearer_token(self, monkeypatch: Any, mock_invoker: Invoker, client: TestClient) -> None:
+ monkeypatch.setattr("invokeai.app.api.routers.auth.ApiDependencies", MockApiDependencies(mock_invoker))
+ monkeypatch.setattr("invokeai.app.api.auth_dependencies.ApiDependencies", MockApiDependencies(mock_invoker))
+ client.cookies.clear()
+ response = client.post("/api/v1/auth/media-cookie")
+ assert response.status_code == 401
+
+ def test_rejects_invalid_token(self, monkeypatch: Any, mock_invoker: Invoker, client: TestClient) -> None:
+ monkeypatch.setattr("invokeai.app.api.routers.auth.ApiDependencies", MockApiDependencies(mock_invoker))
+ monkeypatch.setattr("invokeai.app.api.auth_dependencies.ApiDependencies", MockApiDependencies(mock_invoker))
+ response = client.post("/api/v1/auth/media-cookie", headers={"Authorization": "Bearer not-a-jwt"})
+ assert response.status_code == 401
+ assert response.cookies.get("invokeai_media_token") is None
+
+ def test_rejects_deactivated_user(self, monkeypatch: Any, mock_invoker: Invoker, client: TestClient) -> None:
+ """A still-valid token for a since-deactivated user must not mint a cookie —
+ the cookie grants media access, so it needs the same live-user check as
+ get_current_user."""
+ from invokeai.app.services.users.users_common import UserUpdateRequest
+
+ token = self._login(monkeypatch, mock_invoker, client, "deactivated@example.com")
+ user = mock_invoker.services.users.get_by_email("deactivated@example.com")
+ mock_invoker.services.users.update(user.user_id, UserUpdateRequest(is_active=False))
+ client.cookies.clear()
+
+ response = client.post("/api/v1/auth/media-cookie", headers={"Authorization": f"Bearer {token}"})
+
+ assert response.status_code == 401
+ assert response.cookies.get("invokeai_media_token") is None
+
+ def test_single_user_mode_is_a_successful_noop(
+ self, monkeypatch: Any, mock_invoker: Invoker, client: TestClient
+ ) -> None:
+ """In single-user mode media routes don't require auth; the frontend may
+ still call this on load, so it must succeed without setting a cookie."""
+ monkeypatch.setattr("invokeai.app.api.routers.auth.ApiDependencies", MockApiDependencies(mock_invoker))
+ monkeypatch.setattr("invokeai.app.api.auth_dependencies.ApiDependencies", MockApiDependencies(mock_invoker))
+ mock_invoker.services.configuration.multiuser = False
+ client.cookies.clear()
+
+ response = client.post("/api/v1/auth/media-cookie")
+
+ assert response.status_code == 200
+ assert response.json()["success"] is True
+ assert response.cookies.get("invokeai_media_token") is None
+
+
+class TestMediaCookieBehindSubPathProxy:
+ """The media cookie must be scoped to the *public* path when the app is served
+ under a reverse-proxy sub-path — browsers match cookie paths against the URL
+ they requested, so a cookie scoped to `/api/v1` is never sent to
+ `/invoke/api/v1/videos/...`."""
+
+ BASE_PATH = "/invoke"
+
+ def _proxied_client(self, preserve: bool) -> TestClient:
+ from invokeai.app.api_app import SubPathASGIMiddleware
+
+ wrapped = SubPathASGIMiddleware(app, self.BASE_PATH)
+ root = f"http://testserver{self.BASE_PATH}" if preserve else "http://testserver"
+ return TestClient(wrapped, base_url=root)
+
+ def _login(self, client: TestClient, email: str) -> tuple[str, Any]:
+ response = client.post(
+ "/api/v1/auth/login",
+ json={"email": email, "password": "TestPass123", "remember_me": False},
+ )
+ assert response.status_code == 200
+ return response.json()["token"], response
+
+ @pytest.mark.parametrize("preserve", [True, False], ids=["preserve", "strip"])
+ def test_login_scopes_cookie_to_public_prefix(
+ self, preserve: bool, monkeypatch: Any, mock_invoker: Invoker
+ ) -> None:
+ monkeypatch.setattr("invokeai.app.api.routers.auth.ApiDependencies", MockApiDependencies(mock_invoker))
+ setup_test_user(mock_invoker, f"proxy-login-{preserve}@example.com", "TestPass123")
+ client = self._proxied_client(preserve)
+
+ _token, response = self._login(client, f"proxy-login-{preserve}@example.com")
+
+ assert f"Path={self.BASE_PATH}/api/v1" in response.headers["set-cookie"]
+
+ @pytest.mark.parametrize("preserve", [True, False], ids=["preserve", "strip"])
+ def test_media_cookie_endpoint_scopes_cookie_to_public_prefix(
+ self, preserve: bool, monkeypatch: Any, mock_invoker: Invoker
+ ) -> None:
+ monkeypatch.setattr("invokeai.app.api.routers.auth.ApiDependencies", MockApiDependencies(mock_invoker))
+ monkeypatch.setattr("invokeai.app.api.auth_dependencies.ApiDependencies", MockApiDependencies(mock_invoker))
+ setup_test_user(mock_invoker, f"proxy-media-{preserve}@example.com", "TestPass123")
+ client = self._proxied_client(preserve)
+ token, _ = self._login(client, f"proxy-media-{preserve}@example.com")
+ client.cookies.clear()
+
+ response = client.post("/api/v1/auth/media-cookie", headers={"Authorization": f"Bearer {token}"})
+
+ assert response.status_code == 200
+ assert f"Path={self.BASE_PATH}/api/v1" in response.headers["set-cookie"]
+
+ @pytest.mark.parametrize("preserve", [True, False], ids=["preserve", "strip"])
+ def test_logout_deletes_cookie_at_public_prefix_without_reminting(
+ self, preserve: bool, monkeypatch: Any, mock_invoker: Invoker
+ ) -> None:
+ """Logout must delete the cookie at the prefixed path, and the sliding-window
+ middleware must not mint a replacement token/cookie for the proxied logout URL."""
+ monkeypatch.setattr("invokeai.app.api.routers.auth.ApiDependencies", MockApiDependencies(mock_invoker))
+ monkeypatch.setattr("invokeai.app.api.auth_dependencies.ApiDependencies", MockApiDependencies(mock_invoker))
+ setup_test_user(mock_invoker, f"proxy-logout-{preserve}@example.com", "TestPass123")
+ client = self._proxied_client(preserve)
+ token, _ = self._login(client, f"proxy-logout-{preserve}@example.com")
+
+ response = client.post("/api/v1/auth/logout", headers={"Authorization": f"Bearer {token}"})
+
+ assert response.status_code == 200
+ assert "X-Refreshed-Token" not in response.headers
+ set_cookies = response.headers.get_list("set-cookie")
+ assert len(set_cookies) == 1
+ assert 'invokeai_media_token=""' in set_cookies[0]
+ assert f"Path={self.BASE_PATH}/api/v1" in set_cookies[0]
diff --git a/tests/app/routers/test_boards_multiuser.py b/tests/app/routers/test_boards_multiuser.py
index ab64ac8a9b4..7876427ecdd 100644
--- a/tests/app/routers/test_boards_multiuser.py
+++ b/tests/app/routers/test_boards_multiuser.py
@@ -1,7 +1,8 @@
"""Tests for multiuser boards functionality."""
+from contextlib import nullcontext
from typing import Any
-from unittest.mock import MagicMock
+from unittest.mock import MagicMock, patch
import pytest
from fastapi import status
@@ -75,6 +76,27 @@ def enable_multiuser_for_tests(monkeypatch: Any, mock_invoker: Invoker):
mock_board_images.get_all_board_image_names_for_board.return_value = []
mock_invoker.services.board_images = mock_board_images
+ # delete_board cascade-deletes videos on the board too — stub the video services
+ # so the route doesn't hit AttributeError on the None placeholders in mock_services.
+ mock_board_video_records = MagicMock()
+ mock_board_video_records.get_all_board_video_names_for_board.return_value = []
+ mock_board_video_records.get_video_count_for_board.return_value = 0
+ mock_invoker.services.board_video_records = mock_board_video_records
+ # The board service also consults video_records for cover-image selection (most recent video).
+ mock_video_records = MagicMock()
+ mock_video_records.get_most_recent_video_for_board.return_value = None
+ mock_invoker.services.video_records = mock_video_records
+ mock_invoker.services.videos = MagicMock()
+ # delete_videos_on_board now returns the authoritative ``deleted_videos`` list (only the
+ # videos whose file deletion actually succeeded). Default to an empty list so pydantic
+ # validation on ``DeleteBoardResult`` doesn't reject the MagicMock auto-return.
+ mock_invoker.services.videos.delete_videos_on_board.return_value = ([], [])
+ # The images service is a real ImageService instance in mock_services; the delete-board
+ # cascade calls ``delete_images_on_board`` on it, which fails without an initialized
+ # invoker. Stub it so the multiuser router tests can assert the cascade args.
+ mock_invoker.services.images = MagicMock()
+ mock_invoker.services.images.delete_images_on_board.return_value = ([], [])
+
mock_deps = MockApiDependencies(mock_invoker)
monkeypatch.setattr("invokeai.app.api.routers.auth.ApiDependencies", mock_deps)
monkeypatch.setattr("invokeai.app.api.auth_dependencies.ApiDependencies", mock_deps)
@@ -675,3 +697,195 @@ def test_admin_can_change_any_board_visibility(client: TestClient, admin_token:
)
assert response.status_code == status.HTTP_201_CREATED
assert response.json()["board_visibility"] == "public"
+
+
+# ---------------------------------------------------------------------------
+# Video cascade on board deletion (PR #9163 review fix)
+# ---------------------------------------------------------------------------
+
+
+def test_delete_board_with_include_images_cascades_videos(client: TestClient, mock_invoker: Invoker, user1_token: str):
+ """include_images=true must also call delete_videos_on_board (not image-only)."""
+ create = client.post(
+ "/api/v1/boards/?board_name=Cascade+Test+Board",
+ headers={"Authorization": f"Bearer {user1_token}"},
+ )
+ assert create.status_code == status.HTTP_201_CREATED
+ board_id = create.json()["board_id"]
+
+ # The cascade returns the names of videos it actually deleted; the router must surface
+ # *that* list (not the pre-delete enumeration) so the response can't claim a video was
+ # destroyed when its DB record was preserved due to a file-delete failure.
+ mock_invoker.services.videos.delete_videos_on_board.return_value = (["video_a.mp4", "video_b.mp4"], [])
+
+ response = client.delete(
+ f"/api/v1/boards/{board_id}?include_images=true",
+ headers={"Authorization": f"Bearer {user1_token}"},
+ )
+ assert response.status_code == status.HTTP_200_OK
+ body = response.json()
+ assert body["board_id"] == board_id
+ assert set(body["deleted_videos"]) == {"video_a.mp4", "video_b.mp4"}
+ mock_invoker.services.videos.delete_videos_on_board.assert_called_once()
+ call = mock_invoker.services.videos.delete_videos_on_board.call_args
+ assert call.kwargs["board_id"] == board_id
+
+
+def test_delete_board_with_partial_video_file_delete_failure_reports_only_actual_deletes(
+ client: TestClient, mock_invoker: Invoker, user1_token: str
+):
+ """JPPhoto PR #9163 May-22 follow-up: ``delete_board`` previously reported every video
+ that *would have been* deleted, but ``delete_videos_on_board`` now intentionally preserves
+ records whose backing files failed to delete (so the file isn't orphaned on disk). The
+ response must therefore report only the videos that were actually destroyed; preserved
+ records cascade to "uncategorized" via the board_videos FK and the frontend must learn
+ they still exist so the cache stays consistent.
+ """
+ create = client.post(
+ "/api/v1/boards/?board_name=Partial+Failure+Board",
+ headers={"Authorization": f"Bearer {user1_token}"},
+ )
+ assert create.status_code == status.HTTP_201_CREATED
+ board_id = create.json()["board_id"]
+
+ # The service deleted "good.mp4" successfully but preserved "stuck.mp4" because its
+ # file delete failed. The router must NOT claim "stuck.mp4" was deleted, and must
+ # report the failure from the service's own accounting (not a racy listing diff).
+ mock_invoker.services.images.delete_images_on_board.return_value = (["good.png"], ["stuck.png"])
+ mock_invoker.services.videos.delete_videos_on_board.return_value = (["good.mp4"], ["stuck.mp4"])
+
+ response = client.delete(
+ f"/api/v1/boards/{board_id}?include_images=true",
+ headers={"Authorization": f"Bearer {user1_token}"},
+ )
+ assert response.status_code == status.HTTP_200_OK
+ body = response.json()
+ assert body["deleted_images"] == ["good.png"]
+ assert body["failed_images"] == ["stuck.png"]
+ assert body["deleted_videos"] == ["good.mp4"]
+ assert body["failed_videos"] == ["stuck.mp4"]
+
+
+@pytest.mark.parametrize("failure_phase", ["videos", "board"])
+def test_delete_board_reports_partial_cascade_completion(
+ client: TestClient, mock_invoker: Invoker, user1_token: str, failure_phase: str
+):
+ create = client.post(
+ "/api/v1/boards/?board_name=Partial+Cascade+Failure",
+ headers={"Authorization": f"Bearer {user1_token}"},
+ )
+ assert create.status_code == status.HTTP_201_CREATED
+ board_id = create.json()["board_id"]
+ mock_invoker.services.images.delete_images_on_board.return_value = (["deleted.png"], [])
+ mock_invoker.services.videos.delete_videos_on_board.return_value = (["deleted.mp4"], [])
+ if failure_phase == "videos":
+ mock_invoker.services.videos.delete_videos_on_board.side_effect = RuntimeError("video delete failed")
+
+ board_delete_patch = (
+ patch.object(mock_invoker.services.boards, "delete", side_effect=RuntimeError("board delete failed"))
+ if failure_phase == "board"
+ else nullcontext()
+ )
+ with board_delete_patch:
+ response = client.delete(
+ f"/api/v1/boards/{board_id}?include_images=true",
+ headers={"Authorization": f"Bearer {user1_token}"},
+ )
+
+ assert response.status_code == status.HTTP_500_INTERNAL_SERVER_ERROR
+ detail = response.json()["detail"]
+ assert detail["deleted_images"] == ["deleted.png"]
+ assert detail["deleted_videos"] == ([] if failure_phase == "videos" else ["deleted.mp4"])
+ assert detail["board_deleted"] is False
+
+
+def test_delete_board_with_include_images_filters_cascade_by_user(
+ client: TestClient, mock_invoker: Invoker, user1_token: str
+):
+ """A non-admin board owner deleting a public/shared board with include_images=True must
+ only destroy videos and images they themselves uploaded — other users' contributions are
+ preserved (they fall through to ``uncategorized`` via the FK cascade on board_videos /
+ board_images).
+
+ This guards against a privilege-escalation gap: previously the cascade iterated *all*
+ board_videos rows for the board_id regardless of uploader, letting the board owner
+ delete other users' files just by deleting the board.
+ """
+ create = client.post(
+ "/api/v1/boards/?board_name=Public+Cascade+Board",
+ headers={"Authorization": f"Bearer {user1_token}"},
+ )
+ assert create.status_code == status.HTTP_201_CREATED
+ board_id = create.json()["board_id"]
+
+ response = client.delete(
+ f"/api/v1/boards/{board_id}?include_images=true",
+ headers={"Authorization": f"Bearer {user1_token}"},
+ )
+ assert response.status_code == status.HTTP_200_OK
+
+ # The router must pass the current user's id through to the cascade so the SQL filter
+ # narrows the lookup + delete to that user's rows only. Admins skip the filter (pass
+ # ``user_id=None``); this test exercises the non-admin path.
+ delete_images_call = mock_invoker.services.images.delete_images_on_board.call_args
+ delete_videos_call = mock_invoker.services.videos.delete_videos_on_board.call_args
+ assert delete_images_call.kwargs.get("user_id") is not None
+ assert delete_videos_call.kwargs.get("user_id") is not None
+ # Both cascade calls must share the same user_id (the requester).
+ requester_id = delete_images_call.kwargs["user_id"]
+ assert delete_images_call.kwargs["user_id"] == requester_id
+ assert delete_videos_call.kwargs["user_id"] == requester_id
+
+
+def test_delete_board_with_include_images_admin_skips_user_filter(
+ client: TestClient, mock_invoker: Invoker, admin_token: str
+):
+ """Admins keep the legacy behavior: cascade deletes everything on the board regardless of
+ uploader. The filter is bypassed by passing ``user_id=None`` so the SQL ``WHERE`` clause
+ has no per-user restriction.
+ """
+ create = client.post(
+ "/api/v1/boards/?board_name=Admin+Cascade+Board",
+ headers={"Authorization": f"Bearer {admin_token}"},
+ )
+ assert create.status_code == status.HTTP_201_CREATED
+ board_id = create.json()["board_id"]
+
+ response = client.delete(
+ f"/api/v1/boards/{board_id}?include_images=true",
+ headers={"Authorization": f"Bearer {admin_token}"},
+ )
+ assert response.status_code == status.HTTP_200_OK
+
+ delete_images_call = mock_invoker.services.images.delete_images_on_board.call_args
+ delete_videos_call = mock_invoker.services.videos.delete_videos_on_board.call_args
+ assert delete_images_call.kwargs.get("user_id") is None
+ assert delete_videos_call.kwargs.get("user_id") is None
+
+
+def test_delete_board_without_include_images_lists_uncategorized_videos(
+ client: TestClient, mock_invoker: Invoker, user1_token: str
+):
+ """include_images=false: videos cascade out of board_videos and become uncategorized.
+
+ The response now reports those names in deleted_board_videos so the frontend can
+ invalidate the right caches.
+ """
+ create = client.post(
+ "/api/v1/boards/?board_name=Soft+Delete+Board",
+ headers={"Authorization": f"Bearer {user1_token}"},
+ )
+ assert create.status_code == status.HTTP_201_CREATED
+ board_id = create.json()["board_id"]
+
+ mock_invoker.services.board_video_records.get_all_board_video_names_for_board.return_value = ["v1.mp4"]
+
+ response = client.delete(
+ f"/api/v1/boards/{board_id}",
+ headers={"Authorization": f"Bearer {user1_token}"},
+ )
+ assert response.status_code == status.HTTP_200_OK
+ body = response.json()
+ assert body["deleted_board_videos"] == ["v1.mp4"]
+ # delete_videos_on_board MUST NOT be invoked when include_images is false.
+ mock_invoker.services.videos.delete_videos_on_board.assert_not_called()
diff --git a/tests/app/routers/test_image_moves.py b/tests/app/routers/test_image_moves.py
index 697447269c3..1dd6066b17a 100644
--- a/tests/app/routers/test_image_moves.py
+++ b/tests/app/routers/test_image_moves.py
@@ -10,6 +10,7 @@
from invokeai.app.services.auth.token_service import set_jwt_secret
from invokeai.app.services.board_image_records.board_image_records_sqlite import SqliteBoardImageRecordStorage
from invokeai.app.services.board_records.board_records_sqlite import SqliteBoardRecordStorage
+from invokeai.app.services.board_video_records.board_video_records_sqlite import SqliteBoardVideoRecordStorage
from invokeai.app.services.boards.boards_default import BoardService
from invokeai.app.services.bulk_download.bulk_download_default import BulkDownloadService
from invokeai.app.services.client_state_persistence.client_state_persistence_sqlite import ClientStatePersistenceSqlite
@@ -23,6 +24,7 @@
from invokeai.app.services.invoker import Invoker
from invokeai.app.services.users.users_common import UserCreateRequest
from invokeai.app.services.users.users_default import UserService
+from invokeai.app.services.video_records.video_records_sqlite import SqliteVideoRecordStorage
from invokeai.app.services.workflow_records.workflow_records_sqlite import SqliteWorkflowRecordsStorage
from invokeai.backend.util.logging import InvokeAILogger
from tests.fixtures.sqlite_database import create_mock_sqlite_database
@@ -79,6 +81,11 @@ def mock_services() -> InvocationServices:
model_relationships=None, # type: ignore
client_state_persistence=ClientStatePersistenceSqlite(db=db),
users=UserService(db),
+ videos=None, # type: ignore
+ video_files=None, # type: ignore
+ video_records=SqliteVideoRecordStorage(db=db),
+ board_video_records=SqliteBoardVideoRecordStorage(db=db),
+ gallery=None, # type: ignore
image_moves=image_moves,
)
diff --git a/tests/app/routers/test_model_manager_authorization.py b/tests/app/routers/test_model_manager_authorization.py
index 5ee03a161fd..3cb937f511a 100644
--- a/tests/app/routers/test_model_manager_authorization.py
+++ b/tests/app/routers/test_model_manager_authorization.py
@@ -158,8 +158,9 @@ def test_create_image_upload_entry_requires_auth_before_the_501_stub(
# (method, path) pairs that are deliberately public. Everything else must carry an auth dependency.
#
# - auth bootstrap: reachable pre-login by construction
-# - binary /thumbnail routes: browsers cannot attach a Bearer header to requests; closing
-# these needs a signed-URL or cookie scheme (tracked separately, see PR #9367)
+# - binary /thumbnail routes: browsers cannot attach a Bearer header to requests. Image and
+# video media routes now authenticate via the path-scoped media cookie (get_current_media_user_or_default);
+# the workflow-thumbnail and model-image routes remain open pending the same treatment (see PR #9367)
# - version: intentionally public
# - docs/redoc: API documentation
PUBLIC_ROUTES = {
@@ -167,9 +168,6 @@ def test_create_image_upload_entry_requires_auth_before_the_501_stub(
("POST", "/api/v1/auth/login"),
("POST", "/api/v1/auth/setup"),
("GET", "/api/v1/auth/status"),
- ("GET", "/api/v1/images/i/{image_name}/full"),
- ("HEAD", "/api/v1/images/i/{image_name}/full"),
- ("GET", "/api/v1/images/i/{image_name}/thumbnail"),
("GET", "/api/v1/workflows/i/{workflow_id}/thumbnail"),
("GET", "/api/v2/models/i/{key}/image"),
("GET", "/docs"),
@@ -184,7 +182,11 @@ def _routes_without_auth() -> set[tuple[str, str]]:
from invokeai.app.api import auth_dependencies
from invokeai.app.api_app import app
- auth_functions = {auth_dependencies.get_current_user, auth_dependencies.get_current_user_or_default}
+ auth_functions = {
+ auth_dependencies.get_current_user,
+ auth_dependencies.get_current_user_or_default,
+ auth_dependencies.get_current_media_user_or_default,
+ }
def has_auth(dependant: Any) -> bool:
if dependant.call in auth_functions:
diff --git a/tests/app/routers/test_multiuser_authorization.py b/tests/app/routers/test_multiuser_authorization.py
index 0475d99ce14..1d8d3f5f52a 100644
--- a/tests/app/routers/test_multiuser_authorization.py
+++ b/tests/app/routers/test_multiuser_authorization.py
@@ -9,6 +9,7 @@
"""
import logging
+from pathlib import Path
from typing import Any
from unittest.mock import AsyncMock, MagicMock
@@ -68,6 +69,7 @@ def client():
def mock_services() -> InvocationServices:
from invokeai.app.services.board_image_records.board_image_records_sqlite import SqliteBoardImageRecordStorage
from invokeai.app.services.board_records.board_records_sqlite import SqliteBoardRecordStorage
+ from invokeai.app.services.board_video_records.board_video_records_sqlite import SqliteBoardVideoRecordStorage
from invokeai.app.services.boards.boards_default import BoardService
from invokeai.app.services.bulk_download.bulk_download_default import BulkDownloadService
from invokeai.app.services.client_state_persistence.client_state_persistence_sqlite import (
@@ -78,6 +80,7 @@ def mock_services() -> InvocationServices:
from invokeai.app.services.invocation_cache.invocation_cache_memory import MemoryInvocationCache
from invokeai.app.services.invocation_stats.invocation_stats_default import InvocationStatsService
from invokeai.app.services.users.users_default import UserService
+ from invokeai.app.services.video_records.video_records_sqlite import SqliteVideoRecordStorage
from tests.test_nodes import TestEventService
configuration = InvokeAIAppConfig(use_memory_db=True, node_cache_size=0)
@@ -116,6 +119,11 @@ def mock_services() -> InvocationServices:
client_state_persistence=ClientStatePersistenceSqlite(db=db),
users=UserService(db),
external_generation=None, # type: ignore
+ videos=None, # type: ignore
+ video_files=None, # type: ignore
+ video_records=SqliteVideoRecordStorage(db=db),
+ board_video_records=SqliteBoardVideoRecordStorage(db=db),
+ gallery=None, # type: ignore
)
@@ -396,15 +404,66 @@ def test_get_image_metadata_requires_auth(self, enable_multiuser: Any, client: T
r = client.get("/api/v1/images/i/some-image/metadata")
assert r.status_code == status.HTTP_401_UNAUTHORIZED
- def test_get_image_full_is_unauthenticated(self, enable_multiuser: Any, client: TestClient):
- # Binary image endpoints are intentionally unauthenticated because
- # browsers load them via which cannot send Bearer tokens.
- r = client.get("/api/v1/images/i/some-image/full")
- assert r.status_code != status.HTTP_401_UNAUTHORIZED
+ @pytest.mark.parametrize("suffix", ["full", "thumbnail"])
+ def test_image_media_requires_auth(self, enable_multiuser: Any, client: TestClient, suffix: str):
+ client.cookies.clear()
- def test_get_image_thumbnail_is_unauthenticated(self, enable_multiuser: Any, client: TestClient):
- r = client.get("/api/v1/images/i/some-image/thumbnail")
- assert r.status_code != status.HTTP_401_UNAUTHORIZED
+ r = client.get(f"/api/v1/images/i/some-image/{suffix}")
+
+ assert r.status_code == status.HTTP_401_UNAUTHORIZED
+
+ @pytest.mark.parametrize("suffix,thumbnail", [("full", False), ("thumbnail", True)])
+ def test_image_owner_can_load_media_with_login_cookie(
+ self,
+ monkeypatch: Any,
+ client: TestClient,
+ mock_invoker: Invoker,
+ user1_token: str,
+ tmp_path: Path,
+ suffix: str,
+ thumbnail: bool,
+ ):
+ user1 = mock_invoker.services.users.get_by_email("user1@test.com")
+ assert user1 is not None
+ _save_image(mock_invoker, "owned-image", user1.user_id)
+ media_path = tmp_path / ("image.webp" if thumbnail else "image.png")
+ media_path.write_bytes(b"media")
+ get_path = MagicMock(return_value=media_path)
+ monkeypatch.setattr(mock_invoker.services.images, "get_path", get_path)
+ client.cookies.clear()
+ login = client.post(
+ "/api/v1/auth/login",
+ json={"email": "user1@test.com", "password": "TestPass123", "remember_me": False},
+ )
+ assert login.status_code == status.HTTP_200_OK
+
+ r = client.get(f"/api/v1/images/i/owned-image/{suffix}")
+
+ assert r.status_code == status.HTTP_200_OK
+ assert r.headers["cache-control"] == "private, no-store"
+ if thumbnail:
+ get_path.assert_called_once_with("owned-image", thumbnail=True)
+ else:
+ get_path.assert_called_once_with("owned-image")
+
+ def test_non_owner_cannot_load_private_image_media(
+ self,
+ monkeypatch: Any,
+ client: TestClient,
+ mock_invoker: Invoker,
+ user1_token: str,
+ user2_token: str,
+ ):
+ user1 = mock_invoker.services.users.get_by_email("user1@test.com")
+ assert user1 is not None
+ _save_image(mock_invoker, "private-image", user1.user_id)
+ get_path = MagicMock()
+ monkeypatch.setattr(mock_invoker.services.images, "get_path", get_path)
+
+ r = client.get("/api/v1/images/i/private-image/full", headers=_auth(user2_token))
+
+ assert r.status_code == status.HTTP_403_FORBIDDEN
+ get_path.assert_not_called()
def test_get_image_urls_requires_auth(self, enable_multiuser: Any, client: TestClient):
r = client.get("/api/v1/images/i/some-image/urls")
@@ -611,6 +670,33 @@ def test_delete_uncategorized_requires_auth(self, enable_multiuser: Any, client:
r = client.delete("/api/v1/images/uncategorized")
assert r.status_code == status.HTTP_401_UNAUTHORIZED
+ def test_delete_uncategorized_reports_owned_images_that_failed(
+ self,
+ client: TestClient,
+ mock_invoker: Invoker,
+ monkeypatch: pytest.MonkeyPatch,
+ user1_token: str,
+ ):
+ user = mock_invoker.services.users.get_by_email("user1@test.com")
+ assert user is not None
+ _save_image(mock_invoker, "deleted.png", user.user_id)
+ _save_image(mock_invoker, "stuck.png", user.user_id)
+ mock_invoker.services.board_images.get_all_board_image_names_for_board.return_value = [
+ "deleted.png",
+ "stuck.png",
+ ]
+ monkeypatch.setattr(
+ mock_invoker.services.images,
+ "delete",
+ MagicMock(side_effect=[None, OSError("file busy")]),
+ )
+
+ response = client.delete("/api/v1/images/uncategorized", headers=_auth(user1_token))
+
+ assert response.status_code == status.HTTP_200_OK
+ assert response.json()["deleted_images"] == ["deleted.png"]
+ assert response.json()["failed_images"] == ["stuck.png"]
+
def test_non_owner_cannot_delete_image(
self, client: TestClient, mock_invoker: Invoker, user1_token: str, user2_token: str
):
@@ -689,6 +775,11 @@ def test_non_owner_cannot_star_image(
def test_non_owner_cannot_batch_delete_image(
self, client: TestClient, mock_invoker: Invoker, user1_token: str, user2_token: str
):
+ """Batch delete tolerates foreign items by skipping them in-loop and returning the
+ per-item delete list to the client. Previously the route re-raised the first 403,
+ dropping any partial successes — see PR #9163 review (finding 3). The non-owner
+ must still not destroy the image; only the response shape changes.
+ """
user1 = mock_invoker.services.users.get_by_email("user1@test.com")
assert user1 is not None
_save_image(mock_invoker, "user1-batch-del", user1.user_id)
@@ -698,7 +789,12 @@ def test_non_owner_cannot_batch_delete_image(
json={"image_names": ["user1-batch-del"]},
headers=_auth(user2_token),
)
- assert r.status_code == status.HTTP_403_FORBIDDEN
+ assert r.status_code == status.HTTP_200_OK
+ body = r.json()
+ # Auth failure must not advertise the foreign image as deleted, and the underlying
+ # record must still exist.
+ assert body["deleted_images"] == []
+ mock_invoker.services.image_records.get("user1-batch-del")
def test_non_owner_can_delete_image_from_public_board(
self, client: TestClient, mock_invoker: Invoker, user1_token: str, user2_token: str
diff --git a/tests/app/routers/test_session_queue_workflow_call.py b/tests/app/routers/test_session_queue_workflow_call.py
index d8fd465d8d3..69be937125f 100644
--- a/tests/app/routers/test_session_queue_workflow_call.py
+++ b/tests/app/routers/test_session_queue_workflow_call.py
@@ -45,6 +45,7 @@ def client():
def mock_services() -> InvocationServices:
from invokeai.app.services.board_image_records.board_image_records_sqlite import SqliteBoardImageRecordStorage
from invokeai.app.services.board_records.board_records_sqlite import SqliteBoardRecordStorage
+ from invokeai.app.services.board_video_records.board_video_records_sqlite import SqliteBoardVideoRecordStorage
from invokeai.app.services.boards.boards_default import BoardService
from invokeai.app.services.bulk_download.bulk_download_default import BulkDownloadService
from invokeai.app.services.client_state_persistence.client_state_persistence_sqlite import (
@@ -55,6 +56,7 @@ def mock_services() -> InvocationServices:
from invokeai.app.services.invocation_cache.invocation_cache_memory import MemoryInvocationCache
from invokeai.app.services.invocation_stats.invocation_stats_default import InvocationStatsService
from invokeai.app.services.users.users_default import UserService
+ from invokeai.app.services.video_records.video_records_sqlite import SqliteVideoRecordStorage
from tests.test_nodes import TestEventService
configuration = InvokeAIAppConfig(use_memory_db=True, node_cache_size=0)
@@ -93,6 +95,11 @@ def mock_services() -> InvocationServices:
client_state_persistence=ClientStatePersistenceSqlite(db=db),
users=UserService(db),
external_generation=None, # type: ignore
+ videos=None, # type: ignore
+ video_files=None, # type: ignore
+ video_records=SqliteVideoRecordStorage(db=db),
+ board_video_records=SqliteBoardVideoRecordStorage(db=db),
+ gallery=None, # type: ignore
)
diff --git a/tests/app/routers/test_video_range_header.py b/tests/app/routers/test_video_range_header.py
new file mode 100644
index 00000000000..eace12a4458
--- /dev/null
+++ b/tests/app/routers/test_video_range_header.py
@@ -0,0 +1,47 @@
+"""Tests for the video route's HTTP Range header parser."""
+
+import pytest
+
+from invokeai.app.api.routers.videos import _parse_range_header
+
+FILE_SIZE = 1000
+
+
+@pytest.mark.parametrize(
+ "header,expected",
+ [
+ ("bytes=0-499", (0, 499)),
+ ("bytes=500-999", (500, 999)),
+ ("bytes=500-", (500, 999)), # open-ended
+ ("bytes=-500", (500, 999)), # suffix: last 500 bytes
+ ("bytes=-2000", (0, 999)), # suffix longer than file clamps to whole file
+ ("bytes=0-1999", (0, 999)), # end clamps to file size
+ (" bytes=0-0 ", (0, 0)), # whitespace tolerated, single byte
+ ],
+)
+def test_valid_ranges(header: str, expected: tuple[int, int]) -> None:
+ assert _parse_range_header(header, FILE_SIZE) == expected
+
+
+@pytest.mark.parametrize(
+ "header",
+ [
+ "bytes=-", # no start, no end
+ "bytes=abc-def",
+ "bytes=100", # missing dash
+ "0-499", # missing bytes= prefix
+ "bytes=500-100", # start > end
+ "bytes=1000-", # start beyond EOF
+ "bytes=-0", # zero-length suffix
+ "bytes=0-499,600-699", # multipart ranges unsupported
+ ],
+)
+def test_malformed_or_unsatisfiable_ranges(header: str) -> None:
+ assert _parse_range_header(header, FILE_SIZE) is None
+
+
+@pytest.mark.parametrize("header", ["bytes=-500", "bytes=0-", "bytes=0-0"])
+def test_empty_file_is_never_satisfiable(header: str) -> None:
+ """Regression: a suffix range against a zero-length file used to return (0, -1),
+ producing a 206 with `Content-Range: bytes 0--1/0` instead of a 416."""
+ assert _parse_range_header(header, 0) is None
diff --git a/tests/app/routers/test_videos_multiuser.py b/tests/app/routers/test_videos_multiuser.py
new file mode 100644
index 00000000000..d72aa5cf5ad
--- /dev/null
+++ b/tests/app/routers/test_videos_multiuser.py
@@ -0,0 +1,812 @@
+"""Multiuser regression tests for the /v1/videos/ routes.
+
+Covers JPPhoto's code-review finding (PR #9163): the list endpoints accepted
+an explicit ``board_id`` with no read-access check, so a non-admin user could
+enumerate videos on someone else's private board if they happened to know its
+id. The fix added ``_assert_board_read_access`` to both ``list_video_dtos``
+and ``get_video_names``.
+
+These tests exercise the HTTP layer end-to-end (auth + route guards) using the
+same fixture pattern as test_boards_multiuser. The storage-level user_id
+filter is covered separately in tests/app/services/video_records.
+"""
+
+from pathlib import Path
+from typing import Any
+from unittest.mock import MagicMock, patch
+
+import pytest
+from fastapi import status
+from fastapi.testclient import TestClient
+
+from invokeai.app.api.dependencies import ApiDependencies
+from invokeai.app.api.routers.videos import _is_mp4_file
+from invokeai.app.api_app import app
+from invokeai.app.services.invoker import Invoker
+from invokeai.app.services.users.users_common import UserCreateRequest
+from invokeai.app.services.videos.videos_common import VideoDTO
+
+
+class MockApiDependencies(ApiDependencies):
+ invoker: Invoker
+
+ def __init__(self, invoker: Invoker) -> None:
+ self.invoker = invoker
+
+
+@pytest.fixture
+def setup_jwt_secret():
+ from invokeai.app.services.auth.token_service import set_jwt_secret
+
+ set_jwt_secret("test-secret-key-for-unit-tests-only-do-not-use-in-production")
+
+
+@pytest.fixture
+def client():
+ return TestClient(app)
+
+
+def setup_test_user(
+ mock_invoker: Invoker,
+ email: str,
+ display_name: str,
+ password: str = "TestPass123",
+ is_admin: bool = False,
+) -> str:
+ user_service = mock_invoker.services.users
+ user = user_service.create(
+ UserCreateRequest(email=email, display_name=display_name, password=password, is_admin=is_admin)
+ )
+ return user.user_id
+
+
+def get_user_token(client: TestClient, email: str, password: str = "TestPass123") -> str:
+ response = client.post(
+ "/api/v1/auth/login",
+ json={"email": email, "password": password, "remember_me": False},
+ )
+ assert response.status_code == 200
+ return response.json()["token"]
+
+
+@pytest.fixture
+def enable_multiuser_for_videos(monkeypatch: Any, mock_invoker: Invoker):
+ """Enable multiuser and stub services the video routes touch."""
+ mock_invoker.services.configuration.multiuser = True
+
+ # The list routes call services.videos.get_many / get_video_names. We don't care about
+ # the payloads here — only whether the route runs the board-access guard *before* the
+ # service call. A return value of "any non-error response" is enough.
+ mock_videos = MagicMock()
+ mock_videos.get_many.return_value = {"items": [], "offset": 0, "limit": 10, "total": 0}
+ mock_videos.get_video_names.return_value = {"video_names": [], "starred_count": 0, "total_count": 0}
+ mock_invoker.services.videos = mock_videos
+
+ # board_video_records is touched by remove_video_from_board; not exercised by the
+ # list tests but stub it defensively so unrelated routes don't blow up.
+ mock_invoker.services.board_video_records = MagicMock()
+ # The board service computes video_count + cover_video_name on every get_dto/update;
+ # an unconfigured MagicMock returns nested MagicMocks that fail Pydantic validation and
+ # the boards route swallows the exception as a 404. Pin sane defaults.
+ mock_invoker.services.board_video_records.get_video_count_for_board.return_value = 0
+ mock_invoker.services.video_records = MagicMock()
+ mock_invoker.services.video_records.get_most_recent_video_for_board.return_value = None
+ mock_invoker.services.board_images = MagicMock()
+ mock_invoker.services.board_images.get_all_board_image_names_for_board.return_value = []
+
+ mock_deps = MockApiDependencies(mock_invoker)
+ monkeypatch.setattr("invokeai.app.api.routers.auth.ApiDependencies", mock_deps)
+ monkeypatch.setattr("invokeai.app.api_app.ApiDependencies", mock_deps)
+ monkeypatch.setattr("invokeai.app.api.auth_dependencies.ApiDependencies", mock_deps)
+ monkeypatch.setattr("invokeai.app.api.routers.boards.ApiDependencies", mock_deps)
+ monkeypatch.setattr("invokeai.app.api.routers.videos.ApiDependencies", mock_deps)
+ monkeypatch.setattr("invokeai.app.api.routers.images.ApiDependencies", mock_deps)
+ # _access.assert_board_read_access is called from list_video_dtos and get_video_names
+ # via the videos router; it uses ApiDependencies from its own module scope.
+ monkeypatch.setattr("invokeai.app.api.routers._access.ApiDependencies", mock_deps)
+ yield
+
+
+@pytest.fixture
+def admin_token(setup_jwt_secret: None, enable_multiuser_for_videos: Any, mock_invoker: Invoker, client: TestClient):
+ setup_test_user(mock_invoker, "admin@test.com", "Test Admin", is_admin=True)
+ return get_user_token(client, "admin@test.com")
+
+
+@pytest.fixture
+def user1_token(enable_multiuser_for_videos: Any, mock_invoker: Invoker, client: TestClient, admin_token: str):
+ setup_test_user(mock_invoker, "user1@test.com", "User One", is_admin=False)
+ return get_user_token(client, "user1@test.com")
+
+
+@pytest.fixture
+def user2_token(enable_multiuser_for_videos: Any, mock_invoker: Invoker, client: TestClient, admin_token: str):
+ setup_test_user(mock_invoker, "user2@test.com", "User Two", is_admin=False)
+ return get_user_token(client, "user2@test.com")
+
+
+@pytest.fixture
+def user1_private_board(client: TestClient, user1_token: str) -> str:
+ response = client.post(
+ "/api/v1/boards/?board_name=User1+Private+Board",
+ headers={"Authorization": f"Bearer {user1_token}"},
+ )
+ assert response.status_code == status.HTTP_201_CREATED
+ return response.json()["board_id"]
+
+
+# ---------------------------------------------------------------------------
+# Auth requirement
+# ---------------------------------------------------------------------------
+
+
+def test_list_video_dtos_requires_auth(enable_multiuser_for_videos: Any, client: TestClient):
+ response = client.get("/api/v1/videos/")
+ assert response.status_code == status.HTTP_401_UNAUTHORIZED
+
+
+def test_get_video_names_requires_auth(enable_multiuser_for_videos: Any, client: TestClient):
+ response = client.get("/api/v1/videos/names")
+ assert response.status_code == status.HTTP_401_UNAUTHORIZED
+
+
+# ---------------------------------------------------------------------------
+# Explicit board_id with no read access (the JPPhoto finding)
+# ---------------------------------------------------------------------------
+
+
+def test_list_video_dtos_forbidden_for_other_users_private_board(
+ client: TestClient, user1_private_board: str, user2_token: str
+):
+ """user2 cannot list videos on user1's private board even if they know the board_id."""
+ response = client.get(
+ f"/api/v1/videos/?board_id={user1_private_board}",
+ headers={"Authorization": f"Bearer {user2_token}"},
+ )
+ assert response.status_code == status.HTTP_403_FORBIDDEN
+
+
+def test_get_video_names_forbidden_for_other_users_private_board(
+ client: TestClient, user1_private_board: str, user2_token: str
+):
+ response = client.get(
+ f"/api/v1/videos/names?board_id={user1_private_board}",
+ headers={"Authorization": f"Bearer {user2_token}"},
+ )
+ assert response.status_code == status.HTTP_403_FORBIDDEN
+
+
+def test_owner_can_list_videos_on_their_private_board(client: TestClient, user1_private_board: str, user1_token: str):
+ response = client.get(
+ f"/api/v1/videos/?board_id={user1_private_board}",
+ headers={"Authorization": f"Bearer {user1_token}"},
+ )
+ assert response.status_code == status.HTTP_200_OK
+
+
+def test_admin_can_list_videos_on_any_private_board(client: TestClient, user1_private_board: str, admin_token: str):
+ response = client.get(
+ f"/api/v1/videos/?board_id={user1_private_board}",
+ headers={"Authorization": f"Bearer {admin_token}"},
+ )
+ assert response.status_code == status.HTTP_200_OK
+
+
+# ---------------------------------------------------------------------------
+# Omitted board_id: route should not blow up; isolation enforced at SQL layer
+# ---------------------------------------------------------------------------
+
+
+def test_list_video_dtos_no_board_id_succeeds_for_any_authed_user(client: TestClient, user2_token: str):
+ """The route allows omitted board_id (the SQL layer filters by user_id) — no 403 here."""
+ response = client.get(
+ "/api/v1/videos/",
+ headers={"Authorization": f"Bearer {user2_token}"},
+ )
+ assert response.status_code == status.HTTP_200_OK
+
+
+def test_list_video_dtos_none_board_succeeds_for_any_authed_user(client: TestClient, user2_token: str):
+ response = client.get(
+ "/api/v1/videos/?board_id=none",
+ headers={"Authorization": f"Bearer {user2_token}"},
+ )
+ assert response.status_code == status.HTTP_200_OK
+
+
+# ---------------------------------------------------------------------------
+# POST /videos/delete must not re-raise mid-loop (PR #9163 review fix)
+# ---------------------------------------------------------------------------
+
+
+def test_delete_videos_from_list_dedupes_repeated_names(client: TestClient, mock_invoker: Invoker, admin_token: str):
+ """A name repeated in the request must be processed once. Previously the second pass
+ hit a not-found error for the already-deleted name (a plain Exception under the admin
+ ownership bypass), landing the same name in BOTH deleted_videos and failed_videos and
+ toasting a spurious partial-failure warning (JPPhoto non-merge-blocker, 2026-07-22).
+ """
+ fake_dto = MagicMock()
+ fake_dto.board_id = None
+ mock_invoker.services.videos.get_dto.return_value = fake_dto
+
+ response = client.post(
+ "/api/v1/videos/delete",
+ json={"video_names": ["dup.mp4", "dup.mp4", "other.mp4"]},
+ headers={"Authorization": f"Bearer {admin_token}"},
+ )
+
+ assert response.status_code == status.HTTP_200_OK
+ body = response.json()
+ assert sorted(body["deleted_videos"]) == ["dup.mp4", "other.mp4"]
+ assert body["failed_videos"] == []
+ # The service must have been asked to delete each unique name exactly once.
+ delete_calls = [call.args[0] for call in mock_invoker.services.videos.delete.call_args_list]
+ assert sorted(delete_calls) == ["dup.mp4", "other.mp4"]
+
+
+def test_delete_videos_from_list_skips_foreign_items_and_returns_owned(
+ client: TestClient, mock_invoker: Invoker, user1_token: str
+):
+ """A non-admin batch delete that includes a video owned by another user must keep going
+ and return 200 with the owned items in ``deleted_videos``. Previously the route raised
+ 403 mid-loop, throwing away the response payload so the frontend cache never learned
+ about already-deleted records and the UI showed stale entries until the next refresh.
+ """
+ # Resolve user1's id from the token claim so we can wire up the ownership stub
+ # without depending on test-internal user state.
+ user1 = mock_invoker.services.users.get_by_email("user1@test.com")
+ assert user1 is not None
+ user1_id = user1.user_id
+
+ def fake_get_user_id(video_name: str):
+ # Names beginning with 'mine_' belong to user1, anything else to a stranger.
+ return user1_id if video_name.startswith("mine_") else "other-user-id"
+
+ mock_invoker.services.video_records.get_user_id.side_effect = fake_get_user_id
+ # When _assert_video_owner falls back to the board check, return no board so the public
+ # fallback path doesn't relax permissions for the foreign video.
+ mock_invoker.services.board_video_records.get_board_for_video.return_value = None
+
+ fake_dto = MagicMock()
+ fake_dto.board_id = None
+ mock_invoker.services.videos.get_dto.return_value = fake_dto
+
+ response = client.post(
+ "/api/v1/videos/delete",
+ json={"video_names": ["mine_a.mp4", "foreign.mp4", "mine_b.mp4"]},
+ headers={"Authorization": f"Bearer {user1_token}"},
+ )
+ assert response.status_code == status.HTTP_200_OK
+ body = response.json()
+ # Both owned items must appear; the foreign one must be skipped silently — an
+ # authorization skip is not a failed deletion and must not be toasted as one.
+ assert set(body["deleted_videos"]) == {"mine_a.mp4", "mine_b.mp4"}
+ assert body["failed_videos"] == []
+ # The service must have been told to delete the owned names but not the foreign one.
+ delete_calls = {call.args[0] for call in mock_invoker.services.videos.delete.call_args_list}
+ assert delete_calls == {"mine_a.mp4", "mine_b.mp4"}
+
+
+# ---------------------------------------------------------------------------
+# POST /videos/star and /videos/unstar must not re-raise mid-loop either
+# (PR #9163 review fix — same partial-mutation-then-403 pattern as bulk delete)
+# ---------------------------------------------------------------------------
+
+
+def _setup_mixed_ownership_batch(mock_invoker: Invoker, user1_id: str) -> None:
+ """Names beginning with 'mine_' belong to user1, anything else to a stranger."""
+
+ def fake_get_user_id(video_name: str):
+ return user1_id if video_name.startswith("mine_") else "other-user-id"
+
+ mock_invoker.services.video_records.get_user_id.side_effect = fake_get_user_id
+ # When _assert_video_owner falls back to the board check, return no board so the public
+ # fallback path doesn't relax permissions for the foreign video.
+ mock_invoker.services.board_video_records.get_board_for_video.return_value = None
+
+ # The route reads ``updated.board_id`` to build ``affected_boards``; a bare MagicMock
+ # there would fail the response model's Pydantic validation.
+ fake_updated = MagicMock()
+ fake_updated.board_id = None
+ mock_invoker.services.videos.update.return_value = fake_updated
+
+
+def test_star_videos_from_list_skips_foreign_items_and_returns_owned(
+ client: TestClient, mock_invoker: Invoker, user1_token: str
+):
+ """A batch star that includes a video owned by another user must keep going and return
+ 200 with the owned items in ``starred_videos``. Previously the route raised 403
+ mid-loop: earlier videos were already mutated, but the error-shaped response carried no
+ payload, so the client never invalidated caches for the successful updates.
+ """
+ user1 = mock_invoker.services.users.get_by_email("user1@test.com")
+ assert user1 is not None
+ _setup_mixed_ownership_batch(mock_invoker, user1.user_id)
+
+ response = client.post(
+ "/api/v1/videos/star",
+ json={"video_names": ["mine_a.mp4", "foreign.mp4", "mine_b.mp4"]},
+ headers={"Authorization": f"Bearer {user1_token}"},
+ )
+ assert response.status_code == status.HTTP_200_OK
+ body = response.json()
+ assert set(body["starred_videos"]) == {"mine_a.mp4", "mine_b.mp4"}
+ # The service must have been asked to update the owned names but not the foreign one.
+ update_calls = {call.args[0] for call in mock_invoker.services.videos.update.call_args_list}
+ assert update_calls == {"mine_a.mp4", "mine_b.mp4"}
+
+
+def test_unstar_videos_from_list_skips_foreign_items_and_returns_owned(
+ client: TestClient, mock_invoker: Invoker, user1_token: str
+):
+ user1 = mock_invoker.services.users.get_by_email("user1@test.com")
+ assert user1 is not None
+ _setup_mixed_ownership_batch(mock_invoker, user1.user_id)
+
+ response = client.post(
+ "/api/v1/videos/unstar",
+ json={"video_names": ["mine_a.mp4", "foreign.mp4", "mine_b.mp4"]},
+ headers={"Authorization": f"Bearer {user1_token}"},
+ )
+ assert response.status_code == status.HTTP_200_OK
+ body = response.json()
+ assert set(body["unstarred_videos"]) == {"mine_a.mp4", "mine_b.mp4"}
+ update_calls = {call.args[0] for call in mock_invoker.services.videos.update.call_args_list}
+ assert update_calls == {"mine_a.mp4", "mine_b.mp4"}
+
+
+# ---------------------------------------------------------------------------
+# POST /videos/upload must reject malformed MP4 payloads with 415 (residual
+# verification flagged in JPPhoto's PR #9163 review)
+# ---------------------------------------------------------------------------
+
+
+def test_upload_video_malformed_mp4_returns_415_and_cleans_up_tmp(
+ client: TestClient, mock_invoker: Invoker, user1_token: str, tmp_path: Path
+):
+ """An upload that looks like an MP4 on the surface (``.mp4`` extension or video MIME
+ type) but contains bytes ``probe_video`` can't decode must:
+
+ 1. Reach ``probe_video`` (the extension/MIME gate is intentionally permissive — the
+ real validation is the decode probe).
+ 2. Surface a 415 to the caller.
+ 3. Unlink the streamed-to-disk temp file so the server doesn't leak storage on every
+ garbage upload.
+ """
+ # Capture the tmp path the route created so we can prove it was unlinked after the
+ # 415 response. ``tempfile.NamedTemporaryFile(..., delete=False)`` is invoked inside
+ # the route, so we wrap the real call and stash the resulting path.
+ captured_paths: list[Path] = []
+
+ import tempfile as _tempfile
+
+ real_named_tmp = _tempfile.NamedTemporaryFile
+
+ def spying_named_tmp(*args: Any, **kwargs: Any):
+ handle = real_named_tmp(*args, **kwargs)
+ captured_paths.append(Path(handle.name))
+ return handle
+
+ # The fixture's videos mock would no-op the service call; we explicitly do NOT want
+ # that path to fire because we're asserting probe_video runs and rejects.
+ mock_invoker.services.videos.create.side_effect = AssertionError(
+ "videos.create should not be called when probe_video rejects the upload"
+ )
+
+ with (
+ patch("invokeai.app.api.routers.videos.tempfile.NamedTemporaryFile", side_effect=spying_named_tmp),
+ patch(
+ "invokeai.app.api.routers.videos.probe_video",
+ side_effect=RuntimeError("not a decodable mp4"),
+ ),
+ ):
+ response = client.post(
+ "/api/v1/videos/upload",
+ params={"video_category": "general", "is_intermediate": False},
+ files={"file": ("renamed_text.mp4", b"this is not an mp4 payload at all", "video/mp4")},
+ headers={"Authorization": f"Bearer {user1_token}"},
+ )
+
+ assert response.status_code == status.HTTP_415_UNSUPPORTED_MEDIA_TYPE
+ # The route should have allocated exactly one tmp file and then unlinked it.
+ assert len(captured_paths) == 1, f"expected one tmp file, got {captured_paths}"
+ tmp_file = captured_paths[0]
+ assert not tmp_file.exists(), f"tmp file leaked after 415: {tmp_file}"
+
+
+def test_upload_video_rejects_non_mp4_container_with_spoofed_mime(
+ client: TestClient, mock_invoker: Invoker, user1_token: str
+):
+ mock_invoker.services.videos.create.side_effect = AssertionError("non-MP4 payload reached video creation")
+ with patch("invokeai.app.api.routers.videos._probe_decodable_video", return_value=((64, 64, 1.0, 8.0), None)):
+ response = client.post(
+ "/api/v1/videos/upload",
+ params={"video_category": "general", "is_intermediate": False},
+ files={"file": ("spoofed.mp4", b"\x1aE\xdf\xa3webm payload", "video/mp4")},
+ headers={"Authorization": f"Bearer {user1_token}"},
+ )
+
+ assert response.status_code == status.HTTP_415_UNSUPPORTED_MEDIA_TYPE
+ mock_invoker.services.videos.create.assert_not_called()
+
+
+def _uploaded_video_dto() -> VideoDTO:
+ return VideoDTO.model_validate(
+ {
+ "video_name": "uploaded.mp4",
+ "video_origin": "external",
+ "video_category": "general",
+ "width": 64,
+ "height": 64,
+ "duration": 1.0,
+ "fps": 8.0,
+ "created_at": "2026-01-01T00:00:00Z",
+ "updated_at": "2026-01-01T00:00:00Z",
+ "is_intermediate": False,
+ "starred": False,
+ "has_workflow": False,
+ "video_subfolder": "",
+ "video_url": "/api/v1/videos/i/uploaded.mp4/full",
+ "thumbnail_url": "/api/v1/videos/i/uploaded.mp4/thumbnail",
+ }
+ )
+
+
+@pytest.mark.parametrize("metadata", ["not json", '["not", "an", "object"]'])
+def test_upload_video_rejects_malformed_metadata_before_create(
+ client: TestClient, mock_invoker: Invoker, user1_token: str, metadata: str
+):
+ mock_invoker.services.videos.create.return_value = _uploaded_video_dto()
+ mp4 = b"\x00\x00\x00\x18ftypmp42" + b"\x00" * 12
+
+ with patch("invokeai.app.api.routers.videos._probe_decodable_video", return_value=((64, 64, 1.0, 8.0), None)):
+ response = client.post(
+ "/api/v1/videos/upload",
+ params={"video_category": "general", "is_intermediate": False},
+ files={"file": ("video.mp4", mp4, "video/mp4")},
+ data={"metadata": metadata},
+ headers={"Authorization": f"Bearer {user1_token}"},
+ )
+
+ assert response.status_code == status.HTTP_422_UNPROCESSABLE_ENTITY
+ mock_invoker.services.videos.create.assert_not_called()
+
+
+def test_upload_video_accepts_object_metadata(client: TestClient, mock_invoker: Invoker, user1_token: str):
+ mock_invoker.services.videos.create.return_value = _uploaded_video_dto()
+ mp4 = b"\x00\x00\x00\x18ftypmp42" + b"\x00" * 12
+ metadata = '{"seed": 123}'
+
+ with patch("invokeai.app.api.routers.videos._probe_decodable_video", return_value=((64, 64, 1.0, 8.0), None)):
+ response = client.post(
+ "/api/v1/videos/upload",
+ params={"video_category": "general", "is_intermediate": False},
+ files={"file": ("video.mp4", mp4, "video/mp4")},
+ data={"metadata": metadata},
+ headers={"Authorization": f"Bearer {user1_token}"},
+ )
+
+ assert response.status_code == status.HTTP_201_CREATED
+ assert mock_invoker.services.videos.create.call_args.kwargs["metadata"] == metadata
+
+
+def test_mp4_validation_allows_boxes_before_file_type(tmp_path: Path) -> None:
+ path = tmp_path / "valid.mp4"
+ path.write_bytes(b"\x00\x00\x00\x08free" + b"\x00\x00\x00\x18ftypmp42" + b"\x00" * 12)
+
+ assert _is_mp4_file(path)
+
+
+def test_mp4_validation_rejects_quicktime_brand(tmp_path: Path) -> None:
+ path = tmp_path / "quicktime.mp4"
+ path.write_bytes(b"\x00\x00\x00\x18ftypqt " + b"\x00" * 12)
+
+ assert not _is_mp4_file(path)
+
+
+@pytest.mark.parametrize("suffix,thumbnail", [("full", False), ("thumbnail", True)])
+def test_video_media_requires_auth_in_multiuser_mode(
+ enable_multiuser_for_videos: Any,
+ client: TestClient,
+ mock_invoker: Invoker,
+ tmp_path: Path,
+ suffix: str,
+ thumbnail: bool,
+):
+ client.cookies.clear()
+ media_path = tmp_path / ("video.webp" if thumbnail else "video.mp4")
+ media_path.write_bytes(b"media")
+ mock_invoker.services.videos.get_path.return_value = str(media_path)
+
+ response = client.get(f"/api/v1/videos/i/private.mp4/{suffix}")
+
+ assert response.status_code == status.HTTP_401_UNAUTHORIZED
+ mock_invoker.services.videos.get_path.assert_not_called()
+
+
+@pytest.mark.parametrize("suffix,thumbnail", [("full", False), ("thumbnail", True)])
+def test_video_owner_can_load_media_with_login_cookie(
+ client: TestClient,
+ mock_invoker: Invoker,
+ user1_token: str,
+ tmp_path: Path,
+ suffix: str,
+ thumbnail: bool,
+):
+ user1 = mock_invoker.services.users.get_by_email("user1@test.com")
+ assert user1 is not None
+ mock_invoker.services.video_records.get_user_id.return_value = user1.user_id
+ media_path = tmp_path / ("video.webp" if thumbnail else "video.mp4")
+ media_path.write_bytes(b"media")
+ mock_invoker.services.videos.get_path.return_value = str(media_path)
+ client.cookies.clear()
+ login = client.post(
+ "/api/v1/auth/login",
+ json={"email": "user1@test.com", "password": "TestPass123", "remember_me": False},
+ )
+ assert login.status_code == status.HTTP_200_OK
+
+ response = client.get(f"/api/v1/videos/i/private.mp4/{suffix}")
+
+ assert response.status_code == status.HTTP_200_OK
+ assert response.headers["cache-control"] == "private, no-store"
+ mock_invoker.services.videos.get_path.assert_called_once_with("private.mp4", thumbnail=thumbnail)
+
+
+def test_foreign_user_cannot_load_private_video_media(
+ client: TestClient, mock_invoker: Invoker, user1_token: str, user2_token: str
+):
+ user1 = mock_invoker.services.users.get_by_email("user1@test.com")
+ assert user1 is not None
+ mock_invoker.services.video_records.get_user_id.return_value = user1.user_id
+ mock_invoker.services.board_video_records.get_board_for_video.return_value = None
+
+ response = client.get(
+ "/api/v1/videos/i/private.mp4/full",
+ headers={"Authorization": f"Bearer {user2_token}"},
+ )
+
+ assert response.status_code == status.HTTP_403_FORBIDDEN
+ mock_invoker.services.videos.get_path.assert_not_called()
+
+
+# ---------------------------------------------------------------------------
+# GET /videos/i/{video_name}/thumbnail must return 404 when the thumbnail file
+# is missing on disk (JPPhoto PR #9163 follow-up). Video saves are allowed
+# without a thumbnail in video_files_disk.save, so this is reachable.
+# ---------------------------------------------------------------------------
+
+
+def test_get_video_thumbnail_missing_file_returns_404(
+ client: TestClient,
+ mock_invoker: Invoker,
+ user1_token: str,
+ tmp_path: Path,
+):
+ """If videos.get_path resolves successfully but the file doesn't exist, the route must
+ return 404 up front. Previously it returned FileResponse and the missing-file error was
+ raised by Starlette *after* the route's try/except, so callers saw a 500-class failure
+ instead of the documented 404.
+ """
+ missing_path = tmp_path / "does_not_exist.webp"
+ assert not missing_path.exists()
+ user1 = mock_invoker.services.users.get_by_email("user1@test.com")
+ assert user1 is not None
+ mock_invoker.services.video_records.get_user_id.return_value = user1.user_id
+ mock_invoker.services.videos.get_path.return_value = str(missing_path)
+
+ response = client.get(
+ "/api/v1/videos/i/some_video.mp4/thumbnail",
+ headers={"Authorization": f"Bearer {user1_token}"},
+ )
+ assert response.status_code == status.HTTP_404_NOT_FOUND
+ mock_invoker.services.videos.get_path.assert_called_once_with("some_video.mp4", thumbnail=True)
+
+
+# ---------------------------------------------------------------------------
+# DELETE /videos/board: stranded-contributor recovery (JPPhoto PR #9163 May-22 follow-up)
+#
+# Scenario: user2 uploads to user1's Public board, user1 later flips the board to
+# Shared/Private. Without a fallback path, neither the uploader nor the board owner
+# can detach the video — _assert_video_direct_owner rejects user1, and
+# _assert_board_write_access rejects user2 because the board is no longer Public.
+# The route must accept removal from either the video owner OR a user with write
+# access to the destination board (mirrors remove_image_from_board).
+# ---------------------------------------------------------------------------
+
+
+def test_remove_video_from_board_succeeds_for_video_owner_on_foreign_private_board(
+ client: TestClient, mock_invoker: Invoker, user1_token: str, user2_token: str
+):
+ """user2 owns the video; the video sits on user1's now-private board. user2 must still
+ be able to detach it via its direct ownership."""
+ user1 = mock_invoker.services.users.get_by_email("user1@test.com")
+ user2 = mock_invoker.services.users.get_by_email("user2@test.com")
+ assert user1 is not None and user2 is not None
+
+ # user2 owns the video.
+ mock_invoker.services.video_records.get_user_id.return_value = user2.user_id
+ # The video lives on user1's now-private board.
+ mock_invoker.services.board_video_records.get_board_for_video.return_value = "user1-private-board"
+
+ response = client.request(
+ "DELETE",
+ "/api/v1/videos/board",
+ json={"video_name": "uploaded.mp4"},
+ headers={"Authorization": f"Bearer {user2_token}"},
+ )
+ assert response.status_code == status.HTTP_200_OK
+ mock_invoker.services.board_video_records.remove_video_from_board.assert_called_once_with(video_name="uploaded.mp4")
+
+
+def test_remove_video_from_board_succeeds_for_board_owner_of_non_owned_video(
+ client: TestClient, mock_invoker: Invoker, user1_token: str, user2_token: str
+):
+ """user1 owns the board; user2 owns the video sitting on it. user1 must be able to
+ detach the foreign video from their board even though they are not the video owner."""
+ from invokeai.app.services.board_records.board_records_common import BoardVisibility
+
+ user1 = mock_invoker.services.users.get_by_email("user1@test.com")
+ user2 = mock_invoker.services.users.get_by_email("user2@test.com")
+ assert user1 is not None and user2 is not None
+
+ mock_invoker.services.video_records.get_user_id.return_value = user2.user_id
+ mock_invoker.services.board_video_records.get_board_for_video.return_value = "user1-board"
+
+ # _assert_board_write_access reads the board DTO to check ownership/visibility.
+ fake_board = MagicMock()
+ fake_board.user_id = user1.user_id
+ fake_board.board_visibility = BoardVisibility.Private
+ with patch.object(mock_invoker.services.boards, "get_dto", return_value=fake_board):
+ response = client.request(
+ "DELETE",
+ "/api/v1/videos/board",
+ json={"video_name": "stranded.mp4"},
+ headers={"Authorization": f"Bearer {user1_token}"},
+ )
+ assert response.status_code == status.HTTP_200_OK
+ mock_invoker.services.board_video_records.remove_video_from_board.assert_called_once_with(video_name="stranded.mp4")
+
+
+def test_remove_video_from_board_rejects_third_party(
+ client: TestClient, mock_invoker: Invoker, user1_token: str, user2_token: str
+):
+ """A user who is neither the video owner nor a board write-access holder must be
+ rejected — the relaxed path is a stranded-contributor escape hatch, not an open door."""
+ from invokeai.app.services.board_records.board_records_common import BoardVisibility
+
+ admin = mock_invoker.services.users.get_by_email("admin@test.com")
+ user1 = mock_invoker.services.users.get_by_email("user1@test.com")
+ assert admin is not None and user1 is not None
+
+ # Video is owned by admin; board is owned by user1 and is Private.
+ mock_invoker.services.video_records.get_user_id.return_value = admin.user_id
+ mock_invoker.services.board_video_records.get_board_for_video.return_value = "user1-board"
+
+ fake_board = MagicMock()
+ fake_board.user_id = user1.user_id
+ fake_board.board_visibility = BoardVisibility.Private
+
+ # user2 has no claim to either resource.
+ with patch.object(mock_invoker.services.boards, "get_dto", return_value=fake_board):
+ response = client.request(
+ "DELETE",
+ "/api/v1/videos/board",
+ json={"video_name": "not_mine.mp4"},
+ headers={"Authorization": f"Bearer {user2_token}"},
+ )
+ assert response.status_code == status.HTTP_403_FORBIDDEN
+ mock_invoker.services.board_video_records.remove_video_from_board.assert_not_called()
+
+
+# ---------------------------------------------------------------------------
+# GET /videos/i/{video_name}/workflow (PR #9163 review fix — generated video
+# workflows were persisted but had no retrieval endpoint)
+# ---------------------------------------------------------------------------
+
+
+def test_get_video_workflow_returns_workflow_and_graph_for_owner(
+ client: TestClient, mock_invoker: Invoker, user1_token: str
+):
+ user1 = mock_invoker.services.users.get_by_email("user1@test.com")
+ assert user1 is not None
+ mock_invoker.services.video_records.get_user_id.return_value = user1.user_id
+ mock_invoker.services.videos.get_workflow.return_value = '{"nodes": []}'
+ mock_invoker.services.videos.get_graph.return_value = '{"edges": []}'
+
+ response = client.get(
+ "/api/v1/videos/i/mine_a.mp4/workflow",
+ headers={"Authorization": f"Bearer {user1_token}"},
+ )
+
+ assert response.status_code == status.HTTP_200_OK
+ assert response.json() == {"workflow": '{"nodes": []}', "graph": '{"edges": []}'}
+
+
+def test_get_video_workflow_returns_nulls_when_video_has_none(
+ client: TestClient, mock_invoker: Invoker, user1_token: str
+):
+ user1 = mock_invoker.services.users.get_by_email("user1@test.com")
+ assert user1 is not None
+ mock_invoker.services.video_records.get_user_id.return_value = user1.user_id
+ mock_invoker.services.videos.get_workflow.return_value = None
+ mock_invoker.services.videos.get_graph.return_value = None
+
+ response = client.get(
+ "/api/v1/videos/i/mine_a.mp4/workflow",
+ headers={"Authorization": f"Bearer {user1_token}"},
+ )
+
+ assert response.status_code == status.HTTP_200_OK
+ assert response.json() == {"workflow": None, "graph": None}
+
+
+def test_get_video_workflow_missing_video_returns_404(client: TestClient, mock_invoker: Invoker, user1_token: str):
+ user1 = mock_invoker.services.users.get_by_email("user1@test.com")
+ assert user1 is not None
+ mock_invoker.services.video_records.get_user_id.return_value = user1.user_id
+ mock_invoker.services.videos.get_workflow.side_effect = Exception("video file not found")
+
+ response = client.get(
+ "/api/v1/videos/i/mine_a.mp4/workflow",
+ headers={"Authorization": f"Bearer {user1_token}"},
+ )
+
+ assert response.status_code == status.HTTP_404_NOT_FOUND
+
+
+def test_get_video_workflow_forbidden_for_foreign_private_video(
+ client: TestClient, mock_invoker: Invoker, user2_token: str
+):
+ mock_invoker.services.video_records.get_user_id.return_value = "other-user-id"
+ mock_invoker.services.board_video_records.get_board_for_video.return_value = None
+
+ response = client.get(
+ "/api/v1/videos/i/not_mine.mp4/workflow",
+ headers={"Authorization": f"Bearer {user2_token}"},
+ )
+
+ assert response.status_code == status.HTTP_403_FORBIDDEN
+ mock_invoker.services.videos.get_workflow.assert_not_called()
+
+
+def test_get_video_workflow_requires_auth(enable_multiuser_for_videos: Any, client: TestClient):
+ response = client.get("/api/v1/videos/i/a.mp4/workflow")
+ assert response.status_code == status.HTTP_401_UNAUTHORIZED
+
+
+# ---------------------------------------------------------------------------
+# DELETE /videos/uncategorized (PR #9163 review fix — "Delete All Uncategorized
+# Images/Videos" previously deleted only images)
+# ---------------------------------------------------------------------------
+
+
+def test_delete_uncategorized_videos_deletes_only_owned(client: TestClient, mock_invoker: Invoker, user1_token: str):
+ user1 = mock_invoker.services.users.get_by_email("user1@test.com")
+ assert user1 is not None
+ _setup_mixed_ownership_batch(mock_invoker, user1.user_id)
+
+ names_result = MagicMock()
+ names_result.video_names = ["mine_a.mp4", "foreign.mp4", "mine_b.mp4"]
+ mock_invoker.services.videos.get_video_names.return_value = names_result
+
+ response = client.delete(
+ "/api/v1/videos/uncategorized",
+ headers={"Authorization": f"Bearer {user1_token}"},
+ )
+
+ assert response.status_code == status.HTTP_200_OK
+ body = response.json()
+ assert set(body["deleted_videos"]) == {"mine_a.mp4", "mine_b.mp4"}
+ # The foreign item is an intentional skip, not a failure — matching the images endpoint.
+ assert body["failed_videos"] == []
+ # The service must be scoped to the caller's uncategorized bucket.
+ assert mock_invoker.services.videos.get_video_names.call_args.kwargs["board_id"] == "none"
+ assert mock_invoker.services.videos.get_video_names.call_args.kwargs["user_id"] == user1.user_id
+ delete_calls = {call.args[0] for call in mock_invoker.services.videos.delete.call_args_list}
+ assert delete_calls == {"mine_a.mp4", "mine_b.mp4"}
+
+
+def test_delete_uncategorized_videos_requires_auth(enable_multiuser_for_videos: Any, client: TestClient):
+ response = client.delete("/api/v1/videos/uncategorized")
+ assert response.status_code == status.HTTP_401_UNAUTHORIZED
diff --git a/tests/app/routers/test_virtual_boards.py b/tests/app/routers/test_virtual_boards.py
index 42ae0be778b..f5468dcc802 100644
--- a/tests/app/routers/test_virtual_boards.py
+++ b/tests/app/routers/test_virtual_boards.py
@@ -25,6 +25,21 @@ def _save_image(mock_invoker: Invoker, image_name: str, user_id: str) -> None:
)
+def _save_video(mock_invoker: Invoker, video_name: str, user_id: str) -> None:
+ mock_invoker.services.video_records.save(
+ video_name=video_name,
+ video_origin=ResourceOrigin.INTERNAL,
+ video_category=ImageCategory.GENERAL,
+ width=10,
+ height=10,
+ duration=1.0,
+ fps=8.0,
+ has_workflow=False,
+ is_intermediate=False,
+ user_id=user_id,
+ )
+
+
def test_list_by_date_requires_auth(enable_multiuser: Any, client: TestClient):
r = client.get("/api/v1/virtual_boards/by_date")
assert r.status_code == status.HTTP_401_UNAUTHORIZED
@@ -79,3 +94,54 @@ def test_admin_sees_all_dates(
assert r.status_code == status.HTTP_200_OK
total = sum(b.get("image_count", 0) for b in r.json())
assert total >= 2 # admin sees images from both users
+
+
+def test_item_names_by_date_requires_auth(enable_multiuser: Any, client: TestClient):
+ r = client.get("/api/v1/virtual_boards/by_date/2026-05-18/item_names")
+ assert r.status_code == status.HTTP_401_UNAUTHORIZED
+
+
+def test_video_only_date_appears_as_virtual_board(client: TestClient, user1_token: str, mock_invoker: Invoker):
+ """A date containing only videos must still surface as a virtual board (video_count > 0)."""
+ user1 = mock_invoker.services.users.get_by_email("user1@test.com")
+ assert user1 is not None
+
+ _save_video(mock_invoker, "u1-vid-only.mp4", user1.user_id)
+
+ r = client.get(
+ "/api/v1/virtual_boards/by_date",
+ headers={"Authorization": f"Bearer {user1_token}"},
+ )
+ assert r.status_code == status.HTTP_200_OK
+ boards = r.json()
+ assert len(boards) == 1
+ assert boards[0]["image_count"] == 0
+ assert boards[0]["video_count"] == 1
+ assert boards[0]["cover_video_name"] == "u1-vid-only.mp4"
+ assert boards[0]["cover_image_name"] is None
+
+
+def test_item_names_by_date_returns_video_refs(client: TestClient, user1_token: str, mock_invoker: Invoker):
+ """Selecting a virtual date must return polymorphic refs — videos included, per-user filtered."""
+ user1 = mock_invoker.services.users.get_by_email("user1@test.com")
+ assert user1 is not None
+
+ _save_image(mock_invoker, "u1-mixed.png", user1.user_id)
+ _save_video(mock_invoker, "u1-mixed.mp4", user1.user_id)
+
+ r = client.get(
+ "/api/v1/virtual_boards/by_date",
+ headers={"Authorization": f"Bearer {user1_token}"},
+ )
+ assert r.status_code == status.HTTP_200_OK
+ date = r.json()[0]["date"]
+
+ r = client.get(
+ f"/api/v1/virtual_boards/by_date/{date}/item_names",
+ headers={"Authorization": f"Bearer {user1_token}"},
+ )
+ assert r.status_code == status.HTTP_200_OK
+ body = r.json()
+ refs = {(item["kind"], item["name"]) for item in body["items"]}
+ assert refs == {("image", "u1-mixed.png"), ("video", "u1-mixed.mp4")}
+ assert body["total_count"] == 2
diff --git a/tests/app/routers/test_workflows_multiuser.py b/tests/app/routers/test_workflows_multiuser.py
index 10c9fde17e7..c6492c70dcb 100644
--- a/tests/app/routers/test_workflows_multiuser.py
+++ b/tests/app/routers/test_workflows_multiuser.py
@@ -59,6 +59,7 @@ def client():
def mock_services() -> InvocationServices:
from invokeai.app.services.board_image_records.board_image_records_sqlite import SqliteBoardImageRecordStorage
from invokeai.app.services.board_records.board_records_sqlite import SqliteBoardRecordStorage
+ from invokeai.app.services.board_video_records.board_video_records_sqlite import SqliteBoardVideoRecordStorage
from invokeai.app.services.boards.boards_default import BoardService
from invokeai.app.services.bulk_download.bulk_download_default import BulkDownloadService
from invokeai.app.services.client_state_persistence.client_state_persistence_sqlite import (
@@ -69,6 +70,7 @@ def mock_services() -> InvocationServices:
from invokeai.app.services.invocation_cache.invocation_cache_memory import MemoryInvocationCache
from invokeai.app.services.invocation_stats.invocation_stats_default import InvocationStatsService
from invokeai.app.services.users.users_default import UserService
+ from invokeai.app.services.video_records.video_records_sqlite import SqliteVideoRecordStorage
from tests.test_nodes import TestEventService
configuration = InvokeAIAppConfig(use_memory_db=True, node_cache_size=0)
@@ -107,6 +109,11 @@ def mock_services() -> InvocationServices:
client_state_persistence=ClientStatePersistenceSqlite(db=db),
users=UserService(db),
external_generation=None, # type: ignore
+ videos=None, # type: ignore
+ video_files=None, # type: ignore
+ video_records=SqliteVideoRecordStorage(db=db),
+ board_video_records=SqliteBoardVideoRecordStorage(db=db),
+ gallery=None, # type: ignore
)
diff --git a/tests/app/services/gallery/test_gallery_default.py b/tests/app/services/gallery/test_gallery_default.py
new file mode 100644
index 00000000000..cf088714fa9
--- /dev/null
+++ b/tests/app/services/gallery/test_gallery_default.py
@@ -0,0 +1,274 @@
+"""Regression tests for SqliteGalleryService multiuser isolation and date-based
+virtual boards.
+
+Covers JPPhoto's code-review findings (PR #9163):
+
+1. The gallery /items/ and /items/names endpoints returned every user's items
+ when ``board_id`` was omitted, because ``_build_half`` only applied a user
+ filter for the explicit "none" sentinel. The fix added an ``elif user_id is
+ not None and not is_admin`` branch; these tests pin the behaviour for both
+ halves of the polymorphic union.
+
+2. Date-based virtual boards were image-only: video-only dates did not appear
+ at all, and mixed dates omitted videos from counts/contents/covers. The
+ gallery service now owns ``get_dates`` and a ``created_date`` filter on
+ ``list_item_names`` so virtual boards cover both kinds.
+"""
+
+import pytest
+
+from invokeai.app.services.config.config_default import InvokeAIAppConfig
+from invokeai.app.services.gallery.gallery_common import GalleryItemKind
+from invokeai.app.services.gallery.gallery_default import SqliteGalleryService
+from invokeai.app.services.image_records.image_records_common import ImageCategory, ResourceOrigin
+from invokeai.app.services.image_records.image_records_sqlite import SqliteImageRecordStorage
+from invokeai.app.services.video_records.video_records_sqlite import SqliteVideoRecordStorage
+from invokeai.backend.util.logging import InvokeAILogger
+from tests.fixtures.sqlite_database import create_mock_sqlite_database
+
+
+@pytest.fixture
+def services():
+ config = InvokeAIAppConfig(use_memory_db=True)
+ logger = InvokeAILogger.get_logger(config=config)
+ db = create_mock_sqlite_database(config, logger)
+ return {
+ "gallery": SqliteGalleryService(db=db),
+ "images": SqliteImageRecordStorage(db=db),
+ "videos": SqliteVideoRecordStorage(db=db),
+ }
+
+
+def _save_image(store: SqliteImageRecordStorage, name: str, user_id: str) -> None:
+ store.save(
+ image_name=name,
+ image_origin=ResourceOrigin.INTERNAL,
+ image_category=ImageCategory.GENERAL,
+ width=64,
+ height=64,
+ has_workflow=False,
+ is_intermediate=False,
+ user_id=user_id,
+ )
+
+
+def _save_video(store: SqliteVideoRecordStorage, name: str, user_id: str) -> None:
+ store.save(
+ video_name=name,
+ video_origin=ResourceOrigin.INTERNAL,
+ video_category=ImageCategory.GENERAL,
+ width=64,
+ height=64,
+ duration=1.0,
+ fps=8.0,
+ has_workflow=False,
+ is_intermediate=False,
+ user_id=user_id,
+ )
+
+
+@pytest.fixture
+def seeded(services):
+ # Mixed-kind items for two users, no board association — which is the path that
+ # previously bypassed user filtering entirely.
+ _save_image(services["images"], "alice.png", user_id="alice")
+ _save_video(services["videos"], "alice.mp4", user_id="alice")
+ _save_image(services["images"], "bob.png", user_id="bob")
+ _save_video(services["videos"], "bob.mp4", user_id="bob")
+ return services
+
+
+class TestListItemNamesOmittedBoardIdMultiuser:
+ def test_non_admin_only_sees_own_items(self, seeded) -> None:
+ result = seeded["gallery"].list_item_names(user_id="alice", is_admin=False)
+ names = {(item.kind, item.name) for item in result.items}
+ assert names == {
+ (GalleryItemKind.IMAGE, "alice.png"),
+ (GalleryItemKind.VIDEO, "alice.mp4"),
+ }
+ assert result.total_count == 2
+
+ def test_admin_sees_all_items(self, seeded) -> None:
+ result = seeded["gallery"].list_item_names(user_id="alice", is_admin=True)
+ names = {(item.kind, item.name) for item in result.items}
+ assert names == {
+ (GalleryItemKind.IMAGE, "alice.png"),
+ (GalleryItemKind.IMAGE, "bob.png"),
+ (GalleryItemKind.VIDEO, "alice.mp4"),
+ (GalleryItemKind.VIDEO, "bob.mp4"),
+ }
+ assert result.total_count == 4
+
+ def test_explicit_none_board_still_isolates(self, seeded) -> None:
+ # Before the fix this branch was correct; included here as a guard against
+ # accidental regression in the still-functioning code path.
+ result = seeded["gallery"].list_item_names(board_id="none", user_id="alice", is_admin=False)
+ names = {(item.kind, item.name) for item in result.items}
+ assert names == {
+ (GalleryItemKind.IMAGE, "alice.png"),
+ (GalleryItemKind.VIDEO, "alice.mp4"),
+ }
+
+
+def _backdate(services, table: str, name_col: str, name: str, created_at: str) -> None:
+ """Rewrites created_at so tests can build multi-date galleries (save() always stamps now)."""
+ db = services["images"]._db
+ with db.transaction() as cursor:
+ cursor.execute(f"UPDATE {table} SET created_at = ? WHERE {name_col} = ?", (created_at, name))
+
+
+class TestGetDatesPolymorphic:
+ def test_video_only_date_appears(self, services) -> None:
+ # A date with videos and no images must still produce a virtual board — with the
+ # video as its cover, since there is no image to fall back to.
+ _save_video(services["videos"], "only.mp4", user_id="alice")
+ _backdate(services, "videos", "video_name", "only.mp4", "2026-01-02 10:00:00")
+
+ boards = services["gallery"].get_dates(user_id="alice", is_admin=False)
+
+ assert len(boards) == 1
+ board = boards[0]
+ assert board.date == "2026-01-02"
+ assert board.image_count == 0
+ assert board.asset_count == 0
+ assert board.video_count == 1
+ assert board.cover_image_name is None
+ assert board.cover_video_name == "only.mp4"
+
+ def test_mixed_date_counts_both_kinds(self, services) -> None:
+ _save_image(services["images"], "day1.png", user_id="alice")
+ _save_video(services["videos"], "day1.mp4", user_id="alice")
+ _save_video(services["videos"], "day1b.mp4", user_id="alice")
+ _backdate(services, "images", "image_name", "day1.png", "2026-01-03 09:00:00")
+ _backdate(services, "videos", "video_name", "day1.mp4", "2026-01-03 10:00:00")
+ _backdate(services, "videos", "video_name", "day1b.mp4", "2026-01-03 11:00:00")
+
+ boards = services["gallery"].get_dates(user_id="alice", is_admin=False)
+
+ assert len(boards) == 1
+ board = boards[0]
+ assert board.date == "2026-01-03"
+ assert board.image_count == 1
+ assert board.video_count == 2
+ # The newest item of the date is a video, so the cover is the video.
+ assert board.cover_video_name == "day1b.mp4"
+ assert board.cover_image_name is None
+
+ def test_newest_image_wins_cover(self, services) -> None:
+ _save_video(services["videos"], "old.mp4", user_id="alice")
+ _save_image(services["images"], "new.png", user_id="alice")
+ _backdate(services, "videos", "video_name", "old.mp4", "2026-01-04 09:00:00")
+ _backdate(services, "images", "image_name", "new.png", "2026-01-04 10:00:00")
+
+ boards = services["gallery"].get_dates(user_id="alice", is_admin=False)
+
+ assert len(boards) == 1
+ assert boards[0].cover_image_name == "new.png"
+ assert boards[0].cover_video_name is None
+
+ def test_dates_are_user_isolated(self, seeded) -> None:
+ boards = seeded["gallery"].get_dates(user_id="alice", is_admin=False)
+ # alice has one image + one video, both created today.
+ assert len(boards) == 1
+ assert boards[0].image_count == 1
+ assert boards[0].video_count == 1
+
+ def test_admin_sees_all_dates(self, seeded) -> None:
+ boards = seeded["gallery"].get_dates(user_id="alice", is_admin=True)
+ assert len(boards) == 1
+ assert boards[0].image_count == 2
+ assert boards[0].video_count == 2
+
+
+class TestListItemNamesByCreatedDate:
+ def test_returns_only_items_of_date_including_videos(self, services) -> None:
+ _save_image(services["images"], "target.png", user_id="alice")
+ _save_video(services["videos"], "target.mp4", user_id="alice")
+ _save_image(services["images"], "other.png", user_id="alice")
+ _backdate(services, "images", "image_name", "target.png", "2026-01-05 09:00:00")
+ _backdate(services, "videos", "video_name", "target.mp4", "2026-01-05 10:00:00")
+ _backdate(services, "images", "image_name", "other.png", "2026-01-06 09:00:00")
+
+ result = services["gallery"].list_item_names(user_id="alice", is_admin=False, created_date="2026-01-05")
+
+ names = {(item.kind, item.name) for item in result.items}
+ assert names == {
+ (GalleryItemKind.IMAGE, "target.png"),
+ (GalleryItemKind.VIDEO, "target.mp4"),
+ }
+ assert result.total_count == 2
+
+ def test_created_date_is_user_isolated(self, services) -> None:
+ _save_video(services["videos"], "alice-day.mp4", user_id="alice")
+ _save_video(services["videos"], "bob-day.mp4", user_id="bob")
+ _backdate(services, "videos", "video_name", "alice-day.mp4", "2026-01-07 09:00:00")
+ _backdate(services, "videos", "video_name", "bob-day.mp4", "2026-01-07 10:00:00")
+
+ result = services["gallery"].list_item_names(user_id="alice", is_admin=False, created_date="2026-01-07")
+
+ assert [(item.kind, item.name) for item in result.items] == [(GalleryItemKind.VIDEO, "alice-day.mp4")]
+
+
+class TestOrderingTieBreakers:
+ """PR #9163 review fix: ordering only by (starred, created_at) left images and videos
+ created within the same timestamp granularity with no defined relative order — rows
+ could reorder across refetches or shift between offset pages, and the virtual-board
+ cover could flicker between equally-new items."""
+
+ SAME_TS = "2026-01-05 12:00:00"
+
+ def _seed_same_timestamp(self, services) -> None:
+ _save_image(services["images"], "b.png", user_id="alice")
+ _save_image(services["images"], "a.png", user_id="alice")
+ _save_video(services["videos"], "b.mp4", user_id="alice")
+ _save_video(services["videos"], "a.mp4", user_id="alice")
+ for table, col, name in [
+ ("images", "image_name", "a.png"),
+ ("images", "image_name", "b.png"),
+ ("videos", "video_name", "a.mp4"),
+ ("videos", "video_name", "b.mp4"),
+ ]:
+ _backdate(services, table, col, name, self.SAME_TS)
+
+ def test_same_timestamp_order_is_deterministic(self, services) -> None:
+ self._seed_same_timestamp(services)
+ gallery = services["gallery"]
+
+ first = [(i.kind, i.name) for i in gallery.list_item_names(user_id="alice", is_admin=False).items]
+ for _ in range(5):
+ again = [(i.kind, i.name) for i in gallery.list_item_names(user_id="alice", is_admin=False).items]
+ assert again == first
+
+ # Descending: videos sort before images ('video' > 'image'), names descending.
+ assert first == [
+ (GalleryItemKind.VIDEO, "b.mp4"),
+ (GalleryItemKind.VIDEO, "a.mp4"),
+ (GalleryItemKind.IMAGE, "b.png"),
+ (GalleryItemKind.IMAGE, "a.png"),
+ ]
+
+ def test_ascending_is_mirror_of_descending(self, services) -> None:
+ from invokeai.app.services.shared.sqlite.sqlite_common import SQLiteDirection
+
+ self._seed_same_timestamp(services)
+ gallery = services["gallery"]
+
+ desc = [(i.kind, i.name) for i in gallery.list_item_names(user_id="alice", is_admin=False).items]
+ asc = [
+ (i.kind, i.name)
+ for i in gallery.list_item_names(user_id="alice", is_admin=False, order_dir=SQLiteDirection.Ascending).items
+ ]
+ assert asc == list(reversed(desc))
+
+ def test_same_timestamp_cover_is_deterministic(self, services) -> None:
+ self._seed_same_timestamp(services)
+ gallery = services["gallery"]
+
+ covers = set()
+ for _ in range(5):
+ boards = gallery.get_dates(user_id="alice", is_admin=False)
+ assert len(boards) == 1
+ covers.add((boards[0].cover_image_name, boards[0].cover_video_name))
+
+ # One stable choice across refetches: the kind/name-descending winner (b.mp4).
+ assert covers == {(None, "b.mp4")}
diff --git a/tests/app/services/image_files/test_image_files_disk.py b/tests/app/services/image_files/test_image_files_disk.py
index 9a819568bc0..75e59380912 100644
--- a/tests/app/services/image_files/test_image_files_disk.py
+++ b/tests/app/services/image_files/test_image_files_disk.py
@@ -8,6 +8,7 @@
from PIL import Image
from invokeai.app.services.image_files.image_files_disk import DiskImageFileStorage, _should_use_png_rle
+from invokeai.app.services.image_records.image_records_common import ImageRecordNotFoundException
from invokeai.app.util.thumbnails import get_thumbnail_name
@@ -252,3 +253,61 @@ def test_save_flat_and_subfolder_coexist(self, disk_storage: DiskImageFileStorag
assert flat_path.exists()
assert nested_path.exists()
assert flat_path.parent != nested_path.parent
+
+ def test_staged_delete_can_be_rolled_back(self, disk_storage: DiskImageFileStorage):
+ image_name = "rollback.png"
+ disk_storage.save(image=Image.new("RGB", (32, 32)), image_name=image_name)
+ image_path = disk_storage.get_path(image_name)
+ thumbnail_path = disk_storage.get_path(image_name, thumbnail=True)
+
+ token = disk_storage.stage_delete(image_name)
+
+ assert not image_path.exists()
+ assert not thumbnail_path.exists()
+
+ disk_storage.rollback_delete(token)
+
+ assert image_path.exists()
+ assert thumbnail_path.exists()
+
+ def test_staged_delete_can_be_committed(self, disk_storage: DiskImageFileStorage, tmp_path: Path):
+ image_name = "commit.png"
+ disk_storage.save(image=Image.new("RGB", (32, 32)), image_name=image_name)
+
+ token = disk_storage.stage_delete(image_name)
+ disk_storage.commit_delete(token)
+
+ assert not list(tmp_path.glob(".delete_*"))
+
+ def test_invalid_staged_delete_does_not_create_staging_directory(
+ self, disk_storage: DiskImageFileStorage, tmp_path: Path
+ ):
+ with pytest.raises(ValueError, match="Invalid image name"):
+ disk_storage.stage_delete("../invalid.png")
+
+ assert not list(tmp_path.glob(".delete_*"))
+
+ def test_startup_restores_staged_files_when_record_exists(self, disk_storage: DiskImageFileStorage):
+ image_name = "recover.png"
+ disk_storage.save(image=Image.new("RGB", (32, 32)), image_name=image_name)
+ image_path = disk_storage.get_path(image_name)
+ disk_storage.stage_delete(image_name)
+
+ invoker = MagicMock()
+ invoker.services.image_records.get.return_value = object()
+ restarted = DiskImageFileStorage(disk_storage.image_root)
+ restarted.start(invoker)
+
+ assert image_path.exists()
+
+ def test_startup_purges_staged_files_when_record_was_deleted(self, disk_storage: DiskImageFileStorage):
+ image_name = "purge.png"
+ disk_storage.save(image=Image.new("RGB", (32, 32)), image_name=image_name)
+ disk_storage.stage_delete(image_name)
+
+ invoker = MagicMock()
+ invoker.services.image_records.get.side_effect = ImageRecordNotFoundException
+ restarted = DiskImageFileStorage(disk_storage.image_root)
+ restarted.start(invoker)
+
+ assert not list(disk_storage.image_root.glob(".delete_*"))
diff --git a/tests/app/services/images/test_images_default.py b/tests/app/services/images/test_images_default.py
index 97832d8ed0b..c97916dd139 100644
--- a/tests/app/services/images/test_images_default.py
+++ b/tests/app/services/images/test_images_default.py
@@ -206,11 +206,9 @@ def test_delete_intermediates_forwards_subfolder(self, image_service: ImageServi
class TestDeleteImagesOnBoardContract:
- """Tests for the silent-failure behavior of delete_images_on_board."""
+ """A file-delete failure must preserve the corresponding database record."""
- def test_db_rows_deleted_even_when_file_delete_fails(self, image_service: ImageService):
- """Current behavior: DB rows are deleted even if file cleanup fails for some images.
- This test documents the contract so any change is intentional."""
+ def test_record_preserved_when_file_delete_fails(self, image_service: ImageService):
invoker = image_service._ImageService__invoker # type: ignore
invoker.services.board_image_records.get_all_board_image_names_for_board.return_value = [
"good.png",
@@ -222,13 +220,14 @@ def test_db_rows_deleted_even_when_file_delete_fails(self, image_service: ImageS
bad_record = _make_record(image_name="bad.png", image_subfolder="bad/path")
invoker.services.image_records.get.side_effect = [good_record, bad_record]
- # File delete succeeds for first, fails for second
- invoker.services.image_files.delete.side_effect = [None, Exception("disk error")]
+ # File staging succeeds for first, fails for second
+ invoker.services.image_files.stage_delete.side_effect = [object(), Exception("disk error")]
- image_service.delete_images_on_board("board-1")
+ deleted, failed = image_service.delete_images_on_board("board-1")
- # DB rows are still deleted for all images
- invoker.services.image_records.delete_many.assert_called_once_with(["good.png", "bad.png"])
+ invoker.services.image_records.delete_many.assert_called_once_with(["good.png"])
+ assert deleted == ["good.png"]
+ assert failed == ["bad.png"]
def test_file_cleanup_failure_does_not_raise(self, image_service: ImageService):
"""File cleanup errors are swallowed, not propagated."""
@@ -237,13 +236,13 @@ def test_file_cleanup_failure_does_not_raise(self, image_service: ImageService):
record = _make_record(image_name="img.png", image_subfolder="sub")
invoker.services.image_records.get.return_value = record
- invoker.services.image_files.delete.side_effect = Exception("permission denied")
+ invoker.services.image_files.stage_delete.side_effect = Exception("permission denied")
- # Should not raise
- image_service.delete_images_on_board("board-1")
+ deleted, failed = image_service.delete_images_on_board("board-1")
- # DB delete still happens
- invoker.services.image_records.delete_many.assert_called_once()
+ invoker.services.image_records.delete_many.assert_called_once_with([])
+ assert deleted == []
+ assert failed == ["img.png"]
def test_record_lookup_failure_does_not_block_others(self, image_service: ImageService):
"""If getting the record for one image fails, other images are still processed."""
@@ -256,9 +255,24 @@ def test_record_lookup_failure_does_not_block_others(self, image_service: ImageS
ok_record = _make_record(image_name="ok.png", image_subfolder="")
invoker.services.image_records.get.side_effect = [Exception("not found"), ok_record]
- image_service.delete_images_on_board("board-1")
+ deleted, failed = image_service.delete_images_on_board("board-1")
- # File delete was attempted for the second image only
- invoker.services.image_files.delete.assert_called_once_with("ok.png", image_subfolder="")
- # DB rows are still deleted for all
- invoker.services.image_records.delete_many.assert_called_once_with(["missing.png", "ok.png"])
+ # File staging was attempted for the second image only
+ invoker.services.image_files.stage_delete.assert_called_once_with("ok.png", image_subfolder="")
+ invoker.services.image_records.delete_many.assert_called_once_with(["ok.png"])
+ assert deleted == ["ok.png"]
+ assert failed == ["missing.png"]
+
+ def test_database_failure_restores_staged_files(self, image_service: ImageService):
+ invoker = image_service._ImageService__invoker # type: ignore
+ invoker.services.board_image_records.get_all_board_image_names_for_board.return_value = ["img.png"]
+ invoker.services.image_records.get.return_value = _make_record(image_name="img.png", image_subfolder="general")
+ token = object()
+ invoker.services.image_files.stage_delete.return_value = token
+ invoker.services.image_records.delete_many.side_effect = RuntimeError("database unavailable")
+
+ with pytest.raises(RuntimeError, match="database unavailable"):
+ image_service.delete_images_on_board("board-1")
+
+ invoker.services.image_files.rollback_delete.assert_called_once_with(token)
+ invoker.services.image_files.commit_delete.assert_not_called()
diff --git a/tests/app/services/shared/sqlite_migrator/migrations/test_migration_2026_07_01_add_videos_tables.py b/tests/app/services/shared/sqlite_migrator/migrations/test_migration_2026_07_01_add_videos_tables.py
new file mode 100644
index 00000000000..062d7ff836a
--- /dev/null
+++ b/tests/app/services/shared/sqlite_migrator/migrations/test_migration_2026_07_01_add_videos_tables.py
@@ -0,0 +1,90 @@
+"""Tests for migration 2026_07_01_add_videos_tables: add videos and board_videos tables."""
+
+import sqlite3
+
+import pytest
+
+from invokeai.app.services.shared.sqlite_migrator.migrations.migration_2026_07_01_add_videos_tables import (
+ AddVideosTablesCallback,
+ build_migration,
+)
+
+
+def _create_boards_table(conn: sqlite3.Connection) -> None:
+ conn.execute(
+ """
+ CREATE TABLE boards (
+ board_id TEXT NOT NULL PRIMARY KEY,
+ board_name TEXT NOT NULL
+ );
+ """
+ )
+
+
+def _get_table_names(conn: sqlite3.Connection) -> set[str]:
+ return {row[0] for row in conn.execute("SELECT name FROM sqlite_master WHERE type='table'").fetchall()}
+
+
+@pytest.fixture
+def db() -> sqlite3.Connection:
+ conn = sqlite3.connect(":memory:")
+ conn.execute("PRAGMA foreign_keys = ON;")
+ _create_boards_table(conn)
+ return conn
+
+
+class TestAddVideosTables:
+ def test_creates_videos_and_board_videos_tables(self, db: sqlite3.Connection):
+ AddVideosTablesCallback()(db.cursor())
+ db.commit()
+
+ assert {"videos", "board_videos"} <= _get_table_names(db)
+
+ def test_videos_updated_at_trigger(self, db: sqlite3.Connection):
+ AddVideosTablesCallback()(db.cursor())
+ db.commit()
+
+ db.execute(
+ "INSERT INTO videos (video_name, video_origin, video_category, width, height)"
+ " VALUES ('v1', 'internal', 'general', 640, 480)"
+ )
+ before = db.execute("SELECT updated_at FROM videos WHERE video_name='v1'").fetchone()[0]
+ db.execute("UPDATE videos SET starred = TRUE WHERE video_name='v1'")
+ after = db.execute("SELECT updated_at FROM videos WHERE video_name='v1'").fetchone()[0]
+ assert after >= before
+
+ def test_board_delete_cascades_to_board_videos(self, db: sqlite3.Connection):
+ AddVideosTablesCallback()(db.cursor())
+ db.commit()
+
+ db.execute("INSERT INTO boards (board_id, board_name) VALUES ('b1', 'Board 1')")
+ db.execute(
+ "INSERT INTO videos (video_name, video_origin, video_category, width, height)"
+ " VALUES ('v1', 'internal', 'general', 640, 480)"
+ )
+ db.execute("INSERT INTO board_videos (board_id, video_name) VALUES ('b1', 'v1')")
+ db.execute("DELETE FROM boards WHERE board_id='b1'")
+
+ assert db.execute("SELECT COUNT(*) FROM board_videos").fetchone()[0] == 0
+ # The video itself is not deleted, only its board association.
+ assert db.execute("SELECT COUNT(*) FROM videos").fetchone()[0] == 1
+
+ def test_idempotent_when_tables_exist(self, db: sqlite3.Connection):
+ cursor = db.cursor()
+ AddVideosTablesCallback()(cursor)
+ db.execute(
+ "INSERT INTO videos (video_name, video_origin, video_category, width, height)"
+ " VALUES ('v1', 'internal', 'general', 640, 480)"
+ )
+ AddVideosTablesCallback()(cursor)
+ db.commit()
+
+ # Re-running must not drop or recreate existing tables/data.
+ assert db.execute("SELECT COUNT(*) FROM videos").fetchone()[0] == 1
+
+ def test_builder_metadata(self):
+ migration = build_migration()
+ assert migration.id == "2026_07_01_add_videos_tables"
+ assert migration.depends_on == "migration_27"
+ assert migration.from_version is None
+ assert migration.to_version is None
diff --git a/tests/app/services/shared/test_invocation_context_images.py b/tests/app/services/shared/test_invocation_context_images.py
new file mode 100644
index 00000000000..db2d461b7e3
--- /dev/null
+++ b/tests/app/services/shared/test_invocation_context_images.py
@@ -0,0 +1,95 @@
+from unittest.mock import MagicMock
+
+import pytest
+
+from invokeai.app.services.board_records.board_records_common import BoardVisibility
+from invokeai.app.services.shared.invocation_context import ImagesInterface
+
+
+def _make_interface(visibility: BoardVisibility, owner_id: str = "owner") -> tuple[ImagesInterface, MagicMock]:
+ services = MagicMock()
+ services.configuration.multiuser = True
+ services.users.get.return_value = MagicMock(is_admin=False)
+ services.boards.get_dto.return_value = MagicMock(user_id=owner_id, board_visibility=visibility)
+ data = MagicMock()
+ data.queue_item.user_id = "queue-user"
+ data.queue_item.workflow = None
+ data.queue_item.session.graph = None
+ data.queue_item.session_id = "session"
+ data.invocation.is_intermediate = False
+ return ImagesInterface(services, data, MagicMock()), services
+
+
+def test_image_save_rejects_foreign_private_board() -> None:
+ images, services = _make_interface(BoardVisibility.Private)
+
+ with pytest.raises(PermissionError, match="not authorized"):
+ images.save(MagicMock(), board_id="foreign-board")
+
+ services.images.create.assert_not_called()
+
+
+def test_image_save_allows_foreign_public_board() -> None:
+ images, services = _make_interface(BoardVisibility.Public)
+
+ images.save(MagicMock(), board_id="public-board")
+
+ services.images.create.assert_called_once()
+
+
+def test_image_save_allows_board_owner() -> None:
+ images, services = _make_interface(BoardVisibility.Private, owner_id="queue-user")
+
+ images.save(MagicMock(), board_id="owned-board")
+
+ services.images.create.assert_called_once()
+
+
+def test_image_save_allows_any_board_in_single_user_mode() -> None:
+ images, services = _make_interface(BoardVisibility.Private)
+ services.configuration.multiuser = False
+ services.users.get.return_value = None
+
+ images.save(MagicMock(), board_id="foreign-board")
+
+ services.images.create.assert_called_once()
+
+
+@pytest.mark.parametrize(
+ ("method_name", "service_method"),
+ [
+ ("get_pil", "get_pil_image"),
+ ("get_metadata", "get_metadata"),
+ ("get_dto", "get_dto"),
+ ("get_path", "get_path"),
+ ],
+)
+def test_image_reads_reject_foreign_private_image(method_name: str, service_method: str) -> None:
+ images, services = _make_interface(BoardVisibility.Private)
+ services.image_records.get_user_id.return_value = "owner"
+ services.board_image_records.get_board_for_image.return_value = "foreign-board"
+
+ with pytest.raises(PermissionError, match="not authorized"):
+ getattr(images, method_name)("private-image")
+
+ getattr(services.images, service_method).assert_not_called()
+
+
+def test_image_dto_allows_foreign_shared_image() -> None:
+ images, services = _make_interface(BoardVisibility.Shared)
+ services.image_records.get_user_id.return_value = "owner"
+ services.board_image_records.get_board_for_image.return_value = "shared-board"
+
+ images.get_dto("shared-image")
+
+ services.images.get_dto.assert_called_once_with("shared-image")
+
+
+def test_image_read_allows_any_image_in_single_user_mode() -> None:
+ images, services = _make_interface(BoardVisibility.Private)
+ services.configuration.multiuser = False
+ services.users.get.return_value = None
+
+ images.get_dto("foreign-image")
+
+ services.images.get_dto.assert_called_once_with("foreign-image")
diff --git a/tests/app/services/shared/test_invocation_context_videos.py b/tests/app/services/shared/test_invocation_context_videos.py
new file mode 100644
index 00000000000..1a3e0cca1b2
--- /dev/null
+++ b/tests/app/services/shared/test_invocation_context_videos.py
@@ -0,0 +1,67 @@
+from pathlib import Path
+from unittest.mock import MagicMock
+
+import pytest
+
+from invokeai.app.services.board_records.board_records_common import BoardVisibility
+from invokeai.app.services.shared.invocation_context import VideosInterface
+
+
+def _make_interface(visibility: BoardVisibility, owner_id: str = "owner") -> tuple[VideosInterface, MagicMock]:
+ services = MagicMock()
+ services.users.get.return_value = MagicMock(is_admin=False)
+ services.boards.get_dto.return_value = MagicMock(user_id=owner_id, board_visibility=visibility)
+ data = MagicMock()
+ data.queue_item.user_id = "queue-user"
+ data.queue_item.workflow = None
+ data.queue_item.session.graph = None
+ data.queue_item.session_id = "session"
+ data.invocation.is_intermediate = False
+ return VideosInterface(services, data, MagicMock()), services
+
+
+def test_video_save_rejects_foreign_private_board() -> None:
+ videos, services = _make_interface(BoardVisibility.Private)
+
+ with pytest.raises(PermissionError, match="not authorized"):
+ videos.save(Path("output.mp4"), width=64, height=64, duration=1.0, board_id="foreign-board")
+
+ services.videos.create.assert_not_called()
+
+
+@pytest.mark.parametrize("visibility", [BoardVisibility.Public])
+def test_video_save_allows_writable_foreign_board(visibility: BoardVisibility) -> None:
+ videos, services = _make_interface(visibility)
+
+ videos.save(Path("output.mp4"), width=64, height=64, duration=1.0, board_id="public-board")
+
+ services.videos.create.assert_called_once()
+
+
+def test_video_save_allows_board_owner() -> None:
+ videos, services = _make_interface(BoardVisibility.Private, owner_id="queue-user")
+
+ videos.save(Path("output.mp4"), width=64, height=64, duration=1.0, board_id="owned-board")
+
+ services.videos.create.assert_called_once()
+
+
+def test_video_path_rejects_foreign_private_video() -> None:
+ videos, services = _make_interface(BoardVisibility.Private)
+ services.video_records.get_user_id.return_value = "owner"
+ services.board_video_records.get_board_for_video.return_value = "foreign-board"
+
+ with pytest.raises(PermissionError, match="not authorized"):
+ videos.get_path("private.mp4")
+
+ services.videos.get_path.assert_not_called()
+
+
+def test_video_dto_allows_foreign_shared_video() -> None:
+ videos, services = _make_interface(BoardVisibility.Shared)
+ services.video_records.get_user_id.return_value = "owner"
+ services.board_video_records.get_board_for_video.return_value = "shared-board"
+
+ videos.get_dto("shared.mp4")
+
+ services.videos.get_dto.assert_called_once_with("shared.mp4")
diff --git a/tests/app/services/test_image_move_startup_safety.py b/tests/app/services/test_image_move_startup_safety.py
index f7f673322ea..e6b1380e5e8 100644
--- a/tests/app/services/test_image_move_startup_safety.py
+++ b/tests/app/services/test_image_move_startup_safety.py
@@ -38,6 +38,11 @@ def _services(**overrides):
"workflow_thumbnails": object(),
"client_state_persistence": object(),
"users": object(),
+ "videos": object(),
+ "video_files": object(),
+ "video_records": object(),
+ "board_video_records": object(),
+ "gallery": object(),
"image_moves": None,
}
services.update(overrides)
diff --git a/tests/app/services/video_files/test_video_files_disk.py b/tests/app/services/video_files/test_video_files_disk.py
new file mode 100644
index 00000000000..f793746b45e
--- /dev/null
+++ b/tests/app/services/video_files/test_video_files_disk.py
@@ -0,0 +1,179 @@
+"""Tests for DiskVideoFileStorage (video_files_disk.py).
+
+Covers the save-failure cleanup contract (JPPhoto PR #9163 follow-up): ``save()`` moves the
+source MP4 into permanent storage *before* writing the thumbnail and sidecar, so a failure in
+either of those later steps used to leave the moved MP4 (and any partial artifacts) on disk
+with no DB record through which they could be managed — the caller rolls the record back on
+``VideoFileSaveException`` but nothing removed the files.
+"""
+
+from pathlib import Path
+from unittest.mock import MagicMock
+
+import pytest
+
+from invokeai.app.services.video_files.video_files_common import VideoFileSaveException
+from invokeai.app.services.video_files.video_files_disk import DiskVideoFileStorage
+
+VIDEO_NAME = "abc123.mp4"
+
+
+@pytest.fixture
+def storage(tmp_path: Path) -> DiskVideoFileStorage:
+ return DiskVideoFileStorage(tmp_path / "videos")
+
+
+def _make_source(tmp_path: Path) -> Path:
+ # Not a decodable MP4 — thumbnail extraction fails gracefully (best-effort), which lets
+ # these tests drive the sidecar path without a real video file.
+ source = tmp_path / "source.mp4"
+ source.write_bytes(b"\x00\x00\x00\x18ftypmp42 not a real mp4")
+ return source
+
+
+def _all_files(root: Path) -> list[Path]:
+ return [p for p in root.rglob("*") if p.is_file()]
+
+
+def test_save_writes_video_and_sidecar(storage: DiskVideoFileStorage, tmp_path: Path):
+ source = _make_source(tmp_path)
+
+ storage.save(source_path=source, video_name=VIDEO_NAME, metadata='{"seed": 1}')
+
+ assert storage.get_path(VIDEO_NAME).exists()
+ assert not source.exists()
+ assert storage.get_workflow(VIDEO_NAME) is None # sidecar readable, workflow not set
+
+
+def test_save_failure_after_move_removes_all_destination_files(
+ storage: DiskVideoFileStorage, tmp_path: Path, monkeypatch: pytest.MonkeyPatch
+):
+ source = _make_source(tmp_path)
+
+ def broken_dump(*args, **kwargs):
+ raise OSError("disk full")
+
+ # Force the sidecar write (the last step of save) to fail after the MP4 has been moved.
+ monkeypatch.setattr("invokeai.app.services.video_files.video_files_disk.json.dump", broken_dump)
+
+ with pytest.raises(VideoFileSaveException):
+ storage.save(source_path=source, video_name=VIDEO_NAME, metadata='{"seed": 1}')
+
+ assert _all_files(tmp_path / "videos") == []
+
+
+def test_save_failure_in_thumbnail_write_removes_moved_video(
+ storage: DiskVideoFileStorage, tmp_path: Path, monkeypatch: pytest.MonkeyPatch
+):
+ source = _make_source(tmp_path)
+
+ # Frame extraction itself is best-effort, but a failure while *writing* the extracted
+ # thumbnail propagates. Simulate that: extraction succeeds, the write blows up.
+ monkeypatch.setattr(
+ "invokeai.app.services.video_files.video_files_disk.extract_video_frame",
+ lambda *args, **kwargs: MagicMock(),
+ )
+ broken_thumbnail = MagicMock()
+ broken_thumbnail.save.side_effect = OSError("read-only filesystem")
+ monkeypatch.setattr(
+ "invokeai.app.services.video_files.video_files_disk.make_thumbnail",
+ lambda *args, **kwargs: broken_thumbnail,
+ )
+
+ with pytest.raises(VideoFileSaveException):
+ storage.save(source_path=source, video_name=VIDEO_NAME)
+
+ assert _all_files(tmp_path / "videos") == []
+
+
+def test_save_failure_cleanup_covers_subfolders(
+ storage: DiskVideoFileStorage, tmp_path: Path, monkeypatch: pytest.MonkeyPatch
+):
+ source = _make_source(tmp_path)
+
+ def broken_dump(*args, **kwargs):
+ raise OSError("disk full")
+
+ monkeypatch.setattr("invokeai.app.services.video_files.video_files_disk.json.dump", broken_dump)
+
+ with pytest.raises(VideoFileSaveException):
+ storage.save(
+ source_path=source,
+ video_name=VIDEO_NAME,
+ video_subfolder="2026/07",
+ metadata='{"seed": 1}',
+ )
+
+ assert _all_files(tmp_path / "videos") == []
+
+
+def test_staged_delete_can_be_rolled_back(storage: DiskVideoFileStorage, tmp_path: Path):
+ source = _make_source(tmp_path)
+ storage.save(source_path=source, video_name=VIDEO_NAME, metadata='{"seed": 1}')
+ video_path = storage.get_path(VIDEO_NAME)
+
+ token = storage.stage_delete(VIDEO_NAME)
+ assert not video_path.exists()
+
+ storage.rollback_delete(token)
+ assert video_path.exists()
+ assert storage.get_workflow(VIDEO_NAME) is None
+
+
+def test_staged_delete_can_be_committed(storage: DiskVideoFileStorage, tmp_path: Path):
+ source = _make_source(tmp_path)
+ storage.save(source_path=source, video_name=VIDEO_NAME, metadata='{"seed": 1}')
+
+ token = storage.stage_delete(VIDEO_NAME)
+ storage.commit_delete(token)
+
+ assert _all_files(tmp_path / "videos") == []
+
+
+@pytest.mark.parametrize(
+ "video_name,video_subfolder",
+ [
+ ("../outside.mp4", ""),
+ (VIDEO_NAME, "../outside"),
+ ],
+)
+def test_invalid_staged_delete_does_not_create_staging_directory(
+ storage: DiskVideoFileStorage, tmp_path: Path, video_name: str, video_subfolder: str
+) -> None:
+ video_path = storage.get_path(VIDEO_NAME)
+ video_path.write_bytes(b"video")
+
+ with pytest.raises(ValueError):
+ storage.stage_delete(video_name, video_subfolder)
+
+ assert video_path.exists()
+ assert not list((tmp_path / "videos").glob(".delete_*"))
+
+
+def test_start_restores_staged_delete_when_record_still_exists(storage: DiskVideoFileStorage, tmp_path: Path):
+ source = _make_source(tmp_path)
+ storage.save(source_path=source, video_name=VIDEO_NAME, metadata='{"seed": 1}')
+ storage.stage_delete(VIDEO_NAME)
+ assert not storage.get_path(VIDEO_NAME).exists()
+ invoker = MagicMock()
+ invoker.services.video_records.get.return_value = MagicMock()
+
+ DiskVideoFileStorage(tmp_path / "videos").start(invoker)
+
+ assert storage.get_path(VIDEO_NAME).exists()
+ assert not list((tmp_path / "videos").glob(".delete_*"))
+
+
+def test_start_purges_staged_delete_when_record_is_gone(storage: DiskVideoFileStorage, tmp_path: Path):
+ from invokeai.app.services.video_records.video_records_common import VideoRecordNotFoundException
+
+ source = _make_source(tmp_path)
+ storage.save(source_path=source, video_name=VIDEO_NAME, metadata='{"seed": 1}')
+ storage.stage_delete(VIDEO_NAME)
+ invoker = MagicMock()
+ invoker.services.video_records.get.side_effect = VideoRecordNotFoundException
+
+ DiskVideoFileStorage(tmp_path / "videos").start(invoker)
+
+ assert _all_files(tmp_path / "videos") == []
+ assert not list((tmp_path / "videos").glob(".delete_*"))
diff --git a/tests/app/services/video_records/test_video_records_sqlite.py b/tests/app/services/video_records/test_video_records_sqlite.py
new file mode 100644
index 00000000000..70d21be87ec
--- /dev/null
+++ b/tests/app/services/video_records/test_video_records_sqlite.py
@@ -0,0 +1,209 @@
+"""Regression tests for SqliteVideoRecordStorage multiuser isolation.
+
+Covers JPPhoto's code-review finding (PR #9163): when ``board_id`` was omitted
+from /v1/videos/ and /v1/videos/names, the SQL builder applied no user filter
+and a non-admin caller saw every user's videos. The fix added an
+``elif user_id is not None and not is_admin`` branch; these tests pin the
+behaviour so the regression cannot reappear.
+"""
+
+import pytest
+
+from invokeai.app.services.board_records.board_records_sqlite import SqliteBoardRecordStorage
+from invokeai.app.services.config.config_default import InvokeAIAppConfig
+from invokeai.app.services.image_records.image_records_common import ImageCategory, ResourceOrigin
+from invokeai.app.services.shared.sqlite.sqlite_common import SQLiteDirection
+from invokeai.app.services.shared.sqlite.sqlite_database import SqliteDatabase
+from invokeai.app.services.users.users_common import UserCreateRequest
+from invokeai.app.services.users.users_default import UserService
+from invokeai.app.services.video_records.video_records_sqlite import SqliteVideoRecordStorage
+from invokeai.backend.util.logging import InvokeAILogger
+from tests.fixtures.sqlite_database import create_mock_sqlite_database
+
+
+@pytest.fixture
+def store() -> SqliteVideoRecordStorage:
+ config = InvokeAIAppConfig(use_memory_db=True)
+ logger = InvokeAILogger.get_logger(config=config)
+ db = create_mock_sqlite_database(config, logger)
+ return SqliteVideoRecordStorage(db=db)
+
+
+def _save(store: SqliteVideoRecordStorage, name: str, user_id: str) -> None:
+ store.save(
+ video_name=name,
+ video_origin=ResourceOrigin.INTERNAL,
+ video_category=ImageCategory.GENERAL,
+ width=64,
+ height=64,
+ duration=1.0,
+ fps=8.0,
+ has_workflow=False,
+ is_intermediate=False,
+ user_id=user_id,
+ )
+
+
+@pytest.fixture
+def seeded_store(store: SqliteVideoRecordStorage) -> SqliteVideoRecordStorage:
+ # Two videos per user; all without board association (the bug occurred when board_id
+ # was omitted from the query).
+ _save(store, "alice_1.mp4", user_id="alice")
+ _save(store, "alice_2.mp4", user_id="alice")
+ _save(store, "bob_1.mp4", user_id="bob")
+ _save(store, "bob_2.mp4", user_id="bob")
+ return store
+
+
+class TestGetManyOmittedBoardIdMultiuser:
+ """get_many() with board_id=None must filter by user_id for non-admin callers."""
+
+ def test_non_admin_only_sees_own_videos(self, seeded_store: SqliteVideoRecordStorage) -> None:
+ result = seeded_store.get_many(user_id="alice", is_admin=False)
+ names = {v.video_name for v in result.items}
+ assert names == {"alice_1.mp4", "alice_2.mp4"}
+ assert result.total == 2
+
+ def test_admin_sees_every_users_videos(self, seeded_store: SqliteVideoRecordStorage) -> None:
+ result = seeded_store.get_many(user_id="alice", is_admin=True)
+ names = {v.video_name for v in result.items}
+ assert names == {"alice_1.mp4", "alice_2.mp4", "bob_1.mp4", "bob_2.mp4"}
+
+ def test_no_user_id_returns_all(self, seeded_store: SqliteVideoRecordStorage) -> None:
+ # No user_id means the caller is bypassing user filtering entirely (e.g. internal calls).
+ result = seeded_store.get_many(user_id=None, is_admin=False)
+ names = {v.video_name for v in result.items}
+ assert names == {"alice_1.mp4", "alice_2.mp4", "bob_1.mp4", "bob_2.mp4"}
+
+
+class TestGetVideoNamesOmittedBoardIdMultiuser:
+ """get_video_names() with board_id=None must filter by user_id for non-admin callers."""
+
+ def test_non_admin_only_sees_own_videos(self, seeded_store: SqliteVideoRecordStorage) -> None:
+ result = seeded_store.get_video_names(user_id="alice", is_admin=False)
+ assert set(result.video_names) == {"alice_1.mp4", "alice_2.mp4"}
+ assert result.total_count == 2
+
+ def test_admin_sees_every_users_videos(self, seeded_store: SqliteVideoRecordStorage) -> None:
+ result = seeded_store.get_video_names(user_id="alice", is_admin=True)
+ assert set(result.video_names) == {"alice_1.mp4", "alice_2.mp4", "bob_1.mp4", "bob_2.mp4"}
+
+ def test_explicit_none_board_still_isolates(self, seeded_store: SqliteVideoRecordStorage) -> None:
+ # The "none" sentinel (uncategorized) must also apply the user filter — this was the
+ # only path that was correct *before* the fix; the test guards against accidental
+ # regression there too.
+ result = seeded_store.get_video_names(board_id="none", user_id="alice", is_admin=False)
+ assert set(result.video_names) == {"alice_1.mp4", "alice_2.mp4"}
+
+
+class TestDeterministicOrdering:
+ @staticmethod
+ def _set_same_timestamp(store: SqliteVideoRecordStorage) -> None:
+ with store._db.transaction() as cursor:
+ cursor.execute("UPDATE videos SET created_at = '2026-07-20 00:00:00.000'")
+
+ def test_same_timestamp_pages_have_stable_mirrored_order(self, seeded_store: SqliteVideoRecordStorage) -> None:
+ self._set_same_timestamp(seeded_store)
+
+ ascending = [
+ seeded_store.get_many(
+ offset=offset,
+ limit=1,
+ starred_first=False,
+ order_dir=SQLiteDirection.Ascending,
+ user_id="alice",
+ )
+ .items[0]
+ .video_name
+ for offset in range(2)
+ ]
+ descending = [
+ seeded_store.get_many(
+ offset=offset,
+ limit=1,
+ starred_first=False,
+ order_dir=SQLiteDirection.Descending,
+ user_id="alice",
+ )
+ .items[0]
+ .video_name
+ for offset in range(2)
+ ]
+
+ assert ascending == ["alice_1.mp4", "alice_2.mp4"]
+ assert descending == list(reversed(ascending))
+
+ def test_same_timestamp_name_list_has_stable_mirrored_order(self, seeded_store: SqliteVideoRecordStorage) -> None:
+ self._set_same_timestamp(seeded_store)
+
+ ascending = seeded_store.get_video_names(
+ starred_first=False,
+ order_dir=SQLiteDirection.Ascending,
+ user_id="alice",
+ ).video_names
+ descending = seeded_store.get_video_names(
+ starred_first=False,
+ order_dir=SQLiteDirection.Descending,
+ user_id="alice",
+ ).video_names
+
+ assert ascending == ["alice_1.mp4", "alice_2.mp4"]
+ assert descending == list(reversed(ascending))
+
+ def test_board_cover_uses_name_as_same_timestamp_tie_breaker(self, store: SqliteVideoRecordStorage) -> None:
+ board = SqliteBoardRecordStorage(store._db).save("Board", "system")
+ _save(store, "a.mp4", user_id="system")
+ _save(store, "b.mp4", user_id="system")
+ self._set_same_timestamp(store)
+ with store._db.transaction() as cursor:
+ cursor.executemany(
+ "INSERT INTO board_videos (board_id, video_name) VALUES (?, ?)",
+ [(board.board_id, "a.mp4"), (board.board_id, "b.mp4")],
+ )
+
+ assert store.get_most_recent_video_for_board(board.board_id).video_name == "b.mp4"
+
+
+class TestUserDeletionLifecycle:
+ """Documents the intended videos↔users lifecycle (JPPhoto PR #9163 July-10 follow-up).
+
+ ``videos.user_id`` deliberately has no FK to ``users`` — exactly like ``images``,
+ ``boards`` and ``workflows``, whose user_id columns (migration_27) are index-only.
+ Deleting a user therefore leaves their videos in place instead of cascading a row
+ delete that would strand the files on disk; the orphaned records stay visible to
+ administrators (and only to administrators), who can clean them up or reassign them.
+ These tests pin that platform-wide behavior for videos so any future change to the
+ user-deletion story is a deliberate decision rather than an accident.
+ """
+
+ @pytest.fixture
+ def migrated_db(self) -> SqliteDatabase:
+ config = InvokeAIAppConfig(use_memory_db=True)
+ logger = InvokeAILogger.get_logger(config=config)
+ return create_mock_sqlite_database(config, logger)
+
+ def test_videos_survive_owner_deletion_and_remain_admin_only(self, migrated_db: SqliteDatabase) -> None:
+ users = UserService(migrated_db)
+ store = SqliteVideoRecordStorage(db=migrated_db)
+
+ owner = users.create(
+ UserCreateRequest(
+ email="doomed@example.com",
+ display_name="Doomed User",
+ password="TestPassword123",
+ is_admin=False,
+ )
+ )
+ _save(store, "doomed.mp4", user_id=owner.user_id)
+
+ users.delete(owner.user_id)
+ assert users.get(owner.user_id) is None
+
+ # The record survives, still attributed to the deleted owner...
+ assert store.get_user_id("doomed.mp4") == owner.user_id
+ # ...is visible to admins for cleanup...
+ admin_view = store.get_many(user_id="some-admin", is_admin=True)
+ assert "doomed.mp4" in {v.video_name for v in admin_view.items}
+ # ...and no regular user inherits it.
+ other_view = store.get_many(user_id="bystander", is_admin=False)
+ assert "doomed.mp4" not in {v.video_name for v in other_view.items}
diff --git a/tests/app/services/videos/__init__.py b/tests/app/services/videos/__init__.py
new file mode 100644
index 00000000000..e69de29bb2d
diff --git a/tests/app/services/videos/test_videos_default.py b/tests/app/services/videos/test_videos_default.py
new file mode 100644
index 00000000000..6cef061522d
--- /dev/null
+++ b/tests/app/services/videos/test_videos_default.py
@@ -0,0 +1,286 @@
+"""Tests for VideoService (videos_default.py).
+
+Covers the board-cascade delete contract (JPPhoto PR #9163 follow-up). The old
+implementation silently swallowed per-file delete errors and then deleted every
+record anyway, which orphaned the file on disk while reporting success.
+"""
+
+from unittest.mock import MagicMock
+
+import pytest
+
+from invokeai.app.services.image_records.image_records_common import ImageCategory, ResourceOrigin
+from invokeai.app.services.video_records.video_records_common import VideoRecord
+from invokeai.app.services.videos.videos_default import VideoService
+from invokeai.app.util.misc import get_iso_timestamp
+
+
+def _make_record(video_name: str = "abc.mp4", video_subfolder: str = "") -> VideoRecord:
+ now = get_iso_timestamp()
+ return VideoRecord(
+ video_name=video_name,
+ video_origin=ResourceOrigin.INTERNAL,
+ video_category=ImageCategory.GENERAL,
+ width=64,
+ height=64,
+ duration=1.0,
+ fps=24.0,
+ created_at=now,
+ updated_at=now,
+ is_intermediate=False,
+ starred=False,
+ has_workflow=False,
+ video_subfolder=video_subfolder,
+ )
+
+
+@pytest.fixture
+def video_service() -> VideoService:
+ svc = VideoService()
+ invoker = MagicMock()
+ svc.start(invoker)
+ return svc
+
+
+class TestDeleteVideosOnBoardContract:
+ """Per JPPhoto's PR review: a file-delete failure must NOT result in the DB record being
+ deleted. Otherwise the API reports success while the file lingers on disk with no record
+ pointing at it, and the user has no way to discover or clean up the leak.
+ """
+
+ def test_record_preserved_when_file_delete_fails(self, video_service: VideoService):
+ invoker = video_service._VideoService__invoker # type: ignore[attr-defined]
+ invoker.services.board_video_records.get_all_board_video_names_for_board.return_value = [
+ "good.mp4",
+ "bad.mp4",
+ ]
+ invoker.services.video_records.get.side_effect = [
+ _make_record(video_name="good.mp4", video_subfolder="general"),
+ _make_record(video_name="bad.mp4", video_subfolder="general"),
+ ]
+ invoker.services.video_files.stage_delete.side_effect = [object(), Exception("disk error")]
+
+ deleted, failed = video_service.delete_videos_on_board("board-1")
+
+ # Only the video whose file we successfully removed should have its record deleted.
+ invoker.services.video_records.delete_many.assert_called_once_with(["good.mp4"])
+ # The method must also surface the truthful sets to the caller so the API response can
+ # avoid claiming the preserved record was deleted (JPPhoto PR #9163 May-22 follow-up)
+ # and can report the failure without racily diffing a board listing.
+ assert deleted == ["good.mp4"]
+ assert failed == ["bad.mp4"]
+
+ def test_file_cleanup_failure_does_not_raise(self, video_service: VideoService):
+ """A single file-delete failure must not surface as a 500 to the user — the rest of
+ the board deletion has to keep going so other videos and the board itself can still
+ be cleaned up."""
+ invoker = video_service._VideoService__invoker # type: ignore[attr-defined]
+ invoker.services.board_video_records.get_all_board_video_names_for_board.return_value = ["v.mp4"]
+ invoker.services.video_records.get.return_value = _make_record(video_name="v.mp4")
+ invoker.services.video_files.stage_delete.side_effect = Exception("permission denied")
+
+ # Should not raise
+ deleted, failed = video_service.delete_videos_on_board("board-1")
+
+ # And the failing video's record must be preserved.
+ invoker.services.video_records.delete_many.assert_called_once_with([])
+ assert deleted == []
+ assert failed == ["v.mp4"]
+
+ def test_all_records_deleted_on_full_success(self, video_service: VideoService):
+ invoker = video_service._VideoService__invoker # type: ignore[attr-defined]
+ invoker.services.board_video_records.get_all_board_video_names_for_board.return_value = [
+ "a.mp4",
+ "b.mp4",
+ ]
+ invoker.services.video_records.get.side_effect = [
+ _make_record(video_name="a.mp4"),
+ _make_record(video_name="b.mp4"),
+ ]
+ invoker.services.video_files.stage_delete.side_effect = [object(), object()]
+
+ deleted, failed = video_service.delete_videos_on_board("board-1")
+
+ invoker.services.video_records.delete_many.assert_called_once_with(["a.mp4", "b.mp4"])
+ assert deleted == ["a.mp4", "b.mp4"]
+ assert failed == []
+
+ def test_staging_cleanup_failure_is_deferred_after_records_are_deleted(self, video_service: VideoService):
+ invoker = video_service._VideoService__invoker # type: ignore[attr-defined]
+ invoker.services.board_video_records.get_all_board_video_names_for_board.return_value = ["v.mp4"]
+ invoker.services.video_records.get.return_value = _make_record(video_name="v.mp4")
+ invoker.services.video_files.stage_delete.return_value = object()
+ invoker.services.video_files.commit_delete.side_effect = OSError("staging directory busy")
+
+ deleted, failed = video_service.delete_videos_on_board("board-1")
+
+ assert deleted == ["v.mp4"]
+ assert failed == []
+ invoker.services.video_records.delete_many.assert_called_once_with(["v.mp4"])
+ invoker.services.logger.error.assert_called()
+
+ def test_all_staged_files_are_rolled_back_when_record_delete_fails(self, video_service: VideoService):
+ invoker = video_service._VideoService__invoker # type: ignore[attr-defined]
+ invoker.services.board_video_records.get_all_board_video_names_for_board.return_value = ["a.mp4", "b.mp4"]
+ invoker.services.video_records.get.side_effect = [_make_record("a.mp4"), _make_record("b.mp4")]
+ invoker.services.video_files.stage_delete.side_effect = ["token-a", "token-b"]
+ invoker.services.video_records.delete_many.side_effect = RuntimeError("database unavailable")
+ invoker.services.video_files.rollback_delete.side_effect = [OSError("first rollback failed"), None]
+
+ with pytest.raises(RuntimeError, match="database unavailable"):
+ video_service.delete_videos_on_board("board-1")
+
+ assert [call.args[0] for call in invoker.services.video_files.rollback_delete.call_args_list] == [
+ "token-a",
+ "token-b",
+ ]
+
+
+class TestDeleteAtomicity:
+ def test_single_delete_rolls_files_back_when_record_delete_fails(self, video_service: VideoService):
+ invoker = video_service._VideoService__invoker # type: ignore[attr-defined]
+ invoker.services.video_records.get.return_value = _make_record()
+ invoker.services.video_records.delete.side_effect = RuntimeError("database unavailable")
+
+ with pytest.raises(RuntimeError, match="database unavailable"):
+ video_service.delete("abc.mp4")
+
+ invoker.services.video_files.stage_delete.assert_called_once_with("abc.mp4", video_subfolder="")
+ invoker.services.video_files.rollback_delete.assert_called_once()
+ invoker.services.video_files.commit_delete.assert_not_called()
+
+
+class TestCreateRollback:
+ """Per JPPhoto's PR review (May 22 follow-up): if the video file save fails after the DB
+ record has been written, the create path must roll back the record (and any board
+ attachment) so the gallery never contains a DB-only ghost whose file endpoints 404.
+ """
+
+ def _wire_minimal_create_dependencies(self, invoker: MagicMock, video_name: str = "v.mp4") -> None:
+ invoker.services.names.create_video_name.return_value = video_name
+ invoker.services.configuration.image_subfolder_strategy = "flat"
+ invoker.services.logger = MagicMock()
+
+ def test_record_and_board_relation_rolled_back_when_file_save_fails(self, video_service: VideoService, tmp_path):
+ from invokeai.app.services.video_files.video_files_common import VideoFileSaveException
+
+ invoker = video_service._VideoService__invoker # type: ignore[attr-defined]
+ self._wire_minimal_create_dependencies(invoker, video_name="ghost.mp4")
+
+ # The DB record save succeeds; the board attach succeeds; the file save explodes.
+ invoker.services.video_records.save.return_value = None
+ invoker.services.board_video_records.add_video_to_board.return_value = None
+ invoker.services.video_files.save.side_effect = VideoFileSaveException("disk full")
+
+ with pytest.raises(VideoFileSaveException):
+ video_service.create(
+ source_path=tmp_path / "src.mp4",
+ width=64,
+ height=64,
+ duration=1.0,
+ fps=24.0,
+ video_origin=ResourceOrigin.EXTERNAL,
+ video_category=ImageCategory.GENERAL,
+ board_id="some-board",
+ )
+
+ # Both the DB record AND the board association must be unwound — otherwise the
+ # gallery would show a ghost video whose file endpoints 404.
+ invoker.services.board_video_records.remove_video_from_board.assert_called_once_with(video_name="ghost.mp4")
+ invoker.services.video_records.delete.assert_called_once_with("ghost.mp4")
+
+ def test_record_rolled_back_when_no_board_and_file_save_fails(self, video_service: VideoService, tmp_path):
+ from invokeai.app.services.video_files.video_files_common import VideoFileSaveException
+
+ invoker = video_service._VideoService__invoker # type: ignore[attr-defined]
+ self._wire_minimal_create_dependencies(invoker, video_name="solo.mp4")
+ invoker.services.video_records.save.return_value = None
+ invoker.services.video_files.save.side_effect = VideoFileSaveException("disk full")
+
+ with pytest.raises(VideoFileSaveException):
+ video_service.create(
+ source_path=tmp_path / "src.mp4",
+ width=64,
+ height=64,
+ duration=1.0,
+ fps=24.0,
+ video_origin=ResourceOrigin.EXTERNAL,
+ video_category=ImageCategory.GENERAL,
+ board_id=None,
+ )
+
+ # No board attachment was attempted, so no detach call should be made — but the
+ # record must still be rolled back.
+ invoker.services.board_video_records.remove_video_from_board.assert_not_called()
+ invoker.services.video_records.delete.assert_called_once_with("solo.mp4")
+
+ def test_files_rolled_back_when_failure_occurs_after_file_save(self, video_service: VideoService, tmp_path):
+ """The disk layer cleans up after its own save failures, but a failure *after* a
+ successful file save (e.g. building the DTO) must also unwind the files — otherwise
+ they'd sit on disk with no record pointing at them (JPPhoto PR #9163 July-10
+ follow-up)."""
+ invoker = video_service._VideoService__invoker # type: ignore[attr-defined]
+ self._wire_minimal_create_dependencies(invoker, video_name="late.mp4")
+ invoker.services.video_records.save.return_value = None
+ invoker.services.video_files.save.return_value = None
+ # get_dto reads the record back — make that step explode.
+ invoker.services.video_records.get.side_effect = RuntimeError("db went away")
+
+ with pytest.raises(RuntimeError):
+ video_service.create(
+ source_path=tmp_path / "src.mp4",
+ width=64,
+ height=64,
+ duration=1.0,
+ fps=24.0,
+ video_origin=ResourceOrigin.EXTERNAL,
+ video_category=ImageCategory.GENERAL,
+ board_id=None,
+ )
+
+ invoker.services.video_records.delete.assert_called_once_with("late.mp4")
+ invoker.services.video_files.delete.assert_called_once()
+ assert invoker.services.video_files.delete.call_args.args[0] == "late.mp4"
+
+
+class TestCreateBoardAttachFallback:
+ """Board attachment during create is best-effort, mirroring ImageService.create: a board
+ deleted between the caller's access check and the insert must not destroy a just-generated
+ video. The fallback must be explicit, not silent — the returned DTO reports the actual
+ (missing) board association and a warning is logged (JPPhoto PR #9163 July-10 follow-up).
+ """
+
+ def test_create_succeeds_with_explicit_fallback_when_board_attach_fails(
+ self, video_service: VideoService, tmp_path
+ ):
+ invoker = video_service._VideoService__invoker # type: ignore[attr-defined]
+ invoker.services.names.create_video_name.return_value = "orphan.mp4"
+ invoker.services.configuration.image_subfolder_strategy = "flat"
+ invoker.services.logger = MagicMock()
+
+ invoker.services.video_records.save.return_value = None
+ invoker.services.board_video_records.add_video_to_board.side_effect = Exception("board was deleted")
+ invoker.services.video_files.save.return_value = None
+ # get_dto reads the record and the *actual* board association back.
+ invoker.services.video_records.get.return_value = _make_record(video_name="orphan.mp4")
+ invoker.services.board_video_records.get_board_for_video.return_value = None
+ invoker.services.urls.get_video_url.return_value = "http://localhost/videos/orphan.mp4"
+
+ video_dto = video_service.create(
+ source_path=tmp_path / "src.mp4",
+ width=64,
+ height=64,
+ duration=1.0,
+ fps=24.0,
+ video_origin=ResourceOrigin.EXTERNAL,
+ video_category=ImageCategory.GENERAL,
+ board_id="deleted-board",
+ )
+
+ # The video survives, but the DTO must not claim the requested board attachment
+ # succeeded, and the fallback must be logged.
+ assert video_dto.board_id is None
+ invoker.services.logger.warning.assert_called_once()
+ assert "deleted-board" in invoker.services.logger.warning.call_args.args[0]
+ # Nothing was attached, so nothing should be unwound.
+ invoker.services.board_video_records.remove_video_from_board.assert_not_called()
diff --git a/tests/app/services/workflow_records/test_default_workflows_registry.py b/tests/app/services/workflow_records/test_default_workflows_registry.py
new file mode 100644
index 00000000000..93a4282904d
--- /dev/null
+++ b/tests/app/services/workflow_records/test_default_workflows_registry.py
@@ -0,0 +1,53 @@
+"""Verify the bundled Wan/video workflows agree with the invocation registry.
+
+The pre-existing default workflows (SD1.5/SDXL/FLUX...) carry stale node versions and
+even removed node types — the editor tolerates this with "node needs update" badges, so
+they are deliberately NOT checked here. The workflows this PR ships should not start
+life stale: every embedded node must exist and carry the invocation's current version,
+and every embedded input must be a real field on the invocation. This is exactly the
+check that would have caught the wan_ref_image_encoder 1.0.0/1.1.0 embeds shipped while
+the invocation was at 1.2.0.
+"""
+
+import json
+from pathlib import Path
+
+import pytest
+
+from invokeai.app.invocations.baseinvocation import InvocationRegistry
+from invokeai.app.services.shared.graph import * # noqa: F401 F403 -- imports all invocations, populating the registry
+
+WORKFLOW_DIR = Path("invokeai/app/services/workflow_records/default_workflows")
+WAN_WORKFLOWS = sorted(
+ {p for p in WORKFLOW_DIR.glob("*.json") if "Wan" in p.name or "Video" in p.name},
+ key=lambda p: p.name,
+)
+
+
+def test_wan_workflow_glob_finds_the_bundled_workflows() -> None:
+ # Guard against the glob silently matching nothing (e.g. after a rename).
+ assert len(WAN_WORKFLOWS) == 12
+
+
+@pytest.mark.parametrize("workflow_path", WAN_WORKFLOWS, ids=lambda p: p.stem)
+def test_bundled_workflow_nodes_match_invocation_registry(workflow_path: Path) -> None:
+ invocations = InvocationRegistry.get_invocations_map()
+ workflow = json.loads(workflow_path.read_text(encoding="utf-8"))
+
+ for node in workflow["nodes"]:
+ data = node["data"]
+ node_type = data["type"]
+ cls = invocations.get(node_type)
+ assert cls is not None, f"{workflow_path.name}: node type '{node_type}' is not a registered invocation"
+
+ current_version = cls.UIConfig.version
+ assert data["version"] == current_version, (
+ f"{workflow_path.name}: node '{node_type}' embeds version {data['version']} "
+ f"but the invocation is at {current_version} — update the bundled workflow"
+ )
+
+ field_names = set(cls.model_fields.keys())
+ for input_name in data.get("inputs", {}):
+ assert input_name in field_names, (
+ f"{workflow_path.name}: node '{node_type}' embeds unknown input '{input_name}'"
+ )
diff --git a/tests/app/util/test_video_encoding.py b/tests/app/util/test_video_encoding.py
new file mode 100644
index 00000000000..14ee177b39f
--- /dev/null
+++ b/tests/app/util/test_video_encoding.py
@@ -0,0 +1,46 @@
+"""Regression tests for make_mp4_writer (PR #9163 review).
+
+The bug: the production encoders relied on imageio's default ``macro_block_size=16``,
+which makes ffmpeg silently *rescale* frames to the next multiple of 16 — a 1920x1080
+upload trimmed by Frame Range from Video came back as 1920x1088 while the DTO recorded
+1080, so concatenating the trim with its own source failed the same-dimensions check.
+``make_mp4_writer`` pins ``macro_block_size=1`` so encoded dimensions always match the
+input frames exactly.
+"""
+
+from pathlib import Path
+
+import imageio.v2 as iio2
+import numpy as np
+import pytest
+
+from invokeai.app.invocations.video_frame_extract_range import _validate_even_dimensions
+from invokeai.app.util.video_encoding import make_mp4_writer
+
+
+def test_non_multiple_of_16_dimensions_are_preserved(tmp_path: Path) -> None:
+ # 120x84: even (so yuv420p-encodable) but not a multiple of 16 — the imageio
+ # default would silently rescale this to 128x96.
+ width, height = 120, 84
+ path = tmp_path / "out.mp4"
+ writer = make_mp4_writer(path, fps=8.0)
+ try:
+ for i in range(4):
+ writer.append_data(np.full((height, width, 3), i * 10, dtype=np.uint8))
+ finally:
+ writer.close()
+
+ reader = iio2.get_reader(str(path))
+ try:
+ first = reader.get_data(0)
+ finally:
+ reader.close()
+ assert first.shape[:2] == (height, width)
+
+
+def test_validate_even_dimensions_accepts_even_and_rejects_odd() -> None:
+ _validate_even_dimensions(1920, 1080, "ok.mp4")
+ with pytest.raises(ValueError, match="even dimensions"):
+ _validate_even_dimensions(833, 480, "odd-width.mp4")
+ with pytest.raises(ValueError, match="even dimensions"):
+ _validate_even_dimensions(832, 481, "odd-height.mp4")
diff --git a/tests/app/util/test_video_thumbnails.py b/tests/app/util/test_video_thumbnails.py
new file mode 100644
index 00000000000..f2d2cfdefc7
--- /dev/null
+++ b/tests/app/util/test_video_thumbnails.py
@@ -0,0 +1,479 @@
+"""Tests for the subprocess-bounded video decode helpers (PR #9163 review).
+
+The bug: ``probe_video`` / ``extract_video_frame`` decoded untrusted uploads in-process
+with no timeout, despite the module itself noting that cv2 has historically hung on some
+containers. A crafted MP4 that makes the imageio probe fail and then blocks inside
+``cv2.VideoCapture()`` would pin the FastAPI request worker that called it forever;
+repeated uploads could exhaust the worker pool. Decoding now runs in a killable child
+process with a hard timeout.
+
+The hang tests substitute a worker command that never returns and assert the helpers
+fail within a bounded interval; the happy-path tests run the real worker against a real
+synthetic MP4 so the subprocess plumbing is actually validated end to end.
+"""
+
+import io
+import subprocess
+import sys
+import time
+from pathlib import Path
+from threading import Event
+from unittest.mock import MagicMock
+
+import imageio.v3 as iio
+import numpy as np
+import pytest
+
+from invokeai.app.services.session_processor.session_processor_common import CanceledException
+from invokeai.app.util import video_decode_worker, video_thumbnails
+from invokeai.app.util.video_thumbnails import decoder_frame_count, extract_video_frame, iter_video_frames, probe_video
+
+FRAMES = 12
+FPS = 8.0
+
+
+@pytest.fixture
+def synthetic_mp4(tmp_path: Path) -> Path:
+ path = tmp_path / "synth.mp4"
+ frames = [np.full((32, 48, 3), 32 + i * 16, dtype=np.uint8) for i in range(FRAMES)]
+ iio.imwrite(path, frames, plugin="FFMPEG", codec="libx264", fps=FPS, macro_block_size=1)
+ return path
+
+
+@pytest.fixture
+def hanging_worker(monkeypatch: pytest.MonkeyPatch):
+ """Replaces the decode worker with a child process that sleeps forever."""
+
+ def _hang_command(*args: str) -> list[str]:
+ return [sys.executable, "-c", "import time; time.sleep(600)"]
+
+ monkeypatch.setattr(video_thumbnails, "_worker_command", _hang_command)
+
+
+class TestHappyPathThroughSubprocess:
+ def test_probe_returns_metadata(self, synthetic_mp4: Path) -> None:
+ width, height, duration, fps = probe_video(synthetic_mp4)
+ assert (width, height) == (48, 32)
+ assert fps == pytest.approx(FPS)
+ assert duration == pytest.approx(FRAMES / FPS, abs=0.5)
+
+ def test_extract_frame_returns_image(self, synthetic_mp4: Path) -> None:
+ frame = extract_video_frame(synthetic_mp4, frame_index=0)
+ assert frame is not None
+ assert frame.size == (48, 32)
+
+ def test_frame_count_matches(self, synthetic_mp4: Path) -> None:
+ assert decoder_frame_count(synthetic_mp4) == FRAMES
+
+
+class TestUnreadableInput:
+ def test_probe_rejects_garbage_bytes(self, tmp_path: Path) -> None:
+ bogus = tmp_path / "junk.mp4"
+ bogus.write_bytes(b"not actually an mp4")
+ with pytest.raises(FileNotFoundError):
+ probe_video(bogus)
+
+ def test_extract_frame_returns_none_for_garbage_bytes(self, tmp_path: Path) -> None:
+ bogus = tmp_path / "junk.mp4"
+ bogus.write_bytes(b"not actually an mp4")
+ assert extract_video_frame(bogus) is None
+
+
+class TestHungDecoderIsBounded:
+ """A decode backend that never returns must fail within the timeout, not hang the caller."""
+
+ TIMEOUT = 2.0
+ # Generous ceiling for CI scheduling jitter; the point is "seconds, not forever".
+ MAX_ELAPSED = 15.0
+
+ def test_probe_fails_within_bounded_interval(self, hanging_worker, tmp_path: Path) -> None:
+ target = tmp_path / "malicious.mp4"
+ target.write_bytes(b"pretend this hangs the decoder")
+ started = time.monotonic()
+ with pytest.raises(FileNotFoundError):
+ probe_video(target, timeout=self.TIMEOUT)
+ assert time.monotonic() - started < self.MAX_ELAPSED
+
+ def test_extract_frame_fails_within_bounded_interval(self, hanging_worker, tmp_path: Path) -> None:
+ target = tmp_path / "malicious.mp4"
+ target.write_bytes(b"pretend this hangs the decoder")
+ started = time.monotonic()
+ assert extract_video_frame(target, timeout=self.TIMEOUT) is None
+ assert time.monotonic() - started < self.MAX_ELAPSED
+
+ def test_frame_count_fails_within_bounded_interval(self, hanging_worker, tmp_path: Path) -> None:
+ target = tmp_path / "malicious.mp4"
+ target.write_bytes(b"pretend this hangs the decoder")
+ started = time.monotonic()
+ assert decoder_frame_count(target, timeout=self.TIMEOUT) is None
+ assert time.monotonic() - started < self.MAX_ELAPSED
+
+
+class TestStreamedDecoderIsBounded:
+ def test_streams_real_frames_through_worker(self, synthetic_mp4: Path) -> None:
+ frames = list(iter_video_frames(synthetic_mp4))
+ assert len(frames) == FRAMES
+ assert frames[0].shape == (32, 48, 3)
+
+ def test_consumer_time_does_not_count_as_decoder_inactivity(self, synthetic_mp4: Path) -> None:
+ frames = iter_video_frames(synthetic_mp4, timeout=2.0)
+ next(frames)
+ # Sleep longer than the inactivity timeout. This time belongs to the consumer and
+ # must not expire the decoder, while the two-second window avoids treating normal
+ # process/FFmpeg scheduling latency on macOS CI as a decoder hang.
+ time.sleep(2.2)
+ assert next(frames).shape == (32, 48, 3)
+
+ def test_times_out_when_worker_stops_producing_frames(self, hanging_worker, tmp_path: Path) -> None:
+ target = tmp_path / "malicious.mp4"
+ target.write_bytes(b"pretend this hangs the decoder")
+ started = time.monotonic()
+ with pytest.raises(TimeoutError, match="Timed out decoding"):
+ next(iter_video_frames(target, timeout=0.2))
+ assert time.monotonic() - started < 5
+
+ def test_cancellation_terminates_blocked_decoder(self, hanging_worker, tmp_path: Path) -> None:
+ target = tmp_path / "malicious.mp4"
+ target.write_bytes(b"pretend this hangs the decoder")
+ canceled = Event()
+ canceled.set()
+ with pytest.raises(CanceledException):
+ next(iter_video_frames(target, timeout=5, is_canceled=canceled.is_set))
+
+ def test_midstream_worker_failure_includes_stderr(self, monkeypatch: pytest.MonkeyPatch, tmp_path: Path) -> None:
+ target = tmp_path / "failed.mp4"
+ target.write_bytes(b"unused")
+ script = (
+ "import io, struct, sys, numpy as np; "
+ "record = io.BytesIO(); "
+ "np.save(record, np.zeros((2, 2, 3), dtype=np.uint8), allow_pickle=False); "
+ "payload = record.getvalue(); "
+ "sys.stdout.buffer.write(struct.pack('>Q', len(payload)) + payload); "
+ "sys.stdout.buffer.flush(); "
+ "print('decoder exploded', file=sys.stderr); "
+ "raise SystemExit(3)"
+ )
+ monkeypatch.setattr(video_thumbnails, "_worker_command", lambda *args: [sys.executable, "-c", script])
+
+ frames = iter_video_frames(target)
+ assert next(frames).shape == (2, 2, 3)
+ with pytest.raises(ValueError, match="decoder exploded"):
+ next(frames)
+
+ def test_worker_stderr_capture_is_bounded(self, monkeypatch: pytest.MonkeyPatch, tmp_path: Path) -> None:
+ target = tmp_path / "noisy.mp4"
+ target.write_bytes(b"unused")
+ script = "import sys; sys.stderr.write('x' * 100000); raise SystemExit(3)"
+ monkeypatch.setattr(video_thumbnails, "_worker_command", lambda *args: [sys.executable, "-c", script])
+
+ with pytest.raises(ValueError) as error:
+ next(iter_video_frames(target))
+
+ assert len(str(error.value)) < video_thumbnails.MAX_DECODE_STDERR_BYTES + 500
+
+ def test_closed_stream_from_live_worker_does_not_leak_timeout_expired(
+ self, monkeypatch: pytest.MonkeyPatch, tmp_path: Path
+ ) -> None:
+ target = tmp_path / "stalled.mp4"
+ target.write_bytes(b"unused")
+ proc = MagicMock()
+ proc.stdout = io.BytesIO()
+ proc.stderr = io.BytesIO()
+ proc.poll.return_value = None
+
+ def wait(timeout: float | None = None) -> int:
+ if timeout is not None:
+ raise subprocess.TimeoutExpired("decoder-worker", timeout)
+ return -9
+
+ proc.wait.side_effect = wait
+ monkeypatch.setattr(video_thumbnails, "_spawn_worker", lambda *args, **kwargs: proc)
+ monkeypatch.setattr(video_thumbnails, "_worker_tree_rss", lambda worker: 0)
+ monkeypatch.setattr(video_thumbnails, "_terminate_process_tree", lambda worker: None)
+
+ with pytest.raises(TimeoutError, match="decoder worker"):
+ next(iter_video_frames(target, timeout=0.2))
+
+
+def test_timeout_kills_worker_descendants(monkeypatch: pytest.MonkeyPatch, tmp_path: Path) -> None:
+ child_pid_path = tmp_path / "child.pid"
+
+ def _descendant_command(*args: str) -> list[str]:
+ script = (
+ "import pathlib, subprocess, sys, time; "
+ "child = subprocess.Popen([sys.executable, '-c', 'import time; time.sleep(600)']); "
+ "pathlib.Path(sys.argv[1]).write_text(str(child.pid)); "
+ "time.sleep(600)"
+ )
+ return [sys.executable, "-c", script, str(child_pid_path)]
+
+ monkeypatch.setattr(video_thumbnails, "_worker_command", _descendant_command)
+ # The timeout must comfortably cover interpreter startup + the child spawn on a
+ # loaded CI machine — if the worker is killed before it writes the pid file, the
+ # test can't observe the descendant and fails spuriously.
+ assert video_thumbnails._run_worker(["probe", "unused"], timeout=2.0) is None
+ child_pid = int(child_pid_path.read_text())
+
+ deadline = time.monotonic() + 5
+ while video_thumbnails._is_process_running(child_pid) and time.monotonic() < deadline:
+ time.sleep(0.05)
+ assert not video_thumbnails._is_process_running(child_pid)
+
+
+class TestWorkerMemoryMonitor:
+ def test_terminates_worker_over_memory_limit(self, monkeypatch: pytest.MonkeyPatch) -> None:
+ proc = MagicMock()
+ proc.poll.return_value = None
+ terminated = Event()
+ monkeypatch.setattr(
+ video_thumbnails,
+ "_worker_tree_rss",
+ lambda worker: video_thumbnails.MAX_VIDEO_DECODE_WORKER_RSS_BYTES + 1,
+ )
+ monkeypatch.setattr(video_thumbnails, "_terminate_process_tree", lambda worker: terminated.set())
+
+ stop, exceeded, monitor = video_thumbnails._start_worker_memory_monitor(proc)
+ assert terminated.wait(timeout=1)
+ stop.set()
+ monitor.join(timeout=1)
+
+ assert exceeded.is_set()
+
+ def test_leaves_worker_within_memory_limit_running(self, monkeypatch: pytest.MonkeyPatch) -> None:
+ proc = MagicMock()
+ proc.poll.return_value = None
+ terminated = Event()
+ monkeypatch.setattr(
+ video_thumbnails,
+ "_worker_tree_rss",
+ lambda worker: video_thumbnails.MAX_VIDEO_DECODE_WORKER_RSS_BYTES,
+ )
+ monkeypatch.setattr(video_thumbnails, "_terminate_process_tree", lambda worker: terminated.set())
+
+ stop, exceeded, monitor = video_thumbnails._start_worker_memory_monitor(proc)
+ time.sleep(video_thumbnails.WORKER_MEMORY_POLL_SECONDS * 2)
+ stop.set()
+ monitor.join(timeout=1)
+
+ assert not exceeded.is_set()
+ assert not terminated.is_set()
+
+
+class TestRunWorkerUnexpectedFailureCleanup:
+ """An unexpected exception from ``proc.communicate()`` (e.g. OSError on a broken
+ pipe or fd exhaustion) must not leak the worker tree: ``_run_worker``'s finally
+ stops the RSS-monitor backstop, so if the except path doesn't terminate the tree,
+ nothing ever will (JPPhoto non-merge-blocker, 2026-07-22)."""
+
+ def _proc_with_failing_communicate(self, error: Exception) -> MagicMock:
+ proc = MagicMock()
+ proc.communicate.side_effect = error
+ proc.poll.return_value = None
+ return proc
+
+ def test_oserror_from_communicate_terminates_worker_tree(self, monkeypatch: pytest.MonkeyPatch) -> None:
+ proc = self._proc_with_failing_communicate(OSError("broken pipe"))
+ terminated: list[object] = []
+ monkeypatch.setattr(video_thumbnails, "_spawn_worker", lambda *args, **kwargs: proc)
+ monkeypatch.setattr(video_thumbnails, "_worker_tree_rss", lambda worker: 0)
+ monkeypatch.setattr(video_thumbnails, "_terminate_process_tree", terminated.append)
+
+ assert video_thumbnails._run_worker(["probe", "unused"], timeout=1.0) is None
+ assert terminated == [proc]
+
+ def test_spawn_failure_returns_none_without_terminate(self, monkeypatch: pytest.MonkeyPatch) -> None:
+ def _fail_spawn(*args: object, **kwargs: object) -> None:
+ raise OSError("fork failed")
+
+ terminated: list[object] = []
+ monkeypatch.setattr(video_thumbnails, "_spawn_worker", _fail_spawn)
+ monkeypatch.setattr(video_thumbnails, "_terminate_process_tree", terminated.append)
+
+ assert video_thumbnails._run_worker(["probe", "unused"], timeout=1.0) is None
+ assert terminated == []
+
+
+class TestProbeMetadataValidation:
+ """probe_video must reject decoder-reported metadata no sane video has (JPPhoto
+ review 2026-07-21): the upload path persists these values and sizes thumbnail
+ decoding by them, so zero/negative/absurd dimensions and non-finite durations must
+ fail before ``videos.create`` ever sees them."""
+
+ def _probe_with(self, monkeypatch: pytest.MonkeyPatch, payload: dict) -> tuple:
+ monkeypatch.setattr(video_thumbnails, "_run_worker", lambda args, timeout: payload)
+ return probe_video(Path("fake.mp4"))
+
+ @pytest.mark.parametrize(
+ "payload",
+ [
+ {"width": 0, "height": 32, "duration": 1.0, "fps": 8.0},
+ {"width": 48, "height": -32, "duration": 1.0, "fps": 8.0},
+ {"width": 100_000, "height": 100_000, "duration": 1.0, "fps": 8.0},
+ {"width": 48, "height": 32, "duration": float("nan"), "fps": 8.0},
+ {"width": 48, "height": 32, "duration": float("inf"), "fps": 8.0},
+ {"width": 48, "height": 32, "duration": -1.0, "fps": 8.0},
+ {"width": float("inf"), "height": 32, "duration": 1.0, "fps": 8.0},
+ {"width": float("nan"), "height": 32, "duration": 1.0, "fps": 8.0},
+ ],
+ ids=[
+ "zero-width",
+ "negative-height",
+ "over-limit-pixels",
+ "nan-duration",
+ "inf-duration",
+ "negative-duration",
+ "inf-width",
+ "nan-width",
+ ],
+ )
+ def test_invalid_metadata_is_rejected(self, monkeypatch: pytest.MonkeyPatch, payload: dict) -> None:
+ with pytest.raises(FileNotFoundError):
+ self._probe_with(monkeypatch, payload)
+
+ def test_boundary_dimensions_are_accepted(self, monkeypatch: pytest.MonkeyPatch) -> None:
+ # 8192x8192 = 64 MP, exactly at the cap.
+ width, height, duration, fps = self._probe_with(
+ monkeypatch, {"width": 8192, "height": 8192, "duration": 0.0, "fps": 8.0}
+ )
+ assert (width, height, duration, fps) == (8192, 8192, 0.0, 8.0)
+
+ @pytest.mark.parametrize("bad_fps", [float("nan"), float("inf"), -1.0, 0.0])
+ def test_unusable_fps_is_coerced_to_unknown(self, monkeypatch: pytest.MonkeyPatch, bad_fps: float) -> None:
+ # fps=None already means "unknown" to every caller, so garbage fps degrades to
+ # that instead of rejecting an otherwise-decodable file.
+ *_rest, fps = self._probe_with(monkeypatch, {"width": 48, "height": 32, "duration": 1.0, "fps": bad_fps})
+ assert fps is None
+
+
+class TestWorkerDecodeBounds:
+ """The worker refuses to decode frames from files whose reported dimensions exceed
+ the bound — the full frame is only allocated at decode time, so a small crafted
+ container claiming 100k x 100k would otherwise attempt a ~30 GB allocation."""
+
+ def test_oversized_dims_refused_before_frame_decode(self, monkeypatch: pytest.MonkeyPatch) -> None:
+ from invokeai.app.util import video_decode_worker as worker
+
+ monkeypatch.setattr(worker, "_probe", lambda path: (100_000, 100_000, 1.0, 8.0))
+ with pytest.raises(ValueError, match="exceed the maximum decodable size"):
+ worker._assert_decodable_dims(Path("fake.mp4"))
+
+ def test_unprobeable_file_is_refused_before_decode(self, monkeypatch: pytest.MonkeyPatch) -> None:
+ from invokeai.app.util import video_decode_worker as worker
+
+ def _raise(path: Path):
+ raise FileNotFoundError("no metadata")
+
+ monkeypatch.setattr(worker, "_probe", _raise)
+ with pytest.raises(ValueError, match="Unable to validate video dimensions"):
+ worker._assert_decodable_dims(Path("fake.mp4"))
+
+ @pytest.mark.parametrize("width,height", [(0, 32), (48, 0), (-1, 32), (48, -1)])
+ def test_non_positive_dims_refused_before_decode(
+ self, monkeypatch: pytest.MonkeyPatch, width: int, height: int
+ ) -> None:
+ from invokeai.app.util import video_decode_worker as worker
+
+ monkeypatch.setattr(worker, "_probe", lambda path: (width, height, 1.0, 8.0))
+ with pytest.raises(ValueError, match="invalid dimensions"):
+ worker._assert_decodable_dims(Path("fake.mp4"))
+
+ def test_in_bounds_dims_pass(self, monkeypatch: pytest.MonkeyPatch) -> None:
+ from invokeai.app.util import video_decode_worker as worker
+
+ monkeypatch.setattr(worker, "_probe", lambda path: (48, 32, 1.0, 8.0))
+ worker._assert_decodable_dims(Path("fake.mp4")) # must not raise
+
+
+class TestStreamBackendFallback:
+ """_stream must fall back to cv2 like probe/frame/count do (JPPhoto review
+ 2026-07-21) — a file accepted at upload via the cv2 fallback previously worked in
+ single-frame extract but failed in the frame-range and concat nodes."""
+
+ def _collect_stream(self, monkeypatch: pytest.MonkeyPatch, video_path: Path) -> list[np.ndarray]:
+ from invokeai.app.util import video_decode_worker as worker
+
+ buffer = io.BytesIO()
+ fake_stdout = MagicMock()
+ fake_stdout.buffer = buffer
+ monkeypatch.setattr(worker.sys, "stdout", fake_stdout)
+ worker._stream(video_path)
+
+ import struct
+
+ frames: list[np.ndarray] = []
+ buffer.seek(0)
+ while size_bytes := buffer.read(8):
+ (size,) = struct.unpack(">Q", size_bytes)
+ frames.append(np.load(io.BytesIO(buffer.read(size)), allow_pickle=False))
+ return frames
+
+ def test_falls_back_to_cv2_when_imageio_cannot_stream(
+ self, monkeypatch: pytest.MonkeyPatch, synthetic_mp4: Path
+ ) -> None:
+ from invokeai.app.util import video_decode_worker as worker
+
+ def _imiter_fails(*args, **kwargs):
+ raise RuntimeError("imageio cannot decode this container")
+ yield # pragma: no cover - makes this a generator
+
+ monkeypatch.setattr(worker.iio, "imiter", _imiter_fails)
+
+ frames = self._collect_stream(monkeypatch, synthetic_mp4)
+
+ assert len(frames) == FRAMES
+ assert frames[0].shape == (32, 48, 3)
+
+ def test_falls_back_to_cv2_when_imageio_stream_is_empty(
+ self, monkeypatch: pytest.MonkeyPatch, synthetic_mp4: Path
+ ) -> None:
+ from invokeai.app.util import video_decode_worker as worker
+
+ monkeypatch.setattr(worker.iio, "imiter", lambda *args, **kwargs: iter(()))
+
+ frames = self._collect_stream(monkeypatch, synthetic_mp4)
+
+ assert len(frames) == FRAMES
+ assert frames[0].shape == (32, 48, 3)
+
+ def test_midstream_failure_does_not_restart_from_frame_zero(
+ self, monkeypatch: pytest.MonkeyPatch, synthetic_mp4: Path
+ ) -> None:
+ from invokeai.app.util import video_decode_worker as worker
+
+ def _imiter_dies_midstream(*args, **kwargs):
+ yield np.zeros((32, 48, 3), dtype=np.uint8)
+ raise RuntimeError("decoder died mid-stream")
+
+ monkeypatch.setattr(worker.iio, "imiter", _imiter_dies_midstream)
+
+ with pytest.raises(RuntimeError, match="mid-stream"):
+ self._collect_stream(monkeypatch, synthetic_mp4)
+
+ def test_totally_undecodable_file_raises(self, monkeypatch: pytest.MonkeyPatch, tmp_path: Path) -> None:
+ bogus = tmp_path / "junk.mp4"
+ bogus.write_bytes(b"not actually an mp4")
+ # imageio raises first; cv2 then fails to open -> FileNotFoundError. Either way
+ # the worker must error rather than emit an empty, successful stream.
+ with pytest.raises((FileNotFoundError, ValueError)):
+ self._collect_stream(monkeypatch, bogus)
+
+
+class TestDecodedFrameValidation:
+ def test_accepts_frame_within_pixel_limit(self, monkeypatch: pytest.MonkeyPatch) -> None:
+ monkeypatch.setattr(video_decode_worker, "MAX_FRAME_PIXELS", 4)
+
+ video_decode_worker._validate_decoded_frame(np.zeros((2, 2, 3), dtype=np.uint8))
+
+ def test_rejects_later_frame_over_pixel_limit(self, monkeypatch: pytest.MonkeyPatch) -> None:
+ monkeypatch.setattr(video_decode_worker, "MAX_FRAME_PIXELS", 4)
+
+ with pytest.raises(ValueError, match="Decoded frame dimensions"):
+ video_decode_worker._validate_decoded_frame(np.zeros((3, 2, 3), dtype=np.uint8))
+
+ @pytest.mark.parametrize(
+ "frame",
+ [np.zeros((2, 2), dtype=np.uint8), np.zeros((2, 2, 4), dtype=np.uint8)],
+ )
+ def test_rejects_non_rgb_frames(self, frame: np.ndarray) -> None:
+ with pytest.raises(ValueError, match="RGB"):
+ video_decode_worker._validate_decoded_frame(frame)
diff --git a/tests/backend/model_manager/configs/test_wan_gguf_config.py b/tests/backend/model_manager/configs/test_wan_gguf_config.py
new file mode 100644
index 00000000000..5678c381817
--- /dev/null
+++ b/tests/backend/model_manager/configs/test_wan_gguf_config.py
@@ -0,0 +1,326 @@
+"""Tests for the GGUF Wan probe (Main_GGUF_Wan_Config)."""
+
+from pathlib import Path
+from tempfile import TemporaryDirectory
+from unittest.mock import MagicMock
+
+import gguf
+import pytest
+import torch
+
+from invokeai.backend.model_manager.configs.identification_utils import NotAMatchError
+from invokeai.backend.model_manager.configs.main import (
+ Main_GGUF_Wan_Config,
+ _detect_wan_gguf_expert,
+ _detect_wan_gguf_variant,
+ _has_wan_keys,
+ _is_native_wan_layout,
+)
+from invokeai.backend.model_manager.model_on_disk import ModelOnDisk
+from invokeai.backend.model_manager.taxonomy import BaseModelType, ModelFormat, WanVariantType
+from invokeai.backend.quantization.gguf.ggml_tensor import GGMLTensor
+
+
+def _ggml(shape: tuple[int, ...]) -> GGMLTensor:
+ return GGMLTensor(
+ data=torch.zeros((1,), dtype=torch.uint8),
+ ggml_quantization_type=gguf.GGMLQuantizationType.Q4_0,
+ tensor_shape=torch.Size(shape),
+ compute_dtype=torch.float32,
+ )
+
+
+def _wan_a14b_state_dict(prefix: str = "") -> dict:
+ """Synthetic Wan A14B GGUF state dict (16-channel patch embed)."""
+ return {
+ f"{prefix}patch_embedding.weight": _ggml((5120, 16, 1, 2, 2)),
+ f"{prefix}condition_embedder.text_embedder.linear_1.weight": _ggml((5120, 4096)),
+ f"{prefix}blocks.0.attn1.to_q.weight": _ggml((5120, 5120)),
+ f"{prefix}blocks.0.ffn.net.0.proj.weight": _ggml((13824, 5120)),
+ }
+
+
+def _wan_ti2v_state_dict() -> dict:
+ """Synthetic Wan TI2V-5B GGUF state dict (48-channel patch embed)."""
+ return {
+ "patch_embedding.weight": _ggml((3072, 48, 1, 2, 2)),
+ "condition_embedder.text_embedder.linear_1.weight": _ggml((3072, 4096)),
+ "blocks.0.attn1.to_q.weight": _ggml((3072, 3072)),
+ "blocks.0.ffn.net.0.proj.weight": _ggml((14336, 3072)),
+ }
+
+
+def _wan_i2v_a14b_state_dict() -> dict:
+ """Wan 2.2 I2V-A14B GGUF: same shape as T2V except patch_embedding has 36
+ input channels (16 noise + 16 ref-image latents + 4 first-frame mask)."""
+ return {
+ "patch_embedding.weight": _ggml((5120, 36, 1, 2, 2)),
+ "condition_embedder.text_embedder.linear_1.weight": _ggml((5120, 4096)),
+ "blocks.0.attn1.to_q.weight": _ggml((5120, 5120)),
+ "blocks.0.ffn.net.0.proj.weight": _ggml((13824, 5120)),
+ }
+
+
+def _wan_a14b_native_state_dict() -> dict:
+ """Synthetic Wan A14B GGUF state dict using the native upstream key layout
+ (text_embedding/self_attn/cross_attn/ffn.0 — what QuantStack and ComfyUI ship)."""
+ return {
+ "patch_embedding.weight": _ggml((5120, 16, 1, 2, 2)),
+ "text_embedding.0.weight": _ggml((5120, 4096)),
+ "text_embedding.2.weight": _ggml((5120, 5120)),
+ "blocks.0.self_attn.q.weight": _ggml((5120, 5120)),
+ "blocks.0.cross_attn.q.weight": _ggml((5120, 5120)),
+ "blocks.0.ffn.0.weight": _ggml((13824, 5120)),
+ "blocks.0.modulation": _ggml((1, 6, 5120)),
+ "head.head.weight": _ggml((64, 5120)),
+ "head.modulation": _ggml((1, 2, 5120)),
+ }
+
+
+def _build_overrides(model_path: Path, name: str) -> dict:
+ return {
+ "hash": "test-hash",
+ "path": str(model_path),
+ "file_size": 0,
+ "name": name,
+ "source": str(model_path),
+ "source_type": "path",
+ }
+
+
+def _make_mod(path: Path, sd: dict, metadata: dict[str, str] | None = None) -> MagicMock:
+ mod = MagicMock()
+ mod.path = path
+ mod.load_state_dict.return_value = sd
+ mod.metadata.return_value = metadata or {}
+ return mod
+
+
+def test_model_on_disk_reads_gguf_name_metadata(tmp_path: Path) -> None:
+ path = tmp_path / "renamed.gguf"
+ writer = gguf.GGUFWriter(path, "wan")
+ writer.add_name("Wan2.2 T2V A14B")
+ writer.write_header_to_file()
+ writer.write_kv_data_to_file()
+ writer.write_tensors_to_file()
+ writer.close()
+
+ assert ModelOnDisk(path).metadata()["general.name"] == "Wan2.2 T2V A14B"
+
+
+class TestKeyFingerprint:
+ def test_recognises_bare_keys(self):
+ assert _has_wan_keys(_wan_ti2v_state_dict()) is True
+
+ def test_recognises_comfyui_prefix(self):
+ assert _has_wan_keys(_wan_a14b_state_dict(prefix="model.diffusion_model.")) is True
+
+ def test_recognises_diffusion_model_prefix(self):
+ assert _has_wan_keys(_wan_a14b_state_dict(prefix="diffusion_model.")) is True
+
+ def test_recognises_native_upstream_layout(self):
+ assert _has_wan_keys(_wan_a14b_native_state_dict()) is True
+
+ def test_rejects_qwen_image(self):
+ sd = {"txt_in.weight": _ggml((1, 1)), "img_in.weight": _ggml((1, 1))}
+ assert _has_wan_keys(sd) is False
+
+ def test_rejects_flux(self):
+ sd = {"double_blocks.0.img_attn.proj.weight": _ggml((1, 1))}
+ assert _has_wan_keys(sd) is False
+
+
+class TestNativeLayoutDetection:
+ def test_native_a14b(self):
+ assert _is_native_wan_layout(_wan_a14b_native_state_dict()) is True
+
+ def test_diffusers_a14b_is_not_native(self):
+ assert _is_native_wan_layout(_wan_a14b_state_dict()) is False
+
+ def test_diffusers_ti2v_is_not_native(self):
+ assert _is_native_wan_layout(_wan_ti2v_state_dict()) is False
+
+
+class TestVariantDetection:
+ def test_a14b_from_16ch(self):
+ sd = _wan_a14b_state_dict()
+ assert _detect_wan_gguf_variant(sd) == WanVariantType.T2V_A14B
+
+ def test_ti2v_from_48ch(self):
+ sd = _wan_ti2v_state_dict()
+ assert _detect_wan_gguf_variant(sd) == WanVariantType.TI2V_5B
+
+ def test_i2v_a14b_from_36ch(self):
+ """Wan 2.2 I2V has the same A14B architecture as T2V but with
+ in_channels=36 because the ref-image latents and first-frame mask are
+ concatenated to the noise along the channel dim before patch embedding."""
+ sd = _wan_i2v_a14b_state_dict()
+ assert _detect_wan_gguf_variant(sd) == WanVariantType.I2V_A14B
+
+ def test_unknown_channel_count_returns_none(self):
+ sd = {"patch_embedding.weight": _ggml((1, 32, 1, 2, 2))}
+ assert _detect_wan_gguf_variant(sd) is None
+
+ def test_missing_patch_embedding_returns_none(self):
+ sd = {"blocks.0.attn1.to_q.weight": _ggml((1, 1))}
+ assert _detect_wan_gguf_variant(sd) is None
+
+
+class TestExpertFilenameHeuristic:
+ @pytest.mark.parametrize(
+ "name, expected",
+ [
+ ("wan2.2-t2v-a14b-high_noise-Q4_K_M", "high"),
+ ("Wan2.2-T2V-A14B-High-Noise-Q4_K_M", "high"),
+ ("wan_a14b_highnoise_q4", "high"),
+ ("wan2.2-t2v-a14b-low_noise-Q4_K_M", "low"),
+ ("Wan2.2-A14B-LowNoise-Q4", "low"),
+ ("wan2.2-ti2v-5b-Q4_K_M", "none"),
+ ("wan-A14B-flagship", "none"),
+ ],
+ )
+ def test_filename_heuristic(self, name: str, expected: str):
+ assert _detect_wan_gguf_expert(name) == expected
+
+
+class TestProbe:
+ @pytest.mark.parametrize(
+ "filename,state_dict",
+ [
+ ("Wan2.1-T2V-14B-Q4_K_M.gguf", _wan_a14b_state_dict()),
+ ("Wan2.1-I2V-14B-Q4_K_M.gguf", _wan_i2v_a14b_state_dict()),
+ ],
+ )
+ def test_rejects_wan_2_1(self, filename: str, state_dict: dict) -> None:
+ with TemporaryDirectory() as tmp:
+ path = Path(tmp) / filename
+ path.touch()
+
+ with pytest.raises(NotAMatchError, match="Wan 2.1"):
+ Main_GGUF_Wan_Config.from_model_on_disk(
+ _make_mod(path, state_dict),
+ _build_overrides(path, "unsupported Wan 2.1"),
+ )
+
+ def test_rejects_ambiguous_a14b_filename(self) -> None:
+ with TemporaryDirectory() as tmp:
+ path = Path(tmp) / "renamed-high_noise-Q4_K_M.gguf"
+ path.touch()
+
+ with pytest.raises(NotAMatchError, match="Wan 2.2"):
+ Main_GGUF_Wan_Config.from_model_on_disk(
+ _make_mod(path, _wan_a14b_state_dict()),
+ _build_overrides(path, "ambiguous Wan A14B"),
+ )
+
+ def test_accepts_renamed_wan_2_2_from_gguf_metadata(self) -> None:
+ with TemporaryDirectory() as tmp:
+ path = Path(tmp) / "renamed-high_noise-Q4_K_M.gguf"
+ path.touch()
+
+ config = Main_GGUF_Wan_Config.from_model_on_disk(
+ _make_mod(path, _wan_a14b_state_dict(), {"general.name": "Wan2.2 T2V A14B"}),
+ _build_overrides(path, "renamed Wan 2.2"),
+ )
+
+ assert config.variant == WanVariantType.T2V_A14B
+
+ def test_rejects_wan_2_1_metadata_despite_wan_2_2_filename(self) -> None:
+ with TemporaryDirectory() as tmp:
+ path = Path(tmp) / "Wan2.2-T2V-A14B-high_noise-Q4_K_M.gguf"
+ path.touch()
+
+ with pytest.raises(NotAMatchError, match="Wan 2.1"):
+ Main_GGUF_Wan_Config.from_model_on_disk(
+ _make_mod(path, _wan_a14b_state_dict(), {"general.name": "Wan2.1 T2V 14B"}),
+ _build_overrides(path, "misnamed Wan 2.1"),
+ )
+
+ def test_a14b_high_noise_filename(self):
+ with TemporaryDirectory() as tmp:
+ f = Path(tmp) / "wan2.2-t2v-a14b-high_noise-Q4_K_M.gguf"
+ f.touch()
+
+ cfg = Main_GGUF_Wan_Config.from_model_on_disk(
+ _make_mod(f, _wan_a14b_state_dict()),
+ _build_overrides(f, "Wan A14B (high)"),
+ )
+ assert cfg.base == BaseModelType.Wan
+ assert cfg.format == ModelFormat.GGUFQuantized
+ assert cfg.variant == WanVariantType.T2V_A14B
+ assert cfg.expert == "high"
+
+ def test_a14b_low_noise_filename(self):
+ with TemporaryDirectory() as tmp:
+ f = Path(tmp) / "wan2.2-t2v-a14b-low_noise-Q4_K_M.gguf"
+ f.touch()
+
+ cfg = Main_GGUF_Wan_Config.from_model_on_disk(
+ _make_mod(f, _wan_a14b_state_dict()),
+ _build_overrides(f, "Wan A14B (low)"),
+ )
+ assert cfg.expert == "low"
+
+ def test_ti2v_5b_unambiguous(self):
+ with TemporaryDirectory() as tmp:
+ f = Path(tmp) / "wan2.2-ti2v-5b-Q4_K_M.gguf"
+ f.touch()
+
+ cfg = Main_GGUF_Wan_Config.from_model_on_disk(
+ _make_mod(f, _wan_ti2v_state_dict()),
+ _build_overrides(f, "Wan TI2V-5B"),
+ )
+ assert cfg.variant == WanVariantType.TI2V_5B
+ assert cfg.expert == "none"
+
+ def test_rejects_non_gguf(self):
+ with TemporaryDirectory() as tmp:
+ f = Path(tmp) / "wan-a14b.safetensors"
+ f.touch()
+ sd = {"patch_embedding.weight": torch.zeros(5120, 16, 1, 2, 2)} # NOT a GGMLTensor
+
+ with pytest.raises(NotAMatchError, match="GGUF"):
+ Main_GGUF_Wan_Config.from_model_on_disk(
+ _make_mod(f, sd),
+ _build_overrides(f, "non-gguf"),
+ )
+
+ def test_rejects_unrecognised_state_dict(self):
+ with TemporaryDirectory() as tmp:
+ f = Path(tmp) / "junk.gguf"
+ f.touch()
+ sd = {"random.key": _ggml((1, 1))}
+
+ with pytest.raises(NotAMatchError, match="Wan transformer"):
+ Main_GGUF_Wan_Config.from_model_on_disk(
+ _make_mod(f, sd),
+ _build_overrides(f, "junk"),
+ )
+
+ def test_native_upstream_a14b_high_noise(self):
+ """QuantStack-style GGUF: native upstream keys + HighNoise filename."""
+ with TemporaryDirectory() as tmp:
+ f = Path(tmp) / "Wan2.2-T2V-A14B-HighNoise-Q4_K_M.gguf"
+ f.touch()
+
+ cfg = Main_GGUF_Wan_Config.from_model_on_disk(
+ _make_mod(f, _wan_a14b_native_state_dict()),
+ _build_overrides(f, "Wan A14B QuantStack (high)"),
+ )
+ assert cfg.base == BaseModelType.Wan
+ assert cfg.format == ModelFormat.GGUFQuantized
+ assert cfg.variant == WanVariantType.T2V_A14B
+ assert cfg.expert == "high"
+
+ def test_explicit_expert_override(self):
+ with TemporaryDirectory() as tmp:
+ f = Path(tmp) / "wan2.2-a14b-flagship.gguf"
+ f.touch()
+ overrides = _build_overrides(f, "user-tagged")
+ overrides["expert"] = "low"
+
+ cfg = Main_GGUF_Wan_Config.from_model_on_disk(
+ _make_mod(f, _wan_a14b_state_dict()),
+ overrides,
+ )
+ assert cfg.expert == "low"
diff --git a/tests/backend/model_manager/configs/test_wan_lora_config.py b/tests/backend/model_manager/configs/test_wan_lora_config.py
new file mode 100644
index 00000000000..43f55db06b2
--- /dev/null
+++ b/tests/backend/model_manager/configs/test_wan_lora_config.py
@@ -0,0 +1,372 @@
+"""Tests for the Wan LoRA probe (LoRA_LyCORIS_Wan_Config).
+
+These tests cover detection across the three formats Wan LoRAs ship in:
+
+- **Diffusers PEFT**, with or without a ``transformer.`` prefix
+- **Native upstream PEFT** with ``diffusion_model.`` prefix (ComfyUI-trained)
+- **Kohya** ``lora_unet_blocks_N_`` with both diffusers and native
+ attention naming
+
+And the anti-pattern guards that prevent false positives on:
+
+- Anima (Cosmos DiT — ``cross_attn_q_proj`` / ``mlp`` / ``adaln_modulation``)
+- QwenImage (``transformer_blocks.``)
+- Flux (``double_blocks`` / ``single_blocks`` / ``single_transformer_blocks``)
+- Z-Image (``diffusion_model.layers.``)
+"""
+
+from pathlib import Path
+from tempfile import TemporaryDirectory
+from unittest.mock import MagicMock
+
+import pytest
+import torch
+
+from invokeai.backend.model_manager.configs.identification_utils import NotAMatchError
+from invokeai.backend.model_manager.configs.lora import LoRA_LyCORIS_Wan_Config
+from invokeai.backend.model_manager.taxonomy import BaseModelType, ModelFormat
+from invokeai.backend.patches.lora_conversions.wan_lora_constants import (
+ has_non_wan_architecture_keys,
+ has_wan_kohya_keys,
+ has_wan_peft_keys,
+)
+
+
+def _make_mod(path: Path, sd: dict) -> MagicMock:
+ mod = MagicMock()
+ mod.path = path
+ mod.load_state_dict.return_value = sd
+ return mod
+
+
+def _overrides(model_path: Path, name: str) -> dict:
+ return {
+ "hash": "test-hash",
+ "path": str(model_path),
+ "file_size": 0,
+ "name": name,
+ "source": str(model_path),
+ "source_type": "path",
+ }
+
+
+def _t(shape: tuple[int, ...]) -> torch.Tensor:
+ return torch.zeros(shape)
+
+
+class TestDiffusersPEFTPositives:
+ def test_attn1_to_q(self):
+ keys = ["transformer.blocks.0.attn1.to_q.lora_A.weight"]
+ assert has_wan_peft_keys(keys) is True
+ assert has_non_wan_architecture_keys(keys) is False
+
+ def test_attn2_to_k(self):
+ keys = ["blocks.0.attn2.to_k.lora_A.weight"]
+ assert has_wan_peft_keys(keys) is True
+ assert has_non_wan_architecture_keys(keys) is False
+
+ def test_ffn_net(self):
+ keys = ["transformer.blocks.0.ffn.net.0.proj.lora_A.weight"]
+ assert has_wan_peft_keys(keys) is True
+ assert has_non_wan_architecture_keys(keys) is False
+
+ def test_base_model_peft_prefix(self):
+ keys = ["base_model.model.transformer.blocks.0.attn1.to_q.lora_A.weight"]
+ assert has_wan_peft_keys(keys) is True
+ assert has_non_wan_architecture_keys(keys) is False
+
+
+class TestNativePEFTPositives:
+ def test_self_attn_q(self):
+ keys = ["diffusion_model.blocks.0.self_attn.q.lora_A.weight"]
+ assert has_wan_peft_keys(keys) is True
+ assert has_non_wan_architecture_keys(keys) is False
+
+ def test_cross_attn_k(self):
+ keys = ["diffusion_model.blocks.0.cross_attn.k.lora_A.weight"]
+ assert has_wan_peft_keys(keys) is True
+ assert has_non_wan_architecture_keys(keys) is False
+
+ def test_cross_attn_o(self):
+ keys = ["transformer.blocks.0.cross_attn.o.lora_A.weight"]
+ assert has_wan_peft_keys(keys) is True
+ assert has_non_wan_architecture_keys(keys) is False
+
+ def test_ffn_native(self):
+ keys = ["diffusion_model.blocks.0.ffn.0.lora_A.weight"]
+ assert has_wan_peft_keys(keys) is True
+ assert has_non_wan_architecture_keys(keys) is False
+
+
+class TestKohyaPositives:
+ def test_kohya_diffusers_attn1_to_q(self):
+ keys = ["lora_unet_blocks_0_attn1_to_q.lora_down.weight"]
+ assert has_wan_kohya_keys(keys) is True
+ assert has_non_wan_architecture_keys(keys) is False
+
+ def test_kohya_diffusers_attn2_to_out(self):
+ keys = ["lora_unet_blocks_0_attn2_to_out_0.lora_down.weight"]
+ assert has_wan_kohya_keys(keys) is True
+ assert has_non_wan_architecture_keys(keys) is False
+
+ def test_kohya_native_self_attn_q(self):
+ keys = ["lora_unet_blocks_0_self_attn_q.lora_down.weight"]
+ assert has_wan_kohya_keys(keys) is True
+ assert has_non_wan_architecture_keys(keys) is False
+
+ def test_kohya_native_cross_attn_v(self):
+ keys = ["lora_unet_blocks_5_cross_attn_v.lora_down.weight"]
+ assert has_wan_kohya_keys(keys) is True
+ assert has_non_wan_architecture_keys(keys) is False
+
+ def test_kohya_native_ffn_0(self):
+ keys = ["lora_unet_blocks_0_ffn_0.lora_down.weight"]
+ assert has_wan_kohya_keys(keys) is True
+ assert has_non_wan_architecture_keys(keys) is False
+
+
+class TestArchitectureGuards:
+ """Anti-pattern checks: non-Wan architectures must be flagged so the
+ probe rejects them even if a wan-ish substring matches."""
+
+ @pytest.mark.parametrize(
+ "label, keys",
+ [
+ ("anima_kohya_q_proj", ["lora_unet_blocks_0_cross_attn_q_proj.lora_down.weight"]),
+ ("anima_peft_mlp", ["transformer.blocks.0.mlp.layer1.lora_A.weight"]),
+ ("anima_peft_adaln", ["transformer.blocks.0.adaln_modulation.linear.lora_A.weight"]),
+ ("anima_peft_self_attn_q_proj", ["transformer.blocks.0.self_attn.q_proj.lora_A.weight"]),
+ ("qwen_image", ["transformer_blocks.0.attn.to_q.lora_A.weight"]),
+ ("flux_kohya_double", ["lora_unet_double_blocks_0_img_attn_qkv.lora_down.weight"]),
+ ("flux_kohya_single", ["lora_unet_single_blocks_0_linear1.lora_down.weight"]),
+ ("flux_diffusers_single_transformer", ["transformer.single_transformer_blocks.0.attn.to_q.lora_A.weight"]),
+ ("z_image", ["diffusion_model.layers.0.attn.to_q.lora_A.weight"]),
+ ],
+ )
+ def test_non_wan_archs_are_flagged(self, label: str, keys: list[str]):
+ assert has_non_wan_architecture_keys(keys) is True
+
+
+class TestProbeAcceptance:
+ """End-to-end probe behavior — Wan LoRA must be accepted, non-Wan rejected."""
+
+ def _wan_diffusers_sd(self) -> dict:
+ return {
+ "transformer.blocks.0.attn1.to_q.lora_A.weight": _t((128, 5120)),
+ "transformer.blocks.0.attn1.to_q.lora_B.weight": _t((5120, 128)),
+ "transformer.blocks.0.ffn.net.0.proj.lora_A.weight": _t((128, 5120)),
+ "transformer.blocks.0.ffn.net.0.proj.lora_B.weight": _t((13824, 128)),
+ }
+
+ def _wan_native_sd(self) -> dict:
+ return {
+ "diffusion_model.blocks.0.self_attn.q.lora_A.weight": _t((128, 5120)),
+ "diffusion_model.blocks.0.self_attn.q.lora_B.weight": _t((5120, 128)),
+ }
+
+ def _wan_kohya_sd(self) -> dict:
+ return {
+ "lora_unet_blocks_0_attn1_to_q.lora_down.weight": _t((128, 5120)),
+ "lora_unet_blocks_0_attn1_to_q.lora_up.weight": _t((5120, 128)),
+ }
+
+ def _wan_ti2v5b_sd(self) -> dict:
+ """A TI2V-5B LoRA — inner_dim 3072, not 5120."""
+ return {
+ "transformer.blocks.0.attn1.to_q.lora_A.weight": _t((64, 3072)),
+ "transformer.blocks.0.attn1.to_q.lora_B.weight": _t((3072, 64)),
+ }
+
+ def test_accepts_diffusers_wan(self):
+ with TemporaryDirectory() as tmp:
+ f = Path(tmp) / "my-wan-lora.safetensors"
+ f.touch()
+ cfg = LoRA_LyCORIS_Wan_Config.from_model_on_disk(
+ _make_mod(f, self._wan_diffusers_sd()),
+ _overrides(f, "wan-lora"),
+ )
+ assert cfg.base == BaseModelType.Wan
+ assert cfg.format == ModelFormat.LyCORIS
+ assert cfg.expert is None
+ assert cfg.variant == "a14b" # 5120-dim state dict
+
+ def test_accepts_native_wan(self):
+ with TemporaryDirectory() as tmp:
+ f = Path(tmp) / "wan-style-lora.safetensors"
+ f.touch()
+ cfg = LoRA_LyCORIS_Wan_Config.from_model_on_disk(
+ _make_mod(f, self._wan_native_sd()),
+ _overrides(f, "wan-native"),
+ )
+ assert cfg.base == BaseModelType.Wan
+
+ def test_accepts_kohya_wan(self):
+ with TemporaryDirectory() as tmp:
+ f = Path(tmp) / "wan-kohya.safetensors"
+ f.touch()
+ cfg = LoRA_LyCORIS_Wan_Config.from_model_on_disk(
+ _make_mod(f, self._wan_kohya_sd()),
+ _overrides(f, "wan-kohya"),
+ )
+ assert cfg.base == BaseModelType.Wan
+
+ def test_filename_marks_high_noise_expert(self):
+ with TemporaryDirectory() as tmp:
+ f = Path(tmp) / "stylize-high_noise.safetensors"
+ f.touch()
+ cfg = LoRA_LyCORIS_Wan_Config.from_model_on_disk(
+ _make_mod(f, self._wan_diffusers_sd()),
+ _overrides(f, "high-noise lora"),
+ )
+ assert cfg.expert == "high"
+
+ def test_filename_marks_low_noise_expert(self):
+ with TemporaryDirectory() as tmp:
+ f = Path(tmp) / "fine-detail-LowNoise.safetensors"
+ f.touch()
+ cfg = LoRA_LyCORIS_Wan_Config.from_model_on_disk(
+ _make_mod(f, self._wan_diffusers_sd()),
+ _overrides(f, "low-noise lora"),
+ )
+ assert cfg.expert == "low"
+
+ def test_explicit_expert_override_wins(self):
+ with TemporaryDirectory() as tmp:
+ f = Path(tmp) / "ambiguous-name.safetensors"
+ f.touch()
+ overrides = _overrides(f, "override")
+ overrides["expert"] = "low"
+ cfg = LoRA_LyCORIS_Wan_Config.from_model_on_disk(
+ _make_mod(f, self._wan_diffusers_sd()),
+ overrides,
+ )
+ assert cfg.expert == "low"
+
+ def test_expert_none_for_untagged_filename(self):
+ with TemporaryDirectory() as tmp:
+ f = Path(tmp) / "my-lora.safetensors"
+ f.touch()
+ cfg = LoRA_LyCORIS_Wan_Config.from_model_on_disk(
+ _make_mod(f, self._wan_diffusers_sd()),
+ _overrides(f, "untagged"),
+ )
+ assert cfg.expert is None
+
+ def test_variant_detected_as_5b_when_inner_dim_3072(self):
+ """TI2V-5B LoRAs have inner_dim 3072. Detector must classify them as
+ '5b' so the FE filter doesn't route them to an A14B main and crash."""
+ with TemporaryDirectory() as tmp:
+ f = Path(tmp) / "ti2v5b-lora.safetensors"
+ f.touch()
+ cfg = LoRA_LyCORIS_Wan_Config.from_model_on_disk(
+ _make_mod(f, self._wan_ti2v5b_sd()),
+ _overrides(f, "ti2v5b"),
+ )
+ assert cfg.base == BaseModelType.Wan
+ assert cfg.variant == "5b"
+
+ def test_variant_none_when_unrecognised_inner_dim(self):
+ """A future Wan family or a LoRA touching only ffn at non-attn dims
+ should map to variant=None rather than mis-classify."""
+ with TemporaryDirectory() as tmp:
+ f = Path(tmp) / "future-wan.safetensors"
+ f.touch()
+ # Only an ffn LoRA — no attn weight to read inner_dim from.
+ # Also a non-5120, non-3072 dim that would otherwise mis-classify.
+ sd = {
+ "transformer.blocks.0.ffn.net.0.proj.lora_A.weight": _t((128, 4096)),
+ "transformer.blocks.0.ffn.net.0.proj.lora_B.weight": _t((11008, 128)),
+ }
+ cfg = LoRA_LyCORIS_Wan_Config.from_model_on_disk(_make_mod(f, sd), _overrides(f, "future"))
+ assert cfg.variant is None
+
+ def test_explicit_variant_override_wins(self):
+ with TemporaryDirectory() as tmp:
+ f = Path(tmp) / "manual.safetensors"
+ f.touch()
+ overrides = _overrides(f, "manual")
+ overrides["variant"] = "5b"
+ # State dict is 5120-dim (auto-detect would say "a14b") but the
+ # explicit override should stick.
+ cfg = LoRA_LyCORIS_Wan_Config.from_model_on_disk(
+ _make_mod(f, self._wan_diffusers_sd()),
+ overrides,
+ )
+ assert cfg.variant == "5b"
+
+ def test_rejects_anima_lora(self):
+ with TemporaryDirectory() as tmp:
+ f = Path(tmp) / "anima.safetensors"
+ f.touch()
+ sd = {
+ "transformer.blocks.0.cross_attn.q_proj.lora_A.weight": _t((128, 4096)),
+ "transformer.blocks.0.mlp.layer1.lora_A.weight": _t((128, 4096)),
+ }
+ with pytest.raises(NotAMatchError, match="Wan LoRA"):
+ LoRA_LyCORIS_Wan_Config.from_model_on_disk(_make_mod(f, sd), _overrides(f, "anima"))
+
+ def test_rejects_qwen_image_lora(self):
+ with TemporaryDirectory() as tmp:
+ f = Path(tmp) / "qwen.safetensors"
+ f.touch()
+ sd = {"transformer_blocks.0.attn.to_q.lora_A.weight": _t((128, 4096))}
+ with pytest.raises(NotAMatchError, match="Wan LoRA"):
+ LoRA_LyCORIS_Wan_Config.from_model_on_disk(_make_mod(f, sd), _overrides(f, "qwen"))
+
+ def test_rejects_flux_lora(self):
+ with TemporaryDirectory() as tmp:
+ f = Path(tmp) / "flux.safetensors"
+ f.touch()
+ sd = {"lora_unet_double_blocks_0_img_attn_qkv.lora_down.weight": _t((128, 3072))}
+ with pytest.raises(NotAMatchError, match="Wan LoRA"):
+ LoRA_LyCORIS_Wan_Config.from_model_on_disk(_make_mod(f, sd), _overrides(f, "flux"))
+
+
+class TestProbeMutualExclusivity:
+ """Regression: Anima's probe must REJECT Wan-native LoRA keys, so probing
+ is correct regardless of which config the factory iterates first.
+
+ ``Config_Base.CONFIG_CLASSES`` is a ``set``, so iteration order is
+ non-deterministic across Python process restarts. Probes therefore need
+ to be mutually exclusive at the per-config level — see also
+ ``test_wan_lora_probe_independence.py`` for the broader cross-architecture
+ coverage."""
+
+ def test_anima_rejects_wan_native_lora(self):
+ """Wan native LoRAs (``diffusion_model.blocks.X.self_attn.q.lora_*``)
+ used to false-positive on Anima's probe because Anima accepted any
+ ``cross_attn``/``self_attn`` substring. Anima now requires
+ Cosmos-DiT-exclusive markers (``mlp``, ``adaln_modulation``, or the
+ ``_proj`` attention suffix), so a Wan LoRA — which has none of those —
+ is correctly rejected."""
+ from invokeai.backend.model_manager.configs.lora import LoRA_LyCORIS_Anima_Config
+
+ with TemporaryDirectory() as tmp:
+ f = Path(tmp) / "wan_native_lora.safetensors"
+ f.touch()
+ # Realistic Wan native PEFT keys — what lightx2v's Lightning
+ # distillations and most ComfyUI-trained Wan LoRAs look like.
+ sd = {
+ "diffusion_model.blocks.0.self_attn.q.lora_A.weight": _t((128, 5120)),
+ "diffusion_model.blocks.0.self_attn.q.lora_B.weight": _t((5120, 128)),
+ "diffusion_model.blocks.0.cross_attn.k.lora_A.weight": _t((128, 5120)),
+ "diffusion_model.blocks.0.cross_attn.k.lora_B.weight": _t((5120, 128)),
+ }
+ with pytest.raises(NotAMatchError, match="Anima LoRA"):
+ LoRA_LyCORIS_Anima_Config.from_model_on_disk(_make_mod(f, sd), _overrides(f, "wan-native-lora"))
+
+ def test_wan_rejects_anima_lora(self):
+ """Mirror direction: a real Anima LoRA must not be matched by Wan.
+ Wan's anti-patterns already cover ``_proj`` suffix, ``mlp``, and
+ ``adaln_modulation``."""
+ with TemporaryDirectory() as tmp:
+ f = Path(tmp) / "anima_lora.safetensors"
+ f.touch()
+ sd = {
+ "transformer.blocks.0.self_attn.q_proj.lora_A.weight": _t((128, 4096)),
+ "transformer.blocks.0.self_attn.q_proj.lora_B.weight": _t((4096, 128)),
+ "transformer.blocks.0.mlp.layer1.lora_A.weight": _t((128, 4096)),
+ "transformer.blocks.0.mlp.layer1.lora_B.weight": _t((4096, 128)),
+ }
+ with pytest.raises(NotAMatchError, match="Wan LoRA"):
+ LoRA_LyCORIS_Wan_Config.from_model_on_disk(_make_mod(f, sd), _overrides(f, "anima-lora"))
diff --git a/tests/backend/model_manager/configs/test_wan_lora_probe_independence.py b/tests/backend/model_manager/configs/test_wan_lora_probe_independence.py
new file mode 100644
index 00000000000..93fdf054639
--- /dev/null
+++ b/tests/backend/model_manager/configs/test_wan_lora_probe_independence.py
@@ -0,0 +1,275 @@
+"""Regression tests for Wan vs Anima LoRA probe mutual exclusivity.
+
+InvokeAI's ``Config_Base.CONFIG_CLASSES`` is a ``set``, so iteration order is
+non-deterministic across Python process restarts. The probe MUST therefore be
+mutually exclusive at the per-config level — first-match-wins is not safe to
+rely on.
+
+The historic bug these tests guard against: Anima's probe accepted anything
+with the ``cross_attn`` or ``self_attn`` substring, which collides with Wan's
+native LoRA key layout (``diffusion_model.blocks.X.cross_attn.q.lora_down.weight``).
+A Wan native LoRA — including lightx2v's Lightning distillations — would
+randomly identify as ``BaseModelType.Anima`` depending on dict hash order.
+
+The fix tightened Anima's probe to require Cosmos-DiT-exclusive markers
+(``mlp``, ``adaln_modulation``, or attention with the ``_proj`` suffix).
+
+Each test below feeds a fixed state dict shape to BOTH the Wan and Anima
+probes individually and asserts at most one accepts — order-independent.
+"""
+
+from pathlib import Path
+from tempfile import TemporaryDirectory
+from unittest.mock import MagicMock
+
+import pytest
+import torch
+
+from invokeai.backend.model_manager.configs.identification_utils import NotAMatchError
+from invokeai.backend.model_manager.configs.lora import (
+ LoRA_LyCORIS_Anima_Config,
+ LoRA_LyCORIS_Wan_Config,
+)
+from invokeai.backend.model_manager.taxonomy import BaseModelType
+
+
+def _t(shape: tuple[int, ...]) -> torch.Tensor:
+ return torch.zeros(shape)
+
+
+def _make_mod(path: Path, sd: dict) -> MagicMock:
+ mod = MagicMock()
+ mod.path = path
+ mod.load_state_dict.return_value = sd
+ return mod
+
+
+def _overrides(p: Path, name: str) -> dict:
+ return {
+ "hash": "test-hash",
+ "path": str(p),
+ "file_size": 0,
+ "name": name,
+ "source": str(p),
+ "source_type": "path",
+ }
+
+
+def _probe(cls, path: Path, sd: dict, name: str):
+ """Try a probe; return (accepted: bool, instance_or_exc)."""
+ try:
+ return True, cls.from_model_on_disk(_make_mod(path, sd), _overrides(path, name))
+ except NotAMatchError as e:
+ return False, e
+
+
+def _i2v_lightning_v1_keys() -> dict:
+ """Realistic key shape from lightx2v's I2V-A14B Lightning V1 — the actual
+ LoRA that triggered the bug. Native upstream Wan naming with
+ ``diffusion_model.`` prefix, no ``_proj`` suffix on attention."""
+ sd: dict[str, torch.Tensor] = {}
+ for block in range(3):
+ for sub in ("self_attn", "cross_attn"):
+ for proj in ("q", "k", "v", "o"):
+ base = f"diffusion_model.blocks.{block}.{sub}.{proj}"
+ sd[f"{base}.lora_down.weight"] = _t((64, 5120))
+ sd[f"{base}.lora_up.weight"] = _t((5120, 64))
+ sd[f"{base}.alpha"] = torch.tensor(8.0)
+ for ffn_idx in (0, 2):
+ base = f"diffusion_model.blocks.{block}.ffn.{ffn_idx}"
+ sd[f"{base}.lora_down.weight"] = _t((64, 5120))
+ sd[f"{base}.lora_up.weight"] = _t((5120, 64))
+ sd[f"{base}.alpha"] = torch.tensor(8.0)
+ return sd
+
+
+def _t2v_lightning_v2_keys() -> dict:
+ """Same layout as I2V Lightning — both lightx2v releases use native Wan
+ keys with ``diffusion_model.`` prefix. The T2V version had been working
+ (after a manual factory reorder), but only by luck of dict-hash order."""
+ return _i2v_lightning_v1_keys() # structurally identical to I2V V1
+
+
+def _wan_kohya_keys() -> dict:
+ """Hypothetical Kohya-format Wan LoRA — same native naming, underscore
+ separators. Lightning hasn't shipped in this format, but other community
+ LoRAs do."""
+ sd: dict[str, torch.Tensor] = {}
+ for block in range(2):
+ for sub in ("self_attn", "cross_attn"):
+ for proj in ("q", "k", "v", "o"):
+ base = f"lora_unet_blocks_{block}_{sub}_{proj}"
+ sd[f"{base}.lora_down.weight"] = _t((64, 5120))
+ sd[f"{base}.lora_up.weight"] = _t((5120, 64))
+ return sd
+
+
+def _wan_diffusers_peft_keys() -> dict:
+ """Wan diffusers-style LoRA: ``transformer.blocks.X.attn1.to_q.lora_A.weight``
+ etc. Distinct enough from Anima that even the loose probes wouldn't collide,
+ but covered here for completeness."""
+ sd: dict[str, torch.Tensor] = {}
+ for block in range(2):
+ for attn in ("attn1", "attn2"):
+ for to in ("to_q", "to_k", "to_v"):
+ base = f"transformer.blocks.{block}.{attn}.{to}"
+ sd[f"{base}.lora_A.weight"] = _t((64, 5120))
+ sd[f"{base}.lora_B.weight"] = _t((5120, 64))
+ sd[f"transformer.blocks.{block}.ffn.net.0.proj.lora_A.weight"] = _t((64, 5120))
+ sd[f"transformer.blocks.{block}.ffn.net.0.proj.lora_B.weight"] = _t((13824, 64))
+ return sd
+
+
+def _anima_peft_keys() -> dict:
+ """Realistic Anima Cosmos-DiT LoRA: ``q_proj``/``k_proj`` attention naming
+ plus ``mlp`` and ``adaln_modulation`` modules. Wan has none of these."""
+ sd: dict[str, torch.Tensor] = {}
+ for block in range(2):
+ for sub in ("self_attn", "cross_attn"):
+ for proj in ("q_proj", "k_proj", "v_proj", "output_proj"):
+ base = f"transformer.blocks.{block}.{sub}.{proj}"
+ sd[f"{base}.lora_A.weight"] = _t((64, 4096))
+ sd[f"{base}.lora_B.weight"] = _t((4096, 64))
+ sd[f"transformer.blocks.{block}.mlp.layer1.lora_A.weight"] = _t((64, 4096))
+ sd[f"transformer.blocks.{block}.mlp.layer1.lora_B.weight"] = _t((4096, 64))
+ sd[f"transformer.blocks.{block}.adaln_modulation.linear.lora_A.weight"] = _t((64, 4096))
+ sd[f"transformer.blocks.{block}.adaln_modulation.linear.lora_B.weight"] = _t((4096, 64))
+ return sd
+
+
+def _anima_kohya_keys() -> dict:
+ """Same Anima content in Kohya format."""
+ sd: dict[str, torch.Tensor] = {}
+ for block in range(2):
+ for sub in ("self_attn", "cross_attn"):
+ for proj in ("q_proj", "k_proj", "v_proj", "output_proj"):
+ base = f"lora_unet_blocks_{block}_{sub}_{proj}"
+ sd[f"{base}.lora_down.weight"] = _t((64, 4096))
+ sd[f"{base}.lora_up.weight"] = _t((4096, 64))
+ sd[f"lora_unet_blocks_{block}_mlp_layer1.lora_down.weight"] = _t((64, 4096))
+ sd[f"lora_unet_blocks_{block}_mlp_layer1.lora_up.weight"] = _t((4096, 64))
+ return sd
+
+
+# ---------------------------------------------------------------------------
+# Mutual-exclusivity assertions
+# ---------------------------------------------------------------------------
+
+
+@pytest.mark.parametrize(
+ "label, sd_builder",
+ [
+ ("i2v_lightning_v1", _i2v_lightning_v1_keys),
+ ("t2v_lightning_v2", _t2v_lightning_v2_keys),
+ ("wan_kohya_native", _wan_kohya_keys),
+ ("wan_diffusers_peft", _wan_diffusers_peft_keys),
+ ],
+)
+def test_wan_loras_only_match_wan(label: str, sd_builder) -> None:
+ """Wan probe accepts; Anima probe rejects. Independent of factory order."""
+ sd = sd_builder()
+ with TemporaryDirectory() as tmp:
+ f = Path(tmp) / f"{label}.safetensors"
+ f.touch()
+
+ wan_ok, wan_result = _probe(LoRA_LyCORIS_Wan_Config, f, sd, label)
+ anima_ok, anima_result = _probe(LoRA_LyCORIS_Anima_Config, f, sd, label)
+
+ assert wan_ok, f"Wan probe must accept {label}; got {wan_result}"
+ assert wan_result.base == BaseModelType.Wan
+ assert not anima_ok, (
+ f"Anima probe must reject {label} so probing is order-independent. Instead it accepted: {anima_result}"
+ )
+
+
+@pytest.mark.parametrize(
+ "label, sd_builder",
+ [
+ ("anima_peft", _anima_peft_keys),
+ ("anima_kohya", _anima_kohya_keys),
+ ],
+)
+def test_anima_loras_only_match_anima(label: str, sd_builder) -> None:
+ """Anima probe accepts; Wan probe rejects. Independent of factory order."""
+ sd = sd_builder()
+ with TemporaryDirectory() as tmp:
+ f = Path(tmp) / f"{label}.safetensors"
+ f.touch()
+
+ wan_ok, wan_result = _probe(LoRA_LyCORIS_Wan_Config, f, sd, label)
+ anima_ok, anima_result = _probe(LoRA_LyCORIS_Anima_Config, f, sd, label)
+
+ assert anima_ok, f"Anima probe must accept {label}; got {anima_result}"
+ assert anima_result.base == BaseModelType.Anima
+ assert not wan_ok, (
+ f"Wan probe must reject {label} so probing is order-independent. Instead it accepted: {wan_result}"
+ )
+
+
+# ---------------------------------------------------------------------------
+# Belt-and-suspenders: confirm CONFIG_CLASSES doesn't ALSO produce a match for
+# any unrelated LoRA config. This is the test that would have caught the
+# original bug regardless of which LoRA configs are registered in the future.
+# ---------------------------------------------------------------------------
+
+
+def test_at_most_one_lora_config_matches_wan_lightning() -> None:
+ """Run every LoRA config in the factory against an I2V Lightning state
+ dict. Only one should accept. If a future LoRA config (a hypothetical
+ new model with cross_attn naming) starts matching too, this test fires
+ so we can tighten that probe rather than relying on factory ordering."""
+ from invokeai.backend.model_manager.configs.base import Config_Base
+ from invokeai.backend.model_manager.taxonomy import ModelType
+
+ sd = _i2v_lightning_v1_keys()
+ with TemporaryDirectory() as tmp:
+ f = Path(tmp) / "wan_lightning.safetensors"
+ f.touch()
+ mod = _make_mod(f, sd)
+ overrides = _overrides(f, "wan_lightning")
+
+ accepting: list[str] = []
+ for cls in Config_Base.CONFIG_CLASSES:
+ # Only LoRA configs are at risk of collision with each other; skip
+ # the rest. (Main models can also probe-accept-then-reject on type
+ # mismatch, but they're disambiguated by ``matches_sort_key``.)
+ if getattr(cls.model_fields.get("type", None), "default", None) != ModelType.LoRA:
+ continue
+ try:
+ cls.from_model_on_disk(mod, dict(overrides))
+ accepting.append(cls.__name__)
+ except (NotAMatchError, Exception):
+ continue
+
+ assert accepting == ["LoRA_LyCORIS_Wan_Config"], (
+ f"Exactly one LoRA config must accept a Wan Lightning LoRA; got {accepting}. "
+ "If a new LoRA config starts matching here, tighten its probe to be "
+ "mutually exclusive with Wan rather than relying on factory ordering."
+ )
+
+
+def test_at_most_one_lora_config_matches_anima_peft() -> None:
+ """Same exclusivity guarantee for the Anima side."""
+ from invokeai.backend.model_manager.configs.base import Config_Base
+ from invokeai.backend.model_manager.taxonomy import ModelType
+
+ sd = _anima_peft_keys()
+ with TemporaryDirectory() as tmp:
+ f = Path(tmp) / "anima_peft.safetensors"
+ f.touch()
+ mod = _make_mod(f, sd)
+ overrides = _overrides(f, "anima_peft")
+
+ accepting: list[str] = []
+ for cls in Config_Base.CONFIG_CLASSES:
+ if getattr(cls.model_fields.get("type", None), "default", None) != ModelType.LoRA:
+ continue
+ try:
+ cls.from_model_on_disk(mod, dict(overrides))
+ accepting.append(cls.__name__)
+ except (NotAMatchError, Exception):
+ continue
+
+ assert accepting == ["LoRA_LyCORIS_Anima_Config"], (
+ f"Exactly one LoRA config must accept an Anima LoRA; got {accepting}."
+ )
diff --git a/tests/backend/model_manager/configs/test_wan_main_config.py b/tests/backend/model_manager/configs/test_wan_main_config.py
new file mode 100644
index 00000000000..d3f4f00451e
--- /dev/null
+++ b/tests/backend/model_manager/configs/test_wan_main_config.py
@@ -0,0 +1,152 @@
+"""Tests for Wan 2.2 model identification (Main_Diffusers_Wan_Config)."""
+
+import json
+from pathlib import Path
+from tempfile import TemporaryDirectory
+from unittest.mock import MagicMock
+
+import pytest
+
+from invokeai.backend.model_manager.configs.main import Main_Diffusers_Wan_Config
+from invokeai.backend.model_manager.taxonomy import BaseModelType, ModelFormat, WanVariantType
+
+
+def _write_json(path: Path, data: dict) -> None:
+ path.parent.mkdir(parents=True, exist_ok=True)
+ with path.open("w") as f:
+ json.dump(data, f)
+
+
+def _build_a14b_layout(root: Path) -> None:
+ """Synthetic on-disk layout for Wan-AI/Wan2.2-T2V-A14B: dual transformers, z_dim=16."""
+ _write_json(root / "model_index.json", {"_class_name": "WanPipeline"})
+ _write_json(root / "transformer" / "config.json", {"_class_name": "WanTransformer3DModel", "in_channels": 16})
+ _write_json(root / "transformer_2" / "config.json", {"_class_name": "WanTransformer3DModel", "in_channels": 16})
+ _write_json(root / "vae" / "config.json", {"_class_name": "AutoencoderKLWan", "z_dim": 16})
+
+
+def _build_ti2v_5b_layout(root: Path) -> None:
+ """Synthetic on-disk layout for Wan-AI/Wan2.2-TI2V-5B: single transformer, z_dim=48."""
+ _write_json(root / "model_index.json", {"_class_name": "WanImageToVideoPipeline"})
+ _write_json(root / "transformer" / "config.json", {"_class_name": "WanTransformer3DModel", "in_channels": 48})
+ _write_json(root / "vae" / "config.json", {"_class_name": "AutoencoderKLWan", "z_dim": 48})
+
+
+def _build_i2v_a14b_layout(root: Path) -> None:
+ """Wan-AI/Wan2.2-I2V-A14B: dual transformers, z_dim=16, transformer in_channels=36.
+
+ The Wan 2.2 I2V transformer concatenates noise latents (16) + ref-image
+ latents (16) + first-frame mask (4) along the channel dim, so its
+ ``in_channels`` is 36 vs 16 for T2V.
+ """
+ _write_json(root / "model_index.json", {"_class_name": "WanImageToVideoPipeline"})
+ _write_json(
+ root / "transformer" / "config.json",
+ {"_class_name": "WanTransformer3DModel", "in_channels": 36, "image_dim": None},
+ )
+ _write_json(
+ root / "transformer_2" / "config.json",
+ {"_class_name": "WanTransformer3DModel", "in_channels": 36, "image_dim": None},
+ )
+ _write_json(root / "vae" / "config.json", {"_class_name": "AutoencoderKLWan", "z_dim": 16})
+
+
+def _build_overrides(model_path: Path, name: str) -> dict:
+ return {
+ "hash": "test-hash",
+ "path": str(model_path),
+ "file_size": 0,
+ "name": name,
+ "source": str(model_path),
+ "source_type": "path",
+ }
+
+
+def _make_mod(model_path: Path) -> MagicMock:
+ mod = MagicMock()
+ mod.path = model_path
+ return mod
+
+
+class TestWanDiffusersIdentification:
+ """Wan diffusers probe: variant detection from transformer / VAE / dir layout."""
+
+ def test_a14b_detected_from_dual_transformer(self) -> None:
+ with TemporaryDirectory() as tmp:
+ root = Path(tmp) / "Wan2.2-T2V-A14B"
+ _build_a14b_layout(root)
+
+ cfg = Main_Diffusers_Wan_Config.from_model_on_disk(_make_mod(root), _build_overrides(root, "A14B"))
+
+ assert cfg.base == BaseModelType.Wan
+ assert cfg.format == ModelFormat.Diffusers
+ assert cfg.variant == WanVariantType.T2V_A14B
+ assert cfg.has_dual_expert is True
+
+ def test_i2v_a14b_detected_from_in_channels_36(self) -> None:
+ """I2V-A14B has the same dual-expert + z_dim=16 layout as T2V, but its
+ transformer's ``in_channels`` is 36 (16 noise + 16 ref-image latents +
+ 4 first-frame mask). That's the canonical Wan 2.2 differentiator."""
+ with TemporaryDirectory() as tmp:
+ root = Path(tmp) / "Wan2.2-I2V-A14B"
+ _build_i2v_a14b_layout(root)
+
+ cfg = Main_Diffusers_Wan_Config.from_model_on_disk(_make_mod(root), _build_overrides(root, "I2V"))
+
+ assert cfg.variant == WanVariantType.I2V_A14B
+ assert cfg.has_dual_expert is True
+
+ def test_t2v_a14b_kept_when_in_channels_is_16(self) -> None:
+ """A14B layout with ``in_channels=16`` resolves to T2V (not I2V)."""
+ with TemporaryDirectory() as tmp:
+ root = Path(tmp) / "Wan2.2-T2V-A14B"
+ _build_a14b_layout(root)
+
+ cfg = Main_Diffusers_Wan_Config.from_model_on_disk(_make_mod(root), _build_overrides(root, "T2V"))
+
+ assert cfg.variant == WanVariantType.T2V_A14B
+
+ def test_ti2v_5b_detected_from_z_dim(self) -> None:
+ with TemporaryDirectory() as tmp:
+ root = Path(tmp) / "Wan2.2-TI2V-5B"
+ _build_ti2v_5b_layout(root)
+
+ cfg = Main_Diffusers_Wan_Config.from_model_on_disk(_make_mod(root), _build_overrides(root, "TI2V-5B"))
+
+ assert cfg.variant == WanVariantType.TI2V_5B
+ assert cfg.has_dual_expert is False
+
+ def test_filename_heuristic_when_vae_config_missing(self) -> None:
+ """When ``vae/config.json`` is missing, fall back to the directory name."""
+ with TemporaryDirectory() as tmp:
+ root = Path(tmp) / "Wan2.2-TI2V-5B"
+ _write_json(root / "model_index.json", {"_class_name": "WanPipeline"})
+ _write_json(root / "transformer" / "config.json", {"_class_name": "WanTransformer3DModel"})
+ # No vae/config.json — single-transformer + dirname containing "5b" → TI2V-5B.
+
+ cfg = Main_Diffusers_Wan_Config.from_model_on_disk(_make_mod(root), _build_overrides(root, "TI2V-5B"))
+
+ assert cfg.variant == WanVariantType.TI2V_5B
+
+ def test_explicit_variant_override_takes_precedence(self) -> None:
+ with TemporaryDirectory() as tmp:
+ root = Path(tmp) / "wan-something"
+ _build_a14b_layout(root)
+ overrides = _build_overrides(root, "Custom A14B")
+ overrides["variant"] = WanVariantType.TI2V_5B # Explicit override.
+
+ cfg = Main_Diffusers_Wan_Config.from_model_on_disk(_make_mod(root), overrides)
+ assert cfg.variant == WanVariantType.TI2V_5B
+ # has_dual_expert is still detected from disk; the override only forces variant.
+ assert cfg.has_dual_expert is True
+
+ def test_rejects_non_wan_pipeline(self) -> None:
+ """A model_index.json that isn't a Wan class name must not match."""
+ from invokeai.backend.model_manager.configs.identification_utils import NotAMatchError
+
+ with TemporaryDirectory() as tmp:
+ root = Path(tmp) / "not-wan"
+ _write_json(root / "model_index.json", {"_class_name": "FluxPipeline"})
+
+ with pytest.raises(NotAMatchError):
+ Main_Diffusers_Wan_Config.from_model_on_disk(_make_mod(root), _build_overrides(root, "fake"))
diff --git a/tests/backend/model_manager/configs/test_wan_t5_encoder_config.py b/tests/backend/model_manager/configs/test_wan_t5_encoder_config.py
new file mode 100644
index 00000000000..2755f0ebc76
--- /dev/null
+++ b/tests/backend/model_manager/configs/test_wan_t5_encoder_config.py
@@ -0,0 +1,109 @@
+"""Tests for the WanT5Encoder config probe (UMT5-XXL diffusers folder)."""
+
+import json
+from pathlib import Path
+from tempfile import TemporaryDirectory
+from unittest.mock import MagicMock
+
+import pytest
+
+from invokeai.backend.model_manager.configs.identification_utils import NotAMatchError
+from invokeai.backend.model_manager.configs.wan_t5_encoder import WanT5Encoder_WanT5Encoder_Config
+from invokeai.backend.model_manager.taxonomy import BaseModelType, ModelFormat, ModelType
+
+
+def _build_overrides(model_path: Path, name: str) -> dict:
+ return {
+ "hash": "test-hash",
+ "path": str(model_path),
+ "file_size": 0,
+ "name": name,
+ "source": str(model_path),
+ "source_type": "path",
+ }
+
+
+def _make_mod(model_path: Path) -> MagicMock:
+ mod = MagicMock()
+ mod.path = model_path
+ return mod
+
+
+def _write_encoder_config(target: Path, model_type: str) -> None:
+ target.parent.mkdir(parents=True, exist_ok=True)
+ with target.open("w") as f:
+ json.dump({"model_type": model_type, "architectures": ["UMT5EncoderModel"]}, f)
+
+
+class TestWanT5EncoderProbe:
+ def test_accepts_nested_text_encoder_layout(self):
+ """Standard layout: /text_encoder/config.json with model_type=umt5."""
+ with TemporaryDirectory() as tmp:
+ root = Path(tmp) / "wan-encoder-bundle"
+ root.mkdir()
+ _write_encoder_config(root / "text_encoder" / "config.json", "umt5")
+
+ cfg = WanT5Encoder_WanT5Encoder_Config.from_model_on_disk(
+ _make_mod(root), _build_overrides(root, "wan-encoder")
+ )
+
+ assert cfg.base == BaseModelType.Any
+ assert cfg.type == ModelType.WanT5Encoder
+ assert cfg.format == ModelFormat.WanT5Encoder
+
+ def test_accepts_flat_encoder_layout(self):
+ """Flat layout: /config.json directly (just the encoder folder)."""
+ with TemporaryDirectory() as tmp:
+ root = Path(tmp) / "umt5-xxl"
+ root.mkdir()
+ _write_encoder_config(root / "config.json", "umt5")
+
+ cfg = WanT5Encoder_WanT5Encoder_Config.from_model_on_disk(
+ _make_mod(root), _build_overrides(root, "umt5-xxl")
+ )
+ assert cfg.format == ModelFormat.WanT5Encoder
+
+ def test_rejects_t5(self):
+ """A regular T5-XXL encoder must not match (different vocabulary)."""
+ with TemporaryDirectory() as tmp:
+ root = Path(tmp) / "t5-xxl"
+ root.mkdir()
+ _write_encoder_config(root / "config.json", "t5")
+
+ with pytest.raises(NotAMatchError, match="not 'umt5'"):
+ WanT5Encoder_WanT5Encoder_Config.from_model_on_disk(_make_mod(root), _build_overrides(root, "t5-xxl"))
+
+ def test_rejects_full_pipeline(self):
+ """A folder with model_index.json or transformer/ is a full pipeline, not an encoder."""
+ with TemporaryDirectory() as tmp:
+ root = Path(tmp) / "full-pipeline"
+ root.mkdir()
+ _write_encoder_config(root / "text_encoder" / "config.json", "umt5")
+ (root / "model_index.json").touch()
+
+ with pytest.raises(NotAMatchError, match="full Wan pipeline"):
+ WanT5Encoder_WanT5Encoder_Config.from_model_on_disk(
+ _make_mod(root), _build_overrides(root, "full-pipeline")
+ )
+
+ def test_rejects_missing_config(self):
+ """Empty directory has no encoder config to read."""
+ with TemporaryDirectory() as tmp:
+ root = Path(tmp) / "empty"
+ root.mkdir()
+
+ with pytest.raises(NotAMatchError, match="no encoder config"):
+ WanT5Encoder_WanT5Encoder_Config.from_model_on_disk(_make_mod(root), _build_overrides(root, "empty"))
+
+
+class TestWanT5EncoderCpuOnly:
+ """The standalone UMT5 encoder must support the cpu_only placement setting like every
+ other standalone text-encoder config (the loader honors any config with the field)."""
+
+ def test_cpu_only_defaults_to_unset(self):
+ config = WanT5Encoder_WanT5Encoder_Config(**_build_overrides(Path("/tmp/x"), "wan-encoder"))
+ assert config.cpu_only is None
+
+ def test_cpu_only_round_trips_through_serialization(self):
+ config = WanT5Encoder_WanT5Encoder_Config(**_build_overrides(Path("/tmp/x"), "wan-encoder"), cpu_only=True)
+ assert WanT5Encoder_WanT5Encoder_Config.model_validate(config.model_dump()).cpu_only is True
diff --git a/tests/backend/model_manager/configs/test_wan_vae_config.py b/tests/backend/model_manager/configs/test_wan_vae_config.py
new file mode 100644
index 00000000000..21c3f42a7b8
--- /dev/null
+++ b/tests/backend/model_manager/configs/test_wan_vae_config.py
@@ -0,0 +1,173 @@
+"""Tests for Wan 2.2 VAE config probes (checkpoint + diffusers)."""
+
+import json
+from pathlib import Path
+from tempfile import TemporaryDirectory
+from unittest.mock import MagicMock
+
+import pytest
+import torch
+
+from invokeai.backend.model_manager.configs.identification_utils import NotAMatchError
+from invokeai.backend.model_manager.configs.vae import (
+ VAE_Checkpoint_QwenImage_Config,
+ VAE_Checkpoint_Wan_Config,
+ VAE_Diffusers_Wan_Config,
+ _wan_vae_z_dim,
+)
+from invokeai.backend.model_manager.taxonomy import BaseModelType, ModelFormat
+
+
+def _build_overrides(model_path: Path, name: str) -> dict:
+ return {
+ "hash": "test-hash",
+ "path": str(model_path),
+ "file_size": 0,
+ "name": name,
+ "source": str(model_path),
+ "source_type": "path",
+ }
+
+
+def _make_mod(model_path: Path, state_dict: dict | None = None) -> MagicMock:
+ mod = MagicMock()
+ mod.path = model_path
+ if state_dict is not None:
+ mod.load_state_dict.return_value = state_dict
+ return mod
+
+
+def _wan_vae_state_dict(z_dim: int) -> dict:
+ """Synthetic 5D Wan-style VAE state dict."""
+ return {
+ "decoder.conv_in.weight": torch.zeros(96, z_dim, 1, 3, 3),
+ "encoder.conv_in.weight": torch.zeros(z_dim, 3, 1, 3, 3),
+ }
+
+
+class TestZDimDetection:
+ def test_detects_16_channel(self):
+ assert _wan_vae_z_dim(_wan_vae_state_dict(16)) == 16
+
+ def test_detects_48_channel(self):
+ assert _wan_vae_z_dim(_wan_vae_state_dict(48)) == 48
+
+ def test_rejects_unknown_z_dim(self):
+ # Some other 5D conv weight (not Wan).
+ sd = {"decoder.conv_in.weight": torch.zeros(96, 32, 1, 3, 3)}
+ assert _wan_vae_z_dim(sd) is None
+
+ def test_rejects_4d_conv(self):
+ # Standard SD/SDXL 4D conv — not Wan.
+ sd = {"decoder.conv_in.weight": torch.zeros(96, 16, 3, 3)}
+ assert _wan_vae_z_dim(sd) is None
+
+
+class TestVAECheckpointWanConfig:
+ """Probe + filename-heuristic disambiguation from Qwen Image VAE."""
+
+ def test_48_channel_unambiguous_wan(self):
+ with TemporaryDirectory() as tmp:
+ vae_path = Path(tmp) / "wan2.2-vae.safetensors"
+ vae_path.touch()
+
+ cfg = VAE_Checkpoint_Wan_Config.from_model_on_disk(
+ _make_mod(vae_path, state_dict=_wan_vae_state_dict(48)),
+ _build_overrides(vae_path, "Wan2.2-VAE"),
+ )
+
+ assert cfg.base == BaseModelType.Wan
+ assert cfg.format == ModelFormat.Checkpoint
+ assert cfg.latent_channels == 48
+
+ def test_16_channel_with_wan_in_filename(self):
+ with TemporaryDirectory() as tmp:
+ vae_path = Path(tmp) / "wan-vae.safetensors"
+ vae_path.touch()
+
+ cfg = VAE_Checkpoint_Wan_Config.from_model_on_disk(
+ _make_mod(vae_path, state_dict=_wan_vae_state_dict(16)),
+ _build_overrides(vae_path, "Wan VAE"),
+ )
+
+ assert cfg.latent_channels == 16
+
+ def test_16_channel_without_wan_in_filename_defers(self):
+ """Filename without 'wan' should let Qwen Image VAE win."""
+ with TemporaryDirectory() as tmp:
+ vae_path = Path(tmp) / "qwen_vae.safetensors"
+ vae_path.touch()
+
+ with pytest.raises(NotAMatchError, match="deferring to Qwen Image"):
+ VAE_Checkpoint_Wan_Config.from_model_on_disk(
+ _make_mod(vae_path, state_dict=_wan_vae_state_dict(16)),
+ _build_overrides(vae_path, "QwenImage VAE"),
+ )
+
+ def test_qwen_image_defers_when_filename_says_wan(self):
+ """The mirror case — QwenImage config refuses files whose filenames suggest Wan."""
+ with TemporaryDirectory() as tmp:
+ vae_path = Path(tmp) / "wan-vae.safetensors"
+ vae_path.touch()
+
+ with pytest.raises(NotAMatchError, match="filename suggests a Wan"):
+ VAE_Checkpoint_QwenImage_Config.from_model_on_disk(
+ _make_mod(vae_path, state_dict=_wan_vae_state_dict(16)),
+ _build_overrides(vae_path, "Wan VAE"),
+ )
+
+ def test_rejects_non_wan_state_dict(self):
+ with TemporaryDirectory() as tmp:
+ vae_path = Path(tmp) / "wan-junk.safetensors"
+ vae_path.touch()
+ sd = {"foo.bar": torch.zeros(1)}
+
+ with pytest.raises(NotAMatchError):
+ VAE_Checkpoint_Wan_Config.from_model_on_disk(
+ _make_mod(vae_path, state_dict=sd),
+ _build_overrides(vae_path, "junk"),
+ )
+
+
+class TestVAEDiffusersWanConfig:
+ """Diffusers-folder probe; latent_channels read from vae/config.json."""
+
+ def test_z_dim_from_config_json(self):
+ with TemporaryDirectory() as tmp:
+ root = Path(tmp) / "Wan2.2-VAE"
+ root.mkdir()
+ with (root / "config.json").open("w") as f:
+ json.dump({"_class_name": "AutoencoderKLWan", "z_dim": 48}, f)
+
+ cfg = VAE_Diffusers_Wan_Config.from_model_on_disk(
+ _make_mod(root),
+ _build_overrides(root, "Wan2.2-VAE"),
+ )
+ assert cfg.latent_channels == 48
+ assert cfg.format == ModelFormat.Diffusers
+
+ def test_default_to_16_when_z_dim_missing(self):
+ with TemporaryDirectory() as tmp:
+ root = Path(tmp) / "Wan-VAE"
+ root.mkdir()
+ with (root / "config.json").open("w") as f:
+ json.dump({"_class_name": "AutoencoderKLWan"}, f) # No z_dim.
+
+ cfg = VAE_Diffusers_Wan_Config.from_model_on_disk(
+ _make_mod(root),
+ _build_overrides(root, "Wan-VAE"),
+ )
+ assert cfg.latent_channels == 16
+
+ def test_rejects_non_wan_class(self):
+ with TemporaryDirectory() as tmp:
+ root = Path(tmp) / "FluxVAE"
+ root.mkdir()
+ with (root / "config.json").open("w") as f:
+ json.dump({"_class_name": "AutoencoderKL"}, f)
+
+ with pytest.raises(NotAMatchError):
+ VAE_Diffusers_Wan_Config.from_model_on_disk(
+ _make_mod(root),
+ _build_overrides(root, "FluxVAE"),
+ )
diff --git a/tests/backend/model_manager/load/test_wan_loader.py b/tests/backend/model_manager/load/test_wan_loader.py
new file mode 100644
index 00000000000..31d30522446
--- /dev/null
+++ b/tests/backend/model_manager/load/test_wan_loader.py
@@ -0,0 +1,175 @@
+"""Tests for Wan loader helpers (native -> diffusers key conversion)."""
+
+import gguf
+import torch
+
+from invokeai.backend.model_manager.load.model_loaders.wan import (
+ _convert_wan_native_to_diffusers,
+ _unwrap_unquantized_to_compute_dtype,
+)
+from invokeai.backend.quantization.gguf.ggml_tensor import GGMLTensor
+
+
+def test_converts_text_and_time_embedders():
+ sd = {
+ "text_embedding.0.weight": "a",
+ "text_embedding.0.bias": "b",
+ "text_embedding.2.weight": "c",
+ "time_embedding.0.weight": "d",
+ "time_embedding.2.weight": "e",
+ "time_projection.1.weight": "f",
+ }
+ out = _convert_wan_native_to_diffusers(sd)
+ assert "condition_embedder.text_embedder.linear_1.weight" in out
+ assert "condition_embedder.text_embedder.linear_1.bias" in out
+ assert "condition_embedder.text_embedder.linear_2.weight" in out
+ assert "condition_embedder.time_embedder.linear_1.weight" in out
+ assert "condition_embedder.time_embedder.linear_2.weight" in out
+ assert "condition_embedder.time_proj.weight" in out
+
+
+def test_converts_attention_blocks():
+ sd = {
+ "blocks.0.self_attn.q.weight": 1,
+ "blocks.0.self_attn.k.weight": 2,
+ "blocks.0.self_attn.v.weight": 3,
+ "blocks.0.self_attn.o.weight": 4,
+ "blocks.0.self_attn.norm_q.weight": 5,
+ "blocks.0.self_attn.norm_k.weight": 6,
+ "blocks.0.cross_attn.q.weight": 7,
+ "blocks.0.cross_attn.k.weight": 8,
+ "blocks.0.cross_attn.v.weight": 9,
+ "blocks.0.cross_attn.o.weight": 10,
+ }
+ out = _convert_wan_native_to_diffusers(sd)
+ assert "blocks.0.attn1.to_q.weight" in out
+ assert "blocks.0.attn1.to_k.weight" in out
+ assert "blocks.0.attn1.to_v.weight" in out
+ assert "blocks.0.attn1.to_out.0.weight" in out
+ assert "blocks.0.attn1.norm_q.weight" in out
+ assert "blocks.0.attn1.norm_k.weight" in out
+ assert "blocks.0.attn2.to_q.weight" in out
+ assert "blocks.0.attn2.to_out.0.weight" in out
+
+
+def test_converts_ffn_and_modulation():
+ sd = {
+ "blocks.0.ffn.0.weight": 1,
+ "blocks.0.ffn.0.bias": 2,
+ "blocks.0.ffn.2.weight": 3,
+ "blocks.0.modulation": 4,
+ }
+ out = _convert_wan_native_to_diffusers(sd)
+ assert "blocks.0.ffn.net.0.proj.weight" in out
+ assert "blocks.0.ffn.net.0.proj.bias" in out
+ assert "blocks.0.ffn.net.2.weight" in out
+ assert "blocks.0.scale_shift_table" in out
+
+
+def test_swaps_norm2_and_norm3():
+ """Native norm3 has params (cross-attn norm in diffusers norm2 slot)
+ while native norm2 is the elementwise-affine-False norm. The swap
+ via placeholder must not collide."""
+ sd = {
+ "blocks.0.norm2.weight": "native_norm2",
+ "blocks.0.norm3.weight": "native_norm3",
+ }
+ out = _convert_wan_native_to_diffusers(sd)
+ assert out["blocks.0.norm3.weight"] == "native_norm2"
+ assert out["blocks.0.norm2.weight"] == "native_norm3"
+
+
+def test_converts_head_keys():
+ sd = {
+ "head.head.weight": 1,
+ "head.head.bias": 2,
+ "head.modulation": 3,
+ }
+ out = _convert_wan_native_to_diffusers(sd)
+ assert "proj_out.weight" in out
+ assert "proj_out.bias" in out
+ assert "scale_shift_table" in out
+
+
+def test_diffusers_keys_pass_through_unchanged():
+ """If a state dict is already in diffusers form, the substring rules
+ must be no-ops — none of the native fingerprints are present."""
+ sd = {
+ "patch_embedding.weight": 1,
+ "condition_embedder.text_embedder.linear_1.weight": 2,
+ "blocks.0.attn1.to_q.weight": 3,
+ "blocks.0.ffn.net.0.proj.weight": 4,
+ "scale_shift_table": 5,
+ "proj_out.weight": 6,
+ }
+ out = _convert_wan_native_to_diffusers(sd)
+ assert set(out.keys()) == set(sd.keys())
+ assert all(out[k] == sd[k] for k in sd)
+
+
+def test_does_not_mutate_input():
+ sd = {"text_embedding.0.weight": 1}
+ snapshot = dict(sd)
+ _convert_wan_native_to_diffusers(sd)
+ assert sd == snapshot
+
+
+def test_non_string_keys_pass_through():
+ sd = {0: "ignored", "text_embedding.0.weight": "renamed"}
+ out = _convert_wan_native_to_diffusers(sd)
+ assert out[0] == "ignored"
+ assert "condition_embedder.text_embedder.linear_1.weight" in out
+
+
+def _ggml(data: torch.Tensor, qtype: gguf.GGMLQuantizationType, compute_dtype: torch.dtype) -> GGMLTensor:
+ return GGMLTensor(
+ data=data,
+ ggml_quantization_type=qtype,
+ tensor_shape=data.shape,
+ compute_dtype=compute_dtype,
+ )
+
+
+class TestUnwrapUnquantized:
+ """The QuantStack GGUFs store ``patch_embedding.bias`` as F16 while latents
+ flow through the model as bf16. Conv3d isn't in GGMLTensor's dispatch table,
+ so without unwrapping the F16 wrapper goes into conv3d as-is and crashes
+ with ``Input type (c10::BFloat16) and bias type (c10::Half) should be the same``.
+ These tests guard the unwrap step that prevents that."""
+
+ def test_f16_compatible_qtype_is_unwrapped_and_cast(self):
+ # F16 storage that should become bf16 plain tensor.
+ f16_data = torch.zeros((4,), dtype=torch.float16)
+ sd = {"bias": _ggml(f16_data, gguf.GGMLQuantizationType.F16, torch.bfloat16)}
+ out = _unwrap_unquantized_to_compute_dtype(sd)
+
+ result = out["bias"]
+ assert not isinstance(result, GGMLTensor)
+ assert result.dtype == torch.bfloat16
+
+ def test_f32_compatible_qtype_is_unwrapped_and_cast(self):
+ # patch_embedding.weight in QuantStack is F32 — same path.
+ f32_data = torch.zeros((4,), dtype=torch.float32)
+ sd = {"weight": _ggml(f32_data, gguf.GGMLQuantizationType.F32, torch.bfloat16)}
+ out = _unwrap_unquantized_to_compute_dtype(sd)
+
+ result = out["weight"]
+ assert not isinstance(result, GGMLTensor)
+ assert result.dtype == torch.bfloat16
+
+ def test_quantized_tensor_stays_wrapped(self):
+ # Q4_K and friends must remain GGMLTensor so on-demand dequant works
+ # via the linear/addmm dispatch path. The byte storage shape is fake
+ # but irrelevant for this test.
+ q4_data = torch.zeros((1,), dtype=torch.uint8)
+ sd = {"linear.weight": _ggml(q4_data, gguf.GGMLQuantizationType.Q4_K, torch.bfloat16)}
+ out = _unwrap_unquantized_to_compute_dtype(sd)
+
+ assert isinstance(out["linear.weight"], GGMLTensor)
+ assert out["linear.weight"]._ggml_quantization_type == gguf.GGMLQuantizationType.Q4_K
+
+ def test_plain_torch_tensor_passes_through(self):
+ plain = torch.zeros((4,), dtype=torch.bfloat16)
+ sd = {"plain": plain}
+ out = _unwrap_unquantized_to_compute_dtype(sd)
+ assert out["plain"] is plain
diff --git a/tests/backend/model_manager/load/test_wan_vae_loader.py b/tests/backend/model_manager/load/test_wan_vae_loader.py
new file mode 100644
index 00000000000..c06022ead18
--- /dev/null
+++ b/tests/backend/model_manager/load/test_wan_vae_loader.py
@@ -0,0 +1,92 @@
+"""Tests for the Wan VAE single-file loader helper.
+
+Covers the bug where ``AutoencoderKLWan`` was always instantiated with the A14B
+defaults (base_dim=96, out_channels=3, no patchify), causing the TI2V-5B VAE
+checkpoint to fail state_dict loading with shape mismatches throughout the
+encoder + decoder. The fix routes z_dim=48 to the TI2V-5B-specific
+constructor kwargs.
+"""
+
+from unittest.mock import MagicMock, patch
+
+import accelerate
+import torch
+from diffusers.models.autoencoders.autoencoder_kl_wan import AutoencoderKLWan
+
+from invokeai.backend.model_manager.load.model_loaders.vae import VAELoader, _wan_vae_init_kwargs_for
+
+
+def test_a14b_returns_default_z_dim_only() -> None:
+ # The A14B path should still be the trivial case — only z_dim is overridden,
+ # leaving diffusers' defaults (base_dim=96, out_channels=3, etc.) intact.
+ assert _wan_vae_init_kwargs_for(16) == {"z_dim": 16}
+
+
+def test_ti2v_5b_returns_full_architectural_override() -> None:
+ kw = _wan_vae_init_kwargs_for(48)
+ assert kw["z_dim"] == 48
+ assert kw["base_dim"] == 160
+ assert kw["decoder_base_dim"] == 256
+ assert kw["in_channels"] == 12
+ assert kw["out_channels"] == 12
+ assert kw["patch_size"] == 2
+ assert kw["scale_factor_spatial"] == 16
+ assert kw["is_residual"] is True
+ # latents_mean/std need to be 48-vectors so the model can construct.
+ assert len(kw["latents_mean"]) == 48
+ assert len(kw["latents_std"]) == 48
+
+
+def test_ti2v_5b_kwargs_instantiate_with_expected_shapes() -> None:
+ # End-to-end check: the kwargs let AutoencoderKLWan build cleanly and the
+ # resulting model carries the TI2V-5B-shaped layers (z_dim=48, decoder
+ # outputs 12 channels — this is what failed before the fix).
+ with accelerate.init_empty_weights():
+ model = AutoencoderKLWan(**_wan_vae_init_kwargs_for(48))
+ assert model.z_dim == 48
+ assert model.config.base_dim == 160
+ assert model.config.decoder_base_dim == 256
+ assert model.config.out_channels == 12
+ assert model.config.patch_size == 2
+ # decoder.conv_out emits the patchified 12-channel output (3 RGB x 2x2 patch).
+ assert model.decoder.conv_out.weight.shape[0] == 12
+
+
+def test_a14b_kwargs_instantiate_with_expected_shapes() -> None:
+ with accelerate.init_empty_weights():
+ model = AutoencoderKLWan(**_wan_vae_init_kwargs_for(16))
+ assert model.z_dim == 16
+ assert model.config.base_dim == 96
+ assert model.config.out_channels == 3
+ assert model.config.patch_size is None
+
+
+def test_wan_vae_checkpoint_loads_bfloat16_even_when_loader_dtype_is_fp16() -> None:
+ """fp16 is unstable on the Wan VAE (NaN/garbled frames), so the checkpoint path must
+ force bfloat16 like ``_load_wan_vae_diffusers`` and ``WanDiffusersModel`` do — even
+ though ``precision: auto`` resolves the loader's default dtype to float16 on CUDA."""
+ loader = VAELoader.__new__(VAELoader)
+ loader._ram_cache = MagicMock()
+ loader._torch_dtype = torch.float16 # what `precision: auto` yields on CUDA
+
+ sd = {
+ "decoder.weight": torch.zeros(2, 2, dtype=torch.float32),
+ "step_counter": torch.zeros(1, dtype=torch.int64),
+ }
+ config = MagicMock()
+ config.latent_channels = 16
+
+ fake_model = MagicMock()
+ with (
+ patch("safetensors.torch.load_file", return_value=sd),
+ patch("diffusers.models.autoencoders.autoencoder_kl_wan.AutoencoderKLWan", return_value=fake_model),
+ # The real ROCm patch mutates WanCausalConv3d.forward process-wide, which breaks
+ # the conv3d idempotency test when it runs later in the same session.
+ patch("invokeai.backend.wan.rocm_causal_conv3d.patch_wan_causal_conv3d_for_rocm"),
+ ):
+ result = loader._load_wan_vae(config)
+
+ assert result is fake_model
+ loaded_sd = fake_model.load_state_dict.call_args.args[0]
+ assert loaded_sd["decoder.weight"].dtype == torch.bfloat16
+ assert loaded_sd["step_counter"].dtype == torch.int64 # non-float buffers untouched
diff --git a/tests/backend/model_manager/test_wan_default_settings.py b/tests/backend/model_manager/test_wan_default_settings.py
new file mode 100644
index 00000000000..ff66cf4f067
--- /dev/null
+++ b/tests/backend/model_manager/test_wan_default_settings.py
@@ -0,0 +1,25 @@
+"""Tests for Wan 2.2 default settings."""
+
+from invokeai.backend.model_manager.configs.main import MainModelDefaultSettings
+from invokeai.backend.model_manager.taxonomy import BaseModelType, WanVariantType
+
+
+class TestWanDefaultSettings:
+ def test_a14b_defaults(self) -> None:
+ s = MainModelDefaultSettings.from_base(BaseModelType.Wan, WanVariantType.T2V_A14B)
+ assert s is not None
+ assert s.steps == 40
+ assert s.cfg_scale == 4.0
+ assert s.width == 1024
+ assert s.height == 1024
+
+ def test_ti2v_5b_defaults(self) -> None:
+ s = MainModelDefaultSettings.from_base(BaseModelType.Wan, WanVariantType.TI2V_5B)
+ assert s is not None
+ assert s.steps == 30
+ assert s.cfg_scale == 5.0
+
+ def test_no_variant_falls_back_to_a14b_settings(self) -> None:
+ s = MainModelDefaultSettings.from_base(BaseModelType.Wan)
+ assert s is not None
+ assert s.steps == 40
diff --git a/tests/backend/patches/lora_conversions/test_wan_lora_conversion_utils.py b/tests/backend/patches/lora_conversions/test_wan_lora_conversion_utils.py
new file mode 100644
index 00000000000..f9cac4bd61b
--- /dev/null
+++ b/tests/backend/patches/lora_conversions/test_wan_lora_conversion_utils.py
@@ -0,0 +1,175 @@
+"""Tests for Wan LoRA state-dict conversion to ModelPatchRaw."""
+
+import torch
+
+from invokeai.backend.patches.lora_conversions.wan_lora_constants import WAN_LORA_TRANSFORMER_PREFIX
+from invokeai.backend.patches.lora_conversions.wan_lora_conversion_utils import (
+ _kohya_layer_to_diffusers_path,
+ _native_layer_path_to_diffusers,
+ _strip_peft_prefix,
+ lora_model_from_wan_state_dict,
+)
+
+
+def _ab_pair(in_dim: int, out_dim: int, rank: int = 16) -> dict[str, torch.Tensor]:
+ """PEFT-style lora_A (in→rank) + lora_B (rank→out) pair."""
+ return {
+ "lora_A.weight": torch.zeros((rank, in_dim)),
+ "lora_B.weight": torch.zeros((out_dim, rank)),
+ }
+
+
+def _down_up_pair(in_dim: int, out_dim: int, rank: int = 16) -> dict[str, torch.Tensor]:
+ """Kohya-style lora_down + lora_up pair."""
+ return {
+ "lora_down.weight": torch.zeros((rank, in_dim)),
+ "lora_up.weight": torch.zeros((out_dim, rank)),
+ }
+
+
+class TestKohyaLayerToDiffusersPath:
+ def test_diffusers_self_attention(self):
+ assert _kohya_layer_to_diffusers_path("lora_unet_blocks_0_attn1_to_q") == "blocks.0.attn1.to_q"
+ assert _kohya_layer_to_diffusers_path("lora_unet_blocks_5_attn1_to_out_0") == "blocks.5.attn1.to_out.0"
+
+ def test_diffusers_cross_attention(self):
+ assert _kohya_layer_to_diffusers_path("lora_unet_blocks_0_attn2_to_k") == "blocks.0.attn2.to_k"
+ assert _kohya_layer_to_diffusers_path("lora_unet_blocks_0_attn2_to_v") == "blocks.0.attn2.to_v"
+
+ def test_native_self_attention_maps_to_attn1(self):
+ assert _kohya_layer_to_diffusers_path("lora_unet_blocks_0_self_attn_q") == "blocks.0.attn1.to_q"
+ assert _kohya_layer_to_diffusers_path("lora_unet_blocks_0_self_attn_o") == "blocks.0.attn1.to_out.0"
+
+ def test_native_cross_attention_maps_to_attn2(self):
+ assert _kohya_layer_to_diffusers_path("lora_unet_blocks_2_cross_attn_v") == "blocks.2.attn2.to_v"
+
+ def test_ffn_diffusers(self):
+ assert _kohya_layer_to_diffusers_path("lora_unet_blocks_0_ffn_net_0_proj") == "blocks.0.ffn.net.0.proj"
+ assert _kohya_layer_to_diffusers_path("lora_unet_blocks_0_ffn_net_2") == "blocks.0.ffn.net.2"
+
+ def test_ffn_native_maps_to_diffusers(self):
+ assert _kohya_layer_to_diffusers_path("lora_unet_blocks_0_ffn_0") == "blocks.0.ffn.net.0.proj"
+ assert _kohya_layer_to_diffusers_path("lora_unet_blocks_0_ffn_2") == "blocks.0.ffn.net.2"
+
+ def test_unknown_submodule_returns_none(self):
+ assert _kohya_layer_to_diffusers_path("lora_unet_blocks_0_unknown_thing") is None
+
+ def test_non_kohya_returns_none(self):
+ assert _kohya_layer_to_diffusers_path("transformer.blocks.0.attn1.to_q") is None
+
+
+class TestPEFTPathConversion:
+ def test_strip_transformer_prefix(self):
+ assert _strip_peft_prefix("transformer.blocks.0.attn1.to_q") == "blocks.0.attn1.to_q"
+
+ def test_strip_diffusion_model_prefix(self):
+ assert _strip_peft_prefix("diffusion_model.blocks.0.self_attn.q") == "blocks.0.self_attn.q"
+
+ def test_strip_base_model_prefix(self):
+ assert _strip_peft_prefix("base_model.model.transformer.blocks.0.attn1.to_q") == "blocks.0.attn1.to_q"
+
+ def test_no_prefix_unchanged(self):
+ assert _strip_peft_prefix("blocks.0.attn1.to_q") == "blocks.0.attn1.to_q"
+
+ def test_diffusers_path_passes_through(self):
+ assert _native_layer_path_to_diffusers("blocks.0.attn1.to_q") == "blocks.0.attn1.to_q"
+ assert _native_layer_path_to_diffusers("blocks.0.ffn.net.0.proj") == "blocks.0.ffn.net.0.proj"
+
+ def test_native_self_attn_becomes_attn1(self):
+ assert _native_layer_path_to_diffusers("blocks.0.self_attn.q") == "blocks.0.attn1.to_q"
+ assert _native_layer_path_to_diffusers("blocks.0.self_attn.k") == "blocks.0.attn1.to_k"
+ assert _native_layer_path_to_diffusers("blocks.0.self_attn.v") == "blocks.0.attn1.to_v"
+ assert _native_layer_path_to_diffusers("blocks.0.self_attn.o") == "blocks.0.attn1.to_out.0"
+
+ def test_native_cross_attn_becomes_attn2(self):
+ assert _native_layer_path_to_diffusers("blocks.7.cross_attn.q") == "blocks.7.attn2.to_q"
+ assert _native_layer_path_to_diffusers("blocks.7.cross_attn.o") == "blocks.7.attn2.to_out.0"
+
+ def test_native_ffn_becomes_diffusers_ffn(self):
+ assert _native_layer_path_to_diffusers("blocks.0.ffn.0") == "blocks.0.ffn.net.0.proj"
+ assert _native_layer_path_to_diffusers("blocks.0.ffn.2") == "blocks.0.ffn.net.2"
+
+ def test_non_block_path_rejected(self):
+ assert _native_layer_path_to_diffusers("patch_embedding.weight") is None
+
+
+class TestLoRAModelFromStateDict:
+ """End-to-end conversion: state dict -> ModelPatchRaw."""
+
+ def test_diffusers_peft_with_transformer_prefix(self):
+ sd = {f"transformer.blocks.0.attn1.to_q.{k}": v for k, v in _ab_pair(5120, 5120).items()}
+ patch = lora_model_from_wan_state_dict(sd)
+ expected_key = f"{WAN_LORA_TRANSFORMER_PREFIX}blocks.0.attn1.to_q"
+ assert expected_key in patch.layers
+
+ def test_diffusers_peft_bare(self):
+ sd = {f"blocks.5.attn2.to_k.{k}": v for k, v in _ab_pair(5120, 5120).items()}
+ patch = lora_model_from_wan_state_dict(sd)
+ assert f"{WAN_LORA_TRANSFORMER_PREFIX}blocks.5.attn2.to_k" in patch.layers
+
+ def test_native_peft_diffusion_model_prefix(self):
+ sd = {f"diffusion_model.blocks.0.self_attn.q.{k}": v for k, v in _ab_pair(5120, 5120).items()}
+ patch = lora_model_from_wan_state_dict(sd)
+ # native self_attn.q must be rewritten to attn1.to_q
+ assert f"{WAN_LORA_TRANSFORMER_PREFIX}blocks.0.attn1.to_q" in patch.layers
+
+ def test_native_peft_cross_attn_to_attn2(self):
+ sd = {f"diffusion_model.blocks.3.cross_attn.o.{k}": v for k, v in _ab_pair(5120, 5120).items()}
+ patch = lora_model_from_wan_state_dict(sd)
+ assert f"{WAN_LORA_TRANSFORMER_PREFIX}blocks.3.attn2.to_out.0" in patch.layers
+
+ def test_native_peft_ffn_to_diffusers(self):
+ sd = {f"diffusion_model.blocks.0.ffn.0.{k}": v for k, v in _ab_pair(5120, 13824).items()}
+ patch = lora_model_from_wan_state_dict(sd)
+ assert f"{WAN_LORA_TRANSFORMER_PREFIX}blocks.0.ffn.net.0.proj" in patch.layers
+
+ def test_kohya_diffusers_naming(self):
+ sd = {f"lora_unet_blocks_0_attn1_to_q.{k}": v for k, v in _down_up_pair(5120, 5120).items()}
+ patch = lora_model_from_wan_state_dict(sd)
+ assert f"{WAN_LORA_TRANSFORMER_PREFIX}blocks.0.attn1.to_q" in patch.layers
+
+ def test_kohya_native_naming(self):
+ sd = {f"lora_unet_blocks_0_self_attn_q.{k}": v for k, v in _down_up_pair(5120, 5120).items()}
+ patch = lora_model_from_wan_state_dict(sd)
+ assert f"{WAN_LORA_TRANSFORMER_PREFIX}blocks.0.attn1.to_q" in patch.layers
+
+ def test_kohya_ffn_native_naming(self):
+ sd = {f"lora_unet_blocks_0_ffn_0.{k}": v for k, v in _down_up_pair(5120, 13824).items()}
+ patch = lora_model_from_wan_state_dict(sd)
+ assert f"{WAN_LORA_TRANSFORMER_PREFIX}blocks.0.ffn.net.0.proj" in patch.layers
+
+ def test_multiple_layers(self):
+ """Cover a realistic mix of attn + ffn keys across multiple blocks."""
+ sd = {}
+ for block in range(3):
+ for k, v in _ab_pair(5120, 5120).items():
+ sd[f"transformer.blocks.{block}.attn1.to_q.{k}"] = v
+ sd[f"transformer.blocks.{block}.attn2.to_v.{k}"] = v
+ for k, v in _ab_pair(5120, 13824).items():
+ sd[f"transformer.blocks.{block}.ffn.net.0.proj.{k}"] = v
+
+ patch = lora_model_from_wan_state_dict(sd)
+ expected_paths = []
+ for block in range(3):
+ expected_paths.append(f"blocks.{block}.attn1.to_q")
+ expected_paths.append(f"blocks.{block}.attn2.to_v")
+ expected_paths.append(f"blocks.{block}.ffn.net.0.proj")
+ for path in expected_paths:
+ assert f"{WAN_LORA_TRANSFORMER_PREFIX}{path}" in patch.layers
+
+ def test_alpha_override_propagates(self):
+ sd = {f"blocks.0.attn1.to_q.{k}": v for k, v in _ab_pair(5120, 5120).items()}
+ patch = lora_model_from_wan_state_dict(sd, alpha=8.0)
+ layer = patch.layers[f"{WAN_LORA_TRANSFORMER_PREFIX}blocks.0.attn1.to_q"]
+ # any_lora_layer_from_state_dict picks LoRALayer / LoKR / etc. — the
+ # layer object should at minimum have processed the alpha into its state.
+ assert layer is not None
+
+ def test_unknown_kohya_submodule_is_skipped_silently(self):
+ sd = {f"lora_unet_blocks_0_unknown_thing.{k}": v for k, v in _down_up_pair(5120, 5120).items()}
+ patch = lora_model_from_wan_state_dict(sd)
+ assert len(patch.layers) == 0
+
+ def test_empty_state_dict(self):
+ patch = lora_model_from_wan_state_dict({})
+ assert len(patch.layers) == 0
diff --git a/tests/backend/wan/__init__.py b/tests/backend/wan/__init__.py
new file mode 100644
index 00000000000..e69de29bb2d
diff --git a/tests/backend/wan/test_rocm_causal_conv3d.py b/tests/backend/wan/test_rocm_causal_conv3d.py
new file mode 100644
index 00000000000..9bb533706dd
--- /dev/null
+++ b/tests/backend/wan/test_rocm_causal_conv3d.py
@@ -0,0 +1,89 @@
+"""Tests for the ROCm WanCausalConv3d conv2d decomposition.
+
+The decomposition replaces MIOpen's Im3d2Col conv3d fallback (61% of Wan VAE
+decode GPU time on RDNA3; ~48x slower than the decomposed path). These tests pin
+that the decomposed forward is numerically equivalent to the stock diffusers
+forward on CPU — including the causal feature-cache path — so the ROCm-gated
+class patch can never change results, only speed.
+"""
+
+import pytest
+import torch
+
+from invokeai.backend.wan.rocm_causal_conv3d import (
+ _decomposed_conv3d,
+ _decomposed_forward,
+ _patch_wan_causal_conv3d,
+)
+
+diffusers = pytest.importorskip("diffusers")
+from diffusers.models.autoencoders.autoencoder_kl_wan import WanCausalConv3d # noqa: E402
+
+
+@pytest.mark.parametrize(
+ "kernel_size",
+ [(3, 3, 3), (1, 1, 1), (3, 1, 1), (1, 3, 3)],
+ ids=["3x3x3", "1x1x1", "temporal-only", "spatial-only"],
+)
+def test_decomposed_conv3d_matches_f_conv3d(kernel_size: tuple[int, int, int]) -> None:
+ torch.manual_seed(0)
+ conv = torch.nn.Conv3d(6, 10, kernel_size=kernel_size)
+ x = torch.randn(2, 6, 5, 12, 16)
+ ref = torch.nn.functional.conv3d(x, conv.weight, conv.bias)
+ got = _decomposed_conv3d(conv, x)
+ assert torch.allclose(ref, got, atol=1e-5)
+
+
+def test_decomposed_forward_matches_stock_wan_causal_conv3d() -> None:
+ torch.manual_seed(1)
+ conv = WanCausalConv3d(4, 8, kernel_size=3, padding=1)
+ x = torch.randn(1, 4, 5, 10, 14)
+ ref = WanCausalConv3d.forward(conv, x)
+ got = _decomposed_forward(conv, x)
+ assert ref.shape == got.shape
+ assert torch.allclose(ref, got, atol=1e-5)
+
+
+def test_decomposed_forward_matches_stock_with_feature_cache() -> None:
+ """The VAE's frame-by-frame decode passes cached trailing frames as cache_x;
+ the decomposition must reproduce the stock causal-cache arithmetic exactly."""
+ torch.manual_seed(2)
+ conv = WanCausalConv3d(4, 8, kernel_size=3, padding=1)
+ x = torch.randn(1, 4, 4, 10, 14)
+ cache = torch.randn(1, 4, 2, 10, 14)
+ ref = WanCausalConv3d.forward(conv, x, cache_x=cache)
+ got = _decomposed_forward(conv, x, cache_x=cache)
+ assert torch.allclose(ref, got, atol=1e-5)
+
+
+def test_decomposed_forward_falls_back_to_conv3d_for_strided_convs() -> None:
+ """Encoder downsample convs are strided; the temporal taps couple under stride,
+ so those must go through F.conv3d untouched."""
+ torch.manual_seed(3)
+ conv = WanCausalConv3d(4, 8, kernel_size=3, stride=(1, 2, 2), padding=1)
+ x = torch.randn(1, 4, 5, 12, 16)
+ ref = WanCausalConv3d.forward(conv, x)
+ got = _decomposed_forward(conv, x)
+ assert ref.shape == got.shape
+ assert torch.allclose(ref, got, atol=1e-5)
+
+
+def test_class_patch_is_idempotent_and_preserves_behavior() -> None:
+ torch.manual_seed(4)
+ stock_forward = WanCausalConv3d.forward
+ try:
+ conv = WanCausalConv3d(4, 8, kernel_size=3, padding=1)
+ x = torch.randn(1, 4, 5, 10, 14)
+ ref = conv(x)
+
+ _patch_wan_causal_conv3d()
+ patched_forward = WanCausalConv3d.forward
+ _patch_wan_causal_conv3d() # second call must be a no-op
+ assert WanCausalConv3d.forward is patched_forward
+ assert WanCausalConv3d.forward is not stock_forward
+
+ assert torch.allclose(conv(x), ref, atol=1e-5)
+ finally:
+ WanCausalConv3d.forward = stock_forward
+ if hasattr(WanCausalConv3d, "_invokeai_rocm_conv2d_decomposition"):
+ delattr(WanCausalConv3d, "_invokeai_rocm_conv2d_decomposition")
diff --git a/tests/backend/wan/test_sampling_utils.py b/tests/backend/wan/test_sampling_utils.py
new file mode 100644
index 00000000000..ec52f357a87
--- /dev/null
+++ b/tests/backend/wan/test_sampling_utils.py
@@ -0,0 +1,91 @@
+"""Tests for Wan 2.2 sampling utilities."""
+
+import torch
+
+from invokeai.backend.model_manager.taxonomy import WanVariantType
+from invokeai.backend.wan.sampling_utils import (
+ get_default_latent_channels,
+ get_spatial_scale_factor,
+ make_noise,
+)
+
+
+class TestVariantConstants:
+ def test_a14b_uses_8x_spatial(self) -> None:
+ assert get_spatial_scale_factor(WanVariantType.T2V_A14B) == 8
+
+ def test_ti2v_5b_uses_16x_spatial(self) -> None:
+ assert get_spatial_scale_factor(WanVariantType.TI2V_5B) == 16
+
+ def test_a14b_default_channels(self) -> None:
+ assert get_default_latent_channels(WanVariantType.T2V_A14B) == 16
+
+ def test_ti2v_5b_default_channels(self) -> None:
+ assert get_default_latent_channels(WanVariantType.TI2V_5B) == 48
+
+
+class TestMakeNoise:
+ def test_a14b_shape_at_1024(self) -> None:
+ noise = make_noise(
+ batch_size=1,
+ latent_channels=16,
+ height=1024,
+ width=1024,
+ spatial_scale_factor=8,
+ device=torch.device("cpu"),
+ dtype=torch.bfloat16,
+ seed=42,
+ )
+ assert noise.shape == (1, 16, 1, 128, 128)
+ assert noise.dtype == torch.bfloat16
+
+ def test_ti2v_shape_at_1024(self) -> None:
+ noise = make_noise(
+ batch_size=1,
+ latent_channels=48,
+ height=1024,
+ width=1024,
+ spatial_scale_factor=16,
+ device=torch.device("cpu"),
+ dtype=torch.bfloat16,
+ seed=42,
+ )
+ assert noise.shape == (1, 48, 1, 64, 64)
+
+ def test_seed_is_deterministic(self) -> None:
+ kwargs = {
+ "batch_size": 1,
+ "latent_channels": 16,
+ "height": 256,
+ "width": 256,
+ "spatial_scale_factor": 8,
+ "device": torch.device("cpu"),
+ "dtype": torch.float32,
+ "seed": 123,
+ }
+ a = make_noise(**kwargs)
+ b = make_noise(**kwargs)
+ assert torch.allclose(a, b)
+
+ def test_seed_changes_output(self) -> None:
+ a = make_noise(
+ batch_size=1,
+ latent_channels=16,
+ height=256,
+ width=256,
+ spatial_scale_factor=8,
+ device=torch.device("cpu"),
+ dtype=torch.float32,
+ seed=1,
+ )
+ b = make_noise(
+ batch_size=1,
+ latent_channels=16,
+ height=256,
+ width=256,
+ spatial_scale_factor=8,
+ device=torch.device("cpu"),
+ dtype=torch.float32,
+ seed=2,
+ )
+ assert not torch.allclose(a, b)
diff --git a/tests/backend/wan/test_wan_ref_image_extension.py b/tests/backend/wan/test_wan_ref_image_extension.py
new file mode 100644
index 00000000000..fda879bb0d8
--- /dev/null
+++ b/tests/backend/wan/test_wan_ref_image_extension.py
@@ -0,0 +1,230 @@
+"""Tests for the Wan 2.2 I2V reference-image VAE-latent encoder helper."""
+
+from unittest.mock import MagicMock
+
+import pytest
+import torch
+from PIL import Image
+
+from invokeai.backend.wan.extensions.wan_ref_image_extension import (
+ encode_reference_image_to_condition,
+ encode_reference_image_to_video_condition,
+ preprocess_reference_image,
+)
+
+
+def _make_fake_vae(z_dim: int = 16, spatial_scale: int = 8, temporal_scale: int = 4) -> MagicMock:
+ """Stand-in for ``AutoencoderKLWan`` that returns deterministic latents.
+
+ ``encode(pixel)`` returns a fake distribution whose ``sample()`` yields
+ a tensor sized exactly as the real Wan VAE would: ``[B, z_dim, T_lat, H/8, W/8]``.
+ """
+ vae = MagicMock()
+
+ # ``next(iter(vae.parameters())).dtype`` is queried; pin to float32.
+ param = torch.zeros(1, dtype=torch.float32)
+ vae.parameters = MagicMock(return_value=iter([param]))
+
+ # Config carries per-channel normalisation stats.
+ vae.config = MagicMock()
+ vae.config.latents_mean = [0.0] * z_dim
+ vae.config.latents_std = [1.0] * z_dim
+
+ def fake_encode(pixel: torch.Tensor, return_dict: bool = False):
+ b, _, t, h, w = pixel.shape
+ t_lat = (t - 1) // temporal_scale + 1
+ h_lat = h // spatial_scale
+ w_lat = w // spatial_scale
+ latents = torch.zeros(b, z_dim, t_lat, h_lat, w_lat, dtype=pixel.dtype)
+
+ dist = MagicMock()
+ dist.sample = MagicMock(return_value=latents)
+ # The pipeline does ``vae.encode(...)[0]`` for non-dict returns.
+ return (dist,) if return_dict is False else MagicMock(latent_dist=dist)
+
+ vae.encode = fake_encode
+ return vae
+
+
+class TestPreprocess:
+ def test_resize_to_target_dims(self):
+ img = Image.new("RGB", (200, 300), (128, 128, 128))
+ out = preprocess_reference_image(img, width=64, height=64)
+ # Shape: [batch=1, channels=3, time=1, H, W]
+ assert out.shape == (1, 3, 1, 64, 64)
+
+ def test_normalised_to_minus_one_to_one(self):
+ # Pure-grey image preprocessed should be exactly 0 (since 128/255*2 - 1 ≈ 0.004).
+ img = Image.new("RGB", (64, 64), (255, 255, 255))
+ out = preprocess_reference_image(img, width=64, height=64)
+ # White → 1.0
+ assert torch.allclose(out, torch.ones_like(out), atol=1e-4)
+
+ black = Image.new("RGB", (64, 64), (0, 0, 0))
+ out_b = preprocess_reference_image(black, width=64, height=64)
+ # Black → -1.0
+ assert torch.allclose(out_b, -torch.ones_like(out_b), atol=1e-4)
+
+ def test_rejects_non_multiple_of_8(self):
+ img = Image.new("RGB", (100, 100))
+ import pytest
+
+ with pytest.raises(ValueError, match="multiples of 8"):
+ preprocess_reference_image(img, width=65, height=64)
+
+
+class TestEncodeReferenceImageToCondition:
+ """The condition tensor must be 20-channel (4 mask + 16 image latents)
+ and shaped for the denoise step's later concat with 16-ch noise latents."""
+
+ def test_shape_at_64x64(self):
+ img = Image.new("RGB", (64, 64))
+ vae = _make_fake_vae()
+ cond = encode_reference_image_to_condition(
+ image=img, vae=vae, width=64, height=64, device=torch.device("cpu"), dtype=torch.float32
+ )
+ # [1, 20, 1, 8, 8] — 4-ch mask + 16-ch latents at H/8, W/8.
+ assert cond.shape == (1, 20, 1, 8, 8)
+
+ def test_shape_at_1024x1024(self):
+ img = Image.new("RGB", (1024, 1024))
+ vae = _make_fake_vae()
+ cond = encode_reference_image_to_condition(
+ image=img, vae=vae, width=1024, height=1024, device=torch.device("cpu"), dtype=torch.float32
+ )
+ # 1024/8 = 128 latent spatial dim.
+ assert cond.shape == (1, 20, 1, 128, 128)
+
+ def test_first_four_channels_are_all_ones_mask(self):
+ img = Image.new("RGB", (64, 64))
+ vae = _make_fake_vae()
+ cond = encode_reference_image_to_condition(
+ image=img, vae=vae, width=64, height=64, device=torch.device("cpu"), dtype=torch.float32
+ )
+ mask = cond[:, :4]
+ # First-frame mask is all-ones at num_frames=1 (every position is the first frame).
+ assert torch.equal(mask, torch.ones_like(mask))
+
+ def test_returns_dtype(self):
+ img = Image.new("RGB", (64, 64))
+ vae = _make_fake_vae()
+ cond = encode_reference_image_to_condition(
+ image=img, vae=vae, width=64, height=64, device=torch.device("cpu"), dtype=torch.bfloat16
+ )
+ assert cond.dtype == torch.bfloat16
+
+
+class TestEncodeReferenceImageToVideoCondition:
+ """Multi-frame A14B I2V condition, including first-last-frame (FLF2V) mode.
+
+ The fake VAE returns zero latents, so we can't assert on the *content* of the
+ last frame — but the 4-channel mask is built independently of the VAE output,
+ so we can verify exactly which latent frames get anchored.
+ """
+
+ def test_video_condition_shape(self):
+ img = Image.new("RGB", (64, 64))
+ vae = _make_fake_vae()
+ # num_frames=5 → t_lat = (5-1)//4 + 1 = 2.
+ cond = encode_reference_image_to_video_condition(
+ image=img, vae=vae, width=64, height=64, num_frames=5, device=torch.device("cpu"), dtype=torch.float32
+ )
+ assert cond.shape == (1, 20, 2, 8, 8)
+
+ def test_single_reference_anchors_only_first_latent_frame(self):
+ img = Image.new("RGB", (64, 64))
+ vae = _make_fake_vae()
+ cond = encode_reference_image_to_video_condition(
+ image=img, vae=vae, width=64, height=64, num_frames=5, device=torch.device("cpu"), dtype=torch.float32
+ )
+ mask = cond[:, :4] # [1, 4, t_lat=2, 8, 8]
+ # First latent frame fully anchored; the rest entirely free.
+ assert torch.equal(mask[:, :, 0], torch.ones_like(mask[:, :, 0]))
+ assert torch.equal(mask[:, :, 1:], torch.zeros_like(mask[:, :, 1:]))
+
+ def test_flf2v_anchors_first_and_last_latent_frame(self):
+ img = Image.new("RGB", (64, 64))
+ end = Image.new("RGB", (64, 64), (255, 255, 255))
+ vae = _make_fake_vae()
+ cond = encode_reference_image_to_video_condition(
+ image=img,
+ vae=vae,
+ width=64,
+ height=64,
+ num_frames=5,
+ device=torch.device("cpu"),
+ dtype=torch.float32,
+ last_image=end,
+ )
+ mask = cond[:, :4] # [1, 4, 2, 8, 8]
+ # First latent frame still fully anchored.
+ assert torch.equal(mask[:, :, 0], torch.ones_like(mask[:, :, 0]))
+ # The end image anchors the final pixel-frame, which lands in the last
+ # mask channel of the last latent frame (and only there).
+ assert torch.equal(mask[:, 3, 1], torch.ones_like(mask[:, 3, 1]))
+ assert torch.equal(mask[:, :3, 1], torch.zeros_like(mask[:, :3, 1]))
+
+ def test_flf2v_requires_multiple_frames(self):
+ img = Image.new("RGB", (64, 64))
+ end = Image.new("RGB", (64, 64))
+ vae = _make_fake_vae()
+ with pytest.raises(ValueError, match="FLF2V"):
+ encode_reference_image_to_video_condition(
+ image=img,
+ vae=vae,
+ width=64,
+ height=64,
+ num_frames=1,
+ device=torch.device("cpu"),
+ dtype=torch.float32,
+ last_image=end,
+ )
+
+
+class TestEncodeReferenceImageToTI2VCondition:
+ """TI2V-5B's condition tensor is a single 48-channel latent frame (no mask
+ channels) at the Wan2.2-VAE's 16x spatial scale. The denoise loop blends
+ this with the noise via a first_frame_mask at each step.
+ """
+
+ def test_shape_at_64x64(self):
+ from invokeai.backend.wan.extensions.wan_ref_image_extension import (
+ encode_reference_image_to_ti2v_condition,
+ )
+
+ img = Image.new("RGB", (64, 64))
+ vae = _make_fake_vae(z_dim=48, spatial_scale=16, temporal_scale=4)
+ cond = encode_reference_image_to_ti2v_condition(
+ image=img, vae=vae, width=64, height=64, device=torch.device("cpu"), dtype=torch.float32
+ )
+ # [1, 48, 1, 4, 4] — single latent frame at H/16, W/16.
+ assert cond.shape == (1, 48, 1, 4, 4)
+
+ def test_shape_at_832x480(self):
+ # Common Wan video resolution: latent 30x52 single frame.
+ from invokeai.backend.wan.extensions.wan_ref_image_extension import (
+ encode_reference_image_to_ti2v_condition,
+ )
+
+ img = Image.new("RGB", (832, 480))
+ vae = _make_fake_vae(z_dim=48, spatial_scale=16, temporal_scale=4)
+ cond = encode_reference_image_to_ti2v_condition(
+ image=img, vae=vae, width=832, height=480, device=torch.device("cpu"), dtype=torch.float32
+ )
+ assert cond.shape == (1, 48, 1, 30, 52)
+
+ def test_no_mask_channels(self):
+ # Distinguishing feature vs A14B: no leading 4-ch mask. All 48 channels
+ # are latent content. With latents_mean=0 and latents_std=1, encoded zeros
+ # stay zero (the function returns latents straight through).
+ from invokeai.backend.wan.extensions.wan_ref_image_extension import (
+ encode_reference_image_to_ti2v_condition,
+ )
+
+ img = Image.new("RGB", (64, 64))
+ vae = _make_fake_vae(z_dim=48, spatial_scale=16, temporal_scale=4)
+ cond = encode_reference_image_to_ti2v_condition(
+ image=img, vae=vae, width=64, height=64, device=torch.device("cpu"), dtype=torch.float32
+ )
+ # Fake VAE.encode returns zero latents; normalized zeros stay zero.
+ assert torch.equal(cond, torch.zeros_like(cond))
diff --git a/tests/conftest.py b/tests/conftest.py
index 80df0aa93e9..d930a4de78e 100644
--- a/tests/conftest.py
+++ b/tests/conftest.py
@@ -12,11 +12,13 @@
from invokeai.app.services.board_image_records.board_image_records_sqlite import SqliteBoardImageRecordStorage
from invokeai.app.services.board_records.board_records_sqlite import SqliteBoardRecordStorage
+from invokeai.app.services.board_video_records.board_video_records_sqlite import SqliteBoardVideoRecordStorage
from invokeai.app.services.boards.boards_default import BoardService
from invokeai.app.services.bulk_download.bulk_download_default import BulkDownloadService
from invokeai.app.services.client_state_persistence.client_state_persistence_sqlite import ClientStatePersistenceSqlite
from invokeai.app.services.config.config_default import InvokeAIAppConfig
from invokeai.app.services.external_generation.external_generation_default import ExternalGenerationService
+from invokeai.app.services.gallery.gallery_default import SqliteGalleryService
from invokeai.app.services.image_records.image_records_sqlite import SqliteImageRecordStorage
from invokeai.app.services.images.images_default import ImageService
from invokeai.app.services.invocation_cache.invocation_cache_memory import MemoryInvocationCache
@@ -24,6 +26,7 @@
from invokeai.app.services.invocation_stats.invocation_stats_default import InvocationStatsService
from invokeai.app.services.invoker import Invoker
from invokeai.app.services.users.users_default import UserService
+from invokeai.app.services.video_records.video_records_sqlite import SqliteVideoRecordStorage
from invokeai.app.services.workflow_records.workflow_records_sqlite import SqliteWorkflowRecordsStorage
from invokeai.backend.util.logging import InvokeAILogger
from tests.backend.model_manager.model_manager_fixtures import * # noqa: F403
@@ -70,6 +73,13 @@ def mock_services() -> InvocationServices:
model_relationships=None, # type: ignore
client_state_persistence=ClientStatePersistenceSqlite(db=db),
users=UserService(db),
+ videos=None, # type: ignore
+ video_files=None, # type: ignore
+ video_records=SqliteVideoRecordStorage(db=db),
+ board_video_records=SqliteBoardVideoRecordStorage(db=db),
+ # Real SQLite-backed gallery service: the virtual-boards router reads dates and
+ # per-date item names through it, and MagicMock cannot exercise the filter SQL.
+ gallery=SqliteGalleryService(db=db),
)
diff --git a/tests/test_invocation_cache_memory.py b/tests/test_invocation_cache_memory.py
index f776b86ee26..5dadbe63b07 100644
--- a/tests/test_invocation_cache_memory.py
+++ b/tests/test_invocation_cache_memory.py
@@ -181,3 +181,27 @@ def test_invocation_cache_memory_status():
assert status.hits == 0
assert status.misses == 0
assert status.max_size == 0
+
+
+def test_invocation_cache_memory_purges_video_outputs_on_video_deletion():
+ """Regression: without the videos.on_deleted hook, a cached VideoOutput outlives the
+ video's deletion — re-running an identical graph then "succeeds" with a 404 video."""
+ from unittest.mock import MagicMock
+
+ from invokeai.app.invocations.fields import VideoField
+ from invokeai.app.invocations.primitives import VideoOutput
+
+ cache = MemoryInvocationCache(max_cache_size=5)
+ invoker = MagicMock()
+ cache.start(invoker)
+ # The video service's on_deleted must be wired to the same purge callback as images.
+ invoker.services.videos.on_deleted.assert_called_once_with(cache._delete_by_match)
+ invoker.services.images.on_deleted.assert_called_once_with(cache._delete_by_match)
+
+ output = VideoOutput(
+ video=VideoField(video_name="clip.mp4"), width=832, height=480, num_frames=81, fps=16.0, duration=5.0625
+ )
+ cache.save(1, output)
+ cache._delete_by_match("clip.mp4")
+ assert cache.get(1) is None
+ assert len(cache._cache) == 0
diff --git a/tests/test_session_queue.py b/tests/test_session_queue.py
index 40116422ff4..7a363faea4b 100644
--- a/tests/test_session_queue.py
+++ b/tests/test_session_queue.py
@@ -3,6 +3,8 @@
import pytest
from pydantic import TypeAdapter, ValidationError
+from invokeai.app.invocations.fields import VideoField
+from invokeai.app.invocations.video_frame_extract import VideoFrameExtractInvocation
from invokeai.app.services.session_queue.session_queue_common import (
Batch,
BatchDataCollection,
@@ -88,6 +90,32 @@ def test_create_sessions_from_batch_with_runs(batch_data_collection, batch_graph
assert json.loads(t[7][1])["graph"]["nodes"]["4"]["prompt"] == "Nissan"
+def test_create_sessions_from_batch_with_video_fields():
+ """VideoField batch data must validate and expand into separate sessions, just like
+ ImageField — video workflows use the same generic batching capability (JPPhoto PR #9163
+ July-10 follow-up: VideoField values used to fail Batch validation before enqueueing)."""
+ g = Graph()
+ g.add_node(VideoFrameExtractInvocation(id="1", video=VideoField(video_name="placeholder.mp4")))
+ b = Batch(
+ graph=g,
+ data=[
+ [
+ BatchDatum(
+ node_path="1",
+ field_name="video",
+ items=[VideoField(video_name="first.mp4"), VideoField(video_name="second.mp4")],
+ )
+ ]
+ ],
+ )
+
+ assert calc_session_count(batch=b) == 2
+ t = list(create_session_nfv_tuples(batch=b, maximum=1000))
+ assert len(t) == 2
+ assert json.loads(t[0][1])["graph"]["nodes"]["1"]["video"]["video_name"] == "first.mp4"
+ assert json.loads(t[1][1])["graph"]["nodes"]["1"]["video"]["video_name"] == "second.mp4"
+
+
def test_create_sessions_from_batch_without_runs(batch_data_collection, batch_graph):
b = Batch(graph=batch_graph, data=batch_data_collection)
t = list(create_session_nfv_tuples(batch=b, maximum=1000))
diff --git a/uv.lock b/uv.lock
index 83ee591a31e..e0d9d21359f 100644
--- a/uv.lock
+++ b/uv.lock
@@ -1041,6 +1041,39 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/1e/5e/d4e9f1a599fb8e573b7b87160658329fbf28d19eac2718f51fc3def3aa5a/idna-3.18-py3-none-any.whl", hash = "sha256:7f952cbe720b688055e3f87de14f5c3e5fdaa8bc3928985c4077ca689de849a2", size = 65455, upload-time = "2026-06-02T14:34:06.319Z" },
]
+[[package]]
+name = "imageio"
+version = "2.37.3"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "numpy", marker = "(platform_machine != 'aarch64' and platform_machine != 'x86_64' and extra == 'extra-8-invokeai-cpu' and extra == 'extra-8-invokeai-cuda') or (platform_machine != 'aarch64' and platform_machine != 'x86_64' and extra == 'extra-8-invokeai-cpu' and extra == 'extra-8-invokeai-rocm') or (platform_machine != 'aarch64' and platform_machine != 'x86_64' and extra == 'extra-8-invokeai-cuda' and extra == 'extra-8-invokeai-rocm') or (platform_machine == 'aarch64' and sys_platform == 'linux') or (platform_machine == 'x86_64' and sys_platform == 'linux') or sys_platform == 'darwin' or sys_platform == 'win32' or (sys_platform != 'linux' and extra == 'extra-8-invokeai-cpu' and extra == 'extra-8-invokeai-cuda') or (sys_platform != 'linux' and extra == 'extra-8-invokeai-cpu' and extra == 'extra-8-invokeai-rocm') or (sys_platform != 'linux' and extra == 'extra-8-invokeai-cuda' and extra == 'extra-8-invokeai-rocm')" },
+ { name = "pillow", marker = "(platform_machine != 'aarch64' and platform_machine != 'x86_64' and extra == 'extra-8-invokeai-cpu' and extra == 'extra-8-invokeai-cuda') or (platform_machine != 'aarch64' and platform_machine != 'x86_64' and extra == 'extra-8-invokeai-cpu' and extra == 'extra-8-invokeai-rocm') or (platform_machine != 'aarch64' and platform_machine != 'x86_64' and extra == 'extra-8-invokeai-cuda' and extra == 'extra-8-invokeai-rocm') or (platform_machine == 'aarch64' and sys_platform == 'linux') or (platform_machine == 'x86_64' and sys_platform == 'linux') or sys_platform == 'darwin' or sys_platform == 'win32' or (sys_platform != 'linux' and extra == 'extra-8-invokeai-cpu' and extra == 'extra-8-invokeai-cuda') or (sys_platform != 'linux' and extra == 'extra-8-invokeai-cpu' and extra == 'extra-8-invokeai-rocm') or (sys_platform != 'linux' and extra == 'extra-8-invokeai-cuda' and extra == 'extra-8-invokeai-rocm')" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/b1/84/93bcd1300216ea50811cee96873b84a1bebf8d0489ffaf7f2a3756bab866/imageio-2.37.3.tar.gz", hash = "sha256:bbb37efbfc4c400fcd534b367b91fcd66d5da639aaa138034431a1c5e0a41451", size = 389673, upload-time = "2026-03-09T11:31:12.573Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/49/fa/391e437a34e55095173dca5f24070d89cbc233ff85bf1c29c93248c6588d/imageio-2.37.3-py3-none-any.whl", hash = "sha256:46f5bb8522cd421c0f5ae104d8268f569d856b29eb1a13b92829d1970f32c9f0", size = 317646, upload-time = "2026-03-09T11:31:10.771Z" },
+]
+
+[package.optional-dependencies]
+ffmpeg = [
+ { name = "imageio-ffmpeg", marker = "(platform_machine != 'aarch64' and platform_machine != 'x86_64' and extra == 'extra-8-invokeai-cpu' and extra == 'extra-8-invokeai-cuda') or (platform_machine != 'aarch64' and platform_machine != 'x86_64' and extra == 'extra-8-invokeai-cpu' and extra == 'extra-8-invokeai-rocm') or (platform_machine != 'aarch64' and platform_machine != 'x86_64' and extra == 'extra-8-invokeai-cuda' and extra == 'extra-8-invokeai-rocm') or (platform_machine == 'aarch64' and sys_platform == 'linux') or (platform_machine == 'x86_64' and sys_platform == 'linux') or sys_platform == 'darwin' or sys_platform == 'win32' or (sys_platform != 'linux' and extra == 'extra-8-invokeai-cpu' and extra == 'extra-8-invokeai-cuda') or (sys_platform != 'linux' and extra == 'extra-8-invokeai-cpu' and extra == 'extra-8-invokeai-rocm') or (sys_platform != 'linux' and extra == 'extra-8-invokeai-cuda' and extra == 'extra-8-invokeai-rocm')" },
+ { name = "psutil", marker = "(platform_machine != 'aarch64' and platform_machine != 'x86_64' and extra == 'extra-8-invokeai-cpu' and extra == 'extra-8-invokeai-cuda') or (platform_machine != 'aarch64' and platform_machine != 'x86_64' and extra == 'extra-8-invokeai-cpu' and extra == 'extra-8-invokeai-rocm') or (platform_machine != 'aarch64' and platform_machine != 'x86_64' and extra == 'extra-8-invokeai-cuda' and extra == 'extra-8-invokeai-rocm') or (platform_machine == 'aarch64' and sys_platform == 'linux') or (platform_machine == 'x86_64' and sys_platform == 'linux') or sys_platform == 'darwin' or sys_platform == 'win32' or (sys_platform != 'linux' and extra == 'extra-8-invokeai-cpu' and extra == 'extra-8-invokeai-cuda') or (sys_platform != 'linux' and extra == 'extra-8-invokeai-cpu' and extra == 'extra-8-invokeai-rocm') or (sys_platform != 'linux' and extra == 'extra-8-invokeai-cuda' and extra == 'extra-8-invokeai-rocm')" },
+]
+
+[[package]]
+name = "imageio-ffmpeg"
+version = "0.6.0"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/44/bd/c3343c721f2a1b0c9fc71c1aebf1966a3b7f08c2eea8ed5437a2865611d6/imageio_ffmpeg-0.6.0.tar.gz", hash = "sha256:e2556bed8e005564a9f925bb7afa4002d82770d6b08825078b7697ab88ba1755", size = 25210, upload-time = "2025-01-16T21:34:32.747Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/da/58/87ef68ac83f4c7690961bce288fd8e382bc5f1513860fc7f90a9c1c1c6bf/imageio_ffmpeg-0.6.0-py3-none-macosx_10_9_intel.macosx_10_9_x86_64.whl", hash = "sha256:9d2baaf867088508d4a3458e61eeb30e945c4ad8016025545f66c4b5aaef0a61", size = 24932969, upload-time = "2025-01-16T21:34:20.464Z" },
+ { url = "https://files.pythonhosted.org/packages/40/5c/f3d8a657d362cc93b81aab8feda487317da5b5d31c0e1fdfd5e986e55d17/imageio_ffmpeg-0.6.0-py3-none-macosx_11_0_arm64.whl", hash = "sha256:b1ae3173414b5fc5f538a726c4e48ea97edc0d2cdc11f103afee655c463fa742", size = 21113891, upload-time = "2025-01-16T21:34:00.277Z" },
+ { url = "https://files.pythonhosted.org/packages/33/e7/1925bfbc563c39c1d2e82501d8372734a5c725e53ac3b31b4c2d081e895b/imageio_ffmpeg-0.6.0-py3-none-manylinux2014_aarch64.whl", hash = "sha256:1d47bebd83d2c5fc770720d211855f208af8a596c82d17730aa51e815cdee6dc", size = 25632706, upload-time = "2025-01-16T21:33:53.475Z" },
+ { url = "https://files.pythonhosted.org/packages/a0/2d/43c8522a2038e9d0e7dbdf3a61195ecc31ca576fb1527a528c877e87d973/imageio_ffmpeg-0.6.0-py3-none-manylinux2014_x86_64.whl", hash = "sha256:c7e46fcec401dd990405049d2e2f475e2b397779df2519b544b8aab515195282", size = 29498237, upload-time = "2025-01-16T21:34:13.726Z" },
+ { url = "https://files.pythonhosted.org/packages/a0/13/59da54728351883c3c1d9fca1710ab8eee82c7beba585df8f25ca925f08f/imageio_ffmpeg-0.6.0-py3-none-win32.whl", hash = "sha256:196faa79366b4a82f95c0f4053191d2013f4714a715780f0ad2a68ff37483cc2", size = 19652251, upload-time = "2025-01-16T21:34:06.812Z" },
+ { url = "https://files.pythonhosted.org/packages/2c/c6/fa760e12a2483469e2bf5058c5faff664acf66cadb4df2ad6205b016a73d/imageio_ffmpeg-0.6.0-py3-none-win_amd64.whl", hash = "sha256:02fa47c83703c37df6bfe4896aab339013f62bf02c5ebf2dce6da56af04ffc0a", size = 31246824, upload-time = "2025-01-16T21:34:28.6Z" },
+]
+
[[package]]
name = "importlib-metadata"
version = "9.0.0"
@@ -1081,6 +1114,7 @@ dependencies = [
{ name = "fastapi-events", marker = "(platform_machine != 'aarch64' and platform_machine != 'x86_64' and extra == 'extra-8-invokeai-cpu' and extra == 'extra-8-invokeai-cuda') or (platform_machine != 'aarch64' and platform_machine != 'x86_64' and extra == 'extra-8-invokeai-cpu' and extra == 'extra-8-invokeai-rocm') or (platform_machine != 'aarch64' and platform_machine != 'x86_64' and extra == 'extra-8-invokeai-cuda' and extra == 'extra-8-invokeai-rocm') or (platform_machine == 'aarch64' and sys_platform == 'linux') or (platform_machine == 'x86_64' and sys_platform == 'linux') or sys_platform == 'darwin' or sys_platform == 'win32' or (sys_platform != 'linux' and extra == 'extra-8-invokeai-cpu' and extra == 'extra-8-invokeai-cuda') or (sys_platform != 'linux' and extra == 'extra-8-invokeai-cpu' and extra == 'extra-8-invokeai-rocm') or (sys_platform != 'linux' and extra == 'extra-8-invokeai-cuda' and extra == 'extra-8-invokeai-rocm')" },
{ name = "gguf", marker = "(platform_machine != 'aarch64' and platform_machine != 'x86_64' and extra == 'extra-8-invokeai-cpu' and extra == 'extra-8-invokeai-cuda') or (platform_machine != 'aarch64' and platform_machine != 'x86_64' and extra == 'extra-8-invokeai-cpu' and extra == 'extra-8-invokeai-rocm') or (platform_machine != 'aarch64' and platform_machine != 'x86_64' and extra == 'extra-8-invokeai-cuda' and extra == 'extra-8-invokeai-rocm') or (platform_machine == 'aarch64' and sys_platform == 'linux') or (platform_machine == 'x86_64' and sys_platform == 'linux') or sys_platform == 'darwin' or sys_platform == 'win32' or (sys_platform != 'linux' and extra == 'extra-8-invokeai-cpu' and extra == 'extra-8-invokeai-cuda') or (sys_platform != 'linux' and extra == 'extra-8-invokeai-cpu' and extra == 'extra-8-invokeai-rocm') or (sys_platform != 'linux' and extra == 'extra-8-invokeai-cuda' and extra == 'extra-8-invokeai-rocm')" },
{ name = "huggingface-hub", marker = "(platform_machine != 'aarch64' and platform_machine != 'x86_64' and extra == 'extra-8-invokeai-cpu' and extra == 'extra-8-invokeai-cuda') or (platform_machine != 'aarch64' and platform_machine != 'x86_64' and extra == 'extra-8-invokeai-cpu' and extra == 'extra-8-invokeai-rocm') or (platform_machine != 'aarch64' and platform_machine != 'x86_64' and extra == 'extra-8-invokeai-cuda' and extra == 'extra-8-invokeai-rocm') or (platform_machine == 'aarch64' and sys_platform == 'linux') or (platform_machine == 'x86_64' and sys_platform == 'linux') or sys_platform == 'darwin' or sys_platform == 'win32' or (sys_platform != 'linux' and extra == 'extra-8-invokeai-cpu' and extra == 'extra-8-invokeai-cuda') or (sys_platform != 'linux' and extra == 'extra-8-invokeai-cpu' and extra == 'extra-8-invokeai-rocm') or (sys_platform != 'linux' and extra == 'extra-8-invokeai-cuda' and extra == 'extra-8-invokeai-rocm')" },
+ { name = "imageio", extra = ["ffmpeg"], marker = "(platform_machine != 'aarch64' and platform_machine != 'x86_64' and extra == 'extra-8-invokeai-cpu' and extra == 'extra-8-invokeai-cuda') or (platform_machine != 'aarch64' and platform_machine != 'x86_64' and extra == 'extra-8-invokeai-cpu' and extra == 'extra-8-invokeai-rocm') or (platform_machine != 'aarch64' and platform_machine != 'x86_64' and extra == 'extra-8-invokeai-cuda' and extra == 'extra-8-invokeai-rocm') or (platform_machine == 'aarch64' and sys_platform == 'linux') or (platform_machine == 'x86_64' and sys_platform == 'linux') or sys_platform == 'darwin' or sys_platform == 'win32' or (sys_platform != 'linux' and extra == 'extra-8-invokeai-cpu' and extra == 'extra-8-invokeai-cuda') or (sys_platform != 'linux' and extra == 'extra-8-invokeai-cpu' and extra == 'extra-8-invokeai-rocm') or (sys_platform != 'linux' and extra == 'extra-8-invokeai-cuda' and extra == 'extra-8-invokeai-rocm')" },
{ name = "mediapipe", marker = "(platform_machine != 'aarch64' and platform_machine != 'x86_64' and extra == 'extra-8-invokeai-cpu' and extra == 'extra-8-invokeai-cuda') or (platform_machine != 'aarch64' and platform_machine != 'x86_64' and extra == 'extra-8-invokeai-cpu' and extra == 'extra-8-invokeai-rocm') or (platform_machine != 'aarch64' and platform_machine != 'x86_64' and extra == 'extra-8-invokeai-cuda' and extra == 'extra-8-invokeai-rocm') or (platform_machine == 'aarch64' and sys_platform == 'linux') or (platform_machine == 'x86_64' and sys_platform == 'linux') or sys_platform == 'darwin' or sys_platform == 'win32' or (sys_platform != 'linux' and extra == 'extra-8-invokeai-cpu' and extra == 'extra-8-invokeai-cuda') or (sys_platform != 'linux' and extra == 'extra-8-invokeai-cpu' and extra == 'extra-8-invokeai-rocm') or (sys_platform != 'linux' and extra == 'extra-8-invokeai-cuda' and extra == 'extra-8-invokeai-rocm')" },
{ name = "numpy", marker = "(platform_machine != 'aarch64' and platform_machine != 'x86_64' and extra == 'extra-8-invokeai-cpu' and extra == 'extra-8-invokeai-cuda') or (platform_machine != 'aarch64' and platform_machine != 'x86_64' and extra == 'extra-8-invokeai-cpu' and extra == 'extra-8-invokeai-rocm') or (platform_machine != 'aarch64' and platform_machine != 'x86_64' and extra == 'extra-8-invokeai-cuda' and extra == 'extra-8-invokeai-rocm') or (platform_machine == 'aarch64' and sys_platform == 'linux') or (platform_machine == 'x86_64' and sys_platform == 'linux') or sys_platform == 'darwin' or sys_platform == 'win32' or (sys_platform != 'linux' and extra == 'extra-8-invokeai-cpu' and extra == 'extra-8-invokeai-cuda') or (sys_platform != 'linux' and extra == 'extra-8-invokeai-cpu' and extra == 'extra-8-invokeai-rocm') or (sys_platform != 'linux' and extra == 'extra-8-invokeai-cuda' and extra == 'extra-8-invokeai-rocm')" },
{ name = "onnx", marker = "(platform_machine != 'aarch64' and platform_machine != 'x86_64' and extra == 'extra-8-invokeai-cpu' and extra == 'extra-8-invokeai-cuda') or (platform_machine != 'aarch64' and platform_machine != 'x86_64' and extra == 'extra-8-invokeai-cpu' and extra == 'extra-8-invokeai-rocm') or (platform_machine != 'aarch64' and platform_machine != 'x86_64' and extra == 'extra-8-invokeai-cuda' and extra == 'extra-8-invokeai-rocm') or (platform_machine == 'aarch64' and sys_platform == 'linux') or (platform_machine == 'x86_64' and sys_platform == 'linux') or sys_platform == 'darwin' or sys_platform == 'win32' or (sys_platform != 'linux' and extra == 'extra-8-invokeai-cpu' and extra == 'extra-8-invokeai-cuda') or (sys_platform != 'linux' and extra == 'extra-8-invokeai-cpu' and extra == 'extra-8-invokeai-rocm') or (sys_platform != 'linux' and extra == 'extra-8-invokeai-cuda' and extra == 'extra-8-invokeai-rocm')" },
@@ -1090,6 +1124,7 @@ dependencies = [
{ name = "picklescan", marker = "(platform_machine != 'aarch64' and platform_machine != 'x86_64' and extra == 'extra-8-invokeai-cpu' and extra == 'extra-8-invokeai-cuda') or (platform_machine != 'aarch64' and platform_machine != 'x86_64' and extra == 'extra-8-invokeai-cpu' and extra == 'extra-8-invokeai-rocm') or (platform_machine != 'aarch64' and platform_machine != 'x86_64' and extra == 'extra-8-invokeai-cuda' and extra == 'extra-8-invokeai-rocm') or (platform_machine == 'aarch64' and sys_platform == 'linux') or (platform_machine == 'x86_64' and sys_platform == 'linux') or sys_platform == 'darwin' or sys_platform == 'win32' or (sys_platform != 'linux' and extra == 'extra-8-invokeai-cpu' and extra == 'extra-8-invokeai-cuda') or (sys_platform != 'linux' and extra == 'extra-8-invokeai-cpu' and extra == 'extra-8-invokeai-rocm') or (sys_platform != 'linux' and extra == 'extra-8-invokeai-cuda' and extra == 'extra-8-invokeai-rocm')" },
{ name = "pillow", marker = "(platform_machine != 'aarch64' and platform_machine != 'x86_64' and extra == 'extra-8-invokeai-cpu' and extra == 'extra-8-invokeai-cuda') or (platform_machine != 'aarch64' and platform_machine != 'x86_64' and extra == 'extra-8-invokeai-cpu' and extra == 'extra-8-invokeai-rocm') or (platform_machine != 'aarch64' and platform_machine != 'x86_64' and extra == 'extra-8-invokeai-cuda' and extra == 'extra-8-invokeai-rocm') or (platform_machine == 'aarch64' and sys_platform == 'linux') or (platform_machine == 'x86_64' and sys_platform == 'linux') or sys_platform == 'darwin' or sys_platform == 'win32' or (sys_platform != 'linux' and extra == 'extra-8-invokeai-cpu' and extra == 'extra-8-invokeai-cuda') or (sys_platform != 'linux' and extra == 'extra-8-invokeai-cpu' and extra == 'extra-8-invokeai-rocm') or (sys_platform != 'linux' and extra == 'extra-8-invokeai-cuda' and extra == 'extra-8-invokeai-rocm')" },
{ name = "prompt-toolkit", marker = "(platform_machine != 'aarch64' and platform_machine != 'x86_64' and extra == 'extra-8-invokeai-cpu' and extra == 'extra-8-invokeai-cuda') or (platform_machine != 'aarch64' and platform_machine != 'x86_64' and extra == 'extra-8-invokeai-cpu' and extra == 'extra-8-invokeai-rocm') or (platform_machine != 'aarch64' and platform_machine != 'x86_64' and extra == 'extra-8-invokeai-cuda' and extra == 'extra-8-invokeai-rocm') or (platform_machine == 'aarch64' and sys_platform == 'linux') or (platform_machine == 'x86_64' and sys_platform == 'linux') or sys_platform == 'darwin' or sys_platform == 'win32' or (sys_platform != 'linux' and extra == 'extra-8-invokeai-cpu' and extra == 'extra-8-invokeai-cuda') or (sys_platform != 'linux' and extra == 'extra-8-invokeai-cpu' and extra == 'extra-8-invokeai-rocm') or (sys_platform != 'linux' and extra == 'extra-8-invokeai-cuda' and extra == 'extra-8-invokeai-rocm')" },
+ { name = "psutil", marker = "(platform_machine != 'aarch64' and platform_machine != 'x86_64' and extra == 'extra-8-invokeai-cpu' and extra == 'extra-8-invokeai-cuda') or (platform_machine != 'aarch64' and platform_machine != 'x86_64' and extra == 'extra-8-invokeai-cpu' and extra == 'extra-8-invokeai-rocm') or (platform_machine != 'aarch64' and platform_machine != 'x86_64' and extra == 'extra-8-invokeai-cuda' and extra == 'extra-8-invokeai-rocm') or (platform_machine == 'aarch64' and sys_platform == 'linux') or (platform_machine == 'x86_64' and sys_platform == 'linux') or sys_platform == 'darwin' or sys_platform == 'win32' or (sys_platform != 'linux' and extra == 'extra-8-invokeai-cpu' and extra == 'extra-8-invokeai-cuda') or (sys_platform != 'linux' and extra == 'extra-8-invokeai-cpu' and extra == 'extra-8-invokeai-rocm') or (sys_platform != 'linux' and extra == 'extra-8-invokeai-cuda' and extra == 'extra-8-invokeai-rocm')" },
{ name = "pydantic", marker = "(platform_machine != 'aarch64' and platform_machine != 'x86_64' and extra == 'extra-8-invokeai-cpu' and extra == 'extra-8-invokeai-cuda') or (platform_machine != 'aarch64' and platform_machine != 'x86_64' and extra == 'extra-8-invokeai-cpu' and extra == 'extra-8-invokeai-rocm') or (platform_machine != 'aarch64' and platform_machine != 'x86_64' and extra == 'extra-8-invokeai-cuda' and extra == 'extra-8-invokeai-rocm') or (platform_machine == 'aarch64' and sys_platform == 'linux') or (platform_machine == 'x86_64' and sys_platform == 'linux') or sys_platform == 'darwin' or sys_platform == 'win32' or (sys_platform != 'linux' and extra == 'extra-8-invokeai-cpu' and extra == 'extra-8-invokeai-cuda') or (sys_platform != 'linux' and extra == 'extra-8-invokeai-cpu' and extra == 'extra-8-invokeai-rocm') or (sys_platform != 'linux' and extra == 'extra-8-invokeai-cuda' and extra == 'extra-8-invokeai-rocm')" },
{ name = "pydantic-settings", marker = "(platform_machine != 'aarch64' and platform_machine != 'x86_64' and extra == 'extra-8-invokeai-cpu' and extra == 'extra-8-invokeai-cuda') or (platform_machine != 'aarch64' and platform_machine != 'x86_64' and extra == 'extra-8-invokeai-cpu' and extra == 'extra-8-invokeai-rocm') or (platform_machine != 'aarch64' and platform_machine != 'x86_64' and extra == 'extra-8-invokeai-cuda' and extra == 'extra-8-invokeai-rocm') or (platform_machine == 'aarch64' and sys_platform == 'linux') or (platform_machine == 'x86_64' and sys_platform == 'linux') or sys_platform == 'darwin' or sys_platform == 'win32' or (sys_platform != 'linux' and extra == 'extra-8-invokeai-cpu' and extra == 'extra-8-invokeai-cuda') or (sys_platform != 'linux' and extra == 'extra-8-invokeai-cpu' and extra == 'extra-8-invokeai-rocm') or (sys_platform != 'linux' and extra == 'extra-8-invokeai-cuda' and extra == 'extra-8-invokeai-rocm')" },
{ name = "pypatchmatch", marker = "(platform_machine != 'aarch64' and platform_machine != 'x86_64' and extra == 'extra-8-invokeai-cpu' and extra == 'extra-8-invokeai-cuda') or (platform_machine != 'aarch64' and platform_machine != 'x86_64' and extra == 'extra-8-invokeai-cpu' and extra == 'extra-8-invokeai-rocm') or (platform_machine != 'aarch64' and platform_machine != 'x86_64' and extra == 'extra-8-invokeai-cuda' and extra == 'extra-8-invokeai-rocm') or (platform_machine == 'aarch64' and sys_platform == 'linux') or (platform_machine == 'x86_64' and sys_platform == 'linux') or sys_platform == 'darwin' or sys_platform == 'win32' or (sys_platform != 'linux' and extra == 'extra-8-invokeai-cpu' and extra == 'extra-8-invokeai-cuda') or (sys_platform != 'linux' and extra == 'extra-8-invokeai-cpu' and extra == 'extra-8-invokeai-rocm') or (sys_platform != 'linux' and extra == 'extra-8-invokeai-cuda' and extra == 'extra-8-invokeai-rocm')" },
@@ -1194,6 +1229,7 @@ requires-dist = [
{ name = "httpx", marker = "extra == 'test'" },
{ name = "huggingface-hub" },
{ name = "humanize", marker = "extra == 'test'", specifier = "==4.12.1" },
+ { name = "imageio", extras = ["ffmpeg"], specifier = ">=2.37" },
{ name = "jurigged", marker = "extra == 'dev'" },
{ name = "mediapipe", specifier = "==0.10.14" },
{ name = "mypy", marker = "extra == 'test'" },
@@ -1212,6 +1248,7 @@ requires-dist = [
{ name = "polyfactory", marker = "extra == 'test'", specifier = "==2.19.0" },
{ name = "pre-commit", marker = "extra == 'test'" },
{ name = "prompt-toolkit" },
+ { name = "psutil", specifier = ">=6" },
{ name = "pudb", marker = "extra == 'dev'" },
{ name = "pydantic" },
{ name = "pydantic-settings" },
@@ -3901,10 +3938,10 @@ source = { registry = "https://pypi.org/simple" }
resolution-markers = [
"python_full_version >= '3.12' and platform_machine == 'aarch64' and sys_platform == 'linux'",
"python_full_version < '3.12' and platform_machine == 'aarch64' and sys_platform == 'linux'",
- "python_full_version >= '3.12' and sys_platform == 'darwin'",
- "python_full_version < '3.12' and sys_platform == 'darwin'",
"(python_full_version >= '3.12' and platform_machine == 'x86_64' and sys_platform == 'linux') or (python_full_version >= '3.12' and sys_platform == 'win32')",
"(python_full_version < '3.12' and platform_machine == 'x86_64' and sys_platform == 'linux') or (python_full_version < '3.12' and sys_platform == 'win32')",
+ "python_full_version >= '3.12' and sys_platform == 'darwin'",
+ "python_full_version < '3.12' and sys_platform == 'darwin'",
]
dependencies = [
{ name = "filelock", marker = "(platform_machine != 'aarch64' and platform_machine != 'x86_64' and extra == 'extra-8-invokeai-cpu' and extra == 'extra-8-invokeai-cuda') or (platform_machine != 'aarch64' and platform_machine != 'x86_64' and extra == 'extra-8-invokeai-cpu' and extra == 'extra-8-invokeai-rocm') or (platform_machine != 'aarch64' and platform_machine != 'x86_64' and extra == 'extra-8-invokeai-cuda' and extra == 'extra-8-invokeai-rocm') or (platform_machine == 'aarch64' and sys_platform == 'linux') or (platform_machine == 'x86_64' and sys_platform == 'linux' and extra != 'extra-8-invokeai-cpu' and extra != 'extra-8-invokeai-cuda' and extra != 'extra-8-invokeai-rocm') or (platform_machine == 'x86_64' and extra == 'extra-8-invokeai-cpu' and extra == 'extra-8-invokeai-cuda') or (platform_machine == 'x86_64' and extra == 'extra-8-invokeai-cpu' and extra == 'extra-8-invokeai-rocm') or (platform_machine == 'x86_64' and extra == 'extra-8-invokeai-cuda' and extra == 'extra-8-invokeai-rocm') or (sys_platform == 'darwin' and extra == 'extra-8-invokeai-rocm') or (sys_platform != 'linux' and extra == 'extra-8-invokeai-cpu' and extra == 'extra-8-invokeai-cuda') or (sys_platform != 'linux' and extra == 'extra-8-invokeai-cpu' and extra == 'extra-8-invokeai-rocm') or (sys_platform != 'linux' and extra == 'extra-8-invokeai-cuda' and extra == 'extra-8-invokeai-rocm') or (sys_platform == 'darwin' and extra == 'extra-8-invokeai-cpu' and extra == 'extra-8-invokeai-cuda') or (sys_platform == 'darwin' and extra != 'extra-8-invokeai-cpu' and extra != 'extra-8-invokeai-cuda') or (sys_platform == 'win32' and extra == 'extra-8-invokeai-cpu' and extra == 'extra-8-invokeai-cuda') or (sys_platform == 'win32' and extra == 'extra-8-invokeai-cpu' and extra == 'extra-8-invokeai-rocm') or (sys_platform == 'win32' and extra == 'extra-8-invokeai-cuda' and extra == 'extra-8-invokeai-rocm') or (sys_platform == 'win32' and extra != 'extra-8-invokeai-cpu' and extra != 'extra-8-invokeai-cuda' and extra != 'extra-8-invokeai-rocm')" },
@@ -4061,10 +4098,10 @@ source = { registry = "https://pypi.org/simple" }
resolution-markers = [
"python_full_version >= '3.12' and platform_machine == 'aarch64' and sys_platform == 'linux'",
"python_full_version < '3.12' and platform_machine == 'aarch64' and sys_platform == 'linux'",
- "python_full_version >= '3.12' and sys_platform == 'darwin'",
- "python_full_version < '3.12' and sys_platform == 'darwin'",
"(python_full_version >= '3.12' and platform_machine == 'x86_64' and sys_platform == 'linux') or (python_full_version >= '3.12' and sys_platform == 'win32')",
"(python_full_version < '3.12' and platform_machine == 'x86_64' and sys_platform == 'linux') or (python_full_version < '3.12' and sys_platform == 'win32')",
+ "python_full_version >= '3.12' and sys_platform == 'darwin'",
+ "python_full_version < '3.12' and sys_platform == 'darwin'",
]
dependencies = [
{ name = "numpy", marker = "(platform_machine != 'aarch64' and platform_machine != 'x86_64' and extra == 'extra-8-invokeai-cpu' and extra == 'extra-8-invokeai-cuda') or (platform_machine != 'aarch64' and platform_machine != 'x86_64' and extra == 'extra-8-invokeai-cpu' and extra == 'extra-8-invokeai-rocm') or (platform_machine != 'aarch64' and platform_machine != 'x86_64' and extra == 'extra-8-invokeai-cuda' and extra == 'extra-8-invokeai-rocm') or (platform_machine == 'aarch64' and sys_platform == 'linux') or (platform_machine == 'x86_64' and sys_platform == 'linux' and extra != 'extra-8-invokeai-cpu' and extra != 'extra-8-invokeai-cuda' and extra != 'extra-8-invokeai-rocm') or (platform_machine == 'x86_64' and extra == 'extra-8-invokeai-cpu' and extra == 'extra-8-invokeai-cuda') or (platform_machine == 'x86_64' and extra == 'extra-8-invokeai-cpu' and extra == 'extra-8-invokeai-rocm') or (platform_machine == 'x86_64' and extra == 'extra-8-invokeai-cuda' and extra == 'extra-8-invokeai-rocm') or (sys_platform == 'darwin' and extra == 'extra-8-invokeai-rocm') or (sys_platform != 'linux' and extra == 'extra-8-invokeai-cpu' and extra == 'extra-8-invokeai-cuda') or (sys_platform != 'linux' and extra == 'extra-8-invokeai-cpu' and extra == 'extra-8-invokeai-rocm') or (sys_platform != 'linux' and extra == 'extra-8-invokeai-cuda' and extra == 'extra-8-invokeai-rocm') or (sys_platform == 'darwin' and extra == 'extra-8-invokeai-cpu' and extra == 'extra-8-invokeai-cuda') or (sys_platform == 'darwin' and extra != 'extra-8-invokeai-cpu' and extra != 'extra-8-invokeai-cuda') or (sys_platform == 'win32' and extra == 'extra-8-invokeai-cpu' and extra == 'extra-8-invokeai-cuda') or (sys_platform == 'win32' and extra == 'extra-8-invokeai-cpu' and extra == 'extra-8-invokeai-rocm') or (sys_platform == 'win32' and extra == 'extra-8-invokeai-cuda' and extra == 'extra-8-invokeai-rocm') or (sys_platform == 'win32' and extra != 'extra-8-invokeai-cpu' and extra != 'extra-8-invokeai-cuda' and extra != 'extra-8-invokeai-rocm')" },