auto-editor 28.0.2__py3-none-any.whl → 28.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__ = "28.0.2"
1
+ __version__ = "28.1.0"
auto_editor/__main__.py CHANGED
@@ -441,6 +441,7 @@ def main() -> None:
441
441
  ({"--export-to-resolve", "-exr"}, ["--export", "resolve"]),
442
442
  ({"--export-to-final-cut-pro", "-exf"}, ["--export", "final-cut-pro"]),
443
443
  ({"--export-to-shotcut", "-exs"}, ["--export", "shotcut"]),
444
+ ({"--export-to-kdenlive", "-exk"}, ["--export", "kdenlive"]),
444
445
  ({"--export-as-clip-sequence", "-excs"}, ["--export", "clip-sequence"]),
445
446
  ({"--edit-based-on"}, ["--edit"]),
446
447
  ],
@@ -454,13 +455,13 @@ def main() -> None:
454
455
  buf.write(f"OS: {plat.system()} {plat.release()} {plat.machine().lower()}\n")
455
456
  buf.write(f"Python: {plat.python_version()}\nAV: ")
456
457
  try:
457
- import bv
458
+ import av
458
459
  except (ModuleNotFoundError, ImportError):
459
460
  buf.write("not found")
460
461
  else:
461
462
  try:
462
- buf.write(f"{bv.__version__} ")
463
- license = bv._core.library_meta["libavcodec"]["license"]
463
+ buf.write(f"{av.__version__} ")
464
+ license = av._core.library_meta["libavcodec"]["license"]
464
465
  buf.write(f"({license})")
465
466
  except AttributeError:
466
467
  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 bv
12
+ import av
13
13
  import numpy as np
14
- from bv.audio.fifo import AudioFifo
15
- from bv.subtitles.subtitle import AssSubtitle
14
+ from av.audio.fifo import AudioFifo
15
+ from av.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: bv.AudioStream, tb: Fraction) -> Iterator[np.float32]:
75
+ def iter_audio(audio_stream: av.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: bv.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 = bv.AudioResampler(bv.AudioFormat("flt"), audio_stream.layout, sr)
83
+ resampler = av.AudioResampler(av.AudioFormat("flt"), audio_stream.layout, sr)
84
84
 
85
85
  container = audio_stream.container
86
- assert isinstance(container, bv.container.InputContainer)
86
+ assert isinstance(container, av.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: bv.AudioStream, tb: Fraction) -> Iterator[np.float3
103
103
 
104
104
 
105
105
  def iter_motion(
106
- video: bv.VideoStream, tb: Fraction, blur: int, width: int
106
+ video: av.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 = bv.filter.Graph()
116
+ graph = av.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, bv.container.InputContainer)
126
+ assert isinstance(container, av.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: bv.container.InputContainer
157
+ container: av.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 / bv.time_base * self.tb)
261
+ inaccurate_dur = int(container.duration / av.time_base * self.tb)
262
262
  else:
263
263
  inaccurate_dur = 1024
264
264
 
@@ -385,8 +385,8 @@ def initLevels(
385
385
  src: FileInfo, tb: Fraction, bar: Bar, no_cache: bool, log: Log
386
386
  ) -> Levels:
387
387
  try:
388
- container = bv.open(src.path)
389
- except bv.FFmpegError as e:
388
+ container = av.open(src.path)
389
+ except av.FFmpegError as e:
390
390
  log.error(e)
391
391
 
392
392
  mod_time = int(src.path.stat().st_mtime)
auto_editor/cmds/desc.py CHANGED
@@ -1,7 +1,7 @@
1
1
  import sys
2
2
  from dataclasses import dataclass, field
3
3
 
4
- import bv
4
+ import av
5
5
 
6
6
  from auto_editor.vanparse import ArgumentParser
7
7
 
@@ -21,7 +21,7 @@ def main(sys_args: list[str] = sys.argv[1:]) -> None:
21
21
  args = desc_options(ArgumentParser("desc")).parse_args(DescArgs, sys_args)
22
22
  for path in args.input:
23
23
  try:
24
- container = bv.open(path)
24
+ container = av.open(path)
25
25
  desc = container.metadata.get("description", None)
26
26
  except Exception:
27
27
  desc = None
@@ -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 bv
8
+ import av
9
9
  import numpy as np
10
10
 
11
11
  from auto_editor.analyze import *
@@ -137,7 +137,7 @@ def main(sys_args: list[str] = sys.argv[1:]) -> None:
137
137
  ):
138
138
  print_arr(arr)
139
139
  else:
140
- container = bv.open(src.path, "r")
140
+ container = av.open(src.path, "r")
141
141
  audio_stream = container.streams.audio[obj["stream"]]
142
142
 
143
143
  values = []
@@ -162,7 +162,7 @@ def main(sys_args: list[str] = sys.argv[1:]) -> None:
162
162
  ):
