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,124 @@
|
|
|
1
|
+
"""Bounded, transient Douyin single-work detail acquisition."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import re
|
|
6
|
+
from dataclasses import replace
|
|
7
|
+
from datetime import datetime, timezone
|
|
8
|
+
from urllib.parse import parse_qs, urlparse
|
|
9
|
+
|
|
10
|
+
from openmd.backends.youtube_discovery import _run_opencli
|
|
11
|
+
from openmd.core.detector import extract_first_url
|
|
12
|
+
from openmd.core.models import ContentItem
|
|
13
|
+
from openmd.core.opencli_output import load_opencli_json
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class DouyinDetailError(ValueError):
|
|
17
|
+
def __init__(self, code: str, summary: str):
|
|
18
|
+
super().__init__(summary)
|
|
19
|
+
self.code = code
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def douyin_author(payload: dict) -> str | None:
|
|
23
|
+
"""Read nickname-shaped fields without stringifying API objects or IDs."""
|
|
24
|
+
fields = {str(key).lower().replace("_", ""): value for key, value in payload.items()}
|
|
25
|
+
for key in ("author", "nickname", "authorname", "user", "username", "userinfo"):
|
|
26
|
+
value = fields.get(key)
|
|
27
|
+
if isinstance(value, dict):
|
|
28
|
+
value = douyin_author(value) or value.get("name")
|
|
29
|
+
if isinstance(value, str) and value.strip():
|
|
30
|
+
return value.strip()
|
|
31
|
+
return None
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def douyin_published_at(payload: dict) -> str | None:
|
|
35
|
+
"""Keep source calendar times; normalize Unix seconds/milliseconds in UTC."""
|
|
36
|
+
fields = {str(key).lower().replace("_", ""): value for key, value in payload.items()}
|
|
37
|
+
for key in ("publishedat", "createtime", "created", "createdat", "publishtime", "date"):
|
|
38
|
+
value = fields.get(key)
|
|
39
|
+
if not isinstance(value, (str, int, float)) or isinstance(value, bool):
|
|
40
|
+
continue
|
|
41
|
+
text = str(value).strip()
|
|
42
|
+
if not text:
|
|
43
|
+
continue
|
|
44
|
+
try:
|
|
45
|
+
if re.fullmatch(r"\d{8}", text):
|
|
46
|
+
return datetime.strptime(text, "%Y%m%d").date().isoformat()
|
|
47
|
+
if re.fullmatch(r"\d{10}(?:\.\d+)?|\d{13}(?:\.\d+)?", text):
|
|
48
|
+
seconds = float(text) / (1000 if len(text.split(".")[0]) == 13 else 1)
|
|
49
|
+
return datetime.fromtimestamp(seconds, tz=timezone.utc).isoformat()
|
|
50
|
+
datetime.fromisoformat(text.replace("Z", "+00:00"))
|
|
51
|
+
return text
|
|
52
|
+
except (ValueError, OverflowError, OSError):
|
|
53
|
+
continue
|
|
54
|
+
return None
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
def douyin_video_id(value: str) -> str | None:
|
|
58
|
+
target = extract_first_url(value)
|
|
59
|
+
if re.fullmatch(r"\d{6,20}", target):
|
|
60
|
+
return target
|
|
61
|
+
parsed = urlparse(target if "://" in target else "https://" + target)
|
|
62
|
+
host = (parsed.hostname or "").lower()
|
|
63
|
+
if host != "douyin.com" and not host.endswith(".douyin.com") and host != "iesdouyin.com" and not host.endswith(".iesdouyin.com"):
|
|
64
|
+
return None
|
|
65
|
+
for key in ("modal_id", "aweme_id", "item_id"):
|
|
66
|
+
candidate = parse_qs(parsed.query).get(key, [""])[0]
|
|
67
|
+
if re.fullmatch(r"\d{6,20}", candidate):
|
|
68
|
+
return candidate
|
|
69
|
+
match = re.search(r"/(?:video|note)/(\d{6,20})(?:/|$)", parsed.path)
|
|
70
|
+
return match.group(1) if match else None
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
class DouyinDetailProvider:
|
|
74
|
+
name = "opencli.douyin.detail"
|
|
75
|
+
|
|
76
|
+
def __init__(self, *, runner=None, timeout: int = 90):
|
|
77
|
+
self.runner = runner or _run_opencli
|
|
78
|
+
self.timeout = timeout
|
|
79
|
+
|
|
80
|
+
def resolve(self, item: ContentItem) -> ContentItem:
|
|
81
|
+
target = extract_first_url(item.url)
|
|
82
|
+
expected = douyin_video_id(target) or (item.id if item.id and item.id.isdigit() else None)
|
|
83
|
+
parsed = urlparse(target)
|
|
84
|
+
if not expected and (parsed.hostname or "").lower() not in {"v.douyin.com", "www.iesdouyin.com"}:
|
|
85
|
+
raise DouyinDetailError("douyin_invalid_video_target", "A video ID, video URL, modal URL, or video share link is required.")
|
|
86
|
+
command = ["opencli", "douyin", "detail", expected or target, "--window", "background", "--keep-tab", "false", "-f", "json"]
|
|
87
|
+
try:
|
|
88
|
+
code, stdout, _stderr = self.runner(command, timeout=self.timeout)
|
|
89
|
+
except Exception as exc:
|
|
90
|
+
raise DouyinDetailError("douyin_detail_read_failed", "The bounded video detail command could not complete.") from exc
|
|
91
|
+
if code:
|
|
92
|
+
raise DouyinDetailError("douyin_detail_read_failed", "The bounded video detail command failed; check the browser session or access permissions.")
|
|
93
|
+
try:
|
|
94
|
+
payload = load_opencli_json(stdout)
|
|
95
|
+
if isinstance(payload, list) and len(payload) == 1:
|
|
96
|
+
payload = payload[0]
|
|
97
|
+
if not isinstance(payload, dict) or payload.get("ok") is False:
|
|
98
|
+
raise ValueError("invalid detail")
|
|
99
|
+
video_id = str(payload.get("aweme_id") or payload.get("id") or "")
|
|
100
|
+
if not re.fullmatch(r"\d{6,20}", video_id):
|
|
101
|
+
raise ValueError("missing identity")
|
|
102
|
+
except (ValueError, TypeError) as exc:
|
|
103
|
+
raise DouyinDetailError("douyin_detail_invalid", "Video detail returned no valid single-work identity.") from exc
|
|
104
|
+
if expected and video_id != expected:
|
|
105
|
+
raise DouyinDetailError("douyin_detail_id_mismatch", "Video detail did not match the requested work ID.")
|
|
106
|
+
images = payload.get("images")
|
|
107
|
+
is_image = bool(images) or payload.get("media_kind") == "image_text" or payload.get("aweme_type") in {2, 68, 150}
|
|
108
|
+
raw_meta = {**item.raw_meta, "detail_resolved": True, "identity_unresolved": False, "content_id": video_id}
|
|
109
|
+
raw_meta.update({"duration": payload.get("duration_s", payload.get("duration", 0)), "media_kind": "image_text" if is_image else "video"})
|
|
110
|
+
if is_image:
|
|
111
|
+
# Image-post descriptions are author text, distinct from video
|
|
112
|
+
# captions. Keep an empty value too: a missing description must not
|
|
113
|
+
# fall back to an ID or a separately supplied display title.
|
|
114
|
+
raw_meta["author_text"] = payload.get("desc") if isinstance(payload.get("desc"), str) else ""
|
|
115
|
+
raw_meta["author_text_source"] = "detail_desc"
|
|
116
|
+
for key in ("play_url", "transcript_candidates", "subtitles", "caption_info", "captionInfo"):
|
|
117
|
+
if payload.get(key):
|
|
118
|
+
raw_meta[key] = payload[key]
|
|
119
|
+
# A video's description remains title metadata, never a transcript.
|
|
120
|
+
return replace(item, url=f"https://www.douyin.com/video/{video_id}", id=video_id,
|
|
121
|
+
title=item.title if item.title and item.title != video_id else str(payload.get("desc") or payload.get("title") or video_id),
|
|
122
|
+
author=item.author or douyin_author(payload),
|
|
123
|
+
published_at=item.published_at or douyin_published_at(payload),
|
|
124
|
+
source_type="image_text" if is_image else "video", raw_meta=raw_meta)
|