diff --git a/.gitignore b/.gitignore index 876e20e..2638864 100644 --- a/.gitignore +++ b/.gitignore @@ -129,3 +129,4 @@ dmypy.json .pyre/ .vscode/settings.json .DS_Store +/.vs diff --git a/iz_helpers/__init__.py b/iz_helpers/__init__.py index 8798cf1..19c0211 100644 --- a/iz_helpers/__init__.py +++ b/iz_helpers/__init__.py @@ -1,2 +1,2 @@ -from .image import shrink_and_paste_on_blank -from .video import write_video +# from .ui import on_ui_tabs +# from .settings import on_ui_settings diff --git a/iz_helpers/extra.py b/iz_helpers/extra.py new file mode 100644 index 0000000..e69de29 diff --git a/iz_helpers/helpers.py b/iz_helpers/helpers.py new file mode 100644 index 0000000..c7b110e --- /dev/null +++ b/iz_helpers/helpers.py @@ -0,0 +1,126 @@ +import math +import os +import modules.shared as shared +import modules.sd_models +import gradio as gr +from scripts import postprocessing_upscale +from .prompt_util import readJsonPrompt +import asyncio + + +def fix_env_Path_ffprobe(): + envpath = os.environ["PATH"] + ffppath = shared.opts.data.get("infzoom_ffprobepath", "") + + if ffppath and not ffppath in envpath: + path_sep = ";" if os.name == "nt" else ":" + os.environ["PATH"] = envpath + path_sep + ffppath + + +def closest_upper_divisible_by_eight(num): + if num % 8 == 0: + return num + else: + return math.ceil(num / 8) * 8 + + +def load_model_from_setting(model_field_name, progress, progress_desc): + # fix typo in Automatic1111 vs Vlad111 + if hasattr(modules.sd_models, "checkpoint_alisases"): + checkPList = modules.sd_models.checkpoint_alisases + elif hasattr(modules.sd_models, "checkpoint_aliases"): + checkPList = modules.sd_models.checkpoint_aliases + else: + raise Exception( + "This is not a compatible StableDiffusion Platform, can not access checkpoints" + ) + + model_name = shared.opts.data.get(model_field_name) + if model_name is not None and model_name != "": + checkinfo = checkPList[model_name] + + if not checkinfo: + raise NameError(model_field_name + " Does not exist in your models.") + + if progress: + progress(0, desc=progress_desc + checkinfo.name) + + modules.sd_models.load_model(checkinfo) + + +def do_upscaleImg(curImg, upscale_do, upscaler_name, upscale_by): + if not upscale_do: + return curImg + + # ensure even width and even height for ffmpeg + # if odd, switch to scale to mode + rwidth = round(curImg.width * upscale_by) + rheight = round(curImg.height * upscale_by) + + ups_mode = 2 # upscale_by + if (rwidth % 2) == 1: + ups_mode = 1 + rwidth += 1 + if (rheight % 2) == 1: + ups_mode = 1 + rheight += 1 + + if 1 == ups_mode: + print( + "Infinite Zoom: aligning output size to even width and height: " + + str(rwidth) + + " x " + + str(rheight), + end="\r", + ) + + pp = postprocessing_upscale.scripts_postprocessing.PostprocessedImage(curImg) + ups = postprocessing_upscale.ScriptPostprocessingUpscale() + ups.process( + pp, + upscale_mode=ups_mode, + upscale_by=upscale_by, + upscale_to_width=rwidth, + upscale_to_height=rheight, + upscale_crop=False, + upscaler_1_name=upscaler_name, + upscaler_2_name=None, + upscaler_2_visibility=0.0, + ) + return pp.image + +async def showGradioErrorAsync(txt, delay=1): + await asyncio.sleep(delay) # sleep for 1 second + raise gr.Error(txt) + +def putPrompts(files): + try: + with open(files.name, "r") as f: + file_contents = f.read() + + data = readJsonPrompt(file_contents,False) + return [ + gr.Textbox.update(data["prePrompt"]), + gr.DataFrame.update(data["prompts"]), + gr.Textbox.update(data["postPrompt"]), + gr.Textbox.update(data["negPrompt"]) + ] + + except Exception: + print( + "[InfiniteZoom:] Loading your prompt failed. It seems to be invalid. Your prompt table is preserved." + ) + + # error only be shown with raise, so ui gets broken. + #asyncio.run(showGradioErrorAsync("Loading your prompts failed. It seems to be invalid. Your prompt table has been preserved.",5)) + + return [gr.Textbox.update(), gr.DataFrame.update(), gr.Textbox.update(),gr.Textbox.update()] + + +def clearPrompts(): + return [ + gr.DataFrame.update(value=[[0, "Infinite Zoom. Start over"]]), + gr.Textbox.update(""), + gr.Textbox.update(""), + gr.Textbox.update("") + ] diff --git a/iz_helpers/prompt_util.py b/iz_helpers/prompt_util.py new file mode 100644 index 0000000..4ef5433 --- /dev/null +++ b/iz_helpers/prompt_util.py @@ -0,0 +1,67 @@ +import json +from jsonschema import validate + +from .static_variables import ( + empty_prompt, + invalid_prompt, + jsonprompt_schemafile, + promptTableHeaders +) + +def completeOptionals(j): + if isinstance(j, dict): + # Remove header information, user dont pimp our ui + if "prompts" in j: + if "headers" in j["prompts"]: + del j["prompts"]["headers"] + j["prompts"]["headers"]=promptTableHeaders + + if "negPrompt" not in j: + j["negPrompt"]="" + + if "prePrompt" not in j: + if "commonPromptPrefix" in j: + j["prePrompt"]=j["commonPromptPrefix"] + else: + j["prePrompt"]="" + + if "postPrompt" not in j: + if "commonPromptSuffix" in j: + j["postPrompt"]=j["commonPromptSuffix"] + else: + j["postPrompt"]="" + + return j + + +def validatePromptJson_throws(data): + with open(jsonprompt_schemafile, "r") as s: + schema = json.load(s) + try: + validate(instance=data, schema=schema) + + except Exception: + raise Exception("Your prompts are not schema valid.") + + return completeOptionals(data) + + +def readJsonPrompt(txt, returnFailPrompt=False): + if not txt: + return empty_prompt + + try: + jpr = json.loads(txt) + except Exception: + if returnFailPrompt: + print (f"Infinite Zoom: Corrupted Json structure: {txt[:24]} ...") + return invalid_prompt + raise (f"Infinite Zoom: Corrupted Json structure: {txt[:24]} ...") + + try: + return validatePromptJson_throws(jpr) + except Exception: + if returnFailPrompt: + return invalid_prompt + pass + diff --git a/iz_helpers/promptschema.json b/iz_helpers/promptschema.json new file mode 100644 index 0000000..ec513e0 --- /dev/null +++ b/iz_helpers/promptschema.json @@ -0,0 +1,60 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "$id": "1.1", + "type": "object", + "properties": { + "prompts": { + "type": "object", + "properties": { + "data": { + "type": "array", + "items": { + "type": "array", + "items": [ + { + "oneOf": [ + { + "type": "integer", + "minimum": 0 + }, + { + "type": "string" + } + ] + }, + { + "type": "string" + } + ], + "minItems": 0, + "maxItems": 999, + "uniqueItems": false + }, + "minItems": 0 + }, + "headers": { + "type": "array", + "items": { + "type": "string" + }, + "minItems": 2 + } + }, + "required": [ + "data" + ] + }, + "negPrompt": { + "type": "string" + }, + "prePrompt": { + "type": "string" + }, + "postPrompt": { + "type": "string" + } + }, + "required": [ + "prompts" + ] +} \ No newline at end of file diff --git a/iz_helpers/run.py b/iz_helpers/run.py new file mode 100644 index 0000000..d224af5 --- /dev/null +++ b/iz_helpers/run.py @@ -0,0 +1,512 @@ +import math, time, os +import numpy as np +from PIL import Image, ImageFilter, ImageDraw +from modules.ui import plaintext_to_html +import modules.shared as shared +from modules.paths_internal import script_path +from .helpers import ( + fix_env_Path_ffprobe, + closest_upper_divisible_by_eight, + load_model_from_setting, + do_upscaleImg, +) +from .sd_helpers import renderImg2Img, renderTxt2Img +from .image import shrink_and_paste_on_blank +from .video import write_video + + +def crop_fethear_ellipse(image, feather_margin=30, width_offset=0, height_offset=0): + # Create a blank mask image with the same size as the original image + mask = Image.new("L", image.size, 0) + draw = ImageDraw.Draw(mask) + + # Calculate the ellipse's bounding box + ellipse_box = ( + width_offset, + height_offset, + image.width - width_offset, + image.height - height_offset, + ) + + # Draw the ellipse on the mask + draw.ellipse(ellipse_box, fill=255) + + # Apply the mask to the original image + result = Image.new("RGBA", image.size) + result.paste(image, mask=mask) + + # Crop the resulting image to the ellipse's bounding box + cropped_image = result.crop(ellipse_box) + + # Create a new mask image with a black background (0) + mask = Image.new("L", cropped_image.size, 0) + draw = ImageDraw.Draw(mask) + + # Draw an ellipse on the mask image + draw.ellipse( + ( + 0 + feather_margin, + 0 + feather_margin, + cropped_image.width - feather_margin, + cropped_image.height - feather_margin, + ), + fill=255, + outline=0, + ) + + # Apply a Gaussian blur to the mask image + mask = mask.filter(ImageFilter.GaussianBlur(radius=feather_margin / 2)) + cropped_image.putalpha(mask) + res = Image.new(cropped_image.mode, (image.width, image.height)) + paste_pos = ( + int((res.width - cropped_image.width) / 2), + int((res.height - cropped_image.height) / 2), + ) + res.paste(cropped_image, paste_pos) + + return res + + +def outpaint_steps( + width, + height, + common_prompt_pre, + common_prompt_suf, + prompts, + negative_prompt, + seed, + sampler, + num_inference_steps, + guidance_scale, + inpainting_denoising_strength, + inpainting_mask_blur, + inpainting_fill_mode, + inpainting_full_res, + inpainting_padding, + init_img, + outpaint_steps, + out_config, + mask_width, + mask_height, + custom_exit_image, + frame_correction=True, # TODO: add frame_Correction in UI +): + main_frames = [init_img.convert("RGB")] + + for i in range(outpaint_steps): + print_out = ( + "Outpaint step: " + + str(i + 1) + + " / " + + str(outpaint_steps) + + " Seed: " + + str(seed) + ) + print(print_out) + current_image = main_frames[-1] + current_image = shrink_and_paste_on_blank( + current_image, mask_width, mask_height + ) + + mask_image = np.array(current_image)[:, :, 3] + mask_image = Image.fromarray(255 - mask_image).convert("RGB") + # create mask (black image with white mask_width width edges) + + if custom_exit_image and ((i + 1) == outpaint_steps): + current_image = custom_exit_image.resize( + (width, height), resample=Image.LANCZOS + ) + main_frames.append(current_image.convert("RGB")) + # print("using Custom Exit Image") + save2Collect(current_image, out_config, f"exit_img.png") + else: + pr = prompts[max(k for k in prompts.keys() if k <= i)] + processed, newseed = renderImg2Img( + f"{common_prompt_pre}\n{pr}\n{common_prompt_suf}".strip(), + negative_prompt, + sampler, + num_inference_steps, + guidance_scale, + seed, + width, + height, + current_image, + mask_image, + inpainting_denoising_strength, + inpainting_mask_blur, + inpainting_fill_mode, + inpainting_full_res, + inpainting_padding, + ) + + if len(processed.images) > 0: + main_frames.append(processed.images[0].convert("RGB")) + save2Collect(processed.images[0], out_config, f"outpain_step_{i}.png") + seed = newseed + # TODO: seed behavior + + if frame_correction and inpainting_mask_blur > 0: + corrected_frame = crop_inner_image( + main_frames[i + 1], mask_width, mask_height + ) + + enhanced_img = crop_fethear_ellipse( + main_frames[i], + 30, + inpainting_mask_blur / 3 // 2, + inpainting_mask_blur / 3 // 2, + ) + save2Collect(main_frames[i], out_config, f"main_frame_{i}") + save2Collect(enhanced_img, out_config, f"main_frame_enhanced_{i}") + corrected_frame.paste(enhanced_img, mask=enhanced_img) + main_frames[i] = corrected_frame + # else :TEST + # current_image.paste(prev_image, mask=prev_image) + return main_frames, processed + + +def create_zoom( + common_prompt_pre, + prompts_array, + common_prompt_suf, + negative_prompt, + num_outpainting_steps, + guidance_scale, + num_inference_steps, + custom_init_image, + custom_exit_image, + video_frame_rate, + video_zoom_mode, + video_start_frame_dupe_amount, + video_last_frame_dupe_amount, + inpainting_mask_blur, + inpainting_fill_mode, + zoom_speed, + seed, + outputsizeW, + outputsizeH, + batchcount, + sampler, + upscale_do, + upscaler_name, + upscale_by, + inpainting_denoising_strength=1, + inpainting_full_res=0, + inpainting_padding=0, + progress=None, +): + for i in range(batchcount): + print(f"Batch {i+1}/{batchcount}") + result = create_zoom_single( + common_prompt_pre, + prompts_array, + common_prompt_suf, + negative_prompt, + num_outpainting_steps, + guidance_scale, + num_inference_steps, + custom_init_image, + custom_exit_image, + video_frame_rate, + video_zoom_mode, + video_start_frame_dupe_amount, + video_last_frame_dupe_amount, + inpainting_mask_blur, + inpainting_fill_mode, + zoom_speed, + seed, + outputsizeW, + outputsizeH, + sampler, + upscale_do, + upscaler_name, + upscale_by, + inpainting_denoising_strength, + inpainting_full_res, + inpainting_padding, + progress, + ) + return result + + +def prepare_output_path(): + isCollect = shared.opts.data.get("infzoom_collectAllResources", False) + output_path = shared.opts.data.get("infzoom_outpath", "outputs") + + save_path = os.path.join( + output_path, shared.opts.data.get("infzoom_outSUBpath", "infinite-zooms") + ) + + if isCollect: + save_path = os.path.join(save_path, "iz_collect" + str(int(time.time()))) + + if not os.path.exists(save_path): + os.makedirs(save_path) + + video_filename = os.path.join( + save_path, "infinite_zoom_" + str(int(time.time())) + ".mp4" + ) + + return { + "isCollect": isCollect, + "save_path": save_path, + "video_filename": video_filename, + } + + +def save2Collect(img, out_config, name): + if out_config["isCollect"]: + img.save(f'{out_config["save_path"]}/{name}.png') + + +def frame2Collect(all_frames, out_config): + save2Collect(all_frames[-1], out_config, f"frame_{len(all_frames)}") + + +def frames2Collect(all_frames, out_config): + for i, f in enumerate(all_frames): + save2Collect(f, out_config, f"frame_{i}") + + +def crop_inner_image(outpainted_img, width_offset, height_offset): + width, height = outpainted_img.size + + center_x, center_y = int(width / 2), int(height / 2) + + # Crop the image to the center + cropped_img = outpainted_img.crop( + ( + center_x - width_offset, + center_y - height_offset, + center_x + width_offset, + center_y + height_offset, + ) + ) + prev_step_img = cropped_img.resize((width, height), resample=Image.LANCZOS) + # resized_img = resized_img.filter(ImageFilter.SHARPEN) + + return prev_step_img + + +def create_zoom_single( + common_prompt_pre, + prompts_array, + common_prompt_suf, + negative_prompt, + num_outpainting_steps, + guidance_scale, + num_inference_steps, + custom_init_image, + custom_exit_image, + video_frame_rate, + video_zoom_mode, + video_start_frame_dupe_amount, + video_last_frame_dupe_amount, + inpainting_mask_blur, + inpainting_fill_mode, + zoom_speed, + seed, + outputsizeW, + outputsizeH, + sampler, + upscale_do, + upscaler_name, + upscale_by, + inpainting_denoising_strength, + inpainting_full_res, + inpainting_padding, + progress, +): + # try: + # if gr.Progress() is not None: + # progress = gr.Progress() + # progress(0, desc="Preparing Initial Image") + # except Exception: + # pass + fix_env_Path_ffprobe() + out_config = prepare_output_path() + + prompts = {} + + for x in prompts_array: + try: + key = int(x[0]) + value = str(x[1]) + prompts[key] = value + except ValueError: + pass + + assert len(prompts_array) > 0, "prompts is empty" + + width = closest_upper_divisible_by_eight(outputsizeW) + height = closest_upper_divisible_by_eight(outputsizeH) + + current_image = Image.new(mode="RGBA", size=(width, height)) + mask_image = np.array(current_image)[:, :, 3] + mask_image = Image.fromarray(255 - mask_image).convert("RGB") + current_image = current_image.convert("RGB") + current_seed = seed + + if custom_init_image: + current_image = custom_init_image.resize( + (width, height), resample=Image.LANCZOS + ) + save2Collect(current_image, out_config, f"init_custom.png") + + else: + load_model_from_setting( + "infzoom_txt2img_model", progress, "Loading Model for txt2img: " + ) + + pr = prompts[min(k for k in prompts.keys() if k >= 0)] + processed, newseed = renderTxt2Img( + f"{common_prompt_pre}\n{pr}\n{common_prompt_suf}".strip(), + negative_prompt, + sampler, + num_inference_steps, + guidance_scale, + current_seed, + width, + height, + ) + if len(processed.images) > 0: + current_image = processed.images[0] + save2Collect(current_image, out_config, f"init_txt2img.png") + current_seed = newseed + + mask_width = math.trunc(width / 4) # was initially 512px => 128px + mask_height = math.trunc(height / 4) # was initially 512px => 128px + + num_interpol_frames = round(video_frame_rate * zoom_speed) + + all_frames = [] + + if upscale_do and progress: + progress(0, desc="upscaling inital image") + + load_model_from_setting( + "infzoom_inpainting_model", progress, "Loading Model for inpainting/img2img: " + ) + main_frames, processed = outpaint_steps( + width, + height, + common_prompt_pre, + common_prompt_suf, + prompts, + negative_prompt, + seed, + sampler, + num_inference_steps, + guidance_scale, + inpainting_denoising_strength, + inpainting_mask_blur, + inpainting_fill_mode, + inpainting_full_res, + inpainting_padding, + current_image, + num_outpainting_steps, + out_config, + mask_width, + mask_height, + custom_exit_image, + ) + all_frames.append( + do_upscaleImg(main_frames[0], upscale_do, upscaler_name, upscale_by) + if upscale_do + else main_frames[0] + ) + for i in range(len(main_frames) - 1): + # interpolation steps between 2 inpainted images (=sequential zoom and crop) + for j in range(num_interpol_frames - 1): + current_image = main_frames[i + 1] + interpol_image = current_image + save2Collect(interpol_image, out_config, f"interpol_img_{i}_{j}].png") + + interpol_width = math.ceil( + ( + 1 + - (1 - 2 * mask_width / width) + ** (1 - (j + 1) / num_interpol_frames) + ) + * width + / 2 + ) + + interpol_height = math.ceil( + ( + 1 + - (1 - 2 * mask_height / height) + ** (1 - (j + 1) / num_interpol_frames) + ) + * height + / 2 + ) + + interpol_image = interpol_image.crop( + ( + interpol_width, + interpol_height, + width - interpol_width, + height - interpol_height, + ) + ) + + interpol_image = interpol_image.resize((width, height)) + save2Collect(interpol_image, out_config, f"interpol_resize_{i}_{j}.png") + + # paste the higher resolution previous image in the middle to avoid drop in quality caused by zooming + interpol_width2 = math.ceil( + (1 - (width - 2 * mask_width) / (width - 2 * interpol_width)) + / 2 + * width + ) + + interpol_height2 = math.ceil( + (1 - (height - 2 * mask_height) / (height - 2 * interpol_height)) + / 2 + * height + ) + + prev_image_fix_crop = shrink_and_paste_on_blank( + main_frames[i], interpol_width2, interpol_height2 + ) + + interpol_image.paste(prev_image_fix_crop, mask=prev_image_fix_crop) + save2Collect(interpol_image, out_config, f"interpol_prevcrop_{i}_{j}.png") + + if upscale_do and progress: + progress(((i + 1) / num_outpainting_steps), desc="upscaling interpol") + + all_frames.append( + do_upscaleImg(interpol_image, upscale_do, upscaler_name, upscale_by) + if upscale_do + else interpol_image + ) + + if upscale_do and progress: + progress(((i + 1) / num_outpainting_steps), desc="upscaling current") + + all_frames.append( + do_upscaleImg(current_image, upscale_do, upscaler_name, upscale_by) + if upscale_do + else current_image + ) + + frames2Collect(all_frames, out_config) + + write_video( + out_config["video_filename"], + all_frames, + video_frame_rate, + video_zoom_mode, + int(video_start_frame_dupe_amount), + int(video_last_frame_dupe_amount), + ) + print("Video saved in: " + os.path.join(script_path, out_config["video_filename"])) + return ( + out_config["video_filename"], + main_frames, + processed.js(), + plaintext_to_html(processed.info), + plaintext_to_html(""), + ) diff --git a/iz_helpers/sd_helpers.py b/iz_helpers/sd_helpers.py new file mode 100644 index 0000000..e6d5d6b --- /dev/null +++ b/iz_helpers/sd_helpers.py @@ -0,0 +1,81 @@ +from modules.processing import ( + process_images, + StableDiffusionProcessingTxt2Img, + StableDiffusionProcessingImg2Img, +) +import modules.shared as shared + + +def renderTxt2Img( + prompt, negative_prompt, sampler, steps, cfg_scale, seed, width, height +): + processed = None + p = StableDiffusionProcessingTxt2Img( + sd_model=shared.sd_model, + outpath_samples=shared.opts.outdir_txt2img_samples, + outpath_grids=shared.opts.outdir_txt2img_grids, + prompt=prompt, + negative_prompt=negative_prompt, + seed=seed, + sampler_name=sampler, + n_iter=1, + steps=steps, + cfg_scale=cfg_scale, + width=width, + height=height, + ) + processed = process_images(p) + newseed = p.seed + return processed, newseed + + +def renderImg2Img( + prompt, + negative_prompt, + sampler, + steps, + cfg_scale, + seed, + width, + height, + init_image, + mask_image, + inpainting_denoising_strength, + inpainting_mask_blur, + inpainting_fill_mode, + inpainting_full_res, + inpainting_padding, +): + processed = None + + p = StableDiffusionProcessingImg2Img( + sd_model=shared.sd_model, + outpath_samples=shared.opts.outdir_img2img_samples, + outpath_grids=shared.opts.outdir_img2img_grids, + prompt=prompt, + negative_prompt=negative_prompt, + seed=seed, + sampler_name=sampler, + n_iter=1, + steps=steps, + cfg_scale=cfg_scale, + width=width, + height=height, + init_images=[init_image], + denoising_strength=inpainting_denoising_strength, + mask_blur=inpainting_mask_blur, + inpainting_fill=inpainting_fill_mode, + inpaint_full_res=inpainting_full_res, + inpaint_full_res_padding=inpainting_padding, + mask=mask_image, + ) + # p.latent_mask = Image.new("RGB", (p.width, p.height), "white") + + processed = process_images(p) + # For those that use Image grids this will make sure that ffmpeg does not crash out + if (len(processed.images) > 1) and (processed.images[0].size[0] != processed.images[-1].size[0]): + processed.images.pop(0) + print("\nGrid image detected applying patch") + + newseed = p.seed + return processed, newseed diff --git a/iz_helpers/settings.py b/iz_helpers/settings.py new file mode 100644 index 0000000..5b000c6 --- /dev/null +++ b/iz_helpers/settings.py @@ -0,0 +1,108 @@ +import gradio as gr +import modules.shared as shared +from .static_variables import default_prompt + + +def on_ui_settings(): + section = ("infinite-zoom", "Infinite Zoom") + + shared.opts.add_option( + "infzoom_outpath", + shared.OptionInfo( + "outputs", + "Path where to store your infinite video. Default is Outputs", + gr.Textbox, + {"interactive": True}, + section=section, + ), + ) + + shared.opts.add_option( + "infzoom_outSUBpath", + shared.OptionInfo( + "infinite-zooms", + "Which subfolder name to be created in the outpath. Default is 'infinite-zooms'", + gr.Textbox, + {"interactive": True}, + section=section, + ), + ) + + shared.opts.add_option( + "infzoom_outsizeW", + shared.OptionInfo( + 512, + "Default width of your video", + gr.Slider, + {"minimum": 16, "maximum": 2048, "step": 16}, + section=section, + ), + ) + + shared.opts.add_option( + "infzoom_outsizeH", + shared.OptionInfo( + 512, + "Default height your video", + gr.Slider, + {"minimum": 16, "maximum": 2048, "step": 16}, + section=section, + ), + ) + + shared.opts.add_option( + "infzoom_ffprobepath", + shared.OptionInfo( + "", + "Writing videos has dependency to an existing FFPROBE executable on your machine. D/L here (https://github.com/BtbN/FFmpeg-Builds/releases) your OS variant and point to your installation path", + gr.Textbox, + {"interactive": True}, + section=section, + ), + ) + + shared.opts.add_option( + "infzoom_txt2img_model", + shared.OptionInfo( + None, + "Name of your desired model to render keyframes (txt2img)", + gr.Dropdown, + lambda: {"choices": [x for x in list(shared.list_checkpoint_tiles()) if "inpainting" not in x]}, + section=section, + ), + ) + + shared.opts.add_option( + "infzoom_inpainting_model", + shared.OptionInfo( + None, + "Name of your desired inpaint model (img2img-inpaint). Default is vanilla sd-v1-5-inpainting.ckpt ", + gr.Dropdown, + lambda: {"choices": [x for x in list(shared.list_checkpoint_tiles()) if "inpainting" in x]}, + section=section, + ), + ) + + shared.opts.add_option( + "infzoom_defPrompt", + shared.OptionInfo( + default_prompt, + "Default prompt-setup to start with'", + gr.Code, + {"interactive": True, "language": "json"}, + section=section, + ), + ) + + + shared.opts.add_option( + "infzoom_collectAllResources", + shared.OptionInfo( + False, + "!!! Store all images (txt2img, init_image,exit_image, inpainting, interpolation) into one folder in your OUTPUT Path. Very slow, a lot of data. Dont do this on long runs !!!", + gr.Checkbox, + {"interactive": True}, + section=section, + ), + ) + diff --git a/iz_helpers/static_variables.py b/iz_helpers/static_variables.py new file mode 100644 index 0000000..a54c1f5 --- /dev/null +++ b/iz_helpers/static_variables.py @@ -0,0 +1,51 @@ +import os +from modules import scripts +import modules.sd_samplers + +default_sampling_steps = 35 +default_sampler = "DDIM" +default_cfg_scale = 8 +default_mask_blur = 48 +default_total_outpaints = 5 +promptTableHeaders = ["Start at second [0,1,...]", "Prompt"] + +default_prompt = """ +{ + "prePrompt": "Huge spectacular Waterfall in ", + "prompts": { + "data": [ + [0, "a dense tropical forest"], + [2, "a Lush jungle"], + [3, "a Thick rainforest"], + [5, "a Verdant canopy"] + ] + }, + "postPrompt": "epic perspective,(vegetation overgrowth:1.3)(intricate, ornamentation:1.1),(baroque:1.1), fantasy, (realistic:1) digital painting , (magical,mystical:1.2) , (wide angle shot:1.4), (landscape composed:1.2)(medieval:1.1),(tropical forest:1.4),(river:1.3) volumetric lighting ,epic, style by Alex Horley Wenjun Lin greg rutkowski Ruan Jia (Wayne Barlowe:1.2)", + "negPrompt": "frames, border, edges, borderline, text, character, duplicate, error, out of frame, watermark, low quality, ugly, deformed, blur, bad-artist" +} +""" + +empty_prompt = '{"prompts":{"data":[],"negPrompt":"", prePrompt:"", postPrompt:""}' + +invalid_prompt = { + "prompts": { + "data": [[0, "Your prompt-json is invalid, please check Settings"]], + }, + "negPrompt": "Invalid prompt-json", + "prePromp": "Invalid prompt", + "postPrompt": "Invalid prompt", +} + +available_samplers = [ + s.name for s in modules.sd_samplers.samplers if "UniPc" not in s.name +] + +current_script_dir = scripts.basedir().split(os.sep)[ + -2: +] # contains install and our extension foldername +jsonprompt_schemafile = ( + current_script_dir[0] + + "/" + + current_script_dir[1] + + "/iz_helpers/promptschema.json" +) diff --git a/iz_helpers/ui.py b/iz_helpers/ui.py new file mode 100644 index 0000000..7f28b48 --- /dev/null +++ b/iz_helpers/ui.py @@ -0,0 +1,307 @@ +import gradio as gr +from .run import create_zoom +import modules.shared as shared +from webui import wrap_gradio_gpu_call +from modules.ui import create_output_panel + +from .static_variables import ( + default_prompt, + available_samplers, + default_total_outpaints, + default_sampling_steps, + default_cfg_scale, + default_mask_blur, + default_sampler, +) +from .helpers import putPrompts, clearPrompts +from .prompt_util import readJsonPrompt +from .static_variables import promptTableHeaders + + +def on_ui_tabs(): + with gr.Blocks(analytics_enabled=False) as infinite_zoom_interface: + gr.HTML( + """ +

