Repository: DeepLabCut/DeepLabCut (language: python)

Issue:
Issue with AnalyseVideos.py (Moviepy installation problem) 
Thanks in advance for any comments or suggestions.

 **Describe the problem**
An error occurred in AnalyseVideos.py
After running the evaluation, I now have a file named "DeepCut_resnet_50_95shuffle1_50000forTask_reaching.h5" in Evaluation-Tools/Results folder.
I also have a video named "MovieS2_Perturbation_noLaser_compressed" in DeepLabCut/videos folder.
I called AnalyseVideos.py from Analysis-tools folder, and the path to videos in myconfig_analysis.py is set to videofolder = '../videos/'. Therefore, it should have the video file in it. However when I run the script I get an error saying that the file could not be found. Please help me!

**Here is the exact error that appeared in terminal:**

FileNotFoundError: File MovieS2_Perturbation_noLaser_compressedDeepCut_resnet50_
reachingJan30shuffle1_50000.h5 does not exist

During handling of the above exception, another exception occurred:

Traceback (most recent call last):
  File "AnalyzeVideos.py", line 124, in <module>
    clip = VideoFileClip(video)
  File "C:\Users\annaL\AppData\Local\Programs\Python\Python35\lib\site-packages\
moviepy\video\io\VideoFileClip.py", line 91, in __init__
    fps_source=fps_source)
  File "C:\Users\annaL\AppData\Local\Programs\Python\Python35\lib\site-packages\
moviepy\video\io\ffmpeg_reader.py", line 33, in __init__
    fps_source)
  File "C:\Users\annaL\AppData\Local\Programs\Python\Python35\lib\site-packages\
moviepy\video\io\ffmpeg_reader.py", line 272, in ffmpeg_parse_infos
    "path.")%filename)
OSError: MoviePy error: the file MovieS2_Perturbation_noLaser_compressed.avi cou
ld not be found!
Please check that you entered the correct path.
PyTorch maDLC on 1 indiv creates saDLC tracks which fail in create_labeled_video
### Is there an existing issue for this?

- [X] I have searched the existing issues

### Bug description

I have a long-running maDLC project which I recently decided to try with PyTorch. While it is maDLC, it always only tracks a single animal.
The new model trained to 120 epochs with `batch_size = 8`, and evaluated to pixel errors about 2.1, which is as good (or better) than the last tensorflow model.
Upon analyzing a new video with `auto_track=True`, I now get three output files:

- `...shuffle1_snapshot_115.h5`
- `...shuffle1_snapshot_115_full.pickle`
- `...shuffle1_snapshot_115_meta.pickle`

So, unlike the previous version, I don't get `assemblies` or `..._el.h5` for the tracker.

The `full.pickle` file appears rather empty when I load it directly, and `create_video_with_all_detections` creates a video with no markers and prints errors for each frame indicating no data exists.
The h5 file appears to be assembled tracks, which is great. However, it doesn't have the `_el` that downstream methods (like `create_labeled_video` are looking for. I can add the _el to the name, but the file is also missing the "indiv" level of the multi_index. 

So short of changing my config to not be maDLC, the downstream methods don't work. This includes `create_labeled_video` and `filterpredictions` at least. Both of which return an error like

```
Filtering with median model C:\Users\...\videos-raw\cam1_28-Mar-24_18-00-34.mov
No unfiltered data file found in C:\Users\...\videos-raw for video cam1_28-Mar-24_18-00-34 and scorer DLC_DlcrnetStride16Ms5_...shuffle1_snapshot_115 and ellipse tracker.
```

By the way, if I edit my config file to pretend to be a single-animal project, filtering and creating videos works without a problem.


### Operating System

Windows 10

### DeepLabCut version

DLC 3.0.0rc4

### DeepLabCut mode

multi animal

### Device type

gpu

### Steps To Reproduce

_No response_

### Relevant log output

_No response_

### Anything else?

_No response_

### Code of Conduct

- [X] I agree to follow this project's [Code of Conduct](https://github.com/DeepLabCut/DeepLabCut/blob/master/CODE_OF_CONDUCT.md)


Discussion / hints:
Hey,

You seem to mix up 2 things:

"Evaluation-Tools/Results"  contains evaluation files of the train/test set. I.e. DeepCut_resnet_50_95shuffle1_50000forTask_reaching.h5 is one of those

In https://github.com/AlexEMG/DeepLabCut/tree/master/Analysis-tools one runs the trained network for videos (e.g. in ../videos/) Did you do that? Does "MovieS2_Perturbation_noLaser_compressedDeepCut_resnet50_
reachingJan30shuffle1_50000.h5" 

exist there?

Cheers,
Alexander

Hello Alex, thank you for your reply.

I ran the evaluation successfully. 
I wasn't able to run the analysis. I thought the problem might have emerged because something went wrong at one of the previous steps.. The only file that I have in the ../videos/ folder is the video itself.

Thank you.
I see. Judged by "Traceback (most recent call last):
File "AnalyzeVideos.py", line 124, in 
clip = VideoFileClip(video)
File "C:\Users\annaL\AppData\Local\Programs\Python\Python35\lib\site-packages
moviepy\video\io\VideoFileClip.py", line 91, in init
fps_source=fps_source)
File "C:\Users\annaL\AppData\Local\Programs\Python\Python35\lib\site-packages
moviepy\video\io\ffmpeg_reader.py", line 33, in init
fps_source)
File "C:\Users\annaL\AppData\Local\Programs\Python\Python35\lib\site-packages
moviepy\video\io\ffmpeg_reader.py", line 272, in ffmpeg_parse_infos
"path.")%filename)
OSError: MoviePy error: the file MovieS2_Perturbation_noLaser_compressed.avi cou
ld not be found!
Please check that you entered the correct path." 

