Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,7 @@ jobs:
run: |
BIN=.build/arm64-apple-macosx/release/yap
"$BIN" --help
"$BIN" models list
"$BIN" --version
"$BIN" bench --help

# The embedded __TEXT,__info_plist is what lets TCC attribute the
Expand Down
75 changes: 48 additions & 27 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -106,16 +106,21 @@ jobs:
id: pkg
working-directory: dist
run: |
DMG="yap-${{ steps.v.outputs.version }}.dmg"
shasum -a 256 "$DMG" > "$DMG.sha256"
echo "sha256=$(shasum -a 256 "$DMG" | cut -d' ' -f1)" >> "$GITHUB_OUTPUT"
V="${{ steps.v.outputs.version }}"
shasum -a 256 "yap-$V.dmg" > "yap-$V.dmg.sha256"
# The in-app updater fetches this one and compares it against the zip
# before it unpacks anything.
shasum -a 256 "yap-$V.zip" > "yap-$V.zip.sha256"
echo "sha256=$(shasum -a 256 "yap-$V.dmg" | cut -d' ' -f1)" >> "$GITHUB_OUTPUT"

- name: Publish release
uses: softprops/action-gh-release@3d0d9888cb7fd7b750713d6e236d1fcb99157228 # v3.0.2
with:
files: |
dist/yap-${{ steps.v.outputs.version }}.dmg
dist/yap-${{ steps.v.outputs.version }}.dmg.sha256
dist/yap-${{ steps.v.outputs.version }}.zip
dist/yap-${{ steps.v.outputs.version }}.zip.sha256
generate_release_notes: true
fail_on_unmatched_files: true

Expand Down Expand Up @@ -151,34 +156,52 @@ jobs:
depends_on macos: :sequoia

app "yap.app"
# yap is a CLI first. The bundle exists so the build can be
# notarized and so TCC has something stable to hang grants on; this
# puts the command itself on PATH.
binary "#{appdir}/yap.app/Contents/MacOS/yap"
# No "binary" stanza. yap is a menu-bar app; the only subcommand
# left is "bench", which is a development tool, and putting a
# second path to the same executable on PATH is how the daemon
# ended up with two LaunchServices identities in the first place.

# Homebrew replaces /Applications/yap.app, but a process keeps the
# image it already mapped. Without this you stay on the old version
# until something restarts the daemon, while "yap --version" reads
# the new binary on disk and agrees with the version you just
# installed — the worst shape for a bug, because it looks fixed.
#
# "kickstart -k" rather than an uninstall stanza: it replaces the
# job launchd already owns, so the daemon stays inside launchd and
# the plist — the login item — is untouched.
# The rewrite first, and it is not cosmetic. yap 0.2's plist ran
# "yap run --skip-doctor", an argument 0.3 does not take: launchd
# would start it, ArgumentParser would exit 64, KeepAlive would
# relaunch it, and the login item would spin for ever. Nothing but
# this line and yap itself ever rewrites that file.
#
# bootout + bootstrap rather than "kickstart -k", because launchd
# reads a plist when the job is bootstrapped and not again — a
# kickstart would faithfully relaunch the arguments it already had
# in memory, rewritten file or not.
#
# Measured, because the alternative is losing someone's meeting:
# -k delivers SIGTERM, not SIGKILL, and yap catches SIGTERM and
# routes it through applicationWillTerminate. So a recording in
# bootout delivers SIGTERM, not SIGKILL, and yap catches SIGTERM
# and routes it through applicationWillTerminate. So a recording in
# flight is finalized and transcribes on the next start instead of
# losing its meta.json. launchd brings the new image up about five
# seconds later.
# losing its meta.json.
#
# must_succeed: false because an install with no login item has no
# job to restart, and launchctl exits 113 there. Not having asked
# for launch-at-login is not a reason to fail an upgrade.
# must_succeed: false throughout because an install with no login
# item has no job to restart, and launchctl exits 113 there. Not
# having asked for launch-at-login is not a reason to fail an
# upgrade.
postflight do
agent = File.expand_path("~/Library/LaunchAgents/com.terrifiedbug.yap.plist")
next unless File.exist?(agent)

