Skip to content

Add fast local English dictation and configurable triggers#3

Open
0xdef1cafe wants to merge 1 commit into
jpthor:mainfrom
0xdef1cafe:feature/local-dictation-upgrade
Open

Add fast local English dictation and configurable triggers#3
0xdef1cafe wants to merge 1 commit into
jpthor:mainfrom
0xdef1cafe:feature/local-dictation-upgrade

Conversation

@0xdef1cafe

@0xdef1cafe 0xdef1cafe commented Jul 12, 2026

Copy link
Copy Markdown

Summary

  • add Parakeet Unified English as the recommended fast local Apple Silicon engine, while retaining Whisper models as fallbacks
  • add the 626 MB Apple Silicon-optimized Whisper Large v3 conversion
  • optimize English-only Whisper decoding by skipping language detection and unused timestamps
  • support independent, learnable keyboard and auxiliary mouse bindings
  • add hold/toggle interaction modes and Escape-to-cancel
  • make text insertion clipboard-safe and remove toggle-mode activation delay
  • improve model download validation, selection rollback, settings layout, and local app packaging
  • add Australian-English benchmark fixtures plus Parakeet and Whisper benchmark tools

Performance

Measured locally on an M4 Max using the included 19-second Australian-English fixture:

Configuration Transcription time
Original Whisper Large v3 4.27 s
Large v3 Optimized (626 MB) 1.21 s

The optimized model was approximately 3.5x faster and produced the same transcript for this fixture. No model was fine-tuned locally; this uses Argmax existing Core ML conversion from whisperkit-coreml.

Verification

  • swift test: 85 tests passed
  • release build and ad-hoc app bundle verified
  • exercised keyboard and mouse toggle recording, Escape cancellation, model switching, transcription, and clipboard-safe insertion locally

Compatibility

This raises the minimum deployment target from macOS 13 to macOS 14 because the selected FluidAudio release requires Sonoma.

Summary by CodeRabbit

  • New Features

    • Added configurable keyboard and mouse dictation triggers with hold or toggle modes.
    • Added Escape-to-cancel support and safer clipboard restoration after transcription.
    • Added Parakeet Unified English as the recommended default speech model, with additional model options.
    • Added local app packaging and speech recognition benchmarking tools.
  • Documentation

    • Updated onboarding, usage guidance, requirements, model information, and benchmark instructions.
    • Added an Australian English 30-phrase benchmark and scoring utility.
  • Compatibility

    • The app now requires macOS 14 or later.

@coderabbitai

coderabbitai Bot commented Jul 12, 2026

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Walkthrough

The PR adds Parakeet Unified support as the default Apple Silicon transcription engine, configurable keyboard and mouse triggers with hold/toggle modes, updated onboarding and settings UI, benchmark tooling, dependency pinning, and a local app build script.

Changes

VoiceScribe transcription and trigger flow

