auto-editor 24.29.1__tar.gz → 24.31.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.31.1}/PKG-INFO +2 -2
  2. auto_editor-24.31.1/auto_editor/__init__.py +2 -0
  3. {auto_editor-24.29.1 → auto_editor-24.31.1}/auto_editor/analyze.py +143 -132
  4. {auto_editor-24.29.1 → auto_editor-24.31.1}/auto_editor/edit.py +14 -21
  5. {auto_editor-24.29.1 → auto_editor-24.31.1}/auto_editor/lang/palet.py +12 -16
  6. {auto_editor-24.29.1 → auto_editor-24.31.1}/auto_editor/lib/data_structs.py +2 -0
  7. {auto_editor-24.29.1 → auto_editor-24.31.1}/auto_editor/make_layers.py +3 -6
  8. {auto_editor-24.29.1 → auto_editor-24.31.1}/auto_editor/output.py +5 -5
  9. {auto_editor-24.29.1 → auto_editor-24.31.1}/auto_editor/preview.py +2 -3
  10. {auto_editor-24.29.1 → auto_editor-24.31.1}/auto_editor/render/subtitle.py +1 -1
  11. {auto_editor-24.29.1 → auto_editor-24.31.1}/auto_editor/render/video.py +8 -18
  12. {auto_editor-24.29.1 → auto_editor-24.31.1}/auto_editor/subcommands/levels.py +18 -17
  13. {auto_editor-24.29.1 → auto_editor-24.31.1}/auto_editor/subcommands/repl.py +3 -12
  14. auto_editor-24.31.1/auto_editor/subcommands/subdump.py +20 -0
  15. {auto_editor-24.29.1 → auto_editor-24.31.1}/auto_editor/subcommands/test.py +2 -2
  16. auto_editor-24.31.1/auto_editor/utils/container.py +96 -0
  17. {auto_editor-24.29.1 → auto_editor-24.31.1}/auto_editor.egg-info/PKG-INFO +2 -2
  18. {auto_editor-24.29.1 → auto_editor-24.31.1}/auto_editor.egg-info/requires.txt +1 -1
  19. {auto_editor-24.29.1 → auto_editor-24.31.1}/pyproject.toml +1 -1
  20. auto_editor-24.29.1/auto_editor/__init__.py +0 -2
  21. auto_editor-24.29.1/auto_editor/subcommands/subdump.py +0 -23
  22. auto_editor-24.29.1/auto_editor/utils/container.py +0 -338
  23. {auto_editor-24.29.1 → auto_editor-24.31.1}/LICENSE +0 -0
  24. {auto_editor-24.29.1 → auto_editor-24.31.1}/README.md +0 -0
  25. {auto_editor-24.29.1 → auto_editor-24.31.1}/auto_editor/__main__.py +0 -0
  26. {auto_editor-24.29.1 → auto_editor-24.31.1}/auto_editor/ffwrapper.py +0 -0
  27. {auto_editor-24.29.1 → auto_editor-24.31.1}/auto_editor/formats/__init__.py +0 -0
  28. {auto_editor-24.29.1 → auto_editor-24.31.1}/auto_editor/formats/fcp11.py +0 -0
  29. {auto_editor-24.29.1 → auto_editor-24.31.1}/auto_editor/formats/fcp7.py +0 -0
  30. {auto_editor-24.29.1 → auto_editor-24.31.1}/auto_editor/formats/json.py +0 -0
  31. {auto_editor-24.29.1 → auto_editor-24.31.1}/auto_editor/formats/shotcut.py +0 -0
  32. {auto_editor-24.29.1 → auto_editor-24.31.1}/auto_editor/formats/utils.py +0 -0
  33. {auto_editor-24.29.1 → auto_editor-24.31.1}/auto_editor/help.py +0 -0
  34. {auto_editor-24.29.1 → auto_editor-24.31.1}/auto_editor/lang/__init__.py +0 -0
  35. {auto_editor-24.29.1 → auto_editor-24.31.1}/auto_editor/lang/json.py +0 -0
  36. {auto_editor-24.29.1 → auto_editor-24.31.1}/auto_editor/lang/libmath.py +0 -0
  37. {auto_editor-24.29.1 → auto_editor-24.31.1}/auto_editor/lib/__init__.py +0 -0
  38. {auto_editor-24.29.1 → auto_editor-24.31.1}/auto_editor/lib/contracts.py +0 -0
  39. {auto_editor-24.29.1 → auto_editor-24.31.1}/auto_editor/lib/err.py +0 -0
  40. {auto_editor-24.29.1 → auto_editor-24.31.1}/auto_editor/render/__init__.py +0 -0
  41. {auto_editor-24.29.1 → auto_editor-24.31.1}/auto_editor/render/audio.py +0 -0
  42. {auto_editor-24.29.1 → auto_editor-24.31.1}/auto_editor/subcommands/__init__.py +0 -0
  43. {auto_editor-24.29.1 → auto_editor-24.31.1}/auto_editor/subcommands/desc.py +0 -0
  44. {auto_editor-24.29.1 → auto_editor-24.31.1}/auto_editor/subcommands/info.py +0 -0
  45. {auto_editor-24.29.1 → auto_editor-24.31.1}/auto_editor/subcommands/palet.py +0 -0
  46. {auto_editor-24.29.1 → auto_editor-24.31.1}/auto_editor/timeline.py +0 -0
  47. {auto_editor-24.29.1 → auto_editor-24.31.1}/auto_editor/utils/__init__.py +0 -0
  48. {auto_editor-24.29.1 → auto_editor-24.31.1}/auto_editor/utils/bar.py +0 -0
  49. {auto_editor-24.29.1 → auto_editor-24.31.1}/auto_editor/utils/chunks.py +0 -0
  50. {auto_editor-24.29.1 → auto_editor-24.31.1}/auto_editor/utils/cmdkw.py +0 -0
  51. {auto_editor-24.29.1 → auto_editor-24.31.1}/auto_editor/utils/encoder.py +0 -0
  52. {auto_editor-24.29.1 → auto_editor-24.31.1}/auto_editor/utils/func.py +0 -0
  53. {auto_editor-24.29.1 → auto_editor-24.31.1}/auto_editor/utils/log.py +0 -0
  54. {auto_editor-24.29.1 → auto_editor-24.31.1}/auto_editor/utils/subtitle_tools.py +0 -0
  55. {auto_editor-24.29.1 → auto_editor-24.31.1}/auto_editor/utils/types.py +0 -0
  56. {auto_editor-24.29.1 → auto_editor-24.31.1}/auto_editor/validate_input.py +0 -0
  57. {auto_editor-24.29.1 → auto_editor-24.31.1}/auto_editor/vanparse.py +0 -0
  58. {auto_editor-24.29.1 → auto_editor-24.31.1}/auto_editor/wavfile.py +0 -0
  59. {auto_editor-24.29.1 → auto_editor-24.31.1}/auto_editor.egg-info/SOURCES.txt +0 -0
  60. {auto_editor-24.29.1 → auto_editor-24.31.1}/auto_editor.egg-info/dependency_links.txt +0 -0
  61. {auto_editor-24.29.1 → auto_editor-24.31.1}/auto_editor.egg-info/entry_points.txt +0 -0
  62. {auto_editor-24.29.1 → auto_editor-24.31.1}/auto_editor.egg-info/top_level.txt +0 -0
  63. {auto_editor-24.29.1 → auto_editor-24.31.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.31.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.31.1"
2
+ version = "24w31a"
@@ -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))
@@ -620,42 +620,36 @@ def make_array(dtype: Sym, size: int, v: int = 0) -> np.ndarray:
620
620
  raise MyError(f"number too large to be converted to {dtype}")
