auto-editor 26.3.0__py3-none-any.whl → 26.3.2__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
auto_editor/__init__.py CHANGED
@@ -1 +1 @@
1
- __version__ = "26.3.0"
1
+ __version__ = "26.3.2"
auto_editor/__main__.py CHANGED
@@ -3,6 +3,7 @@
3
3
  import platform as plat
4
4
  import re
5
5
  import sys
6
+ from io import StringIO
6
7
  from os import environ
7
8
  from os.path import exists, isdir, isfile, lexists, splitext
8
9
  from subprocess import run
@@ -174,6 +175,27 @@ def main_options(parser: ArgumentParser) -> ArgumentParser:
174
175
  flag=True,
175
176
  help="Show stats on how the input will be cut and halt",
176
177
  )
178
+ parser.add_text("Container Settings:")
179
+ parser.add_argument(
180
+ "-sn",
181
+ flag=True,
182
+ help="Disable the inclusion of subtitle streams in the output file",
183
+ )
184
+ parser.add_argument(
185
+ "-dn",
186
+ flag=True,
187
+ help="Disable the inclusion of data streams in the output file",
188
+ )
189
+ parser.add_argument(
190
+ "--fragmented",
191
+ flag=True,
192
+ help="Use fragmented mp4/mov to allow playback before video is complete\nSee: https://ffmpeg.org/ffmpeg-formats.html#Fragmentation",
193
+ )
194
+ parser.add_argument(
195
+ "--no-fragmented",
196
+ flag=True,
197
+ help="Do not use fragmented mp4/mov for better compatibility (default)",
198
+ )
177
199
  parser.add_text("Video Rendering:")
178
200
  parser.add_argument(
179
201
  "--video-codec",
@@ -230,16 +252,6 @@ def main_options(parser: ArgumentParser) -> ArgumentParser:
230
252
  help="Apply audio rendering to all audio tracks. Applied right before rendering the output file",
231
253
  )
232
254
  parser.add_text("Miscellaneous:")
233
- parser.add_argument(
234
- "-sn",
235
- flag=True,
236
- help="Disable the inclusion of subtitle streams in the output file",
237
- )
238
- parser.add_argument(
239
- "-dn",
240
- flag=True,
241
- help="Disable the inclusion of data streams in the output file",
242
- )
243
255
  parser.add_argument(
244
256
  "--config", flag=True, help="When set, look for `config.pal` and run it"
245
257
  )
@@ -320,23 +332,26 @@ def main() -> None:
320
332
  )
321
333
 
322
334
  if args.version:
323
- print(auto_editor.__version__)
324
- return
335
+ return print(auto_editor.__version__)
325
336
 
326
337
  if args.debug and not args.input:
327
- print(f"OS: {plat.system()} {plat.release()} {plat.machine().lower()}")
328
- print(f"Python: {plat.python_version()}")
329
-
338
+ buf = StringIO()
339
+ buf.write(f"OS: {plat.system()} {plat.release()} {plat.machine().lower()}\n")
340
+ buf.write(f"Python: {plat.python_version()}\nPyAV: ")
330
341
  try:
331
342
  import av
332
-
333
- license = av._core.library_meta["libavcodec"]["license"]
334
- print(f"PyAV: {av.__version__} ({license})")
335
343
  except (ModuleNotFoundError, ImportError):
336
- print("PyAV: error")
337
-
338
- print(f"Auto-Editor: {auto_editor.__version__}")
339
- return
344
+ buf.write("not found")
345
+ else:
346
+ try:
347
+ buf.write(f"{av.__version__} ")
348
+ license = av._core.library_meta["libavcodec"]["license"]
349
+ buf.write(f"({license})")
350
+ except AttributeError:
351
+ buf.write("error")
352
+
353
+ buf.write(f"\nAuto-Editor: {auto_editor.__version__}")
354
+ return print(buf.getvalue())
340
355
 
341
356
  if not args.input:
