auto-editor 24.31.1__tar.gz → 25.0.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 (61) hide show
  1. {auto_editor-24.31.1 → auto_editor-25.0.1}/PKG-INFO +1 -1
  2. auto_editor-25.0.1/auto_editor/__init__.py +1 -0
  3. {auto_editor-24.31.1 → auto_editor-25.0.1}/auto_editor/__main__.py +15 -20
  4. {auto_editor-24.31.1 → auto_editor-25.0.1}/auto_editor/analyze.py +26 -30
  5. {auto_editor-24.31.1 → auto_editor-25.0.1}/auto_editor/edit.py +8 -12
  6. {auto_editor-24.31.1 → auto_editor-25.0.1}/auto_editor/ffwrapper.py +4 -2
  7. {auto_editor-24.31.1 → auto_editor-25.0.1}/auto_editor/lang/palet.py +5 -7
  8. {auto_editor-24.31.1 → auto_editor-25.0.1}/auto_editor/lib/contracts.py +5 -2
  9. {auto_editor-24.31.1 → auto_editor-25.0.1}/auto_editor/make_layers.py +28 -47
  10. {auto_editor-24.31.1 → auto_editor-25.0.1}/auto_editor/output.py +3 -5
  11. {auto_editor-24.31.1 → auto_editor-25.0.1}/auto_editor/preview.py +2 -2
  12. {auto_editor-24.31.1 → auto_editor-25.0.1}/auto_editor/render/audio.py +2 -1
  13. {auto_editor-24.31.1 → auto_editor-25.0.1}/auto_editor/render/video.py +2 -7
  14. {auto_editor-24.31.1 → auto_editor-25.0.1}/auto_editor/subcommands/info.py +2 -2
  15. {auto_editor-24.31.1 → auto_editor-25.0.1}/auto_editor/subcommands/levels.py +4 -9
  16. {auto_editor-24.31.1 → auto_editor-25.0.1}/auto_editor/subcommands/repl.py +4 -7
  17. {auto_editor-24.31.1 → auto_editor-25.0.1}/auto_editor/subcommands/test.py +40 -33
  18. {auto_editor-24.31.1 → auto_editor-25.0.1}/auto_editor/timeline.py +10 -12
  19. {auto_editor-24.31.1 → auto_editor-25.0.1}/auto_editor/utils/container.py +6 -0
  20. {auto_editor-24.31.1 → auto_editor-25.0.1}/auto_editor/utils/func.py +10 -26
  21. {auto_editor-24.31.1 → auto_editor-25.0.1}/auto_editor/utils/log.py +35 -8
  22. {auto_editor-24.31.1 → auto_editor-25.0.1}/auto_editor/utils/types.py +2 -1
  23. {auto_editor-24.31.1 → auto_editor-25.0.1}/auto_editor/validate_input.py +12 -7
  24. {auto_editor-24.31.1 → auto_editor-25.0.1}/auto_editor/vanparse.py +1 -2
  25. {auto_editor-24.31.1 → auto_editor-25.0.1}/auto_editor.egg-info/PKG-INFO +1 -1
  26. auto_editor-24.31.1/auto_editor/__init__.py +0 -2
  27. {auto_editor-24.31.1 → auto_editor-25.0.1}/LICENSE +0 -0
  28. {auto_editor-24.31.1 → auto_editor-25.0.1}/README.md +0 -0
  29. {auto_editor-24.31.1 → auto_editor-25.0.1}/auto_editor/formats/__init__.py +0 -0
  30. {auto_editor-24.31.1 → auto_editor-25.0.1}/auto_editor/formats/fcp11.py +0 -0
  31. {auto_editor-24.31.1 → auto_editor-25.0.1}/auto_editor/formats/fcp7.py +0 -0
  32. {auto_editor-24.31.1 → auto_editor-25.0.1}/auto_editor/formats/json.py +0 -0
  33. {auto_editor-24.31.1 → auto_editor-25.0.1}/auto_editor/formats/shotcut.py +0 -0
  34. {auto_editor-24.31.1 → auto_editor-25.0.1}/auto_editor/formats/utils.py +0 -0
  35. {auto_editor-24.31.1 → auto_editor-25.0.1}/auto_editor/help.py +0 -0
  36. {auto_editor-24.31.1 → auto_editor-25.0.1}/auto_editor/lang/__init__.py +0 -0
  37. {auto_editor-24.31.1 → auto_editor-25.0.1}/auto_editor/lang/json.py +0 -0
  38. {auto_editor-24.31.1 → auto_editor-25.0.1}/auto_editor/lang/libmath.py +0 -0
  39. {auto_editor-24.31.1 → auto_editor-25.0.1}/auto_editor/lib/__init__.py +0 -0
  40. {auto_editor-24.31.1 → auto_editor-25.0.1}/auto_editor/lib/data_structs.py +0 -0
  41. {auto_editor-24.31.1 → auto_editor-25.0.1}/auto_editor/lib/err.py +0 -0
  42. {auto_editor-24.31.1 → auto_editor-25.0.1}/auto_editor/render/__init__.py +0 -0
  43. {auto_editor-24.31.1 → auto_editor-25.0.1}/auto_editor/render/subtitle.py +0 -0
  44. {auto_editor-24.31.1 → auto_editor-25.0.1}/auto_editor/subcommands/__init__.py +0 -0
  45. {auto_editor-24.31.1 → auto_editor-25.0.1}/auto_editor/subcommands/desc.py +0 -0
  46. {auto_editor-24.31.1 → auto_editor-25.0.1}/auto_editor/subcommands/palet.py +0 -0
  47. {auto_editor-24.31.1 → auto_editor-25.0.1}/auto_editor/subcommands/subdump.py +0 -0
  48. {auto_editor-24.31.1 → auto_editor-25.0.1}/auto_editor/utils/__init__.py +0 -0
  49. {auto_editor-24.31.1 → auto_editor-25.0.1}/auto_editor/utils/bar.py +0 -0
  50. {auto_editor-24.31.1 → auto_editor-25.0.1}/auto_editor/utils/chunks.py +0 -0
  51. {auto_editor-24.31.1 → auto_editor-25.0.1}/auto_editor/utils/cmdkw.py +0 -0
  52. {auto_editor-24.31.1 → auto_editor-25.0.1}/auto_editor/utils/encoder.py +0 -0
  53. {auto_editor-24.31.1 → auto_editor-25.0.1}/auto_editor/utils/subtitle_tools.py +0 -0
  54. {auto_editor-24.31.1 → auto_editor-25.0.1}/auto_editor/wavfile.py +0 -0
  55. {auto_editor-24.31.1 → auto_editor-25.0.1}/auto_editor.egg-info/SOURCES.txt +0 -0
  56. {auto_editor-24.31.1 → auto_editor-25.0.1}/auto_editor.egg-info/dependency_links.txt +0 -0
  57. {auto_editor-24.31.1 → auto_editor-25.0.1}/auto_editor.egg-info/entry_points.txt +0 -0
  58. {auto_editor-24.31.1 → auto_editor-25.0.1}/auto_editor.egg-info/requires.txt +0 -0
  59. {auto_editor-24.31.1 → auto_editor-25.0.1}/auto_editor.egg-info/top_level.txt +0 -0
  60. {auto_editor-24.31.1 → auto_editor-25.0.1}/pyproject.toml +0 -0
  61. {auto_editor-24.31.1 → auto_editor-25.0.1}/setup.cfg +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: auto-editor
