Repository: Haidra-Org/hordelib (language: python)

Issue:
feat: img2img on Stable Cascade, update comfyui to `40e124c6`; fix: minor bug fixes
- Adds remix support for Stable Cascade
- Fixes comfyui compat for upcoming comfyui version
  - See commit notes for https://github.com/Haidra-Org/hordelib/pull/219/commits/3b5909e722b026fb9abb22a41475647486a818b8 
- allow adhoc lora downloads to run concurrently 
  - See https://github.com/Haidra-Org/hordelib/pull/219/commits/2f42d315d6835b17837ae6e2b73f27f6dc7349db

TODO:
- [ ] Add additional fields to sdk (https://github.com/Haidra-Org/horde-sdk/issues/165)


Discussion / hints:

Fix patch:
diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml
index 47ec946c..6c3b6439 100644
--- a/.pre-commit-config.yaml
+++ b/.pre-commit-config.yaml
@@ -6,16 +6,16 @@ repos:
     -   id: end-of-file-fixer
     -   id: trailing-whitespace
 -   repo: https://github.com/psf/black
-    rev: 24.2.0
+    rev: 24.3.0
     hooks:
     -   id: black
         exclude: ^hordelib/nodes/.*\..*$
 -   repo: https://github.com/astral-sh/ruff-pre-commit
-    rev: v0.2.2
+    rev: v0.3.3
     hooks:
     -    id: ruff
 -   repo: https://github.com/pre-commit/mirrors-mypy
-    rev: 'v1.8.0'
+    rev: 'v1.9.0'
     hooks:
     -   id: mypy
         exclude: ^examples/.*$ # FIXME
diff --git a/examples/kudos.py b/examples/kudos.py
index a962374a..6e398c26 100644
--- a/examples/kudos.py
+++ b/examples/kudos.py
@@ -88,6 +88,7 @@ class KudosModel:
         "inpainting",
         "outpainting",
         "txt2img",
+        "remix",
     ]
 
     def __init__(self, model_filename=None):
diff --git a/hordelib/comfy_horde.py b/hordelib/comfy_horde.py
index c2163c50..c9a1759f 100644
--- a/hordelib/comfy_horde.py
+++ b/hordelib/comfy_horde.py
@@ -506,6 +506,7 @@ def _fix_node_names(self, data: dict, design: dict) -> dict:
     # pace. This is why the only thing that partially relies on that format, is in fact, optional.
     def _patch_pipeline(self, data: dict, design: dict) -> dict:
         """Patch the pipeline data with the design data."""
+        # FIXME: This can now be done through the _meta.title key included with each API export.
         # First replace comfyui standard types with hordelib node types
         data = self._fix_pipeline_types(data)
         # Now try to find better parameter names
@@ -689,11 +690,12 @@ def _run_pipeline(
         # This is useful for dumping the entire pipeline to the terminal when
         # developing and debugging new pipelines. A badly structured pipeline
         # file just results in a cryptic error from comfy
-        if False:  # This isn't here, Tazlin :)
-            pretty_pipeline = pformat(pipeline)
-            logger.warning(pretty_pipeline)
-            with open("pipeline_debug.json", "w") as outfile:
-                outfile.write(json.dumps(pipeline, indent=4))
+        # if False:  # This isn't here, Tazlin :)
+        # with open("pipeline_debug.json", "w") as outfile:
+        #     default = lambda o: f"<<non-serializable: {type(o).__qualname__}>>"
+        #     outfile.write(json.dumps(pipeline, indent=4, default=default))
+        # pretty_pipeline = pformat(pipeline)
+        # logger.warning(pretty_pipeline)
 
         # The client_id parameter here is just so we receive comfy callbacks for debugging.
         # We pretend we are a web client and want async callbacks.
diff --git a/hordelib/config_path.py b/hordelib/config_path.py
index 8c9f686b..fa1ddb08 100644
--- a/hordelib/config_path.py
+++ b/hordelib/config_path.py
@@ -23,4 +23,5 @@ def set_system_path() -> None:
     """Adds ComfyUI to the python path, as it is not a proper library."""
     comfyui_path = get_comfyui_path()
     sys.path.append(str(comfyui_path))
+    sys.path.append(str(comfyui_path) + "/comfy")
     sys.path.append(str(get_hordelib_path() / "nodes"))
diff --git a/hordelib/consts.py b/hordelib/consts.py
index 8ba150a0..eaec533c 100644
--- a/hordelib/consts.py
+++ b/hordelib/consts.py
@@ -6,7 +6,7 @@
 
 from hordelib.config_path import get_hordelib_path
 
-COMFYUI_VERSION = "2a813c3b09292c9aeab622ddf65d77e5d8171d0d"
+COMFYUI_VERSION = "0c55f16c9e66eaa4915e288b34e4f848fb2d949f"
 """The exact version of ComfyUI version to load."""
 
 REMOTE_PROXY = ""
diff --git a/hordelib/horde.py b/hordelib/horde.py
index aa43f874..53582b2c 100644
--- a/hordelib/horde.py
+++ b/hordelib/horde.py
@@ -135,7 +135,7 @@ class HordeLib:
         "hough": "control_mlsd_fp16.safetensors",
     }
 
-    SOURCE_IMAGE_PROCESSING_OPTIONS = ["img2img", "inpainting", "outpainting"]
+    SOURCE_IMAGE_PROCESSING_OPTIONS = ["img2img", "inpainting", "outpainting", "remix"]
 
     SCHEDULERS = ["normal", "karras", "simple", "ddim_uniform", "sgm_uniform", "exponential"]
 
@@ -170,6 +170,12 @@ class HordeLib:
         "model_name": {"datatype": str, "default": "stable_diffusion"},  # Used internally by hordelib
         "stable_cascade_stage_b": {"datatype": str, "default": None},  # Stable Cascade
         "stable_cascade_stage_c": {"datatype": str, "default": None},  # Stable Cascade
