champollion 0.3.3
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.
- package/LICENSE +133 -0
- package/README.md +387 -0
- package/bin/cli.js +278 -0
- package/index.js +135 -0
- package/lib/api-key.js +127 -0
- package/lib/autofix.js +432 -0
- package/lib/bridge/method_bridge.py +430 -0
- package/lib/card-source-resolution.mjs +284 -0
- package/lib/cards/cache.js +169 -0
- package/lib/cards/env.js +82 -0
- package/lib/cards/fetch-card-child.js +38 -0
- package/lib/cards/reader.js +435 -0
- package/lib/cards/refresh.js +111 -0
- package/lib/cards/remote.js +387 -0
- package/lib/cldf-export.mjs +540 -0
- package/lib/cldf-terms.mjs +62 -0
- package/lib/command-help.js +790 -0
- package/lib/commands/audit.js +49 -0
- package/lib/commands/card.js +454 -0
- package/lib/commands/doctor.js +559 -0
- package/lib/commands/fonts.js +489 -0
- package/lib/commands/help.js +91 -0
- package/lib/commands/init.js +1259 -0
- package/lib/commands/integrity.js +148 -0
- package/lib/commands/leaderboard.js +478 -0
- package/lib/commands/lint.js +30 -0
- package/lib/commands/models.js +177 -0
- package/lib/commands/plugin.js +103 -0
- package/lib/commands/provenance.js +45 -0
- package/lib/commands/recommend.js +75 -0
- package/lib/commands/register-corpus.js +678 -0
- package/lib/commands/repair-script.js +42 -0
- package/lib/commands/seal-corpus.js +355 -0
- package/lib/commands/seo.js +72 -0
- package/lib/commands/serve.js +147 -0
- package/lib/commands/status.js +265 -0
- package/lib/commands/submit.js +332 -0
- package/lib/commands/sync.js +89 -0
- package/lib/commands/tm.js +573 -0
- package/lib/commands/verify.js +39 -0
- package/lib/commands/watch.js +20 -0
- package/lib/commands/wrap.js +138 -0
- package/lib/commands/xliff.js +327 -0
- package/lib/commercial-eligibility.js +235 -0
- package/lib/concurrent.js +87 -0
- package/lib/config.js +523 -0
- package/lib/contamination-lane.js +76 -0
- package/lib/content-sync.js +731 -0
- package/lib/content.js +733 -0
- package/lib/corpus-registration.mjs +608 -0
- package/lib/cost-report.js +346 -0
- package/lib/diff.js +155 -0
- package/lib/docusaurus-sync.js +1256 -0
- package/lib/flatten.js +55 -0
- package/lib/format.js +954 -0
- package/lib/hash.js +159 -0
- package/lib/icu.js +473 -0
- package/lib/integrity.js +689 -0
- package/lib/license-gate.mjs +478 -0
- package/lib/license-identify.mjs +229 -0
- package/lib/lint.js +629 -0
- package/lib/method-manifest.js +60 -0
- package/lib/methods/anthropic.js +140 -0
- package/lib/methods/apertium.js +163 -0
- package/lib/methods/api.js +316 -0
- package/lib/methods/base.js +184 -0
- package/lib/methods/content-separator.js +45 -0
- package/lib/methods/deepl.js +426 -0
- package/lib/methods/direct-llm.js +586 -0
- package/lib/methods/external.js +332 -0
- package/lib/methods/fetch-with-retry.js +124 -0
- package/lib/methods/gemini.js +147 -0
- package/lib/methods/google-translate.js +402 -0
- package/lib/methods/http-utils.js +122 -0
- package/lib/methods/libretranslate.js +314 -0
- package/lib/methods/llm-coached.js +670 -0
- package/lib/methods/llm.js +592 -0
- package/lib/methods/local.js +76 -0
- package/lib/methods/microsoft-translator.js +331 -0
- package/lib/methods/openai.js +131 -0
- package/lib/methods/openrouter-client.js +327 -0
- package/lib/methods/openrouter-pricing.js +156 -0
- package/lib/methods/provider-env.js +115 -0
- package/lib/methods/provider-pricing.js +310 -0
- package/lib/methods/tilde.js +150 -0
- package/lib/methods/translated.js +229 -0
- package/lib/methods/translation-error.js +80 -0
- package/lib/models.js +258 -0
- package/lib/no-translate.js +233 -0
- package/lib/output.js +238 -0
- package/lib/pairs.js +547 -0
- package/lib/plugins.js +447 -0
- package/lib/provenance.js +323 -0
- package/lib/recommend.js +648 -0
- package/lib/registers.js +1185 -0
- package/lib/repair-script.js +266 -0
- package/lib/scripts.js +994 -0
- package/lib/seal.mjs +464 -0
- package/lib/sealed-qualifier.mjs +211 -0
- package/lib/security.js +59 -0
- package/lib/segment.js +369 -0
- package/lib/seo.js +275 -0
- package/lib/serve.js +854 -0
- package/lib/string-classify.js +85 -0
- package/lib/submit.mjs +344 -0
- package/lib/sync.js +969 -0
- package/lib/tags/bcp47.js +202 -0
- package/lib/tags/resolve.js +314 -0
- package/lib/terminology.js +111 -0
- package/lib/tm-seed.js +294 -0
- package/lib/tm.js +515 -0
- package/lib/translate-pair.js +197 -0
- package/lib/translate.js +203 -0
- package/lib/types.js +230 -0
- package/lib/validate.js +510 -0
- package/lib/verify.js +451 -0
- package/lib/watch.js +145 -0
- package/lib/xliff.js +184 -0
- package/package.json +93 -0
- package/shared/ATTRIBUTION.md +145 -0
- package/shared/CORPORA-CARDS.md +288 -0
- package/shared/DATA-SOVEREIGNTY.md +500 -0
- package/shared/LANGUAGE-CARD-FIELDS.md +532 -0
- package/shared/card-lint-baseline.json +3189 -0
- package/shared/cards-fallback.json +1 -0
- package/shared/catalogue/card-config.json +6091 -0
- package/shared/catalogue/external-results.json +3888 -0
- package/shared/catalogue/gender-guidance.json +1038 -0
- package/shared/catalogue/method-coverage.json +1751 -0
- package/shared/catalogue/metric-coverage.json +170 -0
- package/shared/catalogue/metric-reliability.json +1 -0
- package/shared/catalogue/register-presets.json +3180 -0
- package/shared/catalogue/vitality-scales.json +55 -0
- package/shared/cldr-index.json +1115 -0
- package/shared/code-bridge.json +253 -0
- package/shared/corpora-cards-v1-reference.md +281 -0
- package/shared/curated-dictionary-flags.json +35 -0
- package/shared/curated-endonyms.json +35 -0
- package/shared/curated-fsts.json +51 -0
- package/shared/curated-orthography-conventions.json +26 -0
- package/shared/curated-sil-resources.json +374 -0
- package/shared/curated-tools.json +41 -0
- package/shared/docent/corpus.json +11333 -0
- package/shared/docent/faq.en.json +564 -0
- package/shared/docent/register-blocks.json +60 -0
- package/shared/docent/system-prompt.md +144 -0
- package/shared/domain-taxonomy.json +35 -0
- package/shared/explainers/glossary.json +2975 -0
- package/shared/explainers/tc-features.json +20112 -0
- package/shared/explainers/term-watchlist.json +147 -0
- package/shared/human-services.json +59 -0
- package/shared/license-corrections.json +261 -0
- package/shared/license-evidence.json +13452 -0
- package/shared/licenses.json +6781 -0
- package/shared/method-registry.json +236 -0
- package/shared/metric-registry.json +620 -0
- package/shared/model-aliases.json +7 -0
- package/shared/schemas/champollion-plugin.schema.json +206 -0
- package/shared/schemas/corpora-card.schema.json +957 -0
- package/shared/schemas/domain-taxonomy.schema.json +64 -0
- package/shared/schemas/external-results.schema.json +314 -0
- package/shared/schemas/human-services.schema.json +90 -0
- package/shared/schemas/language-card.schema.json +1308 -0
- package/shared/schemas/licenses.schema.json +155 -0
- package/shared/schemas/method-card.schema.json +412 -0
- package/shared/schemas/method-registry.schema.json +85 -0
- package/shared/schemas/metric-registry.schema.json +96 -0
- package/shared/schemas/metric-reliability.schema.json +178 -0
- package/shared/schemas/model-aliases.schema.json +27 -0
- package/shared/schemas/source-snapshot.schema.json +96 -0
|
@@ -0,0 +1,430 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Method Bridge — ships with the champollion CLI npm package.
|
|
3
|
+
|
|
4
|
+
Loads Python translation method modules and exposes them via
|
|
5
|
+
stdin/stdout JSON-lines protocol. Self-contained — no Arena dependency.
|
|
6
|
+
|
|
7
|
+
The CLI's ExternalMethod class spawns this as a subprocess:
|
|
8
|
+
python cli/lib/bridge/method_bridge.py --method path/to/method_plugin
|
|
9
|
+
|
|
10
|
+
The bridge also supports standalone discovery mode:
|
|
11
|
+
python cli/lib/bridge/method_bridge.py --discover
|
|
12
|
+
|
|
13
|
+
Protocol (JSON-lines over stdin/stdout):
|
|
14
|
+
stdin: one JSON object per line with an "action" field
|
|
15
|
+
stdout: one JSON response per line with an "ok" field
|
|
16
|
+
stderr: human-readable logging (not parsed by CLI)
|
|
17
|
+
|
|
18
|
+
Actions:
|
|
19
|
+
discover → list installed method modules (via pip entry points)
|
|
20
|
+
translate → translate key-value pairs through the loaded method
|
|
21
|
+
method_card → return method metadata
|
|
22
|
+
shutdown → clean exit
|
|
23
|
+
|
|
24
|
+
This module has ZERO imports beyond Python 3.10+ stdlib (except for
|
|
25
|
+
the method modules themselves, which bring their own dependencies).
|
|
26
|
+
|
|
27
|
+
Why self-contained:
|
|
28
|
+
The bridge inlines the method_loader logic (~60 lines) so the user
|
|
29
|
+
does NOT need to pip install the Arena. Only the method module
|
|
30
|
+
(e.g., pip install crk-translate) is required.
|
|
31
|
+
|
|
32
|
+
Maintained alongside the Arena's method_loader.py — if the manifest
|
|
33
|
+
format changes there, it must be updated here. Both live in the same
|
|
34
|
+
monorepo so this is easy to track.
|
|
35
|
+
"""
|
|
36
|
+
|
|
37
|
+
from __future__ import annotations
|
|
38
|
+
|
|
39
|
+
import asyncio
|
|
40
|
+
import importlib.metadata
|
|
41
|
+
import importlib.util
|
|
42
|
+
import json
|
|
43
|
+
import logging
|
|
44
|
+
import sys
|
|
45
|
+
from pathlib import Path
|
|
46
|
+
|
|
47
|
+
logger = logging.getLogger("champollion.bridge")
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
# ── Inline method loader ─────────────────────────────────────────────
|
|
51
|
+
#
|
|
52
|
+
# Self-contained reimplementation of arena/mt_eval_harness/method_loader.py.
|
|
53
|
+
# Reads method.json, parses the entry_point, loads the Python module,
|
|
54
|
+
# and instantiates the TranslationMethod class.
|
|
55
|
+
#
|
|
56
|
+
# Kept in sync manually. If method.json schema changes in the Arena's
|
|
57
|
+
# method_loader.py, update this section to match.
|
|
58
|
+
|
|
59
|
+
_REQUIRED_MANIFEST_FIELDS = {"name", "entry_point"}
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
class MethodLoadError(Exception):
|
|
63
|
+
"""Raised when a method plugin cannot be loaded."""
|
|
64
|
+
pass
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
def _load_method(method_dir: Path) -> tuple:
|
|
68
|
+
"""Load a TranslationMethod from a plugin directory.
|
|
69
|
+
|
|
70
|
+
Returns:
|
|
71
|
+
(instance, manifest) tuple — the method object and its parsed
|
|
72
|
+
method.json for metadata access.
|
|
73
|
+
|
|
74
|
+
Raises:
|
|
75
|
+
MethodLoadError with human-readable messages explaining what
|
|
76
|
+
went wrong and what the user should do about it.
|
|
77
|
+
"""
|
|
78
|
+
method_dir = Path(method_dir).resolve()
|
|
79
|
+
|
|
80
|
+
if not method_dir.is_dir():
|
|
81
|
+
raise MethodLoadError(
|
|
82
|
+
f"Not a directory: {method_dir}\n"
|
|
83
|
+
f" Expected a method plugin directory containing method.json.\n"
|
|
84
|
+
f" If you installed a method module via pip, try:\n"
|
|
85
|
+
f" python {__file__} --discover"
|
|
86
|
+
)
|
|
87
|
+
|
|
88
|
+
manifest_path = method_dir / "method.json"
|
|
89
|
+
if not manifest_path.exists():
|
|
90
|
+
raise MethodLoadError(
|
|
91
|
+
f"No method.json found in {method_dir}\n"
|
|
92
|
+
f" A method plugin directory must contain a method.json manifest.\n"
|
|
93
|
+
f" See: https://champollion.dev/docs/guides/serving-a-method"
|
|
94
|
+
)
|
|
95
|
+
|
|
96
|
+
try:
|
|
97
|
+
manifest = json.loads(manifest_path.read_text(encoding="utf-8"))
|
|
98
|
+
except json.JSONDecodeError as e:
|
|
99
|
+
raise MethodLoadError(
|
|
100
|
+
f"Invalid JSON in {manifest_path}: {e}"
|
|
101
|
+
) from e
|
|
102
|
+
|
|
103
|
+
missing = _REQUIRED_MANIFEST_FIELDS - set(manifest.keys())
|
|
104
|
+
if missing:
|
|
105
|
+
raise MethodLoadError(
|
|
106
|
+
f"method.json is missing required fields: {sorted(missing)}.\n"
|
|
107
|
+
f" Required: {sorted(_REQUIRED_MANIFEST_FIELDS)}"
|
|
108
|
+
)
|
|
109
|
+
|
|
110
|
+
entry_point = manifest["entry_point"]
|
|
111
|
+
if ":" not in entry_point:
|
|
112
|
+
raise MethodLoadError(
|
|
113
|
+
f"Invalid entry_point: '{entry_point}'.\n"
|
|
114
|
+
f" Expected format: 'module_name:ClassName'\n"
|
|
115
|
+
f" Example: 'pipeline:CrkPipelineMethod'"
|
|
116
|
+
)
|
|
117
|
+
|
|
118
|
+
module_name, class_name = entry_point.split(":", 1)
|
|
119
|
+
module_file = method_dir / f"{module_name}.py"
|
|
120
|
+
if not module_file.exists():
|
|
121
|
+
raise MethodLoadError(
|
|
122
|
+
f"Entry point module not found: {module_file}\n"
|
|
123
|
+
f" The entry_point '{entry_point}' expects {module_name}.py\n"
|
|
124
|
+
f" in {method_dir}"
|
|
125
|
+
)
|
|
126
|
+
|
|
127
|
+
# Add method dir + parent to sys.path so the plugin can import siblings
|
|
128
|
+
# (e.g., a method package imported from its method_plugin/ directory)
|
|
129
|
+
for p in [str(method_dir), str(method_dir.parent)]:
|
|
130
|
+
if p not in sys.path:
|
|
131
|
+
sys.path.insert(0, p)
|
|
132
|
+
|
|
133
|
+
spec = importlib.util.spec_from_file_location(
|
|
134
|
+
f"method_plugin.{module_name}", module_file
|
|
135
|
+
)
|
|
136
|
+
if spec is None or spec.loader is None:
|
|
137
|
+
raise MethodLoadError(
|
|
138
|
+
f"Python could not create a module spec for {module_file}"
|
|
139
|
+
)
|
|
140
|
+
|
|
141
|
+
try:
|
|
142
|
+
module = importlib.util.module_from_spec(spec)
|
|
143
|
+
spec.loader.exec_module(module)
|
|
144
|
+
except Exception as e:
|
|
145
|
+
raise MethodLoadError(
|
|
146
|
+
f"Failed to load {module_file}: {type(e).__name__}: {e}\n"
|
|
147
|
+
f" Check that the method module's dependencies are installed.\n"
|
|
148
|
+
f" Try: pip install -e {method_dir.parent}"
|
|
149
|
+
) from e
|
|
150
|
+
|
|
151
|
+
if not hasattr(module, class_name):
|
|
152
|
+
available = [n for n in dir(module) if not n.startswith("_")]
|
|
153
|
+
raise MethodLoadError(
|
|
154
|
+
f"{module_name}.py does not export '{class_name}'.\n"
|
|
155
|
+
f" Available names: {available}"
|
|
156
|
+
)
|
|
157
|
+
|
|
158
|
+
method_class = getattr(module, class_name)
|
|
159
|
+
|
|
160
|
+
# Instantiate — try with manifest/method_dir, fall back to no-arg
|
|
161
|
+
try:
|
|
162
|
+
instance = method_class(manifest=manifest, method_dir=method_dir)
|
|
163
|
+
except TypeError:
|
|
164
|
+
try:
|
|
165
|
+
instance = method_class()
|
|
166
|
+
except Exception as e:
|
|
167
|
+
raise MethodLoadError(
|
|
168
|
+
f"Could not instantiate {class_name}: {e}"
|
|
169
|
+
) from e
|
|
170
|
+
|
|
171
|
+
if not hasattr(instance, "translate") or not callable(instance.translate):
|
|
172
|
+
raise MethodLoadError(
|
|
173
|
+
f"{class_name} must have an async translate(entries, config) method.\n"
|
|
174
|
+
f" See: https://champollion.dev/docs/guides/serving-a-method"
|
|
175
|
+
)
|
|
176
|
+
|
|
177
|
+
return instance, manifest
|
|
178
|
+
|
|
179
|
+
|
|
180
|
+
# ── Entry-point discovery ─────────────────────────────────────────────
|
|
181
|
+
|
|
182
|
+
|
|
183
|
+
def _discover_installed_methods() -> list[dict]:
|
|
184
|
+
"""Find all pip-installed method modules via entry points.
|
|
185
|
+
|
|
186
|
+
Method modules register in their pyproject.toml:
|
|
187
|
+
|
|
188
|
+
[project.entry-points."champollion.methods"]
|
|
189
|
+
crk-decomp-recomp = "method_plugin.pipeline:CrkPipelineMethod"
|
|
190
|
+
|
|
191
|
+
Returns:
|
|
192
|
+
List of dicts with name, method_id, entry_point, supported_pairs,
|
|
193
|
+
and plugin_path for each discovered module.
|
|
194
|
+
"""
|
|
195
|
+
methods = []
|
|
196
|
+
|
|
197
|
+
try:
|
|
198
|
+
eps = importlib.metadata.entry_points(group="champollion.methods")
|
|
199
|
+
except Exception:
|
|
200
|
+
# Python < 3.12 or no entry points registered
|
|
201
|
+
return methods
|
|
202
|
+
|
|
203
|
+
for ep in eps:
|
|
204
|
+
try:
|
|
205
|
+
# Resolve the entry point's module to find its file path
|
|
206
|
+
module_ref = ep.value.split(":")[0]
|
|
207
|
+
module_spec = importlib.util.find_spec(module_ref)
|
|
208
|
+
|
|
209
|
+
if module_spec and module_spec.origin:
|
|
210
|
+
method_dir = Path(module_spec.origin).parent
|
|
211
|
+
manifest_path = method_dir / "method.json"
|
|
212
|
+
|
|
213
|
+
if manifest_path.exists():
|
|
214
|
+
manifest = json.loads(
|
|
215
|
+
manifest_path.read_text(encoding="utf-8")
|
|
216
|
+
)
|
|
217
|
+
methods.append({
|
|
218
|
+
"name": manifest.get("name", ep.name),
|
|
219
|
+
"method_id": manifest.get("method_id", ep.name),
|
|
220
|
+
"supported_pairs": manifest.get("supported_pairs", []),
|
|
221
|
+
"entry_point": ep.value,
|
|
222
|
+
"plugin_path": str(method_dir),
|
|
223
|
+
})
|
|
224
|
+
else:
|
|
225
|
+
logger.debug(
|
|
226
|
+
"Entry point %s has no method.json at %s",
|
|
227
|
+
ep.name, method_dir,
|
|
228
|
+
)
|
|
229
|
+
except Exception as e:
|
|
230
|
+
logger.debug("Skipping entry point %s: %s", ep.name, e)
|
|
231
|
+
|
|
232
|
+
return methods
|
|
233
|
+
|
|
234
|
+
|
|
235
|
+
# ── Protocol ──────────────────────────────────────────────────────────
|
|
236
|
+
|
|
237
|
+
|
|
238
|
+
def _send(obj: dict) -> None:
|
|
239
|
+
"""Write a JSON response line to stdout and flush immediately."""
|
|
240
|
+
sys.stdout.write(json.dumps(obj, ensure_ascii=False) + "\n")
|
|
241
|
+
sys.stdout.flush()
|
|
242
|
+
|
|
243
|
+
|
|
244
|
+
async def _handle_translate(method, request: dict) -> dict:
|
|
245
|
+
"""Convert CLI key-value request → Arena entries → translate → CLI map.
|
|
246
|
+
|
|
247
|
+
The CLI sends:
|
|
248
|
+
{ keys: { "greeting": "Hello", "farewell": "Goodbye" },
|
|
249
|
+
source_locale: "en", target_locale: "crk",
|
|
250
|
+
config: { model: "anthropic/claude-sonnet-4.6" } }
|
|
251
|
+
|
|
252
|
+
The bridge converts to Arena format:
|
|
253
|
+
[{ id: 0, source: "Hello" }, { id: 1, source: "Goodbye" }]
|
|
254
|
+
|
|
255
|
+
Calls method.translate(entries, config), then converts results back:
|
|
256
|
+
{ translations: { "greeting": "tânisi", "farewell": "êkwa" } }
|
|
257
|
+
"""
|
|
258
|
+
keys_payload = request.get("keys", {})
|
|
259
|
+
config_overrides = request.get("config", {})
|
|
260
|
+
|
|
261
|
+
# Build Arena-shaped entries from CLI key-value pairs
|
|
262
|
+
entries = []
|
|
263
|
+
key_order = [] # preserves insertion order for response mapping
|
|
264
|
+
for idx, (key, source_text) in enumerate(keys_payload.items()):
|
|
265
|
+
entries.append({"id": idx, "source": source_text})
|
|
266
|
+
key_order.append(key)
|
|
267
|
+
|
|
268
|
+
if not entries:
|
|
269
|
+
return {"ok": True, "translations": {}, "meta": {}}
|
|
270
|
+
|
|
271
|
+
# Build a minimal config namespace matching what method plugins read.
|
|
272
|
+
# The bridge passes through whatever config the CLI sends — the method
|
|
273
|
+
# plugin decides what it uses. At minimum, model and source/target lang.
|
|
274
|
+
class BridgeConfig:
|
|
275
|
+
"""Minimal config namespace for method plugin compatibility."""
|
|
276
|
+
pass
|
|
277
|
+
|
|
278
|
+
cfg = BridgeConfig()
|
|
279
|
+
cfg.model = config_overrides.get("model")
|
|
280
|
+
cfg.model_id = config_overrides.get("model") # CrkPipelineMethod reads this
|
|
281
|
+
cfg.source_lang = request.get("source_locale", "en")
|
|
282
|
+
cfg.target_lang = request.get("target_locale", "")
|
|
283
|
+
cfg.temperature = config_overrides.get("temperature", 0.3)
|
|
284
|
+
|
|
285
|
+
# Call the method's translate — same interface the Arena uses
|
|
286
|
+
results = await method.translate(entries, cfg)
|
|
287
|
+
|
|
288
|
+
# Convert Arena results back to CLI key-value map
|
|
289
|
+
translations = {}
|
|
290
|
+
meta = {"total_tokens": 0, "total_latency_s": 0.0, "errors": []}
|
|
291
|
+
|
|
292
|
+
for result in results:
|
|
293
|
+
idx = result.get("id", 0)
|
|
294
|
+
if idx < len(key_order):
|
|
295
|
+
key = key_order[idx]
|
|
296
|
+
if result.get("error"):
|
|
297
|
+
meta["errors"].append({"key": key, "error": result["error"]})
|
|
298
|
+
else:
|
|
299
|
+
predicted = result.get("predicted", "")
|
|
300
|
+
if predicted: # skip empty predictions
|
|
301
|
+
translations[key] = predicted
|
|
302
|
+
|
|
303
|
+
usage = result.get("usage", {})
|
|
304
|
+
meta["total_tokens"] += usage.get("total_tokens", 0)
|
|
305
|
+
meta["total_latency_s"] += result.get("latency_s", 0)
|
|
306
|
+
|
|
307
|
+
return {"ok": True, "translations": translations, "meta": meta}
|
|
308
|
+
|
|
309
|
+
|
|
310
|
+
async def _run_bridge(method_path: str | None = None) -> None:
|
|
311
|
+
"""Main loop: read JSON-lines from stdin, dispatch, respond.
|
|
312
|
+
|
|
313
|
+
If method_path is given, load the method on startup.
|
|
314
|
+
If not, run in discovery-only mode (responds to 'discover' only).
|
|
315
|
+
"""
|
|
316
|
+
method = None
|
|
317
|
+
manifest = None
|
|
318
|
+
|
|
319
|
+
if method_path:
|
|
320
|
+
try:
|
|
321
|
+
method, manifest = _load_method(Path(method_path))
|
|
322
|
+
except MethodLoadError as e:
|
|
323
|
+
_send({"ok": False, "error": str(e)})
|
|
324
|
+
return
|
|
325
|
+
|
|
326
|
+
method_name = getattr(method, "name", manifest.get("name", "Unknown"))
|
|
327
|
+
_send({"ok": True, "ready": True, "name": method_name})
|
|
328
|
+
else:
|
|
329
|
+
# Discovery-only mode — no method loaded
|
|
330
|
+
_send({"ok": True, "ready": True, "name": "champollion-bridge"})
|
|
331
|
+
|
|
332
|
+
for line in sys.stdin:
|
|
333
|
+
line = line.strip()
|
|
334
|
+
if not line:
|
|
335
|
+
continue
|
|
336
|
+
|
|
337
|
+
try:
|
|
338
|
+
request = json.loads(line)
|
|
339
|
+
except json.JSONDecodeError as e:
|
|
340
|
+
_send({"ok": False, "error": f"Invalid JSON: {e}"})
|
|
341
|
+
continue
|
|
342
|
+
|
|
343
|
+
action = request.get("action", "")
|
|
344
|
+
|
|
345
|
+
if action == "discover":
|
|
346
|
+
methods = _discover_installed_methods()
|
|
347
|
+
_send({"ok": True, "methods": methods})
|
|
348
|
+
|
|
349
|
+
elif action == "translate":
|
|
350
|
+
if not method:
|
|
351
|
+
_send({
|
|
352
|
+
"ok": False,
|
|
353
|
+
"error": (
|
|
354
|
+
"No method loaded. Start the bridge with --method.\n"
|
|
355
|
+
" Example: python method_bridge.py "
|
|
356
|
+
"--method ./my-method/method_plugin"
|
|
357
|
+
),
|
|
358
|
+
})
|
|
359
|
+
continue
|
|
360
|
+
try:
|
|
361
|
+
response = await _handle_translate(method, request)
|
|
362
|
+
_send(response)
|
|
363
|
+
except Exception as e:
|
|
364
|
+
logger.exception("Translation error")
|
|
365
|
+
_send({
|
|
366
|
+
"ok": False,
|
|
367
|
+
"error": f"{type(e).__name__}: {e}",
|
|
368
|
+
})
|
|
369
|
+
|
|
370
|
+
elif action == "method_card":
|
|
371
|
+
card = manifest or {}
|
|
372
|
+
if method and hasattr(method, "method_card"):
|
|
373
|
+
try:
|
|
374
|
+
card = method.method_card()
|
|
375
|
+
except Exception:
|
|
376
|
+
pass # fall back to manifest
|
|
377
|
+
_send({"ok": True, "card": card})
|
|
378
|
+
|
|
379
|
+
elif action == "shutdown":
|
|
380
|
+
_send({"ok": True})
|
|
381
|
+
break
|
|
382
|
+
|
|
383
|
+
else:
|
|
384
|
+
_send({
|
|
385
|
+
"ok": False,
|
|
386
|
+
"error": f"Unknown action: '{action}'",
|
|
387
|
+
})
|
|
388
|
+
|
|
389
|
+
|
|
390
|
+
def main():
|
|
391
|
+
"""CLI entry point for the method bridge."""
|
|
392
|
+
import argparse
|
|
393
|
+
|
|
394
|
+
parser = argparse.ArgumentParser(
|
|
395
|
+
description=(
|
|
396
|
+
"Champollion method bridge — connects the CLI to Python "
|
|
397
|
+
"translation method modules via JSON-lines protocol."
|
|
398
|
+
),
|
|
399
|
+
)
|
|
400
|
+
parser.add_argument(
|
|
401
|
+
"--method",
|
|
402
|
+
help=(
|
|
403
|
+
"Path to method plugin directory (must contain method.json). "
|
|
404
|
+
"Example: ./my-method/method_plugin"
|
|
405
|
+
),
|
|
406
|
+
)
|
|
407
|
+
parser.add_argument(
|
|
408
|
+
"--discover",
|
|
409
|
+
action="store_true",
|
|
410
|
+
help="List all pip-installed method modules and exit.",
|
|
411
|
+
)
|
|
412
|
+
args = parser.parse_args()
|
|
413
|
+
|
|
414
|
+
# Logging goes to stderr — stdout is reserved for the JSON protocol
|
|
415
|
+
logging.basicConfig(
|
|
416
|
+
level=logging.INFO,
|
|
417
|
+
format="%(name)s: %(message)s",
|
|
418
|
+
stream=sys.stderr,
|
|
419
|
+
)
|
|
420
|
+
|
|
421
|
+
if args.discover:
|
|
422
|
+
methods = _discover_installed_methods()
|
|
423
|
+
_send({"ok": True, "methods": methods})
|
|
424
|
+
return
|
|
425
|
+
|
|
426
|
+
asyncio.run(_run_bridge(args.method))
|
|
427
|
+
|
|
428
|
+
|
|
429
|
+
if __name__ == "__main__":
|
|
430
|
+
main()
|