+ GitHub Repo + Discord server +

+ + """ + ) + with gr.Row(): + generate_btn = gr.Button(value="Generate video", variant="primary") + interrupt = gr.Button(value="Interrupt", elem_id="interrupt_training") + with gr.Row(): + with gr.Column(scale=1, variant="panel"): + with gr.Tab("Main"): + with gr.Row(): + batchcount_slider = gr.Slider( + minimum=1, + maximum=25, + value=shared.opts.data.get("infzoom_batchcount", 1), + step=1, + label="Batch Count", + ) + main_outpaint_steps = gr.Number( + label="Total video length [s]", + value=default_total_outpaints, + precision=0, + interactive=True, + ) + + # safe reading json prompt + pr = shared.opts.data.get("infzoom_defPrompt", default_prompt) + jpr = readJsonPrompt(pr, True) + + main_common_prompt_pre = gr.Textbox( + value=jpr["prePrompt"], label="Common Prompt Prefix" + ) + + main_prompts = gr.Dataframe( + type="array", + headers=promptTableHeaders, + datatype=["number", "str"], + row_count=1, + col_count=(2, "fixed"), + value=jpr["prompts"], + wrap=True, + ) + + main_common_prompt_suf = gr.Textbox( + value=jpr["postPrompt"], label="Common Prompt Suffix" + ) + + main_negative_prompt = gr.Textbox( + value=jpr["negPrompt"], label="Negative Prompt" + ) + + # these button will be moved using JS under the dataframe view as small ones + exportPrompts_button = gr.Button( + value="Export prompts", + variant="secondary", + elem_classes="sm infzoom_tab_butt", + elem_id="infzoom_exP_butt", + ) + importPrompts_button = gr.UploadButton( + label="Import prompts", + variant="secondary", + elem_classes="sm infzoom_tab_butt", + elem_id="infzoom_imP_butt", + ) + exportPrompts_button.click( + None, + _js="exportPrompts", + inputs=[ + main_common_prompt_pre, + main_prompts, + main_common_prompt_suf, + main_negative_prompt, + ], + outputs=None, + ) + importPrompts_button.upload( + fn=putPrompts, + outputs=[ + main_common_prompt_pre, + main_prompts, + main_common_prompt_suf, + main_negative_prompt, + ], + inputs=[importPrompts_button], + ) + + clearPrompts_button = gr.Button( + value="Clear prompts", + variant="secondary", + elem_classes="sm infzoom_tab_butt", + elem_id="infzoom_clP_butt", + ) + clearPrompts_button.click( + fn=clearPrompts, + inputs=[], + outputs=[ + main_prompts, + main_negative_prompt, + main_common_prompt_pre, + main_common_prompt_suf, + ], + ) + + with gr.Accordion("Render settings"): + with gr.Row(): + seed = gr.Number( + label="Seed", value=-1, precision=0, interactive=True + ) + main_sampler = gr.Dropdown( + label="Sampler", + choices=available_samplers, + value=default_sampler, + type="value", + ) + with gr.Row(): + main_width = gr.Slider( + minimum=16, + maximum=2048, + value=shared.opts.data.get("infzoom_outsizeW", 512), + step=16, + label="Output Width", + ) + main_height = gr.Slider( + minimum=16, + maximum=2048, + value=shared.opts.data.get("infzoom_outsizeH", 512), + step=16, + label="Output Height", + ) + with gr.Row(): + main_guidance_scale = gr.Slider( + minimum=0.1, + maximum=15, + step=0.1, + value=default_cfg_scale, + label="Guidance Scale", + ) + sampling_step = gr.Slider( + minimum=1, + maximum=150, + step=1, + value=default_sampling_steps, + label="Sampling Steps for each outpaint", + ) + with gr.Row(): + init_image = gr.Image( + type="pil", label="Custom initial image" + ) + exit_image = gr.Image( + type="pil", label="Custom exit image", visible=False + ) + with gr.Tab("Video"): + video_frame_rate = gr.Slider( + label="Frames per second", + value=30, + minimum=1, + maximum=60, + ) + video_zoom_mode = gr.Radio( + label="Zoom mode", + choices=["Zoom-out", "Zoom-in"], + value="Zoom-out", + type="index", + ) + video_start_frame_dupe_amount = gr.Slider( + label="number of start frame dupe", + info="Frames to freeze at the start of the video", + value=0, + minimum=1, + maximum=60, + ) + video_last_frame_dupe_amount = gr.Slider( + label="number of last frame dupe", + info="Frames to freeze at the end of the video", + value=0, + minimum=1, + maximum=60, + ) + video_zoom_speed = gr.Slider( + label="Zoom Speed", + value=1.0, + minimum=0.1, + maximum=20.0, + step=0.1, + info="Zoom speed in seconds (higher values create slower zoom)", + ) + + with gr.Tab("Outpaint"): + inpainting_mask_blur = gr.Slider( + label="Mask Blur", + minimum=0, + maximum=64, + value=default_mask_blur, + ) + inpainting_fill_mode = gr.Radio( + label="Masked content", + choices=["fill", "original", "latent noise", "latent nothing"], + value="latent noise", + type="index", + ) + + with gr.Tab("Post proccess"): + upscale_do = gr.Checkbox(False, label="Enable Upscale") + upscaler_name = gr.Dropdown( + label="Upscaler", + elem_id="infZ_upscaler", + choices=[x.name for x in shared.sd_upscalers], + value=shared.sd_upscalers[0].name, + ) + upscale_by = gr.Slider( + label="Upscale by factor", + minimum=1, + maximum=8, + step=0.5, + value=2, + ) + with gr.Accordion("Help", open=False): + gr.Markdown( + """# Performance critical +Depending on amount of frames and which upscaler you choose it might took a long time to render. +Our best experience and trade-off is the R-ERSGAn4x upscaler. +""" + ) + + with gr.Column(scale=1, variant="compact"): + output_video = gr.Video(label="Output").style(width=512, height=512) + ( + out_image, + generation_info, + html_info, + html_log, + ) = create_output_panel( + "infinite-zoom", shared.opts.outdir_img2img_samples + ) + + generate_btn.click( + fn=wrap_gradio_gpu_call(create_zoom, extra_outputs=[None, "", ""]), + inputs=[ + main_common_prompt_pre, + main_prompts, + main_common_prompt_suf, + main_negative_prompt, + main_outpaint_steps, + main_guidance_scale, + sampling_step, + init_image, + exit_image, + video_frame_rate, + video_zoom_mode, + video_start_frame_dupe_amount, + video_last_frame_dupe_amount, + inpainting_mask_blur, + inpainting_fill_mode, + video_zoom_speed, + seed, + main_width, + main_height, + batchcount_slider, + main_sampler, + upscale_do, + upscaler_name, + upscale_by, + ], + outputs=[output_video, out_image, generation_info, html_info, html_log], + ) + + main_prompts.change( + fn=checkPrompts, inputs=[main_prompts], outputs=[generate_btn] + ) + + interrupt.click(fn=shared.state.interrupt(), inputs=[], outputs=[]) + infinite_zoom_interface.queue() + return [(infinite_zoom_interface, "Infinite Zoom", "iz_interface")] + + +def checkPrompts(p): + return gr.Button.update( + interactive=any(0 in sublist for sublist in p) + or any("0" in sublist for sublist in p) + ) diff --git a/iz_helpers/video.py b/iz_helpers/video.py index c3d61a3..cb1c5d9 100644 --- a/iz_helpers/video.py +++ b/iz_helpers/video.py @@ -13,9 +13,8 @@ def write_video(file_path, frames, fps, reversed=True, start_frame_dupe_amount=1 if reversed == True: frames = frames[::-1] - # Get dimensions of the first frames, all subsequent has to be same sized - for k in frames: - assert (k.size == frames[0].size,"Different frame sizes found!") + # Drop missformed frames + frames = [frame for frame in frames if frame.size == frames[0].size] # Create an imageio video writer, avoid block size of 512. writer = imageio.get_writer(file_path, fps=fps, macro_block_size=None) diff --git a/javascript/infinite-zoom-hints.js b/javascript/infinite-zoom-hints.js new file mode 100644 index 0000000..e046f65 --- /dev/null +++ b/javascript/infinite-zoom-hints.js @@ -0,0 +1,49 @@ +// mouseover tooltips for various UI elements + +infzoom_titles = { + "Batch Count":"How many separate videos to create", + "Total video length [s]":"For each seconds frame (FPS) will be generated. Define prompts at which time they should start wihtin this duration.", + "Common Prompt Prefix":"Prompt inserted before each step", + "Common Prompt Suffix":"Prompt inserted after each step", + "Negative Prompt":"What your model shall avoid", + "Export prompts": "Downloads a JSON file to save all prompts", + "Import prompts": "Restore Prompts table from a specific JSON file", + "Clear prompts": "Start over, remove all entries from prompt table, prefix, suffix, negative", + "Custom initial image":"An image at the end resp. begin of your movie, depending or ZoomIn or Out", + "Custom exit image":"An image at the end resp. begin of your movie, depending or ZoomIn or Out", + "Zoom Speed":"Varies additional frames per second", + "Start at second [0,1,...]": "At which time the prompt has to be occure. We need at least one prompt starting at time 0", + "Generate video": "Start rendering. If it´s disabled the prompt table is invalid, check we have a start prompt at time 0" +} + + +onUiUpdate(function(){ + gradioApp().querySelectorAll('span, button, select, p').forEach(function(span){ + tooltip = infzoom_titles[span.textContent]; + + if(!tooltip){ + tooltip = infzoom_titles[span.value]; + } + + if(!tooltip){ + for (const c of span.classList) { + if (c in infzoom_titles) { + tooltip = infzoom_titles[c]; + break; + } + } + } + + if(tooltip){ + span.title = tooltip; + } + }) + + gradioApp().querySelectorAll('select').forEach(function(select){ + if (select.onchange != null) return; + + select.onchange = function(){ + select.title = infzoom_titles[select.value] || ""; + } + }) +}) diff --git a/javascript/infinite-zoom.js b/javascript/infinite-zoom.js index 8f7d546..5709977 100644 --- a/javascript/infinite-zoom.js +++ b/javascript/infinite-zoom.js @@ -1,9 +1,9 @@ // Function to download data to a file -function exportPrompts(p, np, filename = "infinite-zoom-prompts.json") { +function exportPrompts(cppre,p, cpsuf,np, filename = "infinite-zoom-prompts.json") { - let J = { prompts: p, negPrompt: np } + let J = { prompts: p, negPrompt: np, prePrompt: cppre, postPrompt: cpsuf } - var file = new Blob([JSON.stringify(J)], { type: "text/csv" }); + var file = new Blob([JSON.stringify(J,null,2)], { type: "text/csv" }); if (window.navigator.msSaveOrOpenBlob) // IE10+ window.navigator.msSaveOrOpenBlob(file, filename); else { // Others diff --git a/scripts/infinite-zoom.py b/scripts/infinite-zoom.py index a8238d7..60751b0 100644 --- a/scripts/infinite-zoom.py +++ b/scripts/infinite-zoom.py @@ -1,880 +1,5 @@ -import sys -import os -import time -import json -from jsonschema import validate - -import numpy as np -import gradio as gr -from PIL import Image -import math -import json - -from iz_helpers import shrink_and_paste_on_blank, write_video -from webui import wrap_gradio_gpu_call -from modules import script_callbacks, scripts -import modules.shared as shared -from modules.processing import ( - process_images, - StableDiffusionProcessingTxt2Img, - StableDiffusionProcessingImg2Img, -) - -from scripts import postprocessing_upscale - -from modules.ui import create_output_panel, plaintext_to_html -import modules.sd_models -import modules.sd_samplers - -from modules import scripts - -usefulDirs = scripts.basedir().split(os.sep)[ - -2: -] # contains install and our extension foldername -jsonprompt_schemafile = ( - usefulDirs[0] + "/" + usefulDirs[1] + "/scripts/promptschema.json" -) - -available_samplers = [s.name for s in modules.sd_samplers.samplers if "UniPc" not in s.name] - -default_prompt = """ -{ - "prompts":{ - "headers":["outpaint steps","prompt"], - "data":[ - [0,"Huge spectacular Waterfall in a dense tropical forest,epic perspective,(vegetation overgrowth:1.3)(intricate, ornamentation:1.1),(baroque:1.1), fantasy, (realistic:1) digital painting , (magical,mystical:1.2) , (wide angle shot:1.4), (landscape composed:1.2)(medieval:1.1), divine,cinematic,(tropical forest:1.4),(river:1.3)mythology,india, volumetric lighting, Hindu ,epic, Alex Horley Wenjun Lin greg rutkowski Ruan Jia (Wayne Barlowe:1.2) "] - ] - }, - "negPrompt":"frames, borderline, text, character, duplicate, error, out of frame, watermark, low quality, ugly, deformed, blur bad-artist" -} -""" - -empty_prompt = ( - '{"prompts":{"data":[],"headers":["outpaint steps","prompt"]},"negPrompt":""}' -) - -# must be python dict -invalid_prompt = { - "prompts": { - "data": [[0, "Your prompt-json is invalid, please check Settings"]], - "headers": ["outpaint steps", "prompt"], - }, - "negPrompt": "Invalid prompt-json", -} - - -def closest_upper_divisible_by_eight(num): - if num % 8 == 0: - return num - else: - return math.ceil(num / 8) * 8 - - -# example fail: 720 px width * 1.66 upscale => 1195.2 => 1195 crash -# 512 px * 1.66 = 513.66 = ? -# assume ffmpeg will CUT to integer -# 721 /720 - -def do_upscaleImg(curImg, upscale_do, upscaler_name, upscale_by): - if not upscale_do: - return curImg - - # ensure even width and even height for ffmpeg - # if odd, switch to scale to mode - rwidth = round(curImg.width * upscale_by) - rheight = round(curImg.height * upscale_by) - - ups_mode = 2 # upscale_by - if ( (rwidth %2) == 1 ): - ups_mode = 1 - rwidth += 1 - if ( (rheight %2) == 1 ): - ups_mode = 1 - rheight += 1 - - if (1 == ups_mode ): - print ("Infinite Zoom: aligning output size to even width and height: " + str(rwidth) +" x "+str(rheight), end='\r' ) - - pp = postprocessing_upscale.scripts_postprocessing.PostprocessedImage( - curImg - ) - ups = postprocessing_upscale.ScriptPostprocessingUpscale() - ups.process( - pp, - upscale_mode=ups_mode, - upscale_by=upscale_by, - upscale_to_width=rwidth, - upscale_to_height=rheight, - upscale_crop=False, - upscaler_1_name=upscaler_name, - upscaler_2_name=None, - upscaler_2_visibility=0.0, - ) - return pp.image - - -def renderTxt2Img(prompt, negative_prompt, sampler, steps, cfg_scale, width, height): - processed = None - p = StableDiffusionProcessingTxt2Img( - sd_model=shared.sd_model, - outpath_samples=shared.opts.outdir_txt2img_samples, - outpath_grids=shared.opts.outdir_txt2img_grids, - prompt=prompt, - negative_prompt=negative_prompt, - # seed=-1, - sampler_name=sampler, - n_iter=1, - steps=steps, - cfg_scale=cfg_scale, - width=width, - height=height, - ) - processed = process_images(p) - return processed - - -def renderImg2Img( - prompt, - negative_prompt, - sampler, - steps, - cfg_scale, - width, - height, - init_image, - mask_image, - inpainting_denoising_strength, - inpainting_mask_blur, - inpainting_fill_mode, - inpainting_full_res, - inpainting_padding, -): - processed = None - - p = StableDiffusionProcessingImg2Img( - sd_model=shared.sd_model, - outpath_samples=shared.opts.outdir_img2img_samples, - outpath_grids=shared.opts.outdir_img2img_grids, - prompt=prompt, - negative_prompt=negative_prompt, - # seed=-1, - sampler_name=sampler, - n_iter=1, - steps=steps, - cfg_scale=cfg_scale, - width=width, - height=height, - init_images=[init_image], - denoising_strength=inpainting_denoising_strength, - mask_blur=inpainting_mask_blur, - inpainting_fill=inpainting_fill_mode, - inpaint_full_res=inpainting_full_res, - inpaint_full_res_padding=inpainting_padding, - mask=mask_image, - ) - # p.latent_mask = Image.new("RGB", (p.width, p.height), "white") - - processed = process_images(p) - return processed - - -def fix_env_Path_ffprobe(): - envpath = os.environ["PATH"] - ffppath = shared.opts.data.get("infzoom_ffprobepath", "") - - if ffppath and not ffppath in envpath: - path_sep = ";" if os.name == "nt" else ":" - os.environ["PATH"] = envpath + path_sep + ffppath - - -def load_model_from_setting(model_field_name, progress, progress_desc): - # fix typo in Automatic1111 vs Vlad111 - if hasattr(modules.sd_models, "checkpoint_alisases"): - checkPList = modules.sd_models.checkpoint_alisases - elif hasattr(modules.sd_models, "checkpoint_aliases"): - checkPList = modules.sd_models.checkpoint_aliases - else: - raise Exception("This is not a compatible StableDiffusion Platform, can not access checkpoints") - - model_name = shared.opts.data.get(model_field_name) - if model_name is not None and model_name != "": - checkinfo = checkPList[model_name] - - if not checkinfo: - raise NameError(model_field_name + " Does not exist in your models.") - - if progress: - progress(0, desc=progress_desc + checkinfo.name) - - modules.sd_models.load_model(checkinfo) - - -def create_zoom( - prompts_array, - negative_prompt, - num_outpainting_steps, - guidance_scale, - num_inference_steps, - custom_init_image, - custom_exit_image, - video_frame_rate, - video_zoom_mode, - video_start_frame_dupe_amount, - video_last_frame_dupe_amount, - inpainting_denoising_strength, - inpainting_mask_blur, - inpainting_fill_mode, - inpainting_full_res, - inpainting_padding, - zoom_speed, - outputsizeW, - outputsizeH, - batchcount, - sampler, - upscale_do, - upscaler_name, - upscale_by, - progress=None, -): - - for i in range(batchcount): - print(f"Batch {i+1}/{batchcount}") - result = create_zoom_single( - prompts_array, - negative_prompt, - num_outpainting_steps, - guidance_scale, - num_inference_steps, - custom_init_image, - custom_exit_image, - video_frame_rate, - video_zoom_mode, - video_start_frame_dupe_amount, - video_last_frame_dupe_amount, - inpainting_denoising_strength, - inpainting_mask_blur, - inpainting_fill_mode, - inpainting_full_res, - inpainting_padding, - zoom_speed, - outputsizeW, - outputsizeH, - sampler, - upscale_do, - upscaler_name, - upscale_by, - progress, - ) - return result - - -def create_zoom_single( - prompts_array, - negative_prompt, - num_outpainting_steps, - guidance_scale, - num_inference_steps, - custom_init_image, - custom_exit_image, - video_frame_rate, - video_zoom_mode, - video_start_frame_dupe_amount, - video_last_frame_dupe_amount, - inpainting_denoising_strength, - inpainting_mask_blur, - inpainting_fill_mode, - inpainting_full_res, - inpainting_padding, - zoom_speed, - outputsizeW, - outputsizeH, - sampler, - upscale_do, - upscaler_name, - upscale_by, - progress=None, -): - # try: - # if gr.Progress() is not None: - # progress = gr.Progress() - # progress(0, desc="Preparing Initial Image") - # except Exception: - # pass - fix_env_Path_ffprobe() - - prompts = {} - for x in prompts_array: - try: - key = int(x[0]) - value = str(x[1]) - prompts[key] = value - except ValueError: - pass - assert len(prompts_array) > 0, "prompts is empty" - - width = closest_upper_divisible_by_eight(outputsizeW) - height = closest_upper_divisible_by_eight(outputsizeH) - - current_image = Image.new(mode="RGBA", size=(width, height)) - mask_image = np.array(current_image)[:, :, 3] - mask_image = Image.fromarray(255 - mask_image).convert("RGB") - current_image = current_image.convert("RGB") - - if custom_init_image: - current_image = custom_init_image.resize( - (width, height), resample=Image.LANCZOS - ) - else: - load_model_from_setting("infzoom_txt2img_model", progress, "Loading Model for txt2img: ") - - processed = renderTxt2Img( - prompts[min(k for k in prompts.keys() if k >= 0)], - negative_prompt, - sampler, - num_inference_steps, - guidance_scale, - width, - height, - ) - current_image = processed.images[0] - - mask_width = math.trunc(width / 4) # was initially 512px => 128px - mask_height = math.trunc(height / 4) # was initially 512px => 128px - - num_interpol_frames = round(video_frame_rate * zoom_speed) - - all_frames = [] - - if upscale_do and progress: - progress(0, desc="upscaling inital image") - - all_frames.append( - do_upscaleImg(current_image, upscale_do, upscaler_name, upscale_by) - if upscale_do - else current_image - ) - - load_model_from_setting("infzoom_inpainting_model", progress, "Loading Model for inpainting/img2img: " ) - - for i in range(num_outpainting_steps): - print_out = "Outpaint step: " + str(i + 1) + " / " + str(num_outpainting_steps) - print(print_out) - if progress: - progress(((i + 1) / num_outpainting_steps), desc=print_out) - - prev_image_fix = current_image - prev_image = shrink_and_paste_on_blank(current_image, mask_width, mask_height) - current_image = prev_image - - # create mask (black image with white mask_width width edges) - mask_image = np.array(current_image)[:, :, 3] - mask_image = Image.fromarray(255 - mask_image).convert("RGB") - - # inpainting step - current_image = current_image.convert("RGB") - - processed = renderImg2Img( - prompts[max(k for k in prompts.keys() if k <= i)], - negative_prompt, - sampler, - num_inference_steps, - guidance_scale, - width, - height, - current_image, - mask_image, - inpainting_denoising_strength, - inpainting_mask_blur, - inpainting_fill_mode, - inpainting_full_res, - inpainting_padding, - ) - current_image = processed.images[0] - - current_image.paste(prev_image, mask=prev_image) - - # interpolation steps between 2 inpainted images (=sequential zoom and crop) - for j in range(num_interpol_frames - 1): - interpol_image = current_image - - interpol_width = round( - ( - 1 - - (1 - 2 * mask_width / width) - ** (1 - (j + 1) / num_interpol_frames) - ) - * width - / 2 - ) - - interpol_height = round( - ( - 1 - - (1 - 2 * mask_height / height) - ** (1 - (j + 1) / num_interpol_frames) - ) - * height - / 2 - ) - - interpol_image = interpol_image.crop( - ( - interpol_width, - interpol_height, - width - interpol_width, - height - interpol_height, - ) - ) - - interpol_image = interpol_image.resize((width, height)) - - # paste the higher resolution previous image in the middle to avoid drop in quality caused by zooming - interpol_width2 = round( - (1 - (width - 2 * mask_width) / (width - 2 * interpol_width)) - / 2 - * width - ) - - interpol_height2 = round( - (1 - (height - 2 * mask_height) / (height - 2 * interpol_height)) - / 2 - * height - ) - - prev_image_fix_crop = shrink_and_paste_on_blank( - prev_image_fix, interpol_width2, interpol_height2 - ) - - interpol_image.paste(prev_image_fix_crop, mask=prev_image_fix_crop) - - if upscale_do and progress: - progress(((i + 1) / num_outpainting_steps), desc="upscaling interpol") - - all_frames.append( - do_upscaleImg(interpol_image, upscale_do, upscaler_name, upscale_by) - if upscale_do - else interpol_image - ) - - if upscale_do and progress: - progress(((i + 1) / num_outpainting_steps), desc="upscaling current") - - all_frames.append( - do_upscaleImg(current_image, upscale_do, upscaler_name, upscale_by) - if upscale_do - else current_image - ) - - video_file_name = "infinite_zoom_" + str(int(time.time())) + ".mp4" - output_path = shared.opts.data.get( - "infzoom_outpath", shared.opts.data.get("outdir_img2img_samples") - ) - save_path = os.path.join( - output_path, shared.opts.data.get("infzoom_outSUBpath", "infinite-zooms") - ) - if not os.path.exists(save_path): - os.makedirs(save_path) - out = os.path.join(save_path, video_file_name) - write_video( - out, - all_frames, - video_frame_rate, - video_zoom_mode, - int(video_start_frame_dupe_amount), - int(video_last_frame_dupe_amount), - ) - - return ( - out, - processed.images, - processed.js(), - plaintext_to_html(processed.info), - plaintext_to_html(""), - ) - - -def validatePromptJson_throws(data): - with open(jsonprompt_schemafile, "r") as s: - schema = json.load(s) - validate(instance=data, schema=schema) - - -def putPrompts(files): - try: - with open(files.name, "r") as f: - file_contents = f.read() - data = json.loads(file_contents) - validatePromptJson_throws(data) - return [ - gr.DataFrame.update(data["prompts"]), - gr.Textbox.update(data["negPrompt"]), - ] - - except Exception: - gr.Error( - "loading your prompt failed. It seems to be invalid. Your prompt table is preserved." - ) - print( - "[InfiniteZoom:] Loading your prompt failed. It seems to be invalid. Your prompt table is preserved." - ) - return [gr.DataFrame.update(), gr.Textbox.update()] - - -def clearPrompts(): - return [ - gr.DataFrame.update(value=[[0, "Infinite Zoom. Start over"]]), - gr.Textbox.update(""), - ] - - -def on_ui_tabs(): - with gr.Blocks(analytics_enabled=False) as infinite_zoom_interface: - gr.HTML( - """ -

