Skip to content

Commit

Permalink
Merge pull request #30 from jhogsett/misc-fixes
Browse files Browse the repository at this point in the history
A few small fixes
  • Loading branch information
jhogsett committed Jun 15, 2023
2 parents 9308605 + 790c9e2 commit 849fa02
Show file tree
Hide file tree
Showing 12 changed files with 124 additions and 43 deletions.
11 changes: 6 additions & 5 deletions create_ui.py
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ def create_ui(config : SimpleConfig,
restart_fn : Callable):
"""Construct the Gradio Blocks UI"""

app_header = gr.HTML(SimpleIcons.CLAPPER + "EMA-VFI Web UI", elem_id="appheading")
app_header = gr.HTML(SimpleIcons.APP_SYMBOL + "EMA-VFI Web UI", elem_id="appheading")
sep = '  •  '
footer = (SimpleIcons.COPYRIGHT + ' 2023 J. Hogsett' +
sep + '<a href="https://github.com/jhogsett/EMA-VFI-WebUI">Github</a>' +
Expand Down Expand Up @@ -61,14 +61,15 @@ def create_ui(config : SimpleConfig,
PNGtoMP4(config, engine, log.log).render_tab()
GIFtoPNG(config, engine, log.log).render_tab()
PNGtoGIF(config, engine, log.log).render_tab()
SimplifyPngFiles(config, engine, log.log).render_tab()
DedupeFrames(config, engine, log.log).render_tab()
ResequenceFiles(config, engine, log.log).render_tab()
SimplifyPngFiles(config, engine, log.log).render_tab()
ChangeFPS(config, engine, log.log).render_tab()
UpscaleFrames(config, engine, log.log).render_tab()
Resources(config, engine, log.log).render_tab()
Options(config, engine, log.log, restart_fn).render_tab()
LogViewer(config, engine, log.log, log).render_tab()
with gr.Tab(SimpleIcons.APP_SYMBOL + "Application"):
Options(config, engine, log.log, restart_fn).render_tab()
Resources(config, engine, log.log).render_tab()
LogViewer(config, engine, log.log, log).render_tab()
if config.user_interface["show_header"]:
app_footer.render()
return app
29 changes: 29 additions & 0 deletions guide/video_blender_new_project.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
**Video Blender New Project** - Automated setup of a new Video Blender project

## How To Use

1. Set _New Project Name_ to a unique name for the project
1. Set _New Project Path_ to a directory to contain the project files
1. Set _Frame Rate_ to the frame rate of the video being restored
1. Ensure _Split MP4 to PNG Frames Set_ is **checked**
1. Set _MP4 Path_ to a path on this server to the MP4 file
- This step creates a set of _source frames_ from the source video
- _Tip: other file types will work such as:_ `.mpg`, `.mov`, _and_ `.wmv`
1. Ensure _Resynthesize Repair Frames Set_ is **checked**
- This step creates an interpolated set of _repair frames_ that can be used for restoring damaged frames
1. Ensure _Init Restored Set from Source_ is **checked**
- This step creates a set of _working frames_ that are modified by _Frame Chooser_ and _Frame Fixer_
- These frames become the final restored video
1. Ensure _Sync Frame Numbers Across Sets_ is **checked**
- This step ensures that frame numbers shown in the _Frame Chooser_ UI match the frame indexes of the source PNG frame files on disk
- Also the _Clean PNG Files_ feature is used to simplify the PNG frame files
1. Click _Create New Project_
1. When done, return to the _Project Settings_ tab to open the new project

## Note
- This process could be slow, perhaps many hours long!
- Progress is shown in the console using standard progress bars
- The browser window does NOT need to be left open

## Important
- `ffmpeg.exe` must be available on the system path
14 changes: 14 additions & 0 deletions guide/video_blender_reset_project.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
**Video Blender New Project** - Reset a Video Blender project

## How To Use

1. Choose an existing project
1. Click _Reset Project_

## Note
- This process uses the _New Project_ tab to selectively recreate parts of a _Video Blender_ project
- **The default use case is:** wipe out any edits made to the _working frames_ set and restore them from _source frames_

## Important
- The `edits.csv` file is not deleted by _Reset Project_
- The zeroth frame files are not deleted from _source frames_ and _working frames_ when _Reset Project_ is used
7 changes: 4 additions & 3 deletions tabs/mp4_to_png_ui.py
Original file line number Diff line number Diff line change
Expand Up @@ -27,8 +27,9 @@ def render_tab(self):
output_path_text_mp = gr.Text(max_lines=1, label="PNG Files Path",
placeholder="Path on this server to a directory for the converted PNG files")
with gr.Row():
output_pattern_text_mp = gr.Text(max_lines=1, label="Output Filename Pattern",
placeholder="Pattern like image%03d.png")
output_pattern_text_mp = gr.Text(max_lines=1,
label="Output Filename Pattern (leave blank for auto-detection)",
placeholder="Example: 'image%09d.png'")
input_frame_rate_mp = gr.Slider(minimum=1, maximum=60, value=frame_rate,
step=1, label="Frame Rate")
with gr.Row():
Expand All @@ -46,7 +47,7 @@ def convert_mp4_to_png(self,
frame_rate : int,
output_path: str):
"""Convert button handler"""
if input_filepath and output_pattern and output_path:
if input_filepath and output_path:
create_directory(output_path)
ffmpeg_cmd = _MP4toPNG(input_filepath, output_pattern, int(frame_rate), output_path)
return gr.update(value=ffmpeg_cmd, visible=True)
8 changes: 4 additions & 4 deletions tabs/png_to_gif_ui.py
Original file line number Diff line number Diff line change
Expand Up @@ -27,11 +27,11 @@ def render_tab(self):
output_path_text_pg = gr.Text(max_lines=1, label="GIF File",
placeholder="Path and filename on this server for the converted GIF file")
with gr.Row():
input_pattern_text_pg = gr.Text(max_lines=1, label="Input Filename Pattern",
placeholder="Pattern like image%03d.png (auto=automatic pattern)")
input_pattern_text_pg = gr.Text(max_lines=1,
label="Input Filename Pattern (leave blank for auto-detect)",
placeholder="Example: 'image%09d.png'")
framerate_pg = gr.Slider(value=30, minimum=1, maximum=240, step=1,
label="GIF Frame Rate")

with gr.Row():
convert_button_pg = gr.Button("Convert", variant="primary")
output_info_text_pg = gr.Textbox(label="Details", interactive=False)
Expand All @@ -47,7 +47,7 @@ def convert_png_to_gif(self,
output_filepath : str,
frame_rate : int):
"""Convert button handler"""
if input_path and input_pattern and output_filepath:
if input_path and output_filepath:
directory, _, _ = split_filepath(output_filepath)
create_directory(directory)
ffmpeg_cmd = _PNGtoGIF(input_path, input_pattern, output_filepath, frame_rate)
Expand Down
7 changes: 4 additions & 3 deletions tabs/png_to_mp4_ui.py
Original file line number Diff line number Diff line change
Expand Up @@ -30,8 +30,9 @@ def render_tab(self):
output_path_text_pm = gr.Text(max_lines=1, label="MP4 File",
placeholder="Path and filename on this server for the converted MP4 file")
with gr.Row():
input_pattern_text_pm = gr.Text(max_lines=1, label="Input Filename Pattern",
placeholder="Pattern like image%03d.png (auto=automatic pattern)")
input_pattern_text_pm = gr.Text(max_lines=1,
label="Input Filename Pattern (leave blank for auto-detection)",
placeholder="Example: 'image%09d.png'")
input_frame_rate_pm = gr.Slider(minimum=1, maximum=60, value=frame_rate,
step=1, label="Frame Rate")
quality_slider_pm = gr.Slider(minimum=minimum_crf, maximum=maximum_crf,
Expand All @@ -52,7 +53,7 @@ def convert_png_to_mp4(self,
output_filepath: str,
quality : str):
"""Convert button handler"""
if input_path and input_pattern and output_filepath:
if input_path and output_filepath:
directory, _, _ = split_filepath(output_filepath)
create_directory(directory)
ffmpeg_cmd = _PNGtoMP4(input_path, input_pattern, int(frame_rate), output_filepath,
Expand Down
17 changes: 14 additions & 3 deletions tabs/upscale_frames_ui.py
Original file line number Diff line number Diff line change
Expand Up @@ -34,14 +34,25 @@ def render_tab(self):
with gr.Row():
scale_input = gr.Slider(value=4.0, minimum=1.0, maximum=8.0, step=0.05,
label="Frame Upscale Factor")
use_tiling = gr.Radio(label="Use Tiling", choices=["Auto (Tile If Needed)", "No (Best Quality)", "Yes (For Low VRAM)"], value="Auto (Tile If Needed)")
upscale_button = gr.Button("Upscale Frames", variant="primary")
use_tiling = gr.Radio(label="Use Tiling",
choices=[
"Auto (Tile If Needed)",
"No (Best Quality)",
"Yes (For Low VRAM)"],
value="Auto (Tile If Needed)")
gr.Markdown("*Progress can be tracked in the console*")
upscale_button = gr.Button("Upscale Frames " + SimpleIcons.SLOW_SYMBOL,
variant="primary")
with gr.Accordion(SimpleIcons.TIPS_SYMBOL + " Guide", open=False):
WebuiTips.upscale_frames.render()
upscale_button.click(self.upscale_frames,
inputs=[input_path_text, output_path_text, scale_input, use_tiling])

def upscale_frames(self, input_path : str, output_path : str | None, upscale_factor : float, use_tiling : str):
def upscale_frames(self,
input_path : str,
output_path : str | None,
upscale_factor : float,
use_tiling : str):
"""Upscale Frames button handler"""
if input_path:
model_name = self.config.realesrgan_settings["model_name"]
Expand Down
18 changes: 13 additions & 5 deletions tabs/video_blender_ui.py
Original file line number Diff line number Diff line change
Expand Up @@ -196,7 +196,6 @@ def render_tab(self):
new_project_frame_rate = gr.Slider(minimum=1, maximum=60,
value=frame_rate, step=1,
label="Frame Rate")

with gr.Row():
with gr.Column(variant="compact"):
gr.HTML("Check Applicable Setup Steps")
Expand Down Expand Up @@ -234,15 +233,21 @@ def render_tab(self):
gr.Markdown("*Progress can be tracked in the console*")
new_project_button = gr.Button("Create New Project " + SimpleIcons.SLOW_SYMBOL,
variant="primary")
with gr.Accordion(SimpleIcons.TIPS_SYMBOL + " Guide", open=False):
WebuiTips.video_blender_new_project.render()

### RESET PROJECT
with gr.Tab(SimpleIcons.RECYCLE + "Reset Project", id=5):
with gr.Row():
choices = self.video_blender_projects.get_project_names()
reset_project_dropdown = gr.Dropdown(label=SimpleIcons.PROP_SYMBOL +
" Projects", choices=choices, value=choices[0])
gr.Markdown(
"*Reset Project uses the New Project tab to selectively revert parts of a project*")
with gr.Row():
reset_project_button = gr.Button("Reset Project", variant="primary")
with gr.Accordion(SimpleIcons.TIPS_SYMBOL + " Guide", open=False):
WebuiTips.video_blender_reset_project.render()

projects_dropdown_vb.change(self.video_blender_choose_project,
inputs=[projects_dropdown_vb],
Expand Down Expand Up @@ -309,7 +314,8 @@ def render_tab(self):
fix_frames_count],
show_progress=False)
fix_frames_button_vb.click(self.video_blender_fix_frames,
inputs=[input_project_path_vb, fix_frames_last_before, fix_frames_first_after],
inputs=[input_project_path_vb, fix_frames_count, fix_frames_last_before,
fix_frames_first_after],
outputs=[tabs_video_blender, project_path_ff, input_clean_before_ff,
input_clean_after_ff, fixed_path_ff])
preview_button_ff.click(self.video_blender_preview_fixed,
Expand Down Expand Up @@ -449,10 +455,12 @@ def video_blender_compute_fix_frames(self, frame : str, damage_count : str):
return last_clean_before, first_clean_after, damage_count
return 0, 0, 0

def video_blender_fix_frames(self, project_path : str, last_frame_before : float,
first_frame_after : float):
def video_blender_fix_frames(self, project_path : str, damage_count: int,
last_frame_before : int, first_frame_after : int):
"""Fix Frames button handler"""
return gr.update(selected=2), project_path, last_frame_before, first_frame_after, None
if damage_count > 0:
return gr.update(selected=2), project_path, last_frame_before, first_frame_after, None
return gr.update(selected=1), None, 0, 0, None

def video_blender_preview_fixed(self,
project_path : str,
Expand Down
6 changes: 6 additions & 0 deletions webui_tips.py
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,12 @@ def load_markdown(path : str, name : str):
video_blender_video_preview = gr.Markdown(load_markdown(
tips_path,
"video_blender_video_preview"))
video_blender_new_project = gr.Markdown(load_markdown(
tips_path,
"video_blender_new_project"))
video_blender_reset_project = gr.Markdown(load_markdown(
tips_path,
"video_blender_reset_project"))
mp4_to_png = gr.Markdown(load_markdown(tips_path, "mp4_to_png"))
png_to_mp4 = gr.Markdown(load_markdown(tips_path, "png_to_mp4"))
gif_to_png = gr.Markdown(load_markdown(tips_path, "gif_to_png"))
Expand Down
8 changes: 5 additions & 3 deletions webui_utils/simple_icons.py
Original file line number Diff line number Diff line change
Expand Up @@ -90,12 +90,14 @@ class SimpleIcons:
TWO_HEARTS = "💕"

# shared symbol definitions
TIPS_SYMBOL = QUESTION
SLOW_SYMBOL = WATCH
PROP_SYMBOL = PROPERTIES
APP_SYMBOL = CLAPPER
CONV_SYMBOL = STILL
PROP_SYMBOL = PROPERTIES
SLOW_SYMBOL = WATCH
TIPS_SYMBOL = QUESTION

SYMBOLS = [
APP_SYMBOL,
CONV_SYMBOL,
PROP_SYMBOL,
SLOW_SYMBOL,
Expand Down
2 changes: 1 addition & 1 deletion webui_utils/test_simple_icons.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
from .simple_icons import *

GOOD_EXAMPLES = [
(SimpleIcons.SYMBOLS, 4, 6),
(SimpleIcons.SYMBOLS, 5, 6),
(SimpleIcons.APP_ICONS, 35, 51)]

def test_SimpleIcons():
Expand Down
40 changes: 24 additions & 16 deletions webui_utils/video_utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,28 +12,35 @@
QUALITY_SMALLER_SIZE = 28
QUALITY_DEFAULT = 23

def determine_pattern(input_path : str):
"""Determine the FFmpeg wildcard pattern needed for a set of files"""
files = sorted(glob.glob(os.path.join(input_path, "*.png")))
def determine_input_pattern(png_files_path : str) -> str:
"""Determine the FFmpeg wildcard pattern needed to read a set of PNG files"""
files = sorted(glob.glob(os.path.join(png_files_path, "*.png")))
first_file = files[0]
file_count = len(files)
num_width = len(str(file_count))
_, name_part, ext_part = split_filepath(first_file)
return f"{name_part[:-num_width]}%0{num_width}d{ext_part}"

def determine_output_pattern(mp4_file_path : str) -> str:
"""Determine the FFmpeg wildcard pattern needed to write a set of PNG files"""
frame_count = get_frame_count(mp4_file_path)
num_width = len(str(frame_count))
_, filename, _ = split_filepath(mp4_file_path)
return f"{filename}%0{num_width}d.png"

def PNGtoMP4(input_path : str, # pylint: disable=invalid-name
filename_pattern : str,
frame_rate : int,
output_filepath : str,
crf : int = QUALITY_DEFAULT):
"""Encapsulate logic for the PNG Sequence to MP4 feature"""
# if filename_pattern is "auto" it uses the filename of the first found file
# if filename_pattern is empty it uses the filename of the first found file
# and the count of file to determine the pattern, .png as the file type
# ffmpeg -framerate 60 -i .\upscaled_frames%05d.png -c:v libx264 -r 60 -pix_fmt yuv420p
# -crf 28 test.mp4 if filename_pattern == "auto":
filename_pattern = determine_pattern(input_path)
# -crf 28 test.mp4
pattern = filename_pattern or determine_input_pattern(input_path)
ffcmd = FFmpeg(
inputs= {os.path.join(input_path, filename_pattern) : f"-framerate {frame_rate}"},
inputs= {os.path.join(input_path, pattern) : f"-framerate {frame_rate}"},
outputs={output_filepath : f"-c:v libx264 -r {frame_rate} -pix_fmt yuv420p -crf {crf}"},
global_options="-y")
cmd = ffcmd.cmd
Expand All @@ -48,8 +55,9 @@ def MP4toPNG(input_path : str, # pylint: disable=invalid-name
output_path : str,
start_number : int = 0):
"""Encapsulate logic for the MP4 to PNG Sequence feature"""
pattern = filename_pattern or determine_output_pattern(input_path)
ffcmd = FFmpeg(inputs= {input_path : None},
outputs={os.path.join(output_path, filename_pattern) :
outputs={os.path.join(output_path, pattern) :
f"-filter:v fps={frame_rate} -start_number {start_number}"},
global_options="-y")
cmd = ffcmd.cmd
Expand All @@ -66,7 +74,7 @@ def PNGtoPalette(input_path : str, # pylint: disable=invalid-name
output_filepath : str):
"""Create a palette from a set of PNG files to feed into animated GIF creation"""
if filename_pattern == "auto":
filename_pattern = determine_pattern(input_path)
filename_pattern = determine_input_pattern(input_path)
ffcmd = FFmpeg(inputs= {os.path.join(input_path, filename_pattern) : None},
outputs={output_filepath : "-vf palettegen"},
global_options="-y")
Expand All @@ -79,18 +87,17 @@ def PNGtoGIF(input_path : str, # pylint: disable=invalid-name
output_filepath : str,
frame_rate : int):
"""Encapsulates logic for the PNG sequence to GIF feature"""
# if filename_pattern is "auto" it uses the filename of the first found file
# if filename_pattern is empty it uses the filename of the first found file
# and the count of file to determine the pattern, .png as the file type
# ffmpeg -i gifframes_%02d.png -i palette.png -lavfi paletteuse video.gif
# ffmpeg -framerate 3 -i image%01d.png video.gif
if filename_pattern == "auto":
filename_pattern = determine_pattern(input_path)
pattern = filename_pattern or determine_input_pattern(input_path)
output_path, base_filename, _ = split_filepath(output_filepath)
palette_filepath = os.path.join(output_path, base_filename + "-palette.png")
palette_cmd = PNGtoPalette(input_path, filename_pattern, palette_filepath)
palette_cmd = PNGtoPalette(input_path, pattern, palette_filepath)

ffcmd = FFmpeg(inputs= {
os.path.join(input_path, filename_pattern) : f"-framerate {frame_rate}",
os.path.join(input_path, pattern) : f"-framerate {frame_rate}",
palette_filepath : None},
outputs={output_filepath : "-lavfi paletteuse"},
global_options="-y")
Expand Down Expand Up @@ -129,7 +136,7 @@ def deduplicate_frames(input_path : str,
# ffmpeg -i "C:\CONTENT\ODDS\odds%04d.png"
# -vf mpdecimate=hi=2047:lo=2047:frac=1:max=0,setpts=N/FRAME_RATE/TB
# -start_number 0 "C:\CONTENT\TEST\odds%04d.png"
filename_pattern = determine_pattern(input_path)
filename_pattern = determine_input_pattern(input_path)
input_sequence = os.path.join(input_path, filename_pattern)
output_sequence = os.path.join(output_path, filename_pattern)
filter = f"mpdecimate=hi={threshold}:lo={threshold}:frac=1:max=0,setpts=N/FRAME_RATE/TB"
Expand All @@ -146,9 +153,10 @@ def get_frame_count(input_path : str) -> int:
# ffprobe.exe -v quiet -count_frames -show_entries stream=nb_read_frames -print_format default=nokey=1:noprint_wrappers=1 file.mp4
# 1763
ffcmd = FFprobe(inputs= {input_path :
"-count_frames -show_entries stream=nb_read_frames" +
" -select_streams v -count_frames -show_entries stream=nb_read_frames" +
" -print_format default=nokey=1:noprint_wrappers=1"},
global_options="-v quiet")
print(ffcmd.cmd)
result = ffcmd.run(stdout=subprocess.PIPE)
stdout = result[0].decode("UTF-8").strip()
return int(stdout)
Expand Down

0 comments on commit 849fa02

Please sign in to comment.