auto-editor 26.3.2__py3-none-any.whl → 27.0.0__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 CHANGED
@@ -1 +1 @@
1
- __version__ = "26.3.2"
1
+ __version__ = "27.0.0"
auto_editor/__main__.py CHANGED
@@ -3,6 +3,8 @@
3
3
  import platform as plat
4
4
  import re
5
5
  import sys
6
+ from dataclasses import dataclass, field
7
+ from fractions import Fraction
6
8
  from io import StringIO
7
9
  from os import environ
8
10
  from os.path import exists, isdir, isfile, lexists, splitext
@@ -12,21 +14,128 @@ import auto_editor
12
14
  from auto_editor.utils.func import get_stdout
13
15
  from auto_editor.utils.log import Log
14
16
  from auto_editor.utils.types import (
15
- Args,
17
+ CoerceError,
16
18
  frame_rate,
17
- margin,
19
+ natural,
18
20
  number,
19
21
  parse_color,
20
- resolution,
21
- sample_rate,
22
- speed,
23
- speed_range,
24
- time_range,
22
+ split_num_str,
25
23
  )
26
24
  from auto_editor.vanparse import ArgumentParser
27
25
 
28
26
 
27
+ @dataclass(slots=True)
28
+ class Args:
29
+ input: list[str] = field(default_factory=list)
30
+ help: bool = False
31
+
32
+ # Editing Options
33
+ margin: tuple[str, str] = ("0.2s", "0.2s")
34
+ edit: str = "audio"
35
+ export: str | None = None
36
+ output: str | None = None
37
+ silent_speed: float = 99999.0
38
+ video_speed: float = 1.0
39
+ cut_out: list[tuple[str, str]] = field(default_factory=list)
40
+ add_in: list[tuple[str, str]] = field(default_factory=list)
41
+ set_speed_for_range: list[tuple[float, str, str]] = field(default_factory=list)
42
+
43
+ # Timeline Options
44
+ frame_rate: Fraction | None = None
45
+ sample_rate: int | None = None
46
+ resolution: tuple[int, int] | None = None
47
+ background: str = "#000000"
48
+
49
+ # URL download Options
50
+ yt_dlp_location: str = "yt-dlp"
51
+ download_format: str | None = None
52
+ output_format: str | None = None
53
+ yt_dlp_extras: str | None = None
54
+
55
+ # Display Options
56
+ progress: str = "modern"
57
+ debug: bool = False
58
+ quiet: bool = False
59
+ preview: bool = False
60
+
61
+ # Container Settings
62
+ sn: bool = False
63
+ dn: bool = False
64
+ faststart: bool = False
65
+ no_faststart: bool = False
66
+ fragmented: bool = False
67
+ no_fragmented: bool = False
68
+
69
+ # Video Rendering
70
+ video_codec: str = "auto"
71
+ video_bitrate: str = "auto"
72
+ vprofile: str | None = None
73
+ scale: float = 1.0
74
+ no_seek: bool = False
75
+
76
+ # Audio Rendering
77
+ audio_codec: str = "auto"
78
+ audio_bitrate: str = "auto"
79
+ mix_audio_streams: bool = False
80
+ keep_tracks_separate: bool = False
81
+ audio_normalize: str = "#f"
82
+
83
+ # Misc.
84
+ config: bool = False
85
+ no_cache: bool = False
86
+ no_open: bool = False
87
+ temp_dir: str | None = None
88
+ player: str | None = None
89
+ version: bool = False
90
+
91
+
29
92
  def main_options(parser: ArgumentParser) -> ArgumentParser:
93
+ def margin(val: str) -> tuple[str, str]:
94
+ vals = val.strip().split(",")
95
+ if len(vals) == 1:
96
+ vals.append(vals[0])
97
+ if len(vals) != 2:
98
+ raise CoerceError("--margin has too many arguments.")
99
+ return vals[0], vals[1]
100
+
101
+ def speed(val: str) -> float:
102
+ _s = number(val)
103
+ if _s <= 0 or _s > 99999:
104
+ return 99999.0
105
+ return _s
106
+
107
+ def resolution(val: str | None) -> tuple[int, int] | None:
108
+ if val is None:
109
+ return None
110
+ vals = val.strip().split(",")
111
+ if len(vals) != 2:
112
+ raise CoerceError(f"'{val}': Resolution takes two numbers")
113
+ return natural(vals[0]), natural(vals[1])
114
+
115
+ def sample_rate(val: str) -> int:
116
+ num, unit = split_num_str(val)
117
+ if unit in {"kHz", "KHz"}:
118
+ return natural(num * 1000)
119
+ if unit not in {"", "Hz"}:
120
+ raise CoerceError(f"Unknown unit: '{unit}'")
121
+ return natural(num)
122
+
123
+ def _comma_coerce(name: str, val: str, num_args: int) -> list[str]:
124
+ vals = val.strip().split(",")
125
+ if num_args > len(vals):
126
+ raise CoerceError(f"Too few arguments for {name}.")
127
+ if len(vals) > num_args:
128
+ raise CoerceError(f"Too many arguments for {name}.")
129
+ return vals
130
+
131
+ def time_range(val: str) -> tuple[str, str]:
132
+ a = _comma_coerce("time_range", val, 2)
133
+ return a[0], a[1]
134
+
135
+ def speed_range(val: str) -> tuple[float, str, str]:
136
+ a = _comma_coerce("speed_range", val, 3)
137
+ return number(a[0]), a[1], a[2]
138
+
30
139
  parser.add_required("input", nargs="*", metavar="[file | url ...] [options]")
31
140
  parser.add_text("Editing Options:")
32
141
  parser.add_argument(
@@ -41,6 +150,16 @@ def main_options(parser: ArgumentParser) -> ArgumentParser:
41
150
  metavar="METHOD",
42
151
  help="Set an expression which determines how to make auto edits",
43
152
  )
153
+ parser.add_argument(
154
+ "--export", "-ex", metavar="EXPORT:ATTRS?", help="Choose the export mode"
155
+ )
156
+ parser.add_argument(
157
+ "--output",
158
+ "--output-file",
159
+ "-o",
160
+ metavar="FILE",
161
+ help="Set the name/path of the new output file",
162
+ )
44
163
  parser.add_argument(
45
164
  "--silent-speed",
46
165
  "-s",
@@ -111,12 +230,6 @@ def main_options(parser: ArgumentParser) -> ArgumentParser:
111
230
  metavar="COLOR",
112
231
  help="Set the background as a solid RGB color",
113
232
  )
114
- parser.add_argument(
115
- "--add",
116
- nargs="*",
117
- metavar="OBJ:START,DUR,ATTRS?",
118
- help="Insert an audio/video object to the timeline",
119
- )
120
233
  parser.add_text("URL Download Options:")
121
234
  parser.add_argument(
122
235
  "--yt-dlp-location",
@@ -138,28 +251,6 @@ def main_options(parser: ArgumentParser) -> ArgumentParser:
138
251
  metavar="CMD",
139
252
  help="Add extra options for yt-dlp. Must be in quotes",
140
253
  )
141
- parser.add_text("Utility Options:")
142
- parser.add_argument(
143
- "--export", "-ex", metavar="EXPORT:ATTRS?", help="Choose the export mode"
144
- )
145
- parser.add_argument(
146
- "--output-file",
147
- "--output",
148
- "-o",
149
- metavar="FILE",
150
- help="Set the name/path of the new output file",
151
- )
152
- parser.add_argument(
153
- "--player", "-p", metavar="CMD", help="Set player to open output media files"
154
- )
155
- parser.add_argument(
156
- "--no-open", flag=True, help="Do not open the output file after editing is done"
157
- )
158
- parser.add_argument(
159
- "--temp-dir",
160
- metavar="PATH",
161
- help="Set where the temporary directory is located",
162
- )
163
254
  parser.add_text("Display Options:")
164
255
  parser.add_argument(
165
256
  "--progress",
@@ -186,6 +277,16 @@ def main_options(parser: ArgumentParser) -> ArgumentParser:
186
277
  flag=True,
187
278
  help="Disable the inclusion of data streams in the output file",
188
279
  )
280
+ parser.add_argument(
281
+ "--faststart",
282
+ flag=True,
283
+ help="Enable movflags +faststart, recommended for web (default)",
284
+ )
285
+ parser.add_argument(
286
+ "--no-faststart",
287
+ flag=True,
288
+ help="Disable movflags +faststart, will be faster for large files",
289
+ )
189
290
  parser.add_argument(
190
291
  "--fragmented",
191
292
  flag=True,
@@ -241,10 +342,13 @@ def main_options(parser: ArgumentParser) -> ArgumentParser:
241
342
  metavar="BITRATE",
242
343
  help="Set the number of bits per second for audio",
243
344
  )
