ffmpeg-skill 1.13.0 → 1.14.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/scripts/report.py CHANGED
@@ -7,6 +7,7 @@ Examples:
7
7
  python3 report.py --before raw.mov --after final.mp4 -o report.html
8
8
  python3 report.py --after final.mp4 --platform reels --title "Episode 12 — Reels cut" -o report.html
9
9
  python3 report.py --before raw.mov --after final.mp4 --commands commands.txt --notes notes.md
10
+ python3 report.py --pack talk_pack.md -o pack.html # the social pack table as HTML
10
11
  """
11
12
  import argparse
12
13
  import base64
@@ -79,9 +80,76 @@ def media_rows(meta: Dict[str, Any], ld: Dict[str, Any]) -> List[List[str]]:
79
80
  return rows
80
81
 
81
82
 
83
+ PACK_CSS = """
84
+ :root{--bg:#F4F6F8;--paper:#fff;--ink:#161B21;--ink2:#4B5661;--line:#D8DEE4;--ok:#2C8A5B;--bad:#B4362F}
85
+ @media (prefers-color-scheme:dark){:root{--bg:#111518;--paper:#191E23;--ink:#E8ECEF;--ink2:#AEB6BE;--line:#2A3138;--ok:#5CC38C;--bad:#F07A73}}
86
+ body{margin:0;background:var(--bg);color:var(--ink);font:15px/1.6 system-ui,-apple-system,"Segoe UI",Roboto,"Noto Sans JP",sans-serif}
87
+ .wrap{max-width:900px;margin:0 auto;padding:32px 20px 60px}
88
+ h1{font-size:24px;margin:0 0 16px}
89
+ table{border-collapse:collapse;width:100%;font-size:14px;background:var(--paper);border:1px solid var(--line);border-radius:6px;overflow:hidden}
90
+ th,td{text-align:left;padding:7px 10px;border-bottom:1px solid var(--line)}th{color:var(--ink2);font-weight:500}
91
+ td.pass{color:var(--ok);font-weight:600}td.bad{color:var(--bad);font-weight:600}
92
+ p.note{color:var(--ink2);font-size:13px}
93
+ .foot{color:var(--ink2);font-size:12px;margin-top:30px;border-top:1px solid var(--line);padding-top:10px}
94
+ """
95
+
96
+
97
+ def pack_report(args) -> int:
98
+ """render.py --template all writes <stem>_pack.md: one row per destination. This renders the
99
+ same rows as HTML, so a pack can be handed over the way a single delivery report is -- no
100
+ re-measurement, because the pack table is what the renders actually produced."""
101
+ text = read_text_or_die(args.pack, "--pack")
102
+ rows: List[List[str]] = []
103
+ header: List[str] = []
104
+ for line in text.splitlines():
105
+ line = line.strip()
106
+ if not line.startswith("|"):
107
+ continue
108
+ cells = [c.strip() for c in line.strip("|").split("|")]
109
+ if all(set(c) <= set("-: ") for c in cells):
110
+ continue
111
+ if not header:
112
+ header = cells
113
+ else:
114
+ rows.append(cells)
115
+ if not rows:
116
+ die(f"{args.pack}: no pack table found (expected the Markdown table render.py --template all writes)")
117
+ title = args.title or f"Social pack — {Path(args.pack).stem.replace('_pack', '')}"
118
+ output = args.output or str(Path(args.pack).with_suffix(".html"))
119
+ head = "".join(f"<th>{html.escape(h)}</th>" for h in header)
120
+ body = ""
121
+ for r in rows:
122
+ cells = ""
123
+ for i, c in enumerate(r):
124
+ cls = ""
125
+ if header[i:i + 1] == ["check"]:
126
+ cls = " class='pass'" if c.lower() == "pass" else " class='bad'"
127
+ cells += f"<td{cls}>{html.escape(c)}</td>"
128
+ body += f"<tr>{cells}</tr>"
129
+ doc = ("<!doctype html><html><head><meta charset='utf-8'><meta name='viewport' content='width=device-width,initial-scale=1'>"
130
+ f"<title>{html.escape(title)}</title><style>{PACK_CSS}</style></head><body><div class='wrap'>"
131
+ f"<h1>{html.escape(title)}</h1><table><tr>{head}</tr>{body}</table>"
132
+ f"<p class='note'>{len(rows)} destination(s) from one edit.</p>"
133
+ "<p class='foot'>Generated by ffmpeg-skill · local FFmpeg, no cloud.</p></div></body></html>")
134
+ if STATE.dry_run:
135
+ info(f"wrote {output}")
136
+ else:
137
+ try:
138
+ Path(output).write_text(doc, encoding="utf-8")
139
+ except OSError as e:
140
+ die(f"cannot write {output}: {e}", kind="output")
141
+ info(f"wrote {output} ({os.path.getsize(output) / 1024:.0f} KB)")
142
+ emit(None, report=output, pack=[dict(zip(header, r)) for r in rows], check=None,
143
+ verification=([{"step": "exists", "ok": True}] if not STATE.dry_run else []))
144
+ if not args.json:
145
+ print(output)
146
+ return 0
147
+
148
+
82
149
  def main() -> int:
83
150
  ap = argparse.ArgumentParser(description=__doc__, formatter_class=argparse.RawDescriptionHelpFormatter)
84
- ap.add_argument("--after", required=True, help="the deliverable")
151
+ ap.add_argument("--after", help="the deliverable (required unless --pack is given)")
152
+ ap.add_argument("--pack", metavar="FILE", help="a social pack table written by render.py --template all (<stem>_pack.md): render it as HTML")
85
153
  ap.add_argument("--before", help="the source (optional)")
86
154
  ap.add_argument("-o", "--output", help="report path (default: <after>_report.html)")
87
155
  ap.add_argument("--title", help="report title")
@@ -93,6 +161,10 @@ def main() -> int:
93
161
  args = ap.parse_args()
94
162
  apply_common(args)
95
163
 
164
+ if args.pack:
165
+ return pack_report(args)
166
+ if not args.after:
167
+ die("--after is required (or --pack FILE for a social pack table)")
96
168
  after = probe(args.after)
97
169
  before = probe(args.before) if args.before else None
98
170
  ld_after = loudness(args.after) if after.get("audio") else {}
@@ -0,0 +1,47 @@
1
+ {
2
+ "template": "facebook",
3
+ "output": "$OUTPUT",
4
+ "clips": [
5
+ {
6
+ "src": "$INPUT"
7
+ }
8
+ ],
9
+ "frame": {
10
+ "aspect": "16:9",
11
+ "fit": "pad"
12
+ },
13
+ "brand": "$BRAND",
14
+ "captions": {
15
+ "text": "$CUES",
16
+ "animate": "none",
17
+ "karaoke": false,
18
+ "size": 20,
19
+ "position": "bottom",
20
+ "margin": 14
21
+ },
22
+ "graphics": [
23
+ {
24
+ "template": "lower-third",
25
+ "name": "$TITLE",
26
+ "start": 1,
27
+ "end": 6
28
+ }
29
+ ],
30
+ "overlays": [
31
+ {
32
+ "image": "$LOGO",
33
+ "position": "top-left"
34
+ }
35
+ ],
36
+ "loudness": {
37
+ "lufs": -14,
38
+ "tp": -1.0
39
+ },
40
+ "export": {
41
+ "preset": "facebook",
42
+ "normalize": true
43
+ },
44
+ "check": {
45
+ "platform": "facebook"
46
+ }
47
+ }
@@ -0,0 +1,47 @@
1
+ {
2
+ "template": "linkedin",
3
+ "output": "$OUTPUT",
4
+ "clips": [
5
+ {
6
+ "src": "$INPUT"
7
+ }
8
+ ],
9
+ "frame": {
10
+ "aspect": "1:1",
11
+ "fit": "crop"
12
+ },
13
+ "brand": "$BRAND",
14
+ "captions": {
15
+ "text": "$CUES",
16
+ "animate": "none",
17
+ "karaoke": false,
18
+ "size": 20,
19
+ "position": "bottom",
20
+ "margin": 14
21
+ },
22
+ "graphics": [
23
+ {
24
+ "template": "lower-third",
25
+ "name": "$TITLE",
26
+ "start": 1,
27
+ "end": 6
28
+ }
29
+ ],
30
+ "overlays": [
31
+ {
32
+ "image": "$LOGO",
33
+ "position": "top-left"
34
+ }
35
+ ],
36
+ "loudness": {
37
+ "lufs": -14,
38
+ "tp": -1.0
39
+ },
40
+ "export": {
41
+ "preset": "linkedin",
42
+ "normalize": true
43
+ },
44
+ "check": {
45
+ "platform": "linkedin"
46
+ }
47
+ }
@@ -0,0 +1,22 @@
1
+ {
2
+ "template": "podcast",
3
+ "output": "$OUTPUT",
4
+ "clips": [
5
+ {
6
+ "src": "$INPUT"
7
+ }
8
+ ],
9
+ "silence": {
10
+ "threshold": -40,
11
+ "min_silence": 1.0,
12
+ "margin": 0.15
13
+ },
14
+ "loudness": {
15
+ "lufs": -16,
16
+ "tp": -1
17
+ },
18
+ "chapters": "$CHAPTERS",
19
+ "check": {
20
+ "platform": "podcast"
21
+ }
22
+ }
@@ -0,0 +1,47 @@
1
+ {
2
+ "template": "reels",
3
+ "output": "$OUTPUT",
4
+ "clips": [
5
+ {
6
+ "src": "$INPUT"
7
+ }
8
+ ],
9
+ "frame": {
10
+ "aspect": "9:16",
11
+ "fit": "crop"
12
+ },
13
+ "brand": "$BRAND",
14
+ "captions": {
15
+ "text": "$CUES",
16
+ "animate": "pop",
17
+ "karaoke": true,
18
+ "size": 24,
19
+ "position": "bottom",
20
+ "margin": 58
21
+ },
22
+ "graphics": [
23
+ {
24
+ "template": "hook",
25
+ "title": "$TITLE",
26
+ "start": 0,
27
+ "end": 3
28
+ }
29
+ ],
30
+ "overlays": [
31
+ {
32
+ "image": "$LOGO",
33
+ "position": "top-left"
34
+ }
35
+ ],
36
+ "loudness": {
37
+ "lufs": -14,
38
+ "tp": -1.0
39
+ },
40
+ "export": {
41
+ "preset": "reels",
42
+ "normalize": true
43
+ },
44
+ "check": {
45
+ "platform": "reels"
46
+ }
47
+ }
@@ -0,0 +1,47 @@
1
+ {
2
+ "template": "shorts",
3
+ "output": "$OUTPUT",
4
+ "clips": [
5
+ {
6
+ "src": "$INPUT"
7
+ }
8
+ ],
9
+ "frame": {
10
+ "aspect": "9:16",
11
+ "fit": "crop"
12
+ },
13
+ "brand": "$BRAND",
14
+ "captions": {
15
+ "text": "$CUES",
16
+ "animate": "pop",
17
+ "karaoke": true,
18
+ "size": 24,
19
+ "position": "bottom",
20
+ "margin": 52
21
+ },
22
+ "graphics": [
23
+ {
24
+ "template": "hook",
25
+ "title": "$TITLE",
26
+ "start": 0,
27
+ "end": 3
28
+ }
29
+ ],
30
+ "overlays": [
31
+ {
32
+ "image": "$LOGO",
33
+ "position": "top-left"
34
+ }
35
+ ],
36
+ "loudness": {
37
+ "lufs": -14,
38
+ "tp": -1.0
39
+ },
40
+ "export": {
41
+ "preset": "shorts",
42
+ "normalize": true
43
+ },
44
+ "check": {
45
+ "platform": "shorts"
46
+ }
47
+ }
@@ -0,0 +1,47 @@
1
+ {
2
+ "template": "tiktok",
3
+ "output": "$OUTPUT",
4
+ "clips": [
5
+ {
6
+ "src": "$INPUT"
7
+ }
8
+ ],
9
+ "frame": {
10
+ "aspect": "9:16",
11
+ "fit": "crop"
12
+ },
13
+ "brand": "$BRAND",
14
+ "captions": {
15
+ "text": "$CUES",
16
+ "animate": "pop",
17
+ "karaoke": true,
18
+ "size": 24,
19
+ "position": "bottom",
20
+ "margin": 63
21
+ },
22
+ "graphics": [
23
+ {
24
+ "template": "hook",
25
+ "title": "$TITLE",
26
+ "start": 0,
27
+ "end": 3
28
+ }
29
+ ],
30
+ "overlays": [
31
+ {
32
+ "image": "$LOGO",
33
+ "position": "top-left"
34
+ }
35
+ ],
36
+ "loudness": {
37
+ "lufs": -14,
38
+ "tp": -1.0
39
+ },
40
+ "export": {
41
+ "preset": "tiktok",
42
+ "normalize": true
43
+ },
44
+ "check": {
45
+ "platform": "tiktok"
46
+ }
47
+ }
@@ -0,0 +1,47 @@
1
+ {
2
+ "template": "x",
3
+ "output": "$OUTPUT",
4
+ "clips": [
5
+ {
6
+ "src": "$INPUT"
7
+ }
8
+ ],
9
+ "frame": {
10
+ "aspect": "16:9",
11
+ "fit": "pad"
12
+ },
13
+ "brand": "$BRAND",
14
+ "captions": {
15
+ "text": "$CUES",
16
+ "animate": "none",
17
+ "karaoke": false,
18
+ "size": 20,
19
+ "position": "bottom",
20
+ "margin": 14
21
+ },
22
+ "graphics": [
23
+ {
24
+ "template": "lower-third",
25
+ "name": "$TITLE",
26
+ "start": 1,
27
+ "end": 6
28
+ }
29
+ ],
30
+ "overlays": [
31
+ {
32
+ "image": "$LOGO",
33
+ "position": "top-left"
34
+ }
35
+ ],
36
+ "loudness": {
37
+ "lufs": -14,
38
+ "tp": -1.0
39
+ },
40
+ "export": {
41
+ "preset": "x",
42
+ "normalize": true
43
+ },
44
+ "check": {
45
+ "platform": "x"
46
+ }
47
+ }
@@ -0,0 +1,47 @@
1
+ {
2
+ "template": "youtube-shorts",
3
+ "output": "$OUTPUT",
4
+ "clips": [
5
+ {
6
+ "src": "$INPUT"
7
+ }
8
+ ],
9
+ "frame": {
10
+ "aspect": "9:16",
11
+ "fit": "crop"
12
+ },
13
+ "brand": "$BRAND",
14
+ "captions": {
15
+ "text": "$CUES",
16
+ "animate": "pop",
17
+ "karaoke": true,
18
+ "size": 24,
19
+ "position": "bottom",
20
+ "margin": 52
21
+ },
22
+ "graphics": [
23
+ {
24
+ "template": "hook",
25
+ "title": "$TITLE",
26
+ "start": 0,
27
+ "end": 3
28
+ }
29
+ ],
30
+ "overlays": [
31
+ {
32
+ "image": "$LOGO",
33
+ "position": "top-left"
34
+ }
35
+ ],
36
+ "loudness": {
37
+ "lufs": -14,
38
+ "tp": -1.0
39
+ },
40
+ "export": {
41
+ "preset": "shorts",
42
+ "normalize": true
43
+ },
44
+ "check": {
45
+ "platform": "shorts"
46
+ }
47
+ }
@@ -0,0 +1,47 @@
1
+ {
2
+ "template": "youtube",
3
+ "output": "$OUTPUT",
4
+ "clips": [
5
+ {
6
+ "src": "$INPUT"
7
+ }
8
+ ],
9
+ "frame": {
10
+ "aspect": "16:9",
11
+ "fit": "pad"
12
+ },
13
+ "brand": "$BRAND",
14
+ "captions": {
15
+ "text": "$CUES",
16
+ "animate": "none",
17
+ "karaoke": false,
18
+ "size": 20,
19
+ "position": "bottom",
20
+ "margin": 14
21
+ },
22
+ "graphics": [
23
+ {
24
+ "template": "lower-third",
25
+ "name": "$TITLE",
26
+ "start": 1,
27
+ "end": 6
28
+ }
29
+ ],
30
+ "overlays": [
31
+ {
32
+ "image": "$LOGO",
33
+ "position": "top-left"
34
+ }
35
+ ],
36
+ "loudness": {
37
+ "lufs": -14,
38
+ "tp": -1.0
39
+ },
40
+ "export": {
41
+ "preset": "youtube",
42
+ "normalize": true
43
+ },
44
+ "check": {
45
+ "platform": "youtube"
46
+ }
47
+ }