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
openmd/__init__.py
ADDED
openmd/__main__.py
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"""Platform adapter interfaces and registry."""
|
openmd/adapters/base.py
ADDED
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
"""Platform adapter interface and skeleton adapter implementation."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from abc import ABC, abstractmethod
|
|
6
|
+
|
|
7
|
+
from openmd.core.detector import detect_platform, extract_first_url
|
|
8
|
+
from openmd.core.models import ContentItem, TranscriptResult
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class PlatformAdapter(ABC):
|
|
12
|
+
id: str
|
|
13
|
+
|
|
14
|
+
@abstractmethod
|
|
15
|
+
def can_handle(self, input_value: str) -> bool:
|
|
16
|
+
raise NotImplementedError
|
|
17
|
+
|
|
18
|
+
@abstractmethod
|
|
19
|
+
def resolve_items(
|
|
20
|
+
self, input_value: str, *, limit: int | None = None, all: bool = False
|
|
21
|
+
) -> list[ContentItem]:
|
|
22
|
+
raise NotImplementedError
|
|
23
|
+
|
|
24
|
+
@abstractmethod
|
|
25
|
+
def extract_text(
|
|
26
|
+
self,
|
|
27
|
+
item: ContentItem,
|
|
28
|
+
*,
|
|
29
|
+
backend: str | None = None,
|
|
30
|
+
backend_chain: tuple[str, ...] | None = None,
|
|
31
|
+
allow_fallback: bool = True,
|
|
32
|
+
) -> TranscriptResult:
|
|
33
|
+
raise NotImplementedError
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
class StubPlatformAdapter(PlatformAdapter):
|
|
37
|
+
"""Read-only adapter used until real platform backends are wired."""
|
|
38
|
+
|
|
39
|
+
def __init__(self, platform_id: str, default_backend: str) -> None:
|
|
40
|
+
self.id = platform_id
|
|
41
|
+
self.default_backend = default_backend
|
|
42
|
+
|
|
43
|
+
def can_handle(self, input_value: str) -> bool:
|
|
44
|
+
return detect_platform(input_value) == self.id
|
|
45
|
+
|
|
46
|
+
def resolve_items(
|
|
47
|
+
self, input_value: str, *, limit: int | None = None, all: bool = False
|
|
48
|
+
) -> list[ContentItem]:
|
|
49
|
+
input_type = "collection" if limit is not None or all else "single"
|
|
50
|
+
source_type = "video" if self.id in {"bilibili", "youtube", "douyin", "tiktok"} else "article"
|
|
51
|
+
if self.id in {"xiaoyuzhou"}:
|
|
52
|
+
source_type = "audio"
|
|
53
|
+
if self.id in {"feishu"}:
|
|
54
|
+
source_type = "document"
|
|
55
|
+
return [
|
|
56
|
+
ContentItem(
|
|
57
|
+
platform=self.id,
|
|
58
|
+
input_type=input_type,
|
|
59
|
+
url=extract_first_url(input_value),
|
|
60
|
+
source_type=source_type,
|
|
61
|
+
)
|
|
62
|
+
]
|
|
63
|
+
|
|
64
|
+
def extract_text(
|
|
65
|
+
self,
|
|
66
|
+
item: ContentItem,
|
|
67
|
+
*,
|
|
68
|
+
backend: str | None = None,
|
|
69
|
+
backend_chain: tuple[str, ...] | None = None,
|
|
70
|
+
allow_fallback: bool = True,
|
|
71
|
+
) -> TranscriptResult:
|
|
72
|
+
chain = backend_chain or (backend or self.default_backend,)
|
|
73
|
+
if not allow_fallback:
|
|
74
|
+
chain = chain[:1]
|
|
75
|
+
selected_backend = chain[0] if chain else self.default_backend
|
|
76
|
+
if selected_backend == "mock":
|
|
77
|
+
return TranscriptResult(
|
|
78
|
+
text="[00:00:00] Mock transcript for Openmd skeleton.\n",
|
|
79
|
+
status="verified",
|
|
80
|
+
source_level="third_party_extractor",
|
|
81
|
+
backend="mock",
|
|
82
|
+
source_notes=["local smoke check only"],
|
|
83
|
+
)
|
|
84
|
+
return TranscriptResult(
|
|
85
|
+
text="",
|
|
86
|
+
status="failed",
|
|
87
|
+
source_level="partial_extraction",
|
|
88
|
+
backend=selected_backend,
|
|
89
|
+
source_notes=[
|
|
90
|
+
"adapter skeleton only; real extraction not implemented",
|
|
91
|
+
f"backend_chain: {' -> '.join(chain)}",
|
|
92
|
+
],
|
|
93
|
+
error_code="backend_not_implemented",
|
|
94
|
+
error_summary="Openmd V0 skeleton routed the item but did not run a platform extractor.",
|
|
95
|
+
)
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
"""Bilibili V1 platform adapter."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from openmd.adapters.base import PlatformAdapter
|
|
6
|
+
|
|
7
|
+
from .adapter import BilibiliAdapter
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
def create_adapter(platform_id: str, default_backend: str) -> PlatformAdapter:
|
|
11
|
+
return BilibiliAdapter(platform_id, default_backend)
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
"""V1 adapter for Bilibili video/audio-like package inputs."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import hashlib
|
|
6
|
+
|
|
7
|
+
from openmd.adapters.base import PlatformAdapter
|
|
8
|
+
from openmd.backends.bilibili import extract_bilibili_uid, extract_bvid
|
|
9
|
+
from openmd.core.detector import detect_platform, extract_first_url
|
|
10
|
+
from openmd.core.models import ContentItem, TranscriptResult
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class BilibiliAdapter(PlatformAdapter):
|
|
14
|
+
"""Resolve Bilibili BV and UP/collection selectors without side effects."""
|
|
15
|
+
|
|
16
|
+
def __init__(self, platform_id: str = "bilibili", default_backend: str = "bilibili-harvest.core") -> None:
|
|
17
|
+
self.id = platform_id
|
|
18
|
+
self.default_backend = default_backend
|
|
19
|
+
|
|
20
|
+
def can_handle(self, input_value: str) -> bool:
|
|
21
|
+
return detect_platform(input_value) == "bilibili"
|
|
22
|
+
|
|
23
|
+
def resolve_items(
|
|
24
|
+
self,
|
|
25
|
+
input_value: str,
|
|
26
|
+
*,
|
|
27
|
+
limit: int | None = None,
|
|
28
|
+
all: bool = False,
|
|
29
|
+
) -> list[ContentItem]:
|
|
30
|
+
target = extract_first_url(input_value)
|
|
31
|
+
bvid = extract_bvid(target)
|
|
32
|
+
if bvid is not None:
|
|
33
|
+
return [
|
|
34
|
+
ContentItem(
|
|
35
|
+
platform=self.id,
|
|
36
|
+
input_type="single",
|
|
37
|
+
url=_bilibili_video_url(bvid),
|
|
38
|
+
id=bvid,
|
|
39
|
+
source_type="video",
|
|
40
|
+
raw_meta={
|
|
41
|
+
"profile": "media",
|
|
42
|
+
"permission_scope": "user_accessible",
|
|
43
|
+
"content_id": bvid,
|
|
44
|
+
},
|
|
45
|
+
)
|
|
46
|
+
]
|
|
47
|
+
|
|
48
|
+
uid = extract_bilibili_uid(target)
|
|
49
|
+
return [
|
|
50
|
+
ContentItem(
|
|
51
|
+
platform=self.id,
|
|
52
|
+
input_type="collection",
|
|
53
|
+
url=f"https://space.bilibili.com/{uid}" if uid else target,
|
|
54
|
+
id=f"space-{uid}" if uid else _stable_id(target),
|
|
55
|
+
source_type="video",
|
|
56
|
+
raw_meta={
|
|
57
|
+
"profile": "media",
|
|
58
|
+
"permission_scope": "user_accessible",
|
|
59
|
+
"collection_selector": input_value.strip(),
|
|
60
|
+
"up_uid": uid,
|
|
61
|
+
"discovery_limit": limit,
|
|
62
|
+
"allow_all": all,
|
|
63
|
+
},
|
|
64
|
+
)
|
|
65
|
+
]
|
|
66
|
+
|
|
67
|
+
def extract_text(
|
|
68
|
+
self,
|
|
69
|
+
item: ContentItem,
|
|
70
|
+
*,
|
|
71
|
+
backend: str | None = None,
|
|
72
|
+
backend_chain: tuple[str, ...] | None = None,
|
|
73
|
+
allow_fallback: bool = True,
|
|
74
|
+
) -> TranscriptResult:
|
|
75
|
+
selected = (backend_chain or (backend or self.default_backend,))[0]
|
|
76
|
+
if selected in {"mock", "bilibili.mock"}:
|
|
77
|
+
return TranscriptResult(
|
|
78
|
+
text="[00:00:00] Mock Bilibili transcript for Openmd package smoke.\n",
|
|
79
|
+
status="verified",
|
|
80
|
+
source_level="third_party_extractor",
|
|
81
|
+
backend=selected,
|
|
82
|
+
title=item.title or item.id or "Mock Bilibili item",
|
|
83
|
+
source_notes=["local mock adapter smoke only; no network/browser/OpenCLI/media action was attempted"],
|
|
84
|
+
)
|
|
85
|
+
return TranscriptResult(
|
|
86
|
+
text="",
|
|
87
|
+
status="failed",
|
|
88
|
+
source_level="partial_extraction",
|
|
89
|
+
backend=selected,
|
|
90
|
+
source_notes=[
|
|
91
|
+
"bilibili adapter resolved the item only; real extraction is handled by gated backend providers",
|
|
92
|
+
f"allow_fallback={allow_fallback}",
|
|
93
|
+
],
|
|
94
|
+
error_code="backend_requires_runtime_gate",
|
|
95
|
+
error_summary="Enable the appropriate Openmd backend gate before running real Bilibili extraction.",
|
|
96
|
+
)
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
def _bilibili_video_url(bvid: str) -> str:
|
|
100
|
+
return f"https://www.bilibili.com/video/{bvid}/"
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
def _stable_id(value: str) -> str:
|
|
104
|
+
return hashlib.sha256(value.strip().encode("utf-8")).hexdigest()[:12]
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
"""Bilibili discovery lane helpers.
|
|
2
|
+
|
|
3
|
+
The real providers live in ``openmd.backends.bilibili`` and are registered only
|
|
4
|
+
behind explicit network/browser gates. This module keeps adapter-lane imports
|
|
5
|
+
stable without duplicating provider logic.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
from openmd.backends.bilibili import (
|
|
11
|
+
BILIBILI_NATIVE_DISCOVERY_BACKEND,
|
|
12
|
+
BILIBILI_OPENCLI_DISCOVERY_BACKEND,
|
|
13
|
+
BilibiliNativeUserVideosProvider,
|
|
14
|
+
OpenCliBilibiliUserVideosProvider,
|
|
15
|
+
extract_bilibili_uid,
|
|
16
|
+
)
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def build_opencli_bilibili_user_videos_args(selector: str, *, limit: int | None) -> list[str]:
|
|
20
|
+
uid = extract_bilibili_uid(selector) or selector.strip()
|
|
21
|
+
command = ["opencli", "bilibili", "user-videos", uid]
|
|
22
|
+
if limit is not None:
|
|
23
|
+
command.extend(["--limit", str(max(0, limit))])
|
|
24
|
+
return command
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
__all__ = [
|
|
28
|
+
"BILIBILI_NATIVE_DISCOVERY_BACKEND",
|
|
29
|
+
"BILIBILI_OPENCLI_DISCOVERY_BACKEND",
|
|
30
|
+
"BilibiliNativeUserVideosProvider",
|
|
31
|
+
"OpenCliBilibiliUserVideosProvider",
|
|
32
|
+
"build_opencli_bilibili_user_videos_args",
|
|
33
|
+
]
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
"""Douyin V1 platform adapter."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from openmd.adapters.base import PlatformAdapter
|
|
6
|
+
|
|
7
|
+
from .adapter import DouyinAdapter
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
def create_adapter(platform_id: str, default_backend: str) -> PlatformAdapter:
|
|
11
|
+
return DouyinAdapter(platform_id, default_backend)
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
"""V1 adapter for Douyin author/video package inputs."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import hashlib
|
|
6
|
+
from urllib.parse import parse_qs, urlparse
|
|
7
|
+
|
|
8
|
+
from openmd.adapters.base import PlatformAdapter
|
|
9
|
+
from openmd.backends.douyin import douyin_user_target, normalize_douyin_collection_url
|
|
10
|
+
from openmd.backends.douyin_detail import douyin_video_id
|
|
11
|
+
from openmd.core.detector import detect_platform, extract_first_url
|
|
12
|
+
from openmd.core.models import ContentItem, TranscriptResult
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class DouyinAdapter(PlatformAdapter):
|
|
16
|
+
"""Resolve Douyin single video and author selectors without side effects."""
|
|
17
|
+
|
|
18
|
+
def __init__(self, platform_id: str = "douyin", default_backend: str = "douyin.transcript") -> None:
|
|
19
|
+
self.id = platform_id
|
|
20
|
+
self.default_backend = default_backend
|
|
21
|
+
|
|
22
|
+
def can_handle(self, input_value: str) -> bool:
|
|
23
|
+
return detect_platform(input_value) == "douyin"
|
|
24
|
+
|
|
25
|
+
def resolve_items(
|
|
26
|
+
self,
|
|
27
|
+
input_value: str,
|
|
28
|
+
*,
|
|
29
|
+
limit: int | None = None,
|
|
30
|
+
all: bool = False,
|
|
31
|
+
) -> list[ContentItem]:
|
|
32
|
+
target = extract_first_url(input_value)
|
|
33
|
+
aweme_id = douyin_video_id(target)
|
|
34
|
+
if aweme_id is not None:
|
|
35
|
+
return [
|
|
36
|
+
ContentItem(
|
|
37
|
+
platform=self.id,
|
|
38
|
+
input_type="single",
|
|
39
|
+
url=f"https://www.douyin.com/video/{aweme_id}",
|
|
40
|
+
id=aweme_id,
|
|
41
|
+
source_type="video",
|
|
42
|
+
raw_meta={
|
|
43
|
+
"profile": "media",
|
|
44
|
+
"permission_scope": "user_accessible",
|
|
45
|
+
"content_id": aweme_id,
|
|
46
|
+
},
|
|
47
|
+
)
|
|
48
|
+
]
|
|
49
|
+
|
|
50
|
+
if (urlparse(target).hostname or "").lower() == "v.douyin.com":
|
|
51
|
+
return [ContentItem(platform=self.id, input_type="single", url=target,
|
|
52
|
+
source_type="video", raw_meta={"profile": "media", "identity_unresolved": True})]
|
|
53
|
+
|
|
54
|
+
sec_uid = douyin_user_target(target)
|
|
55
|
+
normalized = normalize_douyin_collection_url(target) or target
|
|
56
|
+
return [
|
|
57
|
+
ContentItem(
|
|
58
|
+
platform=self.id,
|
|
59
|
+
input_type="collection",
|
|
60
|
+
url=normalized,
|
|
61
|
+
id=f"user-{sec_uid}" if sec_uid else _stable_id(target),
|
|
62
|
+
source_type="video",
|
|
63
|
+
raw_meta={
|
|
64
|
+
"profile": "media",
|
|
65
|
+
"permission_scope": "user_accessible",
|
|
66
|
+
"collection_selector": input_value.strip(),
|
|
67
|
+
"sec_uid": sec_uid,
|
|
68
|
+
"discovery_limit": limit,
|
|
69
|
+
"allow_all": all,
|
|
70
|
+
},
|
|
71
|
+
)
|
|
72
|
+
]
|
|
73
|
+
|
|
74
|
+
def extract_text(
|
|
75
|
+
self,
|
|
76
|
+
item: ContentItem,
|
|
77
|
+
*,
|
|
78
|
+
backend: str | None = None,
|
|
79
|
+
backend_chain: tuple[str, ...] | None = None,
|
|
80
|
+
allow_fallback: bool = True,
|
|
81
|
+
) -> TranscriptResult:
|
|
82
|
+
selected = (backend_chain or (backend or self.default_backend,))[0]
|
|
83
|
+
if selected in {"mock", "douyin.mock"}:
|
|
84
|
+
return TranscriptResult(
|
|
85
|
+
text="[00:00:00] Mock Douyin transcript for Openmd package smoke.\n",
|
|
86
|
+
status="verified",
|
|
87
|
+
source_level="platform_transcript",
|
|
88
|
+
backend=selected,
|
|
89
|
+
title=item.title or item.id or "Mock Douyin item",
|
|
90
|
+
source_notes=["local mock adapter smoke only; no sidecar/OpenCLI/browser/account/media/ASR action was attempted"],
|
|
91
|
+
)
|
|
92
|
+
return TranscriptResult(
|
|
93
|
+
text="",
|
|
94
|
+
status="failed",
|
|
95
|
+
source_level="partial_extraction",
|
|
96
|
+
backend=selected,
|
|
97
|
+
source_notes=[
|
|
98
|
+
"douyin adapter resolved the item only; real acquisition is handled by gated discovery/backend providers",
|
|
99
|
+
f"allow_fallback={allow_fallback}",
|
|
100
|
+
],
|
|
101
|
+
error_code="backend_requires_runtime_gate",
|
|
102
|
+
error_summary="Enable the appropriate Openmd backend gate before running real Douyin acquisition.",
|
|
103
|
+
)
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
def _douyin_aweme_id(value: str) -> str | None:
|
|
107
|
+
parsed = urlparse(value if "://" in value else f"https://{value}")
|
|
108
|
+
parts = [part for part in parsed.path.split("/") if part]
|
|
109
|
+
if parts[:1] == ["video"] and len(parts) >= 2 and parts[1].isdigit():
|
|
110
|
+
return parts[1]
|
|
111
|
+
query = parse_qs(parsed.query)
|
|
112
|
+
modal = query.get("modal_id", [""])[0].strip()
|
|
113
|
+
return modal if modal.isdigit() else None
|
|
114
|
+
|
|
115
|
+
|
|
116
|
+
def _stable_id(value: str) -> str:
|
|
117
|
+
return hashlib.sha256(value.strip().encode("utf-8")).hexdigest()[:12]
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
"""Douyin discovery lane helpers.
|
|
2
|
+
|
|
3
|
+
The real bounded OpenCLI discovery provider lives in ``openmd.backends.douyin``
|
|
4
|
+
and is registered only behind the explicit browser backend gate.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
from openmd.backends.douyin import (
|
|
10
|
+
DOUYIN_OPENMD_CREATOR_VIDEOS_BACKEND,
|
|
11
|
+
DOUYIN_OPENCLI_USER_VIDEOS_BACKEND,
|
|
12
|
+
OpenCliOpenmdDouyinCreatorVideosProvider,
|
|
13
|
+
OpenCliDouyinUserVideosProvider,
|
|
14
|
+
build_opencli_openmd_douyin_creator_videos_args,
|
|
15
|
+
build_opencli_douyin_user_videos_args,
|
|
16
|
+
classify_douyin_input_failure,
|
|
17
|
+
douyin_user_target,
|
|
18
|
+
normalize_douyin_collection_url,
|
|
19
|
+
parse_opencli_douyin_user_videos,
|
|
20
|
+
parse_opencli_openmd_douyin_creator_videos,
|
|
21
|
+
)
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
__all__ = [
|
|
25
|
+
"DOUYIN_OPENMD_CREATOR_VIDEOS_BACKEND",
|
|
26
|
+
"DOUYIN_OPENCLI_USER_VIDEOS_BACKEND",
|
|
27
|
+
"OpenCliOpenmdDouyinCreatorVideosProvider",
|
|
28
|
+
"OpenCliDouyinUserVideosProvider",
|
|
29
|
+
"build_opencli_openmd_douyin_creator_videos_args",
|
|
30
|
+
"build_opencli_douyin_user_videos_args",
|
|
31
|
+
"classify_douyin_input_failure",
|
|
32
|
+
"douyin_user_target",
|
|
33
|
+
"normalize_douyin_collection_url",
|
|
34
|
+
"parse_opencli_douyin_user_videos",
|
|
35
|
+
"parse_opencli_openmd_douyin_creator_videos",
|
|
36
|
+
]
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
"""Read-only safety envelope for an optional Douyin local sidecar."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from urllib.parse import urljoin
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
ALLOWED_SIDECAR_ENDPOINTS: frozenset[tuple[str, str]] = frozenset(
|
|
9
|
+
{
|
|
10
|
+
("GET", "/api/v1/monitors"),
|
|
11
|
+
("GET", "/api/v1/contents"),
|
|
12
|
+
("GET", "/api/v1/contents/:id"),
|
|
13
|
+
("GET", "/api/v1/contents/:id/comments"),
|
|
14
|
+
}
|
|
15
|
+
)
|
|
16
|
+
|
|
17
|
+
FORBIDDEN_SIDECAR_PATH_PREFIXES: tuple[str, ...] = (
|
|
18
|
+
"/auth/",
|
|
19
|
+
"/api/v1/accounts",
|
|
20
|
+
"/api/v1/admin",
|
|
21
|
+
"/api/v1/creator",
|
|
22
|
+
)
|
|
23
|
+
|
|
24
|
+
FORBIDDEN_SIDECAR_PATH_PARTS: tuple[str, ...] = (
|
|
25
|
+
"/upload",
|
|
26
|
+
"/cancel-upload",
|
|
27
|
+
"/retry-download",
|
|
28
|
+
"/publish",
|
|
29
|
+
"/draft",
|
|
30
|
+
"/delete",
|
|
31
|
+
"/reply",
|
|
32
|
+
"/like",
|
|
33
|
+
"/approve",
|
|
34
|
+
)
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def is_allowed_sidecar_endpoint(method: str, path: str) -> bool:
|
|
38
|
+
normalized_method = method.upper().strip()
|
|
39
|
+
normalized_path = _normalize_path(path)
|
|
40
|
+
if normalized_method != "GET":
|
|
41
|
+
return False
|
|
42
|
+
if any(normalized_path.startswith(prefix) for prefix in FORBIDDEN_SIDECAR_PATH_PREFIXES):
|
|
43
|
+
return False
|
|
44
|
+
if any(part in normalized_path for part in FORBIDDEN_SIDECAR_PATH_PARTS):
|
|
45
|
+
return False
|
|
46
|
+
if (normalized_method, normalized_path) in ALLOWED_SIDECAR_ENDPOINTS:
|
|
47
|
+
return True
|
|
48
|
+
return _is_content_detail_path(normalized_method, normalized_path) or _is_content_comments_path(
|
|
49
|
+
normalized_method, normalized_path
|
|
50
|
+
)
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
def build_sidecar_url(base_url: str, path: str) -> str:
|
|
54
|
+
normalized_base = base_url.rstrip("/") + "/"
|
|
55
|
+
normalized_path = _normalize_path(path).lstrip("/")
|
|
56
|
+
return urljoin(normalized_base, normalized_path)
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
def _is_content_detail_path(method: str, path: str) -> bool:
|
|
60
|
+
if method != "GET":
|
|
61
|
+
return False
|
|
62
|
+
parts = [part for part in path.split("/") if part]
|
|
63
|
+
return len(parts) == 4 and parts[:3] == ["api", "v1", "contents"] and parts[3].isdigit()
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
def _is_content_comments_path(method: str, path: str) -> bool:
|
|
67
|
+
if method != "GET":
|
|
68
|
+
return False
|
|
69
|
+
parts = [part for part in path.split("/") if part]
|
|
70
|
+
return len(parts) == 5 and parts[:3] == ["api", "v1", "contents"] and parts[3].isdigit() and parts[4] == "comments"
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
def _normalize_path(path: str) -> str:
|
|
74
|
+
value = "/" + path.strip().split("?", 1)[0].lstrip("/")
|
|
75
|
+
while "//" in value:
|
|
76
|
+
value = value.replace("//", "/")
|
|
77
|
+
return value
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
"""Generic media adapter factory.
|
|
2
|
+
|
|
3
|
+
Local file media is currently routed through the existing ``generic_media``
|
|
4
|
+
platform. This module lets the registry discover a concrete local-audio
|
|
5
|
+
adapter without editing the shared registry.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
from openmd.adapters.local_audio import LocalAudioAdapter
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def create_adapter(platform_id: str, default_backend: str) -> LocalAudioAdapter:
|
|
14
|
+
return LocalAudioAdapter(platform_id, default_backend)
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
"""Shared high-privacy export/cache status helpers.
|
|
2
|
+
|
|
3
|
+
High-privacy social/chat lanes must report operator-actionable states without
|
|
4
|
+
claiming ownership of platform sessions, browser profiles, databases, cookies,
|
|
5
|
+
or decrypted local stores.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
from typing import Any
|
|
11
|
+
|
|
12
|
+
from openmd.core.models import TranscriptResult
|
|
13
|
+
from openmd.core.secrets import sanitize_error_summary
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
HIGH_PRIVACY_STATUS_KEY = "high_privacy_status"
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def high_privacy_status_metadata(
|
|
20
|
+
*,
|
|
21
|
+
platform: str,
|
|
22
|
+
target_kind: str,
|
|
23
|
+
state: str,
|
|
24
|
+
next_action: str,
|
|
25
|
+
permission_scope: str | None = None,
|
|
26
|
+
) -> dict[str, Any]:
|
|
27
|
+
return {
|
|
28
|
+
"platform": platform,
|
|
29
|
+
"target_kind": target_kind,
|
|
30
|
+
"state": state,
|
|
31
|
+
"permission_scope": permission_scope or "unknown",
|
|
32
|
+
"next_action": sanitize_error_summary(next_action),
|
|
33
|
+
"core_owns_session": False,
|
|
34
|
+
"reads_private_database": False,
|
|
35
|
+
"decrypts_database": False,
|
|
36
|
+
"exports_cookies": False,
|
|
37
|
+
"persists_private_cache": False,
|
|
38
|
+
"requires_explicit_scope": True,
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def high_privacy_blocker_result(
|
|
43
|
+
*,
|
|
44
|
+
platform: str,
|
|
45
|
+
backend: str,
|
|
46
|
+
target_kind: str,
|
|
47
|
+
state: str,
|
|
48
|
+
error_code: str,
|
|
49
|
+
error_summary: str,
|
|
50
|
+
next_action: str,
|
|
51
|
+
permission_scope: str | None = None,
|
|
52
|
+
title: str | None = None,
|
|
53
|
+
source_notes: list[str] | None = None,
|
|
54
|
+
) -> TranscriptResult:
|
|
55
|
+
return TranscriptResult(
|
|
56
|
+
text="",
|
|
57
|
+
status="failed",
|
|
58
|
+
source_level="partial_extraction",
|
|
59
|
+
backend=backend,
|
|
60
|
+
title=title,
|
|
61
|
+
source_notes=[
|
|
62
|
+
*(source_notes or []),
|
|
63
|
+
"Openmd core did not read private databases, decrypt stores, export cookies, or own platform sessions.",
|
|
64
|
+
],
|
|
65
|
+
error_code=error_code,
|
|
66
|
+
error_summary=sanitize_error_summary(error_summary),
|
|
67
|
+
metadata={
|
|
68
|
+
"failure_class": "user_resolvable",
|
|
69
|
+
HIGH_PRIVACY_STATUS_KEY: high_privacy_status_metadata(
|
|
70
|
+
platform=platform,
|
|
71
|
+
target_kind=target_kind,
|
|
72
|
+
state=state,
|
|
73
|
+
next_action=next_action,
|
|
74
|
+
permission_scope=permission_scope,
|
|
75
|
+
),
|
|
76
|
+
},
|
|
77
|
+
)
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
"""Local audio adapter lane.
|
|
2
|
+
|
|
3
|
+
The detector currently routes media-like inputs to ``generic_media``. The
|
|
4
|
+
``generic_media`` adapter module reuses this local-audio adapter for local files
|
|
5
|
+
without requiring platform lanes to edit the shared registry.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
from .adapter import LocalAudioAdapter
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def create_adapter(platform_id: str, default_backend: str) -> LocalAudioAdapter:
|
|
14
|
+
return LocalAudioAdapter(platform_id, default_backend)
|