Files
OSGKeyboard/OSGKeyboardShared/Services/LocalASRTranscriptCorrector.swift
Rocky 200265fbd6 feat(macos): local ASR model manager, menu-bar polish, and release 0.5.2
Adds a bundled local ASR model catalog for the macOS app with one-click
Sherpa Qwen3 / SenseVoice downloads (pause/resume, inline actions) and a
shared model storage directory used by MLX Qwen3. Fixes the light-mode
sidebar material and makes the menu-bar icon follow the system appearance
with a refreshed status mark. Renames the built product to OSGKeyboard.app.

Bumps version to 0.5.2 (build 19).
2026-07-09 08:55:37 +08:00

69 lines
2.0 KiB
Swift

// LocalASRTranscriptCorrector.swift
// OSGKeyboard · Shared
//
// Deterministic alias → canonical term replacement between raw ASR output
// and the LLM polish step. Only applies whole-phrase matches.
import Foundation
public enum LocalASRTranscriptCorrector {
/// Applies high-confidence alias replacements (longest match first).
public static func apply(
_ text: String,
pairs: [LocalASRCorrectionPair]
) -> String {
guard !text.isEmpty, !pairs.isEmpty else { return text }
let sorted = pairs.sorted { lhs, rhs in
if lhs.alias.count != rhs.alias.count {
return lhs.alias.count > rhs.alias.count
}
return lhs.alias.localizedCaseInsensitiveCompare(rhs.alias) == .orderedAscending
}
var result = text
for pair in sorted {
result = replaceWholeMatches(
in: result,
alias: pair.alias,
term: pair.term
)
}
return result
}
// MARK: - Private
private static func replaceWholeMatches(
in text: String,
alias: String,
term: String
) -> String {
guard !alias.isEmpty, alias != term else { return text }
if alias.unicodeScalars.allSatisfy({ $0.isASCII }) {
return replaceASCIIWord(in: text, alias: alias, term: term)
}
return text.replacingOccurrences(of: alias, with: term)
}
private static func replaceASCIIWord(
in text: String,
alias: String,
term: String
) -> String {
let escaped = NSRegularExpression.escapedPattern(for: alias)
let pattern = "(?i)(?<![A-Za-z0-9_])\(escaped)(?![A-Za-z0-9_])"
guard let regex = try? NSRegularExpression(pattern: pattern) else {
return text
}
let range = NSRange(text.startIndex..<text.endIndex, in: text)
return regex.stringByReplacingMatches(
in: text,
range: range,
withTemplate: term
)
}
}