From fb3e185d1d93334bed0c3ead81afb2b615aac120 Mon Sep 17 00:00:00 2001 From: Ellis Hewes Date: Sun, 7 Jun 2026 20:38:57 +0100 Subject: [PATCH 1/3] Add GIF bench and plotting tools; update docs Add scripts/bench_gifs.py (comprehensive GIF moderation pipeline benchmark) and scripts/plot_results.py (generate media/perf_stages.png and media/perf_latency.png from bench_results.jsonl). Update docs/performance.md to document installing bench deps and the new bench/plot workflow. Tweak scripts/bench_decode.py wording to clarify that ViT inference dominates wall-clock (~91%) and adjust the delta message. bench_gifs runs the real prescreen pipeline (precise backend stubbed), outputs per-GIF JSONL and scaling/timing analysis. --- docs/performance.md | 5 +- scripts/bench_decode.py | 4 +- scripts/bench_gifs.py | 530 ++++++++++++++++++++++++++++++++++++++++ scripts/plot_results.py | 69 ++++++ 4 files changed, 605 insertions(+), 3 deletions(-) create mode 100644 scripts/bench_gifs.py create mode 100644 scripts/plot_results.py diff --git a/docs/performance.md b/docs/performance.md index 83cbcb5..b9b3186 100644 --- a/docs/performance.md +++ b/docs/performance.md @@ -65,7 +65,10 @@ Against ~239 ms of inference, the difference is noise. ## Reproduce ```bash -python scripts/bench_decode.py # decode comparison +pip install psutil matplotlib # bench/plot tools, not runtime deps +python scripts/bench_gifs.py --procs "1" --duration 30 # single-worker profile -> bench_results.jsonl +python scripts/bench_decode.py # decode comparison +python scripts/plot_results.py bench_results.jsonl # regenerate the charts in media/ ``` ## Results log diff --git a/scripts/bench_decode.py b/scripts/bench_decode.py index 4de1ca7..7a2256b 100644 --- a/scripts/bench_decode.py +++ b/scripts/bench_decode.py @@ -2,7 +2,7 @@ """Micro-benchmark: cv2 file-path decode vs in-memory Pillow decode (the scan_bytes path). Shows the per-GIF decode cost of each, so you can see the millisecond delta against -the ~789 ms ViT inference that dominates total time. +the ViT inference that dominates total time (~91% of wall-clock; see docs/performance.md). python scripts/bench_decode.py [path/to.gif] # synthesizes one if omitted """ @@ -52,4 +52,4 @@ def synth(path, n=60, w=320): print(f" cv2 file-path decode: median {c:6.1f} ms") print(f" in-memory bytes decode: median {m:6.1f} ms") delta = m - c - print(f" delta: {delta:+.1f} ms ({delta / c * 100:+.0f}%) -- vs ~789 ms ViT inference, this is noise") + print(f" delta: {delta:+.1f} ms ({delta / c * 100:+.0f}%) -- vs the ViT inference (~91% of total), this is noise") diff --git a/scripts/bench_gifs.py b/scripts/bench_gifs.py new file mode 100644 index 0000000..9294f29 --- /dev/null +++ b/scripts/bench_gifs.py @@ -0,0 +1,530 @@ +#!/usr/bin/env python3 +"""Engine benchmark for PyFrame's GIF moderation path. + +Measures the real pipeline's throughput, latency, per-stage timing, memory, and scaling +under a multiprocessing pool. It drives the REAL pipeline (no reimplementation); per GIF +it calls, in order: + pyframe.media.iter_frames (decode) + pyframe.sampling.DenseUniformSampler.select (prescreen sampling @ screen_fps) + pyframe.media.Frame.to_pil (preprocess: BGR->RGB->PIL) + pyframe.backends.LocalBackend.classify_image (local ViT inference, per frame) + gate (score >= escalate_threshold, fail-open) (escalation decision) + on escalation: pyframe.sampling.SuspicionSampler.select + image_utils.merge_to_grid + -> StubBackend (precise/AWS backend MOCKED: instant, counted only) + +The precise backend is stubbed (instant, no network), so this measures LOCAL throughput. + +Concurrency = a multiprocessing (spawn) process pool (decode is CPU-bound; threads lose +to the GIL). Each worker forces single-threaded inference (OMP/MKL/OpenBLAS/torch/onnx=1) +so N processes don't each spawn multi-threaded torch and oversubscribe cores. + +Outputs: + 1. Per-process-count scaling curve + sweet spot, knee, bottleneck class. + 2. Per-stage median timing + inference fraction f. + 3. bench_results.jsonl: one record per GIF. + 4. Environment block (host, versions, pinned config). + +Run: + python scripts/bench_gifs.py --corpus ./gifs + python scripts/bench_gifs.py --procs "1" # single-worker profile + +Flags: see --help. +""" + +# ruff: noqa: E402 (thread caps are deliberately set before heavy imports below) +# Thread caps MUST be set before numpy/torch/cv2 import (they read these at import). +import os + +for _v in ( + "OMP_NUM_THREADS", + "MKL_NUM_THREADS", + "OPENBLAS_NUM_THREADS", + "NUMEXPR_NUM_THREADS", + "VECLIB_MAXIMUM_THREADS", # macOS Accelerate + "ONNXRUNTIME_INTRA_OP_NUM_THREADS", + "TOKENIZERS_PARALLELISM", +): + os.environ.setdefault(_v, "1" if _v != "TOKENIZERS_PARALLELISM" else "false") +os.environ.setdefault("TRANSFORMERS_VERBOSITY", "error") +os.environ.setdefault("HF_HUB_DISABLE_PROGRESS_BARS", "1") + +import argparse +import json +import multiprocessing as mp +import platform +import statistics +import subprocess +import sys +import tempfile +import threading +import time +from itertools import islice +from pathlib import Path + +import numpy as np + +try: + import psutil +except ImportError: # bench-only dep, not a runtime requirement of pyframe + raise SystemExit( + "bench_gifs.py needs psutil for host CPU/RAM sampling.\n" + " pip install psutil matplotlib" + ) + +from pyframe.backends import load_backend +from pyframe.backends.base import Backend +from pyframe.image_utils import merge_to_grid +from pyframe.media import iter_frames +from pyframe.sampling import DenseUniformSampler, SuspicionSampler + +# Worker-process globals (populated by worker_init under spawn). +_SCREEN = None +_STUB = None +_CFG = None + + +class StubBackend(Backend): + """Mocked precise backend: returns instantly, no network.""" + + name = "stub" + cost_per_image = 0.0 + + def _score(self, image): + return 0.0, [], None + + +# --------------------------------------------------------------------------- # +# Corpus +# --------------------------------------------------------------------------- # +def _synth_one(path, n_frames, width): + from PIL import Image + + height = max(8, int(round(width * 0.6))) + grad = np.linspace(0, 255, width, dtype=np.uint8)[None, :, None] + frames = [] + for i in range(n_frames): + arr = np.zeros((height, width, 3), np.uint8) + arr[:, :, 0:1] = grad # horizontal gradient (R channel) + x = int((i / max(1, n_frames - 1)) * (width - 24)) + arr[height // 2 - 8 : height // 2 + 8, x : x + 24, :] = 255 # moving block -> motion + noise = np.random.randint(-12, 12, (height, width, 3), dtype=np.int16) + arr = np.clip(arr.astype(np.int16) + noise, 0, 255).astype(np.uint8) + frames.append(Image.fromarray(arr)) + frames[0].save(path, save_all=True, append_images=frames[1:], duration=66, loop=0, optimize=False) + + +def synthesize_corpus(out_dir, count, rng): + # (weight, frame-range, width-range) + buckets = [ + (0.60, (10, 40), (128, 320)), + (0.30, (40, 90), (320, 480)), + (0.10, (90, 150), (480, 640)), + ] + out_dir = Path(out_dir) + out_dir.mkdir(parents=True, exist_ok=True) + paths = [] + for i in range(count): + r = rng.random() + cum = 0.0 + chosen = buckets[-1] + for b in buckets: + cum += b[0] + if r <= cum: + chosen = b + break + nf = rng.randint(*chosen[1]) + w = rng.randint(*chosen[2]) + p = out_dir / f"synth_{i:04d}_{nf}f_{w}px.gif" + _synth_one(str(p), nf, w) + paths.append(str(p)) + return paths + + +def describe_corpus(paths): + sizes, frame_counts, widths = [], [], [] + for p in paths: + sizes.append(os.path.getsize(p) / 1e6) + try: + from PIL import Image + + with Image.open(p) as im: + widths.append(im.size[0]) + frame_counts.append(getattr(im, "n_frames", 1)) + except Exception: + pass + + def pct(a, q): + return float(np.percentile(a, q)) if a else 0.0 + + print("\n=== CORPUS ===") + print(f" files: {len(paths)} mean file size: {statistics.mean(sizes):.2f} MB" if sizes else " (empty)") + if frame_counts: + print( + f" frames/GIF p50={pct(frame_counts,50):.0f} p90={pct(frame_counts,90):.0f} " + f"min={min(frame_counts)} max={max(frame_counts)}" + ) + if widths: + print( + f" width px p50={pct(widths,50):.0f} p90={pct(widths,90):.0f} " + f"min={min(widths)} max={max(widths)}" + ) + + +# --------------------------------------------------------------------------- # +# Worker +# --------------------------------------------------------------------------- # +def worker_init(cfg): + global _SCREEN, _STUB, _CFG + _CFG = cfg + try: + import torch + + torch.set_num_threads(1) + torch.set_num_interop_threads(1) + except Exception: + pass + try: + import onnxruntime as ort + + _ = ort # intra-op threads pinned via env ONNXRUNTIME_INTRA_OP_NUM_THREADS=1 + except Exception: + pass + _SCREEN = load_backend("local", model=cfg["model"]) + _STUB = StubBackend() + + +def process_one(path): + """Run the real cascade prescreen path on one GIF with per-stage timing.""" + proc = psutil.Process() + esc = _CFG["escalate_threshold"] + t0 = time.perf_counter() + frames = list(iter_frames(path)) + n_total = len(frames) + screen_frames = DenseUniformSampler(_CFG["screen_fps"]).select(frames) + t1 = time.perf_counter() + + pils = [f.to_pil() for f in screen_frames] + t2 = time.perf_counter() + + verdicts = [ + _SCREEN.classify_image(p, min_confidence=esc, index=f.index, timestamp=f.timestamp) + for f, p in zip(screen_frames, pils) + ] + t3 = time.perf_counter() + + scores = {v.frame_index: v.score for v in verdicts} + flagged = [v.frame_index for v in verdicts if v.score >= esc or (v.error and True)] + max_local = max((v.score for v in verdicts), default=0.0) + escalated = bool(flagged) + if escalated: + # Mirror Scanner._cascade escalation: top-suspicious -> merged grids -> precise. + per_batch = max(1, _CFG["frames_per_batch"]) + budget = _CFG["max_escalations"] * per_batch + fset = set(flagged) + flagged_frames = [f for f in frames if f.index in fset] + selected = SuspicionSampler().select(flagged_frames, budget, scores) + for i in range(0, len(selected), per_batch): + grid = merge_to_grid([fr.to_pil() for fr in selected[i : i + per_batch]]) + _STUB.classify_image(grid, min_confidence=0.8) # precise backend mocked: instant, counted + t4 = time.perf_counter() + + return { + "gif_id": os.path.basename(path), + "n_frames_total": n_total, + "n_frames_scored": len(screen_frames), + "t_decode_sample": t1 - t0, + "t_preprocess": t2 - t1, + "t_inference": t3 - t2, + "t_gate": t4 - t3, + "latency_ms": (t4 - t0) * 1000.0, + "peak_rss_mb": round(proc.memory_info().rss / 1e6, 1), + "max_local_score": round(float(max_local), 4), + "escalated": escalated, + } + + +# --------------------------------------------------------------------------- # +# Resource monitor (samples worker PIDs during a level) +# --------------------------------------------------------------------------- # +class ResourceMonitor(threading.Thread): + def __init__(self, pids, interval=0.5): + super().__init__(daemon=True) + self.interval = interval + self.procs = [] + for pid in pids: + try: + self.procs.append(psutil.Process(pid)) + except psutil.NoSuchProcess: + pass + self._stop = threading.Event() + self.peak_rss = 0 + self.cpu_samples = [] + + def run(self): + psutil.cpu_percent(None) # prime system-wide + while not self._stop.wait(self.interval): + self.cpu_samples.append(psutil.cpu_percent(None)) + total = 0 + for pr in self.procs: + try: + total += pr.memory_info().rss + except (psutil.NoSuchProcess, psutil.AccessDenied): + pass + self.peak_rss = max(self.peak_rss, total) + + def stop(self): + self._stop.set() + self.join(timeout=3) + + @property + def mean_cpu(self): + return statistics.mean(self.cpu_samples) if self.cpu_samples else 0.0 + + +def _path_stream(corpus): + while True: + for p in corpus: + yield p + + +# --------------------------------------------------------------------------- # +# Run one concurrency level +# --------------------------------------------------------------------------- # +def run_level(P, corpus, cfg, duration, min_gifs, warmup): + ctx = mp.get_context("spawn") + pool = ctx.Pool(P, initializer=worker_init, initargs=(cfg,)) + try: + pids = [w.pid for w in getattr(pool, "_pool", [])] + # Warm-up: model load happened in init; exercise JIT/caches and discard. + for _ in pool.imap_unordered(process_one, list(islice(_path_stream(corpus), warmup))): + pass + + mon = ResourceMonitor(pids) + mon.start() + records = [] + t_start = time.perf_counter() + for rec in pool.imap_unordered(process_one, _path_stream(corpus)): + records.append(rec) + elapsed = time.perf_counter() - t_start + if elapsed >= duration and len(records) >= min_gifs: + break + wall = time.perf_counter() - t_start + mon.stop() + finally: + pool.terminate() + pool.join() + + lat = np.array([r["latency_ms"] for r in records], dtype=float) + frames_scored = sum(r["n_frames_scored"] for r in records) + gifs = len(records) + result = { + "P": P, + "wall_s": wall, + "gifs": gifs, + "gifs_per_sec": gifs / wall, + "gifs_per_hr": gifs / wall * 3600.0, + "frames_scored_per_sec": frames_scored / wall, + "lat_p50_ms": float(np.percentile(lat, 50)), + "lat_p95_ms": float(np.percentile(lat, 95)), + "lat_p99_ms": float(np.percentile(lat, 99)), + "mean_cpu_pct": mon.mean_cpu, + "peak_rss_mb": mon.peak_rss / 1e6, + "escalation_rate": float(np.mean([r["escalated"] for r in records])) if records else 0.0, + } + return result, records + + +# --------------------------------------------------------------------------- # +# Analysis +# --------------------------------------------------------------------------- # +def make_sweep(vcpu, max_procs, explicit): + if explicit: + seq = sorted({int(x) for x in explicit.split(",") if x.strip()}) + return [p for p in seq if p >= 1] + cap = max_procs or 2 * vcpu + base = [1, 2, 4, 8, 12, 16, 24, 32, 48, 64] + seq = sorted({p for p in base + [vcpu, 2 * vcpu] if 1 <= p <= cap}) + return seq + + +def analyse(levels, vcpu, ram_gb): + best = max(levels, key=lambda r: r["gifs_per_hr"]) + best_hr = best["gifs_per_hr"] + # knee = smallest P reaching >=90% of peak throughput + knee = min((r for r in levels if r["gifs_per_hr"] >= 0.9 * best_hr), key=lambda r: r["P"]) + peak_rss_gb = best["peak_rss_mb"] / 1024.0 + if peak_rss_gb >= 0.85 * ram_gb: + bottleneck = "RAM-capacity-bound (aggregate RSS approaches host RAM before the knee)" + elif knee["P"] >= 0.9 * vcpu: + bottleneck = f"CPU-bound (knee P={knee['P']} ~ vCPU={vcpu})" + elif best["mean_cpu_pct"] < 85.0: + bottleneck = ( + f"memory-bandwidth-bound (knee P={knee['P']} < vCPU={vcpu}, " + f"CPU only {best['mean_cpu_pct']:.0f}% at sweet spot)" + ) + else: + bottleneck = f"CPU-bound (knee P={knee['P']})" + return best, knee, bottleneck + + +# --------------------------------------------------------------------------- # +# Environment +# --------------------------------------------------------------------------- # +def cpu_model(): + try: + if sys.platform == "darwin": + return subprocess.check_output(["sysctl", "-n", "machdep.cpu.brand_string"], text=True).strip() + if sys.platform.startswith("linux"): + for line in Path("/proc/cpuinfo").read_text().splitlines(): + if line.startswith("model name"): + return line.split(":", 1)[1].strip() + except Exception: + pass + return platform.processor() or platform.machine() + + +def versions(): + out = {} + for mod in ("torch", "onnxruntime", "transformers"): + try: + out[mod] = __import__(mod).__version__ + except Exception: + out[mod] = "n/a" + return out + + +def hr(title): + print("\n" + "=" * 78) + print(title) + print("=" * 78) + + +def main(): + ap = argparse.ArgumentParser( + description="Engine benchmark for the GIF moderation path.", + formatter_class=argparse.ArgumentDefaultsHelpFormatter, + ) + ap.add_argument("--corpus", default=None, help="directory of real .gif files (else synthesize)") + ap.add_argument("--synth-count", type=int, default=120, help="GIFs to synthesize when no --corpus") + ap.add_argument("--out", default="bench_results.jsonl", help="per-GIF JSONL output") + ap.add_argument("--duration", type=float, default=120.0, help="steady-state seconds per level") + ap.add_argument("--min-gifs", type=int, default=2000, help="min GIFs per level (overrides short duration)") + ap.add_argument("--warmup", type=int, default=20, help="GIFs to process+discard before timing") + ap.add_argument("--procs", default=None, help="explicit comma list of process counts (overrides sweep)") + ap.add_argument("--max-procs", type=int, default=None, help="cap the process sweep (default 2x vCPU)") + ap.add_argument("--screen-fps", type=float, default=2.0, help="prescreen sample rate (sweepable)") + ap.add_argument("--max-frames", type=int, default=10, help="motion-sample frame budget (sweepable)") + ap.add_argument("--escalate-threshold", type=float, default=0.15, help="gate threshold (recall-safe)") + ap.add_argument("--frames-per-batch", type=int, default=2, help="frames per merged grid on escalation") + ap.add_argument("--max-escalations", type=int, default=2, help="precise-backend call cap per GIF") + ap.add_argument("--model", default="AdamCodd/vit-base-nsfw-detector", help="local ViT model id") + args = ap.parse_args() + + vcpu = os.cpu_count() or 1 + ram_gb = psutil.virtual_memory().total / 1e9 + cfg = { + "screen_fps": args.screen_fps, + "max_frames": args.max_frames, + "escalate_threshold": args.escalate_threshold, + "frames_per_batch": args.frames_per_batch, + "max_escalations": args.max_escalations, + "model": args.model, + } + + # --- environment + pinned config --- + hr("ENVIRONMENT & PINNED CONFIG") + ver = versions() + print(f" host: {vcpu} logical vCPU | {ram_gb:.1f} GB RAM | {cpu_model()}") + print(f" OS: {platform.platform()} Python {platform.python_version()}") + print(f" torch={ver['torch']} onnxruntime={ver['onnxruntime']} transformers={ver['transformers']}") + print(" inference backend: torch CPU, single-threaded per worker") + print(" threading: OMP/MKL/OpenBLAS/VECLIB/ONNX=1, torch.set_num_threads(1) per worker") + print( + " pinned: prescreen.enabled=True " + f"screen_fps={cfg['screen_fps']} max_frames={cfg['max_frames']} sampler=motion " + f"escalate_threshold={cfg['escalate_threshold']} frames_per_batch={cfg['frames_per_batch']} " + f"max_escalations={cfg['max_escalations']}" + ) + print(f" precise backend: STUBBED (instant, counted) model={cfg['model']}") + print( + " pipeline fns/GIF: iter_frames -> DenseUniformSampler.select -> Frame.to_pil" + " -> LocalBackend.classify_image (ViT) -> gate -> [SuspicionSampler.select -> merge_to_grid -> StubBackend]" + ) + + # --- corpus --- + tmp = None + if args.corpus and Path(args.corpus).is_dir(): + corpus = sorted(str(p) for p in Path(args.corpus).glob("*.gif")) + if not corpus: + print(f"\nNo .gif files in {args.corpus}", file=sys.stderr) + return 2 + else: + rng = __import__("random").Random(1234) + tmp = tempfile.mkdtemp(prefix="bench_gifs_") + print(f"\nSynthesizing {args.synth_count} GIFs into {tmp} ...", flush=True) + corpus = synthesize_corpus(tmp, args.synth_count, rng) + describe_corpus(corpus) + + # --- sweep --- + sweep = make_sweep(vcpu, args.max_procs, args.procs) + hr("PER-CORE SCALING CURVE") + print(f" sweep P = {sweep} (steady state: max({args.duration:.0f}s, {args.min_gifs} GIFs)/level)\n") + header = f" {'P':>3} {'GIFs/s':>8} {'GIFs/hr':>10} {'frm/s':>8} {'p50ms':>8} {'p95ms':>8} {'p99ms':>8} {'CPU%':>6} {'RSS_GB':>7} {'esc%':>5}" + print(header) + print(" " + "-" * (len(header) - 2)) + levels = [] + records_by_p = {} + for P in sweep: + res, recs = run_level(P, corpus, cfg, args.duration, args.min_gifs, args.warmup) + levels.append(res) + records_by_p[P] = recs + print( + f" {res['P']:>3} {res['gifs_per_sec']:>8.2f} {res['gifs_per_hr']:>10.0f} " + f"{res['frames_scored_per_sec']:>8.1f} {res['lat_p50_ms']:>8.1f} {res['lat_p95_ms']:>8.1f} " + f"{res['lat_p99_ms']:>8.1f} {res['mean_cpu_pct']:>6.0f} {res['peak_rss_mb']/1024:>7.2f} " + f"{res['escalation_rate']*100:>5.0f}", + flush=True, + ) + + best, knee, bottleneck = analyse(levels, vcpu, ram_gb) + sweet_records = records_by_p[best["P"]] + + # per-worker throughput == per-core for single-threaded inference-bound work + per_core_hr = best["gifs_per_hr"] / min(best["P"], vcpu) + rss_per_worker_mb = best["peak_rss_mb"] / best["P"] + + print(f"\n SWEET SPOT: P={best['P']} {best['gifs_per_hr']:.0f} GIFs/hr " + f"(p95={best['lat_p95_ms']:.0f}ms, CPU={best['mean_cpu_pct']:.0f}%, RSS={best['peak_rss_mb']/1024:.2f}GB)") + print(f" KNEE: P={knee['P']} (>=90% of peak throughput)") + print(f" BOTTLENECK: {bottleneck}") + print(f" GIFs/hr per core = {per_core_hr:.0f}") + print(f" RSS per worker = {rss_per_worker_mb:.0f} MB") + + # --- per-stage timing --- + hr("PER-STAGE TIMING") + stages = ["t_decode_sample", "t_preprocess", "t_inference", "t_gate"] + sums = {s: sum(r[s] for r in sweet_records) for s in stages} + meds = {s: statistics.median(r[s] for r in sweet_records) for s in stages} + total = sum(sums.values()) or 1e-9 + f = sums["t_inference"] / total + for s in stages: + print(f" {s:<16} median={meds[s]*1000:>8.2f} ms share={sums[s]/total*100:>5.1f}%") + print(f" inference fraction f = {f:.3f}") + + # --- jsonl --- + schema_keys = [ + "gif_id", "n_frames_total", "n_frames_scored", "t_decode_sample", "t_preprocess", + "t_inference", "t_gate", "latency_ms", "peak_rss_mb", "max_local_score", "escalated", + ] + with open(args.out, "w") as fh: + for r in sweet_records: + fh.write(json.dumps({k: r[k] for k in schema_keys}) + "\n") + print(f"\nWrote {len(sweet_records)} per-GIF records -> {args.out}") + + if tmp: + print(f"(synthesized corpus left in {tmp}; delete when done)") + return 0 + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/scripts/plot_results.py b/scripts/plot_results.py new file mode 100644 index 0000000..15648a1 --- /dev/null +++ b/scripts/plot_results.py @@ -0,0 +1,69 @@ +#!/usr/bin/env python3 +"""Generate PyFrame performance charts from a bench_results.jsonl into media/. + + pip install matplotlib + python scripts/plot_results.py [bench_results.jsonl] + +Produces media/perf_stages.png (per-stage median timing) and +media/perf_latency.png (per-GIF latency percentiles). +""" + +import json +import statistics +import sys +from pathlib import Path + +try: + import matplotlib +except ImportError: # plot-only dep, not a runtime requirement of pyframe + raise SystemExit("plot_results.py needs matplotlib.\n pip install matplotlib") + +matplotlib.use("Agg") +import matplotlib.pyplot as plt # noqa: E402 +import numpy as np # noqa: E402 + + +def load(path): + with open(path) as fh: + return [json.loads(line) for line in fh if line.strip()] + + +if __name__ == "__main__": + src = sys.argv[1] if len(sys.argv) > 1 else "bench_results.jsonl" + recs = load(src) + n = len(recs) + out = Path("media") + out.mkdir(exist_ok=True) + + # Chart 1: per-stage median time per GIF (log x, since inference dwarfs the rest) + stages = ["t_decode_sample", "t_preprocess", "t_inference", "t_gate"] + labels = ["decode + sample", "preprocess", "inference (ViT)", "gate"] + vals = [statistics.median(r[s] for r in recs) * 1000 for s in stages] + colors = ["#6c8ebf", "#bdbdbd", "#d6604d", "#bdbdbd"] + fig, ax = plt.subplots(figsize=(7.2, 3.0)) + bars = ax.barh(labels, vals, color=colors) + ax.set_xscale("log") + ax.set_xlabel("median time per GIF (ms, log scale)") + ax.set_title(f"PyFrame per-stage timing (n={n} GIFs, single worker, CPU)") + ax.invert_yaxis() + for b, v in zip(bars, vals): + ax.text(v * 1.05, b.get_y() + b.get_height() / 2, f"{v:.1f} ms", va="center", fontsize=9) + fig.tight_layout() + fig.savefig(out / "perf_stages.png", dpi=130) + plt.close(fig) + + # Chart 2: per-GIF latency percentiles + lat = np.array([r["latency_ms"] for r in recs]) + pcts = [50, 90, 95, 99] + pv = [float(np.percentile(lat, p)) for p in pcts] + fig, ax = plt.subplots(figsize=(7.2, 3.0)) + ax.bar([f"p{p}" for p in pcts], pv, color="#6c8ebf") + ax.set_ylabel("per-GIF latency (ms)") + ax.set_title(f"PyFrame latency percentiles (n={n} GIFs, single worker, CPU)") + for i, v in enumerate(pv): + ax.text(i, v, f"{v:.0f}", ha="center", va="bottom", fontsize=9) + fig.tight_layout() + fig.savefig(out / "perf_latency.png", dpi=130) + plt.close(fig) + + print(f"wrote media/perf_stages.png and media/perf_latency.png from {n} records") From ec914f1a0de72b6ba2af8037d7d597c647301cb4 Mon Sep 17 00:00:00 2001 From: Ellis Hewes Date: Sun, 7 Jun 2026 21:02:28 +0100 Subject: [PATCH 2/3] Cache backend instances and reuse in CLI Add caching for constructed backends to avoid reloading large model weights per invocation: introduce _cache, _cache_lock, _construct, and a cache-aware load_backend (with cache=False bypass). Export clear_backend_cache() to allow tests or users to drop cached backends. Update package exports to include clear_backend_cache. Update CLI to build a Config and create a single Scanner.from_config() (reusing the constructed backend) and then scan each path with that scanner, handling BackendUnavailableError up-front. Also update two performance images (media/perf_latency.png, media/perf_stages.png). --- media/perf_latency.png | Bin 23947 -> 25825 bytes media/perf_stages.png | Bin 26383 -> 26516 bytes src/pyframe/__init__.py | 3 +- src/pyframe/backends/__init__.py | 57 ++++++++++++++++++++++++++----- src/pyframe/cli.py | 48 +++++++++++++++----------- 5 files changed, 79 insertions(+), 29 deletions(-) diff --git a/media/perf_latency.png b/media/perf_latency.png index 5f81bbd05f6368234412dab18d2b09186fb829f8..6c0126c65ffaff9d738fe96b17c523f48e195d85 100644 GIT binary patch literal 25825 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 dyk&vb=cR)C<|ZWqg;L_q`O0;HeGi; zzxUkt&YgGe%$YfJ-*f)Z?I3&a=ULBM-}R~Q`k2^58|p>xI6TwyTYokGY36lB&6vo0F@Tlf4D)Yikcrdsi1;4sI?EVK!Pj zFE2Mw5l&9$|NIROR}WiGM!E4<;8QT&6kd5EAram|e~{6Wh2J3|*&-?aBdzl$dw=0! zw$AVSi~F!prRi9#DE81Ovgw@Zg@q^!L9}Ta6vEoCgrZ!vgrYUvTvGoeyWPc4T?QQ7 z9gK|p8TmW%cX1&=oF?A=Xe1}Y&~vM8)Hly_cHN`J=p(&iB7NBh)$xfZwBTQg1UMQD z&})WSFbniLh>D^Ey=pO%N9Ko-awVHB4w7QO|V*T-RKzMyGk@ZBVyPnY<0Z+|1%sk-SA5bu3hy zb{@{TcK&uW%?-M{tU2ho?z>I*-OT9m8doLV%=R3qnRWOaz7{Vj(RMN-`p`6OzGAyV{HN-O3RRjJM6S zFCP11JL-1Wg&wJA;#)N6h!qed;=N0h`pPE54X(%(E_ruRxO}vFr!jb%5@b}}akblg z6M|0teB1=vSsX`e!MS-Q!aUb^^TYk!PDx((=jsj$G<<4GmzIsxH{*N13gur(*v5;W zJ#Wr_kY!K>`<6DeA)_q&G@u}y1k3pA>q;#%y@ePWNr`NaL1uICChR$vug*UbS*zQ4 zNcbGr8s}WFulJiwZ_c~1IVlAe->h+24<;X8?N#?*|A`e`B+GrX`k@A{#QM^A|Ls9L z>72N za@%Zfw06{D&dwVTq4Rmqo~MzN1RP)g)ETr(&b9oh8zS+KLo#+yEyb91@29eyiBifZ z)-&S%DS3axZFhUvgTE#o-v3LUhH3=-gr$1PCd=8vEXuX>Rv>kBL}ahRZLhMa?|_+D zZ$pZKZ0XW6PMFcDZh+C<@FkkCV#8#Equ#TK`NNeSR`z=^gdXjFr%|JoZO4PBS219` zSvulP$0t8Cmlh+)l9xJ_=$G0$^d)Idg9ULxSD9C#pOhqw&&aZ20f#WqrN9o4%bX-+4}CT z2Qy!6|8At2R@IFJ-QT9Yy}R7vp;5Q{vgo8yJ6HlOulHwrZ-xHU^mqi_9LcA6FZ!Ph&x41YH)j#eu5^_f z06w{e!?^YP*6VrqewO~vtxKN}r!TjgO~6w=YEkBFug81)S;oL-K;GdK67i_`xlu3f zF<8u{C$?nyQ_#+y&hmG+Q#m6f(2B`A~pN~Wg_)){@ux_WXt7N zp5)&iOr~s&{Z{`@OdI9PT!s`Av8e*t$ochwq#=_0xF>=fQ((oX1vI#+>7fa{xT6nrr*uN^o; zz2g)IrTh2C#5+N<5jdDxt{uKAX@#7m0m2GdPfz0}S}bnO6s@yFyd8uN+D^I?l{v^w zhw8v1&}oYc*f`l?;_1QR{l0eigD#$EVH4a8Yw=tSHM0kdHEHoRm;vt;lNf(H-0u)} zzfRP>{mDD}&U^9|(;4ns0H{&>hkw3|raFtlZ=WfW&wC89_kaaQd_9o@M-Q(rVsD&& zMlCsV34VvV)#rd3Ph>m)5Zs8aNZyB=J3h=2{(y0TX|u44K+5x1wmJM2wS5#T#aS-p zuSKB=`R4D8X=BJOEqh&Ypb?G#p|pS89_!4Vs^9`2^|jWo(Imv;daL4rVJnSBtwib0 z#zE(O;QZ^xusvTe-*84ma(VGhqbqPoB=;2F4vRN z@A|}A>lT_#@a=ss&h*+$oPpjlDG6)F-UM ziFGk>%aX4ZECc2Kyu2Y~Ik&?W!N@$j( z%**r#OT>N77eDZP*V8}IQ(5K5oqHZbQDcXt<=Bg;yA9ZKUf|Wv@2a7#@$;{Z8@%v$ z{W0W(gW*fm9ju6k>Z-N^IC;L3_1RWlknT-ZmYCr6u)rMKnI?;X+d#vV$&;Y5*GmEC z`X8%UU7Cqv{(e2^xJVmS4P?78XL1WzixupCfX((O=%>Ez`i}(e_X`f6E6exy6uQpA z1j32tXtl$6aB(EI7Q<^{z5%wV_YX0zyvLZLJDL#vs}bV<7l$kdrwcwyy`fkrBpUCT z@}HHi%YUG;xjLH^PB03pT|o^ zwMBUTsHNey_{S(d&@KG1uoLX2kxh$!ths`5v@Ai6PMd(E_5G5C{*{RnSE2uW&+{>lVb}U!as_26el!Dc+ib8C zn&$Lov`& z7;ko*acS=X>a5xyLcWuqyj!_oktvNHk$?HgqwIf|q%Og180G3w(b9p!S{@TIZfw12=^&AnlEjr*xBwF-Y3TRq(Jb;$*&G8VYDcm;4|9HUtR_0d8 zvCxRQqoF;1;zi5fmjfOn<3AGUSX1L^Bs*5`h63s-&r2p>dN6Tth@*u4M{Nk&DFgl?mW#(tz)L!f$~9cEy_2`AurK% z6$>>?w}sZx@%bN?_EKMV;{NB8J_ew>ZqQTlp?V;Ljq!nI?}G`CrLT!tcq~1~=i#Hv zU+V{1KI>p1`MSjNPnIGiZd~eaQVm_oj>&HV8K>T~##@U>vFrGLG+}CSxrkE{-LG$H z+010cV@s~C>SvmX`!Xsas;9>LlS|fr=|-wmC%nT}M=cjjN#gSeF9ze52Rc=%xg{P9 z#tCJu?wRCeILuU+3X!dc-Po+`mLnW+Gldkzb3^4TfrLQAIeVL9ZNGGrP`-YPW}i;* zJ1f1UrZKlW3WOHqx7tTDwt2+E`P@oL{CbqQkWk2y`(#IVJ8c*4&o}PZ#jC@)Z^;aJ5)x(5@~&W)2y- zoaTtS9j_0p0Hhu#yFD%$6*!Wut(A(MGrmqnq{O=+<|%f(WdzG3Dl-Ad+4&bjA70q? znvVp2cU*KG@$y`3D#+RGmQ)F|u#;>`;p+Ax&>c^RF&zH4S*?X889N}M>%5XNTuVpz z0D)-}D0LQA7DRw3*hnCrSHJlCD?T0ZgU#tlSqV&TlP)JKu;VnxCPHT@q9cBt_1u>0 zg+>Pq-~G;~mIJI&iq*y51FG{oFVmj&CD<@i*)PQ7B6XW%Ba!wr4kXin)0a03^z9Nf zoq0~20oyy+*-N8|I&1VBts*cbPpV_XK3B6QmxaMVf2E!4S)xRDykqwRYllpawOLq@ z$OiLY?HJ$uR$i%;K`wk2$$1ZmUJJ1U>T80p9l4{=#C3h6YTH zXw~{0wh|4Es{Q9)71+45(ksWH&R3VlEQ@KUZ*&FA5%Dy~Z3q3lpF+E3+xenXVNoz9 ztcsS*{JN6JU_@ADv7{K8Yf$*`A5|=4p8@_?<0rjnY^)K1bK&$To3N$oVoMcMcIt36 z$8v16BSlx`mNPoX=NFH+-4+P#NWXG=#9=l@;|j?yYwRA$5Ll5=)T2dG``P)!rZ^|6 z>lo+rPSAH)VDsk*eHSq@Q^vK9RsZ+EcW5U_1-Vqc_S>KaWyQx zx37y&CdayMe1^$*f{x63O2);A+Qp7?QNte2GBoJ!@8Bt+)O8-Yw0m`zN`I2ih@$5% zMxOPov%6#>D$Ob;Zg+A^-!pGD*D2UQ60KBA{Lvc?Ljx=qBbDP3e91yWt&`DU_6akd zH~$DT`q)?vSzL&PYNHk9>!wh+V-3V`sRYY9zpj-|bZ}sZg0*q{bezo6Wb71|=QV4r`Q%5Q zPLV&PUX1Rxs`WJZBi}gIq7p|u^}JH|cbRImW@7UouX9IlL(RPZ7a`B&5eFEbZmJ`WjAW#VOGRJj)Rq*XC}7|T%r zi;ttVbIP>@oGedsagf%D&9NE)pn_<(MlKaZY!~i+7b}sLW|_K9ux}9`QNv)u z1nFtoccoDutm@U}3Y~uJm%0$Gdr>^9lvFHd*vSC3#)(aA3@b_U%949xKFL6-d`kR$ zUb|>n^G`O9mt>iB{o=`ie|WLDVlmy*obnm+6F6OKS|*tLXeYZAO?1^dQp=BTymjAZggGhc$sS}j8!yFqI^*NNZFCF z%b>vPcb{(6wSsn#T+3h0UcQO;XrvTJF-(mlYcmn%*7t{$#4Potk4uZ?VXA@A)AFI; zWhzS*Q`N70J_}$Z+6@$aI-WFijqMNa)KRj%TpaO9O>oONUA{RP<-AttxJ;}=A}2si zzuYLiS(>mQx1^M<<9$BPZ}Y-gd4rEvi|&(L5{Vv`f{L@uEPHs4mc!+4SQZC+!_7yM z)=T)OIsL?LJn5G$#B=dhS1~Y7RasYg=Z|Q~D%#!Ix%LD2o&~}VNoMatH43_iseI-& z4^Ql*tJ5G=Ve8C!{58s9&mDWcp9wO1ebt zM7aRT<}NH>Q5iMNl}Je$7Hnv@2+!ntFYAZIQqzezDSBvi(0=Zcy#M@N=+O&OW1SkB zC+spmXqH*Zgl!+C`s4U@#nUEz9eE_LR z;p8T!rL?lhk(bYusqEghC}L`1E)ULRrawe@e>#`}ESd0Hu;hAL6I(sXs)ZqZI>O<% z1C~y?ranVH%p<-Yu5~5BLrPbv@9&!aRroeA8^21B1&vuD)nWNt2)A+i&xb6uQB<~Y z33C6cf{6_YnNHn~lcLx1Wka^f+7SLozFtg$`SQdIRqmn&{*)XRXP3_8oXh%2V_+9( z5$NOAjmh*5h<0@r!pQU+sDafPC;~yju-Q*@=sPnyV|DikcD1D>iq(sW(^(YuRvi{? zWVf>!+en~-wSCG9pdd!_k58)8383_z&^+Qaw*Jwfu;$ODC&oUj!=mx}IlZt%!;BuW zZB+HdkJ5&cvt$VpShwa!XS@0uZYjc@SFm~$pfl74OUKd(`d%qOK&F8Ek=PuU%FP3I7&BVTCn`y57bVUVd{IK-6caN=KEsZ^37TmX&`f`%zJn3F{74 zlLnuP>P8R1vS-NuWcK@|UaWmqJ(|E|!}LcXyn2I=#R3-J-!}&t>A`N@huw3Z+ocsA z_`)3&UJxFbE(ANOhVQY8d$_}Azj-SirdgrTSIXtrG20tI!>ykFkp;UimM7Qa7kpAQ}wKOeYJ)&sZdpRPXY(ei(c*wJzY0)S>r?*Z12gS_{Z_ ze)hb?|C_633rl>9T#HMKx)@;KwUpiGp>JJesq$Kl-eQgjOY_rF;!}$W71MYcYuGD4 z)Zl5Acs?~T6?nZ*Y7%sNdNHT8V#0#$bNj__#u) zL4mJ^W7y^Ro7;Tpu&An}+^~Aa-p!yPpMENzbzOb)a3MWb&kF8)%99Y1tfac)7MAE? zs*Ip)tl`ri?>0zH{oS*Ncrq~BJhw*NbNc`}6jt*yF1J#9G15<{F8267F90Y=mN$YY z)Fj$-`_BagZB-f7b1Anv=fOp;-9!(Zv3FdowzQuwejU7N9%!WUT~BcDXtTO&M4WyT zt4K2q^sTZT&2?Xt*>nQ*Sk;qGF6Oz=tG5)M2i(6zJx91*O_w?X)~xnybA4_67a+aT z3vVzCUqa}w?Dg6Hngm8-(!0&!<@H%9oH+;+t zGSXPjBWWFO)ltS)TVYBS1LApnCF4ony5rm^StaK z>>&gB@;~14drxm~?N+$jivuTt>M?Em!5}~mQ}^%9&Z3@DE9pb5yx#98;Nlcw7LFgCL!68XPE8W=wu(Ql_!335tH8~O10>2r{M{CfebhZ>7ZgQMyoW62e${? zOY)u1uhul*n~lg;sb5VQ`7C%_=gn0$6daKEr#)^>x-(J<`)5fTYZg&yxg6_Z;bnNW zuc`2 z^DUlsl{gx|cOPX_yM04eZ6^obrWP?I&;#Kz=s*5%%p=qP@-1Tf*F+W_Fgn8%=;Zt4 zl3^9I2ISyN6ybiDYS)OJ@&EYWm)p~WU-N|(AdmEa$rF{CD_%q@E4w*}YwBsMzMGrK zda9VHlQ-sMJXQ*JbB%xZ`7Hb6aPcKB%%&mBh3(5vt^L;e`a~@MDPx~-S$uK6pP)FJ z12q&2=w$NgLhcq90H0fC9>(yFkdYYJ6}a^h>*@I2zQZK!ek@P^ z0wbV!{C>F@c}{z=c~b@10^$l~!a#dg-aSFqa)6B5=7nsUs>fAKRVW~qprw){!Vd&! zi6iL@m;_``rL(kwy(r}-6LN4t_Q4I@dN|m(xgj5{)<`bukTJ`NVCbdO6flsrm@lBK zUvpFg4$>ij)4d*ZyPvD61A*P@w;wi9uH|3$@{>&@?ddlZd>m!=2Aw}K9cQzS{h6Qq zQ3Wdc(~eJ0+!4n5K8;Znz`MbfWmx2raw*^isS;y7x#0kITA4c zqbS0Jx^9c?WQS--o=<6lbm#lYu%M|vfOX1& zxEuXLoSkBKS#pGfu5I|S&%o$-8jYB#Cu?H{UkXAfzGjHy>vs>G?!an?0SbC41}>}h zIk3}LeX{vL@yuWq1NFp^jy%8_z`U3SS+)R+Rl>^07~iL zg!z!&AGHCMYgFRe$!P->nH`zvKy?{tq2qntn5mP%^*mfpQtz|QKdseR_5@Z?Z~lkT zJYX!drjCD74jvwkpX~4Z9vc(DG@a#G)xwgR2Z9kPlXTw-pV7t!X9q}bWqWo*VS`za z8`$wR5T)_kE|@03GO!coL_MSlyfAla@52-Q^XPs!R!~xkO4!4S(kPfO9nrGUob$oRh$A|LKR^j1hOiACS$NK9p|n#ck*nME?o1&OHfcFgs%ic$7m{eGS@gD=Fx)MPfoAfO@RccIW@VGEeq+H9*pqBUECyCaK zEg3K9Q9_NfeJGm6D)c8)@c+! z5Mik%iis0`W8b87sjR7aZr^B-Jg{c38&rBxgVD z`9Ub`-zHjrEu~Ire)uK=V`O56+h3;O@UyGOX|@~&Lg>3}u~&z*ji8wX+k`<;YtId= zP#uR96$2SpFhlk6o()wI!Pa@s20+H=apr?AK3sz2tv_|J)N<(uIuDc#M9Zv|(0LGo z3`NhJf4v}$4N7Xe+NI}z3CU?Hze@j}B+b@YjlsM+Ll7bZrf|dP$}HOOG%r24vg7RJ z?qSBIv|tD`d%bsnY1|0}S!mvX@@{GH=UWKM8b&9jkBbdLc9@2$C7GhqOeeKxD)ysx)&58{GiGGLd6@VGw-?ePtq!UGg&jyneboiz1=gWKnoru!#7vQ{b{( zv?UMD1&DAN9G+knQ#@g=x>R_ziI#~YovTB%-sI+?f~CrcKk6*-BexcRoRA8aYg|RV~;Nr z@{NvQ0(G4q4-#i*ZA&11+^{Ln%)TEpX(Por3?-qZE*JeyTwtt3L(vZ9|w)m}Oz zN3B7XLDih-PAJZ%$%p49akk(@e3WQ$4XdZTnAXB1wY{30p@u{mFd zML-CY{p6Q)_Sm5QEvNJ}Ad|LEL)A*VfFbYKdE<`Ht8|~PayoJ4M3_@+{r&wDM_Gpt zi9EaSAJeg*2|LG9ddop<=f5OF8|I}GVegEyp$|_PB>A7FZvX3f(*MDoC>t!R$4b=m ziWhOfB`Dta|NnUn&RKpau;OEYy&^skG6|g8D3nKn1Xf9zpftGs1or>UsoDSfgc_bh zfDCN+X6XR2$VB2%b%T5d1;F@46sG5ZOGN-i&RYSZqvj*RTR4D}X+tTfW)P$60jyx^ z`5cF$mZL4m3p;RwL^1GT*}I`Q9!K+X=)aj+;mvfq3P9sl0N*G(Ygh`rc5Vi-%x+L7 zD8t|Y-*Z+p&N`R_(Hjrgjb4TOVf2cXuOQm1m^f%XU1Poi7#)7R#FgFjo3+@rI{rz0 zIbhrenEL&Xd5)}0c8UR z1Y&emc;`T9xd-BPQy`Rbxn&OF=!XP z8b*TmR9O6*|0$kk=uZu_{#;R~)C^*$$`a~{2I!%(2Ffi5MeHEe%l+d9U}=Zj-1+n7 zH1{Bu-q&-^r2j%}z^T4Ztb**R03`^N9tDncf*t*Dl+L=B_0#i7kQ8KWoFq^&_4@U~ z{BO$^9wC!j#i@XD;)GGgsv#p7JR=->z_yn!?h&T5_9Z#pkk^4S=taYX#X)4&IAfD_ z2#{>=hHLw&u93tL+n0AJctW(3*_{A>njbEAt$>h59JAy8Y{O?nM*X9o0&`B>7dId> zV6ns;+75!&3?zN<=g*H!(5bvMD8=X%wJpDn{twO;@nuAWl_a$aESDOH52n6X6_|mo zibvp(6PS)R9;F2*-1-6svImF}G+`=`@^3ztFYSI!=4J)r+#hbcrfnOKKFZ>K*sE2q z$E&Gi%65~PKL<(dezUyPcfBNrY$VT_m!aJ9(R5vm*H`d4k`Vkd2Qh8C8c&w8%S*8O zdbE_;Us?;F53XO%7UO8QyarROAFED_62zvDB(TwpKGFPvlt$LY(l@BY9y%;gQ1YlL zSVLRUW-#q%(qt zD!VCtQ~Z{-+yF!wYa_MKz{*7Wxo5|m|B}yr5J;&v*M;#CQFFdvcJ!r2&VD*gaaMl< zxK!nmyw;vYe)35na7=@`^N+1xq=VpgA-pzlXU~{cV5_quFPhjGz|s=nQdkqGOm0r zx6J$$B2>5&A$!)&?Xt8Zz^3(wjQtp>GxfQF{!Y}OpY3UZV|5CJT6u)fVgep1BLU>cipW6oWznOXd94JT1?>E-AW=~el&nKVe#>Y!n7AlkO`rp3+Z zXxP!UkXxjeDhbb^%?{2?-PAhFsJ|$tfw3q%WHkFcHrhn@wn}(pl?rE3EN8DQ_OdlI zqeByQz=?ULa{lOVf{qcKJ+y+5Wfs(Pc9kP%t8lsZDp*?bRNgv82~6Y1j!Q#FEw~{8 z1q{J5#(}iCG&hNGxqL-aEx5nwo0agf&Y*j;QeNVJK-zTG4+R}BhItI9^m{AL?mrGZ zAuMH`zY89QM#3gT?y`U)XAF{gs@AoOKj`udH!oe?rR; zr}>e5{zJaRLeNXCJ*af`hJ4sA{w5y!6C2IeQmP5qJnit-Xtp|qHa!WDM?AXUk^5%m z4Ft#78A?RgR>FveeW|ZT%lR+8Jn4kQCDc}3u29Z%QN=_9Y;FIcb+8G?6b><72=Xr> zrmUBFnND4w7Y{fhiBd#|Mh-zHR26|YRF=c0#_oar`l zUF?cG)-DoPD%^=1KnQuY(Vn<(mPyH#k5Sfl2`?MG)E*B;h@vGcou#d%FA8wAs}kIh z>^d@*ov!BgmA1?;k-_4EQ@$m+JkSZ;STYVxX!PSf98Ni4_`*>N?QeDJBb*F)-)TQHcQ{Qv+ zMZt)C}tD8WA_dSm}-kzVX~*hp{JUH31OQr}-}9ECT&r{b{sx+O$M5 z{Hb?ZS85H|<$E8$CV&6#{8LfzrS=Tkq9i$`Js_9y<6JfEsEcj*{V8k1Jc>$%79Tq{ z`A-Fz$W-Z)3MEho7|uKVLEDH!*HR3^HUYD{Z5A-I*H8^MRk)J>*RoFe$fy56Om36? zVHYLcCc%5HYjsB!?OKgOH8Z8pb`o0l_D}Oi8M~-Ho5=-IT)#Jl1Nr)gZqmx{Y|3M^N+Eyxp18)K z&?Sh-V^MkH5qz}BuulEJP9F2_=Mk(S3}J3yr+5JMe@%GQ4+sYav_CTY<)EZ&3XiWV zcw9p;;k&yVVegXe5@FUdhS8UpiO-`BmawAr#A7-+9UPKbiW&6(&vr5g@~bd7X?^Wg z9^;{in8{+8ii4xKtuu@2yusXMwo?>2JN1$!oAJ^#Hw`OxMW0mkYG{M1YdW4)KQ- zU>vbsbO5yBs_(mz0=X;%kYwlf<7s4an`-g^=$q={0wIm>$EVh5#v}$&Hc#?XdqCsX zTDVj&RLjEwMi>4GTou*!R~cR_;iM6@mi-X`NQm6s14KyOi33Fd8miZZe)>VK%Q0mL z%V^3m#57p$7J*;C0T?7@Yox0Gu=Q@>T#@YCrLF8yi|$>O3A5i zGf3QbgVja(w|On@!Zmb;y=k5Yhgz%|{#N4f8;UuwCt^pJ0LiHR}V{da9yAC z>MhiaIM(F4GzPC9_{H`c2f)1`b2v<~05s$gGks|+T@eBS;vPsmbrjN>1|GgF=$x~Z z@&yChYmNFDa_aR#KU6o6@%0Y9*ZSk}p&Ym;ARyO4>=w~|j zONv?ym9O4=ByoTPx&bhzn;Z}571>aWJ$Q6#7XZZej7r`c)j9z;2@hQGYchTZophVb z`zf0XVBSPPHV|4Be~Raa@HbR*wPP~a;hGBd?B&_Qy-qr@>dS zigSVP3cXjks3ptyiYW>sc~C*qz_QkdA1nIpgW)wT^UtQq{^WD82tm^i+%lmNjer8a z1TyrHIf4Hq{cY@>wmjlzTjv?PLlk1pJO09#_E8IG_6%rR5#{a5d2Oj=KlXj6oZ zUzaSg*Wz&F$LjY);6}eHL!IA%3)D-Yn(`G8NGz^9jtf=n^RT@W|K`)@=yaAHxzDm4==nJ0u!p<_m;i$CD^M=s~Sa$ridY zhkIo9J?y>yWsz(K^wfq0dYeQP4^z#A>>dn(!qe6_1@9i{Z3Oe9-)kCDI##7Q1o$b8G%n+WR^Kai zG27j)!xHCm0%;6vz-`J;rT{H2e_E3sR^j<*)9up_U^ZwwZW)>Es0 zwECD(lFX{oq3A>LPc4#Su}qqEtxYECd0_6NMKQTD%08? zeeEM@Hg}{2T1*4r91q)@`IF(iH8c>egzoUb=E1hr^S?Lw?MBnN zR&(XDZn-Y)Y6cp(T0qOydBTVHYvUl(vFZc+``)qPSRxZN0t>8+n`5pG*TKP`X_=|X z&hoJo#C0+5Z{Lh<%hCLn4nylT%jS6vUp1g%=aH%${n4RFD>uvO`G!Qsblkt*i^Q)Q z^dNoLnYB0$o4Rs(TQG`VnbDiM_*wZt)pZFJGZQZ=_!~uY+NNAJIBv#bmf2gSj4!E3 zBF4zj@ddw0EL${+OXiHdt&RH13w|eaYeUsh$+Di_uE^?gue#Hu=8uFeru95qzIO`k z4Tx8~$JODBBhME=%B(DF&zH+@P3_m>G9YEH!{O4&E69&-m;}nx*Wbk?hnM*%KE~(a z%Pt_NQ7#~!8S>F?!y47XfDy8Ag)k-{H&>n(^!`p@qQ6Y%u~zyMhSe9Z2pXsb-Py?c z{8tjxo-&Row5~lS9c#x&{Si(vIvA2{)i*W+0EcC^)VV_6LP7Mi1%jc17iZFy_z&3C zSPC7AI6J(CB5*zU&-4=K2a0nO)Mx+G-5H0`E~7bCb&tQuN=v4b8K z&u9XGR{DngW%jY(UF(jA=*9)qD~Q+l&$9BS4ZO$|giy+Ks%-t!Zdx>%mw1C6k1xGc z*YACkdDN7Re=Q~7m0GD1#PH%WNsD-vr#jZq8){(FHf299gVH!;R$6miFQg@e3V+wJ z1raUDkTIt*8$(D!=ou+{S+{OQ_;^uA_^Nq>9#>|@ zcnx(NiqUyK{)fkcbT5H!MC=n3F`>LE6EWKM=T?;cF`ANPW%8Pt6woZA;QYkOoKBmuAXH`(7=6v2cdjZ}w^JV=MpMPMB@n2TPcOh2#%7#WK<(;ny zxjWlV+X7Lk^3#S)qPJpM{GbQTUt`X4IdT;sPaZ&%l69>VLNY1F1J-wax6{v1}SGLfJDZ?TEr&vB( zXffav1E`PC94P--fLdWd5ge!uTPg4sTH1c+f2nMsj9823mK(4Z-T`-`M7rq}xD6IT z6rHjU8y+n+K!Sb;LXnb2An3OOlDSx(Ke^?hs7?=%z_r)*pg#3~tqyRM3Xj;;cHzHq z6kyQcLE*MSr%v!(bU2)c&eXK1jKSJ72Zxnlf-Th6E;a)92?=yy3Yigvf81v%94j6} z2N9@=?7*7aL?C4!q_l~_sPygSp4+fVHIEMm?b%kRRG7<&sgLYcdMId?q8zfrh>vHtPH?aQf320aE6m-mn z`M9beZjOjI7Q~%|p!VDoAh+vp|Ng9}B5DHAaXE}C2prS_kv3e2Or?WC=mABX)X*~s zWC;BzAA{xDpxcgOWxF2EYzRv&qcZ{ zwDz8J{A-Dv0a^(un9~Gc0Q>uBlO`9S?D zsJ;Qxj^q$+Qrdf*902wr?%tFjCVUCjBgogRY@B83KtE^zs#qknr7u@9TtJ?~zp

