Skip to content

Add custom vocabulary prompt for Whisper models - #281

Open
jrxwdy wants to merge 2 commits into
kitlangton:mainfrom
jrxwdy:feat/custom-vocabulary-prompt
Open

Add custom vocabulary prompt for Whisper models#281
jrxwdy wants to merge 2 commits into
kitlangton:mainfrom
jrxwdy:feat/custom-vocabulary-prompt

Conversation

@jrxwdy

@jrxwdy jrxwdy commented Aug 6, 2026

Copy link
Copy Markdown

Summary

Hex gets names and jargon wrong because the Whisper decoder has no way to know the user's domain vocabulary. Whisper's canonical fix for this is the initial_prompt / hotwords mechanism, and WhisperKit already exposes it via DecodingOptions.promptTokens — Hex just never wires anything into it.

This PR adds a Custom vocabulary section to the Transforms tab: a comma/newline-separated list of terms (names, jargon, product words) that Hex tokenizes with the loaded model's tokenizer and injects as promptTokens before transcription. The decoder treats them as previously transcribed context and is measurably more likely to reproduce their exact spelling/casing.

  • CustomVocabularyPrompt (HexCore): parses terms, builds a labeled prompt (Vocabulary: Langton, TCA.), caps it at 220 chars trimming whole terms — Whisper's decoder context is ~224 tokens shared with prefill, so over-long prompts degrade rather than help
  • TranscriptionClient.transcribe gains an optional vocabulary prompt parameter; nil prompt or nil tokenizer leaves DecodingOptions untouched (zero behavior change for existing users)
  • Persisted via the settings schema with backward-compatible defaults (enabled, empty)
  • No-op for Parakeet, which doesn't support prompt conditioning — the settings UI says so when a Parakeet model is selected. (If FluidAudio grows vocabulary-boosting later, this section can drive it too.)
  • Changeset included

