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/__init__.py +2 -2
- auto_editor/__main__.py +5 -0
- auto_editor/analyze.py +0 -82
- auto_editor/edit.py +3 -3
- auto_editor/ffwrapper.py +8 -5
- auto_editor/formats/fcp11.py +1 -4
- auto_editor/formats/fcp7.py +71 -132
- auto_editor/help.py +5 -6
- auto_editor/lang/libmath.py +23 -0
- auto_editor/lang/palet.py +165 -70
- auto_editor/lib/contracts.py +57 -10
- auto_editor/lib/data_structs.py +1 -0
- auto_editor/output.py +4 -2
- auto_editor/render/video.py +30 -24
- auto_editor/subcommands/test.py +5 -7
- auto_editor/utils/cmdkw.py +41 -0
- auto_editor/utils/types.py +1 -0
- {auto_editor-24.3.1.dist-info → auto_editor-24.9.1.dist-info}/METADATA +1 -1
- {auto_editor-24.3.1.dist-info → auto_editor-24.9.1.dist-info}/RECORD +23 -22
- {auto_editor-24.3.1.dist-info → auto_editor-24.9.1.dist-info}/LICENSE +0 -0
- {auto_editor-24.3.1.dist-info → auto_editor-24.9.1.dist-info}/WHEEL +0 -0
- {auto_editor-24.3.1.dist-info → auto_editor-24.9.1.dist-info}/entry_points.txt +0 -0
- {auto_editor-24.3.1.dist-info → auto_editor-24.9.1.dist-info}/top_level.txt +0 -0
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
|
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
|
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
|
-
|
339
|
-
|
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
|
-
|
353
|
+
from auto_editor.utils.cmdkw import parse_method
|
349
354
|
|
350
|
-
|
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) ->
|
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 =
|
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
|
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,
|
825
|
-
if
|
826
|
-
raise MyError(
|
827
|
-
env[
|
828
|
-
|
829
|
-
|
830
|
-
|
831
|
-
|
832
|
-
|
833
|
-
|
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
|
-
|
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] =
|
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
|
-
|
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
|
auto_editor/lib/contracts.py
CHANGED
@@ -47,7 +47,7 @@ def check_contract(c: object, val: object) -> bool:
|
|
47
47
|
|
48
48
|
|
49
49
|
def check_args(
|
50
|
-
|
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"`{
|
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"`{
|
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
|
-
|
90
|
-
|
91
|
-
|
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
|
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
|
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
|
|
auto_editor/lib/data_structs.py
CHANGED
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
|
-
|
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?"])
|
auto_editor/render/video.py
CHANGED
@@ -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
|
-
|
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"{
|
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(
|
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
|
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
|
auto_editor/subcommands/test.py
CHANGED
@@ -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
|
|