ffmpeg-skill 1.16.1 → 1.17.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.
@@ -64,6 +64,7 @@ BT.2020 primaries on an SDR transfer, which `hdr_format` names "BT.2020 SDR" --
64
64
  ### cut.py — cut / join segments
65
65
  ```
66
66
  cut.py INPUT [--start T] [--end T | --duration T] [--segments A-B,C-D,...] [--accurate] [-o OUT]
67
+ cut.py INPUT --start T --end T --snap beats [--snap-tolerance 0.12] [--snap-source FILE] [--min-confidence 0.5]
67
68
  ```
68
69
  Times accept `12.5`, `1:30`, `00:01:30.250`. Default is `-c copy` (snaps to
69
70
  keyframes, instant, lossless); if the snapped result deviates more than
@@ -74,6 +75,49 @@ the result was "lossless stream copy" or "re-encoded"; when the snap forced a
74
75
  re-encode, the result's `lossless_alternative` names the nearest keyframe
75
76
  `--start` that would stream-copy instead, so the trade can be offered.
76
77
 
78
+ **`--snap beats` (1.17)** moves each in/out point to the nearest *measured*
79
+ beat within `--snap-tolerance` seconds (default 0.12, about a quarter of a beat
80
+ at 120 BPM). The grid comes from the input's own audio, or from
81
+ `--snap-source` — either a `scenes.py --beats --json` document (no second
82
+ decode) or a separate music file. The snap happens *before* the
83
+ keyframe/tolerance decision, so lossless-vs-accurate is judged on where the cut
84
+ actually lands.
85
+
86
+ "Measured" is exact: the only points a cut may move onto are the
87
+ `supported_beats` — the grid points a measured onset actually marks — never the
88
+ full regular grid, which by construction runs on through a passage with no music
89
+ in it. A cut asked for inside that passage stays where it was asked for, and
90
+ `snap.grid` is `"supported"` with `snap.grid_points` saying how many there were.
91
+ `--min-confidence` must be greater than 0: at 0 every grid is "reliable",
92
+ including one measured from noise.
93
+
94
+ The number of in/out points never changes: a point with no beat inside the
95
+ tolerance is returned unchanged, and no point is ever invented. Three
96
+ `kind: input` refusals, nothing written:
97
+
98
+ - confidence below `--min-confidence` → *"no reliable beat grid in this audio
99
+ (confidence 0.21, needs 0.5): cutting to invented beats would move your in/out
100
+ points to times nothing in the audio supports. Re-run with `--snap none`, or
101
+ pass `--snap-source` from a music bed."*
102
+ - no audio stream → names `--snap none`.
103
+ - no in/out point at all (a whole-file copy) → there is nothing to snap, and
104
+ this tool never proposes cuts of its own.
105
+
106
+ Result: `snap.mode`, `snap.tolerance`, `snap.confidence`, `snap.tempo_bpm`,
107
+ `snap.moved` (one row per point, with `from`/`to`/`delta`/`snapped`),
108
+ `snap.snapped`, `snap.unchanged`, `snap.grid`, `snap.grid_points`, and
109
+ `snap.source` (`"measured"` or the path). In a `render.py` result, `snap.clips`
110
+ carries one entry per snapped clip with `clip` naming its index, and the first
111
+ entry's keys are repeated at the top level for a single-clip project. A clip
112
+ served from `--cache` was snapped when it was first rendered, and says so
113
+ (`source: "cache"`) rather than reporting `snap: null`. A `--snap-source`
114
+ document that does
115
+ not carry `beat_grid.supported_beats` (one written before 1.17) is refused
116
+ rather than treated as if every grid point were supported, as is one whose
117
+ `tempo_bpm` is null while it lists beats.
118
+ `render.py` forwards a project's `"snap": {"to": "beats", ...}` (project-wide
119
+ or per clip) to this flag and reports what came back.
120
+
77
121
  ### fit.py — target duration and/or aspect, rotate/flip
78
122
  ```
79
123
  fit.py INPUT [--duration T --method speed|trim [--from-center] [--max-speed 4]]
@@ -422,6 +466,9 @@ each shorter clip's last frame (with silence) out to the longest.
422
466
  ### silence.py — remove dead air / jump cuts
423
467
  ```
