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.
@@ -0,0 +1,322 @@
1
+ import json
2
+ import xml.etree.ElementTree as ET
3
+ from os import getcwd
4
+ from uuid import uuid4
5
+
6
+ from auto_editor.timeline import Clip, v3
7
+ from auto_editor.utils.func import aspect_ratio, to_timecode
8
+
9
+ """
10
+ kdenlive uses the MLT timeline format
11
+
12
+ See docs here:
13
+ https://mltframework.org/docs/mltxml/
14
+
15
+ kdenlive specifics:
16
+ https://github.com/KDE/kdenlive/blob/master/dev-docs/fileformat.md
17
+ """
18
+
19
+
20
+ def kdenlive_write(output: str, tl: v3) -> None:
21
+ mlt = ET.Element(
22
+ "mlt",
23
+ attrib={
24
+ "LC_NUMERIC": "C",
25
+ "version": "7.22.0",
26
+ "producer": "main_bin",
27
+ "root": f"{getcwd()}",
28
+ },
29
+ )
30
+
31
+ width, height = tl.res
32
+ num, den = aspect_ratio(width, height)
33
+ tb = tl.tb
34
+ seq_uuid = uuid4()
35
+
36
+ ET.SubElement(
37
+ mlt,
38
+ "profile",
39
+ attrib={
40
+ "description": "automatic",
41
+ "width": f"{width}",
42
+ "height": f"{height}",
43
+ "progressive": "1",
44
+ "sample_aspect_num": "1",
45
+ "sample_aspect_den": "1",
46
+ "display_aspect_num": f"{num}",
47
+ "display_aspect_den": f"{den}",
48
+ "frame_rate_num": f"{tb.numerator}",
49
+ "frame_rate_den": f"{tb.denominator}",
50
+ "colorspace": "709",
51
+ },
52
+ )
53
+
54
+ # Reserved producer0
55
+ global_out = to_timecode(len(tl) / tb, "standard")
56
+ producer = ET.SubElement(mlt, "producer", id="producer0")
57
+ ET.SubElement(producer, "property", name="length").text = global_out
58
+ ET.SubElement(producer, "property", name="eof").text = "continue"
59
+ ET.SubElement(producer, "property", name="resource").text = "black"
60
+ ET.SubElement(producer, "property", name="mlt_service").text = "color"
61
+ ET.SubElement(producer, "property", name="kdenlive:playlistid").text = "black_track"
62
+ ET.SubElement(producer, "property", name="mlt_image_format").text = "rgba"
63
+ ET.SubElement(producer, "property", name="aspect_ratio").text = "1"
64
+
65
+ # Get all clips
66
+ if tl.v:
67
+ clips = [clip for clip in tl.v[0] if isinstance(clip, Clip)]
68
+ elif tl.a:
69
+ clips = tl.a[0]
70
+ else:
71
+ clips = []
72
+
73
+ source_ids = {}
74
+ source_id = 4
75
+ clip_playlists = []
76
+ chains = 0
77
+ playlists = 0
78
+ producers = 1
79
+ a_channels = len(tl.a)
80
+ v_channels = len(tl.v)
81
+ warped_clips = [i for i, clip in enumerate(clips) if clip.speed != 1]
82
+
83
+ # create all producers for warped clips
84
+ for clip_idx in warped_clips:
85
+ for i in range(a_channels + v_channels):
86
+ clip = clips[clip_idx]
87
+ path = str(clip.src.path)
88
+
89
+ if path not in source_ids:
90
+ source_ids[path] = str(source_id)
91
+ source_id += 1
92
+
93
+ prod = ET.SubElement(
94
+ mlt,
95
+ "producer",
96
+ attrib={
97
+ "id": f"producer{producers}",
98
+ "in": "00:00:00.000",
99
+ "out": global_out,
100
+ },
101
+ )
102
+ ET.SubElement(
103
+ prod, "property", name="resource"
104
+ ).text = f"{clip.speed}:{path}"
105
+ ET.SubElement(prod, "property", name="warp_speed").text = str(clip.speed)
106
+ ET.SubElement(prod, "property", name="warp_resource").text = path
107
+ ET.SubElement(prod, "property", name="warp_pitch").text = "0"
108
+ ET.SubElement(prod, "property", name="mlt_service").text = "timewarp"
109
+ ET.SubElement(prod, "property", name="kdenlive:id").text = source_ids[path]
110
+
111
+ if i < a_channels:
112
+ ET.SubElement(prod, "property", name="vstream").text = "0"
113
+ ET.SubElement(prod, "property", name="astream").text = str(
114
+ a_channels - 1 - i
115
+ )
116
+ ET.SubElement(prod, "property", name="set.test_audio").text = "0"
117
+ ET.SubElement(prod, "property", name="set.test_video").text = "1"
118
+ else:
119
+ ET.SubElement(prod, "property", name="vstream").text = str(
120
+ v_channels - 1 - (i - a_channels)
121
+ )
122
+ ET.SubElement(prod, "property", name="astream").text = "0"
123
+ ET.SubElement(prod, "property", name="set.test_audio").text = "1"
124
+ ET.SubElement(prod, "property", name="set.test_video").text = "0"
125
+
126
+ producers += 1
127
+
128
+ # create chains, playlists and tractors for audio channels
129
+ for i, audio in enumerate(tl.a):
130
+ path = str(audio[0].src.path)
131
+
132
+ if path not in source_ids:
133
+ source_ids[path] = str(source_id)
134
+ source_id += 1
135
+
136
+ chain = ET.SubElement(mlt, "chain", attrib={"id": f"chain{chains}"})
137
+ ET.SubElement(chain, "property", name="resource").text = path
138
+ ET.SubElement(
139
+ chain, "property", name="mlt_service"
140
+ ).text = "avformat-novalidate"
141
+ ET.SubElement(chain, "property", name="vstream").text = "0"
142
+ ET.SubElement(chain, "property", name="astream").text = str(a_channels - 1 - i)
143
+ ET.SubElement(chain, "property", name="set.test_audio").text = "0"
144
+ ET.SubElement(chain, "property", name="set.test_video").text = "1"
145
+ ET.SubElement(chain, "property", name="kdenlive:id").text = source_ids[path]
146
+
147
+ for _i in range(2):
148
+ playlist = ET.SubElement(mlt, "playlist", id=f"playlist{playlists}")
149
+ clip_playlists.append(playlist)
150
+ ET.SubElement(playlist, "property", name="kdenlive:audio_track").text = "1"
151
+ playlists += 1
152
+
153
+ tractor = ET.SubElement(
154
+ mlt,
155
+ "tractor",
156
+ attrib={"id": f"tractor{chains}", "in": "00:00:00.000", "out": global_out},
157
+ )
158
+ ET.SubElement(tractor, "property", name="kdenlive:audio_track").text = "1"
159
+ ET.SubElement(tractor, "property", name="kdenlive:timeline_active").text = "1"
160
+ ET.SubElement(tractor, "property", name="kdenlive:audio_rec")
161
+ ET.SubElement(
162
+ tractor,
163
+ "track",
164
+ attrib={"hide": "video", "producer": f"playlist{playlists - 2}"},
165
+ )
166
+ ET.SubElement(
167
+ tractor,
168
+ "track",
169
+ attrib={"hide": "video", "producer": f"playlist{playlists - 1}"},
170
+ )
171
+ chains += 1
172
+
173
+ # create chains, playlists and tractors for video channels
174
+ for i, video in enumerate(tl.v):
175
+ path = f"{video[0].src.path}" # type: ignore
176
+
177
+ if path not in source_ids:
178
+ source_ids[path] = str(source_id)
179
+ source_id += 1
180
+
181
+ chain = ET.SubElement(mlt, "chain", attrib={"id": f"chain{chains}"})
182
+ ET.SubElement(chain, "property", name="resource").text = path
183
+ ET.SubElement(
184
+ chain, "property", name="mlt_service"
185
+ ).text = "avformat-novalidate"
186
+ ET.SubElement(chain, "property", name="vstream").text = str(v_channels - 1 - i)
187
+ ET.SubElement(chain, "property", name="astream").text = "0"
188
+ ET.SubElement(chain, "property", name="set.test_audio").text = "1"
189
+ ET.SubElement(chain, "property", name="set.test_video").text = "0"
190
+ ET.SubElement(chain, "property", name="kdenlive:id").text = source_ids[path]
191
+
192
+ for _i in range(2):
193
+ playlist = ET.SubElement(mlt, "playlist", id=f"playlist{playlists}")
194
+ clip_playlists.append(playlist)
195
+ playlists += 1
196
+
197
+ tractor = ET.SubElement(
198
+ mlt,
199
+ "tractor",
200
+ attrib={"id": f"tractor{chains}", "in": "00:00:00.000", "out": global_out},
201
+ )
202
+ ET.SubElement(tractor, "property", name="kdenlive:timeline_active").text = "1"
203
+ ET.SubElement(
204
+ tractor,
205
+ "track",
206
+ attrib={"hide": "audio", "producer": f"playlist{playlists - 2}"},
207
+ )
208
+ ET.SubElement(
209
+ tractor,
210
+ "track",
211
+ attrib={"hide": "audio", "producer": f"playlist{playlists - 1}"},
212
+ )
213
+ chains += 1
214
+
215
+ # final chain for the project bin
216
+ path = str(clips[0].src.path)
217
+ chain = ET.SubElement(mlt, "chain", attrib={"id": f"chain{chains}"})
218
+ ET.SubElement(chain, "property", name="resource").text = path
219
+ ET.SubElement(chain, "property", name="mlt_service").text = "avformat-novalidate"
220
+ ET.SubElement(chain, "property", name="audio_index").text = "1"
221
+ ET.SubElement(chain, "property", name="video_index").text = "0"
222
+ ET.SubElement(chain, "property", name="vstream").text = "0"
223
+ ET.SubElement(chain, "property", name="astream").text = "0"
224
+ ET.SubElement(chain, "property", name="kdenlive:id").text = source_ids[path]
225
+
226
+ groups = []
227
+ group_counter = 0
228
+ producers = 1
229
+
230
+ for clip in clips:
231
+ group_children: list[object] = []
232
+ _in = to_timecode(clip.offset / tb, "standard")
233
+ _out = to_timecode((clip.offset + clip.dur) / tb, "standard")
234
+ path = str(clip.src.path)
235
+
236
+ for i, playlist in enumerate(clip_playlists[::2]):
237
+ # adding 1 extra frame for each previous group to the start time works but feels hacky?
238
+ group_children.append(
239
+ {
240
+ "data": f"{i}:{clip.start + group_counter}",
241
+ "leaf": "clip",
242
+ "type": "Leaf",
243
+ }
244
+ )
245
+ clip_prod = ""
246
+
247
+ if clip.speed == 1:
248
+ clip_prod = f"chain{i}"
249
+ else:
250
+ clip_prod = f"producer{producers}"
251
+ producers += 1
252
+
253
+ entry = ET.SubElement(
254
+ playlist,
255
+ "entry",
256
+ attrib={"producer": f"{clip_prod}", "in": _in, "out": _out},
257
+ )
258
+ ET.SubElement(entry, "property", name="kdenlive:id").text = source_ids[path]
259
+
260
+ groups.append({"children": group_children[:], "type": "Normal"})
261
+ group_counter += 1
262
+
263
+ # default sequence tractor
264
+ sequence = ET.SubElement(
265
+ mlt,
266
+ "tractor",
267
+ attrib={"id": f"{{{seq_uuid}}}", "in": "00:00:00.000", "out": "00:00:00.000"},
268
+ )
269
+ ET.SubElement(sequence, "property", name="kdenlive:uuid").text = f"{{{seq_uuid}}}"
270
+ ET.SubElement(sequence, "property", name="kdenlive:clipname").text = "Sequence 1"
271
+ ET.SubElement(
272
+ sequence, "property", name="kdenlive:sequenceproperties.groups"
273
+ ).text = json.dumps(groups, indent=4)
274
+ ET.SubElement(sequence, "track", producer="producer0")
275
+
276
+ for i in range(chains):
277
+ ET.SubElement(sequence, "track", producer=f"tractor{i}")
278
+
279
+ # main bin
280
+ playlist_bin = ET.SubElement(mlt, "playlist", id="main_bin")
281
+ ET.SubElement(
282
+ playlist_bin, "property", name="kdenlive:docproperties.uuid"
283
+ ).text = f"{{{seq_uuid}}}"
284
+ ET.SubElement(
285
+ playlist_bin, "property", name="kdenlive:docproperties.version"
286
+ ).text = "1.1"
287
+ ET.SubElement(playlist_bin, "property", name="xml_retain").text = "1"
288
+ ET.SubElement(
289
+ playlist_bin,
290
+ "entry",
291
+ attrib={
292
+ "producer": f"{{{seq_uuid}}}",
293
+ "in": "00:00:00.000",
294
+ "out": "00:00:00.000",
295
+ },
296
+ )
297
+ ET.SubElement(
298
+ playlist_bin,
299
+ "entry",
300
+ attrib={"producer": f"chain{chains}", "in": "00:00:00.000"},
301
+ )
302
+
303
+ # reserved last tractor for project
304
+ tractor = ET.SubElement(
305
+ mlt,
306
+ "tractor",
307
+ attrib={"id": f"tractor{chains}", "in": "00:00:00.000", "out": global_out},
308
+ )
309
+ ET.SubElement(tractor, "property", name="kdenlive:projectTractor").text = "1"
310
+ ET.SubElement(
311
+ tractor,
312
+ "track",
313
+ attrib={"producer": f"{{{seq_uuid}}}", "in": "00:00:00.000", "out": global_out},
314
+ )
315
+ tree = ET.ElementTree(mlt)
316
+
317
+ ET.indent(tree, space="\t", level=0)
318
+
319
+ if output == "-":
320
+ print(ET.tostring(mlt, encoding="unicode"))
321
+ else:
322
+ tree.write(output, xml_declaration=True, encoding="utf-8")
auto_editor/ffwrapper.py CHANGED
@@ -4,14 +4,14 @@ from dataclasses import dataclass
4
4
  from fractions import Fraction
