auto-editor 26.3.1__tar.gz → 26.3.2__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 (62) hide show
  1. {auto_editor-26.3.1 → auto_editor-26.3.2}/PKG-INFO +1 -1
  2. auto_editor-26.3.2/auto_editor/__init__.py +1 -0
  3. {auto_editor-26.3.1 → auto_editor-26.3.2}/auto_editor/__main__.py +37 -22
  4. {auto_editor-26.3.1 → auto_editor-26.3.2}/auto_editor/analyze.py +47 -45
  5. {auto_editor-26.3.1 → auto_editor-26.3.2}/auto_editor/cmds/levels.py +1 -1
  6. {auto_editor-26.3.1 → auto_editor-26.3.2}/auto_editor/cmds/repl.py +2 -3
  7. {auto_editor-26.3.1 → auto_editor-26.3.2}/auto_editor/cmds/test.py +328 -384
  8. {auto_editor-26.3.1 → auto_editor-26.3.2}/auto_editor/edit.py +19 -2
  9. {auto_editor-26.3.1 → auto_editor-26.3.2}/auto_editor/lang/palet.py +23 -27
  10. {auto_editor-26.3.1 → auto_editor-26.3.2}/auto_editor/make_layers.py +28 -17
  11. {auto_editor-26.3.1 → auto_editor-26.3.2}/auto_editor/preview.py +3 -2
  12. {auto_editor-26.3.1 → auto_editor-26.3.2}/auto_editor/utils/types.py +2 -0
  13. {auto_editor-26.3.1 → auto_editor-26.3.2}/auto_editor.egg-info/PKG-INFO +1 -1
  14. auto_editor-26.3.1/auto_editor/__init__.py +0 -1
  15. {auto_editor-26.3.1 → auto_editor-26.3.2}/LICENSE +0 -0
  16. {auto_editor-26.3.1 → auto_editor-26.3.2}/README.md +0 -0
  17. {auto_editor-26.3.1 → auto_editor-26.3.2}/auto_editor/cmds/__init__.py +0 -0
  18. {auto_editor-26.3.1 → auto_editor-26.3.2}/auto_editor/cmds/cache.py +0 -0
  19. {auto_editor-26.3.1 → auto_editor-26.3.2}/auto_editor/cmds/desc.py +0 -0
  20. {auto_editor-26.3.1 → auto_editor-26.3.2}/auto_editor/cmds/info.py +0 -0
  21. {auto_editor-26.3.1 → auto_editor-26.3.2}/auto_editor/cmds/palet.py +0 -0
  22. {auto_editor-26.3.1 → auto_editor-26.3.2}/auto_editor/cmds/subdump.py +0 -0
  23. {auto_editor-26.3.1 → auto_editor-26.3.2}/auto_editor/ffwrapper.py +0 -0
  24. {auto_editor-26.3.1 → auto_editor-26.3.2}/auto_editor/formats/__init__.py +0 -0
  25. {auto_editor-26.3.1 → auto_editor-26.3.2}/auto_editor/formats/fcp11.py +0 -0
  26. {auto_editor-26.3.1 → auto_editor-26.3.2}/auto_editor/formats/fcp7.py +0 -0
  27. {auto_editor-26.3.1 → auto_editor-26.3.2}/auto_editor/formats/json.py +0 -0
  28. {auto_editor-26.3.1 → auto_editor-26.3.2}/auto_editor/formats/shotcut.py +0 -0
  29. {auto_editor-26.3.1 → auto_editor-26.3.2}/auto_editor/formats/utils.py +0 -0
  30. {auto_editor-26.3.1 → auto_editor-26.3.2}/auto_editor/help.py +0 -0
  31. {auto_editor-26.3.1 → auto_editor-26.3.2}/auto_editor/lang/__init__.py +0 -0
  32. {auto_editor-26.3.1 → auto_editor-26.3.2}/auto_editor/lang/json.py +0 -0
  33. {auto_editor-26.3.1 → auto_editor-26.3.2}/auto_editor/lang/libintrospection.py +0 -0
  34. {auto_editor-26.3.1 → auto_editor-26.3.2}/auto_editor/lang/libmath.py +0 -0
  35. {auto_editor-26.3.1 → auto_editor-26.3.2}/auto_editor/lang/stdenv.py +0 -0
  36. {auto_editor-26.3.1 → auto_editor-26.3.2}/auto_editor/lib/__init__.py +0 -0
  37. {auto_editor-26.3.1 → auto_editor-26.3.2}/auto_editor/lib/contracts.py +0 -0
  38. {auto_editor-26.3.1 → auto_editor-26.3.2}/auto_editor/lib/data_structs.py +0 -0
  39. {auto_editor-26.3.1 → auto_editor-26.3.2}/auto_editor/lib/err.py +0 -0
  40. {auto_editor-26.3.1 → auto_editor-26.3.2}/auto_editor/output.py +0 -0
  41. {auto_editor-26.3.1 → auto_editor-26.3.2}/auto_editor/render/__init__.py +0 -0
  42. {auto_editor-26.3.1 → auto_editor-26.3.2}/auto_editor/render/audio.py +0 -0
  43. {auto_editor-26.3.1 → auto_editor-26.3.2}/auto_editor/render/subtitle.py +0 -0
  44. {auto_editor-26.3.1 → auto_editor-26.3.2}/auto_editor/render/video.py +0 -0
  45. {auto_editor-26.3.1 → auto_editor-26.3.2}/auto_editor/timeline.py +0 -0
  46. {auto_editor-26.3.1 → auto_editor-26.3.2}/auto_editor/utils/__init__.py +0 -0
  47. {auto_editor-26.3.1 → auto_editor-26.3.2}/auto_editor/utils/bar.py +0 -0
  48. {auto_editor-26.3.1 → auto_editor-26.3.2}/auto_editor/utils/chunks.py +0 -0
  49. {auto_editor-26.3.1 → auto_editor-26.3.2}/auto_editor/utils/cmdkw.py +0 -0
  50. {auto_editor-26.3.1 → auto_editor-26.3.2}/auto_editor/utils/container.py +0 -0
  51. {auto_editor-26.3.1 → auto_editor-26.3.2}/auto_editor/utils/func.py +0 -0
  52. {auto_editor-26.3.1 → auto_editor-26.3.2}/auto_editor/utils/log.py +0 -0
  53. {auto_editor-26.3.1 → auto_editor-26.3.2}/auto_editor/vanparse.py +0 -0
  54. {auto_editor-26.3.1 → auto_editor-26.3.2}/auto_editor/wavfile.py +0 -0
  55. {auto_editor-26.3.1 → auto_editor-26.3.2}/auto_editor.egg-info/SOURCES.txt +0 -0
  56. {auto_editor-26.3.1 → auto_editor-26.3.2}/auto_editor.egg-info/dependency_links.txt +0 -0
  57. {auto_editor-26.3.1 → auto_editor-26.3.2}/auto_editor.egg-info/entry_points.txt +0 -0
  58. {auto_editor-26.3.1 → auto_editor-26.3.2}/auto_editor.egg-info/requires.txt +0 -0
  59. {auto_editor-26.3.1 → auto_editor-26.3.2}/auto_editor.egg-info/top_level.txt +0 -0
  60. {auto_editor-26.3.1 → auto_editor-26.3.2}/docs/build.py +0 -0
  61. {auto_editor-26.3.1 → auto_editor-26.3.2}/pyproject.toml +0 -0
  62. {auto_editor-26.3.1 → auto_editor-26.3.2}/setup.cfg +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.2
