contextir 1.0.1__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.
- contextir/__init__.py +23 -0
- contextir/clients.py +180 -0
- contextir/dataset.py +121 -0
- contextir/evaluation/__init__.py +1 -0
- contextir/evaluation/metrics.py +94 -0
- contextir/gateway.py +757 -0
- contextir/models/__init__.py +1 -0
- contextir/models/baseline_translator.py +26 -0
- contextir/models/semantic_encoder.py +101 -0
- contextir/models/semantic_translator.py +74 -0
- contextir/pipeline.py +336 -0
- contextir/py.typed +1 -0
- contextir/schemas/__init__.py +13 -0
- contextir/schemas/contextir_contract_v2.schema.json +165 -0
- contextir/sir_dataset_export.py +172 -0
- contextir/sir_graph.py +155 -0
- contextir/sir_graph_core.py +261 -0
- contextir/sir_graph_embedding_core.py +306 -0
- contextir/sir_ml_core.py +253 -0
- contextir/sir_neural_kernel.py +287 -0
- contextir/sir_roundtrip.py +313 -0
- contextir/sir_runtime.py +543 -0
- contextir/sir_sources.py +328 -0
- contextir/tokenizer.py +20 -0
- contextir/utils/__init__.py +1 -0
- contextir/utils/config.py +17 -0
- contextir/utils/seed.py +12 -0
- contextir-1.0.1.dist-info/METADATA +244 -0
- contextir-1.0.1.dist-info/RECORD +33 -0
- contextir-1.0.1.dist-info/WHEEL +4 -0
- contextir-1.0.1.dist-info/entry_points.txt +2 -0
- contextir-1.0.1.dist-info/licenses/LICENSE +201 -0
- contextir-1.0.1.dist-info/licenses/NOTICE +7 -0
contextir/__init__.py
ADDED
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
from contextir.clients import ModelResponse, OllamaClient, OpenAICompatibleClient
|
|
2
|
+
from contextir.gateway import ContractCheck, ContextBundle, ContextIR, SIRKernel, load_contextir, load_kernel
|
|
3
|
+
from contextir.pipeline import ContextPipeline, PipelinePolicy, PipelineResult, PreparedContext, ResponseVerification
|
|
4
|
+
|
|
5
|
+
__version__ = "1.0.1"
|
|
6
|
+
|
|
7
|
+
__all__ = [
|
|
8
|
+
"ContextIR",
|
|
9
|
+
"ContextPipeline",
|
|
10
|
+
"ModelResponse",
|
|
11
|
+
"OllamaClient",
|
|
12
|
+
"OpenAICompatibleClient",
|
|
13
|
+
"PipelinePolicy",
|
|
14
|
+
"PipelineResult",
|
|
15
|
+
"PreparedContext",
|
|
16
|
+
"ResponseVerification",
|
|
17
|
+
"ContractCheck",
|
|
18
|
+
"ContextBundle",
|
|
19
|
+
"SIRKernel",
|
|
20
|
+
"__version__",
|
|
21
|
+
"load_contextir",
|
|
22
|
+
"load_kernel",
|
|
23
|
+
]
|
contextir/clients.py
ADDED
|
@@ -0,0 +1,180 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import json
|
|
4
|
+
import time
|
|
5
|
+
import urllib.error
|
|
6
|
+
import urllib.request
|
|
7
|
+
from dataclasses import dataclass
|
|
8
|
+
from typing import Any
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
@dataclass(frozen=True)
|
|
12
|
+
class ModelResponse:
|
|
13
|
+
text: str
|
|
14
|
+
prompt_tokens: int | None = None
|
|
15
|
+
output_tokens: int | None = None
|
|
16
|
+
prompt_ms: float | None = None
|
|
17
|
+
generation_ms: float | None = None
|
|
18
|
+
latency_ms: float | None = None
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
class OllamaClient:
|
|
22
|
+
"""Dependency-free callable client for a local Ollama chat endpoint."""
|
|
23
|
+
|
|
24
|
+
def __init__(
|
|
25
|
+
self,
|
|
26
|
+
model: str,
|
|
27
|
+
base_url: str = "http://127.0.0.1:11434",
|
|
28
|
+
timeout: float = 180,
|
|
29
|
+
context_length: int = 32768,
|
|
30
|
+
max_output_tokens: int = 256,
|
|
31
|
+
temperature: float = 0,
|
|
32
|
+
seed: int = 42,
|
|
33
|
+
) -> None:
|
|
34
|
+
if not model.strip():
|
|
35
|
+
raise ValueError("model must not be empty")
|
|
36
|
+
validate_endpoint(base_url, timeout, max_output_tokens)
|
|
37
|
+
if context_length < 1:
|
|
38
|
+
raise ValueError("context_length must be positive")
|
|
39
|
+
self.model = model
|
|
40
|
+
self.url = base_url.rstrip("/") + "/api/chat"
|
|
41
|
+
self.timeout = timeout
|
|
42
|
+
self.context_length = context_length
|
|
43
|
+
self.max_output_tokens = max_output_tokens
|
|
44
|
+
self.temperature = temperature
|
|
45
|
+
self.seed = seed
|
|
46
|
+
|
|
47
|
+
def __call__(self, prompt: str) -> str:
|
|
48
|
+
return self.complete(prompt).text
|
|
49
|
+
|
|
50
|
+
def complete(self, prompt: str) -> ModelResponse:
|
|
51
|
+
started = time.perf_counter()
|
|
52
|
+
result = post_json(
|
|
53
|
+
self.url,
|
|
54
|
+
{
|
|
55
|
+
"model": self.model,
|
|
56
|
+
"messages": [{"role": "user", "content": prompt}],
|
|
57
|
+
"stream": False,
|
|
58
|
+
"think": False,
|
|
59
|
+
"options": {
|
|
60
|
+
"temperature": self.temperature,
|
|
61
|
+
"seed": self.seed,
|
|
62
|
+
"num_ctx": self.context_length,
|
|
63
|
+
"num_predict": self.max_output_tokens,
|
|
64
|
+
},
|
|
65
|
+
},
|
|
66
|
+
timeout=self.timeout,
|
|
67
|
+
)
|
|
68
|
+
try:
|
|
69
|
+
text = str(result["message"]["content"]).strip()
|
|
70
|
+
except (KeyError, TypeError) as exc:
|
|
71
|
+
raise RuntimeError(f"invalid Ollama response from {self.url}: {exc}") from exc
|
|
72
|
+
return ModelResponse(
|
|
73
|
+
text=text,
|
|
74
|
+
prompt_tokens=as_int(result.get("prompt_eval_count")),
|
|
75
|
+
output_tokens=as_int(result.get("eval_count")),
|
|
76
|
+
prompt_ms=ns_to_ms(result.get("prompt_eval_duration")),
|
|
77
|
+
generation_ms=ns_to_ms(result.get("eval_duration")),
|
|
78
|
+
latency_ms=round((time.perf_counter() - started) * 1000, 3),
|
|
79
|
+
)
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
class OpenAICompatibleClient:
|
|
83
|
+
"""Dependency-free callable client for OpenAI-compatible chat endpoints."""
|
|
84
|
+
|
|
85
|
+
def __init__(
|
|
86
|
+
self,
|
|
87
|
+
model: str,
|
|
88
|
+
base_url: str = "http://127.0.0.1:1234/v1",
|
|
89
|
+
api_key: str = "",
|
|
90
|
+
timeout: float = 180,
|
|
91
|
+
max_output_tokens: int = 256,
|
|
92
|
+
temperature: float = 0,
|
|
93
|
+
seed: int | None = 42,
|
|
94
|
+
) -> None:
|
|
95
|
+
if not model.strip():
|
|
96
|
+
raise ValueError("model must not be empty")
|
|
97
|
+
validate_endpoint(base_url, timeout, max_output_tokens)
|
|
98
|
+
self.model = model
|
|
99
|
+
self.url = base_url.rstrip("/") + "/chat/completions"
|
|
100
|
+
self.api_key = api_key
|
|
101
|
+
self.timeout = timeout
|
|
102
|
+
self.max_output_tokens = max_output_tokens
|
|
103
|
+
self.temperature = temperature
|
|
104
|
+
self.seed = seed
|
|
105
|
+
|
|
106
|
+
def __call__(self, prompt: str) -> str:
|
|
107
|
+
return self.complete(prompt).text
|
|
108
|
+
|
|
109
|
+
def complete(self, prompt: str) -> ModelResponse:
|
|
110
|
+
payload: dict[str, Any] = {
|
|
111
|
+
"model": self.model,
|
|
112
|
+
"messages": [{"role": "user", "content": prompt}],
|
|
113
|
+
"temperature": self.temperature,
|
|
114
|
+
"max_tokens": self.max_output_tokens,
|
|
115
|
+
}
|
|
116
|
+
if self.seed is not None:
|
|
117
|
+
payload["seed"] = self.seed
|
|
118
|
+
started = time.perf_counter()
|
|
119
|
+
result = post_json(
|
|
120
|
+
self.url,
|
|
121
|
+
payload,
|
|
122
|
+
timeout=self.timeout,
|
|
123
|
+
headers={"Authorization": f"Bearer {self.api_key}"} if self.api_key else None,
|
|
124
|
+
)
|
|
125
|
+
usage = result.get("usage", {})
|
|
126
|
+
try:
|
|
127
|
+
text = str(result["choices"][0]["message"]["content"]).strip()
|
|
128
|
+
except (IndexError, KeyError, TypeError) as exc:
|
|
129
|
+
raise RuntimeError(f"invalid OpenAI-compatible response from {self.url}: {exc}") from exc
|
|
130
|
+
return ModelResponse(
|
|
131
|
+
text=text,
|
|
132
|
+
prompt_tokens=as_int(usage.get("prompt_tokens")),
|
|
133
|
+
output_tokens=as_int(usage.get("completion_tokens")),
|
|
134
|
+
latency_ms=round((time.perf_counter() - started) * 1000, 3),
|
|
135
|
+
)
|
|
136
|
+
|
|
137
|
+
|
|
138
|
+
def post_json(
|
|
139
|
+
url: str,
|
|
140
|
+
payload: dict[str, Any],
|
|
141
|
+
timeout: float,
|
|
142
|
+
headers: dict[str, str] | None = None,
|
|
143
|
+
) -> dict[str, Any]:
|
|
144
|
+
request_headers = {"Content-Type": "application/json", **(headers or {})}
|
|
145
|
+
request = urllib.request.Request(
|
|
146
|
+
url,
|
|
147
|
+
data=json.dumps(payload).encode("utf-8"),
|
|
148
|
+
headers=request_headers,
|
|
149
|
+
method="POST",
|
|
150
|
+
)
|
|
151
|
+
try:
|
|
152
|
+
with urllib.request.urlopen(request, timeout=timeout) as response:
|
|
153
|
+
return json.loads(response.read().decode("utf-8"))
|
|
154
|
+
except urllib.error.HTTPError as exc:
|
|
155
|
+
detail = exc.read().decode("utf-8", errors="replace")
|
|
156
|
+
raise RuntimeError(f"HTTP {exc.code} from {url}: {detail}") from exc
|
|
157
|
+
except urllib.error.URLError as exc:
|
|
158
|
+
raise RuntimeError(f"cannot reach model endpoint {url}: {exc.reason}") from exc
|
|
159
|
+
except (KeyError, TypeError, ValueError, json.JSONDecodeError) as exc:
|
|
160
|
+
raise RuntimeError(f"invalid model response from {url}: {exc}") from exc
|
|
161
|
+
|
|
162
|
+
|
|
163
|
+
def as_int(value: Any) -> int | None:
|
|
164
|
+
return int(value) if value is not None else None
|
|
165
|
+
|
|
166
|
+
|
|
167
|
+
def ns_to_ms(value: Any) -> float | None:
|
|
168
|
+
return round(float(value) / 1_000_000, 3) if value is not None else None
|
|
169
|
+
|
|
170
|
+
|
|
171
|
+
def validate_endpoint(base_url: str, timeout: float, max_output_tokens: int) -> None:
|
|
172
|
+
if not base_url.startswith(("http://", "https://")):
|
|
173
|
+
raise ValueError("base_url must use http or https")
|
|
174
|
+
if timeout <= 0:
|
|
175
|
+
raise ValueError("timeout must be positive")
|
|
176
|
+
if max_output_tokens < 1:
|
|
177
|
+
raise ValueError("max_output_tokens must be positive")
|
|
178
|
+
|
|
179
|
+
|
|
180
|
+
__all__ = ["ModelResponse", "OllamaClient", "OpenAICompatibleClient"]
|
contextir/dataset.py
ADDED
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import json
|
|
4
|
+
import random
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
from typing import Iterable
|
|
7
|
+
|
|
8
|
+
from contextir.tokenizer import normalize
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
ENTITIES = [
|
|
12
|
+
("cat", "кошка", "the cat", ["кошка", "животное", "питомец"], ["the cat", "an animal", "a pet"]),
|
|
13
|
+
("dog", "собака", "the dog", ["собака", "пёс", "животное"], ["the dog", "a dog", "an animal"]),
|
|
14
|
+
("child", "ребёнок", "the child", ["ребёнок", "мальчик", "девочка"], ["the child", "a kid", "the youngster"]),
|
|
15
|
+
("robot", "робот", "the robot", ["робот", "машина", "автомат"], ["the robot", "a machine", "the automaton"]),
|
|
16
|
+
("teacher", "учитель", "the teacher", ["учитель", "преподаватель", "наставник"], ["the teacher", "the instructor", "the mentor"]),
|
|
17
|
+
("bird", "птица", "the bird", ["птица", "пернатое", "животное"], ["the bird", "a bird", "the animal"]),
|
|
18
|
+
]
|
|
19
|
+
|
|
20
|
+
ACTIONS = [
|
|
21
|
+
("sitting", "сидит", "is sitting", ["сидит", "находится", "расположена"], ["is sitting", "is located", "is"]),
|
|
22
|
+
("standing", "стоит", "is standing", ["стоит", "находится", "расположена"], ["is standing", "is located", "is"]),
|
|
23
|
+
("sleeping", "спит", "is sleeping", ["спит", "дремлет", "отдыхает"], ["is sleeping", "is napping", "is resting"]),
|
|
24
|
+
("looking", "смотрит", "is looking", ["смотрит", "наблюдает", "глядит"], ["is looking", "is watching", "is gazing"]),
|
|
25
|
+
("walking", "идёт", "is walking", ["идёт", "шагает", "движется"], ["is walking", "is moving", "is going"]),
|
|
26
|
+
]
|
|
27
|
+
|
|
28
|
+
PLACES = [
|
|
29
|
+
("table", "на столе", "on the table", ["на столе", "сверху стола", "у стола"], ["on the table", "at the table", "near the table"]),
|
|
30
|
+
("window", "у окна", "by the window", ["у окна", "возле окна", "рядом с окном"], ["by the window", "near the window", "at the window"]),
|
|
31
|
+
("garden", "в саду", "in the garden", ["в саду", "среди растений", "на участке"], ["in the garden", "among plants", "outside"]),
|
|
32
|
+
("room", "в комнате", "in the room", ["в комнате", "в помещении", "дома"], ["in the room", "indoors", "inside"]),
|
|
33
|
+
("street", "на улице", "on the street", ["на улице", "на дороге", "снаружи"], ["on the street", "on the road", "outside"]),
|
|
34
|
+
]
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def _record(entity: tuple, action: tuple, place: tuple) -> dict:
|
|
38
|
+
eid, ru_entity, en_entity, ru_entity_alts, en_entity_alts = entity
|
|
39
|
+
aid, ru_action, en_action, ru_action_alts, en_action_alts = action
|
|
40
|
+
pid, ru_place, en_place, ru_place_alts, en_place_alts = place
|
|
41
|
+
meaning_id = f"m_{eid}_{aid}_{pid}"
|
|
42
|
+
ru = f"{ru_entity} {ru_action} {ru_place}"
|
|
43
|
+
en = f"{en_entity} {en_action} {en_place}"
|
|
44
|
+
ru_paraphrases = [
|
|
45
|
+
f"{ru_place_alts[1]} {ru_action_alts[1]} {ru_entity_alts[0]}",
|
|
46
|
+
f"{ru_entity_alts[1]} {ru_action_alts[2]} {ru_place_alts[2]}",
|
|
47
|
+
f"{ru_entity_alts[2]} {ru_action_alts[0]} {ru_place_alts[0]}",
|
|
48
|
+
]
|
|
49
|
+
en_paraphrases = [
|
|
50
|
+
f"{en_entity_alts[1]} {en_action_alts[1]} {en_place_alts[1]}",
|
|
51
|
+
f"{en_entity_alts[2]} {en_action_alts[2]} {en_place_alts[2]}",
|
|
52
|
+
f"{en_entity_alts[0]} {en_action_alts[0]} {en_place_alts[0]}",
|
|
53
|
+
]
|
|
54
|
+
atoms = [f"object:{eid}", f"state:{aid}", f"place:{pid}"]
|
|
55
|
+
return {
|
|
56
|
+
"meaning_id": meaning_id,
|
|
57
|
+
"ru": ru,
|
|
58
|
+
"en": en,
|
|
59
|
+
"ru_paraphrases": ru_paraphrases,
|
|
60
|
+
"en_paraphrases": en_paraphrases,
|
|
61
|
+
"semantic_atoms": atoms,
|
|
62
|
+
"definition_ru": f"смысл: {ru_entity} {ru_action} {ru_place}",
|
|
63
|
+
"definition_en": f"meaning: {en_entity} {en_action} {en_place}",
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
def build_meanings() -> list[dict]:
|
|
68
|
+
return [_record(e, a, p) for e in ENTITIES for a in ACTIONS for p in PLACES]
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
def variants(record: dict, lang: str) -> list[str]:
|
|
72
|
+
if lang == "ru":
|
|
73
|
+
return [record["ru"], *record["ru_paraphrases"], record["definition_ru"]]
|
|
74
|
+
if lang == "en":
|
|
75
|
+
return [record["en"], *record["en_paraphrases"], record["definition_en"]]
|
|
76
|
+
raise ValueError(f"unsupported language: {lang}")
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
def paired_examples(records: Iterable[dict]) -> list[dict]:
|
|
80
|
+
rows: list[dict] = []
|
|
81
|
+
for rec in records:
|
|
82
|
+
for ru_text in variants(rec, "ru"):
|
|
83
|
+
rows.append({"meaning_id": rec["meaning_id"], "source_lang": "ru", "target_lang": "en", "source": ru_text, "target": rec["en"], "semantic_atoms": rec["semantic_atoms"]})
|
|
84
|
+
for en_text in variants(rec, "en"):
|
|
85
|
+
rows.append({"meaning_id": rec["meaning_id"], "source_lang": "en", "target_lang": "ru", "source": en_text, "target": rec["ru"], "semantic_atoms": rec["semantic_atoms"]})
|
|
86
|
+
return rows
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
def write_jsonl(path: str | Path, rows: Iterable[dict]) -> None:
|
|
90
|
+
with Path(path).open("w", encoding="utf-8") as f:
|
|
91
|
+
for row in rows:
|
|
92
|
+
f.write(json.dumps(row, ensure_ascii=False) + "\n")
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
def read_jsonl(path: str | Path) -> list[dict]:
|
|
96
|
+
with Path(path).open("r", encoding="utf-8") as f:
|
|
97
|
+
return [json.loads(line) for line in f if line.strip()]
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
def split_examples(rows: list[dict], seed: int, train_size: int, valid_size: int, test_size: int) -> tuple[list[dict], list[dict], list[dict]]:
|
|
101
|
+
grouped: dict[str, list[dict]] = {}
|
|
102
|
+
for row in rows:
|
|
103
|
+
grouped.setdefault(row["meaning_id"], []).append(row)
|
|
104
|
+
rng = random.Random(seed)
|
|
105
|
+
train: list[dict] = []
|
|
106
|
+
valid: list[dict] = []
|
|
107
|
+
test: list[dict] = []
|
|
108
|
+
for group in grouped.values():
|
|
109
|
+
rng.shuffle(group)
|
|
110
|
+
train.extend(group[:6])
|
|
111
|
+
valid.extend(group[6:8])
|
|
112
|
+
test.extend(group[8:])
|
|
113
|
+
rng.shuffle(train)
|
|
114
|
+
rng.shuffle(valid)
|
|
115
|
+
rng.shuffle(test)
|
|
116
|
+
return train[:train_size], valid[:valid_size], test[:test_size]
|
|
117
|
+
|
|
118
|
+
|
|
119
|
+
def phrase_index(rows: Iterable[dict]) -> dict[str, dict]:
|
|
120
|
+
return {normalize(row["source"]): row for row in rows}
|
|
121
|
+
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import difflib
|
|
4
|
+
import time
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
|
|
7
|
+
import numpy as np
|
|
8
|
+
|
|
9
|
+
from contextir.models.baseline_translator import BaselineTranslator
|
|
10
|
+
from contextir.models.semantic_encoder import SemanticEncoder
|
|
11
|
+
from contextir.models.semantic_translator import SemanticTranslator, cosine
|
|
12
|
+
from contextir.tokenizer import normalize, token_accuracy
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def char_similarity(pred: str, gold: str) -> float:
|
|
16
|
+
return difflib.SequenceMatcher(None, normalize(pred), normalize(gold)).ratio()
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def translation_metrics(predictions: list[tuple[str, str]]) -> dict[str, float]:
|
|
20
|
+
if not predictions:
|
|
21
|
+
return {"exact_match": 0.0, "token_accuracy": 0.0, "char_similarity": 0.0}
|
|
22
|
+
exact = [normalize(p) == normalize(g) for p, g in predictions]
|
|
23
|
+
return {
|
|
24
|
+
"exact_match": float(np.mean(exact)),
|
|
25
|
+
"token_accuracy": float(np.mean([token_accuracy(p, g) for p, g in predictions])),
|
|
26
|
+
"char_similarity": float(np.mean([char_similarity(p, g) for p, g in predictions])),
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def semantic_coherence(rows: list[dict], encoder: SemanticEncoder) -> dict[str, float]:
|
|
31
|
+
vectors = [(row["meaning_id"], encoder.encode_text(row["source"])) for row in rows]
|
|
32
|
+
intra: list[float] = []
|
|
33
|
+
inter: list[float] = []
|
|
34
|
+
for i in range(len(vectors)):
|
|
35
|
+
for j in range(i + 1, len(vectors)):
|
|
36
|
+
score = cosine(vectors[i][1], vectors[j][1])
|
|
37
|
+
if vectors[i][0] == vectors[j][0]:
|
|
38
|
+
intra.append(score)
|
|
39
|
+
else:
|
|
40
|
+
inter.append(score)
|
|
41
|
+
intra_mean = float(np.mean(intra)) if intra else 0.0
|
|
42
|
+
inter_mean = float(np.mean(inter)) if inter else 0.0
|
|
43
|
+
return {
|
|
44
|
+
"intra_cluster_similarity": intra_mean,
|
|
45
|
+
"inter_cluster_similarity": inter_mean,
|
|
46
|
+
"semantic_gap": intra_mean - inter_mean,
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
def evaluate_baseline(rows: list[dict], baseline: BaselineTranslator) -> dict[str, float]:
|
|
51
|
+
return translation_metrics([(baseline.translate(row["source"]), row["target"]) for row in rows])
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
def evaluate_semantic(rows: list[dict], encoder: SemanticEncoder, translator: SemanticTranslator) -> dict[str, float]:
|
|
55
|
+
preds = []
|
|
56
|
+
for row in rows:
|
|
57
|
+
pred, _, _, _ = translator.translate(row["source"], row["target_lang"], encoder)
|
|
58
|
+
preds.append((pred, row["target"]))
|
|
59
|
+
return translation_metrics(preds)
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
def cycle_consistency(rows: list[dict], encoder: SemanticEncoder, translator: SemanticTranslator) -> float:
|
|
63
|
+
scores: list[float] = []
|
|
64
|
+
for row in rows:
|
|
65
|
+
original = encoder.encode_text(row["source"])
|
|
66
|
+
translated, _, _, _ = translator.translate(row["source"], row["target_lang"], encoder)
|
|
67
|
+
after = encoder.encode_text(translated)
|
|
68
|
+
scores.append(cosine(original, after))
|
|
69
|
+
return float(np.mean(scores)) if scores else 0.0
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
def ablation_quality(rows: list[dict], encoder: SemanticEncoder, translator: SemanticTranslator, seed: int) -> dict[str, float]:
|
|
73
|
+
rng = np.random.default_rng(seed)
|
|
74
|
+
vectors = [encoder.encode_text(row["source"]) for row in rows]
|
|
75
|
+
rng.shuffle(vectors)
|
|
76
|
+
preds = []
|
|
77
|
+
for row, vector in zip(rows, vectors):
|
|
78
|
+
pred, _, _ = translator.translate_vector(vector, row["target_lang"])
|
|
79
|
+
preds.append((pred, row["target"]))
|
|
80
|
+
return translation_metrics(preds)
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
def latency_ms(rows: list[dict], fn) -> float:
|
|
84
|
+
start = time.perf_counter()
|
|
85
|
+
for row in rows:
|
|
86
|
+
fn(row)
|
|
87
|
+
elapsed = time.perf_counter() - start
|
|
88
|
+
return float((elapsed / max(len(rows), 1)) * 1000)
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
def file_size(path: str | Path) -> int:
|
|
92
|
+
p = Path(path)
|
|
93
|
+
return p.stat().st_size if p.exists() else 0
|
|
94
|
+
|