Repository: ynput/OpenPype
Issue: Nuke: multiple baking streams with correct slate

 **Describe the bug**
At the moment we support only one slate frame per all baking streams. This is confusing if other stream is configured as `noLut` (disabled viewer input process node - lut). This `noLut` stream will have slate with baked look image.

**Expected behavior**
Each baking stream needs to be paired with own slate frame, according to look configuration.



[cuID:]

Changed files:
openpype/plugins/publish/extract_review_slate.py, openpype/modules/deadline/plugins/publish/submit_publish_job.py, openpype/hosts/nuke/api/plugin.py, openpype/hosts/nuke/api/__init__.py, openpype/hosts/nuke/plugins/publish/extract_review_data_mov.py, openpype/hosts/nuke/plugins/publish/extract_slate_frame.py, openpype/hosts/nuke/plugins/publish/extract_thumbnail.py, openpype/hosts/nuke/api/lib.py

diff --git a/openpype/hosts/nuke/api/__init__.py b/openpype/hosts/nuke/api/__init__.py
index b571c4098c..77fe4503d3 100644
--- a/openpype/hosts/nuke/api/__init__.py
+++ b/openpype/hosts/nuke/api/__init__.py
@@ -26,7 +26,11 @@ from .pipeline import (
     update_container,
 )
 from .lib import (
-    maintained_selection
+    maintained_selection,
+    reset_selection,
+    get_view_process_node,
+    duplicate_node
+
 )
 
 from .utils import (
@@ -58,6 +62,9 @@ __all__ = (
     "update_container",
 
     "maintained_selection",
+    "reset_selection",
+    "get_view_process_node",
+    "duplicate_node",
 
     "colorspace_exists_on_node",
     "get_colorspace_list"
diff --git a/openpype/hosts/nuke/api/lib.py b/openpype/hosts/nuke/api/lib.py
index 505eb19419..a8e01d0a36 100644
--- a/openpype/hosts/nuke/api/lib.py
+++ b/openpype/hosts/nuke/api/lib.py
@@ -3,6 +3,7 @@ from pprint import pformat
 import re
 import six
 import platform
+import tempfile
 import contextlib
 from collections import OrderedDict
 
@@ -711,6 +712,20 @@ def get_imageio_input_colorspace(filename):
     return preset_clrsp
 
 
+def get_view_process_node():
+    reset_selection()
+
+    ipn_orig = None
+    for v in nuke.allNodes(filter="Viewer"):
+        ipn = v['input_process_node'].getValue()
+        if "VIEWER_INPUT" not in ipn:
+            ipn_orig = nuke.toNode(ipn)
+            ipn_orig.setSelected(True)
+
+    if ipn_orig:
+        return duplicate_node(ipn_orig)
+
+
 def on_script_load():
     ''' Callback for ffmpeg support
     '''
@@ -2374,6 +2389,8 @@ def process_workfile_builder():
         env_value_to_bool,
         get_custom_workfile_template
     )
+    # to avoid looping of the callback, remove it!
+    nuke.removeOnCreate(process_workfile_builder, nodeClass="Root")
 
     # get state from settings
     workfile_builder = get_current_project_settings()["nuke"].get(
@@ -2429,9 +2446,6 @@ def process_workfile_builder():
     if not openlv_on or not os.path.exists(last_workfile_path):
         return
 
-    # to avoid looping of the callback, remove it!
-    nuke.removeOnCreate(process_workfile_builder, nodeClass="Root")
-
     log.info("Opening last workfile...")
     # open workfile
     open_file(last_workfile_path)
@@ -2617,6 +2631,57 @@ class DirmapCache:
         return cls._sync_module
 
 
+@contextlib.contextmanager
+def _duplicate_node_temp():
+    """Create a temp file where node is pasted during duplication.
+
+    This is to avoid using clipboard for node duplication.
+    """
+
+    duplicate_node_temp_path = os.path.join(
+        tempfile.gettempdir(),
+        "openpype_nuke_duplicate_temp_{}".format(os.getpid())
+    )
+
+    # This can happen only if 'duplicate_node' would be
+    if os.path.exists(duplicate_node_temp_path):
+        log.warning((
+            "Temp file for node duplication already exists."
+            " Trying to remove {}"
+        ).format(duplicate_node_temp_path))
+        os.remove(duplicate_node_temp_path)
+
+    try:
+        # Yield the path where node can be copied
+        yield duplicate_node_temp_path
+
+    finally:
+        # Remove the file at the end
+        os.remove(duplicate_node_temp_path)
+
+
+def duplicate_node(node):
+    reset_selection()
+
+    # select required node for duplication
+    node.setSelected(True)
+
+    with _duplicate_node_temp() as filepath:
+        # copy selected to temp filepath
+        nuke.nodeCopy(filepath)
+
+        # reset selection
+        reset_selection()
+
+        # paste node and selection is on it only
+        dupli_node = nuke.nodePaste(filepath)
+
+    # reset selection
+    reset_selection()
+
+    return dupli_node
+
+
 def dirmap_file_name_filter(file_name):
     """Nuke callback function with single full path argument.
 
diff --git a/openpype/hosts/nuke/api/plugin.py b/openpype/hosts/nuke/api/plugin.py
index b8b56ef2b8..925cab0bef 100644
--- a/openpype/hosts/nuke/api/plugin.py
+++ b/openpype/hosts/nuke/api/plugin.py
@@ -14,12 +14,12 @@ from openpype.pipeline import (
 from .lib import (
     Knobby,
     check_subsetname_exists,
-    reset_selection,
     maintained_selection,
     set_avalon_knob_data,
     add_publish_knob,
     get_nuke_imageio_settings,
-    set_node_knobs_from_settings
+    set_node_knobs_from_settings,
+    get_view_process_node
 )
 
 
@@ -216,37 +216,6 @@ class ExporterReview(object):
 
         self.data["representations"].append(repre)
 
-    def get_view_input_process_node(self):
-        """
-        Will get any active view process.
-
-        Arguments:
-            self (class): in object definition
-
-        Returns:
-            nuke.Node: copy node of Input Process node
-        """
-        reset_selection()
-        ipn_orig = None
-        for v in nuke.allNodes(filter="Viewer"):
-            ip = v["input_process"].getValue()
-            ipn = v["input_process_node"].getValue()
-            if "VIEWER_INPUT" not in ipn and ip:
-                ipn_orig = nuke.toNode(ipn)
-                ipn_orig.setSelected(True)
-
-        if ipn_orig:
-            # copy selected to clipboard
-            nuke.nodeCopy("%clipboard%")
-            # reset selection
-            reset_selection()
-            # paste node and selection is on it only
-            nuke.nodePaste("%clipboard%")
-            # assign to variable
-            ipn = nuke.selectedNode()
-
-            return ipn
-
     def get_imageio_baking_profile(self):
         from . import lib as opnlib
         nuke_imageio = opnlib.get_nuke_imageio_settings()
@@ -311,7 +280,7 @@ class ExporterReviewLut(ExporterReview):
         self._temp_nodes = []
         self.log.info("Deleted nodes...")
 
-    def generate_lut(self):
+    def generate_lut(self, **kwargs):
         bake_viewer_process = kwargs["bake_viewer_process"]
         bake_viewer_input_process_node = kwargs[
             "bake_viewer_input_process"]
@@ -329,7 +298,7 @@ class ExporterReviewLut(ExporterReview):
         if bake_viewer_process:
             # Node View Process
             if bake_viewer_input_process_node:
-                ipn = self.get_view_input_process_node()
+                ipn = get_view_process_node()
                 if ipn is not None:
                     # connect
                     ipn.setInput(0, self.previous_node)
@@ -511,7 +480,7 @@ class ExporterReviewMov(ExporterReview):
         if bake_viewer_process:
             if bake_viewer_input_process_node:
                 # View Process node
-                ipn = self.get_view_input_process_node()
+                ipn = get_view_process_node()
                 if ipn is not None:
                     # connect
                     ipn.setInput(0, self.previous_node)
diff --git a/openpype/hosts/nuke/plugins/publish/extract_review_data_mov.py b/openpype/hosts/nuke/plugins/publish/extract_review_data_mov.py
index 2a79d600ba..5ea7c352b9 100644
--- a/openpype/hosts/nuke/plugins/publish/extract_review_data_mov.py
+++ b/openpype/hosts/nuke/plugins/publish/extract_review_data_mov.py
@@ -1,4 +1,5 @@
 import os
+from pprint import pformat
 import re
 import pyblish.api
 import openpype
@@ -50,6 +51,8 @@ class ExtractReviewDataMov(openpype.api.Extractor):
         with maintained_selection():
             generated_repres = []
             for o_name, o_data in self.outputs.items():
+                self.log.debug(
+                    "o_name: {}, o_data: {}".format(o_name, pformat(o_data)))
                 f_families = o_data["filter"]["families"]
                 f_task_types = o_data["filter"]["task_types"]
                 f_subsets = o_data["filter"]["subsets"]
@@ -88,7 +91,13 @@ class ExtractReviewDataMov(openpype.api.Extractor):
                 # check if settings have more then one preset
                 # so we dont need to add outputName to representation
                 # in case there is only one preset
-                multiple_presets = bool(len(self.outputs.keys()) > 1)
+                multiple_presets = len(self.outputs.keys()) > 1
+
+                # adding bake presets to instance data for other plugins
+                if not instance.data.get("bakePresets"):
+                    instance.data["bakePresets"] = {}
+                # add preset to bakePresets
+                instance.data["bakePresets"][o_name] = o_data
 
                 # create exporter instance
                 exporter = plugin.ExporterReviewMov(
diff --git a/openpype/hosts/nuke/plugins/publish/extract_slate_frame.py b/openpype/hosts/nuke/plugins/publish/extract_slate_frame.py
index fb52fc18b4..e0c4bdb953 100644
--- a/openpype/hosts/nuke/plugins/publish/extract_slate_frame.py
+++ b/openpype/hosts/nuke/plugins/publish/extract_slate_frame.py
@@ -1,11 +1,16 @@
 import os
+from pprint import pformat
 import nuke
 import copy
 
 import pyblish.api
 
 import openpype
-from openpype.hosts.nuke.api.lib import maintained_selection
+from openpype.hosts.nuke.api import (
+    maintained_selection,
+    duplicate_node,
+    get_view_process_node
+)
 
 
 class ExtractSlateFrame(openpype.api.Extractor):
@@ -15,14 +20,13 @@ class ExtractSlateFrame(openpype.api.Extractor):
 
     """
 
-    order = pyblish.api.ExtractorOrder - 0.001
+    order = pyblish.api.ExtractorOrder + 0.011
     label = "Extract Slate Frame"
 
     families = ["slate"]
     hosts = ["nuke"]
 
     # Settings values
-    # - can be extended by other attributes from node in the future
     key_value_mapping = {
         "f_submission_note": [True, "{comment}"],
         "f_submitting_for": [True, "{intent[value]}"],
@@ -30,44 +34,107 @@ class ExtractSlateFrame(openpype.api.Extractor):
     }
 
     def process(self, instance):
-        if hasattr(self, "viewer_lut_raw"):
-            self.viewer_lut_raw = self.viewer_lut_raw
-        else:
-            self.viewer_lut_raw = False
+
+        if "representations" not in instance.data:
+            instance.data["representations"] = []
+
+        self._create_staging_dir(instance)
 
         with maintained_selection():
             self.log.debug("instance: {}".format(instance))
             self.log.debug("instance.data[families]: {}".format(
                 instance.data["families"]))
 
-            self.render_slate(instance)
+            if instance.data.get("bakePresets"):
+                for o_name, o_data in instance.data["bakePresets"].items():
+                    self.log.info("_ o_name: {}, o_data: {}".format(
+                        o_name, pformat(o_data)))
+                    self.render_slate(
+                        instance,
+                        o_name,
+                        o_data["bake_viewer_process"],
+                        o_data["bake_viewer_input_process"]
+                    )
+            else:
+                # backward compatibility
+                self.render_slate(instance)
+
+            # also render image to sequence
+            self._render_slate_to_sequence(instance)
+
+    def _create_staging_dir(self, instance):
 
-    def render_slate(self, instance):
-        node_subset_name = instance.data.get("name", None)
-        node = instance[0]  # group node
         self.log.info("Creating staging dir...")
 
-        if "representations" not in instance.data:
-            instance.data["representations"] = list()
-
         staging_dir = os.path.normpath(
-            os.path.dirname(instance.data['path']))
+            os.path.dirname(instance.data["path"]))
 
         instance.data["stagingDir"] = staging_dir
 
         self.log.info(
             "StagingDir `{0}`...".format(instance.data["stagingDir"]))
 
-        frame_start = instance.data["frameStart"]
-        frame_end = instance.data["frameEnd"]
-        handle_start = instance.data["handleStart"]
-        handle_end = instance.data["handleEnd"]
+    def _check_frames_exists(self, instance):
+        # rendering path from group write node
+        fpath = instance.data["path"]
+
+        # instance frame range with handles
+        first = instance.data["frameStartHandle"]
+        last = instance.data["frameEndHandle"]
+
+        padding = fpath.count('#')
+
+        test_path_template = fpath
+        if padding:
+            repl_string = "#" * padding
+            test_path_template = fpath.replace(
+                repl_string, "%0{}d".format(padding))
+
+        for frame in range(first, last + 1):
+            test_file = test_path_template % frame
+            if not os.path.exists(test_file):
+                self.log.debug("__ test_file: `{}`".format(test_file))
+                return None
+
+        return True
+
+    def render_slate(
+        self,
+        instance,
+        output_name=None,
+        bake_viewer_process=True,
+        bake_viewer_input_process=True
+    ):
+        """Slate frame renderer
+
+        Args:
+            instance (PyblishInstance): Pyblish instance with subset data
+            output_name (str, optional):
+                Slate variation name. Defaults to None.
+            bake_viewer_process (bool, optional):
+                Switch for viewer profile baking. Defaults to True.
+            bake_viewer_input_process (bool, optional):
+                Switch for input process node baking. Defaults to True.
+        """
+        slate_node = instance.data["slateNode"]
+
+        # rendering path from group write node
+        fpath = instance.data["path"]
+
+        # instance frame range with handles
+        first_frame = instance.data["frameStartHandle"]
+        last_frame = instance.data["frameEndHandle"]
 
-        frame_length = int(
-            (frame_end - frame_start + 1) + (handle_start + handle_end)
-        )
+        # fill slate node with comments
+        self.add_comment_slate_node(instance, slate_node)
+
+        # solve output name if any is set
+        _output_name = output_name or ""
+        if _output_name:
+            _output_name = "_" + _output_name
+
+        slate_first_frame = first_frame - 1
 
-        temporary_nodes = []
         collection = instance.data.get("collection", None)
 
         if collection:
@@ -75,99 +142,101 @@ class ExtractSlateFrame(openpype.api.Extractor):
             fname = os.path.basename(collection.format(
                 "{head}{padding}{tail}"))
             fhead = collection.format("{head}")
-
-            collected_frames_len = int(len(collection.indexes))
-
-            # get first and last frame
-            first_frame = min(collection.indexes) - 1
-            self.log.info('frame_length: {}'.format(frame_length))
-            self.log.info(
-                'len(collection.indexes): {}'.format(collected_frames_len)
-            )
-            if ("slate" in instance.data["families"]) \\
-                    and (frame_length != collected_frames_len):
-                first_frame += 1
-
-            last_frame = first_frame
         else:
-            fname = os.path.basename(instance.data.get("path", None))
+            fname = os.path.basename(fpath)
             fhead = os.path.splitext(fname)[0] + "."
-            first_frame = instance.data.get("frameStartHandle", None) - 1
-            last_frame = first_frame
 
         if "#" in fhead:
             fhead = fhead.replace("#", "")[:-1]
 
-        previous_node = node
-
-        # get input process and connect it to baking
-        ipn = self.get_view_process_node()
-        if ipn is not None:
-            ipn.setInput(0, previous_node)
-            previous_node = ipn
-            temporary_nodes.append(ipn)
-
-        if not self.viewer_lut_raw:
+        self.log.debug("__ first_frame: {}".format(first_frame))
+        self.log.debug("__ slate_first_frame: {}".format(slate_first_frame))
+
+        # fallback if files does not exists
+        if self._check_frames_exists(instance):
+            # Read node
+            r_node = nuke.createNode("Read")
+            r_node["file"].setValue(fpath)
+            r_node["first"].setValue(first_frame)
+            r_node["origfirst"].setValue(first_frame)
+            r_node["last"].setValue(last_frame)
+            r_node["origlast"].setValue(last_frame)
+            r_node["colorspace"].setValue(instance.data["colorspace"])
+            previous_node = r_node
+            temporary_nodes = [previous_node]
+        else:
+            previous_node = slate_node.dependencies().pop()
+            temporary_nodes = []
+
+        # only create colorspace baking if toggled on
+        if bake_viewer_process:
+            if bake_viewer_input_process:
+                # get input process and connect it to baking
+                ipn = get_view_process_node()
+                if ipn is not None:
+                    ipn.setInput(0, previous_node)
+                    previous_node = ipn
+                    temporary_nodes.append(ipn)
+
+            # add duplicate slate node and connect to previous
+            duply_slate_node = duplicate_node(slate_node)
+            duply_slate_node.setInput(0, previous_node)
+            previous_node = duply_slate_node
+            temporary_nodes.append(duply_slate_node)
+
+            # add viewer display transformation node
             dag_node = nuke.createNode("OCIODisplay")
             dag_node.setInput(0, previous_node)
             previous_node = dag_node
             temporary_nodes.append(dag_node)
 
+        else:
+            # add duplicate slate node and connect to previous
+            duply_slate_node = duplicate_node(slate_node)
+            duply_slate_node.setInput(0, previous_node)
+            previous_node = duply_slate_node
+            temporary_nodes.append(duply_slate_node)
+
         # create write node
         write_node = nuke.createNode("Write")
-        file = fhead + "slate.png"
-        path = os.path.join(staging_dir, file).replace("\\\\", "/")
-        instance.data["slateFrame"] = path
+        file = fhead[:-1] + _output_name + "_slate.png"
+        path = os.path.join(
+            instance.data["stagingDir"], file).replace("\\\\", "/")
+
+        # add slate path to `slateFrames` instance data attr
+        if not instance.data.get("slateFrames"):
+            instance.data["slateFrames"] = {}
+
+        instance.data["slateFrames"][output_name or "*"] = path
+
+        # create write node
         write_node["file"].setValue(path)
         write_node["file_type"].setValue("png")
         write_node["raw"].setValue(1)
         write_node.setInput(0, previous_node)
         temporary_nodes.append(write_node)
 
-        # fill slate node with comments
-        self.add_comment_slate_node(instance)
-
         # Render frames
-        nuke.execute(write_node.name(), int(first_frame), int(last_frame))
-        # also render slate as sequence frame
-        nuke.execute(node_subset_name, int(first_frame), int(last_frame))
-
-        self.log.debug(
-            "slate frame path: {}".format(instance.data["slateFrame"]))
+        nuke.execute(
+            write_node.name(), int(slate_first_frame), int(slate_first_frame))
 
         # Clean up
         for node in temporary_nodes:
             nuke.delete(node)
 
-    def get_view_process_node(self):
-        # Select only the target node
-        if nuke.selectedNodes():
-            [n.setSelected(False) for n in nuke.selectedNodes()]
-
-        ipn_orig = None
-        for v in [n for n in nuke.allNodes()
-                  if "Viewer" in n.Class()]:
-            ip = v['input_process'].getValue()
-            ipn = v['input_process_node'].getValue()
-            if "VIEWER_INPUT" not in ipn and ip:
-                ipn_orig = nuke.toNode(ipn)
-                ipn_orig.setSelected(True)
-
-        if ipn_orig:
-            nuke.nodeCopy('%clipboard%')
-
-            [n.setSelected(False) for n in nuke.selectedNodes()]  # Deselect all
+    def _render_slate_to_sequence(self, instance):
+        # set slate frame
+        first_frame = instance.data["frameStartHandle"]
+        slate_first_frame = first_frame - 1
 
-            nuke.nodePaste('%clipboard%')
-
-            ipn = nuke.selectedNode()
-
-            return ipn
+        # render slate as sequence frame
+        nuke.execute(
+            instance.data["name"],
+            int(slate_first_frame),
+            int(slate_first_frame)
+        )
 
-    def add_comment_slate_node(self, instance):
-        node = instance.data.get("slateNode")
-        if not node:
-            return
+    def add_comment_slate_node(self, instance, node):
 
         comment = instance.context.data.get("comment")
         intent = instance.context.data.get("intent")
@@ -186,8 +255,8 @@ class ExtractSlateFrame(openpype.api.Extractor):
             "intent": intent
         })
 
