예제 #1
0
    def buildScript(self):
        """
        Override the default buildScript functionality to also output a temp movie
        file if needed for uploading to Shotgun
        """

        # This is a bit of a hack to account for some changes to the
        # transcode exporter that ships with Nuke/Hiero 9.0 compared
        # to earlier versions of Hiero.

        file_type = self._preset.properties()["file_type"]
        self.app.log_debug("Transcode export file_type: %s" % file_type)

        if file_type in ["mov", "ffmpeg"]:
            if not self._preset.properties()[file_type].get("encoder"):
                encoder_name = self.app.get_default_encoder_name()
                self._preset.properties()[file_type]["encoder"] = encoder_name

        # Build the usual script using the base code
        FnTranscodeExporter.TranscodeExporter.buildScript(self)
        self.app.log_debug("Transcode base script built")

        # If we are not creating a version then we do not need the extra node
        if not self._preset.properties()["create_version"]:
            return

        if file_type in ["mov", "ffmpeg"]:
            # already outputting a mov file, use that for upload
            self._quicktime_path = self.resolvedExportPath()
            self._temp_quicktime = False
            return

        self._quicktime_path = os.path.join(tempfile.mkdtemp(), "preview.mov")
        self._temp_quicktime = True
        nodeName = "SG Screening Room Media"

        framerate = None
        if self._sequence:
            framerate = self._sequence.framerate()
        if self._clip.framerate().isValid():
            framerate = self._clip.framerate()

        preset = FnTranscodeExporter.TranscodePreset(
            "Qt Write", self._preset.properties()
        )

        # insert the write node to generate the quicktime
        file_type, properties = self.app.execute_hook(
            "hook_get_quicktime_settings",
            for_shotgun=True,
            base_class=HieroGetQuicktimeSettings,
        )
        self.app.log_info("Transcode quicktime settings: %s" % (properties,))
        preset.properties().update(
            {"file_type": file_type, file_type: properties,}
        )

        # Sadly Foundry has a habit of changing the interfaces of
        # their Python classes out from under us, so now we're going
        # to have to handle this the ugly way, via introspecting the
        # arguments expected by the createWriteNode method.
        arg_spec = inspect.getargspec(FnExternalRender.createWriteNode)
        if "projectsettings" in arg_spec.args:
            kwargs = dict(
                path=self._quicktime_path,
                preset=preset,
                nodeName=nodeName,
                framerate=framerate,
                projectsettings=self._projectSettings,
            )
        elif "ctx" in arg_spec.args:
            kwargs = dict(
                ctx=self,
                path=self._quicktime_path,
                preset=preset,
                nodeName=nodeName,
                framerate=framerate,
                project=self._project,
            )
        else:
            kwargs = dict(
                path=self._quicktime_path,
                preset=preset,
                nodeName=nodeName,
                framerate=framerate,
                project=self._project,
            )
        mov_write_node = FnExternalRender.createWriteNode(**kwargs)

        # We create a push node and connect it to the set node we created just before the base write node was created
        # This means that our write node will parent to the same node the base write node gets parented to.
        push_command = nuke.PushNode(self._write_set_node_label)
        self._script.addNode(push_command)

        self._script.addNode(mov_write_node)
    def _taskStep(self):
        hiero.core.TaskBase.taskStep(self)
        if self._nothingToDo:
            return False

        script = nuke.ScriptWriter()
        start, end = self.outputRange(ignoreRetimes=True, clampToSource=False)
        unclampedStart = start
        hiero.core.log.debug("rootNode range is %s %s %s", start, end,
                             self._startFrame)

        firstFrame = start
        if self._startFrame is not None:
            firstFrame = self._startFrame

        # if startFrame is negative we can only assume this is intentional
        if start < 0 and (self._startFrame is None or self._startFrame >= 0):
            # We dont want to export an image sequence with negative frame numbers
            self.setWarning(
                "%i Frames of handles will result in a negative frame index.\nFirst frame clamped to 0."
                % self._cutHandles)
            start = 0
            firstFrame = 0

        framerate = self._sequence.framerate()
        dropFrames = self._sequence.dropFrame()
        if self._clip and self._clip.framerate().isValid():
            framerate = self._clip.framerate()
            dropFrames = self._clip.dropFrame()
        fps = framerate.toFloat()

        showAnnotations = False

        # Create the root node, this specifies the global frame range and frame rate
        rootNode = nuke.RootNode(start, end, fps, showAnnotations)
        rootNode.addProjectSettings(self._projectSettings)
        dailiesScriptCheck = 'writeNodeManager.checkDailiesTab()'
        rootNode.setKnob("onScriptLoad", dailiesScriptCheck)
        script.addNode(rootNode)

        if isinstance(self._item, hiero.core.TrackItem):
            rootNode.addInputTextKnob(
                "shot_guid",
                value=hiero.core.FnNukeHelpers._guidFromCopyTag(self._item),
                tooltip=
                "This is used to identify the master track item within the script",
                visible=False)
            inHandle, outHandle = self.outputHandles(self._retime != True)
            rootNode.addInputTextKnob("in_handle",
                                      value=int(inHandle),
                                      visible=False)
            rootNode.addInputTextKnob("out_handle",
                                      value=int(outHandle),
                                      visible=False)

        # Set the format knob of the root node
        rootNode.setKnob("format", str(self.rootFormat()))

        # BUG 40367 - proxy_type should be set to 'scale' by default to reflect
        # the custom default set in Nuke. Sadly this value can't be queried,
        # as it's set by nuke.knobDefault, hence the hard coding.
        rootNode.setKnob("proxy_type", "scale")

        # Project setting for using OCIO nodes for colourspace transform
        useOCIONodes = self._project.lutUseOCIOForExport()

        # A dict of arguments which are used when calling addToNukeScript on any clip/sequence/trackitem
        addToScriptCommonArgs = {'useOCIO': useOCIONodes}

        writeNodes = self._createWriteNodes(firstFrame, start, end, framerate,
                                            rootNode)

        if not writeNodes:
            # Blank preset is valid, if preset has been set and doesn't exist, report as error
            self.setWarning(
                str("NukeShotExporter: No write node destination selected"))

        if self.writingSequence():
            self.writeSequence(script, addToScriptCommonArgs)

            # Write out the single track item
        else:
            self.writeTrackItem(script, addToScriptCommonArgs, firstFrame)

        script.pushLayoutContext("write", "%s_Render" % self._item.name())

        metadataNode = nuke.MetadataNode(metadatavalues=[(
            "hiero/project",
            self._projectName), (
                "hiero/project_guid",
                self._project.guid()), ("hiero/shot_tag_guid",
                                        self._tag_guid)])

        # Add sequence Tags to metadata
        metadataNode.addMetadataFromTags(self._sequence.tags())
        metadataNode.addMetadata([("hiero/sequence", self._sequence.name())])
        metadataNode.addMetadata([("hiero/shot", self._clip.name())])

        # Apply timeline offset to nuke output
        if isinstance(self._item, hiero.core.TrackItem):
            if self._cutHandles is None:
                # Whole clip, so timecode start frame is first frame of clip
                timeCodeNodeStartFrame = unclampedStart
            else:
                startHandle, endHandle = self.outputHandles()
                timeCodeNodeStartFrame = trackItemTimeCodeNodeStartFrame(
                    unclampedStart, self._item, startHandle, endHandle)
            timecodeStart = self._clip.timecodeStart()
        else:
            # Exporting whole sequence/clip
            timeCodeNodeStartFrame = unclampedStart
            timecodeStart = self._item.timecodeStart()

        script.addNode(
            nuke.AddTimeCodeNode(timecodeStart=timecodeStart,
                                 fps=framerate,
                                 dropFrames=dropFrames,
                                 frame=timeCodeNodeStartFrame))
        # The AddTimeCode field will insert an integer framerate into the metadata, if the framerate is floating point, we need to correct this
        metadataNode.addMetadata([("input/frame_rate", framerate.toFloat())])

        script.addNode(metadataNode)

        # Add the delivery gizmo
        deliveryNode = nuke.Node('delivery')
        writeNodes.append(nuke.PushNode("ScriptEnd"))
        writeNodes.append(deliveryNode)

        # Generate Write nodes for nuke renders.

        # Bug 45843 - Branch the viewer when using OCIO nodes so
        # the OCIOColorSpace node created for the writer doesn't
        # get used when rendering the viewer.
        branchViewer = useOCIONodes
        if branchViewer:
            branchStackId = "OCIOWriterBranch"
            script.addNode(nuke.SetNode(branchStackId, 0))
            assert (isinstance(writeNodes[-1], nuke.WriteNode))
            ocioNodes = [
                node for node in writeNodes[-1].getNodes()
                if node.type() == "OCIOColorSpace"
            ]
            assert (len(ocioNodes))
            ocioNode = ocioNodes[0]
            ocioNode.setAlignToNode(metadataNode)

        scriptFilename = self.resolvedExportPath()

        for node in writeNodes:
            if node.knobs().has_key('file_type'):
                if node.knob('file_type') == 'mov':
                    slateNode = nuke.Node('slate')
                    script.addNode(slateNode)
            node.setInputNode(0, metadataNode)
            script.addNode(node)

        if branchViewer:
            script.addNode(nuke.PushNode(branchStackId))

        # add a viewer
        viewerNode = nuke.Node("Viewer")
        # Bug 45914: If the user has for some reason selected a custom OCIO config, but then set the 'Use OCIO nodes when export' option to False,
        # don't set the 'viewerProcess' knob, it's referencing a LUT in the OCIO config which Nuke doesn't know about
        setViewerProcess = True
        if not self._projectSettings[
                'lutUseOCIOForExport'] and self._projectSettings[
                    'ocioConfigPath']:
            setViewerProcess = False

        if setViewerProcess:
            # Bug 45845 - default viewer lut should be set in the comp
            viewerLut = _toNukeViewerLutFormat(
                self._projectSettings['lutSettingViewer'])
            viewerNode.setKnob("viewerProcess", viewerLut)

        script.addNode(viewerNode)

        hiero.core.log.debug("Writing Script to: %s", scriptFilename)

        script.popLayoutContext()

        # Layout the script
        #hiero.exporters.FnScriptLayout.scriptLayout(script)

        script.writeToDisk(scriptFilename)

        # Nothing left to do, return False.
        return False
    def _createWriteNodes(self, firstFrame, start, end, framerate, rootNode):

        # To add Write nodes, we get a task for the paths with the preset
        # (default is the "Nuke Write Node" preset) and ask it to generate the Write node for
        # us, since it knows all about codecs and extensions and can do the token
        # substitution properly for that particular item.
        # And doing it here rather than in taskStep out of threading paranoia.
        writeNodes = []

        firstWriteNode = True

        # Create a stack to prevent multiple write nodes inputting into each other
        stackId = "ScriptEnd"
        writeNodes.append(nuke.SetNode(stackId, 0))

        writePaths = self._preset.properties()["writePaths"]

        for (itemPath, itemPreset) in self._exportTemplate.flatten():
            for writePath in writePaths:
                if writePath == itemPath:
                    # Generate a task on same items as this one but swap in the shot path that goes with this preset.
                    taskData = hiero.core.TaskData(
                        itemPreset,
                        self._item,
                        self._exportRoot,
                        itemPath,
                        self._version,
                        self._exportTemplate,
                        project=self._project,
                        cutHandles=self._cutHandles,
                        retime=self._retime,
                        startFrame=firstFrame,
                        resolver=self._resolver,
                        skipOffline=self._skipOffline,
                        shotNameIndex=self._shotNameIndex)
                    task = hiero.core.taskRegistry.createTaskFromPreset(
                        itemPreset, taskData)
                    if hasattr(task, "nukeWriteNode"):

                        # Push to the stack before adding the write node
                        writeNodes.append(nuke.PushNode(stackId))

                        try:
                            reformatNode = reformatNodeFromPreset(itemPreset)
                            if reformatNode:
                                writeNodes.append(reformatNode)
                        except Exception as e:
                            self.setError(str(e))

                        # Add Burnin group (if enabled)
                        burninGroup = task.addBurninNodes(script=None)
                        if burninGroup is not None:
                            writeNodes.append(burninGroup)

                        try:
                            writeNode = task.nukeWriteNode(
                                framerate, project=self._project)
                            writeNode.setKnob("first", start)
                            writeNode.setKnob("last", end)
                            if writeNode.knob('file_type') == 'mov':
                                writeNode.setKnob(
                                    'file',
                                    "[python writeNodeManager.setOutputPath('mov')]"
                                )
                                writeNode.setKnob(
                                    'afterRender',
                                    'ftrackUpload.uploadToFtrack()')
                            else:
                                writeNode.setKnob(
                                    'file',
                                    "[python writeNodeManager.setOutputPath('img')]"
                                )
                            writeNodes.append(writeNode)

                            # Set the first write node in the list as the one to be shown/rendered in the timeline
                            if firstWriteNode:
                                firstWriteNode = False
                                rootNode.setKnob(
                                    nuke.RootNode.kTimelineWriteNodeKnobName,
                                    writeNode.knob("name"))

                        except RuntimeError as e:
                            # Failed to generate write node, set task error in export queue
                            # Most likely because could not map default colourspace for format settings.
                            self.setError(str(e))

        return writeNodes
    def taskStep(self):
        FnShotExporter.ShotTask.taskStep(self)
        if self._nothingToDo:
            return False

        eventManager = FnAssetAPI.Events.getEventManager()
        script = nuke.ScriptWriter()

        start, end = self.outputRange(ignoreRetimes=True, clampToSource=False)
        unclampedStart = start
        hiero.core.log.debug("rootNode range is %s %s %s", start, end,
                             self._startFrame)

        firstFrame = start
        if self._startFrame is not None:
            firstFrame = self._startFrame

        # if startFrame is negative we can only assume this is intentional
        if start < 0 and (self._startFrame is None or self._startFrame >= 0):
            # We dont want to export an image sequence with negative frame numbers
            self.setWarning(
                "%i Frames of handles will result in a negative frame index.\nFirst frame clamped to 0."
                % self._cutHandles)
            start = 0

        # Clip framerate may be invalid, then use parent sequence framerate
        framerate = self._sequence.framerate()
        dropFrames = self._sequence.dropFrame()
        if self._clip and self._clip.framerate().isValid():
            framerate = self._clip.framerate()
            dropFrames = self._clip.dropFrame()
        fps = framerate.toFloat()

        # Create the root node, this specifies the global frame range and frame rate
        rootNode = nuke.RootNode(start, end, fps)
        rootNode.addProjectSettings(self._projectSettings)
        #rootNode.setKnob("project_directory", os.path.split(self.resolvedExportPath())[0])
        script.addNode(rootNode)

        reformat = None
        # Set the root node format to default to source format
        if isinstance(self._item, hiero.core.Sequence) or self._collate:
            reformat = self._sequence.format().addToNukeScript(None)
        elif isinstance(self._item, hiero.core.TrackItem):
            reformat = self._clip.format().addToNukeScript(None)

        if isinstance(self._item, hiero.core.TrackItem):
            rootNode.addInputTextKnob(
                "shot_guid",
                value=hiero.core.FnNukeHelpers._guidFromCloneTag(self._item),
                tooltip=
                "This is used to identify the master track item within the script"
            )
            inHandle, outHandle = self.outputHandles(self._retime != True)
            rootNode.addInputTextKnob("in_handle", value=int(inHandle))
            rootNode.addInputTextKnob("out_handle", value=int(outHandle))

        # This sets the format knob of the root node in the Nuke Script
        rootReformat = None
        if isinstance(self._item, hiero.core.Sequence) or self._collate:
            rootReformat = self._sequence.format().addToNukeScript(None)
        elif isinstance(self._item, hiero.core.TrackItem):
            rootReformat = self._item.parentSequence().format(
            ).addToNukeScript(None)

        rootNode.setKnob("format", rootReformat.knob("format"))

        # Add Unconnected additional nodes
        if self._preset.properties()["additionalNodesEnabled"]:
            script.addNode(
                FnExternalRender.createAdditionalNodes(
                    FnExternalRender.kUnconnected,
                    self._preset.properties()["additionalNodesData"],
                    self._item))

        # Project setting for using OCIO nodes for colourspace transform
        useOCIONodes = self._project.lutUseOCIOForExport()

        # To add Write nodes, we get a task for the paths with the preset
        # (default is the "Nuke Write Node" preset) and ask it to generate the Write node for
        # us, since it knows all about codecs and extensions and can do the token
        # substitution properly for that particular item.
        # And doing it here rather than in taskStep out of threading paranoia.
        self._writeNodes = []

        stackId = "ScriptEnd"
        self._writeNodes.append(nuke.SetNode(stackId, 0))

        writePathExists = False
        writePaths = self._preset.properties()["writePaths"]

        for (itemPath, itemPreset) in self._exportTemplate.flatten():
            for writePath in writePaths:
                if writePath == itemPath:
                    # Generate a task on same items as this one but swap in the shot path that goes with this preset.
                    taskData = hiero.core.TaskData(
                        itemPreset,
                        self._item,
                        self._exportRoot,
                        itemPath,
                        self._version,
                        self._exportTemplate,
                        project=self._project,
                        cutHandles=self._cutHandles,
                        retime=self._retime,
                        startFrame=firstFrame,
                        resolver=self._resolver,
                        skipOffline=self._skipOffline)
                    task = hiero.core.taskRegistry.createTaskFromPreset(
                        itemPreset, taskData)
                    if hasattr(task, "nukeWriteNode"):
                        self._writeNodes.append(nuke.PushNode(stackId))

                        rf = itemPreset.properties()["reformat"]
                        # If the reformat field has been set, create a reformat node immediately before the Write.
                        if str(rf["to_type"]) == nuke.ReformatNode.kToFormat:
                            if "width" in rf and "height" in rf and "pixelAspect" in rf and "name" in rf and "resize" in rf:
                                format = hiero.core.Format(
                                    rf["width"], rf["height"],
                                    rf["pixelAspect"], rf["name"])
                                resize = rf["resize"]
                                reformat = format.addToNukeScript(
                                    None, resize=resize)
                                self._writeNodes.append(reformat)
                            else:
                                self.setError(
                                    "reformat mode set to kToFormat but preset properties do not contain required settings."
                                )

                        # Add Burnin group (if enabled)
                        burninGroup = task.addBurninNodes(script=None)
                        if burninGroup is not None:
                            self._writeNodes.append(burninGroup)

                        try:
                            writeNode = task.nukeWriteNode(
                                framerate,
                                projectsettings=self._projectSettings)
                            writeNode.setKnob("first", start)
                            writeNode.setKnob("last", end)

                            self._writeNodes.append(writeNode)

                        except RuntimeError as e:
                            # Failed to generate write node, set task error in export queue
                            # Most likely because could not map default colourspace for format settings.
                            self.setError(str(e))

                    writePathExists = True

        # MPLEC TODO should enforce in UI that you can't pick things that won't work.
        if not writePaths:
            # Blank preset is valid, if preset has been set and doesn't exist, report as error
            self.setWarning(
                str("NukeShotExporter: No write node destination selected"))

        # If this flag is True, a read node pointing at the original media will be added
        # If read nodes which point at export items are selected, this flag will be set False
        originalMediaReadNode = True

        useEntityRefs = self._preset.properties().get("useAssets", False)

        for item in [self._item]:
            originalMediaReadNode = True
            if not self._collate:
                # Build read nodes for selected entries in the shot template
                readPaths = self._preset.properties()["readPaths"]
                for (itemPath, itemPreset) in self._exportTemplate.flatten():
                    for readPath in readPaths:
                        if itemPath == readPath:

                            # Generate a task on same items as this one but swap in the shot path that goes with this preset.
                            taskData = hiero.core.TaskData(
                                itemPreset,
                                item,
                                self._exportRoot,
                                itemPath,
                                self._version,
                                self._exportTemplate,
                                project=self._project,
                                cutHandles=self._cutHandles,
                                retime=self._retime,
                                startFrame=self._startFrame,
                                resolver=self._resolver,
                                skipOffline=self._skipOffline)
                            task = hiero.core.taskRegistry.createTaskFromPreset(
                                itemPreset, taskData)

                            readNodePath = task.resolvedExportPath()
                            itemStart, itemEnd = task.outputRange()
                            itemFirstFrame = firstFrame
                            if self._startFrame:
                                itemFirstFrame = self._startFrame

                            if hiero.core.isVideoFileExtension(
                                    os.path.splitext(readNodePath)[1].lower()):
                                # Don't specify frame range when media is single file
                                newSource = hiero.core.MediaSource(
                                    readNodePath)
                                itemEnd = itemEnd - itemStart
                                itemStart = 0

                            else:
                                # File is image sequence, so specify frame range
                                newSource = hiero.core.MediaSource(
                                    readNodePath +
                                    (" %i-%i" % task.outputRange()))

                            newClip = hiero.core.Clip(newSource, itemStart,
                                                      itemEnd)

                            originalMediaReadNode = False

                            if self._cutHandles is None:
                                newClip.addToNukeScript(
                                    script,
                                    firstFrame=itemFirstFrame,
                                    trimmed=True,
                                    useOCIO=useOCIONodes,
                                    additionalNodesCallback=self.
                                    _buildAdditionalNodes,
                                    nodeLabel=item.parent().name(),
                                    useEntityRefs=useEntityRefs)
                            else:
                                # Clone track item and replace source with new clip (which may be offline)
                                newTrackItem = hiero.core.TrackItem(
                                    item.name(), item.mediaType())

                                for tag in self._item.tags():
                                    newTrackItem.addTag(tag)

                                # Handles may not be exactly what the user specified. They may be clamped to media range
                                inHandle, outHandle = 0, 0
                                if self._cutHandles:
                                    # Get the output range without handles
                                    inHandle, outHandle = task.outputHandles()
                                    hiero.core.log.debug(
                                        "in/outHandle %s %s", inHandle,
                                        outHandle)

                                newTrackItem.setSource(newClip)

                                # Trackitem in/out
                                newTrackItem.setTimelineIn(item.timelineIn())
                                newTrackItem.setTimelineOut(item.timelineOut())

                                # Source in/out is the clip range less the handles.
                                newTrackItem.setSourceIn(inHandle * -1)
                                newTrackItem.setSourceOut((newClip.duration() -
                                                           1) - outHandle)

                                #print "New trackitem (src/dst/clip) ", newTrackItem.sourceDuration(), newTrackItem.duration(), newClip.duration()

                                # Add track item to nuke script
                                newTrackItem.addToNukeScript(
                                    script,
                                    firstFrame=itemFirstFrame,
                                    includeRetimes=self._retime,
                                    retimeMethod=self._preset.properties()
                                    ["method"],
                                    startHandle=self._cutHandles,
                                    endHandle=self._cutHandles,
                                    additionalNodesCallback=self.
                                    _buildAdditionalNodes,
                                    useOCIO=useOCIONodes,
                                    nodeLabel=item.parent().name(),
                                    useEntityRefs=useEntityRefs)

            if originalMediaReadNode:

                if isinstance(self._item,
                              hiero.core.Sequence) or self._collate:

                    # When building a collated sequence, everything is offset by 1000
                    # This gives head room for shots which may go negative when transposed to a
                    # custom start frame. This offset is negated during script generation.
                    offset = -1000 if self._collate else 0
                    self._sequence.addToNukeScript(
                        script,
                        additionalNodesCallback=self._buildAdditionalNodes,
                        includeRetimes=True,
                        offset=offset,
                        useOCIO=useOCIONodes,
                        skipOffline=self._skipOffline,
                        useEntityRefs=useEntityRefs)
                elif isinstance(self._item, hiero.core.TrackItem):
                    clip = item.source()

                    # Add a Read Node for this Clip.
                    if self._cutHandles is None:
                        clip.addToNukeScript(
                            script,
                            firstFrame=firstFrame,
                            trimmed=True,
                            additionalNodesCallback=self._buildAdditionalNodes,
                            useOCIO=useOCIONodes,
                            nodeLabel=item.parent().name(),
                            useEntityRefs=useEntityRefs)
                    else:
                        item.addToNukeScript(
                            script,
                            firstFrame=firstFrame,
                            includeRetimes=self._retime,
                            retimeMethod=self._preset.properties()["method"],
                            startHandle=self._cutHandles,
                            endHandle=self._cutHandles,
                            additionalNodesCallback=self._buildAdditionalNodes,
                            useOCIO=useOCIONodes,
                            nodeLabel=item.parent().name(),
                            useEntityRefs=useEntityRefs)

        metadataNode = nuke.MetadataNode(metadatavalues=[(
            "hiero/project",
            self._projectName), (
                "hiero/project_guid",
                self._project.guid()), ("hiero/shot_tag_guid",
                                        self._tag_guid)])

        # Add sequence Tags to metadata
        metadataNode.addMetadata([('"hiero/tags/' + tag.name() + '"',
                                   tag.name())
                                  for tag in self._sequence.tags()])
        metadataNode.addMetadata([('"hiero/tags/' + tag.name() + '/note"',
                                   tag.note())
                                  for tag in self._sequence.tags()])

        # Apply timeline offset to nuke output
        if isinstance(self._item, hiero.core.TrackItem):
            if self._cutHandles is None:
                # Whole clip, so timecode start frame is first frame of clip
                timeCodeNodeStartFrame = unclampedStart
            else:
                # Exporting shot with handles, adjust timecode start frame by shot trim and handle count
                timeCodeNodeStartFrame = (
                    unclampedStart - self._item.sourceIn()) + self._cutHandles
            timecodeStart = self._clip.timecodeStart()
        else:
            # Exporting whole sequence/clip
            timeCodeNodeStartFrame = unclampedStart
            timecodeStart = self._item.timecodeStart()

        script.addNode(
            nuke.AddTimeCodeNode(timecodeStart=timecodeStart,
                                 fps=framerate,
                                 dropFrames=dropFrames,
                                 frame=timeCodeNodeStartFrame))
        # The AddTimeCode field will insert an integer framerate into the metadata, if the framerate is floating point, we need to correct this
        metadataNode.addMetadata([("input/frame_rate", framerate.toFloat())])

        script.addNode(metadataNode)

        # Generate Write nodes for nuke renders.
        for node in self._writeNodes:
            script.addNode(node)
            # We do this here, rather than when its created so the event happens at a
            # suitable time, otherwise, it's called before the Read node is created.
            if node.type() == "Write" and self._preset.properties(
            )["useAssets"]:
                ## @todo Assuming that we're always on a thread for now, this should be verified
                eventManager.blockingEvent(False, 'hieroToNukeScriptAddWrite',
                                           self._item, node.knob("file"), node,
                                           script)

        # add a viewer
        viewerNode = nuke.Node("Viewer")
        script.addNode(viewerNode)

        self._writeScript(script)

        # Nothing left to do, return False.
        return False