Repository navigation
Expand file tree
/
Copy pathembedding_runtime.py
More file actions
123 lines (99 loc) · 5.06 KB
/
Copy pathembedding_runtime.py
File metadata and controls
123 lines (99 loc) · 5.06 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
"""Bounded local embeddings in an isolated, cancellable native process."""
import math
import os
from pathlib import Path
import subprocess
import sys
import threading
from embedding_models import MODEL_REVISION, embedding_model_path
from i18n import tr
from meeting_index import IndexCancelled
from summary_runtime import _WorkerClient
DIMENSION = 768
PREPROCESSING_VERSION = "search-document-v1"
MAX_TEXT_CHARS = 8000
MAX_BATCH = 16
def normalize_vector(value):
if not isinstance(value, (list, tuple)) or len(value) != DIMENSION:
raise ValueError(tr("O vetor de embeddings deve ter 768 dimensões."))
if any(isinstance(item, bool) or not isinstance(item, (int, float)) for item in value):
raise ValueError(tr("O vetor de embeddings contém valores inválidos."))
try:
numeric = [float(item) for item in value]
except OverflowError as error:
raise ValueError(tr("O vetor de embeddings contém valores inválidos.")) from error
if any(not math.isfinite(item) for item in numeric):
raise ValueError(tr("O vetor de embeddings contém valores inválidos."))
norm = math.hypot(*numeric)
if not math.isfinite(norm) or norm == 0:
raise ValueError(tr("O vetor de embeddings não pode ser normalizado."))
return [item / norm for item in numeric]
def check_cancelled(event):
if event is not None and event.is_set():
raise IndexCancelled(tr("A busca semântica foi cancelada."))
def _text(value):
if not isinstance(value, str) or not value.strip() or len(value) > MAX_TEXT_CHARS:
raise ValueError(tr("O texto de embeddings está vazio ou excede o limite de 8000 caracteres."))
return value
def query_text(query):
return "task: search result | query: " + _text(query)
def document_text(text, title=""):
if not isinstance(title, str) or len(title) > 512:
raise ValueError(tr("O título de embeddings é inválido."))
return f"title: {title or 'none'} | text: {_text(text)}"
def _spawn_worker():
frozen = getattr(sys, "frozen", False)
executable = sys.executable
options = dict(stdin=subprocess.PIPE, stdout=subprocess.PIPE, stderr=subprocess.DEVNULL,
text=True, encoding="utf-8", bufsize=1)
if os.name == "nt":
options["creationflags"] = subprocess.CREATE_NO_WINDOW
if not frozen and sys.prefix != sys.base_prefix:
# CPython's Windows venv launcher creates another process. Launch
# the interpreter directly (as multiprocessing does) so cancellation
# and RSS measurement target native inference, preserving the venv.
executable = sys._base_executable
options["env"] = {**os.environ, "__PYVENV_LAUNCHER__": sys.executable}
command = ([executable, "--embedding-worker"] if frozen
else [executable, "-u", str(Path(__file__).with_name("embedding_runtime_worker.py"))])
return subprocess.Popen(command, **options)
class EmbeddingRuntime:
model_revision = MODEL_REVISION
def __init__(self, *, cancel_event=None, cache_dir=None):
check_cancelled(cancel_event)
path = embedding_model_path(cache_dir)
if path is None:
raise RuntimeError(tr("Instale o EmbeddingGemma 2 para usar a busca semântica. A busca por palavras continua disponível."))
self._lock = threading.RLock()
self._worker = _WorkerClient(spawn_worker=_spawn_worker)
try:
self._request({"type": "open", "model_path": path}, cancel_event)
except Exception:
self._worker.close(force=True)
raise
def _request(self, payload, cancel_event=None):
try:
# ceiling: 120 seconds per native batch including load; increase
# only after measuring legitimate work on representative hardware.
return self._worker.request(payload, cancel_event=cancel_event, timeout=120)
except RuntimeError as error:
check_cancelled(cancel_event)
raise RuntimeError(tr("O runtime local de embeddings falhou ou não suporta este modelo. A busca por palavras continua disponível.")) from error
def _embed(self, texts, cancel_event=None):
if not 1 <= len(texts) <= MAX_BATCH:
raise ValueError(tr("O lote de embeddings é inválido."))
with self._lock:
check_cancelled(cancel_event)
response = self._request({"type": "embed", "texts": texts}, cancel_event)
values = response.get("vectors")
if not isinstance(values, list) or len(values) != len(texts):
raise ValueError(tr("O runtime de embeddings retornou um lote inválido."))
return [normalize_vector(value) for value in values]
def embed_query(self, query, cancel_event=None):
return self._embed([query_text(query)], cancel_event)[0]
def embed_documents(self, records, cancel_event=None):
return self._embed([document_text(item["text"], item.get("title", ""))
for item in records], cancel_event)
def close(self):
with self._lock:
self._worker.close(force=True)