auto-editor 24.24.1__py3-none-any.whl → 24.27.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 +29 -26
- auto_editor/analyze.py +54 -19
- auto_editor/edit.py +7 -17
- auto_editor/ffwrapper.py +6 -2
- auto_editor/formats/fcp7.py +3 -3
- auto_editor/formats/shotcut.py +1 -2
- auto_editor/make_layers.py +46 -8
- auto_editor/output.py +50 -14
- auto_editor/render/subtitle.py +7 -13
- auto_editor/render/video.py +9 -9
- auto_editor/subcommands/info.py +1 -1
- auto_editor/subcommands/levels.py +1 -1
- auto_editor/subcommands/repl.py +1 -1
- auto_editor/subcommands/test.py +12 -5
- auto_editor/utils/log.py +33 -34
- auto_editor/utils/subtitle_tools.py +29 -0
- auto_editor/validate_input.py +9 -12
- auto_editor/vanparse.py +26 -27
- {auto_editor-24.24.1.dist-info → auto_editor-24.27.1.dist-info}/METADATA +6 -8
- {auto_editor-24.24.1.dist-info → auto_editor-24.27.1.dist-info}/RECORD +25 -24
- {auto_editor-24.24.1.dist-info → auto_editor-24.27.1.dist-info}/WHEEL +1 -1
- {auto_editor-24.24.1.dist-info → auto_editor-24.27.1.dist-info}/LICENSE +0 -0
- {auto_editor-24.24.1.dist-info → auto_editor-24.27.1.dist-info}/entry_points.txt +0 -0
- {auto_editor-24.24.1.dist-info → auto_editor-24.27.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.27.1"
|
2
|
+
version = "24w27a"
|
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
|
|
315
|
-
|
316
|
-
|
317
|
-
print(f"
|
318
|
-
print(f"
|
319
|
-
|
312
|
+
import av
|
313
|
+
|
314
|
+
print(f"OS: {plat.system()} {plat.release()} {plat.machine().lower()}")
|
315
|
+
print(f"Python: {plat.python_version()}")
|
316
|
+
print(f"PyAV: {av.__version__}")
|
317
|
+
print(f"Auto-Editor: {auto_editor.version}")
|
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
|
@@ -160,7 +160,7 @@ def edit_media(
|
|
160
160
|
if path_ext == ".xml":
|
161
161
|
from auto_editor.formats.fcp7 import fcp7_read_xml
|
162
162
|
|
163
|
-
tl = fcp7_read_xml(paths[0],
|
163
|
+
tl = fcp7_read_xml(paths[0], log)
|
164
164
|
assert tl.src is not None
|
165
165
|
sources: list[FileInfo] = [tl.src]
|
166
166
|
src: FileInfo | None = tl.src
|
@@ -168,7 +168,7 @@ def edit_media(
|
|
168
168
|
elif path_ext == ".mlt":
|
169
169
|
from auto_editor.formats.shotcut import shotcut_read_mlt
|
170
170
|
|
171
|
-
tl = shotcut_read_mlt(paths[0],
|
171
|
+
tl = shotcut_read_mlt(paths[0], log)
|
172
172
|
assert tl.src is not None
|
173
173
|
sources = [tl.src]
|
174
174
|
src = tl.src
|
@@ -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")
|
@@ -210,18 +209,9 @@ def edit_media(
|
|
210
209
|
else:
|
211
210
|
samplerate = args.sample_rate
|
212
211
|
|
213
|
-
ensure = Ensure(ffmpeg, samplerate, temp, log)
|
212
|
+
ensure = Ensure(ffmpeg, bar, 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/ffwrapper.py
CHANGED
@@ -11,6 +11,8 @@ from shutil import which
|
|
11
11
|
from subprocess import PIPE, Popen
|
12
12
|
from typing import Any
|
13
13
|
|
14
|
+
import av
|
15
|
+
|
14
16
|
from auto_editor.utils.func import get_stdout
|
15
17
|
from auto_editor.utils.log import Log
|
16
18
|
|
@@ -190,10 +192,12 @@ class FileInfo:
|
|
190
192
|
|
191
193
|
|
192
194
|
def initFileInfo(path: str, log: Log) -> FileInfo:
|
193
|
-
import av
|
194
|
-
|
195
195
|
try:
|
196
196
|
cont = av.open(path, "r")
|
197
|
+
except av.error.FileNotFoundError:
|
198
|
+
log.error(f"Could not find '{path}'")
|
199
|
+
except av.error.IsADirectoryError:
|
200
|
+
log.error(f"Expected a media file, but got a directory: {path}")
|
197
201
|
except av.error.InvalidDataError:
|
198
202
|
log.error(f"Invalid data when processing: {path}")
|
199
203
|
|
auto_editor/formats/fcp7.py
CHANGED
@@ -7,7 +7,7 @@ from math import ceil
|
|
7
7
|
from typing import TYPE_CHECKING
|
8
8
|
from xml.etree.ElementTree import Element
|
9
9
|
|
10
|
-
from auto_editor.ffwrapper import
|
10
|
+
from auto_editor.ffwrapper import FileInfo, initFileInfo
|
11
11
|
from auto_editor.timeline import ASpace, TlAudio, TlVideo, VSpace, v3
|
12
12
|
|
13
13
|
from .utils import Validator, show
|
@@ -177,7 +177,7 @@ def read_filters(clipitem: Element, log: Log) -> float:
|
|
177
177
|
return 1.0
|
178
178
|
|
179
179
|
|
180
|
-
def fcp7_read_xml(path: str,
|
180
|
+
def fcp7_read_xml(path: str, log: Log) -> v3:
|
181
181
|
def xml_bool(val: str) -> bool:
|
182
182
|
if val == "TRUE":
|
183
183
|
return True
|
@@ -188,7 +188,7 @@ def fcp7_read_xml(path: str, ffmpeg: FFmpeg, log: Log) -> v3:
|
|
188
188
|
try:
|
189
189
|
tree = ET.parse(path)
|
190
190
|
except FileNotFoundError:
|
191
|
-
log.
|
191
|
+
log.error(f"Could not find '{path}'")
|
192
192
|
|
193
193
|
root = tree.getroot()
|
194
194
|
|
auto_editor/formats/shotcut.py
CHANGED
@@ -9,7 +9,6 @@ from auto_editor.utils.func import aspect_ratio, to_timecode
|
|
9
9
|
if TYPE_CHECKING:
|
10
10
|
from collections.abc import Sequence
|
11
11
|
|
12
|
-
from auto_editor.ffwrapper import FFmpeg
|
13
12
|
from auto_editor.timeline import TlAudio, TlVideo
|
14
13
|
from auto_editor.utils.log import Log
|
15
14
|
|
@@ -22,7 +21,7 @@ https://mltframework.org/docs/mltxml/
|
|
22
21
|
"""
|
23
22
|
|
24
23
|
|
25
|
-
def shotcut_read_mlt(path: str,
|
24
|
+
def shotcut_read_mlt(path: str, log: Log) -> v3:
|
26
25
|
raise NotImplementedError
|
27
26
|
|
28
27
|
|
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
|
@@ -37,7 +38,7 @@ def clipify(chunks: Chunks, src: FileInfo, start: int = 0) -> list[Clip]:
|
|
37
38
|
clips: list[Clip] = []
|
38
39
|
i = 0
|
39
40
|
for chunk in chunks:
|
40
|
-
if chunk[2]
|
41
|
+
if chunk[2] > 0 and chunk[2] < 99999.0:
|
41
42
|
dur = round((chunk[1] - chunk[0]) / chunk[2])
|
42
43
|
if dur == 0:
|
43
44
|
continue
|
@@ -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
@@ -4,7 +4,11 @@ import os.path
|
|
4
4
|
from dataclasses import dataclass, field
|
5
5
|
from fractions import Fraction
|
6
6
|
|
7
|
+
import av
|
8
|
+
from av.audio.resampler import AudioResampler
|
9
|
+
|
7
10
|
from auto_editor.ffwrapper import FFmpeg, FileInfo
|
11
|
+
from auto_editor.utils.bar import Bar
|
8
12
|
from auto_editor.utils.container import Container
|
9
13
|
from auto_editor.utils.log import Log
|
10
14
|
from auto_editor.utils.types import Args
|
@@ -13,44 +17,76 @@ from auto_editor.utils.types import Args
|
|
13
17
|
@dataclass(slots=True)
|
14
18
|
class Ensure:
|
15
19
|
_ffmpeg: FFmpeg
|
20
|
+
_bar: Bar
|
16
21
|
_sr: int
|
17
22
|
temp: str
|
18
23
|
log: Log
|
19
|
-
|
20
|
-
|
24
|
+
_audios: list[tuple[FileInfo, int]] = field(default_factory=list)
|
25
|
+
_subtitles: list[tuple[FileInfo, int, str]] = field(default_factory=list)
|
21
26
|
|
22
27
|
def audio(self, src: FileInfo, stream: int) -> str:
|
23
28
|
try:
|
24
|
-
label = self.
|
29
|
+
label = self._audios.index((src, stream))
|
25
30
|
first_time = False
|
26
31
|
except ValueError:
|
27
|
-
self.
|
28
|
-
label = len(self.
|
32
|
+
self._audios.append((src, stream))
|
33
|
+
label = len(self._audios) - 1
|
29
34
|
first_time = True
|
30
35
|
|
31
36
|
out_path = os.path.join(self.temp, f"{label:x}.wav")
|
32
37
|
|
33
38
|
if first_time:
|
34
|
-
self.
|
39
|
+
sample_rate = self._sr
|
40
|
+
bar = self._bar
|
41
|
+
self.log.debug(f"Making external audio: {out_path}")
|
42
|
+
|
43
|
+
in_container = av.open(src.path, "r")
|
44
|
+
out_container = av.open(
|
45
|
+
out_path, "w", format="wav", options={"rf64": "always"}
|
46
|
+
)
|
47
|
+
astream = in_container.streams.audio[stream]
|
48
|
+
|
49
|
+
if astream.duration is None or astream.time_base is None:
|
50
|
+
dur = 1
|
51
|
+
else:
|
52
|
+
dur = int(astream.duration * astream.time_base)
|
53
|
+
|
54
|
+
bar.start(dur, "Extracting audio")
|
55
|
+
|
56
|
+
# PyAV always uses "stereo" layout, which is what we want.
|
57
|
+
output_astream = out_container.add_stream("pcm_s16le", rate=sample_rate)
|
58
|
+
assert isinstance(output_astream, av.audio.stream.AudioStream)
|
59
|
+
|
60
|
+
resampler = AudioResampler(format="s16", layout="stereo", rate=sample_rate) # type: ignore
|
61
|
+
for i, frame in enumerate(in_container.decode(astream)):
|
62
|
+
if i % 1500 == 0:
|
63
|
+
bar.tick(0 if frame.time is None else frame.time)
|
64
|
+
|
65
|
+
for new_frame in resampler.resample(frame):
|
66
|
+
for packet in output_astream.encode(new_frame):
|
67
|
+
out_container.mux_one(packet)
|
68
|
+
|
69
|
+
for packet in output_astream.encode():
|
70
|
+
out_container.mux_one(packet)
|
35
71
|
|
36
|
-
|
37
|
-
|
38
|
-
|
72
|
+
out_container.close()
|
73
|
+
in_container.close()
|
74
|
+
bar.end()
|
39
75
|
|
40
76
|
return out_path
|
41
77
|
|
42
|
-
def subtitle(self, src: FileInfo, stream: int) -> str:
|
78
|
+
def subtitle(self, src: FileInfo, stream: int, ext: str) -> str:
|
43
79
|
try:
|
44
|
-
|
80
|
+
self._subtitles.index((src, stream, ext))
|
45
81
|
first_time = False
|
46
82
|
except ValueError:
|
47
|
-
self.
|
48
|
-
label = len(self.sub_labels) - 1
|
83
|
+
self._subtitles.append((src, stream, ext))
|
49
84
|
first_time = True
|
50
85
|
|
51
|
-
out_path = os.path.join(self.temp, f"{
|
86
|
+
out_path = os.path.join(self.temp, f"{stream}s.{ext}")
|
52
87
|
|
53
88
|
if first_time:
|
89
|
+
self.log.debug(f"Making external subtitle: {out_path}")
|
54
90
|
self.log.conwrite("Extracting subtitle")
|
55
91
|
self._ffmpeg.run(["-i", f"{src.path}", "-map", f"0:s:{stream}", out_path])
|
56
92
|
|
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/render/video.py
CHANGED
@@ -103,8 +103,8 @@ def make_image_cache(tl: v3) -> dict[tuple[FileInfo, int], np.ndarray]:
|
|
103
103
|
graph.add("scale", f"{obj.width}:-1"),
|
104
104
|
graph.add("buffersink"),
|
105
105
|
)
|
106
|
-
graph.
|
107
|
-
frame = graph.
|
106
|
+
graph.vpush(frame)
|
107
|
+
frame = graph.vpull()
|
108
108
|
img_cache[(obj.src, obj.width)] = frame.to_ndarray(
|
109
109
|
format="rgb24"
|
110
110
|
)
|
@@ -122,7 +122,7 @@ def render_av(
|
|
122
122
|
log: Log,
|
123
123
|
) -> tuple[str, bool]:
|
124
124
|
src = tl.src
|
125
|
-
cns: dict[FileInfo, av.InputContainer] = {}
|
125
|
+
cns: dict[FileInfo, av.container.InputContainer] = {}
|
126
126
|
decoders: dict[FileInfo, Iterator[av.VideoFrame]] = {}
|
127
127
|
seek_cost: dict[FileInfo, int] = {}
|
128
128
|
tous: dict[FileInfo, int] = {}
|
@@ -302,8 +302,8 @@ def render_av(
|
|
302
302
|
graph.add("pad", f"{width}:{height}:-1:-1:color={bg}"),
|
303
303
|
graph.add("buffersink"),
|
304
304
|
)
|
305
|
-
graph.
|
306
|
-
frame = graph.
|
305
|
+
graph.vpush(frame)
|
306
|
+
frame = graph.vpull()
|
307
307
|
elif isinstance(obj, TlRect):
|
308
308
|
graph = av.filter.Graph()
|
309
309
|
x, y = apply_anchor(obj.x, obj.y, obj.width, obj.height, obj.anchor)
|
@@ -315,8 +315,8 @@ def render_av(
|
|
315
315
|
),
|
316
316
|
graph.add("buffersink"),
|
317
317
|
)
|
318
|
-
graph.
|
319
|
-
frame = graph.
|
318
|
+
graph.vpush(frame)
|
319
|
+
frame = graph.vpull()
|
320
320
|
elif isinstance(obj, TlImage):
|
321
321
|
img = img_cache[(obj.src, obj.width)]
|
322
322
|
array = frame.to_ndarray(format="rgb24")
|
@@ -355,8 +355,8 @@ def render_av(
|
|
355
355
|
frame = av.VideoFrame.from_ndarray(array, format="rgb24")
|
356
356
|
|
357
357
|
if scale_graph is not None and frame.width != target_width:
|
358
|
-
scale_graph.
|
359
|
-
frame = scale_graph.
|
358
|
+
scale_graph.vpush(frame)
|
359
|
+
frame = scale_graph.vpull()
|
360
360
|
|
361
361
|
if frame.format.name != target_pix_fmt:
|
362
362
|
frame = frame.reformat(format=target_pix_fmt)
|
auto_editor/subcommands/info.py
CHANGED
@@ -85,7 +85,7 @@ def main(sys_args: list[str] = sys.argv[1:]) -> None:
|
|
85
85
|
src = sources[0]
|
86
86
|
|
87
87
|
tb = src.get_fps() if args.timebase is None else args.timebase
|
88
|
-
ensure = Ensure(ffmpeg, src.get_sr(), temp, log)
|
88
|
+
ensure = Ensure(ffmpeg, bar, src.get_sr(), temp, log)
|
89
89
|
|
90
90
|
if ":" in args.edit:
|
91
91
|
method, attrs = args.edit.split(":", 1)
|
auto_editor/subcommands/repl.py
CHANGED
@@ -73,8 +73,8 @@ def main(sys_args: list[str] = sys.argv[1:]) -> None:
|
|
73
73
|
sources = [initFileInfo(path, log) for path in args.input]
|
74
74
|
src = sources[0]
|
75
75
|
tb = src.get_fps() if args.timebase is None else args.timebase
|
76
|
-
ensure = Ensure(ffmpeg, src.get_sr(), temp, log)
|
77
76
|
bar = Bar("none")
|
77
|
+
ensure = Ensure(ffmpeg, bar, src.get_sr(), temp, log)
|
78
78
|
env["timebase"] = tb
|
79
79
|
env["@levels"] = Levels(ensure, src, tb, bar, temp, log)
|
80
80
|
env["@filesetup"] = FileSetup(src, ensure, strict, tb, bar, temp, log)
|
auto_editor/subcommands/test.py
CHANGED
@@ -340,11 +340,19 @@ def main(sys_args: list[str] | None = None):
|
|
340
340
|
def track_tests():
|
341
341
|
return run.main(["resources/multi-track.mov"], ["--keep_tracks_seperate"])
|
342
342
|
|
343
|
-
def
|
343
|
+
def export_json_tests():
|
344
344
|
out = run.main(["example.mp4"], ["--export_as_json"])
|
345
345
|
out2 = run.main([out], [])
|
346
346
|
return out, out2
|
347
347
|
|
348
|
+
def import_v1_tests():
|
349
|
+
with open("v1.json", "w") as file:
|
350
|
+
file.write(
|
351
|
+
"""{"version": "1", "source": "example.mp4", "chunks": [ [0, 26, 1.0], [26, 34, 0] ]}"""
|
352
|
+
)
|
353
|
+
|
354
|
+
return run.main(["v1.json"], [])
|
355
|
+
|
348
356
|
def premiere_named_export():
|
349
357
|
run.main(["example.mp4"], ["--export", 'premiere:name="Foo Bar"'])
|
350
358
|
|
@@ -524,9 +532,7 @@ def main(sys_args: list[str] | None = None):
|
|
524
532
|
# Issue 280
|
525
533
|
def SAR():
|
526
534
|
out = run.main(["resources/SAR-2by3.mp4"], [])
|
527
|
-
|
528
|
-
# It's working, PyAV just can't detect the changes.
|
529
|
-
# assert checker.check(out).videos[0].sar == Fraction(2, 3)
|
535
|
+
assert checker.check(out).videos[0].sar == Fraction(2, 3)
|
530
536
|
|
531
537
|
return out
|
532
538
|
|
@@ -711,7 +717,8 @@ def main(sys_args: list[str] | None = None):
|
|
711
717
|
edit_positive_tests,
|
712
718
|
audio_norm_f,
|
713
719
|
audio_norm_ebu,
|
714
|
-
|
720
|
+
export_json_tests,
|
721
|
+
import_v1_tests,
|
715
722
|
high_speed_test,
|
716
723
|
video_speed,
|
717
724
|
multi_track_edit,
|
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
@@ -67,20 +67,17 @@ def download_video(my_input: str, args: Args, ffmpeg: FFmpeg, log: Log) -> str:
|
|
67
67
|
|
68
68
|
|
69
69
|
def valid_input(inputs: list[str], ffmpeg: FFmpeg, args: Args, log: Log) -> list[str]:
|
70
|
-
|
70
|
+
result = []
|
71
71
|
|
72
72
|
for my_input in inputs:
|
73
|
-
if
|
73
|
+
if my_input.startswith("http://") or my_input.startswith("https://"):
|
74
|
+
result.append(download_video(my_input, args, ffmpeg, log))
|
75
|
+
else:
|
74
76
|
_, ext = os.path.splitext(my_input)
|
75
77
|
if ext == "":
|
76
|
-
|
77
|
-
|
78
|
-
|
79
|
-
|
80
|
-
new_inputs.append(download_video(my_input, args, ffmpeg, log))
|
81
|
-
else:
|
82
|
-
if os.path.isdir(my_input):
|
83
|
-
log.error("Input must be a file or a URL, not a directory.")
|
84
|
-
log.nofile(my_input)
|
78
|
+
if os.path.isdir(my_input):
|
79
|
+
log.error("Input must be a file or a URL, not a directory.")
|
80
|
+
log.error("Input file must have an extension.")
|
81
|
+
result.append(my_input)
|
85
82
|
|
86
|
-
return
|
83
|
+
return result
|
auto_editor/vanparse.py
CHANGED
@@ -119,7 +119,7 @@ def to_key(op: Options | Required) -> str:
|
|
119
119
|
return op.names[0][:2].replace("-", "") + op.names[0][2:].replace("-", "_")
|
120
120
|
|
121
121
|
|
122
|
-
def print_option_help(
|
122
|
+
def print_option_help(name: str | None, ns_obj: object, option: Options) -> None:
|
123
123
|
text = StringIO()
|
124
124
|
text.write(
|
125
125
|
f" {', '.join(option.names)} {'' if option.metavar is None else option.metavar}\n\n"
|
@@ -145,8 +145,8 @@ def print_option_help(program_name: str | None, ns_obj: T, option: Options) -> N
|
|
145
145
|
|
146
146
|
from auto_editor.help import data
|
147
147
|
|
148
|
-
if
|
149
|
-
text.write(indent(data[
|
148
|
+
if name is not None and option.names[0] in data[name]:
|
149
|
+
text.write(indent(data[name][option.names[0]], " ") + "\n")
|
150
150
|
else:
|
151
151
|
text.write(f" {option.help}\n\n")
|
152
152
|
|
@@ -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.27.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.10
|
|
12
12
|
Description-Content-Type: text/markdown
|
13
13
|
License-File: LICENSE
|
14
14
|
Requires-Dist: numpy >=1.22.0
|
15
|
-
Requires-Dist: pyav ==12.
|
15
|
+
Requires-Dist: pyav ==12.2.*
|
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>
|
@@ -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,24 +1,24 @@
|
|
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=
|
8
|
-
auto_editor/ffwrapper.py,sha256=
|
4
|
+
auto_editor/__init__.py,sha256=x3FkUVgQK07IAJYRgjIfWDxsjSINz0ig_JBv3pG5gas,43
|
5
|
+
auto_editor/__main__.py,sha256=nkb8N_bxF_qld53LWo4c6Y0n9NDRdsPfANpVF1RD1cQ,9863
|
6
|
+
auto_editor/analyze.py,sha256=NVDARF7ZxNLWMJ89HsuqCS1TMSeYhf2e5C66cQUTKrk,13200
|
7
|
+
auto_editor/edit.py,sha256=EEB9PJdGpXh5b1vDdSnsEtI8aJEcF64-SNZjFRg6Nrs,11484
|
8
|
+
auto_editor/ffwrapper.py,sha256=jga7-HbQnC4w21qZk4aY4kwLT7UPqkGn6NJPFM5Qssc,7811
|
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=P49tkQ2td0s6-IPWpnM9iFlyVV-KckLK4Y9xdxrRyhk,9613
|
11
|
+
auto_editor/output.py,sha256=5_4HtSkd6Lwv2ATEwLKJuXL1yBGScWAPLxvh_nHrBA4,8101
|
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=JQt7J5xOBJDp6lnd2sQptpYhf7Z_hyxNEzLsE9E7LKU,2596
|
15
|
+
auto_editor/vanparse.py,sha256=p0u1X2gKM_lWB9bg-Lotqq9-bjfLgsVW9-U4Lwbz0aU,10029
|
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=fH86sxhlUysWisjvlqzZJgDrRpj3dSzFG-Eho2sehdc,17610
|
20
20
|
auto_editor/formats/json.py,sha256=Br-xHVHj59C0OPP2FwfJeht_fImepRXsaw0iDFvK7-w,7693
|
21
|
-
auto_editor/formats/shotcut.py,sha256
|
21
|
+
auto_editor/formats/shotcut.py,sha256=-ES854LLFCMCBe100JRJedDmuk8zPev17aQMTrzPv-g,4923
|
22
22
|
auto_editor/formats/utils.py,sha256=GIZw28WHuCIaZ_zMI0v6Kxbq0QaIpbLsdSegdYwQxQ8,1990
|
23
23
|
auto_editor/lang/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
24
24
|
auto_editor/lang/json.py,sha256=OsNcYlfEj8ZLlzLK-gkLcrCCKI7mJz9rpe-6XLr4f9U,9231
|
@@ -30,16 +30,16 @@ 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=
|
34
|
-
auto_editor/render/video.py,sha256=
|
33
|
+
auto_editor/render/subtitle.py,sha256=RUG0hNh0Mbt3IRxef9hKUpUPXvyTa3HVoqoSHYivAT4,4355
|
34
|
+
auto_editor/render/video.py,sha256=E1snevFC4mMk_S_oDquJPKyElQfdDodO7EJEQ22JJF8,13202
|
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=
|
38
|
-
auto_editor/subcommands/levels.py,sha256=
|
37
|
+
auto_editor/subcommands/info.py,sha256=7Sgt9WR0rWxe7juCRKseMxW6gKv3z3voqFcL8-MOVVM,6927
|
38
|
+
auto_editor/subcommands/levels.py,sha256=qSEXSkYPOCmr4_VZ1xAwtCZzaBOe8wXY0T-orN3Qg_A,4006
|
39
39
|
auto_editor/subcommands/palet.py,sha256=tbQoRWoT4jR3yu0etGApfprM-oQgXIjC-rIY-QG3nM0,655
|
40
|
-
auto_editor/subcommands/repl.py,sha256=
|
40
|
+
auto_editor/subcommands/repl.py,sha256=nbCVIyFwG3HRtGr8q-yLtASVMbMKDzAbn5vqA6RC9bk,3722
|
41
41
|
auto_editor/subcommands/subdump.py,sha256=2rIaGVtWWMBbPJ0NouPD7fY5lhk0QD_XKE_4EnAeWPw,892
|
42
|
-
auto_editor/subcommands/test.py,sha256=
|
42
|
+
auto_editor/subcommands/test.py,sha256=UcpJp-PMcoUyz6LDD4y2V6EQ9w1ed66fPZr4GpzJReg,25050
|
43
43
|
auto_editor/utils/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
44
44
|
auto_editor/utils/bar.py,sha256=RJqkJ8gNr8op_Z-2hh48ExjSonmTPX-RshctK_itv14,3988
|
45
45
|
auto_editor/utils/chunks.py,sha256=J-eGKtEz68gFtRrj1kOSgH4Tj_Yz6prNQ7Xr-d9NQJw,52
|
@@ -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.27.1.dist-info/LICENSE,sha256=yiq99pWITHfqS0pbZMp7cy2dnbreTuvBwudsU-njvIM,1210
|
54
|
+
auto_editor-24.27.1.dist-info/METADATA,sha256=GBxzQhnfrlu3KJ89iT7RZ04nvEhUeJfAOzPl6OxtS6A,6322
|
55
|
+
auto_editor-24.27.1.dist-info/WHEEL,sha256=y4mX-SOX4fYIkonsAGA5N0Oy-8_gI4FXw5HNI1xqvWg,91
|
56
|
+
auto_editor-24.27.1.dist-info/entry_points.txt,sha256=-H7zdTw4MqnAcwrN5xTNkGIhzZtJMxS9r6lTMeR9-aA,240
|
57
|
+
auto_editor-24.27.1.dist-info/top_level.txt,sha256=xwV1JV1ZeRmlH9VeBRZXgXtWHpWSD4w1mY5II56D3ns,22
|
58
|
+
auto_editor-24.27.1.dist-info/RECORD,,
|
File without changes
|
File without changes
|
File without changes
|