Skip to content

Add Apple SpeechAnalyzer (macOS 26+) as a third transcription engine - #269

Open
buffpesos wants to merge 4 commits into
kitlangton:mainfrom
buffpesos:feat/apple-speechanalyzer-engine
Open

buffpesos wants to merge 4 commits into
kitlangton:mainfrom
buffpesos:feat/apple-speechanalyzer-engine

Conversation

@buffpesos

@buffpesos buffpesos commented Jul 21, 2026

Copy link
Copy Markdown

Closes #255

Adds Apple's on-device SpeechAnalyzer/SpeechTranscriber engine as a selectable "Apple Speech" entry in the model library, alongside WhisperKit and Parakeet. Batch-only: it transcribes the recorded file after the hotkey is released, exactly like the existing engines.

How it fits the existing architecture

Routing follows the Parakeet pattern precisely: a new AppleSpeechModel enum ("apple-speechanalyzer") acts as the routing identifier, and TranscriptionClientLive gains an isAppleSpeech branch parallel to isParakeet in each method. The engine lives in a new AppleSpeechClient actor mirroring ParakeetClient's structure, including the compile-time fallback stub — here keyed on #if compiler(>=6.2) (the toolchain that ships the macOS 26 SDK) instead of canImport(FluidAudio).

Design decisions (and why)

  • One row, not per-locale rows. SpeechAnalyzer is one engine with OS-managed per-locale asset packs, and the existing Output Language setting already expresses "which language". "Installed" for this row means "assets for the currently selected language are on-device"; switching to a language without assets flips the row to not-downloaded and the normal Download button installs the new locale pack — with the OS's native download Progress, and Cancel wired through.
  • Deployment target stays macOS 14. Two gates: CuratedModelLoader filters the row out below macOS 26 at State init, and .modelsLoaded drops it unless the engine advertised its identifier via getAvailableModels (macOS 26 + SpeechTranscriber.isAvailable hardware check + macOS 26 SDK build). The AppleSpeechClient actor is deliberately stateless so the non-@available TranscriptionClientLive can store it; all SDK calls live in a private @available(macOS 26.0, *) enum.
  • Auto-switch exemption. The .modelsLoaded fallback that switches away from not-downloaded selections gets a narrow exception: when Apple Speech is selected and its row is present, a missing locale pack prompts a download instead of silently switching engines. When the row is absent (OS downgrade, unsupported hardware), the existing healing to an installed local model still applies.
  • Show in Finder / Remove Download are hidden for this row. Assets live in OS-private storage and AssetInventory has no uninstall API — offering either would mislead. deleteModel is a defensive no-op.
  • Recommended default unchanged (Parakeet). Promoting Apple Speech is a separate discussion.
  • Locale resolution is pure logic in HexCore (SpeechLocaleResolution): exact BCP-47 match, else same-language preferring the user's region ("en" → "en-US" on a US Mac); Auto falls back through the current locale → English → first supported. Unsupported languages throw an actionable error rather than silently transcribing in the wrong language. Being OS-independent, it unit-tests on macOS 14.
  • Reservation quota: stale locale reservations are released before reserving the active one (the system caps per-app reservations); reservation failure is non-fatal since installed assets keep working.

Toolchain note for releases

The engine only exists in binaries built with Xcode 26. A build from Xcode 16 compiles the stub and the row simply never appears — graceful, but worth knowing that the release toolchain gates the feature.

Tests

  • 10 new HexCore tests for the locale resolver (cd HexCore && swift test, runs on any macOS).
  • 5 new ModelDownloadFeatureTests covering the OS filter, the engine-advertisement gating, installed-state merging, the auto-switch exemption, and OS-downgrade healing. Two skip via XCTSkip on hosts below macOS 26 (they need the bundled curated row).
  • Full suite green (xcodebuild test), Release build compiles.

Manual verification (macOS 26, Apple Silicon)

  • Selected Apple Speech in the model library; row renders with dots and "Managed by macOS", no Finder/Delete menu items.
  • Repeated hotkey dictations transcribe and paste correctly; Console shows AppleSpeech category logs with transcript text and file names correctly marked private.
  • Locale assets already installed system-wide were detected immediately (no download needed for English).
  • Permissions: microphone only — no speech-recognition TCC prompt, no new entitlements.

Possible follow-up (intentionally out of scope): streaming transcription while recording via SpeechAnalyzer's volatile results, which would pair naturally with the live-preview work in #238.

🤖 Generated with Claude Code

Summary by CodeRabbit

  • New Features
    • Added Apple’s on-device SpeechAnalyzer (Apple Speech) as a selectable transcription engine on supported macOS 26+ systems.
    • Added per-language Apple Speech locale assets and installation handling.
    • Added Apple Speech to the model library with download/install state support.
  • Improvements
    • Refresh model availability when changing output language.
    • Updated Apple Speech UI so local file management options are hidden when not supported.
    • Enhanced Apple Speech transcription reliability with safer cancellation and result handling.
  • Tests
    • Added coverage for Apple Speech availability/selection and locale resolution behavior.

