-
-
Notifications
You must be signed in to change notification settings - Fork 11
Expand file tree
/
Copy pathpyproject.toml
More file actions
627 lines (612 loc) · 32.6 KB
/
Copy pathpyproject.toml
File metadata and controls
627 lines (612 loc) · 32.6 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
[build-system]
requires = ["hatchling>=1.31.0"]
build-backend = "hatchling.build"
[project]
name = "quill"
dynamic = ["version"]
description = "Screen-reader-first writing and document environment for Windows and macOS"
readme = "README.md"
requires-python = ">=3.12"
license = { text = "MIT" }
authors = [
{ name = "Blind Information Technology Solutions (BITS)" },
{ name = "Community Access" },
]
classifiers = [
"Programming Language :: Python :: 3",
"Programming Language :: Python :: 3.12",
"Programming Language :: Python :: 3.13",
"Operating System :: Microsoft :: Windows",
"Operating System :: MacOS",
]
dependencies = [
"regex>=2026.7.19",
"defusedxml>=0.7.1",
# RadioDNS (Quill Radio 3.0, core/radio/radiodns.py). The standard library
# resolves A records and nothing else; RadioDNS needs CNAME chasing and an
# SRV lookup (_radioepg._tcp) to find a broadcaster's service document, so
# a real resolver is required rather than convenient. Approved 2026-08-13.
# Podcast RSS/Atom feed parsing (Tools > Media > Podcasts). Real-world feeds
# are too messy (malformed XML, mixed RSS/Atom/iTunes/Podcasting-2.0
# namespaces) for a hand-rolled ElementTree walk to handle reliably; this is
# the standard Python choice. Pure Python, no C extension, tiny -- a normal
# dependency, not an on-demand optional component like Pandoc/speech engines.
"feedparser>=6.0.14",
# Runtime deps of the vendored autoupdate library (quill/_vendor/autoupdate).
"requests>=2.34.2",
"platform_utils>=1.6.3",
# Windows has no system IANA timezone database, so stdlib zoneinfo needs
# this package to resolve real zone names (publishing schedule dialog).
"tzdata>=2026.3",
# Native .docx reading/writing without requiring Pandoc.
"python-docx>=1.2.0",
# AES-GCM for Quill Beacon's encrypted sync (apps/beacon/quillsync/crypto.py)
# and PuTTY private-key parsing (core/ssh/putty_key.py). Hard-imported by the
# beacon sync module, so it is a base dependency, not an optional extra.
"cryptography>=50.0.0",
# Windows COM. Not a UI concern despite living in the [ui] extra until now:
# quill/core/persona_launcher.py builds persona .lnk shortcuts through
# WScript.Shell, and quill/platform/windows (sapi5, narrator_announce,
# comtypes_setup) is imported from quill/core/read_aloud.py. Core-layer use
# means this belongs in the base dependency set, Windows-gated.
"comtypes>=1.4.16; sys_platform == 'win32'",
# NOTE: huggingface_hub is deliberately NOT a base dependency. QUILL's default
# speech models (whisper.cpp, Faster Whisper) and Piper voices download from
# QUILL's own SHA-verified assets-v1 release, not Hugging Face, so a clean
# install is Hugging-Face-free. huggingface_hub is only pulled in by the
# optional [fasterwhisper] extra (a transitive dep of faster-whisper) and by a
# user-installed pyannote for the opt-in diarization upgrade.
]
[project.optional-dependencies]
ui = [
# Pinned exactly for 1.0.0, not floored. wxPython 4.3.x tracks the
# wxWidgets master branch, which upstream documents as API/ABI UNSTABLE
# between 4.3.x releases -- so a bare floor means a release build can
# silently ship a wx the acceptance run never tested, and QUILL's whole
# product is screen-reader behavior on those widgets. 4.3.1 (wxWidgets
# 3.3.3) is the version the 1.0.0 suite and JAWS/NVDA pass ran against.
# Same rationale as the llama-cpp-python exact pin below. Re-test the
# editor, dialogs, and braille surfaces before bumping.
"wxPython==4.3.1",
# Accessible wx.html2.WebView surfaces (chat, preview, dialogs) — extracted
# from Quill and published so any wxPython app can reuse it.
"wx-accessible-webview>=0.2.0",
# pywin32: previously undeclared -- it reached shipping builds only as a
# transitive dependency, while build_windows_distribution.py prunes its
# unused extras (~21 MB) as though it were intentional. Two live call
# sites: quill/platform/windows/screen_capture.py (win32gui, for the
# active-window capture behind AI image description) and
# quill/_vendor/autoupdate/autoupdate.py (win32api.ShellExecute, to hand
# off to the update bootstrapper). Both are UI-reachable only, so it sits
# in this extra rather than in the base set.
"pywin32>=312; sys_platform == 'win32'",
# CA bundle for verified HTTPS update checks (macOS Python lacks trusted roots).
"certifi>=2026.7.22",
# Prism screen-reader speech bridge (Windows) — routes announcements to
# NVDA/JAWS/Narrator instead of speaking over them.
"prismatoid>=0.17.3; sys_platform == 'win32'",
# accessible_output2: fallback screen-reader speech bridge (Windows). Its
# per-reader is_active() detection is used when Prism cannot acquire a live
# backend, before resorting to the SAPI self-voice. Pulls in libloader.
"accessible_output2>=0.17; sys_platform == 'win32'",
# Pillow: screen capture (PIL.ImageGrab) and clipboard image support.
# Also a transitive dependency of pillow-heif on platforms where that installs.
"Pillow>=12.3.0",
# HEIC/HEIF image support for AI image description (iPhone default format).
# Registers a PIL opener so HEIC files can be read and converted to JPEG
# in memory before being sent to the vision API.
"pillow-heif>=1.5.0",
# HTML-to-text converter for intelligent paste cleaning (AccessibleApps, MIT).
"html_to_text>=1.0.0",
# Ed25519 / minisign-shaped Quillin signature verification (PyNaCl). Bundled
# at runtime so every shipping build can verify publisher-signed Quillins
# in the Quillins Manager -- the signature check was previously dev/CI-only
# (the [signing]/[dev] extras), which meant real users always saw "PyNaCl is
# not installed" and could never verify a Quillin (#919 follow-up). The
# graceful missing-nacl path in quill.tools.signing stays as defense in
# depth; it just almost never triggers now.
"PyNaCl>=1.6.2",
]
spellcheck = ["pyenchant>=3.3.0"]
# macOS native rich text (One Editor, Every Format, Phase 6): PyObjC lets
# QuillMacRichText drive the editor's NSTextView (RTF via NSAttributedString).
# The shipped Mac .app BUNDLES this (the [macos] build extra installs the full
# pyobjc and scripts/setup_macos.py lists objc/AppKit/Foundation in the py2app
# includes, since the imports are lazy and the tracer misses them) — nothing
# for end users to install. This extra covers source/pip installs on a Mac.
# The code path still degrades to converted rich if the bridge is ever absent.
mac = ["pyobjc-framework-Cocoa>=12.2.1; sys_platform == 'darwin'"]
# Direct GitHub issue submission for Report a Bug (#210): feedback_hub provides
# the accessible FeedbackDialog driven by quill/core/schemas/feedback.json and
# files the issue via the GitHub API. Ships in every build (this is the only
# Report a Bug form); on failure QUILL copies the online support-form URL.
# 1.0.1 fixes the wx sizer-parenting crash that broke the dialog on wx 4.2.5+.
# 1.1.0 adds crash fingerprinting: a repeat of a crash somebody already
# reported comments on that issue instead of filing another one.
#
# 1.1.0 was published 2026-08-13, so the floor moves with it -- this extra is
# installed by scripts/build_windows_distribution.py, and it was deliberately
# held at 1.0.2 until then rather than pointing a shipping build at a version
# that did not exist. The call site stays version-tolerant either way:
# issue_submit._submit_parameters() asks the installed signature what it
# accepts, so an older feedback-hub still works and simply files duplicates.
feedback = ["feedback-hub>=1.1.0"]
# Cross-platform earcon audio via BASS (Un4seen). Enables true mixing so multiple
# sounds can play simultaneously. Falls back to winsound (Windows) or silence without
# this extra. Confirm Un4seen commercial license terms before paid distribution.
audio = ["sound_lib>=0.83"]
# Edit files over SSH/SFTP (issue #139): browse a remote host, download to edit,
# upload on save with a tilde backup. paramiko handles password and OpenSSH-key
# auth; it is optional so the rest of Quill runs without it.
ssh = ["paramiko>=5.0.0"]
# On-device AI for the Ask Quill chat. Windows/Linux use llama.cpp (CPU, GGUF);
# a GGUF model (e.g. Phi-4-mini Q4) goes in <app data>/models or QUILL_LLAMA_MODEL.
# Pinned to the newest version with a prebuilt CPU wheel on the abetlen index
# (see requirements.txt). A higher floor has no wheel and makes pip compile
# llama.cpp from source. Install via requirements.txt (which carries the
# --extra-index-url) or pass that index so the wheel resolves.
ai = ["llama-cpp-python==0.3.32"]
dictation = ["SpeechRecognition>=3.17.0"]
# Offline speech-to-text (#617). The whisper.cpp executable + GGML models are
# managed out-of-band (AI > Speech > Manage Speech Models); this optional extra
# adds microphone capture for offline dictate-at-cursor via PortAudio.
speech = ["sounddevice>=0.5.5"]
# Optional higher-throughput offline speech engine (#617 S4). Faster Whisper is
# a CTranslate2-based library that uses the GPU when available; huggingface_hub
# fetches its CTranslate2 model repositories. Users opt in via AI > Speech >
# Manage Speech Models > Speech Engine; the bundled whisper.cpp engine remains
# the default and needs none of this.
fasterwhisper = ["faster-whisper>=1.2.1", "huggingface_hub>=1.27.0"]
# Optional very-low-resource offline English engine: Vosk (Kaldi) (#669). Tiny
# CPU-only models (~40 MB) for old/constrained machines. Models download from
# alphacephei.com via Manage Speech Models; no GPU needed.
vosk = ["vosk>=0.3.45"]
# Optional offline English engine: NVIDIA Nemotron Speech Streaming via
# sherpa-onnx (#669 follow-up). 600M Cache-Aware FastConformer-RNNT run int8 on
# the CPU — no torch/CUDA (sherpa-onnx bundles its own native onnxruntime). The
# model downloads from QUILL's assets-v1 release via Manage Speech Models.
nemotron = ["sherpa-onnx>=1.10.0", "numpy>=2.5.2"]
# MP3 chapter markers for batch document-to-speech (§4.8): mutagen writes the
# ID3v2.3 CHAP/CTOC frames. Imported lazily in quill/core/speech/chapters.py.
mp3 = ["mutagen>=1.48.1"]
# Ed25519 / minisign-shaped signing for QUILL artifacts (Hub Submission Forge,
# in-app submit dialog, in-app install path). Transitive on systems that
# already have flask-migrate; promoted to a direct extra so the Hub's deploy
# install is explicit.
signing = ["PyNaCl>=1.6.2"]
# Native Word (.docx) export with full formatting fidelity (font family, point
# size, color, highlight, paragraph alignment) for the hidden-codes feature
# (QUILL PRD hidden-codes appendix), and — since One Editor, Every Format —
# the docx rich reader (quill/io/docx_reader.py). Optional: when absent,
# write_docx_document falls back to the Pandoc path (which drops those run/
# paragraph attributes) and .docx opens read-extract instead of rich.
docx = ["python-docx>=1.1.0"]
# Premium cloud TTS via the official ElevenLabs SDK (roadmap §4.1, audio export).
# Optional: the ElevenLabs AI Voice provider is inert unless this is installed and
# an "ElevenLabs API key" credential is configured. All calls go through the
# host-owned gateway quill/core/ai/elevenlabs_tts.py (the only importer of the SDK).
elevenlabs = ["elevenlabs>=2.59.0"]
# Native Windows OCR (OCR-1): thin pip wheels projecting the Windows.Media.Ocr
# WinRT APIs that ship inside Windows 10/11. The OCR engine itself is a built-in
# OS component, so end users install nothing extra — bundling these wheels makes
# the zero-install Windows backend available; Tesseract (OCR-2) stays a separate
# opt-in install for users who choose it.
#
# These are the Microsoft pywinrt ``winrt-*`` namespace packages. They replace
# the legacy ``winsdk`` monolith, whose wheels stopped at cp312 and so blocked
# the embedded-runtime move to Python 3.13; the projected API is identical (see
# quill/platform/windows/windows_ocr.py). Only the directly-imported namespaces
# plus winrt-runtime are listed; pywinrt pulls the rest (Foundation,
# Storage.Streams, ...) transitively.
ocr = [
"winrt-runtime>=3.2.1; sys_platform == 'win32'",
"winrt-Windows.Media.Ocr>=3.2.1; sys_platform == 'win32'",
"winrt-Windows.Graphics.Imaging>=3.2.1; sys_platform == 'win32'",
"winrt-Windows.Globalization>=3.2.1; sys_platform == 'win32'",
"winrt-Windows.Storage>=3.2.1; sys_platform == 'win32'",
]
# Kokoro neural TTS (offline, no GPU/torch required). kokoro-onnx runs the Kokoro
# voice model through onnxruntime (CPU). Voice model files (~114 MB) are downloaded
# on first use via Voice Picker > Download Kokoro and stored in the app data dir.
# phonemizer-fork and espeakng-loader (transitive deps of kokoro-onnx) are
# self-contained — they bundle eSpeak-NG phoneme data and do not require a system install.
# soundfile writes the synthesized PCM to WAV; it bundles libsndfile on Windows.
kokoro = [
"kokoro-onnx>=0.5.0",
"soundfile>=0.14.0",
# Runtime dep of the Kokoro phonemizer chain
# (kokoro-onnx -> phonemizer -> segments -> csvw -> babel.numbers). csvw
# normally pulls it transitively, but declaring it here makes the Kokoro
# runtime chain self-contained so a pruned/partial install can't leave the
# ONNX voice path importing a missing 'babel'. Keep in sync with [dev].
"Babel>=2.18.0",
]
pages = ["keynote-parser>=1.14.4.0"]
# Free-first PDF/Office text extraction (quill/io/docconvert.py Tier 1 +
# quill/io/pdf.py): MarkItDown for Office + PDF, pdfplumber/pypdf as the PDF
# text floor. Was briefly a base dependency (#909 fixed a build that shipped
# with neither this extra nor a base dependency, so a clean install had no
# PDF/Office text extractor at all). Downloadable on demand via Help > Download
# Optional Components (quill/core/pdf_ocr_install.py). Shipped builds bundle the
# lean `office-text` extra below instead of this one -- see the note there.
pdf-ocr = [
"markitdown[docx,pptx,xlsx,xls,pdf]>=0.1.7",
"pdfplumber>=0.11.10",
"pypdf>=6.15.0",
]
# The lean half of pdf-ocr, and the half that actually ships (#1279). MarkItDown
# is the better reader, but its dependency chain is not lean: magika (its stream
# sniffer) hard-requires onnxruntime + numpy, and its xlsx/xls converters require
# pandas -- roughly 150 MB installed, which is not a reasonable tax on an
# accessibility app's download when the formats can be read without it. These
# three are ~16 MB installed and cover the real gaps in a packaged build: PDF text
# (quill/io/pdf.py has no reader at all without pdfplumber/pypdf) and .xlsx/.xls
# sheets (quill/io/structured.py::_format_spreadsheet needs openpyxl). Word reads
# through python-docx, which is already a base dependency. MarkItDown stays a
# one-click download for anyone who wants it (Help > Download Optional Components).
office-text = [
"pdfplumber>=0.11.10",
"pypdf>=6.15.0",
"openpyxl>=3.1.5",
]
# GLOW document accessibility engine (audit / fix / convert for docx, pptx,
# xlsx, pdf, epub, md). quill-glow-core is the thin, stable service contract
# Quill imports; its [glow] extra pulls in the acb-large-print backend. The
# dependency is named — never a drive path — so it resolves the same from any
# checkout root on any machine. Until these are published to an index, install
# the vendored, drive-independent pure-Python wheels with a repo-relative path:
# pip install --find-links vendor/wheels "quill-glow-core[glow]"
# acb-large-print is pinned >=8.0.0 explicitly: the MCP-server-era split moved
# the dispatch backend into acb_large_print_core (new in 8.0.0), and the
# contract wheel's own extra only demands >=3.0.0 — loose enough for a stale
# pre-split install to satisfy the resolver and leave the engine silently
# unavailable inside QUILL.
glow = ["quill-glow-core[glow]>=0.1.1", "acb-large-print>=8.0.0"]
# LaTeX -> MathML conversion for quill/core/math/ (docs/planning/math.md, Tier 1-B).
# Pure Python, no native build. Optional: the math bridge degrades to accepting
# MathML input only (no LaTeX typing) when this is absent.
math = ["latex2mathml>=3.81.0"]
# macOS build + runtime. py2app 0.28.x is incompatible with setuptools >= 83
# (the build aborts with "install_requires is no longer supported"), so pin it.
# Security note (Dependabot alert 14, GHSA setuptools < 83 MANIFEST.in exclusion
# bypass on APFS/HFS+): accepted. The vulnerable path is building sdists on
# macOS with security-relevant MANIFEST.in exclusions; QUILL builds .app
# bundles via py2app, never sdists, and no newer py2app supports setuptools 83
# (0.28.10, 2026-02, is current). Re-check when py2app moves.
# pyobjc powers the macOS platform layer; apple-fm-sdk is the Foundation Models
# backend used by Ask Quill on macOS.
macos = [
"py2app>=0.28.10; sys_platform == 'darwin'",
"setuptools<83; sys_platform == 'darwin'",
"pyobjc>=12.2.1; sys_platform == 'darwin'",
"apple-fm-sdk>=0.2.1; sys_platform == 'darwin'",
]
github = [
# GitHub repository browser and remote file open/save (File > Open Remote > GitHub).
"PyGithub>=2.9.1",
]
# Optional agentic-AI harness packs (QUILL 2.0): OpenAI Agents, Claude Agent, and
# GitHub Copilot. Each pulls one third-party agent SDK; the matching
# quill/ai_packs/ module imports it LAZILY only when a session runs, and an
# uninstalled pack reports "Install the X pack" without crashing. Every pack
# bridges to the same Safe Editor Tool Gateway + Permission Broker and emits the
# same normalized events; none edits the buffer directly. The Native harness
# always works without any of these.
ai-copilot = ["github-copilot-sdk>=1.0.8"] # PyPI package; imports as `copilot`
ai-claude = ["claude-agent-sdk>=0.2.110"]
ai-openai = ["openai-agents>=0.18.3"]
# Build-time only: compile the optional Table Studio native UIA provider
# (quill/native/table_uia -> _quill_table_uia.pyd). Also needs MSVC + the
# Windows 10 SDK + CMake. Not required at runtime (MSAA fallback ships).
native-build = ["pybind11>=3.0.4"]
# The dependency set the shared QuillVille Runtime ships
# (standalone/runtime/build_runtime.ps1 -> quillville-runtime.spec), which every
# companion app installs once and launches through.
#
# Why this exists: the spec builds with collect_all("quill"), so PyInstaller
# bundles whatever happens to be importable in the builder's virtualenv. With no
# declared set, the runtime's contents were a photograph of one machine at one
# moment -- a drifted venv silently shipped a runtime with no offline dictation
# and wxPython below its own pin, and the build reported success either way.
# scripts/check_build_env.py verifies the venv against this list before a build,
# so a drift fails loudly instead of shipping.
#
# This mirrors what build_portable.py already does for the portable bundles (it
# resolves groups straight from this file), which is exactly why those artifacts
# were unaffected by the same drift.
#
# Trimming this list is the supported way to shrink every app installer at once:
# the speech engines below (fasterwhisper, vosk, kokoro) all have on-demand
# install paths in quill/core/speech/engine_install.py and are bundled here only
# so a fresh install works without a download.
youtube = [
# yt-dlp resolves a YouTube page link to a playable audio stream: Radio's
# YouTube stations and playlists, and the audio converter's URL import.
#
# Bundled rather than installed on demand. The wheel is ~3 MB (10 MB
# unpacked, but 8 MB of that is 941 site-specific extractor modules that
# compress hard in the PYZ), which is small enough that making every new
# user consent to a download before their first YouTube link is a worse
# trade than the disk. The on-demand installer in
# quill/core/speech/engine_install.py stays: it is now the *upgrade* path,
# because yt-dlp goes stale whenever YouTube changes its player and ships
# fixes far more often than QUILL ships releases. An engine-pack copy wins
# over this bundled one -- see engine_install.prefer_engine_pack_yt_dlp.
"yt-dlp>=2026.7.4",
]
runtime = [
"quill[ui]",
"quill[spellcheck]",
"quill[audio]",
"quill[speech]",
"quill[dictation]",
"quill[mp3]",
"quill[docx]",
"quill[math]",
# Windows OCR (the winrt-* packages) shipped in the 2026-08-09 release and is
# absent from the current one. It is ~1.5 MB and needs no model download, so
# restoring it is a one-line change: add "quill[ocr]" here. Left out for now
# because what the runtime ships is a product decision, not a build one.
"quill[office-text]",
"quill[pdf-ocr]",
"quill[youtube]",
"quill[fasterwhisper]",
"quill[vosk]",
"quill[kokoro]",
]
# Build tools the release scripts invoke, NOT dependencies of the shipped app.
#
# Why this is separate from [runtime]: that group is the set the shared
# QuillVille Runtime *ships*, so putting PyInstaller there would bundle the
# packaging tool into every installer. This group is only ever checked, never
# collected into an artifact.
#
# Why it exists at all: PyInstaller used to be declared nowhere. It was present
# on the one machine that cut releases purely because that checkout's virtualenv
# happened to have it, so check_build_env.py reported "Build environment
# satisfies pyproject [runtime]" and the build then died seven seconds later on
# "No module named PyInstaller". A gate that cannot see the single most
# important build tool is not a gate; build_runtime.ps1 now checks
# `--groups runtime,packaging`.
packaging = [
"pyinstaller>=6.22.0",
# Ships the upstream hooks for third-party packages (wx, babel, huggingface);
# a stale copy silently drops data files from the frozen tree.
"pyinstaller-hooks-contrib>=2026.6",
]
dev = [
"pytest>=9.1.1",
"pytest-xdist>=3.8.0",
"pytest-timeout>=2.4.0",
"pytest-cov>=7.1.0",
# Property-based testing for the pure parsers (find_model, regex_helper,
# quill/io readers): generates edge cases a hand-written example never
# thinks of. See tests/unit/core/test_find_model_properties.py.
"hypothesis>=6.165.2",
# Randomized test ordering, DISABLED by default via addopts (-p no:randomly).
# Opt in with `pytest -p randomly ...` to hunt order-dependent coupling.
"pytest-randomly>=4.1.0",
"ruff>=0.16.2",
"mypy>=2.3.0",
# I18N: string extraction, .po update, .mo compile
"Babel>=2.18.0",
# Quillin Hub artifact signing (quill.tools.signing); CI runs its tests.
"PyNaCl>=1.6.2",
# Windows UI-automation regression tests (tests/uia, marker "uia"): drives
# the real app through UIA and asserts accessible names, keyboard reach, and
# spoken output. Windows-only by nature; harmless to install elsewhere.
"pywinauto>=0.6.9; sys_platform == 'win32'",
]
[project.scripts]
quill = "quill.__main__:main"
quill-weather = "quill.apps.weather:main"
[tool.hatch.version]
# quill/__init__.py is the authoritative version source.
# pyproject.toml, the installer, and all tooling derive from it.
path = "quill/__init__.py"
[tool.hatch.metadata]
# Allow direct URL references in optional-dependencies. Quill is an app, not a
# PyPI-published library, so direct references are fine — and hatchling refuses
# to build metadata without this, which broke `pip install -e .` in CI.
allow-direct-references = true
[tool.hatch.build.targets.wheel]
packages = ["quill"]
[tool.hatch.build.targets.wheel.force-include]
"quill/data/words_alpha.txt" = "quill/data/words_alpha.txt"
"quill/data/words_alpha.LICENSE.txt" = "quill/data/words_alpha.LICENSE.txt"
"quill/data/th_en_US_v2.dat" = "quill/data/th_en_US_v2.dat"
"quill/data/th_en_US_LICENSE.txt" = "quill/data/th_en_US_LICENSE.txt"
"quill/data/th_en_US_WordNet_LICENSE.txt" = "quill/data/th_en_US_WordNet_LICENSE.txt"
"quill/data/emoji_catalog.json" = "quill/data/emoji_catalog.json"
"quill/core/schemas/extension.json" = "quill/core/schemas/extension.json"
"quill/core/schemas/sound_pack.json" = "quill/core/schemas/sound_pack.json"
"quill/core/schemas/agent.json" = "quill/core/schemas/agent.json"
"quill/core/ai/agents" = "quill/core/ai/agents"
"quill/assets/sound_packs" = "quill/assets/sound_packs"
"quill/assets/audio" = "quill/assets/audio"
"quill/quillins_bundled" = "quill/quillins_bundled"
# I18N: ship the gettext template and any compiled translation catalogs so the
# display-language switcher can find them in installed builds.
"quill/locale" = "quill/locale"
[tool.uv]
# The GLOW backend wheels (quill-glow-core, acb-large-print) are vendored, not
# on PyPI yet; without this, `uv lock` / `uv sync --all-extras` cannot resolve
# the [glow] extra at all (pip users pass --find-links vendor/wheels manually).
find-links = ["vendor/wheels"]
# azure-ai-contentunderstanding is pinned to a pre-release (>=1.2.0b1); uv
# refuses pre-releases across resolver splits unless told otherwise, which
# made every `uv lock` fail outright.
prerelease = "allow"
[tool.pytest.ini_options]
# Pin pythonpath so the worktree's `quill` package wins over the parent
# checkout's `quill` package when both pyproject.toml files are visible
# to pytest (e.g. when running from inside .claude/worktrees/quill-0.7.0).
pythonpath = ["."]
testpaths = ["tests"]
python_files = ["test_*.py"]
timeout = 30
log_cli = true
log_level = "INFO"
# pytest-randomly is installed but OFF by default. It shuffles test order on
# every run, which is exactly how you find order-dependent coupling (the wx
# 4.3 MediaCtrl/COM crash on 2026-08-07 was one) -- but an unpredictable
# ordering in the required CI gates would turn a real failure into a
# can't-reproduce. Opt in deliberately when hunting that class of bug:
# pytest -p randomly tests/unit/core
# pytest -p randomly -p no:cacheprovider tests/unit/ui # a suspect area
# Reproduce a specific shuffle with the seed it prints: -p randomly -p randomly.seed=12345
addopts = "-p no:randomly"
markers = [
"perf: wall-clock performance budgets; run with RUN_PERF=1 or exclude with -m 'not perf'",
"smoke: fast, high-signal core checks; run the quick subset with -m smoke",
"uia: Windows UI-automation regression tests (launch the real app via UIA); opt-in with QUILL_UIA_TESTS=1 and -m uia on a Windows desktop session",
]
[tool.ruff]
line-length = 100
target-version = "py312"
# Vendored, generated, and distribution trees are not first-party source and
# must not be linted or format-checked.
extend-exclude = [
"build",
# Standalone-product build tooling (Radio/Cast/Audio Studio wrappers:
# launcher, PyInstaller spec, installer, docs). Packaging shells that
# bundle the quill package into per-product exes -- not first-party source.
"standalone",
"dist",
"windows-distribution",
"installer-smoke",
"installer-smoke-2",
"release-dist-0.1-final",
"release-dist-0.1-r2",
"kokoro",
"tmp",
# Untracked local scratch dir of third-party sample scripts; not first-party.
"Code Samples to Innovate With",
# Local scratch under docs/planning/source (e.g. the gitignored Table Studio /
# CSV prototype in source/test). The ACB audio-pipeline reference that once
# lived here was retired once its learnings shipped (roadmap §5, §7). Not
# first-party; excluded from lint/format.
"docs/planning/source",
# One-off dispatch helper that drove the podcast episode reorg. Not
# first-party app code; long literal brief strings are intentional.
"docs/podcast/dispatch_main.py",
]
[tool.ruff.format]
preview = true
# Markdown under docs/ is pandoc-owned source (rendered to .html/.epub by
# scripts/gen.ps1), not executed code. Ruff's preview mode reformats Python
# code-fences inside Markdown, which only churns historical planning docs and
# forces artifact regeneration on every ruff bump. Keep .md out of the
# formatter; the docs-artifacts gate is the sole authority on doc rendering.
exclude = ["**/*.md"]
[tool.ruff.lint]
select = ["E", "F", "I", "UP", "B"]
[tool.ruff.lint.per-file-ignores]
"scripts/build_windows_distribution.py" = ["E501"]
"scripts/local_ai_editing_proof.py" = ["E501"]
"tests/unit/core/ai/test_foundation_models.py" = ["E402", "E501"]
# This test asserts the exact long Inno Setup script lines emitted by
# scripts/build_windows_distribution.py (which itself carries the same E501
# ignore); wrapping the literal installer directives would obscure the match.
"tests/unit/scripts/test_build_windows_distribution.py" = ["E501"]
# py2app requires patching finalize_options before importing the platform
# shell-integration module, so that import is deliberately late.
"scripts/setup_macos.py" = ["E402"]
# macOS-only platform tests import the module under test after a
# sys.platform skip guard, so the import is deliberately not at the top.
# Vendored Beacon dialog/UI tests gate wx behind `wx = pytest.importorskip("wx")`
# and import the modules under test after it, so those imports are deliberately
# not at the top of the file (same headless-skip pattern as the macOS tests).
"tests/unit/apps/beacon/*.py" = ["E402"]
"tests/unit/platform/macos/test_high_contrast.py" = ["E402"]
"tests/unit/platform/macos/test_keychain.py" = ["E402"]
"tests/unit/platform/macos/test_sr_detect.py" = ["E402"]
[tool.mypy]
python_version = "3.12"
warn_return_any = true
warn_unused_configs = true
strict_optional = true
check_untyped_defs = true
disallow_untyped_defs = true
exclude = ["quill/ui/"]
# Third-party modules without type stubs or a py.typed marker. Quill treats
# these as untyped at the import boundary so the scoped strict gate
# (mypy quill/core quill/io, see CONTRIBUTING) stays green without weakening
# typing in our own code.
[[tool.mypy.overrides]]
module = [
"defusedxml.*",
"dns",
"dns.*",
"feedparser",
"regex",
"enchant",
"keynote_parser.*",
"faster_whisper",
"ctranslate2",
# Optional kokoro read-aloud fallback (quill[kokoro]) imports torch lazily in
# read_aloud.py; heavy and not in the dev extra, so the scoped gate must not
# depend on it.
"torch",
# Optional Vosk engine (quill[vosk]); imported lazily in providers/vosk.py.
"vosk",
# Optional Nemotron engine (quill[nemotron]); sherpa-onnx imported lazily in
# providers/nemotron_onnx.py. Ships no type stubs.
"sherpa_onnx",
# PyNaCl (Ed25519 unlock-code verification, quill/core/unlock_codes.py); no stubs.
"nacl",
"nacl.*",
# Optional ElevenLabs SDK (quill[elevenlabs]); imported lazily only in the
# host-owned gateway quill/core/ai/elevenlabs_tts.py (roadmap §4.1).
"elevenlabs",
"elevenlabs.*",
# Optional MP3 chapter markers (quill[mp3]); imported lazily in speech/chapters.py.
"mutagen",
"mutagen.*",
"huggingface_hub",
"huggingface_hub.*",
"sounddevice",
"kokoro",
"openpyxl",
# Optional runtime dependencies that are not installed in the scoped-typing
# CI job (which installs only .[dev]); the gate should measure our code, not
# whether an optional extra happens to be present.
"markitdown",
"certifi",
"pdfplumber",
"comtypes",
"comtypes.client",
# Optional GLOW shared-core engine, pulled in only by the `glow` extra.
# core/glow.py imports it behind a guarded try/except; the scoped strict
# gate must not depend on the extra being installed.
"quill_glow_core",
"quill_glow_core.*",
# Optional GitHub integration (quill[github]); imported lazily in github_provider.py.
"github",
"github.*",
# Optional HEIC/HEIF support; imported lazily in core/ai/vision.py behind
# a try/except so the strict gate passes without the [ui] extra installed.
"pillow_heif",
# PIL/Pillow: used by screen_capture (platform layer) and vision.py HEIC
# conversion. Pillow ships py.typed but is not in the dev extra, so mark
# it untyped here to keep the scoped gate clean.
"PIL",
"PIL.*",
# Optional crash-reporting integration; imported lazily in
# core/issue_submit.py behind a try/except. Not in the dev extra.
"feedback_hub",
# Optional native Word writer (quill[docx]); python-docx is imported lazily in
# io/docx_writer.py with a Pandoc fallback, so the scoped gate must not depend
# on it being installed.
"docx",
"docx.*",
# Optional LaTeX->MathML bridge (quill[math]); imported lazily in
# core/math/latex_bridge.py behind a try/except, so the scoped gate must not
# depend on the extra being installed.
"latex2mathml",
"latex2mathml.*",
# Optional yt-dlp (audio-converter URL import, #1255 §4.6); installed on
# demand and imported lazily in core/audio/url_import.py. Ships no stubs.
"yt_dlp",
]
ignore_missing_imports = true