auto-editor 24.31.1__py3-none-any.whl → 25.0.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 +1 -2
- auto_editor/__main__.py +15 -20
- auto_editor/analyze.py +26 -30
- auto_editor/edit.py +8 -12
- auto_editor/ffwrapper.py +4 -2
- auto_editor/lang/palet.py +5 -7
- auto_editor/lib/contracts.py +5 -2
- auto_editor/make_layers.py +28 -47
- auto_editor/output.py +3 -5
- auto_editor/preview.py +2 -2
- auto_editor/render/audio.py +2 -1
- auto_editor/render/video.py +2 -7
- auto_editor/subcommands/info.py +2 -2
- auto_editor/subcommands/levels.py +4 -9
- auto_editor/subcommands/repl.py +4 -7
- auto_editor/subcommands/test.py +40 -33
- auto_editor/timeline.py +10 -12
- auto_editor/utils/container.py +6 -0
- auto_editor/utils/func.py +10 -26
- auto_editor/utils/log.py +35 -8
- auto_editor/utils/types.py +2 -1
- auto_editor/validate_input.py +12 -7
- auto_editor/vanparse.py +1 -2
- {auto_editor-24.31.1.dist-info → auto_editor-25.0.1.dist-info}/METADATA +1 -1
- auto_editor-25.0.1.dist-info/RECORD +55 -0
- {auto_editor-24.31.1.dist-info → auto_editor-25.0.1.dist-info}/WHEEL +1 -1
- auto_editor-24.31.1.dist-info/RECORD +0 -55
- {auto_editor-24.31.1.dist-info → auto_editor-25.0.1.dist-info}/LICENSE +0 -0
- {auto_editor-24.31.1.dist-info → auto_editor-25.0.1.dist-info}/entry_points.txt +0 -0
- {auto_editor-24.31.1.dist-info → auto_editor-25.0.1.dist-info}/top_level.txt +0 -0
auto_editor/__init__.py
CHANGED
@@ -1,2 +1 @@
|
|
1
|
-
__version__ = "
|
2
|
-
version = "24w31a"
|
1
|
+
__version__ = "25.0.1"
|
auto_editor/__main__.py
CHANGED
@@ -6,7 +6,6 @@ from os import environ
|
|
6
6
|
import auto_editor
|
7
7
|
from auto_editor.edit import edit_media
|
8
8
|
from auto_editor.ffwrapper import FFmpeg
|
9
|
-
from auto_editor.utils.func import setup_tempdir
|
10
9
|
from auto_editor.utils.log import Log
|
11
10
|
from auto_editor.utils.types import (
|
12
11
|
Args,
|
@@ -140,10 +139,7 @@ def main_options(parser: ArgumentParser) -> ArgumentParser:
|
|
140
139
|
)
|
141
140
|
parser.add_text("Utility Options:")
|
142
141
|
parser.add_argument(
|
143
|
-
"--export",
|
144
|
-
"-ex",
|
145
|
-
metavar="EXPORT:ATTRS?",
|
146
|
-
help="Choose the export mode",
|
142
|
+
"--export", "-ex", metavar="EXPORT:ATTRS?", help="Choose the export mode"
|
147
143
|
)
|
148
144
|
parser.add_argument(
|
149
145
|
"--output-file",
|
@@ -153,15 +149,10 @@ def main_options(parser: ArgumentParser) -> ArgumentParser:
|
|
153
149
|
help="Set the name/path of the new output file.",
|
154
150
|
)
|
155
151
|
parser.add_argument(
|
156
|
-
"--player",
|
157
|
-
"-p",
|
158
|
-
metavar="CMD",
|
159
|
-
help="Set player to open output media files",
|
152
|
+
"--player", "-p", metavar="CMD", help="Set player to open output media files"
|
160
153
|
)
|
161
154
|
parser.add_argument(
|
162
|
-
"--no-open",
|
163
|
-
flag=True,
|
164
|
-
help="Do not open the output file after editing is done",
|
155
|
+
"--no-open", flag=True, help="Do not open the output file after editing is done"
|
165
156
|
)
|
166
157
|
parser.add_argument(
|
167
158
|
"--temp-dir",
|
@@ -255,7 +246,7 @@ def main_options(parser: ArgumentParser) -> ArgumentParser:
|
|
255
246
|
parser.add_argument(
|
256
247
|
"--audio-normalize",
|
257
248
|
metavar="NORM-TYPE",
|
258
|
-
help="Apply audio rendering to all audio tracks. Applied right before rendering the output file
|
249
|
+
help="Apply audio rendering to all audio tracks. Applied right before rendering the output file",
|
259
250
|
)
|
260
251
|
parser.add_text("Miscellaneous:")
|
261
252
|
parser.add_argument(
|
@@ -268,6 +259,9 @@ def main_options(parser: ArgumentParser) -> ArgumentParser:
|
|
268
259
|
metavar="CMD",
|
269
260
|
help="Add extra options for ffmpeg. Must be in quotes",
|
270
261
|
)
|
262
|
+
parser.add_argument(
|
263
|
+
"--no-cache", flag=True, help="Don't look for or write a cache file"
|
264
|
+
)
|
271
265
|
parser.add_argument("--version", "-V", flag=True, help="Display version and halt")
|
272
266
|
return parser
|
273
267
|
|
@@ -303,7 +297,7 @@ def main() -> None:
|
|
303
297
|
)
|
304
298
|
|
305
299
|
if args.version:
|
306
|
-
print(
|
300
|
+
print(auto_editor.__version__)
|
307
301
|
return
|
308
302
|
|
309
303
|
if args.debug and not args.input:
|
@@ -311,18 +305,19 @@ def main() -> None:
|
|
311
305
|
|
312
306
|
import av
|
313
307
|
|
308
|
+
license = av._core.library_meta["libavcodec"]["license"]
|
309
|
+
|
314
310
|
print(f"OS: {plat.system()} {plat.release()} {plat.machine().lower()}")
|
315
311
|
print(f"Python: {plat.python_version()}")
|
316
|
-
print(f"PyAV: {av.__version__}")
|
317
|
-
print(f"Auto-Editor: {auto_editor.
|
312
|
+
print(f"PyAV: {av.__version__} ({license})")
|
313
|
+
print(f"Auto-Editor: {auto_editor.__version__}")
|
318
314
|
return
|
319
315
|
|
320
316
|
if not args.input:
|
321
317
|
log.error("You need to give auto-editor an input file.")
|
322
318
|
|
323
|
-
|
324
|
-
log = Log(args.debug, args.quiet,
|
325
|
-
log.debug(f"Temp Directory: {temp}")
|
319
|
+
is_machine = args.progress == "machine"
|
320
|
+
log = Log(args.debug, args.quiet, args.temp_dir, is_machine, no_color)
|
326
321
|
|
327
322
|
ffmpeg = FFmpeg(
|
328
323
|
args.ffmpeg_location,
|
@@ -333,7 +328,7 @@ def main() -> None:
|
|
333
328
|
paths = valid_input(args.input, ffmpeg, args, log)
|
334
329
|
|
335
330
|
try:
|
336
|
-
edit_media(paths, ffmpeg, args,
|
331
|
+
edit_media(paths, ffmpeg, args, log)
|
337
332
|
except KeyboardInterrupt:
|
338
333
|
log.error("Keyboard Interrupt")
|
339
334
|
log.cleanup()
|
auto_editor/analyze.py
CHANGED
@@ -5,6 +5,7 @@ import re
|
|
5
5
|
from dataclasses import dataclass
|
6
6
|
from fractions import Fraction
|
7
7
|
from math import ceil
|
8
|
+
from tempfile import gettempdir
|
8
9
|
from typing import TYPE_CHECKING
|
9
10
|
|
10
11
|
import av
|
@@ -12,7 +13,7 @@ import numpy as np
|
|
12
13
|
from av.audio.fifo import AudioFifo
|
13
14
|
from av.subtitles.subtitle import AssSubtitle
|
14
15
|
|
15
|
-
from auto_editor import
|
16
|
+
from auto_editor import __version__
|
16
17
|
from auto_editor.utils.subtitle_tools import convert_ass_to_text
|
17
18
|
|
18
19
|
if TYPE_CHECKING:
|
@@ -27,16 +28,6 @@ if TYPE_CHECKING:
|
|
27
28
|
from auto_editor.utils.log import Log
|
28
29
|
|
29
30
|
|
30
|
-
@dataclass(slots=True)
|
31
|
-
class FileSetup:
|
32
|
-
src: FileInfo
|
33
|
-
strict: bool
|
34
|
-
tb: Fraction
|
35
|
-
bar: Bar
|
36
|
-
temp: str
|
37
|
-
log: Log
|
38
|
-
|
39
|
-
|
40
31
|
class LevelError(Exception):
|
41
32
|
pass
|
42
33
|
|
@@ -88,7 +79,7 @@ def obj_tag(tag: str, tb: Fraction, obj: dict[str, Any]) -> str:
|
|
88
79
|
return key
|
89
80
|
|
90
81
|
|
91
|
-
def iter_audio(src, tb: Fraction, stream: int = 0) -> Iterator[
|
82
|
+
def iter_audio(src, tb: Fraction, stream: int = 0) -> Iterator[np.float32]:
|
92
83
|
fifo = AudioFifo()
|
93
84
|
try:
|
94
85
|
container = av.open(src.path, "r")
|
@@ -117,13 +108,13 @@ def iter_audio(src, tb: Fraction, stream: int = 0) -> Iterator[float]:
|
|
117
108
|
audio_chunk = fifo.read(current_size)
|
118
109
|
assert audio_chunk is not None
|
119
110
|
arr = audio_chunk.to_ndarray().flatten()
|
120
|
-
yield
|
111
|
+
yield np.max(np.abs(arr))
|
121
112
|
|
122
113
|
finally:
|
123
114
|
container.close()
|
124
115
|
|
125
116
|
|
126
|
-
def iter_motion(src, tb, stream: int, blur: int, width: int) -> Iterator[
|
117
|
+
def iter_motion(src, tb, stream: int, blur: int, width: int) -> Iterator[np.float32]:
|
127
118
|
container = av.open(src.path, "r")
|
128
119
|
|
129
120
|
video = container.streams.video[stream]
|
@@ -155,11 +146,11 @@ def iter_motion(src, tb, stream: int, blur: int, width: int) -> Iterator[float]:
|
|
155
146
|
|
156
147
|
current_frame = frame.to_ndarray()
|
157
148
|
if prev_frame is None:
|
158
|
-
value = 0.0
|
149
|
+
value = np.float32(0.0)
|
159
150
|
else:
|
160
151
|
# Use `int16` to avoid underflow with `uint8` datatype
|
161
152
|
diff = np.abs(prev_frame.astype(np.int16) - current_frame.astype(np.int16))
|
162
|
-
value = np.count_nonzero(diff) / total_pixels
|
153
|
+
value = np.float32(np.count_nonzero(diff) / total_pixels)
|
163
154
|
|
164
155
|
for _ in range(index - prev_index):
|
165
156
|
yield value
|
@@ -175,8 +166,9 @@ class Levels:
|
|
175
166
|
src: FileInfo
|
176
167
|
tb: Fraction
|
177
168
|
bar: Bar
|
178
|
-
|
169
|
+
no_cache: bool
|
179
170
|
log: Log
|
171
|
+
strict: bool
|
180
172
|
|
181
173
|
@property
|
182
174
|
def media_length(self) -> int:
|
@@ -210,9 +202,10 @@ class Levels:
|
|
210
202
|
return np.zeros(self.media_length, dtype=np.bool_)
|
211
203
|
|
212
204
|
def read_cache(self, tag: str, obj: dict[str, Any]) -> None | np.ndarray:
|
213
|
-
|
214
|
-
|
215
|
-
|
205
|
+
if self.no_cache:
|
206
|
+
return None
|
207
|
+
|
208
|
+
workfile = os.path.join(gettempdir(), f"ae-{__version__}", "cache.npz")
|
216
209
|
|
217
210
|
try:
|
218
211
|
npzfile = np.load(workfile, allow_pickle=False)
|
@@ -227,8 +220,11 @@ class Levels:
|
|
227
220
|
self.log.debug("Using cache")
|
228
221
|
return npzfile[key]
|
229
222
|
|
230
|
-
def cache(self, tag: str, obj: dict[str, Any]
|
231
|
-
|
223
|
+
def cache(self, arr: np.ndarray, tag: str, obj: dict[str, Any]) -> np.ndarray:
|
224
|
+
if self.no_cache:
|
225
|
+
return arr
|
226
|
+
|
227
|
+
workdur = os.path.join(gettempdir(), f"ae-{__version__}")
|
232
228
|
if not os.path.exists(workdur):
|
233
229
|
os.mkdir(workdur)
|
234
230
|
|
@@ -237,7 +233,7 @@ class Levels:
|
|
237
233
|
|
238
234
|
return arr
|
239
235
|
|
240
|
-
def audio(self, stream: int) -> NDArray[np.
|
236
|
+
def audio(self, stream: int) -> NDArray[np.float32]:
|
241
237
|
if stream >= len(self.src.audios):
|
242
238
|
raise LevelError(f"audio: audio stream '{stream}' does not exist.")
|
243
239
|
|
@@ -256,21 +252,21 @@ class Levels:
|
|
256
252
|
bar = self.bar
|
257
253
|
bar.start(inaccurate_dur, "Analyzing audio volume")
|
258
254
|
|
259
|
-
result = np.zeros((inaccurate_dur), dtype=np.
|
255
|
+
result = np.zeros((inaccurate_dur), dtype=np.float32)
|
260
256
|
index = 0
|
261
257
|
for value in iter_audio(self.src, self.tb, stream):
|
262
258
|
if index > len(result) - 1:
|
263
259
|
result = np.concatenate(
|
264
|
-
(result, np.zeros((len(result)), dtype=np.
|
260
|
+
(result, np.zeros((len(result)), dtype=np.float32))
|
265
261
|
)
|
266
262
|
result[index] = value
|
267
263
|
bar.tick(index)
|
268
264
|
index += 1
|
269
265
|
|
270
266
|
bar.end()
|
271
|
-
return self.cache("audio", {"stream": stream}
|
267
|
+
return self.cache(result[:index], "audio", {"stream": stream})
|
272
268
|
|
273
|
-
def motion(self, stream: int, blur: int, width: int) -> NDArray[np.
|
269
|
+
def motion(self, stream: int, blur: int, width: int) -> NDArray[np.float32]:
|
274
270
|
if stream >= len(self.src.videos):
|
275
271
|
raise LevelError(f"motion: video stream '{stream}' does not exist.")
|
276
272
|
|
@@ -289,19 +285,19 @@ class Levels:
|
|
289
285
|
bar = self.bar
|
290
286
|
bar.start(inaccurate_dur, "Analyzing motion")
|
291
287
|
|
292
|
-
result = np.zeros((inaccurate_dur), dtype=np.
|
288
|
+
result = np.zeros((inaccurate_dur), dtype=np.float32)
|
293
289
|
index = 0
|
294
290
|
for value in iter_motion(self.src, self.tb, stream, blur, width):
|
295
291
|
if index > len(result) - 1:
|
296
292
|
result = np.concatenate(
|
297
|
-
(result, np.zeros((len(result)), dtype=np.
|
293
|
+
(result, np.zeros((len(result)), dtype=np.float32))
|
298
294
|
)
|
299
295
|
result[index] = value
|
300
296
|
bar.tick(index)
|
301
297
|
index += 1
|
302
298
|
|
303
299
|
bar.end()
|
304
|
-
return self.cache("motion", mobj
|
300
|
+
return self.cache(result[:index], "motion", mobj)
|
305
301
|
|
306
302
|
def subtitle(
|
307
303
|
self,
|
auto_editor/edit.py
CHANGED
@@ -142,9 +142,7 @@ def parse_export(export: str, log: Log) -> dict[str, Any]:
|
|
142
142
|
log.error(f"'{name}': Export must be [{', '.join([s for s in parsing.keys()])}]")
|
143
143
|
|
144
144
|
|
145
|
-
def edit_media(
|
146
|
-
paths: list[str], ffmpeg: FFmpeg, args: Args, temp: str, log: Log
|
147
|
-
) -> None:
|
145
|
+
def edit_media(paths: list[str], ffmpeg: FFmpeg, args: Args, log: Log) -> None:
|
148
146
|
bar = Bar(args.progress)
|
149
147
|
tl = None
|
150
148
|
|
@@ -203,7 +201,7 @@ def edit_media(
|
|
203
201
|
samplerate = args.sample_rate
|
204
202
|
|
205
203
|
if tl is None:
|
206
|
-
tl = make_timeline(sources, args, samplerate, bar,
|
204
|
+
tl = make_timeline(sources, args, samplerate, bar, log)
|
207
205
|
|
208
206
|
if export["export"] == "timeline":
|
209
207
|
from auto_editor.formats.json import make_json_timeline
|
@@ -214,7 +212,7 @@ def edit_media(
|
|
214
212
|
if args.preview:
|
215
213
|
from auto_editor.preview import preview
|
216
214
|
|
217
|
-
preview(tl,
|
215
|
+
preview(tl, log)
|
218
216
|
return
|
219
217
|
|
220
218
|
if export["export"] == "json":
|
@@ -263,22 +261,21 @@ def edit_media(
|
|
263
261
|
sub_output = []
|
264
262
|
apply_later = False
|
265
263
|
|
266
|
-
ensure = Ensure(ffmpeg, bar, samplerate,
|
267
|
-
|
264
|
+
ensure = Ensure(ffmpeg, bar, samplerate, log)
|
268
265
|
if ctr.default_sub != "none" and not args.sn:
|
269
|
-
sub_output = make_new_subtitles(tl, ensure, temp)
|
266
|
+
sub_output = make_new_subtitles(tl, ensure, log.temp)
|
270
267
|
|
271
268
|
if ctr.default_aud != "none":
|
272
|
-
audio_output = make_new_audio(tl, ensure, args, ffmpeg, bar,
|
269
|
+
audio_output = make_new_audio(tl, ensure, args, ffmpeg, bar, log)
|
273
270
|
|
274
271
|
if ctr.default_vid != "none":
|
275
272
|
if tl.v:
|
276
|
-
out_path, apply_later = render_av(ffmpeg, tl, args, bar, ctr,
|
273
|
+
out_path, apply_later = render_av(ffmpeg, tl, args, bar, ctr, log)
|
277
274
|
visual_output.append((True, out_path))
|
278
275
|
|
279
276
|
for v, vid in enumerate(src.videos, start=1):
|
280
277
|
if ctr.allow_image and vid.codec in ("png", "mjpeg", "webp"):
|
281
|
-
out_path = os.path.join(temp, f"{v}.{vid.codec}")
|
278
|
+
out_path = os.path.join(log.temp, f"{v}.{vid.codec}")
|
282
279
|
# fmt: off
|
283
280
|
ffmpeg.run(["-i", f"{src.path}", "-map", "0:v", "-map", "-0:V",
|
284
281
|
"-c", "copy", out_path])
|
@@ -297,7 +294,6 @@ def edit_media(
|
|
297
294
|
tl.tb,
|
298
295
|
args,
|
299
296
|
src,
|
300
|
-
temp,
|
301
297
|
log,
|
302
298
|
)
|
303
299
|
|
auto_editor/ffwrapper.py
CHANGED
@@ -149,6 +149,7 @@ class VideoStream:
|
|
149
149
|
class AudioStream:
|
150
150
|
codec: str
|
151
151
|
samplerate: int
|
152
|
+
layout: str
|
152
153
|
channels: int
|
153
154
|
duration: float
|
154
155
|
bitrate: int
|
@@ -195,7 +196,7 @@ def initFileInfo(path: str, log: Log) -> FileInfo:
|
|
195
196
|
try:
|
196
197
|
cont = av.open(path, "r")
|
197
198
|
except av.error.FileNotFoundError:
|
198
|
-
log.error(f"
|
199
|
+
log.error(f"Input file doesn't exist: {path}")
|
199
200
|
except av.error.IsADirectoryError:
|
200
201
|
log.error(f"Expected a media file, but got a directory: {path}")
|
201
202
|
except av.error.InvalidDataError:
|
@@ -232,7 +233,7 @@ def initFileInfo(path: str, log: Log) -> FileInfo:
|
|
232
233
|
vdur,
|
233
234
|
sar,
|
234
235
|
v.time_base,
|
235
|
-
|
236
|
+
getattr(v.format, "name", None),
|
236
237
|
cc.color_range,
|
237
238
|
cc.colorspace,
|
238
239
|
cc.color_primaries,
|
@@ -252,6 +253,7 @@ def initFileInfo(path: str, log: Log) -> FileInfo:
|
|
252
253
|
AudioStream(
|
253
254
|
a_cc.name,
|
254
255
|
0 if a_cc.sample_rate is None else a_cc.sample_rate,
|
256
|
+
a.layout.name,
|
255
257
|
a_cc.channels,
|
256
258
|
adur,
|
257
259
|
0 if a_cc.bit_rate is None else a_cc.bit_rate,
|
auto_editor/lang/palet.py
CHANGED
@@ -1487,12 +1487,12 @@ def edit_audio(
|
|
1487
1487
|
mincut: int = 6,
|
1488
1488
|
minclip: int = 3,
|
1489
1489
|
) -> np.ndarray:
|
1490
|
-
if "@levels" not in env
|
1490
|
+
if "@levels" not in env:
|
1491
1491
|
raise MyError("Can't use `audio` if there's no input media")
|
1492
1492
|
|
1493
1493
|
levels = env["@levels"]
|
1494
|
-
src =
|
1495
|
-
strict =
|
1494
|
+
src = levels.src
|
1495
|
+
strict = levels.strict
|
1496
1496
|
|
1497
1497
|
stream_data: NDArray[np.bool_] | None = None
|
1498
1498
|
if stream == Sym("all"):
|
@@ -1531,11 +1531,10 @@ def edit_motion(
|
|
1531
1531
|
raise MyError("Can't use `motion` if there's no input media")
|
1532
1532
|
|
1533
1533
|
levels = env["@levels"]
|
1534
|
-
strict = env["@filesetup"].strict
|
1535
1534
|
try:
|
1536
1535
|
return levels.motion(stream, blur, width) >= threshold
|
1537
1536
|
except LevelError as e:
|
1538
|
-
return raise_(e) if strict else levels.all()
|
1537
|
+
return raise_(e) if levels.strict else levels.all()
|
1539
1538
|
|
1540
1539
|
|
1541
1540
|
def edit_subtitle(pattern, stream=0, **kwargs):
|
@@ -1543,7 +1542,6 @@ def edit_subtitle(pattern, stream=0, **kwargs):
|
|
1543
1542
|
raise MyError("Can't use `subtitle` if there's no input media")
|
1544
1543
|
|
1545
1544
|
levels = env["@levels"]
|
1546
|
-
strict = env["@filesetup"].strict
|
1547
1545
|
if "ignore-case" not in kwargs:
|
1548
1546
|
kwargs["ignore-case"] = False
|
1549
1547
|
if "max-count" not in kwargs:
|
@@ -1553,7 +1551,7 @@ def edit_subtitle(pattern, stream=0, **kwargs):
|
|
1553
1551
|
try:
|
1554
1552
|
return levels.subtitle(pattern, stream, ignore_case, max_count)
|
1555
1553
|
except LevelError as e:
|
1556
|
-
return raise_(e) if strict else levels.all()
|
1554
|
+
return raise_(e) if levels.strict else levels.all()
|
1557
1555
|
|
1558
1556
|
|
1559
1557
|
def my_eval(env: Env, node: object) -> Any:
|
auto_editor/lib/contracts.py
CHANGED
@@ -1,15 +1,18 @@
|
|
1
1
|
from __future__ import annotations
|
2
2
|
|
3
|
-
from collections.abc import Callable
|
4
3
|
from dataclasses import dataclass
|
5
4
|
from fractions import Fraction
|
6
|
-
from typing import
|
5
|
+
from typing import TYPE_CHECKING
|
7
6
|
|
8
7
|
from numpy import float64
|
9
8
|
|
10
9
|
from .data_structs import Sym, print_str
|
11
10
|
from .err import MyError
|
12
11
|
|
12
|
+
if TYPE_CHECKING:
|
13
|
+
from collections.abc import Callable
|
14
|
+
from typing import Any
|
15
|
+
|
13
16
|
|
14
17
|
@dataclass(slots=True)
|
15
18
|
class Contract:
|
auto_editor/make_layers.py
CHANGED
@@ -6,7 +6,7 @@ from typing import TYPE_CHECKING, NamedTuple
|
|
6
6
|
|
7
7
|
import numpy as np
|
8
8
|
|
9
|
-
from auto_editor.analyze import
|
9
|
+
from auto_editor.analyze import Levels
|
10
10
|
from auto_editor.ffwrapper import FileInfo
|
11
11
|
from auto_editor.lang.palet import Lexer, Parser, env, interpret, is_boolarr
|
12
12
|
from auto_editor.lib.data_structs import print_str
|
@@ -71,44 +71,6 @@ def make_av(src: FileInfo, all_clips: list[list[Clip]]) -> tuple[VSpace, ASpace]
|
|
71
71
|
return vtl, atl
|
72
72
|
|
73
73
|
|
74
|
-
def run_interpreter_for_edit_option(
|
75
|
-
text: str, filesetup: FileSetup
|
76
|
-
) -> NDArray[np.bool_]:
|
77
|
-
src = filesetup.src
|
78
|
-
tb = filesetup.tb
|
79
|
-
bar = filesetup.bar
|
80
|
-
temp = filesetup.temp
|
81
|
-
log = filesetup.log
|
82
|
-
|
83
|
-
try:
|
84
|
-
parser = Parser(Lexer("`--edit`", text))
|
85
|
-
if log.is_debug:
|
86
|
-
log.debug(f"edit: {parser}")
|
87
|
-
|
88
|
-
env["timebase"] = tb
|
89
|
-
env["@levels"] = Levels(src, tb, bar, temp, log)
|
90
|
-
env["@filesetup"] = filesetup
|
91
|
-
|
92
|
-
results = interpret(env, parser)
|
93
|
-
|
94
|
-
if len(results) == 0:
|
95
|
-
raise MyError("Expression in --edit must return a bool-array, got nothing")
|
96
|
-
|
97
|
-
result = results[-1]
|
98
|
-
if callable(result):
|
99
|
-
result = result()
|
100
|
-
|
101
|
-
if not is_boolarr(result):
|
102
|
-
raise MyError(
|
103
|
-
f"Expression in --edit must return a bool-array, got {print_str(result)}"
|
104
|
-
)
|
105
|
-
except MyError as e:
|
106
|
-
log.error(e)
|
107
|
-
|
108
|
-
assert isinstance(result, np.ndarray)
|
109
|
-
return result
|
110
|
-
|
111
|
-
|
112
74
|
def make_sane_timebase(fps: Fraction) -> Fraction:
|
113
75
|
tb = round(fps, 2)
|
114
76
|
|
@@ -140,7 +102,6 @@ def make_timeline(
|
|
140
102
|
args: Args,
|
141
103
|
sr: int,
|
142
104
|
bar: Bar,
|
143
|
-
temp: str,
|
144
105
|
log: Log,
|
145
106
|
) -> v3:
|
146
107
|
inp = None if not sources else sources[0]
|
@@ -159,20 +120,40 @@ def make_timeline(
|
|
159
120
|
except CoerceError as e:
|
160
121
|
log.error(e)
|
161
122
|
|
162
|
-
method = args.edit_based_on
|
163
|
-
|
164
123
|
has_loud = np.array([], dtype=np.bool_)
|
165
124
|
src_index = np.array([], dtype=np.int32)
|
166
125
|
concat = np.concatenate
|
167
126
|
|
168
127
|
for i, src in enumerate(sources):
|
169
|
-
|
128
|
+
try:
|
129
|
+
parser = Parser(Lexer("`--edit`", args.edit_based_on))
|
130
|
+
if log.is_debug:
|
131
|
+
log.debug(f"edit: {parser}")
|
132
|
+
|
133
|
+
env["timebase"] = tb
|
134
|
+
env["@levels"] = Levels(src, tb, bar, args.no_cache, log, len(sources) < 2)
|
135
|
+
|
136
|
+
results = interpret(env, parser)
|
137
|
+
|
138
|
+
if len(results) == 0:
|
139
|
+
log.error("Expression in --edit must return a bool-array, got nothing")
|
140
|
+
|
141
|
+
result = results[-1]
|
142
|
+
if callable(result):
|
143
|
+
result = result()
|
144
|
+
except MyError as e:
|
145
|
+
log.error(e)
|
146
|
+
|
147
|
+
if not is_boolarr(result):
|
148
|
+
log.error(
|
149
|
+
f"Expression in --edit must return a bool-array, got {print_str(result)}"
|
150
|
+
)
|
151
|
+
assert isinstance(result, np.ndarray)
|
170
152
|
|
171
|
-
|
172
|
-
mut_margin(edit_result, start_margin, end_margin)
|
153
|
+
mut_margin(result, start_margin, end_margin)
|
173
154
|
|
174
|
-
has_loud = concat((has_loud,
|
175
|
-
src_index = concat((src_index, np.full(len(
|
155
|
+
has_loud = concat((has_loud, result))
|
156
|
+
src_index = concat((src_index, np.full(len(result), i, dtype=np.int32)))
|
176
157
|
|
177
158
|
# Setup for handling custom speeds
|
178
159
|
speed_index = has_loud.astype(np.uint)
|
auto_editor/output.py
CHANGED
@@ -19,7 +19,6 @@ class Ensure:
|
|
19
19
|
_ffmpeg: FFmpeg
|
20
20
|
_bar: Bar
|
21
21
|
_sr: int
|
22
|
-
temp: str
|
23
22
|
log: Log
|
24
23
|
_audios: list[tuple[FileInfo, int]] = field(default_factory=list)
|
25
24
|
_subtitles: list[tuple[FileInfo, int, str]] = field(default_factory=list)
|
@@ -33,7 +32,7 @@ class Ensure:
|
|
33
32
|
label = len(self._audios) - 1
|
34
33
|
first_time = True
|
35
34
|
|
36
|
-
out_path = os.path.join(self.temp, f"{label:x}.wav")
|
35
|
+
out_path = os.path.join(self.log.temp, f"{label:x}.wav")
|
37
36
|
|
38
37
|
if first_time:
|
39
38
|
sample_rate = self._sr
|
@@ -83,7 +82,7 @@ class Ensure:
|
|
83
82
|
self._subtitles.append((src, stream, ext))
|
84
83
|
first_time = True
|
85
84
|
|
86
|
-
out_path = os.path.join(self.temp, f"{stream}s.{ext}")
|
85
|
+
out_path = os.path.join(self.log.temp, f"{stream}s.{ext}")
|
87
86
|
|
88
87
|
if first_time:
|
89
88
|
self.log.debug(f"Making external subtitle: {out_path}")
|
@@ -119,7 +118,6 @@ def mux_quality_media(
|
|
119
118
|
tb: Fraction,
|
120
119
|
args: Args,
|
121
120
|
src: FileInfo,
|
122
|
-
temp: str,
|
123
121
|
log: Log,
|
124
122
|
) -> None:
|
125
123
|
v_tracks = len(visual_output)
|
@@ -142,7 +140,7 @@ def mux_quality_media(
|
|
142
140
|
cmd.extend(["-i", path])
|
143
141
|
else:
|
144
142
|
# Merge all the audio a_tracks into one.
|
145
|
-
new_a_file = os.path.join(temp, "new_audio.wav")
|
143
|
+
new_a_file = os.path.join(log.temp, "new_audio.wav")
|
146
144
|
if a_tracks > 1:
|
147
145
|
new_cmd = []
|
148
146
|
for path in audio_output:
|
auto_editor/preview.py
CHANGED
@@ -48,7 +48,7 @@ def all_cuts(tl: v3, in_len: int) -> list[int]:
|
|
48
48
|
return cut_lens
|
49
49
|
|
50
50
|
|
51
|
-
def preview(tl: v3,
|
51
|
+
def preview(tl: v3, log: Log) -> None:
|
52
52
|
log.conwrite("")
|
53
53
|
tb = tl.tb
|
54
54
|
|
@@ -65,7 +65,7 @@ def preview(tl: v3, temp: str, log: Log) -> None:
|
|
65
65
|
|
66
66
|
in_len = 0
|
67
67
|
for src in all_sources:
|
68
|
-
in_len += Levels(src, tb, Bar("none"),
|
68
|
+
in_len += Levels(src, tb, Bar("none"), False, log, False).media_length
|
69
69
|
|
70
70
|
out_len = tl.out_len()
|
71
71
|
|
auto_editor/render/audio.py
CHANGED
@@ -166,7 +166,7 @@ def apply_audio_normalization(
|
|
166
166
|
|
167
167
|
|
168
168
|
def make_new_audio(
|
169
|
-
tl: v3, ensure: Ensure, args: Args, ffmpeg: FFmpeg, bar: Bar,
|
169
|
+
tl: v3, ensure: Ensure, args: Args, ffmpeg: FFmpeg, bar: Bar, log: Log
|
170
170
|
) -> list[str]:
|
171
171
|
sr = tl.sr
|
172
172
|
tb = tl.tb
|
@@ -176,6 +176,7 @@ def make_new_audio(
|
|
176
176
|
norm = parse_norm(args.audio_normalize, log)
|
177
177
|
|
178
178
|
af_tick = 0
|
179
|
+
temp = log.temp
|
179
180
|
|
180
181
|
if not tl.a or not tl.a[0]:
|
181
182
|
log.error("Trying to render empty audio timeline")
|
auto_editor/render/video.py
CHANGED
@@ -105,13 +105,7 @@ def make_image_cache(tl: v3) -> dict[tuple[FileInfo, int], np.ndarray]:
|
|
105
105
|
|
106
106
|
|
107
107
|
def render_av(
|
108
|
-
ffmpeg: FFmpeg,
|
109
|
-
tl: v3,
|
110
|
-
args: Args,
|
111
|
-
bar: Bar,
|
112
|
-
ctr: Container,
|
113
|
-
temp: str,
|
114
|
-
log: Log,
|
108
|
+
ffmpeg: FFmpeg, tl: v3, args: Args, bar: Bar, ctr: Container, log: Log
|
115
109
|
) -> tuple[str, bool]:
|
116
110
|
src = tl.src
|
117
111
|
cns: dict[FileInfo, av.container.InputContainer] = {}
|
@@ -121,6 +115,7 @@ def render_av(
|
|
121
115
|
|
122
116
|
target_pix_fmt = "yuv420p" # Reasonable default
|
123
117
|
img_cache = make_image_cache(tl)
|
118
|
+
temp = log.temp
|
124
119
|
|
125
120
|
first_src: FileInfo | None = None
|
126
121
|
for src in tl.sources:
|
auto_editor/subcommands/info.py
CHANGED
@@ -47,7 +47,7 @@ class VideoJson(TypedDict):
|
|
47
47
|
class AudioJson(TypedDict):
|
48
48
|
codec: str
|
49
49
|
samplerate: int
|
50
|
-
|
50
|
+
layout: str
|
51
51
|
duration: float
|
52
52
|
bitrate: int
|
53
53
|
lang: str | None
|
@@ -150,8 +150,8 @@ def main(sys_args: list[str] = sys.argv[1:]) -> None:
|
|
150
150
|
for track, a in enumerate(src.audios):
|
151
151
|
aud: AudioJson = {
|
152
152
|
"codec": a.codec,
|
153
|
+
"layout": a.layout,
|
153
154
|
"samplerate": a.samplerate,
|
154
|
-
"channels": a.channels,
|
155
155
|
"duration": a.duration,
|
156
156
|
"bitrate": a.bitrate,
|
157
157
|
"lang": a.lang,
|