Layer / File(s) Summary
Trigger and model contracts
Sources/VoiceScribeCore/*, Tests/VoiceScribeTests/*
Adds persisted keyboard bindings, trigger modes and interaction rules, and expands model metadata for Parakeet and an optimized Whisper variant with corresponding tests.
Parakeet and Whisper model lifecycle
Sources/VoiceScribe/Core/TranscriptionEngine.swift, Sources/VoiceScribe/Core/AudioRecorder.swift, Package.swift
Integrates UnifiedAsrManager, adds platform-specific model loading and transcription, preserves Whisper fallback behavior, adds rollback handling, and prepares audio recording.
Keyboard and mouse trigger monitoring
Sources/VoiceScribe/App/AppState.swift, Sources/VoiceScribe/Core/*Monitor.swift
Adds event-tap monitors and capture flows for keyboard and mouse bindings, Escape cancellation, and hold/toggle recording actions.
Onboarding, settings, and transcription output
Sources/VoiceScribe/Views/*, Sources/VoiceScribe/Core/TextInserter.swift
Updates model onboarding, settings controls, trigger instructions, model switching, clipboard restoration, and paste event handling.
Benchmark tools and local packaging
Benchmarks/*, Sources/ParakeetSmoke/*, Sources/WhisperBenchmark/*, build-local-app.sh, Package.swift, README.md
Adds Australian English benchmark assets and scoring, Parakeet and Whisper benchmark executables, dependency lock data, macOS 14 metadata, documentation, and signed local app packaging.

Estimated code review effort: 5 (Critical) | ~90 minutes

Sequence Diagram(s)

sequenceDiagram
  participant User
  participant KeyMouseMonitor
  participant AppState
  participant AudioRecorder
  participant TranscriptionEngine
  participant TextInserter
  User->>KeyMouseMonitor: Press configured trigger
  KeyMouseMonitor->>AppState: Report trigger state
  AppState->>AudioRecorder: Start or stop recording
  AppState->>TranscriptionEngine: Transcribe recorded audio
  TranscriptionEngine-->>AppState: Return transcript
  AppState->>TextInserter: Insert transcript
Loading
🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly summarizes the main change: fast local English dictation with configurable recording triggers.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 5

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
Benchmarks/australian-english-30/hypotheses.txt (1)

1-31: 📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

Populate or remove this benchmark fixture.

hypotheses.txt contains no hypotheses—only blank lines—so it provides no testable benchmark assumptions. Add the intended entries, ideally one per phrase, or omit the file until it has meaningful content.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@Benchmarks/australian-english-30/hypotheses.txt` around lines 1 - 31,
Populate hypotheses.txt with meaningful benchmark assumptions, preferably one
hypothesis per phrase, or remove the empty fixture until those entries are
available.
🧹 Nitpick comments (1)
Benchmarks/australian-english-30/score.py (1)

57-57: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Make the validated zip contract explicit.

The line-count check immediately above guarantees equal lengths, so use strict=True to satisfy B905 and protect this invariant if the loop is later refactored. Confirm the benchmark tooling targets Python 3.10 or newer before applying this change.

Proposed fix
-    for index, (reference, hypothesis) in enumerate(zip(references, hypotheses), start=1):
+    for index, (reference, hypothesis) in enumerate(
+        zip(references, hypotheses, strict=True), start=1
+    ):
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@Benchmarks/australian-english-30/score.py` at line 57, Update the
references/hypotheses zip call in the benchmark loop to use strict=True,
preserving the existing start=1 enumeration. First confirm the benchmark tooling
targets Python 3.10 or newer; apply the change only if that compatibility
requirement is satisfied.

Source: Linters/SAST tools

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@Sources/ParakeetSmoke/main.swift`:
- Around line 18-26: Move the “Loading model: complete” stderr write in the
startup flow to immediately after manager.loadModels() returns and before
starting transcription. Remove it from after manager.transcribe(samples) so the
loading message reflects only model loading completion.

In `@Sources/VoiceScribe/Core/KeyMouseMonitor.swift`:
- Around line 78-85: Update the Fn handling branch in KeyMouseMonitor to require
the incoming event’s keyCode to equal kVK_Function in addition to the existing
binding and flagsChanged checks. Preserve the current keyboard state update and
return behavior only for actual Fn events, allowing other modifier changes to
continue through normal processing.

In `@Sources/VoiceScribe/Core/TextInserter.swift`:
- Around line 16-32: Update the pasteboard setup in the text insertion method so
the transcription string is successfully set before clearing or replacing the
original clipboard. Preserve the existing backup and restoration flow, ensuring
a failed setString operation returns without wiping the prior clipboard.

In `@Sources/VoiceScribe/Views/OnboardingView.swift`:
- Around line 79-88: Update startDefaultModelDownloadIfNeeded to skip the
default-model prefetch on x86_64, gating the existing download flow to arm64
before accessing or downloading ModelMetadata.defaultModel; preserve the current
guards and Task behavior for supported arm64 systems.

In `@Sources/WhisperBenchmark/main.swift`:
- Around line 14-60: Remove the obsolete download: argument from the WhisperKit
initializer call in the benchmark setup, keeping the existing modelFolder,
computeOptions, verbose, logLevel, prewarm, and load arguments unchanged so it
matches the 0.18.0 public initializer.

---

Outside diff comments:
In `@Benchmarks/australian-english-30/hypotheses.txt`:
- Around line 1-31: Populate hypotheses.txt with meaningful benchmark
assumptions, preferably one hypothesis per phrase, or remove the empty fixture
until those entries are available.

---

Nitpick comments:
In `@Benchmarks/australian-english-30/score.py`:
- Line 57: Update the references/hypotheses zip call in the benchmark loop to
use strict=True, preserving the existing start=1 enumeration. First confirm the
benchmark tooling targets Python 3.10 or newer; apply the change only if that
compatibility requirement is satisfied.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 73980a2f-c9d2-4f13-b439-4fd2c19dc661

📥 Commits

Reviewing files that changed from the base of the PR and between 501679c and a6e6312.

📒 Files selected for processing (33)
  • .gitignore
  • Benchmarks/australian-english-30/README.md
  • Benchmarks/australian-english-30/hypotheses.txt
  • Benchmarks/australian-english-30/phrases.txt
  • Benchmarks/australian-english-30/score.py
  • Benchmarks/performance/australian-english.aiff
  • Package.resolved
  • Package.swift
  • README.md
  • Resources/Info.plist
  • Sources/ParakeetSmoke/main.swift
  • Sources/VoiceScribe/App/AppState.swift
  • Sources/VoiceScribe/App/VoiceScribeApp.swift
  • Sources/VoiceScribe/Core/AudioRecorder.swift
  • Sources/VoiceScribe/Core/EscapeKeyMonitor.swift
  • Sources/VoiceScribe/Core/FnKeyMonitor.swift
  • Sources/VoiceScribe/Core/KeyMouseMonitor.swift
  • Sources/VoiceScribe/Core/KeyboardBindingCaptureMonitor.swift
  • Sources/VoiceScribe/Core/MouseButtonCaptureMonitor.swift
  • Sources/VoiceScribe/Core/SpacebarMonitor.swift
  • Sources/VoiceScribe/Core/TextInserter.swift
  • Sources/VoiceScribe/Core/TranscriptionEngine.swift
  • Sources/VoiceScribe/Views/MenuBarView.swift
  • Sources/VoiceScribe/Views/OnboardingView.swift
  • Sources/VoiceScribe/Views/SettingsView.swift
  • Sources/VoiceScribeCore/KeyboardBinding.swift
  • Sources/VoiceScribeCore/ModelMetadata.swift
  • Sources/VoiceScribeCore/TriggerInteraction.swift
  • Sources/VoiceScribeCore/TriggerKey.swift
  • Sources/WhisperBenchmark/main.swift
  • Tests/VoiceScribeTests/ModelMetadataTests.swift
  • Tests/VoiceScribeTests/TriggerKeyTests.swift
  • build-local-app.sh

Comment on lines +18 to +26
FileHandle.standardError.write(Data("Loading model...\n".utf8))
try await manager.loadModels()

let started = ContinuousClock.now
let transcript = try await manager.transcribe(samples)
let elapsed = started.duration(to: .now)
FileHandle.standardError.write(Data("Loading model: complete\n".utf8))
print(transcript)
FileHandle.standardError.write(Data("Transcription time: \(elapsed)\n".utf8))

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

"Loading model: complete" is printed after transcription, not after model loading.

Line 24 writes the completion message to stderr after transcribe(samples) at line 22, making it appear that model loading includes transcription time. Move it to right after loadModels() returns.

🔧 Proposed fix: move the completion log to after loadModels()
         FileHandle.standardError.write(Data("Loading model...\n".utf8))
         try await manager.loadModels()
+        FileHandle.standardError.write(Data("Loading model: complete\n".utf8))
 
         let started = ContinuousClock.now
         let transcript = try await manager.transcribe(samples)
         let elapsed = started.duration(to: .now)
-        FileHandle.standardError.write(Data("Loading model: complete\n".utf8))
         print(transcript)
         FileHandle.standardError.write(Data("Transcription time: \(elapsed)\n".utf8))
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
FileHandle.standardError.write(Data("Loading model...\n".utf8))
try await manager.loadModels()
let started = ContinuousClock.now
let transcript = try await manager.transcribe(samples)
let elapsed = started.duration(to: .now)
FileHandle.standardError.write(Data("Loading model: complete\n".utf8))
print(transcript)
FileHandle.standardError.write(Data("Transcription time: \(elapsed)\n".utf8))
FileHandle.standardError.write(Data("Loading model...\n".utf8))
try await manager.loadModels()
FileHandle.standardError.write(Data("Loading model: complete\n".utf8))
let started = ContinuousClock.now
let transcript = try await manager.transcribe(samples)
let elapsed = started.duration(to: .now)
print(transcript)
FileHandle.standardError.write(Data("Transcription time: \(elapsed)\n".utf8))
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@Sources/ParakeetSmoke/main.swift` around lines 18 - 26, Move the “Loading
model: complete” stderr write in the startup flow to immediately after
manager.loadModels() returns and before starting transcription. Remove it from
after manager.transcribe(samples) so the loading message reflects only model
loading completion.

Comment on lines +78 to +85
if keyboardBinding.keyCode == Int64(kVK_Function), type == .flagsChanged {
let pressed = event.flags.contains(.maskSecondaryFn)
if pressed != keyboardIsDown {
keyboardIsDown = pressed
onStateChanged(.keyboard, pressed)
}
return nil
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🌐 Web query:

CGEvent flagsChanged keyboardEventKeycode Fn key kVK_Function value

💡 Result:

The virtual key code constant for the Function (Fn) key on macOS is kVK_Function, which has a hexadecimal value of 0x3F [1][2][3]. When monitoring keyboard events using the Core Graphics framework, changes to the state of the Fn key (as well as other modifier keys like Shift, Control, Option, and Command) are captured via events of type CGEventType.flagsChanged [4][5]. Key implementation details include: 1. Identification: The constant kVK_Function is defined as 0x3F in the macOS SDK and is widely used across various programming environments (e.g., Swift, Rust, C++) to represent the physical Fn key [1][6][3][7][8]. 2. Detection and Monitoring: To intercept changes in modifier key states globally, you typically use an event tap created with CGEventTapCreate, specifying the event mask CGEventMaskBit(kCGEventFlagsChanged) [4][9]. 3. Modifier Flags: While kVK_Function represents the physical key code, the status of the Fn key as a modifier is often checked or set using the flag mask kCGEventFlagMaskSecondaryFn [10][11]. Note that simply setting flags on a CGEvent may not always trigger the desired behavior in target applications; if you are attempting to simulate key presses involving the Fn key, you may need to explicitly post the key-down and key-up events for the Fn key itself in sequence with the target key, rather than relying solely on setting event flags [10][11]. Ensure your application has the necessary Accessibility permissions in System Settings when attempting to monitor or inject global events [11].

Citations:


🏁 Script executed:

#!/bin/bash
set -euo pipefail

file="Sources/VoiceScribe/Core/KeyMouseMonitor.swift"

echo "== outline =="
ast-grep outline "$file" --view expanded || true

echo
echo "== relevant lines =="
sed -n '60,95p' "$file" | cat -n

echo
echo "== search for keyCode usage in file =="
rg -n "keyCode|flagsChanged|maskSecondaryFn|kVK_Function" "$file"

Repository: jpthor/voicescribe

Length of output: 2587


🌐 Web query:

macOS CGEvent flagsChanged Fn keyboardEventKeycode kVK_Function keyboardEventKeycode

💡 Result:

In macOS development, the Fn (Function) key is treated as a modifier key, and its state changes are captured using the kCGEventFlagsChanged event type [1][2][3]. The virtual key code associated with the Fn key is kVK_Function, which has the hexadecimal value 0x3F (decimal 63) [4][5][6][7]. When using Core Graphics Event Taps to monitor or intercept keyboard events, you should use the kCGEventFlagsChanged event type to detect when modifier keys, including Fn, are pressed or released [2][3]. To specifically identify the Fn key in these events, you can check both the event's key code (kVK_Function) and its modifier flags [3]. Key technical details: - Virtual Key Code: kVK_Function = 0x3F [4][5][7]. - Modifier Flag Mask: In many macOS APIs, the Fn key state is represented by kCGEventFlagMaskSecondaryFn (or simply the.function modifier flag in Swift/AppKit) [8][4][9][3]. - Event Type: Monitor kCGEventFlagsChanged to handle changes in the Fn key state [1][3]. If you are using Swift, you can detect the Fn key state changes by monitoring.flagsChanged events and checking for the combination of kVK_Function (63) and the.function modifier flag [3]. Note that simulating the Fn key press sometimes requires specific permissions in System Settings (Accessibility) and, depending on the macOS version, may involve complex handling of modifier flags within the event object [8][10][9].

Citations:


🏁 Script executed:

#!/bin/bash
set -euo pipefail

file="Sources/VoiceScribe/Core/KeyMouseMonitor.swift"

echo "== file map =="
ast-grep outline "$file" --view expanded || true

echo
echo "== relevant sections =="
sed -n '1,140p' "$file" | cat -n

echo
echo "== event/state callbacks in repo =="
rg -n "onStateChanged\(|keyboardIsDown|mouseIsDown|keyboardBinding|flagsChanged|kVK_Function|maskSecondaryFn" Sources/VoiceScribe -g '!**/*.xcassets'