3
- Version: 24.31.1
3
+ Version: 25.0.1
4
4
  Summary: Auto-Editor: Effort free video editing!
5
5
  Author-email: WyattBlue <wyattblue@auto-editor.com>
6
6
  License: Unlicense
@@ -0,0 +1 @@
1
+ __version__ = "25.0.1"
@@ -6,7 +6,6 @@ from os import environ
6
6
  import auto_editor
7
7
  from auto_editor.edit import edit_media
8
8
  from auto_editor.ffwrapper import FFmpeg
9
- from auto_editor.utils.func import setup_tempdir
10
9
  from auto_editor.utils.log import Log
11
10
  from auto_editor.utils.types import (
12
11
  Args,
@@ -140,10 +139,7 @@ def main_options(parser: ArgumentParser) -> ArgumentParser:
140
139
  )
141
140
  parser.add_text("Utility Options:")
142
141
  parser.add_argument(
143
- "--export",
144
- "-ex",
145
- metavar="EXPORT:ATTRS?",
146
- help="Choose the export mode",
142
+ "--export", "-ex", metavar="EXPORT:ATTRS?", help="Choose the export mode"
147
143
  )
148
144
  parser.add_argument(
149
145
  "--output-file",
@@ -153,15 +149,10 @@ def main_options(parser: ArgumentParser) -> ArgumentParser:
153
149
  help="Set the name/path of the new output file.",
154
150
  )
155
151
  parser.add_argument(
156
- "--player",
157
- "-p",
158
- metavar="CMD",
159
- help="Set player to open output media files",
152
+ "--player", "-p", metavar="CMD", help="Set player to open output media files"
160
153
  )
161
154
  parser.add_argument(
162
- "--no-open",
163
- flag=True,
164
- help="Do not open the output file after editing is done",
155
+ "--no-open", flag=True, help="Do not open the output file after editing is done"
165
156
  )
