ffmpeg-skill 1.10.0 → 1.11.0

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.
package/scripts/render.py CHANGED
@@ -49,6 +49,7 @@ Examples:
49
49
  python3 render.py project.json --fast # preview quality
50
50
  """
51
51
  import argparse
52
+ import difflib
52
53
  import re
53
54
  import json
54
55
  import os
@@ -79,6 +80,58 @@ TEMPLATE = {
79
80
  }
80
81
 
81
82
 
83
+ # Every key render.py reads, per object. Anything else is a refusal rather than a silent no-op:
84
+ # a clip "start"/"end" (the spelling titles, graphics and overlays use) rendered the whole clip
85
+ # untrimmed, and a mistyped stage name dropped the stage -- both reported as a success (review 9).
86
+ OBJECT_KEYS: Dict[str, frozenset] = {
87
+ "project": frozenset({"output", "frame", "clips", "transition", "silence", "brand", "captions",
88
+ "graphics", "overlays", "audio", "loudness", "fit", "export", "check"}),
89
+ "clips[]": frozenset({"src", "in", "out", "speed"}),
90
+ "frame": frozenset({"aspect", "width", "height", "fps"}),
91
+ "transition": frozenset({"type", "duration"}),
92
+ "silence": frozenset({"threshold", "min_silence", "margin"}),
93
+ "captions": frozenset({"text", "srt", "ass", "font", "size", "color", "position", "margin",
94
+ "animate", "highlight_color", "outline", "karaoke", "bold", "box"}),
95
+ "graphics[]": frozenset({"template", "name", "title", "subtitle", "start", "end", "position",
96
+ "from", "scale", "primary", "text_color"}),
97
+ "overlays[]": frozenset({"logo", "image", "text", "position", "start", "end", "fade", "opacity",
98
+ "scale", "font_size", "font", "font_file", "margin", "box"}),
99
+ "audio": frozenset({"music", "replace", "music_volume", "fade_in", "fade_out", "music_fade_out",
100
+ "gain", "duck_amount", "voice", "denoise", "duck", "music_loop", "stereo",
101
+ "mono", "downmix"}),
102
+ "loudness": frozenset({"lufs", "tp"}),
103
+ "fit": frozenset({"duration", "method", "aspect", "fit", "width", "height", "fps", "smooth"}),
104
+ "export": frozenset({"preset", "fit", "crf", "normalize"}),
105
+ "check": frozenset({"platform"}),
106
+ }
107
+ # Typos difflib cannot see: a clip is trimmed with in/out, not the start/end that time a title.
108
+ NEAR_KEYS: Dict[str, Dict[str, str]] = {"clips[]": {"start": "in", "end": "out", "from": "in", "to": "out"}}
109
+
110
+
111
+ def check_keys(obj: Any, schema: str, label: str) -> None:
112
+ """Refuse an unrecognised key, naming the object, the key and the nearest valid one."""
113
+ if not isinstance(obj, dict):
114
+ return
115
+ valid = OBJECT_KEYS[schema]
116
+ for key in obj:
117
+ if key in valid:
118
+ continue
119
+ near = NEAR_KEYS.get(schema, {}).get(str(key)) or next(iter(difflib.get_close_matches(str(key), sorted(valid), n=1, cutoff=0.6)), None)
120
+ die(f"{label}: unknown key {key!r}" + (f" (did you mean {near!r}?)" if near
121
+ else f" (valid keys: {', '.join(sorted(valid))})"))
122
+
123
+
124
+ def validate_project(proj: Dict[str, Any]) -> None:
125
+ check_keys(proj, "project", "project")
126
+ for name in ("frame", "transition", "silence", "captions", "audio", "loudness", "fit", "export", "check"):
127
+ check_keys(proj.get(name), name, name)
128
+ for name in ("clips", "graphics", "overlays"):
129
+ items = proj.get(name)
130
+ if isinstance(items, list):
131
+ for i, item in enumerate(items):
132
+ check_keys(item, f"{name}[]", f"{name}[{i}]")
133
+
134
+
82
135
  def sh(script: str, *argv: Any, extra: List[str] = None) -> str:
83
136
  """Run a sibling script, forwarding --fast / --dry-run, returning its printed output path."""
84
137
  cmd = [str(HERE / script)] + [str(a) for a in argv] + (extra or []) + child_args() + ["--json"]
@@ -226,6 +279,7 @@ def main() -> int:
226
279
  die(f"{args.project}: not a project or plan object (top level is {type(proj).__name__})")
227
280
  if "plan_version" in proj:
228
281
  return execute_plan(proj, os.path.abspath(args.project))
282
+ validate_project(proj)
229
283
  base = Path(args.project).resolve().parent
230
284
 
231
285
  def rel(p: Any) -> str: