auto-editor 26.3.1__tar.gz → 26.3.3__tar.gz
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-26.3.1 → auto_editor-26.3.3}/PKG-INFO +2 -2
- {auto_editor-26.3.1 → auto_editor-26.3.3}/README.md +1 -1
- auto_editor-26.3.3/auto_editor/__init__.py +1 -0
- {auto_editor-26.3.1 → auto_editor-26.3.3}/auto_editor/__main__.py +181 -55
- {auto_editor-26.3.1 → auto_editor-26.3.3}/auto_editor/analyze.py +47 -45
- {auto_editor-26.3.1 → auto_editor-26.3.3}/auto_editor/cmds/levels.py +1 -1
- {auto_editor-26.3.1 → auto_editor-26.3.3}/auto_editor/cmds/repl.py +2 -3
- {auto_editor-26.3.1 → auto_editor-26.3.3}/auto_editor/cmds/test.py +334 -386
- {auto_editor-26.3.1 → auto_editor-26.3.3}/auto_editor/edit.py +28 -9
- {auto_editor-26.3.1 → auto_editor-26.3.3}/auto_editor/lang/palet.py +23 -27
- {auto_editor-26.3.1 → auto_editor-26.3.3}/auto_editor/make_layers.py +30 -18
- {auto_editor-26.3.1 → auto_editor-26.3.3}/auto_editor/output.py +2 -2
- {auto_editor-26.3.1 → auto_editor-26.3.3}/auto_editor/preview.py +3 -2
- {auto_editor-26.3.1 → auto_editor-26.3.3}/auto_editor/render/audio.py +4 -1
- {auto_editor-26.3.1 → auto_editor-26.3.3}/auto_editor/render/video.py +1 -1
- {auto_editor-26.3.1 → auto_editor-26.3.3}/auto_editor/timeline.py +8 -1
- {auto_editor-26.3.1 → auto_editor-26.3.3}/auto_editor/utils/types.py +7 -116
- {auto_editor-26.3.1 → auto_editor-26.3.3}/auto_editor.egg-info/PKG-INFO +2 -2
- auto_editor-26.3.1/auto_editor/__init__.py +0 -1
- {auto_editor-26.3.1 → auto_editor-26.3.3}/LICENSE +0 -0
- {auto_editor-26.3.1 → auto_editor-26.3.3}/auto_editor/cmds/__init__.py +0 -0
- {auto_editor-26.3.1 → auto_editor-26.3.3}/auto_editor/cmds/cache.py +0 -0
- {auto_editor-26.3.1 → auto_editor-26.3.3}/auto_editor/cmds/desc.py +0 -0
- {auto_editor-26.3.1 → auto_editor-26.3.3}/auto_editor/cmds/info.py +0 -0
- {auto_editor-26.3.1 → auto_editor-26.3.3}/auto_editor/cmds/palet.py +0 -0
- {auto_editor-26.3.1 → auto_editor-26.3.3}/auto_editor/cmds/subdump.py +0 -0
- {auto_editor-26.3.1 → auto_editor-26.3.3}/auto_editor/ffwrapper.py +0 -0
- {auto_editor-26.3.1 → auto_editor-26.3.3}/auto_editor/formats/__init__.py +0 -0
- {auto_editor-26.3.1 → auto_editor-26.3.3}/auto_editor/formats/fcp11.py +0 -0
- {auto_editor-26.3.1 → auto_editor-26.3.3}/auto_editor/formats/fcp7.py +0 -0
- {auto_editor-26.3.1 → auto_editor-26.3.3}/auto_editor/formats/json.py +0 -0
- {auto_editor-26.3.1 → auto_editor-26.3.3}/auto_editor/formats/shotcut.py +0 -0
- {auto_editor-26.3.1 → auto_editor-26.3.3}/auto_editor/formats/utils.py +0 -0
- {auto_editor-26.3.1 → auto_editor-26.3.3}/auto_editor/help.py +0 -0
- {auto_editor-26.3.1 → auto_editor-26.3.3}/auto_editor/lang/__init__.py +0 -0
- {auto_editor-26.3.1 → auto_editor-26.3.3}/auto_editor/lang/json.py +0 -0
- {auto_editor-26.3.1 → auto_editor-26.3.3}/auto_editor/lang/libintrospection.py +0 -0
- {auto_editor-26.3.1 → auto_editor-26.3.3}/auto_editor/lang/libmath.py +0 -0
- {auto_editor-26.3.1 → auto_editor-26.3.3}/auto_editor/lang/stdenv.py +0 -0
- {auto_editor-26.3.1 → auto_editor-26.3.3}/auto_editor/lib/__init__.py +0 -0
- {auto_editor-26.3.1 → auto_editor-26.3.3}/auto_editor/lib/contracts.py +0 -0
- {auto_editor-26.3.1 → auto_editor-26.3.3}/auto_editor/lib/data_structs.py +0 -0
- {auto_editor-26.3.1 → auto_editor-26.3.3}/auto_editor/lib/err.py +0 -0
- {auto_editor-26.3.1 → auto_editor-26.3.3}/auto_editor/render/__init__.py +0 -0
- {auto_editor-26.3.1 → auto_editor-26.3.3}/auto_editor/render/subtitle.py +0 -0
- {auto_editor-26.3.1 → auto_editor-26.3.3}/auto_editor/utils/__init__.py +0 -0
- {auto_editor-26.3.1 → auto_editor-26.3.3}/auto_editor/utils/bar.py +0 -0
- {auto_editor-26.3.1 → auto_editor-26.3.3}/auto_editor/utils/chunks.py +0 -0
- {auto_editor-26.3.1 → auto_editor-26.3.3}/auto_editor/utils/cmdkw.py +0 -0
- {auto_editor-26.3.1 → auto_editor-26.3.3}/auto_editor/utils/container.py +0 -0
- {auto_editor-26.3.1 → auto_editor-26.3.3}/auto_editor/utils/func.py +0 -0
- {auto_editor-26.3.1 → auto_editor-26.3.3}/auto_editor/utils/log.py +0 -0
- {auto_editor-26.3.1 → auto_editor-26.3.3}/auto_editor/vanparse.py +0 -0
- {auto_editor-26.3.1 → auto_editor-26.3.3}/auto_editor/wavfile.py +0 -0
- {auto_editor-26.3.1 → auto_editor-26.3.3}/auto_editor.egg-info/SOURCES.txt +0 -0
- {auto_editor-26.3.1 → auto_editor-26.3.3}/auto_editor.egg-info/dependency_links.txt +0 -0
- {auto_editor-26.3.1 → auto_editor-26.3.3}/auto_editor.egg-info/entry_points.txt +0 -0
- {auto_editor-26.3.1 → auto_editor-26.3.3}/auto_editor.egg-info/requires.txt +0 -0
- {auto_editor-26.3.1 → auto_editor-26.3.3}/auto_editor.egg-info/top_level.txt +0 -0
- {auto_editor-26.3.1 → auto_editor-26.3.3}/docs/build.py +0 -0
- {auto_editor-26.3.1 → auto_editor-26.3.3}/pyproject.toml +0 -0
- {auto_editor-26.3.1 → auto_editor-26.3.3}/setup.cfg +0 -0
@@ -1,6 +1,6 @@
|
|
1
1
|
Metadata-Version: 2.2
|
2
2
|
Name: auto-editor
|
3
|
-
Version: 26.3.
|
3
|
+
Version: 26.3.3
|
4
4
|
Summary: Auto-Editor: Effort free video editing!
|
5
5
|
Author-email: WyattBlue <wyattblue@auto-editor.com>
|
6
6
|
License: Unlicense
|
@@ -21,7 +21,7 @@ Requires-Dist: pyav==14.2.*
|
|
21
21
|
---
|
22
22
|
|
23
23
|
[](https://github.com/wyattblue/auto-editor/actions)
|
24
|
-
|
24
|
+
[](https://github.com/astral-sh/ruff)
|
25
25
|
|
26
26
|
Before doing the real editing, you first cut out the "dead space" which is typically silence. This is known as a "first pass". Cutting these is a boring task, especially if the video is very long.
|
27
27
|
|
@@ -5,7 +5,7 @@
|
|
5
5
|
---
|
6
6
|
|
7
7
|
[](https://github.com/wyattblue/auto-editor/actions)
|
8
|
-
|
8
|
+
[](https://github.com/astral-sh/ruff)
|
9
9
|
|
10
10
|
Before doing the real editing, you first cut out the "dead space" which is typically silence. This is known as a "first pass". Cutting these is a boring task, especially if the video is very long.
|
11
11
|
|
@@ -0,0 +1 @@
|
|
1
|
+
__version__ = "26.3.3"
|
@@ -3,6 +3,9 @@
|
|
3
3
|
import platform as plat
|
4
4
|
import re
|
5
5
|
import sys
|
6
|
+
from dataclasses import dataclass, field
|
7
|
+
from fractions import Fraction
|
8
|
+
from io import StringIO
|
6
9
|
from os import environ
|
7
10
|
from os.path import exists, isdir, isfile, lexists, splitext
|
8
11
|
from subprocess import run
|
@@ -11,21 +14,127 @@ import auto_editor
|
|
11
14
|
from auto_editor.utils.func import get_stdout
|
12
15
|
from auto_editor.utils.log import Log
|
13
16
|
from auto_editor.utils.types import (
|
14
|
-
|
17
|
+
CoerceError,
|
15
18
|
frame_rate,
|
16
|
-
|
19
|
+
natural,
|
17
20
|
number,
|
18
21
|
parse_color,
|
19
|
-
|
20
|
-
sample_rate,
|
21
|
-
speed,
|
22
|
-
speed_range,
|
23
|
-
time_range,
|
22
|
+
split_num_str,
|
24
23
|
)
|
25
24
|
from auto_editor.vanparse import ArgumentParser
|
26
25
|
|
27
26
|
|
27
|
+
@dataclass(slots=True)
|
28
|
+
class Args:
|
29
|
+
input: list[str] = field(default_factory=list)
|
30
|
+
help: bool = False
|
31
|
+
|
32
|
+
# Editing Options
|
33
|
+
margin: tuple[str, str] = ("0.2s", "0.2s")
|
34
|
+
edit: str = "audio"
|
35
|
+
export: str | None = None
|
36
|
+
output: str | None = None
|
37
|
+
silent_speed: float = 99999.0
|
38
|
+
video_speed: float = 1.0
|
39
|
+
cut_out: list[tuple[str, str]] = field(default_factory=list)
|
40
|
+
add_in: list[tuple[str, str]] = field(default_factory=list)
|
41
|
+
set_speed_for_range: list[tuple[float, str, str]] = field(default_factory=list)
|
42
|
+
|
43
|
+
# Timeline Options
|
44
|
+
frame_rate: Fraction | None = None
|
45
|
+
sample_rate: int | None = None
|
46
|
+
resolution: tuple[int, int] | None = None
|
47
|
+
background: str = "#000000"
|
48
|
+
|
49
|
+
# URL download Options
|
50
|
+
yt_dlp_location: str = "yt-dlp"
|
51
|
+
download_format: str | None = None
|
52
|
+
output_format: str | None = None
|
53
|
+
yt_dlp_extras: str | None = None
|
54
|
+
|
55
|
+
# Display Options
|
56
|
+
progress: str = "modern"
|
57
|
+
debug: bool = False
|
58
|
+
quiet: bool = False
|
59
|
+
preview: bool = False
|
60
|
+
|
61
|
+
# Container Settings
|
62
|
+
sn: bool = False
|
63
|
+
dn: bool = False
|
64
|
+
faststart: bool = False
|
65
|
+
no_faststart: bool = False
|
66
|
+
fragmented: bool = False
|
67
|
+
no_fragmented: bool = False
|
68
|
+
|
69
|
+
# Video Rendering
|
70
|
+
video_codec: str = "auto"
|
71
|
+
video_bitrate: str = "auto"
|
72
|
+
vprofile: str | None = None
|
73
|
+
scale: float = 1.0
|
74
|
+
no_seek: bool = False
|
75
|
+
|
76
|
+
# Audio Rendering
|
77
|
+
audio_codec: str = "auto"
|
78
|
+
audio_bitrate: str = "auto"
|
79
|
+
keep_tracks_separate: bool = False
|
80
|
+
audio_normalize: str = "#f"
|
81
|
+
|
82
|
+
# Misc.
|
83
|
+
config: bool = False
|
84
|
+
no_cache: bool = False
|
85
|
+
no_open: bool = False
|
86
|
+
temp_dir: str | None = None
|
87
|
+
player: str | None = None
|
88
|
+
version: bool = False
|
89
|
+
|
90
|
+
|
28
91
|
def main_options(parser: ArgumentParser) -> ArgumentParser:
|
92
|
+
def margin(val: str) -> tuple[str, str]:
|
93
|
+
vals = val.strip().split(",")
|
94
|
+
if len(vals) == 1:
|
95
|
+
vals.append(vals[0])
|
96
|
+
if len(vals) != 2:
|
97
|
+
raise CoerceError("--margin has too many arguments.")
|
98
|
+
return vals[0], vals[1]
|
99
|
+
|
100
|
+
def speed(val: str) -> float:
|
101
|
+
_s = number(val)
|
102
|
+
if _s <= 0 or _s > 99999:
|
103
|
+
return 99999.0
|
104
|
+
return _s
|
105
|
+
|
106
|
+
def resolution(val: str | None) -> tuple[int, int] | None:
|
107
|
+
if val is None:
|
108
|
+
return None
|
109
|
+
vals = val.strip().split(",")
|
110
|
+
if len(vals) != 2:
|
111
|
+
raise CoerceError(f"'{val}': Resolution takes two numbers")
|
112
|
+
return natural(vals[0]), natural(vals[1])
|
113
|
+
|
114
|
+
def sample_rate(val: str) -> int:
|
115
|
+
num, unit = split_num_str(val)
|
116
|
+
if unit in {"kHz", "KHz"}:
|
117
|
+
return natural(num * 1000)
|
118
|
+
if unit not in {"", "Hz"}:
|
119
|
+
raise CoerceError(f"Unknown unit: '{unit}'")
|
120
|
+
return natural(num)
|
121
|
+
|
122
|
+
def _comma_coerce(name: str, val: str, num_args: int) -> list[str]:
|
123
|
+
vals = val.strip().split(",")
|
124
|
+
if num_args > len(vals):
|
125
|
+
raise CoerceError(f"Too few arguments for {name}.")
|
126
|
+
if len(vals) > num_args:
|
127
|
+
raise CoerceError(f"Too many arguments for {name}.")
|
128
|
+
return vals
|
129
|
+
|
130
|
+
def time_range(val: str) -> tuple[str, str]:
|
131
|
+
a = _comma_coerce("time_range", val, 2)
|
132
|
+
return a[0], a[1]
|
133
|
+
|
134
|
+
def speed_range(val: str) -> tuple[float, str, str]:
|
135
|
+
a = _comma_coerce("speed_range", val, 3)
|
136
|
+
return number(a[0]), a[1], a[2]
|
137
|
+
|
29
138
|
parser.add_required("input", nargs="*", metavar="[file | url ...] [options]")
|
30
139
|
parser.add_text("Editing Options:")
|
31
140
|
parser.add_argument(
|
@@ -40,6 +149,16 @@ def main_options(parser: ArgumentParser) -> ArgumentParser:
|
|
40
149
|
metavar="METHOD",
|
41
150
|
help="Set an expression which determines how to make auto edits",
|
42
151
|
)
|
152
|
+
parser.add_argument(
|
153
|
+
"--export", "-ex", metavar="EXPORT:ATTRS?", help="Choose the export mode"
|
154
|
+
)
|
155
|
+
parser.add_argument(
|
156
|
+
"--output",
|
157
|
+
"--output-file",
|
158
|
+
"-o",
|
159
|
+
metavar="FILE",
|
160
|
+
help="Set the name/path of the new output file",
|
161
|
+
)
|
43
162
|
parser.add_argument(
|
44
163
|
"--silent-speed",
|
45
164
|
"-s",
|
@@ -110,12 +229,6 @@ def main_options(parser: ArgumentParser) -> ArgumentParser:
|
|
110
229
|
metavar="COLOR",
|
111
230
|
help="Set the background as a solid RGB color",
|
112
231
|
)
|
113
|
-
parser.add_argument(
|
114
|
-
"--add",
|
115
|
-
nargs="*",
|
116
|
-
metavar="OBJ:START,DUR,ATTRS?",
|
117
|
-
help="Insert an audio/video object to the timeline",
|
118
|
-
)
|
119
232
|
parser.add_text("URL Download Options:")
|
120
233
|
parser.add_argument(
|
121
234
|
"--yt-dlp-location",
|
@@ -137,28 +250,6 @@ def main_options(parser: ArgumentParser) -> ArgumentParser:
|
|
137
250
|
metavar="CMD",
|
138
251
|
help="Add extra options for yt-dlp. Must be in quotes",
|
139
252
|
)
|
140
|
-
parser.add_text("Utility Options:")
|
141
|
-
parser.add_argument(
|
142
|
-
"--export", "-ex", metavar="EXPORT:ATTRS?", help="Choose the export mode"
|
143
|
-
)
|
144
|
-
parser.add_argument(
|
145
|
-
"--output-file",
|
146
|
-
"--output",
|
147
|
-
"-o",
|
148
|
-
metavar="FILE",
|
149
|
-
help="Set the name/path of the new output file",
|
150
|
-
)
|
151
|
-
parser.add_argument(
|
152
|
-
"--player", "-p", metavar="CMD", help="Set player to open output media files"
|
153
|
-
)
|
154
|
-
parser.add_argument(
|
155
|
-
"--no-open", flag=True, help="Do not open the output file after editing is done"
|
156
|
-
)
|
157
|
-
parser.add_argument(
|
158
|
-
"--temp-dir",
|
159
|
-
metavar="PATH",
|
160
|
-
help="Set where the temporary directory is located",
|
161
|
-
)
|
162
253
|
parser.add_text("Display Options:")
|
163
254
|
parser.add_argument(
|
164
255
|
"--progress",
|
@@ -174,6 +265,37 @@ def main_options(parser: ArgumentParser) -> ArgumentParser:
|
|
174
265
|
flag=True,
|
175
266
|
help="Show stats on how the input will be cut and halt",
|
176
267
|
)
|
268
|
+
parser.add_text("Container Settings:")
|
269
|
+
parser.add_argument(
|
270
|
+
"-sn",
|
271
|
+
flag=True,
|
272
|
+
help="Disable the inclusion of subtitle streams in the output file",
|
273
|
+
)
|
274
|
+
parser.add_argument(
|
275
|
+
"-dn",
|
276
|
+
flag=True,
|
277
|
+
help="Disable the inclusion of data streams in the output file",
|
278
|
+
)
|
279
|
+
parser.add_argument(
|
280
|
+
"--faststart",
|
281
|
+
flag=True,
|
282
|
+
help="Enable movflags +faststart, recommended for web (default)",
|
283
|
+
)
|
284
|
+
parser.add_argument(
|
285
|
+
"--no-faststart",
|
286
|
+
flag=True,
|
287
|
+
help="Disable movflags +faststart, will be faster for large files",
|
288
|
+
)
|
289
|
+
parser.add_argument(
|
290
|
+
"--fragmented",
|
291
|
+
flag=True,
|
292
|
+
help="Use fragmented mp4/mov to allow playback before video is complete\nSee: https://ffmpeg.org/ffmpeg-formats.html#Fragmentation",
|
293
|
+
)
|
294
|
+
parser.add_argument(
|
295
|
+
"--no-fragmented",
|
296
|
+
flag=True,
|
297
|
+
help="Do not use fragmented mp4/mov for better compatibility (default)",
|
298
|
+
)
|
177
299
|
parser.add_text("Video Rendering:")
|
178
300
|
parser.add_argument(
|
179
301
|
"--video-codec",
|
@@ -231,20 +353,21 @@ def main_options(parser: ArgumentParser) -> ArgumentParser:
|
|
231
353
|
)
|
232
354
|
parser.add_text("Miscellaneous:")
|
233
355
|
parser.add_argument(
|
234
|
-
"
|
235
|
-
flag=True,
|
236
|
-
help="Disable the inclusion of subtitle streams in the output file",
|
356
|
+
"--config", flag=True, help="When set, look for `config.pal` and run it"
|
237
357
|
)
|
238
358
|
parser.add_argument(
|
239
|
-
"-
|
240
|
-
flag=True,
|
241
|
-
help="Disable the inclusion of data streams in the output file",
|
359
|
+
"--no-cache", flag=True, help="Don't look for or write a cache file"
|
242
360
|
)
|
243
361
|
parser.add_argument(
|
244
|
-
"--
|
362
|
+
"--no-open", flag=True, help="Do not open the output file after editing is done"
|
245
363
|
)
|
246
364
|
parser.add_argument(
|
247
|
-
"--
|
365
|
+
"--temp-dir",
|
366
|
+
metavar="PATH",
|
367
|
+
help="Set where the temporary directory is located",
|
368
|
+
)
|
369
|
+
parser.add_argument(
|
370
|
+
"--player", "-p", metavar="CMD", help="Set player to open output media files"
|
248
371
|
)
|
249
372
|
parser.add_argument("--version", "-V", flag=True, help="Display version and halt")
|
250
373
|
return parser
|
@@ -320,23 +443,26 @@ def main() -> None:
|
|
320
443
|
)
|
321
444
|
|
322
445
|
if args.version:
|
323
|
-
print(auto_editor.__version__)
|
324
|
-
return
|
446
|
+
return print(auto_editor.__version__)
|
325
447
|
|
326
448
|
if args.debug and not args.input:
|
327
|
-
|
328
|
-
|
329
|
-
|
449
|
+
buf = StringIO()
|
450
|
+
buf.write(f"OS: {plat.system()} {plat.release()} {plat.machine().lower()}\n")
|
451
|
+
buf.write(f"Python: {plat.python_version()}\nPyAV: ")
|
330
452
|
try:
|
331
453
|
import av
|
332
|
-
|
333
|
-
license = av._core.library_meta["libavcodec"]["license"]
|
334
|
-
print(f"PyAV: {av.__version__} ({license})")
|
335
454
|
except (ModuleNotFoundError, ImportError):
|
336
|
-
|
337
|
-
|
338
|
-
|
339
|
-
|
455
|
+
buf.write("not found")
|
456
|
+
else:
|
457
|
+
try:
|
458
|
+
buf.write(f"{av.__version__} ")
|
459
|
+
license = av._core.library_meta["libavcodec"]["license"]
|
460
|
+
buf.write(f"({license})")
|
461
|
+
except AttributeError:
|
462
|
+
buf.write("error")
|
463
|
+
|
464
|
+
buf.write(f"\nAuto-Editor: {auto_editor.__version__}")
|
465
|
+
return print(buf.getvalue())
|
340
466
|
|
341
467
|
if not args.input:
|
342
468
|
log.error("You need to give auto-editor an input file.")
|
@@ -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)
|
@@ -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:
|
@@ -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__}")
|