orbitkit 0.9.12__tar.gz → 0.9.22__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.9.12/orbitkit.egg-info → orbitkit-0.9.22}/PKG-INFO +2 -1
- orbitkit-0.9.22/orbitkit/VERSION +1 -0
- orbitkit-0.9.22/orbitkit/airflow_handler/file_flow_entry.py +46 -0
- orbitkit-0.9.22/orbitkit/pdf_chunking/__init__.py +62 -0
- orbitkit-0.9.22/orbitkit/pdf_chunking/chunk_fallback.py +81 -0
- orbitkit-0.9.22/orbitkit/pdf_chunking/chunk_plan.py +91 -0
- orbitkit-0.9.22/orbitkit/pdf_chunking/page_limit.py +61 -0
- orbitkit-0.9.22/orbitkit/pdf_chunking/pdf_metadata.py +157 -0
- orbitkit-0.9.22/orbitkit/pdf_pipeline_io/__init__.py +36 -0
- orbitkit-0.9.12/orbitkit/cdn_uploader/uploader.py → orbitkit-0.9.22/orbitkit/pdf_pipeline_io/cdn.py +3 -3
- orbitkit-0.9.22/orbitkit/pdf_pipeline_io/pg_task_store.py +475 -0
- orbitkit-0.9.22/orbitkit/pdf_quality/__init__.py +93 -0
- orbitkit-0.9.22/orbitkit/pdf_quality/failure_codes.py +145 -0
- orbitkit-0.9.22/orbitkit/pdf_quality/ocr_lang.py +122 -0
- orbitkit-0.9.22/orbitkit/pdf_quality/parse_record.py +272 -0
- orbitkit-0.9.22/orbitkit/pdf_quality/verdict.py +43 -0
- {orbitkit-0.9.12 → orbitkit-0.9.22}/orbitkit/pdf_transformer/__init__.py +1 -1
- {orbitkit-0.9.12 → orbitkit-0.9.22}/orbitkit/pdf_transformer/mineru/__init__.py +50 -1
- orbitkit-0.9.22/orbitkit/pdf_transformer/mineru/backends.py +82 -0
- {orbitkit-0.9.12 → orbitkit-0.9.22}/orbitkit/pdf_transformer/mineru/bbox_rotation.py +8 -0
- {orbitkit-0.9.12 → orbitkit-0.9.22}/orbitkit/pdf_transformer/mineru/result_transformer.py +305 -25
- orbitkit-0.9.22/orbitkit/pdf_validator/__init__.py +96 -0
- orbitkit-0.9.22/orbitkit/pdf_validator/filters/drop_invalid_bbox.py +27 -0
- orbitkit-0.9.22/orbitkit/pdf_validator/filters/repeated_text.py +335 -0
- orbitkit-0.9.22/orbitkit/pdf_validator/pipeline.py +194 -0
- orbitkit-0.9.22/orbitkit/pdf_validator/registry.py +105 -0
- orbitkit-0.9.22/orbitkit/pdf_validator/validate.py +391 -0
- {orbitkit-0.9.12 → orbitkit-0.9.22/orbitkit.egg-info}/PKG-INFO +2 -1
- {orbitkit-0.9.12 → orbitkit-0.9.22}/orbitkit.egg-info/SOURCES.txt +17 -2
- {orbitkit-0.9.12 → orbitkit-0.9.22}/orbitkit.egg-info/requires.txt +1 -0
- {orbitkit-0.9.12 → orbitkit-0.9.22}/setup.py +1 -0
- orbitkit-0.9.12/orbitkit/VERSION +0 -1
- orbitkit-0.9.12/orbitkit/cdn_uploader/__init__.py +0 -16
- orbitkit-0.9.12/orbitkit/pdf_validator/__init__.py +0 -35
- orbitkit-0.9.12/orbitkit/pdf_validator/filters/repeated_text.py +0 -128
- orbitkit-0.9.12/orbitkit/pdf_validator/pipeline.py +0 -103
- orbitkit-0.9.12/orbitkit/pdf_validator/registry.py +0 -64
- {orbitkit-0.9.12 → orbitkit-0.9.22}/LICENSE +0 -0
- {orbitkit-0.9.12 → orbitkit-0.9.22}/MANIFEST.in +0 -0
- {orbitkit-0.9.12 → orbitkit-0.9.22}/README.md +0 -0
- {orbitkit-0.9.12 → orbitkit-0.9.22}/orbitkit/__init__.py +0 -0
- {orbitkit-0.9.12 → orbitkit-0.9.22}/orbitkit/airflow_handler/__init__.py +0 -0
- {orbitkit-0.9.12 → orbitkit-0.9.22}/orbitkit/airflow_handler/data_preprocessing.py +0 -0
- {orbitkit-0.9.12 → orbitkit-0.9.22}/orbitkit/airflow_handler/file_flow_entry_process.py +0 -0
- {orbitkit-0.9.12 → orbitkit-0.9.22}/orbitkit/airflow_handler/file_flow_exit_process.py +0 -0
- {orbitkit-0.9.12 → orbitkit-0.9.22}/orbitkit/airflow_handler/file_handler.py +0 -0
- {orbitkit-0.9.12 → orbitkit-0.9.22}/orbitkit/airflow_handler/file_handler_v2.py +0 -0
- {orbitkit-0.9.12 → orbitkit-0.9.22}/orbitkit/audio_transcoder/__init__.py +0 -0
- {orbitkit-0.9.12 → orbitkit-0.9.22}/orbitkit/audio_transcoder/netmind_extract_v1.py +0 -0
- {orbitkit-0.9.12 → orbitkit-0.9.22}/orbitkit/constant/__init__.py +0 -0
- {orbitkit-0.9.12 → orbitkit-0.9.22}/orbitkit/constant/report_schema.py +0 -0
- {orbitkit-0.9.12 → orbitkit-0.9.22}/orbitkit/id_srv/__init__.py +0 -0
- {orbitkit-0.9.12 → orbitkit-0.9.22}/orbitkit/id_srv/id_gen.py +0 -0
- {orbitkit-0.9.12 → orbitkit-0.9.22}/orbitkit/id_srv/id_perm_like.py +0 -0
- {orbitkit-0.9.12 → orbitkit-0.9.22}/orbitkit/lark_send/__init__.py +0 -0
- {orbitkit-0.9.12 → orbitkit-0.9.22}/orbitkit/lark_send/lark.py +0 -0
- {orbitkit-0.9.12 → orbitkit-0.9.22}/orbitkit/llm_tools/__init__.py +0 -0
- {orbitkit-0.9.12 → orbitkit-0.9.22}/orbitkit/llm_tools/quick_rag_chat.py +0 -0
- {orbitkit-0.9.12 → orbitkit-0.9.22}/orbitkit/orbit_type/__init__.py +0 -0
- {orbitkit-0.9.12 → orbitkit-0.9.22}/orbitkit/orbit_type/doc_4_compile_rule.py +0 -0
- {orbitkit-0.9.12 → orbitkit-0.9.22}/orbitkit/orbit_type/orbit_type_simple.py +0 -0
- {orbitkit-0.9.12 → orbitkit-0.9.22}/orbitkit/orbit_type/tools.py +0 -0
- {orbitkit-0.9.12 → orbitkit-0.9.22}/orbitkit/pdf_embedding/__init__.py +0 -0
- {orbitkit-0.9.12 → orbitkit-0.9.22}/orbitkit/pdf_embedding/pdf_txt_embedding.py +0 -0
- {orbitkit-0.9.12 → orbitkit-0.9.22}/orbitkit/pdf_embedding/pdf_txt_embedding_v2.py +0 -0
- {orbitkit-0.9.12 → orbitkit-0.9.22}/orbitkit/pdf_extractor/__init__.py +0 -0
- {orbitkit-0.9.12 → orbitkit-0.9.22}/orbitkit/pdf_extractor/a_stock_extractor_v1.py +0 -0
- {orbitkit-0.9.12 → orbitkit-0.9.22}/orbitkit/pdf_extractor/exceptions.py +0 -0
- {orbitkit-0.9.12 → orbitkit-0.9.22}/orbitkit/pdf_extractor/mineru_demo.py +0 -0
- {orbitkit-0.9.12 → orbitkit-0.9.22}/orbitkit/pdf_extractor/pdf_block_extractor_base.py +0 -0
- {orbitkit-0.9.12 → orbitkit-0.9.22}/orbitkit/pdf_extractor/pdf_block_extractor_v1.py +0 -0
- {orbitkit-0.9.12 → orbitkit-0.9.22}/orbitkit/pdf_extractor/pdf_block_extractor_v2.py +0 -0
- {orbitkit-0.9.12 → orbitkit-0.9.22}/orbitkit/pdf_extractor/pdf_extractor_azure.py +0 -0
- {orbitkit-0.9.12 → orbitkit-0.9.22}/orbitkit/pdf_extractor/pdf_extractor_minerU_v1.py +0 -0
- {orbitkit-0.9.12 → orbitkit-0.9.22}/orbitkit/pdf_extractor/pdf_extractor_netmind_v1.py +0 -0
- {orbitkit-0.9.12 → orbitkit-0.9.22}/orbitkit/pdf_extractor/pdf_extractor_netmind_v2.py +0 -0
- {orbitkit-0.9.12 → orbitkit-0.9.22}/orbitkit/pdf_extractor/pdf_extractor_netmind_v3.py +0 -0
- {orbitkit-0.9.12 → orbitkit-0.9.22}/orbitkit/pdf_extractor/pdf_extractor_orbit.py +0 -0
- {orbitkit-0.9.12 → orbitkit-0.9.22}/orbitkit/pdf_extractor_simple/__init__.py +0 -0
- {orbitkit-0.9.12 → orbitkit-0.9.22}/orbitkit/pdf_extractor_simple/base.py +0 -0
- {orbitkit-0.9.12 → orbitkit-0.9.22}/orbitkit/pdf_extractor_simple/cloud_provider.py +0 -0
- {orbitkit-0.9.12 → orbitkit-0.9.22}/orbitkit/pdf_extractor_simple/core.py +0 -0
- {orbitkit-0.9.12 → orbitkit-0.9.22}/orbitkit/pdf_extractor_simple/exceptions.py +0 -0
- {orbitkit-0.9.12 → orbitkit-0.9.22}/orbitkit/pdf_extractor_simple/extractors.py +0 -0
- {orbitkit-0.9.12 → orbitkit-0.9.22}/orbitkit/pdf_extractor_simple/utils.py +0 -0
- {orbitkit-0.9.12 → orbitkit-0.9.22}/orbitkit/pdf_validator/filters/__init__.py +0 -0
- {orbitkit-0.9.12 → orbitkit-0.9.22}/orbitkit/pdf_validator/filters/drop_non_text.py +0 -0
- {orbitkit-0.9.12 → orbitkit-0.9.22}/orbitkit/pdf_validator/filters/drop_vlm_artifacts.py +0 -0
- {orbitkit-0.9.12 → orbitkit-0.9.22}/orbitkit/pdf_writer/__init__.py +0 -0
- {orbitkit-0.9.12 → orbitkit-0.9.22}/orbitkit/pdf_writer/pdf_writer_simple.py +0 -0
- {orbitkit-0.9.12 → orbitkit-0.9.22}/orbitkit/util/__init__.py +0 -0
- {orbitkit-0.9.12 → orbitkit-0.9.22}/orbitkit/util/cache_asset_downloader.py +0 -0
- {orbitkit-0.9.12 → orbitkit-0.9.22}/orbitkit/util/common.py +0 -0
- {orbitkit-0.9.12 → orbitkit-0.9.22}/orbitkit/util/customize_regix_manager.py +0 -0
- {orbitkit-0.9.12 → orbitkit-0.9.22}/orbitkit/util/is_xbrl_structure.py +0 -0
- {orbitkit-0.9.12 → orbitkit-0.9.22}/orbitkit/util/secret_manager.py +0 -0
- {orbitkit-0.9.12 → orbitkit-0.9.22}/orbitkit/util/universal_extractor.py +0 -0
- {orbitkit-0.9.12 → orbitkit-0.9.22}/orbitkit/util/util_aliyun.py +0 -0
- {orbitkit-0.9.12 → orbitkit-0.9.22}/orbitkit/util/util_aliyun_oss_simple.py +0 -0
- {orbitkit-0.9.12 → orbitkit-0.9.22}/orbitkit/util/util_aws.py +0 -0
- {orbitkit-0.9.12 → orbitkit-0.9.22}/orbitkit/util/util_aws_s3_wrapper.py +0 -0
- {orbitkit-0.9.12 → orbitkit-0.9.22}/orbitkit/util/util_date.py +0 -0
- {orbitkit-0.9.12 → orbitkit-0.9.22}/orbitkit/util/util_html.py +0 -0
- {orbitkit-0.9.12 → orbitkit-0.9.22}/orbitkit/util/util_kafka.py +0 -0
- {orbitkit-0.9.12 → orbitkit-0.9.22}/orbitkit/util/util_md5.py +0 -0
- {orbitkit-0.9.12 → orbitkit-0.9.22}/orbitkit/util/util_selenium.py +0 -0
- {orbitkit-0.9.12 → orbitkit-0.9.22}/orbitkit/util/util_simple_timer.py +0 -0
- {orbitkit-0.9.12 → orbitkit-0.9.22}/orbitkit/util/util_str.py +0 -0
- {orbitkit-0.9.12 → orbitkit-0.9.22}/orbitkit/util/util_type_mapping.py +0 -0
- {orbitkit-0.9.12 → orbitkit-0.9.22}/orbitkit/util/util_url.py +0 -0
- {orbitkit-0.9.12 → orbitkit-0.9.22}/orbitkit.egg-info/dependency_links.txt +0 -0
- {orbitkit-0.9.12 → orbitkit-0.9.22}/orbitkit.egg-info/not-zip-safe +0 -0
- {orbitkit-0.9.12 → orbitkit-0.9.22}/orbitkit.egg-info/top_level.txt +0 -0
- {orbitkit-0.9.12 → orbitkit-0.9.22}/setup.cfg +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: orbitkit
|
|
3
|
-
Version: 0.9.
|
|
3
|
+
Version: 0.9.22
|
|
4
4
|
Summary: This project is only for Orbit Tech internal use.
|
|
5
5
|
Home-page: https://github.com/clown-0726/orbitkit
|
|
6
6
|
Author: Lilu Cao
|
|
@@ -29,6 +29,7 @@ Classifier: Programming Language :: Python :: 3.14
|
|
|
29
29
|
Classifier: Topic :: Software Development :: Libraries
|
|
30
30
|
Description-Content-Type: text/markdown
|
|
31
31
|
License-File: LICENSE
|
|
32
|
+
Requires-Dist: asyncpg>=0.29.0
|
|
32
33
|
Requires-Dist: boto3>=1.40.46
|
|
33
34
|
Requires-Dist: aioboto3>=15.5.0
|
|
34
35
|
Requires-Dist: aiofiles>=25.1.0
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
0.9.22
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
"""file_flow 任务下发入口(独立运维工具,旁路)。
|
|
2
|
+
|
|
3
|
+
通过 orbitkit.airflow_handler.FilingOfficialProcessor 把 MongoDB query 命中的 ID
|
|
4
|
+
派发到 worker 队列。本入口与 .claude/skills/attachments-process-* 4-skill 体系并行,
|
|
5
|
+
**不经 runtime registry**,仅作运维直跑使用。
|
|
6
|
+
|
|
7
|
+
queue_type 选项:
|
|
8
|
+
|
|
9
|
+
- ``MineruExtract`` : CPU 队列,默认,吞吐适中、成本低
|
|
10
|
+
- ``MineruExtract-GPU`` : GPU 队列,吞吐高,需要 GPU 资源就绪
|
|
11
|
+
|
|
12
|
+
切换 queue_type 时通常同步调整 priority(GPU 路径占用更高优先级)。
|
|
13
|
+
|
|
14
|
+
与 4-skill 体系的关系见项目根 ``CLAUDE.md`` 的「attachments_process/(独立运维工具集,旁路)」段。
|
|
15
|
+
"""
|
|
16
|
+
import csv
|
|
17
|
+
import datetime
|
|
18
|
+
import logging
|
|
19
|
+
import os
|
|
20
|
+
import asyncio
|
|
21
|
+
from orbitkit.airflow_handler.file_flow_entry_process import FilingOfficialProcessor
|
|
22
|
+
from orbitkit.util.secret_manager import SecretManager
|
|
23
|
+
|
|
24
|
+
logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(name)s - %(levelname)s - %(message)s')
|
|
25
|
+
logger = logging.getLogger(__name__)
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
async def main_loop(max_process_count):
|
|
29
|
+
SecretManager.load_secret_2_envs(['prod/fileflow/data'])
|
|
30
|
+
processor = FilingOfficialProcessor(
|
|
31
|
+
mongo_uri=os.environ['MONGO_URI'],
|
|
32
|
+
postgres_uri=os.environ['POSTGRES_URI'],
|
|
33
|
+
aws_access_key_id=os.environ['AWS_ACCESS_KEY_ID'],
|
|
34
|
+
aws_secret_access_key=os.environ['AWS_SECRET_ACCESS_KEY'],
|
|
35
|
+
pi2_postgres_uri=os.environ['PG_URI_CX45_USER_FILING']
|
|
36
|
+
)
|
|
37
|
+
query_dict = {
|
|
38
|
+
'_id': 'f_aXz7Y4n9qTlTRyuTc8sWsb',
|
|
39
|
+
}
|
|
40
|
+
await processor.process_task_entry(source='filing_data_staging', query=query_dict,
|
|
41
|
+
tags=['daily_increment_data'], allow_parse_over_limit=True,
|
|
42
|
+
priority='1', is_important=False, important_level=1, queue_type='MineruExtract-GPU')
|
|
43
|
+
|
|
44
|
+
if __name__ == '__main__':
|
|
45
|
+
asyncio.run(main_loop(1))
|
|
46
|
+
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
"""pdf_chunking:大 PDF 的页数门槛 / 按页切块 / 切块兜底选块(纯计算)。
|
|
2
|
+
|
|
3
|
+
与 ``pdf_transformer.mineru.transform_chunk_zips`` 是一对:这里负责「切」和
|
|
4
|
+
「决定」,那边负责「合」。本包不调 Router / CDN / PostgreSQL;唯一的外部依赖是
|
|
5
|
+
PyMuPDF(可选,缺失时 :mod:`.pdf_metadata` 一律返回 None,退化为整篇提交)。
|
|
6
|
+
|
|
7
|
+
提交前::
|
|
8
|
+
|
|
9
|
+
from orbitkit.pdf_chunking import count_pdf_pages, decide_pages, plan_chunks
|
|
10
|
+
|
|
11
|
+
n = count_pdf_pages(pdf_bytes) # None = 数不出,放行
|
|
12
|
+
d = decide_pages(n, max_pdf_pages=1000, chunk_pages=64,
|
|
13
|
+
allow_parse_over_limit=False)
|
|
14
|
+
if d.reject: ... # 10024,不送 Router
|
|
15
|
+
plan = plan_chunks(pdf_bytes, chunk_pages=64, should_chunk=d.should_chunk)
|
|
16
|
+
if plan.chunked:
|
|
17
|
+
parts = plan.take_parts() # 所有权只能取一次
|
|
18
|
+
names = [plan.part_name(filename, k) for k in range(plan.n_chunks)]
|
|
19
|
+
|
|
20
|
+
切块兜底(每块一份 ``FilterStats``,整篇 ``Verdict`` 决定按哪些维度选块)::
|
|
21
|
+
|
|
22
|
+
from orbitkit.pdf_chunking import bad_chunks, empty_chunks, all_empty
|
|
23
|
+
|
|
24
|
+
redo = bad_chunks(stats_per_chunk,
|
|
25
|
+
degenerate=record.repetition_failed, rd_threshold=0.06,
|
|
26
|
+
bad_block=record.bad_block_failed, bad_block_threshold=0.06)
|
|
27
|
+
"""
|
|
28
|
+
|
|
29
|
+
from .chunk_fallback import (
|
|
30
|
+
all_empty,
|
|
31
|
+
bad_block_chunks,
|
|
32
|
+
bad_chunks,
|
|
33
|
+
degenerate_chunks,
|
|
34
|
+
empty_chunks,
|
|
35
|
+
)
|
|
36
|
+
from .chunk_plan import ChunkPlan, Splitter, plan_chunks
|
|
37
|
+
from .page_limit import PageDecision, decide_pages
|
|
38
|
+
from .pdf_metadata import (
|
|
39
|
+
HAS_PYMUPDF,
|
|
40
|
+
PageGeometry,
|
|
41
|
+
count_pdf_pages,
|
|
42
|
+
extract_page_geometry,
|
|
43
|
+
split_pdf_pages,
|
|
44
|
+
)
|
|
45
|
+
|
|
46
|
+
__all__ = [
|
|
47
|
+
"PageDecision",
|
|
48
|
+
"decide_pages",
|
|
49
|
+
"ChunkPlan",
|
|
50
|
+
"Splitter",
|
|
51
|
+
"plan_chunks",
|
|
52
|
+
"empty_chunks",
|
|
53
|
+
"all_empty",
|
|
54
|
+
"degenerate_chunks",
|
|
55
|
+
"bad_block_chunks",
|
|
56
|
+
"bad_chunks",
|
|
57
|
+
"HAS_PYMUPDF",
|
|
58
|
+
"PageGeometry",
|
|
59
|
+
"count_pdf_pages",
|
|
60
|
+
"split_pdf_pages",
|
|
61
|
+
"extract_page_geometry",
|
|
62
|
+
]
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
"""切块兜底的块级判定 —— 「哪些块要重投」的唯一实现(纯函数,无 I/O)。
|
|
2
|
+
|
|
3
|
+
整篇路径的两条兜底(空解析 / 复读退化)都是「整篇一个数、整篇重投一次」。
|
|
4
|
+
切块(> ``chunk_pages`` 页)的文档是 N 个块 ZIP 合成一份,整篇数字会被
|
|
5
|
+
好块稀释,重投整篇又是主路径几倍的 GPU 时间。所以切块路径只重投**坏块**:
|
|
6
|
+
|
|
7
|
+
- 空块:``total_in == 0``。0 block 的块没有分母,闸门看不见它,只能在这里判。
|
|
8
|
+
- 退化块 / 坏块:只在**整篇合并后的对应率也超阀**时才找(保守版)——整篇不
|
|
9
|
+
超阀就当没事,避免块级分母变小(64 页 vs 整篇)让同一阈值在块上更敏感、兜底
|
|
10
|
+
触发率悄悄变高。整篇率 = Σerr / Σtotal ≤ max 块率,所以整篇超阀时至少一块超阀;
|
|
11
|
+
万一因序列 filter 跨块边界的口径差一块都没超,退回取率最高的那一块。
|
|
12
|
+
复读、坏块两个维度各自按上述规则选块,取并集(:func:`bad_chunks`)。
|
|
13
|
+
|
|
14
|
+
``None`` 的块(middle.json 解析不了)一律不算坏块:让 transform 去报真正的
|
|
15
|
+
错误码,跟 ``count_zip_blocks`` 的约定一致。
|
|
16
|
+
"""
|
|
17
|
+
|
|
18
|
+
from __future__ import annotations
|
|
19
|
+
|
|
20
|
+
from typing import Callable, List, Optional, Sequence, Set
|
|
21
|
+
|
|
22
|
+
from ..pdf_quality import VLM_ARTIFACT_FILTER
|
|
23
|
+
from ..pdf_validator import FilterStats
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def empty_chunks(stats: Sequence[Optional[FilterStats]]) -> List[int]:
|
|
27
|
+
"""0 个 block 的块下标(升序)。"""
|
|
28
|
+
return [k for k, s in enumerate(stats) if s is not None and s.total_in == 0]
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def all_empty(stats: Sequence[Optional[FilterStats]]) -> bool:
|
|
32
|
+
"""每一块都判得出且都是 0 block(= 整篇空解析)。"""
|
|
33
|
+
return bool(stats) and all(s is not None and s.total_in == 0 for s in stats)
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def _over_threshold(
|
|
37
|
+
stats: Sequence[Optional[FilterStats]],
|
|
38
|
+
count: Callable[[FilterStats], int],
|
|
39
|
+
threshold: float,
|
|
40
|
+
) -> List[int]:
|
|
41
|
+
rates = [
|
|
42
|
+
(k, count(s) / s.total_in)
|
|
43
|
+
for k, s in enumerate(stats)
|
|
44
|
+
if s is not None and s.total_in > 0
|
|
45
|
+
]
|
|
46
|
+
over = [k for k, r in rates if r > threshold]
|
|
47
|
+
if over or not rates:
|
|
48
|
+
return over
|
|
49
|
+
return [max(rates, key=lambda kr: kr[1])[0]]
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
def degenerate_chunks(
|
|
53
|
+
stats: Sequence[Optional[FilterStats]], *, threshold: float,
|
|
54
|
+
) -> List[int]:
|
|
55
|
+
"""复读率**严格大于** ``threshold`` 的块下标(升序);一块都没有则退回
|
|
56
|
+
率最高的那一块(非空块里选)。全部判不出 / 全空 → 空列表。"""
|
|
57
|
+
return _over_threshold(stats, lambda s: s.degenerate, threshold)
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
def bad_block_chunks(
|
|
61
|
+
stats: Sequence[Optional[FilterStats]], *, threshold: float,
|
|
62
|
+
) -> List[int]:
|
|
63
|
+
"""坏块率(``drop_vlm_artifacts`` 丢弃数 / 总块)超阀的块下标;规则同
|
|
64
|
+
:func:`degenerate_chunks`。"""
|
|
65
|
+
return _over_threshold(
|
|
66
|
+
stats, lambda s: s.dropped_by(VLM_ARTIFACT_FILTER), threshold,
|
|
67
|
+
)
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
def bad_chunks(
|
|
71
|
+
stats: Sequence[Optional[FilterStats]], *,
|
|
72
|
+
degenerate: bool, rd_threshold: float,
|
|
73
|
+
bad_block: bool, bad_block_threshold: float,
|
|
74
|
+
) -> List[int]:
|
|
75
|
+
"""整篇哪些闸超了(``degenerate`` / ``bad_block``)就按那些维度选块,并集升序。"""
|
|
76
|
+
picked: Set[int] = set()
|
|
77
|
+
if degenerate:
|
|
78
|
+
picked.update(degenerate_chunks(stats, threshold=rd_threshold))
|
|
79
|
+
if bad_block:
|
|
80
|
+
picked.update(bad_block_chunks(stats, threshold=bad_block_threshold))
|
|
81
|
+
return sorted(picked)
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
"""切块计划 —— 把「切不切 / 切失败怎么办 / 块怎么命名 / 每块几页」收成一处。
|
|
2
|
+
|
|
3
|
+
切块用的页数和合并(``pdf_transformer.mineru.transform_chunk_zips(chunk_pages=...)``
|
|
4
|
+
做 page offset 累加)用的页数必须是同一个值,否则合并后的页码会静默错位。
|
|
5
|
+
:class:`ChunkPlan` 把这个值随计划一起交给 transform,两者不可能不一致。
|
|
6
|
+
|
|
7
|
+
## 为什么不是 frozen dataclass
|
|
8
|
+
|
|
9
|
+
计划要携带切出来的字节。如果它一直持有这份引用,高并发下就等于每篇 PDF 多留
|
|
10
|
+
一份在内存里。所以字节的所有权只能被 :meth:`ChunkPlan.take_parts` **取走一次**,
|
|
11
|
+
取走后本对象不再持有;重复取或对整篇计划取都是大声失败(释放后访问不要静默)。
|
|
12
|
+
计划本身的形状(``chunked`` / ``n_chunks``)不受取走影响。
|
|
13
|
+
"""
|
|
14
|
+
|
|
15
|
+
from __future__ import annotations
|
|
16
|
+
|
|
17
|
+
from typing import Callable, List, Optional
|
|
18
|
+
|
|
19
|
+
from .pdf_metadata import split_pdf_pages
|
|
20
|
+
|
|
21
|
+
Splitter = Callable[[bytes, int], Optional[List[bytes]]]
|
|
22
|
+
"""``(pdf_bytes, pages_per_chunk) -> 各块字节 | None``(None = 不切 / 切不成)。"""
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
class ChunkPlan:
|
|
26
|
+
"""一篇 PDF 的提交计划:整篇一次,或按页切成 N 块。"""
|
|
27
|
+
|
|
28
|
+
__slots__ = ("chunk_pages", "split_failed", "_parts", "_chunked", "_n_chunks")
|
|
29
|
+
|
|
30
|
+
def __init__(
|
|
31
|
+
self,
|
|
32
|
+
parts: Optional[List[bytes]],
|
|
33
|
+
*,
|
|
34
|
+
chunk_pages: int,
|
|
35
|
+
split_failed: bool = False,
|
|
36
|
+
) -> None:
|
|
37
|
+
self._parts = parts
|
|
38
|
+
self._chunked = parts is not None
|
|
39
|
+
self._n_chunks = len(parts) if parts is not None else 1
|
|
40
|
+
self.chunk_pages = chunk_pages
|
|
41
|
+
self.split_failed = split_failed
|
|
42
|
+
|
|
43
|
+
@property
|
|
44
|
+
def chunked(self) -> bool:
|
|
45
|
+
"""是否走切块路径(False = 整篇单次提交)。"""
|
|
46
|
+
return self._chunked
|
|
47
|
+
|
|
48
|
+
@property
|
|
49
|
+
def n_chunks(self) -> int:
|
|
50
|
+
return self._n_chunks
|
|
51
|
+
|
|
52
|
+
@property
|
|
53
|
+
def holds_parts(self) -> bool:
|
|
54
|
+
"""本对象是否还持有各块字节(取走所有权后为 False)。"""
|
|
55
|
+
return self._parts is not None
|
|
56
|
+
|
|
57
|
+
def take_parts(self) -> List[bytes]:
|
|
58
|
+
"""转移各块字节的所有权(只能取一次;取后本对象不再持有引用)。"""
|
|
59
|
+
if not self._chunked:
|
|
60
|
+
raise RuntimeError("ChunkPlan is whole-file; there are no parts")
|
|
61
|
+
if self._parts is None:
|
|
62
|
+
raise RuntimeError("ChunkPlan parts already taken")
|
|
63
|
+
parts, self._parts = self._parts, None
|
|
64
|
+
return parts
|
|
65
|
+
|
|
66
|
+
@staticmethod
|
|
67
|
+
def part_name(filename: str, index: int) -> str:
|
|
68
|
+
"""第 ``index`` 块提交给 Router 的文件名(命名唯一实现)。"""
|
|
69
|
+
return f"{filename}.part{index:03d}.pdf"
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
def plan_chunks(
|
|
73
|
+
pdf_bytes: bytes,
|
|
74
|
+
*,
|
|
75
|
+
chunk_pages: int,
|
|
76
|
+
should_chunk: bool,
|
|
77
|
+
splitter: Optional[Splitter] = None,
|
|
78
|
+
) -> ChunkPlan:
|
|
79
|
+
"""按页切块;不需要切或切不出来时,退化成整篇提交的计划。
|
|
80
|
+
|
|
81
|
+
默认 ``splitter`` 是 PyMuPDF 版 :func:`split_pdf_pages`,对「无需切块 / 加密 /
|
|
82
|
+
PyMuPDF 不可用 / 解析失败」一律返回 None(不抛异常),这里把它统一归成整篇
|
|
83
|
+
提交,并用 ``split_failed`` 区分「本来就不用切」和「想切但没切成」(后者调用
|
|
84
|
+
方要记一行 WARNING)。``splitter`` 可注入别的实现(其它 PDF 库 / 测试桩)。
|
|
85
|
+
"""
|
|
86
|
+
if not should_chunk:
|
|
87
|
+
return ChunkPlan(None, chunk_pages=chunk_pages)
|
|
88
|
+
parts = (splitter or split_pdf_pages)(pdf_bytes, chunk_pages)
|
|
89
|
+
if not parts:
|
|
90
|
+
return ChunkPlan(None, chunk_pages=chunk_pages, split_failed=True)
|
|
91
|
+
return ChunkPlan(parts, chunk_pages=chunk_pages)
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
"""页数门槛判定 —— 纯函数,不碰 IO、不看 config 对象。
|
|
2
|
+
|
|
3
|
+
页数极多的 PDF 会长时间独占 GPU / 拖垮 Router(大面积 30001 的根因),所以
|
|
4
|
+
数出页数后要一次性回答三件事:
|
|
5
|
+
|
|
6
|
+
1. 是否硬拦截(不送 Router,判 10024)
|
|
7
|
+
2. 是否「超限但被 op_meta 的 ``allow_parse_over_limit`` 放行」(要记一行 INFO)
|
|
8
|
+
3. 是否切块(与 1、2 无关:放行的超限文档**仍然**切块)
|
|
9
|
+
|
|
10
|
+
三条判据都取决于同一组输入,分开写就会出现「改了拦截忘了切块」这类静默不一致。
|
|
11
|
+
收成一个 :class:`PageDecision`,各分支各一个单测。
|
|
12
|
+
|
|
13
|
+
判据:
|
|
14
|
+
- ``max_pdf_pages <= 0`` → 禁用页数上限,从不拦截。
|
|
15
|
+
- ``allow_parse_over_limit`` → 不拦截(仍切块)。
|
|
16
|
+
- ``page_count is None``(加密 / 解析失败 / PyMuPDF 不可用)→ 放行,
|
|
17
|
+
按整篇提交处理;此时也**不切块**(页数未知,切不出来)。
|
|
18
|
+
"""
|
|
19
|
+
|
|
20
|
+
from __future__ import annotations
|
|
21
|
+
|
|
22
|
+
from dataclasses import dataclass
|
|
23
|
+
from typing import Optional
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
@dataclass(frozen=True)
|
|
27
|
+
class PageDecision:
|
|
28
|
+
"""页数门槛的完整结论。三个字段互相独立,调用方不再自己重算。"""
|
|
29
|
+
|
|
30
|
+
reject: bool
|
|
31
|
+
"""硬拦截:不送 Router,判 ``pdf_quality.failure_codes.PAGE_COUNT_EXCEEDS_LIMIT`` (10024)。"""
|
|
32
|
+
|
|
33
|
+
over_limit_allowed: bool
|
|
34
|
+
"""超过 ``max_pdf_pages`` 但被 op_meta 开关放行(仅用于记一行 INFO)。"""
|
|
35
|
+
|
|
36
|
+
should_chunk: bool
|
|
37
|
+
"""按 ``chunk_pages`` 切块提交。"""
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def decide_pages(
|
|
41
|
+
page_count: Optional[int],
|
|
42
|
+
*,
|
|
43
|
+
max_pdf_pages: int,
|
|
44
|
+
chunk_pages: int,
|
|
45
|
+
allow_parse_over_limit: bool,
|
|
46
|
+
) -> PageDecision:
|
|
47
|
+
"""由页数 + 两个上限 + meta 开关,一次算出全部页数相关结论。"""
|
|
48
|
+
over_limit = (
|
|
49
|
+
max_pdf_pages > 0
|
|
50
|
+
and page_count is not None
|
|
51
|
+
and page_count > max_pdf_pages
|
|
52
|
+
)
|
|
53
|
+
return PageDecision(
|
|
54
|
+
reject=over_limit and not allow_parse_over_limit,
|
|
55
|
+
over_limit_allowed=over_limit and allow_parse_over_limit,
|
|
56
|
+
should_chunk=(
|
|
57
|
+
chunk_pages > 0
|
|
58
|
+
and page_count is not None
|
|
59
|
+
and page_count > chunk_pages
|
|
60
|
+
),
|
|
61
|
+
)
|
|
@@ -0,0 +1,157 @@
|
|
|
1
|
+
"""PDF 页级元信息:页数 / 按页切块 / 每页 mediabox + /Rotate(PyMuPDF,可选依赖)。
|
|
2
|
+
|
|
3
|
+
三个函数都「失败返回 None、不抛异常」:解析失败 / 加密打不开 / PyMuPDF 未安装
|
|
4
|
+
时调用方应放行(按整篇提交、跳过 bbox 纠正),不因为数不出页数就误杀文档。
|
|
5
|
+
|
|
6
|
+
``extract_page_geometry`` 的结果直接喂给
|
|
7
|
+
``pdf_transformer.mineru.transform_zip(page_geometry=...)`` 做 bbox 旋转纠正。
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
import logging
|
|
11
|
+
from typing import List, Optional
|
|
12
|
+
|
|
13
|
+
from ..pdf_transformer.mineru.bbox_rotation import PageGeometry
|
|
14
|
+
|
|
15
|
+
try:
|
|
16
|
+
import fitz # PyMuPDF
|
|
17
|
+
HAS_PYMUPDF = True
|
|
18
|
+
except ImportError:
|
|
19
|
+
HAS_PYMUPDF = False
|
|
20
|
+
|
|
21
|
+
log = logging.getLogger(__name__)
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
# ----------------------------------------------------------
|
|
25
|
+
# Per-page geometry extraction
|
|
26
|
+
# ----------------------------------------------------------
|
|
27
|
+
#
|
|
28
|
+
# 用于 result_transformer 的 bbox 旋转纠正:mineru 的 middle.json 里
|
|
29
|
+
# `page_size` 可能是"应用了 /Rotate 之后的逻辑帧",与 PDF 原 mediabox 不同。
|
|
30
|
+
# 后处理时需要拿到原 mediabox 才能判定是否要把 bbox 反向旋转回 mediabox 帧。
|
|
31
|
+
#
|
|
32
|
+
# 这里特意 **只返回 raw mediabox 尺寸 + /Rotate 值**,不主动应用 /Rotate。
|
|
33
|
+
# fitz 的 `page.rect` 默认是"应用了 /Rotate 之后"的可视尺寸;要拿原 mediabox
|
|
34
|
+
# 必须从 `page.mediabox` 读,并显式取 width/height 元组。
|
|
35
|
+
|
|
36
|
+
__all__ = [
|
|
37
|
+
"HAS_PYMUPDF",
|
|
38
|
+
"PageGeometry",
|
|
39
|
+
"count_pdf_pages",
|
|
40
|
+
"split_pdf_pages",
|
|
41
|
+
"extract_page_geometry",
|
|
42
|
+
]
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def count_pdf_pages(pdf_bytes: bytes) -> Optional[int]:
|
|
46
|
+
"""返回 PDF 页数;无法判定时返回 None(不抛异常)。
|
|
47
|
+
|
|
48
|
+
用于 submit 前的页数门槛:页数过多的 PDF 会拖垮 Router,直接拦截。
|
|
49
|
+
解析失败 / PyMuPDF 不可用 / 加密打不开 → 返回 None,调用方应放行
|
|
50
|
+
(不因为数不出页数就误杀),交给后续阶段按原逻辑处理。
|
|
51
|
+
"""
|
|
52
|
+
if not HAS_PYMUPDF:
|
|
53
|
+
log.debug("count_pdf_pages: PyMuPDF unavailable")
|
|
54
|
+
return None
|
|
55
|
+
if not pdf_bytes:
|
|
56
|
+
return None
|
|
57
|
+
|
|
58
|
+
try:
|
|
59
|
+
with fitz.open(stream=pdf_bytes, filetype="pdf") as doc:
|
|
60
|
+
if doc.is_encrypted and not doc.authenticate(""):
|
|
61
|
+
log.debug("count_pdf_pages: encrypted PDF, skipping")
|
|
62
|
+
return None
|
|
63
|
+
return int(doc.page_count)
|
|
64
|
+
except Exception as e:
|
|
65
|
+
log.warning("count_pdf_pages failed: %s", e)
|
|
66
|
+
return None
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
def split_pdf_pages(
|
|
70
|
+
pdf_bytes: bytes, pages_per_chunk: int
|
|
71
|
+
) -> Optional[List[bytes]]:
|
|
72
|
+
"""把 PDF 按 ``pages_per_chunk`` 页切成多块,每块返回独立的 PDF 字节。
|
|
73
|
+
|
|
74
|
+
用于大 PDF 切块 + 高优先级抢占:页数过多的 PDF 会长时间独占 GPU 槽位,
|
|
75
|
+
切成小块后每块单独提交 Router,块之间腾出槽位让高优先级任务插队。
|
|
76
|
+
|
|
77
|
+
Args:
|
|
78
|
+
pdf_bytes: 原始 PDF 字节。
|
|
79
|
+
pages_per_chunk: 每块页数(> 0)。
|
|
80
|
+
|
|
81
|
+
Returns:
|
|
82
|
+
- 若成功且需要切块(总页数 > pages_per_chunk):返回按顺序排列的
|
|
83
|
+
每块 PDF 字节列表(长度 >= 2)。
|
|
84
|
+
- 若无需切块(总页数 <= pages_per_chunk):返回 None(调用方应按整篇
|
|
85
|
+
单次提交处理,不做切块)。
|
|
86
|
+
- 若 PyMuPDF 不可用 / 加密打不开 / 解析失败 / 参数非法:返回 None
|
|
87
|
+
(不抛异常;调用方回退到整篇单次提交)。
|
|
88
|
+
"""
|
|
89
|
+
if not HAS_PYMUPDF:
|
|
90
|
+
log.debug("split_pdf_pages: PyMuPDF unavailable")
|
|
91
|
+
return None
|
|
92
|
+
if not pdf_bytes or pages_per_chunk <= 0:
|
|
93
|
+
return None
|
|
94
|
+
|
|
95
|
+
try:
|
|
96
|
+
with fitz.open(stream=pdf_bytes, filetype="pdf") as doc:
|
|
97
|
+
if doc.is_encrypted and not doc.authenticate(""):
|
|
98
|
+
log.debug("split_pdf_pages: encrypted PDF, skipping")
|
|
99
|
+
return None
|
|
100
|
+
total = int(doc.page_count)
|
|
101
|
+
if total <= pages_per_chunk:
|
|
102
|
+
return None # 无需切块
|
|
103
|
+
|
|
104
|
+
chunks: List[bytes] = []
|
|
105
|
+
for start in range(0, total, pages_per_chunk):
|
|
106
|
+
end = min(start + pages_per_chunk, total) - 1
|
|
107
|
+
sub = fitz.open()
|
|
108
|
+
try:
|
|
109
|
+
sub.insert_pdf(doc, from_page=start, to_page=end)
|
|
110
|
+
chunks.append(sub.tobytes())
|
|
111
|
+
finally:
|
|
112
|
+
sub.close()
|
|
113
|
+
return chunks
|
|
114
|
+
except Exception as e:
|
|
115
|
+
log.warning("split_pdf_pages failed: %s", e)
|
|
116
|
+
return None
|
|
117
|
+
|
|
118
|
+
|
|
119
|
+
def extract_page_geometry(pdf_bytes: bytes) -> Optional[List[PageGeometry]]:
|
|
120
|
+
"""从 PDF 字节中提取每页 (mediabox_w, mediabox_h, /Rotate)。
|
|
121
|
+
|
|
122
|
+
用于后处理 bbox 旋转纠正:与 mineru.page_size 对比,发现维度交换时
|
|
123
|
+
根据 /Rotate 反向旋转 bbox。
|
|
124
|
+
|
|
125
|
+
Args:
|
|
126
|
+
pdf_bytes: PDF 文件字节
|
|
127
|
+
|
|
128
|
+
Returns:
|
|
129
|
+
每页一个 (W, H, rotate) 元组的列表;解析失败返回 None(不抛异常)。
|
|
130
|
+
None 时调用方应跳过纠正逻辑、按原 mineru 输出走。
|
|
131
|
+
"""
|
|
132
|
+
if not HAS_PYMUPDF:
|
|
133
|
+
log.debug("extract_page_geometry: PyMuPDF unavailable")
|
|
134
|
+
return None
|
|
135
|
+
if not pdf_bytes:
|
|
136
|
+
return None
|
|
137
|
+
|
|
138
|
+
try:
|
|
139
|
+
with fitz.open(stream=pdf_bytes, filetype="pdf") as doc:
|
|
140
|
+
if doc.is_encrypted and not doc.authenticate(""):
|
|
141
|
+
log.debug("extract_page_geometry: encrypted PDF, skipping")
|
|
142
|
+
return None
|
|
143
|
+
geometry: List[PageGeometry] = []
|
|
144
|
+
for page in doc:
|
|
145
|
+
# mediabox 是 raw、未应用 /Rotate 的原始页面盒
|
|
146
|
+
# fitz.Rect: (x0, y0, x1, y1)
|
|
147
|
+
mb = page.mediabox
|
|
148
|
+
width = float(abs(mb.x1 - mb.x0))
|
|
149
|
+
height = float(abs(mb.y1 - mb.y0))
|
|
150
|
+
# `page.rotation` 是 /Rotate 字段值(0/90/180/270),不是
|
|
151
|
+
# 应用过旋转的渲染状态
|
|
152
|
+
rot = int(page.rotation) % 360
|
|
153
|
+
geometry.append((width, height, rot))
|
|
154
|
+
return geometry
|
|
155
|
+
except Exception as e:
|
|
156
|
+
log.warning("extract_page_geometry failed: %s", e)
|
|
157
|
+
return None
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
"""pdf_pipeline_io:PDF 处理流水线的 I/O 工具包。
|
|
2
|
+
|
|
3
|
+
合并两块与外部系统交互的能力:
|
|
4
|
+
|
|
5
|
+
- **CDN 对象存储**(:class:`CdnUploader`):经自建 CDN 网关做对象下载 / 上传
|
|
6
|
+
(基于 ``requests``,不用 boto3),省 S3 出站费。
|
|
7
|
+
- **PG 任务队列**(:class:`DbTaskSource`):从生产 batch_worker 抽出的"更新
|
|
8
|
+
PG 业务流",拉取 / 锁定 / 状态回写;schema / 队列 / 状态由
|
|
9
|
+
:class:`TaskStoreConfig` 参数化,默认值即 mineru 生产在用的一套。
|
|
10
|
+
|
|
11
|
+
用法::
|
|
12
|
+
|
|
13
|
+
from orbitkit.pdf_pipeline_io import CdnUploader, DbTaskSource, TaskStoreConfig
|
|
14
|
+
|
|
15
|
+
with CdnUploader("https://cdn.example.com", upload_key=KEY) as cdn:
|
|
16
|
+
cdn.upload_files("my-bucket", "txt-vector/<key>", result.to_files())
|
|
17
|
+
|
|
18
|
+
db = DbTaskSource(dsn, config=TaskStoreConfig())
|
|
19
|
+
await db.connect()
|
|
20
|
+
tasks = await db.fetch_batch(limit=8)
|
|
21
|
+
"""
|
|
22
|
+
|
|
23
|
+
from .cdn import DEFAULT_CONTENT_TYPE, CdnUploader
|
|
24
|
+
from .pg_task_store import (
|
|
25
|
+
AttachmentTask,
|
|
26
|
+
DbTaskSource,
|
|
27
|
+
TaskStoreConfig,
|
|
28
|
+
)
|
|
29
|
+
|
|
30
|
+
__all__ = [
|
|
31
|
+
"CdnUploader",
|
|
32
|
+
"DEFAULT_CONTENT_TYPE",
|
|
33
|
+
"DbTaskSource",
|
|
34
|
+
"AttachmentTask",
|
|
35
|
+
"TaskStoreConfig",
|
|
36
|
+
]
|
orbitkit-0.9.12/orbitkit/cdn_uploader/uploader.py → orbitkit-0.9.22/orbitkit/pdf_pipeline_io/cdn.py
RENAMED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
"""自建 CDN 网关客户端(省 S3 出站费),基于 ``requests``,不依赖 boto3。
|
|
2
2
|
|
|
3
|
-
设了 CDN 网关(形如 ``https://
|
|
3
|
+
设了 CDN 网关(形如 ``https://cdn.example.com``,路径 ``/{bucket}/{key}``)
|
|
4
4
|
后,把对象的下载 / 上传改走网关而不是直连 S3。收益:S3→CDN 回源免费 +
|
|
5
5
|
CDN 出站费率低 / 有免费额度 → 省 egress。
|
|
6
6
|
|
|
@@ -47,7 +47,7 @@ class CdnUploader:
|
|
|
47
47
|
"""经自建 CDN 网关做对象的下载 / 上传(同步)。
|
|
48
48
|
|
|
49
49
|
Args:
|
|
50
|
-
base_url: 网关根地址,如 ``https://
|
|
50
|
+
base_url: 网关根地址,如 ``https://cdn.example.com``。
|
|
51
51
|
upload_key: 网关鉴权 key(``x-upload-key``);下载时也一并带上(网关
|
|
52
52
|
不需要会忽略)。**绝不硬编码**,由调用方从环境 / secret 传入。
|
|
53
53
|
timeout_seconds: 单次请求超时。
|
|
@@ -148,4 +148,4 @@ class CdnUploader:
|
|
|
148
148
|
return self
|
|
149
149
|
|
|
150
150
|
def __exit__(self, *exc) -> None:
|
|
151
|
-
self.close()
|
|
151
|
+
self.close()
|