5
5
  from pathlib import Path
6
6
 
7
- import bv
7
+ import av
8
8
 
9
9
  from auto_editor.utils.log import Log
10
10
 
11
11
 
12
12
  def mux(input: Path, output: Path, stream: int) -> None:
13
- input_container = bv.open(input, "r")
14
- output_container = bv.open(output, "w")
13
+ input_container = av.open(input, "r")
14
+ output_container = av.open(output, "w")
15
15
 
16
16
  input_audio_stream = input_container.streams.audio[stream]
17
17
  output_audio_stream = output_container.add_stream("pcm_s16le")
@@ -89,12 +89,12 @@ class FileInfo:
89
89
  @classmethod
90
90
  def init(self, path: str, log: Log) -> FileInfo:
91
91
  try:
92
- cont = bv.open(path, "r")
93
- except bv.error.FileNotFoundError:
92
+ cont = av.open(path, "r")
93
+ except av.error.FileNotFoundError:
94
94
  log.error(f"Input file doesn't exist: {path}")
95
- except bv.error.IsADirectoryError:
95
+ except av.error.IsADirectoryError:
96
96
  log.error(f"Expected a media file, but got a directory: {path}")
97
- except bv.error.InvalidDataError:
97
+ except av.error.InvalidDataError:
98
98
  log.error(f"Invalid data when processing: {path}")