342
357
  log.error("You need to give auto-editor an input file.")
auto_editor/analyze.py CHANGED
@@ -19,7 +19,6 @@ from auto_editor import __version__
19
19
  if TYPE_CHECKING:
20
20
  from collections.abc import Iterator, Sequence
21
21
  from fractions import Fraction
22
- from pathlib import Path
23
22
 
24
23
  from numpy.typing import NDArray
25
24
 
@@ -28,7 +27,7 @@ if TYPE_CHECKING:
28
27
  from auto_editor.utils.log import Log
29
28
 
30
29
 
31
- __all__ = ("LevelError", "Levels", "iter_audio", "iter_motion")
30
+ __all__ = ("LevelError", "initLevels", "iter_audio", "iter_motion")
32
31
 
33
32
 
34
33
  class LevelError(Exception):
@@ -153,52 +152,48 @@ def iter_motion(
153
152
  prev_index = index
154
153
 
155
154
 
156
- def obj_tag(path: Path, kind: str, tb: Fraction, obj: Sequence[object]) -> str:
157
- mod_time = int(path.stat().st_mtime)
158
- key = f"{path.name}:{mod_time:x}:{tb}:" + ",".join(f"{v}" for v in obj)
159
- part1 = sha1(key.encode()).hexdigest()[:16]
160
-
161
- return f"{part1}{kind}"
162
-
163
-
164
155
  @dataclass(slots=True)
165
156
  class Levels:
166
- src: FileInfo
157
+ container: av.container.InputContainer
158
+ name: str
159
+ mod_time: int
167
160
  tb: Fraction
168
161
  bar: Bar
169
162
  no_cache: bool
170
163
  log: Log
171
- strict: bool
172
164
 
173
165
  @property
174
166
  def media_length(self) -> int:
175
- if self.src.audios:
167
+ container = self.container
168
+ if container.streams.audio:
176
169
  if (arr := self.read_cache("audio", (0,))) is not None:
177
170
  return len(arr)
178
171
 
179
- with av.open(self.src.path, "r") as container:
180
- audio_stream = container.streams.audio[0]
181
- self.log.experimental(audio_stream.codec)
182
- result = sum(1 for _ in iter_audio(audio_stream, self.tb))
183
-
172
+ audio_stream = container.streams.audio[0]
173
+ result = sum(1 for _ in iter_audio(audio_stream, self.tb))
174
+ container.seek(0)
184
175
  self.log.debug(f"Audio Length: {result}")
185
176
  return result
186
177
 
187
178
  # If there's no audio, get length in video metadata.
188
- with av.open(self.src.path) as container:
189
- if len(container.streams.video) == 0:
190
- self.log.error("Could not get media duration")
191
-
192
- video = container.streams.video[0]
179
+ if not container.streams.video:
180
+ self.log.error("Could not get media duration")
193
181
 
194
- if video.duration is None or video.time_base is None:
195
- dur = 0
196
- else:
197
- dur = int(video.duration * video.time_base * self.tb)
198
- self.log.debug(f"Video duration: {dur}")
182
+ video = container.streams.video[0]
183
+ if video.duration is None or video.time_base is None:
184
+ dur = 0
185
+ else:
186
+ dur = int(video.duration * video.time_base * self.tb)
187
+ self.log.debug(f"Video duration: {dur}")
188
+ container.seek(0)
199
189
 
200
190
  return dur
201
191
 
192
+ def obj_tag(self, kind: str, obj: Sequence[object]) -> str:
193
+ mod_time = self.mod_time
194
+ key = f"{self.name}:{mod_time:x}:{self.tb}:" + ",".join(f"{v}" for v in obj)
195
+ return f"{sha1(key.encode()).hexdigest()[:16]}{kind}"
196
+
202
197
  def none(self) -> NDArray[np.bool_]:
203
198
  return np.ones(self.media_length, dtype=np.bool_)
204
199
 
@@ -209,7 +204,7 @@ class Levels:
209
204
  if self.no_cache:
210
205
  return None
211
206
 
212
- key = obj_tag(self.src.path, kind, self.tb, obj)
207
+ key = self.obj_tag(kind, obj)
213
208
  cache_file = os.path.join(gettempdir(), f"ae-{__version__}", f"{key}.npz")
214
209
 
215
210
  try:
@@ -224,11 +219,8 @@ class Levels:
224
219
  return arr
225
220
 
226
221
  workdir = os.path.join(gettempdir(), f"ae-{__version__}")
227
- if not os.path.exists(workdir):
228
- os.mkdir(workdir)
229
-
230
- key = obj_tag(self.src.path, kind, self.tb, obj)
231
- cache_file = os.path.join(workdir, f"{key}.npz")
222
+ os.makedirs(workdir, exist_ok=True)
223
+ cache_file = os.path.join(workdir, f"{self.obj_tag(kind, obj)}.npz")
232
224
 
233
225
  try:
234
226
  np.savez(cache_file, data=arr)
@@ -254,18 +246,15 @@ class Levels:
254
246
  return arr
255
247
 
256
248
  def audio(self, stream: int) -> NDArray[np.float32]:
257
- if stream >= len(self.src.audios):
249
+ container = self.container
250
+ if stream >= len(container.streams.audio):
258
251
  raise LevelError(f"audio: audio stream '{stream}' does not exist.")
259
252
 
260
253
  if (arr := self.read_cache("audio", (stream,))) is not None:
261
254
  return arr
262
255
 
263
- container = av.open(self.src.path, "r")
264
256
  audio = container.streams.audio[stream]
265
257
 
266
- if audio.codec.experimental:
267
- self.log.error(f"`{audio.codec.name}` is an experimental codec")
268
-
269
258
  if audio.duration is not None and audio.time_base is not None:
270
259
  inaccurate_dur = int(audio.duration * audio.time_base * self.tb)
271
260
  elif container.duration is not None:
@@ -290,35 +279,30 @@ class Levels:
290
279
  index += 1
291
280
 
292
281
  bar.end()
282
+ container.seek(0)
293
283
  assert len(result) > 0
294
284
  return self.cache(result[:index], "audio", (stream,))
295
285
 
296
286
  def motion(self, stream: int, blur: int, width: int) -> NDArray[np.float32]:
297
- if stream >= len(self.src.videos):
287
+ container = self.container
288
+ if stream >= len(container.streams.video):
298
289
  raise LevelError(f"motion: video stream '{stream}' does not exist.")
299
290
 
300
291
  mobj = (stream, width, blur)
301
292
  if (arr := self.read_cache("motion", mobj)) is not None:
302
293
  return arr
303
294
 
304
- container = av.open(self.src.path, "r")
305
295
  video = container.streams.video[stream]
306
-
307
- if video.codec.experimental:
308
- self.log.experimental(video.codec)
309
-
310
296
  inaccurate_dur = (
311
297
  1024
312
298
  if video.duration is None or video.time_base is None
313
299
  else int(video.duration * video.time_base * self.tb)
314
300
  )
315
-
316
301
  bar = self.bar
317
302
  bar.start(inaccurate_dur, "Analyzing motion")
318
303
 
319
304
  result: NDArray[np.float32] = np.zeros(inaccurate_dur, dtype=np.float32)
320
305
  index = 0
321
-
322
306
  for value in iter_motion(video, self.tb, blur, width):
323
307
  if index > len(result) - 1:
324
308
  result = np.concatenate(
@@ -329,6 +313,7 @@ class Levels:
329
313
  index += 1
330
314
 
331
315
  bar.end()
316
+ container.seek(0)
332
317
  return self.cache(result[:index], "motion", mobj)
333
318
 
334
319
  def subtitle(
@@ -338,7 +323,8 @@ class Levels:
338
323
  ignore_case: bool,
339
324
  max_count: int | None,
340
325
  ) -> NDArray[np.bool_]:
341
- if stream >= len(self.src.subtitles):
326
+ container = self.container
327
+ if stream >= len(container.streams.subtitles):
342
328
  raise LevelError(f"subtitle: subtitle stream '{stream}' does not exist.")
343
329
 
344
330
  try:
@@ -346,9 +332,7 @@ class Levels:
346
332
  re_pattern = re.compile(pattern, flags)
347
333
  except re.error as e:
348
334
  self.log.error(e)
349
-
350
335
  try:
351
- container = av.open(self.src.path, "r")
352
336
  subtitle_stream = container.streams.subtitles[stream]
353
337
  assert isinstance(subtitle_stream.time_base, Fraction)
354
338
  except Exception as e:
@@ -399,6 +383,17 @@ class Levels:
399
383
  result[san_start:san_end] = 1
400
384
  count += 1
401
385
 
402
- container.close()
403
-
386
+ container.seek(0)
404
387
  return result
388
+
389
+
390
+ def initLevels(
391
+ src: FileInfo, tb: Fraction, bar: Bar, no_cache: bool, log: Log
392
+ ) -> Levels:
393
+ try:
394
+ container = av.open(src.path)
395
+ except av.FFmpegError as e:
396
+ log.error(e)
397
+
398
+ mod_time = int(src.path.stat().st_mtime)
399
+ return Levels(container, src.path.name, mod_time, tb, bar, no_cache, log)
@@ -128,7 +128,7 @@ def main(sys_args: list[str] = sys.argv[1:]) -> None:
128
128
  except ParserError as e:
129
129
  log.error(e)
130
130
 
131
- levels = Levels(src, tb, bar, False, log, strict=True)
131
+ levels = initLevels(src, tb, bar, False, log)
132
132
  try:
133
133
  if method == "audio":
134
134
  if (arr := levels.read_cache("audio", (obj["stream"],))) is not None:
@@ -136,7 +136,6 @@ def main(sys_args: list[str] = sys.argv[1:]) -> None:
136
136
  else:
137
137
  container = av.open(src.path, "r")
138
138
  audio_stream = container.streams.audio[obj["stream"]]
139
- log.experimental(audio_stream.codec)
140
139
 
141
140
  values = []
142
141
 
@@ -158,7 +157,6 @@ def main(sys_args: list[str] = sys.argv[1:]) -> None:
158
157
  else:
159
158
  container = av.open(src.path, "r")
160
159
  video_stream = container.streams.video[obj["stream"]]
161
- log.experimental(video_stream.codec)
162
160
 
163
161
  values = []
164
162
 
auto_editor/cmds/repl.py CHANGED
@@ -6,7 +6,7 @@ from fractions import Fraction
6
6
  from os import environ
7
7
 
8
8
  import auto_editor
9
- from auto_editor.analyze import Levels
9
+ from auto_editor.analyze import initLevels
10
10
  from auto_editor.ffwrapper import initFileInfo
11
11
  from auto_editor.lang.palet import ClosingError, Lexer, Parser, env, interpret
12
12
  from auto_editor.lang.stdenv import make_standard_env
@@ -61,12 +61,11 @@ def main(sys_args: list[str] = sys.argv[1:]) -> None:
61
61
 
62
62
  if args.input:
63
63
  log = Log(quiet=True, temp_dir=args.temp_dir)
64
- strict = len(args.input) < 2
65
64
  sources = [initFileInfo(path, log) for path in args.input]
66
65
  src = sources[0]
67
66
  tb = src.get_fps() if args.timebase is None else args.timebase
68
67
  env["timebase"] = tb
69
- env["@levels"] = Levels(src, tb, initBar("modern"), False, log, strict)
68
+ env["@levels"] = initLevels(src, tb, initBar("modern"), False, log)
70
69
 
71
70
  env.update(make_standard_env())
72
71
  print(f"Auto-Editor {auto_editor.__version__}")