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/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
- from auto_editor.utils.types import Args
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.output_file, args.export, src, log)
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
- output = av.open(output_path, "w")
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
- bar.tick(index)
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(src.audios))
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 as e:
590
- raise_(e) if strict else levels.all()
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
- return stream_data
590
+ if stream_data is None:
591
+ return np.array([], dtype=np.bool_)
597
592
 
598
- stream = 0 if stream == Sym("all") else stream
599
- return raise_(f"audio stream '{stream}' does not exist") if strict else levels.all()
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 as e:
615
- return raise_(e) if levels.strict else levels.all()
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 as e:
632
- return raise_(e) if levels.strict else levels.all()
627
+ except LevelError:
628
+ return np.array([], dtype=np.bool_)
633
629
 
634
630
 
635
631
  class StackTraceManager:
@@ -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 Levels
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, is_boolarr
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 Args, CoerceError, time
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["src"] = f"{src.path}"
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
- results = interpret(env, parser)
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 = results[-1]
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 is_boolarr(result):
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
- has_loud = concat((has_loud, result))
170
- src_index = concat((src_index, np.full(len(result), i, dtype=np.int32)))
171
-
172
- assert len(has_loud) > 0
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 _split_num_str
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 = _split_num_str(input_)
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 Levels
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 += Levels(src, tb, initBar("none"), False, log, False).media_length
69
+ in_len += initLevels(src, tb, bar, False, log).media_length
69
70
 
70
71
  out_len = tl.out_len()
71
72
 
@@ -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",
@@ -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, threshold
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),
@@ -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 _comma_coerce(name: str, val: str, num_args: int) -> list[str]:
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 = _split_num_str(val)
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 = _split_num_str(val)
57
+ num, unit = split_num_str(val)
73
58
  if unit == "%":
74
59
  return num / 100
75
- _unit_check(unit, ("",))
76
- return num
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 = _split_num_str(val)
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.1
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
  [![Actions Status](https://github.com/wyattblue/auto-editor/workflows/build/badge.svg)](https://github.com/wyattblue/auto-editor/actions)
24
- <a href="https://github.com/psf/black"><img alt="Code style: black" src="https://img.shields.io/badge/code%20style-black-000000.svg"></a>
24
+ [![Ruff](https://img.shields.io/endpoint?url=https://raw.githubusercontent.com/astral-sh/ruff/main/assets/badge/v2.json)](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