166
157
  parser.add_argument(
167
158
  "--temp-dir",
@@ -255,7 +246,7 @@ def main_options(parser: ArgumentParser) -> ArgumentParser:
255
246
  parser.add_argument(
256
247
  "--audio-normalize",
257
248
  metavar="NORM-TYPE",
258
- help="Apply audio rendering to all audio tracks. Applied right before rendering the output file.",
249
+ help="Apply audio rendering to all audio tracks. Applied right before rendering the output file",
259
250
  )
260
251
  parser.add_text("Miscellaneous:")
261
252
  parser.add_argument(
@@ -268,6 +259,9 @@ def main_options(parser: ArgumentParser) -> ArgumentParser:
268
259
  metavar="CMD",
269
260
  help="Add extra options for ffmpeg. Must be in quotes",
270
261
  )
262
+ parser.add_argument(
263
+ "--no-cache", flag=True, help="Don't look for or write a cache file"
264
+ )
271
265
  parser.add_argument("--version", "-V", flag=True, help="Display version and halt")
272
266
  return parser
273
267
 
@@ -303,7 +297,7 @@ def main() -> None:
303
297
  )
304
298
 
305
299
  if args.version:
306
- print(f"{auto_editor.version} ({auto_editor.__version__})")
300
+ print(auto_editor.__version__)
307
301
  return
308
302
 
309
303
  if args.debug and not args.input:
@@ -311,18 +305,19 @@ def main() -> None:
311
305
 
312
306
  import av
313
307
 
308
+ license = av._core.library_meta["libavcodec"]["license"]
309
+
314
310
  print(f"OS: {plat.system()} {plat.release()} {plat.machine().lower()}")
315
311
  print(f"Python: {plat.python_version()}")
316
- print(f"PyAV: {av.__version__}")
317
- print(f"Auto-Editor: {auto_editor.version}")
312
+ print(f"PyAV: {av.__version__} ({license})")
313
+ print(f"Auto-Editor: {auto_editor.__version__}")
318
314
  return
319
315
 
320
316
  if not args.input:
321
317
  log.error("You need to give auto-editor an input file.")
322
318
 
323
- temp = setup_tempdir(args.temp_dir, log)
324
- log = Log(args.debug, args.quiet, temp, args.progress == "machine", no_color)
325
- log.debug(f"Temp Directory: {temp}")
319
+ is_machine = args.progress == "machine"
320
+ log = Log(args.debug, args.quiet, args.temp_dir, is_machine, no_color)
326
321
 
