From e15f066dde11137ed2c24166eea5c6a9321e116b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sybren=20A=2E=20St=C3=BCvel?= Date: Tue, 15 Mar 2022 10:56:58 +0100 Subject: [PATCH] Automatically evaluate hidden job settings Job settings that are not visible and have an `eval` key will be automatically evaluated when the job is submitted. --- addon/flamenco/job_submission.py | 26 +++++++++ addon/flamenco/job_types_propgroup.py | 12 ++++- addon/flamenco/operators.py | 12 +++-- .../job_compilers/job_compilers_test.go | 21 ++++---- .../scripts/simple_blender_render.js | 53 +++++++++++++++---- 5 files changed, 98 insertions(+), 26 deletions(-) diff --git a/addon/flamenco/job_submission.py b/addon/flamenco/job_submission.py index 806ed3cb..298e00d5 100644 --- a/addon/flamenco/job_submission.py +++ b/addon/flamenco/job_submission.py @@ -68,6 +68,32 @@ def set_blend_file( job.settings[BLENDFILE_SETTING_KEY] = str(blendfile) +def eval_hidden_settings( + context: bpy.types.Context, job_type: _AvailableJobType, job: _SubmittedJob +) -> None: + """Assign values to settings hidden from the UI. + + If the setting has an `eval` property, it'll be evaluated and used as the + setting value. Otherwise the default is used. + """ + for setting in job_type.settings: + if setting.get("visible", True): + # Skip those settings that will be visible in the GUI. + continue + + setting_eval = setting.get("eval", "") + if setting_eval: + value = JobTypePropertyGroup.eval_setting(context, setting_eval) + elif "default" in setting: + value = setting.default + else: + # No way to get a default value, so just don't bother overwriting + # anything. + continue + + job.settings[setting.key] = value + + def submit_job(job: _SubmittedJob, api_client: _ApiClient) -> _Job: """Send the given job to Flamenco Manager.""" from flamenco.manager import ApiClient diff --git a/addon/flamenco/job_types_propgroup.py b/addon/flamenco/job_types_propgroup.py index 78a88638..a79ea326 100644 --- a/addon/flamenco/job_types_propgroup.py +++ b/addon/flamenco/job_types_propgroup.py @@ -47,15 +47,23 @@ class JobTypePropertyGroup: return js - def eval_setting( + def eval_and_assign( self, context: bpy.types.Context, setting_key: str, setting_eval: str ) -> None: + """Evaluate `setting_eval` and assign the result to the job setting.""" + value = self.eval_setting(context, setting_eval) + setattr(self, setting_key, value) + + @staticmethod + def eval_setting(context: bpy.types.Context, setting_eval: str) -> Any: + """Evaluate `setting_eval` and return the result.""" + eval_globals = { "bpy": bpy, "C": context, } value = eval(setting_eval, eval_globals, {}) - setattr(self, setting_key, value) + return value # Mapping from AvailableJobType.setting.type to a callable that converts a value diff --git a/addon/flamenco/operators.py b/addon/flamenco/operators.py index 9ed5fb34..b0f24944 100644 --- a/addon/flamenco/operators.py +++ b/addon/flamenco/operators.py @@ -116,7 +116,7 @@ class FLAMENCO_OT_eval_setting(FlamencoOpMixin, bpy.types.Operator): def execute(self, context: bpy.types.Context) -> set[str]: propgroup = context.scene.flamenco_job_settings - propgroup.eval_setting(context, self.setting_key, self.setting_eval) + propgroup.eval_and_assign(context, self.setting_key, self.setting_eval) return {"FINISHED"} @@ -261,11 +261,15 @@ class FLAMENCO_OT_submit_job(FlamencoOpMixin, bpy.types.Operator): def _submit_job(self, context: bpy.types.Context) -> None: """Use the Flamenco API to submit the new Job.""" assert self.job is not None - - job_type = job_types.active_job_type(context.scene) - job_submission.set_blend_file(job_type, self.job, self.blendfile_on_farm) + assert self.blendfile_on_farm is not None api_client = self.get_api_client(context) + + job_type = job_types.active_job_type(context.scene) + assert job_type is not None # If we're here, the job type should be known. + + job_submission.set_blend_file(job_type, self.job, self.blendfile_on_farm) + job_submission.eval_hidden_settings(context, job_type, self.job) job = job_submission.submit_job(self.job, api_client) self.report({"INFO"}, "Job %s submitted" % job.name) diff --git a/internal/manager/job_compilers/job_compilers_test.go b/internal/manager/job_compilers/job_compilers_test.go index 99f8ae32..7cea0c94 100644 --- a/internal/manager/job_compilers/job_compilers_test.go +++ b/internal/manager/job_compilers/job_compilers_test.go @@ -18,16 +18,17 @@ import ( func exampleSubmittedJob() api.SubmittedJob { settings := api.JobSettings{ AdditionalProperties: map[string]interface{}{ - "blender_cmd": "{blender}", - "blendfile": "/render/sf/jobs/scene123.blend", - "chunk_size": 3, - "extract_audio": true, - "format": "PNG", - "fps": 24, - "frames": "1-10", - "images_or_video": "images", - "output_file_extension": ".png", - "render_output": "/render/sprites/farm_output/promo/square_ellie/square_ellie.lighting_light_breakdown2/######", + "blender_cmd": "{blender}", + "blendfile": "/render/sf/jobs/scene123.blend", + "chunk_size": 3, + "extract_audio": true, + "format": "PNG", + "fps": 24.0, + "frames": "1-10", + "images_or_video": "images", + "image_file_extension": ".png", + "video_container_format": "", + "render_output": "/render/sprites/farm_output/promo/square_ellie/square_ellie.lighting_light_breakdown2/######", }} metadata := api.JobMetadata{ AdditionalProperties: map[string]string{ diff --git a/internal/manager/job_compilers/scripts/simple_blender_render.js b/internal/manager/job_compilers/scripts/simple_blender_render.js index ea892abb..ffc26990 100644 --- a/internal/manager/job_compilers/scripts/simple_blender_render.js +++ b/internal/manager/job_compilers/scripts/simple_blender_render.js @@ -3,13 +3,15 @@ const JOB_TYPE = { label: "Simple Blender Render", settings: [ - { key: "blender_cmd", type: "string", default: "{blender}", visible: false }, - { key: "blendfile", type: "string", required: true, description: "Path of the Blend file to render", visible: false }, + // Settings for artists to determine: { key: "chunk_size", type: "int32", default: 1, description: "Number of frames to render in one Blender render task" }, { key: "frames", type: "string", required: true, eval: "f'{C.scene.frame_start}-{C.scene.frame_end}'"}, { key: "render_output", type: "string", subtype: "hashed_file_path", required: true }, - { key: "fps", type: "float", eval: "C.scene.render.fps / C.scene.render.fps_base" }, - { key: "extract_audio", type: "bool", default: true }, + + // Automatically evaluated settings: + { key: "blender_cmd", type: "string", default: "{blender}", visible: false }, + { key: "blendfile", type: "string", required: true, description: "Path of the Blend file to render", visible: false }, + { key: "fps", type: "float", eval: "C.scene.render.fps / C.scene.render.fps_base", visible: false }, { key: "images_or_video", type: "string", @@ -18,8 +20,11 @@ const JOB_TYPE = { visible: false, eval: "'video' if C.scene.render.image_settings.file_format in {'FFMPEG', 'AVI_RAW', 'AVI_JPEG'} else 'image'" }, - { key: "format", type: "string", required: true, eval: "C.scene.render.image_settings.file_format" }, - { key: "output_file_extension", type: "string", required: true }, + { key: "format", type: "string", required: true, eval: "C.scene.render.image_settings.file_format", visible: false }, + { key: "image_file_extension", type: "string", required: true, eval: "C.scene.render.file_extension", visible: false, + description: "File extension used when rendering images; ignored when images_or_video='video'" }, + { key: "video_container_format", type: "string", required: true, eval: "C.scene.render.ffmpeg.format", visible: false, + description: "Container format used when rendering video; ignored when images_or_video='images'" }, ] }; @@ -33,6 +38,17 @@ const ffmpegIncompatibleImageFormats = new Set([ "OPEN_EXR_MULTILAYER", // DNA values for these formats. ]); +// Mapping from video container (scene.render.ffmpeg.format) to the file name +// extension typically used to store those videos. +const videoContainerToExtension = { + "QUICKTIME": ".mov", + "MPEG1": ".mpg", + "MPEG2": ".dvd", + "MPEG4": ".mp4", + "OGG": ".ogv", + "FLASH": ".flv", +}; + function compileJob(job) { print("Blender Render job submitted"); print("job: ", job); @@ -91,21 +107,25 @@ function authorRenderTasks(settings, renderDir, renderOutput) { } function authorCreateVideoTask(settings, renderDir) { + if (settings.images_or_video == "video") { + print("Not authoring video task, render output is already a video"); + } if (ffmpegIncompatibleImageFormats.has(settings.format)) { print("Not authoring video task, FFmpeg-incompatible render output") return; } - if (!settings.fps || !settings.output_file_extension) { - print("Not authoring video task, no FPS or output file extension setting:", settings) + if (!settings.fps) { + print("Not authoring video task, no FPS known:", settings); return; } const stem = path.stem(settings.blendfile).replace('.flamenco', ''); const outfile = path.join(renderDir, `${stem}-${settings.frames}.mp4`); + const outfileExt = guessOutputFileExtension(settings); const task = author.Task('create-video', 'ffmpeg'); const command = author.Command("create-video", { - input_files: path.join(renderDir, `*${settings.output_file_extension}`), + input_files: path.join(renderDir, `*${outfileExt}`), output_file: outfile, fps: settings.fps, }); @@ -113,4 +133,17 @@ function authorCreateVideoTask(settings, renderDir) { print(`Creating output video for ${settings.format}`); return task; -} \ No newline at end of file +} + +// Return file name extension, including period, like '.png' or '.mkv'. +function guessOutputFileExtension(settings) { + if (settings.images_or_video == "images") { + return settings.image_file_extension; + } + + container = scene.render.ffmpeg.format + if (container in videoContainerToExtension) { + return videoContainerToExtension[container]; + } + return "." + container.lower(); +}