Repository: jpthor/voicescribe

Length of output: 10104


Fn binding should only handle Fn flagsChanged events.

flagsChanged fires for every modifier, so this branch currently swallows Shift/Control/Option/Command/CapsLock changes whenever the bound key is Fn. That can also emit a false .keyboard release when another modifier changes while Fn is held. Gate this on keyCode == kVK_Function here; the event key code is already available.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@Sources/VoiceScribe/Core/KeyMouseMonitor.swift` around lines 78 - 85, Update
the Fn handling branch in KeyMouseMonitor to require the incoming event’s
keyCode to equal kVK_Function in addition to the existing binding and
flagsChanged checks. Preserve the current keyboard state update and return
behavior only for actual Fn events, allowing other modifier changes to continue
through normal processing.

Comment on lines 16 to 32
let pasteboard = NSPasteboard.general

let backup = backupClipboard(pasteboard)

pasteboard.clearContents()
pasteboard.setString(text, forType: .string)
guard pasteboard.setString(text, forType: .string) else { return }
let transcriptionChangeCount = pasteboard.changeCount

simulatePaste()

DispatchQueue.main.asyncAfter(deadline: .now() + 0.15) {
DispatchQueue.main.asyncAfter(deadline: .now() + restoreDelay) { [weak self] in
guard let self else { return }
// If the user or another app copied something after transcription,
// preserve that newer clipboard rather than restoring stale data.
guard pasteboard.changeCount == transcriptionChangeCount else { return }
self.restoreClipboard(pasteboard, from: backup)
}
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Clipboard is wiped if setString fails.

clearContents() (Line 19) runs before the setString guard (Line 20). If setString returns false, the method returns early and the async restore never runs, so the previously backed-up clipboard is lost. Reorder so the original clipboard is only cleared once the transcription string is guaranteed to be set.

🛡️ Proposed fix
-        pasteboard.clearContents()
-        guard pasteboard.setString(text, forType: .string) else { return }
-        let transcriptionChangeCount = pasteboard.changeCount
+        pasteboard.clearContents()
+        guard pasteboard.setString(text, forType: .string) else {
+            // Restore what we just cleared so the user's clipboard isn't lost.
+            restoreClipboard(pasteboard, from: backup)
+            return
+        }
+        let transcriptionChangeCount = pasteboard.changeCount
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
let pasteboard = NSPasteboard.general
let backup = backupClipboard(pasteboard)
pasteboard.clearContents()
pasteboard.setString(text, forType: .string)
guard pasteboard.setString(text, forType: .string) else { return }
let transcriptionChangeCount = pasteboard.changeCount
simulatePaste()
DispatchQueue.main.asyncAfter(deadline: .now() + 0.15) {
DispatchQueue.main.asyncAfter(deadline: .now() + restoreDelay) { [weak self] in
guard let self else { return }
// If the user or another app copied something after transcription,
// preserve that newer clipboard rather than restoring stale data.
guard pasteboard.changeCount == transcriptionChangeCount else { return }
self.restoreClipboard(pasteboard, from: backup)
}
}
let pasteboard = NSPasteboard.general
let backup = backupClipboard(pasteboard)
pasteboard.clearContents()
guard pasteboard.setString(text, forType: .string) else {
// Restore what we just cleared so the user's clipboard isn't lost.
restoreClipboard(pasteboard, from: backup)
return
}
let transcriptionChangeCount = pasteboard.changeCount
simulatePaste()
DispatchQueue.main.asyncAfter(deadline: .now() + restoreDelay) { [weak self] in
guard let self else { return }
// If the user or another app copied something after transcription,
// preserve that newer clipboard rather than restoring stale data.
guard pasteboard.changeCount == transcriptionChangeCount else { return }
self.restoreClipboard(pasteboard, from: backup)
}
}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@Sources/VoiceScribe/Core/TextInserter.swift` around lines 16 - 32, Update the
pasteboard setup in the text insertion method so the transcription string is
successfully set before clearing or replacing the original clipboard. Preserve
the existing backup and restoration flow, ensuring a failed setString operation
returns without wiping the prior clipboard.