-        for key, value in self.key_value_mapping.items():
-            enabled, template = value
+        for key, _values in self.key_value_mapping.items():
+            enabled, template = _values
             if not enabled:
                 self.log.debug("Key \\"{}\\" is disabled".format(key))
                 continue
@@ -221,5 +290,5 @@ class ExtractSlateFrame(openpype.api.Extractor):
                 ))
             except NameError:
                 self.log.warning((
-                    "Failed to set value \\"{}\\" on node attribute \\"{}\\""
+                    "Failed to set value \\"{0}\\" on node attribute \\"{0}\\""
                 ).format(value))
diff --git a/openpype/hosts/nuke/plugins/publish/extract_thumbnail.py b/openpype/hosts/nuke/plugins/publish/extract_thumbnail.py
index a622271855..2a919051d2 100644
--- a/openpype/hosts/nuke/plugins/publish/extract_thumbnail.py
+++ b/openpype/hosts/nuke/plugins/publish/extract_thumbnail.py
@@ -3,7 +3,10 @@ import os
 import nuke
 import pyblish.api
 import openpype
-from openpype.hosts.nuke.api.lib import maintained_selection
+from openpype.hosts.nuke.api import (
+    maintained_selection,
+    get_view_process_node
+)
 
 
 if sys.version_info[0] >= 3:
@@ -17,7 +20,7 @@ class ExtractThumbnail(openpype.api.Extractor):
 
     """
 
-    order = pyblish.api.ExtractorOrder + 0.01
+    order = pyblish.api.ExtractorOrder + 0.011
     label = "Extract Thumbnail"
 
     families = ["review"]
@@ -39,15 +42,32 @@ class ExtractThumbnail(openpype.api.Extractor):
             self.log.debug("instance.data[families]: {}".format(
                 instance.data["families"]))
 
-            self.render_thumbnail(instance)
-
-    def render_thumbnail(self, instance):
+            if instance.data.get("bakePresets"):
+                for o_name, o_data in instance.data["bakePresets"].items():
+                    self.render_thumbnail(instance, o_name, **o_data)
+            else:
+                viewer_process_swithes = {
+                    "bake_viewer_process": True,
+                    "bake_viewer_input_process": True
+                }
+                self.render_thumbnail(instance, None, **viewer_process_swithes)
+
+    def render_thumbnail(self, instance, output_name=None, **kwargs):
         first_frame = instance.data["frameStartHandle"]
         last_frame = instance.data["frameEndHandle"]
 
         # find frame range and define middle thumb frame
         mid_frame = int((last_frame - first_frame) / 2)
 
+        # solve output name if any is set
+        output_name = output_name or ""
+        if output_name:
+            output_name = "_" + output_name
+
+        bake_viewer_process = kwargs["bake_viewer_process"]
+        bake_viewer_input_process_node = kwargs[
+            "bake_viewer_input_process"]
+
         node = instance[0]  # group node
         self.log.info("Creating staging dir...")
 
@@ -106,17 +126,7 @@ class ExtractThumbnail(openpype.api.Extractor):
             temporary_nodes.append(rnode)
             previous_node = rnode
 
-        # bake viewer input look node into thumbnail image
-        if self.bake_viewer_input_process:
-            # get input process and connect it to baking
-            ipn = self.get_view_process_node()
-            if ipn is not None:
-                ipn.setInput(0, previous_node)
-                previous_node = ipn
-                temporary_nodes.append(ipn)
-
         reformat_node = nuke.createNode("Reformat")
-
         ref_node = self.nodes.get("Reformat", None)
         if ref_node:
             for k, v in ref_node:
@@ -129,8 +139,16 @@ class ExtractThumbnail(openpype.api.Extractor):
         previous_node = reformat_node
         temporary_nodes.append(reformat_node)
 
-        # bake viewer colorspace into thumbnail image
-        if self.bake_viewer_process:
+        # only create colorspace baking if toggled on
+        if bake_viewer_process:
+            if bake_viewer_input_process_node:
+                # get input process and connect it to baking
+                ipn = get_view_process_node()
+                if ipn is not None:
+                    ipn.setInput(0, previous_node)
+                    previous_node = ipn
+                    temporary_nodes.append(ipn)
+
             dag_node = nuke.createNode("OCIODisplay")
             dag_node.setInput(0, previous_node)
             previous_node = dag_node
@@ -138,7 +156,7 @@ class ExtractThumbnail(openpype.api.Extractor):
 
         # create write node
         write_node = nuke.createNode("Write")
-        file = fhead + "jpg"
+        file = fhead[:-1] + output_name + ".jpg"
         name = "thumbnail"
         path = os.path.join(staging_dir, file).replace("\\\\", "/")
         instance.data["thumbnail"] = path
@@ -168,30 +186,3 @@ class ExtractThumbnail(openpype.api.Extractor):
         # Clean up
         for node in temporary_nodes:
             nuke.delete(node)
-
-    def get_view_process_node(self):
-
-        # Select only the target node
-        if nuke.selectedNodes():
-            [n.setSelected(False) for n in nuke.selectedNodes()]
-
-        ipn_orig = None
-        for v in [n for n in nuke.allNodes()
-                  if "Viewer" == n.Class()]:
-            ip = v['input_process'].getValue()
-            ipn = v['input_process_node'].getValue()
-            if "VIEWER_INPUT" not in ipn and ip:
-                ipn_orig = nuke.toNode(ipn)
-                ipn_orig.setSelected(True)
-
-        if ipn_orig:
-            nuke.nodeCopy('%clipboard%')
-
-            # Deselect all
-            [n.setSelected(False) for n in nuke.selectedNodes()]
-
-            nuke.nodePaste('%clipboard%')
-
-            ipn = nuke.selectedNode()
-
-            return ipn
diff --git a/openpype/modules/deadline/plugins/publish/submit_publish_job.py b/openpype/modules/deadline/plugins/publish/submit_publish_job.py
index 6d08e72839..b54b00d099 100644
--- a/openpype/modules/deadline/plugins/publish/submit_publish_job.py
+++ b/openpype/modules/deadline/plugins/publish/submit_publish_job.py
@@ -147,7 +147,7 @@ class ProcessSubmittedJobOnFarm(pyblish.api.InstancePlugin):
     # mapping of instance properties to be transfered to new instance for every
     # specified family
     instance_transfer = {
-        "slate": ["slateFrame"],
+        "slate": ["slateFrames"],
         "review": ["lutPath"],
         "render2d": ["bakingNukeScripts", "version"],
         "renderlayer": ["convertToScanline"]
diff --git a/openpype/plugins/publish/extract_review_slate.py b/openpype/plugins/publish/extract_review_slate.py
index cff71f67ac..28685c2e90 100644
--- a/openpype/plugins/publish/extract_review_slate.py
+++ b/openpype/plugins/publish/extract_review_slate.py
@@ -1,4 +1,6 @@
 import os
+from pprint import pformat
+import re
 import openpype.api
 import pyblish
 from openpype.lib import (
@@ -21,6 +23,8 @@ class ExtractReviewSlate(openpype.api.Extractor):
     families = ["slate", "review"]
     match = pyblish.api.Subset
 
+    SUFFIX = "_slate"
+
     hosts = ["nuke", "shell"]
     optional = True
 
@@ -29,27 +33,18 @@ class ExtractReviewSlate(openpype.api.Extractor):
         if "representations" not in inst_data:
             raise RuntimeError("Burnin needs already created mov to work on.")
 
-        suffix = "_slate"
-        slate_path = inst_data.get("slateFrame")
-        ffmpeg_path = get_ffmpeg_tool_path("ffmpeg")
+        # get slates frame from upstream
+        slates_data = inst_data.get("slateFrames")
+        if not slates_data:
+            # make it backward compatible and open for slates generator
+            # premium plugin
+            slates_data = {
+                "*": inst_data["slateFrame"]
+            }
 
-        slate_streams = get_ffprobe_streams(slate_path, self.log)
-        # Try to find first stream with defined 'width' and 'height'
-        # - this is to avoid order of streams where audio can be as first
-        # - there may be a better way (checking `codec_type`?)+
-        slate_width = None
-        slate_height = None
-        for slate_stream in slate_streams:
-            if "width" in slate_stream and "height" in slate_stream:
-                slate_width = int(slate_stream["width"])
-                slate_height = int(slate_stream["height"])
-                break
+        self.log.info("_ slates_data: {}".format(pformat(slates_data)))
 
-        # Raise exception of any stream didn't define input resolution
-        if slate_width is None:
-            raise AssertionError((
-                "FFprobe couldn't read resolution from input file: \\"{}\\""
-            ).format(slate_path))
+        ffmpeg_path = get_ffmpeg_tool_path("ffmpeg")
 
         if "reviewToWidth" in inst_data:
             use_legacy_code = True
@@ -77,6 +72,12 @@ class ExtractReviewSlate(openpype.api.Extractor):
             streams = get_ffprobe_streams(
                 input_path, self.log
             )
+            # get slate data
+            slate_path = self._get_slate_path(input_file, slates_data)
+            self.log.info("_ slate_path: {}".format(slate_path))
+
+            slate_width, slate_height = self._get_slates_resolution(slate_path)
+
             # Get video metadata
             (
                 input_width,
@@ -138,7 +139,7 @@ class ExtractReviewSlate(openpype.api.Extractor):
             _remove_at_end = []
 
             ext = os.path.splitext(input_file)[1]
-            output_file = input_file.replace(ext, "") + suffix + ext
+            output_file = input_file.replace(ext, "") + self.SUFFIX + ext
 
             _remove_at_end.append(input_path)
 
@@ -369,6 +370,43 @@ class ExtractReviewSlate(openpype.api.Extractor):
 
         self.log.debug(inst_data["representations"])
 
+    def _get_slate_path(self, input_file, slates_data):
+        slate_path = None
+        for sl_n, _slate_path in slates_data.items():
+            if "*" in sl_n:
+                slate_path = _slate_path
+                break
+            elif re.search(sl_n, input_file):
+                slate_path = _slate_path
+                break
+
+        if not slate_path:
+            raise AttributeError(
+                "Missing slates paths: {}".format(slates_data))
+
+        return slate_path
+
+    def _get_slates_resolution(self, slate_path):
+        slate_streams = get_ffprobe_streams(slate_path, self.log)
+        # Try to find first stream with defined 'width' and 'height'
+        # - this is to avoid order of streams where audio can be as first
+        # - there may be a better way (checking `codec_type`?)+
+        slate_width = None
+        slate_height = None
+        for slate_stream in slate_streams:
+            if "width" in slate_stream and "height" in slate_stream:
+                slate_width = int(slate_stream["width"])
+                slate_height = int(slate_stream["height"])
+                break
+
+        # Raise exception of any stream didn't define input resolution
+        if slate_width is None:
+            raise AssertionError((
+                "FFprobe couldn't read resolution from input file: \\"{}\\""
+            ).format(slate_path))
+
+        return (slate_width, slate_height)
+
     def _get_video_metadata(self, streams):
         input_timecode = ""
         input_width = None