ffmpeg-skill 3.1.0 → 3.1.1
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/docs/contract.md +2 -2
- package/package.json +1 -1
- package/scripts/_common/asr.py +10 -4
- package/scripts/silence.py +5 -1
package/docs/contract.md
CHANGED
|
@@ -21,7 +21,7 @@ The contract is derived from the code that runs, not maintained beside it:
|
|
|
21
21
|
| Field | Meaning | Changes when |
|
|
22
22
|
|---|---|---|
|
|
23
23
|
| `contract_version` | shape of this document (`2.0`) | a key is renamed, removed or changes meaning |
|
|
24
|
-
| `skill.version` | the npm / package.json version (`3.1.
|
|
24
|
+
| `skill.version` | the npm / package.json version (`3.1.1`) | any release |
|
|
25
25
|
|
|
26
26
|
A release that adds a tool or a flag keeps `contract_version`; a breaking change to the
|
|
27
27
|
ToolSpec shape bumps it. Consumers pin on `contract_version` and read `skill.version`
|
|
@@ -149,7 +149,7 @@ JSON would have been rewritten for that. The flat keys, typed per tool in `outpu
|
|
|
149
149
|
"deprecated": [],
|
|
150
150
|
"removed": [{"what": "...", "since": "1.10.0", "replacement": "...", "removed_in": "2.0.0", "where": "cli | json | mcp | behaviour"},
|
|
151
151
|
{"what": "running on FFmpeg 5.x (the requirements.ffmpeg floor)", "since": "2.14.0", "replacement": "FFmpeg 6.1 or later; ...", "removed_in": "3.0.0", "where": "behaviour"}],
|
|
152
|
-
"skill": {"id": "ffmpeg-skill", "version": "3.1.
|
|
152
|
+
"skill": {"id": "ffmpeg-skill", "version": "3.1.1", "execution_mode": "local", "kind": "execution",
|
|
153
153
|
"entrypoints": {"cli": "...", "mcp": "...", "contract": "...", "doctor": "..."},
|
|
154
154
|
"not_provided": ["AI reasoning", "decisions", "production plans", "project IR", "approvals", "network access", "transcription engine"]},
|
|
155
155
|
"requirements": {"python": ">=3.10 (standard library only)", "ffmpeg": ">=6.1", "ffprobe": ">=6.1"},
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "ffmpeg-skill",
|
|
3
|
-
"version": "3.1.
|
|
3
|
+
"version": "3.1.1",
|
|
4
4
|
"description": "Agent Skill that gives coding agents (Claude Code, Cursor, Codex) a local video editor: 42 FFmpeg tools with a machine-readable contract, contract-derived MCP server, FFmpeg capability detection, probe-first / verify-last workflow. Cut, join, silence removal, fit, captions and karaoke, overlays, motion graphics, HDR to SDR, LUTs, audio clean-up and typed dynamics, sync with drift correction, multicam, loudness, delivery checks, project rendering, batch. No API keys, no cloud, no dependencies.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"ffmpeg",
|
package/scripts/_common/asr.py
CHANGED
|
@@ -991,7 +991,7 @@ def transcribe_words(video: str, language: "Optional[str]" = None, model: "Optio
|
|
|
991
991
|
|
|
992
992
|
def transcribe_words_result(video: str, language: "Optional[str]" = None, model: "Optional[str]" = None,
|
|
993
993
|
audio_stream: int = 0, engine: "Optional[str]" = None,
|
|
994
|
-
language_flag: str = "--filler-lang") -> Transcription:
|
|
994
|
+
language_flag: str = "--filler-lang", prompt: "Optional[str]" = None) -> Transcription:
|
|
995
995
|
"""[{word, start, end}, ...] from a local speech engine, as a Transcription naming the engine.
|
|
996
996
|
|
|
997
997
|
`model` (else $FFMPEG_SKILL_ASR_MODEL, else base: requested_model()) is the model and
|
|
@@ -1003,7 +1003,10 @@ def transcribe_words_result(video: str, language: "Optional[str]" = None, model:
|
|
|
1003
1003
|
when the engine ran but its build produced no word-level timings, so the caller can refuse
|
|
1004
1004
|
naming that engine instead of pretending the audio had no words in it. No engine at all
|
|
1005
1005
|
raises through die_no_engine(), the same refusal caption.py gives. `language_flag` is the
|
|
1006
|
-
caller's flag for the language, named in assumed_english_note().
|
|
1006
|
+
caller's flag for the language, named in assumed_english_note(). `prompt` is passed to a
|
|
1007
|
+
Whisper engine as its initial prompt (whisper.cpp --prompt, faster-whisper initial_prompt,
|
|
1008
|
+
openai-whisper --initial_prompt): Whisper writes clean text and leaves out "uh"/"um" unless
|
|
1009
|
+
the context shows it that they are wanted, so silence.py --filler primes it with its own list.
|
|
1007
1010
|
"""
|
|
1008
1011
|
import shutil as _shutil
|
|
1009
1012
|
from _common.runner import SafeShutil
|
|
@@ -1058,7 +1061,7 @@ def transcribe_words_result(video: str, language: "Optional[str]" = None, model:
|
|
|
1058
1061
|
base = os.path.join(tmpdir, "out")
|
|
1059
1062
|
# whisper.cpp's own default is -l en: without "auto" it decodes any speech as English
|
|
1060
1063
|
cmd = [cli, "-m", model_path, "-f", wav, "--output-json-full", "-of", base, "-l", lang or "auto",
|
|
1061
|
-
*WHISPER_CPP_NO_CONTEXT]
|
|
1064
|
+
*WHISPER_CPP_NO_CONTEXT] + (["--prompt", prompt] if prompt else [])
|
|
1062
1065
|
proc = _asr_run(cmd, _subprocess, "whisper.cpp")
|
|
1063
1066
|
if proc.returncode == 0 and os.path.exists(base + ".json"):
|
|
1064
1067
|
words = _words_from_whisper_cpp_json(base + ".json")
|
|
@@ -1084,7 +1087,8 @@ def transcribe_words_result(video: str, language: "Optional[str]" = None, model:
|
|
|
1084
1087
|
def work() -> None:
|
|
1085
1088
|
try:
|
|
1086
1089
|
m = WhisperModel(model, device="cpu", compute_type="int8")
|
|
1087
|
-
segments, decoded = m.transcribe(wav, language=lang, word_timestamps=True
|
|
1090
|
+
segments, decoded = m.transcribe(wav, language=lang, word_timestamps=True,
|
|
1091
|
+
**({"initial_prompt": prompt} if prompt else {}))
|
|
1088
1092
|
heard.append(getattr(decoded, "language", None))
|
|
1089
1093
|
for seg in segments:
|
|
1090
1094
|
for w in (getattr(seg, "words", None) or []):
|
|
@@ -1119,6 +1123,8 @@ def transcribe_words_result(video: str, language: "Optional[str]" = None, model:
|
|
|
1119
1123
|
"--output_format", "json", "--output_dir", tmpdir]
|
|
1120
1124
|
if lang:
|
|
1121
1125
|
cmd += ["--language", lang]
|
|
1126
|
+
if prompt:
|
|
1127
|
+
cmd += ["--initial_prompt", prompt]
|
|
1122
1128
|
proc = _asr_run(cmd, _subprocess, "openai-whisper")
|
|
1123
1129
|
doc = os.path.join(tmpdir, "audio.json")
|
|
1124
1130
|
if proc.returncode == 0 and os.path.exists(doc):
|
package/scripts/silence.py
CHANGED
|
@@ -246,8 +246,12 @@ def resolve_filler(args, meta):
|
|
|
246
246
|
# faster-whisper word_timestamps=True, openai-whisper --word_timestamps True). An SRT
|
|
247
247
|
# cannot answer this question: a cue has a start and an end, a word does not.
|
|
248
248
|
# The call's own result, not module state: engine, routing and notes belong to this run.
|
|
249
|
+
# Whisper leaves "uh"/"um" out of its text unless the context shows them: prime it with the
|
|
250
|
+
# filler list of the language asked for (English under auto, where most filler cuts are)
|
|
251
|
+
primer = FILLER_WORDS.get(args.filler_lang if args.filler_lang != "auto" else "en") or []
|
|
252
|
+
prompt = (", ".join(sorted(primer)[:8]).capitalize() + ". So, I mean...") if primer else None
|
|
249
253
|
asr_run = _asr.transcribe_words_result(args.input, args.filler_lang if args.filler_lang != "auto" else None,
|
|
250
|
-
model=args.model, engine=args.engine)
|
|
254
|
+
model=args.model, engine=args.engine, prompt=prompt)
|
|
251
255
|
words, engine = asr_run.words, asr_run.engine
|
|
252
256
|
transcription = dict(asr_run.facts)
|
|
253
257
|
args._asr_notes = list(asr_run.notes)
|