auto-editor 26.0.1__py3-none-any.whl → 26.1.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.0.1"
1
+ __version__ = "26.1.0"
auto_editor/__main__.py CHANGED
@@ -8,7 +8,6 @@ from subprocess import run
8
8
 
9
9
  import auto_editor
10
10
  from auto_editor.edit import edit_media
11
- from auto_editor.ffwrapper import FFmpeg
12
11
  from auto_editor.utils.func import get_stdout
13
12
  from auto_editor.utils.log import Log
14
13
  from auto_editor.utils.types import (
@@ -34,13 +33,12 @@ def main_options(parser: ArgumentParser) -> ArgumentParser:
34
33
  "-m",
35
34
  type=margin,
36
35
  metavar="LENGTH",
37
- help='Set sections near "loud" as "loud" too if section is less than LENGTH away.',
36
+ help='Set sections near "loud" as "loud" too if section is less than LENGTH away',
38
37
  )
39
38
  parser.add_argument(
40
- "--edit-based-on",
41
39
  "--edit",
42
40
  metavar="METHOD",
43
- help="Decide which method to use when making edits",
41
+ help="Set an expression which determines how to make auto edits",
44
42
  )
45
43
  parser.add_argument(
46
44
  "--silent-speed",
@@ -148,7 +146,7 @@ def main_options(parser: ArgumentParser) -> ArgumentParser:
148
146
  "--output",
149
147
  "-o",
150
148
  metavar="FILE",
151
- help="Set the name/path of the new output file.",
149
+ help="Set the name/path of the new output file",
152
150
  )
153
151
  parser.add_argument(
154
152
  "--player", "-p", metavar="CMD", help="Set player to open output media files"
@@ -161,11 +159,6 @@ def main_options(parser: ArgumentParser) -> ArgumentParser:
161
159
  metavar="PATH",
162
160
  help="Set where the temporary directory is located",
163
161
  )
164
- parser.add_argument(
165
- "--ffmpeg-location",
166
- metavar="PATH",
167
- help="Set a custom path to the ffmpeg location",
168
- )
169
162
  parser.add_text("Display Options:")
170
163
  parser.add_argument(
171
164
  "--progress",
@@ -241,11 +234,6 @@ def main_options(parser: ArgumentParser) -> ArgumentParser:
241
234
  flag=True,
242
235
  help="Disable the inclusion of data streams in the output file",
243
236
  )
244
- parser.add_argument(
245
- "--extras",
246
- metavar="CMD",
247
- help="Add extra options for ffmpeg. Must be in quotes",
248
- )
249
237
  parser.add_argument(
250
238
  "--config", flag=True, help="When set, look for `config.pal` and run it"
251
239
  )
@@ -256,7 +244,7 @@ def main_options(parser: ArgumentParser) -> ArgumentParser:
256
244
  return parser
257
245
 
258
246
 
259
- def download_video(my_input: str, args: Args, ffmpeg: FFmpeg, log: Log) -> str:
247
+ def download_video(my_input: str, args: Args, log: Log) -> str:
260
248
  log.conwrite("Downloading video...")
261
249
 
262
250
  def get_domain(url: str) -> str:
@@ -272,18 +260,15 @@ def download_video(my_input: str, args: Args, ffmpeg: FFmpeg, log: Log) -> str:
272
260
  else:
273
261
  output_format = args.output_format
274
262
 
275
- yt_dlp_path = args.yt_dlp_location
276
-
277
- cmd = ["--ffmpeg-location", ffmpeg.get_path("yt-dlp", log)]
278
-
263
+ cmd = []
279
264
  if download_format is not None:
280
265
  cmd.extend(["-f", download_format])
281
266
 
282
267
  cmd.extend(["-o", output_format, my_input])
283
-
284
268
  if args.yt_dlp_extras is not None:
285
269
  cmd.extend(args.yt_dlp_extras.split(" "))
286
270
 
271
+ yt_dlp_path = args.yt_dlp_location
287
272
  try:
288
273
  location = get_stdout(
289
274
  [yt_dlp_path, "--get-filename", "--no-warnings"] + cmd
@@ -326,6 +311,7 @@ def main() -> None:
326
311
  ({"--export-as-json"}, ["--export", "json"]),
327
312
  ({"--export-as-clip-sequence", "-excs"}, ["--export", "clip-sequence"]),
328
313
  ({"--keep-tracks-seperate"}, ["--keep-tracks-separate"]),
314
+ ({"--edit-based-on"}, ["--edit"]),
329
315
  ],
330
316
  )
331
317
 
@@ -352,11 +338,10 @@ def main() -> None:
352
338
  is_machine = args.progress == "machine"
353
339
  log = Log(args.debug, args.quiet, args.temp_dir, is_machine, no_color)
354
340
 
355
- ffmpeg = FFmpeg(args.ffmpeg_location)
356
341
  paths = []
357
342
  for my_input in args.input:
358
343
  if my_input.startswith("http://") or my_input.startswith("https://"):
359
- paths.append(download_video(my_input, args, ffmpeg, log))
344
+ paths.append(download_video(my_input, args, log))
360
345
  else:
361
346
  if not splitext(my_input)[1]:
362
347
  if isdir(my_input):
@@ -370,7 +355,7 @@ def main() -> None:
370
355
  paths.append(my_input)
371
356
 
372
357
  try:
373
- edit_media(paths, ffmpeg, args, log)
358
+ edit_media(paths, args, log)
374
359
  except KeyboardInterrupt:
375
360
  log.error("Keyboard Interrupt")
376
361
  log.cleanup()
auto_editor/analyze.py CHANGED
@@ -27,6 +27,9 @@ if TYPE_CHECKING:
27
27
  from auto_editor.utils.log import Log
28
28
 
29
29
 
30
+ __all__ = ("LevelError", "Levels", "iter_audio", "iter_motion")
31
+
32
+
30
33
  class LevelError(Exception):
31
34
  pass
32
35
 
@@ -69,45 +72,39 @@ def mut_remove_large(
69
72
  active = False
70
73
 
71
74
 
72
- def iter_audio(src, tb: Fraction, stream: int = 0) -> Iterator[np.float32]:
75
+ def iter_audio(audio_stream: av.AudioStream, tb: Fraction) -> Iterator[np.float32]:
73
76
  fifo = AudioFifo()
74
- try:
75
- container = av.open(src.path, "r")
76
- audio_stream = container.streams.audio[stream]
77
- sample_rate = audio_stream.rate
77
+ sr = audio_stream.rate
78
78
 
79
- exact_size = (1 / tb) * sample_rate
80
- accumulated_error = 0
79
+ exact_size = (1 / tb) * sr
80
+ accumulated_error = Fraction(0)
81
81
 
82
- # Resample so that audio data is between [-1, 1]
83
- resampler = av.AudioResampler(
84
- av.AudioFormat("flt"), audio_stream.layout, sample_rate
85
- )
82
+ # Resample so that audio data is between [-1, 1]
83
+ resampler = av.AudioResampler(av.AudioFormat("flt"), audio_stream.layout, sr)
86
84
 
87
- for frame in container.decode(audio=stream):
88
- frame.pts = None # Skip time checks
85
+ container = audio_stream.container
86
+ assert isinstance(container, av.container.InputContainer)
89
87
 
90
- for reframe in resampler.resample(frame):
91
- fifo.write(reframe)
88
+ for frame in container.decode(audio_stream):
89
+ frame.pts = None # Skip time checks
92
90
 
93
- while fifo.samples >= ceil(exact_size):
94
- size_with_error = exact_size + accumulated_error
95
- current_size = round(size_with_error)
96
- accumulated_error = size_with_error - current_size
91
+ for reframe in resampler.resample(frame):
92
+ fifo.write(reframe)
97
93
 
98
- audio_chunk = fifo.read(current_size)
99
- assert audio_chunk is not None
100
- arr = audio_chunk.to_ndarray().flatten()
101
- yield np.max(np.abs(arr))
102
-
103
- finally:
104
- container.close()
94
+ while fifo.samples >= ceil(exact_size):
95
+ size_with_error = exact_size + accumulated_error
96
+ current_size = round(size_with_error)
97
+ accumulated_error = size_with_error - current_size
105
98
 
99
+ audio_chunk = fifo.read(current_size)
100
+ assert audio_chunk is not None
101
+ arr = audio_chunk.to_ndarray().flatten()
102
+ yield np.max(np.abs(arr))
106
103
 
107
- def iter_motion(src, tb, stream: int, blur: int, width: int) -> Iterator[np.float32]:
108
- container = av.open(src.path, "r")
109
104
 
110
- video = container.streams.video[stream]
105
+ def iter_motion(
106
+ video: av.VideoStream, tb: Fraction, blur: int, width: int
107
+ ) -> Iterator[np.float32]:
111
108
  video.thread_type = "AUTO"
112
109
 
113
110
  prev_frame = None
@@ -125,6 +122,9 @@ def iter_motion(src, tb, stream: int, blur: int, width: int) -> Iterator[np.floa
125
122
  graph.add("buffersink"),
126
123
  ).configure()
127
124
 
125
+ container = video.container
126
+ assert isinstance(container, av.container.InputContainer)
127
+
128
128
  for unframe in container.decode(video):
129
129
  if unframe.pts is None:
130
130
  continue
@@ -151,8 +151,6 @@ def iter_motion(src, tb, stream: int, blur: int, width: int) -> Iterator[np.floa
151
151
  prev_frame = current_frame
152
152
  prev_index = index
153
153
 
154
- container.close()
155
-
156
154
 
157
155
  def obj_tag(path: Path, kind: str, tb: Fraction, obj: Sequence[object]) -> str:
158
156
  mod_time = int(path.stat().st_mtime)
@@ -175,7 +173,11 @@ class Levels:
175
173
  if (arr := self.read_cache("audio", (0,))) is not None:
176
174
  return len(arr)
177
175
 
178
- result = sum(1 for _ in iter_audio(self.src, self.tb, 0))
176
+ with av.open(self.src.path, "r") as container:
177
+ audio_stream = container.streams.audio[0]
178
+ self.log.experimental(audio_stream.codec)
179
+ result = sum(1 for _ in iter_audio(audio_stream, self.tb))
180
+
179
181
  self.log.debug(f"Audio Length: {result}")
180
182
  return result
181
183
 
@@ -239,21 +241,26 @@ class Levels:
239
241
  if (arr := self.read_cache("audio", (stream,))) is not None:
240
242
  return arr
241
243
 
242
- with av.open(self.src.path, "r") as container:
243
- audio = container.streams.audio[stream]
244
- if audio.duration is not None and audio.time_base is not None:
245
- inaccurate_dur = int(audio.duration * audio.time_base * self.tb)
246
- elif container.duration is not None:
247
- inaccurate_dur = int(container.duration / av.time_base * self.tb)
248
- else:
249
- inaccurate_dur = 1024
244
+ container = av.open(self.src.path, "r")
245
+ audio = container.streams.audio[stream]
246
+
247
+ if audio.codec.experimental:
248
+ self.log.error(f"`{audio.codec.name}` is an experimental codec")
249
+
250
+ if audio.duration is not None and audio.time_base is not None:
251
+ inaccurate_dur = int(audio.duration * audio.time_base * self.tb)
252
+ elif container.duration is not None:
253
+ inaccurate_dur = int(container.duration / av.time_base * self.tb)
254
+ else:
255
+ inaccurate_dur = 1024
250
256
 
251
257
  bar = self.bar
252
258
  bar.start(inaccurate_dur, "Analyzing audio volume")
253
259
 
254
260
  result = np.zeros((inaccurate_dur), dtype=np.float32)
255
261
  index = 0
256
- for value in iter_audio(self.src, self.tb, stream):
262
+
263
+ for value in iter_audio(audio, self.tb):
257
264
  if index > len(result) - 1:
258
265
  result = np.concatenate(
259
266
  (result, np.zeros((len(result)), dtype=np.float32))
@@ -263,6 +270,7 @@ class Levels:
263
270
  index += 1
264
271
 
265
272
  bar.end()
273
+ assert len(result) > 0
266
274
  return self.cache(result[:index], "audio", (stream,))
267
275
 
268
276
  def motion(self, stream: int, blur: int, width: int) -> NDArray[np.float32]:
@@ -273,20 +281,25 @@ class Levels:
273
281
  if (arr := self.read_cache("motion", mobj)) is not None:
274
282
  return arr
275
283
 
276
- with av.open(self.src.path, "r") as container:
277
- video = container.streams.video[stream]
278
- inaccurate_dur = (
279
- 1024
280
- if video.duration is None or video.time_base is None
281
- else int(video.duration * video.time_base * self.tb)
282
- )
284
+ container = av.open(self.src.path, "r")
285
+ video = container.streams.video[stream]
286
+
287
+ if video.codec.experimental:
288
+ self.log.experimental(video.codec)
289
+
290
+ inaccurate_dur = (
291
+ 1024
292
+ if video.duration is None or video.time_base is None
293
+ else int(video.duration * video.time_base * self.tb)
294
+ )
283
295
 
284
296
  bar = self.bar
285
297
  bar.start(inaccurate_dur, "Analyzing motion")
286
298
 
287
299
  result = np.zeros((inaccurate_dur), dtype=np.float32)
288
300
  index = 0
289
- for value in iter_motion(self.src, self.tb, stream, blur, width):
301
+
302
+ for value in iter_motion(video, self.tb, blur, width):
290
303
  if index > len(result) - 1:
291
304
  result = np.concatenate(
292
305
  (result, np.zeros((len(result)), dtype=np.float32))
auto_editor/edit.py CHANGED
@@ -10,7 +10,7 @@ from typing import Any
10
10
  import av
11
11
  from av import AudioResampler
12
12
 
13
- from auto_editor.ffwrapper import FFmpeg, FileInfo, initFileInfo
13
+ from auto_editor.ffwrapper import FileInfo, initFileInfo
14
14
  from auto_editor.lib.contracts import is_int, is_str
15
15
  from auto_editor.make_layers import clipify, make_av, make_timeline
16
16
  from auto_editor.output import Ensure, parse_bitrate
@@ -160,7 +160,7 @@ def parse_export(export: str, log: Log) -> dict[str, Any]:
160
160
  log.error(f"'{name}': Export must be [{', '.join([s for s in parsing.keys()])}]")
161
161
 
162
162
 
163
- def edit_media(paths: list[str], ffmpeg: FFmpeg, args: Args, log: Log) -> None:
163
+ def edit_media(paths: list[str], args: Args, log: Log) -> None:
164
164
  bar = initBar(args.progress)
165
165
  tl = None
166
166
 
@@ -294,7 +294,7 @@ def edit_media(paths: list[str], ffmpeg: FFmpeg, args: Args, log: Log) -> None:
294
294
 
295
295
  if ctr.default_aud != "none":
296
296
  ensure = Ensure(bar, samplerate, log)
297
- audio_paths = make_new_audio(tl, ctr, ensure, args, ffmpeg, bar, log)
297
+ audio_paths = make_new_audio(tl, ctr, ensure, args, bar, log)
298
298
  else:
299
299
  audio_paths = []
300
300
 
@@ -343,8 +343,8 @@ def edit_media(paths: list[str], ffmpeg: FFmpeg, args: Args, log: Log) -> None:
343
343
  for i, sub_path in enumerate(sub_paths):
344
344
  subtitle_input = av.open(sub_path)
345
345
  subtitle_inputs.append(subtitle_input)
346
- subtitle_stream = output.add_stream(
347
- template=subtitle_input.streams.subtitles[0]
346
+ subtitle_stream = output.add_stream_from_template(
347
+ subtitle_input.streams.subtitles[0]
348
348
  )
349
349
  if i < len(src.subtitles) and src.subtitles[i].lang is not None:
350
350
  subtitle_stream.metadata["language"] = src.subtitles[i].lang # type: ignore
auto_editor/ffwrapper.py CHANGED
@@ -3,40 +3,12 @@ from __future__ import annotations
3
3
  from dataclasses import dataclass
4
4
  from fractions import Fraction
5
5
  from pathlib import Path
6
- from shutil import which
7
- from subprocess import PIPE, Popen
8
6
 
9
7
  import av
10
8
 
11
9
  from auto_editor.utils.log import Log
12
10
 
13
11
 
14
- def _get_ffmpeg(reason: str, ffloc: str | None, log: Log) -> str:
15
- program = "ffmpeg" if ffloc is None else ffloc
16
- if (path := which(program)) is None:
17
- log.error(f"{reason} needs ffmpeg cli but couldn't find ffmpeg on PATH.")
18
- return path
19
-
20
-
21
- @dataclass(slots=True)
22
- class FFmpeg:
23
- ffmpeg_location: str | None
24
- path: str | None = None
25
-
26
- def get_path(self, reason: str, log: Log) -> str:
27
- if self.path is not None:
28
- return self.path
29
-
30
- self.path = _get_ffmpeg(reason, self.ffmpeg_location, log)
31
- return self.path
32
-
33
- def Popen(self, reason: str, cmd: list[str], log: Log) -> Popen:
34
- if self.path is None:
35
- self.path = _get_ffmpeg(reason, self.ffmpeg_location, log)
36
-
37
- return Popen([self.path] + cmd, stdout=PIPE, stderr=PIPE)
38
-
39
-
40
12
  def mux(input: Path, output: Path, stream: int) -> None:
41
13
  input_container = av.open(input, "r")
42
14
  output_container = av.open(output, "w")
auto_editor/help.py CHANGED
@@ -24,10 +24,23 @@ example:
24
24
  will set the speed from 400 ticks to 800 ticks to 2.5x
25
25
  If timebase is 30, 400 ticks to 800 means 13.33 to 26.66 seconds
26
26
  """.strip(),
27
- "--edit-based-on": """
27
+ "--edit": """
28
28
  Evaluates a palet expression that returns a bool-array?. The array is then used for
29
29
  editing.
30
30
 
31
+ Examples:
32
+ --edit audio
33
+ --edit audio:0.03 ; Change the threshold. Can be a value between 0-1.
34
+ --edit audio:3% ; You can also use the `%` macro.
35
+ --edit audio:0.03,stream=0 ; Only consider the first stream for editing.
36
+ --edit audio:stream=1,threshold=0.05 ; Here's how you use keyword arguments.
37
+ --edit (or audio:0.04,stream=0 audio:0.08,stream=1) ; Consider both streams for editing (merge with logical or), but with different thresholds.
38
+ --edit motion
39
+ --edit motion:0.02,blur=3
40
+ --edit (or audio:0.04 motion:0.02,blur=3)
41
+ --edit none
42
+ --edit all/e
43
+
31
44
  Editing Methods:
32
45
  - audio ; Audio silence/loudness detection
33
46
  - threshold threshold? : 4%
@@ -52,19 +65,6 @@ Editing Methods:
52
65
 
53
66
  - none ; Do not modify the media in anyway; mark all sections as "loud" (1).
54
67
  - all/e ; Cut out everything out; mark all sections as "silent" (0).
55
-
56
-
57
- Command-line Examples:
58
- --edit audio
59
- --edit audio:threshold=4%
60
- --edit audio:threshold=0.03
61
- --edit audio:stream=1
62
- --edit (or audio:4%,stream=0 audio:8%,stream=1) ; `threshold` is first
63
- --edit motion
64
- --edit motion:threshold=2%,blur=3
65
- --edit (or audio:4% motion:2%,blur=3)
66
- --edit none
67
- --edit all/e
68
68
  """.strip(),
69
69
  "--export": """
70
70
  This option controls how timelines are exported.
@@ -144,8 +144,6 @@ If not set, tempdir will be set with Python's tempfile module
144
144
  The directory doesn't have to exist beforehand, however, the root path must be valid.
145
145
  Beware that the temp directory can get quite big.
146
146
  """.strip(),
147
- "--ffmpeg-location": "This takes precedence over `--my-ffmpeg`.",
148
- "--my-ffmpeg": "This is equivalent to `--ffmpeg-location ffmpeg`.",
149
147
  "--audio-bitrate": """
150
148
  `--audio-bitrate` sets the target bitrate for the audio encoder.
151
149
  By default, the value is `auto` (let the encoder decide).
@@ -139,7 +139,7 @@ def make_timeline(
139
139
 
140
140
  for i, src in enumerate(sources):
141
141
  try:
142
- parser = Parser(Lexer("`--edit`", args.edit_based_on))
142
+ parser = Parser(Lexer("`--edit`", args.edit))
143
143
  if log.is_debug:
144
144
  log.debug(f"edit: {parser}")
145
145
 
@@ -169,6 +169,8 @@ def make_timeline(
169
169
  has_loud = concat((has_loud, result))
170
170
  src_index = concat((src_index, np.full(len(result), i, dtype=np.int32)))
171
171
 
172
+ assert len(has_loud) > 0
173
+
172
174
  # Setup for handling custom speeds
173
175
  speed_index = has_loud.astype(np.uint)
174
176
  speed_map = [args.silent_speed, args.video_speed]
@@ -2,12 +2,12 @@ from __future__ import annotations
2
2
 
3
3
  import io
4
4
  from pathlib import Path
5
- from platform import system
6
5
 
7
6
  import av
8
7
  import numpy as np
8
+ from av.filter.loudnorm import stats
9
9
 
10
- from auto_editor.ffwrapper import FFmpeg, FileInfo
10
+ from auto_editor.ffwrapper import FileInfo
11
11
  from auto_editor.lang.json import Lexer, Parser
12
12
  from auto_editor.lang.palet import env
13
13
  from auto_editor.lib.contracts import andc, between_c, is_int_or_float
@@ -56,25 +56,11 @@ def parse_norm(norm: str, log: Log) -> dict | None:
56
56
  log.error(e)
57
57
 
58
58
 
59
- def parse_ebu_bytes(norm: dict, stderr: bytes, log: Log) -> tuple[str, str]:
60
- start = end = 0
61
- lines = stderr.splitlines()
62
-
63
- for index, line in enumerate(lines):
64
- if line.startswith(b"[Parsed_loudnorm"):
65
- start = index + 1
66
- continue
67
- if start != 0 and line.startswith(b"}"):
68
- end = index + 1
69
- break
70
-
71
- if start == 0 or end == 0:
72
- log.error(f"Invalid loudnorm stats.\n{stderr!r}")
73
-
59
+ def parse_ebu_bytes(norm: dict, stat: bytes, log: Log) -> tuple[str, str]:
74
60
  try:
75
- parsed = Parser(Lexer("loudnorm", b"\n".join(lines[start:end]))).expr()
61
+ parsed = Parser(Lexer("loudnorm", stat)).expr()
76
62
  except MyError:
77
- log.error(f"Invalid loudnorm stats.\n{start=},{end=}\n{stderr!r}")
63
+ log.error(f"Invalid loudnorm stats.\n{stat!r}")
78
64
 
79
65
  for key in ("input_i", "input_tp", "input_lra", "input_thresh", "target_offset"):
80
66
  val = float(parsed[key])
@@ -101,29 +87,17 @@ def parse_ebu_bytes(norm: dict, stderr: bytes, log: Log) -> tuple[str, str]:
101
87
 
102
88
 
103
89
  def apply_audio_normalization(
104
- ffmpeg: FFmpeg, norm: dict, pre_master: Path, path: Path, log: Log
90
+ norm: dict, pre_master: Path, path: Path, log: Log
105
91
  ) -> None:
106
92
  if norm["tag"] == "ebu":
107
93
  first_pass = (
108
- f"loudnorm=i={norm['i']}:lra={norm['lra']}:tp={norm['tp']}:"
109
- f"offset={norm['gain']}:print_format=json"
94
+ f"i={norm['i']}:lra={norm['lra']}:tp={norm['tp']}:" f"offset={norm['gain']}"
110
95
  )
111
96
  log.debug(f"audio norm first pass: {first_pass}")
112
- file_null = "NUL" if system() in ("Windows", "cli") else "/dev/null"
113
- cmd = [
114
- "-hide_banner",
115
- "-i",
116
- f"{pre_master}",
117
- "-af",
118
- first_pass,
119
- "-vn",
120
- "-sn",
121
- "-f",
122
- "null",
123
- file_null,
124
- ]
125
- stderr = ffmpeg.Popen("EBU", cmd, log).communicate()[1]
126
- name, filter_args = parse_ebu_bytes(norm, stderr, log)
97
+ with av.open(f"{pre_master}") as container:
98
+ stats_ = stats(first_pass, container.streams.audio[0])
99
+
100
+ name, filter_args = parse_ebu_bytes(norm, stats_, log)
127
101
  else:
128
102
  assert "t" in norm
129
103
 
@@ -310,13 +284,7 @@ def mix_audio_files(sr: int, audio_paths: list[str], output_path: str) -> None:
310
284
 
311
285
 
312
286
  def make_new_audio(
313
- tl: v3,
314
- ctr: Container,
315
- ensure: Ensure,
316
- args: Args,
317
- ffmpeg: FFmpeg,
318
- bar: Bar,
319
- log: Log,
287
+ tl: v3, ctr: Container, ensure: Ensure, args: Args, bar: Bar, log: Log
320
288
  ) -> list[str]:
321
289
  sr = tl.sr
322
290
  tb = tl.tb
@@ -390,7 +358,7 @@ def make_new_audio(
390
358
  with open(pre_master, "wb") as fid:
391
359
  write(fid, sr, arr)
392
360
 
393
- apply_audio_normalization(ffmpeg, norm, pre_master, path, log)
361
+ apply_audio_normalization(norm, pre_master, path, log)
394
362
 
395
363
  bar.end()
396
364
 
@@ -162,7 +162,7 @@ def _ensure(input_: Input, format: str, stream: int) -> str:
162
162
  output = av.open(output_bytes, "w", format=format)
163
163
 
164
164
  in_stream = input_.streams.subtitles[stream]
165
- out_stream = output.add_stream(template=in_stream)
165
+ out_stream = output.add_stream_from_template(in_stream)
166
166
 
167
167
  for packet in input_.demux(in_stream):
168
168
  if packet.dts is None:
@@ -163,6 +163,8 @@ def main(sys_args: list[str] = sys.argv[1:]) -> None:
163
163
  file_info[file]["subtitle"].append(sub)
164
164
 
165
165
  if args.json:
166
+ if sys.platform == "win32":
167
+ sys.stdout.reconfigure(encoding="utf-8")
166
168
  dump(file_info, sys.stdout, indent=4)
167
169
  return
168
170
 
@@ -5,9 +5,10 @@ from dataclasses import dataclass, field
5
5
  from fractions import Fraction
6
6
  from typing import TYPE_CHECKING
7
7
 
8
+ import av
8
9
  import numpy as np
9
10
 
10
- from auto_editor.analyze import LevelError, Levels, iter_audio, iter_motion
11
+ from auto_editor.analyze import *
11
12
  from auto_editor.ffwrapper import initFileInfo
12
13
  from auto_editor.lang.palet import env
13
14
  from auto_editor.lib.contracts import is_bool, is_nat, is_nat1, is_str, is_void, orc
@@ -130,9 +131,19 @@ def main(sys_args: list[str] = sys.argv[1:]) -> None:
130
131
  levels = Levels(src, tb, bar, False, log, strict=True)
131
132
  try:
132
133
  if method == "audio":
133
- print_arr_gen(iter_audio(src, tb, **obj))
134
+ container = av.open(src.path, "r")
135
+ audio_stream = container.streams.audio[obj["stream"]]
136
+ log.experimental(audio_stream.codec)
137
+ print_arr_gen(iter_audio(audio_stream, tb))
138
+ container.close()
139
+
134
140
  elif method == "motion":
135
- print_arr_gen(iter_motion(src, tb, **obj))
141
+ container = av.open(src.path, "r")
142
+ video_stream = container.streams.video[obj["stream"]]
143
+ log.experimental(video_stream.codec)
144
+ print_arr_gen(iter_motion(video_stream, tb, obj["blur"], obj["width"]))
145
+ container.close()
146
+
136
147
  elif method == "subtitle":
137
148
  print_arr(levels.subtitle(**obj))
138
149
  elif method == "none":
auto_editor/utils/log.py CHANGED
@@ -7,6 +7,8 @@ from tempfile import mkdtemp
7
7
  from time import perf_counter, sleep
8
8
  from typing import NoReturn
9
9
 
10
+ import av
11
+
10
12
 
11
13
  class Log:
12
14
  __slots__ = ("is_debug", "quiet", "machine", "no_color", "_temp", "_ut", "_s")
@@ -97,6 +99,10 @@ class Log:
97
99
 
98
100
  sys.stdout.write(f"Finished. took {second_len} seconds ({minute_len})\n")
99
101
 
102
+ def experimental(self, codec: av.Codec) -> None:
103
+ if codec.experimental:
104
+ self.error(f"`{codec.name}` is an experimental codec")
105
+
100
106
  def error(self, message: str | Exception) -> NoReturn:
101
107
  if self.is_debug and isinstance(message, Exception):
102
108
  self.cleanup()
@@ -210,14 +210,13 @@ class Args:
210
210
  sample_rate: int | None = None
211
211
  resolution: tuple[int, int] | None = None
212
212
  background: str = "#000000"
213
- edit_based_on: str = "audio"
213
+ edit: str = "audio"
214
214
  keep_tracks_separate: bool = False
215
215
  audio_normalize: str = "#f"
216
216
  export: str | None = None
217
217
  player: str | None = None
218
218
  no_open: bool = False
219
219
  temp_dir: str | None = None
220
- ffmpeg_location: str | None = None
221
220
  progress: str = "modern"
222
221
  version: bool = False
223
222
  debug: bool = False
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: auto-editor
3
- Version: 26.0.1
3
+ Version: 26.1.0
4
4
  Summary: Auto-Editor: Effort free video editing!
5
5
  Author-email: WyattBlue <wyattblue@auto-editor.com>
6
6
  License: Unlicense
@@ -11,8 +11,8 @@ Keywords: video,audio,media,editor,editing,processing,nonlinear,automatic,silenc
11
11
  Requires-Python: <3.14,>=3.10
12
12
  Description-Content-Type: text/markdown
13
13
  License-File: LICENSE
14
- Requires-Dist: numpy <3.0,>=1.23.0
15
- Requires-Dist: pyav ==13.1.*
14
+ Requires-Dist: numpy<3.0,>=1.24
15
+ Requires-Dist: pyav==14.*
16
16
 
17
17
  <p align="center"><img src="https://auto-editor.com/img/auto-editor-banner.webp" title="Auto-Editor" width="700"></p>
18
18
 
@@ -1,10 +1,10 @@
1
- auto_editor/__init__.py,sha256=IVlNYVWUewREksm_90yLqKeN61i9bI8Lx_lWZz4SObk,23
2
- auto_editor/__main__.py,sha256=vSvw0fWi4GOIsak_RuOBnMB_CrtNNp8COh7i8LV4acE,11541
3
- auto_editor/analyze.py,sha256=uCi21659BB-lbPwZ6yxNLekS6Q3yoB2ypLNXPhmhTfg,11688
4
- auto_editor/edit.py,sha256=_fjvs2UzK84Wl6DVZwyRBbqbxh16z4VPNTHUlvjU9iQ,15977
5
- auto_editor/ffwrapper.py,sha256=Sx1-9OmAStR73I-RR2XPwWPTmGywM7ssqT9-U0sffA4,5615
6
- auto_editor/help.py,sha256=62s3L0rlhA7nkkOjtXItRUl779EJ__A7_6E-VFH3J_E,7924
7
- auto_editor/make_layers.py,sha256=8uFy5SvMArAP-5slYJrxa_iGAEwimQBFeM-T01VORVw,8995
1
+ auto_editor/__init__.py,sha256=8MQdwPYn_Y7GCbtRLrmuh9XSy5S52w2pxd3bulKs9Ag,23
2
+ auto_editor/__main__.py,sha256=eAsNa1BP4Y6Oyp4l838YmcxEwsM0LUdbaGeNFELe4h0,11124
3
+ auto_editor/analyze.py,sha256=HyRdnty3VW9ZTwwPwjsZp3bLVRLvII_1Y6NlEItDKfw,11947
4
+ auto_editor/edit.py,sha256=eEMRaQbn0jylfJ6D_egnUXjoMCbdQVsAu7MDrn-xlGo,15950
5
+ auto_editor/ffwrapper.py,sha256=Tct_Q-uy5F51h8M7UFam50UzRFpgkBvUamJP1AoKVvc,4749
6
+ auto_editor/help.py,sha256=CzfDTsL4GuGu596ySHKj_wKnxGR9h8B0KUdkZpo33oE,8044
7
+ auto_editor/make_layers.py,sha256=vEeJt0PnE1vc9-cQZ_AlXVDjvWhObRCWJSCQGraoMvU,9016
8
8
  auto_editor/output.py,sha256=ho8Lpqz4Sv_Gw0Vj2OvG39s83xHpyZlvtRNryTPbXqc,2563
9
9
  auto_editor/preview.py,sha256=HUsjmV9Fx73rZ26BXrpz9z-z_e4oiui3u9e7qbbGoBY,3037
10
10
  auto_editor/timeline.py,sha256=XfaH9cH-RB-MObOpMr5IfLcqJcjmabO1XwkUkT3_FQM,8186
@@ -27,13 +27,13 @@ auto_editor/lib/contracts.py,sha256=lExGQymcQUmwG5lC1lO4qm4GY8W0q_yzK_miTaAoPA4,
27
27
  auto_editor/lib/data_structs.py,sha256=dcsXgsLLzbmFDUZucoirzewPALsKzoxz7z5L22_QJM8,7091
28
28
  auto_editor/lib/err.py,sha256=UlszQJdzMZwkbT8x3sY4GkCV_5x9yrd6uVVUzvA8iiI,35
29
29
  auto_editor/render/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
30
- auto_editor/render/audio.py,sha256=KvhAJf-5_HFkRoaYaKOYraT2uQr65gdayuicWYOpjgk,13376
31
- auto_editor/render/subtitle.py,sha256=qyP_AZHwGToVBeH8qMSa9LUenMaNmsnJN8w0Y7SXQ3o,6235
30
+ auto_editor/render/audio.py,sha256=1iOQCeRXfRz28cqnHp2XeK-f3_UnPf80AKQAfifGvdE,12584
31
+ auto_editor/render/subtitle.py,sha256=lf2l1QWJgFiqlpQWWBwSlKJnSgW8Lkfi59WrJMbIDqM,6240
32
32
  auto_editor/render/video.py,sha256=dje0RNW2dKILfTzt0VAF0WR6REfGOsc6l17pP1Z4ooA,12215
33
33
  auto_editor/subcommands/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
34
34
  auto_editor/subcommands/desc.py,sha256=GDrKJYiHMaeTrplZAceXl1JwoqD78XsV2_5lc0Xd7po,869
35
- auto_editor/subcommands/info.py,sha256=t5n43HLt9hpMFSIfGV777X4zIPBAFugOKlpCfRjiKxY,6921
36
- auto_editor/subcommands/levels.py,sha256=ChJMDTd34-jgxewqHRmmd3VNhFdy964w0DcQG0ls-hY,4079
35
+ auto_editor/subcommands/info.py,sha256=UDdoxd6_fqSoRPwthkWXqnpxHp7dJQ0Dn96lYX_ubWc,7010
36
+ auto_editor/subcommands/levels.py,sha256=psSSIsGfzr9j0HGKp2yvK6nMlrkLwxkwsyI0uF2xb_c,4496
37
37
  auto_editor/subcommands/palet.py,sha256=ONzTqemaQq9YEfIOsDRNnwzfqnEMUMSXIQrETxyroRU,749
38
38
  auto_editor/subcommands/repl.py,sha256=TF_I7zsFY7-KdgidrqjafTz7o_eluVbLvgTcOBG-UWQ,3449
39
39
  auto_editor/subcommands/subdump.py,sha256=af_XBf7kaevqHn1A71z8C-7x8pS5WKD9FE_ugkCw6rk,665
@@ -44,12 +44,12 @@ auto_editor/utils/chunks.py,sha256=J-eGKtEz68gFtRrj1kOSgH4Tj_Yz6prNQ7Xr-d9NQJw,5
44
44
  auto_editor/utils/cmdkw.py,sha256=aUGBvBel2Ko1o6Rwmr4rEL-BMc5hEnzYLbyZ1GeJdcY,5729
45
45
  auto_editor/utils/container.py,sha256=Wf1ZL0tvXWl6m1B9mK_SkgVl89ilV_LpwlQq0TVroCc,2704
46
46
  auto_editor/utils/func.py,sha256=kB-pNDn20M6YT7sljyd_auve5teK-E2G4TgwVOAIuJw,2754
47
- auto_editor/utils/log.py,sha256=M2QKeQHMRNLm3HMVUKedZPRprT2u5dipOStiO4miPBk,3613
48
- auto_editor/utils/types.py,sha256=ecjTQmTlKoT9Wbwb_N4p6wC7s3bxiKPmq8sF15WfyVs,10772
47
+ auto_editor/utils/log.py,sha256=C1b-vnszSsohMd5fyaRcCuf0OPobZVMkV77cP-_JNP4,3776
48
+ auto_editor/utils/types.py,sha256=7BF7R7DA5eKmtI6f5ia7bOYNL0u_2sviiPsE1VmP0lc,10724
49
49
  docs/build.py,sha256=CM-ZWgQk8wSNjivx_-6wGIaG7cstrNKsX2d4TzFVivE,1642
50
- auto_editor-26.0.1.dist-info/LICENSE,sha256=yiq99pWITHfqS0pbZMp7cy2dnbreTuvBwudsU-njvIM,1210
51
- auto_editor-26.0.1.dist-info/METADATA,sha256=e_JZKirHFmWBAXaCDqXHIOB4J-IIpbuoK72fzhlXMtw,6115
52
- auto_editor-26.0.1.dist-info/WHEEL,sha256=P9jw-gEje8ByB7_hXoICnHtVCrEwMQh-630tKvQWehc,91
53
- auto_editor-26.0.1.dist-info/entry_points.txt,sha256=-H7zdTw4MqnAcwrN5xTNkGIhzZtJMxS9r6lTMeR9-aA,240
54
- auto_editor-26.0.1.dist-info/top_level.txt,sha256=jBV5zlbWRbKOa-xaWPvTD45QL7lGExx2BDzv-Ji4dTw,17
55
- auto_editor-26.0.1.dist-info/RECORD,,
50
+ auto_editor-26.1.0.dist-info/LICENSE,sha256=yiq99pWITHfqS0pbZMp7cy2dnbreTuvBwudsU-njvIM,1210
51
+ auto_editor-26.1.0.dist-info/METADATA,sha256=QDfveFTnxTtnA2WqZTvgM4vNGQ1sTnziQ2MSAyqF5WQ,6109
52
+ auto_editor-26.1.0.dist-info/WHEEL,sha256=PZUExdf71Ui_so67QXpySuHtCi3-J3wvF4ORK6k_S8U,91
53
+ auto_editor-26.1.0.dist-info/entry_points.txt,sha256=-H7zdTw4MqnAcwrN5xTNkGIhzZtJMxS9r6lTMeR9-aA,240
54
+ auto_editor-26.1.0.dist-info/top_level.txt,sha256=jBV5zlbWRbKOa-xaWPvTD45QL7lGExx2BDzv-Ji4dTw,17
55
+ auto_editor-26.1.0.dist-info/RECORD,,
@@ -1,5 +1,5 @@
1
1
  Wheel-Version: 1.0
2
- Generator: setuptools (75.3.0)
2
+ Generator: setuptools (75.6.0)
3
3
  Root-Is-Purelib: true
4
4
  Tag: py3-none-any
5
5