Skip to content

Commit f8c7371

Browse files
authored
Merge pull request #7 from Sxela/v0.3.0
V0.3.0
2 parents aeae7e0 + 60fd528 commit f8c7371

File tree

7 files changed

+8836
-17
lines changed

7 files changed

+8836
-17
lines changed

README.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,3 +44,6 @@ Save frame to a folder, using current frame number.
4444

4545
## MixConsistencyMaps
4646
Mix consistency maps, blur, dilate.
47+
48+
## RenderVideo
49+
Trigger output video render at a given frame

frame_nodes.py

Lines changed: 41 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44
import torch
55
import numpy as np
66
import folder_paths
7-
from .frame_utils import FrameDataset, StylizedFrameDataset, get_size
7+
from .frame_utils import FrameDataset, StylizedFrameDataset, get_size, save_video
88

99
class LoadFrameSequence:
1010
@classmethod
@@ -264,14 +264,49 @@ def INPUT_TYPES(self):
264264

265265
def save_img(self, image, output_dir, batch_name, frame_number):
266266
os.makedirs(output_dir, exist_ok=True)
267-
fname = f'{batch_name}_{frame_number}.png'
267+
fname = f'{batch_name}_{frame_number:06}.png'
268268
out_fname = os.path.join(output_dir, fname)
269269
print('image.shape', image.shape, image.max(), image.min())
270270
image = (image[0].clip(0,1)*255.).cpu().numpy().astype('uint8')
271271
image = Image.fromarray(image)
272272
image.save(out_fname)
273273
print('fname', out_fname)
274274
return ()
275+
276+
class RenderVideo:
277+
@classmethod
278+
def INPUT_TYPES(self):
279+
return {"required":
280+
{
281+
"output_dir": ("STRING", {"multiline": True,
282+
"default":''}),
283+
"frames_input_dir": ("STRING", {"multiline": True,
284+
"default":''}),
285+
"batch_name": ("STRING", {"default":'ComfyWarp'}),
286+
"first_frame":("INT",{"default": 0, "min": 0, "max": 9999999999}),
287+
"last_frame":("INT",{"default": -1, "min": -1, "max": 9999999999}),
288+
"render_at_frame":("INT",{"default": 0, "min": 0, "max": 9999999999}),
289+
"current_frame":("INT",{"default": 0, "min": 0, "max": 9999999999}),
290+
"fps":("FLOAT",{"default": 24, "min": 0, "max": 9999999999}),
291+
"output_format":(["h264_mp4", "qtrle_mov", "prores_mov"],),
292+
"use_deflicker": ("BOOLEAN", {"default": False})
293+
}
294+
}
295+
296+
CATEGORY = "WarpFusion"
297+
298+
RETURN_TYPES = ()
299+
FUNCTION = "export_video"
300+
OUTPUT_NODE = True
301+
302+
def export_video(self, output_dir, frames_input_dir, batch_name, first_frame=1, last_frame=-1,
303+
render_at_frame=999999, current_frame=0, fps=30, output_format='h264_mp4', use_deflicker=False):
304+
if current_frame>=render_at_frame:
305+
print('Exporting video.')
306+
save_video(indir=frames_input_dir, video_out=output_dir, batch_name=batch_name, start_frame=first_frame,
307+
last_frame=last_frame, fps=fps, output_format=output_format, use_deflicker=use_deflicker)
308+
return ()
309+
275310

276311
NODE_CLASS_MAPPINGS = {
277312
"LoadFrameSequence": LoadFrameSequence,
@@ -281,7 +316,8 @@ def save_img(self, image, output_dir, batch_name, frame_number):
281316
"LoadFramePairFromDataset":LoadFramePairFromDataset,
282317
"LoadFrameFromFolder":LoadFrameFromFolder,
283318
"ResizeToFit":ResizeToFit,
284-
"SaveFrame":SaveFrame
319+
"SaveFrame":SaveFrame,
320+
"RenderVideo": RenderVideo
285321
}
286322

287323
NODE_DISPLAY_NAME_MAPPINGS = {
@@ -292,5 +328,6 @@ def save_img(self, image, output_dir, batch_name, frame_number):
292328
"LoadFramePairFromDataset":"Load Frame Pair From Dataset",
293329
"LoadFrameFromFolder": "Maybe Load Frame From Folder",
294330
"ResizeToFit":"Resize To Fit",
295-
"SaveFrame":"SaveFrame"
331+
"SaveFrame":"SaveFrame",
332+
"RenderVideo": "RenderVideo"
296333
}

frame_utils.py

Lines changed: 106 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,13 @@
11
#(c) Alex Spirin 2023
22

3-
import hashlib, os, sys, glob, subprocess, pathlib
4-
3+
import hashlib, os, sys, glob, subprocess, pathlib, platform
4+
import zipfile
5+
from shutil import copy
6+
import requests
57

68
def generate_file_hash(input_file):
9+
"""Generates has for video vile based on name, size, creation time
10+
"""
711
# Get file name and metadata
812
file_name = os.path.basename(input_file)
913
file_size = os.path.getsize(input_file)
@@ -50,7 +54,7 @@ def __init__(self, source_path, outdir_prefix='', videoframes_root='', update_on
5054
if len(glob.glob(source_path))>0:
5155
self.frame_paths = sorted(glob.glob(source_path))
5256
else:
53-
raise Exception(f'Frame source for {outdir_prefix} not found at {source_path}\nPlease specify an existing source path.')
57+
raise FileNotFoundError(f'Frame source for {outdir_prefix} not found at {source_path}\nPlease specify an existing source path.')
5458
if os.path.exists(source_path):
5559
if os.path.isfile(source_path):
5660
if os.path.splitext(source_path)[1][1:].lower() in image_extenstions:
@@ -63,11 +67,11 @@ def __init__(self, source_path, outdir_prefix='', videoframes_root='', update_on
6367
self.frame_paths = glob.glob(os.path.join(out_path, '*.*'))
6468
self.source_path = out_path
6569
if len(self.frame_paths)<1:
66-
raise Exception(f'Couldn`t extract frames from {source_path}\nPlease specify an existing source path.')
70+
raise FileNotFoundError(f'Couldn`t extract frames from {source_path}\nPlease specify an existing source path.')
6771
elif os.path.isdir(source_path):
6872
self.frame_paths = glob.glob(os.path.join(source_path, '*.*'))
6973
if len(self.frame_paths)<1:
70-
raise Exception(f'Found 0 frames in {source_path}\nPlease specify an existing source path.')
74+
raise FileNotFoundError(f'Found 0 frames in {source_path}\nPlease specify an existing source path.')
7175
extensions = []
7276
if self.frame_paths is not None:
7377
for f in self.frame_paths:
@@ -81,7 +85,7 @@ def __init__(self, source_path, outdir_prefix='', videoframes_root='', update_on
8185

8286
self.frame_paths = sorted(self.frame_paths)
8387

84-
else: raise Exception(f'Frame source for {outdir_prefix} not found at {source_path}\nPlease specify an existing source path.')
88+
else: raise FileNotFoundError(f'Frame source for {outdir_prefix} not found at {source_path}\nPlease specify an existing source path.')
8589
print(f'Found {len(self.frame_paths)} frames at {source_path}')
8690

8791
def __getitem__(self, idx):
@@ -100,10 +104,10 @@ def __init__(self, source_path):
100104
self.frame_paths = None
101105
self.source_path = source_path
102106
if not os.path.exists(source_path):
103-
raise Exception(f'Frame source not found at {source_path}\nPlease specify an existing source path.')
107+
raise FileNotFoundError(f'Frame source not found at {source_path}\nPlease specify an existing source path.')
104108
if os.path.exists(source_path):
105109
if os.path.isfile(source_path):
106-
raise Exception(f'{source_path} is a file. Please specify path to a folder.')
110+
raise NotADirectoryError(f'{source_path} is a file. Please specify path to a folder.')
107111
elif os.path.isdir(source_path):
108112
self.frame_paths = glob.glob(os.path.join(source_path, '*.*'))
109113

@@ -123,8 +127,97 @@ def get_size(size, max_size, divisible_by=8):
123127
divisible_by = int(divisible_by)
124128
x,y = size
125129
max_dim = max(size)
126-
if max_dim>max_size:
127-
ratio = max_size/max_dim
128-
new_size = ((int(x*ratio)//divisible_by*divisible_by),(int(y*ratio)//divisible_by*divisible_by))
129-
return new_size
130-
return size
130+
# if max_dim>max_size:
131+
ratio = max_size/max_dim
132+
new_size = ((int(x*ratio)//divisible_by*divisible_by),(int(y*ratio)//divisible_by*divisible_by))
133+
return new_size
134+
# return size
135+
136+
def find_ffmpeg(start_dir):
137+
files = glob.glob(f"{start_dir}/**/*.*", recursive=True)
138+
for f in files:
139+
if platform.system() == 'Linux':
140+
if f.endswith('ffmpeg'): return f
141+
elif f.endswith('ffmpeg.exe'): return f
142+
return None
143+
144+
def download_ffmpeg(start_dir):
145+
ffmpeg_url = 'https://github.com/GyanD/codexffmpeg/releases/download/6.0/ffmpeg-6.0-full_build.zip'
146+
print('ffmpeg.exe not found, downloading...')
147+
r = requests.get(ffmpeg_url, allow_redirects=True, timeout=60)
148+
print('downloaded, extracting')
149+
open('ffmpeg-6.0-full_build.zip', 'wb').write(r.content)
150+
151+
with zipfile.ZipFile('ffmpeg-6.0-full_build.zip', 'r') as zip_ref:
152+
zip_ref.extractall(f'{start_dir}/')
153+
154+
copy(f'{start_dir}/ffmpeg-6.0-full_build/bin/ffmpeg.exe', f'{start_dir}/')
155+
return f'{start_dir}/ffmpeg.exe'
156+
157+
158+
def get_ffmpeg():
159+
start_dir = os.getcwd()
160+
ffmpeg_path = find_ffmpeg(start_dir)
161+
if ffmpeg_path is None or not os.path.exists(ffmpeg_path):
162+
ffmpeg_path = download_ffmpeg(start_dir)
163+
return ffmpeg_path
164+
165+
def save_video(indir, video_out, batch_name='', start_frame=1, last_frame=-1, fps=30, output_format='h264_mp4', use_deflicker=False):
166+
ffmpeg_path = get_ffmpeg()
167+
print('Found ffmpeg at: ', ffmpeg_path)
168+
os.makedirs(video_out, exist_ok=True)
169+
indir = indir.replace('\\','/')
170+
image_path = f"{indir}/{batch_name}_%06d.png"
171+
172+
postfix = ''
173+
if use_deflicker:
174+
postfix+='_dfl'
175+
176+
indir_stem = indir.replace('\\','/').split('/')[-1]
177+
out_filepath = f"{video_out}/{indir_stem}_{postfix}.{output_format.split('_')[-1]}"
178+
if last_frame == -1:
179+
last_frame = len(glob.glob(f"{indir}/*.png"))
180+
181+
cmd = [ffmpeg_path,
182+
'-y',
183+
'-vcodec',
184+
'png',
185+
'-r',
186+
str(fps),
187+
'-start_number',
188+
str(start_frame),
189+
'-i',
190+
image_path,
191+
'-frames:v',
192+
str(last_frame+1),
193+
'-c:v']
194+
195+
if output_format == 'h264_mp4':
196+
cmd+=['libx264',
197+
'-pix_fmt',
198+
'yuv420p']
199+
elif output_format == 'qtrle_mov':
200+
cmd+=['qtrle',
201+
'-vf',
202+
f'fps={fps}']
203+
elif output_format == 'prores_mov':
204+
cmd+=['prores_aw',
205+
'-profile:v',
206+
'2',
207+
'-pix_fmt',
208+
'yuv422p10',
209+
'-vf',
210+
f'fps={fps}']
211+
212+
if use_deflicker:
213+
cmd+=['-vf','deflicker=mode=pm:size=10']
214+
cmd+=[out_filepath]
215+
216+
process = subprocess.Popen(cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
217+
stdout, stderr = process.communicate()
218+
if process.returncode != 0:
219+
print(stderr)
220+
raise RuntimeError(stderr)
221+
else:
222+
print(f"The video is ready and saved to {out_filepath}")
223+
return 0

0 commit comments

Comments
 (0)