ffmpeg-skill 3.1.1 → 3.1.3
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/SKILL.md +163 -170
- package/docs/contract.md +10 -4
- package/package.json +6 -1
- package/references/flags.md +16 -0
- package/references/gotchas.md +29 -2
- package/references/report.md +43 -0
- package/references/requests.md +95 -0
- package/references/timestamps.md +3 -0
- package/references/workflow.md +53 -0
- package/scripts/_common/__init__.py +2 -2
- package/scripts/_common/emit.py +82 -1
- package/scripts/_common/emoji.py +49 -3
- package/scripts/_common/probe.py +4 -0
- package/scripts/_common/runner.py +40 -3
- package/scripts/_contract.py +8 -2
- package/scripts/caption.py +44 -6
- package/scripts/cut.py +13 -2
- package/scripts/graphics.py +9 -3
- package/scripts/grid.py +9 -0
- package/scripts/look.py +10 -0
- package/scripts/scenes.py +10 -1
package/scripts/caption.py
CHANGED
|
@@ -44,7 +44,7 @@ from typing import Any, Dict, List, Optional, Tuple
|
|
|
44
44
|
from _platforms import (PLATFORMS, PLATFORM_CHOICES, ASS_SCRIPT_HEIGHT, ass_units,
|
|
45
45
|
resolve as resolve_platform)
|
|
46
46
|
from _ass_overlay import EMOJI_SENTINEL, emoji_placeholder, ass_escape
|
|
47
|
-
from _common import emoji_filter_chain, EMOJI_ASSET_HINT, emoji_asset_for, emoji_codepoint_name, emoji_support, resolve_emoji_assets, ADVANCE_EM, LATIN_EM, NO_SPACE_SCRIPTS, _char_em, char_script, text_width_em, emoji_clusters, has_emoji, detect_script, BIDI_SCRIPTS, STATE, brand_states_font, script_font_for_text, signed_time_arg, brand_caption_style, color_hex, drawn_chain, label_chain, load_brand, rgb_chain_to_bt709, video_args, add_common, apply_common, emit, aac_args, cfr_args, default_output, die, escape_filter_path, ffmpeg_base, fmt_srt_time, fmt_smpte_time, info, MissingFpsError, parse_time, probe, run, x264_args, X264_PRESETS, read_text_or_die, fmt_secs, refuse_existing_outputs
|
|
47
|
+
from _common import require_filter, emoji_filter_chain, EMOJI_ASSET_HINT, emoji_asset_for, emoji_codepoint_name, emoji_support, resolve_emoji_assets, ADVANCE_EM, LATIN_EM, NO_SPACE_SCRIPTS, _char_em, char_script, text_width_em, emoji_clusters, has_emoji, detect_script, BIDI_SCRIPTS, STATE, brand_states_font, script_font_for_text, signed_time_arg, brand_caption_style, color_hex, drawn_chain, label_chain, load_brand, rgb_chain_to_bt709, video_args, add_common, apply_common, emit, aac_args, cfr_args, default_output, die, escape_filter_path, ffmpeg_base, fmt_srt_time, fmt_smpte_time, info, MissingFpsError, parse_time, probe, run, x264_args, X264_PRESETS, read_text_or_die, fmt_secs, refuse_existing_outputs
|
|
48
48
|
# The line breaker, lifted into _common/text.py in 1.16.0 so graphics.py can use the same rules.
|
|
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
|
|
@@ -78,10 +78,27 @@ TIME_RE = re.compile(
|
|
|
78
78
|
)
|
|
79
79
|
|
|
80
80
|
|
|
81
|
+
# an SRT timing line: the times alone, the cue text on the lines after it
|
|
82
|
+
DROP_FRAME_RE = re.compile(r"^\s*\d+:\d{2}:\d{2};\d+")
|
|
83
|
+
SRT_TIMING_RE = re.compile(r"^\s*\d+:\d{2}:\d{2}[,.]\d+\s*-->\s*\d+:\d{2}:\d{2}[,.]\d+\s*$")
|
|
84
|
+
|
|
85
|
+
|
|
81
86
|
def parse_text_cues(path: str, auto_seconds: float, gap: float, fps: Optional[float] = None) -> List[Tuple[float, float, str]]:
|
|
82
87
|
cues: List[Tuple[float, float, str]] = []
|
|
83
88
|
cursor = 0.0
|
|
84
|
-
|
|
89
|
+
lines = read_text_or_die(path, "--text").lstrip("\ufeff").splitlines(True)
|
|
90
|
+
for n, raw in enumerate(lines, 1):
|
|
91
|
+
if SRT_TIMING_RE.match(raw.rstrip("\r\n")):
|
|
92
|
+
# a cue file puts the text on the line with its times; this line has the times alone, so
|
|
93
|
+
# the file is an SRT. Read as a cue file, its numbers and timing lines would be burned in
|
|
94
|
+
# as captions, auto-timed 3 s apart (#396)
|
|
95
|
+
die(f"--text: {path} is an SRT file (line {n} is a timing line with no text: "
|
|
96
|
+
f"'{raw.strip()}'); pass it as --srt (render.py: --srt, not --cues)", kind="input")
|
|
97
|
+
if DROP_FRAME_RE.match(raw):
|
|
98
|
+
# TIME_RE has no ';', so the line would be burned as caption text, auto-timed
|
|
99
|
+
die(f"--text: line {n} has a drop-frame timecode (';'), which is not read: write it as "
|
|
100
|
+
"hh:mm:ss:ff@29.97 or in seconds", kind="input")
|
|
101
|
+
for raw in lines:
|
|
85
102
|
line = raw.rstrip("\n")
|
|
86
103
|
if not line.strip():
|
|
87
104
|
continue
|
|
@@ -200,7 +217,7 @@ def plan_emoji(cues, args, play_w, play_h, brand=None):
|
|
|
200
217
|
return cues, None
|
|
201
218
|
assets = resolve_emoji_assets(getattr(args, "emoji_assets", None), None, brand)
|
|
202
219
|
want = getattr(args, "emoji", "auto")
|
|
203
|
-
support = emoji_support(assets, probe=True)
|
|
220
|
+
support = emoji_support(assets, probe=True, fonts_dir=getattr(args, "fonts_dir", None))
|
|
204
221
|
mode = support["mode"] if want == "auto" else want
|
|
205
222
|
if want == "color" and not support["libass_color"]:
|
|
206
223
|
die("--emoji color: this ffmpeg renders emoji monochrome through libass "
|
|
@@ -208,6 +225,13 @@ def plan_emoji(cues, args, play_w, play_h, brand=None):
|
|
|
208
225
|
kind="input")
|
|
209
226
|
if want == "png" and not assets:
|
|
210
227
|
die("--emoji png: no emoji assets directory resolved -- " + EMOJI_ASSET_HINT, kind="input")
|
|
228
|
+
# --emoji mono asks for the font's glyph, and there is none: empty boxes (tofu) are a failed job
|
|
229
|
+
# (#395). An auto run takes mode "none" instead, as on a machine with no emoji face at all,
|
|
230
|
+
# and says why below; --emoji mono never uses assets, so they do not save it
|
|
231
|
+
if want == "mono" and support.get("libass_glyph") is False:
|
|
232
|
+
die(f"--emoji mono: this ffmpeg draws no emoji glyph through libass ({support['detail']}), so "
|
|
233
|
+
f"{len(clusters_all)} emoji would come out as empty boxes -- pass --emoji-assets DIR for colour, "
|
|
234
|
+
"or --emoji none to leave them out of the drawn text", kind="input")
|
|
211
235
|
plan = {"mode": mode, "count": len(clusters_all),
|
|
212
236
|
"clusters": sorted({emoji_codepoint_name(cl) for cl in clusters_all}),
|
|
213
237
|
"assets": assets, "missing": [], "overlays": []}
|
|
@@ -217,7 +241,14 @@ def plan_emoji(cues, args, play_w, play_h, brand=None):
|
|
|
217
241
|
for cl in {cl for _i, cl in emoji_clusters(text)}:
|
|
218
242
|
text = text.replace(cl, "")
|
|
219
243
|
out.append((start, end, re.sub(r"[ \t]{2,}", " ", text).strip()))
|
|
220
|
-
|
|
244
|
+
if want == "auto":
|
|
245
|
+
why = ("libass draws no emoji glyph on this build (an empty box at best)"
|
|
246
|
+
if support.get("libass_glyph") is False else "no face here draws them")
|
|
247
|
+
msg = (f"emoji: {len(clusters_all)} left out of the drawn text: {why} -- pass --emoji-assets DIR "
|
|
248
|
+
"to draw them as PNGs")
|
|
249
|
+
warn(msg, "emoji_not_drawn", count=len(clusters_all), reason=support.get("detail"))
|
|
250
|
+
else:
|
|
251
|
+
info("emoji: stripped from the drawn text (--emoji none)")
|
|
221
252
|
return out, plan
|
|
222
253
|
if mode in ("color", "mono"):
|
|
223
254
|
if mode == "mono":
|
|
@@ -1097,6 +1128,10 @@ def main() -> int:
|
|
|
1097
1128
|
die(f"--audio-stream {args.audio_stream}: input has {len(audio_streams)} audio stream(s), 0..{len(audio_streams) - 1}")
|
|
1098
1129
|
if args.audio_stream and not audio_streams:
|
|
1099
1130
|
die("--audio-stream needs an input with audio streams")
|
|
1131
|
+
if args.mode != "mux":
|
|
1132
|
+
# before any transcription or sidecar: a build without libass fails these at parse time
|
|
1133
|
+
# with a message that names no filter (#393), so ask the build first, dry run included
|
|
1134
|
+
require_filter("ass" if (args.ass or args.animate != "none" or args.karaoke) else "subtitles", "burning captions")
|
|
1100
1135
|
fps_for_tc = args.fps
|
|
1101
1136
|
if fps_for_tc is None and meta is not None:
|
|
1102
1137
|
fps_for_tc = meta.get("video", {}).get("fps")
|
|
@@ -1586,8 +1621,11 @@ def main() -> int:
|
|
|
1586
1621
|
side_notes.append(msg)
|
|
1587
1622
|
caption_stats["cues_burned"] = burned
|
|
1588
1623
|
caption_stats["cues_outside"] = outside
|
|
1624
|
+
require_filter("ass" if args.ass else "subtitles", "burning captions")
|
|
1625
|
+
# the path as a named option: with a positional first value a build without the filter fails at
|
|
1626
|
+
# parse time ("No option name near ...") instead of saying "No such filter" (#393)
|
|
1589
1627
|
if args.ass:
|
|
1590
|
-
vf = f"ass={escape_filter_path(args.ass)}"
|
|
1628
|
+
vf = f"ass=filename={escape_filter_path(args.ass)}"
|
|
1591
1629
|
if args.fonts_dir:
|
|
1592
1630
|
vf += f":fontsdir={escape_filter_path(args.fonts_dir)}"
|
|
1593
1631
|
else:
|
|
@@ -1608,7 +1646,7 @@ def main() -> int:
|
|
|
1608
1646
|
f"MarginV={args.margin}",
|
|
1609
1647
|
]
|
|
1610
1648
|
force = ",".join(style).replace("\\", "\\\\").replace("'", "\\'")
|
|
1611
|
-
vf = f"subtitles={escape_filter_path(srt_path)}:force_style='{force}'"
|
|
1649
|
+
vf = f"subtitles=filename={escape_filter_path(srt_path)}:force_style='{force}'"
|
|
1612
1650
|
if args.fonts_dir:
|
|
1613
1651
|
vf += f":fontsdir={escape_filter_path(args.fonts_dir)}"
|
|
1614
1652
|
if generated_ass or not args.ass:
|
package/scripts/cut.py
CHANGED
|
@@ -61,7 +61,7 @@ from typing import List, Tuple
|
|
|
61
61
|
from _common import (beat_grid, snap_points, decode_pcm_mono, rms_envelope, BEAT_MIN_CONFIDENCE)
|
|
62
62
|
from _common import require_tool
|
|
63
63
|
from _common.probe import TS_EXTS, signatures_match, stream_signature
|
|
64
|
-
from _common import sibling_temp, source_codec_video_args, STATE, add_common, apply_common, audio_codec_for, emit, aac_args, cfr_args, default_output, die, ffmpeg_base, info, is_audio_output, time_arg, probe, run, X264_PRESETS, keyframes_near, MissingFpsError, concat_list_line, place_output, refuse_output_is_input, fmt_secs, measure_frame_timing, dry_run_input_pending
|
|
64
|
+
from _common import warn, sibling_temp, source_codec_video_args, STATE, add_common, apply_common, audio_codec_for, emit, aac_args, cfr_args, default_output, die, ffmpeg_base, info, is_audio_output, time_arg, probe, run, X264_PRESETS, keyframes_near, MissingFpsError, concat_list_line, place_output, refuse_output_is_input, fmt_secs, measure_frame_timing, dry_run_input_pending
|
|
65
65
|
|
|
66
66
|
# outputs whose re-encode dropped a subtitle/data stream (reported as dropped_non_av_streams)
|
|
67
67
|
DROPPED_STREAMS: List[str] = []
|
|
@@ -1706,9 +1706,20 @@ def main() -> int:
|
|
|
1706
1706
|
# selection writes one of each, so that is the count; the source's own picture/sound gap is
|
|
1707
1707
|
# the reference for the lengths check
|
|
1708
1708
|
expect_streams = {"video": int(bool(meta.get("video")) and not is_audio_output(output)), "audio": int(bool(meta.get("audio")))}
|
|
1709
|
+
# cut writes one audio track (ffmpeg's default selection, on a stream copy as on a re-encode):
|
|
1710
|
+
# a source with more must say which ones did not come along (#397), as export.py's non_av_streams does
|
|
1711
|
+
src_audio = meta.get("audio_streams") or []
|
|
1712
|
+
out_audio = (result.get("audio_streams") or []) if not STATE.dry_run else src_audio[:1]
|
|
1713
|
+
dropped_audio = len(src_audio) - len(out_audio) if src_audio else 0
|
|
1714
|
+
if dropped_audio > 0:
|
|
1715
|
+
kept = len(out_audio)
|
|
1716
|
+
msg = (f"the source has {len(src_audio)} audio tracks; this cut kept {kept} and left {dropped_audio} out "
|
|
1717
|
+
f"(cut writes one audio track, on a stream copy as on a re-encode)")
|
|
1718
|
+
warn(msg, "audio_tracks_dropped", source=len(src_audio), kept=kept, dropped=dropped_audio)
|
|
1719
|
+
notes.append(msg)
|
|
1709
1720
|
emit(output, expected_duration=round(expected, 6), duration_error_ms=error_ms, precision=precision, reencoded=reencoded,
|
|
1710
1721
|
expect_streams=expect_streams, stream_reference=[meta], stream_windows=[(s, e) for s, e in requested],
|
|
1711
|
-
dropped_non_av_streams=bool(DROPPED_STREAMS),
|
|
1722
|
+
dropped_non_av_streams=bool(DROPPED_STREAMS), dropped_audio_streams=max(dropped_audio, 0),
|
|
1712
1723
|
requested_start=round(requested[0][0], 6) if len(requested) == 1 else None,
|
|
1713
1724
|
requested_end=round(requested[0][1], 6) if len(requested) == 1 else None,
|
|
1714
1725
|
requested_segments=[[round(s, 6), round(e, 6)] for s, e in requested] if len(requested) > 1 else None,
|
package/scripts/graphics.py
CHANGED
|
@@ -32,7 +32,7 @@ import sys
|
|
|
32
32
|
from typing import List, Optional, Tuple
|
|
33
33
|
|
|
34
34
|
from _platforms import PLATFORMS, PLATFORM_CHOICES, safe_margins_px, resolve as resolve_platform
|
|
35
|
-
from _common import (aac_args, add_common, brand_caption_style, script_font_for_text, apply_common, cfr_args,
|
|
35
|
+
from _common import (require_filter, aac_args, add_common, brand_caption_style, script_font_for_text, apply_common, cfr_args,
|
|
36
36
|
color_hex, default_output, die, draws_chroma, drawn_chain, emit, escape_drawtext, escape_filter_path,
|
|
37
37
|
ffmpeg_base, info, label_chain, load_brand, parse_time, probe, rgb_chain_to_bt709, run, run_keeping_subtitles, video_args,
|
|
38
38
|
drawtext_boxborderw, drawtext_center_align, X264_PRESETS, time_arg, fmt_secs, STATE, drawtext_text_opts,
|
|
@@ -237,13 +237,18 @@ def main(force_route: Optional[str] = None) -> int:
|
|
|
237
237
|
emoji_mode = None
|
|
238
238
|
emoji_overlays: List[dict] = []
|
|
239
239
|
if has_emoji(all_text):
|
|
240
|
-
|
|
240
|
+
_ff = args.font_file or brand.get("font_file")
|
|
241
|
+
support = emoji_support(emoji_assets, probe=True, fonts_dir=os.path.dirname(os.path.abspath(_ff)) if _ff else None)
|
|
241
242
|
emoji_mode = support["mode"] if args.emoji == "auto" else args.emoji
|
|
242
243
|
if args.emoji == "color" and not support["libass_color"]:
|
|
243
244
|
die("--emoji color: this ffmpeg renders emoji monochrome through libass "
|
|
244
245
|
f"({support['detail']}) -- pass --emoji-assets DIR for colour, or --emoji mono", kind="input")
|
|
245
246
|
if args.emoji == "png" and not emoji_assets:
|
|
246
247
|
die("--emoji png: no emoji assets directory resolved -- " + EMOJI_ASSET_HINT, kind="input")
|
|
248
|
+
if args.emoji == "mono" and support.get("libass_glyph") is False:
|
|
249
|
+
die("--emoji mono: this ffmpeg draws no emoji glyph through libass "
|
|
250
|
+
f"({support['detail']}), so they would come out as empty boxes -- pass --emoji-assets DIR "
|
|
251
|
+
"for colour, or --emoji none", kind="input")
|
|
247
252
|
if args.text_render == "drawtext" and shaping:
|
|
248
253
|
die(f"{LANGUAGE_NAMES.get(_script, _script)} text cannot be shaped by drawtext on any ffmpeg "
|
|
249
254
|
"build (the marks are reordered by harfbuzz, which drawtext does not use): drop "
|
|
@@ -642,6 +647,7 @@ def main(force_route: Optional[str] = None) -> int:
|
|
|
642
647
|
"assets": emoji_assets, "missing": missing,
|
|
643
648
|
"overlays": len(emoji_overlays)}
|
|
644
649
|
ass_path = args.write_ass or os.path.splitext(output)[0] + "_gfx.ass"
|
|
650
|
+
require_filter("ass", "drawing this text through libass") # before the sidecar is written
|
|
645
651
|
if STATE.dry_run:
|
|
646
652
|
info(f"[dry-run] would write {ass_path} ({len(elements)} text elements)")
|
|
647
653
|
else:
|
|
@@ -682,7 +688,7 @@ def main(force_route: Optional[str] = None) -> int:
|
|
|
682
688
|
chains = list(fc) if fc else [f"[0:v]{text or 'null'}[vout]"]
|
|
683
689
|
last = "vout"
|
|
684
690
|
if route == "ass":
|
|
685
|
-
vf = f"ass={escape_filter_path(ass_path)}"
|
|
691
|
+
vf = f"ass=filename={escape_filter_path(ass_path)}"
|
|
686
692
|
fdir = ass_fonts_dir()
|
|
687
693
|
if fdir:
|
|
688
694
|
vf += f":fontsdir={escape_filter_path(fdir)}"
|
package/scripts/grid.py
CHANGED
|
@@ -29,6 +29,8 @@ from _common import add_common, apply_common, aac_args, cfr_args, default_output
|
|
|
29
29
|
draws_chroma, drawn_chain, escape_drawtext, ffmpeg_base, info, label_chain, matrix_chain, probe, run, validate_color, video_args
|
|
30
30
|
from _common.fonts import font_choice, record_font_choice
|
|
31
31
|
from _common import drawtext_fontfile # noqa: E402
|
|
32
|
+
from _common import ffmpeg_filters, filter_missing_hint # noqa: E402
|
|
33
|
+
from _common.emit import note, record_fallback # noqa: E402
|
|
32
34
|
|
|
33
35
|
LABEL_MARGIN = 10
|
|
34
36
|
|
|
@@ -56,6 +58,13 @@ def main() -> int:
|
|
|
56
58
|
add_common(ap)
|
|
57
59
|
args = ap.parse_args()
|
|
58
60
|
apply_common(args)
|
|
61
|
+
if args.label == "auto" and ffmpeg_filters() and "drawtext" not in ffmpeg_filters():
|
|
62
|
+
# the labels are the only drawtext in the grid: without the filter (Homebrew's plain
|
|
63
|
+
# ffmpeg) draw the grid unlabelled and say so, as look.py does for its stamps (#394)
|
|
64
|
+
args.label = "none"
|
|
65
|
+
record_fallback("this ffmpeg build has no drawtext filter, so the cells are not labelled "
|
|
66
|
+
"(the same as --label none)", "grid_no_drawtext")
|
|
67
|
+
note("no cell labels: this ffmpeg build has no drawtext filter (" + filter_missing_hint("drawtext") + ")")
|
|
59
68
|
|
|
60
69
|
n = args.cols * args.rows
|
|
61
70
|
if args.cols <= 0 or args.rows <= 0:
|
package/scripts/look.py
CHANGED
|
@@ -19,6 +19,8 @@ from typing import List, Optional
|
|
|
19
19
|
from _platforms import PLATFORMS, PLATFORM_CHOICES, resolve as resolve_platform
|
|
20
20
|
from _common import STATE, require_tool, add_common, apply_common, default_font_file, die, emit, escape_drawtext, ffmpeg_base, info, parse_time, probe, run, run_analysis, time_arg
|
|
21
21
|
from _common import drawtext_fontfile # noqa: E402
|
|
22
|
+
from _common import ffmpeg_filters, filter_missing_hint # noqa: E402
|
|
23
|
+
from _common.emit import note, record_fallback # noqa: E402
|
|
22
24
|
|
|
23
25
|
FONT = "fontcolor=white:fontsize=h/18:box=1:boxcolor=black@0.55:boxborderw=6:x=8:y=8"
|
|
24
26
|
|
|
@@ -162,6 +164,14 @@ def main() -> int:
|
|
|
162
164
|
# a resolvable font file, given as fontfile=, is the only form confirmed not to crash drawtext's
|
|
163
165
|
# own fontconfig resolution on some real Windows ffmpeg builds (#100); font= is the fallback when
|
|
164
166
|
# nothing can be resolved, unchanged from before this existed.
|
|
167
|
+
if not args.no_timecode and ffmpeg_filters() and "drawtext" not in ffmpeg_filters():
|
|
168
|
+
# a build without drawtext (Homebrew's plain ffmpeg) cannot stamp the timecodes, but the
|
|
169
|
+
# sheet itself only needs tile: draw it without them and say so, rather than failing the
|
|
170
|
+
# step every picture change ends with (#394)
|
|
171
|
+
args.no_timecode = True
|
|
172
|
+
record_fallback("this ffmpeg build has no drawtext filter, so the sheet has no timecode stamps "
|
|
173
|
+
"(the same as --no-timecode)", "look_no_drawtext")
|
|
174
|
+
note("no timecode stamps: this ffmpeg build has no drawtext filter (" + filter_missing_hint("drawtext") + ")")
|
|
165
175
|
if args.no_timecode:
|
|
166
176
|
font_prefix, tc = "", "" # nothing is drawn, so no font is resolved or copied
|
|
167
177
|
else:
|
package/scripts/scenes.py
CHANGED
|
@@ -27,6 +27,8 @@ from _common import (guard_sidecar, detect_scenes, STATE, add_common, apply_comm
|
|
|
27
27
|
ffmpeg_base, info, print_json, probe, run, decode_pcm_mono,
|
|
28
28
|
rms_envelope, BEAT_MIN_CONFIDENCE, decode_gray_frames, frame_flow, label_shot_flow)
|
|
29
29
|
from _common import drawtext_fontfile # noqa: E402
|
|
30
|
+
from _common import ffmpeg_filters, filter_missing_hint # noqa: E402
|
|
31
|
+
from _common.emit import note, record_fallback # noqa: E402
|
|
30
32
|
|
|
31
33
|
SHOT_FPS = 4.0 # frames/second sampled for --shots' flow estimate
|
|
32
34
|
SHOT_W, SHOT_H = 48, 27 # decode size for --shots (16:9-ish; enough blocks, still a few KB/shot)
|
|
@@ -287,6 +289,13 @@ def main() -> int:
|
|
|
287
289
|
fps = meta["video"].get("fps") or 30.0
|
|
288
290
|
expr = "+".join(f"eq(n\\,{int(round(sc['start'] * fps))})" for sc in scenes)
|
|
289
291
|
stamp = ""
|
|
292
|
+
if not args.no_timecode and ffmpeg_filters() and "drawtext" not in ffmpeg_filters():
|
|
293
|
+
# as look.py (#394): the sheet needs drawtext only for its stamps
|
|
294
|
+
args.no_timecode = True
|
|
295
|
+
record_fallback("this ffmpeg build has no drawtext filter, so the --sheet has no timecode stamps "
|
|
296
|
+
"(the same as --no-timecode)", "scenes_sheet_no_drawtext")
|
|
297
|
+
note("--sheet without timecode stamps: this ffmpeg build has no drawtext filter ("
|
|
298
|
+
+ filter_missing_hint("drawtext") + ")")
|
|
290
299
|
if not args.no_timecode:
|
|
291
300
|
default_font = default_font_file("DejaVu Sans")
|
|
292
301
|
font_prefix = f"{drawtext_fontfile(default_font)}:" if default_font else ""
|
|
@@ -297,7 +306,7 @@ def main() -> int:
|
|
|
297
306
|
info(f"wrote {args.sheet}")
|
|
298
307
|
result["sheet"] = args.sheet
|
|
299
308
|
|
|
300
|
-
if
|
|
309
|
+
if STATE.json: # --json-brief too: emit() is what adds the diagnostics (scenes_sheet_no_drawtext)
|
|
301
310
|
emit(None, **result)
|
|
302
311
|
else:
|
|
303
312
|
print_json(result)
|