auto-editor 24.19.1__tar.gz → 24.25.1__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {auto_editor-24.19.1/auto_editor.egg-info → auto_editor-24.25.1}/PKG-INFO +6 -8
- {auto_editor-24.19.1 → auto_editor-24.25.1}/README.md +4 -6
- auto_editor-24.25.1/auto_editor/__init__.py +2 -0
- {auto_editor-24.19.1 → auto_editor-24.25.1}/auto_editor/__main__.py +25 -21
- {auto_editor-24.19.1 → auto_editor-24.25.1}/auto_editor/analyze.py +55 -29
- {auto_editor-24.19.1 → auto_editor-24.25.1}/auto_editor/edit.py +4 -15
- {auto_editor-24.19.1 → auto_editor-24.25.1}/auto_editor/ffwrapper.py +0 -2
- {auto_editor-24.19.1 → auto_editor-24.25.1}/auto_editor/formats/fcp7.py +1 -1
- {auto_editor-24.19.1 → auto_editor-24.25.1}/auto_editor/formats/json.py +6 -3
- {auto_editor-24.19.1 → auto_editor-24.25.1}/auto_editor/make_layers.py +79 -41
- {auto_editor-24.19.1 → auto_editor-24.25.1}/auto_editor/output.py +11 -10
- {auto_editor-24.19.1 → auto_editor-24.25.1}/auto_editor/render/subtitle.py +7 -13
- {auto_editor-24.19.1 → auto_editor-24.25.1}/auto_editor/render/video.py +0 -3
- {auto_editor-24.19.1 → auto_editor-24.25.1}/auto_editor/subcommands/info.py +10 -1
- {auto_editor-24.19.1 → auto_editor-24.25.1}/auto_editor/subcommands/subdump.py +7 -7
- {auto_editor-24.19.1 → auto_editor-24.25.1}/auto_editor/utils/bar.py +2 -2
- {auto_editor-24.19.1 → auto_editor-24.25.1}/auto_editor/utils/log.py +36 -34
- auto_editor-24.25.1/auto_editor/utils/subtitle_tools.py +29 -0
- {auto_editor-24.19.1 → auto_editor-24.25.1}/auto_editor/utils/types.py +5 -4
- {auto_editor-24.19.1 → auto_editor-24.25.1}/auto_editor/validate_input.py +1 -1
- {auto_editor-24.19.1 → auto_editor-24.25.1}/auto_editor/vanparse.py +23 -24
- {auto_editor-24.19.1 → auto_editor-24.25.1/auto_editor.egg-info}/PKG-INFO +6 -8
- {auto_editor-24.19.1 → auto_editor-24.25.1}/auto_editor.egg-info/SOURCES.txt +1 -0
- {auto_editor-24.19.1 → auto_editor-24.25.1}/auto_editor.egg-info/requires.txt +1 -1
- {auto_editor-24.19.1 → auto_editor-24.25.1}/pyproject.toml +1 -1
- auto_editor-24.19.1/auto_editor/__init__.py +0 -2
- {auto_editor-24.19.1 → auto_editor-24.25.1}/LICENSE +0 -0
- {auto_editor-24.19.1 → auto_editor-24.25.1}/ae-ffmpeg/ae_ffmpeg/__init__.py +0 -0
- {auto_editor-24.19.1 → auto_editor-24.25.1}/ae-ffmpeg/ae_ffmpeg/py.typed +0 -0
- {auto_editor-24.19.1 → auto_editor-24.25.1}/ae-ffmpeg/setup.py +0 -0
- {auto_editor-24.19.1 → auto_editor-24.25.1}/auto_editor/formats/__init__.py +0 -0
- {auto_editor-24.19.1 → auto_editor-24.25.1}/auto_editor/formats/fcp11.py +0 -0
- {auto_editor-24.19.1 → auto_editor-24.25.1}/auto_editor/formats/shotcut.py +0 -0
- {auto_editor-24.19.1 → auto_editor-24.25.1}/auto_editor/formats/utils.py +0 -0
- {auto_editor-24.19.1 → auto_editor-24.25.1}/auto_editor/help.py +0 -0
- {auto_editor-24.19.1 → auto_editor-24.25.1}/auto_editor/lang/__init__.py +0 -0
- {auto_editor-24.19.1 → auto_editor-24.25.1}/auto_editor/lang/json.py +0 -0
- {auto_editor-24.19.1 → auto_editor-24.25.1}/auto_editor/lang/libmath.py +0 -0
- {auto_editor-24.19.1 → auto_editor-24.25.1}/auto_editor/lang/palet.py +0 -0
- {auto_editor-24.19.1 → auto_editor-24.25.1}/auto_editor/lib/__init__.py +0 -0
- {auto_editor-24.19.1 → auto_editor-24.25.1}/auto_editor/lib/contracts.py +0 -0
- {auto_editor-24.19.1 → auto_editor-24.25.1}/auto_editor/lib/data_structs.py +0 -0
- {auto_editor-24.19.1 → auto_editor-24.25.1}/auto_editor/lib/err.py +0 -0
- {auto_editor-24.19.1 → auto_editor-24.25.1}/auto_editor/preview.py +0 -0
- {auto_editor-24.19.1 → auto_editor-24.25.1}/auto_editor/render/__init__.py +0 -0
- {auto_editor-24.19.1 → auto_editor-24.25.1}/auto_editor/render/audio.py +0 -0
- {auto_editor-24.19.1 → auto_editor-24.25.1}/auto_editor/subcommands/__init__.py +0 -0
- {auto_editor-24.19.1 → auto_editor-24.25.1}/auto_editor/subcommands/desc.py +0 -0
- {auto_editor-24.19.1 → auto_editor-24.25.1}/auto_editor/subcommands/levels.py +0 -0
- {auto_editor-24.19.1 → auto_editor-24.25.1}/auto_editor/subcommands/palet.py +0 -0
- {auto_editor-24.19.1 → auto_editor-24.25.1}/auto_editor/subcommands/repl.py +0 -0
- {auto_editor-24.19.1 → auto_editor-24.25.1}/auto_editor/subcommands/test.py +0 -0
- {auto_editor-24.19.1 → auto_editor-24.25.1}/auto_editor/timeline.py +0 -0
- {auto_editor-24.19.1 → auto_editor-24.25.1}/auto_editor/utils/__init__.py +0 -0
- {auto_editor-24.19.1 → auto_editor-24.25.1}/auto_editor/utils/chunks.py +0 -0
- {auto_editor-24.19.1 → auto_editor-24.25.1}/auto_editor/utils/cmdkw.py +0 -0
- {auto_editor-24.19.1 → auto_editor-24.25.1}/auto_editor/utils/container.py +0 -0
- {auto_editor-24.19.1 → auto_editor-24.25.1}/auto_editor/utils/encoder.py +0 -0
- {auto_editor-24.19.1 → auto_editor-24.25.1}/auto_editor/utils/func.py +0 -0
- {auto_editor-24.19.1 → auto_editor-24.25.1}/auto_editor/wavfile.py +0 -0
- {auto_editor-24.19.1 → auto_editor-24.25.1}/auto_editor.egg-info/dependency_links.txt +0 -0
- {auto_editor-24.19.1 → auto_editor-24.25.1}/auto_editor.egg-info/entry_points.txt +0 -0
- {auto_editor-24.19.1 → auto_editor-24.25.1}/auto_editor.egg-info/top_level.txt +0 -0
- {auto_editor-24.19.1 → auto_editor-24.25.1}/setup.cfg +0 -0
@@ -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
|
@@ -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.0
|
15
|
+
Requires-Dist: pyav==12.1.0
|
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
|
@@ -88,16 +88,14 @@ Auto-Editor can also export to:
|
|
88
88
|
- Individual media clips with `--export clip-sequence`
|
89
89
|
|
90
90
|
### Naming Timelines
|
91
|
-
By default, auto-editor will
|
91
|
+
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.
|
92
92
|
|
93
93
|
```
|
94
|
+
# for POSIX shells
|
94
95
|
auto-editor example.mp4 --export 'premiere:name="Your name here"'
|
95
96
|
|
96
|
-
|
97
|
-
|
98
|
-
auto-editor example.mp4 --export 'final-cut-pro:name="Your name here"'
|
99
|
-
|
100
|
-
# No other export options support naming
|
97
|
+
# for Powershell
|
98
|
+
auto-editor example.mp4 --export 'premiere:name=""Your name here""'
|
101
99
|
```
|
102
100
|
|
103
101
|
### Split by Clip
|
@@ -1,8 +1,11 @@
|
|
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
10
|
from auto_editor.utils.log import Log
|
8
11
|
from auto_editor.utils.types import (
|
@@ -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,36 +304,32 @@ 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
|
-
|
312
|
+
import av
|
313
|
+
|
314
|
+
print(f"Python Version: {plat.python_version()}")
|
317
315
|
print(f"Platform: {plat.system()} {plat.release()} {plat.machine().lower()}")
|
318
|
-
print(f"
|
316
|
+
print(f"PyAV Version: {av.__version__}")
|
319
317
|
print(f"Auto-Editor Version: {auto_editor.version}")
|
320
|
-
|
318
|
+
return
|
321
319
|
|
322
|
-
if args.input
|
320
|
+
if not args.input:
|
323
321
|
log.error("You need to give auto-editor an input file.")
|
324
322
|
|
325
|
-
temp = setup_tempdir(args.temp_dir,
|
326
|
-
log = Log(args.debug, args.quiet, temp
|
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
334
|
|
331
335
|
try:
|
@@ -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:
|
@@ -170,8 +171,6 @@ class Levels:
|
|
170
171
|
# If there's no audio, get length in video metadata.
|
171
172
|
import av
|
172
173
|
|
173
|
-
av.logging.set_level(av.logging.PANIC)
|
174
|
-
|
175
174
|
with av.open(f"{self.src.path}") as cn:
|
176
175
|
if len(cn.streams.video) < 1:
|
177
176
|
self.log.error("Could not get media duration")
|
@@ -227,11 +226,7 @@ class Levels:
|
|
227
226
|
except Exception:
|
228
227
|
json_object = {}
|
229
228
|
|
230
|
-
entry = {
|
231
|
-
"type": str(arr.dtype),
|
232
|
-
"arr": arr.tolist(),
|
233
|
-
}
|
234
|
-
|
229
|
+
entry = {"type": str(arr.dtype), "arr": arr.tolist()}
|
235
230
|
src_key = f"{self.src.path}"
|
236
231
|
|
237
232
|
if src_key in json_object:
|
@@ -313,44 +308,75 @@ class Levels:
|
|
313
308
|
except re.error as e:
|
314
309
|
self.log.error(e)
|
315
310
|
|
316
|
-
|
317
|
-
|
311
|
+
import av
|
312
|
+
from av.subtitles.subtitle import AssSubtitle, TextSubtitle
|
318
313
|
|
319
|
-
|
320
|
-
|
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)
|
321
320
|
|
322
|
-
#
|
323
|
-
|
324
|
-
|
325
|
-
|
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)
|
326
331
|
|
327
|
-
|
328
|
-
|
332
|
+
end = round((start + dur) * self.tb)
|
333
|
+
sub_length = max(sub_length, end)
|
329
334
|
|
330
|
-
result = np.zeros((
|
335
|
+
result = np.zeros((sub_length), dtype=np.bool_)
|
336
|
+
del sub_length
|
331
337
|
|
332
338
|
count = 0
|
333
|
-
|
334
|
-
|
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:
|
335
345
|
break
|
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)
|
336
356
|
|
337
|
-
|
338
|
-
|
339
|
-
|
340
|
-
|
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()
|
341
370
|
|
342
371
|
return result
|
343
372
|
|
344
373
|
def motion(self, s: int, blur: int, width: int) -> NDArray[np.float64]:
|
345
374
|
import av
|
346
375
|
|
347
|
-
av.logging.set_level(av.logging.PANIC)
|
348
|
-
|
349
|
-
mobj = {"stream": s, "width": width, "blur": blur}
|
350
|
-
|
351
376
|
if s >= len(self.src.videos):
|
352
377
|
raise LevelError(f"motion: video stream '{s}' does not exist.")
|
353
378
|
|
379
|
+
mobj = {"stream": s, "width": width, "blur": blur}
|
354
380
|
if (arr := self.read_cache("motion", mobj)) is not None:
|
355
381
|
return arr
|
356
382
|
|
@@ -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
|
|
@@ -152,7 +152,6 @@ def parse_export(export: str, log: Log) -> dict[str, Any]:
|
|
152
152
|
def edit_media(
|
153
153
|
paths: list[str], ffmpeg: FFmpeg, args: Args, temp: str, log: Log
|
154
154
|
) -> None:
|
155
|
-
timer = Timer(args.quiet)
|
156
155
|
bar = Bar(args.progress)
|
157
156
|
tl = None
|
158
157
|
|
@@ -191,7 +190,6 @@ def edit_media(
|
|
191
190
|
|
192
191
|
if export["export"] == "timeline":
|
193
192
|
log.quiet = True
|
194
|
-
timer.quiet = True
|
195
193
|
|
196
194
|
if not args.preview:
|
197
195
|
log.conwrite("Starting")
|
@@ -214,16 +212,7 @@ def edit_media(
|
|
214
212
|
ensure = Ensure(ffmpeg, samplerate, temp, log)
|
215
213
|
|
216
214
|
if tl is None:
|
217
|
-
|
218
|
-
if src is not None and len(src.subtitles) > 0 and not args.sn:
|
219
|
-
cmd = ["-i", f"{src.path}", "-hide_banner"]
|
220
|
-
for s, sub in enumerate(src.subtitles):
|
221
|
-
cmd.extend(["-map", f"0:s:{s}"])
|
222
|
-
for s, sub in enumerate(src.subtitles):
|
223
|
-
cmd.extend([os.path.join(temp, f"{s}s.{sub.ext}")])
|
224
|
-
ffmpeg.run(cmd)
|
225
|
-
|
226
|
-
tl = make_timeline(sources, ffmpeg, ensure, args, samplerate, bar, temp, log)
|
215
|
+
tl = make_timeline(sources, ensure, args, samplerate, bar, temp, log)
|
227
216
|
|
228
217
|
if export["export"] == "timeline":
|
229
218
|
from auto_editor.formats.json import make_json_timeline
|
@@ -284,7 +273,7 @@ def edit_media(
|
|
284
273
|
apply_later = False
|
285
274
|
|
286
275
|
if ctr.allow_subtitle and not args.sn:
|
287
|
-
sub_output = make_new_subtitles(tl,
|
276
|
+
sub_output = make_new_subtitles(tl, ensure, temp)
|
288
277
|
|
289
278
|
if ctr.allow_audio:
|
290
279
|
audio_output = make_new_audio(tl, ensure, args, ffmpeg, bar, temp, log)
|
@@ -358,7 +347,7 @@ def edit_media(
|
|
358
347
|
else:
|
359
348
|
make_media(tl, output)
|
360
349
|
|
361
|
-
|
350
|
+
log.stop_timer()
|
362
351
|
|
363
352
|
if not args.no_open and export["export"] in ("default", "audio", "clip-sequence"):
|
364
353
|
if args.player is None:
|
@@ -181,11 +181,11 @@ def read_v1(tl: Any, log: Log) -> v3:
|
|
181
181
|
for i, chunk in enumerate(chunks):
|
182
182
|
if type(chunk) is not list or len(chunk) != 3:
|
183
183
|
log.error(f"Invalid chunk at chunk {i}")
|
184
|
-
if type(chunk[0])
|
184
|
+
if type(chunk[0]) not in (int, float) or chunk[0] < 0:
|
185
185
|
log.error(f"Invalid start at chunk {i}")
|
186
|
-
if type(chunk[1])
|
186
|
+
if type(chunk[1]) not in (int, float) or chunk[1] <= chunk[0]:
|
187
187
|
log.error(f"Invalid end at chunk {i}")
|
188
|
-
if type(chunk[2])
|
188
|
+
if type(chunk[2]) not in (int, float) or chunk[2] < 0.0 or chunk[2] > 99999.0:
|
189
189
|
log.error(f"Invalid speed at chunk {i}")
|
190
190
|
|
191
191
|
if i == 0 and chunk[0] != 0:
|
@@ -194,6 +194,9 @@ def read_v1(tl: Any, log: Log) -> v3:
|
|
194
194
|
log.error(f"Invalid start at chunk {i}")
|
195
195
|
last_end = chunk[1]
|
196
196
|
|
197
|
+
if type(chunk[0]) is float or type(chunk[1]) is float or type(chunk[2]) is int:
|
198
|
+
chunks[i] = (int(chunk[0]), int(chunk[1]), float(chunk[2]))
|
199
|
+
|
197
200
|
for c in clipify(chunks, src):
|
198
201
|
if src.videos:
|
199
202
|
if len(vtl) == 0:
|
@@ -1,12 +1,13 @@
|
|
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
|
7
8
|
|
8
9
|
from auto_editor.analyze import FileSetup, Levels
|
9
|
-
from auto_editor.ffwrapper import
|
10
|
+
from auto_editor.ffwrapper import FileInfo
|
10
11
|
from auto_editor.lang.palet import Lexer, Parser, env, interpret, is_boolarr
|
11
12
|
from auto_editor.lib.data_structs import print_str
|
12
13
|
from auto_editor.lib.err import MyError
|
@@ -110,9 +111,34 @@ def run_interpreter_for_edit_option(
|
|
110
111
|
return result
|
111
112
|
|
112
113
|
|
114
|
+
def make_sane_timebase(fps: Fraction) -> Fraction:
|
115
|
+
tb = round(fps, 2)
|
116
|
+
|
117
|
+
ntsc_60 = Fraction(60_000, 1001)
|
118
|
+
ntsc = Fraction(30_000, 1001)
|
119
|
+
film_ntsc = Fraction(24_000, 1001)
|
120
|
+
|
121
|
+
if tb == round(ntsc_60, 2):
|
122
|
+
return ntsc_60
|
123
|
+
if tb == round(ntsc, 2):
|
124
|
+
return ntsc
|
125
|
+
if tb == round(film_ntsc, 2):
|
126
|
+
return film_ntsc
|
127
|
+
return tb
|
128
|
+
|
129
|
+
|
130
|
+
def parse_time(val: str, arr: NDArray, tb: Fraction) -> int: # raises: `CoerceError`
|
131
|
+
if val == "start":
|
132
|
+
return 0
|
133
|
+
if val == "end":
|
134
|
+
return len(arr)
|
135
|
+
|
136
|
+
num = time(val, tb)
|
137
|
+
return num if num >= 0 else num + len(arr)
|
138
|
+
|
139
|
+
|
113
140
|
def make_timeline(
|
114
141
|
sources: list[FileInfo],
|
115
|
-
ffmpeg: FFmpeg,
|
116
142
|
ensure: Ensure,
|
117
143
|
args: Args,
|
118
144
|
sr: int,
|
@@ -125,14 +151,9 @@ def make_timeline(
|
|
125
151
|
if inp is None:
|
126
152
|
tb, res = Fraction(30), (1920, 1080)
|
127
153
|
else:
|
128
|
-
tb =
|
129
|
-
|
130
|
-
|
131
|
-
if tb == round(ntsc, 2):
|
132
|
-
tb = ntsc
|
133
|
-
elif tb == round(film_ntsc, 2):
|
134
|
-
tb = film_ntsc
|
135
|
-
|
154
|
+
tb = make_sane_timebase(
|
155
|
+
inp.get_fps() if args.frame_rate is None else args.frame_rate
|
156
|
+
)
|
136
157
|
res = inp.get_res() if args.resolution is None else args.resolution
|
137
158
|
|
138
159
|
try:
|
@@ -171,36 +192,21 @@ def make_timeline(
|
|
171
192
|
speed_hash[len(speed_map) - 1] = speed
|
172
193
|
return len(speed_map) - 1
|
173
194
|
|
174
|
-
def parse_time(val: str, arr: NDArray) -> int:
|
175
|
-
if val == "start":
|
176
|
-
return 0
|
177
|
-
if val == "end":
|
178
|
-
return len(arr)
|
179
|
-
try:
|
180
|
-
num = time(val, tb)
|
181
|
-
return num if num >= 0 else num + len(arr)
|
182
|
-
except CoerceError as e:
|
183
|
-
log.error(e)
|
184
|
-
|
185
|
-
def mut_set_range(arr: NDArray, _ranges: list[list[str]], index: float) -> None:
|
186
|
-
for _range in _ranges:
|
187
|
-
assert len(_range) == 2
|
188
|
-
pair = [parse_time(val, arr) for val in _range]
|
189
|
-
arr[pair[0] : pair[1]] = index
|
190
|
-
|
191
195
|
try:
|
192
|
-
|
196
|
+
for _range in args.cut_out:
|
193
197
|
# always cut out even if 'silent_speed' is not 99,999
|
194
|
-
|
198
|
+
pair = [parse_time(val, speed_index, tb) for val in _range]
|
199
|
+
speed_index[pair[0] : pair[1]] = get_speed_index(99_999)
|
195
200
|
|
196
|
-
|
201
|
+
for _range in args.add_in:
|
197
202
|
# set to 'video_speed' index
|
198
|
-
|
203
|
+
pair = [parse_time(val, speed_index, tb) for val in _range]
|
204
|
+
speed_index[pair[0] : pair[1]] = 1
|
199
205
|
|
200
206
|
for speed_range in args.set_speed_for_range:
|
201
|
-
|
202
|
-
|
203
|
-
|
207
|
+
start_in = parse_time(speed_range[1], speed_index, tb)
|
208
|
+
end_in = parse_time(speed_range[2], speed_index, tb)
|
209
|
+
speed_index[start_in:end_in] = get_speed_index(speed_range[0])
|
204
210
|
except CoerceError as e:
|
205
211
|
log.error(e)
|
206
212
|
|
@@ -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
|
@@ -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
|
|
@@ -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)
|
@@ -7,6 +7,7 @@ from typing import Any, Literal, TypedDict
|
|
7
7
|
|
8
8
|
from auto_editor.ffwrapper import initFileInfo
|
9
9
|
from auto_editor.lang.json import dump
|
10
|
+
from auto_editor.make_layers import make_sane_timebase
|
10
11
|
from auto_editor.timeline import v3
|
11
12
|
from auto_editor.utils.func import aspect_ratio
|
12
13
|
from auto_editor.utils.log import Log
|
@@ -68,6 +69,7 @@ class MediaJson(TypedDict, total=False):
|
|
68
69
|
subtitle: list[SubtitleJson]
|
69
70
|
container: ContainerJson
|
70
71
|
type: Literal["media", "timeline", "unknown"]
|
72
|
+
recommendedTimebase: str
|
71
73
|
version: Literal["v1", "v3"]
|
72
74
|
clips: int
|
73
75
|
|
@@ -81,7 +83,7 @@ def main(sys_args: list[str] = sys.argv[1:]) -> None:
|
|
81
83
|
|
82
84
|
for file in args.input:
|
83
85
|
if not os.path.isfile(file):
|
84
|
-
log.
|
86
|
+
log.error(f"Could not find '{file}'")
|
85
87
|
|
86
88
|
ext = os.path.splitext(file)[1]
|
87
89
|
if ext == ".json":
|
@@ -108,6 +110,7 @@ def main(sys_args: list[str] = sys.argv[1:]) -> None:
|
|
108
110
|
|
109
111
|
file_info[file] = {
|
110
112
|
"type": "media",
|
113
|
+
"recommendedTimebase": "30/1",
|
111
114
|
"video": [],
|
112
115
|
"audio": [],
|
113
116
|
"subtitle": [],
|
@@ -117,6 +120,12 @@ def main(sys_args: list[str] = sys.argv[1:]) -> None:
|
|
117
120
|
},
|
118
121
|
}
|
119
122
|
|
123
|
+
if src.videos:
|
124
|
+
recTb = make_sane_timebase(src.videos[0].fps)
|
125
|
+
file_info[file]["recommendedTimebase"] = (
|
126
|
+
f"{recTb.numerator}/{recTb.denominator}"
|
127
|
+
)
|
128
|
+
|
120
129
|
for track, v in enumerate(src.videos):
|
121
130
|
w, h = v.width, v.height
|
122
131
|
|
@@ -1,7 +1,7 @@
|
|
1
1
|
import sys
|
2
2
|
|
3
3
|
import av
|
4
|
-
from av.subtitles.subtitle import
|
4
|
+
from av.subtitles.subtitle import AssSubtitle, TextSubtitle
|
5
5
|
|
6
6
|
|
7
7
|
def main(sys_args: list[str] = sys.argv[1:]) -> None:
|
@@ -10,12 +10,12 @@ def main(sys_args: list[str] = sys.argv[1:]) -> None:
|
|
10
10
|
for s in range(len(container.streams.subtitles)):
|
11
11
|
print(f"file: {input_file} ({s}:{container.streams.subtitles[s].name})")
|
12
12
|
for packet in container.demux(subtitles=s):
|
13
|
-
for
|
14
|
-
|
15
|
-
if
|
16
|
-
print(
|
17
|
-
elif
|
18
|
-
print(
|
13
|
+
for subset in packet.decode():
|
14
|
+
for sub in subset.rects:
|
15
|
+
if isinstance(sub, AssSubtitle):
|
16
|
+
print(sub.ass.decode("utf-8", errors="ignore"))
|
17
|
+
elif isinstance(sub, TextSubtitle):
|
18
|
+
print(sub.text.decode("utf-8", errors="ignore"))
|
19
19
|
print("------")
|
20
20
|
|
21
21
|
|
@@ -67,9 +67,9 @@ class Bar:
|
|
67
67
|
|
68
68
|
if self.machine:
|
69
69
|
index = min(index, self.total)
|
70
|
-
|
70
|
+
secs_til_eta = round(self.begin_time + rate - time(), 2)
|
71
71
|
print(
|
72
|
-
f"{self.title}~{index}~{self.total}~{
|
72
|
+
f"{self.title}~{index}~{self.total}~{secs_til_eta}",
|
73
73
|
end="\r",
|
74
74
|
flush=True,
|
75
75
|
)
|
@@ -2,36 +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")
|
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
|
24
|
+
self.machine = machine
|
25
|
+
self.no_color = no_color
|
26
|
+
self.start_time = 0 if self.quiet or self.machine else perf_counter()
|
35
27
|
|
36
28
|
def debug(self, message: object) -> None:
|
37
29
|
if self.is_debug:
|
@@ -55,17 +47,40 @@ class Log:
|
|
55
47
|
self.debug(f"Failed to delete temp dir:\n{e}")
|
56
48
|
|
57
49
|
def conwrite(self, message: str) -> None:
|
58
|
-
if
|
50
|
+
if self.machine:
|
51
|
+
print(message, flush=True)
|
52
|
+
elif not self.quiet:
|
59
53
|
buffer = " " * (get_terminal_size().columns - len(message) - 3)
|
60
54
|
sys.stdout.write(f" {message}{buffer}\r")
|
61
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
|
+
|
62
73
|
def error(self, message: str | Exception) -> NoReturn:
|
63
74
|
if self.is_debug and isinstance(message, Exception):
|
64
75
|
self.cleanup()
|
65
76
|
raise message
|
66
77
|
|
67
78
|
self.conwrite("")
|
68
|
-
|
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
|
+
|
69
84
|
self.cleanup()
|
70
85
|
from platform import system
|
71
86
|
|
@@ -78,16 +93,3 @@ class Log:
|
|
78
93
|
import os
|
79
94
|
|
80
95
|
os._exit(1)
|
81
|
-
|
82
|
-
def nofile(self, path: str | Path) -> NoReturn:
|
83
|
-
self.error(f"Could not find '{path}'")
|
84
|
-
|
85
|
-
def warning(self, message: str) -> None:
|
86
|
-
if not self.quiet:
|
87
|
-
self.conwrite("")
|
88
|
-
sys.stderr.write(f"Warning! {message}\n")
|
89
|
-
|
90
|
-
def print(self, message: str) -> None:
|
91
|
-
if not self.quiet:
|
92
|
-
self.conwrite("")
|
93
|
-
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
|
@@ -164,8 +164,9 @@ def margin(val: str) -> tuple[str, str]:
|
|
164
164
|
return vals[0], vals[1]
|
165
165
|
|
166
166
|
|
167
|
-
def time_range(val: str) ->
|
168
|
-
|
167
|
+
def time_range(val: str) -> tuple[str, str]:
|
168
|
+
a = _comma_coerce("time_range", val, 2)
|
169
|
+
return a[0], a[1]
|
169
170
|
|
170
171
|
|
171
172
|
def speed_range(val: str) -> tuple[float, str, str]:
|
@@ -231,8 +232,8 @@ class Args:
|
|
231
232
|
extras: str | None = None
|
232
233
|
sn: bool = False
|
233
234
|
no_seek: bool = False
|
234
|
-
cut_out: list[
|
235
|
-
add_in: list[
|
235
|
+
cut_out: list[tuple[str, str]] = field(default_factory=list)
|
236
|
+
add_in: list[tuple[str, str]] = field(default_factory=list)
|
236
237
|
set_speed_for_range: list[tuple[float, str, str]] = field(default_factory=list)
|
237
238
|
frame_rate: Fraction | None = None
|
238
239
|
sample_rate: int | None = None
|
@@ -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
|
@@ -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
|
@@ -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.0
|
15
|
+
Requires-Dist: pyav==12.1.0
|
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
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|