openmd-cli 0.1.0__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.
- openmd/__init__.py +3 -0
- openmd/__main__.py +5 -0
- openmd/adapters/__init__.py +1 -0
- openmd/adapters/base.py +95 -0
- openmd/adapters/bilibili/__init__.py +11 -0
- openmd/adapters/bilibili/adapter.py +104 -0
- openmd/adapters/bilibili/discovery.py +33 -0
- openmd/adapters/douyin/__init__.py +11 -0
- openmd/adapters/douyin/adapter.py +117 -0
- openmd/adapters/douyin/discovery.py +36 -0
- openmd/adapters/douyin/sidecar.py +77 -0
- openmd/adapters/generic_media/__init__.py +14 -0
- openmd/adapters/high_privacy.py +77 -0
- openmd/adapters/local_audio/__init__.py +14 -0
- openmd/adapters/local_audio/adapter.py +169 -0
- openmd/adapters/local_audio/asr.py +58 -0
- openmd/adapters/local_audio/backends/__init__.py +1 -0
- openmd/adapters/local_audio/backends/auto.py +177 -0
- openmd/adapters/local_audio/backends/faster_whisper.py +285 -0
- openmd/adapters/local_audio/backends/funasr_sense_voice.py +14 -0
- openmd/adapters/local_audio/backends/qwen3_asr.py +254 -0
- openmd/adapters/opencli.py +770 -0
- openmd/adapters/pdf/__init__.py +11 -0
- openmd/adapters/pdf/adapter.py +153 -0
- openmd/adapters/pdf/backends/__init__.py +1 -0
- openmd/adapters/pdf/backends/markitdown.py +165 -0
- openmd/adapters/pdf/parser.py +56 -0
- openmd/adapters/qq_group/__init__.py +11 -0
- openmd/adapters/qq_group/adapter.py +289 -0
- openmd/adapters/qq_group/parsers/__init__.py +1 -0
- openmd/adapters/qq_group/parsers/html.py +60 -0
- openmd/adapters/qq_group/parsers/json.py +71 -0
- openmd/adapters/registry.py +149 -0
- openmd/adapters/screenshot/__init__.py +11 -0
- openmd/adapters/screenshot/adapter.py +154 -0
- openmd/adapters/screenshot/backends/__init__.py +1 -0
- openmd/adapters/screenshot/backends/paddle.py +15 -0
- openmd/adapters/screenshot/backends/rapidocr.py +176 -0
- openmd/adapters/screenshot/backends/tesseract.py +15 -0
- openmd/adapters/screenshot/ocr.py +84 -0
- openmd/adapters/wechat_group/__init__.py +11 -0
- openmd/adapters/wechat_group/adapter.py +160 -0
- openmd/adapters/wechat_group/wx_cli.py +268 -0
- openmd/adapters/wechat_moments/__init__.py +11 -0
- openmd/adapters/wechat_moments/adapter.py +167 -0
- openmd/adapters/wechat_moments/wx_cli.py +265 -0
- openmd/adapters/weixin/__init__.py +11 -0
- openmd/adapters/weixin/adapter.py +148 -0
- openmd/adapters/weixin/discovery.py +510 -0
- openmd/adapters/weixin/report.py +278 -0
- openmd/adapters/xiaohongshu.py +158 -0
- openmd/backends/__init__.py +1 -0
- openmd/backends/base.py +204 -0
- openmd/backends/bilibili.py +1480 -0
- openmd/backends/bilibili_wbi.py +277 -0
- openmd/backends/discourse.py +426 -0
- openmd/backends/douyin.py +868 -0
- openmd/backends/douyin_detail.py +124 -0
- openmd/backends/douyin_providers.py +602 -0
- openmd/backends/executor.py +546 -0
- openmd/backends/feishu.py +861 -0
- openmd/backends/generic_media.py +77 -0
- openmd/backends/interactive.py +60 -0
- openmd/backends/ocr.py +41 -0
- openmd/backends/opencli.py +82 -0
- openmd/backends/pdf.py +122 -0
- openmd/backends/registry.py +234 -0
- openmd/backends/screenshot.py +79 -0
- openmd/backends/video_metadata.py +167 -0
- openmd/backends/web_article.py +363 -0
- openmd/backends/wechat_group.py +35 -0
- openmd/backends/wechat_moments.py +35 -0
- openmd/backends/weixin.py +767 -0
- openmd/backends/x.py +455 -0
- openmd/backends/xiaoe.py +940 -0
- openmd/backends/xiaohongshu.py +911 -0
- openmd/backends/xiaohongshu_providers.py +876 -0
- openmd/backends/xiaoyuzhou.py +1017 -0
- openmd/backends/youtube.py +280 -0
- openmd/backends/youtube_discovery.py +1245 -0
- openmd/backends/youtube_transcript.py +213 -0
- openmd/browser_extension/background.js +231 -0
- openmd/browser_extension/icons/icon-128.png +0 -0
- openmd/browser_extension/icons/icon-16.png +0 -0
- openmd/browser_extension/icons/icon-32.png +0 -0
- openmd/browser_extension/icons/icon-48.png +0 -0
- openmd/browser_extension/icons/icon-512.png +0 -0
- openmd/browser_extension/idle.html +6 -0
- openmd/browser_extension/manifest.json +43 -0
- openmd/browser_extension/options.html +42 -0
- openmd/browser_extension/options.js +117 -0
- openmd/browser_extension/popup.css +71 -0
- openmd/browser_extension/popup.html +17 -0
- openmd/browser_extension/popup.js +59 -0
- openmd/browser_extension/vendor/opencli-bridge/LICENSE +190 -0
- openmd/browser_extension/vendor/opencli-bridge/VENDOR.md +31 -0
- openmd/browser_extension/vendor/opencli-bridge/background.js +2742 -0
- openmd/browser_host.py +503 -0
- openmd/browser_setup.py +106 -0
- openmd/cli.py +2012 -0
- openmd/console.py +32 -0
- openmd/core/__init__.py +1 -0
- openmd/core/capabilities.py +162 -0
- openmd/core/content_validation.py +323 -0
- openmd/core/coverage.py +163 -0
- openmd/core/detector.py +247 -0
- openmd/core/doctor.py +104 -0
- openmd/core/execution_policy.py +164 -0
- openmd/core/filename.py +64 -0
- openmd/core/input_validation.py +91 -0
- openmd/core/local_config.py +96 -0
- openmd/core/manifest.py +156 -0
- openmd/core/models.py +168 -0
- openmd/core/opencli_output.py +34 -0
- openmd/core/operation_contract.py +60 -0
- openmd/core/output.py +194 -0
- openmd/core/package_contract.py +335 -0
- openmd/core/package_validator.py +489 -0
- openmd/core/package_writer.py +666 -0
- openmd/core/planner.py +345 -0
- openmd/core/preferences.py +61 -0
- openmd/core/readable.py +129 -0
- openmd/core/receipt.py +285 -0
- openmd/core/renderer.py +185 -0
- openmd/core/runlog.py +208 -0
- openmd/core/secrets.py +313 -0
- openmd/doctor_view.py +68 -0
- openmd/interactive.py +453 -0
- openmd/ocr/__init__.py +1 -0
- openmd/ocr/breakers.py +84 -0
- openmd/ocr/config.py +54 -0
- openmd/ocr/documents.py +522 -0
- openmd/ocr/endpoint.py +72 -0
- openmd/ocr/layout.py +92 -0
- openmd/ocr/layout_filter.py +118 -0
- openmd/ocr/layout_worker.py +89 -0
- openmd/ocr/native_math.py +220 -0
- openmd/ocr/office.py +305 -0
- openmd/ocr/ollama.py +200 -0
- openmd/ocr/otsl.py +51 -0
- openmd/ocr/ovis-prompt.txt +1 -0
- openmd/ocr/pipeline.py +384 -0
- openmd/ocr/settings.json +25 -0
- openmd/ocr/tiling.py +631 -0
- openmd/post_video.py +154 -0
- openmd/probe.py +397 -0
- openmd/routing/__init__.py +1 -0
- openmd/routing/cards/builtin.json +620 -0
- openmd/routing/cards/defuddle.json +70 -0
- openmd/routing/cards/x-tweet-fetcher.json +71 -0
- openmd/routing/cards/xyz-dl.json +60 -0
- openmd/routing/cards/yutto.json +68 -0
- openmd/routing/entry_shim.py +71 -0
- openmd/routing/lifecycle.py +459 -0
- openmd/routing/loader.py +138 -0
- openmd/routing/providers.py +158 -0
- openmd/routing/readers.py +108 -0
- openmd/routing/routes.json +504 -0
- openmd/routing/runner.py +192 -0
- openmd/routing/view.py +40 -0
- openmd/runtime/__init__.py +1 -0
- openmd/runtime/_windows_job.py +234 -0
- openmd/runtime/asr.py +217 -0
- openmd/runtime/asr_worker.py +119 -0
- openmd/runtime/gpu_lock.py +81 -0
- openmd/runtime/gpu_quarantine.py +85 -0
- openmd/runtime/interpreters.py +407 -0
- openmd/runtime/managed_tools.py +204 -0
- openmd/runtime/models.py +202 -0
- openmd/runtime/services.py +175 -0
- openmd/runtime/tempdirs.py +227 -0
- openmd_cli-0.1.0.dist-info/METADATA +168 -0
- openmd_cli-0.1.0.dist-info/RECORD +177 -0
- openmd_cli-0.1.0.dist-info/WHEEL +5 -0
- openmd_cli-0.1.0.dist-info/entry_points.txt +2 -0
- openmd_cli-0.1.0.dist-info/licenses/LICENSE +21 -0
- openmd_cli-0.1.0.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,459 @@
|
|
|
1
|
+
"""Explicit, bounded route trials and append-only local route overrides.
|
|
2
|
+
|
|
3
|
+
Usable text without a reference is an availability observation, never a quality
|
|
4
|
+
ranking. This module does not schedule probes or modify bundled route data.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
from collections import defaultdict
|
|
10
|
+
from collections.abc import Callable, Mapping, Sequence
|
|
11
|
+
from dataclasses import replace
|
|
12
|
+
from datetime import datetime, timezone
|
|
13
|
+
import hashlib
|
|
14
|
+
import json
|
|
15
|
+
import math
|
|
16
|
+
import platform
|
|
17
|
+
from pathlib import Path
|
|
18
|
+
import re
|
|
19
|
+
import time
|
|
20
|
+
from typing import Any
|
|
21
|
+
from urllib.parse import urlsplit
|
|
22
|
+
import uuid
|
|
23
|
+
|
|
24
|
+
from openmd.core.content_validation import validate_extraction_content
|
|
25
|
+
from openmd.core.local_config import checkout_root
|
|
26
|
+
from openmd.core.models import TranscriptResult
|
|
27
|
+
from openmd.core.secrets import sanitize_error_summary, sanitize_url_text
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
TRIAL_SCHEMA = "openmd.route-trial/v1"
|
|
31
|
+
OVERLAY_SCHEMA = "openmd.route-overlay/v1"
|
|
32
|
+
SAMPLE_SCHEMA = "openmd.route-samples/v1"
|
|
33
|
+
MAX_FILE_BYTES = 8 * 1024 * 1024
|
|
34
|
+
MAX_CANDIDATES = 8
|
|
35
|
+
MAX_TEXT_CHARS = 2_000_000
|
|
36
|
+
_TOOL_OUTPUT_FAILURES = frozenset({
|
|
37
|
+
"tool_artifact_missing", "tool_media_missing", "tool_json_invalid",
|
|
38
|
+
"tool_subtitle_missing", "tool_content_empty", "tool_html_instead_of_markdown",
|
|
39
|
+
})
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def _now() -> str:
|
|
43
|
+
return datetime.now(timezone.utc).isoformat(timespec="microseconds")
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def _digest(path: Path) -> str:
|
|
47
|
+
hasher = hashlib.sha256()
|
|
48
|
+
with path.open("rb") as stream:
|
|
49
|
+
for chunk in iter(lambda: stream.read(1024 * 1024), b""):
|
|
50
|
+
hasher.update(chunk)
|
|
51
|
+
return hasher.hexdigest()
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
def _root() -> Path:
|
|
55
|
+
root = checkout_root()
|
|
56
|
+
if root is None:
|
|
57
|
+
raise ValueError("benchmark_checkout_required: supply explicit sample, ledger and artifact paths outside a checkout")
|
|
58
|
+
return root
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
def _json_file(path: Path) -> Any:
|
|
62
|
+
if not path.is_file() or path.stat().st_size > MAX_FILE_BYTES:
|
|
63
|
+
raise ValueError("benchmark_file_missing_or_oversized")
|
|
64
|
+
try:
|
|
65
|
+
return json.loads(path.read_text(encoding="utf-8-sig"))
|
|
66
|
+
except (OSError, ValueError) as exc:
|
|
67
|
+
raise ValueError("benchmark_json_invalid") from exc
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
def _read_jsonl(path: Path, schema: str) -> list[dict[str, Any]]:
|
|
71
|
+
if not path.exists():
|
|
72
|
+
return []
|
|
73
|
+
if path.stat().st_size > MAX_FILE_BYTES:
|
|
74
|
+
raise ValueError("benchmark_ledger_oversized")
|
|
75
|
+
rows: list[dict[str, Any]] = []
|
|
76
|
+
for line in path.read_text(encoding="utf-8-sig").splitlines():
|
|
77
|
+
if not line.strip():
|
|
78
|
+
continue
|
|
79
|
+
try:
|
|
80
|
+
row = json.loads(line)
|
|
81
|
+
except ValueError as exc:
|
|
82
|
+
raise ValueError("benchmark_ledger_invalid_json") from exc
|
|
83
|
+
if not isinstance(row, dict) or row.get("schema") != schema:
|
|
84
|
+
raise ValueError("benchmark_ledger_schema_invalid")
|
|
85
|
+
rows.append(row)
|
|
86
|
+
return rows
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
def _append_jsonl(path: Path, row: Mapping[str, Any]) -> None:
|
|
90
|
+
path.parent.mkdir(parents=True, exist_ok=True)
|
|
91
|
+
if path.exists() and path.stat().st_size > MAX_FILE_BYTES:
|
|
92
|
+
raise ValueError("benchmark_ledger_oversized")
|
|
93
|
+
data = json.dumps(dict(row), ensure_ascii=False, separators=(",", ":")) + "\n"
|
|
94
|
+
# A single append preserves every prior trial/event. Malformed prior data
|
|
95
|
+
# is rejected by callers before appending rather than silently truncated.
|
|
96
|
+
with path.open("a", encoding="utf-8", newline="\n") as stream:
|
|
97
|
+
stream.write(data)
|
|
98
|
+
stream.flush()
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
def load_samples(path: str | Path | None = None) -> tuple[dict[str, Any], ...]:
|
|
102
|
+
sample_path = Path(path) if path is not None else _root() / "docs/benchmarks/routes/samples.json"
|
|
103
|
+
value = _json_file(sample_path)
|
|
104
|
+
if not isinstance(value, dict) or value.get("schema") != SAMPLE_SCHEMA or not isinstance(value.get("samples"), list):
|
|
105
|
+
raise ValueError("benchmark_samples_schema_invalid")
|
|
106
|
+
seen: set[str] = set()
|
|
107
|
+
samples: list[dict[str, Any]] = []
|
|
108
|
+
for raw in value["samples"]:
|
|
109
|
+
if not isinstance(raw, dict) or not all(isinstance(raw.get(k), str) and raw[k] for k in ("id", "source", "capability", "url")):
|
|
110
|
+
raise ValueError("benchmark_sample_invalid")
|
|
111
|
+
if raw["id"] in seen:
|
|
112
|
+
raise ValueError("benchmark_sample_duplicate")
|
|
113
|
+
seen.add(raw["id"])
|
|
114
|
+
parsed = urlsplit(raw["url"])
|
|
115
|
+
from openmd.adapters.local_audio.adapter import local_path_from_input
|
|
116
|
+
|
|
117
|
+
local = local_path_from_input(raw["url"]) if raw["source"] in {"screenshot", "pdf"} else None
|
|
118
|
+
local_sample = local is not None and local.is_file()
|
|
119
|
+
if not local_sample and (parsed.scheme not in {"http", "https"} or not parsed.netloc or parsed.username or parsed.password):
|
|
120
|
+
raise ValueError("benchmark_sample_url_invalid")
|
|
121
|
+
if sanitize_url_text(raw["url"]) != raw["url"]:
|
|
122
|
+
raise ValueError("benchmark_sample_private_url")
|
|
123
|
+
sample = dict(raw)
|
|
124
|
+
sample["_base_dir"] = str(sample_path.resolve().parent)
|
|
125
|
+
samples.append(sample)
|
|
126
|
+
return tuple(samples)
|
|
127
|
+
|
|
128
|
+
|
|
129
|
+
def _plain(text: str) -> str:
|
|
130
|
+
text = re.sub(r"<[^>]*>", "", text)
|
|
131
|
+
lines: list[str] = []
|
|
132
|
+
for raw in text.splitlines():
|
|
133
|
+
line = raw.strip()
|
|
134
|
+
if not line or line in {"WEBVTT", ""} or line.startswith(("Kind:", "Language:", "NOTE")):
|
|
135
|
+
continue
|
|
136
|
+
if " --> " in line or line.isdigit():
|
|
137
|
+
continue
|
|
138
|
+
line = re.sub(r"^\[\d{2}:\d{2}(?::\d{2})?(?:\s*-\s*\d{2}:\d{2}(?::\d{2})?)?\]\s*", "", line)
|
|
139
|
+
lines.append(line)
|
|
140
|
+
return "\n".join(lines)
|
|
141
|
+
|
|
142
|
+
|
|
143
|
+
def _quality(text: str, sample: Mapping[str, Any]) -> tuple[float | None, dict[str, Any]]:
|
|
144
|
+
reference = sample.get("reference")
|
|
145
|
+
if not reference:
|
|
146
|
+
return None, {"kind": "unscored", "reason": "reference_missing", "reference_sha256": None}
|
|
147
|
+
path = Path(reference)
|
|
148
|
+
if not path.is_absolute():
|
|
149
|
+
path = Path(str(sample["_base_dir"])) / path
|
|
150
|
+
if not path.is_file() or path.stat().st_size > MAX_FILE_BYTES:
|
|
151
|
+
return None, {"kind": "unscored", "reason": "reference_unavailable", "reference_sha256": None}
|
|
152
|
+
sha = _digest(path)
|
|
153
|
+
if sample.get("reference_sha256") and sample["reference_sha256"] != sha:
|
|
154
|
+
raise ValueError("benchmark_reference_hash_mismatch")
|
|
155
|
+
ref = _plain(path.read_text(encoding="utf-8-sig"))
|
|
156
|
+
hyp = _plain(text)
|
|
157
|
+
metric = sample.get("metric", "paragraph_recall")
|
|
158
|
+
evidence: dict[str, Any] = {"kind": metric, "reference_sha256": sha}
|
|
159
|
+
if metric in {"cer", "wer"}:
|
|
160
|
+
try:
|
|
161
|
+
from rapidfuzz.distance import Levenshtein
|
|
162
|
+
except ImportError:
|
|
163
|
+
return None, {**evidence, "reason": "metric_dependency_missing"}
|
|
164
|
+
ref = re.sub(r"[^\w\s]", "", ref.lower())
|
|
165
|
+
hyp = re.sub(r"[^\w\s]", "", hyp.lower())
|
|
166
|
+
if metric == "cer":
|
|
167
|
+
if sample.get("language") == "zh":
|
|
168
|
+
try:
|
|
169
|
+
from opencc import OpenCC
|
|
170
|
+
except ImportError:
|
|
171
|
+
return None, {**evidence, "reason": "metric_dependency_missing_opencc"}
|
|
172
|
+
ref, hyp = OpenCC("t2s").convert(ref), OpenCC("t2s").convert(hyp)
|
|
173
|
+
a, b = "".join(ref.split()), "".join(hyp.split())
|
|
174
|
+
else:
|
|
175
|
+
a, b = ref.split(), hyp.split()
|
|
176
|
+
if not a:
|
|
177
|
+
return None, {**evidence, "reason": "reference_empty"}
|
|
178
|
+
errors = Levenshtein.distance(a, b)
|
|
179
|
+
rate = errors / len(a)
|
|
180
|
+
return max(0.0, 1.0 - rate), {**evidence, "error_rate": round(rate, 6), "reference_units": len(a), "errors": errors}
|
|
181
|
+
if metric != "paragraph_recall":
|
|
182
|
+
raise ValueError("benchmark_metric_unsupported")
|
|
183
|
+
normalized = lambda s: " ".join(s.lower().split())
|
|
184
|
+
paragraphs = [normalized(p) for p in re.split(r"\n\s*\n|\n", ref) if normalized(p)]
|
|
185
|
+
haystack = normalized(hyp)
|
|
186
|
+
if not paragraphs:
|
|
187
|
+
return None, {**evidence, "reason": "reference_empty"}
|
|
188
|
+
recalled = sum(len(p) for p in paragraphs if p in haystack)
|
|
189
|
+
total = sum(map(len, paragraphs))
|
|
190
|
+
recall = recalled / total
|
|
191
|
+
# Exact reference-paragraph coverage penalizes excess text without an
|
|
192
|
+
# unbounded quadratic diff over long articles. This is a content metric,
|
|
193
|
+
# not CER/WER and not a substitute for independently reviewed references.
|
|
194
|
+
precision = min(1.0, recalled / max(1, len(haystack)))
|
|
195
|
+
return recall * precision, {**evidence, "paragraph_recall": round(recall, 6), "text_precision": round(precision, 6)}
|
|
196
|
+
|
|
197
|
+
|
|
198
|
+
def _sample_fingerprint(sample: Mapping[str, Any]) -> str:
|
|
199
|
+
identity = {key: value for key, value in sample.items() if not key.startswith("_")}
|
|
200
|
+
if sample.get("source") in {"pdf", "screenshot"}:
|
|
201
|
+
from openmd.adapters.local_audio.adapter import local_path_from_input
|
|
202
|
+
|
|
203
|
+
local = local_path_from_input(str(sample.get("url", "")))
|
|
204
|
+
if local is not None and local.is_file():
|
|
205
|
+
identity["actual_input_sha256"] = _digest(local)
|
|
206
|
+
if sample.get("reference"):
|
|
207
|
+
path = Path(str(sample["reference"]))
|
|
208
|
+
if not path.is_absolute():
|
|
209
|
+
path = Path(str(sample["_base_dir"])) / path
|
|
210
|
+
identity["actual_reference_sha256"] = _digest(path) if path.is_file() and path.stat().st_size <= MAX_FILE_BYTES else None
|
|
211
|
+
return hashlib.sha256(json.dumps(identity, ensure_ascii=False, sort_keys=True).encode("utf-8")).hexdigest()
|
|
212
|
+
|
|
213
|
+
|
|
214
|
+
def score_trials(rows: Sequence[Mapping[str, Any]]) -> list[dict[str, Any]]:
|
|
215
|
+
"""Score measured quality at 80%, log throughput at 20%, within one sample."""
|
|
216
|
+
result = [dict(row) for row in rows]
|
|
217
|
+
groups: dict[tuple[Any, ...], list[dict[str, Any]]] = defaultdict(list)
|
|
218
|
+
for row in result:
|
|
219
|
+
valid = row.get("validation", {}).get("ok") is True
|
|
220
|
+
quality = row.get("quality")
|
|
221
|
+
row["score"] = 0.0 if not valid else None
|
|
222
|
+
row["speed"] = None
|
|
223
|
+
row["rankable"] = False
|
|
224
|
+
if valid and isinstance(quality, (int, float)) and not isinstance(quality, bool):
|
|
225
|
+
if not math.isfinite(quality) or not 0 <= quality <= 1:
|
|
226
|
+
raise ValueError("benchmark_quality_invalid")
|
|
227
|
+
seconds = row.get("seconds")
|
|
228
|
+
if not isinstance(seconds, (int, float)) or not math.isfinite(seconds) or seconds <= 0:
|
|
229
|
+
raise ValueError("benchmark_duration_invalid")
|
|
230
|
+
reference_sha = row.get("quality_evidence", {}).get("reference_sha256")
|
|
231
|
+
if reference_sha:
|
|
232
|
+
groups[(row.get("source"), row.get("capability"), row.get("sample_id"), reference_sha, row.get("quality_evidence", {}).get("kind"))].append(row)
|
|
233
|
+
for group in groups.values():
|
|
234
|
+
speeds = [math.log(1 / row["seconds"]) for row in group]
|
|
235
|
+
low, high = min(speeds), max(speeds)
|
|
236
|
+
for row, speed in zip(group, speeds):
|
|
237
|
+
normalized = (speed - low) / (high - low) if high > low else 1.0
|
|
238
|
+
row["speed"] = round(normalized, 6)
|
|
239
|
+
row["score"] = round(0.8 * row["quality"] + 0.2 * normalized, 6)
|
|
240
|
+
row["rankable"] = True
|
|
241
|
+
return result
|
|
242
|
+
|
|
243
|
+
|
|
244
|
+
def _execute_backend(candidate: str, sample: dict[str, Any], options: Any) -> TranscriptResult:
|
|
245
|
+
from openmd.adapters.registry import create_default_registry
|
|
246
|
+
from openmd.backends.executor import BackendExecutor
|
|
247
|
+
from openmd.backends.registry import create_runtime_backend_provider_registry
|
|
248
|
+
|
|
249
|
+
adapter = create_default_registry().get_adapter(sample["source"])
|
|
250
|
+
items = adapter.resolve_items(sample["url"])
|
|
251
|
+
if len(items) != 1:
|
|
252
|
+
raise ValueError("benchmark_single_item_required")
|
|
253
|
+
selected = replace(options, backend_override=candidate)
|
|
254
|
+
registry = create_runtime_backend_provider_registry(sample["source"], selected)
|
|
255
|
+
executor = BackendExecutor(provider_registry=registry)
|
|
256
|
+
identity = executor.resolve_item_identity(items[0], (candidate,))
|
|
257
|
+
if not identity.ok:
|
|
258
|
+
return TranscriptResult(text="", status="failed", source_level="partial_extraction", backend=candidate, error_code=identity.error_code, error_summary=identity.error_summary)
|
|
259
|
+
return executor.extract(identity.item, (candidate,), allow_fallback=False)
|
|
260
|
+
|
|
261
|
+
|
|
262
|
+
def _trial_artifacts(folder: Path, result: TranscriptResult, summary: dict[str, Any]) -> list[dict[str, Any]]:
|
|
263
|
+
folder.mkdir(parents=True, exist_ok=False)
|
|
264
|
+
if result.has_usable_text:
|
|
265
|
+
(folder / "content.md").write_text(result.text, encoding="utf-8", newline="\n")
|
|
266
|
+
(folder / "result.json").write_text(json.dumps(summary, ensure_ascii=False, sort_keys=True), encoding="utf-8", newline="\n")
|
|
267
|
+
return [{"path": str(path.resolve()), "sha256": _digest(path), "bytes": path.stat().st_size} for path in sorted(folder.iterdir())]
|
|
268
|
+
|
|
269
|
+
|
|
270
|
+
def run_benchmark(
|
|
271
|
+
*, source: str, capability: str, candidates: Sequence[str] | None,
|
|
272
|
+
options: Any, ledger_path: str | Path | None = None,
|
|
273
|
+
samples_path: str | Path | None = None, artifact_dir: str | Path | None = None,
|
|
274
|
+
sample_id: str | None = None,
|
|
275
|
+
executor: Callable[[str, dict[str, Any], Any], TranscriptResult] | None = None,
|
|
276
|
+
) -> dict[str, Any]:
|
|
277
|
+
"""Run one fixed sample once per candidate using explicit runtime gates."""
|
|
278
|
+
from openmd.routing.loader import load_catalog
|
|
279
|
+
|
|
280
|
+
catalog = load_catalog()
|
|
281
|
+
routes = [r for r in catalog.routes if r["source"] == source and r["capability"] == capability]
|
|
282
|
+
if len(routes) != 1:
|
|
283
|
+
raise ValueError("benchmark_route_not_found")
|
|
284
|
+
chain = routes[0]["chain"]
|
|
285
|
+
selected = list(dict.fromkeys(candidates if candidates is not None else chain))
|
|
286
|
+
if not selected or len(selected) > MAX_CANDIDATES or any(c not in chain and (c not in catalog.capabilities or source not in catalog.capabilities[c].get("sources", []) or catalog.capabilities[c].get("capability") != capability) for c in selected):
|
|
287
|
+
raise ValueError("benchmark_candidates_invalid")
|
|
288
|
+
matching = [s for s in load_samples(samples_path) if s["source"] == source and s["capability"] == capability and (sample_id is None or s["id"] == sample_id)]
|
|
289
|
+
if not matching:
|
|
290
|
+
raise ValueError("benchmark_sample_not_found")
|
|
291
|
+
sample = matching[0]
|
|
292
|
+
sample_sha256 = _sample_fingerprint(sample)
|
|
293
|
+
gate_options = {
|
|
294
|
+
"network": "enable_network_backends", "browser": "enable_browser_backends",
|
|
295
|
+
"media_download": "enable_media_download_backends", "asr": "enable_asr_backends",
|
|
296
|
+
"cloud": "enable_cloud_backends",
|
|
297
|
+
}
|
|
298
|
+
# Validate every candidate before starting any work or writing artifacts.
|
|
299
|
+
for candidate in selected:
|
|
300
|
+
cap = catalog.capabilities[candidate]
|
|
301
|
+
for gate in cap.get("gates", []):
|
|
302
|
+
if gate == "local_compute":
|
|
303
|
+
continue
|
|
304
|
+
if gate == "account" or gate not in gate_options or not getattr(options, gate_options[gate], False):
|
|
305
|
+
raise ValueError(f"benchmark_gate_required: {candidate}: {gate}")
|
|
306
|
+
if cap.get("requires_login"):
|
|
307
|
+
raise ValueError("benchmark_account_candidate_blocked")
|
|
308
|
+
ledger = Path(ledger_path) if ledger_path is not None else _root() / f"docs/benchmarks/routes/trials-{datetime.now(timezone.utc):%Y-%m}.jsonl"
|
|
309
|
+
_read_jsonl(ledger, TRIAL_SCHEMA)
|
|
310
|
+
artifacts = Path(artifact_dir) if artifact_dir is not None else _root() / "out/route-benchmarks"
|
|
311
|
+
rows: list[dict[str, Any]] = []
|
|
312
|
+
for candidate in selected:
|
|
313
|
+
cap = catalog.capabilities[candidate]
|
|
314
|
+
card = catalog.cards[cap["card_id"]]
|
|
315
|
+
started = time.perf_counter()
|
|
316
|
+
try:
|
|
317
|
+
result = (executor or _execute_backend)(candidate, sample, options)
|
|
318
|
+
if not isinstance(result, TranscriptResult):
|
|
319
|
+
raise ValueError("benchmark_provider_invalid_result")
|
|
320
|
+
result = validate_extraction_content(result)
|
|
321
|
+
except Exception as exc:
|
|
322
|
+
result = TranscriptResult(text="", status="failed", source_level="partial_extraction", backend=candidate, error_code="benchmark_provider_failed", error_summary=sanitize_error_summary(exc))
|
|
323
|
+
elapsed = max(time.perf_counter() - started, 0.000001)
|
|
324
|
+
if len(result.text) > MAX_TEXT_CHARS:
|
|
325
|
+
result = replace(result, text="", status="failed", source_level="partial_extraction", error_code="benchmark_output_oversized")
|
|
326
|
+
# The measured hypothesis must be exactly the retained, sanitized text
|
|
327
|
+
# so a reviewer can reproduce quality from the evidence artifact.
|
|
328
|
+
result = replace(result, text=sanitize_url_text(result.text))
|
|
329
|
+
tool_run = result.metadata.get("tool_run", {})
|
|
330
|
+
tool_version = tool_run.get("tool_version") or card.get("version")
|
|
331
|
+
tool_version_verified = tool_run.get("tool_version_verified") is True
|
|
332
|
+
trial_id = uuid.uuid4().hex
|
|
333
|
+
validation = {"ok": result.status == "verified" and bool(result.text.strip()), "code": result.error_code or (None if result.status == "verified" and result.text.strip() else "benchmark_content_not_verified")}
|
|
334
|
+
quality, quality_evidence = _quality(result.text, sample) if validation["ok"] else (0.0, {"kind": "failed", "reference_sha256": None})
|
|
335
|
+
summary = {"schema": TRIAL_SCHEMA, "trial_id": trial_id, "ts": _now(), "source": source, "capability": capability, "candidate": candidate, "tool": cap["card_id"], "tool_version": tool_version, "tool_version_verified": tool_version_verified, "sample_id": sample["id"], "sample_sha256": sample_sha256, "validation": validation, "status": result.status, "source_level": result.source_level, "backend": result.backend, "chars": len(result.text)}
|
|
336
|
+
if result.metadata.get("pages"):
|
|
337
|
+
from openmd.core.secrets import sanitize_document_metadata
|
|
338
|
+
|
|
339
|
+
summary["document_metadata"] = sanitize_document_metadata({"pages": result.metadata["pages"]})
|
|
340
|
+
evidence_files = _trial_artifacts(artifacts / trial_id, result, summary)
|
|
341
|
+
row = {**summary, "card_id": cap["card_id"], "route_version": catalog.version, "sample_url": sanitize_url_text(sample["url"]), "argv_redacted": cap.get("argv", []), "exit_code": tool_run.get("exit_code"), "seconds": round(elapsed, 6), "files": tool_run.get("files", {}), "artifacts": evidence_files, "quality": quality, "quality_evidence": quality_evidence, "host_fingerprint": hashlib.sha256(f"{platform.system()}:{platform.machine()}:{platform.python_version()}".encode()).hexdigest()[:16]}
|
|
342
|
+
rows.append(row)
|
|
343
|
+
scored = score_trials(rows)
|
|
344
|
+
for row in scored:
|
|
345
|
+
_append_jsonl(ledger, row)
|
|
346
|
+
ranked = sorted((r for r in scored if r["rankable"]), key=lambda r: r["score"], reverse=True)
|
|
347
|
+
return {"schema": "openmd.route-benchmark-report/v1", "source": source, "capability": capability, "sample_id": sample["id"], "ledger": str(ledger.resolve()), "rows": scored, "ranking": [{"candidate": r["candidate"], "score": r["score"]} for r in ranked], "quality_note": "No reference means unscored availability evidence; scores compare only the same sample/reference."}
|
|
348
|
+
|
|
349
|
+
|
|
350
|
+
def _overlay_path(path: str | Path | None) -> Path:
|
|
351
|
+
if path is not None:
|
|
352
|
+
return Path(path)
|
|
353
|
+
from openmd.routing.loader import overlay_file
|
|
354
|
+
return Path(overlay_file())
|
|
355
|
+
|
|
356
|
+
|
|
357
|
+
def apply_overlay(routes: Sequence[Mapping[str, Any]], overlay_path: str | Path | None) -> list[dict[str, Any]]:
|
|
358
|
+
"""Replay local events and return fresh effective route dictionaries."""
|
|
359
|
+
output = [{**route, "chain": list(route["chain"]), "overlay_applied": False} for route in routes]
|
|
360
|
+
if overlay_path is None:
|
|
361
|
+
return output
|
|
362
|
+
active: dict[tuple[str, str], bool] = {}
|
|
363
|
+
for event in _read_jsonl(Path(overlay_path), OVERLAY_SCHEMA):
|
|
364
|
+
if event.get("op") not in {"demote", "restore"} or not all(isinstance(event.get(k), str) and event[k] for k in ("source", "capability_id", "ts")):
|
|
365
|
+
raise ValueError("route_overlay_event_invalid")
|
|
366
|
+
if not any(r["source"] == event["source"] and event["capability_id"] in r["chain"] for r in output):
|
|
367
|
+
raise ValueError("route_overlay_candidate_unknown")
|
|
368
|
+
active[(event["source"], event["capability_id"])] = event["op"] == "demote"
|
|
369
|
+
for route in output:
|
|
370
|
+
chain = route["chain"]
|
|
371
|
+
demoted = [c for c in chain if active.get((route["source"], c), False)]
|
|
372
|
+
route["chain"] = [c for c in chain if c not in demoted] + demoted
|
|
373
|
+
route["overlay_applied"] = bool(demoted)
|
|
374
|
+
return output
|
|
375
|
+
|
|
376
|
+
|
|
377
|
+
def _known_candidate(capability_id: str, source: str) -> None:
|
|
378
|
+
from openmd.routing.loader import load_catalog
|
|
379
|
+
if not any(r["source"] == source and capability_id in r["chain"] for r in load_catalog().routes):
|
|
380
|
+
raise ValueError("route_overlay_candidate_unknown")
|
|
381
|
+
|
|
382
|
+
|
|
383
|
+
def _verify_trial_artifacts(row: Mapping[str, Any]) -> None:
|
|
384
|
+
artifacts = row.get("artifacts")
|
|
385
|
+
if not isinstance(artifacts, list) or not artifacts or len(artifacts) > 16:
|
|
386
|
+
raise ValueError("route_evidence_artifacts_missing")
|
|
387
|
+
summaries: list[dict[str, Any]] = []
|
|
388
|
+
for artifact in artifacts:
|
|
389
|
+
if not isinstance(artifact, dict) or not isinstance(artifact.get("path"), str):
|
|
390
|
+
raise ValueError("route_evidence_artifact_invalid")
|
|
391
|
+
path = Path(artifact["path"])
|
|
392
|
+
if not path.is_file() or path.is_symlink() or path.stat().st_size != artifact.get("bytes") or _digest(path) != artifact.get("sha256"):
|
|
393
|
+
raise ValueError("route_evidence_artifact_changed")
|
|
394
|
+
if path.name == "result.json":
|
|
395
|
+
summaries.append(_json_file(path))
|
|
396
|
+
keys = ("schema", "trial_id", "ts", "source", "capability", "candidate", "tool", "tool_version", "tool_version_verified", "sample_id", "sample_sha256", "validation", "status", "source_level", "backend", "chars")
|
|
397
|
+
if len(summaries) != 1 or any(summaries[0].get(k) != row.get(k) for k in keys):
|
|
398
|
+
raise ValueError("route_evidence_summary_mismatch")
|
|
399
|
+
|
|
400
|
+
|
|
401
|
+
def demote(
|
|
402
|
+
capability_id: str, *, source: str, evidence_ids: Sequence[str], ledger_path: str | Path,
|
|
403
|
+
overlay_path: str | Path | None = None, note: str = "",
|
|
404
|
+
) -> dict[str, Any]:
|
|
405
|
+
"""Demote only after two reproducible, artifact-backed tool failures."""
|
|
406
|
+
_known_candidate(capability_id, source)
|
|
407
|
+
ids = list(dict.fromkeys(evidence_ids))
|
|
408
|
+
if len(ids) < 2:
|
|
409
|
+
raise ValueError("route_evidence_two_trials_required")
|
|
410
|
+
rows = _read_jsonl(Path(ledger_path), TRIAL_SCHEMA)
|
|
411
|
+
selected: list[dict[str, Any]] = []
|
|
412
|
+
observed_times: set[datetime] = set()
|
|
413
|
+
for trial_id in ids:
|
|
414
|
+
matches = [r for r in rows if r.get("trial_id") == trial_id]
|
|
415
|
+
if len(matches) != 1:
|
|
416
|
+
raise ValueError("route_evidence_trial_not_unique")
|
|
417
|
+
row = matches[0]
|
|
418
|
+
if row.get("source") != source or row.get("candidate") != capability_id or row.get("status") != "failed" or row.get("validation", {}).get("ok") is not False:
|
|
419
|
+
raise ValueError("route_evidence_failed_candidate_required")
|
|
420
|
+
code = row.get("validation", {}).get("code")
|
|
421
|
+
# Exit codes/timeouts can reflect networking or machine health; ASR and
|
|
422
|
+
# harness failures belong to different components. Only an explicit
|
|
423
|
+
# tool-output contract failure supports this automatic attribution.
|
|
424
|
+
if code not in _TOOL_OUTPUT_FAILURES:
|
|
425
|
+
raise ValueError("route_evidence_not_tool_failure")
|
|
426
|
+
version = row.get("tool_version")
|
|
427
|
+
if row.get("tool_version_verified") is not True or not isinstance(version, str) or version.strip().lower() in {"", "unknown", "unlocked", "unverified"}:
|
|
428
|
+
raise ValueError("route_evidence_version_required")
|
|
429
|
+
try:
|
|
430
|
+
parsed_time = datetime.fromisoformat(row["ts"].replace("Z", "+00:00"))
|
|
431
|
+
except (KeyError, ValueError, TypeError) as exc:
|
|
432
|
+
raise ValueError("route_evidence_timestamp_invalid") from exc
|
|
433
|
+
if parsed_time.tzinfo is None:
|
|
434
|
+
raise ValueError("route_evidence_timestamp_invalid")
|
|
435
|
+
if parsed_time in observed_times:
|
|
436
|
+
raise ValueError("route_evidence_not_comparable")
|
|
437
|
+
observed_times.add(parsed_time)
|
|
438
|
+
_verify_trial_artifacts(row)
|
|
439
|
+
selected.append(row)
|
|
440
|
+
group_keys = ("sample_id", "sample_sha256", "sample_url", "tool_version", "tool", "capability", "route_version")
|
|
441
|
+
if any(len({r.get(k) for r in selected}) != 1 for k in group_keys) or len({r["ts"] for r in selected}) != len(selected):
|
|
442
|
+
raise ValueError("route_evidence_not_comparable")
|
|
443
|
+
path = _overlay_path(overlay_path)
|
|
444
|
+
_read_jsonl(path, OVERLAY_SCHEMA)
|
|
445
|
+
event = {"schema": OVERLAY_SCHEMA, "op": "demote", "source": source, "capability_id": capability_id, "evidence": ids, "ts": _now(), "note": sanitize_error_summary(note) or "Two comparable tool failures with verified artifacts."}
|
|
446
|
+
_append_jsonl(path, event)
|
|
447
|
+
return event
|
|
448
|
+
|
|
449
|
+
|
|
450
|
+
def restore(capability_id: str, *, source: str, overlay_path: str | Path | None = None, note: str = "") -> dict[str, Any]:
|
|
451
|
+
"""Restore the bundled position by appending an explicit local event."""
|
|
452
|
+
_known_candidate(capability_id, source)
|
|
453
|
+
path = _overlay_path(overlay_path)
|
|
454
|
+
prior = _read_jsonl(path, OVERLAY_SCHEMA)
|
|
455
|
+
if not any(r.get("op") == "demote" and r.get("source") == source and r.get("capability_id") == capability_id for r in prior):
|
|
456
|
+
raise ValueError("route_overlay_no_demotion_to_restore")
|
|
457
|
+
event = {"schema": OVERLAY_SCHEMA, "op": "restore", "source": source, "capability_id": capability_id, "evidence": [], "ts": _now(), "note": sanitize_error_summary(note) or "Explicit restore of bundled route position."}
|
|
458
|
+
_append_jsonl(path, event)
|
|
459
|
+
return event
|
openmd/routing/loader.py
ADDED
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
"""Validated bundled cards and their read-only runtime projections."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import copy
|
|
6
|
+
import json
|
|
7
|
+
from dataclasses import dataclass
|
|
8
|
+
from functools import lru_cache
|
|
9
|
+
from importlib.resources import files
|
|
10
|
+
from pathlib import Path
|
|
11
|
+
from typing import Any
|
|
12
|
+
|
|
13
|
+
from openmd.core.local_config import load_local_config, local_config_path
|
|
14
|
+
from openmd.core.models import SOURCE_LEVEL_VALUES
|
|
15
|
+
|
|
16
|
+
GATES = frozenset({"network", "browser", "media_download", "asr", "cloud", "account", "local_compute", "local_export"})
|
|
17
|
+
READERS = frozenset({"markdown_file", "json_stdout", "json_file", "subtitle_dir", "media_dir"})
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
@dataclass(frozen=True)
|
|
21
|
+
class Catalog:
|
|
22
|
+
cards: dict[str, dict[str, Any]]
|
|
23
|
+
capabilities: dict[str, dict[str, Any]]
|
|
24
|
+
routes: tuple[dict[str, Any], ...]
|
|
25
|
+
version: str
|
|
26
|
+
capability_metadata: tuple[dict[str, Any], ...]
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def validate_catalog(cards: list[dict[str, Any]], route_data: dict[str, Any]) -> Catalog:
|
|
30
|
+
if route_data.get("schema") != "openmd.routes/v1" or not route_data.get("version"):
|
|
31
|
+
raise ValueError("routes_schema_invalid")
|
|
32
|
+
tools: dict[str, dict[str, Any]] = {}
|
|
33
|
+
capabilities: dict[str, dict[str, Any]] = {}
|
|
34
|
+
for card in cards:
|
|
35
|
+
tool_id = card.get("id")
|
|
36
|
+
if card.get("schema") != "openmd.tool-card/v1" or not isinstance(tool_id, str) or not tool_id or tool_id in tools:
|
|
37
|
+
raise ValueError("tool_card_schema_invalid_or_duplicate")
|
|
38
|
+
if card.get("kind") not in {"command", "python_entry", "python_script", "builtin"}:
|
|
39
|
+
raise ValueError(f"tool_card_kind_invalid: {tool_id}")
|
|
40
|
+
if not isinstance(card.get("capabilities"), list) or not card["capabilities"]:
|
|
41
|
+
raise ValueError(f"tool_card_capabilities_missing: {tool_id}")
|
|
42
|
+
tools[tool_id] = card
|
|
43
|
+
for cap in card["capabilities"]:
|
|
44
|
+
cap_id = cap.get("id")
|
|
45
|
+
if not isinstance(cap_id, str) or not cap_id or cap_id in capabilities or not cap.get("sources"):
|
|
46
|
+
raise ValueError("tool_capability_invalid_or_duplicate")
|
|
47
|
+
if not set(cap.get("gates", [])) <= GATES:
|
|
48
|
+
raise ValueError(f"tool_gate_invalid: {cap_id}")
|
|
49
|
+
if card["kind"] == "builtin":
|
|
50
|
+
if cap.get("provider") != cap_id or "argv" in cap:
|
|
51
|
+
raise ValueError(f"builtin_card_invalid: {cap_id}")
|
|
52
|
+
else:
|
|
53
|
+
if set(cap.get("gates", [])) & {"local_compute", "local_export"}:
|
|
54
|
+
raise ValueError(f"tool_gate_invalid: {cap_id}")
|
|
55
|
+
if not card.get("version") or not card.get("license") or not card.get("installation") or not card.get("evidence"):
|
|
56
|
+
raise ValueError(f"tool_evidence_or_identity_missing: {tool_id}")
|
|
57
|
+
if not isinstance(cap.get("argv"), list) or not all(isinstance(x, str) for x in cap["argv"]):
|
|
58
|
+
raise ValueError(f"tool_argv_invalid: {cap_id}")
|
|
59
|
+
if cap.get("reader", {}).get("type") not in READERS or cap.get("source_level") not in SOURCE_LEVEL_VALUES:
|
|
60
|
+
raise ValueError(f"tool_output_contract_invalid: {cap_id}")
|
|
61
|
+
if not 0 < cap.get("timeout_s", 0) <= 3600:
|
|
62
|
+
raise ValueError(f"tool_timeout_invalid: {cap_id}")
|
|
63
|
+
if cap.get("reader", {}).get("type") == "media_dir":
|
|
64
|
+
if cap.get("then") != "asr.auto" or not {"media_download", "asr"} <= set(cap.get("gates", [])):
|
|
65
|
+
raise ValueError(f"tool_media_requires_asr: {cap_id}")
|
|
66
|
+
capabilities[cap_id] = {**cap, "card_id": tool_id}
|
|
67
|
+
seen: set[tuple[str, str]] = set()
|
|
68
|
+
routes = route_data.get("routes")
|
|
69
|
+
if not isinstance(routes, list):
|
|
70
|
+
raise ValueError("routes_missing")
|
|
71
|
+
for route in routes:
|
|
72
|
+
key = (route.get("source"), route.get("capability"))
|
|
73
|
+
chain = route.get("chain")
|
|
74
|
+
if not all(isinstance(x, str) and x for x in key) or key in seen or not isinstance(chain, list) or not chain:
|
|
75
|
+
raise ValueError("route_invalid_or_duplicate")
|
|
76
|
+
seen.add(key)
|
|
77
|
+
if len(chain) != len(set(chain)):
|
|
78
|
+
raise ValueError("route_backend_duplicate")
|
|
79
|
+
for backend in chain:
|
|
80
|
+
if backend not in capabilities or route["source"] not in capabilities[backend]["sources"]:
|
|
81
|
+
raise ValueError(f"route_backend_unknown: {backend}")
|
|
82
|
+
return Catalog(tools, capabilities, tuple(routes), route_data["version"], tuple(route_data.get("capabilities", [])))
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
@lru_cache(maxsize=1)
|
|
86
|
+
def load_catalog() -> Catalog:
|
|
87
|
+
resource = files("openmd.routing")
|
|
88
|
+
cards = [json.loads(p.read_text(encoding="utf-8")) for p in sorted(resource.joinpath("cards").iterdir(), key=lambda p: p.name) if p.name.endswith(".json")]
|
|
89
|
+
return validate_catalog(cards, json.loads(resource.joinpath("routes.json").read_text(encoding="utf-8")))
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
def overlay_file() -> Path:
|
|
93
|
+
config = load_local_config()
|
|
94
|
+
if config.get("route_overlay"):
|
|
95
|
+
return Path(config["route_overlay"]).expanduser().resolve()
|
|
96
|
+
location = local_config_path()
|
|
97
|
+
return (location.parent if location else Path.home() / ".openmd") / "route-overrides.json"
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
def effective_routes(source: str | None = None) -> list[dict[str, Any]]:
|
|
101
|
+
routes = copy.deepcopy(list(load_catalog().routes))
|
|
102
|
+
path = overlay_file()
|
|
103
|
+
if path.is_file():
|
|
104
|
+
from .lifecycle import apply_overlay
|
|
105
|
+
|
|
106
|
+
routes = apply_overlay(routes, path)
|
|
107
|
+
for route in routes:
|
|
108
|
+
route.setdefault("overlay_applied", False)
|
|
109
|
+
return [r for r in routes if source is None or r["source"] == source]
|
|
110
|
+
|
|
111
|
+
|
|
112
|
+
def default_backend_chains(*, effective: bool = True) -> dict[str, tuple[str, ...]]:
|
|
113
|
+
result: dict[str, list[str]] = {}
|
|
114
|
+
priority = {"transcript": 0, "content": 0, "media": 1}
|
|
115
|
+
routes = effective_routes() if effective else load_catalog().routes
|
|
116
|
+
for route in sorted(routes, key=lambda r: priority.get(r["capability"], 2)):
|
|
117
|
+
chain = result.setdefault(route["source"], [])
|
|
118
|
+
chain.extend(backend for backend in route["chain"] if backend not in chain)
|
|
119
|
+
return {source: tuple(chain) for source, chain in result.items()}
|
|
120
|
+
|
|
121
|
+
|
|
122
|
+
def route_view(source: str | None = None, *, effective: bool = True) -> dict[str, Any]:
|
|
123
|
+
catalog = load_catalog()
|
|
124
|
+
routes = effective_routes(source) if effective else [{**r, "overlay_applied": False} for r in catalog.routes if source is None or r["source"] == source]
|
|
125
|
+
backend_ids = {b for r in routes for b in r["chain"]}
|
|
126
|
+
tool_ids = {catalog.capabilities[b]["card_id"] for b in backend_ids}
|
|
127
|
+
return {"schema": "openmd.route-view/v1", "version": catalog.version, "overlay_applied": any(r["overlay_applied"] for r in routes), "routes": routes, "tools": [{"id": c["id"], "kind": c["kind"], "version": c.get("version"), "license": c.get("license")} for c in catalog.cards.values() if c["id"] in tool_ids]}
|
|
128
|
+
|
|
129
|
+
|
|
130
|
+
def route_trace(source: str, backend: str | None) -> dict[str, Any]:
|
|
131
|
+
"""Safe provenance fields shared by providers, receipts and runlogs."""
|
|
132
|
+
catalog = load_catalog()
|
|
133
|
+
capability = catalog.capabilities.get(backend or "", {})
|
|
134
|
+
return {
|
|
135
|
+
"card_id": capability.get("card_id"),
|
|
136
|
+
"route_version": catalog.version,
|
|
137
|
+
"overlay_applied": any(route["overlay_applied"] for route in effective_routes(source)),
|
|
138
|
+
}
|