Version 2.0
Quill Radio is QUILL's internet radio, shipped as its own small Windows app for people who want the radio on without loading a full writing environment. It is screen-reader-first, keyboard-complete, and deliberately small -- and by 1.0 it is a complete radio product: organization, recording resilience, timers, and appliance-grade startup.
quill
package (quill.apps.radio, RadioMixin,
AppShellFrame, quill/core/radio/*,
quill/ui/radio/*). This repository contains only the
product wrapper (entry point, icon, installer, docs). Nothing here
reimplements a feature.quill from the upstream repository, and the
one-file build pulls the entire package. Divergence is only permitted
for content that exists because QUILL is not in the picture.%APPDATA%\Quill store QUILL uses.Listening
core/radio/iheart.py,
core/radio/tunein.py,
core/radio/directory_search.py; #1116, #1117, #1132).
iHeart is indexed from its public XML sitemap
(www.iheart.com/sitemap.xml -> the
livestations sub-sitemap, two HTTPS GETs) and each
station's real stream (iHeart-native HLS or a StreamTheWorld redirect)
is resolved lazily from its own page on demand; the sitemap index is
cached once per Browse Stations session with a Refresh button, and a
name search resolves up to IHEART_RESOLVE_CAP = 5 matches.
TuneIn goes through RadioTime's open OPML API
(opml.radiotime.com --
Search.ashx/Browse.ashx/Tune.ashx?partnerId=RadioTime,
no key/auth; a bad guide id self-validates to "nothing found"),
resolving up to TUNEIN_RESOLVE_CAP = 10 per search.
directory_search.merge_and_rank blends all four sources
into one de-duped (by stream URL, then name+country), exact-match-first
list; each non-RadioBrowser row is labeled "via link_finder _PORTAL_HOSTS,
_tunein_candidates; #1131, #1105, #1087). All
failure-tolerant (a down source never blanks the list), egress via each
client's single reviewed _fetch, off in Safe Mode via
refuse_in_safe_mode. This reverses the earlier "TuneIn
deliberately left out" non-goal (upstream QUILL PRD §5.84f), approved
2026-07-17; TuneIn uses RadioTime's public API, not a competitor-data
scrape.core/radio/triton.py): Triton Digital / StreamTheWorld
players (the player.listenlive.co network and thousands of
broadcast stations) compute their stream URL in JavaScript, so it never
appears in the page HTML and a plain-HTML scan finds nothing. Quill
Radio detects the player, reads the station callsign from the Triton
PWA's own logo asset name, and resolves it to a real playable mount
through Triton's JS-free provisioning API
(playerservices.streamtheworld.com), offering both the MP3
and AAC streams. Gated to pages that actually are Triton players and to
a callsign the API validates, so it never surfaces a wrong stream; the
response is parsed through the hardened core/safe_xml
wrapper. One added egress, inventoried in QUILL's network-egress audit
(§N-1), reached only from the same explicit Scan button and disabled in
Safe Mode.core/radio/now_playing.py, #1068): parses the
key="value" broadcast-automation metadata some stations
pack into their ICY StreamTitle (and the plain "Artist - Title"
convention) into title/artist fields, and renders them through a
user-set token template
({title}/{artist}/{raw} with
[optional] segments) stored in
RadioHistory.now_playing_template and edited in Preferences
(Ctrl+,). Default {title}[ by {artist}]. 2.0.1 adds
review/copy: radio.whats_playing_details opens a read-only,
selectable, character-reviewable dialog with a Copy button, and
radio.copy_whats_playing copies the clean text straight to
the clipboard (#1134).core/radio/station_status.py, #1111, #1112): when the ICY
side-tap and the playback engine's own media-title both
come up empty (common on HLS), Quill Radio reads the current title from
the stream server's own public now-playing status endpoint -- Icecast
/status-json.xsl, SHOUTcast v2 /stats?json=1,
or v1 /7.html -- on the same host it is already streaming
from. Same-host only, off in Safe Mode.radio_browser.search_stations offset + the Browse Stations
dialog, #1064): 200 most-listened-first results per request (was 50)
plus a More Stations button that pages the RadioBrowser directory beyond
the first page; a finished search states when more exist and suggests
narrowing.core/radio/recovery.py, #1065): on a playback error, a
confidence-ordered ladder runs off-thread -- re-resolve a moved
StreamTheWorld mount, refresh the URL from the directory, then (opt-in,
RadioHistory.recover_from_website, default on, off in Safe
Mode) scan the station's website with the shared Triton + "Listen Live"
link-following scanner. A single unambiguous result auto-plays and
self-heals the favorite; multiple candidates are announced for the user
to pick in Find Streams. One attempt per station per session; all egress
via the already-reviewed sites. link_finder also now
follows a bounded allowlist of listen/live/play/tune-in
<a> links one level deep, benefiting the manual Find
Streams too.Organization
Recording
quill/core/radio/*, ui/radio/*): the recorder
holds any number of independent recordings, not one.
RadioRecorder became a manager of
{job_id: RecordingJob} -- each job owns its own ffmpeg
process, recent-stderr tail (no cross-contamination of the
fatal/transient verdict), reconnect counter, user-stopped flag, Windows
kill-on-close handle, and resume marker;
active_jobs()/job(id)/active_count
replace the old scalar current_* getters (kept as
back-compat), stop(job_id) / stop_all()
replace the single stop(), and the state-changed callback
carries the job_id.
RecordingSettings.max_concurrent_recordings caps
concurrency (0 = unlimited, the default); at the cap
start() raises RecordingLimitError (a
RecordingError subclass) and the scheduler holds the entry
pending via on_busy and retries within its window -- the
old "already in progress" hard refusal and its fragile string-match in
the scheduler are gone, so overlapping scheduled shows all record. A
reconnect reuses the same job_id (and the original
started_at/scheduled_end) so a recording's row
and marker keep a stable identity across a drop. Record Now targets the
listened station (_radio_playing_job_id): stop it if it is
recording, else start a new job; Record Station's single-recording guard
is removed. UI: the Recordings window lists one row per active recording
with per-row Stop Recording (by job id) and
Stop All Recordings (shown at >=2), mirrored in the
Record menu and the tray/status menu
(radio.stop_all_recordings); the status bar / tray read "(N
recording)". Crash-resume is multi-marker:
recording_resume.py writes one
<job_id>.json per recording under a markers directory
(migrating a legacy single-marker file), and launch offers a single
ResumeRecordingDialog for one interrupted recording or a
batched ResumeRecordingsBatchDialog for several. Everything
lands in the shared quill package; nothing is vendored into
the wrapper.ui/radio/schedule_recording_dialog.py, #1106): Edit an
entry in place, Duplicate it, and Enable/disable it without deleting (a
disabled entry renders "(disabled)" and does not fire); 12-hour or
24-hour time entry (parse_time_of_day, "7:30 PM" or
"19:30"); and a per-entry time-zone dropdown ("(local time)" plus all
zoneinfo zones) so an entry fires at the correct absolute moment and the
list shows zone-labeled times.core/radio/recording.py,
listener request): a "Raw stream -- exactly as sent, no re-encoding
(lossless)" format (format="copy") stream-copies the
server's audio packets with ffmpeg's -c:a copy -- no
decode, no re-encode -- so the saved file is bit-for-bit the original
broadcast, the most faithful capture for a listener who wants to do
their own lossless editing/conversion. Bitrate and Sound Enhancements
are meaningless with no re-encode and are dropped. The output container
follows the stream's own codec, chosen from a one-time bounded
ffprobe of the first audio stream
(mp3->.mp3, aac->.aac,
vorbis->.ogg, opus->.opus,
flac->.flac, ...), falling back to Matroska audio
(.mka) for anything unrecognized -- a universal lossless
copy container; a missing/failed probe degrades to .mka
rather than blocking the recording, and the resolved extension is reused
across auto-reconnect continuation files.quill/core/radio/*,
2.0.0; R1-R4): a reported round of recording bugs closed in four phases,
delivered in the 2.0.0 release. (R1) the Recordings list is an in-place
diff keyed by file path -- a screen reader is no longer yanked to the
top mid-read; the active recording is counted from the recorder itself
(a temp-folder recording is no longer invisible), a firing schedule is
no longer double-counted, completed one-time entries drop out of the
scheduled count, and the active row shows a live elapsed time. (R2) the
scheduler uses a next-due-timestamp window model (an entry is due from
start through start + duration, so a late
arrival starts with the remaining minutes and launch catch-up is free);
last_fired is stamped by entry id only on a successful
start, once entries auto-disable after firing, a
same-minute conflict defers via on_busy, and the scheduler
thread is lock-guarded and can no longer die silently. (R3, new feature)
resume across restart: an ActiveRecordingMarker is written
at start and cleared on a clean stop (a crash leaves it); on launch,
reconcile_temp_strays moves finished temp orphans to the
destination and leaves a still-writing file untouched, then per
RadioHistory.recording_resume_choice
(ask|always|never, default
ask) Quill Radio offers to resume for the remaining minutes
within a 10-minute grace via an accessible dialog
(Resume/Skip/Don't-ask-again; a corrupt marker is discarded). (R4)
pipeline hardening: a reconnect records only the remaining time to
_scheduled_end (not a fresh full duration);
uniquify() replaces the unconditional -y
overwrite and continuation parts keep the original start timestamp; a
drop is classified fatal (disk full / HTTP 404/410/451 only, narrowed in
2.0.1 so a transient 403/408/5xx/network drop reconnects) vs transient
before any reconnect attempt is spent, with the stderr tail cleared on a
reconnect/progress signal so a recovered-from error cannot poison a
later verdict; and on Windows ffmpeg runs in a job object with
JOB_OBJECT_LIMIT_KILL_ON_JOB_CLOSE so a crashed host takes
the child down, with the stop wait moved off the UI thread. Everything
lands in the shared quill package; nothing is vendored into
the wrapper.Timers
Shell
future.adp_assistant is unlocked.core/radio/radio_logging.py,
RadioHistory.debug_mode/log_dir; #1130, #1124,
#1122): a Verbose logging (debug-mode) checkbox in Preferences applied
live via set_radio_debug (no restart), and a settable Log
folder; recording stderr is captured into the log so a failed capture
leaves a trail.Out of scope by decision: QUILL's editor, AI, speech transcription, braille, and TTS stacks (not installed); a custom update engine beyond download-and-run.
data\ folder that switches storage
to travel with the app.%APPDATA%\Quill -- QUILL
or QUILL Cast may still use it.Quill-Radio-Setup-<version>.exe, tagged
v<version>, which Help > Check for Updates
compares against and downloads.www.iheart.com public sitemap index +
on-demand station-page GETs to resolve a stream); TuneIn via RadioTime's
OPML directory (opml.radiotime.com search/browse/tune,
partnerId=RadioTime, no key); the user-typed page for Find
Streams (plus, for a Triton/StreamTheWorld player page or an
iHeart/TuneIn page, one follow-on call to that provider's public API to
resolve the stream); the playing stream itself for ICY titles and, as a
same-host last-resort for What's Playing, that stream server's own
Icecast/SHOUTcast status endpoint; and this repository's GitHub releases
for the update check. Playback (mpv) and the metadata/status requests
send a "Quill Radio" User-Agent. No telemetry of any kind. (Sound
Enhancements' local relay, §8, is loopback-only and never reaches the
network itself -- it filters the same stream this section already
covers.)macOS/Linux standalone builds (upstream QUILL covers macOS; the tray pattern does not exist there), auto-updating in place, telemetry. A full DSP effects rack (reverb, tempo/pitch, spatial audio) -- Sound Enhancements (§8) is a small, purpose-built three-band EQ and compressor, not a general effects rack.
quill/core/radio/backup.py,
ui/radio/backup_ui.py, apps/radio.py,
ui/main_frame_radio.py,
core/radio/radio_browser.py,
core/radio/wxindex.py,
core/radio/favorites.py, ui/window_manager.py,
ui/window_menu.py, ui/dialog_contract.py,
ui/app_shell.py). Everything since 2.1.2; 2.2.0 was never
shipped as a separate build.
BrowseTreeDialog),
Search Stations (StationBrowserDialog), Manage Favorites
(FavoritesManagerDialog), Schedule Recording
(ScheduleRecordingDialog), and the Weather Center
(WeatherCenterDialog) -- convert from
wx.Dialog to modeless wx.Frames, each carrying
the shared menu bar, fixing the reported "menu bar disappears" defect (a
wx.Dialog cannot host a menu bar) and removing the modal
lock-out of the main window. A pure WindowRegistry
(ui/window_manager.py) holds open order + numbering +
cyclic next/previous (unit-tested, wx-free); the wx
WindowManager (ui/window_menu.py) installs a
dynamic &Window menu (numbered 1-9 in open order),
the Ctrl+Tab / Ctrl+Shift+Tab / Ctrl+1..9 accelerators, and
register-on-show / unregister-and-activate-previous on close, with
"Entered ..."/"Exited ..." cues and primary-control focus via
dialog_contract.show_modeless_surface. Each surface takes
an optional windows= param: present (standalone Quill
Radio) -> modeless frame; absent (embedded QUILL) -> the classic
modal dialog, so one class serves both hosts. Design record:
docs/design/2026-07-20-radio-window-model.md.core/radio/backup.py bundles the state files (favorites,
history/settings, wake timer, recording schedule) and, on request, the
recorded audio into a single SHA-manifested .qrbackup zip;
restore_backup validates it, accepts only the known state
filenames, is zip-slip-guarded, and reloads the running app.
Station-menu UI in ui/radio/backup_ui.py (worker-threaded,
native file dialogs). Directly serves R-3 device migration.NowPlayingDialog (arrow/copy the title+artist) instead of
only speaking it; the window existed but had no menu item or key.
Fetch-and-speak fallback when no title is cached.RadioStation.details_text in the
same reviewable window the search results use.radio_browser.fetch_genres/genre_display/fetch_genre_stations
over the existing tag/search API.apps/radio.py) with two persisted visibility toggles.
Show Station Details
(RadioHistory.show_station_details, default on) shows/hides
the read-only details pane in Browse and Search Stations, threaded to
BrowseTreeDialog/StationBrowserDialog via a
show_details param that root.Hides the control
(and its label) so a hidden pane is never a focus stop. Show Status
Bar (RadioHistory.show_status_bar, default on)
shows/hides the main-window status bar.ui/radio/status_bar.py (RadioStatusBar) builds
an arrow-navigable status strip along the bottom of the main window --
cells for Now Playing, Volume (Volume Boost indicator), Recording, Sleep
timer, Favorites count, and time. F6 enters it (a second F6 / Escape
returns focus to the favorites tree), Left/Right/Home/End navigate,
Enter/Space activates (reusing the existing radio_* actions
and _build_radio_status_bar_menu), and each cell has a
context menu. The bar refreshes on every
_refresh_statusbar; apps/radio.py only wires
it (build, View toggle, F6 char-hook branch), so the same bar can serve
embedded QUILL radio later.RadioHistory.favorites_sort), Expand All / Collapse All
Folders on the favorites tree, and a Text Size submenu
(Normal/Large/Larger) driving RadioHistory.ui_font_scale
(clamped [1.0, 2.0]) applied to the tree, buttons, now-playing line, and
status bar via _apply_text_size /
RadioStatusBar.set_font (A-8).FavoritesManagerDialog were
disabled unless the folder was already manual; they now switch to manual
on the first move (_switch_to_manual_if_needed, which flips
the sort only) and move within the preserved stored order, mirroring the
main list's Alt+Shift+Up/Down. The stored list is NOT baked to the
sorted view -- doing so silently destroyed a hand-arranged order
(#1186); the reload reveals the preserved order. The host persists the
switch via _radio_switch_favorites_to_manual (new
on_switch_to_manual callback).platform/keep_awake.set_keep_awake (Windows
SetThreadExecutionState,
ES_CONTINUOUS | ES_SYSTEM_REQUIRED; no-op elsewhere) holds
off system standby while a station plays or a recording runs, released
when neither is active. _update_sleep_inhibitor reads live
playback + recorder state and RadioHistory.prevent_sleep
(default on; Preferences checkbox) on every state change. Display sleep
is deliberately not inhibited.wxindex.states_with_playable_feeds). A later feedback
round also fixed: the transport button dropped its colliding Alt+S/Alt+P
mnemonic so Ctrl+P is the reliable Stop/Play key (#1208); New Folder
Ctrl+Shift+E handled in the app char hook so it fires from the favorites
tree (#1211); Add to Favorites for TuneIn resolves the stream on demand
(#1210); adding a custom station refreshes the favorites tree
immediately (#1205); a Remove All favorites action with
confirmation + rolling-backup recovery
(RadioFavoritesStore.clear,
remove_all_favorites, #1201); the Browse directory cache is
bypassed when the bundled snapshot is newer so an in-place update
surfaces new listings (wxindex._directory_stations, #1207);
and the mpv engine is hard-terminated on exit so audio never outlives
the app (MpvRadioEngine.terminate, #1195). Two more feature
requests: Schedule Recording takes duration as hours (0-24) + minutes
(0-59) instead of a single minutes box, computed to the unchanged
duration_minutes total with a < 1 guard in
build_schedule_entry (#1213); and a wx.Slider
Volume control sits in the main-window Tab order
(arrow-adjustable), wired to
RadioPlayerController.set_volume and kept in step with
Ctrl+Up/Down and per-station memory via _sync_volume_slider
in _refresh_statusbar (#1214).quill/core/radio/wxindex*.py,
core/radio/reading_services.py,
core/radio/directory_search.py,
ui/radio/browse_tree_dialog.py,
ui/main_frame_weather.py,
apps/radio.py).
radio_browser.noaa_weather_stations, removed) for the
authoritative WeatherIndex directory (api.wxindex.org):
a lazy State -> Station tree (call sign, frequency, place), SAME-code
/ callsign / "County, ST" routing in unified search
(directory_search.wxindex_search_stations), and Weather
menu > Listen to your Local NOAA Weather Radio,
resolved from the saved Weather location's county and coordinates
(wxindex.local_stations: county/SAME match first, nearest
covering transmitter fallback). A three-tier resolver -- live API
(Safe-Mode-blocked, in the network-egress audit) -> app-data cache
(<app_data>/radio/wxindex-cache/) -> bundled
snapshot quill/data/noaa_directory.json (1,035 transmitters
across every state and territory; regenerated by
scripts/snapshot_wxindex.py) -- keeps every capability
working offline and outliving the API. Weather menu > Update
NOAA Weather Radio Directory force-pulls the directory into the
cache tier on demand; the bundled snapshot is the permanent floor and is
never overwritten. Stations adapt to RadioStation
(wxindex.to_radio_station), so Favorites, recording, and
scheduling work unchanged. Partially delivers the §18 NWR stream
capability (directory + streams; alert interruption and generated audio
remain future work). Detailed in §18.5.directory_search.reading_services_search_stations) for the
audio information services that read print aloud for blind and
print-disabled listeners. 20 vetted services ship as a bundled snapshot
(quill/data/reading_services.json); Station >
Update Radio Reading Services refreshes live from
RadioBrowser through a cache -> live -> snapshot resolver
mirroring wxindex (Safe-Mode-guarded). Detailed in §18.6-18.7.ui/radio/browse_tree_dialog.py).
The XML sitemap the search path uses carries no genre, so browse reads
iHeart's free, keyless JSON content API (us.api.iheart.com:
/content/genre,
/content/liveStations?genreId=), where each row embeds its
own stream -- one GET per genre, no per-station page fetch. New core:
iheart.parse_genres/parse_genre_stations/fetch_genres/fetch_genre_stations,
routed through the existing reviewed iheart._fetch egress
site (rationale expanded to cover the content API).
Safe-Mode-guarded.directory_search.merge_and_rank now records every absorbed
duplicate's source on a transient, identity-neutral
RadioStation.alt_sources, and the facet matches the full
set via directory_search.station_source_labels; the result
row still badges the winning source.quill/core/radio/*,
quill/apps/radio.py,
quill/ui/radio/*).
pan=stereo|c0=c0|c1=c0 duplicated a single source channel
to both outputs; corrected to send the whole stereo field to one output
and silence the other (pan=stereo|c0=0.5*c0+0.5*c1|c1=0*c0,
and the mirror for right) in core/audio_enhance.py.RadioHistory.favorites_sort (az/za/manual, default az) +
per-folder folder_sort_orders; non-mutating
RadioFavoritesStore.favorites_in_display_order /
folders_in_display_order so the manual order survives.
Applied to the main tree, the Favorites Manager (Move buttons disabled
for sorted folders), and the Station-menu submenu; re-sorts on add.
Preferences choice for the default; a folder context-menu override.core/radio/playlist_import.parse_m3u +
split_new_and_duplicates; Station > Import Stations from
Playlist... with folder targeting/creation at any depth and a
skip-vs-import-all duplicate prompt.quill/core/radio/*,
quill/ui/radio/*). From the first round of live
feedback:
_FATAL_STDERR_RE (core/radio/recording.py) now
matches only genuinely-terminal outcomes (disk full; HTTP 404/410/451);
a transient 403 (rotating CDN token), 408/409, 5xx, or bare EOF
reconnects within the attempt budget. The stderr tail is cleared on a
reconnect/progress signal (_RECOVERY_STDERR_RE) so an error
ffmpeg recovered from can't poison a later drop's verdict -- fixing
"recording stops after ~1 minute" reports.main_frame_radio.radio_record_toggle /
open_record_station_dialog).radio.whats_playing_details (a read-only, selectable,
char-reviewable NowPlayingDialog with Copy) and
radio.copy_whats_playing (_copy_to_clipboard);
no new setting.RadioHistory.channel_mode and
FavoriteStation.channel_mode (stereo/mono/left/right)
replace the mono_enabled bool (migrated on load).
audio_enhance gains left/right pan filters
(pan=stereo|c0=c0|c1=c0 / ...|c0=c1|c1=c1);
the Sound Enhancements dialog's mono checkbox becomes a Channel-mode
RadioBox. Global default AND per-station override (resolved with the EQ
via _radio_resolve_enhancement); night mode stays
global.Ctrl+Up/Down
char-hook on the (modal) Recordings dialog drives the shared
controller's volume, so a played-back recording is adjustable like a
live stream.quill/core/radio/*; R1-R4). The headline of 2.0.0:
a reported round of recording bugs closed and the one missing piece
added -- a recording that survives a restart. Resume across restart with
an ask-on-launch dialog and a 10-minute grace window; window-based
scheduling with launch catch-up; a flicker-free, place-keeping
Recordings list with honest counts and a live elapsed time; and pipeline
hardening against dropped connections, dead streams, and a crashed host.
Detailed as delivered scope in §3 (Recording). Everything lands in the
shared quill package; nothing is vendored into the
wrapper.core/radio/iheart.py,
core/radio/tunein.py,
core/radio/directory_search.py; #1116, #1117,
#1132). Two of the largest internet-radio directories added as
keyless, account-free station sources blended into Browse Stations, with
a Source facet, genre/country dropdown filters, "via ui/radio/schedule_recording_dialog.py, #1106).
Edit, Duplicate, and Enable/disable for schedule entries; 12-or-24-hour
time entry; per-entry time zones with zone-labeled list times. Detailed
in §3 (Recording).core/radio/station_status.py, #1111,
#1112). A same-host last resort reading the stream server's own
Icecast/SHOUTcast now-playing status page when ICY and the engine title
channel are both empty. Detailed in §3 (Listening).core/radio/radio_logging.py; #1130, #1124, #1122).
A live Verbose logging (debug-mode) checkbox and a settable Log folder
in Preferences, with recording stderr captured to the log. Detailed in
§3 (Shell).quill/ui/radio/mpv_radio_engine.py +
player_controller.py, #1076). A second, preferred
audio backend: libmpv, live-stream-aware (readiness from
core-idle, not the duration a live stream never reports),
bundled at {app}\tools\mpv.
RadioHistory.playback_engine = auto (mpv when present) / wx
("Windows Media (classic)", the byte-for-byte pre-1.1 escape hatch) /
mpv; one silent cross-engine rescue per play attempt in either
direction. Combined stream-format coverage is effectively complete: MP3,
AAC and HE-AAC (AAC+), Ogg Vorbis, Opus, FLAC streams, and HLS (m3u8) --
Ogg Vorbis/Opus/HLS were undecodable by WMP. The engine delivers:
Radio output device (Preferences;
RadioHistory.output_device; screen reader and app sounds
stay on the system default; unplugged devices remembered, spoken
fallback when unusable); live DVR (a seekable
~45-minute demuxer cache: pause/resume live radio, Rewind/Forward 30
Seconds, Back to Live, each announcing how far behind live);
Volume Boost (up to 150% for quiet stations; the 0-100
scale, per-station volumes, and mute untouched); engine-native
What's Playing fallback (mpv media-title when the
ICY side-tap gets nothing or the stream is HLS); and
"Buffering..." announcements on mid-stream stalls.core/audio_enhance.py,
ui/sound_enhance_dialog.py). One-touch
broadcast-processing modes in Sound Enhancements, adapted from OptiLab
Core by dgl1984 (github.com/dgl1984/optilab, Apache-2.0). OptiLab is a
GUI-only plugin (JSFX/CLAP/Winamp DSP) with no library API and a
Windows-64-only binary, so rather than hosting the binary the modes
reproduce the shape of its three chains -- Podcast Leveler (HPF
-> speechnorm -> acompressor -> 65 Hz tame -> alimiter),
Stream Polish (dynaudnorm -> acompressor -> presence ->
alimiter), Smooth Limiter (acompressor -> alimiter) -- as ffmpeg
filter chains appended last in build_filter_graph (their
lookahead limiter guards the output), so they ride the same three
delivery paths (mpv-native live, relay, recordings) and work
cross-platform. Controls map OptiLab's onto ffmpeg: Mode picks the
chain, Input is a front-end volume trim (0 dB default, per
product choice), Auto-Adapt (0-100%) leans the leveling/density more
assertive. A bypass checkbox (optilab_enabled) keeps the
chosen mode remembered while off. Stored on RadioHistory
and, as of 2.0.2, also per-station on FavoriteStation,
carried by
RadioPlayerController.set_sound_options/preview_enhancements
and resolved per stream by ResolvedEnhancement. A faithful
adaptation, not a bit-for-bit port; credited in release notes, the About
box, and the third-party notices.ui/sound_enhance_dialog.py,
ui/radio/player_controller.py). Every control in
the Sound Enhancements dialog previews live on the playing stream via an
on_live_change callback (debounced ~180 ms) into
RadioPlayerController.preview_enhancements (one apply for
the whole set, so a wx drag reconnects once, not per field; mpv applies
natively). OK keeps; Cancel/Escape reverts to the snapshot captured on
open (the Reset button's own restore is exempted). And all
listener-level settings -- previously EQ/compressor/channel per-station
but night mode + OptiLab global-only -- are now per-station as well as
global: FavoriteStation gained
night_mode_enabled/optilab_*,
set_enhancement and the ResolvedEnhancement
NamedTuple carry the full set, play_station applies all of
it per stream, and open_sound_enhancements saves the whole
dialog to the favorite override (when a favorite plays) or the shared
RadioHistory default (otherwise). Reset to Default and
Reset All Stations both restore the shared default including night mode
and OptiLab.FavoriteStation, mirroring QUILL Cast's
per-podcast override) as well as a shared default in
RadioHistory; RadioPlayerController resolves
which applies via an injected callback at the top of every
play_station. Recording Settings' "Apply Sound Enhancements
to recordings" (off by default) optionally records the filtered audio
too.See CHANGELOG.md for the full, versioned history.
The following is the complete QUILL Weather product-requirements document. The Weather menu shipped in 2.1.0 implements its text-only first slice; the sections below describe the full roadmap.
Working title: QUILL Weather
Ecosystem: QUILL / QuillVille
Document status: Product definition and
implementation-ready working draft
Version: 1.0
Date: July 19, 2026
Primary platforms: Windows first; macOS next; iOS
considered in later phases
Product posture: Accessibility-first,
screen-reader-first, keyboard-first, local-first, provider-based,
safety-conscious
QUILL Weather transforms official weather data into an immediate, understandable, highly configurable, and delightfully accessible weather experience.
It is not merely a weather screen. It is a persistent Weather Guardian, an accessible Weather Center, a flexible audio weather channel generator, a location-aware Alert Center, and a deeply configurable Voice Studio built on the QUILL speech framework.
A user should be able to:
The initial primary data provider will be the United States National
Weather Service at api.weather.gov. The NWS API provides
forecasts, hourly forecasts, observations, alerts, zones, stations, and
grid data as open government data. It is cache-aware, supports
conditional requests, and requires an identifying User-Agent. NWS
recommends requesting alert updates no more frequently than every 30
seconds.
For faster alerts, a later QUILL Alert Relay can subscribe to the NOAA Weather Wire Service Open Interface. NWS describes NWWS as its fastest method for receiving text alerts and weather products, generally within 10 seconds of issuance. The relay will reconcile those pushed products with the public NWS alerts API and deliver normalized updates to subscribed QUILL clients.
QUILL Weather must never imply that it replaces Wireless Emergency Alerts, a physical NOAA Weather Radio, local emergency instructions, or other official safety channels. It is an additional accessible delivery and interpretation tool.
Weather should never be hidden behind a map, buried in a dashboard, delayed by an inaccessible workflow, or spoken in a voice the user cannot understand.
QUILL Weather meets people where they are. It provides as much or as little weather information as the user wants, in the voice they choose, for the places and people they care about.
QUILL Weather makes five promises:
Every location, alert, forecast period, setting, history item, source status, and audio control is keyboard accessible and represented through native or predictably accessible controls.
An alert’s status, severity, urgency, certainty, effective time, expiration, affected area, source, update history, and delivery state are available as text and speech. Color, icon, animation, and screen position are never the only means of conveying meaning.
QUILL preserves the source alert, its identifiers, its lifecycle, and its authoritative text. QUILL may organize or deterministically summarize information, but it will not silently rewrite emergency instructions.
Different weather content can use different QUILL speech providers, voices, rates, pitches, volumes, pronunciation dictionaries, earcons, and interruption behaviors.
QUILL prioritizes alert speed while using deduplication, update reconciliation, source freshness, delivery logging, and transparent fallback behavior.
QUILL Weather will be considered successful when:
The initial product will not:
The main accessible weather workspace.
Primary sections:
The Weather Center will use a simple semantic structure with predictable headings, lists, property views, and command menus. Users can choose a compact view or detailed view.
A lightweight background process responsible for:
Weather Guardian starts with the user only when explicitly enabled. It does not require administrator privileges.
Closing the Weather Center does not close Weather Guardian. Exiting Weather Guardian requires an explicit Exit Monitoring command.
A Weather Channel is a generated audio feed assembled from selected structured content.
Example channel:
Channels can be played on demand or continuously. New alerts can interrupt or queue according to the channel’s alert policy.
A complete, searchable history and current-state view for all monitored locations.
It distinguishes:
The Alert Center preserves alert revisions and clearly identifies what changed.
The configuration surface for weather speech scenarios.
Voice Studio allows the user to assign voices and behaviors by:
A searchable transmitter and stream catalog containing:
QUILL must clearly differentiate:
On first launch, QUILL Weather asks one accessible question:
Which location would you like to use first?
Available methods:
The user reviews resolved choices before saving. QUILL announces ambiguities such as multiple cities with the same name.
After selection, QUILL immediately presents:
The user is then offered an optional, clearly explained choice to enable Weather Guardian at sign-in.
A configurable global command speaks:
Example:
Phoenix. 108 degrees, feels like 112. Mostly sunny. Southwest wind 8 miles per hour. One Excessive Heat Warning is active. Conditions were updated 6 minutes ago.
The quick response must be deterministic and configurable.
When a new alert arrives:
Example spoken sequence:
Weather Warning. Tornado Warning for Pima County, including the Tucson area, until 4:45 PM. Take shelter now. Press the configured Alert Details command for the complete official message.
For warnings where the official message contains instructions, the user must be able to hear those instructions immediately without navigating through unrelated details.
An update must not be treated as a duplicate merely because the event name is unchanged.
QUILL compares:
The announcement can say:
Update to the Tornado Warning for Pima County. The warning now expires at 5:15 PM. The affected area has expanded eastward. Instructions remain unchanged.
A user can choose:
When an alert is cancelled or expires:
The Severe Thunderstorm Warning for Maricopa County has expired. Two other advisories remain active.
A user starts “Home Weather Radio.”
QUILL generates a continuous audio experience using a selected program clock. Routine content repeats at user-defined intervals, but only changed content needs to be spoken on every cycle.
A new warning can:
Critical alerts default to immediate interruption, but the user retains control.
A user creates a group named “Family” containing:
The group can use:
Example:
Family Weather Scan. Phoenix has one warning. Tucson has no active alerts. Austin has a Heat Advisory.
Travel mode can monitor:
Current location is sampled only with permission. Precise location history is not retained unless the user explicitly enables it.
QUILL supports:
Each saved location includes:
{
"id": "loc_uuid",
"display_name": "Home",
"resolved_name": "Phoenix, Arizona",
"latitude": 33.4484,
"longitude": -112.0740,
"timezone": "America/Phoenix",
"country": "US",
"state": "AZ",
"county_name": "Maricopa",
"county_zone": "AZC013",
"forecast_zone": "AZZ543",
"fire_zone": null,
"marine_zone": null,
"nws_office": "PSR",
"grid_x": 159,
"grid_y": 57,
"forecast_url": "...",
"hourly_forecast_url": "...",
"grid_data_url": "...",
"observation_station_ids": [],
"nwr_transmitters": [],
"source_resolved_at": "2026-07-19T17:00:00Z",
"privacy_classification": "precise",
"sync_enabled": false
}Actual provider URLs are stored as provider-owned metadata and can be refreshed.
The NWS API requires latitude and longitude for point metadata and does not provide general address geocoding. QUILL therefore uses a pluggable Geocoder Provider interface.
Resolution sequence:
/points/{lat},{lon}
endpoint.A feed is a named weather experience containing one or more locations and one or more content segments.
Example feed types:
{
"id": "feed_uuid",
"name": "Home Weather Radio",
"locations": ["loc_home"],
"segments": [
"identity",
"critical_alerts",
"current_conditions",
"next_six_hours",
"today_tonight",
"extended_forecast",
"hazardous_outlook",
"source_status"
],
"repeat_interval_minutes": 15,
"speak_only_changes_after_first_cycle": true,
"alert_interruption_policy": "critical_immediate",
"voice_profile_id": "voice_home_radio",
"output_device_id": "default",
"live_stream_fallback_policy": "generated_audio",
"enabled": true
}Supported segment types include:
Every segment exposes:
The QUILL speech framework is not merely a text-to-speech output switch. For QUILL Weather it becomes a scenario router.
A scenario answers:
Rules can be assigned at these levels, from broadest to most specific:
The most specific enabled rule wins. The resolved rule is inspectable.
{
"id": "scenario_uuid",
"name": "Critical Warning Voice",
"match": {
"content_domain": "alert",
"severity": ["Extreme", "Severe"],
"urgency": ["Immediate", "Expected"],
"event_types": ["Tornado Warning", "Flash Flood Warning"],
"feed_ids": ["*"],
"location_ids": ["*"]
},
"speech": {
"provider_id": "sapi5",
"voice_id": "Microsoft David Desktop",
"rate": -1,
"pitch": 0,
"volume": 100,
"language": "en-US",
"pronunciation_dictionary_id": "weather_terms",
"number_style": "natural",
"time_style": "local_explicit",
"units_style": "spoken_full"
},
"presentation": {
"earcon_id": "warning_critical",
"earcon_before": true,
"earcon_after": false,
"priority": "critical",
"interruption": "immediate",
"duck_other_audio_percent": 80,
"repeat_policy": "until_acknowledged_or_changed",
"repeat_interval_minutes": 5,
"maximum_repeats": 3
},
"fallbacks": [
"weather_default_voice",
"system_default_voice",
"screen_reader_announcement"
]
}QUILL ships with editable defaults:
A user can make each generated feed sound distinct.
Example:
A feed may also use multiple voices internally:
The renderer must correctly handle:
Examples:
WSW 8 mph becomes “west southwest wind at 8 miles per
hour.”40% becomes “a 40 percent chance.”AZC013 is normally spoken as “Maricopa County,” with
the code available in details.KPHX can be spoken as “K P H X” or “Phoenix Sky
Harbor,” based on context.Weather Voice Studio supports:
Users can correct local names without modifying source text.
If a selected voice or engine fails:
Default endpoint patterns include:
The default local polling interval is 30 seconds, matching NWS guidance not to request alert updates more frequently.
The relay can receive NOAA Weather Wire Service products through NWWS-OI, which requires NWS-issued credentials and an XMPP client. The relay:
A future provider may add FEMA IPAWS for non-weather and broader all-hazards alerts, subject to access requirements, agreements, testing, and explicit provenance.
Recommended default after the relay is production ready:
The normalized alert model must preserve at least:
{
"provider": "nws",
"source_id": "official-alert-id",
"status": "Actual",
"message_type": "Alert",
"scope": "Public",
"sent": "...",
"effective": "...",
"onset": "...",
"expires": "...",
"ends": "...",
"event": "Flash Flood Warning",
"sender": "...",
"sender_name": "...",
"headline": "...",
"description": "...",
"instruction": "...",
"response": "Shelter",
"urgency": "Immediate",
"severity": "Severe",
"certainty": "Observed",
"area_description": "...",
"geometry": {},
"geocodes": {},
"affected_zones": [],
"references": [],
"parameters": {},
"language": "en-US",
"raw_payload_hash": "...",
"received_at": "...",
"normalized_at": "..."
}Raw payloads are retained according to the user’s history setting so developers and users can verify interpretation.
An alert can match a location through:
Where geometry and zone matching disagree, QUILL records the discrepancy and follows a configurable conservative policy. Default behavior favors notifying the user rather than suppressing a potentially relevant warning.
QUILL uses:
A revision graph links all related alert versions.
A repeated provider response with no meaningful change is not reannounced unless the user selected periodic reminders.
QUILL does not reduce urgency, severity, and certainty to a single hidden number. It preserves all three.
For delivery, QUILL computes a transparent priority tier:
Users can inspect why a tier was selected.
The default mapping considers:
An accessible notification can offer:
Users can configure:
Default behavior:
A “Silence all critical alerts” action requires explicit confirmation, states the consequence, and can be time-limited.
For life-safety alerts:
For each point location:
WeatherProvider
get_capabilities()
resolve_point_metadata()
get_forecast()
get_hourly_forecast()
get_grid_data()
get_observation_stations()
get_latest_observations()
get_alerts()
get_alert()
get_zones()
get_text_products()
get_source_status()
GeocoderProvider
search()
reverse_geocode()
normalize_result()
AlertPushProvider
connect()
subscribe()
unsubscribe()
receive()
acknowledge_cursor()
get_status()
NwrMetadataProvider
search_transmitters()
get_county_coverage()
get_transmitter_status()
get_same_codes()
WeatherAudioStreamProvider
search_streams()
resolve_stream()
verify_health()
get_provenance()
Providers register capabilities through stable contribution points. Provider provenance is available in text and speech.
QUILL stores:
NWS gridpoint data may represent values across ISO 8601 time intervals rather than one record per hour. QUILL’s interval engine must:
QUILL supports:
The user can set units globally, by feed, or by content type.
The source value is never destroyed when converted.
Observation station selection considers:
QUILL states the observation source and age.
It must distinguish:
Every view and speech response can expose:
Default stale thresholds are content-specific.
Example:
Current conditions were last observed 47 minutes ago and may be stale. The forecast was updated 18 minutes ago.
QUILL must not hide stale data behind a generic “updated” label.
QUILL honors:
The client avoids cache-busting query parameters.
Point-to-grid mapping is cached long-term and refreshed on provider errors, source changes, or a scheduled maintenance interval.
When a request fails:
The tray item has an accessible name reflecting state:
Visual icons may differ, but text state is authoritative.
Keyboard-accessible commands:
Destructive or safety-relevant commands include confirmation and a clear status announcement.
Notifications must:
Global commands are opt-in and configurable.
Suggested defaults:
QUILL checks for conflicts and allows reassignment.
Settings include:
Per location:
Every context menu is reachable through keyboard commands and Shift+F10 where applicable.
List items support:
QUILL Weather displays a concise notice during onboarding and in About:
QUILL Weather is an additional accessible weather information tool. Delivery can be delayed or interrupted by network, device, provider, or software failures. Do not rely on QUILL Weather as your only source of emergency information.
Every weather object has:
Generated audio identifies itself as QUILL-generated weather using official data.
QUILL does not silently:
Test alerts are clearly identified through:
Users can choose to announce, log, or ignore provider-designated tests, but development simulation mode cannot impersonate an actual alert without a persistent simulation label.
SQLite is recommended for:
locationslocation_groupslocation_group_membersprovider_point_metadataweather_snapshotsforecast_periodstime_series_valuesobservation_stationsobservationsalertsalert_revisionsalert_location_matchesalert_deliveriesalert_acknowledgmentsfeedsfeed_segmentsvoice_profilesvoice_scenariospronunciation_entriesnwr_transmittersnwr_coverageweather_streamsstream_health_checkssource_status_eventssettingsDefaults:
All are configurable within safe storage limits.
The relay exists to improve speed, scalability, and resilience—not to make local weather dependent on the cloud.
Clients preferably subscribe using:
Exact addresses are not sent.
For point-specific polygon matching, options are:
The first option is the preferred initial design.
Recommended protocol:
The client continuously knows:
A relay failure automatically activates local polling if enabled.
QUILL imports and normalizes official transmitter information:
A stream record includes:
{
"id": "stream_uuid",
"call_sign": "WXL30",
"transmitter_name": "Phoenix",
"stream_url": "...",
"provider_name": "Community Receiver Operator",
"provider_type": "community",
"official_noaa_stream": false,
"codec": "MP3",
"bitrate": 32,
"last_verified": "...",
"health": "online",
"terms": "...",
"redistribution_allowed": true
}A later QUILL Community Receiver program may provide:
The NWR directory-and-streams portion of this section shipped in
Quill Radio 2.1.1, powered by the WeatherIndex API
(https://api.wxindex.org) -- a curated, no-auth JSON
directory of NWR transmitters plus internet re-stream URLs, organized by
state, county/SAME, and NWS Weather Forecast Office.
quill/core/radio/wxindex.py, wx-free):
list_states, stations_for_state,
search_stations(county/state/same/callsign),
station_detail,
local_stations(lat, lon, county), and
to_radio_station adapting each transmitter to the existing
playable model, so Favorites, recording, and scheduling required no new
code. WxStation carries call sign, frequency, state,
county/SAME coverage, WFO, coordinates, and the ordered re-stream feed
URLs (best first) -- the metadata set of §18.1 as far as the upstream
directory provides it.quill/data/noaa_directory.json, 1,035
transmitters, regenerated by scripts/snapshot_wxindex.py)
-- so browse, search, and local-station resolution work fully offline
and survive the API disappearing. A corrupt or missing snapshot logs and
yields an empty directory rather than raising.Radio reading services -- the audio information services affiliated with IAAIS (the International Association of Audio Information Services) that read newspapers, magazines, and local print aloud for people who are blind or print-disabled -- are a natural companion to community NWR audio and shipped alongside it:
quill/core/radio/reading_services.py and
directory_search.py.quill/data/reading_services.json (WRBH 88.3 Reading Radio,
Sun Sounds of Arizona, CRIS Radio / The Chicago Lighthouse, Connecticut
Radio Information System, KPBS and WKAR reading services, WUFT RRS, Down
East RRS, Recording Library of West Texas, Audible Local Ledger, Owl
Radio, Voice Corps, 95alive, ACB Media 1-5, NFB Radio Network, and the
American Council of the Blind stream), so the category works
offline.The bundled list was built with a discovery pass, kept here as the method for future refreshes of the curated set:
Recommended reading order:
Default sort:
Each item speaks:
Tornado Warning. Pima County. Immediate, extreme, observed. Updated 2 minutes ago. Expires at 4:45 PM.
Headings:
Accessible list alternatives:
A visual chart may be included but never replaces the list.
All settings are searchable by plain language.
Example search terms:
Search results explain the setting path and current value.
Suggested command IDs:
weather.openCenter
weather.speakQuick
weather.openAlerts
weather.repeatLastMessage
weather.stopSpeech
weather.startFeed
weather.stopFeed
weather.switchLocation
weather.addLocation
weather.openAlertInstructions
weather.acknowledgeAlert
weather.openVoiceStudio
weather.openSourceStatus
weather.refresh
weather.pauseMonitoring
weather.resumeMonitoring
Extension-contributed commands use the QUILL extension naming convention, such as:
ext.vendor.weatherCommand
Extensions may contribute:
An extension cannot silently suppress critical alerts. Any suppression capability requires explicit user authorization and is visible in diagnostics.
Source Status answers:
The user can create a privacy-reviewed support bundle containing:
Precise coordinates, addresses, alert text, and account identifiers are excluded by default and require explicit inclusion.
Expert users and developers can inspect:
The inspector is fully accessible and supports copying selected sections.
Saved fixtures for:
A built-in developer and QA laboratory can simulate:
Every simulation is unmistakably marked as a simulation.
Test with:
Potential future additions:
Each addition must preserve provider provenance and accessibility.
Mitigation: Clear safety notice, source-status visibility, failure announcements, no claims of certification, and encouragement to use multiple official channels.
Mitigation: Revision graph, deterministic change comparison, acknowledgment, changes-only announcements, and configurable repeat rules.
Mitigation: Transparent rule trace, critical-silence confirmation, safety review, reset-to-safe-defaults command, and diagnostics showing suppressed events.
Mitigation: Conditional requests, centralized relay for scale, backoff, cache, local fallback, stale-state communication, and provider abstraction.
Mitigation: Multi-step fallback chain, screen-reader/OS notification fallback, and delivery logging.
Mitigation: Combine point, geometry, and zone methods; conservative default; match explanation; testing near boundaries; user-selected county/zone override.
Mitigation: Health checks, multiple streams, generated audio fallback, and independent structured alert monitoring.
Mitigation: No generative rewrite as the authoritative alert; official text always primary; deterministic summaries; explicit labels.
Mitigation: local-first storage, no default history, optional coarse subscriptions, encrypted sync, redacted diagnostics, and explicit permissions.
quill_weather/
providers/
nws/
geocoding/
nwr/
streams/
domain/
locations/
forecasts/
observations/
alerts/
feeds/
speech/
services/
weather_guardian/
alert_monitor/
feed_engine/
cache/
sync/
notifications/
ui/
weather_center/
alert_center/
location_manager/
feed_builder/
voice_studio/
settings/
diagnostics/
platform/
windows/
macos/
storage/
tests/
The user can modify the mapping, but the dispatcher always exposes the active queue and allows immediate stop.
Home, Phoenix. 108 degrees and mostly sunny. It feels like 112. Southwest wind at 8 miles per hour. An Excessive Heat Warning is active until 8 PM Monday. The observation is 6 minutes old.
Critical weather alert for Home. Flash Flood Warning. Immediate, severe, and observed. In effect until 6:30 PM. Move to higher ground now. Do not drive through flooded roadways. Press the Alert Details command to hear the complete official message.
Update for Home. The Flash Flood Warning has been extended until 7:15 PM. The affected area now includes northern Maricopa County. Official instructions are unchanged.
Weather data for Travel Location may be stale. QUILL last reached the National Weather Service 38 minutes ago. Alert monitoring is retrying.
QUILL Alert Relay is unavailable. Direct National Weather Service alert monitoring remains active and checks every 30 seconds.
The initial design is grounded in these official NWS capabilities and constraints:
The NWS API provides forecasts, alerts, observations, and other
weather data as open data without usage fees, subject to reasonable rate
limits.
https://www.weather.gov/documentation/services-web-api
NWS forecast lookup begins with latitude and longitude through
/points/{lat},{lon}, which returns forecast, hourly
forecast, and gridpoint metadata. The point mapping can be cached.
https://weather-gov.github.io/api/general-faqs
The NWS alerts service supports JSON-LD, CAP v1.2, and Atom. NWS
recommends requesting new alerts no more frequently than every 30
seconds.
https://www.weather.gov/documentation/services-web-alerts
NWS CAP fields such as urgency, severity, and certainty are
specifically intended to support decision tools and synthesized voice
applications.
https://www.weather.gov/documentation/services-web-alerts
NOAA Weather Wire Service is described by NWS as its fastest
method of receiving text alerts and weather information, within
approximately 10 seconds of issuance. NWWS-OI requires NWS-issued
credentials and an XMPP client.
https://www.weather.gov/nwws/faq
NOAA Weather Radio is a nationwide network of more than 1,000
transmitters broadcasting continuous official information on seven VHF
frequencies. It is fundamentally a radio transmitter service and
requires a compatible receiver.
https://www.weather.gov/nwr
Official NWR station search and county coverage information
includes call signs, frequencies, SAME codes, and transmitter
coverage.
https://www.weather.gov/nwr/station_search
https://www.weather.gov/nwr/county_coverage
QUILL Weather should feel like a trusted weather desk that belongs to the user.
It should be fast without being frantic, detailed without being overwhelming, configurable without becoming inaccessible, and powerful without hiding what it is doing.
The product’s defining achievement will not simply be that it speaks the weather. It will be that it understands the structure, timing, source, priority, location, and lifecycle of weather information—and then gives every user direct control over how that information reaches them.
QUILL Weather will turn official data into an accessible living service: