Commit f501067
authored
Zerm 2.7.0 — honest metrics, working enhancement, real docs (#277)
* Store usage statistics so the dashboard stops resetting
Every dashboard number was recomputed by scanning surviving Transcription
rows, and TranscriptionAutoCleanupService hard-deletes those rows on a
retention timer defaulting to 1440 minutes. Lifetime totals were therefore
only ever a view of whatever history had not been swept yet, which is why
the dashboard appeared to reset itself daily.
Aggregates now live in their own usage.store, alongside the existing
transcript and dictionary stores. The separation is the point: transcript
retention cannot reach it, and it holds counts and durations only — no
transcript text — so it is safe to keep even under zero retention. Existing
history is backfilled once; anything an earlier sweep already deleted is
unrecoverable, and the docs say so rather than implying otherwise.
Also fixed in the same area:
- sweepOldTranscriptions used max(retentionMinutes, 0) and never read the
declared defaultRetentionMinutes, so an unset key read as 0 and set the
cutoff to now, deleting the entire history at launch. An explicit 0 is
still honoured as "Immediately".
- AudioFileTranscriptionService created records without a status, leaving
them pending forever and invisible to the dashboard while still showing
in History.
- Added Reset Statistics: clearing transcripts used to clear the numbers as
a side effect, and making them durable removed the only way to erase them.
The dashboard is rebuilt around a range selector and charts, and drops
DashboardPromotionsSection (both flags hardcoded false, unreachable) and
MetricsSetupView (no references).
* Run AI enhancement after the paste, not before it
AI enhancement was dead product-wide while its toggle read ON.
InstantTranscriptionMode was a UserDefaults flag with no UI anywhere,
defaulted true and force-set true for every existing install by a
migration. It disabled enhancement three separate ways: clearing
isEnhancementEnabled on every launch (and persisting that through didSet,
so the user's choice was erased between sessions), gating the pipeline
branch so a toggle reading ON never enhanced, and clearing it again on
every Power Mode switch. The same migration pinned the enhancement timeout
to 2 seconds with retry off, so anything that did run timed out and
silently pasted the raw transcript.
Enhancement was only slow because it sat before the paste. It now sits
after. DictationOutputMode replaces the flag with Instant, Instant +
Refine, and Enhanced. In Instant + Refine the raw transcript is pasted at
the existing latency, the enhancement runs concurrently, and the inserted
range is replaced in place through the Accessibility API once it returns.
The paste path gains no work: the anchor read runs concurrently with the
clipboard write and resolves inside the pre-paste delay that already
existed. Measured hotkey to audio-unit-running still reaches 85 ms, inside
the documented 79-86 ms band.
Replacement is gated hard and fails closed, so it can fail to improve the
text but cannot corrupt it. It genuinely works only in native AppKit text
views; Chromium and WebKit implement no working AXSelectedText setter, and
terminals are on a deny-list, so for Electron apps, browser content and
terminals the refined text is offered instead. That fallback is the usual
path, not an edge case.
Notable interactions: auto-send is incompatible and degrades to Enhanced;
the selected-text read posts a synthetic Cmd+C and is suppressed for
refine, which would otherwise fire while the user is typing; refine yields
to Read Aloud, since both queue on the same on-device model actor; and the
prompt is pinned for the request, because dismissing the recorder ends the
Power Mode session and restores the global selection mid-refine.
Power Mode enhancement becomes a tri-state. The old bool could not express
"leave it alone", so every Power Mode — including the seeded default that
matches when nothing else does — forced enhancement off on every recording.
* Replace the unregistered docs domain with a real documentation site
All seventeen in-app links pointed at tryzerm.com, a domain that is not
registered — NXDOMAIN, no WHOIS record. It was a find/replace artifact from
upstream VoiceInk's tryvoiceink.com. The real site is arcusis.github.io/Zerm,
which had no /docs/ subtree at all, so even a corrected hostname would have
404'd.
Links.swift is the URL registry the app never had; every link was an inline
string literal, which is how this drifted unnoticed. Sixteen documentation
pages are authored as markdown under site-content/docs and generated into
docs/docs by the existing site builder, with screenshots of the app.
Also removed: the leftover VoiceInk monetization routes (/buy, /affiliate,
/social-share), which Zerm has no equivalent for.
The support flow opened a mail draft addressed to the upstream maintainer's
personal address, so every Zerm support report — including the attached
system information — was being sent to an unrelated person. It now opens the
project's own issue tracker.
announcements.json was never published, so the feed the app fetches on every
launch returned 404, and its contents announced a "Zerm iOS App" that does
not exist. It is now published, mirrored from the repo root on every build.
* Explain every setting in place
Roughly forty user-facing options across the ten panes had no explanation
at all, including the whole of Power Mode's trigger and enhancement
sections, Audio Input, Dictionary, Read Aloud and most of Settings.
InfoTip is rebuilt on a Button rather than a tap gesture, so it now has a
hover cursor, keyboard activation and a VoiceOver label. Its "Learn more"
affordance is a discrete Link instead of a tap handler attached to the
whole popover body, which previously meant tapping anywhere in the popover
opened the URL and the popover never dismissed.
Copy notes: Power Mode website matching is a substring match on a cleaned
URL, which nothing in the interface said; the Power Mode empty state was
grammatically broken VoiceInk-era text; and the enhancement and dictionary
settings buttons were bare gear glyphs in section headers, identified only
by a tooltip.
The recorder's enhancement indicator distinguished on from off by a 40%
opacity step on a black pill, which is not a state anyone could read at a
glance. It is now a filled accent capsule.
* Run the test suite, and guard the docs in CI
The scheme has always had a TestAction wired to ZermTests, but nothing ever
invoked it — no make target and no CI step — so the suite had never run.
Debug configuration is required, not incidental: @testable import needs
ENABLE_TESTABILITY, which Release turns off, and the tests fail to compile
without it. That is why this was never noticed.
The website lint gains three checks, each covering a failure that actually
shipped: every Links.Doc slug must resolve to a published page and back,
every screenshot a docs page embeds must exist, and no file may reference
the unregistered domain again.
* Release 2.7.0
Version 2.7.0, build 270.
Notebook: two new notes covering the subsystems added here — the refine-in-
place design, including where accessibility replacement cannot work and why
the fallback is the usual path, and the durable usage store, including why
it is a separate store and why clearing it had to be added explicitly.
* Sign the 2.7.0 appcast entry
Signed with the Sparkle EdDSA key; enclosure length verified against the
uploaded zip. Release notes match what actually shipped.
* Stop CI reporting success for builds that failed
The Swift job piped xcodebuild into xcpretty with a `|| xcodebuild ... | tail`
fallback, so the step's exit code came from the pipeline tail rather than from
xcodebuild. Every run since the job was written has failed on missing native
XCFrameworks and reported success — the app has never actually been built in CI,
and the unit test step I just added inherited the same flaw.
`make setup` stages whisper and sherpa but there is no target for llama, which is
built out of band and is not mentioned in BUILDING.md, so a clean checkout cannot
compile the app at all. Until that is fixed this job cannot honestly claim to
build anything, so it now asserts the frameworks are present and fails with an
explanatory message instead of passing silently.
set -o pipefail on both steps so a failure can never be masked again.1 parent cd8ceaa commit f501067
127 files changed
Lines changed: 7666 additions & 865 deletions
File tree
- .github/workflows
- Notebook
- Zerm.xcodeproj
- ZermTests
- Zerm
- LocalLLM
- Models
- PowerMode
- Resources
- Services
- AIEnhancement
- TextReplacement
- TextToSpeech
- Transcription/Engine
- Views
- AI Models
- Components
- Dictionary
- Metrics
- Onboarding
- Recorder
- Settings
- docs
- docs
- img
- scripts
- site-content/docs
Some content is hidden
Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
34 | 34 | | |
35 | 35 | | |
36 | 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 | + | |
37 | 63 | | |
38 | 64 | | |
| 65 | + | |
39 | 66 | | |
40 | 67 | | |
41 | 68 | | |
| |||
45 | 72 | | |
46 | 73 | | |
47 | 74 | | |
48 | | - | |
49 | | - | |
50 | | - | |
51 | | - | |
52 | | - | |
53 | | - | |
54 | | - | |
55 | | - | |
56 | | - | |
57 | | - | |
| 75 | + | |
| 76 | + | |
| 77 | + | |
| 78 | + | |
| 79 | + | |
| 80 | + | |
| 81 | + | |
| 82 | + | |
| 83 | + | |
| 84 | + | |
| 85 | + | |
| 86 | + | |
| 87 | + | |
| 88 | + | |
| 89 | + | |
| 90 | + | |
| 91 | + | |
| 92 | + | |
58 | 93 | | |
59 | 94 | | |
60 | 95 | | |
| |||
81 | 116 | | |
82 | 117 | | |
83 | 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 | + | |
84 | 197 | | |
85 | 198 | | |
86 | 199 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
12 | 12 | | |
13 | 13 | | |
14 | 14 | | |
15 | | - | |
| 15 | + | |
16 | 16 | | |
17 | 17 | | |
18 | 18 | | |
| |||
162 | 162 | | |
163 | 163 | | |
164 | 164 | | |
| 165 | + | |
| 166 | + | |
| 167 | + | |
| 168 | + | |
| 169 | + | |
| 170 | + | |
| 171 | + | |
| 172 | + | |
| 173 | + | |
| 174 | + | |
| 175 | + | |
165 | 176 | | |
166 | 177 | | |
167 | 178 | | |
168 | 179 | | |
169 | 180 | | |
170 | 181 | | |
171 | 182 | | |
| 183 | + | |
172 | 184 | | |
173 | 185 | | |
174 | 186 | | |
| |||
178 | 190 | | |
179 | 191 | | |
180 | 192 | | |
181 | | - | |
| 193 | + | |
| 194 | + | |
| 195 | + | |
182 | 196 | | |
183 | 197 | | |
File renamed without changes.
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
| 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 | + | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
| 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 | + | |
0 commit comments