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,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
|
+
)
|