c0c9dad149
Replace the Sherpa offline pipeline with native MLX streaming, resilient model downloads, live transcript previews, and supporting tests and documentation.
86 lines
3.1 KiB
Swift
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
|
|
}
|
|
}
|
|
}
|