K~ui@_E-S~!|_1s%p6C!8$Ew0d}Vr0iL4$43vvYHGVzg z28eh#v{Q1-0rqK$O}SHWBN}V31*FM5=rEjcC^%p5e0|R`1sPL7$>3RmY!~QUl$6Oi zzEI{yQD<;Y!&7C9)_cI%S%#7(M_~*N(~g>dPEpT_Tx2P3^)l)hgUAzi9PM^&P*(u? zq|5E$w(R?!IxdSROpiC8oL9#NF8=p-P|Vi>UXKgg`U4ITNJ#}-Y>`j8kbP) zO5}OvC)gI0ZDa^w3?R49m;*XSH%L*hy`_(b^>Qi-vTp*OgA3VwMd-*bsblvD$b9K% z=?r3)_VunzY!i4T7%P!qC>L7}7i^gSH_r+Z!Tl%yEUT9vfG&nd@n4~uV2D=Af*GJ{ zADd;9IhghnI+>*8{XqXs4hZ7LhV6{yB`hbSORE%2cE~^-z87gbT8UFk@$`0dT;+J| zHIO8OfFid)2(FF^yd;ml6A}+W{TGnFGR?%-$spnkMO+K_7J<>2^1$c{NgWt3XV=a+ z^{E6n|oc}1j+T31t?j0O~>;Q4w|Y63{^4fFf1Te0C|Tg2noe+FR-}F zy262z(=vNb8@!fa4P|csEGa7=0nvukuzaUy$h{3Z`JhsnL3*mnxCi*?+O>bc=}#X{ zN_gVg;sFB@D>mW1#oqaa8;)Fkb-G82E~j)wG|rShIR~=RHC+;VVDhu4A(tz4n+gB) zy#z7bmkuwWCUT1_X9`bOf_D;#F=3u7m z{c*aQ@U)YXH1l-}G)|>CvoPFla2U-aG#s)=C@X{pVDv=QByavUeDUEE!{|@s7Q8;Wz-`T0|TvW z+3R_IcY=%x_6o3=HLPiA40bW56NQDdc!5AAUd4PXBz~)Qce$cz2%W&?zy21edY{=W zVH*hHjd>u@;WpfK&1ptsq@hjoP2&h4$Um{AgK1j>`pES%I=#RF5OHCuf}uBsWDcAi zKtB&^er-cztET_Ip%)rx@=0$4b%|)}SvzpgEu@sL{sMKd2aC1h5qh8EqjwS@{JQd; zP#wqi|7z~a!=c>!K9Q89lO-e~WZ%VjvLCVy8lol|$5O^pA*YZfq#h#MNFwXVl%*W| zv1V&5k!^^gkhNsr!t?z(@AF*GpYQv}xvuwLu1s^!eSd$S?`NA7Z!5QRPTSpRRb$Fs z&kF*Jc(`;93h*4%Ad(%-MfX_Z&kciHp+CHZL&d~Nr?&NK^r1n}_C-~B(e>nowlo)I zGIL(Sy1$VR^a7E)JedrI(%PFV$?FE3$Ost`Ad^{tBpG1{mx1If`XZSfS2ggbQU8}p z9T!|A;L+W*&r`Xdkm#cjiF$U?=|vo}7y~6oe*bka9$u%%6S4`8yAKeTyxcE&6eMTN z#N+- zm>?Dxm?)Ny72Ygh=gy`1VUtzmbHNk&8PWy&QF`PDjdo=#sV}c%@!qq{zDH?+Z=(g% zp{_{Kc-rq7??Tng!Um3D(c)XI-l42c<1GmBth}!Kk)@Ie|Gnx2OU=H^4CDUS1KD`Z ztOD7bF~@-co&0wurI^`Vq_-W22fe*!KL@!B3fec6^Dd=Mu&Aq`h~L8+>2GJ0$NXsH z%jD|p_+=NrXRf~a%9rAqV#aoad6ikF9uNF-U zs$^m;VCW?Q7H+WzG2l^g*w6Db3L4%@J7V}FZ&z-Z?d8W0d6H{P+`J(senH@ z!`KA)^`rQBGyoR<0BiR>@U6gYXB_!xX$}j6x)=5ru1HlsF zXtR55XAlmZE$%O{#T!73yg}x9)S4m(^&nK$RJ_ z!bj0D1WAT)4YBC#d4@iCir13y6Uuhl#oji@P)VK53Xm|CQk$rcr4W#0hW3i?z@M7J zKR>CkvHBmsz-0c0gJ39c`G<~>-MwO13ioEwN`0EXz`%t0*44WdE za|W-MByoSldiQwxu;y?Vu>LhUWTBx{WCjv^Chg)&^^34?G{isqP?AjXnddc^zkF|<6+alpSR{j2Tv;>Dxl{aEfy@Guba_Yc0yMRIh>rk zv$06|my{MzyXib@2FZ&*l?F;E9=HGd)%QV?@GQ=WctRKi(qiu?+E?>bqsWKIi^SzSE@*yp3cSb5FC1+V(hX|G< zb5$_gGvlYg34RY|k$DU4eD>u?@}a?YZnLw!pY$2+a?U6_ZT=^LWr1NyUvpn3e&v%PV;W^bnRjx?n z+@&u-hq?x6l;sT^0#-!-J+Rxt=UQy^3O8GKwZ{=TH=+6Q01|v)DvIzr5=h69B!D7# zqnM6yPiz$?mRD=nW{PVqY2tMa1;1_+UAA}%yeM+hJ-9b`2gnTK!3UN}P*HaV$s3zIBk)>mg)iyxrDoO;C3OK${44Lu^L^5f&{FRno(+AyuT(Gcme}%bq1NL>UI|VQ@ z6y0Fh{|5By1MNXXbwli!qg!lI9poWY{2Ea=fqKGFQJWmZy(DHhr$aViU1?k;dwWB{ z?0)pbBZ*mcGA-mqR#*URfb+{)lc+qbq-g%hY%@e4Ep7}RsN#n}i36FVYZ|1*RwzWR zdIjVC9>{8q99B4%^)}##PVoWS{|#ilh{5{m&e+Fu@ItHzm7`H)^T?_WW#Cb>ZaT-T z%Ak>*f$_;^NlZ=hq7Djnz5=&vL&q1z{Uc&+6UHks?gOxQI7o`kSWTeb4C&|K>T|Cf z;Xr}-Gc~M{93x;T7Dt`0dXEOdq0Dhzd}|hL^{FqEbrE%{FC192jCk zp%GX0vRUOmntZc-`BmX#)U%Vh`+W4)560uY%$E0JsCp`r#mU z(5wO+E6K!zEY!O~IA??SZCNN0kY@&nG+@udGov84pA!F1#P^kCoXeT;Vah3o1!UAO zKKR>iO`RVY3Ap5M1VDwPeW&i-pU~PEuI2C&~h;S$ZeA-+a?68Rvog+JdCLT7cBQcs4`4NyF{$)KUzt+tB9Y$2{OIbY2X59Cu z2ZH(ddkvXdE_yU_M&#?_RY)v0CsA?lFiY^UJ(b9&%zqSOr&YUT4U|L6(;0sX1k#KR zw#l2SpF`R0JC%HwCNI$cK#65!;f-gRScdyEVpAurN&d4MM|jsAC%B9 z`0ubY421!m*|6CNr}fd&NC)ebiaTZ~8(PEQrdUAg2_6GF`UCWjb{Gi4BYR0^6{eY$ zR3PA}(FM@+zal&mKAsCa#p0M1h&$m?41v z;4VqAEC4W0qFJWuAjB6Un&oyBS$O2^wC+6G#Zv1i4DAUETbneZ?OOF8@k8I*#*`OC zIW0)5Q?vrYHK0qW{y@B*U-1BDvK&_l|5XqF6?FmHVVdqnOm%|lO66()c46AJ^k#VW ztVdUpLGAYV2)lW~q{j?g@47`Ls6_o*4J1$aIE2uR&;0(6?`42uQAF1p2GxWOW}Fa_ zdS}6~GTX}5yVq!94a#TVw!?Zce`da_9%9gk2{k}bf6X>c!A}j{2=I~FQWqD-Fcnm- z;EQLD-|W=(s1no-vxJ`y0Bo_Yn<%eFMDI|RVcd)4#jyY!ZH|iE4+Jj|#4Z11i%k-+ zxC4)Wn)k_%(*~}B279rk8m|uJDFJWl)2M*&ch(*Nly3iWf8IsvNn&~W^5LRu0r1p# z*mm)=X9;(CS|7kI88|RXD^Ra?#omWunAhU!oRZaedW*m4M9kL#b7DZ2dRJkhe#8;^ zCP4XC;#gJ8r>RljR_W4)7$?J9CIS}tXkuN)@_nm*+!C}TcCDwt#c+sFogx{!%){-( zZXrUHHw)xv>k=`{%U+g^Oz?c?ByO?6B3?}tsuR5Nh0mqurt zQ;b*Axw|r0QcmIZ4Y6jgsmwz5`vtYeAg};lkyM<8O9PltDwwJQ^`7oRs_` zo8W|wEHdcIOAn;$$p$MC9pv+shx0i>Nd6`r?;XAEhM!`u6d-QELgLppGLl3gZluDK zW1!pwc57UhhOvu=$vi2GE8|k>HPUu!k2QzU=4km%LWgjzVDD@0Jr^GGDobojE0On2 z#gK)16K@(H__XeCji_1PRasjDCxXGKRH=P)ey108;peDPUDC*N?gL+@v?z%>Tk3J6 z?U>K&V`s800wPkH8&FZY#eL%##%#m1B*NWj8}=+xoRzIa@XP6v@x<3FzUhsvmLHKH%;1NY}HtG{eeAldit9I3Kk?svrqsCwnv z#{ST&>9ud+nG{aN2E9$wA~U)vFE5&0LX`Z-#U+cjvk@|E9K8L{1%;cV`YPqm>Jo#zXZGw$42{r+|qcr%f$hC zfu#Q|DpI(>-5fdEDlbGQv*d63vUN4o>#}os*4GYrCmalqgwGdbC*%s(ipQ5UKcJA) zyd#5duKoB-+LM)pqklT;A;L=P8NX@dXxrHmuSqiUtc>%DO+Je&nCTL_$o`I}+t<8K zE6C#A-Pq~ix(F@&HC?QtZd#*3GgYhuCTW0)-fu6GZrV=>u=fsv& zIB-EM-!AFekJ)a#<#Od|F)PhGBz|1XnklSUj<{#RI_#H`ctFfccbcWrYFRJ$kq&`=fb2m-S;z@fnL9n8C}_N{Vn> z-FU}?eR?iHq*w}cuvq(6abo(?N#T)nS&8oUPBopEqS9APq;(~a?8OC(1PkH`iAKS} zU*G^ZYO$GEk|k<#v94CJDA8P%Y`%>WLhH)M4s6XjzsG=iZ`3A%k&&vz1C9kZXQE?u zACgeM%WkP+^xJ+*DiV4etnJG^azE{E%N_{^W*y)PY{cJ|0U8*k(`C#na`|PbQ%#Sq zUS4#>fbI18OH2P88}gWSE_QSg!r2bUYTS>%u;H$7*sN>#kMzUl9@to;7A4K`redrf zUC7oAKFp^!m6rL53as3mm_L4h;uBAD$=W*)*zFIB7iQI*>Tc_-S%m#BkC_p(oz=C# zmp4}QyXO#0mj&(yg$Eb0TM86r`_4Zc@!Y|!a6qUA(0FphtN3ZsH1?TO459C@C(p!~ zFn?7aU+a8Ms!j0;pK5O9kgL44UD-6g_UsjF+g+nuL1B}AG87`TfX7PU&+V6S)+4_% zOn*!l5?>G6t^(qIa6!}uuZyzZUytlV9fpcr`Qo{Kp(9~?RPV|xsxDxP4$coyYtN21 z@x4H6E26pdTyN;^%RzboG40Ojdp`Knaod%Qt8MQuN=O`LHp1vz^4-utF*XTe$-|A7 zpUI4FjQewqD@3&qdj%@2I{E*?-RbKKjbV-2KDW)wYkh>rW9x49M}?_9UeB~<@BAh4 zeFedkagA>jY;W=b0?bL=iQ%rP7zcLhazjpsw_f9Zl8%jJTVAVk^}|}Mq>sYFjbQJx zFz11>&H8e(eXs_Z@4jQ`tbNXdjC*m~knM`KbAML6ljn}g2f zvJc7jUkSE};+$(-eK=DS|4WAJrh0mZl(w99c~-)6RZU zR8NxLBae@O&2%Nh-~Je{za|?rW$3e*vMkG9`5}rgG<@VvtrMSW%!*djBh^uQ6rOyM zgny-siy|7jON{fjms)1Mk6mbg(6UrdhIUJ{3v(SI@C7+^hI*#I J=Ihu6{|hHvmSX?_ literal 26383 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 AqXQU=`C|4>Q%JO4sYJ(Ul|Cf7+Swjkv2 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 bool: @@ -19,7 +20,31 @@ def _autodetect() -> str: return "local" # surfaces the friendly install hint on construction -def load_backend(spec, **kwargs) -> Backend: +# Constructing a backend loads model weights (~0.5 GB for the local ViT) and is the +# dominant setup cost. Cache by construction identity so the CLI's per-file loop and +# repeated Pipe()/scan() calls reuse one loaded model instead of reloading it each time. +# Backends are stateless after construction, so sharing one instance is safe; the lock +# keeps a concurrent first-miss from loading the same model twice. +_cache: dict[tuple, Backend] = {} +_cache_lock = threading.Lock() + + +def _construct(spec: str, kwargs: dict) -> Backend: + if spec == "local": + from .local import LocalBackend + + return LocalBackend(model=kwargs.get("model")) + if spec == "aws": + from .aws import RekognitionBackend + + return RekognitionBackend(region=kwargs.get("region", "us-east-1")) + + raise ValueError(f"Unknown backend: {spec!r}. Use 'local', 'aws', or 'local:'.") + + +def load_backend(spec, *, cache: bool = True, **kwargs) -> Backend: + # An already-built backend (e.g. a test fake or a user-constructed instance) is + # passed straight through, never cached. if isinstance(spec, Backend): return spec @@ -31,13 +56,27 @@ def load_backend(spec, **kwargs) -> Backend: kwargs.setdefault("model", spec.split(":", 1)[1]) spec = "local" - if spec == "local": - from .local import LocalBackend + if not cache: + return _construct(spec, kwargs) - return LocalBackend(model=kwargs.get("model")) - if spec == "aws": - from .aws import RekognitionBackend + # Key on exactly the inputs that change the constructed backend: load_backend only + # varies model (local) and region (aws). Custom nsfw_labels/label_floor are reachable + # only by constructing a backend directly, which bypasses this cache. + key = (spec, kwargs.get("model"), kwargs.get("region", "us-east-1")) + cached = _cache.get(key) + if cached is not None: + return cached + with _cache_lock: + cached = _cache.get(key) + if cached is not None: + return cached + backend = _construct(spec, kwargs) # construction failures are not cached + _cache[key] = backend + return backend - return RekognitionBackend(region=kwargs.get("region", "us-east-1")) - raise ValueError(f"Unknown backend: {spec!r}. Use 'local', 'aws', or 'local:'.") +def clear_backend_cache() -> None: + """Drop cached backends so their model weights can be garbage-collected. Useful in + tests or to reclaim memory after a batch of scans.""" + with _cache_lock: + _cache.clear() diff --git a/src/pyframe/cli.py b/src/pyframe/cli.py index 162a9a3..4cd652b 100644 --- a/src/pyframe/cli.py +++ b/src/pyframe/cli.py @@ -61,30 +61,40 @@ def build_parser() -> argparse.ArgumentParser: def main() -> int: - from .pipe import scan + from .config import Config, PrescreenConfig + from .scanner import Scanner args = build_parser().parse_args() + + config = Config( + backend=args.backend, + model=args.model, + region=args.region, + max_frames=args.max_frames, + min_confidence=args.min_confidence, + sampler=args.sampler, + use_merged=args.use_merged, + frames_per_batch=args.frames_per_batch, + prescreen=PrescreenConfig( + enabled=args.prescreen, + screen_fps=args.screen_fps, + escalate_threshold=args.escalate_threshold, + max_escalations=args.max_escalations, + ), + ) + + # Build the backend (and load the model) once, then reuse it for every file, + # instead of reloading the model per path. + try: + scanner = Scanner.from_config(config) + except BackendUnavailableError as exc: + print(exc, file=sys.stderr) + return 3 + rc = 0 for path in args.paths: try: - result = scan( - path, - backend=args.backend, - model=args.model, - region=args.region, - max_frames=args.max_frames, - min_confidence=args.min_confidence, - sampler=args.sampler, - use_merged=args.use_merged, - frames_per_batch=args.frames_per_batch, - prescreen=args.prescreen, - escalate_threshold=args.escalate_threshold, - max_escalations=args.max_escalations, - screen_fps=args.screen_fps, - ) - except BackendUnavailableError as exc: - print(exc, file=sys.stderr) - return 3 + result = scanner.scan(path) except (UnsupportedMediaError, MediaDecodeError, FileNotFoundError, PyFrameError) as exc: print(f"error: {exc}", file=sys.stderr) rc = max(rc, 2) From 10c52202df583c74db85a38c819ebff2df06b3a1 Mon Sep 17 00:00:00 2001 From: Ellis Hewes Date: Sun, 7 Jun 2026 21:05:44 +0100 Subject: [PATCH 3/3] Bump version to 0.3.0 Update project version in pyproject.toml and synchronize the __version__ fallback values in src/pyframe/__init__.py to 0.3.0 in preparation for a new release. --- pyproject.toml | 2 +- src/pyframe/__init__.py | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 369c918..10cb848 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "hatchling.build" [project] name = "pyframe-gif-video-image-moderation" -version = "0.2.0" +version = "0.3.0" description = "Two-stage NSFW moderation for GIFs, videos, and images via local HuggingFace models and/or AWS Rekognition." readme = "README.md" requires-python = ">=3.10" diff --git a/src/pyframe/__init__.py b/src/pyframe/__init__.py index fc78471..feef6ea 100644 --- a/src/pyframe/__init__.py +++ b/src/pyframe/__init__.py @@ -20,9 +20,9 @@ try: __version__ = version("pyframe-gif-video-image-moderation") except PackageNotFoundError: - __version__ = "0.2.0" + __version__ = "0.3.0" except Exception: - __version__ = "0.2.0" + __version__ = "0.3.0" __all__ = [ "Pipe",