feat: add desktop document search and verified native CPU embeddings
Core CI / core (ubuntu-latest) (push) Failing after 1m22s
Core CI / core (macos-latest) (push) Canceled after 0s
Core CI / core (windows-latest) (push) Canceled after 0s
Core CI / desktop (ubuntu-latest) (push) Successful in 3m46s
Core CI / desktop (macos-latest) (push) Canceled after 0s
Core CI / desktop (windows-latest) (push) Canceled after 0s
Core CI / core (ubuntu-latest) (push) Failing after 1m22s
Core CI / core (macos-latest) (push) Canceled after 0s
Core CI / core (windows-latest) (push) Canceled after 0s
Core CI / desktop (ubuntu-latest) (push) Successful in 3m46s
Core CI / desktop (macos-latest) (push) Canceled after 0s
Core CI / desktop (windows-latest) (push) Canceled after 0s
This commit is contained in:
1 parent
f77112db97
commit
a9b6592330
46 files changed
+10794
-77
No files matched your search
@@ -0,0 +1,3 @@
|
||||
.venv/
|
||||
model/
|
||||
__pycache__/
|
||||
@@ -0,0 +1,53 @@
|
||||
"""Explicit provisioning only; inference never imports this downloader."""
|
||||
import argparse
|
||||
import hashlib
|
||||
import json
|
||||
import os
|
||||
from pathlib import Path
|
||||
import urllib.request
|
||||
|
||||
HERE = Path(__file__).resolve().parent
|
||||
|
||||
|
||||
def digest(path):
|
||||
with path.open("rb") as source:
|
||||
return hashlib.file_digest(source, "sha256").hexdigest()
|
||||
|
||||
|
||||
def verified(path, spec):
|
||||
return path.is_file() and path.stat().st_size == spec["bytes"] and digest(path) == spec["sha256"]
|
||||
|
||||
|
||||
def main():
|
||||
parser = argparse.ArgumentParser(description="Explicitly download ~136 MB of pinned, verified model artifacts")
|
||||
parser.add_argument("--model-dir", type=Path, default=HERE / "model")
|
||||
args = parser.parse_args()
|
||||
manifest = json.loads((HERE / "model-manifest.json").read_text())
|
||||
for spec in manifest["files"]:
|
||||
target = args.model_dir / spec["path"]
|
||||
if verified(target, spec):
|
||||
print(f"Verified existing {spec['path']}")
|
||||
continue
|
||||
target.parent.mkdir(parents=True, exist_ok=True)
|
||||
temporary = target.with_suffix(target.suffix + ".part")
|
||||
url = f"https://huggingface.co/{manifest['model_id']}/resolve/{manifest['revision']}/{spec['path']}"
|
||||
print(f"Downloading {spec['path']} ({spec['bytes']} bytes)", flush=True)
|
||||
try:
|
||||
request = urllib.request.Request(url, headers={"User-Agent": "Searchi-semantic-proof/0.1"})
|
||||
with urllib.request.urlopen(request, timeout=60) as response, temporary.open("wb") as output:
|
||||
total = 0
|
||||
while chunk := response.read(1024 * 1024):
|
||||
total += len(chunk)
|
||||
if total > spec["bytes"]:
|
||||
raise ValueError("Download exceeds manifest size")
|
||||
output.write(chunk)
|
||||
if not verified(temporary, spec):
|
||||
raise ValueError(f"Size or SHA256 mismatch for {spec['path']}")
|
||||
os.replace(temporary, target)
|
||||
print(f"Verified SHA256 {spec['sha256']}")
|
||||
finally:
|
||||
temporary.unlink(missing_ok=True)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -0,0 +1,20 @@
|
||||
{
|
||||
"documents": [
|
||||
{"id": "supplier-contract", "text": "The supplier agreement may be ended by giving sixty days written notice. Early cancellation incurs no additional fee."},
|
||||
{"id": "backup-policy", "text": "Backups are encrypted and stored each night. Recovery copies are retained for thirty days."},
|
||||
{"id": "travel-expenses", "text": "Business train tickets and hotel stays can be reimbursed. Submit receipts with the monthly expense report."},
|
||||
{"id": "meeting-notes", "text": "The design team decided to move the launch to November because accessibility testing needs another week."},
|
||||
{"id": "apartment-lease", "text": "Die Wohnung kostet monatlich 950 Euro. Die Kaution beträgt drei Monatsmieten und wird nach dem Auszug zurückgezahlt."},
|
||||
{"id": "garden-guide", "text": "Tomaten brauchen viel Sonnenlicht und regelmäßiges Gießen. Die Pflanzen sollten vor Frost geschützt werden."},
|
||||
{"id": "password-reset", "text": "To recover your account, request a password reset link. The link expires after fifteen minutes."},
|
||||
{"id": "invoice", "text": "Invoice 1042 lists five software licenses at forty euros each. Payment is due on the first of December."}
|
||||
],
|
||||
"queries": [
|
||||
{"text": "How do we stop working with the vendor?", "expected": "supplier-contract", "language": "en"},
|
||||
{"text": "Wie können wir die Zusammenarbeit mit dem Lieferanten beenden?", "expected": "supplier-contract", "language": "de-crosslingual"},
|
||||
{"text": "Wo finde ich die Regeln zur Erstattung meiner Dienstreise?", "expected": "travel-expenses", "language": "de-crosslingual"},
|
||||
{"text": "Why was the product release postponed?", "expected": "meeting-notes", "language": "en"},
|
||||
{"text": "How much money must I leave as a security deposit for the flat?", "expected": "apartment-lease", "language": "en-crosslingual"},
|
||||
{"text": "Wie pflege ich die Gemüsepflanzen im Garten?", "expected": "garden-guide", "language": "de"}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
{
|
||||
"model_id": "Xenova/paraphrase-multilingual-MiniLM-L12-v2",
|
||||
"revision": "2c4055b12046f11709e9df2c122e59ffbdc2f900",
|
||||
"base_model": "sentence-transformers/paraphrase-multilingual-MiniLM-L12-v2",
|
||||
"base_license": "Apache-2.0",
|
||||
"dimensions": 384,
|
||||
"max_tokens": 128,
|
||||
"pooling": "attention-mask-mean",
|
||||
"normalization": "l2",
|
||||
"files": [
|
||||
{"path": "onnx/model_quantized.onnx", "bytes": 118308126, "sha256": "66fc00f5f29afcaff34092e1bdd20008ca3918265a82fb9695a551e510cc4ebc"},
|
||||
{"path": "tokenizer.json", "bytes": 17082913, "sha256": "b60b6b43406a48bf3638526314f3d232d97058bc93472ff2de930d43686fa441"}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,135 @@
|
||||
"""Real, local ONNX text embeddings with an enforced CPU-only session."""
|
||||
import argparse
|
||||
import hashlib
|
||||
import json
|
||||
import os
|
||||
from pathlib import Path
|
||||
import platform
|
||||
import statistics
|
||||
import sys
|
||||
import time
|
||||
|
||||
# Fail rather than silently contacting a model service or downloading an asset.
|
||||
os.environ["HF_HUB_OFFLINE"] = "1"
|
||||
os.environ["TRANSFORMERS_OFFLINE"] = "1"
|
||||
os.environ["TOKENIZERS_PARALLELISM"] = "false"
|
||||
|
||||
|
||||
def reject_network(event, args):
|
||||
if event in {"socket.connect", "socket.getaddrinfo", "socket.bind"}:
|
||||
raise RuntimeError("Semantic proof inference is offline; network access is forbidden")
|
||||
|
||||
|
||||
sys.addaudithook(reject_network)
|
||||
|
||||
import numpy as np
|
||||
import onnxruntime as ort
|
||||
from tokenizers import Tokenizer
|
||||
|
||||
HERE = Path(__file__).resolve().parent
|
||||
|
||||
|
||||
class CpuEmbedder:
|
||||
"""Bounded reference contract for a future Rust ONNX adapter."""
|
||||
|
||||
def __init__(self, model_dir, threads=1):
|
||||
if not 1 <= threads <= 8:
|
||||
raise ValueError("threads must be between 1 and 8")
|
||||
self.manifest = json.loads((HERE / "model-manifest.json").read_text())
|
||||
for spec in self.manifest["files"]:
|
||||
path = model_dir / spec["path"]
|
||||
if not path.is_file():
|
||||
raise ValueError(f"Missing local artifact {path}; explicitly run download.py first")
|
||||
with path.open("rb") as source:
|
||||
checksum = hashlib.file_digest(source, "sha256").hexdigest()
|
||||
if path.stat().st_size != spec["bytes"] or checksum != spec["sha256"]:
|
||||
raise ValueError(f"Artifact size/SHA256 mismatch: {path}")
|
||||
options = ort.SessionOptions()
|
||||
options.intra_op_num_threads = threads
|
||||
options.inter_op_num_threads = 1
|
||||
options.execution_mode = ort.ExecutionMode.ORT_SEQUENTIAL
|
||||
self.session = ort.InferenceSession(
|
||||
str(model_dir / "onnx/model_quantized.onnx"),
|
||||
sess_options=options,
|
||||
providers=["CPUExecutionProvider"],
|
||||
)
|
||||
if self.session.get_providers() != ["CPUExecutionProvider"]:
|
||||
raise RuntimeError("Only CPUExecutionProvider is allowed")
|
||||
self.tokenizer = Tokenizer.from_file(str(model_dir / "tokenizer.json"))
|
||||
self.tokenizer.enable_truncation(max_length=self.manifest["max_tokens"])
|
||||
self.tokenizer.enable_padding(pad_id=1, pad_token="<pad>")
|
||||
|
||||
def encode(self, texts):
|
||||
if not texts or len(texts) > 8:
|
||||
raise ValueError("Batch size must be between 1 and 8")
|
||||
if any(not text.strip() or len(text.encode("utf-8")) > 65536 for text in texts):
|
||||
raise ValueError("Text must be nonempty and at most 64 KiB UTF-8")
|
||||
encoded = self.tokenizer.encode_batch(texts)
|
||||
available = {
|
||||
"input_ids": np.asarray([item.ids for item in encoded], dtype=np.int64),
|
||||
"attention_mask": np.asarray([item.attention_mask for item in encoded], dtype=np.int64),
|
||||
"token_type_ids": np.asarray([item.type_ids for item in encoded], dtype=np.int64),
|
||||
}
|
||||
inputs = {item.name: available[item.name] for item in self.session.get_inputs()}
|
||||
hidden = self.session.run(None, inputs)[0]
|
||||
mask = available["attention_mask"][..., None].astype(np.float32)
|
||||
pooled = (hidden * mask).sum(axis=1) / np.maximum(mask.sum(axis=1), 1e-9)
|
||||
vectors = pooled / np.maximum(np.linalg.norm(pooled, axis=1, keepdims=True), 1e-9)
|
||||
if vectors.shape != (len(texts), self.manifest["dimensions"]) or not np.isfinite(vectors).all():
|
||||
raise RuntimeError("Invalid model embedding shape or nonfinite values")
|
||||
return vectors.astype(np.float32)
|
||||
|
||||
|
||||
def main():
|
||||
parser = argparse.ArgumentParser(description=__doc__)
|
||||
parser.add_argument("--model-dir", type=Path, default=HERE / "model")
|
||||
parser.add_argument("--fixture", type=Path, default=HERE / "fixture.json")
|
||||
parser.add_argument("--output", type=Path)
|
||||
parser.add_argument("--threads", type=int, default=1, choices=range(1, 9))
|
||||
parser.add_argument("--repeats", type=int, default=10, choices=range(1, 101))
|
||||
args = parser.parse_args()
|
||||
fixture = json.loads(args.fixture.read_text(encoding="utf-8"))
|
||||
started = time.perf_counter()
|
||||
embedder = CpuEmbedder(args.model_dir, args.threads)
|
||||
load_ms = (time.perf_counter() - started) * 1000
|
||||
started = time.perf_counter()
|
||||
vectors = embedder.encode([item["text"] for item in fixture["documents"]])
|
||||
corpus_ms = (time.perf_counter() - started) * 1000
|
||||
rankings = []
|
||||
timings = []
|
||||
for query in fixture["queries"]:
|
||||
vector = embedder.encode([query["text"]])[0] # Warm this input shape.
|
||||
for _ in range(args.repeats):
|
||||
started = time.perf_counter()
|
||||
vector = embedder.encode([query["text"]])[0]
|
||||
scores = vectors @ vector
|
||||
order = np.argsort(-scores)
|
||||
timings.append((time.perf_counter() - started) * 1000)
|
||||
ranked = [{"id": fixture["documents"][int(i)]["id"], "cosine": float(scores[i])} for i in order]
|
||||
expected_rank = next(i + 1 for i, result in enumerate(ranked) if result["id"] == query["expected"])
|
||||
rankings.append({**query, "expected_rank": expected_rank, "top3": ranked[:3]})
|
||||
report = {
|
||||
"platform": platform.platform(), "python": platform.python_version(),
|
||||
"machine": platform.machine(), "logical_cpus": os.cpu_count(),
|
||||
"onnxruntime": ort.__version__, "numpy": np.__version__,
|
||||
"providers": embedder.session.get_providers(), "threads": args.threads,
|
||||
"model": embedder.manifest, "documents": len(fixture["documents"]),
|
||||
"queries": len(rankings), "timed_query_runs": len(timings),
|
||||
"artifact_verify_and_load_ms": load_ms, "corpus_embedding_ms": corpus_ms,
|
||||
"query_p50_ms": statistics.median(timings),
|
||||
"query_p95_ms": float(np.percentile(timings, 95)),
|
||||
"top1_accuracy": sum(item["expected_rank"] == 1 for item in rankings) / len(rankings),
|
||||
"recall_at_3": sum(item["expected_rank"] <= 3 for item in rankings) / len(rankings),
|
||||
"network": "Python socket audit guard active during import, model load, and inference",
|
||||
"rankings": rankings,
|
||||
}
|
||||
serialized = json.dumps(report, ensure_ascii=False, indent=2)
|
||||
if args.output:
|
||||
args.output.write_text(serialized + "\n", encoding="utf-8")
|
||||
print(serialized)
|
||||
if report["top1_accuracy"] < 1:
|
||||
raise SystemExit("Fixture top-1 quality gate failed")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -0,0 +1,21 @@
|
||||
anyio==4.15.1
|
||||
click==8.5.0
|
||||
colorama==0.4.6
|
||||
filelock==4.1.1
|
||||
flatbuffers==25.12.19
|
||||
fsspec==2026.9.0
|
||||
h11==0.16.0
|
||||
hf-xet==1.7.0
|
||||
httpcore2==2.13.1
|
||||
httpx2==2.13.1
|
||||
huggingface_hub==2.2.0
|
||||
idna==3.20
|
||||
numpy==2.5.4
|
||||
onnxruntime==1.31.0
|
||||
packaging==26.3
|
||||
protobuf==7.36.2
|
||||
PyYAML==6.0.3
|
||||
tokenizers==0.23.3
|
||||
tqdm==4.70.1
|
||||
truststore==0.10.4
|
||||
typing_extensions==4.16.0
|
||||
@@ -0,0 +1,4 @@
|
||||
# CPU wheel only. Never replace with onnxruntime-gpu.
|
||||
onnxruntime==1.31.0
|
||||
tokenizers==0.23.3
|
||||
numpy==2.5.4
|
||||
@@ -0,0 +1,166 @@
|
||||
{
|
||||
"platform": "Windows-11-10.0.26300-SP0",
|
||||
"python": "3.14.8",
|
||||
"machine": "AMD64",
|
||||
"logical_cpus": 4,
|
||||
"onnxruntime": "1.31.0",
|
||||
"numpy": "2.5.4",
|
||||
"providers": [
|
||||
"CPUExecutionProvider"
|
||||
],
|
||||
"threads": 1,
|
||||
"model": {
|
||||
"model_id": "Xenova/paraphrase-multilingual-MiniLM-L12-v2",
|
||||
"revision": "2c4055b12046f11709e9df2c122e59ffbdc2f900",
|
||||
"base_model": "sentence-transformers/paraphrase-multilingual-MiniLM-L12-v2",
|
||||
"base_license": "Apache-2.0",
|
||||
"dimensions": 384,
|
||||
"max_tokens": 128,
|
||||
"pooling": "attention-mask-mean",
|
||||
"normalization": "l2",
|
||||
"files": [
|
||||
{
|
||||
"path": "onnx/model_quantized.onnx",
|
||||
"bytes": 118308126,
|
||||
"sha256": "66fc00f5f29afcaff34092e1bdd20008ca3918265a82fb9695a551e510cc4ebc"
|
||||
},
|
||||
{
|
||||
"path": "tokenizer.json",
|
||||
"bytes": 17082913,
|
||||
"sha256": "b60b6b43406a48bf3638526314f3d232d97058bc93472ff2de930d43686fa441"
|
||||
}
|
||||
]
|
||||
},
|
||||
"documents": 8,
|
||||
"queries": 6,
|
||||
"timed_query_runs": 60,
|
||||
"artifact_verify_and_load_ms": 3399.298300006194,
|
||||
"corpus_embedding_ms": 145.497699995758,
|
||||
"query_p50_ms": 3.0544000037480146,
|
||||
"query_p95_ms": 11.88376000645803,
|
||||
"top1_accuracy": 0.8333333333333334,
|
||||
"recall_at_3": 1.0,
|
||||
"network": "Python socket audit guard active during import, model load, and inference",
|
||||
"rankings": [
|
||||
{
|
||||
"text": "How do we stop working with the vendor?",
|
||||
"expected": "supplier-contract",
|
||||
"language": "en",
|
||||
"expected_rank": 1,
|
||||
"top3": [
|
||||
{
|
||||
"id": "supplier-contract",
|
||||
"cosine": 0.389570415019989
|
||||
},
|
||||
{
|
||||
"id": "invoice",
|
||||
"cosine": 0.16107530891895294
|
||||
},
|
||||
{
|
||||
"id": "travel-expenses",
|
||||
"cosine": 0.06973522156476974
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"text": "Wie können wir die Zusammenarbeit mit dem Lieferanten beenden?",
|
||||
"expected": "supplier-contract",
|
||||
"language": "de-crosslingual",
|
||||
"expected_rank": 1,
|
||||
"top3": [
|
||||
{
|
||||
"id": "supplier-contract",
|
||||
"cosine": 0.5199419260025024
|
||||
},
|
||||
{
|
||||
"id": "invoice",
|
||||
"cosine": 0.21696728467941284
|
||||
},
|
||||
{
|
||||
"id": "meeting-notes",
|
||||
"cosine": 0.14755962789058685
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"text": "Wo finde ich die Regeln zur Erstattung meiner Dienstreise?",
|
||||
"expected": "travel-expenses",
|
||||
"language": "de-crosslingual",
|
||||
"expected_rank": 1,
|
||||
"top3": [
|
||||
{
|
||||
"id": "travel-expenses",
|
||||
"cosine": 0.5095880627632141
|
||||
},
|
||||
{
|
||||
"id": "apartment-lease",
|
||||
"cosine": 0.3584939241409302
|
||||
},
|
||||
{
|
||||
"id": "invoice",
|
||||
"cosine": 0.2985326647758484
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"text": "Why was the product release postponed?",
|
||||
"expected": "meeting-notes",
|
||||
"language": "en",
|
||||
"expected_rank": 2,
|
||||
"top3": [
|
||||
{
|
||||
"id": "supplier-contract",
|
||||
"cosine": 0.3999302387237549
|
||||
},
|
||||
{
|
||||
"id": "meeting-notes",
|
||||
"cosine": 0.3511156439781189
|
||||
},
|
||||
{
|
||||
"id": "backup-policy",
|
||||
"cosine": 0.19898240268230438
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"text": "How much money must I leave as a security deposit for the flat?",
|
||||
"expected": "apartment-lease",
|
||||
"language": "en-crosslingual",
|
||||
"expected_rank": 1,
|
||||
"top3": [
|
||||
{
|
||||
"id": "apartment-lease",
|
||||
"cosine": 0.5188605785369873
|
||||
},
|
||||
{
|
||||
"id": "travel-expenses",
|
||||
"cosine": 0.2738097310066223
|
||||
},
|
||||
{
|
||||
"id": "backup-policy",
|
||||
"cosine": 0.2614522874355316
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"text": "Wie pflege ich die Gemüsepflanzen im Garten?",
|
||||
"expected": "garden-guide",
|
||||
"language": "de",
|
||||
"expected_rank": 1,
|
||||
"top3": [
|
||||
{
|
||||
"id": "garden-guide",
|
||||
"cosine": 0.5030614733695984
|
||||
},
|
||||
{
|
||||
"id": "apartment-lease",
|
||||
"cosine": 0.03017299994826317
|
||||
},
|
||||
{
|
||||
"id": "meeting-notes",
|
||||
"cosine": 0.030125245451927185
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
Reference in new issue
Block a user