345
+ parser.add_argument(
346
+ "--mix-audio-streams", flag=True, help="Mix all audio streams together into one"
347
+ )
244
348
  parser.add_argument(
245
349
  "--keep-tracks-separate",
246
350
  flag=True,
247
- help="Don't mix all audio tracks into one when exporting",
351
+ help="Don't mix all audio streams into one when exporting (default)",
248
352
  )
249
353
  parser.add_argument(
250
354
  "--audio-normalize",
@@ -258,6 +362,17 @@ def main_options(parser: ArgumentParser) -> ArgumentParser:
258
362
  parser.add_argument(
259
363
  "--no-cache", flag=True, help="Don't look for or write a cache file"
260
364
  )
365
+ parser.add_argument(
366
+ "--no-open", flag=True, help="Do not open the output file after editing is done"
367
+ )
368
+ parser.add_argument(
369
+ "--temp-dir",
370
+ metavar="PATH",
371
+ help="Set where the temporary directory is located",
372
+ )
373
+ parser.add_argument(
374
+ "--player", "-p", metavar="CMD", help="Set player to open output media files"
375
+ )
261
376
  parser.add_argument("--version", "-V", flag=True, help="Display version and halt")
262
377
  return parser
263
378
 
@@ -337,15 +452,15 @@ def main() -> None:
337
452
  if args.debug and not args.input:
338
453
  buf = StringIO()
339
454
  buf.write(f"OS: {plat.system()} {plat.release()} {plat.machine().lower()}\n")
340
- buf.write(f"Python: {plat.python_version()}\nPyAV: ")
455
+ buf.write(f"Python: {plat.python_version()}\nAV: ")
341
456
  try:
342
- import av
457
+ import bv
343
458
  except (ModuleNotFoundError, ImportError):
344
459
  buf.write("not found")
345
460
  else:
346
461
  try:
347
- buf.write(f"{av.__version__} ")
348
- license = av._core.library_meta["libavcodec"]["license"]
462
+ buf.write(f"{bv.__version__} ")
463
+ license = bv._core.library_meta["libavcodec"]["license"]
349
464
  buf.write(f"({license})")
350
465
  except AttributeError:
351
466
  buf.write("error")
auto_editor/analyze.py CHANGED
@@ -9,10 +9,10 @@ from math import ceil
9
9
  from tempfile import gettempdir
10
10
  from typing import TYPE_CHECKING
11
11
 
12
- import av
12
+ import bv
13
13
  import numpy as np
14
- from av.audio.fifo import AudioFifo
15
- from av.subtitles.subtitle import AssSubtitle
14
+ from bv.audio.fifo import AudioFifo
15
+ from bv.subtitles.subtitle import AssSubtitle
16
16
 
17
17
  from auto_editor import __version__
18
18
 
@@ -72,7 +72,7 @@ def mut_remove_large(
72
72
  active = False
73
73
 
74
74
 
75
- def iter_audio(audio_stream: av.AudioStream, tb: Fraction) -> Iterator[np.float32]:
75
+ def iter_audio(audio_stream: bv.AudioStream, tb: Fraction) -> Iterator[np.float32]:
76
76
  fifo = AudioFifo()
77
77
  sr = audio_stream.rate
78
78
 
@@ -80,10 +80,10 @@ def iter_audio(audio_stream: av.AudioStream, tb: Fraction) -> Iterator[np.float3
80
80
  accumulated_error = Fraction(0)
81
81
 
82
82
  # Resample so that audio data is between [-1, 1]
83
- resampler = av.AudioResampler(av.AudioFormat("flt"), audio_stream.layout, sr)
83
+ resampler = bv.AudioResampler(bv.AudioFormat("flt"), audio_stream.layout, sr)
84
84
 
85
85
  container = audio_stream.container
86
- assert isinstance(container, av.container.InputContainer)
86
+ assert isinstance(container, bv.container.InputContainer)
87
87
 
88
88
  for frame in container.decode(audio_stream):
89
89
  frame.pts = None # Skip time checks
@@ -103,7 +103,7 @@ def iter_audio(audio_stream: av.AudioStream, tb: Fraction) -> Iterator[np.float3
103
103
 
104
104
 
105
105
  def iter_motion(
106
- video: av.VideoStream, tb: Fraction, blur: int, width: int
106
+ video: bv.VideoStream, tb: Fraction, blur: int, width: int
107
107
  ) -> Iterator[np.float32]:
108
108
  video.thread_type = "AUTO"
109
109
 
@@ -113,7 +113,7 @@ def iter_motion(
113
113
  index = 0
114
114
  prev_index = -1
115
115
 
116
- graph = av.filter.Graph()
116
+ graph = bv.filter.Graph()
117
117
  graph.link_nodes(
118
118
  graph.add_buffer(template=video),
119
119
  graph.add("scale", f"{width}:-1"),
@@ -123,7 +123,7 @@ def iter_motion(
123
123
  ).configure()
124
124
 
125
125
  container = video.container
126
- assert isinstance(container, av.container.InputContainer)
126
+ assert isinstance(container, bv.container.InputContainer)
127
127
 
128
128
  for unframe in container.decode(video):
129
129
  if unframe.pts is None:
@@ -154,7 +154,7 @@ def iter_motion(
154
154
 
155
155
  @dataclass(slots=True)
156
156
  class Levels:
157
- container: av.container.InputContainer
157
+ container: bv.container.InputContainer
158
158
  name: str
159
159
  mod_time: int
160
160
  tb: Fraction
@@ -258,7 +258,7 @@ class Levels:
258
258
  if audio.duration is not None and audio.time_base is not None:
259
259
  inaccurate_dur = int(audio.duration * audio.time_base * self.tb)
260
260
  elif container.duration is not None:
261
- inaccurate_dur = int(container.duration / av.time_base * self.tb)
261
+ inaccurate_dur = int(container.duration / bv.time_base * self.tb)
262
262
  else:
263
263
  inaccurate_dur = 1024
264
264
 
@@ -343,15 +343,9 @@ class Levels:
343
343
  for packet in container.demux(subtitle_stream):
344
344
  if packet.pts is None or packet.duration is None:
345
345
  continue
346
- for subset in packet.decode():
347
- # See definition of `AVSubtitle`
348
- # in: https://ffmpeg.org/doxygen/trunk/avcodec_8h_source.html
349
- start = float(packet.pts * subtitle_stream.time_base)
350
- dur = float(packet.duration * subtitle_stream.time_base)
351
-
352
- end = round((start + dur) * self.tb)
353
- sub_length = max(sub_length, end)
346
+ sub_length = max(sub_length, packet.pts + packet.duration)
354
347
 
348
+ sub_length = round(sub_length * subtitle_stream.time_base * self.tb)
355
349
  result = np.zeros((sub_length), dtype=np.bool_)
356
350
  del sub_length
357
351
 
@@ -363,25 +357,25 @@ class Levels:
363
357
  continue
364
358
  if early_exit:
365
359
  break
366
- for subset in packet.decode():
367
- if max_count is not None and count >= max_count:
368
- early_exit = True
369
- break
370
360
 
371
- start = float(packet.pts * subtitle_stream.time_base)
372
- dur = float(packet.duration * subtitle_stream.time_base)
361
+ if max_count is not None and count >= max_count:
362
+ early_exit = True
363
+ break
364
+
365
+ start = float(packet.pts * subtitle_stream.time_base)
366
+ dur = float(packet.duration * subtitle_stream.time_base)
373
367
 
374
- san_start = round(start * self.tb)
375
- san_end = round((start + dur) * self.tb)
368
+ san_start = round(start * self.tb)
369
+ san_end = round((start + dur) * self.tb)
376
370
 
377
- for sub in subset:
378
- if not isinstance(sub, AssSubtitle):
379
- continue
371
+ for sub in packet.decode():
372
+ if not isinstance(sub, AssSubtitle):
373
+ continue
380
374
 
381
- line = sub.dialogue.decode(errors="ignore")
382
- if line and re.search(re_pattern, line):
383
- result[san_start:san_end] = 1
384
- count += 1
375
+ line = sub.dialogue.decode(errors="ignore")
376
+ if line and re.search(re_pattern, line):
377
+ result[san_start:san_end] = 1
378
+ count += 1
385
379
 
386
380
  container.seek(0)
387
381
  return result
@@ -391,8 +385,8 @@ def initLevels(
391
385
  src: FileInfo, tb: Fraction, bar: Bar, no_cache: bool, log: Log
392
386
  ) -> Levels:
393
387
  try:
394
- container = av.open(src.path)
395
- except av.FFmpegError as e:
388
+ container = bv.open(src.path)
389
+ except bv.FFmpegError as e:
396
390
  log.error(e)
397
391
 
398
392
  mod_time = int(src.path.stat().st_mtime)
auto_editor/cmds/info.py CHANGED
@@ -6,7 +6,7 @@ from dataclasses import dataclass, field
6
6
  from typing import Any, Literal, TypedDict
7
7
 
8
8
  from auto_editor.ffwrapper import initFileInfo
9
- from auto_editor.lang.json import dump
9
+ from auto_editor.json import dump
10
10
  from auto_editor.make_layers import make_sane_timebase
11
11
  from auto_editor.timeline import v3
12
12
  from auto_editor.utils.func import aspect_ratio
@@ -5,7 +5,7 @@ from dataclasses import dataclass, field
5
5
  from fractions import Fraction
6
6
  from typing import TYPE_CHECKING
7
7
 
8
- import av
8
+ import bv
9
9
  import numpy as np
10
10
 
11
11
  from auto_editor.analyze import *
@@ -134,7 +134,7 @@ def main(sys_args: list[str] = sys.argv[1:]) -> None:
134
134
  if (arr := levels.read_cache("audio", (obj["stream"],))) is not None:
135
135
  print_arr(arr)
136
136
  else:
137
- container = av.open(src.path, "r")
137
+ container = bv.open(src.path, "r")
138
138
  audio_stream = container.streams.audio[obj["stream"]]
139
139
 
140
140
  values = []
@@ -155,7 +155,7 @@ def main(sys_args: list[str] = sys.argv[1:]) -> None:
155
155
  if (arr := levels.read_cache("motion", mobj)) is not None:
156
156
  print_arr(arr)
157
157
  else:
158
- container = av.open(src.path, "r")
158
+ container = bv.open(src.path, "r")
159
159
  video_stream = container.streams.video[obj["stream"]]
160
160
 
161
161
  values = []
@@ -1,18 +1,72 @@
1
1
  import sys
2
+ from dataclasses import dataclass, field
2
3
 
3
- import av
4
- from av.subtitles.subtitle import AssSubtitle
4
+ import bv
5
+ from bv.subtitles.subtitle import AssSubtitle
6
+
7
+ from auto_editor.json import dump
8
+ from auto_editor.vanparse import ArgumentParser
9
+
10
+
11
+ @dataclass(slots=True)
12
+ class SubdumpArgs:
13
+ help: bool = False
14
+ input: list[str] = field(default_factory=list)
15
+ json: bool = False
5
16
 
6
17
 
7
18
  def main(sys_args: list[str] = sys.argv[1:]) -> None:
8
- for i, input_file in enumerate(sys_args):
9
- with av.open(input_file) as container:
19
+ parser = ArgumentParser("subdump")
20
+ parser.add_required("input", nargs="*")
21
+ parser.add_argument("--json", flag=True)
22
+ args = parser.parse_args(SubdumpArgs, sys_args)
23
+
24
+ do_filter = True
25
+
26
+ if args.json:
27
+ data = {}
28
+ for input_file in args.input:
29
+ container = bv.open(input_file)
10
30
  for s in range(len(container.streams.subtitles)):
11
- print(f"file: {input_file} ({s}:{container.streams.subtitles[s].name})")
12
- for subset in container.decode(subtitles=s):
13
- for sub in subset:
31
+ entry_data = []
32
+
33
+ input_stream = container.streams.subtitles[s]
34
+ assert input_stream.time_base is not None
35
+ for packet in container.demux(input_stream):
36
+ if (
37
+ packet.dts is None
38
+ or packet.pts is None
39
+ or packet.duration is None
40
+ ):
41
+ continue
42
+
43
+ start = packet.pts * input_stream.time_base
44
+ end = start + packet.duration * input_stream.time_base
45
+
46
+ startf = round(float(start), 3)
47
+ endf = round(float(end), 3)
48
+
49
+ if do_filter and endf - startf <= 0.02:
50
+ continue
51
+
52
+ for sub in packet.decode():
14
53
  if isinstance(sub, AssSubtitle):
15
- print(sub.ass.decode("utf-8", errors="ignore"))
54
+ content = sub.dialogue.decode("utf-8", errors="ignore")
55
+ entry_data.append([startf, endf, content])
56
+
57
+ data[f"{input_file}:{s}"] = entry_data
58
+ container.close()
59
+
60
+ dump(data, sys.stdout, indent=4)
61
+ return
62
+
63
+ for i, input_file in enumerate(args.input):
64
+ with bv.open(input_file) as container:
65
+ for s in range(len(container.streams.subtitles)):
66
+ print(f"file: {input_file} ({s}:{container.streams.subtitles[s].name})")
67
+ for sub2 in container.decode(subtitles=s):
68
+ if isinstance(sub2, AssSubtitle):
69
+ print(sub2.ass.decode("utf-8", errors="ignore"))
16
70
  print("------")
17
71
 
18
72