auto-editor 26.3.1__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 -45
- auto_editor/cmds/levels.py +1 -1
- auto_editor/cmds/repl.py +2 -3
- auto_editor/cmds/test.py +328 -384
- auto_editor/edit.py +19 -2
- auto_editor/lang/palet.py +23 -27
- auto_editor/make_layers.py +28 -17
- auto_editor/preview.py +3 -2
- auto_editor/utils/types.py +2 -0
- {auto_editor-26.3.1.dist-info → auto_editor-26.3.2.dist-info}/METADATA +1 -1
- {auto_editor-26.3.1.dist-info → auto_editor-26.3.2.dist-info}/RECORD +17 -17
- {auto_editor-26.3.1.dist-info → auto_editor-26.3.2.dist-info}/WHEEL +1 -1
- {auto_editor-26.3.1.dist-info → auto_editor-26.3.2.dist-info}/LICENSE +0 -0
- {auto_editor-26.3.1.dist-info → auto_editor-26.3.2.dist-info}/entry_points.txt +0 -0
- {auto_editor-26.3.1.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,51 +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
|
-
|
172
|
+
audio_stream = container.streams.audio[0]
|
173
|
+
result = sum(1 for _ in iter_audio(audio_stream, self.tb))
|
174
|
+
container.seek(0)
|
183
175
|
self.log.debug(f"Audio Length: {result}")
|
184
176
|
return result
|
185
177
|
|
186
178
|
# If there's no audio, get length in video metadata.
|
187
|
-
|
188
|
-
|
189
|
-
self.log.error("Could not get media duration")
|
190
|
-
|
191
|
-
video = container.streams.video[0]
|
179
|
+
if not container.streams.video:
|
180
|
+
self.log.error("Could not get media duration")
|
192
181
|
|
193
|
-
|
194
|
-
|
195
|
-
|
196
|
-
|
197
|
-
|
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)
|
198
189
|
|
199
190
|
return dur
|
200
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
|
+
|
201
197
|
def none(self) -> NDArray[np.bool_]:
|
202
198
|
return np.ones(self.media_length, dtype=np.bool_)
|
203
199
|
|
@@ -208,7 +204,7 @@ class Levels:
|
|
208
204
|
if self.no_cache:
|
209
205
|
return None
|
210
206
|
|
211
|
-
key = obj_tag(
|
207
|
+
key = self.obj_tag(kind, obj)
|
212
208
|
cache_file = os.path.join(gettempdir(), f"ae-{__version__}", f"{key}.npz")
|
213
209
|
|
214
210
|
try:
|
@@ -223,11 +219,8 @@ class Levels:
|
|
223
219
|
return arr
|
224
220
|
|
225
221
|
workdir = os.path.join(gettempdir(), f"ae-{__version__}")
|
226
|
-
|
227
|
-
|
228
|
-
|
229
|
-
key = obj_tag(self.src.path, kind, self.tb, obj)
|
230
|
-
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")
|
231
224
|
|
232
225
|
try:
|
233
226
|
np.savez(cache_file, data=arr)
|
@@ -253,13 +246,13 @@ class Levels:
|
|
253
246
|
return arr
|
254
247
|
|
255
248
|
def audio(self, stream: int) -> NDArray[np.float32]:
|
256
|
-
|
249
|
+
container = self.container
|
250
|
+
if stream >= len(container.streams.audio):
|
257
251
|
raise LevelError(f"audio: audio stream '{stream}' does not exist.")
|
258
252
|
|
259
253
|
if (arr := self.read_cache("audio", (stream,))) is not None:
|
260
254
|
return arr
|
261
255
|
|
262
|
-
container = av.open(self.src.path, "r")
|
263
256
|
audio = container.streams.audio[stream]
|
264
257
|
|
265
258
|
if audio.duration is not None and audio.time_base is not None:
|
@@ -286,32 +279,30 @@ class Levels:
|
|
286
279
|
index += 1
|
287
280
|
|
288
281
|
bar.end()
|
282
|
+
container.seek(0)
|
289
283
|
assert len(result) > 0
|
290
284
|
return self.cache(result[:index], "audio", (stream,))
|
291
285
|
|
292
286
|
def motion(self, stream: int, blur: int, width: int) -> NDArray[np.float32]:
|
293
|
-
|
287
|
+
container = self.container
|
288
|
+
if stream >= len(container.streams.video):
|
294
289
|
raise LevelError(f"motion: video stream '{stream}' does not exist.")
|
295
290
|
|
296
291
|
mobj = (stream, width, blur)
|
297
292
|
if (arr := self.read_cache("motion", mobj)) is not None:
|
298
293
|
return arr
|
299
294
|
|
300
|
-
container = av.open(self.src.path, "r")
|
301
295
|
video = container.streams.video[stream]
|
302
|
-
|
303
296
|
inaccurate_dur = (
|
304
297
|
1024
|
305
298
|
if video.duration is None or video.time_base is None
|
306
299
|
else int(video.duration * video.time_base * self.tb)
|
307
300
|
)
|
308
|
-
|
309
301
|
bar = self.bar
|
310
302
|
bar.start(inaccurate_dur, "Analyzing motion")
|
311
303
|
|
312
304
|
result: NDArray[np.float32] = np.zeros(inaccurate_dur, dtype=np.float32)
|
313
305
|
index = 0
|
314
|
-
|
315
306
|
for value in iter_motion(video, self.tb, blur, width):
|
316
307
|
if index > len(result) - 1:
|
317
308
|
result = np.concatenate(
|
@@ -322,6 +313,7 @@ class Levels:
|
|
322
313
|
index += 1
|
323
314
|
|
324
315
|
bar.end()
|
316
|
+
container.seek(0)
|
325
317
|
return self.cache(result[:index], "motion", mobj)
|
326
318
|
|
327
319
|
def subtitle(
|
@@ -331,7 +323,8 @@ class Levels:
|
|
331
323
|
ignore_case: bool,
|
332
324
|
max_count: int | None,
|
333
325
|
) -> NDArray[np.bool_]:
|
334
|
-
|
326
|
+
container = self.container
|
327
|
+
if stream >= len(container.streams.subtitles):
|
335
328
|
raise LevelError(f"subtitle: subtitle stream '{stream}' does not exist.")
|
336
329
|
|
337
330
|
try:
|
@@ -339,9 +332,7 @@ class Levels:
|
|
339
332
|
re_pattern = re.compile(pattern, flags)
|
340
333
|
except re.error as e:
|
341
334
|
self.log.error(e)
|
342
|
-
|
343
335
|
try:
|
344
|
-
container = av.open(self.src.path, "r")
|
345
336
|
subtitle_stream = container.streams.subtitles[stream]
|
346
337
|
assert isinstance(subtitle_stream.time_base, Fraction)
|
347
338
|
except Exception as e:
|
@@ -392,6 +383,17 @@ class Levels:
|
|
392
383
|
result[san_start:san_end] = 1
|
393
384
|
count += 1
|
394
385
|
|
395
|
-
container.
|
396
|
-
|
386
|
+
container.seek(0)
|
397
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:
|
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__}")
|