424
468
  silence.py INPUT [--threshold -35] [--min-silence 0.6] [--margin 0.15] [--min-keep 0.2] [--list] [--edl keep.txt] [-o OUT]
469
+ silence.py INPUT --filler --words transcript.json [--filler-lang auto|en|ja|es|de|fr|pt|it]
470
+ [--filler-words FILE] [--filler-extra W,W] [--filler-keep W,W] [--filler-pad 0.02]
471
+ [--transcribe] [--filler-list] [--max-cuts 400]
425
472
  ```
426
473
  Runs `silencedetect`, keeps `--margin` seconds of air around speech, drops
427
474
  gaps shorter than `--min-silence`, and re-encodes once with `select`/`aselect`
@@ -430,6 +477,60 @@ without rendering; `--edl` saves the kept ranges in `cut.py --segments` format
430
477
  so the user can edit the list by hand. Quiet rooms need `--threshold -40`
431
478
  to `-45`; noisy ones `-30`. Always tell the user how many seconds were removed.
432
479
 
480
+ **`--filler` (1.17)** removes filler words as well, through the same
481
+ `keep_ranges()`/`aselect` graph with more, shorter ranges. It is **never**
482
+ applied without measured word timings: pass `--words` (a whisper JSON carrying
483
+ per-word `start`/`end`) or `--transcribe` (the same local engine `caption.py`
484
+ uses, never required). There is no heuristic fallback — finding an "um" by
485
+ looking for short quiet blips would cut real speech, so the tool refuses
486
+ instead. `--filler-list` reports what would go and writes nothing.
487
+
488
+ Built-in lists, one per language:
489
+
490
+ | lang | words |
491
+ |---|---|
492
+ | en | um, uh, erm, hmm, mm, mhm, er, ah |
493
+ | ja | えー, えーと, えっと, あの, あのー, その, そのー, まあ, なんか |
494
+ | es | eh, este, esto, mmm |
495
+ | de | äh, ähm, hm |
496
+ | fr | euh, hein |
497
+ | pt | é, hum |
498
+ | it | ehm |
499
+
500
+ **Discourse markers are not disfluencies.** `like`, `tipo` and `cioè` are
501
+ deliberately *not* in the default lists: they are grammatical words in most
502
+ sentences, and removing them cuts meaning rather than noise — a judgement about
503
+ content, which this skill does not make. They are reachable with
504
+ `--filler-extra like`, which says so in its own `--help`. Japanese `なんか` is
505
+ in the `ja` list because leaving it out makes the flag useless for Japanese,
506
+ and it is orthographically identical to the pronoun use — so every run that
507
+ removes one warns, and `--filler-keep なんか` takes it back out. Matching is
508
+ case-folded, punctuation-stripped and whole-token only: `umbrella` survives.
509
+
510
+ Three refusals, all `kind: input`, all before any encode:
511
+
512
+ - `--filler` with neither `--words` nor `--transcribe` → names both flags.
513
+ - `--transcribe` with no engine on PATH → the same message `caption.py` gives,
514
+ with the three install lines.
515
+ - `--transcribe` where the engine runs but its build produces no word-level
516
+ timings → names that engine, says some builds do not support word timestamps,
517
+ and points at `--words`. (`--transcribe` drives whichever engine is installed
518
+ with *its* word-timestamp option — whisper.cpp `--output-json-full`,
519
+ faster-whisper `word_timestamps=True`, openai-whisper `--word_timestamps
520
+ True` — because an SRT cannot answer this: a cue has a start and an end, a
521
+ word does not. `--words` is the tested path and the one to prefer.)
522
+ - a transcript with segments but no word-level timings → says that cutting on
523
+ segment boundaries would remove whole sentences, and how to re-run whisper.
524
+
525
+ Results: `filler.removed_count`, `filler.removed_seconds`, `filler.removed`
526
+ (one entry per span), `filler.lang`, `filler.list`, `filler.word_timings` and
527
+ `filler.warnings`. The existing `removed_seconds` keeps exactly the meaning it
528
+ has always had — the seconds of **silence** this run removed, the figure the
529
+ same run would report without `--filler` — and `removed_seconds_total` is the
530
+ additive sibling covering everything that went. A filler word quiet enough to
531
+ sit inside a detected silence is merged into it rather than counted twice, so
532
+ the two figures can be equal.
533
+
433
534
  ### join.py — concatenate with transitions
434
535
  ```
