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.
- scrollbreaker-0.1.0/LICENSE +21 -0
- scrollbreaker-0.1.0/PKG-INFO +231 -0
- scrollbreaker-0.1.0/README.md +209 -0
- scrollbreaker-0.1.0/pyproject.toml +34 -0
- scrollbreaker-0.1.0/scrollbreaker/__init__.py +32 -0
- scrollbreaker-0.1.0/scrollbreaker/accumulate.py +204 -0
- scrollbreaker-0.1.0/scrollbreaker/cli.py +180 -0
- scrollbreaker-0.1.0/scrollbreaker/config.py +88 -0
- scrollbreaker-0.1.0/scrollbreaker/dedup.py +110 -0
- scrollbreaker-0.1.0/scrollbreaker/ocr.py +91 -0
- scrollbreaker-0.1.0/scrollbreaker/pagetype.py +165 -0
- scrollbreaker-0.1.0/scrollbreaker.egg-info/PKG-INFO +231 -0
- scrollbreaker-0.1.0/scrollbreaker.egg-info/SOURCES.txt +17 -0
- scrollbreaker-0.1.0/scrollbreaker.egg-info/dependency_links.txt +1 -0
- scrollbreaker-0.1.0/scrollbreaker.egg-info/entry_points.txt +2 -0
- scrollbreaker-0.1.0/scrollbreaker.egg-info/requires.txt +7 -0
- scrollbreaker-0.1.0/scrollbreaker.egg-info/top_level.txt +1 -0
- scrollbreaker-0.1.0/setup.cfg +4 -0
- scrollbreaker-0.1.0/tests/test_accumulate.py +108 -0
|
@@ -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"
|