163
163
  print_arr(arr)
164
164
  else:
165
- container = bv.open(src.path, "r")
165
+ container = av.open(src.path, "r")
166
166
  video_stream = container.streams.video[obj["stream"]]
167
167
 
168
168
  values = []
@@ -1,8 +1,8 @@
1
1
  import sys
2
2
  from dataclasses import dataclass, field
3
3
 
4
- import bv
5
- from bv.subtitles.subtitle import AssSubtitle
4
+ import av
5
+ from av.subtitles.subtitle import AssSubtitle
6
6
 
7
7
  from auto_editor.json import dump
8
8
  from auto_editor.vanparse import ArgumentParser
@@ -24,7 +24,7 @@ def main(sys_args: list[str] = sys.argv[1:]) -> None:
24
24
  if args.json:
25
25
  data = {}
26
26
  for input_file in args.input:
27
- container = bv.open(input_file)
27
+ container = av.open(input_file)
28
28
  for s in range(len(container.streams.subtitles)):
29
29
  entry_data = []
30
30
 
@@ -59,7 +59,7 @@ def main(sys_args: list[str] = sys.argv[1:]) -> None:
59
59
  return
60
60
 
61
61
  for input_file in args.input:
62
- with bv.open(input_file) as container:
62
+ with av.open(input_file) as container:
63
63
  for s in range(len(container.streams.subtitles)):
64
64
  print(f"file: {input_file} ({s}:{container.streams.subtitles[s].name})")
65
65
  for sub2 in container.decode(subtitles=s):
auto_editor/cmds/test.py CHANGED
@@ -11,8 +11,9 @@ from hashlib import sha256
11
11
  from tempfile import mkdtemp
12
12
  from time import perf_counter
13
13
 
14
- import bv
14
+ import av
15
15
  import numpy as np
16
+ from av import AudioStream, VideoStream
16
17
 
17
18
  from auto_editor.ffwrapper import FileInfo
18
19
  from auto_editor.lang.palet import Lexer, Parser, env, interpret
@@ -58,7 +59,7 @@ all_files = (
58
59
  "mov_text.mp4",
59
60
  "testsrc.mkv",
60
61
  )
61
- log = Log()
62
+ log = Log(is_debug=True)
62
63
 
63
64
 
64
65
  def fileinfo(path: str) -> FileInfo:
@@ -103,10 +104,11 @@ class Runner:
103
104
 
104
105
  return output
105
106
 
106
- def raw(self, cmd: list[str]) -> None:
107
+ def raw(self, cmd: list[str]) -> str:
107
108
  returncode, stdout, stderr = pipe_to_console(self.program + cmd)
108
109
  if returncode > 0:
109
110
  raise Exception(f"{stdout}\n{stderr}\n")
111
+ return stdout
110
112
 
111
113
  def check(self, cmd: list[str], match=None) -> None:
112
114
  returncode, stdout, stderr = pipe_to_console(self.program + cmd)
@@ -133,11 +135,13 @@ class Runner:
133
135
 
134
136
  def test_version(self):
135
137
  """Test version flags and debug by itself."""
136
- self.raw(["--version"])
137
- self.raw(["-V"])
138
+ v1 = self.raw(["--version"])
139
+ v2 = self.raw(["-V"])
140
+ assert "." in v1 and len(v1) > 4
141
+ assert v1 == v2
138
142
 
139
143
  def test_parser(self):
140
- self.check(["example.mp4", "--video-speed"], "needs argument")
144
+ self.check(["example.mp4", "--margin"], "needs argument")
141
145
 
142
146
  def info(self):
143
147
  self.raw(["info", "example.mp4"])
@@ -161,36 +165,36 @@ class Runner:
161
165
  file = "resources/testsrc.mp4"