435
536
  join.py CLIP1 CLIP2 [...] [--transition fade|dissolve|wipeleft|slideleft|fadeblack|fadewhite|circleopen|none]
@@ -447,6 +548,7 @@ output; the result says so with `dropped_non_av_streams: true`.
447
548
  ```
448
549
  render.py --init project.json # starter file
449
550
  render.py project.json [--fast] [--dry-run] [--stop-after STAGE] [--work DIR --keep]
551
+ render.py project.json --cache DIR [--from STAGE] # reuse identical stages from a previous run
450
552
  render.py plan.json # execute a plan written by <tool> --plan plan.json
451
553
  ```
452
554
  A plan is a single tool's dry run as an artifact: `cut.py in.mp4 --start 2 --end 8
@@ -479,6 +581,44 @@ entries and the file path are validated before the first stage runs, and the
479
581
  stage plans its `metadata.py` command under `--dry-run`/`--plan` like every
480
582
  other stage, so the plan lists `chapters` and the run does the same work.
481
583
 
584
+ **`--cache DIR` (1.17)** reuses the artifact of a stage that already ran with
585
+ exactly the same arguments and the same inputs. Opt-in only: **there is no
586
+ default cache directory** — one appearing on someone's disk unasked would
587
+ contradict this tool's "a plan leaves nothing behind" posture. The key is a
588
+ sha1 over the stage name, the tool, its arguments (every existing path replaced
589
+ by its content hash), the input hashes, the **forwarded** flags (`--fast`,
590
+ `--overwrite`, `--timeout`, `--codec`), the output's extension, and **the ffmpeg
591
+ build banner, the skill version and the contract version**. `--fast` matters as
592
+ much as any of them: it rewrites every child's preset to `veryfast`, so without
593
+ it in the key a `--cache --fast` draft would be served back to a later run that
594
+ asked for the delivery. The banner rather than `major.minor` because two 7.1.x
595
+ builds with different libx264 produce different bytes from the same command. Those last three are in the key deliberately: a
596
+ different build simply *misses* rather than being asked to trust a file it did
597
+ not write, and a stage whose implementation changed cannot serve back an
598
+ artifact the old one produced. Each entry is `DIR/<key><ext>` plus a
599
+ `DIR/<key>.json` sidecar (stage, versions, creation time, size, seconds). A hit
600
+ hardlinks the artifact into the work directory, or copies it where the
601
+ filesystem will not link — never moves it, since the cache has to outlive the
602
+ run's own cleanup. Any mismatch is a silent miss.
603
+
604
+ A stage whose flags changed misses the cache, so editing the captions block (for
605
+ example to `"fit_size": "off"`) re-runs captions and everything after it and
606
+ serves the earlier stages from the cache.
607
+
608
+ `stages_done` is unchanged: a cached stage is still a stage that happened.
609
+ Nothing is written under `--dry-run`, which instead reports `cache.would_hit`.
610
+ Result: `cache.dir`, `cache.ffmpeg`, `cache.hits`, `cache.misses`,
611
+ `cache.saved_seconds`, `cache.entries`. `--from STAGE` starts at that stage and
612
+ takes every earlier one from the cache; without `--cache`, or when an earlier
613
+ stage is not there, it refuses (`kind: input`) rather than quietly re-encoding
614
+ what it promised to skip. An unwritable cache directory is `kind: output`.
615
+
616
+ `"snap": {"to": "beats", "tolerance": 0.12, "min_confidence": 0.5, "source":
617
+ "music.mp3"}` at the project root (or inside one clip) forwards `--snap beats`
618
+ to the clip cut for every clip that has `in`/`out`; the measured grid comes back
619
+ in the render result's `snap`. A project without `"snap"` builds the command
620
+ line 1.16 built.
621
+
482
622
  Stages: clips (cut, optional speed) → join (transition) → silence → fit →
483
623
  captions → graphics → overlays → audio → loudness → export → chapters → check. Keys mirror the
484
624
  CLI flags of each script (see the docstring); a key `render.py` does not read -- at
@@ -509,6 +649,17 @@ Alias spellings are accepted everywhere one name is: `youtube-shorts`/`yt-shorts
509
649
  (`check.py --platform`, `export.py --preset`, `caption.py`/`graphics.py`/`overlay.py
