auto-editor 25.3.0__tar.gz → 25.3.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 (64) hide show
  1. {auto_editor-25.3.0 → auto_editor-25.3.1}/PKG-INFO +2 -2
  2. auto_editor-25.3.1/auto_editor/__init__.py +1 -0
  3. {auto_editor-25.3.0 → auto_editor-25.3.1}/auto_editor/__main__.py +67 -6
  4. {auto_editor-25.3.0 → auto_editor-25.3.1}/auto_editor/analyze.py +11 -14
  5. {auto_editor-25.3.0 → auto_editor-25.3.1}/auto_editor/edit.py +21 -8
  6. {auto_editor-25.3.0 → auto_editor-25.3.1}/auto_editor/ffwrapper.py +38 -75
  7. {auto_editor-25.3.0 → auto_editor-25.3.1}/auto_editor/output.py +1 -1
  8. {auto_editor-25.3.0 → auto_editor-25.3.1}/auto_editor/render/audio.py +1 -2
  9. {auto_editor-25.3.0 → auto_editor-25.3.1}/auto_editor/render/subtitle.py +5 -3
  10. {auto_editor-25.3.0 → auto_editor-25.3.1}/auto_editor/render/video.py +1 -1
  11. {auto_editor-25.3.0 → auto_editor-25.3.1}/auto_editor/subcommands/test.py +10 -3
  12. {auto_editor-25.3.0 → auto_editor-25.3.1}/auto_editor/utils/func.py +0 -26
  13. {auto_editor-25.3.0 → auto_editor-25.3.1}/auto_editor.egg-info/PKG-INFO +2 -2
  14. {auto_editor-25.3.0 → auto_editor-25.3.1}/auto_editor.egg-info/SOURCES.txt +0 -2
  15. {auto_editor-25.3.0 → auto_editor-25.3.1}/auto_editor.egg-info/requires.txt +1 -1
  16. {auto_editor-25.3.0 → auto_editor-25.3.1}/docs/build.py +1 -0
  17. {auto_editor-25.3.0 → auto_editor-25.3.1}/pyproject.toml +1 -1
  18. auto_editor-25.3.0/auto_editor/__init__.py +0 -1
  19. auto_editor-25.3.0/auto_editor/utils/subtitle_tools.py +0 -29
  20. auto_editor-25.3.0/auto_editor/validate_input.py +0 -88
  21. {auto_editor-25.3.0 → auto_editor-25.3.1}/LICENSE +0 -0
  22. {auto_editor-25.3.0 → auto_editor-25.3.1}/README.md +0 -0
  23. {auto_editor-25.3.0 → auto_editor-25.3.1}/auto_editor/formats/__init__.py +0 -0
  24. {auto_editor-25.3.0 → auto_editor-25.3.1}/auto_editor/formats/fcp11.py +0 -0
  25. {auto_editor-25.3.0 → auto_editor-25.3.1}/auto_editor/formats/fcp7.py +0 -0
  26. {auto_editor-25.3.0 → auto_editor-25.3.1}/auto_editor/formats/json.py +0 -0
  27. {auto_editor-25.3.0 → auto_editor-25.3.1}/auto_editor/formats/shotcut.py +0 -0
  28. {auto_editor-25.3.0 → auto_editor-25.3.1}/auto_editor/formats/utils.py +0 -0
  29. {auto_editor-25.3.0 → auto_editor-25.3.1}/auto_editor/help.py +0 -0
  30. {auto_editor-25.3.0 → auto_editor-25.3.1}/auto_editor/lang/__init__.py +0 -0
  31. {auto_editor-25.3.0 → auto_editor-25.3.1}/auto_editor/lang/json.py +0 -0
  32. {auto_editor-25.3.0 → auto_editor-25.3.1}/auto_editor/lang/libintrospection.py +0 -0
  33. {auto_editor-25.3.0 → auto_editor-25.3.1}/auto_editor/lang/libmath.py +0 -0
  34. {auto_editor-25.3.0 → auto_editor-25.3.1}/auto_editor/lang/palet.py +0 -0
  35. {auto_editor-25.3.0 → auto_editor-25.3.1}/auto_editor/lang/stdenv.py +0 -0
  36. {auto_editor-25.3.0 → auto_editor-25.3.1}/auto_editor/lib/__init__.py +0 -0
  37. {auto_editor-25.3.0 → auto_editor-25.3.1}/auto_editor/lib/contracts.py +0 -0
  38. {auto_editor-25.3.0 → auto_editor-25.3.1}/auto_editor/lib/data_structs.py +0 -0
  39. {auto_editor-25.3.0 → auto_editor-25.3.1}/auto_editor/lib/err.py +0 -0
  40. {auto_editor-25.3.0 → auto_editor-25.3.1}/auto_editor/make_layers.py +0 -0
  41. {auto_editor-25.3.0 → auto_editor-25.3.1}/auto_editor/preview.py +0 -0
  42. {auto_editor-25.3.0 → auto_editor-25.3.1}/auto_editor/render/__init__.py +0 -0
  43. {auto_editor-25.3.0 → auto_editor-25.3.1}/auto_editor/subcommands/__init__.py +0 -0
  44. {auto_editor-25.3.0 → auto_editor-25.3.1}/auto_editor/subcommands/desc.py +0 -0
  45. {auto_editor-25.3.0 → auto_editor-25.3.1}/auto_editor/subcommands/info.py +0 -0
  46. {auto_editor-25.3.0 → auto_editor-25.3.1}/auto_editor/subcommands/levels.py +0 -0
  47. {auto_editor-25.3.0 → auto_editor-25.3.1}/auto_editor/subcommands/palet.py +0 -0
  48. {auto_editor-25.3.0 → auto_editor-25.3.1}/auto_editor/subcommands/repl.py +0 -0
  49. {auto_editor-25.3.0 → auto_editor-25.3.1}/auto_editor/subcommands/subdump.py +0 -0
  50. {auto_editor-25.3.0 → auto_editor-25.3.1}/auto_editor/timeline.py +0 -0
  51. {auto_editor-25.3.0 → auto_editor-25.3.1}/auto_editor/utils/__init__.py +0 -0
  52. {auto_editor-25.3.0 → auto_editor-25.3.1}/auto_editor/utils/bar.py +0 -0
  53. {auto_editor-25.3.0 → auto_editor-25.3.1}/auto_editor/utils/chunks.py +0 -0
  54. {auto_editor-25.3.0 → auto_editor-25.3.1}/auto_editor/utils/cmdkw.py +0 -0
  55. {auto_editor-25.3.0 → auto_editor-25.3.1}/auto_editor/utils/container.py +0 -0
  56. {auto_editor-25.3.0 → auto_editor-25.3.1}/auto_editor/utils/encoder.py +0 -0
  57. {auto_editor-25.3.0 → auto_editor-25.3.1}/auto_editor/utils/log.py +0 -0
  58. {auto_editor-25.3.0 → auto_editor-25.3.1}/auto_editor/utils/types.py +0 -0
  59. {auto_editor-25.3.0 → auto_editor-25.3.1}/auto_editor/vanparse.py +0 -0
  60. {auto_editor-25.3.0 → auto_editor-25.3.1}/auto_editor/wavfile.py +0 -0
  61. {auto_editor-25.3.0 → auto_editor-25.3.1}/auto_editor.egg-info/dependency_links.txt +0 -0
  62. {auto_editor-25.3.0 → auto_editor-25.3.1}/auto_editor.egg-info/entry_points.txt +0 -0
  63. {auto_editor-25.3.0 → auto_editor-25.3.1}/auto_editor.egg-info/top_level.txt +0 -0
  64. {auto_editor-25.3.0 → auto_editor-25.3.1}/setup.cfg +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: auto-editor
3
- Version: 25.3.0
3
+ Version: 25.3.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.14,>=3.10
12
12
  Description-Content-Type: text/markdown
13
13
  License-File: LICENSE
14
14
  Requires-Dist: numpy<3.0,>=1.23.0
15
- Requires-Dist: pyav==13.0.*
15
+ Requires-Dist: pyav==13.1.*
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 @@
1
+ __version__ = "25.3.1"
@@ -1,11 +1,15 @@
1
1
  #!/usr/bin/env python3
2
2
 
3
+ import re
3
4
  import sys
4
5
  from os import environ
6
+ from os.path import exists, isdir, isfile, lexists, splitext
7
+ from subprocess import run
5
8
 
6
9
  import auto_editor
7
10
  from auto_editor.edit import edit_media
8
- from auto_editor.ffwrapper import FFmpeg
11
+ from auto_editor.ffwrapper import FFmpeg, initFFmpeg
12
+ from auto_editor.utils.func import get_stdout
9
13
  from auto_editor.utils.log import Log
10
14
  from auto_editor.utils.types import (
11
15
  Args,
@@ -20,7 +24,6 @@ from auto_editor.utils.types import (
20
24
  speed_range,
21
25
  time_range,
22
26
  )
23
- from auto_editor.validate_input import valid_input
24
27
  from auto_editor.vanparse import ArgumentParser
25
28
 
26
29
 
@@ -274,6 +277,50 @@ def main_options(parser: ArgumentParser) -> ArgumentParser:
274
277
  return parser
275
278
 
276
279
 
280
+ def download_video(my_input: str, args: Args, ffmpeg: FFmpeg, log: Log) -> str:
281
+ log.conwrite("Downloading video...")
282
+
283
+ def get_domain(url: str) -> str:
284
+ t = __import__("urllib").parse.urlparse(url).netloc
285
+ return ".".join(t.split(".")[-2:])
286
+
287
+ download_format = args.download_format
288
+ if download_format is None and get_domain(my_input) == "youtube.com":
289
+ download_format = "bestvideo[ext=mp4]+bestaudio[ext=m4a]"
290
+
291
+ if args.output_format is None:
292
+ output_format = re.sub(r"\W+", "-", splitext(my_input)[0]) + ".%(ext)s"
293
+ else:
294
+ output_format = args.output_format
295
+
296
+ yt_dlp_path = args.yt_dlp_location
297
+
298
+ cmd = ["--ffmpeg-location", ffmpeg.path]
299
+
300
+ if download_format is not None:
301
+ cmd.extend(["-f", download_format])
302
+
303
+ cmd.extend(["-o", output_format, my_input])
304
+
305
+ if args.yt_dlp_extras is not None:
306
+ cmd.extend(args.yt_dlp_extras.split(" "))
307
+
308
+ try:
309
+ location = get_stdout(
310
+ [yt_dlp_path, "--get-filename", "--no-warnings"] + cmd
311
+ ).strip()
312
+ except FileNotFoundError:
313
+ log.error("Program `yt-dlp` must be installed and on PATH.")
314
+
315
+ if not isfile(location):
316
+ run([yt_dlp_path] + cmd)
317
+
318
+ if not isfile(location):
319
+ log.error(f"Download file wasn't created: {location}")
320
+
321
+ return location
322
+
323
+
277
324
  def main() -> None:
278
325
  subcommands = ("test", "info", "levels", "subdump", "desc", "repl", "palet")
279
326
 
@@ -284,8 +331,7 @@ def main() -> None:
284
331
  obj.main(sys.argv[2:])
285
332
  return
286
333
 
287
- ff_color = "AV_LOG_FORCE_NOCOLOR"
288
- no_color = bool(environ.get("NO_COLOR")) or bool(environ.get(ff_color))
334
+ no_color = bool(environ.get("NO_COLOR") or environ.get("AV_LOG_FORCE_NOCOLOR"))
289
335
  log = Log(no_color=no_color)
290
336
 
291
337
  args = main_options(ArgumentParser("Auto-Editor")).parse_args(
@@ -327,13 +373,28 @@ def main() -> None:
327
373
  is_machine = args.progress == "machine"
328
374
  log = Log(args.debug, args.quiet, args.temp_dir, is_machine, no_color)
329
375
 
330
- ffmpeg = FFmpeg(
376
+ ffmpeg = initFFmpeg(
377
+ log,
331
378
  args.ffmpeg_location,
332
379
  args.my_ffmpeg,
333
380
  args.show_ffmpeg_commands,
334
381
  args.show_ffmpeg_output,
335
382
  )
336
- paths = valid_input(args.input, ffmpeg, args, log)
383
+ paths = []
384
+ for my_input in args.input:
385
+ if my_input.startswith("http://") or my_input.startswith("https://"):
386
+ paths.append(download_video(my_input, args, ffmpeg, log))
387
+ else:
388
+ if not splitext(my_input)[1]:
389
+ if isdir(my_input):
390
+ log.error("Input must be a file or a URL, not a directory.")
391
+ if exists(my_input):
392
+ log.error(f"Input file must have an extension: {my_input}")
393
+ if lexists(my_input):
394
+ log.error(f"Input file is a broken symbolic link: {my_input}")
395
+ if my_input.startswith("-"):
396
+ log.error(f"Option/Input file doesn't exist: {my_input}")
397
+ paths.append(my_input)
337
398
 
338
399
  try:
339
400
  edit_media(paths, ffmpeg, args, log)
@@ -14,13 +14,11 @@ from av.audio.fifo import AudioFifo
14
14
  from av.subtitles.subtitle import AssSubtitle
15
15
 
16
16
  from auto_editor import __version__
17
- from auto_editor.utils.subtitle_tools import convert_ass_to_text
18
17
 
19
18
  if TYPE_CHECKING:
20
- from collections.abc import Iterator
19
+ from collections.abc import Iterator, Sequence
21
20
  from fractions import Fraction
22
21
  from pathlib import Path
23
- from typing import Any
24
22
 
25
23
  from numpy.typing import NDArray
26
24
 
@@ -156,10 +154,10 @@ def iter_motion(src, tb, stream: int, blur: int, width: int) -> Iterator[np.floa
156
154
  container.close()
157
155
 
158
156
 
159
- def obj_tag(path: Path, kind: str, tb: Fraction, obj: dict[str, Any]) -> str:
157
+ def obj_tag(path: Path, kind: str, tb: Fraction, obj: Sequence[object]) -> str:
160
158
  mod_time = int(path.stat().st_mtime)
161
159
  key = f"{path.name}:{mod_time:x}:{kind}:{tb}:"
162
- return key + ",".join(f"{v}" for v in obj.values())
160
+ return key + ",".join(f"{v}" for v in obj)
163
161
 
164
162
 
165
163
  @dataclass(slots=True)
@@ -174,7 +172,7 @@ class Levels:
174
172
  @property
175
173
  def media_length(self) -> int:
176
174
  if self.src.audios:
177
- if (arr := self.read_cache("audio", {"stream": 0})) is not None:
175
+ if (arr := self.read_cache("audio", (0,))) is not None:
178
176
  return len(arr)
179
177
 
180
178
  result = sum(1 for _ in iter_audio(self.src, self.tb, 0))
@@ -202,7 +200,7 @@ class Levels:
202
200
  def all(self) -> NDArray[np.bool_]:
203
201
  return np.zeros(self.media_length, dtype=np.bool_)
204
202
 
205
- def read_cache(self, kind: str, obj: dict[str, Any]) -> None | np.ndarray:
203
+ def read_cache(self, kind: str, obj: Sequence[object]) -> None | np.ndarray:
206
204
  if self.no_cache:
207
205
  return None
208
206
 
@@ -221,7 +219,7 @@ class Levels:
221
219
  self.log.debug("Using cache")
222
220
  return npzfile[key]
223
221
 
224
- def cache(self, arr: np.ndarray, kind: str, obj: dict[str, Any]) -> np.ndarray:
222
+ def cache(self, arr: np.ndarray, kind: str, obj: Sequence[object]) -> np.ndarray:
225
223
  if self.no_cache:
226
224
  return arr
227
225
 
@@ -238,7 +236,7 @@ class Levels:
238
236
  if stream >= len(self.src.audios):
239
237
  raise LevelError(f"audio: audio stream '{stream}' does not exist.")
240
238
 
241
- if (arr := self.read_cache("audio", {"stream": stream})) is not None:
239
+ if (arr := self.read_cache("audio", (stream,))) is not None:
242
240
  return arr
243
241
 
244
242
  with av.open(self.src.path, "r") as container:
@@ -265,13 +263,13 @@ class Levels:
265
263
  index += 1
266
264
 
267
265
  bar.end()
268
- return self.cache(result[:index], "audio", {"stream": stream})
266
+ return self.cache(result[:index], "audio", (stream,))
269
267
 
270
268
  def motion(self, stream: int, blur: int, width: int) -> NDArray[np.float32]:
271
269
  if stream >= len(self.src.videos):
272
270
  raise LevelError(f"motion: video stream '{stream}' does not exist.")
273
271
 
274
- mobj = {"stream": stream, "width": width, "blur": blur}
272
+ mobj = (stream, width, blur)
275
273
  if (arr := self.read_cache("motion", mobj)) is not None:
276
274
  return arr
277
275
 
@@ -360,11 +358,10 @@ class Levels:
360
358
  san_end = round((start + dur) * self.tb)
361
359
 
362
360
  for sub in subset:
363
- if isinstance(sub, AssSubtitle):
364
- line = convert_ass_to_text(sub.ass.decode(errors="ignore"))
365
- else:
361
+ if not isinstance(sub, AssSubtitle):
366
362
  continue
367
363
 
364
+ line = sub.dialogue.decode(errors="ignore")
368
365
  if line and re.search(re_pattern, line):
369
366
  result[san_start:san_end] = 1
370
367
  count += 1
@@ -1,6 +1,8 @@
1
1
  from __future__ import annotations
2
2
 
3
3
  import os
4
+ import sys
5
+ from subprocess import run
4
6
  from typing import Any
5
7
 
6
8
  from auto_editor.ffwrapper import FFmpeg, FileInfo, initFileInfo
@@ -15,7 +17,6 @@ from auto_editor.utils.bar import initBar
15
17
  from auto_editor.utils.chunks import Chunk, Chunks
16
18
  from auto_editor.utils.cmdkw import ParserError, parse_with_palet, pAttr, pAttrs
17
19
  from auto_editor.utils.container import Container, container_constructor
18
- from auto_editor.utils.func import open_with_system_default
19
20
  from auto_editor.utils.log import Log
20
21
  from auto_editor.utils.types import Args
21
22
 
@@ -328,7 +329,7 @@ def edit_media(paths: list[str], ffmpeg: FFmpeg, args: Args, log: Log) -> None:
328
329
  total_frames = tl.v1.chunks[-1][1] - 1
329
330
  clip_num = 0
330
331
  for chunk in tl.v1.chunks:
331
- if chunk[2] == 99999:
332
+ if chunk[2] == 0 or chunk[2] >= 99999:
332
333
  continue
333
334
 
334
335
  padded_chunks = pad_chunk(chunk, total_frames)
@@ -354,11 +355,23 @@ def edit_media(paths: list[str], ffmpeg: FFmpeg, args: Args, log: Log) -> None:
354
355
 
355
356
  log.stop_timer()
356
357
 
357
- if not args.no_open and export in ("default", "audio", "clip-sequence"):
358
+ if not args.no_open and export in ("default", "audio"):
358
359
  if args.player is None:
359
- open_with_system_default(output, log)
360
+ if sys.platform == "win32":
361
+ try:
362
+ os.startfile(output)
363
+ except OSError:
364
+ log.warning(f"Could not find application to open file: {output}")
365
+ else:
366
+ try: # MacOS case
367
+ run(["open", output])
368
+ except Exception:
369
+ try: # WSL2 case
370
+ run(["cmd.exe", "/C", "start", output])
371
+ except Exception:
372
+ try: # Linux case
373
+ run(["xdg-open", output])
374
+ except Exception:
375
+ log.warning(f"Could not open output file: {output}")
360
376
  else:
361
- import subprocess
362
- from shlex import split
363
-
364
- subprocess.run(split(args.player) + [output])
377
+ run(__import__("shlex").split(args.player) + [output])
@@ -1,83 +1,59 @@
1
1
  from __future__ import annotations
2
2
 
3
3
  import os.path
4
- import subprocess
5
4
  import sys
6
5
  from dataclasses import dataclass
7
6
  from fractions import Fraction
8
7
  from pathlib import Path
9
8
  from re import search
10
9
  from shutil import which
11
- from subprocess import PIPE, Popen
10
+ from subprocess import PIPE, Popen, run
12
11
  from typing import Any
13
12
 
14
13
  import av
15
14
 
16
- from auto_editor.utils.func import get_stdout
17
15
  from auto_editor.utils.log import Log
18
16
 
19
17
 
20
- class FFmpeg:
21
- __slots__ = ("debug", "show_cmd", "path", "version")
22
-
23
- def __init__(
24
- self,
25
- ff_location: str | None = None,
26
- my_ffmpeg: bool = False,
27
- show_cmd: bool = False,
28
- debug: bool = False,
29
- ):
30
- def _set_ff_path(ff_location: str | None, my_ffmpeg: bool) -> str:
31
- if ff_location is not None:
32
- return ff_location
33
- if my_ffmpeg:
34
- return "ffmpeg"
35
-
36
- try:
37
- import ae_ffmpeg
38
-
39
- return ae_ffmpeg.get_path()
40
- except ImportError:
41
- return "ffmpeg"
42
-
43
- self.debug = debug
44
- self.show_cmd = show_cmd
45
- _path: str | None = _set_ff_path(ff_location, my_ffmpeg)
46
-
47
- if _path == "ffmpeg":
48
- _path = which("ffmpeg")
49
-
50
- if _path is None:
51
- Log().error("Did not find ffmpeg on PATH.")
52
- self.path = _path
53
-
18
+ def initFFmpeg(
19
+ log: Log, ff_location: str | None, my_ffmpeg: bool, show_cmd: bool, debug: bool
20
+ ) -> FFmpeg:
21
+ if ff_location is not None:
22
+ program = ff_location
23
+ elif my_ffmpeg:
24
+ program = "ffmpeg"
25
+ else:
54
26
  try:
55
- _version = get_stdout([self.path, "-version"]).split("\n")[0]
56
- self.version = _version.replace("ffmpeg version", "").strip().split(" ")[0]
57
- except FileNotFoundError:
58
- Log().error("ffmpeg must be installed and on PATH.")
27
+ import ae_ffmpeg
59
28
 
60
- def print(self, message: str) -> None:
61
- if self.debug:
62
- sys.stderr.write(f"FFmpeg: {message}\n")
29
+ program = ae_ffmpeg.get_path()
30
+ except ImportError:
31
+ program = "ffmpeg"
63
32
 
64
- def print_cmd(self, cmd: list[str]) -> None:
65
- if self.show_cmd:
66
- sys.stderr.write(f"{' '.join(cmd)}\n\n")
33
+ path: str | None = which(program)
34
+ if path is None:
35
+ log.error("Did not find ffmpeg on PATH.")
36
+
37
+ return FFmpeg(log, path, show_cmd, debug)
38
+
39
+
40
+ @dataclass(slots=True)
41
+ class FFmpeg:
42
+ log: Log
43
+ path: str
44
+ show_cmd: bool
45
+ debug: bool
67
46
 
68
47
  def run(self, cmd: list[str]) -> None:
69
48
  cmd = [self.path, "-hide_banner", "-y"] + cmd
70
49
  if not self.debug:
71
50
  cmd.extend(["-nostats", "-loglevel", "error"])
72
- self.print_cmd(cmd)
73
- subprocess.run(cmd)
51
+ if self.show_cmd:
52
+ sys.stderr.write(f"{' '.join(cmd)}\n\n")
53
+ run(cmd)
74
54
 
75
55
  def run_check_errors(
76
- self,
77
- cmd: list[str],
78
- log: Log,
79
- show_out: bool = False,
80
- path: str | None = None,
56
+ self, cmd: list[str], show_out: bool = False, path: str | None = None
81
57
  ) -> None:
82
58
  process = self.Popen(cmd, stdin=PIPE, stdout=PIPE, stderr=PIPE)
83
59
  _, stderr = process.communicate()
@@ -104,40 +80,27 @@ class FFmpeg:
104
80
 
105
81
  for item in error_list:
106
82
  if check := search(item, output):
107
- log.error(check.group())
83
+ self.log.error(check.group())
108
84
 
109
85
  if path is not None and not os.path.isfile(path):
110
- log.error(f"The file {path} was not created.")
111
- elif show_out and not self.debug:
86
+ self.log.error(f"The file {path} was not created.")
87
+ if show_out and not self.debug:
112
88
  print(f"stderr: {output}")
113
89
 
114
90
  def Popen(
115
91
  self, cmd: list[str], stdin: Any = None, stdout: Any = PIPE, stderr: Any = None
116
92
  ) -> Popen:
117
- cmd = [self.path] + cmd
118
- self.print_cmd(cmd)
119
- return Popen(cmd, stdin=stdin, stdout=stdout, stderr=stderr)
120
-
121
- def pipe(self, cmd: list[str]) -> str:
122
- cmd = [self.path, "-y"] + cmd
123
-
124
- self.print_cmd(cmd)
125
- output = get_stdout(cmd)
126
- self.print(output)
127
- return output
93
+ if self.show_cmd:
94
+ sys.stderr.write(f"{self.path} {' '.join(cmd)}\n\n")
95
+ return Popen([self.path] + cmd, stdin=stdin, stdout=stdout, stderr=stderr)
128
96
 
129
97
 
130
- def mux(input: Path, output: Path, stream: int, codec: str | None = None) -> None:
98
+ def mux(input: Path, output: Path, stream: int) -> None:
131
99
  input_container = av.open(input, "r")
132
100
  output_container = av.open(output, "w")
133
101
 
134
102
  input_audio_stream = input_container.streams.audio[stream]
135
-
136
- if codec is None:
137
- codec = "pcm_s16le"
138
-
139
- output_audio_stream = output_container.add_stream(codec)
140
- assert isinstance(output_audio_stream, av.audio.AudioStream)
103
+ output_audio_stream = output_container.add_stream("pcm_s16le")
141
104
 
142
105
  for frame in input_container.decode(input_audio_stream):
143
106
  packet = output_audio_stream.encode(frame)
@@ -241,4 +241,4 @@ def mux_quality_media(
241
241
  cmd.extend(["-map", "0:d?"])
242
242
 
243
243
  cmd.append(output_path)
244
- ffmpeg.run_check_errors(cmd, log, path=output_path)
244
+ ffmpeg.run_check_errors(cmd, path=output_path)
@@ -180,7 +180,6 @@ def process_audio_clip(
180
180
  output_bytes = io.BytesIO()
181
181
  output_file = av.open(output_bytes, mode="w", format="wav")
182
182
  output_stream = output_file.add_stream("pcm_s16le", rate=sr)
183
- assert isinstance(output_stream, av.audio.AudioStream)
184
183
 
185
184
  graph = av.filter.Graph()
186
185
  args = [graph.add_abuffer(template=input_stream)]
@@ -212,7 +211,7 @@ def process_audio_clip(
212
211
  while True:
213
212
  try:
214
213
  aframe = graph.pull()
215
- assert isinstance(aframe, av.audio.AudioFrame)
214
+ assert isinstance(aframe, av.AudioFrame)
216
215
  for packet in output_stream.encode(aframe):
217
216
  output_file.mux(packet)
218
217
  except (av.BlockingIOError, av.EOFError):
@@ -112,13 +112,15 @@ class SubtitleParser:
112
112
  self.contents = new_content
113
113
 
114
114
  def write(self, file_path: str) -> None:
115
+ codec = self.codec
115
116
  with open(file_path, "w", encoding="utf-8") as file:
116
117
  file.write(self.header)
117
118
  for c in self.contents:
118
119
  file.write(
119
- f"{c.before}{to_timecode(c.start / self.tb, self.codec)}"
120
- f"{c.middle}{to_timecode(c.end / self.tb, self.codec)}"
121
- f"{c.after}"
120
+ f"{c.before}{to_timecode(c.start / self.tb, codec)}"
121
+ + f"{c.middle}{to_timecode(c.end / self.tb, codec)}"
122
+ + c.after
123
+ + ("\n" if codec == "webvtt" else "")
122
124
  )
123
125
  file.write(self.footer)
124
126
 
@@ -339,7 +339,7 @@ def render_av(
339
339
  process2.wait()
340
340
  except (OSError, BrokenPipeError):
341
341
  bar.end()
342
- ffmpeg.run_check_errors(cmd, log, True)
342
+ ffmpeg.run_check_errors(cmd, True)
343
343
  log.error("FFmpeg Error!")
344
344
 
345
345
  log.debug(f"Total frames saved seeking: {frames_saved}")
@@ -186,7 +186,7 @@ def main(sys_args: list[str] | None = None):
186
186
  "wav/pcm-f32le.wav",
187
187
  "wav/pcm-s32le.wav",
188
188
  "multi-track.mov",
189
- "subtitle.mp4",
189
+ "mov_text.mp4",
190
190
  "testsrc.mkv",
191
191
  )
192
192
 
@@ -222,7 +222,8 @@ def main(sys_args: list[str] | None = None):
222
222
  run.raw(["levels", "resources/new-commentary.mp3"])
223
223
 
224
224
  def subdump():
225
- run.raw(["subdump", "resources/subtitle.mp4"])
225
+ run.raw(["subdump", "resources/mov_text.mp4"])
226
+ run.raw(["subdump", "resources/webvtt.mkv"])
226
227
 
227
228
  def desc():
228
229
  run.raw(["desc", "example.mp4"])
@@ -357,7 +358,13 @@ def main(sys_args: list[str] | None = None):
357
358
  run.main(["example.mp4"], ["--export", 'premiere:name="Foo Bar"'])
358
359
 
359
360
  def export_subtitles():
360
- cn = fileinfo(run.main(["resources/subtitle.mp4"], []))
361
+ cn = fileinfo(run.main(["resources/mov_text.mp4"], []))
362
+
363
+ assert len(cn.videos) == 1
364
+ assert len(cn.audios) == 1
365
+ assert len(cn.subtitles) == 1
366
+
367
+ cn = fileinfo(run.main(["resources/webvtt.mkv"], []))
361
368
 
362
369
  assert len(cn.videos) == 1
363
370
  assert len(cn.audios) == 1
@@ -10,8 +10,6 @@ if TYPE_CHECKING:
10
10
 
11
11
  from numpy.typing import NDArray
12
12
 
13
- from auto_editor.utils.log import Log
14
-
15
13
  BoolList = NDArray[np.bool_]
16
14
  BoolOperand = Callable[[BoolList, BoolList], BoolList]
17
15
 
@@ -135,30 +133,6 @@ def human_readable_time(time_in_secs: float) -> str:
135
133
  return f"{time_in_secs} {units}"
136
134
 
137
135
 
138
- def open_with_system_default(path: str, log: Log) -> None:
139
- import sys
140
- from subprocess import run
141
-
142
- if sys.platform == "win32":
143
- from os import startfile
144
-
145
- try:
146
- startfile(path)
147
- except OSError:
148
- log.warning("Could not find application to open file.")
149
- else:
150
- try: # MacOS case
151
- run(["open", path])
152
- except Exception:
153
- try: # WSL2 case
154
- run(["cmd.exe", "/C", "start", path])
155
- except Exception:
156
- try: # Linux case
157
- run(["xdg-open", path])
158
- except Exception:
159
- log.warning("Could not open output file.")
160
-
161
-
162
136
  def append_filename(path: str, val: str) -> str:
163
137
  from os.path import splitext
164
138
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: auto-editor
3
- Version: 25.3.0
3
+ Version: 25.3.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.14,>=3.10
12
12
  Description-Content-Type: text/markdown
13
13
  License-File: LICENSE
14
14
  Requires-Dist: numpy<3.0,>=1.23.0
15
- Requires-Dist: pyav==13.0.*
15
+ Requires-Dist: pyav==13.1.*
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>
@@ -11,7 +11,6 @@ auto_editor/make_layers.py
11
11
  auto_editor/output.py
12
12
  auto_editor/preview.py
13
13
  auto_editor/timeline.py
14
- auto_editor/validate_input.py
15
14
  auto_editor/vanparse.py
16
15
  auto_editor/wavfile.py
17
16
  auto_editor.egg-info/PKG-INFO
@@ -56,6 +55,5 @@ auto_editor/utils/container.py
56
55
  auto_editor/utils/encoder.py
57
56
  auto_editor/utils/func.py
58
57
  auto_editor/utils/log.py
59
- auto_editor/utils/subtitle_tools.py
60
58
  auto_editor/utils/types.py
61
59
  docs/build.py
@@ -1,3 +1,3 @@
1
1
  numpy<3.0,>=1.23.0
2
- pyav==13.0.*
2
+ pyav==13.1.*
3
3
  ae-ffmpeg==1.2.*
@@ -49,5 +49,6 @@ def main():
49
49
  print(e)
50
50
  quit(1)
51
51
 
52
+
52
53
  if __name__ == "__main__":
53
54
  main()
@@ -10,7 +10,7 @@ authors = [{ name = "WyattBlue", email = "wyattblue@auto-editor.com" }]
10
10
  requires-python = ">=3.10,<3.14"
11
11
  dependencies = [
12
12
  "numpy>=1.23.0,<3.0",
13
- "pyav==13.0.*",
13
+ "pyav==13.1.*",
14
14
  "ae-ffmpeg==1.2.*",
15
15
  ]
16
16
  keywords = [
@@ -1 +0,0 @@
1
- __version__ = "25.3.0"
@@ -1,29 +0,0 @@
1
- def convert_ass_to_text(ass_text: str) -> str:
2
- result = ""
3
- comma_count = i = 0
4
-
5
- while comma_count < 8 and i < len(ass_text):
6
- if ass_text[i] == ",":
7
- comma_count += 1
8
- i += 1
9
-
10
- state = False
11
- while i < len(ass_text):
12
- char = ass_text[i]
13
- next_char = "" if i + 1 >= len(ass_text) else ass_text[i + 1]
14
-
15
- if char == "\\" and next_char == "N":
16
- result += "\n"
17
- i += 2
18
- continue
19
-
20
- if not state:
21
- if char == "{":
22
- state = True
23
- else:
24
- result += ass_text[i]
25
- elif char == "}":
26
- state = False
27
- i += 1
28
-
29
- return result
@@ -1,88 +0,0 @@
1
- from __future__ import annotations
2
-
3
- import re
4
- import subprocess
5
- import sys
6
- from os.path import exists, isdir, isfile, lexists, splitext
7
-
8
- from auto_editor.ffwrapper import FFmpeg
9
- from auto_editor.utils.func import get_stdout
10
- from auto_editor.utils.log import Log
11
- from auto_editor.utils.types import Args
12
-
13
-
14
- def get_domain(url: str) -> str:
15
- from urllib.parse import urlparse
16
-
17
- t = urlparse(url).netloc
18
- return ".".join(t.split(".")[-2:])
19
-
20
-
21
- def download_video(my_input: str, args: Args, ffmpeg: FFmpeg, log: Log) -> str:
22
- log.conwrite("Downloading video...")
23
-
24
- download_format = args.download_format
25
-
26
- if download_format is None and get_domain(my_input) == "youtube.com":
27
- download_format = "bestvideo[ext=mp4]+bestaudio[ext=m4a]"
28
-
29
- if args.output_format is None:
30
- output_format = re.sub(r"\W+", "-", splitext(my_input)[0]) + ".%(ext)s"
31
- else:
32
- output_format = args.output_format
33
-
34
- yt_dlp_path = args.yt_dlp_location
35
-
36
- cmd = ["--ffmpeg-location", ffmpeg.path]
37
-
38
- if download_format is not None:
39
- cmd.extend(["-f", download_format])
40
-
41
- cmd.extend(["-o", output_format, my_input])
42
-
43
- if args.yt_dlp_extras is not None:
44
- cmd.extend(args.yt_dlp_extras.split(" "))
45
-
46
- try:
47
- location = get_stdout(
48
- [yt_dlp_path, "--get-filename", "--no-warnings"] + cmd
49
- ).strip()
50
- except FileNotFoundError:
51
- msg = "Could not find program 'yt-dlp' when attempting to download a URL. Install yt-dlp with "
52
- if sys.platform == "win32":
53
- msg += "your favorite package manager (pip, choco, winget)."
54
- elif sys.platform == "darwin":
55
- msg += "brew or pip and make sure it's in PATH."
56
- else:
57
- msg += "pip or your favorite package manager and make sure it's in PATH."
58
- log.error(msg)
59
-
60
- if not isfile(location):
61
- subprocess.run([yt_dlp_path] + cmd)
62
-
63
- if not isfile(location):
64
- log.error(f"Download file wasn't created: {location}")
65
-
66
- return location
67
-
68
-
69
- def valid_input(inputs: list[str], ffmpeg: FFmpeg, args: Args, log: Log) -> list[str]:
70
- result = []
71
-
72
- for my_input in inputs:
73
- if my_input.startswith("http://") or my_input.startswith("https://"):
74
- result.append(download_video(my_input, args, ffmpeg, log))
75
- else:
76
- _, ext = splitext(my_input)
77
- if ext == "":
78
- if isdir(my_input):
79
- log.error("Input must be a file or a URL, not a directory.")
80
- if exists(my_input):
81
- log.error(f"Input file must have an extension: {my_input}")
82
- if lexists(my_input):
83
- log.error(f"Input file is a broken symbolic link: {my_input}")
84
- if my_input.startswith("-"):
85
- log.error(f"Option/Input file doesn't exist: {my_input}")
86
- result.append(my_input)
87
-
88
- return result
File without changes
File without changes
File without changes