+        "extra_source_images": {"datatype": list, "default": []},  # Stable Cascade Remix
+    }
+
+    EXTRA_IMAGES_SCHEMA = {
+        "image": {"datatype": Image.Image, "default": None},
+        "strength": {"datatype": float, "min": 0.0, "max": 5.0, "default": 1.0},
     }
 
     LORA_SCHEMA = {
@@ -219,6 +225,8 @@ class HordeLib:
         "stable_cascade_empty_latent_image.width": "width",
         "stable_cascade_empty_latent_image.height": "height",
         "stable_cascade_empty_latent_image.batch_size": "n_iter",
+        "sc_image_loader.image": "source_image",
+        "sc_image_loader_0.image": "source_image",
         "sampler_stage_c.sampler_name": "sampler_name",
         "sampler_stage_b.sampler_name": "sampler_name",
         "sampler_stage_c.cfg": "cfg_scale",
@@ -338,6 +346,12 @@ def _validate_data_structure(self, data, schema_definition=PAYLOAD_SCHEMA):
             # Remove invalid tis
             data["tis"] = [x for x in data["tis"] if x.get("name")]
 
+        # Do the same for extra images, if we have them in this data structure
+        if data.get("extra_source_images"):
+            for i, img in enumerate(data.get("extra_source_images")):
+                data["extra_source_images"][i] = self._validate_data_structure(img, HordeLib.EXTRA_IMAGES_SCHEMA)
+            data["extra_source_images"] = [x for x in data["extra_source_images"] if x.get("image")]
+
         return data
 
     def _apply_aihorde_compatibility_hacks(self, payload: dict) -> tuple[dict, list[GenMetadataEntry]]:
@@ -802,6 +816,58 @@ def _final_pipeline_adjustments(self, payload, pipeline_data) -> tuple[dict, lis
         # the source image instead of the latent noise generator
         if pipeline_params.get("image_loader.image"):
             self.generator.reconnect_input(pipeline_data, "sampler.latent_image", "vae_encode")
+        if pipeline_params.get("sc_image_loader.image"):
+            self.generator.reconnect_input(
+                pipeline_data,
+                "sampler_stage_c.latent_image",
+                "stablecascade_stagec_vaeencode",
+            )
+            self.generator.reconnect_input(
+                pipeline_data,
+                "sampler_stage_b.latent_image",
+                "stablecascade_stagec_vaeencode",
+            )
+
+        # If we have a remix request, we check for extra images to add to the pipeline
+        if payload.get("source_processing") == "remix":
+            logger.debug([payload.get("source_image"), payload.get("extra_source_images")])
+            for image_index in range(len(payload.get("extra_source_images", []))):
+                # The first image is always taken from the source_image param
+                # That will sit on the 0 spot.
+                # Therefore we want the extra images to start iterating from 1
+                extra_image = payload["extra_source_images"][image_index]["image"]
+                extra_image_strength = payload["extra_source_images"][image_index].get("strength", 1)
+                node_index = image_index + 1
+                pipeline_data[f"sc_image_loader_{node_index}"] = {
+                    "inputs": {"image": extra_image, "upload": "image"},
+                    "class_type": "HordeImageLoader",
+                }
+                pipeline_data[f"clip_vision_encode_{node_index}"] = {
+                    "inputs": {
+                        "clip_vision": ["model_loader_stage_c", 3],
+                        "image": [f"sc_image_loader_{node_index}", 0],
+                    },
+                    "class_type": "CLIPVisionEncode",
+                }
+                pipeline_data[f"unclip_conditioning_{node_index}"] = {
+                    "inputs": {
+                        "strength": extra_image_strength,
+                        "noise_augmentation": 0,
+                        # Each conditioning ingests the conditioning before it like a chain
+                        "conditioning": [f"unclip_conditioning_{node_index-1}", 0],
+                        "clip_vision_output": [f"clip_vision_encode_{node_index}", 0],
+                    },
+                    "class_type": "unCLIPConditioning",
+                }
+
+                # The last extra image always connects to the stage_c sampler positive prompt
+                if image_index == len(payload.get("extra_source_images")) - 1:
+                    self.generator.reconnect_input(
+                        pipeline_data,
+                        "sampler_stage_c.positive",
+                        f"unclip_conditioning_{node_index}",
+                    )
+
         return pipeline_params, faults
 
     def _get_appropriate_pipeline(self, params):
@@ -818,11 +884,15 @@ def _get_appropriate_pipeline(self, params):
         #       controlnet_annotator
         #     image_facefix
         #     image_upscale
+        #     stable_cascade
+        #       stable_cascade_remix
 
         # controlnet, controlnet_hires_fix controlnet_annotator
         if params.get("model_name"):
             model_details = SharedModelManager.manager.compvis.get_model_reference_info(params["model_name"])
             if model_details.get("baseline") == "stable_cascade":
+                if params.get("source_processing") == "remix":
+                    return "stable_cascade_remix"
                 return "stable_cascade"
         if params.get("control_type"):
             if params.get("return_control_map", False):
diff --git a/hordelib/model_manager/lora.py b/hordelib/model_manager/lora.py
index f9f0623e..fc014369 100644
--- a/hordelib/model_manager/lora.py
+++ b/hordelib/model_manager/lora.py
@@ -1153,10 +1153,12 @@ def get_lora_last_use(self, lora_name, is_version: bool = False):
             return None
         return datetime.strptime(lora["versions"][version]["last_used"], "%Y-%m-%d %H:%M:%S")
 
-    def fetch_adhoc_lora(self, lora_name, timeout=45, is_version: bool = False) -> str | None:
-        """Checks if a LoRa is available
-        If not, immediately downloads it
-        Finally, returns the lora name"""
+    def fetch_adhoc_lora(self, lora_name, timeout: int | None = 45, is_version: bool = False) -> str | None:
+        """Checks if a LoRa is available. If not, waits for it to be downloaded to complete *if timeout is set*.
+
+        - If timeout is set, it will wait for the download to complete and return the lora name.
+        - If timeout is *not* set, it will begin a download thread and return `None` immediately.
+        """
         if is_version and not isinstance(lora_name, int) and not lora_name.isdigit():
             logger.debug("Lora version requested, but lora name is not an integer")
             return None
@@ -1199,16 +1201,18 @@ def fetch_adhoc_lora(self, lora_name, timeout=45, is_version: bool = False) -> s
                 self._touch_lora(lora_name, False)
                 return fuzzy_find
         self._download_lora(lora)
-        # We need to wait a bit to make sure the threads pick up the download
-        time.sleep(self.THREAD_WAIT_TIME)
-        self.wait_for_downloads(timeout)
-        version = self.find_latest_version(lora)
-        if is_version:
-            version = lora_name
-        if version is None:
-            return None
-        self._touch_lora(version, True)
-        return lora["name"]
+        if timeout is not None:
+            # We need to wait a bit to make sure the threads pick up the download
+            time.sleep(self.THREAD_WAIT_TIME)
+            self.wait_for_downloads(timeout)
+            version = self.find_latest_version(lora)
+            if is_version:
+                version = lora_name
+            if version is None:
+                return None
+            self._touch_lora(version, True)
+            return lora["name"]
+        return None
 
     def do_baselines_match(self, lora_name, model_details, is_version: bool = False):
         self._check_for_refresh(lora_name)
diff --git a/hordelib/pipeline_designs/pipeline_stable_cascade.json b/hordelib/pipeline_designs/pipeline_stable_cascade.json
index 81cd4321..6331dcb3 100644
--- a/hordelib/pipeline_designs/pipeline_stable_cascade.json
+++ b/hordelib/pipeline_designs/pipeline_stable_cascade.json
@@ -1,6 +1,6 @@
 {
-  "last_node_id": 46,
-  "last_link_id": 114,
+  "last_node_id": 50,
+  "last_link_id": 118,
   "nodes": [
     {
       "id": 8,
@@ -14,7 +14,7 @@
         "1": 46
       },
       "flags": {},
-      "order": 9,
+      "order": 12,
       "mode": 0,
       "inputs": [
         {
@@ -55,7 +55,7 @@
         "1": 46
       },
       "flags": {},
-      "order": 7,
+      "order": 10,
       "mode": 0,
       "inputs": [
         {
@@ -96,7 +96,7 @@
         "1": 26
       },
       "flags": {},
-      "order": 5,
+      "order": 7,
       "mode": 0,
       "inputs": [
         {
@@ -181,7 +181,7 @@
         "1": 262
       },
       "flags": {},
-      "order": 8,
+      "order": 11,
       "mode": 0,
       "inputs": [
         {
@@ -224,7 +224,7 @@
         969764120050513,
         "randomize",
         10,
-        1.1,
+        1,
         "euler_ancestral",
         "simple",
         1
@@ -287,7 +287,7 @@
         "1": 262
       },
       "flags": {},
-      "order": 6,
+      "order": 8,
       "mode": 0,
       "inputs": [
         {
@@ -348,7 +348,7 @@
         "1": 192
       },
       "flags": {},
-      "order": 3,
+      "order": 4,
       "mode": 0,
       "inputs": [
         {
@@ -378,6 +378,74 @@
       "color": "#232",
       "bgcolor": "#353"
     },
+    {
+      "id": 7,
+      "type": "CLIPTextEncode",
+      "pos": [
+        91,
+        553
+      ],
+      "size": {
+        "0": 432,
+        "1": 96
+      },
+      "flags": {},
+      "order": 5,
+      "mode": 0,
+      "inputs": [
+        {
+          "name": "clip",
+          "type": "CLIP",
+          "link": 112
+        }
+      ],
+      "outputs": [
+        {
+          "name": "CONDITIONING",
+          "type": "CONDITIONING",
+          "links": [
+            108
+          ],
+          "slot_index": 0
+        }
+      ],
+      "title": "negative_prompt",
+      "properties": {
+        "Node name for S&R": "CLIPTextEncode"
+      },
+      "widgets_values": [
+        "duo, multiple"
+      ],
+      "color": "#322",
+      "bgcolor": "#533"
+    },
+    {
+      "id": 9,
+      "type": "SaveImage",
+      "pos": [
+        1903,
+        81
+      ],
+      "size": {
+        "0": 971.827392578125,
+        "1": 1000.2752075195312
+      },
+      "flags": {},
+      "order": 13,
+      "mode": 0,
+      "inputs": [
+        {
+          "name": "images",
+          "type": "IMAGE",
+          "link": 9
+        }
+      ],
+      "title": "save_image",
+      "properties": {},
+      "widgets_values": [
+        "ComfyUI"
+      ]
+    },
     {
       "id": 41,
       "type": "CheckpointLoaderSimple",
@@ -415,8 +483,11 @@
         {
           "name": "VAE",
           "type": "VAE",
-          "links": null,
-          "shape": 3
+          "links": [
+            116
+          ],
+          "shape": 3,
+          "slot_index": 2
         }
       ],
       "title": "model_loader_stage_c",
@@ -428,71 +499,131 @@
       ]
     },
     {
-      "id": 7,
-      "type": "CLIPTextEncode",
+      "id": 47,
+      "type": "StableCascade_StageC_VAEEncode",
       "pos": [
-        91,
-        553
+        445,
+        926
       ],
       "size": {
-        "0": 432,
-        "1": 96
+        "0": 315,
+        "1": 78
       },
       "flags": {},
-      "order": 4,
+      "order": 9,
       "mode": 0,
       "inputs": [
         {
-          "name": "clip",
-          "type": "CLIP",
-          "link": 112
+          "name": "image",
+          "type": "IMAGE",
+          "link": 118
+        },
+        {
+          "name": "vae",
+          "type": "VAE",
+          "link": 116
         }
       ],
       "outputs": [
         {
-          "name": "CONDITIONING",
-          "type": "CONDITIONING",
+          "name": "stage_c",
+          "type": "LATENT",
+          "links": null,
+          "shape": 3
+        },
+        {
+          "name": "stage_b",
+          "type": "LATENT",
+          "links": null,
+          "shape": 3
+        }
+      ],
+      "title": "stablecascade_stagec_vaeencode",
+      "properties": {
+        "Node name for S&R": "StableCascade_StageC_VAEEncode"
+      },
+      "widgets_values": [
+        42
+      ]
+    },
+    {
+      "id": 48,
+      "type": "LoadImage",
+      "pos": [
+        -263,
+        724
+      ],
+      "size": [
+        315,
+        314
+      ],
+      "flags": {},
+      "order": 3,
+      "mode": 0,
+      "outputs": [
+        {
+          "name": "IMAGE",
+          "type": "IMAGE",
           "links": [
-            108
+            117
           ],
+          "shape": 3,
           "slot_index": 0
+        },
+        {
+          "name": "MASK",
+          "type": "MASK",
+          "links": null,
+          "shape": 3
         }
       ],
-      "title": "negative_prompt",
+      "title": "sc_image_loader",
       "properties": {
-        "Node name for S&R": "CLIPTextEncode"
+        "Node name for S&R": "LoadImage"
       },
       "widgets_values": [
-        "duo, multiple"
-      ],
-      "color": "#322",
-      "bgcolor": "#533"
+        "example.png",
+        "image"
+      ]
     },
     {
-      "id": 9,
-      "type": "SaveImage",
+      "id": 50,
+      "type": "RepeatImageBatch",
       "pos": [
-        1903,
-        81
+        99,
+        936
       ],
       "size": {
-        "0": 971.827392578125,
-        "1": 1000.2752075195312
+        "0": 315,
+        "1": 58
       },
       "flags": {},
-      "order": 10,
+      "order": 6,
       "mode": 0,
       "inputs": [
         {
-          "name": "images",
+          "name": "image",
           "type": "IMAGE",
-          "link": 9
+          "link": 117
         }
       ],
-      "title": "save_image",
-      "properties": {},
+      "outputs": [
+        {
+          "name": "IMAGE",
+          "type": "IMAGE",
+          "links": [
+            118
+          ],
+          "shape": 3,
+          "slot_index": 0
+        }
+      ],
+      "title": "repeat_image_batch",
+      "properties": {
+        "Node name for S&R": "RepeatImageBatch"
+      },
       "widgets_values": [
-        "ComfyUI"
+        1
       ]
     }
   ],
@@ -624,6 +755,30 @@
       8,
       1,
       "VAE"
+    ],
+    [
+      116,
+      41,
+      2,
+      47,
+      1,
+      "VAE"
+    ],
+    [
+      117,
+      48,
+      0,
+      50,
+      0,
+      "IMAGE"
+    ],
+    [
+      118,
+      50,
+      0,
+      47,
+      0,
+      "IMAGE"
     ]
   ],
   "groups": [
diff --git a/hordelib/pipeline_designs/pipeline_stable_cascade_remix.json b/hordelib/pipeline_designs/pipeline_stable_cascade_remix.json
new file mode 100644
index 00000000..efc0f567
--- /dev/null
+++ b/hordelib/pipeline_designs/pipeline_stable_cascade_remix.json
@@ -0,0 +1,780 @@
+{
+  "last_node_id": 57,
+  "last_link_id": 140,
+  "nodes": [
+    {
+      "id": 8,
+      "type": "VAEDecode",
+      "pos": [
+        1621,
+        267
+      ],
+      "size": {
+        "0": 210,
+        "1": 46
+      },
+      "flags": {},
+      "order": 11,
+      "mode": 0,
+      "inputs": [
+        {
+          "name": "samples",
+          "type": "LATENT",
+          "link": 98
+        },
+        {
+          "name": "vae",
+          "type": "VAE",
+          "link": 114,
+          "slot_index": 1
+        }
+      ],
+      "outputs": [
+        {
+          "name": "IMAGE",
+          "type": "IMAGE",
+          "links": [
+            9
+          ],
+          "slot_index": 0
+        }
+      ],
+      "properties": {
+        "Node name for S&R": "VAEDecode"
+      }
+    },
+    {
+      "id": 9,
+      "type": "SaveImage",
+      "pos": [
+        1864,
+        269
+      ],
+      "size": {
+        "0": 971.827392578125,
+        "1": 1000.2752075195312
+      },
+      "flags": {},
+      "order": 12,
+      "mode": 0,
+      "inputs": [
+        {
+          "name": "images",
+          "type": "IMAGE",
+          "link": 9
+        }
+      ],
+      "properties": {},
+      "widgets_values": [
+        "ComfyUI"
+      ]
+    },
+    {
+      "id": 42,
+      "type": "CheckpointLoaderSimple",
+      "pos": [
+        895,
+        122
+      ],
+      "size": {
+        "0": 349.90911865234375,
+        "1": 98
+      },
+      "flags": {},
+      "order": 0,
+      "mode": 0,
+      "outputs": [
+        {
+          "name": "MODEL",
+          "type": "MODEL",
+          "links": [
+            113
+          ],
+          "shape": 3,
+          "slot_index": 0
+        },
+        {
+          "name": "CLIP",
+          "type": "CLIP",
+          "links": null,
+          "shape": 3
+        },
+        {
+          "name": "VAE",
+          "type": "VAE",
+          "links": [
+            114
+          ],
+          "shape": 3,
+          "slot_index": 2
+        }
+      ],
+      "title": "model_loader_stage_b",
+      "properties": {
+        "Node name for S&R": "CheckpointLoaderSimple"
+      },
+      "widgets_values": [
+        "stable_cascade_stage_b.safetensors"
+      ]
+    },
+    {
+      "id": 7,
+      "type": "CLIPTextEncode",
+      "pos": [
+        -310.36394708806836,
+        470.81817904385645
+      ],
+      "size": {
+        "0": 432,
+        "1": 96
+      },
+      "flags": {},
+      "order": 5,
+      "mode": 0,
+      "inputs": [
+        {
+          "name": "clip",
+          "type": "CLIP",
+          "link": 120
+        }
+      ],
+      "outputs": [
+        {
+          "name": "CONDITIONING",
+          "type": "CONDITIONING",
+          "links": [
+            108,
+            134
+          ],
+          "slot_index": 0
+        }
+      ],
+      "title": "negative_prompt",
+      "properties": {
+        "Node name for S&R": "CLIPTextEncode"
+      },
+      "widgets_values": [
+        "text, watermark"
+      ],
+      "color": "#322",
+      "bgcolor": "#533"
+    },
+    {
+      "id": 49,
+      "type": "unCLIPCheckpointLoader",
+      "pos": [
+        -742,
+        314
+      ],
+      "size": {
+        "0": 354.7052917480469,
+        "1": 118
+      },
+      "flags": {},
+      "order": 1,
+      "mode": 0,
+      "outputs": [
+        {
+          "name": "MODEL",
+          "type": "MODEL",
+          "links": [
+            118
+          ],
+          "shape": 3,
+          "slot_index": 0
+        },
+        {
+          "name": "CLIP",
+          "type": "CLIP",
+          "links": [
+            119,
+            120
+          ],
+          "shape": 3,
+          "slot_index": 1
+        },
+        {
+          "name": "VAE",
+          "type": "VAE",
+          "links": null,
+          "shape": 3
+        },
+        {
+          "name": "CLIP_VISION",
+          "type": "CLIP_VISION",
+          "links": [
+            121
+          ],
+          "shape": 3,
+          "slot_index": 3
+        }
+      ],
+      "title": "model_loader_stage_c",
+      "properties": {
+        "Node name for S&R": "unCLIPCheckpointLoader"
+      },
+      "widgets_values": [
+        "stable_cascade_stage_c.safetensors"
+      ]
+    },
+    {
+      "id": 33,
+      "type": "KSampler",
+      "pos": [
+        1278,
+        265
+      ],
+      "size": {
+        "0": 315,
+        "1": 262
+      },
+      "flags": {},
+      "order": 10,
+      "mode": 0,
+      "inputs": [
+        {
+          "name": "model",
+          "type": "MODEL",
+          "link": 113,
+          "slot_index": 0
+        },
+        {
+          "name": "positive",
+          "type": "CONDITIONING",
+          "link": 86
+        },
+        {
+          "name": "negative",
+          "type": "CONDITIONING",
+          "link": 134
+        },
+        {
+          "name": "latent_image",
+          "type": "LATENT",
+          "link": 83
+        }
+      ],
+      "outputs": [
+        {
+          "name": "LATENT",
+          "type": "LATENT",
+          "links": [
+            98
+          ],
+          "slot_index": 0
+        }
+      ],
+      "title": "sampler_stage_b",
+      "properties": {
+        "Node name for S&R": "KSampler"
+      },
+      "widgets_values": [
+        1010381433308348,
+        "randomize",
+        10,
+        1,
+        "euler_ancestral",
+        "simple",
+        1
+      ]
+    },
+    {
+      "id": 3,
+      "type": "KSampler",
+      "pos": [
+        487,
+        211
+      ],
+      "size": {
+        "0": 315,
+        "1": 262
+      },
+      "flags": {},
+      "order": 8,
+      "mode": 0,
+      "inputs": [
+        {
+          "name": "model",
+          "type": "MODEL",
+          "link": 118,
+          "slot_index": 0
+        },
+        {
+          "name": "positive",
+          "type": "CONDITIONING",
+          "link": 140
+        },
+        {
+          "name": "negative",
+          "type": "CONDITIONING",
+          "link": 108
+        },
+        {
+          "name": "latent_image",
+          "type": "LATENT",
+          "link": 123
+        }
+      ],
+      "outputs": [
+        {
+          "name": "LATENT",
+          "type": "LATENT",
+          "links": [
+            88
+          ],
+          "slot_index": 0
+        }
+      ],
+      "title": "sampler_stage_c",
+      "properties": {
+        "Node name for S&R": "KSampler"
+      },
+      "widgets_values": [
+        528868033851571,
+        "randomize",
+        20,
+        4,
+        "euler_ancestral",
+        "simple",
+        1
+      ]
+    },
+    {
+      "id": 36,
+      "type": "StableCascade_StageB_Conditioning",
+      "pos": [
+        953,
+        287
+      ],
+      "size": {
+        "0": 277.20001220703125,
+        "1": 46
+      },
+      "flags": {},
+      "order": 9,
+      "mode": 0,
+      "inputs": [
+        {
+          "name": "conditioning",
+          "type": "CONDITIONING",
+          "link": 133
+        },
+        {
+          "name": "stage_c",
+          "type": "LATENT",
+          "link": 88
+        }
+      ],
+      "outputs": [
+        {
+          "name": "CONDITIONING",
+          "type": "CONDITIONING",
+          "links": [
+            86
+          ],
+          "shape": 3,
+          "slot_index": 0
+        }
+      ],
+      "properties": {
+        "Node name for S&R": "StableCascade_StageB_Conditioning"
+      }
+    },
+    {
+      "id": 48,
+      "type": "LoadImage",
+      "pos": [
+        -569,
+        648
+      ],
+      "size": {
+        "0": 315,
+        "1": 314
+      },
+      "flags": {},
+      "order": 2,
+      "mode": 0,
+      "outputs": [
+        {
+          "name": "IMAGE",
+          "type": "IMAGE",
+          "links": [
+            122
+          ],
+          "shape": 3,
+          "slot_index": 0
+        },
+        {
+          "name": "MASK",
+          "type": "MASK",
+          "links": null,
+          "shape": 3
+        }
+      ],
+      "title": "sc_image_loader_0",
+      "properties": {
+        "Node name for S&R": "LoadImage"
+      },
+      "widgets_values": [
+        "mountains.png",
+        "image"
+      ]
+    },
+    {
+      "id": 50,
+      "type": "CLIPVisionEncode",
+      "pos": [
+        -206,
+        659
+      ],
+      "size": {
+        "0": 253.60000610351562,
+        "1": 46
+      },
+      "flags": {},
+      "order": 6,
+      "mode": 0,
+      "inputs": [
+        {
+          "name": "clip_vision",
+          "type": "CLIP_VISION",
+          "link": 121
+        },
+        {
+          "name": "image",
+          "type": "IMAGE",
+          "link": 122
+        }
+      ],
+      "outputs": [
+        {
+          "name": "CLIP_VISION_OUTPUT",
+          "type": "CLIP_VISION_OUTPUT",
+          "links": [
+            124
+          ],
+          "shape": 3,
+          "slot_index": 0
+        }
+      ],
+      "title": "clip_vision_encode_0",
+      "properties": {
+        "Node name for S&R": "CLIPVisionEncode"
+      }
+    },
+    {
+      "id": 51,
+      "type": "unCLIPConditioning",
+      "pos": [
+        187,
+        307
+      ],
+      "size": {
+        "0": 262,
+        "1": 102
+      },
+      "flags": {},
+      "order": 7,
+      "mode": 0,
+      "inputs": [
+        {
+          "name": "conditioning",
+          "type": "CONDITIONING",
+          "link": 125
+        },
+        {
+          "name": "clip_vision_output",
+          "type": "CLIP_VISION_OUTPUT",
+          "link": 124
+        }
+      ],
+      "outputs": [
+        {
+          "name": "CONDITIONING",
+          "type": "CONDITIONING",
+          "links": [
+            140
+          ],
+          "shape": 3,
+          "slot_index": 0
+        }
+      ],
+      "title": "unclip_conditioning_0",
+      "properties": {
+        "Node name for S&R": "unCLIPConditioning"
+      },
+      "widgets_values": [
+        1,
+        0
+      ]
+    },
+    {
+      "id": 34,
+      "type": "StableCascade_EmptyLatentImage",
+      "pos": [
+        142,
+        771
+      ],
+      "size": {
+        "0": 315,
+        "1": 150
+      },
+      "flags": {},
+      "order": 3,
+      "mode": 0,
+      "outputs": [
+        {
+          "name": "stage_c",
+          "type": "LATENT",
+          "links": [
+            123
+          ],
+          "shape": 3,
+          "slot_index": 0
+        },
+        {
+          "name": "stage_b",
+          "type": "LATENT",
+          "links": [
+            83
+          ],
+          "shape": 3,
+          "slot_index": 1
+        }
+      ],
+      "title": "stable_cascade_empty_latent_image",
+      "properties": {
+        "Node name for S&R": "StableCascade_EmptyLatentImage"
+      },
+      "widgets_values": [
+        1024,
+        1024,
+        42,
+        1
+      ]
+    },
+    {
+      "id": 6,
+      "type": "CLIPTextEncode",
+      "pos": [
+        -310.36394708806836,
+        230.81817904385656
+      ],
+      "size": {
+        "0": 437.3416748046875,
+        "1": 184.05055236816406
+      },
+      "flags": {},
+      "order": 4,
+      "mode": 0,
+      "inputs": [
+        {
+          "name": "clip",
+          "type": "CLIP",
+          "link": 119
+        }
+      ],
+      "outputs": [
+        {
+          "name": "CONDITIONING",
+          "type": "CONDITIONING",
+          "links": [
+            125,
+            133
+          ],
+          "slot_index": 0
+        }
+      ],
+      "title": "prompt",
+      "properties": {
+        "Node name for S&R": "CLIPTextEncode"
+      },
+      "widgets_values": [
+        "A stampede of wild goats"
+      ],
+      "color": "#232",
+      "bgcolor": "#353"
+    }
+  ],
+  "links": [
+    [
+      9,
+      8,
+      0,
+      9,
+      0,
+      "IMAGE"
+    ],
+    [
+      83,
+      34,
+      1,
+      33,
+      3,
+      "LATENT"
+    ],
+    [
+      86,
+      36,
+      0,
+      33,
+      1,
+      "CONDITIONING"
+    ],
+    [
+      88,
+      3,
+      0,
+      36,
+      1,
+      "LATENT"
+    ],
+    [
+      98,
+      33,
+      0,
+      8,
+      0,
+      "LATENT"
+    ],
+    [
+      108,
+      7,
+      0,
+      3,
+      2,
+      "CONDITIONING"
+    ],
+    [
+      113,
+      42,
+      0,
+      33,
+      0,
+      "MODEL"
+    ],
+    [
+      114,
+      42,
+      2,
+      8,
+      1,
+      "VAE"
+    ],
+    [
+      118,
+      49,
+      0,
+      3,
+      0,
+      "MODEL"
+    ],
+    [
+      119,
+      49,
+      1,
+      6,
+      0,
+      "CLIP"
+    ],
+    [
+      120,
+      49,
+      1,
+      7,
+      0,
+      "CLIP"
+    ],
+    [
+      121,
+      49,
+      3,
+      50,
+      0,
+      "CLIP_VISION"
+    ],
+    [
+      122,
+      48,
+      0,
+      50,
+      1,
+      "IMAGE"
+    ],
+    [
+      123,
+      34,
+      0,
+      3,
+      3,
+      "LATENT"
+    ],
+    [
+      124,
+      50,
+      0,
+      51,
+      1,
+      "CLIP_VISION_OUTPUT"
+    ],
+    [
+      125,
+      6,
+      0,
+      51,
+      0,
+      "CONDITIONING"
+    ],
+    [
+      133,
+      6,
+      0,
+      36,
+      0,
+      "CONDITIONING"
+    ],
+    [
+      134,
+      7,
+      0,
+      33,
+      2,
+      "CONDITIONING"
+    ],
+    [
+      140,
+      51,
+      0,
+      3,
+      1,
+      "CONDITIONING"
+    ]
+  ],
+  "groups": [
+    {
+      "title": "Stage B",
+      "bounding": [
+        877,
+        -63,
+        736,
+        662
+      ],
+      "color": "#8A8",
+      "font_size": 24
+    },
+    {
+      "title": "Stage C",
+      "bounding": [
+        -793,
+        163,
+        1597,
+        518
+      ],
+      "color": "#b06634",
+      "font_size": 24
+    }
+  ],
+  "config": {},
+  "extra": {},
+  "version": 0.4
+}
diff --git a/hordelib/pipelines/pipeline_stable_cascade.json b/hordelib/pipelines/pipeline_stable_cascade.json
index 89c7d49b..846c885d 100644
--- a/hordelib/pipelines/pipeline_stable_cascade.json
+++ b/hordelib/pipelines/pipeline_stable_cascade.json
@@ -81,7 +81,7 @@
     },
     "class_type": "SaveImage",
     "_meta": {
-      "title": "Save Image"
+      "title": "save_image"
     }
   },
   "33": {
@@ -171,5 +171,45 @@
     "_meta": {
       "title": "model_loader_stage_b"
     }
+  },
+  "47": {
+    "inputs": {
+      "compression": 42,
+      "image": [
+        "50",
+        0
+      ],
+      "vae": [
+        "41",
+        2
+      ]
+    },
+    "class_type": "StableCascade_StageC_VAEEncode",
+    "_meta": {
+      "title": "stablecascade_stagec_vaeencode"
+    }
+  },
+  "48": {
+    "inputs": {
+      "image": "example.png",
+      "upload": "image"
+    },
+    "class_type": "LoadImage",
+    "_meta": {
+      "title": "sc_image_loader"
+    }
+  },
+  "50": {
+    "inputs": {
+      "amount": 1,
+      "image": [
+        "48",
+        0
+      ]
+    },
+    "class_type": "RepeatImageBatch",
+    "_meta": {
+      "title": "repeat_image_batch"
+    }
   }
 }
diff --git a/hordelib/pipelines/pipeline_stable_cascade_remix.json b/hordelib/pipelines/pipeline_stable_cascade_remix.json
new file mode 100644
index 00000000..3036c79d
--- /dev/null
+++ b/hordelib/pipelines/pipeline_stable_cascade_remix.json
@@ -0,0 +1,207 @@
+{
+  "3": {
+    "inputs": {
+      "seed": 528868033851571,
+      "steps": 20,
+      "cfg": 4,
+      "sampler_name": "euler_ancestral",
+      "scheduler": "simple",
+      "denoise": 1,
+      "model": [
+        "49",
+        0
+      ],
+      "positive": [
+        "51",
+        0
+      ],
+      "negative": [
+        "7",
+        0
+      ],
+      "latent_image": [
+        "34",
+        0
+      ]
+    },
+    "class_type": "KSampler",
+    "_meta": {
+      "title": "sampler_stage_c"
+    }
+  },
+  "6": {
+    "inputs": {
+      "text": "A stampede of wild goats",
+      "clip": [
+        "49",
+        1
+      ]
+    },
+    "class_type": "CLIPTextEncode",
+    "_meta": {
+      "title": "prompt"
+    }
+  },
+  "7": {
+    "inputs": {
+      "text": "text, watermark",
+      "clip": [
+        "49",
+        1
+      ]
+    },
+    "class_type": "CLIPTextEncode",
+    "_meta": {
+      "title": "negative_prompt"
+    }
+  },
+  "8": {
+    "inputs": {
+      "samples": [
+        "33",
+        0
+      ],
+      "vae": [
+        "42",
+        2
+      ]
+    },
+    "class_type": "VAEDecode",
+    "_meta": {
+      "title": "VAE Decode"
+    }
+  },
+  "9": {
+    "inputs": {
+      "filename_prefix": "ComfyUI",
+      "images": [
+        "8",
+        0
+      ]
+    },
+    "class_type": "SaveImage",
+    "_meta": {
+      "title": "Save Image"
+    }
+  },
+  "33": {
+    "inputs": {
+      "seed": 1010381433308348,
+      "steps": 10,
+      "cfg": 1,
+      "sampler_name": "euler_ancestral",
+      "scheduler": "simple",
+      "denoise": 1,
+      "model": [
+        "42",
+        0
+      ],
+      "positive": [
+        "36",
+        0
+      ],
+      "negative": [
+        "7",
+        0
+      ],
+      "latent_image": [
+        "34",
+        1
+      ]
+    },
+    "class_type": "KSampler",
+    "_meta": {
+      "title": "sampler_stage_b"
+    }
+  },
+  "34": {
+    "inputs": {
+      "width": 1024,
+      "height": 1024,
+      "compression": 42,
+      "batch_size": 1
+    },
+    "class_type": "StableCascade_EmptyLatentImage",
+    "_meta": {
+      "title": "stable_cascade_empty_latent_image"
+    }
+  },
+  "36": {
+    "inputs": {
+      "conditioning": [
+        "6",
+        0
+      ],
+      "stage_c": [
+        "3",
+        0
+      ]
+    },
+    "class_type": "StableCascade_StageB_Conditioning",
+    "_meta": {
+      "title": "StableCascade_StageB_Conditioning"
+    }
+  },
+  "42": {
+    "inputs": {
+      "ckpt_name": "stable_cascade_stage_b.safetensors"
+    },
+    "class_type": "CheckpointLoaderSimple",
+    "_meta": {
+      "title": "model_loader_stage_b"
+    }
+  },
+  "48": {
+    "inputs": {
+      "image": "mountains.png",
+      "upload": "image"
+    },
+    "class_type": "LoadImage",
+    "_meta": {
+      "title": "sc_image_loader_0"
+    }
+  },
+  "49": {
+    "inputs": {
+      "ckpt_name": "stable_cascade_stage_c.safetensors"
+    },
+    "class_type": "unCLIPCheckpointLoader",
+    "_meta": {
+      "title": "model_loader_stage_c"
+    }
+  },
+  "50": {
+    "inputs": {
+      "clip_vision": [
+        "49",
+        3
+      ],
+      "image": [
+        "48",
+        0
+      ]
+    },
+    "class_type": "CLIPVisionEncode",
+    "_meta": {
+      "title": "clip_vision_encode_0"
+    }
+  },
+  "51": {
+    "inputs": {
+      "strength": 1,
+      "noise_augmentation": 0,
+      "conditioning": [
+        "6",
+        0
+      ],
+      "clip_vision_output": [
+        "50",
+        0
+      ]
+    },
+    "class_type": "unCLIPConditioning",
+    "_meta": {
+      "title": "unclip_conditioning_0"
+    }
+  }
+}
diff --git a/images_expected/stable_cascade_image_remix_double.png b/images_expected/stable_cascade_image_remix_double.png
new file mode 100644
index 00000000..d0056eea
Binary files /dev/null and b/images_expected/stable_cascade_image_remix_double.png differ
diff --git a/images_expected/stable_cascade_image_remix_double_weak.png b/images_expected/stable_cascade_image_remix_double_weak.png
new file mode 100644
index 00000000..578ca4aa
Binary files /dev/null and b/images_expected/stable_cascade_image_remix_double_weak.png differ
diff --git a/images_expected/stable_cascade_image_remix_single.png b/images_expected/stable_cascade_image_remix_single.png
new file mode 100644
index 00000000..83b9a163
Binary files /dev/null and b/images_expected/stable_cascade_image_remix_single.png differ
diff --git a/images_expected/stable_cascade_image_remix_triple.png b/images_expected/stable_cascade_image_remix_triple.png
new file mode 100644
index 00000000..01b8dd7f
Binary files /dev/null and b/images_expected/stable_cascade_image_remix_triple.png differ
diff --git a/images_expected/stable_cascade_image_to_image_n_iter_0.png b/images_expected/stable_cascade_image_to_image_n_iter_0.png
new file mode 100644
index 00000000..cc3c7804
Binary files /dev/null and b/images_expected/stable_cascade_image_to_image_n_iter_0.png differ
diff --git a/images_expected/stable_cascade_image_to_image_n_iter_1.png b/images_expected/stable_cascade_image_to_image_n_iter_1.png
new file mode 100644
index 00000000..d43dc803
Binary files /dev/null and b/images_expected/stable_cascade_image_to_image_n_iter_1.png differ
diff --git a/requirements.dev.txt b/requirements.dev.txt
index 9dd5faaa..b0b285d2 100644
--- a/requirements.dev.txt
+++ b/requirements.dev.txt
@@ -1,8 +1,8 @@
-pytest==8.0.1
-mypy==1.8.0
-black==24.2.0
-ruff==0.2.2
-tox~=4.13.0
+pytest==8.1.1
+mypy==1.9.0
+black==24.3.0
+ruff==0.3.3
+tox~=4.14.1
 pre-commit~=3.6.2
 build>=0.10.0
 coverage>=7.2.7
diff --git a/requirements.txt b/requirements.txt
index da68e870..6dcf9a55 100644
--- a/requirements.txt
+++ b/requirements.txt
@@ -1,6 +1,6 @@
 # Add this in for tox, comment out for build
 --extra-index-url https://download.pytorch.org/whl/cu121
-horde_sdk>=0.7.25
+horde_sdk>=0.8.3
 horde_model_reference>=0.5.2
 pydantic
 torch>=2.1.0
@@ -45,3 +45,4 @@ mediapipe>=0.9.1.0
 unidecode
 fuzzywuzzy
 strenum
+kornia


Test patch:
diff --git a/images/test_mountains.png b/images/test_mountains.png
new file mode 100644
index 00000000..ecc55459
Binary files /dev/null and b/images/test_mountains.png differ
diff --git a/images/test_sunset.png b/images/test_sunset.png
new file mode 100644
index 00000000..725517a4
Binary files /dev/null and b/images/test_sunset.png differ
diff --git a/tests/model_managers/test_mm_lora.py b/tests/model_managers/test_mm_lora.py
index 138137f5..40c8b549 100644
--- a/tests/model_managers/test_mm_lora.py
+++ b/tests/model_managers/test_mm_lora.py
@@ -220,7 +220,7 @@ def test_adhoc_non_existing_intstring_large(self):
         lora_model_manager.wait_for_adhoc_reset(15)
         lora_name = "99999999999999"
         lora_key = lora_model_manager.fetch_adhoc_lora(lora_name)
-        assert lora_model_manager.total_retries_attempted == 1
+        assert lora_model_manager.total_retries_attempted == 0
         assert lora_key is None
         assert not lora_model_manager.is_model_available(lora_name)
         lora_model_manager.stop_all()
diff --git a/tests/test_horde_inference.py b/tests/test_horde_inference.py
index e4858382..8e65dd54 100644
--- a/tests/test_horde_inference.py
+++ b/tests/test_horde_inference.py
@@ -391,7 +391,6 @@ def test_callback_with_post_processors(
         hordelib_instance: HordeLib,
         stable_diffusion_model_name_for_testing: str,
     ):
-
         data = {
             "sampler_name": "k_euler",
             "cfg_scale": 7.5,
diff --git a/tests/test_horde_inference_cascade.py b/tests/test_horde_inference_cascade.py
index 6a6fa85f..9f8cf7bf 100644
--- a/tests/test_horde_inference_cascade.py
+++ b/tests/test_horde_inference_cascade.py
@@ -100,3 +100,222 @@ def test_cascade_text_to_image_n_iter(
         )
 
         assert check_list_inference_images_similarity(img_pairs_to_check)
+
+    def test_cascade_image_to_image(
+        self,
+        stable_cascade_base_model_name: str,
+        hordelib_instance: HordeLib,
+    ):
+        data = {
+            "sampler_name": "k_euler",
+            "cfg_scale": 7.5,
+            "denoising_strength": 0.4,
+            "seed": 1312,
+            "height": 1024,
+            "width": 1024,
+            "karras": False,
+            "tiling": False,
+            "hires_fix": False,
+            "clip_skip": 1,
+            "control_type": None,
+            "image_is_control": False,
+            "return_control_map": False,
+            "prompt": (
+                "a medieval fantasy swashbuckler with a big floppy hat walking towards "
+                "a camera while there's an explosion in the background"
+            ),
+            "ddim_steps": 20,
+            "n_iter": 2,
+            "model": stable_cascade_base_model_name,
+            "source_image": Image.open("images/test_db0.jpg"),
+            "source_processing": "img2img",
+        }
+        image_results = hordelib_instance.basic_inference(data)
+
+        assert len(image_results) == 2
+
+        img_pairs_to_check = []
+
+        img_filename_base = "stable_cascade_image_to_image_n_iter_{0}.png"
+
+        for i, image_result in enumerate(image_results):
+            assert image_result.image is not None
+            assert isinstance(image_result.image, Image.Image)
+
+            img_filename = img_filename_base.format(i)
+
+            image_result.image.save(f"images/{img_filename}", quality=100)
+            img_pairs_to_check.append((f"images_expected/{img_filename}", image_result.image))
+
+        assert check_list_inference_images_similarity(img_pairs_to_check)
+
+    def test_cascade_image_remix_single(
+        self,
+        stable_cascade_base_model_name: str,
+        hordelib_instance: HordeLib,
+    ):
+        data = {
+            "sampler_name": "k_euler",
+            "cfg_scale": 4,
+            "denoising_strength": 1,
+            "seed": 1312,
+            "height": 1024,
+            "width": 1024,
+            "karras": False,
+            "tiling": False,
+            "hires_fix": False,
+            "clip_skip": 1,
+            "control_type": None,
+            "image_is_control": False,
+            "return_control_map": False,
+            "prompt": "A herd of goats grazing under the sunset",
+            "ddim_steps": 20,
+            "n_iter": 1,
+            "model": stable_cascade_base_model_name,
+            "source_image": Image.open("images/test_mountains.png"),
+            "source_processing": "remix",
+        }
+        pil_image = hordelib_instance.basic_inference_single_image(data).image
+        assert pil_image is not None
+        assert isinstance(pil_image, Image.Image)
+
+        img_filename = "stable_cascade_image_remix_single.png"
+        pil_image.save(f"images/{img_filename}", quality=100)
+
+        assert check_single_inference_image_similarity(
+            f"images_expected/{img_filename}",
+            pil_image,
+        )
+
+    def test_cascade_image_remix_double(
+        self,
+        stable_cascade_base_model_name: str,
+        hordelib_instance: HordeLib,
+    ):
+        data = {
+            "sampler_name": "k_euler",
+            "cfg_scale": 4,
+            "denoising_strength": 1,
+            "seed": 1312,
+            "height": 1024,
+            "width": 1024,
+            "karras": False,
+            "tiling": False,
+            "hires_fix": False,
+            "clip_skip": 1,
+            "control_type": None,
+            "image_is_control": False,
+            "return_control_map": False,
+            "prompt": "A herd of goats grazing",
+            "ddim_steps": 20,
+            "n_iter": 1,
+            "model": stable_cascade_base_model_name,
+            "source_image": Image.open("images/test_mountains.png"),
+            "source_processing": "remix",
+            "extra_source_images": [
+                {
+                    "image": Image.open("images/test_sunset.png"),
+                },
+            ],
+        }
+        pil_image = hordelib_instance.basic_inference_single_image(data).image
+        assert pil_image is not None
+        assert isinstance(pil_image, Image.Image)
+
+        img_filename = "stable_cascade_image_remix_double.png"
+        pil_image.save(f"images/{img_filename}", quality=100)
+
+        assert check_single_inference_image_similarity(
+            f"images_expected/{img_filename}",
+            pil_image,
+        )
+
+    def test_cascade_image_remix_double_weak(
+        self,
+        stable_cascade_base_model_name: str,
+        hordelib_instance: HordeLib,
+    ):
+        data = {
+            "sampler_name": "k_euler",
+            "cfg_scale": 4,
+            "denoising_strength": 1,
+            "seed": 1312,
+            "height": 1024,
+            "width": 1024,
+            "karras": False,
+            "tiling": False,
+            "hires_fix": False,
+            "clip_skip": 1,
+            "control_type": None,
+            "image_is_control": False,
+            "return_control_map": False,
+            "prompt": "A herd of goats grazing",
+            "ddim_steps": 20,
+            "n_iter": 1,
+            "model": stable_cascade_base_model_name,
+            "source_image": Image.open("images/test_mountains.png"),
+            "source_processing": "remix",
+            "extra_source_images": [
+                {
+                    "image": Image.open("images/test_sunset.png"),
+                    "strength": 0.05,
+                },
+            ],
+        }
+        pil_image = hordelib_instance.basic_inference_single_image(data).image
+        assert pil_image is not None
+        assert isinstance(pil_image, Image.Image)
+
+        img_filename = "stable_cascade_image_remix_double_weak.png"
+        pil_image.save(f"images/{img_filename}", quality=100)
+
+        assert check_single_inference_image_similarity(
+            f"images_expected/{img_filename}",
+            pil_image,
+        )
+
+    def test_cascade_image_remix_triple(
+        self,
+        stable_cascade_base_model_name: str,
+        hordelib_instance: HordeLib,
+    ):
+        data = {
+            "sampler_name": "k_euler",
+            "cfg_scale": 4,
+            "denoising_strength": 1,
+            "seed": 1312,
+            "height": 1024,
+            "width": 1024,
+            "karras": False,
+            "tiling": False,
+            "hires_fix": False,
+            "clip_skip": 1,
+            "control_type": None,
+            "image_is_control": False,
+            "return_control_map": False,
+            "prompt": "Baking Sun",
+            "ddim_steps": 20,
+            "n_iter": 1,
+            "model": stable_cascade_base_model_name,
+            "source_image": Image.open("images/test_mountains.png"),
+            "source_processing": "remix",
+            "extra_source_images": [
+                {
+                    "image": Image.open("images/test_sunset.png"),
+                },
+                {
+                    "image": Image.open("images/test_db0.jpg"),
+                },
+            ],
+        }
+        pil_image = hordelib_instance.basic_inference_single_image(data).image
+        assert pil_image is not None
+        assert isinstance(pil_image, Image.Image)
+
+        img_filename = "stable_cascade_image_remix_triple.png"
+        pil_image.save(f"images/{img_filename}", quality=100)
+
+        assert check_single_inference_image_similarity(
+            f"images_expected/{img_filename}",
+            pil_image,
+        )