510
650
  --platform`, `look.py --safe`, `render.py --template`).
511
651
 
652
+ **Caption size on the template path (1.17.1).** A template's caption `size` and
653
+ `margin` come from the delivery table, not from a literal in the JSON, so the
654
+ filled project also states `"fit_size": "on"`: a size nobody asked for must not
655
+ switch off the size fitter the way a stated `--size` does (before 1.17.1 a long
656
+ cue was split across two consecutive cues on every `--template` run). A template
657
+ file that states its own `fit_size`, and a `--brand` whose caption block states a
658
+ size, both win. A project may state the policy itself — `"captions": {"fit_size":
659
+ "auto"|"on"|"off", "min_size": N, "fit_size_scope": "file"|"cue"}` — and
660
+ `"fit_size": "off"` renders the captions 1.17.0 rendered, byte for byte. The
661
+ render result carries the caption stage's own block as `caption`.
662
+
512
663
  Under `--dry-run` a pack prints every child's planned commands and its table reads `planned`
513
664
  with no size or duration: nothing was encoded, so nothing is reported as verified. `--chapters`
514
665
  reaches a pack's audio destination like it does the single-template form.
@@ -549,6 +700,7 @@ already has a picture.
549
700
  ### scenes.py — scene changes and highlight candidates
550
701
  ```
551
702
  scenes.py INPUT [--threshold 10] [--min-scene 1] [--highlights N [--target SECONDS] [--max-scene 15]] [--edl picks.txt] [--sheet scenes.png] [--json]
703
+ scenes.py INPUT --beats [--beat-step 0.01] [--beat-range 60-200] [--min-confidence 0.5] [--json]
552
704
  ```
553
705
  Lists scenes with audio energy, the loudest moments, and (with
554
706
  `--highlights`) proposes N ranges that add up to `--target` seconds, biased to
@@ -560,6 +712,45 @@ Dissolves and very slow fades are not cuts and will be missed. Highlights are
560
712
  a proposal engine, not a judgement of content: tell the user what it picked
561
713
  and why (energy, scene length).
562
714
 
715
+ **`--beats` (1.17)** measures the music's beat grid and reports it:
716
+
717
+ ```json
718
+ "beats": [0.0, 0.5, 1.0, ...], // 25 of them over a 12 s click track
719
+ "beat_grid": {"tempo_bpm": 120.0, "interval": 0.5, "confidence": 0.997,
720
+ "supported_beats": [0.0, 0.5, 1.0, ...], // the 24 an onset marks
721
+ "phase": 0.0, "onsets": 24, "supported": 24, "unsupported": 1,
722
+ "method": "rms-flux-autocorrelation", "step_s": 0.01,
723
+ "range_bpm": [60, 200], "usable": true}
724
+ ```
725
+
726
+ Method: onset strength as the half-wave-rectified first difference of
727
+ `log(envelope)`, peaks above `median + 1.5·MAD` over a ±1 s window with a 60 ms
728
+ refractory gap, tempo from the autocorrelation of the onset signal inside
729
+ `--beat-range` with its half and double checked, phase chosen to catch the most
730
+ onset strength. `confidence` is half how far the winning lag stands above the
731
+ other lags (in standard deviations) and half the fraction of onsets that land
732
+ on the grid. With `--beats` the file is decoded once, at 22050 Hz, and the
733
+ `supported + unsupported == len(beats)` always, and `supported_beats` is that
734
+ supported subset — the list `cut.py --snap beats` moves onto.
735
+
736
+ With `--beats` the file is decoded once, at 22050 Hz, and the 0.5 s scene
737
+ envelope is derived from that same pass rather than from a second 8 kHz decode.
738
+ One consequence worth knowing: `scenes.py X --json` and `scenes.py X --beats
739
+ --json` report very slightly different `audio_rms`/`audio_peak` figures for the
740
+ same file, because the two envelopes are built from PCM at different rates. The
741
+ scene boundaries and their ranking are unaffected; only the fourth decimal of
742
+ the level moves. `--beats` also holds ~2.75x the samples in memory, which is
743
+ worth knowing on a feature-length input.
744
+
745
+ **A beat grid is a measurement of the music's periodicity, not of where a cut
746
+ belongs.** A low confidence means the audio has no steady pulse — speech,
747
+ ambience, rubato — and the skill will not snap to a grid it cannot measure.
748
+ `scenes.py` is the analysis tool, so it *reports* a weak measurement
749
+ (`usable: false`) rather than refusing it; refusing belongs to the tools that
750
+ would change a file on the strength of it (`cut.py --snap beats`). `--edl` with
751
+ `--beats` is unchanged: beats are never written as an EDL, because a beat is
752
+ not a cut. No audio stream is a `kind: input` refusal.
753
+
563
754
  ### check.py — pre-delivery compliance
564
755
  ```
