auto-editor 24.31.1__py3-none-any.whl → 25.0.0__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 +12 -19
- auto_editor/analyze.py +26 -30
- auto_editor/edit.py +8 -11
- auto_editor/ffwrapper.py +1 -1
- 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/levels.py +4 -9
- auto_editor/subcommands/repl.py +4 -7
- auto_editor/subcommands/test.py +31 -32
- auto_editor/timeline.py +10 -12
- auto_editor/utils/container.py +4 -0
- auto_editor/utils/func.py +10 -26
- auto_editor/utils/log.py +35 -8
- auto_editor/utils/types.py +2 -1
- auto_editor/vanparse.py +1 -2
- {auto_editor-24.31.1.dist-info → auto_editor-25.0.0.dist-info}/METADATA +1 -1
- {auto_editor-24.31.1.dist-info → auto_editor-25.0.0.dist-info}/RECORD +27 -27
- {auto_editor-24.31.1.dist-info → auto_editor-25.0.0.dist-info}/LICENSE +0 -0
- {auto_editor-24.31.1.dist-info → auto_editor-25.0.0.dist-info}/WHEEL +0 -0
- {auto_editor-24.31.1.dist-info → auto_editor-25.0.0.dist-info}/entry_points.txt +0 -0
- {auto_editor-24.31.1.dist-info → auto_editor-25.0.0.dist-info}/top_level.txt +0 -0
auto_editor/__init__.py
CHANGED
@@ -1,2 +1 @@
|
|
1
|
-
__version__ = "
|
2
|
-
version = "24w31a"
|
1
|
+
__version__ = "25.0.0"
|
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:
|
@@ -314,15 +308,14 @@ def main() -> None:
|
|
314
308
|
print(f"OS: {plat.system()} {plat.release()} {plat.machine().lower()}")
|
315
309
|
print(f"Python: {plat.python_version()}")
|
316
310
|
print(f"PyAV: {av.__version__}")
|
317
|
-
print(f"Auto-Editor: {auto_editor.
|
311
|
+
print(f"Auto-Editor: {auto_editor.__version__}")
|
318
312
|
return
|
319
313
|
|
320
314
|
if not args.input:
|
321
315
|
log.error("You need to give auto-editor an input file.")
|
322
316
|
|
323
|
-
|
324
|
-
log = Log(args.debug, args.quiet,
|
325
|
-
log.debug(f"Temp Directory: {temp}")
|
317
|
+
is_machine = args.progress == "machine"
|
318
|
+
log = Log(args.debug, args.quiet, args.temp_dir, is_machine, no_color)
|
326
319
|
|
327
320
|
ffmpeg = FFmpeg(
|
328
321
|
args.ffmpeg_location,
|
@@ -333,7 +326,7 @@ def main() -> None:
|
|
333
326
|
paths = valid_input(args.input, ffmpeg, args, log)
|
334
327
|
|
335
328
|
try:
|
336
|
-
edit_media(paths, ffmpeg, args,
|
329
|
+
edit_media(paths, ffmpeg, args, log)
|
337
330
|
except KeyboardInterrupt:
|
338
331
|
log.error("Keyboard Interrupt")
|
339
332
|
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,22 @@ def edit_media(
|
|
263
261
|
sub_output = []
|
264
262
|
apply_later = False
|
265
263
|
|
266
|
-
ensure = Ensure(ffmpeg, bar, samplerate,
|
264
|
+
ensure = Ensure(ffmpeg, bar, samplerate, log)
|
267
265
|
|
268
266
|
if ctr.default_sub != "none" and not args.sn:
|
269
|
-
sub_output = make_new_subtitles(tl, ensure, temp)
|
267
|
+
sub_output = make_new_subtitles(tl, ensure, log.temp)
|
270
268
|
|
271
269
|
if ctr.default_aud != "none":
|
272
|
-
audio_output = make_new_audio(tl, ensure, args, ffmpeg, bar,
|
270
|
+
audio_output = make_new_audio(tl, ensure, args, ffmpeg, bar, log)
|
273
271
|
|
274
272
|
if ctr.default_vid != "none":
|
275
273
|
if tl.v:
|
276
|
-
out_path, apply_later = render_av(ffmpeg, tl, args, bar, ctr,
|
274
|
+
out_path, apply_later = render_av(ffmpeg, tl, args, bar, ctr, log)
|
277
275
|
visual_output.append((True, out_path))
|
278
276
|
|
279
277
|
for v, vid in enumerate(src.videos, start=1):
|
280
278
|
if ctr.allow_image and vid.codec in ("png", "mjpeg", "webp"):
|
281
|
-
out_path = os.path.join(temp, f"{v}.{vid.codec}")
|
279
|
+
out_path = os.path.join(log.temp, f"{v}.{vid.codec}")
|
282
280
|
# fmt: off
|
283
281
|
ffmpeg.run(["-i", f"{src.path}", "-map", "0:v", "-map", "-0:V",
|
284
282
|
"-c", "copy", out_path])
|
@@ -297,7 +295,6 @@ def edit_media(
|
|
297
295
|
tl.tb,
|
298
296
|
args,
|
299
297
|
src,
|
300
|
-
temp,
|
301
298
|
log,
|
302
299
|
)
|
303
300
|
|
auto_editor/ffwrapper.py
CHANGED
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:
|
@@ -19,7 +19,6 @@ from auto_editor.utils.cmdkw import (
|
|
19
19
|
pAttr,
|
20
20
|
pAttrs,
|
21
21
|
)
|
22
|
-
from auto_editor.utils.func import setup_tempdir
|
23
22
|
from auto_editor.utils.log import Log
|
24
23
|
from auto_editor.utils.types import frame_rate
|
25
24
|
from auto_editor.vanparse import ArgumentParser
|
@@ -72,14 +71,11 @@ def print_arr(arr: NDArray) -> None:
|
|
72
71
|
print("")
|
73
72
|
|
74
73
|
|
75
|
-
def print_arr_gen(arr: Iterator[
|
74
|
+
def print_arr_gen(arr: Iterator[float | np.float32]) -> None:
|
76
75
|
print("")
|
77
76
|
print("@start")
|
78
77
|
for a in arr:
|
79
|
-
|
80
|
-
print(f"{a:.20f}")
|
81
|
-
else:
|
82
|
-
print(a)
|
78
|
+
print(f"{a:.20f}")
|
83
79
|
print("")
|
84
80
|
|
85
81
|
|
@@ -88,8 +84,7 @@ def main(sys_args: list[str] = sys.argv[1:]) -> None:
|
|
88
84
|
args = parser.parse_args(LevelArgs, sys_args)
|
89
85
|
|
90
86
|
bar = Bar("none")
|
91
|
-
|
92
|
-
log = Log(quiet=True, temp=temp)
|
87
|
+
log = Log(quiet=True)
|
93
88
|
|
94
89
|
sources = [initFileInfo(path, log) for path in args.input]
|
95
90
|
if len(sources) < 1:
|
@@ -132,7 +127,7 @@ def main(sys_args: list[str] = sys.argv[1:]) -> None:
|
|
132
127
|
except ParserError as e:
|
133
128
|
log.error(e)
|
134
129
|
|
135
|
-
levels = Levels(src, tb, bar,
|
130
|
+
levels = Levels(src, tb, bar, False, log, strict=True)
|
136
131
|
try:
|
137
132
|
if method == "audio":
|
138
133
|
print_arr_gen(iter_audio(src, tb, **obj))
|
auto_editor/subcommands/repl.py
CHANGED
@@ -5,13 +5,12 @@ from dataclasses import dataclass, field
|
|
5
5
|
from fractions import Fraction
|
6
6
|
|
7
7
|
import auto_editor
|
8
|
-
from auto_editor.analyze import
|
8
|
+
from auto_editor.analyze import Levels
|
9
9
|
from auto_editor.ffwrapper import initFileInfo
|
10
10
|
from auto_editor.lang.palet import ClosingError, Lexer, Parser, env, interpret
|
11
11
|
from auto_editor.lib.data_structs import print_str
|
12
12
|
from auto_editor.lib.err import MyError
|
13
13
|
from auto_editor.utils.bar import Bar
|
14
|
-
from auto_editor.utils.func import setup_tempdir
|
15
14
|
from auto_editor.utils.log import Log
|
16
15
|
from auto_editor.utils.types import frame_rate
|
17
16
|
from auto_editor.vanparse import ArgumentParser
|
@@ -59,18 +58,16 @@ def main(sys_args: list[str] = sys.argv[1:]) -> None:
|
|
59
58
|
args = repl_options(ArgumentParser(None)).parse_args(REPL_Args, sys_args)
|
60
59
|
|
61
60
|
if args.input:
|
62
|
-
|
63
|
-
log = Log(quiet=True, temp=temp)
|
61
|
+
log = Log(quiet=True, temp_dir=args.temp_dir)
|
64
62
|
strict = len(args.input) < 2
|
65
63
|
sources = [initFileInfo(path, log) for path in args.input]
|
66
64
|
src = sources[0]
|
67
65
|
tb = src.get_fps() if args.timebase is None else args.timebase
|
68
66
|
bar = Bar("modern")
|
69
67
|
env["timebase"] = tb
|
70
|
-
env["@levels"] = Levels(src, tb, bar,
|
71
|
-
env["@filesetup"] = FileSetup(src, strict, tb, bar, temp, log)
|
68
|
+
env["@levels"] = Levels(src, tb, bar, False, log, strict)
|
72
69
|
|
73
|
-
print(f"Auto-Editor {auto_editor.
|
70
|
+
print(f"Auto-Editor {auto_editor.__version__}")
|
74
71
|
text = None
|
75
72
|
|
76
73
|
try:
|
auto_editor/subcommands/test.py
CHANGED
@@ -1,15 +1,13 @@
|
|
1
|
-
# type: ignore
|
2
1
|
from __future__ import annotations
|
3
2
|
|
4
3
|
import os
|
5
4
|
import shutil
|
6
5
|
import subprocess
|
7
6
|
import sys
|
8
|
-
from collections.abc import Callable
|
9
7
|
from dataclasses import dataclass, field
|
10
8
|
from fractions import Fraction
|
11
9
|
from time import perf_counter
|
12
|
-
from typing import
|
10
|
+
from typing import TYPE_CHECKING
|
13
11
|
|
14
12
|
import numpy as np
|
15
13
|
|
@@ -20,6 +18,12 @@ from auto_editor.lib.err import MyError
|
|
20
18
|
from auto_editor.utils.log import Log
|
21
19
|
from auto_editor.vanparse import ArgumentParser
|
22
20
|
|
21
|
+
if TYPE_CHECKING:
|
22
|
+
from collections.abc import Callable
|
23
|
+
from typing import Any
|
24
|
+
|
25
|
+
from auto_editor.vanparse import ArgumentParser
|
26
|
+
|
23
27
|
|
24
28
|
@dataclass(slots=True)
|
25
29
|
class TestArgs:
|
@@ -29,7 +33,7 @@ class TestArgs:
|
|
29
33
|
category: str = "cli"
|
30
34
|
|
31
35
|
|
32
|
-
def test_options(parser):
|
36
|
+
def test_options(parser: ArgumentParser) -> ArgumentParser:
|
33
37
|
parser.add_argument("--only", "-n", nargs="*")
|
34
38
|
parser.add_argument("--no-fail-fast", flag=True)
|
35
39
|
parser.add_required(
|
@@ -47,14 +51,6 @@ def pipe_to_console(cmd: list[str]) -> tuple[int, str, str]:
|
|
47
51
|
return process.returncode, stdout.decode("utf-8"), stderr.decode("utf-8")
|
48
52
|
|
49
53
|
|
50
|
-
class Checker:
|
51
|
-
def __init__(self, log: Log):
|
52
|
-
self.log = log
|
53
|
-
|
54
|
-
def check(self, path: str) -> FileInfo:
|
55
|
-
return initFileInfo(path, self.log)
|
56
|
-
|
57
|
-
|
58
54
|
class Runner:
|
59
55
|
def __init__(self) -> None:
|
60
56
|
self.program = [sys.executable, "-m", "auto_editor"]
|
@@ -176,7 +172,10 @@ def main(sys_args: list[str] | None = None):
|
|
176
172
|
args = test_options(ArgumentParser("test")).parse_args(TestArgs, sys_args)
|
177
173
|
|
178
174
|
run = Runner()
|
179
|
-
|
175
|
+
log = Log()
|
176
|
+
|
177
|
+
def fileinfo(path: str) -> FileInfo:
|
178
|
+
return initFileInfo(path, log)
|
180
179
|
|
181
180
|
### Tests ###
|
182
181
|
|
@@ -229,7 +228,7 @@ def main(sys_args: list[str] | None = None):
|
|
229
228
|
|
230
229
|
def example():
|
231
230
|
out = run.main(inputs=["example.mp4"], cmd=[])
|
232
|
-
cn =
|
231
|
+
cn = fileinfo(out)
|
233
232
|
video = cn.videos[0]
|
234
233
|
|
235
234
|
assert video.fps == 30
|
@@ -295,7 +294,7 @@ def main(sys_args: list[str] | None = None):
|
|
295
294
|
out = run.main(
|
296
295
|
["resources/only-video/man-on-green-screen.gif"], ["--edit", "none"]
|
297
296
|
)
|
298
|
-
assert
|
297
|
+
assert fileinfo(out).videos[0].codec == "gif"
|
299
298
|
|
300
299
|
return out
|
301
300
|
|
@@ -319,11 +318,11 @@ def main(sys_args: list[str] | None = None):
|
|
319
318
|
out = run.main(inputs=["example.mp4"], cmd=[], output="out")
|
320
319
|
|
321
320
|
assert out == "out.mp4"
|
322
|
-
assert
|
321
|
+
assert fileinfo(out).videos[0].codec == "h264"
|
323
322
|
|
324
323
|
out = run.main(inputs=["resources/testsrc.mkv"], cmd=[], output="out")
|
325
324
|
assert out == "out.mkv"
|
326
|
-
assert
|
325
|
+
assert fileinfo(out).videos[0].codec == "h264"
|
327
326
|
|
328
327
|
return "out.mp4", "out.mkv"
|
329
328
|
|
@@ -357,14 +356,14 @@ def main(sys_args: list[str] | None = None):
|
|
357
356
|
run.main(["example.mp4"], ["--export", 'premiere:name="Foo Bar"'])
|
358
357
|
|
359
358
|
def resolution_and_scale():
|
360
|
-
cn =
|
359
|
+
cn = fileinfo(run.main(["example.mp4"], ["--scale", "1.5"]))
|
361
360
|
|
362
361
|
assert cn.videos[0].fps == 30
|
363
362
|
assert cn.videos[0].width == 1920
|
364
363
|
assert cn.videos[0].height == 1080
|
365
364
|
assert cn.audios[0].samplerate == 48000
|
366
365
|
|
367
|
-
cn =
|
366
|
+
cn = fileinfo(run.main(["example.mp4"], ["--scale", "0.2"]))
|
368
367
|
|
369
368
|
assert cn.videos[0].fps == 30
|
370
369
|
assert cn.videos[0].width == 256
|
@@ -372,7 +371,7 @@ def main(sys_args: list[str] | None = None):
|
|
372
371
|
assert cn.audios[0].samplerate == 48000
|
373
372
|
|
374
373
|
out = run.main(["example.mp4"], ["-res", "700,380", "-b", "darkgreen"])
|
375
|
-
cn =
|
374
|
+
cn = fileinfo(out)
|
376
375
|
|
377
376
|
assert cn.videos[0].fps == 30
|
378
377
|
assert cn.videos[0].width == 700
|
@@ -416,7 +415,7 @@ def main(sys_args: list[str] | None = None):
|
|
416
415
|
["--edit", "audio:stream=1"],
|
417
416
|
"out.mov",
|
418
417
|
)
|
419
|
-
assert len(
|
418
|
+
assert len(fileinfo(out).audios) == 1
|
420
419
|
|
421
420
|
return out
|
422
421
|
|
@@ -427,7 +426,7 @@ def main(sys_args: list[str] | None = None):
|
|
427
426
|
|
428
427
|
def concat_mux_tracks():
|
429
428
|
out = run.main(["example.mp4", "resources/multi-track.mov"], [], "out.mov")
|
430
|
-
assert len(
|
429
|
+
assert len(fileinfo(out).audios) == 1
|
431
430
|
|
432
431
|
return out
|
433
432
|
|
@@ -437,30 +436,30 @@ def main(sys_args: list[str] | None = None):
|
|
437
436
|
["--keep-tracks-separate"],
|
438
437
|
"out.mov",
|
439
438
|
)
|
440
|
-
assert len(
|
439
|
+
assert len(fileinfo(out).audios) == 2
|
441
440
|
out = run.main(
|
442
441
|
["example.mp4", "resources/multi-track.mov"],
|
443
442
|
["--keep-tracks-separate"],
|
444
443
|
"out.mov",
|
445
444
|
)
|
446
|
-
assert len(
|
445
|
+
assert len(fileinfo(out).audios) == 2
|
447
446
|
|
448
447
|
return out
|
449
448
|
|
450
449
|
def frame_rate():
|
451
|
-
cn =
|
450
|
+
cn = fileinfo(run.main(["example.mp4"], ["-r", "15", "--no-seek"]))
|
452
451
|
video = cn.videos[0]
|
453
452
|
assert video.fps == 15
|
454
453
|
assert video.time_base == Fraction(1, 15)
|
455
454
|
assert float(video.duration) - 17.33333333333333333333333 < 3
|
456
455
|
|
457
|
-
cn =
|
456
|
+
cn = fileinfo(run.main(["example.mp4"], ["-r", "20"]))
|
458
457
|
video = cn.videos[0]
|
459
458
|
assert video.fps == 20
|
460
459
|
assert video.time_base == Fraction(1, 20)
|
461
460
|
assert float(video.duration) - 17.33333333333333333333333 < 2
|
462
461
|
|
463
|
-
cn =
|
462
|
+
cn = fileinfo(out := run.main(["example.mp4"], ["-r", "60"]))
|
464
463
|
video = cn.videos[0]
|
465
464
|
|
466
465
|
assert video.fps == 60
|
@@ -471,22 +470,22 @@ def main(sys_args: list[str] | None = None):
|
|
471
470
|
|
472
471
|
def embedded_image():
|
473
472
|
out1 = run.main(["resources/embedded-image/h264-png.mp4"], [])
|
474
|
-
cn =
|
473
|
+
cn = fileinfo(out1)
|
475
474
|
assert cn.videos[0].codec == "h264"
|
476
475
|
assert cn.videos[1].codec == "png"
|
477
476
|
|
478
477
|
out2 = run.main(["resources/embedded-image/h264-mjpeg.mp4"], [])
|
479
|
-
cn =
|
478
|
+
cn = fileinfo(out2)
|
480
479
|
assert cn.videos[0].codec == "h264"
|
481
480
|
assert cn.videos[1].codec == "mjpeg"
|
482
481
|
|
483
482
|
out3 = run.main(["resources/embedded-image/h264-png.mkv"], [])
|
484
|
-
cn =
|
483
|
+
cn = fileinfo(out3)
|
485
484
|
assert cn.videos[0].codec == "h264"
|
486
485
|
assert cn.videos[1].codec == "png"
|
487
486
|
|
488
487
|
out4 = run.main(["resources/embedded-image/h264-mjpeg.mkv"], [])
|
489
|
-
cn =
|
488
|
+
cn = fileinfo(out4)
|
490
489
|
assert cn.videos[0].codec == "h264"
|
491
490
|
assert cn.videos[1].codec == "mjpeg"
|
492
491
|
|
@@ -532,7 +531,7 @@ def main(sys_args: list[str] | None = None):
|
|
532
531
|
# Issue 280
|
533
532
|
def SAR():
|
534
533
|
out = run.main(["resources/SAR-2by3.mp4"], [])
|
535
|
-
assert
|
534
|
+
assert fileinfo(out).videos[0].sar == Fraction(2, 3)
|
536
535
|
|
537
536
|
return out
|
538
537
|
|
auto_editor/timeline.py
CHANGED
@@ -1,21 +1,19 @@
|
|
1
1
|
from __future__ import annotations
|
2
2
|
|
3
|
-
from collections.abc import Iterator
|
4
3
|
from dataclasses import dataclass
|
5
|
-
from
|
6
|
-
from typing import Any
|
4
|
+
from typing import TYPE_CHECKING
|
7
5
|
|
8
|
-
from auto_editor.ffwrapper import FileInfo
|
9
6
|
from auto_editor.lib.contracts import *
|
10
|
-
from auto_editor.utils.chunks import Chunks
|
11
7
|
from auto_editor.utils.cmdkw import Required, pAttr, pAttrs
|
12
|
-
from auto_editor.utils.types import
|
13
|
-
|
14
|
-
|
15
|
-
|
16
|
-
|
17
|
-
|
18
|
-
|
8
|
+
from auto_editor.utils.types import anchor, color, natural, number, threshold
|
9
|
+
|
10
|
+
if TYPE_CHECKING:
|
11
|
+
from collections.abc import Iterator
|
12
|
+
from fractions import Fraction
|
13
|
+
from typing import Any
|
14
|
+
|
15
|
+
from auto_editor.ffwrapper import FileInfo
|
16
|
+
from auto_editor.utils.chunks import Chunks
|
19
17
|
|
20
18
|
|
21
19
|
@dataclass(slots=True)
|
auto_editor/utils/container.py
CHANGED
@@ -83,6 +83,10 @@ def container_constructor(ext: str) -> Container:
|
|
83
83
|
vcodecs.add(codec)
|
84
84
|
if codec == "h264":
|
85
85
|
vcodecs.add("libx264")
|
86
|
+
if codec == "av1":
|
87
|
+
vcodecs.add("libsvtav1")
|
88
|
+
if codec == "hevc":
|
89
|
+
vcodecs.add("hevc_nvenc")
|
86
90
|
if kind == "audio":
|
87
91
|
acodecs.add(codec)
|
88
92
|
if kind == "subtitle":
|
auto_editor/utils/func.py
CHANGED
@@ -1,15 +1,19 @@
|
|
1
1
|
from __future__ import annotations
|
2
2
|
|
3
|
-
from
|
4
|
-
from fractions import Fraction
|
3
|
+
from typing import TYPE_CHECKING
|
5
4
|
|
6
5
|
import numpy as np
|
7
|
-
from numpy.typing import NDArray
|
8
6
|
|
9
|
-
|
7
|
+
if TYPE_CHECKING:
|
8
|
+
from collections.abc import Callable
|
9
|
+
from fractions import Fraction
|
10
10
|
|
11
|
-
|
12
|
-
|
11
|
+
from numpy.typing import NDArray
|
12
|
+
|
13
|
+
from auto_editor.utils.log import Log
|
14
|
+
|
15
|
+
BoolList = NDArray[np.bool_]
|
16
|
+
BoolOperand = Callable[[BoolList, BoolList], BoolList]
|
13
17
|
|
14
18
|
|
15
19
|
def boolop(a: BoolList, b: BoolList, call: BoolOperand) -> BoolList:
|
@@ -25,26 +29,6 @@ def boolop(a: BoolList, b: BoolList, call: BoolOperand) -> BoolList:
|
|
25
29
|
return call(a, b)
|
26
30
|
|
27
31
|
|
28
|
-
def setup_tempdir(temp: str | None, log: Log) -> str:
|
29
|
-
if temp is None:
|
30
|
-
import tempfile
|
31
|
-
|
32
|
-
return tempfile.mkdtemp()
|
33
|
-
|
34
|
-
import os.path
|
35
|
-
from os import listdir, mkdir
|
36
|
-
|
37
|
-
if os.path.isfile(temp):
|
38
|
-
log.error("Temp directory cannot be an already existing file.")
|
39
|
-
if os.path.isdir(temp):
|
40
|
-
if len(listdir(temp)) != 0:
|
41
|
-
log.error("Temp directory should be empty!")
|
42
|
-
else:
|
43
|
-
mkdir(temp)
|
44
|
-
|
45
|
-
return temp
|
46
|
-
|
47
|
-
|
48
32
|
def to_timecode(secs: float | Fraction, fmt: str) -> str:
|
49
33
|
sign = ""
|
50
34
|
if secs < 0:
|
auto_editor/utils/log.py
CHANGED
@@ -3,45 +3,72 @@ from __future__ import annotations
|
|
3
3
|
import sys
|
4
4
|
from datetime import timedelta
|
5
5
|
from shutil import get_terminal_size, rmtree
|
6
|
+
from tempfile import mkdtemp
|
6
7
|
from time import perf_counter, sleep
|
7
8
|
from typing import NoReturn
|
8
9
|
|
9
10
|
|
10
11
|
class Log:
|
11
|
-
__slots__ = ("is_debug", "quiet", "
|
12
|
+
__slots__ = ("is_debug", "quiet", "machine", "no_color", "_temp", "_ut", "_s")
|
12
13
|
|
13
14
|
def __init__(
|
14
15
|
self,
|
15
16
|
is_debug: bool = False,
|
16
17
|
quiet: bool = False,
|
17
|
-
|
18
|
+
temp_dir: str | None = None,
|
18
19
|
machine: bool = False,
|
19
20
|
no_color: bool = True,
|
20
21
|
):
|
21
22
|
self.is_debug = is_debug
|
22
23
|
self.quiet = quiet
|
23
|
-
self.temp = temp
|
24
24
|
self.machine = machine
|
25
25
|
self.no_color = no_color
|
26
|
-
self.
|
26
|
+
self._temp: str | None = None
|
27
|
+
self._ut = temp_dir
|
28
|
+
self._s = 0 if self.quiet or self.machine else perf_counter()
|
27
29
|
|
28
30
|
def debug(self, message: object) -> None:
|
29
31
|
if self.is_debug:
|
30
32
|
self.conwrite("")
|
31
33
|
sys.stderr.write(f"Debug: {message}\n")
|
32
34
|
|
35
|
+
@property
|
36
|
+
def temp(self) -> str:
|
37
|
+
if self._temp is not None:
|
38
|
+
return self._temp
|
39
|
+
|
40
|
+
if self._ut is None:
|
41
|
+
result = mkdtemp()
|
42
|
+
else:
|
43
|
+
import os.path
|
44
|
+
from os import listdir, mkdir
|
45
|
+
|
46
|
+
if os.path.isfile(self._ut):
|
47
|
+
self.error("Temp directory cannot be an already existing file.")
|
48
|
+
|
49
|
+
if os.path.isdir(self._ut):
|
50
|
+
if len(listdir(self._ut)) != 0:
|
51
|
+
self.error("Temp directory should be empty!")
|
52
|
+
else:
|
53
|
+
mkdir(self._ut)
|
54
|
+
result = self._ut
|
55
|
+
|
56
|
+
self.debug(f"Temp Directory: {result}")
|
57
|
+
self._temp = result
|
58
|
+
return result
|
59
|
+
|
33
60
|
def cleanup(self) -> None:
|
34
|
-
if self.
|
61
|
+
if self._temp is None:
|
35
62
|
return
|
36
63
|
try:
|
37
|
-
rmtree(self.
|
64
|
+
rmtree(self._temp)
|
38
65
|
self.debug("Removed Temp Directory.")
|
39
66
|
except FileNotFoundError:
|
40
67
|
pass
|
41
68
|
except PermissionError:
|
42
69
|
sleep(0.1)
|
43
70
|
try:
|
44
|
-
rmtree(self.
|
71
|
+
rmtree(self._temp)
|
45
72
|
self.debug("Removed Temp Directory.")
|
46
73
|
except Exception as e:
|
47
74
|
self.debug(f"Failed to delete temp dir:\n{e}")
|
@@ -65,7 +92,7 @@ class Log:
|
|
65
92
|
|
66
93
|
def stop_timer(self) -> None:
|
67
94
|
if not self.quiet and not self.machine:
|
68
|
-
second_len = round(perf_counter() - self.
|
95
|
+
second_len = round(perf_counter() - self._s, 2)
|
69
96
|
minute_len = timedelta(seconds=round(second_len))
|
70
97
|
|
71
98
|
sys.stdout.write(f"Finished. took {second_len} seconds ({minute_len})\n")
|
auto_editor/utils/types.py
CHANGED
@@ -217,7 +217,7 @@ def resolution(val: str | None) -> tuple[int, int] | None:
|
|
217
217
|
return natural(vals[0]), natural(vals[1])
|
218
218
|
|
219
219
|
|
220
|
-
@dataclass
|
220
|
+
@dataclass(slots=True)
|
221
221
|
class Args:
|
222
222
|
yt_dlp_location: str = "yt-dlp"
|
223
223
|
download_format: str | None = None
|
@@ -255,6 +255,7 @@ class Args:
|
|
255
255
|
show_ffmpeg_output: bool = False
|
256
256
|
quiet: bool = False
|
257
257
|
preview: bool = False
|
258
|
+
no_cache: bool = False
|
258
259
|
margin: tuple[str, str] = ("0.2s", "0.2s")
|
259
260
|
silent_speed: float = 99999.0
|
260
261
|
video_speed: float = 1.0
|
auto_editor/vanparse.py
CHANGED
@@ -4,7 +4,6 @@ import difflib
|
|
4
4
|
import re
|
5
5
|
import sys
|
6
6
|
import textwrap
|
7
|
-
from collections.abc import Iterator
|
8
7
|
from dataclasses import dataclass
|
9
8
|
from io import StringIO
|
10
9
|
from shutil import get_terminal_size
|
@@ -14,7 +13,7 @@ from auto_editor.utils.log import Log
|
|
14
13
|
from auto_editor.utils.types import CoerceError
|
15
14
|
|
16
15
|
if TYPE_CHECKING:
|
17
|
-
from collections.abc import Callable
|
16
|
+
from collections.abc import Callable, Iterator
|
18
17
|
from typing import Any, Literal, TypeVar
|
19
18
|
|
20
19
|
T = TypeVar("T")
|
@@ -1,15 +1,15 @@
|
|
1
|
-
auto_editor/__init__.py,sha256=
|
2
|
-
auto_editor/__main__.py,sha256=
|
3
|
-
auto_editor/analyze.py,sha256=
|
4
|
-
auto_editor/edit.py,sha256=
|
5
|
-
auto_editor/ffwrapper.py,sha256=
|
1
|
+
auto_editor/__init__.py,sha256=hNpKYQArhnrmhf1u3TNPTwtZw8XS7EeDs-TfBLusM-s,23
|
2
|
+
auto_editor/__main__.py,sha256=89pW_zaFB8WEyKTGadkALLuv_MFONIcfEu8t0jKqeWI,9774
|
3
|
+
auto_editor/analyze.py,sha256=pHoSZ_-wyV1hLJyJpPg9Ha7oecjdGHGJ0a4hbKnb9NY,11779
|
4
|
+
auto_editor/edit.py,sha256=ihyCi9YyIV-WYP81pOnc5jG8n-e8m5aMogg27XVxPVY,11268
|
5
|
+
auto_editor/ffwrapper.py,sha256=RU74pfZUMEIVdpvZVVoJt92AILBGkYaPLZbI94Y0i60,7832
|
6
6
|
auto_editor/help.py,sha256=BFiP7vBz42TUzum4-zaQIrV1OY7kHeN0pe0MPE0T5xw,7997
|
7
|
-
auto_editor/make_layers.py,sha256=
|
8
|
-
auto_editor/output.py,sha256=
|
9
|
-
auto_editor/preview.py,sha256=
|
10
|
-
auto_editor/timeline.py,sha256=
|
7
|
+
auto_editor/make_layers.py,sha256=ybTxPRD6bDbEX-7e9qu4OYUvYkrdNb4BjnN9hzwkRiQ,8496
|
8
|
+
auto_editor/output.py,sha256=D8NCJwwmcjDf5rvoBnWKu5XY7QtxF3thxbnTxKAxGu8,8043
|
9
|
+
auto_editor/preview.py,sha256=noWkgyzdE14zwG8ZDYxLDual5iVt6zYTt4HTe-QAgFY,3029
|
10
|
+
auto_editor/timeline.py,sha256=d9Qhup2pBwsj7j9kF-Iw22xHbQvGx-keDq2eLI11Mt4,7117
|
11
11
|
auto_editor/validate_input.py,sha256=JQt7J5xOBJDp6lnd2sQptpYhf7Z_hyxNEzLsE9E7LKU,2596
|
12
|
-
auto_editor/vanparse.py,sha256=
|
12
|
+
auto_editor/vanparse.py,sha256=f0vViZ-aYtDxEyVrFHJ5X2pPTQAfqtw3N2gZgzn51kU,10002
|
13
13
|
auto_editor/wavfile.py,sha256=7N2LX_WfFVRnoXrKveLvuyTYpIz2htpGqfCD8tR4kJ8,9168
|
14
14
|
auto_editor/formats/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
15
15
|
auto_editor/formats/fcp11.py,sha256=VwJWJs1qNDIVC8-pswipmKCk0e4V3LnE5fAMA0pPWVg,5857
|
@@ -20,36 +20,36 @@ auto_editor/formats/utils.py,sha256=GIZw28WHuCIaZ_zMI0v6Kxbq0QaIpbLsdSegdYwQxQ8,
|
|
20
20
|
auto_editor/lang/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
21
21
|
auto_editor/lang/json.py,sha256=OsNcYlfEj8ZLlzLK-gkLcrCCKI7mJz9rpe-6XLr4f9U,9231
|
22
22
|
auto_editor/lang/libmath.py,sha256=z33A161Oe6vYYK7R6pgYjdZZe63dQkN38Qf36TL3prg,847
|
23
|
-
auto_editor/lang/palet.py,sha256=
|
23
|
+
auto_editor/lang/palet.py,sha256=m22TQnKomUM2quxIH4bz7ya_gM166rbJ5pW7Ei-43IA,59712
|
24
24
|
auto_editor/lib/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
25
|
-
auto_editor/lib/contracts.py,sha256=
|
25
|
+
auto_editor/lib/contracts.py,sha256=a3ZT-bGMa3-UjWKKFrEwLayw2Gl-rhqd5Bmvmrj85oE,7413
|
26
26
|
auto_editor/lib/data_structs.py,sha256=xyB6aEcpdB9NNWp_dU3d2ds5Z8zOfHXNX4mNQLh2pNw,6977
|
27
27
|
auto_editor/lib/err.py,sha256=UlszQJdzMZwkbT8x3sY4GkCV_5x9yrd6uVVUzvA8iiI,35
|
28
28
|
auto_editor/render/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
29
|
-
auto_editor/render/audio.py,sha256=
|
29
|
+
auto_editor/render/audio.py,sha256=darKvlglNXkknSUaPnH-qEUyccM1Awnv03_Px4yY81I,8844
|
30
30
|
auto_editor/render/subtitle.py,sha256=g195kDN0LcwKlZeQMCflXPH5n_74iwCk1RPLSQ5eP34,4373
|
31
|
-
auto_editor/render/video.py,sha256=
|
31
|
+
auto_editor/render/video.py,sha256=gMVcLehC_QpdtIzNLR_7tv2CZmYeEWisS_5as4ceHV0,12971
|
32
32
|
auto_editor/subcommands/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
33
33
|
auto_editor/subcommands/desc.py,sha256=GDrKJYiHMaeTrplZAceXl1JwoqD78XsV2_5lc0Xd7po,869
|
34
34
|
auto_editor/subcommands/info.py,sha256=7Sgt9WR0rWxe7juCRKseMxW6gKv3z3voqFcL8-MOVVM,6927
|
35
|
-
auto_editor/subcommands/levels.py,sha256=
|
35
|
+
auto_editor/subcommands/levels.py,sha256=ZB8_9jbOA5s1AQUcUNZDiLAjyJOKl7Be2YeVWdkOr0Q,4071
|
36
36
|
auto_editor/subcommands/palet.py,sha256=tbQoRWoT4jR3yu0etGApfprM-oQgXIjC-rIY-QG3nM0,655
|
37
|
-
auto_editor/subcommands/repl.py,sha256=
|
37
|
+
auto_editor/subcommands/repl.py,sha256=OfxIOBjE7W12UemfaaxMnzHcmV5cUTt7g5328R7rAYU,3116
|
38
38
|
auto_editor/subcommands/subdump.py,sha256=af_XBf7kaevqHn1A71z8C-7x8pS5WKD9FE_ugkCw6rk,665
|
39
|
-
auto_editor/subcommands/test.py,sha256=
|
39
|
+
auto_editor/subcommands/test.py,sha256=Qg_zY1Cg8Mo7JXsUWFEJxJZAe5pkkXyK4ydnBVpG0I8,24992
|
40
40
|
auto_editor/utils/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
41
41
|
auto_editor/utils/bar.py,sha256=RJqkJ8gNr8op_Z-2hh48ExjSonmTPX-RshctK_itv14,3988
|
42
42
|
auto_editor/utils/chunks.py,sha256=J-eGKtEz68gFtRrj1kOSgH4Tj_Yz6prNQ7Xr-d9NQJw,52
|
43
43
|
auto_editor/utils/cmdkw.py,sha256=XApxw7FZBOEJV9N4LHhdw1GVfHbFfCjr-zCZ1gJsSvY,6002
|
44
|
-
auto_editor/utils/container.py,sha256=
|
44
|
+
auto_editor/utils/container.py,sha256=qSoS5d8JqLRH4_BIWfnJ-37eCKKe_J290yrUNULGT94,2643
|
45
45
|
auto_editor/utils/encoder.py,sha256=auNYo7HXbcU4iTUCc0LE5lpwFmSvdWvBm6-5KIaRK8w,2983
|
46
|
-
auto_editor/utils/func.py,sha256=
|
47
|
-
auto_editor/utils/log.py,sha256=
|
46
|
+
auto_editor/utils/func.py,sha256=kcxCOqe-tg6k-kxutIran8LpffRiHDjKB6rm-ngFiSU,4460
|
47
|
+
auto_editor/utils/log.py,sha256=M2QKeQHMRNLm3HMVUKedZPRprT2u5dipOStiO4miPBk,3613
|
48
48
|
auto_editor/utils/subtitle_tools.py,sha256=TjjVPiT8bWzZJcrZjF7ddpgfIsVkLE4LyxXzBswHAGU,693
|
49
|
-
auto_editor/utils/types.py,sha256=
|
50
|
-
auto_editor-
|
51
|
-
auto_editor-
|
52
|
-
auto_editor-
|
53
|
-
auto_editor-
|
54
|
-
auto_editor-
|
55
|
-
auto_editor-
|
49
|
+
auto_editor/utils/types.py,sha256=JdAwfuT9Ty_FXUm89GUTo0M8FPFrXbqnlk-g4pWP1_k,11609
|
50
|
+
auto_editor-25.0.0.dist-info/LICENSE,sha256=yiq99pWITHfqS0pbZMp7cy2dnbreTuvBwudsU-njvIM,1210
|
51
|
+
auto_editor-25.0.0.dist-info/METADATA,sha256=ucOoacVK0LCr-DKicxIkO9w6Zx2LWvlfMPgBiXucYkk,6137
|
52
|
+
auto_editor-25.0.0.dist-info/WHEEL,sha256=R0nc6qTxuoLk7ShA2_Y-UWkN8ZdfDBG2B6Eqpz2WXbs,91
|
53
|
+
auto_editor-25.0.0.dist-info/entry_points.txt,sha256=-H7zdTw4MqnAcwrN5xTNkGIhzZtJMxS9r6lTMeR9-aA,240
|
54
|
+
auto_editor-25.0.0.dist-info/top_level.txt,sha256=ky1HUkqq9i034c4CUU_0wBw0xZsxxyGEak1eTbdvpyA,12
|
55
|
+
auto_editor-25.0.0.dist-info/RECORD,,
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|