Files
OSGKeyboard/OSGKeyboardMac/MacHallucinationFilter.swift
T
Rocky c0c9dad149 feat(mac): add Qwen3 MLX streaming dictation
Replace the Sherpa offline pipeline with native MLX streaming, resilient model downloads, live transcript previews, and supporting tests and documentation.
2026-07-23 14:34:56 +08:00

86 lines
3.1 KiB
Swift

// MacHallucinationFilter.swift
// OSGKeyboard · Mac
//
// Strips Qwen3 / MLX streaming scaffold tokens and silence hallucinations.
import Foundation
enum MacQwen3LanguageHint {
/// Map persisted BCP-47 locale ids to Qwen3 prompt language names.
/// Returns `nil` for auto-detect.
static func from(locale: Locale) -> String? {
let raw = locale.identifier.lowercased()
if raw.isEmpty || raw == "auto" { return nil }
if raw.hasPrefix("zh") { return "Chinese" }
if raw.hasPrefix("en") { return "English" }
if raw.hasPrefix("ja") { return "Japanese" }
if raw.hasPrefix("ko") { return "Korean" }
if raw.hasPrefix("fr") { return "French" }
if raw.hasPrefix("de") { return "German" }
if raw.hasPrefix("es") { return "Spanish" }
if raw.hasPrefix("pt") { return "Portuguese" }
if raw.hasPrefix("ru") { return "Russian" }
if raw.hasPrefix("ar") { return "Arabic" }
return nil
}
}
enum MacHallucinationFilter {
/// RMS below this skips feeding audio into the MLX streaming session.
static let silencePeakThreshold: Float = 0.0005
static func strip(_ raw: String) -> String {
var text = raw.trimmingCharacters(in: .whitespacesAndNewlines)
if text.isEmpty { return "" }
if let marker = text.range(of: "<asr_text>", options: .backwards) {
text = String(text[marker.upperBound...])
.trimmingCharacters(in: .whitespacesAndNewlines)
} else if let match = text.range(
of: #"^language\s+\S+\s*"#,
options: [.regularExpression, .caseInsensitive]
) {
text = String(text[match.upperBound...])
.trimmingCharacters(in: .whitespacesAndNewlines)
}
if isMetadataNoiseLine(text) { return "" }
return text
}
/// When the transcript is mostly vocabulary tokens and audio energy stayed low, drop it.
static func shouldDiscardHotwordDump(
text: String,
peakRMS: Float,
bias: LocalASRBiasPayload?
) -> Bool {
let trimmed = text.trimmingCharacters(in: .whitespacesAndNewlines)
guard !trimmed.isEmpty else { return true }
guard peakRMS < FlowCaptureTailDrainPolicy.flowDefault.silenceRMSThreshold else {
return false
}
guard let bias, !bias.hardHotwords.isEmpty else { return false }
let lowered = trimmed.lowercased()
let hits = bias.hardHotwords.filter { lowered.contains($0.lowercased()) }.count
let wordCount = max(1, trimmed.split { $0.isWhitespace }.count)
return hits >= wordCount
}
private static func isMetadataNoiseLine(_ line: String) -> Bool {
let lowered = line.lowercased()
switch lowered {
case "language", "emotion", "event", "text",
"<asr_text>", "</asr_text>", "<|im_end|>":
return true
default:
if lowered.range(
of: #"^language(\s+\S+)?$"#,
options: .regularExpression
) != nil {
return true
}
return false
}
}
}