Skip to content

Commit e6f7cfb

Browse files
andyhtranclaude
andcommitted
Initial release
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
0 parents  commit e6f7cfb

45 files changed

Lines changed: 4067 additions & 0 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

.envrc.example

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
# Copy to .envrc and fill in your values
2+
# If using direnv, run: direnv allow
3+
4+
# Code signing (for local dev builds via `just run`)
5+
export CODESIGN_IDENTITY="-" # ad-hoc signing, or "Developer ID Application: Your Name (TEAMID)"
6+
export CODESIGN_TEAM_ID="" # your Apple team ID, leave empty for ad-hoc
7+
8+
# Release only (for `just publish`)
9+
export TAP_DIR="" # path to your local homebrew-tap checkout

.github/workflows/ci.yml

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
name: CI
2+
3+
on:
4+
push:
5+
branches: [main]
6+
pull_request:
7+
branches: [main]
8+
9+
jobs:
10+
build:
11+
runs-on: macos-15
12+
steps:
13+
- uses: actions/checkout@v4
14+
- run: swift build
15+
- run: swift test

.gitignore

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
.build/
2+
build/
3+
Package.resolved
4+
.swiftpm/
5+
*.xcodeproj
6+
*.xcworkspace
7+
xcuserdata/
8+
DerivedData/
9+
.DS_Store
10+
.envrc

CLAUDE.md

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
# MiniWhisper
2+
3+
**CLI-only** — no Xcode. All builds use `swift build` via SPM. Use `just` as the task runner.
4+
5+
- macOS 14.0+, Swift 6.0+
6+
7+
```bash
8+
just dev # Kill existing, build, package, launch
9+
just build # Debug build
10+
just package # Release build + create .app bundle
11+
just clean # Remove build artifacts
12+
```

LICENSE

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
MIT License
2+
3+
Copyright (c) 2026 Andy Tran
4+
5+
Permission is hereby granted, free of charge, to any person obtaining a copy
6+
of this software and associated documentation files (the "Software"), to deal
7+
in the Software without restriction, including without limitation the rights
8+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9+
copies of the Software, and to permit persons to whom the Software is
10+
furnished to do so, subject to the following conditions:
11+
12+
The above copyright notice and this permission notice shall be included in all
13+
copies or substantial portions of the Software.
14+
15+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21+
SOFTWARE.

Package.swift

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
// swift-tools-version: 6.0
2+
3+
import PackageDescription
4+
5+
let package = Package(
6+
name: "MiniWhisper",
7+
platforms: [
8+
.macOS(.v14)
9+
],
10+
products: [
11+
.executable(name: "MiniWhisper", targets: ["MiniWhisper"])
12+
],
13+
dependencies: [
14+
.package(url: "https://github.com/FluidInference/FluidAudio.git", from: "0.9.1")
15+
],
16+
targets: [
17+
.executableTarget(
18+
name: "MiniWhisper",
19+
dependencies: [
20+
"FluidAudio"
21+
],
22+
path: "Sources/MiniWhisper",
23+
exclude: ["Resources"],
24+
swiftSettings: [
25+
.enableExperimentalFeature("StrictConcurrency")
26+
]
27+
),
28+
.testTarget(
29+
name: "MiniWhisperTests",
30+
dependencies: ["MiniWhisper"],
31+
path: "Tests/MiniWhisperTests",
32+
swiftSettings: [
33+
.enableExperimentalFeature("StrictConcurrency")
34+
]
35+
)
36+
]
37+
)

