auto-editor 24.24.1__py3-none-any.whl → 24.25.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 +2 -2
- auto_editor/__main__.py +26 -23
- auto_editor/analyze.py +54 -19
- auto_editor/edit.py +4 -14
- auto_editor/formats/fcp7.py +1 -1
- auto_editor/make_layers.py +45 -7
- auto_editor/output.py +11 -10
- auto_editor/render/subtitle.py +7 -13
- auto_editor/subcommands/info.py +1 -1
- auto_editor/utils/log.py +33 -34
- auto_editor/utils/subtitle_tools.py +29 -0
- auto_editor/validate_input.py +1 -1
- auto_editor/vanparse.py +23 -24
- {auto_editor-24.24.1.dist-info → auto_editor-24.25.1.dist-info}/METADATA +5 -7
- {auto_editor-24.24.1.dist-info → auto_editor-24.25.1.dist-info}/RECORD +19 -18
- {auto_editor-24.24.1.dist-info → auto_editor-24.25.1.dist-info}/WHEEL +1 -1
- {auto_editor-24.24.1.dist-info → auto_editor-24.25.1.dist-info}/LICENSE +0 -0
- {auto_editor-24.24.1.dist-info → auto_editor-24.25.1.dist-info}/entry_points.txt +0 -0
- {auto_editor-24.24.1.dist-info → auto_editor-24.25.1.dist-info}/top_level.txt +0 -0
auto_editor/__init__.py
CHANGED
@@ -1,2 +1,2 @@
|
|
1
|
-
__version__ = "24.
|
2
|
-
version = "
|
1
|
+
__version__ = "24.25.1"
|
2
|
+
version = "24w25a"
|
auto_editor/__main__.py
CHANGED
@@ -1,10 +1,13 @@
|
|
1
1
|
#!/usr/bin/env python3
|
2
2
|
|
3
3
|
import sys
|
4
|
+
from os import environ
|
4
5
|
|
5
6
|
import auto_editor
|
7
|
+
from auto_editor.edit import edit_media
|
8
|
+
from auto_editor.ffwrapper import FFmpeg
|
6
9
|
from auto_editor.utils.func import setup_tempdir
|
7
|
-
from auto_editor.utils.log import Log
|
10
|
+
from auto_editor.utils.log import Log
|
8
11
|
from auto_editor.utils.types import (
|
9
12
|
Args,
|
10
13
|
bitrate,
|
@@ -277,11 +280,16 @@ def main() -> None:
|
|
277
280
|
f"auto_editor.subcommands.{sys.argv[1]}", fromlist=["subcommands"]
|
278
281
|
)
|
279
282
|
obj.main(sys.argv[2:])
|
280
|
-
|
283
|
+
return
|
284
|
+
|
285
|
+
ff_color = "AV_LOG_FORCE_NOCOLOR"
|
286
|
+
no_color = bool(environ.get("NO_COLOR")) or bool(environ.get(ff_color))
|
287
|
+
log = Log(no_color=no_color)
|
281
288
|
|
282
289
|
args = main_options(ArgumentParser("Auto-Editor")).parse_args(
|
283
290
|
Args,
|
284
291
|
sys.argv[1:],
|
292
|
+
log,
|
285
293
|
macros=[
|
286
294
|
({"--frame-margin"}, ["--margin"]),
|
287
295
|
({"--export-to-premiere", "-exp"}, ["--export", "premiere"]),
|
@@ -296,41 +304,36 @@ def main() -> None:
|
|
296
304
|
|
297
305
|
if args.version:
|
298
306
|
print(f"{auto_editor.version} ({auto_editor.__version__})")
|
299
|
-
|
300
|
-
|
301
|
-
from auto_editor.edit import edit_media
|
302
|
-
from auto_editor.ffwrapper import FFmpeg
|
303
|
-
|
304
|
-
log = Log(args.debug, args.quiet)
|
305
|
-
ffmpeg = FFmpeg(
|
306
|
-
args.ffmpeg_location,
|
307
|
-
args.my_ffmpeg,
|
308
|
-
args.show_ffmpeg_commands,
|
309
|
-
args.show_ffmpeg_output,
|
310
|
-
)
|
307
|
+
return
|
311
308
|
|
312
|
-
if args.debug and args.input
|
309
|
+
if args.debug and not args.input:
|
313
310
|
import platform as plat
|
314
311
|
|
312
|
+
import av
|
313
|
+
|
315
314
|
print(f"Python Version: {plat.python_version()}")
|
316
315
|
print(f"Platform: {plat.system()} {plat.release()} {plat.machine().lower()}")
|
317
|
-
print(f"
|
316
|
+
print(f"PyAV Version: {av.__version__}")
|
318
317
|
print(f"Auto-Editor Version: {auto_editor.version}")
|
319
|
-
|
318
|
+
return
|
320
319
|
|
321
|
-
if args.input
|
320
|
+
if not args.input:
|
322
321
|
log.error("You need to give auto-editor an input file.")
|
323
322
|
|
324
|
-
temp = setup_tempdir(args.temp_dir,
|
325
|
-
log = Log(args.debug, args.quiet, temp)
|
326
|
-
log.machine = args.progress == "machine"
|
323
|
+
temp = setup_tempdir(args.temp_dir, log)
|
324
|
+
log = Log(args.debug, args.quiet, temp, args.progress == "machine", no_color)
|
327
325
|
log.debug(f"Temp Directory: {temp}")
|
328
326
|
|
327
|
+
ffmpeg = FFmpeg(
|
328
|
+
args.ffmpeg_location,
|
329
|
+
args.my_ffmpeg,
|
330
|
+
args.show_ffmpeg_commands,
|
331
|
+
args.show_ffmpeg_output,
|
332
|
+
)
|
329
333
|
paths = valid_input(args.input, ffmpeg, args, log)
|
330
|
-
timer = Timer(args.quiet or log.machine)
|
331
334
|
|
332
335
|
try:
|
333
|
-
edit_media(paths, ffmpeg, args, temp,
|
336
|
+
edit_media(paths, ffmpeg, args, temp, log)
|
334
337
|
except KeyboardInterrupt:
|
335
338
|
log.error("Keyboard Interrupt")
|
336
339
|
log.cleanup()
|
auto_editor/analyze.py
CHANGED
@@ -3,6 +3,7 @@ from __future__ import annotations
|
|
3
3
|
import os
|
4
4
|
import re
|
5
5
|
from dataclasses import dataclass
|
6
|
+
from fractions import Fraction
|
6
7
|
from typing import TYPE_CHECKING
|
7
8
|
|
8
9
|
import numpy as np
|
@@ -19,12 +20,12 @@ from auto_editor.lib.contracts import (
|
|
19
20
|
orc,
|
20
21
|
)
|
21
22
|
from auto_editor.lib.data_structs import Sym
|
22
|
-
from auto_editor.render.subtitle import SubtitleParser
|
23
23
|
from auto_editor.utils.cmdkw import (
|
24
24
|
Required,
|
25
25
|
pAttr,
|
26
26
|
pAttrs,
|
27
27
|
)
|
28
|
+
from auto_editor.utils.subtitle_tools import convert_ass_to_text
|
28
29
|
from auto_editor.wavfile import read
|
29
30
|
|
30
31
|
if TYPE_CHECKING:
|
@@ -307,31 +308,65 @@ class Levels:
|
|
307
308
|
except re.error as e:
|
308
309
|
self.log.error(e)
|
309
310
|
|
310
|
-
|
311
|
-
|
311
|
+
import av
|
312
|
+
from av.subtitles.subtitle import AssSubtitle, TextSubtitle
|
312
313
|
|
313
|
-
|
314
|
-
|
314
|
+
try:
|
315
|
+
container = av.open(self.src.path, "r")
|
316
|
+
subtitle_stream = container.streams.subtitles[stream]
|
317
|
+
assert isinstance(subtitle_stream.time_base, Fraction)
|
318
|
+
except Exception as e:
|
319
|
+
self.log.error(e)
|
315
320
|
|
316
|
-
#
|
317
|
-
|
318
|
-
|
319
|
-
|
321
|
+
# Get the length of the subtitle stream.
|
322
|
+
sub_length = 0
|
323
|
+
for packet in container.demux(subtitle_stream):
|
324
|
+
if packet.pts is None or packet.duration is None:
|
325
|
+
continue
|
326
|
+
for subset in packet.decode():
|
327
|
+
# See definition of `AVSubtitle`
|
328
|
+
# in: https://ffmpeg.org/doxygen/trunk/avcodec_8h_source.html
|
329
|
+
start = float(packet.pts * subtitle_stream.time_base)
|
330
|
+
dur = float(packet.duration * subtitle_stream.time_base)
|
320
331
|
|
321
|
-
|
322
|
-
|
332
|
+
end = round((start + dur) * self.tb)
|
333
|
+
sub_length = max(sub_length, end)
|
323
334
|
|
324
|
-
result = np.zeros((
|
335
|
+
result = np.zeros((sub_length), dtype=np.bool_)
|
336
|
+
del sub_length
|
325
337
|
|
326
338
|
count = 0
|
327
|
-
|
328
|
-
|
339
|
+
early_exit = False
|
340
|
+
container.seek(0)
|
341
|
+
for packet in container.demux(subtitle_stream):
|
342
|
+
if packet.pts is None or packet.duration is None:
|
343
|
+
continue
|
344
|
+
if early_exit:
|
329
345
|
break
|
330
|
-
|
331
|
-
|
332
|
-
|
333
|
-
|
334
|
-
|
346
|
+
for subset in packet.decode():
|
347
|
+
if max_count is not None and count >= max_count:
|
348
|
+
early_exit = True
|
349
|
+
break
|
350
|
+
|
351
|
+
start = float(packet.pts * subtitle_stream.time_base)
|
352
|
+
dur = float(packet.duration * subtitle_stream.time_base)
|
353
|
+
|
354
|
+
san_start = round(start * self.tb)
|
355
|
+
san_end = round((start + dur) * self.tb)
|
356
|
+
|
357
|
+
for sub in subset:
|
358
|
+
if isinstance(sub, AssSubtitle):
|
359
|
+
line = convert_ass_to_text(sub.ass.decode(errors="ignore"))
|
360
|
+
elif isinstance(sub, TextSubtitle):
|
361
|
+
line = sub.text.decode(errors="ignore")
|
362
|
+
else:
|
363
|
+
continue
|
364
|
+
|
365
|
+
if line and re.search(pattern, line):
|
366
|
+
result[san_start:san_end] = 1
|
367
|
+
count += 1
|
368
|
+
|
369
|
+
container.close()
|
335
370
|
|
336
371
|
return result
|
337
372
|
|
auto_editor/edit.py
CHANGED
@@ -15,7 +15,7 @@ from auto_editor.utils.bar import Bar
|
|
15
15
|
from auto_editor.utils.chunks import Chunk, Chunks
|
16
16
|
from auto_editor.utils.cmdkw import ParserError, parse_with_palet, pAttr, pAttrs
|
17
17
|
from auto_editor.utils.container import Container, container_constructor
|
18
|
-
from auto_editor.utils.log import Log
|
18
|
+
from auto_editor.utils.log import Log
|
19
19
|
from auto_editor.utils.types import Args
|
20
20
|
|
21
21
|
|
@@ -150,7 +150,7 @@ def parse_export(export: str, log: Log) -> dict[str, Any]:
|
|
150
150
|
|
151
151
|
|
152
152
|
def edit_media(
|
153
|
-
paths: list[str], ffmpeg: FFmpeg, args: Args, temp: str,
|
153
|
+
paths: list[str], ffmpeg: FFmpeg, args: Args, temp: str, log: Log
|
154
154
|
) -> None:
|
155
155
|
bar = Bar(args.progress)
|
156
156
|
tl = None
|
@@ -190,7 +190,6 @@ def edit_media(
|
|
190
190
|
|
191
191
|
if export["export"] == "timeline":
|
192
192
|
log.quiet = True
|
193
|
-
timer.quiet = True
|
194
193
|
|
195
194
|
if not args.preview:
|
196
195
|
log.conwrite("Starting")
|
@@ -213,15 +212,6 @@ def edit_media(
|
|
213
212
|
ensure = Ensure(ffmpeg, samplerate, temp, log)
|
214
213
|
|
215
214
|
if tl is None:
|
216
|
-
# Extract subtitles in their native format.
|
217
|
-
if src is not None and len(src.subtitles) > 0 and not args.sn:
|
218
|
-
cmd = ["-i", f"{src.path}", "-hide_banner"]
|
219
|
-
for s, sub in enumerate(src.subtitles):
|
220
|
-
cmd.extend(["-map", f"0:s:{s}"])
|
221
|
-
for s, sub in enumerate(src.subtitles):
|
222
|
-
cmd.extend([os.path.join(temp, f"{s}s.{sub.ext}")])
|
223
|
-
ffmpeg.run(cmd)
|
224
|
-
|
225
215
|
tl = make_timeline(sources, ensure, args, samplerate, bar, temp, log)
|
226
216
|
|
227
217
|
if export["export"] == "timeline":
|
@@ -283,7 +273,7 @@ def edit_media(
|
|
283
273
|
apply_later = False
|
284
274
|
|
285
275
|
if ctr.allow_subtitle and not args.sn:
|
286
|
-
sub_output = make_new_subtitles(tl,
|
276
|
+
sub_output = make_new_subtitles(tl, ensure, temp)
|
287
277
|
|
288
278
|
if ctr.allow_audio:
|
289
279
|
audio_output = make_new_audio(tl, ensure, args, ffmpeg, bar, temp, log)
|
@@ -357,7 +347,7 @@ def edit_media(
|
|
357
347
|
else:
|
358
348
|
make_media(tl, output)
|
359
349
|
|
360
|
-
|
350
|
+
log.stop_timer()
|
361
351
|
|
362
352
|
if not args.no_open and export["export"] in ("default", "audio", "clip-sequence"):
|
363
353
|
if args.player is None:
|
auto_editor/formats/fcp7.py
CHANGED
auto_editor/make_layers.py
CHANGED
@@ -1,6 +1,7 @@
|
|
1
1
|
from __future__ import annotations
|
2
2
|
|
3
3
|
from fractions import Fraction
|
4
|
+
from math import ceil
|
4
5
|
from typing import TYPE_CHECKING, NamedTuple
|
5
6
|
|
6
7
|
import numpy as np
|
@@ -112,8 +113,13 @@ def run_interpreter_for_edit_option(
|
|
112
113
|
|
113
114
|
def make_sane_timebase(fps: Fraction) -> Fraction:
|
114
115
|
tb = round(fps, 2)
|
116
|
+
|
117
|
+
ntsc_60 = Fraction(60_000, 1001)
|
115
118
|
ntsc = Fraction(30_000, 1001)
|
116
119
|
film_ntsc = Fraction(24_000, 1001)
|
120
|
+
|
121
|
+
if tb == round(ntsc_60, 2):
|
122
|
+
return ntsc_60
|
117
123
|
if tb == round(ntsc, 2):
|
118
124
|
return ntsc
|
119
125
|
if tb == round(film_ntsc, 2):
|
@@ -226,21 +232,35 @@ def make_timeline(
|
|
226
232
|
chunks.append((src, start, arr_length, speed_map[arr[j]]))
|
227
233
|
return chunks
|
228
234
|
|
235
|
+
# Assert timeline is monotonic because non-monotonic timelines are incorrect
|
236
|
+
# here and causes back-seeking (performance issue) in video rendering.
|
237
|
+
|
238
|
+
# We don't properly check monotonicity for multiple sources, so skip those.
|
239
|
+
|
240
|
+
check_monotonic = len(sources) == 1
|
241
|
+
last_i = 0
|
242
|
+
|
229
243
|
clips: list[Clip] = []
|
230
|
-
i = 0
|
231
244
|
start = 0
|
245
|
+
|
232
246
|
for chunk in echunk(speed_index, src_index):
|
233
247
|
if chunk[3] != 99999:
|
234
|
-
dur =
|
248
|
+
dur = int((chunk[2] - chunk[1]) / chunk[3])
|
235
249
|
if dur == 0:
|
236
250
|
continue
|
237
251
|
|
238
|
-
offset =
|
252
|
+
offset = ceil(chunk[1] / chunk[3])
|
253
|
+
|
254
|
+
if check_monotonic:
|
255
|
+
max_end = start + dur - 1
|
256
|
+
this_i = round((offset + max_end - start) * chunk[3])
|
257
|
+
if this_i < last_i:
|
258
|
+
raise ValueError("not monotonic", sources, this_i, last_i)
|
259
|
+
last_i = this_i
|
260
|
+
|
261
|
+
clips.append(Clip(start, dur, offset, chunk[3], chunk[0]))
|
239
262
|
|
240
|
-
if not (clips and clips[-1].start == round(start)):
|
241
|
-
clips.append(Clip(start, dur, offset, chunk[3], chunk[0]))
|
242
263
|
start += dur
|
243
|
-
i += 1
|
244
264
|
|
245
265
|
vtl: VSpace = []
|
246
266
|
atl: ASpace = []
|
@@ -276,4 +296,22 @@ def make_timeline(
|
|
276
296
|
else:
|
277
297
|
v1_compatiable = None
|
278
298
|
|
279
|
-
|
299
|
+
tl = v3(inp, tb, sr, res, args.background, vtl, atl, v1_compatiable)
|
300
|
+
|
301
|
+
# Additional monotonic check, o(n^2) time complexity so disable by default.
|
302
|
+
|
303
|
+
# if len(sources) != 1:
|
304
|
+
# return tl
|
305
|
+
|
306
|
+
# last_i = 0
|
307
|
+
# for index in range(tl.end):
|
308
|
+
# for layer in tl.v:
|
309
|
+
# for lobj in layer:
|
310
|
+
# if index >= lobj.start and index < (lobj.start + lobj.dur):
|
311
|
+
# _i = round((lobj.offset + index - lobj.start) * lobj.speed)
|
312
|
+
# if (_i < last_i):
|
313
|
+
# print(_i, last_i)
|
314
|
+
# raise ValueError("not monotonic")
|
315
|
+
# last_i = _i
|
316
|
+
|
317
|
+
return tl
|
auto_editor/output.py
CHANGED
@@ -16,21 +16,22 @@ class Ensure:
|
|
16
16
|
_sr: int
|
17
17
|
temp: str
|
18
18
|
log: Log
|
19
|
-
|
20
|
-
|
19
|
+
_audios: list[tuple[FileInfo, int]] = field(default_factory=list)
|
20
|
+
_subtitles: list[tuple[FileInfo, int, str]] = field(default_factory=list)
|
21
21
|
|
22
22
|
def audio(self, src: FileInfo, stream: int) -> str:
|
23
23
|
try:
|
24
|
-
label = self.
|
24
|
+
label = self._audios.index((src, stream))
|
25
25
|
first_time = False
|
26
26
|
except ValueError:
|
27
|
-
self.
|
28
|
-
label = len(self.
|
27
|
+
self._audios.append((src, stream))
|
28
|
+
label = len(self._audios) - 1
|
29
29
|
first_time = True
|
30
30
|
|
31
31
|
out_path = os.path.join(self.temp, f"{label:x}.wav")
|
32
32
|
|
33
33
|
if first_time:
|
34
|
+
self.log.debug(f"Making external audio: {out_path}")
|
34
35
|
self.log.conwrite("Extracting audio")
|
35
36
|
|
36
37
|
cmd = ["-i", f"{src.path}", "-map", f"0:a:{stream}"]
|
@@ -39,18 +40,18 @@ class Ensure:
|
|
39
40
|
|
40
41
|
return out_path
|
41
42
|
|
42
|
-
def subtitle(self, src: FileInfo, stream: int) -> str:
|
43
|
+
def subtitle(self, src: FileInfo, stream: int, ext: str) -> str:
|
43
44
|
try:
|
44
|
-
|
45
|
+
self._subtitles.index((src, stream, ext))
|
45
46
|
first_time = False
|
46
47
|
except ValueError:
|
47
|
-
self.
|
48
|
-
label = len(self.sub_labels) - 1
|
48
|
+
self._subtitles.append((src, stream, ext))
|
49
49
|
first_time = True
|
50
50
|
|
51
|
-
out_path = os.path.join(self.temp, f"{
|
51
|
+
out_path = os.path.join(self.temp, f"{stream}s.{ext}")
|
52
52
|
|
53
53
|
if first_time:
|
54
|
+
self.log.debug(f"Making external subtitle: {out_path}")
|
54
55
|
self.log.conwrite("Extracting subtitle")
|
55
56
|
self._ffmpeg.run(["-i", f"{src.path}", "-map", f"0:s:{stream}", out_path])
|
56
57
|
|
auto_editor/render/subtitle.py
CHANGED
@@ -10,10 +10,9 @@ from auto_editor.utils.func import to_timecode
|
|
10
10
|
if TYPE_CHECKING:
|
11
11
|
from fractions import Fraction
|
12
12
|
|
13
|
-
from auto_editor.
|
13
|
+
from auto_editor.output import Ensure
|
14
14
|
from auto_editor.timeline import v3
|
15
15
|
from auto_editor.utils.chunks import Chunks
|
16
|
-
from auto_editor.utils.log import Log
|
17
16
|
|
18
17
|
|
19
18
|
@dataclass(slots=True)
|
@@ -122,26 +121,21 @@ class SubtitleParser:
|
|
122
121
|
file.write(self.footer)
|
123
122
|
|
124
123
|
|
125
|
-
def make_new_subtitles(tl: v3,
|
124
|
+
def make_new_subtitles(tl: v3, ensure: Ensure, temp: str) -> list[str]:
|
126
125
|
if tl.v1 is None:
|
127
126
|
return []
|
128
127
|
|
129
128
|
new_paths = []
|
130
129
|
|
131
130
|
for s, sub in enumerate(tl.v1.source.subtitles):
|
132
|
-
file_path = os.path.join(temp, f"{s}s.{sub.ext}")
|
133
131
|
new_path = os.path.join(temp, f"new{s}s.{sub.ext}")
|
134
|
-
|
135
132
|
parser = SubtitleParser(tl.tb)
|
136
133
|
|
137
|
-
if sub.codec in parser.supported_codecs
|
138
|
-
|
139
|
-
|
140
|
-
|
141
|
-
|
142
|
-
ffmpeg.run(["-i", file_path, convert_path])
|
143
|
-
with open(convert_path, encoding="utf-8") as file:
|
144
|
-
parser.parse(file.read(), "webvtt")
|
134
|
+
ext = sub.ext if sub.codec in parser.supported_codecs else "vtt"
|
135
|
+
file_path = ensure.subtitle(tl.v1.source, s, ext)
|
136
|
+
|
137
|
+
with open(file_path, encoding="utf-8") as file:
|
138
|
+
parser.parse(file.read(), sub.codec)
|
145
139
|
|
146
140
|
parser.edit(tl.v1.chunks)
|
147
141
|
parser.write(new_path)
|
auto_editor/subcommands/info.py
CHANGED
auto_editor/utils/log.py
CHANGED
@@ -2,37 +2,28 @@ from __future__ import annotations
|
|
2
2
|
|
3
3
|
import sys
|
4
4
|
from datetime import timedelta
|
5
|
-
from pathlib import Path
|
6
5
|
from shutil import get_terminal_size, rmtree
|
7
6
|
from time import perf_counter, sleep
|
8
7
|
from typing import NoReturn
|
9
8
|
|
10
9
|
|
11
|
-
class Timer:
|
12
|
-
__slots__ = ("start_time", "quiet")
|
13
|
-
|
14
|
-
def __init__(self, quiet: bool = False):
|
15
|
-
self.start_time = perf_counter()
|
16
|
-
self.quiet = quiet
|
17
|
-
|
18
|
-
def stop(self) -> None:
|
19
|
-
if not self.quiet:
|
20
|
-
second_len = round(perf_counter() - self.start_time, 2)
|
21
|
-
minute_len = timedelta(seconds=round(second_len))
|
22
|
-
|
23
|
-
sys.stdout.write(f"Finished. took {second_len} seconds ({minute_len})\n")
|
24
|
-
|
25
|
-
|
26
10
|
class Log:
|
27
|
-
__slots__ = ("is_debug", "quiet", "temp", "machine")
|
11
|
+
__slots__ = ("is_debug", "quiet", "temp", "machine", "start_time", "no_color")
|
28
12
|
|
29
13
|
def __init__(
|
30
|
-
self,
|
14
|
+
self,
|
15
|
+
is_debug: bool = False,
|
16
|
+
quiet: bool = False,
|
17
|
+
temp: str | None = None,
|
18
|
+
machine: bool = False,
|
19
|
+
no_color: bool = True,
|
31
20
|
):
|
32
|
-
self.is_debug =
|
21
|
+
self.is_debug = is_debug
|
33
22
|
self.quiet = quiet
|
34
23
|
self.temp = temp
|
35
|
-
self.machine =
|
24
|
+
self.machine = machine
|
25
|
+
self.no_color = no_color
|
26
|
+
self.start_time = 0 if self.quiet or self.machine else perf_counter()
|
36
27
|
|
37
28
|
def debug(self, message: object) -> None:
|
38
29
|
if self.is_debug:
|
@@ -62,13 +53,34 @@ class Log:
|
|
62
53
|
buffer = " " * (get_terminal_size().columns - len(message) - 3)
|
63
54
|
sys.stdout.write(f" {message}{buffer}\r")
|
64
55
|
|
56
|
+
def print(self, message: str) -> None:
|
57
|
+
if not self.quiet:
|
58
|
+
self.conwrite("")
|
59
|
+
sys.stdout.write(f"{message}\n")
|
60
|
+
|
61
|
+
def warning(self, message: str) -> None:
|
62
|
+
if not self.quiet:
|
63
|
+
self.conwrite("")
|
64
|
+
sys.stderr.write(f"Warning! {message}\n")
|
65
|
+
|
66
|
+
def stop_timer(self) -> None:
|
67
|
+
if not self.quiet and not self.machine:
|
68
|
+
second_len = round(perf_counter() - self.start_time, 2)
|
69
|
+
minute_len = timedelta(seconds=round(second_len))
|
70
|
+
|
71
|
+
sys.stdout.write(f"Finished. took {second_len} seconds ({minute_len})\n")
|
72
|
+
|
65
73
|
def error(self, message: str | Exception) -> NoReturn:
|
66
74
|
if self.is_debug and isinstance(message, Exception):
|
67
75
|
self.cleanup()
|
68
76
|
raise message
|
69
77
|
|
70
78
|
self.conwrite("")
|
71
|
-
|
79
|
+
if self.no_color:
|
80
|
+
sys.stderr.write(f"Error! {message}\n")
|
81
|
+
else:
|
82
|
+
sys.stderr.write(f"\033[31;40mError! {message}\033[0m\n")
|
83
|
+
|
72
84
|
self.cleanup()
|
73
85
|
from platform import system
|
74
86
|
|
@@ -81,16 +93,3 @@ class Log:
|
|
81
93
|
import os
|
82
94
|
|
83
95
|
os._exit(1)
|
84
|
-
|
85
|
-
def nofile(self, path: str | Path) -> NoReturn:
|
86
|
-
self.error(f"Could not find '{path}'")
|
87
|
-
|
88
|
-
def warning(self, message: str) -> None:
|
89
|
-
if not self.quiet:
|
90
|
-
self.conwrite("")
|
91
|
-
sys.stderr.write(f"Warning! {message}\n")
|
92
|
-
|
93
|
-
def print(self, message: str) -> None:
|
94
|
-
if not self.quiet:
|
95
|
-
self.conwrite("")
|
96
|
-
sys.stdout.write(f"{message}\n")
|
@@ -0,0 +1,29 @@
|
|
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
CHANGED
@@ -81,6 +81,6 @@ def valid_input(inputs: list[str], ffmpeg: FFmpeg, args: Args, log: Log) -> list
|
|
81
81
|
else:
|
82
82
|
if os.path.isdir(my_input):
|
83
83
|
log.error("Input must be a file or a URL, not a directory.")
|
84
|
-
log.
|
84
|
+
log.error(f"Could not find '{my_input}'")
|
85
85
|
|
86
86
|
return new_inputs
|
auto_editor/vanparse.py
CHANGED
@@ -160,25 +160,6 @@ def get_option(name: str, options: list[Options]) -> Options | None:
|
|
160
160
|
return None
|
161
161
|
|
162
162
|
|
163
|
-
def parse_value(option: Options | Required, val: str | None) -> Any:
|
164
|
-
if val is None and option.nargs == 1:
|
165
|
-
Log().error(f"{option.names[0]} needs argument.")
|
166
|
-
|
167
|
-
try:
|
168
|
-
value = option.type(val)
|
169
|
-
except CoerceError as e:
|
170
|
-
Log().error(e)
|
171
|
-
|
172
|
-
if option.choices is not None and value not in option.choices:
|
173
|
-
choices = ", ".join(option.choices)
|
174
|
-
|
175
|
-
Log().error(
|
176
|
-
f"{value} is not a choice for {option.names[0]}\nchoices are:\n {choices}"
|
177
|
-
)
|
178
|
-
|
179
|
-
return value
|
180
|
-
|
181
|
-
|
182
163
|
class ArgumentParser:
|
183
164
|
def __init__(self, program_name: str | None):
|
184
165
|
self.program_name = program_name
|
@@ -201,6 +182,7 @@ class ArgumentParser:
|
|
201
182
|
self,
|
202
183
|
ns_obj: type[T],
|
203
184
|
sys_args: list[str],
|
185
|
+
log_: Log | None = None,
|
204
186
|
macros: list[tuple[set[str], list[str]]] | None = None,
|
205
187
|
) -> T:
|
206
188
|
if not sys_args and self.program_name is not None:
|
@@ -219,9 +201,26 @@ class ArgumentParser:
|
|
219
201
|
del _macros
|
220
202
|
del macros
|
221
203
|
|
204
|
+
log = Log() if log_ is None else log_
|
222
205
|
ns = ns_obj()
|
223
206
|
option_names: list[str] = []
|
224
207
|
|
208
|
+
def parse_value(option: Options | Required, val: str | None) -> Any:
|
209
|
+
if val is None and option.nargs == 1:
|
210
|
+
log.error(f"{option.names[0]} needs argument.")
|
211
|
+
|
212
|
+
try:
|
213
|
+
value = option.type(val)
|
214
|
+
except CoerceError as e:
|
215
|
+
log.error(e)
|
216
|
+
|
217
|
+
if option.choices is not None and value not in option.choices:
|
218
|
+
log.error(
|
219
|
+
f"{value} is not a choice for {option.names[0]}\n"
|
220
|
+
f"choices are:\n {', '.join(option.choices)}"
|
221
|
+
)
|
222
|
+
return value
|
223
|
+
|
225
224
|
program_name = self.program_name
|
226
225
|
requireds = self.requireds
|
227
226
|
options = self.options
|
@@ -256,7 +255,7 @@ class ArgumentParser:
|
|
256
255
|
val = oplist_coerce(arg)
|
257
256
|
ns.__setattr__(oplist_name, getattr(ns, oplist_name) + [val])
|
258
257
|
except (CoerceError, ValueError) as e:
|
259
|
-
|
258
|
+
log.error(e)
|
260
259
|
elif requireds and not arg.startswith("--"):
|
261
260
|
if requireds[0].nargs == 1:
|
262
261
|
ns.__setattr__(req_list_name, parse_value(requireds[0], arg))
|
@@ -268,19 +267,19 @@ class ArgumentParser:
|
|
268
267
|
|
269
268
|
# 'Did you mean' message might appear that options need a comma.
|
270
269
|
if arg.replace(",", "") in option_names:
|
271
|
-
|
270
|
+
log.error(f"Option '{arg}' has an unnecessary comma.")
|
272
271
|
|
273
272
|
close_matches = difflib.get_close_matches(arg, option_names)
|
274
273
|
if close_matches:
|
275
|
-
|
274
|
+
log.error(
|
276
275
|
f"Unknown {label}: {arg}\n\n Did you mean:\n "
|
277
276
|
+ ", ".join(close_matches)
|
278
277
|
)
|
279
|
-
|
278
|
+
log.error(f"Unknown {label}: {arg}")
|
280
279
|
else:
|
281
280
|
if option.nargs != "*":
|
282
281
|
if option in used_options:
|
283
|
-
|
282
|
+
log.error(
|
284
283
|
f"Option {option.names[0]} may not be used more than once."
|
285
284
|
)
|
286
285
|
used_options.append(option)
|
@@ -1,6 +1,6 @@
|
|
1
1
|
Metadata-Version: 2.1
|
2
2
|
Name: auto-editor
|
3
|
-
Version: 24.
|
3
|
+
Version: 24.25.1
|
4
4
|
Summary: Auto-Editor: Effort free video editing!
|
5
5
|
Author-email: WyattBlue <wyattblue@auto-editor.com>
|
6
6
|
License: Unlicense
|
@@ -105,16 +105,14 @@ Auto-Editor can also export to:
|
|
105
105
|
- Individual media clips with `--export clip-sequence`
|
106
106
|
|
107
107
|
### Naming Timelines
|
108
|
-
By default, auto-editor will
|
108
|
+
Some editors support naming timelines. By default, auto-editor will use the name "Auto-Editor Media Group". For `premiere` `resolve` and `final-cut-pro` export options, you can change the name with the following syntax.
|
109
109
|
|
110
110
|
```
|
111
|
+
# for POSIX shells
|
111
112
|
auto-editor example.mp4 --export 'premiere:name="Your name here"'
|
112
113
|
|
113
|
-
|
114
|
-
|
115
|
-
auto-editor example.mp4 --export 'final-cut-pro:name="Your name here"'
|
116
|
-
|
117
|
-
# No other export options support naming
|
114
|
+
# for Powershell
|
115
|
+
auto-editor example.mp4 --export 'premiere:name=""Your name here""'
|
118
116
|
```
|
119
117
|
|
120
118
|
### Split by Clip
|
@@ -1,22 +1,22 @@
|
|
1
1
|
ae-ffmpeg/setup.py,sha256=HeORyrs8OyJ32lSnMaIhI2B7U1lkk3QP6wOjxpoiF3Y,1891
|
2
2
|
ae-ffmpeg/ae_ffmpeg/__init__.py,sha256=Fd2YsCINa0dB3tf9VVKDTPT9P6MDH-ve3RT2pqArImI,453
|
3
3
|
ae-ffmpeg/ae_ffmpeg/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
4
|
-
auto_editor/__init__.py,sha256=
|
5
|
-
auto_editor/__main__.py,sha256=
|
6
|
-
auto_editor/analyze.py,sha256=
|
7
|
-
auto_editor/edit.py,sha256=
|
4
|
+
auto_editor/__init__.py,sha256=n_J78DWpJcAL-7yvGTQUnjctoX2q1IQOSGKs6n5s6n8,43
|
5
|
+
auto_editor/__main__.py,sha256=u5bu0_aCxMGgGvzn1smKQmiqEyD85svsUFHwV-15Z3A,9893
|
6
|
+
auto_editor/analyze.py,sha256=NVDARF7ZxNLWMJ89HsuqCS1TMSeYhf2e5C66cQUTKrk,13200
|
7
|
+
auto_editor/edit.py,sha256=sJdjDtxMgKzPtNHcB9tgBCRkXbU_6qs1XCSmkPFNgCg,11495
|
8
8
|
auto_editor/ffwrapper.py,sha256=tZv12az2HLGjVjLfUukhjiuC75D_Ga6JJJppCrwJmMc,7618
|
9
9
|
auto_editor/help.py,sha256=BFiP7vBz42TUzum4-zaQIrV1OY7kHeN0pe0MPE0T5xw,7997
|
10
|
-
auto_editor/make_layers.py,sha256=
|
11
|
-
auto_editor/output.py,sha256=
|
10
|
+
auto_editor/make_layers.py,sha256=PqRU-9OEZwktN1qHTva59Vr23ZDWsjXsJWWuco7SzJM,9595
|
11
|
+
auto_editor/output.py,sha256=oa4VmZkU5d7U8DtMPFmI3ku0Q3Qola7huD_HogkkpEs,6825
|
12
12
|
auto_editor/preview.py,sha256=fo2BDIkvl96q_ssq8AAu1tl6FN_j23h8987aDPSmjDs,3094
|
13
13
|
auto_editor/timeline.py,sha256=JwcS-8AS5vsoTL_m03aosYijScQef4AGa2lyutQ8wbI,7069
|
14
|
-
auto_editor/validate_input.py,sha256=
|
15
|
-
auto_editor/vanparse.py,sha256=
|
14
|
+
auto_editor/validate_input.py,sha256=bngFLeV7YIOf8JIvoRRO55vzsg0qxVFngEHef5mpE00,2692
|
15
|
+
auto_editor/vanparse.py,sha256=zaTfmvxeN5m8Uy8rnRv6OUzCLE1EDz9wtoddBzJ8f8Y,10056
|
16
16
|
auto_editor/wavfile.py,sha256=7N2LX_WfFVRnoXrKveLvuyTYpIz2htpGqfCD8tR4kJ8,9168
|
17
17
|
auto_editor/formats/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
18
18
|
auto_editor/formats/fcp11.py,sha256=VwJWJs1qNDIVC8-pswipmKCk0e4V3LnE5fAMA0pPWVg,5857
|
19
|
-
auto_editor/formats/fcp7.py,sha256=
|
19
|
+
auto_editor/formats/fcp7.py,sha256=mO9PwcqPQyko1GdZNNAecU3PUqhTPOcxzbXITKFUyW0,17634
|
20
20
|
auto_editor/formats/json.py,sha256=Br-xHVHj59C0OPP2FwfJeht_fImepRXsaw0iDFvK7-w,7693
|
21
21
|
auto_editor/formats/shotcut.py,sha256=pbBQwOZ8Kqfm5ns0k_rBUX0XH_urIGfp77GORrzoW5Y,4984
|
22
22
|
auto_editor/formats/utils.py,sha256=GIZw28WHuCIaZ_zMI0v6Kxbq0QaIpbLsdSegdYwQxQ8,1990
|
@@ -30,11 +30,11 @@ auto_editor/lib/data_structs.py,sha256=EXNcdMsdmZxMRlpbXmIbRoC-YfGzvPZi7EdBQGwvp
|
|
30
30
|
auto_editor/lib/err.py,sha256=UlszQJdzMZwkbT8x3sY4GkCV_5x9yrd6uVVUzvA8iiI,35
|
31
31
|
auto_editor/render/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
32
32
|
auto_editor/render/audio.py,sha256=pUhD4rQZfUnyzKgpuxNxl_2CUGwbkAWo2356HUAW7VM,8835
|
33
|
-
auto_editor/render/subtitle.py,sha256=
|
33
|
+
auto_editor/render/subtitle.py,sha256=RUG0hNh0Mbt3IRxef9hKUpUPXvyTa3HVoqoSHYivAT4,4355
|
34
34
|
auto_editor/render/video.py,sha256=eSklzWvIdoagjJ7r4yTJMgHWUs2xqxAoF4gZtv90yIg,13184
|
35
35
|
auto_editor/subcommands/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
36
36
|
auto_editor/subcommands/desc.py,sha256=GDrKJYiHMaeTrplZAceXl1JwoqD78XsV2_5lc0Xd7po,869
|
37
|
-
auto_editor/subcommands/info.py,sha256=
|
37
|
+
auto_editor/subcommands/info.py,sha256=7Sgt9WR0rWxe7juCRKseMxW6gKv3z3voqFcL8-MOVVM,6927
|
38
38
|
auto_editor/subcommands/levels.py,sha256=XHMG3jsdoXBvG0TlP1bBbtjD0m5EgWnOMBTIYx8VAnA,4001
|
39
39
|
auto_editor/subcommands/palet.py,sha256=tbQoRWoT4jR3yu0etGApfprM-oQgXIjC-rIY-QG3nM0,655
|
40
40
|
auto_editor/subcommands/repl.py,sha256=xoNq88PtbvX3r1-FLStOb5jNoJ_rFzrl7R3Tk8a7zyI,3717
|
@@ -47,11 +47,12 @@ auto_editor/utils/cmdkw.py,sha256=XApxw7FZBOEJV9N4LHhdw1GVfHbFfCjr-zCZ1gJsSvY,60
|
|
47
47
|
auto_editor/utils/container.py,sha256=cl8wN5w-PjShPabnppil56r2dykQCfWdsR45jBbCkuo,7976
|
48
48
|
auto_editor/utils/encoder.py,sha256=auNYo7HXbcU4iTUCc0LE5lpwFmSvdWvBm6-5KIaRK8w,2983
|
49
49
|
auto_editor/utils/func.py,sha256=H38xO6Wxg1TZILVrx-nCowCzj_mqBUtJuOFp4DV3Hsc,4843
|
50
|
-
auto_editor/utils/log.py,sha256=
|
50
|
+
auto_editor/utils/log.py,sha256=ry-C92PRkJ-c8PQYIs1imk1qigDYfsCoLBYK6CQSP7I,2844
|
51
|
+
auto_editor/utils/subtitle_tools.py,sha256=TjjVPiT8bWzZJcrZjF7ddpgfIsVkLE4LyxXzBswHAGU,693
|
51
52
|
auto_editor/utils/types.py,sha256=zWbU_VkcdP4yHHzKyaSiXu560n5U53i0x5SPkUDsCZU,11570
|
52
|
-
auto_editor-24.
|
53
|
-
auto_editor-24.
|
54
|
-
auto_editor-24.
|
55
|
-
auto_editor-24.
|
56
|
-
auto_editor-24.
|
57
|
-
auto_editor-24.
|
53
|
+
auto_editor-24.25.1.dist-info/LICENSE,sha256=yiq99pWITHfqS0pbZMp7cy2dnbreTuvBwudsU-njvIM,1210
|
54
|
+
auto_editor-24.25.1.dist-info/METADATA,sha256=gVnK8FEce556cmoFlkOha5mBcYEXnDxpNFQMrJz7RwI,6322
|
55
|
+
auto_editor-24.25.1.dist-info/WHEEL,sha256=cpQTJ5IWu9CdaPViMhC9YzF8gZuS5-vlfoFihTBC86A,91
|
56
|
+
auto_editor-24.25.1.dist-info/entry_points.txt,sha256=-H7zdTw4MqnAcwrN5xTNkGIhzZtJMxS9r6lTMeR9-aA,240
|
57
|
+
auto_editor-24.25.1.dist-info/top_level.txt,sha256=xwV1JV1ZeRmlH9VeBRZXgXtWHpWSD4w1mY5II56D3ns,22
|
58
|
+
auto_editor-24.25.1.dist-info/RECORD,,
|
File without changes
|
File without changes
|
File without changes
|