scrollbreaker 0.1.0__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.
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 scrollcap contributors
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,231 @@
1
+ Metadata-Version: 2.4
2
+ Name: scrollbreaker
3
+ Version: 0.1.0
4
+ Summary: Scroll-aware screen content collector for LLM agents — 把滚动的屏幕切成能读的页
5
+ Author: scrollbreaker contributors
6
+ License: MIT
7
+ Keywords: ocr,screen,scroll,llm,agent,capture,information-collector,content-collector
8
+ Classifier: Development Status :: 3 - Alpha
9
+ Classifier: Intended Audience :: Developers
10
+ Classifier: License :: OSI Approved :: MIT License
11
+ Classifier: Programming Language :: Python :: 3
12
+ Classifier: Topic :: Text Processing
13
+ Requires-Python: >=3.10
14
+ Description-Content-Type: text/markdown
15
+ License-File: LICENSE
16
+ Provides-Extra: image
17
+ Requires-Dist: pillow>=9; extra == "image"
18
+ Requires-Dist: mss>=9; extra == "image"
19
+ Provides-Extra: dev
20
+ Requires-Dist: pytest>=7; extra == "dev"
21
+ Dynamic: license-file
22
+
23
+ # ScrollBreaker
24
+
25
+ **A scroll-aware screen content collector for LLM agents** — 一个会分辨页面的信息采集器。
26
+
27
+ > **Your LLM is eating garbage. The screenshots are the problem.**
28
+ >
29
+ > Every scroll-capture tool on the planet splices frames together by line overlap.
30
+ > That works — until it silently fuses an article and a chat log into one record
31
+ > that looks perfectly normal. ScrollBreaker stops that with **four gates**.
32
+
33
+ Most **information collectors** answer the question *"how do I grab the screen?"*
34
+ ScrollBreaker answers the harder one: *"are these two grabs the same page at all?"*
35
+
36
+
37
+ Screenshot → OCR → merge is the standard pipeline for feeding visual content to an
38
+ agent. It has one failure mode that poisons your dataset and never warns you:
39
+ **unrelated pages that happen to share lines.**
40
+
41
+ ```
42
+ Article page : 正常 / 好的 / 收到 / 在看 / +关注 / body paragraph...
43
+ Chat page : 正常 / 好的 / 收到 / Alice: stop machine 4 / Bob: on it
44
+ Shared lines : 3
45
+ ```
46
+
47
+ Any tool that merges on "overlap ≥ 2" fuses these two. The result is one record
48
+ containing an article with a factory conversation stapled into its middle. It parses
49
+ fine. It embeds fine. It is **wrong**, and nothing downstream will tell you.
50
+
51
+ ScrollBreaker refuses to merge unless four gates all pass.
52
+
53
+ ## The four gates
54
+
55
+ | # | Gate | Rule | Default |
56
+ |---|------|------|---------|
57
+ | 1 | Time window | Frames further apart than N seconds are a different reading session | `180s` |
58
+ | 2 | Same type | `article` / `chat` / `list` / `text` must match | — |
59
+ | 3 | Same anchor | Same page header (relaxed for articles — titles scroll away) | — |
60
+ | 4 | Real overlap | ≥ N **identical** lines, because 1 line is noise | `2` |
61
+
62
+ Fail any one → **new record, no merge. Ever.**
63
+
64
+ ## Install
65
+
66
+ No PyPI account needed — the wheel is attached to the release:
67
+
68
+ ```bash
69
+ pip install https://github.com/xiangjianhuan007/scrollbreaker/releases/download/v0.1.0/scrollbreaker-0.1.0-py3-none-any.whl
70
+ ```
71
+
72
+ Or from source, if you want to poke at it:
73
+
74
+ ```bash
75
+ git clone https://github.com/xiangjianhuan007/scrollbreaker
76
+ cd scrollbreaker
77
+ pip install -e ".[dev]"
78
+ pytest -q
79
+ ```
80
+
81
+ Verify the install the same way CI does:
82
+
83
+ ```bash
84
+ scrollbreaker --version
85
+ scrollbreaker replay examples/frames.jsonl
86
+ ```
87
+
88
+ Zero runtime dependencies. The `image` extra (`pillow`, `mss`) is only needed if you
89
+ want to feed it real screenshots instead of OCR'd text.
90
+
91
+
92
+ There is a fifth state, `mixed`: a frame containing both article and chat/list
93
+ signatures. That frame *is* a page transition in progress, so it can neither start
94
+ an accumulation nor be absorbed into one.
95
+
96
+ ## Quick start
97
+
98
+ ```bash
99
+ pip install -e .
100
+ # for live capture: pip install mss pillow
101
+ ```
102
+
103
+ ```python
104
+ from scrollbreaker import Accumulator, Frame, Mode
105
+
106
+ acc = Accumulator() # every threshold lives in Config
107
+
108
+ for snap in screenshots: # one frame every N seconds
109
+ m = acc.push(Frame(lines=snap.lines, ts=snap.ts))
110
+
111
+ if m.mode is Mode.ROLL:
112
+ pass # same page scrolling; m.text is the full text
113
+ else:
114
+ handle_page(m.text) # page changed; previous record is complete
115
+ ```
116
+
117
+ Every decision is auditable — `m.gate_log` tells you which gate failed:
118
+
119
+ ```python
120
+ {"time_window": True, "same_type": False, "same_anchor": False,
121
+ "anchor_relaxed": True, "overlap_enough": False}
122
+ ```
123
+
124
+ ### CLI
125
+
126
+ ```bash
127
+ # offline replay — tune thresholds without capturing anything
128
+ scrollbreaker replay examples/frames.jsonl
129
+
130
+ # live capture
131
+ scrollbreaker watch --interval 10 \
132
+ --ocr-cmd tesseract {img} stdout -l eng --psm 6 \
133
+ --output records.jsonl
134
+ ```
135
+
136
+ Real output from `examples/frames.jsonl`:
137
+
138
+ ```
139
+ frame 1 new chat anchor=Alice|Bob
140
+ frame 2 roll chat anchor=Alice|Bob overlap=2 <- same conversation, merged
141
+ frame 3 new article same_type=false <- switched pages, hard break
142
+ ```
143
+
144
+ That third line is the whole point: **wrong type, no merge — no matter the overlap.**
145
+
146
+ ## Android capture app (optional)
147
+
148
+ `android/` contains the phone half of the pair: interval screen capture → dHash
149
+ dedup → refuse password/payment screens → POST the frame to an endpoint you control.
150
+
151
+ It is source, not a shipped app — you build it yourself, and **nothing leaves the
152
+ device until you enter an endpoint**. Requires Android 11+ (API 30) because
153
+ `AccessibilityService.takeScreenshot()` is the only unattended capture API.
154
+
155
+ ```
156
+ android/ 769 lines Kotlin, zero dependencies
157
+ CaptureService.kt the loop + the four gates + the privacy gate
158
+ ImageSig.kt dHash + blank detection
159
+ Uplink.kt one POST, no SDK
160
+ CaptureConfig.kt everything configurable, nothing hard-coded
161
+ MainActivity.kt settings screen (built in code, no layouts)
162
+ ```
163
+
164
+ See [android/README.md](android/README.md) for the permission trade-off, the payload
165
+ shape and a 10-line receiver.
166
+
167
+
168
+ ## Output shape
169
+
170
+ ```json
171
+ {"ts": 1000.0, "mode": "roll", "page": "chat", "anchor": "Alice|Bob",
172
+ "scene": "chat|Alice|Bob", "overlap": 2, "text": "..."}
173
+ ```
174
+
175
+ `scene` is the field that matters downstream. Group by `scene` when you store,
176
+ embed, or summarize, and unrelated captures can no longer contaminate each other.
177
+
178
+ ## Design trade-offs
179
+
180
+ **OCR is a plug-in.** The whole contract is `read(bytes) -> str`. Use tesseract,
181
+ PaddleOCR, a cloud API, a local VLM — ScrollBreaker never looks at pixels, only lines.
182
+
183
+ **Screen only, no database spelunking.** No file parsing, no process injection,
184
+ no root. You lose some fidelity versus reading the source directly; you gain
185
+ application-agnostic capture that survives app updates.
186
+
187
+ **Thresholds are empirical, not laws.** `180s / 80 lines / 2 overlap` came from real
188
+ long-form reading and group-chat scrolling. Monitoring a dashboard or a log tail?
189
+ Change `Config`.
190
+
191
+ **Chat anchors degrade gracefully.** A chat header scrolls out of view, at which point
192
+ the first line is a message, not a header. ScrollBreaker falls back to the set of
193
+ speakers seen in the frame — stable within a conversation, rarely shared across two.
194
+
195
+ ## Test suite
196
+
197
+ ```bash
198
+ pip install -e ".[dev]" && pytest -q
199
+ ```
200
+
201
+ 9 tests covering each gate. The one that matters is `test_article_to_chat_never_merged`:
202
+ two frames sharing **6 lines** with different page types must still produce two records.
203
+ It exists because that exact fusion happened and corrupted real data. Run it before
204
+ touching the merge logic.
205
+
206
+ ## Compliance
207
+
208
+ - Reads **only what is visible on screen**. No application data files, no databases,
209
+ no process memory.
210
+ - **No network, no telemetry.** Everything stays local; output is a JSONL on your disk.
211
+ - Capturing other people's screens, or redistributing captured content, may violate
212
+ local law and the target platform's terms. **Use it on your own device, your own
213
+ account, your own visible content.**
214
+ - This project provides no authentication bypass, no database decryption, and no
215
+ process injection — and will not accept PRs that add them.
216
+
217
+ ## 中文说明
218
+
219
+ **ScrollBreaker(破滚者)** —— 把滚动的屏幕切成能读的页。
220
+
221
+ 核心不是「截图 + OCR」,是**判断两屏到底是不是同一页**。绝大多数采集工具按「有
222
+ 两行重合」就拼,于是把一篇文章和一段聊天糊成一条记录——数据被污染了还看不出来。
223
+
224
+ 四道闸门(时间窗 / 类型一致 / 页头一致 / 实质重合)全部通过才允许合并,任一条不过
225
+ 就果断断开。`mixed`(一屏里同时出现文章和聊天特征)是切页中间态,既不累积也不被吸收。
226
+
227
+ 阈值都在 `Config` 里,改成你的场景就行。
228
+
229
+ ## License
230
+
231
+ MIT — do whatever you want, just don't blame us when your OCR is worse than ours.
@@ -0,0 +1,209 @@
1
+ # ScrollBreaker
2
+
3
+ **A scroll-aware screen content collector for LLM agents** — 一个会分辨页面的信息采集器。
4
+
5
+ > **Your LLM is eating garbage. The screenshots are the problem.**
6
+ >
7
+ > Every scroll-capture tool on the planet splices frames together by line overlap.
8
+ > That works — until it silently fuses an article and a chat log into one record
9
+ > that looks perfectly normal. ScrollBreaker stops that with **four gates**.
10
+
11
+ Most **information collectors** answer the question *"how do I grab the screen?"*
12
+ ScrollBreaker answers the harder one: *"are these two grabs the same page at all?"*
13
+
14
+
15
+ Screenshot → OCR → merge is the standard pipeline for feeding visual content to an
16
+ agent. It has one failure mode that poisons your dataset and never warns you:
17
+ **unrelated pages that happen to share lines.**
18
+
19
+ ```
20
+ Article page : 正常 / 好的 / 收到 / 在看 / +关注 / body paragraph...
21
+ Chat page : 正常 / 好的 / 收到 / Alice: stop machine 4 / Bob: on it
22
+ Shared lines : 3
23
+ ```
24
+
25
+ Any tool that merges on "overlap ≥ 2" fuses these two. The result is one record
26
+ containing an article with a factory conversation stapled into its middle. It parses
27
+ fine. It embeds fine. It is **wrong**, and nothing downstream will tell you.
28
+
29
+ ScrollBreaker refuses to merge unless four gates all pass.
30
+
31
+ ## The four gates
32
+
33
+ | # | Gate | Rule | Default |
34
+ |---|------|------|---------|
35
+ | 1 | Time window | Frames further apart than N seconds are a different reading session | `180s` |
36
+ | 2 | Same type | `article` / `chat` / `list` / `text` must match | — |
37
+ | 3 | Same anchor | Same page header (relaxed for articles — titles scroll away) | — |
38
+ | 4 | Real overlap | ≥ N **identical** lines, because 1 line is noise | `2` |
39
+
40
+ Fail any one → **new record, no merge. Ever.**
41
+
42
+ ## Install
43
+
44
+ No PyPI account needed — the wheel is attached to the release:
45
+
46
+ ```bash
47
+ pip install https://github.com/xiangjianhuan007/scrollbreaker/releases/download/v0.1.0/scrollbreaker-0.1.0-py3-none-any.whl
48
+ ```
49
+
50
+ Or from source, if you want to poke at it:
51
+
52
+ ```bash
53
+ git clone https://github.com/xiangjianhuan007/scrollbreaker
54
+ cd scrollbreaker
55
+ pip install -e ".[dev]"
56
+ pytest -q
57
+ ```
58
+
59
+ Verify the install the same way CI does:
60
+
61
+ ```bash
62
+ scrollbreaker --version
63
+ scrollbreaker replay examples/frames.jsonl
64
+ ```
65
+
66
+ Zero runtime dependencies. The `image` extra (`pillow`, `mss`) is only needed if you
67
+ want to feed it real screenshots instead of OCR'd text.
68
+
69
+
70
+ There is a fifth state, `mixed`: a frame containing both article and chat/list
71
+ signatures. That frame *is* a page transition in progress, so it can neither start
72
+ an accumulation nor be absorbed into one.
73
+
74
+ ## Quick start
75
+
76
+ ```bash
77
+ pip install -e .
78
+ # for live capture: pip install mss pillow
79
+ ```
80
+
81
+ ```python
82
+ from scrollbreaker import Accumulator, Frame, Mode
83
+
84
+ acc = Accumulator() # every threshold lives in Config
85
+
86
+ for snap in screenshots: # one frame every N seconds
87
+ m = acc.push(Frame(lines=snap.lines, ts=snap.ts))
88
+
89
+ if m.mode is Mode.ROLL:
90
+ pass # same page scrolling; m.text is the full text
91
+ else:
92
+ handle_page(m.text) # page changed; previous record is complete
93
+ ```
94
+
95
+ Every decision is auditable — `m.gate_log` tells you which gate failed:
96
+
97
+ ```python
98
+ {"time_window": True, "same_type": False, "same_anchor": False,
99
+ "anchor_relaxed": True, "overlap_enough": False}
100
+ ```
101
+
102
+ ### CLI
103
+
104
+ ```bash
105
+ # offline replay — tune thresholds without capturing anything
106
+ scrollbreaker replay examples/frames.jsonl
107
+
108
+ # live capture
109
+ scrollbreaker watch --interval 10 \
110
+ --ocr-cmd tesseract {img} stdout -l eng --psm 6 \
111
+ --output records.jsonl
112
+ ```
113
+
114
+ Real output from `examples/frames.jsonl`:
115
+
116
+ ```
117
+ frame 1 new chat anchor=Alice|Bob
118
+ frame 2 roll chat anchor=Alice|Bob overlap=2 <- same conversation, merged
119
+ frame 3 new article same_type=false <- switched pages, hard break
120
+ ```
121
+
122
+ That third line is the whole point: **wrong type, no merge — no matter the overlap.**
123
+
124
+ ## Android capture app (optional)
125
+
126
+ `android/` contains the phone half of the pair: interval screen capture → dHash
127
+ dedup → refuse password/payment screens → POST the frame to an endpoint you control.
128
+
129
+ It is source, not a shipped app — you build it yourself, and **nothing leaves the
130
+ device until you enter an endpoint**. Requires Android 11+ (API 30) because
131
+ `AccessibilityService.takeScreenshot()` is the only unattended capture API.
132
+
133
+ ```
134
+ android/ 769 lines Kotlin, zero dependencies
135
+ CaptureService.kt the loop + the four gates + the privacy gate
136
+ ImageSig.kt dHash + blank detection
137
+ Uplink.kt one POST, no SDK
138
+ CaptureConfig.kt everything configurable, nothing hard-coded
139
+ MainActivity.kt settings screen (built in code, no layouts)
140
+ ```
141
+
142
+ See [android/README.md](android/README.md) for the permission trade-off, the payload
143
+ shape and a 10-line receiver.
144
+
145
+
146
+ ## Output shape
147
+
148
+ ```json
149
+ {"ts": 1000.0, "mode": "roll", "page": "chat", "anchor": "Alice|Bob",
150
+ "scene": "chat|Alice|Bob", "overlap": 2, "text": "..."}
151
+ ```
152
+
153
+ `scene` is the field that matters downstream. Group by `scene` when you store,
154
+ embed, or summarize, and unrelated captures can no longer contaminate each other.
155
+
156
+ ## Design trade-offs
157
+
158
+ **OCR is a plug-in.** The whole contract is `read(bytes) -> str`. Use tesseract,
159
+ PaddleOCR, a cloud API, a local VLM — ScrollBreaker never looks at pixels, only lines.
160
+
161
+ **Screen only, no database spelunking.** No file parsing, no process injection,
162
+ no root. You lose some fidelity versus reading the source directly; you gain
163
+ application-agnostic capture that survives app updates.
164
+
165
+ **Thresholds are empirical, not laws.** `180s / 80 lines / 2 overlap` came from real
166
+ long-form reading and group-chat scrolling. Monitoring a dashboard or a log tail?
167
+ Change `Config`.
168
+
169
+ **Chat anchors degrade gracefully.** A chat header scrolls out of view, at which point
170
+ the first line is a message, not a header. ScrollBreaker falls back to the set of
171
+ speakers seen in the frame — stable within a conversation, rarely shared across two.
172
+
173
+ ## Test suite
174
+
175
+ ```bash
176
+ pip install -e ".[dev]" && pytest -q
177
+ ```
178
+
179
+ 9 tests covering each gate. The one that matters is `test_article_to_chat_never_merged`:
180
+ two frames sharing **6 lines** with different page types must still produce two records.
181
+ It exists because that exact fusion happened and corrupted real data. Run it before
182
+ touching the merge logic.
183
+
184
+ ## Compliance
185
+
186
+ - Reads **only what is visible on screen**. No application data files, no databases,
187
+ no process memory.
188
+ - **No network, no telemetry.** Everything stays local; output is a JSONL on your disk.
189
+ - Capturing other people's screens, or redistributing captured content, may violate
190
+ local law and the target platform's terms. **Use it on your own device, your own
191
+ account, your own visible content.**
192
+ - This project provides no authentication bypass, no database decryption, and no
193
+ process injection — and will not accept PRs that add them.
194
+
195
+ ## 中文说明
196
+
197
+ **ScrollBreaker(破滚者)** —— 把滚动的屏幕切成能读的页。
198
+
199
+ 核心不是「截图 + OCR」,是**判断两屏到底是不是同一页**。绝大多数采集工具按「有
200
+ 两行重合」就拼,于是把一篇文章和一段聊天糊成一条记录——数据被污染了还看不出来。
201
+
202
+ 四道闸门(时间窗 / 类型一致 / 页头一致 / 实质重合)全部通过才允许合并,任一条不过
203
+ 就果断断开。`mixed`(一屏里同时出现文章和聊天特征)是切页中间态,既不累积也不被吸收。
204
+
205
+ 阈值都在 `Config` 里,改成你的场景就行。
206
+
207
+ ## License
208
+
209
+ MIT — do whatever you want, just don't blame us when your OCR is worse than ours.
@@ -0,0 +1,34 @@
1
+ [build-system]
2
+ requires = ["setuptools>=68"]
3
+ build-backend = "setuptools.build_meta"
4
+
5
+ [project]
6
+ name = "scrollbreaker"
7
+ version = "0.1.0"
8
+ description = "Scroll-aware screen content collector for LLM agents — 把滚动的屏幕切成能读的页"
9
+ readme = "README.md"
10
+ requires-python = ">=3.10"
11
+ license = { text = "MIT" }
12
+ authors = [{ name = "scrollbreaker contributors" }]
13
+ keywords = ["ocr", "screen", "scroll", "llm", "agent", "capture", "information-collector", "content-collector"]
14
+ classifiers = [
15
+ "Development Status :: 3 - Alpha",
16
+ "Intended Audience :: Developers",
17
+ "License :: OSI Approved :: MIT License",
18
+ "Programming Language :: Python :: 3",
19
+ "Topic :: Text Processing",
20
+ ]
21
+ dependencies = []
22
+
23
+ [project.optional-dependencies]
24
+ image = ["pillow>=9", "mss>=9"]
25
+ dev = ["pytest>=7"]
26
+
27
+ [project.scripts]
28
+ scrollbreaker = "scrollbreaker.cli:main"
29
+
30
+ [tool.setuptools.packages.find]
31
+ include = ["scrollbreaker*"]
32
+
33
+ [tool.pytest.ini_options]
34
+ testpaths = ["tests"]
@@ -0,0 +1,32 @@
1
+ """scrollbreaker —— 把滚动的屏幕,切成能读的页。
2
+
3
+ 核心问题:手机/电脑上读长内容时,一屏只能看到一小段。连续截屏会有三种噪声:
4
+ 1. 屏幕没动,重复截到同一屏(浪费 token)
5
+ 2. 内容滚动了,前后两屏有一段重叠(直接拼会重复)
6
+ 3. **换页面了**,但上一屏和这一屏恰好有相同的一行(直接拼会把两件事糊成一件)
7
+
8
+ 前两个好解决,第三个才是真正会毁掉数据的问题。scrollbreaker 用四道闸门判定
9
+ 「这两屏到底是不是同一页」,只有全过才拼,否则另起一条。
10
+ """
11
+
12
+ from .config import Config, DEFAULT_CONFIG
13
+ from .pagetype import page_type, head_anchor, body_lines
14
+ from .accumulate import Accumulator, Frame, Merge, Mode
15
+ from .dedup import dhash, is_duplicate, is_blank
16
+
17
+ __all__ = [
18
+ "Config",
19
+ "DEFAULT_CONFIG",
20
+ "page_type",
21
+ "head_anchor",
22
+ "body_lines",
23
+ "Accumulator",
24
+ "Frame",
25
+ "Merge",
26
+ "Mode",
27
+ "dhash",
28
+ "is_duplicate",
29
+ "is_blank",
30
+ ]
31
+
32
+ __version__ = "0.1.0"