327
322
  ffmpeg = FFmpeg(
328
323
  args.ffmpeg_location,
@@ -333,7 +328,7 @@ def main() -> None:
333
328
  paths = valid_input(args.input, ffmpeg, args, log)
334
329
 
335
330
  try:
336
- edit_media(paths, ffmpeg, args, temp, log)
331
+ edit_media(paths, ffmpeg, args, log)
337
332
  except KeyboardInterrupt:
338
333
  log.error("Keyboard Interrupt")
339
334
  log.cleanup()
@@ -5,6 +5,7 @@ import re
5
5
  from dataclasses import dataclass
6
6
  from fractions import Fraction
7
7
  from math import ceil
8
+ from tempfile import gettempdir
8
9
  from typing import TYPE_CHECKING
9
10
 
10
11
  import av
@@ -12,7 +13,7 @@ import numpy as np
12
13
  from av.audio.fifo import AudioFifo
13
14
  from av.subtitles.subtitle import AssSubtitle
14
15
 
15
- from auto_editor import version
16
+ from auto_editor import __version__
16
17
  from auto_editor.utils.subtitle_tools import convert_ass_to_text
17
18
 
18
19
  if TYPE_CHECKING:
@@ -27,16 +28,6 @@ if TYPE_CHECKING:
27
28
  from auto_editor.utils.log import Log
28
29
 
29
30
 
30
- @dataclass(slots=True)
31
- class FileSetup:
32
- src: FileInfo
33
- strict: bool
34
- tb: Fraction
35
- bar: Bar
36
- temp: str
37
- log: Log
38
-
39
-
40
31
  class LevelError(Exception):
41
32
  pass
42
33
 
@@ -88,7 +79,7 @@ def obj_tag(tag: str, tb: Fraction, obj: dict[str, Any]) -> str:
88
79
  return key
89
80
 
90
81
 
91
- def iter_audio(src, tb: Fraction, stream: int = 0) -> Iterator[float]:
82
+ def iter_audio(src, tb: Fraction, stream: int = 0) -> Iterator[np.float32]:
92
83
  fifo = AudioFifo()
93
84
  try:
94
85
  container = av.open(src.path, "r")
@@ -117,13 +108,13 @@ def iter_audio(src, tb: Fraction, stream: int = 0) -> Iterator[float]:
117
108
  audio_chunk = fifo.read(current_size)
118
109
  assert audio_chunk is not None
119
110
  arr = audio_chunk.to_ndarray().flatten()
120
- yield float(np.max(np.abs(arr)))
111
+ yield np.max(np.abs(arr))
121
112
 
122
113
  finally:
123
114
  container.close()
124
115
 
125
116
 
126
- def iter_motion(src, tb, stream: int, blur: int, width: int) -> Iterator[float]:
117
+ def iter_motion(src, tb, stream: int, blur: int, width: int) -> Iterator[np.float32]:
127
118
  container = av.open(src.path, "r")
128
119
 
129
120
  video = container.streams.video[stream]
@@ -155,11 +146,11 @@ def iter_motion(src, tb, stream: int, blur: int, width: int) -> Iterator[float]:
155
146
 
156
147
  current_frame = frame.to_ndarray()
157
148
  if prev_frame is None:
158
- value = 0.0
149
+ value = np.float32(0.0)
159
150
  else:
160
151
  # Use `int16` to avoid underflow with `uint8` datatype
161
152
  diff = np.abs(prev_frame.astype(np.int16) - current_frame.astype(np.int16))
162
- value = np.count_nonzero(diff) / total_pixels
153
+ value = np.float32(np.count_nonzero(diff) / total_pixels)
163
154
 
164
155
  for _ in range(index - prev_index):
165
156
  yield value
@@ -175,8 +166,9 @@ class Levels:
175
166
  src: FileInfo
176
167
  tb: Fraction
177
168
  bar: Bar
178
- temp: str
169
+ no_cache: bool
179
170
  log: Log
171
+ strict: bool
180
172
 
181
173
  @property
182
174
  def media_length(self) -> int:
@@ -210,9 +202,10 @@ class Levels:
210
202
  return np.zeros(self.media_length, dtype=np.bool_)
211
203
 
212
204
  def read_cache(self, tag: str, obj: dict[str, Any]) -> None | np.ndarray:
213
- workfile = os.path.join(
214
- os.path.dirname(self.temp), f"ae-{version}", "cache.npz"
215
- )
205
+ if self.no_cache:
206
+ return None
207
+
208
+ workfile = os.path.join(gettempdir(), f"ae-{__version__}", "cache.npz")
216
209
 
217
210
  try:
218
211
  npzfile = np.load(workfile, allow_pickle=False)
@@ -227,8 +220,11 @@ class Levels:
227
220
  self.log.debug("Using cache")
228
221
  return npzfile[key]
229
222
 
230
- def cache(self, tag: str, obj: dict[str, Any], arr: np.ndarray) -> np.ndarray:
231
- workdur = os.path.join(os.path.dirname(self.temp), f"ae-{version}")
223
+ def cache(self, arr: np.ndarray, tag: str, obj: dict[str, Any]) -> np.ndarray:
224
+ if self.no_cache:
225
+ return arr
226
+
227
+ workdur = os.path.join(gettempdir(), f"ae-{__version__}")
232
228
  if not os.path.exists(workdur):
233
229
  os.mkdir(workdur)
234
230
 
@@ -237,7 +233,7 @@ class Levels:
237
233
 
238
234
  return arr
239
235
 
240
- def audio(self, stream: int) -> NDArray[np.float64]:
236
+ def audio(self, stream: int) -> NDArray[np.float32]:
241
237
  if stream >= len(self.src.audios):
242
238
  raise LevelError(f"audio: audio stream '{stream}' does not exist.")
243
239
 
@@ -256,21 +252,21 @@ class Levels:
256
252
  bar = self.bar
257
253
  bar.start(inaccurate_dur, "Analyzing audio volume")
258
254
 
259
- result = np.zeros((inaccurate_dur), dtype=np.float64)
255
+ result = np.zeros((inaccurate_dur), dtype=np.float32)
260
256
  index = 0
261
257
  for value in iter_audio(self.src, self.tb, stream):
262
258
  if index > len(result) - 1:
263
259
  result = np.concatenate(
264
- (result, np.zeros((len(result)), dtype=np.float64))
260
+ (result, np.zeros((len(result)), dtype=np.float32))
265
261
  )
266
262
  result[index] = value
267
263
  bar.tick(index)
268
264
  index += 1
269
265
 
270
266
  bar.end()
271
- return self.cache("audio", {"stream": stream}, result[:index])
267
+ return self.cache(result[:index], "audio", {"stream": stream})
272
268
 
273
- def motion(self, stream: int, blur: int, width: int) -> NDArray[np.float64]:
269
+ def motion(self, stream: int, blur: int, width: int) -> NDArray[np.float32]:
274
270
  if stream >= len(self.src.videos):
275
271
  raise LevelError(f"motion: video stream '{stream}' does not exist.")
276
272
 
@@ -289,19 +285,19 @@ class Levels:
289
285
  bar = self.bar
290
286
  bar.start(inaccurate_dur, "Analyzing motion")
291
287
 
292
- result = np.zeros((inaccurate_dur), dtype=np.float64)
288
+ result = np.zeros((inaccurate_dur), dtype=np.float32)
293
289
  index = 0
294
290
  for value in iter_motion(self.src, self.tb, stream, blur, width):
295
291
  if index > len(result) - 1:
296
292
  result = np.concatenate(
297
- (result, np.zeros((len(result)), dtype=np.float64))
293
+ (result, np.zeros((len(result)), dtype=np.float32))
298
294
  )
299
295
  result[index] = value
300
296
  bar.tick(index)
301
297
  index += 1
302
298
 
303
299
  bar.end()
304
- return self.cache("motion", mobj, result[:index])
300
+ return self.cache(result[:index], "motion", mobj)
305
301
 
306
302
  def subtitle(
307
303
  self,
@@ -142,9 +142,7 @@ def parse_export(export: str, log: Log) -> dict[str, Any]:
142
142
  log.error(f"'{name}': Export must be [{', '.join([s for s in parsing.keys()])}]")
143
143
 
144
144
 
145
- def edit_media(
146
- paths: list[str], ffmpeg: FFmpeg, args: Args, temp: str, log: Log
147
- ) -> None:
145
+ def edit_media(paths: list[str], ffmpeg: FFmpeg, args: Args, log: Log) -> None:
148
146
  bar = Bar(args.progress)
149
147
  tl = None
150
148
 
@@ -203,7 +201,7 @@ def edit_media(
203
201
  samplerate = args.sample_rate
204
202
 
205
203
  if tl is None:
206
- tl = make_timeline(sources, args, samplerate, bar, temp, log)
204
+ tl = make_timeline(sources, args, samplerate, bar, log)
207
205
 
208
206
  if export["export"] == "timeline":
209
207
  from auto_editor.formats.json import make_json_timeline
@@ -214,7 +212,7 @@ def edit_media(
214
212
  if args.preview:
215
213
  from auto_editor.preview import preview
216
214
 
217
- preview(tl, temp, log)
215
+ preview(tl, log)
218
216
  return
219
217
 
220
218
  if export["export"] == "json":
@@ -263,22 +261,21 @@ def edit_media(
263
261
  sub_output = []
264
262
  apply_later = False
265
263
 
266
- ensure = Ensure(ffmpeg, bar, samplerate, temp, log)
267
-
264
+ ensure = Ensure(ffmpeg, bar, samplerate, log)
268
265
  if ctr.default_sub != "none" and not args.sn:
269
- sub_output = make_new_subtitles(tl, ensure, temp)
266
+ sub_output = make_new_subtitles(tl, ensure, log.temp)
270
267
 
271
268
  if ctr.default_aud != "none":
272
- audio_output = make_new_audio(tl, ensure, args, ffmpeg, bar, temp, log)
269
+ audio_output = make_new_audio(tl, ensure, args, ffmpeg, bar, log)
273
270
 
274
271
  if ctr.default_vid != "none":
275
272
  if tl.v:
276
- out_path, apply_later = render_av(ffmpeg, tl, args, bar, ctr, temp, log)
273
+ out_path, apply_later = render_av(ffmpeg, tl, args, bar, ctr, log)
277
274
  visual_output.append((True, out_path))
278
275
 
279
276
  for v, vid in enumerate(src.videos, start=1):
280
277
  if ctr.allow_image and vid.codec in ("png", "mjpeg", "webp"):
281
- out_path = os.path.join(temp, f"{v}.{vid.codec}")
278
+ out_path = os.path.join(log.temp, f"{v}.{vid.codec}")
282
279
  # fmt: off
283
280
  ffmpeg.run(["-i", f"{src.path}", "-map", "0:v", "-map", "-0:V",
284
281
  "-c", "copy", out_path])
@@ -297,7 +294,6 @@ def edit_media(
297
294
  tl.tb,
298
295
  args,
299
296
  src,
300
- temp,
301
297
  log,
302
298
  )
303
299
 
@@ -149,6 +149,7 @@ class VideoStream:
149
149
  class AudioStream:
150
150
  codec: str
151
151
  samplerate: int
152
+ layout: str
152
153
  channels: int
153
154
  duration: float
154
155
  bitrate: int
@@ -195,7 +196,7 @@ def initFileInfo(path: str, log: Log) -> FileInfo:
195
196
  try:
196
197
  cont = av.open(path, "r")
197
198
  except av.error.FileNotFoundError:
198
- log.error(f"Could not find '{path}'")
199
+ log.error(f"Input file doesn't exist: {path}")
199
200
  except av.error.IsADirectoryError:
200
201
  log.error(f"Expected a media file, but got a directory: {path}")
201
202
  except av.error.InvalidDataError:
@@ -232,7 +233,7 @@ def initFileInfo(path: str, log: Log) -> FileInfo:
232
233
  vdur,
233
234
  sar,
234
235
  v.time_base,
235
- cc.pix_fmt,
236
+ getattr(v.format, "name", None),
236
237
  cc.color_range,
237
238
  cc.colorspace,
238
239
  cc.color_primaries,
@@ -252,6 +253,7 @@ def initFileInfo(path: str, log: Log) -> FileInfo:
252
253
  AudioStream(
253
254
  a_cc.name,
254
255
  0 if a_cc.sample_rate is None else a_cc.sample_rate,
256
+ a.layout.name,
255
257
  a_cc.channels,
256
258
  adur,
257
259
  0 if a_cc.bit_rate is None else a_cc.bit_rate,
@@ -1487,12 +1487,12 @@ def edit_audio(
1487
1487
  mincut: int = 6,
1488
1488
  minclip: int = 3,
1489
1489
  ) -> np.ndarray:
1490
- if "@levels" not in env or "@filesetup" not in env:
1490
+ if "@levels" not in env:
1491
1491
  raise MyError("Can't use `audio` if there's no input media")
1492
1492
 
1493
1493
  levels = env["@levels"]
1494
- src = env["@filesetup"].src
1495
- strict = env["@filesetup"].strict
1494
+ src = levels.src
1495
+ strict = levels.strict
1496
1496
 
1497
1497
  stream_data: NDArray[np.bool_] | None = None
1498
1498
  if stream == Sym("all"):
@@ -1531,11 +1531,10 @@ def edit_motion(
1531
1531
  raise MyError("Can't use `motion` if there's no input media")
1532
1532
 
1533
1533
  levels = env["@levels"]
1534
- strict = env["@filesetup"].strict
1535
1534
  try:
1536
1535
  return levels.motion(stream, blur, width) >= threshold
1537
1536
  except LevelError as e:
1538
- return raise_(e) if strict else levels.all()
1537
+ return raise_(e) if levels.strict else levels.all()
1539
1538
 
1540
1539
 
1541
1540
  def edit_subtitle(pattern, stream=0, **kwargs):
@@ -1543,7 +1542,6 @@ def edit_subtitle(pattern, stream=0, **kwargs):
1543
1542
  raise MyError("Can't use `subtitle` if there's no input media")
1544
1543
 
1545
1544
  levels = env["@levels"]
1546
- strict = env["@filesetup"].strict
1547
1545
  if "ignore-case" not in kwargs:
1548
1546
  kwargs["ignore-case"] = False
1549
1547
  if "max-count" not in kwargs:
@@ -1553,7 +1551,7 @@ def edit_subtitle(pattern, stream=0, **kwargs):
1553
1551
  try:
1554
1552
  return levels.subtitle(pattern, stream, ignore_case, max_count)
1555
1553
  except LevelError as e:
1556
- return raise_(e) if strict else levels.all()
1554
+ return raise_(e) if levels.strict else levels.all()
1557
1555
 
1558
1556
 
1559
1557
  def my_eval(env: Env, node: object) -> Any:
@@ -1,15 +1,18 @@
1
1
  from __future__ import annotations
2
2
 
3
- from collections.abc import Callable
4
3
  from dataclasses import dataclass
5
4
  from fractions import Fraction
6
- from typing import Any
5
+ from typing import TYPE_CHECKING
7
6
 
8
7
  from numpy import float64
9
8
 
10
9
  from .data_structs import Sym, print_str
11
10
  from .err import MyError
12
11
 
12
+ if TYPE_CHECKING:
13
+ from collections.abc import Callable
14
+ from typing import Any
15
+
13
16
 
14
17
  @dataclass(slots=True)
15
18
  class Contract:
@@ -6,7 +6,7 @@ from typing import TYPE_CHECKING, NamedTuple
6
6
 
7
7
  import numpy as np
8
8
 
9
- from auto_editor.analyze import FileSetup, Levels
9
+ from auto_editor.analyze import Levels
10
10
  from auto_editor.ffwrapper import FileInfo
11
11
  from auto_editor.lang.palet import Lexer, Parser, env, interpret, is_boolarr
12
12
  from auto_editor.lib.data_structs import print_str
@@ -71,44 +71,6 @@ def make_av(src: FileInfo, all_clips: list[list[Clip]]) -> tuple[VSpace, ASpace]
71
71
  return vtl, atl
72
72
 
73
73
 
74
- def run_interpreter_for_edit_option(
75
- text: str, filesetup: FileSetup
76
- ) -> NDArray[np.bool_]:
77
- src = filesetup.src
78
- tb = filesetup.tb
79
- bar = filesetup.bar
80
- temp = filesetup.temp
81
- log = filesetup.log
82
-
83
- try:
84
- parser = Parser(Lexer("`--edit`", text))
85
- if log.is_debug:
86
- log.debug(f"edit: {parser}")
87
-
88
- env["timebase"] = tb
89
- env["@levels"] = Levels(src, tb, bar, temp, log)
90
- env["@filesetup"] = filesetup
91
-
92
- results = interpret(env, parser)
93
-
94
- if len(results) == 0:
95
- raise MyError("Expression in --edit must return a bool-array, got nothing")
96
-
97
- result = results[-1]
98
- if callable(result):
99
- result = result()
100
-
101
- if not is_boolarr(result):
102
- raise MyError(
103
- f"Expression in --edit must return a bool-array, got {print_str(result)}"
104
- )
105
- except MyError as e:
106
- log.error(e)
107
-
108
- assert isinstance(result, np.ndarray)
109
- return result
110
-
111
-
112
74
  def make_sane_timebase(fps: Fraction) -> Fraction:
113
75
  tb = round(fps, 2)
114
76
 
@@ -140,7 +102,6 @@ def make_timeline(
140
102
  args: Args,
141
103
  sr: int,
142
104
  bar: Bar,
143
- temp: str,
144
105
  log: Log,
145
106
  ) -> v3:
146
107
  inp = None if not sources else sources[0]
@@ -159,20 +120,40 @@ def make_timeline(
159
120
  except CoerceError as e:
160
121
  log.error(e)
161
122
 
162
- method = args.edit_based_on
163
-
164
123
  has_loud = np.array([], dtype=np.bool_)
165
124
  src_index = np.array([], dtype=np.int32)
166
125
  concat = np.concatenate
167
126
 
168
127
  for i, src in enumerate(sources):
169
- filesetup = FileSetup(src, len(sources) < 2, tb, bar, temp, log)
128
+ try:
129
+ parser = Parser(Lexer("`--edit`", args.edit_based_on))
130
+ if log.is_debug:
131
+ log.debug(f"edit: {parser}")
132
+
133
+ env["timebase"] = tb
134
+ env["@levels"] = Levels(src, tb, bar, args.no_cache, log, len(sources) < 2)
135
+
136
+ results = interpret(env, parser)
137
+
138
+ if len(results) == 0:
139
+ log.error("Expression in --edit must return a bool-array, got nothing")
140
+
141
+ result = results[-1]
142
+ if callable(result):
143
+ result = result()
144
+ except MyError as e:
145
+ log.error(e)
146
+
147
+ if not is_boolarr(result):
148
+ log.error(
149
+ f"Expression in --edit must return a bool-array, got {print_str(result)}"
150
+ )
151
+ assert isinstance(result, np.ndarray)
170
152
 
171
- edit_result = run_interpreter_for_edit_option(method, filesetup)
172
- mut_margin(edit_result, start_margin, end_margin)
153
+ mut_margin(result, start_margin, end_margin)
173
154
 
174
- has_loud = concat((has_loud, edit_result))
175
- src_index = concat((src_index, np.full(len(edit_result), i, dtype=np.int32)))
155
+ has_loud = concat((has_loud, result))
156
+ src_index = concat((src_index, np.full(len(result), i, dtype=np.int32)))
176
157
 
177
158
  # Setup for handling custom speeds
178
159
  speed_index = has_loud.astype(np.uint)
@@ -19,7 +19,6 @@ class Ensure:
19
19
  _ffmpeg: FFmpeg
20
20
  _bar: Bar
21
21
  _sr: int
22
- temp: str
23
22
  log: Log
24
23
  _audios: list[tuple[FileInfo, int]] = field(default_factory=list)
25
24
  _subtitles: list[tuple[FileInfo, int, str]] = field(default_factory=list)
@@ -33,7 +32,7 @@ class Ensure:
33
32
  label = len(self._audios) - 1
34
33
  first_time = True
35
34
 
36
- out_path = os.path.join(self.temp, f"{label:x}.wav")
35
+ out_path = os.path.join(self.log.temp, f"{label:x}.wav")
37
36
 
38
37
  if first_time:
39
38
  sample_rate = self._sr
@@ -83,7 +82,7 @@ class Ensure:
83
82
  self._subtitles.append((src, stream, ext))
84
83
  first_time = True
85
84
 
86
- out_path = os.path.join(self.temp, f"{stream}s.{ext}")
85
+ out_path = os.path.join(self.log.temp, f"{stream}s.{ext}")
87
86
 
88
87
  if first_time:
89
88
  self.log.debug(f"Making external subtitle: {out_path}")
@@ -119,7 +118,6 @@ def mux_quality_media(
119
118
  tb: Fraction,
120
119
  args: Args,
121
120
  src: FileInfo,
122
- temp: str,
123
121
  log: Log,
124
122
  ) -> None:
125
123
  v_tracks = len(visual_output)
@@ -142,7 +140,7 @@ def mux_quality_media(
142
140
  cmd.extend(["-i", path])
143
141
  else:
144
142
  # Merge all the audio a_tracks into one.
145
- new_a_file = os.path.join(temp, "new_audio.wav")
143
+ new_a_file = os.path.join(log.temp, "new_audio.wav")
146
144
  if a_tracks > 1:
147
145
  new_cmd = []
148
146
  for path in audio_output:
@@ -48,7 +48,7 @@ def all_cuts(tl: v3, in_len: int) -> list[int]:
48
48
  return cut_lens
49
49
 
50
50
 
51
- def preview(tl: v3, temp: str, log: Log) -> None:
51
+ def preview(tl: v3, log: Log) -> None:
52
52
  log.conwrite("")
53
53
  tb = tl.tb
54
54
 
@@ -65,7 +65,7 @@ def preview(tl: v3, temp: str, log: Log) -> None:
65
65
 
66
66
  in_len = 0
67
67
  for src in all_sources:
68
- in_len += Levels(src, tb, Bar("none"), temp, log).media_length
68
+ in_len += Levels(src, tb, Bar("none"), False, log, False).media_length
69
69
 
70
70
  out_len = tl.out_len()
71
71
 
@@ -166,7 +166,7 @@ def apply_audio_normalization(
166
166
 
167
167
 
168
168
  def make_new_audio(
169
- tl: v3, ensure: Ensure, args: Args, ffmpeg: FFmpeg, bar: Bar, temp: str, log: Log
169
+ tl: v3, ensure: Ensure, args: Args, ffmpeg: FFmpeg, bar: Bar, log: Log
170
170
  ) -> list[str]:
171
171
  sr = tl.sr
172
172
  tb = tl.tb
@@ -176,6 +176,7 @@ def make_new_audio(
176
176
  norm = parse_norm(args.audio_normalize, log)
177
177
 
178
178
  af_tick = 0
179
+ temp = log.temp
179
180
 
180
181
  if not tl.a or not tl.a[0]:
181
182
  log.error("Trying to render empty audio timeline")
@@ -105,13 +105,7 @@ def make_image_cache(tl: v3) -> dict[tuple[FileInfo, int], np.ndarray]:
105
105
 
106
106
 
107
107
  def render_av(
108
- ffmpeg: FFmpeg,
109
- tl: v3,
110
- args: Args,
111
- bar: Bar,
112
- ctr: Container,
113
- temp: str,
114
- log: Log,
108
+ ffmpeg: FFmpeg, tl: v3, args: Args, bar: Bar, ctr: Container, log: Log
115
109
  ) -> tuple[str, bool]:
116
110
  src = tl.src
117
111
  cns: dict[FileInfo, av.container.InputContainer] = {}
@@ -121,6 +115,7 @@ def render_av(
121
115
 
122
116
  target_pix_fmt = "yuv420p" # Reasonable default
123
117
  img_cache = make_image_cache(tl)
118
+ temp = log.temp
124
119
 
125
120
  first_src: FileInfo | None = None
126
121
  for src in tl.sources:
@@ -47,7 +47,7 @@ class VideoJson(TypedDict):
47
47
  class AudioJson(TypedDict):
48
48
  codec: str
49
49
  samplerate: int
50
- channels: int
50
+ layout: str
51
51
  duration: float
52
52
  bitrate: int
53
53
  lang: str | None
@@ -150,8 +150,8 @@ def main(sys_args: list[str] = sys.argv[1:]) -> None:
150
150
  for track, a in enumerate(src.audios):
151
151
  aud: AudioJson = {
152
152
  "codec": a.codec,
153
+ "layout": a.layout,
153
154
  "samplerate": a.samplerate,
154
- "channels": a.channels,
155
155
  "duration": a.duration,
156
156
  "bitrate": a.bitrate,
157
157
  "lang": a.lang,