vidgod/bin/vg-find
type-two 47412ca4b7 fix: sqlite WAL + busy timeout (concurrent farm index jobs), hatchling for -e installs
Two concurrent vidgod_index jobs on one node hit "database is locked" —
vg-index now opens the library in WAL mode with a 60s busy timeout (vg-find
30s). setup_venvs.sh installs hatchling+editables and does the MatAnyone -e
with --no-deps (its pyproject drags in cchardet, dead on py3.12).
Found during the m4probook standup + farm spill test.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-26 10:29:56 +10:00

96 lines
3.8 KiB
Bash
Executable File

#!/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()