kgmodule-utils 0.4.2__tar.gz → 0.4.4__tar.gz
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.
- {kgmodule_utils-0.4.2 → kgmodule_utils-0.4.4}/PKG-INFO +1 -1
- {kgmodule_utils-0.4.2 → kgmodule_utils-0.4.4}/pyproject.toml +1 -1
- {kgmodule_utils-0.4.2 → kgmodule_utils-0.4.4}/src/kg_utils/__init__.py +1 -1
- {kgmodule_utils-0.4.2 → kgmodule_utils-0.4.4}/src/kg_utils/embedder.py +29 -11
- {kgmodule_utils-0.4.2 → kgmodule_utils-0.4.4}/src/kg_utils/synthesis/_image.py +43 -4
- {kgmodule_utils-0.4.2 → kgmodule_utils-0.4.4}/src/kg_utils/worker/client.py +18 -10
- {kgmodule_utils-0.4.2 → kgmodule_utils-0.4.4}/src/kg_utils/worker/ops.py +22 -7
- {kgmodule_utils-0.4.2 → kgmodule_utils-0.4.4}/LICENSE +0 -0
- {kgmodule_utils-0.4.2 → kgmodule_utils-0.4.4}/README.md +0 -0
- {kgmodule_utils-0.4.2 → kgmodule_utils-0.4.4}/src/kg_utils/embed.py +0 -0
- {kgmodule_utils-0.4.2 → kgmodule_utils-0.4.4}/src/kg_utils/extractor.py +0 -0
- {kgmodule_utils-0.4.2 → kgmodule_utils-0.4.4}/src/kg_utils/module.py +0 -0
- {kgmodule_utils-0.4.2 → kgmodule_utils-0.4.4}/src/kg_utils/pipeline.py +0 -0
- {kgmodule_utils-0.4.2 → kgmodule_utils-0.4.4}/src/kg_utils/py.typed +0 -0
- {kgmodule_utils-0.4.2 → kgmodule_utils-0.4.4}/src/kg_utils/retrieval/__init__.py +0 -0
- {kgmodule_utils-0.4.2 → kgmodule_utils-0.4.4}/src/kg_utils/retrieval/hits.py +0 -0
- {kgmodule_utils-0.4.2 → kgmodule_utils-0.4.4}/src/kg_utils/semantic.py +0 -0
- {kgmodule_utils-0.4.2 → kgmodule_utils-0.4.4}/src/kg_utils/snapshots/__init__.py +0 -0
- {kgmodule_utils-0.4.2 → kgmodule_utils-0.4.4}/src/kg_utils/snapshots/manager.py +0 -0
- {kgmodule_utils-0.4.2 → kgmodule_utils-0.4.4}/src/kg_utils/snapshots/models.py +0 -0
- {kgmodule_utils-0.4.2 → kgmodule_utils-0.4.4}/src/kg_utils/specs.py +0 -0
- {kgmodule_utils-0.4.2 → kgmodule_utils-0.4.4}/src/kg_utils/store.py +0 -0
- {kgmodule_utils-0.4.2 → kgmodule_utils-0.4.4}/src/kg_utils/synthesis/__init__.py +0 -0
- {kgmodule_utils-0.4.2 → kgmodule_utils-0.4.4}/src/kg_utils/synthesis/_config.py +0 -0
- {kgmodule_utils-0.4.2 → kgmodule_utils-0.4.4}/src/kg_utils/synthesis/_text.py +0 -0
- {kgmodule_utils-0.4.2 → kgmodule_utils-0.4.4}/src/kg_utils/synthesis/factory.py +0 -0
- {kgmodule_utils-0.4.2 → kgmodule_utils-0.4.4}/src/kg_utils/worker/__init__.py +0 -0
|
@@ -10,7 +10,7 @@ build-backend = "poetry.core.masonry.api"
|
|
|
10
10
|
|
|
11
11
|
[project]
|
|
12
12
|
name = "kgmodule-utils"
|
|
13
|
-
version = "0.4.
|
|
13
|
+
version = "0.4.4"
|
|
14
14
|
description = "Shared types, graph store, semantic index, and pipeline base for the KGModule SDK"
|
|
15
15
|
readme = "README.md"
|
|
16
16
|
license = { text = "Elastic-2.0" }
|
|
@@ -34,12 +34,12 @@ License: Elastic 2.0
|
|
|
34
34
|
|
|
35
35
|
from __future__ import annotations
|
|
36
36
|
|
|
37
|
+
import importlib
|
|
37
38
|
import os
|
|
38
39
|
from typing import Any
|
|
39
40
|
|
|
40
41
|
from kg_utils.embed import DEFAULT_MODEL, KNOWN_MODELS, resolve_model_path
|
|
41
42
|
|
|
42
|
-
|
|
43
43
|
# ---------------------------------------------------------------------------
|
|
44
44
|
# Abstract base
|
|
45
45
|
# ---------------------------------------------------------------------------
|
|
@@ -75,7 +75,7 @@ class Embedder:
|
|
|
75
75
|
# ---------------------------------------------------------------------------
|
|
76
76
|
|
|
77
77
|
|
|
78
|
-
def load_sentence_transformer(model_name: str = DEFAULT_MODEL) -> Any:
|
|
78
|
+
def load_sentence_transformer(model_name: str = DEFAULT_MODEL, device: str | None = None) -> Any:
|
|
79
79
|
"""Load a ``SentenceTransformer`` with the canonical safe-load sequence.
|
|
80
80
|
|
|
81
81
|
Resolution order:
|
|
@@ -90,13 +90,22 @@ def load_sentence_transformer(model_name: str = DEFAULT_MODEL) -> Any:
|
|
|
90
90
|
retry loops leave stale thread state that causes SIGBUS on the first
|
|
91
91
|
``encode()`` call.
|
|
92
92
|
|
|
93
|
+
Device precedence: explicit *device* arg > ``KG_EMBED_DEVICE`` env >
|
|
94
|
+
auto-detect. The env var exists because ``spawn``-based embedding workers
|
|
95
|
+
inherit ``os.environ`` but can't easily receive a Python arg — without a way
|
|
96
|
+
to pin the device, each worker auto-selects MPS and N parallel workers stack
|
|
97
|
+
N GPU allocations into an OOM. So CPU multiprocessing embedding on Apple
|
|
98
|
+
Silicon is only safe with this knob.
|
|
99
|
+
|
|
93
100
|
:param model_name: HuggingFace model ID or KNOWN_MODELS alias.
|
|
101
|
+
:param device: Explicit device (``"cpu"``/``"mps"``/``"cuda"``). ``None``
|
|
102
|
+
falls back to ``KG_EMBED_DEVICE`` then CUDA→MPS→CPU auto-detect.
|
|
94
103
|
:return: Loaded ``SentenceTransformer`` instance.
|
|
95
104
|
"""
|
|
96
|
-
|
|
105
|
+
SentenceTransformer = importlib.import_module("sentence_transformers").SentenceTransformer
|
|
97
106
|
|
|
98
107
|
try:
|
|
99
|
-
|
|
108
|
+
hf_logging = importlib.import_module("transformers.logging")
|
|
100
109
|
|
|
101
110
|
hf_logging.set_verbosity_error()
|
|
102
111
|
# TQDM_DISABLE alone misses transformers' _tqdm_active gate
|
|
@@ -106,9 +115,18 @@ def load_sentence_transformer(model_name: str = DEFAULT_MODEL) -> Any:
|
|
|
106
115
|
|
|
107
116
|
os.environ["TQDM_DISABLE"] = "1"
|
|
108
117
|
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
118
|
+
torch = importlib.import_module("torch")
|
|
119
|
+
|
|
120
|
+
# Device precedence: explicit arg > KG_EMBED_DEVICE env > auto-detect.
|
|
121
|
+
# The env var lets spawn-based embedding workers (which inherit os.environ
|
|
122
|
+
# but can't easily receive a Python arg) pin to e.g. CPU — without it each
|
|
123
|
+
# worker auto-selects MPS and N parallel workers stack N GPU allocations →
|
|
124
|
+
# MPS OOM. The override is why CPU multiprocessing embedding is safe on
|
|
125
|
+
# Apple Silicon.
|
|
126
|
+
sel = (device or os.environ.get("KG_EMBED_DEVICE", "")).strip().lower()
|
|
127
|
+
if sel:
|
|
128
|
+
device = sel
|
|
129
|
+
elif torch.cuda.is_available():
|
|
112
130
|
device = "cuda"
|
|
113
131
|
else:
|
|
114
132
|
try:
|
|
@@ -158,7 +176,7 @@ class SentenceTransformerEmbedder(Embedder):
|
|
|
158
176
|
|
|
159
177
|
def __init__(self, model_name: str = DEFAULT_MODEL) -> None:
|
|
160
178
|
try:
|
|
161
|
-
|
|
179
|
+
hf_logging = importlib.import_module("transformers.logging")
|
|
162
180
|
|
|
163
181
|
hf_logging.set_verbosity_error()
|
|
164
182
|
hf_logging.disable_progress_bar()
|
|
@@ -188,7 +206,7 @@ class SentenceTransformerEmbedder(Embedder):
|
|
|
188
206
|
:param texts: Input strings.
|
|
189
207
|
:param encode_batch_size: Passed to ``model.encode()`` — tune down if OOM on MPS.
|
|
190
208
|
"""
|
|
191
|
-
|
|
209
|
+
np = importlib.import_module("numpy")
|
|
192
210
|
|
|
193
211
|
vecs = self.model.encode(
|
|
194
212
|
texts,
|
|
@@ -200,7 +218,7 @@ class SentenceTransformerEmbedder(Embedder):
|
|
|
200
218
|
|
|
201
219
|
def embed_query(self, query: str) -> list[float]:
|
|
202
220
|
"""Embed a single query string into a float32 vector."""
|
|
203
|
-
|
|
221
|
+
np = importlib.import_module("numpy")
|
|
204
222
|
|
|
205
223
|
vec = self.model.encode([query], normalize_embeddings=True)[0]
|
|
206
224
|
return list(np.asarray(vec, dtype="float32").tolist())
|
|
@@ -233,7 +251,7 @@ def wrap_embedder(st_model: Any, model_name: str = DEFAULT_MODEL) -> Embedder:
|
|
|
233
251
|
:param model_name: Model name stored as metadata on the wrapper.
|
|
234
252
|
:return: An :class:`Embedder` that delegates all calls to *st_model*.
|
|
235
253
|
"""
|
|
236
|
-
|
|
254
|
+
np = importlib.import_module("numpy")
|
|
237
255
|
|
|
238
256
|
resolved = KNOWN_MODELS.get(model_name, model_name)
|
|
239
257
|
_dim_fn = getattr(st_model, "get_embedding_dimension", None) or getattr(
|
|
@@ -4,6 +4,7 @@
|
|
|
4
4
|
from __future__ import annotations
|
|
5
5
|
|
|
6
6
|
import base64
|
|
7
|
+
import importlib
|
|
7
8
|
import random
|
|
8
9
|
from io import BytesIO
|
|
9
10
|
from typing import TYPE_CHECKING, Any
|
|
@@ -51,6 +52,29 @@ _GPT_IMAGE_SIZES: dict[str, str] = {
|
|
|
51
52
|
}
|
|
52
53
|
|
|
53
54
|
|
|
55
|
+
# ---------------------------------------------------------------------------
|
|
56
|
+
# Size parsing
|
|
57
|
+
# ---------------------------------------------------------------------------
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
def _parse_size(size: str | None) -> tuple[int, int] | None:
|
|
61
|
+
"""Parse an explicit ``"WIDTHxHEIGHT"`` string into an ``(width, height)`` pair.
|
|
62
|
+
|
|
63
|
+
:param size: Size string such as ``"768x512"`` (case-insensitive ``x``), or None.
|
|
64
|
+
:returns: ``(width, height)`` when *size* parses to two positive ints, else None.
|
|
65
|
+
"""
|
|
66
|
+
if not size:
|
|
67
|
+
return None
|
|
68
|
+
try:
|
|
69
|
+
w_str, h_str = size.lower().split("x", 1)
|
|
70
|
+
width, height = int(w_str), int(h_str)
|
|
71
|
+
except (ValueError, AttributeError):
|
|
72
|
+
return None
|
|
73
|
+
if width <= 0 or height <= 0:
|
|
74
|
+
return None
|
|
75
|
+
return width, height
|
|
76
|
+
|
|
77
|
+
|
|
54
78
|
# ---------------------------------------------------------------------------
|
|
55
79
|
# Synthesizer
|
|
56
80
|
# ---------------------------------------------------------------------------
|
|
@@ -79,7 +103,10 @@ class ImageSynthesizer:
|
|
|
79
103
|
def _load_mflux(self, model_name: str) -> Any:
|
|
80
104
|
if self._mflux_model is not None and self._mflux_model_name == model_name:
|
|
81
105
|
return self._mflux_model
|
|
82
|
-
|
|
106
|
+
|
|
107
|
+
Flux2Klein = importlib.import_module(
|
|
108
|
+
"mflux.models.flux2.variants.txt2img.flux2_klein"
|
|
109
|
+
).Flux2Klein
|
|
83
110
|
|
|
84
111
|
self._mflux_model = Flux2Klein(model_path=model_name)
|
|
85
112
|
self._mflux_model_name = model_name
|
|
@@ -97,6 +124,7 @@ class ImageSynthesizer:
|
|
|
97
124
|
seed: int | None = None,
|
|
98
125
|
steps: int | None = None,
|
|
99
126
|
model: str | None = None,
|
|
127
|
+
size: str | None = None,
|
|
100
128
|
) -> PILImage:
|
|
101
129
|
"""Generate an image and return a PIL Image.
|
|
102
130
|
|
|
@@ -105,6 +133,9 @@ class ImageSynthesizer:
|
|
|
105
133
|
:param seed: Random seed for reproducibility (random int if omitted).
|
|
106
134
|
:param steps: Override inference steps (mflux backends only; ignored for OpenAI).
|
|
107
135
|
:param model: Override the configured model for this single call.
|
|
136
|
+
:param size: Explicit ``"WIDTHxHEIGHT"`` override (mflux backends only). When given
|
|
137
|
+
and valid, it takes priority over the *aspect_ratio* size table. OpenAI
|
|
138
|
+
backends ignore it because they accept only a fixed set of sizes.
|
|
108
139
|
:returns: PIL Image.
|
|
109
140
|
"""
|
|
110
141
|
cfg = self._cfg
|
|
@@ -118,6 +149,7 @@ class ImageSynthesizer:
|
|
|
118
149
|
seed=seed,
|
|
119
150
|
steps=effective_steps,
|
|
120
151
|
aspect_ratio=aspect_ratio,
|
|
152
|
+
size=size,
|
|
121
153
|
)
|
|
122
154
|
elif cfg.backend == ImageBackend.MFLUX_SERVE:
|
|
123
155
|
return self._generate_via_server(
|
|
@@ -126,6 +158,7 @@ class ImageSynthesizer:
|
|
|
126
158
|
seed=seed,
|
|
127
159
|
steps=effective_steps,
|
|
128
160
|
aspect_ratio=aspect_ratio,
|
|
161
|
+
size=size,
|
|
129
162
|
)
|
|
130
163
|
else:
|
|
131
164
|
return self._generate_openai(
|
|
@@ -142,6 +175,7 @@ class ImageSynthesizer:
|
|
|
142
175
|
seed: int | None = None,
|
|
143
176
|
steps: int | None = None,
|
|
144
177
|
model: str | None = None,
|
|
178
|
+
size: str | None = None,
|
|
145
179
|
) -> str:
|
|
146
180
|
"""Generate an image and return it as a base64-encoded PNG string.
|
|
147
181
|
|
|
@@ -152,9 +186,12 @@ class ImageSynthesizer:
|
|
|
152
186
|
:param seed: Random seed for reproducibility.
|
|
153
187
|
:param steps: Override inference steps (mflux backends only).
|
|
154
188
|
:param model: Override the configured model for this single call.
|
|
189
|
+
:param size: Explicit ``"WIDTHxHEIGHT"`` override (mflux backends only).
|
|
155
190
|
:returns: Base64-encoded PNG string.
|
|
156
191
|
"""
|
|
157
|
-
img = self.generate(
|
|
192
|
+
img = self.generate(
|
|
193
|
+
prompt, aspect_ratio=aspect_ratio, seed=seed, steps=steps, model=model, size=size
|
|
194
|
+
)
|
|
158
195
|
buf = BytesIO()
|
|
159
196
|
img.save(buf, format="PNG")
|
|
160
197
|
return base64.b64encode(buf.getvalue()).decode()
|
|
@@ -171,8 +208,9 @@ class ImageSynthesizer:
|
|
|
171
208
|
seed: int | None,
|
|
172
209
|
steps: int,
|
|
173
210
|
aspect_ratio: str,
|
|
211
|
+
size: str | None = None,
|
|
174
212
|
) -> PILImage:
|
|
175
|
-
width, height = _MFLUX_SIZES.get(aspect_ratio, _MFLUX_SIZES["3:2"])
|
|
213
|
+
width, height = _parse_size(size) or _MFLUX_SIZES.get(aspect_ratio, _MFLUX_SIZES["3:2"])
|
|
176
214
|
effective_seed = seed if seed is not None else random.randint(0, 2**31 - 1)
|
|
177
215
|
flux = self._load_mflux(model)
|
|
178
216
|
result = flux.generate_image(
|
|
@@ -194,11 +232,12 @@ class ImageSynthesizer:
|
|
|
194
232
|
seed: int | None,
|
|
195
233
|
steps: int,
|
|
196
234
|
aspect_ratio: str,
|
|
235
|
+
size: str | None = None,
|
|
197
236
|
) -> PILImage:
|
|
198
237
|
import httpx
|
|
199
238
|
from PIL import Image # type: ignore[import-unresolved]
|
|
200
239
|
|
|
201
|
-
width, height = _MFLUX_SIZES.get(aspect_ratio, _MFLUX_SIZES["3:2"])
|
|
240
|
+
width, height = _parse_size(size) or _MFLUX_SIZES.get(aspect_ratio, _MFLUX_SIZES["3:2"])
|
|
202
241
|
payload: dict[str, Any] = {
|
|
203
242
|
"prompt": prompt,
|
|
204
243
|
"n": 1,
|
|
@@ -8,6 +8,7 @@ This module centralizes payload construction and response/error decoding for
|
|
|
8
8
|
from __future__ import annotations
|
|
9
9
|
|
|
10
10
|
import json
|
|
11
|
+
from typing import Any, cast
|
|
11
12
|
|
|
12
13
|
import httpx
|
|
13
14
|
|
|
@@ -29,8 +30,9 @@ def _format_error_data(error_data: object) -> str:
|
|
|
29
30
|
return str(decoded)
|
|
30
31
|
|
|
31
32
|
if isinstance(error_data, dict):
|
|
32
|
-
|
|
33
|
-
|
|
33
|
+
d = cast("dict[str, Any]", error_data)
|
|
34
|
+
err_type = d.get("error_type", "Unknown")
|
|
35
|
+
err_msg = d.get("error_message", str(error_data))
|
|
34
36
|
return f"{err_type}: {err_msg}"
|
|
35
37
|
|
|
36
38
|
return str(error_data)
|
|
@@ -41,10 +43,11 @@ def extract_worker_error(data: object) -> str | None:
|
|
|
41
43
|
if not isinstance(data, dict):
|
|
42
44
|
return str(data)
|
|
43
45
|
|
|
44
|
-
|
|
45
|
-
|
|
46
|
+
d = cast("dict[str, Any]", data)
|
|
47
|
+
if d.get("status") == "FAILED" or "error_type" in d:
|
|
48
|
+
return _format_error_data(d.get("error", d))
|
|
46
49
|
|
|
47
|
-
out =
|
|
50
|
+
out = d.get("output")
|
|
48
51
|
if isinstance(out, dict) and isinstance(out.get("error"), str):
|
|
49
52
|
return out["error"]
|
|
50
53
|
|
|
@@ -60,7 +63,8 @@ def decode_worker_response(data: object) -> dict:
|
|
|
60
63
|
if not isinstance(data, dict):
|
|
61
64
|
raise WorkerError(f"unexpected worker response type: {type(data).__name__}")
|
|
62
65
|
|
|
63
|
-
|
|
66
|
+
d2 = cast("dict[str, Any]", data)
|
|
67
|
+
out = d2.get("output", d2)
|
|
64
68
|
if not isinstance(out, dict):
|
|
65
69
|
raise WorkerError(f"unexpected worker output type: {type(out).__name__}")
|
|
66
70
|
return out
|
|
@@ -133,14 +137,18 @@ class WorkerClient:
|
|
|
133
137
|
image_backend: str = "",
|
|
134
138
|
aspect_ratio: str = "3:2",
|
|
135
139
|
steps: int | None = None,
|
|
140
|
+
size: str | None = None,
|
|
136
141
|
) -> tuple[str | None, str | None, str | None, str | None]:
|
|
137
|
-
|
|
142
|
+
inp: dict[str, Any] = {"op": "imagine", "prompt": prompt, "aspect_ratio": aspect_ratio}
|
|
138
143
|
if image_backend:
|
|
139
|
-
|
|
144
|
+
inp["image_backend"] = image_backend
|
|
140
145
|
if steps is not None:
|
|
141
|
-
|
|
146
|
+
inp["steps"] = steps
|
|
147
|
+
if size:
|
|
148
|
+
inp["size"] = size
|
|
142
149
|
if self._secret:
|
|
143
|
-
|
|
150
|
+
inp["secret"] = self._secret
|
|
151
|
+
payload: dict[str, Any] = {"input": inp}
|
|
144
152
|
|
|
145
153
|
try:
|
|
146
154
|
data = self._post(
|
|
@@ -48,24 +48,39 @@ def handle_aux_ops(
|
|
|
48
48
|
return {"error": "imagine requires a non-empty 'prompt'"}
|
|
49
49
|
|
|
50
50
|
aspect = inp.get("aspect_ratio", "3:2")
|
|
51
|
+
size = inp.get("size")
|
|
51
52
|
seed = inp.get("seed")
|
|
52
53
|
steps = inp.get("steps")
|
|
53
54
|
img_synth = image_synth_factory(inp.get("image_backend", ""))
|
|
55
|
+
seed_int = int(seed) if seed is not None else None
|
|
56
|
+
steps_int = int(steps) if steps is not None else None
|
|
54
57
|
|
|
55
58
|
try:
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
59
|
+
if size is not None:
|
|
60
|
+
b64 = img_synth.generate_b64(
|
|
61
|
+
prompt,
|
|
62
|
+
aspect_ratio=aspect,
|
|
63
|
+
seed=seed_int,
|
|
64
|
+
steps=steps_int,
|
|
65
|
+
size=size,
|
|
66
|
+
)
|
|
67
|
+
else:
|
|
68
|
+
b64 = img_synth.generate_b64(
|
|
69
|
+
prompt,
|
|
70
|
+
aspect_ratio=aspect,
|
|
71
|
+
seed=seed_int,
|
|
72
|
+
steps=steps_int,
|
|
73
|
+
)
|
|
74
|
+
result = {
|
|
63
75
|
"image_b64": b64,
|
|
64
76
|
"prompt": prompt,
|
|
65
77
|
"aspect_ratio": aspect,
|
|
66
78
|
"image_model": img_synth._cfg.resolved_model(), # pylint: disable=protected-access
|
|
67
79
|
"image_backend": img_synth._cfg.backend.value, # pylint: disable=protected-access
|
|
68
80
|
}
|
|
81
|
+
if size is not None:
|
|
82
|
+
result["size"] = size
|
|
83
|
+
return result
|
|
69
84
|
except Exception as exc: # noqa: BLE001 # pylint: disable=broad-exception-caught
|
|
70
85
|
return {"error": f"image generation failed: {exc}"}
|
|
71
86
|
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|