2
2
  Name: auto-editor
3
- Version: 26.3.1
3
+ Version: 26.3.2
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__ = "26.3.2"
@@ -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.")
@@ -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,51 +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
- result = sum(1 for _ in iter_audio(audio_stream, self.tb))
182
-
172
+ audio_stream = container.streams.audio[0]
173
+ result = sum(1 for _ in iter_audio(audio_stream, self.tb))
174
+ container.seek(0)
183
175
  self.log.debug(f"Audio Length: {result}")
184
176
  return result
185
177
 
186
178
  # If there's no audio, get length in video metadata.
187
- with av.open(self.src.path) as container:
188
- if len(container.streams.video) == 0:
189
- self.log.error("Could not get media duration")
190
-
191
- video = container.streams.video[0]
179
+ if not container.streams.video:
180
+ self.log.error("Could not get media duration")
192
181
 
193
- if video.duration is None or video.time_base is None:
194
- dur = 0
195
- else:
196
- dur = int(video.duration * video.time_base * self.tb)
197
- 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)
198
189
 
199
190
  return dur
200
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
+
201
197
  def none(self) -> NDArray[np.bool_]:
202
198
  return np.ones(self.media_length, dtype=np.bool_)
203
199
 
@@ -208,7 +204,7 @@ class Levels:
208
204
  if self.no_cache:
