Skip to content

Commit dab3a01

Browse files
committed
Add reading_flow: column-aware reading order via XY-cut
element_parse.reading_order is a flat top-to-bottom sort that interleaves columns on multi-column pages (A1, B1, A2, B2). Recover the correct order with recursive XY-cut: split at the widest whitespace valley (vertical -> columns, horizontal -> rows), so a two-column page reads A1, A2, B1, B2. flow_order returns the same index-tagged contract as reading_order (a drop-in column-aware upgrade, named to not shadow it).
1 parent 363d8a5 commit dab3a01

15 files changed

Lines changed: 364 additions & 0 deletions

File tree

README/WHATS_NEW_zh-CN.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,11 @@
11
# 本次更新 — AutoControl
22

3+
## 本次更新 (2026-06-24) — 列感知阅读顺序(XY-Cut)
4+
5+
读多列布局时*完整读完*每一列,而非交错。完整参考:[`docs/source/Zh/doc/new_features/v173_features_doc.rst`](../docs/source/Zh/doc/new_features/v173_features_doc.rst)
6+
7+
- **`flow_order` / `xy_cut` / `to_blocks`**(`AC_flow_order``AC_xy_cut`):`element_parse.reading_order` 是平面上到下排序,会交错列(读作 A1, B1, A2, B2…)。本功能以递归 XY-cut 还原正确顺序——在最宽留白谷切分(垂直 → 列、水平 → 行),故两列页面读作 A1, A2, B1, B2。`flow_order` 返回与 `reading_order` 相同的 `index` 标记契约(列感知的直接升级,且命名不遮蔽它);`xy_cut` 暴露区域树;`to_blocks` 列出叶区块。纯标准库;不导入 `PySide6`
8+
39
## 本次更新 (2026-06-24) — Grounding 自我一致性(提案共识)
410

511
把多个 grounding 提案融合成单一一致目标,并附 agreement 分数。完整参考:[`docs/source/Zh/doc/new_features/v172_features_doc.rst`](../docs/source/Zh/doc/new_features/v172_features_doc.rst)

README/WHATS_NEW_zh-TW.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,11 @@
11
# 本次更新 — AutoControl
22

3+
## 本次更新 (2026-06-24) — 欄位感知閱讀順序(XY-Cut)
4+
5+
讀多欄版面時*完整讀完*每一欄,而非交錯。完整參考:[`docs/source/Zh/doc/new_features/v173_features_doc.rst`](../docs/source/Zh/doc/new_features/v173_features_doc.rst)
6+
7+
- **`flow_order` / `xy_cut` / `to_blocks`**(`AC_flow_order``AC_xy_cut`):`element_parse.reading_order` 是平面上到下排序,會交錯欄位(讀作 A1, B1, A2, B2…)。本功能以遞迴 XY-cut 還原正確順序——在最寬留白谷切分(垂直 → 欄、水平 → 列),故兩欄頁面讀作 A1, A2, B1, B2。`flow_order` 回傳與 `reading_order` 相同的 `index` 標記契約(欄位感知的直接升級,且命名不遮蔽它);`xy_cut` 暴露區域樹;`to_blocks` 列出葉區塊。純標準函式庫;不匯入 `PySide6`
8+
39
## 本次更新 (2026-06-24) — Grounding 自我一致性(提案共識)
410

511
把多個 grounding 提案融合成單一一致目標,並附 agreement 分數。完整參考:[`docs/source/Zh/doc/new_features/v172_features_doc.rst`](../docs/source/Zh/doc/new_features/v172_features_doc.rst)

WHATS_NEW.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,11 @@
11
# What's New — AutoControl
22

3+
## What's new (2026-06-24) — Column-Aware Reading Order (XY-Cut)
4+
5+
Read multi-column layouts down each column instead of interleaving them. Full reference: [`docs/source/Eng/doc/new_features/v173_features_doc.rst`](docs/source/Eng/doc/new_features/v173_features_doc.rst).
6+
7+
- **`flow_order` / `xy_cut` / `to_blocks`** (`AC_flow_order`, `AC_xy_cut`): `element_parse.reading_order` is a flat top-to-bottom sort that interleaves columns (reads A1, B1, A2, B2…). This recovers the correct order with recursive XY-cut — split at the widest whitespace valley (vertical → columns, horizontal → rows), so a two-column page reads A1, A2, B1, B2. `flow_order` returns the same `index`-tagged contract as `reading_order` (a drop-in column-aware upgrade, named to not shadow it); `xy_cut` exposes the region tree; `to_blocks` lists the leaf blocks. Pure-stdlib; no `PySide6`.
8+
39
## What's new (2026-06-24) — Grounding Self-Consistency (Consensus Over Proposals)
410

