auto-editor 24.29.1__py3-none-any.whl → 24.31.1__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- auto_editor/__init__.py +2 -2
- auto_editor/analyze.py +143 -132
- auto_editor/edit.py +14 -21
- auto_editor/lang/palet.py +12 -16
- auto_editor/lib/data_structs.py +2 -0
- auto_editor/make_layers.py +3 -6
- auto_editor/output.py +5 -5
- auto_editor/preview.py +2 -3
- auto_editor/render/subtitle.py +1 -1
- auto_editor/render/video.py +8 -18
- auto_editor/subcommands/levels.py +18 -17
- auto_editor/subcommands/repl.py +3 -12
- auto_editor/subcommands/subdump.py +5 -8
- auto_editor/subcommands/test.py +2 -2
- auto_editor/utils/container.py +71 -313
- {auto_editor-24.29.1.dist-info → auto_editor-24.31.1.dist-info}/METADATA +2 -2
- {auto_editor-24.29.1.dist-info → auto_editor-24.31.1.dist-info}/RECORD +21 -21
- {auto_editor-24.29.1.dist-info → auto_editor-24.31.1.dist-info}/WHEEL +1 -1
- {auto_editor-24.29.1.dist-info → auto_editor-24.31.1.dist-info}/LICENSE +0 -0
- {auto_editor-24.29.1.dist-info → auto_editor-24.31.1.dist-info}/entry_points.txt +0 -0
- {auto_editor-24.29.1.dist-info → auto_editor-24.31.1.dist-info}/top_level.txt +0 -0
auto_editor/__init__.py
CHANGED
@@ -1,2 +1,2 @@
|
|
1
|
-
__version__ = "24.
|
2
|
-
version = "
|
1
|
+
__version__ = "24.31.1"
|
2
|
+
version = "24w31a"
|
auto_editor/analyze.py
CHANGED
@@ -4,23 +4,25 @@ import os
|
|
4
4
|
import re
|
5
5
|
from dataclasses import dataclass
|
6
6
|
from fractions import Fraction
|
7
|
+
from math import ceil
|
7
8
|
from typing import TYPE_CHECKING
|
8
9
|
|
10
|
+
import av
|
9
11
|
import numpy as np
|
12
|
+
from av.audio.fifo import AudioFifo
|
13
|
+
from av.subtitles.subtitle import AssSubtitle
|
10
14
|
|
11
15
|
from auto_editor import version
|
12
16
|
from auto_editor.utils.subtitle_tools import convert_ass_to_text
|
13
|
-
from auto_editor.wavfile import read
|
14
17
|
|
15
18
|
if TYPE_CHECKING:
|
19
|
+
from collections.abc import Iterator
|
16
20
|
from fractions import Fraction
|
17
21
|
from typing import Any
|
18
22
|
|
19
|
-
from av.filter import FilterContext
|
20
23
|
from numpy.typing import NDArray
|
21
24
|
|
22
25
|
from auto_editor.ffwrapper import FileInfo
|
23
|
-
from auto_editor.output import Ensure
|
24
26
|
from auto_editor.utils.bar import Bar
|
25
27
|
from auto_editor.utils.log import Log
|
26
28
|
|
@@ -28,7 +30,6 @@ if TYPE_CHECKING:
|
|
28
30
|
@dataclass(slots=True)
|
29
31
|
class FileSetup:
|
30
32
|
src: FileInfo
|
31
|
-
ensure: Ensure
|
32
33
|
strict: bool
|
33
34
|
tb: Fraction
|
34
35
|
bar: Bar
|
@@ -40,11 +41,6 @@ class LevelError(Exception):
|
|
40
41
|
pass
|
41
42
|
|
42
43
|
|
43
|
-
def link_nodes(*nodes: FilterContext) -> None:
|
44
|
-
for c, n in zip(nodes, nodes[1:]):
|
45
|
-
c.link_to(n)
|
46
|
-
|
47
|
-
|
48
44
|
def mut_remove_small(
|
49
45
|
arr: NDArray[np.bool_], lim: int, replace: int, with_: int
|
50
46
|
) -> None:
|
@@ -92,9 +88,90 @@ def obj_tag(tag: str, tb: Fraction, obj: dict[str, Any]) -> str:
|
|
92
88
|
return key
|
93
89
|
|
94
90
|
|
91
|
+
def iter_audio(src, tb: Fraction, stream: int = 0) -> Iterator[float]:
|
92
|
+
fifo = AudioFifo()
|
93
|
+
try:
|
94
|
+
container = av.open(src.path, "r")
|
95
|
+
audio_stream = container.streams.audio[stream]
|
96
|
+
sample_rate = audio_stream.rate
|
97
|
+
|
98
|
+
exact_size = (1 / tb) * sample_rate
|
99
|
+
accumulated_error = 0
|
100
|
+
|
101
|
+
# Resample so that audio data is between [-1, 1]
|
102
|
+
resampler = av.AudioResampler(
|
103
|
+
av.AudioFormat("flt"), audio_stream.layout, sample_rate
|
104
|
+
)
|
105
|
+
|
106
|
+
for frame in container.decode(audio=stream):
|
107
|
+
frame.pts = None # Skip time checks
|
108
|
+
|
109
|
+
for reframe in resampler.resample(frame):
|
110
|
+
fifo.write(reframe)
|
111
|
+
|
112
|
+
while fifo.samples >= ceil(exact_size):
|
113
|
+
size_with_error = exact_size + accumulated_error
|
114
|
+
current_size = round(size_with_error)
|
115
|
+
accumulated_error = size_with_error - current_size
|
116
|
+
|
117
|
+
audio_chunk = fifo.read(current_size)
|
118
|
+
assert audio_chunk is not None
|
119
|
+
arr = audio_chunk.to_ndarray().flatten()
|
120
|
+
yield float(np.max(np.abs(arr)))
|
121
|
+
|
122
|
+
finally:
|
123
|
+
container.close()
|
124
|
+
|
125
|
+
|
126
|
+
def iter_motion(src, tb, stream: int, blur: int, width: int) -> Iterator[float]:
|
127
|
+
container = av.open(src.path, "r")
|
128
|
+
|
129
|
+
video = container.streams.video[stream]
|
130
|
+
video.thread_type = "AUTO"
|
131
|
+
|
132
|
+
prev_frame = None
|
133
|
+
current_frame = None
|
134
|
+
total_pixels = src.videos[0].width * src.videos[0].height
|
135
|
+
index = 0
|
136
|
+
prev_index = -1
|
137
|
+
|
138
|
+
graph = av.filter.Graph()
|
139
|
+
graph.link_nodes(
|
140
|
+
graph.add_buffer(template=video),
|
141
|
+
graph.add("scale", f"{width}:-1"),
|
142
|
+
graph.add("format", "gray"),
|
143
|
+
graph.add("gblur", f"sigma={blur}"),
|
144
|
+
graph.add("buffersink"),
|
145
|
+
).configure()
|
146
|
+
|
147
|
+
for unframe in container.decode(video):
|
148
|
+
if unframe.pts is None:
|
149
|
+
continue
|
150
|
+
|
151
|
+
graph.push(unframe)
|
152
|
+
frame = graph.pull()
|
153
|
+
assert frame.time is not None
|
154
|
+
index = round(frame.time * tb)
|
155
|
+
|
156
|
+
current_frame = frame.to_ndarray()
|
157
|
+
if prev_frame is None:
|
158
|
+
value = 0.0
|
159
|
+
else:
|
160
|
+
# Use `int16` to avoid underflow with `uint8` datatype
|
161
|
+
diff = np.abs(prev_frame.astype(np.int16) - current_frame.astype(np.int16))
|
162
|
+
value = np.count_nonzero(diff) / total_pixels
|
163
|
+
|
164
|
+
for _ in range(index - prev_index):
|
165
|
+
yield value
|
166
|
+
|
167
|
+
prev_frame = current_frame
|
168
|
+
prev_index = index
|
169
|
+
|
170
|
+
container.close()
|
171
|
+
|
172
|
+
|
95
173
|
@dataclass(slots=True)
|
96
174
|
class Levels:
|
97
|
-
ensure: Ensure
|
98
175
|
src: FileInfo
|
99
176
|
tb: Fraction
|
100
177
|
bar: Bar
|
@@ -107,26 +184,16 @@ class Levels:
|
|
107
184
|
if (arr := self.read_cache("audio", {"stream": 0})) is not None:
|
108
185
|
return len(arr)
|
109
186
|
|
110
|
-
|
111
|
-
|
112
|
-
|
113
|
-
|
114
|
-
samp_per_ticks = sr / self.tb
|
115
|
-
ticks = int(samp_count / samp_per_ticks)
|
116
|
-
self.log.debug(f"Audio Length: {ticks}")
|
117
|
-
self.log.debug(
|
118
|
-
f"... without rounding: {float(samp_count / samp_per_ticks)}"
|
119
|
-
)
|
120
|
-
return ticks
|
187
|
+
result = sum(1 for _ in iter_audio(self.src, self.tb, 0))
|
188
|
+
self.log.debug(f"Audio Length: {result}")
|
189
|
+
return result
|
121
190
|
|
122
191
|
# If there's no audio, get length in video metadata.
|
123
|
-
|
124
|
-
|
125
|
-
with av.open(f"{self.src.path}") as cn:
|
126
|
-
if len(cn.streams.video) < 1:
|
192
|
+
with av.open(self.src.path) as container:
|
193
|
+
if len(container.streams.video) == 0:
|
127
194
|
self.log.error("Could not get media duration")
|
128
195
|
|
129
|
-
video =
|
196
|
+
video = container.streams.video[0]
|
130
197
|
|
131
198
|
if video.duration is None or video.time_base is None:
|
132
199
|
dur = 0
|
@@ -171,56 +238,70 @@ class Levels:
|
|
171
238
|
return arr
|
172
239
|
|
173
240
|
def audio(self, stream: int) -> NDArray[np.float64]:
|
174
|
-
if stream
|
241
|
+
if stream >= len(self.src.audios):
|
175
242
|
raise LevelError(f"audio: audio stream '{stream}' does not exist.")
|
176
243
|
|
177
244
|
if (arr := self.read_cache("audio", {"stream": stream})) is not None:
|
178
245
|
return arr
|
179
246
|
|
180
|
-
|
181
|
-
|
182
|
-
|
183
|
-
|
184
|
-
|
185
|
-
|
186
|
-
|
187
|
-
|
188
|
-
max_volume = get_max_volume(samples)
|
189
|
-
self.log.debug(f"Max volume: {max_volume}")
|
247
|
+
with av.open(self.src.path, "r") as container:
|
248
|
+
audio = container.streams.audio[stream]
|
249
|
+
if audio.duration is not None and audio.time_base is not None:
|
250
|
+
inaccurate_dur = int(audio.duration * audio.time_base * self.tb)
|
251
|
+
elif container.duration is not None:
|
252
|
+
inaccurate_dur = int(container.duration / av.time_base * self.tb)
|
253
|
+
else:
|
254
|
+
inaccurate_dur = 1024
|
190
255
|
|
191
|
-
|
192
|
-
|
256
|
+
bar = self.bar
|
257
|
+
bar.start(inaccurate_dur, "Analyzing audio volume")
|
193
258
|
|
194
|
-
|
195
|
-
|
196
|
-
|
197
|
-
|
198
|
-
|
199
|
-
|
259
|
+
result = np.zeros((inaccurate_dur), dtype=np.float64)
|
260
|
+
index = 0
|
261
|
+
for value in iter_audio(self.src, self.tb, stream):
|
262
|
+
if index > len(result) - 1:
|
263
|
+
result = np.concatenate(
|
264
|
+
(result, np.zeros((len(result)), dtype=np.float64))
|
265
|
+
)
|
266
|
+
result[index] = value
|
267
|
+
bar.tick(index)
|
268
|
+
index += 1
|
200
269
|
|
201
|
-
|
202
|
-
self.
|
203
|
-
f"analyze: audio length: {audio_ticks} ({float(samp_count / samp_per_ticks)})"
|
204
|
-
)
|
205
|
-
self.bar.start(audio_ticks, "Analyzing audio volume")
|
270
|
+
bar.end()
|
271
|
+
return self.cache("audio", {"stream": stream}, result[:index])
|
206
272
|
|
207
|
-
|
273
|
+
def motion(self, stream: int, blur: int, width: int) -> NDArray[np.float64]:
|
274
|
+
if stream >= len(self.src.videos):
|
275
|
+
raise LevelError(f"motion: video stream '{stream}' does not exist.")
|
208
276
|
|
209
|
-
|
210
|
-
|
277
|
+
mobj = {"stream": stream, "width": width, "blur": blur}
|
278
|
+
if (arr := self.read_cache("motion", mobj)) is not None:
|
279
|
+
return arr
|
211
280
|
|
212
|
-
|
213
|
-
|
214
|
-
|
215
|
-
|
281
|
+
with av.open(self.src.path, "r") as container:
|
282
|
+
video = container.streams.video[stream]
|
283
|
+
inaccurate_dur = (
|
284
|
+
1024
|
285
|
+
if video.duration is None or video.time_base is None
|
286
|
+
else int(video.duration * video.time_base * self.tb)
|
287
|
+
)
|
216
288
|
|
217
|
-
|
218
|
-
|
289
|
+
bar = self.bar
|
290
|
+
bar.start(inaccurate_dur, "Analyzing motion")
|
219
291
|
|
220
|
-
|
292
|
+
result = np.zeros((inaccurate_dur), dtype=np.float64)
|
293
|
+
index = 0
|
294
|
+
for value in iter_motion(self.src, self.tb, stream, blur, width):
|
295
|
+
if index > len(result) - 1:
|
296
|
+
result = np.concatenate(
|
297
|
+
(result, np.zeros((len(result)), dtype=np.float64))
|
298
|
+
)
|
299
|
+
result[index] = value
|
300
|
+
bar.tick(index)
|
301
|
+
index += 1
|
221
302
|
|
222
|
-
|
223
|
-
return self.cache("
|
303
|
+
bar.end()
|
304
|
+
return self.cache("motion", mobj, result[:index])
|
224
305
|
|
225
306
|
def subtitle(
|
226
307
|
self,
|
@@ -238,9 +319,6 @@ class Levels:
|
|
238
319
|
except re.error as e:
|
239
320
|
self.log.error(e)
|
240
321
|
|
241
|
-
import av
|
242
|
-
from av.subtitles.subtitle import AssSubtitle, TextSubtitle
|
243
|
-
|
244
322
|
try:
|
245
323
|
container = av.open(self.src.path, "r")
|
246
324
|
subtitle_stream = container.streams.subtitles[stream]
|
@@ -287,8 +365,6 @@ class Levels:
|
|
287
365
|
for sub in subset:
|
288
366
|
if isinstance(sub, AssSubtitle):
|
289
367
|
line = convert_ass_to_text(sub.ass.decode(errors="ignore"))
|
290
|
-
elif isinstance(sub, TextSubtitle):
|
291
|
-
line = sub.text.decode(errors="ignore")
|
292
368
|
else:
|
293
369
|
continue
|
294
370
|
|
@@ -299,68 +375,3 @@ class Levels:
|
|
299
375
|
container.close()
|
300
376
|
|
301
377
|
return result
|
302
|
-
|
303
|
-
def motion(self, stream: int, blur: int, width: int) -> NDArray[np.float64]:
|
304
|
-
import av
|
305
|
-
|
306
|
-
if stream >= len(self.src.videos):
|
307
|
-
raise LevelError(f"motion: video stream '{stream}' does not exist.")
|
308
|
-
|
309
|
-
mobj = {"stream": stream, "width": width, "blur": blur}
|
310
|
-
if (arr := self.read_cache("motion", mobj)) is not None:
|
311
|
-
return arr
|
312
|
-
|
313
|
-
container = av.open(f"{self.src.path}", "r")
|
314
|
-
|
315
|
-
video = container.streams.video[stream]
|
316
|
-
video.thread_type = "AUTO"
|
317
|
-
|
318
|
-
inaccurate_dur = 1 if video.duration is None else video.duration
|
319
|
-
self.bar.start(inaccurate_dur, "Analyzing motion")
|
320
|
-
|
321
|
-
prev_frame = None
|
322
|
-
current_frame = None
|
323
|
-
total_pixels = self.src.videos[0].width * self.src.videos[0].height
|
324
|
-
index = 0
|
325
|
-
|
326
|
-
graph = av.filter.Graph()
|
327
|
-
link_nodes(
|
328
|
-
graph.add_buffer(template=video),
|
329
|
-
graph.add("scale", f"{width}:-1"),
|
330
|
-
graph.add("format", "gray"),
|
331
|
-
graph.add("gblur", f"sigma={blur}"),
|
332
|
-
graph.add("buffersink"),
|
333
|
-
)
|
334
|
-
graph.configure()
|
335
|
-
|
336
|
-
threshold_list = np.zeros((1024), dtype=np.float64)
|
337
|
-
|
338
|
-
for unframe in container.decode(video):
|
339
|
-
graph.push(unframe)
|
340
|
-
frame = graph.pull()
|
341
|
-
|
342
|
-
# Showing progress ...
|
343
|
-
assert frame.time is not None
|
344
|
-
index = int(frame.time * self.tb)
|
345
|
-
if frame.pts is not None:
|
346
|
-
self.bar.tick(frame.pts)
|
347
|
-
|
348
|
-
current_frame = frame.to_ndarray()
|
349
|
-
|
350
|
-
if index > len(threshold_list) - 1:
|
351
|
-
threshold_list = np.concatenate(
|
352
|
-
(threshold_list, np.zeros((len(threshold_list)), dtype=np.float64)),
|
353
|
-
axis=0,
|
354
|
-
)
|
355
|
-
|
356
|
-
if prev_frame is not None:
|
357
|
-
# Use `int16` to avoid underflow with `uint8` datatype
|
358
|
-
diff = np.abs(
|
359
|
-
prev_frame.astype(np.int16) - current_frame.astype(np.int16)
|
360
|
-
)
|
361
|
-
threshold_list[index] = np.count_nonzero(diff) / total_pixels
|
362
|
-
|
363
|
-
prev_frame = current_frame
|
364
|
-
|
365
|
-
self.bar.end()
|
366
|
-
return self.cache("motion", mobj, threshold_list[:index])
|
auto_editor/edit.py
CHANGED
@@ -68,12 +68,8 @@ def set_video_codec(
|
|
68
68
|
) -> str:
|
69
69
|
if codec == "auto":
|
70
70
|
codec = "h264" if (src is None or not src.videos) else src.videos[0].codec
|
71
|
-
if ctr.vcodecs
|
72
|
-
|
73
|
-
return ctr.vcodecs[0]
|
74
|
-
|
75
|
-
if codec in ctr.disallow_v:
|
76
|
-
return ctr.vcodecs[0]
|
71
|
+
if codec not in ctr.vcodecs and ctr.default_vid != "none":
|
72
|
+
return ctr.default_vid
|
77
73
|
return codec
|
78
74
|
|
79
75
|
if codec == "copy":
|
@@ -83,12 +79,7 @@ def set_video_codec(
|
|
83
79
|
log.error("Input file does not have a video stream to copy codec from.")
|
84
80
|
codec = src.videos[0].codec
|
85
81
|
|
86
|
-
if ctr.
|
87
|
-
assert ctr.vcodecs is not None
|
88
|
-
if codec not in ctr.vcodecs:
|
89
|
-
log.error(codec_error.format(codec, out_ext))
|
90
|
-
|
91
|
-
if codec in ctr.disallow_v:
|
82
|
+
if ctr.vcodecs is not None and codec not in ctr.vcodecs:
|
92
83
|
log.error(codec_error.format(codec, out_ext))
|
93
84
|
|
94
85
|
return codec
|
@@ -99,8 +90,10 @@ def set_audio_codec(
|
|
99
90
|
) -> str:
|
100
91
|
if codec == "auto":
|
101
92
|
codec = "aac" if (src is None or not src.audios) else src.audios[0].codec
|
102
|
-
if
|
103
|
-
return ctr.
|
93
|
+
if codec not in ctr.acodecs and ctr.default_aud != "none":
|
94
|
+
return ctr.default_aud
|
95
|
+
if codec == "mp3float":
|
96
|
+
return "mp3"
|
104
97
|
return codec
|
105
98
|
|
106
99
|
if codec == "copy":
|
@@ -209,10 +202,8 @@ def edit_media(
|
|
209
202
|
else:
|
210
203
|
samplerate = args.sample_rate
|
211
204
|
|
212
|
-
ensure = Ensure(ffmpeg, bar, samplerate, temp, log)
|
213
|
-
|
214
205
|
if tl is None:
|
215
|
-
tl = make_timeline(sources,
|
206
|
+
tl = make_timeline(sources, args, samplerate, bar, temp, log)
|
216
207
|
|
217
208
|
if export["export"] == "timeline":
|
218
209
|
from auto_editor.formats.json import make_json_timeline
|
@@ -223,7 +214,7 @@ def edit_media(
|
|
223
214
|
if args.preview:
|
224
215
|
from auto_editor.preview import preview
|
225
216
|
|
226
|
-
preview(
|
217
|
+
preview(tl, temp, log)
|
227
218
|
return
|
228
219
|
|
229
220
|
if export["export"] == "json":
|
@@ -272,13 +263,15 @@ def edit_media(
|
|
272
263
|
sub_output = []
|
273
264
|
apply_later = False
|
274
265
|
|
275
|
-
|
266
|
+
ensure = Ensure(ffmpeg, bar, samplerate, temp, log)
|
267
|
+
|
268
|
+
if ctr.default_sub != "none" and not args.sn:
|
276
269
|
sub_output = make_new_subtitles(tl, ensure, temp)
|
277
270
|
|
278
|
-
if ctr.
|
271
|
+
if ctr.default_aud != "none":
|
279
272
|
audio_output = make_new_audio(tl, ensure, args, ffmpeg, bar, temp, log)
|
280
273
|
|
281
|
-
if ctr.
|
274
|
+
if ctr.default_vid != "none":
|
282
275
|
if tl.v:
|
283
276
|
out_path, apply_later = render_av(ffmpeg, tl, args, bar, ctr, temp, log)
|
284
277
|
visual_output.append((True, out_path))
|
auto_editor/lang/palet.py
CHANGED
@@ -620,42 +620,36 @@ def make_array(dtype: Sym, size: int, v: int = 0) -> np.ndarray:
|
|
620
620
|
raise MyError(f"number too large to be converted to {dtype}")
|
621
621
|
|
622
622
|
|
623
|
-
def minclip(oarr: BoolList, _min: int) -> BoolList:
|
623
|
+
def minclip(oarr: BoolList, _min: int, /) -> BoolList:
|
624
624
|
arr = np.copy(oarr)
|
625
625
|
mut_remove_small(arr, _min, replace=1, with_=0)
|
626
626
|
return arr
|
627
627
|
|
628
628
|
|
629
|
-
def mincut(oarr: BoolList, _min: int) -> BoolList:
|
629
|
+
def mincut(oarr: BoolList, _min: int, /) -> BoolList:
|
630
630
|
arr = np.copy(oarr)
|
631
631
|
mut_remove_small(arr, _min, replace=0, with_=1)
|
632
632
|
return arr
|
633
633
|
|
634
634
|
|
635
|
-
def maxclip(oarr: BoolList, _min: int) -> BoolList:
|
635
|
+
def maxclip(oarr: BoolList, _min: int, /) -> BoolList:
|
636
636
|
arr = np.copy(oarr)
|
637
637
|
mut_remove_large(arr, _min, replace=1, with_=0)
|
638
638
|
return arr
|
639
639
|
|
640
640
|
|
641
|
-
def maxcut(oarr: BoolList, _min: int) -> BoolList:
|
641
|
+
def maxcut(oarr: BoolList, _min: int, /) -> BoolList:
|
642
642
|
arr = np.copy(oarr)
|
643
643
|
mut_remove_large(arr, _min, replace=0, with_=1)
|
644
644
|
return arr
|
645
645
|
|
646
646
|
|
647
|
-
def margin(
|
648
|
-
if c is None:
|
649
|
-
check_args("margin", [a, b], (2, 2), (is_int, is_boolarr))
|
650
|
-
oarr = b
|
651
|
-
start, end = a, a
|
652
|
-
else:
|
653
|
-
check_args("margin", [a, b, c], (3, 3), (is_int, is_int, is_boolarr))
|
654
|
-
oarr = c
|
655
|
-
start, end = a, b
|
656
|
-
|
647
|
+
def margin(oarr: BoolList, start: int, end: int | None = None, /) -> BoolList:
|
657
648
|
arr = np.copy(oarr)
|
658
|
-
|
649
|
+
if end is None:
|
650
|
+
mut_margin(arr, start, start)
|
651
|
+
else:
|
652
|
+
mut_margin(arr, start, end)
|
659
653
|
return arr
|
660
654
|
|
661
655
|
|
@@ -1741,6 +1735,8 @@ env.update({
|
|
1741
1735
|
"round": Proc("round", round, (1, 1), is_real),
|
1742
1736
|
"max": Proc("max", lambda *v: max(v), (1, None), is_real),
|
1743
1737
|
"min": Proc("min", lambda *v: min(v), (1, None), is_real),
|
1738
|
+
"max-seq": Proc("max-seq", max, (1, 1), is_sequence),
|
1739
|
+
"min-seq": Proc("min-seq", min, (1, 1), is_sequence),
|
1744
1740
|
"mod": Proc("mod", mod, (2, 2), is_int),
|
1745
1741
|
"modulo": Proc("modulo", mod, (2, 2), is_int),
|
1746
1742
|
# symbols
|
@@ -1796,7 +1792,7 @@ env.update({
|
|
1796
1792
|
"bool-array": Proc(
|
1797
1793
|
"bool-array", lambda *a: np.array(a, dtype=np.bool_), (1, None), is_nat
|
1798
1794
|
),
|
1799
|
-
"margin": Proc("margin", margin, (2, 3)),
|
1795
|
+
"margin": Proc("margin", margin, (2, 3), is_boolarr, is_int),
|
1800
1796
|
"mincut": Proc("mincut", mincut, (2, 2), is_boolarr, is_nat),
|
1801
1797
|
"minclip": Proc("minclip", minclip, (2, 2), is_boolarr, is_nat),
|
1802
1798
|
"maxcut": Proc("maxcut", maxcut, (2, 2), is_boolarr, is_nat),
|
auto_editor/lib/data_structs.py
CHANGED
@@ -185,6 +185,8 @@ def display_str(val: object) -> str:
|
|
185
185
|
return f"{val.real}{join}{val.imag}i"
|
186
186
|
if type(val) is np.bool_:
|
187
187
|
return "1" if val else "0"
|
188
|
+
if type(val) is np.float64 or type(val) is np.float32:
|
189
|
+
return f"{float(val)}"
|
188
190
|
if type(val) is Fraction:
|
189
191
|
return f"{val.numerator}/{val.denominator}"
|
190
192
|
|
auto_editor/make_layers.py
CHANGED
@@ -18,7 +18,6 @@ from auto_editor.utils.types import Args, CoerceError, time
|
|
18
18
|
if TYPE_CHECKING:
|
19
19
|
from numpy.typing import NDArray
|
20
20
|
|
21
|
-
from auto_editor.output import Ensure
|
22
21
|
from auto_editor.utils.bar import Bar
|
23
22
|
from auto_editor.utils.chunks import Chunks
|
24
23
|
from auto_editor.utils.log import Log
|
@@ -75,7 +74,6 @@ def make_av(src: FileInfo, all_clips: list[list[Clip]]) -> tuple[VSpace, ASpace]
|
|
75
74
|
def run_interpreter_for_edit_option(
|
76
75
|
text: str, filesetup: FileSetup
|
77
76
|
) -> NDArray[np.bool_]:
|
78
|
-
ensure = filesetup.ensure
|
79
77
|
src = filesetup.src
|
80
78
|
tb = filesetup.tb
|
81
79
|
bar = filesetup.bar
|
@@ -87,8 +85,8 @@ def run_interpreter_for_edit_option(
|
|
87
85
|
if log.is_debug:
|
88
86
|
log.debug(f"edit: {parser}")
|
89
87
|
|
90
|
-
env["timebase"] =
|
91
|
-
env["@levels"] = Levels(
|
88
|
+
env["timebase"] = tb
|
89
|
+
env["@levels"] = Levels(src, tb, bar, temp, log)
|
92
90
|
env["@filesetup"] = filesetup
|
93
91
|
|
94
92
|
results = interpret(env, parser)
|
@@ -139,7 +137,6 @@ def parse_time(val: str, arr: NDArray, tb: Fraction) -> int: # raises: `CoerceE
|
|
139
137
|
|
140
138
|
def make_timeline(
|
141
139
|
sources: list[FileInfo],
|
142
|
-
ensure: Ensure,
|
143
140
|
args: Args,
|
144
141
|
sr: int,
|
145
142
|
bar: Bar,
|
@@ -169,7 +166,7 @@ def make_timeline(
|
|
169
166
|
concat = np.concatenate
|
170
167
|
|
171
168
|
for i, src in enumerate(sources):
|
172
|
-
filesetup = FileSetup(src,
|
169
|
+
filesetup = FileSetup(src, len(sources) < 2, tb, bar, temp, log)
|
173
170
|
|
174
171
|
edit_result = run_interpreter_for_edit_option(method, filesetup)
|
175
172
|
mut_margin(edit_result, start_margin, end_margin)
|
auto_editor/output.py
CHANGED
@@ -57,7 +57,7 @@ class Ensure:
|
|
57
57
|
output_astream = out_container.add_stream("pcm_s16le", rate=sample_rate)
|
58
58
|
assert isinstance(output_astream, av.audio.stream.AudioStream)
|
59
59
|
|
60
|
-
resampler = AudioResampler(format="s16", layout="stereo", rate=sample_rate)
|
60
|
+
resampler = AudioResampler(format="s16", layout="stereo", rate=sample_rate)
|
61
61
|
for i, frame in enumerate(in_container.decode(astream)):
|
62
62
|
if i % 1500 == 0:
|
63
63
|
bar.tick(0 if frame.time is None else frame.time)
|
@@ -99,7 +99,7 @@ def _ffset(option: str, value: str | None) -> list[str]:
|
|
99
99
|
return [option] + [value]
|
100
100
|
|
101
101
|
|
102
|
-
def video_quality(args: Args
|
102
|
+
def video_quality(args: Args) -> list[str]:
|
103
103
|
return (
|
104
104
|
_ffset("-b:v", args.video_bitrate)
|
105
105
|
+ ["-c:v", args.video_codec]
|
@@ -174,7 +174,7 @@ def mux_quality_media(
|
|
174
174
|
for is_video, path in visual_output:
|
175
175
|
if is_video:
|
176
176
|
if apply_v:
|
177
|
-
cmd += video_quality(args
|
177
|
+
cmd += video_quality(args)
|
178
178
|
else:
|
179
179
|
# Real video is only allowed on track 0
|
180
180
|
cmd += ["-c:v:0", "copy"]
|
@@ -211,7 +211,7 @@ def mux_quality_media(
|
|
211
211
|
cmd.extend(["-c:s", scodec])
|
212
212
|
elif ctr.scodecs is not None:
|
213
213
|
if scodec not in ctr.scodecs:
|
214
|
-
scodec = ctr.
|
214
|
+
scodec = ctr.default_sub
|
215
215
|
cmd.extend(["-c:s", scodec])
|
216
216
|
|
217
217
|
if a_tracks > 0:
|
@@ -227,7 +227,7 @@ def mux_quality_media(
|
|
227
227
|
cmd.extend(["-color_range", f"{color_range}"])
|
228
228
|
if colorspace in (0, 1) or (colorspace >= 3 and colorspace < 16):
|
229
229
|
cmd.extend(["-colorspace", f"{colorspace}"])
|
230
|
-
if color_prim
|
230
|
+
if color_prim == 1 or (color_prim >= 4 and color_prim < 17):
|
231
231
|
cmd.extend(["-color_primaries", f"{color_prim}"])
|
232
232
|
if color_trc == 1 or (color_trc >= 4 and color_trc < 22):
|
233
233
|
cmd.extend(["-color_trc", f"{color_trc}"])
|
auto_editor/preview.py
CHANGED
@@ -6,7 +6,6 @@ from statistics import fmean, median
|
|
6
6
|
from typing import TextIO
|
7
7
|
|
8
8
|
from auto_editor.analyze import Levels
|
9
|
-
from auto_editor.output import Ensure
|
10
9
|
from auto_editor.timeline import v3
|
11
10
|
from auto_editor.utils.bar import Bar
|
12
11
|
from auto_editor.utils.func import to_timecode
|
@@ -49,7 +48,7 @@ def all_cuts(tl: v3, in_len: int) -> list[int]:
|
|
49
48
|
return cut_lens
|
50
49
|
|
51
50
|
|
52
|
-
def preview(
|
51
|
+
def preview(tl: v3, temp: str, log: Log) -> None:
|
53
52
|
log.conwrite("")
|
54
53
|
tb = tl.tb
|
55
54
|
|
@@ -66,7 +65,7 @@ def preview(ensure: Ensure, tl: v3, temp: str, log: Log) -> None:
|
|
66
65
|
|
67
66
|
in_len = 0
|
68
67
|
for src in all_sources:
|
69
|
-
in_len += Levels(
|
68
|
+
in_len += Levels(src, tb, Bar("none"), temp, log).media_length
|
70
69
|
|
71
70
|
out_len = tl.out_len()
|
72
71
|
|
auto_editor/render/subtitle.py
CHANGED
@@ -49,7 +49,7 @@ class SubtitleParser:
|
|
49
49
|
self.codec = codec
|
50
50
|
self.contents = []
|
51
51
|
|
52
|
-
if codec == "ass":
|
52
|
+
if codec == "ass" or codec == "ssa":
|
53
53
|
time_code = re.compile(r"(.*)(\d+:\d+:[\d.]+)(.*)(\d+:\d+:[\d.]+)(.*)")
|
54
54
|
elif codec == "webvtt":
|
55
55
|
time_code = re.compile(r"()(\d+:[\d.]+)( --> )(\d+:[\d.]+)(\n.*)")
|