orbitkit 0.9.22__tar.gz → 0.9.24__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.
Files changed (109) hide show
  1. {orbitkit-0.9.22/orbitkit.egg-info → orbitkit-0.9.24}/PKG-INFO +2 -3
  2. orbitkit-0.9.24/orbitkit/VERSION +1 -0
  3. {orbitkit-0.9.22 → orbitkit-0.9.24}/orbitkit/pdf_quality/parse_record.py +9 -3
  4. {orbitkit-0.9.22 → orbitkit-0.9.24}/orbitkit/pdf_validator/__init__.py +2 -0
  5. {orbitkit-0.9.22 → orbitkit-0.9.24}/orbitkit/pdf_validator/filters/drop_vlm_artifacts.py +53 -16
  6. {orbitkit-0.9.22 → orbitkit-0.9.24}/orbitkit/pdf_validator/filters/repeated_text.py +105 -6
  7. {orbitkit-0.9.22 → orbitkit-0.9.24}/orbitkit/pdf_validator/pipeline.py +26 -1
  8. {orbitkit-0.9.22 → orbitkit-0.9.24/orbitkit.egg-info}/PKG-INFO +2 -3
  9. orbitkit-0.9.22/orbitkit/VERSION +0 -1
  10. {orbitkit-0.9.22 → orbitkit-0.9.24}/LICENSE +0 -0
  11. {orbitkit-0.9.22 → orbitkit-0.9.24}/MANIFEST.in +0 -0
  12. {orbitkit-0.9.22 → orbitkit-0.9.24}/README.md +0 -0
  13. {orbitkit-0.9.22 → orbitkit-0.9.24}/orbitkit/__init__.py +0 -0
  14. {orbitkit-0.9.22 → orbitkit-0.9.24}/orbitkit/airflow_handler/__init__.py +0 -0
  15. {orbitkit-0.9.22 → orbitkit-0.9.24}/orbitkit/airflow_handler/data_preprocessing.py +0 -0
  16. {orbitkit-0.9.22 → orbitkit-0.9.24}/orbitkit/airflow_handler/file_flow_entry.py +0 -0
  17. {orbitkit-0.9.22 → orbitkit-0.9.24}/orbitkit/airflow_handler/file_flow_entry_process.py +0 -0
  18. {orbitkit-0.9.22 → orbitkit-0.9.24}/orbitkit/airflow_handler/file_flow_exit_process.py +0 -0
  19. {orbitkit-0.9.22 → orbitkit-0.9.24}/orbitkit/airflow_handler/file_handler.py +0 -0
  20. {orbitkit-0.9.22 → orbitkit-0.9.24}/orbitkit/airflow_handler/file_handler_v2.py +0 -0
  21. {orbitkit-0.9.22 → orbitkit-0.9.24}/orbitkit/audio_transcoder/__init__.py +0 -0
  22. {orbitkit-0.9.22 → orbitkit-0.9.24}/orbitkit/audio_transcoder/netmind_extract_v1.py +0 -0
  23. {orbitkit-0.9.22 → orbitkit-0.9.24}/orbitkit/constant/__init__.py +0 -0
  24. {orbitkit-0.9.22 → orbitkit-0.9.24}/orbitkit/constant/report_schema.py +0 -0
  25. {orbitkit-0.9.22 → orbitkit-0.9.24}/orbitkit/id_srv/__init__.py +0 -0
  26. {orbitkit-0.9.22 → orbitkit-0.9.24}/orbitkit/id_srv/id_gen.py +0 -0
  27. {orbitkit-0.9.22 → orbitkit-0.9.24}/orbitkit/id_srv/id_perm_like.py +0 -0
  28. {orbitkit-0.9.22 → orbitkit-0.9.24}/orbitkit/lark_send/__init__.py +0 -0
  29. {orbitkit-0.9.22 → orbitkit-0.9.24}/orbitkit/lark_send/lark.py +0 -0
  30. {orbitkit-0.9.22 → orbitkit-0.9.24}/orbitkit/llm_tools/__init__.py +0 -0
  31. {orbitkit-0.9.22 → orbitkit-0.9.24}/orbitkit/llm_tools/quick_rag_chat.py +0 -0
  32. {orbitkit-0.9.22 → orbitkit-0.9.24}/orbitkit/orbit_type/__init__.py +0 -0
  33. {orbitkit-0.9.22 → orbitkit-0.9.24}/orbitkit/orbit_type/doc_4_compile_rule.py +0 -0
  34. {orbitkit-0.9.22 → orbitkit-0.9.24}/orbitkit/orbit_type/orbit_type_simple.py +0 -0
  35. {orbitkit-0.9.22 → orbitkit-0.9.24}/orbitkit/orbit_type/tools.py +0 -0
  36. {orbitkit-0.9.22 → orbitkit-0.9.24}/orbitkit/pdf_chunking/__init__.py +0 -0
  37. {orbitkit-0.9.22 → orbitkit-0.9.24}/orbitkit/pdf_chunking/chunk_fallback.py +0 -0
  38. {orbitkit-0.9.22 → orbitkit-0.9.24}/orbitkit/pdf_chunking/chunk_plan.py +0 -0
  39. {orbitkit-0.9.22 → orbitkit-0.9.24}/orbitkit/pdf_chunking/page_limit.py +0 -0
  40. {orbitkit-0.9.22 → orbitkit-0.9.24}/orbitkit/pdf_chunking/pdf_metadata.py +0 -0
  41. {orbitkit-0.9.22 → orbitkit-0.9.24}/orbitkit/pdf_embedding/__init__.py +0 -0
  42. {orbitkit-0.9.22 → orbitkit-0.9.24}/orbitkit/pdf_embedding/pdf_txt_embedding.py +0 -0
  43. {orbitkit-0.9.22 → orbitkit-0.9.24}/orbitkit/pdf_embedding/pdf_txt_embedding_v2.py +0 -0
  44. {orbitkit-0.9.22 → orbitkit-0.9.24}/orbitkit/pdf_extractor/__init__.py +0 -0
  45. {orbitkit-0.9.22 → orbitkit-0.9.24}/orbitkit/pdf_extractor/a_stock_extractor_v1.py +0 -0
  46. {orbitkit-0.9.22 → orbitkit-0.9.24}/orbitkit/pdf_extractor/exceptions.py +0 -0
  47. {orbitkit-0.9.22 → orbitkit-0.9.24}/orbitkit/pdf_extractor/mineru_demo.py +0 -0
  48. {orbitkit-0.9.22 → orbitkit-0.9.24}/orbitkit/pdf_extractor/pdf_block_extractor_base.py +0 -0
  49. {orbitkit-0.9.22 → orbitkit-0.9.24}/orbitkit/pdf_extractor/pdf_block_extractor_v1.py +0 -0
  50. {orbitkit-0.9.22 → orbitkit-0.9.24}/orbitkit/pdf_extractor/pdf_block_extractor_v2.py +0 -0
  51. {orbitkit-0.9.22 → orbitkit-0.9.24}/orbitkit/pdf_extractor/pdf_extractor_azure.py +0 -0
  52. {orbitkit-0.9.22 → orbitkit-0.9.24}/orbitkit/pdf_extractor/pdf_extractor_minerU_v1.py +0 -0
  53. {orbitkit-0.9.22 → orbitkit-0.9.24}/orbitkit/pdf_extractor/pdf_extractor_netmind_v1.py +0 -0
  54. {orbitkit-0.9.22 → orbitkit-0.9.24}/orbitkit/pdf_extractor/pdf_extractor_netmind_v2.py +0 -0
  55. {orbitkit-0.9.22 → orbitkit-0.9.24}/orbitkit/pdf_extractor/pdf_extractor_netmind_v3.py +0 -0
  56. {orbitkit-0.9.22 → orbitkit-0.9.24}/orbitkit/pdf_extractor/pdf_extractor_orbit.py +0 -0
  57. {orbitkit-0.9.22 → orbitkit-0.9.24}/orbitkit/pdf_extractor_simple/__init__.py +0 -0
  58. {orbitkit-0.9.22 → orbitkit-0.9.24}/orbitkit/pdf_extractor_simple/base.py +0 -0
  59. {orbitkit-0.9.22 → orbitkit-0.9.24}/orbitkit/pdf_extractor_simple/cloud_provider.py +0 -0
  60. {orbitkit-0.9.22 → orbitkit-0.9.24}/orbitkit/pdf_extractor_simple/core.py +0 -0
  61. {orbitkit-0.9.22 → orbitkit-0.9.24}/orbitkit/pdf_extractor_simple/exceptions.py +0 -0
  62. {orbitkit-0.9.22 → orbitkit-0.9.24}/orbitkit/pdf_extractor_simple/extractors.py +0 -0
  63. {orbitkit-0.9.22 → orbitkit-0.9.24}/orbitkit/pdf_extractor_simple/utils.py +0 -0
  64. {orbitkit-0.9.22 → orbitkit-0.9.24}/orbitkit/pdf_pipeline_io/__init__.py +0 -0
  65. {orbitkit-0.9.22 → orbitkit-0.9.24}/orbitkit/pdf_pipeline_io/cdn.py +0 -0
  66. {orbitkit-0.9.22 → orbitkit-0.9.24}/orbitkit/pdf_pipeline_io/pg_task_store.py +0 -0
  67. {orbitkit-0.9.22 → orbitkit-0.9.24}/orbitkit/pdf_quality/__init__.py +0 -0
  68. {orbitkit-0.9.22 → orbitkit-0.9.24}/orbitkit/pdf_quality/failure_codes.py +0 -0
  69. {orbitkit-0.9.22 → orbitkit-0.9.24}/orbitkit/pdf_quality/ocr_lang.py +0 -0
  70. {orbitkit-0.9.22 → orbitkit-0.9.24}/orbitkit/pdf_quality/verdict.py +0 -0
  71. {orbitkit-0.9.22 → orbitkit-0.9.24}/orbitkit/pdf_transformer/__init__.py +0 -0
  72. {orbitkit-0.9.22 → orbitkit-0.9.24}/orbitkit/pdf_transformer/mineru/__init__.py +0 -0
  73. {orbitkit-0.9.22 → orbitkit-0.9.24}/orbitkit/pdf_transformer/mineru/backends.py +0 -0
  74. {orbitkit-0.9.22 → orbitkit-0.9.24}/orbitkit/pdf_transformer/mineru/bbox_rotation.py +0 -0
  75. {orbitkit-0.9.22 → orbitkit-0.9.24}/orbitkit/pdf_transformer/mineru/result_transformer.py +0 -0
  76. {orbitkit-0.9.22 → orbitkit-0.9.24}/orbitkit/pdf_validator/filters/__init__.py +0 -0
  77. {orbitkit-0.9.22 → orbitkit-0.9.24}/orbitkit/pdf_validator/filters/drop_invalid_bbox.py +0 -0
  78. {orbitkit-0.9.22 → orbitkit-0.9.24}/orbitkit/pdf_validator/filters/drop_non_text.py +0 -0
  79. {orbitkit-0.9.22 → orbitkit-0.9.24}/orbitkit/pdf_validator/registry.py +0 -0
  80. {orbitkit-0.9.22 → orbitkit-0.9.24}/orbitkit/pdf_validator/validate.py +0 -0
  81. {orbitkit-0.9.22 → orbitkit-0.9.24}/orbitkit/pdf_writer/__init__.py +0 -0
  82. {orbitkit-0.9.22 → orbitkit-0.9.24}/orbitkit/pdf_writer/pdf_writer_simple.py +0 -0
  83. {orbitkit-0.9.22 → orbitkit-0.9.24}/orbitkit/util/__init__.py +0 -0
  84. {orbitkit-0.9.22 → orbitkit-0.9.24}/orbitkit/util/cache_asset_downloader.py +0 -0
  85. {orbitkit-0.9.22 → orbitkit-0.9.24}/orbitkit/util/common.py +0 -0
  86. {orbitkit-0.9.22 → orbitkit-0.9.24}/orbitkit/util/customize_regix_manager.py +0 -0
  87. {orbitkit-0.9.22 → orbitkit-0.9.24}/orbitkit/util/is_xbrl_structure.py +0 -0
  88. {orbitkit-0.9.22 → orbitkit-0.9.24}/orbitkit/util/secret_manager.py +0 -0
  89. {orbitkit-0.9.22 → orbitkit-0.9.24}/orbitkit/util/universal_extractor.py +0 -0
  90. {orbitkit-0.9.22 → orbitkit-0.9.24}/orbitkit/util/util_aliyun.py +0 -0
  91. {orbitkit-0.9.22 → orbitkit-0.9.24}/orbitkit/util/util_aliyun_oss_simple.py +0 -0
  92. {orbitkit-0.9.22 → orbitkit-0.9.24}/orbitkit/util/util_aws.py +0 -0
  93. {orbitkit-0.9.22 → orbitkit-0.9.24}/orbitkit/util/util_aws_s3_wrapper.py +0 -0
  94. {orbitkit-0.9.22 → orbitkit-0.9.24}/orbitkit/util/util_date.py +0 -0
  95. {orbitkit-0.9.22 → orbitkit-0.9.24}/orbitkit/util/util_html.py +0 -0
  96. {orbitkit-0.9.22 → orbitkit-0.9.24}/orbitkit/util/util_kafka.py +0 -0
  97. {orbitkit-0.9.22 → orbitkit-0.9.24}/orbitkit/util/util_md5.py +0 -0
  98. {orbitkit-0.9.22 → orbitkit-0.9.24}/orbitkit/util/util_selenium.py +0 -0
  99. {orbitkit-0.9.22 → orbitkit-0.9.24}/orbitkit/util/util_simple_timer.py +0 -0
  100. {orbitkit-0.9.22 → orbitkit-0.9.24}/orbitkit/util/util_str.py +0 -0
  101. {orbitkit-0.9.22 → orbitkit-0.9.24}/orbitkit/util/util_type_mapping.py +0 -0
  102. {orbitkit-0.9.22 → orbitkit-0.9.24}/orbitkit/util/util_url.py +0 -0
  103. {orbitkit-0.9.22 → orbitkit-0.9.24}/orbitkit.egg-info/SOURCES.txt +0 -0
  104. {orbitkit-0.9.22 → orbitkit-0.9.24}/orbitkit.egg-info/dependency_links.txt +0 -0
  105. {orbitkit-0.9.22 → orbitkit-0.9.24}/orbitkit.egg-info/not-zip-safe +0 -0
  106. {orbitkit-0.9.22 → orbitkit-0.9.24}/orbitkit.egg-info/requires.txt +0 -0
  107. {orbitkit-0.9.22 → orbitkit-0.9.24}/orbitkit.egg-info/top_level.txt +0 -0
  108. {orbitkit-0.9.22 → orbitkit-0.9.24}/setup.cfg +0 -0
  109. {orbitkit-0.9.22 → orbitkit-0.9.24}/setup.py +0 -0