99
99
 
100
100
  videos: tuple[VideoStream, ...] = ()
@@ -177,7 +177,7 @@ class FileInfo:
177
177
 
178
178
  timecode = get_timecode()
179
179
  bitrate = 0 if cont.bit_rate is None else cont.bit_rate
180
- dur = 0 if cont.duration is None else cont.duration / bv.time_base
180
+ dur = 0 if cont.duration is None else cont.duration / av.time_base
181
181
 
182
182
  cont.close()
183
183
 
auto_editor/help.py CHANGED
@@ -78,6 +78,7 @@ Export Methods:
78
78
  - final-cut-pro ; Export as an XML timeline file for Final Cut Pro
79
79
  - name : "Auto-Editor Media Group"
80
80
  - shotcut ; Export as an XML timeline file for Shotcut
81
+ - kdenlive ; Export as an XML timeline file for kdenlive
81
82
  - v3 ; Export as an auto-editor v3 timeline file
82
83
  - v1 ; Export as an auto-editor v1 timeline file
83
84
  - clip-sequence ; Export as multiple numbered media files
@@ -3,8 +3,6 @@ from __future__ import annotations
3
3
  from dataclasses import dataclass
4
4
  from typing import TYPE_CHECKING
5
5
 
6
- import bv
7
-
8
6
  from auto_editor.analyze import mut_remove_large, mut_remove_small
