-
Notifications
You must be signed in to change notification settings - Fork 2
Expand file tree
/
Copy pathvideo.py
More file actions
355 lines (291 loc) · 13.4 KB
/
video.py
File metadata and controls
355 lines (291 loc) · 13.4 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
from misc import *
from databox import DataBox
class Video:
@classmethod
def errorCheckFile(cls, path):
with open(path, 'r') as f:
begin_settings_index, end_settings_index, begin_data_index, end_data_index = cls.getFileIndexes(path)
lf = list(f)
for i in range(begin_settings_index,end_settings_index+1):
if lf[i] != "\n": # Make sure not new line
if "=" not in lf[i] or len(lf[i].split("=")) != 2:
raise ValueError("Error in file at " + path + " at line " + str(i+1))
for i in range(begin_data_index,end_data_index+1):
if lf[i] != "\n": # Make sure not new line
row = lf[i].split("|")
if len(row) != 4:
raise ValueError("Error in file at " + path + " at line " + str(i+1))
else:
for val in row:
if "=" not in val or len(val.split("=")) != 2:
raise ValueError("Error in file at " + path + " at line " + str(i+1))
return
@classmethod
def getFileIndexes(cls, path):
with open(path, 'r') as f:
lf = list(f)
end_data_index = len(lf)-1
begin_settings_index = 0
begin_data_index = 0
end_settings_index = 0
for i in range(len(lf)):
if "||BEGIN_DATA||" in lf[i]:
begin_data_index = i+1
end_settings_index = i-1
return [begin_settings_index, end_settings_index, begin_data_index, end_data_index]
@classmethod
def fromFile(cls, path):
debugMessage("Parsing data file at " + path)
cls.errorCheckFile(path)
begin_settings_index, end_settings_index, begin_data_index, end_data_index = cls.getFileIndexes(path)
with open(path, 'r') as f:
lf = list(f)
lf = [line.replace("\n", "") for line in lf] # remove new line chars & new lines in general
settings_rows = lf[begin_settings_index:end_settings_index+1]
data_rows = lf[begin_data_index:end_data_index+1]
settings = {setting[0]: setting[1] for setting in [setting_string.split('=') for setting_string in settings_rows if setting_string != ""]} # Parse into dict of key-value pairs, 1 line = 1 pair, remove new lines
data = [{data_setting.split('=')[0]: data_setting.split('=')[1] for data_setting in row.split('|')} for row in data_rows if row != ""] # Same for data, but 1 line = 1 dict, ^^^^^^^^^^^^^^^^
for d in data: # Replace blank values with defaults from misc.py for settings, data
for k,v in d.items():
if d[k] == '':
d[k] = DEFAULTS[k]
for k in settings.keys():
if settings[k] == '':
settings[k] = DEFAULTS[k]
settings['data_box_height'] = settings['height']
settings['data_box_width'] = DataBox.calculateDataBoxWidth(settings['width'])
data_settings = getPairsInList(settings, ['data_box_width', 'data_box_height', 'data_value', 'unit', 'unit_place', 'prefix', 'suffix', 'data_title', 'color', 'bg_light_color', 'bg_color'])
video_settings = getPairsInList(settings, ['width', 'height', 'title', 'music', 'out_path', 'gap_color'])
data_boxes = [DataBox(**data_settings, **data[i], position=len(data)-i) for i in range(len(data))] # Convert data row strings to DataBox classes
video = cls(**video_settings, data_boxes=data_boxes) # Unpack settings, convert to video class
return video
def __init__(self, width, height, title, music, data_boxes, out_path, gap_color):
self.width = int(width)
self.height = int(height)
self.raw_title = self.generateRawTitle(title) # For filename, etc.
self.music = music
self.data_boxes = data_boxes # Boxes used in video
self.out_path = self.setupDirectoryStructure(out_path) # Path to output assets and video to
self.gap_color = getColorTuple(gap_color)
self.image = None # Eventual video image
self.image_pixels = None # Pixels of image
self.frames = [] # Array used to store frames for video
self.clip = None # Video clip to be used
self.dimensions = [width, height]
self.title = self.generateTitle(title) # For video upload / 'pretty title'
# self.rankDataBoxes()
def generateRawTitle(self, title):
debugMessage("Generating raw title")
new_title = title
for forbidden in "\\/:*?\"<>|":
new_title.replace(forbidden, "_")
return new_title.lower().replace(" ", "_")
def generateTitle(self, title):
debugMessage("Generating main title")
return f"Data Comparison: {title} | {CHANNEL_NAME}" # Constant from misc file
def previewDataBoxes(self):
print("== PREVIEWING DATA BOXES ==\n")
for data_box in self.data_boxes:
print(f"Previewing {data_box.data_string}")
ok = False
while not ok:
data_box.savePreviewImage(path=os.path.join(self.out_path, "preview_databox.png"))
# Checks if already icon, asks to pull new icon for ease of access (most likely change)
if data_box.data_image_type == "icon":
new_icon = input("Pull new icon? [y/n]: ").strip().lower()[0]
if new_icon == "y":
# If there are icons to pull from, change and restart preview
if len(data_box.icon_urls) > 0:
data_box.popDataIcon()
continue
else:
print("No more icons to try. Change the icon query or image type in the next step.")
# Checks if good, handling bad inputs
good = input("Data box is good? [y/n]: ").strip().lower()[0]
while good != "y" and good != "n":
good = input("Please enter a valid value: ").strip().lower()[0]
# If user likes data box, exits preview
if good == "y":
ok = True
else:
# Used to store new values, defaults to existing value
new_values = {
'prefix': data_box.prefix,
'data_value': data_box.data_value,
'unit': data_box.unit,
'unit_place': data_box.unit_place,
'suffix': data_box.suffix,
'data_title': data_box.data_title,
'image_type': '',
'color': data_box.color,
'bg_light_color': data_box.bg_light_color,
'bg_color': data_box.bg_color
}
#Information included in the change prompt
new_value_settings = {
'prefix': '',
'data_value': '',
'unit': '',
'unit_place': 'before/after',
'suffix': '',
'data_title': '',
'image_type': 'file/icon',
'color': 'BG color for data value, hex/RGB',
'bg_light_color': 'color for title bar, hex/RGB',
'bg_color': 'color for image section, hex/RGB'
}
# Values to change
to_change = input("""Enter the changes you would like to make, separating multiple changes with a comma
[prefix,data_value,unit,unit_place,suffix,data_title,image_type,color,bg_color,bg_light_color]: """)
to_change = to_change.strip().lower().split(",")
#Ask for new values based on input above
for changed in to_change:
new_values[changed] = input(f"Enter new {changed} [{new_value_settings[changed]}]: ")
#Set data box values based on inputs
data_box.prefix = new_values['prefix']
data_box.data_value = new_values['data_value']
data_box.unit = new_values['unit']
data_box.unit_place = new_values['unit_place']
data_box.suffix = new_values['suffix']
data_box.data_title = new_values['data_title']
data_box.color = new_values['color']
data_box.bg_light_color = new_values['bg_light_color']
data_box.bg_color = new_values['bg_color']
if new_values['image_type'] != "":
#If file, change type to file and ask for path
if new_values['image_type'] == "file":
data_box.setDataImageType("file")
file_path = input("Enter path to image: ")
data_box.data_image = file_path
#If icon, change type to icon and ask for queries
elif new_values['image_type'] == "icon":
data_box.setDataImageType("icon")
query = input("Enter icon querie(s), comma-separated: ")
data_box.data_image = query
data_box.getDataIcon()
data_box.generateImage()
os.remove(os.path.join(self.out_path, "preview_databox.png"))
def outputDataBoxes(self, out_dir):
if not os.path.exists(out_dir):
os.mkdir(out_dir)
debugMessage("Writing data boxes to files")
for i in range(len(self.data_boxes)):
debugMessage("Writing data box to " + os.path.join(out_dir, f"{self.raw_title}_data_box_{i}.png"))
path = os.path.abspath(os.path.join(out_dir, f"{self.raw_title}_data_box_{i}.png"))
if os.path.exists(path):
os.remove(path)
self.data_boxes[i].outputImage(path)
def outputDataBoxFromIndex(self, index, out_dir):
debugMessage("Outputting data box to " + out_dir + " at index " + index)
path = os.path.abspath(os.path.join(out_dir, f"{self.raw_title}_databox_{index}.png"))
if os.path.exists(path):
os.remove(path)
self.data_boxes[index].outputImage(path)
def generateVideoImageBase(self):
debugMessage("Generating video image base")
# Generates base of image used to create video frames
size = ( int( ( len(self.data_boxes) * (DATA_BOX_PERCENTAGE*self.width) ) + ( (len(self.data_boxes)+1) * (GAP_PERCENTAGE*self.width) ) ) , int(self.height) )
img = Image.new("RGBA",size,getColorComplement(self.gap_color, shift=-25))
return img
def fillVideoImage(self, img):
debugMessage("Filling video image")
# Place data box images on the video image
gap = int(GAP_PERCENTAGE*self.width)
x = gap
for i in range(len(self.data_boxes)):
img.paste(self.data_boxes[i].image, (x,0))
x += self.data_boxes[i].data_box_width + gap
return img
def generateVideoImage(self):
debugMessage("Generating video image base")
self.image = self.generateVideoImageBase()
self.image = self.fillVideoImage(self.image)
return self.image
def outputVideoImage(self, out_path):
debugMessage("Outputting video image to " + out_path)
self.image.save(os.path.abspath(out_path))
def generateVideoFrames(self):
debugMessage("Generating video frames")
# Generates the frames used to create the video
self.image_pixels = np.array(self.image.getdata()) # Gets pixel values of image
#Resize to rows-columns-colors shape
self.image_pixels = self.image_pixels.reshape( (self.image.size[1], self.image.size[0], 4) )
shape = self.image_pixels.shape
#Markers for sliding window
x1 = 0
x2 = self.width
number = 1
# While right marker not at end
while x2 < shape[1]:
debugMessage("Generating frame " + str(number))
#Slice of image to fit video dimensions, remove alpha component
self.frames.append(self.image_pixels[0:shape[0],x1:x2,:3])
# If the right marker is closer to the end than the ppf value, add remaining pixel value
if shape[1]-x2 < PPF:
x1 += shape[1]-x2
x2 += shape[1]-x2
# Otherwise, shift normally
else:
x1 += PPF
x2 += PPF
number += 1
return self.frames
def setFade(self, direction):
debugMessage(f"Setting {direction}-fade")
if direction == "in":
base_frame = self.frames[0]
self.clip = concatenate_videoclips([ ImageSequenceClip([base_frame for i in range(math.floor(FPS*FADE_TIME))], fps=FPS), self.clip ])
self.clip = self.clip.fx(vfx.fadein, duration=FADE_TIME, initial_color=FADE_COLOR)
self.setMusic()
elif direction == "out":
base_frame = self.frames[len(self.frames)-1]
self.clip = concatenate_videoclips([ self.clip, ImageSequenceClip([base_frame for i in range(math.floor(FPS*FADE_TIME))], fps=FPS) ])
self.clip = self.clip.fx(vfx.fadeout, duration=FADE_TIME, final_color=FADE_COLOR)
self.setMusic()
return self.clip
def generateVideoClipFromFrames(self):
debugMessage("Creating video clip")
self.clip = concatenate_videoclips([ ImageSequenceClip([self.frames[0] for i in range(math.floor(FPS*BEGINNING_WAIT_TIME))], fps=FPS), ImageSequenceClip(self.frames, fps=FPS), ImageSequenceClip([self.frames[len(self.frames)-1] for i in range(math.floor(FPS*END_WAIT_TIME))], fps=FPS) ])
self.setMusic()
return self.clip
def setMusic(self):
if self.music != "":
audio_clip = AudioFileClip(os.path.abspath(self.music))
audio_clip = concatenate_audioclips([audio_clip for i in range(math.ceil(float(self.clip.duration)/float(audio_clip.duration)))])
audio_clip = audio_clip.set_duration(self.clip.duration)
audio_clip = audio_clip.fx(afx.audio_fadein, FADE_TIME).fx(afx.audio_fadeout, FADE_TIME)
self.clip = self.clip.set_audio(audio_clip)
def outputVideo(self):
debugMessage("Outputting video")
if self.clip:
self.clip.write_videofile(os.path.join(self.out_path, "video.mp4"), **WRITE_SETTINGS)
def setupDirectoryStructure(self, out_path):
if not os.path.exists(os.path.join(out_path, "inforank/")):
os.mkdir(os.path.join(out_path, "inforank"))
if not os.path.exists(os.path.join(out_path, "inforank", self.raw_title)):
os.mkdir(os.path.join(out_path, "inforank", self.raw_title))
self.out_path = os.path.join(out_path, "inforank", self.raw_title)
return self.out_path
def generateVideo(self, preview=True):
if preview:
self.previewDataBoxes()
debugMessage("Generating full video")
self.generateVideoImage()
self.generateVideoFrames()
self.generateVideoClipFromFrames()
self.setFade("in")
self.setFade("out")
self.outputDataBoxes(os.path.join(self.out_path, "data_boxes"))
self.outputVideoImage(os.path.join(self.out_path, "video_image.png"))
self.outputVideo()
self.outputVideoInformation()
def outputVideoInformation(self):
info = self.title + "\n\n"
for i in range(len(self.data_boxes)):
info += f"=== {self.data_boxes[i].data_string} ===\n"
info += f"{self.data_boxes[i].prefix}\n{self.data_boxes[i].data_value}\n{self.data_boxes[i].suffix}\n{self.data_boxes[i].data_title}\n{self.data_boxes[i].icon_urls[0]}\n\n"
for j in range(1, len(self.data_boxes[i].icon_urls)):
info += f"{self.data_boxes[i].icon_urls[j]}\n"
info += "\n=== ===\n\n"
with open(os.path.join(self.out_path, "info.txt"), "w+") as f:
f.write(info)