auto-editor 24.3.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/lang/palet.py CHANGED
@@ -3,11 +3,9 @@ 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
- import cmath
10
- import math
8
+ from cmath import sqrt as complex_sqrt
11
9
  from dataclasses import dataclass
12
10
  from difflib import get_close_matches
13
11
  from fractions import Fraction
@@ -20,7 +18,12 @@ from typing import TYPE_CHECKING
20
18
  import numpy as np
21
19
  from numpy import logical_and, logical_not, logical_or, logical_xor
22
20
 
23
- 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
+ )
24
27
  from auto_editor.lib.contracts import *
25
28
  from auto_editor.lib.data_structs import *
26
29
  from auto_editor.lib.err import MyError
@@ -49,9 +52,8 @@ class ClosingError(MyError):
49
52
  ###############################################################################
50
53
 
51
54
  LPAREN, RPAREN, LBRAC, RBRAC, LCUR, RCUR, EOF = "(", ")", "[", "]", "{", "}", "EOF"
52
- 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"
53
56
  SEC_UNITS = ("s", "sec", "secs", "second", "seconds")
54
- METHODS = ("audio:", "motion:", "subtitle:")
55
57
  brac_pairs = {LPAREN: RPAREN, LBRAC: RBRAC, LCUR: RCUR}
56
58
 
