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 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.0`) | any release |
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.0", "execution_mode": "local", "kind": "execution",
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.0",
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",
@@ -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):
@@ -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)