auto-editor 26.3.1__tar.gz → 26.3.3__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.3}/PKG-INFO +2 -2
  2. {auto_editor-26.3.1 → auto_editor-26.3.3}/README.md +1 -1
  3. auto_editor-26.3.3/auto_editor/__init__.py +1 -0
  4. {auto_editor-26.3.1 → auto_editor-26.3.3}/auto_editor/__main__.py +181 -55
  5. {auto_editor-26.3.1 → auto_editor-26.3.3}/auto_editor/analyze.py +47 -45
  6. {auto_editor-26.3.1 → auto_editor-26.3.3}/auto_editor/cmds/levels.py +1 -1
  7. {auto_editor-26.3.1 → auto_editor-26.3.3}/auto_editor/cmds/repl.py +2 -3
  8. {auto_editor-26.3.1 → auto_editor-26.3.3}/auto_editor/cmds/test.py +334 -386
  9. {auto_editor-26.3.1 → auto_editor-26.3.3}/auto_editor/edit.py +28 -9
  10. {auto_editor-26.3.1 → auto_editor-26.3.3}/auto_editor/lang/palet.py +23 -27
  11. {auto_editor-26.3.1 → auto_editor-26.3.3}/auto_editor/make_layers.py +30 -18
  12. {auto_editor-26.3.1 → auto_editor-26.3.3}/auto_editor/output.py +2 -2
  13. {auto_editor-26.3.1 → auto_editor-26.3.3}/auto_editor/preview.py +3 -2
  14. {auto_editor-26.3.1 → auto_editor-26.3.3}/auto_editor/render/audio.py +4 -1
  15. {auto_editor-26.3.1 → auto_editor-26.3.3}/auto_editor/render/video.py +1 -1
  16. {auto_editor-26.3.1 → auto_editor-26.3.3}/auto_editor/timeline.py +8 -1
  17. {auto_editor-26.3.1 → auto_editor-26.3.3}/auto_editor/utils/types.py +7 -116
  18. {auto_editor-26.3.1 → auto_editor-26.3.3}/auto_editor.egg-info/PKG-INFO +2 -2
  19. auto_editor-26.3.1/auto_editor/__init__.py +0 -1
  20. {auto_editor-26.3.1 → auto_editor-26.3.3}/LICENSE +0 -0
  21. {auto_editor-26.3.1 → auto_editor-26.3.3}/auto_editor/cmds/__init__.py +0 -0
  22. {auto_editor-26.3.1 → auto_editor-26.3.3}/auto_editor/cmds/cache.py +0 -0
  23. {auto_editor-26.3.1 → auto_editor-26.3.3}/auto_editor/cmds/desc.py +0 -0
  24. {auto_editor-26.3.1 → auto_editor-26.3.3}/auto_editor/cmds/info.py +0 -0
  25. {auto_editor-26.3.1 → auto_editor-26.3.3}/auto_editor/cmds/palet.py +0 -0
  26. {auto_editor-26.3.1 → auto_editor-26.3.3}/auto_editor/cmds/subdump.py +0 -0
  27. {auto_editor-26.3.1 → auto_editor-26.3.3}/auto_editor/ffwrapper.py +0 -0
  28. {auto_editor-26.3.1 → auto_editor-26.3.3}/auto_editor/formats/__init__.py +0 -0
  29. {auto_editor-26.3.1 → auto_editor-26.3.3}/auto_editor/formats/fcp11.py +0 -0
  30. {auto_editor-26.3.1 → auto_editor-26.3.3}/auto_editor/formats/fcp7.py +0 -0
  31. {auto_editor-26.3.1 → auto_editor-26.3.3}/auto_editor/formats/json.py +0 -0
  32. {auto_editor-26.3.1 → auto_editor-26.3.3}/auto_editor/formats/shotcut.py +0 -0
  33. {auto_editor-26.3.1 → auto_editor-26.3.3}/auto_editor/formats/utils.py +0 -0
  34. {auto_editor-26.3.1 → auto_editor-26.3.3}/auto_editor/help.py +0 -0
  35. {auto_editor-26.3.1 → auto_editor-26.3.3}/auto_editor/lang/__init__.py +0 -0
  36. {auto_editor-26.3.1 → auto_editor-26.3.3}/auto_editor/lang/json.py +0 -0
  37. {auto_editor-26.3.1 → auto_editor-26.3.3}/auto_editor/lang/libintrospection.py +0 -0
  38. {auto_editor-26.3.1 → auto_editor-26.3.3}/auto_editor/lang/libmath.py +0 -0
  39. {auto_editor-26.3.1 → auto_editor-26.3.3}/auto_editor/lang/stdenv.py +0 -0
  40. {auto_editor-26.3.1 → auto_editor-26.3.3}/auto_editor/lib/__init__.py +0 -0
  41. {auto_editor-26.3.1 → auto_editor-26.3.3}/auto_editor/lib/contracts.py +0 -0
  42. {auto_editor-26.3.1 → auto_editor-26.3.3}/auto_editor/lib/data_structs.py +0 -0
  43. {auto_editor-26.3.1 → auto_editor-26.3.3}/auto_editor/lib/err.py +0 -0
  44. {auto_editor-26.3.1 → auto_editor-26.3.3}/auto_editor/render/__init__.py +0 -0
  45. {auto_editor-26.3.1 → auto_editor-26.3.3}/auto_editor/render/subtitle.py +0 -0
  46. {auto_editor-26.3.1 → auto_editor-26.3.3}/auto_editor/utils/__init__.py +0 -0
  47. {auto_editor-26.3.1 → auto_editor-26.3.3}/auto_editor/utils/bar.py +0 -0
  48. {auto_editor-26.3.1 → auto_editor-26.3.3}/auto_editor/utils/chunks.py +0 -0
  49. {auto_editor-26.3.1 → auto_editor-26.3.3}/auto_editor/utils/cmdkw.py +0 -0
  50. {auto_editor-26.3.1 → auto_editor-26.3.3}/auto_editor/utils/container.py +0 -0
  51. {auto_editor-26.3.1 → auto_editor-26.3.3}/auto_editor/utils/func.py +0 -0
  52. {auto_editor-26.3.1 → auto_editor-26.3.3}/auto_editor/utils/log.py +0 -0
  53. {auto_editor-26.3.1 → auto_editor-26.3.3}/auto_editor/vanparse.py +0 -0
  54. {auto_editor-26.3.1 → auto_editor-26.3.3}/auto_editor/wavfile.py +0 -0
  55. {auto_editor-26.3.1 → auto_editor-26.3.3}/auto_editor.egg-info/SOURCES.txt +0 -0
  56. {auto_editor-26.3.1 → auto_editor-26.3.3}/auto_editor.egg-info/dependency_links.txt +0 -0
  57. {auto_editor-26.3.1 → auto_editor-26.3.3}/auto_editor.egg-info/entry_points.txt +0 -0
  58. {auto_editor-26.3.1 → auto_editor-26.3.3}/auto_editor.egg-info/requires.txt +0 -0
  59. {auto_editor-26.3.1 → auto_editor-26.3.3}/auto_editor.egg-info/top_level.txt +0 -0
  60. {auto_editor-26.3.1 → auto_editor-26.3.3}/docs/build.py +0 -0
  61. {auto_editor-26.3.1 → auto_editor-26.3.3}/pyproject.toml +0 -0
  62. {auto_editor-26.3.1 → auto_editor-26.3.3}/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.3
