ffmpeg-skill 2.6.0 → 2.7.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.
@@ -54,6 +54,8 @@ ROLES = {
54
54
  # filter:<name> `ffmpeg -filters`
55
55
  # bsf:<name> `ffmpeg -bsfs`
56
56
  # external:whisper a local whisper engine (whisper.cpp / faster-whisper / openai-whisper)
57
+ # external:parakeet a local Parakeet engine (parakeet-mlx, or parakeet-cli with a .gguf model);
58
+ # English. Either external engine satisfies --transcribe.
57
59
  # "optional" entries name the flag or condition under which the capability is needed.
58
60
  FF = ["ffmpeg", "ffprobe"]
59
61
  X264 = "encoder:libx264"
@@ -145,7 +147,7 @@ TOOL_META: Dict[str, Dict[str, Any]] = {
145
147
  required=FF + [X264], optional=[],
146
148
  video_required=True, audio_only=False, visual=True, verify=["probe", "look"], produces_artifact=True, idempotency="content_equivalent", deterministic=True),
147
149
  "caption": dict(role="execution", inputs=["video asset", "SRT/ASS file or timed text (--text)"], outputs=["video artifact with burnt-in captions (--mode burn)", "video artifact with one or several language-tagged soft subtitle streams (--mode mux, --srt file:lang repeated)", "generated .srt / .ass sidecar"],
148
- required=FF + [X264, AAC, "filter:subtitles"], optional=[{"capability": "filter:ass", "when": "--animate / --karaoke"}, HDR_X265, {"capability": "external:whisper", "when": "--transcribe"},
150
+ required=FF + [X264, AAC, "filter:subtitles"], optional=[{"capability": "filter:ass", "when": "--animate / --karaoke"}, HDR_X265, {"capability": "external:whisper", "when": "--transcribe"}, {"capability": "external:parakeet", "when": "--transcribe (English; either engine family satisfies it)"},
149
151
  {"capability": "encoder:mov_text", "when": "--mode mux with a .mp4/.m4v/.mov output"}, {"capability": "encoder:webvtt", "when": "--mode mux with a .webm output"}, {"capability": "encoder:srt", "when": "--mode mux with a .mkv output"}],
150
152
  video_required=True, audio_only=False, visual=True, verify=["probe", "look"], produces_artifact=True, idempotency="content_equivalent", deterministic=True),
151
153
  "overlay": dict(role="execution", inputs=["video asset", "image (--image / --logo), text (--text), or a second video (--video) to composite"], outputs=["video artifact with the overlay composited"],
@@ -172,7 +174,8 @@ TOOL_META: Dict[str, Dict[str, Any]] = {
172
174
  video_required=False, audio_only=True, visual=False, verify=["probe", "check"], produces_artifact=True, idempotency="content_equivalent", deterministic=True),
173
175
  "silence": dict(role="analysis_and_execution", inputs=["video or audio asset"], outputs=["silence list JSON (--list)", "artifact with silences removed", "EDL text (--edl)"],
174
176
  required=FF + ["filter:silencedetect"], optional=[{"capability": X264, "when": "removing silences from a video"}, HDR_X265, {"capability": AAC, "when": "removing silences from a video"},
175
- {"capability": "external:whisper", "when": "--filler --transcribe"}] + AUDIO_OUT,
177
+ {"capability": "external:whisper", "when": "--filler --transcribe"},
178
+ {"capability": "external:parakeet", "when": "--filler --transcribe (English; either engine family satisfies it)"}] + AUDIO_OUT,
176
179
  video_required=False, audio_only=True, visual=False, verify=["probe"], produces_artifact=True, idempotency="content_equivalent", deterministic=True),
177
180
  "join": dict(role="execution", inputs=["two or more video assets, or two or more audio-only assets"], outputs=["concatenated video artifact", "concatenated audio artifact (audio-only inputs, audio output extension)"],
178
181
  required=FF + ["filter:xfade", "filter:acrossfade"],
@@ -409,7 +412,8 @@ def output_schema(name: str, meta: Dict[str, Any], hw: bool = False) -> Dict[str
409
412
  "notes": {"type": "array", "items": {"type": "string"}, "description": "present when no --platform was named: youtube was assumed and judgement rows are WARN"},
410
413
  "checks": {"type": "array", "items": {"type": "object", "properties": {"check": {"type": "string", "description": "row name; --content adds black, frozen and silence; audio FAILs when the track is present but silent (peak <= -50 dBFS)"}, "status": {"enum": ["PASS", "WARN", "FAIL"]}, "value": {}, "expected": {}, "fix": {"type": "string"}, "kind": {"enum": ["format", "judgement"]}}}}}
411
414
  elif name == "caption":
412
- extra = {"caption": {"type": "object", "description": "cue layout: shifted / wrapped / rebalanced / split / extended / dropped counts, plus wrap ('phrase' or 'measured'), phrase_breaks (1.16), broken_inside_word -- atoms hard-sliced at the column edge because they did not fit alone even at the size floor (1.18.4) -- and overlong -- now residual: a single character alone wider than the column. Burn mode: cues_burned -- non-blank cues that overlap [0, duration] and are drawn -- and cues_outside -- non-blank cues wholly outside it (2.2.6); a burn with no visible cue is refused, kind input"},
415
+ extra = {"transcription": {"type": "object", "description": "with --transcribe: {engine, model, language, routing (why auto chose it), detected_language when auto detected it}; when auto ran the English-only Parakeet model on English assumed, notes says so"},
416
+ "caption": {"type": "object", "description": "cue layout: shifted / wrapped / rebalanced / split / extended / dropped counts, plus wrap ('phrase' or 'measured'), phrase_breaks (1.16), broken_inside_word -- atoms hard-sliced at the column edge because they did not fit alone even at the size floor (1.18.4) -- and overlong -- now residual: a single character alone wider than the column. Burn mode: cues_burned -- non-blank cues that overlap [0, duration] and are drawn -- and cues_outside -- non-blank cues wholly outside it (2.2.6); a burn with no visible cue is refused, kind input"},
413
417
  "tracks": {"type": "array", "description": "--mode mux: one entry per subtitle stream in the output ({index, file, language, title, codec, default, cues, kept_from_input}); a stream the input already carried has file null and kept_from_input true (1.16)"},
414
418
  "subtitle_tracks": {"type": "integer", "description": "--mode mux: how many subtitle streams the output carries"},
415
419
  "emoji": {"type": "object", "description": "how the emoji in the text were drawn (mode, overlays, missing)"},
@@ -430,7 +434,9 @@ def output_schema(name: str, meta: Dict[str, Any], hw: bool = False) -> Dict[str
430
434
  "speech": {"type": "array", "description": "--speech (1.18): [{time, speech_music_ratio}]"}}
431
435
  elif name == "silence":
432
436
  extra = {"silences": {"type": "array"}, "keep": {"type": "array"}, "input_duration": {"type": "number"}, "kept_duration": {"type": "number"}, "removed_seconds": {"type": "number"},
433
- "speech_aware": {"type": "object", "description": "--speech-aware (1.18): {min_silence, floor, breaths_kept, breaths_kept_seconds, breaths}"}}
437
+ "speech_aware": {"type": "object", "description": "--speech-aware (1.18): {min_silence, floor, breaths_kept, breaths_kept_seconds, breaths}"},
438
+ "filler": {"type": "object", "description": "--filler: {lang, source (whisper-json:FILE | whisper:ENGINE | parakeet:ENGINE), engine, transcription (with --transcribe: {engine, model, language, routing}), removed, removed_count, ...}"},
439
+ "notes": {"type": "array", "items": {"type": "string"}, "description": "--filler --transcribe under --engine auto: present when the English-only Parakeet model ran on English assumed (no --filler-lang, no language detected, no Whisper engine installed or none that succeeded); the transcript is wrong if the speech is not English"}}
434
440
  elif name == "sync":
435
441
  extra = {"reference": {"type": "string"}, "second": {"type": "string"}, "offset_seconds": {"type": "number"}, "confidence": {"type": "number"}, "meaning": {"type": "string"}, "drift": {"type": "object"},
436
442
  "sources": {"type": "array", "description": "1.18: [{path, offset_s, confidence, drift_ppm}], one per SOURCE; the only per-source shape once more than one SOURCE is given"}}
@@ -672,6 +678,13 @@ def _whisper_available() -> bool:
672
678
  return importlib.util.find_spec("faster_whisper") is not None or importlib.util.find_spec("whisper") is not None
673
679
 
674
680
 
681
+ def _parakeet_available() -> bool:
682
+ """parakeet-mlx on PATH, or parakeet-cli with a model it can find (PARAKEET_CPP_MODEL or a
683
+ .gguf in ~/.cache/parakeet.cpp) -- the same test the speech bridge applies before running it."""
684
+ from _common.asr import _parakeet_available as runtime_check # the bridge's own test, not a copy of it
685
+ return any(runtime_check(e, shutil) for e in ("parakeet-mlx", "parakeet.cpp"))
686
+
687
+
675
688
  def _hw_default() -> Dict[str, Any]:
676
689
  """--hw's machine facts, normalised (never the raw environment value): whether VideoToolbox
677
690
  constant-quality encoding can run here (the tools' own check: Apple Silicon hardware, also
@@ -870,6 +883,8 @@ def doctor() -> Dict[str, Any]:
870
883
  state[cap] = _from("bsfs", cap[4:])
871
884
  elif cap == "external:whisper":
872
885
  state[cap] = "available" if _whisper_available() else "missing"
886
+ elif cap == "external:parakeet":
887
+ state[cap] = "available" if _parakeet_available() else "missing"
873
888
  else:
874
889
  state[cap] = "missing"
875
890
  available = sorted(c for c, st in state.items() if st == "available")
@@ -997,7 +1012,15 @@ def _capability_fix_hint(cap: str) -> str:
997
1012
  return (f"no installed font covers {LANGUAGE_NAMES.get(script, script)} text on this machine; "
998
1013
  f"{FONT_INSTALL_HINT} (doctor --json .fonts.scripts lists every script)")
999
1014
  if cap == "external:whisper":
1000
- return "install a local whisper (whisper-cli, whisper-cpp, faster-whisper or openai-whisper) for --transcribe"
1015
+ return ("install a local whisper (whisper-cli, whisper-cpp, faster-whisper or openai-whisper) for --transcribe; "
1016
+ "faster-whisper and openai-whisper download their model on first use, and every engine transcribes "
1017
+ "on this machine (the audio is never uploaded)")
1018
+ if cap == "external:parakeet":
1019
+ from _common.asr import PARAKEET_MLX_DEFAULT_MODEL
1020
+ return ("for English --transcribe, install parakeet-mlx (uv tool install parakeet-mlx; its first run downloads "
1021
+ f"{PARAKEET_MLX_DEFAULT_MODEL} from Hugging Face, then it transcribes on this machine and the audio is "
1022
+ "never uploaded) or parakeet-cli with a tdt-0.6b-v2 .gguf in ~/.cache/parakeet.cpp; a local whisper "
1023
+ "covers every language")
1001
1024
  return f"'{cap}' is not available; see docs/contract.md"
1002
1025
 
1003
1026
 
@@ -1362,7 +1385,7 @@ def build(detect: bool = True) -> Dict[str, Any]:
1362
1385
  version = skill_version()
1363
1386
  tools = [tool_spec(n, version) for n in public_tools()]
1364
1387
  wanted = required_capabilities()
1365
- caps: Dict[str, Any] = {"required": wanted["required"], "optional": wanted["optional"], "naming": "ffmpeg | ffprobe | encoder:<name> | filter:<name> | bsf:<name> | external:whisper"}
1388
+ caps: Dict[str, Any] = {"required": wanted["required"], "optional": wanted["optional"], "naming": "ffmpeg | ffprobe | encoder:<name> | filter:<name> | bsf:<name> | external:whisper | external:parakeet"}
1366
1389
  if detect:
1367
1390
  d = doctor()
1368
1391
  caps.update({"available": d["available"], "missing": d["missing"], "missing_optional": d["missing_optional"],
@@ -1389,8 +1412,21 @@ def build(detect: bool = True) -> Dict[str, Any]:
1389
1412
  "execution": {
1390
1413
  "shell": False,
1391
1414
  "arbitrary_executables": False,
1392
- "subprocess": "argv list only: [python3, scripts/<tool>.py, ...] and [ffmpeg|ffprobe, ...] resolved from PATH",
1415
+ "subprocess": "argv list only: [python3, scripts/<tool>.py, ...] and [ffmpeg|ffprobe, ...] resolved from PATH; "
1416
+ "with --transcribe, a local speech engine's CLI (whisper.cpp's whisper-cli / whisper-cpp / main, "
1417
+ "openai-whisper's whisper, parakeet-mlx, parakeet-cli) resolved from PATH",
1393
1418
  "network": False,
1419
+ # `network` is about the skill's own code. A speech engine may fetch its model weights the
1420
+ # first time it runs; faster-whisper is a library, so its fetch is made from the skill's own
1421
+ # process. A caller that sandboxes the network must know that before a first --transcribe.
1422
+ "model_downloads": {
1423
+ "when": "first use of a speech engine (--transcribe) whose model is not cached yet; media is never uploaded",
1424
+ "in_process": {"faster-whisper": "Hugging Face (Systran/faster-whisper-<model>)"},
1425
+ "child_process": {"parakeet-mlx": "Hugging Face (mlx-community/parakeet-*)",
1426
+ "openai-whisper": "OpenAI (its model checkpoint)"},
1427
+ "none": ["whisper.cpp", "parakeet.cpp"],
1428
+ "offline": "HF_HUB_OFFLINE=1 keeps faster-whisper and parakeet-mlx on the cached copy",
1429
+ },
1394
1430
  "input_mutation": False,
1395
1431
  },
1396
1432
  "invocation": {
@@ -49,6 +49,8 @@ from _common import emoji_filter_chain, EMOJI_ASSET_HINT, emoji_asset_for, emoji
49
49
  # The ASR bridge and the SRT reader/writer live in _common.asr since 1.17 (silence.py --filler
50
50
  # shares them). They stay caption.py's public names -- every caller and test that reached for
51
51
  # caption.parse_srt / caption.transcribe / caption.whisper_word_timings before 1.17 still does.
52
+ from _common import asr as _asr
53
+ from _common.asr import ENGINE_CHOICES
52
54
  from _common import (ASR_INSTALL_HINT, die_no_engine, parse_srt, transcribe, whisper_word_timings,
53
55
  write_srt)
54
56
  from _common import (SAFE_WIDTH_FRACTION, ORPHAN_MIN_EM, WRAP_MODES, wrap_text, wrap_variants, best_break,
@@ -901,7 +903,7 @@ def main() -> int:
901
903
  "them; .mkv is the honest multi-track container and stores the code you give verbatim")
902
904
  src.add_argument("--ass", help="ASS file to burn (styles inside the file are used)")
903
905
  src.add_argument("--text", help="plain text cue file to convert into SRT (see format above)")
904
- src.add_argument("--transcribe", action="store_true", help="generate the SRT from the audio with a local speech-to-text engine if one is installed (whisper-cli / whisper / faster-whisper); never required")
906
+ src.add_argument("--transcribe", action="store_true", help="generate the SRT from the audio with a local speech-to-text engine if one is installed (parakeet-mlx / parakeet.cpp / whisper-cli / whisper / faster-whisper); never required")
905
907
  src.add_argument("--language", "--lang", help="language code (e.g. en, ja, zh, ko): the language for --transcribe (default auto), "
906
908
  "the tag on the subtitle stream with --mode mux, and the hint that says whether Han-only "
907
909
  "text is Chinese, Japanese or Korean when a font is picked by script")
@@ -914,7 +916,12 @@ def main() -> int:
914
916
  "(default: none, so no player burns in a language the viewer did not ask for)")
915
917
  src.add_argument("--offset", default="0", help="shift every cue by TIME (seconds, mm:ss, hh:mm:ss.ms or "
916
918
  "hh:mm:ss:ff; a leading - shifts earlier); works for --text, --srt and --ass")
917
- src.add_argument("--model", default="base", help="whisper model name/path for --transcribe (default base)")
919
+ src.add_argument("--model", default="base",
920
+ help="speech model for --transcribe: a whisper model name/path (default base), or a Parakeet "
921
+ "model (an mlx-community/parakeet-* repo or a .gguf) for the Parakeet engines")
922
+ src.add_argument("--engine", choices=ENGINE_CHOICES, default=None,
923
+ help="speech engine for --transcribe (default: $FFMPEG_SKILL_ASR_ENGINE, else auto = Parakeet "
924
+ "for English speech when installed, else whisper.cpp / faster-whisper / openai-whisper)")
918
925
  src.add_argument("--write-srt", help="where to save the generated SRT (default: <text>.srt)")
919
926
  src.add_argument("--auto-seconds", type=float, default=3.0, help="duration for cues without timing (default 3)")
920
927
  src.add_argument("--gap", type=float, default=0.0, help="gap after auto-timed cues in seconds")
@@ -1241,8 +1248,13 @@ def main() -> int:
1241
1248
  cues = []
1242
1249
  info(f"[dry-run] would transcribe {args.input} and write {srt_path}")
1243
1250
  else:
1244
- cues = transcribe(args.input, srt_path, args.language, args.model, args.audio_stream)
1245
- args._word_timings = whisper_word_timings(srt_path)
1251
+ # the call's own result, not module state: engine, routing and words belong to this run
1252
+ asr_run = _asr.transcribe_result(args.input, srt_path, args.language, args.model, args.audio_stream,
1253
+ engine=args.engine)
1254
+ cues = asr_run.cues
1255
+ args._asr = dict(asr_run.facts)
1256
+ args._asr_notes = list(asr_run.notes)
1257
+ args._word_timings = whisper_word_timings(srt_path) or sorted((w["start"], w["end"], w["word"]) for w in asr_run.words)
1246
1258
  cues, changed = lay_out(cues)
1247
1259
  if changed:
1248
1260
  write_srt(cues, srt_path)
@@ -1279,7 +1291,8 @@ def main() -> int:
1279
1291
  # WRITING waits for a real run (the rule every side file in this tool follows), which is why
1280
1292
  # the cues are kept in hand below for the font sample and the ASS generator.
1281
1293
  planned_cues: Optional[List[Tuple[float, float, str]]] = None
1282
- side_notes: List[str] = []
1294
+ # an English-only transcript made on an assumption says so first (asr.assumed_english_note)
1295
+ side_notes: List[str] = list(getattr(args, "_asr_notes", None) or [])
1283
1296
  ass_sample_path = args.ass
1284
1297
  if args.srt and not (args.text or args.transcribe) and os.path.exists(srt_path or ""):
1285
1298
  adjusted, changed = lay_out(parse_srt(srt_path))
@@ -1416,7 +1429,8 @@ def main() -> int:
1416
1429
  result = probe(output, role="output")
1417
1430
  info(f"wrote {output} ({fmt_secs(result.get('duration'))}, mux, {len(added)} "
1418
1431
  f"subtitle track(s) added, codec {codec})")
1419
- emit(output, tracks=tracks, subtitle_tracks=total, **({"notes": notes} if notes else {}))
1432
+ emit(output, tracks=tracks, subtitle_tracks=total, **({"notes": notes} if notes else {}),
1433
+ **({"transcription": args._asr} if getattr(args, "_asr", None) else {}))
1420
1434
  return 0
1421
1435
 
1422
1436
  # A font that covers the text, before anything is rendered: non-Latin cues in a Latin-only
@@ -1582,6 +1596,8 @@ def main() -> int:
1582
1596
  info("caption text unchanged: the cues were burned exactly as given (line breaks, timing "
1583
1597
  "and type size only)")
1584
1598
  extra["caption"] = dict(caption_stats)
1599
+ if getattr(args, "_asr", None):
1600
+ extra["transcription"] = args._asr
1585
1601
  if emoji_plan:
1586
1602
  notes = list(extra.get("notes") or [])
1587
1603
  if emoji_plan["mode"] == "mono":
@@ -20,7 +20,8 @@ from typing import List, Tuple
20
20
  # `detect` moved into _common/probe.py in 1.16.0 so metadata.py --auto-chapters can measure the
21
21
  # same silences without importing this tool; the body is unchanged and the name still lives here.
22
22
  from _common import (filler_spans, FILLER_WORDS, FILLER_AMBIGUOUS, FILLER_DISCOURSE_MARKERS,
23
- FILLER_PAD, transcribe_words, read_text_or_die)
23
+ FILLER_PAD, read_text_or_die)
24
+ from _common import asr as _asr
24
25
  from _common import detect_silences as detect, STATE, video_args, add_common, apply_common, audio_codec_for, cfr_args, default_output, die, emit, ffmpeg_base, info, is_audio_output, print_json, probe, run, X264_PRESETS, measured_level_dbfs, fmt_secs
25
26
 
26
27
 
@@ -99,7 +100,7 @@ def resolve_filler(args, meta):
99
100
  die("--filler needs word timings: pass --words transcript.json (a whisper JSON with word "
100
101
  "timestamps) or --transcribe. There is no way to find a filler word without them -- "
101
102
  "cutting the short quiet blips instead would remove real speech.", kind="input")
102
- source, engine, raw = None, None, None
103
+ source, engine, raw, transcription = None, None, None, {}
103
104
  if args.words:
104
105
  try:
105
106
  raw = json.loads(read_text_or_die(args.words, "--words"))
@@ -118,14 +119,20 @@ def resolve_filler(args, meta):
118
119
  '{"words": [{"word": ..., "start": ..., "end": ...}]} (or the same inside '
119
120
  '"segments").', kind="input")
120
121
  else:
121
- # No pre-check for an installed engine here: transcribe_words() probes for one and
122
+ # No pre-check for an installed engine here: transcribe_words_result() probes for one and
122
123
  # raises the same die_no_engine() refusal when there is none. Two places deciding "is
123
124
  # whisper here" is two places to disagree.
124
125
  # The engine is driven with ITS word-timestamp option (whisper.cpp --output-json-full,
125
126
  # faster-whisper word_timestamps=True, openai-whisper --word_timestamps True). An SRT
126
127
  # cannot answer this question: a cue has a start and an end, a word does not.
127
- words, engine = transcribe_words(args.input, args.filler_lang if args.filler_lang != "auto" else None)
128
- source = f"whisper:{engine}" if engine else "whisper"
128
+ # The call's own result, not module state: engine, routing and notes belong to this run.
129
+ asr_run = _asr.transcribe_words_result(args.input, args.filler_lang if args.filler_lang != "auto" else None,
130
+ engine=args.engine)
131
+ words, engine = asr_run.words, asr_run.engine
132
+ transcription = dict(asr_run.facts)
133
+ args._asr_notes = list(asr_run.notes)
134
+ family = "parakeet" if engine in _asr.PARAKEET_ENGINES else "whisper"
135
+ source = f"{family}:{engine}" if engine else "whisper"
129
136
  if not words:
130
137
  die(f"{engine or 'the local engine'} ran but produced no word-level timings, so there "
131
138
  "is nothing for --filler to cut on. Some builds do not support word timestamps. "
@@ -168,6 +175,7 @@ def resolve_filler(args, meta):
168
175
  "disfluency -- this will cut real sentences.")
169
176
  block = {
170
177
  "lang": lang, "source": source, "engine": engine,
178
+ **({"transcription": transcription} if engine and transcription else {}),
171
179
  "words": sorted(wordlist), "removed": [dict(s) for s in spans],
172
180
  "removed_count": len(spans),
173
181
  "removed_seconds": round(sum(s["end"] - s["start"] for s in spans), 3),
@@ -235,8 +243,11 @@ def main() -> int:
235
243
  fil.add_argument("--words", metavar="FILE",
236
244
  help="a whisper JSON with word-level timings, for --filler")
237
245
  fil.add_argument("--transcribe", action="store_true",
238
- help="produce the word timings with a local whisper (never required; the same "
246
+ help="produce the word timings with a local speech engine, Parakeet or whisper (never required; the same "
239
247
  "bridge caption.py uses)")
248
+ fil.add_argument("--engine", choices=_asr.ENGINE_CHOICES, default=None,
249
+ help="speech engine for --transcribe (default: $FFMPEG_SKILL_ASR_ENGINE, else auto = Parakeet "
250
+ "for English speech when installed, else whisper)")
240
251
  fil.add_argument("--filler-list", action="store_true",
241
252
  help="report what --filler would remove and write nothing")
242
253
  fil.add_argument("--max-cuts", type=int, default=400,
@@ -311,6 +322,10 @@ def main() -> int:
311
322
  f"({filler_info['lang']}, list {filler_info['list']})")
312
323
  for warning in filler_info.get("warnings") or []:
313
324
  info("warning: " + warning)
325
+ if getattr(args, "_asr_notes", None):
326
+ # an English-only transcript made on an assumption (asr.assumed_english_note): the
327
+ # bridge already logged the warning; the result document carries it at the top level
328
+ summary["notes"] = list(args._asr_notes)
314
329
  if len(keeps) > args.max_cuts:
315
330
  die(f"{len(keeps)} keep ranges is above --max-cuts {args.max_cuts}: the filter graph grows "
316
331
  "with every range and a graph this size is slow and fragile. Raise --max-cuts if you "