Initial commit of Edison Voice menu-bar dictation app
This commit is contained in:
@@ -0,0 +1,103 @@
|
||||
import Foundation
|
||||
|
||||
/// The cleanup pass between raw transcription and injection.
|
||||
///
|
||||
/// This is where Wispr Flow actually earns its keep — raw STT output is full of filler
|
||||
/// words, missing punctuation, and spoken corrections. Swapping in an LLM-backed
|
||||
/// formatter (Apple Foundation Models on-device, or Claude for the high-quality tier)
|
||||
/// is the point of keeping this behind a protocol.
|
||||
public protocol TextFormatter: Sendable {
|
||||
func format(_ raw: String) async -> String
|
||||
}
|
||||
|
||||
/// Deterministic, zero-latency cleanup. Good enough to be useful on its own and always
|
||||
/// the fallback when a model-backed formatter is unavailable or times out.
|
||||
public struct RuleBasedFormatter: TextFormatter {
|
||||
public init() {}
|
||||
/// Standalone filler words, stripped only when surrounded by word boundaries.
|
||||
private static let fillers = ["um", "uh", "erm", "uhm", "hmm", "mhm"]
|
||||
|
||||
/// Spoken punctuation people actually use mid-dictation.
|
||||
private static let spokenPunctuation: [(String, String)] = [
|
||||
("new paragraph", "\n\n"),
|
||||
("new line", "\n"),
|
||||
("open paren", " ("),
|
||||
("close paren", ") "),
|
||||
]
|
||||
|
||||
public func format(_ raw: String) async -> String {
|
||||
var text = raw.trimmingCharacters(in: .whitespacesAndNewlines)
|
||||
guard !text.isEmpty else { return text }
|
||||
|
||||
text = stripFillers(from: text)
|
||||
text = applySpokenPunctuation(to: text)
|
||||
text = collapseWhitespace(in: text)
|
||||
text = capitalizeSentences(in: text)
|
||||
text = ensureTerminalPunctuation(in: text)
|
||||
|
||||
return text
|
||||
}
|
||||
|
||||
private func stripFillers(from text: String) -> String {
|
||||
var result = text
|
||||
for filler in Self.fillers {
|
||||
// Match the filler as a whole word, plus a trailing comma if the ASR added one.
|
||||
let pattern = "(?i)(?<![\\w'])\(filler)\\b,?"
|
||||
result = result.replacingOccurrences(
|
||||
of: pattern,
|
||||
with: "",
|
||||
options: .regularExpression
|
||||
)
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
private func applySpokenPunctuation(to text: String) -> String {
|
||||
var result = text
|
||||
for (phrase, replacement) in Self.spokenPunctuation {
|
||||
result = result.replacingOccurrences(
|
||||
of: "(?i)\\b\(phrase)\\b",
|
||||
with: replacement,
|
||||
options: .regularExpression
|
||||
)
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
private func collapseWhitespace(in text: String) -> String {
|
||||
text
|
||||
.replacingOccurrences(of: "[ \\t]+", with: " ", options: .regularExpression)
|
||||
.replacingOccurrences(of: " +([,.!?;:])", with: "$1", options: .regularExpression)
|
||||
.replacingOccurrences(of: "\\n{3,}", with: "\n\n", options: .regularExpression)
|
||||
.trimmingCharacters(in: .whitespacesAndNewlines)
|
||||
}
|
||||
|
||||
private func capitalizeSentences(in text: String) -> String {
|
||||
var result = ""
|
||||
var capitalizeNext = true
|
||||
|
||||
for character in text {
|
||||
if capitalizeNext, character.isLetter {
|
||||
result.append(Character(character.uppercased()))
|
||||
capitalizeNext = false
|
||||
} else {
|
||||
result.append(character)
|
||||
if ".!?\n".contains(character) { capitalizeNext = true }
|
||||
}
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
private func ensureTerminalPunctuation(in text: String) -> String {
|
||||
guard let last = text.last, last.isLetter || last.isNumber else { return text }
|
||||
return text + "."
|
||||
}
|
||||
}
|
||||
|
||||
/// No-op formatter, for comparing raw engine output against the cleanup pass.
|
||||
public struct PassthroughFormatter: TextFormatter {
|
||||
public init() {}
|
||||
public func format(_ raw: String) async -> String {
|
||||
raw.trimmingCharacters(in: .whitespacesAndNewlines)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user