buffpesos and others added 3 commits July 20, 2026 22:29
Groundwork for offering Apple's on-device SpeechAnalyzer (macOS 26+) as a
third transcription engine alongside WhisperKit and Parakeet (kitlangton#255).

- AppleSpeechModel: a single reserved identifier ("apple-speechanalyzer")
  mirroring ParakeetModel's role as the engine routing table. One entry by
  design: per-locale assets are OS-managed via AssetInventory and follow the
  existing Output Language setting, so the engine is one "model" from the
  user's perspective.
- SpeechLocaleResolution: pure resolver mapping the outputLanguage setting
  (bare ISO codes, nil = Auto) onto SpeechTranscriber's supported locales.
  Exact BCP-47 match first, then same-language with the user's region
  preferred ("en" -> "en-US" on a US Mac); auto falls back through the
  current locale, English, then the first supported locale. Returns nil for
  unsupported languages so callers surface a clear error instead of
  silently transcribing in the wrong language. Kept free of Speech imports
  so it compiles and tests on the macOS 14 deployment target.
- HexLog: new AppleSpeech category so Console predicates stay consistent.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Second of three commits adding Apple's on-device SpeechAnalyzer as a third
transcription engine (kitlangton#255).

AppleSpeechClient mirrors ParakeetClient's shape: an actor with a
compile-time fallback stub, here keyed on `#if compiler(>=6.2)` (the
toolchain that ships the macOS 26 SDK) so the app still builds on Xcode 16.
The actor is deliberately stateless — locale assets live in the OS-managed
AssetInventory and SpeechTranscriber construction is cheap — which is what
lets the non-@available TranscriptionClientLive store it directly; all
Speech-SDK calls live in a private @available(macOS 26.0, *) enum.

- Batch transcription: AVAudioFile -> analyzeSequence -> finalizeAndFinish,
  results drained concurrently, with a timeout race whose cancellation
  handler explicitly cancels the unstructured results task. Every error
  path (including TCA effect cancellation from ESC) tears down via
  cancelAndFinishNow so no analyzer outlives a request.
- Asset management: installs locale packs on demand via
  AssetInventory.assetInstallationRequest, reporting the request's native
  Progress through the existing download-progress plumbing; releases stale
  locale reservations before reserving to avoid exhausting the system's
  per-app reservation quota (reservation failure is non-fatal).
- Locale selection: the outputLanguage setting resolves against
  SpeechTranscriber.supportedLocales via HexCore's SpeechLocaleResolution;
  unsupported languages throw an actionable error rather than silently
  transcribing in the wrong language.

TranscriptionClientLive routes by model-name identity exactly like
Parakeet: a new isAppleSpeech predicate branches transcribe / download /
delete (no-op; assets are OS-managed) / isModelDownloaded, and
getAvailableModels advertises the identifier only when the engine reports
itself usable (macOS 26+, capable hardware, macOS 26 SDK build) — the UI
will key the row's visibility off that in the next commit.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Final commit of three adding Apple's on-device SpeechAnalyzer engine
(kitlangton#255). Surfaces the engine as a single "Apple Speech" row in Settings →
Transcription Model, gated at two levels:

- OS gate: CuratedModelLoader filters the row out below macOS 26 at State
  init, so it never exists on older systems (NoModelChooser unaffected).
- Hardware/build gate: .modelsLoaded drops the row unless the engine
  advertised its identifier via getAvailableModels (macOS 26 + capable
  hardware + macOS 26 SDK build).

One row by design: locale assets are OS-managed and follow the existing
Output Language setting, so per-locale rows would duplicate that setting.
"Installed" for this row means "assets for the currently selected language
are on-device" — switching to a language without assets flips the row to
not-downloaded, and the existing Download button flow installs the new
locale pack with native progress. setOutputLanguage now refreshes the
model list so that state tracks the language setting.

The auto-switch fallback in .modelsLoaded gets a narrow exemption: when
Apple Speech is selected and its row is present, a missing locale pack
must prompt a download rather than silently switching engines. When the
row is absent (OS downgrade, unsupported hardware), the existing healing
to an installed local model still applies — covered by new tests.

Show in Finder and Remove Download are hidden for this row: assets live in
OS-private storage and AssetInventory has no uninstall API, so offering
either would mislead. Parakeet remains the recommended default.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@coderabbitai

coderabbitai Bot commented Jul 21, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: bbf12a50-46d4-41ee-b776-bd12cdb38eaf

📥 Commits

Reviewing files that changed from the base of the PR and between 30e4396 and a2d2cd5.

📒 Files selected for processing (1)
  • Hex/Clients/TranscriptionClient.swift
🚧 Files skipped from review as they are similar to previous changes (1)
  • Hex/Clients/TranscriptionClient.swift

📝 Walkthrough

Walkthrough

Adds Apple’s on-device SpeechAnalyzer as a selectable, locale-aware transcription engine on macOS 26+, with model-library filtering, OS-managed asset controls, cancellation-safe transcription, and coverage for locale resolution and model selection.

Changes

Apple SpeechAnalyzer support

Layer / File(s) Summary
Speech model and locale contracts
HexCore/Sources/HexCore/Models/AppleSpeechModel.swift, HexCore/Sources/HexCore/Logic/SpeechLocaleResolution.swift, HexCore/Sources/HexCore/Logging.swift, Hex/Resources/Data/models.json, HexCore/Tests/HexCoreTests/SpeechLocaleResolutionTests.swift
Defines the Apple Speech identifier, locale fallback rules, logging category, model metadata, and locale-resolution tests.
Apple Speech client and transcription engine
Hex/Clients/AppleSpeechClient.swift
Adds SpeechAnalyzer availability checks, locale asset installation, batch transcription, timeout handling, cancellation teardown, localized errors, and unsupported-toolchain stubs.
Transcription client integration
Hex/Clients/TranscriptionClient.swift, Hex/Features/Settings/SettingsFeature.swift
Routes Apple Speech model operations, reports locale-specific installation state, skips filesystem deletion, exposes the engine when supported, and refreshes models after language changes.
Model library availability and controls
Hex/Features/Settings/ModelDownload/ModelDownloadFeature.swift, Hex/Features/Settings/ModelDownload/ModelDownloadView.swift, HexTests/ModelDownloadFeatureTests.swift, CLAUDE.md, .changeset/766b5f22.md
Filters Apple Speech by platform and engine availability, preserves locale-dependent selection behavior, hides local-file controls for OS-managed assets, updates documentation and release notes, and tests model-library behavior.

Estimated code review effort: 4 (Complex) | ~45 minutes

Sequence Diagram(s)

sequenceDiagram
  participant SettingsFeature
  participant ModelDownloadFeature
  participant TranscriptionClientLive
  participant AppleSpeechClient
  participant SpeechAnalyzer

  SettingsFeature->>ModelDownloadFeature: refresh models after output language change
  ModelDownloadFeature->>TranscriptionClientLive: fetch available models and installation state
  TranscriptionClientLive->>AppleSpeechClient: check supported locale assets
  AppleSpeechClient-->>TranscriptionClientLive: return installed state
  TranscriptionClientLive-->>ModelDownloadFeature: return Apple Speech model information
  TranscriptionClientLive->>AppleSpeechClient: transcribe audio
  AppleSpeechClient->>SpeechAnalyzer: analyze audio sequence
  SpeechAnalyzer-->>AppleSpeechClient: return transcription results
  AppleSpeechClient-->>TranscriptionClientLive: return transcript
Loading

Possibly related PRs

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 26.67% 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 accurately summarizes the main change: adding Apple SpeechAnalyzer as a third transcription engine on macOS 26+.
Linked Issues check ✅ Passed The changes implement #255 by adding Apple SpeechAnalyzer support as an additional speech-to-text engine on supported macOS 26 systems.
Out of Scope Changes check ✅ Passed The added docs, tests, and release note support the Apple Speech work and do not introduce unrelated scope.
✨ 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
Contributor

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🤖 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 `@Hex/Clients/TranscriptionClient.swift`:
- Around line 104-105: Update the Apple Speech comment above
isAppleSpeech(variant) to append the GitHub issue reference (`#255`), preserving
the existing explanation.
🪄 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: f4fa7101-0998-45f5-b9e9-dedc00e52538

📥 Commits

Reviewing files that changed from the base of the PR and between 881c46f and 1e82ed7.

📒 Files selected for processing (13)
  • .changeset/766b5f22.md
  • CLAUDE.md
  • Hex/Clients/AppleSpeechClient.swift
  • Hex/Clients/TranscriptionClient.swift
  • Hex/Features/Settings/ModelDownload/ModelDownloadFeature.swift
  • Hex/Features/Settings/ModelDownload/ModelDownloadView.swift
  • Hex/Features/Settings/SettingsFeature.swift
  • Hex/Resources/Data/models.json
  • HexCore/Sources/HexCore/Logging.swift
  • HexCore/Sources/HexCore/Logic/SpeechLocaleResolution.swift
  • HexCore/Sources/HexCore/Models/AppleSpeechModel.swift
  • HexCore/Tests/HexCoreTests/SpeechLocaleResolutionTests.swift
  • HexTests/ModelDownloadFeatureTests.swift

Comment thread Hex/Clients/TranscriptionClient.swift Outdated
Addresses CodeRabbit review feedback on PR kitlangton#269: CLAUDE.md asks that code
comments link the GitHub issue a change addresses.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@buffpesos
buffpesos force-pushed the feat/apple-speechanalyzer-engine branch from 30e4396 to a2d2cd5 Compare July 21, 2026 06:59
@buffpesos

Copy link
Copy Markdown
Author

Addressed the review note — (#255) is now referenced in the routing comment (a2d2cd5). Note: the committable suggestion as generated dropped the if isAppleSpeech(variant) { line (its range covered two lines but replaced them with one), so the suggestion commit was replaced with a hand-made equivalent rather than kept.

@buffpesos

Copy link
Copy Markdown
Author

@kitlangton could you take a look at this?

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.

Add support for SpeechAnalyzer (Apple)

1 participant