auto-editor 24.27.1__py3-none-any.whl → 24.30.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 +159 -218
- auto_editor/edit.py +14 -21
- auto_editor/lang/palet.py +29 -9
- auto_editor/lib/contracts.py +12 -6
- auto_editor/make_layers.py +4 -25
- auto_editor/output.py +4 -4
- 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 +52 -37
- auto_editor/subcommands/repl.py +4 -13
- auto_editor/subcommands/subdump.py +5 -8
- auto_editor/utils/container.py +71 -313
- {auto_editor-24.27.1.dist-info → auto_editor-24.30.1.dist-info}/METADATA +3 -6
- {auto_editor-24.27.1.dist-info → auto_editor-24.30.1.dist-info}/RECORD +20 -23
- {auto_editor-24.27.1.dist-info → auto_editor-24.30.1.dist-info}/WHEEL +1 -1
- {auto_editor-24.27.1.dist-info → auto_editor-24.30.1.dist-info}/top_level.txt +0 -1
- ae-ffmpeg/ae_ffmpeg/__init__.py +0 -16
- ae-ffmpeg/ae_ffmpeg/py.typed +0 -0
- ae-ffmpeg/setup.py +0 -65
- {auto_editor-24.27.1.dist-info → auto_editor-24.30.1.dist-info}/LICENSE +0 -0
- {auto_editor-24.27.1.dist-info → auto_editor-24.30.1.dist-info}/entry_points.txt +0 -0
auto_editor/__init__.py
CHANGED
@@ -1,2 +1,2 @@
|
|
1
|
-
__version__ = "24.
|
2
|
-
version = "
|
1
|
+
__version__ = "24.30.1"
|
2
|
+
version = "24w30a"
|
auto_editor/analyze.py
CHANGED
@@ -4,76 +4,32 @@ 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
|
-
from auto_editor.lang.json import Lexer, Parser, dump
|
13
|
-
from auto_editor.lib.contracts import (
|
14
|
-
is_bool,
|
15
|
-
is_nat,
|
16
|
-
is_nat1,
|
17
|
-
is_str,
|
18
|
-
is_threshold,
|
19
|
-
is_void,
|
20
|
-
orc,
|
21
|
-
)
|
22
|
-
from auto_editor.lib.data_structs import Sym
|
23
|
-
from auto_editor.utils.cmdkw import (
|
24
|
-
Required,
|
25
|
-
pAttr,
|
26
|
-
pAttrs,
|
27
|
-
)
|
28
16
|
from auto_editor.utils.subtitle_tools import convert_ass_to_text
|
29
|
-
from auto_editor.wavfile import read
|
30
17
|
|
31
18
|
if TYPE_CHECKING:
|
19
|
+
from collections.abc import Iterator
|
32
20
|
from fractions import Fraction
|
33
21
|
from typing import Any
|
34
22
|
|
35
|
-
from av.filter import FilterContext
|
36
23
|
from numpy.typing import NDArray
|
37
24
|
|
38
25
|
from auto_editor.ffwrapper import FileInfo
|
39
|
-
from auto_editor.output import Ensure
|
40
26
|
from auto_editor.utils.bar import Bar
|
41
27
|
from auto_editor.utils.log import Log
|
42
28
|
|
43
29
|
|
44
|
-
audio_builder = pAttrs(
|
45
|
-
"audio",
|
46
|
-
pAttr("threshold", 0.04, is_threshold),
|
47
|
-
pAttr("stream", 0, orc(is_nat, Sym("all"), "all")),
|
48
|
-
pAttr("mincut", 6, is_nat),
|
49
|
-
pAttr("minclip", 3, is_nat),
|
50
|
-
)
|
51
|
-
motion_builder = pAttrs(
|
52
|
-
"motion",
|
53
|
-
pAttr("threshold", 0.02, is_threshold),
|
54
|
-
pAttr("stream", 0, is_nat),
|
55
|
-
pAttr("blur", 9, is_nat),
|
56
|
-
pAttr("width", 400, is_nat1),
|
57
|
-
)
|
58
|
-
subtitle_builder = pAttrs(
|
59
|
-
"subtitle",
|
60
|
-
pAttr("pattern", Required, is_str),
|
61
|
-
pAttr("stream", 0, is_nat),
|
62
|
-
pAttr("ignore-case", False, is_bool),
|
63
|
-
pAttr("max-count", None, orc(is_nat, is_void)),
|
64
|
-
)
|
65
|
-
|
66
|
-
builder_map = {
|
67
|
-
"audio": audio_builder,
|
68
|
-
"motion": motion_builder,
|
69
|
-
"subtitle": subtitle_builder,
|
70
|
-
}
|
71
|
-
|
72
|
-
|
73
30
|
@dataclass(slots=True)
|
74
31
|
class FileSetup:
|
75
32
|
src: FileInfo
|
76
|
-
ensure: Ensure
|
77
33
|
strict: bool
|
78
34
|
tb: Fraction
|
79
35
|
bar: Bar
|
@@ -85,15 +41,6 @@ class LevelError(Exception):
|
|
85
41
|
pass
|
86
42
|
|
87
43
|
|
88
|
-
def link_nodes(*nodes: FilterContext) -> None:
|
89
|
-
for c, n in zip(nodes, nodes[1:]):
|
90
|
-
c.link_to(n)
|
91
|
-
|
92
|
-
|
93
|
-
def to_threshold(arr: np.ndarray, t: int | float) -> NDArray[np.bool_]:
|
94
|
-
return np.fromiter((x >= t for x in arr), dtype=np.bool_)
|
95
|
-
|
96
|
-
|
97
44
|
def mut_remove_small(
|
98
45
|
arr: NDArray[np.bool_], lim: int, replace: int, with_: int
|
99
46
|
) -> None:
|
@@ -141,9 +88,90 @@ def obj_tag(tag: str, tb: Fraction, obj: dict[str, Any]) -> str:
|
|
141
88
|
return key
|
142
89
|
|
143
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
|
+
|
144
173
|
@dataclass(slots=True)
|
145
174
|
class Levels:
|
146
|
-
ensure: Ensure
|
147
175
|
src: FileInfo
|
148
176
|
tb: Fraction
|
149
177
|
bar: Bar
|
@@ -156,26 +184,16 @@ class Levels:
|
|
156
184
|
if (arr := self.read_cache("audio", {"stream": 0})) is not None:
|
157
185
|
return len(arr)
|
158
186
|
|
159
|
-
|
160
|
-
|
161
|
-
|
162
|
-
|
163
|
-
samp_per_ticks = sr / self.tb
|
164
|
-
ticks = int(samp_count / samp_per_ticks)
|
165
|
-
self.log.debug(f"Audio Length: {ticks}")
|
166
|
-
self.log.debug(
|
167
|
-
f"... without rounding: {float(samp_count / samp_per_ticks)}"
|
168
|
-
)
|
169
|
-
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
|
170
190
|
|
171
191
|
# If there's no audio, get length in video metadata.
|
172
|
-
|
173
|
-
|
174
|
-
with av.open(f"{self.src.path}") as cn:
|
175
|
-
if len(cn.streams.video) < 1:
|
192
|
+
with av.open(self.src.path) as container:
|
193
|
+
if len(container.streams.video) == 0:
|
176
194
|
self.log.error("Could not get media duration")
|
177
195
|
|
178
|
-
video =
|
196
|
+
video = container.streams.video[0]
|
179
197
|
|
180
198
|
if video.duration is None or video.time_base is None:
|
181
199
|
dur = 0
|
@@ -193,107 +211,101 @@ class Levels:
|
|
193
211
|
|
194
212
|
def read_cache(self, tag: str, obj: dict[str, Any]) -> None | np.ndarray:
|
195
213
|
workfile = os.path.join(
|
196
|
-
os.path.dirname(self.temp), f"ae-{version}", "cache.
|
214
|
+
os.path.dirname(self.temp), f"ae-{version}", "cache.npz"
|
197
215
|
)
|
198
216
|
|
199
217
|
try:
|
200
|
-
|
201
|
-
|
202
|
-
|
218
|
+
npzfile = np.load(workfile, allow_pickle=False)
|
219
|
+
except Exception as e:
|
220
|
+
self.log.debug(e)
|
203
221
|
return None
|
204
222
|
|
205
|
-
|
223
|
+
key = f"{self.src.path}:{obj_tag(tag, self.tb, obj)}"
|
224
|
+
if key not in npzfile.files:
|
206
225
|
return None
|
207
226
|
|
208
|
-
|
209
|
-
|
210
|
-
if key not in (root := cache[f"{self.src.path.resolve()}"]):
|
211
|
-
return None
|
212
|
-
|
213
|
-
return np.asarray(root[key]["arr"], dtype=root[key]["type"])
|
227
|
+
self.log.debug("Using cache")
|
228
|
+
return npzfile[key]
|
214
229
|
|
215
230
|
def cache(self, tag: str, obj: dict[str, Any], arr: np.ndarray) -> np.ndarray:
|
216
231
|
workdur = os.path.join(os.path.dirname(self.temp), f"ae-{version}")
|
217
|
-
workfile = os.path.join(workdur, "cache.json")
|
218
232
|
if not os.path.exists(workdur):
|
219
233
|
os.mkdir(workdur)
|
220
234
|
|
221
|
-
|
222
|
-
|
223
|
-
try:
|
224
|
-
with open(workfile, encoding="utf-8") as file:
|
225
|
-
json_object = Parser(Lexer(workfile, file)).expr()
|
226
|
-
except Exception:
|
227
|
-
json_object = {}
|
228
|
-
|
229
|
-
entry = {"type": str(arr.dtype), "arr": arr.tolist()}
|
230
|
-
src_key = f"{self.src.path}"
|
231
|
-
|
232
|
-
if src_key in json_object:
|
233
|
-
json_object[src_key][key] = entry
|
234
|
-
else:
|
235
|
-
json_object[src_key] = {key: entry}
|
236
|
-
|
237
|
-
with open(os.path.join(workdur, "cache.json"), "w", encoding="utf-8") as file:
|
238
|
-
dump(json_object, file)
|
235
|
+
tag = obj_tag(tag, self.tb, obj)
|
236
|
+
np.savez(os.path.join(workdur, "cache.npz"), **{f"{self.src.path}:{tag}": arr})
|
239
237
|
|
240
238
|
return arr
|
241
239
|
|
242
|
-
def audio(self,
|
243
|
-
if
|
244
|
-
raise LevelError(f"audio: audio stream '{
|
240
|
+
def audio(self, stream: int) -> NDArray[np.float64]:
|
241
|
+
if stream >= len(self.src.audios):
|
242
|
+
raise LevelError(f"audio: audio stream '{stream}' does not exist.")
|
245
243
|
|
246
|
-
if (arr := self.read_cache("audio", {"stream":
|
244
|
+
if (arr := self.read_cache("audio", {"stream": stream})) is not None:
|
247
245
|
return arr
|
248
246
|
|
249
|
-
|
250
|
-
|
251
|
-
|
252
|
-
|
253
|
-
|
254
|
-
|
255
|
-
|
256
|
-
|
257
|
-
max_volume = get_max_volume(samples)
|
258
|
-
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
|
259
255
|
|
260
|
-
|
261
|
-
|
256
|
+
bar = self.bar
|
257
|
+
bar.start(inaccurate_dur, "Analyzing audio volume")
|
262
258
|
|
263
|
-
|
264
|
-
|
265
|
-
|
266
|
-
|
267
|
-
|
268
|
-
|
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
|
269
269
|
|
270
|
-
|
271
|
-
self.
|
272
|
-
f"analyze: audio length: {audio_ticks} ({float(samp_count / samp_per_ticks)})"
|
273
|
-
)
|
274
|
-
self.bar.start(audio_ticks, "Analyzing audio volume")
|
270
|
+
bar.end()
|
271
|
+
return self.cache("audio", {"stream": stream}, result[:index])
|
275
272
|
|
276
|
-
|
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.")
|
277
276
|
|
278
|
-
|
279
|
-
|
277
|
+
mobj = {"stream": stream, "width": width, "blur": blur}
|
278
|
+
if (arr := self.read_cache("motion", mobj)) is not None:
|
279
|
+
return arr
|
280
280
|
|
281
|
-
|
282
|
-
|
283
|
-
|
284
|
-
|
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
|
+
)
|
285
288
|
|
286
|
-
|
287
|
-
|
289
|
+
bar = self.bar
|
290
|
+
bar.start(inaccurate_dur, "Analyzing motion")
|
288
291
|
|
289
|
-
|
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
|
290
302
|
|
291
|
-
|
292
|
-
return self.cache("
|
303
|
+
bar.end()
|
304
|
+
return self.cache("motion", mobj, result[:index])
|
293
305
|
|
294
306
|
def subtitle(
|
295
307
|
self,
|
296
|
-
|
308
|
+
pattern: str,
|
297
309
|
stream: int,
|
298
310
|
ignore_case: bool,
|
299
311
|
max_count: int | None,
|
@@ -303,14 +315,10 @@ class Levels:
|
|
303
315
|
|
304
316
|
try:
|
305
317
|
flags = re.IGNORECASE if ignore_case else 0
|
306
|
-
|
307
|
-
del patterns # make sure we don't accidentally use it
|
318
|
+
re_pattern = re.compile(pattern, flags)
|
308
319
|
except re.error as e:
|
309
320
|
self.log.error(e)
|
310
321
|
|
311
|
-
import av
|
312
|
-
from av.subtitles.subtitle import AssSubtitle, TextSubtitle
|
313
|
-
|
314
322
|
try:
|
315
323
|
container = av.open(self.src.path, "r")
|
316
324
|
subtitle_stream = container.streams.subtitles[stream]
|
@@ -357,80 +365,13 @@ class Levels:
|
|
357
365
|
for sub in subset:
|
358
366
|
if isinstance(sub, AssSubtitle):
|
359
367
|
line = convert_ass_to_text(sub.ass.decode(errors="ignore"))
|
360
|
-
elif isinstance(sub, TextSubtitle):
|
361
|
-
line = sub.text.decode(errors="ignore")
|
362
368
|
else:
|
363
369
|
continue
|
364
370
|
|
365
|
-
if line and re.search(
|
371
|
+
if line and re.search(re_pattern, line):
|
366
372
|
result[san_start:san_end] = 1
|
367
373
|
count += 1
|
368
374
|
|
369
375
|
container.close()
|
370
376
|
|
371
377
|
return result
|
372
|
-
|
373
|
-
def motion(self, s: int, blur: int, width: int) -> NDArray[np.float64]:
|
374
|
-
import av
|
375
|
-
|
376
|
-
if s >= len(self.src.videos):
|
377
|
-
raise LevelError(f"motion: video stream '{s}' does not exist.")
|
378
|
-
|
379
|
-
mobj = {"stream": s, "width": width, "blur": blur}
|
380
|
-
if (arr := self.read_cache("motion", mobj)) is not None:
|
381
|
-
return arr
|
382
|
-
|
383
|
-
container = av.open(f"{self.src.path}", "r")
|
384
|
-
|
385
|
-
stream = container.streams.video[s]
|
386
|
-
stream.thread_type = "AUTO"
|
387
|
-
|
388
|
-
inaccurate_dur = 1 if stream.duration is None else stream.duration
|
389
|
-
self.bar.start(inaccurate_dur, "Analyzing motion")
|
390
|
-
|
391
|
-
prev_frame = None
|
392
|
-
current_frame = None
|
393
|
-
total_pixels = self.src.videos[0].width * self.src.videos[0].height
|
394
|
-
index = 0
|
395
|
-
|
396
|
-
graph = av.filter.Graph()
|
397
|
-
link_nodes(
|
398
|
-
graph.add_buffer(template=stream),
|
399
|
-
graph.add("scale", f"{width}:-1"),
|
400
|
-
graph.add("format", "gray"),
|
401
|
-
graph.add("gblur", f"sigma={blur}"),
|
402
|
-
graph.add("buffersink"),
|
403
|
-
)
|
404
|
-
graph.configure()
|
405
|
-
|
406
|
-
threshold_list = np.zeros((1024), dtype=np.float64)
|
407
|
-
|
408
|
-
for unframe in container.decode(stream):
|
409
|
-
graph.push(unframe)
|
410
|
-
frame = graph.pull()
|
411
|
-
|
412
|
-
# Showing progress ...
|
413
|
-
assert frame.time is not None
|
414
|
-
index = int(frame.time * self.tb)
|
415
|
-
if frame.pts is not None:
|
416
|
-
self.bar.tick(frame.pts)
|
417
|
-
|
418
|
-
current_frame = frame.to_ndarray()
|
419
|
-
|
420
|
-
if index > len(threshold_list) - 1:
|
421
|
-
threshold_list = np.concatenate(
|
422
|
-
(threshold_list, np.zeros((len(threshold_list)), dtype=np.float64)),
|
423
|
-
axis=0,
|
424
|
-
)
|
425
|
-
|
426
|
-
if prev_frame is not None:
|
427
|
-
# Use `int16` to avoid underflow with `uint8` datatype
|
428
|
-
diff = np.abs(
|
429
|
-
prev_frame.astype(np.int16) - current_frame.astype(np.int16)
|
430
|
-
)
|
431
|
-
threshold_list[index] = np.count_nonzero(diff) / total_pixels
|
432
|
-
|
433
|
-
prev_frame = current_frame
|
434
|
-
|
435
|
-
self.bar.end()
|
436
|
-
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
@@ -18,12 +18,7 @@ from typing import TYPE_CHECKING
|
|
18
18
|
import numpy as np
|
19
19
|
from numpy import logical_and, logical_not, logical_or, logical_xor
|
20
20
|
|
21
|
-
from auto_editor.analyze import
|
22
|
-
LevelError,
|
23
|
-
mut_remove_large,
|
24
|
-
mut_remove_small,
|
25
|
-
to_threshold,
|
26
|
-
)
|
21
|
+
from auto_editor.analyze import LevelError, mut_remove_large, mut_remove_small
|
27
22
|
from auto_editor.lib.contracts import *
|
28
23
|
from auto_editor.lib.data_structs import *
|
29
24
|
from auto_editor.lib.err import MyError
|
@@ -690,6 +685,9 @@ def palet_map(proc: Proc, seq: Any) -> Any:
|
|
690
685
|
return Quoted(tuple(map(proc, seq.val)))
|
691
686
|
if isinstance(seq, list | range):
|
692
687
|
return list(map(proc, seq))
|
688
|
+
elif isinstance(seq, np.ndarray):
|
689
|
+
vectorized_proc = np.vectorize(proc)
|
690
|
+
return vectorized_proc(seq)
|
693
691
|
return proc(seq)
|
694
692
|
|
695
693
|
|
@@ -1469,6 +1467,26 @@ def edit_all() -> np.ndarray:
|
|
1469
1467
|
return env["@levels"].all()
|
1470
1468
|
|
1471
1469
|
|
1470
|
+
def audio_levels(stream: int) -> np.ndarray:
|
1471
|
+
if "@levels" not in env:
|
1472
|
+
raise MyError("Can't use `audio` if there's no input media")
|
1473
|
+
|
1474
|
+
try:
|
1475
|
+
return env["@levels"].audio(stream)
|
1476
|
+
except LevelError as e:
|
1477
|
+
raise MyError(e)
|
1478
|
+
|
1479
|
+
|
1480
|
+
def motion_levels(stream: int, blur: int = 9, width: int = 400) -> np.ndarray:
|
1481
|
+
if "@levels" not in env:
|
1482
|
+
raise MyError("Can't use `motion` if there's no input media")
|
1483
|
+
|
1484
|
+
try:
|
1485
|
+
return env["@levels"].motion(stream, blur, width)
|
1486
|
+
except LevelError as e:
|
1487
|
+
raise MyError(e)
|
1488
|
+
|
1489
|
+
|
1472
1490
|
def edit_audio(
|
1473
1491
|
threshold: float = 0.04,
|
1474
1492
|
stream: object = Sym("all"),
|
@@ -1491,7 +1509,7 @@ def edit_audio(
|
|
1491
1509
|
|
1492
1510
|
try:
|
1493
1511
|
for s in stream_range:
|
1494
|
-
audio_list =
|
1512
|
+
audio_list = levels.audio(s) >= threshold
|
1495
1513
|
if stream_data is None:
|
1496
1514
|
stream_data = audio_list
|
1497
1515
|
else:
|
@@ -1521,7 +1539,7 @@ def edit_motion(
|
|
1521
1539
|
levels = env["@levels"]
|
1522
1540
|
strict = env["@filesetup"].strict
|
1523
1541
|
try:
|
1524
|
-
return
|
1542
|
+
return levels.motion(stream, blur, width) >= threshold
|
1525
1543
|
except LevelError as e:
|
1526
1544
|
return raise_(e) if strict else levels.all()
|
1527
1545
|
|
@@ -1582,7 +1600,7 @@ def my_eval(env: Env, node: object) -> Any:
|
|
1582
1600
|
return ref(oper, my_eval(env, node[1]))
|
1583
1601
|
|
1584
1602
|
raise MyError(
|
1585
|
-
f"
|
1603
|
+
f"{print_str(oper)} is not a function. Tried to run with args: {print_str(node[1:])}"
|
1586
1604
|
)
|
1587
1605
|
|
1588
1606
|
if type(oper) is Syntax:
|
@@ -1617,10 +1635,12 @@ env.update({
|
|
1617
1635
|
# edit procedures
|
1618
1636
|
"none": Proc("none", edit_none, (0, 0)),
|
1619
1637
|
"all/e": Proc("all/e", edit_all, (0, 0)),
|
1638
|
+
"audio-levels": Proc("audio-levels", audio_levels, (1, 1), is_nat),
|
1620
1639
|
"audio": Proc("audio", edit_audio, (0, 4),
|
1621
1640
|
is_threshold, orc(is_nat, Sym("all")), is_nat,
|
1622
1641
|
{"threshold": 0, "stream": 1, "minclip": 2, "mincut": 2}
|
1623
1642
|
),
|
1643
|
+
"motion-levels": Proc("motion-levels", motion_levels, (1, 3), is_nat, is_nat1, {"blur": 1, "width": 2}),
|
1624
1644
|
"motion": Proc("motion", edit_motion, (0, 4),
|
1625
1645
|
is_threshold, is_nat, is_nat1,
|
1626
1646
|
{"threshold": 0, "stream": 1, "blur": 1, "width": 2}
|