auto-editor 26.0.1__py3-none-any.whl → 26.1.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 -1
- auto_editor/__main__.py +9 -24
- auto_editor/analyze.py +62 -49
- auto_editor/edit.py +5 -5
- auto_editor/ffwrapper.py +0 -28
- auto_editor/help.py +14 -16
- auto_editor/make_layers.py +3 -1
- auto_editor/render/audio.py +13 -45
- auto_editor/render/subtitle.py +1 -1
- auto_editor/subcommands/info.py +2 -0
- auto_editor/subcommands/levels.py +14 -3
- auto_editor/utils/log.py +6 -0
- auto_editor/utils/types.py +1 -2
- {auto_editor-26.0.1.dist-info → auto_editor-26.1.0.dist-info}/METADATA +3 -3
- {auto_editor-26.0.1.dist-info → auto_editor-26.1.0.dist-info}/RECORD +19 -19
- {auto_editor-26.0.1.dist-info → auto_editor-26.1.0.dist-info}/WHEEL +1 -1
- {auto_editor-26.0.1.dist-info → auto_editor-26.1.0.dist-info}/LICENSE +0 -0
- {auto_editor-26.0.1.dist-info → auto_editor-26.1.0.dist-info}/entry_points.txt +0 -0
- {auto_editor-26.0.1.dist-info → auto_editor-26.1.0.dist-info}/top_level.txt +0 -0
auto_editor/__init__.py
CHANGED
@@ -1 +1 @@
|
|
1
|
-
__version__ = "26.0
|
1
|
+
__version__ = "26.1.0"
|
auto_editor/__main__.py
CHANGED
@@ -8,7 +8,6 @@ from subprocess import run
|
|
8
8
|
|
9
9
|
import auto_editor
|
10
10
|
from auto_editor.edit import edit_media
|
11
|
-
from auto_editor.ffwrapper import FFmpeg
|
12
11
|
from auto_editor.utils.func import get_stdout
|
13
12
|
from auto_editor.utils.log import Log
|
14
13
|
from auto_editor.utils.types import (
|
@@ -34,13 +33,12 @@ def main_options(parser: ArgumentParser) -> ArgumentParser:
|
|
34
33
|
"-m",
|
35
34
|
type=margin,
|
36
35
|
metavar="LENGTH",
|
37
|
-
help='Set sections near "loud" as "loud" too if section is less than LENGTH away
|
36
|
+
help='Set sections near "loud" as "loud" too if section is less than LENGTH away',
|
38
37
|
)
|
39
38
|
parser.add_argument(
|
40
|
-
"--edit-based-on",
|
41
39
|
"--edit",
|
42
40
|
metavar="METHOD",
|
43
|
-
help="
|
41
|
+
help="Set an expression which determines how to make auto edits",
|
44
42
|
)
|
45
43
|
parser.add_argument(
|
46
44
|
"--silent-speed",
|
@@ -148,7 +146,7 @@ def main_options(parser: ArgumentParser) -> ArgumentParser:
|
|
148
146
|
"--output",
|
149
147
|
"-o",
|
150
148
|
metavar="FILE",
|
151
|
-
help="Set the name/path of the new output file
|
149
|
+
help="Set the name/path of the new output file",
|
152
150
|
)
|
153
151
|
parser.add_argument(
|
154
152
|
"--player", "-p", metavar="CMD", help="Set player to open output media files"
|
@@ -161,11 +159,6 @@ def main_options(parser: ArgumentParser) -> ArgumentParser:
|
|
161
159
|
metavar="PATH",
|
162
160
|
help="Set where the temporary directory is located",
|
163
161
|
)
|
164
|
-
parser.add_argument(
|
165
|
-
"--ffmpeg-location",
|
166
|
-
metavar="PATH",
|
167
|
-
help="Set a custom path to the ffmpeg location",
|
168
|
-
)
|
169
162
|
parser.add_text("Display Options:")
|
170
163
|
parser.add_argument(
|
171
164
|
"--progress",
|
@@ -241,11 +234,6 @@ def main_options(parser: ArgumentParser) -> ArgumentParser:
|
|
241
234
|
flag=True,
|
242
235
|
help="Disable the inclusion of data streams in the output file",
|
243
236
|
)
|
244
|
-
parser.add_argument(
|
245
|
-
"--extras",
|
246
|
-
metavar="CMD",
|
247
|
-
help="Add extra options for ffmpeg. Must be in quotes",
|
248
|
-
)
|
249
237
|
parser.add_argument(
|
250
238
|
"--config", flag=True, help="When set, look for `config.pal` and run it"
|
251
239
|
)
|
@@ -256,7 +244,7 @@ def main_options(parser: ArgumentParser) -> ArgumentParser:
|
|
256
244
|
return parser
|
257
245
|
|
258
246
|
|
259
|
-
def download_video(my_input: str, args: Args,
|
247
|
+
def download_video(my_input: str, args: Args, log: Log) -> str:
|
260
248
|
log.conwrite("Downloading video...")
|
261
249
|
|
262
250
|
def get_domain(url: str) -> str:
|
@@ -272,18 +260,15 @@ def download_video(my_input: str, args: Args, ffmpeg: FFmpeg, log: Log) -> str:
|
|
272
260
|
else:
|
273
261
|
output_format = args.output_format
|
274
262
|
|
275
|
-
|
276
|
-
|
277
|
-
cmd = ["--ffmpeg-location", ffmpeg.get_path("yt-dlp", log)]
|
278
|
-
|
263
|
+
cmd = []
|
279
264
|
if download_format is not None:
|
280
265
|
cmd.extend(["-f", download_format])
|
281
266
|
|
282
267
|
cmd.extend(["-o", output_format, my_input])
|
283
|
-
|
284
268
|
if args.yt_dlp_extras is not None:
|
285
269
|
cmd.extend(args.yt_dlp_extras.split(" "))
|
286
270
|
|
271
|
+
yt_dlp_path = args.yt_dlp_location
|
287
272
|
try:
|
288
273
|
location = get_stdout(
|
289
274
|
[yt_dlp_path, "--get-filename", "--no-warnings"] + cmd
|
@@ -326,6 +311,7 @@ def main() -> None:
|
|
326
311
|
({"--export-as-json"}, ["--export", "json"]),
|
327
312
|
({"--export-as-clip-sequence", "-excs"}, ["--export", "clip-sequence"]),
|
328
313
|
({"--keep-tracks-seperate"}, ["--keep-tracks-separate"]),
|
314
|
+
({"--edit-based-on"}, ["--edit"]),
|
329
315
|
],
|
330
316
|
)
|
331
317
|
|
@@ -352,11 +338,10 @@ def main() -> None:
|
|
352
338
|
is_machine = args.progress == "machine"
|
353
339
|
log = Log(args.debug, args.quiet, args.temp_dir, is_machine, no_color)
|
354
340
|
|
355
|
-
ffmpeg = FFmpeg(args.ffmpeg_location)
|
356
341
|
paths = []
|
357
342
|
for my_input in args.input:
|
358
343
|
if my_input.startswith("http://") or my_input.startswith("https://"):
|
359
|
-
paths.append(download_video(my_input, args,
|
344
|
+
paths.append(download_video(my_input, args, log))
|
360
345
|
else:
|
361
346
|
if not splitext(my_input)[1]:
|
362
347
|
if isdir(my_input):
|
@@ -370,7 +355,7 @@ def main() -> None:
|
|
370
355
|
paths.append(my_input)
|
371
356
|
|
372
357
|
try:
|
373
|
-
edit_media(paths,
|
358
|
+
edit_media(paths, args, log)
|
374
359
|
except KeyboardInterrupt:
|
375
360
|
log.error("Keyboard Interrupt")
|
376
361
|
log.cleanup()
|
auto_editor/analyze.py
CHANGED
@@ -27,6 +27,9 @@ if TYPE_CHECKING:
|
|
27
27
|
from auto_editor.utils.log import Log
|
28
28
|
|
29
29
|
|
30
|
+
__all__ = ("LevelError", "Levels", "iter_audio", "iter_motion")
|
31
|
+
|
32
|
+
|
30
33
|
class LevelError(Exception):
|
31
34
|
pass
|
32
35
|
|
@@ -69,45 +72,39 @@ def mut_remove_large(
|
|
69
72
|
active = False
|
70
73
|
|
71
74
|
|
72
|
-
def iter_audio(
|
75
|
+
def iter_audio(audio_stream: av.AudioStream, tb: Fraction) -> Iterator[np.float32]:
|
73
76
|
fifo = AudioFifo()
|
74
|
-
|
75
|
-
container = av.open(src.path, "r")
|
76
|
-
audio_stream = container.streams.audio[stream]
|
77
|
-
sample_rate = audio_stream.rate
|
77
|
+
sr = audio_stream.rate
|
78
78
|
|
79
|
-
|
80
|
-
|
79
|
+
exact_size = (1 / tb) * sr
|
80
|
+
accumulated_error = Fraction(0)
|
81
81
|
|
82
|
-
|
83
|
-
|
84
|
-
av.AudioFormat("flt"), audio_stream.layout, sample_rate
|
85
|
-
)
|
82
|
+
# Resample so that audio data is between [-1, 1]
|
83
|
+
resampler = av.AudioResampler(av.AudioFormat("flt"), audio_stream.layout, sr)
|
86
84
|
|
87
|
-
|
88
|
-
|
85
|
+
container = audio_stream.container
|
86
|
+
assert isinstance(container, av.container.InputContainer)
|
89
87
|
|
90
|
-
|
91
|
-
|
88
|
+
for frame in container.decode(audio_stream):
|
89
|
+
frame.pts = None # Skip time checks
|
92
90
|
|
93
|
-
|
94
|
-
|
95
|
-
current_size = round(size_with_error)
|
96
|
-
accumulated_error = size_with_error - current_size
|
91
|
+
for reframe in resampler.resample(frame):
|
92
|
+
fifo.write(reframe)
|
97
93
|
|
98
|
-
|
99
|
-
|
100
|
-
|
101
|
-
|
102
|
-
|
103
|
-
finally:
|
104
|
-
container.close()
|
94
|
+
while fifo.samples >= ceil(exact_size):
|
95
|
+
size_with_error = exact_size + accumulated_error
|
96
|
+
current_size = round(size_with_error)
|
97
|
+
accumulated_error = size_with_error - current_size
|
105
98
|
|
99
|
+
audio_chunk = fifo.read(current_size)
|
100
|
+
assert audio_chunk is not None
|
101
|
+
arr = audio_chunk.to_ndarray().flatten()
|
102
|
+
yield np.max(np.abs(arr))
|
106
103
|
|
107
|
-
def iter_motion(src, tb, stream: int, blur: int, width: int) -> Iterator[np.float32]:
|
108
|
-
container = av.open(src.path, "r")
|
109
104
|
|
110
|
-
|
105
|
+
def iter_motion(
|
106
|
+
video: av.VideoStream, tb: Fraction, blur: int, width: int
|
107
|
+
) -> Iterator[np.float32]:
|
111
108
|
video.thread_type = "AUTO"
|
112
109
|
|
113
110
|
prev_frame = None
|
@@ -125,6 +122,9 @@ def iter_motion(src, tb, stream: int, blur: int, width: int) -> Iterator[np.floa
|
|
125
122
|
graph.add("buffersink"),
|
126
123
|
).configure()
|
127
124
|
|
125
|
+
container = video.container
|
126
|
+
assert isinstance(container, av.container.InputContainer)
|
127
|
+
|
128
128
|
for unframe in container.decode(video):
|
129
129
|
if unframe.pts is None:
|
130
130
|
continue
|
@@ -151,8 +151,6 @@ def iter_motion(src, tb, stream: int, blur: int, width: int) -> Iterator[np.floa
|
|
151
151
|
prev_frame = current_frame
|
152
152
|
prev_index = index
|
153
153
|
|
154
|
-
container.close()
|
155
|
-
|
156
154
|
|
157
155
|
def obj_tag(path: Path, kind: str, tb: Fraction, obj: Sequence[object]) -> str:
|
158
156
|
mod_time = int(path.stat().st_mtime)
|
@@ -175,7 +173,11 @@ class Levels:
|
|
175
173
|
if (arr := self.read_cache("audio", (0,))) is not None:
|
176
174
|
return len(arr)
|
177
175
|
|
178
|
-
|
176
|
+
with av.open(self.src.path, "r") as container:
|
177
|
+
audio_stream = container.streams.audio[0]
|
178
|
+
self.log.experimental(audio_stream.codec)
|
179
|
+
result = sum(1 for _ in iter_audio(audio_stream, self.tb))
|
180
|
+
|
179
181
|
self.log.debug(f"Audio Length: {result}")
|
180
182
|
return result
|
181
183
|
|
@@ -239,21 +241,26 @@ class Levels:
|
|
239
241
|
if (arr := self.read_cache("audio", (stream,))) is not None:
|
240
242
|
return arr
|
241
243
|
|
242
|
-
|
243
|
-
|
244
|
-
|
245
|
-
|
246
|
-
|
247
|
-
|
248
|
-
|
249
|
-
|
244
|
+
container = av.open(self.src.path, "r")
|
245
|
+
audio = container.streams.audio[stream]
|
246
|
+
|
247
|
+
if audio.codec.experimental:
|
248
|
+
self.log.error(f"`{audio.codec.name}` is an experimental codec")
|
249
|
+
|
250
|
+
if audio.duration is not None and audio.time_base is not None:
|
251
|
+
inaccurate_dur = int(audio.duration * audio.time_base * self.tb)
|
252
|
+
elif container.duration is not None:
|
253
|
+
inaccurate_dur = int(container.duration / av.time_base * self.tb)
|
254
|
+
else:
|
255
|
+
inaccurate_dur = 1024
|
250
256
|
|
251
257
|
bar = self.bar
|
252
258
|
bar.start(inaccurate_dur, "Analyzing audio volume")
|
253
259
|
|
254
260
|
result = np.zeros((inaccurate_dur), dtype=np.float32)
|
255
261
|
index = 0
|
256
|
-
|
262
|
+
|
263
|
+
for value in iter_audio(audio, self.tb):
|
257
264
|
if index > len(result) - 1:
|
258
265
|
result = np.concatenate(
|
259
266
|
(result, np.zeros((len(result)), dtype=np.float32))
|
@@ -263,6 +270,7 @@ class Levels:
|
|
263
270
|
index += 1
|
264
271
|
|
265
272
|
bar.end()
|
273
|
+
assert len(result) > 0
|
266
274
|
return self.cache(result[:index], "audio", (stream,))
|
267
275
|
|
268
276
|
def motion(self, stream: int, blur: int, width: int) -> NDArray[np.float32]:
|
@@ -273,20 +281,25 @@ class Levels:
|
|
273
281
|
if (arr := self.read_cache("motion", mobj)) is not None:
|
274
282
|
return arr
|
275
283
|
|
276
|
-
|
277
|
-
|
278
|
-
|
279
|
-
|
280
|
-
|
281
|
-
|
282
|
-
|
284
|
+
container = av.open(self.src.path, "r")
|
285
|
+
video = container.streams.video[stream]
|
286
|
+
|
287
|
+
if video.codec.experimental:
|
288
|
+
self.log.experimental(video.codec)
|
289
|
+
|
290
|
+
inaccurate_dur = (
|
291
|
+
1024
|
292
|
+
if video.duration is None or video.time_base is None
|
293
|
+
else int(video.duration * video.time_base * self.tb)
|
294
|
+
)
|
283
295
|
|
284
296
|
bar = self.bar
|
285
297
|
bar.start(inaccurate_dur, "Analyzing motion")
|
286
298
|
|
287
299
|
result = np.zeros((inaccurate_dur), dtype=np.float32)
|
288
300
|
index = 0
|
289
|
-
|
301
|
+
|
302
|
+
for value in iter_motion(video, self.tb, blur, width):
|
290
303
|
if index > len(result) - 1:
|
291
304
|
result = np.concatenate(
|
292
305
|
(result, np.zeros((len(result)), dtype=np.float32))
|
auto_editor/edit.py
CHANGED
@@ -10,7 +10,7 @@ from typing import Any
|
|
10
10
|
import av
|
11
11
|
from av import AudioResampler
|
12
12
|
|
13
|
-
from auto_editor.ffwrapper import
|
13
|
+
from auto_editor.ffwrapper import FileInfo, initFileInfo
|
14
14
|
from auto_editor.lib.contracts import is_int, is_str
|
15
15
|
from auto_editor.make_layers import clipify, make_av, make_timeline
|
16
16
|
from auto_editor.output import Ensure, parse_bitrate
|
@@ -160,7 +160,7 @@ def parse_export(export: str, log: Log) -> dict[str, Any]:
|
|
160
160
|
log.error(f"'{name}': Export must be [{', '.join([s for s in parsing.keys()])}]")
|
161
161
|
|
162
162
|
|
163
|
-
def edit_media(paths: list[str],
|
163
|
+
def edit_media(paths: list[str], args: Args, log: Log) -> None:
|
164
164
|
bar = initBar(args.progress)
|
165
165
|
tl = None
|
166
166
|
|
@@ -294,7 +294,7 @@ def edit_media(paths: list[str], ffmpeg: FFmpeg, args: Args, log: Log) -> None:
|
|
294
294
|
|
295
295
|
if ctr.default_aud != "none":
|
296
296
|
ensure = Ensure(bar, samplerate, log)
|
297
|
-
audio_paths = make_new_audio(tl, ctr, ensure, args,
|
297
|
+
audio_paths = make_new_audio(tl, ctr, ensure, args, bar, log)
|
298
298
|
else:
|
299
299
|
audio_paths = []
|
300
300
|
|
@@ -343,8 +343,8 @@ def edit_media(paths: list[str], ffmpeg: FFmpeg, args: Args, log: Log) -> None:
|
|
343
343
|
for i, sub_path in enumerate(sub_paths):
|
344
344
|
subtitle_input = av.open(sub_path)
|
345
345
|
subtitle_inputs.append(subtitle_input)
|
346
|
-
subtitle_stream = output.
|
347
|
-
|
346
|
+
subtitle_stream = output.add_stream_from_template(
|
347
|
+
subtitle_input.streams.subtitles[0]
|
348
348
|
)
|
349
349
|
if i < len(src.subtitles) and src.subtitles[i].lang is not None:
|
350
350
|
subtitle_stream.metadata["language"] = src.subtitles[i].lang # type: ignore
|
auto_editor/ffwrapper.py
CHANGED
@@ -3,40 +3,12 @@ from __future__ import annotations
|
|
3
3
|
from dataclasses import dataclass
|
4
4
|
from fractions import Fraction
|
5
5
|
from pathlib import Path
|
6
|
-
from shutil import which
|
7
|
-
from subprocess import PIPE, Popen
|
8
6
|
|
9
7
|
import av
|
10
8
|
|
11
9
|
from auto_editor.utils.log import Log
|
12
10
|
|
13
11
|
|
14
|
-
def _get_ffmpeg(reason: str, ffloc: str | None, log: Log) -> str:
|
15
|
-
program = "ffmpeg" if ffloc is None else ffloc
|
16
|
-
if (path := which(program)) is None:
|
17
|
-
log.error(f"{reason} needs ffmpeg cli but couldn't find ffmpeg on PATH.")
|
18
|
-
return path
|
19
|
-
|
20
|
-
|
21
|
-
@dataclass(slots=True)
|
22
|
-
class FFmpeg:
|
23
|
-
ffmpeg_location: str | None
|
24
|
-
path: str | None = None
|
25
|
-
|
26
|
-
def get_path(self, reason: str, log: Log) -> str:
|
27
|
-
if self.path is not None:
|
28
|
-
return self.path
|
29
|
-
|
30
|
-
self.path = _get_ffmpeg(reason, self.ffmpeg_location, log)
|
31
|
-
return self.path
|
32
|
-
|
33
|
-
def Popen(self, reason: str, cmd: list[str], log: Log) -> Popen:
|
34
|
-
if self.path is None:
|
35
|
-
self.path = _get_ffmpeg(reason, self.ffmpeg_location, log)
|
36
|
-
|
37
|
-
return Popen([self.path] + cmd, stdout=PIPE, stderr=PIPE)
|
38
|
-
|
39
|
-
|
40
12
|
def mux(input: Path, output: Path, stream: int) -> None:
|
41
13
|
input_container = av.open(input, "r")
|
42
14
|
output_container = av.open(output, "w")
|
auto_editor/help.py
CHANGED
@@ -24,10 +24,23 @@ example:
|
|
24
24
|
will set the speed from 400 ticks to 800 ticks to 2.5x
|
25
25
|
If timebase is 30, 400 ticks to 800 means 13.33 to 26.66 seconds
|
26
26
|
""".strip(),
|
27
|
-
"--edit
|
27
|
+
"--edit": """
|
28
28
|
Evaluates a palet expression that returns a bool-array?. The array is then used for
|
29
29
|
editing.
|
30
30
|
|
31
|
+
Examples:
|
32
|
+
--edit audio
|
33
|
+
--edit audio:0.03 ; Change the threshold. Can be a value between 0-1.
|
34
|
+
--edit audio:3% ; You can also use the `%` macro.
|
35
|
+
--edit audio:0.03,stream=0 ; Only consider the first stream for editing.
|
36
|
+
--edit audio:stream=1,threshold=0.05 ; Here's how you use keyword arguments.
|
37
|
+
--edit (or audio:0.04,stream=0 audio:0.08,stream=1) ; Consider both streams for editing (merge with logical or), but with different thresholds.
|
38
|
+
--edit motion
|
39
|
+
--edit motion:0.02,blur=3
|
40
|
+
--edit (or audio:0.04 motion:0.02,blur=3)
|
41
|
+
--edit none
|
42
|
+
--edit all/e
|
43
|
+
|
31
44
|
Editing Methods:
|
32
45
|
- audio ; Audio silence/loudness detection
|
33
46
|
- threshold threshold? : 4%
|
@@ -52,19 +65,6 @@ Editing Methods:
|
|
52
65
|
|
53
66
|
- none ; Do not modify the media in anyway; mark all sections as "loud" (1).
|
54
67
|
- all/e ; Cut out everything out; mark all sections as "silent" (0).
|
55
|
-
|
56
|
-
|
57
|
-
Command-line Examples:
|
58
|
-
--edit audio
|
59
|
-
--edit audio:threshold=4%
|
60
|
-
--edit audio:threshold=0.03
|
61
|
-
--edit audio:stream=1
|
62
|
-
--edit (or audio:4%,stream=0 audio:8%,stream=1) ; `threshold` is first
|
63
|
-
--edit motion
|
64
|
-
--edit motion:threshold=2%,blur=3
|
65
|
-
--edit (or audio:4% motion:2%,blur=3)
|
66
|
-
--edit none
|
67
|
-
--edit all/e
|
68
68
|
""".strip(),
|
69
69
|
"--export": """
|
70
70
|
This option controls how timelines are exported.
|
@@ -144,8 +144,6 @@ If not set, tempdir will be set with Python's tempfile module
|
|
144
144
|
The directory doesn't have to exist beforehand, however, the root path must be valid.
|
145
145
|
Beware that the temp directory can get quite big.
|
146
146
|
""".strip(),
|
147
|
-
"--ffmpeg-location": "This takes precedence over `--my-ffmpeg`.",
|
148
|
-
"--my-ffmpeg": "This is equivalent to `--ffmpeg-location ffmpeg`.",
|
149
147
|
"--audio-bitrate": """
|
150
148
|
`--audio-bitrate` sets the target bitrate for the audio encoder.
|
151
149
|
By default, the value is `auto` (let the encoder decide).
|
auto_editor/make_layers.py
CHANGED
@@ -139,7 +139,7 @@ def make_timeline(
|
|
139
139
|
|
140
140
|
for i, src in enumerate(sources):
|
141
141
|
try:
|
142
|
-
parser = Parser(Lexer("`--edit`", args.
|
142
|
+
parser = Parser(Lexer("`--edit`", args.edit))
|
143
143
|
if log.is_debug:
|
144
144
|
log.debug(f"edit: {parser}")
|
145
145
|
|
@@ -169,6 +169,8 @@ def make_timeline(
|
|
169
169
|
has_loud = concat((has_loud, result))
|
170
170
|
src_index = concat((src_index, np.full(len(result), i, dtype=np.int32)))
|
171
171
|
|
172
|
+
assert len(has_loud) > 0
|
173
|
+
|
172
174
|
# Setup for handling custom speeds
|
173
175
|
speed_index = has_loud.astype(np.uint)
|
174
176
|
speed_map = [args.silent_speed, args.video_speed]
|
auto_editor/render/audio.py
CHANGED
@@ -2,12 +2,12 @@ from __future__ import annotations
|
|
2
2
|
|
3
3
|
import io
|
4
4
|
from pathlib import Path
|
5
|
-
from platform import system
|
6
5
|
|
7
6
|
import av
|
8
7
|
import numpy as np
|
8
|
+
from av.filter.loudnorm import stats
|
9
9
|
|
10
|
-
from auto_editor.ffwrapper import
|
10
|
+
from auto_editor.ffwrapper import FileInfo
|
11
11
|
from auto_editor.lang.json import Lexer, Parser
|
12
12
|
from auto_editor.lang.palet import env
|
13
13
|
from auto_editor.lib.contracts import andc, between_c, is_int_or_float
|
@@ -56,25 +56,11 @@ def parse_norm(norm: str, log: Log) -> dict | None:
|
|
56
56
|
log.error(e)
|
57
57
|
|
58
58
|
|
59
|
-
def parse_ebu_bytes(norm: dict,
|
60
|
-
start = end = 0
|
61
|
-
lines = stderr.splitlines()
|
62
|
-
|
63
|
-
for index, line in enumerate(lines):
|
64
|
-
if line.startswith(b"[Parsed_loudnorm"):
|
65
|
-
start = index + 1
|
66
|
-
continue
|
67
|
-
if start != 0 and line.startswith(b"}"):
|
68
|
-
end = index + 1
|
69
|
-
break
|
70
|
-
|
71
|
-
if start == 0 or end == 0:
|
72
|
-
log.error(f"Invalid loudnorm stats.\n{stderr!r}")
|
73
|
-
|
59
|
+
def parse_ebu_bytes(norm: dict, stat: bytes, log: Log) -> tuple[str, str]:
|
74
60
|
try:
|
75
|
-
parsed = Parser(Lexer("loudnorm",
|
61
|
+
parsed = Parser(Lexer("loudnorm", stat)).expr()
|
76
62
|
except MyError:
|
77
|
-
log.error(f"Invalid loudnorm stats.\n{
|
63
|
+
log.error(f"Invalid loudnorm stats.\n{stat!r}")
|
78
64
|
|
79
65
|
for key in ("input_i", "input_tp", "input_lra", "input_thresh", "target_offset"):
|
80
66
|
val = float(parsed[key])
|
@@ -101,29 +87,17 @@ def parse_ebu_bytes(norm: dict, stderr: bytes, log: Log) -> tuple[str, str]:
|
|
101
87
|
|
102
88
|
|
103
89
|
def apply_audio_normalization(
|
104
|
-
|
90
|
+
norm: dict, pre_master: Path, path: Path, log: Log
|
105
91
|
) -> None:
|
106
92
|
if norm["tag"] == "ebu":
|
107
93
|
first_pass = (
|
108
|
-
f"
|
109
|
-
f"offset={norm['gain']}:print_format=json"
|
94
|
+
f"i={norm['i']}:lra={norm['lra']}:tp={norm['tp']}:" f"offset={norm['gain']}"
|
110
95
|
)
|
111
96
|
log.debug(f"audio norm first pass: {first_pass}")
|
112
|
-
|
113
|
-
|
114
|
-
|
115
|
-
|
116
|
-
f"{pre_master}",
|
117
|
-
"-af",
|
118
|
-
first_pass,
|
119
|
-
"-vn",
|
120
|
-
"-sn",
|
121
|
-
"-f",
|
122
|
-
"null",
|
123
|
-
file_null,
|
124
|
-
]
|
125
|
-
stderr = ffmpeg.Popen("EBU", cmd, log).communicate()[1]
|
126
|
-
name, filter_args = parse_ebu_bytes(norm, stderr, log)
|
97
|
+
with av.open(f"{pre_master}") as container:
|
98
|
+
stats_ = stats(first_pass, container.streams.audio[0])
|
99
|
+
|
100
|
+
name, filter_args = parse_ebu_bytes(norm, stats_, log)
|
127
101
|
else:
|
128
102
|
assert "t" in norm
|
129
103
|
|
@@ -310,13 +284,7 @@ def mix_audio_files(sr: int, audio_paths: list[str], output_path: str) -> None:
|
|
310
284
|
|
311
285
|
|
312
286
|
def make_new_audio(
|
313
|
-
tl: v3,
|
314
|
-
ctr: Container,
|
315
|
-
ensure: Ensure,
|
316
|
-
args: Args,
|
317
|
-
ffmpeg: FFmpeg,
|
318
|
-
bar: Bar,
|
319
|
-
log: Log,
|
287
|
+
tl: v3, ctr: Container, ensure: Ensure, args: Args, bar: Bar, log: Log
|
320
288
|
) -> list[str]:
|
321
289
|
sr = tl.sr
|
322
290
|
tb = tl.tb
|
@@ -390,7 +358,7 @@ def make_new_audio(
|
|
390
358
|
with open(pre_master, "wb") as fid:
|
391
359
|
write(fid, sr, arr)
|
392
360
|
|
393
|
-
apply_audio_normalization(
|
361
|
+
apply_audio_normalization(norm, pre_master, path, log)
|
394
362
|
|
395
363
|
bar.end()
|
396
364
|
|
auto_editor/render/subtitle.py
CHANGED
@@ -162,7 +162,7 @@ def _ensure(input_: Input, format: str, stream: int) -> str:
|
|
162
162
|
output = av.open(output_bytes, "w", format=format)
|
163
163
|
|
164
164
|
in_stream = input_.streams.subtitles[stream]
|
165
|
-
out_stream = output.
|
165
|
+
out_stream = output.add_stream_from_template(in_stream)
|
166
166
|
|
167
167
|
for packet in input_.demux(in_stream):
|
168
168
|
if packet.dts is None:
|
auto_editor/subcommands/info.py
CHANGED
@@ -163,6 +163,8 @@ def main(sys_args: list[str] = sys.argv[1:]) -> None:
|
|
163
163
|
file_info[file]["subtitle"].append(sub)
|
164
164
|
|
165
165
|
if args.json:
|
166
|
+
if sys.platform == "win32":
|
167
|
+
sys.stdout.reconfigure(encoding="utf-8")
|
166
168
|
dump(file_info, sys.stdout, indent=4)
|
167
169
|
return
|
168
170
|
|
@@ -5,9 +5,10 @@ from dataclasses import dataclass, field
|
|
5
5
|
from fractions import Fraction
|
6
6
|
from typing import TYPE_CHECKING
|
7
7
|
|
8
|
+
import av
|
8
9
|
import numpy as np
|
9
10
|
|
10
|
-
from auto_editor.analyze import
|
11
|
+
from auto_editor.analyze import *
|
11
12
|
from auto_editor.ffwrapper import initFileInfo
|
12
13
|
from auto_editor.lang.palet import env
|
13
14
|
from auto_editor.lib.contracts import is_bool, is_nat, is_nat1, is_str, is_void, orc
|
@@ -130,9 +131,19 @@ def main(sys_args: list[str] = sys.argv[1:]) -> None:
|
|
130
131
|
levels = Levels(src, tb, bar, False, log, strict=True)
|
131
132
|
try:
|
132
133
|
if method == "audio":
|
133
|
-
|
134
|
+
container = av.open(src.path, "r")
|
135
|
+
audio_stream = container.streams.audio[obj["stream"]]
|
136
|
+
log.experimental(audio_stream.codec)
|
137
|
+
print_arr_gen(iter_audio(audio_stream, tb))
|
138
|
+
container.close()
|
139
|
+
|
134
140
|
elif method == "motion":
|
135
|
-
|
141
|
+
container = av.open(src.path, "r")
|
142
|
+
video_stream = container.streams.video[obj["stream"]]
|
143
|
+
log.experimental(video_stream.codec)
|
144
|
+
print_arr_gen(iter_motion(video_stream, tb, obj["blur"], obj["width"]))
|
145
|
+
container.close()
|
146
|
+
|
136
147
|
elif method == "subtitle":
|
137
148
|
print_arr(levels.subtitle(**obj))
|
138
149
|
elif method == "none":
|
auto_editor/utils/log.py
CHANGED
@@ -7,6 +7,8 @@ from tempfile import mkdtemp
|
|
7
7
|
from time import perf_counter, sleep
|
8
8
|
from typing import NoReturn
|
9
9
|
|
10
|
+
import av
|
11
|
+
|
10
12
|
|
11
13
|
class Log:
|
12
14
|
__slots__ = ("is_debug", "quiet", "machine", "no_color", "_temp", "_ut", "_s")
|
@@ -97,6 +99,10 @@ class Log:
|
|
97
99
|
|
98
100
|
sys.stdout.write(f"Finished. took {second_len} seconds ({minute_len})\n")
|
99
101
|
|
102
|
+
def experimental(self, codec: av.Codec) -> None:
|
103
|
+
if codec.experimental:
|
104
|
+
self.error(f"`{codec.name}` is an experimental codec")
|
105
|
+
|
100
106
|
def error(self, message: str | Exception) -> NoReturn:
|
101
107
|
if self.is_debug and isinstance(message, Exception):
|
102
108
|
self.cleanup()
|
auto_editor/utils/types.py
CHANGED
@@ -210,14 +210,13 @@ class Args:
|
|
210
210
|
sample_rate: int | None = None
|
211
211
|
resolution: tuple[int, int] | None = None
|
212
212
|
background: str = "#000000"
|
213
|
-
|
213
|
+
edit: str = "audio"
|
214
214
|
keep_tracks_separate: bool = False
|
215
215
|
audio_normalize: str = "#f"
|
216
216
|
export: str | None = None
|
217
217
|
player: str | None = None
|
218
218
|
no_open: bool = False
|
219
219
|
temp_dir: str | None = None
|
220
|
-
ffmpeg_location: str | None = None
|
221
220
|
progress: str = "modern"
|
222
221
|
version: bool = False
|
223
222
|
debug: bool = False
|
@@ -1,6 +1,6 @@
|
|
1
1
|
Metadata-Version: 2.1
|
2
2
|
Name: auto-editor
|
3
|
-
Version: 26.0
|
3
|
+
Version: 26.1.0
|
4
4
|
Summary: Auto-Editor: Effort free video editing!
|
5
5
|
Author-email: WyattBlue <wyattblue@auto-editor.com>
|
6
6
|
License: Unlicense
|
@@ -11,8 +11,8 @@ Keywords: video,audio,media,editor,editing,processing,nonlinear,automatic,silenc
|
|
11
11
|
Requires-Python: <3.14,>=3.10
|
12
12
|
Description-Content-Type: text/markdown
|
13
13
|
License-File: LICENSE
|
14
|
-
Requires-Dist: numpy
|
15
|
-
Requires-Dist: pyav
|
14
|
+
Requires-Dist: numpy<3.0,>=1.24
|
15
|
+
Requires-Dist: pyav==14.*
|
16
16
|
|
17
17
|
<p align="center"><img src="https://auto-editor.com/img/auto-editor-banner.webp" title="Auto-Editor" width="700"></p>
|
18
18
|
|
@@ -1,10 +1,10 @@
|
|
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=
|
6
|
-
auto_editor/help.py,sha256=
|
7
|
-
auto_editor/make_layers.py,sha256=
|
1
|
+
auto_editor/__init__.py,sha256=8MQdwPYn_Y7GCbtRLrmuh9XSy5S52w2pxd3bulKs9Ag,23
|
2
|
+
auto_editor/__main__.py,sha256=eAsNa1BP4Y6Oyp4l838YmcxEwsM0LUdbaGeNFELe4h0,11124
|
3
|
+
auto_editor/analyze.py,sha256=HyRdnty3VW9ZTwwPwjsZp3bLVRLvII_1Y6NlEItDKfw,11947
|
4
|
+
auto_editor/edit.py,sha256=eEMRaQbn0jylfJ6D_egnUXjoMCbdQVsAu7MDrn-xlGo,15950
|
5
|
+
auto_editor/ffwrapper.py,sha256=Tct_Q-uy5F51h8M7UFam50UzRFpgkBvUamJP1AoKVvc,4749
|
6
|
+
auto_editor/help.py,sha256=CzfDTsL4GuGu596ySHKj_wKnxGR9h8B0KUdkZpo33oE,8044
|
7
|
+
auto_editor/make_layers.py,sha256=vEeJt0PnE1vc9-cQZ_AlXVDjvWhObRCWJSCQGraoMvU,9016
|
8
8
|
auto_editor/output.py,sha256=ho8Lpqz4Sv_Gw0Vj2OvG39s83xHpyZlvtRNryTPbXqc,2563
|
9
9
|
auto_editor/preview.py,sha256=HUsjmV9Fx73rZ26BXrpz9z-z_e4oiui3u9e7qbbGoBY,3037
|
10
10
|
auto_editor/timeline.py,sha256=XfaH9cH-RB-MObOpMr5IfLcqJcjmabO1XwkUkT3_FQM,8186
|
@@ -27,13 +27,13 @@ auto_editor/lib/contracts.py,sha256=lExGQymcQUmwG5lC1lO4qm4GY8W0q_yzK_miTaAoPA4,
|
|
27
27
|
auto_editor/lib/data_structs.py,sha256=dcsXgsLLzbmFDUZucoirzewPALsKzoxz7z5L22_QJM8,7091
|
28
28
|
auto_editor/lib/err.py,sha256=UlszQJdzMZwkbT8x3sY4GkCV_5x9yrd6uVVUzvA8iiI,35
|
29
29
|
auto_editor/render/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
30
|
-
auto_editor/render/audio.py,sha256=
|
31
|
-
auto_editor/render/subtitle.py,sha256=
|
30
|
+
auto_editor/render/audio.py,sha256=1iOQCeRXfRz28cqnHp2XeK-f3_UnPf80AKQAfifGvdE,12584
|
31
|
+
auto_editor/render/subtitle.py,sha256=lf2l1QWJgFiqlpQWWBwSlKJnSgW8Lkfi59WrJMbIDqM,6240
|
32
32
|
auto_editor/render/video.py,sha256=dje0RNW2dKILfTzt0VAF0WR6REfGOsc6l17pP1Z4ooA,12215
|
33
33
|
auto_editor/subcommands/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
34
34
|
auto_editor/subcommands/desc.py,sha256=GDrKJYiHMaeTrplZAceXl1JwoqD78XsV2_5lc0Xd7po,869
|
35
|
-
auto_editor/subcommands/info.py,sha256=
|
36
|
-
auto_editor/subcommands/levels.py,sha256=
|
35
|
+
auto_editor/subcommands/info.py,sha256=UDdoxd6_fqSoRPwthkWXqnpxHp7dJQ0Dn96lYX_ubWc,7010
|
36
|
+
auto_editor/subcommands/levels.py,sha256=psSSIsGfzr9j0HGKp2yvK6nMlrkLwxkwsyI0uF2xb_c,4496
|
37
37
|
auto_editor/subcommands/palet.py,sha256=ONzTqemaQq9YEfIOsDRNnwzfqnEMUMSXIQrETxyroRU,749
|
38
38
|
auto_editor/subcommands/repl.py,sha256=TF_I7zsFY7-KdgidrqjafTz7o_eluVbLvgTcOBG-UWQ,3449
|
39
39
|
auto_editor/subcommands/subdump.py,sha256=af_XBf7kaevqHn1A71z8C-7x8pS5WKD9FE_ugkCw6rk,665
|
@@ -44,12 +44,12 @@ auto_editor/utils/chunks.py,sha256=J-eGKtEz68gFtRrj1kOSgH4Tj_Yz6prNQ7Xr-d9NQJw,5
|
|
44
44
|
auto_editor/utils/cmdkw.py,sha256=aUGBvBel2Ko1o6Rwmr4rEL-BMc5hEnzYLbyZ1GeJdcY,5729
|
45
45
|
auto_editor/utils/container.py,sha256=Wf1ZL0tvXWl6m1B9mK_SkgVl89ilV_LpwlQq0TVroCc,2704
|
46
46
|
auto_editor/utils/func.py,sha256=kB-pNDn20M6YT7sljyd_auve5teK-E2G4TgwVOAIuJw,2754
|
47
|
-
auto_editor/utils/log.py,sha256=
|
48
|
-
auto_editor/utils/types.py,sha256=
|
47
|
+
auto_editor/utils/log.py,sha256=C1b-vnszSsohMd5fyaRcCuf0OPobZVMkV77cP-_JNP4,3776
|
48
|
+
auto_editor/utils/types.py,sha256=7BF7R7DA5eKmtI6f5ia7bOYNL0u_2sviiPsE1VmP0lc,10724
|
49
49
|
docs/build.py,sha256=CM-ZWgQk8wSNjivx_-6wGIaG7cstrNKsX2d4TzFVivE,1642
|
50
|
-
auto_editor-26.0.
|
51
|
-
auto_editor-26.0.
|
52
|
-
auto_editor-26.0.
|
53
|
-
auto_editor-26.0.
|
54
|
-
auto_editor-26.0.
|
55
|
-
auto_editor-26.0.
|
50
|
+
auto_editor-26.1.0.dist-info/LICENSE,sha256=yiq99pWITHfqS0pbZMp7cy2dnbreTuvBwudsU-njvIM,1210
|
51
|
+
auto_editor-26.1.0.dist-info/METADATA,sha256=QDfveFTnxTtnA2WqZTvgM4vNGQ1sTnziQ2MSAyqF5WQ,6109
|
52
|
+
auto_editor-26.1.0.dist-info/WHEEL,sha256=PZUExdf71Ui_so67QXpySuHtCi3-J3wvF4ORK6k_S8U,91
|
53
|
+
auto_editor-26.1.0.dist-info/entry_points.txt,sha256=-H7zdTw4MqnAcwrN5xTNkGIhzZtJMxS9r6lTMeR9-aA,240
|
54
|
+
auto_editor-26.1.0.dist-info/top_level.txt,sha256=jBV5zlbWRbKOa-xaWPvTD45QL7lGExx2BDzv-Ji4dTw,17
|
55
|
+
auto_editor-26.1.0.dist-info/RECORD,,
|
File without changes
|
File without changes
|
File without changes
|