4
4
  Summary: Auto-Editor: Effort free video editing!
5
5
  Author-email: WyattBlue <wyattblue@auto-editor.com>
6
6
  License: Unlicense
@@ -21,7 +21,7 @@ Requires-Dist: pyav==14.2.*
21
21
  ---
22
22
 
23
23
  [![Actions Status](https://github.com/wyattblue/auto-editor/workflows/build/badge.svg)](https://github.com/wyattblue/auto-editor/actions)
24
- <a href="https://github.com/psf/black"><img alt="Code style: black" src="https://img.shields.io/badge/code%20style-black-000000.svg"></a>
24
+ [![Ruff](https://img.shields.io/endpoint?url=https://raw.githubusercontent.com/astral-sh/ruff/main/assets/badge/v2.json)](https://github.com/astral-sh/ruff)
25
25
 
26
26
  Before doing the real editing, you first cut out the "dead space" which is typically silence. This is known as a "first pass". Cutting these is a boring task, especially if the video is very long.
27
27
 
@@ -5,7 +5,7 @@
5
5
  ---
6
6
 
7
7
  [![Actions Status](https://github.com/wyattblue/auto-editor/workflows/build/badge.svg)](https://github.com/wyattblue/auto-editor/actions)
8
- <a href="https://github.com/psf/black"><img alt="Code style: black" src="https://img.shields.io/badge/code%20style-black-000000.svg"></a>
8
+ [![Ruff](https://img.shields.io/endpoint?url=https://raw.githubusercontent.com/astral-sh/ruff/main/assets/badge/v2.json)](https://github.com/astral-sh/ruff)
9
9
 
10
10
  Before doing the real editing, you first cut out the "dead space" which is typically silence. This is known as a "first pass". Cutting these is a boring task, especially if the video is very long.
11
11
 
@@ -0,0 +1 @@
1
+ __version__ = "26.3.3"
@@ -3,6 +3,9 @@
3
3
  import platform as plat
4
4
  import re
5
5
  import sys
6
+ from dataclasses import dataclass, field
7
+ from fractions import Fraction
8
+ from io import StringIO
6
9
  from os import environ
7
10
  from os.path import exists, isdir, isfile, lexists, splitext
8
11
  from subprocess import run
@@ -11,21 +14,127 @@ import auto_editor
11
14
  from auto_editor.utils.func import get_stdout
12
15
  from auto_editor.utils.log import Log
13
16
  from auto_editor.utils.types import (
14
- Args,
17
+ CoerceError,
15
18
  frame_rate,
16
- margin,
19
+ natural,
17
20
  number,
18
21
  parse_color,
19
- resolution,
20
- sample_rate,
21
- speed,
22
- speed_range,
23
- time_range,
22
+ split_num_str,
24
23
  )
25
24
  from auto_editor.vanparse import ArgumentParser
26
25
 
27
26
 
27
+ @dataclass(slots=True)
28
+ class Args:
29
+ input: list[str] = field(default_factory=list)
30
+ help: bool = False
31
+
32
+ # Editing Options
33
+ margin: tuple[str, str] = ("0.2s", "0.2s")
34
+ edit: str = "audio"
35
+ export: str | None = None
36
+ output: str | None = None
37
+ silent_speed: float = 99999.0
38
+ video_speed: float = 1.0
39
+ cut_out: list[tuple[str, str]] = field(default_factory=list)
40
+ add_in: list[tuple[str, str]] = field(default_factory=list)
41
+ set_speed_for_range: list[tuple[float, str, str]] = field(default_factory=list)
42
+
43
+ # Timeline Options
44
+ frame_rate: Fraction | None = None
45
+ sample_rate: int | None = None
46
+ resolution: tuple[int, int] | None = None
47
+ background: str = "#000000"
48
+
49
+ # URL download Options
50
+ yt_dlp_location: str = "yt-dlp"
51
+ download_format: str | None = None
52
+ output_format: str | None = None
53
+ yt_dlp_extras: str | None = None
54
+
55
+ # Display Options
56
+ progress: str = "modern"
57
+ debug: bool = False
58
+ quiet: bool = False
59
+ preview: bool = False
60
+
61
+ # Container Settings
62
+ sn: bool = False
63
+ dn: bool = False
64
+ faststart: bool = False
65
+ no_faststart: bool = False
66
+ fragmented: bool = False
67
+ no_fragmented: bool = False
68
+
69
+ # Video Rendering
70
+ video_codec: str = "auto"
71
+ video_bitrate: str = "auto"
72
+ vprofile: str | None = None
73
+ scale: float = 1.0
74
+ no_seek: bool = False
75
+
76
+ # Audio Rendering
77
+ audio_codec: str = "auto"
78
+ audio_bitrate: str = "auto"
79
+ keep_tracks_separate: bool = False
80
+ audio_normalize: str = "#f"
81
+
82
+ # Misc.
83
+ config: bool = False
84
+ no_cache: bool = False
85
+ no_open: bool = False
86
+ temp_dir: str | None = None
87
+ player: str | None = None
88
+ version: bool = False
89
+
90
+
28
91
  def main_options(parser: ArgumentParser) -> ArgumentParser:
92
+ def margin(val: str) -> tuple[str, str]:
93
+ vals = val.strip().split(",")
94
+ if len(vals) == 1:
95
+ vals.append(vals[0])
96
+ if len(vals) != 2:
97
+ raise CoerceError("--margin has too many arguments.")
98
+ return vals[0], vals[1]
99
+
100
+ def speed(val: str) -> float:
101
+ _s = number(val)
102
+ if _s <= 0 or _s > 99999:
103
+ return 99999.0
104
+ return _s
105
+
106
+ def resolution(val: str | None) -> tuple[int, int] | None:
107
+ if val is None:
108
+ return None
109
+ vals = val.strip().split(",")
110
+ if len(vals) != 2:
111
+ raise CoerceError(f"'{val}': Resolution takes two numbers")
112
+ return natural(vals[0]), natural(vals[1])
113
+
114
+ def sample_rate(val: str) -> int:
115
+ num, unit = split_num_str(val)
116
+ if unit in {"kHz", "KHz"}:
117
+ return natural(num * 1000)
118
+ if unit not in {"", "Hz"}:
119
+ raise CoerceError(f"Unknown unit: '{unit}'")
120
+ return natural(num)
121
+
122
+ def _comma_coerce(name: str, val: str, num_args: int) -> list[str]:
123
+ vals = val.strip().split(",")
124
+ if num_args > len(vals):
125
+ raise CoerceError(f"Too few arguments for {name}.")
126
+ if len(vals) > num_args:
127
+ raise CoerceError(f"Too many arguments for {name}.")
128
+ return vals
129
+
130
+ def time_range(val: str) -> tuple[str, str]:
131
+ a = _comma_coerce("time_range", val, 2)
132
+ return a[0], a[1]
133
+
134
+ def speed_range(val: str) -> tuple[float, str, str]:
135
+ a = _comma_coerce("speed_range", val, 3)
136
+ return number(a[0]), a[1], a[2]
137
+
29
138
  parser.add_required("input", nargs="*", metavar="[file | url ...] [options]")
30
139
  parser.add_text("Editing Options:")
31
140
  parser.add_argument(
@@ -40,6 +149,16 @@ def main_options(parser: ArgumentParser) -> ArgumentParser:
40
149
  metavar="METHOD",
41
150
  help="Set an expression which determines how to make auto edits",
42
151
  )
152
+ parser.add_argument(
153
+ "--export", "-ex", metavar="EXPORT:ATTRS?", help="Choose the export mode"
154
+ )
155
+ parser.add_argument(
156
+ "--output",
157
+ "--output-file",
158
+ "-o",
159
+ metavar="FILE",
160
+ help="Set the name/path of the new output file",
161
+ )
43
162
  parser.add_argument(
44
163
  "--silent-speed",
45
164
  "-s",
@@ -110,12 +229,6 @@ def main_options(parser: ArgumentParser) -> ArgumentParser:
110
229
  metavar="COLOR",
111
230
  help="Set the background as a solid RGB color",
112
231
  )
113
- parser.add_argument(
114
- "--add",
115
- nargs="*",
116
- metavar="OBJ:START,DUR,ATTRS?",
117
- help="Insert an audio/video object to the timeline",
118
- )
119
232
  parser.add_text("URL Download Options:")
120
233
  parser.add_argument(
121
234
  "--yt-dlp-location",
@@ -137,28 +250,6 @@ def main_options(parser: ArgumentParser) -> ArgumentParser:
137
250
  metavar="CMD",
138
251
  help="Add extra options for yt-dlp. Must be in quotes",
139
252
  )
140
- parser.add_text("Utility Options:")
141
- parser.add_argument(
142
- "--export", "-ex", metavar="EXPORT:ATTRS?", help="Choose the export mode"
143
- )
144
- parser.add_argument(
145
- "--output-file",
146
- "--output",
147
- "-o",
148
- metavar="FILE",
149
- help="Set the name/path of the new output file",
150
- )
151
- parser.add_argument(
152
- "--player", "-p", metavar="CMD", help="Set player to open output media files"
153
- )
154
- parser.add_argument(
155
- "--no-open", flag=True, help="Do not open the output file after editing is done"
156
- )
157
- parser.add_argument(
158
- "--temp-dir",
159
- metavar="PATH",
160
- help="Set where the temporary directory is located",
161
- )
162
253
  parser.add_text("Display Options:")
163
254
  parser.add_argument(
164
255
  "--progress",
@@ -174,6 +265,37 @@ def main_options(parser: ArgumentParser) -> ArgumentParser:
174
265
  flag=True,
175
266
  help="Show stats on how the input will be cut and halt",
176
267
  )
268
+ parser.add_text("Container Settings:")
269
+ parser.add_argument(
270
+ "-sn",
271
+ flag=True,
272
+ help="Disable the inclusion of subtitle streams in the output file",
273
+ )
274
+ parser.add_argument(
275
+ "-dn",
276
+ flag=True,
277
+ help="Disable the inclusion of data streams in the output file",
278
+ )
279
+ parser.add_argument(
280
+ "--faststart",
281
+ flag=True,
282
+ help="Enable movflags +faststart, recommended for web (default)",
283
+ )
284
+ parser.add_argument(
285
+ "--no-faststart",
286
+ flag=True,
287
+ help="Disable movflags +faststart, will be faster for large files",
288
+ )
289
+ parser.add_argument(
290
+ "--fragmented",
291
+ flag=True,
292
+ help="Use fragmented mp4/mov to allow playback before video is complete\nSee: https://ffmpeg.org/ffmpeg-formats.html#Fragmentation",
293
+ )
294
+ parser.add_argument(
295
+ "--no-fragmented",
296
+ flag=True,
297
+ help="Do not use fragmented mp4/mov for better compatibility (default)",
298
+ )
177
299
  parser.add_text("Video Rendering:")
178
300
  parser.add_argument(
179
301
  "--video-codec",
@@ -231,20 +353,21 @@ def main_options(parser: ArgumentParser) -> ArgumentParser:
231
353
  )
232
354
  parser.add_text("Miscellaneous:")
233
355
  parser.add_argument(
234
- "-sn",
235
- flag=True,
236
- help="Disable the inclusion of subtitle streams in the output file",
356
+ "--config", flag=True, help="When set, look for `config.pal` and run it"
237
357
  )
238
358
  parser.add_argument(
239
- "-dn",
240
- flag=True,
241
- help="Disable the inclusion of data streams in the output file",
359
+ "--no-cache", flag=True, help="Don't look for or write a cache file"
242
360
  )
243
361
  parser.add_argument(
244
- "--config", flag=True, help="When set, look for `config.pal` and run it"
362
+ "--no-open", flag=True, help="Do not open the output file after editing is done"
245
363
  )
246
364
  parser.add_argument(
247
- "--no-cache", flag=True, help="Don't look for or write a cache file"
365
+ "--temp-dir",
366
+ metavar="PATH",
367
+ help="Set where the temporary directory is located",
368
+ )
369
+ parser.add_argument(
370
+ "--player", "-p", metavar="CMD", help="Set player to open output media files"
248
371
  )
249
372
  parser.add_argument("--version", "-V", flag=True, help="Display version and halt")
250
373
  return parser
@@ -320,23 +443,26 @@ def main() -> None:
320
443
  )
321
444
 
322
445
  if args.version:
323
- print(auto_editor.__version__)
324
- return
446
+ return print(auto_editor.__version__)
325
447
 
326
448
  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
-
449
+ buf = StringIO()
450
+ buf.write(f"OS: {plat.system()} {plat.release()} {plat.machine().lower()}\n")
451
+ buf.write(f"Python: {plat.python_version()}\nPyAV: ")
330
452
  try:
331
453
  import av
332
-
333
- license = av._core.library_meta["libavcodec"]["license"]
334
- print(f"PyAV: {av.__version__} ({license})")
335
454
  except (ModuleNotFoundError, ImportError):
336
- print("PyAV: error")
337
-
338
- print(f"Auto-Editor: {auto_editor.__version__}")
339
- return
455
+ buf.write("not found")
456
+ else:
457
+ try:
458
+ buf.write(f"{av.__version__} ")
459
+ license = av._core.library_meta["libavcodec"]["license"]
460
+ buf.write(f"({license})")
461
+ except AttributeError:
462
+ buf.write("error")
463
+
464
+ buf.write(f"\nAuto-Editor: {auto_editor.__version__}")
465
+ return print(buf.getvalue())
340
466
 
341
467
  if not args.input:
342
468
  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__}")