565
756
  check.py INPUT --platform youtube|shorts|reels|tiktok|x|linkedin|facebook|broadcast|podcast|custom [--no-loudness] [--json]
@@ -582,13 +773,42 @@ construction, not by two lists agreeing.
582
773
 
583
774
  ### batch.py — same recipe over a folder, cached
584
775
  ```
585
- batch.py FOLDER --recipe batch.json [--force] [--watch SECONDS] [--json]
776
+ batch.py FOLDER --recipe batch.json [--force] [--watch SECONDS] [--jobs N|auto] [--json]
586
777
  ```
587
778
  `batch.json` holds either `steps` (a list of script argv with `{in}`/`{out}`
588
779
  placeholders, chained) or `project` (a render project applied per file).
589
780
  Outputs land in `output_dir` with `suffix`; a content-hash cache skips files
590
781
  already done with the same recipe. Use `--dry-run` to preview the plan.
591
782
 
783
+ **`--jobs N` (1.17)** processes N files at once (threads: the work is
784
+ subprocess waiting). Capped at `min(N, cpu_count, 8)` — every item is itself an
785
+ ffmpeg that already threads across cores, so beyond a few concurrent encodes
786
+ the jobs contend and wall-clock stops improving while memory does not. A number
787
+ above the cap is clamped with a note, not refused, and both `jobs` and
788
+ `jobs_requested` are in the result. `--jobs auto` is `min(cpu_count, 4)`.
789
+
790
+ - **One timeout budget for the whole batch**, not one per item: `--timeout` is
791
+ computed into a deadline once, no new item starts after it, and the run exits
792
+ 124 `kind: timeout` with the items that never started marked
793
+ `"skipped": "timeout"`. That shared budget applies when a `--timeout` was
794
+ actually given, or when `--jobs > 1` asked for the batch to be treated as one
795
+ piece of work; the default sequential path with the default timeout is 1.16's
796
+ behaviour exactly, where a long folder was never cut off part-way.
797
+ - **Determinism**: every item is written into its own slot in one list indexed
798
+ by position in the sorted file list — cached hits included, which is what keeps
799
+ the table in file order when the cache is only partially warm — and each item's
800
+ log lines are buffered and flushed in file order, so the per-item table and the
801
+ log read exactly as a serial run's whatever order the encodes finish in.
802
+ - **A failed item is a row, not a dead run**: a worker that raises becomes
803
+ `ok: false` with its reason, and the summary and the table are still printed.
804
+ Ctrl-C cancels what has not started, keeps what finished, and exits 130
805
+ `kind: interrupted` with the partial table.
806
+ - With `--jobs > 1` each item gets its own work subdirectory
807
+ `<workdir>/<index>-<stem>/`, because step file names are stem-derived and two
808
+ sources sharing a stem would otherwise write over each other. `--jobs 1` (the
809
+ default) keeps the flat layout and is 1.16's path exactly.
810
+ - `--watch` composes: each pass is parallel, passes are sequential.
811
+
592
812
  ### caption.py --transcribe — optional local speech-to-text
593
813
  If `whisper-cli` (whisper.cpp), `faster-whisper` or `whisper` is installed,
594
814
  `caption.py input.mp4 --transcribe [--language ja] [--model base]` writes the