621
621
 
622
622
 
623
- def minclip(oarr: BoolList, _min: int) -> BoolList:
623
+ def minclip(oarr: BoolList, _min: int, /) -> BoolList:
624
624
  arr = np.copy(oarr)
625
625
  mut_remove_small(arr, _min, replace=1, with_=0)
626
626
  return arr
627
627
 
628
628
 
629
- def mincut(oarr: BoolList, _min: int) -> BoolList:
629
+ def mincut(oarr: BoolList, _min: int, /) -> BoolList:
630
630
  arr = np.copy(oarr)
631
631
  mut_remove_small(arr, _min, replace=0, with_=1)
632
632
  return arr
633
633
 
634
634
 
635
- def maxclip(oarr: BoolList, _min: int) -> BoolList:
635
+ def maxclip(oarr: BoolList, _min: int, /) -> BoolList:
636
636
  arr = np.copy(oarr)
637
637
  mut_remove_large(arr, _min, replace=1, with_=0)
638
638
  return arr
639
639
 
640
640
 
641
- def maxcut(oarr: BoolList, _min: int) -> BoolList:
641
+ def maxcut(oarr: BoolList, _min: int, /) -> BoolList:
642
642
  arr = np.copy(oarr)
643
643
  mut_remove_large(arr, _min, replace=0, with_=1)
