auto-editor 24.29.1__tar.gz → 24.30.1__tar.gz

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.
Files changed (63) hide show
  1. {auto_editor-24.29.1 → auto_editor-24.30.1}/PKG-INFO +2 -2
  2. auto_editor-24.30.1/auto_editor/__init__.py +2 -0
  3. {auto_editor-24.29.1 → auto_editor-24.30.1}/auto_editor/analyze.py +143 -132
  4. {auto_editor-24.29.1 → auto_editor-24.30.1}/auto_editor/edit.py +14 -21
  5. {auto_editor-24.29.1 → auto_editor-24.30.1}/auto_editor/make_layers.py +3 -6
  6. {auto_editor-24.29.1 → auto_editor-24.30.1}/auto_editor/output.py +4 -4
  7. {auto_editor-24.29.1 → auto_editor-24.30.1}/auto_editor/preview.py +2 -3
  8. {auto_editor-24.29.1 → auto_editor-24.30.1}/auto_editor/render/subtitle.py +1 -1
  9. {auto_editor-24.29.1 → auto_editor-24.30.1}/auto_editor/render/video.py +8 -18
  10. {auto_editor-24.29.1 → auto_editor-24.30.1}/auto_editor/subcommands/levels.py +18 -17
  11. {auto_editor-24.29.1 → auto_editor-24.30.1}/auto_editor/subcommands/repl.py +3 -12
  12. auto_editor-24.30.1/auto_editor/subcommands/subdump.py +20 -0
  13. auto_editor-24.30.1/auto_editor/utils/container.py +96 -0
  14. {auto_editor-24.29.1 → auto_editor-24.30.1}/auto_editor.egg-info/PKG-INFO +2 -2
  15. {auto_editor-24.29.1 → auto_editor-24.30.1}/auto_editor.egg-info/requires.txt +1 -1
  16. {auto_editor-24.29.1 → auto_editor-24.30.1}/pyproject.toml +1 -1
  17. auto_editor-24.29.1/auto_editor/__init__.py +0 -2
  18. auto_editor-24.29.1/auto_editor/subcommands/subdump.py +0 -23
  19. auto_editor-24.29.1/auto_editor/utils/container.py +0 -338
  20. {auto_editor-24.29.1 → auto_editor-24.30.1}/LICENSE +0 -0
  21. {auto_editor-24.29.1 → auto_editor-24.30.1}/README.md +0 -0
  22. {auto_editor-24.29.1 → auto_editor-24.30.1}/auto_editor/__main__.py +0 -0
  23. {auto_editor-24.29.1 → auto_editor-24.30.1}/auto_editor/ffwrapper.py +0 -0
  24. {auto_editor-24.29.1 → auto_editor-24.30.1}/auto_editor/formats/__init__.py +0 -0
  25. {auto_editor-24.29.1 → auto_editor-24.30.1}/auto_editor/formats/fcp11.py +0 -0
  26. {auto_editor-24.29.1 → auto_editor-24.30.1}/auto_editor/formats/fcp7.py +0 -0
  27. {auto_editor-24.29.1 → auto_editor-24.30.1}/auto_editor/formats/json.py +0 -0
  28. {auto_editor-24.29.1 → auto_editor-24.30.1}/auto_editor/formats/shotcut.py +0 -0
  29. {auto_editor-24.29.1 → auto_editor-24.30.1}/auto_editor/formats/utils.py +0 -0
  30. {auto_editor-24.29.1 → auto_editor-24.30.1}/auto_editor/help.py +0 -0
  31. {auto_editor-24.29.1 → auto_editor-24.30.1}/auto_editor/lang/__init__.py +0 -0
  32. {auto_editor-24.29.1 → auto_editor-24.30.1}/auto_editor/lang/json.py +0 -0
  33. {auto_editor-24.29.1 → auto_editor-24.30.1}/auto_editor/lang/libmath.py +0 -0
  34. {auto_editor-24.29.1 → auto_editor-24.30.1}/auto_editor/lang/palet.py +0 -0
  35. {auto_editor-24.29.1 → auto_editor-24.30.1}/auto_editor/lib/__init__.py +0 -0
  36. {auto_editor-24.29.1 → auto_editor-24.30.1}/auto_editor/lib/contracts.py +0 -0
  37. {auto_editor-24.29.1 → auto_editor-24.30.1}/auto_editor/lib/data_structs.py +0 -0
  38. {auto_editor-24.29.1 → auto_editor-24.30.1}/auto_editor/lib/err.py +0 -0
  39. {auto_editor-24.29.1 → auto_editor-24.30.1}/auto_editor/render/__init__.py +0 -0
  40. {auto_editor-24.29.1 → auto_editor-24.30.1}/auto_editor/render/audio.py +0 -0
  41. {auto_editor-24.29.1 → auto_editor-24.30.1}/auto_editor/subcommands/__init__.py +0 -0
  42. {auto_editor-24.29.1 → auto_editor-24.30.1}/auto_editor/subcommands/desc.py +0 -0
  43. {auto_editor-24.29.1 → auto_editor-24.30.1}/auto_editor/subcommands/info.py +0 -0
  44. {auto_editor-24.29.1 → auto_editor-24.30.1}/auto_editor/subcommands/palet.py +0 -0
  45. {auto_editor-24.29.1 → auto_editor-24.30.1}/auto_editor/subcommands/test.py +0 -0
  46. {auto_editor-24.29.1 → auto_editor-24.30.1}/auto_editor/timeline.py +0 -0
  47. {auto_editor-24.29.1 → auto_editor-24.30.1}/auto_editor/utils/__init__.py +0 -0
  48. {auto_editor-24.29.1 → auto_editor-24.30.1}/auto_editor/utils/bar.py +0 -0
  49. {auto_editor-24.29.1 → auto_editor-24.30.1}/auto_editor/utils/chunks.py +0 -0
  50. {auto_editor-24.29.1 → auto_editor-24.30.1}/auto_editor/utils/cmdkw.py +0 -0
  51. {auto_editor-24.29.1 → auto_editor-24.30.1}/auto_editor/utils/encoder.py +0 -0
  52. {auto_editor-24.29.1 → auto_editor-24.30.1}/auto_editor/utils/func.py +0 -0
  53. {auto_editor-24.29.1 → auto_editor-24.30.1}/auto_editor/utils/log.py +0 -0
  54. {auto_editor-24.29.1 → auto_editor-24.30.1}/auto_editor/utils/subtitle_tools.py +0 -0
  55. {auto_editor-24.29.1 → auto_editor-24.30.1}/auto_editor/utils/types.py +0 -0
  56. {auto_editor-24.29.1 → auto_editor-24.30.1}/auto_editor/validate_input.py +0 -0
  57. {auto_editor-24.29.1 → auto_editor-24.30.1}/auto_editor/vanparse.py +0 -0
  58. {auto_editor-24.29.1 → auto_editor-24.30.1}/auto_editor/wavfile.py +0 -0
  59. {auto_editor-24.29.1 → auto_editor-24.30.1}/auto_editor.egg-info/SOURCES.txt +0 -0
  60. {auto_editor-24.29.1 → auto_editor-24.30.1}/auto_editor.egg-info/dependency_links.txt +0 -0
  61. {auto_editor-24.29.1 → auto_editor-24.30.1}/auto_editor.egg-info/entry_points.txt +0 -0
  62. {auto_editor-24.29.1 → auto_editor-24.30.1}/auto_editor.egg-info/top_level.txt +0 -0
  63. {auto_editor-24.29.1 → auto_editor-24.30.1}/setup.cfg +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: auto-editor