162
166
  out = self.main([file], ["--faststart"]) + ".mp4"
163
167
  fast = calculate_sha256(out)
164
- with bv.open(out) as container:
165
- assert isinstance(container.streams[0], bv.VideoStream)
166
- assert isinstance(container.streams[1], bv.AudioStream)
168
+ with av.open(out) as container:
169
+ assert isinstance(container.streams[0], VideoStream)
170
+ assert isinstance(container.streams[1], AudioStream)
167
171
 
168
172
  out = self.main([file], ["--no-faststart"]) + ".mp4"
169
173
  nofast = calculate_sha256(out)
170
- with bv.open(out) as container:
171
- assert isinstance(container.streams[0], bv.VideoStream)
172
- assert isinstance(container.streams[1], bv.AudioStream)
174
+ with av.open(out) as container:
175
+ assert isinstance(container.streams[0], VideoStream)
176
+ assert isinstance(container.streams[1], AudioStream)
173
177
 
174
178
  out = self.main([file], ["--fragmented"]) + ".mp4"
175
179
  frag = calculate_sha256(out)
176
- with bv.open(out) as container:
177
- assert isinstance(container.streams[0], bv.VideoStream)
178
- assert isinstance(container.streams[1], bv.AudioStream)
180
+ with av.open(out) as container:
181
+ assert isinstance(container.streams[0], VideoStream)
182
+ assert isinstance(container.streams[1], AudioStream)
179
183
 
180
184
  assert fast != nofast, "+faststart is not being applied"
181
185
  assert frag not in (fast, nofast), "fragmented output should diff."
182
186
 
183
187
  def test_example(self) -> None:
184
188
  out = self.main(["example.mp4"], [], output="example_ALTERED.mp4")
185
- with bv.open(out) as container:
189
+ with av.open(out) as container:
186
190
  assert container.duration is not None
187
191
  assert container.duration > 17300000 and container.duration < 2 << 24
188
192
 
189
193
  assert len(container.streams) == 2
190
194
  video = container.streams[0]
191
195
  audio = container.streams[1]
192
- assert isinstance(video, bv.VideoStream)
193
- assert isinstance(audio, bv.AudioStream)
196
+ assert isinstance(video, VideoStream)
197
+ assert isinstance(audio, AudioStream)
194
198
  assert video.base_rate == 30
195
199
  assert video.average_rate is not None
196
200
  assert video.average_rate == 30, video.average_rate
@@ -204,28 +208,28 @@ class Runner:
204
208
 
205
209
  def test_video_to_mp3(self) -> None:
206
210
  out = self.main(["example.mp4"], [], output="example_ALTERED.mp3")
207
- with bv.open(out) as container:
211
+ with av.open(out) as container:
208
212
  assert container.duration is not None
209
213
  assert container.duration > 17300000 and container.duration < 2 << 24
210
214
 
211
215
  assert len(container.streams) == 1
212
216
  audio = container.streams[0]
213
- assert isinstance(audio, bv.AudioStream)
217
+ assert isinstance(audio, AudioStream)
214
218
  assert audio.codec.name in ("mp3", "mp3float")
215
219
  assert audio.sample_rate == 48000
216
220
  assert audio.layout.name == "stereo"
217
221
 
218
222
  def test_to_mono(self) -> None:
219
223
  out = self.main(["example.mp4"], ["-layout", "mono"], output="example_mono.mp4")
220
- with bv.open(out) as container:
224
+ with av.open(out) as container:
221
225
  assert container.duration is not None
222
226
  assert container.duration > 17300000 and container.duration < 2 << 24
223
227
 
224
228
  assert len(container.streams) == 2
225
229
  video = container.streams[0]
226
230
  audio = container.streams[1]
227
- assert isinstance(video, bv.VideoStream)
228
- assert isinstance(audio, bv.AudioStream)
231
+ assert isinstance(video, VideoStream)
232
+ assert isinstance(audio, AudioStream)
229
233
  assert video.base_rate == 30
230
234
  assert video.average_rate is not None
231
235
  assert video.average_rate == 30, video.average_rate
@@ -302,18 +306,18 @@ class Runner:
302
306
  self.check([path, "--no-open"], "must have an extension")
303
307
 
304
308
  def test_silent_threshold(self):