@@ -1,6 +1,6 @@
1
- Metadata-Version: 2.4
1
+ Metadata-Version: 2.2
2
2
  Name: orbitkit
3
- Version: 0.9.22
3
+ Version: 0.9.24
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
@@ -45,7 +45,6 @@ Dynamic: description
45
45
  Dynamic: description-content-type
46
46
  Dynamic: home-page
47
47
  Dynamic: license
48
- Dynamic: license-file
49
48
  Dynamic: maintainer
50
49
  Dynamic: maintainer-email
51
50
  Dynamic: platform
@@ -0,0 +1 @@
1
+ 0.9.24
@@ -15,8 +15,8 @@
15
15
  parsing_tool_ext 工具专属:mineru 用
16
16
  {"mode", "vlm_rd_rate", "vlm_rd_threshold",
17
17
  "bbox_error_rate", "bbox_error_threshold",
18
- "vlm_rd_samples", "fallback_lang", "fallback_method",
19
- "fallback"}
18
+ "vlm_rd_samples", "vlm_artifact_samples",
19
+ "fallback_lang", "fallback_method", "fallback"}
20
20
 
21
21
  质量闸门(三道,任一超标即未过;失败码按下面的优先级取第一个超标的):
22
22
  1. ``self_heal_error_rate > self_heal_threshold``(坏块率)→ 30007,可兜底重投