- GitHub Repo - Discord server -

- - """ - ) - with gr.Row(): - generate_btn = gr.Button(value="Generate video", variant="primary") - interrupt = gr.Button(value="Interrupt", elem_id="interrupt_training") - with gr.Row(): - with gr.Column(scale=1, variant="panel"): - with gr.Tab("Main"): - main_outpaint_steps = gr.Slider( - minimum=2, - maximum=100, - step=1, - value=8, - label="Total Outpaint Steps", - info="The more it is, the longer your videos will be", - ) - - # safe reading json prompt - pr = shared.opts.data.get("infzoom_defPrompt", default_prompt) - if not pr: - pr = empty_prompt - - try: - jpr = json.loads(pr) - validatePromptJson_throws(jpr) - except Exception: - jpr = invalid_prompt - - main_prompts = gr.Dataframe( - type="array", - headers=["outpaint step", "prompt"], - datatype=["number", "str"], - row_count=1, - col_count=(2, "fixed"), - value=jpr["prompts"], - wrap=True, - ) - - main_negative_prompt = gr.Textbox( - value=jpr["negPrompt"], label="Negative Prompt" - ) - - # these button will be moved using JS unde the dataframe view as small ones - exportPrompts_button = gr.Button( - value="Export prompts", - variant="secondary", - elem_classes="sm infzoom_tab_butt", - elem_id="infzoom_exP_butt", - ) - importPrompts_button = gr.UploadButton( - label="Import prompts", - variant="secondary", - elem_classes="sm infzoom_tab_butt", - elem_id="infzoom_imP_butt", - ) - exportPrompts_button.click( - None, - _js="exportPrompts", - inputs=[main_prompts, main_negative_prompt], - outputs=None, - ) - importPrompts_button.upload( - fn=putPrompts, - outputs=[main_prompts, main_negative_prompt], - inputs=[importPrompts_button], - ) - - clearPrompts_button = gr.Button( - value="Clear prompts", - variant="secondary", - elem_classes="sm infzoom_tab_butt", - elem_id="infzoom_clP_butt", - ) - clearPrompts_button.click( - fn=clearPrompts, - inputs=[], - outputs=[main_prompts, main_negative_prompt], - ) - - main_sampler = gr.Dropdown( - label="Sampler", - choices=available_samplers, - value="Euler a", - type="value", - ) - with gr.Row(): - main_width = gr.Slider( - minimum=16, - maximum=2048, - value=shared.opts.data.get("infzoom_outsizeW", 512), - step=16, - label="Output Width", - ) - main_height = gr.Slider( - minimum=16, - maximum=2048, - value=shared.opts.data.get("infzoom_outsizeH", 512), - step=16, - label="Output Height", - ) - with gr.Row(): - main_guidance_scale = gr.Slider( - minimum=0.1, - maximum=15, - step=0.1, - value=7, - label="Guidance Scale", - ) - sampling_step = gr.Slider( - minimum=1, - maximum=100, - step=1, - value=50, - label="Sampling Steps for each outpaint", - ) - with gr.Row(): - init_image = gr.Image(type="pil", label="custom initial image") - exit_image = gr.Image( - type="pil", label="custom exit image", visible=False - ) # TODO: implement exit-image rendering - - batchcount_slider = gr.Slider( - minimum=1, - maximum=25, - value=shared.opts.data.get("infzoom_batchcount", 1), - step=1, - label="Batch Count", - ) - with gr.Tab("Video"): - video_frame_rate = gr.Slider( - label="Frames per second", - value=30, - minimum=1, - maximum=60, - ) - video_zoom_mode = gr.Radio( - label="Zoom mode", - choices=["Zoom-out", "Zoom-in"], - value="Zoom-out", - type="index", - ) - video_start_frame_dupe_amount = gr.Slider( - label="number of start frame dupe", - info="Frames to freeze at the start of the video", - value=0, - minimum=1, - maximum=60, - ) - video_last_frame_dupe_amount = gr.Slider( - label="number of last frame dupe", - info="Frames to freeze at the end of the video", - value=0, - minimum=1, - maximum=60, - ) - video_zoom_speed = gr.Slider( - label="Zoom Speed", - value=1.0, - minimum=0.1, - maximum=20.0, - step=0.1, - info="Zoom speed in seconds (higher values create slower zoom)", - ) - - with gr.Tab("Outpaint"): - inpainting_denoising_strength = gr.Slider( - label="Denoising Strength", minimum=0.75, maximum=1, value=1 - ) - inpainting_mask_blur = gr.Slider( - label="Mask Blur", minimum=0, maximum=64, value=0 - ) - inpainting_fill_mode = gr.Radio( - label="Masked content", - choices=["fill", "original", "latent noise", "latent nothing"], - value="latent noise", - type="index", - ) - inpainting_full_res = gr.Checkbox(label="Inpaint Full Resolution") - inpainting_padding = gr.Slider( - label="masked padding", minimum=0, maximum=256, value=0 - ) - - with gr.Tab("Post proccess"): - upscale_do = gr.Checkbox(False, label="Enable Upscale") - upscaler_name = gr.Dropdown( - label="Upscaler", - elem_id="infZ_upscaler", - choices=[x.name for x in shared.sd_upscalers], - value=shared.sd_upscalers[0].name, - ) - - upscale_by = gr.Slider( - label="Upscale by factor", minimum=1, maximum=8, value=1 - ) - with gr.Accordion("Help", open=False): - gr.Markdown( - """# Performance critical -Depending on amount of frames and which upscaler you choose it might took a long time to render. -Our best experience and trade-off is the R-ERSGAn4x upscaler. -""" - ) - - with gr.Column(scale=1, variant="compact"): - output_video = gr.Video(label="Output").style(width=512, height=512) - ( - out_image, - generation_info, - html_info, - html_log, - ) = create_output_panel( - "infinite-zoom", shared.opts.outdir_img2img_samples - ) - generate_btn.click( - fn=wrap_gradio_gpu_call(create_zoom, extra_outputs=[None, "", ""]), - inputs=[ - main_prompts, - main_negative_prompt, - main_outpaint_steps, - main_guidance_scale, - sampling_step, - init_image, - exit_image, - video_frame_rate, - video_zoom_mode, - video_start_frame_dupe_amount, - video_last_frame_dupe_amount, - inpainting_denoising_strength, - inpainting_mask_blur, - inpainting_fill_mode, - inpainting_full_res, - inpainting_padding, - video_zoom_speed, - main_width, - main_height, - batchcount_slider, - main_sampler, - upscale_do, - upscaler_name, - upscale_by, - ], - outputs=[output_video, out_image, generation_info, html_info, html_log], - ) - interrupt.click(fn=lambda: shared.state.interrupt(), inputs=[], outputs=[]) - infinite_zoom_interface.queue() - return [(infinite_zoom_interface, "Infinite Zoom", "iz_interface")] - - -def on_ui_settings(): - section = ("infinite-zoom", "Infinite Zoom") - - shared.opts.add_option( - "outputs" - "infzoom_outpath", - shared.OptionInfo( - "", - "Path where to store your infinite video. Default is Outputs", - gr.Textbox, - {"interactive": True}, - section=section, - ), - ) - - shared.opts.add_option( - "infzoom_outSUBpath", - shared.OptionInfo( - "infinite-zooms", - "Which subfolder name to be created in the outpath. Default is 'infinite-zooms'", - gr.Textbox, - {"interactive": True}, - section=section, - ), - ) - - shared.opts.add_option( - "infzoom_outsizeW", - shared.OptionInfo( - 512, - "Default width of your video", - gr.Slider, - {"minimum": 16, "maximum": 2048, "step": 16}, - section=section, - ), - ) - - shared.opts.add_option( - "infzoom_outsizeH", - shared.OptionInfo( - 512, - "Default height your video", - gr.Slider, - {"minimum": 16, "maximum": 2048, "step": 16}, - section=section, - ), - ) - - shared.opts.add_option( - "infzoom_ffprobepath", - shared.OptionInfo( - "", - "Writing videos has dependency to an existing FFPROBE executable on your machine. D/L here (https://github.com/BtbN/FFmpeg-Builds/releases) your OS variant and point to your installation path", - gr.Textbox, - {"interactive": True}, - section=section, - ), - ) - - shared.opts.add_option( - "infzoom_txt2img_model", - shared.OptionInfo( - None, - "Name of your desired model to render keyframes (txt2img)", - gr.Dropdown, - lambda: {"choices": shared.list_checkpoint_tiles()}, - section=section, - ), - ) - - shared.opts.add_option( - "infzoom_inpainting_model", - shared.OptionInfo( - None, - "Name of your desired inpaint model (img2img-inpaint). Default is vanilla sd-v1-5-inpainting.ckpt ", - gr.Dropdown, - lambda: {"choices": shared.list_checkpoint_tiles()}, - section=section, - ), - ) - - shared.opts.add_option( - "infzoom_defPrompt", - shared.OptionInfo( - default_prompt, - "Default prompt-setup to start with'", - gr.Code, - {"interactive": True, "language": "json"}, - section=section, - ), - ) - - +from modules import script_callbacks +from iz_helpers.ui import on_ui_tabs +from iz_helpers.settings import on_ui_settings script_callbacks.on_ui_tabs(on_ui_tabs) -script_callbacks.on_ui_settings(on_ui_settings) +script_callbacks.on_ui_settings(on_ui_settings) \ No newline at end of file diff --git a/scripts/promptschema.json b/scripts/promptschema.json deleted file mode 100644 index b7fcfd4..0000000 --- a/scripts/promptschema.json +++ /dev/null @@ -1,49 +0,0 @@ -{ - "$schema": "http://json-schema.org/draft-07/schema#", - "type": "object", - "properties": { - "prompts": { - "type": "object", - "properties": { - "data": { - "type": "array", - "items": { - "type": "array", - "items": [ - { - "oneOf": [ - { - "type": "integer", - "minimum": 0 - }, - { - "type": "string" - } - ] - }, - { - "type": "string" - } - ], - "minItems": 0, - "maxItems": 999, - "uniqueItems": false - }, - "minItems": 0 - }, - "headers": { - "type": "array", - "items": { - "type": "string" - }, - "minItems": 2 - } - }, - "required": ["data", "headers"] - }, - "negPrompt": { - "type": "string" - } - }, - "required": ["prompts", "negPrompt"] - } \ No newline at end of file