diff --git a/USER_ISOLATION_IMPLEMENTATION.md b/USER_ISOLATION_IMPLEMENTATION.md
deleted file mode 100644
index 324c40db562..00000000000
--- a/USER_ISOLATION_IMPLEMENTATION.md
+++ /dev/null
@@ -1,169 +0,0 @@
-# User Isolation Implementation Summary
-
-This document describes the implementation of user isolation features in the InvokeAI session queue and processing system to address issues identified in the enhancement request.
-
-## Issues Addressed
-
-### 1. Cross-User Image/Preview Visibility
-**Problem:** When two users are logged in simultaneously and one initiates a generation, the generation preview shows up in both users' browsers and the generated image gets saved to both users' image boards.
-
-**Solution:** Implemented socket-level event filtering based on user authentication:
-
-#### Backend Changes (`invokeai/app/api/sockets.py`):
-- Added socket authentication middleware in `_handle_connect()` method
-- Extracts JWT token from socket auth data or HTTP headers
-- Verifies token using existing `verify_token()` function
-- Stores `user_id` and `is_admin` in socket session for later use
-- Modified `_handle_queue_event()` to filter events by user:
- - For `QueueItemEventBase` events, only emit to:
- - The user who owns the queue item (`user_id` matches)
- - Admin users (`is_admin` is True)
- - For general queue events, emit to all subscribers
-
-#### Event System Changes (`invokeai/app/services/events/events_common.py`):
-- Added `user_id` field to `QueueItemEventBase` class
-- Updated all event builders to include `user_id` from queue items:
- - `InvocationStartedEvent.build()`
- - `InvocationProgressEvent.build()`
- - `InvocationCompleteEvent.build()`
- - `InvocationErrorEvent.build()`
- - `QueueItemStatusChangedEvent.build()`
-
-### 2. Batch Field Values Privacy
-**Problem:** Users can see batch field values from generation processes launched by other users.
-
-**Solution:** Implemented field value sanitization at the API level:
-
-#### API Router Changes (`invokeai/app/api/routers/session_queue.py`):
-- Created `sanitize_queue_item_for_user()` helper function
- - Clears `field_values` for non-admin users viewing other users' items
- - Admins and item owners can see all field values
-- Updated endpoints to require authentication and sanitize responses:
- - `list_all_queue_items()` - Added `CurrentUser` dependency
- - `get_queue_items_by_item_ids()` - Added `CurrentUser` dependency
- - `get_queue_item()` - Added `CurrentUser` dependency
-
-### 3. Queue Updates Across Browser Windows
-**Problem:** When the job queue tab is open in multiple browsers and a generation is begun in one browser window, the queue does not update in the other window.
-
-**Status:** This issue is likely resolved by the socket authentication and event filtering changes. The existing socket subscription mechanism (`subscribe_queue` event) already supports multiple connections per user. Testing is required to confirm this works correctly with the new authentication flow.
-
-### 4. User Information Display
-**Problem:** Queue table lacks user identification, making it difficult to know who launched which job.
-
-**Solution:** Added user information to queue items and UI:
-
-#### Database Layer (`invokeai/app/services/session_queue/session_queue_sqlite.py`):
-- Updated SQL queries to JOIN with `users` table
-- Modified methods to fetch user information:
- - `get_queue_item()` - Now selects `display_name` and `email` from users table
- - `dequeue()` - Includes user info
- - `get_next()` - Includes user info
- - `get_current()` - Includes user info
- - `list_all_queue_items()` - Includes user info
-
-#### Data Model Changes (`invokeai/app/services/session_queue/session_queue_common.py`):
-- Added optional fields to `SessionQueueItem`:
- - `user_display_name: Optional[str]` - Display name from users table
- - `user_email: Optional[str]` - Email from users table
- - Note: `user_id` field already existed from Migration 25
-
-#### Frontend UI Changes:
-- **Constants** (`constants.ts`): Added `user: '8rem'` column width
-- **Header** (`QueueListHeader.tsx`): Added "User" column header
-- **Item Component** (`QueueItemComponent.tsx`):
- - Added logic to display user information (display_name → email → user_id)
- - Added user column to queue item row
- - Added tooltip with full username on hover
- - Added "Hidden for privacy" message when field_values are null for non-owned items
-- **Localization** (`en.json`): Added translations:
- - `"user": "User"`
- - `"fieldValuesHidden": "Hidden for privacy"`
-
-## Security Considerations
-
-### Token Verification
-- Tokens are verified using the existing `verify_token()` function from `invokeai.app.services.auth.token_service`
-- Invalid or missing tokens default to "system" user with non-admin privileges
-- Socket connections without valid tokens are still accepted for backward compatibility but have limited access
-
-### Data Privacy
-- Field values are only visible to:
- - The user who created the queue item
- - Admin users
-- Non-admin users viewing other users' queue items see "Hidden for privacy" instead of field values
-
-### Admin Privileges
-- Admin users can see all queue events and field values across all users
-- Admin status is determined from the JWT token's `is_admin` field
-
-## Migration Notes
-
-No database migration is required. The changes leverage:
-- Existing `user_id` column in `session_queue` table (added in Migration 25)
-- Existing `users` table (added in Migration 25)
-- SQL LEFT JOINs to fetch user information (gracefully handles missing user records)
-
-## Testing Requirements
-
-### Backend Testing
-1. **Socket Authentication:**
- - Verify valid tokens are accepted and user context is stored
- - Verify invalid tokens default to system user
- - Verify expired tokens are rejected
-
-2. **Event Filtering:**
- - User A should only receive events for their own queue items
- - Admin users should receive all events
- - Non-admin users should not receive events from other users
-
-3. **Field Value Sanitization:**
- - Non-admin users should see null field_values for other users' items
- - Admins should see all field values
- - Users should see their own field values
-
-### Frontend Testing
-1. **UI Display:**
- - User column should display in queue list
- - Display name should be shown when available
- - Email should be shown as fallback when display name is missing
- - User ID should be shown when both display name and email are missing
- - Tooltip should show full username on hover
-
-2. **Field Values Display:**
- - "Hidden for privacy" message should appear when viewing other users' items
- - Own items should show field values normally
-
-3. **Multi-Browser Testing:**
- - Open queue tab in two browsers with different users
- - Start generation in one browser
- - Verify other browser doesn't see the preview/progress
- - Verify admin user can see all generations
-
-### Integration Testing
-1. Multi-user scenarios with simultaneous generations
-2. Queue updates across multiple browser windows
-3. Admin vs. non-admin privilege differentiation
-4. Socket reconnection handling
-
-## Known Limitations
-
-1. **TypeScript Types:**
- - The OpenAPI schema needs to be regenerated to include new fields
- - Run: `cd invokeai/frontend/web && python ../../../scripts/generate_openapi_schema.py | pnpm typegen`
-
-2. **Backward Compatibility:**
- - System user ("system") entries will not have display name or email
- - Existing queue items from before Migration 25 will have user_id="system"
-
-3. **Socket.IO Session Storage:**
- - Socket.IO's in-memory session storage may not persist across server restarts
- - Consider implementing persistent session storage if needed for production
-
-## Future Enhancements
-
-1. Add user filtering to queue list (show only my items vs. all items)
-2. Add permission system for queue management operations (cancel, retry, delete)
-3. Implement queue item ownership transfer for administrative purposes
-4. Add audit logging for queue operations with user attribution
-5. Consider implementing user-specific queue limits or quotas
diff --git a/docs/src/content/docs/features/gallery.mdx b/docs/src/content/docs/features/gallery.mdx
index 1d490b33d76..6b3ad2e47cb 100644
--- a/docs/src/content/docs/features/gallery.mdx
+++ b/docs/src/content/docs/features/gallery.mdx
@@ -1,6 +1,6 @@
---
title: Gallery Panel
-description: Learn how to manage, organize, and use your generated images and assets with the Gallery Panel in InvokeAI.
+description: Learn how to manage, organize, and use your generated images, videos, and assets with the Gallery Panel in InvokeAI.
lastUpdated: 2026-02-19
sidebar:
order: 1
@@ -8,7 +8,7 @@ sidebar:
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.

@@ -51,10 +51,10 @@ 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.
+- **Delete Board:** Permanently removes the board and all of its contents — both images **and** videos.
:::danger
-Deleting a board will **permanently delete all images** contained within it. Proceed with caution!
+Deleting a board will **permanently delete all images and videos** contained within it. Proceed with caution!
:::
### Board Contents
@@ -130,6 +130,27 @@ 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.
+
+### 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..3e3d31104a3
--- /dev/null
+++ b/docs/src/content/docs/features/video-generation.mdx
@@ -0,0 +1,251 @@
+---
+title: Video Generation (experimental)
+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-05-13
+---
+
+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.
+:::
+
+---
+
+## 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.
+:::
+
+### 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 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.
+:::
+
+### Recommended starting parameters
+
+| | T2V-A14B + Lightning | T2V-A14B | I2V-A14B + Lightning | TI2V-5B (T2V or I2V) |
+|---|---|---|---|---|
+| Steps | 4 | 40–50 | 4 | 40–50 |
+| 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 (FlowMatchEuler) | Auto | Auto | 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/start-here/system-requirements.mdx b/docs/src/content/docs/start-here/system-requirements.mdx
index 114698ce158..5eff2bc427a 100644
--- a/docs/src/content/docs/start-here/system-requirements.mdx
+++ b/docs/src/content/docs/start-here/system-requirements.mdx
@@ -2,7 +2,7 @@
title: Hardware Requirements
sidebar:
order: 1
-lastUpdated: 2026-02-18
+lastUpdated: 2026-05-11
---
import { Tabs, TabItem, Steps } from '@astrojs/starlight/components'
@@ -28,6 +28,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/dependencies.py b/invokeai/app/api/dependencies.py
index e7468c1bca4..9cee3cc637d 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_records.image_records_sqlite import SqliteImageRecordStorage
from invokeai.app.services.images.images_default import ImageService
@@ -51,6 +53,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 (
@@ -62,6 +67,7 @@
QwenImageConditioningInfo,
SD3ConditioningInfo,
SDXLConditioningInfo,
+ WanConditioningInfo,
ZImageConditioningInfo,
)
from invokeai.backend.util.logging import InvokeAILogger
@@ -107,6 +113,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
@@ -131,6 +138,10 @@ def initialize(
bulk_download = BulkDownloadService()
image_records = SqliteImageRecordStorage(db=db)
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](
@@ -152,6 +163,7 @@ def initialize(
ZImageConditioningInfo,
QwenImageConditioningInfo,
AnimaConditioningInfo,
+ WanConditioningInfo,
],
ephemeral=True,
),
@@ -221,6 +233,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/boards.py b/invokeai/app/api/routers/boards.py
index 6897e90aff4..adba3cb672b 100644
--- a/invokeai/app/api/routers/boards.py
+++ b/invokeai/app/api/routers/boards.py
@@ -21,6 +21,14 @@ 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.",
+ )
@boards_router.post(
@@ -116,19 +124,34 @@ 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
+
try:
if include_images is True:
deleted_images = ApiDependencies.invoker.services.board_images.get_all_board_image_names_for_board(
board_id=board_id,
categories=None,
is_intermediate=None,
+ user_id=cascade_user_id,
+ )
+ deleted_videos = ApiDependencies.invoker.services.board_video_records.get_all_board_video_names_for_board(
+ board_id=board_id,
+ categories=None,
+ is_intermediate=None,
+ user_id=cascade_user_id,
)
- ApiDependencies.invoker.services.images.delete_images_on_board(board_id=board_id)
+ ApiDependencies.invoker.services.images.delete_images_on_board(board_id=board_id, user_id=cascade_user_id)
+ ApiDependencies.invoker.services.videos.delete_videos_on_board(board_id=board_id, user_id=cascade_user_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,
)
else:
deleted_board_images = ApiDependencies.invoker.services.board_images.get_all_board_image_names_for_board(
@@ -136,11 +159,20 @@ 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 Exception:
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..4ffa5238802
--- /dev/null
+++ b/invokeai/app/api/routers/gallery.py
@@ -0,0 +1,97 @@
+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 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.",
+ ),
+ offset: int = Query(default=0, description="The page offset"),
+ limit: int = Query(default=10, 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 a3ae6fce82b..6f763b20230 100644
--- a/invokeai/app/api/routers/images.py
+++ b/invokeai/app/api/routers/images.py
@@ -551,6 +551,10 @@ async def delete_images_from_list(
image_names: list[str] = Body(description="The list of names of images to delete", embed=True),
) -> DeleteImagesResult:
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()
affected_boards: set[str] = set()
for image_name in image_names:
@@ -562,7 +566,7 @@ async def delete_images_from_list(
deleted_images.add(image_name)
affected_boards.add(board_id)
except HTTPException:
- raise
+ continue
except Exception:
pass
return DeleteImagesResult(
diff --git a/invokeai/app/api/routers/videos.py b/invokeai/app/api/routers/videos.py
new file mode 100644
index 00000000000..1206f5647f3
--- /dev/null
+++ b/invokeai/app/api/routers/videos.py
@@ -0,0 +1,677 @@
+import re
+import tempfile
+import traceback
+from pathlib import Path
+from typing import Optional
+
+from fastapi import Body, HTTPException, Query, Request, Response, UploadFile
+from fastapi import Path as PathParam
+from fastapi.responses import FileResponse
+from fastapi.routing import APIRouter
+from pydantic import BaseModel, Field
+
+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.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, VideoRecordChanges
+from invokeai.app.services.videos.videos_common import (
+ AddVideosToBoardResult,
+ DeleteVideosResult,
+ RemoveVideosFromBoardResult,
+ StarredVideosResult,
+ UnstarredVideosResult,
+ VideoDTO,
+ VideoUrlsDTO,
+)
+from invokeai.app.util.video_thumbnails import 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
+
+
+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
+
+
+@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."""
+ # 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.
+ 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)",
+ )
+ tmp.write(chunk)
+ tmp.close()
+
+ try:
+ width, height, duration, fps = probe_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 = ApiDependencies.invoker.services.videos.create(
+ source_path=tmp_path,
+ width=width,
+ height=height,
+ duration=duration,
+ fps=fps,
+ 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_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],
+ 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.
+ deleted_videos: set[str] = set()
+ affected_boards: set[str] = set()
+ for video_name in 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:
+ pass
+ return DeleteVideosResult(
+ deleted_videos=list(deleted_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)
+
+
+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
+ 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,
+ 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.
+
+ Like the image equivalent, this endpoint is intentionally unauthenticated because browsers
+ load videos via tags which cannot send Bearer tokens. Video names are UUIDs,
+ providing security through unguessability.
+ """
+ try:
+ path_str = ApiDependencies.invoker.services.videos.get_path(video_name)
+ except Exception:
+ raise HTTPException(status_code=404)
+
+ path = Path(path_str)
+ if not path.exists():
+ raise HTTPException(status_code=404)
+
+ file_size = path.stat().st_size
+ range_header = request.headers.get("range") or request.headers.get("Range")
+
+ common_headers = {
+ "Accept-Ranges": "bytes",
+ "Cache-Control": f"max-age={VIDEO_MAX_AGE}",
+ "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 the file via sendfile() rather than reading it into RAM — multi-GB
+ # MP4 downloads (clients without Range, CLI tools, CDN edge fetches) would
+ # otherwise allocate a multi-GB Python bytes object per request.
+ return FileResponse(
+ path,
+ media_type="video/mp4",
+ headers=common_headers,
+ )
+
+ 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
+ with open(path, "rb") as f:
+ f.seek(start)
+ # Read at most one chunk; clients ask for more via subsequent ranges.
+ read_length = min(length, RANGE_CHUNK_SIZE)
+ chunk = f.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)),
+ },
+ )
+
+
+@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(
+ video_name: str = PathParam(description="The name of thumbnail file to get"),
+) -> Response:
+ """Returns the first-frame WebP thumbnail of a video. Unauthenticated; UUIDs provide unguessability."""
+ try:
+ path = ApiDependencies.invoker.services.videos.get_path(video_name, thumbnail=True)
+ return FileResponse(
+ path,
+ media_type="image/webp",
+ headers={"Cache-Control": f"max-age={VIDEO_MAX_AGE}"},
+ )
+ except Exception:
+ raise HTTPException(status_code=404)
+
+
+@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.",
+ ),
+ offset: int = Query(default=0, description="The page offset"),
+ limit: int = Query(default=10, 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:
+ 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:
+ raise
+ 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:
+ 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:
+ raise
+ 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:
+ _assert_video_direct_owner(video_name, current_user)
+ old_board_id = ApiDependencies.invoker.services.board_video_records.get_board_for_video(video_name)
+ if old_board_id is not None:
+ _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_app.py b/invokeai/app/api_app.py
index 4b79e1eeb0c..4e7b1634e14 100644
--- a/invokeai/app/api_app.py
+++ b/invokeai/app/api_app.py
@@ -23,6 +23,7 @@
client_state,
custom_nodes,
download_queue,
+ gallery,
images,
model_manager,
model_relationships,
@@ -30,6 +31,7 @@
session_queue,
style_presets,
utilities,
+ videos,
virtual_boards,
workflows,
)
@@ -177,6 +179,8 @@ async def dispatch(self, request: Request, call_next: RequestResponseEndpoint):
app.include_router(model_manager.model_manager_router, prefix="/api")
app.include_router(download_queue.download_queue_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 e53aeb417b2..bc0fbc8f07b 100644
--- a/invokeai/app/invocations/fields.py
+++ b/invokeai/app/invocations/fields.py
@@ -140,6 +140,7 @@ class UIComponent(str, Enum, metaclass=MetaEnum):
None_ = "none"
Textarea = "textarea"
Slider = "slider"
+ VideoFrameIndex = "video-frame-index"
class FieldDescriptions:
@@ -173,6 +174,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"
@@ -240,6 +244,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"""
@@ -364,6 +374,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 0c96cdb1d9d..c33d207fec4 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,46 @@ 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="Optional separate LoRAs for the low-noise expert (Wan 2.2 A14B). "
+ "If empty and transformer_low_noise is set, the primary 'loras' list is reused.",
+ )
+ 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 7ec6c3dc149..426f7894fe2 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
@@ -497,6 +501,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"""
@@ -508,6 +550,53 @@ 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) -> "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
+ 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:
+ video_dto = context.videos.get_dto(self.video.video_name)
+ return VideoOutput.build(video_dto=video_dto)
+
+
@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..b6246ede8eb
--- /dev/null
+++ b/invokeai/app/invocations/video_concat.py
@@ -0,0 +1,237 @@
+"""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. All decoded frames live in RAM at once; this is fine
+for the short clips the I2V chain produces (a few hundred frames at 832x480),
+but be aware before piping in long uploads.
+"""
+
+import tempfile
+from pathlib import Path
+from typing import Literal, Optional
+
+import imageio.v3 as iio
+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.shared.invocation_context import InvocationContext
+from invokeai.app.util.video_thumbnails import probe_video
+
+TransitionMode = Literal["cut", "crossfade", "fade_through_black"]
+
+
+def _crossfade(a_tail: list[np.ndarray], b_head: list[np.ndarray]) -> list[np.ndarray]:
+ """Linear A→B cross-dissolve. Consumes N frames from each side, returns N blended frames."""
+ n = len(a_tail)
+ out: list[np.ndarray] = []
+ 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
+ out.append(np.clip(blended, 0, 255).astype(np.uint8))
+ return out
+
+
+def _fade_through_black(a_tail: list[np.ndarray], b_head: list[np.ndarray]) -> list[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.
+ """
+ out: list[np.ndarray] = []
+ 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)
+ out.append(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)
+ out.append(np.clip(fb.astype(np.float32) * alpha, 0, 255).astype(np.uint8))
+ return out
+
+
+@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]
+ output_fps = float(self.fps) if self.fps is not None else (first_fps or 16.0)
+
+ context.util.signal_progress(f"Decoding {len(self.videos)} clip(s)")
+ clip_frames: list[list[np.ndarray]] = []
+ for idx, p in enumerate(paths):
+ # iio.imiter is a generator — collecting to a list keeps things simple and the
+ # downstream blending math straightforward. Memory cost is fine for I2V-length
+ # clips; if this ever needs to handle hour-long uploads, switch to streaming.
+ frames = [np.ascontiguousarray(f) for f in iio.imiter(p, plugin="FFMPEG")]
+ if not frames:
+ raise ValueError(f"Input video {idx} ({self.videos[idx].video_name}) decoded to zero frames.")
+ clip_frames.append(frames)
+
+ # Validate transition windows fit within the surrounding clips.
+ if self.transition != "cut" and self.transition_frames > 0:
+ tf = self.transition_frames
+ # For fade_through_black, split tf asymmetrically so an odd tf still emits exactly
+ # tf frames (per the docstring contract). tail_half is consumed from the previous
+ # clip's tail (the fade-out), head_half from the next clip's head (the fade-in).
+ tail_half = tf // 2
+ head_half = tf - tail_half
+ for i, frames in enumerate(clip_frames):
+ # Each non-edge clip uses transition_frames from both its head and tail.
+ head_need = 0 if i == 0 else (tf if self.transition == "crossfade" else head_half)
+ tail_need = 0 if i == len(clip_frames) - 1 else (tf if self.transition == "crossfade" else tail_half)
+ if head_need + tail_need > len(frames):
+ raise ValueError(
+ f"Clip {i} has {len(frames)} frames but the requested transitions need "
+ f"{head_need} from its head + {tail_need} from its tail. Lower "
+ f"transition_frames or use longer clips."
+ )
+
+ context.util.signal_progress(f"Joining clips ({self.transition})")
+ output_frames = self._assemble(clip_frames)
+
+ if not output_frames:
+ raise ValueError("Concatenation produced zero output frames.")
+
+ num_frames = len(output_frames)
+ duration = num_frames / output_fps
+ context.logger.info(
+ f"Encoding concatenated MP4: {num_frames} frames @ {output_fps:.2f} fps "
+ f"({duration:.2f}s) at {width}x{height}"
+ )
+ context.util.signal_progress(f"Encoding MP4 ({num_frames} frames @ {output_fps:.2f} fps)")
+
+ tmp = tempfile.NamedTemporaryFile(prefix="invokeai_video_concat_", suffix=".mp4", delete=False)
+ tmp.close()
+ tmp_path = Path(tmp.name)
+ try:
+ iio.imwrite(
+ tmp_path,
+ output_frames,
+ plugin="FFMPEG",
+ codec="libx264",
+ fps=output_fps,
+ )
+ 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 _assemble(self, clip_frames: list[list[np.ndarray]]) -> list[np.ndarray]:
+ if self.transition == "cut" or self.transition_frames == 0:
+ return [f for frames in clip_frames for f in frames]
+
+ tf = self.transition_frames
+ if self.transition == "crossfade":
+ # Reduction layout: keep clip[i] minus tf from its tail (except the last clip),
+ # then insert tf blended frames at each boundary.
+ output: list[np.ndarray] = []
+ for i, frames in enumerate(clip_frames):
+ head_trim = 0 if i == 0 else tf
+ tail_trim = 0 if i == len(clip_frames) - 1 else tf
+ output.extend(frames[head_trim : len(frames) - tail_trim])
+ if i < len(clip_frames) - 1:
+ a_tail = frames[len(frames) - tail_trim :]
+ b_head = clip_frames[i + 1][:tf]
+ output.extend(_crossfade(a_tail, b_head))
+ return output
+
+ # fade_through_black: each boundary emits exactly `tf` frames. To preserve that
+ # contract for odd tf, the fade-out (consumed from clip[i] tail) gets tf // 2 frames
+ # and the fade-in (consumed from clip[i+1] head) gets the remainder. Even tf is
+ # symmetric as before.
+ tail_half = tf // 2
+ head_half = tf - tail_half
+ if tail_half == 0 and head_half == 0:
+ return [f for frames in clip_frames for f in frames]
+ output_ftb: list[np.ndarray] = []
+ for i, frames in enumerate(clip_frames):
+ head_trim = 0 if i == 0 else head_half
+ tail_trim = 0 if i == len(clip_frames) - 1 else tail_half
+ output_ftb.extend(frames[head_trim : len(frames) - tail_trim])
+ if i < len(clip_frames) - 1:
+ a_tail = frames[len(frames) - tail_trim :] if tail_trim else []
+ b_head = clip_frames[i + 1][:head_half] if head_half else []
+ output_ftb.extend(_fade_through_black(a_tail, b_head))
+ return output_ftb
diff --git a/invokeai/app/invocations/video_frame_extract.py b/invokeai/app/invocations/video_frame_extract.py
new file mode 100644
index 00000000000..66dea47fb71
--- /dev/null
+++ b/invokeai/app/invocations/video_frame_extract.py
@@ -0,0 +1,117 @@
+"""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.
+"""
+
+import imageio.v3 as iio
+
+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 ImageOutput
+from invokeai.app.services.shared.invocation_context import InvocationContext
+from invokeai.app.util.video_thumbnails import extract_video_frame, probe_video
+
+
+@invocation(
+ "video_frame_extract",
+ title="Frame from Video",
+ tags=["video", "image", "frame"],
+ category="image",
+ version="1.0.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.",
+ )
+
+ 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 (iio.improps) 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)
+
+
+def _decoder_frame_count(video_path) -> int | None:
+ """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 the ``duration * fps`` estimate used by the legacy code path,
+ which can overshoot by one on VFR uploads or containers with imprecise metadata.
+ """
+ import math
+
+ 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
+
+ # Fallback: cv2 reads libx264 frame counts exactly. We only import cv2 here because
+ # it's a heavy module and the improps path covers other codecs without paying that cost.
+ 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
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..95fe398a314
--- /dev/null
+++ b/invokeai/app/invocations/video_frame_extract_range.py
@@ -0,0 +1,188 @@
+"""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 pathlib import Path
+
+import imageio.v3 as iio
+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.invocations.video_frame_extract import _decoder_frame_count
+from invokeai.app.services.shared.invocation_context import InvocationContext
+from invokeai.app.util.video_thumbnails import probe_video
+
+
+@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.0.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
+ defaults to 16 fps, matching the other Wan video nodes.
+
+ 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=16,
+ ge=1,
+ le=120,
+ description="Output frame rate.",
+ )
+
+ 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."
+ )
+
+ output_fps = float(self.fps)
+
+ context.util.signal_progress(f"Decoding frames {start}-{end} of {n_frames}")
+ # imageio's iter_index isn't exposed by iio.imiter, so we enumerate and skip.
+ # The downstream slice is contiguous; bailing early after `end` keeps us from
+ # decoding the rest of the file unnecessarily for short ranges of long videos.
+ frames: list[np.ndarray] = []
+ for idx, frame in enumerate(iio.imiter(video_path, plugin="FFMPEG")):
+ if idx < start:
+ continue
+ if idx > end:
+ break
+ frames.append(np.ascontiguousarray(frame))
+
+ if not frames:
+ raise ValueError(
+ f"Decoded zero frames for range {start}-{end} of {self.video.video_name} "
+ f"(probed {n_frames} frames). The container's metadata may be inaccurate."
+ )
+
+ num_frames = len(frames)
+ out_duration = num_frames / output_fps
+ context.logger.info(
+ f"Encoding trimmed MP4: {num_frames} frames @ {output_fps:.2f} fps "
+ f"({out_duration:.2f}s) at {width}x{height}"
+ )
+ context.util.signal_progress(f"Encoding MP4 ({num_frames} frames @ {output_fps:.2f} fps)")
+
+ tmp = tempfile.NamedTemporaryFile(prefix="invokeai_video_range_", suffix=".mp4", delete=False)
+ tmp.close()
+ tmp_path = Path(tmp.name)
+ try:
+ iio.imwrite(
+ tmp_path,
+ frames,
+ plugin="FFMPEG",
+ codec="libx264",
+ fps=output_fps,
+ )
+ 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..b5edf22946f
--- /dev/null
+++ b/invokeai/app/invocations/wan_denoise.py
@@ -0,0 +1,699 @@
+"""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 _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 FlowMatchEulerDiscreteScheduler, UniPCMultistepScheduler
+
+ if variant == WanVariantType.TI2V_5B:
+ # Wan-AI/Wan2.2-TI2V-5B-Diffusers/scheduler/scheduler_config.json. The
+ # combination of flow_prediction + use_flow_sigmas + flow_shift=5.0 is
+ # what differentiates this from a generic UniPC schedule; without it
+ # samples drift on this model.
+ return UniPCMultistepScheduler(
+ num_train_timesteps=1000,
+ solver_order=2,
+ prediction_type="flow_prediction",
+ flow_shift=5.0,
+ use_flow_sigmas=True,
+ solver_type="bh2",
+ final_sigmas_type="zero",
+ )
+ # A14B variants ship FlowMatchEulerDiscreteScheduler at default settings.
+ return FlowMatchEulerDiscreteScheduler()
+
+
+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.
+ if self._active_lora_ctx is not None:
+ self._active_lora_ctx.__exit__(None, None, None)
+ if self._active_device_ctx is not None:
+ self._active_device_ctx.__exit__(None, None, None)
+ 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 a flow-matching Euler schedule via Diffusers'
+ ``FlowMatchEulerDiscreteScheduler``. 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)
+ 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)
+ do_cfg = self.guidance_scale != 1.0 and self.negative_conditioning is not None
+ 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``, falling back
+ # to the primary list if empty (matches the WanTransformerField semantics).
+ # 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 or self.transformer.loras
+ 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 do_cfg and neg_cond is not None:
+ 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``.
+
+ Wan model variants ship different schedulers — e.g. TI2V-5B uses
+ ``UniPCMultistepScheduler`` with ``flow_shift=5.0``, while the
+ standard A14B reference uses ``FlowMatchEulerDiscreteScheduler``.
+ We dispatch on ``_class_name`` so the noise schedule matches what the
+ model was trained against. When no on-disk config is available
+ (standalone GGUF / single-file installs that don't ship a
+ ``scheduler/`` directory), fall back to a variant-aware default —
+ TI2V-5B gets its UniPC scheduler with the right flow params instead
+ of the generic FlowMatchEuler, which otherwise produces drifty
+ samples for that model.
+ """
+ import json
+
+ import diffusers
+ from diffusers import FlowMatchEulerDiscreteScheduler
+
+ scheduler_dir = _scheduler_path_for_transformer(context, self.transformer)
+ if scheduler_dir is None:
+ variant = _resolve_variant(context, self.transformer)
+ return _default_scheduler_for_variant(variant)
+
+ # 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_cls is None:
+ scheduler_cls = FlowMatchEulerDiscreteScheduler
+
+ 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..693391e3996
--- /dev/null
+++ b/invokeai/app/invocations/wan_ideal_dimensions.py
@@ -0,0 +1,116 @@
+"""Compute Wan 2.2 I2V-compatible pixel dimensions for a target short-side resolution.
+
+Wan's transformer ``patch_size=(1, 2, 2)`` combined with the VAE's 8x spatial
+compression requires pixel dimensions to be multiples of 16 (see
+``wan_ref_image_encoder.py``). This node 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 ``wan_ref_image_encoder``
+and the matching ``wan_denoise`` inputs.
+"""
+
+import math
+from typing import Literal
+
+from invokeai.app.invocations.baseinvocation import BaseInvocation, 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"]
+
+# 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)",
+}
+
+
+@invocation(
+ "wan_i2v_ideal_dimensions",
+ title="Wan 2.2 I2V Ideal Dimensions",
+ tags=["wan", "video", "dimensions", "math"],
+ category="video",
+ version="1.1.0",
+)
+class WanI2VIdealDimensionsInvocation(BaseInvocation):
+ """Compute Wan I2V-compatible (width, height) for a chosen resolution preset.
+
+ 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 (Wan's pixel-grid
+ constraint). Wire from ``Image Primitive``'s width/height outputs and into
+ ``wan_ref_image_encoder`` / ``wan_denoise``.
+ """
+
+ 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: Literal["nearest", "floor", "ceiling"] = InputField(
+ default="nearest",
+ 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)."
+ ),
+ )
+
+ def invoke(self, context: InvocationContext) -> IdealSizeOutput:
+ short = min(self.width, self.height)
+ if short <= 0:
+ raise ValueError("Source dimensions must be positive.")
+
+ target_short_side = WAN_TARGET_RESOLUTION_PX[self.target_resolution]
+ # Reject sources so narrow that the scaled long side is still under one Wan
+ # pixel grid (16 px). The downstream clamp to ``max(w, 16)`` would otherwise
+ # silently return 16×16, which has no relation to the requested aspect ratio
+ # — better to fail fast and have the workflow author fix the inputs.
+ long_side = max(self.width, self.height)
+ if long_side < 16:
+ raise ValueError(
+ f"Source longer side ({long_side}px) is smaller than the Wan pixel grid (16px). "
+ "Use an input image at least 16px on its longer side."
+ )
+
+ scale = target_short_side / short
+ raw_w = self.width * scale
+ raw_h = self.height * scale
+
+ if self.rounding == "floor":
+ w = int(raw_w // 16) * 16
+ h = int(raw_h // 16) * 16
+ elif self.rounding == "ceiling":
+ w = int(math.ceil(raw_w / 16)) * 16
+ h = int(math.ceil(raw_h / 16)) * 16
+ else: # nearest
+ w = round(raw_w / 16) * 16
+ h = round(raw_h / 16) * 16
+
+ # Belt-and-suspenders clamp against floor-of-<16 — should be unreachable now
+ # that the long_side guard above runs first, but keeps the contract that the
+ # returned dimensions are always valid Wan inputs.
+ w = max(w, 16)
+ h = max(h, 16)
+ 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..d5827110d0a
--- /dev/null
+++ b/invokeai/app/invocations/wan_image_to_latents.py
@@ -0,0 +1,104 @@
+"""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.stable_diffusion.diffusers_pipeline import image_resized_to_grid_as_tensor
+from invokeai.backend.util.devices import TorchDevice
+from invokeai.backend.util.vae_working_memory import estimate_vae_working_memory_flux
+
+
+@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_flux(
+ operation="encode",
+ image_tensor=image_tensor,
+ vae=vae_info.model,
+ )
+
+ 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=TorchDevice.choose_torch_device(), 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..049959646c1
--- /dev/null
+++ b/invokeai/app/invocations/wan_latents_to_image.py
@@ -0,0 +1,93 @@
+"""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.util.devices import TorchDevice
+from invokeai.backend.util.vae_working_memory import estimate_vae_working_memory_flux
+
+
+@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)
+
+ 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__}.")
+
+ estimated_working_memory = estimate_vae_working_memory_flux(
+ operation="decode",
+ image_tensor=latents,
+ vae=vae_info.model,
+ )
+
+ 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=TorchDevice.choose_torch_device(), 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)
+
+ # 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..703a8e4cffa
--- /dev/null
+++ b/invokeai/app/invocations/wan_latents_to_video.py
@@ -0,0 +1,151 @@
+"""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 pathlib import Path
+
+import imageio.v3 as iio
+import numpy as np
+import torch
+from diffusers.models.autoencoders import AutoencoderKLWan
+from einops import rearrange
+
+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.shared.invocation_context import InvocationContext
+from invokeai.backend.util.devices import TorchDevice
+
+
+@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)
+ 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__}.")
+
+ with vae_info.model_on_device() as (_, vae):
+ assert isinstance(vae, AutoencoderKLWan)
+ _, _, t_lat, h_lat, w_lat = latents.shape
+ t_pixel = (t_lat - 1) * 4 + 1
+ context.logger.info(
+ f"Running Wan VAE decode: {t_lat} latent frames -> {t_pixel} pixel frames at {w_lat * 8}x{h_lat * 8}"
+ )
+ context.util.signal_progress("Running Wan VAE decode (video)")
+
+ vae_dtype = next(iter(vae.parameters())).dtype
+ latents = latents.to(device=TorchDevice.choose_torch_device(), dtype=vae_dtype)
+
+ TorchDevice.empty_cache()
+
+ 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]
+
+ decoded = decoded.clamp(-1, 1)
+ # Take batch 0 (we generate one video at a time).
+ decoded = decoded[0] # [C, T, H, W]
+
+ TorchDevice.empty_cache()
+
+ # Convert to a list of numpy uint8 frames [H, W, C].
+ decoded = rearrange(decoded, "c t h w -> t h w c")
+ # [-1, 1] -> [0, 255]
+ frames = (127.5 * (decoded.cpu().float() + 1.0)).round().clamp(0, 255).byte().numpy()
+ frames_list = [np.ascontiguousarray(frames[i]) for i in range(frames.shape[0])]
+
+ if not frames_list:
+ raise ValueError("Wan VAE decode produced zero frames.")
+
+ height, width = frames_list[0].shape[:2]
+ num_frames = len(frames_list)
+ duration = num_frames / float(self.fps)
+
+ # Encode to a temporary MP4 via imageio's FFMPEG plugin (backed by the
+ # bundled imageio-ffmpeg binary). libx264 + yuv420p is the default for
+ # this plugin, which is what we want for broadly-compatible browser
+ # playback — no need to override.
+ 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)")
+ iio.imwrite(
+ tmp_path,
+ frames_list,
+ plugin="FFMPEG",
+ codec="libx264",
+ fps=self.fps,
+ )
+ 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..66034685e00
--- /dev/null
+++ b/invokeai/app/invocations/wan_lora_loader.py
@@ -0,0 +1,188 @@
+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
+
+# 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 _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.0",
+ 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; the low-noise routing is harmless but ignored.
+ """
+
+ 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}!")
+
+ output = WanLoRALoaderOutput()
+ if self.transformer is None:
+ return output
+
+ lora_config = context.models.get_config(self.lora)
+ lora_expert = getattr(lora_config, "expert", None)
+ to_primary, to_low_noise = _resolve_target(self.target, lora_expert)
+
+ # 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.0",
+ 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",
+ )
+ 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()
+
+ for lora in loras:
+ if lora is None or lora.lora.key in added:
+ continue
+
+ if not context.models.exists(lora.lora.key):
+ raise ValueError(f"Unknown lora: {lora.lora.key}!")
+
+ if lora.lora.base is not BaseModelType.Wan:
+ raise ValueError(
+ f"LoRA '{lora.lora.key}' is for "
+ f"{lora.lora.base.value if lora.lora.base else 'unknown'} models, "
+ "not Wan 2.2."
+ )
+
+ lora_config = context.models.get_config(lora.lora)
+ lora_expert = getattr(lora_config, "expert", None)
+ to_primary, to_low_noise = _resolve_target("auto", lora_expert)
+
+ added.add(lora.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..a4d986d8aa3
--- /dev/null
+++ b/invokeai/app/invocations/wan_model_loader.py
@@ -0,0 +1,239 @@
+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
+
+
+@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.0",
+ 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
+
+ # 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.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:
+ 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")
+
+ # 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
+ # 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 getattr(main_config, "variant", None) and main_config.variant.value == "t2v_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_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:
+ 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_format(context, self.component_source)
+ 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."
+ )
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..33151157a0e
--- /dev/null
+++ b/invokeai/app/invocations/wan_ref_image_encoder.py
@@ -0,0 +1,161 @@
+"""Reference-image (VAE-latent) encoder for Wan 2.2 I2V-A14B.
+
+Wan 2.2 I2V conditions on a reference image by VAE-encoding it and
+concatenating the resulting latents to the noise latents along the channel
+dim. This invocation produces the 20-channel condition tensor (4-ch first-
+frame mask + 16-ch image latents) the denoise loop will consume.
+
+Supports both single-frame (image I2V, ``num_frames=1``) and multi-frame
+(video I2V, e.g. ``num_frames=81``) condition tensors.
+"""
+
+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.util.devices import TorchDevice
+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.1.0",
+ classification=Classification.Prototype,
+)
+class WanRefImageEncoderInvocation(BaseInvocation):
+ """VAE-encode a reference image into Wan 2.2 I2V conditioning.
+
+ Output is a ``[1, 20, T_lat, height // 8, width // 8]`` condition tensor
+ that the denoise loop concatenates to the 16-channel noise latents each
+ step, producing the 36-channel input the I2V-A14B transformer expects.
+
+ For image (single-frame) I2V leave ``num_frames=1`` (T_lat=1). For video
+ I2V set ``num_frames`` to match the value on the video-denoise node
+ (e.g. 81 for the Wan 2.2 reference defaults).
+
+ Only works with I2V-A14B (the denoise loop's variant gate enforces this).
+ For T2V or TI2V-5B, omit this node entirely.
+ """
+
+ image: ImageField = InputField(description="Reference image to condition on.")
+ 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",
+ )
+
+ @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")
+
+ vae_info = context.models.load(self.vae.vae)
+ device = TorchDevice.choose_torch_device()
+ target_dtype = TorchDevice.choose_bfloat16_safe_dtype(device)
+
+ with vae_info.model_on_device() as (_, vae):
+ if not isinstance(vae, AutoencoderKLWan):
+ raise TypeError(f"Reference-image encoder requires AutoencoderKLWan, got {type(vae).__name__}.")
+ context.util.signal_progress(
+ "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 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,
+ )
+
+ 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..396819d5434
--- /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=226, 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..f201949ff55
--- /dev/null
+++ b/invokeai/app/invocations/wan_video_denoise.py
@@ -0,0 +1,405 @@
+"""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,
+)
+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)
+ spatial_scale = get_spatial_scale_factor(variant)
+
+ # Reuse the image denoise's scheduler construction so we pick up whatever
+ # scheduler the variant ships with (FlowMatchEulerDiscreteScheduler,
+ # UniPCMultistepScheduler, etc.).
+ 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)
+ do_cfg = self.guidance_scale != 1.0 and self.negative_conditioning is not None
+ 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}."
+ )
+ if variant == WanVariantType.TI2V_5B and (self.width % 32 or self.height % 32):
+ raise ValueError(
+ f"TI2V-5B I2V requires width and height to be multiples of 32 "
+ f"(got {self.width}x{self.height}). Wan 2.2-VAE 16x spatial * "
+ f"transformer patch_size 2 = pixel dims must divide by 32."
+ )
+ 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
+
+ high_loras = self.transformer.loras
+ low_loras = self.transformer.loras_low_noise or self.transformer.loras
+ 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 do_cfg and neg_cond is not None:
+ 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 febd4f4d4b1..af5bb99d9ec 100644
--- a/invokeai/app/run_app.py
+++ b/invokeai/app/run_app.py
@@ -1,3 +1,22 @@
+import os
+
+# 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
importing from api_app does a bunch of stuff - it's more like calling a function than importing a module.
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/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..dcf738be305
--- /dev/null
+++ b/invokeai/app/services/gallery/gallery_base.py
@@ -0,0 +1,45 @@
+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
+
+
+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,
+ ) -> GalleryItemNamesResult:
+ """Returns ordered (kind, name) refs for optimistic UI / virtualized lists."""
+ 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..9a3f496a06f
--- /dev/null
+++ b/invokeai/app/services/gallery/gallery_default.py
@@ -0,0 +1,297 @@
+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
+
+
+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,
+ )
+
+ if starred_first:
+ order_clause = f"ORDER BY starred DESC, created_at {order_dir.value}"
+ else:
+ order_clause = f"ORDER BY created_at {order_dir.value}"
+
+ 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,
+ ) -> 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,
+ )
+ 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,
+ )
+
+ if starred_first:
+ order_clause = f"ORDER BY starred DESC, created_at {order_dir.value}"
+ else:
+ order_clause = f"ORDER BY created_at {order_dir.value}"
+
+ 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 _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,
+ ) -> 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 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/images/images_base.py b/invokeai/app/services/images/images_base.py
index aebbead2f35..15a9bab4908 100644
--- a/invokeai/app/services/images/images_base.py
+++ b/invokeai/app/services/images/images_base.py
@@ -148,8 +148,13 @@ 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):
+ """Deletes all images on a board.
+
+ 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.
+ """
pass
@abstractmethod
diff --git a/invokeai/app/services/images/images_default.py b/invokeai/app/services/images/images_default.py
index 4a190f37edc..9010f149cf6 100644
--- a/invokeai/app/services/images/images_default.py
+++ b/invokeai/app/services/images/images_default.py
@@ -291,12 +291,15 @@ 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):
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,
)
for image_name in image_names:
try:
diff --git a/invokeai/app/services/invocation_services.py b/invokeai/app/services/invocation_services.py
index 2c95f87b41d..49659109287 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_records.image_records_base import ImageRecordStorageBase
from invokeai.app.services.images.images_base import ImageServiceABC
@@ -38,6 +40,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
@@ -79,6 +84,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",
):
self.board_images = board_images
self.board_image_records = board_image_records
@@ -111,3 +121,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/shared/invocation_context.py b/invokeai/app/services/shared/invocation_context.py
index e38766d5ba2..266f6e335b2 100644
--- a/invokeai/app/services/shared/invocation_context.py
+++ b/invokeai/app/services/shared/invocation_context.py
@@ -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
@@ -292,6 +293,89 @@ def get_path(self, image_name: str, thumbnail: bool = False) -> Path:
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 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
+
+ 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."""
+ 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."""
+ 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.
@@ -736,6 +820,7 @@ class InvocationContext:
def __init__(
self,
images: ImagesInterface,
+ videos: VideosInterface,
tensors: TensorsInterface,
conditioning: ConditioningInterface,
models: ModelsInterface,
@@ -748,6 +833,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
@@ -790,10 +877,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/sqlite/sqlite_util.py b/invokeai/app/services/shared/sqlite/sqlite_util.py
index 12642610c8c..3e1d5c53f3e 100644
--- a/invokeai/app/services/shared/sqlite/sqlite_util.py
+++ b/invokeai/app/services/shared/sqlite/sqlite_util.py
@@ -34,6 +34,7 @@
from invokeai.app.services.shared.sqlite_migrator.migrations.migration_29 import build_migration_29
from invokeai.app.services.shared.sqlite_migrator.migrations.migration_30 import build_migration_30
from invokeai.app.services.shared.sqlite_migrator.migrations.migration_31 import build_migration_31
+from invokeai.app.services.shared.sqlite_migrator.migrations.migration_32 import build_migration_32
from invokeai.app.services.shared.sqlite_migrator.sqlite_migrator_impl import SqliteMigrator
@@ -85,6 +86,7 @@ def init_db(config: InvokeAIAppConfig, logger: Logger, image_files: ImageFileSto
migrator.register_migration(build_migration_29())
migrator.register_migration(build_migration_30())
migrator.register_migration(build_migration_31())
+ migrator.register_migration(build_migration_32())
migrator.run_migrations()
return db
diff --git a/invokeai/app/services/shared/sqlite_migrator/migrations/migration_32.py b/invokeai/app/services/shared/sqlite_migrator/migrations/migration_32.py
new file mode 100644
index 00000000000..f1695850f75
--- /dev/null
+++ b/invokeai/app/services/shared/sqlite_migrator/migrations/migration_32.py
@@ -0,0 +1,115 @@
+"""Migration 32: 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 Migration32Callback:
+ 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,
+ 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_32() -> Migration:
+ return Migration(
+ from_version=31,
+ to_version=32,
+ callback=Migration32Callback(),
+ )
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..74e9d96fb63
--- /dev/null
+++ b/invokeai/app/services/video_files/video_files_base.py
@@ -0,0 +1,48 @@
+from abc import ABC, abstractmethod
+from pathlib import Path
+from typing import Optional
+
+
+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,
+ ) -> 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.
+ """
+ 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 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..c7af86e3b8d
--- /dev/null
+++ b/invokeai/app/services/video_files/video_files_disk.py
@@ -0,0 +1,190 @@
+import json
+import shutil
+from pathlib import Path
+from typing import Optional, Union
+
+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
+
+
+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
+
+ 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,
+ ) -> 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.
+ 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:
+ raise VideoFileSaveException from e
+
+ def delete(self, video_name: str, video_subfolder: str = "") -> None:
+ try:
+ video_path = self.get_path(video_name, video_subfolder=video_subfolder)
+ if video_path.exists():
+ video_path.unlink()
+
+ thumbnail_name = get_video_thumbnail_name(video_name)
+ thumbnail_path = self.get_path(thumbnail_name, thumbnail=True, video_subfolder=video_subfolder)
+ if thumbnail_path.exists():
+ thumbnail_path.unlink()
+
+ sidecar_path = self.__get_sidecar_path(video_name, video_subfolder=video_subfolder)
+ if sidecar_path.exists():
+ sidecar_path.unlink()
+ 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)
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..a334e2a5b66
--- /dev/null
+++ b/invokeai/app/services/video_records/video_records_base.py
@@ -0,0 +1,103 @@
+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_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..112a0d42aa6
--- /dev/null
+++ b/invokeai/app/services/video_records/video_records_sqlite.py
@@ -0,0 +1,356 @@
+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_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} LIMIT ? OFFSET ? "
+ )
+ else:
+ query_pagination = f" ORDER BY videos.created_at {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} "
+ if starred_first
+ else f" ORDER BY videos.created_at {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..e2a071107bd
--- /dev/null
+++ b/invokeai/app/services/videos/videos_base.py
@@ -0,0 +1,152 @@
+from abc import ABC, abstractmethod
+from pathlib import Path
+from typing import Callable, 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,
+)
+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,
+ ) -> VideoDTO:
+ """Creates a video by moving/copying the file at `source_path` into storage and recording it."""
+ 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) -> None:
+ """Deletes all videos on a board.
+
+ 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.
+ """
+ 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..bd37bd0366c
--- /dev/null
+++ b/invokeai/app/services/videos/videos_common.py
@@ -0,0 +1,61 @@
+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")
+
+
+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..5977e58db21
--- /dev/null
+++ b/invokeai/app/services/videos/videos_default.py
@@ -0,0 +1,318 @@
+from pathlib import Path
+from typing import Optional
+
+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,
+ ) -> 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)
+
+ 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,
+ )
+ if board_id is not None:
+ try:
+ self.__invoker.services.board_video_records.add_video_to_board(
+ board_id=board_id, video_name=video_name
+ )
+ 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,
+ )
+
+ 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 VideoFileSaveException:
+ self.__invoker.services.logger.error("Failed to save video file")
+ raise
+ except Exception as e:
+ self.__invoker.services.logger.error(f"Problem saving video record and file: {str(e)}")
+ raise e
+
+ 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:
+ try:
+ record = self.__invoker.services.video_records.get(video_name)
+ self.__invoker.services.video_files.delete(video_name, video_subfolder=record.video_subfolder)
+ self.__invoker.services.video_records.delete(video_name)
+ self._on_deleted(video_name)
+ except VideoRecordDeleteException:
+ 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:
+ 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) -> None:
+ 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
+ )
+ for video_name in video_names:
+ try:
+ record = self.__invoker.services.video_records.get(video_name)
+ self.__invoker.services.video_files.delete(video_name, video_subfolder=record.video_subfolder)
+ except Exception:
+ pass
+ self.__invoker.services.video_records.delete_many(video_names)
+ for video_name in video_names:
+ self._on_deleted(video_name)
+ 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/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..7975e2c0a56
--- /dev/null
+++ b/invokeai/app/services/workflow_records/default_workflows/Wan 2.2 Image to Image.json
@@ -0,0 +1,305 @@
+{
+ "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.0",
+ "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.0.0",
+ "label": "",
+ "notes": "",
+ "isOpen": true,
+ "isIntermediate": true,
+ "useCache": true,
+ "inputs": {
+ "image": { "name": "image", "label": "Reference Image" },
+ "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 Image to Video - Add Frames.json b/invokeai/app/services/workflow_records/default_workflows/Wan 2.2 Image to Video - Add Frames.json
new file mode 100644
index 00000000000..45b227481fd
--- /dev/null
+++ b/invokeai/app/services/workflow_records/default_workflows/Wan 2.2 Image to Video - Add Frames.json
@@ -0,0 +1,1153 @@
+{
+ "name": "Image to Video - Add Frames",
+ "author": "InvokeAI",
+ "description": "Extend an existing video by generating new frames with Wan 2.2 I2V A14B + the Lightning LoRA pair. Drop a video into the 'Video' input \u2014 the workflow extracts the penultimate frame and uses it as the reference for a new image-to-video generation, then concatenates the result onto the original with a short crossfade transition. Lightning distillation converges the denoise in 4 steps at CFG 1.0. The 'Frames' input controls how many new frames are generated; the source video's dimensions are read from the input node, so width/height on the Denoise Video node are overridden at runtime.",
+ "version": "1.0.0",
+ "contact": "",
+ "tags": "wan2.2, Text to Video, Image to Video, video, lightning, lora",
+ "notes": "Prerequisite model downloads: a Wan 2.2 I2V A14B main (Diffusers or GGUF expert pair), plus the matching I2V Lightning LoRA pair (distinct from the T2V Lightning LoRAs). For GGUF mains, also install the Component Source (Diffusers Wan I2V) OR the standalone Wan VAE + UMT5-XXL encoder. The reference image is VAE-encoded across the full latent temporal dim (frame 0 carries the image, remaining frames are zero pixels). If your Lightning LoRAs are untagged, set the 'Apply LoRA (High)' target to 'high' and 'Apply LoRA (Low)' target to 'low' manually. CFG=1.0 skips the negative-conditioning branch; the Negative Prompt content 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": "d781a3a6-eb2d-48dd-87a7-eb6898f1fca9",
+ "fieldName": "lora"
+ },
+ {
+ "nodeId": "d781a3a6-eb2d-48dd-87a7-eb6898f1fca9",
+ "fieldName": "weight"
+ },
+ {
+ "nodeId": "d781a3a6-eb2d-48dd-87a7-eb6898f1fca9",
+ "fieldName": "target"
+ },
+ {
+ "nodeId": "06bf5449-a0ca-44d0-a7cc-f37e03651761",
+ "fieldName": "lora"
+ },
+ {
+ "nodeId": "06bf5449-a0ca-44d0-a7cc-f37e03651761",
+ "fieldName": "weight"
+ },
+ {
+ "nodeId": "06bf5449-a0ca-44d0-a7cc-f37e03651761",
+ "fieldName": "target"
+ },
+ {
+ "nodeId": "7a6edc2d-f38e-a0c1-e27a-8f9dcbb20fce",
+ "fieldName": "image"
+ },
+ {
+ "nodeId": "7a6edc2d-f38e-a0c1-e27a-8f9dcbb20fce",
+ "fieldName": "num_frames"
+ },
+ {
+ "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.0",
+ "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": ""
+ }
+ },
+ "isOpen": true,
+ "isIntermediate": true,
+ "useCache": false
+ },
+ "position": {
+ "x": 131.0881399719261,
+ "y": 0.7948374148289883
+ }
+ },
+ {
+ "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": 587.9298543401748,
+ "y": -95.02732472842037
+ }
+ },
+ {
+ "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": "Negative Prompt",
+ "description": "",
+ "value": ""
+ },
+ "wan_t5_encoder": {
+ "name": "wan_t5_encoder",
+ "label": "",
+ "description": ""
+ }
+ },
+ "isOpen": true,
+ "isIntermediate": true,
+ "useCache": true
+ },
+ "position": {
+ "x": 559.3876517680619,
+ "y": 282.5706231529781
+ }
+ },
+ {
+ "id": "7a6edc2d-f38e-a0c1-e27a-8f9dcbb20fce",
+ "type": "invocation",
+ "data": {
+ "id": "7a6edc2d-f38e-a0c1-e27a-8f9dcbb20fce",
+ "version": "1.1.0",
+ "nodePack": "invokeai",
+ "label": "",
+ "notes": "",
+ "type": "wan_ref_image_encoder",
+ "inputs": {
+ "image": {
+ "name": "image",
+ "label": "Reference Image",
+ "description": ""
+ },
+ "vae": {
+ "name": "vae",
+ "label": "",
+ "description": ""
+ },
+ "width": {
+ "name": "width",
+ "label": "",
+ "description": "",
+ "value": 480
+ },
+ "height": {
+ "name": "height",
+ "label": "",
+ "description": "",
+ "value": 720
+ },
+ "num_frames": {
+ "name": "num_frames",
+ "label": "Frames",
+ "description": "",
+ "value": 81
+ }
+ },
+ "isOpen": true,
+ "isIntermediate": true,
+ "useCache": true
+ },
+ "position": {
+ "x": 557.9103163691349,
+ "y": 447.22846466649344
+ }
+ },
+ {
+ "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": 692.4427537381185,
+ "y": 767.8930416853509
+ }
+ },
+ {
+ "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": 1
+ },
+ "guidance_scale_low_noise": {
+ "name": "guidance_scale_low_noise",
+ "label": "CFG (Low)",
+ "description": "",
+ "value": 1
+ },
+ "width": {
+ "name": "width",
+ "label": "",
+ "description": "",
+ "value": 480
+ },
+ "height": {
+ "name": "height",
+ "label": "",
+ "description": "",
+ "value": 720
+ },
+ "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": 1100,
+ "y": -50
+ }
+ },
+ {
+ "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": true,
+ "useCache": true
+ },
+ "position": {
+ "x": 1550,
+ "y": -50
+ }
+ },
+ {
+ "id": "b5239ffa-041c-421c-b343-735c02709bb3",
+ "type": "invocation",
+ "data": {
+ "id": "b5239ffa-041c-421c-b343-735c02709bb3",
+ "version": "1.0.1",
+ "nodePack": "invokeai",
+ "label": "",
+ "notes": "",
+ "type": "integer",
+ "inputs": {
+ "value": {
+ "name": "value",
+ "label": "Total Frames",
+ "description": "",
+ "value": 81
+ }
+ },
+ "isOpen": true,
+ "isIntermediate": true,
+ "useCache": true
+ },
+ "position": {
+ "x": 187.29555089875728,
+ "y": 816.2354388686249
+ }
+ },
+ {
+ "id": "d781a3a6-eb2d-48dd-87a7-eb6898f1fca9",
+ "type": "invocation",
+ "data": {
+ "id": "d781a3a6-eb2d-48dd-87a7-eb6898f1fca9",
+ "version": "1.0.0",
+ "nodePack": "invokeai",
+ "label": "Apply LoRA (High)",
+ "notes": "",
+ "type": "wan_lora_loader",
+ "inputs": {
+ "lora": {
+ "name": "lora",
+ "label": "",
+ "description": ""
+ },
+ "weight": {
+ "name": "weight",
+ "label": "",
+ "description": "",
+ "value": 1
+ },
+ "target": {
+ "name": "target",
+ "label": "",
+ "description": "",
+ "value": "auto"
+ },
+ "transformer": {
+ "name": "transformer",
+ "label": "",
+ "description": ""
+ }
+ },
+ "isOpen": true,
+ "isIntermediate": true,
+ "useCache": true
+ },
+ "position": {
+ "x": 338.56334682023754,
+ "y": -333.1468780837475
+ }
+ },
+ {
+ "id": "06bf5449-a0ca-44d0-a7cc-f37e03651761",
+ "type": "invocation",
+ "data": {
+ "id": "06bf5449-a0ca-44d0-a7cc-f37e03651761",
+ "version": "1.0.0",
+ "nodePack": "invokeai",
+ "label": "Apply LoRA (Low)",
+ "notes": "",
+ "type": "wan_lora_loader",
+ "inputs": {
+ "lora": {
+ "name": "lora",
+ "label": "",
+ "description": ""
+ },
+ "weight": {
+ "name": "weight",
+ "label": "",
+ "description": "",
+ "value": 1
+ },
+ "target": {
+ "name": "target",
+ "label": "",
+ "description": "",
+ "value": "auto"
+ },
+ "transformer": {
+ "name": "transformer",
+ "label": "",
+ "description": ""
+ }
+ },
+ "isOpen": true,
+ "isIntermediate": true,
+ "useCache": true
+ },
+ "position": {
+ "x": 712.3477390652625,
+ "y": -334.8761434372357
+ }
+ },
+ {
+ "id": "642e7cda-08c1-435c-8e6b-99700b61200b",
+ "type": "invocation",
+ "data": {
+ "id": "642e7cda-08c1-435c-8e6b-99700b61200b",
+ "version": "1.0.0",
+ "nodePack": "invokeai",
+ "label": "",
+ "notes": "",
+ "type": "video",
+ "inputs": {
+ "video": {
+ "name": "video",
+ "label": "Starting Video",
+ "description": ""
+ }
+ },
+ "isOpen": true,
+ "isIntermediate": true,
+ "useCache": true
+ },
+ "position": {
+ "x": -1083.4154885344983,
+ "y": 534.2366394445222
+ }
+ },
+ {
+ "id": "956718a3-d447-4e6e-ad38-671baeb2350e",
+ "type": "invocation",
+ "data": {
+ "id": "956718a3-d447-4e6e-ad38-671baeb2350e",
+ "version": "1.0.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": -2
+ }
+ },
+ "isOpen": true,
+ "isIntermediate": true,
+ "useCache": true
+ },
+ "position": {
+ "x": -552.074720634205,
+ "y": 275.8409586730836
+ }
+ },
+ {
+ "id": "8648065d-056f-47fc-ab84-b762d976c021",
+ "type": "invocation",
+ "data": {
+ "id": "8648065d-056f-47fc-ab84-b762d976c021",
+ "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": 16
+ }
+ },
+ "isOpen": true,
+ "isIntermediate": false,
+ "useCache": true
+ },
+ "position": {
+ "x": -241.48844550023864,
+ "y": 689.2802200749895
+ }
+ },
+ {
+ "id": "1167a8f8-87b9-4e32-bf85-cff170e926ee",
+ "type": "invocation",
+ "data": {
+ "id": "1167a8f8-87b9-4e32-bf85-cff170e926ee",
+ "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": -663.8758722590726,
+ "y": 879.6897902329404
+ }
+ },
+ {
+ "id": "e39ebfc3-c2cc-49bd-8b1b-0e67b0ad4176",
+ "type": "invocation",
+ "data": {
+ "id": "e39ebfc3-c2cc-49bd-8b1b-0e67b0ad4176",
+ "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": -1080.732035096065,
+ "y": 901.1443896873569
+ }
+ }
+ ],
+ "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-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-l2v",
+ "type": "default",
+ "source": "4d3b9faf-c05b-7e6f-bf4d-5c6a7f8e9dab",
+ "target": "6f5dcb1c-e27d-9fb0-d16f-7e8cbaa1fcbd",
+ "sourceHandle": "latents",
+ "targetHandle": "latents"
+ },
+ {
+ "id": "reactflow__edge-b5239ffa-041c-421c-b343-735c02709bb3value-7a6edc2d-f38e-a0c1-e27a-8f9dcbb20fcenum_frames",
+ "type": "default",
+ "source": "b5239ffa-041c-421c-b343-735c02709bb3",
+ "target": "7a6edc2d-f38e-a0c1-e27a-8f9dcbb20fce",
+ "sourceHandle": "value",
+ "targetHandle": "num_frames"
+ },
+ {
+ "id": "reactflow__edge-b5239ffa-041c-421c-b343-735c02709bb3value-4d3b9faf-c05b-7e6f-bf4d-5c6a7f8e9dabnum_frames",
+ "type": "default",
+ "source": "b5239ffa-041c-421c-b343-735c02709bb3",
+ "target": "4d3b9faf-c05b-7e6f-bf4d-5c6a7f8e9dab",
+ "sourceHandle": "value",
+ "targetHandle": "num_frames"
+ },
+ {
+ "id": "reactflow__edge-1a0e6c7b-9d2f-4b3c-8e1a-2f3d4c5b6a7etransformer-d781a3a6-eb2d-48dd-87a7-eb6898f1fca9transformer",
+ "type": "default",
+ "source": "1a0e6c7b-9d2f-4b3c-8e1a-2f3d4c5b6a7e",
+ "target": "d781a3a6-eb2d-48dd-87a7-eb6898f1fca9",
+ "sourceHandle": "transformer",
+ "targetHandle": "transformer"
+ },
+ {
+ "id": "reactflow__edge-d781a3a6-eb2d-48dd-87a7-eb6898f1fca9transformer-06bf5449-a0ca-44d0-a7cc-f37e03651761transformer",
+ "type": "default",
+ "source": "d781a3a6-eb2d-48dd-87a7-eb6898f1fca9",
+ "target": "06bf5449-a0ca-44d0-a7cc-f37e03651761",
+ "sourceHandle": "transformer",
+ "targetHandle": "transformer"
+ },
+ {
+ "id": "reactflow__edge-06bf5449-a0ca-44d0-a7cc-f37e03651761transformer-4d3b9faf-c05b-7e6f-bf4d-5c6a7f8e9dabtransformer",
+ "type": "default",
+ "source": "06bf5449-a0ca-44d0-a7cc-f37e03651761",
+ "target": "4d3b9faf-c05b-7e6f-bf4d-5c6a7f8e9dab",
+ "sourceHandle": "transformer",
+ "targetHandle": "transformer"
+ },
+ {
+ "id": "reactflow__edge-642e7cda-08c1-435c-8e6b-99700b61200bvideo-956718a3-d447-4e6e-ad38-671baeb2350evideo",
+ "type": "default",
+ "source": "642e7cda-08c1-435c-8e6b-99700b61200b",
+ "target": "956718a3-d447-4e6e-ad38-671baeb2350e",
+ "sourceHandle": "video",
+ "targetHandle": "video"
+ },
+ {
+ "id": "reactflow__edge-1167a8f8-87b9-4e32-bf85-cff170e926eecollection-8648065d-056f-47fc-ab84-b762d976c021videos",
+ "type": "default",
+ "source": "1167a8f8-87b9-4e32-bf85-cff170e926ee",
+ "target": "8648065d-056f-47fc-ab84-b762d976c021",
+ "sourceHandle": "collection",
+ "targetHandle": "videos"
+ },
+ {
+ "id": "reactflow__edge-e39ebfc3-c2cc-49bd-8b1b-0e67b0ad4176collection-1167a8f8-87b9-4e32-bf85-cff170e926eecollection",
+ "type": "default",
+ "source": "e39ebfc3-c2cc-49bd-8b1b-0e67b0ad4176",
+ "target": "1167a8f8-87b9-4e32-bf85-cff170e926ee",
+ "sourceHandle": "collection",
+ "targetHandle": "collection"
+ },
+ {
+ "id": "reactflow__edge-642e7cda-08c1-435c-8e6b-99700b61200bvideo-e39ebfc3-c2cc-49bd-8b1b-0e67b0ad4176item",
+ "type": "default",
+ "source": "642e7cda-08c1-435c-8e6b-99700b61200b",
+ "target": "e39ebfc3-c2cc-49bd-8b1b-0e67b0ad4176",
+ "sourceHandle": "video",
+ "targetHandle": "item"
+ },
+ {
+ "id": "reactflow__edge-6f5dcb1c-e27d-9fb0-d16f-7e8cbaa1fcbdvideo-1167a8f8-87b9-4e32-bf85-cff170e926eeitem",
+ "type": "default",
+ "source": "6f5dcb1c-e27d-9fb0-d16f-7e8cbaa1fcbd",
+ "target": "1167a8f8-87b9-4e32-bf85-cff170e926ee",
+ "sourceHandle": "video",
+ "targetHandle": "item"
+ },
+ {
+ "id": "reactflow__edge-642e7cda-08c1-435c-8e6b-99700b61200bwidth-7a6edc2d-f38e-a0c1-e27a-8f9dcbb20fcewidth",
+ "type": "default",
+ "source": "642e7cda-08c1-435c-8e6b-99700b61200b",
+ "target": "7a6edc2d-f38e-a0c1-e27a-8f9dcbb20fce",
+ "sourceHandle": "width",
+ "targetHandle": "width"
+ },
+ {
+ "id": "reactflow__edge-642e7cda-08c1-435c-8e6b-99700b61200bheight-7a6edc2d-f38e-a0c1-e27a-8f9dcbb20fceheight",
+ "type": "default",
+ "source": "642e7cda-08c1-435c-8e6b-99700b61200b",
+ "target": "7a6edc2d-f38e-a0c1-e27a-8f9dcbb20fce",
+ "sourceHandle": "height",
+ "targetHandle": "height"
+ },
+ {
+ "id": "reactflow__edge-642e7cda-08c1-435c-8e6b-99700b61200bwidth-4d3b9faf-c05b-7e6f-bf4d-5c6a7f8e9dabwidth",
+ "type": "default",
+ "source": "642e7cda-08c1-435c-8e6b-99700b61200b",
+ "target": "4d3b9faf-c05b-7e6f-bf4d-5c6a7f8e9dab",
+ "sourceHandle": "width",
+ "targetHandle": "width"
+ },
+ {
+ "id": "reactflow__edge-642e7cda-08c1-435c-8e6b-99700b61200bheight-4d3b9faf-c05b-7e6f-bf4d-5c6a7f8e9dabheight",
+ "type": "default",
+ "source": "642e7cda-08c1-435c-8e6b-99700b61200b",
+ "target": "4d3b9faf-c05b-7e6f-bf4d-5c6a7f8e9dab",
+ "sourceHandle": "height",
+ "targetHandle": "height"
+ },
+ {
+ "id": "reactflow__edge-956718a3-d447-4e6e-ad38-671baeb2350eimage-7a6edc2d-f38e-a0c1-e27a-8f9dcbb20fceimage",
+ "type": "default",
+ "source": "956718a3-d447-4e6e-ad38-671baeb2350e",
+ "target": "7a6edc2d-f38e-a0c1-e27a-8f9dcbb20fce",
+ "sourceHandle": "image",
+ "targetHandle": "image"
+ }
+ ],
+ "form": {
+ "elements": {
+ "root": {
+ "id": "root",
+ "data": {
+ "layout": "column",
+ "children": [
+ "node-field-tUu8xjrAMW",
+ "node-field-3H5g6vMeUr",
+ "container-WTyJo616Mt",
+ "container-NT48l2NzRe",
+ "node-field-WWwirfRWJM",
+ "node-field-IQzdqnlPjU",
+ "node-field-cvo27uAZJW",
+ "node-field-rjFxfyqXUE",
+ "node-field-dlpRVIIP44"
+ ]
+ },
+ "type": "container"
+ },
+ "node-field-IQzdqnlPjU": {
+ "id": "node-field-IQzdqnlPjU",
+ "parentId": "root",
+ "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-dlpRVIIP44": {
+ "id": "node-field-dlpRVIIP44",
+ "parentId": "root",
+ "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-rjFxfyqXUE": {
+ "id": "node-field-rjFxfyqXUE",
+ "parentId": "root",
+ "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-cvo27uAZJW": {
+ "id": "node-field-cvo27uAZJW",
+ "parentId": "root",
+ "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-tUu8xjrAMW": {
+ "id": "node-field-tUu8xjrAMW",
+ "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-WWwirfRWJM": {
+ "id": "node-field-WWwirfRWJM",
+ "parentId": "root",
+ "data": {
+ "fieldIdentifier": {
+ "nodeId": "7a6edc2d-f38e-a0c1-e27a-8f9dcbb20fce",
+ "fieldName": "num_frames"
+ },
+ "showDescription": false,
+ "settings": {
+ "type": "integer-field-config",
+ "showShuffle": false,
+ "component": "number-input"
+ }
+ },
+ "type": "node-field"
+ },
+ "node-field-deb9Zb2Pea": {
+ "id": "node-field-deb9Zb2Pea",
+ "parentId": "container-NT48l2NzRe",
+ "data": {
+ "fieldIdentifier": {
+ "nodeId": "1a0e6c7b-9d2f-4b3c-8e1a-2f3d4c5b6a7e",
+ "fieldName": "wan_t5_encoder_model"
+ },
+ "showDescription": false
+ },
+ "type": "node-field"
+ },
+ "node-field-gmXZokDmtz": {
+ "id": "node-field-gmXZokDmtz",
+ "parentId": "container-NT48l2NzRe",
+ "data": {
+ "fieldIdentifier": {
+ "nodeId": "1a0e6c7b-9d2f-4b3c-8e1a-2f3d4c5b6a7e",
+ "fieldName": "vae_model"
+ },
+ "showDescription": false
+ },
+ "type": "node-field"
+ },
+ "node-field-Qu2wxKebZ3": {
+ "id": "node-field-Qu2wxKebZ3",
+ "parentId": "container-WTyJo616Mt",
+ "data": {
+ "fieldIdentifier": {
+ "nodeId": "1a0e6c7b-9d2f-4b3c-8e1a-2f3d4c5b6a7e",
+ "fieldName": "component_source"
+ },
+ "showDescription": false
+ },
+ "type": "node-field"
+ },
+ "node-field-ryYpbo6Cvy": {
+ "id": "node-field-ryYpbo6Cvy",
+ "parentId": "container-QW4jkChXMH",
+ "data": {
+ "fieldIdentifier": {
+ "nodeId": "1a0e6c7b-9d2f-4b3c-8e1a-2f3d4c5b6a7e",
+ "fieldName": "transformer_low_noise_model"
+ },
+ "showDescription": false
+ },
+ "type": "node-field"
+ },
+ "node-field-TSLaEfQaZZ": {
+ "id": "node-field-TSLaEfQaZZ",
+ "parentId": "container-QW4jkChXMH",
+ "data": {
+ "fieldIdentifier": {
+ "nodeId": "1a0e6c7b-9d2f-4b3c-8e1a-2f3d4c5b6a7e",
+ "fieldName": "model"
+ },
+ "showDescription": false
+ },
+ "type": "node-field"
+ },
+ "container-WTyJo616Mt": {
+ "id": "container-WTyJo616Mt",
+ "parentId": "root",
+ "data": {
+ "layout": "column",
+ "children": [
+ "container-QW4jkChXMH",
+ "node-field-Qu2wxKebZ3"
+ ]
+ },
+ "type": "container"
+ },
+ "container-NT48l2NzRe": {
+ "id": "container-NT48l2NzRe",
+ "parentId": "root",
+ "data": {
+ "layout": "row",
+ "children": [
+ "node-field-deb9Zb2Pea",
+ "node-field-gmXZokDmtz"
+ ]
+ },
+ "type": "container"
+ },
+ "container-QW4jkChXMH": {
+ "id": "container-QW4jkChXMH",
+ "parentId": "container-WTyJo616Mt",
+ "data": {
+ "layout": "row",
+ "children": [
+ "node-field-TSLaEfQaZZ",
+ "node-field-ryYpbo6Cvy"
+ ]
+ },
+ "type": "container"
+ },
+ "node-field-3H5g6vMeUr": {
+ "id": "node-field-3H5g6vMeUr",
+ "parentId": "root",
+ "data": {
+ "fieldIdentifier": {
+ "nodeId": "642e7cda-08c1-435c-8e6b-99700b61200b",
+ "fieldName": "video"
+ },
+ "showDescription": true
+ },
+ "type": "node-field"
+ }
+ },
+ "rootElementId": "root"
+ },
+ "id": "default_wan22_i2v_add_frames_64fe9209-6871-49dd-a972-0a729b5332e7"
+}
\ No newline at end of file
diff --git a/invokeai/app/services/workflow_records/default_workflows/Wan 2.2 Image to Video Lightning.json b/invokeai/app/services/workflow_records/default_workflows/Wan 2.2 Image to Video Lightning.json
new file mode 100644
index 00000000000..949af92722d
--- /dev/null
+++ b/invokeai/app/services/workflow_records/default_workflows/Wan 2.2 Image to Video Lightning.json
@@ -0,0 +1,395 @@
+{
+ "id": "default_wan22_i2v_lightning_b6f9c5d7-7a8b-9c0d-ef1f-c03b4c5d6e7f",
+ "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. The reference image becomes frame 0 of the generated MP4 and the model animates outward from there; the Lightning distillation lets the denoise converge in 4 steps with CFG 1.0 (~20x faster than the standard I2V workflow). The 'Frames' on the Reference Image node must match the value on the Denoise Video node — both default to 81.",
+ "version": "1.0.0",
+ "contact": "",
+ "tags": "wan2.2, image to video, video, lightning, lora",
+ "notes": "Prerequisite model downloads: a Wan 2.2 I2V A14B main (Diffusers or GGUF expert pair), plus the matching I2V Lightning LoRA pair (distinct from the T2V Lightning LoRAs). For GGUF mains, also install the Component Source (Diffusers Wan I2V) OR the standalone Wan VAE + UMT5-XXL encoder. The reference image is VAE-encoded across the full latent temporal dim (frame 0 carries the image, remaining frames are zero pixels). If your Lightning LoRAs are untagged, set the 'Apply LoRA (High)' target to 'high' and 'Apply LoRA (Low)' target to 'low' manually. CFG=1.0 skips the negative-conditioning branch; the Negative Prompt content 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": "7a6edc2d-f38e-a0c1-e27a-8f9dcbb20fce",
+ "fieldName": "image"
+ },
+ {
+ "nodeId": "7a6edc2d-f38e-a0c1-e27a-8f9dcbb20fce",
+ "fieldName": "num_frames"
+ },
+ {
+ "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": "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.0",
+ "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": 0, "y": 0 }
+ },
+ {
+ "id": "3466ad11-a931-4adc-b394-9ec287f0c23b",
+ "type": "invocation",
+ "data": {
+ "id": "3466ad11-a931-4adc-b394-9ec287f0c23b",
+ "type": "wan_lora_loader",
+ "version": "1.0.0",
+ "label": "Apply LoRA (High)",
+ "notes": "",
+ "isOpen": true,
+ "isIntermediate": true,
+ "useCache": true,
+ "inputs": {
+ "lora": { "name": "lora", "label": "" },
+ "weight": { "name": "weight", "label": "", "value": 1 },
+ "target": { "name": "target", "label": "", "value": "auto" }
+ }
+ },
+ "position": { "x": 300, "y": 0 }
+ },
+ {
+ "id": "9b4cde65-699f-42c1-8889-76ac91fcb62f",
+ "type": "invocation",
+ "data": {
+ "id": "9b4cde65-699f-42c1-8889-76ac91fcb62f",
+ "type": "wan_lora_loader",
+ "version": "1.0.0",
+ "label": "Apply LoRA (Low)",
+ "notes": "",
+ "isOpen": true,
+ "isIntermediate": true,
+ "useCache": true,
+ "inputs": {
+ "lora": { "name": "lora", "label": "" },
+ "weight": { "name": "weight", "label": "", "value": 1 },
+ "target": { "name": "target", "label": "", "value": "auto" }
+ }
+ },
+ "position": { "x": 600, "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": -300 }
+ },
+ {
+ "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 (unused at CFG=1.0)",
+ "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.1.0",
+ "label": "",
+ "notes": "",
+ "isOpen": true,
+ "isIntermediate": true,
+ "useCache": true,
+ "inputs": {
+ "image": { "name": "image", "label": "Reference Image" },
+ "vae": { "name": "vae", "label": "" },
+ "width": { "name": "width", "label": "", "value": 832 },
+ "height": { "name": "height", "label": "", "value": 480 },
+ "num_frames": { "name": "num_frames", "label": "Frames", "value": 81 }
+ }
+ },
+ "position": { "x": 700, "y": 400 }
+ },
+ {
+ "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": 650 }
+ },
+ {
+ "id": "4d3b9faf-c05b-7e6f-bf4d-5c6a7f8e9dab",
+ "type": "invocation",
+ "data": {
+ "id": "4d3b9faf-c05b-7e6f-bf4d-5c6a7f8e9dab",
+ "type": "wan_video_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": "" },
+ "guidance_scale": { "name": "guidance_scale", "label": "CFG", "value": 1.0 },
+ "guidance_scale_low_noise": { "name": "guidance_scale_low_noise", "label": "CFG (Low)", "value": 1.0 },
+ "width": { "name": "width", "label": "", "value": 832 },
+ "height": { "name": "height", "label": "", "value": 480 },
+ "num_frames": { "name": "num_frames", "label": "Frames", "value": 81 },
+ "steps": { "name": "steps", "label": "", "value": 4 },
+ "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_l2v",
+ "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": "" },
+ "fps": { "name": "fps", "label": "FPS", "value": 16 }
+ }
+ },
+ "position": { "x": 1550, "y": -50 }
+ }
+ ],
+ "edges": [
+ {
+ "id": "edge-loader-transformer-lora1",
+ "type": "default",
+ "source": "1a0e6c7b-9d2f-4b3c-8e1a-2f3d4c5b6a7e",
+ "target": "3466ad11-a931-4adc-b394-9ec287f0c23b",
+ "sourceHandle": "transformer",
+ "targetHandle": "transformer"
+ },
+ {
+ "id": "edge-lora1-lora2",
+ "type": "default",
+ "source": "3466ad11-a931-4adc-b394-9ec287f0c23b",
+ "target": "9b4cde65-699f-42c1-8889-76ac91fcb62f",
+ "sourceHandle": "transformer",
+ "targetHandle": "transformer"
+ },
+ {
+ "id": "edge-lora2-denoise",
+ "type": "default",
+ "source": "9b4cde65-699f-42c1-8889-76ac91fcb62f",
+ "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-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-l2v",
+ "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 Image to Video.json b/invokeai/app/services/workflow_records/default_workflows/Wan 2.2 Image to Video.json
new file mode 100644
index 00000000000..e79c2ad1f8e
--- /dev/null
+++ b/invokeai/app/services/workflow_records/default_workflows/Wan 2.2 Image to Video.json
@@ -0,0 +1,315 @@
+{
+ "id": "default_wan22_i2v_video_e4d7a3b5-5f6a-7b8c-cd9d-ae1f2a3b4c5d",
+ "name": "Image to Video - Wan 2.2",
+ "author": "InvokeAI",
+ "description": "Image-to-video generation with Wan 2.2 I2V A14B. The reference image becomes frame 0 of the generated MP4 and the model animates outward from there. Drop a reference image into the 'Reference Image' input, then invoke. The 'Frames' value on the Reference Image node must match the value on the Denoise Video node — both default to 81 (~5 s @ 16 FPS).",
+ "version": "1.0.0",
+ "contact": "",
+ "tags": "wan2.2, image to video, video",
+ "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. The reference image is VAE-encoded across the full latent temporal dim (frame 0 carries the image, remaining frames are zero pixels — the model fills them in). Performance note: 81 frames at 832x480 / 40 steps on a 24 GB GPU takes several minutes. Lower `num_frames` (5, 9, 13, ...) and `steps` for quicker iteration. IMPORTANT: keep the `num_frames` on the Reference Image node equal to the value on the Denoise Video node — the denoise loop validates this and will raise if they differ.",
+ "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": "7a6edc2d-f38e-a0c1-e27a-8f9dcbb20fce",
+ "fieldName": "num_frames"
+ },
+ {
+ "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": "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.0",
+ "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.1.0",
+ "label": "",
+ "notes": "",
+ "isOpen": true,
+ "isIntermediate": true,
+ "useCache": true,
+ "inputs": {
+ "image": { "name": "image", "label": "Reference Image" },
+ "vae": { "name": "vae", "label": "" },
+ "width": { "name": "width", "label": "", "value": 832 },
+ "height": { "name": "height", "label": "", "value": 480 },
+ "num_frames": { "name": "num_frames", "label": "Frames", "value": 81 }
+ }
+ },
+ "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_video_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": "" },
+ "guidance_scale": { "name": "guidance_scale", "label": "CFG", "value": 5.0 },
+ "guidance_scale_low_noise": { "name": "guidance_scale_low_noise", "label": "CFG (Low)", "value": 4.0 },
+ "width": { "name": "width", "label": "", "value": 832 },
+ "height": { "name": "height", "label": "", "value": 480 },
+ "num_frames": { "name": "num_frames", "label": "Frames", "value": 81 },
+ "steps": { "name": "steps", "label": "", "value": 40 },
+ "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_l2v",
+ "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": "" },
+ "fps": { "name": "fps", "label": "FPS", "value": 16 }
+ }
+ },
+ "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-l2v",
+ "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-l2v",
+ "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..3fc9395c232
--- /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.0",
+ "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/services/workflow_records/default_workflows/Wan 2.2 Text to Video (Lightning).json b/invokeai/app/services/workflow_records/default_workflows/Wan 2.2 Text to Video (Lightning).json
new file mode 100644
index 00000000000..43df9283ecb
--- /dev/null
+++ b/invokeai/app/services/workflow_records/default_workflows/Wan 2.2 Text to Video (Lightning).json
@@ -0,0 +1,331 @@
+{
+ "id": "default_wan22_t2v_video_lightning_f5e8b4c6-6a7b-8c9d-de1e-bf2a3b4c5d6e",
+ "name": "Text to Video - Wan 2.2 Lightning",
+ "author": "InvokeAI",
+ "description": "Faster text-to-video with the Wan 2.2 Lightning LoRA pair. Runs at 4 denoising steps with CFG disabled (1.0), which skips the negative-conditioning branch — roughly 20× faster than the default 'Text to Video - Wan 2.2' workflow at similar quality. Requires the Wan 2.2 T2V A14B Lightning LoRAs (one for each MoE expert).",
+ "version": "1.0.0",
+ "contact": "",
+ "tags": "wan2.2, text to video, video, lightning, lora",
+ "notes": "Prerequisite models: a Wan 2.2 T2V A14B main (Diffusers or GGUF expert pair) PLUS the matching Wan 2.2 T2V Lightning LoRA pair (high-noise + low-noise). Wire the high-noise Lightning LoRA into the first 'Apply LoRA - Wan 2.2' node and the low-noise one into the second; if the LoRAs are properly tagged with their expert ('high'/'low'), leave the 'target' dropdown on 'auto'. Each LoRA's weight defaults to 1.0; lower it if the output looks over-baked. Don't change steps below 4 or raise guidance above 1.0 — that's where the Lightning distillation lives.",
+ "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": "9b4cde65-699f-42c1-8889-76ac91fcb62f",
+ "fieldName": "lora"
+ },
+ {
+ "nodeId": "9b4cde65-699f-42c1-8889-76ac91fcb62f",
+ "fieldName": "weight"
+ },
+ {
+ "nodeId": "2b1f7d8c-ae3f-5c4d-9f2b-3a4e5d6c7b8f",
+ "fieldName": "prompt"
+ },
+ {
+ "nodeId": "3c2a8e9d-bf4a-6d5e-af3c-4b5f6e7d8c9a",
+ "fieldName": "prompt"
+ },
+ {
+ "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": "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.0",
+ "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": 0, "y": 0 }
+ },
+ {
+ "id": "3466ad11-a931-4adc-b394-9ec287f0c23b",
+ "type": "invocation",
+ "data": {
+ "id": "3466ad11-a931-4adc-b394-9ec287f0c23b",
+ "type": "wan_lora_loader",
+ "version": "1.0.0",
+ "label": "Lightning LoRA (High)",
+ "notes": "",
+ "isOpen": true,
+ "isIntermediate": true,
+ "useCache": true,
+ "inputs": {
+ "lora": { "name": "lora", "label": "" },
+ "weight": { "name": "weight", "label": "", "value": 1.0 },
+ "target": { "name": "target", "label": "", "value": "auto" },
+ "transformer": { "name": "transformer", "label": "" }
+ }
+ },
+ "position": { "x": 230, "y": 0 }
+ },
+ {
+ "id": "9b4cde65-699f-42c1-8889-76ac91fcb62f",
+ "type": "invocation",
+ "data": {
+ "id": "9b4cde65-699f-42c1-8889-76ac91fcb62f",
+ "type": "wan_lora_loader",
+ "version": "1.0.0",
+ "label": "Lightning LoRA (Low)",
+ "notes": "",
+ "isOpen": true,
+ "isIntermediate": true,
+ "useCache": true,
+ "inputs": {
+ "lora": { "name": "lora", "label": "" },
+ "weight": { "name": "weight", "label": "", "value": 1.0 },
+ "target": { "name": "target", "label": "", "value": "auto" },
+ "transformer": { "name": "transformer", "label": "" }
+ }
+ },
+ "position": { "x": 460, "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 walking through a field of tall grass, cinematic" },
+ "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 (unused at CFG=1)",
+ "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_video_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": "" },
+ "guidance_scale": { "name": "guidance_scale", "label": "CFG", "value": 1.0 },
+ "guidance_scale_low_noise": { "name": "guidance_scale_low_noise", "label": "CFG (Low)", "value": 1.0 },
+ "width": { "name": "width", "label": "", "value": 832 },
+ "height": { "name": "height", "label": "", "value": 480 },
+ "num_frames": { "name": "num_frames", "label": "Frames", "value": 81 },
+ "steps": { "name": "steps", "label": "", "value": 4 },
+ "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_l2v",
+ "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": "" },
+ "fps": { "name": "fps", "label": "FPS", "value": 16 }
+ }
+ },
+ "position": { "x": 1550, "y": -50 }
+ }
+ ],
+ "edges": [
+ {
+ "id": "edge-loader-transformer-lora-high",
+ "type": "default",
+ "source": "1a0e6c7b-9d2f-4b3c-8e1a-2f3d4c5b6a7e",
+ "target": "3466ad11-a931-4adc-b394-9ec287f0c23b",
+ "sourceHandle": "transformer",
+ "targetHandle": "transformer"
+ },
+ {
+ "id": "edge-lora-high-lora-low",
+ "type": "default",
+ "source": "3466ad11-a931-4adc-b394-9ec287f0c23b",
+ "target": "9b4cde65-699f-42c1-8889-76ac91fcb62f",
+ "sourceHandle": "transformer",
+ "targetHandle": "transformer"
+ },
+ {
+ "id": "edge-lora-low-denoise",
+ "type": "default",
+ "source": "9b4cde65-699f-42c1-8889-76ac91fcb62f",
+ "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"
+ }
+ ]
+}
diff --git a/invokeai/app/services/workflow_records/default_workflows/Wan 2.2 Text to Video Lightning.json b/invokeai/app/services/workflow_records/default_workflows/Wan 2.2 Text to Video Lightning.json
new file mode 100644
index 00000000000..af4605ba83d
--- /dev/null
+++ b/invokeai/app/services/workflow_records/default_workflows/Wan 2.2 Text to Video Lightning.json
@@ -0,0 +1,349 @@
+{
+ "id": "default_wan22_t2v_lightning_a5e8b4c6-6f7a-8b9c-de0e-bf2a3b4c5d6e",
+ "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": "Prerequisite model downloads: a Wan 2.2 T2V A14B main model (Diffusers or GGUF expert pair), plus the matching Lightning LoRA pair (e.g. LightX2V's 4-step distillation). The Lightning LoRAs are A14B-specific — distinct LoRAs exist for T2V and I2V variants. If your Lightning LoRAs are untagged (no 'expert' field), use the LoRA loader's 'target' dropdown to manually route the first to 'high' and the second to 'low'. CFG=1.0 skips the negative-conditioning branch, so the Negative Prompt content 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": "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.0",
+ "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": 0, "y": 0 }
+ },
+ {
+ "id": "3466ad11-a931-4adc-b394-9ec287f0c23b",
+ "type": "invocation",
+ "data": {
+ "id": "3466ad11-a931-4adc-b394-9ec287f0c23b",
+ "type": "wan_lora_loader",
+ "version": "1.0.0",
+ "label": "Apply LoRA (High)",
+ "notes": "",
+ "isOpen": true,
+ "isIntermediate": true,
+ "useCache": true,
+ "inputs": {
+ "lora": { "name": "lora", "label": "" },
+ "weight": { "name": "weight", "label": "", "value": 1 },
+ "target": { "name": "target", "label": "", "value": "auto" }
+ }
+ },
+ "position": { "x": 300, "y": 0 }
+ },
+ {
+ "id": "9b4cde65-699f-42c1-8889-76ac91fcb62f",
+ "type": "invocation",
+ "data": {
+ "id": "9b4cde65-699f-42c1-8889-76ac91fcb62f",
+ "type": "wan_lora_loader",
+ "version": "1.0.0",
+ "label": "Apply LoRA (Low)",
+ "notes": "",
+ "isOpen": true,
+ "isIntermediate": true,
+ "useCache": true,
+ "inputs": {
+ "lora": { "name": "lora", "label": "" },
+ "weight": { "name": "weight", "label": "", "value": 1 },
+ "target": { "name": "target", "label": "", "value": "auto" }
+ }
+ },
+ "position": { "x": 600, "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 walking through a field of tall grass, cinematic" },
+ "wan_t5_encoder": { "name": "wan_t5_encoder", "label": "" }
+ }
+ },
+ "position": { "x": 700, "y": -300 }
+ },
+ {
+ "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 (unused at CFG=1.0)",
+ "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": "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": 500 }
+ },
+ {
+ "id": "4d3b9faf-c05b-7e6f-bf4d-5c6a7f8e9dab",
+ "type": "invocation",
+ "data": {
+ "id": "4d3b9faf-c05b-7e6f-bf4d-5c6a7f8e9dab",
+ "type": "wan_video_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": "" },
+ "guidance_scale": { "name": "guidance_scale", "label": "CFG", "value": 1.0 },
+ "guidance_scale_low_noise": { "name": "guidance_scale_low_noise", "label": "CFG (Low)", "value": 1.0 },
+ "width": { "name": "width", "label": "", "value": 832 },
+ "height": { "name": "height", "label": "", "value": 480 },
+ "num_frames": { "name": "num_frames", "label": "Frames", "value": 81 },
+ "steps": { "name": "steps", "label": "", "value": 4 },
+ "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_l2v",
+ "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": "" },
+ "fps": { "name": "fps", "label": "FPS", "value": 16 }
+ }
+ },
+ "position": { "x": 1550, "y": -50 }
+ }
+ ],
+ "edges": [
+ {
+ "id": "edge-loader-transformer-lora1",
+ "type": "default",
+ "source": "1a0e6c7b-9d2f-4b3c-8e1a-2f3d4c5b6a7e",
+ "target": "3466ad11-a931-4adc-b394-9ec287f0c23b",
+ "sourceHandle": "transformer",
+ "targetHandle": "transformer"
+ },
+ {
+ "id": "edge-lora1-lora2",
+ "type": "default",
+ "source": "3466ad11-a931-4adc-b394-9ec287f0c23b",
+ "target": "9b4cde65-699f-42c1-8889-76ac91fcb62f",
+ "sourceHandle": "transformer",
+ "targetHandle": "transformer"
+ },
+ {
+ "id": "edge-lora2-denoise",
+ "type": "default",
+ "source": "9b4cde65-699f-42c1-8889-76ac91fcb62f",
+ "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"
+ }
+ ]
+}
diff --git a/invokeai/app/services/workflow_records/default_workflows/Wan 2.2 Text to Video.json b/invokeai/app/services/workflow_records/default_workflows/Wan 2.2 Text to Video.json
new file mode 100644
index 00000000000..4a0b26e7134
--- /dev/null
+++ b/invokeai/app/services/workflow_records/default_workflows/Wan 2.2 Text to Video.json
@@ -0,0 +1,269 @@
+{
+ "id": "default_wan22_t2v_video_d3c6f2a4-4e5f-6a7b-bf8c-9d0e1f2a3b4c",
+ "name": "Text to Video - Wan 2.2",
+ "author": "InvokeAI",
+ "description": "Text-to-video generation with Wan 2.2 T2V A14B. Produces an MP4 of `num_frames` frames at `fps` frames-per-second (defaults: 81 frames @ 16 FPS = ~5 s, 832x480). The output lands in the gallery alongside images and plays inline in the viewer. Only the T2V-A14B variant is supported by this workflow; for image generation use the 'Text to Image - Wan 2.2' workflow.",
+ "version": "1.0.0",
+ "contact": "",
+ "tags": "wan2.2, text to video, video",
+ "notes": "Prerequisite model downloads: a Wan 2.2 T2V A14B main model (Diffusers or GGUF expert pair). 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. Performance note: video generation is GPU- and time-intensive — 81 frames at 832x480 / 40 steps on a 24 GB GPU takes several minutes. Lower `num_frames` (5, 9, 13, ...) and `steps` for quicker iteration. `num_frames` must satisfy (num_frames - 1) %% 4 == 0 due to the Wan VAE's 4x temporal compression.",
+ "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": "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.0",
+ "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 walking through a field of tall grass, cinematic" },
+ "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_video_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": "" },
+ "guidance_scale": { "name": "guidance_scale", "label": "CFG", "value": 5.0 },
+ "guidance_scale_low_noise": { "name": "guidance_scale_low_noise", "label": "CFG (Low)", "value": 4.0 },
+ "width": { "name": "width", "label": "", "value": 832 },
+ "height": { "name": "height", "label": "", "value": 480 },
+ "num_frames": { "name": "num_frames", "label": "Frames", "value": 81 },
+ "steps": { "name": "steps", "label": "", "value": 40 },
+ "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_l2v",
+ "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": "" },
+ "fps": { "name": "fps", "label": "FPS", "value": 16 }
+ }
+ },
+ "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-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"
+ }
+ ]
+}
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_thumbnails.py b/invokeai/app/util/video_thumbnails.py
new file mode 100644
index 00000000000..db3783039c4
--- /dev/null
+++ b/invokeai/app/util/video_thumbnails.py
@@ -0,0 +1,100 @@
+"""Video frame/probe helpers used by the video file store.
+
+The primary backend is imageio's FFMPEG plugin (the same one ``wan_l2v`` uses
+to *encode* output MP4s — so reading our own output is guaranteed to work).
+We fall back to ``cv2.VideoCapture`` only if imageio fails; cv2 wheels have
+historically hung on certain codec/container combinations, so we never rely
+on it as the primary path.
+"""
+
+import os
+from pathlib import Path
+from typing import Optional
+
+import imageio.v3 as iio
+from PIL import Image
+
+
+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 extract_video_frame(video_path: Path, frame_index: int = 0) -> 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 (with a controlled context that can't hang
+ silently — at worst it raises and we return None).
+ """
+ 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)
+ return Image.fromarray(frame)
+ except Exception:
+ pass
+
+ # Fallback: cv2.VideoCapture. Only used if imageio couldn't decode the file
+ # — uploaded videos with unusual codecs may need this path.
+ 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)
+ return Image.fromarray(frame_rgb)
+ finally:
+ capture.release()
+ except Exception:
+ return None
+
+
+def probe_video(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
+ FileNotFoundError 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
+
+ try:
+ 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
+ except FileNotFoundError:
+ raise
+ except Exception as e:
+ raise FileNotFoundError(f"Unable to open video at {video_path}") from e
diff --git a/invokeai/backend/model_manager/configs/factory.py b/invokeai/backend/model_manager/configs/factory.py
index 985cb982d30..feedafe6c38 100644
--- a/invokeai/backend/model_manager/configs/factory.py
+++ b/invokeai/backend/model_manager/configs/factory.py
@@ -54,6 +54,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,
@@ -78,10 +79,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,
)
@@ -119,10 +122,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,
@@ -173,6 +179,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
@@ -192,6 +199,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()],
@@ -199,12 +207,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()],
@@ -226,6 +240,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()],
@@ -251,6 +272,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 46606a3c0d5..d0372ba3f14 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,
+)
class LoraModelDefaultSettings(BaseModel):
@@ -885,16 +894,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(
@@ -917,19 +930,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 e1e408a3483..e2178dd5267 100644
--- a/invokeai/backend/model_manager/configs/main.py
+++ b/invokeai/backend/model_manager/configs/main.py
@@ -31,6 +31,7 @@
QwenImageVariantType,
SchedulerPredictionType,
SubModelType,
+ WanVariantType,
ZImageVariantType,
)
from invokeai.backend.quantization.gguf.ggml_tensor import GGMLTensor
@@ -63,7 +64,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:
@@ -93,6 +99,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
@@ -1383,6 +1395,269 @@ 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")
+
+ 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")
+
+ 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 5a88cf12781..00b96c3c1ac 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).
@@ -113,9 +146,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:
@@ -215,9 +249,96 @@ 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)
+ 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)
+ 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..efda6a551a2
--- /dev/null
+++ b/invokeai/backend/model_manager/configs/wan_t5_encoder.py
@@ -0,0 +1,84 @@
+"""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)
+
+ @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 6cf06d48074..a38ad2acd71 100644
--- a/invokeai/backend/model_manager/load/model_loaders/lora.py
+++ b/invokeai/backend/model_manager/load/model_loaders/lora.py
@@ -62,6 +62,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
@@ -170,6 +171,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..b3d2eae38ee 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)
@@ -39,6 +168,10 @@ def _load_model(
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 +182,67 @@ 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).
+ """
+ import accelerate
+ from diffusers.models.autoencoders.autoencoder_kl_wan import AutoencoderKLWan
+ from safetensors.torch import load_file
+
+ sd = load_file(config.path)
+
+ if self._torch_dtype is not None:
+ for k in list(sd.keys()):
+ if sd[k].is_floating_point():
+ sd[k] = sd[k].to(self._torch_dtype)
+
+ 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
+
+ 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..f3bb7de7b61
--- /dev/null
+++ b/invokeai/backend/model_manager/load/model_loaders/wan.py
@@ -0,0 +1,354 @@
+"""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.")
+
+ 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
+
+ # bfloat16 across the board: matches Diffusers WanPipeline reference and
+ # avoids the fp16 instability seen in the Wan VAE.
+ dtype_kwarg = {"dtype": torch.bfloat16}
+ try:
+ result: AnyModel = load_class.from_pretrained(
+ model_path,
+ **dtype_kwarg,
+ variant=variant,
+ local_files_only=True,
+ )
+ except TypeError:
+ # Older diffusers releases use torch_dtype instead of dtype.
+ dtype_kwarg = {"torch_dtype": torch.bfloat16}
+ result = load_class.from_pretrained(
+ model_path,
+ **dtype_kwarg,
+ variant=variant,
+ local_files_only=True,
+ )
+ except OSError as e:
+ # 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.
+ if variant and "no file named" in str(e):
+ result = load_class.from_pretrained(model_path, **dtype_kwarg, local_files_only=True)
+ else:
+ raise
+
+ 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/starter_models.py b/invokeai/backend/model_manager/starter_models.py
index 2ab3b2767ee..760af358650 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,
@@ -1690,6 +1914,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,
@@ -1800,6 +2044,31 @@ def _gemini_3_resolution_presets(
t5_base_encoder,
]
+# 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),
@@ -1808,6 +2077,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/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..92bfabcc265
--- /dev/null
+++ b/invokeai/backend/wan/extensions/wan_ref_image_extension.py
@@ -0,0 +1,196 @@
+"""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,
+) -> 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.
+
+ Mirrors :class:`diffusers.WanImageToVideoPipeline.prepare_latents` with
+ ``last_image=None`` and ``expand_timesteps=False``:
+
+ 1. The reference image is concatenated with zero pixel-frames to form a
+ ``[1, 3, num_frames, H, W]`` pseudo-video — only frame 0 carries the
+ image content, all other frames 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 frame 0 and 0 elsewhere. The first frame is repeated 4× then
+ the whole thing is reshaped/transposed into ``[1, 4, T_lat, ...]`` —
+ so the first latent frame's 4 mask channels are all 1 and the rest
+ are all 0.
+
+ 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.
+ """
+ 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.
+ if num_frames > 1:
+ 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:
+ 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/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 4a36cf31e2b..83b5c7c1140 100644
--- a/invokeai/frontend/web/openapi.json
+++ b/invokeai/frontend/web/openapi.json
@@ -786,6 +786,9 @@
{
"$ref": "#/components/schemas/Main_Diffusers_QwenImage_Config"
},
+ {
+ "$ref": "#/components/schemas/Main_Diffusers_Wan_Config"
+ },
{
"$ref": "#/components/schemas/Main_Diffusers_ZImage_Config"
},
@@ -825,6 +828,9 @@
{
"$ref": "#/components/schemas/Main_GGUF_QwenImage_Config"
},
+ {
+ "$ref": "#/components/schemas/Main_GGUF_Wan_Config"
+ },
{
"$ref": "#/components/schemas/Main_GGUF_ZImage_Config"
},
@@ -843,6 +849,9 @@
{
"$ref": "#/components/schemas/VAE_Checkpoint_Flux2_Config"
},
+ {
+ "$ref": "#/components/schemas/VAE_Checkpoint_Wan_Config"
+ },
{
"$ref": "#/components/schemas/VAE_Checkpoint_QwenImage_Config"
},
@@ -858,6 +867,9 @@
{
"$ref": "#/components/schemas/VAE_Diffusers_Flux2_Config"
},
+ {
+ "$ref": "#/components/schemas/VAE_Diffusers_Wan_Config"
+ },
{
"$ref": "#/components/schemas/ControlNet_Checkpoint_SD1_Config"
},
@@ -906,6 +918,9 @@
{
"$ref": "#/components/schemas/LoRA_LyCORIS_QwenImage_Config"
},
+ {
+ "$ref": "#/components/schemas/LoRA_LyCORIS_Wan_Config"
+ },
{
"$ref": "#/components/schemas/LoRA_LyCORIS_Anima_Config"
},
@@ -957,6 +972,9 @@
{
"$ref": "#/components/schemas/QwenVLEncoder_Checkpoint_Config"
},
+ {
+ "$ref": "#/components/schemas/WanT5Encoder_WanT5Encoder_Config"
+ },
{
"$ref": "#/components/schemas/TI_File_SD1_Config"
},
@@ -1104,6 +1122,9 @@
{
"$ref": "#/components/schemas/Main_Diffusers_QwenImage_Config"
},
+ {
+ "$ref": "#/components/schemas/Main_Diffusers_Wan_Config"
+ },
{
"$ref": "#/components/schemas/Main_Diffusers_ZImage_Config"
},
@@ -1143,6 +1164,9 @@
{
"$ref": "#/components/schemas/Main_GGUF_QwenImage_Config"
},
+ {
+ "$ref": "#/components/schemas/Main_GGUF_Wan_Config"
+ },
{
"$ref": "#/components/schemas/Main_GGUF_ZImage_Config"
},
@@ -1161,6 +1185,9 @@
{
"$ref": "#/components/schemas/VAE_Checkpoint_Flux2_Config"
},
+ {
+ "$ref": "#/components/schemas/VAE_Checkpoint_Wan_Config"
+ },
{
"$ref": "#/components/schemas/VAE_Checkpoint_QwenImage_Config"
},
@@ -1176,6 +1203,9 @@
{
"$ref": "#/components/schemas/VAE_Diffusers_Flux2_Config"
},
+ {
+ "$ref": "#/components/schemas/VAE_Diffusers_Wan_Config"
+ },
{
"$ref": "#/components/schemas/ControlNet_Checkpoint_SD1_Config"
},
@@ -1224,6 +1254,9 @@
{
"$ref": "#/components/schemas/LoRA_LyCORIS_QwenImage_Config"
},
+ {
+ "$ref": "#/components/schemas/LoRA_LyCORIS_Wan_Config"
+ },
{
"$ref": "#/components/schemas/LoRA_LyCORIS_Anima_Config"
},
@@ -1275,6 +1308,9 @@
{
"$ref": "#/components/schemas/QwenVLEncoder_Checkpoint_Config"
},
+ {
+ "$ref": "#/components/schemas/WanT5Encoder_WanT5Encoder_Config"
+ },
{
"$ref": "#/components/schemas/TI_File_SD1_Config"
},
@@ -1422,6 +1458,9 @@
{
"$ref": "#/components/schemas/Main_Diffusers_QwenImage_Config"
},
+ {
+ "$ref": "#/components/schemas/Main_Diffusers_Wan_Config"
+ },
{
"$ref": "#/components/schemas/Main_Diffusers_ZImage_Config"
},
@@ -1461,6 +1500,9 @@
{
"$ref": "#/components/schemas/Main_GGUF_QwenImage_Config"
},
+ {
+ "$ref": "#/components/schemas/Main_GGUF_Wan_Config"
+ },
{
"$ref": "#/components/schemas/Main_GGUF_ZImage_Config"
},
@@ -1479,6 +1521,9 @@
{
"$ref": "#/components/schemas/VAE_Checkpoint_Flux2_Config"
},
+ {
+ "$ref": "#/components/schemas/VAE_Checkpoint_Wan_Config"
+ },
{
"$ref": "#/components/schemas/VAE_Checkpoint_QwenImage_Config"
},
@@ -1494,6 +1539,9 @@
{
"$ref": "#/components/schemas/VAE_Diffusers_Flux2_Config"
},
+ {
+ "$ref": "#/components/schemas/VAE_Diffusers_Wan_Config"
+ },
{
"$ref": "#/components/schemas/ControlNet_Checkpoint_SD1_Config"
},
@@ -1542,6 +1590,9 @@
{
"$ref": "#/components/schemas/LoRA_LyCORIS_QwenImage_Config"
},
+ {
+ "$ref": "#/components/schemas/LoRA_LyCORIS_Wan_Config"
+ },
{
"$ref": "#/components/schemas/LoRA_LyCORIS_Anima_Config"
},
@@ -1593,6 +1644,9 @@
{
"$ref": "#/components/schemas/QwenVLEncoder_Checkpoint_Config"
},
+ {
+ "$ref": "#/components/schemas/WanT5Encoder_WanT5Encoder_Config"
+ },
{
"$ref": "#/components/schemas/TI_File_SD1_Config"
},
@@ -1790,6 +1844,9 @@
{
"$ref": "#/components/schemas/Main_Diffusers_QwenImage_Config"
},
+ {
+ "$ref": "#/components/schemas/Main_Diffusers_Wan_Config"
+ },
{
"$ref": "#/components/schemas/Main_Diffusers_ZImage_Config"
},
@@ -1829,6 +1886,9 @@
{
"$ref": "#/components/schemas/Main_GGUF_QwenImage_Config"
},
+ {
+ "$ref": "#/components/schemas/Main_GGUF_Wan_Config"
+ },
{
"$ref": "#/components/schemas/Main_GGUF_ZImage_Config"
},
@@ -1847,6 +1907,9 @@
{
"$ref": "#/components/schemas/VAE_Checkpoint_Flux2_Config"
},
+ {
+ "$ref": "#/components/schemas/VAE_Checkpoint_Wan_Config"
+ },
{
"$ref": "#/components/schemas/VAE_Checkpoint_QwenImage_Config"
},
@@ -1862,6 +1925,9 @@
{
"$ref": "#/components/schemas/VAE_Diffusers_Flux2_Config"
},
+ {
+ "$ref": "#/components/schemas/VAE_Diffusers_Wan_Config"
+ },
{
"$ref": "#/components/schemas/ControlNet_Checkpoint_SD1_Config"
},
@@ -1910,6 +1976,9 @@
{
"$ref": "#/components/schemas/LoRA_LyCORIS_QwenImage_Config"
},
+ {
+ "$ref": "#/components/schemas/LoRA_LyCORIS_Wan_Config"
+ },
{
"$ref": "#/components/schemas/LoRA_LyCORIS_Anima_Config"
},
@@ -1961,6 +2030,9 @@
{
"$ref": "#/components/schemas/QwenVLEncoder_Checkpoint_Config"
},
+ {
+ "$ref": "#/components/schemas/WanT5Encoder_WanT5Encoder_Config"
+ },
{
"$ref": "#/components/schemas/TI_File_SD1_Config"
},
@@ -2182,6 +2254,9 @@
{
"$ref": "#/components/schemas/Main_Diffusers_QwenImage_Config"
},
+ {
+ "$ref": "#/components/schemas/Main_Diffusers_Wan_Config"
+ },
{
"$ref": "#/components/schemas/Main_Diffusers_ZImage_Config"
},
@@ -2221,6 +2296,9 @@
{
"$ref": "#/components/schemas/Main_GGUF_QwenImage_Config"
},
+ {
+ "$ref": "#/components/schemas/Main_GGUF_Wan_Config"
+ },
{
"$ref": "#/components/schemas/Main_GGUF_ZImage_Config"
},
@@ -2239,6 +2317,9 @@
{
"$ref": "#/components/schemas/VAE_Checkpoint_Flux2_Config"
},
+ {
+ "$ref": "#/components/schemas/VAE_Checkpoint_Wan_Config"
+ },
{
"$ref": "#/components/schemas/VAE_Checkpoint_QwenImage_Config"
},
@@ -2254,6 +2335,9 @@
{
"$ref": "#/components/schemas/VAE_Diffusers_Flux2_Config"
},
+ {
+ "$ref": "#/components/schemas/VAE_Diffusers_Wan_Config"
+ },
{
"$ref": "#/components/schemas/ControlNet_Checkpoint_SD1_Config"
},
@@ -2302,6 +2386,9 @@
{
"$ref": "#/components/schemas/LoRA_LyCORIS_QwenImage_Config"
},
+ {
+ "$ref": "#/components/schemas/LoRA_LyCORIS_Wan_Config"
+ },
{
"$ref": "#/components/schemas/LoRA_LyCORIS_Anima_Config"
},
@@ -2353,6 +2440,9 @@
{
"$ref": "#/components/schemas/QwenVLEncoder_Checkpoint_Config"
},
+ {
+ "$ref": "#/components/schemas/WanT5Encoder_WanT5Encoder_Config"
+ },
{
"$ref": "#/components/schemas/TI_File_SD1_Config"
},
@@ -3394,6 +3484,9 @@
{
"$ref": "#/components/schemas/Main_Diffusers_QwenImage_Config"
},
+ {
+ "$ref": "#/components/schemas/Main_Diffusers_Wan_Config"
+ },
{
"$ref": "#/components/schemas/Main_Diffusers_ZImage_Config"
},
@@ -3433,6 +3526,9 @@
{
"$ref": "#/components/schemas/Main_GGUF_QwenImage_Config"
},
+ {
+ "$ref": "#/components/schemas/Main_GGUF_Wan_Config"
+ },
{
"$ref": "#/components/schemas/Main_GGUF_ZImage_Config"
},
@@ -3451,6 +3547,9 @@
{
"$ref": "#/components/schemas/VAE_Checkpoint_Flux2_Config"
},
+ {
+ "$ref": "#/components/schemas/VAE_Checkpoint_Wan_Config"
+ },
{
"$ref": "#/components/schemas/VAE_Checkpoint_QwenImage_Config"
},
@@ -3466,6 +3565,9 @@
{
"$ref": "#/components/schemas/VAE_Diffusers_Flux2_Config"
},
+ {
+ "$ref": "#/components/schemas/VAE_Diffusers_Wan_Config"
+ },
{
"$ref": "#/components/schemas/ControlNet_Checkpoint_SD1_Config"
},
@@ -3514,6 +3616,9 @@
{
"$ref": "#/components/schemas/LoRA_LyCORIS_QwenImage_Config"
},
+ {
+ "$ref": "#/components/schemas/LoRA_LyCORIS_Wan_Config"
+ },
{
"$ref": "#/components/schemas/LoRA_LyCORIS_Anima_Config"
},
@@ -3565,6 +3670,9 @@
{
"$ref": "#/components/schemas/QwenVLEncoder_Checkpoint_Config"
},
+ {
+ "$ref": "#/components/schemas/WanT5Encoder_WanT5Encoder_Config"
+ },
{
"$ref": "#/components/schemas/TI_File_SD1_Config"
},
@@ -5375,12 +5483,310 @@
]
}
},
- "/api/v1/boards/": {
- "post": {
- "tags": ["boards"],
- "summary": "Create Board",
- "description": "Creates a board for the current user",
- "operationId": "create_board",
+ "/api/v1/videos/upload": {
+ "post": {
+ "tags": ["videos"],
+ "summary": "Upload Video",
+ "description": "Uploads a video for the current user.",
+ "operationId": "upload_video",
+ "security": [
+ {
+ "HTTPBearer": []
+ }
+ ],
+ "parameters": [
+ {
+ "name": "video_category",
+ "in": "query",
+ "required": true,
+ "schema": {
+ "$ref": "#/components/schemas/ImageCategory",
+ "description": "The category of the video"
+ },
+ "description": "The category of the video"
+ },
+ {
+ "name": "is_intermediate",
+ "in": "query",
+ "required": true,
+ "schema": {
+ "type": "boolean",
+ "description": "Whether this is an intermediate video",
+ "title": "Is Intermediate"
+ },
+ "description": "Whether this is an intermediate video"
+ },
+ {
+ "name": "board_id",
+ "in": "query",
+ "required": false,
+ "schema": {
+ "anyOf": [
+ {
+ "type": "string"
+ },
+ {
+ "type": "null"
+ }
+ ],
+ "description": "The board to add this video to, if any",
+ "title": "Board Id"
+ },
+ "description": "The board to add this video to, if any"
+ },
+ {
+ "name": "session_id",
+ "in": "query",
+ "required": false,
+ "schema": {
+ "anyOf": [
+ {
+ "type": "string"
+ },
+ {
+ "type": "null"
+ }
+ ],
+ "description": "The session ID associated with this upload, if any",
+ "title": "Session Id"
+ },
+ "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": {
+ "201": {
+ "description": "The video was uploaded successfully",
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/VideoDTO"
+ }
+ }
+ }
+ },
+ "415": {
+ "description": "Video upload failed"
+ },
+ "422": {
+ "description": "Validation Error",
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/HTTPValidationError"
+ }
+ }
+ }
+ }
+ }
+ }
+ },
+ "/api/v1/videos/i/{video_name}": {
+ "delete": {
+ "tags": ["videos"],
+ "summary": "Delete Video",
+ "operationId": "delete_video",
+ "security": [
+ {
+ "HTTPBearer": []
+ }
+ ],
+ "parameters": [
+ {
+ "name": "video_name",
+ "in": "path",
+ "required": true,
+ "schema": {
+ "type": "string",
+ "description": "The name of the video to delete",
+ "title": "Video Name"
+ },
+ "description": "The name of the video to delete"
+ }
+ ],
+ "responses": {
+ "200": {
+ "description": "Successful Response",
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/DeleteVideosResult"
+ }
+ }
+ }
+ },
+ "422": {
+ "description": "Validation Error",
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/HTTPValidationError"
+ }
+ }
+ }
+ }
+ }
+ },
+ "patch": {
+ "tags": ["videos"],
+ "summary": "Update Video",
+ "operationId": "update_video",
+ "security": [
+ {
+ "HTTPBearer": []
+ }
+ ],
+ "parameters": [
+ {
+ "name": "video_name",
+ "in": "path",
+ "required": true,
+ "schema": {
+ "type": "string",
+ "description": "The name of the video to update",
+ "title": "Video Name"
+ },
+ "description": "The name of the video to update"
+ }
+ ],
+ "requestBody": {
+ "required": true,
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/VideoRecordChanges",
+ "description": "The changes to apply to the video"
+ }
+ }
+ }
+ },
+ "responses": {
+ "200": {
+ "description": "Successful Response",
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/VideoDTO"
+ }
+ }
+ }
+ },
+ "422": {
+ "description": "Validation Error",
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/HTTPValidationError"
+ }
+ }
+ }
+ }
+ }
+ },
+ "get": {
+ "tags": ["videos"],
+ "summary": "Get Video Dto",
+ "operationId": "get_video_dto",
+ "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": {
+ "200": {
+ "description": "Successful Response",
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/VideoDTO"
+ }
+ }
+ }
+ },
+ "422": {
+ "description": "Validation Error",
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/HTTPValidationError"
+ }
+ }
+ }
+ }
+ }
+ }
+ },
+ "/api/v1/videos/delete": {
+ "post": {
+ "tags": ["videos"],
+ "summary": "Delete Videos From List",
+ "operationId": "delete_videos_from_list",
+ "requestBody": {
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/Body_delete_videos_from_list"
+ }
+ }
+ },
+ "required": true
+ },
+ "responses": {
+ "200": {
+ "description": "Successful Response",
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/DeleteVideosResult"
+ }
+ }
+ }
+ },
+ "422": {
+ "description": "Validation Error",
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/HTTPValidationError"
+ }
+ }
+ }
+ }
+ },
+ "security": [
+ {
+ "HTTPBearer": []
+ }
+ ]
+ }
+ },
+ "/api/v1/videos/i/{video_name}/metadata": {
+ "get": {
+ "tags": ["videos"],
+ "summary": "Get Video Metadata",
+ "operationId": "get_video_metadata",
"security": [
{
"HTTPBearer": []
@@ -5388,25 +5794,32 @@
],
"parameters": [
{
- "name": "board_name",
- "in": "query",
+ "name": "video_name",
+ "in": "path",
"required": true,
"schema": {
"type": "string",
- "maxLength": 300,
- "description": "The name of the board to create",
- "title": "Board Name"
+ "description": "The name of video to get",
+ "title": "Video Name"
},
- "description": "The name of the board to create"
+ "description": "The name of video to get"
}
],
"responses": {
- "201": {
- "description": "The board was created successfully",
+ "200": {
+ "description": "Successful Response",
"content": {
"application/json": {
"schema": {
- "$ref": "#/components/schemas/BoardDTO"
+ "anyOf": [
+ {
+ "$ref": "#/components/schemas/MetadataField"
+ },
+ {
+ "type": "null"
+ }
+ ],
+ "title": "Response Get Video Metadata"
}
}
}
@@ -5422,129 +5835,83 @@
}
}
}
- },
- "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/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\nLike the image equivalent, this endpoint is intentionally unauthenticated because browsers\nload videos via tags which cannot send Bearer tokens. Video names are UUIDs,\nproviding security through unguessability.",
+ "operationId": "get_video_full_head",
"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"
- },
- "description": "The direction to order by"
- },
- {
- "name": "all",
- "in": "query",
- "required": false,
+ "name": "video_name",
+ "in": "path",
+ "required": true,
"schema": {
- "anyOf": [
- {
- "type": "boolean"
- },
- {
- "type": "null"
- }
- ],
- "description": "Whether to list all boards",
- "title": "All"
+ "type": "string",
+ "description": "The name of video file to get",
+ "title": "Video Name"
},
- "description": "Whether to list all boards"
+ "description": "The name of video file to get"
+ }
+ ],
+ "responses": {
+ "200": {
+ "description": "Return the full video file",
+ "content": {
+ "video/mp4": {}
+ }
},
- {
- "name": "offset",
- "in": "query",
- "required": false,
- "schema": {
- "anyOf": [
- {
- "type": "integer"
- },
- {
- "type": "null"
- }
- ],
- "description": "The page offset",
- "title": "Offset"
- },
- "description": "The page offset"
+ "404": {
+ "description": "Video not found"
},
- {
- "name": "limit",
- "in": "query",
- "required": false,
- "schema": {
- "anyOf": [
- {
- "type": "integer"
- },
- {
- "type": "null"
+ "422": {
+ "description": "Validation Error",
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/HTTPValidationError"
}
- ],
- "description": "The number of boards per page",
- "title": "Limit"
- },
- "description": "The number of boards per page"
- },
+ }
+ }
+ }
+ }
+ },
+ "get": {
+ "tags": ["videos"],
+ "summary": "Get Video Full",
+ "description": "Serves the video file with HTTP Range support so HTML5 seek/scrub works.\n\nLike the image equivalent, this endpoint is intentionally unauthenticated because browsers\nload videos via tags which cannot send Bearer tokens. Video names are UUIDs,\nproviding security through unguessability.",
+ "operationId": "get_video_full",
+ "parameters": [
{
- "name": "include_archived",
- "in": "query",
- "required": false,
+ "name": "video_name",
+ "in": "path",
+ "required": true,
"schema": {
- "type": "boolean",
- "description": "Whether or not to include archived boards in list",
- "default": false,
- "title": "Include Archived"
+ "type": "string",
+ "description": "The name of video file to get",
+ "title": "Video Name"
},
- "description": "Whether or not to include archived boards in list"
+ "description": "The name of video file to get"
}
],
"responses": {
"200": {
- "description": "Successful Response",
+ "description": "Return the full video file",
"content": {
- "application/json": {
- "schema": {
- "anyOf": [
- {
- "$ref": "#/components/schemas/OffsetPaginatedResults_BoardDTO_"
- },
- {
- "type": "array",
- "items": {
- "$ref": "#/components/schemas/BoardDTO"
- }
- }
- ],
- "title": "Response List Boards"
- }
- }
+ "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": {
@@ -5558,41 +5925,35 @@
}
}
},
- "/api/v1/boards/{board_id}": {
+ "/api/v1/videos/i/{video_name}/thumbnail": {
"get": {
- "tags": ["boards"],
- "summary": "Get Board",
- "description": "Gets a board (user must have access to it)",
- "operationId": "get_board",
- "security": [
- {
- "HTTPBearer": []
- }
- ],
+ "tags": ["videos"],
+ "summary": "Get Video Thumbnail",
+ "description": "Returns the first-frame WebP thumbnail of a video. Unauthenticated; UUIDs provide unguessability.",
+ "operationId": "get_video_thumbnail",
"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 thumbnail file to get",
+ "title": "Video Name"
},
- "description": "The id of board to get"
+ "description": "The name of thumbnail file to get"
}
],
"responses": {
"200": {
- "description": "Successful Response",
+ "description": "Return the video thumbnail",
"content": {
- "application/json": {
- "schema": {
- "$ref": "#/components/schemas/BoardDTO"
- }
- }
+ "image/webp": {}
}
},
+ "404": {
+ "description": "Video not found"
+ },
"422": {
"description": "Validation Error",
"content": {
@@ -5604,12 +5965,13 @@
}
}
}
- },
- "patch": {
- "tags": ["boards"],
- "summary": "Update Board",
- "description": "Updates a board (user must have access to it)",
- "operationId": "update_board",
+ }
+ },
+ "/api/v1/videos/i/{video_name}/urls": {
+ "get": {
+ "tags": ["videos"],
+ "summary": "Get Video Urls",
+ "operationId": "get_video_urls",
"security": [
{
"HTTPBearer": []
@@ -5617,35 +5979,24 @@
],
"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 whose URL to get",
+ "title": "Video Name"
},
- "description": "The id of board to update"
+ "description": "The name of the video whose URL to get"
}
],
- "requestBody": {
- "required": true,
- "content": {
- "application/json": {
- "schema": {
- "$ref": "#/components/schemas/BoardChanges",
- "description": "The changes to apply to the board"
- }
- }
- }
- },
"responses": {
- "201": {
- "description": "The board was updated successfully",
+ "200": {
+ "description": "Successful Response",
"content": {
"application/json": {
"schema": {
- "$ref": "#/components/schemas/BoardDTO"
+ "$ref": "#/components/schemas/VideoUrlsDTO"
}
}
}
@@ -5661,47 +6012,159 @@
}
}
}
- },
- "delete": {
- "tags": ["boards"],
- "summary": "Delete Board",
- "description": "Deletes a board (user must have access to it)",
- "operationId": "delete_board",
+ }
+ },
+ "/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": [
{
"HTTPBearer": []
}
],
"parameters": [
+ {
+ "name": "video_origin",
+ "in": "query",
+ "required": false,
+ "schema": {
+ "anyOf": [
+ {
+ "$ref": "#/components/schemas/ResourceOrigin"
+ },
+ {
+ "type": "null"
+ }
+ ],
+ "description": "The origin of videos to list.",
+ "title": "Video Origin"
+ },
+ "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": "path",
- "required": true,
+ "in": "query",
+ "required": false,
"schema": {
- "type": "string",
- "description": "The id of board to delete",
+ "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 id of board to delete"
+ "description": "The board id to filter by. Use 'none' to find videos without a board."
},
{
- "name": "include_images",
+ "name": "offset",
+ "in": "query",
+ "required": false,
+ "schema": {
+ "type": "integer",
+ "description": "The page offset",
+ "default": 0,
+ "title": "Offset"
+ },
+ "description": "The page offset"
+ },
+ {
+ "name": "limit",
+ "in": "query",
+ "required": false,
+ "schema": {
+ "type": "integer",
+ "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",
"in": "query",
"required": false,
"schema": {
"anyOf": [
{
- "type": "boolean"
+ "type": "string"
},
{
"type": "null"
}
],
- "description": "Permanently delete all images on the board",
- "default": false,
- "title": "Include Images"
+ "description": "The term to search for",
+ "title": "Search Term"
},
- "description": "Permanently delete all images on the board"
+ "description": "The term to search for"
}
],
"responses": {
@@ -5710,7 +6173,7 @@
"content": {
"application/json": {
"schema": {
- "$ref": "#/components/schemas/DeleteBoardResult"
+ "$ref": "#/components/schemas/OffsetPaginatedResults_VideoDTO_"
}
}
}
@@ -5728,12 +6191,12 @@
}
}
},
- "/api/v1/boards/{board_id}/image_names": {
+ "/api/v1/videos/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 Names",
+ "description": "Gets ordered list of video names with metadata for optimistic updates.",
+ "operationId": "get_video_names",
"security": [
{
"HTTPBearer": []
@@ -5741,15 +6204,22 @@
],
"parameters": [
{
- "name": "board_id",
- "in": "path",
- "required": true,
+ "name": "video_origin",
+ "in": "query",
+ "required": false,
"schema": {
- "type": "string",
- "description": "The id of the board or 'none' for uncategorized images",
- "title": "Board Id"
+ "anyOf": [
+ {
+ "$ref": "#/components/schemas/ResourceOrigin"
+ },
+ {
+ "type": "null"
+ }
+ ],
+ "description": "The origin of videos to list.",
+ "title": "Video Origin"
},
- "description": "The id of the board or 'none' for uncategorized images"
+ "description": "The origin of videos to list."
},
{
"name": "categories",
@@ -5767,10 +6237,10 @@
"type": "null"
}
],
- "description": "The categories of image to include.",
+ "description": "The categories of video to include.",
"title": "Categories"
},
- "description": "The categories of image to include."
+ "description": "The categories of video to include."
},
{
"name": "is_intermediate",
@@ -5785,10 +6255,69 @@
"type": "null"
}
],
- "description": "Whether to list intermediate images.",
+ "description": "Whether to list intermediate videos.",
"title": "Is Intermediate"
},
- "description": "Whether to list intermediate images."
+ "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": {
@@ -5797,11 +6326,7 @@
"content": {
"application/json": {
"schema": {
- "type": "array",
- "items": {
- "type": "string"
- },
- "title": "Response List All Board Image Names"
+ "$ref": "#/components/schemas/VideoNamesResult"
}
}
}
@@ -5819,29 +6344,28 @@
}
}
},
- "/api/v1/board_images/": {
+ "/api/v1/videos/star": {
"post": {
- "tags": ["boards"],
- "summary": "Add Image To Board",
- "description": "Creates a board_image",
- "operationId": "add_image_to_board",
+ "tags": ["videos"],
+ "summary": "Star Videos In List",
+ "operationId": "star_videos_in_list",
"requestBody": {
"content": {
"application/json": {
"schema": {
- "$ref": "#/components/schemas/Body_add_image_to_board"
+ "$ref": "#/components/schemas/Body_star_videos_in_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/StarredVideosResult"
}
}
}
@@ -5862,29 +6386,30 @@
"HTTPBearer": []
}
]
- },
- "delete": {
- "tags": ["boards"],
- "summary": "Remove Image From Board",
- "description": "Removes an image from its board, if it had one",
- "operationId": "remove_image_from_board",
+ }
+ },
+ "/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/Body_remove_image_from_board"
+ "$ref": "#/components/schemas/Body_unstar_videos_in_list"
}
}
},
"required": true
},
"responses": {
- "201": {
- "description": "The image was removed from the board successfully",
+ "200": {
+ "description": "Successful Response",
"content": {
"application/json": {
"schema": {
- "$ref": "#/components/schemas/RemoveImagesFromBoardResult"
+ "$ref": "#/components/schemas/UnstarredVideosResult"
}
}
}
@@ -5907,29 +6432,28 @@
]
}
},
- "/api/v1/board_images/batch": {
+ "/api/v1/videos/board": {
"post": {
- "tags": ["boards"],
- "summary": "Add Images To Board",
- "description": "Adds a list of images to a board",
- "operationId": "add_images_to_board",
+ "tags": ["videos"],
+ "summary": "Add Video To Board",
+ "operationId": "add_video_to_board",
"requestBody": {
"content": {
"application/json": {
"schema": {
- "$ref": "#/components/schemas/Body_add_images_to_board"
+ "$ref": "#/components/schemas/VideoBoardArg"
}
}
},
"required": true
},
"responses": {
- "201": {
- "description": "Images were added to board successfully",
+ "200": {
+ "description": "Successful Response",
"content": {
"application/json": {
"schema": {
- "$ref": "#/components/schemas/AddImagesToBoardResult"
+ "$ref": "#/components/schemas/AddVideosToBoardResult"
}
}
}
@@ -5950,31 +6474,28 @@
"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",
+ },
+ "delete": {
+ "tags": ["videos"],
+ "summary": "Remove Video From Board",
+ "operationId": "remove_video_from_board",
"requestBody": {
"content": {
"application/json": {
"schema": {
- "$ref": "#/components/schemas/Body_remove_images_from_board"
+ "$ref": "#/components/schemas/Body_remove_video_from_board"
}
}
},
"required": true
},
"responses": {
- "201": {
- "description": "Images were removed from board successfully",
+ "200": {
+ "description": "Successful Response",
"content": {
"application/json": {
"schema": {
- "$ref": "#/components/schemas/RemoveImagesFromBoardResult"
+ "$ref": "#/components/schemas/RemoveVideosFromBoardResult"
}
}
}
@@ -5997,41 +6518,12 @@
]
}
},
- "/api/v1/virtual_boards/by_date": {
+ "/api/v1/gallery/items/": {
"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"
- }
- }
- }
- }
- },
- "security": [
- {
- "HTTPBearer": []
- }
- ]
- }
- },
- "/api/v1/virtual_boards/by_date/{date}/image_names": {
- "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": ["gallery"],
+ "summary": "List Gallery Items",
+ "description": "Returns a paginated, time-sorted stream of polymorphic gallery items (images + videos).",
+ "operationId": "list_gallery_items",
"security": [
{
"HTTPBearer": []
@@ -6039,59 +6531,126 @@
],
"parameters": [
{
- "name": "date",
- "in": "path",
- "required": true,
+ "name": "origin",
+ "in": "query",
+ "required": false,
"schema": {
- "type": "string",
- "description": "The ISO date string, e.g. '2026-03-18'",
- "title": "Date"
+ "anyOf": [
+ {
+ "$ref": "#/components/schemas/ResourceOrigin"
+ },
+ {
+ "type": "null"
+ }
+ ],
+ "description": "The origin of items to list.",
+ "title": "Origin"
},
- "description": "The ISO date string, e.g. '2026-03-18'"
+ "description": "The origin of items to list."
},
{
- "name": "starred_first",
+ "name": "categories",
"in": "query",
"required": false,
"schema": {
- "type": "boolean",
- "description": "Whether to sort starred images first",
- "default": true,
- "title": "Starred First"
+ "anyOf": [
+ {
+ "type": "array",
+ "items": {
+ "$ref": "#/components/schemas/ImageCategory"
+ }
+ },
+ {
+ "type": "null"
+ }
+ ],
+ "description": "The categories to include. Shared between images and videos.",
+ "title": "Categories"
},
- "description": "Whether to sort starred images first"
+ "description": "The categories to include. Shared between images and videos."
},
{
- "name": "order_dir",
+ "name": "is_intermediate",
"in": "query",
"required": false,
"schema": {
- "$ref": "#/components/schemas/SQLiteDirection",
- "description": "The sort direction",
- "default": "DESC"
+ "anyOf": [
+ {
+ "type": "boolean"
+ },
+ {
+ "type": "null"
+ }
+ ],
+ "description": "Whether to list intermediate items.",
+ "title": "Is Intermediate"
},
- "description": "The sort direction"
+ "description": "Whether to list intermediate items."
},
{
- "name": "categories",
+ "name": "board_id",
"in": "query",
"required": false,
"schema": {
"anyOf": [
{
- "type": "array",
- "items": {
- "$ref": "#/components/schemas/ImageCategory"
- }
+ "type": "string"
},
{
"type": "null"
}
],
- "description": "The categories of images to include",
- "title": "Categories"
+ "description": "The board id to filter by. Use 'none' to find items without a board.",
+ "title": "Board Id"
},
- "description": "The categories of images to include"
+ "description": "The board id to filter by. Use 'none' to find items without a board."
+ },
+ {
+ "name": "offset",
+ "in": "query",
+ "required": false,
+ "schema": {
+ "type": "integer",
+ "description": "The page offset",
+ "default": 0,
+ "title": "Offset"
+ },
+ "description": "The page offset"
+ },
+ {
+ "name": "limit",
+ "in": "query",
+ "required": false,
+ "schema": {
+ "type": "integer",
+ "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",
@@ -6106,10 +6665,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": {
@@ -6118,7 +6677,7 @@
"content": {
"application/json": {
"schema": {
- "$ref": "#/components/schemas/ImageNamesResult"
+ "$ref": "#/components/schemas/OffsetPaginatedResults_GalleryItem_"
}
}
}
@@ -6136,229 +6695,152 @@
}
}
},
- "/api/v1/model_relationships/i/{model_key}": {
+ "/api/v1/gallery/items/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": ["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": []
+ }
+ ],
"parameters": [
{
- "name": "model_key",
- "in": "path",
- "required": true,
+ "name": "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 items to list.",
+ "title": "Origin"
},
- "description": "The key of the model to get relationships for"
- }
- ],
- "responses": {
- "200": {
- "description": "A list of related model keys was retrieved successfully",
- "content": {
- "application/json": {
- "schema": {
+ "description": "The origin of items to list."
+ },
+ {
+ "name": "categories",
+ "in": "query",
+ "required": false,
+ "schema": {
+ "anyOf": [
+ {
"type": "array",
"items": {
- "type": "string"
- },
- "title": "Response Get Related Models"
+ "$ref": "#/components/schemas/ImageCategory"
+ }
},
- "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"
- }
- }
- }
- },
- "/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"
- }
- }
- },
- "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"
+ {
+ "type": "null"
+ }
+ ],
+ "description": "The categories to include. Shared between images and videos.",
+ "title": "Categories"
+ },
+ "description": "The categories to include. Shared between images and videos."
},
- "400": {
- "description": "Invalid model keys or self-referential relationship"
+ {
+ "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."
},
- "404": {
- "description": "The relationship does not exist"
+ {
+ "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."
},
- "422": {
- "description": "Validation error"
+ {
+ "name": "order_dir",
+ "in": "query",
+ "required": false,
+ "schema": {
+ "$ref": "#/components/schemas/SQLiteDirection",
+ "description": "The order of sort",
+ "default": "DESC"
+ },
+ "description": "The order of sort"
},
- "500": {
- "description": "Internal server error"
- }
- }
- }
- },
- "/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"
- }
- }
+ {
+ "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"
},
- "required": true
- },
- "responses": {
- "200": {
- "description": "Related model keys retrieved successfully",
- "content": {
- "application/json": {
- "schema": {
- "items": {
- "type": "string"
- },
- "type": "array",
- "title": "Response Get Related Models Batch"
+ {
+ "name": "search_term",
+ "in": "query",
+ "required": false,
+ "schema": {
+ "anyOf": [
+ {
+ "type": "string"
},
- "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"
- },
- "500": {
- "description": "Internal server error"
- }
- }
- }
- },
- "/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"
+ {
+ "type": "null"
}
- }
- }
+ ],
+ "description": "The term to search for",
+ "title": "Search Term"
+ },
+ "description": "The term to search for"
}
- }
- }
- },
- "/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"
+ "$ref": "#/components/schemas/GalleryItemNamesResult"
}
}
}
- }
- }
- }
- },
- "/api/v1/app/patchmatch_status": {
- "get": {
- "tags": ["app"],
- "summary": "Get Patchmatch Status",
- "operationId": "get_patchmatch_status",
- "responses": {
- "200": {
- "description": "Successful Response",
+ },
+ "422": {
+ "description": "Validation Error",
"content": {
"application/json": {
"schema": {
- "type": "boolean",
- "title": "Response Get Patchmatch Status"
+ "$ref": "#/components/schemas/HTTPValidationError"
}
}
}
@@ -6366,46 +6848,38 @@
}
}
},
- "/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/InvokeAIAppConfigWithSetFields"
- }
- }
- }
+ "/api/v1/boards/": {
+ "post": {
+ "tags": ["boards"],
+ "summary": "Create Board",
+ "description": "Creates a board for the current user",
+ "operationId": "create_board",
+ "security": [
+ {
+ "HTTPBearer": []
}
- }
- },
- "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"
- }
- }
- },
- "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": "Successful Response",
+ "201": {
+ "description": "The board was created successfully",
"content": {
"application/json": {
"schema": {
- "$ref": "#/components/schemas/InvokeAIAppConfigWithSetFields"
+ "$ref": "#/components/schemas/BoardDTO"
}
}
}
@@ -6420,65 +6894,13 @@
}
}
}
- },
- "security": [
- {
- "HTTPBearer": []
- }
- ]
- }
- },
- "/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": {
- "items": {
- "$ref": "#/components/schemas/ExternalProviderStatusModel"
- },
- "type": "array",
- "title": "Response Get External Provider Statuses"
- }
- }
- }
- }
- }
- }
- },
- "/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"
- },
- "type": "array",
- "title": "Response Get External Provider Configs"
- }
- }
- }
- }
- }
- }
- },
- "/api/v1/app/external_providers/config/{provider_id}": {
- "post": {
- "tags": ["app"],
- "summary": "Set External Provider Config",
- "operationId": "set_external_provider_config",
+ }
+ },
+ "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": []
@@ -6486,35 +6908,112 @@
],
"parameters": [
{
- "name": "provider_id",
- "in": "path",
- "required": true,
+ "name": "order_by",
+ "in": "query",
+ "required": false,
"schema": {
- "type": "string",
- "description": "The external provider identifier",
- "title": "Provider Id"
+ "$ref": "#/components/schemas/BoardRecordOrderBy",
+ "description": "The attribute to order by",
+ "default": "created_at"
},
- "description": "The external provider identifier"
+ "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"
+ },
+ "description": "The direction to order by"
+ },
+ {
+ "name": "all",
+ "in": "query",
+ "required": false,
+ "schema": {
+ "anyOf": [
+ {
+ "type": "boolean"
+ },
+ {
+ "type": "null"
+ }
+ ],
+ "description": "Whether to list all boards",
+ "title": "All"
+ },
+ "description": "Whether to list all boards"
+ },
+ {
+ "name": "offset",
+ "in": "query",
+ "required": false,
+ "schema": {
+ "anyOf": [
+ {
+ "type": "integer"
+ },
+ {
+ "type": "null"
+ }
+ ],
+ "description": "The page offset",
+ "title": "Offset"
+ },
+ "description": "The page offset"
+ },
+ {
+ "name": "limit",
+ "in": "query",
+ "required": false,
+ "schema": {
+ "anyOf": [
+ {
+ "type": "integer"
+ },
+ {
+ "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": "Whether or not to include archived boards in list"
}
],
- "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"
+ "anyOf": [
+ {
+ "$ref": "#/components/schemas/OffsetPaginatedResults_BoardDTO_"
+ },
+ {
+ "type": "array",
+ "items": {
+ "$ref": "#/components/schemas/BoardDTO"
+ }
+ }
+ ],
+ "title": "Response List Boards"
}
}
}
@@ -6530,11 +7029,14 @@
}
}
}
- },
- "delete": {
- "tags": ["app"],
- "summary": "Reset External Provider Config",
- "operationId": "reset_external_provider_config",
+ }
+ },
+ "/api/v1/boards/{board_id}": {
+ "get": {
+ "tags": ["boards"],
+ "summary": "Get Board",
+ "description": "Gets a board (user must have access to it)",
+ "operationId": "get_board",
"security": [
{
"HTTPBearer": []
@@ -6542,15 +7044,15 @@
],
"parameters": [
{
- "name": "provider_id",
+ "name": "board_id",
"in": "path",
"required": true,
"schema": {
"type": "string",
- "description": "The external provider identifier",
- "title": "Provider Id"
+ "description": "The id of board to get",
+ "title": "Board Id"
},
- "description": "The external provider identifier"
+ "description": "The id of board to get"
}
],
"responses": {
@@ -6559,7 +7061,7 @@
"content": {
"application/json": {
"schema": {
- "$ref": "#/components/schemas/ExternalProviderConfigModel"
+ "$ref": "#/components/schemas/BoardDTO"
}
}
}
@@ -6575,147 +7077,12 @@
}
}
}
- }
- },
- "/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"
- }
- }
- }
- }
- }
},
- "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
- },
- "responses": {
- "200": {
- "description": "The operation was successful",
- "content": {
- "application/json": {
- "schema": {
- "$ref": "#/components/schemas/LogLevel"
- }
- }
- }
- },
- "422": {
- "description": "Validation Error",
- "content": {
- "application/json": {
- "schema": {
- "$ref": "#/components/schemas/HTTPValidationError"
- }
- }
- }
- }
- }
- }
- },
- "/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": {}
- }
- }
- }
- }
- }
- },
- "/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": {}
- }
- }
- }
- }
- }
- },
- "/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": {}
- }
- }
- }
- }
- }
- },
- "/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"
- }
- }
- }
- }
- }
- }
- },
- "/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",
+ "patch": {
+ "tags": ["boards"],
+ "summary": "Update Board",
+ "description": "Updates a board (user must have access to it)",
+ "operationId": "update_board",
"security": [
{
"HTTPBearer": []
@@ -6723,15 +7090,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": {
@@ -6739,31 +7106,22 @@
"content": {
"application/json": {
"schema": {
- "$ref": "#/components/schemas/Body_enqueue_batch"
+ "$ref": "#/components/schemas/BoardChanges",
+ "description": "The changes to apply to the board"
}
}
}
},
"responses": {
- "200": {
- "description": "Successful Response",
- "content": {
- "application/json": {
- "schema": {
- "$ref": "#/components/schemas/EnqueueBatchResult"
- }
- }
- }
- },
"201": {
+ "description": "The board was updated successfully",
"content": {
"application/json": {
"schema": {
- "$ref": "#/components/schemas/EnqueueBatchResult"
+ "$ref": "#/components/schemas/BoardDTO"
}
}
- },
- "description": "Created"
+ }
},
"422": {
"description": "Validation Error",
@@ -6776,14 +7134,12 @@
}
}
}
- }
- },
- "/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",
+ },
+ "delete": {
+ "tags": ["boards"],
+ "summary": "Delete Board",
+ "description": "Deletes a board (user must have access to it)",
+ "operationId": "delete_board",
"security": [
{
"HTTPBearer": []
@@ -6791,33 +7147,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": "destination",
+ "name": "include_images",
"in": "query",
"required": false,
"schema": {
"anyOf": [
{
- "type": "string"
+ "type": "boolean"
},
{
"type": "null"
}
],
- "description": "The destination of queue items to fetch",
- "title": "Destination"
+ "description": "Permanently delete all images on the board",
+ "default": false,
+ "title": "Include Images"
},
- "description": "The destination of queue items to fetch"
+ "description": "Permanently delete all images on the board"
}
],
"responses": {
@@ -6826,11 +7183,7 @@
"content": {
"application/json": {
"schema": {
- "type": "array",
- "items": {
- "$ref": "#/components/schemas/SessionQueueItem"
- },
- "title": "Response 200 List All Queue Items"
+ "$ref": "#/components/schemas/DeleteBoardResult"
}
}
}
@@ -6848,12 +7201,12 @@
}
}
},
- "/api/v1/queue/{queue_id}/item_ids": {
+ "/api/v1/boards/{board_id}/image_names": {
"get": {
- "tags": ["queue"],
- "summary": "Get Queue Item Ids",
- "description": "Gets all queue item ids that match the given parameters. Non-admin users only see their own items.",
- "operationId": "get_queue_item_ids",
+ "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": []
@@ -6861,26 +7214,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": "order_dir",
+ "name": "categories",
"in": "query",
"required": false,
"schema": {
- "$ref": "#/components/schemas/SQLiteDirection",
- "description": "The order of sort",
- "default": "DESC"
+ "anyOf": [
+ {
+ "type": "array",
+ "items": {
+ "$ref": "#/components/schemas/ImageCategory"
+ }
+ },
+ {
+ "type": "null"
+ }
+ ],
+ "description": "The categories of image to include.",
+ "title": "Categories"
},
- "description": "The order of sort"
+ "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": {
@@ -6889,7 +7270,11 @@
"content": {
"application/json": {
"schema": {
- "$ref": "#/components/schemas/ItemIdsResult"
+ "type": "array",
+ "items": {
+ "type": "string"
+ },
+ "title": "Response List All Board Image Names"
}
}
}
@@ -6907,51 +7292,29 @@
}
}
},
- "/api/v1/queue/{queue_id}/items_by_ids": {
+ "/api/v1/board_images/": {
"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": []
- }
- ],
- "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"
- }
- ],
+ "tags": ["boards"],
+ "summary": "Add Image To Board",
+ "description": "Creates a board_image",
+ "operationId": "add_image_to_board",
"requestBody": {
- "required": true,
"content": {
"application/json": {
"schema": {
- "$ref": "#/components/schemas/Body_get_queue_items_by_item_ids"
+ "$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": {
- "type": "array",
- "items": {
- "$ref": "#/components/schemas/SessionQueueItem"
- },
- "title": "Response 200 Get Queue Items By Item Ids"
+ "$ref": "#/components/schemas/AddImagesToBoardResult"
}
}
}
@@ -6966,40 +7329,35 @@
}
}
}
- }
- }
- },
- "/api/v1/queue/{queue_id}/processor/resume": {
- "put": {
- "tags": ["queue"],
- "summary": "Resume",
- "description": "Resumes session processor. Admin only.",
- "operationId": "resume",
+ },
"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": {
+ ]
+ },
+ "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": {
+ "201": {
+ "description": "The image was removed from the board successfully",
+ "content": {
"application/json": {
"schema": {
- "$ref": "#/components/schemas/SessionProcessorStatus"
+ "$ref": "#/components/schemas/RemoveImagesFromBoardResult"
}
}
}
@@ -7014,40 +7372,37 @@
}
}
}
- }
- }
- },
- "/api/v1/queue/{queue_id}/processor/pause": {
- "put": {
- "tags": ["queue"],
- "summary": "Pause",
- "description": "Pauses session processor. Admin only.",
- "operationId": "pause",
+ },
"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": {
+ "content": {
+ "application/json": {
+ "schema": {
+ "$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/SessionProcessorStatus"
+ "$ref": "#/components/schemas/AddImagesToBoardResult"
}
}
}
@@ -7062,40 +7417,37 @@
}
}
}
- }
- }
- },
- "/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/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
+ },
"responses": {
- "200": {
- "description": "Successful Response",
+ "201": {
+ "description": "Images were removed from board successfully",
"content": {
"application/json": {
"schema": {
- "$ref": "#/components/schemas/CancelAllExceptCurrentResult"
+ "$ref": "#/components/schemas/RemoveImagesFromBoardResult"
}
}
}
@@ -7110,63 +7462,49 @@
}
}
}
- }
- }
- },
- "/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"
- }
- ],
+ ]
+ }
+ },
+ "/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": {
- "$ref": "#/components/schemas/DeleteAllExceptCurrentResult"
- }
- }
- }
- },
- "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"
}
}
}
}
- }
+ },
+ "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",
+ "/api/v1/virtual_boards/by_date/{date}/image_names": {
+ "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",
"security": [
{
"HTTPBearer": []
@@ -7174,34 +7512,86 @@
],
"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"
}
],
- "requestBody": {
- "required": true,
- "content": {
- "application/json": {
- "schema": {
- "$ref": "#/components/schemas/Body_cancel_by_batch_ids"
- }
- }
- }
- },
"responses": {
"200": {
"description": "Successful Response",
"content": {
"application/json": {
"schema": {
- "$ref": "#/components/schemas/CancelByBatchIDsResult"
+ "$ref": "#/components/schemas/ImageNamesResult"
}
}
}
@@ -7219,121 +7609,186 @@
}
}
},
- "/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": []
- }
- ],
+ "/api/v1/model_relationships/i/{model_key}": {
+ "get": {
+ "tags": ["model_relationships"],
+ "summary": "Get Related Models",
+ "description": "Get a list of model keys related to a given model.",
+ "operationId": "get_related_models",
"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 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 key of the model to get relationships for",
+ "title": "Model Key"
},
- "description": "The destination to cancel all queue items for"
+ "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/CancelByDestinationResult"
- }
+ "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}/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/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"
}
- ],
+ }
+ },
+ "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": {
- "required": true,
"content": {
"application/json": {
"schema": {
- "type": "array",
- "items": {
- "type": "integer"
- },
- "description": "The queue item ids to retry",
- "title": "Item Ids"
+ "$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"
}
+ }
+ }
+ },
+ "/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/RetryItemsResult"
- }
+ "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"
+ }
+ }
+ }
+ },
+ "/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"
}
}
}
@@ -7341,39 +7796,91 @@
}
}
},
- "/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/app/app_deps": {
+ "get": {
+ "tags": ["app"],
+ "summary": "Get App Deps",
+ "operationId": "get_app_deps",
"responses": {
"200": {
"description": "Successful Response",
"content": {
"application/json": {
"schema": {
- "$ref": "#/components/schemas/ClearResult"
- }
- }
+ "additionalProperties": {
+ "type": "string"
+ },
+ "type": "object",
+ "title": "Response Get App Deps"
+ }
+ }
+ }
+ }
+ }
+ }
+ },
+ "/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"
+ }
+ }
+ }
+ }
+ }
+ }
+ },
+ "/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/InvokeAIAppConfigWithSetFields"
+ }
+ }
+ }
+ }
+ }
+ },
+ "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"
+ }
+ }
+ },
+ "required": true
+ },
+ "responses": {
+ "200": {
+ "description": "Successful Response",
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/InvokeAIAppConfigWithSetFields"
+ }
+ }
}
},
"422": {
@@ -7386,15 +7893,65 @@
}
}
}
+ },
+ "security": [
+ {
+ "HTTPBearer": []
+ }
+ ]
+ }
+ },
+ "/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": {
+ "items": {
+ "$ref": "#/components/schemas/ExternalProviderStatusModel"
+ },
+ "type": "array",
+ "title": "Response Get External Provider Statuses"
+ }
+ }
+ }
+ }
}
}
},
- "/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",
+ "/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"
+ },
+ "type": "array",
+ "title": "Response Get External Provider Configs"
+ }
+ }
+ }
+ }
+ }
+ }
+ },
+ "/api/v1/app/external_providers/config/{provider_id}": {
+ "post": {
+ "tags": ["app"],
+ "summary": "Set External Provider Config",
+ "operationId": "set_external_provider_config",
"security": [
{
"HTTPBearer": []
@@ -7402,24 +7959,35 @@
],
"parameters": [
{
- "name": "queue_id",
+ "name": "provider_id",
"in": "path",
"required": true,
"schema": {
"type": "string",
- "description": "The queue id to perform this operation on",
- "title": "Queue Id"
+ "description": "The external provider identifier",
+ "title": "Provider Id"
},
- "description": "The queue id to perform this operation on"
+ "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/PruneResult"
+ "$ref": "#/components/schemas/ExternalProviderConfigModel"
}
}
}
@@ -7435,14 +8003,11 @@
}
}
}
- }
- },
- "/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",
+ },
+ "delete": {
+ "tags": ["app"],
+ "summary": "Reset External Provider Config",
+ "operationId": "reset_external_provider_config",
"security": [
{
"HTTPBearer": []
@@ -7450,15 +8015,15 @@
],
"parameters": [
{
- "name": "queue_id",
+ "name": "provider_id",
"in": "path",
"required": true,
"schema": {
"type": "string",
- "description": "The queue id to perform this operation on",
- "title": "Queue Id"
+ "description": "The external provider identifier",
+ "title": "Provider Id"
},
- "description": "The queue id to perform this operation on"
+ "description": "The external provider identifier"
}
],
"responses": {
@@ -7467,21 +8032,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/ExternalProviderConfigModel"
}
}
}
@@ -7499,51 +8050,48 @@
}
}
},
- "/api/v1/queue/{queue_id}/next": {
+ "/api/v1/app/logging": {
"get": {
- "tags": ["queue"],
- "summary": "Get Next Queue Item",
- "description": "Gets the next queue item, without executing it",
- "operationId": "get_next_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"
+ "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"
+ }
+ }
+ }
}
- ],
+ }
+ },
+ "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
+ },
"responses": {
"200": {
- "description": "Successful Response",
+ "description": "The operation was successful",
"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/LogLevel"
}
}
}
@@ -7561,12 +8109,86 @@
}
}
},
- "/api/v1/queue/{queue_id}/status": {
+ "/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": {}
+ }
+ }
+ }
+ }
+ }
+ },
+ "/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": {}
+ }
+ }
+ }
+ }
+ }
+ },
+ "/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": {}
+ }
+ }
+ }
+ }
+ }
+ },
+ "/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"
+ }
+ }
+ }
+ }
+ }
+ }
+ },
+ "/api/v1/queue/{queue_id}/enqueue_batch": {
+ "post": {
"tags": ["queue"],
- "summary": "Get Queue Status",
- "description": "Gets the status of the session queue. Non-admin users see only their own counts and cannot see current item details unless they own it.",
- "operationId": "get_queue_status",
+ "summary": "Enqueue Batch",
+ "description": "Processes a batch and enqueues the output graphs for execution for the current user.",
+ "operationId": "enqueue_batch",
"security": [
{
"HTTPBearer": []
@@ -7585,17 +8207,37 @@
"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/SessionQueueAndProcessorStatus"
+ "$ref": "#/components/schemas/EnqueueBatchResult"
}
}
}
},
+ "201": {
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/EnqueueBatchResult"
+ }
+ }
+ },
+ "description": "Created"
+ },
"422": {
"description": "Validation Error",
"content": {
@@ -7609,12 +8251,12 @@
}
}
},
- "/api/v1/queue/{queue_id}/b/{batch_id}/status": {
+ "/api/v1/queue/{queue_id}/list_all": {
"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",
+ "summary": "List All Queue Items",
+ "description": "Gets all queue items",
+ "operationId": "list_all_queue_items",
"security": [
{
"HTTPBearer": []
@@ -7633,15 +8275,22 @@
"description": "The queue id to perform this operation on"
},
{
- "name": "batch_id",
- "in": "path",
- "required": true,
+ "name": "destination",
+ "in": "query",
+ "required": false,
"schema": {
- "type": "string",
- "description": "The batch to get the status of",
- "title": "Batch Id"
+ "anyOf": [
+ {
+ "type": "string"
+ },
+ {
+ "type": "null"
+ }
+ ],
+ "description": "The destination of queue items to fetch",
+ "title": "Destination"
},
- "description": "The batch to get the status of"
+ "description": "The destination of queue items to fetch"
}
],
"responses": {
@@ -7650,7 +8299,11 @@
"content": {
"application/json": {
"schema": {
- "$ref": "#/components/schemas/BatchStatus"
+ "type": "array",
+ "items": {
+ "$ref": "#/components/schemas/SessionQueueItem"
+ },
+ "title": "Response 200 List All Queue Items"
}
}
}
@@ -7668,12 +8321,12 @@
}
}
},
- "/api/v1/queue/{queue_id}/i/{item_id}": {
+ "/api/v1/queue/{queue_id}/item_ids": {
"get": {
"tags": ["queue"],
- "summary": "Get Queue Item",
- "description": "Gets a queue item",
- "operationId": "get_queue_item",
+ "summary": "Get Queue Item Ids",
+ "description": "Gets all queue item ids that match the given parameters. Non-admin users only see their own items.",
+ "operationId": "get_queue_item_ids",
"security": [
{
"HTTPBearer": []
@@ -7692,15 +8345,15 @@
"description": "The queue id to perform this operation on"
},
{
- "name": "item_id",
- "in": "path",
- "required": true,
+ "name": "order_dir",
+ "in": "query",
+ "required": false,
"schema": {
- "type": "integer",
- "description": "The queue item to get",
- "title": "Item Id"
+ "$ref": "#/components/schemas/SQLiteDirection",
+ "description": "The order of sort",
+ "default": "DESC"
},
- "description": "The queue item to get"
+ "description": "The order of sort"
}
],
"responses": {
@@ -7709,7 +8362,7 @@
"content": {
"application/json": {
"schema": {
- "$ref": "#/components/schemas/SessionQueueItem"
+ "$ref": "#/components/schemas/ItemIdsResult"
}
}
}
@@ -7725,12 +8378,14 @@
}
}
}
- },
- "delete": {
+ }
+ },
+ "/api/v1/queue/{queue_id}/items_by_ids": {
+ "post": {
"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",
+ "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": []
@@ -7747,25 +8402,30 @@
"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 delete",
- "title": "Item Id"
- },
- "description": "The queue item to delete"
}
],
+ "requestBody": {
+ "required": true,
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/Body_get_queue_items_by_item_ids"
+ }
+ }
+ }
+ },
"responses": {
"200": {
"description": "Successful Response",
"content": {
"application/json": {
- "schema": {}
+ "schema": {
+ "type": "array",
+ "items": {
+ "$ref": "#/components/schemas/SessionQueueItem"
+ },
+ "title": "Response 200 Get Queue Items By Item Ids"
+ }
}
}
},
@@ -7782,12 +8442,12 @@
}
}
},
- "/api/v1/queue/{queue_id}/i/{item_id}/cancel": {
+ "/api/v1/queue/{queue_id}/processor/resume": {
"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",
+ "summary": "Resume",
+ "description": "Resumes session processor. Admin only.",
+ "operationId": "resume",
"security": [
{
"HTTPBearer": []
@@ -7804,17 +8464,6 @@
"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"
}
],
"responses": {
@@ -7823,7 +8472,7 @@
"content": {
"application/json": {
"schema": {
- "$ref": "#/components/schemas/SessionQueueItem"
+ "$ref": "#/components/schemas/SessionProcessorStatus"
}
}
}
@@ -7841,12 +8490,12 @@
}
}
},
- "/api/v1/queue/{queue_id}/counts_by_destination": {
- "get": {
+ "/api/v1/queue/{queue_id}/processor/pause": {
+ "put": {
"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",
+ "summary": "Pause",
+ "description": "Pauses session processor. Admin only.",
+ "operationId": "pause",
"security": [
{
"HTTPBearer": []
@@ -7859,21 +8508,10 @@
"required": true,
"schema": {
"type": "string",
- "description": "The queue id to query",
+ "description": "The queue id to perform this operation on",
"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 destination to query"
+ "description": "The queue id to perform this operation on"
}
],
"responses": {
@@ -7882,7 +8520,7 @@
"content": {
"application/json": {
"schema": {
- "$ref": "#/components/schemas/SessionQueueCountsByDestination"
+ "$ref": "#/components/schemas/SessionProcessorStatus"
}
}
}
@@ -7900,12 +8538,12 @@
}
}
},
- "/api/v1/queue/{queue_id}/d/{destination}": {
- "delete": {
+ "/api/v1/queue/{queue_id}/cancel_all_except_current": {
+ "put": {
"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",
+ "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": []
@@ -7918,21 +8556,10 @@
"required": true,
"schema": {
"type": "string",
- "description": "The queue id to query",
+ "description": "The queue id to perform this operation on",
"title": "Queue Id"
},
- "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"
+ "description": "The queue id to perform this operation on"
}
],
"responses": {
@@ -7941,7 +8568,7 @@
"content": {
"application/json": {
"schema": {
- "$ref": "#/components/schemas/DeleteByDestinationResult"
+ "$ref": "#/components/schemas/CancelAllExceptCurrentResult"
}
}
}
@@ -7959,12 +8586,12 @@
}
}
},
- "/api/v1/workflows/i/{workflow_id}": {
- "get": {
- "tags": ["workflows"],
- "summary": "Get Workflow",
- "description": "Gets a workflow",
- "operationId": "get_workflow",
+ "/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": []
@@ -7972,15 +8599,15 @@
],
"parameters": [
{
- "name": "workflow_id",
+ "name": "queue_id",
"in": "path",
"required": true,
"schema": {
"type": "string",
- "description": "The workflow to get",
- "title": "Workflow Id"
+ "description": "The queue id to perform this operation on",
+ "title": "Queue Id"
},
- "description": "The workflow to get"
+ "description": "The queue id to perform this operation on"
}
],
"responses": {
@@ -7989,7 +8616,7 @@
"content": {
"application/json": {
"schema": {
- "$ref": "#/components/schemas/WorkflowRecordWithThumbnailDTO"
+ "$ref": "#/components/schemas/DeleteAllExceptCurrentResult"
}
}
}
@@ -8005,23 +8632,38 @@
}
}
}
- },
- "patch": {
- "tags": ["workflows"],
- "summary": "Update Workflow",
- "description": "Updates a workflow",
- "operationId": "update_workflow",
+ }
+ },
+ "/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"
+ }
+ ],
"requestBody": {
"required": true,
"content": {
"application/json": {
"schema": {
- "$ref": "#/components/schemas/Body_update_workflow"
+ "$ref": "#/components/schemas/Body_cancel_by_batch_ids"
}
}
}
@@ -8032,7 +8674,7 @@
"content": {
"application/json": {
"schema": {
- "$ref": "#/components/schemas/WorkflowRecordDTO"
+ "$ref": "#/components/schemas/CancelByBatchIDsResult"
}
}
}
@@ -8048,12 +8690,14 @@
}
}
}
- },
- "delete": {
- "tags": ["workflows"],
- "summary": "Delete Workflow",
- "description": "Deletes a workflow",
- "operationId": "delete_workflow",
+ }
+ },
+ "/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": []
@@ -8061,15 +8705,26 @@
],
"parameters": [
{
- "name": "workflow_id",
+ "name": "queue_id",
"in": "path",
"required": true,
"schema": {
"type": "string",
- "description": "The workflow to delete",
- "title": "Workflow Id"
+ "description": "The queue id to perform this operation on",
+ "title": "Queue Id"
},
- "description": "The workflow to delete"
+ "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": {
@@ -8077,7 +8732,9 @@
"description": "Successful Response",
"content": {
"application/json": {
- "schema": {}
+ "schema": {
+ "$ref": "#/components/schemas/CancelByDestinationResult"
+ }
}
}
},
@@ -8094,23 +8751,41 @@
}
}
},
- "/api/v1/workflows/": {
- "post": {
- "tags": ["workflows"],
- "summary": "Create Workflow",
- "description": "Creates a workflow",
- "operationId": "create_workflow",
+ "/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"
+ }
+ ],
"requestBody": {
"required": true,
"content": {
"application/json": {
"schema": {
- "$ref": "#/components/schemas/Body_create_workflow"
+ "type": "array",
+ "items": {
+ "type": "integer"
+ },
+ "description": "The queue item ids to retry",
+ "title": "Item Ids"
}
}
}
@@ -8121,7 +8796,7 @@
"content": {
"application/json": {
"schema": {
- "$ref": "#/components/schemas/WorkflowRecordDTO"
+ "$ref": "#/components/schemas/RetryItemsResult"
}
}
}
@@ -8137,12 +8812,14 @@
}
}
}
- },
- "get": {
- "tags": ["workflows"],
- "summary": "List Workflows",
- "description": "Gets a page of workflows",
- "operationId": "list_workflows",
+ }
+ },
+ "/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": []
@@ -8150,152 +8827,15 @@
],
"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,
- "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,
+ "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": {
@@ -8304,7 +8844,7 @@
"content": {
"application/json": {
"schema": {
- "$ref": "#/components/schemas/PaginatedResults_WorkflowRecordListItemWithThumbnailDTO_"
+ "$ref": "#/components/schemas/ClearResult"
}
}
}
@@ -8322,12 +8862,12 @@
}
}
},
- "/api/v1/workflows/i/{workflow_id}/thumbnail": {
+ "/api/v1/queue/{queue_id}/prune": {
"put": {
- "tags": ["workflows"],
- "summary": "Set Workflow Thumbnail",
- "description": "Sets a workflow's thumbnail image",
- "operationId": "set_workflow_thumbnail",
+ "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": []
@@ -8335,34 +8875,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": {
- "multipart/form-data": {
- "schema": {
- "$ref": "#/components/schemas/Body_set_workflow_thumbnail"
- }
- }
- }
- },
"responses": {
"200": {
"description": "Successful Response",
"content": {
"application/json": {
"schema": {
- "$ref": "#/components/schemas/WorkflowRecordDTO"
+ "$ref": "#/components/schemas/PruneResult"
}
}
}
@@ -8378,12 +8908,14 @@
}
}
}
- },
- "delete": {
- "tags": ["workflows"],
- "summary": "Delete Workflow Thumbnail",
- "description": "Removes a workflow's thumbnail image",
- "operationId": "delete_workflow_thumbnail",
+ }
+ },
+ "/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": []
@@ -8391,15 +8923,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": {
@@ -8408,7 +8940,21 @@
"content": {
"application/json": {
"schema": {
- "$ref": "#/components/schemas/WorkflowRecordDTO"
+ "anyOf": [
+ {
+ "$ref": "#/components/schemas/SessionQueueItem"
+ },
+ {
+ "type": "null"
+ },
+ {
+ "$ref": "#/components/schemas/SessionQueueItem"
+ },
+ {
+ "type": "null"
+ }
+ ],
+ "title": "Response 200 Get Current Queue Item"
}
}
}
@@ -8424,40 +8970,57 @@
}
}
}
- },
+ }
+ },
+ "/api/v1/queue/{queue_id}/next": {
"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",
+ "tags": ["queue"],
+ "summary": "Get Next Queue Item",
+ "description": "Gets the next queue item, without executing it",
+ "operationId": "get_next_queue_item",
+ "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": {
+ "anyOf": [
+ {
+ "$ref": "#/components/schemas/SessionQueueItem"
+ },
+ {
+ "type": "null"
+ },
+ {
+ "$ref": "#/components/schemas/SessionQueueItem"
+ },
+ {
+ "type": "null"
+ }
+ ],
+ "title": "Response 200 Get Next Queue Item"
+ }
}
}
},
- "400": {
- "description": "Bad request"
- },
- "404": {
- "description": "The workflow thumbnail could not be found"
- },
"422": {
"description": "Validation Error",
"content": {
@@ -8471,12 +9034,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}/status": {
+ "get": {
+ "tags": ["queue"],
+ "summary": "Get Queue Status",
+ "description": "Gets the status of the session queue. Non-admin users see only their own counts and cannot see current item details unless they own it.",
+ "operationId": "get_queue_status",
"security": [
{
"HTTPBearer": []
@@ -8484,34 +9047,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/SessionQueueAndProcessorStatus"
}
}
}
@@ -8529,12 +9082,12 @@
}
}
},
- "/api/v1/workflows/tags": {
+ "/api/v1/queue/{queue_id}/b/{batch_id}/status": {
"get": {
- "tags": ["workflows"],
- "summary": "Get All Tags",
- "description": "Gets all unique tags from workflows",
- "operationId": "get_all_tags",
+ "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": []
@@ -8542,43 +9095,26 @@
],
"parameters": [
{
- "name": "categories",
- "in": "query",
- "required": false,
+ "name": "queue_id",
+ "in": "path",
+ "required": true,
"schema": {
- "anyOf": [
- {
- "type": "array",
- "items": {
- "$ref": "#/components/schemas/WorkflowCategory"
- }
- },
- {
- "type": "null"
- }
- ],
- "description": "The categories to include",
- "title": "Categories"
+ "type": "string",
+ "description": "The queue id to perform this operation on",
+ "title": "Queue Id"
},
- "description": "The categories to include"
+ "description": "The queue id to perform this operation on"
},
{
- "name": "is_public",
- "in": "query",
- "required": false,
+ "name": "batch_id",
+ "in": "path",
+ "required": true,
"schema": {
- "anyOf": [
- {
- "type": "boolean"
- },
- {
- "type": "null"
- }
- ],
- "description": "Filter by public/shared status",
- "title": "Is Public"
+ "type": "string",
+ "description": "The batch to get the status of",
+ "title": "Batch Id"
},
- "description": "Filter by public/shared status"
+ "description": "The batch to get the status of"
}
],
"responses": {
@@ -8587,11 +9123,7 @@
"content": {
"application/json": {
"schema": {
- "type": "array",
- "items": {
- "type": "string"
- },
- "title": "Response Get All Tags"
+ "$ref": "#/components/schemas/BatchStatus"
}
}
}
@@ -8609,12 +9141,12 @@
}
}
},
- "/api/v1/workflows/counts_by_tag": {
+ "/api/v1/queue/{queue_id}/i/{item_id}": {
"get": {
- "tags": ["workflows"],
- "summary": "Get Counts By Tag",
- "description": "Counts workflows by tag",
- "operationId": "get_counts_by_tag",
+ "tags": ["queue"],
+ "summary": "Get Queue Item",
+ "description": "Gets a queue item",
+ "operationId": "get_queue_item",
"security": [
{
"HTTPBearer": []
@@ -8622,75 +9154,26 @@
],
"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"
+ "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",
- "in": "query",
- "required": false,
+ "name": "item_id",
+ "in": "path",
+ "required": true,
"schema": {
- "anyOf": [
- {
- "type": "boolean"
- },
- {
- "type": "null"
- }
- ],
- "description": "Filter by public/shared status",
- "title": "Is Public"
+ "type": "integer",
+ "description": "The queue item to get",
+ "title": "Item Id"
},
- "description": "Filter by public/shared status"
+ "description": "The queue item to get"
}
],
"responses": {
@@ -8699,11 +9182,7 @@
"content": {
"application/json": {
"schema": {
- "type": "object",
- "additionalProperties": {
- "type": "integer"
- },
- "title": "Response Get Counts By Tag"
+ "$ref": "#/components/schemas/SessionQueueItem"
}
}
}
@@ -8719,14 +9198,12 @@
}
}
}
- }
- },
- "/api/v1/workflows/counts_by_category": {
- "get": {
- "tags": ["workflows"],
- "summary": "Counts By Category",
- "description": "Counts workflows by category",
- "operationId": "counts_by_category",
+ },
+ "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": []
@@ -8734,54 +9211,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",
- "in": "query",
- "required": false,
+ "name": "item_id",
+ "in": "path",
+ "required": true,
"schema": {
- "anyOf": [
- {
- "type": "boolean"
- },
- {
- "type": "null"
- }
- ],
- "description": "Filter by public/shared status",
- "title": "Is Public"
+ "type": "integer",
+ "description": "The queue item to delete",
+ "title": "Item Id"
},
- "description": "Filter by public/shared status"
+ "description": "The queue item to delete"
}
],
"responses": {
@@ -8789,13 +9238,7 @@
"description": "Successful Response",
"content": {
"application/json": {
- "schema": {
- "type": "object",
- "additionalProperties": {
- "type": "integer"
- },
- "title": "Response Counts By Category"
- }
+ "schema": {}
}
}
},
@@ -8812,12 +9255,12 @@
}
}
},
- "/api/v1/workflows/i/{workflow_id}/opened_at": {
+ "/api/v1/queue/{queue_id}/i/{item_id}/cancel": {
"put": {
- "tags": ["workflows"],
- "summary": "Update Opened At",
- "description": "Updates the opened_at field of a workflow",
- "operationId": "update_opened_at",
+ "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": []
@@ -8825,15 +9268,26 @@
],
"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"
+ },
+ {
+ "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"
}
],
"responses": {
@@ -8841,7 +9295,9 @@
"description": "Successful Response",
"content": {
"application/json": {
- "schema": {}
+ "schema": {
+ "$ref": "#/components/schemas/SessionQueueItem"
+ }
}
}
},
@@ -8858,23 +9314,39 @@
}
}
},
- "/api/v1/style_presets/i/{style_preset_id}": {
+ "/api/v1/queue/{queue_id}/counts_by_destination": {
"get": {
- "tags": ["style_presets"],
- "summary": "Get Style Preset",
- "description": "Gets a style preset",
- "operationId": "get_style_preset",
+ "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": []
+ }
+ ],
"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 query",
+ "title": "Queue Id"
},
- "description": "The style preset to get"
+ "description": "The queue id to query"
+ },
+ {
+ "name": "destination",
+ "in": "query",
+ "required": true,
+ "schema": {
+ "type": "string",
+ "description": "The destination to query",
+ "title": "Destination"
+ },
+ "description": "The destination to query"
}
],
"responses": {
@@ -8883,7 +9355,7 @@
"content": {
"application/json": {
"schema": {
- "$ref": "#/components/schemas/StylePresetRecordWithImage"
+ "$ref": "#/components/schemas/SessionQueueCountsByDestination"
}
}
}
@@ -8899,42 +9371,50 @@
}
}
}
- },
- "patch": {
- "tags": ["style_presets"],
- "summary": "Update Style Preset",
- "description": "Updates a style preset",
- "operationId": "update_style_preset",
+ }
+ },
+ "/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": []
+ }
+ ],
"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 query",
+ "title": "Queue Id"
},
- "description": "The id of the style preset to update"
+ "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"
}
],
- "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/DeleteByDestinationResult"
}
}
}
@@ -8950,23 +9430,30 @@
}
}
}
- },
- "delete": {
- "tags": ["style_presets"],
- "summary": "Delete Style Preset",
- "description": "Deletes a style preset",
- "operationId": "delete_style_preset",
+ }
+ },
+ "/api/v1/workflows/i/{workflow_id}": {
+ "get": {
+ "tags": ["workflows"],
+ "summary": "Get Workflow",
+ "description": "Gets a workflow",
+ "operationId": "get_workflow",
+ "security": [
+ {
+ "HTTPBearer": []
+ }
+ ],
"parameters": [
{
- "name": "style_preset_id",
+ "name": "workflow_id",
"in": "path",
"required": true,
"schema": {
"type": "string",
- "description": "The style preset to delete",
- "title": "Style Preset Id"
+ "description": "The workflow to get",
+ "title": "Workflow Id"
},
- "description": "The style preset to delete"
+ "description": "The workflow to get"
}
],
"responses": {
@@ -8974,7 +9461,9 @@
"description": "Successful Response",
"content": {
"application/json": {
- "schema": {}
+ "schema": {
+ "$ref": "#/components/schemas/WorkflowRecordWithThumbnailDTO"
+ }
}
}
},
@@ -8989,45 +9478,26 @@
}
}
}
- }
- },
- "/api/v1/style_presets/": {
- "get": {
- "tags": ["style_presets"],
- "summary": "List Style Presets",
- "description": "Gets a page of style presets",
- "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"
- }
- }
- }
- }
- }
},
- "post": {
- "tags": ["style_presets"],
- "summary": "Create Style Preset",
- "description": "Creates a style preset",
- "operationId": "create_style_preset",
+ "patch": {
+ "tags": ["workflows"],
+ "summary": "Update Workflow",
+ "description": "Updates a workflow",
+ "operationId": "update_workflow",
+ "security": [
+ {
+ "HTTPBearer": []
+ }
+ ],
"requestBody": {
+ "required": true,
"content": {
- "multipart/form-data": {
+ "application/json": {
"schema": {
- "$ref": "#/components/schemas/Body_create_style_preset"
+ "$ref": "#/components/schemas/Body_update_workflow"
}
}
- },
- "required": true
+ }
},
"responses": {
"200": {
@@ -9035,7 +9505,7 @@
"content": {
"application/json": {
"schema": {
- "$ref": "#/components/schemas/StylePresetRecordWithImage"
+ "$ref": "#/components/schemas/WorkflowRecordDTO"
}
}
}
@@ -9051,42 +9521,39 @@
}
}
}
- }
- },
- "/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",
+ },
+ "delete": {
+ "tags": ["workflows"],
+ "summary": "Delete Workflow",
+ "description": "Deletes a workflow",
+ "operationId": "delete_workflow",
+ "security": [
+ {
+ "HTTPBearer": []
+ }
+ ],
"parameters": [
{
- "name": "style_preset_id",
+ "name": "workflow_id",
"in": "path",
"required": true,
"schema": {
"type": "string",
- "description": "The id of the style preset image to get",
- "title": "Style Preset Id"
+ "description": "The workflow to delete",
+ "title": "Workflow Id"
},
- "description": "The id of the style preset image to get"
+ "description": "The workflow to delete"
}
],
"responses": {
"200": {
- "description": "The style preset image was fetched successfully",
+ "description": "Successful Response",
"content": {
"application/json": {
"schema": {}
}
}
},
- "400": {
- "description": "Bad request"
- },
- "404": {
- "description": "The style preset image could not be found"
- },
"422": {
"description": "Validation Error",
"content": {
@@ -9100,45 +9567,35 @@
}
}
},
- "/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": {}
- }
- }
- }
- }
- },
- "/api/v1/style_presets/import": {
+ "/api/v1/workflows/": {
"post": {
- "tags": ["style_presets"],
- "summary": "Import Style Presets",
- "operationId": "import_style_presets",
+ "tags": ["workflows"],
+ "summary": "Create Workflow",
+ "description": "Creates a workflow",
+ "operationId": "create_workflow",
+ "security": [
+ {
+ "HTTPBearer": []
+ }
+ ],
"requestBody": {
+ "required": true,
"content": {
- "multipart/form-data": {
+ "application/json": {
"schema": {
- "$ref": "#/components/schemas/Body_import_style_presets"
+ "$ref": "#/components/schemas/Body_create_workflow"
}
}
- },
- "required": true
+ }
},
"responses": {
"200": {
"description": "Successful Response",
"content": {
"application/json": {
- "schema": {}
+ "schema": {
+ "$ref": "#/components/schemas/WorkflowRecordDTO"
+ }
}
}
},
@@ -9153,14 +9610,12 @@
}
}
}
- }
- },
- "/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",
+ "tags": ["workflows"],
+ "summary": "List Workflows",
+ "description": "Gets a page of workflows",
+ "operationId": "list_workflows",
"security": [
{
"HTTPBearer": []
@@ -9168,43 +9623,161 @@
],
"parameters": [
{
- "name": "queue_id",
- "in": "path",
- "required": true,
+ "name": "page",
+ "in": "query",
+ "required": false,
"schema": {
- "type": "string",
- "description": "The queue id (ignored, kept for backwards compatibility)",
- "title": "Queue Id"
+ "type": "integer",
+ "description": "The page to get",
+ "default": 0,
+ "title": "Page"
},
- "description": "The queue id (ignored, kept for backwards compatibility)"
+ "description": "The page to get"
},
{
- "name": "key",
+ "name": "per_page",
"in": "query",
- "required": true,
+ "required": false,
"schema": {
- "type": "string",
- "description": "Key to get",
- "title": "Key"
- },
- "description": "Key to get"
- }
- ],
- "responses": {
- "200": {
- "description": "Successful Response",
- "content": {
+ "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"
+ }
+ ],
+ "responses": {
+ "200": {
+ "description": "Successful Response",
+ "content": {
"application/json": {
"schema": {
- "anyOf": [
- {
- "type": "string"
- },
- {
- "type": "null"
- }
- ],
- "title": "Response Get Client State By Key"
+ "$ref": "#/components/schemas/PaginatedResults_WorkflowRecordListItemWithThumbnailDTO_"
}
}
}
@@ -9222,12 +9795,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",
+ "/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": []
@@ -9235,36 +9808,23 @@
],
"parameters": [
{
- "name": "queue_id",
+ "name": "workflow_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": "The workflow to update",
+ "title": "Workflow Id"
},
- "description": "Key to set"
+ "description": "The workflow to update"
}
],
"requestBody": {
"required": true,
"content": {
- "application/json": {
+ "multipart/form-data": {
"schema": {
- "type": "string",
- "description": "Stringified value to set",
- "title": "Value"
+ "$ref": "#/components/schemas/Body_set_workflow_thumbnail"
}
}
}
@@ -9275,8 +9835,7 @@
"content": {
"application/json": {
"schema": {
- "type": "string",
- "title": "Response Set Client State"
+ "$ref": "#/components/schemas/WorkflowRecordDTO"
}
}
}
@@ -9292,14 +9851,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",
+ },
+ "delete": {
+ "tags": ["workflows"],
+ "summary": "Delete Workflow Thumbnail",
+ "description": "Removes a workflow's thumbnail image",
+ "operationId": "delete_workflow_thumbnail",
"security": [
{
"HTTPBearer": []
@@ -9307,26 +9864,15 @@
],
"parameters": [
{
- "name": "queue_id",
+ "name": "workflow_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": "The workflow to update",
+ "title": "Workflow Id"
},
- "description": "Prefix to filter keys by"
+ "description": "The workflow to update"
}
],
"responses": {
@@ -9335,11 +9881,7 @@
"content": {
"application/json": {
"schema": {
- "type": "array",
- "items": {
- "type": "string"
- },
- "title": "Response Get Client State Keys By Prefix"
+ "$ref": "#/components/schemas/WorkflowRecordDTO"
}
}
}
@@ -9355,54 +9897,39 @@
}
}
}
- }
- },
- "/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": []
- }
- ],
+ },
+ "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": "queue_id",
+ "name": "workflow_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": "The id of the workflow thumbnail to get",
+ "title": "Workflow Id"
},
- "description": "Key to delete"
+ "description": "The id of the workflow thumbnail to get"
}
],
"responses": {
"200": {
- "description": "Successful Response",
+ "description": "The workflow thumbnail was fetched successfully",
"content": {
"application/json": {
"schema": {}
}
}
},
- "204": {
- "description": "Client state key deleted"
+ "400": {
+ "description": "Bad request"
+ },
+ "404": {
+ "description": "The workflow thumbnail could not be found"
},
"422": {
"description": "Validation Error",
@@ -9417,12 +9944,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/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": []
@@ -9430,29 +9957,38 @@
],
"parameters": [
{
- "name": "queue_id",
+ "name": "workflow_id",
"in": "path",
"required": true,
"schema": {
"type": "string",
- "description": "The queue id (ignored, kept for backwards compatibility)",
- "title": "Queue Id"
+ "description": "The workflow to update",
+ "title": "Workflow Id"
},
- "description": "The queue id (ignored, kept for backwards compatibility)"
+ "description": "The workflow to update"
}
],
+ "requestBody": {
+ "required": true,
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/Body_update_workflow_is_public"
+ }
+ }
+ }
+ },
"responses": {
"200": {
"description": "Successful Response",
"content": {
"application/json": {
- "schema": {}
+ "schema": {
+ "$ref": "#/components/schemas/WorkflowRecordDTO"
+ }
}
}
},
- "204": {
- "description": "Client state deleted"
- },
"422": {
"description": "Validation Error",
"content": {
@@ -9466,12 +10002,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\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/tags": {
+ "get": {
+ "tags": ["workflows"],
+ "summary": "Get All Tags",
+ "description": "Gets all unique tags from workflows",
+ "operationId": "get_all_tags",
"security": [
{
"HTTPBearer": []
@@ -9479,49 +10015,56 @@
],
"parameters": [
{
- "name": "queue_id",
- "in": "path",
- "required": true,
+ "name": "categories",
+ "in": "query",
+ "required": false,
"schema": {
- "type": "string",
- "description": "The queue id to perform this operation on",
- "title": "Queue Id"
+ "anyOf": [
+ {
+ "type": "array",
+ "items": {
+ "$ref": "#/components/schemas/WorkflowCategory"
+ }
+ },
+ {
+ "type": "null"
+ }
+ ],
+ "description": "The categories to include",
+ "title": "Categories"
},
- "description": "The queue id to perform this operation on"
+ "description": "The categories to include"
},
{
- "name": "strict",
+ "name": "is_public",
"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"
+ "anyOf": [
+ {
+ "type": "boolean"
+ },
+ {
+ "type": "null"
+ }
+ ],
+ "description": "Filter by public/shared status",
+ "title": "Is Public"
},
- "description": "When true, parameters not included in the request are reset to their defaults (cleared)."
+ "description": "Filter by public/shared status"
}
],
- "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"
+ "type": "array",
+ "items": {
+ "type": "string"
+ },
+ "title": "Response Get All Tags"
}
}
}
@@ -9537,12 +10080,14 @@
}
}
}
- },
+ }
+ },
+ "/api/v1/workflows/counts_by_tag": {
"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",
+ "tags": ["workflows"],
+ "summary": "Get Counts By Tag",
+ "description": "Counts workflows by tag",
+ "operationId": "get_counts_by_tag",
"security": [
{
"HTTPBearer": []
@@ -9550,15 +10095,75 @@
],
"parameters": [
{
- "name": "queue_id",
- "in": "path",
+ "name": "tags",
+ "in": "query",
"required": true,
"schema": {
- "type": "string",
- "description": "The queue id to retrieve parameters for",
- "title": "Queue Id"
+ "type": "array",
+ "items": {
+ "type": "string"
+ },
+ "description": "The tags to get counts for",
+ "title": "Tags"
},
- "description": "The queue id to retrieve parameters for"
+ "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": {
@@ -9568,8 +10173,10 @@
"application/json": {
"schema": {
"type": "object",
- "additionalProperties": true,
- "title": "Response Get Recall Parameters"
+ "additionalProperties": {
+ "type": "integer"
+ },
+ "title": "Response Get Counts By Tag"
}
}
}
@@ -9587,55 +10194,80 @@
}
}
},
- "/api/v2/custom_nodes/": {
+ "/api/v1/workflows/counts_by_category": {
"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"
- }
- }
- }
- }
- },
+ "tags": ["workflows"],
+ "summary": "Counts By Category",
+ "description": "Counts workflows by category",
+ "operationId": "counts_by_category",
"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
- },
+ ],
+ "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": {
- "$ref": "#/components/schemas/InstallNodePackResponse"
+ "type": "object",
+ "additionalProperties": {
+ "type": "integer"
+ },
+ "title": "Response Counts By Category"
}
}
}
@@ -9650,46 +10282,174 @@
}
}
}
- },
+ }
+ }
+ },
+ "/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/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": [
+ "/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",
+ "parameters": [
{
- "HTTPBearer": []
+ "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",
"parameters": [
{
- "name": "pack_name",
+ "name": "style_preset_id",
"in": "path",
"required": true,
"schema": {
"type": "string",
- "title": "Pack Name"
- }
+ "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/UninstallNodePackResponse"
+ "$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",
+ "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",
@@ -9704,583 +10464,779 @@
}
}
},
- "/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",
+ "/api/v1/style_presets/": {
+ "get": {
+ "tags": ["style_presets"],
+ "summary": "List Style Presets",
+ "description": "Gets a page of style presets",
+ "operationId": "list_style_presets",
"responses": {
"200": {
"description": "Successful Response",
"content": {
"application/json": {
"schema": {
- "additionalProperties": {
- "type": "string"
+ "items": {
+ "$ref": "#/components/schemas/StylePresetRecordWithImage"
},
- "type": "object",
- "title": "Response Reload Custom Nodes"
+ "type": "array",
+ "title": "Response 200 List Style Presets"
}
}
}
}
+ }
+ },
+ "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
},
- "security": [
- {
- "HTTPBearer": []
+ "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"
+ }
+ }
+ }
}
- ]
+ }
}
- }
- },
- "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"
+ },
+ "/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",
+ "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"
},
- "type": "array",
- "title": "Added Images",
- "description": "The image names that were added to the board"
+ "description": "The id of the style preset image to get"
}
- },
- "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"
+ ],
+ "responses": {
+ "200": {
+ "description": "The style preset image was fetched successfully",
+ "content": {
+ "application/json": {
+ "schema": {}
+ }
+ }
},
- "a": {
- "default": 0,
- "description": "The first number",
- "field_kind": "input",
- "input": "any",
- "orig_default": 0,
- "orig_required": false,
- "title": "A",
- "type": "integer"
+ "400": {
+ "description": "Bad request"
},
- "b": {
- "default": 0,
- "description": "The second number",
- "field_kind": "input",
- "input": "any",
- "orig_default": 0,
- "orig_required": false,
- "title": "B",
- "type": "integer"
+ "404": {
+ "description": "The style preset image could not be found"
},
- "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"
+ }
+ },
+ "/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": {}
},
- {
- "type": "null"
+ "text/csv": {}
+ }
+ }
+ }
+ }
+ },
+ "/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"
}
- ],
- "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
- }
+ "required": true
},
- "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"
+ "responses": {
+ "200": {
+ "description": "Successful Response",
+ "content": {
+ "application/json": {
+ "schema": {}
}
- ],
- "title": "Display Name",
- "description": "Display name"
+ }
},
- "password": {
- "anyOf": [
- {
- "type": "string"
- },
- {
- "type": "null"
+ "422": {
+ "description": "Validation Error",
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/HTTPValidationError"
+ }
}
- ],
- "title": "Password",
- "description": "New password"
- },
- "is_admin": {
- "anyOf": [
- {
- "type": "boolean"
- },
- {
- "type": "null"
- }
- ],
- "title": "Is Admin",
- "description": "Whether user should have admin privileges"
+ }
+ }
+ }
+ }
+ },
+ "/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)"
},
- "is_active": {
- "anyOf": [
- {
- "type": "boolean"
- },
- {
- "type": "null"
- }
- ],
- "title": "Is Active",
- "description": "Whether user account should be active"
+ {
+ "name": "key",
+ "in": "query",
+ "required": true,
+ "schema": {
+ "type": "string",
+ "description": "Key to get",
+ "title": "Key"
+ },
+ "description": "Key to get"
}
- },
- "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": {
+ "anyOf": [
+ {
+ "type": "string"
+ },
+ {
+ "type": "null"
+ }
+ ],
+ "title": "Response Get Client State By Key"
+ }
}
- ],
- "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/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)"
},
- "model": {
- "anyOf": [
- {
- "$ref": "#/components/schemas/ModelIdentifierField"
- },
- {
- "type": "null"
+ {
+ "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"
}
- ],
- "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"
+ }
+ }
+ },
+ "responses": {
+ "200": {
+ "description": "Successful Response",
+ "content": {
+ "application/json": {
+ "schema": {
+ "type": "string",
+ "title": "Response Set Client State"
+ }
}
- ],
- "default": null,
- "description": "Prompt",
- "field_kind": "input",
- "input": "any",
- "orig_required": true,
- "title": "Prompt"
+ }
},
- "seed": {
- "anyOf": [
- {
- "type": "integer"
- },
- {
- "type": "null"
+ "422": {
+ "description": "Validation Error",
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/HTTPValidationError"
+ }
}
- ],
- "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,
- "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"
+ }
+ }
+ }
+ }
+ },
+ "/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)"
},
- "image_size": {
- "anyOf": [
- {
- "type": "string"
- },
- {
- "type": "null"
+ {
+ "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"
+ }
}
- ],
- "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"
+ }
},
- "init_image": {
- "anyOf": [
- {
- "$ref": "#/components/schemas/ImageField"
- },
- {
- "type": "null"
+ "422": {
+ "description": "Validation Error",
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/HTTPValidationError"
+ }
}
- ],
- "default": null,
- "description": "Init image for img2img/inpaint",
- "field_kind": "input",
- "input": "any",
- "orig_default": null,
- "orig_required": false
+ }
+ }
+ }
+ }
+ },
+ "/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)"
},
- "mask_image": {
- "anyOf": [
- {
- "$ref": "#/components/schemas/ImageField"
- },
- {
- "type": "null"
+ {
+ "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": {}
}
- ],
- "default": null,
- "description": "Mask image for inpaint",
- "field_kind": "input",
- "input": "any",
- "orig_default": null,
- "orig_required": false
+ }
},
- "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"
+ "204": {
+ "description": "Client state key deleted"
},
- "type": {
- "const": "alibabacloud_image_generation",
- "default": "alibabacloud_image_generation",
- "field_kind": "node_attribute",
- "title": "type",
- "type": "string"
+ "422": {
+ "description": "Validation Error",
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/HTTPValidationError"
+ }
+ }
+ }
}
- },
- "required": ["type", "id"],
- "tags": ["external", "generation", "alibabacloud", "dashscope"],
- "title": "Alibaba Cloud DashScope Image Generation",
- "type": "object",
- "version": "1.0.0",
- "output": {
- "$ref": "#/components/schemas/ImageCollectionOutput"
}
- },
- "AlphaMaskToTensorInvocation": {
- "category": "mask",
- "class": "invocation",
- "classification": "stable",
- "description": "Convert a mask image to a tensor. Opaque regions are 1 and transparent regions are 0.",
- "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"
- },
- "image": {
- "anyOf": [
- {
- "$ref": "#/components/schemas/ImageField"
- },
- {
- "type": "null"
+ }
+ },
+ "/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": {}
}
- ],
- "default": null,
- "description": "The mask image to convert.",
- "field_kind": "input",
- "input": "any",
- "orig_required": true
+ }
},
- "invert": {
- "default": false,
- "description": "Whether to invert the mask.",
- "field_kind": "input",
- "input": "any",
- "orig_default": false,
- "orig_required": false,
- "title": "Invert",
- "type": "boolean"
+ "204": {
+ "description": "Client state deleted"
},
- "type": {
- "const": "alpha_mask_to_tensor",
- "default": "alpha_mask_to_tensor",
- "field_kind": "node_attribute",
- "title": "type",
- "type": "string"
+ "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\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)."
+ }
+ ],
+ "requestBody": {
+ "required": true,
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/RecallParameter",
+ "description": "Recall parameters to update"
+ }
+ }
}
},
- "required": ["type", "id"],
- "tags": ["conditioning"],
- "title": "Alpha Mask to Tensor",
- "type": "object",
- "version": "1.0.0",
- "output": {
- "$ref": "#/components/schemas/MaskOutput"
+ "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"
+ }
+ }
+ }
+ }
}
},
- "AnimaConditioningField": {
- "description": "An Anima conditioning tensor primitive value.\n\nAnima conditioning contains Qwen3 0.6B hidden states and T5-XXL token IDs,\nwhich are combined by the LLM Adapter inside the transformer.",
- "properties": {
- "conditioning_name": {
- "description": "The name of conditioning tensor",
- "title": "Conditioning Name",
- "type": "string"
+ "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"
+ }
+ }
+ }
},
- "mask": {
- "anyOf": [
- {
- "$ref": "#/components/schemas/TensorField"
- },
- {
- "type": "null"
+ "422": {
+ "description": "Validation Error",
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/HTTPValidationError"
+ }
}
- ],
- "default": null,
- "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."
+ }
+ }
+ }
+ }
+ },
+ "/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"
+ }
+ }
+ }
}
},
- "required": ["conditioning_name"],
- "title": "AnimaConditioningField",
- "type": "object"
- },
- "AnimaConditioningOutput": {
- "class": "output",
- "description": "Base class for nodes that output an Anima text conditioning tensor.",
+ "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": {
- "conditioning": {
- "$ref": "#/components/schemas/AnimaConditioningField",
- "description": "Conditioning tensor",
- "field_kind": "output",
- "ui_hidden": false
+ "affected_boards": {
+ "items": {
+ "type": "string"
+ },
+ "type": "array",
+ "title": "Affected Boards",
+ "description": "The ids of boards affected by the delete operation"
},
- "type": {
- "const": "anima_conditioning_output",
- "default": "anima_conditioning_output",
- "field_kind": "node_attribute",
- "title": "type",
- "type": "string"
+ "added_images": {
+ "items": {
+ "type": "string"
+ },
+ "type": "array",
+ "title": "Added Images",
+ "description": "The image names that were added to the board"
}
},
- "required": ["output_meta", "conditioning", "type", "type"],
- "title": "AnimaConditioningOutput",
- "type": "object"
+ "type": "object",
+ "required": ["affected_boards", "added_images"],
+ "title": "AddImagesToBoardResult"
},
- "AnimaDenoiseInvocation": {
- "category": "image",
+ "AddInvocation": {
+ "category": "math",
"class": "invocation",
- "classification": "prototype",
- "description": "Run the denoising process with an Anima model.\n\nUses rectified flow sampling with shift=3.0 and the Cosmos Predict2 DiT\nbackbone with integrated LLM Adapter for text conditioning.\n\nSupports txt2img, img2img (via latents input), and inpainting (via denoise_mask).",
+ "classification": "stable",
+ "description": "Adds two numbers",
"node_pack": "invokeai",
"properties": {
"id": {
@@ -10307,35 +11263,575 @@
"title": "Use Cache",
"type": "boolean"
},
- "latents": {
+ "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": [
{
- "$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": "Display Name",
+ "description": "Display name"
},
- "noise": {
+ "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": [
{
- "$ref": "#/components/schemas/LatentsField"
+ "type": "string"
},
{
"type": "null"
}
],
- "default": null,
- "description": "Noise tensor",
- "field_kind": "input",
- "input": "connection",
+ "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",
+ "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,
+ "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"
+ },
+ {
+ "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"
+ },
+ "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
+ },
+ "mask_image": {
+ "anyOf": [
+ {
+ "$ref": "#/components/schemas/ImageField"
+ },
+ {
+ "type": "null"
+ }
+ ],
+ "default": null,
+ "description": "Mask image for inpaint",
+ "field_kind": "input",
+ "input": "any",
+ "orig_default": null,
+ "orig_required": false
+ },
+ "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"
+ },
+ "type": {
+ "const": "alibabacloud_image_generation",
+ "default": "alibabacloud_image_generation",
+ "field_kind": "node_attribute",
+ "title": "type",
+ "type": "string"
+ }
+ },
+ "required": ["type", "id"],
+ "tags": ["external", "generation", "alibabacloud", "dashscope"],
+ "title": "Alibaba Cloud DashScope Image Generation",
+ "type": "object",
+ "version": "1.0.0",
+ "output": {
+ "$ref": "#/components/schemas/ImageCollectionOutput"
+ }
+ },
+ "AlphaMaskToTensorInvocation": {
+ "category": "mask",
+ "class": "invocation",
+ "classification": "stable",
+ "description": "Convert a mask image to a tensor. Opaque regions are 1 and transparent regions are 0.",
+ "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"
+ },
+ "image": {
+ "anyOf": [
+ {
+ "$ref": "#/components/schemas/ImageField"
+ },
+ {
+ "type": "null"
+ }
+ ],
+ "default": null,
+ "description": "The mask image to convert.",
+ "field_kind": "input",
+ "input": "any",
+ "orig_required": true
+ },
+ "invert": {
+ "default": false,
+ "description": "Whether to invert the mask.",
+ "field_kind": "input",
+ "input": "any",
+ "orig_default": false,
+ "orig_required": false,
+ "title": "Invert",
+ "type": "boolean"
+ },
+ "type": {
+ "const": "alpha_mask_to_tensor",
+ "default": "alpha_mask_to_tensor",
+ "field_kind": "node_attribute",
+ "title": "type",
+ "type": "string"
+ }
+ },
+ "required": ["type", "id"],
+ "tags": ["conditioning"],
+ "title": "Alpha Mask to Tensor",
+ "type": "object",
+ "version": "1.0.0",
+ "output": {
+ "$ref": "#/components/schemas/MaskOutput"
+ }
+ },
+ "AnimaConditioningField": {
+ "description": "An Anima conditioning tensor primitive value.\n\nAnima conditioning contains Qwen3 0.6B hidden states and T5-XXL token IDs,\nwhich are combined by the LLM Adapter inside the transformer.",
+ "properties": {
+ "conditioning_name": {
+ "description": "The name of conditioning tensor",
+ "title": "Conditioning Name",
+ "type": "string"
+ },
+ "mask": {
+ "anyOf": [
+ {
+ "$ref": "#/components/schemas/TensorField"
+ },
+ {
+ "type": "null"
+ }
+ ],
+ "default": null,
+ "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."
+ }
+ },
+ "required": ["conditioning_name"],
+ "title": "AnimaConditioningField",
+ "type": "object"
+ },
+ "AnimaConditioningOutput": {
+ "class": "output",
+ "description": "Base class for nodes that output an Anima text conditioning tensor.",
+ "properties": {
+ "conditioning": {
+ "$ref": "#/components/schemas/AnimaConditioningField",
+ "description": "Conditioning tensor",
+ "field_kind": "output",
+ "ui_hidden": false
+ },
+ "type": {
+ "const": "anima_conditioning_output",
+ "default": "anima_conditioning_output",
+ "field_kind": "node_attribute",
+ "title": "type",
+ "type": "string"
+ }
+ },
+ "required": ["output_meta", "conditioning", "type", "type"],
+ "title": "AnimaConditioningOutput",
+ "type": "object"
+ },
+ "AnimaDenoiseInvocation": {
+ "category": "image",
+ "class": "invocation",
+ "classification": "prototype",
+ "description": "Run the denoising process with an Anima model.\n\nUses rectified flow sampling with shift=3.0 and the Cosmos Predict2 DiT\nbackbone with integrated LLM Adapter for text conditioning.\n\nSupports txt2img, img2img (via latents input), and inpainting (via denoise_mask).",
+ "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/LatentsField"
+ },
+ {
+ "type": "null"
+ }
+ ],
+ "default": null,
+ "description": "Latents tensor",
+ "field_kind": "input",
+ "input": "connection",
+ "orig_default": null,
+ "orig_required": false
+ },
+ "noise": {
+ "anyOf": [
+ {
+ "$ref": "#/components/schemas/LatentsField"
+ },
+ {
+ "type": "null"
+ }
+ ],
+ "default": null,
+ "description": "Noise tensor",
+ "field_kind": "input",
+ "input": "connection",
"orig_default": null,
"orig_required": false
},
@@ -11292,6 +12788,9 @@
{
"$ref": "#/components/schemas/Main_Diffusers_QwenImage_Config"
},
+ {
+ "$ref": "#/components/schemas/Main_Diffusers_Wan_Config"
+ },
{
"$ref": "#/components/schemas/Main_Diffusers_ZImage_Config"
},
@@ -11331,6 +12830,9 @@
{
"$ref": "#/components/schemas/Main_GGUF_QwenImage_Config"
},
+ {
+ "$ref": "#/components/schemas/Main_GGUF_Wan_Config"
+ },
{
"$ref": "#/components/schemas/Main_GGUF_ZImage_Config"
},
@@ -11349,6 +12851,9 @@
{
"$ref": "#/components/schemas/VAE_Checkpoint_Flux2_Config"
},
+ {
+ "$ref": "#/components/schemas/VAE_Checkpoint_Wan_Config"
+ },
{
"$ref": "#/components/schemas/VAE_Checkpoint_QwenImage_Config"
},
@@ -11364,6 +12869,9 @@
{
"$ref": "#/components/schemas/VAE_Diffusers_Flux2_Config"
},
+ {
+ "$ref": "#/components/schemas/VAE_Diffusers_Wan_Config"
+ },
{
"$ref": "#/components/schemas/ControlNet_Checkpoint_SD1_Config"
},
@@ -11412,6 +12920,9 @@
{
"$ref": "#/components/schemas/LoRA_LyCORIS_QwenImage_Config"
},
+ {
+ "$ref": "#/components/schemas/LoRA_LyCORIS_Wan_Config"
+ },
{
"$ref": "#/components/schemas/LoRA_LyCORIS_Anima_Config"
},
@@ -11463,6 +12974,9 @@
{
"$ref": "#/components/schemas/QwenVLEncoder_Checkpoint_Config"
},
+ {
+ "$ref": "#/components/schemas/WanT5Encoder_WanT5Encoder_Config"
+ },
{
"$ref": "#/components/schemas/TI_File_SD1_Config"
},
@@ -11828,6 +13342,7 @@
"external",
"qwen-image",
"anima",
+ "wan",
"unknown"
],
"title": "BaseModelType",
@@ -12652,6 +14167,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": {
@@ -12859,6 +14389,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": {
@@ -12887,6 +14429,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": {
@@ -12902,6 +14459,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": {
@@ -12999,248 +14571,272 @@
"required": ["file"],
"title": "Body_upload_image"
},
- "BooleanCollectionInvocation": {
- "category": "primitives",
- "class": "invocation",
- "classification": "stable",
- "description": "A collection of boolean 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": {
- "default": [],
- "description": "The collection of boolean values",
- "field_kind": "input",
- "input": "any",
- "items": {
- "type": "boolean"
- },
- "orig_default": [],
- "orig_required": false,
- "title": "Collection",
- "type": "array"
- },
- "type": {
- "const": "boolean_collection",
- "default": "boolean_collection",
- "field_kind": "node_attribute",
- "title": "type",
- "type": "string"
- }
- },
- "required": ["type", "id"],
- "tags": ["primitives", "boolean", "collection"],
- "title": "Boolean Collection Primitive",
- "type": "object",
- "version": "1.0.2",
- "output": {
- "$ref": "#/components/schemas/BooleanCollectionOutput"
- }
- },
- "BooleanCollectionOutput": {
- "class": "output",
- "description": "Base class for nodes that output a collection of booleans",
+ "Body_upload_video": {
"properties": {
- "collection": {
- "description": "The output boolean collection",
- "field_kind": "output",
- "items": {
- "type": "boolean"
- },
- "title": "Collection",
- "type": "array",
- "ui_hidden": false
- },
- "type": {
- "const": "boolean_collection_output",
- "default": "boolean_collection_output",
- "field_kind": "node_attribute",
- "title": "type",
- "type": "string"
- }
- },
- "required": ["output_meta", "collection", "type", "type"],
- "title": "BooleanCollectionOutput",
- "type": "object"
- },
- "BooleanInvocation": {
- "category": "primitives",
- "class": "invocation",
- "classification": "stable",
- "description": "A boolean 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": false,
- "description": "The boolean value",
- "field_kind": "input",
- "input": "any",
- "orig_default": false,
- "orig_required": false,
- "title": "Value",
- "type": "boolean"
- },
- "type": {
- "const": "boolean",
- "default": "boolean",
- "field_kind": "node_attribute",
- "title": "type",
- "type": "string"
- }
- },
- "required": ["type", "id"],
- "tags": ["primitives", "boolean"],
- "title": "Boolean Primitive",
- "type": "object",
- "version": "1.0.1",
- "output": {
- "$ref": "#/components/schemas/BooleanOutput"
- }
- },
- "BooleanOutput": {
- "class": "output",
- "description": "Base class for nodes that output a single boolean",
- "properties": {
- "value": {
- "description": "The output boolean",
- "field_kind": "output",
- "title": "Value",
- "type": "boolean",
- "ui_hidden": false
- },
- "type": {
- "const": "boolean_output",
- "default": "boolean_output",
- "field_kind": "node_attribute",
- "title": "type",
- "type": "string"
- }
- },
- "required": ["output_meta", "value", "type", "type"],
- "title": "BooleanOutput",
- "type": "object"
- },
- "BoundingBoxCollectionOutput": {
- "class": "output",
- "description": "Base class for nodes that output a collection of bounding boxes",
- "properties": {
- "collection": {
- "description": "The output bounding boxes.",
- "field_kind": "output",
- "items": {
- "$ref": "#/components/schemas/BoundingBoxField"
- },
- "title": "Bounding Boxes",
- "type": "array",
- "ui_hidden": false
- },
- "type": {
- "const": "bounding_box_collection_output",
- "default": "bounding_box_collection_output",
- "field_kind": "node_attribute",
- "title": "type",
- "type": "string"
- }
- },
- "required": ["output_meta", "collection", "type", "type"],
- "title": "BoundingBoxCollectionOutput",
- "type": "object"
- },
- "BoundingBoxField": {
- "description": "A bounding box primitive value.",
- "properties": {
- "x_min": {
- "description": "The minimum x-coordinate of the bounding box (inclusive).",
- "title": "X Min",
- "type": "integer"
- },
- "x_max": {
- "description": "The maximum x-coordinate of the bounding box (exclusive).",
- "title": "X Max",
- "type": "integer"
- },
- "y_min": {
- "description": "The minimum y-coordinate of the bounding box (inclusive).",
- "title": "Y Min",
- "type": "integer"
- },
- "y_max": {
- "description": "The maximum y-coordinate of the bounding box (exclusive).",
- "title": "Y Max",
- "type": "integer"
+ "file": {
+ "type": "string",
+ "format": "binary",
+ "title": "File"
},
- "score": {
+ "metadata": {
"anyOf": [
{
- "maximum": 1.0,
- "minimum": 0.0,
- "type": "number"
+ "type": "string"
},
{
"type": "null"
}
],
- "default": null,
- "description": "The score associated with the bounding box. In the range [0, 1]. This value is typically set when the bounding box was produced by a detector and has an associated confidence score.",
- "title": "Score"
+ "title": "Metadata",
+ "description": "The metadata to associate with the video, must be a stringified JSON dict"
}
},
- "required": ["x_min", "x_max", "y_min", "y_max"],
- "title": "BoundingBoxField",
- "type": "object"
+ "type": "object",
+ "required": ["file"],
+ "title": "Body_upload_video"
},
- "BoundingBoxInvocation": {
+ "BooleanCollectionInvocation": {
"category": "primitives",
"class": "invocation",
"classification": "stable",
- "description": "Create a bounding box manually by supplying box coordinates",
+ "description": "A collection of boolean 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": {
+ "default": [],
+ "description": "The collection of boolean values",
+ "field_kind": "input",
+ "input": "any",
+ "items": {
+ "type": "boolean"
+ },
+ "orig_default": [],
+ "orig_required": false,
+ "title": "Collection",
+ "type": "array"
+ },
+ "type": {
+ "const": "boolean_collection",
+ "default": "boolean_collection",
+ "field_kind": "node_attribute",
+ "title": "type",
+ "type": "string"
+ }
+ },
+ "required": ["type", "id"],
+ "tags": ["primitives", "boolean", "collection"],
+ "title": "Boolean Collection Primitive",
+ "type": "object",
+ "version": "1.0.2",
+ "output": {
+ "$ref": "#/components/schemas/BooleanCollectionOutput"
+ }
+ },
+ "BooleanCollectionOutput": {
+ "class": "output",
+ "description": "Base class for nodes that output a collection of booleans",
+ "properties": {
+ "collection": {
+ "description": "The output boolean collection",
+ "field_kind": "output",
+ "items": {
+ "type": "boolean"
+ },
+ "title": "Collection",
+ "type": "array",
+ "ui_hidden": false
+ },
+ "type": {
+ "const": "boolean_collection_output",
+ "default": "boolean_collection_output",
+ "field_kind": "node_attribute",
+ "title": "type",
+ "type": "string"
+ }
+ },
+ "required": ["output_meta", "collection", "type", "type"],
+ "title": "BooleanCollectionOutput",
+ "type": "object"
+ },
+ "BooleanInvocation": {
+ "category": "primitives",
+ "class": "invocation",
+ "classification": "stable",
+ "description": "A boolean 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": false,
+ "description": "The boolean value",
+ "field_kind": "input",
+ "input": "any",
+ "orig_default": false,
+ "orig_required": false,
+ "title": "Value",
+ "type": "boolean"
+ },
+ "type": {
+ "const": "boolean",
+ "default": "boolean",
+ "field_kind": "node_attribute",
+ "title": "type",
+ "type": "string"
+ }
+ },
+ "required": ["type", "id"],
+ "tags": ["primitives", "boolean"],
+ "title": "Boolean Primitive",
+ "type": "object",
+ "version": "1.0.1",
+ "output": {
+ "$ref": "#/components/schemas/BooleanOutput"
+ }
+ },
+ "BooleanOutput": {
+ "class": "output",
+ "description": "Base class for nodes that output a single boolean",
+ "properties": {
+ "value": {
+ "description": "The output boolean",
+ "field_kind": "output",
+ "title": "Value",
+ "type": "boolean",
+ "ui_hidden": false
+ },
+ "type": {
+ "const": "boolean_output",
+ "default": "boolean_output",
+ "field_kind": "node_attribute",
+ "title": "type",
+ "type": "string"
+ }
+ },
+ "required": ["output_meta", "value", "type", "type"],
+ "title": "BooleanOutput",
+ "type": "object"
+ },
+ "BoundingBoxCollectionOutput": {
+ "class": "output",
+ "description": "Base class for nodes that output a collection of bounding boxes",
+ "properties": {
+ "collection": {
+ "description": "The output bounding boxes.",
+ "field_kind": "output",
+ "items": {
+ "$ref": "#/components/schemas/BoundingBoxField"
+ },
+ "title": "Bounding Boxes",
+ "type": "array",
+ "ui_hidden": false
+ },
+ "type": {
+ "const": "bounding_box_collection_output",
+ "default": "bounding_box_collection_output",
+ "field_kind": "node_attribute",
+ "title": "type",
+ "type": "string"
+ }
+ },
+ "required": ["output_meta", "collection", "type", "type"],
+ "title": "BoundingBoxCollectionOutput",
+ "type": "object"
+ },
+ "BoundingBoxField": {
+ "description": "A bounding box primitive value.",
+ "properties": {
+ "x_min": {
+ "description": "The minimum x-coordinate of the bounding box (inclusive).",
+ "title": "X Min",
+ "type": "integer"
+ },
+ "x_max": {
+ "description": "The maximum x-coordinate of the bounding box (exclusive).",
+ "title": "X Max",
+ "type": "integer"
+ },
+ "y_min": {
+ "description": "The minimum y-coordinate of the bounding box (inclusive).",
+ "title": "Y Min",
+ "type": "integer"
+ },
+ "y_max": {
+ "description": "The maximum y-coordinate of the bounding box (exclusive).",
+ "title": "Y Max",
+ "type": "integer"
+ },
+ "score": {
+ "anyOf": [
+ {
+ "maximum": 1.0,
+ "minimum": 0.0,
+ "type": "number"
+ },
+ {
+ "type": "null"
+ }
+ ],
+ "default": null,
+ "description": "The score associated with the bounding box. In the range [0, 1]. This value is typically set when the bounding box was produced by a detector and has an associated confidence score.",
+ "title": "Score"
+ }
+ },
+ "required": ["x_min", "x_max", "y_min", "y_max"],
+ "title": "BoundingBoxField",
+ "type": "object"
+ },
+ "BoundingBoxInvocation": {
+ "category": "primitives",
+ "class": "invocation",
+ "classification": "stable",
+ "description": "Create a bounding box manually by supplying box coordinates",
"node_pack": "invokeai",
"properties": {
"id": {
@@ -18739,7 +20335,12 @@
"anima_txt2img",
"anima_img2img",
"anima_inpaint",
- "anima_outpaint"
+ "anima_outpaint",
+ "wan_txt2img",
+ "wan_img2img",
+ "wan_inpaint",
+ "wan_outpaint",
+ "wan_i2v"
],
"type": "string"
},
@@ -20239,6 +21840,22 @@
"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."
}
},
"type": "object",
@@ -20321,6 +21938,29 @@
"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"
+ }
+ },
+ "type": "object",
+ "required": ["affected_boards", "deleted_videos"],
+ "title": "DeleteVideosResult"
+ },
"DenoiseLatentsInvocation": {
"category": "latents",
"class": "invocation",
@@ -22767,6 +24407,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\ndefaults to 16 fps, matching the other Wan video nodes.\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": 16,
+ "description": "Output frame rate.",
+ "field_kind": "input",
+ "input": "any",
+ "maximum": 120,
+ "minimum": 1,
+ "orig_default": 16,
+ "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.0.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",
@@ -28065,6 +29917,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",
@@ -28669,6 +30681,9 @@
{
"$ref": "#/components/schemas/ExpandMaskWithFadeInvocation"
},
+ {
+ "$ref": "#/components/schemas/ExtractVideoRangeInvocation"
+ },
{
"$ref": "#/components/schemas/FLUXLoRACollectionLoader"
},
@@ -29233,6 +31248,48 @@
{
"$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/WanTextEncoderInvocation"
+ },
+ {
+ "$ref": "#/components/schemas/WanVideoDenoiseInvocation"
+ },
{
"$ref": "#/components/schemas/ZImageControlInvocation"
},
@@ -29374,6 +31431,9 @@
{
"$ref": "#/components/schemas/DenoiseMaskOutput"
},
+ {
+ "$ref": "#/components/schemas/ExtractVideoRangeOutput"
+ },
{
"$ref": "#/components/schemas/FaceMaskOutput"
},
@@ -29575,6 +31635,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/ZImageConditioningOutput"
},
@@ -36154,6 +38229,9 @@
{
"$ref": "#/components/schemas/ExpandMaskWithFadeInvocation"
},
+ {
+ "$ref": "#/components/schemas/ExtractVideoRangeInvocation"
+ },
{
"$ref": "#/components/schemas/FLUXLoRACollectionLoader"
},
@@ -36718,6 +38796,48 @@
{
"$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/WanTextEncoderInvocation"
+ },
+ {
+ "$ref": "#/components/schemas/WanVideoDenoiseInvocation"
+ },
{
"$ref": "#/components/schemas/ZImageControlInvocation"
},
@@ -36816,6 +38936,9 @@
{
"$ref": "#/components/schemas/DenoiseMaskOutput"
},
+ {
+ "$ref": "#/components/schemas/ExtractVideoRangeOutput"
+ },
{
"$ref": "#/components/schemas/FaceMaskOutput"
},
@@ -37017,6 +39140,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/ZImageConditioningOutput"
},
@@ -37280,6 +39418,9 @@
{
"$ref": "#/components/schemas/ExpandMaskWithFadeInvocation"
},
+ {
+ "$ref": "#/components/schemas/ExtractVideoRangeInvocation"
+ },
{
"$ref": "#/components/schemas/FLUXLoRACollectionLoader"
},
@@ -37844,6 +39985,48 @@
{
"$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/WanTextEncoderInvocation"
+ },
+ {
+ "$ref": "#/components/schemas/WanVideoDenoiseInvocation"
+ },
{
"$ref": "#/components/schemas/ZImageControlInvocation"
},
@@ -38078,6 +40261,9 @@
"expand_mask_with_fade": {
"$ref": "#/components/schemas/ImageOutput"
},
+ "extract_video_range": {
+ "$ref": "#/components/schemas/ExtractVideoRangeOutput"
+ },
"face_identifier": {
"$ref": "#/components/schemas/ImageOutput"
},
@@ -38651,6 +40837,48 @@
"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_video_denoise": {
+ "$ref": "#/components/schemas/LatentsOutput"
+ },
"z_image_control": {
"$ref": "#/components/schemas/ZImageControlOutput"
},
@@ -38736,6 +40964,7 @@
"dynamic_prompt",
"esrgan",
"expand_mask_with_fade",
+ "extract_video_range",
"face_identifier",
"face_mask_detection",
"face_off",
@@ -38927,6 +41156,20 @@
"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_video_denoise",
"z_image_control",
"z_image_denoise",
"z_image_denoise_meta",
@@ -39170,6 +41413,9 @@
{
"$ref": "#/components/schemas/ExpandMaskWithFadeInvocation"
},
+ {
+ "$ref": "#/components/schemas/ExtractVideoRangeInvocation"
+ },
{
"$ref": "#/components/schemas/FLUXLoRACollectionLoader"
},
@@ -39734,6 +41980,48 @@
{
"$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/WanTextEncoderInvocation"
+ },
+ {
+ "$ref": "#/components/schemas/WanVideoDenoiseInvocation"
+ },
{
"$ref": "#/components/schemas/ZImageControlInvocation"
},
@@ -40054,6 +42342,9 @@
{
"$ref": "#/components/schemas/ExpandMaskWithFadeInvocation"
},
+ {
+ "$ref": "#/components/schemas/ExtractVideoRangeInvocation"
+ },
{
"$ref": "#/components/schemas/FLUXLoRACollectionLoader"
},
@@ -40618,6 +42909,48 @@
{
"$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/WanTextEncoderInvocation"
+ },
+ {
+ "$ref": "#/components/schemas/WanVideoDenoiseInvocation"
+ },
{
"$ref": "#/components/schemas/ZImageControlInvocation"
},
@@ -45879,7 +48212,7 @@
],
"title": "LoRA_LyCORIS_SDXL_Config"
},
- "LoRA_LyCORIS_ZImage_Config": {
+ "LoRA_LyCORIS_Wan_Config": {
"properties": {
"key": {
"type": "string",
@@ -46004,172 +48337,33 @@
},
"base": {
"type": "string",
- "const": "z-image",
+ "const": "wan",
"title": "Base",
- "default": "z-image"
- },
- "variant": {
- "anyOf": [
- {
- "$ref": "#/components/schemas/ZImageVariantType"
- },
- {
- "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",
- "format",
- "base",
- "variant"
- ],
- "title": "LoRA_LyCORIS_ZImage_Config",
- "description": "Model config for Z-Image LoRA models in LyCORIS format."
- },
- "LoRA_OMI_FLUX_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"
- },
- "type": {
- "type": "string",
- "const": "lora",
- "title": "Type",
- "default": "lora"
+ "default": "wan"
},
- "trigger_phrases": {
+ "expert": {
"anyOf": [
{
- "items": {
- "type": "string"
- },
- "type": "array",
- "uniqueItems": true
+ "type": "string",
+ "enum": ["high", "low"]
},
{
"type": "null"
}
],
- "title": "Trigger Phrases",
- "description": "Set of trigger phrases for this model"
+ "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."
},
- "default_settings": {
+ "variant": {
"anyOf": [
{
- "$ref": "#/components/schemas/LoraModelDefaultSettings"
+ "$ref": "#/components/schemas/WanLoRAVariantType"
},
{
"type": "null"
}
],
- "description": "Default settings for this model"
- },
- "format": {
- "type": "string",
- "const": "omi",
- "title": "Format",
- "default": "omi"
- },
- "base": {
- "type": "string",
- "const": "flux",
- "title": "Base",
- "default": "flux"
+ "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."
}
},
"type": "object",
@@ -46189,11 +48383,328 @@
"trigger_phrases",
"default_settings",
"format",
- "base"
+ "base",
+ "expert",
+ "variant"
],
- "title": "LoRA_OMI_FLUX_Config"
+ "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."
},
- "LoRA_OMI_SDXL_Config": {
+ "LoRA_LyCORIS_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"
+ },
+ {
+ "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": {
+ "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": "lycoris",
+ "title": "Format",
+ "default": "lycoris"
+ },
+ "base": {
+ "type": "string",
+ "const": "z-image",
+ "title": "Base",
+ "default": "z-image"
+ },
+ "variant": {
+ "anyOf": [
+ {
+ "$ref": "#/components/schemas/ZImageVariantType"
+ },
+ {
+ "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",
+ "format",
+ "base",
+ "variant"
+ ],
+ "title": "LoRA_LyCORIS_ZImage_Config",
+ "description": "Model config for Z-Image LoRA models in LyCORIS format."
+ },
+ "LoRA_OMI_FLUX_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"
+ },
+ "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": "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_OMI_FLUX_Config"
+ },
+ "LoRA_OMI_SDXL_Config": {
"properties": {
"key": {
"type": "string",
@@ -49942,6 +52453,187 @@
],
"title": "Main_Diffusers_SDXL_Config"
},
+ "Main_Diffusers_Wan_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"
+ },
+ "type": {
+ "type": "string",
+ "const": "main",
+ "title": "Type",
+ "default": "main"
+ },
+ "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/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": "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": {
@@ -50617,6 +53309,183 @@
"title": "Main_GGUF_QwenImage_Config",
"description": "Model config for GGUF-quantized Qwen Image transformer models."
},
+ "Main_GGUF_Wan_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"
+ },
+ "type": {
+ "type": "string",
+ "const": "main",
+ "title": "Type",
+ "default": "main"
+ },
+ "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/MainModelDefaultSettings"
+ },
+ {
+ "type": "null"
+ }
+ ],
+ "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": "wan",
+ "title": "Base",
+ "default": "wan"
+ },
+ "format": {
+ "type": "string",
+ "const": "gguf_quantized",
+ "title": "Format",
+ "default": "gguf_quantized"
+ },
+ "variant": {
+ "$ref": "#/components/schemas/WanVariantType"
+ },
+ "expert": {
+ "type": "string",
+ "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",
+ "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",
+ "expert"
+ ],
+ "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_GGUF_ZImage_Config": {
"properties": {
"key": {
@@ -54297,6 +57166,7 @@
"t5_encoder",
"qwen3_encoder",
"qwen_vl_encoder",
+ "wan_t5_encoder",
"bnb_quantized_int8b",
"bnb_quantized_nf4b",
"gguf_quantized",
@@ -54567,6 +57437,9 @@
{
"$ref": "#/components/schemas/Main_Diffusers_QwenImage_Config"
},
+ {
+ "$ref": "#/components/schemas/Main_Diffusers_Wan_Config"
+ },
{
"$ref": "#/components/schemas/Main_Diffusers_ZImage_Config"
},
@@ -54606,6 +57479,9 @@
{
"$ref": "#/components/schemas/Main_GGUF_QwenImage_Config"
},
+ {
+ "$ref": "#/components/schemas/Main_GGUF_Wan_Config"
+ },
{
"$ref": "#/components/schemas/Main_GGUF_ZImage_Config"
},
@@ -54624,6 +57500,9 @@
{
"$ref": "#/components/schemas/VAE_Checkpoint_Flux2_Config"
},
+ {
+ "$ref": "#/components/schemas/VAE_Checkpoint_Wan_Config"
+ },
{
"$ref": "#/components/schemas/VAE_Checkpoint_QwenImage_Config"
},
@@ -54639,6 +57518,9 @@
{
"$ref": "#/components/schemas/VAE_Diffusers_Flux2_Config"
},
+ {
+ "$ref": "#/components/schemas/VAE_Diffusers_Wan_Config"
+ },
{
"$ref": "#/components/schemas/ControlNet_Checkpoint_SD1_Config"
},
@@ -54687,6 +57569,9 @@
{
"$ref": "#/components/schemas/LoRA_LyCORIS_QwenImage_Config"
},
+ {
+ "$ref": "#/components/schemas/LoRA_LyCORIS_Wan_Config"
+ },
{
"$ref": "#/components/schemas/LoRA_LyCORIS_Anima_Config"
},
@@ -54738,6 +57623,9 @@
{
"$ref": "#/components/schemas/QwenVLEncoder_Checkpoint_Config"
},
+ {
+ "$ref": "#/components/schemas/WanT5Encoder_WanT5Encoder_Config"
+ },
{
"$ref": "#/components/schemas/TI_File_SD1_Config"
},
@@ -55136,6 +58024,9 @@
{
"$ref": "#/components/schemas/Main_Diffusers_QwenImage_Config"
},
+ {
+ "$ref": "#/components/schemas/Main_Diffusers_Wan_Config"
+ },
{
"$ref": "#/components/schemas/Main_Diffusers_ZImage_Config"
},
@@ -55175,6 +58066,9 @@
{
"$ref": "#/components/schemas/Main_GGUF_QwenImage_Config"
},
+ {
+ "$ref": "#/components/schemas/Main_GGUF_Wan_Config"
+ },
{
"$ref": "#/components/schemas/Main_GGUF_ZImage_Config"
},
@@ -55193,6 +58087,9 @@
{
"$ref": "#/components/schemas/VAE_Checkpoint_Flux2_Config"
},
+ {
+ "$ref": "#/components/schemas/VAE_Checkpoint_Wan_Config"
+ },
{
"$ref": "#/components/schemas/VAE_Checkpoint_QwenImage_Config"
},
@@ -55208,6 +58105,9 @@
{
"$ref": "#/components/schemas/VAE_Diffusers_Flux2_Config"
},
+ {
+ "$ref": "#/components/schemas/VAE_Diffusers_Wan_Config"
+ },
{
"$ref": "#/components/schemas/ControlNet_Checkpoint_SD1_Config"
},
@@ -55256,6 +58156,9 @@
{
"$ref": "#/components/schemas/LoRA_LyCORIS_QwenImage_Config"
},
+ {
+ "$ref": "#/components/schemas/LoRA_LyCORIS_Wan_Config"
+ },
{
"$ref": "#/components/schemas/LoRA_LyCORIS_Anima_Config"
},
@@ -55307,6 +58210,9 @@
{
"$ref": "#/components/schemas/QwenVLEncoder_Checkpoint_Config"
},
+ {
+ "$ref": "#/components/schemas/WanT5Encoder_WanT5Encoder_Config"
+ },
{
"$ref": "#/components/schemas/TI_File_SD1_Config"
},
@@ -55591,308 +58497,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_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_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": "Config"
- },
- "submodel_type": {
- "anyOf": [
- {
- "$ref": "#/components/schemas/SubModelType"
- },
- {
- "type": "null"
- }
- ],
- "default": null,
- "description": "The submodel type, if any"
- }
- },
- "required": ["timestamp", "config", "submodel_type"],
- "title": "ModelLoadCompleteEvent",
- "type": "object"
- },
- "ModelLoadStartedEvent": {
- "description": "Event model for model_load_started",
- "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_Wan_Config"
},
{
"$ref": "#/components/schemas/Main_Diffusers_ZImage_Config"
@@ -55933,6 +58538,331 @@
{
"$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_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"
+ },
+ "submodel_type": {
+ "anyOf": [
+ {
+ "$ref": "#/components/schemas/SubModelType"
+ },
+ {
+ "type": "null"
+ }
+ ],
+ "default": null,
+ "description": "The submodel type, if any"
+ }
+ },
+ "required": ["timestamp", "config", "submodel_type"],
+ "title": "ModelLoadCompleteEvent",
+ "type": "object"
+ },
+ "ModelLoadStartedEvent": {
+ "description": "Event model for model_load_started",
+ "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_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_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"
},
@@ -55951,6 +58881,9 @@
{
"$ref": "#/components/schemas/VAE_Checkpoint_Flux2_Config"
},
+ {
+ "$ref": "#/components/schemas/VAE_Checkpoint_Wan_Config"
+ },
{
"$ref": "#/components/schemas/VAE_Checkpoint_QwenImage_Config"
},
@@ -55966,6 +58899,9 @@
{
"$ref": "#/components/schemas/VAE_Diffusers_Flux2_Config"
},
+ {
+ "$ref": "#/components/schemas/VAE_Diffusers_Wan_Config"
+ },
{
"$ref": "#/components/schemas/ControlNet_Checkpoint_SD1_Config"
},
@@ -56014,6 +58950,9 @@
{
"$ref": "#/components/schemas/LoRA_LyCORIS_QwenImage_Config"
},
+ {
+ "$ref": "#/components/schemas/LoRA_LyCORIS_Wan_Config"
+ },
{
"$ref": "#/components/schemas/LoRA_LyCORIS_Anima_Config"
},
@@ -56065,6 +59004,9 @@
{
"$ref": "#/components/schemas/QwenVLEncoder_Checkpoint_Config"
},
+ {
+ "$ref": "#/components/schemas/WanT5Encoder_WanT5Encoder_Config"
+ },
{
"$ref": "#/components/schemas/TI_File_SD1_Config"
},
@@ -56456,6 +59398,12 @@
{
"$ref": "#/components/schemas/QwenImageVariantType"
},
+ {
+ "$ref": "#/components/schemas/WanVariantType"
+ },
+ {
+ "$ref": "#/components/schemas/WanLoRAVariantType"
+ },
{
"$ref": "#/components/schemas/Qwen3VariantType"
},
@@ -56598,6 +59546,7 @@
"t5_encoder",
"qwen3_encoder",
"qwen_vl_encoder",
+ "wan_t5_encoder",
"spandrel_image_to_image",
"siglip",
"flux_redux",
@@ -56647,6 +59596,9 @@
{
"$ref": "#/components/schemas/Main_Diffusers_QwenImage_Config"
},
+ {
+ "$ref": "#/components/schemas/Main_Diffusers_Wan_Config"
+ },
{
"$ref": "#/components/schemas/Main_Diffusers_ZImage_Config"
},
@@ -56686,6 +59638,9 @@
{
"$ref": "#/components/schemas/Main_GGUF_QwenImage_Config"
},
+ {
+ "$ref": "#/components/schemas/Main_GGUF_Wan_Config"
+ },
{
"$ref": "#/components/schemas/Main_GGUF_ZImage_Config"
},
@@ -56704,6 +59659,9 @@
{
"$ref": "#/components/schemas/VAE_Checkpoint_Flux2_Config"
},
+ {
+ "$ref": "#/components/schemas/VAE_Checkpoint_Wan_Config"
+ },
{
"$ref": "#/components/schemas/VAE_Checkpoint_QwenImage_Config"
},
@@ -56719,6 +59677,9 @@
{
"$ref": "#/components/schemas/VAE_Diffusers_Flux2_Config"
},
+ {
+ "$ref": "#/components/schemas/VAE_Diffusers_Wan_Config"
+ },
{
"$ref": "#/components/schemas/ControlNet_Checkpoint_SD1_Config"
},
@@ -56767,6 +59728,9 @@
{
"$ref": "#/components/schemas/LoRA_LyCORIS_QwenImage_Config"
},
+ {
+ "$ref": "#/components/schemas/LoRA_LyCORIS_Wan_Config"
+ },
{
"$ref": "#/components/schemas/LoRA_LyCORIS_Anima_Config"
},
@@ -56818,6 +59782,9 @@
{
"$ref": "#/components/schemas/QwenVLEncoder_Checkpoint_Config"
},
+ {
+ "$ref": "#/components/schemas/WanT5Encoder_WanT5Encoder_Config"
+ },
{
"$ref": "#/components/schemas/TI_File_SD1_Config"
},
@@ -56979,232 +59946,2589 @@
"title": "Node Path",
"description": "The node into which this batch data item will be substituted."
},
- "field_name": {
+ "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"
+ }
+ ],
+ "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",
+ "class": "invocation",
+ "classification": "stable",
+ "description": "Generates latent noise for supported denoiser architectures.",
+ "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"
+ },
+ "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": {
+ "default": 0,
+ "description": "Seed for random number generation",
+ "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",
+ "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": "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",
+ "class": "invocation",
+ "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": [
+ {
+ "$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"
+ },
+ "image": {
+ "anyOf": [
+ {
+ "$ref": "#/components/schemas/ImageField"
+ },
+ {
+ "type": "null"
+ }
+ ],
+ "default": null,
+ "description": "The image to process",
+ "field_kind": "input",
+ "input": "any",
+ "orig_required": true
+ },
+ "type": {
+ "const": "normal_map",
+ "default": "normal_map",
+ "field_kind": "node_attribute",
+ "title": "type",
+ "type": "string"
+ }
+ },
+ "required": ["type", "id"],
+ "tags": ["controlnet", "normal"],
+ "title": "Normal Map",
+ "type": "object",
+ "version": "1.0.0",
+ "output": {
+ "$ref": "#/components/schemas/ImageOutput"
+ }
+ },
+ "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": "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": [
+ {
+ "$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"
+ },
+ "image": {
+ "anyOf": [
+ {
+ "$ref": "#/components/schemas/ImageField"
+ },
+ {
+ "type": "null"
+ }
+ ],
+ "default": null,
+ "description": "The image to use",
+ "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",
+ "field_kind": "input",
+ "input": "any",
+ "minimum": 0,
+ "orig_default": 50,
+ "orig_required": false,
+ "title": "Strength",
+ "type": "number"
+ },
+ "type": {
+ "const": "unsharp_mask_oklab",
+ "default": "unsharp_mask_oklab",
+ "field_kind": "node_attribute",
+ "title": "type",
+ "type": "string"
+ }
+ },
+ "required": ["type", "id"],
+ "tags": ["image", "unsharp_mask", "oklab"],
+ "title": "Unsharp Mask (Oklab)",
+ "type": "object",
+ "version": "1.0.0",
+ "output": {
+ "$ref": "#/components/schemas/ImageOutput"
+ }
+ },
+ "OklchImageHueAdjustmentInvocation": {
+ "category": "image",
+ "class": "invocation",
+ "classification": "stable",
+ "description": "Adjusts the hue of an image in Oklch 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": [
+ {
+ "$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"
+ },
+ "image": {
+ "anyOf": [
+ {
+ "$ref": "#/components/schemas/ImageField"
+ },
+ {
+ "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"
+ },
+ "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": {
+ "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": ["openai"],
+ "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,
+ "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"
+ },
+ {
+ "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"
+ },
+ "init_image": {
+ "anyOf": [
+ {
+ "$ref": "#/components/schemas/ImageField"
+ },
+ {
+ "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
+ },
+ "mask_image": {
+ "anyOf": [
+ {
+ "$ref": "#/components/schemas/ImageField"
+ },
+ {
+ "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": [
+ {
+ "enum": ["low", "high"],
+ "type": "string"
+ },
+ {
+ "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"
+ },
+ "ui_order": {
+ "anyOf": [
+ {
+ "type": "integer"
+ },
+ {
+ "type": "null"
+ }
+ ],
+ "default": null,
+ "title": "Ui Order"
+ },
+ "ui_type": {
+ "anyOf": [
+ {
+ "$ref": "#/components/schemas/UIType"
+ },
+ {
+ "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": {
+ "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"
+ },
+ "image": {
+ "anyOf": [
+ {
+ "$ref": "#/components/schemas/ImageField"
+ },
+ {
+ "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"
+ },
+ "image": {
+ "anyOf": [
+ {
+ "$ref": "#/components/schemas/ImageField"
+ },
+ {
+ "type": "null"
+ }
+ ],
+ "default": null,
+ "description": "The tile image.",
+ "field_kind": "input",
+ "input": "any",
+ "orig_required": true
+ },
+ "tile": {
+ "anyOf": [
+ {
+ "$ref": "#/components/schemas/Tile"
+ },
+ {
+ "type": "null"
+ }
+ ],
+ "default": null,
+ "description": "The tile properties.",
+ "field_kind": "input",
+ "input": "any",
+ "orig_required": true
+ },
+ "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
+ },
+ "type": {
+ "const": "pair_tile_image_output",
+ "default": "pair_tile_image_output",
+ "field_kind": "node_attribute",
+ "title": "type",
+ "type": "string"
+ }
+ },
+ "required": ["output_meta", "tile_with_image", "type", "type"],
+ "title": "PairTileImageOutput",
+ "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
+ },
+ "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"
+ },
+ "source_image": {
+ "anyOf": [
+ {
+ "$ref": "#/components/schemas/ImageField"
+ },
+ {
+ "type": "null"
+ }
+ ],
+ "default": null,
+ "description": "The image to paste",
+ "field_kind": "input",
+ "input": "any",
+ "orig_required": true
+ },
+ "target_image": {
+ "anyOf": [
+ {
+ "$ref": "#/components/schemas/ImageField"
+ },
+ {
+ "type": "null"
+ }
+ ],
+ "default": null,
+ "description": "The image to paste into",
+ "field_kind": "input",
+ "input": "any",
+ "orig_required": true
+ },
+ "bounding_box": {
+ "anyOf": [
+ {
+ "$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"
+ }
+ ],
+ "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"
+ },
+ "image": {
+ "anyOf": [
+ {
+ "$ref": "#/components/schemas/ImageField"
+ },
+ {
+ "type": "null"
+ }
+ ],
+ "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",
+ "version": "1.0.0",
+ "output": {
+ "$ref": "#/components/schemas/ImageOutput"
+ }
+ },
+ "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": {
+ "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 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"
+ },
+ "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",
+ "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": ["type", "id"],
+ "tags": ["prompt", "template", "style", "preset"],
+ "title": "Prompt Template",
+ "type": "object",
+ "version": "1.0.0",
+ "output": {
+ "$ref": "#/components/schemas/PromptTemplateOutput"
+ }
+ },
+ "PromptTemplateOutput": {
+ "class": "output",
+ "description": "Output for the Prompt Template node",
+ "properties": {
+ "positive_prompt": {
+ "description": "The positive prompt with the template applied",
+ "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",
+ "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": [
+ {
+ "type": "string"
+ },
+ {
+ "type": "null"
+ }
+ ],
+ "default": null,
+ "description": "Path to prompt text file",
+ "field_kind": "input",
+ "input": "any",
+ "orig_required": true,
+ "title": "File Path"
+ },
+ "pre_prompt": {
+ "anyOf": [
+ {
+ "type": "string"
+ },
+ {
+ "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": [
+ {
+ "type": "string"
+ },
+ {
+ "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"
+ }
+ },
+ "required": ["timestamp", "queue_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", "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"
+ },
+ "error_message": {
+ "anyOf": [
+ {
+ "type": "string"
+ },
+ {
+ "type": "null"
+ }
+ ],
+ "default": null,
+ "description": "The error message, if any",
+ "title": "Error Message"
+ },
+ "error_traceback": {
+ "anyOf": [
+ {
+ "type": "string"
+ },
+ {
+ "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": [
+ {
+ "type": "string"
+ },
+ {
+ "type": "null"
+ }
+ ],
+ "default": null,
+ "description": "The timestamp when the queue item was started",
+ "title": "Started At"
+ },
+ "completed_at": {
+ "anyOf": [
+ {
+ "type": "string"
+ },
+ {
+ "type": "null"
+ }
+ ],
+ "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"
+ },
+ "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"
+ },
+ "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"
+ }
+ },
+ "required": ["timestamp", "queue_id", "retried_item_ids"],
+ "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)."
+ },
+ "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": "qwen3_encoder",
+ "title": "Type",
+ "default": "qwen3_encoder"
+ },
+ "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"
+ },
+ "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": [
+ {
+ "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": "qwen3_encoder",
+ "title": "Type",
+ "default": "qwen3_encoder"
+ },
+ "format": {
+ "type": "string",
+ "const": "gguf_quantized",
+ "title": "Format",
+ "default": "gguf_quantized"
+ },
+ "cpu_only": {
+ "anyOf": [
+ {
+ "type": "boolean"
+ },
+ {
+ "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": [
+ {
+ "type": "string"
+ },
+ {
+ "type": "null"
+ }
+ ],
+ "title": "Description",
+ "description": "Model description"
+ },
+ "source": {
"type": "string",
- "title": "Field Name",
- "description": "The field into which this batch data item will be substituted."
+ "title": "Source",
+ "description": "The original source of the model (path, URL or repo_id)."
},
- "value": {
+ "source_type": {
+ "$ref": "#/components/schemas/ModelSourceType",
+ "description": "The type of source"
+ },
+ "source_api_response": {
"anyOf": [
{
"type": "string"
},
{
- "type": "number"
+ "type": "null"
+ }
+ ],
+ "title": "Source Api Response",
+ "description": "The original API response from the source, as stringified JSON."
+ },
+ "source_url": {
+ "anyOf": [
+ {
+ "type": "string"
},
{
- "type": "integer"
+ "type": "null"
+ }
+ ],
+ "title": "Source Url",
+ "description": "Optional URL for the model (e.g. download page or model page)."
+ },
+ "cover_image": {
+ "anyOf": [
+ {
+ "type": "string"
},
{
- "$ref": "#/components/schemas/ImageField"
+ "type": "null"
}
],
- "title": "Value",
- "description": "The value to substitute into the node/field."
- }
- },
- "type": "object",
- "required": ["node_path", "field_name", "value"],
- "title": "NodeFieldValue"
- },
- "NodePackInfo": {
- "properties": {
- "name": {
+ "title": "Cover Image",
+ "description": "Url for image to preview model"
+ },
+ "base": {
"type": "string",
- "title": "Name",
- "description": "The name of the node pack."
+ "const": "any",
+ "title": "Base",
+ "default": "any"
},
- "path": {
+ "type": {
"type": "string",
- "title": "Path",
- "description": "The path to the node pack directory."
+ "const": "qwen3_encoder",
+ "title": "Type",
+ "default": "qwen3_encoder"
},
- "node_count": {
- "type": "integer",
- "title": "Node Count",
- "description": "The number of nodes in the pack."
+ "format": {
+ "type": "string",
+ "const": "qwen3_encoder",
+ "title": "Format",
+ "default": "qwen3_encoder"
},
- "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."
+ "cpu_only": {
+ "anyOf": [
+ {
+ "type": "boolean"
+ },
+ {
+ "type": "null"
+ }
+ ],
+ "title": "Cpu Only",
+ "description": "Whether this model should run on CPU only"
},
- "custom_nodes_path": {
- "type": "string",
- "title": "Custom Nodes Path",
- "description": "The configured custom nodes directory path."
+ "variant": {
+ "$ref": "#/components/schemas/Qwen3VariantType",
+ "description": "Qwen3 model size variant (4B or 8B)"
}
},
"type": "object",
- "required": ["node_packs", "custom_nodes_path"],
- "title": "NodePackListResponse",
- "description": "Response for listing installed node packs."
+ "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."
},
- "NoiseInvocation": {
- "category": "latents",
- "class": "invocation",
- "classification": "stable",
- "description": "Generates latent noise for supported denoiser architectures.",
- "node_pack": "invokeai",
+ "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": {
- "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"
- },
- "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": {
- "default": 0,
- "description": "Seed for random number generation",
- "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",
- "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",
+ "conditioning_name": {
+ "description": "The name of conditioning tensor",
+ "title": "Conditioning Name",
"type": "string"
}
},
- "required": ["type", "id"],
- "tags": ["latents", "noise"],
- "title": "Create Latent Noise",
- "type": "object",
- "version": "1.1.0",
- "output": {
- "$ref": "#/components/schemas/NoiseOutput"
- }
+ "required": ["conditioning_name"],
+ "title": "QwenImageConditioningField",
+ "type": "object"
},
- "NoiseOutput": {
+ "QwenImageConditioningOutput": {
"class": "output",
- "description": "Invocation noise output",
+ "description": "Base class for nodes that output a Qwen Image Edit conditioning tensor.",
"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)",
+ "conditioning": {
+ "$ref": "#/components/schemas/QwenImageConditioningField",
+ "description": "Conditioning tensor",
"field_kind": "output",
- "title": "Height",
- "type": "integer",
"ui_hidden": false
},
"type": {
- "const": "noise_output",
- "default": "noise_output",
+ "const": "qwen_image_conditioning_output",
+ "default": "qwen_image_conditioning_output",
"field_kind": "node_attribute",
"title": "type",
"type": "string"
}
},
- "required": ["output_meta", "noise", "width", "height", "type", "type"],
- "title": "NoiseOutput",
+ "required": ["output_meta", "conditioning", "type", "type"],
+ "title": "QwenImageConditioningOutput",
"type": "object"
},
- "NormalMapInvocation": {
- "category": "controlnet_preprocessors",
+ "QwenImageDenoiseInvocation": {
+ "category": "image",
"class": "invocation",
- "classification": "stable",
- "description": "Generates a normal map.",
+ "classification": "prototype",
+ "description": "Run the denoising process with a Qwen Image model.",
"node_pack": "invokeai",
"properties": {
"board": {
@@ -57263,103 +62587,227 @@
"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": "connection",
+ "orig_default": null,
+ "orig_required": false
+ },
+ "reference_latents": {
+ "anyOf": [
+ {
+ "$ref": "#/components/schemas/LatentsField"
+ },
+ {
+ "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/DenoiseMaskField"
+ },
+ {
+ "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/TransformerField"
+ },
+ {
+ "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/QwenImageConditioningField"
+ },
+ {
+ "type": "null"
+ }
+ ],
+ "default": null,
+ "description": "Positive conditioning tensor",
+ "field_kind": "input",
+ "input": "connection",
"orig_required": true
},
- "type": {
- "const": "normal_map",
- "default": "normal_map",
- "field_kind": "node_attribute",
- "title": "type",
- "type": "string"
- }
- },
- "required": ["type", "id"],
- "tags": ["controlnet", "normal"],
- "title": "Normal Map",
- "type": "object",
- "version": "1.0.0",
- "output": {
- "$ref": "#/components/schemas/ImageOutput"
- }
- },
- "OffsetPaginatedResults_BoardDTO_": {
- "properties": {
- "limit": {
- "type": "integer",
- "title": "Limit",
- "description": "Limit of items to get"
+ "negative_conditioning": {
+ "anyOf": [
+ {
+ "$ref": "#/components/schemas/QwenImageConditioningField"
+ },
+ {
+ "type": "null"
+ }
+ ],
+ "default": null,
+ "description": "Negative conditioning tensor",
+ "field_kind": "input",
+ "input": "connection",
+ "orig_default": null,
+ "orig_required": false
},
- "offset": {
- "type": "integer",
- "title": "Offset",
- "description": "Offset from which to retrieve items"
+ "cfg_scale": {
+ "anyOf": [
+ {
+ "type": "number"
+ },
+ {
+ "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"
},
- "total": {
- "type": "integer",
- "title": "Total",
- "description": "Total number of items in result"
+ "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"
},
- "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"
+ "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"
},
- "offset": {
- "type": "integer",
- "title": "Offset",
- "description": "Offset from which to retrieve items"
+ "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"
},
- "total": {
- "type": "integer",
- "title": "Total",
- "description": "Total number of items in result"
+ "shift": {
+ "anyOf": [
+ {
+ "type": "number"
+ },
+ {
+ "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"
},
- "items": {
- "items": {
- "$ref": "#/components/schemas/ImageDTO"
- },
- "type": "array",
- "title": "Items",
- "description": "Items"
+ "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",
- "required": ["limit", "offset", "total", "items"],
- "title": "OffsetPaginatedResults[ImageDTO]"
+ "version": "1.0.0",
+ "output": {
+ "$ref": "#/components/schemas/LatentsOutput"
+ }
},
- "OklabUnsharpMaskInvocation": {
+ "QwenImageImageToLatentsInvocation": {
"category": "image",
"class": "invocation",
- "classification": "stable",
- "description": "Applies an unsharp mask filter to an image in the Oklab color space",
+ "classification": "prototype",
+ "description": "Generates latents from an image using the Qwen Image VAE.",
"node_pack": "invokeai",
"properties": {
"board": {
@@ -57428,55 +62876,82 @@
}
],
"default": null,
- "description": "The image to use",
+ "description": "The image to encode.",
"field_kind": "input",
"input": "any",
"orig_required": true
},
- "radius": {
- "default": 2,
- "description": "Unsharp mask radius",
- "exclusiveMinimum": 0,
+ "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": 2,
+ "orig_default": null,
"orig_required": false,
- "title": "Radius",
- "type": "number"
+ "title": "Width"
},
- "strength": {
- "default": 50,
- "description": "Unsharp mask strength",
+ "height": {
+ "anyOf": [
+ {
+ "type": "integer"
+ },
+ {
+ "type": "null"
+ }
+ ],
+ "default": null,
+ "description": "Resize the image to this height before encoding. If not set, encodes at the image's original size.",
"field_kind": "input",
"input": "any",
- "minimum": 0,
- "orig_default": 50,
+ "orig_default": null,
"orig_required": false,
- "title": "Strength",
- "type": "number"
+ "title": "Height"
},
"type": {
- "const": "unsharp_mask_oklab",
- "default": "unsharp_mask_oklab",
+ "const": "qwen_image_i2l",
+ "default": "qwen_image_i2l",
"field_kind": "node_attribute",
"title": "type",
"type": "string"
}
},
"required": ["type", "id"],
- "tags": ["image", "unsharp_mask", "oklab"],
- "title": "Unsharp Mask (Oklab)",
+ "tags": ["image", "latents", "vae", "i2l", "qwen_image"],
+ "title": "Image to Latents - Qwen Image",
"type": "object",
"version": "1.0.0",
"output": {
- "$ref": "#/components/schemas/ImageOutput"
+ "$ref": "#/components/schemas/LatentsOutput"
}
},
- "OklchImageHueAdjustmentInvocation": {
- "category": "image",
+ "QwenImageLatentsToImageInvocation": {
+ "category": "latents",
"class": "invocation",
- "classification": "stable",
- "description": "Adjusts the hue of an image in Oklch space.",
+ "classification": "prototype",
+ "description": "Generates an image from latents using the Qwen Image VAE.",
"node_pack": "invokeai",
"properties": {
"board": {
@@ -57535,87 +63010,148 @@
"title": "Use Cache",
"type": "boolean"
},
- "image": {
+ "latents": {
"anyOf": [
{
- "$ref": "#/components/schemas/ImageField"
+ "$ref": "#/components/schemas/LatentsField"
},
{
"type": "null"
}
],
"default": null,
- "description": "The image to adjust",
+ "description": "Latents tensor",
"field_kind": "input",
- "input": "any",
+ "input": "connection",
"orig_required": true
},
- "hue": {
- "default": 0,
- "description": "The degrees by which to rotate the hue, 0-360",
+ "vae": {
+ "anyOf": [
+ {
+ "$ref": "#/components/schemas/VAEField"
+ },
+ {
+ "type": "null"
+ }
+ ],
+ "default": null,
+ "description": "VAE",
"field_kind": "input",
- "input": "any",
- "orig_default": 0,
- "orig_required": false,
- "title": "Hue",
- "type": "integer"
+ "input": "connection",
+ "orig_required": true
},
"type": {
- "const": "img_hue_adjust_oklch",
- "default": "img_hue_adjust_oklch",
+ "const": "qwen_image_l2i",
+ "default": "qwen_image_l2i",
"field_kind": "node_attribute",
"title": "type",
"type": "string"
}
},
"required": ["type", "id"],
- "tags": ["image", "hue", "oklch"],
- "title": "Adjust Image Hue (Oklch)",
+ "tags": ["latents", "image", "vae", "l2i", "qwen_image"],
+ "title": "Latents to Image - Qwen Image",
"type": "object",
"version": "1.0.0",
"output": {
"$ref": "#/components/schemas/ImageOutput"
}
},
- "OpenAIImageGenerationInvocation": {
- "category": "image",
+ "QwenImageLoRACollectionLoader": {
+ "category": "model",
"class": "invocation",
- "classification": "stable",
- "description": "Generate images using an OpenAI-hosted external model.",
+ "classification": "prototype",
+ "description": "Applies a collection of LoRAs to a Qwen Image transformer.",
"node_pack": "invokeai",
"properties": {
- "board": {
+ "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/BoardField"
+ "$ref": "#/components/schemas/LoRAField"
+ },
+ {
+ "items": {
+ "$ref": "#/components/schemas/LoRAField"
+ },
+ "type": "array"
},
{
"type": "null"
}
],
"default": null,
- "description": "The board to save the image to",
- "field_kind": "internal",
- "input": "direct",
+ "description": "LoRA models and weights. May be a single LoRA or collection.",
+ "field_kind": "input",
+ "input": "any",
+ "orig_default": null,
"orig_required": false,
- "ui_hidden": false
+ "title": "LoRAs"
},
- "metadata": {
+ "transformer": {
"anyOf": [
{
- "$ref": "#/components/schemas/MetadataField"
+ "$ref": "#/components/schemas/TransformerField"
},
{
"type": "null"
}
],
"default": null,
- "description": "Optional metadata to be saved with the image",
- "field_kind": "internal",
+ "description": "Transformer",
+ "field_kind": "input",
"input": "connection",
+ "orig_default": null,
"orig_required": false,
- "ui_hidden": false
+ "title": "Transformer"
},
+ "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.0",
+ "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",
@@ -57640,7 +63176,7 @@
"title": "Use Cache",
"type": "boolean"
},
- "model": {
+ "lora": {
"anyOf": [
{
"$ref": "#/components/schemas/ModelIdentifierField"
@@ -57650,147 +63186,292 @@
}
],
"default": null,
- "description": "Main model (UNet, VAE, CLIP) to load",
+ "description": "LoRA model 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": "LoRA",
+ "ui_model_base": ["qwen-image"],
+ "ui_model_type": ["lora"]
},
- "mode": {
- "default": "txt2img",
- "description": "Generation mode.",
- "enum": ["txt2img", "img2img", "inpaint"],
+ "weight": {
+ "default": 1.0,
+ "description": "The weight at which the LoRA is applied to each model",
"field_kind": "input",
"input": "any",
- "orig_default": "txt2img",
+ "orig_default": 1.0,
"orig_required": false,
- "title": "Mode",
- "type": "string",
- "ui_hidden": true
+ "title": "Weight",
+ "type": "number"
},
- "prompt": {
+ "transformer": {
"anyOf": [
{
- "type": "string"
+ "$ref": "#/components/schemas/TransformerField"
},
{
"type": "null"
}
],
"default": null,
- "description": "Prompt",
+ "description": "Transformer",
"field_kind": "input",
- "input": "any",
- "orig_required": true,
- "title": "Prompt"
+ "input": "connection",
+ "orig_default": null,
+ "orig_required": false,
+ "title": "Transformer"
},
- "seed": {
+ "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": [
{
- "type": "integer"
+ "$ref": "#/components/schemas/TransformerField"
},
{
"type": "null"
}
],
"default": null,
- "description": "Seed for random number generation",
- "field_kind": "input",
- "input": "any",
- "orig_default": null,
- "orig_required": false,
- "title": "Seed"
+ "description": "Transformer",
+ "field_kind": "output",
+ "title": "Transformer",
+ "ui_hidden": false
},
- "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"
+ "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"
},
- "width": {
- "default": 1024,
- "description": "Width of output (px)",
- "exclusiveMinimum": 0,
+ "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": "any",
- "orig_default": 1024,
- "orig_required": false,
- "title": "Width",
- "type": "integer"
+ "input": "direct",
+ "orig_required": true,
+ "title": "Transformer",
+ "ui_model_base": ["qwen-image"],
+ "ui_model_type": ["main"]
},
- "height": {
- "default": 1024,
- "description": "Height of output (px)",
- "exclusiveMinimum": 0,
+ "vae_model": {
+ "anyOf": [
+ {
+ "$ref": "#/components/schemas/ModelIdentifierField"
+ },
+ {
+ "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).",
"field_kind": "input",
- "input": "any",
- "orig_default": 1024,
+ "input": "direct",
+ "orig_default": null,
"orig_required": false,
- "title": "Height",
- "type": "integer"
+ "title": "VAE",
+ "ui_model_base": ["qwen-image"],
+ "ui_model_type": ["vae"]
},
- "image_size": {
+ "qwen_vl_encoder_model": {
"anyOf": [
{
- "type": "string"
+ "$ref": "#/components/schemas/ModelIdentifierField"
},
{
"type": "null"
}
],
"default": null,
- "description": "Image size preset (e.g. 1K, 2K, 4K)",
+ "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": "any",
+ "input": "direct",
"orig_default": null,
"orig_required": false,
- "title": "Image Size"
+ "title": "Qwen VL Encoder",
+ "ui_model_type": ["qwen_vl_encoder"]
},
- "init_image": {
+ "component_source": {
"anyOf": [
{
- "$ref": "#/components/schemas/ImageField"
+ "$ref": "#/components/schemas/ModelIdentifierField"
},
{
"type": "null"
}
],
"default": null,
- "description": "Init image (use reference_images instead)",
+ "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": "any",
+ "input": "direct",
"orig_default": null,
"orig_required": false,
- "ui_hidden": true
+ "title": "Component Source (Diffusers)",
+ "ui_model_base": ["qwen-image"],
+ "ui_model_format": ["diffusers"],
+ "ui_model_type": ["main"]
+ },
+ "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"
},
- "mask_image": {
+ "use_cache": {
+ "default": true,
+ "description": "Whether or not to use the cache",
+ "field_kind": "node_attribute",
+ "title": "Use Cache",
+ "type": "boolean"
+ },
+ "prompt": {
"anyOf": [
{
- "$ref": "#/components/schemas/ImageField"
+ "type": "string"
},
{
"type": "null"
}
],
"default": null,
- "description": "Mask image for inpaint",
+ "description": "Text prompt describing the desired edit.",
"field_kind": "input",
"input": "any",
- "orig_default": null,
- "orig_required": false,
- "ui_hidden": true
+ "orig_required": true,
+ "title": "Prompt",
+ "ui_component": "textarea"
},
"reference_images": {
"default": [],
- "description": "Reference images",
+ "description": "Reference images to guide the edit. The model can use multiple reference images.",
"field_kind": "input",
"input": "any",
"items": {
@@ -57801,172 +63482,338 @@
"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": {
+ "qwen_vl_encoder": {
"anyOf": [
{
- "enum": ["low", "high"],
- "type": "string"
+ "$ref": "#/components/schemas/QwenVLEncoderField"
},
{
"type": "null"
}
],
"default": null,
- "description": "Fidelity to source images (edits only)",
+ "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": null,
+ "orig_default": "none",
"orig_required": false,
- "title": "Input Fidelity"
+ "title": "Quantization",
+ "type": "string"
},
"type": {
- "const": "openai_image_generation",
- "default": "openai_image_generation",
+ "const": "qwen_image_text_encoder",
+ "default": "qwen_image_text_encoder",
"field_kind": "node_attribute",
"title": "type",
"type": "string"
}
},
"required": ["type", "id"],
- "tags": ["external", "generation", "openai"],
- "title": "OpenAI Image Generation",
+ "tags": ["prompt", "conditioning", "qwen_image"],
+ "title": "Prompt - Qwen Image",
"type": "object",
- "version": "1.0.0",
+ "version": "1.2.0",
"output": {
- "$ref": "#/components/schemas/ImageCollectionOutput"
+ "$ref": "#/components/schemas/QwenImageConditioningOutput"
}
},
- "OrphanedModelInfo": {
+ "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": "Relative path to the orphaned directory from models root"
+ "description": "Path to the model on the filesystem. Relative paths are relative to the Invoke root directory."
},
- "absolute_path": {
+ "file_size": {
+ "type": "integer",
+ "title": "File Size",
+ "description": "The size of the model in bytes."
+ },
+ "name": {
"type": "string",
- "title": "Absolute Path",
- "description": "Absolute path to the orphaned directory"
+ "title": "Name",
+ "description": "Name of the model."
},
- "files": {
- "items": {
- "type": "string"
- },
- "type": "array",
- "title": "Files",
- "description": "List of model files in this directory"
+ "description": {
+ "anyOf": [
+ {
+ "type": "string"
+ },
+ {
+ "type": "null"
+ }
+ ],
+ "title": "Description",
+ "description": "Model description"
},
- "size_bytes": {
- "type": "integer",
- "title": "Size Bytes",
- "description": "Total size of all files in bytes"
+ "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": ["path", "absolute_path", "files", "size_bytes"],
- "title": "OrphanedModelInfo",
- "description": "Information about an orphaned model directory."
+ "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."
},
- "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.",
+ "QwenVLEncoder_Diffusers_Config": {
"properties": {
- "field_kind": {
- "$ref": "#/components/schemas/FieldKind"
+ "key": {
+ "type": "string",
+ "title": "Key",
+ "description": "A unique key for this model."
},
- "ui_hidden": {
- "default": false,
- "title": "Ui Hidden",
- "type": "boolean"
+ "hash": {
+ "type": "string",
+ "title": "Hash",
+ "description": "The hash of the model file(s)."
},
- "ui_order": {
+ "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"
+ "type": "string"
},
{
"type": "null"
}
],
- "default": null,
- "title": "Ui Order"
+ "title": "Description",
+ "description": "Model description"
},
- "ui_type": {
+ "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/UIType"
+ "type": "string"
},
{
"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": "Source Api Response",
+ "description": "The original API response from the source, as stringified JSON."
+ },
+ "source_url": {
"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 Url",
+ "description": "Optional URL for the model (e.g. download page or model page)."
},
- "metadata": {
+ "cover_image": {
"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": "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": "Outputs a single random float",
+ "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",
@@ -57985,141 +63832,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"
},
- "image": {
- "anyOf": [
- {
- "$ref": "#/components/schemas/ImageField"
- },
- {
- "type": "null"
- }
- ],
- "default": null,
- "description": "Input image",
+ "low": {
+ "default": 0.0,
+ "description": "The inclusive low value",
"field_kind": "input",
"input": "any",
- "orig_required": true
+ "orig_default": 0.0,
+ "orig_required": false,
+ "title": "Low",
+ "type": "number"
},
- "tile_size": {
- "default": 512,
- "description": "Tile size",
+ "high": {
+ "default": 1.0,
+ "description": "The exclusive high value",
"field_kind": "input",
"input": "any",
- "orig_default": 512,
+ "orig_default": 1.0,
"orig_required": false,
- "title": "Tile Size",
- "type": "integer"
+ "title": "High",
+ "type": "number"
},
- "border_mode": {
- "default": "none",
- "description": "Border mode to apply to eliminate any artifacts or seams",
- "enum": ["none", "seamless", "mirror", "replicate"],
+ "decimals": {
+ "default": 2,
+ "description": "The number of decimal places to round to",
"field_kind": "input",
"input": "any",
- "orig_default": "none",
+ "orig_default": 2,
"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
+ "title": "Decimals",
+ "type": "integer"
},
"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"
+ "const": "rand_float",
+ "default": "rand_float",
+ "field_kind": "node_attribute",
+ "title": "type",
+ "type": "string"
}
},
+ "required": ["type", "id"],
+ "tags": ["math", "float", "random"],
+ "title": "Random Float",
"type": "object",
- "required": ["page", "pages", "per_page", "total", "items"],
- "title": "PaginatedResults[WorkflowRecordListItemWithThumbnailDTO]"
+ "version": "1.0.1",
+ "output": {
+ "$ref": "#/components/schemas/FloatOutput"
+ }
},
- "PairTileImageInvocation": {
- "category": "tiles",
+ "RandomIntInvocation": {
+ "category": "math",
"class": "invocation",
"classification": "stable",
- "description": "Pair an image with its tile properties.",
+ "description": "Outputs a single random integer.",
"node_pack": "invokeai",
"properties": {
"id": {
@@ -58140,119 +63910,56 @@
"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"
},
- "image": {
- "anyOf": [
- {
- "$ref": "#/components/schemas/ImageField"
- },
- {
- "type": "null"
- }
- ],
- "default": null,
- "description": "The tile image.",
+ "low": {
+ "default": 0,
+ "description": "The inclusive low value",
"field_kind": "input",
"input": "any",
- "orig_required": true
+ "orig_default": 0,
+ "orig_required": false,
+ "title": "Low",
+ "type": "integer"
},
- "tile": {
- "anyOf": [
- {
- "$ref": "#/components/schemas/Tile"
- },
- {
- "type": "null"
- }
- ],
- "default": null,
- "description": "The tile properties.",
+ "high": {
+ "default": 2147483647,
+ "description": "The exclusive high value",
"field_kind": "input",
"input": "any",
- "orig_required": true
+ "orig_default": 2147483647,
+ "orig_required": false,
+ "title": "High",
+ "type": "integer"
},
"type": {
- "const": "pair_tile_image",
- "default": "pair_tile_image",
+ "const": "rand_int",
+ "default": "rand_int",
"field_kind": "node_attribute",
"title": "type",
"type": "string"
}
},
"required": ["type", "id"],
- "tags": ["tiles"],
- "title": "Pair Tile with Image",
+ "tags": ["math", "random"],
+ "title": "Random Integer",
"type": "object",
"version": "1.0.1",
"output": {
- "$ref": "#/components/schemas/PairTileImageOutput"
+ "$ref": "#/components/schemas/IntegerOutput"
}
},
- "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
- },
- "type": {
- "const": "pair_tile_image_output",
- "default": "pair_tile_image_output",
- "field_kind": "node_attribute",
- "title": "type",
- "type": "string"
- }
- },
- "required": ["output_meta", "tile_with_image", "type", "type"],
- "title": "PairTileImageOutput",
- "type": "object"
- },
- "PasteImageIntoBoundingBoxInvocation": {
- "category": "image",
+ "RandomRangeInvocation": {
+ "category": "batch",
"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.",
+ "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",
- "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",
@@ -58271,113 +63978,78 @@
"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"
},
- "source_image": {
- "anyOf": [
- {
- "$ref": "#/components/schemas/ImageField"
- },
- {
- "type": "null"
- }
- ],
- "default": null,
- "description": "The image to paste",
+ "low": {
+ "default": 0,
+ "description": "The inclusive low value",
"field_kind": "input",
"input": "any",
- "orig_required": true
+ "orig_default": 0,
+ "orig_required": false,
+ "title": "Low",
+ "type": "integer"
},
- "target_image": {
- "anyOf": [
- {
- "$ref": "#/components/schemas/ImageField"
- },
- {
- "type": "null"
- }
- ],
- "default": null,
- "description": "The image to paste into",
+ "high": {
+ "default": 2147483647,
+ "description": "The exclusive high value",
"field_kind": "input",
"input": "any",
- "orig_required": true
+ "orig_default": 2147483647,
+ "orig_required": false,
+ "title": "High",
+ "type": "integer"
},
- "bounding_box": {
- "anyOf": [
- {
- "$ref": "#/components/schemas/BoundingBoxField"
- },
- {
- "type": "null"
- }
- ],
- "default": null,
- "description": "The bounding box to paste the image into",
+ "size": {
+ "default": 1,
+ "description": "The number of values to generate",
"field_kind": "input",
"input": "any",
- "orig_required": true
+ "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,
+ "title": "Seed",
+ "type": "integer"
},
"type": {
- "const": "paste_image_into_bounding_box",
- "default": "paste_image_into_bounding_box",
+ "const": "random_range",
+ "default": "random_range",
"field_kind": "node_attribute",
"title": "type",
"type": "string"
}
},
"required": ["type", "id"],
- "tags": ["image", "crop"],
- "title": "Paste Image into Bounding Box",
+ "tags": ["range", "integer", "random", "collection"],
+ "title": "Random Range",
"type": "object",
- "version": "1.0.0",
+ "version": "1.0.1",
"output": {
- "$ref": "#/components/schemas/ImageOutput"
+ "$ref": "#/components/schemas/IntegerCollectionOutput"
}
},
- "PiDiNetEdgeDetectionInvocation": {
- "category": "controlnet_preprocessors",
+ "RangeInvocation": {
+ "category": "batch",
"class": "invocation",
"classification": "stable",
- "description": "Generates an edge map using PiDiNet.",
+ "description": "Creates a range of numbers from start to stop with 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",
@@ -58402,111 +64074,58 @@
"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"
},
- "quantize_edges": {
- "default": false,
- "description": "Whether or not to use safe mode",
+ "stop": {
+ "default": 10,
+ "description": "The stop of the range",
"field_kind": "input",
"input": "any",
- "orig_default": false,
+ "orig_default": 10,
"orig_required": false,
- "title": "Quantize Edges",
- "type": "boolean"
+ "title": "Stop",
+ "type": "integer"
},
- "scribble": {
- "default": false,
- "description": "Whether or not to use scribble mode",
+ "step": {
+ "default": 1,
+ "description": "The step of the range",
"field_kind": "input",
"input": "any",
- "orig_default": false,
+ "orig_default": 1,
"orig_required": false,
- "title": "Scribble",
- "type": "boolean"
+ "title": "Step",
+ "type": "integer"
},
"type": {
- "const": "pidi_edge_detection",
- "default": "pidi_edge_detection",
+ "const": "range",
+ "default": "range",
"field_kind": "node_attribute",
"title": "type",
"type": "string"
}
},
"required": ["type", "id"],
- "tags": ["controlnet", "edge"],
- "title": "PiDiNet Edge Detection",
+ "tags": ["collection", "integer", "range"],
+ "title": "Integer Range",
"type": "object",
"version": "1.0.0",
"output": {
- "$ref": "#/components/schemas/ImageOutput"
+ "$ref": "#/components/schemas/IntegerCollectionOutput"
}
},
- "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": {
- "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",
+ "RangeOfSizeInvocation": {
+ "category": "batch",
"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": "Creates a range from start to start + (size * step) incremented by step",
"node_pack": "invokeai",
"properties": {
"id": {
@@ -58533,122 +64152,117 @@
"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",
+ "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"
},
- "positive_prompt": {
- "default": "",
- "description": "The positive prompt to insert into the template's {prompt} placeholder",
+ "size": {
+ "default": 1,
+ "description": "The number of values",
+ "exclusiveMinimum": 0,
"field_kind": "input",
"input": "any",
- "orig_default": "",
+ "orig_default": 1,
"orig_required": false,
- "title": "Positive Prompt",
- "type": "string",
- "ui_component": "textarea"
+ "title": "Size",
+ "type": "integer"
},
- "negative_prompt": {
- "default": "",
- "description": "The negative prompt to insert into the template's {prompt} placeholder",
+ "step": {
+ "default": 1,
+ "description": "The step of the range",
"field_kind": "input",
"input": "any",
- "orig_default": "",
+ "orig_default": 1,
"orig_required": false,
- "title": "Negative Prompt",
- "type": "string",
- "ui_component": "textarea"
+ "title": "Step",
+ "type": "integer"
},
"type": {
- "const": "prompt_template",
- "default": "prompt_template",
+ "const": "range_of_size",
+ "default": "range_of_size",
"field_kind": "node_attribute",
"title": "type",
"type": "string"
}
},
"required": ["type", "id"],
- "tags": ["prompt", "template", "style", "preset"],
- "title": "Prompt Template",
+ "tags": ["collection", "integer", "size", "range"],
+ "title": "Integer Range of Size",
"type": "object",
"version": "1.0.0",
"output": {
- "$ref": "#/components/schemas/PromptTemplateOutput"
+ "$ref": "#/components/schemas/IntegerCollectionOutput"
}
},
- "PromptTemplateOutput": {
- "class": "output",
- "description": "Output for the Prompt Template node",
+ "RecallParameter": {
"properties": {
"positive_prompt": {
- "description": "The positive prompt with the template applied",
- "field_kind": "output",
+ "anyOf": [
+ {
+ "type": "string"
+ },
+ {
+ "type": "null"
+ }
+ ],
"title": "Positive Prompt",
- "type": "string",
- "ui_hidden": false
+ "description": "Positive prompt text"
},
"negative_prompt": {
- "description": "The negative prompt with the template applied",
- "field_kind": "output",
+ "anyOf": [
+ {
+ "type": "string"
+ },
+ {
+ "type": "null"
+ }
+ ],
"title": "Negative Prompt",
- "type": "string",
- "ui_hidden": false
+ "description": "Negative prompt text"
},
- "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"
+ "model": {
+ "anyOf": [
+ {
+ "type": "string"
+ },
+ {
+ "type": "null"
+ }
+ ],
+ "title": "Model",
+ "description": "Main 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"
+ "refiner_model": {
+ "anyOf": [
+ {
+ "type": "string"
+ },
+ {
+ "type": "null"
+ }
+ ],
+ "title": "Refiner Model",
+ "description": "Refiner model name/identifier"
},
- "use_cache": {
- "default": true,
- "description": "Whether or not to use the cache",
- "field_kind": "node_attribute",
- "title": "Use Cache",
- "type": "boolean"
+ "vae_model": {
+ "anyOf": [
+ {
+ "type": "string"
+ },
+ {
+ "type": "null"
+ }
+ ],
+ "title": "Vae Model",
+ "description": "VAE model name/identifier"
},
- "file_path": {
+ "scheduler": {
"anyOf": [
{
"type": "string"
@@ -58657,692 +64271,508 @@
"type": "null"
}
],
- "default": null,
- "description": "Path to prompt text file",
- "field_kind": "input",
- "input": "any",
- "orig_required": true,
- "title": "File Path"
+ "title": "Scheduler",
+ "description": "Scheduler name"
},
- "pre_prompt": {
+ "steps": {
"anyOf": [
{
- "type": "string"
+ "type": "integer",
+ "minimum": 1.0
},
{
"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"
+ "title": "Steps",
+ "description": "Number of generation steps"
},
- "post_prompt": {
+ "refiner_steps": {
"anyOf": [
{
- "type": "string"
+ "type": "integer",
+ "minimum": 0.0
},
{
"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"
- }
- },
- "required": ["timestamp", "queue_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"
+ "title": "Refiner Steps",
+ "description": "Number of refiner steps"
},
- "origin": {
+ "cfg_scale": {
"anyOf": [
{
- "type": "string"
+ "type": "number"
},
{
"type": "null"
}
],
- "default": null,
- "description": "The origin of the queue item",
- "title": "Origin"
+ "title": "Cfg Scale",
+ "description": "CFG scale for guidance"
},
- "destination": {
+ "cfg_rescale_multiplier": {
"anyOf": [
{
- "type": "string"
+ "type": "number"
},
{
"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", "completed", "failed", "canceled"],
- "title": "Status",
- "type": "string"
+ "title": "Cfg Rescale Multiplier",
+ "description": "CFG rescale multiplier"
},
- "status_sequence": {
+ "refiner_cfg_scale": {
"anyOf": [
{
- "type": "integer"
+ "type": "number"
},
{
"type": "null"
}
],
- "default": null,
- "description": "A monotonically increasing version for this queue item's visible status lifecycle",
- "title": "Status Sequence"
+ "title": "Refiner Cfg Scale",
+ "description": "Refiner CFG scale"
},
- "error_type": {
+ "guidance": {
"anyOf": [
{
- "type": "string"
+ "type": "number"
},
{
"type": "null"
}
],
- "default": null,
- "description": "The error type, if any",
- "title": "Error Type"
+ "title": "Guidance",
+ "description": "Guidance scale"
},
- "error_message": {
+ "width": {
"anyOf": [
{
- "type": "string"
+ "type": "integer",
+ "minimum": 64.0
},
{
"type": "null"
}
],
- "default": null,
- "description": "The error message, if any",
- "title": "Error Message"
+ "title": "Width",
+ "description": "Image width in pixels"
},
- "error_traceback": {
+ "height": {
"anyOf": [
{
- "type": "string"
+ "type": "integer",
+ "minimum": 64.0
},
{
"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"
+ "title": "Height",
+ "description": "Image height in pixels"
},
- "started_at": {
+ "seed": {
"anyOf": [
{
- "type": "string"
+ "type": "integer",
+ "minimum": 0.0
},
{
"type": "null"
}
],
- "default": null,
- "description": "The timestamp when the queue item was started",
- "title": "Started At"
+ "title": "Seed",
+ "description": "Random seed"
},
- "completed_at": {
+ "denoise_strength": {
"anyOf": [
{
- "type": "string"
+ "type": "number",
+ "maximum": 1.0,
+ "minimum": 0.0
},
{
"type": "null"
}
],
- "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"
- },
- "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"
- },
- "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"
- }
- },
- "required": ["timestamp", "queue_id", "retried_item_ids"],
- "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."
+ "title": "Denoise Strength",
+ "description": "Denoising strength"
},
- "file_size": {
- "type": "integer",
- "title": "File Size",
- "description": "The size of the model in bytes."
+ "refiner_denoise_start": {
+ "anyOf": [
+ {
+ "type": "number",
+ "maximum": 1.0,
+ "minimum": 0.0
+ },
+ {
+ "type": "null"
+ }
+ ],
+ "title": "Refiner Denoise Start",
+ "description": "Refiner denoising start"
},
- "name": {
- "type": "string",
- "title": "Name",
- "description": "Name of the model."
+ "clip_skip": {
+ "anyOf": [
+ {
+ "type": "integer",
+ "minimum": 0.0
+ },
+ {
+ "type": "null"
+ }
+ ],
+ "title": "Clip Skip",
+ "description": "CLIP skip layers"
},
- "description": {
+ "seamless_x": {
"anyOf": [
{
- "type": "string"
+ "type": "boolean"
},
{
"type": "null"
}
],
- "title": "Description",
- "description": "Model description"
+ "title": "Seamless X",
+ "description": "Enable seamless X tiling"
},
- "source": {
- "type": "string",
- "title": "Source",
- "description": "The original source of the model (path, URL or repo_id)."
+ "seamless_y": {
+ "anyOf": [
+ {
+ "type": "boolean"
+ },
+ {
+ "type": "null"
+ }
+ ],
+ "title": "Seamless Y",
+ "description": "Enable seamless Y tiling"
},
- "source_type": {
- "$ref": "#/components/schemas/ModelSourceType",
- "description": "The type of source"
+ "refiner_positive_aesthetic_score": {
+ "anyOf": [
+ {
+ "type": "number"
+ },
+ {
+ "type": "null"
+ }
+ ],
+ "title": "Refiner Positive Aesthetic Score",
+ "description": "Refiner positive aesthetic score"
},
- "source_api_response": {
+ "refiner_negative_aesthetic_score": {
"anyOf": [
{
- "type": "string"
+ "type": "number"
},
{
"type": "null"
}
],
- "title": "Source Api Response",
- "description": "The original API response from the source, as stringified JSON."
+ "title": "Refiner Negative Aesthetic Score",
+ "description": "Refiner negative aesthetic score"
},
- "source_url": {
+ "loras": {
"anyOf": [
{
- "type": "string"
+ "items": {
+ "$ref": "#/components/schemas/LoRARecallParameter"
+ },
+ "type": "array"
},
{
"type": "null"
}
],
- "title": "Source Url",
- "description": "Optional URL for the model (e.g. download page or model page)."
+ "title": "Loras",
+ "description": "List of LoRAs with their weights"
},
- "cover_image": {
+ "control_layers": {
"anyOf": [
{
- "type": "string"
+ "items": {
+ "$ref": "#/components/schemas/ControlNetRecallParameter"
+ },
+ "type": "array"
},
{
"type": "null"
}
],
- "title": "Cover Image",
- "description": "Url for image to preview model"
+ "title": "Control Layers",
+ "description": "List of control adapters (ControlNet, T2I Adapter, Control LoRA) with their settings"
},
- "config_path": {
+ "ip_adapters": {
"anyOf": [
{
- "type": "string"
+ "items": {
+ "$ref": "#/components/schemas/IPAdapterRecallParameter"
+ },
+ "type": "array"
},
{
"type": "null"
}
],
- "title": "Config Path",
- "description": "Path to the config for this model, if any."
+ "title": "Ip Adapters",
+ "description": "List of IP Adapters with their settings"
},
- "base": {
- "type": "string",
- "const": "any",
- "title": "Base",
- "default": "any"
+ "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."
+ }
+ },
+ "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"
},
- "type": {
- "type": "string",
- "const": "qwen3_encoder",
- "title": "Type",
- "default": "qwen3_encoder"
+ "queue_id": {
+ "description": "The ID of the queue",
+ "title": "Queue Id",
+ "type": "string"
},
- "format": {
- "type": "string",
- "const": "checkpoint",
- "title": "Format",
- "default": "checkpoint"
+ "user_id": {
+ "description": "The ID of the user whose recall parameters were updated",
+ "title": "User Id",
+ "type": "string"
},
- "cpu_only": {
+ "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": {
"anyOf": [
{
- "type": "boolean"
+ "$ref": "#/components/schemas/MetadataField"
},
{
"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)."
+ "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": {
+ "width": {
"anyOf": [
{
- "type": "string"
+ "type": "integer"
},
{
"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 width of the entire mask.",
+ "field_kind": "input",
+ "input": "any",
+ "orig_required": true,
+ "title": "Width"
},
- "source_api_response": {
+ "height": {
"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 height of the entire mask.",
+ "field_kind": "input",
+ "input": "any",
+ "orig_required": true,
+ "title": "Height"
},
- "source_url": {
+ "x_left": {
"anyOf": [
{
- "type": "string"
+ "type": "integer"
},
{
"type": "null"
}
],
- "title": "Source Url",
- "description": "Optional URL for the model (e.g. download page or model page)."
+ "default": null,
+ "description": "The left x-coordinate of the rectangular masked region (inclusive).",
+ "field_kind": "input",
+ "input": "any",
+ "orig_required": true,
+ "title": "X Left"
},
- "cover_image": {
+ "y_top": {
"anyOf": [
{
- "type": "string"
+ "type": "integer"
},
{
"type": "null"
}
],
- "title": "Cover Image",
- "description": "Url for image to preview model"
+ "default": null,
+ "description": "The top y-coordinate of the rectangular masked region (inclusive).",
+ "field_kind": "input",
+ "input": "any",
+ "orig_required": true,
+ "title": "Y Top"
},
- "config_path": {
+ "rectangle_width": {
"anyOf": [
{
- "type": "string"
+ "type": "integer"
},
{
"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": "The width of the rectangular masked region.",
+ "field_kind": "input",
+ "input": "any",
+ "orig_required": true,
+ "title": "Rectangle Width"
},
- "cpu_only": {
+ "rectangle_height": {
"anyOf": [
{
- "type": "boolean"
+ "type": "integer"
},
{
"type": "null"
}
],
- "title": "Cpu Only",
- "description": "Whether this model should run on CPU only"
+ "default": null,
+ "description": "The height of the rectangular masked region.",
+ "field_kind": "input",
+ "input": "any",
+ "orig_required": true,
+ "title": "Rectangle Height"
},
- "variant": {
- "$ref": "#/components/schemas/Qwen3VariantType",
- "description": "Qwen3 model size variant (4B or 8B)"
+ "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",
- "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.0.1",
+ "output": {
+ "$ref": "#/components/schemas/MaskOutput"
+ }
},
- "Qwen3Encoder_Qwen3Encoder_Config": {
+ "ReferenceImageRecallParameter": {
"properties": {
- "key": {
+ "image_name": {
"type": "string",
- "title": "Key",
- "description": "A unique key for this model."
- },
- "hash": {
+ "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",
- "title": "Hash",
- "description": "The hash of the model file(s)."
+ "minLength": 1,
+ "format": "uri",
+ "title": "Url",
+ "description": "The url to download this model file"
},
"path": {
"type": "string",
+ "format": "path",
"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."
+ "description": "The path to the file, relative to the model root"
},
- "name": {
- "type": "string",
- "title": "Name",
- "description": "Name of the model."
+ "size": {
+ "anyOf": [
+ {
+ "type": "integer"
+ },
+ {
+ "type": "null"
+ }
+ ],
+ "title": "Size",
+ "description": "The size of this file, in bytes",
+ "default": 0
},
- "description": {
+ "sha256": {
"anyOf": [
{
"type": "string"
@@ -59351,119 +64781,319 @@
"type": "null"
}
],
- "title": "Description",
- "description": "Model description"
+ "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"
},
- "source": {
- "type": "string",
- "title": "Source",
- "description": "The original source of the model (path, URL or repo_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"
+ },
+ "RemoveVideosFromBoardResult": {
+ "properties": {
+ "affected_boards": {
+ "items": {
+ "type": "string"
+ },
+ "type": "array",
+ "title": "Affected Boards",
+ "description": "The ids of boards affected by the operation"
},
- "source_type": {
- "$ref": "#/components/schemas/ModelSourceType",
- "description": "The type of source"
+ "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",
+ "title": "Id",
+ "type": "string"
},
- "source_api_response": {
+ "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": "The original API response from the source, as stringified JSON."
+ "default": null,
+ "description": "Latents tensor",
+ "field_kind": "input",
+ "input": "connection",
+ "orig_required": true
},
- "source_url": {
+ "width": {
"anyOf": [
{
- "type": "string"
+ "minimum": 64,
+ "multipleOf": 8,
+ "type": "integer"
},
{
"type": "null"
}
],
- "title": "Source Url",
- "description": "Optional URL for the model (e.g. download page or model page)."
+ "default": null,
+ "description": "Width of output (px)",
+ "field_kind": "input",
+ "input": "any",
+ "orig_required": true,
+ "title": "Width"
},
- "cover_image": {
+ "height": {
"anyOf": [
{
- "type": "string"
+ "minimum": 64,
+ "multipleOf": 8,
+ "type": "integer"
},
{
"type": "null"
}
],
- "title": "Cover Image",
- "description": "Url for image to preview model"
+ "default": null,
+ "description": "Width of output (px)",
+ "field_kind": "input",
+ "input": "any",
+ "orig_required": true,
+ "title": "Height"
},
- "base": {
+ "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"
+ },
+ "type": {
+ "const": "lresize",
+ "default": "lresize",
+ "field_kind": "node_attribute",
+ "title": "type",
+ "type": "string"
+ }
+ },
+ "required": ["type", "id"],
+ "tags": ["latents", "resize"],
+ "title": "Resize Latents",
+ "type": "object",
+ "version": "1.0.2",
+ "output": {
+ "$ref": "#/components/schemas/LatentsOutput"
+ }
+ },
+ "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)."
+ },
+ "RetryItemsResult": {
+ "properties": {
+ "queue_id": {
"type": "string",
- "const": "any",
- "title": "Base",
- "default": "any"
+ "title": "Queue Id",
+ "description": "The ID of the queue"
+ },
+ "retried_item_ids": {
+ "items": {
+ "type": "integer"
+ },
+ "type": "array",
+ "title": "Retried Item Ids",
+ "description": "The IDs of the queue items that were retried"
+ }
+ },
+ "type": "object",
+ "required": ["queue_id", "retried_item_ids"],
+ "title": "RetryItemsResult"
+ },
+ "RoundInvocation": {
+ "category": "math",
+ "class": "invocation",
+ "classification": "stable",
+ "description": "Rounds a float to a specified number of decimal places.",
+ "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": 0,
+ "description": "The float value",
+ "field_kind": "input",
+ "input": "any",
+ "orig_default": 0,
+ "orig_required": false,
+ "title": "Value",
+ "type": "number"
},
- "type": {
- "type": "string",
- "const": "qwen3_encoder",
- "title": "Type",
- "default": "qwen3_encoder"
+ "decimals": {
+ "default": 0,
+ "description": "The number of decimal places",
+ "field_kind": "input",
+ "input": "any",
+ "orig_default": 0,
+ "orig_required": false,
+ "title": "Decimals",
+ "type": "integer"
},
- "format": {
- "type": "string",
- "const": "qwen3_encoder",
- "title": "Format",
- "default": "qwen3_encoder"
+ "type": {
+ "const": "round_float",
+ "default": "round_float",
+ "field_kind": "node_attribute",
+ "title": "type",
+ "type": "string"
+ }
+ },
+ "required": ["type", "id"],
+ "tags": ["math", "round"],
+ "title": "Round Float",
+ "type": "object",
+ "version": "1.0.1",
+ "output": {
+ "$ref": "#/components/schemas/FloatOutput"
+ }
+ },
+ "SAMPoint": {
+ "properties": {
+ "x": {
+ "description": "The x-coordinate of the point",
+ "title": "X",
+ "type": "integer"
},
- "cpu_only": {
- "anyOf": [
- {
- "type": "boolean"
- },
- {
- "type": "null"
- }
- ],
- "title": "Cpu Only",
- "description": "Whether this model should run on CPU only"
+ "y": {
+ "description": "The y-coordinate of the point",
+ "title": "Y",
+ "type": "integer"
},
- "variant": {
- "$ref": "#/components/schemas/Qwen3VariantType",
- "description": "Qwen3 model size variant (4B or 8B)"
+ "label": {
+ "$ref": "#/components/schemas/SAMPointLabel",
+ "description": "The label of the point"
}
},
- "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."
+ "required": ["x", "y", "label"],
+ "title": "SAMPoint",
+ "type": "object"
},
- "Qwen3VariantType": {
- "type": "string",
- "enum": ["qwen3_4b", "qwen3_8b", "qwen3_06b"],
- "title": "Qwen3VariantType",
- "description": "Qwen3 text encoder variants based on model size."
+ "SAMPointLabel": {
+ "enum": [-1, 0, 1],
+ "title": "SAMPointLabel",
+ "type": "integer"
},
- "QwenImageConditioningField": {
- "description": "A Qwen Image Edit conditioning tensor primitive value",
+ "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",
@@ -59472,36 +65102,36 @@
}
},
"required": ["conditioning_name"],
- "title": "QwenImageConditioningField",
+ "title": "SD3ConditioningField",
"type": "object"
},
- "QwenImageConditioningOutput": {
+ "SD3ConditioningOutput": {
"class": "output",
- "description": "Base class for nodes that output a Qwen Image Edit conditioning tensor.",
+ "description": "Base class for nodes that output a single SD3 conditioning tensor",
"properties": {
"conditioning": {
- "$ref": "#/components/schemas/QwenImageConditioningField",
+ "$ref": "#/components/schemas/SD3ConditioningField",
"description": "Conditioning tensor",
"field_kind": "output",
"ui_hidden": false
},
"type": {
- "const": "qwen_image_conditioning_output",
- "default": "qwen_image_conditioning_output",
+ "const": "sd3_conditioning_output",
+ "default": "sd3_conditioning_output",
"field_kind": "node_attribute",
"title": "type",
"type": "string"
}
},
"required": ["output_meta", "conditioning", "type", "type"],
- "title": "QwenImageConditioningOutput",
+ "title": "SD3ConditioningOutput",
"type": "object"
},
- "QwenImageDenoiseInvocation": {
- "category": "image",
+ "SD3DenoiseInvocation": {
+ "category": "latents",
"class": "invocation",
- "classification": "prototype",
- "description": "Run the denoising process with a Qwen Image model.",
+ "classification": "stable",
+ "description": "Run denoising process with a SD3 model.",
"node_pack": "invokeai",
"properties": {
"board": {
@@ -59576,7 +65206,7 @@
"orig_default": null,
"orig_required": false
},
- "reference_latents": {
+ "noise": {
"anyOf": [
{
"$ref": "#/components/schemas/LatentsField"
@@ -59586,7 +65216,7 @@
}
],
"default": null,
- "description": "Reference image latents to guide generation. Encoded through the VAE.",
+ "description": "Noise tensor",
"field_kind": "input",
"input": "connection",
"orig_default": null,
@@ -59642,7 +65272,7 @@
}
],
"default": null,
- "description": "Qwen Image Edit model (Transformer) to load",
+ "description": "SD3 model (MMDiTX) to load",
"field_kind": "input",
"input": "connection",
"orig_required": true,
@@ -59651,7 +65281,7 @@
"positive_conditioning": {
"anyOf": [
{
- "$ref": "#/components/schemas/QwenImageConditioningField"
+ "$ref": "#/components/schemas/SD3ConditioningField"
},
{
"type": "null"
@@ -59666,7 +65296,7 @@
"negative_conditioning": {
"anyOf": [
{
- "$ref": "#/components/schemas/QwenImageConditioningField"
+ "$ref": "#/components/schemas/SD3ConditioningField"
},
{
"type": "null"
@@ -59676,8 +65306,7 @@
"description": "Negative conditioning tensor",
"field_kind": "input",
"input": "connection",
- "orig_default": null,
- "orig_required": false
+ "orig_required": true
},
"cfg_scale": {
"anyOf": [
@@ -59691,11 +65320,11 @@
"type": "array"
}
],
- "default": 4.0,
+ "default": 3.5,
"description": "Classifier-Free Guidance scale",
"field_kind": "input",
"input": "any",
- "orig_default": 4.0,
+ "orig_default": 3.5,
"orig_required": false,
"title": "CFG Scale"
},
@@ -59722,12 +65351,12 @@
"type": "integer"
},
"steps": {
- "default": 40,
+ "default": 10,
"description": "Number of steps to run",
"exclusiveMinimum": 0,
"field_kind": "input",
"input": "any",
- "orig_default": 40,
+ "orig_default": 10,
"orig_required": false,
"title": "Steps",
"type": "integer"
@@ -59742,45 +65371,138 @@
"title": "Seed",
"type": "integer"
},
- "shift": {
+ "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": "number"
+ "$ref": "#/components/schemas/BoardField"
},
{
"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": "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"
+ },
+ "image": {
+ "anyOf": [
+ {
+ "$ref": "#/components/schemas/ImageField"
+ },
+ {
+ "type": "null"
+ }
+ ],
+ "default": null,
+ "description": "The image to encode",
"field_kind": "input",
"input": "any",
- "orig_default": null,
- "orig_required": false,
- "title": "Shift"
+ "orig_required": true
+ },
+ "vae": {
+ "anyOf": [
+ {
+ "$ref": "#/components/schemas/VAEField"
+ },
+ {
+ "type": "null"
+ }
+ ],
+ "default": null,
+ "description": "VAE",
+ "field_kind": "input",
+ "input": "connection",
+ "orig_required": true
},
"type": {
- "const": "qwen_image_denoise",
- "default": "qwen_image_denoise",
+ "const": "sd3_i2l",
+ "default": "sd3_i2l",
"field_kind": "node_attribute",
"title": "type",
"type": "string"
}
},
"required": ["type", "id"],
- "tags": ["image", "qwen_image"],
- "title": "Denoise - Qwen Image",
+ "tags": ["image", "latents", "vae", "i2l", "sd3"],
+ "title": "Image to Latents - SD3",
"type": "object",
- "version": "1.0.0",
+ "version": "1.0.1",
"output": {
"$ref": "#/components/schemas/LatentsOutput"
}
},
- "QwenImageImageToLatentsInvocation": {
- "category": "image",
+ "SD3LatentsToImageInvocation": {
+ "category": "latents",
"class": "invocation",
- "classification": "prototype",
- "description": "Generates latents from an image using the Qwen Image VAE.",
+ "classification": "stable",
+ "description": "Generates an image from latents.",
"node_pack": "invokeai",
"properties": {
"board": {
@@ -59799,22 +65521,278 @@
"orig_required": false,
"ui_hidden": false
},
- "metadata": {
+ "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"
+ },
+ "latents": {
+ "anyOf": [
+ {
+ "$ref": "#/components/schemas/LatentsField"
+ },
+ {
+ "type": "null"
+ }
+ ],
+ "default": null,
+ "description": "Latents tensor",
+ "field_kind": "input",
+ "input": "connection",
+ "orig_required": true
+ },
+ "vae": {
+ "anyOf": [
+ {
+ "$ref": "#/components/schemas/VAEField"
+ },
+ {
+ "type": "null"
+ }
+ ],
+ "default": null,
+ "description": "VAE",
+ "field_kind": "input",
+ "input": "connection",
+ "orig_required": true
+ },
+ "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",
+ "version": "1.3.2",
+ "output": {
+ "$ref": "#/components/schemas/ImageOutput"
+ }
+ },
+ "SDXLCompelPromptInvocation": {
+ "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"
+ },
+ "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",
+ "ui_component": "textarea"
+ },
+ "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"
+ },
+ "target_width": {
+ "default": 1024,
+ "description": "",
+ "field_kind": "input",
+ "input": "any",
+ "orig_default": 1024,
+ "orig_required": false,
+ "title": "Target Width",
+ "type": "integer"
+ },
+ "target_height": {
+ "default": 1024,
+ "description": "",
+ "field_kind": "input",
+ "input": "any",
+ "orig_default": 1024,
+ "orig_required": false,
+ "title": "Target Height",
+ "type": "integer"
+ },
+ "clip": {
+ "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 1"
+ },
+ "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,
+ "title": "CLIP 2"
+ },
+ "mask": {
"anyOf": [
{
- "$ref": "#/components/schemas/MetadataField"
+ "$ref": "#/components/schemas/TensorField"
},
{
"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": "A mask defining the region that this conditioning prompt applies to.",
+ "field_kind": "input",
+ "input": "any",
+ "orig_default": null,
+ "orig_required": false
},
+ "type": {
+ "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",
@@ -59839,126 +65817,104 @@
"title": "Use Cache",
"type": "boolean"
},
- "image": {
+ "loras": {
"anyOf": [
{
- "$ref": "#/components/schemas/ImageField"
+ "$ref": "#/components/schemas/LoRAField"
+ },
+ {
+ "items": {
+ "$ref": "#/components/schemas/LoRAField"
+ },
+ "type": "array"
},
{
"type": "null"
}
],
"default": null,
- "description": "The image to encode.",
+ "description": "LoRA models and weights. May be a single LoRA or collection.",
"field_kind": "input",
"input": "any",
- "orig_required": true
+ "orig_default": null,
+ "orig_required": false,
+ "title": "LoRAs"
},
- "vae": {
+ "unet": {
"anyOf": [
{
- "$ref": "#/components/schemas/VAEField"
+ "$ref": "#/components/schemas/UNetField"
},
{
"type": "null"
}
],
"default": null,
- "description": "VAE",
+ "description": "UNet (scheduler, LoRAs)",
"field_kind": "input",
"input": "connection",
- "orig_required": true
+ "orig_default": null,
+ "orig_required": false,
+ "title": "UNet"
},
- "width": {
+ "clip": {
"anyOf": [
{
- "type": "integer"
+ "$ref": "#/components/schemas/CLIPField"
},
{
"type": "null"
}
],
"default": null,
- "description": "Resize the image to this width before encoding. If not set, encodes at the image's original size.",
+ "description": "CLIP (tokenizer, text encoder, LoRAs) and skipped layer count",
"field_kind": "input",
- "input": "any",
+ "input": "connection",
"orig_default": null,
"orig_required": false,
- "title": "Width"
+ "title": "CLIP"
},
- "height": {
+ "clip2": {
"anyOf": [
{
- "type": "integer"
+ "$ref": "#/components/schemas/CLIPField"
},
{
"type": "null"
}
],
"default": null,
- "description": "Resize the image to this height before encoding. If not set, encodes at the image's original size.",
+ "description": "CLIP (tokenizer, text encoder, LoRAs) and skipped layer count",
"field_kind": "input",
- "input": "any",
+ "input": "connection",
"orig_default": null,
"orig_required": false,
- "title": "Height"
+ "title": "CLIP 2"
},
"type": {
- "const": "qwen_image_i2l",
- "default": "qwen_image_i2l",
+ "const": "sdxl_lora_collection_loader",
+ "default": "sdxl_lora_collection_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"],
+ "title": "Apply LoRA Collection - SDXL",
"type": "object",
- "version": "1.0.0",
+ "version": "1.1.2",
"output": {
- "$ref": "#/components/schemas/LatentsOutput"
+ "$ref": "#/components/schemas/SDXLLoRALoaderOutput"
}
},
- "QwenImageLatentsToImageInvocation": {
- "category": "latents",
+ "SDXLLoRALoaderInvocation": {
+ "category": "model",
"class": "invocation",
- "classification": "prototype",
- "description": "Generates an image from latents using the Qwen Image VAE.",
+ "classification": "stable",
+ "description": "Apply selected lora to unet and text_encoder.",
"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",
@@ -59983,146 +65939,168 @@
"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": "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": {
+ "anyOf": [
+ {
+ "$ref": "#/components/schemas/UNetField"
+ },
+ {
+ "type": "null"
+ }
+ ],
+ "default": null,
+ "description": "UNet (scheduler, LoRAs)",
"field_kind": "input",
"input": "connection",
- "orig_required": true
+ "orig_default": null,
+ "orig_required": false,
+ "title": "UNet"
},
- "vae": {
+ "clip": {
"anyOf": [
{
- "$ref": "#/components/schemas/VAEField"
+ "$ref": "#/components/schemas/CLIPField"
},
{
"type": "null"
}
],
"default": null,
- "description": "VAE",
+ "description": "CLIP (tokenizer, text encoder, LoRAs) and skipped layer count",
"field_kind": "input",
"input": "connection",
- "orig_required": true
+ "orig_default": null,
+ "orig_required": false,
+ "title": "CLIP 1"
+ },
+ "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_default": null,
+ "orig_required": false,
+ "title": "CLIP 2"
},
"type": {
- "const": "qwen_image_l2i",
- "default": "qwen_image_l2i",
+ "const": "sdxl_lora_loader",
+ "default": "sdxl_lora_loader",
"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": ["lora", "model"],
+ "title": "Apply LoRA - SDXL",
"type": "object",
- "version": "1.0.0",
+ "version": "1.0.5",
"output": {
- "$ref": "#/components/schemas/ImageOutput"
+ "$ref": "#/components/schemas/SDXLLoRALoaderOutput"
}
},
- "QwenImageLoRACollectionLoader": {
- "category": "model",
- "class": "invocation",
- "classification": "prototype",
- "description": "Applies a collection of LoRAs to a Qwen Image transformer.",
- "node_pack": "invokeai",
+ "SDXLLoRALoaderOutput": {
+ "class": "output",
+ "description": "SDXL LoRA 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"
- },
- "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": {
+ "unet": {
"anyOf": [
{
- "$ref": "#/components/schemas/LoRAField"
+ "$ref": "#/components/schemas/UNetField"
},
{
- "items": {
- "$ref": "#/components/schemas/LoRAField"
- },
- "type": "array"
+ "type": "null"
+ }
+ ],
+ "default": null,
+ "description": "UNet (scheduler, LoRAs)",
+ "field_kind": "output",
+ "title": "UNet",
+ "ui_hidden": false
+ },
+ "clip": {
+ "anyOf": [
+ {
+ "$ref": "#/components/schemas/CLIPField"
},
{
"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"
+ "description": "CLIP (tokenizer, text encoder, LoRAs) and skipped layer count",
+ "field_kind": "output",
+ "title": "CLIP 1",
+ "ui_hidden": false
},
- "transformer": {
+ "clip2": {
"anyOf": [
{
- "$ref": "#/components/schemas/TransformerField"
+ "$ref": "#/components/schemas/CLIPField"
},
{
"type": "null"
}
],
"default": null,
- "description": "Transformer",
- "field_kind": "input",
- "input": "connection",
- "orig_default": null,
- "orig_required": false,
- "title": "Transformer"
+ "description": "CLIP (tokenizer, text encoder, LoRAs) and skipped layer count",
+ "field_kind": "output",
+ "title": "CLIP 2",
+ "ui_hidden": false
},
"type": {
- "const": "qwen_image_lora_collection_loader",
- "default": "qwen_image_lora_collection_loader",
+ "const": "sdxl_lora_loader_output",
+ "default": "sdxl_lora_loader_output",
"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.0",
- "output": {
- "$ref": "#/components/schemas/QwenImageLoRALoaderOutput"
- }
+ "required": ["output_meta", "unet", "clip", "clip2", "type", "type"],
+ "title": "SDXLLoRALoaderOutput",
+ "type": "object"
},
- "QwenImageLoRALoaderInvocation": {
+ "SDXLModelLoaderInvocation": {
"category": "model",
"class": "invocation",
- "classification": "prototype",
- "description": "Apply a LoRA model to a Qwen Image transformer.",
+ "classification": "stable",
+ "description": "Loads an sdxl base model, outputting its submodels.",
"node_pack": "invokeai",
"properties": {
"id": {
@@ -60149,7 +66127,7 @@
"title": "Use Cache",
"type": "boolean"
},
- "lora": {
+ "model": {
"anyOf": [
{
"$ref": "#/components/schemas/ModelIdentifierField"
@@ -60159,94 +66137,79 @@
}
],
"default": null,
- "description": "LoRA model to load",
+ "description": "SDXL Main model (UNet, VAE, CLIP1, CLIP2) to load",
"field_kind": "input",
"input": "any",
"orig_required": true,
- "title": "LoRA",
- "ui_model_base": ["qwen-image"],
- "ui_model_type": ["lora"]
- },
- "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/TransformerField"
- },
- {
- "type": "null"
- }
- ],
- "default": null,
- "description": "Transformer",
- "field_kind": "input",
- "input": "connection",
- "orig_default": null,
- "orig_required": false,
- "title": "Transformer"
+ "ui_model_base": ["sdxl"],
+ "ui_model_type": ["main"]
},
"type": {
- "const": "qwen_image_lora_loader",
- "default": "qwen_image_lora_loader",
+ "const": "sdxl_model_loader",
+ "default": "sdxl_model_loader",
"field_kind": "node_attribute",
"title": "type",
"type": "string"
}
},
"required": ["type", "id"],
- "tags": ["lora", "model", "qwen_image"],
- "title": "Apply LoRA - Qwen Image",
+ "tags": ["model", "sdxl"],
+ "title": "Main Model - SDXL",
"type": "object",
- "version": "1.0.0",
+ "version": "1.0.4",
"output": {
- "$ref": "#/components/schemas/QwenImageLoRALoaderOutput"
+ "$ref": "#/components/schemas/SDXLModelLoaderOutput"
}
},
- "QwenImageLoRALoaderOutput": {
+ "SDXLModelLoaderOutput": {
"class": "output",
- "description": "Qwen Image LoRA Loader Output",
+ "description": "SDXL base model loader output",
"properties": {
- "transformer": {
- "anyOf": [
- {
- "$ref": "#/components/schemas/TransformerField"
- },
- {
- "type": "null"
- }
- ],
- "default": null,
- "description": "Transformer",
+ "unet": {
+ "$ref": "#/components/schemas/UNetField",
+ "description": "UNet (scheduler, LoRAs)",
"field_kind": "output",
- "title": "Transformer",
+ "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": "qwen_image_lora_loader_output",
- "default": "qwen_image_lora_loader_output",
+ "const": "sdxl_model_loader_output",
+ "default": "sdxl_model_loader_output",
"field_kind": "node_attribute",
"title": "type",
"type": "string"
}
},
- "required": ["output_meta", "transformer", "type", "type"],
- "title": "QwenImageLoRALoaderOutput",
+ "required": ["output_meta", "unet", "clip", "clip2", "vae", "type", "type"],
+ "title": "SDXLModelLoaderOutput",
"type": "object"
},
- "QwenImageModelLoaderInvocation": {
- "category": "model",
+ "SDXLRefinerCompelPromptInvocation": {
+ "category": "prompt",
"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": "Parse prompt using compel package to conditioning.",
"node_pack": "invokeai",
"properties": {
"id": {
@@ -60273,132 +66236,104 @@
"title": "Use Cache",
"type": "boolean"
},
- "model": {
- "$ref": "#/components/schemas/ModelIdentifierField",
- "description": "Qwen Image Edit model (Transformer) to load",
+ "style": {
+ "default": "",
+ "description": "Prompt to be parsed by Compel to create a conditioning tensor",
"field_kind": "input",
- "input": "direct",
- "orig_required": true,
- "title": "Transformer",
- "ui_model_base": ["qwen-image"],
- "ui_model_type": ["main"]
+ "input": "any",
+ "orig_default": "",
+ "orig_required": false,
+ "title": "Style",
+ "type": "string",
+ "ui_component": "textarea"
},
- "vae_model": {
- "anyOf": [
- {
- "$ref": "#/components/schemas/ModelIdentifierField"
- },
- {
- "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).",
+ "original_width": {
+ "default": 1024,
+ "description": "",
"field_kind": "input",
- "input": "direct",
- "orig_default": null,
+ "input": "any",
+ "orig_default": 1024,
"orig_required": false,
- "title": "VAE",
- "ui_model_base": ["qwen-image"],
- "ui_model_type": ["vae"]
+ "title": "Original Width",
+ "type": "integer"
},
- "qwen_vl_encoder_model": {
- "anyOf": [
- {
- "$ref": "#/components/schemas/ModelIdentifierField"
- },
- {
- "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).",
+ "original_height": {
+ "default": 1024,
+ "description": "",
"field_kind": "input",
- "input": "direct",
- "orig_default": null,
+ "input": "any",
+ "orig_default": 1024,
"orig_required": false,
- "title": "Qwen VL Encoder",
- "ui_model_type": ["qwen_vl_encoder"]
+ "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",
+ "field_kind": "input",
+ "input": "any",
+ "orig_default": 6.0,
+ "orig_required": false,
+ "title": "Aesthetic Score",
+ "type": "number"
},
- "component_source": {
+ "clip2": {
"anyOf": [
{
- "$ref": "#/components/schemas/ModelIdentifierField"
+ "$ref": "#/components/schemas/CLIPField"
},
{
"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.",
+ "description": "CLIP (tokenizer, text encoder, LoRAs) and skipped layer count",
"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"]
+ "input": "connection",
+ "orig_required": true
},
"type": {
- "const": "qwen_image_model_loader",
- "default": "qwen_image_model_loader",
+ "const": "sdxl_refiner_compel_prompt",
+ "default": "sdxl_refiner_compel_prompt",
"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": ["sdxl", "compel", "prompt"],
+ "title": "Prompt - SDXL Refiner",
"type": "object",
- "version": "1.2.0",
+ "version": "1.1.2",
"output": {
- "$ref": "#/components/schemas/QwenImageModelLoaderOutput"
+ "$ref": "#/components/schemas/ConditioningOutput"
}
},
- "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",
+ "SDXLRefinerModelLoaderInvocation": {
+ "category": "model",
"class": "invocation",
- "classification": "prototype",
- "description": "Encodes text and reference images for Qwen Image using Qwen2.5-VL.",
+ "classification": "stable",
+ "description": "Loads an sdxl refiner model, outputting its submodels.",
"node_pack": "invokeai",
"properties": {
"id": {
@@ -60425,366 +66360,182 @@
"title": "Use Cache",
"type": "boolean"
},
- "prompt": {
+ "model": {
"anyOf": [
{
- "type": "string"
+ "$ref": "#/components/schemas/ModelIdentifierField"
},
{
"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.",
+ "description": "SDXL Refiner Main Modde (UNet, VAE, CLIP2) to load",
"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"
- }
- ],
- "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"
+ "ui_model_base": ["sdxl-refiner"],
+ "ui_model_type": ["main"]
},
"type": {
- "const": "qwen_image_text_encoder",
- "default": "qwen_image_text_encoder",
+ "const": "sdxl_refiner_model_loader",
+ "default": "sdxl_refiner_model_loader",
"field_kind": "node_attribute",
"title": "type",
"type": "string"
}
},
"required": ["type", "id"],
- "tags": ["prompt", "conditioning", "qwen_image"],
- "title": "Prompt - Qwen Image",
+ "tags": ["model", "sdxl", "refiner"],
+ "title": "Refiner Model - SDXL",
"type": "object",
- "version": "1.2.0",
+ "version": "1.0.4",
"output": {
- "$ref": "#/components/schemas/QwenImageConditioningOutput"
+ "$ref": "#/components/schemas/SDXLRefinerModelLoaderOutput"
}
},
- "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": {
+ "SDXLRefinerModelLoaderOutput": {
+ "class": "output",
+ "description": "SDXL refiner model loader 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"
+ "unet": {
+ "$ref": "#/components/schemas/UNetField",
+ "description": "UNet (scheduler, LoRAs)",
+ "field_kind": "output",
+ "title": "UNet",
+ "ui_hidden": false
},
- "config_path": {
- "anyOf": [
- {
- "type": "string"
- },
- {
- "type": "null"
- }
- ],
- "title": "Config Path",
- "description": "Path to the config for this model, if any."
+ "clip2": {
+ "$ref": "#/components/schemas/CLIPField",
+ "description": "CLIP (tokenizer, text encoder, LoRAs) and skipped layer count",
+ "field_kind": "output",
+ "title": "CLIP 2",
+ "ui_hidden": false
},
- "base": {
- "type": "string",
- "const": "any",
- "title": "Base",
- "default": "any"
+ "vae": {
+ "$ref": "#/components/schemas/VAEField",
+ "description": "VAE",
+ "field_kind": "output",
+ "title": "VAE",
+ "ui_hidden": false
},
"type": {
- "type": "string",
- "const": "qwen_vl_encoder",
- "title": "Type",
- "default": "qwen_vl_encoder"
- },
- "format": {
- "type": "string",
- "const": "checkpoint",
- "title": "Format",
- "default": "checkpoint"
+ "const": "sdxl_refiner_model_loader_output",
+ "default": "sdxl_refiner_model_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",
- "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."
+ "required": ["output_meta", "unet", "clip2", "vae", "type", "type"],
+ "title": "SDXLRefinerModelLoaderOutput",
+ "type": "object"
},
- "QwenVLEncoder_Diffusers_Config": {
+ "SQLiteDirection": {
+ "type": "string",
+ "enum": ["ASC", "DESC"],
+ "title": "SQLiteDirection"
+ },
+ "SaveImageInvocation": {
+ "category": "image",
+ "class": "invocation",
+ "classification": "stable",
+ "description": "Saves an image. Unlike an image primitive, this invocation stores a copy of the image.",
+ "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": {
+ "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"
+ },
+ "image": {
"anyOf": [
{
- "type": "string"
+ "$ref": "#/components/schemas/ImageField"
},
{
"type": "null"
}
],
- "title": "Cover Image",
- "description": "Url for image to preview model"
- },
- "base": {
- "type": "string",
- "const": "any",
- "title": "Base",
- "default": "any"
+ "default": null,
+ "description": "The image to process",
+ "field_kind": "input",
+ "input": "any",
+ "orig_required": true
},
"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": "save_image",
+ "default": "save_image",
+ "field_kind": "node_attribute",
+ "title": "type",
+ "type": "string"
}
},
+ "required": ["type", "id"],
+ "tags": ["primitives", "image"],
+ "title": "Save Image",
"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."
+ "version": "1.2.2",
+ "output": {
+ "$ref": "#/components/schemas/ImageOutput"
+ }
},
- "RandomFloatInvocation": {
- "category": "math",
+ "ScaleLatentsInvocation": {
+ "category": "latents",
"class": "invocation",
"classification": "stable",
- "description": "Outputs a single random float",
+ "description": "Scales latents by a given factor.",
"node_pack": "invokeai",
"properties": {
"id": {
@@ -60805,64 +66556,87 @@
"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",
+ "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",
"field_kind": "input",
"input": "any",
- "orig_default": 0.0,
- "orig_required": false,
- "title": "Low",
- "type": "number"
+ "orig_required": true,
+ "title": "Scale Factor"
},
- "high": {
- "default": 1.0,
- "description": "The exclusive high value",
+ "mode": {
+ "default": "bilinear",
+ "description": "Interpolation mode",
+ "enum": ["nearest", "linear", "bilinear", "bicubic", "trilinear", "area", "nearest-exact"],
"field_kind": "input",
"input": "any",
- "orig_default": 1.0,
+ "orig_default": "bilinear",
"orig_required": false,
- "title": "High",
- "type": "number"
+ "title": "Mode",
+ "type": "string"
},
- "decimals": {
- "default": 2,
- "description": "The number of decimal places to round to",
+ "antialias": {
+ "default": false,
+ "description": "Whether or not to apply antialiasing (bilinear or bicubic only)",
"field_kind": "input",
"input": "any",
- "orig_default": 2,
+ "orig_default": false,
"orig_required": false,
- "title": "Decimals",
- "type": "integer"
+ "title": "Antialias",
+ "type": "boolean"
},
"type": {
- "const": "rand_float",
- "default": "rand_float",
+ "const": "lscale",
+ "default": "lscale",
"field_kind": "node_attribute",
"title": "type",
"type": "string"
}
},
"required": ["type", "id"],
- "tags": ["math", "float", "random"],
- "title": "Random Float",
+ "tags": ["latents", "resize"],
+ "title": "Scale Latents",
"type": "object",
- "version": "1.0.1",
+ "version": "1.0.2",
"output": {
- "$ref": "#/components/schemas/FloatOutput"
+ "$ref": "#/components/schemas/LatentsOutput"
}
},
- "RandomIntInvocation": {
- "category": "math",
+ "SchedulerInvocation": {
+ "category": "latents",
"class": "invocation",
"classification": "stable",
- "description": "Outputs a single random integer.",
+ "description": "Selects a scheduler.",
"node_pack": "invokeai",
"properties": {
"id": {
@@ -60883,54 +66657,140 @@
"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",
- "field_kind": "input",
- "input": "any",
- "orig_default": 0,
- "orig_required": false,
- "title": "Low",
- "type": "integer"
- },
- "high": {
- "default": 2147483647,
- "description": "The exclusive high value",
+ "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": 2147483647,
+ "orig_default": "euler",
"orig_required": false,
- "title": "High",
- "type": "integer"
+ "title": "Scheduler",
+ "type": "string",
+ "ui_type": "SchedulerField"
},
"type": {
- "const": "rand_int",
- "default": "rand_int",
+ "const": "scheduler",
+ "default": "scheduler",
"field_kind": "node_attribute",
"title": "type",
"type": "string"
}
},
"required": ["type", "id"],
- "tags": ["math", "random"],
- "title": "Random Integer",
+ "tags": ["scheduler"],
+ "title": "Scheduler",
"type": "object",
- "version": "1.0.1",
+ "version": "1.0.0",
"output": {
- "$ref": "#/components/schemas/IntegerOutput"
+ "$ref": "#/components/schemas/SchedulerOutput"
}
},
- "RandomRangeInvocation": {
- "category": "batch",
+ "SchedulerOutput": {
+ "class": "output",
+ "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"
+ ],
+ "field_kind": "output",
+ "title": "Scheduler",
+ "type": "string",
+ "ui_hidden": false,
+ "ui_type": "SchedulerField"
+ },
+ "type": {
+ "const": "scheduler_output",
+ "default": "scheduler_output",
+ "field_kind": "node_attribute",
+ "title": "type",
+ "type": "string"
+ }
+ },
+ "required": ["output_meta", "scheduler", "type", "type"],
+ "title": "SchedulerOutput",
+ "type": "object"
+ },
+ "SchedulerPredictionType": {
+ "type": "string",
+ "enum": ["epsilon", "v_prediction", "sample"],
+ "title": "SchedulerPredictionType",
+ "description": "Scheduler prediction type."
+ },
+ "Sd3ModelLoaderInvocation": {
+ "category": "model",
"class": "invocation",
"classification": "stable",
- "description": "Creates a collection of random numbers",
+ "description": "Loads a SD3 base model, outputting its submodels.",
"node_pack": "invokeai",
"properties": {
"id": {
@@ -60951,76 +66811,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,
- "description": "The inclusive low value",
+ "model": {
+ "$ref": "#/components/schemas/ModelIdentifierField",
+ "description": "SD3 model (MMDiTX) to load",
"field_kind": "input",
- "input": "any",
- "orig_default": 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": "integer"
+ "title": "T5 Encoder",
+ "ui_model_type": ["t5_encoder"]
},
- "high": {
- "default": 2147483647,
- "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": 2147483647,
+ "input": "direct",
+ "orig_default": null,
"orig_required": false,
- "title": "High",
- "type": "integer"
+ "title": "CLIP L Encoder",
+ "ui_model_type": ["clip_embed"],
+ "ui_model_variant": ["large"]
},
- "size": {
- "default": 1,
- "description": "The number of values to generate",
+ "clip_g_model": {
+ "anyOf": [
+ {
+ "$ref": "#/components/schemas/ModelIdentifierField"
+ },
+ {
+ "type": "null"
+ }
+ ],
+ "default": null,
+ "description": "CLIP-G Embed loader",
"field_kind": "input",
- "input": "any",
- "orig_default": 1,
+ "input": "direct",
+ "orig_default": null,
"orig_required": false,
- "title": "Size",
- "type": "integer"
+ "title": "CLIP G Encoder",
+ "ui_model_type": ["clip_embed"],
+ "ui_model_variant": ["gigantic"]
},
- "seed": {
- "default": 0,
- "description": "The seed for the RNG (omit for random)",
+ "vae_model": {
+ "anyOf": [
+ {
+ "$ref": "#/components/schemas/ModelIdentifierField"
+ },
+ {
+ "type": "null"
+ }
+ ],
+ "default": null,
+ "description": "VAE model to load",
"field_kind": "input",
"input": "any",
- "maximum": 4294967295,
- "minimum": 0,
- "orig_default": 0,
+ "orig_default": null,
"orig_required": false,
- "title": "Seed",
- "type": "integer"
+ "title": "VAE",
+ "ui_model_base": ["sd-3"],
+ "ui_model_type": ["vae"]
},
"type": {
- "const": "random_range",
- "default": "random_range",
+ "const": "sd3_model_loader",
+ "default": "sd3_model_loader",
"field_kind": "node_attribute",
"title": "type",
"type": "string"
}
},
- "required": ["type", "id"],
- "tags": ["range", "integer", "random", "collection"],
- "title": "Random Range",
+ "required": ["model", "type", "id"],
+ "tags": ["model", "sd3"],
+ "title": "Main Model - SD3",
"type": "object",
"version": "1.0.1",
"output": {
- "$ref": "#/components/schemas/IntegerCollectionOutput"
+ "$ref": "#/components/schemas/Sd3ModelLoaderOutput"
}
},
- "RangeInvocation": {
- "category": "batch",
+ "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",
"class": "invocation",
"classification": "stable",
- "description": "Creates a range of numbers from start to stop with step",
+ "description": "Encodes and preps a prompt for a SD3 image.",
"node_pack": "invokeai",
"properties": {
"id": {
@@ -61047,58 +67000,93 @@
"title": "Use Cache",
"type": "boolean"
},
- "start": {
- "default": 0,
- "description": "The start of the range",
+ "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": "Start",
- "type": "integer"
+ "input": "connection",
+ "orig_required": true,
+ "title": "CLIP L"
},
- "stop": {
- "default": 10,
- "description": "The stop of the range",
+ "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": 10,
+ "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",
+ "field_kind": "input",
+ "input": "connection",
+ "orig_default": null,
"orig_required": false,
- "title": "Stop",
- "type": "integer"
+ "title": "T5Encoder"
},
- "step": {
- "default": 1,
- "description": "The step of the range",
+ "prompt": {
+ "anyOf": [
+ {
+ "type": "string"
+ },
+ {
+ "type": "null"
+ }
+ ],
+ "default": null,
+ "description": "Text prompt to encode.",
"field_kind": "input",
"input": "any",
- "orig_default": 1,
- "orig_required": false,
- "title": "Step",
- "type": "integer"
+ "orig_required": true,
+ "title": "Prompt"
},
"type": {
- "const": "range",
- "default": "range",
+ "const": "sd3_text_encoder",
+ "default": "sd3_text_encoder",
"field_kind": "node_attribute",
"title": "type",
"type": "string"
}
},
"required": ["type", "id"],
- "tags": ["collection", "integer", "range"],
- "title": "Integer Range",
+ "tags": ["prompt", "conditioning", "sd3"],
+ "title": "Prompt - SD3",
"type": "object",
- "version": "1.0.0",
+ "version": "1.0.1",
"output": {
- "$ref": "#/components/schemas/IntegerCollectionOutput"
+ "$ref": "#/components/schemas/SD3ConditioningOutput"
}
},
- "RangeOfSizeInvocation": {
- "category": "batch",
+ "SeamlessModeInvocation": {
+ "category": "model",
"class": "invocation",
"classification": "stable",
- "description": "Creates a range from start to start + (size * step) incremented by step",
+ "description": "Applies the seamless transformation to the Model UNet and VAE.",
"node_pack": "invokeai",
"properties": {
"id": {
@@ -61125,117 +67113,218 @@
"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"
},
- "size": {
- "default": 1,
- "description": "The number of values",
- "exclusiveMinimum": 0,
+ "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": 1,
+ "orig_default": true,
"orig_required": false,
- "title": "Size",
- "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_of_size",
- "default": "range_of_size",
+ "const": "seamless",
+ "default": "seamless",
"field_kind": "node_attribute",
"title": "type",
"type": "string"
}
},
"required": ["type", "id"],
- "tags": ["collection", "integer", "size", "range"],
- "title": "Integer Range of Size",
+ "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"
}
},
- "RecallParameter": {
+ "SeamlessModeOutput": {
+ "class": "output",
+ "description": "Modified Seamless Model output",
"properties": {
- "positive_prompt": {
+ "unet": {
"anyOf": [
{
- "type": "string"
+ "$ref": "#/components/schemas/UNetField"
},
{
"type": "null"
}
],
- "title": "Positive Prompt",
- "description": "Positive prompt text"
+ "default": null,
+ "description": "UNet (scheduler, LoRAs)",
+ "field_kind": "output",
+ "title": "UNet",
+ "ui_hidden": false
},
- "negative_prompt": {
+ "vae": {
"anyOf": [
{
- "type": "string"
+ "$ref": "#/components/schemas/VAEField"
},
{
"type": "null"
}
],
- "title": "Negative Prompt",
- "description": "Negative prompt text"
+ "default": null,
+ "description": "VAE",
+ "field_kind": "output",
+ "title": "VAE",
+ "ui_hidden": false
},
- "model": {
+ "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": "Generate images using a BytePlus Seedream model.",
+ "node_pack": "invokeai",
+ "properties": {
+ "board": {
"anyOf": [
{
- "type": "string"
+ "$ref": "#/components/schemas/BoardField"
},
{
"type": "null"
}
],
- "title": "Model",
- "description": "Main model name/identifier"
+ "default": null,
+ "description": "The board to save the image to",
+ "field_kind": "internal",
+ "input": "direct",
+ "orig_required": false,
+ "ui_hidden": false
},
- "refiner_model": {
+ "metadata": {
"anyOf": [
{
- "type": "string"
+ "$ref": "#/components/schemas/MetadataField"
},
{
"type": "null"
}
],
- "title": "Refiner Model",
- "description": "Refiner model name/identifier"
+ "default": null,
+ "description": "Optional metadata to be saved with the image",
+ "field_kind": "internal",
+ "input": "connection",
+ "orig_required": false,
+ "ui_hidden": false
},
- "vae_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"
+ },
+ "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": [
{
- "type": "string"
+ "$ref": "#/components/schemas/ModelIdentifierField"
},
{
"type": "null"
}
],
- "title": "Vae Model",
- "description": "VAE model name/identifier"
+ "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"]
},
- "scheduler": {
+ "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"
@@ -61244,402 +67333,601 @@
"type": "null"
}
],
- "title": "Scheduler",
- "description": "Scheduler name"
+ "default": null,
+ "description": "Prompt",
+ "field_kind": "input",
+ "input": "any",
+ "orig_required": true,
+ "title": "Prompt"
},
- "steps": {
+ "seed": {
"anyOf": [
{
- "type": "integer",
- "minimum": 1.0
+ "type": "integer"
},
{
"type": "null"
}
],
- "title": "Steps",
- "description": "Number of generation steps"
+ "default": null,
+ "description": "Seed for random number generation",
+ "field_kind": "input",
+ "input": "any",
+ "orig_default": null,
+ "orig_required": false,
+ "title": "Seed"
},
- "refiner_steps": {
+ "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": "integer",
- "minimum": 0.0
+ "type": "string"
},
{
"type": "null"
}
],
- "title": "Refiner Steps",
- "description": "Number of refiner steps"
+ "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"
},
- "cfg_scale": {
+ "init_image": {
"anyOf": [
{
- "type": "number"
+ "$ref": "#/components/schemas/ImageField"
},
{
"type": "null"
}
],
- "title": "Cfg Scale",
- "description": "CFG scale for guidance"
+ "default": null,
+ "description": "Init image for img2img/inpaint",
+ "field_kind": "input",
+ "input": "any",
+ "orig_default": null,
+ "orig_required": false
},
- "cfg_rescale_multiplier": {
+ "mask_image": {
"anyOf": [
{
- "type": "number"
+ "$ref": "#/components/schemas/ImageField"
},
{
"type": "null"
}
],
- "title": "Cfg Rescale Multiplier",
- "description": "CFG rescale multiplier"
+ "default": null,
+ "description": "Mask image for inpaint",
+ "field_kind": "input",
+ "input": "any",
+ "orig_default": null,
+ "orig_required": false,
+ "ui_hidden": true
},
- "refiner_cfg_scale": {
+ "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": [
{
- "type": "number"
+ "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": "Refiner Cfg Scale",
- "description": "Refiner CFG scale"
+ "default": null,
+ "description": "The Segment Anything model to use (SAM or SAM2).",
+ "field_kind": "input",
+ "input": "any",
+ "orig_required": true,
+ "title": "Model"
},
- "guidance": {
+ "image": {
"anyOf": [
{
- "type": "number"
+ "$ref": "#/components/schemas/ImageField"
},
{
"type": "null"
}
],
- "title": "Guidance",
- "description": "Guidance scale"
+ "default": null,
+ "description": "The image to segment.",
+ "field_kind": "input",
+ "input": "any",
+ "orig_required": true
},
- "width": {
+ "bounding_boxes": {
"anyOf": [
{
- "type": "integer",
- "minimum": 64.0
+ "items": {
+ "$ref": "#/components/schemas/BoundingBoxField"
+ },
+ "type": "array"
},
{
"type": "null"
}
],
- "title": "Width",
- "description": "Image width in pixels"
+ "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"
+ },
+ "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.",
+ "field_kind": "input",
+ "input": "any",
+ "orig_default": null,
+ "orig_required": false,
+ "title": "Point Lists"
+ },
+ "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"
+ },
+ "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"
+ },
+ "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"
+ },
+ "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"
+ },
+ "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", "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", "completed", "failed", "canceled"],
+ "title": "Status",
+ "description": "The status of this queue item",
+ "default": "pending"
},
- "height": {
+ "status_sequence": {
"anyOf": [
{
- "type": "integer",
- "minimum": 64.0
+ "type": "integer"
},
{
"type": "null"
}
],
- "title": "Height",
- "description": "Image height in pixels"
+ "title": "Status Sequence",
+ "description": "A monotonically increasing version for this queue item's visible status lifecycle"
},
- "seed": {
- "anyOf": [
- {
- "type": "integer",
- "minimum": 0.0
- },
- {
- "type": "null"
- }
- ],
- "title": "Seed",
- "description": "Random seed"
+ "priority": {
+ "type": "integer",
+ "title": "Priority",
+ "description": "The priority of this queue item",
+ "default": 0
},
- "denoise_strength": {
+ "batch_id": {
+ "type": "string",
+ "title": "Batch Id",
+ "description": "The ID of the batch associated with this queue item"
+ },
+ "origin": {
"anyOf": [
{
- "type": "number",
- "maximum": 1.0,
- "minimum": 0.0
+ "type": "string"
},
{
"type": "null"
}
],
- "title": "Denoise Strength",
- "description": "Denoising strength"
+ "title": "Origin",
+ "description": "The origin of this queue item. This data is used by the frontend to determine how to handle results."
},
- "refiner_denoise_start": {
+ "destination": {
"anyOf": [
{
- "type": "number",
- "maximum": 1.0,
- "minimum": 0.0
+ "type": "string"
},
{
"type": "null"
}
],
- "title": "Refiner Denoise Start",
- "description": "Refiner denoising start"
+ "title": "Destination",
+ "description": "The origin of this queue item. This data is used by the frontend to determine how to handle results"
},
- "clip_skip": {
+ "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": "integer",
- "minimum": 0.0
+ "type": "string"
},
{
"type": "null"
}
],
- "title": "Clip Skip",
- "description": "CLIP skip layers"
+ "title": "Error Type",
+ "description": "The error type if this queue item errored"
},
- "seamless_x": {
+ "error_message": {
"anyOf": [
{
- "type": "boolean"
+ "type": "string"
},
{
"type": "null"
}
],
- "title": "Seamless X",
- "description": "Enable seamless X tiling"
+ "title": "Error Message",
+ "description": "The error message if this queue item errored"
},
- "seamless_y": {
+ "error_traceback": {
"anyOf": [
{
- "type": "boolean"
+ "type": "string"
},
{
"type": "null"
}
],
- "title": "Seamless Y",
- "description": "Enable seamless Y tiling"
+ "title": "Error Traceback",
+ "description": "The error traceback if this queue item errored"
},
- "refiner_positive_aesthetic_score": {
+ "created_at": {
"anyOf": [
{
- "type": "number"
+ "type": "string",
+ "format": "date-time"
},
{
- "type": "null"
+ "type": "string"
}
],
- "title": "Refiner Positive Aesthetic Score",
- "description": "Refiner positive aesthetic score"
+ "title": "Created At",
+ "description": "When this queue item was created"
},
- "refiner_negative_aesthetic_score": {
+ "updated_at": {
"anyOf": [
{
- "type": "number"
+ "type": "string",
+ "format": "date-time"
},
{
- "type": "null"
+ "type": "string"
}
],
- "title": "Refiner Negative Aesthetic Score",
- "description": "Refiner negative aesthetic score"
+ "title": "Updated At",
+ "description": "When this queue item was updated"
},
- "loras": {
+ "started_at": {
"anyOf": [
{
- "items": {
- "$ref": "#/components/schemas/LoRARecallParameter"
- },
- "type": "array"
+ "type": "string",
+ "format": "date-time"
},
{
- "type": "null"
- }
- ],
- "title": "Loras",
- "description": "List of LoRAs with their weights"
- },
- "control_layers": {
- "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": "Started At",
+ "description": "When this queue item was started"
},
- "ip_adapters": {
+ "completed_at": {
"anyOf": [
{
- "items": {
- "$ref": "#/components/schemas/IPAdapterRecallParameter"
- },
- "type": "array"
+ "type": "string",
+ "format": "date-time"
},
{
- "type": "null"
- }
- ],
- "title": "Ip Adapters",
- "description": "List of IP Adapters with their settings"
- },
- "reference_images": {
- "anyOf": [
- {
- "items": {
- "$ref": "#/components/schemas/ReferenceImageRecallParameter"
- },
- "type": "array"
+ "type": "string"
},
{
"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"
+ "title": "Completed At",
+ "description": "When this queue item was completed"
},
"queue_id": {
- "description": "The ID of the queue",
+ "type": "string",
"title": "Queue Id",
- "type": "string"
+ "description": "The id of the queue with which this item is associated"
},
"user_id": {
- "description": "The ID of the user whose recall parameters were updated",
+ "type": "string",
"title": "User Id",
- "type": "string"
- },
- "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": {
- "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"
+ "description": "The id of the user who created this queue item",
+ "default": "system"
},
- "width": {
+ "user_display_name": {
"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": "User Display Name",
+ "description": "The display name of the user who created this queue item, if available"
},
- "height": {
+ "user_email": {
"anyOf": [
{
- "type": "integer"
+ "type": "string"
},
{
"type": "null"
}
],
- "default": null,
- "description": "The height of the entire mask.",
- "field_kind": "input",
- "input": "any",
- "orig_required": true,
- "title": "Height"
+ "title": "User Email",
+ "description": "The email of the user who created this queue item, if available"
},
- "x_left": {
+ "field_values": {
"anyOf": [
{
- "type": "integer"
+ "items": {
+ "$ref": "#/components/schemas/NodeFieldValue"
+ },
+ "type": "array"
},
{
"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": "Field Values",
+ "description": "The field values that were used for this queue item"
},
- "y_top": {
+ "retried_from_item_id": {
"anyOf": [
{
"type": "integer"
@@ -61648,91 +67936,49 @@
"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": "Retried From Item Id",
+ "description": "The item_id of the queue item that this item was retried from"
},
- "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"
+ "session": {
+ "$ref": "#/components/schemas/GraphExecutionState",
+ "description": "The fully-populated session to be executed"
},
- "rectangle_height": {
+ "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"
@@ -61741,11 +67987,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"
@@ -61754,189 +67999,154 @@
"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"
- },
- "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"
- },
- "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": "Batch Id",
+ "description": "The current queue item's batch id"
},
- "latents": {
+ "session_id": {
"anyOf": [
{
- "$ref": "#/components/schemas/LatentsField"
+ "type": "string"
},
{
"type": "null"
}
],
- "default": null,
- "description": "Latents tensor",
- "field_kind": "input",
- "input": "connection",
- "orig_required": true
+ "title": "Session Id",
+ "description": "The current queue item's session id"
},
- "width": {
- "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"
+ "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'"
+ },
+ "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"
+ }
+ },
+ "type": "object",
+ "required": [
+ "queue_id",
+ "item_id",
+ "batch_id",
+ "session_id",
+ "pending",
+ "in_progress",
+ "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": {
@@ -61963,386 +68173,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": {
- "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": "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
- },
- "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",
+ "SigLIP_Diffusers_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
- },
- "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"
+ "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."
},
- "latents": {
- "anyOf": [
- {
- "$ref": "#/components/schemas/LatentsField"
- },
- {
- "type": "null"
- }
- ],
- "default": null,
- "description": "Latents tensor",
- "field_kind": "input",
- "input": "connection",
- "orig_default": null,
- "orig_required": false
+ "file_size": {
+ "type": "integer",
+ "title": "File Size",
+ "description": "The size of the model in bytes."
},
- "noise": {
- "anyOf": [
- {
- "$ref": "#/components/schemas/LatentsField"
- },
- {
- "type": "null"
- }
- ],
- "default": null,
- "description": "Noise tensor",
- "field_kind": "input",
- "input": "connection",
- "orig_default": null,
- "orig_required": false
+ "name": {
+ "type": "string",
+ "title": "Name",
+ "description": "Name of the model."
},
- "denoise_mask": {
+ "description": {
"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": "Description",
+ "description": "Model description"
},
- "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"
+ "source": {
+ "type": "string",
+ "title": "Source",
+ "description": "The original source of the model (path, URL or repo_id)."
},
- "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"
+ "source_type": {
+ "$ref": "#/components/schemas/ModelSourceType",
+ "description": "The type of source"
},
- "transformer": {
+ "source_api_response": {
"anyOf": [
{
- "$ref": "#/components/schemas/TransformerField"
+ "type": "string"
},
{
"type": "null"
}
],
- "default": null,
- "description": "SD3 model (MMDiTX) to load",
- "field_kind": "input",
- "input": "connection",
- "orig_required": true,
- "title": "Transformer"
+ "title": "Source Api Response",
+ "description": "The original API response from the source, as stringified JSON."
},
- "positive_conditioning": {
+ "source_url": {
"anyOf": [
{
- "$ref": "#/components/schemas/SD3ConditioningField"
+ "type": "string"
},
{
"type": "null"
}
],
- "default": null,
- "description": "Positive conditioning tensor",
- "field_kind": "input",
- "input": "connection",
- "orig_required": true
+ "title": "Source Url",
+ "description": "Optional URL for the model (e.g. download page or model page)."
},
- "negative_conditioning": {
+ "cover_image": {
"anyOf": [
{
- "$ref": "#/components/schemas/SD3ConditioningField"
+ "type": "string"
},
{
"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"
+ "title": "Cover Image",
+ "description": "Url for image to preview model"
},
- "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"
+ "format": {
+ "type": "string",
+ "const": "diffusers",
+ "title": "Format",
+ "default": "diffusers"
},
- "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"
+ "repo_variant": {
+ "$ref": "#/components/schemas/ModelRepoVariant",
+ "default": ""
},
- "seed": {
- "default": 0,
- "description": "Randomness seed for reproducibility.",
- "field_kind": "input",
- "input": "any",
- "orig_default": 0,
- "orig_required": false,
- "title": "Seed",
- "type": "integer"
+ "type": {
+ "type": "string",
+ "const": "siglip",
+ "title": "Type",
+ "default": "siglip"
},
- "type": {
- "const": "sd3_denoise",
- "default": "sd3_denoise",
- "field_kind": "node_attribute",
- "title": "type",
- "type": "string"
+ "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": ["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": {
@@ -62411,48 +68419,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": {
@@ -62511,920 +68553,550 @@
"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"
- },
- "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"
- },
- "target_width": {
- "default": 1024,
- "description": "",
- "field_kind": "input",
- "input": "any",
- "orig_default": 1024,
- "orig_required": false,
- "title": "Target Width",
- "type": "integer"
+ "title": "Hash",
+ "description": "The hash of the model file(s)."
},
- "target_height": {
- "default": 1024,
- "description": "",
- "field_kind": "input",
- "input": "any",
- "orig_default": 1024,
- "orig_required": false,
- "title": "Target Height",
- "type": "integer"
+ "path": {
+ "type": "string",
+ "title": "Path",
+ "description": "Path to the model on the filesystem. Relative paths are relative to the Invoke root directory."
},
- "clip": {
- "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 1"
+ "file_size": {
+ "type": "integer",
+ "title": "File Size",
+ "description": "The size of the model in bytes."
},
- "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,
- "title": "CLIP 2"
+ "name": {
+ "type": "string",
+ "title": "Name",
+ "description": "Name of the model."
},
- "mask": {
+ "description": {
"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
- },
- "type": {
- "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"
+ "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"
},
- "loras": {
+ "source_api_response": {
"anyOf": [
{
- "$ref": "#/components/schemas/LoRAField"
- },
- {
- "items": {
- "$ref": "#/components/schemas/LoRAField"
- },
- "type": "array"
+ "type": "string"
},
{
"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"
+ "title": "Source Api Response",
+ "description": "The original API response from the source, as stringified JSON."
},
- "unet": {
+ "source_url": {
"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 Url",
+ "description": "Optional URL for the model (e.g. download page or model page)."
},
- "clip": {
+ "cover_image": {
"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": "Cover Image",
+ "description": "Url for image to preview model"
},
- "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_default": null,
- "orig_required": false,
- "title": "CLIP 2"
+ "base": {
+ "type": "string",
+ "const": "any",
+ "title": "Base",
+ "default": "any"
+ },
+ "type": {
+ "type": "string",
+ "const": "spandrel_image_to_image",
+ "title": "Type",
+ "default": "spandrel_image_to_image"
+ },
+ "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",
+ "base",
+ "type",
+ "format"
+ ],
+ "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"
+ },
+ "StarredVideosResult": {
+ "properties": {
+ "affected_boards": {
+ "items": {
+ "type": "string"
+ },
+ "type": "array",
+ "title": "Affected Boards",
+ "description": "The ids of boards affected by the operation"
},
- "type": {
- "const": "sdxl_lora_collection_loader",
- "default": "sdxl_lora_collection_loader",
- "field_kind": "node_attribute",
- "title": "type",
- "type": "string"
+ "starred_videos": {
+ "items": {
+ "type": "string"
+ },
+ "type": "array",
+ "title": "Starred Videos",
+ "description": "The names of the videos that were starred"
}
},
- "required": ["type", "id"],
- "tags": ["model"],
- "title": "Apply LoRA Collection - SDXL",
"type": "object",
- "version": "1.1.2",
- "output": {
- "$ref": "#/components/schemas/SDXLLoRALoaderOutput"
- }
+ "required": ["affected_boards", "starred_videos"],
+ "title": "StarredVideosResult"
},
- "SDXLLoRALoaderInvocation": {
- "category": "model",
- "class": "invocation",
- "classification": "stable",
- "description": "Apply selected lora to unet and text_encoder.",
- "node_pack": "invokeai",
+ "StarterModel": {
"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": {
+ "type": "string",
+ "title": "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"
},
- "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"
},
- "lora": {
+ "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",
- "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"
+ "previous_names": {
+ "items": {
+ "type": "string"
+ },
+ "type": "array",
+ "title": "Previous Names",
+ "default": []
},
- "model": {
+ "dependencies": {
"anyOf": [
{
- "$ref": "#/components/schemas/ModelIdentifierField"
+ "items": {
+ "$ref": "#/components/schemas/StarterModelWithoutDependencies"
+ },
+ "type": "array"
},
{
"type": "null"
}
],
- "default": null,
- "description": "SDXL Main model (UNet, VAE, CLIP1, CLIP2) to load",
- "field_kind": "input",
- "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"
+ "title": "Dependencies"
}
},
- "required": ["type", "id"],
- "tags": ["model", "sdxl"],
- "title": "Main Model - SDXL",
"type": "object",
- "version": "1.0.4",
- "output": {
- "$ref": "#/components/schemas/SDXLModelLoaderOutput"
- }
+ "required": ["description", "source", "name", "base", "type"],
+ "title": "StarterModel"
},
- "SDXLModelLoaderOutput": {
- "class": "output",
- "description": "SDXL base model loader output",
+ "StarterModelBundle": {
"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
+ "name": {
+ "type": "string",
+ "title": "Name"
},
- "type": {
- "const": "sdxl_model_loader_output",
- "default": "sdxl_model_loader_output",
- "field_kind": "node_attribute",
- "title": "type",
- "type": "string"
+ "models": {
+ "items": {
+ "$ref": "#/components/schemas/StarterModel"
+ },
+ "type": "array",
+ "title": "Models"
}
},
- "required": ["output_meta", "unet", "clip", "clip2", "vae", "type", "type"],
- "title": "SDXLModelLoaderOutput",
- "type": "object"
+ "type": "object",
+ "required": ["name", "models"],
+ "title": "StarterModelBundle"
},
- "SDXLRefinerCompelPromptInvocation": {
- "category": "prompt",
- "class": "invocation",
- "classification": "stable",
- "description": "Parse prompt using compel package to conditioning.",
- "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"
- },
- "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"
+ "starter_models": {
+ "items": {
+ "$ref": "#/components/schemas/StarterModel"
+ },
+ "type": "array",
+ "title": "Starter Models"
},
- "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",
+ "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",
- "ui_component": "textarea"
- },
- "original_width": {
- "default": 1024,
- "description": "",
- "field_kind": "input",
- "input": "any",
- "orig_default": 1024,
- "orig_required": false,
- "title": "Original Width",
- "type": "integer"
+ "title": "Description"
},
- "original_height": {
- "default": 1024,
- "description": "",
- "field_kind": "input",
- "input": "any",
- "orig_default": 1024,
- "orig_required": false,
- "title": "Original Height",
- "type": "integer"
+ "source": {
+ "type": "string",
+ "title": "Source"
},
- "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"
},
- "crop_left": {
- "default": 0,
- "description": "",
- "field_kind": "input",
- "input": "any",
- "orig_default": 0,
- "orig_required": false,
- "title": "Crop Left",
- "type": "integer"
+ "base": {
+ "$ref": "#/components/schemas/BaseModelType"
},
- "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"
+ "type": {
+ "$ref": "#/components/schemas/ModelType"
},
- "clip2": {
+ "format": {
"anyOf": [
{
- "$ref": "#/components/schemas/CLIPField"
+ "$ref": "#/components/schemas/ModelFormat"
},
{
"type": "null"
}
- ],
- "default": null,
- "description": "CLIP (tokenizer, text encoder, LoRAs) and skipped layer count",
- "field_kind": "input",
- "input": "connection",
- "orig_required": true
+ ]
},
- "type": {
- "const": "sdxl_refiner_compel_prompt",
- "default": "sdxl_refiner_compel_prompt",
- "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"
- }
- },
- "SDXLRefinerModelLoaderInvocation": {
- "category": "model",
- "class": "invocation",
- "classification": "stable",
- "description": "Loads an sdxl refiner 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"
+ "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"
},
- "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",
+ "is_installed": {
"type": "boolean",
- "ui_hidden": false,
- "ui_type": "IsIntermediate"
+ "title": "Is Installed",
+ "default": false
},
- "use_cache": {
- "default": true,
- "description": "Whether or not to use the cache",
- "field_kind": "node_attribute",
- "title": "Use Cache",
- "type": "boolean"
+ "capabilities": {
+ "anyOf": [
+ {
+ "$ref": "#/components/schemas/ExternalModelCapabilities"
+ },
+ {
+ "type": "null"
+ }
+ ]
},
- "model": {
+ "default_settings": {
"anyOf": [
{
- "$ref": "#/components/schemas/ModelIdentifierField"
+ "$ref": "#/components/schemas/ExternalApiModelDefaultSettings"
},
{
"type": "null"
}
- ],
- "default": null,
- "description": "SDXL Refiner Main Modde (UNet, VAE, CLIP2) to load",
- "field_kind": "input",
- "input": "any",
- "orig_required": true,
- "ui_model_base": ["sdxl-refiner"],
- "ui_model_type": ["main"]
+ ]
},
- "type": {
- "const": "sdxl_refiner_model_loader",
- "default": "sdxl_refiner_model_loader",
- "field_kind": "node_attribute",
- "title": "type",
- "type": "string"
+ "panel_schema": {
+ "anyOf": [
+ {
+ "$ref": "#/components/schemas/ExternalModelPanelSchema"
+ },
+ {
+ "type": "null"
+ }
+ ]
+ },
+ "previous_names": {
+ "items": {
+ "type": "string"
+ },
+ "type": "array",
+ "title": "Previous Names",
+ "default": []
}
},
- "required": ["type", "id"],
- "tags": ["model", "sdxl", "refiner"],
- "title": "Refiner Model - SDXL",
"type": "object",
- "version": "1.0.4",
- "output": {
- "$ref": "#/components/schemas/SDXLRefinerModelLoaderOutput"
- }
+ "required": ["description", "source", "name", "base", "type"],
+ "title": "StarterModelWithoutDependencies"
},
- "SDXLRefinerModelLoaderOutput": {
+ "String2Output": {
"class": "output",
- "description": "SDXL refiner model loader output",
+ "description": "Base class for invocations that output two 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",
+ "string_1": {
+ "description": "string 1",
"field_kind": "output",
- "title": "CLIP 2",
+ "title": "String 1",
+ "type": "string",
"ui_hidden": false
},
- "vae": {
- "$ref": "#/components/schemas/VAEField",
- "description": "VAE",
+ "string_2": {
+ "description": "string 2",
"field_kind": "output",
- "title": "VAE",
+ "title": "String 2",
+ "type": "string",
"ui_hidden": false
},
"type": {
- "const": "sdxl_refiner_model_loader_output",
- "default": "sdxl_refiner_model_loader_output",
+ "const": "string_2_output",
+ "default": "string_2_output",
"field_kind": "node_attribute",
"title": "type",
"type": "string"
}
},
- "required": ["output_meta", "unet", "clip2", "vae", "type", "type"],
- "title": "SDXLRefinerModelLoaderOutput",
+ "required": ["output_meta", "string_1", "string_2", "type", "type"],
+ "title": "String2Output",
"type": "object"
},
- "SQLiteDirection": {
- "type": "string",
- "enum": ["ASC", "DESC"],
- "title": "SQLiteDirection"
- },
- "SaveImageInvocation": {
- "category": "image",
+ "StringBatchInvocation": {
+ "category": "batch",
"class": "invocation",
- "classification": "stable",
- "description": "Saves an image. Unlike an image primitive, this invocation stores a copy of the image.",
+ "classification": "special",
+ "description": "Create a batched generation, where the workflow is executed once for each string in the batch.",
"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",
@@ -63443,49 +69115,65 @@
"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": {
+ "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/ImageField"
+ "items": {
+ "type": "string"
+ },
+ "minItems": 1,
+ "type": "array"
},
{
"type": "null"
}
],
"default": null,
- "description": "The image to process",
+ "description": "The strings to batch over",
"field_kind": "input",
"input": "any",
- "orig_required": true
+ "orig_required": true,
+ "title": "Strings"
},
"type": {
- "const": "save_image",
- "default": "save_image",
+ "const": "string_batch",
+ "default": "string_batch",
"field_kind": "node_attribute",
"title": "type",
"type": "string"
}
},
"required": ["type", "id"],
- "tags": ["primitives", "image"],
- "title": "Save Image",
+ "tags": ["primitives", "string", "batch", "special"],
+ "title": "String Batch",
"type": "object",
- "version": "1.2.2",
+ "version": "1.0.0",
"output": {
- "$ref": "#/components/schemas/ImageOutput"
+ "$ref": "#/components/schemas/StringOutput"
}
},
- "ScaleLatentsInvocation": {
- "category": "latents",
+ "StringCollectionInvocation": {
+ "category": "primitives",
"class": "invocation",
"classification": "stable",
- "description": "Scales latents by a given factor.",
+ "description": "A collection of string primitive values",
"node_pack": "invokeai",
"properties": {
"id": {
@@ -63512,81 +69200,67 @@
"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",
- "field_kind": "input",
- "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": "Mode",
- "type": "string"
- },
- "antialias": {
- "default": false,
- "description": "Whether or not to apply antialiasing (bilinear or bicubic only)",
+ "collection": {
+ "default": [],
+ "description": "The collection of string values",
"field_kind": "input",
"input": "any",
- "orig_default": false,
+ "items": {
+ "type": "string"
+ },
+ "orig_default": [],
"orig_required": false,
- "title": "Antialias",
- "type": "boolean"
+ "title": "Collection",
+ "type": "array"
},
"type": {
- "const": "lscale",
- "default": "lscale",
+ "const": "string_collection",
+ "default": "string_collection",
"field_kind": "node_attribute",
"title": "type",
"type": "string"
}
},
"required": ["type", "id"],
- "tags": ["latents", "resize"],
- "title": "Scale Latents",
+ "tags": ["primitives", "string", "collection"],
+ "title": "String Collection Primitive",
"type": "object",
"version": "1.0.2",
"output": {
- "$ref": "#/components/schemas/LatentsOutput"
+ "$ref": "#/components/schemas/StringCollectionOutput"
}
},
- "SchedulerInvocation": {
- "category": "latents",
+ "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": "string_collection_output",
+ "default": "string_collection_output",
+ "field_kind": "node_attribute",
+ "title": "type",
+ "type": "string"
+ }
+ },
+ "required": ["output_meta", "collection", "type", "type"],
+ "title": "StringCollectionOutput",
+ "type": "object"
+ },
+ "StringGenerator": {
+ "category": "batch",
"class": "invocation",
- "classification": "stable",
- "description": "Selects a scheduler.",
+ "classification": "special",
+ "description": "Generated a range of strings for use in a batched generation",
"node_pack": "invokeai",
"properties": {
"id": {
@@ -63613,134 +69287,67 @@
"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"
- ],
+ "generator": {
+ "$ref": "#/components/schemas/StringGeneratorField",
+ "description": "The string generator.",
"field_kind": "input",
- "input": "any",
- "orig_default": "euler",
- "orig_required": false,
- "title": "Scheduler",
- "type": "string",
- "ui_type": "SchedulerField"
+ "input": "direct",
+ "orig_required": true,
+ "title": "Generator Type"
},
"type": {
- "const": "scheduler",
- "default": "scheduler",
+ "const": "string_generator",
+ "default": "string_generator",
"field_kind": "node_attribute",
"title": "type",
"type": "string"
}
},
- "required": ["type", "id"],
- "tags": ["scheduler"],
- "title": "Scheduler",
+ "required": ["generator", "type", "id"],
+ "tags": ["primitives", "string", "number", "batch", "special"],
+ "title": "String Generator",
"type": "object",
"version": "1.0.0",
"output": {
- "$ref": "#/components/schemas/SchedulerOutput"
+ "$ref": "#/components/schemas/StringGeneratorOutput"
}
},
- "SchedulerOutput": {
+ "StringGeneratorField": {
+ "properties": {},
+ "title": "StringGeneratorField",
+ "type": "object"
+ },
+ "StringGeneratorOutput": {
"class": "output",
+ "description": "Base class for nodes that output a collection of strings",
"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"
- ],
+ "strings": {
+ "description": "The generated strings",
"field_kind": "output",
- "title": "Scheduler",
- "type": "string",
- "ui_hidden": false,
- "ui_type": "SchedulerField"
+ "items": {
+ "type": "string"
+ },
+ "title": "Strings",
+ "type": "array",
+ "ui_hidden": false
},
"type": {
- "const": "scheduler_output",
- "default": "scheduler_output",
+ "const": "string_generator_output",
+ "default": "string_generator_output",
"field_kind": "node_attribute",
"title": "type",
"type": "string"
}
},
- "required": ["output_meta", "scheduler", "type", "type"],
- "title": "SchedulerOutput",
+ "required": ["output_meta", "strings", "type", "type"],
+ "title": "StringGeneratorOutput",
"type": "object"
},
- "SchedulerPredictionType": {
- "type": "string",
- "enum": ["epsilon", "v_prediction", "sample"],
- "title": "SchedulerPredictionType",
- "description": "Scheduler prediction type."
- },
- "Sd3ModelLoaderInvocation": {
- "category": "model",
+ "StringInvocation": {
+ "category": "primitives",
"class": "invocation",
"classification": "stable",
- "description": "Loads a SD3 base model, outputting its submodels.",
+ "description": "A string primitive value",
"node_pack": "invokeai",
"properties": {
"id": {
@@ -63762,168 +69369,44 @@
},
"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": "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",
- "field_kind": "input",
- "input": "direct",
- "orig_default": null,
- "orig_required": false,
- "title": "T5 Encoder",
- "ui_model_type": ["t5_encoder"]
- },
- "clip_l_model": {
- "anyOf": [
- {
- "$ref": "#/components/schemas/ModelIdentifierField"
- },
- {
- "type": "null"
- }
- ],
- "default": null,
- "description": "CLIP Embed loader",
- "field_kind": "input",
- "input": "direct",
- "orig_default": null,
- "orig_required": false,
- "title": "CLIP L Encoder",
- "ui_model_type": ["clip_embed"],
- "ui_model_variant": ["large"]
- },
- "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"]
+ "description": "Whether or not to use the cache",
+ "field_kind": "node_attribute",
+ "title": "Use Cache",
+ "type": "boolean"
},
- "vae_model": {
- "anyOf": [
- {
- "$ref": "#/components/schemas/ModelIdentifierField"
- },
- {
- "type": "null"
- }
- ],
- "default": null,
- "description": "VAE model to load",
+ "value": {
+ "default": "",
+ "description": "The string value",
"field_kind": "input",
"input": "any",
- "orig_default": null,
+ "orig_default": "",
"orig_required": false,
- "title": "VAE",
- "ui_model_base": ["sd-3"],
- "ui_model_type": ["vae"]
+ "title": "Value",
+ "type": "string",
+ "ui_component": "textarea"
},
"type": {
- "const": "sd3_model_loader",
- "default": "sd3_model_loader",
+ "const": "string",
+ "default": "string",
"field_kind": "node_attribute",
"title": "type",
"type": "string"
}
},
- "required": ["model", "type", "id"],
- "tags": ["model", "sd3"],
- "title": "Main Model - SD3",
+ "required": ["type", "id"],
+ "tags": ["primitives", "string"],
+ "title": "String Primitive",
"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",
+ "StringJoinInvocation": {
+ "category": "strings",
"class": "invocation",
"classification": "stable",
- "description": "Encodes and preps a prompt for a SD3 image.",
+ "description": "Joins string left to string right",
"node_pack": "invokeai",
"properties": {
"id": {
@@ -63950,93 +69433,50 @@
"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_left": {
+ "default": "",
+ "description": "String Left",
"field_kind": "input",
- "input": "connection",
- "orig_default": null,
+ "input": "any",
+ "orig_default": "",
"orig_required": false,
- "title": "T5Encoder"
+ "title": "String Left",
+ "type": "string",
+ "ui_component": "textarea"
},
- "prompt": {
- "anyOf": [
- {
- "type": "string"
- },
- {
- "type": "null"
- }
- ],
- "default": null,
- "description": "Text prompt to encode.",
+ "string_right": {
+ "default": "",
+ "description": "String Right",
"field_kind": "input",
"input": "any",
- "orig_required": true,
- "title": "Prompt"
+ "orig_default": "",
+ "orig_required": false,
+ "title": "String Right",
+ "type": "string",
+ "ui_component": "textarea"
},
"type": {
- "const": "sd3_text_encoder",
- "default": "sd3_text_encoder",
+ "const": "string_join",
+ "default": "string_join",
"field_kind": "node_attribute",
"title": "type",
"type": "string"
}
},
"required": ["type", "id"],
- "tags": ["prompt", "conditioning", "sd3"],
- "title": "Prompt - SD3",
+ "tags": ["string", "join"],
+ "title": "String Join",
"type": "object",
"version": "1.0.1",
"output": {
- "$ref": "#/components/schemas/SD3ConditioningOutput"
+ "$ref": "#/components/schemas/StringOutput"
}
},
- "SeamlessModeInvocation": {
- "category": "model",
+ "StringJoinThreeInvocation": {
+ "category": "strings",
"class": "invocation",
"classification": "stable",
- "description": "Applies the seamless transformation to the Model UNet and VAE.",
+ "description": "Joins string left to string middle to string right",
"node_pack": "invokeai",
"properties": {
"id": {
@@ -64063,162 +69503,116 @@
"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",
+ "string_left": {
+ "default": "",
+ "description": "String Left",
"field_kind": "input",
- "input": "connection",
- "orig_default": null,
+ "input": "any",
+ "orig_default": "",
"orig_required": false,
- "title": "VAE"
+ "title": "String Left",
+ "type": "string",
+ "ui_component": "textarea"
},
- "seamless_y": {
- "default": true,
- "description": "Specify whether Y axis is seamless",
+ "string_middle": {
+ "default": "",
+ "description": "String Middle",
"field_kind": "input",
"input": "any",
- "orig_default": true,
+ "orig_default": "",
"orig_required": false,
- "title": "Seamless Y",
- "type": "boolean"
+ "title": "String Middle",
+ "type": "string",
+ "ui_component": "textarea"
},
- "seamless_x": {
- "default": true,
- "description": "Specify whether X axis is seamless",
+ "string_right": {
+ "default": "",
+ "description": "String Right",
"field_kind": "input",
"input": "any",
- "orig_default": true,
+ "orig_default": "",
"orig_required": false,
- "title": "Seamless X",
- "type": "boolean"
+ "title": "String Right",
+ "type": "string",
+ "ui_component": "textarea"
},
"type": {
- "const": "seamless",
- "default": "seamless",
+ "const": "string_join_three",
+ "default": "string_join_three",
"field_kind": "node_attribute",
"title": "type",
"type": "string"
}
},
"required": ["type", "id"],
- "tags": ["seamless", "model"],
- "title": "Apply Seamless - SD1.5, SDXL",
+ "tags": ["string", "join"],
+ "title": "String Join Three",
"type": "object",
- "version": "1.0.2",
+ "version": "1.0.1",
"output": {
- "$ref": "#/components/schemas/SeamlessModeOutput"
+ "$ref": "#/components/schemas/StringOutput"
}
},
- "SeamlessModeOutput": {
+ "StringOutput": {
"class": "output",
- "description": "Modified Seamless Model output",
+ "description": "Base class for nodes that output a single string",
"properties": {
- "unet": {
- "anyOf": [
- {
- "$ref": "#/components/schemas/UNetField"
- },
- {
- "type": "null"
- }
- ],
- "default": null,
- "description": "UNet (scheduler, LoRAs)",
+ "value": {
+ "description": "The output string",
"field_kind": "output",
- "title": "UNet",
+ "title": "Value",
+ "type": "string",
"ui_hidden": false
},
- "vae": {
- "anyOf": [
- {
- "$ref": "#/components/schemas/VAEField"
- },
- {
- "type": "null"
- }
- ],
- "default": null,
- "description": "VAE",
+ "type": {
+ "const": "string_output",
+ "default": "string_output",
+ "field_kind": "node_attribute",
+ "title": "type",
+ "type": "string"
+ }
+ },
+ "required": ["output_meta", "value", "type", "type"],
+ "title": "StringOutput",
+ "type": "object"
+ },
+ "StringPosNegOutput": {
+ "class": "output",
+ "description": "Base class for invocations that output a positive and negative string",
+ "properties": {
+ "positive_string": {
+ "description": "Positive string",
"field_kind": "output",
- "title": "VAE",
+ "title": "Positive String",
+ "type": "string",
+ "ui_hidden": false
+ },
+ "negative_string": {
+ "description": "Negative string",
+ "field_kind": "output",
+ "title": "Negative String",
+ "type": "string",
"ui_hidden": false
},
"type": {
- "const": "seamless_output",
- "default": "seamless_output",
+ "const": "string_pos_neg_output",
+ "default": "string_pos_neg_output",
"field_kind": "node_attribute",
"title": "type",
"type": "string"
}
},
- "required": ["output_meta", "unet", "vae", "type", "type"],
- "title": "SeamlessModeOutput",
+ "required": ["output_meta", "positive_string", "negative_string", "type", "type"],
+ "title": "StringPosNegOutput",
"type": "object"
},
- "SeedreamImageGenerationInvocation": {
- "category": "image",
+ "StringReplaceInvocation": {
+ "category": "strings",
"class": "invocation",
"classification": "stable",
- "description": "Generate images using a BytePlus Seedream model.",
+ "description": "Replaces the search string with the replace string",
"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",
@@ -64240,211 +69634,74 @@
"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": ["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,
- "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"
- },
- {
- "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"
- },
- "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
- },
- "mask_image": {
- "anyOf": [
- {
- "$ref": "#/components/schemas/ImageField"
- },
- {
- "type": "null"
- }
- ],
- "default": null,
- "description": "Mask image for inpaint",
+ "title": "Use Cache",
+ "type": "boolean"
+ },
+ "string": {
+ "default": "",
+ "description": "String to work on",
"field_kind": "input",
"input": "any",
- "orig_default": null,
+ "orig_default": "",
"orig_required": false,
- "ui_hidden": true
+ "title": "String",
+ "type": "string",
+ "ui_component": "textarea"
},
- "reference_images": {
- "default": [],
- "description": "Reference images",
+ "search_string": {
+ "default": "",
+ "description": "String to search for",
"field_kind": "input",
"input": "any",
- "items": {
- "$ref": "#/components/schemas/ImageField"
- },
- "orig_default": [],
+ "orig_default": "",
"orig_required": false,
- "title": "Reference Images",
- "type": "array"
+ "title": "Search String",
+ "type": "string",
+ "ui_component": "textarea"
},
- "watermark": {
- "default": false,
- "description": "Add watermark to generated images",
+ "replace_string": {
+ "default": "",
+ "description": "String to replace the search",
"field_kind": "input",
"input": "any",
- "orig_default": false,
+ "orig_default": "",
"orig_required": false,
- "title": "Watermark",
- "type": "boolean"
+ "title": "Replace String",
+ "type": "string",
+ "ui_component": "textarea"
},
- "optimize_prompt": {
+ "use_regex": {
"default": false,
- "description": "Let the model optimize the prompt before generation",
+ "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": "Optimize Prompt",
+ "title": "Use Regex",
"type": "boolean"
},
"type": {
- "const": "seedream_image_generation",
- "default": "seedream_image_generation",
+ "const": "string_replace",
+ "default": "string_replace",
"field_kind": "node_attribute",
"title": "type",
"type": "string"
}
},
"required": ["type", "id"],
- "tags": ["external", "generation", "seedream"],
- "title": "Seedream Image Generation",
+ "tags": ["string", "replace", "regex"],
+ "title": "String Replace",
"type": "object",
- "version": "1.1.0",
+ "version": "1.0.1",
"output": {
- "$ref": "#/components/schemas/ImageCollectionOutput"
+ "$ref": "#/components/schemas/StringOutput"
}
},
- "SegmentAnythingInvocation": {
- "category": "segmentation",
+ "StringSplitInvocation": {
+ "category": "strings",
"class": "invocation",
"classification": "stable",
- "description": "Runs a Segment Anything Model (SAM or SAM2).",
+ "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": {
@@ -64471,553 +69728,599 @@
"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"
- }
- ],
- "default": null,
- "description": "The Segment Anything model to use (SAM or SAM2).",
- "field_kind": "input",
- "input": "any",
- "orig_required": true,
- "title": "Model"
- },
- "image": {
- "anyOf": [
- {
- "$ref": "#/components/schemas/ImageField"
- },
- {
- "type": "null"
- }
- ],
- "default": null,
- "description": "The image to segment.",
- "field_kind": "input",
- "input": "any",
- "orig_required": true
- },
- "bounding_boxes": {
- "anyOf": [
- {
- "items": {
- "$ref": "#/components/schemas/BoundingBoxField"
- },
- "type": "array"
- },
- {
- "type": "null"
- }
- ],
- "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"
- },
- "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.",
- "field_kind": "input",
- "input": "any",
- "orig_default": null,
- "orig_required": false,
- "title": "Point Lists"
- },
- "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).",
+ "string": {
+ "default": "",
+ "description": "String to split",
"field_kind": "input",
"input": "any",
- "orig_default": true,
+ "orig_default": "",
"orig_required": false,
- "title": "Apply Polygon Refinement",
- "type": "boolean"
+ "title": "String",
+ "type": "string",
+ "ui_component": "textarea"
},
- "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"],
+ "delimiter": {
+ "default": "",
+ "description": "Delimiter to spilt with. blank will split on the first whitespace",
"field_kind": "input",
"input": "any",
- "orig_default": "all",
+ "orig_default": "",
"orig_required": false,
- "title": "Mask Filter",
+ "title": "Delimiter",
"type": "string"
},
"type": {
- "const": "segment_anything",
- "default": "segment_anything",
+ "const": "string_split",
+ "default": "string_split",
"field_kind": "node_attribute",
"title": "type",
"type": "string"
}
},
"required": ["type", "id"],
- "tags": ["prompt", "segmentation", "sam", "sam2"],
- "title": "Segment Anything",
+ "tags": ["string", "split"],
+ "title": "String Split",
"type": "object",
- "version": "1.3.0",
+ "version": "1.0.1",
"output": {
- "$ref": "#/components/schemas/MaskOutput"
+ "$ref": "#/components/schemas/String2Output"
}
},
- "SessionProcessorStatus": {
+ "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",
"properties": {
- "is_started": {
- "type": "boolean",
- "title": "Is Started",
- "description": "Whether the session processor is started"
+ "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_processing": {
+ "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": "Is Processing",
- "description": "Whether a session is being processed"
+ "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 split",
+ "field_kind": "input",
+ "input": "any",
+ "orig_default": "",
+ "orig_required": false,
+ "title": "String",
+ "type": "string",
+ "ui_component": "textarea"
+ },
+ "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",
- "required": ["is_started", "is_processing"],
- "title": "SessionProcessorStatus"
+ "version": "1.0.1",
+ "output": {
+ "$ref": "#/components/schemas/StringPosNegOutput"
+ }
},
- "SessionQueueAndProcessorStatus": {
+ "StylePresetField": {
+ "description": "A style preset primitive field",
"properties": {
- "queue": {
- "$ref": "#/components/schemas/SessionQueueStatus"
- },
- "processor": {
- "$ref": "#/components/schemas/SessionProcessorStatus"
+ "style_preset_id": {
+ "description": "The id of the style preset",
+ "title": "Style Preset Id",
+ "type": "string"
}
},
- "type": "object",
- "required": ["queue", "processor"],
- "title": "SessionQueueAndProcessorStatus",
- "description": "The overall status of session queue and processor"
+ "required": ["style_preset_id"],
+ "title": "StylePresetField",
+ "type": "object"
},
- "SessionQueueCountsByDestination": {
+ "StylePresetRecordWithImage": {
"properties": {
- "queue_id": {
- "type": "string",
- "title": "Queue Id",
- "description": "The ID of the queue"
- },
- "destination": {
+ "name": {
"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"
- },
- "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"
+ "title": "Name",
+ "description": "The name of the style preset."
},
- "canceled": {
- "type": "integer",
- "title": "Canceled",
- "description": "Number of queue items with status 'canceled' for the destination"
+ "preset_data": {
+ "$ref": "#/components/schemas/PresetData",
+ "description": "The preset data"
},
- "total": {
- "type": "integer",
- "title": "Total",
- "description": "Total number of queue items for the destination"
- }
- },
- "type": "object",
- "required": ["queue_id", "destination", "pending", "in_progress", "completed", "failed", "canceled", "total"],
- "title": "SessionQueueCountsByDestination"
- },
- "SessionQueueItem": {
- "properties": {
- "item_id": {
- "type": "integer",
- "title": "Item Id",
- "description": "The identifier of the session queue item"
+ "type": {
+ "$ref": "#/components/schemas/PresetType",
+ "description": "The type of style preset"
},
- "status": {
+ "id": {
"type": "string",
- "enum": ["pending", "in_progress", "completed", "failed", "canceled"],
- "title": "Status",
- "description": "The status of this queue item",
- "default": "pending"
+ "title": "Id",
+ "description": "The style preset ID."
},
- "status_sequence": {
+ "image": {
"anyOf": [
{
- "type": "integer"
+ "type": "string"
},
{
"type": "null"
}
],
- "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": {
+ "title": "Image",
+ "description": "The path for image"
+ }
+ },
+ "type": "object",
+ "required": ["name", "preset_data", "type", "id", "image"],
+ "title": "StylePresetRecordWithImage"
+ },
+ "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": {
+ "path_or_prefix": {
"type": "string",
- "title": "Batch Id",
- "description": "The ID of the batch associated with this queue item"
+ "title": "Path Or Prefix"
},
- "origin": {
+ "model_type": {
+ "$ref": "#/components/schemas/ModelType"
+ },
+ "variant": {
"anyOf": [
{
- "type": "string"
+ "$ref": "#/components/schemas/ModelVariantType"
},
{
- "type": "null"
- }
- ],
- "title": "Origin",
- "description": "The origin of this queue item. This data is used by the frontend to determine how to handle results."
- },
- "destination": {
- "anyOf": [
+ "$ref": "#/components/schemas/ClipVariantType"
+ },
{
- "type": "string"
+ "$ref": "#/components/schemas/FluxVariantType"
},
{
- "type": "null"
- }
- ],
- "title": "Destination",
- "description": "The origin of this queue item. This data is used by the frontend to determine how to handle results"
- },
- "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": [
+ "$ref": "#/components/schemas/Flux2VariantType"
+ },
{
- "type": "string"
+ "$ref": "#/components/schemas/ZImageVariantType"
},
{
- "type": "null"
- }
- ],
- "title": "Error Type",
- "description": "The error type if this queue item errored"
- },
- "error_message": {
- "anyOf": [
+ "$ref": "#/components/schemas/QwenImageVariantType"
+ },
{
- "type": "string"
+ "$ref": "#/components/schemas/WanVariantType"
},
{
- "type": "null"
- }
- ],
- "title": "Error Message",
- "description": "The error message if this queue item errored"
- },
- "error_traceback": {
- "anyOf": [
+ "$ref": "#/components/schemas/WanLoRAVariantType"
+ },
{
- "type": "string"
+ "$ref": "#/components/schemas/Qwen3VariantType"
},
{
"type": "null"
}
],
- "title": "Error Traceback",
- "description": "The error traceback if this queue item errored"
+ "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",
+ "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": "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."
},
- "created_at": {
- "anyOf": [
- {
- "type": "string",
- "format": "date-time"
- },
- {
- "type": "string"
- }
- ],
- "title": "Created At",
- "description": "When this queue item was created"
+ "t2i_adapter_model": {
+ "$ref": "#/components/schemas/ModelIdentifierField",
+ "description": "The T2I-Adapter model to use."
},
- "updated_at": {
+ "weight": {
"anyOf": [
{
- "type": "string",
- "format": "date-time"
+ "type": "number"
},
{
- "type": "string"
+ "items": {
+ "type": "number"
+ },
+ "type": "array"
}
],
- "title": "Updated At",
- "description": "When this queue item was updated"
+ "default": 1,
+ "description": "The weight given to the T2I-Adapter",
+ "title": "Weight"
},
- "started_at": {
- "anyOf": [
- {
- "type": "string",
- "format": "date-time"
- },
- {
- "type": "string"
- },
- {
- "type": "null"
- }
- ],
- "title": "Started At",
- "description": "When this queue item was started"
+ "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"
},
- "completed_at": {
- "anyOf": [
- {
- "type": "string",
- "format": "date-time"
- },
- {
- "type": "string"
- },
- {
- "type": "null"
- }
- ],
- "title": "Completed At",
- "description": "When this queue item was completed"
+ "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"
},
- "queue_id": {
- "type": "string",
- "title": "Queue Id",
- "description": "The id of the queue with which this item is associated"
+ "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": "T2IAdapterField",
+ "type": "object"
+ },
+ "T2IAdapterInvocation": {
+ "category": "conditioning",
+ "class": "invocation",
+ "classification": "stable",
+ "description": "Collects T2I-Adapter info to pass to other nodes.",
+ "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"
},
- "user_id": {
- "type": "string",
- "title": "User Id",
- "description": "The id of the user who created this queue item",
- "default": "system"
+ "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"
},
- "user_display_name": {
+ "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": "User Display Name",
- "description": "The display name of the user who created this queue item, if available"
+ "default": null,
+ "description": "The IP-Adapter image prompt.",
+ "field_kind": "input",
+ "input": "any",
+ "orig_required": true
},
- "user_email": {
+ "t2i_adapter_model": {
"anyOf": [
{
- "type": "string"
+ "$ref": "#/components/schemas/ModelIdentifierField"
},
{
"type": "null"
}
],
- "title": "User Email",
- "description": "The email of the user who created this queue item, if available"
+ "default": null,
+ "description": "The T2I-Adapter model.",
+ "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
},
- "field_values": {
+ "weight": {
"anyOf": [
+ {
+ "type": "number"
+ },
{
"items": {
- "$ref": "#/components/schemas/NodeFieldValue"
+ "type": "number"
},
"type": "array"
- },
- {
- "type": "null"
}
],
- "title": "Field Values",
- "description": "The field values that were used for this queue item"
+ "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"
},
- "retried_from_item_id": {
- "anyOf": [
- {
- "type": "integer"
- },
- {
- "type": "null"
- }
- ],
- "title": "Retried From Item Id",
- "description": "The item_id of the queue item that this item was retried from"
+ "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"
},
- "session": {
- "$ref": "#/components/schemas/GraphExecutionState",
- "description": "The fully-populated session to be executed"
+ "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"
},
- "workflow": {
- "anyOf": [
- {
- "$ref": "#/components/schemas/WorkflowWithoutID"
- },
- {
- "type": "null"
- }
- ],
- "description": "The workflow associated with this queue item"
+ "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"
+ },
+ "type": {
+ "const": "t2i_adapter",
+ "default": "t2i_adapter",
+ "field_kind": "node_attribute",
+ "title": "type",
+ "type": "string"
}
},
+ "required": ["type", "id"],
+ "tags": ["t2i_adapter", "control"],
+ "title": "T2I-Adapter - SD1.5, SDXL",
"type": "object",
- "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."
+ "version": "1.0.4",
+ "output": {
+ "$ref": "#/components/schemas/T2IAdapterOutput"
+ }
},
- "SessionQueueStatus": {
+ "T2IAdapterMetadataField": {
"properties": {
- "queue_id": {
- "type": "string",
- "title": "Queue Id",
- "description": "The ID of the queue"
+ "image": {
+ "$ref": "#/components/schemas/ImageField",
+ "description": "The control image."
},
- "item_id": {
+ "processed_image": {
"anyOf": [
{
- "type": "integer"
+ "$ref": "#/components/schemas/ImageField"
},
{
"type": "null"
}
],
- "title": "Item Id",
- "description": "The current queue item id"
+ "default": null,
+ "description": "The control image, after processing."
},
- "batch_id": {
- "anyOf": [
- {
- "type": "string"
- },
- {
- "type": "null"
- }
- ],
- "title": "Batch Id",
- "description": "The current queue item's batch id"
+ "t2i_adapter_model": {
+ "$ref": "#/components/schemas/ModelIdentifierField",
+ "description": "The T2I-Adapter model to use."
},
- "session_id": {
+ "weight": {
"anyOf": [
{
- "type": "string"
+ "type": "number"
},
{
- "type": "null"
+ "items": {
+ "type": "number"
+ },
+ "type": "array"
}
],
- "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'"
+ "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'"
+ "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'"
+ "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'"
+ "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"
+ "type": {
+ "const": "t2i_adapter_output",
+ "default": "t2i_adapter_output",
+ "field_kind": "node_attribute",
+ "title": "type",
+ "type": "string"
}
},
- "type": "object",
- "required": [
- "queue_id",
- "item_id",
- "batch_id",
- "session_id",
- "pending",
- "in_progress",
- "completed",
- "failed",
- "canceled",
- "total"
- ],
- "title": "SessionQueueStatus"
+ "required": ["output_meta", "t2i_adapter", "type", "type"],
+ "title": "T2IAdapterOutput",
+ "type": "object"
},
- "SetupRequest": {
+ "T2IAdapter_Diffusers_SD1_Config": {
"properties": {
- "email": {
+ "key": {
"type": "string",
- "title": "Email",
- "description": "Admin email address"
+ "title": "Key",
+ "description": "A unique key for this model."
},
- "display_name": {
+ "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"
@@ -65026,55 +70329,19 @@
"type": "null"
}
],
- "title": "Display Name",
- "description": "Admin display name"
+ "title": "Description",
+ "description": "Model description"
},
- "password": {
+ "source": {
"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"
- },
- "multiuser_enabled": {
- "type": "boolean",
- "title": "Multiuser Enabled",
- "description": "Whether multiuser mode is enabled"
+ "title": "Source",
+ "description": "The original source of the model (path, URL or repo_id)."
},
- "strict_password_checking": {
- "type": "boolean",
- "title": "Strict Password Checking",
- "description": "Whether strict password requirements are enforced"
+ "source_type": {
+ "$ref": "#/components/schemas/ModelSourceType",
+ "description": "The type of source"
},
- "admin_email": {
+ "source_api_response": {
"anyOf": [
{
"type": "string"
@@ -65083,79 +70350,88 @@
"type": "null"
}
],
- "title": "Admin Email",
- "description": "Email of the first active admin user, if any"
- }
- },
- "type": "object",
- "required": ["setup_required", "multiuser_enabled", "strict_password_checking"],
- "title": "SetupStatusResponse",
- "description": "Response for setup status check."
- },
- "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",
- "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": "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 show",
- "field_kind": "input",
- "input": "any",
- "orig_required": true
+ "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": "show_image",
- "default": "show_image",
- "field_kind": "node_attribute",
- "title": "type",
- "type": "string"
+ "type": "string",
+ "const": "t2i_adapter",
+ "title": "Type",
+ "default": "t2i_adapter"
+ },
+ "default_settings": {
+ "anyOf": [
+ {
+ "$ref": "#/components/schemas/ControlAdapterDefaultSettings"
+ },
+ {
+ "type": "null"
+ }
+ ]
+ },
+ "base": {
+ "type": "string",
+ "const": "sd-1",
+ "title": "Base",
+ "default": "sd-1"
}
},
- "required": ["type", "id"],
- "tags": ["image"],
- "title": "Show Image",
"type": "object",
- "version": "1.0.1",
- "output": {
- "$ref": "#/components/schemas/ImageOutput"
- }
+ "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"
},
- "SigLIP_Diffusers_Config": {
+ "T2IAdapter_Diffusers_SDXL_Config": {
"properties": {
"key": {
"type": "string",
@@ -65251,27 +70527,25 @@
},
"type": {
"type": "string",
- "const": "siglip",
+ "const": "t2i_adapter",
"title": "Type",
- "default": "siglip"
- },
- "base": {
- "type": "string",
- "const": "any",
- "title": "Base",
- "default": "any"
+ "default": "t2i_adapter"
},
- "cpu_only": {
+ "default_settings": {
"anyOf": [
{
- "type": "boolean"
+ "$ref": "#/components/schemas/ControlAdapterDefaultSettings"
},
{
"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",
@@ -65290,279 +70564,330 @@
"format",
"repo_variant",
"type",
- "base",
- "cpu_only"
+ "default_settings",
+ "base"
],
- "title": "SigLIP_Diffusers_Config",
- "description": "Model config for SigLIP."
+ "title": "T2IAdapter_Diffusers_SDXL_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",
+ "T5EncoderField": {
"properties": {
- "board": {
+ "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": [
{
- "$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"]
+ "title": "Cover Image",
+ "description": "Url for image to preview model"
},
- "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"
+ "base": {
+ "type": "string",
+ "const": "any",
+ "title": "Base",
+ "default": "any"
},
"type": {
- "const": "spandrel_image_to_image_autoscale",
- "default": "spandrel_image_to_image_autoscale",
- "field_kind": "node_attribute",
- "title": "type",
- "type": "string"
+ "type": "string",
+ "const": "t5_encoder",
+ "title": "Type",
+ "default": "t5_encoder"
},
- "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": "bnb_quantized_int8b",
+ "title": "Format",
+ "default": "bnb_quantized_int8b"
},
- "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"
+ "cpu_only": {
+ "anyOf": [
+ {
+ "type": "boolean"
+ },
+ {
+ "type": "null"
+ }
+ ],
+ "title": "Cpu Only",
+ "description": "Whether this model should run on CPU only"
}
},
- "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",
+ "base",
+ "type",
+ "format",
+ "cpu_only"
+ ],
+ "title": "T5Encoder_BnBLLMint8_Config",
+ "description": "Configuration for T5 Encoder models quantized by bitsandbytes' LLM.int8."
},
- "SpandrelImageToImageInvocation": {
- "category": "upscale",
- "class": "invocation",
- "classification": "stable",
- "description": "Run any spandrel image-to-image model (https://github.com/chaiNNer-org/spandrel).",
- "node_pack": "invokeai",
+ "T5Encoder_T5Encoder_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"
+ "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 input image",
- "field_kind": "input",
- "input": "any",
- "orig_required": true
+ "title": "Cover Image",
+ "description": "Url for image to preview model"
},
- "image_to_image_model": {
+ "base": {
+ "type": "string",
+ "const": "any",
+ "title": "Base",
+ "default": "any"
+ },
+ "type": {
+ "type": "string",
+ "const": "t5_encoder",
+ "title": "Type",
+ "default": "t5_encoder"
+ },
+ "format": {
+ "type": "string",
+ "const": "t5_encoder",
+ "title": "Format",
+ "default": "t5_encoder"
+ },
+ "cpu_only": {
"anyOf": [
{
- "$ref": "#/components/schemas/ModelIdentifierField"
+ "type": "boolean"
},
{
"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"]
+ "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",
+ "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."
+ },
+ "TBLR": {
+ "properties": {
+ "top": {
+ "title": "Top",
+ "type": "integer"
+ },
+ "bottom": {
+ "title": "Bottom",
+ "type": "integer"
},
- "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",
+ "left": {
+ "title": "Left",
"type": "integer"
},
- "type": {
- "const": "spandrel_image_to_image",
- "default": "spandrel_image_to_image",
- "field_kind": "node_attribute",
- "title": "type",
- "type": "string"
+ "right": {
+ "title": "Right",
+ "type": "integer"
}
},
- "required": ["type", "id"],
- "tags": ["upscale"],
- "title": "Image-to-Image",
- "type": "object",
- "version": "1.3.0",
- "output": {
- "$ref": "#/components/schemas/ImageOutput"
- }
+ "required": ["top", "bottom", "left", "right"],
+ "title": "TBLR",
+ "type": "object"
},
- "Spandrel_Checkpoint_Config": {
+ "TI_File_SD1_Config": {
"properties": {
"key": {
"type": "string",
@@ -65646,23 +70971,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_file",
"title": "Format",
- "default": "checkpoint"
+ "default": "embedding_file"
+ },
+ "base": {
+ "type": "string",
+ "const": "sd-1",
+ "title": "Base",
+ "default": "sd-1"
}
},
"type": "object",
@@ -65678,854 +71003,640 @@
"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_File_SD1_Config"
},
- "StarterModel": {
+ "TI_File_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_file",
+ "title": "Format",
+ "default": "embedding_file"
},
- "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_File_SD2_Config"
},
- "StarterModelWithoutDependencies": {
+ "TI_File_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_file",
+ "title": "Format",
+ "default": "embedding_file"
+ },
+ "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_File_SDXL_Config"
},
- "String2Output": {
- "class": "output",
- "description": "Base class for invocations that output two strings",
+ "TI_Folder_SD1_Config": {
"properties": {
- "string_1": {
- "description": "string 1",
- "field_kind": "output",
- "title": "String 1",
+ "key": {
"type": "string",
- "ui_hidden": false
+ "title": "Key",
+ "description": "A unique key for this model."
},
- "string_2": {
- "description": "string 2",
- "field_kind": "output",
- "title": "String 2",
+ "hash": {
"type": "string",
- "ui_hidden": false
+ "title": "Hash",
+ "description": "The hash of the model file(s)."
},
- "type": {
- "const": "string_2_output",
- "default": "string_2_output",
- "field_kind": "node_attribute",
- "title": "type",
- "type": "string"
- }
- },
- "required": ["output_meta", "string_1", "string_2", "type", "type"],
- "title": "String2Output",
- "type": "object"
- },
- "StringBatchInvocation": {
- "category": "batch",
- "class": "invocation",
- "classification": "special",
- "description": "Create a batched generation, where the workflow is executed once for each string in the batch.",
- "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"
+ "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."
},
- "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"
+ "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"
},
- "strings": {
+ "source_api_response": {
"anyOf": [
{
- "items": {
- "type": "string"
- },
- "minItems": 1,
- "type": "array"
+ "type": "string"
},
{
"type": "null"
}
],
- "default": null,
- "description": "The strings to batch over",
- "field_kind": "input",
- "input": "any",
- "orig_required": true,
- "title": "Strings"
+ "title": "Source Api Response",
+ "description": "The original API response from the source, as stringified JSON."
},
- "type": {
- "const": "string_batch",
- "default": "string_batch",
- "field_kind": "node_attribute",
- "title": "type",
- "type": "string"
- }
- },
- "required": ["type", "id"],
- "tags": ["primitives", "string", "batch", "special"],
- "title": "String Batch",
- "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",
- "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_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": "embedding",
+ "title": "Type",
+ "default": "embedding"
},
- "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"
+ "format": {
+ "type": "string",
+ "const": "embedding_folder",
+ "title": "Format",
+ "default": "embedding_folder"
},
- "type": {
- "const": "string_collection",
- "default": "string_collection",
- "field_kind": "node_attribute",
- "title": "type",
- "type": "string"
+ "base": {
+ "type": "string",
+ "const": "sd-1",
+ "title": "Base",
+ "default": "sd-1"
}
},
- "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": "string_collection_output",
- "default": "string_collection_output",
- "field_kind": "node_attribute",
- "title": "type",
- "type": "string"
- }
- },
- "required": ["output_meta", "collection", "type", "type"],
- "title": "StringCollectionOutput",
- "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_SD1_Config"
},
- "StringGenerator": {
- "category": "batch",
- "class": "invocation",
- "classification": "special",
- "description": "Generated a range of strings for use in a batched generation",
- "node_pack": "invokeai",
+ "TI_Folder_SD2_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"
+ "key": {
+ "type": "string",
+ "title": "Key",
+ "description": "A unique key for this model."
},
- "generator": {
- "$ref": "#/components/schemas/StringGeneratorField",
- "description": "The string generator.",
- "field_kind": "input",
- "input": "direct",
- "orig_required": true,
- "title": "Generator Type"
+ "hash": {
+ "type": "string",
+ "title": "Hash",
+ "description": "The hash of the model file(s)."
},
- "type": {
- "const": "string_generator",
- "default": "string_generator",
- "field_kind": "node_attribute",
- "title": "type",
- "type": "string"
- }
- },
- "required": ["generator", "type", "id"],
- "tags": ["primitives", "string", "number", "batch", "special"],
- "title": "String Generator",
- "type": "object",
- "version": "1.0.0",
- "output": {
- "$ref": "#/components/schemas/StringGeneratorOutput"
- }
- },
- "StringGeneratorField": {
- "properties": {},
- "title": "StringGeneratorField",
- "type": "object"
- },
- "StringGeneratorOutput": {
- "class": "output",
- "description": "Base class for nodes that output a collection of strings",
- "properties": {
- "strings": {
- "description": "The generated strings",
- "field_kind": "output",
- "items": {
- "type": "string"
- },
- "title": "Strings",
- "type": "array",
- "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": "string_generator_output",
- "default": "string_generator_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"
+ "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": {
+ "anyOf": [
+ {
+ "type": "string"
+ },
+ {
+ "type": "null"
+ }
+ ],
+ "title": "Description",
+ "description": "Model description"
},
- "value": {
- "default": "",
- "description": "The string value",
- "field_kind": "input",
- "input": "any",
- "orig_default": "",
- "orig_required": false,
- "title": "Value",
+ "source": {
"type": "string",
- "ui_component": "textarea"
+ "title": "Source",
+ "description": "The original source of the model (path, URL or repo_id)."
},
- "type": {
- "const": "string",
- "default": "string",
- "field_kind": "node_attribute",
- "title": "type",
- "type": "string"
- }
- },
- "required": ["type", "id"],
- "tags": ["primitives", "string"],
- "title": "String Primitive",
- "type": "object",
- "version": "1.0.1",
- "output": {
- "$ref": "#/components/schemas/StringOutput"
- }
- },
- "StringJoinInvocation": {
- "category": "strings",
- "class": "invocation",
- "classification": "stable",
- "description": "Joins string left to string right",
- "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": {
+ "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)."
},
- "string_left": {
- "default": "",
- "description": "String Left",
- "field_kind": "input",
- "input": "any",
- "orig_default": "",
- "orig_required": false,
- "title": "String Left",
+ "cover_image": {
+ "anyOf": [
+ {
+ "type": "string"
+ },
+ {
+ "type": "null"
+ }
+ ],
+ "title": "Cover Image",
+ "description": "Url for image to preview model"
+ },
+ "type": {
"type": "string",
- "ui_component": "textarea"
+ "const": "embedding",
+ "title": "Type",
+ "default": "embedding"
},
- "string_right": {
- "default": "",
- "description": "String Right",
- "field_kind": "input",
- "input": "any",
- "orig_default": "",
- "orig_required": false,
- "title": "String Right",
+ "format": {
"type": "string",
- "ui_component": "textarea"
+ "const": "embedding_folder",
+ "title": "Format",
+ "default": "embedding_folder"
},
- "type": {
- "const": "string_join",
- "default": "string_join",
- "field_kind": "node_attribute",
- "title": "type",
- "type": "string"
+ "base": {
+ "type": "string",
+ "const": "sd-2",
+ "title": "Base",
+ "default": "sd-2"
}
},
- "required": ["type", "id"],
- "tags": ["string", "join"],
- "title": "String Join",
"type": "object",
- "version": "1.0.1",
- "output": {
- "$ref": "#/components/schemas/StringOutput"
- }
+ "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"
},
- "StringJoinThreeInvocation": {
- "category": "strings",
- "class": "invocation",
- "classification": "stable",
- "description": "Joins string left to string middle to string right",
- "node_pack": "invokeai",
+ "TI_Folder_SDXL_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_left": {
- "default": "",
- "description": "String Left",
- "field_kind": "input",
- "input": "any",
- "orig_default": "",
- "orig_required": false,
- "title": "String Left",
+ "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."
},
- "string_middle": {
- "default": "",
- "description": "String Middle",
- "field_kind": "input",
- "input": "any",
- "orig_default": "",
- "orig_required": false,
- "title": "String Middle",
+ "file_size": {
+ "type": "integer",
+ "title": "File Size",
+ "description": "The size of the model in bytes."
+ },
+ "name": {
"type": "string",
- "ui_component": "textarea"
+ "title": "Name",
+ "description": "Name of the model."
},
- "string_right": {
- "default": "",
- "description": "String Right",
- "field_kind": "input",
- "input": "any",
- "orig_default": "",
- "orig_required": false,
- "title": "String Right",
+ "description": {
+ "anyOf": [
+ {
+ "type": "string"
+ },
+ {
+ "type": "null"
+ }
+ ],
+ "title": "Description",
+ "description": "Model description"
+ },
+ "source": {
"type": "string",
- "ui_component": "textarea"
+ "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": {
- "const": "string_join_three",
- "default": "string_join_three",
- "field_kind": "node_attribute",
- "title": "type",
- "type": "string"
- }
- },
- "required": ["type", "id"],
- "tags": ["string", "join"],
- "title": "String Join Three",
- "type": "object",
- "version": "1.0.1",
- "output": {
- "$ref": "#/components/schemas/StringOutput"
- }
- },
- "StringOutput": {
- "class": "output",
- "description": "Base class for nodes that output a single string",
- "properties": {
- "value": {
- "description": "The output string",
- "field_kind": "output",
- "title": "Value",
"type": "string",
- "ui_hidden": false
+ "const": "embedding",
+ "title": "Type",
+ "default": "embedding"
},
- "type": {
- "const": "string_output",
- "default": "string_output",
- "field_kind": "node_attribute",
- "title": "type",
- "type": "string"
+ "format": {
+ "type": "string",
+ "const": "embedding_folder",
+ "title": "Format",
+ "default": "embedding_folder"
+ },
+ "base": {
+ "type": "string",
+ "const": "sdxl",
+ "title": "Base",
+ "default": "sdxl"
}
},
- "required": ["output_meta", "value", "type", "type"],
- "title": "StringOutput",
- "type": "object"
+ "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"
},
- "StringPosNegOutput": {
- "class": "output",
- "description": "Base class for invocations that output a positive and negative string",
+ "TensorField": {
+ "description": "A tensor primitive field.",
"properties": {
- "positive_string": {
- "description": "Positive string",
- "field_kind": "output",
- "title": "Positive String",
- "type": "string",
- "ui_hidden": false
- },
- "negative_string": {
- "description": "Negative string",
- "field_kind": "output",
- "title": "Negative String",
- "type": "string",
- "ui_hidden": false
- },
- "type": {
- "const": "string_pos_neg_output",
- "default": "string_pos_neg_output",
- "field_kind": "node_attribute",
- "title": "type",
+ "tensor_name": {
+ "description": "The name of a tensor.",
+ "title": "Tensor Name",
"type": "string"
}
},
- "required": ["output_meta", "positive_string", "negative_string", "type", "type"],
- "title": "StringPosNegOutput",
+ "required": ["tensor_name"],
+ "title": "TensorField",
"type": "object"
},
- "StringReplaceInvocation": {
- "category": "strings",
+ "TextLLMInvocation": {
+ "category": "llm",
"class": "invocation",
- "classification": "stable",
- "description": "Replaces the search string with the replace string",
+ "classification": "beta",
+ "description": "Run a text language model to generate or expand text (e.g. for prompt expansion).",
"node_pack": "invokeai",
"properties": {
"id": {
@@ -66552,228 +71663,123 @@
"title": "Use Cache",
"type": "boolean"
},
- "string": {
+ "prompt": {
"default": "",
- "description": "String to work on",
+ "description": "Input text prompt.",
"field_kind": "input",
"input": "any",
"orig_default": "",
"orig_required": false,
- "title": "String",
+ "title": "Prompt",
"type": "string",
"ui_component": "textarea"
},
- "search_string": {
- "default": "",
- "description": "String to search for",
+ "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": "",
+ "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": "Search String",
+ "title": "System Prompt",
"type": "string",
"ui_component": "textarea"
},
- "replace_string": {
- "default": "",
- "description": "String to replace the search",
+ "text_llm_model": {
+ "anyOf": [
+ {
+ "$ref": "#/components/schemas/ModelIdentifierField"
+ },
+ {
+ "type": "null"
+ }
+ ],
+ "default": null,
+ "description": "The text language model to use for text generation",
"field_kind": "input",
"input": "any",
- "orig_default": "",
- "orig_required": false,
- "title": "Replace String",
- "type": "string",
- "ui_component": "textarea"
+ "orig_required": true,
+ "title": "Text LLM Model",
+ "ui_model_type": ["text_llm"]
},
- "use_regex": {
- "default": false,
- "description": "Use search string as a regex expression (non regex is case insensitive)",
+ "max_tokens": {
+ "default": 300,
+ "description": "Maximum number of tokens to generate.",
"field_kind": "input",
"input": "any",
- "orig_default": false,
+ "maximum": 2048,
+ "minimum": 1,
+ "orig_default": 300,
"orig_required": false,
- "title": "Use Regex",
- "type": "boolean"
+ "title": "Max Tokens",
+ "type": "integer"
},
"type": {
- "const": "string_replace",
- "default": "string_replace",
+ "const": "text_llm",
+ "default": "text_llm",
"field_kind": "node_attribute",
"title": "type",
"type": "string"
}
},
"required": ["type", "id"],
- "tags": ["string", "replace", "regex"],
- "title": "String Replace",
+ "tags": ["llm", "text", "prompt"],
+ "title": "Text LLM",
"type": "object",
- "version": "1.0.1",
+ "version": "1.0.0",
"output": {
"$ref": "#/components/schemas/StringOutput"
}
},
- "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",
+ "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"
- },
- "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 split",
- "field_kind": "input",
- "input": "any",
- "orig_default": "",
- "orig_required": false,
- "title": "String",
+ "key": {
"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"
- }
- },
- "required": ["type", "id"],
- "tags": ["string", "split"],
- "title": "String Split",
- "type": "object",
- "version": "1.0.1",
- "output": {
- "$ref": "#/components/schemas/String2Output"
- }
- },
- "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",
- "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": "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."
- },
- "preset_data": {
- "$ref": "#/components/schemas/PresetData",
- "description": "The preset data"
+ "description": "Name of the model."
},
- "type": {
- "$ref": "#/components/schemas/PresetType",
- "description": "The type of style preset"
+ "description": {
+ "anyOf": [
+ {
+ "type": "string"
+ },
+ {
+ "type": "null"
+ }
+ ],
+ "title": "Description",
+ "description": "Model description"
},
- "id": {
+ "source": {
"type": "string",
- "title": "Id",
- "description": "The style preset ID."
+ "title": "Source",
+ "description": "The original source of the model (path, URL or repo_id)."
},
- "image": {
+ "source_type": {
+ "$ref": "#/components/schemas/ModelSourceType",
+ "description": "The type of source"
+ },
+ "source_api_response": {
"anyOf": [
{
"type": "string"
@@ -66782,82 +71788,110 @@
"type": "null"
}
],
- "title": "Image",
- "description": "The path for image"
- }
- },
- "type": "object",
- "required": ["name", "preset_data", "type", "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"
- ],
- "title": "SubModelType",
- "description": "Submodel type."
- },
- "SubmodelDefinition": {
- "properties": {
- "path_or_prefix": {
+ "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",
- "title": "Path Or Prefix"
+ "const": "diffusers",
+ "title": "Format",
+ "default": "diffusers"
},
- "model_type": {
- "$ref": "#/components/schemas/ModelType"
+ "repo_variant": {
+ "$ref": "#/components/schemas/ModelRepoVariant",
+ "default": ""
+ },
+ "type": {
+ "type": "string",
+ "const": "text_llm",
+ "title": "Type",
+ "default": "text_llm"
+ },
+ "base": {
+ "type": "string",
+ "const": "any",
+ "title": "Base",
+ "default": "any"
},
- "variant": {
+ "cpu_only": {
"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": "boolean"
},
{
"type": "null"
}
],
- "title": "Variant"
+ "title": "Cpu Only",
+ "description": "Whether this model should run on CPU only"
}
},
"type": "object",
- "required": ["path_or_prefix", "model_type"],
- "title": "SubmodelDefinition"
+ "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)."
},
- "SubtractInvocation": {
- "category": "math",
+ "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": "Subtracts two numbers",
+ "description": "Split a Tile into its individual properties.",
"node_pack": "invokeai",
"properties": {
"id": {
@@ -66884,102 +71918,155 @@
"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",
+ "tile": {
+ "anyOf": [
+ {
+ "$ref": "#/components/schemas/Tile"
+ },
+ {
+ "type": "null"
+ }
+ ],
+ "default": null,
+ "description": "The tile to split into properties.",
"field_kind": "input",
"input": "any",
- "orig_default": 0,
- "orig_required": false,
- "title": "B",
- "type": "integer"
+ "orig_required": true
},
"type": {
- "const": "sub",
- "default": "sub",
+ "const": "tile_to_properties",
+ "default": "tile_to_properties",
"field_kind": "node_attribute",
"title": "type",
"type": "string"
}
},
"required": ["type", "id"],
- "tags": ["math", "subtract"],
- "title": "Subtract Integers",
+ "tags": ["tiles"],
+ "title": "Tile to Properties",
"type": "object",
"version": "1.0.1",
"output": {
- "$ref": "#/components/schemas/IntegerOutput"
+ "$ref": "#/components/schemas/TileToPropertiesOutput"
}
},
- "T2IAdapterField": {
+ "TileToPropertiesOutput": {
+ "class": "output",
"properties": {
- "image": {
- "$ref": "#/components/schemas/ImageField",
- "description": "The T2I-Adapter image prompt."
+ "coords_left": {
+ "description": "Left coordinate of the tile relative to its parent image.",
+ "field_kind": "output",
+ "title": "Coords Left",
+ "type": "integer",
+ "ui_hidden": false
},
- "t2i_adapter_model": {
- "$ref": "#/components/schemas/ModelIdentifierField",
- "description": "The T2I-Adapter model to use."
+ "coords_right": {
+ "description": "Right coordinate of the tile relative to its parent image.",
+ "field_kind": "output",
+ "title": "Coords Right",
+ "type": "integer",
+ "ui_hidden": false
},
- "weight": {
- "anyOf": [
- {
- "type": "number"
- },
- {
- "items": {
- "type": "number"
- },
- "type": "array"
- }
- ],
- "default": 1,
- "description": "The weight given to the T2I-Adapter",
- "title": "Weight"
+ "coords_top": {
+ "description": "Top coordinate of the tile relative to its parent image.",
+ "field_kind": "output",
+ "title": "Coords Top",
+ "type": "integer",
+ "ui_hidden": false
},
- "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"
+ "coords_bottom": {
+ "description": "Bottom coordinate of the tile relative to its parent image.",
+ "field_kind": "output",
+ "title": "Coords Bottom",
+ "type": "integer",
+ "ui_hidden": false
},
- "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"
+ "width": {
+ "description": "The width of the tile. Equal to coords_right - coords_left.",
+ "field_kind": "output",
+ "title": "Width",
+ "type": "integer",
+ "ui_hidden": false
},
- "resize_mode": {
- "default": "just_resize",
- "description": "The resize mode to use",
- "enum": ["just_resize", "crop_resize", "fill_resize", "just_resize_simple"],
- "title": "Resize Mode",
+ "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": "tile_to_properties_output",
+ "default": "tile_to_properties_output",
+ "field_kind": "node_attribute",
+ "title": "type",
"type": "string"
}
},
- "required": ["image", "t2i_adapter_model"],
- "title": "T2IAdapterField",
+ "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"
},
- "T2IAdapterInvocation": {
- "category": "conditioning",
+ "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": "Collects T2I-Adapter info to pass to other nodes.",
+ "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": {
@@ -67006,136 +72093,116 @@
"title": "Use Cache",
"type": "boolean"
},
- "image": {
+ "positive_conditioning": {
"anyOf": [
{
- "$ref": "#/components/schemas/ImageField"
+ "$ref": "#/components/schemas/ConditioningField"
},
{
"type": "null"
}
],
"default": null,
- "description": "The IP-Adapter image prompt.",
+ "description": "Positive conditioning tensor",
"field_kind": "input",
- "input": "any",
+ "input": "connection",
"orig_required": true
},
- "t2i_adapter_model": {
+ "negative_conditioning": {
"anyOf": [
{
- "$ref": "#/components/schemas/ModelIdentifierField"
+ "$ref": "#/components/schemas/ConditioningField"
},
{
"type": "null"
}
],
"default": null,
- "description": "The T2I-Adapter model.",
+ "description": "Negative conditioning tensor",
"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
+ "input": "connection",
+ "orig_required": true
},
- "weight": {
+ "noise": {
"anyOf": [
{
- "type": "number"
+ "$ref": "#/components/schemas/LatentsField"
},
{
- "items": {
- "type": "number"
- },
- "type": "array"
+ "type": "null"
}
],
- "default": 1,
- "description": "The weight given to the T2I-Adapter",
+ "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",
- "ge": 0,
"input": "any",
- "orig_default": 1,
+ "multipleOf": 8,
+ "orig_default": 1024,
"orig_required": false,
- "title": "Weight"
+ "title": "Tile Height",
+ "type": "integer"
},
- "begin_step_percent": {
- "default": 0,
- "description": "When the T2I-Adapter is first applied (% of total steps)",
+ "tile_width": {
+ "default": 1024,
+ "description": "Width of the tiles in image space.",
+ "exclusiveMinimum": 0,
"field_kind": "input",
"input": "any",
- "maximum": 1,
- "minimum": 0,
- "orig_default": 0,
+ "multipleOf": 8,
+ "orig_default": 1024,
"orig_required": false,
- "title": "Begin Step Percent",
- "type": "number"
+ "title": "Tile Width",
+ "type": "integer"
},
- "end_step_percent": {
- "default": 1,
- "description": "When the T2I-Adapter is last applied (% of total steps)",
+ "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",
- "maximum": 1,
- "minimum": 0,
- "orig_default": 1,
+ "multipleOf": 8,
+ "orig_default": 32,
"orig_required": false,
- "title": "End Step Percent",
- "type": "number"
+ "title": "Tile Overlap",
+ "type": "integer"
},
- "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"],
+ "steps": {
+ "default": 18,
+ "description": "Number of steps to run",
+ "exclusiveMinimum": 0,
"field_kind": "input",
"input": "any",
- "orig_default": "just_resize",
+ "orig_default": 18,
"orig_required": false,
- "title": "Resize Mode",
- "type": "string"
- },
- "type": {
- "const": "t2i_adapter",
- "default": "t2i_adapter",
- "field_kind": "node_attribute",
- "title": "type",
- "type": "string"
- }
- },
- "required": ["type", "id"],
- "tags": ["t2i_adapter", "control"],
- "title": "T2I-Adapter - SD1.5, SDXL",
- "type": "object",
- "version": "1.0.4",
- "output": {
- "$ref": "#/components/schemas/T2IAdapterOutput"
- }
- },
- "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."
+ "title": "Steps",
+ "type": "integer"
},
- "weight": {
+ "cfg_scale": {
"anyOf": [
{
"type": "number"
@@ -67147,133 +72214,194 @@
"type": "array"
}
],
- "default": 1,
- "description": "The weight given to the T2I-Adapter",
- "title": "Weight"
+ "default": 6.0,
+ "description": "Classifier-Free Guidance scale",
+ "field_kind": "input",
+ "input": "any",
+ "orig_default": 6.0,
+ "orig_required": false,
+ "title": "CFG Scale"
},
- "begin_step_percent": {
- "default": 0,
- "description": "When the T2I-Adapter is first applied (% of total steps)",
+ "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,
- "title": "Begin Step Percent",
+ "orig_default": 0.0,
+ "orig_required": false,
+ "title": "Denoising Start",
"type": "number"
},
- "end_step_percent": {
- "default": 1,
- "description": "When the T2I-Adapter is last applied (% of total steps)",
+ "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,
- "title": "End Step Percent",
+ "orig_default": 1.0,
+ "orig_required": false,
+ "title": "Denoising End",
"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": {
- "class": "output",
- "properties": {
- "t2i_adapter": {
- "$ref": "#/components/schemas/T2IAdapterField",
- "description": "T2I-Adapter(s) to apply",
- "field_kind": "output",
- "title": "T2I Adapter",
- "ui_hidden": false
- },
- "type": {
- "const": "t2i_adapter_output",
- "default": "t2i_adapter_output",
- "field_kind": "node_attribute",
- "title": "type",
- "type": "string"
- }
- },
- "required": ["output_meta", "t2i_adapter", "type", "type"],
- "title": "T2IAdapterOutput",
- "type": "object"
- },
- "T2IAdapter_Diffusers_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)."
- },
- "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": {
+ "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",
- "title": "Name",
- "description": "Name of the model."
+ "ui_type": "SchedulerField"
},
- "description": {
+ "unet": {
"anyOf": [
{
- "type": "string"
+ "$ref": "#/components/schemas/UNetField"
},
{
"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": "UNet (scheduler, LoRAs)",
+ "field_kind": "input",
+ "input": "connection",
+ "orig_required": true,
+ "title": "UNet"
},
- "source_type": {
- "$ref": "#/components/schemas/ModelSourceType",
- "description": "The type of source"
+ "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"
},
- "source_api_response": {
+ "control": {
"anyOf": [
{
- "type": "string"
+ "$ref": "#/components/schemas/ControlField"
+ },
+ {
+ "items": {
+ "$ref": "#/components/schemas/ControlField"
+ },
+ "type": "array"
},
{
"type": "null"
}
],
- "title": "Source Api Response",
- "description": "The original API response from the source, as stringified JSON."
+ "default": null,
+ "field_kind": "input",
+ "input": "connection",
+ "orig_default": null,
+ "orig_required": false,
+ "title": "Control"
},
- "source_url": {
+ "type": {
+ "const": "tiled_multi_diffusion_denoise_latents",
+ "default": "tiled_multi_diffusion_denoise_latents",
+ "field_kind": "node_attribute",
+ "title": "type",
+ "type": "string"
+ }
+ },
+ "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"
+ }
+ },
+ "TransformerField": {
+ "properties": {
+ "transformer": {
+ "$ref": "#/components/schemas/ModelIdentifierField",
+ "description": "Info to load Transformer submodel"
+ },
+ "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": [
{
- "type": "string"
+ "items": {
+ "type": "string"
+ },
+ "type": "array"
},
{
"type": "null"
}
],
- "title": "Source Url",
- "description": "Optional URL for the model (e.g. download page or model page)."
+ "default": null,
+ "description": "The node's tags",
+ "title": "Tags"
},
- "cover_image": {
+ "title": {
"anyOf": [
{
"type": "string"
@@ -67282,136 +72410,198 @@
"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": "t2i_adapter",
- "title": "Type",
- "default": "t2i_adapter"
+ "default": null,
+ "description": "The node's display name",
+ "title": "Title"
},
- "default_settings": {
+ "category": {
"anyOf": [
{
- "$ref": "#/components/schemas/ControlAdapterDefaultSettings"
+ "type": "string"
},
{
"type": "null"
}
- ]
+ ],
+ "default": null,
+ "description": "The node's category",
+ "title": "Category"
},
- "base": {
- "type": "string",
- "const": "sd-1",
- "title": "Base",
- "default": "sd-1"
+ "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"
}
},
- "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"
+ "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",
+ "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": "T2IAdapter_Diffusers_SD1_Config"
+ "title": "UIType",
+ "type": "string"
},
- "T2IAdapter_Diffusers_SDXL_Config": {
+ "UNetField": {
"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)."
+ "unet": {
+ "$ref": "#/components/schemas/ModelIdentifierField",
+ "description": "Info to load unet submodel"
},
- "path": {
- "type": "string",
- "title": "Path",
- "description": "Path to the model on the filesystem. Relative paths are relative to the Invoke root directory."
+ "scheduler": {
+ "$ref": "#/components/schemas/ModelIdentifierField",
+ "description": "Info to load scheduler submodel"
},
- "file_size": {
- "type": "integer",
- "title": "File Size",
- "description": "The size of the model in bytes."
+ "loras": {
+ "description": "LoRAs to apply on model loading",
+ "items": {
+ "$ref": "#/components/schemas/LoRAField"
+ },
+ "title": "Loras",
+ "type": "array"
},
- "name": {
- "type": "string",
- "title": "Name",
- "description": "Name of the model."
+ "seamless_axes": {
+ "description": "Axes(\"x\" and \"y\") to which apply seamless",
+ "items": {
+ "type": "string"
+ },
+ "title": "Seamless Axes",
+ "type": "array"
},
- "description": {
+ "freeu_config": {
"anyOf": [
{
- "type": "string"
+ "$ref": "#/components/schemas/FreeUConfig"
},
{
"type": "null"
}
],
- "title": "Description",
- "description": "Model description"
+ "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
},
- "source": {
+ "type": {
+ "const": "unet_output",
+ "default": "unet_output",
+ "field_kind": "node_attribute",
+ "title": "type",
+ "type": "string"
+ }
+ },
+ "required": ["output_meta", "unet", "type", "type"],
+ "title": "UNetOutput",
+ "type": "object"
+ },
+ "URLModelSource": {
+ "properties": {
+ "url": {
"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)."
+ "minLength": 1,
+ "format": "uri",
+ "title": "Url"
},
- "cover_image": {
+ "access_token": {
"anyOf": [
{
"type": "string"
@@ -67420,87 +72610,61 @@
"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": ""
+ "title": "Access Token"
},
"type": {
"type": "string",
- "const": "t2i_adapter",
+ "const": "url",
"title": "Type",
- "default": "t2i_adapter"
- },
- "default_settings": {
- "anyOf": [
- {
- "$ref": "#/components/schemas/ControlAdapterDefaultSettings"
- },
- {
- "type": "null"
- }
- ]
+ "default": "url"
+ }
+ },
+ "type": "object",
+ "required": ["url"],
+ "title": "URLModelSource",
+ "description": "A generic URL point to a checkpoint file."
+ },
+ "URLRegexTokenPair": {
+ "properties": {
+ "url_regex": {
+ "type": "string",
+ "title": "Url Regex",
+ "description": "Regular expression to match against the URL"
},
- "base": {
+ "token": {
"type": "string",
- "const": "sdxl",
- "title": "Base",
- "default": "sdxl"
+ "title": "Token",
+ "description": "Token to use when the URL matches the regex"
}
},
"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"
+ "required": ["url_regex", "token"],
+ "title": "URLRegexTokenPair"
},
- "T5EncoderField": {
+ "UninstallNodePackResponse": {
"properties": {
- "tokenizer": {
- "$ref": "#/components/schemas/ModelIdentifierField",
- "description": "Info to load tokenizer submodel"
+ "name": {
+ "type": "string",
+ "title": "Name",
+ "description": "The name of the uninstalled node pack."
},
- "text_encoder": {
- "$ref": "#/components/schemas/ModelIdentifierField",
- "description": "Info to load text_encoder submodel"
+ "success": {
+ "type": "boolean",
+ "title": "Success",
+ "description": "Whether the uninstall was successful."
},
- "loras": {
- "description": "LoRAs to apply on model loading",
- "items": {
- "$ref": "#/components/schemas/LoRAField"
- },
- "title": "Loras",
- "type": "array"
+ "message": {
+ "type": "string",
+ "title": "Message",
+ "description": "Status message."
}
},
- "required": ["tokenizer", "text_encoder", "loras"],
- "title": "T5EncoderField",
- "type": "object"
+ "type": "object",
+ "required": ["name", "success", "message"],
+ "title": "UninstallNodePackResponse",
+ "description": "Response after uninstalling a node pack."
},
- "T5Encoder_BnBLLMint8_Config": {
+ "Unknown_Config": {
"properties": {
"key": {
"type": "string",
@@ -67586,33 +72750,21 @@
},
"base": {
"type": "string",
- "const": "any",
+ "const": "unknown",
"title": "Base",
- "default": "any"
+ "default": "unknown"
},
"type": {
"type": "string",
- "const": "t5_encoder",
+ "const": "unknown",
"title": "Type",
- "default": "t5_encoder"
+ "default": "unknown"
},
"format": {
"type": "string",
- "const": "bnb_quantized_int8b",
+ "const": "unknown",
"title": "Format",
- "default": "bnb_quantized_int8b"
- },
- "cpu_only": {
- "anyOf": [
- {
- "type": "boolean"
- },
- {
- "type": "null"
- }
- ],
- "title": "Cpu Only",
- "description": "Whether this model should run on CPU only"
+ "default": "unknown"
}
},
"type": "object",
@@ -67630,13 +72782,419 @@
"cover_image",
"base",
"type",
- "format",
- "cpu_only"
+ "format"
],
- "title": "T5Encoder_BnBLLMint8_Config",
- "description": "Configuration for T5 Encoder models quantized by bitsandbytes' LLM.int8."
+ "title": "Unknown_Config",
+ "description": "Model config for unknown models, used as a fallback when we cannot positively identify a model."
},
- "T5Encoder_T5Encoder_Config": {
+ "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/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"
+ },
+ "image": {
+ "anyOf": [
+ {
+ "$ref": "#/components/schemas/ImageField"
+ },
+ {
+ "type": "null"
+ }
+ ],
+ "default": null,
+ "description": "The image to use",
+ "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",
+ "field_kind": "input",
+ "input": "any",
+ "minimum": 0,
+ "orig_default": 50,
+ "orig_required": false,
+ "title": "Strength",
+ "type": "number"
+ },
+ "type": {
+ "const": "unsharp_mask",
+ "default": "unsharp_mask",
+ "field_kind": "node_attribute",
+ "title": "type",
+ "type": "string"
+ }
+ },
+ "required": ["type", "id"],
+ "tags": ["image", "unsharp_mask"],
+ "title": "Unsharp Mask",
+ "type": "object",
+ "version": "1.2.2",
+ "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"
+ },
+ "UnstarredVideosResult": {
+ "properties": {
+ "affected_boards": {
+ "items": {
+ "type": "string"
+ },
+ "type": "array",
+ "title": "Affected Boards",
+ "description": "The ids of boards affected by the operation"
+ },
+ "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": "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",
+ "class": "invocation",
+ "classification": "stable",
+ "description": "Loads a VAE model, outputting a VaeLoaderOutput",
+ "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"
+ },
+ "vae_model": {
+ "anyOf": [
+ {
+ "$ref": "#/components/schemas/ModelIdentifierField"
+ },
+ {
+ "type": "null"
+ }
+ ],
+ "default": null,
+ "description": "VAE model 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"]
+ },
+ "type": {
+ "const": "vae_loader",
+ "default": "vae_loader",
+ "field_kind": "node_attribute",
+ "title": "type",
+ "type": "string"
+ }
+ },
+ "required": ["type", "id"],
+ "tags": ["vae", "model"],
+ "title": "VAE Model - SD1.5, SD2, SDXL, SD3, FLUX",
+ "type": "object",
+ "version": "1.0.4",
+ "output": {
+ "$ref": "#/components/schemas/VAEOutput"
+ }
+ },
+ "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": {
"properties": {
"key": {
"type": "string",
@@ -67720,35 +73278,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"
},
- "cpu_only": {
- "anyOf": [
- {
- "type": "boolean"
- },
- {
- "type": "null"
- }
- ],
- "title": "Cpu Only",
- "description": "Whether this model should run on CPU only"
+ "base": {
+ "type": "string",
+ "const": "anima",
+ "title": "Base",
+ "default": "anima"
}
},
"type": "object",
@@ -67764,38 +73322,15 @@
"source_api_response",
"source_url",
"cover_image",
- "base",
+ "config_path",
"type",
"format",
- "cpu_only"
+ "base"
],
- "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_Anima_Config",
+ "description": "Model config for Anima QwenImage VAE checkpoint models (AutoencoderKLQwenImage)."
},
- "TI_File_SD1_Config": {
+ "VAE_Checkpoint_FLUX_Config": {
"properties": {
"key": {
"type": "string",
@@ -67879,23 +73414,35 @@
"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"
},
"base": {
"type": "string",
- "const": "sd-1",
+ "const": "flux",
"title": "Base",
- "default": "sd-1"
+ "default": "flux"
}
},
"type": "object",
@@ -67911,13 +73458,14 @@
"source_api_response",
"source_url",
"cover_image",
+ "config_path",
"type",
"format",
"base"
],
- "title": "TI_File_SD1_Config"
+ "title": "VAE_Checkpoint_FLUX_Config"
},
- "TI_File_SD2_Config": {
+ "VAE_Checkpoint_Flux2_Config": {
"properties": {
"key": {
"type": "string",
@@ -68001,23 +73549,35 @@
"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"
},
"base": {
"type": "string",
- "const": "sd-2",
+ "const": "flux2",
"title": "Base",
- "default": "sd-2"
+ "default": "flux2"
}
},
"type": "object",
@@ -68033,13 +73593,15 @@
"source_api_response",
"source_url",
"cover_image",
+ "config_path",
"type",
"format",
"base"
],
- "title": "TI_File_SD2_Config"
+ "title": "VAE_Checkpoint_Flux2_Config",
+ "description": "Model config for FLUX.2 VAE checkpoint models (AutoencoderKLFlux2)."
},
- "TI_File_SDXL_Config": {
+ "VAE_Checkpoint_QwenImage_Config": {
"properties": {
"key": {
"type": "string",
@@ -68123,23 +73685,35 @@
"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"
},
"base": {
"type": "string",
- "const": "sdxl",
+ "const": "qwen-image",
"title": "Base",
- "default": "sdxl"
+ "default": "qwen-image"
}
},
"type": "object",
@@ -68155,13 +73729,15 @@
"source_api_response",
"source_url",
"cover_image",
+ "config_path",
"type",
"format",
"base"
],
- "title": "TI_File_SDXL_Config"
+ "title": "VAE_Checkpoint_QwenImage_Config",
+ "description": "Model config for Qwen Image VAE checkpoint models (AutoencoderKLQwenImage)."
},
- "TI_Folder_SD1_Config": {
+ "VAE_Checkpoint_SD1_Config": {
"properties": {
"key": {
"type": "string",
@@ -68245,17 +73821,29 @@
"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",
@@ -68277,13 +73865,14 @@
"source_api_response",
"source_url",
"cover_image",
+ "config_path",
"type",
"format",
"base"
],
- "title": "TI_Folder_SD1_Config"
+ "title": "VAE_Checkpoint_SD1_Config"
},
- "TI_Folder_SD2_Config": {
+ "VAE_Checkpoint_SD2_Config": {
"properties": {
"key": {
"type": "string",
@@ -68367,17 +73956,29 @@
"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",
@@ -68399,13 +74000,14 @@
"source_api_response",
"source_url",
"cover_image",
+ "config_path",
"type",
"format",
"base"
],
- "title": "TI_Folder_SD2_Config"
+ "title": "VAE_Checkpoint_SD2_Config"
},
- "TI_Folder_SDXL_Config": {
+ "VAE_Checkpoint_SDXL_Config": {
"properties": {
"key": {
"type": "string",
@@ -68489,157 +74091,58 @@
"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",
- "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"
- },
- "prompt": {
- "default": "",
- "description": "Input text prompt.",
- "field_kind": "input",
- "input": "any",
- "orig_default": "",
- "orig_required": false,
- "title": "Prompt",
- "type": "string",
- "ui_component": "textarea"
- },
- "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": {
+ "config_path": {
"anyOf": [
{
- "$ref": "#/components/schemas/ModelIdentifierField"
+ "type": "string"
},
{
"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": "Config Path",
+ "description": "Path to the config for this model, if any."
},
"type": {
- "const": "text_llm",
- "default": "text_llm",
- "field_kind": "node_attribute",
- "title": "type",
- "type": "string"
+ "type": "string",
+ "const": "vae",
+ "title": "Type",
+ "default": "vae"
+ },
+ "format": {
+ "type": "string",
+ "const": "checkpoint",
+ "title": "Format",
+ "default": "checkpoint"
+ },
+ "base": {
+ "type": "string",
+ "const": "sdxl",
+ "title": "Base",
+ "default": "sdxl"
}
},
- "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",
+ "config_path",
+ "type",
+ "format",
+ "base"
+ ],
+ "title": "VAE_Checkpoint_SDXL_Config"
},
- "TextLLM_Diffusers_Config": {
+ "VAE_Checkpoint_Wan_Config": {
"properties": {
"key": {
"type": "string",
@@ -68723,39 +74226,41 @@
"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": "text_llm",
+ "const": "vae",
"title": "Type",
- "default": "text_llm"
+ "default": "vae"
+ },
+ "format": {
+ "type": "string",
+ "const": "checkpoint",
+ "title": "Format",
+ "default": "checkpoint"
},
"base": {
"type": "string",
- "const": "any",
+ "const": "wan",
"title": "Base",
- "default": "any"
+ "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",
@@ -68771,545 +74276,192 @@
"source_api_response",
"source_url",
"cover_image",
- "format",
- "repo_variant",
+ "config_path",
"type",
+ "format",
"base",
- "cpu_only"
+ "latent_channels"
],
- "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_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``."
},
- "TileToPropertiesOutput": {
- "class": "output",
+ "VAE_Diffusers_Flux2_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"
- },
- "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_type": "SchedulerField"
+ "title": "Source Api Response",
+ "description": "The original API response from the source, as stringified JSON."
},
- "unet": {
+ "source_url": {
"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"
- },
- "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"
+ "title": "Source Url",
+ "description": "Optional URL for the model (e.g. download page or model page)."
},
- "control": {
+ "cover_image": {
"anyOf": [
{
- "$ref": "#/components/schemas/ControlField"
- },
- {
- "items": {
- "$ref": "#/components/schemas/ControlField"
- },
- "type": "array"
+ "type": "string"
},
{
"type": "null"
}
],
- "default": null,
- "field_kind": "input",
- "input": "connection",
- "orig_default": null,
- "orig_required": false,
- "title": "Control"
+ "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": "tiled_multi_diffusion_denoise_latents",
- "default": "tiled_multi_diffusion_denoise_latents",
- "field_kind": "node_attribute",
- "title": "type",
- "type": "string"
+ "type": "string",
+ "const": "vae",
+ "title": "Type",
+ "default": "vae"
+ },
+ "base": {
+ "type": "string",
+ "const": "flux2",
+ "title": "Base",
+ "default": "flux2"
}
},
- "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"
+ ],
+ "title": "VAE_Diffusers_Flux2_Config",
+ "description": "Model config for FLUX.2 VAE models in diffusers format (AutoencoderKLFlux2)."
},
- "TransformerField": {
+ "VAE_Diffusers_SD1_Config": {
"properties": {
- "transformer": {
- "$ref": "#/components/schemas/ModelIdentifierField",
- "description": "Info to load Transformer submodel"
+ "key": {
+ "type": "string",
+ "title": "Key",
+ "description": "A unique key for this model."
},
- "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"],
- "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": {
+ "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": [
{
- "items": {
- "type": "string"
- },
- "type": "array"
+ "type": "string"
},
{
"type": "null"
}
],
- "default": null,
- "description": "The node's tags",
- "title": "Tags"
+ "title": "Description",
+ "description": "Model description"
},
- "title": {
+ "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"
@@ -69318,11 +74470,10 @@
"type": "null"
}
],
- "default": null,
- "description": "The node's display name",
- "title": "Title"
+ "title": "Source Api Response",
+ "description": "The original API response from the source, as stringified JSON."
},
- "category": {
+ "source_url": {
"anyOf": [
{
"type": "string"
@@ -69331,185 +74482,113 @@
"type": "null"
}
],
- "default": null,
- "description": "The node's category",
- "title": "Category"
+ "title": "Source Url",
+ "description": "Optional URL for the model (e.g. download page or model page)."
},
- "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"
+ "cover_image": {
+ "anyOf": [
+ {
+ "type": "string"
+ },
+ {
+ "type": "null"
+ }
+ ],
+ "title": "Cover Image",
+ "description": "Url for image to preview model"
},
- "node_pack": {
- "description": "The node pack that this node belongs to, will be 'invokeai' for built-in nodes",
- "title": "Node Pack",
- "type": "string"
+ "format": {
+ "type": "string",
+ "const": "diffusers",
+ "title": "Format",
+ "default": "diffusers"
},
- "classification": {
- "$ref": "#/components/schemas/Classification",
- "default": "stable",
- "description": "The node's classification"
+ "repo_variant": {
+ "$ref": "#/components/schemas/ModelRepoVariant",
+ "default": ""
+ },
+ "type": {
+ "type": "string",
+ "const": "vae",
+ "title": "Type",
+ "default": "vae"
+ },
+ "base": {
+ "type": "string",
+ "const": "sd-1",
+ "title": "Base",
+ "default": "sd-1"
}
},
- "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",
- "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"
+ "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"
],
- "title": "UIType",
- "type": "string"
+ "title": "VAE_Diffusers_SD1_Config"
},
- "UNetField": {
+ "VAE_Diffusers_SDXL_Config": {
"properties": {
- "unet": {
- "$ref": "#/components/schemas/ModelIdentifierField",
- "description": "Info to load unet submodel"
+ "key": {
+ "type": "string",
+ "title": "Key",
+ "description": "A unique key for this model."
},
- "scheduler": {
- "$ref": "#/components/schemas/ModelIdentifierField",
- "description": "Info to load scheduler submodel"
+ "hash": {
+ "type": "string",
+ "title": "Hash",
+ "description": "The hash of the model file(s)."
},
- "loras": {
- "description": "LoRAs to apply on model loading",
- "items": {
- "$ref": "#/components/schemas/LoRAField"
- },
- "title": "Loras",
- "type": "array"
+ "path": {
+ "type": "string",
+ "title": "Path",
+ "description": "Path to the model on the filesystem. Relative paths are relative to the Invoke root directory."
},
- "seamless_axes": {
- "description": "Axes(\"x\" and \"y\") to which apply seamless",
- "items": {
- "type": "string"
- },
- "title": "Seamless Axes",
- "type": "array"
+ "file_size": {
+ "type": "integer",
+ "title": "File Size",
+ "description": "The size of the model in bytes."
},
- "freeu_config": {
+ "name": {
+ "type": "string",
+ "title": "Name",
+ "description": "Name of the model."
+ },
+ "description": {
"anyOf": [
{
- "$ref": "#/components/schemas/FreeUConfig"
+ "type": "string"
},
{
"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
+ "title": "Description",
+ "description": "Model description"
},
- "type": {
- "const": "unet_output",
- "default": "unet_output",
- "field_kind": "node_attribute",
- "title": "type",
- "type": "string"
- }
- },
- "required": ["output_meta", "unet", "type", "type"],
- "title": "UNetOutput",
- "type": "object"
- },
- "URLModelSource": {
- "properties": {
- "url": {
+ "source": {
"type": "string",
- "minLength": 1,
- "format": "uri",
- "title": "Url"
+ "title": "Source",
+ "description": "The original source of the model (path, URL or repo_id)."
},
- "access_token": {
+ "source_type": {
+ "$ref": "#/components/schemas/ModelSourceType",
+ "description": "The type of source"
+ },
+ "source_api_response": {
"anyOf": [
{
"type": "string"
@@ -69518,61 +74597,77 @@
"type": "null"
}
],
- "title": "Access Token"
+ "title": "Source Api Response",
+ "description": "The original API response from the source, as stringified JSON."
},
- "type": {
- "type": "string",
- "const": "url",
- "title": "Type",
- "default": "url"
- }
- },
- "type": "object",
- "required": ["url"],
- "title": "URLModelSource",
- "description": "A generic URL point to a checkpoint file."
- },
- "URLRegexTokenPair": {
- "properties": {
- "url_regex": {
- "type": "string",
- "title": "Url Regex",
- "description": "Regular expression to match against the URL"
+ "source_url": {
+ "anyOf": [
+ {
+ "type": "string"
+ },
+ {
+ "type": "null"
+ }
+ ],
+ "title": "Source Url",
+ "description": "Optional URL for the model (e.g. download page or model page)."
},
- "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": {
+ "cover_image": {
+ "anyOf": [
+ {
+ "type": "string"
+ },
+ {
+ "type": "null"
+ }
+ ],
+ "title": "Cover Image",
+ "description": "Url for image to preview model"
+ },
+ "format": {
"type": "string",
- "title": "Name",
- "description": "The name of the uninstalled node pack."
+ "const": "diffusers",
+ "title": "Format",
+ "default": "diffusers"
},
- "success": {
- "type": "boolean",
- "title": "Success",
- "description": "Whether the uninstall was successful."
+ "repo_variant": {
+ "$ref": "#/components/schemas/ModelRepoVariant",
+ "default": ""
},
- "message": {
+ "type": {
"type": "string",
- "title": "Message",
- "description": "Status message."
+ "const": "vae",
+ "title": "Type",
+ "default": "vae"
+ },
+ "base": {
+ "type": "string",
+ "const": "sdxl",
+ "title": "Base",
+ "default": "sdxl"
}
},
"type": "object",
- "required": ["name", "success", "message"],
- "title": "UninstallNodePackResponse",
- "description": "Response after uninstalling a node pack."
+ "required": [
+ "key",
+ "hash",
+ "path",
+ "file_size",
+ "name",
+ "description",
+ "source",
+ "source_type",
+ "source_api_response",
+ "source_url",
+ "cover_image",
+ "format",
+ "repo_variant",
+ "type",
+ "base"
+ ],
+ "title": "VAE_Diffusers_SDXL_Config"
},
- "Unknown_Config": {
+ "VAE_Diffusers_Wan_Config": {
"properties": {
"key": {
"type": "string",
@@ -69656,23 +74751,34 @@
"title": "Cover Image",
"description": "Url for image to preview model"
},
- "base": {
+ "format": {
"type": "string",
- "const": "unknown",
- "title": "Base",
- "default": "unknown"
+ "const": "diffusers",
+ "title": "Format",
+ "default": "diffusers"
+ },
+ "repo_variant": {
+ "$ref": "#/components/schemas/ModelRepoVariant",
+ "default": ""
},
"type": {
"type": "string",
- "const": "unknown",
+ "const": "vae",
"title": "Type",
- "default": "unknown"
+ "default": "vae"
},
- "format": {
+ "base": {
"type": "string",
- "const": "unknown",
- "title": "Format",
- "default": "unknown"
+ "const": "wan",
+ "title": "Base",
+ "default": "wan"
+ },
+ "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
}
},
"type": "object",
@@ -69688,18 +74794,66 @@
"source_api_response",
"source_url",
"cover_image",
- "base",
+ "format",
+ "repo_variant",
"type",
- "format"
+ "base",
+ "latent_channels"
],
- "title": "Unknown_Config",
- "description": "Model config for unknown models, used as a fallback when we cannot positively identify a model."
+ "title": "VAE_Diffusers_Wan_Config",
+ "description": "Model config for Wan 2.2 VAE in diffusers folder layout (AutoencoderKLWan)."
},
- "UnsharpMaskInvocation": {
- "category": "image",
+ "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"
+ },
+ "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"
+ },
+ "VideoConcatInvocation": {
+ "category": "video",
"class": "invocation",
- "classification": "stable",
- "description": "Applies an unsharp mask filter to an image",
+ "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": {
"board": {
@@ -69758,203 +74912,626 @@
"title": "Use Cache",
"type": "boolean"
},
- "image": {
+ "videos": {
"anyOf": [
{
- "$ref": "#/components/schemas/ImageField"
+ "items": {
+ "$ref": "#/components/schemas/VideoField"
+ },
+ "minItems": 2,
+ "type": "array"
},
{
"type": "null"
}
],
"default": null,
- "description": "The image to use",
+ "description": "Videos to concatenate, in order. At least two are required.",
"field_kind": "input",
"input": "any",
- "orig_required": true
+ "orig_required": true,
+ "title": "Videos"
},
- "radius": {
- "default": 2,
- "description": "Unsharp mask radius",
- "exclusiveMinimum": 0,
+ "transition": {
+ "default": "cut",
+ "description": "Transition between consecutive clips.",
+ "enum": ["cut", "crossfade", "fade_through_black"],
"field_kind": "input",
"input": "any",
- "orig_default": 2,
+ "orig_default": "cut",
"orig_required": false,
- "title": "Radius",
- "type": "number"
+ "title": "Transition",
+ "type": "string"
},
- "strength": {
- "default": 50,
- "description": "Unsharp mask strength",
+ "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": 50,
+ "orig_default": 8,
"orig_required": false,
- "title": "Strength",
- "type": "number"
+ "title": "Transition Frames",
+ "type": "integer"
+ },
+ "fps": {
+ "anyOf": [
+ {
+ "maximum": 120,
+ "minimum": 1,
+ "type": "integer"
+ },
+ {
+ "type": "null"
+ }
+ ],
+ "default": null,
+ "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": "unsharp_mask",
- "default": "unsharp_mask",
+ "const": "video_concat",
+ "default": "video_concat",
"field_kind": "node_attribute",
"title": "type",
"type": "string"
}
},
"required": ["type", "id"],
- "tags": ["image", "unsharp_mask"],
- "title": "Unsharp Mask",
+ "tags": ["video", "concat", "transition"],
+ "title": "Concatenate Videos",
"type": "object",
- "version": "1.2.2",
+ "version": "1.0.0",
"output": {
- "$ref": "#/components/schemas/ImageOutput"
+ "$ref": "#/components/schemas/VideoOutput"
}
},
- "UnstarredImagesResult": {
+ "VideoDTO": {
"properties": {
- "affected_boards": {
- "items": {
- "type": "string"
- },
- "type": "array",
- "title": "Affected Boards",
- "description": "The ids of boards affected by the delete operation"
+ "video_name": {
+ "type": "string",
+ "title": "Video Name",
+ "description": "The unique name of the video."
},
- "unstarred_images": {
- "items": {
- "type": "string"
- },
- "type": "array",
- "title": "Unstarred Images",
- "description": "The names of the images that were unstarred"
+ "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)."
+ },
+ "video_origin": {
+ "$ref": "#/components/schemas/ResourceOrigin",
+ "description": "The origin of the video."
+ },
+ "video_category": {
+ "$ref": "#/components/schemas/ImageCategory",
+ "description": "The category of the video (reuses ImageCategory)."
+ },
+ "width": {
+ "type": "integer",
+ "title": "Width",
+ "description": "The pixel width of the video."
+ },
+ "height": {
+ "type": "integer",
+ "title": "Height",
+ "description": "The pixel height of the video."
+ },
+ "duration": {
+ "type": "number",
+ "title": "Duration",
+ "description": "The duration of the video in seconds."
+ },
+ "fps": {
+ "anyOf": [
+ {
+ "type": "number"
+ },
+ {
+ "type": "null"
+ }
+ ],
+ "title": "Fps",
+ "description": "The frames-per-second of the video, if known."
+ },
+ "created_at": {
+ "anyOf": [
+ {
+ "type": "string",
+ "format": "date-time"
+ },
+ {
+ "type": "string"
+ }
+ ],
+ "title": "Created At",
+ "description": "The created timestamp of the video."
+ },
+ "updated_at": {
+ "anyOf": [
+ {
+ "type": "string",
+ "format": "date-time"
+ },
+ {
+ "type": "string"
+ }
+ ],
+ "title": "Updated At",
+ "description": "The updated timestamp of the video."
+ },
+ "deleted_at": {
+ "anyOf": [
+ {
+ "type": "string",
+ "format": "date-time"
+ },
+ {
+ "type": "string"
+ },
+ {
+ "type": "null"
+ }
+ ],
+ "title": "Deleted At",
+ "description": "The deleted timestamp of the video."
+ },
+ "is_intermediate": {
+ "type": "boolean",
+ "title": "Is Intermediate",
+ "description": "Whether this is an intermediate video."
+ },
+ "session_id": {
+ "anyOf": [
+ {
+ "type": "string"
+ },
+ {
+ "type": "null"
+ }
+ ],
+ "title": "Session Id",
+ "description": "The session ID that produced this video, if any."
+ },
+ "node_id": {
+ "anyOf": [
+ {
+ "type": "string"
+ },
+ {
+ "type": "null"
+ }
+ ],
+ "title": "Node Id",
+ "description": "The node ID that produced this video, if any."
+ },
+ "starred": {
+ "type": "boolean",
+ "title": "Starred",
+ "description": "Whether this video is starred."
+ },
+ "has_workflow": {
+ "type": "boolean",
+ "title": "Has Workflow",
+ "description": "Whether this video has a workflow associated."
+ },
+ "video_subfolder": {
+ "type": "string",
+ "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": ["affected_boards", "unstarred_images"],
- "title": "UnstarredImagesResult"
+ "required": [
+ "video_name",
+ "video_url",
+ "thumbnail_url",
+ "video_origin",
+ "video_category",
+ "width",
+ "height",
+ "duration",
+ "created_at",
+ "updated_at",
+ "is_intermediate",
+ "starred",
+ "has_workflow"
+ ],
+ "title": "VideoDTO",
+ "description": "Deserialized video record, enriched for the frontend."
},
- "UpdateAppGenerationSettingsRequest": {
+ "VideoField": {
+ "description": "A video primitive field",
"properties": {
- "image_subfolder_strategy": {
- "type": "string",
- "enum": ["flat", "date", "type", "hash"],
- "title": "Image Subfolder Strategy",
- "description": "Strategy for organizing images into subfolders."
+ "video_name": {
+ "description": "The name of the video",
+ "title": "Video Name",
+ "type": "string"
+ }
+ },
+ "required": ["video_name"],
+ "title": "VideoField",
+ "type": "object"
+ },
+ "VideoFrameExtractInvocation": {
+ "category": "image",
+ "class": "invocation",
+ "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": {
+ "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
},
- "max_queue_history": {
+ "metadata": {
"anyOf": [
{
- "type": "integer",
- "minimum": 0.0
+ "$ref": "#/components/schemas/MetadataField"
},
{
"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."
+ "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 from.",
+ "field_kind": "input",
+ "input": "any",
+ "orig_required": true
+ },
+ "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",
+ "orig_default": -1,
+ "orig_required": false,
+ "title": "Frame Index",
+ "type": "integer"
+ },
+ "type": {
+ "const": "video_frame_extract",
+ "default": "video_frame_extract",
+ "field_kind": "node_attribute",
+ "title": "type",
+ "type": "string"
}
},
+ "required": ["type", "id"],
+ "tags": ["video", "image", "frame"],
+ "title": "Frame from Video",
"type": "object",
- "title": "UpdateAppGenerationSettingsRequest",
- "description": "Writable generation-related app settings."
+ "version": "1.0.0",
+ "output": {
+ "$ref": "#/components/schemas/ImageOutput"
+ }
},
- "UserDTO": {
+ "VideoInvocation": {
+ "category": "primitives",
+ "class": "invocation",
+ "classification": "stable",
+ "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": {
- "user_id": {
- "type": "string",
- "title": "User Id",
- "description": "Unique user identifier"
+ "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"
},
- "email": {
- "type": "string",
- "title": "Email",
- "description": "User email address"
+ "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"
},
- "display_name": {
+ "use_cache": {
+ "default": true,
+ "description": "Whether or not to use the cache",
+ "field_kind": "node_attribute",
+ "title": "Use Cache",
+ "type": "boolean"
+ },
+ "video": {
"anyOf": [
{
- "type": "string"
+ "$ref": "#/components/schemas/VideoField"
},
{
"type": "null"
}
],
- "title": "Display Name",
- "description": "Display name"
+ "default": null,
+ "description": "The video to load",
+ "field_kind": "input",
+ "input": "any",
+ "orig_required": true
},
- "is_admin": {
- "type": "boolean",
- "title": "Is Admin",
- "description": "Whether user has admin privileges",
- "default": false
+ "type": {
+ "const": "video",
+ "default": "video",
+ "field_kind": "node_attribute",
+ "title": "type",
+ "type": "string"
+ }
+ },
+ "required": ["type", "id"],
+ "tags": ["primitives", "video"],
+ "title": "Video Primitive",
+ "type": "object",
+ "version": "1.0.0",
+ "output": {
+ "$ref": "#/components/schemas/VideoOutput"
+ }
+ },
+ "VideoNamesResult": {
+ "properties": {
+ "video_names": {
+ "items": {
+ "type": "string"
+ },
+ "type": "array",
+ "title": "Video Names",
+ "description": "Ordered list of video names"
},
- "is_active": {
- "type": "boolean",
- "title": "Is Active",
- "description": "Whether user account is active",
- "default": true
+ "starred_count": {
+ "type": "integer",
+ "title": "Starred Count",
+ "description": "Number of starred videos (when starred_first=True)"
},
- "created_at": {
- "type": "string",
- "format": "date-time",
- "title": "Created At",
- "description": "When the user was created"
+ "total_count": {
+ "type": "integer",
+ "title": "Total Count",
+ "description": "Total number of videos matching the query"
+ }
+ },
+ "type": "object",
+ "required": ["video_names", "starred_count", "total_count"],
+ "title": "VideoNamesResult",
+ "description": "Response containing ordered video names with metadata for optimistic updates."
+ },
+ "VideoOutput": {
+ "class": "output",
+ "description": "Output of a node that produces a video file (e.g. Wan 2.2 latents-to-video).",
+ "properties": {
+ "video": {
+ "$ref": "#/components/schemas/VideoField",
+ "description": "The output video",
+ "field_kind": "output",
+ "ui_hidden": false
},
- "updated_at": {
- "type": "string",
- "format": "date-time",
- "title": "Updated At",
- "description": "When the user was last updated"
+ "width": {
+ "description": "The width of the video in pixels",
+ "field_kind": "output",
+ "title": "Width",
+ "type": "integer",
+ "ui_hidden": false
},
- "last_login_at": {
+ "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 video",
+ "field_kind": "output",
+ "title": "Num Frames",
+ "type": "integer",
+ "ui_hidden": false
+ },
+ "fps": {
+ "description": "The frames-per-second of the video",
+ "field_kind": "output",
+ "title": "Fps",
+ "type": "number",
+ "ui_hidden": false
+ },
+ "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",
- "format": "date-time"
+ "$ref": "#/components/schemas/ImageCategory"
+ },
+ {
+ "type": "null"
+ }
+ ],
+ "description": "The video's new category."
+ },
+ "session_id": {
+ "anyOf": [
+ {
+ "type": "string"
},
{
"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": {
+ "title": "Session Id",
+ "description": "The video's new session ID."
+ },
+ "is_intermediate": {
"anyOf": [
{
- "type": "string"
+ "type": "boolean"
},
{
"type": "null"
}
],
- "title": "Display Name",
- "description": "New display name"
+ "title": "Is Intermediate",
+ "description": "The video's new `is_intermediate` flag."
},
- "current_password": {
+ "starred": {
"anyOf": [
{
- "type": "string"
+ "type": "boolean"
},
{
"type": "null"
}
],
- "title": "Current Password",
- "description": "Current password (required when changing password)"
+ "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."
},
- "new_password": {
+ "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."
+ },
+ "cover_image_name": {
"anyOf": [
{
"type": "string"
@@ -69963,38 +75540,55 @@
"type": "null"
}
],
- "title": "New Password",
- "description": "New password"
+ "title": "Cover Image Name",
+ "description": "The most recent image name for this date."
}
},
"type": "object",
- "title": "UserProfileUpdateRequest",
- "description": "Request body for a user to update their own profile."
+ "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."
},
- "VAEField": {
+ "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": {
- "vae": {
- "$ref": "#/components/schemas/ModelIdentifierField",
- "description": "Info to load vae submodel"
+ "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
},
- "seamless_axes": {
- "description": "Axes(\"x\" and \"y\") to which apply seamless",
- "items": {
- "type": "string"
- },
- "title": "Seamless Axes",
- "type": "array"
+ "type": {
+ "const": "wan_conditioning_output",
+ "default": "wan_conditioning_output",
+ "field_kind": "node_attribute",
+ "title": "type",
+ "type": "string"
}
},
- "required": ["vae"],
- "title": "VAEField",
+ "required": ["output_meta", "conditioning", "type", "type"],
+ "title": "WanConditioningOutput",
"type": "object"
},
- "VAELoaderInvocation": {
- "category": "model",
+ "WanDenoiseInvocation": {
+ "category": "image",
"class": "invocation",
- "classification": "stable",
- "description": "Loads a VAE model, outputting a VaeLoaderOutput",
+ "classification": "prototype",
+ "description": "Run the denoising process with a Wan 2.2 model.\n\nDrives a flow-matching Euler schedule via Diffusers'\n``FlowMatchEulerDiscreteScheduler``. CFG is supported when negative\nconditioning 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": {
@@ -70021,1013 +75615,1254 @@
"title": "Use Cache",
"type": "boolean"
},
- "vae_model": {
+ "transformer": {
"anyOf": [
{
- "$ref": "#/components/schemas/ModelIdentifierField"
+ "$ref": "#/components/schemas/WanTransformerField"
},
{
"type": "null"
}
],
"default": null,
- "description": "VAE model to load",
+ "description": "Wan transformer field (transformer + optional dual-expert metadata).",
"field_kind": "input",
- "input": "any",
+ "input": "connection",
"orig_required": true,
- "title": "VAE",
- "ui_model_base": ["sd-1", "sd-2", "sdxl", "sd-3", "flux", "flux2"],
- "ui_model_type": ["vae"]
- },
- "type": {
- "const": "vae_loader",
- "default": "vae_loader",
- "field_kind": "node_attribute",
- "title": "type",
- "type": "string"
- }
- },
- "required": ["type", "id"],
- "tags": ["vae", "model"],
- "title": "VAE Model - SD1.5, SD2, SDXL, SD3, FLUX",
- "type": "object",
- "version": "1.0.4",
- "output": {
- "$ref": "#/components/schemas/VAEOutput"
- }
- },
- "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": {
- "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": "Transformer"
},
- "description": {
+ "positive_conditioning": {
"anyOf": [
{
- "type": "string"
+ "$ref": "#/components/schemas/WanConditioningField"
},
{
"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": "Positive conditioning tensor",
+ "field_kind": "input",
+ "input": "connection",
+ "orig_required": true
},
- "source_type": {
- "$ref": "#/components/schemas/ModelSourceType",
- "description": "The type of source"
+ "negative_conditioning": {
+ "anyOf": [
+ {
+ "$ref": "#/components/schemas/WanConditioningField"
+ },
+ {
+ "type": "null"
+ }
+ ],
+ "default": null,
+ "description": "Negative conditioning tensor",
+ "field_kind": "input",
+ "input": "connection",
+ "orig_default": null,
+ "orig_required": false
},
- "source_api_response": {
+ "ref_image": {
"anyOf": [
{
- "type": "string"
+ "$ref": "#/components/schemas/WanRefImageConditioningField"
},
{
"type": "null"
}
],
- "title": "Source Api Response",
- "description": "The original API response from the source, as stringified JSON."
+ "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"
},
- "source_url": {
+ "latents": {
"anyOf": [
{
- "type": "string"
+ "$ref": "#/components/schemas/LatentsField"
},
{
"type": "null"
}
],
- "title": "Source Url",
- "description": "Optional URL for the model (e.g. download page or model page)."
+ "default": null,
+ "description": "Latents tensor",
+ "field_kind": "input",
+ "input": "connection",
+ "orig_default": null,
+ "orig_required": false
},
- "cover_image": {
+ "denoise_mask": {
"anyOf": [
{
- "type": "string"
+ "$ref": "#/components/schemas/DenoiseMaskField"
},
{
"type": "null"
}
],
- "title": "Cover Image",
- "description": "Url for image to preview model"
+ "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
},
- "config_path": {
+ "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"
+ },
+ "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"
+ },
+ "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": "string"
+ "minimum": 0.0,
+ "type": "number"
},
{
"type": "null"
}
],
- "title": "Config Path",
- "description": "Path to the config for this model, if any."
+ "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)"
},
- "type": {
- "type": "string",
- "const": "vae",
- "title": "Type",
- "default": "vae"
+ "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"
},
- "format": {
- "type": "string",
- "const": "checkpoint",
- "title": "Format",
- "default": "checkpoint"
+ "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"
},
- "base": {
- "type": "string",
- "const": "anima",
- "title": "Base",
- "default": "anima"
+ "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"
- ],
- "title": "VAE_Checkpoint_Anima_Config",
- "description": "Model config for Anima QwenImage VAE checkpoint models (AutoencoderKLQwenImage)."
+ "version": "1.0.0",
+ "output": {
+ "$ref": "#/components/schemas/LatentsOutput"
+ }
},
- "VAE_Checkpoint_FLUX_Config": {
+ "WanI2VIdealDimensionsInvocation": {
+ "category": "video",
+ "class": "invocation",
+ "classification": "stable",
+ "description": "Compute Wan I2V-compatible (width, height) for a chosen resolution preset.\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 (Wan's 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."
- },
- "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."
+ "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"
},
- "description": {
- "anyOf": [
- {
- "type": "string"
- },
- {
- "type": "null"
- }
- ],
- "title": "Description",
- "description": "Model description"
+ "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"
},
- "source": {
+ "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": "Source",
- "description": "The original source of the model (path, URL or repo_id)."
+ "ui_choice_labels": {
+ "1080p": "1080p (extrapolated \u2014 not a Wan training size)",
+ "480p": "480p (Wan native)",
+ "720p": "720p (Wan native, default)"
+ }
},
- "source_type": {
- "$ref": "#/components/schemas/ModelSourceType",
- "description": "The type of source"
+ "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"
},
- "source_api_response": {
+ "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",
+ "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": "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": {
+ "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": "Cover Image",
- "description": "Url for image to preview model"
+ "default": null,
+ "description": "The image to encode.",
+ "field_kind": "input",
+ "input": "any",
+ "orig_required": true
},
- "config_path": {
+ "vae": {
"anyOf": [
{
- "type": "string"
+ "$ref": "#/components/schemas/VAEField"
},
{
"type": "null"
}
],
- "title": "Config Path",
- "description": "Path to the config for this model, if any."
+ "default": null,
+ "description": "VAE",
+ "field_kind": "input",
+ "input": "connection",
+ "orig_required": true
},
"type": {
- "type": "string",
- "const": "vae",
- "title": "Type",
- "default": "vae"
- },
- "format": {
- "type": "string",
- "const": "checkpoint",
- "title": "Format",
- "default": "checkpoint"
- },
- "base": {
- "type": "string",
- "const": "flux",
- "title": "Base",
- "default": "flux"
+ "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",
- "required": [
- "key",
- "hash",
- "path",
- "file_size",
- "name",
- "description",
- "source",
- "source_type",
- "source_api_response",
- "source_url",
- "cover_image",
- "config_path",
- "type",
- "format",
- "base"
- ],
- "title": "VAE_Checkpoint_FLUX_Config"
+ "version": "1.0.0",
+ "output": {
+ "$ref": "#/components/schemas/LatentsOutput"
+ }
},
- "VAE_Checkpoint_Flux2_Config": {
+ "WanLatentsToImageInvocation": {
+ "category": "latents",
+ "class": "invocation",
+ "classification": "prototype",
+ "description": "Decodes Wan latents back to RGB.",
+ "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": {
+ "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": "Cover Image",
- "description": "Url for image to preview model"
+ "default": null,
+ "description": "Latents tensor",
+ "field_kind": "input",
+ "input": "connection",
+ "orig_required": true
},
- "config_path": {
+ "vae": {
"anyOf": [
{
- "type": "string"
+ "$ref": "#/components/schemas/VAEField"
},
{
"type": "null"
}
],
- "title": "Config Path",
- "description": "Path to the config for this model, if any."
+ "default": null,
+ "description": "VAE",
+ "field_kind": "input",
+ "input": "connection",
+ "orig_required": true
},
"type": {
- "type": "string",
- "const": "vae",
- "title": "Type",
- "default": "vae"
- },
- "format": {
- "type": "string",
- "const": "checkpoint",
- "title": "Format",
- "default": "checkpoint"
- },
- "base": {
- "type": "string",
- "const": "flux2",
- "title": "Base",
- "default": "flux2"
+ "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",
- "required": [
- "key",
- "hash",
- "path",
- "file_size",
- "name",
- "description",
- "source",
- "source_type",
- "source_api_response",
- "source_url",
- "cover_image",
- "config_path",
- "type",
- "format",
- "base"
- ],
- "title": "VAE_Checkpoint_Flux2_Config",
- "description": "Model config for FLUX.2 VAE checkpoint models (AutoencoderKLFlux2)."
+ "version": "1.0.0",
+ "output": {
+ "$ref": "#/components/schemas/ImageOutput"
+ }
},
- "VAE_Checkpoint_QwenImage_Config": {
+ "WanLatentsToVideoInvocation": {
+ "category": "latents",
+ "class": "invocation",
+ "classification": "prototype",
+ "description": "Decode 5D Wan latents to RGB frames and encode an MP4.",
+ "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": {
+ "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": "Cover Image",
- "description": "Url for image to preview model"
+ "default": null,
+ "description": "Latents tensor",
+ "field_kind": "input",
+ "input": "connection",
+ "orig_required": true
},
- "config_path": {
+ "vae": {
"anyOf": [
{
- "type": "string"
+ "$ref": "#/components/schemas/VAEField"
},
{
"type": "null"
}
],
- "title": "Config Path",
- "description": "Path to the config for this model, if any."
- },
- "type": {
- "type": "string",
- "const": "vae",
- "title": "Type",
- "default": "vae"
+ "default": null,
+ "description": "VAE",
+ "field_kind": "input",
+ "input": "connection",
+ "orig_required": true
},
- "format": {
- "type": "string",
- "const": "checkpoint",
- "title": "Format",
- "default": "checkpoint"
+ "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"
},
- "base": {
- "type": "string",
- "const": "qwen-image",
- "title": "Base",
- "default": "qwen-image"
+ "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",
- "base"
- ],
- "title": "VAE_Checkpoint_QwenImage_Config",
- "description": "Model config for Qwen Image VAE checkpoint models (AutoencoderKLQwenImage)."
+ "version": "1.0.0",
+ "output": {
+ "$ref": "#/components/schemas/VideoOutput"
+ }
},
- "VAE_Checkpoint_SD1_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)."
- },
- "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": {
+ "loras": {
"anyOf": [
{
- "type": "string"
+ "$ref": "#/components/schemas/LoRAField"
},
{
- "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"
+ "items": {
+ "$ref": "#/components/schemas/LoRAField"
+ },
+ "type": "array"
},
{
"type": "null"
}
],
- "title": "Source Api Response",
- "description": "The original API response from the source, as stringified JSON."
+ "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"
},
- "source_url": {
+ "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": "Transformer",
+ "field_kind": "input",
+ "input": "connection",
+ "orig_default": null,
+ "orig_required": false,
+ "title": "Wan Transformer"
},
- "cover_image": {
+ "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.0",
+ "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; the low-noise routing is harmless but ignored.",
+ "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": "Cover Image",
- "description": "Url for image to preview model"
+ "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"]
},
- "config_path": {
+ "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"
+ },
+ "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"
+ },
+ "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"
- },
- "base": {
- "type": "string",
- "const": "sd-1",
- "title": "Base",
- "default": "sd-1"
+ "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",
- "required": [
- "key",
- "hash",
- "path",
- "file_size",
- "name",
- "description",
- "source",
- "source_type",
- "source_api_response",
- "source_url",
- "cover_image",
- "config_path",
- "type",
- "format",
- "base"
- ],
- "title": "VAE_Checkpoint_SD1_Config"
+ "version": "1.0.0",
+ "output": {
+ "$ref": "#/components/schemas/WanLoRALoaderOutput"
+ }
},
- "VAE_Checkpoint_SD2_Config": {
+ "WanLoRALoaderOutput": {
+ "class": "output",
+ "description": "Wan 2.2 LoRA loader 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": {
+ "transformer": {
"anyOf": [
{
- "type": "string"
+ "$ref": "#/components/schemas/WanTransformerField"
},
{
"type": "null"
}
],
- "title": "Description",
- "description": "Model description"
+ "default": null,
+ "description": "Transformer",
+ "field_kind": "output",
+ "title": "Wan Transformer",
+ "ui_hidden": false
},
- "source": {
- "type": "string",
- "title": "Source",
- "description": "The original source of the model (path, URL or repo_id)."
+ "type": {
+ "const": "wan_lora_loader_output",
+ "default": "wan_lora_loader_output",
+ "field_kind": "node_attribute",
+ "title": "type",
+ "type": "string"
+ }
+ },
+ "required": ["output_meta", "transformer", "type", "type"],
+ "title": "WanLoRALoaderOutput",
+ "type": "object"
+ },
+ "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": {
+ "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"
+ },
+ "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"]
+ },
+ "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"
- },
- "base": {
- "type": "string",
- "const": "sd-2",
- "title": "Base",
- "default": "sd-2"
+ "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",
- "base"
- ],
- "title": "VAE_Checkpoint_SD2_Config"
+ "version": "1.0.0",
+ "output": {
+ "$ref": "#/components/schemas/WanModelLoaderOutput"
+ }
},
- "VAE_Checkpoint_SDXL_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\nOutput is a ``[1, 20, T_lat, height // 8, width // 8]`` condition tensor\nthat the denoise loop concatenates to the 16-channel noise latents each\nstep, producing the 36-channel input the I2V-A14B transformer expects.\n\nFor image (single-frame) I2V leave ``num_frames=1`` (T_lat=1). For video\nI2V set ``num_frames`` to match the value on the video-denoise node\n(e.g. 81 for the Wan 2.2 reference defaults).\n\nOnly works with I2V-A14B (the denoise loop's variant gate enforces this).\nFor T2V or TI2V-5B, 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": {
- "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": {
+ "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": "Reference image to condition on.",
+ "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,
+ "title": "VAE"
},
- "config_path": {
- "anyOf": [
- {
- "type": "string"
- },
- {
- "type": "null"
- }
- ],
- "title": "Config Path",
- "description": "Path to the config for this model, if any."
+ "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"
+ },
+ "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.1.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"
},
- "format": {
- "type": "string",
- "const": "checkpoint",
- "title": "Format",
- "default": "checkpoint"
+ "text_encoder": {
+ "$ref": "#/components/schemas/ModelIdentifierField",
+ "description": "Info to load text_encoder submodel"
},
- "base": {
- "type": "string",
- "const": "sdxl",
- "title": "Base",
- "default": "sdxl"
+ "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",
- "config_path",
- "type",
- "format",
- "base"
- ],
- "title": "VAE_Checkpoint_SDXL_Config"
+ "required": ["tokenizer", "text_encoder"],
+ "title": "WanT5EncoderField",
+ "type": "object"
},
- "VAE_Diffusers_Flux2_Config": {
+ "WanT5Encoder_WanT5Encoder_Config": {
"properties": {
"key": {
"type": "string",
@@ -71111,27 +76946,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"
},
- "base": {
+ "format": {
"type": "string",
- "const": "flux2",
- "title": "Base",
- "default": "flux2"
+ "const": "wan_t5_encoder",
+ "title": "Format",
+ "default": "wan_t5_encoder"
}
},
"type": "object",
@@ -71147,42 +76978,45 @@
"source_api_response",
"source_url",
"cover_image",
- "format",
- "repo_variant",
+ "base",
"type",
- "base"
+ "format"
],
- "title": "VAE_Diffusers_Flux2_Config",
- "description": "Model config for FLUX.2 VAE models in diffusers format (AutoencoderKLFlux2)."
+ "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_SD1_Config": {
+ "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=226, 4096]) plus\nan attention mask marking valid (non-padding) tokens. The Wan transformer\nconsumes these directly as ``encoder_hidden_states``.",
+ "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": {
+ "prompt": {
"anyOf": [
{
"type": "string"
@@ -71191,297 +77025,295 @@
"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": "Text prompt for Wan 2.2.",
+ "field_kind": "input",
+ "input": "any",
+ "orig_required": true,
+ "title": "Prompt",
+ "ui_component": "textarea"
},
- "source_api_response": {
+ "wan_t5_encoder": {
"anyOf": [
{
- "type": "string"
+ "$ref": "#/components/schemas/WanT5EncoderField"
},
{
"type": "null"
}
],
- "title": "Source Api Response",
- "description": "The original API response from the source, as stringified JSON."
+ "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_url": {
- "anyOf": [
- {
- "type": "string"
- },
- {
- "type": "null"
- }
- ],
- "title": "Source Url",
- "description": "Optional URL for the model (e.g. download page or model page)."
+ "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."
},
- "cover_image": {
+ "transformer_low_noise": {
"anyOf": [
{
- "type": "string"
+ "$ref": "#/components/schemas/ModelIdentifierField"
},
{
"type": "null"
}
],
- "title": "Cover Image",
- "description": "Url for image to preview model"
- },
- "format": {
- "type": "string",
- "const": "diffusers",
- "title": "Format",
- "default": "diffusers"
+ "default": null,
+ "description": "Low-noise transformer expert (Wan 2.2 A14B only). None for TI2V-5B."
},
- "repo_variant": {
- "$ref": "#/components/schemas/ModelRepoVariant",
- "default": ""
+ "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"
},
- "type": {
- "type": "string",
- "const": "vae",
- "title": "Type",
- "default": "vae"
+ "loras_low_noise": {
+ "description": "Optional separate LoRAs for the low-noise expert (Wan 2.2 A14B). If empty and transformer_low_noise is set, the primary 'loras' list is reused.",
+ "items": {
+ "$ref": "#/components/schemas/LoRAField"
+ },
+ "title": "Loras Low Noise",
+ "type": "array"
},
- "base": {
- "type": "string",
- "const": "sd-1",
- "title": "Base",
- "default": "sd-1"
+ "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"
}
},
- "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"
- ],
- "title": "VAE_Diffusers_SD1_Config"
+ "required": ["transformer"],
+ "title": "WanTransformerField",
+ "type": "object"
},
- "VAE_Diffusers_SDXL_Config": {
+ "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": {
- "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": {
+ "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)."
- },
- "source_type": {
- "$ref": "#/components/schemas/ModelSourceType",
- "description": "The type of source"
+ "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"
},
- "source_api_response": {
+ "positive_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": "Positive conditioning tensor",
+ "field_kind": "input",
+ "input": "connection",
+ "orig_required": true
},
- "source_url": {
+ "negative_conditioning": {
"anyOf": [
{
- "type": "string"
+ "$ref": "#/components/schemas/WanConditioningField"
},
{
"type": "null"
}
],
- "title": "Source Url",
- "description": "Optional URL for the model (e.g. download page or model page)."
+ "default": null,
+ "description": "Negative conditioning tensor",
+ "field_kind": "input",
+ "input": "connection",
+ "orig_default": null,
+ "orig_required": false
},
- "cover_image": {
+ "ref_image": {
"anyOf": [
{
- "type": "string"
+ "$ref": "#/components/schemas/WanRefImageConditioningField"
},
{
"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"
- },
- "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",
- "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'."
- },
- "image_count": {
- "type": "integer",
- "title": "Image Count",
- "description": "The number of general images for this date."
+ "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 a79eab2f70e..f53534fe21a 100644
--- a/invokeai/frontend/web/public/locales/en.json
+++ b/invokeai/frontend/web/public/locales/en.json
@@ -129,6 +129,10 @@
"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",
@@ -529,6 +533,12 @@
"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",
"download": "Download",
@@ -554,7 +564,19 @@
"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",
@@ -1372,6 +1394,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",
@@ -1411,6 +1441,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",
@@ -1673,6 +1707,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",
@@ -1728,6 +1763,7 @@
"showOptionsPanel": "Show Side Panel (O or T)",
"shift": "Shift",
"shuffle": "Shuffle Seed",
+ "wanGuidanceScaleLowNoise": "CFG (Low)",
"steps": "Steps",
"strength": "Strength",
"symmetry": "Symmetry",
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/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/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 20303fe0183..62f813db357 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
@@ -18,6 +18,9 @@ import {
setZImageScheduler,
syncedToOptimalDimension,
vaeSelected,
+ wanComponentSourceSelected,
+ wanT5EncoderModelSelected,
+ wanVaeModelSelected,
zImageQwen3EncoderModelSelected,
zImageQwen3SourceModelSelected,
zImageVaeModelSelected,
@@ -37,6 +40,7 @@ import {
isAspectRatioID,
isFlux2ReferenceImageConfig,
isQwenImageReferenceImageConfig,
+ isWanReferenceImageConfig,
} from 'features/controlLayers/store/types';
import {
initialFlux2ReferenceImage,
@@ -44,6 +48,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';
@@ -63,6 +68,9 @@ import {
selectQwenVLEncoderModels,
selectRegionalRefImageModels,
selectT5EncoderModels,
+ selectWanDiffusersModels,
+ selectWanT5EncoderModels,
+ selectWanVAEModels,
selectZImageDiffusersModels,
} from 'services/api/hooks/modelsByType';
import type { FLUXKontextModelConfig, FLUXReduxModelConfig, IPAdapterModelConfig } from 'services/api/types';
@@ -321,6 +329,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.
@@ -377,6 +408,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;
@@ -425,6 +472,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) ||
@@ -480,6 +550,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/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/useImageUploadButton.tsx b/invokeai/frontend/web/src/common/hooks/useImageUploadButton.tsx
index fc173de979f..c660cf0daf0 100644
--- a/invokeai/frontend/web/src/common/hooks/useImageUploadButton.tsx
+++ b/invokeai/frontend/web/src/common/hooks/useImageUploadButton.tsx
@@ -10,7 +10,8 @@ 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';
@@ -24,6 +25,18 @@ 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[]),
+ 'video/mp4': ['.mp4'].reduce(addUpperCaseReducer, [] as string[]),
+ 'video/webm': ['.webm'].reduce(addUpperCaseReducer, [] as string[]),
+ 'video/quicktime': ['.mov'].reduce(addUpperCaseReducer, [] as string[]),
+};
+
+/** Returns true when the file looks like a video (by MIME or by extension). */
+const isVideoFile = (file: File): boolean => {
+ if (file.type && file.type.startsWith('video/')) {
+ return true;
+ }
+ const name = file.name.toLowerCase();
+ return name.endsWith('.mp4') || name.endsWith('.webm') || name.endsWith('.mov') || name.endsWith('.mkv');
};
type UseImageUploadButtonArgs =
@@ -31,6 +44,8 @@ type UseImageUploadButtonArgs =
isDisabled?: boolean;
allowMultiple: false;
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;
}
@@ -38,6 +53,7 @@ type UseImageUploadButtonArgs =
isDisabled?: boolean;
allowMultiple: true;
onUpload?: (imageDTOs: ImageDTO[]) => void;
+ onUploadVideo?: (videoDTOs: VideoDTO[]) => void;
onUploadStarted?: (files: File[]) => void;
onError?: (error: unknown) => void;
};
@@ -65,6 +81,7 @@ const log = logger('gallery');
*/
export const useImageUploadButton = ({
onUpload,
+ onUploadVideo,
isDisabled,
allowMultiple,
onUploadStarted,
@@ -72,6 +89,7 @@ export const useImageUploadButton = ({
}: UseImageUploadButtonArgs) => {
const autoAddBoardId = useAppSelector(selectAutoAddBoardId);
const [uploadImage, request] = useUploadImageMutation();
+ const [uploadVideo] = useUploadVideoMutation();
const { t } = useTranslation();
const onDropAccepted = useCallback(
@@ -90,32 +108,68 @@ 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 (isVideoFile(file)) {
+ 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);
+
+ // Split the dropped files into images and videos and upload each set through
+ // its own batch helper so a single drop can include a mix.
+ const imageFiles = files.filter((f) => !isVideoFile(f));
+ const videoFiles = files.filter((f) => isVideoFile(f));
+
+ 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);
}
}
} catch (error) {
@@ -127,7 +181,7 @@ export const useImageUploadButton = ({
});
}
},
- [allowMultiple, onUploadStarted, uploadImage, autoAddBoardId, onUpload, onError, t]
+ [allowMultiple, onUploadStarted, uploadImage, uploadVideo, autoAddBoardId, onUpload, onUploadVideo, onError, t]
);
const onDropRejected = useCallback(
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/RefImageSettings.tsx b/invokeai/frontend/web/src/features/controlLayers/components/RefImage/RefImageSettings.tsx
index 54b345361d5..3edf9594b79 100644
--- a/invokeai/frontend/web/src/features/controlLayers/components/RefImage/RefImageSettings.tsx
+++ b/invokeai/frontend/web/src/features/controlLayers/components/RefImage/RefImageSettings.tsx
@@ -39,6 +39,7 @@ import {
isFLUXReduxConfig,
isIPAdapterConfig,
isQwenImageReferenceImageConfig,
+ isWanReferenceImageConfig,
} from 'features/controlLayers/store/types';
import type { SetGlobalReferenceImageDndTargetData } from 'features/dnd/dnd';
import { setGlobalReferenceImageDndTarget } from 'features/dnd/dnd';
@@ -129,9 +130,12 @@ const RefImageSettingsContent = memo(() => {
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 a5200ef1ff8..21fecaf6437 100644
--- a/invokeai/frontend/web/src/features/controlLayers/store/paramsSlice.ts
+++ b/invokeai/frontend/web/src/features/controlLayers/store/paramsSlice.ts
@@ -291,6 +291,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;
},
@@ -610,6 +641,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;
};
@@ -662,6 +698,11 @@ export const {
qwenImageQwenVLEncoderModelSelected,
qwenImageQuantizationChanged,
qwenImageShiftChanged,
+ wanTransformerLowNoiseSelected,
+ wanComponentSourceSelected,
+ wanVaeModelSelected,
+ wanT5EncoderModelSelected,
+ wanGuidanceScaleLowNoiseChanged,
setClipSkip,
shouldUseCpuNoiseChanged,
setColorCompensation,
@@ -752,6 +793,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;
@@ -783,6 +825,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);
@@ -842,7 +889,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 cbeccdfa930..6820d6e1ea8 100644
--- a/invokeai/frontend/web/src/features/controlLayers/store/types.ts
+++ b/invokeai/frontend/web/src/features/controlLayers/store/types.ts
@@ -418,6 +418,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,
@@ -434,6 +443,7 @@ export const zRefImageState = z.object({
zFluxKontextReferenceImageConfig,
zFlux2ReferenceImageConfig,
zQwenImageReferenceImageConfig,
+ zWanReferenceImageConfig,
]),
});
export type RefImageState = z.infer;
@@ -455,6 +465,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;
@@ -843,6 +856,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),
@@ -928,6 +948,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 c8cb49dde3f..9f0fd779e70 100644
--- a/invokeai/frontend/web/src/features/controlLayers/store/util.ts
+++ b/invokeai/frontend/web/src/features/controlLayers/store/util.ts
@@ -21,6 +21,7 @@ import type {
RegionalGuidanceIPAdapterConfig,
RgbColor,
T2IAdapterConfig,
+ WanReferenceImageConfig,
ZImageControlConfig,
} from 'features/controlLayers/store/types';
import type { ImageDTO } from 'services/api/types';
@@ -122,6 +123,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 db5ad4f7662..c1ea2b4797c 100644
--- a/invokeai/frontend/web/src/features/controlLayers/store/validators.ts
+++ b/invokeai/frontend/web/src/features/controlLayers/store/validators.ts
@@ -147,8 +147,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);
@@ -159,8 +163,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.ts b/invokeai/frontend/web/src/features/deleteImageModal/store/state.ts
index c50aa9465f5..2df56133f89 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';
@@ -80,22 +84,25 @@ 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;
+
+ await dispatch(imagesApi.endpoints.deleteImages.initiate({ image_names }, { track: false })).unwrap();
if (intersection(state.gallery.selection, image_names).length > 0) {
- if (newSelectedImage) {
- // Some selected images were deleted, clear selection
- dispatch(imageSelected(newSelectedImage));
- } 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, new Set(image_names))
+ : 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
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..38c2d7828ca
--- /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.ts b/invokeai/frontend/web/src/features/deleteVideoModal/store/state.ts
new file mode 100644
index 00000000000..0a0b59868d4
--- /dev/null
+++ b/invokeai/frontend/web/src/features/deleteVideoModal/store/state.ts
@@ -0,0 +1,118 @@
+import { useStore } from '@nanostores/react';
+import type { AppStore } 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 { selectSystemShouldConfirmOnDelete } from 'features/system/store/systemSlice';
+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, node fields, ref-image entities, or upscale inputs the way images do,
+// so there is no "usage" analysis to compute. The dialog is a straight confirm.
+
+type DeleteVideosModalState = {
+ video_names: string[];
+ isOpen: boolean;
+ resolve?: () => void;
+ reject?: (reason?: string) => void;
+};
+
+const getInitialState = (): DeleteVideosModalState => ({
+ video_names: [],
+ isOpen: false,
+});
+
+const $deleteModalState = atom(getInitialState());
+
+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,
+ });
+ });
+};
+
+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;
+
+ // The backend exposes single-video DELETE today; loop here so the API surface for callers
+ // stays "give me a list, I'll handle it" and a future bulk endpoint can be slotted in
+ // without touching call sites.
+ for (const video_name of video_names) {
+ try {
+ await dispatch(videosApi.endpoints.deleteVideo.initiate({ video_name }, { track: false })).unwrap();
+ } catch {
+ // Continue with the rest of the batch — partial failures shouldn't leave the user
+ // with a broken modal state.
+ }
+ }
+
+ // If anything in the active selection was deleted, advance to a still-living neighbour
+ // (prev > next) so the Viewer doesn't drop to its empty-state placeholder.
+ const stateAfter = getState();
+ if (intersection(stateAfter.gallery.selection, video_names).length > 0) {
+ const replacement =
+ lastSelectedIndex >= 0
+ ? pickSelectionAfterDelete(galleryItemNames, lastSelectedIndex, new Set(video_names))
+ : null;
+ dispatch(imageSelected(replacement));
+ }
+};
+
+const confirmDeletion = async (store: AppStore) => {
+ const state = $deleteModalState.get();
+ await handleDeletions(state.video_names, store);
+ state.resolve?.();
+ closeSilently();
+};
+
+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/FullscreenDropzone.tsx b/invokeai/frontend/web/src/features/dnd/FullscreenDropzone.tsx
index e5d7df68f28..5df7ee23715 100644
--- a/invokeai/frontend/web/src/features/dnd/FullscreenDropzone.tsx
+++ b/invokeai/frontend/web/src/features/dnd/FullscreenDropzone.tsx
@@ -16,12 +16,23 @@ import { selectActiveTab } from 'features/ui/store/uiSelectors';
import { memo, useCallback, useEffect, useRef, useState } from 'react';
import { useTranslation } from 'react-i18next';
import { uploadImages } from 'services/api/endpoints/images';
+import { uploadVideos } from 'services/api/endpoints/videos';
import { useBoardName } from 'services/api/hooks/useBoardName';
-import type { UploadImageArg } from 'services/api/types';
+import type { UploadImageArg, UploadVideoArg } from 'services/api/types';
import { z } from 'zod';
const ACCEPTED_IMAGE_TYPES = ['image/png', 'image/jpg', 'image/jpeg', 'image/webp'];
-const ACCEPTED_FILE_EXTENSIONS = ['.png', '.jpg', '.jpeg', '.webp'];
+const ACCEPTED_VIDEO_TYPES = ['video/mp4', 'video/webm', 'video/quicktime', 'video/x-matroska'];
+const ACCEPTED_IMAGE_EXTENSIONS = ['.png', '.jpg', '.jpeg', '.webp'];
+const ACCEPTED_VIDEO_EXTENSIONS = ['.mp4', '.webm', '.mov', '.mkv'];
+
+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));
+};
// const MAX_IMAGE_SIZE = 4; //In MegaBytes
// const sizeInMB = (sizeInBytes: number, decimalsNum = 2) => {
@@ -39,13 +50,18 @@ const zUploadFile = z
// )
.refine(
(file) => {
- return ACCEPTED_IMAGE_TYPES.includes(file.type.toLowerCase());
+ const type = file.type.toLowerCase();
+ return ACCEPTED_IMAGE_TYPES.includes(type) || ACCEPTED_VIDEO_TYPES.includes(type);
},
{ message: `File type is not supported` }
)
.refine(
(file) => {
- return ACCEPTED_FILE_EXTENSIONS.some((ext) => file.name.toLowerCase().endsWith(ext));
+ const lower = file.name.toLowerCase();
+ return (
+ ACCEPTED_IMAGE_EXTENSIONS.some((ext) => lower.endsWith(ext)) ||
+ ACCEPTED_VIDEO_EXTENSIONS.some((ext) => lower.endsWith(ext))
+ );
},
{ message: `File extension is not supported` }
);
@@ -86,25 +102,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]
);
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/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/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..ae1daee7a67
--- /dev/null
+++ b/invokeai/frontend/web/src/features/gallery/components/ContextMenu/VideoContextMenu.tsx
@@ -0,0 +1,238 @@
+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 { selectSelectionCount } 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 selectionCount = useAppSelector(selectSelectionCount);
+ if (!state.videoDTO) {
+ return null;
+ }
+ if (selectionCount > 1) {
+ 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 1b20edba172..d56c5b37170 100644
--- a/invokeai/frontend/web/src/features/gallery/components/GalleryImageGrid.tsx
+++ b/invokeai/frontend/web/src/features/gallery/components/GalleryImageGrid.tsx
@@ -12,6 +12,7 @@ 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';
@@ -29,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';
@@ -47,35 +50,40 @@ 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);
+ // 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) {
+ const videoDTO = videoState.currentData;
+ if (!videoDTO) {
+ return ;
+ }
+ return ;
+ }
+
+ const imageDTO = imageState.currentData;
if (!imageDTO) {
return ;
}
-
return ;
});
ImageAtPosition.displayName = 'ImageAtPosition';
@@ -310,30 +318,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/ImageGrid/GalleryImage.tsx b/invokeai/frontend/web/src/features/gallery/components/ImageGrid/GalleryImage.tsx
index d03a884a094..1e0cf5de9f3 100644
--- a/invokeai/frontend/web/src/features/gallery/components/ImageGrid/GalleryImage.tsx
+++ b/invokeai/frontend/web/src/features/gallery/components/ImageGrid/GalleryImage.tsx
@@ -15,18 +15,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 +36,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 +61,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();
}
@@ -136,11 +133,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,
});
}
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..0f07650b725
--- /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, Image } 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 { 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';
+
+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);
+
+ // 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/ImageViewer/CurrentVideoPreview.tsx b/invokeai/frontend/web/src/features/gallery/components/ImageViewer/CurrentVideoPreview.tsx
new file mode 100644
index 00000000000..cc46b319dab
--- /dev/null
+++ b/invokeai/frontend/web/src/features/gallery/components/ImageViewer/CurrentVideoPreview.tsx
@@ -0,0 +1,360 @@
+import { Box, Button, Flex, IconButton } from '@invoke-ai/ui-library';
+import { useStore } from '@nanostores/react';
+import { useAppSelector } from 'app/store/storeHooks';
+import { useClipboard } from 'common/hooks/useClipboard';
+import { useDownloadItem } from 'common/hooks/useDownloadImage';
+import { useDeleteVideoModalApi } from 'features/deleteVideoModal/store/state';
+import NextPrevItemButtons from 'features/gallery/components/NextPrevItemButtons';
+import { useNextPrevItemNavigation } from 'features/gallery/components/useNextPrevItemNavigation';
+import { useRegisteredHotkeys } from 'features/system/components/HotkeysModal/useHotkeyData';
+import { toast } from 'features/toast/toast';
+import { navigationApi } from 'features/ui/layouts/navigation-api';
+import { selectActiveTab, 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 { t } = useTranslation();
+ const videoName = videoDTO?.video_name ?? null;
+ const videoRef = useRef(null);
+ const [isPlaying, setIsPlaying] = useState(false);
+ const shouldShowProgressInViewer = useAppSelector(selectShouldShowProgressInViewer);
+ 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]);
+
+ 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.
+ void videoRef.current?.play();
+ }, []);
+
+ // 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 && }
+ {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/ImageViewer.tsx b/invokeai/frontend/web/src/features/gallery/components/ImageViewer/ImageViewer.tsx
index ce9795ee8b0..35f5e40eaca 100644
--- a/invokeai/frontend/web/src/features/gallery/components/ImageViewer/ImageViewer.tsx
+++ b/invokeai/frontend/web/src/features/gallery/components/ImageViewer/ImageViewer.tsx
@@ -1,12 +1,13 @@
import { Divider, Flex } from '@invoke-ai/ui-library';
import { useAppSelector } from 'app/store/storeHooks';
+import { useGalleryItemDTO } from 'common/hooks/useGalleryItemDTO';
import { setComparisonImageDndTarget } from 'features/dnd/dnd';
import { DndDropTarget } from 'features/dnd/DndDropTarget';
import { CurrentImagePreview } from 'features/gallery/components/ImageViewer/CurrentImagePreview';
+import { CurrentVideoPreview } from 'features/gallery/components/ImageViewer/CurrentVideoPreview';
import { selectLastSelectedItem } from 'features/gallery/store/gallerySelectors';
import { memo } from 'react';
import { useTranslation } from 'react-i18next';
-import { useImageDTO } from 'services/api/endpoints/images';
import { ImageViewerToolbar } from './ImageViewerToolbar';
@@ -16,18 +17,30 @@ export const ImageViewer = memo(() => {
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..dac87094e4b 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,28 @@
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 { ToggleProgressButton } from './ToggleProgressButton';
export const ImageViewerToolbar = memo(() => {
const lastSelectedItem = useAppSelector(selectLastSelectedItem);
- const imageDTO = useImageDTO(lastSelectedItem);
+ const galleryItem = useGalleryItemDTO(lastSelectedItem);
+
+ // Videos don't carry workflows or recallable metadata yet — the action row + metadata viewer
+ // toggle are image-specific. We still show the progress button (it's media-agnostic).
+ const showImageActions = galleryItem?.kind === 'image';
return (
- {imageDTO && }
+ {showImageActions && }
- {imageDTO && }
+ {showImageActions && }
);
});
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.ts b/invokeai/frontend/web/src/features/gallery/components/use-gallery-image-names.ts
index 487c5609062..b6c965550be 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,11 +3,26 @@ 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 { useMemo } from 'react';
+import { useGetGalleryItemNamesQuery } from 'services/api/endpoints/gallery';
import { useGetVirtualBoardImageNamesByDateQuery } from 'services/api/endpoints/virtual_boards';
import { useDebounce } from 'use-debounce';
-const selectFromResult = ({
+const selectFromGalleryItemNamesResult = ({
+ currentData,
+ isLoading,
+ isFetching,
+}: {
+ currentData?: { items: { kind: 'image' | 'video'; name: string }[] };
+ isLoading: boolean;
+ isFetching: boolean;
+}) => ({
+ items: currentData?.items ?? (EMPTY_ARRAY as { kind: 'image' | 'video'; name: string }[]),
+ isLoading,
+ isFetching,
+});
+
+const selectFromVirtualBoardResult = ({
currentData,
isLoading,
isFetching,
@@ -21,41 +36,60 @@ const selectFromResult = ({
isFetching,
});
-const queryOptions = {
+const galleryQueryOptions = {
+ refetchOnReconnect: true,
+ selectFromResult: selectFromGalleryItemNamesResult,
+};
+
+const virtualBoardQueryOptions = {
refetchOnReconnect: true,
- selectFromResult,
+ selectFromResult: selectFromVirtualBoardResult,
};
+/**
+ * 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) are image-only for now and call the legacy by-date 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(
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
+ virtualBoardQueryOptions
);
- 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(() => {
+ if (isVirtual) {
+ return virtualResult.imageNames;
+ }
+ return galleryResult.items.map((ref) => ref.name);
+ }, [isVirtual, virtualResult.imageNames, 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/useRangeBasedImageFetching.ts b/invokeai/frontend/web/src/features/gallery/hooks/useRangeBasedImageFetching.ts
index 8ea8e9023e3..8229d0b7e37 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 {
@@ -30,9 +32,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 +48,34 @@ 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');
+ const uncachedVideoNames = getUncachedNames(allNames, cachedVideoNames, ranges).filter((n) => isVideoName(n));
+ for (const videoName of uncachedVideoNames) {
+ store.dispatch(videosApi.endpoints.getVideoDTO.initiate(videoName));
+ }
+
setPendingRanges([]);
},
[enabled, getImageDTOsByNames, store]
);
- const throttledFetchImages = useThrottledCallback(fetchImages, 500);
+ const throttledFetchItems = useThrottledCallback(fetchItems, 500);
const onRangeChanged = useCallback((range: ListRange) => {
setLastRange(range);
@@ -68,8 +84,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/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 60c6ba49fcb..d238001a9e5 100644
--- a/invokeai/frontend/web/src/features/metadata/parsing.tsx
+++ b/invokeai/frontend/web/src/features/metadata/parsing.tsx
@@ -58,6 +58,11 @@ import {
setZImageSeedVarianceStrength,
setZImageShift,
vaeSelected,
+ wanComponentSourceSelected,
+ wanGuidanceScaleLowNoiseChanged,
+ wanT5EncoderModelSelected,
+ wanTransformerLowNoiseSelected,
+ wanVaeModelSelected,
widthChanged,
zImageQwen3EncoderModelSelected,
zImageQwen3SourceModelSelected,
@@ -845,6 +850,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,
@@ -1665,6 +1797,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/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 60a3f8e472a..0cfdd37b879 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
@@ -13,6 +13,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';
@@ -57,6 +58,8 @@ import {
isStringGeneratorFieldInputTemplate,
isStylePresetFieldInputInstance,
isStylePresetFieldInputTemplate,
+ isVideoFieldInputInstance,
+ isVideoFieldInputTemplate,
} from 'features/nodes/types/field';
import type { NodeFieldElement } from 'features/nodes/types/workflow';
import { memo } from 'react';
@@ -70,6 +73,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;
@@ -124,6 +128,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 ;
}
@@ -202,6 +213,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..4b9e7796b6f
--- /dev/null
+++ b/invokeai/frontend/web/src/features/nodes/components/flow/nodes/Invocation/fields/IntegerField/VideoFrameIndexFieldInput.tsx
@@ -0,0 +1,199 @@
+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 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);
+
+ // 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 && 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..c3e4d85958b
--- /dev/null
+++ b/invokeai/frontend/web/src/features/nodes/components/flow/nodes/Invocation/fields/inputs/VideoFieldInputComponent.tsx
@@ -0,0 +1,101 @@
+import { Flex, 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 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 { useGetVideoDTOQuery } from 'services/api/endpoints/videos';
+import { $isConnected } from 'services/events/stores';
+
+import type { FieldComponentProps } from './types';
+
+/**
+ * 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, isError } = useGetVideoDTOQuery(field.value?.video_name ?? skipToken);
+
+ 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 — mirrors the image-field behavior.
+ useEffect(() => {
+ if (isConnected && isError) {
+ handleReset();
+ }
+ }, [handleReset, isConnected, isError]);
+
+ return (
+
+ {!videoDTO && (
+
+
+ {t('gallery.drop')}
+
+
+ )}
+ {videoDTO && (
+ <>
+
+
+
+ {`${videoDTO.width}x${videoDTO.height}`}
+ >
+ )}
+
+
+ );
+};
+
+export default memo(VideoFieldInputComponent);
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 6713ee8fb42..950f8d444d7 100644
--- a/invokeai/frontend/web/src/features/nodes/store/nodesSlice.ts
+++ b/invokeai/frontend/web/src/features/nodes/store/nodesSlice.ts
@@ -49,6 +49,7 @@ import type {
StringFieldValue,
StringGeneratorFieldValue,
StylePresetFieldValue,
+ VideoFieldValue,
} from 'features/nodes/types/field';
import {
zBoardFieldValue,
@@ -71,6 +72,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';
@@ -518,6 +520,9 @@ const slice = createSlice({
fieldImageCollectionValueChanged: (state, action: FieldValueAction) => {
fieldValueReducer(state, action, zImageFieldCollectionValue);
},
+ fieldVideoValueChanged: (state, action: FieldValueAction) => {
+ fieldValueReducer(state, action, zVideoFieldValue);
+ },
fieldColorValueChanged: (state, action: FieldValueAction) => {
fieldValueReducer(state, action, zColorFieldValue);
},
@@ -666,6 +671,7 @@ export const {
fieldEnumModelValueChanged,
fieldImageValueChanged,
fieldImageCollectionValueChanged,
+ fieldVideoValueChanged,
fieldLabelChanged,
fieldModelIdentifierValueChanged,
fieldIntegerValueChanged,
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 ffd87ae3984..242dc7e27e5 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),
@@ -211,6 +216,7 @@ const zStatefulFieldType = z.union([
zBooleanFieldType,
zEnumFieldType,
zImageFieldType,
+ zVideoFieldType,
zBoardFieldType,
zStylePresetFieldType,
zModelIdentifierFieldType,
@@ -536,6 +542,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({
@@ -1285,6 +1311,7 @@ export const zStatefulFieldValue = z.union([
zEnumFieldValue,
zImageFieldValue,
zImageFieldCollectionValue,
+ zVideoFieldValue,
zBoardFieldValue,
zStylePresetFieldValue,
zModelIdentifierFieldValue,
@@ -1313,6 +1340,7 @@ const zStatefulFieldInputInstance = z.union([
zEnumFieldInputInstance,
zImageFieldInputInstance,
zImageFieldCollectionInputInstance,
+ zVideoFieldInputInstance,
zBoardFieldInputInstance,
zStylePresetFieldInputInstance,
zModelIdentifierFieldInputInstance,
@@ -1340,6 +1368,7 @@ const zStatefulFieldInputTemplate = z.union([
zEnumFieldInputTemplate,
zImageFieldInputTemplate,
zImageFieldCollectionInputTemplate,
+ zVideoFieldInputTemplate,
zBoardFieldInputTemplate,
zStylePresetFieldInputTemplate,
zModelIdentifierFieldInputTemplate,
@@ -1368,6 +1397,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 fa01db67e60..03aa0f78a60 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;
@@ -69,7 +70,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 0c57087eaad..79d38075e35 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
@@ -62,7 +62,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,
SchedulerField: 'dpmpp_3m_k',
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 adaa3f413ce..372b1f4472f 100644
--- a/invokeai/frontend/web/src/features/nodes/util/schema/buildFieldInputTemplate.ts
+++ b/invokeai/frontend/web/src/features/nodes/util/schema/buildFieldInputTemplate.ts
@@ -24,6 +24,7 @@ import type {
StringFieldInputTemplate,
StringGeneratorFieldInputTemplate,
StylePresetFieldInputTemplate,
+ VideoFieldInputTemplate,
} from 'features/nodes/types/field';
import {
getFloatGeneratorArithmeticSequenceDefaults,
@@ -318,6 +319,20 @@ const buildImageFieldInputTemplate: FieldInputTemplateBuilder = ({
+ schemaObject,
+ baseField,
+ fieldType,
+}) => {
+ const template: VideoFieldInputTemplate = {
+ ...baseField,
+ type: fieldType,
+ default: schemaObject.default ?? undefined,
+ };
+
+ return template;
+};
+
const buildImageFieldCollectionInputTemplate: FieldInputTemplateBuilder = ({
schemaObject,
baseField,
@@ -471,6 +486,7 @@ const TEMPLATE_BUILDER_MAP: Record {
+ 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..5d3f7b3e34a
--- /dev/null
+++ b/invokeai/frontend/web/src/features/parameters/components/Core/ParamWanGuidanceScaleLowNoise.tsx
@@ -0,0 +1,94 @@
+import { CompositeNumberInput, CompositeSlider, FormControl, FormLabel, IconButton } from '@invoke-ai/ui-library';
+import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
+import {
+ 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 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]
+ );
+
+ const displayValue = value ?? CONSTRAINTS.initial;
+
+ 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 68e1e9a382e..a4f74e22860 100644
--- a/invokeai/frontend/web/src/features/queue/hooks/useEnqueueCanvas.ts
+++ b/invokeai/frontend/web/src/features/queue/hooks/useEnqueueCanvas.ts
@@ -17,6 +17,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';
@@ -69,6 +70,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 54b37e1b95e..17d062b86c0 100644
--- a/invokeai/frontend/web/src/features/queue/hooks/useEnqueueGenerate.ts
+++ b/invokeai/frontend/web/src/features/queue/hooks/useEnqueueGenerate.ts
@@ -15,6 +15,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';
@@ -62,6 +63,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 230fa3348d6..84b7906018b 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;
@@ -774,6 +787,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/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..a47035e2075 100644
--- a/invokeai/frontend/web/src/services/api/endpoints/images.ts
+++ b/invokeai/frontend/web/src/services/api/endpoints/images.ts
@@ -296,7 +296,9 @@ export const imagesApi = api.injectEndpoints({
query: ({ board_id }) => ({ url: buildBoardsUrl(board_id), method: 'DELETE' }),
invalidatesTags: () => [
{ 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 +313,10 @@ export const imagesApi = api.injectEndpoints({
categories: ASSETS_CATEGORIES,
}),
},
+ { type: 'VideoList', id: LIST_TAG },
+ 'VideoNameList',
+ 'GalleryItemList',
+ 'GalleryItemNameList',
],
}),
@@ -323,7 +329,15 @@ 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: () => [
+ { type: 'Board', id: LIST_TAG },
+ { type: 'VideoList', id: LIST_TAG },
+ 'VideoNameList',
+ 'GalleryItemList',
+ 'GalleryItemNameList',
+ ],
}),
addImageToBoard: build.mutation<
paths['/api/v1/board_images/']['post']['responses']['201']['content']['application/json'],
@@ -484,7 +498,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..6872d928552
--- /dev/null
+++ b/invokeai/frontend/web/src/services/api/endpoints/videos.ts
@@ -0,0 +1,339 @@
+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) },
+ { 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 }],
+ }),
+
+ 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 [];
+ }
+ return [
+ ...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 [];
+ }
+ return [
+ ...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']),
+ ];
+ },
+ }),
+
+ 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 [];
+ }
+ // ``starred_first=true`` gallery queries are cached under the LIST_TAG-scoped
+ // ``VideoList`` tag, so without this invalidation the freshly-starred video
+ // stays in its original position until some other mutation refetches the list.
+ 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,
+} = 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,
+ 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();
+};
+
+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/hooks/modelsByType.ts b/invokeai/frontend/web/src/services/api/hooks/modelsByType.ts
index bd1ac088138..73e65b984f7 100644
--- a/invokeai/frontend/web/src/services/api/hooks/modelsByType.ts
+++ b/invokeai/frontend/web/src/services/api/hooks/modelsByType.ts
@@ -37,6 +37,10 @@ import {
isTextLLMModelConfig,
isTIModelConfig,
isVAEModelConfigOrSubmodel,
+ isWanDiffusersMainModelConfig,
+ isWanGGUFLowNoiseMainModelConfig,
+ isWanT5EncoderModelConfig,
+ isWanVAEModelConfig,
isZImageDiffusersMainModelConfig,
} from 'services/api/types';
@@ -111,6 +115,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)
);
@@ -154,5 +162,8 @@ export const selectFlux2DiffusersModels = buildModelsSelector(isFlux2DiffusersMa
export const selectFluxVAEModels = buildModelsSelector(isFluxVAEModelConfig);
export const selectAnimaVAEModels = buildModelsSelector(isAnimaVAEModelConfig);
export const selectT5EncoderModels = buildModelsSelector(isT5EncoderModelConfigOrSubmodel);
+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 a586273f3a7..8391e5d4c10 100644
--- a/invokeai/frontend/web/src/services/api/index.ts
+++ b/invokeai/frontend/web/src/services/api/index.ts
@@ -62,6 +62,15 @@ const tagTypes = [
'UserList',
'CustomNodePacks',
'VirtualBoards',
+ // Video tags (parallel to Image tags).
+ 'Video',
+ 'VideoList',
+ 'VideoMetadata',
+ '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';
diff --git a/invokeai/frontend/web/src/services/api/schema.ts b/invokeai/frontend/web/src/services/api/schema.ts
index e9de97db96d..a2f83773d68 100644
--- a/invokeai/frontend/web/src/services/api/schema.ts
+++ b/invokeai/frontend/web/src/services/api/schema.ts
@@ -1361,6 +1361,280 @@ 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/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}/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.
+ *
+ * Like the image equivalent, this endpoint is intentionally unauthenticated because browsers
+ * load videos via tags which cannot send Bearer tokens. Video names are UUIDs,
+ * providing security through unguessability.
+ */
+ 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.
+ *
+ * Like the image equivalent, this endpoint is intentionally unauthenticated because browsers
+ * load videos via tags which cannot send Bearer tokens. Video names are UUIDs,
+ * providing security through unguessability.
+ */
+ 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 a video. Unauthenticated; UUIDs provide unguessability.
+ */
+ 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;
@@ -2844,6 +3118,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.
@@ -3563,7 +3850,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_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_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_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_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
@@ -3715,7 +4002,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: {
/**
@@ -4174,6 +4461,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: {
/**
@@ -4297,6 +4592,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: {
/**
@@ -4314,6 +4617,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: {
/**
@@ -4322,6 +4633,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: {
/**
@@ -4374,6 +4693,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
@@ -7558,7 +7890,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
@@ -8190,6 +8522,16 @@ 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[];
};
/**
* DeleteByDestinationResult
@@ -8244,6 +8586,19 @@ 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[];
+ };
/**
* Denoise - SD1.5, SDXL
* @description Denoises noisy latents to decodable images
@@ -9399,6 +9754,131 @@ 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
+ * defaults to 16 fps, matching the other Wan video nodes.
+ *
+ * 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.
+ * @default 16
+ */
+ 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.
@@ -12064,6 +12544,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.
@@ -12278,7 +12865,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"]["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"]["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"]["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"]["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"]["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"]["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"]["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"]["WanTextEncoderInvocation"] | components["schemas"]["WanVideoDenoiseInvocation"] | 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
@@ -12315,7 +12902,7 @@ export type components = {
* @description The results of node executions
*/
results: {
- [key: string]: components["schemas"]["AnimaConditioningOutput"] | 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"]["ZImageConditioningOutput"] | components["schemas"]["ZImageControlOutput"] | components["schemas"]["ZImageLoRALoaderOutput"] | components["schemas"]["ZImageModelLoaderOutput"];
+ [key: string]: components["schemas"]["AnimaConditioningOutput"] | 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"]["ZImageConditioningOutput"] | components["schemas"]["ZImageControlOutput"] | components["schemas"]["ZImageLoRALoaderOutput"] | components["schemas"]["ZImageModelLoaderOutput"];
};
/**
* Errors
@@ -15676,7 +16263,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"]["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"]["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"]["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"]["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"]["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"]["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"]["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"]["WanTextEncoderInvocation"] | components["schemas"]["WanVideoDenoiseInvocation"] | 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
@@ -15686,7 +16273,7 @@ export type components = {
* Result
* @description The result of the invocation
*/
- result: components["schemas"]["AnimaConditioningOutput"] | 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"]["ZImageConditioningOutput"] | components["schemas"]["ZImageControlOutput"] | components["schemas"]["ZImageLoRALoaderOutput"] | components["schemas"]["ZImageModelLoaderOutput"];
+ result: components["schemas"]["AnimaConditioningOutput"] | 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"]["ZImageConditioningOutput"] | components["schemas"]["ZImageControlOutput"] | components["schemas"]["ZImageLoRALoaderOutput"] | components["schemas"]["ZImageModelLoaderOutput"];
};
/**
* InvocationErrorEvent
@@ -15740,7 +16327,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"]["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"]["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"]["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"]["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"]["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"]["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"]["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"]["WanTextEncoderInvocation"] | components["schemas"]["WanVideoDenoiseInvocation"] | 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
@@ -15816,6 +16403,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"];
@@ -16007,6 +16595,20 @@ 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_video_denoise: components["schemas"]["LatentsOutput"];
z_image_control: components["schemas"]["ZImageControlOutput"];
z_image_denoise: components["schemas"]["LatentsOutput"];
z_image_denoise_meta: components["schemas"]["LatentsMetaOutput"];
@@ -16070,7 +16672,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"]["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"]["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"]["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"]["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"]["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"]["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"]["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"]["WanTextEncoderInvocation"] | components["schemas"]["WanVideoDenoiseInvocation"] | 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
@@ -16145,7 +16747,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"]["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"]["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"]["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"]["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"]["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"]["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"]["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"]["WanTextEncoderInvocation"] | components["schemas"]["WanVideoDenoiseInvocation"] | 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
@@ -19046,6 +19648,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.
@@ -21123,10 +21823,14 @@ export type components = {
base: "sdxl";
};
/**
- * Main_Diffusers_ZImage_Config
- * @description Model config for Z-Image diffusers models (Z-Image-Turbo, Z-Image-Base).
+ * 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_ZImage_Config: {
+ Main_Diffusers_Wan_Config: {
/**
* Key
* @description A unique key for this model.
@@ -21202,106 +21906,28 @@ export type components = {
repo_variant: components["schemas"]["ModelRepoVariant"];
/**
* Base
- * @default z-image
- * @constant
- */
- base: "z-image";
- variant: components["schemas"]["ZImageVariantType"];
- };
- /**
- * Main_GGUF_FLUX_Config
- * @description Model config for main checkpoint models.
- */
- Main_GGUF_FLUX_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
+ * @default wan
* @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;
+ base: "wan";
+ variant: components["schemas"]["WanVariantType"];
/**
- * Config Path
- * @description Path to the config for this model, if any.
- */
- config_path: string | null;
- /**
- * Base
- * @default flux
- * @constant
+ * Has Dual Expert
+ * @description Whether this model ships two transformer experts (Wan 2.2 A14B MoE). False for TI2V-5B.
+ * @default false
*/
- base: "flux";
+ has_dual_expert: boolean;
/**
- * Format
- * @default gguf_quantized
- * @constant
+ * 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.
*/
- format: "gguf_quantized";
- variant: components["schemas"]["FluxVariantType"];
+ boundary_ratio: number | null;
};
/**
- * Main_GGUF_Flux2_Config
- * @description Model config for GGUF-quantized FLUX.2 checkpoint models (e.g. Klein).
+ * Main_Diffusers_ZImage_Config
+ * @description Model config for Z-Image diffusers models (Z-Image-Turbo, Z-Image-Base).
*/
- Main_GGUF_Flux2_Config: {
+ Main_Diffusers_ZImage_Config: {
/**
* Key
* @description A unique key for this model.
@@ -21368,29 +21994,204 @@ export type components = {
/** @description Default settings for this model */
default_settings: components["schemas"]["MainModelDefaultSettings"] | null;
/**
- * Config Path
- * @description Path to the config for this model, if any.
- */
- config_path: string | null;
- /**
- * Base
- * @default flux2
+ * Format
+ * @default diffusers
* @constant
*/
- base: "flux2";
+ format: "diffusers";
+ /** @default */
+ repo_variant: components["schemas"]["ModelRepoVariant"];
/**
- * Format
- * @default gguf_quantized
+ * Base
+ * @default z-image
* @constant
*/
- format: "gguf_quantized";
- variant: components["schemas"]["Flux2VariantType"];
+ base: "z-image";
+ variant: components["schemas"]["ZImageVariantType"];
};
/**
- * Main_GGUF_QwenImage_Config
- * @description Model config for GGUF-quantized Qwen Image transformer models.
+ * Main_GGUF_FLUX_Config
+ * @description Model config for main checkpoint models.
*/
- Main_GGUF_QwenImage_Config: {
+ Main_GGUF_FLUX_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;
+ /**
+ * Config Path
+ * @description Path to the config for this model, if any.
+ */
+ config_path: string | null;
+ /**
+ * Base
+ * @default flux
+ * @constant
+ */
+ base: "flux";
+ /**
+ * Format
+ * @default gguf_quantized
+ * @constant
+ */
+ format: "gguf_quantized";
+ variant: components["schemas"]["FluxVariantType"];
+ };
+ /**
+ * Main_GGUF_Flux2_Config
+ * @description Model config for GGUF-quantized FLUX.2 checkpoint models (e.g. Klein).
+ */
+ Main_GGUF_Flux2_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;
+ /**
+ * Config Path
+ * @description Path to the config for this model, if any.
+ */
+ config_path: string | null;
+ /**
+ * Base
+ * @default flux2
+ * @constant
+ */
+ base: "flux2";
+ /**
+ * Format
+ * @default gguf_quantized
+ * @constant
+ */
+ format: "gguf_quantized";
+ variant: components["schemas"]["Flux2VariantType"];
+ };
+ /**
+ * Main_GGUF_QwenImage_Config
+ * @description Model config for GGUF-quantized Qwen Image transformer models.
+ */
+ Main_GGUF_QwenImage_Config: {
/**
* Key
* @description A unique key for this model.
@@ -21475,6 +22276,106 @@ export type components = {
format: "gguf_quantized";
variant: components["schemas"]["QwenImageVariantType"] | null;
};
+ /**
+ * 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_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;
+ /**
+ * Config Path
+ * @description Path to the config for this model, if any.
+ */
+ config_path: string | null;
+ /**
+ * Base
+ * @default wan
+ * @constant
+ */
+ base: "wan";
+ /**
+ * Format
+ * @default gguf_quantized
+ * @constant
+ */
+ format: "gguf_quantized";
+ 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";
+ };
/**
* Main_GGUF_ZImage_Config
* @description Model config for GGUF-quantized Z-Image transformer models.
@@ -23255,7 +24156,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: {
/**
@@ -23392,7 +24293,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_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_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_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_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
@@ -23558,7 +24459,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_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_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_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_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
@@ -23644,7 +24545,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_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_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_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_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
@@ -23665,7 +24566,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_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_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_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_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
@@ -23791,7 +24692,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;
/**
@@ -23849,7 +24750,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.
@@ -23862,7 +24763,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_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_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_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_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
@@ -24116,6 +25017,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_: {
/**
@@ -24139,6 +25063,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
@@ -26518,6 +27465,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.
@@ -28514,6 +29474,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 */
@@ -28526,7 +29499,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
@@ -28571,7 +29544,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
@@ -29084,14 +30057,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
@@ -30448,7 +31421,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.
@@ -30753,6 +31726,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.
@@ -31458,6 +32444,96 @@ 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";
+ /**
+ * 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).
@@ -31686,6 +32762,91 @@ export type components = {
*/
base: "sdxl";
};
+ /**
+ * VAE_Diffusers_Wan_Config
+ * @description Model config for Wan 2.2 VAE in diffusers folder layout (AutoencoderKLWan).
+ */
+ VAE_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;
+ /**
+ * Format
+ * @default diffusers
+ * @constant
+ */
+ format: "diffusers";
+ /** @default */
+ repo_variant: components["schemas"]["ModelRepoVariant"];
+ /**
+ * Type
+ * @default vae
+ * @constant
+ */
+ type: "vae";
+ /**
+ * Base
+ * @default wan
+ * @constant
+ */
+ base: "wan";
+ /**
+ * Latent Channels
+ * @description VAE latent channel count: 16 for A14B or 48 for TI2V-5B's Wan2.2-VAE.
+ * @default 16
+ * @enum {integer}
+ */
+ latent_channels: 16 | 48;
+ };
/** ValidationError */
ValidationError: {
/** Location */
@@ -31695,6 +32856,401 @@ export type components = {
/** Error Type */
type: string;
};
+ /** VideoBoardArg */
+ VideoBoardArg: {
+ /**
+ * Board Id
+ * @description The id of the board to add or remove the video from
+ */
+ board_id: string;
+ /**
+ * Video Name
+ * @description The name of the video to add to / remove from the board
+ */
+ 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 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;
+ /**
+ * Videos
+ * @description Videos to concatenate, in order. At least two are required.
+ * @default null
+ */
+ videos?: components["schemas"]["VideoField"][] | null;
+ /**
+ * Transition
+ * @description Transition between consecutive clips.
+ * @default cut
+ * @enum {string}
+ */
+ transition?: "cut" | "crossfade" | "fade_through_black";
+ /**
+ * Transition Frames
+ * @description Length of each transition in frames. Ignored when transition is 'cut'.
+ * @default 8
+ */
+ transition_frames?: number;
+ /**
+ * Fps
+ * @description Output frame rate. Defaults to the first input's fps.
+ * @default null
+ */
+ fps?: number | null;
+ /**
+ * type
+ * @default video_concat
+ * @constant
+ */
+ type: "video_concat";
+ };
+ /**
+ * VideoDTO
+ * @description Deserialized video record, enriched for the frontend.
+ */
+ VideoDTO: {
+ /**
+ * Video Name
+ * @description The unique name of the video.
+ */
+ video_name: string;
+ /**
+ * Video Url
+ * @description The URL of the video file (MP4).
+ */
+ video_url: string;
+ /**
+ * Thumbnail Url
+ * @description The URL of the video's first-frame thumbnail (WebP).
+ */
+ 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"];
+ /**
+ * Width
+ * @description The pixel width of the video.
+ */
+ width: number;
+ /**
+ * Height
+ * @description The pixel height of the video.
+ */
+ height: number;
+ /**
+ * Duration
+ * @description The duration of the video in seconds.
+ */
+ duration: number;
+ /**
+ * Fps
+ * @description The frames-per-second of the video, if known.
+ */
+ fps?: number | null;
+ /**
+ * Created At
+ * @description The created timestamp of the video.
+ */
+ created_at: string;
+ /**
+ * Updated At
+ * @description The updated timestamp of the video.
+ */
+ updated_at: string;
+ /**
+ * Deleted At
+ * @description The deleted timestamp of the video.
+ */
+ deleted_at?: string | null;
+ /**
+ * Is Intermediate
+ * @description Whether this is an intermediate video.
+ */
+ is_intermediate: boolean;
+ /**
+ * Session Id
+ * @description The session ID that produced this video, if any.
+ */
+ session_id?: string | null;
+ /**
+ * Node Id
+ * @description The node ID that produced this video, if any.
+ */
+ node_id?: string | null;
+ /**
+ * Starred
+ * @description Whether this video is starred.
+ */
+ starred: boolean;
+ /**
+ * Has Workflow
+ * @description Whether this video has a workflow associated.
+ */
+ has_workflow: boolean;
+ /**
+ * Video Subfolder
+ * @description The subfolder where the video is stored on disk.
+ * @default
+ */
+ video_subfolder?: string;
+ /**
+ * Board Id
+ * @description The id of the board the video belongs to, if one exists.
+ */
+ board_id?: string | null;
+ };
+ /**
+ * VideoField
+ * @description A video primitive field
+ */
+ VideoField: {
+ /**
+ * Video Name
+ * @description The name of the video
+ */
+ video_name: string;
+ };
+ /**
+ * 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.
+ */
+ VideoFrameExtractInvocation: {
+ /**
+ * @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 from.
+ * @default null
+ */
+ 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";
+ };
+ /**
+ * 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).
+ */
+ VideoInvocation: {
+ /**
+ * 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 load
+ * @default null
+ */
+ video?: components["schemas"]["VideoField"] | null;
+ /**
+ * type
+ * @default video
+ * @constant
+ */
+ type: "video";
+ };
+ /**
+ * VideoNamesResult
+ * @description Response containing ordered video names with metadata for optimistic updates.
+ */
+ VideoNamesResult: {
+ /**
+ * Video Names
+ * @description Ordered list of video names
+ */
+ video_names: string[];
+ /**
+ * Starred Count
+ * @description Number of starred videos (when starred_first=True)
+ */
+ starred_count: number;
+ /**
+ * Total Count
+ * @description Total number of videos matching the query
+ */
+ 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"];
+ /**
+ * 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 video
+ */
+ num_frames: number;
+ /**
+ * Fps
+ * @description The frames-per-second of the video
+ */
+ fps: number;
+ /**
+ * Duration
+ * @description The duration of the video in seconds
+ */
+ duration: number;
+ /**
+ * type
+ * @default video_output
+ * @constant
+ */
+ type: "video_output";
+ };
+ /**
+ * VideoRecordChanges
+ * @description Allowed mutations on a video record.
+ */
+ VideoRecordChanges: {
+ /** @description The video's new category. */
+ video_category?: components["schemas"]["ImageCategory"] | null;
+ /**
+ * Session Id
+ * @description The video's new session ID.
+ */
+ session_id?: string | null;
+ /**
+ * Is Intermediate
+ * @description The video's new `is_intermediate` flag.
+ */
+ is_intermediate?: boolean | null;
+ /**
+ * Starred
+ * @description The video's new `starred` state.
+ */
+ starred?: boolean | null;
+ } & {
+ [key: string]: unknown;
+ };
+ /**
+ * VideoUrlsDTO
+ * @description The URLs for a video and its thumbnail.
+ */
+ VideoUrlsDTO: {
+ /**
+ * Video Name
+ * @description The unique name of the video.
+ */
+ video_name: string;
+ /**
+ * Video Url
+ * @description The URL of the video file (MP4).
+ */
+ video_url: string;
+ /**
+ * Thumbnail Url
+ * @description The URL of the video's first-frame thumbnail (WebP).
+ */
+ thumbnail_url: string;
+ };
/**
* VirtualSubBoardDTO
* @description A virtual sub-board computed from image metadata, not stored in the database.
@@ -31731,389 +33287,384 @@ export type components = {
*/
cover_image_name?: string | null;
};
- /** Workflow */
- Workflow: {
- /**
- * Name
- * @description The name of the workflow.
- */
- name: 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.
+ */
+ WanConditioningField: {
/**
- * Author
- * @description The author of the workflow.
+ * Conditioning Name
+ * @description The name of conditioning tensor
*/
- author: string;
+ 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"];
/**
- * Description
- * @description The description of the workflow.
+ * type
+ * @default wan_conditioning_output
+ * @constant
*/
- description: string;
+ type: "wan_conditioning_output";
+ };
+ /**
+ * Denoise - Wan 2.2
+ * @description Run the denoising process with a Wan 2.2 model.
+ *
+ * Drives a flow-matching Euler schedule via Diffusers'
+ * ``FlowMatchEulerDiscreteScheduler``. 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.
+ */
+ WanDenoiseInvocation: {
/**
- * Version
- * @description The version of the workflow.
+ * Id
+ * @description The id of this instance of an invocation. Must be unique among all instances of invocations.
*/
- version: string;
+ id: string;
/**
- * Contact
- * @description The contact of the workflow.
+ * Is Intermediate
+ * @description Whether or not this is an intermediate invocation.
+ * @default false
*/
- contact: string;
+ is_intermediate?: boolean;
/**
- * Tags
- * @description The tags of the workflow.
+ * Use Cache
+ * @description Whether or not to use the cache
+ * @default true
*/
- tags: string;
+ use_cache?: boolean;
/**
- * Notes
- * @description The notes of the workflow.
+ * Transformer
+ * @description Wan transformer field (transformer + optional dual-expert metadata).
+ * @default null
*/
- notes: string;
+ transformer?: components["schemas"]["WanTransformerField"] | null;
/**
- * Exposedfields
- * @description The exposed fields of the workflow.
+ * @description Positive conditioning tensor
+ * @default null
*/
- exposedFields: components["schemas"]["ExposedField"][];
- /** @description The meta of the workflow. */
- meta: components["schemas"]["WorkflowMeta"];
+ positive_conditioning?: components["schemas"]["WanConditioningField"] | null;
/**
- * Nodes
- * @description The nodes of the workflow.
+ * @description Negative conditioning tensor
+ * @default null
*/
- nodes: {
- [key: string]: components["schemas"]["JsonValue"];
- }[];
+ negative_conditioning?: components["schemas"]["WanConditioningField"] | null;
/**
- * Edges
- * @description The edges of the workflow.
+ * Reference Image
+ * @description Reference-image (VAE-latent) conditioning for Wan 2.2 I2V.
+ * @default null
*/
- edges: {
- [key: string]: components["schemas"]["JsonValue"];
- }[];
+ ref_image?: components["schemas"]["WanRefImageConditioningField"] | null;
/**
- * Form
- * @description The form of the workflow.
+ * @description Latents tensor
+ * @default null
*/
- form?: {
- [key: string]: components["schemas"]["JsonValue"];
- } | null;
+ latents?: components["schemas"]["LatentsField"] | null;
/**
- * Id
- * @description The id of the workflow.
+ * @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
*/
- id: string;
- };
- /** WorkflowAndGraphResponse */
- WorkflowAndGraphResponse: {
+ denoise_mask?: components["schemas"]["DenoiseMaskField"] | null;
/**
- * Workflow
- * @description The workflow used to generate the image, as stringified JSON
+ * Denoising Start
+ * @description When to start denoising, expressed a percentage of total steps
+ * @default 0
*/
- workflow: string | null;
+ denoising_start?: number;
/**
- * Graph
- * @description The graph used to generate the image, as stringified JSON
+ * Denoising End
+ * @description When to stop denoising, expressed a percentage of total steps
+ * @default 1
*/
- graph: string | null;
- };
- /**
- * WorkflowCategory
- * @enum {string}
- */
- WorkflowCategory: "user" | "default";
- /** WorkflowMeta */
- WorkflowMeta: {
+ denoising_end?: number;
/**
- * Version
- * @description The version of the workflow schema.
+ * Add Noise
+ * @description Add noise based on denoising start.
+ * @default true
*/
- version: string;
- /** @description The category of the workflow (user or default). */
- category: components["schemas"]["WorkflowCategory"];
- };
- /** WorkflowRecordDTO */
- WorkflowRecordDTO: {
+ add_noise?: boolean;
/**
- * Workflow Id
- * @description The id of the workflow.
+ * 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
*/
- workflow_id: string;
+ guidance_scale?: number;
/**
- * Name
- * @description The name of the workflow.
+ * 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
*/
- name: string;
+ guidance_scale_low_noise?: number | null;
/**
- * Created At
- * @description The created timestamp of the workflow.
+ * Width
+ * @description Width of the generated image.
+ * @default 1024
*/
- created_at: string;
+ width?: number;
/**
- * Updated At
- * @description The updated timestamp of the workflow.
+ * Height
+ * @description Height of the generated image.
+ * @default 1024
*/
- updated_at: string;
+ height?: number;
/**
- * Opened At
- * @description The opened timestamp of the workflow.
+ * Steps
+ * @description Number of denoising steps.
+ * @default 40
*/
- opened_at?: string | null;
+ steps?: number;
/**
- * User Id
- * @description The id of the user who owns this workflow.
+ * Seed
+ * @description Randomness seed for reproducibility.
+ * @default 0
*/
- user_id: string;
+ seed?: number;
/**
- * Is Public
- * @description Whether this workflow is shared with all users.
+ * type
+ * @default wan_denoise
+ * @constant
*/
- is_public: boolean;
- /** @description The workflow. */
- workflow: components["schemas"]["Workflow"];
+ type: "wan_denoise";
};
- /** WorkflowRecordListItemWithThumbnailDTO */
- WorkflowRecordListItemWithThumbnailDTO: {
- /**
- * Workflow Id
- * @description The id of the workflow.
- */
- workflow_id: string;
- /**
- * Name
- * @description The name of the workflow.
- */
- name: string;
+ /**
+ * Wan 2.2 I2V Ideal Dimensions
+ * @description Compute Wan I2V-compatible (width, height) for a chosen resolution preset.
+ *
+ * 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 (Wan's pixel-grid
+ * constraint). Wire from ``Image Primitive``'s width/height outputs and into
+ * ``wan_ref_image_encoder`` / ``wan_denoise``.
+ */
+ WanI2VIdealDimensionsInvocation: {
/**
- * Created At
- * @description The created timestamp of the workflow.
+ * Id
+ * @description The id of this instance of an invocation. Must be unique among all instances of invocations.
*/
- created_at: string;
+ id: string;
/**
- * Updated At
- * @description The updated timestamp of the workflow.
+ * Is Intermediate
+ * @description Whether or not this is an intermediate invocation.
+ * @default false
*/
- updated_at: string;
+ is_intermediate?: boolean;
/**
- * Opened At
- * @description The opened timestamp of the workflow.
+ * Use Cache
+ * @description Whether or not to use the cache
+ * @default true
*/
- opened_at?: string | null;
+ use_cache?: boolean;
/**
- * User Id
- * @description The id of the user who owns this workflow.
+ * Width
+ * @description Source image width in pixels.
+ * @default 1024
*/
- user_id: string;
+ width?: number;
/**
- * Is Public
- * @description Whether this workflow is shared with all users.
+ * Height
+ * @description Source image height in pixels.
+ * @default 1024
*/
- is_public: boolean;
+ height?: number;
/**
- * Description
- * @description The description of the workflow.
+ * 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}
*/
- description: string;
- /** @description The description of the workflow. */
- category: components["schemas"]["WorkflowCategory"];
+ target_resolution?: "480p" | "720p" | "1080p";
/**
- * Tags
- * @description The tags of the workflow.
+ * 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}
*/
- tags: string;
+ rounding?: "nearest" | "floor" | "ceiling";
/**
- * Thumbnail Url
- * @description The URL of the workflow thumbnail.
+ * type
+ * @default wan_i2v_ideal_dimensions
+ * @constant
*/
- thumbnail_url?: string | null;
+ type: "wan_i2v_ideal_dimensions";
};
/**
- * WorkflowRecordOrderBy
- * @description The order by options for workflow records
- * @enum {string}
+ * 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.
*/
- WorkflowRecordOrderBy: "created_at" | "updated_at" | "opened_at" | "name" | "is_public";
- /** WorkflowRecordWithThumbnailDTO */
- WorkflowRecordWithThumbnailDTO: {
+ WanImageToLatentsInvocation: {
/**
- * Workflow Id
- * @description The id of the workflow.
+ * @description The board to save the image to
+ * @default null
*/
- workflow_id: string;
+ board?: components["schemas"]["BoardField"] | null;
/**
- * Name
- * @description The name of the workflow.
+ * @description Optional metadata to be saved with the image
+ * @default null
*/
- name: string;
+ metadata?: components["schemas"]["MetadataField"] | null;
/**
- * Created At
- * @description The created timestamp of the workflow.
+ * Id
+ * @description The id of this instance of an invocation. Must be unique among all instances of invocations.
*/
- created_at: string;
+ id: string;
/**
- * Updated At
- * @description The updated timestamp of the workflow.
+ * Is Intermediate
+ * @description Whether or not this is an intermediate invocation.
+ * @default false
*/
- updated_at: string;
+ is_intermediate?: boolean;
/**
- * Opened At
- * @description The opened timestamp of the workflow.
+ * Use Cache
+ * @description Whether or not to use the cache
+ * @default true
*/
- opened_at?: string | null;
+ use_cache?: boolean;
/**
- * User Id
- * @description The id of the user who owns this workflow.
+ * @description The image to encode.
+ * @default null
*/
- user_id: string;
+ image?: components["schemas"]["ImageField"] | null;
/**
- * Is Public
- * @description Whether this workflow is shared with all users.
+ * @description VAE
+ * @default null
*/
- is_public: boolean;
- /** @description The workflow. */
- workflow: components["schemas"]["Workflow"];
+ vae?: components["schemas"]["VAEField"] | null;
/**
- * Thumbnail Url
- * @description The URL of the workflow thumbnail.
+ * type
+ * @default wan_i2l
+ * @constant
*/
- thumbnail_url?: string | null;
+ type: "wan_i2l";
};
- /** 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;
+ /**
+ * Latents to Image - Wan 2.2
+ * @description Decodes Wan latents back to RGB.
+ */
+ WanLatentsToImageInvocation: {
/**
- * Version
- * @description The version of the workflow.
+ * @description The board to save the image to
+ * @default null
*/
- version: string;
+ board?: components["schemas"]["BoardField"] | null;
/**
- * Contact
- * @description The contact of the workflow.
+ * @description Optional metadata to be saved with the image
+ * @default null
*/
- contact: string;
+ metadata?: components["schemas"]["MetadataField"] | null;
/**
- * Tags
- * @description The tags of the workflow.
+ * Id
+ * @description The id of this instance of an invocation. Must be unique among all instances of invocations.
*/
- tags: string;
+ id: string;
/**
- * Notes
- * @description The notes of the workflow.
+ * Is Intermediate
+ * @description Whether or not this is an intermediate invocation.
+ * @default false
*/
- notes: string;
+ is_intermediate?: boolean;
/**
- * Exposedfields
- * @description The exposed fields of the workflow.
+ * Use Cache
+ * @description Whether or not to use the cache
+ * @default true
*/
- exposedFields: components["schemas"]["ExposedField"][];
- /** @description The meta of the workflow. */
- meta: components["schemas"]["WorkflowMeta"];
+ use_cache?: boolean;
/**
- * Nodes
- * @description The nodes of the workflow.
+ * @description Latents tensor
+ * @default null
*/
- nodes: {
- [key: string]: components["schemas"]["JsonValue"];
- }[];
+ latents?: components["schemas"]["LatentsField"] | null;
/**
- * Edges
- * @description The edges of the workflow.
+ * @description VAE
+ * @default null
*/
- edges: {
- [key: string]: components["schemas"]["JsonValue"];
- }[];
+ vae?: components["schemas"]["VAEField"] | null;
/**
- * Form
- * @description The form of the workflow.
+ * type
+ * @default wan_l2i
+ * @constant
*/
- form?: {
- [key: string]: components["schemas"]["JsonValue"];
- } | null;
+ type: "wan_l2i";
};
/**
- * ZImageConditioningField
- * @description A Z-Image conditioning tensor primitive value
+ * Latents to Video - Wan 2.2
+ * @description Decode 5D Wan latents to RGB frames and encode an MP4.
*/
- ZImageConditioningField: {
+ WanLatentsToVideoInvocation: {
/**
- * Conditioning Name
- * @description The name of conditioning tensor
+ * @description The board to save the image to
+ * @default null
*/
- conditioning_name: string;
+ board?: components["schemas"]["BoardField"] | null;
/**
- * @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.
+ * @description Optional metadata to be saved with the image
* @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"];
+ metadata?: components["schemas"]["MetadataField"] | null;
/**
- * type
- * @default z_image_conditioning_output
- * @constant
+ * Id
+ * @description The id of this instance of an invocation. Must be unique among all instances of invocations.
*/
- type: "z_image_conditioning_output";
- };
- /**
- * ZImageControlField
- * @description A Z-Image control conditioning field for spatial control (Canny, HED, Depth, Pose, MLSD).
- */
- ZImageControlField: {
+ id: string;
/**
- * Image Name
- * @description The name of the preprocessed control image
+ * Is Intermediate
+ * @description Whether or not this is an intermediate invocation.
+ * @default false
*/
- image_name: string;
- /** @description The Z-Image ControlNet adapter model */
- control_model: components["schemas"]["ModelIdentifierField"];
+ is_intermediate?: boolean;
/**
- * Control Context Scale
- * @description The strength of the control signal. Recommended range: 0.65-0.80.
- * @default 0.75
+ * Use Cache
+ * @description Whether or not to use the cache
+ * @default true
*/
- control_context_scale?: number;
+ use_cache?: boolean;
/**
- * Begin Step Percent
- * @description When the control is first applied (% of total steps)
- * @default 0
+ * @description Latents tensor
+ * @default null
*/
- begin_step_percent?: number;
+ latents?: components["schemas"]["LatentsField"] | null;
/**
- * End Step Percent
- * @description When the control is last applied (% of total steps)
- * @default 1
+ * @description VAE
+ * @default null
*/
- end_step_percent?: number;
+ vae?: components["schemas"]["VAEField"] | null;
+ /**
+ * Fps
+ * @description Frames-per-second for the encoded MP4. Wan 2.2 was trained at 16 FPS.
+ * @default 16
+ */
+ fps?: number;
+ /**
+ * type
+ * @default wan_l2v
+ * @constant
+ */
+ type: "wan_l2v";
};
/**
- * Z-Image ControlNet
- * @description Configure Z-Image ControlNet for spatial conditioning.
+ * Apply LoRA Collection - Wan 2.2
+ * @description Apply a collection of LoRAs 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.
- *
- * Supports 5 control modes: Canny, HED, Depth, Pose, MLSD.
- * Recommended control_context_scale: 0.65-0.80.
+ * 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.
*/
- ZImageControlInvocation: {
+ WanLoRACollectionLoader: {
/**
* Id
* @description The id of this instance of an invocation. Must be unique among all instances of invocations.
@@ -32132,62 +33683,37 @@ export type components = {
*/
use_cache?: boolean;
/**
- * @description The preprocessed control image (Canny, HED, Depth, Pose, or MLSD)
+ * LoRAs
+ * @description LoRAs to apply. May be a single LoRA or a collection.
* @default null
*/
- image?: components["schemas"]["ImageField"] | null;
+ loras?: components["schemas"]["LoRAField"] | components["schemas"]["LoRAField"][] | null;
/**
- * Control Model
- * @description ControlNet model to load
+ * Wan Transformer
+ * @description Transformer
* @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"];
+ transformer?: components["schemas"]["WanTransformerField"] | null;
/**
* type
- * @default z_image_control_output
+ * @default wan_lora_collection_loader
* @constant
*/
- type: "z_image_control_output";
+ type: "wan_lora_collection_loader";
};
/**
- * Denoise - Z-Image
- * @description Run the denoising process with a Z-Image model.
+ * Apply LoRA - Wan 2.2
+ * @description Apply a LoRA to the Wan 2.2 transformer(s).
*
- * Supports regional prompting by connecting multiple conditioning inputs with masks.
+ * 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; the low-noise routing is harmless but ignored.
*/
- ZImageDenoiseInvocation: {
+ WanLoRALoaderInvocation: {
/**
* Id
* @description The id of this instance of an invocation. Must be unique among all instances of invocations.
@@ -32206,126 +33732,87 @@ export type components = {
*/
use_cache?: boolean;
/**
- * @description Latents tensor
+ * LoRA
+ * @description LoRA model to load
* @default null
*/
- latents?: components["schemas"]["LatentsField"] | null;
+ lora?: components["schemas"]["ModelIdentifierField"] | null;
/**
- * @description Noise tensor
- * @default null
+ * Weight
+ * @description The weight at which the LoRA is applied to each model
+ * @default 0.75
*/
- noise?: components["schemas"]["LatentsField"] | null;
+ weight?: number;
/**
- * @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.
+ * 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}
+ */
+ target?: "auto" | "both" | "high" | "low";
+ /**
+ * Wan Transformer
+ * @description Transformer
* @default null
*/
- denoise_mask?: components["schemas"]["DenoiseMaskField"] | null;
+ transformer?: components["schemas"]["WanTransformerField"] | 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
+ * type
+ * @default wan_lora_loader
+ * @constant
*/
- vae?: components["schemas"]["VAEField"] | null;
+ type: "wan_lora_loader";
+ };
+ /**
+ * WanLoRALoaderOutput
+ * @description Wan 2.2 LoRA loader output.
+ */
+ WanLoRALoaderOutput: {
/**
- * 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.
+ * Wan Transformer
+ * @description Transformer
* @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";
+ transformer: components["schemas"]["WanTransformerField"] | null;
/**
* type
- * @default z_image_denoise
+ * @default wan_lora_loader_output
* @constant
*/
- type: "z_image_denoise";
+ type: "wan_lora_loader_output";
};
/**
- * Denoise - Z-Image + Metadata
- * @description Run denoising process with a Z-Image transformer model + metadata.
+ * WanLoRAVariantType
+ * @description 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.
+ * @enum {string}
*/
- ZImageDenoiseMetaInvocation: {
- /**
- * @description Optional metadata to be saved with the image
- * @default null
- */
- metadata?: components["schemas"]["MetadataField"] | null;
+ 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.
@@ -32344,131 +33831,120 @@ export type components = {
*/
use_cache?: boolean;
/**
- * @description Latents tensor
- * @default null
- */
- latents?: components["schemas"]["LatentsField"] | null;
- /**
- * @description Noise tensor
- * @default null
+ * Transformer
+ * @description Wan 2.2 model (Transformer) to load
*/
- noise?: components["schemas"]["LatentsField"] | null;
+ model: components["schemas"]["ModelIdentifierField"];
/**
- * @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.
+ * 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
*/
- 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_low_noise_model?: components["schemas"]["ModelIdentifierField"] | null;
/**
- * 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_meta
- * @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_meta";
+ num_frames?: number;
};
/**
- * Image to Latents - Z-Image
- * @description Generates latents from an image using Z-Image VAE (supports both Diffusers and FLUX VAE).
+ * Reference Image - Wan 2.2
+ * @description VAE-encode a reference image into Wan 2.2 I2V conditioning.
+ *
+ * Output is a ``[1, 20, T_lat, height // 8, width // 8]`` condition tensor
+ * that the denoise loop concatenates to the 16-channel noise latents each
+ * step, producing the 36-channel input the I2V-A14B transformer expects.
+ *
+ * For image (single-frame) I2V leave ``num_frames=1`` (T_lat=1). For video
+ * I2V set ``num_frames`` to match the value on the video-denoise node
+ * (e.g. 81 for the Wan 2.2 reference defaults).
+ *
+ * Only works with I2V-A14B (the denoise loop's variant gate enforces this).
+ * For T2V or TI2V-5B, omit this node entirely.
*/
- 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;
+ WanRefImageEncoderInvocation: {
/**
* Id
* @description The id of this instance of an invocation. Must be unique among all instances of invocations.
@@ -32487,123 +33963,164 @@ export type components = {
*/
use_cache?: boolean;
/**
- * @description The image to encode.
+ * @description Reference image to condition on.
* @default null
*/
image?: components["schemas"]["ImageField"] | null;
/**
+ * VAE
* @description VAE
* @default null
*/
vae?: components["schemas"]["VAEField"] | null;
+ /**
+ * Width
+ * @description Width to resize the reference image to (must match denoise width).
+ * @default 1024
+ */
+ width?: number;
+ /**
+ * Height
+ * @description Height to resize the reference image to (must match denoise height).
+ * @default 1024
+ */
+ height?: number;
+ /**
+ * 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
+ */
+ num_frames?: number;
/**
* type
- * @default z_image_i2l
+ * @default wan_ref_image_encoder
* @constant
*/
- type: "z_image_i2l";
+ type: "wan_ref_image_encoder";
};
/**
- * Latents to Image - Z-Image
- * @description Generates an image from latents using Z-Image VAE (supports both Diffusers and FLUX VAE).
+ * WanRefImageOutput
+ * @description Output of a Wan 2.2 reference-image VAE-encoder.
*/
- ZImageLatentsToImageInvocation: {
+ WanRefImageOutput: {
/**
- * @description The board to save the image to
- * @default null
+ * Reference Image
+ * @description VAE-latent reference-image conditioning for Wan 2.2 I2V.
*/
- board?: components["schemas"]["BoardField"] | null;
+ ref_image: components["schemas"]["WanRefImageConditioningField"];
/**
- * @description Optional metadata to be saved with the image
- * @default null
+ * type
+ * @default wan_ref_image_output
+ * @constant
*/
- metadata?: components["schemas"]["MetadataField"] | null;
+ type: "wan_ref_image_output";
+ };
+ /**
+ * WanT5EncoderField
+ * @description Field for the UMT5-XXL text encoder used by Wan 2.2 models.
+ */
+ WanT5EncoderField: {
+ /** @description Info to load tokenizer submodel */
+ tokenizer: components["schemas"]["ModelIdentifierField"];
+ /** @description Info to load text_encoder submodel */
+ text_encoder: components["schemas"]["ModelIdentifierField"];
/**
- * Id
- * @description The id of this instance of an invocation. Must be unique among all instances of invocations.
+ * Loras
+ * @description LoRAs to apply on model loading
*/
- id: string;
+ 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: {
/**
- * Is Intermediate
- * @description Whether or not this is an intermediate invocation.
- * @default false
+ * Key
+ * @description A unique key for this model.
*/
- is_intermediate?: boolean;
+ key: string;
/**
- * Use Cache
- * @description Whether or not to use the cache
- * @default true
+ * Hash
+ * @description The hash of the model file(s).
*/
- use_cache?: boolean;
+ hash: string;
/**
- * @description Latents tensor
- * @default null
+ * Path
+ * @description Path to the model on the filesystem. Relative paths are relative to the Invoke root directory.
*/
- latents?: components["schemas"]["LatentsField"] | null;
+ path: string;
/**
- * @description VAE
- * @default null
+ * File Size
+ * @description The size of the model in bytes.
*/
- vae?: components["schemas"]["VAEField"] | null;
+ file_size: number;
/**
- * type
- * @default z_image_l2i
- * @constant
+ * Name
+ * @description Name of the model.
*/
- type: "z_image_l2i";
- };
- /**
- * Apply LoRA Collection - Z-Image
- * @description Applies a collection of LoRAs to a Z-Image transformer.
- */
- ZImageLoRACollectionLoader: {
+ name: string;
/**
- * Id
- * @description The id of this instance of an invocation. Must be unique among all instances of invocations.
+ * Description
+ * @description Model description
*/
- id: string;
+ description: string | null;
/**
- * Is Intermediate
- * @description Whether or not this is an intermediate invocation.
- * @default false
+ * Source
+ * @description The original source of the model (path, URL or repo_id).
*/
- is_intermediate?: boolean;
+ source: string;
+ /** @description The type of source */
+ source_type: components["schemas"]["ModelSourceType"];
/**
- * Use Cache
- * @description Whether or not to use the cache
- * @default true
+ * Source Api Response
+ * @description The original API response from the source, as stringified JSON.
*/
- use_cache?: boolean;
+ source_api_response: string | null;
/**
- * LoRAs
- * @description LoRA models and weights. May be a single LoRA or collection.
- * @default null
+ * Source Url
+ * @description Optional URL for the model (e.g. download page or model page).
*/
- loras?: components["schemas"]["LoRAField"] | components["schemas"]["LoRAField"][] | null;
+ source_url: string | null;
/**
- * Transformer
- * @description Transformer
- * @default null
+ * Cover Image
+ * @description Url for image to preview model
*/
- transformer?: components["schemas"]["TransformerField"] | null;
+ cover_image: string | null;
/**
- * Qwen3 Encoder
- * @description Qwen3 tokenizer and text encoder
- * @default null
+ * Base
+ * @default any
+ * @constant
*/
- qwen3_encoder?: components["schemas"]["Qwen3EncoderField"] | null;
+ base: "any";
/**
- * type
- * @default z_image_lora_collection_loader
+ * Type
+ * @default wan_t5_encoder
* @constant
*/
- type: "z_image_lora_collection_loader";
+ type: "wan_t5_encoder";
+ /**
+ * Format
+ * @default wan_t5_encoder
+ * @constant
+ */
+ format: "wan_t5_encoder";
};
/**
- * 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=226, 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.
@@ -32622,70 +34139,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 Optional separate LoRAs for the low-noise expert (Wan 2.2 A14B). If empty and transformer_low_noise is set, the primary 'loras' list is reused.
*/
- 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.
@@ -32705,129 +34256,458 @@ 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;
/**
- * Randomize Percent
- * @description Percentage of embedding values to add noise to (1-100). Lower values create more selective noise patterns.
- * @default 50
+ * Version
+ * @description The version of the workflow.
*/
- randomize_percent?: number;
+ 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;
+ };
+ /** 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;
+ };
+ /**
+ * WorkflowCategory
+ * @enum {string}
+ */
+ WorkflowCategory: "user" | "default";
+ /** 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;
+ };
+ /**
+ * 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;
+ };
+ /** 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_seed_variance_enhancer
+ * @default z_image_conditioning_output
* @constant
*/
- type: "z_image_seed_variance_enhancer";
+ type: "z_image_conditioning_output";
};
/**
- * Prompt - Z-Image
- * @description Encodes and preps a prompt for a Z-Image image.
+ * 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.
*
- * Supports regional prompting by connecting a mask input.
+ * 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.
*/
- ZImageTextEncoderInvocation: {
+ ZImageControlInvocation: {
/**
* Id
* @description The id of this instance of an invocation. Must be unique among all instances of invocations.
@@ -32846,85 +34726,1481 @@ export type components = {
*/
use_cache?: boolean;
/**
- * Prompt
- * @description Text prompt to encode.
+ * @description The preprocessed control image (Canny, HED, Depth, Pose, or MLSD)
* @default null
*/
- prompt?: string | null;
+ image?: components["schemas"]["ImageField"] | null;
/**
- * Qwen3 Encoder
- * @description Qwen3 tokenizer and text encoder
+ * Control Model
+ * @description ControlNet model to load
* @default null
*/
- qwen3_encoder?: components["schemas"]["Qwen3EncoderField"] | null;
+ control_model?: components["schemas"]["ModelIdentifierField"] | null;
/**
- * @description A mask defining the region that this conditioning prompt applies to.
- * @default null
+ * Control Scale
+ * @description Strength of the control signal. Recommended range: 0.65-0.80.
+ * @default 0.75
*/
- mask?: components["schemas"]["TensorField"] | null;
+ 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_text_encoder
+ * @default z_image_control
* @constant
*/
- type: "z_image_text_encoder";
+ type: "z_image_control";
};
/**
- * ZImageVariantType
- * @description Z-Image model variants.
- * @enum {string}
+ * ZImageControlOutput
+ * @description Z-Image Control output containing control configuration.
*/
- 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"];
- };
+ ZImageControlOutput: {
+ /** @description Z-Image control conditioning */
+ control: components["schemas"]["ZImageControlField"];
+ /**
+ * type
+ * @default z_image_control_output
+ * @constant
+ */
+ type: "z_image_control_output";
};
- responses: {
- /** @description Successful Response */
- 200: {
- headers: {
- [name: string]: unknown;
- };
- content: {
- "application/json": components["schemas"]["LoginResponse"];
- };
+ /**
+ * 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.
+ * @default 50
+ */
+ randomize_percent?: number;
+ /**
+ * type
+ * @default z_image_seed_variance_enhancer
+ * @constant
+ */
+ 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";
+ };
+ /**
+ * 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"];
+ };
+ };
+ };
+ };
+ 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"];
+ };
+ };
+ };
+ };
+ 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: {
+ content: {
+ "application/json": components["schemas"]["Body_parse_dynamicprompts"];
+ };
+ };
+ responses: {
+ /** @description Successful Response */
+ 200: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/json": components["schemas"]["DynamicPromptsResponse"];
+ };
+ };
+ /** @description Validation Error */
+ 422: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/json": components["schemas"]["HTTPValidationError"];
+ };
+ };
+ };
+ };
+ expand_prompt: {
+ parameters: {
+ query?: never;
+ header?: never;
+ path?: never;
+ cookie?: never;
+ };
+ requestBody: {
+ content: {
+ "application/json": components["schemas"]["ExpandPromptRequest"];
+ };
+ };
+ responses: {
+ /** @description Successful Response */
+ 200: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/json": components["schemas"]["ExpandPromptResponse"];
+ };
+ };
+ /** @description Validation Error */
+ 422: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/json": components["schemas"]["HTTPValidationError"];
+ };
+ };
+ };
+ };
+ 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 */
+ 200: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/json": components["schemas"]["ModelsList"];
+ };
+ };
+ /** @description Validation Error */
+ 422: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "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: {
+ 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: {
+ 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_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_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_records_by_hash: {
+ parameters: {
+ 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_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_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_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_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"]["ModelRecordChanges"];
+ };
+ };
+ responses: {
+ /** @description The model was updated 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_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_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: {
@@ -32937,67 +36213,58 @@ export interface operations {
};
};
};
- logout_api_v1_auth_logout_post: {
+ reidentify_model: {
parameters: {
query?: never;
header?: never;
- path?: never;
+ path: {
+ /** @description Key of the model to reidentify. */
+ key: string;
+ };
cookie?: never;
};
requestBody?: never;
responses: {
- /** @description Successful Response */
+ /** @description The model configuration was retrieved successfully */
200: {
headers: {
[name: string]: unknown;
};
content: {
- "application/json": components["schemas"]["LogoutResponse"];
+ /** @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_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_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"];
};
};
- };
- };
- 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: {
+ /** @description Bad request */
+ 400: {
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"];
+ content?: never;
};
- };
- responses: {
- /** @description Successful Response */
- 200: {
+ /** @description The model could not be found */
+ 404: {
headers: {
[name: string]: unknown;
};
- content: {
- "application/json": components["schemas"]["UserDTO"];
- };
+ content?: never;
};
/** @description Validation Error */
422: {
@@ -33010,100 +36277,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: {
@@ -33116,26 +36355,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: {
@@ -33148,25 +36401,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: {
@@ -33178,30 +36438,37 @@ 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: {
@@ -33214,7 +36481,7 @@ export interface operations {
};
};
};
- parse_dynamicprompts: {
+ bulk_delete_models: {
parameters: {
query?: never;
header?: never;
@@ -33223,17 +36490,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 */
@@ -33247,7 +36514,7 @@ export interface operations {
};
};
};
- expand_prompt: {
+ bulk_reidentify_models: {
parameters: {
query?: never;
header?: never;
@@ -33256,17 +36523,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 */
@@ -33280,18 +36547,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: {
@@ -33299,50 +36562,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: {
@@ -33353,38 +36619,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;
@@ -33397,25 +36643,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_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_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;
@@ -33423,14 +36674,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_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_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: {
@@ -33443,53 +36708,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_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_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;
@@ -33507,27 +36747,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;
};
@@ -33544,65 +36786,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_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_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;
};
@@ -33619,54 +36825,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_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_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;
};
@@ -33679,33 +36860,33 @@ export interface operations {
};
content: {
"application/json": components["schemas"]["HTTPValidationError"];
- };
- };
- };
- };
- scan_for_models: {
- parameters: {
- query?: {
- /** @description Directory path to search for models */
- scan_path?: string;
+ };
};
+ };
+ };
+ restart_failed_model_install_job: {
+ parameters: {
+ 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;
};
@@ -33722,29 +36903,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;
};
@@ -33761,25 +36946,43 @@ 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_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_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 */
@@ -33789,13 +36992,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: {
@@ -33807,60 +37017,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;
@@ -33869,25 +37075,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;
@@ -33896,17 +37106,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 */
@@ -33920,40 +37130,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;
@@ -33968,53 +37165,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: {
@@ -34025,16 +37201,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;
@@ -34052,7 +37241,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;
@@ -34068,40 +37257,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: {
@@ -34114,12 +37290,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;
@@ -34132,10 +37308,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;
@@ -34153,20 +37329,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;
};
@@ -34174,8 +37350,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;
};
@@ -34192,67 +37375,67 @@ 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: {
+ 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?: never;
+ requestBody: {
+ content: {
+ "multipart/form-data": components["schemas"]["Body_upload_image"];
+ };
+ };
responses: {
- /** @description The job was resumed 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;
@@ -34270,33 +37453,42 @@ export interface operations {
};
};
};
- restart_failed_model_install_job: {
+ list_image_dtos: {
parameters: {
- query?: never;
- header?: never;
- path: {
- /** @description Model install job ID */
- id: number;
+ 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 Failed files restarted successfully */
- 201: {
+ /** @description Successful Response */
+ 200: {
headers: {
[name: string]: unknown;
};
content: {
- "application/json": components["schemas"]["ModelInstallJob"];
- };
- };
- /** @description No such job */
- 415: {
- headers: {
- [name: string]: unknown;
+ "application/json": components["schemas"]["OffsetPaginatedResults_ImageDTO_"];
};
- content?: never;
};
/** @description Validation Error */
422: {
@@ -34309,37 +37501,27 @@ export interface operations {
};
};
};
- restart_model_install_file: {
+ create_image_upload_entry: {
parameters: {
query?: never;
header?: never;
- path: {
- /** @description Model install job ID */
- id: number;
- };
+ path?: never;
cookie?: never;
};
requestBody: {
content: {
- "application/json": string;
+ "application/json": components["schemas"]["Body_create_image_upload_entry"];
};
};
responses: {
- /** @description File restarted successfully */
- 201: {
+ /** @description Successful Response */
+ 200: {
headers: {
[name: string]: unknown;
};
content: {
- "application/json": components["schemas"]["ModelInstallJob"];
- };
- };
- /** @description No such job */
- 415: {
- headers: {
- [name: string]: unknown;
+ "application/json": components["schemas"]["ImageUploadEntry"];
};
- content?: never;
};
/** @description Validation Error */
422: {
@@ -34352,65 +37534,58 @@ export interface operations {
};
};
};
- convert_model: {
+ get_image_dto: {
parameters: {
query?: never;
header?: never;
path: {
- /** @description Unique key of the safetensors main model to convert to diffusers format. */
- key: string;
+ /** @description The name of image to get */
+ image_name: string;
};
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_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_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": components["schemas"]["ImageDTO"];
};
};
- /** @description Bad request */
- 400: {
+ /** @description Validation Error */
+ 422: {
headers: {
[name: string]: unknown;
};
- content?: never;
- };
- /** @description Model not found */
- 404: {
- headers: {
- [name: string]: unknown;
+ content: {
+ "application/json": components["schemas"]["HTTPValidationError"];
};
- content?: never;
};
- /** @description There is already a model registered at this location */
- 409: {
+ };
+ };
+ delete_image: {
+ parameters: {
+ query?: never;
+ header?: never;
+ path: {
+ /** @description The name of the image to delete */
+ image_name: string;
+ };
+ cookie?: never;
+ };
+ requestBody?: never;
+ responses: {
+ /** @description Successful Response */
+ 200: {
headers: {
[name: string]: unknown;
};
- content?: never;
+ content: {
+ "application/json": components["schemas"]["DeleteImagesResult"];
+ };
};
/** @description Validation Error */
422: {
@@ -34423,14 +37598,21 @@ export interface operations {
};
};
};
- get_starter_models: {
+ update_image: {
parameters: {
query?: never;
header?: never;
- path?: never;
+ path: {
+ /** @description The name of the image to update */
+ image_name: string;
+ };
cookie?: never;
};
- requestBody?: never;
+ requestBody: {
+ content: {
+ "application/json": components["schemas"]["ImageRecordChanges"];
+ };
+ };
responses: {
/** @description Successful Response */
200: {
@@ -34438,12 +37620,21 @@ export interface operations {
[name: string]: unknown;
};
content: {
- "application/json": components["schemas"]["StarterModelResponse"];
+ "application/json": components["schemas"]["ImageDTO"];
+ };
+ };
+ /** @description Validation Error */
+ 422: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/json": components["schemas"]["HTTPValidationError"];
};
};
};
};
- get_stats: {
+ get_intermediates_count: {
parameters: {
query?: never;
header?: never;
@@ -34458,12 +37649,12 @@ export interface operations {
[name: string]: unknown;
};
content: {
- "application/json": components["schemas"]["CacheStats"] | null;
+ "application/json": number;
};
};
};
};
- empty_model_cache: {
+ clear_intermediates: {
parameters: {
query?: never;
header?: never;
@@ -34478,16 +37669,19 @@ export interface operations {
[name: string]: unknown;
};
content: {
- "application/json": unknown;
+ "application/json": number;
};
};
};
};
- get_hf_login_status: {
+ get_image_metadata: {
parameters: {
query?: never;
header?: never;
- path?: never;
+ path: {
+ /** @description The name of image to get */
+ image_name: string;
+ };
cookie?: never;
};
requestBody?: never;
@@ -34498,23 +37692,31 @@ export interface operations {
[name: string]: unknown;
};
content: {
- "application/json": components["schemas"]["HFTokenStatus"];
+ "application/json": components["schemas"]["MetadataField"] | null;
+ };
+ };
+ /** @description Validation Error */
+ 422: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/json": components["schemas"]["HTTPValidationError"];
};
};
};
};
- do_hf_login: {
+ get_image_workflow: {
parameters: {
query?: never;
header?: never;
- path?: never;
- cookie?: never;
- };
- requestBody: {
- content: {
- "application/json": components["schemas"]["Body_do_hf_login"];
+ path: {
+ /** @description The name of image whose workflow to get */
+ image_name: string;
};
+ cookie?: never;
};
+ requestBody?: never;
responses: {
/** @description Successful Response */
200: {
@@ -34522,7 +37724,7 @@ export interface operations {
[name: string]: unknown;
};
content: {
- "application/json": components["schemas"]["HFTokenStatus"];
+ "application/json": components["schemas"]["WorkflowAndGraphResponse"];
};
};
/** @description Validation Error */
@@ -34536,68 +37738,73 @@ export interface operations {
};
};
};
- reset_hf_token: {
+ get_image_full: {
parameters: {
query?: never;
header?: never;
- path?: never;
+ path: {
+ /** @description The name of full-resolution image file to get */
+ image_name: string;
+ };
cookie?: never;
};
requestBody?: never;
responses: {
- /** @description Successful Response */
+ /** @description Return the full-resolution image */
200: {
headers: {
[name: string]: unknown;
};
content: {
- "application/json": components["schemas"]["HFTokenStatus"];
+ "image/png": unknown;
};
};
- };
- };
- get_orphaned_models: {
- parameters: {
- query?: never;
- header?: never;
- path?: never;
- cookie?: never;
- };
- requestBody?: never;
- responses: {
- /** @description Successful Response */
- 200: {
+ /** @description Image not found */
+ 404: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content?: never;
+ };
+ /** @description Validation Error */
+ 422: {
headers: {
[name: string]: unknown;
};
content: {
- "application/json": components["schemas"]["OrphanedModelInfo"][];
+ "application/json": components["schemas"]["HTTPValidationError"];
};
};
};
};
- delete_orphaned_models: {
+ get_image_full_head: {
parameters: {
query?: never;
header?: never;
- path?: never;
- cookie?: never;
- };
- requestBody: {
- content: {
- "application/json": components["schemas"]["DeleteOrphanedModelsRequest"];
+ path: {
+ /** @description The name of full-resolution image file to get */
+ image_name: string;
};
+ cookie?: never;
};
+ requestBody?: never;
responses: {
- /** @description Successful Response */
+ /** @description Return the full-resolution image */
200: {
headers: {
[name: string]: unknown;
};
content: {
- "application/json": components["schemas"]["DeleteOrphanedModelsResponse"];
+ "image/png": unknown;
};
};
+ /** @description Image not found */
+ 404: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content?: never;
+ };
/** @description Validation Error */
422: {
headers: {
@@ -34609,31 +37816,53 @@ export interface operations {
};
};
};
- list_downloads: {
+ get_image_thumbnail: {
parameters: {
query?: never;
header?: never;
- path?: never;
+ path: {
+ /** @description The name of thumbnail image file to get */
+ image_name: string;
+ };
cookie?: never;
};
requestBody?: never;
responses: {
- /** @description Successful Response */
+ /** @description Return the image thumbnail */
200: {
headers: {
[name: string]: unknown;
};
content: {
- "application/json": components["schemas"]["DownloadJob"][];
+ "image/webp": unknown;
+ };
+ };
+ /** @description Image not found */
+ 404: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content?: never;
+ };
+ /** @description Validation Error */
+ 422: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/json": components["schemas"]["HTTPValidationError"];
};
};
};
};
- prune_downloads: {
+ get_image_urls: {
parameters: {
query?: never;
header?: never;
- path?: never;
+ path: {
+ /** @description The name of the image whose URL to get */
+ image_name: string;
+ };
cookie?: never;
};
requestBody?: never;
@@ -34644,26 +37873,21 @@ export interface operations {
[name: string]: unknown;
};
content: {
- "application/json": unknown;
+ "application/json": components["schemas"]["ImageUrlsDTO"];
};
};
- /** @description All completed jobs have been pruned */
- 204: {
+ /** @description Validation Error */
+ 422: {
headers: {
[name: string]: unknown;
};
- content?: never;
- };
- /** @description Bad request */
- 400: {
- headers: {
- [name: string]: unknown;
+ content: {
+ "application/json": components["schemas"]["HTTPValidationError"];
};
- content?: never;
};
};
};
- download: {
+ delete_images_from_list: {
parameters: {
query?: never;
header?: never;
@@ -34672,7 +37896,7 @@ export interface operations {
};
requestBody: {
content: {
- "application/json": components["schemas"]["Body_download"];
+ "application/json": components["schemas"]["Body_delete_images_from_list"];
};
};
responses: {
@@ -34682,7 +37906,7 @@ export interface operations {
[name: string]: unknown;
};
content: {
- "application/json": components["schemas"]["DownloadJob"];
+ "application/json": components["schemas"]["DeleteImagesResult"];
};
};
/** @description Validation Error */
@@ -34696,33 +37920,47 @@ export interface operations {
};
};
};
- get_download_job: {
+ delete_uncategorized_images: {
parameters: {
query?: never;
header?: never;
- path: {
- /** @description ID of the download job to fetch. */
- id: number;
- };
+ path?: never;
cookie?: never;
};
requestBody?: never;
responses: {
- /** @description Success */
+ /** @description Successful Response */
200: {
headers: {
[name: string]: unknown;
};
content: {
- "application/json": components["schemas"]["DownloadJob"];
+ "application/json": components["schemas"]["DeleteImagesResult"];
};
};
- /** @description The requested download JobID could not be found */
- 404: {
+ };
+ };
+ star_images_in_list: {
+ parameters: {
+ query?: never;
+ header?: never;
+ path?: never;
+ cookie?: never;
+ };
+ requestBody: {
+ content: {
+ "application/json": components["schemas"]["Body_star_images_in_list"];
+ };
+ };
+ responses: {
+ /** @description Successful Response */
+ 200: {
headers: {
[name: string]: unknown;
};
- content?: never;
+ content: {
+ "application/json": components["schemas"]["StarredImagesResult"];
+ };
};
/** @description Validation Error */
422: {
@@ -34735,17 +37973,18 @@ export interface operations {
};
};
};
- cancel_download_job: {
+ unstar_images_in_list: {
parameters: {
query?: never;
header?: never;
- path: {
- /** @description ID of the download job to cancel. */
- id: number;
- };
+ path?: never;
cookie?: never;
};
- requestBody?: never;
+ requestBody: {
+ content: {
+ "application/json": components["schemas"]["Body_unstar_images_in_list"];
+ };
+ };
responses: {
/** @description Successful Response */
200: {
@@ -34753,22 +37992,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"]["UnstarredImagesResult"];
};
- content?: never;
};
/** @description Validation Error */
422: {
@@ -34781,68 +38006,62 @@ export interface operations {
};
};
};
- cancel_all_download_jobs: {
+ download_images_from_list: {
parameters: {
query?: never;
header?: never;
path?: never;
cookie?: never;
};
- requestBody?: never;
+ requestBody?: {
+ content: {
+ "application/json": components["schemas"]["Body_download_images_from_list"];
+ };
+ };
responses: {
/** @description Successful Response */
- 200: {
+ 202: {
headers: {
[name: string]: unknown;
};
content: {
- "application/json": unknown;
+ "application/json": components["schemas"]["ImagesDownloaded"];
};
};
- /** @description Download jobs have been cancelled */
- 204: {
+ /** @description Validation Error */
+ 422: {
headers: {
[name: string]: unknown;
};
- content?: never;
+ content: {
+ "application/json": components["schemas"]["HTTPValidationError"];
+ };
};
};
};
- upload_image: {
+ get_bulk_download_item: {
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: {
- content: {
- "multipart/form-data": components["schemas"]["Body_upload_image"];
+ 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 The image was uploaded successfully */
- 201: {
+ /** @description Return the complete bulk download item */
+ 200: {
headers: {
[name: string]: unknown;
};
content: {
- "application/json": components["schemas"]["ImageDTO"];
+ "application/zip": unknown;
};
};
- /** @description Image upload failed */
- 415: {
+ /** @description Image not found */
+ 404: {
headers: {
[name: string]: unknown;
};
@@ -34859,7 +38078,7 @@ export interface operations {
};
};
};
- list_image_dtos: {
+ get_image_names: {
parameters: {
query?: {
/** @description The origin of images to list. */
@@ -34870,10 +38089,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 */
@@ -34893,7 +38108,7 @@ export interface operations {
[name: string]: unknown;
};
content: {
- "application/json": components["schemas"]["OffsetPaginatedResults_ImageDTO_"];
+ "application/json": components["schemas"]["ImageNamesResult"];
};
};
/** @description Validation Error */
@@ -34907,7 +38122,7 @@ export interface operations {
};
};
};
- create_image_upload_entry: {
+ get_images_by_names: {
parameters: {
query?: never;
header?: never;
@@ -34916,7 +38131,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: {
@@ -34926,7 +38141,7 @@ export interface operations {
[name: string]: unknown;
};
content: {
- "application/json": components["schemas"]["ImageUploadEntry"];
+ "application/json": components["schemas"]["ImageDTO"][];
};
};
/** @description Validation Error */
@@ -34940,26 +38155,43 @@ export interface operations {
};
};
};
- get_image_dto: {
+ upload_video: {
parameters: {
- query?: never;
- header?: never;
- path: {
- /** @description The name of image to get */
- image_name: string;
+ 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?: never;
+ requestBody: {
+ content: {
+ "multipart/form-data": components["schemas"]["Body_upload_video"];
+ };
+ };
responses: {
- /** @description Successful Response */
- 200: {
+ /** @description The video was uploaded successfully */
+ 201: {
headers: {
[name: string]: unknown;
};
content: {
- "application/json": components["schemas"]["ImageDTO"];
+ "application/json": components["schemas"]["VideoDTO"];
+ };
+ };
+ /** @description Video upload failed */
+ 415: {
+ headers: {
+ [name: string]: unknown;
};
+ content?: never;
};
/** @description Validation Error */
422: {
@@ -34972,13 +38204,13 @@ export interface operations {
};
};
};
- delete_image: {
+ get_video_dto: {
parameters: {
query?: never;
header?: never;
path: {
- /** @description The name of the image to delete */
- image_name: string;
+ /** @description The name of video to get */
+ video_name: string;
};
cookie?: never;
};
@@ -34990,7 +38222,7 @@ export interface operations {
[name: string]: unknown;
};
content: {
- "application/json": components["schemas"]["DeleteImagesResult"];
+ "application/json": components["schemas"]["VideoDTO"];
};
};
/** @description Validation Error */
@@ -35004,21 +38236,17 @@ export interface operations {
};
};
};
- update_image: {
+ delete_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 delete */
+ video_name: string;
};
cookie?: never;
};
- requestBody: {
- content: {
- "application/json": components["schemas"]["ImageRecordChanges"];
- };
- };
+ requestBody?: never;
responses: {
/** @description Successful Response */
200: {
@@ -35026,7 +38254,7 @@ export interface operations {
[name: string]: unknown;
};
content: {
- "application/json": components["schemas"]["ImageDTO"];
+ "application/json": components["schemas"]["DeleteVideosResult"];
};
};
/** @description Validation Error */
@@ -35040,14 +38268,21 @@ export interface operations {
};
};
};
- get_intermediates_count: {
+ update_video: {
parameters: {
query?: never;
header?: never;
- path?: never;
+ path: {
+ /** @description The name of the video to update */
+ video_name: string;
+ };
cookie?: never;
};
- requestBody?: never;
+ requestBody: {
+ content: {
+ "application/json": components["schemas"]["VideoRecordChanges"];
+ };
+ };
responses: {
/** @description Successful Response */
200: {
@@ -35055,42 +38290,32 @@ export interface operations {
[name: string]: unknown;
};
content: {
- "application/json": number;
+ "application/json": components["schemas"]["VideoDTO"];
};
};
- };
- };
- clear_intermediates: {
- 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": number;
+ "application/json": components["schemas"]["HTTPValidationError"];
};
};
};
};
- get_image_metadata: {
+ delete_videos_from_list: {
parameters: {
query?: never;
header?: never;
- path: {
- /** @description The name of image to get */
- image_name: string;
- };
+ path?: never;
cookie?: never;
};
- requestBody?: never;
+ requestBody: {
+ content: {
+ "application/json": components["schemas"]["Body_delete_videos_from_list"];
+ };
+ };
responses: {
/** @description Successful Response */
200: {
@@ -35098,7 +38323,7 @@ export interface operations {
[name: string]: unknown;
};
content: {
- "application/json": components["schemas"]["MetadataField"] | null;
+ "application/json": components["schemas"]["DeleteVideosResult"];
};
};
/** @description Validation Error */
@@ -35112,13 +38337,13 @@ export interface operations {
};
};
};
- get_image_workflow: {
+ get_video_metadata: {
parameters: {
query?: never;
header?: never;
path: {
- /** @description The name of image whose workflow to get */
- image_name: string;
+ /** @description The name of video to get */
+ video_name: string;
};
cookie?: never;
};
@@ -35130,7 +38355,7 @@ export interface operations {
[name: string]: unknown;
};
content: {
- "application/json": components["schemas"]["WorkflowAndGraphResponse"];
+ "application/json": components["schemas"]["MetadataField"] | null;
};
};
/** @description Validation Error */
@@ -35144,28 +38369,37 @@ 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?: 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;
@@ -35183,28 +38417,28 @@ 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?: 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;
@@ -35222,19 +38456,19 @@ 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?: never;
};
requestBody?: never;
responses: {
- /** @description Return the image thumbnail */
+ /** @description Return the video thumbnail */
200: {
headers: {
[name: string]: unknown;
@@ -35243,7 +38477,7 @@ export interface operations {
"image/webp": unknown;
};
};
- /** @description Image not found */
+ /** @description Video not found */
404: {
headers: {
[name: string]: unknown;
@@ -35261,13 +38495,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;
};
@@ -35279,7 +38513,7 @@ export interface operations {
[name: string]: unknown;
};
content: {
- "application/json": components["schemas"]["ImageUrlsDTO"];
+ "application/json": components["schemas"]["VideoUrlsDTO"];
};
};
/** @description Validation Error */
@@ -35293,18 +38527,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: {
@@ -35312,7 +38561,7 @@ export interface operations {
[name: string]: unknown;
};
content: {
- "application/json": components["schemas"]["DeleteImagesResult"];
+ "application/json": components["schemas"]["OffsetPaginatedResults_VideoDTO_"];
};
};
/** @description Validation Error */
@@ -35326,9 +38575,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;
@@ -35341,12 +38605,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;
@@ -35355,7 +38628,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: {
@@ -35365,7 +38638,7 @@ export interface operations {
[name: string]: unknown;
};
content: {
- "application/json": components["schemas"]["StarredImagesResult"];
+ "application/json": components["schemas"]["StarredVideosResult"];
};
};
/** @description Validation Error */
@@ -35379,7 +38652,7 @@ export interface operations {
};
};
};
- unstar_images_in_list: {
+ unstar_videos_in_list: {
parameters: {
query?: never;
header?: never;
@@ -35388,7 +38661,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: {
@@ -35398,7 +38671,7 @@ export interface operations {
[name: string]: unknown;
};
content: {
- "application/json": components["schemas"]["UnstarredImagesResult"];
+ "application/json": components["schemas"]["UnstarredVideosResult"];
};
};
/** @description Validation Error */
@@ -35412,26 +38685,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 */
@@ -35445,33 +38718,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: {
@@ -35484,20 +38751,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;
@@ -35514,7 +38785,7 @@ export interface operations {
[name: string]: unknown;
};
content: {
- "application/json": components["schemas"]["ImageNamesResult"];
+ "application/json": components["schemas"]["OffsetPaginatedResults_GalleryItem_"];
};
};
/** @description Validation Error */
@@ -35528,18 +38799,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: {
@@ -35547,7 +38829,7 @@ export interface operations {
[name: string]: unknown;
};
content: {
- "application/json": components["schemas"]["ImageDTO"][];
+ "application/json": components["schemas"]["GalleryItemNamesResult"];
};
};
/** @description Validation Error */
diff --git a/invokeai/frontend/web/src/services/api/types.ts b/invokeai/frontend/web/src/services/api/types.ts
index 27c6fcbf3c3..4d7559624f2 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;
export type IPAdapterModelConfig = Extract;
@@ -117,6 +118,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;
@@ -321,6 +323,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';
};
@@ -379,6 +388,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
@@ -486,6 +499,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';
};
@@ -612,3 +642,52 @@ 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'];
+/** @knipignore Used by listVideos RTK response typing; surfaced in Phase 4+. */
+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..bd040fe43b8 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,22 @@ 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 });
}
return tags;
diff --git a/invokeai/frontend/web/src/services/events/onInvocationComplete.test.ts b/invokeai/frontend/web/src/services/events/onInvocationComplete.test.ts
new file mode 100644
index 00000000000..e27b062d0a1
--- /dev/null
+++ b/invokeai/frontend/web/src/services/events/onInvocationComplete.test.ts
@@ -0,0 +1,223 @@
+/**
+ * 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';
+
+// 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', () => ({
+ imagesApi: {
+ util: {
+ 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: {
+ util: {
+ upsertQueryEntries: vi.fn(() => ({ type: 'mock/boardsApi/upsertQueryEntries' })),
+ updateQueryData: vi.fn(() => ({ type: 'mock/boardsApi/updateQueryData' })),
+ },
+ endpoints: {
+ getBoardImagesTotal: { select: vi.fn(() => () => ({ data: undefined })) },
+ },
+ },
+}));
+
+vi.mock('services/api/endpoints/queue', () => ({
+ queueApi: {
+ util: {
+ 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: 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/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: 'mock/canvasComplete' })),
+}));
+
+vi.mock('features/nodes/hooks/useNodeExecutionState', () => ({
+ $nodeExecutionStates: { get: vi.fn(() => ({})) },
+ upsertExecutionState: vi.fn(),
+}));
+
+vi.mock('services/events/nodeExecutionState', () => ({
+ getUpdatedNodeExecutionStateOnInvocationComplete: vi.fn(() => null),
+}));
+
+vi.mock('services/events/stores', () => ({
+ $lastProgressEvent: { set: vi.fn() },
+}));
+
+// Import AFTER the mocks above are declared (vi.mock is hoisted; explicit ordering here
+// is for the human reader).
+import { buildOnInvocationComplete } from './onInvocationComplete';
+
+// 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',
+ origin: 'workflows',
+ destination: 'gallery',
+ user_id: 'user-1',
+ session_id: 'session-1',
+ invocation_source_id: 'node-1',
+ 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'];
+
+describe('onInvocationComplete polymorphic gallery cache', () => {
+ beforeEach(() => {
+ vi.clearAllMocks();
+ });
+
+ 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(
+ // 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();
+ });
+});
diff --git a/invokeai/frontend/web/src/services/events/onInvocationComplete.tsx b/invokeai/frontend/web/src/services/events/onInvocationComplete.tsx
index ea6a237d4b2..f5e3d96d0ef 100644
--- a/invokeai/frontend/web/src/services/events/onInvocationComplete.tsx
+++ b/invokeai/frontend/web/src/services/events/onInvocationComplete.tsx
@@ -10,12 +10,14 @@ 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 { LIST_ALL_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 { getUpdatedNodeExecutionStateOnInvocationComplete } from 'services/events/nodeExecutionState';
@@ -160,6 +162,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) {
@@ -223,6 +236,87 @@ 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.
+ dispatch(galleryApi.util.invalidateTags(['GalleryItemNameList', 'GalleryItemList']));
+
+ 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
@@ -270,6 +364,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);
};
diff --git a/pyproject.toml b/pyproject.toml
index 155471d9067..49829ee9259 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -43,6 +43,7 @@ dependencies = [
"onnx==1.16.1",
"onnxruntime==1.19.2",
"opencv-contrib-python",
+ "imageio[ffmpeg]", # video encode (for Wan 2.2 T2V/I2V output)
"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/invocations/test_video_concat.py b/tests/app/invocations/test_video_concat.py
new file mode 100644
index 00000000000..4c4df48698e
--- /dev/null
+++ b/tests/app/invocations/test_video_concat.py
@@ -0,0 +1,100 @@
+"""Regression tests for VideoConcatInvocation._assemble.
+
+Covers JPPhoto's code-review finding (PR #9163): ``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.
+
+We exercise ``_assemble`` 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).
+"""
+
+import numpy as np
+import pytest
+
+from invokeai.app.invocations.fields import VideoField
+from invokeai.app.invocations.video_concat import VideoConcatInvocation
+
+
+def _invocation(transition: str, transition_frames: int) -> VideoConcatInvocation:
+ # ``videos`` requires min_length=2 to construct; values are unused by ``_assemble``.
+ 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 = v._assemble([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 = v._assemble([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 = v._assemble([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 = v._assemble([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 = v._assemble([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 = v._assemble([clip_a, clip_b])
+ assert len(out) == 10
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..ca4553f1579
--- /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 private ``_decoder_frame_count`` helper 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.invocations.video_frame_extract 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_wan_denoise.py b/tests/app/invocations/test_wan_denoise.py
new file mode 100644
index 00000000000..e4eba684c39
--- /dev/null
+++ b/tests/app/invocations/test_wan_denoise.py
@@ -0,0 +1,790 @@
+"""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 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.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_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 → almost all steps go to low-noise expert.
+
+ With FlowMatchEuler the first timestep is exactly 1000 so the high-noise
+ expert handles it (>= boundary), and every subsequent timestep is < 1000.
+ """
+ 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)
+
+ # First step is t==1000 → high. All later steps are < 1000 → low.
+ assert len(high.timesteps_seen) == 1
+ assert high.timesteps_seen[0] == 1000.0
+ assert len(low.timesteps_seen) == 3
+
+ 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.
+ from collections import Counter
+
+ 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
+
+
+@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 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_flow_match_euler(self) -> None:
+ from diffusers import FlowMatchEulerDiscreteScheduler
+
+ from invokeai.app.invocations.wan_denoise import _default_scheduler_for_variant
+
+ for v in (WanVariantType.T2V_A14B, WanVariantType.I2V_A14B):
+ assert isinstance(_default_scheduler_for_variant(v), FlowMatchEulerDiscreteScheduler)
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..bb50bf7fe35
--- /dev/null
+++ b/tests/app/invocations/test_wan_expert_swapper.py
@@ -0,0 +1,565 @@
+"""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()
+
+ 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()
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_lora_loader.py b/tests/app/invocations/test_wan_lora_loader.py
new file mode 100644
index 00000000000..ce250eff86d
--- /dev/null
+++ b/tests/app/invocations/test_wan_lora_loader.py
@@ -0,0 +1,225 @@
+"""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
+
+# --------------------------------------------------------------------------
+# _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_context(lora_expert: str | None) -> MagicMock:
+ """Mock context where context.models.get_config(lora).expert == lora_expert
+ and context.models.exists returns True for any lora key."""
+ ctx = MagicMock()
+ ctx.models.exists.return_value = True
+ config = MagicMock()
+ config.expert = lora_expert
+ ctx.models.get_config.return_value = 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:
+ config = MagicMock()
+ config.expert = expert_by_key[field.key]
+ return config
+
+ 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):
+ wrong_base_lora = LoRAField(
+ lora=ModelIdentifierField(key="not-wan", hash="h", name="n", base=BaseModelType.Flux, type=ModelType.LoRA),
+ weight=0.5,
+ )
+ inv = WanLoRACollectionLoader(id="inv-1", loras=[wrong_base_lora], transformer=_make_transformer_field())
+ ctx = MagicMock()
+ ctx.models.exists.return_value = True
+ with pytest.raises(ValueError, match="not Wan 2.2"):
+ 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(),
+ )
+ ctx = MagicMock()
+ ctx.models.exists.return_value = True
+ config = MagicMock()
+ config.expert = None
+ ctx.models.get_config.return_value = config
+
+ out = inv.invoke(ctx)
+ 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
diff --git a/tests/app/routers/test_boards_multiuser.py b/tests/app/routers/test_boards_multiuser.py
index ab64ac8a9b4..2c7f71303e9 100644
--- a/tests/app/routers/test_boards_multiuser.py
+++ b/tests/app/routers/test_boards_multiuser.py
@@ -75,6 +75,17 @@ 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_invoker.services.board_video_records = mock_board_video_records
+ mock_invoker.services.videos = MagicMock()
+ # 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_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 +686,141 @@ 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"]
+
+ # Pretend the board contains videos so the cascade has something to enumerate
+ mock_invoker.services.board_video_records.get_all_board_video_names_for_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
+ # Regression guard: video info must appear in the result, and delete_videos_on_board
+ # must have been invoked. Previously the route ignored videos entirely.
+ 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_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.
+ images_call = mock_invoker.services.board_images.get_all_board_image_names_for_board.call_args
+ videos_call = mock_invoker.services.board_video_records.get_all_board_video_names_for_board.call_args
+ assert images_call.kwargs.get("user_id") is not None
+ assert videos_call.kwargs.get("user_id") is not None
+
+ 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
+ # Sanity: the four cascade calls must all share the same user_id (the requester).
+ requester_id = images_call.kwargs["user_id"]
+ assert videos_call.kwargs["user_id"] == requester_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
+
+ images_call = mock_invoker.services.board_images.get_all_board_image_names_for_board.call_args
+ videos_call = mock_invoker.services.board_video_records.get_all_board_video_names_for_board.call_args
+ assert images_call.kwargs.get("user_id") is None
+ assert videos_call.kwargs.get("user_id") is None
+
+ 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_multiuser_authorization.py b/tests/app/routers/test_multiuser_authorization.py
index 3461f37e7e9..aa21e886f50 100644
--- a/tests/app/routers/test_multiuser_authorization.py
+++ b/tests/app/routers/test_multiuser_authorization.py
@@ -116,6 +116,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=None, # type: ignore
+ board_video_records=None, # type: ignore
+ gallery=None, # type: ignore
)
@@ -663,6 +668,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)
@@ -672,7 +682,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_videos_multiuser.py b/tests/app/routers/test_videos_multiuser.py
new file mode 100644
index 00000000000..2964e8e762d
--- /dev/null
+++ b/tests/app/routers/test_videos_multiuser.py
@@ -0,0 +1,308 @@
+"""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_app import app
+from invokeai.app.services.invoker import Invoker
+from invokeai.app.services.users.users_common import UserCreateRequest
+
+
+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()
+ mock_invoker.services.video_records = MagicMock()
+ 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.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)
+ 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_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.
+ assert set(body["deleted_videos"]) == {"mine_a.mp4", "mine_b.mp4"}
+ # 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/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}"
diff --git a/tests/app/routers/test_workflows_multiuser.py b/tests/app/routers/test_workflows_multiuser.py
index a1fa5ed5ada..f535f7d22ef 100644
--- a/tests/app/routers/test_workflows_multiuser.py
+++ b/tests/app/routers/test_workflows_multiuser.py
@@ -107,6 +107,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=None, # type: ignore
+ board_video_records=None, # type: ignore
+ 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..f2255babf89
--- /dev/null
+++ b/tests/app/services/gallery/test_gallery_default.py
@@ -0,0 +1,102 @@
+"""Regression tests for SqliteGalleryService multiuser isolation.
+
+Covers JPPhoto's code-review finding (PR #9163): 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.
+"""
+
+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"),
+ }
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..a4e454b8b52
--- /dev/null
+++ b/tests/app/services/video_records/test_video_records_sqlite.py
@@ -0,0 +1,91 @@
+"""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.config.config_default import InvokeAIAppConfig
+from invokeai.app.services.image_records.image_records_common import ImageCategory, ResourceOrigin
+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"}
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..46b8ce499fe
--- /dev/null
+++ b/tests/backend/model_manager/configs/test_wan_gguf_config.py
@@ -0,0 +1,260 @@
+"""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.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) -> MagicMock:
+ mod = MagicMock()
+ mod.path = path
+ mod.load_state_dict.return_value = sd
+ return mod
+
+
+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:
+ 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) / "wan-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..4a5732bc10a
--- /dev/null
+++ b/tests/backend/model_manager/configs/test_wan_t5_encoder_config.py
@@ -0,0 +1,96 @@
+"""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"))
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..09026df26c4
--- /dev/null
+++ b/tests/backend/model_manager/load/test_wan_vae_loader.py
@@ -0,0 +1,58 @@
+"""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.
+"""
+
+import accelerate
+from diffusers.models.autoencoders.autoencoder_kl_wan import AutoencoderKLWan
+
+from invokeai.backend.model_manager.load.model_loaders.vae import _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
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_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..5de7b36dfa8
--- /dev/null
+++ b/tests/backend/wan/test_wan_ref_image_extension.py
@@ -0,0 +1,161 @@
+"""Tests for the Wan 2.2 I2V reference-image VAE-latent encoder helper."""
+
+from unittest.mock import MagicMock
+
+import torch
+from PIL import Image
+
+from invokeai.backend.wan.extensions.wan_ref_image_extension import (
+ encode_reference_image_to_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 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 92f42375ed4..673e2be4e31 100644
--- a/tests/conftest.py
+++ b/tests/conftest.py
@@ -69,6 +69,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=None, # type: ignore
+ board_video_records=None, # type: ignore
+ gallery=None, # type: ignore
)
diff --git a/uv.lock b/uv.lock
index ef0ad022177..ed913997713 100644
--- a/uv.lock
+++ b/uv.lock
@@ -981,6 +981,39 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/76/c6/c88e154df9c4e1a2a66ccf0005a88dfb2650c1dffb6f5ce603dfbd452ce3/idna-3.10-py3-none-any.whl", hash = "sha256:946d195a0d259cbba61165e88e65941f16e9b36ea6ddb97f00452bae8b1287d3", size = 70442, upload-time = "2024-09-15T18:07:37.964Z" },
]
+[[package]]
+name = "imageio"
+version = "2.37.3"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "numpy" },
+ { name = "pillow" },
+]
+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" },
+ { name = "psutil" },
+]
+
+[[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 = "8.7.0"
@@ -1021,6 +1054,7 @@ dependencies = [
{ name = "fastapi-events" },
{ name = "gguf" },
{ name = "huggingface-hub" },
+ { name = "imageio", extra = ["ffmpeg"] },
{ name = "mediapipe" },
{ name = "numpy" },
{ name = "onnx" },
@@ -1134,6 +1168,7 @@ requires-dist = [
{ name = "httpx", marker = "extra == 'test'" },
{ name = "huggingface-hub" },
{ name = "humanize", marker = "extra == 'test'", specifier = "==4.12.1" },
+ { name = "imageio", extras = ["ffmpeg"] },
{ name = "jurigged", marker = "extra == 'dev'" },
{ name = "mediapipe", specifier = "==0.10.14" },
{ name = "mkdocs-git-revision-date-localized-plugin", marker = "extra == 'docs'" },
@@ -2899,8 +2934,8 @@ dependencies = [
{ name = "setuptools", marker = "(sys_platform == 'linux' and extra == 'extra-8-invokeai-rocm') or (extra == 'extra-8-invokeai-cuda' and extra == 'extra-8-invokeai-rocm') or (extra == 'extra-8-invokeai-cpu' and extra == 'extra-8-invokeai-cuda') or (extra == 'extra-8-invokeai-cpu' and extra == 'extra-8-invokeai-rocm')" },
]
wheels = [
- { url = "https://download.pytorch.org/whl/pytorch_triton_rocm-3.3.1-cp311-cp311-linux_x86_64.whl", hash = "sha256:8eb26aec84408b2be3d5b942a9edef9fadc6e249afe6aab795872e227ce8f579" },
- { url = "https://download.pytorch.org/whl/pytorch_triton_rocm-3.3.1-cp312-cp312-linux_x86_64.whl", hash = "sha256:977423eee5c542a3f8aa4f527aec1688c4d485f207089cb595a8e638fcc3888a" },
+ { url = "https://download-r2.pytorch.org/whl/pytorch_triton_rocm-3.3.1-cp311-cp311-linux_x86_64.whl", hash = "sha256:8eb26aec84408b2be3d5b942a9edef9fadc6e249afe6aab795872e227ce8f579", upload-time = "2025-06-03T22:26:32Z" },
+ { url = "https://download-r2.pytorch.org/whl/pytorch_triton_rocm-3.3.1-cp312-cp312-linux_x86_64.whl", hash = "sha256:977423eee5c542a3f8aa4f527aec1688c4d485f207089cb595a8e638fcc3888a", upload-time = "2025-06-03T22:26:30Z" },
]
[[package]]
@@ -3492,13 +3527,13 @@ dependencies = [
{ name = "typing-extensions", marker = "extra == 'extra-8-invokeai-cpu' or (extra == 'extra-8-invokeai-cuda' and extra == 'extra-8-invokeai-rocm')" },
]
wheels = [
- { url = "https://download.pytorch.org/whl/cpu/torch-2.7.1%2Bcpu-cp311-cp311-manylinux_2_28_aarch64.whl", hash = "sha256:5fe6045b8f426bf2d0426e4fe009f1667a954ec2aeb82f1bd0bf60c6d7a85445" },
- { url = "https://download.pytorch.org/whl/cpu/torch-2.7.1%2Bcpu-cp311-cp311-manylinux_2_28_x86_64.whl", hash = "sha256:a1684793e352f03fa14f78857e55d65de4ada8405ded1da2bf4f452179c4b779" },
- { url = "https://download.pytorch.org/whl/cpu/torch-2.7.1%2Bcpu-cp311-cp311-win_amd64.whl", hash = "sha256:7b977eccbc85ae2bd19d6998de7b1f1f4bd3c04eaffd3015deb7934389783399" },
- { url = "https://download.pytorch.org/whl/cpu/torch-2.7.1%2Bcpu-cp312-cp312-manylinux_2_28_aarch64.whl", hash = "sha256:3bf2db5adf77b433844f080887ade049c4705ddf9fe1a32023ff84ff735aa5ad" },
- { url = "https://download.pytorch.org/whl/cpu/torch-2.7.1%2Bcpu-cp312-cp312-manylinux_2_28_x86_64.whl", hash = "sha256:8f8b3cfc53010a4b4a3c7ecb88c212e9decc4f5eeb6af75c3c803937d2d60947" },
- { url = "https://download.pytorch.org/whl/cpu/torch-2.7.1%2Bcpu-cp312-cp312-win_amd64.whl", hash = "sha256:0bc887068772233f532b51a3e8c8cfc682ae62bef74bf4e0c53526c8b9e4138f" },
- { url = "https://download.pytorch.org/whl/cpu/torch-2.7.1%2Bcpu-cp312-cp312-win_arm64.whl", hash = "sha256:a2618775f32eb4126c5b2050686da52001a08cffa331637d9cf51c8250931e00" },
+ { url = "https://download-r2.pytorch.org/whl/cpu/torch-2.7.1%2Bcpu-cp311-cp311-manylinux_2_28_aarch64.whl", hash = "sha256:5fe6045b8f426bf2d0426e4fe009f1667a954ec2aeb82f1bd0bf60c6d7a85445", upload-time = "2025-06-03T18:27:52Z" },
+ { url = "https://download-r2.pytorch.org/whl/cpu/torch-2.7.1%2Bcpu-cp311-cp311-manylinux_2_28_x86_64.whl", hash = "sha256:a1684793e352f03fa14f78857e55d65de4ada8405ded1da2bf4f452179c4b779", upload-time = "2025-06-03T18:27:53Z" },
+ { url = "https://download-r2.pytorch.org/whl/cpu/torch-2.7.1%2Bcpu-cp311-cp311-win_amd64.whl", hash = "sha256:7b977eccbc85ae2bd19d6998de7b1f1f4bd3c04eaffd3015deb7934389783399", upload-time = "2025-06-03T18:27:58Z" },
+ { url = "https://download-r2.pytorch.org/whl/cpu/torch-2.7.1%2Bcpu-cp312-cp312-manylinux_2_28_aarch64.whl", hash = "sha256:3bf2db5adf77b433844f080887ade049c4705ddf9fe1a32023ff84ff735aa5ad", upload-time = "2025-06-03T18:27:57Z" },
+ { url = "https://download-r2.pytorch.org/whl/cpu/torch-2.7.1%2Bcpu-cp312-cp312-manylinux_2_28_x86_64.whl", hash = "sha256:8f8b3cfc53010a4b4a3c7ecb88c212e9decc4f5eeb6af75c3c803937d2d60947", upload-time = "2025-06-03T18:27:57Z" },
+ { url = "https://download-r2.pytorch.org/whl/cpu/torch-2.7.1%2Bcpu-cp312-cp312-win_amd64.whl", hash = "sha256:0bc887068772233f532b51a3e8c8cfc682ae62bef74bf4e0c53526c8b9e4138f", upload-time = "2025-06-03T18:27:56Z" },
+ { url = "https://download-r2.pytorch.org/whl/cpu/torch-2.7.1%2Bcpu-cp312-cp312-win_arm64.whl", hash = "sha256:a2618775f32eb4126c5b2050686da52001a08cffa331637d9cf51c8250931e00", upload-time = "2025-07-16T16:40:20Z" },
]
[[package]]
@@ -3538,12 +3573,12 @@ dependencies = [
{ name = "typing-extensions", marker = "extra == 'extra-8-invokeai-cuda' or (extra == 'extra-8-invokeai-cpu' and extra == 'extra-8-invokeai-rocm')" },
]
wheels = [
- { url = "https://download.pytorch.org/whl/cu128/torch-2.7.1%2Bcu128-cp311-cp311-manylinux_2_28_aarch64.whl", hash = "sha256:3a0954c54fd7cb9f45beab1272dece2a05b0e77023c1da33ba32a7919661260f" },
- { url = "https://download.pytorch.org/whl/cu128/torch-2.7.1%2Bcu128-cp311-cp311-manylinux_2_28_x86_64.whl", hash = "sha256:c301dc280458afd95450af794924c98fe07522dd148ff384739b810e3e3179f2" },
- { url = "https://download.pytorch.org/whl/cu128/torch-2.7.1%2Bcu128-cp311-cp311-win_amd64.whl", hash = "sha256:138c66dcd0ed2f07aafba3ed8b7958e2bed893694990e0b4b55b6b2b4a336aa6" },
- { url = "https://download.pytorch.org/whl/cu128/torch-2.7.1%2Bcu128-cp312-cp312-manylinux_2_28_aarch64.whl", hash = "sha256:268e54db9f0bc2b7b9eb089852d3e592c2dea2facc3db494100c3d3b796549fa" },
- { url = "https://download.pytorch.org/whl/cu128/torch-2.7.1%2Bcu128-cp312-cp312-manylinux_2_28_x86_64.whl", hash = "sha256:0b64f7d0a6f2a739ed052ba959f7b67c677028c9566ce51997f9f90fe573ddaa" },
- { url = "https://download.pytorch.org/whl/cu128/torch-2.7.1%2Bcu128-cp312-cp312-win_amd64.whl", hash = "sha256:2bb8c05d48ba815b316879a18195d53a6472a03e297d971e916753f8e1053d30" },
+ { url = "https://download-r2.pytorch.org/whl/cu128/torch-2.7.1%2Bcu128-cp311-cp311-manylinux_2_28_aarch64.whl", hash = "sha256:3a0954c54fd7cb9f45beab1272dece2a05b0e77023c1da33ba32a7919661260f", upload-time = "2025-06-03T18:31:04Z" },
+ { url = "https://download-r2.pytorch.org/whl/cu128/torch-2.7.1%2Bcu128-cp311-cp311-manylinux_2_28_x86_64.whl", hash = "sha256:c301dc280458afd95450af794924c98fe07522dd148ff384739b810e3e3179f2", upload-time = "2025-06-03T18:31:06Z" },
+ { url = "https://download-r2.pytorch.org/whl/cu128/torch-2.7.1%2Bcu128-cp311-cp311-win_amd64.whl", hash = "sha256:138c66dcd0ed2f07aafba3ed8b7958e2bed893694990e0b4b55b6b2b4a336aa6", upload-time = "2025-06-03T18:31:13Z" },
+ { url = "https://download-r2.pytorch.org/whl/cu128/torch-2.7.1%2Bcu128-cp312-cp312-manylinux_2_28_aarch64.whl", hash = "sha256:268e54db9f0bc2b7b9eb089852d3e592c2dea2facc3db494100c3d3b796549fa", upload-time = "2025-06-03T18:31:20Z" },
+ { url = "https://download-r2.pytorch.org/whl/cu128/torch-2.7.1%2Bcu128-cp312-cp312-manylinux_2_28_x86_64.whl", hash = "sha256:0b64f7d0a6f2a739ed052ba959f7b67c677028c9566ce51997f9f90fe573ddaa", upload-time = "2025-06-03T18:31:26Z" },
+ { url = "https://download-r2.pytorch.org/whl/cu128/torch-2.7.1%2Bcu128-cp312-cp312-win_amd64.whl", hash = "sha256:2bb8c05d48ba815b316879a18195d53a6472a03e297d971e916753f8e1053d30", upload-time = "2025-06-03T18:31:46Z" },
]
[[package]]
@@ -3571,8 +3606,8 @@ dependencies = [
{ name = "typing-extensions", marker = "(extra == 'extra-8-invokeai-cpu' and extra == 'extra-8-invokeai-cuda') or (extra != 'extra-8-invokeai-cuda' and extra == 'extra-8-invokeai-rocm') or (extra != 'extra-8-invokeai-cpu' and extra == 'extra-8-invokeai-rocm')" },
]
wheels = [
- { url = "https://download.pytorch.org/whl/rocm6.3/torch-2.7.1%2Brocm6.3-cp311-cp311-manylinux_2_28_x86_64.whl", hash = "sha256:73b7eb3777ffe6b73bf9881686dd659bd71231ac87ac3696d2477e2fe0c036fc" },
- { url = "https://download.pytorch.org/whl/rocm6.3/torch-2.7.1%2Brocm6.3-cp312-cp312-manylinux_2_28_x86_64.whl", hash = "sha256:b0c10342f64a34998ae8d5084aa1beae7e11defa46a4e05fe9aa6f09ffb0db37" },
+ { url = "https://download-r2.pytorch.org/whl/rocm6.3/torch-2.7.1%2Brocm6.3-cp311-cp311-manylinux_2_28_x86_64.whl", hash = "sha256:73b7eb3777ffe6b73bf9881686dd659bd71231ac87ac3696d2477e2fe0c036fc", upload-time = "2025-06-03T18:34:51Z" },
+ { url = "https://download-r2.pytorch.org/whl/rocm6.3/torch-2.7.1%2Brocm6.3-cp312-cp312-manylinux_2_28_x86_64.whl", hash = "sha256:b0c10342f64a34998ae8d5084aa1beae7e11defa46a4e05fe9aa6f09ffb0db37", upload-time = "2025-06-03T18:34:52Z" },
]
[[package]]
@@ -3639,10 +3674,10 @@ dependencies = [
{ name = "torch", version = "2.7.1+cpu", source = { registry = "https://download.pytorch.org/whl/cpu" }, marker = "extra == 'extra-8-invokeai-cpu' or (extra == 'extra-8-invokeai-cuda' and extra == 'extra-8-invokeai-rocm')" },
]
wheels = [
- { url = "https://download-r2.pytorch.org/whl/cpu/torchvision-0.22.1%2Bcpu-cp311-cp311-manylinux_2_28_x86_64.whl", hash = "sha256:4e0cbc165a472605d0c13da68ae22e84b17a6b815d5e600834777823e1bcb658" },
- { url = "https://download-r2.pytorch.org/whl/cpu/torchvision-0.22.1%2Bcpu-cp311-cp311-win_amd64.whl", hash = "sha256:9482adee074f60a45fd69892f7488281aadfda7836948c94b0a9b0caf55d1d67" },
- { url = "https://download-r2.pytorch.org/whl/cpu/torchvision-0.22.1%2Bcpu-cp312-cp312-manylinux_2_28_x86_64.whl", hash = "sha256:b5fa7044bd82c6358e8229351c98070cf3a7bf4a6e89ea46352ae6c65745ef94" },
- { url = "https://download-r2.pytorch.org/whl/cpu/torchvision-0.22.1%2Bcpu-cp312-cp312-win_amd64.whl", hash = "sha256:433cb4dbced7291f17064cea08ac1e5aebd02ec190e1c207d117ad62a8961f2b" },
+ { url = "https://download-r2.pytorch.org/whl/cpu/torchvision-0.22.1%2Bcpu-cp311-cp311-manylinux_2_28_x86_64.whl", hash = "sha256:4e0cbc165a472605d0c13da68ae22e84b17a6b815d5e600834777823e1bcb658", upload-time = "2025-06-03T18:37:22Z" },
+ { url = "https://download-r2.pytorch.org/whl/cpu/torchvision-0.22.1%2Bcpu-cp311-cp311-win_amd64.whl", hash = "sha256:9482adee074f60a45fd69892f7488281aadfda7836948c94b0a9b0caf55d1d67", upload-time = "2025-06-03T18:37:22Z" },
+ { url = "https://download-r2.pytorch.org/whl/cpu/torchvision-0.22.1%2Bcpu-cp312-cp312-manylinux_2_28_x86_64.whl", hash = "sha256:b5fa7044bd82c6358e8229351c98070cf3a7bf4a6e89ea46352ae6c65745ef94", upload-time = "2025-06-03T18:37:22Z" },
+ { url = "https://download-r2.pytorch.org/whl/cpu/torchvision-0.22.1%2Bcpu-cp312-cp312-win_amd64.whl", hash = "sha256:433cb4dbced7291f17064cea08ac1e5aebd02ec190e1c207d117ad62a8961f2b", upload-time = "2025-06-03T18:37:22Z" },
]
[[package]]
@@ -3663,10 +3698,10 @@ dependencies = [
{ name = "torch", version = "2.7.1+cu128", source = { registry = "https://download.pytorch.org/whl/cu128" }, marker = "extra == 'extra-8-invokeai-cuda' or (extra == 'extra-8-invokeai-cpu' and extra == 'extra-8-invokeai-rocm')" },
]
wheels = [
- { url = "https://download-r2.pytorch.org/whl/cu128/torchvision-0.22.1%2Bcu128-cp311-cp311-manylinux_2_28_x86_64.whl", hash = "sha256:92568ac46b13a8c88b61589800b1b9c4629be091ea7ce080fc6fc622e11e0915" },
- { url = "https://download-r2.pytorch.org/whl/cu128/torchvision-0.22.1%2Bcu128-cp311-cp311-win_amd64.whl", hash = "sha256:85ecd729c947151eccea502853be6efc2c0029dc26e6e5148e04684aed008390" },
- { url = "https://download-r2.pytorch.org/whl/cu128/torchvision-0.22.1%2Bcu128-cp312-cp312-manylinux_2_28_x86_64.whl", hash = "sha256:f64ef9bb91d71ab35d8384912a19f7419e35928685bc67544d58f45148334373" },
- { url = "https://download-r2.pytorch.org/whl/cu128/torchvision-0.22.1%2Bcu128-cp312-cp312-win_amd64.whl", hash = "sha256:650561ba326d21021243f5e064133dc62dc64d52f79623db5cd76637a9665f96" },
+ { url = "https://download-r2.pytorch.org/whl/cu128/torchvision-0.22.1%2Bcu128-cp311-cp311-manylinux_2_28_x86_64.whl", hash = "sha256:92568ac46b13a8c88b61589800b1b9c4629be091ea7ce080fc6fc622e11e0915", upload-time = "2025-06-03T18:37:28Z" },
+ { url = "https://download-r2.pytorch.org/whl/cu128/torchvision-0.22.1%2Bcu128-cp311-cp311-win_amd64.whl", hash = "sha256:85ecd729c947151eccea502853be6efc2c0029dc26e6e5148e04684aed008390", upload-time = "2025-06-03T18:37:28Z" },
+ { url = "https://download-r2.pytorch.org/whl/cu128/torchvision-0.22.1%2Bcu128-cp312-cp312-manylinux_2_28_x86_64.whl", hash = "sha256:f64ef9bb91d71ab35d8384912a19f7419e35928685bc67544d58f45148334373", upload-time = "2025-06-03T18:37:28Z" },
+ { url = "https://download-r2.pytorch.org/whl/cu128/torchvision-0.22.1%2Bcu128-cp312-cp312-win_amd64.whl", hash = "sha256:650561ba326d21021243f5e064133dc62dc64d52f79623db5cd76637a9665f96", upload-time = "2025-06-03T18:37:28Z" },
]
[[package]]
@@ -3689,8 +3724,8 @@ dependencies = [
{ name = "torch", version = "2.7.1+rocm6.3", source = { registry = "https://download.pytorch.org/whl/rocm6.3" }, marker = "(extra == 'extra-8-invokeai-cpu' and extra == 'extra-8-invokeai-cuda') or (extra != 'extra-8-invokeai-cuda' and extra == 'extra-8-invokeai-rocm') or (extra != 'extra-8-invokeai-cpu' and extra == 'extra-8-invokeai-rocm')" },
]
wheels = [
- { url = "https://download-r2.pytorch.org/whl/rocm6.3/torchvision-0.22.1%2Brocm6.3-cp311-cp311-manylinux_2_28_x86_64.whl", hash = "sha256:c150162c2e1de371e5a52c0eb4a98541f307e01716cfe5c850f25c7caa3d3fc4" },
- { url = "https://download-r2.pytorch.org/whl/rocm6.3/torchvision-0.22.1%2Brocm6.3-cp312-cp312-manylinux_2_28_x86_64.whl", hash = "sha256:0dce205fb04d9eb2f6feb74faf17cba9180aff70a8c8ac084912ce41b2dc0ab7" },
+ { url = "https://download-r2.pytorch.org/whl/rocm6.3/torchvision-0.22.1%2Brocm6.3-cp311-cp311-manylinux_2_28_x86_64.whl", hash = "sha256:c150162c2e1de371e5a52c0eb4a98541f307e01716cfe5c850f25c7caa3d3fc4", upload-time = "2025-06-03T18:37:32Z" },
+ { url = "https://download-r2.pytorch.org/whl/rocm6.3/torchvision-0.22.1%2Brocm6.3-cp312-cp312-manylinux_2_28_x86_64.whl", hash = "sha256:0dce205fb04d9eb2f6feb74faf17cba9180aff70a8c8ac084912ce41b2dc0ab7", upload-time = "2025-06-03T18:37:32Z" },
]
[[package]]