auto-editor 26.3.0__py3-none-any.whl → 26.3.2__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 -1
- auto_editor/__main__.py +37 -22
- auto_editor/analyze.py +47 -52
- auto_editor/cmds/levels.py +1 -3
- auto_editor/cmds/repl.py +2 -3
- auto_editor/cmds/test.py +336 -386
- auto_editor/edit.py +113 -28
- auto_editor/lang/palet.py +23 -27
- auto_editor/make_layers.py +28 -17
- auto_editor/preview.py +3 -2
- auto_editor/utils/bar.py +16 -10
- auto_editor/utils/log.py +1 -10
- auto_editor/utils/types.py +2 -0
- {auto_editor-26.3.0.dist-info → auto_editor-26.3.2.dist-info}/METADATA +2 -2
- {auto_editor-26.3.0.dist-info → auto_editor-26.3.2.dist-info}/RECORD +19 -19
- {auto_editor-26.3.0.dist-info → auto_editor-26.3.2.dist-info}/WHEEL +1 -1
- {auto_editor-26.3.0.dist-info → auto_editor-26.3.2.dist-info}/LICENSE +0 -0
- {auto_editor-26.3.0.dist-info → auto_editor-26.3.2.dist-info}/entry_points.txt +0 -0
- {auto_editor-26.3.0.dist-info → auto_editor-26.3.2.dist-info}/top_level.txt +0 -0
auto_editor/__init__.py
CHANGED
@@ -1 +1 @@
|
|
1
|
-
__version__ = "26.3.
|
1
|
+
__version__ = "26.3.2"
|
auto_editor/__main__.py
CHANGED
@@ -3,6 +3,7 @@
|
|
3
3
|
import platform as plat
|
4
4
|
import re
|
5
5
|
import sys
|
6
|
+
from io import StringIO
|
6
7
|
from os import environ
|
7
8
|
from os.path import exists, isdir, isfile, lexists, splitext
|
8
9
|
from subprocess import run
|
@@ -174,6 +175,27 @@ def main_options(parser: ArgumentParser) -> ArgumentParser:
|
|
174
175
|
flag=True,
|
175
176
|
help="Show stats on how the input will be cut and halt",
|
176
177
|
)
|
178
|
+
parser.add_text("Container Settings:")
|
179
|
+
parser.add_argument(
|
180
|
+
"-sn",
|
181
|
+
flag=True,
|
182
|
+
help="Disable the inclusion of subtitle streams in the output file",
|
183
|
+
)
|
184
|
+
parser.add_argument(
|
185
|
+
"-dn",
|
186
|
+
flag=True,
|
187
|
+
help="Disable the inclusion of data streams in the output file",
|
188
|
+
)
|
189
|
+
parser.add_argument(
|
190
|
+
"--fragmented",
|
191
|
+
flag=True,
|
192
|
+
help="Use fragmented mp4/mov to allow playback before video is complete\nSee: https://ffmpeg.org/ffmpeg-formats.html#Fragmentation",
|
193
|
+
)
|
194
|
+
parser.add_argument(
|
195
|
+
"--no-fragmented",
|
196
|
+
flag=True,
|
197
|
+
help="Do not use fragmented mp4/mov for better compatibility (default)",
|
198
|
+
)
|
177
199
|
parser.add_text("Video Rendering:")
|
178
200
|
parser.add_argument(
|
179
201
|
"--video-codec",
|
@@ -230,16 +252,6 @@ def main_options(parser: ArgumentParser) -> ArgumentParser:
|
|
230
252
|
help="Apply audio rendering to all audio tracks. Applied right before rendering the output file",
|
231
253
|
)
|
232
254
|
parser.add_text("Miscellaneous:")
|
233
|
-
parser.add_argument(
|
234
|
-
"-sn",
|
235
|
-
flag=True,
|
236
|
-
help="Disable the inclusion of subtitle streams in the output file",
|
237
|
-
)
|
238
|
-
parser.add_argument(
|
239
|
-
"-dn",
|
240
|
-
flag=True,
|
241
|
-
help="Disable the inclusion of data streams in the output file",
|
242
|
-
)
|
243
255
|
parser.add_argument(
|
244
256
|
"--config", flag=True, help="When set, look for `config.pal` and run it"
|
245
257
|
)
|
@@ -320,23 +332,26 @@ def main() -> None:
|
|
320
332
|
)
|
321
333
|
|
322
334
|
if args.version:
|
323
|
-
print(auto_editor.__version__)
|
324
|
-
return
|
335
|
+
return print(auto_editor.__version__)
|
325
336
|
|
326
337
|
if args.debug and not args.input:
|
327
|
-
|
328
|
-
|
329
|
-
|
338
|
+
buf = StringIO()
|
339
|
+
buf.write(f"OS: {plat.system()} {plat.release()} {plat.machine().lower()}\n")
|
340
|
+
buf.write(f"Python: {plat.python_version()}\nPyAV: ")
|
330
341
|
try:
|
331
342
|
import av
|
332
|
-
|
333
|
-
license = av._core.library_meta["libavcodec"]["license"]
|
334
|
-
print(f"PyAV: {av.__version__} ({license})")
|
335
343
|
except (ModuleNotFoundError, ImportError):
|
336
|
-
|
337
|
-
|
338
|
-
|
339
|
-
|
344
|
+
buf.write("not found")
|
345
|
+
else:
|
346
|
+
try:
|
347
|
+
buf.write(f"{av.__version__} ")
|
348
|
+
license = av._core.library_meta["libavcodec"]["license"]
|
349
|
+
buf.write(f"({license})")
|
350
|
+
except AttributeError:
|
351
|
+
buf.write("error")
|
352
|
+
|
353
|
+
buf.write(f"\nAuto-Editor: {auto_editor.__version__}")
|
354
|
+
return print(buf.getvalue())
|
340
355
|
|
341
356
|
if not args.input:
|
342
357
|
log.error("You need to give auto-editor an input file.")
|
auto_editor/analyze.py
CHANGED
@@ -19,7 +19,6 @@ from auto_editor import __version__
|
|
19
19
|
if TYPE_CHECKING:
|
20
20
|
from collections.abc import Iterator, Sequence
|
21
21
|
from fractions import Fraction
|
22
|
-
from pathlib import Path
|
23
22
|
|
24
23
|
from numpy.typing import NDArray
|
25
24
|
|
@@ -28,7 +27,7 @@ if TYPE_CHECKING:
|
|
28
27
|
from auto_editor.utils.log import Log
|
29
28
|
|
30
29
|
|
31
|
-
__all__ = ("LevelError", "
|
30
|
+
__all__ = ("LevelError", "initLevels", "iter_audio", "iter_motion")
|
32
31
|
|
33
32
|
|
34
33
|
class LevelError(Exception):
|
@@ -153,52 +152,48 @@ def iter_motion(
|
|
153
152
|
prev_index = index
|
154
153
|
|
155
154
|
|
156
|
-
def obj_tag(path: Path, kind: str, tb: Fraction, obj: Sequence[object]) -> str:
|
157
|
-
mod_time = int(path.stat().st_mtime)
|
158
|
-
key = f"{path.name}:{mod_time:x}:{tb}:" + ",".join(f"{v}" for v in obj)
|
159
|
-
part1 = sha1(key.encode()).hexdigest()[:16]
|
160
|
-
|
161
|
-
return f"{part1}{kind}"
|
162
|
-
|
163
|
-
|
164
155
|
@dataclass(slots=True)
|
165
156
|
class Levels:
|
166
|
-
|
157
|
+
container: av.container.InputContainer
|
158
|
+
name: str
|
159
|
+
mod_time: int
|
167
160
|
tb: Fraction
|
168
161
|
bar: Bar
|
169
162
|
no_cache: bool
|
170
163
|
log: Log
|
171
|
-
strict: bool
|
172
164
|
|
173
165
|
@property
|
174
166
|
def media_length(self) -> int:
|
175
|
-
|
167
|
+
container = self.container
|
168
|
+
if container.streams.audio:
|
176
169
|
if (arr := self.read_cache("audio", (0,))) is not None:
|
177
170
|
return len(arr)
|
178
171
|
|
179
|
-
|
180
|
-
|
181
|
-
|
182
|
-
result = sum(1 for _ in iter_audio(audio_stream, self.tb))
|
183
|
-
|
172
|
+
audio_stream = container.streams.audio[0]
|
173
|
+
result = sum(1 for _ in iter_audio(audio_stream, self.tb))
|
174
|
+
container.seek(0)
|
184
175
|
self.log.debug(f"Audio Length: {result}")
|
185
176
|
return result
|
186
177
|
|
187
178
|
# If there's no audio, get length in video metadata.
|
188
|
-
|
189
|
-
|
190
|
-
self.log.error("Could not get media duration")
|
191
|
-
|
192
|
-
video = container.streams.video[0]
|
179
|
+
if not container.streams.video:
|
180
|
+
self.log.error("Could not get media duration")
|
193
181
|
|
194
|
-
|
195
|
-
|
196
|
-
|
197
|
-
|
198
|
-
|
182
|
+
video = container.streams.video[0]
|
183
|
+
if video.duration is None or video.time_base is None:
|
184
|
+
dur = 0
|
185
|
+
else:
|
186
|
+
dur = int(video.duration * video.time_base * self.tb)
|
187
|
+
self.log.debug(f"Video duration: {dur}")
|
188
|
+
container.seek(0)
|
199
189
|
|
200
190
|
return dur
|
201
191
|
|
192
|
+
def obj_tag(self, kind: str, obj: Sequence[object]) -> str:
|
193
|
+
mod_time = self.mod_time
|
194
|
+
key = f"{self.name}:{mod_time:x}:{self.tb}:" + ",".join(f"{v}" for v in obj)
|
195
|
+
return f"{sha1(key.encode()).hexdigest()[:16]}{kind}"
|
196
|
+
|
202
197
|
def none(self) -> NDArray[np.bool_]:
|
203
198
|
return np.ones(self.media_length, dtype=np.bool_)
|
204
199
|
|
@@ -209,7 +204,7 @@ class Levels:
|
|
209
204
|
if self.no_cache:
|
210
205
|
return None
|
211
206
|
|
212
|
-
key = obj_tag(
|
207
|
+
key = self.obj_tag(kind, obj)
|
213
208
|
cache_file = os.path.join(gettempdir(), f"ae-{__version__}", f"{key}.npz")
|
214
209
|
|
215
210
|
try:
|
@@ -224,11 +219,8 @@ class Levels:
|
|
224
219
|
return arr
|
225
220
|
|
226
221
|
workdir = os.path.join(gettempdir(), f"ae-{__version__}")
|
227
|
-
|
228
|
-
|
229
|
-
|
230
|
-
key = obj_tag(self.src.path, kind, self.tb, obj)
|
231
|
-
cache_file = os.path.join(workdir, f"{key}.npz")
|
222
|
+
os.makedirs(workdir, exist_ok=True)
|
223
|
+
cache_file = os.path.join(workdir, f"{self.obj_tag(kind, obj)}.npz")
|
232
224
|
|
233
225
|
try:
|
234
226
|
np.savez(cache_file, data=arr)
|
@@ -254,18 +246,15 @@ class Levels:
|
|
254
246
|
return arr
|
255
247
|
|
256
248
|
def audio(self, stream: int) -> NDArray[np.float32]:
|
257
|
-
|
249
|
+
container = self.container
|
250
|
+
if stream >= len(container.streams.audio):
|
258
251
|
raise LevelError(f"audio: audio stream '{stream}' does not exist.")
|
259
252
|
|
260
253
|
if (arr := self.read_cache("audio", (stream,))) is not None:
|
261
254
|
return arr
|
262
255
|
|
263
|
-
container = av.open(self.src.path, "r")
|
264
256
|
audio = container.streams.audio[stream]
|
265
257
|
|
266
|
-
if audio.codec.experimental:
|
267
|
-
self.log.error(f"`{audio.codec.name}` is an experimental codec")
|
268
|
-
|
269
258
|
if audio.duration is not None and audio.time_base is not None:
|
270
259
|
inaccurate_dur = int(audio.duration * audio.time_base * self.tb)
|
271
260
|
elif container.duration is not None:
|
@@ -290,35 +279,30 @@ class Levels:
|
|
290
279
|
index += 1
|
291
280
|
|
292
281
|
bar.end()
|
282
|
+
container.seek(0)
|
293
283
|
assert len(result) > 0
|
294
284
|
return self.cache(result[:index], "audio", (stream,))
|
295
285
|
|
296
286
|
def motion(self, stream: int, blur: int, width: int) -> NDArray[np.float32]:
|
297
|
-
|
287
|
+
container = self.container
|
288
|
+
if stream >= len(container.streams.video):
|
298
289
|
raise LevelError(f"motion: video stream '{stream}' does not exist.")
|
299
290
|
|
300
291
|
mobj = (stream, width, blur)
|
301
292
|
if (arr := self.read_cache("motion", mobj)) is not None:
|
302
293
|
return arr
|
303
294
|
|
304
|
-
container = av.open(self.src.path, "r")
|
305
295
|
video = container.streams.video[stream]
|
306
|
-
|
307
|
-
if video.codec.experimental:
|
308
|
-
self.log.experimental(video.codec)
|
309
|
-
|
310
296
|
inaccurate_dur = (
|
311
297
|
1024
|
312
298
|
if video.duration is None or video.time_base is None
|
313
299
|
else int(video.duration * video.time_base * self.tb)
|
314
300
|
)
|
315
|
-
|
316
301
|
bar = self.bar
|
317
302
|
bar.start(inaccurate_dur, "Analyzing motion")
|
318
303
|
|
319
304
|
result: NDArray[np.float32] = np.zeros(inaccurate_dur, dtype=np.float32)
|
320
305
|
index = 0
|
321
|
-
|
322
306
|
for value in iter_motion(video, self.tb, blur, width):
|
323
307
|
if index > len(result) - 1:
|
324
308
|
result = np.concatenate(
|
@@ -329,6 +313,7 @@ class Levels:
|
|
329
313
|
index += 1
|
330
314
|
|
331
315
|
bar.end()
|
316
|
+
container.seek(0)
|
332
317
|
return self.cache(result[:index], "motion", mobj)
|
333
318
|
|
334
319
|
def subtitle(
|
@@ -338,7 +323,8 @@ class Levels:
|
|
338
323
|
ignore_case: bool,
|
339
324
|
max_count: int | None,
|
340
325
|
) -> NDArray[np.bool_]:
|
341
|
-
|
326
|
+
container = self.container
|
327
|
+
if stream >= len(container.streams.subtitles):
|
342
328
|
raise LevelError(f"subtitle: subtitle stream '{stream}' does not exist.")
|
343
329
|
|
344
330
|
try:
|
@@ -346,9 +332,7 @@ class Levels:
|
|
346
332
|
re_pattern = re.compile(pattern, flags)
|
347
333
|
except re.error as e:
|
348
334
|
self.log.error(e)
|
349
|
-
|
350
335
|
try:
|
351
|
-
container = av.open(self.src.path, "r")
|
352
336
|
subtitle_stream = container.streams.subtitles[stream]
|
353
337
|
assert isinstance(subtitle_stream.time_base, Fraction)
|
354
338
|
except Exception as e:
|
@@ -399,6 +383,17 @@ class Levels:
|
|
399
383
|
result[san_start:san_end] = 1
|
400
384
|
count += 1
|
401
385
|
|
402
|
-
container.
|
403
|
-
|
386
|
+
container.seek(0)
|
404
387
|
return result
|
388
|
+
|
389
|
+
|
390
|
+
def initLevels(
|
391
|
+
src: FileInfo, tb: Fraction, bar: Bar, no_cache: bool, log: Log
|
392
|
+
) -> Levels:
|
393
|
+
try:
|
394
|
+
container = av.open(src.path)
|
395
|
+
except av.FFmpegError as e:
|
396
|
+
log.error(e)
|
397
|
+
|
398
|
+
mod_time = int(src.path.stat().st_mtime)
|
399
|
+
return Levels(container, src.path.name, mod_time, tb, bar, no_cache, log)
|
auto_editor/cmds/levels.py
CHANGED
@@ -128,7 +128,7 @@ def main(sys_args: list[str] = sys.argv[1:]) -> None:
|
|
128
128
|
except ParserError as e:
|
129
129
|
log.error(e)
|
130
130
|
|
131
|
-
levels =
|
131
|
+
levels = initLevels(src, tb, bar, False, log)
|
132
132
|
try:
|
133
133
|
if method == "audio":
|
134
134
|
if (arr := levels.read_cache("audio", (obj["stream"],))) is not None:
|
@@ -136,7 +136,6 @@ def main(sys_args: list[str] = sys.argv[1:]) -> None:
|
|
136
136
|
else:
|
137
137
|
container = av.open(src.path, "r")
|
138
138
|
audio_stream = container.streams.audio[obj["stream"]]
|
139
|
-
log.experimental(audio_stream.codec)
|
140
139
|
|
141
140
|
values = []
|
142
141
|
|
@@ -158,7 +157,6 @@ def main(sys_args: list[str] = sys.argv[1:]) -> None:
|
|
158
157
|
else:
|
159
158
|
container = av.open(src.path, "r")
|
160
159
|
video_stream = container.streams.video[obj["stream"]]
|
161
|
-
log.experimental(video_stream.codec)
|
162
160
|
|
163
161
|
values = []
|
164
162
|
|
auto_editor/cmds/repl.py
CHANGED
@@ -6,7 +6,7 @@ from fractions import Fraction
|
|
6
6
|
from os import environ
|
7
7
|
|
8
8
|
import auto_editor
|
9
|
-
from auto_editor.analyze import
|
9
|
+
from auto_editor.analyze import initLevels
|
10
10
|
from auto_editor.ffwrapper import initFileInfo
|
11
11
|
from auto_editor.lang.palet import ClosingError, Lexer, Parser, env, interpret
|
12
12
|
from auto_editor.lang.stdenv import make_standard_env
|
@@ -61,12 +61,11 @@ def main(sys_args: list[str] = sys.argv[1:]) -> None:
|
|
61
61
|
|
62
62
|
if args.input:
|
63
63
|
log = Log(quiet=True, temp_dir=args.temp_dir)
|
64
|
-
strict = len(args.input) < 2
|
65
64
|
sources = [initFileInfo(path, log) for path in args.input]
|
66
65
|
src = sources[0]
|
67
66
|
tb = src.get_fps() if args.timebase is None else args.timebase
|
68
67
|
env["timebase"] = tb
|
69
|
-
env["@levels"] =
|
68
|
+
env["@levels"] = initLevels(src, tb, initBar("modern"), False, log)
|
70
69
|
|
71
70
|
env.update(make_standard_env())
|
72
71
|
print(f"Auto-Editor {auto_editor.__version__}")
|