auto-editor 26.3.1__py3-none-any.whl → 26.3.3__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- auto_editor/__init__.py +1 -1
- auto_editor/__main__.py +181 -55
- auto_editor/analyze.py +47 -45
- auto_editor/cmds/levels.py +1 -1
- auto_editor/cmds/repl.py +2 -3
- auto_editor/cmds/test.py +334 -386
- auto_editor/edit.py +28 -9
- auto_editor/lang/palet.py +23 -27
- auto_editor/make_layers.py +30 -18
- auto_editor/output.py +2 -2
- auto_editor/preview.py +3 -2
- auto_editor/render/audio.py +4 -1
- auto_editor/render/video.py +1 -1
- auto_editor/timeline.py +8 -1
- auto_editor/utils/types.py +7 -116
- {auto_editor-26.3.1.dist-info → auto_editor-26.3.3.dist-info}/METADATA +2 -2
- {auto_editor-26.3.1.dist-info → auto_editor-26.3.3.dist-info}/RECORD +21 -21
- {auto_editor-26.3.1.dist-info → auto_editor-26.3.3.dist-info}/WHEEL +1 -1
- {auto_editor-26.3.1.dist-info → auto_editor-26.3.3.dist-info}/LICENSE +0 -0
- {auto_editor-26.3.1.dist-info → auto_editor-26.3.3.dist-info}/entry_points.txt +0 -0
- {auto_editor-26.3.1.dist-info → auto_editor-26.3.3.dist-info}/top_level.txt +0 -0
auto_editor/edit.py
CHANGED
@@ -6,7 +6,7 @@ from fractions import Fraction
|
|
6
6
|
from heapq import heappop, heappush
|
7
7
|
from os.path import splitext
|
8
8
|
from subprocess import run
|
9
|
-
from typing import Any
|
9
|
+
from typing import TYPE_CHECKING, Any
|
10
10
|
|
11
11
|
import av
|
12
12
|
from av import AudioResampler, Codec
|
@@ -24,7 +24,9 @@ from auto_editor.utils.chunks import Chunk, Chunks
|
|
24
24
|
from auto_editor.utils.cmdkw import ParserError, parse_with_palet, pAttr, pAttrs
|
25
25
|
from auto_editor.utils.container import Container, container_constructor
|
26
26
|
from auto_editor.utils.log import Log
|
27
|
-
|
27
|
+
|
28
|
+
if TYPE_CHECKING:
|
29
|
+
from auto_editor.__main__ import Args
|
28
30
|
|
29
31
|
|
30
32
|
def set_output(
|
@@ -206,7 +208,7 @@ def edit_media(paths: list[str], args: Args, log: Log) -> None:
|
|
206
208
|
|
207
209
|
del paths
|
208
210
|
|
209
|
-
output, export_ops = set_output(args.
|
211
|
+
output, export_ops = set_output(args.output, args.export, src, log)
|
210
212
|
assert "export" in export_ops
|
211
213
|
export = export_ops["export"]
|
212
214
|
|
@@ -219,10 +221,6 @@ def edit_media(paths: list[str], args: Args, log: Log) -> None:
|
|
219
221
|
if os.path.isdir(output):
|
220
222
|
log.error("Output path already has an existing directory!")
|
221
223
|
|
222
|
-
if os.path.isfile(output) and src is not None and src.path != output: # type: ignore
|
223
|
-
log.debug(f"Removing already existing file: {output}")
|
224
|
-
os.remove(output)
|
225
|
-
|
226
224
|
if args.sample_rate is None:
|
227
225
|
if tl is None:
|
228
226
|
samplerate = 48000 if src is None else src.get_sr()
|
@@ -297,7 +295,19 @@ def edit_media(paths: list[str], args: Args, log: Log) -> None:
|
|
297
295
|
def make_media(tl: v3, output_path: str) -> None:
|
298
296
|
assert src is not None
|
299
297
|
|
300
|
-
|
298
|
+
options = {}
|
299
|
+
mov_flags = []
|
300
|
+
if args.fragmented and not args.no_fragmented:
|
301
|
+
mov_flags.extend(["default_base_moof", "frag_keyframe", "separate_moof"])
|
302
|
+
options["frag_duration"] = "0.2"
|
303
|
+
if args.faststart:
|
304
|
+
log.warning("Fragmented is enabled, will not apply faststart.")
|
305
|
+
elif not args.no_faststart:
|
306
|
+
mov_flags.append("faststart")
|
307
|
+
if mov_flags:
|
308
|
+
options["movflags"] = "+".join(mov_flags)
|
309
|
+
|
310
|
+
output = av.open(output_path, "w", container_options=options)
|
301
311
|
|
302
312
|
if ctr.default_sub != "none" and not args.sn:
|
303
313
|
sub_paths = make_new_subtitles(tl, log)
|
@@ -444,6 +454,9 @@ def edit_media(paths: list[str], args: Args, log: Log) -> None:
|
|
444
454
|
|
445
455
|
if should_get_audio:
|
446
456
|
audio_frames = [next(frames, None) for frames in audio_gen_frames]
|
457
|
+
if output_stream is None and audio_frames and audio_frames[-1]:
|
458
|
+
assert audio_frames[-1].time is not None
|
459
|
+
index = round(audio_frames[-1].time * tl.tb)
|
447
460
|
else:
|
448
461
|
audio_frames = [None]
|
449
462
|
if should_get_sub:
|
@@ -478,8 +491,11 @@ def edit_media(paths: list[str], args: Args, log: Log) -> None:
|
|
478
491
|
while frame_queue and frame_queue[0].index <= index:
|
479
492
|
item = heappop(frame_queue)
|
480
493
|
frame_type = item.frame_type
|
494
|
+
bar_index = None
|
481
495
|
try:
|
482
496
|
if frame_type in {"video", "audio"}:
|
497
|
+
if item.frame.time is not None:
|
498
|
+
bar_index = round(item.frame.time * tl.tb)
|
483
499
|
output.mux(item.stream.encode(item.frame))
|
484
500
|
elif frame_type == "subtitle":
|
485
501
|
output.mux(item.frame)
|
@@ -488,10 +504,13 @@ def edit_media(paths: list[str], args: Args, log: Log) -> None:
|
|
488
504
|
f"Generic error for encoder: {item.stream.name}\n"
|
489
505
|
f"at {item.index} time_base\nPerhaps video quality settings are too low?"
|
490
506
|
)
|
507
|
+
except av.FileNotFoundError:
|
508
|
+
log.error(f"File not found: {output_path}")
|
491
509
|
except av.FFmpegError as e:
|
492
510
|
log.error(e)
|
493
511
|
|
494
|
-
|
512
|
+
if bar_index:
|
513
|
+
bar.tick(bar_index)
|
495
514
|
|
496
515
|
# Flush streams
|
497
516
|
if output_stream is not None:
|
auto_editor/lang/palet.py
CHANGED
@@ -9,11 +9,11 @@ from dataclasses import dataclass
|
|
9
9
|
from difflib import get_close_matches
|
10
10
|
from fractions import Fraction
|
11
11
|
from io import StringIO
|
12
|
-
from typing import TYPE_CHECKING
|
12
|
+
from typing import TYPE_CHECKING, cast
|
13
13
|
|
14
14
|
import numpy as np
|
15
15
|
|
16
|
-
from auto_editor.analyze import LevelError, mut_remove_small
|
16
|
+
from auto_editor.analyze import LevelError, Levels, mut_remove_small
|
17
17
|
from auto_editor.lib.contracts import *
|
18
18
|
from auto_editor.lib.data_structs import *
|
19
19
|
from auto_editor.lib.err import MyError
|
@@ -21,7 +21,7 @@ from auto_editor.utils.func import boolop
|
|
21
21
|
|
22
22
|
if TYPE_CHECKING:
|
23
23
|
from collections.abc import Callable
|
24
|
-
from typing import Any, NoReturn
|
24
|
+
from typing import Any, NoReturn, TypeGuard
|
25
25
|
|
26
26
|
from numpy.typing import NDArray
|
27
27
|
|
@@ -510,15 +510,16 @@ def p_slice(
|
|
510
510
|
return seq[start:end:step]
|
511
511
|
|
512
512
|
|
513
|
+
def is_boolean_array(v: object) -> TypeGuard[np.ndarray]:
|
514
|
+
return isinstance(v, np.ndarray) and v.dtype.kind == "b"
|
515
|
+
|
516
|
+
|
513
517
|
is_iterable = Contract(
|
514
518
|
"iterable?",
|
515
519
|
lambda v: type(v) in {str, range, list, tuple, dict, Quoted}
|
516
520
|
or isinstance(v, np.ndarray),
|
517
521
|
)
|
518
|
-
is_boolarr = Contract(
|
519
|
-
"bool-array?",
|
520
|
-
lambda v: isinstance(v, np.ndarray) and v.dtype.kind == "b",
|
521
|
-
)
|
522
|
+
is_boolarr = Contract("bool-array?", is_boolean_array)
|
522
523
|
|
523
524
|
|
524
525
|
def raise_(msg: str | Exception) -> NoReturn:
|
@@ -568,13 +569,10 @@ def edit_audio(
|
|
568
569
|
if "@levels" not in env:
|
569
570
|
raise MyError("Can't use `audio` if there's no input media")
|
570
571
|
|
571
|
-
levels = env["@levels"]
|
572
|
-
src = levels.src
|
573
|
-
strict = levels.strict
|
574
|
-
|
572
|
+
levels = cast(Levels, env["@levels"])
|
575
573
|
stream_data: NDArray[np.bool_] | None = None
|
576
574
|
if stream == Sym("all"):
|
577
|
-
stream_range = range(0, len(
|
575
|
+
stream_range = range(0, len(levels.container.streams.audio))
|
578
576
|
else:
|
579
577
|
assert isinstance(stream, int)
|
580
578
|
stream_range = range(stream, stream + 1)
|
@@ -586,17 +584,15 @@ def edit_audio(
|
|
586
584
|
stream_data = audio_list
|
587
585
|
else:
|
588
586
|
stream_data = boolop(stream_data, audio_list, np.logical_or)
|
589
|
-
except LevelError
|
590
|
-
|
591
|
-
|
592
|
-
if stream_data is not None:
|
593
|
-
mut_remove_small(stream_data, minclip, replace=1, with_=0)
|
594
|
-
mut_remove_small(stream_data, mincut, replace=0, with_=1)
|
587
|
+
except LevelError:
|
588
|
+
return np.array([], dtype=np.bool_)
|
595
589
|
|
596
|
-
|
590
|
+
if stream_data is None:
|
591
|
+
return np.array([], dtype=np.bool_)
|
597
592
|
|
598
|
-
|
599
|
-
|
593
|
+
mut_remove_small(stream_data, minclip, replace=1, with_=0)
|
594
|
+
mut_remove_small(stream_data, mincut, replace=0, with_=1)
|
595
|
+
return stream_data
|
600
596
|
|
601
597
|
|
602
598
|
def edit_motion(
|
@@ -608,18 +604,18 @@ def edit_motion(
|
|
608
604
|
if "@levels" not in env:
|
609
605
|
raise MyError("Can't use `motion` if there's no input media")
|
610
606
|
|
611
|
-
levels = env["@levels"]
|
607
|
+
levels = cast(Levels, env["@levels"])
|
612
608
|
try:
|
613
609
|
return levels.motion(stream, blur, width) >= threshold
|
614
|
-
except LevelError
|
615
|
-
return
|
610
|
+
except LevelError:
|
611
|
+
return np.array([], dtype=np.bool_)
|
616
612
|
|
617
613
|
|
618
614
|
def edit_subtitle(pattern, stream=0, **kwargs):
|
619
615
|
if "@levels" not in env:
|
620
616
|
raise MyError("Can't use `subtitle` if there's no input media")
|
621
617
|
|
622
|
-
levels = env["@levels"]
|
618
|
+
levels = cast(Levels, env["@levels"])
|
623
619
|
if "ignore-case" not in kwargs:
|
624
620
|
kwargs["ignore-case"] = False
|
625
621
|
if "max-count" not in kwargs:
|
@@ -628,8 +624,8 @@ def edit_subtitle(pattern, stream=0, **kwargs):
|
|
628
624
|
max_count = kwargs["max-count"]
|
629
625
|
try:
|
630
626
|
return levels.subtitle(pattern, stream, ignore_case, max_count)
|
631
|
-
except LevelError
|
632
|
-
return
|
627
|
+
except LevelError:
|
628
|
+
return np.array([], dtype=np.bool_)
|
633
629
|
|
634
630
|
|
635
631
|
class StackTraceManager:
|
auto_editor/make_layers.py
CHANGED
@@ -6,18 +6,19 @@ from typing import TYPE_CHECKING, NamedTuple
|
|
6
6
|
|
7
7
|
import numpy as np
|
8
8
|
|
9
|
-
from auto_editor.analyze import
|
9
|
+
from auto_editor.analyze import initLevels
|
10
10
|
from auto_editor.ffwrapper import FileInfo
|
11
|
-
from auto_editor.lang.palet import Lexer, Parser, env, interpret,
|
11
|
+
from auto_editor.lang.palet import Lexer, Parser, env, interpret, is_boolean_array
|
12
12
|
from auto_editor.lib.data_structs import print_str
|
13
13
|
from auto_editor.lib.err import MyError
|
14
14
|
from auto_editor.timeline import ASpace, TlAudio, TlVideo, VSpace, v1, v3
|
15
15
|
from auto_editor.utils.func import mut_margin
|
16
|
-
from auto_editor.utils.types import
|
16
|
+
from auto_editor.utils.types import CoerceError, time
|
17
17
|
|
18
18
|
if TYPE_CHECKING:
|
19
19
|
from numpy.typing import NDArray
|
20
20
|
|
21
|
+
from auto_editor.__main__ import Args
|
21
22
|
from auto_editor.utils.bar import Bar
|
22
23
|
from auto_editor.utils.chunks import Chunks
|
23
24
|
from auto_editor.utils.log import Log
|
@@ -122,7 +123,6 @@ def make_timeline(
|
|
122
123
|
|
123
124
|
has_loud = np.array([], dtype=np.bool_)
|
124
125
|
src_index = np.array([], dtype=np.int32)
|
125
|
-
concat = np.concatenate
|
126
126
|
|
127
127
|
try:
|
128
128
|
stdenv = __import__("auto_editor.lang.stdenv", fromlist=["lang"])
|
@@ -137,6 +137,7 @@ def make_timeline(
|
|
137
137
|
parser = Parser(Lexer("config.pal", file.read()))
|
138
138
|
interpret(env, parser)
|
139
139
|
|
140
|
+
results = []
|
140
141
|
for i, src in enumerate(sources):
|
141
142
|
try:
|
142
143
|
parser = Parser(Lexer("`--edit`", args.edit))
|
@@ -144,32 +145,43 @@ def make_timeline(
|
|
144
145
|
log.debug(f"edit: {parser}")
|
145
146
|
|
146
147
|
env["timebase"] = tb
|
147
|
-
env["
|
148
|
-
env["@levels"] = Levels(src, tb, bar, args.no_cache, log, len(sources) < 2)
|
148
|
+
env["@levels"] = initLevels(src, tb, bar, args.no_cache, log)
|
149
149
|
|
150
|
-
|
151
|
-
|
152
|
-
if len(results) == 0:
|
150
|
+
inter_result = interpret(env, parser)
|
151
|
+
if len(inter_result) == 0:
|
153
152
|
log.error("Expression in --edit must return a bool-array, got nothing")
|
154
153
|
|
155
|
-
result =
|
154
|
+
result = inter_result[-1]
|
156
155
|
if callable(result):
|
157
156
|
result = result()
|
158
157
|
except MyError as e:
|
159
158
|
log.error(e)
|
160
159
|
|
161
|
-
if not
|
160
|
+
if not is_boolean_array(result):
|
162
161
|
log.error(
|
163
162
|
f"Expression in --edit must return a bool-array, got {print_str(result)}"
|
164
163
|
)
|
165
|
-
assert isinstance(result, np.ndarray)
|
166
|
-
|
167
164
|
mut_margin(result, start_margin, end_margin)
|
168
|
-
|
169
|
-
|
170
|
-
|
171
|
-
|
172
|
-
|
165
|
+
results.append(result)
|
166
|
+
|
167
|
+
if all(len(result) == 0 for result in results):
|
168
|
+
if "subtitle" in args.edit:
|
169
|
+
log.error("No file(s) have the selected subtitle stream.")
|
170
|
+
if "motion" in args.edit:
|
171
|
+
log.error("No file(s) have the selected video stream.")
|
172
|
+
if "audio" in args.edit:
|
173
|
+
log.error("No file(s) have the selected audio stream.")
|
174
|
+
|
175
|
+
src_indexes = []
|
176
|
+
for i in range(0, len(results)):
|
177
|
+
if len(results[i]) == 0:
|
178
|
+
results[i] = initLevels(sources[i], tb, bar, args.no_cache, log).all()
|
179
|
+
src_indexes.append(np.full(len(results[i]), i, dtype=np.int32))
|
180
|
+
|
181
|
+
has_loud = np.concatenate(results)
|
182
|
+
src_index = np.concatenate(src_indexes)
|
183
|
+
if len(has_loud) == 0:
|
184
|
+
log.error("Empty timeline. Nothing to do.")
|
173
185
|
|
174
186
|
# Setup for handling custom speeds
|
175
187
|
speed_index = has_loud.astype(np.uint)
|
auto_editor/output.py
CHANGED
@@ -9,12 +9,12 @@ from av.audio.resampler import AudioResampler
|
|
9
9
|
from auto_editor.ffwrapper import FileInfo
|
10
10
|
from auto_editor.utils.bar import Bar
|
11
11
|
from auto_editor.utils.log import Log
|
12
|
-
from auto_editor.utils.types import
|
12
|
+
from auto_editor.utils.types import split_num_str
|
13
13
|
|
14
14
|
|
15
15
|
def parse_bitrate(input_: str, log: Log) -> int:
|
16
16
|
try:
|
17
|
-
val, unit =
|
17
|
+
val, unit = split_num_str(input_)
|
18
18
|
except Exception as e:
|
19
19
|
log.error(e)
|
20
20
|
|
auto_editor/preview.py
CHANGED
@@ -5,7 +5,7 @@ from fractions import Fraction
|
|
5
5
|
from statistics import fmean, median
|
6
6
|
from typing import TextIO
|
7
7
|
|
8
|
-
from auto_editor.analyze import
|
8
|
+
from auto_editor.analyze import initLevels
|
9
9
|
from auto_editor.timeline import v3
|
10
10
|
from auto_editor.utils.bar import initBar
|
11
11
|
from auto_editor.utils.func import to_timecode
|
@@ -64,8 +64,9 @@ def preview(tl: v3, log: Log) -> None:
|
|
64
64
|
all_sources.add(aclip.src)
|
65
65
|
|
66
66
|
in_len = 0
|
67
|
+
bar = initBar("none")
|
67
68
|
for src in all_sources:
|
68
|
-
in_len +=
|
69
|
+
in_len += initLevels(src, tb, bar, False, log).media_length
|
69
70
|
|
70
71
|
out_len = tl.out_len()
|
71
72
|
|
auto_editor/render/audio.py
CHANGED
@@ -2,6 +2,7 @@ from __future__ import annotations
|
|
2
2
|
|
3
3
|
import io
|
4
4
|
from pathlib import Path
|
5
|
+
from typing import TYPE_CHECKING
|
5
6
|
|
6
7
|
import av
|
7
8
|
import numpy as np
|
@@ -18,9 +19,11 @@ from auto_editor.utils.bar import Bar
|
|
18
19
|
from auto_editor.utils.cmdkw import ParserError, parse_with_palet, pAttr, pAttrs
|
19
20
|
from auto_editor.utils.container import Container
|
20
21
|
from auto_editor.utils.log import Log
|
21
|
-
from auto_editor.utils.types import Args
|
22
22
|
from auto_editor.wavfile import AudioData, read, write
|
23
23
|
|
24
|
+
if TYPE_CHECKING:
|
25
|
+
from auto_editor.__main__ import Args
|
26
|
+
|
24
27
|
norm_types = {
|
25
28
|
"ebu": pAttrs(
|
26
29
|
"ebu",
|
auto_editor/render/video.py
CHANGED
@@ -13,10 +13,10 @@ if TYPE_CHECKING:
|
|
13
13
|
from collections.abc import Iterator
|
14
14
|
from typing import Any
|
15
15
|
|
16
|
+
from auto_editor.__main__ import Args
|
16
17
|
from auto_editor.ffwrapper import FileInfo
|
17
18
|
from auto_editor.timeline import v3
|
18
19
|
from auto_editor.utils.log import Log
|
19
|
-
from auto_editor.utils.types import Args
|
20
20
|
|
21
21
|
|
22
22
|
@dataclass(slots=True)
|
auto_editor/timeline.py
CHANGED
@@ -6,7 +6,7 @@ from typing import TYPE_CHECKING
|
|
6
6
|
from auto_editor.ffwrapper import initFileInfo, mux
|
7
7
|
from auto_editor.lib.contracts import *
|
8
8
|
from auto_editor.utils.cmdkw import Required, pAttr, pAttrs
|
9
|
-
from auto_editor.utils.types import natural, number, parse_color
|
9
|
+
from auto_editor.utils.types import CoerceError, natural, number, parse_color
|
10
10
|
|
11
11
|
if TYPE_CHECKING:
|
12
12
|
from collections.abc import Iterator
|
@@ -128,6 +128,13 @@ class TlRect:
|
|
128
128
|
}
|
129
129
|
|
130
130
|
|
131
|
+
def threshold(val: str | float) -> float:
|
132
|
+
num = number(val)
|
133
|
+
if num > 1 or num < 0:
|
134
|
+
raise CoerceError(f"'{val}': Threshold must be between 0 and 1 (0%-100%)")
|
135
|
+
return num
|
136
|
+
|
137
|
+
|
131
138
|
video_builder = pAttrs(
|
132
139
|
"video",
|
133
140
|
pAttr("start", Required, is_nat, natural),
|
auto_editor/utils/types.py
CHANGED
@@ -1,7 +1,6 @@
|
|
1
1
|
from __future__ import annotations
|
2
2
|
|
3
3
|
import re
|
4
|
-
from dataclasses import dataclass, field
|
5
4
|
from fractions import Fraction
|
6
5
|
|
7
6
|
|
@@ -9,16 +8,7 @@ class CoerceError(Exception):
|
|
9
8
|
pass
|
10
9
|
|
11
10
|
|
12
|
-
def
|
13
|
-
vals = val.strip().split(",")
|
14
|
-
if num_args > len(vals):
|
15
|
-
raise CoerceError(f"Too few arguments for {name}.")
|
16
|
-
if len(vals) > num_args:
|
17
|
-
raise CoerceError(f"Too many arguments for {name}.")
|
18
|
-
return vals
|
19
|
-
|
20
|
-
|
21
|
-
def _split_num_str(val: str | float) -> tuple[float, str]:
|
11
|
+
def split_num_str(val: str | float) -> tuple[float, str]:
|
22
12
|
if isinstance(val, float | int):
|
23
13
|
return val, ""
|
24
14
|
|
@@ -35,14 +25,9 @@ def _split_num_str(val: str | float) -> tuple[float, str]:
|
|
35
25
|
return float(num), unit
|
36
26
|
|
37
27
|
|
38
|
-
def _unit_check(unit: str, allowed_units: tuple[str, ...]) -> None:
|
39
|
-
if unit not in allowed_units:
|
40
|
-
raise CoerceError(f"Unknown unit: '{unit}'")
|
41
|
-
|
42
|
-
|
43
28
|
# Numbers: 0, 1, 2, 3, ...
|
44
29
|
def natural(val: str | float) -> int:
|
45
|
-
num, unit =
|
30
|
+
num, unit = split_num_str(val)
|
46
31
|
if unit != "":
|
47
32
|
raise CoerceError(f"'{val}': Natural does not allow units.")
|
48
33
|
if not isinstance(num, int) and not num.is_integer():
|
@@ -69,25 +54,12 @@ def number(val: str | float) -> float:
|
|
69
54
|
raise CoerceError(f"'{val}': Denominator must not be zero.")
|
70
55
|
return vs[0] / vs[1]
|
71
56
|
|
72
|
-
num, unit =
|
57
|
+
num, unit = split_num_str(val)
|
73
58
|
if unit == "%":
|
74
59
|
return num / 100
|
75
|
-
|
76
|
-
|
77
|
-
|
78
|
-
|
79
|
-
def speed(val: str) -> float:
|
80
|
-
_s = number(val)
|
81
|
-
if _s <= 0 or _s > 99999:
|
82
|
-
return 99999.0
|
83
|
-
return _s
|
84
|
-
|
85
|
-
|
86
|
-
def threshold(val: str | float) -> float:
|
87
|
-
num = number(val)
|
88
|
-
if num > 1 or num < 0:
|
89
|
-
raise CoerceError(f"'{val}': Threshold must be between 0 and 1 (0%-100%)")
|
90
|
-
return num
|
60
|
+
if unit == "":
|
61
|
+
return num
|
62
|
+
raise CoerceError(f"Unknown unit: '{unit}'")
|
91
63
|
|
92
64
|
|
93
65
|
def frame_rate(val: str) -> Fraction:
|
@@ -102,14 +74,6 @@ def frame_rate(val: str) -> Fraction:
|
|
102
74
|
return Fraction(val)
|
103
75
|
|
104
76
|
|
105
|
-
def sample_rate(val: str) -> int:
|
106
|
-
num, unit = _split_num_str(val)
|
107
|
-
if unit in {"kHz", "KHz"}:
|
108
|
-
return natural(num * 1000)
|
109
|
-
_unit_check(unit, ("", "Hz"))
|
110
|
-
return natural(num)
|
111
|
-
|
112
|
-
|
113
77
|
def time(val: str, tb: Fraction) -> int:
|
114
78
|
if ":" in val:
|
115
79
|
boxes = val.split(":")
|
@@ -121,7 +85,7 @@ def time(val: str, tb: Fraction) -> int:
|
|
121
85
|
)
|
122
86
|
raise CoerceError(f"'{val}': Invalid time format")
|
123
87
|
|
124
|
-
num, unit =
|
88
|
+
num, unit = split_num_str(val)
|
125
89
|
if unit in {"s", "sec", "secs", "second", "seconds"}:
|
126
90
|
return round(num * tb)
|
127
91
|
if unit in {"min", "mins", "minute", "minutes"}:
|
@@ -136,25 +100,6 @@ def time(val: str, tb: Fraction) -> int:
|
|
136
100
|
return int(num)
|
137
101
|
|
138
102
|
|
139
|
-
def margin(val: str) -> tuple[str, str]:
|
140
|
-
vals = val.strip().split(",")
|
141
|
-
if len(vals) == 1:
|
142
|
-
vals.append(vals[0])
|
143
|
-
if len(vals) != 2:
|
144
|
-
raise CoerceError("--margin has too many arguments.")
|
145
|
-
return vals[0], vals[1]
|
146
|
-
|
147
|
-
|
148
|
-
def time_range(val: str) -> tuple[str, str]:
|
149
|
-
a = _comma_coerce("time_range", val, 2)
|
150
|
-
return a[0], a[1]
|
151
|
-
|
152
|
-
|
153
|
-
def speed_range(val: str) -> tuple[float, str, str]:
|
154
|
-
a = _comma_coerce("speed_range", val, 3)
|
155
|
-
return number(a[0]), a[1], a[2]
|
156
|
-
|
157
|
-
|
158
103
|
def parse_color(val: str) -> str:
|
159
104
|
"""
|
160
105
|
Convert a color str into an RGB tuple
|
@@ -179,60 +124,6 @@ def parse_color(val: str) -> str:
|
|
179
124
|
raise ValueError(f"Invalid Color: '{color}'")
|
180
125
|
|
181
126
|
|
182
|
-
def resolution(val: str | None) -> tuple[int, int] | None:
|
183
|
-
if val is None:
|
184
|
-
return None
|
185
|
-
vals = val.strip().split(",")
|
186
|
-
if len(vals) != 2:
|
187
|
-
raise CoerceError(f"'{val}': Resolution takes two numbers")
|
188
|
-
|
189
|
-
return natural(vals[0]), natural(vals[1])
|
190
|
-
|
191
|
-
|
192
|
-
@dataclass(slots=True)
|
193
|
-
class Args:
|
194
|
-
yt_dlp_location: str = "yt-dlp"
|
195
|
-
download_format: str | None = None
|
196
|
-
output_format: str | None = None
|
197
|
-
yt_dlp_extras: str | None = None
|
198
|
-
video_codec: str = "auto"
|
199
|
-
audio_codec: str = "auto"
|
200
|
-
video_bitrate: str = "auto"
|
201
|
-
vprofile: str | None = None
|
202
|
-
audio_bitrate: str = "auto"
|
203
|
-
scale: float = 1.0
|
204
|
-
sn: bool = False
|
205
|
-
dn: bool = False
|
206
|
-
no_seek: bool = False
|
207
|
-
cut_out: list[tuple[str, str]] = field(default_factory=list)
|
208
|
-
add_in: list[tuple[str, str]] = field(default_factory=list)
|
209
|
-
set_speed_for_range: list[tuple[float, str, str]] = field(default_factory=list)
|
210
|
-
frame_rate: Fraction | None = None
|
211
|
-
sample_rate: int | None = None
|
212
|
-
resolution: tuple[int, int] | None = None
|
213
|
-
background: str = "#000000"
|
214
|
-
edit: str = "audio"
|
215
|
-
keep_tracks_separate: bool = False
|
216
|
-
audio_normalize: str = "#f"
|
217
|
-
export: str | None = None
|
218
|
-
player: str | None = None
|
219
|
-
no_open: bool = False
|
220
|
-
temp_dir: str | None = None
|
221
|
-
progress: str = "modern"
|
222
|
-
version: bool = False
|
223
|
-
debug: bool = False
|
224
|
-
config: bool = False
|
225
|
-
quiet: bool = False
|
226
|
-
preview: bool = False
|
227
|
-
no_cache: bool = False
|
228
|
-
margin: tuple[str, str] = ("0.2s", "0.2s")
|
229
|
-
silent_speed: float = 99999.0
|
230
|
-
video_speed: float = 1.0
|
231
|
-
output_file: str | None = None
|
232
|
-
help: bool = False
|
233
|
-
input: list[str] = field(default_factory=list)
|
234
|
-
|
235
|
-
|
236
127
|
colormap = {
|
237
128
|
# Taken from https://www.w3.org/TR/css-color-4/#named-color
|
238
129
|
"aliceblue": "#f0f8ff",
|
@@ -1,6 +1,6 @@
|
|
1
1
|
Metadata-Version: 2.2
|
2
2
|
Name: auto-editor
|
3
|
-
Version: 26.3.
|
3
|
+
Version: 26.3.3
|
4
4
|
Summary: Auto-Editor: Effort free video editing!
|
5
5
|
Author-email: WyattBlue <wyattblue@auto-editor.com>
|
6
6
|
License: Unlicense
|
@@ -21,7 +21,7 @@ Requires-Dist: pyav==14.2.*
|
|
21
21
|
---
|
22
22
|
|
23
23
|
[](https://github.com/wyattblue/auto-editor/actions)
|
24
|
-
|
24
|
+
[](https://github.com/astral-sh/ruff)
|
25
25
|
|
26
26
|
Before doing the real editing, you first cut out the "dead space" which is typically silence. This is known as a "first pass". Cutting these is a boring task, especially if the video is very long.
|
27
27
|
|