← Commit history

Publish 1.0.6

John Lauer ·5a2b548912 ·1mo ago ·parent 2d702e8
5 files changed +58−2
Cargo.toml+1−1
@@ -1,6 +1,6 @@ [package] name = "adom-video-post"-version = "1.0.5"+version = "1.0.6" edition = "2021" description = "Post-process screen recordings: speed up boring sections with overlay captions, voiceover after the fact, publish to the Adom Wiki" license = "MIT"
bin/adom-video-post
⋯ 1 unchanged line ⋯
page.json+1−1
@@ -26,7 +26,7 @@     "binary_name": "adom-video-post"   },   "type": "app",-  "version": "1.0.5",+  "version": "1.0.6",   "description": "AI-driven studio for finishing demo videos: review every captured clip, flag and re-record the weak ones, speed up dead air, narrate, mux and auto-level, validate the final cut, and publish straight to the wiki, driven end to end by your AI.",   "bundled": true,   "dependencies": {},
skills/gang-takes/SKILL.md+4
@@ -38,6 +38,10 @@ A viewer wants to see the thing happen. So: - **Pure screen video.** No title card, no outro, no letterbox. The only thing added to the   frames is text. +## The closing chart++Every comparison video ends with a bar chart of the measured numbers (real time per engine, DRC errors, unconnected items, whatever the video compared). Give the manifest a `chart` block: `title`, up to three `subtitle` lines, `rows` of `{name, value, unit, note}`, optional `max` for the bar scale, `footer` (the page URL), and `narration` that reads the numbers out. It is rendered in the brand fonts on the dark background, fades in, and lasts as long as its narration. This is the one frame that is not a screen recording; it carries data, not decoration.+ ## Text on the frame  Segment label in Familjen Grotesk 700 (teal, top left), chip in JetBrains Mono 500 (top
skills/gang-takes/gang_takes.py+52
@@ -27,6 +27,12 @@ Manifest: {   "size": "1920x1080", "fps": 30,   "region": [0.26, 0.16, 0.77, 0.86],     # x0,y0,x1,y1 fractions watched for change (default: whole frame)+  "chart": {                                # optional closing bar chart, brand style, narrated+    "title": "Real time to route six nets", "subtitle": ["one line", "another"],+    "rows": [{"name": "Freerouting", "value": 3.2, "unit": "s", "note": "one call"}],+    "max": 30, "footer": "wiki.adom.inc/adom/kicad-bridge",+    "narration": "Real time to route the six nets: ..."+  },   "segments": [     {       "clip": "take1.mp4",@@ -217,6 +223,49 @@ def fit_narration(idx, seg, D, work):     return mp3, d, f"+{rate}% (still {d - D:.2f}s over: shorten the text or raise last_post)"  +def render_chart(path, chart, W, H, fonts):+    """Brand-style horizontal bar chart: title, subtitle lines, rows of (name, value, unit, note)."""+    BG = (13, 17, 23); PANEL = (22, 27, 34); TXT2 = (139, 148, 158)+    im = Image.new("RGB", (W, H), BG); d = ImageDraw.Draw(im); m = W / 1920+    disp = ImageFont.truetype(str(fonts["display"]), int(64 * m)); body = ImageFont.truetype(str(fonts["body"]), int(34 * m))+    mono = ImageFont.truetype(str(fonts["mono"]), int(40 * m)); monoS = ImageFont.truetype(str(fonts["mono"]), int(30 * m))+    d.text((120 * m, 100 * m), chart.get("title", ""), font=disp, fill=TEXT)+    subs = chart.get("subtitle", []); subs = [subs] if isinstance(subs, str) else subs+    for i, line in enumerate(subs[:3]):+        d.text((120 * m, (190 + 46 * i) * m), line, font=body, fill=TXT2)+    rows = chart.get("rows", []); vmax = float(chart.get("max") or max((float(r.get("value", 0)) for r in rows), default=1) * 1.08)+    x0, y0, bw, rowh = 520 * m, (360 if len(subs) > 1 else 320) * m, 900 * m, min(150 * m, (H - 500 * m) / max(1, len(rows)))+    for i, r in enumerate(rows):+        y = y0 + i * rowh; val = float(r.get("value", 0)); unit = r.get("unit", "")+        d.text((120 * m, y + 30 * m), str(r.get("name", "")), font=body, fill=TEXT)+        d.rounded_rectangle((x0, y + 18 * m, x0 + bw, y + 98 * m), radius=int(12 * m), fill=PANEL)+        w = max(24 * m, bw * val / vmax if vmax else 0)+        d.rounded_rectangle((x0, y + 18 * m, x0 + w, y + 98 * m), radius=int(12 * m), fill=ACCENT)+        label = f"{val:g}{(' ' + unit) if unit else ''}"; lx = x0 + bw + 28 * m+        d.text((lx, y + 30 * m), label, font=mono, fill=TEXT)+        if r.get("note"):+            d.text((lx + d.textlength(label, font=mono) + 24 * m, y + 38 * m), str(r["note"]), font=monoS, fill=TXT2)+    if chart.get("footer"):+        d.text((120 * m, H - 150 * m), chart["footer"], font=monoS, fill=ACCENT)+    im.save(path)+++def chart_segment(chart, W, H, fps, work, fonts):+    """A narrated chart at the end: lasts as long as its narration (or `seconds`), fades in."""+    png = work / "chart.png"; render_chart(png, chart, W, H, fonts)+    mp3, nd, how = fit_narration("chart", {"narration": chart.get("narration"), "narration_mp3": chart.get("narration_mp3")}, 10_000, work) if (chart.get("narration") or chart.get("narration_mp3")) else (None, 0.0, "none")+    D = round(max(float(chart.get("seconds", 0)), nd + 0.6, 3.0), 3)+    out = work / "chart-seg.mp4"+    if mp3:+        sh(["ffmpeg", "-y", "-v", "error", "-loop", "1", "-i", str(png), "-i", str(mp3), "-filter_complex",+            f"[0:v]fps={fps},format=yuv420p,fade=t=in:st=0:d=0.3[v];[1:a]aresample=48000,apad,atrim=0:{D:.3f},asetpts=PTS-STARTPTS[a]",+            "-map", "[v]", "-map", "[a]", "-t", f"{D:.3f}", "-c:v", "libx264", "-preset", "medium", "-crf", "18", "-pix_fmt", "yuv420p", "-c:a", "aac", "-b:a", "160k", "-ar", "48000", "-ac", "2", str(out)])+    else:+        sh(["ffmpeg", "-y", "-v", "error", "-loop", "1", "-i", str(png), "-f", "lavfi", "-i", "anullsrc=r=48000:cl=stereo", "-filter_complex", f"[0:v]fps={fps},format=yuv420p,fade=t=in:st=0:d=0.3[v]",+            "-map", "[v]", "-map", "1:a", "-t", f"{D:.3f}", "-c:v", "libx264", "-preset", "medium", "-crf", "18", "-pix_fmt", "yuv420p", "-c:a", "aac", "-b:a", "160k", str(out)])+    return out, D, {"seconds": round(nd, 2), "fit": how}++ def main():     ap = argparse.ArgumentParser(description=__doc__, formatter_class=argparse.RawDescriptionHelpFormatter)     ap.add_argument("--manifest", required=True); ap.add_argument("--out", required=True)@@ -245,6 +294,9 @@ def main():         else:             sh(["ffmpeg", "-y", "-v", "error", "-i", str(v), "-f", "lavfi", "-i", "anullsrc=r=48000:cl=stereo", "-map", "0:v", "-map", "1:a", "-c:v", "copy", "-c:a", "aac", "-b:a", "160k", "-shortest", str(f)])         finals.append(f)+    if man.get("chart"):+        cseg, cD, cfit = chart_segment(man["chart"], W, H, fps, work, fonts)+        finals.append(cseg); report.append({"segment": "chart", "seconds": round(cD, 2), "narration": cfit})     lst = work / "final-list.txt"; lst.write_text("".join(f"file '{p}'\n" for p in finals))     sh(["ffmpeg", "-y", "-v", "error", "-f", "concat", "-safe", "0", "-i", str(lst), "-c", "copy", "-movflags", "+faststart", str(out)])     sh(["ffmpeg", "-v", "error", "-i", str(out), "-f", "null", "-"])