Test plan

  • HexCore builds clean via SwiftPM (swift build)
  • Prompt-building logic verified against 9 cases (parse/trim/disable/cap) via standalone runner — same code, extracted
  • New HexTests/CustomVocabularyPromptTests.swift (XCTest) alongside the existing app-level tests — runs with xcodebuild test -scheme Hex (needs full Xcode; my machine only has Command Line Tools, so I couldn't run the app scheme locally)
  • Manual: add vocabulary → dictate a sentence containing a normally-mistranscribed name with a Whisper model → correct spelling

Happy to open an issue first per the contributing note — figured a small, self-contained fix with tests was easier to evaluate as a PR. No worries if you'd rather take it as an issue report.

Summary by CodeRabbit

  • New Features
    • Added custom vocabulary support for Whisper transcription.
    • Enter names or specialized terms in the Transforms tab using comma- or newline-separated entries.
    • Vocabulary prompts are automatically applied during transcription and saved in settings.
    • Added a warning when custom vocabulary is enabled with Parakeet models, which do not support this feature.

Words the transcriber gets wrong (names, jargon, product terms) can now be
listed in the Transforms tab under "Custom vocabulary". The terms are
tokenized and injected via DecodingOptions.promptTokens — WhisperKit's
equivalent of Whisper's initial_prompt/hotwords — so the decoder treats
them as previously transcribed context and is measurably more likely to
reproduce their exact spelling and casing.

- HexSettings: customVocabulary / customVocabularyEnabled (persisted,
  backward-compatible defaults via the settings schema)
- CustomVocabularyPrompt: parses comma/newline-separated terms and builds
  a labeled prompt ("Vocabulary: ...") capped at 220 chars, trimming
  whole terms rather than splitting mid-word (Whisper's decoder context
  is ~224 tokens shared with prefill, so over-long prompts hurt)
- TranscriptionClient.transcribe takes an optional vocabulary prompt and
  encodes it with the loaded model's tokenizer; a nil tokenizer or empty
  vocabulary leaves DecodingOptions untouched
- No-op for Parakeet, which does not support prompt conditioning; the
  settings UI notes this when a Parakeet model is selected
- Tests cover term parsing, enable/disable, empty input, and prompt
  trimming (run via the Hex scheme alongside the other HexTests)
@coderabbitai

coderabbitai Bot commented Aug 6, 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 Plus

Run ID: c29969c8-860f-47a0-a242-9ddc616f2ca3

📥 Commits

Reviewing files that changed from the base of the PR and between 1d3b919 and e80e263.

📒 Files selected for processing (2)
  • HexCore/Sources/HexCore/Models/CustomVocabularyPrompt.swift
  • HexTests/CustomVocabularyPromptTests.swift

📝 Walkthrough

Walkthrough

The change adds persisted custom vocabulary settings, a vocabulary editor, prompt construction and truncation, and WhisperKit integration that converts the prompt into decoder tokens. Parakeet models display a notice and do not use the prompt.

Changes

Custom vocabulary support

Layer / File(s) Summary
Persisted vocabulary settings and controls
HexCore/Sources/HexCore/Settings/HexSettings.swift, Hex/Features/Settings/SettingsFeature.swift, Hex/Features/Remappings/WordRemappingsView.swift
HexSettings persists the enablement flag and vocabulary text. Settings actions update these values. The remappings view provides the toggle, vocabulary field, and Parakeet notice.
Vocabulary prompt construction
HexCore/Sources/HexCore/Models/CustomVocabularyPrompt.swift, HexTests/CustomVocabularyPromptTests.swift
CustomVocabularyPrompt parses comma- or newline-separated terms, formats enabled prompts, removes empty entries, and limits output to complete terms within 220 characters. Tests cover these behaviors.
Transcription prompt integration
Hex/Features/Transcription/TranscriptionFeature.swift, Hex/Clients/TranscriptionClient.swift, .changeset/custom-vocabulary-prompt.md
Transcription builds and forwards the optional prompt. The live client tokenizes it with WhisperKit and assigns the tokens to DecodingOptions.promptTokens. The changeset documents the Whisper-only behavior.

Estimated code review effort: 3 (Moderate) | ~20 minutes

Sequence Diagram(s)

sequenceDiagram
  participant WordRemappingsView
  participant SettingsFeature
  participant TranscriptionFeature
  participant TranscriptionClientLive
  participant WhisperKit
  WordRemappingsView->>SettingsFeature: Update vocabulary settings
  SettingsFeature->>TranscriptionFeature: Persist custom vocabulary
  TranscriptionFeature->>TranscriptionClientLive: Pass custom vocabulary prompt
  TranscriptionClientLive->>WhisperKit: Encode prompt into promptTokens
  WhisperKit-->>TranscriptionClientLive: Transcribe with decoding options
Loading
🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 40.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 and concisely describes the main change: adding a custom vocabulary prompt for Whisper models.
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
Contributor

Choose a reason for hiding this comment

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

Actionable comments posted: 3

🤖 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 @.changeset/custom-vocabulary-prompt.md:
- Line 5: Update the changeset entry to append the relevant GitHub issue or pull
request number in `(`#123`)` format, using the actual reference associated with
the custom vocabulary prompt change.

In `@Hex/Features/Remappings/WordRemappingsView.swift`:
- Around line 80-105: Update the custom vocabulary controls in the settings view
so both the “Enabled” Toggle and vocabulary TextField are disabled when
ParakeetModel(rawValue: store.hexSettings.selectedModel) is non-nil. Preserve
the existing customVocabularyEnabled condition for the text field while
combining it with the Parakeet check.

In `@HexCore/Sources/HexCore/Models/CustomVocabularyPrompt.swift`:
- Around line 42-45: Update the prompt-building logic in CustomVocabularyPrompt
to enforce maxPromptLength even when the vocabulary has a single overlong term
or an overlong final term. Construct the prompt incrementally from complete
terms that fit within the limit, and return nil when no term can be included;
preserve the existing punctuation and formatting for included terms.
🪄 Autofix

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 Plus

Run ID: 613909f3-e670-4b3d-8965-358c9dff9c3d

📥 Commits

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

📒 Files selected for processing (8)
  • .changeset/custom-vocabulary-prompt.md
  • Hex/Clients/TranscriptionClient.swift
  • Hex/Features/Remappings/WordRemappingsView.swift
  • Hex/Features/Settings/SettingsFeature.swift
  • Hex/Features/Transcription/TranscriptionFeature.swift
  • HexCore/Sources/HexCore/Models/CustomVocabularyPrompt.swift
  • HexCore/Sources/HexCore/Settings/HexSettings.swift
  • HexTests/CustomVocabularyPromptTests.swift

"hex-app": minor
---

Add custom vocabulary prompt for Whisper models: a new "Custom vocabulary" section in the Transforms tab lets you list names/jargon the transcriber gets wrong. The terms are injected as decoder prompt tokens (Whisper's `initial_prompt` equivalent) before transcription, biasing WhisperKit toward the exact spelling and casing you entered. Applies to Whisper models only; Parakeet ignores the setting.

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.

📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

Add the GitHub issue or PR reference.

Append the actual issue or PR number in (#123) format.

Proposed fix
-Add custom vocabulary prompt for Whisper models: a new "Custom vocabulary" section in the Transforms tab lets you list names/jargon the transcriber gets wrong. The terms are injected as decoder prompt tokens (Whisper's `initial_prompt` equivalent) before transcription, biasing WhisperKit toward the exact spelling and casing you entered. Applies to Whisper models only; Parakeet ignores the setting.
+Add custom vocabulary prompt for Whisper models: a new "Custom vocabulary" section in the Transforms tab lets you list names/jargon the transcriber gets wrong. The terms are injected as decoder prompt tokens (Whisper's `initial_prompt` equivalent) before transcription, biasing WhisperKit toward the exact spelling and casing you entered. Applies to Whisper models only; Parakeet ignores the setting. (`#123`)

As per coding guidelines, include a GitHub issue or PR number in (#123) format.

🤖 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 @.changeset/custom-vocabulary-prompt.md at line 5, Update the changeset entry
to append the relevant GitHub issue or pull request number in `(`#123`)` format,
using the actual reference associated with the custom vocabulary prompt change.

Source: Coding guidelines

Comment on lines +80 to +105
Toggle(
"Enabled",
isOn: Binding(
get: { store.hexSettings.customVocabularyEnabled },
set: { store.send(.setCustomVocabularyEnabled($0)) }
)
)
.toggleStyle(.switch)
.controlSize(.small)
}

TextField(
"Comma-separated, e.g. Langton, Kit, TCA, WhisperKit",
text: Binding(
get: { store.hexSettings.customVocabulary },
set: { store.send(.setCustomVocabulary($0)) }
),
axis: .vertical
)
.textFieldStyle(.roundedBorder)
.lineLimit(2...4)
.disabled(!store.hexSettings.customVocabularyEnabled)

if ParakeetModel(rawValue: store.hexSettings.selectedModel) != nil {
Text("Custom vocabulary only applies to Whisper models. Your selected model is Parakeet.")
.settingsCaption()

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.

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Disable custom vocabulary controls for Parakeet.

The notice does not prevent edits when the selected model is Parakeet. TranscriptionClient ignores the setting on that path. Disable the toggle and text field when ParakeetModel(rawValue: store.hexSettings.selectedModel) != nil.

Proposed fix
 						.toggleStyle(.switch)
 						.controlSize(.small)
+						.disabled(ParakeetModel(rawValue: store.hexSettings.selectedModel) != nil)
 					}
@@
-					.disabled(!store.hexSettings.customVocabularyEnabled)
+					.disabled(
+						!store.hexSettings.customVocabularyEnabled ||
+						ParakeetModel(rawValue: store.hexSettings.selectedModel) != nil
+					)
📝 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
Toggle(
"Enabled",
isOn: Binding(
get: { store.hexSettings.customVocabularyEnabled },
set: { store.send(.setCustomVocabularyEnabled($0)) }
)
)
.toggleStyle(.switch)
.controlSize(.small)
}
TextField(
"Comma-separated, e.g. Langton, Kit, TCA, WhisperKit",
text: Binding(
get: { store.hexSettings.customVocabulary },
set: { store.send(.setCustomVocabulary($0)) }
),
axis: .vertical
)
.textFieldStyle(.roundedBorder)
.lineLimit(2...4)
.disabled(!store.hexSettings.customVocabularyEnabled)
if ParakeetModel(rawValue: store.hexSettings.selectedModel) != nil {
Text("Custom vocabulary only applies to Whisper models. Your selected model is Parakeet.")
.settingsCaption()
Toggle(
"Enabled",
isOn: Binding(
get: { store.hexSettings.customVocabularyEnabled },
set: { store.send(.setCustomVocabularyEnabled($0)) }
)
)
.toggleStyle(.switch)
.controlSize(.small)
.disabled(ParakeetModel(rawValue: store.hexSettings.selectedModel) != nil)
}
TextField(
"Comma-separated, e.g. Langton, Kit, TCA, WhisperKit",
text: Binding(
get: { store.hexSettings.customVocabulary },
set: { store.send(.setCustomVocabulary($0)) }
),
axis: .vertical
)
.textFieldStyle(.roundedBorder)
.lineLimit(2...4)
.disabled(
!store.hexSettings.customVocabularyEnabled ||
ParakeetModel(rawValue: store.hexSettings.selectedModel) != nil
)
if ParakeetModel(rawValue: store.hexSettings.selectedModel) != nil {
Text("Custom vocabulary only applies to Whisper models. Your selected model is Parakeet.")
.settingsCaption()
🤖 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 `@Hex/Features/Remappings/WordRemappingsView.swift` around lines 80 - 105,
Update the custom vocabulary controls in the settings view so both the “Enabled”
Toggle and vocabulary TextField are disabled when ParakeetModel(rawValue:
store.hexSettings.selectedModel) is non-nil. Preserve the existing
customVocabularyEnabled condition for the text field while combining it with the
Parakeet check.

Comment thread HexCore/Sources/HexCore/Models/CustomVocabularyPrompt.swift Outdated
CodeRabbit review on kitlangton#281: the comma-trim loop only enforced
maxPromptLength when a comma existed to trim back to, so a single
over-long term (or an over-long final term) shipped an over-length
prompt — defeating the guard that protects Whisper's ~224-token
decoder context.

Build the prompt incrementally from whole terms that fit, skipping any
term that would exceed the cap and returning nil when none do. Terms
are still never split mid-word. Adds tests for the single-overlong,
overlong-tail, and overlong-middle cases.
@jrxwdy

jrxwdy commented Aug 6, 2026

Copy link
Copy Markdown
Author

Addressed CodeRabbit's finding on the prompt-length cap (e80e263): the comma-trim loop only enforced maxPromptLength when a comma existed to trim back to, so a single over-long term or an over-long final term could ship an over-length prompt. Rebuilt the prompt incrementally from whole terms that fit — skipping any term that would exceed the cap and returning nil when none do. Added tests for the single-overlong, overlong-tail, and overlong-middle cases; all pass.

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