README.md

Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,70 @@
1+
# MiniWhisper
2+
3+
A minimal macOS menu bar app for voice-to-text using the [Parakeet](https://github.com/FluidInference/FluidAudio) model. Press a hotkey, speak, and the transcription is automatically pasted into the active app.
4+
5+
## Install
6+
7+
### Homebrew
8+
9+
```bash
10+
brew tap andyhtran/tap
11+
brew install --cask miniwhisper
12+
```
13+
14+
### Build from source
15+
16+
Requires macOS 14+ (Sonoma) and Swift 6+.
17+
18+
```bash
19+
git clone https://github.com/andyhtran/MiniWhisper.git
20+
cd MiniWhisper
21+
just dev
22+
```
23+
24+
## Usage
25+
26+
1. Launch MiniWhisper — it lives in your menu bar
27+
2. Grant microphone and accessibility permissions when prompted
28+
3. Press **Option + `** to start recording (customizable in the menu bar panel)
29+
4. Press the hotkey again to stop — the transcription is pasted into the frontmost app
30+
5. Press **Escape** to cancel a recording
31+
32+
Your clipboard is preserved: MiniWhisper temporarily uses it to paste, then restores the previous contents.
33+
34+
## Features
35+
36+
- **Auto-paste** — transcriptions go straight into whatever app you're using
37+
- **Customizable hotkey** — change the toggle shortcut from the menu bar panel
38+
- **Text replacements** — auto-correct words or phrases after transcription
39+
- **Recording history** — browse and copy recent transcriptions
40+
- **Usage stats** — track recordings, speaking time, word count, and average WPM
41+
- **On-device** — all processing happens locally via the Parakeet model, nothing leaves your Mac
42+
43+
## Build commands
44+
45+
```bash
46+
just dev # Build, package, and launch
47+
just build # Debug build only
48+
just release # Release build + .app bundle
49+
just clean # Remove build artifacts
50+
```
51+
52+
## Release
53+
54+
Signing, notarization, and publishing require [`asc`](https://github.com/rudrankriyam/App-Store-Connect-CLI):
55+
56+
```bash
57+
brew install asc
58+
```
59+
60+
See `just --list` for the full set of release recipes (`sign-and-notarize`, `github-release`, `publish`, etc.).
61+
62+
## Configuration
63+
64+
Copy `.envrc.example` to `.envrc` and fill in your values. If using [direnv](https://direnv.net/), run `direnv allow`.
65+
66+
For local development, ad-hoc signing works out of the box — no Apple Developer account needed.
67+
68+
## License
69+
70+
[MIT](LICENSE)

Scripts/build-app.sh

Lines changed: 85 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,85 @@
1+
#!/bin/bash
2+
set -euo pipefail
3+
4+
ROOT=$(cd "$(dirname "$0")/.." && pwd)
5+
source "$ROOT/version.env"
6+
7+
APP_NAME="MiniWhisper"
8+
BUILD_CONFIG="${1:-release}"
9+
BUILD_DIR=".build/${BUILD_CONFIG}"
10+
APP_BUNDLE="build/${APP_NAME}.app"
11+
BUNDLE_ID_DEBUG="com.miniwhisper.dev"
12+
BUNDLE_ID_RELEASE="com.miniwhisper.app"
13+
14+
if [[ "$BUILD_CONFIG" == "debug" ]]; then
15+
BUNDLE_ID="$BUNDLE_ID_DEBUG"
16+
DISPLAY_NAME="MiniWhisper Dev"
17+
else
18+
BUNDLE_ID="$BUNDLE_ID_RELEASE"
19+
DISPLAY_NAME="MiniWhisper"
20+
fi
21+
22+
echo "Building $APP_NAME ($BUILD_CONFIG)..."
23+
24+
swift build -c "$BUILD_CONFIG"
25+
26+
echo "Creating app bundle..."
27+
rm -rf "$APP_BUNDLE"
28+
mkdir -p "$APP_BUNDLE/Contents/MacOS"
29+
mkdir -p "$APP_BUNDLE/Contents/Resources"
30+
31+
cp "$BUILD_DIR/$APP_NAME" "$APP_BUNDLE/Contents/MacOS/"
32+
33+
cat > "$APP_BUNDLE/Contents/Info.plist" << PLIST
34+
<?xml version="1.0" encoding="UTF-8"?>
35+
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
36+
<plist version="1.0">
37+
<dict>
38+
<key>CFBundleDevelopmentRegion</key>
39+
<string>en</string>
40+
<key>CFBundleDisplayName</key>
41+
<string>${DISPLAY_NAME}</string>
42+
<key>CFBundleExecutable</key>
43+
<string>${APP_NAME}</string>
44+
<key>CFBundleIdentifier</key>
45+
<string>${BUNDLE_ID}</string>
46+
<key>CFBundleInfoDictionaryVersion</key>
47+
<string>6.0</string>
48+
<key>CFBundleName</key>
49+
<string>${APP_NAME}</string>
50+
<key>CFBundlePackageType</key>
51+
<string>APPL</string>
52+
<key>CFBundleShortVersionString</key>
53+
<string>${MARKETING_VERSION}</string>
54+
<key>CFBundleVersion</key>
55+
<string>${BUILD_NUMBER}</string>
56+
<key>LSApplicationCategoryType</key>
57+
<string>public.app-category.utilities</string>
58+
<key>LSMinimumSystemVersion</key>
59+
<string>14.0</string>
60+
<key>LSUIElement</key>
61+
<true/>
62+
<key>NSHighResolutionCapable</key>
63+
<true/>
64+
<key>NSPrincipalClass</key>
65+
<string>NSApplication</string>
66+
<key>CFBundleIconFile</key>
67+
<string>AppIcon</string>
68+
<key>NSHumanReadableCopyright</key>
69+
<string>Copyright © 2026 Andy Tran. All rights reserved.</string>
70+
<key>NSMicrophoneUsageDescription</key>
71+
<string>MiniWhisper needs microphone access to record and transcribe speech.</string>
72+
</dict>
73+
</plist>
74+
PLIST
75+
76+
cp "Sources/MiniWhisper/Resources/AppIcon.icns" "$APP_BUNDLE/Contents/Resources/"
77+
78+
echo -n "APPL????" > "$APP_BUNDLE/Contents/PkgInfo"
79+
80+
chmod +x "$APP_BUNDLE/Contents/MacOS/$APP_NAME"
81+
82+
# Copy entitlements (used by signing in justfile, not embedded in bundle)
83+
cp "Sources/MiniWhisper/Resources/MiniWhisper.entitlements" "build/"
84+
85+
echo "Bundle created: $APP_BUNDLE"

Scripts/sign-and-notarize.sh

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
#!/usr/bin/env bash
2+
set -euo pipefail
3+
4+
ROOT=$(cd "$(dirname "$0")/.." && pwd)
5+
source "$ROOT/version.env"
6+
7+
APP_NAME="MiniWhisper"
8+
APP_BUNDLE="build/${APP_NAME}.app"
9+
ZIP_NAME="${APP_NAME}-${MARKETING_VERSION}.zip"
10+
SIGNING_ID="${CODESIGN_IDENTITY:?Set CODESIGN_IDENTITY to your Developer ID Application identity}"
11+
12+
echo "==> Building release..."
13+
bash "$ROOT/Scripts/build-app.sh" release
14+
15+
echo "==> Signing with: $SIGNING_ID"
16+
codesign --force --timestamp --options runtime \
17+
--sign "$SIGNING_ID" \
18+
--entitlements "build/MiniWhisper.entitlements" \
19+
"$APP_BUNDLE"
20+
21+
echo "==> Verifying signature..."
22+
codesign --verify --deep --strict "$APP_BUNDLE"
23+
24+
echo "==> Creating zip for notarization..."
25+
/usr/bin/ditto -c -k --keepParent "$APP_BUNDLE" "/tmp/${APP_NAME}Notarize.zip"
26+
27+
echo "==> Submitting for notarization..."
28+
asc notarization submit --file "/tmp/${APP_NAME}Notarize.zip" --wait
29+
30+
echo "==> Stapling notarization ticket..."
31+
xcrun stapler staple "$APP_BUNDLE"
32+
33+
echo "==> Verifying notarization..."
34+
spctl -a -t exec -vv "$APP_BUNDLE"
35+
36+
echo "==> Creating release zip..."
37+
rm -f "$ZIP_NAME" "/tmp/${APP_NAME}Notarize.zip"
38+
xattr -cr "$APP_BUNDLE"
39+
/usr/bin/ditto -c -k --keepParent "$APP_BUNDLE" "$ZIP_NAME"
40+
41+
echo "Done: $ZIP_NAME ($(shasum -a 256 "$ZIP_NAME" | cut -d' ' -f1))"

Scripts/update-tap.sh

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
#!/usr/bin/env bash
2+
set -euo pipefail
3+
4+
ROOT=$(cd "$(dirname "$0")/.." && pwd)
5+
source "$ROOT/version.env"
6+
7+
APP_NAME="MiniWhisper"
8+
ZIP_NAME="${APP_NAME}-${MARKETING_VERSION}.zip"
9+
TAP_DIR="${TAP_DIR:?Set TAP_DIR to your local homebrew-tap checkout}"
10+
CASK_FILE="$TAP_DIR/Casks/miniwhisper.rb"
11+
12+
if [[ ! -f "$ZIP_NAME" ]]; then
13+
echo "ERROR: $ZIP_NAME not found. Run 'just sign-and-notarize' first." >&2
14+
exit 1
15+
fi
16+
17+
SHA=$(shasum -a 256 "$ZIP_NAME" | cut -d' ' -f1)
18+
19+
echo "==> Updating cask: version=$MARKETING_VERSION sha256=$SHA"
20+
21+
mkdir -p "$TAP_DIR/Casks"
22+
cat > "$CASK_FILE" << RUBY
23+
cask "miniwhisper" do
24+
version "${MARKETING_VERSION}"
25+
sha256 "${SHA}"
26+
27+
url "https://github.com/andyhtran/MiniWhisper/releases/download/v#{version}/MiniWhisper-#{version}.zip",
28+
verified: "github.com/andyhtran/MiniWhisper/"
29+
name "MiniWhisper"
30+
desc "Voice-to-text transcription from the menu bar"
31+
homepage "https://github.com/andyhtran/MiniWhisper"
32+
33+
depends_on macos: ">= :sonoma"
34+
35+
app "MiniWhisper.app"
36+
37+
zap trash: [
38+
"~/Library/Preferences/com.miniwhisper.app.plist",
39+
"~/Library/Application Support/MiniWhisper",
40+
]
41+
end
42+
RUBY
43+
44+
echo "==> Committing to homebrew-tap..."
45+
cd "$TAP_DIR"
46+
git add "Casks/miniwhisper.rb"
47+
git commit -m "miniwhisper ${MARKETING_VERSION}"
48+
git push
49+
50+
echo "Done: cask updated to ${MARKETING_VERSION}"

0 commit comments

Comments
 (0)