#!/bin/sh "exec" "`dirname $0`/../venvs/index/bin/python" "$0" "$@" """vg-find — search the clip library built by vg-index. Usage: vg-find radical search dialogue for a word/phrase (FTS5 syntax ok) vg-find "totally awesome" phrase search vg-find radical --cut DIR also export each matching shot as a ProRes clip into DIR vg-find --shots VIDEO list the detected shots of one video """ import argparse, os, sqlite3, subprocess, sys from pathlib import Path os.environ["PATH"] = "/opt/homebrew/bin:" + os.environ.get("PATH", "") DB = Path(__file__).resolve().parent.parent / "library/clips.sqlite" def hms(t): t = max(0, t) return f"{int(t//3600):02d}:{int(t%3600//60):02d}:{t%60:06.3f}" def cut(src, a, b, dest): subprocess.run(["ffmpeg", "-hide_banner", "-loglevel", "error", "-y", "-ss", f"{a:.3f}", "-to", f"{b:.3f}", "-i", src, "-c:v", "prores_ks", "-profile:v", "1", "-c:a", "pcm_s16le", str(dest)], check=False) def main(): ap = argparse.ArgumentParser() ap.add_argument("query", nargs="*") ap.add_argument("--cut", metavar="DIR", help="export matching shots as ProRes clips") ap.add_argument("--shots", metavar="VIDEO", help="list shots of one video (path substring ok)") ap.add_argument("--limit", type=int, default=40) args = ap.parse_args() if not DB.exists(): sys.exit("no library yet — run vg-index first") db = sqlite3.connect(DB, timeout=30) if args.shots: rows = db.execute("""SELECT v.path, s.idx, s.start_s, s.end_s, s.thumb FROM shots s JOIN videos v ON v.id=s.video_id WHERE v.path LIKE ? ORDER BY v.path, s.idx""", (f"%{args.shots}%",)).fetchall() if not rows: sys.exit("no matching video in library") last = None for path, idx, a, b, thumb in rows: if path != last: print(f"\n{path}") last = path print(f" shot {idx:3d} {hms(a)} - {hms(b)} ({b-a:5.1f}s)") return if not args.query: v, s, g = db.execute("""SELECT (SELECT COUNT(*) FROM videos), (SELECT COUNT(*) FROM shots), (SELECT COUNT(*) FROM segs)""").fetchone() print(f"library: {v} videos, {s} shots, {g} dialogue segments") return q = " ".join(args.query) fts = f'"{q}"' if " " in q and not any(c in q for c in '"*') else q rows = db.execute(""" SELECT v.path, g.start_s, g.end_s, g.text, (SELECT s.start_s FROM shots s WHERE s.video_id=g.video_id AND s.start_s <= (g.start_s+g.end_s)/2 ORDER BY s.start_s DESC LIMIT 1), (SELECT s.end_s FROM shots s WHERE s.video_id=g.video_id AND s.start_s <= (g.start_s+g.end_s)/2 ORDER BY s.start_s DESC LIMIT 1) FROM segs_fts f JOIN segs g ON g.id=f.rowid JOIN videos v ON v.id=g.video_id WHERE segs_fts MATCH ? ORDER BY v.path, g.start_s LIMIT ?""", (fts, args.limit)).fetchall() if not rows: print("no matches") return outdir = None if args.cut: outdir = Path(args.cut).expanduser() outdir.mkdir(parents=True, exist_ok=True) for i, (path, a, b, text, sa, sb) in enumerate(rows, 1): sa = a if sa is None else sa sb = b if sb is None else sb print(f"{hms(a)} {Path(path).name}") print(f" \"{text}\" (shot {hms(sa)} - {hms(sb)})") if outdir: name = f"{i:03d}_{Path(path).stem}_{int(sa)}s.mov" cut(path, sa, sb, outdir / name) print(f" -> {outdir/name}") print(f"\n{len(rows)} matches" + (f", clips in {outdir}" if outdir else "")) if __name__ == "__main__": main()