305
- with bv.open("resources/new-commentary.mp3") as container:
309
+ with av.open("resources/new-commentary.mp3") as container:
306
310
  assert container.duration is not None
307
- assert container.duration / bv.time_base == 6.732
311
+ assert container.duration / av.time_base == 6.732
308
312
 
309
313
  out = self.main(
310
314
  ["resources/new-commentary.mp3"], ["--edit", "audio:threshold=0.1"]
311
315
  )
312
316
  out += ".mp3"
313
317
 
314
- with bv.open(out) as container:
318
+ with av.open(out) as container:
315
319
  assert container.duration is not None
316
- assert container.duration / bv.time_base == 6.552
320
+ assert container.duration / av.time_base == 6.552
317
321
 
318
322
  def test_track(self):
319
323
  out = self.main(["resources/multi-track.mov"], []) + ".mov"
auto_editor/edit.py CHANGED
@@ -9,7 +9,8 @@ from pathlib import Path
9
9
  from subprocess import run
10
10
  from typing import TYPE_CHECKING, Any
11
11
 
12
- import bv
12
+ import av
13
+ from av import Codec
13
14
 
14
15
  from auto_editor.ffwrapper import FileInfo
15
16
  from auto_editor.lib.contracts import is_int, is_str
@@ -50,6 +51,8 @@ def set_output(
50
51
  export = "final-cut-pro"
51
52
  case ".mlt":
52
53
  export = "shotcut"
54
+ case ".kdenlive":
55
+ export = "kdenlive"
53
56
  case ".json" | ".v1":
54
57
  export = "v1"
55
58
  case ".v3":
@@ -64,6 +67,8 @@ def set_output(
64
67
  ext = ".fcpxml"
65
68
  case "shotcut":
66
69
  ext = ".mlt"
70
+ case "kdenlive":
71
+ ext = ".kdenlive"
67
72
  case "v1":
68
73
  if ext != ".json":
69
74
  ext = ".v1"
@@ -79,9 +84,6 @@ def set_output(
79
84
  return f"{root}{ext}", export
80
85
 
81
86
 
82
- codec_error = "'{}' codec is not supported in '{}' container."
83
-
84
-
85
87
  def set_video_codec(
86
88
  codec: str, src: FileInfo | None, out_ext: str, ctr: Container, log: Log
87
89
  ) -> str:
@@ -93,12 +95,14 @@ def set_video_codec(
93
95
 
94
96
  if ctr.vcodecs is not None and codec not in ctr.vcodecs:
95
97
  try:
96
- cobj = bv.Codec(codec, "w")
97
- except bv.codec.codec.UnknownCodecError:
98
+ cobj = Codec(codec, "w")
99
+ except av.codec.codec.UnknownCodecError:
98
100
  log.error(f"Unknown encoder: {codec}")
99
101
  # Normalize encoder names
100
- if cobj.id not in (bv.Codec(x, "w").id for x in ctr.vcodecs):
101
- log.error(codec_error.format(codec, out_ext))
102
+ if cobj.id not in (Codec(x, "w").id for x in ctr.vcodecs):
103
+ log.error(
104
+ f"'{codec}' video encoder is not supported in the '{out_ext}' container"
105
+ )
102
106
 
103
107
  return codec
104
108
 
@@ -111,7 +115,7 @@ def set_audio_codec(
111
115
  codec = "aac"
112
116
  else:
113
117
  codec = src.audios[0].codec
114
- if bv.Codec(codec, "w").audio_formats is None:
118
+ if Codec(codec, "w").audio_formats is None:
115
119
  codec = "aac"
116
120
  if codec not in ctr.acodecs and ctr.default_aud != "none":
117
121
  codec = ctr.default_aud
@@ -121,13 +125,14 @@ def set_audio_codec(
121
125
 
122
126
  if ctr.acodecs is None or codec not in ctr.acodecs:
123
127
  try:
124
- cobj = bv.Codec(codec, "w")
125
- except bv.codec.codec.UnknownCodecError:
128
+ cobj = Codec(codec, "w")
129
+ except av.codec.codec.UnknownCodecError:
126
130
  log.error(f"Unknown encoder: {codec}")
127
131
  # Normalize encoder names
128
- if cobj.id not in (bv.Codec(x, "w").id for x in ctr.acodecs):
129
- log.error(codec_error.format(codec, out_ext))
130
-
132
+ if cobj.id not in (Codec(x, "w").id for x in ctr.acodecs):
133
+ log.error(
134
+ f"'{codec}' audio encoder is not supported in the '{out_ext}' container"
135
+ )
131
136
  return codec
132
137
 
133
138
 
@@ -148,6 +153,7 @@ def parse_export(export: str, log: Log) -> dict[str, Any]:
148
153
  "resolve": pAttrs("resolve", name_attr),
149
154
  "resolve-fcp7": pAttrs("resolve-fcp7", name_attr),
150
155
  "shotcut": pAttrs("shotcut"),
156
+ "kdenlive": pAttrs("kdenlive"),
151
157
  "v1": pAttrs("v1"),
152
158
  "v3": pAttrs("v3"),
153
159
  "clip-sequence": pAttrs("clip-sequence"),
@@ -261,6 +267,12 @@ def edit_media(paths: list[str], args: Args, log: Log) -> None:
261
267
  shotcut_write_mlt(output, tl)
262
268
  return
263
269
 
270
+ if export == "kdenlive":
271
+ from auto_editor.exports.kdenlive import kdenlive_write
272
+
273
+ kdenlive_write(output, tl)
274
+ return
275
+
264
276
  if output == "-":
265
277
  log.error("Exporting media files to stdout is not supported.")
266
278
  out_ext = splitext(output)[1].replace(".", "")
@@ -287,26 +299,26 @@ def edit_media(paths: list[str], args: Args, log: Log) -> None:
287
299
  if mov_flags:
288
300
  options["movflags"] = "+".join(mov_flags)
289
301
 
290
- output = bv.open(output_path, "w", container_options=options)
302
+ output = av.open(output_path, "w", container_options=options)
291
303
 
292
304
  # Setup video
293
305
  if ctr.default_vid not in ("none", "png") and tl.v:
294
306
  vframes = render_av(output, tl, args, log)
295
- output_stream: bv.VideoStream | None
307
+ output_stream: av.VideoStream | None
296
308
  output_stream = next(vframes) # type: ignore
297
309
  else:
298
310
  output_stream, vframes = None, iter([])
299
311
 
300
312
  # Setup audio
301
313
  try:
302
- audio_encoder = bv.Codec(args.audio_codec, "w")
303
- except bv.FFmpegError as e:
314
+ audio_encoder = Codec(args.audio_codec, "w")
315
+ except av.FFmpegError as e:
304
316
  log.error(e)
305
317
  if audio_encoder.audio_formats is None:
306
318
  log.error(f"{args.audio_codec}: No known audio formats avail.")
307
319
  fmt = audio_encoder.audio_formats[0]
308
320
 
309
- audio_streams: list[bv.AudioStream] = []
321
+ audio_streams: list[av.AudioStream] = []
310
322
 
311
323
  if ctr.default_aud == "none":
312
324
  while len(tl.a) > 0:
@@ -333,7 +345,7 @@ def edit_media(paths: list[str], args: Args, log: Log) -> None:
333
345
  sub_gen_frames = []
334
346
 
335
347
  for i, sub_path in enumerate(sub_paths):
336
- subtitle_input = bv.open(sub_path)
348
+ subtitle_input = av.open(sub_path)
337
349
  subtitle_inputs.append(subtitle_input)
338
350
  subtitle_stream = output.add_stream_from_template(
339
351
  subtitle_input.streams.subtitles[0]
@@ -460,14 +472,14 @@ def edit_media(paths: list[str], args: Args, log: Log) -> None:
460
472
  output.mux(item.stream.encode(item.frame))
461
473
  elif frame_type == "subtitle":
462
474
  output.mux(item.frame)
463
- except bv.error.ExternalError:
475
+ except av.error.ExternalError:
464
476
  log.error(
465
477
  f"Generic error for encoder: {item.stream.name}\n"
466
478
  f"at {item.index} time_base\nPerhaps video quality settings are too low?"
467
479
  )
468
- except bv.FileNotFoundError:
480
+ except av.FileNotFoundError:
469
481
  log.error(f"File not found: {output_path}")
470
- except bv.FFmpegError as e:
482
+ except av.FFmpegError as e:
471
483
  log.error(e)
472
484
 
473
485
  if bar_index: