Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

A few small fixes #30

Merged
merged 9 commits into from
Jun 15, 2023
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