3
- Version: 24.29.1
3
+ Version: 24.30.1
4
4
  Summary: Auto-Editor: Effort free video editing!
5
5
  Author-email: WyattBlue <wyattblue@auto-editor.com>
6
6
  License: Unlicense
@@ -12,7 +12,7 @@ Requires-Python: >=3.10
12
12
  Description-Content-Type: text/markdown
13
13
  License-File: LICENSE
14
14
  Requires-Dist: numpy>=1.23.0
15
- Requires-Dist: pyav==12.2.*
15
+ Requires-Dist: pyav==12.3.*
16
16
  Requires-Dist: ae-ffmpeg==1.2.*
17
17
 
18
18
  <p align="center"><img src="https://auto-editor.com/img/auto-editor-banner.webp" title="Auto-Editor" width="700"></p>
@@ -0,0 +1,2 @@
1
+ __version__ = "24.30.1"
2
+ version = "24w30a"
@@ -4,23 +4,25 @@ import os
4
4
  import re
5
5
  from dataclasses import dataclass
6
6
  from fractions import Fraction
7
+ from math import ceil
7
8
  from typing import TYPE_CHECKING
8
9
 
10
+ import av
9
11
  import numpy as np
12
+ from av.audio.fifo import AudioFifo
13
+ from av.subtitles.subtitle import AssSubtitle
10
14
 
11
15
  from auto_editor import version
12
16
  from auto_editor.utils.subtitle_tools import convert_ass_to_text
13
- from auto_editor.wavfile import read
14
17
 
15
18
  if TYPE_CHECKING:
19
+ from collections.abc import Iterator
16
20
  from fractions import Fraction
17
21
  from typing import Any
18
22
 
19
- from av.filter import FilterContext
20
23
  from numpy.typing import NDArray
21
24
 
22
25
  from auto_editor.ffwrapper import FileInfo
23
- from auto_editor.output import Ensure
24
26
  from auto_editor.utils.bar import Bar
25
27
  from auto_editor.utils.log import Log
26
28
 
@@ -28,7 +30,6 @@ if TYPE_CHECKING:
28
30
  @dataclass(slots=True)
29
31
  class FileSetup:
30
32
  src: FileInfo
31
- ensure: Ensure
32
33
  strict: bool
33
34
  tb: Fraction
34
35
  bar: Bar
@@ -40,11 +41,6 @@ class LevelError(Exception):
40
41
  pass
41
42
 
42
43
 
43
- def link_nodes(*nodes: FilterContext) -> None:
44
- for c, n in zip(nodes, nodes[1:]):
45
- c.link_to(n)
46
-
47
-
48
44
  def mut_remove_small(
49
45
  arr: NDArray[np.bool_], lim: int, replace: int, with_: int
50
46
  ) -> None:
@@ -92,9 +88,90 @@ def obj_tag(tag: str, tb: Fraction, obj: dict[str, Any]) -> str:
92
88
  return key
93
89
 
94
90
 
91
+ def iter_audio(src, tb: Fraction, stream: int = 0) -> Iterator[float]:
92
+ fifo = AudioFifo()
93
+ try:
94
+ container = av.open(src.path, "r")
95
+ audio_stream = container.streams.audio[stream]
96
+ sample_rate = audio_stream.rate
97
+
98
+ exact_size = (1 / tb) * sample_rate
99
+ accumulated_error = 0
100
+
101
+ # Resample so that audio data is between [-1, 1]
102
+ resampler = av.AudioResampler(
103
+ av.AudioFormat("flt"), audio_stream.layout, sample_rate
104
+ )
105
+
106
+ for frame in container.decode(audio=stream):
107
+ frame.pts = None # Skip time checks
108
+
109
+ for reframe in resampler.resample(frame):
110
+ fifo.write(reframe)
111
+
112
+ while fifo.samples >= ceil(exact_size):
113
+ size_with_error = exact_size + accumulated_error
114
+ current_size = round(size_with_error)
115
+ accumulated_error = size_with_error - current_size
116
+
117
+ audio_chunk = fifo.read(current_size)
118
+ assert audio_chunk is not None
119
+ arr = audio_chunk.to_ndarray().flatten()
120
+ yield float(np.max(np.abs(arr)))
121
+
122
+ finally:
123
+ container.close()
124
+
125
+
126
+ def iter_motion(src, tb, stream: int, blur: int, width: int) -> Iterator[float]:
127
+ container = av.open(src.path, "r")
128
+
129
+ video = container.streams.video[stream]
130
+ video.thread_type = "AUTO"
131
+
132
+ prev_frame = None
133
+ current_frame = None
134
+ total_pixels = src.videos[0].width * src.videos[0].height
135
+ index = 0
136
+ prev_index = -1
137
+
138
+ graph = av.filter.Graph()
139
+ graph.link_nodes(
140
+ graph.add_buffer(template=video),
141
+ graph.add("scale", f"{width}:-1"),
142
+ graph.add("format", "gray"),
143
+ graph.add("gblur", f"sigma={blur}"),
144
+ graph.add("buffersink"),
145
+ ).configure()
146
+
147
+ for unframe in container.decode(video):
148
+ if unframe.pts is None:
149
+ continue
150
+
151
+ graph.push(unframe)
152
+ frame = graph.pull()
153
+ assert frame.time is not None
154
+ index = round(frame.time * tb)
155
+
156
+ current_frame = frame.to_ndarray()
157
+ if prev_frame is None:
158
+ value = 0.0
159
+ else:
160
+ # Use `int16` to avoid underflow with `uint8` datatype
161
+ diff = np.abs(prev_frame.astype(np.int16) - current_frame.astype(np.int16))
162
+ value = np.count_nonzero(diff) / total_pixels
163
+
164
+ for _ in range(index - prev_index):
165
+ yield value
166
+
167
+ prev_frame = current_frame
168
+ prev_index = index
169
+
170
+ container.close()
171
+
172
+
95
173
  @dataclass(slots=True)
96
174
  class Levels:
97
- ensure: Ensure
98
175
  src: FileInfo
99
176
  tb: Fraction
100
177
  bar: Bar