@@ -35,6 +35,10 @@
35
35
  文本片段):判废只给一个率时,「预定 0.05 是否合理」无法复核,因为无法区分
36
36
  真退化和页眉/表头那种正常重复。没命中时不写这个键。
37
37
 
38
+ ``vlm_artifact_samples`` 同理,是几条被 ``drop_vlm_artifacts`` 丢弃的块(页码 /
39
+ 命中短语 / 截断片段):坏块率超阀时能核「是真幻觉还是误伤正文」,也是提炼新
40
+ anchor 的原料。没丢过不写这个键。
41
+
38
42
  ``fallback`` 是兜底结果(:class:`FallbackTrace`,一个对象):没兜过不写这个键。
39
43
  其它字段只描述**最终采用**的那份产出 —— 兜底重投了但没救回、保留首轮结论
40
44
  时,``mode`` 仍是首轮引擎,只有这里能看出「试过、没成」及原因。
@@ -252,6 +256,8 @@ def build_parse_record(
252
256
  }
253
257
  if samples:
254
258
  ext["vlm_rd_samples"] = [dict(s) for s in samples]
259
+ if stats.artifact_samples:
260
+ ext["vlm_artifact_samples"] = [dict(s) for s in stats.artifact_samples]
255
261
  if fallback_lang:
