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.
- package/README.md +2 -2
- package/SKILL.md +2 -2
- package/docs/contract.md +40 -7
- package/package.json +1 -1
- package/references/scripts.md +43 -9
- package/scripts/_common/asr.py +747 -94
- package/scripts/_contract.py +43 -7
- package/scripts/caption.py +22 -6
- package/scripts/silence.py +21 -6
package/scripts/_contract.py
CHANGED
|
@@ -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"}
|
|
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 = {"
|
|
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": {
|
package/scripts/caption.py
CHANGED
|
@@ -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",
|
|
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
|
-
|
|
1245
|
-
|
|
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
|
-
|
|
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":
|
package/scripts/silence.py
CHANGED
|
@@ -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,
|
|
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:
|
|
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
|
-
|
|
128
|
-
|
|
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 "
|