system_command "/usr/bin/plutil",
args: ["-replace", "ProgramArguments", "-json",
%Q(["#{appdir}/yap.app/Contents/MacOS/yap", "run"]),
agent],
must_succeed: false
system_command "/bin/launchctl",
args: ["bootout", "gui/#{Process.uid}/com.terrifiedbug.yap"],
must_succeed: false
system_command "/bin/launchctl",
args: ["kickstart", "-k", "gui/#{Process.uid}/com.terrifiedbug.yap"],
args: ["bootstrap", "gui/#{Process.uid}", agent],
must_succeed: false
end

Expand All @@ -188,11 +211,11 @@ jobs:
# ~/Library/LaunchAgents/<label>.plist, so every upgrade would
# quietly switch launch-at-login off, and "quit" is recorded and
# then reopened with "open -b" afterwards, which would start the
# daemon outside launchd where neither "yap stop" nor the login
# item can reach it.
# daemon outside launchd where the login item cannot reach it.
#
# Neither is a loss, because the cask never started the daemon.
# "yap install --launch-at-login" did, and it owns stopping it.
# The "Launch at login" toggle in Settings did, and it owns
# stopping it.

# zap only runs on "brew uninstall --zap", which is the one time
# taking the login item away is what was asked for. launchctl
Expand All @@ -208,13 +231,11 @@ jobs:
]

caveats <<~CAVEAT
Finish setting up with:
yap setup
yap install --launch-at-login
Launch yap from Applications. It asks for the permissions it
needs from the menu bar, and downloads the model in the
background on first run.

The login item belongs to yap rather than to this cask, so a
plain uninstall leaves it behind. To remove everything:
yap install --uninstall
To remove everything, including the login item:
brew uninstall --zap --cask yap
CAVEAT
end
Expand Down
106 changes: 57 additions & 49 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,11 +13,16 @@ No account, no upload. The network gets used once, to fetch the model.

```sh
brew install --cask terrifiedbug/tap/yap
yap setup
```

`yap setup` asks for the permissions and pulls the model down. That is 220 MB,
once, and then you are offline forever.
Or download the .dmg from [Releases](https://github.com/TerrifiedBug/yap/releases)
and drag yap to Applications.

Then launch yap from Applications. The menu bar mark appears straight away and
walks you through the rest: it asks for Accessibility, then the microphone, and
pulls the model down in the background. That is 220 MB, once, and then you are
offline: nothing you say ever leaves the machine, and yap makes no network
request of its own again unless you ask it to check for a new version.

Builds are signed with a Developer ID certificate and notarized by Apple, so
there is no Gatekeeper prompt. Apple Silicon only, because the model runs on
Expand All @@ -28,30 +33,32 @@ the Neural Engine.
Hold `fn`, speak, release. A small pill shows up while the mic is live, and the
same mark sits in the menu bar the whole time yap is running.

```sh
yap install --launch-at-login # menu bar, back after every login
yap # foreground instead, dies with the terminal
yap record # record a meeting now, ^C to stop
yap doctor # permissions, key mapping, model
```
Everything else is in that menu: start a recording, copy the last transcript,
open Settings, quit. Dictation and the meeting recorder share one process and
one loaded model. The recorder takes your mic and the system audio as two
separate tracks and gives you one transcript, with timings and speaker labels.

Dictation and the meeting recorder share one process and one loaded model. The
recorder takes your mic and the system audio as two separate tracks and gives
you one transcript, with timings and speaker labels.
"Settings…" opens a window covering every setting below, and every change lands
immediately — the daemon does not need a restart to notice. General has the
login item, updates and a way to the logs; Dictation has the hotkey, which you
set by clicking the field and pressing the key or chord you want. "Open Config
File" at the bottom opens the JSON, for anyone who would rather type.

If `fn` does something else on your Mac, `yap doctor` says how to get it back.
There is also `--hotkey`, and `dictation.hotkey` in the config file.
If `fn` is set to do something else on your Mac, the menu says so and offers to
open Keyboard settings.

"Settings…" in the menu bar opens a window covering every setting below, and
every change lands immediately — the daemon does not need a restart to notice.
"Open Config File" at the bottom of it opens the JSON, for anyone who would
rather type.
"Copy Last Transcript" is there for the press that landed in the wrong window.
yap holds the most recent one in memory and nowhere else.

"Copy Last Transcript" in the menu bar is there for the press that landed in
the wrong window. yap holds the most recent one in memory and nowhere else.
"Quit yap" stops it until you launch it again, or until your next login if
"Launch at login" is on.

"Quit yap" stops the background daemon until your next login. `yap start`
brings it back sooner.
Settings → General has a "Check Now" button. It asks GitHub Releases once,
downloads the new build, verifies its signature against the one yap is running
under, and puts "Update to x.y.z · Restart" in the menu. Nothing is replaced
until you click that, and it is never offered while a recording is in flight.
There is no automatic check and no timer behind it: yap does nothing at all
while it is idle.

One daemon holds the hotkey at a time. Start another — from Applications, from
a terminal, or because an upgrade restarted the login item — and the new one
Expand All @@ -62,26 +69,21 @@ to be seen, look in its hidden section. Managers that file newly-appeared items
there catch yap the first time it shows up. Reveal that section, then hold
Command and drag the mark out of it once; it stays where you put it.

| | |
|---|---|
| `yap run` | The daemon, in the foreground. The default. |
| `yap start` / `yap stop` | Start or stop the background daemon. |
| `yap record` | Record one session now, then transcribe it. |
| `yap models list` | The models, and which ones you have. |
| `yap models download <id>` | Fetch one early. |
| `yap doctor` | Permissions, key mapping, model, login item. |
| `yap setup` | Permissions and the model, in one go. |
| `yap install` | Add or remove the login item. |
| `yap bench --audio FILE` | Time it on your own audio. |
| `yap --version` | Print the version. |
There is one command, and it is not needed for anything you do day to day:

```sh
/Applications/yap.app/Contents/MacOS/yap bench --audio FILE
```

It times transcription on your own audio. `--version` prints the version.

## Configuration

`~/.config/yap/config.json`. Every key is optional and a flag beats the file.
The Settings window is a GUI over this exact file — there is no second store —
and "Open Config File" in it opens the JSON, filled in with the defaults. An
upgrade adds a line for anything new, so the file always lists what this yap
can do. Your own values are never touched.
`~/.config/yap/config.json`. Every key is optional, and this file is the only
store there is. The Settings window is a GUI over it — nothing is kept anywhere
else — and "Open Config File" in it opens the JSON, filled in with the
defaults. An upgrade adds a line for anything new, so the file always lists
what this yap can do. Your own values are never touched.

```json
{
Expand All @@ -105,8 +107,16 @@ can do. Your own values are never touched.

Save it and yap picks it up. The hotkey, `tap_to_toggle`, the overlay,
`mute_output`, `newline_after_release`, `meeting_detection`,
`meeting_auto_record` and `meeting_excluded_apps` all change on the spot. A new
`model` or `recordings_dir` wants a restart, and yap says so when it sees one.
`meeting_auto_record` and `meeting_excluded_apps` all change on the spot. A
new `model` or `recordings_dir` wants a restart, and yap says so when it sees
one.

`hotkey` is a modifier held on its own — `fn`, `rightOption`, `rightCommand`,
`rightControl`, `rightShift`, `leftOption`, `leftControl`, `leftShift` — or a
chord like `cmd+shift+space`, or a lone function key like `f5`. The recorder in
Settings → Dictation writes it for you; typing it by hand is case-insensitive
and ignores `-` and `_`. A chord is swallowed while yap holds it, so the app
underneath never sees it.

`newline_after_release` hits Return once the text is in, which is what you want
for chat boxes.
Expand Down Expand Up @@ -153,8 +163,7 @@ and updates its metadata and heading. Turn it off to use yap as a plain
recorder: `on_stop` then fires when the recording stops rather than after the
transcript. Nothing is lost either way. Turn it back on, restart, and yap works
through every session under `recordings_dir` that has no transcript yet, firing
`on_stop` again for each. Anything you put somewhere else with
`yap record --out` is left alone.
`on_stop` again for each.

## Models

Expand Down Expand Up @@ -185,15 +194,14 @@ own, and that is a single device read a second.
## Uninstall

```sh
yap install --uninstall
brew uninstall --zap --cask yap
```

The login item is yap's rather than the cask's, so the first line is what
takes it away. Skip it and `brew uninstall` on its own leaves launchd trying
to start a binary that is no longer there. `--zap` also clears the config file
and the logs. Your recordings are never touched, and neither are the models,
which are shared with anything else built on FluidAudio.
`--zap` takes the login item away with it, along with the config file and the
logs. Installed from the .dmg instead, drag yap out of Applications and delete
`~/Library/LaunchAgents/com.terrifiedbug.yap.plist`. Your recordings are never
touched, and neither are the models, which are shared with anything else built
on FluidAudio.

## Requirements

Expand Down
Loading
Loading