orbitkit 0.8.78__tar.gz → 0.9.10__tar.gz
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.
- {orbitkit-0.8.78/orbitkit.egg-info → orbitkit-0.9.10}/PKG-INFO +1 -1
- orbitkit-0.9.10/orbitkit/VERSION +1 -0
- {orbitkit-0.8.78 → orbitkit-0.9.10}/orbitkit/airflow_handler/data_preprocessing.py +1 -0
- {orbitkit-0.8.78 → orbitkit-0.9.10}/orbitkit/airflow_handler/file_flow_entry_process.py +15 -0
- orbitkit-0.9.10/orbitkit/cdn_uploader/__init__.py +16 -0
- orbitkit-0.9.10/orbitkit/cdn_uploader/uploader.py +151 -0
- orbitkit-0.9.10/orbitkit/pdf_transformer/__init__.py +16 -0
- orbitkit-0.9.10/orbitkit/pdf_transformer/mineru/__init__.py +41 -0
- orbitkit-0.9.10/orbitkit/pdf_transformer/mineru/bbox_rotation.py +218 -0
- orbitkit-0.9.10/orbitkit/pdf_transformer/mineru/result_transformer.py +687 -0
- orbitkit-0.9.10/orbitkit/pdf_validator/__init__.py +35 -0
- orbitkit-0.9.10/orbitkit/pdf_validator/filters/__init__.py +1 -0
- orbitkit-0.9.10/orbitkit/pdf_validator/filters/drop_non_text.py +19 -0
- orbitkit-0.9.10/orbitkit/pdf_validator/filters/drop_vlm_artifacts.py +69 -0
- orbitkit-0.9.10/orbitkit/pdf_validator/filters/repeated_text.py +128 -0
- orbitkit-0.9.10/orbitkit/pdf_validator/pipeline.py +103 -0
- orbitkit-0.9.10/orbitkit/pdf_validator/registry.py +64 -0
- {orbitkit-0.8.78 → orbitkit-0.9.10/orbitkit.egg-info}/PKG-INFO +1 -1
- {orbitkit-0.8.78 → orbitkit-0.9.10}/orbitkit.egg-info/SOURCES.txt +13 -0
- orbitkit-0.8.78/orbitkit/VERSION +0 -1
- {orbitkit-0.8.78 → orbitkit-0.9.10}/LICENSE +0 -0
- {orbitkit-0.8.78 → orbitkit-0.9.10}/MANIFEST.in +0 -0
- {orbitkit-0.8.78 → orbitkit-0.9.10}/README.md +0 -0
- {orbitkit-0.8.78 → orbitkit-0.9.10}/orbitkit/__init__.py +0 -0
- {orbitkit-0.8.78 → orbitkit-0.9.10}/orbitkit/airflow_handler/__init__.py +0 -0
- {orbitkit-0.8.78 → orbitkit-0.9.10}/orbitkit/airflow_handler/file_flow_exit_process.py +0 -0
- {orbitkit-0.8.78 → orbitkit-0.9.10}/orbitkit/airflow_handler/file_handler.py +0 -0
- {orbitkit-0.8.78 → orbitkit-0.9.10}/orbitkit/airflow_handler/file_handler_v2.py +0 -0
- {orbitkit-0.8.78 → orbitkit-0.9.10}/orbitkit/audio_transcoder/__init__.py +0 -0
- {orbitkit-0.8.78 → orbitkit-0.9.10}/orbitkit/audio_transcoder/netmind_extract_v1.py +0 -0
- {orbitkit-0.8.78 → orbitkit-0.9.10}/orbitkit/constant/__init__.py +0 -0
- {orbitkit-0.8.78 → orbitkit-0.9.10}/orbitkit/constant/report_schema.py +0 -0
- {orbitkit-0.8.78 → orbitkit-0.9.10}/orbitkit/id_srv/__init__.py +0 -0
- {orbitkit-0.8.78 → orbitkit-0.9.10}/orbitkit/id_srv/id_gen.py +0 -0
- {orbitkit-0.8.78 → orbitkit-0.9.10}/orbitkit/id_srv/id_perm_like.py +0 -0
- {orbitkit-0.8.78 → orbitkit-0.9.10}/orbitkit/lark_send/__init__.py +0 -0
- {orbitkit-0.8.78 → orbitkit-0.9.10}/orbitkit/lark_send/lark.py +0 -0
- {orbitkit-0.8.78 → orbitkit-0.9.10}/orbitkit/llm_tools/__init__.py +0 -0
- {orbitkit-0.8.78 → orbitkit-0.9.10}/orbitkit/llm_tools/quick_rag_chat.py +0 -0
- {orbitkit-0.8.78 → orbitkit-0.9.10}/orbitkit/orbit_type/__init__.py +0 -0
- {orbitkit-0.8.78 → orbitkit-0.9.10}/orbitkit/orbit_type/doc_4_compile_rule.py +0 -0
- {orbitkit-0.8.78 → orbitkit-0.9.10}/orbitkit/orbit_type/orbit_type_simple.py +0 -0
- {orbitkit-0.8.78 → orbitkit-0.9.10}/orbitkit/orbit_type/tools.py +0 -0
- {orbitkit-0.8.78 → orbitkit-0.9.10}/orbitkit/pdf_embedding/__init__.py +0 -0
- {orbitkit-0.8.78 → orbitkit-0.9.10}/orbitkit/pdf_embedding/pdf_txt_embedding.py +0 -0
- {orbitkit-0.8.78 → orbitkit-0.9.10}/orbitkit/pdf_embedding/pdf_txt_embedding_v2.py +0 -0
- {orbitkit-0.8.78 → orbitkit-0.9.10}/orbitkit/pdf_extractor/__init__.py +0 -0
- {orbitkit-0.8.78 → orbitkit-0.9.10}/orbitkit/pdf_extractor/a_stock_extractor_v1.py +0 -0
- {orbitkit-0.8.78 → orbitkit-0.9.10}/orbitkit/pdf_extractor/exceptions.py +0 -0
- {orbitkit-0.8.78 → orbitkit-0.9.10}/orbitkit/pdf_extractor/mineru_demo.py +0 -0
- {orbitkit-0.8.78 → orbitkit-0.9.10}/orbitkit/pdf_extractor/pdf_block_extractor_base.py +0 -0
- {orbitkit-0.8.78 → orbitkit-0.9.10}/orbitkit/pdf_extractor/pdf_block_extractor_v1.py +0 -0
- {orbitkit-0.8.78 → orbitkit-0.9.10}/orbitkit/pdf_extractor/pdf_block_extractor_v2.py +0 -0
- {orbitkit-0.8.78 → orbitkit-0.9.10}/orbitkit/pdf_extractor/pdf_extractor_azure.py +0 -0
- {orbitkit-0.8.78 → orbitkit-0.9.10}/orbitkit/pdf_extractor/pdf_extractor_minerU_v1.py +0 -0
- {orbitkit-0.8.78 → orbitkit-0.9.10}/orbitkit/pdf_extractor/pdf_extractor_netmind_v1.py +0 -0
- {orbitkit-0.8.78 → orbitkit-0.9.10}/orbitkit/pdf_extractor/pdf_extractor_netmind_v2.py +0 -0
- {orbitkit-0.8.78 → orbitkit-0.9.10}/orbitkit/pdf_extractor/pdf_extractor_netmind_v3.py +0 -0
- {orbitkit-0.8.78 → orbitkit-0.9.10}/orbitkit/pdf_extractor/pdf_extractor_orbit.py +0 -0
- {orbitkit-0.8.78 → orbitkit-0.9.10}/orbitkit/pdf_extractor_simple/__init__.py +0 -0
- {orbitkit-0.8.78 → orbitkit-0.9.10}/orbitkit/pdf_extractor_simple/base.py +0 -0
- {orbitkit-0.8.78 → orbitkit-0.9.10}/orbitkit/pdf_extractor_simple/cloud_provider.py +0 -0
- {orbitkit-0.8.78 → orbitkit-0.9.10}/orbitkit/pdf_extractor_simple/core.py +0 -0
- {orbitkit-0.8.78 → orbitkit-0.9.10}/orbitkit/pdf_extractor_simple/exceptions.py +0 -0
- {orbitkit-0.8.78 → orbitkit-0.9.10}/orbitkit/pdf_extractor_simple/extractors.py +0 -0
- {orbitkit-0.8.78 → orbitkit-0.9.10}/orbitkit/pdf_extractor_simple/utils.py +0 -0
- {orbitkit-0.8.78 → orbitkit-0.9.10}/orbitkit/pdf_writer/__init__.py +0 -0
- {orbitkit-0.8.78 → orbitkit-0.9.10}/orbitkit/pdf_writer/pdf_writer_simple.py +0 -0
- {orbitkit-0.8.78 → orbitkit-0.9.10}/orbitkit/util/__init__.py +0 -0
- {orbitkit-0.8.78 → orbitkit-0.9.10}/orbitkit/util/cache_asset_downloader.py +0 -0
- {orbitkit-0.8.78 → orbitkit-0.9.10}/orbitkit/util/common.py +0 -0
- {orbitkit-0.8.78 → orbitkit-0.9.10}/orbitkit/util/customize_regix_manager.py +0 -0
- {orbitkit-0.8.78 → orbitkit-0.9.10}/orbitkit/util/is_xbrl_structure.py +0 -0
- {orbitkit-0.8.78 → orbitkit-0.9.10}/orbitkit/util/secret_manager.py +0 -0
- {orbitkit-0.8.78 → orbitkit-0.9.10}/orbitkit/util/universal_extractor.py +0 -0
- {orbitkit-0.8.78 → orbitkit-0.9.10}/orbitkit/util/util_aliyun.py +0 -0
- {orbitkit-0.8.78 → orbitkit-0.9.10}/orbitkit/util/util_aliyun_oss_simple.py +0 -0
- {orbitkit-0.8.78 → orbitkit-0.9.10}/orbitkit/util/util_aws.py +0 -0
- {orbitkit-0.8.78 → orbitkit-0.9.10}/orbitkit/util/util_aws_s3_wrapper.py +0 -0
- {orbitkit-0.8.78 → orbitkit-0.9.10}/orbitkit/util/util_date.py +0 -0
- {orbitkit-0.8.78 → orbitkit-0.9.10}/orbitkit/util/util_html.py +0 -0
- {orbitkit-0.8.78 → orbitkit-0.9.10}/orbitkit/util/util_kafka.py +0 -0
- {orbitkit-0.8.78 → orbitkit-0.9.10}/orbitkit/util/util_md5.py +0 -0
- {orbitkit-0.8.78 → orbitkit-0.9.10}/orbitkit/util/util_selenium.py +0 -0
- {orbitkit-0.8.78 → orbitkit-0.9.10}/orbitkit/util/util_simple_timer.py +0 -0
- {orbitkit-0.8.78 → orbitkit-0.9.10}/orbitkit/util/util_str.py +0 -0
- {orbitkit-0.8.78 → orbitkit-0.9.10}/orbitkit/util/util_type_mapping.py +0 -0
- {orbitkit-0.8.78 → orbitkit-0.9.10}/orbitkit/util/util_url.py +0 -0
- {orbitkit-0.8.78 → orbitkit-0.9.10}/orbitkit.egg-info/dependency_links.txt +0 -0
- {orbitkit-0.8.78 → orbitkit-0.9.10}/orbitkit.egg-info/not-zip-safe +0 -0
- {orbitkit-0.8.78 → orbitkit-0.9.10}/orbitkit.egg-info/requires.txt +0 -0
- {orbitkit-0.8.78 → orbitkit-0.9.10}/orbitkit.egg-info/top_level.txt +0 -0
- {orbitkit-0.8.78 → orbitkit-0.9.10}/setup.cfg +0 -0
- {orbitkit-0.8.78 → orbitkit-0.9.10}/setup.py +0 -0
|
@@ -0,0 +1 @@
|
|
|
1
|
+
0.9.10
|
|
@@ -160,6 +160,7 @@ class DocumentProcessor:
|
|
|
160
160
|
"title": title,
|
|
161
161
|
"published": doc['x_reported_at_utc_date'],
|
|
162
162
|
"tickers": [],
|
|
163
|
+
"tickers_map_arr": [],
|
|
163
164
|
"perm_id_list": doc['x_orbit_data']['perm_id_list'],
|
|
164
165
|
"report_type_id_list_str": doc['x_orbit_data']['report_type_id_list']
|
|
165
166
|
}
|
|
@@ -29,6 +29,7 @@ class FilingOfficialProcessor:
|
|
|
29
29
|
|
|
30
30
|
self.mongo_client = pymongo.MongoClient(mongo_uri)
|
|
31
31
|
self.data_xbrl_convert_collection = self.mongo_client['filing_reports']['data_xbrl_convert']
|
|
32
|
+
self.sm_orbit_entity_id_cache_collection = self.mongo_client['stat_cache']['sm_orbit_entity_id_cache']
|
|
32
33
|
|
|
33
34
|
postgres_uri = os.environ.get('PG_URI_AIRFLOW12_USER_NEWSFEEDSITE') if not postgres_uri else postgres_uri
|
|
34
35
|
if not postgres_uri:
|
|
@@ -139,13 +140,27 @@ class FilingOfficialProcessor:
|
|
|
139
140
|
|
|
140
141
|
if row.ticker is not None:
|
|
141
142
|
orbit_entity_id_ticker_map[row.orbit_entity_id].append(row.ticker)
|
|
143
|
+
# 批量查询 organization_name / primary_ticker(走查 doc 的同一个 mongo 连接)
|
|
144
|
+
orbit_entity_id_org_map = {}
|
|
145
|
+
cache_cursor = self.sm_orbit_entity_id_cache_collection.find(
|
|
146
|
+
{'orbit_entity_id': {'$in': perm_id_list}},
|
|
147
|
+
{'orbit_entity_id': 1, 'organization_name': 1, 'primary_ticker': 1}
|
|
148
|
+
).batch_size(self.max_batch_size)
|
|
149
|
+
for row in cache_cursor:
|
|
150
|
+
orbit_entity_id_org_map[row['orbit_entity_id']] = (row.get('organization_name'), row.get('primary_ticker'))
|
|
151
|
+
|
|
142
152
|
for step_info, records in file_flow_info.items():
|
|
143
153
|
for record in records:
|
|
144
154
|
if 'extends' in record and record.get('extends') is not None:
|
|
145
155
|
tickers = set()
|
|
156
|
+
tickers_map_arr = []
|
|
146
157
|
for i in record['extends']['perm_id_list']:
|
|
147
158
|
tickers.update(orbit_entity_id_ticker_map.get(i, []))
|
|
159
|
+
org_info = orbit_entity_id_org_map.get(i)
|
|
160
|
+
if org_info and org_info[0] is not None:
|
|
161
|
+
tickers_map_arr.append({org_info[0]: org_info[1]})
|
|
148
162
|
record['extends']['tickers'] = list(tickers)
|
|
163
|
+
record['extends']['tickers_map_arr'] = tickers_map_arr
|
|
149
164
|
|
|
150
165
|
record['extends']['report_type_id_list_str'] = [self.report_type_id_name_map.get(i) for i in record['extends']['report_type_id_list_str']]
|
|
151
166
|
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
"""cdn_uploader:经自建 CDN 网关做对象下载 / 上传(基于 requests,不用 boto3)。
|
|
2
|
+
|
|
3
|
+
用法::
|
|
4
|
+
|
|
5
|
+
from orbitkit.cdn_uploader import CdnUploader
|
|
6
|
+
|
|
7
|
+
with CdnUploader("https://oassets.orbitfin.ai", upload_key=KEY) as cdn:
|
|
8
|
+
cdn.upload_files("filing-reports", "txt-vector/<key>", result.to_files())
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
from .uploader import DEFAULT_CONTENT_TYPE, CdnUploader
|
|
12
|
+
|
|
13
|
+
__all__ = [
|
|
14
|
+
"CdnUploader",
|
|
15
|
+
"DEFAULT_CONTENT_TYPE",
|
|
16
|
+
]
|
|
@@ -0,0 +1,151 @@
|
|
|
1
|
+
"""自建 CDN 网关客户端(省 S3 出站费),基于 ``requests``,不依赖 boto3。
|
|
2
|
+
|
|
3
|
+
设了 CDN 网关(形如 ``https://oassets.orbitfin.ai``,路径 ``/{bucket}/{key}``)
|
|
4
|
+
后,把对象的下载 / 上传改走网关而不是直连 S3。收益:S3→CDN 回源免费 +
|
|
5
|
+
CDN 出站费率低 / 有免费额度 → 省 egress。
|
|
6
|
+
|
|
7
|
+
用**独立的 requests.Session(trust_env=False,不读全局环境代理)**,是否走代理
|
|
8
|
+
由 ``proxies`` 显式控制:
|
|
9
|
+
- 传了 ``proxies`` → CDN 请求经该代理出境(例如 xproxy 开启时跟着走 xproxy);
|
|
10
|
+
- 不传 → 直连网关域名。
|
|
11
|
+
|
|
12
|
+
接口(与生产网关约定一致):
|
|
13
|
+
下载 GET {base}/{bucket}/{key}
|
|
14
|
+
- 带 ``x-upload-key`` 头(网关不需要会忽略)
|
|
15
|
+
- 成功 2xx,body = 原始字节
|
|
16
|
+
上传 PUT {base}/{bucket}/{key}
|
|
17
|
+
- ``x-upload-key``: 鉴权 key(**绝不硬编码**,由调用方传入)
|
|
18
|
+
- ``x-amz-content-sha256``: body 的 sha256 十六进制小写
|
|
19
|
+
- ``Content-Type``
|
|
20
|
+
- body = 文件内容
|
|
21
|
+
- 成功 2xx
|
|
22
|
+
"""
|
|
23
|
+
|
|
24
|
+
from __future__ import annotations
|
|
25
|
+
|
|
26
|
+
import hashlib
|
|
27
|
+
import logging
|
|
28
|
+
from typing import Dict, Mapping, Optional
|
|
29
|
+
from urllib.parse import quote
|
|
30
|
+
|
|
31
|
+
import requests
|
|
32
|
+
|
|
33
|
+
log = logging.getLogger(__name__)
|
|
34
|
+
|
|
35
|
+
DEFAULT_CONTENT_TYPE = "application/octet-stream"
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def _proxies_arg(proxies: Optional[Mapping[str, str]], proxy_url: Optional[str]):
|
|
39
|
+
if proxies:
|
|
40
|
+
return dict(proxies)
|
|
41
|
+
if proxy_url:
|
|
42
|
+
return {"http": proxy_url, "https": proxy_url}
|
|
43
|
+
return None
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
class CdnUploader:
|
|
47
|
+
"""经自建 CDN 网关做对象的下载 / 上传(同步)。
|
|
48
|
+
|
|
49
|
+
Args:
|
|
50
|
+
base_url: 网关根地址,如 ``https://oassets.orbitfin.ai``。
|
|
51
|
+
upload_key: 网关鉴权 key(``x-upload-key``);下载时也一并带上(网关
|
|
52
|
+
不需要会忽略)。**绝不硬编码**,由调用方从环境 / secret 传入。
|
|
53
|
+
timeout_seconds: 单次请求超时。
|
|
54
|
+
proxy_url: 简写:出境代理 URL(同时用于 http/https)。
|
|
55
|
+
proxies: requests 风格的 ``{"http": ..., "https": ...}``;给了则优先于
|
|
56
|
+
``proxy_url``。两者都不给 = 直连。
|
|
57
|
+
"""
|
|
58
|
+
|
|
59
|
+
def __init__(
|
|
60
|
+
self,
|
|
61
|
+
base_url: str,
|
|
62
|
+
upload_key: Optional[str] = None,
|
|
63
|
+
*,
|
|
64
|
+
timeout_seconds: int = 300,
|
|
65
|
+
proxy_url: Optional[str] = None,
|
|
66
|
+
proxies: Optional[Mapping[str, str]] = None,
|
|
67
|
+
) -> None:
|
|
68
|
+
if not base_url:
|
|
69
|
+
raise ValueError("CdnUploader requires a non-empty base_url")
|
|
70
|
+
self._base = base_url.rstrip("/")
|
|
71
|
+
self._upload_key = upload_key
|
|
72
|
+
self._timeout = float(timeout_seconds)
|
|
73
|
+
self._proxies = _proxies_arg(proxies, proxy_url)
|
|
74
|
+
self._session = requests.Session()
|
|
75
|
+
# trust_env=False:不读全局 HTTP_PROXY/HTTPS_PROXY / netrc;代理只在
|
|
76
|
+
# 显式给了 proxies/proxy_url 时对这个 session 生效。
|
|
77
|
+
self._session.trust_env = False
|
|
78
|
+
|
|
79
|
+
def _url(self, bucket: str, key: str) -> str:
|
|
80
|
+
# key 里的 '/' 是路径分隔符要保留;其它特殊字符做 URL 编码。
|
|
81
|
+
safe_key = quote(key.lstrip("/"), safe="/")
|
|
82
|
+
return f"{self._base}/{bucket}/{safe_key}"
|
|
83
|
+
|
|
84
|
+
def download(self, bucket: str, key: str) -> bytes:
|
|
85
|
+
headers: Dict[str, str] = {}
|
|
86
|
+
if self._upload_key:
|
|
87
|
+
headers["x-upload-key"] = self._upload_key
|
|
88
|
+
resp = self._session.get(
|
|
89
|
+
self._url(bucket, key),
|
|
90
|
+
headers=headers,
|
|
91
|
+
proxies=self._proxies,
|
|
92
|
+
timeout=self._timeout,
|
|
93
|
+
)
|
|
94
|
+
resp.raise_for_status()
|
|
95
|
+
return resp.content
|
|
96
|
+
|
|
97
|
+
def upload(
|
|
98
|
+
self,
|
|
99
|
+
bucket: str,
|
|
100
|
+
key: str,
|
|
101
|
+
body: bytes,
|
|
102
|
+
content_type: str = DEFAULT_CONTENT_TYPE,
|
|
103
|
+
) -> None:
|
|
104
|
+
headers = {
|
|
105
|
+
"x-amz-content-sha256": hashlib.sha256(body).hexdigest(),
|
|
106
|
+
"Content-Type": content_type,
|
|
107
|
+
}
|
|
108
|
+
if self._upload_key:
|
|
109
|
+
headers["x-upload-key"] = self._upload_key
|
|
110
|
+
resp = self._session.put(
|
|
111
|
+
self._url(bucket, key),
|
|
112
|
+
headers=headers,
|
|
113
|
+
data=body,
|
|
114
|
+
proxies=self._proxies,
|
|
115
|
+
timeout=self._timeout,
|
|
116
|
+
)
|
|
117
|
+
resp.raise_for_status()
|
|
118
|
+
|
|
119
|
+
def upload_files(
|
|
120
|
+
self,
|
|
121
|
+
bucket: str,
|
|
122
|
+
key_prefix: str,
|
|
123
|
+
files: Mapping[str, bytes],
|
|
124
|
+
*,
|
|
125
|
+
content_type_for: Optional[Mapping[str, str]] = None,
|
|
126
|
+
) -> None:
|
|
127
|
+
"""把 ``{相对路径: bytes}`` 批量上传到 ``{bucket}/{key_prefix}/{相对路径}``。
|
|
128
|
+
|
|
129
|
+
Args:
|
|
130
|
+
bucket: 目标 bucket。
|
|
131
|
+
key_prefix: key 前缀(会与相对路径以 ``/`` 拼接;空前缀直接用相对路径)。
|
|
132
|
+
files: 相对路径 → bytes,通常是 ``TransformResult.to_files()`` 的返回。
|
|
133
|
+
content_type_for: 可选,按相对路径覆盖 Content-Type;缺省用
|
|
134
|
+
``application/octet-stream``。
|
|
135
|
+
"""
|
|
136
|
+
prefix = key_prefix.strip("/")
|
|
137
|
+
for rel_path, body in files.items():
|
|
138
|
+
key = f"{prefix}/{rel_path}" if prefix else rel_path
|
|
139
|
+
ctype = DEFAULT_CONTENT_TYPE
|
|
140
|
+
if content_type_for and rel_path in content_type_for:
|
|
141
|
+
ctype = content_type_for[rel_path]
|
|
142
|
+
self.upload(bucket, key, body, ctype)
|
|
143
|
+
|
|
144
|
+
def close(self) -> None:
|
|
145
|
+
self._session.close()
|
|
146
|
+
|
|
147
|
+
def __enter__(self) -> "CdnUploader":
|
|
148
|
+
return self
|
|
149
|
+
|
|
150
|
+
def __exit__(self, *exc) -> None:
|
|
151
|
+
self.close()
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
"""pdf_transformer:各模型专属的 raw 结果 → 统一 blocks / pages 转换器集合。
|
|
2
|
+
|
|
3
|
+
每个模型一个子包,产出同一套与模型无关的 blocks / pages 结构,交给
|
|
4
|
+
:mod:`orbitkit.pdf_validator` 做减法过滤、:mod:`orbitkit.cdn_uploader` 上传:
|
|
5
|
+
|
|
6
|
+
- :mod:`orbitkit.pdf_transformer.mineru` —— MinerU Router ZIP 转换器(现有)。
|
|
7
|
+
- 后续:``datalab`` 等其它模型。
|
|
8
|
+
|
|
9
|
+
用法::
|
|
10
|
+
|
|
11
|
+
from orbitkit.pdf_transformer.mineru import transform_zip, transform_chunk_zips
|
|
12
|
+
"""
|
|
13
|
+
|
|
14
|
+
from . import mineru
|
|
15
|
+
|
|
16
|
+
__all__ = ["mineru"]
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
"""MinerU 结果转换:raw ZIP → 结构化 blocks / pages(已旋转纠正 + 已 Y 翻转)+ 透传文件。
|
|
2
|
+
|
|
3
|
+
本子包只负责 **模型专属的解析 + 几何归一**,产出与模型无关的 blocks / pages
|
|
4
|
+
结构(:class:`TransformResult`)。**内容过滤**(减法)交给
|
|
5
|
+
:mod:`orbitkit.pdf_validator`;**上传**交给 :mod:`orbitkit.cdn_uploader`。
|
|
6
|
+
|
|
7
|
+
MinerU 是 :mod:`orbitkit.pdf_transformer` 下的其中一个模型实现(后续会有
|
|
8
|
+
``datalab`` 等),各模型都产出同一套 blocks / pages 结构以复用 pdf_validator。
|
|
9
|
+
|
|
10
|
+
典型用法::
|
|
11
|
+
|
|
12
|
+
from orbitkit.pdf_transformer.mineru import transform_zip
|
|
13
|
+
from orbitkit.pdf_validator import apply_block_filters, resolve_filters, parse_filter_names
|
|
14
|
+
|
|
15
|
+
result = transform_zip(zip_bytes, page_geometry=geom, bbox_rotate_correction=True)
|
|
16
|
+
result.blocks, result.pages = apply_block_filters(
|
|
17
|
+
result.blocks, result.pages,
|
|
18
|
+
resolve_filters(parse_filter_names("drop_non_text,drop_vlm_artifacts")),
|
|
19
|
+
)
|
|
20
|
+
files = result.to_files() # Dict[相对路径, bytes],可直接上传
|
|
21
|
+
"""
|
|
22
|
+
|
|
23
|
+
from .bbox_rotation import (
|
|
24
|
+
PageGeometry,
|
|
25
|
+
bbox_inverse_rotate,
|
|
26
|
+
correct_middle_bbox_rotation,
|
|
27
|
+
)
|
|
28
|
+
from .result_transformer import (
|
|
29
|
+
TransformResult,
|
|
30
|
+
transform_chunk_zips,
|
|
31
|
+
transform_zip,
|
|
32
|
+
)
|
|
33
|
+
|
|
34
|
+
__all__ = [
|
|
35
|
+
"TransformResult",
|
|
36
|
+
"transform_zip",
|
|
37
|
+
"transform_chunk_zips",
|
|
38
|
+
"PageGeometry",
|
|
39
|
+
"bbox_inverse_rotate",
|
|
40
|
+
"correct_middle_bbox_rotation",
|
|
41
|
+
]
|
|
@@ -0,0 +1,218 @@
|
|
|
1
|
+
"""bbox 旋转纠正:把 mineru middle.json 里 display 帧的 bbox 转回 MediaBox 帧。
|
|
2
|
+
|
|
3
|
+
# 现象(mineru 上游 bug)
|
|
4
|
+
|
|
5
|
+
1. mineru 用 pdfium 渲染时调用 ``page.render()``,pdfium **会自动应用
|
|
6
|
+
/Rotate**,所以渲染出来的图像在 "/Rotate 后的视觉帧"(display frame)。
|
|
7
|
+
VLM 看到的、输出 bbox 的图像就是这个 display frame。
|
|
8
|
+
2. mineru 把 bbox 写进 middle.json 时,**没有**把图像旋转过的事实补回去
|
|
9
|
+
→ middle.json 里的 bbox 留在 display frame。
|
|
10
|
+
3. 但 middle.json 的 ``page_size`` 字段是用 ``page.get_size()`` 拿的
|
|
11
|
+
**未旋转 MediaBox** 尺寸,**没有** W↔H 互换。
|
|
12
|
+
|
|
13
|
+
⇒ 同一个 middle.json 里 ``page_size`` 与 bbox 坐标系不一致:
|
|
14
|
+
- page_size = (mediabox_W, mediabox_H)
|
|
15
|
+
- bbox ∈ display frame = (mediabox_H, mediabox_W) for /Rotate∈{90,270}
|
|
16
|
+
|
|
17
|
+
下游若拿这份 bbox 去叠在原 PDF 渲染图(mediabox 帧)上 → 偏移 / 越界。
|
|
18
|
+
|
|
19
|
+
# 修复
|
|
20
|
+
|
|
21
|
+
把 bbox 反向旋转回 MediaBox 帧。
|
|
22
|
+
|
|
23
|
+
## 检测条件
|
|
24
|
+
|
|
25
|
+
* /Rotate ∈ {90, 270} → 进入纠正分支。
|
|
26
|
+
* **不**依赖 "page_size 维度互换" —— mineru 永远报未旋转 MediaBox,
|
|
27
|
+
这个条件**永远不成立**,不能作为信号。
|
|
28
|
+
* 仅做防御性 sanity:若 page_size 与 MediaBox 不一致(mineru 行为变了
|
|
29
|
+
或上游修了 bug),warn 一下、继续按旋转纠正走,需要人工看日志。
|
|
30
|
+
|
|
31
|
+
不处理 180°:mediabox 维度不变 → 检测不到;即使强制处理,也需要内容
|
|
32
|
+
方向探测(OCR)才知道要不要转。punt。
|
|
33
|
+
|
|
34
|
+
## 旋转公式
|
|
35
|
+
|
|
36
|
+
image 坐标系,top-left 原点 Y 向下:
|
|
37
|
+
- /Rotate=90 反向:``[x0,y0,x1,y1]`` → ``[y0, H_m - x1, y1, H_m - x0]``
|
|
38
|
+
- /Rotate=270 反向:``[x0,y0,x1,y1]`` → ``[W_m - y1, x0, W_m - y0, x1]``
|
|
39
|
+
|
|
40
|
+
推导见 :func:`bbox_inverse_rotate` 函数内注释。
|
|
41
|
+
"""
|
|
42
|
+
|
|
43
|
+
from __future__ import annotations
|
|
44
|
+
|
|
45
|
+
import logging
|
|
46
|
+
from typing import Any, Callable, List, Optional, Tuple
|
|
47
|
+
|
|
48
|
+
log = logging.getLogger(__name__)
|
|
49
|
+
|
|
50
|
+
# 每页几何信息:(mediabox_w, mediabox_h, /Rotate value 0/90/180/270)
|
|
51
|
+
PageGeometry = Tuple[float, float, int]
|
|
52
|
+
|
|
53
|
+
# 检测维度交换时的容差(像素)。mineru 的 page_size 偶尔有 ±1 浮点抖动。
|
|
54
|
+
DIM_SWAP_EPS = 2.0
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
def bbox_inverse_rotate(
|
|
58
|
+
bbox: List[float],
|
|
59
|
+
rotate: int,
|
|
60
|
+
media_w: float,
|
|
61
|
+
media_h: float,
|
|
62
|
+
) -> List[float]:
|
|
63
|
+
"""把 mineru 输出的 bbox(display 帧)反向旋转回 mediabox 帧。
|
|
64
|
+
|
|
65
|
+
bbox 输入约定:top-left 原点、Y 向下,且 ``x0 <= x1, y0 <= y1``。
|
|
66
|
+
输出同约定,在 (media_w, media_h) 帧里。
|
|
67
|
+
|
|
68
|
+
Args:
|
|
69
|
+
bbox: 4 元素 ``[x0, y0, x1, y1]``(display frame)
|
|
70
|
+
rotate: PDF /Rotate 字段值,仅 90/270 会真正旋转;其它值原样返回
|
|
71
|
+
media_w: MediaBox 宽
|
|
72
|
+
media_h: MediaBox 高
|
|
73
|
+
|
|
74
|
+
Returns:
|
|
75
|
+
转回 mediabox 帧的 4 元素 bbox。
|
|
76
|
+
"""
|
|
77
|
+
x0, y0, x1, y1 = bbox
|
|
78
|
+
# 防御性:调用方应保证 x0<=x1 / y0<=y1,但 middle.json 里偶有反向
|
|
79
|
+
if x0 > x1:
|
|
80
|
+
x0, x1 = x1, x0
|
|
81
|
+
if y0 > y1:
|
|
82
|
+
y0, y1 = y1, y0
|
|
83
|
+
|
|
84
|
+
if rotate == 90:
|
|
85
|
+
# 推导:mediabox→display 是 +90° CW,即 (x_m, y_m) → (H_m - y_m, x_m)。
|
|
86
|
+
# 反向是 -90° CW = +270° CW,即 display(x_d, y_d) → (y_d, H_m - x_d)。
|
|
87
|
+
# bbox 4 角全部映射后取 min/max:
|
|
88
|
+
return [y0, media_h - x1, y1, media_h - x0]
|
|
89
|
+
if rotate == 270:
|
|
90
|
+
# 推导:mediabox→display 是 +270° CW,即 (x_m, y_m) → (y_m, W_m - x_m)。
|
|
91
|
+
# 反向是 +90° CW,即 display(x_d, y_d) → (W_m - y_d, x_d)。
|
|
92
|
+
return [media_w - y1, x0, media_w - y0, x1]
|
|
93
|
+
# 其它情况(0 或 180 或非法值)不应到达此函数;保持不变以防错改
|
|
94
|
+
return [x0, y0, x1, y1]
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
def walk_and_fix_bboxes(
|
|
98
|
+
node: Any,
|
|
99
|
+
fix: Callable[[List[float]], List[float]],
|
|
100
|
+
counter: List[int],
|
|
101
|
+
) -> None:
|
|
102
|
+
"""递归遍历 page 子树(para_blocks/discarded_blocks/blocks/lines/spans),
|
|
103
|
+
对所有 ``bbox`` 字段应用 ``fix``。
|
|
104
|
+
|
|
105
|
+
``counter[0]`` 累加修改次数,便于日志统计。仅修改 ``list[float|int]``
|
|
106
|
+
形式且长度 ≥ 4 的 bbox;其它形态保持不变。
|
|
107
|
+
"""
|
|
108
|
+
if isinstance(node, dict):
|
|
109
|
+
bbox = node.get("bbox")
|
|
110
|
+
if (
|
|
111
|
+
isinstance(bbox, list)
|
|
112
|
+
and len(bbox) >= 4
|
|
113
|
+
and all(isinstance(v, (int, float)) for v in bbox[:4])
|
|
114
|
+
):
|
|
115
|
+
new_bbox = fix(list(map(float, bbox[:4])))
|
|
116
|
+
# 保留 bbox 列表后续元素(如有)
|
|
117
|
+
node["bbox"] = new_bbox + list(bbox[4:])
|
|
118
|
+
counter[0] += 1
|
|
119
|
+
for child_key in ("para_blocks", "discarded_blocks", "blocks", "lines", "spans"):
|
|
120
|
+
child = node.get(child_key)
|
|
121
|
+
if isinstance(child, list):
|
|
122
|
+
for item in child:
|
|
123
|
+
walk_and_fix_bboxes(item, fix, counter)
|
|
124
|
+
elif isinstance(node, list):
|
|
125
|
+
for item in node:
|
|
126
|
+
walk_and_fix_bboxes(item, fix, counter)
|
|
127
|
+
|
|
128
|
+
|
|
129
|
+
def correct_middle_bbox_rotation(
|
|
130
|
+
middle: dict,
|
|
131
|
+
page_geometry: List[PageGeometry],
|
|
132
|
+
*,
|
|
133
|
+
attach_id: Optional[int] = None,
|
|
134
|
+
) -> None:
|
|
135
|
+
"""对 middle.json 的每页应用 bbox 旋转纠正。**就地修改** ``middle``。
|
|
136
|
+
|
|
137
|
+
检测:``/Rotate ∈ {90, 270}``。其它情况(0° / 180° / 非法值 / geometry
|
|
138
|
+
越界 / page_size 缺失)→ 跳过。
|
|
139
|
+
|
|
140
|
+
Sanity 校验:mineru 当前行为是 ``page_size`` = 未旋转 MediaBox。若实际
|
|
141
|
+
page_size 与 MediaBox 维度不匹配(说明 mineru 行为变了、可能已经在上游
|
|
142
|
+
修了 bug),打 warn 但仍按旋转纠正走 —— 防止"我们以为修了,结果 mineru
|
|
143
|
+
自己又把 bbox 旋转回去导致双重旋转",需要人工看日志。
|
|
144
|
+
|
|
145
|
+
Args:
|
|
146
|
+
middle: middle.json 顶层 dict(应有 ``pdf_info`` 列表)
|
|
147
|
+
page_geometry: 每页 ``(mediabox_w, mediabox_h, /Rotate)``,索引按
|
|
148
|
+
页面 ``page_idx`` 对齐
|
|
149
|
+
attach_id: 仅用于日志关联,不影响行为
|
|
150
|
+
"""
|
|
151
|
+
pdf_info = middle.get("pdf_info") or []
|
|
152
|
+
for page in pdf_info:
|
|
153
|
+
if not isinstance(page, dict):
|
|
154
|
+
continue
|
|
155
|
+
page_idx = page.get("page_idx", -1)
|
|
156
|
+
if not isinstance(page_idx, int) or page_idx < 0 or page_idx >= len(page_geometry):
|
|
157
|
+
continue
|
|
158
|
+
media_w, media_h, rotate = page_geometry[page_idx]
|
|
159
|
+
|
|
160
|
+
if rotate not in (90, 270):
|
|
161
|
+
# 0° / 180° / 非法 → 不动
|
|
162
|
+
continue
|
|
163
|
+
|
|
164
|
+
mineru_size = page.get("page_size") or [0, 0]
|
|
165
|
+
if len(mineru_size) < 2:
|
|
166
|
+
continue
|
|
167
|
+
try:
|
|
168
|
+
mineru_w = float(mineru_size[0])
|
|
169
|
+
mineru_h = float(mineru_size[1])
|
|
170
|
+
except (TypeError, ValueError):
|
|
171
|
+
continue
|
|
172
|
+
|
|
173
|
+
# Sanity: mineru 期望行为 = page_size 与 MediaBox 维度一致(未旋转)
|
|
174
|
+
size_matches_mediabox = (
|
|
175
|
+
abs(mineru_w - media_w) < DIM_SWAP_EPS
|
|
176
|
+
and abs(mineru_h - media_h) < DIM_SWAP_EPS
|
|
177
|
+
)
|
|
178
|
+
size_is_swapped = (
|
|
179
|
+
abs(mineru_w - media_h) < DIM_SWAP_EPS
|
|
180
|
+
and abs(mineru_h - media_w) < DIM_SWAP_EPS
|
|
181
|
+
)
|
|
182
|
+
if not size_matches_mediabox:
|
|
183
|
+
if size_is_swapped:
|
|
184
|
+
# mineru 行为可能变了:page_size 用旋转后帧。这种情况下 bbox
|
|
185
|
+
# 是已经在 mediabox 帧(mineru 自己修了 bug),还是在 display
|
|
186
|
+
# 帧(mineru 改了 page_size 但忘了改 bbox),从 JSON 看不出。
|
|
187
|
+
# 保守仍按旋转纠正走,加 warn 让人工观察。
|
|
188
|
+
log.warning(
|
|
189
|
+
"bbox-correct: page_size W↔H swap vs MediaBox detected "
|
|
190
|
+
"(mineru=%.0fx%.0f mediabox=%.0fx%.0f /Rotate=%d). mineru "
|
|
191
|
+
"behavior may have changed — verify staging output before "
|
|
192
|
+
"trusting this correction (attach=%s page_idx=%d)",
|
|
193
|
+
mineru_w, mineru_h, media_w, media_h, rotate, attach_id, page_idx,
|
|
194
|
+
)
|
|
195
|
+
else:
|
|
196
|
+
# 完全不一致(既不匹配 MediaBox 也不是 W↔H swap)→ 异常,
|
|
197
|
+
# warn 但仍按旋转纠正走
|
|
198
|
+
log.warning(
|
|
199
|
+
"bbox-correct: page_size %.0fx%.0f mismatches both "
|
|
200
|
+
"MediaBox %.0fx%.0f and its swap; proceeding with rotation "
|
|
201
|
+
"anyway (attach=%s page_idx=%d /Rotate=%d)",
|
|
202
|
+
mineru_w, mineru_h, media_w, media_h, attach_id, page_idx, rotate,
|
|
203
|
+
)
|
|
204
|
+
|
|
205
|
+
def _fix(bbox: List[float]) -> List[float]:
|
|
206
|
+
return bbox_inverse_rotate(bbox, rotate, media_w, media_h)
|
|
207
|
+
|
|
208
|
+
counter = [0]
|
|
209
|
+
walk_and_fix_bboxes(page, _fix, counter)
|
|
210
|
+
# 把 page_size 改为 mediabox 帧;下游 _parse_middle 用 page_size[1]
|
|
211
|
+
# 做 Y 翻转时才能用对的 H_m。即使 mineru 已经写了 mediabox 也无副作用。
|
|
212
|
+
page["page_size"] = [media_w, media_h]
|
|
213
|
+
log.info(
|
|
214
|
+
"bbox-correct: rotated %d bbox(es) by inverse of /Rotate=%d "
|
|
215
|
+
"(attach=%s page_idx=%d, mineru=%.0fx%.0f → mediabox=%.0fx%.0f)",
|
|
216
|
+
counter[0], rotate, attach_id, page_idx,
|
|
217
|
+
mineru_w, mineru_h, media_w, media_h,
|
|
218
|
+
)
|