@@ -107,26 +184,16 @@ class Levels:
107
184
  if (arr := self.read_cache("audio", {"stream": 0})) is not None:
108
185
  return len(arr)
109
186
 
110
- sr, samples = read(self.ensure.audio(self.src, 0))
111
- samp_count = len(samples)
112
- del samples
113
-
114
- samp_per_ticks = sr / self.tb
115
- ticks = int(samp_count / samp_per_ticks)
116
- self.log.debug(f"Audio Length: {ticks}")
117
- self.log.debug(
118
- f"... without rounding: {float(samp_count / samp_per_ticks)}"
119
- )
120
- return ticks
187
+ result = sum(1 for _ in iter_audio(self.src, self.tb, 0))
188
+ self.log.debug(f"Audio Length: {result}")
189
+ return result
121
190
 
122
191
  # If there's no audio, get length in video metadata.
123
- import av
124
-
125
- with av.open(f"{self.src.path}") as cn:
126
- if len(cn.streams.video) < 1:
192
+ with av.open(self.src.path) as container:
193
+ if len(container.streams.video) == 0:
127
194
  self.log.error("Could not get media duration")
128
195
 
129
- video = cn.streams.video[0]
196
+ video = container.streams.video[0]
130
197
 
131
198
  if video.duration is None or video.time_base is None:
132
199
  dur = 0
@@ -171,56 +238,70 @@ class Levels:
171
238
  return arr
172
239
 
173
240
  def audio(self, stream: int) -> NDArray[np.float64]:
174
- if stream > len(self.src.audios) - 1:
241
+ if stream >= len(self.src.audios):
175
242
  raise LevelError(f"audio: audio stream '{stream}' does not exist.")
176
243
 
177
244
  if (arr := self.read_cache("audio", {"stream": stream})) is not None:
178
245
  return arr
179
246
 
180
- sr, samples = read(self.ensure.audio(self.src, stream))
181
-
182
- if len(samples) == 0:
183
- raise LevelError(f"audio: stream '{stream}' has no samples.")
184
-
185
- def get_max_volume(s: np.ndarray) -> float:
186
- return max(float(np.max(s)), -float(np.min(s)))
187
-
188
- max_volume = get_max_volume(samples)
189
- self.log.debug(f"Max volume: {max_volume}")
247
+ with av.open(self.src.path, "r") as container:
248
+ audio = container.streams.audio[stream]
249
+ if audio.duration is not None and audio.time_base is not None:
250
+ inaccurate_dur = int(audio.duration * audio.time_base * self.tb)
251
+ elif container.duration is not None:
252
+ inaccurate_dur = int(container.duration / av.time_base * self.tb)
253
+ else:
254
+ inaccurate_dur = 1024
190
255
 
191
- samp_count = samples.shape[0]
192
- samp_per_ticks = sr / self.tb
256
+ bar = self.bar
257
+ bar.start(inaccurate_dur, "Analyzing audio volume")
193
258
 
194
- if samp_per_ticks < 1:
195
- self.log.error(
196
- f"audio: stream '{stream}'\n Samplerate ({sr}) must be greater than "
197
- f"or equal to timebase ({self.tb})\n"
198
- " Try `-fps 30` and/or `--sample-rate 48000`"
199
- )
259
+ result = np.zeros((inaccurate_dur), dtype=np.float64)
260
+ index = 0
261
+ for value in iter_audio(self.src, self.tb, stream):
262
+ if index > len(result) - 1:
263
+ result = np.concatenate(
264
+ (result, np.zeros((len(result)), dtype=np.float64))
265
+ )
266
+ result[index] = value
267
+ bar.tick(index)
268
+ index += 1
200
269
 
201
- audio_ticks = int(samp_count / samp_per_ticks)
202
- self.log.debug(
203
- f"analyze: audio length: {audio_ticks} ({float(samp_count / samp_per_ticks)})"
204
- )
205
- self.bar.start(audio_ticks, "Analyzing audio volume")
270
+ bar.end()
271
+ return self.cache("audio", {"stream": stream}, result[:index])
206
272
 
207
- threshold_list = np.zeros((audio_ticks), dtype=np.float64)
273
+ def motion(self, stream: int, blur: int, width: int) -> NDArray[np.float64]:
274
+ if stream >= len(self.src.videos):
275
+ raise LevelError(f"motion: video stream '{stream}' does not exist.")
208
276
 
209
- if max_volume == 0: # Prevent dividing by zero
210
- return threshold_list
277
+ mobj = {"stream": stream, "width": width, "blur": blur}
278
+ if (arr := self.read_cache("motion", mobj)) is not None:
279
+ return arr
211
280
 
212
- # Determine when audio is silent or loud.
213
- for i in range(audio_ticks):
214
- if i % 500 == 0:
215
- self.bar.tick(i)
281
+ with av.open(self.src.path, "r") as container:
282
+ video = container.streams.video[stream]
283
+ inaccurate_dur = (
284
+ 1024
285
+ if video.duration is None or video.time_base is None
286
+ else int(video.duration * video.time_base * self.tb)
287
+ )
216
288
 
217
- start = int(i * samp_per_ticks)
218
- end = min(int((i + 1) * samp_per_ticks), samp_count)
289
+ bar = self.bar
290
+ bar.start(inaccurate_dur, "Analyzing motion")
219
291
 
220
- threshold_list[i] = get_max_volume(samples[start:end]) / max_volume
292
+ result = np.zeros((inaccurate_dur), dtype=np.float64)
293
+ index = 0
294
+ for value in iter_motion(self.src, self.tb, stream, blur, width):
295
+ if index > len(result) - 1:
296
+ result = np.concatenate(
297
+ (result, np.zeros((len(result)), dtype=np.float64))
298
+ )
299
+ result[index] = value
300
+ bar.tick(index)
301
+ index += 1
221
302
 
222
- self.bar.end()
223
- return self.cache("audio", {"stream": stream}, threshold_list)
303
+ bar.end()
304
+ return self.cache("motion", mobj, result[:index])
224
305
 
