DeepSeek Harness Remote
DeepSeek Harness Remote: DeepSeek Harness 실행 PC와 모바일·PC를 안전하게 연결해 어디서나 작업 이어받기
Google Slides 등에서 내보낸 PDF에 접근성 구조를 자동으로 삽입하고, 실제 판단이 필요한 부분은 사람에게 물어보는 무료 Mac 앱
A free Mac app that builds the accessibility structure into PDFs exported from Google Slides and the like, and asks a human about the calls that are genuinely judgement.
생산성Python · HTMLGitHub @Chris-Keene온라인운영자 미확인
써 보셨어요? 한 번만 눌러 주세요. 로그인은 필요 없어요.
개인 계정의 공개 저장소 소유자는 GitHub로 로그인해 운영자를 확인하고 소개를 직접 관리할 수 있습니다. 관리자 승인은 필요하지 않습니다.
로그인한 뒤 확인 파일을 만들어 이 저장소의 기본 브랜치에 30분 안에 커밋해 주세요. 다른 브랜치나 포크의 파일은 인정하지 않습니다. 파일을 올린 뒤 아래에서 확인하면 관리자 승인 없이 관리할 수 있습니다. 자격을 유지하려면 파일을 남겨 두세요.
로그인이 필요하면 로그인 화면으로 이동합니다. 로그인 후 이곳으로 돌아와 원하는 확인 버튼을 다시 눌러 주세요.
프로젝트 폴더에서 /nomorevibe verify를 실행하면 소유권을 확인하고 소개를 직접 관리할 수 있습니다.
1분 만에 확인하기Trellis (formerly PDF Tagger): a Mac app that builds the accessibility structure into PDFs exported without any, and asks a human about the calls that are genuinely judgement.
저장소 README에서 가져옴
README 전체를 불러오는 중입니다.
README 전문 보기 ↗Trellis, an accessibility tagger for PDFs. Formerly PDF Tagger. It was renamed in 1.22.0; files PDF Tagger tagged are still recognised as its own work. Releases: https://github.com/Chris-Keene/trellis/releases. More: https://chriskeene.me/trellis.
A Mac app that takes a PDF with no accessibility structure at all — the kind Google Slides, Keynote and most "print to PDF" routes produce — works out what the structure probably is, walks you through confirming it, writes real tags into the file, and then checks the result.
It is the missing half of pdf-accessibility-check. That skill repairs a tag
tree that already exists and correctly refuses a file that has none. This builds
one, with a human deciding the parts a script cannot know.
An untagged PDF has words on the page and nothing else: no headings, no lists, no reading order, no image descriptions. A screen reader gets a soup of text in whatever order the exporter happened to paint it.
Post-processing alone cannot fix that, because the information is not in the file to recover — which is exactly why the check-only skill refuses. What is recoverable is a strong guess: font sizes rank headings, bullet glyphs mark list items, indents give nesting, column geometry gives reading order. The app makes that guess, shows it to you on top of the rendered page, and lets you correct it before anything is written.
Measured on ten real Google Slides exports (1 to 20 pages): all ten reach
veraPDF PDF/UA-1 106/106 rules, isCompliant=true, and all ten render
pixel-identical to the original.
For handing out to non-technical people, see INSTALL.md, which
covers the Gatekeeper first-launch warning step by step. Every
release carries the zip
(Trellis-1.23.0.zip for this version) and both PDF guides.
Double-click Trellis.app. It opens a normal Mac window and walks five steps:
Your original file is never modified. The tagged copy is always a new file.
Because it is not signed by a registered developer, macOS blocks the first launch. Clear it once in System Settings → Privacy & Security → Open Anyway. On macOS 15 the old right-click → Open shortcut no longer works for this.
You can also right-click a PDF and Open With → Trellis, or drop one on the Dock icon. It registers as an alternate handler for PDFs on purpose; hijacking the default handler would be a rude thing for a tagging tool to do.
If Claude Code is installed and signed in, a Claude off switch appears in the top right next to Help tips. Turn it on and two buttons appear: Draft the missing descriptions on the Images step, and Check this page's reading order on the Structure step. It runs on your own Claude Code account; there is no key to paste.
claude once in Terminal and sign in. Trellis calls
that terminal command, which keeps its own sign-in, so do this even if you use
Claude in the desktop app or a browser.app/assist.py), because an app started from
the Dock does not see your shell's PATH.What it sends, and only when you press a button: for a description, a crop of that one image plus up to 600 characters of its page's text; for reading order, that page's text (each item cut to 90 characters), roles and positions, never a picture. Drafts arrive unread and low-confidence and count as outstanding until a person has checked them. The switch is never remembered between launches.
At launch it also asks claude auth status --json whether that command is signed in
(local, free, about a third of a second). Signed out, there is no switch: the first
step says Claude Code is here but not signed in, with a Check again button. An
answer it cannot read (an older Claude Code, or Bedrock and the like, which sign in
elsewhere) gets the benefit of the doubt, and a sign-in that lapses later still
surfaces on first use. A run that stops part way keeps the drafts it already made.
The window is a real NSWindow with a real menu bar, native open and save panels,
and Dock and Cmd-Tab behaviour. Inside it, the UI is the same HTML in app/ui,
shown in a WKWebView.
That split is deliberate, and the alternatives are worse:
So the native part is the shell, which is where "feels like a Mac app" actually
lives. The menu bar is not decoration either: without a real Edit menu, Cmd-C,
Cmd-V and Cmd-A do nothing inside a WKWebView's text fields, which would make
writing image descriptions miserable.
Two details worth knowing if you touch it:
127.0.0.1 with a random port, it is just invisible.native.on_main hops over and blocks until the user
answers.Browser mode is kept as a fallback, which is also what the test suite drives:
./.venv/bin/python run.py # native window (default)
./.venv/bin/python run.py --browser # open in the default browser instead
./.venv/bin/python run.py 8146 --no-browser # server only, no UI
If PyObjC is missing for any reason, it says so in the log and falls back to the browser rather than failing to start.
It can: build a structure tree from scratch; set headings, paragraphs, lists with nesting, figures with alt text, and links; set language, title and the document-level flags; wrap decoration as artifacts; fix reading order; reach full PDF/UA-1 conformance.
It cannot:
/Alt="image" passes every
checker and tells a reader nothing. The app flags filenames, file paths, junk
words and duplicates, but the words are yours.TD/TH.
A real data table needs a route that keeps the table structure.Conformance is a floor, not proof the document reads well. Both things are worth saying to whoever receives the file.
python3.11 -m venv .venv
./.venv/bin/pip install pikepdf pymupdf pyobjc-framework-WebKit
./.venv/bin/python run.py # opens in your browser
Dependencies: pikepdf, pymupdf, and pyobjc-framework-WebKit for the native
window (about 6MB of wheels; without it the app still runs in the browser).
Optional: ./.venv/bin/pip install anthropic and an exported ANTHROPIC_API_KEY
let a source run use the Claude help without Claude Code. The packaged app does not
include the anthropic package, so it only works through Claude Code; when both are
present, Claude Code wins unless it is signed out.
Optional but recommended: brew install verapdf for independent conformance
validation. Without it the app still tags and exports, and says plainly that
conformance was not verified.
./.venv/bin/python scripts/tag_cli.py in.pdf -o out.pdf --alt "placeholder" --report
--report runs veraPDF. Note veraPDF exits non-zero on FAIL, so never chain
it with &&.
./build/make_app.sh
Needs pyinstaller in the venv. Output lands in build/dist/. The release zip
is made with ditto, which keeps the code signature intact:
ditto -c -k --sequesterRsrc --keepParent "build/dist/Trellis.app" dist/Trellis-1.23.0.zip
./build/export_docs.sh # the two PDF guides, into dist/ beside it
Two views of each page are combined:
pymupdf gives the semantic view: text grouped into blocks, lines and
spans with font sizes and boxes. This decides what is a heading.pikepdf) gives the mechanical view: the exact
operator ranges that paint each mark, with positions resolved through the
graphics-state stack. This is what the tagger has to wrap.The two are joined on position. Then every mark on the page is wrapped either in
a marked-content sequence with an MCID or in /Artifact BMC … EMC, because
PDF/UA 7.1 requires that nothing is left unmarked. A /StructTreeRoot is built
over those MCIDs with a /ParentTree mapping each one back to its element.
Rewriting goes through pikepdf's instruction tokeniser rather than raw bytes,
which sidesteps the trap that BDC/BMC are postfix operators — splitting a
stream at the keyword corrupts it in a way that looks like a semantic problem.
| file | what it does |
|---|---|
app/analyse.py | PDF → proposed structure (roles, order, nesting, columns) |
app/tagger.py | confirmed structure → tagged PDF (stream surgery + tree) |
app/hardening.py | /Lang, /MarkInfo, DisplayDocTitle, XMP |
app/checks.py | what still needs a human, in plain English |
app/server.py | local HTTP API, stdlib only |
app/native.py | the native window, menus and file panels (PyObjC) |
app/ui/index.html | the five-step UI, single file, no network calls |
build/make_icon.py | draws the app icon (no extra dependency) |
./.venv/bin/python tests/run_tests.py # everything, a few minutes
./.venv/bin/python tests/run_tests.py --quick # 3 corpus files
./.venv/bin/python tests/run_tests.py --only trip,edge
./.venv/bin/python tests/run_tests.py --native-ui # + the native window checks
The native window checks are opt-in because they briefly open a window.
Most of the suite runs on fixtures it builds itself. A further set of checks runs against real PDFs, because the documents that break a detector are odd real ones rather than anything a person would think to construct. Those files belong to whoever made them, so none of them are in this repository, and every check that needs one is skipped and reported rather than failed when it is missing.
Copy tests/corpus.json.example to tests/corpus.json and point it at your own:
| key | what it wants |
|---|---|
DIR | a folder of untagged exports to sweep (default ~/Downloads) |
DECK | any real slide deck, for heading and quotation checks |
MISTAGGED | an export whose existing tags are wrong, e.g. from Google Docs |
SHADED_TABLE | a document with a borderless, shaded table |
TAGGED | glob patterns for files some other tool already tagged |
TRELLIS_CORPUS_<KEY> overrides any of them (PDFTAG_CORPUS_<KEY>, from
before the rename, still works). DIR is worth setting deliberately:
the default sweeps a whole Downloads folder, which is a rude thing to do to
somebody silently.
Two fixtures work the same way. tests/slides-6page.pdf and
tests/slides-1page.pdf are real exports if you drop them in; without them the
suite builds a deck of the same shape and carries on.
Exits non-zero on failure. A full run is 30 groups and 454 checks without the
corpus (1.23.0), from round-trip, edge cases, fonts and nested text through to
provenance, error messages and the HTTP API, plus 15 more with --native-ui.
Two invariants are worth calling out, because they are what the suite is really built around:
The suite also carries a deeper structural check than veraPDF performs, because
several real defects sail past conformance: duplicate or unbalanced
marked-content ids, nested marked content, MCIDs with no owner in the ParentTree,
/P links that do not point at the real parent, empty /Alt, and skipped
heading levels.
One treatment was doing six different jobs. On a single structure-step screen there were 66 small buttons, all rendered identically: page navigation, view toggles, zoom, help topics, reordering, and the one action that resolves a review item. A pressed toggle also used the same navy fill as the primary button in the footer, so an engaged filter looked like the main call to action.
They are now typed by what they do:
| variant | job | treatment |
|---|---|---|
.commit | resolves a review item | filled green, tick prefix — matches the "checked" badge it produces |
.toggle | on/off | state pip, tinted when engaged; no longer navy |
.seg | related view controls | segmented cluster (zoom, page nav) rather than loose pills |
.step | reorder, two per row | quietest thing on screen: small, muted, arrows only |
.info | explains rather than acts | dashed and tinted |
.jump | takes you elsewhere | link-like, arrow suffix |
Two things this exposed, both from making controls quieter:
--line
is a decorative rule colour at 1.44:1; a control boundary needs 3:1 (WCAG 2.2
1.4.11). Quiet now comes from size and ink, with borders at 3.4–4.5:1.Audited across 154 controls: no text, border or target-size failures.
The structure step is a working canvas, not prose, so it drops the reading-width cap the other steps use and gives the page preview the larger share of the split. On a 1500px window that took the preview from 712px to 863px.
There is a zoom control above it (100% to 400%, plus fit-to-width) for slides where boxes sit close together — a wall of customer logos, say, where at fit width you cannot tell which box covers which mark. The overlay boxes are positioned in percentages, so scaling the stage scales them for free; the page image is re-requested at a matching resolution so zooming in does not just magnify blur.
Once zoomed, the preview scrolls, drags, and takes arrow keys, and selecting an item centres it in view. Two things worth knowing if you touch that centring:
scrollTop before the page image has loaded is silently rounded to 0,
because the stage has almost no height yet.requestAnimationFrame does not fire when the page is not painting (a
background tab, or a headless context), so the callback simply never ran. It uses
setTimeout instead.Every proposed role carries a 0-100 confidence and a sentence saying why, and the score is not decoration: it decides what the user is asked to look at.
It is scored on the strength of the evidence and on what a wrong call would cost. A line with a list marker is 95. Text set at the body size is 95. A line 60% bigger than the body text is a confident heading; one 13% bigger is barely over the line and scores near 68. A bold two-word label that is not bigger than the body text is 62, because it is usually a subheading and sometimes not. Anything sitting between two columns loses points, because its place in the reading order is a guess and reading order is the one thing no checker will ever question.
Weighting by cost matters. An early version scored "smaller than body text" as low confidence, which flagged 43% of one deck — mostly paragraph-versus-caption calls that change nothing for a screen reader, drowning out the paragraph-versus-heading calls that change everything. Tuned, it flags 11% across the test corpus, 0-27% per file.
Below 70 an item is queued for review, and the user can either change the role or confirm it with Looks right. Changing a role counts as confirming. Confirmed items stop being asked about. The page rail counts what is still open per page, the overlay flags uncertain items with a marker, there is a filter to show only those, and a button steps through them across pages.
Images are deliberately not in that list. They have their own step, where the real question is the description rather than the role, and counting them in both places made both lists less useful.
Re-opening a file this app tagged reads the roles back out of its structure tree instead of guessing again, so a role the user changed survives the round trip and comes back marked as checked. Before this, the app reverted the user's changes on re-upload and asked them to review everything a second time, which made confirming a proposal pointless.
Provenance decides how far the tags are trusted. Ours (identified by the
xmp:CreatorTool value alone: Trellis, or PDF Tagger for files tagged before
the rename) means a human already worked through the document, so they are
treated as settled. Another tool's are adopted too — better evidence than a guess
from font sizes — but nobody has vouched for them, so they are used without being
signed off, and the user is told once at the document level rather than being
handed several hundred individual items to confirm.
Anything settled is also exempt from heading re-levelling. Overruling a decision someone made on purpose is worse than leaving a gap in the heading levels and telling them about it.
Each of these produces a file that would otherwise look tagged while being useless, so the app reports them instead of papering over them.
Do — marks the whole document as decoration, which satisfies the
unmarked-content rule and so passes conformance while hiding everything from a
screen reader. It is left unmarked instead, so the file fails honestly, and the
user is told up front./CharProcs,
so there is no font file to embed. They get their own note, because they are
often the signature of the variable-font export that garbles text.Audited in the browser rather than by eye: computed contrast on every visible text
node (including aria-hidden labels, which sighted users still read), computed
accessible names from the real accessibility tree, heading sequence, keyboard
focus order and visible focus, and WCAG 2.2 target sizes. Zero contrast
failures, no unnamed controls, no skipped heading levels.
Two things worth recording:
#F20487 carries white text at only 4.08:1. Buttons and labels
use the derived #D10375 (5.28:1); #F20487 is kept for non-text only, where
the 3:1 threshold applies. Same fix in both directions, since contrast is
symmetric.(tx, ty) as a corner puts a full-bleed
footer graphic at the top of the page.--windowed gives sys.stdout is None, so a bare print()
raises. Everything goes through log(), and run.py installs a null sink.HTTPServer.server_bind() calls socket.getfqdn() just to fill in
server_name, which nothing reads. In the frozen bundle that reverse DNS
lookup blocks for 35 seconds. Server.server_bind skips it: startup went
from 35,238ms to 559ms.docs/README.md. tests/run_tests.py --only brand guards both.minmax(0, …). A bare 1fr is minmax(auto, 1fr),
whose auto minimum is min-content, so the wide page rail pushed its column past
the window and the panel overflowed sideways below about 1000px.