auto-editor 24.7.1__py3-none-any.whl → 24.9.1__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,2 +1,2 @@
1
- __version__ = "24.7.1"
2
- version = "24w07a"
1
+ __version__ = "24.9.1"
2
+ version = "24w09a"
auto_editor/analyze.py CHANGED
@@ -21,13 +21,10 @@ from auto_editor.lib.contracts import (
21
21
  from auto_editor.lib.data_structs import Sym
22
22
  from auto_editor.render.subtitle import SubtitleParser
23
23
  from auto_editor.utils.cmdkw import (
24
- ParserError,
25
24
  Required,
26
- parse_with_palet,
27
25
  pAttr,
28
26
  pAttrs,
29
27
  )
30
- from auto_editor.utils.func import boolop
31
28
  from auto_editor.wavfile import read
32
29
 
33
30
  if TYPE_CHECKING:
@@ -38,7 +35,6 @@ if TYPE_CHECKING:
38
35
  from numpy.typing import NDArray
39
36
 
40
37
  from auto_editor.ffwrapper import FileInfo
41
- from auto_editor.lib.data_structs import Env
42
38
  from auto_editor.output import Ensure
43
39
  from auto_editor.utils.bar import Bar
44
40
  from auto_editor.utils.log import Log
@@ -412,81 +408,3 @@ class Levels:
412
408
 
413
409
  self.bar.end()
414
410
  return self.cache("motion", mobj, threshold_list[:index])
415
-
416
-
417
- def edit_method(val: str, filesetup: FileSetup, env: Env) -> NDArray[np.bool_]:
418
- assert isinstance(filesetup, FileSetup)
419
- src = filesetup.src
420
- tb = filesetup.tb
421
- ensure = filesetup.ensure
422
- strict = filesetup.strict
423
- bar = filesetup.bar
424
- temp = filesetup.temp
425
- log = filesetup.log
426
-
427
- if ":" in val:
428
- method, attrs = val.split(":", 1)
429
- else:
430
- method, attrs = val, ""
431
-
432
- levels = Levels(ensure, src, tb, bar, temp, log)
433
-
434
- if method == "none":
435
- return levels.none()
436
- if method == "all/e":
437
- return levels.all()
438
-
439
- try:
440
- obj = parse_with_palet(attrs, builder_map[method], env)
441
- except ParserError as e:
442
- log.error(e)
443
-
444
- try:
445
- if method == "audio":
446
- s = obj["stream"]
447
- if s == "all" or s == Sym("all"):
448
- total_list: NDArray[np.bool_] | None = None
449
- for s in range(len(src.audios)):
450
- audio_list = to_threshold(levels.audio(s), obj["threshold"])
451
- if total_list is None:
452
- total_list = audio_list
453
- else:
454
- total_list = boolop(total_list, audio_list, np.logical_or)
455
-
456
- if total_list is None:
457
- if strict:
458
- log.error("Input has no audio streams.")
459
- stream_data = levels.all()
460
- else:
461
- stream_data = total_list
462
- else:
463
- assert isinstance(s, int)
464
- stream_data = to_threshold(levels.audio(s), obj["threshold"])
465
-
466
- assert isinstance(obj["minclip"], int)
467
- assert isinstance(obj["mincut"], int)
468
-
469
- mut_remove_small(stream_data, obj["minclip"], replace=1, with_=0)
470
- mut_remove_small(stream_data, obj["mincut"], replace=0, with_=1)
471
-
472
- return stream_data
473
-
474
- if method == "motion":
475
- return to_threshold(
476
- levels.motion(obj["stream"], obj["blur"], obj["width"]),
477
- obj["threshold"],
478
- )
479
-
480
- if method == "subtitle":
481
- return levels.subtitle(
482
- obj["pattern"],
483
- obj["stream"],
484
- obj["ignore_case"],
485
- obj["max_count"],
486
- )
487
- except LevelError as e:
488
- if strict:
489
- log.error(e)
490
-
491
- return levels.all()
492
- raise ValueError("Unreachable")
auto_editor/ffwrapper.py CHANGED
@@ -239,10 +239,13 @@ def initFileInfo(path: str, ffmpeg: FFmpeg, log: Log) -> FileInfo:
239
239
  log.debug("Unexpected ffprobe shape")
240
240
 
241
241
  if v.sample_aspect_ratio is None:
242
- try:
243
- sar = Fraction(_sar.replace(":", "/"))
244
- except Exception:
242
+ if _sar is None:
245
243
  sar = Fraction(1)
244
+ else:
245
+ try:
246
+ sar = Fraction(_sar.replace(":", "/"))
247
+ except Exception:
248
+ sar = Fraction(1)
246
249
  else:
247
250
  sar = v.sample_aspect_ratio
248
251
 
@@ -363,7 +363,6 @@ def media_def(
363
363
  ET.SubElement(rate, "ntsc").text = ntsc
364
364
  ET.SubElement(vschar, "width").text = f"{tl.res[0]}"
365
365
  ET.SubElement(vschar, "height").text = f"{tl.res[1]}"
366
- ET.SubElement(vschar, "anamorphic").text = "FALSE"
367
366
  ET.SubElement(vschar, "pixelaspectratio").text = "square"
368
367
 
369
368
  for aud in src.audios:
@@ -389,7 +388,7 @@ def fcp7_write_xml(name: str, output: str, tl: v3, log: Log) -> None:
389
388
  src_to_id[src] = the_id
390
389
 
391
390
  xmeml = ET.Element("xmeml", version="5")
392
- sequence = ET.SubElement(xmeml, "sequence")
391
+ sequence = ET.SubElement(xmeml, "sequence", explodedTracks="true")
393
392
  ET.SubElement(sequence, "name").text = name
394
393
  ET.SubElement(sequence, "duration").text = f"{int(tl.out_len())}"
395
394
  rate = ET.SubElement(sequence, "rate")
@@ -400,13 +399,14 @@ def fcp7_write_xml(name: str, output: str, tl: v3, log: Log) -> None:
400
399
  vformat = ET.SubElement(video, "format")
401
400
  vschar = ET.SubElement(vformat, "samplecharacteristics")
402
401
 
403
- rate = ET.SubElement(vschar, "rate")
404
- ET.SubElement(rate, "timebase").text = f"{timebase}"
405
- ET.SubElement(rate, "ntsc").text = ntsc
406
402
  ET.SubElement(vschar, "width").text = f"{width}"
407
403
  ET.SubElement(vschar, "height").text = f"{height}"
408
404
  ET.SubElement(vschar, "pixelaspectratio").text = "square"
409
405
 
406
+ rate = ET.SubElement(vschar, "rate")
407
+ ET.SubElement(rate, "timebase").text = f"{timebase}"
408
+ ET.SubElement(rate, "ntsc").text = ntsc
409
+
410
410
  if len(tl.v) > 0 and len(tl.v[0]) > 0:
411
411
  track = ET.SubElement(video, "track")
412
412
 
@@ -420,6 +420,7 @@ def fcp7_write_xml(name: str, output: str, tl: v3, log: Log) -> None:
420
420
 
421
421
  clipitem = ET.SubElement(track, "clipitem", id=f"clipitem-{j+1}")
422
422
  ET.SubElement(clipitem, "name").text = src.path.stem
423
+ ET.SubElement(clipitem, "enabled").text = "TRUE"
423
424
  ET.SubElement(clipitem, "start").text = _start
424
425
  ET.SubElement(clipitem, "end").text = _end
425
426
  ET.SubElement(clipitem, "in").text = _in
@@ -433,6 +434,7 @@ def fcp7_write_xml(name: str, output: str, tl: v3, log: Log) -> None:
433
434
  media_def(filedef, pathurl, clip.src, tl, timebase, ntsc)
434
435
  file_defs.add(pathurl)
435
436
 
437
+ ET.SubElement(clipitem, "compositemode").text = "normal"
436
438
  if clip.speed != 1:
437
439
  clipitem.append(speedup(clip.speed * 100))
438
440
 
@@ -444,8 +446,6 @@ def fcp7_write_xml(name: str, output: str, tl: v3, log: Log) -> None:
444
446
  ET.SubElement(link, "mediatype").text = "video" if i == 0 else "audio"
445
447
  ET.SubElement(link, "trackindex").text = str(max(i, 1))
446
448
  ET.SubElement(link, "clipindex").text = str(j + 1)
447
- if i > 0:
448
- ET.SubElement(link, "groupindex").text = "1"
449
449
 
450
450
  # Audio definitions and clips
451
451
  audio = ET.SubElement(media, "audio")
@@ -489,6 +489,7 @@ def fcp7_write_xml(name: str, output: str, tl: v3, log: Log) -> None:
489
489
  premiereChannelType="stereo",
490
490
  )
491
491
  ET.SubElement(clipitem, "name").text = src.path.stem
492
+ ET.SubElement(clipitem, "enabled").text = "TRUE"
492
493
  ET.SubElement(clipitem, "start").text = _start
493
494
  ET.SubElement(clipitem, "end").text = _end
494
495
  ET.SubElement(clipitem, "in").text = _in
@@ -502,7 +503,7 @@ def fcp7_write_xml(name: str, output: str, tl: v3, log: Log) -> None:
502
503
 
503
504
  sourcetrack = ET.SubElement(clipitem, "sourcetrack")
504
505
  ET.SubElement(sourcetrack, "mediatype").text = "audio"
505
- ET.SubElement(sourcetrack, "trackindex").text = f"{t + 1}"
506
+ ET.SubElement(sourcetrack, "trackindex").text = f"{t}"
506
507
  labels = ET.SubElement(clipitem, "labels")
507
508
  ET.SubElement(labels, "label2").text = "Iris"
508
509
 
@@ -512,5 +513,5 @@ def fcp7_write_xml(name: str, output: str, tl: v3, log: Log) -> None:
512
513
  audio.append(track)
513
514
 
514
515
  tree = ET.ElementTree(xmeml)
515
- ET.indent(tree, space="\t", level=0)
516
+ ET.indent(tree, space=" ", level=0)
516
517
  tree.write(output, xml_declaration=True, encoding="utf-8")
auto_editor/help.py CHANGED
@@ -25,16 +25,19 @@ will set the speed from 400 ticks to 800 ticks to 2.5x
25
25
  If timebase is 30, 400 ticks to 800 means 13.33 to 26.66 seconds
26
26
  """.strip(),
27
27
  "--edit-based-on": """
28
- Evalutes a palet expression that returns a bool-array?. The array is then used for
28
+ Evaluates a palet expression that returns a bool-array?. The array is then used for
29
29
  editing.
30
30
 
31
31
  Editing Methods:
32
32
  - audio ; Audio silence/loudness detection
33
33
  - threshold threshold? : 4%
34
- - stream (or/c nat? 'all "all") : 0
34
+ - stream (or/c nat? 'all) : 'all
35
35
  - mincut nat? : 6
36
36
  - minclip nat? : 3
37
37
 
38
+ ; mincut is more significant, there it has a larger default value.
39
+ ; minclip gets applied first, then mincut
40
+
38
41
  - motion ; Motion detection specialized for noisy real-life videos
39
42
  - threshold threshold? : 2%
40
43
  - stream nat? : 0
@@ -150,10 +153,6 @@ The special value `unset` may also be used, and means: Don't pass any value to f
150
153
  """.strip(),
151
154
  "--video-bitrate": """
152
155
  `--video-bitrate` sets the target bitrate for the video encoder. It accepts the same format as `--audio-bitrate` and the special `unset` value is allowed.
153
- """.strip(),
154
- "--silent-threshold": """
155
- Silent threshold is a percentage where 0% represents absolute silence and 100% represents the highest volume in the media file.
156
- Setting the threshold to `0%` will cut only out areas where area is absolutely silence.
157
156
  """.strip(),
158
157
  "--margin": """
159
158
  Default value: 0.2s,0.2s
auto_editor/lang/palet.py CHANGED
@@ -3,7 +3,6 @@ Palet is a light-weight scripting languge. It handles `--edit` and the `repl`.
3
3
  The syntax is inspired by the Racket Programming language.
4
4
  """
5
5
 
6
-
7
6
  from __future__ import annotations
8
7
 
9
8
  from cmath import sqrt as complex_sqrt
@@ -19,7 +18,12 @@ from typing import TYPE_CHECKING
19
18
  import numpy as np
20
19
  from numpy import logical_and, logical_not, logical_or, logical_xor
21
20
 
22
- from auto_editor.analyze import edit_method, mut_remove_large, mut_remove_small
21
+ from auto_editor.analyze import (
22
+ LevelError,
23
+ mut_remove_large,
24
+ mut_remove_small,
25
+ to_threshold,
26
+ )
23
27
  from auto_editor.lib.contracts import *
24
28
  from auto_editor.lib.data_structs import *
25
29
  from auto_editor.lib.err import MyError
@@ -48,9 +52,8 @@ class ClosingError(MyError):
48
52
  ###############################################################################
49
53
 
50
54
  LPAREN, RPAREN, LBRAC, RBRAC, LCUR, RCUR, EOF = "(", ")", "[", "]", "{", "}", "EOF"
51
- VAL, QUOTE, SEC, DB, DOT, VLIT = "VAL", "QUOTE", "SEC", "DB", "DOT", "VLIT"
55
+ VAL, QUOTE, SEC, DB, DOT, VLIT, M = "VAL", "QUOTE", "SEC", "DB", "DOT", "VLIT", "M"
52
56
  SEC_UNITS = ("s", "sec", "secs", "second", "seconds")
53
- METHODS = ("audio:", "motion:", "subtitle:")
54
57
  brac_pairs = {LPAREN: RPAREN, LBRAC: RBRAC, LCUR: RCUR}
55
58
 
56
59
  str_escape = {
@@ -316,7 +319,6 @@ class Lexer:
316
319
 
317
320
  result = ""
318
321
  has_illegal = False
319
- is_method = False
320
322
 
321
323
  def normal() -> bool:
322
324
  return (
@@ -333,22 +335,24 @@ class Lexer:
333
335
  else:
334
336
  return self.char_is_norm()
335
337
 
338
+ is_method = False
336
339
  while normal():
337
- result += self.char
338
- if (result + ":") in METHODS:
340
+ if self.char == ":":
341
+ name = result
342
+ result = ""
339
343
  is_method = True
340
344
  normal = handle_strings
345
+ else:
346
+ result += self.char
341
347
 
342
348
  if self.char in "'`|\\":
343
349
  has_illegal = True
344
350
  self.advance()
345
351
 
346
352
  if is_method:
347
- return Token(VAL, Method(result))
353
+ from auto_editor.utils.cmdkw import parse_method
348
354
 
349
- for method in METHODS:
350
- if result == method[:-1]:
351
- return Token(VAL, Method(result))
355
+ return Token(M, parse_method(name, result, env))
352
356
 
353
357
  if self.char == ".": # handle `object.method` syntax
354
358
  self.advance()
@@ -369,16 +373,6 @@ class Lexer:
369
373
  ###############################################################################
370
374
 
371
375
 
372
- @dataclass(slots=True)
373
- class Method:
374
- val: str
375
-
376
- def __str__(self) -> str:
377
- return f"#<method:{self.val}>"
378
-
379
- __repr__ = __str__
380
-
381
-
382
376
  class Parser:
383
377
  def __init__(self, lexer: Lexer):
384
378
  self.lexer = lexer
@@ -413,6 +407,16 @@ class Parser:
413
407
  self.eat()
414
408
  return (Sym("pow"), 10, (Sym("/"), token.value, 20))
415
409
 
410
+ if token.type == M:
411
+ self.eat()
412
+ name, args, kwargs = token.value
413
+ _result = [Sym(name)] + args
414
+ for key, val in kwargs.items():
415
+ _result.append(Keyword(key))
416
+ _result.append(val)
417
+
418
+ return tuple(_result)
419
+
416
420
  if token.type == DOT:
417
421
  self.eat()
418
422
  if type(token.value[1].value) is not Sym:
@@ -506,7 +510,7 @@ def initOutPort(name: str) -> OutputPort | Literal[False]:
506
510
  return OutputPort(name, port, port.write, False)
507
511
 
508
512
 
509
- def raise_(msg: str) -> None:
513
+ def raise_(msg: str | Exception) -> NoReturn:
510
514
  raise MyError(msg)
511
515
 
512
516
 
@@ -808,7 +812,7 @@ class UserProc(Proc):
808
812
 
809
813
 
810
814
  @dataclass(slots=True)
811
- class KeywordProc:
815
+ class KeywordUserProc:
812
816
  env: Env
813
817
  name: str
814
818
  parms: list[str]
@@ -817,38 +821,21 @@ class KeywordProc:
817
821
  arity: tuple[int, None]
818
822
  contracts: list[Any] | None = None
819
823
 
820
- def __call__(self, *args: Any) -> Any:
824
+ def __call__(self, *args: Any, **kwargs: Any) -> Any:
821
825
  env = {}
822
-
823
- for i, parm in enumerate(self.parms):
824
- if type(args[i]) is Keyword:
825
- raise MyError(f"Invalid keyword `{args[i]}`")
826
- env[parm] = args[i]
827
-
828
- remain_args = args[len(self.parms) :]
829
-
830
- allow_pos = True
831
- pos_index = 0
832
- key = ""
833
- for arg in remain_args:
834
- if type(arg) is Keyword:
835
- if key:
836
- raise MyError("Expected value for keyword but got another keyword")
837
- key = arg.val
838
- allow_pos = False
839
- elif key:
840
- env[key] = arg
841
- key = ""
826
+ all_parms = self.parms + self.kw_parms
827
+ for i, arg in enumerate(args):
828
+ if i >= len(all_parms):
829
+ raise MyError("Too many arguments")
830
+ env[all_parms[i]] = arg
831
+
832
+ for key, val in kwargs.items():
833
+ if key in env:
834
+ raise MyError(
835
+ f"Keyword: {key} already fulfilled by positional argument."
836
+ )
842
837
  else:
843
- if not allow_pos:
844
- raise MyError("Positional argument not allowed here")
845
- if pos_index >= len(self.kw_parms):
846
- base = f"`{self.name}` has an arity mismatch. Expected"
847
- upper = len(self.parms) + len(self.kw_parms)
848
- raise MyError(f"{base} at most {upper}")
849
-
850
- env[self.kw_parms[pos_index]] = arg
851
- pos_index += 1
838
+ env[key] = val
852
839
 
853
840
  inner_env = Env(env, self.env)
854
841
 
@@ -953,7 +940,7 @@ def syn_define(env: Env, node: Node) -> None:
953
940
  raise MyError(f"{node[0]}: must be an identifier")
954
941
 
955
942
  if kw_only:
956
- env[n] = KeywordProc(env, n, parms, kparms, body, (len(parms), None))
943
+ env[n] = KeywordUserProc(env, n, parms, kparms, body, (len(parms), None))
957
944
  else:
958
945
  env[n] = UserProc(env, n, parms, (), body)
959
946
  return None
@@ -1482,6 +1469,81 @@ def edit_all() -> np.ndarray:
1482
1469
  return env["@levels"].all()
1483
1470
 
1484
1471
 
1472
+ def edit_audio(
1473
+ threshold: float = 0.04,
1474
+ stream: object = Sym("all"),
1475
+ mincut: int = 6,
1476
+ minclip: int = 3,
1477
+ ) -> np.ndarray:
1478
+ if "@levels" not in env or "@filesetup" not in env:
1479
+ raise MyError("Can't use `audio` if there's no input media")
1480
+
1481
+ levels = env["@levels"]
1482
+ src = env["@filesetup"].src
1483
+ strict = env["@filesetup"].strict
1484
+
1485
+ stream_data: NDArray[np.bool_] | None = None
1486
+ if stream == Sym("all"):
1487
+ stream_range = range(0, len(src.audios))
1488
+ else:
1489
+ assert isinstance(stream, int)
1490
+ stream_range = range(stream, stream + 1)
1491
+
1492
+ try:
1493
+ for s in stream_range:
1494
+ audio_list = to_threshold(levels.audio(s), threshold)
1495
+ if stream_data is None:
1496
+ stream_data = audio_list
1497
+ else:
1498
+ stream_data = boolop(stream_data, audio_list, np.logical_or)
1499
+ except LevelError as e:
1500
+ raise_(e) if strict else levels.all()
1501
+
1502
+ if stream_data is not None:
1503
+ mut_remove_small(stream_data, minclip, replace=1, with_=0)
1504
+ mut_remove_small(stream_data, mincut, replace=0, with_=1)
1505
+
1506
+ return stream_data
1507
+
1508
+ stream = 0 if stream == Sym("all") else stream
1509
+ return raise_(f"audio stream '{stream}' does not exist") if strict else levels.all()
1510
+
1511
+
1512
+ def edit_motion(
1513
+ threshold: float = 0.02,
1514
+ stream: int = 0,
1515
+ blur: int = 9,
1516
+ width: int = 400,
1517
+ ) -> np.ndarray:
1518
+ if "@levels" not in env:
1519
+ raise MyError("Can't use `motion` if there's no input media")
1520
+
1521
+ levels = env["@levels"]
1522
+ strict = env["@filesetup"].strict
1523
+ try:
1524
+ return to_threshold(levels.motion(stream, blur, width), threshold)
1525
+ except LevelError as e:
1526
+ return raise_(e) if strict else levels.all()
1527
+
1528
+
1529
+ def edit_subtitle(pattern, stream=0, **kwargs):
1530
+ if "@levels" not in env:
1531
+ raise MyError("Can't use `subtitle` if there's no input media")
1532
+
1533
+ levels = env["@levels"]
1534
+ strict = env["@filesetup"].strict
1535
+ if "ignore-case" not in kwargs:
1536
+ kwargs["ignore-case"] = False
1537
+ if "max-count" not in kwargs:
1538
+ kwargs["max-count"] = None
1539
+ ignore_case = kwargs["ignore-case"]
1540
+ max_count = kwargs["max-count"]
1541
+ try:
1542
+ return levels.subtitle(pattern, stream, ignore_case, max_count)
1543
+ except LevelError as e:
1544
+ return raise_(e) if strict else levels.all()
1545
+
1546
+
1485
1547
  def my_eval(env: Env, node: object) -> Any:
1486
1548
  if type(node) is Sym:
1487
1549
  val = env.get(node.val)
@@ -1495,11 +1557,6 @@ def my_eval(env: Env, node: object) -> Any:
1495
1557
  )
1496
1558
  return val
1497
1559
 
1498
- if isinstance(node, Method):
1499
- if "@filesetup" not in env:
1500
- raise MyError("Can't use edit methods if there's no input files")
1501
- return edit_method(node.val, env["@filesetup"], env)
1502
-
1503
1560
  if type(node) is list:
1504
1561
  return [my_eval(env, item) for item in node]
1505
1562
 
@@ -1531,7 +1588,21 @@ def my_eval(env: Env, node: object) -> Any:
1531
1588
  if type(oper) is Syntax:
1532
1589
  return oper(env, node)
1533
1590
 
1534
- return oper(*(my_eval(env, c) for c in node[1:]))
1591
+ i = 1
1592
+ args: list[Any] = []
1593
+ kwargs: dict[str, Any] = {}
1594
+ while i < len(node):
1595
+ result = my_eval(env, node[i])
1596
+ if type(result) is Keyword:
1597
+ i += 1
1598
+ if i >= len(node):
1599
+ raise MyError("Keyword need argument")
1600
+ kwargs[result.val] = my_eval(env, node[i])
1601
+ else:
1602
+ args.append(result)
1603
+ i += 1
1604
+
1605
+ return oper(*args, **kwargs)
1535
1606
 
1536
1607
  return node
1537
1608
 
@@ -1546,6 +1617,18 @@ env.update({
1546
1617
  # edit procedures
1547
1618
  "none": Proc("none", edit_none, (0, 0)),
1548
1619
  "all/e": Proc("all/e", edit_all, (0, 0)),
1620
+ "audio": Proc("audio", edit_audio, (0, 4),
1621
+ is_threshold, orc(is_nat, Sym("all")), is_nat,
1622
+ {"threshold": 0, "stream": 1, "minclip": 2, "mincut": 2}
1623
+ ),
1624
+ "motion": Proc("motion", edit_motion, (0, 4),
1625
+ is_threshold, is_nat, is_nat1,
1626
+ {"threshold": 0, "stream": 1, "blur": 1, "width": 2}
1627
+ ),
1628
+ "subtitle": Proc("subtitle", edit_subtitle, (1, 4),
1629
+ is_str, is_nat, is_bool, orc(is_nat, is_void),
1630
+ {"pattern": 0, "stream": 1, "ignore-case": 2, "max-count": 3}
1631
+ ),
1549
1632
  # syntax
1550
1633
  "lambda": Syntax(syn_lambda),
1551
1634
  "λ": Syntax(syn_lambda),
@@ -47,7 +47,7 @@ def check_contract(c: object, val: object) -> bool:
47
47
 
48
48
 
49
49
  def check_args(
50
- o: str,
50
+ name: str,
51
51
  values: list | tuple,
52
52
  arity: tuple[int, int | None],
53
53
  cont: tuple[Any, ...],
@@ -56,7 +56,7 @@ def check_args(
56
56
  amount = len(values)
57
57
 
58
58
  assert not (upper is not None and lower > upper)
59
- base = f"`{o}` has an arity mismatch. Expected "
59
+ base = f"`{name}` has an arity mismatch. Expected "
60
60
 
61
61
  if lower == upper and len(values) != lower:
62
62
  raise MyError(f"{base}{lower}, got {amount}")
@@ -72,11 +72,11 @@ def check_args(
72
72
  check = cont[-1] if i >= len(cont) else cont[i]
73
73
  if not check_contract(check, val):
74
74
  exp = f"{check}" if callable(check) else print_str(check)
75
- raise MyError(f"`{o}` expected a {exp}, got {print_str(val)}")
75
+ raise MyError(f"`{name}` expected {exp}, but got {print_str(val)}")
76
76
 
77
77
 
78
78
  class Proc:
79
- __slots__ = ("name", "proc", "arity", "contracts")
79
+ __slots__ = ("name", "proc", "arity", "contracts", "kw_contracts")
80
80
 
81
81
  def __init__(
82
82
  self, n: str, p: Callable, a: tuple[int, int | None] = (1, None), *c: Any
@@ -84,11 +84,52 @@ class Proc:
84
84
  self.name = n
85
85
  self.proc = p
86
86
  self.arity = a
87
- self.contracts: tuple[Any, ...] = c
88
87
 
89
- def __call__(self, *args: Any) -> Any:
90
- check_args(self.name, args, self.arity, self.contracts)
91
- return self.proc(*args)
88
+ if c and type(c[-1]) is dict:
89
+ self.kw_contracts: dict[str, int] | None = c[-1]
90
+ self.contracts: tuple[Any, ...] = c[:-1]
91
+ else:
92
+ self.kw_contracts = None
93
+ self.contracts = c
94
+
95
+ def __call__(self, *args: Any, **kwargs: Any):
96
+ lower, upper = self.arity
97
+ amount = len(args)
98
+ cont = self.contracts
99
+ kws = self.kw_contracts
100
+
101
+ assert not (upper is not None and lower > upper)
102
+ base = f"`{self.name}` has an arity mismatch. Expected "
103
+
104
+ if lower == upper and len(args) != lower:
105
+ raise MyError(f"{base}{lower}, got {amount}")
106
+ if upper is None and amount < lower:
107
+ raise MyError(f"{base}at least {lower}, got {amount}")
108
+ if upper is not None and (amount > upper or amount < lower):
109
+ raise MyError(f"{base}between {lower} and {upper}, got {amount}")
110
+
111
+ if not cont:
112
+ return self.proc(*args)
113
+
114
+ if kws is not None:
115
+ for key, val in kwargs.items():
116
+ check = cont[-1] if kws[key] >= len(cont) else cont[kws[key]]
117
+ if not check_contract(check, val):
118
+ exp = f"{check}" if callable(check) else print_str(check)
119
+ raise MyError(
120
+ f"`{self.name} #:{key}` expected {exp}, but got {print_str(val)}"
121
+ )
122
+
123
+ elif len(kwargs) > 0:
124
+ raise MyError("Keyword arguments are not allowed here")
125
+
126
+ for i, val in enumerate(args):
127
+ check = cont[-1] if i >= len(cont) else cont[i]
128
+ if not check_contract(check, val):
129
+ exp = f"{check}" if callable(check) else print_str(check)
130
+ raise MyError(f"`{self.name}` expected {exp}, but got {print_str(val)}")
131
+
132
+ return self.proc(*args, **kwargs)
92
133
 
93
134
  def __str__(self) -> str:
94
135
  return self.name
@@ -137,13 +178,19 @@ is_threshold = Contract(
137
178
  is_proc = Contract("procedure?", lambda v: isinstance(v, Proc | Contract))
138
179
 
139
180
 
181
+ def contract_printer(cs) -> str:
182
+ return " ".join(
183
+ c.name if isinstance(c, Proc | Contract) else print_str(c) for c in cs
184
+ )
185
+
186
+
140
187
  def andc(*cs: object) -> Proc:
141
- name = "(and/c " + " ".join(f"{c}" for c in cs) + ")"
188
+ name = f"(and/c {contract_printer(cs)})"
142
189
  return Proc(name, lambda v: all(check_contract(c, v) for c in cs), (1, 1), any_p)
143
190
 
144
191
 
145
192
  def orc(*cs: object) -> Proc:
146
- name = "(or/c " + " ".join(f"{c}" for c in cs) + ")"
193
+ name = f"(or/c {contract_printer(cs)})"
147
194
  return Proc(name, lambda v: any(check_contract(c, v) for c in cs), (1, 1), any_p)
148
195
 
149
196
 
@@ -57,6 +57,7 @@ class Sym:
57
57
  __slots__ = ("val", "hash")
58
58
 
59
59
  def __init__(self, val: str):
60
+ assert isinstance(val, str)
60
61
  self.val = val
61
62
  self.hash = hash(val)
62
63
 
auto_editor/output.py CHANGED
@@ -193,7 +193,7 @@ def mux_quality_media(
193
193
  cmd.extend(args.extras.split(" "))
194
194
  cmd.extend(["-strict", "-2"]) # Allow experimental codecs.
195
195
 
196
- if not args.sn:
196
+ if s_tracks > 0:
197
197
  cmd.extend(["-map", "0:t?"]) # Add input attachments to output.
198
198
 
199
199
  # This was causing a crash for 'example.mp4 multi-track.mov'
@@ -97,6 +97,7 @@ def make_image_cache(tl: v3) -> dict[tuple[FileInfo, int], np.ndarray]:
97
97
  for obj in clip:
98
98
  if isinstance(obj, TlImage) and obj.src not in img_cache:
99
99
  with av.open(obj.src.path) as cn:
100
+ assert isinstance(cn, av.InputContainer)
100
101
  my_stream = cn.streams.video[0]
101
102
  for frame in cn.decode(my_stream):
102
103
  if obj.width != 0:
@@ -168,15 +169,26 @@ def render_av(
168
169
  log.debug(f"Target pix_fmt: {target_pix_fmt}")
169
170
 
170
171
  apply_video_later = True
171
-
172
- if args.scale != 1:
173
- apply_video_later = False
174
- elif args.video_codec in encoders:
172
+ if args.video_codec in encoders:
175
173
  apply_video_later = set(encoders[args.video_codec]).isdisjoint(allowed_pix_fmt)
176
174
 
177
175
  log.debug(f"apply video quality settings now: {not apply_video_later}")
178
176
 
179
- width, height = tl.res
177
+ if args.scale == 1.0:
178
+ target_width, target_height = tl.res
179
+ scale_graph = None
180
+ else:
181
+ target_width = max(round(tl.res[0] * args.scale), 2)
182
+ target_height = max(round(tl.res[1] * args.scale), 2)
183
+ scale_graph = av.filter.Graph()
184
+ link_nodes(
185
+ scale_graph.add(
186
+ "buffer", video_size="1x1", time_base="1/1", pix_fmt=target_pix_fmt
187
+ ),
188
+ scale_graph.add("scale", f"{target_width}:{target_height}"),
189
+ scale_graph.add("buffersink"),
190
+ )
191
+
180
192
  spedup = os.path.join(temp, "spedup0.mp4")
181
193
 
182
194
  cmd = [
@@ -189,7 +201,7 @@ def render_av(
189
201
  "-pix_fmt",
190
202
  target_pix_fmt,
191
203
  "-s",
192
- f"{width}*{height}",
204
+ f"{target_width}*{target_height}",
193
205
  "-framerate",
194
206
  f"{tl.tb}",
195
207
  "-i",
@@ -224,7 +236,7 @@ def render_av(
224
236
  bar.start(tl.end, "Creating new video")
225
237
 
226
238
  bg = color(args.background)
227
- null_frame = make_solid(width, height, target_pix_fmt, bg)
239
+ null_frame = make_solid(target_width, target_height, target_pix_fmt, bg)
228
240
  frame_index = -1
229
241
  try:
230
242
  for index in range(tl.end):
@@ -282,7 +294,8 @@ def render_av(
282
294
  if frame.key_frame:
283
295
  log.debug(f"Keyframe {frame_index} {frame.pts}")
284
296
 
285
- if frame.width != width or frame.height != height:
297
+ if (frame.width, frame.height) != tl.res:
298
+ width, height = tl.res
286
299
  graph = av.filter.Graph()
287
300
  link_nodes(
288
301
  graph.add_buffer(template=my_stream),
@@ -345,6 +358,10 @@ def render_av(
345
358
 
346
359
  frame = av.VideoFrame.from_ndarray(array, format="rgb24")
347
360
 
361
+ if scale_graph is not None and frame.width != target_width:
362
+ scale_graph.push(frame)
363
+ frame = scale_graph.pull()
364
+
348
365
  if frame.format.name != target_pix_fmt:
349
366
  frame = frame.reformat(format=target_pix_fmt)
350
367
  bar.tick(index)
@@ -363,19 +380,4 @@ def render_av(
363
380
 
364
381
  log.debug(f"Total frames saved seeking: {frames_saved}")
365
382
 
366
- if args.scale != 1:
367
- sped_input = os.path.join(temp, "spedup0.mp4")
368
- spedup = os.path.join(temp, "scale0.mp4")
369
- scale_filter = f"scale=iw*{args.scale}:ih*{args.scale}"
370
-
371
- cmd = ["-i", sped_input, "-vf", scale_filter, spedup]
372
-
373
- check_errors = ffmpeg.pipe(cmd)
374
- if "Error" in check_errors or "failed" in check_errors:
375
- if "-allow_sw 1" in check_errors:
376
- cmd.insert(-1, "-allow_sw")
377
- cmd.insert(-1, "1")
378
- # Run again to show errors even if it might not work.
379
- ffmpeg.run(cmd)
380
-
381
383
  return spedup, apply_video_later
@@ -132,6 +132,7 @@ def run_tests(tests: list[Callable], args: TestArgs) -> None:
132
132
  for index, test in enumerate(tests, start=1):
133
133
  name = test.__name__
134
134
  start = perf_counter()
135
+ outputs = None
135
136
 
136
137
  try:
137
138
  outputs = test()
@@ -142,6 +143,8 @@ def run_tests(tests: list[Callable], args: TestArgs) -> None:
142
143
  clean_all()
143
144
  sys.exit(1)
144
145
  except Exception as e:
146
+ dur = perf_counter() - start
147
+ total_time += dur
145
148
  print(f"{name:<24} ({index}/{total}) {round(dur, 2):<4} secs [FAILED]")
146
149
  if args.no_fail_fast:
147
150
  print(f"\n{e}")
@@ -684,8 +687,8 @@ def main(sys_args: list[str] | None = None):
684
687
  )
685
688
 
686
689
  def palet_scripts():
687
- run.raw(["palet", "resources/scripts/maxcut.pal"])
688
690
  run.raw(["palet", "resources/scripts/scope.pal"])
691
+ run.raw(["palet", "resources/scripts/maxcut.pal"])
689
692
  run.raw(["palet", "resources/scripts/case.pal"])
690
693
  run.raw(["palet", "resources/scripts/testmath.pal"])
691
694
 
@@ -172,3 +172,44 @@ def parse_with_palet(
172
172
  raise ParserError(f"'{k}' must be specified.")
173
173
 
174
174
  return kwargs
175
+
176
+
177
+ def parse_method(
178
+ name: str, text: str, env: Env
179
+ ) -> tuple[str, list[Any], dict[str, Any]]:
180
+ from auto_editor.lang.palet import Lexer, Parser, interpret
181
+ from auto_editor.lib.err import MyError
182
+
183
+ # Positional Arguments
184
+ # audio:0.04,0,6,3
185
+ # Keyword Arguments
186
+ # audio:threshold=0.04,stream=0,mincut=6,minclip=3
187
+
188
+ args: list[Any] = []
189
+ kwargs: dict[str, Any] = {}
190
+
191
+ allow_positional_args = True
192
+ lexer = PLexer(text)
193
+ while (arg := lexer.get_next_token()) is not None:
194
+ if not arg:
195
+ continue
196
+
197
+ if "=" in arg:
198
+ key, val = arg.split("=", 1)
199
+
200
+ results = interpret(env, Parser(Lexer(name, val)))
201
+ if not results:
202
+ raise MyError("Results must be of length > 0")
203
+
204
+ kwargs[key] = results[-1]
205
+ allow_positional_args = False
206
+
207
+ elif allow_positional_args:
208
+ results = interpret(env, Parser(Lexer(name, arg)))
209
+ if not results:
210
+ raise MyError("Results must be of length > 0")
211
+ args.append(results[-1])
212
+ else:
213
+ raise ParserError(f"{name} positional argument follows keyword argument.")
214
+
215
+ return name, args, kwargs
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: auto-editor
3
- Version: 24.7.1
3
+ Version: 24.9.1
4
4
  Summary: Auto-Editor: Effort free video editing!
5
5
  Author-email: WyattBlue <wyattblue@auto-editor.com>
6
6
  License: Unlicense
@@ -1,14 +1,14 @@
1
1
  ae-ffmpeg/setup.py,sha256=HeORyrs8OyJ32lSnMaIhI2B7U1lkk3QP6wOjxpoiF3Y,1891
2
2
  ae-ffmpeg/ae_ffmpeg/__init__.py,sha256=Y_dn5Uoh1fOrhhJ9B3agOsPF2VXnCbIaXuN3ZRft-pk,453
3
3
  ae-ffmpeg/ae_ffmpeg/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
4
- auto_editor/__init__.py,sha256=Kdyio-Bzf_iENaYEJklkQz98d9-98J_j2hzqLHDU9Yc,42
4
+ auto_editor/__init__.py,sha256=RJIMlcL3cdE7LOygV0nkUUrFZqTrOQb2DimvfAvLfF8,42
5
5
  auto_editor/__main__.py,sha256=Lb_0h7Zop0SHK-nLWgwp7MWFrznuir8Ilo17Vx_0aKs,9827
6
- auto_editor/analyze.py,sha256=DP4L-JF0W3BFt50TlKiyNWufAQZN9ytvJ3SsNKk3AVk,14489
6
+ auto_editor/analyze.py,sha256=zvN4hXyEGXdUUVkfnYlyrCXPgBRl3DoQtBwIfHo7q68,11938
7
7
  auto_editor/edit.py,sha256=oL8-oCHMbTARb89IY4tsnSs_vWeqk6lwVvNhdw4Xp5M,12013
8
- auto_editor/ffwrapper.py,sha256=nBu1lE5-HlqvpiczG0kHI__JAEW6hE3PlGc-X2QUtWM,8570
9
- auto_editor/help.py,sha256=RGCUb_5_lPFwbrXOlgnwlB0G_L88LN8yx-aycTeu8Hs,8150
8
+ auto_editor/ffwrapper.py,sha256=TrYdEyjzbpRif4mGWfi8FHkRl5chbI07-tUUyg-CpBA,8667
9
+ auto_editor/help.py,sha256=BFiP7vBz42TUzum4-zaQIrV1OY7kHeN0pe0MPE0T5xw,7997
10
10
  auto_editor/make_layers.py,sha256=_YyuV7JvF9sneu3FJQPDkvRqzja8Fzscr4624bXN4iI,8214
11
- auto_editor/output.py,sha256=QA7Xk05RfahZCDUmi25D_nvw-iGVM2kqz0RWFSqD7Os,6310
11
+ auto_editor/output.py,sha256=NkdwGbiUAXvieOqjmsH4jMj3pplSKAOrbhN503U2o-o,6311
12
12
  auto_editor/preview.py,sha256=K10TyP0_LWD4yEIUHzqSHs_97hKl0VIaIuA1xGHI8ZI,3023
13
13
  auto_editor/timeline.py,sha256=JwcS-8AS5vsoTL_m03aosYijScQef4AGa2lyutQ8wbI,7069
14
14
  auto_editor/validate_input.py,sha256=G4LzUdt0fSrIPRd-wvP7x9cOzXmHTd7-BPrFk2ZNEWk,2671
@@ -16,22 +16,22 @@ auto_editor/vanparse.py,sha256=kHvGK7itqt37q0MPTSriPljB7ilFpjG5LuEVdulUbyg,9902
16
16
  auto_editor/wavfile.py,sha256=UatK5yRJGe2vIpHC1U0ccxyixaLyzLoNvApzWcrxLy8,9168
17
17
  auto_editor/formats/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
18
18
  auto_editor/formats/fcp11.py,sha256=3Z7-AYkGmlnk2WyzNG11483krZqj3XRAnvw491RTxHE,5860
19
- auto_editor/formats/fcp7.py,sha256=bEEUUgEg9rGQi3bxSjnJgoJppbM5zMiqXw5PLLlilvs,17692
19
+ auto_editor/formats/fcp7.py,sha256=zzkcq6U0NYlckc1X81ioStd4QbdN4pj_de4EHlV5dbo,17757
20
20
  auto_editor/formats/json.py,sha256=AFrQwjzmMMoEJFgYNEipnz2X75ftRfHmPHJKWW_KPwo,6741
21
21
  auto_editor/formats/shotcut.py,sha256=9XM-NGDVSrHyG05Tsq6RR6WacyatQxGa6wPuRu-QtXU,5023
22
22
  auto_editor/formats/utils.py,sha256=GIZw28WHuCIaZ_zMI0v6Kxbq0QaIpbLsdSegdYwQxQ8,1990
23
23
  auto_editor/lang/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
24
24
  auto_editor/lang/json.py,sha256=OsNcYlfEj8ZLlzLK-gkLcrCCKI7mJz9rpe-6XLr4f9U,9231
25
25
  auto_editor/lang/libmath.py,sha256=z33A161Oe6vYYK7R6pgYjdZZe63dQkN38Qf36TL3prg,847
26
- auto_editor/lang/palet.py,sha256=qxd3TwejLZLaiJZ_5cleD2hp0Nn8Gjq8koOILVChbFs,56368
26
+ auto_editor/lang/palet.py,sha256=9eQwirwwejf0qrXxXqwETEFv4MQ98jUs-0X_wHMaxRg,59027
27
27
  auto_editor/lib/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
28
- auto_editor/lib/contracts.py,sha256=olP7QkjP91Tp8zXJmv5-ktwIPmbvh_jKDdZMkFM5j0E,5462
29
- auto_editor/lib/data_structs.py,sha256=y1PXdrMz9im7IAWqNQbNuFG3hXHMaL6MsY4eLVPiUcU,6851
28
+ auto_editor/lib/contracts.py,sha256=CTay7wMWnMSbTD1QAwE-b6yvIcSPvkpxkFQRXoJiqZE,7258
29
+ auto_editor/lib/data_structs.py,sha256=EXNcdMsdmZxMRlpbXmIbRoC-YfGzvPZi7EdBQGwvpP4,6887
30
30
  auto_editor/lib/err.py,sha256=UlszQJdzMZwkbT8x3sY4GkCV_5x9yrd6uVVUzvA8iiI,35
31
31
  auto_editor/render/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
32
32
  auto_editor/render/audio.py,sha256=lEmPuRKW5QipJV6ncCx8CHjg7wyK1knwqRM5p7zKvBY,8816
33
33
  auto_editor/render/subtitle.py,sha256=D4WDiY4iM9HsNfJvZay7zv_gvZPvyd12nd9Fi9vbPjQ,4646
34
- auto_editor/render/video.py,sha256=C9QBuXC1itqd1q1AHZ-f_63UgdCTV3w5vEhLUfrWTrs,13127
34
+ auto_editor/render/video.py,sha256=LufVJJW-r0_5xWSGGpOIH3Clh63FWxEJuct-TORErDQ,13286
35
35
  auto_editor/subcommands/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
36
36
  auto_editor/subcommands/desc.py,sha256=CWnRk3LCTV_sMVJVA45ZMBfPy_ESfwqkySMVuW8Fbrg,1032
37
37
  auto_editor/subcommands/info.py,sha256=etsjaKBEy0AxDtZ7XZNiizSmBnx8Wf2HeRfUyPyBCqA,6237
@@ -39,19 +39,19 @@ auto_editor/subcommands/levels.py,sha256=utCuRmpa2mirnQ_t1ogPexqjZAUBTC9KrCSS_Bb
39
39
  auto_editor/subcommands/palet.py,sha256=tbQoRWoT4jR3yu0etGApfprM-oQgXIjC-rIY-QG3nM0,655
40
40
  auto_editor/subcommands/repl.py,sha256=j5jxWgscaaRULa6ZsrV4tDJQB4vOzxiEQR0jI90v5s0,3725
41
41
  auto_editor/subcommands/subdump.py,sha256=GGekYMnqLkcqfihKjlHcuWkMazvgsYTF0q4ulEDOrRc,1669
42
- auto_editor/subcommands/test.py,sha256=I08qk9mPBwCVMVVhR66C0we_R5a9khXhFuqBrqwW10s,24729
42
+ auto_editor/subcommands/test.py,sha256=YOhmvehSohM1nCbU-Ivub_xEJEM57NwHuadLbrojJ84,24823
43
43
  auto_editor/utils/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
44
44
  auto_editor/utils/bar.py,sha256=eWpiXZpRc2v2LW-EaoAgG_cTtMh5275or8Ttda3Ei-I,3974
45
45
  auto_editor/utils/chunks.py,sha256=J-eGKtEz68gFtRrj1kOSgH4Tj_Yz6prNQ7Xr-d9NQJw,52
46
- auto_editor/utils/cmdkw.py,sha256=qysqLvOHKXlRDcChGnRvP1Ar_Tud_FOhonDQt90Hjog,4785
46
+ auto_editor/utils/cmdkw.py,sha256=XApxw7FZBOEJV9N4LHhdw1GVfHbFfCjr-zCZ1gJsSvY,6002
47
47
  auto_editor/utils/container.py,sha256=WOMlUJ5pxVmeYsy79uCWMU5fCzM4tBRupAx-_Q5-PLg,7939
48
48
  auto_editor/utils/encoder.py,sha256=auNYo7HXbcU4iTUCc0LE5lpwFmSvdWvBm6-5KIaRK8w,2983
49
49
  auto_editor/utils/func.py,sha256=H38xO6Wxg1TZILVrx-nCowCzj_mqBUtJuOFp4DV3Hsc,4843
50
50
  auto_editor/utils/log.py,sha256=6j2EWE97_urQijBvxhk2Gr2-VO_KNR1XbEobcAtTG-w,2668
51
51
  auto_editor/utils/types.py,sha256=aWyJpVBjmctxlxiL5o8r6lplKnaFSjVNQlcoXFgfmSk,11533
52
- auto_editor-24.7.1.dist-info/LICENSE,sha256=yiq99pWITHfqS0pbZMp7cy2dnbreTuvBwudsU-njvIM,1210
53
- auto_editor-24.7.1.dist-info/METADATA,sha256=o8bjNQQc99f4kJtauiZegijPiE45ikxTuiJOa9Z2ATE,7092
54
- auto_editor-24.7.1.dist-info/WHEEL,sha256=oiQVh_5PnQM0E3gPdiz09WCNmwiHDMaGer_elqB3coM,92
55
- auto_editor-24.7.1.dist-info/entry_points.txt,sha256=-H7zdTw4MqnAcwrN5xTNkGIhzZtJMxS9r6lTMeR9-aA,240
56
- auto_editor-24.7.1.dist-info/top_level.txt,sha256=xwV1JV1ZeRmlH9VeBRZXgXtWHpWSD4w1mY5II56D3ns,22
57
- auto_editor-24.7.1.dist-info/RECORD,,
52
+ auto_editor-24.9.1.dist-info/LICENSE,sha256=yiq99pWITHfqS0pbZMp7cy2dnbreTuvBwudsU-njvIM,1210
53
+ auto_editor-24.9.1.dist-info/METADATA,sha256=KeE8QXLu3b7IVUd5-3c6A_rax_5LXoqzuSN96wKp4gk,7092
54
+ auto_editor-24.9.1.dist-info/WHEEL,sha256=oiQVh_5PnQM0E3gPdiz09WCNmwiHDMaGer_elqB3coM,92
55
+ auto_editor-24.9.1.dist-info/entry_points.txt,sha256=-H7zdTw4MqnAcwrN5xTNkGIhzZtJMxS9r6lTMeR9-aA,240
56
+ auto_editor-24.9.1.dist-info/top_level.txt,sha256=xwV1JV1ZeRmlH9VeBRZXgXtWHpWSD4w1mY5II56D3ns,22
57
+ auto_editor-24.9.1.dist-info/RECORD,,