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,158 @@
|
|
|
1
|
+
"""Bridge declarative command cards into the existing backend executor."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import hashlib
|
|
6
|
+
import importlib.metadata
|
|
7
|
+
import json
|
|
8
|
+
import shutil
|
|
9
|
+
import subprocess
|
|
10
|
+
import tempfile
|
|
11
|
+
from dataclasses import replace
|
|
12
|
+
from pathlib import Path
|
|
13
|
+
from typing import Any
|
|
14
|
+
|
|
15
|
+
from openmd.backends.base import BackendCapability, BackendDescriptor, BackendHealth
|
|
16
|
+
from openmd.core.local_config import load_local_config
|
|
17
|
+
from openmd.core.models import TranscriptResult
|
|
18
|
+
from openmd.core.secrets import sanitize_error_summary
|
|
19
|
+
|
|
20
|
+
from .loader import load_catalog, route_trace
|
|
21
|
+
from .readers import read_media, read_text
|
|
22
|
+
from .runner import ToolError, isolated_environment, resolve_launcher, run_tool
|
|
23
|
+
|
|
24
|
+
from openmd.runtime.tempdirs import owned_temporary_directory
|
|
25
|
+
|
|
26
|
+
GATE_OPTIONS = {"network": "enable_network_backends", "browser": "enable_browser_backends", "media_download": "enable_media_download_backends", "asr": "enable_asr_backends", "cloud": "enable_cloud_backends"}
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def gates_open(capability: dict[str, Any], options) -> bool:
|
|
30
|
+
return all(gate not in {"account", "local_export"} and (gate == "local_compute" or bool(getattr(options, GATE_OPTIONS.get(gate, ""), False))) for gate in capability.get("gates", []))
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def installed_identity(card: dict[str, Any]) -> dict[str, Any]:
|
|
34
|
+
launcher = resolve_launcher(card)
|
|
35
|
+
digest = hashlib.sha256(launcher.entry_path.read_bytes()).hexdigest()
|
|
36
|
+
actual = None
|
|
37
|
+
if card["kind"] == "command":
|
|
38
|
+
for parent in [launcher.entry_path.parent, *list(launcher.entry_path.parents)[:3]]:
|
|
39
|
+
metadata = parent / "package.json"
|
|
40
|
+
if metadata.is_file():
|
|
41
|
+
try:
|
|
42
|
+
actual = json.loads(metadata.read_text(encoding="utf-8")).get("version")
|
|
43
|
+
except (OSError, ValueError):
|
|
44
|
+
pass
|
|
45
|
+
if actual:
|
|
46
|
+
break
|
|
47
|
+
elif card["kind"] == "python_entry":
|
|
48
|
+
paths = load_local_config().get("tools", {}).get(card["id"], {}).get("paths")
|
|
49
|
+
wanted = card["id"].lower().replace("_", "-")
|
|
50
|
+
for dist in importlib.metadata.distributions(path=paths):
|
|
51
|
+
if str(dist.metadata.get("Name", "")).lower().replace("_", "-") == wanted:
|
|
52
|
+
actual = dist.version
|
|
53
|
+
break
|
|
54
|
+
if card["kind"] == "python_script":
|
|
55
|
+
actual = "sha256:" + digest
|
|
56
|
+
verified = card.get("installation", {}).get("sha256") == digest
|
|
57
|
+
else:
|
|
58
|
+
verified = actual == card.get("version")
|
|
59
|
+
locked_digest = card.get("installation", {}).get("sha256")
|
|
60
|
+
if locked_digest and locked_digest != digest:
|
|
61
|
+
verified = False
|
|
62
|
+
return {"tool_version": actual, "tool_version_verified": verified, "version_origin": "installed_metadata" if card["kind"] != "python_script" else "source_sha256", "entry_sha256": digest, "origin": launcher.origin}
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
class CommandToolProvider:
|
|
66
|
+
def __init__(self, card: dict[str, Any], capability: dict[str, Any], options) -> None:
|
|
67
|
+
self.card, self.capability, self.options = card, capability, options
|
|
68
|
+
self.name = capability["id"]
|
|
69
|
+
|
|
70
|
+
def descriptor(self) -> BackendDescriptor:
|
|
71
|
+
gates = self.capability.get("gates", [])
|
|
72
|
+
return BackendDescriptor(name=self.name, layer="command_card", capabilities=(BackendCapability(platforms=tuple(self.capability["sources"]), source_types=("article", "audio", "video"), input_types=("single", "batch", "collection"), source_levels=(self.capability["source_level"],), requires_network="network" in gates, requires_credentials=bool(self.capability.get("requires_login"))),), description="Declared tool with validated artifact output.", provider=self.card["id"])
|
|
73
|
+
|
|
74
|
+
def health(self) -> BackendHealth:
|
|
75
|
+
if not gates_open(self.capability, self.options):
|
|
76
|
+
return BackendHealth(ok=False, error_code="tool_permission_required", error_summary="Explicit runtime gates are required for this tool.")
|
|
77
|
+
try:
|
|
78
|
+
identity = installed_identity(self.card)
|
|
79
|
+
if not identity["tool_version_verified"]:
|
|
80
|
+
return BackendHealth(ok=False, error_code="tool_version_mismatch", error_summary="Installed tool identity differs from its locked card; inspect doctor before updating the card.")
|
|
81
|
+
return BackendHealth(ok=True, dependencies={self.card["id"]: identity["tool_version"]})
|
|
82
|
+
except (ToolError, OSError, ValueError) as exc:
|
|
83
|
+
return BackendHealth(ok=False, error_code=getattr(exc, "code", "tool_not_installed"), error_summary=sanitize_error_summary(exc))
|
|
84
|
+
|
|
85
|
+
def extract(self, request) -> TranscriptResult:
|
|
86
|
+
if not gates_open(self.capability, self.options):
|
|
87
|
+
return self._failed("tool_permission_required", "Required runtime gates have not been enabled.")
|
|
88
|
+
if request.item.platform not in self.capability["sources"]:
|
|
89
|
+
return self._failed("tool_input_unsupported", "Tool card does not cover this source.")
|
|
90
|
+
metadata: dict[str, Any] = {}
|
|
91
|
+
try:
|
|
92
|
+
identity = installed_identity(self.card)
|
|
93
|
+
if not identity["tool_version_verified"]:
|
|
94
|
+
return self._failed("tool_version_mismatch", "Installed tool differs from its locked card; run openmd doctor --profile tools.")
|
|
95
|
+
with owned_temporary_directory(prefix="openmd-tool-", ignore_cleanup_errors=True) as tmp:
|
|
96
|
+
workdir = Path(tmp)
|
|
97
|
+
run = run_tool(self.card, self.capability, request.item, workdir)
|
|
98
|
+
metadata = {"tool_run": {"seconds": run.seconds, "exit_code": run.exit_code, "files": run.files, "card_id": self.card["id"], **identity}, **route_trace(request.item.platform, self.name)}
|
|
99
|
+
if self.card["id"] == "yutto":
|
|
100
|
+
metadata["tool_run"]["anonymous_notice"] = "未提供登录认证信息" in run.stdout + run.stderr
|
|
101
|
+
if run.exit_code != 0:
|
|
102
|
+
return replace(self._failed("tool_backend_exit", f"Tool exited with code {run.exit_code}; raw logs were not retained."), metadata=metadata)
|
|
103
|
+
if self.capability.get("then"):
|
|
104
|
+
from openmd.runtime.asr import get_asr_backend
|
|
105
|
+
|
|
106
|
+
media = read_media(workdir)
|
|
107
|
+
result = get_asr_backend(self.capability["then"]).transcribe(media)
|
|
108
|
+
metadata.update({"media_tool": self.card["id"], "media_bytes": media.stat().st_size, **result.metadata})
|
|
109
|
+
result = replace(result, backend=self.name, source_level="media_asr" if result.has_usable_text else "partial_extraction", title=request.item.title or media.stem, source_notes=[*result.source_notes, f"Media fetched by {self.card['id']} and transcribed locally; command logs are not transcript evidence."])
|
|
110
|
+
if getattr(self.options, "keep_media_artifacts", False) and getattr(self.options, "media_artifact_dir", None):
|
|
111
|
+
destination = Path(self.options.media_artifact_dir)
|
|
112
|
+
try:
|
|
113
|
+
destination.mkdir(parents=True, exist_ok=True)
|
|
114
|
+
with tempfile.NamedTemporaryFile(prefix=hashlib.sha256(request.item.url.encode()).hexdigest()[:12] + "-", suffix=media.suffix, dir=destination, delete=False) as dst, media.open("rb") as src:
|
|
115
|
+
shutil.copyfileobj(src, dst)
|
|
116
|
+
metadata["media_retained"] = True
|
|
117
|
+
except OSError:
|
|
118
|
+
metadata["media_retained"] = False
|
|
119
|
+
result = replace(result, source_notes=[*result.source_notes, "Transcript succeeded, but the optional media copy could not be retained."])
|
|
120
|
+
else:
|
|
121
|
+
result = read_text(self.capability, run, workdir)
|
|
122
|
+
return replace(result, metadata={**result.metadata, **metadata})
|
|
123
|
+
except (ToolError, OSError, ValueError) as exc:
|
|
124
|
+
return replace(self._failed(getattr(exc, "code", "tool_artifact_invalid"), sanitize_error_summary(exc) or "Tool failed."), metadata=metadata)
|
|
125
|
+
|
|
126
|
+
def _failed(self, code: str, detail: str) -> TranscriptResult:
|
|
127
|
+
return TranscriptResult(text="", status="failed", source_level="partial_extraction", backend=self.name, source_notes=["The declared tool did not produce validated source content."], error_code=code, error_summary=detail)
|
|
128
|
+
|
|
129
|
+
|
|
130
|
+
def register_card_providers(registry, platform: str, options) -> None:
|
|
131
|
+
catalog = load_catalog()
|
|
132
|
+
requested = options.backend_override
|
|
133
|
+
for capability in catalog.capabilities.values():
|
|
134
|
+
card = catalog.cards[capability["card_id"]]
|
|
135
|
+
if card["kind"] != "builtin" and platform in capability["sources"] and (requested is None or requested == capability["id"]) and gates_open(capability, options):
|
|
136
|
+
registry.register(capability["id"], CommandToolProvider(card, capability, options))
|
|
137
|
+
|
|
138
|
+
|
|
139
|
+
def doctor_tools(*, probe_commands: bool = False) -> dict[str, Any]:
|
|
140
|
+
reports: list[dict[str, Any]] = []
|
|
141
|
+
for card in load_catalog().cards.values():
|
|
142
|
+
if card["kind"] == "builtin":
|
|
143
|
+
continue
|
|
144
|
+
record = {"tool": card["id"], "kind": card["kind"], "declared_version": card["version"], "requires_login": any(c.get("requires_login", False) for c in card["capabilities"]), "gates": sorted({g for c in card["capabilities"] for g in c.get("gates", [])}), "resolved": False, "origin": "none", "version": None, "version_probe": "not_requested"}
|
|
145
|
+
try:
|
|
146
|
+
identity = installed_identity(card)
|
|
147
|
+
record.update(identity)
|
|
148
|
+
record.update({"resolved": True, "version": identity["tool_version"]})
|
|
149
|
+
if not identity["tool_version_verified"]:
|
|
150
|
+
record["error_code"] = "tool_version_mismatch"
|
|
151
|
+
# Local metadata/hash inspection is sufficient for these cards; never
|
|
152
|
+
# run a tool's ordinary operation just to discover a version.
|
|
153
|
+
if probe_commands:
|
|
154
|
+
record["version_probe"] = "installed_metadata_and_source_hash"
|
|
155
|
+
except (ToolError, OSError, ValueError) as exc:
|
|
156
|
+
record["error_code"] = getattr(exc, "code", "tool_not_installed")
|
|
157
|
+
reports.append(record)
|
|
158
|
+
return {"ok": all(r["resolved"] and r.get("tool_version_verified") for r in reports), "tools": reports, "network_access": "not_probed", "private_session_state": "not_read"}
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
"""Read declared artifacts; command logs and metadata never count as source text."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import json
|
|
6
|
+
import re
|
|
7
|
+
from pathlib import Path
|
|
8
|
+
from typing import Any
|
|
9
|
+
|
|
10
|
+
from openmd.core.content_validation import validate_extraction_content
|
|
11
|
+
from openmd.core.models import TranscriptResult
|
|
12
|
+
|
|
13
|
+
from .runner import ToolError, ToolRun
|
|
14
|
+
|
|
15
|
+
MEDIA_EXTENSIONS = frozenset({".m4a", ".mp3", ".aac", ".opus", ".wav", ".mp4", ".webm", ".ogg", ".flac"})
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def field_value(data: Any, path: str | list[str]) -> str | None:
|
|
19
|
+
if isinstance(path, list):
|
|
20
|
+
return next((value for key in path if (value := field_value(data, key))), None)
|
|
21
|
+
current = data
|
|
22
|
+
for key in path.split("."):
|
|
23
|
+
if not isinstance(current, dict) or key not in current:
|
|
24
|
+
return None
|
|
25
|
+
current = current[key]
|
|
26
|
+
return current if isinstance(current, str) and current.strip() else None
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def contained_file(workdir: Path, relative: str) -> Path:
|
|
30
|
+
path = (workdir / relative).resolve()
|
|
31
|
+
if not path.is_relative_to(workdir.resolve()) or not path.is_file():
|
|
32
|
+
raise ToolError("tool_artifact_missing")
|
|
33
|
+
return path
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def subtitle_text(text: str) -> str:
|
|
37
|
+
lines: list[str] = []
|
|
38
|
+
timestamp: str | None = None
|
|
39
|
+
for line in text.splitlines():
|
|
40
|
+
match = re.match(r"\s*((?:\d{2}:)?\d{2}:\d{2})[,.]\d+\s*-->", line)
|
|
41
|
+
if match:
|
|
42
|
+
timestamp = match.group(1)
|
|
43
|
+
if timestamp.count(":") == 1:
|
|
44
|
+
timestamp = "00:" + timestamp
|
|
45
|
+
elif not line.strip() or line.strip().isdigit() or line.strip() == "WEBVTT":
|
|
46
|
+
continue
|
|
47
|
+
elif timestamp:
|
|
48
|
+
value = re.sub(r"<[^>]+>", "", line).strip()
|
|
49
|
+
if value:
|
|
50
|
+
lines.append(f"[{timestamp}] {value}")
|
|
51
|
+
return "\n".join(lines)
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
def read_media(workdir: Path) -> Path:
|
|
55
|
+
media = [p for p in workdir.rglob("*") if p.is_file() and p.suffix.lower() in MEDIA_EXTENSIONS and p.resolve().is_relative_to(workdir.resolve()) and p.stat().st_size > 0]
|
|
56
|
+
if not media:
|
|
57
|
+
raise ToolError("tool_media_missing", "The tool returned no media; metadata and command logs are insufficient.")
|
|
58
|
+
return max(media, key=lambda p: p.stat().st_size)
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
def read_text(capability: dict[str, Any], run: ToolRun, workdir: Path) -> TranscriptResult:
|
|
62
|
+
reader = capability["reader"]
|
|
63
|
+
title = author = published_at = None
|
|
64
|
+
kind = reader["type"]
|
|
65
|
+
if kind == "markdown_file":
|
|
66
|
+
path = contained_file(workdir, reader["path"])
|
|
67
|
+
if path.stat().st_size > 16 * 1024 * 1024:
|
|
68
|
+
raise ToolError("tool_output_too_large")
|
|
69
|
+
text = path.read_text(encoding="utf-8-sig")
|
|
70
|
+
heading = re.search(r"^#\s+(.+)$", text, re.M)
|
|
71
|
+
title = heading.group(1).strip() if heading else None
|
|
72
|
+
elif kind in {"json_stdout", "json_file"}:
|
|
73
|
+
if kind == "json_file":
|
|
74
|
+
path = contained_file(workdir, reader["path"])
|
|
75
|
+
if path.stat().st_size > 16 * 1024 * 1024:
|
|
76
|
+
raise ToolError("tool_output_too_large")
|
|
77
|
+
raw_json = path.read_text(encoding="utf-8-sig")
|
|
78
|
+
else:
|
|
79
|
+
raw_json = run.stdout
|
|
80
|
+
try:
|
|
81
|
+
data = json.loads(raw_json)
|
|
82
|
+
except ValueError as exc:
|
|
83
|
+
raise ToolError("tool_json_invalid") from exc
|
|
84
|
+
if not isinstance(data, dict) or data.get("error") or data.get("success") is False:
|
|
85
|
+
raise ToolError("tool_upstream_failed", "Tool JSON contains an upstream error.")
|
|
86
|
+
fields = reader["fields"]
|
|
87
|
+
text = field_value(data, fields["text"]) or ""
|
|
88
|
+
title = field_value(data, fields.get("title", "title"))
|
|
89
|
+
author = field_value(data, fields.get("author", "author"))
|
|
90
|
+
published_at = field_value(data, fields.get("published_at", "published_at"))
|
|
91
|
+
if not title and text and reader.get("title_from_first_line", False):
|
|
92
|
+
title = text.splitlines()[0][:120]
|
|
93
|
+
elif kind == "subtitle_dir":
|
|
94
|
+
paths = sorted(p for p in workdir.rglob("*") if p.suffix.lower() in {".srt", ".vtt"} and p.is_file() and p.resolve().is_relative_to(workdir.resolve()))
|
|
95
|
+
if not paths:
|
|
96
|
+
raise ToolError("tool_subtitle_missing")
|
|
97
|
+
text = subtitle_text(paths[0].read_text(encoding="utf-8-sig"))
|
|
98
|
+
else:
|
|
99
|
+
raise ToolError("tool_reader_invalid")
|
|
100
|
+
text = text.strip()
|
|
101
|
+
if len(text) < capability.get("validation", {}).get("min_chars", 1):
|
|
102
|
+
raise ToolError("tool_content_empty", "Tool returned insufficient source text.")
|
|
103
|
+
if len(text) < 3000 and re.search(r"(?im)^\s*(?:#\s*)?(?:just a moment[.!…]*|verify you are human|请完成验证|请先登录|访问过于频繁|安全验证)\s*$", text):
|
|
104
|
+
raise ToolError("tool_challenge_page", "A challenge/login page is not source content.")
|
|
105
|
+
if re.match(r"\s*(?:<!doctype html|<html)", text, re.I):
|
|
106
|
+
raise ToolError("tool_html_instead_of_markdown")
|
|
107
|
+
result = validate_extraction_content(TranscriptResult(text=text + "\n", status="verified", source_level=capability["source_level"], backend=capability["id"], title=title, author=author, published_at=published_at, source_notes=["Original source text read from the tool's declared artifact.", *([capability["coverage_note"]] if capability.get("coverage_note") else [])]))
|
|
108
|
+
return result
|