auto-editor 24.7.1__tar.gz → 24.9.1__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {auto-editor-24.7.1/auto_editor.egg-info → auto-editor-24.9.1}/PKG-INFO +1 -1
- auto-editor-24.9.1/auto_editor/__init__.py +2 -0
- {auto-editor-24.7.1 → auto-editor-24.9.1}/auto_editor/analyze.py +0 -82
- {auto-editor-24.7.1 → auto-editor-24.9.1}/auto_editor/ffwrapper.py +6 -3
- {auto-editor-24.7.1 → auto-editor-24.9.1}/auto_editor/formats/fcp7.py +10 -9
- {auto-editor-24.7.1 → auto-editor-24.9.1}/auto_editor/help.py +5 -6
- {auto-editor-24.7.1 → auto-editor-24.9.1}/auto_editor/lang/palet.py +143 -60
- {auto-editor-24.7.1 → auto-editor-24.9.1}/auto_editor/lib/contracts.py +57 -10
- {auto-editor-24.7.1 → auto-editor-24.9.1}/auto_editor/lib/data_structs.py +1 -0
- {auto-editor-24.7.1 → auto-editor-24.9.1}/auto_editor/output.py +1 -1
- {auto-editor-24.7.1 → auto-editor-24.9.1}/auto_editor/render/video.py +25 -23
- {auto-editor-24.7.1 → auto-editor-24.9.1}/auto_editor/subcommands/test.py +4 -1
- {auto-editor-24.7.1 → auto-editor-24.9.1}/auto_editor/utils/cmdkw.py +41 -0
- {auto-editor-24.7.1 → auto-editor-24.9.1/auto_editor.egg-info}/PKG-INFO +1 -1
- auto-editor-24.7.1/auto_editor/__init__.py +0 -2
- {auto-editor-24.7.1 → auto-editor-24.9.1}/LICENSE +0 -0
- {auto-editor-24.7.1 → auto-editor-24.9.1}/README.md +0 -0
- {auto-editor-24.7.1 → auto-editor-24.9.1}/ae-ffmpeg/ae_ffmpeg/__init__.py +0 -0
- {auto-editor-24.7.1 → auto-editor-24.9.1}/ae-ffmpeg/ae_ffmpeg/py.typed +0 -0
- {auto-editor-24.7.1 → auto-editor-24.9.1}/ae-ffmpeg/setup.py +0 -0
- {auto-editor-24.7.1 → auto-editor-24.9.1}/auto_editor/__main__.py +0 -0
- {auto-editor-24.7.1 → auto-editor-24.9.1}/auto_editor/edit.py +0 -0
- {auto-editor-24.7.1 → auto-editor-24.9.1}/auto_editor/formats/__init__.py +0 -0
- {auto-editor-24.7.1 → auto-editor-24.9.1}/auto_editor/formats/fcp11.py +0 -0
- {auto-editor-24.7.1 → auto-editor-24.9.1}/auto_editor/formats/json.py +0 -0
- {auto-editor-24.7.1 → auto-editor-24.9.1}/auto_editor/formats/shotcut.py +0 -0
- {auto-editor-24.7.1 → auto-editor-24.9.1}/auto_editor/formats/utils.py +0 -0
- {auto-editor-24.7.1 → auto-editor-24.9.1}/auto_editor/lang/__init__.py +0 -0
- {auto-editor-24.7.1 → auto-editor-24.9.1}/auto_editor/lang/json.py +0 -0
- {auto-editor-24.7.1 → auto-editor-24.9.1}/auto_editor/lang/libmath.py +0 -0
- {auto-editor-24.7.1 → auto-editor-24.9.1}/auto_editor/lib/__init__.py +0 -0
- {auto-editor-24.7.1 → auto-editor-24.9.1}/auto_editor/lib/err.py +0 -0
- {auto-editor-24.7.1 → auto-editor-24.9.1}/auto_editor/make_layers.py +0 -0
- {auto-editor-24.7.1 → auto-editor-24.9.1}/auto_editor/preview.py +0 -0
- {auto-editor-24.7.1 → auto-editor-24.9.1}/auto_editor/render/__init__.py +0 -0
- {auto-editor-24.7.1 → auto-editor-24.9.1}/auto_editor/render/audio.py +0 -0
- {auto-editor-24.7.1 → auto-editor-24.9.1}/auto_editor/render/subtitle.py +0 -0
- {auto-editor-24.7.1 → auto-editor-24.9.1}/auto_editor/subcommands/__init__.py +0 -0
- {auto-editor-24.7.1 → auto-editor-24.9.1}/auto_editor/subcommands/desc.py +0 -0
- {auto-editor-24.7.1 → auto-editor-24.9.1}/auto_editor/subcommands/info.py +0 -0
- {auto-editor-24.7.1 → auto-editor-24.9.1}/auto_editor/subcommands/levels.py +0 -0
- {auto-editor-24.7.1 → auto-editor-24.9.1}/auto_editor/subcommands/palet.py +0 -0
- {auto-editor-24.7.1 → auto-editor-24.9.1}/auto_editor/subcommands/repl.py +0 -0
- {auto-editor-24.7.1 → auto-editor-24.9.1}/auto_editor/subcommands/subdump.py +0 -0
- {auto-editor-24.7.1 → auto-editor-24.9.1}/auto_editor/timeline.py +0 -0
- {auto-editor-24.7.1 → auto-editor-24.9.1}/auto_editor/utils/__init__.py +0 -0
- {auto-editor-24.7.1 → auto-editor-24.9.1}/auto_editor/utils/bar.py +0 -0
- {auto-editor-24.7.1 → auto-editor-24.9.1}/auto_editor/utils/chunks.py +0 -0
- {auto-editor-24.7.1 → auto-editor-24.9.1}/auto_editor/utils/container.py +0 -0
- {auto-editor-24.7.1 → auto-editor-24.9.1}/auto_editor/utils/encoder.py +0 -0
- {auto-editor-24.7.1 → auto-editor-24.9.1}/auto_editor/utils/func.py +0 -0
- {auto-editor-24.7.1 → auto-editor-24.9.1}/auto_editor/utils/log.py +0 -0
- {auto-editor-24.7.1 → auto-editor-24.9.1}/auto_editor/utils/types.py +0 -0
- {auto-editor-24.7.1 → auto-editor-24.9.1}/auto_editor/validate_input.py +0 -0
- {auto-editor-24.7.1 → auto-editor-24.9.1}/auto_editor/vanparse.py +0 -0
- {auto-editor-24.7.1 → auto-editor-24.9.1}/auto_editor/wavfile.py +0 -0
- {auto-editor-24.7.1 → auto-editor-24.9.1}/auto_editor.egg-info/SOURCES.txt +0 -0
- {auto-editor-24.7.1 → auto-editor-24.9.1}/auto_editor.egg-info/dependency_links.txt +0 -0
- {auto-editor-24.7.1 → auto-editor-24.9.1}/auto_editor.egg-info/entry_points.txt +0 -0
- {auto-editor-24.7.1 → auto-editor-24.9.1}/auto_editor.egg-info/requires.txt +0 -0
- {auto-editor-24.7.1 → auto-editor-24.9.1}/auto_editor.egg-info/top_level.txt +0 -0
- {auto-editor-24.7.1 → auto-editor-24.9.1}/pyproject.toml +0 -0
- {auto-editor-24.7.1 → auto-editor-24.9.1}/setup.cfg +0 -0
@@ -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")
|
@@ -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
|
-
|
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
|
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="
|
516
|
+
ET.indent(tree, space=" ", level=0)
|
516
517
|
tree.write(output, xml_declaration=True, encoding="utf-8")
|
@@ -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
|
-
|
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
|
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
|
@@ -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),
|
@@ -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
|
|
@@ -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
|
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
|
-
|
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",
|
@@ -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(
|
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
|
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
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|