I wonder if your MoviePy works correctly? Can you open videos? 
No, it doesn't! Just tried to open the video "MovieS2_Perturbation_noLaser_compressed.avi" with MoviePy, it doesn't work. Trying to understand why. Thanks for your help!
Ok, I suppose linking ffmpeg to moviepy is your problem. Please check out the help for [moviepy](http://zulko.github.io/moviepy/install.html):

MoviePy depends on the software FFMPEG for video reading and writing. You don’t need to worry about that, as FFMPEG should be automatically downloaded/installed by ImageIO during your first use of MoviePy (it takes a few seconds). If you want to use a specific version of FFMPEG, you can set the FFMPEG_BINARY environment variable See moviepy/config_defaults.py for details.



 


Fix patch:
diff --git a/deeplabcut/compat.py b/deeplabcut/compat.py
index 02798087e..43fba81ef 100644
--- a/deeplabcut/compat.py
+++ b/deeplabcut/compat.py
@@ -58,7 +58,7 @@ def get_available_aug_methods(engine: Engine) -> tuple[str, ...]:
 
 
 def train_network(
-    config: str,
+    config: str | Path,
     shuffle: int = 1,
     trainingsetindex: int = 0,
     max_snapshots_to_keep: int | None = None,
@@ -221,7 +221,7 @@ def train_network(
             max_snapshots_to_keep = 5
 
         return train_network(
-            config,
+            str(config),
             shuffle=shuffle,
             trainingsetindex=trainingsetindex,
             max_snapshots_to_keep=max_snapshots_to_keep,
@@ -317,7 +317,7 @@ def return_train_network_path(
 
 
 def evaluate_network(
-    config,
+    config: str | Path,
     Shuffles: Iterable[int] = (1,),
     trainingsetindex: int | str = 0,
     plotting: bool | str = False,
@@ -458,7 +458,7 @@ def evaluate_network(
     if engine == Engine.TF:
         from deeplabcut.pose_estimation_tensorflow import evaluate_network
         return evaluate_network(
-            config,
+            str(config),
             Shuffles=Shuffles,
             trainingsetindex=trainingsetindex,
             plotting=plotting,
diff --git a/deeplabcut/core/trackingutils.py b/deeplabcut/core/trackingutils.py
index 5dff9804d..565582dcd 100644
--- a/deeplabcut/core/trackingutils.py
+++ b/deeplabcut/core/trackingutils.py
@@ -766,11 +766,11 @@ def fill_tracklets(tracklets, trackers, animals, imname):
         if tracklet_id not in tracklets:
             tracklets[tracklet_id] = {}
         if pred_id != -1:
-            tracklets[tracklet_id][imname] = animals[pred_id]
+            tracklets[tracklet_id][imname] = np.asarray(animals[pred_id])
         else:  # Resort to the tracker prediction
             xy = np.asarray(content[:-2])
             pred = np.insert(xy, range(2, len(xy) + 1, 2), 1)
-            tracklets[tracklet_id][imname] = pred
+            tracklets[tracklet_id][imname] = np.asarray(pred)
 
 
 def calc_bboxes_from_keypoints(data, slack=0, offset=0):
diff --git a/deeplabcut/core/weight_init.py b/deeplabcut/core/weight_init.py
index d671f0945..8a6d374e8 100644
--- a/deeplabcut/core/weight_init.py
+++ b/deeplabcut/core/weight_init.py
@@ -11,63 +11,46 @@
 """Classes to configure how to initialize model weights"""
 from __future__ import annotations
 
+import warnings
 from dataclasses import dataclass
+from pathlib import Path
 
 import numpy as np
 
-import deeplabcut.modelzoo.utils as modelzoo_utils
-
 
 @dataclass
 class WeightInitialization:
-    """The dataset from which to initialize weights
-
-    To build a WeightInitialization instance for a project using the conversion table
-    specified in the project configuration file, use:
-
-        ```
-        from pathlib import Path
-        from deeplabcut.utils.auxiliaryfunctions import read_config
-
-        project_cfg = read_config("/path/to/my/project/config.yaml")
-        super_animal = "superanimal_quadruped"
-        weight_init = WeightInitialization.build(
-            cfg=project_cfg,
-            super_animal="superanimal_quadruped",
-            with_decoder=True,
-            memory_replay=True,
-        )
-        ```
+    """Configures weights initialization when transfer learning or fine-tuning models
 
     Args:
-        dataset: The dataset on which the model weights were trained. Must be one of the
-            SuperAnimal weights.
+        snapshot_path: The path to the snapshot used to initialize pose model weights
+            when training a model.
+        detector_snapshot_path: The path to the snapshot used to initialize detector
+            weights when training a model.
+        dataset: Optionally, the dataset on which the snapshots were trained. Required
+            when fine-tuning SuperAnimal models.
         with_decoder: Whether to load the decoder weights as well.
         memory_replay: Only when ``with_decoder=True``. Whether to train the model with
-            memory replay, so that it predicts all SuperAnimal bodyparts.
-        conversion_array: The mapping from SuperAnimal to project bodyparts. Required
-            when `with_decoder=True`.
+            memory replay, so that it predicts all SuperAnimal (or previous project)
+            bodyparts.
+        conversion_array: The mapping from SuperAnimal (or other project, on which the
+            weights were trained) to project bodyparts. Required when
+            `with_decoder=True`.
             An array [7, 0, 1] means the project has 3 bodyparts, where the 1st bodypart
             corresponds to the 8th bodypart in the pretrained model, the 2nd to the 1st
             and the 3rd to the 2nd (as arrays are 0-indexed).
         bodyparts: Optionally, the name of each bodypart entry in the conversion array.
-        customized_pose_checkpoint: A customized SuperAnimal pose checkpoint, as an
-            alternative to the Hugging Face one
-        customized_detector_checkpoint: A customized SuperAnimal detector
-            checkpoint, as an alternative to the Hugging Face one
     """
 
-    dataset: str
+    snapshot_path: Path
+    detector_snapshot_path: Path | None = None
+    dataset: str | None = None
     with_decoder: bool = False
     memory_replay: bool = False
     conversion_array: np.ndarray | None = None
     bodyparts: list[str] | None = None
-    customized_pose_checkpoint: str | None = None
-    customized_detector_checkpoint: str | None = None
 
     def __post_init__(self):
-        # check that the dataset exists; raises a ValueError if it doesn't
-        _ = modelzoo_utils.get_super_animal_project_cfg(self.dataset)
         if self.memory_replay and not self.with_decoder:
             raise ValueError(
                 "You cannot train a model with memory replay if you do not keep the "
@@ -97,41 +80,78 @@ def __post_init__(self):
 
     def to_dict(self) -> dict:
         """Returns: the weight initialization as a dict"""
-        data = {
-            "dataset": self.dataset,
-            "with_decoder": self.with_decoder,
-            "memory_replay": self.memory_replay,
-        }
+        data = dict()
+        if self.dataset is not None:
+            data["dataset"] = self.dataset
+
+        data["snapshot_path"] = str(self.snapshot_path)
+        if self.detector_snapshot_path is not None:
+            data["detector_snapshot_path"] = str(self.detector_snapshot_path)
+
+        data["with_decoder"] = self.with_decoder
+        data["memory_replay"] = self.memory_replay
 
         if self.conversion_array is not None:
             data["conversion_array"] = self.conversion_array.tolist()
-        if self.customized_pose_checkpoint is not None:
-            data["customized_pose_checkpoint"] = self.customized_pose_checkpoint
-        if self.customized_detector_checkpoint is not None:
-            data["customized_detector_checkpoint"] = self.customized_detector_checkpoint
+
+        if self.bodyparts is not None:
+            data["bodyparts"] = self.bodyparts
 
         return data
 
     @staticmethod
     def from_dict(data: dict) -> "WeightInitialization":
+        if "snapshot_path" not in data:
+            return WeightInitialization.from_dict_legacy(data)
+
+        detector_snapshot_path = data.get("detector_snapshot_path")
+        if detector_snapshot_path is not None:
+            detector_snapshot_path = Path(detector_snapshot_path)
+
         conversion_array = data.get("conversion_array")
         if conversion_array is not None:
+            conversion_array = np.array(conversion_array, dtype=int)
 
+        return WeightInitialization(
+            snapshot_path=Path(data["snapshot_path"]),
+            detector_snapshot_path=detector_snapshot_path,
+            dataset=data.get("dataset"),
+            with_decoder=data["with_decoder"],
+            memory_replay=data["memory_replay"],
+            conversion_array=conversion_array,
+            bodyparts=data.get("bodyparts"),
+        )
+
+    @staticmethod
+    def from_dict_legacy(data: dict) -> "WeightInitialization":
+        """Deals with weight initialization that were created before 3.0.0rc5"""
+        import deeplabcut.pose_estimation_pytorch.modelzoo.utils as utils
+
+        conversion_array = data.get("conversion_array")
+        if conversion_array is not None:
             conversion_array = np.array(conversion_array, dtype=int)
 
         return WeightInitialization(
-            dataset=data["dataset"],
+            snapshot_path=utils.get_super_animal_snapshot_path(
+                dataset=data["dataset"],
+                model_name="hrnet_w32",
+            ),
+            detector_snapshot_path=utils.get_super_animal_snapshot_path(
+                dataset=data["dataset"],
+                model_name="fasterrcnn_resnet50_fpn_v2",
+            ),
             with_decoder=data["with_decoder"],
             memory_replay=data["memory_replay"],
             conversion_array=conversion_array,
-            customized_pose_checkpoint=data.get("customized_pose_checkpoint"),
-            customized_detector_checkpoint=data.get("customized_detector_checkpoint"),
+            bodyparts=data.get("bodyparts"),
         )
 
     @staticmethod
     def build(
         cfg: dict,
         super_animal: str,
+        model_name: str = "hrnet_w32",
+        detector_name: str = "fasterrcnn_resnet50_fpn_v2",
         with_decoder: bool = False,
         memory_replay: bool = False,
         customized_pose_checkpoint: str | None = None,
@@ -139,9 +159,18 @@ def build(
     ) -> "WeightInitialization":
         """Builds a WeightInitialization for a project
 
+        `WeightInitialization.build` is deprecated and will be removed in a future
+        version of DeepLabCut. Please use `build_weight_init` from `deeplabcut.modelzoo`
+        instead.
+
         Args:
             cfg: The project's configuration.
             super_animal: The SuperAnimal model with which to initialize weights.
+            model_name: The name of the model architecture for which to load the weights
+                (defaults to "hrnet_w32" for backwards compatibility).
+            detector_name: The name of the detector architecture for which to load the
+                weights (defaults to "fasterrcnn_resnet50_fpn_v2" for backwards
+                compatibility).
             with_decoder: Whether to load the decoder weights as well. If this is true,
                 a conversion table must be specified for the given SuperAnimal in the
                 project configuration file. See
@@ -157,19 +186,21 @@ def build(
         Returns:
             The built WeightInitialization.
         """
-        conversion_array = None
-        bodyparts = None
-        if with_decoder:
-            conversion_table = modelzoo_utils.get_conversion_table(cfg, super_animal)
-            conversion_array = conversion_table.to_array()
-            bodyparts = conversion_table.converted_bodyparts()
-
-        return WeightInitialization(
-            dataset=super_animal,
-            with_decoder=with_decoder,
-            memory_replay=memory_replay,
-            conversion_array=conversion_array,
-            bodyparts=bodyparts,
-            customized_pose_checkpoint=customized_pose_checkpoint,
-            customized_detector_checkpoint=customized_detector_checkpoint,
+        from deeplabcut.modelzoo import build_weight_init
+        deprecation_warning = (
+            "The `WeightInitialization.build` is deprecated and will be removed in a "
+            "future version of DeepLabCut. Please use `build_weight_init` from "
+            "`deeplabcut.modelzoo` instead."
+        )
+        warnings.warn(deprecation_warning, DeprecationWarning)
+
+        return build_weight_init(
+            cfg,
+            super_animal,
+            model_name,
+            detector_name,
+            with_decoder,
+            memory_replay,
+            customized_pose_checkpoint,
+            customized_detector_checkpoint,
         )
diff --git a/deeplabcut/generate_training_dataset/multiple_individuals_trainingsetmanipulation.py b/deeplabcut/generate_training_dataset/multiple_individuals_trainingsetmanipulation.py
index 8340b719f..1095dfef6 100755
--- a/deeplabcut/generate_training_dataset/multiple_individuals_trainingsetmanipulation.py
+++ b/deeplabcut/generate_training_dataset/multiple_individuals_trainingsetmanipulation.py
@@ -293,10 +293,7 @@ def create_multianimaltraining_dataset(
     multi_stage = False
     ### dlcnet_ms5: backbone resnet50 + multi-fusion & multi-stage module
     ### dlcr101_ms5/dlcr152_ms5: backbone resnet101/152 + multi-fusion & multi-stage module
-    if (
-        all(net in net_type for net in ("dlcr", "_ms5"))
-        and engine != Engine.PYTORCH
-    ):
+    if all(net in net_type for net in ("dlcr", "_ms5")) and engine != Engine.PYTORCH:
         num_layers = re.findall("dlcr([0-9]*)", net_type)[0]
         if num_layers == "":
             num_layers = 50
@@ -394,7 +391,7 @@ def create_multianimaltraining_dataset(
     top_down = False
     if engine == Engine.PYTORCH and net_type.startswith("top_down_"):
         top_down = True
-        net_type = net_type[len("top_down_"):]
+        net_type = net_type[len("top_down_") :]
 
     for trainFraction, shuffle, (trainIndices, testIndices) in splits:
         ####################################################
@@ -455,7 +452,10 @@ def create_multianimaltraining_dataset(
             #################################################################################
 
             modelfoldername = auxiliaryfunctions.get_model_folder(
-                trainFraction, shuffle, cfg, engine=engine,
+                trainFraction,
+                shuffle,
+                cfg,
+                engine=engine,
             )
             auxiliaryfunctions.attempt_to_make_folder(
                 Path(config).parents[0] / modelfoldername, recursive=True
@@ -492,118 +492,126 @@ def create_multianimaltraining_dataset(
                 )
             )
 
-            jointnames = [str(bpt) for bpt in multianimalbodyparts]
-            jointnames.extend([str(bpt) for bpt in uniquebodyparts])
-            items2change = {
-                "dataset": datafilename,
-                "engine": engine.aliases[0],
-                "metadataset": metadatafilename,
-                "num_joints": len(multianimalbodyparts)
-                + len(uniquebodyparts),  # cfg["uniquebodyparts"]),
-                "all_joints": [
-                    [i] for i in range(len(multianimalbodyparts) + len(uniquebodyparts))
-                ],  # cfg["uniquebodyparts"]))],
-                "all_joints_names": jointnames,
-                "init_weights": str(model_path),
-                "project_path": str(cfg["project_path"]),
-                "net_type": net_type,
-                "multi_stage": multi_stage,
-                "pairwise_loss_weight": 0.1,
-                "pafwidth": 20,
-                "partaffinityfield_graph": partaffinityfield_graph,
-                "partaffinityfield_predict": partaffinityfield_predict,
-                "weigh_only_present_joints": False,
-                "num_limbs": len(partaffinityfield_graph),
-                "dataset_type": dataset_type,
-                "optimizer": "adam",
-                "batch_size": 8,
-                "multi_step": [[1e-4, 7500], [5 * 1e-5, 12000], [1e-5, 200000]],
-                "save_iters": 10000,
-                "display_iters": 500,
-                "num_idchannel": (
-                    len(cfg["individuals"]) if cfg.get("identity", False) else 0
-                ),
-                "crop_size": list(crop_size),
-                "crop_sampling": crop_sampling,
-            }
-
-            trainingdata = MakeTrain_pose_yaml(
-                items2change, path_train_config, defaultconfigfile
-            )
-            keys2save = [
-                "dataset",
-                "num_joints",
-                "all_joints",
-                "all_joints_names",
-                "net_type",
-                "multi_stage",
-                "init_weights",
-                "global_scale",
-                "location_refinement",
-                "locref_stdev",
-                "dataset_type",
-                "partaffinityfield_predict",
-                "pairwise_predict",
-                "partaffinityfield_graph",
-                "num_limbs",
-                "dataset_type",
-                "num_idchannel",
-            ]
-
-            MakeTest_pose_yaml(
-                trainingdata,
-                keys2save,
-                path_test_config,
-                nmsradius=5.0,
-                minconfidence=0.01,
-                sigma=1,
-                locref_smooth=False,
-            )  # setting important def. values for inference
-
-            # Setting inference cfg file:
-            defaultinference_configfile = os.path.join(
-                dlcparent_path, "inference_cfg.yaml"
-            )
-            items2change = {
-                "minimalnumberofconnections": int(len(cfg["multianimalbodyparts"]) / 2),
-                "topktoretain": len(cfg["individuals"]),
-                "withid": cfg.get("identity", False),
-            }
-            MakeInference_yaml(
-                items2change, path_inference_config, defaultinference_configfile
-            )
-
-            # Populate the pytorch config yaml file
-            if engine == Engine.PYTORCH:
-                from deeplabcut.pose_estimation_pytorch.config.make_pose_config import make_pytorch_pose_config
-                from deeplabcut.pose_estimation_pytorch.modelzoo.config import make_super_animal_finetune_config
+            if engine == Engine.TF:
+                jointnames = [str(bpt) for bpt in multianimalbodyparts]
+                jointnames.extend([str(bpt) for bpt in uniquebodyparts])
+                items2change = {
+                    "dataset": datafilename,
+                    "engine": engine.aliases[0],
+                    "metadataset": metadatafilename,
+                    "num_joints": len(multianimalbodyparts)
+                    + len(uniquebodyparts),  # cfg["uniquebodyparts"]),
+                    "all_joints": [
+                        [i]
+                        for i in range(len(multianimalbodyparts) + len(uniquebodyparts))
+                    ],  # cfg["uniquebodyparts"]))],
+                    "all_joints_names": jointnames,
+                    "init_weights": str(model_path),
+                    "project_path": str(cfg["project_path"]),
+                    "net_type": net_type,
+                    "multi_stage": multi_stage,
+                    "pairwise_loss_weight": 0.1,
+                    "pafwidth": 20,
+                    "partaffinityfield_graph": partaffinityfield_graph,
+                    "partaffinityfield_predict": partaffinityfield_predict,
+                    "weigh_only_present_joints": False,
+                    "num_limbs": len(partaffinityfield_graph),
+                    "dataset_type": dataset_type,
+                    "optimizer": "adam",
+                    "batch_size": 8,
+                    "multi_step": [[1e-4, 7500], [5 * 1e-5, 12000], [1e-5, 200000]],
+                    "save_iters": 10000,
+                    "display_iters": 500,
+                    "num_idchannel": (
+                        len(cfg["individuals"]) if cfg.get("identity", False) else 0
+                    ),
+                    "crop_size": list(crop_size),
+                    "crop_sampling": crop_sampling,
+                }
+
+                trainingdata = MakeTrain_pose_yaml(
+                    items2change,
+                    path_train_config,
+                    defaultconfigfile,
+                    save=(engine == Engine.TF),
+                )
+                keys2save = [
+                    "dataset",
+                    "num_joints",
+                    "all_joints",
+                    "all_joints_names",
+                    "net_type",
+                    "multi_stage",
+                    "init_weights",
+                    "global_scale",
+                    "location_refinement",
+                    "locref_stdev",
+                    "dataset_type",
+                    "partaffinityfield_predict",
+                    "pairwise_predict",
+                    "partaffinityfield_graph",
+                    "num_limbs",
+                    "dataset_type",
+                    "num_idchannel",
+                ]
+
+                MakeTest_pose_yaml(
+                    trainingdata,
+                    keys2save,
+                    path_test_config,
+                    nmsradius=5.0,
+                    minconfidence=0.01,
+                    sigma=1,
+                    locref_smooth=False,
+                )  # setting important def. values for inference
+            elif engine == Engine.PYTORCH:
+                from deeplabcut.pose_estimation_pytorch.config.make_pose_config import (
+                    make_pytorch_pose_config,
+                    make_pytorch_test_config,
+                )
+                from deeplabcut.pose_estimation_pytorch.modelzoo.config import (
+                    make_super_animal_finetune_config,
+                )
 
                 # backwards compatibility with version 2.X
                 if net_type == "dlcrnet_ms5":
                     net_type = "dlcrnet_stride16_ms5"
 
-                pose_cfg_path = path_train_config.replace("pose_cfg.yaml", "pytorch_config.yaml")
+                config_path = Path(path_train_config).with_name(engine.pose_cfg_name)
                 if weight_init is not None and weight_init.with_decoder:
                     pytorch_cfg = make_super_animal_finetune_config(
                         project_config=cfg,
-                        pose_config_path=path_train_config,
-                        net_type=net_type,
+                        pose_config_path=config_path,
+                        model_name=net_type,
+                        detector_name=detector_type,
                         weight_init=weight_init,
+                        save=True,
                     )
                 else:
                     pytorch_cfg = make_pytorch_pose_config(
                         project_config=cfg,
-                        pose_config_path=path_train_config,
+                        pose_config_path=config_path,
                         net_type=net_type,
                         top_down=top_down,
                         detector_type=detector_type,
                         weight_init=weight_init,
+                        save=True,
                     )
 
-                auxiliaryfunctions.write_plainconfig(pose_cfg_path, pytorch_cfg)
+                make_pytorch_test_config(pytorch_cfg, path_test_config, save=True)
+
+            # Setting inference cfg file:
+            default_inf_path = Path(dlcparent_path) / "inference_cfg.yaml"
+            inf_updates = dict(
+                minimalnumberofconnections=int(len(cfg["multianimalbodyparts"]) / 2),
+                topktoretain=len(cfg["individuals"]),
+                withid=cfg.get("identity", False),
+            )
+            MakeInference_yaml(inf_updates, path_inference_config, default_inf_path)
 
             print(
-                "The training dataset is successfully created. Use the function 'train_network' to start training. Happy training!"
+                "The training dataset is successfully created. Use the function "
+                "'train_network' to start training. Happy training!"
             )
         else:
             pass
diff --git a/deeplabcut/generate_training_dataset/trainingsetmanipulation.py b/deeplabcut/generate_training_dataset/trainingsetmanipulation.py
index 31d9a4455..1cf154c8a 100755
--- a/deeplabcut/generate_training_dataset/trainingsetmanipulation.py
+++ b/deeplabcut/generate_training_dataset/trainingsetmanipulation.py
@@ -398,19 +398,26 @@ def ParseYaml(configfile):
 
 
 def MakeTrain_pose_yaml(
-    itemstochange, saveasconfigfile, defaultconfigfile, items2drop={}
+    itemstochange,
+    saveasconfigfile,
+    defaultconfigfile,
+    items2drop: dict | None = None,
+    save: bool = True,
 ):
+    if items2drop is None:
+        items2drop = {}
+
     docs = ParseYaml(defaultconfigfile)
     for key in items2drop.keys():
-        # print(key, "dropping?")
         if key in docs[0].keys():
             docs[0].pop(key)
 
     for key in itemstochange.keys():
         docs[0][key] = itemstochange[key]
 
-    with open(saveasconfigfile, "w") as f:
-        yaml.dump(docs[0], f)
+    if save:
+        with open(saveasconfigfile, "w") as f:
+            yaml.dump(docs[0], f)
 
     return docs[0]
 
@@ -1205,7 +1212,7 @@ def create_training_dataset(
                         cfg["project_path"],
                         Path(modelfoldername),
                         "train",
-                        "pose_cfg.yaml",
+                        engine.pose_cfg_name,
                     )
                 )
                 path_test_config = str(
@@ -1216,67 +1223,75 @@ def create_training_dataset(
                         "pose_cfg.yaml",
                     )
                 )
-                # str(cfg['proj_path']+'/'+Path(modelfoldername) / 'test'  /  'pose_cfg.yaml')
-                items2change = {
-                    "dataset": datafilename,
-                    "engine": engine.aliases[0],
-                    "metadataset": metadatafilename,
-                    "num_joints": len(bodyparts),
-                    "all_joints": [[i] for i in range(len(bodyparts))],
-                    "all_joints_names": [str(bpt) for bpt in bodyparts],
-                    "init_weights": model_path,
-                    "project_path": str(cfg["project_path"]),
-                    "net_type": net_type,
-                    "dataset_type": augmenter_type,
-                }
-
-                items2drop = {}
-                if augmenter_type == "scalecrop":
-                    # these values are dropped as scalecrop
-                    # doesn't have rotation implemented
-                    items2drop = {"rotation": 0, "rotratio": 0.0}
-                # Also drop maDLC smart cropping augmentation parameters
-                for key in ["pre_resize", "crop_size", "max_shift", "crop_sampling"]:
-                    items2drop[key] = None
-
-                trainingdata = MakeTrain_pose_yaml(
-                    items2change, path_train_config, defaultconfigfile, items2drop
-                )
-
-                keys2save = [
-                    "dataset",
-                    "num_joints",
-                    "all_joints",
-                    "all_joints_names",
-                    "net_type",
-                    "init_weights",
-                    "global_scale",
-                    "location_refinement",
-                    "locref_stdev",
-                ]
-                MakeTest_pose_yaml(trainingdata, keys2save, path_test_config)
-                print(
-                    "The training dataset is successfully created. Use the function 'train_network' to start training. Happy training!"
-                )
+                if engine == Engine.TF:
+                    items2change = {
+                        "dataset": datafilename,
+                        "engine": engine.aliases[0],
+                        "metadataset": metadatafilename,
+                        "num_joints": len(bodyparts),
+                        "all_joints": [[i] for i in range(len(bodyparts))],
+                        "all_joints_names": [str(bpt) for bpt in bodyparts],
+                        "init_weights": model_path,
+                        "project_path": str(cfg["project_path"]),
+                        "net_type": net_type,
+                        "dataset_type": augmenter_type,
+                    }
+
+                    items2drop = {}
+                    if augmenter_type == "scalecrop":
+                        # these values are dropped as scalecrop
+                        # doesn't have rotation implemented
+                        items2drop = {"rotation": 0, "rotratio": 0.0}
+                    # Also drop maDLC smart cropping augmentation parameters
+                    for key in [
+                        "pre_resize",
+                        "crop_size",
+                        "max_shift",
+                        "crop_sampling",
+                    ]:
+                        items2drop[key] = None
+
+                    trainingdata = MakeTrain_pose_yaml(
+                        items2change,
+                        path_train_config,
+                        defaultconfigfile,
+                        items2drop,
+                        save=(engine == Engine.TF),
+                    )
 
-                # Populate the pytorch config yaml file
-                if engine == Engine.PYTORCH:
+                    keys2save = [
+                        "dataset",
+                        "num_joints",
+                        "all_joints",
+                        "all_joints_names",
+                        "net_type",
+                        "init_weights",
+                        "global_scale",
+                        "location_refinement",
+                        "locref_stdev",
+                    ]
+                    MakeTest_pose_yaml(trainingdata, keys2save, path_test_config)
+                    print(
+                        "The training dataset is successfully created. Use the function"
+                        "'train_network' to start training. Happy training!"
+                    )
+                elif engine == Engine.PYTORCH:
                     from deeplabcut.pose_estimation_pytorch.config.make_pose_config import (
                         make_pytorch_pose_config,
+                        make_pytorch_test_config,
                     )
                     from deeplabcut.pose_estimation_pytorch.modelzoo.config import (
                         make_super_animal_finetune_config,
                     )
 
-                    pose_cfg_path = path_train_config.replace(
-                        "pose_cfg.yaml", "pytorch_config.yaml"
-                    )
                     if weight_init is not None and weight_init.with_decoder:
                         pytorch_cfg = make_super_animal_finetune_config(
                             project_config=cfg,
                             pose_config_path=path_train_config,
-                            net_type=net_type,
+                            model_name=net_type,
+                            detector_name=detector_type,
                             weight_init=weight_init,
+                            save=True,
                         )
                     else:
                         pytorch_cfg = make_pytorch_pose_config(
@@ -1286,9 +1301,10 @@ def create_training_dataset(
                             top_down=top_down,
                             detector_type=detector_type,
                             weight_init=weight_init,
+                            save=True,
                         )
 
-                    auxiliaryfunctions.write_plainconfig(pose_cfg_path, pytorch_cfg)
+                    make_pytorch_test_config(pytorch_cfg, path_test_config, save=True)
 
         return splits
 
diff --git a/deeplabcut/gui/displays/selected_shuffle_display.py b/deeplabcut/gui/displays/selected_shuffle_display.py
index fd370a76c..13e6db109 100644
--- a/deeplabcut/gui/displays/selected_shuffle_display.py
+++ b/deeplabcut/gui/displays/selected_shuffle_display.py
@@ -85,7 +85,9 @@ def _update_display(self, new_index: int) -> None:
             return
 
         if not pose_cfg_path.exists():
-            self._set_text_error()
+            self._set_text_error(
+                f"The model configuration file {pose_cfg_path} was not created"
+            )
             return
 
         self._read_pose_config(pose_cfg_path)
diff --git a/deeplabcut/gui/tabs/create_training_dataset.py b/deeplabcut/gui/tabs/create_training_dataset.py
index 7bed81a8f..15c06418c 100644
--- a/deeplabcut/gui/tabs/create_training_dataset.py
+++ b/deeplabcut/gui/tabs/create_training_dataset.py
@@ -13,6 +13,7 @@
 import os
 from pathlib import Path
 
+import dlclibrary
 from PySide6 import QtWidgets
 from PySide6.QtCore import Qt, Slot
 from PySide6.QtGui import QIcon
@@ -32,6 +33,7 @@
     _create_label_widget,
 )
 from deeplabcut.gui.widgets import launch_napari
+from deeplabcut.modelzoo import build_weight_init
 from deeplabcut.utils.auxiliaryfunctions import (
     get_data_and_metadata_filenames,
     get_training_set_folder,
@@ -70,10 +72,14 @@ def __init__(self, root, parent, h1_description):
         self.main_layout.addWidget(self.help_button, alignment=Qt.AlignLeft)
 
     def set_edit_table_visibility(self) -> None:
-        has_conversion_tables = bool(self.root.cfg.get("SuperAnimalConversionTables", {}))
+        has_conversion_tables = bool(
+            self.root.cfg.get("SuperAnimalConversionTables", {})
+        )
         is_pytorch_engine = self.root.engine == Engine.PYTORCH
         is_finetuning = self.weight_init_selector.with_decoder
-        self.mapping_button.setVisible(has_conversion_tables & is_pytorch_engine & is_finetuning)
+        self.mapping_button.setVisible(
+            has_conversion_tables & is_pytorch_engine & is_finetuning
+        )
 
     def show_help_dialog(self):
         dialog = QtWidgets.QDialog(self)
@@ -179,7 +185,6 @@ def log_augmentation_choice(self, augmentation):
 
     def edit_conversion_table(self):
         # Test beforehand whether a conversion table exists
-        weight_init = self.weight_init_selector.get_weight_init()
         memory_replay_folder = Path(self.root.project_folder) / "memory_replay"
         conversion_matrix_out_path = str(memory_replay_folder / "confusion_matrix.png")
         files = [self.root.config]
@@ -213,12 +218,6 @@ def create_training_dataset(self):
                 self.root.writer.write("Training dataset creation failed.")
                 return
 
-        try:
-            weight_init = self.weight_init_selector.get_weight_init()
-        except ValueError as err:
-            print(f"The training dataset could not be created: {err}.")
-            return
-
         if self.model_comparison:
             raise NotImplementedError
             # TODO: finish model_comparison
@@ -235,11 +234,23 @@ def create_training_dataset(self):
                 detector_type = None
                 if engine == Engine.TF:
                     import tensorflow
+
                     # try importing TF so they can't create shuffles for it if they
                     # don't have it installed
                 elif engine == Engine.PYTORCH and "top_down" in net_type:
                     detector_type = self.detector_choice.currentText()
 
+                try:
+                    weight_init = (
+                        self.weight_init_selector.get_super_animal_weight_init(
+                            net_type,
+                            detector_type,
+                        )
+                    )
+                except ValueError as err:
+                    print(f"The training dataset could not be created: {err}.")
+                    return
+
                 if self.data_split_selection.selected:
                     deeplabcut.create_training_dataset_from_existing_split(
                         self.root.config,
@@ -427,6 +438,7 @@ def update_detectors(
         else:
             # FIXME: Circular imports make it impossible to import this at the top
             from deeplabcut.pose_estimation_pytorch import available_detectors
+
             detectors = available_detectors()
             det_filter = self.get_detector_filter()
             if det_filter is not None:
@@ -436,7 +448,10 @@ def update_detectors(
             self.detector_choice.removeItem(0)
 
         self.detector_choice.addItems(detectors)
-        if "ssdlite" in detectors:
+        default_detector = self.get_default_detector()
+        if default_detector in detectors:
+            self.detector_choice.setCurrentIndex(detectors.index(default_detector))
+        elif "ssdlite" in detectors:
             self.detector_choice.setCurrentIndex(detectors.index("ssdlite"))
 
         if net_choice is None:
@@ -478,7 +493,10 @@ def get_net_filter(self) -> list[str] | None:
             return None
 
         weight_init_cfg = _WEIGHT_INIT_OPTIONS[self.weight_init_selector.weight_init]
-        return weight_init_cfg["model_filter"]
+        if "super_animal" in weight_init_cfg:
+            return dlclibrary.get_available_models(weight_init_cfg["super_animal"])
+
+        return None
 
     def get_detector_filter(self) -> list[str] | None:
         """Returns: the detectors that can be used based on weight initialization"""
@@ -489,7 +507,10 @@ def get_detector_filter(self) -> list[str] | None:
             return None
 
         weight_init_cfg = _WEIGHT_INIT_OPTIONS[self.weight_init_selector.weight_init]
-        return weight_init_cfg["detector_filter"]
+        if "super_animal" in weight_init_cfg:
+            return dlclibrary.get_available_detectors(weight_init_cfg["super_animal"])
+
+        return None
 
     def get_default_net(self) -> str | None:
         """Returns: the net type that can be used based on weight initialization"""
@@ -502,6 +523,17 @@ def get_default_net(self) -> str | None:
         weight_init_cfg = _WEIGHT_INIT_OPTIONS[self.weight_init_selector.weight_init]
         return weight_init_cfg.get("default_net")
 
+    def get_default_detector(self) -> str | None:
+        """Returns: the detector type that can be used based on weight initialization"""
+        if self.root.engine != Engine.PYTORCH:
+            return None
+
+        if self.weight_init_selector.weight_init not in _WEIGHT_INIT_OPTIONS:
+            return None
+
+        weight_init_cfg = _WEIGHT_INIT_OPTIONS[self.weight_init_selector.weight_init]
+        return weight_init_cfg.get("default_detector")
+
     def view_shuffles(self) -> None:
         viewer = ShuffleMetadataViewer(root=self.root, parent=self)
         viewer.show()
@@ -551,8 +583,18 @@ def update_choices(self, choices: list[str]) -> None:
             self.weight_init_choice.removeItem(0)
         self.weight_init_choice.addItems(choices)
 
-    def get_weight_init(self) -> WeightInitialization | None:
+    def get_super_animal_weight_init(
+        self,
+        net_type: str,
+        detector_type: str,
+    ) -> WeightInitialization | None:
         """
+        Args:
+            net_type: The architecture of the pose model from which to fine-tune a
+                SuperAnimal model.
+            detector_type: The architecture of the detector from which to fine-tune a
+                SuperAnimal model.
+
         Raises:
             ValueError if WeightInitialization should be defined but could not be
                 created (e.g. if there's no conversion table).
@@ -566,10 +608,14 @@ def get_weight_init(self) -> WeightInitialization | None:
 
         weight_init_data = _WEIGHT_INIT_OPTIONS[weight_init_choice]
         super_animal = weight_init_data["super_animal"]
+        if net_type.startswith("top_down_"):
+            net_type = net_type[len("top_down_") :]
         try:
-            weight_init = WeightInitialization.build(
+            weight_init = build_weight_init(
                 self.root.cfg,
                 super_animal=super_animal,
+                model_name=net_type,
+                detector_name=detector_type,
                 with_decoder=self.with_decoder,
                 memory_replay=self.memory_replay,
             )
@@ -695,34 +741,34 @@ def _create_confirmation_box(title, description):
         "model_filter": None,
         "detector_filter": None,
     },
+    "Transfer Learning - SuperAnimal Bird": {
+        "default_net": "top_down_resnet_50",
+        "default_detector": "fasterrcnn_mobilenet_v3_large_fpn",
+        "super_animal": "superanimal_bird",
+    },
     "Transfer Learning - SuperAnimal Quadruped": {
         "default_net": "top_down_hrnet_w32",
-        "model_filter": [
-            "dekr_w32",
-            "hrnet_w32",
-        ],
-        "detector_filter": ["fasterrcnn_resnet50_fpn_v2"],
+        "default_detector": "fasterrcnn_mobilenet_v3_large_fpn",
         "super_animal": "superanimal_quadruped",
     },
     "Transfer Learning - SuperAnimal TopViewMouse": {
         "default_net": "top_down_hrnet_w32",
-        "model_filter": [
-            "dekr_w32",
-            "hrnet_w32",
-        ],
-        "detector_filter": ["fasterrcnn_resnet50_fpn_v2"],
+        "default_detector": "fasterrcnn_mobilenet_v3_large_fpn",
         "super_animal": "superanimal_topviewmouse",
     },
+    "Fine-tuning - SuperAnimal Bird": {
+        "default_net": "top_down_resnet_50",
+        "default_detector": "fasterrcnn_mobilenet_v3_large_fpn",
+        "super_animal": "superanimal_bird",
+    },
     "Fine-tuning - SuperAnimal Quadruped": {
         "default_net": "top_down_hrnet_w32",
-        "model_filter": ["hrnet_w32"],  # FIXME - Add ResNet
-        "detector_filter": ["fasterrcnn_resnet50_fpn_v2"],
+        "default_detector": "fasterrcnn_mobilenet_v3_large_fpn",
         "super_animal": "superanimal_quadruped",
     },
     "Fine-tuning - SuperAnimal TopViewMouse": {
         "default_net": "top_down_hrnet_w32",
-        "model_filter": ["hrnet_w32"],  # FIXME - Add ResNet
-        "detector_filter": ["fasterrcnn_resnet50_fpn_v2"],
+        "default_detector": "fasterrcnn_mobilenet_v3_large_fpn",
         "super_animal": "superanimal_topviewmouse",
     },
 }
diff --git a/deeplabcut/gui/tabs/modelzoo.py b/deeplabcut/gui/tabs/modelzoo.py
index a47ba1878..13039517f 100644
--- a/deeplabcut/gui/tabs/modelzoo.py
+++ b/deeplabcut/gui/tabs/modelzoo.py
@@ -12,7 +12,7 @@
 import webbrowser
 from functools import partial
 
-from dlclibrary.dlcmodelzoo.modelzoo_download import MODELOPTIONS
+import dlclibrary
 from PySide6 import QtWidgets
 from PySide6.QtCore import QRegularExpression, Qt, QTimer, Signal, Slot
 from PySide6.QtGui import QIcon, QPixmap, QRegularExpressionValidator
@@ -28,7 +28,6 @@
 )
 from deeplabcut.gui.utils import move_to_separate_thread
 from deeplabcut.gui.widgets import ClickableLabel
-from dlclibrary.dlcmodelzoo.modelzoo_download import MODELOPTIONS
 
 
 class RegExpValidator(QRegularExpressionValidator):
@@ -47,6 +46,8 @@ def __init__(self, root, parent, h1_description):
         self._set_page()
         self.root.engine_change.connect(self._on_engine_change)
         self.root.engine_change.connect(self._update_available_models)
+        self._update_pose_models(self.model_combo.currentText())
+        self._update_detectors(self.model_combo.currentText())
         self._destfolder = None
 
     @property
@@ -94,9 +95,20 @@ def _build_common_attributes(self) -> None:
         self.model_combo = QtWidgets.QComboBox()
         self.model_combo.setMinimumWidth(250)
 
+        net_type_text = QtWidgets.QLabel("Net Type")
+        net_type_text.setMinimumWidth(300)
+        self.net_type_selector = QtWidgets.QComboBox()
+
+        self.detector_type_text = QtWidgets.QLabel("Detector Type")
+        self.detector_type_text.setMinimumWidth(300)
+        self.detector_type_selector = QtWidgets.QComboBox()
+
         loc_label = ClickableLabel("Folder to store results:", parent=self)
         loc_label.signal.connect(self.select_folder)
-        self.loc_line = QtWidgets.QLineEdit(self.root.project_folder, self)
+        self.loc_line = QtWidgets.QLineEdit(
+            "<Select a folder - Default: store in same folder as video>",
+            self,
+        )
         self.loc_line.setReadOnly(True)
         action = self.loc_line.addAction(
             QIcon(os.path.join(BASE_DIR, "assets", "icons", "open2.png")),
@@ -107,12 +119,19 @@ def _build_common_attributes(self) -> None:
         settings_layout.addWidget(section_title, 0, 0)
         settings_layout.addWidget(model_combo_text, 1, 0)
         settings_layout.addWidget(self.model_combo, 1, 1)
-        settings_layout.addWidget(loc_label, 2, 0)
-        settings_layout.addWidget(self.loc_line, 2, 1)
+        settings_layout.addWidget(net_type_text, 2, 0)
+        settings_layout.addWidget(self.net_type_selector, 2, 1)
+        settings_layout.addWidget(self.detector_type_text, 3, 0)
+        settings_layout.addWidget(self.detector_type_selector, 3, 1)
+
+        settings_layout.addWidget(loc_label, 4, 0)
+        settings_layout.addWidget(self.loc_line, 4, 1)
 
         self.settings_widget = QtWidgets.QWidget()
         self.settings_widget.setLayout(settings_layout)
         self.main_layout.addWidget(self.settings_widget)
+        self.model_combo.currentTextChanged.connect(self._update_pose_models)
+        self.model_combo.currentTextChanged.connect(self._update_detectors)
 
     def _build_tf_attributes(self) -> None:
         model_settings_layout = _create_grid_layout(margins=(20, 0, 0, 0))
@@ -302,7 +321,7 @@ def signal_analysis_complete(self):
         msg.exec_()
 
     def _gather_kwargs(self) -> dict:
-        kwargs = {}
+        kwargs = dict(model_name=self.net_type_selector.currentText())
         if self.root.engine == Engine.TF:
             scales = []
             scales_ = self.scales_line.text()
@@ -317,6 +336,7 @@ def _gather_kwargs(self) -> dict:
             kwargs["pseudo_threshold"] = self.pseudo_threshold_spinbox.value()
             kwargs["adapt_iterations"] = self.adapt_iter_spinbox.value()
         else:
+            kwargs["detector_name"] = self.detector_type_selector.currentText()
             kwargs["video_adapt"] = self.torch_adapt_checkbox.isChecked()
             kwargs["pseudo_threshold"] = self.torch_pseudo_threshold_spinbox.value()
             kwargs["detector_epochs"] = self.torch_adapt_det_epoch_spinbox.value()
@@ -325,29 +345,58 @@ def _gather_kwargs(self) -> dict:
         return kwargs
 
     def _update_available_models(self, engine: Engine) -> None:
+        current_dataset = self.model_combo.currentText()
+
         while self.model_combo.count() > 0:
             self.model_combo.removeItem(0)
 
-        supermodels = [
-            model
-            for model in MODELOPTIONS
-            if (
-                "superanimal" in model
-                and model not in ("superanimal_topviewmouse", "superanimal_quadruped")
-                and (
-                    (engine == Engine.TF and "dlcrnet" in model)
-                    or (engine == Engine.PYTORCH and "dlcrnet" not in model)
-                )
-            )
-        ]
+        if engine == Engine.TF:
+            supermodels = ["superanimal_topviewmouse", "superanimal_quadruped"]
+        else:
+            supermodels = dlclibrary.get_available_datasets()
+
         self.model_combo.addItems(supermodels)
+        if current_dataset in supermodels:
+            self.model_combo.setCurrentIndex(supermodels.index(current_dataset))
+
+    def _update_pose_models(self, super_animal: str) -> None:
+        while self.net_type_selector.count() > 0:
+            self.net_type_selector.removeItem(0)
+
+        if len(super_animal) == 0:
+            return
+
+        if self.root.engine == Engine.TF:
+            self.net_type_selector.addItems(["dlcrnet"])
+        else:
+            self.net_type_selector.addItems(
+                dlclibrary.get_available_models(super_animal)
+            )
+
+    def _update_detectors(self, super_animal: str) -> None:
+        while self.detector_type_selector.count() > 0:
+            self.detector_type_selector.removeItem(0)
+
+        if len(super_animal) == 0:
+            return
+
+        if self.root.engine == Engine.TF:
+            self.detector_type_selector.addItems(["dlcrnet"])
+        else:
+            self.detector_type_selector.addItems(
+                dlclibrary.get_available_detectors(super_animal)
+            )
 
     @Slot(Engine)
     def _on_engine_change(self, engine: Engine) -> None:
         self._update_available_models(engine)
         if engine == Engine.PYTORCH:
             self.tf_widget.hide()
+            self.detector_type_text.show()
+            self.detector_type_selector.show()
             self.torch_widget.show()
         else:
             self.torch_widget.hide()
+            self.detector_type_text.hide()
+            self.detector_type_selector.hide()
             self.tf_widget.show()
diff --git a/deeplabcut/modelzoo/__init__.py b/deeplabcut/modelzoo/__init__.py
index 117d12714..96c227b4b 100644
--- a/deeplabcut/modelzoo/__init__.py
+++ b/deeplabcut/modelzoo/__init__.py
@@ -8,3 +8,4 @@
 #
 # Licensed under GNU Lesser General Public License v3.0
 #
+from deeplabcut.modelzoo.weight_initialization import build_weight_init
diff --git a/deeplabcut/modelzoo/model_configs/fasterrcnn_mobilenet_v3_large_fpn.yaml b/deeplabcut/modelzoo/model_configs/fasterrcnn_mobilenet_v3_large_fpn.yaml
new file mode 100644
index 000000000..7155b88ba
--- /dev/null
+++ b/deeplabcut/modelzoo/model_configs/fasterrcnn_mobilenet_v3_large_fpn.yaml
@@ -0,0 +1,49 @@
+data:
+  colormode: RGB
+  inference:
+    normalize_images: true
+  train:
+    affine:
+      p: 0.5
+      rotation: 30
+      scaling: [ 1.0, 1.0 ]
+      translation: 40
+    collate:
+      type: ResizeFromDataSizeCollate
+      min_scale: 0.4
+      max_scale: 1.0
+      min_short_side: 128
+      max_short_side: 1152
+      multiple_of: 32
+      to_square: false
+    hflip: true
+    normalize_images: true
+device: auto
+model:
+  type: FasterRCNN
+  variant: fasterrcnn_mobilenet_v3_large_fpn
+  box_score_thresh: 0.6
+  freeze_bn_stats: true
+  freeze_bn_weights: false
+runner:
+  type: DetectorTrainingRunner
+  eval_interval: 10
+  optimizer:
+    type: AdamW
+    params:
+      lr: 1e-5
+  scheduler:
+    type: LRListScheduler
+    params:
+      milestones: [ 90 ]
+      lr_list: [ [ 1e-6 ] ]
+  snapshots:
+    max_snapshots: 5
+    save_epochs: 25
+    save_optimizer_state: false
+train_settings:
+  batch_size: 1
+  dataloader_workers: 0
+  dataloader_pin_memory: false
+  display_iters: 500
+  epochs: 250
diff --git a/deeplabcut/modelzoo/model_configs/fasterrcnn_resnet50_fpn_v2.yaml b/deeplabcut/modelzoo/model_configs/fasterrcnn_resnet50_fpn_v2.yaml
new file mode 100644
index 000000000..ccddc494f
--- /dev/null
+++ b/deeplabcut/modelzoo/model_configs/fasterrcnn_resnet50_fpn_v2.yaml
@@ -0,0 +1,49 @@
+data:
+  colormode: RGB
+  inference:
+    normalize_images: true
+  train:
+    affine:
+      p: 0.5
+      rotation: 30
+      scaling: [ 1.0, 1.0 ]
+      translation: 40
+    collate:
+      type: ResizeFromDataSizeCollate
+      min_scale: 0.4
+      max_scale: 1.0
+      min_short_side: 128
+      max_short_side: 1152
+      multiple_of: 32
+      to_square: false
+    hflip: true
+    normalize_images: true
+device: auto
+model:
+  type: FasterRCNN
+  variant: fasterrcnn_resnet50_fpn_v2
+  box_score_thresh: 0.6
+  freeze_bn_stats: true
+  freeze_bn_weights: false
+runner:
+  type: DetectorTrainingRunner
+  eval_interval: 10
+  optimizer:
+    type: AdamW
+    params:
+      lr: 1e-5
+  scheduler:
+    type: LRListScheduler
+    params:
+      milestones: [ 90 ]
+      lr_list: [ [ 1e-6 ] ]
+  snapshots:
+    max_snapshots: 5
+    save_epochs: 25
+    save_optimizer_state: false
+train_settings:
+  batch_size: 1
+  dataloader_workers: 0
+  dataloader_pin_memory: false
+  display_iters: 500
+  epochs: 250
\ No newline at end of file
diff --git a/deeplabcut/modelzoo/model_configs/hrnetw32.yaml b/deeplabcut/modelzoo/model_configs/hrnet_w32.yaml
similarity index 68%
rename from deeplabcut/modelzoo/model_configs/hrnetw32.yaml
rename to deeplabcut/modelzoo/model_configs/hrnet_w32.yaml
index ec72b0647..011d7727c 100644
--- a/deeplabcut/modelzoo/model_configs/hrnetw32.yaml
+++ b/deeplabcut/modelzoo/model_configs/hrnet_w32.yaml
@@ -16,42 +16,6 @@ data:
     auto_padding:
       pad_width_divisor: 32
       pad_height_divisor: 32
-detector:
-  data:
-    colormode: RGB
-    inference:
-      normalize_images: true
-    train:
-      hflip: true
-      normalize_images: true
-  device: auto
-  model:
-    type: FasterRCNN
-    variant: fasterrcnn_resnet50_fpn_v2
-    box_score_thresh: 0.6
-    pretrained: false
-  runner:
-    type: DetectorTrainingRunner
-    eval_interval: 50
-    optimizer:
-      type: AdamW
-      params:
-        lr: 1e-5
-    scheduler:
-      type: LRListScheduler
-      params:
-        milestones: [ 90 ]
-        lr_list: [ [ 1e-6 ] ]
-    snapshots:
-      max_snapshots: 5
-      save_epochs: 50
-      save_optimizer_state: false
-  train_settings:
-    batch_size: 1
-    dataloader_workers: 0
-    dataloader_pin_memory: true
-    display_iters: 500
-    epochs: 250
 device: auto
 method: td
 model:
@@ -89,6 +53,7 @@ model:
         channels: [32, "num_bodyparts"]
         kernel_size: [1]
         strides: [1]
+net_type: hrnet_w32
 runner:
   type: PoseTrainingRunner
   key_metric: "test.mAP"
@@ -110,8 +75,7 @@ runner:
 train_settings:
   batch_size: 1
   dataloader_workers: 0
-  dataloader_pin_memory: true
+  dataloader_pin_memory: false
   display_iters: 500
   epochs: 200
-  pretrained_weights: null
   seed: 42
diff --git a/deeplabcut/modelzoo/model_configs/resnet_50.yaml b/deeplabcut/modelzoo/model_configs/resnet_50.yaml
new file mode 100644
index 000000000..994840c7d
--- /dev/null
+++ b/deeplabcut/modelzoo/model_configs/resnet_50.yaml
@@ -0,0 +1,88 @@
+data:
+  colormode: RGB
+  inference:
+    normalize_images: true
+  train:
+    affine:
+      p: 0.5
+      scaling: [1.0, 1.0]
+      rotation: 30
+      translation: 0
+    gaussian_noise: 12.75
+    normalize_images: true
+device: auto
+method: td
+model:
+  backbone:
+    type: ResNet
+    model_name: resnet50_gn
+    output_stride: 16
+    freeze_bn_stats: false
+    freeze_bn_weights: false
+  backbone_output_channels: 2048
+  heads:
+    bodypart:
+      type: HeatmapHead
+      weight_init: normal
+      predictor:
+        type: HeatmapPredictor
+        apply_sigmoid: false
+        clip_scores: true
+        location_refinement: true
+        locref_std: 7.2801
+      target_generator:
+        type: HeatmapGaussianGenerator
+        num_heatmaps: "num_bodyparts"
+        pos_dist_thresh: 17
+        heatmap_mode: KEYPOINT
+        generate_locref: true
+        locref_std: 7.2801
+      criterion:
+        heatmap:
+          type: WeightedMSECriterion
+          weight: 1.0
+        locref:
+          type: WeightedHuberCriterion
+          weight: 0.05
+      heatmap_config:
+        channels:
+        - 2048
+        - "num_bodyparts"
+        kernel_size:
+        - 3
+        strides:
+        - 2
+      locref_config:
+        channels:
+        - 2048
+        - "num_bodyparts x 2"
+        kernel_size:
+        - 3
+        strides:
+        - 2
+net_type: resnet_50
+runner:
+  type: PoseTrainingRunner
+  key_metric: "test.mAP"
+  key_metric_asc: true
+  eval_interval: 10
+  optimizer:
+    type: AdamW
+    params:
+      lr: 1e-5
+  scheduler:
+    type: LRListScheduler
+    params:
+      lr_list: [ [ 1e-6 ], [ 1e-7 ] ]
+      milestones: [ 160, 190 ]
+  snapshots:
+    max_snapshots: 5
+    save_epochs: 25
+    save_optimizer_state: false
+train_settings:
+  batch_size: 1
+  dataloader_workers: 0
+  dataloader_pin_memory: false
+  display_iters: 500
+  epochs: 100
+  seed: 42
diff --git a/deeplabcut/modelzoo/model_configs/ssdlite.yaml b/deeplabcut/modelzoo/model_configs/ssdlite.yaml
new file mode 100644
index 000000000..c285cdcd8
--- /dev/null
+++ b/deeplabcut/modelzoo/model_configs/ssdlite.yaml
@@ -0,0 +1,48 @@
+data:
+  colormode: RGB
+  inference:
+    normalize_images: true
+  train:
+    affine:
+      p: 0.5
+      rotation: 30
+      scaling: [ 1.0, 1.0 ]
+      translation: 40
+    collate:
+      type: ResizeFromDataSizeCollate
+      min_scale: 0.4
+      max_scale: 1.0
+      min_short_side: 128
+      max_short_side: 1152
+      multiple_of: 32
+      to_square: false
+    hflip: true
+    normalize_images: true
+device: auto
+model:
+  type: SSDLite
+  box_score_thresh: 0.6
+  freeze_bn_stats: true
+  freeze_bn_weights: false
+runner:
+  type: DetectorTrainingRunner
+  eval_interval: 10
+  optimizer:
+    type: AdamW
+    params:
+      lr: 1e-5
+  scheduler:
+    type: LRListScheduler
+    params:
+      milestones: [ 90 ]
+      lr_list: [ [ 1e-6 ] ]
+  snapshots:
+    max_snapshots: 5
+    save_epochs: 25
+    save_optimizer_state: false
+train_settings:
+  batch_size: 8
+  dataloader_workers: 0
+  dataloader_pin_memory: false
+  display_iters: 500
+  epochs: 250
\ No newline at end of file
diff --git a/deeplabcut/modelzoo/models_to_framework.json b/deeplabcut/modelzoo/models_to_framework.json
index 47d25be63..6ff6f969f 100644
--- a/deeplabcut/modelzoo/models_to_framework.json
+++ b/deeplabcut/modelzoo/models_to_framework.json
@@ -1,4 +1,5 @@
 {
-    "hrnetw32": "pytorch",
-    "dlcrnet": "tensorflow"
+    "dlcrnet": "tensorflow",
+    "hrnet_w32": "pytorch",
+    "resnet_50": "pytorch"
 }
diff --git a/deeplabcut/modelzoo/project_configs/superanimal_bird.yaml b/deeplabcut/modelzoo/project_configs/superanimal_bird.yaml
new file mode 100644
index 000000000..3144433a1
--- /dev/null
+++ b/deeplabcut/modelzoo/project_configs/superanimal_bird.yaml
@@ -0,0 +1,105 @@
+# Project definitions (do not edit)
+Task:
+scorer:
+date:
+multianimalproject:
+identity:
+
+
+# Project path (change when moving around)
+project_path:
+
+
+# Default DeepLabCut engine to use for shuffle creation (either pytorch or tensorflow)
+engine: pytorch
+
+
+# Annotation data set configuration (and individual video cropping parameters)
+video_sets:
+bodyparts:
+- back
+- bill
+- belly
+- breast
+- crown
+- forehead
+- left_eye
+- left_leg
+- left_wing_tip
+- left_wrist
+- nape
+- right_eye
+- right_leg
+- right_wing_tip
+- right_wrist
+- tail_tip
+- throat
+- neck
+- tail_left
+- tail_right
+- upper_spine
+- upper_half_spine
+- lower_half_spine
+- right_foot
+- left_foot
+- left_half_chest
+- right_half_chest
+- chin
+- left_tibia
+- right_tibia
+- lower_spine
+- upper_half_neck
+- lower_half_neck
+- left_chest
+- right_chest
+- upper_neck
+- left_wing_shoulder
+- left_wing_elbow
+- right_wing_shoulder
+- right_wing_elbow
+- upper_cere
+- lower_cere
+
+
+# Fraction of video to start/stop when extracting frames for labeling/refinement
+start:
+stop:
+numframes2pick:
+
+
+# Plotting configuration
+skeleton: []
+skeleton_color: black
+pcutoff:
+dotsize:
+alphavalue:
+colormap: rainbow
+
+
+# Training,Evaluation and Analysis configuration
+TrainingFraction:
+iteration:
+default_net_type:
+default_augmenter:
+snapshotindex:
+detector_snapshotindex: -1
+batch_size: 1
+detector_batch_size: 1
+
+
+# Cropping Parameters (for analysis and outlier frame detection)
+cropping:
+#if cropping is true for analysis, then set the values here:
+x1:
+x2:
+y1:
+y2:
+
+
+# Refinement configuration (parameters from annotation dataset configuration also relevant in this stage)
+corner2move2:
+move2corner:
+
+
+# Conversion tables to fine-tune SuperAnimal weights
+SuperAnimalConversionTables:
diff --git a/deeplabcut/modelzoo/project_configs/superanimal_quadruped.yaml b/deeplabcut/modelzoo/project_configs/superanimal_quadruped.yaml
index 4363d5181..e06f82032 100644
--- a/deeplabcut/modelzoo/project_configs/superanimal_quadruped.yaml
+++ b/deeplabcut/modelzoo/project_configs/superanimal_quadruped.yaml
@@ -79,8 +79,9 @@ iteration:
 default_net_type:
 default_augmenter:
 snapshotindex:
-detector_snapshotindex:
+detector_snapshotindex: -1
 batch_size: 1
+detector_batch_size: 1
 
 
 # Cropping Parameters (for analysis and outlier frame detection)
@@ -96,5 +97,6 @@ y2:
 corner2move2:
 move2corner:
 
+
 # Conversion tables to fine-tune SuperAnimal weights
 SuperAnimalConversionTables:
diff --git a/deeplabcut/modelzoo/project_configs/superanimal_topviewmouse.yaml b/deeplabcut/modelzoo/project_configs/superanimal_topviewmouse.yaml
index ace8da8f3..bd0962832 100644
--- a/deeplabcut/modelzoo/project_configs/superanimal_topviewmouse.yaml
+++ b/deeplabcut/modelzoo/project_configs/superanimal_topviewmouse.yaml
@@ -67,8 +67,9 @@ iteration:
 default_net_type:
 default_augmenter:
 snapshotindex:
-detector_snapshotindex:
+detector_snapshotindex: -1
 batch_size: 1
+detector_batch_size: 1
 
 
 # Cropping Parameters (for analysis and outlier frame detection)
diff --git a/deeplabcut/modelzoo/utils.py b/deeplabcut/modelzoo/utils.py
index 869ade6ab..41fc25e18 100644
--- a/deeplabcut/modelzoo/utils.py
+++ b/deeplabcut/modelzoo/utils.py
@@ -58,6 +58,38 @@ def get_super_animal_project_cfg(super_animal: str) -> dict:
     return read_config(str(super_animal_projects[super_animal]))
 
 
+def get_super_animal_scorer(
+    super_animal: str,
+    model_snapshot_path: Path,
+    detector_snapshot_path: Path | None,
+) -> str:
+    """
+    Args:
+        super_animal: The SuperAnimal dataset on which the models were trained
+        model_snapshot_path: The path for the SuperAnimal pose model snapshot
+        detector_snapshot_path: The path for the SuperAnimal detector snapshot, if a
+            detector is being used.
+
+    Returns:
+        The DLC scorer name to use for the given SuperAnimal models.
+    """
+    super_animal_prefix = super_animal + "_"
+    dlc_scorer = super_animal_prefix
+
+    if detector_snapshot_path is not None:
+        detector_name = detector_snapshot_path.stem
+        if detector_name.startswith(super_animal_prefix):
+            detector_name = detector_name[len(super_animal_prefix) :]
+        dlc_scorer += f"{detector_name}_"
+
+    model_name = model_snapshot_path.stem
+    if model_name.startswith(super_animal_prefix):
+        model_name = model_name[len(super_animal_prefix) :]
+    dlc_scorer += f"{model_name}"
+
+    return dlc_scorer
+
+
 def create_conversion_table(
     config: str | Path,
     super_animal: str,
@@ -120,7 +152,7 @@ def get_conversion_table(cfg: dict | str | Path, super_animal: str) -> Conversio
         cfg = read_config(str(cfg))
 
     conversion_tables = cfg.get("SuperAnimalConversionTables", {})
-    if super_animal not in conversion_tables:
+    if conversion_tables is None or super_animal not in conversion_tables:
         raise ValueError(
             f"No conversion table defined in the project config for {super_animal}."
             "Call deeplabcut.modelzoo.create_conversion_table to create one."
@@ -200,6 +232,56 @@ def parse_project_model_name(superanimal_name: str) -> tuple[str, str]:
 
 
 def get_superanimal_colormaps():
+    # FIXME(shaokai) - Add colormaps for the SuperBird dataset
+    superanimal_bird_colors = (
+        np.array(
+            [
+                (127, 0, 255),
+                (115, 18, 254),
+                (103, 37, 254),
+                (91, 56, 253),
+                (79, 74, 252),
+                (65, 95, 250),
+                (53, 112, 248),
+                (41, 128, 246),
+                (29, 144, 243),
+                (15, 162, 239),
+                (3, 176, 236),
+                (8, 189, 232),
+                (20, 201, 228),
+                (34, 214, 223),
+                (46, 223, 219),
+                (58, 232, 214),
+                (70, 239, 209),
+                (84, 246, 202),
+                (96, 250, 196),
+                (108, 253, 190),
+                (120, 254, 184),
+                (134, 254, 176),
+                (146, 253, 169),
+                (158, 250, 162),
+                (170, 246, 154),
+                (184, 239, 146),
+                (196, 232, 138),
+                (208, 223, 130),
+                (220, 214, 122),
+                (234, 201, 112),
+                (246, 189, 103),
+                (255, 176, 95),
+                (255, 162, 86),
+                (255, 144, 75),
+                (255, 128, 66),
+                (255, 112, 57),
+                (255, 95, 48),
+                (255, 74, 37),
+                (255, 56, 28),
+                (255, 37, 18),
+                (255, 18, 9),
+                (255, 0, 0),
+            ]
+        )
+        / 255
+    )
     superanimal_topviewmouse_colors = (
         np.array(
             [
@@ -282,6 +364,9 @@ def get_superanimal_colormaps():
     )
 
     superanimal_colormaps = {
+        "superanimal_bird": ListedColormap(
+            list(superanimal_bird_colors), name="superanimal_bird"
+        ),
         "superanimal_topviewmouse": ListedColormap(
             list(superanimal_topviewmouse_colors), name="superanimal_topviewmouse"
         ),
diff --git a/deeplabcut/modelzoo/video_inference.py b/deeplabcut/modelzoo/video_inference.py
index 8f304c338..971bf6065 100644
--- a/deeplabcut/modelzoo/video_inference.py
+++ b/deeplabcut/modelzoo/video_inference.py
@@ -15,14 +15,16 @@
 from pathlib import Path
 from typing import Optional, Union
 
-import numpy as np
 from dlclibrary.dlcmodelzoo.modelzoo_download import download_huggingface_model
 from ruamel.yaml import YAML
 
-from deeplabcut.modelzoo.utils import parse_project_model_name
+from deeplabcut.modelzoo.utils import get_super_animal_scorer
+from deeplabcut.pose_estimation_pytorch.config import read_config_as_dict
 from deeplabcut.pose_estimation_pytorch.modelzoo.train_from_coco import adaptation_train
 from deeplabcut.pose_estimation_pytorch.modelzoo.utils import (
-    get_config_model_paths,
+    get_snapshot_folder_path,
+    get_super_animal_snapshot_path,
+    load_super_animal_config,
     update_config,
 )
 from deeplabcut.utils.auxiliaryfunctions import get_deeplabcut_path
@@ -35,9 +37,12 @@
 def video_inference_superanimal(
     videos: Union[str, list],
     superanimal_name: str,
+    model_name: str,
+    detector_name: str | None = None,
     scale_list: Optional[list] = None,
     videotype: str = ".mp4",
     dest_folder: Optional[str] = None,
+    cropping: list[int] | None = None,
     video_adapt: bool = False,
     plot_trajectories: bool = False,
     batch_size: int = 1,
@@ -59,12 +64,10 @@ def video_inference_superanimal(
     This function performs inference on videos using a pretrained SuperAnimal model.
 
     IMPORTANT: Note that since we have both TensorFlow and PyTorch Engines, we will
-    route the engine based on the model you select.
+    route the engine based on the model you select:
 
-    * superanimal_topviewmouse_hrnetw32 - > PyTorch
-    * superanimal_quadruped_hrnetw32 -> PyTorch
-    * superanimal_topviewmouse_dlcrnet -> TensorFlow
-    * superanimal_quadruped_dlcrnet -> TensorFlow
+        * dlcrnet -> TensorFlow
+        * all others - > PyTorch
 
     Parameters
     ----------
@@ -73,10 +76,14 @@ def video_inference_superanimal(
         The path to the video or a list of paths to videos.
 
     superanimal_name (str):
-        The name of the SuperAnimal model.
-        The name should be in the format: {project_name}_{modelname}.
-        For example: `superanimal_topviewmouse_dlcrnet` or `superanimal_quadruped_hrnetw32`.
-        See (model explanation section below).
+        The name of the SuperAnimal dataset for which to load a pre-trained model.
+
+    model_name (str):
+        The model architecture to use for inference.
+
+    detector_name (str):
+        For top-down models (only available with the PyTorch framework), the type of
+        object detector to use for inference.
 
     scale_list (list):
         A list of different resolutions for the spatial pyramid. Used only for bottom up models.
@@ -84,8 +91,16 @@ def video_inference_superanimal(
     videotype (str):
         Checks for the extension of the video in case the input to the video is a directory.
         Only videos with this extension are analyzed. The default is ``.mp4``.
+
     dest_folder (str): The path to the folder where the results should be saved.
 
+    cropping: list or None, optional, default=None
+        Only for SuperAnimal models running with the PyTorch engine.
+        List of cropping coordinates as [x1, x2, y1, y2].
+        Note that the same cropping parameters will then be used for all videos.
+        If different video crops are desired, run ``video_inference_superanimal`` on
+        individual videos with the corresponding cropping coordinates.
+
     video_adapt (bool):
         Whether to perform video adaptation. The default is False.
         You only need to perform it on one video because the adaptation generalizes to all videos that are similar.
@@ -140,38 +155,69 @@ def video_inference_superanimal(
         If the model is not found in the modelzoo.
         Warning: If the superanimal_name will be deprecated in the future.
     
-   (Model Explanation) SuperAnimal-Quadruped: 
+    (Model Explanation) SuperAnimal-Quadruped:
+    `superanimal_quadruped` models aim to work across a large range of quadruped
+    animals, from horses, dogs, sheep, rodents, to elephants. The camera perspective is
+    orthogonal to the animal ("side view"), and most of the data includes the animals
+    face (thus the front and side of the animal). You will note we have several variants
+    that differ in speed vs. performance, so please do test them out on your data to see
+    which is best suited for your application. Also note we have a "video adaptation"
+    feature, which lets you adapt your data to the model in a self-supervised way.
+    No labeling needed!
+
+    All model snapshots are automatically downloaded to modelzoo/checkpoints when used.
 
-    - `superanimal_quadruped_x` models aim to work across a large range of quadruped animals, from horses, dogs, sheep, rodents, to elephants. The camera perspective is orthogonal to the animal ("side view"), and most of the data includes the animals face (thus the front and side of the animal). You will note we have several variants that differ in speed vs. performance, so please do test them out on your data to see which is best suited for your application. Also note we have a "video adaptation" feature, which lets you adapt your data to the model in a self-supervised way. No labeling needed!
     - PLEASE SEE THE FULL DATASHEET: https://zenodo.org/records/10619173
-    - MORE DETAILS ON THE MODELS (detector, pose estimators): https://huggingface.co/mwmathis/DeepLabCutModelZoo-SuperAnimal-Quadruped
+    - MORE DETAILS ON THE MODELS (detector, pose estimators):
+        https://huggingface.co/mwmathis/DeepLabCutModelZoo-SuperAnimal-Quadruped
     - We provide several models:
-        - `superanimal_quadruped_hrnetw32` (pytorch engine)
-            - `superanimal_quadruped_hrnetw32` is a top-down model that is paired with a detector. That means it takes a cropped image from an object detector and predicts the keypoints. The object detector is currently a trained [ResNet50-based Faster-RCNN](https://pytorch.org/vision/stable/models/faster_rcnn.html).
-        - `superanimal_quadruped_dlcrnet` (tensorflow engine)
-            - `superanimal_quadruped_dlcrnet` is a bottom-up model that predicts all keypoints then groups them into individuals. This can be faster, but more error prone.
-        - `superanimal_quadruped` -> This is the same as `superanimal_quadruped_dlcrnet`, this was the old naming and being depreciated.
-        - For all models, they are automatically downloaded to modelzoo/checkpoints when used.
+        - `hrnet_w32` (Top-Down pose estimation model, PyTorch engine)
+            An `hrnet_w32` is a top-down model that is paired with a detector. That
+            means it takes a cropped image from an object detector and predicts the
+            keypoints. When selecting this variant, a `detector_name` must be set with
+            one of the provided object detectors.
+        - `dlcrnet` (TensorFlow engine)
+            This is a bottom-up model that predicts all keypoints then groups them into
+            individuals. This can be faster, but more error prone.
+    - We provide one object detector (only for the PyTorch engine):
+        - `fasterrcnn_resnet50_fpn_v2`
+            This is a FasterRCNN model with a ResNet backbone, see
+            https://pytorch.org/vision/stable/models/faster_rcnn.html
 
     (Model Explanation) SuperAnimal-TopViewMouse:
+    `superanimal_topviewmouse` aims to work across lab mice in different lab settings
+    from a top-view perspective; this is very polar in many behavioral assays in freely
+    moving mice.
+
+    All model snapshots are automatically downloaded to modelzoo/checkpoints when used.
 
-    -  `superanimal_topviewmouse_x` aims to work across lab mice in different lab settings from a top-view perspective; this is very polar in many behavioral assays in freely moving mice.
     - [PLEASE SEE THE FULL DATASHEET HERE](https://zenodo.org/records/10618947)
     - [MORE DETAILS ON THE MODELS (detector, pose estimators)](https://huggingface.co/mwmathis/DeepLabCutModelZoo-SuperAnimal-TopViewMouse)
     - We provide several models:
-        - `superanimal_topviewmouse_hrnetw32` (pytorch engine)
-            - `superanimal_topviewmouse_hrnetw32` is a top-down model that is paired with a detector. That means it takes a cropped image from an object detector and predicts the keypoints. The object detector is currently a trained [ResNet50-based Faster-RCNN](https://pytorch.org/vision/stable/models/faster_rcnn.html).
-        - `superanimal_topviewmouse_dlcrnet` (tensorflow engine)
-            - `superanimal_topviewmouse_dlcrnet` is a bottom-up model that predicts all keypoints then groups them into individuals. This can be faster, but more error prone.
-        - `superanimal_topviewmouse` -> This is the same as `superanimal_topviewmouse_dlcrnet`, this was the old naming and being depreciated.
-        - For all models, they are automatically downloaded to modelzoo/checkpoints when used.
+        - `hrnet_w32` (Top-Down pose estimation model, PyTorch engine)
+            An `hrnet_w32` is a top-down model that is paired with a detector. That
+            means it takes a cropped image from an object detector and predicts the
+            keypoints. When selecting this variant, a `detector_name` must be set with
+            one of the provided object detectors.
+        - `dlcrnet` (TensorFlow engine)
+            This is a bottom-up model that predicts all keypoints then groups them into
+            individuals. This can be faster, but more error prone.
+    - We provide one object detector (only for the PyTorch engine):
+        - `fasterrcnn_resnet50_fpn_v2`
+            This is a FasterRCNN model with a ResNet backbone, see
+            https://pytorch.org/vision/stable/models/faster_rcnn.html
+
+    (Model Explanation) SuperAnimal-Bird:
+    TODO(shaokai)
 
     Examples (PyTorch Engine)
     --------
     >>> import deeplabcut.modelzoo.video_inference.video_inference_superanimal as video_inference_superanimal
     >>> video_inference_superanimal(
         videos=["/mnt/md0/shaokai/DLCdev/3mice_video1_short.mp4"],
-        superanimal_name="superanimal_topviewmouse_hrnetw32",
+        superanimal_name="superanimal_topviewmouse",
+        model_name="hrnet_w32",
+        detector_name="fasterrcnn_resnet50_fpn_v2",
         video_adapt=True,
         max_individuals=3,
         pseudo_threshold=0.1,
@@ -213,16 +259,11 @@ def video_inference_superanimal(
     are sensitive to the scales of the image.
     If you find your predictions not good without scale_list or it's too hard to find
     the right scale_list, you can try to use the PyTorch engine.
-
-
     """
     if scale_list is None:
         scale_list = []
 
-    project_name, model_name = parse_project_model_name(superanimal_name)
-
-    print(f"running video inference on {videos} with {project_name}_{model_name}")
-
+    print(f"Running video inference on {videos} with {superanimal_name}_{model_name}")
     dlc_root_path = get_deeplabcut_path()
     modelzoo_path = os.path.join(dlc_root_path, "modelzoo")
     available_architectures = json.load(
@@ -230,46 +271,24 @@ def video_inference_superanimal(
     )
     framework = available_architectures[model_name]
     print(f"Using {framework} for model {model_name}")
-
-    weight_folder = os.path.join(modelzoo_path, "checkpoints")
-
-    redownload = False
-    if framework == "pytorch":
-        pose_model_name = f"{project_name}_{model_name}.pth"
-        detector_name = f"{project_name}_fasterrcnn.pt"
-        rename_mapping = {
-            "pose_model.pth": pose_model_name,
-            "detector.pt": detector_name,
-        }
-        if customized_pose_checkpoint is None:
-            pose_model_path = os.path.join(weight_folder, pose_model_name)
-        else:
-            pose_model_path = customized_pose_checkpoint
-        detector_model_path = os.path.join(weight_folder, detector_name)
-        if not (
-            os.path.exists(pose_model_path) and os.path.exists(detector_model_path)
-        ):
-            redownload = True
-    elif framework == "tensorflow":
-        weight_folder = os.path.join(weight_folder, f"{project_name}_{model_name}")
-        redownload = not os.path.isdir(weight_folder)
-        rename_mapping = {}
-
-    if redownload:
-        download_huggingface_model(
-            superanimal_name, target_dir=weight_folder, rename_mapping=rename_mapping
-        )
-
     if framework == "tensorflow":
         from deeplabcut.pose_estimation_tensorflow.modelzoo.api.superanimal_inference import (
             _video_inference_superanimal,
         )
 
+        weight_folder = (
+            get_snapshot_folder_path() / f"{superanimal_name}_{model_name}"
+        )
+        if not weight_folder.exists():
+            download_huggingface_model(
+                superanimal_name, target_dir=str(weight_folder), rename_mapping=None
+            )
+
         if isinstance(videos, str):
             videos = [videos]
         _video_inference_superanimal(
             videos,
-            project_name,
+            superanimal_name,
             model_name,
             scale_list,
             videotype,
@@ -284,41 +303,58 @@ def video_inference_superanimal(
             _video_inference_superanimal,
         )
 
+        if customized_model_config is not None:
+            config = read_config_as_dict(customized_model_config)
+        else:
+            config = load_super_animal_config(
+                super_animal=superanimal_name,
+                model_name=model_name,
+                detector_name=detector_name,
+            )
+
+        pose_model_path = customized_pose_checkpoint
+        if pose_model_path is None:
+            pose_model_path = get_super_animal_snapshot_path(
+                dataset=superanimal_name,
+                model_name=model_name,
+            )
+
+        detector_path = customized_detector_checkpoint
+        if detector_path is None:
+            detector_path = get_super_animal_snapshot_path(
+                dataset=superanimal_name,
+                model_name=detector_name,
+            )
+
+        dlc_scorer = get_super_animal_scorer(
+            superanimal_name, pose_model_path, detector_path
+        )
+
+        config = update_config(config, max_individuals, device)
+        output_suffix = "_before_adapt"
         if video_adapt:
             # the users can pass in many videos. For now, we only use one video for
             # video adaptation. As reported in Ye et al. 2024, one video should be
             # sufficient for video adaptation.
             video_path = Path(videos[0])
-            print(f"using {video_path} for video adaptation training")
+            print(f"Using {video_path} for video adaptation training")
 
             # video inference to get pseudo label
             _video_inference_superanimal(
                 [str(video_path)],
-                project_name,
-                model_name,
-                max_individuals,
-                pcutoff,
-                device=device,
+                superanimal_name,
+                model_cfg=config,
+                model_snapshot_path=pose_model_path,
+                detector_snapshot_path=detector_path,
+                max_individuals=max_individuals,
+                pcutoff=pcutoff,
                 batch_size=batch_size,
                 detector_batch_size=detector_batch_size,
+                cropping=cropping,
                 dest_folder=dest_folder,
-                customized_pose_checkpoint=customized_pose_checkpoint,
-                customized_detector_checkpoint=customized_detector_checkpoint,
-                customized_model_config=customized_model_config,
+                output_suffix=output_suffix,
             )
 
-            (
-                model_config,
-                project_config,
-                pose_model_path,
-                detector_path,
-            ) = get_config_model_paths(project_name, model_name)
-            config = {**project_config, **model_config}
-            config = update_config(config, max_individuals, device)
-
-            # we need config to fetch the correct keypoints to dlc3predictions_2_annotation_from_video
-            bodyparts = config["metadata"]["bodyparts"]
-
             # we prepare the pseudo dataset in the same folder of the target video
             pseudo_dataset_folder = video_path.with_name(f"pseudo_{video_path.stem}")
             pseudo_dataset_folder.mkdir(exist_ok=True)
@@ -331,14 +367,19 @@ def video_inference_superanimal(
             else:
                 image_folder.mkdir()
                 print(
-                    f"Video frames being extracted to {image_folder} for video adaptation."
+                    f"Video frames being extracted to {image_folder} for video "
+                    f"adaptation."
                 )
-                video_to_frames(video_path, pseudo_dataset_folder)
+                video_to_frames(video_path, pseudo_dataset_folder, cropping=cropping)
 
             anno_folder = pseudo_dataset_folder / "annotations"
-            if anno_folder.exists():
+            if (
+                (anno_folder / "train.json").exists()
+                and (anno_folder / "test.json").exists()
+            ):
                 print(
-                    f"{anno_folder} exists, skipping the annotation construction. Delete the folder if you want to re-construct pseudo annotations"
+                    f"{anno_folder} exists, skipping the annotation construction. "
+                    f"Delete the folder if you want to re-construct pseudo annotations"
                 )
             else:
                 anno_folder.mkdir()
@@ -347,7 +388,7 @@ def video_inference_superanimal(
                     pseudo_anno_dir = video_path.parent
                 else:
                     pseudo_anno_dir = Path(dest_folder)
-                dlc_scorer = f"{project_name}_{model_name}"
+
                 pseudo_anno_name = f"{video_path.stem}_{dlc_scorer}_before_adapt.json"
                 with open(pseudo_anno_dir / pseudo_anno_name, "r") as f:
                     predictions = json.load(f)
@@ -358,16 +399,12 @@ def video_inference_superanimal(
                 dlc3predictions_2_annotation_from_video(
                     predictions,
                     pseudo_dataset_folder,
-                    bodyparts,
+                    config["metadata"]["bodyparts"],
                     superanimal_name,
                     pose_threshold=pseudo_threshold,
                     bbox_threshold=bbox_threshold,
                 )
 
-            # this is probably needed for video generation
-            individuals = [f"animal{i}" for i in range(max_individuals)]
-            config["metadata"]["individuals"] = individuals
-
             # the model config's parameters need to be updated for adaptation training
             model_config_path = model_folder / "pytorch_config.yaml"
             with open(model_config_path, "w") as f:
@@ -391,27 +428,25 @@ def video_inference_superanimal(
                     "existing checkpoints."
                 )
             else:
-
                 print(
-                    f"""
-Running video adaptation with following parameters: 
-(pose training) pose_epochs: {pose_epochs}
-(pose) save_epochs: 1
-detector_epochs: {detector_epochs}
-detector_save_epochs: 1
-video adaptation batch size: {video_adapt_batch_size}"""
+                    "Running video adaptation with following parameters:\n"
+                    f"  (pose training) pose_epochs: {pose_epochs}\n"
+                    "  (pose) save_epochs: 1\n"
+                    f"  detector_epochs: {detector_epochs}\n"
+                    "  detector_save_epochs: 1\n"
+                    f"  video adaptation batch size: {video_adapt_batch_size}\n"
                 )
-                with open(
-                    os.path.join(pseudo_dataset_folder, "annotations", "train.json"),
-                    "r",
-                ) as f:
+                train_file = pseudo_dataset_folder / "annotations" / "train.json"
+                with open(train_file, "r") as f:
                     temp_obj = json.load(f)
-                    annotations = temp_obj["annotations"]
-                    if len(annotations) == 0:
-                        print(
-                            f"No valid predictions from {str(video_path)}. Check the quality of the video"
-                        )
-                        return
+
+                annotations = temp_obj["annotations"]
+                if len(annotations) == 0:
+                    print(
+                        f"No valid predictions from {str(video_path)}. Check the "
+                        "quality of the video"
+                    )
+                    return
 
                 adaptation_train(
                     project_root=pseudo_dataset_folder,
@@ -430,21 +465,22 @@ def video_inference_superanimal(
                     detector_batch_size=video_adapt_batch_size,
                 )
 
-            # Set the customized checkpoint paths
-            customized_pose_checkpoint = str(adapted_pose_checkpoint)
-            customized_detector_checkpoint = str(adapted_detector_checkpoint)
+            # Set the customized checkpoint paths and
+            output_suffix = "_after_adapt"
+            detector_path = adapted_detector_checkpoint
+            pose_model_path = adapted_pose_checkpoint
 
         return _video_inference_superanimal(
             videos,
-            project_name,
-            model_name,
-            max_individuals,
-            pcutoff,
-            device=device,
+            superanimal_name,
+            model_cfg=config,
+            model_snapshot_path=pose_model_path,
+            detector_snapshot_path=detector_path,
+            max_individuals=max_individuals,
+            pcutoff=pcutoff,
             batch_size=batch_size,
             detector_batch_size=detector_batch_size,
+            cropping=cropping,
             dest_folder=dest_folder,
-            customized_pose_checkpoint=customized_pose_checkpoint,
-            customized_detector_checkpoint=customized_detector_checkpoint,
-            customized_model_config=customized_model_config,
+            output_suffix=output_suffix,
         )
diff --git a/deeplabcut/modelzoo/webapp/inference.py b/deeplabcut/modelzoo/webapp/inference.py
index 9452382a9..806e7778f 100644
--- a/deeplabcut/modelzoo/webapp/inference.py
+++ b/deeplabcut/modelzoo/webapp/inference.py
@@ -12,11 +12,9 @@
 
 import numpy as np
 
+import deeplabcut.pose_estimation_pytorch.modelzoo as modelzoo
 from deeplabcut.pose_estimation_pytorch.apis.utils import get_inference_runners
-from deeplabcut.pose_estimation_pytorch.modelzoo.utils import (
-    get_config_model_paths,
-    update_config,
-)
+from deeplabcut.pose_estimation_pytorch.modelzoo.utils import update_config
 
 
 class SingletonTopDownRunners:
@@ -71,26 +69,21 @@ class SuperanimalPyTorchInference:
     def __init__(
         self,
         project_name: str,
-        pose_model_type: str = "hrnetw32",
+        pose_model_type: str = "hrnet_w32",
+        detector_model_type: str = "fasterrcnn_resnet50_fpn_v2",
         max_individuals: int = 30,
         device: str = "cpu",
     ):
-
-        (
-            model_config,
-            project_config,
-            _,
-            _,
-        ) = get_config_model_paths(project_name, pose_model_type)
-
         self.max_individuals = max_individuals
-        config = {**project_config, **model_config}
+        config = modelzoo.load_super_animal_config(
+            super_animal=project_name,
+            model_name=pose_model_type,
+            detector_name=detector_model_type,
+        )
         config = update_config(config, max_individuals, device)
-
         self._config = config
 
     def initialize_models(self, pose_model_path: str, detector_model_path: str):
-
         self.models = SingletonTopDownRunners(
             self.config,
             pose_model_path,
diff --git a/deeplabcut/modelzoo/weight_initialization.py b/deeplabcut/modelzoo/weight_initialization.py
new file mode 100644
index 000000000..c7775d9ce
--- /dev/null
+++ b/deeplabcut/modelzoo/weight_initialization.py
@@ -0,0 +1,108 @@
+#
+# DeepLabCut Toolbox (deeplabcut.org)
+# © A. & M.W. Mathis Labs
+# https://github.com/DeepLabCut/DeepLabCut
+#
+# Please see AUTHORS for contributors.
+# https://github.com/DeepLabCut/DeepLabCut/blob/main/AUTHORS
+#
+# Licensed under GNU Lesser General Public License v3.0
+#
+"""Functions to build weight initialization parameters for SuperAnimal models"""
+from pathlib import Path
+
+import deeplabcut.modelzoo.utils as utils
+from deeplabcut.pose_estimation_pytorch.config import read_config_as_dict
+from deeplabcut.pose_estimation_pytorch.modelzoo.utils import (
+    get_super_animal_snapshot_path
+)
+from deeplabcut.core.weight_init import WeightInitialization
+
+
+def build_weight_init(
+    cfg: dict | str | Path,
+    super_animal: str,
+    model_name: str,
+    detector_name: str | None,
+    with_decoder: bool = False,
+    memory_replay: bool = False,
+    customized_pose_checkpoint: str | Path | None = None,
+    customized_detector_checkpoint: str | Path | None = None,
+) -> WeightInitialization:
+    """Builds the WeightInitialization from a SuperAnimal model for a project
+
+    Args:
+        cfg: The project's configuration, or the path to the project configuration file.
+        super_animal: The SuperAnimal model with which to initialize weights.
+        model_name: The type of the model architecture for which to load the weights.
+        detector_name: The type of detector architecture for which to load the weights.
+        with_decoder: Whether to load the decoder weights as well. If this is true,
+            a conversion table must be specified for the given SuperAnimal in the
+            project configuration file. See
+            ``deeplabcut.modelzoo.utils.create_conversion_table`` to create a
+            conversion table.
+        memory_replay: Only when ``with_decoder=True``. Whether to train the model
+            with memory replay, so that it predicts all SuperAnimal bodyparts.
+        customized_pose_checkpoint: A customized SuperAnimal pose checkpoint, as an
+            alternative to the Hugging Face one
+        customized_detector_checkpoint: A customized SuperAnimal detector checkpoint, as
+            an alternative to the Hugging Face one
+
+    To build a WeightInitialization instance for a project using the conversion table
+    specified in the project configuration file, use:
+
+        ```
+        from pathlib import Path
+        from deeplabcut.utils.auxiliaryfunctions import read_config
+        from deeplabcut.modelzoo import build_weight_init
+
+        project_cfg = read_config("/path/to/my/project/config.yaml")
+        super_animal = "superanimal_quadruped"
+        weight_init = build_weight_init(
+            cfg=project_cfg,
+            super_animal="superanimal_quadruped",
+            model_name="hrnet_w32",
+            detector_name="fasterrcnn_resnet50_fpn_v2",
+            with_decoder=True,
+            memory_replay=False,
+        )
+        ```
+
+    Returns:
+        The built WeightInitialization.
+    """
+    if isinstance(cfg, (str, Path)):
+        cfg = read_config_as_dict(cfg)
+
+    conversion_array = None
+    bodyparts = None
+    if with_decoder:
+        conversion_table = utils.get_conversion_table(cfg, super_animal)
+        conversion_array = conversion_table.to_array()
+        bodyparts = conversion_table.converted_bodyparts()
+
+    snapshot_path = customized_pose_checkpoint
+    if snapshot_path is None:
+        snapshot_path = get_super_animal_snapshot_path(
+            dataset=super_animal,
+            model_name=model_name,
+            download=True,
+        )
+
+    detector_snapshot_path = customized_detector_checkpoint
+    if detector_snapshot_path is None and detector_name is not None:
+        detector_snapshot_path = get_super_animal_snapshot_path(
+            dataset=super_animal,
+            model_name=detector_name,
+            download=True,
+        )
+
+    return WeightInitialization(
+        snapshot_path=snapshot_path,
+        detector_snapshot_path=detector_snapshot_path,
+        dataset=super_animal,
+        with_decoder=with_decoder,
+        memory_replay=memory_replay,
+        conversion_array=conversion_array,
+        bodyparts=bodyparts,
+    )
diff --git a/deeplabcut/pose_estimation_pytorch/README.md b/deeplabcut/pose_estimation_pytorch/README.md
index bd416b6dd..ef4b562e4 100644
--- a/deeplabcut/pose_estimation_pytorch/README.md
+++ b/deeplabcut/pose_estimation_pytorch/README.md
@@ -469,7 +469,7 @@ pose_runner, detector_runner = get_inference_runners(
 )
 
 predictions = video_inference(
-    video_path=video_path,
+    video=video_path,
     task=pose_task,
     pose_runner=pose_runner,
     detector_runner=detector_runner,
diff --git a/deeplabcut/pose_estimation_pytorch/apis/analyze_images.py b/deeplabcut/pose_estimation_pytorch/apis/analyze_images.py
index 26bcfa3fa..560b2b85e 100644
--- a/deeplabcut/pose_estimation_pytorch/apis/analyze_images.py
+++ b/deeplabcut/pose_estimation_pytorch/apis/analyze_images.py
@@ -22,6 +22,7 @@
 from tqdm import tqdm
 
 import deeplabcut.pose_estimation_pytorch.config.utils as config_utils
+import deeplabcut.pose_estimation_pytorch.modelzoo as modelzoo
 from deeplabcut.core.engine import Engine
 from deeplabcut.modelzoo.utils import get_superanimal_colormaps
 from deeplabcut.pose_estimation_pytorch.apis.utils import (
@@ -31,118 +32,84 @@
     get_scorer_uid,
     parse_snapshot_index_for_analysis,
 )
-from deeplabcut.pose_estimation_pytorch.modelzoo.utils import (
-    get_config_model_paths,
-    update_config,
-)
+from deeplabcut.pose_estimation_pytorch.modelzoo.utils import update_config
 from deeplabcut.pose_estimation_pytorch.task import Task
 from deeplabcut.pose_estimation_pytorch.utils import resolve_device
 from deeplabcut.utils import auxfun_videos, auxiliaryfunctions
-from deeplabcut.utils.auxiliaryfunctions import read_config
 
 
 def superanimal_analyze_images(
     superanimal_name: str,
     model_name: str,
+    detector_name: str,
     images: str | Path | list[str] | list[Path],
     max_individuals: int,
-    out_folder: str,
+    out_folder: str | Path,
     bbox_threshold: float = 0.6,
     progress_bar: bool = True,
     device: str | None = None,
-    customized_pose_checkpoint: str | None = None,
-    customized_detector_checkpoint: str | None = None,
-    customized_model_config: str | None = None,
-):
+) -> dict[str, dict]:
     """
     This funciton inferences a superanimal model on a set of images and saves the
     results as labeled images.
 
-    Parameters
-    ----------
-    superanimal_name: str
-        The name of the superanimal to analyze. Supported list:
+    Args:
+        superanimal_name: The name of the superanimal to analyze. Supported list:
+            - "superanimal_bird"
             - "superanimal_topviewmouse"
             - "superanimal_quadruped"
-    model_name: str
-        The name of the model to use for inference. Supported list:
-            - "hrnetw32"
-    images: str | Path | list[str] | list[Path]
-        The images to analyze. Can either be a directory containing images, or
-        a list of paths of images.
-    max_individuals: int
-        The maximum number of individuals to detect in each image.
-    out_folder: str
-        The directory where the labeled images will be saved.
-    bbox_threshold: float, default=0.1
-        The minimum confidence score to keep bounding box detections. Must be in (0, 1).
-        Only used when `customized_model_config=None` (otherwise, edit your
-        `customized_model_config` with the desired bbox_threshold).
-    progress_bar: bool
-        Whether to display a progress bar when running inference.
-    device: str | None
-        The device to use to run image analysis.
-    customized_pose_checkpoint: str | None
-        A customized SuperAnimal pose checkpoint, as an alternative to the Hugging Face
-        model SuperAnimal models.
-    customized_detector_checkpoint: str | None
-        A customized SuperAnimal detector checkpoint, as an alternative to the Hugging
-        Face SuperAnimal models.
-    customized_model_config: str | None
-        A customized SuperAnimal model config. This is for internal dev/testing use
-
-    Returns
-    -------
-    The predictions over the images
-
-    Examples
-    --------
-    >>> import deeplabcut
-    >>> from deeplabcut.pose_estimation_pytorch.apis.analyze_images import superanimal_analyze_images
-    >>> superanimal_name = "superanimal_quadruped"
-    >>> model_name = "hrnetw32"
-    >>> device = "cuda"
-    >>> max_individuals = 3
-    >>> test_images_folder = "test_rodent_images"
-    >>> out_images_folder = "vis_test_rodent_images"
-    >>> ret = superanimal_analyze_images(
-    >>>     superanimal_name,
-    >>>     model_name,
-    >>>     test_images_folder,
-    >>>     max_individuals,
-    >>>     out_images_folder
-    >>> )
-    """
-
-    os.makedirs(out_folder, exist_ok=True)
-
-    snapshot_path = None
-    detector_path = None
-    if customized_model_config is None:    
-        (
-            model_cfg,
-            project_config,
-            snapshot_path,
-            detector_path,
-        ) = get_config_model_paths(superanimal_name, model_name)
-
-        if "detector" in model_cfg:
-            model_cfg["detector"]["model"]["box_score_thresh"] = bbox_threshold
+        model_name: The name of the pose model architecture to use for inference.
+        detector_name: The name of the detector architecture to use for inference.
+        images: The images to analyze. Can either be a directory containing images, or
+            a list of paths of images.
+        max_individuals: The maximum number of individuals to detect in each image.
+        out_folder: The directory where the labeled images will be saved.
+        bbox_threshold: The minimum confidence score to keep bounding box detections.
+            Must be in (0, 1).
+        progress_bar: Whether to display a progress bar when running inference.
+        device: The device to use to run image analysis.
 
-        config = {**project_config, **model_cfg}
-        config = update_config(config, max_individuals, device)        
-    else:
-        config = read_config(customized_model_config)
-        if customized_pose_checkpoint is None:
-            raise ValueError(f"You must pass a custom checkpoint")
+    Returns:
+        The predictions for each image
+
+    Examples:
+        >>> import deeplabcut
+        >>> from deeplabcut.pose_estimation_pytorch.apis.analyze_images import (
+        >>>     superanimal_analyze_images
+        >>> )
+        >>> superanimal_name = "superanimal_quadruped"
+        >>> model_name = "hrnetw32"
+        >>> device = "cuda:0"
+        >>> max_individuals = 3
+        >>> test_images_folder = "test_rodent_images"
+        >>> out_images_folder = "vis_test_rodent_images"
+        >>> ret = superanimal_analyze_images(
+        >>>     superanimal_name,
+        >>>     model_name,
+        >>>     test_images_folder,
+        >>>     max_individuals,
+        >>>     out_images_folder
+        >>> )
+    """
+    out_folder = Path(out_folder)
+    out_folder.mkdir(exist_ok=True, parents=True)
 
-    if customized_pose_checkpoint is not None:
-        snapshot_path = customized_pose_checkpoint
-    if customized_detector_checkpoint is not None:
-        detector_path = customized_detector_checkpoint
+    snapshot_path = modelzoo.get_super_animal_snapshot_path(
+        dataset=superanimal_name, model_name=model_name,
+    )
+    detector_path = modelzoo.get_super_animal_snapshot_path(
+        dataset=superanimal_name, model_name=detector_name,
+    )
 
-    individuals = [f"animal{i}" for i in range(max_individuals)]
-    config["individuals"] = individuals
+    config = modelzoo.load_super_animal_config(
+        super_animal=superanimal_name,
+        model_name=model_name,
+        detector_name=detector_name,
+    )
+    config = update_config(config, max_individuals, device)
+    config["metadata"]["individuals"] = [f"animal{i}" for i in range(max_individuals)]
+    if "detector" in config:
+        config["detector"]["model"]["box_score_thresh"] = bbox_threshold
 
     predictions = analyze_image_folder(
         model_cfg=config,
@@ -352,13 +319,10 @@ def analyze_image_folder(
 
 
 def create_labeled_images_from_predictions(predictions, out_folder, cmap):
-
     for image_path, prediction in predictions.items():
-
         frame = auxfun_videos.imread(str(image_path), mode="skimage")
         fig, ax = plt.subplots()
         ax.imshow(frame)
-
         for idx, pose in enumerate(prediction["bodyparts"]):
             x, y, confidence = pose[:, 0], pose[:, 1], pose[:, 2]
             if np.sum(pose) < 0:
diff --git a/deeplabcut/pose_estimation_pytorch/apis/analyze_videos.py b/deeplabcut/pose_estimation_pytorch/apis/analyze_videos.py
index 4ddcb2abb..b53a3d515 100644
--- a/deeplabcut/pose_estimation_pytorch/apis/analyze_videos.py
+++ b/deeplabcut/pose_estimation_pytorch/apis/analyze_videos.py
@@ -91,19 +91,35 @@ def __next__(self) -> np.ndarray | tuple[str, dict[str, Any]]:
 
 
 def video_inference(
-    video_path: str | Path,
+    video: str | Path | VideoIterator,
     task: Task,
     pose_runner: InferenceRunner,
     detector_runner: InferenceRunner | None = None,
     with_identity: bool = False,
-    return_video_metadata: bool = False,
     cropping: list[int] | None = None,
 ) -> list[dict[str, np.ndarray]]:
-    """Runs inference on a video"""
-    video = VideoIterator(str(video_path), cropping=cropping)
+    """Runs inference on a video
+
+    Args:
+        video: The video to analyze
+        task: The pose task to run (bottom-up or top-down)
+        pose_runner: The pose runner to run inference with
+        detector_runner: When ``task==Task.TOP_DOWN``, the detector runner to obtain
+            bounding boxes for the video.
+        with_identity: Whether identity predictions should be made with the model.
+        cropping: Optionally, video inference can be run on a cropped version of the
+            video. To do so, pass a list containing 4 elements to specify which area
+            of the video should be analyzed: ``[xmin, xmax, ymin, ymax]``.
+
+    Returns:
+        Predictions for each frame in the video.
+    """
+    if not isinstance(video, VideoIterator):
+        video = VideoIterator(str(video), cropping=cropping)
+
     n_frames = video.get_n_frames()
     vid_w, vid_h = video.dimensions
-    print(f"Starting to analyze {video_path}")
+    print(f"Starting to analyze {video.video_path}")
     print(
         f"Video metadata: \n"
         f"  Overall # of frames:    {n_frames}\n"
@@ -111,11 +127,6 @@ def video_inference(
         f"  fps:                    {video.fps}\n"
         f"  resolution:             w={vid_w}, h={vid_h}\n"
     )
-    video_metadata = {
-        "n_frames": n_frames,
-        "fps": video.fps,
-        "resolution": (vid_w, vid_h),
-    }
 
     if task == Task.TOP_DOWN:
         # Get bounding boxes for context
@@ -147,9 +158,6 @@ def video_inference(
             f"video (tips on how to do that: {tip_url}{header})"
         )
 
-    if return_video_metadata:
-        return predictions, video_metadata
-
     return predictions
 
 
@@ -337,7 +345,7 @@ def analyze_videos(
         else:
             runtime = [time.time()]
             predictions = video_inference(
-                video_path=video,
+                video=video,
                 pose_runner=pose_runner,
                 task=pose_task,
                 detector_runner=detector_runner,
@@ -370,6 +378,7 @@ def analyze_videos(
                 pred_bodyparts=pred_bodyparts,
                 pred_unique_bodyparts=pred_unique_bodyparts,
                 cfg=cfg,
+                model_cfg=model_cfg,
                 dlc_scorer=dlc_scorer,
                 output_path=output_path,
                 output_prefix=output_prefix,
@@ -377,7 +386,7 @@ def analyze_videos(
             )
             results.append((str(video), df))
 
-            if cfg["multianimalproject"] and len(individuals) > 1:
+            if cfg["multianimalproject"]:
                 pred_bodypart_ids = None
                 if with_identity:
                     # reshape from (num_assemblies, num_bpts, num_individuals)
@@ -431,6 +440,7 @@ def create_df_from_prediction(
     pred_unique_bodyparts: np.ndarray | None,
     dlc_scorer: str,
     cfg: dict,
+    model_cfg: dict,
     output_path: str | Path,
     output_prefix: str | Path,
     save_as_csv: bool = False,
@@ -438,16 +448,18 @@ def create_df_from_prediction(
     output_h5 = Path(output_path) / f"{output_prefix}.h5"
     output_pkl = Path(output_path) / f"{output_prefix}_full.pickle"
 
+    bodyparts = model_cfg["metadata"]["bodyparts"]
+    unique_bodyparts = model_cfg["metadata"]["unique_bodyparts"]
+    individuals = model_cfg["metadata"]["individuals"]
+    n_individuals = len(individuals)
+    multianimal_project = cfg.get("multianimalproject")
+
     print(f"Saving results in {output_h5} and {output_pkl}")
-    cols = [
-        [dlc_scorer],
-        list(auxiliaryfunctions.get_bodyparts(cfg)),
-        ["x", "y", "likelihood"],
-    ]
+    coords = ["x", "y", "likelihood"]
+    cols = [[dlc_scorer], bodyparts, coords]
     cols_names = ["scorer", "bodyparts", "coords"]
-    individuals = cfg.get("individuals", ["animal"])
-    n_individuals = len(individuals)
-    if n_individuals > 1:
+
+    if multianimal_project:
         cols.insert(1, individuals)
         cols_names.insert(1, "individuals")
 
@@ -459,29 +471,10 @@ def create_df_from_prediction(
         index=range(len(pred_bodyparts)),
     )
     if pred_unique_bodyparts is not None:
-        coordinate_labels_unique = ["x", "y", "likelihood"]
-        if n_individuals > 1:
-            results_unique_df_index = pd.MultiIndex.from_product(
-                [
-                    [dlc_scorer],
-                    ['single'],
-                    auxiliaryfunctions.get_unique_bodyparts(cfg),
-                    coordinate_labels_unique,
-                ],
-                names=["scorer", "individuals", "bodyparts", "coords"],
-            )
-        else:
-            results_unique_df_index = pd.MultiIndex.from_product(
-                [
-                    [dlc_scorer],
-                    auxiliaryfunctions.get_unique_bodyparts(cfg),
-                    coordinate_labels_unique,
-                ],
-                names=["scorer", "bodyparts", "coords"],
-            )
+        unique_columns = [dlc_scorer], ['single'], unique_bodyparts, coords
         df_u = pd.DataFrame(
             pred_unique_bodyparts.reshape((len(pred_unique_bodyparts), -1)),
-            columns=results_unique_df_index,
+            columns=pd.MultiIndex.from_product(unique_columns, names=cols_names),
             index=range(len(pred_unique_bodyparts)),
         )
         df = df.join(df_u, how="outer")
@@ -573,6 +566,9 @@ def _generate_metadata(
         "training set fraction": train_fraction,
         "cropping": cropping is not None,
         "cropping_parameters": cropping_parameters,
+        "individuals": pytorch_config["metadata"]["individuals"],
+        "bodyparts": pytorch_config["metadata"]["bodyparts"],
+        "unique_bodyparts": pytorch_config["metadata"]["unique_bodyparts"],
     }
     return {"data": metadata}
 
diff --git a/deeplabcut/pose_estimation_pytorch/apis/convert_detections_to_tracklets.py b/deeplabcut/pose_estimation_pytorch/apis/convert_detections_to_tracklets.py
index f602aecc2..8d4a67729 100644
--- a/deeplabcut/pose_estimation_pytorch/apis/convert_detections_to_tracklets.py
+++ b/deeplabcut/pose_estimation_pytorch/apis/convert_detections_to_tracklets.py
@@ -195,7 +195,7 @@ def convert_detections2tracklets(
                     if single_detection is None:
                         continue
                     imindex = int(re.findall(r"\d+", image_name)[0])
-                    _single[imindex] = single_detection
+                    _single[imindex] = np.asarray(single_detection)
                 tracklets["single"].update(_single)
 
             if inference_cfg["topktoretain"] == 1:
@@ -204,7 +204,7 @@ def convert_detections2tracklets(
                     assemblies = ass.get(index)
                     if assemblies is None:
                         continue
-                    tracklets[0][image_name] = assemblies[0].data
+                    tracklets[0][image_name] = np.asarray(assemblies[0].data)
             else:
                 keep = set(multi_bpts).difference(ignore_bodyparts or [])
                 keep_inds = sorted(multi_bpts.index(bpt) for bpt in keep)
diff --git a/deeplabcut/pose_estimation_pytorch/apis/evaluate.py b/deeplabcut/pose_estimation_pytorch/apis/evaluate.py
index d26624d51..f2b3bfea6 100644
--- a/deeplabcut/pose_estimation_pytorch/apis/evaluate.py
+++ b/deeplabcut/pose_estimation_pytorch/apis/evaluate.py
@@ -20,6 +20,7 @@
 from tqdm import tqdm
 
 import deeplabcut.core.metrics as metrics
+from deeplabcut.core.weight_init import WeightInitialization
 from deeplabcut.pose_estimation_pytorch import utils
 from deeplabcut.pose_estimation_pytorch.apis.utils import (
     build_predictions_dataframe,
@@ -119,12 +120,11 @@ def evaluate(
         mode=mode,
         detector_runner=detector_runner,
     )
-    if "weight_init" in loader.model_cfg["train_settings"]:
-        weight_init_cfg = loader.model_cfg["train_settings"]["weight_init"]
-        if weight_init_cfg["memory_replay"]:
-            conversion_array = weight_init_cfg["conversion_array"]
-            for filename, pred in predictions.items():
-                pred["bodyparts"] = pred["bodyparts"][:, conversion_array]
+    if weight_init_cfg := loader.model_cfg["train_settings"].get("weight_init"):
+        weight_init = WeightInitialization.from_dict(weight_init_cfg)
+        if weight_init.memory_replay:
+            for _, pred in predictions.items():
+                pred["bodyparts"] = pred["bodyparts"][:, weight_init.conversion_array]
 
     poses = {filename: pred["bodyparts"] for filename, pred in predictions.items()}
 
@@ -197,17 +197,7 @@ def evaluate_snapshot(
     """
     pose_task = Task(loader.model_cfg.get("method", "bu"))
     parameters = loader.get_dataset_parameters()
-
-    if "weight_init" in loader.model_cfg["train_settings"]:
-        weight_init_cfg = loader.model_cfg["train_settings"]["weight_init"]
-        if weight_init_cfg["memory_replay"]:
-            conversion_array = weight_init_cfg["conversion_array"]
-            bodyparts = list(np.array(parameters.bodyparts)[conversion_array])
-            parameters = PoseDatasetParameters(
-                bodyparts, parameters.unique_bpts, parameters.individuals
-            )
-
-    pcutoff = cfg.get("pcutoff")
+    pcutoff = cfg.get("pcutoff", 0.6)
 
     detector_path = None
     if detector_snapshot is not None:
@@ -225,6 +215,16 @@ def evaluate_snapshot(
         detector_transform=None,
     )
 
+    # The project bodyparts might be different to the bodyparts the model was trained to
+    #  output, if the model is fine-tuned from SuperAnimal with memory replay.
+    #  For evaluation, we want to only use the project bodyparts
+    project_bodyparts = auxiliaryfunctions.get_bodyparts(cfg)
+    parameters = PoseDatasetParameters(
+        bodyparts=project_bodyparts,
+        unique_bpts=parameters.unique_bpts,
+        individuals=parameters.individuals
+    )
+
     predictions = {}
     scores = {
         "%Training dataset": loader.train_fraction,
@@ -308,7 +308,7 @@ def evaluate_snapshot(
 
 
 def evaluate_network(
-    config: str,
+    config: str | Path,
     shuffles: Iterable[int] = (1,),
     trainingsetindex: int | str = 0,
     snapshotindex: int | str | None = None,
diff --git a/deeplabcut/pose_estimation_pytorch/apis/train.py b/deeplabcut/pose_estimation_pytorch/apis/train.py
index fce693348..ff52c73f7 100644
--- a/deeplabcut/pose_estimation_pytorch/apis/train.py
+++ b/deeplabcut/pose_estimation_pytorch/apis/train.py
@@ -189,7 +189,7 @@ def train(
 
 
 def train_network(
-    config: str,
+    config: str | Path,
     shuffle: int = 1,
     trainingsetindex: int = 0,
     modelprefix: str = "",
@@ -233,7 +233,8 @@ def train_network(
         display_iters: overrides the number of iterations between each log of the loss
             within an epoch
         max_snapshots_to_keep: the maximum number of snapshots to save for each model
-        pose_threshold: used for memory-replay. pseudo predictions that are below this are discarded for memory-replay
+        pose_threshold: Used for memory-replay. Pseudo-predictions with confidence lower
+            than this threshold are discarded for memory-replay
         **kwargs : could be any entry of the pytorch_config dictionary. Examples are
             to see the full list see the pytorch_cfg.yaml file in your project folder
     """
@@ -246,30 +247,32 @@ def train_network(
 
     if weight_init_cfg := loader.model_cfg["train_settings"].get("weight_init"):
         weight_init = WeightInitialization.from_dict(weight_init_cfg)
-
         if weight_init.memory_replay:
-            dataset_params = loader.get_dataset_parameters()
-            backbone_name = loader.model_cfg["model"]["backbone"]["model_name"]
-            model_name = modelzoo_utils.get_pose_model_type(backbone_name)
-            # at some point train_network should support a different train_file passing
-            # so memory replay can also take the same train file
+            if weight_init.detector_snapshot_path is None:
+                raise ValueError(
+                    "When fine-tuning a SuperAnimal model with memory replay, a "
+                    "detector must be given as well so animals can be detected in "
+                    "images to obtain pseudo-labels. Please update your weight "
+                    "initialization so that `detector_snapshot_path` is not None."
+                )
 
             print("Preparing data for memory replay (this can take some time)")
+            dataset_params = loader.get_dataset_parameters()
             prepare_memory_replay(
-                loader.project_path,
-                shuffle,
+                config,
+                loader,
                 weight_init.dataset,
-                model_name,
+                weight_init.snapshot_path,
+                weight_init.detector_snapshot_path,
                 device,
                 train_file="train.json",
                 max_individuals=dataset_params.max_num_animals,
                 pose_threshold=pose_threshold,
-                customized_pose_checkpoint=weight_init.customized_pose_checkpoint,
             )
 
             print("Loading memory replay data")
             loader = COCOLoader(
-                project_root=Path(loader.model_folder).parent / "memory_replay",
+                project_root=loader.model_folder / "memory_replay",
                 model_config_path=loader.model_config_path,
                 train_json_filename="memory_replay_train.json",
             )
@@ -305,7 +308,6 @@ def train_network(
 
     # get the pose task
     pose_task = Task(loader.model_cfg.get("method", "bu"))
-    # We should allow people to set detector epochs to 0 if it was already trained. because they will most likely tune the pose estimator
     if (
         pose_task == Task.TOP_DOWN
         and loader.model_cfg["detector"]["train_settings"]["epochs"] > 0
diff --git a/deeplabcut/pose_estimation_pytorch/config/__init__.py b/deeplabcut/pose_estimation_pytorch/config/__init__.py
index 5a67d8711..2cf1468e6 100644
--- a/deeplabcut/pose_estimation_pytorch/config/__init__.py
+++ b/deeplabcut/pose_estimation_pytorch/config/__init__.py
@@ -10,6 +10,7 @@
 #
 from deeplabcut.pose_estimation_pytorch.config.make_pose_config import (
     make_pytorch_pose_config,
+    make_pytorch_test_config,
 )
 from deeplabcut.pose_estimation_pytorch.config.utils import (
     available_detectors,
diff --git a/deeplabcut/pose_estimation_pytorch/config/detectors/fasterrcnn_mobilenet_v3_large_fpn.yaml b/deeplabcut/pose_estimation_pytorch/config/detectors/fasterrcnn_mobilenet_v3_large_fpn.yaml
index ca9e12108..64fe3ab66 100644
--- a/deeplabcut/pose_estimation_pytorch/config/detectors/fasterrcnn_mobilenet_v3_large_fpn.yaml
+++ b/deeplabcut/pose_estimation_pytorch/config/detectors/fasterrcnn_mobilenet_v3_large_fpn.yaml
@@ -43,6 +43,6 @@ runner:
 train_settings:
   batch_size: 1
   dataloader_workers: 0
-  dataloader_pin_memory: true
+  dataloader_pin_memory: false
   display_iters: 500
   epochs: 250
diff --git a/deeplabcut/pose_estimation_pytorch/config/detectors/fasterrcnn_resnet50_fpn_v2.yaml b/deeplabcut/pose_estimation_pytorch/config/detectors/fasterrcnn_resnet50_fpn_v2.yaml
index 5bbb41695..73a3e8bde 100644
--- a/deeplabcut/pose_estimation_pytorch/config/detectors/fasterrcnn_resnet50_fpn_v2.yaml
+++ b/deeplabcut/pose_estimation_pytorch/config/detectors/fasterrcnn_resnet50_fpn_v2.yaml
@@ -43,6 +43,6 @@ runner:
 train_settings:
   batch_size: 1
   dataloader_workers: 0
-  dataloader_pin_memory: true
+  dataloader_pin_memory: false
   display_iters: 500
   epochs: 250
diff --git a/deeplabcut/pose_estimation_pytorch/config/detectors/ssdlite.yaml b/deeplabcut/pose_estimation_pytorch/config/detectors/ssdlite.yaml
index bd1989244..78b051058 100644
--- a/deeplabcut/pose_estimation_pytorch/config/detectors/ssdlite.yaml
+++ b/deeplabcut/pose_estimation_pytorch/config/detectors/ssdlite.yaml
@@ -42,6 +42,6 @@ runner:
 train_settings:
   batch_size: 16
   dataloader_workers: 0
-  dataloader_pin_memory: true
+  dataloader_pin_memory: false
   display_iters: 500
   epochs: 250
diff --git a/deeplabcut/pose_estimation_pytorch/config/make_pose_config.py b/deeplabcut/pose_estimation_pytorch/config/make_pose_config.py
index 804a4ed68..c5df7aa9f 100644
--- a/deeplabcut/pose_estimation_pytorch/config/make_pose_config.py
+++ b/deeplabcut/pose_estimation_pytorch/config/make_pose_config.py
@@ -21,6 +21,7 @@
     read_config_as_dict,
     replace_default_values,
     update_config,
+    write_config,
 )
 from deeplabcut.core.weight_init import WeightInitialization
 from deeplabcut.utils import auxiliaryfunctions, auxfun_multianimal
@@ -33,6 +34,7 @@ def make_pytorch_pose_config(
     top_down: bool = False,
     detector_type: str | None = None,
     weight_init: WeightInitialization | None = None,
+    save: bool = False,
 ) -> dict:
     """Creates a PyTorch pose configuration file for a DeepLabCut project
 
@@ -64,6 +66,7 @@ def make_pytorch_pose_config(
             detection model
         weight_init: Specify how model weights should be initialized. If None, ImageNet
             pretrained weights from Timm will be loaded when training.
+        save: Whether to save the model configuration file to the ``pose_config_path``.
 
     Returns:
         the PyTorch pose configuration file
@@ -117,12 +120,6 @@ def make_pytorch_pose_config(
 
     is_top_down = model_cfg.get("method", "BU").upper() == "TD"
     if is_top_down:
-        if weight_init is not None:
-            # FIXME(niels): We only have fasterrcnn_resnet50_fpn_v2 SuperAnimal weights.
-            #  This should be updated once more SuperAnimal detectors are uploaded,
-            #  so that users can choose which pre-trained detector they use.
-            detector_type = "fasterrcnn_resnet50_fpn_v2"
-
         model_cfg = add_detector(
             configs_dir,
             model_cfg,
@@ -177,7 +174,44 @@ def make_pytorch_pose_config(
         )
 
     # sort first-level keys to make it prettier
-    return dict(sorted(pose_config.items()))
+    pose_config = dict(sorted(pose_config.items()))
+
+    if save:
+        write_config(pose_config_path, pose_config, overwrite=True)
+
+    return pose_config
+
+
+def make_pytorch_test_config(
+    model_config: dict,
+    test_config_path: str | Path,
+    save: bool = False,
+) -> dict:
+    """Creates the test configuration for a model
+
+    Args:
+        model_config: The PyTorch config for the model.
+        test_config_path: The path of the test config
+        save: Whether to save the test config to ``test_config_path``.
+
+    Returns:
+        The test configuration file.
+    """
+    bodyparts = model_config["metadata"]["bodyparts"]
+    test_config = dict(
+        dataset=model_config["metadata"]["project_path"],
+        dataset_type="multi-animal-imgaug",  # required for downstream tracking
+        num_joints=len(bodyparts),
+        all_joints=[[i] for i in range(len(bodyparts))],
+        all_joints_names=bodyparts,
+        net_type=model_config["net_type"],
+        global_scale=1,
+        scoremap_dir="test",
+    )
+    if save:
+        write_config(test_config_path, test_config)
+
+    return test_config
 
 
 def add_metadata(
diff --git a/deeplabcut/pose_estimation_pytorch/data/base.py b/deeplabcut/pose_estimation_pytorch/data/base.py
index 346eaca6b..8d724dd10 100644
--- a/deeplabcut/pose_estimation_pytorch/data/base.py
+++ b/deeplabcut/pose_estimation_pytorch/data/base.py
@@ -27,7 +27,6 @@
     map_id_to_annotations,
 )
 from deeplabcut.pose_estimation_pytorch.task import Task
-from deeplabcut.utils import auxiliaryfunctions
 
 
 class Loader(ABC):
diff --git a/deeplabcut/pose_estimation_pytorch/models/detectors/base.py b/deeplabcut/pose_estimation_pytorch/models/detectors/base.py
index 1b1f9f131..198c14ed0 100644
--- a/deeplabcut/pose_estimation_pytorch/models/detectors/base.py
+++ b/deeplabcut/pose_estimation_pytorch/models/detectors/base.py
@@ -16,7 +16,6 @@
 import torch
 import torch.nn as nn
 
-import deeplabcut.pose_estimation_pytorch.modelzoo.utils as modelzoo_utils
 from deeplabcut.core.weight_init import WeightInitialization
 from deeplabcut.pose_estimation_pytorch.registry import build_from_cfg, Registry
 
@@ -41,16 +40,11 @@ def _build_detector(
     cfg["pretrained"] = pretrained
     detector: BaseDetector = build_from_cfg(cfg, **kwargs)
 
-    if weight_init is not None:
-        _, _, _, snapshot_path = modelzoo_utils.get_config_model_paths(
-            project_name=weight_init.dataset,
-            pose_model_type="hrnetw32",  # pose model does not matter here
-            detector_type="fasterrcnn",  # TODO: include variant
+    if weight_init is not None and weight_init.detector_snapshot_path is not None:
+        logging.info(
+            f"Loading detector checkpoint from {weight_init.detector_snapshot_path}"
         )
-        if weight_init.customized_detector_checkpoint is not None:
-            snapshot_path = weight_init.customized_detector_checkpoint
-        logging.info(f"Loading detector checkpoint from {snapshot_path}")
-        snapshot = torch.load(snapshot_path, map_location="cpu")
+        snapshot = torch.load(weight_init.detector_snapshot_path, map_location="cpu")
         detector.load_state_dict(snapshot["model"])
 
     return detector
diff --git a/deeplabcut/pose_estimation_pytorch/models/model.py b/deeplabcut/pose_estimation_pytorch/models/model.py
index c73ac3196..4f1ee94d0 100644
--- a/deeplabcut/pose_estimation_pytorch/models/model.py
+++ b/deeplabcut/pose_estimation_pytorch/models/model.py
@@ -16,7 +16,6 @@
 import torch
 import torch.nn as nn
 
-import deeplabcut.pose_estimation_pytorch.modelzoo.utils as modelzoo_utils
 from deeplabcut.core.weight_init import WeightInitialization
 from deeplabcut.pose_estimation_pytorch.models.backbones import BACKBONES, BaseBackbone
 from deeplabcut.pose_estimation_pytorch.models.criterions import (
@@ -189,22 +188,8 @@ def build(
 
         if weight_init is not None:
             logging.info(f"Loading pretrained model weights: {weight_init}")
-
-            # TODO: Should we specify the pose_model_type in WeightInitialization?
-            backbone_name = cfg["backbone"]["model_name"]
-            pose_model_type = modelzoo_utils.get_pose_model_type(backbone_name)
-
-            # load pretrained weights
-            if weight_init.customized_pose_checkpoint is None:
-                _, _, snapshot_path, _ = modelzoo_utils.get_config_model_paths(
-                    project_name=weight_init.dataset,
-                    pose_model_type=pose_model_type,
-                )
-            else:
-                snapshot_path = weight_init.customized_pose_checkpoint
-
-            logging.info(f"The pose model is loading from {snapshot_path}")
-            snapshot = torch.load(snapshot_path, map_location="cpu")
+            logging.info(f"The pose model is loading from {weight_init.snapshot_path}")
+            snapshot = torch.load(weight_init.snapshot_path, map_location="cpu")
             state_dict = snapshot["model"]
 
             # load backbone state dict
diff --git a/deeplabcut/pose_estimation_pytorch/modelzoo/__init__.py b/deeplabcut/pose_estimation_pytorch/modelzoo/__init__.py
index 117d12714..e8232cd89 100644
--- a/deeplabcut/pose_estimation_pytorch/modelzoo/__init__.py
+++ b/deeplabcut/pose_estimation_pytorch/modelzoo/__init__.py
@@ -8,3 +8,11 @@
 #
 # Licensed under GNU Lesser General Public License v3.0
 #
+from deeplabcut.pose_estimation_pytorch.modelzoo.utils import (
+    download_super_animal_snapshot,
+    get_snapshot_folder_path,
+    get_super_animal_model_config_path,
+    get_super_animal_project_config_path,
+    get_super_animal_snapshot_path,
+    load_super_animal_config,
+)
diff --git a/deeplabcut/pose_estimation_pytorch/modelzoo/_mmpose_to_dlc3.py b/deeplabcut/pose_estimation_pytorch/modelzoo/_mmpose_to_dlc3.py
deleted file mode 100644
index 85a18508e..000000000
--- a/deeplabcut/pose_estimation_pytorch/modelzoo/_mmpose_to_dlc3.py
+++ /dev/null
@@ -1,142 +0,0 @@
-#
-# DeepLabCut Toolbox (deeplabcut.org)
-# © A. & M.W. Mathis Labs
-# https://github.com/DeepLabCut/DeepLabCut
-#
-# Please see AUTHORS for contributors.
-# https://github.com/DeepLabCut/DeepLabCut/blob/main/AUTHORS
-#
-# Licensed under GNU Lesser General Public License v3.0
-#
-import argparse
-import json
-import os
-from pathlib import Path
-
-import torch
-
-from deeplabcut.pose_estimation_pytorch.apis.utils import build_pose_model
-from deeplabcut.pose_estimation_pytorch.models.model import PoseModel
-from deeplabcut.utils import auxiliaryfunctions
-
-
-def _map_modelzoo_to_dlc(state_dict: dict) -> dict:
-    """Map the model zoo weights to the DLC format
-    Args:
-        state_dict: the model zoo state dict
-    Returns:
-        the mapped state dict
-    """
-
-    updated_dict = {}
-    for k, v in state_dict.items():
-        parts = k.split(".")
-        if parts[0] == "backbone":
-            parts = [parts[0], "model", *parts[1:]]
-        elif parts[0] == "keypoint_head":
-            parts = ["heads", "bodypart", "heatmap_head", "model", parts[-1]]
-            if parts[-1] == "weight":
-                v = v.permute(1, 0, 2, 3)
-
-        updated_dict[".".join(parts)] = v
-    return updated_dict
-
-
-def map_modelzoo_to_dlc(
-    model_zoo_weights_path: str, device: str, pytorch_config: dict
-) -> PoseModel:
-    """Map the model zoo weights to the DLC format
-    Args:
-        model_zoo_weights_path: the path to the model zoo weights
-        device: the device to load the weights on
-        pytorch_config: the pytorch config to use for model building
-    Returns:
-        the mapped state dict
-    """
-    model_weights = torch.load(
-        str(model_zoo_weights_path), map_location=torch.device(device)
-    )
-    model = build_pose_model(pytorch_config)
-    mising_keys = model.load_state_dict(
-        _map_modelzoo_to_dlc(model_weights["state_dict"]), strict=False
-    )
-
-    assert len(mising_keys[1]) == 0
-
-    return model
-
-
-def _shift_category_ids(ann_files):
-    """Shift category ids to 1"""
-
-    for mode in ["train", "test"]:
-        with open(ann_files[mode], "r") as f:
-            data = json.load(f)
-
-        cleaned_data = data.copy()
-        for cat in cleaned_data["categories"]:
-            cat["id"] = 1
-
-        for ann in cleaned_data["annotations"]:
-            ann["category_id"] = 1
-
-        with open(ann_files[mode], "w") as f:
-            json.dump(cleaned_data, f)
-
-
-def _clean_result_json(results_files):
-    """Clean the json files"""
-
-    for mode in ["train", "test"]:
-        with open(results_files[mode], "r") as f:
-            data = json.load(f)
-
-        out_anns = []
-        for ann in data:
-            ann["score"] = ann["bbox_scores"][0]
-            out_anns.append(ann)
-
-        with open(results_files[mode], "w") as f:
-            json.dump(out_anns, f)
-
-
-def _change_path_annotations(ann_files, project_root):
-    for mode in ["train", "test"]:
-        with open(ann_files[mode], "r") as f:
-            data = json.load(f)
-        for i in range(len(data["images"])):
-            basename = os.path.basename(data["images"][i]["file_name"])
-            data["images"][i]["file_name"] = f"{project_root}/images/{basename}"
-
-
-def modify_annotations(project_root, ann_files):
-    _shift_category_ids(ann_files)
-    _change_path_annotations(ann_files, project_root)
-
-
-if __name__ == "__main__":
-    parser = argparse.ArgumentParser()
-    parser.add_argument("project_root")
-    parser.add_argument("pytorch_config_path")
-    parser.add_argument("model_zoo_weights_path")
-    parser.add_argument("--train_file", default="train.json")
-    parser.add_argument("--test_file", default="test.json")
-    parser.add_argument("--device", default="cpu")
-
-    args = parser.parse_args()
-    pytorch_config = auxiliaryfunctions.read_config(args.pytorch_config_path)
-    backbone_type = pytorch_config["model"]["backbone"]["model_name"]
-    save_path = (
-        f"{os.path.dirname(args.pytorch_config_path)}/checkpoints/{backbone_type}.pth"
-    )
-    annotatin_files = {
-        "train": Path(args.project_root) / "annotations" / args.test_file,
-        "test": Path(args.project_root) / "annotations" / args.train_file,
-    }
-
-    modify_annotations(args.project_root, annotatin_files)
-    model = map_modelzoo_to_dlc(
-        args.model_zoo_weights_path, args.device, pytorch_config
-    )
-
-    torch.save(model.state_dict(), save_path)
diff --git a/deeplabcut/pose_estimation_pytorch/modelzoo/config.py b/deeplabcut/pose_estimation_pytorch/modelzoo/config.py
index 47389bbb2..38234147e 100644
--- a/deeplabcut/pose_estimation_pytorch/modelzoo/config.py
+++ b/deeplabcut/pose_estimation_pytorch/modelzoo/config.py
@@ -11,20 +11,29 @@
 """Methods to create the configuration files to fine-tune SuperAnimal models"""
 from __future__ import annotations
 
+import os
+from pathlib import Path
+
+from ruamel.yaml import YAML
+
 import deeplabcut.pose_estimation_pytorch.config.utils as config_utils
-import deeplabcut.pose_estimation_pytorch.modelzoo.utils as modelzoo_utils
 import deeplabcut.utils.auxiliaryfunctions as af
-from deeplabcut.core.weight_init import WeightInitialization
-from pathlib import Path
 from deeplabcut.core.engine import Engine
-from ruamel.yaml import YAML
-import os
+from deeplabcut.core.weight_init import WeightInitialization
+from deeplabcut.pose_estimation_pytorch.modelzoo.utils import (
+    get_super_animal_model_config_path,
+    get_super_animal_project_config_path,
+)
+from deeplabcut.pose_estimation_pytorch.task import Task
+
 
 def make_super_animal_finetune_config(
     weight_init: WeightInitialization,
     project_config: dict,
-    pose_config_path: str,
-    net_type: str | None = None,
+    pose_config_path: str | Path,
+    model_name: str,
+    detector_name: str | None,
+    save: bool = False,
 ) -> dict:
     """
     Creates a PyTorch pose configuration file to finetune a SuperAnimal model on a
@@ -34,7 +43,10 @@ def make_super_animal_finetune_config(
         weight_init: The weight initialization configuration.
         project_config: The project configuration.
         pose_config_path: The path where the pose configuration file will be saved
-        net_type: The type of neural net to finetune.
+        model_name: The type of neural net to finetune.
+        detector_name: The type of detector to use for the SuperAnimal model. If None is
+            given, the model will be set to a Bottom-Up framework.
+        save: Whether to save the model configuration file to the ``pose_config_path``.
 
     Returns:
         The generated pose configuration file.
@@ -45,11 +57,18 @@ def make_super_animal_finetune_config(
             to create configuration files for transfer learning.
     """
     bodyparts = af.get_bodyparts(project_config)
+    if weight_init.dataset is None:
+        raise ValueError(
+            "You must set the ``WeightInitialization.dataset`` when fine-tuning "
+            "SuperAnimal models."
+        )
+
     if not weight_init.with_decoder:
         raise ValueError(
-            "Can call ``make_super_animal_finetune_config`` when `with_decoder=True`, "
-            f" but you had {weight_init}. Please set `with_decoder=True` to fine-tune "
-            "a model or call `make_pytorch_pose_config` to create a transfer learning "
+            "Can only call ``make_super_animal_finetune_config`` when "
+            f" `with_decoder=True`, but you had {weight_init}. Please set "
+            "`with_decoder=True` to fine-tune a model or call "
+            "`make_pytorch_pose_config` to create a transfer learning "
             "pose configuration file."
         )
 
@@ -69,29 +88,37 @@ def make_super_animal_finetune_config(
         )
 
     # Load the exact pose configuration file for the model to fine-tune
-    return create_config_from_modelzoo(
-        net_type=net_type,
+    pose_config = create_config_from_modelzoo(
         super_animal=weight_init.dataset,
+        model_name=model_name,
+        detector_name=detector_name,
         converted_bodyparts=converted_bodyparts,
         weight_init=weight_init,
         project_config=project_config,
         pose_config_path=pose_config_path,
     )
+    if save:
+        config_utils.write_config(pose_config_path, pose_config, overwrite=True)
+
+    return pose_config
 
 
 def create_config_from_modelzoo(
-    net_type: str,
     super_animal: str,
+    model_name: str,
+    detector_name: str | None,
     converted_bodyparts: list[str],
     weight_init: WeightInitialization,
     project_config: dict,
-    pose_config_path: str,
+    pose_config_path: str | Path,
 ) -> dict:
     """Creates a model configuration file to fine-tune a SuperAnimal model
 
     Args:
-        net_type: The type of neural net to finetune.
-        super_animal: The SuperAnimal model to finetune.
+        super_animal: The SuperAnimal dataset on which the model was trained.
+        model_name: The type of neural net to finetune.
+        detector_name: The type of detector to use for the SuperAnimal model. If None is
+            given, the model will be set to a Bottom-Up framework.
         converted_bodyparts: The project bodyparts that the model will learn.
         weight_init: The weight initialization to use.
         project_config: The project configuration.
@@ -100,54 +127,58 @@ def create_config_from_modelzoo(
     Returns:
         The generated pose configuration file.
     """
-    # load the SuperAnimal model config
-    pose_config, project_cfg, _, _ = modelzoo_utils.get_config_model_paths(
-        project_name=super_animal,
-        pose_model_type=modelzoo_utils.get_pose_model_type(net_type),
+    # load the model configuration
+    model_cfg = config_utils.read_config_as_dict(
+        get_super_animal_model_config_path(model_name)
     )
+    if detector_name is None:
+        model_cfg["method"] = Task.BOTTOM_UP.aliases[0].lower()
+        # Use default bottom-up image augmentation if no detector is given (the collate
+        # function might be needed).
+        config_dir = config_utils.get_config_folder_path()
+        aug = config_utils.read_config_as_dict(config_dir / "base" / "aug_default.yaml")
+        model_cfg["data"]["train"] = aug["train"]
+    else:
+        model_cfg["method"] = Task.TOP_DOWN.aliases[0].lower()
+        model_cfg["detector"] = config_utils.read_config_as_dict(
+            get_super_animal_model_config_path(detector_name)
+        )
 
     # use SuperAnimal bodyparts
     if weight_init.memory_replay:
-        converted_bodyparts = project_cfg["bodyparts"]
+        super_animal_project_config = config_utils.read_config_as_dict(
+            get_super_animal_project_config_path(super_animal)
+        )
+        converted_bodyparts = super_animal_project_config["bodyparts"]
 
-    pose_config["net_type"] = net_type
-    pose_config["metadata"] = {
+    model_cfg["net_type"] = model_name
+    model_cfg["metadata"] = {
         "project_path": project_config["project_path"],
-        "pose_config_path": pose_config_path,
+        "pose_config_path": str(pose_config_path),
         "bodyparts": converted_bodyparts,
         "unique_bodyparts": [],
         "individuals": project_config.get("individuals", ["animal"]),
         "with_identity": False,
     }
 
-    pose_config["model"] = config_utils.replace_default_values(
-        pose_config["model"], num_bodyparts=len(converted_bodyparts)
+    model_cfg["model"] = config_utils.replace_default_values(
+        model_cfg["model"], num_bodyparts=len(converted_bodyparts)
     )
-    pose_config["train_settings"]["weight_init"] = weight_init.to_dict()
+    model_cfg["train_settings"]["weight_init"] = weight_init.to_dict()
 
     # sort first-level keys to make it prettier
-    return dict(sorted(pose_config.items()))
+    return dict(sorted(model_cfg.items()))
 
-def write_pytorch_config_for_memory_replay(config_path,
-                                           shuffle,
-                                           pytorch_config):
 
+def write_pytorch_config_for_memory_replay(config_path, shuffle, pytorch_config):
     cfg = af.read_config(config_path)
-
-    trainIndex = 0    
-
+    trainIndex = 0
     dlc_proj_root = Path(config_path).parent
-    
     model_folder = dlc_proj_root / af.get_model_folder(
-        cfg['TrainingFraction'][trainIndex], shuffle, cfg, engine=Engine.PYTORCH)
-    
-
-    os.makedirs(model_folder / 'train', exist_ok = True)
-    
-    out_path = model_folder / 'train' / 'pytorch_config.yaml'
-
-
-    
-    with open(str(out_path), 'w') as f:
+        cfg["TrainingFraction"][trainIndex], shuffle, cfg, engine=Engine.PYTORCH
+    )
+    os.makedirs(model_folder / "train", exist_ok=True)
+    out_path = model_folder / "train" / "pytorch_config.yaml"
+    with open(str(out_path), "w") as f:
         yaml = YAML()
         yaml.dump(pytorch_config, f)
diff --git a/deeplabcut/pose_estimation_pytorch/modelzoo/inference.py b/deeplabcut/pose_estimation_pytorch/modelzoo/inference.py
index 681e32b8f..e97f50c71 100644
--- a/deeplabcut/pose_estimation_pytorch/modelzoo/inference.py
+++ b/deeplabcut/pose_estimation_pytorch/modelzoo/inference.py
@@ -15,21 +15,18 @@
 
 import numpy as np
 
-from deeplabcut.modelzoo.utils import get_superanimal_colormaps
+from deeplabcut.modelzoo.utils import get_super_animal_scorer, get_superanimal_colormaps
 from deeplabcut.pose_estimation_pytorch.apis.analyze_videos import (
     create_df_from_prediction,
     video_inference,
+    VideoIterator,
 )
 from deeplabcut.pose_estimation_pytorch.apis.utils import get_inference_runners
 from deeplabcut.pose_estimation_pytorch.modelzoo.utils import (
-    get_config_model_paths,
     raise_warning_if_called_directly,
-    select_device,
-    update_config,
 )
 from deeplabcut.pose_estimation_pytorch.task import Task
 from deeplabcut.utils.make_labeled_video import _create_labeled_video
-from deeplabcut.utils.auxiliaryfunctions import read_config
 
 
 class NumpyEncoder(json.JSONEncoder):
@@ -51,17 +48,17 @@ def construct_bodypart_names(max_individuals, bodyparts):
 
 def _video_inference_superanimal(
     video_paths: Union[str, list],
-    project_name: str,
-    model_name: str,
+    superanimal_name: str,
+    model_cfg: dict,
+    model_snapshot_path: str | Path,
+    detector_snapshot_path: str | Path | None,
     max_individuals: int,
     pcutoff: float,
     batch_size: int = 1,
     detector_batch_size: int = 1,
-    device: Optional[str] = None,
+    cropping: list[int] | None = None,
     dest_folder: Optional[str] = None,
-    customized_pose_checkpoint: Optional[str] = None,
-    customized_detector_checkpoint: Optional[str] = None,
-    customized_model_config: Optional[str] = None,
+    output_suffix: str = "",
 ) -> dict:
     """
     Perform inference on a video using a superanimal model from the model zoo specified by `superanimal_name`.
@@ -73,33 +70,24 @@ def _video_inference_superanimal(
     called directly. It is designed to be used by deeplabcut.modelzoo.api.video_inference.py
 
     Args:
-        video_paths: Path to the video to be analyzed or list of paths to videos to be analyzed
-
-        project_name: Name of the superanimal project (e.g. superanimal_quadruped)
-
-        model_name: Name of the model (e.g. hrnetw32)
-
+        video_paths: Path to the video to be analyzed or list of paths to videos to be
+            analyzed
+        superanimal_name: Name of the SuperAnimal project (e.g. superanimal_quadruped)
+        model_cfg: The name of the pose model architecture to use for inference.
+        model_snapshot_path: The path to the pose model snapshot to use for inference.
+        detector_snapshot_path: The path to the detector snapshot to use for inference.
         max_individuals: Maximum number of individuals in the video
-
-        pcutoff: Cutoff for cutting off the predicted keypoints with probability lower than pcutoff
-
-        device: The device on which to perform the operation.
-            If not specified, the device is automatically determined by the
-            `select_device` function. Defaults to None, which triggers
-            automatic device selection.
-
+        pcutoff: Cutoff for cutting off the predicted keypoints with probability lower
+            than pcutoff
         batch_size: The batch size to use for video inference.
-
+        cropping: List of cropping coordinates as [x1, x2, y1, y2]. Note that the same
+            cropping parameters will then be used for all videos. If different video
+            crops are desired, run ``video_inference_superanimal`` on individual videos
+            with the corresponding cropping coordinates.
         detector_batch_size: The batch size to use for the detector for video inference.
-
         dest_folder: Destination folder for the results. If not specified, the
             results are saved in the same folder as the video. Defaults to None.
-
-        customized_pose_checkpoint: A customized checkpoint to replace the default
-            SuperAnimal pose checkpoint
-
-        customized_detector_checkpoint: A customized checkpoint to replace the default
-            SuperAnimal detector checkpoint
+        output_suffix: The suffix to add to output file names (e.g. _before_adapt)
 
     Returns:
         results: Dictionary with the result pd.DataFrame for each video
@@ -108,51 +96,17 @@ def _video_inference_superanimal(
         Warning: If the function is called directly.
     """
     raise_warning_if_called_directly()
-
-    if device is None:
-        device = select_device()
-
-    pose_model_path = None
-    detector_path = None
-    if customized_model_config is None:
-        (
-            model_config,
-            project_config,
-            pose_model_path,
-            detector_path,
-        ) = get_config_model_paths(project_name, model_name)
-
-        config = {**project_config, **model_config}
-        config = update_config(config, max_individuals, device)
-    else:
-        config = read_config(customized_model_config)
-        config["bodyparts"] = config["metadata"]["bodyparts"]
-
-        if customized_pose_checkpoint is None:
-            raise ValueError(
-                "When specifying a `customized_model_config`, you must also specify "
-                "the `customized_pose_checkpoint` that goes with it."
-            )
-
-    if customized_pose_checkpoint is not None:
-        pose_model_path = customized_pose_checkpoint
-    if customized_detector_checkpoint is not None:
-        detector_path = customized_detector_checkpoint
-
-    individuals = [f"animal{i}" for i in range(max_individuals)]
-    config["individuals"] = individuals
-
     pose_runner, detector_runner = get_inference_runners(
-        config,
-        snapshot_path=pose_model_path,
+        model_config=model_cfg,
+        snapshot_path=model_snapshot_path,
         max_individuals=max_individuals,
-        num_bodyparts=len(config["bodyparts"]),
+        num_bodyparts=len(model_cfg["metadata"]["bodyparts"]),
         num_unique_bodyparts=0,
         batch_size=batch_size,
         detector_batch_size=detector_batch_size,
-        detector_path=detector_path,
+        detector_path=detector_snapshot_path,
     )
-    pose_task = Task(config.get("method", "BU"))
+    pose_task = Task(model_cfg.get("method", "BU"))
     results = {}
 
     if isinstance(video_paths, str):
@@ -167,73 +121,67 @@ def _video_inference_superanimal(
     for video_path in video_paths:
         print(f"Processing video {video_path}")
 
-        dlc_scorer = f"{project_name}_{model_name}"
+        dlc_scorer = get_super_animal_scorer(
+            superanimal_name, model_snapshot_path, detector_snapshot_path
+        )
 
         output_prefix = f"{Path(video_path).stem}_{dlc_scorer}"
         output_path = Path(dest_folder)
         output_h5 = Path(output_path) / f"{output_prefix}.h5"
 
-        # if there are no customized checkpoints passed, it's before adaptation
-        if (
-            customized_pose_checkpoint is None
-            and customized_detector_checkpoint is None
-        ):
-            output_json = str(output_h5).replace(".h5", "_before_adapt.json")
-        else:
-            output_json = str(output_h5).replace(".h5", "_after_adapt.json")
-        # also output json file so it's easier for video adaptation to handle
-
-        predictions, video_metadata = video_inference(
-            video_path,
+        output_json = output_h5.with_suffix(".json")
+        if len(output_suffix) > 0:
+            # str(output_h5).replace(".h5", "_before_adapt.json")
+            # str(output_h5).replace(".h5", "_after_adapt.json")
+            output_json = output_json.with_stem(output_h5.stem + output_suffix)
+
+        video = VideoIterator(video_path, cropping=cropping)
+        predictions = video_inference(
+            video,
             task=pose_task,
             pose_runner=pose_runner,
             detector_runner=detector_runner,
-            return_video_metadata=True,
         )
+
         pred_bodyparts = np.stack([p["bodyparts"][..., :3] for p in predictions])
         pred_unique_bodyparts = None
 
-        bbox = (0, video_metadata["resolution"][0], 0, video_metadata["resolution"][1])
+        bbox = cropping
+        if cropping is None:
+            vid_w, vid_h = video.dimensions
+            bbox = (0, vid_w, 0, vid_h)
+
         print(f"Saving results to {dest_folder}")
-        config["uniquebodyparts"] = []
-        config["multianimalbodyparts"] = config["bodyparts"]
 
         df = create_df_from_prediction(
             pred_bodyparts=pred_bodyparts,
             pred_unique_bodyparts=pred_unique_bodyparts,
             dlc_scorer=dlc_scorer,
-            cfg=config,
+            cfg=dict(multianimalproject=True),
+            model_cfg=model_cfg,
             output_path=output_path,
             output_prefix=output_prefix,
         )
 
         results[video_path] = df
-
         with open(output_json, "w") as f:
             json.dump(predictions, f, cls=NumpyEncoder)
 
-        if (
-            customized_pose_checkpoint is not None
-            and customized_detector_checkpoint is not None
-        ):
-            # FIXME: customized pose and customized detector passed does not mean it's adapted anymore
-            output_video = output_path / f"{output_prefix}_labeled_after_adapt.mp4"
-        else:
-            output_video = output_path / f"{output_prefix}_labeled.mp4"
+        output_video = output_path / f"{output_prefix}_labeled.mp4"
+        if len(output_suffix) > 0:
+            output_video = output_video.with_stem(output_video.stem + output_suffix)
 
         superanimal_colormaps = get_superanimal_colormaps()
-        colormap = superanimal_colormaps[project_name]
-
+        colormap = superanimal_colormaps[superanimal_name]
         _create_labeled_video(
             video_path,
             output_h5,
             pcutoff=pcutoff,
-            fps=video_metadata["fps"],
+            fps=video.fps,
             bbox=bbox,
             cmap=colormap,
             output_path=str(output_video),
         )
-
         print(f"Video with predictions was saved as {output_path}")
 
     return results
diff --git a/deeplabcut/pose_estimation_pytorch/modelzoo/memory_replay.py b/deeplabcut/pose_estimation_pytorch/modelzoo/memory_replay.py
index 1432c2d1e..002f74bb2 100644
--- a/deeplabcut/pose_estimation_pytorch/modelzoo/memory_replay.py
+++ b/deeplabcut/pose_estimation_pytorch/modelzoo/memory_replay.py
@@ -10,7 +10,6 @@
 #
 from __future__ import annotations
 
-import glob
 import json
 import os
 from collections import defaultdict
@@ -21,7 +20,6 @@
 from scipy.spatial import distance
 
 import deeplabcut.utils.auxiliaryfunctions as af
-from deeplabcut.core.engine import Engine
 from deeplabcut.core.weight_init import WeightInitialization
 from deeplabcut.modelzoo.generalized_data_converter.datasets import (
     COCOPoseDataset,
@@ -29,80 +27,42 @@
     SingleDLCPoseDataset,
 )
 from deeplabcut.pose_estimation_pytorch.apis.utils import get_inference_runners
-import deeplabcut.pose_estimation_pytorch.config.utils as config_utils
-from deeplabcut.pose_estimation_pytorch.modelzoo.utils import get_config_model_paths
-from deeplabcut.pose_estimation_pytorch.runners import InferenceRunner
+from deeplabcut.pose_estimation_pytorch.data.dlcloader import DLCLoader
+from deeplabcut.pose_estimation_pytorch.modelzoo import (
+    get_super_animal_project_config_path,
+)
 from deeplabcut.utils.pseudo_label import calculate_iou
 
 
-def get_superanimal_inference_runners(
-    superanimal_name: str,
-    model_name: str,
-    max_individuals: int,
-    device: str | None = None,
-) -> tuple[InferenceRunner, InferenceRunner | None]:
-    """Creates the inference runners for SuperAnimal models
-
-    Args:
-        superanimal_name: the name of the SuperAnimal dataset for which to load runners
-        model_name: the name of the model
-        max_individuals: the maximum number of individuals to detect per image
-        device: the device on which to run
-
-    Returns:
-        the pose runner for the SuperAnimal model
-        the detector runner for the SuperAnimal model, if it's a top-down model
-    """
-    (
-        model_config,
-        project_config,
-        pose_model_path,
-        detector_path,
-    ) = get_config_model_paths(superanimal_name, model_name)
-    model_config["metadata"]["individuals"] = [
-        f"animal{i}" for i in range(max_individuals)
-    ]
-    model_config = config_utils.replace_default_values(
-        model_config,
-        num_bodyparts=len(model_config["metadata"]["bodyparts"]),
-        num_individuals=max_individuals,
-        backbone_output_channels=model_config["model"]["backbone_output_channels"],
-    )
-    return get_inference_runners(
-        model_config,
-        snapshot_path=pose_model_path,
-        max_individuals=max_individuals,
-        num_bodyparts=len(model_config["metadata"]["bodyparts"]),
-        num_unique_bodyparts=0,
-        device=device,
-        detector_path=detector_path,
-    )
-
-
 def get_pose_predictions(
-    project_root: Path,
+    loader: DLCLoader,
     images: list[str],
     bboxes: dict[str, list],
     superanimal_name: str,
-    model_name: str,
+    model_snapshot_path: str | Path,
+    detector_snapshot_path: str | Path,
     max_individuals: int,
     device: str | None = None,
 ) -> dict[str, dict]:
     """Gets predictions made by a SuperAnimal model on a DeepLabCut project
 
     Args:
-        project_root: The path to the root of the project.
+        loader: The path to the root of the project.
         images: The images on which to run inference with the SuperAnimal model.
         bboxes: The ground truth bounding boxes for each image in the project.
-        superanimal_name: The name of the SuperAnimal dataset to use.
-        model_name: The name of the model to use.
+        superanimal_name: The name of the SuperAnimal dataset being used.
+        model_snapshot_path: The path to the SuperAnimal pose snapshot.
+        detector_snapshot_path: The path to the SuperAnimal detector snapshot.
         max_individuals: The maximum number of individuals to detect per image.
         device: The CUDA device to use.
 
     Returns:
         The predictions made by the SuperAnimal model on each image in the images list.
     """
-    predictions_folder = project_root / "memory_replay" / superanimal_name / model_name
+    model_name = detector_snapshot_path.stem + "-" + model_snapshot_path.stem
+    predictions_folder = (
+        loader.project_path / "memory_replay" / superanimal_name / model_name
+    )
     predictions_folder.mkdir(exist_ok=True, parents=True)
     predictions_file = predictions_folder / "pseudo-labels.json"
 
@@ -128,11 +88,14 @@ def get_pose_predictions(
     if len(images_to_process) == 0:
         return sa_predictions
 
-    pose_runner, detector_runner = get_superanimal_inference_runners(
-        superanimal_name,
-        model_name,
-        max_individuals,
+    pose_runner, detector_runner = get_inference_runners(
+        loader.model_cfg,
+        snapshot_path=model_snapshot_path,
+        max_individuals=max_individuals,
+        num_bodyparts=len(loader.model_cfg["metadata"]["bodyparts"]),
+        num_unique_bodyparts=len(loader.model_cfg["metadata"]["unique_bodyparts"]),
         device=device,
+        detector_path=detector_snapshot_path,
     )
 
     # FIXME(niels, yeshaokai) - Use the detector to combine GT-keypoint created bounding
@@ -140,7 +103,7 @@ def get_pose_predictions(
     # bbox_predictions = detector_runner.inference(images=images_to_process)
     pose_inputs = [
         (
-            project_root / Path(image),
+            str(loader.project_path / Path(image)),
             {"bboxes": np.array(bboxes[image])}
         )
         for image in images_to_process
@@ -167,28 +130,22 @@ def get_pose_predictions(
 
 # this is reading from a coco project
 def prepare_memory_replay_dataset(
-    project_root: str | Path,
+    loader: DLCLoader,
     source_dataset_folder: str | Path,
     superanimal_name: str,
-    model_name: str,
+    model_snapshot_path: str,
+    detector_snapshot_path: str,
     max_individuals: int = 1,
     train_file: str = "train.json",
     pose_threshold: float = 0.0,
     device: str | None = None,
-    customized_pose_checkpoint: str | None = None,
 ):
     """
     Need to first run inference on the source project train file
     """
-    project_root = Path(project_root).resolve()
+    project_root = loader.project_path.resolve()
     source_dataset_folder = Path(source_dataset_folder).resolve()
 
-    if customized_pose_checkpoint is not None:
-        print(
-            "memory replay fine-tuning pose checkpoint is replaced by",
-            customized_pose_checkpoint,
-        )
-
     # Contains the ground truth annotations for the DeepLabCut project
     # .../dlc-models-pytorch/.../...shuffle0/train/memory_replay/annotations/train.json
     with open(source_dataset_folder / "annotations" / train_file, "r") as f:
@@ -222,11 +179,12 @@ def prepare_memory_replay_dataset(
             image_id_to_annotations[image_id].append(annotation)
 
     image_name_to_prediction = get_pose_predictions(
-        project_root=project_root,
+        loader=loader,
         images=[image["file_name"] for image in project_gt["images"]],
         bboxes=image_name_to_bbox,
         superanimal_name=superanimal_name,
-        model_name=model_name,
+        model_snapshot_path=model_snapshot_path,
+        detector_snapshot_path=detector_snapshot_path,
         max_individuals=max_individuals,
         device=device,
     )
@@ -304,45 +262,52 @@ def optimal_match(gts_list, preds_list):
 
 
 def prepare_memory_replay(
-    dlc_proj_root: str | Path,
-    shuffle: int,
+    config: str | Path,
+    loader: DLCLoader,
     superanimal_name: str,
-    model_name: str,
+    model_snapshot_path: str | Path,
+    detector_snapshot_path: str | Path,
     device: str,
     max_individuals: int = 3,
     train_file: str = "train.json",
     pose_threshold: float = 0.1,
-    customized_pose_checkpoint: str | None = None,
-):
-    """TODO: Documentation"""
+) -> None:
+    """Prepares a shuffle to be trained with memory replay.
 
-    # in order to fill the num_bodyparts stuff
+    To be trained using memory replay, predictions must be made on all images in the
+    dataset using the SuperAnimal model. Predictions for bodyparts that aren't labeled
+    in the DeepLabCut project are then used as pseudo-labels during training.
 
-    config_path = Path(dlc_proj_root, "config.yaml")
-    cfg = af.read_config(config_path)
+    This method will create a COCO-format dataset in the same folder as the
+    ``pytorch_config.yaml`` (the model folder).
+
+    Args:
+        config: Path to the DeepLabCut project configuration file.
+        loader: The loader used to load the training/test data on which a model will
+            be fine-tuned with memory replay.
+        superanimal_name: The name of the SuperAnimal model that is being fine-tuned.
+        model_snapshot_path: Path to the SuperAnimal pose snapshot to fine-tune.
+        detector_snapshot_path: Path to the SuperAnimal detector snapshot to fine-tune.
+        device: Device to use to run inference using the SuperAnimal model.
+        max_individuals: Maximum number of animals that can be present in a frame.
+        train_file: Name of the file containing train annotations (e.g. `train.json`).
+        pose_threshold: The minimum score for a prediction to be used as a pseudo-label.
+    """
+    cfg = af.read_config(config)
+    super_animal_cfg = af.read_plainconfig(
+        get_super_animal_project_config_path(super_animal=superanimal_name)
+    )
 
     if "individuals" in cfg:
         temp_dataset = MaDLCPoseDataset(
-            str(dlc_proj_root), "temp_dataset", shuffle=shuffle
+            str(loader.project_path), "temp_dataset", shuffle=loader.shuffle
         )
     else:
         temp_dataset = SingleDLCPoseDataset(
-            str(dlc_proj_root), "temp_dataset", shuffle=shuffle
+            str(loader.project_path), "temp_dataset", shuffle=loader.shuffle
         )
 
-    dlc_proj_root = Path(dlc_proj_root)
-    config_path = dlc_proj_root / "config.yaml"
-
-    cfg = af.read_config(config_path)
-
-    trainIndex = 0
-
-    model_folder = dlc_proj_root / af.get_model_folder(
-        cfg["TrainingFraction"][trainIndex], shuffle, cfg, engine=Engine.PYTORCH
-    )
-
-    memory_replay_folder = model_folder / "memory_replay"
-
+    memory_replay_folder = loader.model_folder / "memory_replay"
     temp_dataset.materialize(
         memory_replay_folder,
         framework="coco",
@@ -350,11 +315,7 @@ def prepare_memory_replay(
         no_image_copy=True,  # use the images in the labeled-data folder
     )
 
-    original_model_config = af.read_config(
-        str(model_folder / "train" / "pytorch_config.yaml")
-    )
-
-    weight_init_cfg = original_model_config["train_settings"].get("weight_init")
+    weight_init_cfg = loader.model_cfg["train_settings"].get("weight_init")
     if weight_init_cfg is None:
         raise ValueError(
             "You can only train models with memory replay when you are fine-tuning a "
@@ -373,11 +334,23 @@ def prepare_memory_replay(
         )
 
     dataset = COCOPoseDataset(memory_replay_folder, "memory_replay_dataset")
-    conversion_table_path = dlc_proj_root / "memory_replay" / "conversion_table.csv"
 
     # here we project the original DLC projects to superanimal space and save them into
     # a coco project format
-    dataset.project_with_conversion_table(str(conversion_table_path))
+    bodyparts = af.get_bodyparts(cfg)
+    sa_bodyparts = af.get_bodyparts(super_animal_cfg)
+    conversion_table = {}
+    for idx, bpt in enumerate(bodyparts):
+        conversion_table[bpt] = sa_bodyparts[weight_init.conversion_array[idx]]
+
+    dataset.project_with_conversion_table(
+        table_path=None,
+        table_dict=dict(
+            master_keypoints=sa_bodyparts,
+            conversion_table=conversion_table,
+        ),
+    )
+
     dataset.materialize(
         memory_replay_folder, framework="coco", deepcopy=False, no_image_copy=True,
     )
@@ -385,13 +358,13 @@ def prepare_memory_replay(
     # then in this function, we do pseudo label to match prediction and gts to create
     # memory-replay dataset that will be named memory_replay_train.json
     prepare_memory_replay_dataset(
-        dlc_proj_root,
+        loader,
         memory_replay_folder,
         superanimal_name,
-        model_name,
+        model_snapshot_path,
+        detector_snapshot_path,
         max_individuals=max_individuals,
         device=device,
         train_file=train_file,
         pose_threshold=pose_threshold,
-        customized_pose_checkpoint=customized_pose_checkpoint,
     )
diff --git a/deeplabcut/pose_estimation_pytorch/modelzoo/train_from_coco.py b/deeplabcut/pose_estimation_pytorch/modelzoo/train_from_coco.py
index 14d3884a5..847be50f8 100644
--- a/deeplabcut/pose_estimation_pytorch/modelzoo/train_from_coco.py
+++ b/deeplabcut/pose_estimation_pytorch/modelzoo/train_from_coco.py
@@ -36,7 +36,7 @@ def adaptation_train(
     detector_path: str | None,
     batch_size: int = 8,
     detector_batch_size: int = 8,
-    eval_interval: int = 1,
+    eval_interval: int | None = None,
 ):
     setup_file_logging(Path(model_folder) / "log.txt")
     loader = COCOLoader(
diff --git a/deeplabcut/pose_estimation_pytorch/modelzoo/utils.py b/deeplabcut/pose_estimation_pytorch/modelzoo/utils.py
index 0a265f4f8..84e13cded 100644
--- a/deeplabcut/pose_estimation_pytorch/modelzoo/utils.py
+++ b/deeplabcut/pose_estimation_pytorch/modelzoo/utils.py
@@ -9,7 +9,6 @@
 # Licensed under GNU Lesser General Public License v3.0
 #
 import inspect
-import os
 import subprocess
 import warnings
 from pathlib import Path
@@ -22,63 +21,128 @@
 from deeplabcut.utils import auxiliaryfunctions
 
 
-def get_config_model_paths(
-    project_name: str,
-    pose_model_type: str,
-    detector_type: str = "fasterrcnn",
-    weight_folder: str = None,
-):
-    """Get the paths to the model and project configs
+def get_model_configs_folder_path() -> Path:
+    """Returns: the folder containing the SuperAnimal model configuration files"""
+    return Path(auxiliaryfunctions.get_deeplabcut_path()) / "modelzoo" / "model_configs"
+
+
+def get_project_configs_folder_path() -> Path:
+    """Returns: the folder containing the SuperAnimal project configuration files"""
+    return (
+        Path(auxiliaryfunctions.get_deeplabcut_path()) / "modelzoo" / "project_configs"
+    )
+
+
+def get_snapshot_folder_path() -> Path:
+    """Returns: the path to the folder containing the SuperAnimal model snapshots"""
+    return Path(auxiliaryfunctions.get_deeplabcut_path()) / "modelzoo" / "checkpoints"
+
+
+def get_super_animal_model_config_path(model_name: str) -> Path:
+    """Gets the path to the configuration file for a SuperAnimal model.
 
     Args:
-        project_name: the name of the project
-        pose_model_type: the name of the pose model
-        detector_type: the type of the detector
-        weight_folder: the folder containing the weights
+        model_name: The name of the model for which to get the path.
 
     Returns:
-        the paths to the models and project configs
+        The path to the config file for a SuperAnimal model.
     """
-    dlc_root_path = auxiliaryfunctions.get_deeplabcut_path()
-    modelzoo_path = os.path.join(dlc_root_path, "modelzoo")
+    return get_model_configs_folder_path() / f"{model_name}.yaml"
 
-    model_cfg_path = os.path.join(
-        modelzoo_path, "model_configs", f"{pose_model_type}.yaml"
-    )
-    model_config = auxiliaryfunctions.read_plainconfig(model_cfg_path)
-    project_config = auxiliaryfunctions.read_config(
-        os.path.join(modelzoo_path, "project_configs", f"{project_name}.yaml")
-    )
 
+def get_super_animal_project_config_path(super_animal: str) -> Path:
+    """Gets the path to a SuperAnimal project configuration file.
+
+    Args:
+        super_animal: The name of the SuperAnimal for which to get the config path.
+
+    Returns:
+        The path to the config file for a SuperAnimal project.
+    """
+    return get_project_configs_folder_path() / f"{super_animal}.yaml"
+
+
+def get_super_animal_snapshot_path(
+    dataset: str,
+    model_name: str,
+    download: bool = True,
+) -> Path:
+    """Gets the path to the snapshot containing SuperAnimal model weights.
+
+    Args:
+        dataset: The name of the SuperAnimal dataset.
+        model_name: The name of the model.
+        download: Whether to download the weights if they aren't already there.
+
+    Returns:
+        The path to the weights for a SuperAnimal model.
+    """
+    model_path = get_snapshot_folder_path() / f"{dataset}_{model_name}.pt"
+    if download and not model_path.exists():
+        download_super_animal_snapshot(dataset, model_name)
+
+    return model_path
+
+
+def load_super_animal_config(
+    super_animal: str,
+    model_name: str,
+    detector_name: str | None = None,
+    max_individuals: int = 30,
+    device: str | None = None,
+) -> dict:
+    """Loads the model configuration file for a model, detector and SuperAnimal
+
+    Args:
+        super_animal: The name of the SuperAnimal for which to create the model config.
+        model_name: The name of the model for which to create the model config.
+        detector_name: The name of the detector for which to create the model config.
+        max_individuals: The maximum number of detections to make in an image
+        device: The device to use to train/run inference on the model
+
+    Returns:
+        The model configuration for a SuperAnimal-pretrained model.
+    """
+    project_cfg_path = get_super_animal_project_config_path(super_animal=super_animal)
+    project_config = config_utils.read_config_as_dict(project_cfg_path)
+
+    model_cfg_path = get_super_animal_model_config_path(model_name=model_name)
+    model_config = config_utils.read_config_as_dict(model_cfg_path)
     model_config = add_metadata(project_config, model_config, model_cfg_path)
-    if weight_folder is None:
-        weight_folder = os.path.join(modelzoo_path, "checkpoints")
-
-    # FIXME - DO NOT DOWNLOAD HERE
-    pose_model_name = f"{project_name}_{pose_model_type}.pth"
-    pose_model_path = os.path.join(weight_folder, pose_model_name)
-    detector_name = f"{project_name}_{detector_type}.pt"
-    detector_model_path = os.path.join(weight_folder, detector_name)
-    if not (Path(pose_model_path).exists() and Path(detector_model_path).exists()):
-        download_huggingface_model(
-            f"{project_name}_{pose_model_type}",
-            target_dir=str(weight_folder),
-            rename_mapping={
-                "pose_model.pth": pose_model_name,
-                "detector.pt": detector_name,
-            },
-        )
+    model_config = update_config(model_config, max_individuals, device)
 
-    # FIXME: Needed due to changes in code - remove when new snapshots are uploaded
-    pose_model_path = _parse_model_snapshot(Path(pose_model_path), device="cpu")
-    detector_model_path = _parse_model_snapshot(Path(detector_model_path), device="cpu")
+    if detector_name is None:
+        model_config["method"] = "BU"
+    else:
+        detector_cfg_path = get_super_animal_model_config_path(model_name=detector_name)
+        detector_cfg = config_utils.read_config_as_dict(detector_cfg_path)
+        model_config["method"] = "TD"
+        model_config["detector"] = detector_cfg
+    return model_config
 
-    return (
-        model_config,
-        project_config,
-        pose_model_path,
-        detector_model_path,
-    )
+
+def download_super_animal_snapshot(dataset: str, model_name: str) -> Path:
+    """Downloads a SuperAnimal snapshot
+
+    Args:
+        dataset: The name of the SuperAnimal dataset for which to download a snapshot.
+        model_name: The name of the model for which to download a snapshot.
+
+    Returns:
+        The path to the downloaded snapshot.
+
+    Raises:
+        RuntimeError if the model fails to download.
+    """
+    snapshot_dir = get_snapshot_folder_path()
+    model_name = f"{dataset}_{model_name}"
+    model_path = snapshot_dir / f"{model_name}.pt"
+
+    download_huggingface_model(model_name, target_dir=str(snapshot_dir))
+    if not model_path.exists():
+        raise RuntimeError(f"Failed to download {model_name} to {model_path}")
+
+    return snapshot_dir / f"{model_name}.pt"
 
 
 def get_gpu_memory_map():
@@ -112,58 +176,27 @@ def raise_warning_if_called_directly():
         )
 
 
-def update_config(config, max_individuals, device):
+def update_config(config: dict, max_individuals: int, device: str):
+    """Loads the model configuration file for a model, detector and SuperAnimal
+
+    Args:
+        config: The default model configuration file.
+        max_individuals: The maximum number of detections to make in an image
+        device: The device to use to train/run inference on the model
+
+    Returns:
+        The model configuration for a SuperAnimal-pretrained model.
+    """
     config = config_utils.replace_default_values(
         config,
-        num_bodyparts=len(config["bodyparts"]),
+        num_bodyparts=len(config["metadata"]["bodyparts"]),
         num_individuals=max_individuals,
         backbone_output_channels=config["model"]["backbone_output_channels"],
     )
-    config["device"] = device
-    config_utils.pretty_print(config)
-    return config
+    config["metadata"]["individuals"] = [f"animal{i}" for i in range(max_individuals)]
 
+    config["device"] = device
+    if "detector" in config:
+        config["detector"]["device"] = device
 
-def _parse_model_snapshot(base: Path, device: str, print_keys: bool = False) -> Path:
-    """FIXME: A new snapshot should be uploaded and used"""
-
-    def _map_model_keys(state_dict: dict) -> dict:
-        updated_dict = {}
-        for k, v in state_dict.items():
-            if not (
-                k.startswith("backbone.model.downsamp_modules.")
-                or k.startswith("backbone.model.final_layer")
-                or k.startswith("backbone.model.classifier")
-            ):
-                parts = k.split(".")
-                if parts[:4] == ["heads", "bodypart", "heatmap_head", "model"]:
-                    parts[3] = "deconv_layers.0"
-                updated_dict[".".join(parts)] = v
-        return updated_dict
-
-    parsed = base.with_stem(base.stem + "_parsed")
-    if not parsed.exists():
-        snapshot = torch.load(base, map_location=device)
-        if print_keys:
-            print(5 * "-----\n")
-            print(base.stem + " keys")
-            for name, _ in snapshot["model_state_dict"].items():
-                print(f"  * {name}")
-            print()
-
-        parsed_model_snapshot = {
-            "model": _map_model_keys(snapshot["model_state_dict"]),
-            "metadata": {"epoch": 0},
-        }
-        torch.save(parsed_model_snapshot, parsed)
-    return parsed
-
-
-def get_pose_model_type(backbone: str) -> str:
-    """Temporary fix: pose_model_types for SuperAnimal models do not match net types"""
-    if backbone.startswith("resnet"):
-        return backbone
-    elif backbone.startswith("hrnet"):
-        return backbone.replace("_", "")
-
-    raise ValueError(f"Unknown backbone for SuperAnimal Weights")
+    return config
diff --git a/deeplabcut/pose_estimation_tensorflow/modelzoo/api/spatiotemporal_adapt.py b/deeplabcut/pose_estimation_tensorflow/modelzoo/api/spatiotemporal_adapt.py
index 8fb03bfda..76ed89731 100644
--- a/deeplabcut/pose_estimation_tensorflow/modelzoo/api/spatiotemporal_adapt.py
+++ b/deeplabcut/pose_estimation_tensorflow/modelzoo/api/spatiotemporal_adapt.py
@@ -155,7 +155,7 @@ def before_adapt_inference(self, make_video=False, **kwargs):
                 filtered=False,
                 init_weights=self.init_weights,
                 draw_skeleton=True,
-                superanimal_name=self.supermodel_name,
+                superanimal_name=self.project_name,
                 **kwargs,
             )
 
@@ -245,6 +245,6 @@ def after_adapt_inference(self, **kwargs):
             filtered=False,
             init_weights=adapt_weights,
             draw_skeleton=True,
-            superanimal_name=self.supermodel_name,
+            superanimal_name=self.project_name,
             **kwargs,
         )
diff --git a/deeplabcut/pose_estimation_tensorflow/modelzoo/api/superanimal_inference.py b/deeplabcut/pose_estimation_tensorflow/modelzoo/api/superanimal_inference.py
index d318b7487..a22f93c82 100644
--- a/deeplabcut/pose_estimation_tensorflow/modelzoo/api/superanimal_inference.py
+++ b/deeplabcut/pose_estimation_tensorflow/modelzoo/api/superanimal_inference.py
@@ -19,10 +19,6 @@
 import imgaug.augmenters as iaa
 import numpy as np
 import pandas as pd
-from dlclibrary.dlcmodelzoo.modelzoo_download import (
-    download_huggingface_model,
-    MODELOPTIONS,
-)
 from skimage.util import img_as_ubyte
 from tqdm import tqdm
 
diff --git a/deeplabcut/utils/make_labeled_video.py b/deeplabcut/utils/make_labeled_video.py
index fdef8ae04..828132d65 100644
--- a/deeplabcut/utils/make_labeled_video.py
+++ b/deeplabcut/utils/make_labeled_video.py
@@ -44,6 +44,7 @@
 from skimage.util import img_as_ubyte
 from tqdm import trange
 
+from deeplabcut.core.engine import Engine
 from deeplabcut.utils import auxfun_multianimal, auxiliaryfunctions, visualization
 from deeplabcut.utils.auxfun_videos import VideoWriter
 from deeplabcut.utils.video_processor import (
@@ -566,18 +567,48 @@ def create_labeled_video(
     if config == "":
         if pcutoff is None:
             pcutoff = 0.6
+
+        individuals = [""]
+        uniquebodyparts = []
     else:
         cfg = auxiliaryfunctions.read_config(config)
-        trainFraction = cfg["TrainingFraction"][trainingsetindex]
+        train_fraction = cfg["TrainingFraction"][trainingsetindex]
         track_method = auxfun_multianimal.get_track_method(
             cfg, track_method=track_method
         )
         if pcutoff is None:
             pcutoff = cfg["pcutoff"]
 
+        # Get individuals from the config
+        individuals = cfg.get("individuals", [""])
+        uniquebodyparts = cfg.get("uniquebodyparts", [])
+
+        # Only for PyTorch engine - check if the shuffle was fine-tuned from a
+        #  SuperAnimal model with memory replay -> SuperAnimal bodyparts must be used
+        model_folder = auxiliaryfunctions.get_model_folder(
+            train_fraction,
+            shuffle,
+            cfg,
+            modelprefix,
+            engine=Engine.PYTORCH,
+        )
+        model_config_path = (
+            Path(config).parent / model_folder / "train" / Engine.PYTORCH.pose_cfg_name
+        )
+        if model_config_path.exists():
+            model_config = auxiliaryfunctions.read_plainconfig(str(model_config_path))
+            if (
+                model_config["train_settings"]
+                .get("weight_init", {})
+                .get("memory_replay", False)
+            ):
+                superanimal_name = model_config["train_settings"]["weight_init"][
+                    "dataset"
+                ]
+
     if init_weights == "":
         DLCscorer, DLCscorerlegacy = auxiliaryfunctions.get_scorer_name(
-            cfg, shuffle, trainFraction, modelprefix=modelprefix
+            cfg, shuffle, train_fraction, modelprefix=modelprefix
         )  # automatically loads corresponding model (even training iteration based on snapshot index)
     else:
         DLCscorer = "DLC_" + Path(init_weights).stem
@@ -593,13 +624,12 @@ def create_labeled_video(
 
     if superanimal_name != "":
         dlc_root_path = auxiliaryfunctions.get_deeplabcut_path()
-        dataset_name = "_".join(superanimal_name.split("_")[:-1])
         test_cfg = auxiliaryfunctions.read_plainconfig(
             os.path.join(
                 dlc_root_path,
                 "modelzoo",
                 "project_configs",
-                f"{dataset_name}.yaml",
+                f"{superanimal_name}.yaml",
             )
         )
 
@@ -611,6 +641,10 @@ def create_labeled_video(
             "dotsize": dotsize,
             "alphavalue": alphavalue,
             "colormap": colormap,
+            "bodyparts": bodyparts,
+            "multianimalbodyparts": bodyparts,
+            "individuals": individuals,
+            "uniquebodyparts": uniquebodyparts,
         }
     else:
         bodyparts = (
diff --git a/deeplabcut/utils/pseudo_label.py b/deeplabcut/utils/pseudo_label.py
index c14f2c630..0f9af1989 100644
--- a/deeplabcut/utils/pseudo_label.py
+++ b/deeplabcut/utils/pseudo_label.py
@@ -23,18 +23,14 @@
 from scipy.spatial import distance
 from scipy.spatial.distance import cdist
 
+import deeplabcut.pose_estimation_pytorch.modelzoo as modelzoo
 import deeplabcut.utils.auxiliaryfunctions as af
-from deeplabcut.core.engine import Engine
 from deeplabcut.modelzoo.generalized_data_converter.datasets import (
-    COCOPoseDataset,
     MaDLCDataFrame,
-    MaDLCPoseDataset,
     SingleDLCDataFrame,
-    SingleDLCPoseDataset,
 )
 from deeplabcut.pose_estimation_pytorch.apis.utils import get_inference_runners
 from deeplabcut.pose_estimation_pytorch.modelzoo.utils import (
-    get_config_model_paths,
     select_device,
     update_config,
 )
@@ -100,7 +96,7 @@ def calculate_iou(box1, box2):
     return iou
 
 
-def video_to_frames(input_video, output_folder):
+def video_to_frames(input_video, output_folder, cropping: list[int] | None = None):
     # Create the output folder if it doesn't exist
     video = cv2.VideoCapture(str(input_video))
     # Get the frames per second (fps) of the video
@@ -113,6 +109,11 @@ def video_to_frames(input_video, output_folder):
         # Break the loop if we have reached the end of the video
         if not ret:
             break
+        # Crop the frame if desired
+        if cropping is not None:
+            x1, x2, y1, y2 = cropping
+            frame = frame[y1:y2, x1:x2]
+
         # Save the frame as an image file.
         frame_str = str(frame_count).zfill(5)
         frame_file = os.path.join(output_folder, "images", f"frame_{frame_str}.png")
@@ -149,6 +150,7 @@ def keypoint_matching(
     config_path: str | Path,
     superanimal_name: str,
     model_name: str,
+    detector_name: str,
     copy_images: bool = False,
     device: str | None = None,
     train_file: str = "train.json",
@@ -161,7 +163,10 @@ def keypoint_matching(
     Args:
         config_path: The path of the DeepLabCut project configuration file.
         superanimal_name: SuperAnimal dataset with which to run keypoint matching.
-        model_name: SuperAnimal model with which to run keypoint matching
+        model_name: Name of the SuperAnimal pose model architecture with which to run
+            keypoint matching
+        detector_name: Name of the SuperAnimal detector architecture with which to run
+            keypoint matching
         copy_images: When False, symlinks are created for the dataset used for keypoint
             matching. Otherwise, images are copied from the `labeled-data` folder to the
             folder used for keypoint matching.
@@ -184,29 +189,33 @@ def keypoint_matching(
         str(memory_replay_folder), framework="coco", deepcopy=copy_images
     )
 
-    # inferencing the train set
-    (
-        model_config,
-        project_config,
-        pose_model_path,
-        detector_path,
-    ) = get_config_model_paths(superanimal_name, model_name)
-
+    # run inference on the train set
+    config = modelzoo.load_super_animal_config(
+        super_animal=superanimal_name,
+        model_name=model_name,
+        detector_name=detector_name,
+    )
     if device is None:
         device = select_device()
 
-    config = {**project_config, **model_config}
-    config = update_config(config, max_individuals, device)
+    # get the SuperAnimal detector and pose model snapshot paths
+    pose_model_path = modelzoo.get_super_animal_snapshot_path(
+        dataset=superanimal_name, model_name=model_name,
+    )
+    detector_path = modelzoo.get_super_animal_snapshot_path(
+        dataset=superanimal_name, model_name=detector_name,
+    )
 
+    config = update_config(config, max_individuals, device)
     individuals = [f"animal{i}" for i in range(max_individuals)]
-    config["individuals"] = individuals
+    config["metadata"]["individuals"] = individuals
     train_file_path = os.path.join(memory_replay_folder, "annotations", train_file)
 
     pose_runner, detector_runner = get_inference_runners(
         config,
         snapshot_path=pose_model_path,
         max_individuals=max_individuals,
-        num_bodyparts=len(model_config["metadata"]["bodyparts"]),
+        num_bodyparts=len(config["metadata"]["bodyparts"]),
         num_unique_bodyparts=0,
         detector_path=detector_path,
     )
@@ -277,7 +286,7 @@ def keypoint_matching(
         name = image_path.split(os.sep)[-1]
         image_name_to_pred[name] = prediction
 
-    pred_keypoint_names = config["bodyparts"]
+    pred_keypoint_names = config["metadata"]["bodyparts"]
     num_pred_keypoints = len(pred_keypoint_names)
     gt_keypoint_names = categories[0]["keypoints"]
     num_gt_keypoints = len(gt_keypoint_names)
@@ -401,39 +410,24 @@ def dlc3predictions_2_annotation_from_video(
 
     num_kpts = len(bodyparts)
 
-    # superquadruped
-    if num_kpts == 39 and superanimal_name is not None:
-        categories = [
-            {
-                "name": "superquadruped",
-                "id": 1,
-                "supercategory": "animal",
-                "keypoints": bodyparts,
-            }
-        ]
-    # supertopviewmouse
-    elif num_kpts == 27 and superanimal_name is not None:
-        categories = [
-            {
-                "name": "supertopviewmouse",
-                "id": 1,
-                "supercategory": "animal",
-                "keypoints": bodyparts,
-            }
-        ]
-
-    else:
+    if not superanimal_name.startswith("superanimal_"):
         raise ValueError("not supporting non superanimal model video adaptation yet")
 
-    assert len(predictions) == len(image_paths)
+    category_name = superanimal_name[len("superanimal_"):]
+    categories = [
+        {
+            "name": category_name,
+            "id": 1,
+            "supercategory": "animal",
+            "keypoints": bodyparts,
+        }
+    ]
 
+    assert len(predictions) == len(image_paths)
     imageid2annotations = defaultdict(list)
     for image_id, (prediction, image_path) in enumerate(zip(predictions, image_paths)):
-
         image_obj = cv2.imread(image_path)
-
         height, width, channels = image_obj.shape
-
         imagename = image_path.split(os.sep)[-1]
         image = {
             "id": image_id,
diff --git a/docs/pytorch/pytorch_config.md b/docs/pytorch/pytorch_config.md
index 6377a85fa..2c77afd1a 100644
--- a/docs/pytorch/pytorch_config.md
+++ b/docs/pytorch/pytorch_config.md
@@ -437,15 +437,15 @@ detector:
     ...
 ```
 
-Currently, the only detector available is a `FasterRCNN`. However, multiple variants are
-available (you can view the different variants on [torchvision's object detection page](
-https://pytorch.org/vision/stable/models.html#object-detection)). It's recommended to
-use the fastest detector that brings enough performance. The recommended variants
-are the following (from fastest to most powerful, taken from torchvision's
-documentation):
-
-| name                              |  Box MAP (larger = more powerful) | Params (larger = more powerful) |    GFLOPS (larger = slower) |
-|-----------------------------------|----------------------------------:|--------------------------------:|----------------------------:|
-| fasterrcnn_mobilenet_v3_large_fpn |                              32.8 |                           19.4M |                        4.49 |
-| fasterrcnn_resnet50_fpn           |                                37 |                           41.8M |                      134.38 |
-| fasterrcnn_resnet50_fpn_v2        |                              46.7 |                           43.7M |                      280.37 |
+Currently, the only detectors available are `FasterRCNN` and `SSDLite`. However, multiple variants of
+`FasterRCNN` are available (you can view the different variants on 
+[torchvision's object detection page](https://pytorch.org/vision/stable/models.html#object-detection)). It's recommended to use the fastest 
+detector that brings enough performance. The recommended variants are the following 
+(from fastest to most powerful, taken from torchvision's documentation):
+
+| name                              | Box MAP (larger = more powerful) | Params (larger = more powerful) | GFLOPS (larger = slower) |
+|-----------------------------------|---------------------------------:|--------------------------------:|-------------------------:|
+| SSDLite                           |                             21.3 |                            3.4M |                     0.58 |
+| fasterrcnn_mobilenet_v3_large_fpn |                             32.8 |                           19.4M |                     4.49 |
+| fasterrcnn_resnet50_fpn           |                               37 |                           41.8M |                   134.38 |
+| fasterrcnn_resnet50_fpn_v2        |                             46.7 |                           43.7M |                   280.37 |
diff --git a/docs/pytorch/user_guide.md b/docs/pytorch/user_guide.md
index 33bb70ec6..d85aee5d0 100644
--- a/docs/pytorch/user_guide.md
+++ b/docs/pytorch/user_guide.md
@@ -74,19 +74,19 @@ as well as indications which options are not yet implemented, and which paramete
 are not valid for the DLC 3.0 API.
 
 
-| API Method                     | Implemented | Parameters not yet implemented                                                                                                | Parameters invalid for pytorch                      |
-|--------------------------------|:-----------:|-------------------------------------------------------------------------------------------------------------------------------|-----------------------------------------------------|
-| `train_network`                |     🟢      | `keepdeconvweights`                                                                                                           | `maxiters`, `saveiters`, `allow_growth`, `autotune` |
-| `return_train_network_path`    |     🟢      |                                                                                                                               |                                                     |
-| `evaluate_network`             |     🟢      | `comparisonbodyparts`, `rescale`, `per_keypoint_evaluation`                                                                   |                                                     |
-| `return_evaluate_network_data` |     🔴      |                                                                                                                               | `TFGPUinference`, `allow_growth`                    |
-| `analyze_videos`               |     🟢      | `use_shelve`, `save_as_csv`, `in_random_order`, `batchsize`, `cropping`, `dynamic`, `robust_nframes`, `n_tracks`, `calibrate` |                                                     |
-| `create_tracking_dataset`      |     🔴      |                                                                                                                               |                                                     |
-| `analyze_time_lapse_frames`    |     🟠      |   the name has changed to  `analyze_images` to better reflect what it actually does (no video needed)                         |                                                     |
-| `convert_detections2tracklets` |     🟢      | `greedy`, `calibrate`, `window_size`                                                                                          |                                                     |
-| `extract_maps`                 |     🔴      |                                                                                                                               |                                                     |
-| `visualize_scoremaps`          |     🔴      |                                                                                                                               |                                                     |
-| `visualize_locrefs`            |     🔴      |                                                                                                                               |                                                     |
-| `visualize_paf`                |     🔴      |                                                                                                                               |                                                     |
-| `extract_save_all_maps`        |     🔴      |                                                                                                                               |                                                     |
-| `export_model`                 |     🔴      |                                                                                                                               |                                                     |
\ No newline at end of file
+| API Method                     | Implemented | Parameters not yet implemented                                                                                              | Parameters invalid for pytorch                      |
+|--------------------------------|:-----------:|-----------------------------------------------------------------------------------------------------------------------------|-----------------------------------------------------|
+| `train_network`                |     🟢      | `keepdeconvweights`                                                                                                         | `maxiters`, `saveiters`, `allow_growth`, `autotune` |
+| `return_train_network_path`    |     🟢      |                                                                                                                             |                                                     |
+| `evaluate_network`             |     🟢      | `comparisonbodyparts`, `rescale`, `per_keypoint_evaluation`                                                                 |                                                     |
+| `return_evaluate_network_data` |     🔴      |                                                                                                                             | `TFGPUinference`, `allow_growth`                    |
+| `analyze_videos`               |     🟢      | `use_shelve`, `save_as_csv`, `in_random_order`, `dynamic`, `robust_nframes`, `n_tracks`, `calibrate` |                                                     |
+| `create_tracking_dataset`      |     🔴      |                                                                                                                             |                                                     |
+| `analyze_time_lapse_frames`    |     🟠      |   the name has changed to  `analyze_images` to better reflect what it actually does (no video needed)                       |                                                     |
+| `convert_detections2tracklets` |     🟢      | `greedy`, `calibrate`, `window_size`                                                                                        |                                                     |
+| `extract_maps`                 |     🔴      |                                                                                                                             |                                                     |
+| `visualize_scoremaps`          |     🔴      |                                                                                                                             |                                                     |
+| `visualize_locrefs`            |     🔴      |                                                                                                                             |                                                     |
+| `visualize_paf`                |     🔴      |                                                                                                                             |                                                     |
+| `extract_save_all_maps`        |     🔴      |                                                                                                                             |                                                     |
+| `export_model`                 |     🔴      |                                                                                                                             |                                                     |
\ No newline at end of file
diff --git a/examples/COLAB/COLAB_YOURDATA_SuperAnimal.ipynb b/examples/COLAB/COLAB_YOURDATA_SuperAnimal.ipynb
index 7d50b0635..43e64cf91 100644
--- a/examples/COLAB/COLAB_YOURDATA_SuperAnimal.ipynb
+++ b/examples/COLAB/COLAB_YOURDATA_SuperAnimal.ipynb
@@ -79,11 +79,13 @@
     "from PIL import Image\n",
     "\n",
     "import deeplabcut\n",
+    "import deeplabcut.utils.auxiliaryfunctions as auxiliaryfunctions\n",
     "from deeplabcut.pose_estimation_pytorch.apis import (\n",
     "    superanimal_analyze_images,\n",
     ")\n",
     "from deeplabcut.core.weight_init import WeightInitialization\n",
     "from deeplabcut.core.engine import Engine\n",
+    "from deeplabcut.modelzoo import build_weight_init\n",
     "from deeplabcut.modelzoo.utils import (\n",
     "    create_conversion_table,\n",
     "    read_conversion_table_from_csv,\n",
@@ -144,7 +146,10 @@
     "\n",
     "# If this cell fails (e.g., when using Safari in place of Google Chrome),\n",
     "# manually upload your video via the Files menu to the left\n",
-    "# and define `video_path` yourself with right click > copy path on the video."
+    "# and define `image_path` yourself with right click > copy path on the image:\n",
+    "#\n",
+    "# image_path = \"/path/to/my/image.png\"\n",
+    "# image_name = os.path.splitext(image_path)[0]"
    ]
   },
   {
@@ -172,7 +177,8 @@
     "# @markdown ---\n",
     "# @markdown SuperAnimal Configurations\n",
     "superanimal_name = \"superanimal_topviewmouse\" #@param [\"superanimal_topviewmouse\", \"superanimal_quadruped\"]\n",
-    "model_name = \"hrnetw32\" #@param [\"hrnetw32\"]\n",
+    "model_name = \"hrnet_w32\" #@param [\"hrnet_w32\"]\n",
+    "detector_name = \"fasterrcnn_resnet50_fpn_v2\" #@param [\"fasterrcnn_resnet50_fpn_v2\"]\n",
     "\n",
     "# @markdown ---\n",
     "# @markdown What is the maximum number of animals you expect to have in an image\n",
@@ -194,6 +200,7 @@
     "_ = superanimal_analyze_images(\n",
     "    superanimal_name,\n",
     "    model_name,\n",
+    "    detector_name,\n",
     "    image_path,\n",
     "    max_individuals,\n",
     "    out_folder=\"/content/\",\n",
@@ -267,7 +274,8 @@
     "# @markdown ---\n",
     "# @markdown SuperAnimal Configurations\n",
     "superanimal_name = \"superanimal_topviewmouse\" #@param [\"superanimal_topviewmouse\", \"superanimal_quadruped\"]\n",
-    "model_name = \"hrnetw32\" #@param [\"hrnetw32\"]\n",
+    "model_name = \"hrnet_w32\" #@param [\"hrnet_w32\"]\n",
+    "detector_name = \"fasterrcnn_resnet50_fpn_v2\" #@param [\"fasterrcnn_resnet50_fpn_v2\"]\n",
     "\n",
     "# @markdown ---\n",
     "# @markdown What is the maximum number of animals you expect to have in an image\n",
@@ -298,7 +306,9 @@
    "source": [
     "_ = video_inference_superanimal(\n",
     "    videos=video_path,\n",
-    "    superanimal_name=f\"{superanimal_name}_{model_name}\",\n",
+    "    superanimal_name=superanimal_name,\n",
+    "    model_name=model_name,\n",
+    "    detector_name=detector_name,\n",
     "    video_adapt=False,\n",
     "    max_individuals=max_individuals,\n",
     "    dest_folder=\"/content/\",\n",
@@ -329,7 +339,9 @@
    "source": [
     "_ = video_inference_superanimal(\n",
     "    videos=[video_path],\n",
-    "    superanimal_name=f\"{superanimal_name}_{model_name}\",\n",
+    "    superanimal_name=superanimal_name,\n",
+    "    model_name=model_name,\n",
+    "    detector_name=detector_name,\n",
     "    video_adapt=True,\n",
     "    max_individuals=max_individuals,\n",
     "    pseudo_threshold=0.1,\n",
@@ -435,7 +447,8 @@
     "# @markdown ---\n",
     "# @markdown SuperAnimal Configurations\n",
     "superanimal_name = \"superanimal_topviewmouse\" #@param [\"superanimal_topviewmouse\", \"superanimal_quadruped\"]\n",
-    "model_name = \"hrnetw32\" #@param [\"hrnetw32\"]"
+    "model_name = \"hrnet_w32\" #@param [\"hrnet_w32\"]\n",
+    "detector_name = \"fasterrcnn_resnet50_fpn_v2\" #@param [\"fasterrcnn_resnet50_fpn_v2\"]"
    ]
   },
   {
@@ -526,7 +539,8 @@
     "deeplabcut.create_training_dataset(\n",
     "    config_path,\n",
     "    Shuffles=[imagenet_transfer_learning_shuffle],\n",
-    "    net_type=\"top_down_hrnet_w32\",\n",
+    "    net_type=f\"top_down_{model_name}\",\n",
+    "    detector_type=detector_name,\n",
     "    engine=Engine.PYTORCH,\n",
     "    userfeedback=False,\n",
     ")"
@@ -647,8 +661,11 @@
    },
    "outputs": [],
    "source": [
-    "weight_init = WeightInitialization(\n",
-    "    dataset=superanimal_name,\n",
+    "weight_init = build_weight_init(\n",
+    "    cfg=auxiliaryfunctions.read_config(config_path), \n",
+    "    super_animal=superanimal_name,\n",
+    "    model_name=model_name,\n",
+    "    detector_name=detector_name,\n",
     "    with_decoder=False,\n",
     ")\n",
     "\n",
@@ -657,7 +674,8 @@
     "    from_shuffle=imagenet_transfer_learning_shuffle,\n",
     "    shuffles=[superanimal_transfer_learning_shuffle],\n",
     "    engine=Engine.PYTORCH,\n",
-    "    net_type=\"top_down_hrnet_w32\",\n",
+    "    net_type=f\"top_down_{model_name}\",\n",
+    "    detector_type=detector_name,\n",
     "    weight_init=weight_init,\n",
     "    userfeedback=False,\n",
     ")"
@@ -806,6 +824,7 @@
     "    config_path,\n",
     "    superanimal_name,\n",
     "    model_name,\n",
+    "    detector_name,\n",
     "    copy_images=True,\n",
     ")\n",
     "\n",
@@ -872,13 +891,49 @@
     "df"
    ]
   },
+  {
+   "cell_type": "markdown",
+   "metadata": {},
+   "source": [
+    "#### Adding the Conversion Table to your project's `config.yaml` file\n",
+    "\n",
+    "Once you've run keypoint matching, you can add the conversion table to your project's `config.yaml` file, and edit it if there are some matches you think are wrong. As an example, for a top-view mouse dataset with 4 bodyparts labeled (`'snout', 'leftear', 'rightear', 'tailbase'`), the conversion table mapping project bodyparts to SuperAnimal bodyparts would be added as:\n",
+    "\n",
+    "```yaml\n",
+    "# Conversion tables to fine-tune SuperAnimal weights\n",
+    "SuperAnimalConversionTables:\n",
+    "  superanimal_topviewmouse:\n",
+    "    snout: nose\n",
+    "    leftear: left_ear\n",
+    "    rightear: right_ear\n",
+    "    tailbase: tail_base\n",
+    "```\n"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": null,
+   "metadata": {},
+   "outputs": [],
+   "source": [
+    "create_conversion_table(\n",
+    "    config=config_path,\n",
+    "    super_animal=superanimal_name,\n",
+    "    project_to_super_animal=read_conversion_table_from_csv(\n",
+    "        conversion_table_path\n",
+    "    ),\n",
+    ")"
+   ]
+  },
   {
    "cell_type": "markdown",
    "metadata": {
     "id": "GkfIo8zTxPoS"
    },
    "source": [
-    "#### Prepare the training shuffle and weight initialization for (naive) fine-tuning with SuperAnimal weights"
+    "#### Prepare the training shuffle and weight initialization for (naive) fine-tuning with SuperAnimal weights\n",
+    "\n",
+    "Then, when you call `build_weight_init` with `with_decoder=True`, the conversion table in your project's `config.yaml` is used to get predictions for the correct bodyparts."
    ]
   },
   {
@@ -892,16 +947,12 @@
    },
    "outputs": [],
    "source": [
-    "table = create_conversion_table(\n",
-    "    config=config_path,\n",
+    "weight_init = build_weight_init(\n",
+    "    cfg=auxiliaryfunctions.read_config(config_path), \n",
     "    super_animal=superanimal_name,\n",
-    "    project_to_super_animal=read_conversion_table_from_csv(conversion_table_path),\n",
-    ")\n",
-    "\n",
-    "weight_init = WeightInitialization(\n",
-    "    dataset=superanimal_name,\n",
+    "    model_name=model_name,\n",
+    "    detector_name=detector_name,\n",
     "    with_decoder=True,\n",
-    "    conversion_array=table.to_array()\n",
     ")\n",
     "\n",
     "deeplabcut.create_training_dataset_from_existing_split(\n",
@@ -909,7 +960,8 @@
     "    from_shuffle=imagenet_transfer_learning_shuffle,\n",
     "    shuffles=[superanimal_naive_finetune_shuffle],\n",
     "    engine=Engine.PYTORCH,\n",
-    "    net_type=\"top_down_hrnet_w32\",\n",
+    "    net_type=f\"top_down_{model_name}\",\n",
+    "    detector_type=detector_name,\n",
     "    weight_init=weight_init,\n",
     "    userfeedback=False,\n",
     ")"
@@ -1027,9 +1079,11 @@
    },
    "outputs": [],
    "source": [
-    "weight_init = WeightInitialization(\n",
-    "    dataset=superanimal_name,\n",
-    "    conversion_array=table.to_array(),\n",
+    "weight_init = build_weight_init(\n",
+    "    cfg=auxiliaryfunctions.read_config(config_path), \n",
+    "    super_animal=superanimal_name,\n",
+    "    model_name=model_name,\n",
+    "    detector_name=detector_name,\n",
     "    with_decoder=True,\n",
     "    memory_replay=True,\n",
     ")\n",
@@ -1039,7 +1093,8 @@
     "    from_shuffle=imagenet_transfer_learning_shuffle,\n",
     "    shuffles=[superanimal_memory_replay_shuffle],\n",
     "    engine=Engine.PYTORCH,\n",
-    "    net_type=\"top_down_hrnet_w32\",\n",
+    "    net_type=f\"top_down_{model_name}\",\n",
+    "    detector_type=detector_name,\n",
     "    weight_init=weight_init,\n",
     "    userfeedback=False,\n",
     ")"
diff --git a/examples/SUPERANIMAL/eval_zeroshot.py b/examples/SUPERANIMAL/eval_zeroshot.py
index 1c7b3f5b3..7b472d0b4 100644
--- a/examples/SUPERANIMAL/eval_zeroshot.py
+++ b/examples/SUPERANIMAL/eval_zeroshot.py
@@ -16,8 +16,8 @@
 import torch
 
 import deeplabcut.utils.auxiliaryfunctions as af
-from deeplabcut.core.weight_init import WeightInitialization
 from deeplabcut.generate_training_dataset import TrainingDatasetMetadata
+from deeplabcut.modelzoo import build_weight_init
 from deeplabcut.pose_estimation_pytorch import DLCLoader
 from deeplabcut.pose_estimation_pytorch.apis.evaluate import evaluate_snapshot
 from deeplabcut.pose_estimation_pytorch.models import PoseModel
@@ -29,6 +29,8 @@ def main(
     super_animal: str,
     shuffle_index: int,
     device: str,
+    super_animal_model: str = "hrnet_w32",
+    super_animal_detector: str = "fasterrcnn_resnet50_fpn_v2",
 ):
     metadata = TrainingDatasetMetadata.load(config_path, load_splits=True)
     shuffles = [s for s in metadata.shuffles if s.index == shuffle_index]
@@ -64,11 +66,13 @@ def main(
     # Build the pose model
     model = PoseModel.build(
         loader.model_cfg["model"],
-        weight_init=WeightInitialization.build(
+        weight_init=build_weight_init(
             cfg=cfg,
             super_animal=super_animal,
+            model_name=super_animal_model,
+            detector_name=super_animal_detector,
             with_decoder=True,
-        )
+        ),
     )
 
     # Save the zero-shot snapshot
diff --git a/examples/SUPERANIMAL/memory_replay_example.py b/examples/SUPERANIMAL/memory_replay_example.py
index 265566c18..8559abfb6 100644
--- a/examples/SUPERANIMAL/memory_replay_example.py
+++ b/examples/SUPERANIMAL/memory_replay_example.py
@@ -18,12 +18,19 @@
     create_conversion_table,
     read_conversion_table_from_csv,
 )
+from deeplabcut.pose_estimation_pytorch.modelzoo.utils import (
+    get_super_animal_snapshot_path,
+)
 from deeplabcut.utils.pseudo_label import keypoint_matching
 
 
-def main(dlc_proj_root: Path, super_animal_name: str):
+def main(
+    dlc_proj_root: Path,
+    super_animal_name: str,
+    super_animal_model: str = "hrnet_w32",
+    super_animal_detector: str = "fasterrcnn_resnet50_fpn_v2",
+):
     config_path = str(dlc_proj_root / "config.yaml")
-    model_name = "hrnetw32"
     shuffle = 0
     device = "cuda"
 
@@ -32,7 +39,8 @@ def main(dlc_proj_root: Path, super_animal_name: str):
     keypoint_matching(
         config_path,
         super_animal_name,
-        model_name,
+        super_animal_model,
+        super_animal_detector,
     )
 
     # keypoint matching creates a memory_replay folder in the root. The conversion table
@@ -47,9 +55,18 @@ def main(dlc_proj_root: Path, super_animal_name: str):
 
     weight_init = WeightInitialization(
         dataset=super_animal_name,
+        snapshot_path=get_super_animal_snapshot_path(
+            dataset=super_animal_name,
+            model_name=super_animal_model,
+            download=True,
+        ),
+        detector_snapshot_path=get_super_animal_snapshot_path(
+            dataset=super_animal_name,
+            model_name=super_animal_detector,
+            download=True,
+        ),
         conversion_array=table.to_array(),
         with_decoder=True,
-        memory_replay=True,
     )
 
     deeplabcut.create_training_dataset(
diff --git a/examples/SUPERANIMAL/superanimal_image_inference.py b/examples/SUPERANIMAL/superanimal_image_inference.py
index ef02b5bdc..26fd4e5b7 100644
--- a/examples/SUPERANIMAL/superanimal_image_inference.py
+++ b/examples/SUPERANIMAL/superanimal_image_inference.py
@@ -15,13 +15,15 @@
 
 if __name__ == "__main__":
     superanimal_name = "superanimal_quadruped"
-    model_name = "hrnetw32"
+    model_name = "hrnet_w32"
+    detector_name = "fasterrcnn_resnet50_fpn_v2"
     device = "cuda"
     max_individuals = 3
 
     ret = superanimal_analyze_images(
         superanimal_name,
         model_name,
+        detector_name,
         "test_rodent_images",
         max_individuals,
         "vis_test_rodent_images",
diff --git a/examples/SUPERANIMAL/video_adapt_example.py b/examples/SUPERANIMAL/video_adapt_example.py
index 856803016..217e7bcfb 100644
--- a/examples/SUPERANIMAL/video_adapt_example.py
+++ b/examples/SUPERANIMAL/video_adapt_example.py
@@ -15,7 +15,9 @@
 def main():
     modelzoo.video_inference_superanimal(
         videos=["/mnt/md0/shaokai/tom_video.mp4"],
-        superanimal_name="superanimal_topviewmouse_hrnetw32",
+        superanimal_name="superanimal_topviewmouse",
+        model_name="hrnet_w32",
+        detector_name="fasterrcnn_resnet50_fpn_v2",
         video_adapt=True,
         max_individuals=3,
         pseudo_threshold=0.1,
diff --git a/examples/openfield-Pranav-2018-10-30/config.yaml b/examples/openfield-Pranav-2018-10-30/config.yaml
index 330f02ddc..d6bdced20 100644
--- a/examples/openfield-Pranav-2018-10-30/config.yaml
+++ b/examples/openfield-Pranav-2018-10-30/config.yaml
@@ -2,6 +2,9 @@
 Task: openfield
 scorer: Pranav
 date: Oct30
+multianimalproject:
+identity:
+
 
 # Project path (change when moving around)
 project_path: WILL BE AUTOMATICALLY UPDATED BY DEMO CODE
@@ -44,7 +47,9 @@ iteration: 0
 default_net_type: resnet_50
 default_augmenter: imgaug
 snapshotindex: -1
+detector_snapshotindex: -1
 batch_size: 4
+detector_batch_size: 1
 
 
 # Cropping Parameters (for analysis and outlier frame detection)
diff --git a/setup.py b/setup.py
index 4d7760608..0fcb80165 100644
--- a/setup.py
+++ b/setup.py
@@ -18,6 +18,21 @@
     long_description = fh.read()
 
 
+def super_animal_config_paths() -> list[str]:
+    config_dirs = [
+        Path("deeplabcut") / "modelzoo" / "model_configs",
+        Path("deeplabcut") / "modelzoo" / "project_configs",
+    ]
+
+    configs = []
+    for subdir in config_dirs:
+        for p in subdir.iterdir():
+            if p.suffix == ".yaml":
+                configs.append(str(p))
+
+    return configs
+
+
 def pytorch_config_paths() -> list[str]:
     pytorch_configs = []
     config_dir = Path("deeplabcut") / "pose_estimation_pytorch" / "config"
@@ -41,7 +56,7 @@ def pytorch_config_paths() -> list[str]:
     url="https://github.com/DeepLabCut/DeepLabCut",
     install_requires=[
         "albumentations<=1.4.3",
-        "dlclibrary>=0.0.5",
+        "dlclibrary>=0.0.7",
         "einops",
         "dlclibrary>=0.0.6",
         "filterpy>=1.4.4",
@@ -97,11 +112,7 @@ def pytorch_config_paths() -> list[str]:
                 "deeplabcut/pose_cfg.yaml",
                 "deeplabcut/inference_cfg.yaml",
                 "deeplabcut/reid_cfg.yaml",
-                "deeplabcut/modelzoo/model_configs/dlcrnet.yaml",
-                "deeplabcut/modelzoo/model_configs/hrnetw32.yaml",
                 "deeplabcut/modelzoo/models_to_framework.json",
-                "deeplabcut/modelzoo/project_configs/superanimal_quadruped.yaml",
-                "deeplabcut/modelzoo/project_configs/superanimal_topviewmouse.yaml",
                 "deeplabcut/pose_estimation_tensorflow/models/pretrained/pretrained_model_urls.yaml",
                 "deeplabcut/gui/style.qss",
                 "deeplabcut/gui/media/logo.png",
@@ -117,7 +128,7 @@ def pytorch_config_paths() -> list[str]:
                 "deeplabcut/gui/assets/icons/new_project2.png",
                 "deeplabcut/gui/assets/icons/open.png",
                 "deeplabcut/gui/assets/icons/open2.png",
-            ] + pytorch_config_paths(),
+            ] + super_animal_config_paths() + pytorch_config_paths(),
         )
     ],
     include_package_data=True,


Test patch:
diff --git a/tests/pose_estimation_pytorch/modelzoo/test_modelzoo_utils.py b/tests/pose_estimation_pytorch/modelzoo/test_modelzoo_utils.py
index 7b3bd49d2..f3a7b15c7 100644
--- a/tests/pose_estimation_pytorch/modelzoo/test_modelzoo_utils.py
+++ b/tests/pose_estimation_pytorch/modelzoo/test_modelzoo_utils.py
@@ -8,33 +8,28 @@
 #
 # Licensed under GNU Lesser General Public License v3.0
 #
-import os
 
 import pytest
 
-from deeplabcut.pose_estimation_pytorch.modelzoo.utils import get_config_model_paths
+import deeplabcut.pose_estimation_pytorch.modelzoo as modelzoo
 
 
-@pytest.mark.skip(reason="require-models")
 @pytest.mark.parametrize(
-    "project_name", ["superanimal_quadruped", "superanimal_topviewmouse"]
+    "super_animal", ["superanimal_quadruped", "superanimal_topviewmouse"]
 )
-def test_get_config_model_paths(project_name):
-    (
-        model_config,
-        project_config,
-        pose_model_path,
-        detector_model_path,
-    ) = get_config_model_paths(
-        project_name,
-        "hrnetw32",
-        detector_type="fasterrcnn",
-        weight_folder=None,
+@pytest.mark.parametrize("model_name", ["hrnet_w32"])
+@pytest.mark.parametrize("detector_name", [None, "fasterrcnn_resnet50_fpn_v2"])
+def test_get_config_model_paths(super_animal, model_name, detector_name):
+    model_config = modelzoo.load_super_animal_config(
+        super_animal=super_animal,
+        model_name=model_name,
+        detector_name=detector_name,
     )
 
     assert isinstance(model_config, dict)
-    assert isinstance(project_config, dict)
-    assert isinstance(pose_model_path, str)
-    assert isinstance(detector_model_path, str)
-    assert os.path.exists(pose_model_path)
-    assert os.path.exists(detector_model_path)
+    if detector_name is None:
+        assert model_config["method"].lower() == "bu"
+        assert "detector" not in model_config
+    else:
+        assert model_config["method"].lower() == "td"
+        assert "detector" in model_config