Skip to content
Open
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
6 changes: 6 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,12 @@ This project uses semantic versioning for public releases. Use `MAJOR.MINOR.PATC
- `MINOR` changes add user-visible features and improvements.
- `PATCH` changes fix bugs, polish existing behavior, or make small internal improvements.

## [Unreleased]

### Added

- Custom vocabulary entries can now map specific mishearings to the intended term with an arrow (`cloud code -> Claude Code`), and multiple heard forms can share one correction (`cloud code | clod code -> Claude Code`). Plain entries behave exactly as before.

## [1.1.0] - 2026-06-03

### Added
Expand Down
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@ FreeFlow is a free Mac dictation app inspired by [Wispr Flow](https://wisprflow.

- **Custom shortcuts:** Customize both hold-to-talk and toggle dictation shortcuts. If your toggle shortcut extends your hold shortcut, you can start in hold mode and press the extra modifier keys to latch into tap mode without stopping the recording.
- **Context-aware cleanup:** FreeFlow can read nearby app context so names, terms, and phrases are spelled correctly when you dictate into email, terminals, docs, and other apps.
- **Custom vocabulary:** Add names, jargon, and project-specific words that FreeFlow should preserve during cleanup.
- **Custom vocabulary:** Add names, jargon, and project-specific words that FreeFlow should preserve during cleanup. Entries can also map specific mishearings to the intended term with an arrow — `cloud code -> Claude Code` — and multiple heard forms can share one correction: `cloud code | clod code -> Claude Code`.
- **OpenAI-compatible providers:** Use Groq by default, or configure a custom model and API URL in settings.

## Edit Mode
Expand Down
92 changes: 72 additions & 20 deletions Sources/PostProcessingService.swift
Original file line number Diff line number Diff line change
Expand Up @@ -383,16 +383,7 @@ Behavior:
request.setValue("application/json", forHTTPHeaderField: "Content-Type")
request.timeoutInterval = postProcessingTimeoutSeconds

let normalizedVocabulary = normalizedVocabularyText(customVocabulary)
let vocabularyPrompt = if !normalizedVocabulary.isEmpty {
"""
The following vocabulary must be treated as high-priority terms while rewriting.
Use these spellings exactly in the output when relevant:
\(normalizedVocabulary)
"""
} else {
""
}
let vocabularyPrompt = vocabularyPromptSection(for: customVocabulary)

var systemPrompt = customSystemPrompt.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty
? Self.defaultSystemPrompt
Expand Down Expand Up @@ -514,16 +505,7 @@ Model: \(model)
request.setValue("application/json", forHTTPHeaderField: "Content-Type")
request.timeoutInterval = postProcessingTimeoutSeconds

let normalizedVocabulary = normalizedVocabularyText(customVocabulary)
let vocabularyPrompt = if !normalizedVocabulary.isEmpty {
"""
The following vocabulary must be treated as high-priority terms while rewriting.
Use these spellings exactly in the output when relevant:
\(normalizedVocabulary)
"""
} else {
""
}
let vocabularyPrompt = vocabularyPromptSection(for: customVocabulary)

var systemPrompt = Self.commandModeSystemPrompt
let trimmedOutputLanguage = outputLanguage.trimmingCharacters(in: .whitespacesAndNewlines)
Expand Down Expand Up @@ -707,6 +689,76 @@ Model: \(model)
})
}

/// A vocabulary list split into plain terms and explicit
/// heard-form -> correct-form correction pairs (issue #125).
private struct ParsedVocabulary {
var terms: [String] = []
var corrections: [(heard: String, correct: String)] = []
}

/// Entries may use "heard form -> Correct Form" (or "=>") to teach the
/// model a specific mishearing. Multiple heard variants can share one
/// correction with "|": "cloud code | clod code -> Claude Code".
/// Entries without an arrow behave exactly as before.
private func parseVocabularyEntries(_ entries: [String]) -> ParsedVocabulary {
var parsed = ParsedVocabulary()
for entry in entries {
guard let arrow = entry.range(of: "->") ?? entry.range(of: "=>") else {
parsed.terms.append(entry)
continue
}
let correct = entry[arrow.upperBound...].trimmingCharacters(in: .whitespacesAndNewlines)
let heardForms = entry[..<arrow.lowerBound]
.split(separator: "|")
.map { $0.trimmingCharacters(in: .whitespacesAndNewlines) }
.filter { !$0.isEmpty }
guard !correct.isEmpty, !heardForms.isEmpty else {
// Malformed mapping: keep whichever side exists as a plain term.
let fallback = correct.isEmpty ? heardForms.joined(separator: ", ") : correct
if !fallback.isEmpty { parsed.terms.append(fallback) }
continue
}
parsed.terms.append(correct)
for heard in heardForms {
parsed.corrections.append((heard: heard, correct: correct))
}
}
// A mapping's correct form may repeat an existing plain entry.
var seen = Set<String>()
parsed.terms = parsed.terms.filter { seen.insert($0.lowercased()).inserted }
return parsed
}

/// Builds the system-prompt section for custom vocabulary: a
/// high-priority term list plus, when mappings are present, explicit
/// mishearing corrections. Returns "" when there is no vocabulary.
private func vocabularyPromptSection(for customVocabulary: [String]) -> String {
let parsed = parseVocabularyEntries(customVocabulary)
var sections: [String] = []

let normalizedVocabulary = normalizedVocabularyText(parsed.terms)
if !normalizedVocabulary.isEmpty {
sections.append("""
The following vocabulary must be treated as high-priority terms while rewriting.
Use these spellings exactly in the output when relevant:
\(normalizedVocabulary)
""")
}

if !parsed.corrections.isEmpty {
let pairs = parsed.corrections
.map { "- \"\($0.heard)\" -> \"\($0.correct)\"" }
.joined(separator: "\n")
sections.append("""
Known mishearings. When the transcript contains a left-hand form below (or a close phonetic variant of it) and the speaker clearly meant the right-hand term, output the right-hand form instead:
\(pairs)
Only apply a correction when the surrounding words make the intended term plausible; otherwise leave the transcript wording unchanged.
""")
}

return sections.joined(separator: "\n\n")
}

private func mergedVocabularyTerms(rawVocabulary: String) -> [String] {
let terms = rawVocabulary
.split(whereSeparator: { $0 == "\n" || $0 == "," || $0 == ";" })
Expand Down
9 changes: 6 additions & 3 deletions Sources/SettingsView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -1315,9 +1315,12 @@ struct GeneralSettingsView: View {
appState.customVocabulary = newValue.trimmingCharacters(in: .whitespacesAndNewlines)
}

Text("Separate entries with commas, new lines, or semicolons.")
.font(.caption)
.foregroundStyle(.secondary)
VStack(alignment: .leading, spacing: 4) {
Text("Separate entries with commas, new lines, or semicolons.")
Text("Teach specific mishearings with an arrow: \"cloud code -> Claude Code\". Separate multiple heard forms with \"|\": \"cloud code | clod code -> Claude Code\".")
}
.font(.caption)
.foregroundStyle(.secondary)
}
}

Expand Down