225
306
  def subtitle(
226
307
  self,
@@ -238,9 +319,6 @@ class Levels:
238
319
  except re.error as e:
239
320
  self.log.error(e)
240
321
 
241
- import av
242
- from av.subtitles.subtitle import AssSubtitle, TextSubtitle
243
-
244
322
  try:
245
323
  container = av.open(self.src.path, "r")
246
324
  subtitle_stream = container.streams.subtitles[stream]
@@ -287,8 +365,6 @@ class Levels:
287
365
  for sub in subset:
288
366
  if isinstance(sub, AssSubtitle):
289
367
  line = convert_ass_to_text(sub.ass.decode(errors="ignore"))
290
- elif isinstance(sub, TextSubtitle):
291
- line = sub.text.decode(errors="ignore")
292
368
  else:
293
369
  continue
294
370
 
@@ -299,68 +375,3 @@ class Levels:
299
375
  container.close()
300
376
 
301
377
  return result
302
-
303
- def motion(self, stream: int, blur: int, width: int) -> NDArray[np.float64]:
304
- import av
305
-
306
- if stream >= len(self.src.videos):
307
- raise LevelError(f"motion: video stream '{stream}' does not exist.")
308
-
309
- mobj = {"stream": stream, "width": width, "blur": blur}
310
- if (arr := self.read_cache("motion", mobj)) is not None:
311
- return arr
312
-
313
- container = av.open(f"{self.src.path}", "r")
314
-
315
- video = container.streams.video[stream]
316
- video.thread_type = "AUTO"
317
-
318
- inaccurate_dur = 1 if video.duration is None else video.duration
319
- self.bar.start(inaccurate_dur, "Analyzing motion")
320
-
321
- prev_frame = None
322
- current_frame = None
323
- total_pixels = self.src.videos[0].width * self.src.videos[0].height
324
- index = 0
325
-
326
- graph = av.filter.Graph()
327
- link_nodes(
328
- graph.add_buffer(template=video),
329
- graph.add("scale", f"{width}:-1"),
330
- graph.add("format", "gray"),
331
- graph.add("gblur", f"sigma={blur}"),
332
- graph.add("buffersink"),
333
- )
334
- graph.configure()
335
-
336
- threshold_list = np.zeros((1024), dtype=np.float64)
337
-
338
- for unframe in container.decode(video):
339
- graph.push(unframe)
340
- frame = graph.pull()
341
-
342
- # Showing progress ...
343
- assert frame.time is not None
344
- index = int(frame.time * self.tb)
345
- if frame.pts is not None:
346
- self.bar.tick(frame.pts)
347
-
348
- current_frame = frame.to_ndarray()
349
-
350
- if index > len(threshold_list) - 1:
351
- threshold_list = np.concatenate(
352
- (threshold_list, np.zeros((len(threshold_list)), dtype=np.float64)),
353
- axis=0,
354
- )
355
-
356
- if prev_frame is not None:
357
- # Use `int16` to avoid underflow with `uint8` datatype
358
- diff = np.abs(
359
- prev_frame.astype(np.int16) - current_frame.astype(np.int16)
360
- )
361
- threshold_list[index] = np.count_nonzero(diff) / total_pixels
362
-
363
- prev_frame = current_frame
364
-
365
- self.bar.end()
366
- return self.cache("motion", mobj, threshold_list[:index])
@@ -68,12 +68,8 @@ def set_video_codec(
68
68
  ) -> str:
69
69
  if codec == "auto":
70
70
  codec = "h264" if (src is None or not src.videos) else src.videos[0].codec
71
- if ctr.vcodecs is not None:
72
- if ctr.vstrict and codec not in ctr.vcodecs:
73
- return ctr.vcodecs[0]
74
-
75
- if codec in ctr.disallow_v:
76
- return ctr.vcodecs[0]
71
+ if codec not in ctr.vcodecs and ctr.default_vid != "none":
72
+ return ctr.default_vid
77
73
  return codec
78
74
 
79
75
  if codec == "copy":
@@ -83,12 +79,7 @@ def set_video_codec(
83
79
  log.error("Input file does not have a video stream to copy codec from.")
84
80
  codec = src.videos[0].codec
85
81
 
86
- if ctr.vstrict:
87
- assert ctr.vcodecs is not None
88
- if codec not in ctr.vcodecs:
89
- log.error(codec_error.format(codec, out_ext))
90
-
91
- if codec in ctr.disallow_v:
82
+ if ctr.vcodecs is not None and codec not in ctr.vcodecs:
92
83
  log.error(codec_error.format(codec, out_ext))
93
84
 
94
85
  return codec
@@ -99,8 +90,10 @@ def set_audio_codec(
99
90
  ) -> str:
100
91
  if codec == "auto":
101
92
  codec = "aac" if (src is None or not src.audios) else src.audios[0].codec
102
- if ctr.acodecs is not None and codec not in ctr.acodecs:
103
- return ctr.acodecs[0]
93
+ if codec not in ctr.acodecs and ctr.default_aud != "none":
94
+ return ctr.default_aud
95
+ if codec == "mp3float":
96
+ return "mp3"
104
97
  return codec
105
98
 
106
99
  if codec == "copy":
@@ -209,10 +202,8 @@ def edit_media(
209
202
  else:
210
203
  samplerate = args.sample_rate
211
204
 
212
- ensure = Ensure(ffmpeg, bar, samplerate, temp, log)
213
-
214
205
  if tl is None:
215
- tl = make_timeline(sources, ensure, args, samplerate, bar, temp, log)
206
+ tl = make_timeline(sources, args, samplerate, bar, temp, log)
216
207
 
217
208
  if export["export"] == "timeline":
218
209
  from auto_editor.formats.json import make_json_timeline
@@ -223,7 +214,7 @@ def edit_media(
223
214
  if args.preview:
224
215
  from auto_editor.preview import preview
225
216
 
226
- preview(ensure, tl, temp, log)
217
+ preview(tl, temp, log)
227
218
  return
228
219
 
229
220
  if export["export"] == "json":
@@ -272,13 +263,15 @@ def edit_media(
272
263
  sub_output = []
273
264
  apply_later = False
274
265
 
275
- if ctr.allow_subtitle and not args.sn:
266
+ ensure = Ensure(ffmpeg, bar, samplerate, temp, log)
267
+
268
+ if ctr.default_sub != "none" and not args.sn:
276
269
  sub_output = make_new_subtitles(tl, ensure, temp)
277
270
 
278
- if ctr.allow_audio:
271
+ if ctr.default_aud != "none":
279
272
  audio_output = make_new_audio(tl, ensure, args, ffmpeg, bar, temp, log)
280
273
 
281
- if ctr.allow_video:
274
+ if ctr.default_vid != "none":
282
275
  if tl.v:
283
276
  out_path, apply_later = render_av(ffmpeg, tl, args, bar, ctr, temp, log)
284
277
  visual_output.append((True, out_path))
@@ -18,7 +18,6 @@ from auto_editor.utils.types import Args, CoerceError, time
18
18
  if TYPE_CHECKING:
19
19
  from numpy.typing import NDArray
20
20
 
21
- from auto_editor.output import Ensure
22
21
  from auto_editor.utils.bar import Bar
23
22
  from auto_editor.utils.chunks import Chunks
24
23
  from auto_editor.utils.log import Log
@@ -75,7 +74,6 @@ def make_av(src: FileInfo, all_clips: list[list[Clip]]) -> tuple[VSpace, ASpace]
75
74
  def run_interpreter_for_edit_option(
76
75
  text: str, filesetup: FileSetup
77
76
  ) -> NDArray[np.bool_]:
78
- ensure = filesetup.ensure
79
77
  src = filesetup.src
80
78
  tb = filesetup.tb
81
79
  bar = filesetup.bar
@@ -87,8 +85,8 @@ def run_interpreter_for_edit_option(
87
85
  if log.is_debug:
88
86
  log.debug(f"edit: {parser}")
89
87
 
90
- env["timebase"] = filesetup.tb
91
- env["@levels"] = Levels(ensure, src, tb, bar, temp, log)
88
+ env["timebase"] = tb
89
+ env["@levels"] = Levels(src, tb, bar, temp, log)
92
90
  env["@filesetup"] = filesetup
93
91
 
94
92
  results = interpret(env, parser)
@@ -139,7 +137,6 @@ def parse_time(val: str, arr: NDArray, tb: Fraction) -> int: # raises: `CoerceE
139
137
 
140
138
  def make_timeline(
141
139
  sources: list[FileInfo],
142
- ensure: Ensure,
143
140
  args: Args,
144
141
  sr: int,
145
142
  bar: Bar,
@@ -169,7 +166,7 @@ def make_timeline(
169
166
  concat = np.concatenate
170
167
 
171
168
  for i, src in enumerate(sources):
172
- filesetup = FileSetup(src, ensure, len(sources) < 2, tb, bar, temp, log)
169
+ filesetup = FileSetup(src, len(sources) < 2, tb, bar, temp, log)
173
170
 
174
171
  edit_result = run_interpreter_for_edit_option(method, filesetup)
175
172
  mut_margin(edit_result, start_margin, end_margin)
@@ -57,7 +57,7 @@ class Ensure:
57
57
  output_astream = out_container.add_stream("pcm_s16le", rate=sample_rate)
58
58
  assert isinstance(output_astream, av.audio.stream.AudioStream)
59
59
 
60
- resampler = AudioResampler(format="s16", layout="stereo", rate=sample_rate) # type: ignore
60
+ resampler = AudioResampler(format="s16", layout="stereo", rate=sample_rate)
61
61
  for i, frame in enumerate(in_container.decode(astream)):
62
62
  if i % 1500 == 0:
63
63
  bar.tick(0 if frame.time is None else frame.time)
@@ -99,7 +99,7 @@ def _ffset(option: str, value: str | None) -> list[str]:
99
99
  return [option] + [value]
100
100
 
101
101
 
102
- def video_quality(args: Args, ctr: Container) -> list[str]:
102
+ def video_quality(args: Args) -> list[str]:
103
103
  return (
104
104
  _ffset("-b:v", args.video_bitrate)
105
105
  + ["-c:v", args.video_codec]
@@ -174,7 +174,7 @@ def mux_quality_media(
174
174
  for is_video, path in visual_output:
175
175
  if is_video:
176
176
  if apply_v:
177
- cmd += video_quality(args, ctr)
177
+ cmd += video_quality(args)
178
178
  else:
179
179
  # Real video is only allowed on track 0
180
180
  cmd += ["-c:v:0", "copy"]
@@ -211,7 +211,7 @@ def mux_quality_media(
211
211
  cmd.extend(["-c:s", scodec])
212
212
  elif ctr.scodecs is not None:
213
213
  if scodec not in ctr.scodecs:
214
- scodec = ctr.scodecs[0]
214
+ scodec = ctr.default_sub
215
215
  cmd.extend(["-c:s", scodec])
216
216
 
217
217
  if a_tracks > 0:
@@ -6,7 +6,6 @@ from statistics import fmean, median
6
6
  from typing import TextIO
7
7
 
8
8
  from auto_editor.analyze import Levels
9
- from auto_editor.output import Ensure
10
9
  from auto_editor.timeline import v3
11
10
  from auto_editor.utils.bar import Bar
12
11
  from auto_editor.utils.func import to_timecode
@@ -49,7 +48,7 @@ def all_cuts(tl: v3, in_len: int) -> list[int]:
49
48
  return cut_lens
50
49
 
51
50
 
52
- def preview(ensure: Ensure, tl: v3, temp: str, log: Log) -> None:
51
+ def preview(tl: v3, temp: str, log: Log) -> None:
53
52
  log.conwrite("")
54
53
  tb = tl.tb
55
54
 
@@ -66,7 +65,7 @@ def preview(ensure: Ensure, tl: v3, temp: str, log: Log) -> None:
66
65
 
67
66
  in_len = 0
68
67
  for src in all_sources:
69
- in_len += Levels(ensure, src, tb, Bar("none"), temp, log).media_length
68
+ in_len += Levels(src, tb, Bar("none"), temp, log).media_length
70
69
 
71
70
  out_len = tl.out_len()
72
71
 
@@ -49,7 +49,7 @@ class SubtitleParser:
49
49
  self.codec = codec
50
50
  self.contents = []
51
51
 
52
- if codec == "ass":
52
+ if codec == "ass" or codec == "ssa":
53
53
  time_code = re.compile(r"(.*)(\d+:\d+:[\d.]+)(.*)(\d+:\d+:[\d.]+)(.*)")
54
54
  elif codec == "webvtt":
55
55
  time_code = re.compile(r"()(\d+:[\d.]+)( --> )(\d+:[\d.]+)(\n.*)")
@@ -17,8 +17,6 @@ from auto_editor.utils.types import color
17
17
  if TYPE_CHECKING:
18
18
  from collections.abc import Iterator
19
19
 
20
- from av.filter import FilterContext
21
-
22
20
  from auto_editor.ffwrapper import FFmpeg, FileInfo
23
21
  from auto_editor.timeline import v3
24
22
  from auto_editor.utils.bar import Bar
@@ -33,11 +31,6 @@ class VideoFrame:
33
31
  src: FileInfo
34
32
 
35
33
 
36
- def link_nodes(*nodes: FilterContext) -> None:
37
- for c, n in zip(nodes, nodes[1:]):
38
- c.link_to(n)
39
-
40
-
41
34
  # From: github.com/PyAV-Org/PyAV/blob/main/av/video/frame.pyx
42
35
  allowed_pix_fmt = {
43
36
  "yuv420p",
@@ -98,12 +91,11 @@ def make_image_cache(tl: v3) -> dict[tuple[FileInfo, int], np.ndarray]:
98
91
  for frame in cn.decode(my_stream):
99
92
  if obj.width != 0:
100
93
  graph = av.filter.Graph()
101
- link_nodes(
94
+ graph.link_nodes(
102
95
  graph.add_buffer(template=my_stream),
103
96
  graph.add("scale", f"{obj.width}:-1"),
104
97
  graph.add("buffersink"),
105
- )
106
- graph.vpush(frame)
98
+ ).vpush(frame)
107
99
  frame = graph.vpull()
108
100
  img_cache[(obj.src, obj.width)] = frame.to_ndarray(
109
101
  format="rgb24"
@@ -177,7 +169,7 @@ def render_av(
177
169
  target_width = max(round(tl.res[0] * args.scale), 2)
178
170
  target_height = max(round(tl.res[1] * args.scale), 2)
179
171
  scale_graph = av.filter.Graph()
180
- link_nodes(
172
+ scale_graph.link_nodes(
181
173
  scale_graph.add(
182
174
  "buffer", video_size="1x1", time_base="1/1", pix_fmt=target_pix_fmt
183
175
  ),
@@ -213,7 +205,7 @@ def render_av(
213
205
  if apply_video_later:
214
206
  cmd += ["-c:v", "mpeg4", "-qscale:v", "1"]
215
207
  else:
216
- cmd += video_quality(args, ctr)
208
+ cmd += video_quality(args)
217
209
 
218
210
  # Setting SAR requires re-encoding so we do it here.
219
211
  if src is not None and src.videos and (sar := src.videos[0].sar) is not None:
@@ -293,7 +285,7 @@ def render_av(
293
285
  if (frame.width, frame.height) != tl.res:
294
286
  width, height = tl.res
295
287
  graph = av.filter.Graph()
296
- link_nodes(
288
+ graph.link_nodes(
297
289
  graph.add_buffer(template=my_stream),
298
290
  graph.add(
299
291
  "scale",
@@ -301,21 +293,19 @@ def render_av(
301
293
  ),
302
294
  graph.add("pad", f"{width}:{height}:-1:-1:color={bg}"),
303
295
  graph.add("buffersink"),
304
- )
305
- graph.vpush(frame)
296
+ ).vpush(frame)
306
297
  frame = graph.vpull()
307
298
  elif isinstance(obj, TlRect):
308
299
  graph = av.filter.Graph()
309
300
  x, y = apply_anchor(obj.x, obj.y, obj.width, obj.height, obj.anchor)
310
- link_nodes(
301
+ graph.link_nodes(
311
302
  graph.add_buffer(template=my_stream),
312
303
  graph.add(
313
304
  "drawbox",
314
305
  f"x={x}:y={y}:w={obj.width}:h={obj.height}:color={obj.fill}:t=fill",
315
306
  ),
316
307
  graph.add("buffersink"),
317
- )
318
- graph.vpush(frame)
308
+ ).vpush(frame)
319
309
  frame = graph.vpull()
320
310
  elif isinstance(obj, TlImage):
321
311
  img = img_cache[(obj.src, obj.width)]