209
205
  return None
210
206
 
211
- key = obj_tag(self.src.path, kind, self.tb, obj)
207
+ key = self.obj_tag(kind, obj)
212
208
  cache_file = os.path.join(gettempdir(), f"ae-{__version__}", f"{key}.npz")
213
209
 
214
210
  try:
@@ -223,11 +219,8 @@ class Levels:
223
219
  return arr
224
220
 
225
221
  workdir = os.path.join(gettempdir(), f"ae-{__version__}")
226
- if not os.path.exists(workdir):
227
- os.mkdir(workdir)
228
-
229
- key = obj_tag(self.src.path, kind, self.tb, obj)
230
- 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")
231
224
 
232
225
  try:
233
226
  np.savez(cache_file, data=arr)
@@ -253,13 +246,13 @@ class Levels:
253
246
  return arr
254
247
 
255
248
  def audio(self, stream: int) -> NDArray[np.float32]:
256
- if stream >= len(self.src.audios):
249
+ container = self.container
250
+ if stream >= len(container.streams.audio):
257
251
  raise LevelError(f"audio: audio stream '{stream}' does not exist.")
258
252
 
259
253
  if (arr := self.read_cache("audio", (stream,))) is not None:
260
254
  return arr
261
255
 
262
- container = av.open(self.src.path, "r")
263
256
  audio = container.streams.audio[stream]
264
257
 
265
258
  if audio.duration is not None and audio.time_base is not None:
@@ -286,32 +279,30 @@ class Levels:
286
279
  index += 1
287
280
 
288
281
  bar.end()
282
+ container.seek(0)
289
283
  assert len(result) > 0
290
284
  return self.cache(result[:index], "audio", (stream,))
291
285
 
292
286
  def motion(self, stream: int, blur: int, width: int) -> NDArray[np.float32]:
293
- if stream >= len(self.src.videos):
287
+ container = self.container
288
+ if stream >= len(container.streams.video):
294
289
  raise LevelError(f"motion: video stream '{stream}' does not exist.")
295
290
 
296
291
  mobj = (stream, width, blur)
297
292
  if (arr := self.read_cache("motion", mobj)) is not None:
298
293
  return arr
299
294
 
300
- container = av.open(self.src.path, "r")
301
295
  video = container.streams.video[stream]
302
-
303
296
  inaccurate_dur = (
304
297
  1024
305
298
  if video.duration is None or video.time_base is None
306
299
  else int(video.duration * video.time_base * self.tb)
307
300
  )
308
-
309
301
  bar = self.bar
310
302
  bar.start(inaccurate_dur, "Analyzing motion")
311
303
 
312
304
  result: NDArray[np.float32] = np.zeros(inaccurate_dur, dtype=np.float32)
313
305
  index = 0
314
-
315
306
  for value in iter_motion(video, self.tb, blur, width):
316
307
  if index > len(result) - 1:
317
308
  result = np.concatenate(
@@ -322,6 +313,7 @@ class Levels:
322
313
  index += 1
323
314
 
324
315
  bar.end()
316
+ container.seek(0)
325
317
  return self.cache(result[:index], "motion", mobj)
326
318
 
327
319
  def subtitle(
@@ -331,7 +323,8 @@ class Levels:
331
323
  ignore_case: bool,
332
324
  max_count: int | None,
333
325
  ) -> NDArray[np.bool_]:
334
- if stream >= len(self.src.subtitles):
326
+ container = self.container
327
+ if stream >= len(container.streams.subtitles):
335
328
  raise LevelError(f"subtitle: subtitle stream '{stream}' does not exist.")
336
329
 
337
330
  try:
@@ -339,9 +332,7 @@ class Levels:
339
332
  re_pattern = re.compile(pattern, flags)
340
333
  except re.error as e:
341
334
  self.log.error(e)
342
-
343
335
  try:
344
- container = av.open(self.src.path, "r")
345
336
  subtitle_stream = container.streams.subtitles[stream]
346
337
  assert isinstance(subtitle_stream.time_base, Fraction)
347
338
  except Exception as e:
@@ -392,6 +383,17 @@ class Levels:
392
383
  result[san_start:san_end] = 1
393
384
  count += 1
394
385
 
395
- container.close()
396
-
386
+ container.seek(0)
397
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:
@@ -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__}")