57
59
  str_escape = {
@@ -317,7 +319,6 @@ class Lexer:
317
319
 
318
320
  result = ""
319
321
  has_illegal = False
320
- is_method = False
321
322
 
322
323
  def normal() -> bool:
323
324
  return (
@@ -334,22 +335,24 @@ class Lexer:
334
335
  else:
335
336
  return self.char_is_norm()
336
337
 
338
+ is_method = False
337
339
  while normal():
338
- result += self.char
339
- if (result + ":") in METHODS:
340
+ if self.char == ":":
341
+ name = result
342
+ result = ""
340
343
  is_method = True
341
344
  normal = handle_strings
345
+ else:
346
+ result += self.char
342
347
 
343
348
  if self.char in "'`|\\":
344
349
  has_illegal = True
345
350
  self.advance()
346
351
 
347
352
  if is_method:
348
- return Token(VAL, Method(result))
353
+ from auto_editor.utils.cmdkw import parse_method
349
354
 
350
- for method in METHODS:
351
- if result == method[:-1]:
352
- return Token(VAL, Method(result))
355
+ return Token(M, parse_method(name, result, env))
353
356
 
354
357
  if self.char == ".": # handle `object.method` syntax
355
358
  self.advance()
@@ -370,16 +373,6 @@ class Lexer:
370
373
  ###############################################################################
371
374
 
372
375
 
373
- @dataclass(slots=True)
374
- class Method:
375
- val: str
376
-
377
- def __str__(self) -> str:
378
- return f"#<method:{self.val}>"
379
-
380
- __repr__ = __str__
381
-
382
-
383
376
  class Parser:
384
377
  def __init__(self, lexer: Lexer):
385
378
  self.lexer = lexer
@@ -414,6 +407,16 @@ class Parser:
414
407
  self.eat()
415
408
  return (Sym("pow"), 10, (Sym("/"), token.value, 20))
416
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
+
417
420
  if token.type == DOT:
418
421
  self.eat()
419
422
  if type(token.value[1].value) is not Sym:
@@ -507,7 +510,7 @@ def initOutPort(name: str) -> OutputPort | Literal[False]:
507
510
  return OutputPort(name, port, port.write, False)
508
511
 
509
512
 
510
- def raise_(msg: str) -> None:
513
+ def raise_(msg: str | Exception) -> NoReturn:
511
514
  raise MyError(msg)
512
515
 
513
516
 
@@ -549,7 +552,7 @@ def int_div(n: int, *m: int) -> int:
549
552
 
550
553
 
551
554
  def _sqrt(v: Number) -> Number:
552
- r = cmath.sqrt(v)
555
+ r = complex_sqrt(v)
553
556
  if r.imag == 0:
554
557
  if int(r.real) == r.real:
555
558
  return int(r.real)
@@ -809,7 +812,7 @@ class UserProc(Proc):
809
812
 
810
813
 
811
814
  @dataclass(slots=True)
812
- class KeywordProc:
815
+ class KeywordUserProc:
813
816
  env: Env
814
817
  name: str
815
818
  parms: list[str]
@@ -818,38 +821,21 @@ class KeywordProc:
818
821
  arity: tuple[int, None]
819
822
  contracts: list[Any] | None = None
820
823
 
821
- def __call__(self, *args: Any) -> Any:
824
+ def __call__(self, *args: Any, **kwargs: Any) -> Any:
822
825
  env = {}
823
-
824
- for i, parm in enumerate(self.parms):
825
- if type(args[i]) is Keyword:
826
- raise MyError(f"Invalid keyword `{args[i]}`")
827
- env[parm] = args[i]
828
-
829
- remain_args = args[len(self.parms) :]
830
-
831
- allow_pos = True
832
- pos_index = 0
833
- key = ""
834
- for arg in remain_args:
835
- if type(arg) is Keyword:
836
- if key:
837
- raise MyError("Expected value for keyword but got another keyword")
838
- key = arg.val
839
- allow_pos = False
840
- elif key:
841
- env[key] = arg
842
- 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
+ )
843
837
  else:
844
- if not allow_pos:
845
- raise MyError("Positional argument not allowed here")
846
- if pos_index >= len(self.kw_parms):
847
- base = f"`{self.name}` has an arity mismatch. Expected"
848
- upper = len(self.parms) + len(self.kw_parms)
849
- raise MyError(f"{base} at most {upper}")
850
-
851
- env[self.kw_parms[pos_index]] = arg
852
- pos_index += 1
838
+ env[key] = val
853
839
 
854
840
  inner_env = Env(env, self.env)
855
841
 
@@ -954,7 +940,7 @@ def syn_define(env: Env, node: Node) -> None:
954
940
  raise MyError(f"{node[0]}: must be an identifier")
955
941
 
956
942
  if kw_only:
957
- env[n] = KeywordProc(env, n, parms, kparms, body, (len(parms), None))
943
+ env[n] = KeywordUserProc(env, n, parms, kparms, body, (len(parms), None))
958
944
  else:
959
945
  env[n] = UserProc(env, n, parms, (), body)
960
946
  return None
@@ -1396,6 +1382,25 @@ def syn_let_star(env: Env, node: Node) -> Any:
1396
1382
  return my_eval(inner_env, node[-1])
1397
1383
 
1398
1384
 
1385
+ def syn_import(env: Env, node: Node) -> None:
1386
+ guard_term(node, 2, 2)
1387
+
1388
+ if type(node[1]) is not Sym:
1389
+ raise MyError("class name must be an identifier")
1390
+
1391
+ module = node[1].val
1392
+ error = MyError(f"No module named `{module}`")
1393
+
1394
+ if module != "math":
1395
+ raise error
1396
+ try:
1397
+ obj = __import__("auto_editor.lang.libmath", fromlist=["lang"])
1398
+ except ImportError:
1399
+ raise error
1400
+
1401
+ env.update(obj.all())
1402
+
1403
+
1399
1404
  def syn_class(env: Env, node: Node) -> None:
1400
1405
  if len(node) < 2:
1401
1406
  raise MyError(f"{node[0]}: Expects at least 1 term")
@@ -1464,6 +1469,81 @@ def edit_all() -> np.ndarray:
1464
1469
  return env["@levels"].all()
1465
1470
 
1466
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
+
1467
1547
  def my_eval(env: Env, node: object) -> Any:
1468
1548
  if type(node) is Sym:
1469
1549
  val = env.get(node.val)
@@ -1477,11 +1557,6 @@ def my_eval(env: Env, node: object) -> Any:
1477
1557
  )
1478
1558
  return val
1479
1559
 
1480
- if isinstance(node, Method):
1481
- if "@filesetup" not in env:
1482
- raise MyError("Can't use edit methods if there's no input files")
1483
- return edit_method(node.val, env["@filesetup"], env)
1484
-
1485
1560
  if type(node) is list:
1486
1561
  return [my_eval(env, item) for item in node]
1487
1562
 
@@ -1513,7 +1588,21 @@ def my_eval(env: Env, node: object) -> Any:
1513
1588
  if type(oper) is Syntax:
1514
1589
  return oper(env, node)
1515
1590
 
1516
- 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)
1517
1606
 
1518
1607
  return node
1519
1608
 
@@ -1528,6 +1617,18 @@ env.update({
1528
1617
  # edit procedures
1529
1618
  "none": Proc("none", edit_none, (0, 0)),
1530
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
+ ),
1531
1632
  # syntax
1532
1633
  "lambda": Syntax(syn_lambda),
1533
1634
  "λ": Syntax(syn_lambda),
@@ -1544,6 +1645,7 @@ env.update({
1544
1645
  "case": Syntax(syn_case),
1545
1646
  "let": Syntax(syn_let),
1546
1647
  "let*": Syntax(syn_let_star),
1648
+ "import": Syntax(syn_import),
1547
1649
  "class": Syntax(syn_class),
1548
1650
  "@r": Syntax(attr),
1549
1651
  # loops
@@ -1615,17 +1717,10 @@ env.update({
1615
1717
  "imag-part": Proc("imag-part", lambda v: v.imag, (1, 1), is_num),
1616
1718
  # reals
1617
1719
  "pow": Proc("pow", pow, (2, 2), is_real),
1618
- "exp": Proc("exp", math.exp, (1, 1), is_real),
1619
1720
  "abs": Proc("abs", abs, (1, 1), is_real),
1620
- "ceil": Proc("ceil", math.ceil, (1, 1), is_real),
1621
- "floor": Proc("floor", math.floor, (1, 1), is_real),
1622
1721
  "round": Proc("round", round, (1, 1), is_real),
1623
1722
  "max": Proc("max", lambda *v: max(v), (1, None), is_real),
1624
1723
  "min": Proc("min", lambda *v: min(v), (1, None), is_real),
1625
- "sin": Proc("sin", math.sin, (1, 1), is_real),
1626
- "cos": Proc("cos", math.cos, (1, 1), is_real),
1627
- "log": Proc("log", math.log, (1, 2), andc(is_real, gt_c(0))),
1628
- "tan": Proc("tan", math.tan, (1, 1), is_real),
1629
1724
  "mod": Proc("mod", mod, (2, 2), is_int),
1630
1725
  "modulo": Proc("modulo", mod, (2, 2), is_int),
1631
1726
  # symbols
@@ -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
@@ -88,7 +88,7 @@ def mux_quality_media(
88
88
  ) -> None:
89
89
  v_tracks = len(visual_output)
90
90
  a_tracks = len(audio_output)
91
- s_tracks = len(sub_output)
91
+ s_tracks = 0 if args.sn else len(sub_output)
92
92
 
93
93
  cmd = ["-hide_banner", "-y", "-i", f"{src.path}"]
94
94
 
@@ -192,7 +192,9 @@ def mux_quality_media(
192
192
  if args.extras is not None:
193
193
  cmd.extend(args.extras.split(" "))
194
194
  cmd.extend(["-strict", "-2"]) # Allow experimental codecs.
195
- cmd.extend(["-map", "0:t?"]) # Add input attachments to output.
195
+
196
+ if s_tracks > 0:
197
+ cmd.extend(["-map", "0:t?"]) # Add input attachments to output.
196
198
 
197
199
  # This was causing a crash for 'example.mp4 multi-track.mov'
198
200
  # cmd.extend(["-map", "0:d?"])
@@ -3,6 +3,7 @@ from __future__ import annotations
3
3
  import os.path
4
4
  from dataclasses import dataclass
5
5
  from subprocess import DEVNULL, PIPE
6
+ from sys import platform
6
7
  from typing import TYPE_CHECKING
7
8
 
8
9
  import av
@@ -96,6 +97,7 @@ def make_image_cache(tl: v3) -> dict[tuple[FileInfo, int], np.ndarray]:
96
97
  for obj in clip:
97
98
  if isinstance(obj, TlImage) and obj.src not in img_cache:
98
99
  with av.open(obj.src.path) as cn:
100
+ assert isinstance(cn, av.InputContainer)
99
101
  my_stream = cn.streams.video[0]
100
102
  for frame in cn.decode(my_stream):
101
103
  if obj.width != 0:
@@ -111,7 +113,6 @@ def make_image_cache(tl: v3) -> dict[tuple[FileInfo, int], np.ndarray]:
111
113
  format="rgb24"
112
114
  )
113
115
  break
114
-
115
116
  return img_cache
116
117
 
117
118
 
@@ -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",
@@ -198,6 +210,10 @@ def render_av(
198
210
  target_pix_fmt,
199
211
  ]
200
212
 
213
+ if platform == "darwin":
214
+ # Fix videotoolbox issue with legacy macs
215
+ cmd += ["-allow_sw", "1"]
216
+
201
217
  if apply_video_later:
202
218
  cmd += ["-c:v", "mpeg4", "-qscale:v", "1"]
203
219
  else:
@@ -220,7 +236,7 @@ def render_av(
220
236
  bar.start(tl.end, "Creating new video")
221
237
 
222
238
  bg = color(args.background)
223
- null_frame = make_solid(width, height, target_pix_fmt, bg)
239
+ null_frame = make_solid(target_width, target_height, target_pix_fmt, bg)
224
240
  frame_index = -1
225
241
  try:
226
242
  for index in range(tl.end):
@@ -278,7 +294,8 @@ def render_av(
278
294
  if frame.key_frame:
279
295
  log.debug(f"Keyframe {frame_index} {frame.pts}")
280
296
 
281
- if frame.width != width or frame.height != height:
297
+ if (frame.width, frame.height) != tl.res:
298
+ width, height = tl.res
282
299
  graph = av.filter.Graph()
283
300
  link_nodes(
284
301
  graph.add_buffer(template=my_stream),
@@ -341,6 +358,10 @@ def render_av(
341
358
 
342
359
  frame = av.VideoFrame.from_ndarray(array, format="rgb24")
343
360
 
361
+ if scale_graph is not None and frame.width != target_width:
362
+ scale_graph.push(frame)
363
+ frame = scale_graph.pull()
364
+
344
365
  if frame.format.name != target_pix_fmt:
345
366
  frame = frame.reformat(format=target_pix_fmt)
346
367
  bar.tick(index)
@@ -359,19 +380,4 @@ def render_av(
359
380
 
360
381
  log.debug(f"Total frames saved seeking: {frames_saved}")
361
382
 
362
- if args.scale != 1:
363
- sped_input = os.path.join(temp, "spedup0.mp4")
364
- spedup = os.path.join(temp, "scale0.mp4")
365
- scale_filter = f"scale=iw*{args.scale}:ih*{args.scale}"
366
-
367
- cmd = ["-i", sped_input, "-vf", scale_filter, spedup]
368
-
369
- check_errors = ffmpeg.pipe(cmd)
370
- if "Error" in check_errors or "failed" in check_errors:
371
- if "-allow_sw 1" in check_errors:
372
- cmd.insert(-1, "-allow_sw")
373
- cmd.insert(-1, "1")
374
- # Run again to show errors even if it might not work.
375
- ffmpeg.run(cmd)
376
-
377
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}")
@@ -577,12 +580,6 @@ def main(sys_args: list[str] | None = None):
577
580
  ("(pow 4 0.5)", 2.0),
578
581
  ("(abs 1.0)", 1.0),
579
582
  ("(abs -1)", 1),
580
- ("(round 3.5)", 4),
581
- ("(round 2.5)", 2),
582
- ("(ceil 2.1)", 3),
583
- ("(ceil 2.9)", 3),
584
- ("(floor 2.1)", 2),
585
- ("(floor 2.9)", 2),
586
583
  ("(bool? #t)", True),
587
584
  ("(bool? #f)", True),
588
585
  ("(bool? 0)", False),
@@ -690,9 +687,10 @@ def main(sys_args: list[str] | None = None):
690
687
  )
691
688
 
692
689
  def palet_scripts():
693
- run.raw(["palet", "resources/scripts/maxcut.pal"])
694
690
  run.raw(["palet", "resources/scripts/scope.pal"])
691
+ run.raw(["palet", "resources/scripts/maxcut.pal"])
695
692
  run.raw(["palet", "resources/scripts/case.pal"])
693
+ run.raw(["palet", "resources/scripts/testmath.pal"])
696
694
 
697
695
  tests = []
698
696