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,280 @@
1
+ """YouTube-specific opt-in backend providers.
2
+
3
+ This extension module is loaded by ``openmd.backends.registry`` using the
4
+ platform naming convention. Providers here stay behind explicit runtime gates;
5
+ importing the module does not touch the network, browser sessions, cookies, or
6
+ media files.
7
+ """
8
+
9
+ from __future__ import annotations
10
+
11
+ import json
12
+ import shutil
13
+ from collections.abc import Callable, Iterable
14
+ from typing import Any
15
+
16
+ from openmd.core.opencli_output import load_opencli_json
17
+ from openmd.core.models import ContentItem, TranscriptResult
18
+ from openmd.core.secrets import sanitize_error_summary
19
+
20
+ from .base import BackendCapability, BackendDescriptor, BackendExtractRequest, BackendHealth
21
+ from .youtube_discovery import _run_opencli
22
+ from .youtube_transcript import extract_youtube_video_id
23
+
24
+
25
+ OPENCLI_YOUTUBE_TRANSCRIPT_BACKEND = "opencli.youtube.transcript"
26
+ DEFAULT_TRANSCRIPT_ATTEMPTS: tuple[tuple[str, str], ...] = (
27
+ ("grouped", "md"),
28
+ ("grouped", "json"),
29
+ ("raw", "md"),
30
+ ("raw", "json"),
31
+ )
32
+ DEFAULT_MIN_TRANSCRIPT_CHARS = 20
33
+
34
+ OpenCliRunner = Callable[..., tuple[int, str, str]]
35
+
36
+
37
+ def register_backend_providers(registry: Any, options: Any) -> None:
38
+ """Register YouTube extraction providers under explicit runtime gates."""
39
+ if getattr(options, "enable_browser_backends", False):
40
+ registry.register(OPENCLI_YOUTUBE_TRANSCRIPT_BACKEND, OpenCliYouTubeTranscriptProvider())
41
+
42
+
43
+ def build_opencli_youtube_transcript_args(video_id: str, *, mode: str, output_format: str) -> list[str]:
44
+ """Build a direct-video-id OpenCLI transcript command."""
45
+ return [
46
+ "opencli",
47
+ "youtube",
48
+ "transcript",
49
+ video_id,
50
+ "--mode",
51
+ mode,
52
+ "-f",
53
+ output_format,
54
+ ]
55
+
56
+
57
+ def classify_youtube_transcript_failure(message: object) -> str:
58
+ """Map known YouTube transcript blockers to stable failure codes."""
59
+ text = str(message or "").lower()
60
+ if "short_transcript" in text or "short transcript" in text:
61
+ return "youtube_transcript_short_result"
62
+ if "empty_result" in text or "empty result" in text or "returned no transcript" in text:
63
+ return "youtube_transcript_empty_result"
64
+ if "confirm you're not a bot" in text or "not a bot" in text or "bot check" in text:
65
+ return "youtube_bot_check"
66
+ if "cookie" in text or "cookies" in text or "login required" in text or "sign in" in text:
67
+ return "youtube_cookie_or_auth_required"
68
+ if "asr" in text or "audio transcription" in text or "download audio" in text:
69
+ return "youtube_asr_required"
70
+ if "timed out" in text or "timeout" in text:
71
+ return "opencli_youtube_transcript_timeout"
72
+ if "caption" in text and ("disabled" in text or "unavailable" in text or "not found" in text):
73
+ return "youtube_transcript_not_found"
74
+ if "blocked" in text:
75
+ return "youtube_request_blocked"
76
+ return "opencli_youtube_transcript_failed"
77
+
78
+
79
+ class OpenCliYouTubeTranscriptProvider:
80
+ """Fetch existing YouTube captions through OpenCLI using direct video ids."""
81
+
82
+ name = OPENCLI_YOUTUBE_TRANSCRIPT_BACKEND
83
+
84
+ def __init__(
85
+ self,
86
+ *,
87
+ runner: OpenCliRunner | None = None,
88
+ timeout: int = 120,
89
+ attempts: tuple[tuple[str, str], ...] = DEFAULT_TRANSCRIPT_ATTEMPTS,
90
+ min_transcript_chars: int = DEFAULT_MIN_TRANSCRIPT_CHARS,
91
+ ) -> None:
92
+ self._runner = runner or _run_opencli
93
+ self.timeout = timeout
94
+ self.attempts = attempts
95
+ self.min_transcript_chars = max(0, min_transcript_chars)
96
+
97
+ def descriptor(self) -> BackendDescriptor:
98
+ capability = BackendCapability(
99
+ platforms=("youtube",),
100
+ source_types=("video",),
101
+ input_types=("single", "collection"),
102
+ source_levels=("native_caption", "auto_caption", "third_party_extractor"),
103
+ requires_network=True,
104
+ requires_credentials=False,
105
+ optional_config=("mode", "format", "timeout"),
106
+ )
107
+ return BackendDescriptor(
108
+ name=self.name,
109
+ layer="L2 browser/session extractor",
110
+ capabilities=(capability,),
111
+ description="Fetch existing YouTube captions through OpenCLI without media download or ASR.",
112
+ provider="opencli.youtube",
113
+ )
114
+
115
+ def health(self) -> BackendHealth:
116
+ if self._runner is not _run_opencli:
117
+ return BackendHealth(ok=True, dependencies={"opencli": "injected"})
118
+ if shutil.which("opencli") is None and shutil.which("opencli.cmd") is None:
119
+ return BackendHealth(
120
+ ok=False,
121
+ dependencies={"opencli": "missing"},
122
+ error_code="backend_dependency_missing",
123
+ error_summary="opencli executable is not available on PATH.",
124
+ )
125
+ return BackendHealth(ok=True, dependencies={"opencli": "available"})
126
+
127
+ def extract(self, request: BackendExtractRequest) -> TranscriptResult:
128
+ item = request.item
129
+ if item.platform != "youtube":
130
+ return _failed(
131
+ item,
132
+ "unsupported_platform",
133
+ f"OpenCLI YouTube transcript only supports youtube, got {item.platform}.",
134
+ )
135
+
136
+ video_ids = _video_id_candidates(item)
137
+ if not video_ids:
138
+ return _failed(item, "invalid_input", "Could not extract a YouTube video id.")
139
+
140
+ last_error: object = "OpenCLI YouTube transcript returned no usable text."
141
+ last_code = "opencli_youtube_transcript_failed"
142
+ attempted: list[dict[str, object]] = []
143
+
144
+ for video_id in video_ids:
145
+ for mode, output_format in self.attempts:
146
+ args = build_opencli_youtube_transcript_args(
147
+ video_id,
148
+ mode=mode,
149
+ output_format=output_format,
150
+ )
151
+ exit_code, stdout, stderr = self._runner(args, timeout=self.timeout)
152
+ attempted.append(
153
+ {
154
+ "video_id": video_id,
155
+ "mode": mode,
156
+ "format": output_format,
157
+ "exit_code": exit_code,
158
+ }
159
+ )
160
+
161
+ signal = stderr or stdout
162
+ if exit_code != 0:
163
+ last_error = signal or f"OpenCLI exited with code {exit_code}."
164
+ last_code = classify_youtube_transcript_failure(last_error)
165
+ if _is_hard_blocker(last_code):
166
+ return _failed(item, last_code, last_error, video_id=video_id, attempted=attempted)
167
+ continue
168
+
169
+ text = _extract_transcript_text(stdout, output_format=output_format)
170
+ if _is_usable_text(text, self.min_transcript_chars):
171
+ return TranscriptResult(
172
+ text=text.rstrip() + "\n",
173
+ status="verified",
174
+ source_level="third_party_extractor",
175
+ backend=self.name,
176
+ source_notes=[
177
+ "OpenCLI fetched existing YouTube transcript text.",
178
+ "Direct video-id command was used; no URL pre-navigation retry was needed.",
179
+ "No cookie export, media download, or ASR action was attempted.",
180
+ f"mode: {mode}",
181
+ f"format: {output_format}",
182
+ ],
183
+ metadata={
184
+ "video_id": video_id,
185
+ "mode": mode,
186
+ "format": output_format,
187
+ "attempt_count": len(attempted),
188
+ },
189
+ )
190
+
191
+ last_error = stdout or "EMPTY_RESULT: OpenCLI returned no transcript text."
192
+ last_code = (
193
+ "youtube_transcript_short_result"
194
+ if text.strip()
195
+ else classify_youtube_transcript_failure(last_error)
196
+ )
197
+
198
+ return _failed(item, last_code, last_error, video_id=video_ids[-1], attempted=attempted)
199
+
200
+
201
+ def _video_id_candidates(item: ContentItem) -> tuple[str, ...]:
202
+ candidates = [extract_youtube_video_id(item.url)]
203
+ if item.id:
204
+ candidates.append(extract_youtube_video_id(item.id) or item.id)
205
+
206
+ deduped: list[str] = []
207
+ for candidate in candidates:
208
+ if candidate and candidate not in deduped:
209
+ deduped.append(candidate)
210
+ return tuple(deduped)
211
+
212
+
213
+ def _extract_transcript_text(stdout: str, *, output_format: str) -> str:
214
+ if output_format == "json":
215
+ try:
216
+ payload = load_opencli_json(stdout)
217
+ except json.JSONDecodeError:
218
+ return stdout.strip()
219
+ return _text_from_json_payload(payload).strip()
220
+ return stdout.strip()
221
+
222
+
223
+ def _text_from_json_payload(payload: Any) -> str:
224
+ if isinstance(payload, str):
225
+ return payload
226
+ if isinstance(payload, list):
227
+ lines = [_text_from_json_payload(item).strip() for item in payload]
228
+ return "\n".join(line for line in lines if line)
229
+ if not isinstance(payload, dict):
230
+ return ""
231
+
232
+ for key in ("text", "transcript", "markdown", "content"):
233
+ value = payload.get(key)
234
+ if isinstance(value, str) and value.strip():
235
+ return value
236
+
237
+ for key in ("segments", "snippets", "items", "lines"):
238
+ value = payload.get(key)
239
+ if isinstance(value, list):
240
+ lines = [_text_from_json_payload(item).strip() for item in value]
241
+ return "\n".join(line for line in lines if line)
242
+ return ""
243
+
244
+
245
+ def _is_usable_text(text: str, min_chars: int) -> bool:
246
+ return len("".join(text.split())) >= min_chars
247
+
248
+
249
+ def _is_hard_blocker(code: str) -> bool:
250
+ return code in {"youtube_bot_check", "youtube_cookie_or_auth_required", "youtube_asr_required"}
251
+
252
+
253
+ def _failed(
254
+ item: ContentItem,
255
+ code: str,
256
+ error: object,
257
+ *,
258
+ video_id: str | None = None,
259
+ attempted: Iterable[dict[str, object]] = (),
260
+ ) -> TranscriptResult:
261
+ metadata: dict[str, object] = {"platform": item.platform}
262
+ if video_id:
263
+ metadata["video_id"] = video_id
264
+ attempted_list = list(attempted)
265
+ if attempted_list:
266
+ metadata["attempted"] = attempted_list
267
+ metadata["attempt_count"] = len(attempted_list)
268
+ return TranscriptResult(
269
+ text="",
270
+ status="failed",
271
+ source_level="partial_extraction",
272
+ backend=OPENCLI_YOUTUBE_TRANSCRIPT_BACKEND,
273
+ source_notes=[
274
+ "OpenCLI YouTube transcript returned structured failure.",
275
+ "No cookie export, media download, or ASR action was attempted.",
276
+ ],
277
+ error_code=code,
278
+ error_summary=sanitize_error_summary(error),
279
+ metadata=metadata,
280
+ )