From ea5461aa367968156fbb9973b8e15b9a663b659f Mon Sep 17 00:00:00 2001 From: SaiAparna Ramamurthy Date: Mon, 19 Mar 2018 17:13:27 +0530 Subject: [PATCH 01/12] #442: Working version of nuke subprocess --- app.py | 2 +- hooks/nuke_batch_render_movie.py | 264 +++++++++++++ info.yml | 14 + python/tk_multi_reviewsubmission/renderer.py | 369 +++++++++++-------- 4 files changed, 492 insertions(+), 157 deletions(-) create mode 100644 hooks/nuke_batch_render_movie.py diff --git a/app.py b/app.py index 9f9959e..59f4b1b 100644 --- a/app.py +++ b/app.py @@ -74,7 +74,7 @@ def render_and_submit_version(self, template, fields, first_frame, last_frame, s # Make sure we don't overwrite the caller's fields fields = copy.copy(fields) - # Tweak fields so that we'll be getting nuke formated sequence markers (%03d, %04d etc): + # Tweak fields so that we'll be getting nuke formatted sequence markers (%03d, %04d etc): for key_name in [key.name for key in template.keys.values() if isinstance(key, sgtk.templatekey.SequenceKey)]: fields[key_name] = "FORMAT: %d" diff --git a/hooks/nuke_batch_render_movie.py b/hooks/nuke_batch_render_movie.py new file mode 100644 index 0000000..4a6285f --- /dev/null +++ b/hooks/nuke_batch_render_movie.py @@ -0,0 +1,264 @@ +import os +import sys +import traceback +import getopt + +import nuke + + +def __create_scale_node(width, height): + """ + Create the Nuke scale node to resize the content. + """ + scale = nuke.nodes.Reformat() + scale["type"].setValue("to box") + scale["box_width"].setValue(width) + scale["box_height"].setValue(height) + scale["resize"].setValue("fit") + scale["box_fixed"].setValue(True) + scale["center"].setValue(True) + scale["black_outside"].setValue(True) + return scale + + +def __create_output_node(path, codec_settings, logger=None): + """ + Create the Nuke output node for the movie. + """ + # get the Write node settings we'll use for generating the Quicktime + + wn_settings = codec_settings.get('quicktime', {}) + + node = nuke.nodes.Write(file_type=wn_settings.get("file_type", '')) + + # apply any additional knob settings provided by the hook. Now that the knob has been + # created, we can be sure specific file_type settings will be valid. + for knob_name, knob_value in wn_settings.iteritems(): + if knob_name != "file_type": + node.knob(knob_name).setValue(knob_value) + + # Don't fail if we're in proxy mode. The default Nuke publish will fail if + # you try and publish while in proxy mode. But in earlier versions of + # tk-multi-publish (< v0.6.9) if there is no proxy template set, it falls + # back on the full-res version and will succeed. This handles that case + # and any custom cases where you may want to send your proxy render to + # screening room. + root_node = nuke.root() + is_proxy = root_node['proxy'].value() + if is_proxy: + if logger: + logger.info("Proxy mode is ON. Rendering proxy.") + node["proxy"].setValue(path.replace(os.sep, "/")) + else: + node["file"].setValue(path.replace(os.sep, "/")) + + return node + + +def render_movie_in_nuke(path, output_path, + width, height, + first_frame, last_frame, + version, name, + color_space, + app_settings, ctx_info, render_info): + """ + Use Nuke to render a movie. This assumes we're running _inside_ Nuke. + + :param path: Path to the input frames for the movie + :param output_path: Path to the output movie that will be rendered + :param width: Width of the output movie + :param height: Height of the output movie + :param first_frame: Start frame for the output movie + :param last_frame: End frame for the output movie + :param version: Version number to use for the output movie slate and burn-in + :param name: Name to use in the slate for the output movie + :param color_space: Colorspace of the input frames + """ + output_node = None + # ctx = self.__app.context + + # create group where everything happens + group = nuke.nodes.Group() + + # now operate inside this group + group.begin() + try: + # create read node + read = nuke.nodes.Read(name="source", file=path.replace(os.sep, "/")) + read["on_error"].setValue("black") + read["first"].setValue(first_frame) + read["last"].setValue(last_frame) + if color_space: + read["colorspace"].setValue(color_space) + + # now create the slate/burnin node + burn = nuke.nodePaste(render_info.get('burnin_nk')) + burn.setInput(0, read) + + font = render_info.get('slate_font') + + # set the fonts for all text fields + burn.node("top_left_text")["font"].setValue(font) + burn.node("top_right_text")["font"].setValue(font) + burn.node("bottom_left_text")["font"].setValue(font) + burn.node("framecounter")["font"].setValue(font) + burn.node("slate_info")["font"].setValue(font) + + # add the logo + logo = app_settings.get('slate_logo', '') + if not os.path.isfile(logo): + logo = '' + + burn.node("logo")["file"].setValue(logo) + + # format the burnins + ver_num_pad = app_settings.get('version_number_padding', 4) + version_padding_format = "%%0%dd" % ver_num_pad + version_str = version_padding_format % version + + task = ctx_info.get('task', {}) + step = ctx_info.get('step', {}) + + if task: + version_label = "%s, v%s" % (task.get("name", ''), version_str) + elif step: + version_label = "%s, v%s" % (step.get("name", ''), version_str) + else: + version_label = "v%s" % version_str + + project = ctx_info.get('project', {}) + entity = ctx_info.get('entity', {}) + + burn.node("top_left_text")["message"].setValue(project.get("name", '')) + burn.node("top_right_text")["message"].setValue(entity.get("name", '')) + burn.node("bottom_left_text")["message"].setValue(version_label) + + # and the slate + slate_str = "Project: %s\n" % project.get("name", '') + slate_str += "%s: %s\n" % (entity.get("type", ''), entity.get("name", '')) + slate_str += "Name: %s\n" % name.capitalize() + slate_str += "Version: %s\n" % version_str + + # --- FOR DEBUGGING ONLY: set this next value to True in order to test an Exception occurring here + DEBUG_TEST_EXCEPTION = False + if DEBUG_TEST_EXCEPTION: + raise Exception( + "Forcing an Exception just to test how calling apps will respond to errors here.") + + if task: + slate_str += "Task: %s\n" % task.get("name", '') + elif step: + slate_str += "Step: %s\n" % step.get("name", '') + + slate_str += "Frames: %s - %s\n" % (first_frame, last_frame) + + burn.node("slate_info")["message"].setValue(slate_str) + + # create a scale node + scale = __create_scale_node(width, height) + scale.setInput(0, burn) + + # Create the output node + output_node = __create_output_node(output_path, render_info.get('codec_settings', {})) + output_node.setInput(0, scale) + + except: + return {'status': 'ERROR', 'error_msg': '{0}'.format(traceback.format_exc()), + 'output_path': output_path} + + group.end() + + if output_node: + try: + # Make sure the output folder exists + output_folder = os.path.dirname(output_path) + # self.__app.ensure_folder_exists(output_folder) + if not os.path.isdir(output_folder): + os.makedirs(output_folder) + + # Render the outputs, first view only + nuke.executeMultiple([output_node], ([first_frame - 1, last_frame, 1],), + [nuke.views()[0]]) + except: + return {'status': 'ERROR', 'error_msg': '{0}'.format(traceback.format_exc()), + 'output_path': output_path} + + # Cleanup after ourselves + nuke.delete(group) + + return {'status': 'OK'} + + +def usage(): + print(''' + Usage: python {0} [ OPTIONS ] + -h | --help ... print this usage message and exit. + --path ... specify full path to frames, with frame spec ... e.g. ".%04d.exr" + --output_path ... specify full path to output movie + --width ... specify width for output movie + --height ... specify height for output movie + --first_frame ... specify first frame number of the input frames + --last_frame ... specify last frame number of the input frames + --version ... specify version number for the slate + --name ... specify name for the slate + --color_space ... specify color space to use for the movie generation + --app_settings ... specify app settings from the Toolkit app calling this + --shotgun_context ... specify shotgun context from the Toolkit app calling this + --render_info ... specify render info from the Toolkit app calling this +'''.format(os.path.basename(sys.argv[0]))) + + +if __name__ == '__main__': + + data_keys = [ + 'path', 'output_path', 'width', 'height', 'first_frame', 'last_frame', + 'version', 'name', 'color_space', 'app_settings', 'shotgun_context', 'render_info', + ] + + non_str_data_list = { + 'width', 'height', 'first_frame', 'last_frame', 'version', + 'app_settings', 'shotgun_context', 'render_info', + } + + short_opt_str = "h" + long_opt_list = ['help'] + ['{0}='.format(k) for k in data_keys] + + input_data = {} + + try: + opt_list, arg_list = getopt.getopt(sys.argv[1:], short_opt_str, long_opt_list) + except getopt.GetoptError as err: + print(str(err)) + usage() + sys.exit(1) + + for opt, opt_value in opt_list: + if opt in ('-h', '--help'): + usage() + sys.exit(0) + elif opt.replace('--', '') in data_keys: + d_key = opt.replace('--', '') + input_data[d_key] = opt_value + if d_key in non_str_data_list: + stmt = '''input_data[ d_key ] = {0}'''.format(opt_value) + exec (stmt) + + for d_key in data_keys: + if d_key not in input_data: + print('ERROR - missing input argument for "--{0}" flag. Aborting'.format(d_key)) + usage() + sys.exit(2) + + ret_status = render_movie_in_nuke(input_data['path'], input_data['output_path'], + input_data['width'], input_data['height'], + input_data['first_frame'], input_data['last_frame'], + input_data['version'], input_data['name'], + input_data['color_space'], + input_data['app_settings'], + input_data['shotgun_context'], + input_data['render_info']) + + if ret_status.get('status', '') == 'OK': + sys.exit(0) + else: + sys.exit(3) diff --git a/info.yml b/info.yml index daea222..0d508d8 100644 --- a/info.yml +++ b/info.yml @@ -90,6 +90,20 @@ configuration: for review. default_value: '{self}/codec_settings.py' + nuke_linux_path: + type: str + description: The path to the application executable on Linux. + default_value: "" + + nuke_windows_path: + type: str + description: The path to the application executable on Windows. + default_value: "" + + nuke_mac_path: + type: str + description: The path to the application executable on Mac OS X. + default_value: "" # the Shotgun fields that this app needs in order to operate correctly requires_shotgun_fields: diff --git a/python/tk_multi_reviewsubmission/renderer.py b/python/tk_multi_reviewsubmission/renderer.py index 0af42bc..dae3f6f 100644 --- a/python/tk_multi_reviewsubmission/renderer.py +++ b/python/tk_multi_reviewsubmission/renderer.py @@ -1,34 +1,42 @@ # Copyright (c) 2013 Shotgun Software Inc. -# +# # CONFIDENTIAL AND PROPRIETARY -# -# This work is provided "AS IS" and subject to the Shotgun Pipeline Toolkit +# +# This work is provided "AS IS" and subject to the Shotgun Pipeline Toolkit # Source Code License included in this distribution package. See LICENSE. -# By accessing, using, copying or modifying this work you indicate your -# agreement to the Shotgun Pipeline Toolkit Source Code License. All rights +# By accessing, using, copying or modifying this work you indicate your +# agreement to the Shotgun Pipeline Toolkit Source Code License. All rights # not expressly granted therein are reserved by Shotgun Software Inc. import sgtk import os import sys -import nuke + +import subprocess +import traceback + +try: + import nuke +except: + nuke = None + class Renderer(object): - def __init__(self): """ Construction """ - self.__app = sgtk.platform.current_bundle() - + self.__app = sgtk.platform.current_bundle() + self._burnin_nk = os.path.join(self.__app.disk_location, "resources", "burnin.nk") - self._font = os.path.join(self.__app.disk_location, "resources", "liberationsans_regular.ttf") - - # If the slate_logo supplied was an empty string, the result of getting + self._font = os.path.join(self.__app.disk_location, "resources", + "liberationsans_regular.ttf") + + # If the slate_logo supplied was an empty string, the result of getting # the setting will be the config folder which is invalid so catch that # and make our logo path an empty string which Nuke won't have issues with. self._logo = None - if os.path.isfile( self.__app.get_setting("slate_logo", "") ): + if os.path.isfile(self.__app.get_setting("slate_logo", "")): self._logo = self.__app.get_setting("slate_logo", "") else: self._logo = "" @@ -37,151 +45,200 @@ def __init__(self): if sys.platform == "win32": self._font = self._font.replace(os.sep, "/") self._logo = self._logo.replace(os.sep, "/") - self._burnin_nk = self._burnin_nk.replace(os.sep, "/") + self._burnin_nk = self._burnin_nk.replace(os.sep, "/") - def render_movie_in_nuke(self, path, output_path, - width, height, - first_frame, last_frame, - version, name, - color_space): - """ - Use Nuke to render a movie. This assumes we're running _inside_ Nuke. - - :param path: Path to the input frames for the movie - :param output_path: Path to the output movie that will be rendered - :param width: Width of the output movie - :param height: Height of the output movie - :param first_frame: Start frame for the output movie - :param last_frame: End frame for the output movie - :param version: Version number to use for the output movie slate and burn-in - :param name: Name to use in the slate for the output movie - :param color_space: Colorspace of the input frames - """ - output_node = None - ctx = self.__app.context + def gather_nuke_render_info(self, path, output_path, + width, height, + first_frame, last_frame, + version, name, + color_space, override_context=None): + + nuke_render_info = {} + + # First get Nuke executable path from project configuration environment + setting_key_by_os = {'win32': 'nuke_windows_path', + 'linux2': 'nuke_linux_path', + 'darwin': 'nuke_mac_path'} + nuke_exe_path = self.__app.get_setting(setting_key_by_os[sys.platform]) + + if os.path.split(nuke_exe_path)[0] != '': + nuke_version_str = os.path.basename(os.path.dirname(nuke_exe_path)) # get Nuke version folder + else: + nuke_version_str = os.path.basename(nuke_exe_path).replace('.exe', '').replace('app', '') - # create group where everything happens - group = nuke.nodes.Group() - - # now operate inside this group - group.begin() - try: - # create read node - read = nuke.nodes.Read(name="source", file=path.replace(os.sep, "/")) - read["on_error"].setValue("black") - read["first"].setValue(first_frame) - read["last"].setValue(last_frame) - if color_space: - read["colorspace"].setValue(color_space) - - # now create the slate/burnin node - burn = nuke.nodePaste(self._burnin_nk) - burn.setInput(0, read) - - # set the fonts for all text fields - burn.node("top_left_text")["font"].setValue(self._font) - burn.node("top_right_text")["font"].setValue(self._font) - burn.node("bottom_left_text")["font"].setValue(self._font) - burn.node("framecounter")["font"].setValue(self._font) - burn.node("slate_info")["font"].setValue(self._font) - - # add the logo - burn.node("logo")["file"].setValue(self._logo) - - # format the burnins - version_padding_format = "%%0%dd" % self.__app.get_setting("version_number_padding") - version_str = version_padding_format % version - - if ctx.task: - version_label = "%s, v%s" % (ctx.task["name"], version_str) - elif ctx.step: - version_label = "%s, v%s" % (ctx.step["name"], version_str) - else: - version_label = "v%s" % version_str - - burn.node("top_left_text")["message"].setValue(ctx.project["name"]) - burn.node("top_right_text")["message"].setValue(ctx.entity["name"]) - burn.node("bottom_left_text")["message"].setValue(version_label) - - # and the slate - slate_str = "Project: %s\n" % ctx.project["name"] - slate_str += "%s: %s\n" % (ctx.entity["type"], ctx.entity["name"]) - slate_str += "Name: %s\n" % name.capitalize() - slate_str += "Version: %s\n" % version_str - - if ctx.task: - slate_str += "Task: %s\n" % ctx.task["name"] - elif ctx.step: - slate_str += "Step: %s\n" % ctx.step["name"] - - slate_str += "Frames: %s - %s\n" % (first_frame, last_frame) - - burn.node("slate_info")["message"].setValue(slate_str) - - # create a scale node - scale = self.__create_scale_node(width, height) - scale.setInput(0, burn) - - # Create the output node - output_node = self.__create_output_node(output_path) - output_node.setInput(0, scale) - finally: - group.end() - - if output_node: - # Make sure the output folder exists - output_folder = os.path.dirname(output_path) - self.__app.ensure_folder_exists(output_folder) - - # Render the outputs, first view only - nuke.executeMultiple([output_node], ([first_frame-1, last_frame, 1],), [nuke.views()[0]]) - - # Cleanup after ourselves - nuke.delete(group) - - - def __create_scale_node(self, width, height): - """ - Create the Nuke scale node to resize the content. - """ - scale = nuke.nodes.Reformat() - scale["type"].setValue("to box") - scale["box_width"].setValue(width) - scale["box_height"].setValue(height) - scale["resize"].setValue("fit") - scale["box_fixed"].setValue(True) - scale["center"].setValue(True) - scale["black_outside"].setValue(True) - return scale - - def __create_output_node(self, path): - """ - Create the Nuke output node for the movie. - """ # get the Write node settings we'll use for generating the Quicktime - wn_settings = self.__app.execute_hook_method("codec_settings_hook", - "get_quicktime_settings") - - node = nuke.nodes.Write(file_type=wn_settings.get("file_type")) - - # apply any additional knob settings provided by the hook. Now that the knob has been - # created, we can be sure specific file_type settings will be valid. - for knob_name, knob_value in wn_settings.iteritems(): - if knob_name != "file_type": - node.knob(knob_name).setValue(knob_value) - - # Don't fail if we're in proxy mode. The default Nuke publish will fail if - # you try and publish while in proxy mode. But in earlier versions of - # tk-multi-publish (< v0.6.9) if there is no proxy template set, it falls - # back on the full-res version and will succeed. This handles that case - # and any custom cases where you may want to send your proxy render to - # screening room. - root_node = nuke.root() - is_proxy = root_node['proxy'].value() - if is_proxy: - self.__app.log_info("Proxy mode is ON. Rendering proxy.") - node["proxy"].setValue(path.replace(os.sep, "/")) + writenode_quicktime_settings = self.__app.execute_hook_method("codec_settings_hook", + "get_quicktime_settings", + nuke_version_str=nuke_version_str) + + render_script_path = os.path.join(self.__app.disk_location, "hooks", + "nuke_batch_render_movie.py") + ctx = self.__app.context + if override_context: + ctx = override_context + + shotgun_context = {} + shotgun_context[ctx.entity.get('type').lower()] = ctx.entity.copy() + for add_entity in ctx.additional_entities: + shotgun_context[add_entity.get('type').lower()] = add_entity.copy() + if ctx.task: + shotgun_context['task'] = ctx.task.copy() + if ctx.step: + shotgun_context['step'] = ctx.task.copy() + shotgun_context['project'] = ctx.project.copy() + + app_settings = { + 'version_number_padding': self.__app.get_setting('version_number_padding'), + 'slate_logo': self._logo, + } + + render_info = { + 'burnin_nk': self._burnin_nk, + 'slate_font': self._font, + 'codec_settings': {'quicktime': writenode_quicktime_settings}, + } + + # # you can specify extra/override environment variables to pass to the Nuke execution + # # if you need to (e.g. pass along NUKE_INTERACTIVE or foundry_LICENSE, etc.) + # extra_env = nuke_settings.get('extra_env', {}) + # for k, v in extra_env.iteritems(): + # env_value = '' + # if type(v) is dict: + # # this means per OS value (key is value from sys.platform) + # env_value = str(v.get(sys.platform, '')) + # else: + # env_value = str(v) # ensure env var values are all string + # # only add to environment if value is not empty + # if env_value: + # extra_env[k] = env_value + + # set needed paths and force them to use forward slashes for use in Nuke (latter needed for Windows) + src_frames_path = path.replace('\\', '/') + movie_output_path = output_path.replace('\\', '/') + + nuke_render_info = { + 'width': width, + 'height': height, + 'first_frame': first_frame, + 'last_frame': last_frame, + 'version': version, + 'name': name, + 'color_space': color_space, + 'nuke_exe_path': nuke_exe_path, + 'nuke_version_str': nuke_version_str, + 'writenode_quicktime_settings': writenode_quicktime_settings, + 'render_script_path': render_script_path, + 'shotgun_context': shotgun_context, + 'app_settings': app_settings, + 'render_info': render_info, + # 'extra_env': extra_env, + 'src_frames_path': src_frames_path, + 'movie_output_path': movie_output_path, + } + + return nuke_render_info + + def render_movie_in_nuke(self, path, output_path, + width, height, + first_frame, last_frame, + version, name, + color_space, override_context=None, + active_progress_info=None): + + render_info = self.gather_nuke_render_info(path, output_path, width, height, first_frame, + last_frame, + version, name, color_space, override_context) + + run_in_batch_mode = True if nuke is None else False # if nuke module wasn't imported it will be None + + if run_in_batch_mode: + # -------------------------------------------------------------------------------------------- + # + # Running within Nuke interactive ... + # + # -------------------------------------------------------------------------------------------- + + # Set-up the subprocess command and arguments + cmd_and_args = [ + render_info.get('nuke_exe_path'), '-t', render_info.get('render_script_path'), + '--path', render_info.get('src_frames_path'), + '--output_path', render_info.get('movie_output_path'), + '--width', str(width), '--height', str(height), '--version', str(version), '--name', + name, + '--color_space', color_space, + '--first_frame', str(first_frame), + '--last_frame', str(last_frame), + '--app_settings', str(render_info.get('app_settings')), + '--shotgun_context', str(render_info.get('shotgun_context')), + '--render_info', str(render_info.get('render_info')), + ] + + extra_env = render_info.get('extra_env', {}) + if extra_env.get('NUKE_INTERACTIVE', '') in ('1',): + cmd_and_args.insert(1, '-i') + + subprocess_env = os.environ.copy() + subprocess_env.update(extra_env) + + p = subprocess.Popen(cmd_and_args, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, + env=subprocess_env) + + output_name = os.path.basename(render_info.get('movie_output_path')) + + progress_fn = progress_label = None + if active_progress_info: + progress_fn = active_progress_info.get('show_progress_fn') + progress_label = active_progress_info.get('label') + + output_lines = [] + + num_frames = last_frame - first_frame + 1 + write_count = 0 + + while p.poll() is None: + line = p.stdout.readline() + if line != '': + output_lines.append(line.rstrip()) + + percent_complete = float(write_count) / float(num_frames) * 100.0 + if progress_fn: + progress_fn(progress_label, + 'Nuke: {0:03.1f}%, {1}'.format(percent_complete, output_name)) + + if line.startswith('Writing '): + # The number of these lines will be number of frames + 1 + write_count += 1 + + if p.returncode != 0: + output_str = '\n'.join(output_lines) + # stmt = 'status_info = {0}'.format(output_str.split('[RETURN_STATUS_DATA]')[1]) + # exec (stmt) + status_info = output_str.split('[RETURN_STATUS_DATA]')[1] + return status_info + + return {'status': 'OK'} + else: - node["file"].setValue(path.replace(os.sep, "/")) + # -------------------------------------------------------------------------------------------- + # + # Running within Nuke interactive ... + # + # -------------------------------------------------------------------------------------------- + import importlib + + render_script_path = render_info.get('render_script_path') + + script_dir_path, script_filename = os.path.split(render_script_path) + sys.path.append(script_dir_path) - return node \ No newline at end of file + render_script_module = importlib.import_module(script_filename.replace('.py', '')) + ret_status = render_script_module.render_movie_in_nuke( + render_info.get('src_frames_path'), + render_info.get('movie_output_path'), + width, height, + first_frame, last_frame, + version, name, color_space, + render_info.get('app_settings'), + render_info.get('shotgun_context'), + render_info.get('render_info')) + return ret_status From 785e40363f898393f1eafa553a6103ea03194e45 Mon Sep 17 00:00:00 2001 From: SaiAparna Ramamurthy Date: Tue, 13 Mar 2018 15:38:57 +0530 Subject: [PATCH 02/12] #377: Made burnin path and logo path configurable. --- info.yml | 12 +++++++++--- python/tk_multi_reviewsubmission/renderer.py | 17 +++++++++-------- 2 files changed, 18 insertions(+), 11 deletions(-) diff --git a/info.yml b/info.yml index 0d508d8..3880671 100644 --- a/info.yml +++ b/info.yml @@ -73,8 +73,9 @@ configuration: if they are defined." slate_logo: - type: config_path - description: This is the path to an image to use on the slate such as a + type: template + fields: context + description: This is the template for a path to an image to use on the slate such as a company logo. The supplied image will be reformated into a 400 pixel box and the lower left corner of the logo will be aligned 100 pixels to the right and 100 pixels above the @@ -82,7 +83,6 @@ configuration: an alpha channel if you want to add transparency. Currently any image format supported by Nuke is adequate. If this setting is an empty string, no logo will be applied. - default_value: "" codec_settings_hook: type: hook @@ -104,6 +104,12 @@ configuration: type: str description: The path to the application executable on Mac OS X. default_value: "" + + burnin_path: + type: template + fields: context + description: Template for burnin nuke file path + # the Shotgun fields that this app needs in order to operate correctly requires_shotgun_fields: diff --git a/python/tk_multi_reviewsubmission/renderer.py b/python/tk_multi_reviewsubmission/renderer.py index dae3f6f..dbdd728 100644 --- a/python/tk_multi_reviewsubmission/renderer.py +++ b/python/tk_multi_reviewsubmission/renderer.py @@ -27,17 +27,18 @@ def __init__(self): Construction """ self.__app = sgtk.platform.current_bundle() + self._font = os.path.join(self.__app.disk_location, "resources", "liberationsans_regular.ttf") - self._burnin_nk = os.path.join(self.__app.disk_location, "resources", "burnin.nk") - self._font = os.path.join(self.__app.disk_location, "resources", - "liberationsans_regular.ttf") + burnin_template = self.__app.get_template("burnin_path") + self._burnin_nk = burnin_template.apply_fields({}) + # If a show specific burnin file has not been defined, take it from the default location + if not os.path.isfile(self._burnin_nk): + self._burnin_nk = os.path.join(self.__app.disk_location, "resources", "burnin.nk") - # If the slate_logo supplied was an empty string, the result of getting - # the setting will be the config folder which is invalid so catch that - # and make our logo path an empty string which Nuke won't have issues with. self._logo = None - if os.path.isfile(self.__app.get_setting("slate_logo", "")): - self._logo = self.__app.get_setting("slate_logo", "") + logo_template = self.__app.get_template("slate_logo") + if os.path.isfile(logo_template.apply_fields({})): + self._logo = logo_template.apply_fields({}) else: self._logo = "" From d8614e36ea5b6f25c18ffbfe25c21b6cbd58306a Mon Sep 17 00:00:00 2001 From: SaiAparna Ramamurthy Date: Thu, 22 Mar 2018 10:35:13 +0530 Subject: [PATCH 03/12] (WIP) Pass nuke version to codec_settings hook. --- python/tk_multi_reviewsubmission/renderer.py | 70 ++++++++------------ 1 file changed, 27 insertions(+), 43 deletions(-) diff --git a/python/tk_multi_reviewsubmission/renderer.py b/python/tk_multi_reviewsubmission/renderer.py index dbdd728..f95b27c 100644 --- a/python/tk_multi_reviewsubmission/renderer.py +++ b/python/tk_multi_reviewsubmission/renderer.py @@ -52,7 +52,7 @@ def gather_nuke_render_info(self, path, output_path, width, height, first_frame, last_frame, version, name, - color_space, override_context=None): + color_space): nuke_render_info = {} @@ -62,30 +62,37 @@ def gather_nuke_render_info(self, path, output_path, 'darwin': 'nuke_mac_path'} nuke_exe_path = self.__app.get_setting(setting_key_by_os[sys.platform]) - if os.path.split(nuke_exe_path)[0] != '': - nuke_version_str = os.path.basename(os.path.dirname(nuke_exe_path)) # get Nuke version folder + if nuke: + nuke_version_major = nuke.NUKE_VERSION_MAJOR else: - nuke_version_str = os.path.basename(nuke_exe_path).replace('.exe', '').replace('app', '') + # TODO: get from nuke context somehow? Is this even required? + if os.path.split(nuke_exe_path)[0] != '': + nuke_version_str = os.path.basename(os.path.dirname(nuke_exe_path)) # get Nuke version folder + else: + nuke_version_str = os.path.basename(nuke_exe_path).replace('.exe', '').replace('app', '') + + bits = nuke_version_str.split('v') + nuke_version_release = None + if len(bits) > 1: + nuke_version_release = bits[1] + nuke_version_major_minor = bits[0] + nuke_version_major = nuke_version_major_minor.split('.')[0] # get the Write node settings we'll use for generating the Quicktime writenode_quicktime_settings = self.__app.execute_hook_method("codec_settings_hook", "get_quicktime_settings", - nuke_version_str=nuke_version_str) + nuke_version_major=nuke_version_major) render_script_path = os.path.join(self.__app.disk_location, "hooks", "nuke_batch_render_movie.py") ctx = self.__app.context - if override_context: - ctx = override_context shotgun_context = {} - shotgun_context[ctx.entity.get('type').lower()] = ctx.entity.copy() - for add_entity in ctx.additional_entities: - shotgun_context[add_entity.get('type').lower()] = add_entity.copy() + shotgun_context['entity'] = ctx.entity.copy() if ctx.task: shotgun_context['task'] = ctx.task.copy() if ctx.step: - shotgun_context['step'] = ctx.task.copy() + shotgun_context['step'] = ctx.step.copy() shotgun_context['project'] = ctx.project.copy() app_settings = { @@ -99,20 +106,6 @@ def gather_nuke_render_info(self, path, output_path, 'codec_settings': {'quicktime': writenode_quicktime_settings}, } - # # you can specify extra/override environment variables to pass to the Nuke execution - # # if you need to (e.g. pass along NUKE_INTERACTIVE or foundry_LICENSE, etc.) - # extra_env = nuke_settings.get('extra_env', {}) - # for k, v in extra_env.iteritems(): - # env_value = '' - # if type(v) is dict: - # # this means per OS value (key is value from sys.platform) - # env_value = str(v.get(sys.platform, '')) - # else: - # env_value = str(v) # ensure env var values are all string - # # only add to environment if value is not empty - # if env_value: - # extra_env[k] = env_value - # set needed paths and force them to use forward slashes for use in Nuke (latter needed for Windows) src_frames_path = path.replace('\\', '/') movie_output_path = output_path.replace('\\', '/') @@ -127,12 +120,10 @@ def gather_nuke_render_info(self, path, output_path, 'color_space': color_space, 'nuke_exe_path': nuke_exe_path, 'nuke_version_str': nuke_version_str, - 'writenode_quicktime_settings': writenode_quicktime_settings, 'render_script_path': render_script_path, 'shotgun_context': shotgun_context, 'app_settings': app_settings, 'render_info': render_info, - # 'extra_env': extra_env, 'src_frames_path': src_frames_path, 'movie_output_path': movie_output_path, } @@ -143,12 +134,11 @@ def render_movie_in_nuke(self, path, output_path, width, height, first_frame, last_frame, version, name, - color_space, override_context=None, + color_space, active_progress_info=None): render_info = self.gather_nuke_render_info(path, output_path, width, height, first_frame, - last_frame, - version, name, color_space, override_context) + last_frame, version, name, color_space) run_in_batch_mode = True if nuke is None else False # if nuke module wasn't imported it will be None @@ -174,18 +164,11 @@ def render_movie_in_nuke(self, path, output_path, '--render_info', str(render_info.get('render_info')), ] - extra_env = render_info.get('extra_env', {}) - if extra_env.get('NUKE_INTERACTIVE', '') in ('1',): - cmd_and_args.insert(1, '-i') - - subprocess_env = os.environ.copy() - subprocess_env.update(extra_env) - - p = subprocess.Popen(cmd_and_args, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, - env=subprocess_env) + p = subprocess.Popen(cmd_and_args, stdout=subprocess.PIPE, stderr=subprocess.STDOUT) output_name = os.path.basename(render_info.get('movie_output_path')) + # TODO: How is this supposed to work? progress_fn = progress_label = None if active_progress_info: progress_fn = active_progress_info.get('show_progress_fn') @@ -212,10 +195,11 @@ def render_movie_in_nuke(self, path, output_path, if p.returncode != 0: output_str = '\n'.join(output_lines) - # stmt = 'status_info = {0}'.format(output_str.split('[RETURN_STATUS_DATA]')[1]) - # exec (stmt) - status_info = output_str.split('[RETURN_STATUS_DATA]')[1] - return status_info + print output_str + # TODO: Confirm this indeed appears on stdout + # status_info = output_str.split('[RETURN_STATUS_DATA]')[1] + # return status_info + return {'status': 'not OK'} return {'status': 'OK'} From 17971cb3fa91c5ae68e1dd8cde9c10f892107770 Mon Sep 17 00:00:00 2001 From: SaiAparna Ramamurthy Date: Thu, 22 Mar 2018 10:38:02 +0530 Subject: [PATCH 04/12] Nuke version not required as hook is overridden in sgtk_config. --- hooks/codec_settings.py | 7 +++++-- python/tk_multi_reviewsubmission/renderer.py | 20 +------------------- 2 files changed, 6 insertions(+), 21 deletions(-) diff --git a/hooks/codec_settings.py b/hooks/codec_settings.py index d397557..2475464 100644 --- a/hooks/codec_settings.py +++ b/hooks/codec_settings.py @@ -15,8 +15,6 @@ import os import sys -import nuke - HookBaseClass = sgtk.get_hook_baseclass() class CodecSettings(HookBaseClass): @@ -27,6 +25,11 @@ def get_quicktime_settings(self, **kwargs): Returns a dictionary of settings to be used for the Write Node that generates the Quicktime in Nuke. """ + # This hook file gets loaded even if overriden in sgtk_config. + # import causes error if not launched from nuke. + # Therefore, moving the import into the function. + import nuke + settings = {} if sys.platform in ["darwin", "win32"]: settings["file_type"] = "mov" diff --git a/python/tk_multi_reviewsubmission/renderer.py b/python/tk_multi_reviewsubmission/renderer.py index f95b27c..cfd39e7 100644 --- a/python/tk_multi_reviewsubmission/renderer.py +++ b/python/tk_multi_reviewsubmission/renderer.py @@ -62,26 +62,9 @@ def gather_nuke_render_info(self, path, output_path, 'darwin': 'nuke_mac_path'} nuke_exe_path = self.__app.get_setting(setting_key_by_os[sys.platform]) - if nuke: - nuke_version_major = nuke.NUKE_VERSION_MAJOR - else: - # TODO: get from nuke context somehow? Is this even required? - if os.path.split(nuke_exe_path)[0] != '': - nuke_version_str = os.path.basename(os.path.dirname(nuke_exe_path)) # get Nuke version folder - else: - nuke_version_str = os.path.basename(nuke_exe_path).replace('.exe', '').replace('app', '') - - bits = nuke_version_str.split('v') - nuke_version_release = None - if len(bits) > 1: - nuke_version_release = bits[1] - nuke_version_major_minor = bits[0] - nuke_version_major = nuke_version_major_minor.split('.')[0] - # get the Write node settings we'll use for generating the Quicktime writenode_quicktime_settings = self.__app.execute_hook_method("codec_settings_hook", - "get_quicktime_settings", - nuke_version_major=nuke_version_major) + "get_quicktime_settings") render_script_path = os.path.join(self.__app.disk_location, "hooks", "nuke_batch_render_movie.py") @@ -119,7 +102,6 @@ def gather_nuke_render_info(self, path, output_path, 'name': name, 'color_space': color_space, 'nuke_exe_path': nuke_exe_path, - 'nuke_version_str': nuke_version_str, 'render_script_path': render_script_path, 'shotgun_context': shotgun_context, 'app_settings': app_settings, From 0d772a0bb860bc28070ab131db6077edbd802dd5 Mon Sep 17 00:00:00 2001 From: SaiAparna Ramamurthy Date: Thu, 22 Mar 2018 10:38:31 +0530 Subject: [PATCH 05/12] Subprocess supports all engines. --- info.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/info.yml b/info.yml index 3880671..0b9dfb4 100644 --- a/info.yml +++ b/info.yml @@ -123,7 +123,7 @@ requires_core_version: "v0.14.0" requires_engine_version: # For now this app is only compatible with Nuke. -supported_engines: ["tk-nuke"] +supported_engines: # the frameworks required to run this app frameworks: From d2b98a60ab082de1e05c7e2a9101d14b8b66593b Mon Sep 17 00:00:00 2001 From: SaiAparna Ramamurthy Date: Thu, 22 Mar 2018 10:39:19 +0530 Subject: [PATCH 06/12] Set nuke root settings for batch render session. --- hooks/nuke_batch_render_movie.py | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/hooks/nuke_batch_render_movie.py b/hooks/nuke_batch_render_movie.py index 4a6285f..12199f4 100644 --- a/hooks/nuke_batch_render_movie.py +++ b/hooks/nuke_batch_render_movie.py @@ -75,7 +75,11 @@ def render_movie_in_nuke(path, output_path, :param color_space: Colorspace of the input frames """ output_node = None - # ctx = self.__app.context + + # set Nuke root settings (since this is a subprocess with a fresh session) + root_node = nuke.root() + root_node["first_frame"].setValue(first_frame-1) + root_node["last_frame"].setValue(last_frame) # create group where everything happens group = nuke.nodes.Group() @@ -91,6 +95,11 @@ def render_movie_in_nuke(path, output_path, if color_space: read["colorspace"].setValue(color_space) + # set root_format = res of read node + read_format = read.format() + read_format.add('READ_FORMAT') + root_node.knob('format').setValue('READ_FORMAT') + # now create the slate/burnin node burn = nuke.nodePaste(render_info.get('burnin_nk')) burn.setInput(0, read) From b05fa00a12deec18d4639b177626c8bed8bb92d3 Mon Sep 17 00:00:00 2001 From: SaiAparna Ramamurthy Date: Thu, 22 Mar 2018 13:16:42 +0530 Subject: [PATCH 07/12] Set correct frame range. Some refactoring. --- hooks/nuke_batch_render_movie.py | 35 +++++++++++++++++++------------- 1 file changed, 21 insertions(+), 14 deletions(-) diff --git a/hooks/nuke_batch_render_movie.py b/hooks/nuke_batch_render_movie.py index 12199f4..0f8ef93 100644 --- a/hooks/nuke_batch_render_movie.py +++ b/hooks/nuke_batch_render_movie.py @@ -1,3 +1,4 @@ +import ast import os import sys import traceback @@ -60,7 +61,9 @@ def render_movie_in_nuke(path, output_path, first_frame, last_frame, version, name, color_space, - app_settings, ctx_info, render_info): + app_settings, + ctx_info, render_info, + is_subprocess): """ Use Nuke to render a movie. This assumes we're running _inside_ Nuke. @@ -75,11 +78,12 @@ def render_movie_in_nuke(path, output_path, :param color_space: Colorspace of the input frames """ output_node = None - - # set Nuke root settings (since this is a subprocess with a fresh session) root_node = nuke.root() - root_node["first_frame"].setValue(first_frame-1) - root_node["last_frame"].setValue(last_frame) + + if is_subprocess: + # set Nuke root settings (since this is a subprocess with a fresh session) + root_node["first_frame"].setValue(first_frame) + root_node["last_frame"].setValue(last_frame) # create group where everything happens group = nuke.nodes.Group() @@ -95,10 +99,11 @@ def render_movie_in_nuke(path, output_path, if color_space: read["colorspace"].setValue(color_space) - # set root_format = res of read node - read_format = read.format() - read_format.add('READ_FORMAT') - root_node.knob('format').setValue('READ_FORMAT') + if is_subprocess: + # set root_format = res of read node + read_format = read.format() + read_format.add('READ_FORMAT') + root_node.knob('format').setValue('READ_FORMAT') # now create the slate/burnin node burn = nuke.nodePaste(render_info.get('burnin_nk')) @@ -107,6 +112,7 @@ def render_movie_in_nuke(path, output_path, font = render_info.get('slate_font') # set the fonts for all text fields + # TODO: find by class instead of using node names burn.node("top_left_text")["font"].setValue(font) burn.node("top_right_text")["font"].setValue(font) burn.node("bottom_left_text")["font"].setValue(font) @@ -138,6 +144,7 @@ def render_movie_in_nuke(path, output_path, project = ctx_info.get('project', {}) entity = ctx_info.get('entity', {}) + # TODO: use context names instead positional so that the nodes can be moved around burn.node("top_left_text")["message"].setValue(project.get("name", '')) burn.node("top_right_text")["message"].setValue(entity.get("name", '')) burn.node("bottom_left_text")["message"].setValue(version_label) @@ -186,7 +193,7 @@ def render_movie_in_nuke(path, output_path, os.makedirs(output_folder) # Render the outputs, first view only - nuke.executeMultiple([output_node], ([first_frame - 1, last_frame, 1],), + nuke.executeMultiple([output_node], ([first_frame-1, last_frame, 1],), [nuke.views()[0]]) except: return {'status': 'ERROR', 'error_msg': '{0}'.format(traceback.format_exc()), @@ -218,7 +225,7 @@ def usage(): if __name__ == '__main__': - + # TODO: copied maquino's code. Refactor? data_keys = [ 'path', 'output_path', 'width', 'height', 'first_frame', 'last_frame', 'version', 'name', 'color_space', 'app_settings', 'shotgun_context', 'render_info', @@ -249,8 +256,7 @@ def usage(): d_key = opt.replace('--', '') input_data[d_key] = opt_value if d_key in non_str_data_list: - stmt = '''input_data[ d_key ] = {0}'''.format(opt_value) - exec (stmt) + input_data[d_key] = ast.literal_eval('''{0}'''.format(opt_value)) for d_key in data_keys: if d_key not in input_data: @@ -265,7 +271,8 @@ def usage(): input_data['color_space'], input_data['app_settings'], input_data['shotgun_context'], - input_data['render_info']) + input_data['render_info'], + is_subprocess=True) if ret_status.get('status', '') == 'OK': sys.exit(0) From cb130ce43fdb6cd511830308889a0d56b278b250 Mon Sep 17 00:00:00 2001 From: SaiAparna Ramamurthy Date: Fri, 23 Mar 2018 13:15:59 +0530 Subject: [PATCH 08/12] Ensure errors are propagated as Exceptions so tk-multi-publish2 doesn't display a success message. --- hooks/nuke_batch_render_movie.py | 67 +++++++------------ python/tk_multi_reviewsubmission/renderer.py | 43 ++++++------ python/tk_multi_reviewsubmission/submitter.py | 8 ++- 3 files changed, 50 insertions(+), 68 deletions(-) diff --git a/hooks/nuke_batch_render_movie.py b/hooks/nuke_batch_render_movie.py index 0f8ef93..65a6503 100644 --- a/hooks/nuke_batch_render_movie.py +++ b/hooks/nuke_batch_render_movie.py @@ -63,7 +63,7 @@ def render_movie_in_nuke(path, output_path, color_space, app_settings, ctx_info, render_info, - is_subprocess): + is_subprocess=False): """ Use Nuke to render a movie. This assumes we're running _inside_ Nuke. @@ -90,6 +90,7 @@ def render_movie_in_nuke(path, output_path, # now operate inside this group group.begin() + try: # create read node read = nuke.nodes.Read(name="source", file=path.replace(os.sep, "/")) @@ -155,12 +156,6 @@ def render_movie_in_nuke(path, output_path, slate_str += "Name: %s\n" % name.capitalize() slate_str += "Version: %s\n" % version_str - # --- FOR DEBUGGING ONLY: set this next value to True in order to test an Exception occurring here - DEBUG_TEST_EXCEPTION = False - if DEBUG_TEST_EXCEPTION: - raise Exception( - "Forcing an Exception just to test how calling apps will respond to errors here.") - if task: slate_str += "Task: %s\n" % task.get("name", '') elif step: @@ -170,40 +165,30 @@ def render_movie_in_nuke(path, output_path, burn.node("slate_info")["message"].setValue(slate_str) - # create a scale node + # Create a scale node scale = __create_scale_node(width, height) scale.setInput(0, burn) # Create the output node output_node = __create_output_node(output_path, render_info.get('codec_settings', {})) output_node.setInput(0, scale) - - except: - return {'status': 'ERROR', 'error_msg': '{0}'.format(traceback.format_exc()), - 'output_path': output_path} - - group.end() + finally: + group.end() if output_node: - try: - # Make sure the output folder exists - output_folder = os.path.dirname(output_path) - # self.__app.ensure_folder_exists(output_folder) - if not os.path.isdir(output_folder): - os.makedirs(output_folder) - - # Render the outputs, first view only - nuke.executeMultiple([output_node], ([first_frame-1, last_frame, 1],), - [nuke.views()[0]]) - except: - return {'status': 'ERROR', 'error_msg': '{0}'.format(traceback.format_exc()), - 'output_path': output_path} + # Make sure the output folder exists + output_folder = os.path.dirname(output_path) + # TODO: jsmk stuff? + if not os.path.isdir(output_folder): + os.makedirs(output_folder) + + # Render the outputs, first view only + nuke.executeMultiple([output_node], ([first_frame-1, last_frame, 1],), + [nuke.views()[0]]) # Cleanup after ourselves nuke.delete(group) - return {'status': 'OK'} - def usage(): print(''' @@ -240,7 +225,6 @@ def usage(): long_opt_list = ['help'] + ['{0}='.format(k) for k in data_keys] input_data = {} - try: opt_list, arg_list = getopt.getopt(sys.argv[1:], short_opt_str, long_opt_list) except getopt.GetoptError as err: @@ -264,17 +248,12 @@ def usage(): usage() sys.exit(2) - ret_status = render_movie_in_nuke(input_data['path'], input_data['output_path'], - input_data['width'], input_data['height'], - input_data['first_frame'], input_data['last_frame'], - input_data['version'], input_data['name'], - input_data['color_space'], - input_data['app_settings'], - input_data['shotgun_context'], - input_data['render_info'], - is_subprocess=True) - - if ret_status.get('status', '') == 'OK': - sys.exit(0) - else: - sys.exit(3) + render_movie_in_nuke(input_data['path'], input_data['output_path'], + input_data['width'], input_data['height'], + input_data['first_frame'], input_data['last_frame'], + input_data['version'], input_data['name'], + input_data['color_space'], + input_data['app_settings'], + input_data['shotgun_context'], + input_data['render_info'], + is_subprocess=True) diff --git a/python/tk_multi_reviewsubmission/renderer.py b/python/tk_multi_reviewsubmission/renderer.py index cfd39e7..a162226 100644 --- a/python/tk_multi_reviewsubmission/renderer.py +++ b/python/tk_multi_reviewsubmission/renderer.py @@ -13,7 +13,6 @@ import sys import subprocess -import traceback try: import nuke @@ -53,7 +52,6 @@ def gather_nuke_render_info(self, path, output_path, first_frame, last_frame, version, name, color_space): - nuke_render_info = {} # First get Nuke executable path from project configuration environment @@ -121,9 +119,8 @@ def render_movie_in_nuke(self, path, output_path, render_info = self.gather_nuke_render_info(path, output_path, width, height, first_frame, last_frame, version, name, color_space) - - run_in_batch_mode = True if nuke is None else False # if nuke module wasn't imported it will be None - + # TODO: can we offload to a thread, similar to submitter? + run_in_batch_mode = True if nuke is None else False if run_in_batch_mode: # -------------------------------------------------------------------------------------------- # @@ -146,10 +143,9 @@ def render_movie_in_nuke(self, path, output_path, '--render_info', str(render_info.get('render_info')), ] - p = subprocess.Popen(cmd_and_args, stdout=subprocess.PIPE, stderr=subprocess.STDOUT) + p = subprocess.Popen(cmd_and_args, stdout=subprocess.PIPE, stderr=subprocess.PIPE) output_name = os.path.basename(render_info.get('movie_output_path')) - # TODO: How is this supposed to work? progress_fn = progress_label = None if active_progress_info: @@ -157,33 +153,37 @@ def render_movie_in_nuke(self, path, output_path, progress_label = active_progress_info.get('label') output_lines = [] + error_lines = [] num_frames = last_frame - first_frame + 1 write_count = 0 while p.poll() is None: - line = p.stdout.readline() - if line != '': - output_lines.append(line.rstrip()) + stdout_line = p.stdout.readline() + stderr_line = p.stderr.readline() + + if stdout_line != '': + output_lines.append(stdout_line.rstrip()) + if stderr_line != '': + error_lines.append(stderr_line.rstrip()) - percent_complete = float(write_count) / float(num_frames) * 100.0 + percent_complete = float(write_count)/float(num_frames) * 100.0 if progress_fn: progress_fn(progress_label, 'Nuke: {0:03.1f}%, {1}'.format(percent_complete, output_name)) - if line.startswith('Writing '): + if stdout_line.startswith('Writing '): # The number of these lines will be number of frames + 1 write_count += 1 if p.returncode != 0: - output_str = '\n'.join(output_lines) - print output_str - # TODO: Confirm this indeed appears on stdout - # status_info = output_str.split('[RETURN_STATUS_DATA]')[1] - # return status_info - return {'status': 'not OK'} - - return {'status': 'OK'} + subproc_error_msg = '\n'.join(error_lines) + self.__app.log_error(subproc_error_msg) + # Do not clutter user message with any warnings etc from Nuke. Print only traceback. + # TODO: is there a better way? + subproc_traceback = 'Traceback' + subproc_error_msg.split('Traceback')[1] + # Make sure we don't display a success message. TODO: Custom exception? + raise Exception("Error in tk-multi-reviewsubmission: " + subproc_traceback) else: # -------------------------------------------------------------------------------------------- @@ -199,7 +199,7 @@ def render_movie_in_nuke(self, path, output_path, sys.path.append(script_dir_path) render_script_module = importlib.import_module(script_filename.replace('.py', '')) - ret_status = render_script_module.render_movie_in_nuke( + render_script_module.render_movie_in_nuke( render_info.get('src_frames_path'), render_info.get('movie_output_path'), width, height, @@ -208,4 +208,3 @@ def render_movie_in_nuke(self, path, output_path, render_info.get('app_settings'), render_info.get('shotgun_context'), render_info.get('render_info')) - return ret_status diff --git a/python/tk_multi_reviewsubmission/submitter.py b/python/tk_multi_reviewsubmission/submitter.py index 444ec07..c58c279 100644 --- a/python/tk_multi_reviewsubmission/submitter.py +++ b/python/tk_multi_reviewsubmission/submitter.py @@ -93,8 +93,12 @@ def _upload_files(self, sg_version, output_path, thumbnail_path, upload_to_shotg event_loop.exec_() # log any errors generated in the thread - for e in thread.get_errors(): - self.__app.log_error(e) + thread_errors = thread.get_errors() + if thread_errors: + for e in thread_errors: + self.__app.log_error(e) + # make sure we don't display a success message. TODO: Custom exception? + raise Exception('\n'.join(thread_errors)) From 9741cb4f1703aeafefce5d492acbb99218ad6332 Mon Sep 17 00:00:00 2001 From: SaiAparna Ramamurthy Date: Mon, 26 Mar 2018 11:42:07 +0530 Subject: [PATCH 09/12] CR: fixed naked except, passed context to Template.apply_fields() --- python/tk_multi_reviewsubmission/renderer.py | 20 +++++++++----------- 1 file changed, 9 insertions(+), 11 deletions(-) diff --git a/python/tk_multi_reviewsubmission/renderer.py b/python/tk_multi_reviewsubmission/renderer.py index a162226..868f443 100644 --- a/python/tk_multi_reviewsubmission/renderer.py +++ b/python/tk_multi_reviewsubmission/renderer.py @@ -16,7 +16,7 @@ try: import nuke -except: +except ImportError: nuke = None @@ -27,17 +27,19 @@ def __init__(self): """ self.__app = sgtk.platform.current_bundle() self._font = os.path.join(self.__app.disk_location, "resources", "liberationsans_regular.ttf") + context_fields = self.__app.context.as_template_fields() burnin_template = self.__app.get_template("burnin_path") - self._burnin_nk = burnin_template.apply_fields({}) + self._burnin_nk = burnin_template.apply_fields(context_fields) # If a show specific burnin file has not been defined, take it from the default location if not os.path.isfile(self._burnin_nk): self._burnin_nk = os.path.join(self.__app.disk_location, "resources", "burnin.nk") self._logo = None logo_template = self.__app.get_template("slate_logo") - if os.path.isfile(logo_template.apply_fields({})): - self._logo = logo_template.apply_fields({}) + logo_file_path = logo_template.apply_fields(context_fields) + if os.path.isfile(logo_file_path): + self._logo = logo_file_path else: self._logo = "" @@ -52,8 +54,6 @@ def gather_nuke_render_info(self, path, output_path, first_frame, last_frame, version, name, color_space): - nuke_render_info = {} - # First get Nuke executable path from project configuration environment setting_key_by_os = {'win32': 'nuke_windows_path', 'linux2': 'nuke_linux_path', @@ -68,13 +68,12 @@ def gather_nuke_render_info(self, path, output_path, "nuke_batch_render_movie.py") ctx = self.__app.context - shotgun_context = {} - shotgun_context['entity'] = ctx.entity.copy() + shotgun_context = {'entity': ctx.entity.copy(), + 'project': ctx.project.copy()} if ctx.task: shotgun_context['task'] = ctx.task.copy() if ctx.step: shotgun_context['step'] = ctx.step.copy() - shotgun_context['project'] = ctx.project.copy() app_settings = { 'version_number_padding': self.__app.get_setting('version_number_padding'), @@ -87,7 +86,7 @@ def gather_nuke_render_info(self, path, output_path, 'codec_settings': {'quicktime': writenode_quicktime_settings}, } - # set needed paths and force them to use forward slashes for use in Nuke (latter needed for Windows) + # set needed paths and force them to use forward slashes for use in Nuke (for Windows) src_frames_path = path.replace('\\', '/') movie_output_path = output_path.replace('\\', '/') @@ -107,7 +106,6 @@ def gather_nuke_render_info(self, path, output_path, 'src_frames_path': src_frames_path, 'movie_output_path': movie_output_path, } - return nuke_render_info def render_movie_in_nuke(self, path, output_path, From 562201b5b5cbd783cfd108e12a454345909c2dea Mon Sep 17 00:00:00 2001 From: SaiAparna Ramamurthy Date: Mon, 26 Mar 2018 13:49:36 +0530 Subject: [PATCH 10/12] Send subprocess to thread and make UI active during processing. --- hooks/nuke_batch_render_movie.py | 16 +- python/tk_multi_reviewsubmission/renderer.py | 182 ++++++++++--------- 2 files changed, 103 insertions(+), 95 deletions(-) diff --git a/hooks/nuke_batch_render_movie.py b/hooks/nuke_batch_render_movie.py index 65a6503..c4ed9a6 100644 --- a/hooks/nuke_batch_render_movie.py +++ b/hooks/nuke_batch_render_movie.py @@ -190,8 +190,8 @@ def render_movie_in_nuke(path, output_path, nuke.delete(group) -def usage(): - print(''' +def get_usage(): + return ''' Usage: python {0} [ OPTIONS ] -h | --help ... print this usage message and exit. --path ... specify full path to frames, with frame spec ... e.g. ".%04d.exr" @@ -206,7 +206,7 @@ def usage(): --app_settings ... specify app settings from the Toolkit app calling this --shotgun_context ... specify shotgun context from the Toolkit app calling this --render_info ... specify render info from the Toolkit app calling this -'''.format(os.path.basename(sys.argv[0]))) +'''.format(os.path.basename(sys.argv[0])) if __name__ == '__main__': @@ -228,13 +228,13 @@ def usage(): try: opt_list, arg_list = getopt.getopt(sys.argv[1:], short_opt_str, long_opt_list) except getopt.GetoptError as err: - print(str(err)) - usage() + sys.stderr.write(str(err)) + sys.stderr.write(get_usage()) sys.exit(1) for opt, opt_value in opt_list: if opt in ('-h', '--help'): - usage() + print get_usage() sys.exit(0) elif opt.replace('--', '') in data_keys: d_key = opt.replace('--', '') @@ -244,8 +244,8 @@ def usage(): for d_key in data_keys: if d_key not in input_data: - print('ERROR - missing input argument for "--{0}" flag. Aborting'.format(d_key)) - usage() + sys.stderr.write('ERROR - missing input argument for "--{0}". Aborting'.format(d_key)) + sys.stderr.write(get_usage()) sys.exit(2) render_movie_in_nuke(input_data['path'], input_data['output_path'], diff --git a/python/tk_multi_reviewsubmission/renderer.py b/python/tk_multi_reviewsubmission/renderer.py index 868f443..ba3dc8c 100644 --- a/python/tk_multi_reviewsubmission/renderer.py +++ b/python/tk_multi_reviewsubmission/renderer.py @@ -11,8 +11,8 @@ import sgtk import os import sys - import subprocess +from sgtk.platform.qt import QtCore try: import nuke @@ -117,92 +117,100 @@ def render_movie_in_nuke(self, path, output_path, render_info = self.gather_nuke_render_info(path, output_path, width, height, first_frame, last_frame, version, name, color_space) - # TODO: can we offload to a thread, similar to submitter? run_in_batch_mode = True if nuke is None else False - if run_in_batch_mode: - # -------------------------------------------------------------------------------------------- - # - # Running within Nuke interactive ... - # - # -------------------------------------------------------------------------------------------- - - # Set-up the subprocess command and arguments - cmd_and_args = [ - render_info.get('nuke_exe_path'), '-t', render_info.get('render_script_path'), - '--path', render_info.get('src_frames_path'), - '--output_path', render_info.get('movie_output_path'), - '--width', str(width), '--height', str(height), '--version', str(version), '--name', - name, - '--color_space', color_space, - '--first_frame', str(first_frame), - '--last_frame', str(last_frame), - '--app_settings', str(render_info.get('app_settings')), - '--shotgun_context', str(render_info.get('shotgun_context')), - '--render_info', str(render_info.get('render_info')), - ] - - p = subprocess.Popen(cmd_and_args, stdout=subprocess.PIPE, stderr=subprocess.PIPE) - - output_name = os.path.basename(render_info.get('movie_output_path')) - # TODO: How is this supposed to work? - progress_fn = progress_label = None - if active_progress_info: - progress_fn = active_progress_info.get('show_progress_fn') - progress_label = active_progress_info.get('label') - - output_lines = [] - error_lines = [] - - num_frames = last_frame - first_frame + 1 - write_count = 0 - - while p.poll() is None: - stdout_line = p.stdout.readline() - stderr_line = p.stderr.readline() - - if stdout_line != '': - output_lines.append(stdout_line.rstrip()) - if stderr_line != '': - error_lines.append(stderr_line.rstrip()) - - percent_complete = float(write_count)/float(num_frames) * 100.0 - if progress_fn: - progress_fn(progress_label, - 'Nuke: {0:03.1f}%, {1}'.format(percent_complete, output_name)) - - if stdout_line.startswith('Writing '): - # The number of these lines will be number of frames + 1 - write_count += 1 - - if p.returncode != 0: - subproc_error_msg = '\n'.join(error_lines) - self.__app.log_error(subproc_error_msg) - # Do not clutter user message with any warnings etc from Nuke. Print only traceback. - # TODO: is there a better way? - subproc_traceback = 'Traceback' + subproc_error_msg.split('Traceback')[1] - # Make sure we don't display a success message. TODO: Custom exception? - raise Exception("Error in tk-multi-reviewsubmission: " + subproc_traceback) + event_loop = QtCore.QEventLoop() + thread = ShooterThread(render_info, run_in_batch_mode) + thread.finished.connect(event_loop.quit) + thread.start() + event_loop.exec_() + + # log any errors generated in the thread + thread_error_msg = thread.get_errors() + if thread_error_msg: + self.__app.log_error("OUTPUT:\n" + thread.get_output()) + self.__app.log_error("ERROR:\n" + thread_error_msg) + + # Do not clutter user message with any warnings etc from Nuke. Print only traceback. + # TODO: is there a better way? + try: + subproc_traceback = 'Traceback' + thread_error_msg.split('Traceback')[1] + except IndexError: + subproc_traceback = thread_error_msg + # Make sure we don't display a success message. TODO: Custom exception? + raise Exception("Error in tk-multi-reviewsubmission: " + subproc_traceback) + +class ShooterThread(QtCore.QThread): + def __init__(self, render_info, batch_mode=True, active_progress_info=None): + QtCore.QThread.__init__(self) + self.render_info = render_info + self.batch_mode = batch_mode + self.active_progress_info = active_progress_info + self.subproc_error_msg = '' + self.subproc_output = '' + + def get_errors(self): + return self.subproc_error_msg + + def get_output(self): + return self.subproc_output + + def run(self): + if self.batch_mode: + nuke_flag = '-t' else: - # -------------------------------------------------------------------------------------------- - # - # Running within Nuke interactive ... - # - # -------------------------------------------------------------------------------------------- - import importlib - - render_script_path = render_info.get('render_script_path') - - script_dir_path, script_filename = os.path.split(render_script_path) - sys.path.append(script_dir_path) - - render_script_module = importlib.import_module(script_filename.replace('.py', '')) - render_script_module.render_movie_in_nuke( - render_info.get('src_frames_path'), - render_info.get('movie_output_path'), - width, height, - first_frame, last_frame, - version, name, color_space, - render_info.get('app_settings'), - render_info.get('shotgun_context'), - render_info.get('render_info')) + nuke_flag = '-it' + + cmd_and_args = [ + self.render_info['nuke_exe_path'], nuke_flag, self.render_info['render_script_path'], + '--path', self.render_info['src_frames_path'], + '--output_path', self.render_info['movie_output_path'], + '--width', str(self.render_info['width']), + '--height', str(self.render_info['height']), + '--version', str(self.render_info['version']), + '--name', self.render_info['name'], + '--color_space', self.render_info['color_space'], + '--first_frame', str(self.render_info['first_frame']), + '--last_frame', str(self.render_info['last_frame']), + '--app_settings', str(self.render_info['app_settings']), + '--shotgun_context', str(self.render_info['shotgun_context']), + '--render_info', str(self.render_info['render_info']), + ] + + p = subprocess.Popen(cmd_and_args, stdout=subprocess.PIPE, stderr=subprocess.PIPE) + + output_name = os.path.basename(self.render_info['movie_output_path']) + # TODO: How is this supposed to work? + progress_fn = progress_label = None + if self.active_progress_info: + progress_fn = self.active_progress_info.get('show_progress_fn') + progress_label = self.active_progress_info.get('label') + + output_lines = [] + error_lines = [] + + num_frames = self.render_info['last_frame'] - self.render_info['first_frame'] + 1 + write_count = 0 + + while p.poll() is None: + stdout_line = p.stdout.readline() + stderr_line = p.stderr.readline() + + if stdout_line != '': + output_lines.append(stdout_line.rstrip()) + if stderr_line != '': + error_lines.append(stderr_line.rstrip()) + + percent_complete = float(write_count) / float(num_frames) * 100.0 + if progress_fn: + progress_fn(progress_label, + 'Nuke: {0:03.1f}%, {1}'.format(percent_complete, output_name)) + # TODO: emit a signal to be captured by calling thread to update UI? + + if stdout_line.startswith('Writing '): + # The number of these lines will be number of frames + 1 + write_count += 1 + + self.subproc_output = '\n'.join(output_lines) + if p.returncode != 0: + self.subproc_error_msg = '\n'.join(error_lines) From 1f7342098c28f0ca67405d6d1636001ff1948e10 Mon Sep 17 00:00:00 2001 From: SaiAparna Ramamurthy Date: Mon, 26 Mar 2018 15:24:22 +0530 Subject: [PATCH 11/12] Use serialized context. --- hooks/nuke_batch_render_movie.py | 41 ++++++++++---------- python/tk_multi_reviewsubmission/renderer.py | 12 ++---- 2 files changed, 23 insertions(+), 30 deletions(-) diff --git a/hooks/nuke_batch_render_movie.py b/hooks/nuke_batch_render_movie.py index c4ed9a6..520011e 100644 --- a/hooks/nuke_batch_render_movie.py +++ b/hooks/nuke_batch_render_movie.py @@ -4,6 +4,8 @@ import traceback import getopt +from sgtk.context import Context + import nuke @@ -62,7 +64,7 @@ def render_movie_in_nuke(path, output_path, version, name, color_space, app_settings, - ctx_info, render_info, + ctx, render_info, is_subprocess=False): """ Use Nuke to render a movie. This assumes we're running _inside_ Nuke. @@ -132,34 +134,28 @@ def render_movie_in_nuke(path, output_path, version_padding_format = "%%0%dd" % ver_num_pad version_str = version_padding_format % version - task = ctx_info.get('task', {}) - step = ctx_info.get('step', {}) - - if task: - version_label = "%s, v%s" % (task.get("name", ''), version_str) - elif step: - version_label = "%s, v%s" % (step.get("name", ''), version_str) + if ctx.task: + version_label = "%s, v%s" % (ctx.task["name"], version_str) + elif ctx.step: + version_label = "%s, v%s" % (ctx.step["name"], version_str) else: version_label = "v%s" % version_str - project = ctx_info.get('project', {}) - entity = ctx_info.get('entity', {}) - # TODO: use context names instead positional so that the nodes can be moved around - burn.node("top_left_text")["message"].setValue(project.get("name", '')) - burn.node("top_right_text")["message"].setValue(entity.get("name", '')) + burn.node("top_left_text")["message"].setValue(ctx.project["name"]) + burn.node("top_right_text")["message"].setValue(ctx.entity["name"]) burn.node("bottom_left_text")["message"].setValue(version_label) # and the slate - slate_str = "Project: %s\n" % project.get("name", '') - slate_str += "%s: %s\n" % (entity.get("type", ''), entity.get("name", '')) + slate_str = "Project: %s\n" % ctx.project["name"] + slate_str += "%s: %s\n" % (ctx.entity["type"], ctx.entity["name"]) slate_str += "Name: %s\n" % name.capitalize() slate_str += "Version: %s\n" % version_str - if task: - slate_str += "Task: %s\n" % task.get("name", '') - elif step: - slate_str += "Step: %s\n" % step.get("name", '') + if ctx.task: + slate_str += "Task: %s\n" % ctx.task["name"] + elif ctx.step: + slate_str += "Step: %s\n" % ctx.step["name"] slate_str += "Frames: %s - %s\n" % (first_frame, last_frame) @@ -238,9 +234,12 @@ def get_usage(): sys.exit(0) elif opt.replace('--', '') in data_keys: d_key = opt.replace('--', '') - input_data[d_key] = opt_value - if d_key in non_str_data_list: + if d_key == 'shotgun_context': + input_data[d_key] = Context.deserialize(opt_value) + elif d_key in non_str_data_list: input_data[d_key] = ast.literal_eval('''{0}'''.format(opt_value)) + else: + input_data[d_key] = opt_value for d_key in data_keys: if d_key not in input_data: diff --git a/python/tk_multi_reviewsubmission/renderer.py b/python/tk_multi_reviewsubmission/renderer.py index ba3dc8c..c8810f9 100644 --- a/python/tk_multi_reviewsubmission/renderer.py +++ b/python/tk_multi_reviewsubmission/renderer.py @@ -66,14 +66,8 @@ def gather_nuke_render_info(self, path, output_path, render_script_path = os.path.join(self.__app.disk_location, "hooks", "nuke_batch_render_movie.py") - ctx = self.__app.context - shotgun_context = {'entity': ctx.entity.copy(), - 'project': ctx.project.copy()} - if ctx.task: - shotgun_context['task'] = ctx.task.copy() - if ctx.step: - shotgun_context['step'] = ctx.step.copy() + serialized_context = self.__app.context.serialize() app_settings = { 'version_number_padding': self.__app.get_setting('version_number_padding'), @@ -100,7 +94,7 @@ def gather_nuke_render_info(self, path, output_path, 'color_space': color_space, 'nuke_exe_path': nuke_exe_path, 'render_script_path': render_script_path, - 'shotgun_context': shotgun_context, + 'serialized_context': serialized_context, 'app_settings': app_settings, 'render_info': render_info, 'src_frames_path': src_frames_path, @@ -173,7 +167,7 @@ def run(self): '--first_frame', str(self.render_info['first_frame']), '--last_frame', str(self.render_info['last_frame']), '--app_settings', str(self.render_info['app_settings']), - '--shotgun_context', str(self.render_info['shotgun_context']), + '--shotgun_context', str(self.render_info['serialized_context']), '--render_info', str(self.render_info['render_info']), ] From d6bf546334cd4bdd65819f660e08087940c7b20f Mon Sep 17 00:00:00 2001 From: SaiAparna Ramamurthy Date: Mon, 26 Mar 2018 15:28:45 +0530 Subject: [PATCH 12/12] Serialize other args too. --- hooks/nuke_batch_render_movie.py | 4 ++-- python/tk_multi_reviewsubmission/renderer.py | 17 +++++++++-------- 2 files changed, 11 insertions(+), 10 deletions(-) diff --git a/hooks/nuke_batch_render_movie.py b/hooks/nuke_batch_render_movie.py index 520011e..1a2d308 100644 --- a/hooks/nuke_batch_render_movie.py +++ b/hooks/nuke_batch_render_movie.py @@ -1,5 +1,5 @@ -import ast import os +import pickle import sys import traceback import getopt @@ -237,7 +237,7 @@ def get_usage(): if d_key == 'shotgun_context': input_data[d_key] = Context.deserialize(opt_value) elif d_key in non_str_data_list: - input_data[d_key] = ast.literal_eval('''{0}'''.format(opt_value)) + input_data[d_key] = pickle.loads(opt_value) else: input_data[d_key] = opt_value diff --git a/python/tk_multi_reviewsubmission/renderer.py b/python/tk_multi_reviewsubmission/renderer.py index c8810f9..b00460b 100644 --- a/python/tk_multi_reviewsubmission/renderer.py +++ b/python/tk_multi_reviewsubmission/renderer.py @@ -10,6 +10,7 @@ import sgtk import os +import pickle import sys import subprocess from sgtk.platform.qt import QtCore @@ -159,16 +160,16 @@ def run(self): self.render_info['nuke_exe_path'], nuke_flag, self.render_info['render_script_path'], '--path', self.render_info['src_frames_path'], '--output_path', self.render_info['movie_output_path'], - '--width', str(self.render_info['width']), - '--height', str(self.render_info['height']), - '--version', str(self.render_info['version']), + '--width', pickle.dumps(self.render_info['width']), + '--height', pickle.dumps(self.render_info['height']), + '--version', pickle.dumps(self.render_info['version']), '--name', self.render_info['name'], '--color_space', self.render_info['color_space'], - '--first_frame', str(self.render_info['first_frame']), - '--last_frame', str(self.render_info['last_frame']), - '--app_settings', str(self.render_info['app_settings']), - '--shotgun_context', str(self.render_info['serialized_context']), - '--render_info', str(self.render_info['render_info']), + '--first_frame', pickle.dumps(self.render_info['first_frame']), + '--last_frame', pickle.dumps(self.render_info['last_frame']), + '--app_settings', pickle.dumps(self.render_info['app_settings']), + '--shotgun_context', self.render_info['serialized_context'], + '--render_info', pickle.dumps(self.render_info['render_info']), ] p = subprocess.Popen(cmd_and_args, stdout=subprocess.PIPE, stderr=subprocess.PIPE)