9
7
  from auto_editor.lib.contracts import *
10
8
  from auto_editor.lib.data_structs import *
@@ -1169,9 +1167,6 @@ def make_standard_env() -> dict[str, Any]:
1169
1167
  "string->vector", lambda s: [Char(c) for c in s], (1, 1), is_str
1170
1168
  ),
1171
1169
  "range->vector": Proc("range->vector", list, (1, 1), is_range),
1172
- # av
1173
- "encoder": Proc("encoder", lambda x: bv.Codec(x, "w"), (1, 1), is_str),
1174
- "decoder": Proc("decoder", lambda x: bv.Codec(x), (1, 1), is_str),
1175
1170
  # reflexion
1176
1171
  "var-exists?": Proc("var-exists?", lambda sym: sym.val in env, (1, 1), is_symbol),
1177
1172
  "rename": Syntax(syn_rename),
@@ -299,9 +299,9 @@ def make_timeline(
299
299
 
300
300
  if len(sources) == 1 and inp is not None:
301
301
  chunks = chunkify(speed_index, speed_hash)
302
- v1_compatiable = v1(inp, chunks)
302
+ v1_compatible = v1(inp, chunks)
303
303
  else:
304
- v1_compatiable = None
304
+ v1_compatible = None
305
305
 
306
306
  if len(vtl) == 0 and len(atl) == 0:
307
307
  log.error("Timeline is empty, nothing to do.")
@@ -312,4 +312,4 @@ def make_timeline(
312
312
  else:
313
313
  template = Template.init(inp, sr, args.audio_layout, res)
314
314
 
315
- return v3(tb, args.background, template, vtl, atl, v1_compatiable)
315
+ return v3(tb, args.background, template, vtl, atl, v1_compatible)
@@ -3,12 +3,12 @@ from __future__ import annotations
3
3
  from fractions import Fraction
4
4
  from io import BytesIO
5
5
  from pathlib import Path
6
- from typing import TYPE_CHECKING
6
+ from typing import TYPE_CHECKING, cast
7
7
 
8
- import bv
8
+ import av
9
9
  import numpy as np
10
- from bv import AudioFrame
11
- from bv.filter.loudnorm import stats
10
+ from av import AudioFrame
11
+ from av.filter.loudnorm import stats
12
12
 
13
13
  from auto_editor.ffwrapper import FileInfo
14
14
  from auto_editor.json import load
@@ -22,7 +22,6 @@ from auto_editor.utils.log import Log
22
22
 
23
23
  if TYPE_CHECKING:
24
24
  from collections.abc import Iterator
25
- from typing import Any
26
25
 
27
26
  from auto_editor.__main__ import Args
28
27
 
@@ -102,7 +101,7 @@ def apply_audio_normalization(
102
101
  f"i={norm['i']}:lra={norm['lra']}:tp={norm['tp']}:offset={norm['gain']}"
103
102
  )
104
103
  log.debug(f"audio norm first pass: {first_pass}")
105
- with bv.open(f"{pre_master}") as container:
104
+ with av.open(f"{pre_master}") as container:
106
105
  stats_ = stats(first_pass, container.streams.audio[0])
107
106
 
108
107
  name, filter_args = parse_ebu_bytes(norm, stats_, log)
@@ -117,7 +116,7 @@ def apply_audio_normalization(
117
116
  return -20.0 * np.log10(max_amplitude)
118
117
  return -99.0
119
118
 
120
- with bv.open(pre_master) as container:
119
+ with av.open(pre_master) as container:
121
120
  max_peak_level = -99.0
122
121
  assert len(container.streams.video) == 0
123
122
  for frame in container.decode(audio=0):
@@ -129,13 +128,13 @@ def apply_audio_normalization(
129
128
  log.print(f"peak adjustment: {adjustment:.3f}dB")
130
129
  name, filter_args = "volume", f"{adjustment}"
131
130
 
132
- with bv.open(pre_master) as container:
131
+ with av.open(pre_master) as container:
133
132
  input_stream = container.streams.audio[0]
134
133
 
135
- output_file = bv.open(path, mode="w")
134
+ output_file = av.open(path, mode="w")
136
135
  output_stream = output_file.add_stream("pcm_s16le", rate=input_stream.rate)
137
136
 
138
- graph = bv.filter.Graph()
137
+ graph = av.filter.Graph()
139
138
  graph.link_nodes(
140
139
  graph.add_abuffer(template=input_stream),
141
140
  graph.add(name, filter_args),
@@ -148,7 +147,7 @@ def apply_audio_normalization(
148
147
  aframe = graph.pull()
149
148
  assert isinstance(aframe, AudioFrame)
150
149
  output_file.mux(output_stream.encode(aframe))
151
- except (bv.BlockingIOError, bv.EOFError):
150
+ except (av.BlockingIOError, av.EOFError):
152
151
  break
153
152
 
154
153
  output_file.mux(output_stream.encode(None))
@@ -156,10 +155,10 @@ def apply_audio_normalization(
156
155
 
157
156
 
158
157
  def process_audio_clip(clip: Clip, data: np.ndarray, sr: int, log: Log) -> np.ndarray:
159
- to_s16 = bv.AudioResampler(format="s16", layout="stereo", rate=sr)
158
+ to_s16 = av.AudioResampler(format="s16", layout="stereo", rate=sr)
160
159
  input_buffer = BytesIO()
161
160
 
162
- with bv.open(input_buffer, "w", format="wav") as container:
161
+ with av.open(input_buffer, "w", format="wav") as container:
163
162
  output_stream = container.add_stream(
164
163
  "pcm_s16le", sample_rate=sr, format="s16", layout="stereo"
165
164
  )
@@ -173,10 +172,10 @@ def process_audio_clip(clip: Clip, data: np.ndarray, sr: int, log: Log) -> np.nd
173
172
 
174
173
  input_buffer.seek(0)
175
174
 
176
- input_file = bv.open(input_buffer, "r")
175
+ input_file = av.open(input_buffer, "r")
177
176
  input_stream = input_file.streams.audio[0]
178
177
 
179
- graph = bv.filter.Graph()
178
+ graph = av.filter.Graph()
180
179
  args = [graph.add_abuffer(template=input_stream)]
181
180
 
182
181
  if clip.speed != 1:
@@ -202,7 +201,7 @@ def process_audio_clip(clip: Clip, data: np.ndarray, sr: int, log: Log) -> np.nd
202
201
  graph.link_nodes(*args).configure()
203
202
 
204
203
  all_frames = []
205
- resampler = bv.AudioResampler(format="s16p", layout="stereo", rate=sr)
204
+ resampler = av.AudioResampler(format="s16p", layout="stereo", rate=sr)
206
205
 
207
206
  for frame in input_file.decode(input_stream):
208
207
  graph.push(frame)
@@ -214,7 +213,7 @@ def process_audio_clip(clip: Clip, data: np.ndarray, sr: int, log: Log) -> np.nd
214
213
  for resampled_frame in resampler.resample(aframe):
215
214
  all_frames.append(resampled_frame.to_ndarray())
216
215
 
217
- except (bv.BlockingIOError, bv.EOFError):
216
+ except (av.BlockingIOError, av.EOFError):
218
217
  break
219
218
 
220
219
  if not all_frames:
@@ -229,7 +228,7 @@ def mix_audio_files(sr: int, audio_paths: list[str], output_path: str) -> None:
229
228
 
230
229
  # First pass: determine the maximum length
231
230
  for path in audio_paths:
232
- container = bv.open(path)
231
+ container = av.open(path)
233
232
  stream = container.streams.audio[0]
234
233
 
235
234
  # Calculate duration in samples
@@ -241,14 +240,11 @@ def mix_audio_files(sr: int, audio_paths: list[str], output_path: str) -> None:
241
240
 
242
241
  # Second pass: read and mix audio
243
242
  for path in audio_paths:
244
- container = bv.open(path)
243
+ container = av.open(path)
245
244
  stream = container.streams.audio[0]
246
245
 
247
- resampler = bv.audio.resampler.AudioResampler(
248
- format="s16", layout="mono", rate=sr
249
- )
250
-
251
246
  audio_array: list[np.ndarray] = []
247
+ resampler = av.AudioResampler(format="s16", layout="mono", rate=sr)
252
248
  for frame in container.decode(audio=0):
253
249
  frame.pts = None
254
250
  resampled = resampler.resample(frame)[0]
@@ -277,7 +273,7 @@ def mix_audio_files(sr: int, audio_paths: list[str], output_path: str) -> None:
277
273
  mixed_audio = mixed_audio * (32767 / max_val)
278
274
  mixed_audio = mixed_audio.astype(np.int16)
279
275
 
280
- output_container = bv.open(output_path, mode="w")
276
+ output_container = av.open(output_path, mode="w")
281
277
  output_stream = output_container.add_stream("pcm_s16le", rate=sr)
282
278
 
283
279
  chunk_size = sr # Process 1 second at a time
@@ -298,9 +294,9 @@ def mix_audio_files(sr: int, audio_paths: list[str], output_path: str) -> None:
298
294
  def file_to_ndarray(src: FileInfo, stream: int, sr: int) -> np.ndarray:
299
295
  all_frames = []
300
296
 
301
- resampler = bv.AudioResampler(format="s16p", layout="stereo", rate=sr)
297
+ resampler = av.AudioResampler(format="s16p", layout="stereo", rate=sr)
302
298
 
303
- with bv.open(src.path) as container:
299
+ with av.open(src.path) as container:
304
300
  for frame in container.decode(audio=stream):
305
301
  for resampled_frame in resampler.resample(frame):
306
302
  all_frames.append(resampled_frame.to_ndarray())
@@ -311,10 +307,10 @@ def file_to_ndarray(src: FileInfo, stream: int, sr: int) -> np.ndarray:
311
307
  def ndarray_to_file(audio_data: np.ndarray, rate: int, out: str | Path) -> None:
312
308
  layout = "stereo"
313
309
 
314
- with bv.open(out, mode="w") as output:
310
+ with av.open(out, mode="w") as output:
315
311
  stream = output.add_stream("pcm_s16le", rate=rate, format="s16", layout=layout)
316
312
 
317
- frame = bv.AudioFrame.from_ndarray(audio_data, format="s16p", layout=layout)
313
+ frame = AudioFrame.from_ndarray(audio_data, format="s16p", layout=layout)
318
314
  frame.rate = rate
319
315
 
320
316
  output.mux(stream.encode(frame))
@@ -322,11 +318,11 @@ def ndarray_to_file(audio_data: np.ndarray, rate: int, out: str | Path) -> None:
322
318
 
323
319
 
324
320
  def ndarray_to_iter(
325
- audio_data: np.ndarray, fmt: bv.AudioFormat, layout: str, rate: int
321
+ audio_data: np.ndarray, fmt: av.AudioFormat, layout: str, rate: int
326
322
  ) -> Iterator[AudioFrame]:
327
323
  chunk_size = rate // 4 # Process 0.25 seconds at a time
328
324
 
329
- resampler = bv.AudioResampler(rate=rate, format=fmt, layout=layout)
325
+ resampler = av.AudioResampler(rate=rate, format=fmt, layout=layout)
330
326
  for i in range(0, audio_data.shape[1], chunk_size):
331
327
  chunk = audio_data[:, i : i + chunk_size]
332
328
 
@@ -338,15 +334,15 @@ def ndarray_to_iter(
338
334
 
339
335
 
340
336
  def make_new_audio(
341
- output: bv.container.OutputContainer,
342
- audio_format: bv.AudioFormat,
337
+ output: av.container.OutputContainer,
338
+ audio_format: av.AudioFormat,
343
339
  tl: v3,
344
340
  args: Args,
345
341
  log: Log,
346
- ) -> tuple[list[bv.AudioStream], list[Iterator[AudioFrame]]]:
342
+ ) -> tuple[list[av.AudioStream], list[Iterator[AudioFrame]]]:
347
343
  audio_inputs = []
348
344
  audio_gen_frames = []
349
- audio_streams: list[bv.AudioStream] = []
345
+ audio_streams: list[av.AudioStream] = []
350
346
  audio_paths = _make_new_audio(tl, audio_format, args, log)
351
347
 
352
348
  for i, audio_path in enumerate(audio_paths):
@@ -357,7 +353,7 @@ def make_new_audio(
357
353
  layout=tl.T.layout,
358
354
  time_base=Fraction(1, tl.sr),
359
355
  )
360
- if not isinstance(audio_stream, bv.AudioStream):
356
+ if not isinstance(audio_stream, av.AudioStream):
361
357
  log.error(f"Not a known audio codec: {args.audio_codec}")
362
358
 
363
359
  if args.audio_bitrate != "auto":
@@ -372,7 +368,7 @@ def make_new_audio(
372
368
  audio_streams.append(audio_stream)
373
369
 
374
370
  if isinstance(audio_path, str):
375
- audio_input = bv.open(audio_path)
371
+ audio_input = av.open(audio_path)
376
372
  audio_inputs.append(audio_input)
377
373
  audio_gen_frames.append(audio_input.decode(audio=0))
378
374
  else:
@@ -385,7 +381,7 @@ class Getter:
385
381
  __slots__ = ("container", "stream", "rate")
386
382
 
387
383
  def __init__(self, path: Path, stream: int, rate: int):
388
- self.container = bv.open(path)
384
+ self.container = av.open(path)
389
385
  self.stream = self.container.streams.audio[stream]
390
386
  self.rate = rate
391
387
 
@@ -394,7 +390,7 @@ class Getter:
394
390
 
395
391
  container = self.container
396
392
  stream = self.stream
397
- resampler = bv.AudioResampler(format="s16p", layout="stereo", rate=self.rate)
393
+ resampler = av.AudioResampler(format="s16p", layout="stereo", rate=self.rate)
398
394
 
399
395
  time_base = stream.time_base
400
396
  assert time_base is not None
@@ -436,10 +432,12 @@ class Getter:
436
432
  return result # Return NumPy array with shape (channels, samples)
437
433
 
438
434
 
439
- def _make_new_audio(tl: v3, fmt: bv.AudioFormat, args: Args, log: Log) -> list[Any]:
435
+ def _make_new_audio(
436
+ tl: v3, fmt: av.AudioFormat, args: Args, log: Log
437
+ ) -> list[str | Iterator[AudioFrame]]:
440
438
  sr = tl.sr
441
439
  tb = tl.tb
442
- output: list[Any] = []
440
+ output: list[str | Iterator[AudioFrame]] = []
443
441
  samples: dict[tuple[FileInfo, int], Getter] = {}
444
442
 
445
443
  norm = parse_norm(args.audio_normalize, log)
@@ -449,7 +447,7 @@ def _make_new_audio(tl: v3, fmt: bv.AudioFormat, args: Args, log: Log) -> list[A
449
447
 
450
448
  layout = tl.T.layout
451
449
  try:
452
- bv.AudioLayout(layout)
450
+ av.AudioLayout(layout)
453
451
  except ValueError:
454
452
  log.error(f"Invalid audio layout: {layout}")
455
453
 
@@ -511,7 +509,9 @@ def _make_new_audio(tl: v3, fmt: bv.AudioFormat, args: Args, log: Log) -> list[A
511
509
 
512
510
  if args.mix_audio_streams and len(output) > 1:
513
511
  new_a_file = f"{Path(log.temp, 'new_audio.wav')}"
514
- mix_audio_files(sr, output, new_a_file)
512
+ # When mix_audio_streams is True, output only contains strings
513
+ audio_paths = cast(list[str], output)
514
+ mix_audio_files(sr, audio_paths, new_a_file)
515
515
  return [new_a_file]
516
516
 
517
517
  return output