256
262
  ext["fallback_lang"] = fallback_lang
257
263
  if fallback_method:
@@ -269,4 +275,4 @@ def build_parse_record(
269
275
  bbox_error_threshold=bbox_error_threshold,
270
276
  degenerate_samples=samples,
271
277
  parsing_tool_ext=ext,
272
- )
278
+ )
@@ -34,6 +34,7 @@ from .filters.repeated_text import (
34
34
  trim_repeated_text,
35
35
  )
36
36
  from .pipeline import (
37
+ ARTIFACT_SAMPLE_LIMIT,
37
38
  DEGENERATE_SAMPLE_LIMIT,
38
39
  BlockFilter,
39
40
  DropHook,
@@ -66,6 +67,7 @@ __all__ = [
66
67
  "SequenceFilter",
67
68
  "DropHook",
68
69
  "FilterStats",
70
+ "ARTIFACT_SAMPLE_LIMIT",
69
71
  "DEGENERATE_SAMPLE_LIMIT",
70
72
  "Sample",
71
73
  "apply_block_filters",
@@ -25,45 +25,82 @@ mineru VLM 偶发把训练时的 chain-of-thought / 评估文本当成"识别结
25
25
  **anchor 短语** —— 单次命中即丢弃。anchor 必须是"真实文档绝不
26
26
  出现"的措辞,零误报。
27
27
 
28
- 判定流程:先查 anchor(单次命中即 drop);再查 soft list(≥2 命中即 drop)。
28
+ 判定流程:先查 anchor(单次命中即 drop);再查 soft list(≥2 个**不同短语**
29
+ 命中、且其中至少 1 个是**强短语**即 drop)。
30
+
31
+ 软短语分强 / 弱:
32
+
33
+ - 强 = 自指 / 评估措辞,真实文档几乎不出现("Ground Truth"、"hallucinated"、
34
+ "OCR output"…);
35
+ - 弱 = 真实文档里常见的措辞("Rule 13.25A"、"inconsistent with"——港交所公告、
36
+ 合同、法律条款里成对出现是常态)。弱短语只能给强短语"补票",两个弱短语同现
37
+ 不判死。
38
+
39
+ "不同短语"按**短语(正则)**计数,不按匹配到的字符串:``rule 13`` 与 ``rule 17``
40
+ 是同一条短语命中两次,只算 1。
29
41
  """
30
42
 
31
43
  import re
32
- from typing import Optional
44
+ from typing import List, Optional
33
45
 
34
46
  # Anchor 短语:真实文档不会出现的 VLM-only 自述句式。单次命中即丢弃。
35
47
  # 添加新 anchor 前必须 grep 历史归档样本(>10k PDFs)确认零命中。
36
48
  _VLM_ANCHOR_PHRASES = [
37
49
  r"\bThe image contains? no text\b",
50
+ r"\bThe OCR has hallucinated\b",
51
+ r"\binconsistent with the Ground Truth\b",
52
+ r"\bThe provided OCR (?:content|result|output)\b",
53
+ r"\bUNDERSCORE & LINE RULES\b",
38
54
  ]
39
55
 
40
- # 软阈值短语:自指 / 评估 / 规则引用。单条可能在真实文档出现,
41
- # 需 ≥ _MIN_HITS 个不同短语共同命中才丢。
42
- _VLM_PHRASES = [
56
+ # 强软短语:自指 / 评估措辞。
57
+ _VLM_STRONG_PHRASES = [
43
58
  r"\bGround Truth\b",
44
- r"\bOCR (?:result|content|output|has|must)\b",
59
+ r"\bOCR (?:result|content|output)\b",
60
+ r"\bOCR (?:has|must)\b",
45
61
  r"\bviolating the rule\b",
46
62
  r"\bhallucinat(?:ed|ing|ion)\b",
47
- r"\bRule \d+",
48
- r"\binconsistent with\b",
49
63
  r"\bplaceholder underscore\b",
50
64
  r"\bprovided OCR\b",
51
65
  r"\bstylistic line\b",
52
66
  ]
53
67
 
68
+ # 弱软短语:真实文档常见,只能配合强短语凑数。
69
+ _VLM_WEAK_PHRASES = [
70
+ r"\bRule \d+",
71
+ r"\binconsistent with\b",
72
+ ]
73
+
54
74
  _VLM_ANCHOR_RE = re.compile("|".join(_VLM_ANCHOR_PHRASES), re.IGNORECASE)
55
- _VLM_RE = re.compile("|".join(_VLM_PHRASES), re.IGNORECASE)
75
+ _VLM_STRONG_RES = [re.compile(p, re.IGNORECASE) for p in _VLM_STRONG_PHRASES]
76
+ _VLM_WEAK_RES = [re.compile(p, re.IGNORECASE) for p in _VLM_WEAK_PHRASES]
56
77
  _MIN_HITS = 2 # 软阈值:不同短语命中数
57
78
  _MIN_TEXT_LEN = 50 # 短文本不可能塞下 2 个不同短语
58
79
 
59
80
 
60
- def drop_vlm_artifacts(block: dict) -> Optional[dict]:
61
- text = block.get("sentence") or ""
81
+ def vlm_artifact_hits(text: str) -> List[str]:
82
+ """判为 VLM 幻觉时命中的短语(每条短语取第 1 次匹配的原文),未判中返回空列表。
83
+
84
+ 供 filter 本身与丢弃样本记录(``x_parse_record.vlm_artifact_samples``)共用,
85
+ 保证"为什么被丢"与"是否被丢"是同一套判定。
86
+ """
62
87
  if len(text) < _MIN_TEXT_LEN:
63
- return block
64
- if _VLM_ANCHOR_RE.search(text):
65
- return None
66
- hits = {m.group(0).lower() for m in _VLM_RE.finditer(text)}
88
+ return []
89
+ anchor = _VLM_ANCHOR_RE.search(text)
90
+ if anchor:
91
+ return [anchor.group(0)]
92
+ strong = [m.group(0) for m in (r.search(text) for r in _VLM_STRONG_RES) if m]
93
+ if not strong:
94
+ return []
95
+ weak = [m.group(0) for m in (r.search(text) for r in _VLM_WEAK_RES) if m]
96
+ hits = strong + weak
67
97
  if len(hits) >= _MIN_HITS:
98
+ return hits
99
+ return []
100
+
101
+
102
+ def drop_vlm_artifacts(block: dict) -> Optional[dict]:
103
+ text = block.get("sentence") or ""
104
+ if vlm_artifact_hits(text):
68
105
  return None
69
- return block
106
+ return block
@@ -32,6 +32,19 @@ VLM/OCR 偶发在一个 block 里把某个短语连续复读很多次,例如::
32
32
  filter**,因此质量闸门的复读退化率始终反映真实情况。它同时给出每个退化块的
33
33
  **命中详情**(复读短语 / 重复次数 / 文本片段),进 ``x_parse_record`` 后
34
34
  「退化率 0.054 该不该判死」才成为可复核的判断,而不是只有一个数字。
35
+
36
+ **不算复读的重复**(表格 / 图表 markdown 里成行出现是常态,不是模型退化):
37
+
38
+ 1. markdown 表格分隔行 ``| --- | --- | --- |``——语法而非内容,整行不参与判定;
39
+ 2. 占位符 / 空值 token(``-`` ``—`` ``na`` ``N/A`` ``none`` ``|`` 纯标点等)
40
+ 对检测**透明**:既不计入短语,也不打断短语;
41
+ 3. 复读短语必须含至少一个**实词**(≥2 个拉丁字母,或 ≥1 个汉字 / 假名 / 谚文),
42
+ 纯数字、纯符号的重复(``0 0 0 0``、``... ... ...``)不算。CJK 按**单字**算实词:
43
+ 韩文按音节、VLM 退化时按单字吐(``的 的 的``、``의 이 의 이``),都是真复读。
44
+
45
+ **token 内复读**:同一字符在一个 token 里连续 ≥ ``_MIN_CHAR_RUN`` 次(``5000000…``、
46
+ ``————…``)没有空白,按词切不出重复;切词时把这种连续段拆成逐字 token,走同一套
47
+ 结构检测(不受实词 / 占位符约束),trim 后只剩首次出现的那个字符。
35
48
  """
36
49
 
37
50
  from __future__ import annotations
@@ -57,6 +70,29 @@ _MIN_BLOCK_WORDS = 4
57
70
 
58
71
  _WORD_RE = re.compile(r"\S+")
59
72
 
73
+ # markdown 表格分隔行:| --- | :---: | ---: |(也允许无首尾竖线)
74
+ _MD_SEPARATOR_LINE_RE = re.compile(r"^\s*\|?\s*:?-{2,}:?\s*(\|\s*:?-{2,}:?\s*)*\|?\s*$")
75
+
76
+ # 占位符 / 空值 token(小写比较):表格里成行出现是常态,对复读检测透明
77
+ _PLACEHOLDER_TOKENS = frozenset(
78
+ {
79
+ "na", "n/a", "n.a.", "n.a", "nan", "none", "non", "null", "nil",
80
+ "nm", "n.m.", "tbd", "tba", "n/d", "nd", "无", "暂无", "不适用",
81
+ "0", "0.0", "0.00", "0%", "0.0%", "0.00%",
82
+ }
83
+ )
84
+
85
+ # 实词:≥2 个连续字母(拉丁 / 西里尔 / 带重音等),或 ≥1 个 CJK 字符(汉字 / 假名 /
86
+ # 谚文音节)——CJK 单字就是词,不能要求 ≥2。
87
+ _REAL_WORD_RE = re.compile(
88
+ r"[^\W\d_]{2,}"
89
+ r"|[\u3040-\u30ff\u3400-\u4dbf\u4e00-\u9fff\uac00-\ud7af\uf900-\ufaff\U00020000-\U0002ffff]"
90
+ )
91
+
92
+ # token 内同一字符连续这么多次算退化(一个 "5000000000" 的真实数字也就 10 个 0)
93
+ _MIN_CHAR_RUN = 20
94
+ _CHAR_RUN_RE = re.compile(r"(.)\1{%d,}" % (_MIN_CHAR_RUN - 1))
95
+
60
96
  # 样本片段截断长度:只为让人判断「是真复读还是页眉/表头」,不做内容留存
61
97
  _SAMPLE_CHARS = 160
62
98
 
@@ -78,6 +114,69 @@ def _tokenize(text: str) -> Tuple[List[str], List[Tuple[int, int]]]:
78
114
  return words, spans
79
115
 
80
116
 
117
+ def _is_placeholder(word: str) -> bool:
118
+ """占位符 / 空值 / 纯标点 token:对复读检测透明。"""
119
+ if not any(ch.isalnum() for ch in word):
120
+ return True # 纯符号:| - — … / * ○ √ × ...
121
+ return word.lower() in _PLACEHOLDER_TOKENS
122
+
123
+
124
+ def _content_tokens(text: str) -> Tuple[List[str], List[Tuple[int, int]]]:
125
+ """复读检测用的词序列:剔除 markdown 分隔行与占位符 token。
126
+
127
+ 返回的 spans 仍是**原串**字符区间,trim 据此在原串上挖段;被剔除的 token
128
+ 不出现在序列里,因此既不计入短语也不打断短语。
129
+ """
130
+ words: List[str] = []
131
+ spans: List[Tuple[int, int]] = []
132
+ offset = 0
133
+ for line in text.splitlines(keepends=True):
134
+ if not _MD_SEPARATOR_LINE_RE.match(line):
135
+ for m in _WORD_RE.finditer(line):
136
+ word = m.group(0)
137
+ if _CHAR_RUN_RE.search(word):
138
+ _split_char_runs(word, offset + m.start(), words, spans)
139
+ continue
140
+ if _is_placeholder(word):
141
+ continue
142
+ words.append(word)
143
+ spans.append((offset + m.start(), offset + m.end()))
144
+ offset += len(line)
145
+ return words, spans
146
+
147
+
148
+ def _split_char_runs(
149
+ word: str, base: int, words: List[str], spans: List[Tuple[int, int]]
150
+ ) -> None:
151
+ """把含同字符连续段的 token 拆成:前缀 / 逐字的连续段 / 后缀(可多段)。
152
+
153
+ 连续段的字符不走占位符过滤:``0``、``-`` 在普通位置是占位符,连写二十个就是退化。
154
+ """
155
+ pos = 0
156
+ for m in _CHAR_RUN_RE.finditer(word):
157
+ if m.start() > pos:
158
+ words.append(word[pos : m.start()])
159
+ spans.append((base + pos, base + m.start()))
160
+ for k in range(m.start(), m.end()):
161
+ words.append(word[k])
162
+ spans.append((base + k, base + k + 1))
163
+ pos = m.end()
164
+ if pos < len(word):
165
+ words.append(word[pos:])
166
+ spans.append((base + pos, base + len(word)))
167
+
168
+
169
+ def _has_real_word(phrase: Sequence[str]) -> bool:
170
+ return any(_REAL_WORD_RE.search(w) for w in phrase)
171
+
172
+
173
+ def _counts_as_repeat(phrase: Sequence[str], reps: int) -> bool:
174
+ """短语含实词,或是单字符连续 ≥ ``_MIN_CHAR_RUN`` 次(token 内复读拆出来的形态)。"""
175
+ if _has_real_word(phrase):
176
+ return True
177
+ return len(phrase) == 1 and len(phrase[0]) == 1 and reps >= _MIN_CHAR_RUN
178
+
179
+
81
180
  def _find_runaway_repeats(
82
181
  words: List[str], *, max_n: int, min_reps: int
83
182
  ) -> List[Tuple[int, int, int]]:
@@ -99,7 +198,7 @@ def _find_runaway_repeats(
99
198
  while j + n <= n_words and words[j : j + n] == words[i : i + n]:
100
199
  reps += 1
101
200
  j += n
102
- if reps >= min_reps:
201
+ if reps >= min_reps and _counts_as_repeat(words[i : i + n], reps):
103
202
  candidates.append((reps * n, i, n))
104
203
  reps_of[(i, n)] = reps
105
204
  i = j # 跳过整段,避免重复检测子段
@@ -139,7 +238,7 @@ def has_runaway_repetition(
139
238
  只判定、不改文本,供统计「退化块占比」用。短于 ``min_words`` 的文本一律
140
239
  返回 ``False``(真实短句可能有正常重复)。
141
240
  """
142
- words, _ = _tokenize(text or "")
241
+ words, _ = _content_tokens(text or "")
143
242
  if len(words) < min_words:
144
243
  return False
145
244
  return bool(_find_runaway_repeats(words, max_n=max_n, min_reps=min_reps))
@@ -215,7 +314,7 @@ def find_degenerate_blocks(blocks: Sequence[dict]) -> Dict[int, Sample]:
215
314
  found: Dict[int, Sample] = {}
216
315
  for i, b in enumerate(blocks):
217
316
  text = b.get("sentence") or ""
218
- words, spans = _tokenize(text)
317
+ words, spans = _content_tokens(text)
219
318
  if len(words) < _MIN_WORDS:
220
319
  continue
221
320
  run = _find_runaway_repeat(words, max_n=_MAX_N, min_reps=_MIN_REPS)
@@ -297,7 +396,7 @@ def trim_repeated_text(block: dict) -> Optional[dict]:
297
396
  (可能是合法用词),删掉其余重复。
298
397
  """
299
398
  text = block.get("sentence") or ""
300
- words, spans = _tokenize(text)
399
+ words, spans = _content_tokens(text)
301
400
  if len(words) < _MIN_WORDS:
302
401
  return block
303
402
 
@@ -321,7 +420,7 @@ def trim_repeated_text(block: dict) -> Optional[dict]:
321
420
  def drop_repeated_text(block: dict) -> Optional[dict]:
322
421
  """复读段主导整块(覆盖词数占比 ≥ 阈值)时整块丢弃,否则保留。"""
323
422
  text = block.get("sentence") or ""
324
- words, _spans = _tokenize(text)
423
+ words, _spans = _content_tokens(text)
325
424
  if len(words) < _MIN_WORDS:
326
425
  return block
327
426
 
@@ -332,4 +431,4 @@ def drop_repeated_text(block: dict) -> Optional[dict]:
332
431
  covered = sum(n * reps for _start, n, reps in runs)
333
432
  if covered / len(words) >= _DROP_COVER_FRACTION:
334
433
  return None
335
- return block
434
+ return block
@@ -28,10 +28,15 @@ from dataclasses import dataclass, field
28
28
  from typing import Callable, Dict, List, Optional, Tuple
29
29
 
30
30
  from .filters.drop_invalid_bbox import has_bbox_error
31
+ from .filters.drop_vlm_artifacts import drop_vlm_artifacts, vlm_artifact_hits
31
32
  from .filters.repeated_text import Sample, find_degenerate_blocks
32
33
 
33
34
  # x_parse_record 里最多带几条退化样本(够人工判断形态,不把 JSONB 当内容库)
34
35
  DEGENERATE_SAMPLE_LIMIT = 3
36
+ # 同上:最多带几条被 drop_vlm_artifacts 丢弃的块样本(供复核假阳 / 提炼新 anchor)
37
+ ARTIFACT_SAMPLE_LIMIT = 3
38
+ _ARTIFACT_SNIPPET_CHARS = 200
39
+ _VLM_ARTIFACT_FILTER = drop_vlm_artifacts.__name__
35
40
 
36
41
  log = logging.getLogger(__name__)
37
42
 
@@ -61,6 +66,9 @@ class FilterStats:
61
66
  (超阀 → 30005)
62
67
  - ``degenerate_samples``:最多 ``DEGENERATE_SAMPLE_LIMIT`` 条命中详情(复读
63
68
  短语 / 重复次数 / 截断的文本片段),让 ``vlm_rd_rate`` 可人工复核。
69
+ - ``artifact_samples``:最多 ``ARTIFACT_SAMPLE_LIMIT`` 条被 ``drop_vlm_artifacts``
70
+ 丢弃的块(页码 / 命中短语 / 截断的文本片段),让坏块率可人工复核:是真
71
+ 幻觉还是被误伤的正文。
64
72
  """
65
73
 
66
74
  total_in: int = 0
@@ -68,6 +76,7 @@ class FilterStats:
68
76
  bbox_error: int = 0
69
77
  degenerate: int = 0
70
78
  degenerate_samples: List[Sample] = field(default_factory=list)
79
+ artifact_samples: List[Sample] = field(default_factory=list)
71
80
 
72
81
  @property
73
82
  def dropped_total(self) -> int:
@@ -94,6 +103,20 @@ def collect_stats(blocks: List[Block], stats: FilterStats) -> FilterStats:
94
103
  return stats
95
104
 
96
105
 
106
+ def _record_artifact_sample(block: Block, stats: FilterStats) -> None:
107
+ if len(stats.artifact_samples) >= ARTIFACT_SAMPLE_LIMIT:
108
+ return
109
+ text = block.get("sentence") or ""
110
+ page = block.get("page")
111
+ stats.artifact_samples.append(
112
+ {
113
+ "page": page if isinstance(page, int) else None,
114
+ "hits": " | ".join(vlm_artifact_hits(text)),
115
+ "snippet": text[:_ARTIFACT_SNIPPET_CHARS],
116
+ }
117
+ )
118
+
119
+
97
120
  def apply_block_filters(
98
121
  blocks: List[Block],
99
122
  pages: List[Page],
@@ -152,6 +175,8 @@ def apply_block_filters(
152
175
  name = dropped_by or "anonymous"
153
176
  if stats is not None:
154
177
  stats.dropped[name] = stats.dropped.get(name, 0) + 1
178
+ if name == _VLM_ARTIFACT_FILTER:
179
+ _record_artifact_sample(b, stats)
155
180
  if on_drop is not None:
156
181
  try:
157
182
  on_drop(name, str(b.get("type") or ""))
@@ -191,4 +216,4 @@ def apply_block_filters(
191
216
  )
192
217
  rebuilt_pages.append(new_p)
193
218
 
194
- return surviving, rebuilt_pages
219
+ return surviving, rebuilt_pages
@@ -1,6 +1,6 @@
1
- Metadata-Version: 2.4
1
+ Metadata-Version: 2.2
2
2
  Name: orbitkit
3
- Version: 0.9.22
3
+ Version: 0.9.24
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
@@ -45,7 +45,6 @@ Dynamic: description
45
45
  Dynamic: description-content-type
46
46
  Dynamic: home-page
47
47
  Dynamic: license
48
- Dynamic: license-file
49
48
  Dynamic: maintainer
50
49
  Dynamic: maintainer-email
51
50
  Dynamic: platform
@@ -1 +0,0 @@
1
- 0.9.22
File without changes
File without changes
File without changes
File without changes
File without changes