auto-editor 24.7.1__py3-none-any.whl → 24.13.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.
- ae-ffmpeg/ae_ffmpeg/__init__.py +1 -1
- auto_editor/__init__.py +2 -2
- auto_editor/analyze.py +0 -82
- auto_editor/edit.py +2 -2
- auto_editor/ffwrapper.py +23 -47
- auto_editor/formats/fcp11.py +6 -6
- auto_editor/formats/fcp7.py +11 -11
- auto_editor/formats/json.py +8 -8
- auto_editor/help.py +5 -6
- auto_editor/lang/palet.py +143 -60
- auto_editor/lib/contracts.py +57 -10
- auto_editor/lib/data_structs.py +1 -0
- auto_editor/output.py +14 -7
- auto_editor/render/video.py +24 -23
- auto_editor/subcommands/desc.py +2 -4
- auto_editor/subcommands/info.py +24 -19
- auto_editor/subcommands/levels.py +1 -1
- auto_editor/subcommands/repl.py +1 -1
- auto_editor/subcommands/subdump.py +14 -49
- auto_editor/subcommands/test.py +11 -7
- auto_editor/utils/cmdkw.py +41 -0
- auto_editor/wavfile.py +3 -3
- {auto_editor-24.7.1.dist-info → auto_editor-24.13.1.dist-info}/METADATA +3 -3
- {auto_editor-24.7.1.dist-info → auto_editor-24.13.1.dist-info}/RECORD +28 -28
- {auto_editor-24.7.1.dist-info → auto_editor-24.13.1.dist-info}/WHEEL +1 -1
- {auto_editor-24.7.1.dist-info → auto_editor-24.13.1.dist-info}/LICENSE +0 -0
- {auto_editor-24.7.1.dist-info → auto_editor-24.13.1.dist-info}/entry_points.txt +0 -0
- {auto_editor-24.7.1.dist-info → auto_editor-24.13.1.dist-info}/top_level.txt +0 -0
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
|
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
|
-
|
338
|
-
|
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
|
-
|
353
|
+
from auto_editor.utils.cmdkw import parse_method
|
348
354
|
|
349
|
-
|
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) ->
|
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
|
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,
|
824
|
-
if
|
825
|
-
raise MyError(
|
826
|
-
env[
|
827
|
-
|
828
|
-
|
829
|
-
|
830
|
-
|
831
|
-
|
832
|
-
|
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
|
-
|
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] =
|
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
|
-
|
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),
|
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
@@ -182,18 +182,25 @@ def mux_quality_media(
|
|
182
182
|
cmd += _ffset("-c:a", args.audio_codec) + _ffset("-b:a", args.audio_bitrate)
|
183
183
|
|
184
184
|
if same_container and v_tracks > 0:
|
185
|
-
|
186
|
-
|
187
|
-
|
188
|
-
|
189
|
-
|
190
|
-
|
185
|
+
color_range = src.videos[0].color_range
|
186
|
+
colorspace = src.videos[0].color_space
|
187
|
+
color_prim = src.videos[0].color_primaries
|
188
|
+
color_trc = src.videos[0].color_transfer
|
189
|
+
|
190
|
+
if color_range == 1 or color_range == 2:
|
191
|
+
cmd.extend(["-color_range", f"{color_range}"])
|
192
|
+
if colorspace in (0, 1) or (colorspace >= 3 and colorspace < 16):
|
193
|
+
cmd.extend(["-colorspace", f"{colorspace}"])
|
194
|
+
if color_prim in (0, 1) or (color_prim >= 4 and color_prim < 17):
|
195
|
+
cmd.extend(["-color_primaries", f"{color_prim}"])
|
196
|
+
if color_trc == 1 or (color_trc >= 4 and color_trc < 22):
|
197
|
+
cmd.extend(["-color_trc", f"{color_trc}"])
|
191
198
|
|
192
199
|
if args.extras is not None:
|
193
200
|
cmd.extend(args.extras.split(" "))
|
194
201
|
cmd.extend(["-strict", "-2"]) # Allow experimental codecs.
|
195
202
|
|
196
|
-
if
|
203
|
+
if s_tracks > 0:
|
197
204
|
cmd.extend(["-map", "0:t?"]) # Add input attachments to output.
|
198
205
|
|
199
206
|
# This was causing a crash for 'example.mp4 multi-track.mov'
|
auto_editor/render/video.py
CHANGED
@@ -168,15 +168,26 @@ def render_av(
|
|
168
168
|
log.debug(f"Target pix_fmt: {target_pix_fmt}")
|
169
169
|
|
170
170
|
apply_video_later = True
|
171
|
-
|
172
|
-
if args.scale != 1:
|
173
|
-
apply_video_later = False
|
174
|
-
elif args.video_codec in encoders:
|
171
|
+
if args.video_codec in encoders:
|
175
172
|
apply_video_later = set(encoders[args.video_codec]).isdisjoint(allowed_pix_fmt)
|
176
173
|
|
177
174
|
log.debug(f"apply video quality settings now: {not apply_video_later}")
|
178
175
|
|
179
|
-
|
176
|
+
if args.scale == 1.0:
|
177
|
+
target_width, target_height = tl.res
|
178
|
+
scale_graph = None
|
179
|
+
else:
|
180
|
+
target_width = max(round(tl.res[0] * args.scale), 2)
|
181
|
+
target_height = max(round(tl.res[1] * args.scale), 2)
|
182
|
+
scale_graph = av.filter.Graph()
|
183
|
+
link_nodes(
|
184
|
+
scale_graph.add(
|
185
|
+
"buffer", video_size="1x1", time_base="1/1", pix_fmt=target_pix_fmt
|
186
|
+
),
|
187
|
+
scale_graph.add("scale", f"{target_width}:{target_height}"),
|
188
|
+
scale_graph.add("buffersink"),
|
189
|
+
)
|
190
|
+
|
180
191
|
spedup = os.path.join(temp, "spedup0.mp4")
|
181
192
|
|
182
193
|
cmd = [
|
@@ -189,7 +200,7 @@ def render_av(
|
|
189
200
|
"-pix_fmt",
|
190
201
|
target_pix_fmt,
|
191
202
|
"-s",
|
192
|
-
f"{
|
203
|
+
f"{target_width}*{target_height}",
|
193
204
|
"-framerate",
|
194
205
|
f"{tl.tb}",
|
195
206
|
"-i",
|
@@ -224,7 +235,7 @@ def render_av(
|
|
224
235
|
bar.start(tl.end, "Creating new video")
|
225
236
|
|
226
237
|
bg = color(args.background)
|
227
|
-
null_frame = make_solid(
|
238
|
+
null_frame = make_solid(target_width, target_height, target_pix_fmt, bg)
|
228
239
|
frame_index = -1
|
229
240
|
try:
|
230
241
|
for index in range(tl.end):
|
@@ -282,7 +293,8 @@ def render_av(
|
|
282
293
|
if frame.key_frame:
|
283
294
|
log.debug(f"Keyframe {frame_index} {frame.pts}")
|
284
295
|
|
285
|
-
if frame.width
|
296
|
+
if (frame.width, frame.height) != tl.res:
|
297
|
+
width, height = tl.res
|
286
298
|
graph = av.filter.Graph()
|
287
299
|
link_nodes(
|
288
300
|
graph.add_buffer(template=my_stream),
|
@@ -345,6 +357,10 @@ def render_av(
|
|
345
357
|
|
346
358
|
frame = av.VideoFrame.from_ndarray(array, format="rgb24")
|
347
359
|
|
360
|
+
if scale_graph is not None and frame.width != target_width:
|
361
|
+
scale_graph.push(frame)
|
362
|
+
frame = scale_graph.pull()
|
363
|
+
|
348
364
|
if frame.format.name != target_pix_fmt:
|
349
365
|
frame = frame.reformat(format=target_pix_fmt)
|
350
366
|
bar.tick(index)
|
@@ -363,19 +379,4 @@ def render_av(
|
|
363
379
|
|
364
380
|
log.debug(f"Total frames saved seeking: {frames_saved}")
|
365
381
|
|
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
382
|
return spedup, apply_video_later
|
auto_editor/subcommands/desc.py
CHANGED
@@ -3,28 +3,26 @@ from __future__ import annotations
|
|
3
3
|
import sys
|
4
4
|
from dataclasses import dataclass, field
|
5
5
|
|
6
|
-
from auto_editor.ffwrapper import
|
6
|
+
from auto_editor.ffwrapper import initFileInfo
|
7
7
|
from auto_editor.utils.log import Log
|
8
8
|
from auto_editor.vanparse import ArgumentParser
|
9
9
|
|
10
10
|
|
11
11
|
@dataclass(slots=True)
|
12
12
|
class DescArgs:
|
13
|
-
ffmpeg_location: str | None = None
|
14
13
|
help: bool = False
|
15
14
|
input: list[str] = field(default_factory=list)
|
16
15
|
|
17
16
|
|
18
17
|
def desc_options(parser: ArgumentParser) -> ArgumentParser:
|
19
18
|
parser.add_required("input", nargs="*")
|
20
|
-
parser.add_argument("--ffmpeg-location", help="Point to your custom ffmpeg file")
|
21
19
|
return parser
|
22
20
|
|
23
21
|
|
24
22
|
def main(sys_args: list[str] = sys.argv[1:]) -> None:
|
25
23
|
args = desc_options(ArgumentParser("desc")).parse_args(DescArgs, sys_args)
|
26
24
|
for path in args.input:
|
27
|
-
src = initFileInfo(path,
|
25
|
+
src = initFileInfo(path, Log())
|
28
26
|
if src.description is not None:
|
29
27
|
sys.stdout.write(f"\n{src.description}\n\n")
|
30
28
|
else:
|