@@ -726,11 +946,71 @@ caption.py INPUT --srt FILE[:LANG] | --ass FILE | --text CUES.txt [--write-srt O
726
946
  [--audio-stream N] [--fps N] [--lang XX] [--offset TIME]
727
947
  [--max-lines N] [--min-duration S] [--wrap phrase|measured]
728
948
  [--font NAME] [--fonts-dir DIR] [--size N] [--color RRGGBB] [--outline N] [--outline-color RRGGBB]
949
+ [--fit-size auto|on|off] [--min-size N] [--fit-size-scope file|cue]
729
950
  [--bold] [--box] [--position bottom|top|center|top-left|...] [--margin N]
730
951
  [--animate none|fade|pop|slide] [--karaoke [--highlight-color RRGGBB]] [--write-ass OUT.ass]
731
952
  [--emoji auto|color|png|mono|none] [--emoji-assets DIR] [--emoji-scale 1.0] [--emoji-max 60] [-o OUT]
732
953
  caption.py --text CUES.txt --write-srt OUT.srt # generate the SRT only
733
954
  ```
955
+ **`--fit-size` (1.17): the size is fitted before a cue is split.** At the TikTok
956
+ caption size (24 ASS units against the 288-line script grid) a line has about
957
+ six em, so an ordinary sentence needs four lines — and `--max-lines 2` then cut
958
+ it into consecutive cues, so half of it arrived late. That was the size, not the
959
+ breaker. `--fit-size` walks the size down until every cue wraps within
960
+ `--max-lines`, and only then lays the cues out.
961
+
962
+ - `auto` (default) shrinks only a size the *skill* chose. An explicit `--size`,
963
+ or a `brand.json` `styles.caption.size`, is a statement about the look and is
964
+ never overridden.
965
+ - `on` always fits, `--size` or not. **`off` is 1.16.1 exactly, byte for byte**
966
+ (a pinned ASS fixture asserts it).
967
+ - `--min-size` is the floor, default `ass_units(0.045) = 13` — **4.5 % of the
968
+ frame height**, one floor for every destination (87 px of type on a 1920-tall
969
+ frame, above the ~3.5 % where mobile legibility and the platforms' own caption
970
+ UIs bottom out). Nothing per-platform is measured, so nothing is claimed.
971
+ - `--fit-size-scope file` (default) uses one size for the whole file; `cue`
972
+ writes a per-cue `{\fsN}` override. A size that changes from cue to cue reads
973
+ as a mistake, so it is opt-in — it exists for the one outlier cue that would
974
+ otherwise shrink a ten-minute file. Each cue is laid out at the size it will
975
+ be **drawn** at: a cue drawn larger has a narrower line in em, so wrapping
976
+ everything to the file-wide budget and then drawing some cues large would put
977
+ lines off the side of the frame.
978
+
979
+ The text is **never** touched: this skill does not rewrite, shorten or
980
+ paraphrase a caption to make it fit. Below the floor the cue is split exactly as
981
+ before and `fit_exhausted: true` says so, with `--min-size` named as the flag
982
+ that would go smaller and `|` as the manual break. `--min-size` above `--size`
983
+ is a `kind: input` refusal. `--mode mux` is unaffected: soft subtitles carry no
984
+ size, so the SRT is the one 1.16 wrote.
985
+
986
+ Results, alongside the existing caption stats:
987
+ `fit_size`, `size_requested`, `size_used`, `size_floor`, `size_pct_height`,
988
+ `shrunk`, `fit_scope`, `fit_exhausted`, and `size_source` (`input` or
989
+ `platform-frame`).
990
+
991
+ **"caption text unchanged" (1.17.1).** When the drawn text equals the cues that
992
+ were handed in — nothing transcribed, no cue dropped, no cue *split* across two
993
+ consecutive cues and no glyph stripped by `--emoji none` — the summary says
994
+ `caption text unchanged: the cues were burned exactly as given (line breaks,
995
+ timing and type size only)` and the result carries `text_unchanged: true`. It is
996
+ the honest sentence for a report, made automatic: only the line breaks, the
997
+ timing and the type size ever move, and those do not count as a change. The key
998
+ is burn mode only; `--mode mux` never touches the text and omits it.
999
+
1000
+ Under `--dry-run`/`--plan` on an input that does not exist yet there is no
1001
+ geometry to measure. With `--platform` the destination's own frame is used —
1002
+ that frame *is* what the real run will have, so the planned `FontSize` is the
1003
+ one the run will burn. Without a platform nothing can stand in for the frame
1004
+ and `size_used` is `null`, rather than presenting the requested size as a
1005
+ fitted one.
1006
+
1007
+ | ASS size | px on 1920 | % frame h | em per line |
1008
+ |---|---|---|---|
1009
+ | 24 (default) | 160.0 | 8.3 % | 6.08 |
1010
+ | 19 | 126.7 | 6.6 % | 7.67 |
1011
+ | 16 | 106.7 | 5.6 % | 9.11 |
1012
+ | 13 (floor) | 86.7 | 4.5 % | 11.22 |
1013
+
734
1014
  Text cue format, one per line: `0:00-0:03 Hello`, `00:00:03.500 --> 00:00:06 Two | lines`,
735
1015
  or `00:00:03:15 --> 00:00:06:00 SMPTE non-drop-frame timecode` (`hh:mm:ss:ff`, frame count
736
1016
  converted with `--fps`, or the input video's own fps when `--input` is given and `--fps` is
@@ -73,10 +73,21 @@ from _common.probe import (
73
73
  from _common.decision import (
74
74
  aac_args, add_pad_fill_args, audio_codec_for, AUDIO_CODECS, brand_caption_style, BRAND_DEFAULTS,
75
75
  description_block, _evidence_rank, fmt_chapter_time, propose_chapters,
76
+ filler_spans, FILLER_WORDS, FILLER_AMBIGUOUS, FILLER_DISCOURSE_MARKERS, FILLER_MAX_WORD,
77
+ FILLER_MIN_GAP, FILLER_PAD, normalise_filler_token,
78
+ beat_grid, snap_points, BEAT_MIN_CONFIDENCE, BEAT_ONSET_K, BEAT_OCTAVE_MARGIN,
79
+ BEAT_REFRACTORY_S, BEAT_WINDOW_S, BEAT_SUPPORT_DIVISOR, BEAT_ALIGN_DIVISOR,
80
+ BEAT_Z_FLOOR, BEAT_Z_SPAN,
81
+ _onset_strength, _pick_onsets, _autocorrelation_peak, _grid_score,
76
82
  brand_states_font, cfr_args, concat_list_line, db_to_linear, default_output, encoder_args, escape_filter_path,
77
83
  fmt_secs, fmt_smpte_time, fmt_srt_time, is_audio_output, load_brand, MissingFpsError, pad_filters, parse_time,
78
84
  signed_time_arg, SVT_PRESET, time_arg, video_args, x264_args, _x264_raw
79
85
  )
86
+ from _common.asr import (
87
+ ASR_ENGINES, ASR_INSTALL_HINT, _asr_run, die_no_engine, parse_srt, transcribe, _transcribe_in,
88
+ transcribe_words, _words_from_openai_whisper_json, _words_from_whisper_cpp_json,
89
+ whisper_word_timings, write_srt
90
+ )
80
91
  from _common.color import (
81
92
  bt709_tag_args, color_hex, _COLOR_TOKEN_RE, _sdr_bt709, validate_color
82
93
  )
@@ -96,11 +107,12 @@ from _common.text import (
96
107
  PENALTY_NEUTRAL, PENALTY_OKURIGANA,
97
108
  PENALTY_PARTICLE, PENALTY_SENTENCE_END, _rebalance, _rebalance_phrase, SAFE_WIDTH_FRACTION, _split_hyphens,
98
109
  _particle_ends, _particle_starts, wrap_text, wrap_variants, WRAP_MODES,
110
+ fit_size, line_em_for_size, MIN_CAPTION_FRACTION, ass_units_local,
99
111
  script_font_for_text, script_font_status, _script_font_uncached, _SCRIPT_RANGES, SCRIPTS, _SHAPING_BUILD_CACHE,
100
112
  SHAPING_SCRIPTS, text_width_em, _VS15, _VS16, WINDOWS_FONTS, _ZWJ
101
113
  )
102
114
 
103
- from _common import color, decision, runner, text # noqa: F401,E402
115
+ from _common import asr, color, decision, runner, text # noqa: F401,E402
104
116
 
105
117
  # `_common.emit` and `_common.probe` are the FUNCTIONS, as they have always been -- the
106
118
  # from-imports above rebound the package attribute the submodule import had set. The two modules
@@ -111,7 +123,7 @@ from _common import color, decision, runner, text # noqa: F401,E402
111
123
  _emit_module = sys.modules["_common.emit"]
112
124
  _probe_module = sys.modules["_common.probe"]
113
125
 
114
- _MODULES = (runner, _emit_module, _probe_module, decision, color, text)
126
+ _MODULES = (runner, _emit_module, _probe_module, decision, color, text, asr)
115
127
 
116
128
 
117
129
  class _Facade(_types.ModuleType):
@@ -168,6 +180,12 @@ __all__ = [
168
180
  "char_script", "_check_existing_output", "_check_no_overwrite_input", "_check_output_path", "child_args",
169
181
  "child_limit", "_CHILDREN", "_cleanup_partial_output", "_cmdline", "CODECS", "color_hex", "_COLOR_TOKEN_RE",
170
182
  "concat_list_line", "Context", "_CRF_DEFAULT", "_CURRENT_CTX", "db_to_linear", "decode_pcm_mono", "description_block", "_evidence_rank", "fmt_chapter_time", "propose_chapters",
183
+ "filler_spans", "FILLER_WORDS", "FILLER_AMBIGUOUS", "FILLER_DISCOURSE_MARKERS",
184
+ "FILLER_MAX_WORD", "FILLER_MIN_GAP", "FILLER_PAD", "normalise_filler_token",
185
+ "beat_grid", "snap_points", "BEAT_MIN_CONFIDENCE", "BEAT_ONSET_K", "BEAT_OCTAVE_MARGIN",
186
+ "BEAT_REFRACTORY_S", "BEAT_WINDOW_S", "BEAT_SUPPORT_DIVISOR", "BEAT_ALIGN_DIVISOR",
187
+ "BEAT_Z_FLOOR", "BEAT_Z_SPAN",
188
+ "_onset_strength", "_pick_onsets", "_autocorrelation_peak", "_grid_score",
171
189
  "default_font_file", "default_output", "DEFAULT_TIMEOUT", "detect_script", "die", "drawtext_boxborderw",
172
190
  "_DRAWTEXT_PENDING", "drawtext_shaping", "drawtext_text_opts", "_DRAWTEXT_TMPDIR", "_drawtext_tmpdir",
173
191
  "dry_run_input_pending", "emit", "emoji_asset_for", "EMOJI_ASSET_HINT", "emoji_clusters",
@@ -193,6 +211,9 @@ __all__ = [
193
211
  "time_arg", "_timed_out", "_to_float", "_to_int", "_unwatch", "_V2_HANDLED", "validate_color", "verify_output",
194
212
  "video_args", "_VS15", "_VS16", "_watch", "WINDOWS_FONTS", "write_plan", "x264_args", "X264_PRESETS",
195
213
  "_x264_raw", "_ZWJ",
214
+ "ASR_ENGINES", "ASR_INSTALL_HINT", "_asr_run", "die_no_engine", "parse_srt", "transcribe",
215
+ "_transcribe_in", "transcribe_words", "_words_from_openai_whisper_json",
216
+ "_words_from_whisper_cpp_json", "whisper_word_timings", "write_srt",
196
217
  "_atoms", "best_break", "_bare_word", "break_penalty", "_break_spaced", "_cut_penalty", "_fix_orphans",
197
218
  "_fix_weak_lines", "_function_words", "FUNCTION_WORDS", "_HYPHENS", "_is_hiragana", "_is_ideograph",
198
219
  "_is_kana", "_is_weak_line", "JA_NO_LINE_END", "JA_NO_LINE_START", "JA_PARTICLE_WORDS", "JA_PARTICLES",
@@ -200,5 +221,6 @@ __all__ = [
200
221
  "JA_SENTENCE_END", "_join", "ORPHAN_MIN_EM", "PENALTY_FORBIDDEN", "PENALTY_FUNCTION_WORD",
201
222
  "PENALTY_IDEOGRAPHS", "PENALTY_NEUTRAL", "PENALTY_OKURIGANA", "PENALTY_PARTICLE", "PENALTY_SENTENCE_END",
202
223
  "_rebalance", "_rebalance_phrase", "SAFE_WIDTH_FRACTION", "_split_hyphens", "_particle_ends",
203
- "_particle_starts", "wrap_text", "wrap_variants", "WRAP_MODES"
224
+ "_particle_starts", "wrap_text", "wrap_variants", "WRAP_MODES",
225
+ "fit_size", "line_em_for_size", "MIN_CAPTION_FRACTION", "ass_units_local"
204
226
  ]