diff --git a/scripts/default_settings.txt b/scripts/default_settings.txt index ddfea212..32e8eb91 100644 --- a/scripts/default_settings.txt +++ b/scripts/default_settings.txt @@ -143,6 +143,7 @@ "hybrid_comp_save_extra_frames": false, "parseq_manifest": "", "parseq_use_deltas": true, + "parseq_non_schedule_overrides": true, "use_looper": false, "init_images": "{\n \"0\": \"https://deforum.github.io/a1/Gi1.png\",\n \"max_f/4-5\": \"https://deforum.github.io/a1/Gi2.png\",\n \"max_f/2-10\": \"https://deforum.github.io/a1/Gi3.png\",\n \"3*max_f/4-15\": \"https://deforum.github.io/a1/Gi4.jpg\",\n \"max_f-20\": \"https://deforum.github.io/a1/Gi1.png\"\n}", "image_strength_schedule": "0:(0.75)", diff --git a/scripts/deforum_helpers/args.py b/scripts/deforum_helpers/args.py index 3561640f..25eab8af 100644 --- a/scripts/deforum_helpers/args.py +++ b/scripts/deforum_helpers/args.py @@ -967,16 +967,23 @@ def LoopArgs(): def ParseqArgs(): return { "parseq_manifest": { - "label": "Parseq Manifest (JSON or URL)", + "label": "Parseq manifest (JSON or URL)", "type": "textbox", "lines": 4, "value": None, }, "parseq_use_deltas": { - "label": "Use delta values for movement parameters", + "label": "Use delta values for movement parameters (recommended)", "type": "checkbox", "value": True, - } + "info": "Recommended. If you uncheck this, Parseq keyframe values as are treated as relative movement values instead of absolute." + }, + "parseq_non_schedule_overrides": { + "label": "Recommended. If you uncheck this, the FPS, max_frames and cadence in the Parseq doc will be ignored, and the values in the A1111 UI will be used.", + "type": "checkbox", + "value": True, + "info": "Use FPS, max_frames and cadence from the Parseq manifest, if present (recommended)." + } } def DeforumOutputArgs(): diff --git a/scripts/deforum_helpers/general_utils.py b/scripts/deforum_helpers/general_utils.py index db94989a..30e9d843 100644 --- a/scripts/deforum_helpers/general_utils.py +++ b/scripts/deforum_helpers/general_utils.py @@ -141,4 +141,7 @@ def download_file_with_checksum(url, expected_checksum, dest_folder, dest_filena if not os.path.exists(expected_full_path) and not os.path.isdir(expected_full_path): load_file_from_url(url=url, model_dir=dest_folder, file_name=dest_filename, progress=True) if checksum(expected_full_path) != expected_checksum: - raise Exception(f"Error while downloading {dest_filename}.]nPlease manually download from: {url}\nAnd place it in: {dest_folder}") \ No newline at end of file + raise Exception(f"Error while downloading {dest_filename}.]nPlease manually download from: {url}\nAnd place it in: {dest_folder}") + +def tickOrCross(value): + return "✅" if value else "❌" \ No newline at end of file diff --git a/scripts/deforum_helpers/parseq_adapter.py b/scripts/deforum_helpers/parseq_adapter.py index fee9943d..d217a71b 100644 --- a/scripts/deforum_helpers/parseq_adapter.py +++ b/scripts/deforum_helpers/parseq_adapter.py @@ -24,6 +24,7 @@ import pandas as pd import requests from .animation_key_frames import DeformAnimKeys, ControlNetKeys, LooperAnimKeys from .rich import console +from .general_utils import tickOrCross logging.basicConfig(format='%(asctime)s - %(name)s - %(levelname)s - %(message)s', level=logging.INFO) @@ -35,11 +36,31 @@ class ParseqAdapter(): # Basic data extraction self.use_parseq = parseq_args.parseq_manifest and parseq_args.parseq_manifest.strip() self.use_deltas = parseq_args.parseq_use_deltas + self.non_schedule_overrides = parseq_args.parseq_non_schedule_overrides + + self.video_args = video_args + self.anim_args = anim_args self.parseq_json = self.load_manifest(parseq_args) if self.use_parseq else json.loads('{ "rendered_frames": [{"frame": 0}] }') self.rendered_frames = self.parseq_json['rendered_frames'] - self.max_frame = self.get_max('frame') - self.required_frames = anim_args.max_frames + + # Store the settings from the UI in case we override them, so we can report clearly on what's going on. + self.a1111_fps = video_args.fps + self.a1111_cadence = anim_args.diffusion_cadence + self.a1111_frame_count = anim_args.max_frames + + # These options in the Parseq manifest will override the options used by Deforum _if and only if_ parseq_non_schedule_overrides is true. + # Warning: we mutate fields on the actual anim_args and video_args objects + if (self.use_parseq): + self.fps = self.parseq_json['options']['output_fps'] if 'output_fps' in self.parseq_json['options'] else None + self.cadence = self.parseq_json['options']['cadence'] if 'cadence' in self.parseq_json['options'] else None + self.frame_count = len(self.rendered_frames) + if self.manages_max_frames(): + anim_args.max_frames = self.frame_count + if self.manages_cadence(): + anim_args.diffusion_cadence = self.cadence + if self.manages_fps(): + video_args.fps = self.fps # Wrap the original schedules with Parseq decorators, so that Parseq values will override the original values IFF appropriate. self.anim_keys = ParseqAnimKeysDecorator(self, DeformAnimKeys(anim_args)) @@ -49,12 +70,10 @@ class ParseqAdapter(): # Validation if (self.use_parseq): - self.required_fps = video_args.fps - self.config_output_fps = self.parseq_json['options']['output_fps'] - count_defined_frames = len(self.rendered_frames) - expected_defined_frames = self.max_frame+1 # frames are 0-indexed - if (expected_defined_frames != count_defined_frames): - logging.warning(f"There may be duplicated or missing frame data in the Parseq input: expected {expected_defined_frames} frames including frame 0 because the highest frame number is {self.max_frame}, but there are {count_defined_frames} frames defined.") + max_frame = self.get_max('frame') + expected_frame_count = max_frame+1 + if (self.frame_count != expected_frame_count): + logging.warning(f"There may be duplicated or missing frame data in the Parseq input: expected {expected_frame_count} frames including frame 0 because the highest frame number is {max_frame}, but there are {self.frame_count} frames defined.") if not mute: self.print_parseq_table() @@ -98,24 +117,34 @@ class ParseqAdapter(): table.add_column("Parseq", style="cyan") table.add_column("Deforum", style="green") - table.add_row("Animation", '\n'.join(self.anim_keys.managed_fields()), '\n'.join(self.anim_keys.unmanaged_fields())) + table.add_row("Animation", '\n'.join(self.anim_keys.managed_fields()), '\n'.join(self.anim_keys.unmanaged_fields())) if self.cn_keys: - table.add_row("ControlNet", '\n'.join(self.cn_keys.managed_fields()), '\n'.join(self.cn_keys.unmanaged_fields())) + table.add_row("ControlNet", '\n'.join(self.cn_keys.managed_fields()), '\n'.join(self.cn_keys.unmanaged_fields())) if self.looper_keys: - table.add_row("Guided Images", '\n'.join(self.looper_keys.managed_fields()), '\n'.join(self.looper_keys.unmanaged_fields())) - table.add_row("Prompts", "✅" if self.manages_prompts() else "❌", "✅" if not self.manages_prompts() else "❌") - table.add_row("Frames", str(len(self.rendered_frames)), str(self.required_frames) + (" ⚠️" if str(self.required_frames) != str(len(self.rendered_frames))+"" else "")) - table.add_row("FPS", str(self.config_output_fps), str(self.required_fps) + (" ⚠️" if str(self.required_fps) != str(self.config_output_fps) else "")) + table.add_row("Guided Images", '\n'.join(self.looper_keys.managed_fields()), '\n'.join(self.looper_keys.unmanaged_fields())) + table.add_row("Prompts", tickOrCross(self.manages_prompts()), tickOrCross(not self.manages_prompts())) + table.add_row("Frames", str(self.frame_count) + tickOrCross(self.manages_max_frames()), str(self.a1111_frame_count) + tickOrCross(not self.manages_max_frames())) + table.add_row("FPS", str(self.fps) + tickOrCross(self.manages_fps()), str(self.a1111_fps) + tickOrCross(not self.manages_fps())) + table.add_row("Cadence", str(self.cadence) + tickOrCross(self.manages_cadence()), str(self.a1111_cadence) + tickOrCross(not self.manages_cadence())) console.print("\nUse this table to validate your Parseq & Deforum setup:") console.print(table) def manages_prompts(self): return self.use_parseq and 'deforum_prompt' in self.rendered_frames[0].keys() - + def manages_seed(self): return self.use_parseq and 'seed' in self.rendered_frames[0].keys() + def manages_cadence(self): + return self.use_parseq and self.non_schedule_overrides and self.cadence + + def manages_fps(self): + return self.use_parseq and self.non_schedule_overrides and self.fps + + def manages_max_frames(self): + return self.use_parseq and self.non_schedule_overrides and self.frame_count + def get_max(self, seriesName): return max(self.rendered_frames, key=itemgetter(seriesName))[seriesName] @@ -135,19 +164,21 @@ class ParseqAbstractDecorator(): logging.debug(f"Found {seriesName} in first frame of Parseq data. Assuming it's defined.") except KeyError: return None + + required_frames = self.adapter.anim_args.max_frames - key_frame_series = pd.Series([np.nan for a in range(self.adapter.required_frames)]) + key_frame_series = pd.Series([np.nan for a in range(required_frames)]) for frame in self.adapter.rendered_frames: frame_idx = frame['frame'] - if frame_idx < self.adapter.required_frames: + if frame_idx < required_frames: if not np.isnan(key_frame_series[frame_idx]): logging.warning(f"Duplicate frame definition {frame_idx} detected for data {seriesName}. Latest wins.") key_frame_series[frame_idx] = frame[seriesName] # If the animation will have more frames than Parseq defines, # duplicate final value to match the required frame count. - while (frame_idx < self.adapter.required_frames): + while (frame_idx < required_frames): key_frame_series[frame_idx] = operator.itemgetter(-1)(self.adapter.rendered_frames)[seriesName] frame_idx += 1 diff --git a/scripts/deforum_helpers/parseq_adapter_test.py b/scripts/deforum_helpers/parseq_adapter_test.py index b05f89e9..1cc23759 100644 --- a/scripts/deforum_helpers/parseq_adapter_test.py +++ b/scripts/deforum_helpers/parseq_adapter_test.py @@ -33,7 +33,7 @@ DEFAULT_ARGS = SimpleNamespace(anim_args = SimpleNamespace(max_frames=2), def buildParseqAdapter(parseq_use_deltas, parseq_manifest, setup_args=DEFAULT_ARGS): - return ParseqAdapter(SimpleNamespace(parseq_use_deltas=parseq_use_deltas, parseq_manifest=parseq_manifest), + return ParseqAdapter(SimpleNamespace(parseq_use_deltas=parseq_use_deltas, parseq_non_schedule_overrides=False, parseq_manifest=parseq_manifest), setup_args.anim_args, setup_args.video_args, setup_args.controlnet_args, setup_args.loop_args) class TestParseqAnimKeys(unittest.TestCase): diff --git a/scripts/deforum_helpers/render.py b/scripts/deforum_helpers/render.py index 70e57ba8..853ae14a 100644 --- a/scripts/deforum_helpers/render.py +++ b/scripts/deforum_helpers/render.py @@ -53,6 +53,10 @@ from .RAFT import RAFT from deforum_api import JobStatusTracker def render_animation(args, anim_args, video_args, parseq_args, loop_args, controlnet_args, root): + + # initialise Parseq adapter + parseq_adapter = ParseqAdapter(parseq_args, anim_args, video_args, controlnet_args, loop_args) + if opts.data.get("deforum_save_gen_info_as_srt", False): # create .srt file and set timeframe mechanism using FPS srt_filename = os.path.join(args.outdir, f"{root.timestring}.srt") srt_frame_duration = init_srt_file(srt_filename, video_args.fps) @@ -80,9 +84,6 @@ def render_animation(args, anim_args, video_args, parseq_args, loop_args, contro if is_controlnet_enabled(controlnet_args): unpack_controlnet_vids(args, anim_args, controlnet_args) - # initialise Parseq adapter - parseq_adapter = ParseqAdapter(parseq_args, anim_args, video_args, controlnet_args, loop_args) - # expand key frame strings to values keys = DeformAnimKeys(anim_args, args.seed) if not parseq_adapter.use_parseq else parseq_adapter.anim_keys loopSchedulesAndData = LooperAnimKeys(loop_args, anim_args, args.seed) if not parseq_adapter.use_parseq else parseq_adapter.looper_keys diff --git a/scripts/deforum_helpers/ui_elements.py b/scripts/deforum_helpers/ui_elements.py index a3b598ad..6e70076e 100644 --- a/scripts/deforum_helpers/ui_elements.py +++ b/scripts/deforum_helpers/ui_elements.py @@ -352,6 +352,8 @@ def get_tab_init(d, da, dp): gr.HTML(value=get_gradio_html('parseq')) with FormRow(): parseq_manifest = create_gr_elem(dp.parseq_manifest) + with FormRow(): + parseq_non_schedule_overrides = create_gr_elem(dp.parseq_non_schedule_overrides) with FormRow(): parseq_use_deltas = create_gr_elem(dp.parseq_use_deltas) return {k: v for k, v in {**locals(), **vars()}.items()}