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.
Files changed (177) hide show
  1. openmd/__init__.py +3 -0
  2. openmd/__main__.py +5 -0
  3. openmd/adapters/__init__.py +1 -0
  4. openmd/adapters/base.py +95 -0
  5. openmd/adapters/bilibili/__init__.py +11 -0
  6. openmd/adapters/bilibili/adapter.py +104 -0
  7. openmd/adapters/bilibili/discovery.py +33 -0
  8. openmd/adapters/douyin/__init__.py +11 -0
  9. openmd/adapters/douyin/adapter.py +117 -0
  10. openmd/adapters/douyin/discovery.py +36 -0
  11. openmd/adapters/douyin/sidecar.py +77 -0
  12. openmd/adapters/generic_media/__init__.py +14 -0
  13. openmd/adapters/high_privacy.py +77 -0
  14. openmd/adapters/local_audio/__init__.py +14 -0
  15. openmd/adapters/local_audio/adapter.py +169 -0
  16. openmd/adapters/local_audio/asr.py +58 -0
  17. openmd/adapters/local_audio/backends/__init__.py +1 -0
  18. openmd/adapters/local_audio/backends/auto.py +177 -0
  19. openmd/adapters/local_audio/backends/faster_whisper.py +285 -0
  20. openmd/adapters/local_audio/backends/funasr_sense_voice.py +14 -0
  21. openmd/adapters/local_audio/backends/qwen3_asr.py +254 -0
  22. openmd/adapters/opencli.py +770 -0
  23. openmd/adapters/pdf/__init__.py +11 -0
  24. openmd/adapters/pdf/adapter.py +153 -0
  25. openmd/adapters/pdf/backends/__init__.py +1 -0
  26. openmd/adapters/pdf/backends/markitdown.py +165 -0
  27. openmd/adapters/pdf/parser.py +56 -0
  28. openmd/adapters/qq_group/__init__.py +11 -0
  29. openmd/adapters/qq_group/adapter.py +289 -0
  30. openmd/adapters/qq_group/parsers/__init__.py +1 -0
  31. openmd/adapters/qq_group/parsers/html.py +60 -0
  32. openmd/adapters/qq_group/parsers/json.py +71 -0
  33. openmd/adapters/registry.py +149 -0
  34. openmd/adapters/screenshot/__init__.py +11 -0
  35. openmd/adapters/screenshot/adapter.py +154 -0
  36. openmd/adapters/screenshot/backends/__init__.py +1 -0
  37. openmd/adapters/screenshot/backends/paddle.py +15 -0
  38. openmd/adapters/screenshot/backends/rapidocr.py +176 -0
  39. openmd/adapters/screenshot/backends/tesseract.py +15 -0
  40. openmd/adapters/screenshot/ocr.py +84 -0
  41. openmd/adapters/wechat_group/__init__.py +11 -0
  42. openmd/adapters/wechat_group/adapter.py +160 -0
  43. openmd/adapters/wechat_group/wx_cli.py +268 -0
  44. openmd/adapters/wechat_moments/__init__.py +11 -0
  45. openmd/adapters/wechat_moments/adapter.py +167 -0
  46. openmd/adapters/wechat_moments/wx_cli.py +265 -0
  47. openmd/adapters/weixin/__init__.py +11 -0
  48. openmd/adapters/weixin/adapter.py +148 -0
  49. openmd/adapters/weixin/discovery.py +510 -0
  50. openmd/adapters/weixin/report.py +278 -0
  51. openmd/adapters/xiaohongshu.py +158 -0
  52. openmd/backends/__init__.py +1 -0
  53. openmd/backends/base.py +204 -0
  54. openmd/backends/bilibili.py +1480 -0
  55. openmd/backends/bilibili_wbi.py +277 -0
  56. openmd/backends/discourse.py +426 -0
  57. openmd/backends/douyin.py +868 -0
  58. openmd/backends/douyin_detail.py +124 -0
  59. openmd/backends/douyin_providers.py +602 -0
  60. openmd/backends/executor.py +546 -0
  61. openmd/backends/feishu.py +861 -0
  62. openmd/backends/generic_media.py +77 -0
  63. openmd/backends/interactive.py +60 -0
  64. openmd/backends/ocr.py +41 -0
  65. openmd/backends/opencli.py +82 -0
  66. openmd/backends/pdf.py +122 -0
  67. openmd/backends/registry.py +234 -0
  68. openmd/backends/screenshot.py +79 -0
  69. openmd/backends/video_metadata.py +167 -0
  70. openmd/backends/web_article.py +363 -0
  71. openmd/backends/wechat_group.py +35 -0
  72. openmd/backends/wechat_moments.py +35 -0
  73. openmd/backends/weixin.py +767 -0
  74. openmd/backends/x.py +455 -0
  75. openmd/backends/xiaoe.py +940 -0
  76. openmd/backends/xiaohongshu.py +911 -0
  77. openmd/backends/xiaohongshu_providers.py +876 -0
  78. openmd/backends/xiaoyuzhou.py +1017 -0
  79. openmd/backends/youtube.py +280 -0
  80. openmd/backends/youtube_discovery.py +1245 -0
  81. openmd/backends/youtube_transcript.py +213 -0
  82. openmd/browser_extension/background.js +231 -0
  83. openmd/browser_extension/icons/icon-128.png +0 -0
  84. openmd/browser_extension/icons/icon-16.png +0 -0
  85. openmd/browser_extension/icons/icon-32.png +0 -0
  86. openmd/browser_extension/icons/icon-48.png +0 -0
  87. openmd/browser_extension/icons/icon-512.png +0 -0
  88. openmd/browser_extension/idle.html +6 -0
  89. openmd/browser_extension/manifest.json +43 -0
  90. openmd/browser_extension/options.html +42 -0
  91. openmd/browser_extension/options.js +117 -0
  92. openmd/browser_extension/popup.css +71 -0
  93. openmd/browser_extension/popup.html +17 -0
  94. openmd/browser_extension/popup.js +59 -0
  95. openmd/browser_extension/vendor/opencli-bridge/LICENSE +190 -0
  96. openmd/browser_extension/vendor/opencli-bridge/VENDOR.md +31 -0
  97. openmd/browser_extension/vendor/opencli-bridge/background.js +2742 -0
  98. openmd/browser_host.py +503 -0
  99. openmd/browser_setup.py +106 -0
  100. openmd/cli.py +2012 -0
  101. openmd/console.py +32 -0
  102. openmd/core/__init__.py +1 -0
  103. openmd/core/capabilities.py +162 -0
  104. openmd/core/content_validation.py +323 -0
  105. openmd/core/coverage.py +163 -0
  106. openmd/core/detector.py +247 -0
  107. openmd/core/doctor.py +104 -0
  108. openmd/core/execution_policy.py +164 -0
  109. openmd/core/filename.py +64 -0
  110. openmd/core/input_validation.py +91 -0
  111. openmd/core/local_config.py +96 -0
  112. openmd/core/manifest.py +156 -0
  113. openmd/core/models.py +168 -0
  114. openmd/core/opencli_output.py +34 -0
  115. openmd/core/operation_contract.py +60 -0
  116. openmd/core/output.py +194 -0
  117. openmd/core/package_contract.py +335 -0
  118. openmd/core/package_validator.py +489 -0
  119. openmd/core/package_writer.py +666 -0
  120. openmd/core/planner.py +345 -0
  121. openmd/core/preferences.py +61 -0
  122. openmd/core/readable.py +129 -0
  123. openmd/core/receipt.py +285 -0
  124. openmd/core/renderer.py +185 -0
  125. openmd/core/runlog.py +208 -0
  126. openmd/core/secrets.py +313 -0
  127. openmd/doctor_view.py +68 -0
  128. openmd/interactive.py +453 -0
  129. openmd/ocr/__init__.py +1 -0
  130. openmd/ocr/breakers.py +84 -0
  131. openmd/ocr/config.py +54 -0
  132. openmd/ocr/documents.py +522 -0
  133. openmd/ocr/endpoint.py +72 -0
  134. openmd/ocr/layout.py +92 -0
  135. openmd/ocr/layout_filter.py +118 -0
  136. openmd/ocr/layout_worker.py +89 -0
  137. openmd/ocr/native_math.py +220 -0
  138. openmd/ocr/office.py +305 -0
  139. openmd/ocr/ollama.py +200 -0
  140. openmd/ocr/otsl.py +51 -0
  141. openmd/ocr/ovis-prompt.txt +1 -0
  142. openmd/ocr/pipeline.py +384 -0
  143. openmd/ocr/settings.json +25 -0
  144. openmd/ocr/tiling.py +631 -0
  145. openmd/post_video.py +154 -0
  146. openmd/probe.py +397 -0
  147. openmd/routing/__init__.py +1 -0
  148. openmd/routing/cards/builtin.json +620 -0
  149. openmd/routing/cards/defuddle.json +70 -0
  150. openmd/routing/cards/x-tweet-fetcher.json +71 -0
  151. openmd/routing/cards/xyz-dl.json +60 -0
  152. openmd/routing/cards/yutto.json +68 -0
  153. openmd/routing/entry_shim.py +71 -0
  154. openmd/routing/lifecycle.py +459 -0
  155. openmd/routing/loader.py +138 -0
  156. openmd/routing/providers.py +158 -0
  157. openmd/routing/readers.py +108 -0
  158. openmd/routing/routes.json +504 -0
  159. openmd/routing/runner.py +192 -0
  160. openmd/routing/view.py +40 -0
  161. openmd/runtime/__init__.py +1 -0
  162. openmd/runtime/_windows_job.py +234 -0
  163. openmd/runtime/asr.py +217 -0
  164. openmd/runtime/asr_worker.py +119 -0
  165. openmd/runtime/gpu_lock.py +81 -0
  166. openmd/runtime/gpu_quarantine.py +85 -0
  167. openmd/runtime/interpreters.py +407 -0
  168. openmd/runtime/managed_tools.py +204 -0
  169. openmd/runtime/models.py +202 -0
  170. openmd/runtime/services.py +175 -0
  171. openmd/runtime/tempdirs.py +227 -0
  172. openmd_cli-0.1.0.dist-info/METADATA +168 -0
  173. openmd_cli-0.1.0.dist-info/RECORD +177 -0
  174. openmd_cli-0.1.0.dist-info/WHEEL +5 -0
  175. openmd_cli-0.1.0.dist-info/entry_points.txt +2 -0
  176. openmd_cli-0.1.0.dist-info/licenses/LICENSE +21 -0
  177. 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
@@ -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
+ }