auto-editor 25.3.0__py3-none-any.whl → 25.3.1__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 +67 -6
- auto_editor/analyze.py +11 -14
- auto_editor/edit.py +21 -8
- auto_editor/ffwrapper.py +38 -75
- auto_editor/output.py +1 -1
- auto_editor/render/audio.py +1 -2
- auto_editor/render/subtitle.py +5 -3
- auto_editor/render/video.py +1 -1
- auto_editor/subcommands/test.py +10 -3
- auto_editor/utils/func.py +0 -26
- {auto_editor-25.3.0.dist-info → auto_editor-25.3.1.dist-info}/METADATA +2 -2
- {auto_editor-25.3.0.dist-info → auto_editor-25.3.1.dist-info}/RECORD +18 -20
- docs/build.py +1 -0
- auto_editor/utils/subtitle_tools.py +0 -29
- auto_editor/validate_input.py +0 -88
- {auto_editor-25.3.0.dist-info → auto_editor-25.3.1.dist-info}/LICENSE +0 -0
- {auto_editor-25.3.0.dist-info → auto_editor-25.3.1.dist-info}/WHEEL +0 -0
- {auto_editor-25.3.0.dist-info → auto_editor-25.3.1.dist-info}/entry_points.txt +0 -0
- {auto_editor-25.3.0.dist-info → auto_editor-25.3.1.dist-info}/top_level.txt +0 -0
auto_editor/__init__.py
CHANGED
@@ -1 +1 @@
|
|
1
|
-
__version__ = "25.3.
|
1
|
+
__version__ = "25.3.1"
|
auto_editor/__main__.py
CHANGED
@@ -1,11 +1,15 @@
|
|
1
1
|
#!/usr/bin/env python3
|
2
2
|
|
3
|
+
import re
|
3
4
|
import sys
|
4
5
|
from os import environ
|
6
|
+
from os.path import exists, isdir, isfile, lexists, splitext
|
7
|
+
from subprocess import run
|
5
8
|
|
6
9
|
import auto_editor
|
7
10
|
from auto_editor.edit import edit_media
|
8
|
-
from auto_editor.ffwrapper import FFmpeg
|
11
|
+
from auto_editor.ffwrapper import FFmpeg, initFFmpeg
|
12
|
+
from auto_editor.utils.func import get_stdout
|
9
13
|
from auto_editor.utils.log import Log
|
10
14
|
from auto_editor.utils.types import (
|
11
15
|
Args,
|
@@ -20,7 +24,6 @@ from auto_editor.utils.types import (
|
|
20
24
|
speed_range,
|
21
25
|
time_range,
|
22
26
|
)
|
23
|
-
from auto_editor.validate_input import valid_input
|
24
27
|
from auto_editor.vanparse import ArgumentParser
|
25
28
|
|
26
29
|
|
@@ -274,6 +277,50 @@ def main_options(parser: ArgumentParser) -> ArgumentParser:
|
|
274
277
|
return parser
|
275
278
|
|
276
279
|
|
280
|
+
def download_video(my_input: str, args: Args, ffmpeg: FFmpeg, log: Log) -> str:
|
281
|
+
log.conwrite("Downloading video...")
|
282
|
+
|
283
|
+
def get_domain(url: str) -> str:
|
284
|
+
t = __import__("urllib").parse.urlparse(url).netloc
|
285
|
+
return ".".join(t.split(".")[-2:])
|
286
|
+
|
287
|
+
download_format = args.download_format
|
288
|
+
if download_format is None and get_domain(my_input) == "youtube.com":
|
289
|
+
download_format = "bestvideo[ext=mp4]+bestaudio[ext=m4a]"
|
290
|
+
|
291
|
+
if args.output_format is None:
|
292
|
+
output_format = re.sub(r"\W+", "-", splitext(my_input)[0]) + ".%(ext)s"
|
293
|
+
else:
|
294
|
+
output_format = args.output_format
|
295
|
+
|
296
|
+
yt_dlp_path = args.yt_dlp_location
|
297
|
+
|
298
|
+
cmd = ["--ffmpeg-location", ffmpeg.path]
|
299
|
+
|
300
|
+
if download_format is not None:
|
301
|
+
cmd.extend(["-f", download_format])
|
302
|
+
|
303
|
+
cmd.extend(["-o", output_format, my_input])
|
304
|
+
|
305
|
+
if args.yt_dlp_extras is not None:
|
306
|
+
cmd.extend(args.yt_dlp_extras.split(" "))
|
307
|
+
|
308
|
+
try:
|
309
|
+
location = get_stdout(
|
310
|
+
[yt_dlp_path, "--get-filename", "--no-warnings"] + cmd
|
311
|
+
).strip()
|
312
|
+
except FileNotFoundError:
|
313
|
+
log.error("Program `yt-dlp` must be installed and on PATH.")
|
314
|
+
|
315
|
+
if not isfile(location):
|
316
|
+
run([yt_dlp_path] + cmd)
|
317
|
+
|
318
|
+
if not isfile(location):
|
319
|
+
log.error(f"Download file wasn't created: {location}")
|
320
|
+
|
321
|
+
return location
|
322
|
+
|
323
|
+
|
277
324
|
def main() -> None:
|
278
325
|
subcommands = ("test", "info", "levels", "subdump", "desc", "repl", "palet")
|
279
326
|
|
@@ -284,8 +331,7 @@ def main() -> None:
|
|
284
331
|
obj.main(sys.argv[2:])
|
285
332
|
return
|
286
333
|
|
287
|
-
|
288
|
-
no_color = bool(environ.get("NO_COLOR")) or bool(environ.get(ff_color))
|
334
|
+
no_color = bool(environ.get("NO_COLOR") or environ.get("AV_LOG_FORCE_NOCOLOR"))
|
289
335
|
log = Log(no_color=no_color)
|
290
336
|
|
291
337
|
args = main_options(ArgumentParser("Auto-Editor")).parse_args(
|
@@ -327,13 +373,28 @@ def main() -> None:
|
|
327
373
|
is_machine = args.progress == "machine"
|
328
374
|
log = Log(args.debug, args.quiet, args.temp_dir, is_machine, no_color)
|
329
375
|
|
330
|
-
ffmpeg =
|
376
|
+
ffmpeg = initFFmpeg(
|
377
|
+
log,
|
331
378
|
args.ffmpeg_location,
|
332
379
|
args.my_ffmpeg,
|
333
380
|
args.show_ffmpeg_commands,
|
334
381
|
args.show_ffmpeg_output,
|
335
382
|
)
|
336
|
-
paths =
|
383
|
+
paths = []
|
384
|
+
for my_input in args.input:
|
385
|
+
if my_input.startswith("http://") or my_input.startswith("https://"):
|
386
|
+
paths.append(download_video(my_input, args, ffmpeg, log))
|
387
|
+
else:
|
388
|
+
if not splitext(my_input)[1]:
|
389
|
+
if isdir(my_input):
|
390
|
+
log.error("Input must be a file or a URL, not a directory.")
|
391
|
+
if exists(my_input):
|
392
|
+
log.error(f"Input file must have an extension: {my_input}")
|
393
|
+
if lexists(my_input):
|
394
|
+
log.error(f"Input file is a broken symbolic link: {my_input}")
|
395
|
+
if my_input.startswith("-"):
|
396
|
+
log.error(f"Option/Input file doesn't exist: {my_input}")
|
397
|
+
paths.append(my_input)
|
337
398
|
|
338
399
|
try:
|
339
400
|
edit_media(paths, ffmpeg, args, log)
|
auto_editor/analyze.py
CHANGED
@@ -14,13 +14,11 @@ from av.audio.fifo import AudioFifo
|
|
14
14
|
from av.subtitles.subtitle import AssSubtitle
|
15
15
|
|
16
16
|
from auto_editor import __version__
|
17
|
-
from auto_editor.utils.subtitle_tools import convert_ass_to_text
|
18
17
|
|
19
18
|
if TYPE_CHECKING:
|
20
|
-
from collections.abc import Iterator
|
19
|
+
from collections.abc import Iterator, Sequence
|
21
20
|
from fractions import Fraction
|
22
21
|
from pathlib import Path
|
23
|
-
from typing import Any
|
24
22
|
|
25
23
|
from numpy.typing import NDArray
|
26
24
|
|
@@ -156,10 +154,10 @@ def iter_motion(src, tb, stream: int, blur: int, width: int) -> Iterator[np.floa
|
|
156
154
|
container.close()
|
157
155
|
|
158
156
|
|
159
|
-
def obj_tag(path: Path, kind: str, tb: Fraction, obj:
|
157
|
+
def obj_tag(path: Path, kind: str, tb: Fraction, obj: Sequence[object]) -> str:
|
160
158
|
mod_time = int(path.stat().st_mtime)
|
161
159
|
key = f"{path.name}:{mod_time:x}:{kind}:{tb}:"
|
162
|
-
return key + ",".join(f"{v}" for v in obj
|
160
|
+
return key + ",".join(f"{v}" for v in obj)
|
163
161
|
|
164
162
|
|
165
163
|
@dataclass(slots=True)
|
@@ -174,7 +172,7 @@ class Levels:
|
|
174
172
|
@property
|
175
173
|
def media_length(self) -> int:
|
176
174
|
if self.src.audios:
|
177
|
-
if (arr := self.read_cache("audio",
|
175
|
+
if (arr := self.read_cache("audio", (0,))) is not None:
|
178
176
|
return len(arr)
|
179
177
|
|
180
178
|
result = sum(1 for _ in iter_audio(self.src, self.tb, 0))
|
@@ -202,7 +200,7 @@ class Levels:
|
|
202
200
|
def all(self) -> NDArray[np.bool_]:
|
203
201
|
return np.zeros(self.media_length, dtype=np.bool_)
|
204
202
|
|
205
|
-
def read_cache(self, kind: str, obj:
|
203
|
+
def read_cache(self, kind: str, obj: Sequence[object]) -> None | np.ndarray:
|
206
204
|
if self.no_cache:
|
207
205
|
return None
|
208
206
|
|
@@ -221,7 +219,7 @@ class Levels:
|
|
221
219
|
self.log.debug("Using cache")
|
222
220
|
return npzfile[key]
|
223
221
|
|
224
|
-
def cache(self, arr: np.ndarray, kind: str, obj:
|
222
|
+
def cache(self, arr: np.ndarray, kind: str, obj: Sequence[object]) -> np.ndarray:
|
225
223
|
if self.no_cache:
|
226
224
|
return arr
|
227
225
|
|
@@ -238,7 +236,7 @@ class Levels:
|
|
238
236
|
if stream >= len(self.src.audios):
|
239
237
|
raise LevelError(f"audio: audio stream '{stream}' does not exist.")
|
240
238
|
|
241
|
-
if (arr := self.read_cache("audio",
|
239
|
+
if (arr := self.read_cache("audio", (stream,))) is not None:
|
242
240
|
return arr
|
243
241
|
|
244
242
|
with av.open(self.src.path, "r") as container:
|
@@ -265,13 +263,13 @@ class Levels:
|
|
265
263
|
index += 1
|
266
264
|
|
267
265
|
bar.end()
|
268
|
-
return self.cache(result[:index], "audio",
|
266
|
+
return self.cache(result[:index], "audio", (stream,))
|
269
267
|
|
270
268
|
def motion(self, stream: int, blur: int, width: int) -> NDArray[np.float32]:
|
271
269
|
if stream >= len(self.src.videos):
|
272
270
|
raise LevelError(f"motion: video stream '{stream}' does not exist.")
|
273
271
|
|
274
|
-
mobj =
|
272
|
+
mobj = (stream, width, blur)
|
275
273
|
if (arr := self.read_cache("motion", mobj)) is not None:
|
276
274
|
return arr
|
277
275
|
|
@@ -360,11 +358,10 @@ class Levels:
|
|
360
358
|
san_end = round((start + dur) * self.tb)
|
361
359
|
|
362
360
|
for sub in subset:
|
363
|
-
if isinstance(sub, AssSubtitle):
|
364
|
-
line = convert_ass_to_text(sub.ass.decode(errors="ignore"))
|
365
|
-
else:
|
361
|
+
if not isinstance(sub, AssSubtitle):
|
366
362
|
continue
|
367
363
|
|
364
|
+
line = sub.dialogue.decode(errors="ignore")
|
368
365
|
if line and re.search(re_pattern, line):
|
369
366
|
result[san_start:san_end] = 1
|
370
367
|
count += 1
|
auto_editor/edit.py
CHANGED
@@ -1,6 +1,8 @@
|
|
1
1
|
from __future__ import annotations
|
2
2
|
|
3
3
|
import os
|
4
|
+
import sys
|
5
|
+
from subprocess import run
|
4
6
|
from typing import Any
|
5
7
|
|
6
8
|
from auto_editor.ffwrapper import FFmpeg, FileInfo, initFileInfo
|
@@ -15,7 +17,6 @@ from auto_editor.utils.bar import initBar
|
|
15
17
|
from auto_editor.utils.chunks import Chunk, Chunks
|
16
18
|
from auto_editor.utils.cmdkw import ParserError, parse_with_palet, pAttr, pAttrs
|
17
19
|
from auto_editor.utils.container import Container, container_constructor
|
18
|
-
from auto_editor.utils.func import open_with_system_default
|
19
20
|
from auto_editor.utils.log import Log
|
20
21
|
from auto_editor.utils.types import Args
|
21
22
|
|
@@ -328,7 +329,7 @@ def edit_media(paths: list[str], ffmpeg: FFmpeg, args: Args, log: Log) -> None:
|
|
328
329
|
total_frames = tl.v1.chunks[-1][1] - 1
|
329
330
|
clip_num = 0
|
330
331
|
for chunk in tl.v1.chunks:
|
331
|
-
if chunk[2] == 99999:
|
332
|
+
if chunk[2] == 0 or chunk[2] >= 99999:
|
332
333
|
continue
|
333
334
|
|
334
335
|
padded_chunks = pad_chunk(chunk, total_frames)
|
@@ -354,11 +355,23 @@ def edit_media(paths: list[str], ffmpeg: FFmpeg, args: Args, log: Log) -> None:
|
|
354
355
|
|
355
356
|
log.stop_timer()
|
356
357
|
|
357
|
-
if not args.no_open and export in ("default", "audio"
|
358
|
+
if not args.no_open and export in ("default", "audio"):
|
358
359
|
if args.player is None:
|
359
|
-
|
360
|
+
if sys.platform == "win32":
|
361
|
+
try:
|
362
|
+
os.startfile(output)
|
363
|
+
except OSError:
|
364
|
+
log.warning(f"Could not find application to open file: {output}")
|
365
|
+
else:
|
366
|
+
try: # MacOS case
|
367
|
+
run(["open", output])
|
368
|
+
except Exception:
|
369
|
+
try: # WSL2 case
|
370
|
+
run(["cmd.exe", "/C", "start", output])
|
371
|
+
except Exception:
|
372
|
+
try: # Linux case
|
373
|
+
run(["xdg-open", output])
|
374
|
+
except Exception:
|
375
|
+
log.warning(f"Could not open output file: {output}")
|
360
376
|
else:
|
361
|
-
|
362
|
-
from shlex import split
|
363
|
-
|
364
|
-
subprocess.run(split(args.player) + [output])
|
377
|
+
run(__import__("shlex").split(args.player) + [output])
|
auto_editor/ffwrapper.py
CHANGED
@@ -1,83 +1,59 @@
|
|
1
1
|
from __future__ import annotations
|
2
2
|
|
3
3
|
import os.path
|
4
|
-
import subprocess
|
5
4
|
import sys
|
6
5
|
from dataclasses import dataclass
|
7
6
|
from fractions import Fraction
|
8
7
|
from pathlib import Path
|
9
8
|
from re import search
|
10
9
|
from shutil import which
|
11
|
-
from subprocess import PIPE, Popen
|
10
|
+
from subprocess import PIPE, Popen, run
|
12
11
|
from typing import Any
|
13
12
|
|
14
13
|
import av
|
15
14
|
|
16
|
-
from auto_editor.utils.func import get_stdout
|
17
15
|
from auto_editor.utils.log import Log
|
18
16
|
|
19
17
|
|
20
|
-
|
21
|
-
|
22
|
-
|
23
|
-
|
24
|
-
|
25
|
-
|
26
|
-
|
27
|
-
|
28
|
-
debug: bool = False,
|
29
|
-
):
|
30
|
-
def _set_ff_path(ff_location: str | None, my_ffmpeg: bool) -> str:
|
31
|
-
if ff_location is not None:
|
32
|
-
return ff_location
|
33
|
-
if my_ffmpeg:
|
34
|
-
return "ffmpeg"
|
35
|
-
|
36
|
-
try:
|
37
|
-
import ae_ffmpeg
|
38
|
-
|
39
|
-
return ae_ffmpeg.get_path()
|
40
|
-
except ImportError:
|
41
|
-
return "ffmpeg"
|
42
|
-
|
43
|
-
self.debug = debug
|
44
|
-
self.show_cmd = show_cmd
|
45
|
-
_path: str | None = _set_ff_path(ff_location, my_ffmpeg)
|
46
|
-
|
47
|
-
if _path == "ffmpeg":
|
48
|
-
_path = which("ffmpeg")
|
49
|
-
|
50
|
-
if _path is None:
|
51
|
-
Log().error("Did not find ffmpeg on PATH.")
|
52
|
-
self.path = _path
|
53
|
-
|
18
|
+
def initFFmpeg(
|
19
|
+
log: Log, ff_location: str | None, my_ffmpeg: bool, show_cmd: bool, debug: bool
|
20
|
+
) -> FFmpeg:
|
21
|
+
if ff_location is not None:
|
22
|
+
program = ff_location
|
23
|
+
elif my_ffmpeg:
|
24
|
+
program = "ffmpeg"
|
25
|
+
else:
|
54
26
|
try:
|
55
|
-
|
56
|
-
self.version = _version.replace("ffmpeg version", "").strip().split(" ")[0]
|
57
|
-
except FileNotFoundError:
|
58
|
-
Log().error("ffmpeg must be installed and on PATH.")
|
27
|
+
import ae_ffmpeg
|
59
28
|
|
60
|
-
|
61
|
-
|
62
|
-
|
29
|
+
program = ae_ffmpeg.get_path()
|
30
|
+
except ImportError:
|
31
|
+
program = "ffmpeg"
|
63
32
|
|
64
|
-
|
65
|
-
|
66
|
-
|
33
|
+
path: str | None = which(program)
|
34
|
+
if path is None:
|
35
|
+
log.error("Did not find ffmpeg on PATH.")
|
36
|
+
|
37
|
+
return FFmpeg(log, path, show_cmd, debug)
|
38
|
+
|
39
|
+
|
40
|
+
@dataclass(slots=True)
|
41
|
+
class FFmpeg:
|
42
|
+
log: Log
|
43
|
+
path: str
|
44
|
+
show_cmd: bool
|
45
|
+
debug: bool
|
67
46
|
|
68
47
|
def run(self, cmd: list[str]) -> None:
|
69
48
|
cmd = [self.path, "-hide_banner", "-y"] + cmd
|
70
49
|
if not self.debug:
|
71
50
|
cmd.extend(["-nostats", "-loglevel", "error"])
|
72
|
-
self.
|
73
|
-
|
51
|
+
if self.show_cmd:
|
52
|
+
sys.stderr.write(f"{' '.join(cmd)}\n\n")
|
53
|
+
run(cmd)
|
74
54
|
|
75
55
|
def run_check_errors(
|
76
|
-
self,
|
77
|
-
cmd: list[str],
|
78
|
-
log: Log,
|
79
|
-
show_out: bool = False,
|
80
|
-
path: str | None = None,
|
56
|
+
self, cmd: list[str], show_out: bool = False, path: str | None = None
|
81
57
|
) -> None:
|
82
58
|
process = self.Popen(cmd, stdin=PIPE, stdout=PIPE, stderr=PIPE)
|
83
59
|
_, stderr = process.communicate()
|
@@ -104,40 +80,27 @@ class FFmpeg:
|
|
104
80
|
|
105
81
|
for item in error_list:
|
106
82
|
if check := search(item, output):
|
107
|
-
log.error(check.group())
|
83
|
+
self.log.error(check.group())
|
108
84
|
|
109
85
|
if path is not None and not os.path.isfile(path):
|
110
|
-
log.error(f"The file {path} was not created.")
|
111
|
-
|
86
|
+
self.log.error(f"The file {path} was not created.")
|
87
|
+
if show_out and not self.debug:
|
112
88
|
print(f"stderr: {output}")
|
113
89
|
|
114
90
|
def Popen(
|
115
91
|
self, cmd: list[str], stdin: Any = None, stdout: Any = PIPE, stderr: Any = None
|
116
92
|
) -> Popen:
|
117
|
-
|
118
|
-
|
119
|
-
return Popen(cmd, stdin=stdin, stdout=stdout, stderr=stderr)
|
120
|
-
|
121
|
-
def pipe(self, cmd: list[str]) -> str:
|
122
|
-
cmd = [self.path, "-y"] + cmd
|
123
|
-
|
124
|
-
self.print_cmd(cmd)
|
125
|
-
output = get_stdout(cmd)
|
126
|
-
self.print(output)
|
127
|
-
return output
|
93
|
+
if self.show_cmd:
|
94
|
+
sys.stderr.write(f"{self.path} {' '.join(cmd)}\n\n")
|
95
|
+
return Popen([self.path] + cmd, stdin=stdin, stdout=stdout, stderr=stderr)
|
128
96
|
|
129
97
|
|
130
|
-
def mux(input: Path, output: Path, stream: int
|
98
|
+
def mux(input: Path, output: Path, stream: int) -> None:
|
131
99
|
input_container = av.open(input, "r")
|
132
100
|
output_container = av.open(output, "w")
|
133
101
|
|
134
102
|
input_audio_stream = input_container.streams.audio[stream]
|
135
|
-
|
136
|
-
if codec is None:
|
137
|
-
codec = "pcm_s16le"
|
138
|
-
|
139
|
-
output_audio_stream = output_container.add_stream(codec)
|
140
|
-
assert isinstance(output_audio_stream, av.audio.AudioStream)
|
103
|
+
output_audio_stream = output_container.add_stream("pcm_s16le")
|
141
104
|
|
142
105
|
for frame in input_container.decode(input_audio_stream):
|
143
106
|
packet = output_audio_stream.encode(frame)
|
auto_editor/output.py
CHANGED
auto_editor/render/audio.py
CHANGED
@@ -180,7 +180,6 @@ def process_audio_clip(
|
|
180
180
|
output_bytes = io.BytesIO()
|
181
181
|
output_file = av.open(output_bytes, mode="w", format="wav")
|
182
182
|
output_stream = output_file.add_stream("pcm_s16le", rate=sr)
|
183
|
-
assert isinstance(output_stream, av.audio.AudioStream)
|
184
183
|
|
185
184
|
graph = av.filter.Graph()
|
186
185
|
args = [graph.add_abuffer(template=input_stream)]
|
@@ -212,7 +211,7 @@ def process_audio_clip(
|
|
212
211
|
while True:
|
213
212
|
try:
|
214
213
|
aframe = graph.pull()
|
215
|
-
assert isinstance(aframe, av.
|
214
|
+
assert isinstance(aframe, av.AudioFrame)
|
216
215
|
for packet in output_stream.encode(aframe):
|
217
216
|
output_file.mux(packet)
|
218
217
|
except (av.BlockingIOError, av.EOFError):
|
auto_editor/render/subtitle.py
CHANGED
@@ -112,13 +112,15 @@ class SubtitleParser:
|
|
112
112
|
self.contents = new_content
|
113
113
|
|
114
114
|
def write(self, file_path: str) -> None:
|
115
|
+
codec = self.codec
|
115
116
|
with open(file_path, "w", encoding="utf-8") as file:
|
116
117
|
file.write(self.header)
|
117
118
|
for c in self.contents:
|
118
119
|
file.write(
|
119
|
-
f"{c.before}{to_timecode(c.start / self.tb,
|
120
|
-
f"{c.middle}{to_timecode(c.end / self.tb,
|
121
|
-
|
120
|
+
f"{c.before}{to_timecode(c.start / self.tb, codec)}"
|
121
|
+
+ f"{c.middle}{to_timecode(c.end / self.tb, codec)}"
|
122
|
+
+ c.after
|
123
|
+
+ ("\n" if codec == "webvtt" else "")
|
122
124
|
)
|
123
125
|
file.write(self.footer)
|
124
126
|
|
auto_editor/render/video.py
CHANGED
@@ -339,7 +339,7 @@ def render_av(
|
|
339
339
|
process2.wait()
|
340
340
|
except (OSError, BrokenPipeError):
|
341
341
|
bar.end()
|
342
|
-
ffmpeg.run_check_errors(cmd,
|
342
|
+
ffmpeg.run_check_errors(cmd, True)
|
343
343
|
log.error("FFmpeg Error!")
|
344
344
|
|
345
345
|
log.debug(f"Total frames saved seeking: {frames_saved}")
|
auto_editor/subcommands/test.py
CHANGED
@@ -186,7 +186,7 @@ def main(sys_args: list[str] | None = None):
|
|
186
186
|
"wav/pcm-f32le.wav",
|
187
187
|
"wav/pcm-s32le.wav",
|
188
188
|
"multi-track.mov",
|
189
|
-
"
|
189
|
+
"mov_text.mp4",
|
190
190
|
"testsrc.mkv",
|
191
191
|
)
|
192
192
|
|
@@ -222,7 +222,8 @@ def main(sys_args: list[str] | None = None):
|
|
222
222
|
run.raw(["levels", "resources/new-commentary.mp3"])
|
223
223
|
|
224
224
|
def subdump():
|
225
|
-
run.raw(["subdump", "resources/
|
225
|
+
run.raw(["subdump", "resources/mov_text.mp4"])
|
226
|
+
run.raw(["subdump", "resources/webvtt.mkv"])
|
226
227
|
|
227
228
|
def desc():
|
228
229
|
run.raw(["desc", "example.mp4"])
|
@@ -357,7 +358,13 @@ def main(sys_args: list[str] | None = None):
|
|
357
358
|
run.main(["example.mp4"], ["--export", 'premiere:name="Foo Bar"'])
|
358
359
|
|
359
360
|
def export_subtitles():
|
360
|
-
cn = fileinfo(run.main(["resources/
|
361
|
+
cn = fileinfo(run.main(["resources/mov_text.mp4"], []))
|
362
|
+
|
363
|
+
assert len(cn.videos) == 1
|
364
|
+
assert len(cn.audios) == 1
|
365
|
+
assert len(cn.subtitles) == 1
|
366
|
+
|
367
|
+
cn = fileinfo(run.main(["resources/webvtt.mkv"], []))
|
361
368
|
|
362
369
|
assert len(cn.videos) == 1
|
363
370
|
assert len(cn.audios) == 1
|
auto_editor/utils/func.py
CHANGED
@@ -10,8 +10,6 @@ if TYPE_CHECKING:
|
|
10
10
|
|
11
11
|
from numpy.typing import NDArray
|
12
12
|
|
13
|
-
from auto_editor.utils.log import Log
|
14
|
-
|
15
13
|
BoolList = NDArray[np.bool_]
|
16
14
|
BoolOperand = Callable[[BoolList, BoolList], BoolList]
|
17
15
|
|
@@ -135,30 +133,6 @@ def human_readable_time(time_in_secs: float) -> str:
|
|
135
133
|
return f"{time_in_secs} {units}"
|
136
134
|
|
137
135
|
|
138
|
-
def open_with_system_default(path: str, log: Log) -> None:
|
139
|
-
import sys
|
140
|
-
from subprocess import run
|
141
|
-
|
142
|
-
if sys.platform == "win32":
|
143
|
-
from os import startfile
|
144
|
-
|
145
|
-
try:
|
146
|
-
startfile(path)
|
147
|
-
except OSError:
|
148
|
-
log.warning("Could not find application to open file.")
|
149
|
-
else:
|
150
|
-
try: # MacOS case
|
151
|
-
run(["open", path])
|
152
|
-
except Exception:
|
153
|
-
try: # WSL2 case
|
154
|
-
run(["cmd.exe", "/C", "start", path])
|
155
|
-
except Exception:
|
156
|
-
try: # Linux case
|
157
|
-
run(["xdg-open", path])
|
158
|
-
except Exception:
|
159
|
-
log.warning("Could not open output file.")
|
160
|
-
|
161
|
-
|
162
136
|
def append_filename(path: str, val: str) -> str:
|
163
137
|
from os.path import splitext
|
164
138
|
|
@@ -1,6 +1,6 @@
|
|
1
1
|
Metadata-Version: 2.1
|
2
2
|
Name: auto-editor
|
3
|
-
Version: 25.3.
|
3
|
+
Version: 25.3.1
|
4
4
|
Summary: Auto-Editor: Effort free video editing!
|
5
5
|
Author-email: WyattBlue <wyattblue@auto-editor.com>
|
6
6
|
License: Unlicense
|
@@ -12,7 +12,7 @@ Requires-Python: <3.14,>=3.10
|
|
12
12
|
Description-Content-Type: text/markdown
|
13
13
|
License-File: LICENSE
|
14
14
|
Requires-Dist: numpy <3.0,>=1.23.0
|
15
|
-
Requires-Dist: pyav ==13.
|
15
|
+
Requires-Dist: pyav ==13.1.*
|
16
16
|
Requires-Dist: ae-ffmpeg ==1.2.*
|
17
17
|
|
18
18
|
<p align="center"><img src="https://auto-editor.com/img/auto-editor-banner.webp" title="Auto-Editor" width="700"></p>
|
@@ -1,14 +1,13 @@
|
|
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=2StKlE00iLSdWP1B73T1WJb76-2gk0h3rPFRqAk5ZXM,23
|
2
|
+
auto_editor/__main__.py,sha256=0t_yI-o11iwDm_LlUW_u84LsX89cv0GuHMfc0NKlp68,12228
|
3
|
+
auto_editor/analyze.py,sha256=uCi21659BB-lbPwZ6yxNLekS6Q3yoB2ypLNXPhmhTfg,11688
|
4
|
+
auto_editor/edit.py,sha256=9G2_g_8FEzQrvB3Ez9M5vtgrIS0feZeA4DdxT9cdObA,12319
|
5
|
+
auto_editor/ffwrapper.py,sha256=K-gs5mAdcwPaaZVdU1yOl7ckptO7eJ_zLQM8V7vwxaA,7491
|
6
6
|
auto_editor/help.py,sha256=BFiP7vBz42TUzum4-zaQIrV1OY7kHeN0pe0MPE0T5xw,7997
|
7
7
|
auto_editor/make_layers.py,sha256=8uFy5SvMArAP-5slYJrxa_iGAEwimQBFeM-T01VORVw,8995
|
8
|
-
auto_editor/output.py,sha256=
|
8
|
+
auto_editor/output.py,sha256=TbwiFL7CQnPv5_Pv0bSaE2faLvepxaIf5_xMvDAFXjQ,7997
|
9
9
|
auto_editor/preview.py,sha256=HUsjmV9Fx73rZ26BXrpz9z-z_e4oiui3u9e7qbbGoBY,3037
|
10
10
|
auto_editor/timeline.py,sha256=tIty8O8jD6TR2Sw2bivUtYtdnpplfuOXT7Dfc-gekr8,8174
|
11
|
-
auto_editor/validate_input.py,sha256=_9vtbNxodhVPf4PzTAH67LSj2K-HS6kShJOARmUMJdY,2904
|
12
11
|
auto_editor/vanparse.py,sha256=f0vViZ-aYtDxEyVrFHJ5X2pPTQAfqtw3N2gZgzn51kU,10002
|
13
12
|
auto_editor/wavfile.py,sha256=1HbZ4L8IBD6Fbg3pd5MQG4ZXy48YZA05t8XllSplhWk,9499
|
14
13
|
auto_editor/formats/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
@@ -28,9 +27,9 @@ auto_editor/lib/contracts.py,sha256=lExGQymcQUmwG5lC1lO4qm4GY8W0q_yzK_miTaAoPA4,
|
|
28
27
|
auto_editor/lib/data_structs.py,sha256=dcsXgsLLzbmFDUZucoirzewPALsKzoxz7z5L22_QJM8,7091
|
29
28
|
auto_editor/lib/err.py,sha256=UlszQJdzMZwkbT8x3sY4GkCV_5x9yrd6uVVUzvA8iiI,35
|
30
29
|
auto_editor/render/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
31
|
-
auto_editor/render/audio.py,sha256=
|
32
|
-
auto_editor/render/subtitle.py,sha256=
|
33
|
-
auto_editor/render/video.py,sha256=
|
30
|
+
auto_editor/render/audio.py,sha256=QF3nQMChIxL0GVy4AONvsnqrX_nqT31RrogLwxwWAO8,9650
|
31
|
+
auto_editor/render/subtitle.py,sha256=HCWL9HvFXZPeGUd0mI1K5okHnKf1f6hw-DgPFFPOKA0,4499
|
32
|
+
auto_editor/render/video.py,sha256=gQJx_zcqvL__WJNG8mfFWblSr8XFkuCeIW3nCXoIdKU,12487
|
34
33
|
auto_editor/subcommands/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
35
34
|
auto_editor/subcommands/desc.py,sha256=GDrKJYiHMaeTrplZAceXl1JwoqD78XsV2_5lc0Xd7po,869
|
36
35
|
auto_editor/subcommands/info.py,sha256=t5n43HLt9hpMFSIfGV777X4zIPBAFugOKlpCfRjiKxY,6921
|
@@ -38,21 +37,20 @@ auto_editor/subcommands/levels.py,sha256=ChJMDTd34-jgxewqHRmmd3VNhFdy964w0DcQG0l
|
|
38
37
|
auto_editor/subcommands/palet.py,sha256=ONzTqemaQq9YEfIOsDRNnwzfqnEMUMSXIQrETxyroRU,749
|
39
38
|
auto_editor/subcommands/repl.py,sha256=DuMz5kImoZFSVMZh6sPQxqZXMbRXPCvXoW3G-MJfivc,3166
|
40
39
|
auto_editor/subcommands/subdump.py,sha256=af_XBf7kaevqHn1A71z8C-7x8pS5WKD9FE_ugkCw6rk,665
|
41
|
-
auto_editor/subcommands/test.py,sha256=
|
40
|
+
auto_editor/subcommands/test.py,sha256=ovYMX5_Fm1qSq8_ojnBj78W9vqkZImoNkbdCSIFFft0,25668
|
42
41
|
auto_editor/utils/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
43
42
|
auto_editor/utils/bar.py,sha256=hG_NiYeuM90TdILzAJORft-UOS5grwWN3SbRuj6upsI,3998
|
44
43
|
auto_editor/utils/chunks.py,sha256=J-eGKtEz68gFtRrj1kOSgH4Tj_Yz6prNQ7Xr-d9NQJw,52
|
45
44
|
auto_editor/utils/cmdkw.py,sha256=uW_qDGQ6UzLPRbB20HTLrCmhMlWVXSfgyQMr4MyGErU,5734
|
46
45
|
auto_editor/utils/container.py,sha256=RnpoMmMYmn7o69LmMbBFHW4TsP3K52jYDhG9qzWXmAs,2720
|
47
46
|
auto_editor/utils/encoder.py,sha256=auNYo7HXbcU4iTUCc0LE5lpwFmSvdWvBm6-5KIaRK8w,2983
|
48
|
-
auto_editor/utils/func.py,sha256=
|
47
|
+
auto_editor/utils/func.py,sha256=3p5f6BxC6wMjsolLSfrcx7OwVQnMRAprwBI7MQ8TjB0,3725
|
49
48
|
auto_editor/utils/log.py,sha256=M2QKeQHMRNLm3HMVUKedZPRprT2u5dipOStiO4miPBk,3613
|
50
|
-
auto_editor/utils/subtitle_tools.py,sha256=TjjVPiT8bWzZJcrZjF7ddpgfIsVkLE4LyxXzBswHAGU,693
|
51
49
|
auto_editor/utils/types.py,sha256=BWj0YalUpWwXfEN7AEL5s_j22lscZOH-eb7x_dYuXfY,11471
|
52
|
-
docs/build.py,sha256=
|
53
|
-
auto_editor-25.3.
|
54
|
-
auto_editor-25.3.
|
55
|
-
auto_editor-25.3.
|
56
|
-
auto_editor-25.3.
|
57
|
-
auto_editor-25.3.
|
58
|
-
auto_editor-25.3.
|
50
|
+
docs/build.py,sha256=CM-ZWgQk8wSNjivx_-6wGIaG7cstrNKsX2d4TzFVivE,1642
|
51
|
+
auto_editor-25.3.1.dist-info/LICENSE,sha256=yiq99pWITHfqS0pbZMp7cy2dnbreTuvBwudsU-njvIM,1210
|
52
|
+
auto_editor-25.3.1.dist-info/METADATA,sha256=0GOm0ktkMSsxqnTtALdMBcxK_IrYPMgFuSjUN-Xs9F0,6148
|
53
|
+
auto_editor-25.3.1.dist-info/WHEEL,sha256=GV9aMThwP_4oNCtvEC2ec3qUYutgWeAzklro_0m4WJQ,91
|
54
|
+
auto_editor-25.3.1.dist-info/entry_points.txt,sha256=-H7zdTw4MqnAcwrN5xTNkGIhzZtJMxS9r6lTMeR9-aA,240
|
55
|
+
auto_editor-25.3.1.dist-info/top_level.txt,sha256=jBV5zlbWRbKOa-xaWPvTD45QL7lGExx2BDzv-Ji4dTw,17
|
56
|
+
auto_editor-25.3.1.dist-info/RECORD,,
|
docs/build.py
CHANGED
@@ -1,29 +0,0 @@
|
|
1
|
-
def convert_ass_to_text(ass_text: str) -> str:
|
2
|
-
result = ""
|
3
|
-
comma_count = i = 0
|
4
|
-
|
5
|
-
while comma_count < 8 and i < len(ass_text):
|
6
|
-
if ass_text[i] == ",":
|
7
|
-
comma_count += 1
|
8
|
-
i += 1
|
9
|
-
|
10
|
-
state = False
|
11
|
-
while i < len(ass_text):
|
12
|
-
char = ass_text[i]
|
13
|
-
next_char = "" if i + 1 >= len(ass_text) else ass_text[i + 1]
|
14
|
-
|
15
|
-
if char == "\\" and next_char == "N":
|
16
|
-
result += "\n"
|
17
|
-
i += 2
|
18
|
-
continue
|
19
|
-
|
20
|
-
if not state:
|
21
|
-
if char == "{":
|
22
|
-
state = True
|
23
|
-
else:
|
24
|
-
result += ass_text[i]
|
25
|
-
elif char == "}":
|
26
|
-
state = False
|
27
|
-
i += 1
|
28
|
-
|
29
|
-
return result
|
auto_editor/validate_input.py
DELETED
@@ -1,88 +0,0 @@
|
|
1
|
-
from __future__ import annotations
|
2
|
-
|
3
|
-
import re
|
4
|
-
import subprocess
|
5
|
-
import sys
|
6
|
-
from os.path import exists, isdir, isfile, lexists, splitext
|
7
|
-
|
8
|
-
from auto_editor.ffwrapper import FFmpeg
|
9
|
-
from auto_editor.utils.func import get_stdout
|
10
|
-
from auto_editor.utils.log import Log
|
11
|
-
from auto_editor.utils.types import Args
|
12
|
-
|
13
|
-
|
14
|
-
def get_domain(url: str) -> str:
|
15
|
-
from urllib.parse import urlparse
|
16
|
-
|
17
|
-
t = urlparse(url).netloc
|
18
|
-
return ".".join(t.split(".")[-2:])
|
19
|
-
|
20
|
-
|
21
|
-
def download_video(my_input: str, args: Args, ffmpeg: FFmpeg, log: Log) -> str:
|
22
|
-
log.conwrite("Downloading video...")
|
23
|
-
|
24
|
-
download_format = args.download_format
|
25
|
-
|
26
|
-
if download_format is None and get_domain(my_input) == "youtube.com":
|
27
|
-
download_format = "bestvideo[ext=mp4]+bestaudio[ext=m4a]"
|
28
|
-
|
29
|
-
if args.output_format is None:
|
30
|
-
output_format = re.sub(r"\W+", "-", splitext(my_input)[0]) + ".%(ext)s"
|
31
|
-
else:
|
32
|
-
output_format = args.output_format
|
33
|
-
|
34
|
-
yt_dlp_path = args.yt_dlp_location
|
35
|
-
|
36
|
-
cmd = ["--ffmpeg-location", ffmpeg.path]
|
37
|
-
|
38
|
-
if download_format is not None:
|
39
|
-
cmd.extend(["-f", download_format])
|
40
|
-
|
41
|
-
cmd.extend(["-o", output_format, my_input])
|
42
|
-
|
43
|
-
if args.yt_dlp_extras is not None:
|
44
|
-
cmd.extend(args.yt_dlp_extras.split(" "))
|
45
|
-
|
46
|
-
try:
|
47
|
-
location = get_stdout(
|
48
|
-
[yt_dlp_path, "--get-filename", "--no-warnings"] + cmd
|
49
|
-
).strip()
|
50
|
-
except FileNotFoundError:
|
51
|
-
msg = "Could not find program 'yt-dlp' when attempting to download a URL. Install yt-dlp with "
|
52
|
-
if sys.platform == "win32":
|
53
|
-
msg += "your favorite package manager (pip, choco, winget)."
|
54
|
-
elif sys.platform == "darwin":
|
55
|
-
msg += "brew or pip and make sure it's in PATH."
|
56
|
-
else:
|
57
|
-
msg += "pip or your favorite package manager and make sure it's in PATH."
|
58
|
-
log.error(msg)
|
59
|
-
|
60
|
-
if not isfile(location):
|
61
|
-
subprocess.run([yt_dlp_path] + cmd)
|
62
|
-
|
63
|
-
if not isfile(location):
|
64
|
-
log.error(f"Download file wasn't created: {location}")
|
65
|
-
|
66
|
-
return location
|
67
|
-
|
68
|
-
|
69
|
-
def valid_input(inputs: list[str], ffmpeg: FFmpeg, args: Args, log: Log) -> list[str]:
|
70
|
-
result = []
|
71
|
-
|
72
|
-
for my_input in inputs:
|
73
|
-
if my_input.startswith("http://") or my_input.startswith("https://"):
|
74
|
-
result.append(download_video(my_input, args, ffmpeg, log))
|
75
|
-
else:
|
76
|
-
_, ext = splitext(my_input)
|
77
|
-
if ext == "":
|
78
|
-
if isdir(my_input):
|
79
|
-
log.error("Input must be a file or a URL, not a directory.")
|
80
|
-
if exists(my_input):
|
81
|
-
log.error(f"Input file must have an extension: {my_input}")
|
82
|
-
if lexists(my_input):
|
83
|
-
log.error(f"Input file is a broken symbolic link: {my_input}")
|
84
|
-
if my_input.startswith("-"):
|
85
|
-
log.error(f"Option/Input file doesn't exist: {my_input}")
|
86
|
-
result.append(my_input)
|
87
|
-
|
88
|
-
return result
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|