mediarag 0.1.14__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
mediarag/__init__.py ADDED
@@ -0,0 +1,140 @@
1
+ """MediaRAG plugin entry — agent-plugins 1.0, loaded as hermes_plugins.<slug>.
2
+
3
+ register(ctx) is the real loader contract: ctx.register_tool(name, toolset,
4
+ schema={"type":"object","parameters":…}, handler, description). Handlers
5
+ absorb dispatch kwargs themselves (**_x — Hermes passes task_id + extras).
6
+
7
+ Env contract: register() checks that onnxruntime, numpy, and transformers
8
+ import, and refuses to register tools when they do not. Install them with
9
+ `pip install -r requirements.txt`. The model snapshot is loaded from the
10
+ Hugging Face cache and is not downloaded on startup.
11
+ """
12
+ from __future__ import annotations
13
+
14
+ import os
15
+ import sys
16
+
17
+ _PLUGIN_DIR = os.path.dirname(os.path.abspath(__file__))
18
+
19
+ def _config_root() -> str:
20
+ """Corpus root from mediarag-config.yaml in the plugin dir (named config, rag-config.yaml pattern).
21
+ # ponytail: flat `root:` line parse — no yaml dependency on the stdio spawn path;
22
+ # switch to yaml.safe_load if the config ever grows beyond one key.
23
+ """
24
+ path = os.path.join(_PLUGIN_DIR, "mediarag-config.yaml")
25
+ try:
26
+ with open(path, encoding="utf-8") as fh:
27
+ for line in fh:
28
+ if line.strip().startswith("root:"):
29
+ return line.split(":", 1)[1].strip().strip("'\"")
30
+ except OSError:
31
+ pass
32
+ return ""
33
+
34
+
35
+ TOOL_ROOT = _config_root() or os.environ.get("MEDIARAG_ROOT") or os.path.expanduser("~/.mediarag")
36
+
37
+ PYVER = f"python{sys.version_info.major}.{sys.version_info.minor}"
38
+
39
+ BOOTSTRAP_HINT = (
40
+ "MediaRAG engine deps missing — pip install -r requirements.txt "
41
+ "from the plugin directory"
42
+ )
43
+
44
+
45
+ def _engine_importable() -> bool:
46
+ try:
47
+ import onnxruntime # noqa: F401
48
+ import numpy # noqa: F401
49
+ import transformers # noqa: F401
50
+ # The processor chain constructs a video processor needing BOTH torch and
51
+ # torchvision; without them every embed raises ImportError at call time
52
+ # while the plugin still advertises five working tools.
53
+ import torch # noqa: F401
54
+ import torchvision # noqa: F401
55
+ return True
56
+ except Exception:
57
+ return False
58
+
59
+
60
+ def register(ctx) -> None:
61
+ """MediaRAG native tool registration (Hermes loader contract)."""
62
+ # Deps arrive with the plugin install (python_dependencies / requirements.txt).
63
+ # Nothing here edits sys.path or the process environment: this runs inside the
64
+ # Hermes gateway, where either would leak into every other plugin.
65
+ if not _engine_importable():
66
+ print(f"[mediarag] {BOOTSTRAP_HINT}")
67
+ return
68
+
69
+ import json as _json
70
+
71
+ from .core import tools as _tools
72
+
73
+ def _json_result(handler):
74
+ """Registry dispatch contract: handler receives ONE positional arg (the
75
+ args dict, task_id-style extras included) and returns a str (dict
76
+ results are rejected — the workspace plugin's lifetime bug)."""
77
+ def wrapped(args=None, **kwargs):
78
+ a = dict(args or {})
79
+ a.update(kwargs)
80
+ return _json.dumps(handler(**a))
81
+ return wrapped
82
+
83
+ def reg(name, desc, handler, params, required):
84
+ ctx.register_tool(
85
+ name=name,
86
+ toolset="mediarag",
87
+ schema={"type": "object", "parameters": {
88
+ "type": "object", "properties": params, "required": required}},
89
+ handler=_json_result(handler),
90
+ check_fn=lambda: True,
91
+ description=desc,
92
+ )
93
+
94
+ reg(
95
+ "mediarag_search",
96
+ "Search documents and media (hybrid: semantic + keyword/BM25): text, images, "
97
+ "audio. Ranked hits with snippets, scores, spans, and image thumbnails.",
98
+ _tools.tool_search,
99
+ {
100
+ "query": {"type": "string", "description": "natural-language query (or keyword terms)"},
101
+ "k": {"type": "integer", "default": 5},
102
+ "kind": {"type": "string", "enum": ["text", "image", "audio"]},
103
+ "include_thumbnails": {"type": "boolean", "default": True},
104
+ },
105
+ ["query"],
106
+ )
107
+ reg(
108
+ "mediarag_add_note",
109
+ "Store a text note into the MediaRAG corpus (searchable afterwards).",
110
+ _tools.tool_add_note,
111
+ {"text": {"type": "string", "description": "note text"},
112
+ },
113
+ ["text"],
114
+ )
115
+ reg(
116
+ "mediarag_ingest_dir",
117
+ "Bulk-index a directory of documents and media (pdf, office, notes, "
118
+ "subtitles, images, audio). Per-file errors are collected, never fatal.",
119
+ _tools.tool_ingest_dir,
120
+ {"path": {"type": "string"},
121
+ },
122
+ ["path"],
123
+ )
124
+ reg(
125
+ "mediarag_add_source",
126
+ "Add one source: local file/dir, http(s) URL, or adapter scheme URI "
127
+ "(raindrop://, pcloud://, obsidian://, rclone://backend:path).",
128
+ _tools.tool_add_source,
129
+ {"source": {"type": "string"},
130
+ },
131
+ ["source"],
132
+ )
133
+ reg(
134
+ "mediarag_status",
135
+ "MediaRAG corpus status: row counts by kind, corpus root, adapters.",
136
+ _tools.tool_status,
137
+ {},
138
+ [],
139
+ )
140
+ print(f"[mediarag] 5 tools registered (corpus root: {TOOL_ROOT})")
File without changes
@@ -0,0 +1,48 @@
1
+ #!/usr/bin/env python3
2
+ """MediaRAG source adapter: dropbox://<path-in-dropbox> — Dropbox assets via the
3
+ desktop client's local folder (or any local mirror).
4
+
5
+ Env: DROPBOX_DIR (default: "~/Dropbox"). Item shape: {"uri", "path"} — the
6
+ indexer decodes/embeds real files itself, so this adapter deliberately does not
7
+ carry its own extension list: `ing.index_file` knows which formats it can read
8
+ (pdf/epub/docx via pandoc, pdf via pdftotext, images, audio) and returns an
9
+ `unsupported_ext` skip for the rest. Duplicating that list here is how Dropbox
10
+ PDFs would go missing without a word.
11
+ """
12
+ import json
13
+ import os
14
+ import sys
15
+
16
+ # Dropbox's own bookkeeping, plus the usual VCS/mount debris. Anything else is
17
+ # the indexer's call.
18
+ SKIP_DIRS = {".dropbox", ".dropbox.cache", ".git", ".obsidian", ".trash"}
19
+
20
+
21
+ def main():
22
+ if len(sys.argv) != 3 or sys.argv[1] != "fetch":
23
+ print("usage: dropbox fetch <path-in-dropbox> (e.g. dropbox://Papers)", file=sys.stderr)
24
+ return 2
25
+ base = os.path.expanduser(os.environ.get("DROPBOX_DIR", "~/Dropbox"))
26
+ sub = sys.argv[2].lstrip("/")
27
+ root = os.path.join(base, sub)
28
+ if not os.path.isdir(root):
29
+ print(f"not a directory: {root} (set DROPBOX_DIR if your Dropbox lives elsewhere)",
30
+ file=sys.stderr)
31
+ return 1
32
+ total = 0
33
+ for dirpath, dirnames, filenames in os.walk(root):
34
+ dirnames[:] = [d for d in dirnames if not d.startswith(".") and d not in SKIP_DIRS]
35
+ for fn in sorted(filenames):
36
+ if fn.startswith(".") or fn.endswith(".dropbox.attr"):
37
+ continue
38
+ p = os.path.join(dirpath, fn)
39
+ print(json.dumps({"uri": f"dropbox://{os.path.relpath(p, base)}", "path": p},
40
+ ensure_ascii=False))
41
+ total += 1
42
+ print(f"emitted {total} files under {sub or '/'} (indexer decides what it can read)",
43
+ file=sys.stderr)
44
+ return 0
45
+
46
+
47
+ if __name__ == "__main__":
48
+ sys.exit(main())
@@ -0,0 +1,53 @@
1
+ #!/usr/bin/env python3
2
+ """MediaRAG source adapter: obsidian://<vault-path> — an Obsidian vault.
3
+
4
+ Emits {"uri", "path"} items for indexable files under the vault, skipping
5
+ Obsidian's own config/noise (.obsidian, .trash, .DS_Store, hidden dirs, .tmp).
6
+ Vault resolution order for the arg (the part after obsidian://):
7
+ 1. absolute or ~-expanded path as given
8
+ 2. $OBSIDIAN_VAULT + arg
9
+ Env: OBSIDIAN_VAULT (optional default vault). Indexable extensions = the server's.
10
+ """
11
+ import json
12
+ import os
13
+ import sys
14
+
15
+ SKIP_DIRS = {".obsidian", ".trash", ".git", ".smart-env", ".DS_Store"}
16
+ SKIP_EXT = {".tmp", ".bak", ".partial"}
17
+
18
+
19
+ def main():
20
+ if len(sys.argv) != 3 or sys.argv[1] != "fetch":
21
+ print("usage: obsidian fetch <vault-path> (e.g. obsidian://~/notes/vault)", file=sys.stderr)
22
+ return 2
23
+ sub = sys.argv[2]
24
+ candidates = []
25
+ if sub.startswith("/"):
26
+ candidates.append(sub)
27
+ if sub.startswith("~"):
28
+ candidates.append(os.path.expanduser(sub))
29
+ default = os.environ.get("OBSIDIAN_VAULT", "")
30
+ if default:
31
+ candidates.append(os.path.join(os.path.expanduser(default), sub.lstrip("/")))
32
+ root = next((c for c in candidates if os.path.isdir(c)), None)
33
+ if root is None:
34
+ print(f"vault not found: tried {candidates} (set OBSIDIAN_VAULT or pass an absolute path)",
35
+ file=sys.stderr)
36
+ return 1
37
+ total = 0
38
+ for dirpath, dirnames, filenames in os.walk(root):
39
+ dirnames[:] = [d for d in dirnames if not d.startswith(".") and d not in SKIP_DIRS]
40
+ for fn in sorted(filenames):
41
+ base, ext = os.path.splitext(fn)
42
+ if fn.startswith(".") or fn.endswith(".tmp") or ext.lower() in SKIP_EXT:
43
+ continue
44
+ p = os.path.join(dirpath, fn)
45
+ rel = os.path.relpath(p, root)
46
+ print(json.dumps({"uri": f"obsidian://{rel}", "path": p}, ensure_ascii=False))
47
+ total += 1
48
+ print(f"emitted {total} indexable files from vault {root}", file=sys.stderr)
49
+ return 0
50
+
51
+
52
+ if __name__ == "__main__":
53
+ sys.exit(main())
@@ -0,0 +1,42 @@
1
+ #!/usr/bin/env python3
2
+ """MediaRAG source adapter: pcloud://<path-in-drive> — pCloud assets via the
3
+ rclone FUSE mount (or any local mount point).
4
+
5
+ Env: PCLOUD_MOUNT (default: "~/pCloud Drive"). Item shape: {"uri", "path"} —
6
+ the indexer decodes/embeds real files itself. Every file is emitted; the indexer
7
+ classifies extensions itself and reports unsupported ones as skips.
8
+ """
9
+ import json
10
+ import os
11
+ import sys
12
+
13
+
14
+ def main():
15
+ if len(sys.argv) != 3 or sys.argv[1] != "fetch":
16
+ print("usage: pcloud fetch <path-inside-drive> (e.g. pcloud://Shared/wiki)", file=sys.stderr)
17
+ return 2
18
+ mount = os.environ.get("PCLOUD_MOUNT", "~/pCloud Drive")
19
+ mount = os.path.expanduser(mount)
20
+ sub = sys.argv[2]
21
+ if sub.startswith("/"): # accept both "Shard/x" and "/Shared/x"
22
+ sub = sub.lstrip("/")
23
+ root = os.path.join(mount, sub)
24
+ if not os.path.isdir(root):
25
+ print(f"not a directory: {root} (set PCLOUD_MOUNT if your mount differs)", file=sys.stderr)
26
+ return 1
27
+ total = 0
28
+ for dirpath, dirnames, filenames in os.walk(root):
29
+ dirnames[:] = [d for d in dirnames if not d.startswith(".") and d != ".corpus"]
30
+ for fn in sorted(filenames):
31
+ if fn.startswith("."):
32
+ continue
33
+ p = os.path.join(dirpath, fn)
34
+ print(json.dumps({"uri": f"pcloud://{os.path.relpath(p, mount)}", "path": p},
35
+ ensure_ascii=False))
36
+ total += 1
37
+ print(f"emitted {total} indexable files under {sub}", file=sys.stderr)
38
+ return 0
39
+
40
+
41
+ if __name__ == "__main__":
42
+ sys.exit(main())
@@ -0,0 +1,64 @@
1
+ #!/usr/bin/env python3
2
+ """MediaRAG source adapter: raindrop://bookmarks — Raindrop.io bookmarks as text items.
3
+
4
+ Env: RAINDROP_API_TOKEN (required). Stdout = one JSON line per bookmark.
5
+ Stdlib only; no dependencies. Community adapters follow this shape.
6
+ """
7
+ import json
8
+ import os
9
+ import sys
10
+ import urllib.request
11
+
12
+
13
+ def resolve_token():
14
+ return os.environ.get("RAINDROP_API_TOKEN", "").strip()
15
+
16
+
17
+ def main():
18
+ if len(sys.argv) != 3 or sys.argv[1] != "fetch":
19
+ print("usage: raindrop fetch <what> (e.g. raindrop://bookmarks)", file=sys.stderr)
20
+ return 2
21
+ token = resolve_token()
22
+ if not token:
23
+ print("set RAINDROP_API_TOKEN in the server environment", file=sys.stderr)
24
+ return 1
25
+ total = 0
26
+ page = -1 # raindrop pagination is 0-indexed; loop pre-increments
27
+ while True:
28
+ page += 1
29
+ url = f"https://api.raindrop.io/rest/v1/raindrops/0?perpage=50&page={page}"
30
+ req = urllib.request.Request(url, headers={"Authorization": f"Bearer {token}"})
31
+ try:
32
+ resp = json.load(urllib.request.urlopen(req, timeout=30))
33
+ except Exception as e:
34
+ print(f"raindrop API page {page} failed: {e}", file=sys.stderr)
35
+ return 1
36
+ if resp.get("result") not in (0, True):
37
+ print(f"raindrop API error on page {page}: {resp.get('error')}", file=sys.stderr)
38
+ return 1
39
+ items = resp.get("items") or []
40
+ for d in items:
41
+ title = (d.get("title") or "").strip() or "(untitled)"
42
+ parts = [f"# {title}"]
43
+ if d.get("excerpt"):
44
+ parts.append(f"Excerpt: {d['excerpt']}")
45
+ if d.get("tags"):
46
+ parts.append("Tags: " + ", ".join(d["tags"]))
47
+ if d.get("note"):
48
+ parts.append(f"Note: {d['note']}")
49
+ if d.get("link"):
50
+ parts.append(f"URL: {d['link']}")
51
+ text = "\n\n".join(parts).strip()
52
+ if not text:
53
+ continue
54
+ item = {"uri": f"raindrop://{d['_id']}", "title": title, "text": text}
55
+ print(json.dumps(item, ensure_ascii=False))
56
+ total += 1
57
+ if len(items) < 50:
58
+ break
59
+ print(f"emitted {total} bookmarks", file=sys.stderr)
60
+ return 0
61
+
62
+
63
+ if __name__ == "__main__":
64
+ sys.exit(main())
@@ -0,0 +1,68 @@
1
+ #!/usr/bin/env python3
2
+ """MediaRAG source adapter: rclone://<remote>:<path> — ANY rclone-configured
3
+ backend (Google Drive, OneDrive, S3, WebDAV, Dropbox, SFTP, …) in one adapter.
4
+
5
+ Mechanism: `rclone copy` the subtree into a persistent local cache, then emit
6
+ {"uri", "path"} items for indexable files (the server's native indexer decodes
7
+ and embeds them itself). The cache is persistent (default ~/.cache/mediarag/
8
+ rclone/<remote>/<path>) so thumbnails and re-indexing keep working afterwards.
9
+
10
+ Env: RCLONE_ADAPTER_CACHE (default ~/.cache/mediarag/rclone). rclone credentials
11
+ come from the server's rclone environment (config file / env), same as any
12
+ rclone invocation.
13
+ """
14
+ import json
15
+ import os
16
+ import subprocess
17
+ import sys
18
+
19
+ SKIP_DIRS = {".obsidian", ".trash", ".git"}
20
+
21
+
22
+ def main():
23
+ if len(sys.argv) != 3 or sys.argv[1] != "fetch":
24
+ print("usage: rclone fetch \"<remote>:<path>\" (e.g. rclone://gdrive:/notes)", file=sys.stderr)
25
+ return 2
26
+ target = sys.argv[2]
27
+ if ":" not in target:
28
+ print('target must be "<remote>:<path>", e.g. "gdrive:/notes"', file=sys.stderr)
29
+ return 1
30
+ remote, _, remote_path = target.partition(":")
31
+ # remote is joined into the cache path below, so it must be a bare remote
32
+ # name: "x/../../etc:y" would otherwise escape the cache directory.
33
+ if not remote or remote.startswith(".") or any(c in remote for c in ("/", "\\")):
34
+ print(f"invalid remote name {remote!r}: expected a bare rclone remote", file=sys.stderr)
35
+ return 1
36
+ remote_path = "/" + remote_path.lstrip("/")
37
+ cache = os.environ.get("RCLONE_ADAPTER_CACHE",
38
+ os.path.expanduser("~/.cache/mediarag/rclone"))
39
+ dest = os.path.join(cache, remote, remote_path.lstrip("/"))
40
+ os.makedirs(dest, exist_ok=True)
41
+ # rclone copy: parallel, respects remotes' own config/env; exits non-zero on
42
+ # partial failure with a summary on stderr.
43
+ cp = subprocess.run(["rclone", "copy", f"{remote}:{remote_path}", dest,
44
+ "--transfers", "8", "--checkers", "8"],
45
+ capture_output=True, text=True)
46
+ for line in cp.stderr.strip().splitlines()[-3:]:
47
+ print(f"rclone: {line}", file=sys.stderr)
48
+ if cp.returncode != 0:
49
+ print(f"rclone copy failed (exit {cp.returncode})", file=sys.stderr)
50
+ return 1
51
+ total = 0
52
+ for dirpath, dirnames, filenames in os.walk(dest):
53
+ dirnames[:] = [d for d in dirnames if not d.startswith(".") and d not in SKIP_DIRS]
54
+ for fn in sorted(filenames):
55
+ if fn.startswith("."):
56
+ continue
57
+ p = os.path.join(dirpath, fn)
58
+ rel = os.path.relpath(p, os.path.join(cache, remote))
59
+ print(json.dumps({"uri": f"rclone://{remote}/{rel}", "path": p},
60
+ ensure_ascii=False))
61
+ total += 1
62
+ print(f"emitted {total} indexable files from {remote}:{remote_path} (cache {dest})",
63
+ file=sys.stderr)
64
+ return 0
65
+
66
+
67
+ if __name__ == "__main__":
68
+ sys.exit(main())
@@ -0,0 +1,238 @@
1
+ """MediaRAG plugin engine: EG2 towers via the HF processor + onnxruntime.
2
+
3
+ Mirrors scripts/export_golden.py exactly (the validated reference stack).
4
+ Parity gate: tests/bench_engine.py — cosine >= 0.999 vs goldens, all towers.
5
+
6
+ Memory law (live-verified): variable-shape audio/image workloads grow ort
7
+ arenas unbounded with the default memory pattern — every session sets
8
+ enable_mem_pattern=False (12 GB + 27 GB swap observed without it).
9
+ """
10
+ from __future__ import annotations
11
+
12
+ import glob
13
+ import os
14
+ import shutil
15
+ import sys
16
+ import wave
17
+ from functools import lru_cache
18
+
19
+ try:
20
+ import fcntl
21
+ except ImportError: # Windows: no flock; materialize is single-process there
22
+ fcntl = None
23
+
24
+ import numpy as np
25
+ import onnxruntime as ort
26
+
27
+ from transformers import EmbeddingGemma2Processor # noqa: E402
28
+ from PIL import Image # noqa: E402
29
+
30
+ QUERY_PREFIX = "task: search result | query: "
31
+ DOC_PREFIX = "title: none | text: "
32
+ REPO = "onnx-community/embeddinggemma-2-ONNX"
33
+ MODEL_FILES = {
34
+ "trunk": "model_q4.onnx",
35
+ "vision": "vision_encoder_q4.onnx",
36
+ "audio": "audio_encoder_q4.onnx",
37
+ }
38
+
39
+
40
+ _REAL = os.path.expanduser("~/.cache/mediarag/onnx-real")
41
+
42
+
43
+ def _external_escapes(onnx_dir: str) -> bool:
44
+ """onnxruntime resolves the model symlink, then requires external data
45
+ inside that directory. huggingface_hub shards blobs into blobs/ab/ vs
46
+ blobs/cd/, so the data file 'escapes'. A flat blobs/ dir does not.
47
+ """
48
+ for name in MODEL_FILES.values():
49
+ graph = os.path.join(onnx_dir, name)
50
+ data = graph + "_data"
51
+ if not (os.path.exists(graph) and os.path.exists(data)):
52
+ continue
53
+ gdir = os.path.dirname(os.path.realpath(graph))
54
+ try:
55
+ if os.path.commonpath([gdir, os.path.realpath(data)]) != gdir:
56
+ return True
57
+ except ValueError:
58
+ return True
59
+ return False
60
+
61
+
62
+ def _fresh(dst: str, src: str) -> bool:
63
+ stamp = os.path.join(dst, ".src")
64
+ if not os.path.isfile(stamp):
65
+ return False
66
+ with open(stamp) as f:
67
+ if f.read() != src:
68
+ return False
69
+ for name in MODEL_FILES.values():
70
+ for fn in (name, name + "_data"):
71
+ sp = os.path.join(src, fn)
72
+ dp = os.path.join(dst, fn)
73
+ if not os.path.lexists(sp):
74
+ continue
75
+ if os.path.islink(dp) or not os.path.isfile(dp):
76
+ return False
77
+ if os.path.getsize(dp) != os.path.getsize(sp):
78
+ return False
79
+ return True
80
+
81
+
82
+ def _materialize(src: str, dest: str | None = None) -> str:
83
+ """Copy the q4 towers to real files beside each other. ~480MB, once per snapshot.
84
+ ponytail: flock, not a lock manager. A crash mid-copy leaves a .tmp that the
85
+ next call deletes and rewrites.
86
+ """
87
+ dst = dest or _REAL
88
+ os.makedirs(os.path.dirname(dst), exist_ok=True)
89
+ fh = open(dst + ".lock", "a")
90
+ try:
91
+ if fcntl is not None:
92
+ fcntl.flock(fh, fcntl.LOCK_EX)
93
+ if _fresh(dst, src):
94
+ return dst
95
+ tmp = dst + ".tmp"
96
+ shutil.rmtree(tmp, ignore_errors=True)
97
+ os.makedirs(tmp)
98
+ for name in MODEL_FILES.values():
99
+ for fn in (name, name + "_data"):
100
+ sp = os.path.join(src, fn)
101
+ if os.path.lexists(sp):
102
+ shutil.copyfile(sp, os.path.join(tmp, fn)) # follows the symlink
103
+ with open(os.path.join(tmp, ".src"), "w") as f:
104
+ f.write(src)
105
+ if os.path.isdir(dst):
106
+ shutil.rmtree(dst)
107
+ os.rename(tmp, dst)
108
+ return dst
109
+ finally:
110
+ if fcntl is not None:
111
+ fcntl.flock(fh, fcntl.LOCK_UN)
112
+ fh.close()
113
+
114
+
115
+ def snap_dir() -> str:
116
+ hits = glob.glob(
117
+ os.path.expanduser(
118
+ "~/.cache/huggingface/hub/models--onnx-community--embeddinggemma-2-ONNX/snapshots/*/onnx"
119
+ )
120
+ )
121
+ if not hits:
122
+ raise RuntimeError(
123
+ "embedding model not downloaded — snapshot_download "
124
+ "onnx-community/embeddinggemma-2-ONNX (onnx/*_q4.onnx + *_q4.onnx_data)"
125
+ )
126
+ src = sorted(hits)[-1]
127
+ return _materialize(src) if _external_escapes(src) else src
128
+
129
+
130
+ def _sess(model_path: str) -> ort.InferenceSession:
131
+ so = ort.SessionOptions()
132
+ so.enable_mem_pattern = False # memory law
133
+ so.graph_optimization_level = ort.GraphOptimizationLevel.ORT_ENABLE_ALL
134
+ so.intra_op_num_threads = 4
135
+ return ort.InferenceSession(model_path, so, providers=["CPUExecutionProvider"])
136
+
137
+
138
+ @lru_cache(maxsize=1)
139
+ def _processor():
140
+ return EmbeddingGemma2Processor.from_pretrained(REPO, local_files_only=True)
141
+
142
+
143
+ @lru_cache(maxsize=4)
144
+ def _trunk() -> ort.InferenceSession:
145
+ return _sess(os.path.join(snap_dir(), MODEL_FILES["trunk"]))
146
+
147
+
148
+ @lru_cache(maxsize=2)
149
+ def _vis() -> ort.InferenceSession:
150
+ return _sess(os.path.join(snap_dir(), MODEL_FILES["vision"]))
151
+
152
+
153
+ @lru_cache(maxsize=2)
154
+ def _aud() -> ort.InferenceSession:
155
+ return _sess(os.path.join(snap_dir(), MODEL_FILES["audio"]))
156
+
157
+
158
+ Z = np.zeros((0, 512), np.float32)
159
+
160
+
161
+ MAX_TOK = 1024 # ponytail: EG2 trunk is quadratic in sequence length — an unbounded
162
+ # doc chunk (e.g. one 80KB paragraph) OOMs the host; 1024 tokens ≈ 4KB text, plenty for retrieval.
163
+
164
+
165
+ def _cap_tokens(feeds: dict) -> dict:
166
+ """Slice input_ids/attention_mask to MAX_TOK (trust-boundary guard: tool inputs
167
+ like add_note/adapter text are unbounded; the trunk is not)."""
168
+ for k in ("input_ids", "attention_mask"):
169
+ if k in feeds and feeds[k].shape[-1] > MAX_TOK:
170
+ feeds[k] = feeds[k][..., :MAX_TOK]
171
+ return feeds
172
+
173
+
174
+ def _finish(feeds: dict) -> np.ndarray:
175
+ v = np.asarray(_trunk().run(None, feeds)[1], dtype=np.float32).reshape(-1) # [1] = sentence_embedding
176
+ return v / np.linalg.norm(v)
177
+
178
+
179
+ def embed_query(q: str) -> np.ndarray:
180
+ q = (QUERY_PREFIX + q.strip()).strip()
181
+ p = _processor()(text=[q], return_tensors="np")
182
+ return _finish(_cap_tokens({"input_ids": p["input_ids"].astype(np.int64),
183
+ "attention_mask": p["attention_mask"].astype(np.int64),
184
+ "image_features": Z, "video_features": Z, "audio_features": Z}))
185
+
186
+
187
+ def embed_doc(raw: str) -> np.ndarray:
188
+ raw = (DOC_PREFIX + raw.strip()).strip()
189
+ if raw == DOC_PREFIX.strip():
190
+ raise ValueError("empty text chunk")
191
+ p = _processor()(text=[raw], return_tensors="np")
192
+ return _finish(_cap_tokens({"input_ids": p["input_ids"].astype(np.int64),
193
+ "attention_mask": p["attention_mask"].astype(np.int64),
194
+ "image_features": Z, "video_features": Z, "audio_features": Z}))
195
+
196
+
197
+ def embed_plain(raw: str) -> np.ndarray:
198
+ """verbatim text, no prompt template (caption/label lane)."""
199
+ raw = raw.strip()
200
+ if not raw:
201
+ raise ValueError("empty text chunk")
202
+ p = _processor()(text=[raw], return_tensors="np")
203
+ return _finish(_cap_tokens({"input_ids": p["input_ids"].astype(np.int64),
204
+ "attention_mask": p["attention_mask"].astype(np.int64),
205
+ "image_features": Z, "video_features": Z, "audio_features": Z}))
206
+
207
+
208
+ def embed_image(path: str) -> np.ndarray:
209
+ img = Image.open(path).convert("RGB")
210
+ # ponytail: oversized images blow up the soft-token patch grid → onnxruntime
211
+ # arena abort (observed: 14 TB single-buffer request kills the process).
212
+ # Cap at 64 MP (8192^2): EG2 resizes to ≤ 560 px patches anyway — above this
213
+ # cap the pixels are pure waste. Upgrade path: pre-resize instead of reject.
214
+ if img.width * img.height > 64_000_000 or img.width > 16_384 or img.height > 16_384:
215
+ raise ValueError(f"image too large to embed: {img.size}")
216
+ p = _processor()(images=[img], return_tensors="np")
217
+ enc = _vis().run(None, {"pixel_values": p["pixel_values"],
218
+ "pixel_position_ids": p["image_position_ids"].astype(np.int64)})[0]
219
+ n = int((p["input_ids"][0] == _processor().image_token_id).sum())
220
+ assert n == enc.shape[0], f"image token mismatch: {n} != {enc.shape[0]}"
221
+ return _finish({"input_ids": p["input_ids"].astype(np.int64),
222
+ "attention_mask": p["attention_mask"].astype(np.int64),
223
+ "image_features": enc.astype(np.float32),
224
+ "video_features": Z, "audio_features": Z})
225
+
226
+
227
+ def embed_audio(path: str) -> np.ndarray:
228
+ with wave.open(path, "rb") as w:
229
+ a = np.frombuffer(w.readframes(w.getnframes()), dtype=np.int16).astype(np.float32) / 32768.0
230
+ p = _processor()(audio=[a], sampling_rate=16000, return_tensors="np")
231
+ enc = _aud().run(None, {"input_features": p["input_features"].astype(np.float32),
232
+ "input_features_mask": p["input_features_mask"]})[0]
233
+ n = int((p["input_ids"][0] == _processor().audio_token_id).sum())
234
+ assert n == enc.shape[0], f"audio token mismatch: {n} != {enc.shape[0]}"
235
+ return _finish({"input_ids": p["input_ids"].astype(np.int64),
236
+ "attention_mask": p["attention_mask"].astype(np.int64),
237
+ "image_features": Z, "video_features": Z,
238
+ "audio_features": enc.astype(np.float32)})