511
Fuse several grounding proposals into one agreed target with an agreement score. Full reference: [`docs/source/Eng/doc/new_features/v172_features_doc.rst`](docs/source/Eng/doc/new_features/v172_features_doc.rst).
Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
Column-Aware Reading Order (XY-Cut)
2+
===================================
3+
4+
``element_parse.reading_order`` is a flat top-to-bottom / left-to-right sort — it interleaves
5+
columns on any multi-column page (the well-documented naive-sort failure: it reads row 1 of
6+
column A, then row 1 of column B, then row 2 of A …). ``reading_flow`` recovers the correct
7+
order with recursive **XY-cut**: it repeatedly splits the boxes at the widest whitespace valley
8+
(a vertical gutter → columns, a horizontal gutter → rows / blocks), so a two-column layout is
9+
read *down* column A fully, then column B.
10+
11+
The public flattener is named ``flow_order`` to sit *beside* — not shadow —
12+
``element_parse.reading_order``; it returns the same ``index``-tagged element contract, so it is
13+
a drop-in column-aware upgrade. Pure-stdlib geometry over plain box dicts (no image, no OCR
14+
engine); reuses ``table_grid_fill``'s box-bounds reader. Imports no ``PySide6``.
15+
16+
Headless API
17+
------------
18+
19+
.. code-block:: python
20+
21+
from je_auto_control import flow_order, xy_cut, to_blocks
22+
23+
# two columns, two rows each: reads A1, A2, B1, B2 — not A1, B1, A2, B2
24+
for element in flow_order(ocr_boxes, min_gap=12):
25+
print(element["index"], element["text"])
26+
27+
tree = xy_cut(ocr_boxes, min_gap=12) # {type, axis, children|boxes}
28+
blocks = to_blocks(tree) # leaf blocks in reading order
29+
30+
``flow_order`` returns the boxes in column-aware reading order, each tagged with an ``index``.
31+
``xy_cut`` returns the recursive region tree (each node is a ``split`` on ``axis`` ``"x"`` /
32+
``"y"`` or a ``leaf`` of boxes). ``to_blocks`` flattens the tree to its leaf blocks in order.
33+
``min_gap`` is the smallest whitespace valley treated as a column / row break.
34+
35+
Executor commands
36+
-----------------
37+
38+
``AC_flow_order`` (``boxes`` / ``min_gap`` → ``{count, elements}``) and ``AC_xy_cut``
39+
(``boxes`` / ``min_gap`` → ``{tree}``). They are exposed as the MCP tools ``ac_flow_order`` /
40+
``ac_xy_cut`` (read-only) and as the Script Builder commands **Reading Order (column-aware)** /
41+
**XY-Cut Region Tree** under **OCR**.

docs/source/Eng/eng_index.rst

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -195,6 +195,7 @@ Comprehensive guides for all AutoControl features.
195195
doc/new_features/v170_features_doc
196196
doc/new_features/v171_features_doc
197197
doc/new_features/v172_features_doc
198+
doc/new_features/v173_features_doc
198199
doc/ocr_backends/ocr_backends_doc
199200
doc/observability/observability_doc
200201
doc/operations_layer/operations_layer_doc
Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
欄位感知閱讀順序(XY-Cut)
2+
==========================
3+
4+
``element_parse.reading_order`` 是平面的上到下 / 左到右排序——在任何多欄頁面都會交錯欄位
5+
(眾所周知的 naive 排序失敗:它先讀 A 欄第 1 列、再讀 B 欄第 1 列、再讀 A 欄第 2 列…)。
6+
``reading_flow`` 以遞迴 **XY-cut** 還原正確順序:在最寬的留白谷反覆切分(垂直 gutter → 欄、
7+
水平 gutter → 列 / 區塊),因此兩欄版面會*完整讀完*A 欄,再讀 B 欄。
8+
9+
公開的展平函式命名為 ``flow_order``,以*並列*而非遮蔽 ``element_parse.reading_order``;它回傳
10+
相同的 ``index`` 標記元素契約,因此是欄位感知的直接升級。純標準函式庫幾何,作用於純框字典
11+
(不需影像、不需 OCR 引擎);重用 ``table_grid_fill`` 的框邊界讀取器。不匯入 ``PySide6``。
12+
13+
無頭 API
14+
--------
15+
16+
.. code-block:: python
17+
18+
from je_auto_control import flow_order, xy_cut, to_blocks
19+
20+
# 兩欄各兩列:讀作 A1, A2, B1, B2——而非 A1, B1, A2, B2
21+
for element in flow_order(ocr_boxes, min_gap=12):
22+
print(element["index"], element["text"])
23+
24+
tree = xy_cut(ocr_boxes, min_gap=12) # {type, axis, children|boxes}
25+
blocks = to_blocks(tree) # 依閱讀順序的葉區塊
26+
27+
``flow_order`` 以欄位感知閱讀順序回傳各框,每個帶 ``index``。``xy_cut`` 回傳遞迴區域樹
28+
(每個節點為對 ``axis`` ``"x"`` / ``"y"`` 的 ``split`` 或框的 ``leaf``)。``to_blocks`` 把樹
29+
展平為依序的葉區塊。``min_gap`` 為被視為欄 / 列分隔的最小留白谷。
30+
31+
執行器指令
32+
----------
33+
34+
``AC_flow_order``(``boxes`` / ``min_gap`` → ``{count, elements}``)與 ``AC_xy_cut``
35+
(``boxes`` / ``min_gap`` → ``{tree}``)。兩者以 MCP 工具 ``ac_flow_order`` / ``ac_xy_cut``
36+
(唯讀)及 Script Builder 指令 **Reading Order (column-aware)** / **XY-Cut Region Tree**
37+
(位於 **OCR** 分類下)形式提供。

