feat(keyboard): improve typing, voice flow, and polish reliability

Reduce extension memory pressure and delivery races while adding richer candidates, tactile feedback, and safer two-level creative polishing.
This commit is contained in:
Rocky
2026-08-05 21:39:31 +08:00
parent 38e5ad570d
commit 31f5937a7f
177 changed files with 8343 additions and 3904 deletions
@@ -2,33 +2,17 @@
// OSGKeyboard · Shared
//
// Deterministic protection for content that must survive an LLM rewrite.
// High-confidence violations are enforced; noisier heuristics are observed.
import Foundation
public enum PolishViolation: Equatable, Sendable {
case missingDictionaryTerms([String])
case missingIdentifiers([String])
case missingNumbers([String])
case lengthOutOfRange(ratio: Double, allowed: ClosedRange<Double>)
case languageDrift(inputCJK: Double, outputCJK: Double)
public var isHard: Bool {
switch self {
case .missingDictionaryTerms, .missingIdentifiers:
return true
case .missingNumbers, .lengthOutOfRange, .languageDrift:
return false
}
}
public var logLabel: String {
switch self {
case .missingDictionaryTerms(let values): return "dictionary:\(values.count)"
case .missingIdentifiers(let values): return "identifier:\(values.count)"
case .missingNumbers(let values): return "number:\(values.count)"
case .lengthOutOfRange: return "length:1"
case .languageDrift: return "language:1"
}
}
}
@@ -37,8 +21,7 @@ public enum PolishOutputValidator {
public static func validate(
input: String,
output: String,
dictionary: PersonalDictionary,
lengthRatio: ClosedRange<Double>
dictionary: PersonalDictionary
) -> [PolishViolation] {
var violations: [PolishViolation] = []
@@ -61,50 +44,9 @@ public enum PolishOutputValidator {
violations.append(.missingIdentifiers(missingIdentifiers))
}
let inputNumbers = matches(#"\d+(?:[.,]\d+)*"#, in: input)
let allowedOrdinalNumbers = allowedOrdinalRepairNumbers(input: input, output: output)
let missingNumbers = Array(Set(inputNumbers.filter {
!output.contains($0) && !allowedOrdinalNumbers.contains($0)
})).sorted()
if !missingNumbers.isEmpty {
violations.append(.missingNumbers(missingNumbers))
}
if input.count >= 20 {
let ratio = Double(output.count) / Double(max(input.count, 1))
if !lengthRatio.contains(ratio) {
violations.append(.lengthOutOfRange(ratio: ratio, allowed: lengthRatio))
}
}
let inputCJK = TranscriptLanguageDetector.cjkRatio(input)
let outputCJK = TranscriptLanguageDetector.cjkRatio(output)
if input.count >= 20, abs(inputCJK - outputCJK) >= 0.15 {
violations.append(.languageDrift(inputCJK: inputCJK, outputCJK: outputCJK))
}
return violations
}
public static func retryInstruction(
for violations: [PolishViolation],
useChinese: Bool
) -> String {
let protectedValues = violations.flatMap { violation -> [String] in
switch violation {
case .missingDictionaryTerms(let values), .missingIdentifiers(let values):
return values
default:
return []
}
}
guard !protectedValues.isEmpty else { return "" }
let joined = protectedValues.joined(separator: ", ")
return useChinese
? "上一次输出遗漏或修改了以下受保护内容:\(joined)。重新处理,并确保它们逐字符原样保留。"
: "The previous output omitted or changed protected content: \(joined). Process it again and preserve every item exactly."
}
private static func protectedIdentifiers(in text: String) -> Set<String> {
let patterns = [
#"https?://[^\s<>"']+"#,
@@ -141,8 +83,7 @@ public enum PolishOutputValidator {
let segments = normalized.split(separator: "/", omittingEmptySubsequences: true)
guard segments.count >= 2 else { return false }
// Dates and fractions such as 2025/03/01, 3/4, and 3/5 are numeric
// values, not file paths. They remain covered by soft number telemetry.
// Dates and fractions such as 2025/03/01, 3/4, and 3/5 are not paths.
if segments.allSatisfy({ $0.allSatisfy(\.isNumber) }) {
return false
}
@@ -151,68 +92,6 @@ public enum PolishOutputValidator {
return segments.contains { $0.contains(".") || $0.contains("_") }
}
private static func allowedOrdinalRepairNumbers(
input: String,
output: String
) -> Set<String> {
let pattern = #"\s*(\d+)\s*[:]\s*00"#
guard let regex = try? NSRegularExpression(pattern: pattern) else { return [] }
let fullRange = NSRange(input.startIndex..<input.endIndex, in: input)
var allowed = Set<String>()
for match in regex.matches(in: input, range: fullRange) {
guard match.numberOfRanges > 1,
let ordinalRange = Range(match.range(at: 1), in: input),
let matchRange = Range(match.range, in: input) else {
continue
}
let ordinal = String(input[ordinalRange])
let prefixRange = input.startIndex..<matchRange.lowerBound
let prefix = String(input[prefixRange])
guard hasEstablishedEnumeration(prefix) else { continue }
let escaped = NSRegularExpression.escapedPattern(for: ordinal)
let arabicListPattern = #"(?m)(?:^|\n)\s*"# + escaped + #"\s*[.)]"#
let chineseOrdinal = Int(ordinal).flatMap(chineseNumeral)
let hasArabicOrdinal = output.range(
of: arabicListPattern,
options: .regularExpression
) != nil
let hasChineseOrdinal = chineseOrdinal.map {
output.contains("\($0)")
} ?? false
if hasArabicOrdinal || hasChineseOrdinal {
allowed.insert(ordinal)
allowed.insert("00")
}
}
return allowed
}
private static func hasEstablishedEnumeration(_ prefix: String) -> Bool {
prefix.range(
of: #"(?:|[]+|)"#,
options: .regularExpression
) != nil
}
private static func chineseNumeral(_ value: Int) -> String? {
let digits = ["", "", "", "", "", "", "", "", "", ""]
switch value {
case 0...9:
return digits[value]
case 10:
return ""
case 11...19:
return "" + digits[value % 10]
case 20...99:
let tens = digits[value / 10] + ""
return value % 10 == 0 ? tens : tens + digits[value % 10]
default:
return nil
}
}
private static func matches(_ pattern: String, in text: String) -> [String] {
guard let regex = try? NSRegularExpression(pattern: pattern) else { return [] }
let range = NSRange(text.startIndex..<text.endIndex, in: text)