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>
96 lines
3.8 KiB
Bash
Executable File
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()
|