docs/source/Zh/zh_index.rst

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -195,6 +195,7 @@ AutoControl 所有功能的完整使用指南。
195195
doc/new_features/v170_features_doc
196196
doc/new_features/v171_features_doc
197197
doc/new_features/v172_features_doc
198+
doc/new_features/v173_features_doc
198199
doc/ocr_backends/ocr_backends_doc
199200
doc/observability/observability_doc
200201
doc/operations_layer/operations_layer_doc

je_auto_control/__init__.py

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -315,6 +315,10 @@
315315
from je_auto_control.utils.column_layout import (
316316
assign_columns, column_gutters, detect_borderless_table, vertical_projection,
317317
)
318+
# Column-aware reading order via recursive XY-cut
319+
from je_auto_control.utils.reading_flow import (
320+
flow_order, to_blocks, xy_cut,
321+
)
318322
# Associate form labels with values (multi-direction) + checkbox state
319323
from je_auto_control.utils.form_fields import (
320324
associate_fields, checkbox_state, match_labels_to_widgets,
@@ -1272,6 +1276,9 @@ def start_autocontrol_gui(*args, **kwargs):
12721276
"column_gutters",
12731277
"assign_columns",
12741278
"detect_borderless_table",
1279+
"xy_cut",
1280+
"flow_order",
1281+
"to_blocks",
12751282
"associate_fields",
12761283
"match_labels_to_widgets",
12771284
"checkbox_state",

je_auto_control/gui/script_builder/command_schema.py

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -788,6 +788,25 @@ def _add_ocr_specs(specs: List[CommandSpec]) -> None:
788788
),
789789
description="Match each checkbox/radio/input to its nearest label by centre.",
790790
))
791+
specs.append(CommandSpec(
792+
"AC_flow_order", "OCR", "Reading Order (column-aware)",
793+
fields=(
794+
FieldSpec("boxes", FieldType.STRING,
795+
placeholder='[{"x":0,"y":0,"width":40,"height":20,'
796+
'"text":"A1"}]'),
797+
FieldSpec("min_gap", FieldType.INT, optional=True, default=12),
798+
),
799+
description="Column-aware reading order via XY-cut (reads down columns).",
800+
))
801+
specs.append(CommandSpec(
802+
"AC_xy_cut", "OCR", "XY-Cut Region Tree",
803+
fields=(
804+
FieldSpec("boxes", FieldType.STRING,
805+
placeholder='[{"x":0,"y":0,"width":40,"height":20}]'),
806+
FieldSpec("min_gap", FieldType.INT, optional=True, default=12),
807+
),
808+
description="Recursive XY-cut region tree (split at widest whitespace).",
809+
))
791810
specs.append(CommandSpec(
792811
"AC_scroll_to_find", "OCR", "Scroll Until Visible",
793812
fields=(

je_auto_control/utils/executor/action_executor.py

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3498,6 +3498,25 @@ def _match_labels_to_widgets(labels: Any, widgets: Any) -> Dict[str, Any]:
34983498
return {"count": len(pairs), "pairs": pairs}
34993499

35003500

3501+
def _flow_order(boxes: Any, min_gap: Any = 12) -> Dict[str, Any]:
3502+
"""Adapter: column-aware reading order of OCR boxes (XY-cut)."""
3503+
import json
3504+
from je_auto_control.utils.reading_flow import flow_order
3505+
if isinstance(boxes, str):
3506+
boxes = json.loads(boxes)
3507+
ordered = flow_order(boxes, min_gap=int(min_gap))
3508+
return {"count": len(ordered), "elements": ordered}
3509+
3510+
3511+
def _xy_cut(boxes: Any, min_gap: Any = 12) -> Dict[str, Any]:
3512+
"""Adapter: recursive XY-cut region tree of OCR boxes."""
3513+
import json
3514+
from je_auto_control.utils.reading_flow import xy_cut
3515+
if isinstance(boxes, str):
3516+
boxes = json.loads(boxes)
3517+
return {"tree": xy_cut(boxes, min_gap=int(min_gap))}
3518+
3519+
35013520
def _find_color_region(rgb: Any, tolerance: Any = 20, min_area: Any = 50,
35023521
region: Any = None) -> Dict[str, Any]:
35033522
"""Adapter: locate coloured regions on the screen, largest first."""
@@ -6033,6 +6052,8 @@ def __init__(self):
60336052
"AC_detect_borderless_table": _detect_borderless_table,
60346053
"AC_associate_fields": _associate_fields,
60356054
"AC_match_labels_to_widgets": _match_labels_to_widgets,
6055+
"AC_flow_order": _flow_order,
6056+
"AC_xy_cut": _xy_cut,
60366057
"AC_ssim_compare": _ssim_compare,
60376058
"AC_ssim_changed_regions": _ssim_changed_regions,
60386059
"AC_feature_match": _feature_match,

0 commit comments

Comments
 (0)