644
644
  return arr
645
645
 
646
646
 
647
- def margin(a: int, b: Any, c: Any = None) -> BoolList:
648
- if c is None:
649
- check_args("margin", [a, b], (2, 2), (is_int, is_boolarr))
650
- oarr = b
651
- start, end = a, a
652
- else:
653
- check_args("margin", [a, b, c], (3, 3), (is_int, is_int, is_boolarr))
654
- oarr = c
655
- start, end = a, b
656
-
647
+ def margin(oarr: BoolList, start: int, end: int | None = None, /) -> BoolList:
657
648
  arr = np.copy(oarr)
658
- mut_margin(arr, start, end)
649
+ if end is None:
650
+ mut_margin(arr, start, start)
651
+ else:
652
+ mut_margin(arr, start, end)
659
653
  return arr
660
654
 
661
655
 
@@ -1741,6 +1735,8 @@ env.update({
1741
1735
  "round": Proc("round", round, (1, 1), is_real),
1742
1736
  "max": Proc("max", lambda *v: max(v), (1, None), is_real),
1743
1737
  "min": Proc("min", lambda *v: min(v), (1, None), is_real),
1738
+ "max-seq": Proc("max-seq", max, (1, 1), is_sequence),
1739
+ "min-seq": Proc("min-seq", min, (1, 1), is_sequence),
1744
1740
  "mod": Proc("mod", mod, (2, 2), is_int),
1745
1741
  "modulo": Proc("modulo", mod, (2, 2), is_int),
1746
1742
  # symbols
@@ -1796,7 +1792,7 @@ env.update({
1796
1792
  "bool-array": Proc(
1797
1793
  "bool-array", lambda *a: np.array(a, dtype=np.bool_), (1, None), is_nat
1798
1794
  ),
1799
- "margin": Proc("margin", margin, (2, 3)),
1795
+ "margin": Proc("margin", margin, (2, 3), is_boolarr, is_int),
1800
1796
  "mincut": Proc("mincut", mincut, (2, 2), is_boolarr, is_nat),
1801
1797
  "minclip": Proc("minclip", minclip, (2, 2), is_boolarr, is_nat),
1802
1798
  "maxcut": Proc("maxcut", maxcut, (2, 2), is_boolarr, is_nat),
@@ -185,6 +185,8 @@ def display_str(val: object) -> str:
185
185
  return f"{val.real}{join}{val.imag}i"
186
186
  if type(val) is np.bool_:
187
187
  return "1" if val else "0"
188
+ if type(val) is np.float64 or type(val) is np.float32:
189
+ return f"{float(val)}"
188
190
  if type(val) is Fraction:
189
191
  return f"{val.numerator}/{val.denominator}"
190
192
 
@@ -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:
@@ -227,7 +227,7 @@ def mux_quality_media(
227
227
  cmd.extend(["-color_range", f"{color_range}"])
228
228
  if colorspace in (0, 1) or (colorspace >= 3 and colorspace < 16):
229
229
  cmd.extend(["-colorspace", f"{colorspace}"])
230
- if color_prim in (0, 1) or (color_prim >= 4 and color_prim < 17):
230
+ if color_prim == 1 or (color_prim >= 4 and color_prim < 17):
231
231
  cmd.extend(["-color_primaries", f"{color_prim}"])
232
232
  if color_trc == 1 or (color_trc >= 4 and color_trc < 22):
233
233
  cmd.extend(["-color_trc", f"{color_trc}"])
@@ -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.*)")