Comment on lines +79 to 88
private func startDefaultModelDownloadIfNeeded() {
let model = ModelMetadata.defaultModel
guard !hasStartedDefaultModelDownload else { return }
guard !appState.transcriptionEngine.isModelDownloaded(model) else { return }

hasStartedTinyDownload = true
hasStartedDefaultModelDownload = true
Task {
try? await appState.transcriptionEngine.downloadModel("tiny")
try? await appState.transcriptionEngine.downloadModel(model)
}
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Inspect defaultModel definition for arch conditionals.
rg -nP --type=swift -C4 'static\s+(let|var)\s+defaultModel'

Repository: jpthor/voicescribe

Length of output: 156


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Locate the relevant files and inspect the default model definition and onboarding logic.
git ls-files | rg 'Sources/.+VoiceScribe|ModelMetadata|TranscriptionEngine|OnboardingView\.swift$'
printf '\n--- defaultModel matches ---\n'
rg -n --type=swift 'defaultModel|ModelMetadata\.defaultModel|downloadModel\(' Sources
printf '\n--- arch checks ---\n'
rg -n --type=swift 'arm64|x86_64|`#if`|`#else`|`#endif`|targetEnvironment|arch' Sources

Repository: jpthor/voicescribe

Length of output: 5176


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Read the relevant files in small chunks once located.
for f in Sources/VoiceScribe/Models/ModelMetadata.swift Sources/VoiceScribe/Services/TranscriptionEngine.swift Sources/VoiceScribe/Views/OnboardingView.swift; do
  if [ -f "$f" ]; then
    echo "### $f"
    wc -l "$f"
    echo
    sed -n '1,260p' "$f" | cat -n
    echo
  fi
done

Repository: jpthor/voicescribe

Length of output: 12281


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Focused probe for architecture-dependent default model and model URL lookup behavior.
python3 - <<'PY'
from pathlib import Path
import re

paths = [Path('Sources/VoiceScribe/Models/ModelMetadata.swift'),
         Path('Sources/VoiceScribe/Services/TranscriptionEngine.swift'),
         Path('Sources/VoiceScribe/Views/OnboardingView.swift')]
for p in paths:
    if p.exists():
        text = p.read_text()
        print(f'## {p}')
        for pat in [
            r'defaultModel[^\n]*',
            r'if .*arm64.*|`#if`.*arm64|`#else`|`#endif`',
            r'modelURLs\[model\][^\n]*',
            r'Unknown model[^\n]*',
        ]:
            for m in re.finditer(pat, text):
                start = max(0, m.start()-120)
                end = min(len(text), m.end()+200)
                print(text[start:end].replace('\n','\n'))
                print('---')
PY

Repository: jpthor/voicescribe

Length of output: 880


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Determine whether defaultModel differs by architecture using only static source inspection.
rg -n --type=swift -C6 'static\s+(let|var)\s+defaultModel|defaultModel\s*=' Sources/VoiceScribe

Repository: jpthor/voicescribe

Length of output: 156


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Search for architecture-specific defaults and the model lookup implementation.
rg -n --type=swift -C8 'defaultModel|modelURLs\[model\]|Unknown model|downloadModel\(' Sources/VoiceScribe

Repository: jpthor/voicescribe

Length of output: 19486


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Inspect the non-arm64 model URL table and related model selection logic.
sed -n '430,490p' Sources/VoiceScribe/Core/TranscriptionEngine.swift | cat -n
printf '\n--- ModelMetadata ---\n'
sed -n '1,120p' Sources/VoiceScribeCore/ModelMetadata.swift | cat -n
printf '\n--- tests ---\n'
sed -n '1,220p' Tests/VoiceScribeTests/ModelMetadataTests.swift | cat -n

Repository: jpthor/voicescribe

Length of output: 10802


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Inspect the x86_64 model URL map and the load/change paths around the default model.
sed -n '1,260p' Sources/VoiceScribe/Core/TranscriptionEngine.swift | cat -n

Repository: jpthor/voicescribe

Length of output: 11449


Skip the Parakeet download on x86_64. ModelMetadata.defaultModel is always parakeet-unified-en, but the Intel download path only supports tiny/base/small/medium, so this try? makes the onboarding prefetch a silent no-op on Intel Macs. Gate it to arm64 or use the current selected model instead.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@Sources/VoiceScribe/Views/OnboardingView.swift` around lines 79 - 88, Update
startDefaultModelDownloadIfNeeded to skip the default-model prefetch on x86_64,
gating the existing download flow to arm64 before accessing or downloading
ModelMetadata.defaultModel; preserve the current guards and Task behavior for
supported arm64 systems.

Comment on lines +14 to +60
if modelFolder.hasPrefix("download:") {
let variant = String(modelFolder.dropFirst("download:".count))
let base = FileManager.default.homeDirectoryForCurrentUser
.appendingPathComponent("Library/Caches/WhisperKit")
let downloaded = try await WhisperKit.download(
variant: variant,
downloadBase: base,
from: "argmaxinc/whisperkit-coreml"
)
modelFolder = downloaded.path
print("downloaded_model=\(modelFolder)")
}
let audioPath = CommandLine.arguments[2]
let mode = CommandLine.arguments.count > 3 ? CommandLine.arguments[3] : "default"
let runs = CommandLine.arguments.count > 4 ? Int(CommandLine.arguments[4]) ?? 3 : 3

let options: DecodingOptions?
switch mode {
case "default":
options = nil
case "english":
options = DecodingOptions(language: "en", detectLanguage: false)
case "english-no-timestamps":
options = DecodingOptions(
language: "en",
detectLanguage: false,
withoutTimestamps: true
)
default:
throw BenchmarkError.invalidMode(mode)
}

let loadStart = ContinuousClock.now
let whisper = try await WhisperKit(
modelFolder: modelFolder,
computeOptions: ModelComputeOptions(),
verbose: false,
logLevel: .error,
prewarm: false,
load: true,
download: false
)
print("load_seconds=\(seconds(since: loadStart))")

for run in 1...runs {
let start = ContinuousClock.now
let results = try await whisper.transcribe(audioPath: audioPath, decodeOptions: options)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Verify the whisper-benchmark product name in Package.swift
rg -n 'whisper-benchmark|WhisperBenchmark' Package.swift

# Check WhisperKit dependency version
rg -n 'WhisperKit' Package.swift Package.resolved

# Search for the API symbols in the WhisperKit package
fd -t d 'WhisperKit' .build 2>/dev/null | head -5
rg -rn 'public static func download|public init\(|struct DecodingOptions|func transcribe' .build/checkouts/WhisperKit 2>/dev/null | head -30

Repository: jpthor/voicescribe

Length of output: 622


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Locate any local WhisperKit source or cached checkout
fd -a 'WhisperKit' . .build ~/Library/Caches 2>/dev/null | head -100

# Inspect the resolved dependency details
jq '.object.pins[] | select(.identity=="whisperkit")' Package.resolved 2>/dev/null || true

# Search for public API names in any locally available WhisperKit sources
for d in .build/checkouts/WhisperKit .build/repositories/WhisperKit.git ~/Library/Caches/org.swift.swiftpm/repositories/WhisperKit*; do
  [ -e "$d" ] || continue
  echo "== $d =="
  rg -n 'public (static )?func download|public init\(|struct DecodingOptions|func transcribe|audioPath:decodeOptions|withoutTimestamps|detectLanguage' "$d" | head -200
done

Repository: jpthor/voicescribe

Length of output: 156


🌐 Web query:

WhisperKit 0.18.0 public API download variant downloadBase from init modelFolder computeOptions verbose logLevel prewarm load download DecodingOptions language detectLanguage withoutTimestamps transcribe audioPath decodeOptions

💡 Result:

In WhisperKit 0.18.0, the public API centers around the WhisperKit class and its configuration objects [1][2]. Below is a synthesis of the requested parameters and functionality. ### Initialization and Model Management WhisperKit is initialized via the WhisperKit class, which accepts a WhisperKitConfig object or a convenience initializer [2]. Key parameters include: - variant/model: Specifies the model to load (e.g., large-v3-v20240930_626MB) [3][2]. - modelFolder: A String representing the local path to the model directory [1][2]. - downloadBase: A URL specifying the base directory for model downloads; defaults to a system-appropriate Hugging Face cache directory [4][5]. - computeOptions: An object of type ModelComputeOptions used to configure inference compute settings (e.g., target devices, precision) [2]. - verbose and logLevel: Control the verbosity and logging level (e.g.,.info,.debug) for the library [2]. - prewarm: A boolean to trigger early initialization of model components [2][6]. - load: A boolean indicating whether to eager-load models during initialization; if false (default), models are loaded lazily upon the first request [1][2]. - download: A boolean that, when true, enables automatic downloading of the specified model [2][7]. ### Transcription Transcription is performed by invoking methods on an initialized WhisperKit instance, typically involving: - transcribe: The main function to initiate audio processing [8]. - audioPath: The path to the audio file to be transcribed [3]. - decodeOptions: A DecodingOptions object used to control decoding behavior [8][9]. This includes: - language: The source language code [3][9]. - detectLanguage: A boolean to enable automatic language detection [9]. - withoutTimestamps: A configuration within decoding options to suppress timestamp generation in the output [3][9]. In version 0.18.0, architectural changes improved model lifecycle management by unifying download, load, and unload processes through shared managers, with a focus on lazy loading by default to optimize resource usage [1][10].

Citations:


🌐 Web query:

site:github.com/argmaxinc/WhisperKit 0.18.0 "public static func download" "variant" "downloadBase" "from" "public init(" "modelFolder" "computeOptions" "verbose" "logLevel" "prewarm" "load" "download" "DecodingOptions" "withoutTimestamps" "transcribe(audioPath:decodeOptions:)"

💡 Result:

In WhisperKit, the components you referenced relate to model management and the primary transcription API [1][2][3]. The public static method for downloading models is defined within the WhisperKit core as follows: public static func download(variant: String, downloadBase: URL?, useBackgroundSession: Bool, from repo: String, token: String?) async throws -> String [2][4] This method allows you to specify the model variant and a custom downloadBase path, overriding the default behavior that typically saves models to the user's Documents or application support folders [5][6][4]. The initializer for the WhisperKit object is used to configure and load the model environment [7][8][9]. A typical structure for this initializer includes parameters for configuration and state: public init(modelFolder: String? = nil, computeOptions: ComputeOptions? = nil, verbose: Bool = false, logLevel: Logging.LogLevel =.info, prewarm: Bool = true, load: Bool = true) async throws [10][9] The transcribe function enables audio processing and is available in multiple overloads to support different input types [3]: public func transcribe(audioPath: String, decodeOptions: DecodingOptions? = nil) async throws -> TranscriptionResult? [11][3] In this context, DecodingOptions is used to configure parameters such as withoutTimestamps (a boolean property) [3]. Note that the library has evolved significantly; while earlier versions relied on direct initializer parameters, newer versions often utilize a WhisperKitConfig struct for cleaner management of these settings [7]. [1][7]

Citations:


Remove the obsolete download: initializer argument WhisperKit(modelFolder:computeOptions:verbose:logLevel:prewarm:load:) is the 0.18.0 public initializer; download: isn’t part of that signature, so this call won’t match the pinned API.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@Sources/WhisperBenchmark/main.swift` around lines 14 - 60, Remove the
obsolete download: argument from the WhisperKit initializer call in the
benchmark setup, keeping the existing modelFolder, computeOptions, verbose,
logLevel, prewarm, and load arguments unchanged so it matches the 0.18.0 public
initializer.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant