diff --git a/README.md b/README.md index 097f78d..7774f52 100644 --- a/README.md +++ b/README.md @@ -179,7 +179,7 @@ There are still two steps away from a really smooth and high resolution animatio #### setup once -⚪ auto install (for Windows) +⚪ auto install (Windows) - run `cd tools & install.cmd` - trouble shooting @@ -187,12 +187,12 @@ There are still two steps away from a really smooth and high resolution animatio - if you got SSL errors about `curl schannel ... Unknown error ... certificate.`, the downloader not work due to some SSL security reasons, just turn to install manually... - you will have four components: [Busybox](https://frippery.org/busybox/), [Real-ESRGAN](https://github.com/xinntao/Real-ESRGAN-ncnn-vulkan), [RIFE](https://github.com/nihui/rife-ncnn-vulkan) and [FFmpeg](https://ffmpeg.org/) installed under the [tools](tools) folder -⚪ manually install (for Windows/Linux/Mac) +⚪ manually install (Windows/Linux/Mac) ℹ Understand the `tools` folder layout first => [tools/README.txt](tools/README.txt) ℹ If you indeed wanna put the tools elsewhere, modify paths in [tools/link.cmd](tools/link.cmd) and run `cd tools & link.cmd` 😉 -For **Windows**: +For Windows: - download [Busybox](https://frippery.org/busybox/) - download [Real-ESRGAN](https://github.com/xinntao/Real-ESRGAN/releases) (e.g.: `realesrgan-ncnn-vulkan-20220424-windows.zip`) @@ -200,7 +200,7 @@ For **Windows**: - download [rife-ncnn-vulkan](https://github.com/nihui/rife-ncnn-vulkan/releases) bundle (e.g.: `rife-ncnn-vulkan-20221029-windows.zip `) - download [FFmpeg](https://ffmpeg.org/download.html) binary (e.g.: `ffmpeg-release-full-shared.7z` or `ffmpeg-git-full.7z`) -For **Linux/Mac**: +For Linux/Mac: - download [Real-ESRGAN](https://github.com/xinntao/Real-ESRGAN/releases) and [rife-ncnn-vulkan](https://github.com/nihui/rife-ncnn-vulkan/releases), put them according to the `tools` folder layout, manually apply `chmod 755` to the executables - `ffmpeg` can be easily found in your app store or package manager, run like `apt install ffmpeg`; DO NOT need to link it under `tools` folder @@ -208,18 +208,23 @@ For **Linux/Mac**: #### run each time -⚪ tkinter GUI (for Windows/Linux/Mac) +⚪ tkinter GUI (Windows/Linux/Mac) -![postprocessor](img/postprocessor-gui.png) +![manager](img/manager.png) -- start webui's python venv - - for Windows: run `cmd_here.cmd` - - for Linux/Mac: run `../../venv/Scripts/activate` -- run `pip install -r requirements.txt` (only setup once) -- run `python postprocessor.py`, or for Windows just run the [DOSKEY](https://learn.microsoft.com/en-us/windows-server/administration/windows-commands/doskey) `pp` -- find usage help message in right click pop menu to start +For Windows: + - run `manager.cmd`, to start webui's python venv + - run the [DOSKEY](https://learn.microsoft.com/en-us/windows-server/administration/windows-commands/doskey) `install` (only setup once) + - run the DOSKEY `run` -⚪ cmd script (for Windows) +For Linux/Mac: + - run `../../venv/Scripts/activate`, to start webui's python venv + - run `pip install -r requirements.txt` (only setup once) + - run `python manager.py` + +ℹ find usage help message in right click pop menu~ + +⚪ cmd script (Windows) - deprecated - check params in [postprocess-config.cmd](postprocess-config.cmd) - pick one way to start 😃 diff --git a/img/postprocessor-gui.png b/img/manager.png similarity index 100% rename from img/postprocessor-gui.png rename to img/manager.png diff --git a/cmd_here.cmd b/manager.cmd similarity index 58% rename from cmd_here.cmd rename to manager.cmd index cc885b4..cfc2811 100644 --- a/cmd_here.cmd +++ b/manager.cmd @@ -15,9 +15,17 @@ SET PY_BIN=python.exe %PY_BIN% --version > NUL IF ERRORLEVEL 1 GOTO die -DOSKEY pp=python postprocessor.py +DOSKEY run=python manager.py +DOSKEY install=pip install -r requirements.txt + +CMD /K activate.bat ^& ^ + ECHO VENV_PATH: %VENV_PATH% ^& ^ + %PY_BIN% --version ^& ^ + ECHO. ^& ^ + ECHO Commands shortcuts: ^& ^ + ECHO run start ptravel manager ^& ^ + ECHO install install requirements.txt -CMD /K activate.bat ^& ECHO VENV_PATH: %VENV_PATH% ^& %PY_BIN% --version GOTO EOF diff --git a/postprocessor.py b/manager.py similarity index 80% rename from postprocessor.py rename to manager.py index 46fcf9b..7a68dd9 100644 --- a/postprocessor.py +++ b/manager.py @@ -22,6 +22,8 @@ import tkinter.messagebox as tkmsg import tkinter.filedialog as tkfdlg from traceback import print_exc, format_exc +__version__ = '0.1' + BASE_PATH = Path(__file__).absolute().parent WEBUI_PATH = BASE_PATH.parent.parent OUTPUT_PATH = WEBUI_PATH / 'outputs' @@ -41,9 +43,19 @@ RESR_MODELS = { 'realesrgan-x4plus': [4], } RIFE_MODELS = [ + 'rife', + 'rife-anime', + 'rife-HD', + 'rife-UHD', + 'rife-v2', + 'rife-v2.3', + 'rife-v2.4', + 'rife-v3.0', + 'rife-v3.1', 'rife-v4', + 'rife-v4.6', ] -EXPORT_FMT = [ +EXPORT_FMT = [ 'mp4', 'gif', 'webm', @@ -75,14 +87,24 @@ def run_resr(model:str, ratio:int, in_dp:Path, out_dp:Path) -> bool: out_dp.mkdir(exist_ok=True) if model == 'realesr-animevideov3': model = f'realesr-animevideov3-x{ratio}' - return run_cmd(f'realesrgan-ncnn-vulkan -v -s {ratio} -n {model} -i "{sanitize_pathname(in_dp)}" -o "{sanitize_pathname(out_dp)}"') + safe_out_dp = sanitize_pathname(out_dp) + ok = run_cmd(f'realesrgan-ncnn-vulkan -v -s {ratio} -n {model} -i "{sanitize_pathname(in_dp)}" -o "{safe_out_dp}"') + + # NOTE: fix case of Embryo mode + embryo_fp: Path = out_dp / 'embryo.png' + if embryo_fp.exists(): embryo_fp.unlink() + + return ok def run_rife(model:str, interp:int, in_dp:Path, out_dp:Path) -> bool: if out_dp.exists(): shutil.rmtree(str(out_dp)) out_dp.mkdir(exist_ok=True) - if interp > 0: interp *= len(list(in_dp.iterdir())) - return run_cmd(f'rife-ncnn-vulkan -v -n {interp} -m {model} -i "{sanitize_pathname(in_dp)}" -o "{sanitize_pathname(out_dp)}"') + if model == 'rife-v4': + if interp > 0: interp *= len(list(in_dp.iterdir())) + return run_cmd(f'rife-ncnn-vulkan -v -n {interp} -m {model} -i "{sanitize_pathname(in_dp)}" -o "{sanitize_pathname(out_dp)}"') + else: + return run_cmd(f'rife-ncnn-vulkan -v -m {model} -i "{sanitize_pathname(in_dp)}" -o "{sanitize_pathname(out_dp)}"') def run_ffmpeg(fps:float, fmt:str, in_dp:Path, out_dp:Path) -> bool: out_fp = out_dp / f'synth.{fmt}' @@ -90,11 +112,16 @@ def run_ffmpeg(fps:float, fmt:str, in_dp:Path, out_dp:Path) -> bool: if fmt == 'gif': return run_cmd(f'ffmpeg -y -framerate {fps} -i "{sanitize_pathname(in_dp / r"%08d.png")}" "{sanitize_pathname(out_fp)}"') - else: - return run_cmd(f'ffmpeg -y -framerate {fps} -i "{sanitize_pathname(in_dp / r"%08d.png")}" -crf 20 -c:v libx264 -pix_fmt yuv420p "{sanitize_pathname(out_fp)}"') + if fmt == 'mp4': + return run_cmd(f'ffmpeg -y -framerate {fps} -i "{sanitize_pathname(in_dp / r"%08d.png")}" -crf 30 -c:v libx264 -pix_fmt yuv420p "{sanitize_pathname(out_fp)}"') + if fmt == 'webm': + # -c:v libvpx/libvpx-vp9/libaom-av1 (VP8/VP9/AV1) + # -b:v 0/1M + # -crf 15~30 + return run_cmd(f'ffmpeg -y -framerate {fps} -i "{sanitize_pathname(in_dp / r"%08d.png")}" -crf 30 -c:v libvpx-vp9 -pix_fmt yuv420p "{sanitize_pathname(out_fp)}"') -WINDOW_TITLE = 'Postprocessor Pipeline GUI' +WINDOW_TITLE = f'Prompt Travel Manager v{__version__}' WINDOW_SIZE = (700, 660) IMAGE_SIZE = 512 LIST_HEIGHT = 100 @@ -106,10 +133,14 @@ MEMINFO_REFRESH = 16 # refresh status memory info every k-image loads HELP_INFO = ''' [Settings] resr: model_name, upscale_ratio + - only realesr-animevideov3 supports custom upscale_ratio + - others are forced x4 rife: model_name, interp_ratio (NOT frame count!!) + - only rife-v4 supports custom interp_ratio + - others are forced x2 ffmpeg: export_format, export_fps -The check boxes are enable swicthes specifying to run or not. +The checkboxes are enable switches specifying to run or not :) ''' @@ -119,7 +150,7 @@ class App: self.setup_gui() self.is_running = False - self.cur_name = None # str + self.cur_name = None # str, current travel id self.cache = {} # { 'name': [Image|Path] } self.p = psutil.Process(os.getpid()) @@ -148,7 +179,8 @@ class App: # menu menu = tk.Menu(wnd, tearoff=0) - menu.add_command(label='Open folder...', command=self._ls_open_dir) + menu.add_command(label='Open folder...', command=self._menu_open_dir) + menu.add_command(label='Remove folder', command=self._menu_remove_dir) menu.add_separator() menu.add_command(label='Memory cache clean', command=self.mem_clear) menu.add_command(label='Help', command=lambda: tkmsg.showinfo('Help', HELP_INFO)) @@ -201,11 +233,17 @@ class App: cb_r = ttk.Combobox(frm2111, text='ratio', values=[], textvariable=self.var_resr_r, state='readonly', width=COMBOX_WIDTH1) cb_m.grid(row=0, column=0, padx=2) cb_r.grid(row=0, column=1, padx=2) + self.cb_resr = cb_r + def _cb_r_update(): values = RESR_MODELS[self.var_resr_m.get()] cb_r.config(values=values) if self.var_resr_r.get() not in values: self.var_resr_r.set(values[0]) + if len(values) == 1: + self.cb_resr.config(state=tk.DISABLED) + else: + self.cb_resr.config(state=tk.NORMAL) cb_m.bind('<>', lambda evt: _cb_r_update()) _cb_r_update() @@ -216,6 +254,16 @@ class App: et = ttk.Entry(frm2112, text='ratio', textvariable=self.var_rife_r, width=ENTRY_WIDTH) cb.grid(row=0, column=0, padx=2) et.grid(row=0, column=1, padx=2) + self.et_rife = et + + def _et_update(): + if self.var_rife_m.get() != 'rife-v4': + self.var_rife_r.set(2) + self.et_rife.config(state=tk.DISABLED) + else: + self.et_rife.config(state=tk.NORMAL) + cb.bind('<>', lambda evt: _et_update()) + _et_update() frm2113 = ttk.LabelFrame(frm211, text='FFmpeg') frm2113.pack(expand=tk.YES, fill=tk.X) @@ -275,6 +323,30 @@ class App: sc.pack(anchor=tk.S, expand=tk.YES, fill=tk.X) self.sc = sc + def _menu_open_dir(self): + try: startfile(Path(self.var_root_dp.get()) / self.cur_name) + except: print_exc() + + def _menu_remove_dir(self): + idx: tuple = self.ls.curselection() + if not idx: return + name = self.ls.get(idx) + if name is None: return + + dp = Path(self.var_root_dp.get()) / name + if name in self.cache: + cnt = len(self.cache[name]) + else: + cnt = len([fp for fp in dp.iterdir() if fp.suffix.lower() in ['.png', '.jpg', '.jpeg']]) + + if not tkmsg.askyesno('Remove', f'Confirm to remove folder "{name}" with {cnt} images?'): + return + + try: + shutil.rmtree(str(dp)) + self.ls.delete(idx) + except: print_exc() + def _mem_info_str(self, title='Mem'): mem = self.p.memory_info() return f'[{title}] rss: {mem.rss//2**20:.3f} MB, vms: {mem.vms//2**20:.3f} MB' @@ -289,7 +361,12 @@ class App: info2 = self._mem_info_str('After') tkmsg.showinfo('Meminfo', info1 + '\n' + info2) + self.cnt_pv_load = 0 + self.var_status.set(self._mem_info_str()) + def open_(self, root_dp:Path=None): + ''' Open a new travel root folder ''' + if root_dp is None: root_dp = tkfdlg.askdirectory(initialdir=str(OUTPUT_PATH)) if not root_dp: return @@ -311,6 +388,8 @@ class App: self._ls_change() def _ls_change(self): + ''' Open a new travel id folder ''' + idx: tuple = self.ls.curselection() if not idx: return name = self.ls.get(idx) @@ -329,11 +408,9 @@ class App: self.var_fps_ip.set(0) self._pv_change() - def _ls_open_dir(self): - try: startfile(Path(self.var_root_dp.get()) / self.cur_name) - except: print_exc() - def _pv_change(self, evt=None): + ''' Load a travel frame ''' + if not self.cur_name: return cache = self.cache[self.cur_name] @@ -385,20 +462,21 @@ class App: try: self.is_running = True self.btn.config(state=tk.DISABLED, text='Running...') - + if var_resr: assert run_resr(var_resr_m, var_resr_r, base_dp, base_dp / 'resr') - # NOTE: fix case of Embryo mode - embryo_fp: Path = base_dp / 'resr' / 'embryo.png' - if embryo_fp.exists(): embryo_fp.unlink() - if var_rife: assert run_rife(var_rife_m, var_rife_r, base_dp / 'resr', base_dp / 'rife') - + if var_ffmpeg: - assert run_ffmpeg(var_ffmpeg_r, var_ffmpeg_f, base_dp / 'rife', base_dp) - + dp: Path = base_dp / 'rife' + if dp.exists(): + assert run_ffmpeg(var_ffmpeg_r, var_ffmpeg_f, base_dp / 'rife', base_dp) + else: + if tkmsg.askyesno('Warn', 'rife results not found, try synth from resr results?'): + assert run_ffmpeg(var_ffmpeg_r, var_ffmpeg_f, base_dp / 'resr', base_dp) + print(f'[Task] done ({time() - t:3f}s)') r = tkmsg.askyesno('Ok', 'Task done! Open output folder?') if r: startfile(base_dp)