Ускорить локальную диктовку стабильным префиксом

This commit is contained in:
2026-07-29 16:35:25 +06:00
parent 67895c6157
commit 4712c3f56e
6 changed files with 402 additions and 22 deletions
+120
View File
@@ -141,6 +141,126 @@ enum TranscriptJoiner {
}
}
struct StableTranscriptUpdate: Equatable {
let text: String
let continuesPrevious: Bool
}
struct StableTranscript {
private struct Token: Equatable {
let surface: String
let key: String
}
private var committed: [Token] = []
private var previousPending: [Token] = []
mutating func ingest(_ hypothesis: String, final: Bool = false) -> StableTranscriptUpdate? {
let current = tokenize(hypothesis)
if current.isEmpty {
guard final, !previousPending.isEmpty else { return nil }
let pending = previousPending
previousPending.removeAll()
return commit(pending)
}
let anchored = uncommittedTokens(in: current)
if !committed.isEmpty, !anchored.foundAnchor {
// A rolling window without the recent committed anchor is
// ambiguous. Keeping the previous tail is safer than duplicating
// text that is already in the target field.
if final, !previousPending.isEmpty {
let pending = previousPending
previousPending.removeAll()
return commit(pending)
}
return nil
}
let pending = anchored.tokens
if final {
guard !pending.isEmpty else { return nil }
previousPending.removeAll()
return commit(pending)
}
guard !previousPending.isEmpty else {
previousPending = pending
return nil
}
let stableCount = commonPrefixCount(previousPending, pending)
guard stableCount > 0 else {
previousPending = pending
return nil
}
let stable = Array(pending.prefix(stableCount))
previousPending = Array(pending.dropFirst(stableCount))
return commit(stable)
}
private mutating func commit(_ tokens: [Token]) -> StableTranscriptUpdate? {
guard !tokens.isEmpty else { return nil }
let continuesPrevious = committed.last.map {
$0.surface.range(
of: #"[.!?…][”"'»)]*$"#,
options: .regularExpression
) == nil
} ?? false
committed.append(contentsOf: tokens)
return StableTranscriptUpdate(
text: tokens.map(\.surface).joined(separator: " "),
continuesPrevious: continuesPrevious
)
}
private func uncommittedTokens(in current: [Token]) -> (tokens: [Token], foundAnchor: Bool) {
guard !committed.isEmpty else { return (current, true) }
let maximumAnchor = min(8, committed.count, current.count)
if maximumAnchor >= 2 {
for length in stride(from: maximumAnchor, through: 2, by: -1) {
let anchor = committed.suffix(length).map(\.key)
for start in 0...(current.count - length) {
if current[start..<(start + length)].map(\.key) == anchor {
return (Array(current.dropFirst(start + length)), true)
}
}
}
}
let lastKey = committed.last?.key
for start in 0..<min(3, current.count) where current[start].key == lastKey {
return (Array(current.dropFirst(start + 1)), true)
}
return (current, false)
}
private func commonPrefixCount(_ lhs: [Token], _ rhs: [Token]) -> Int {
var count = 0
while count < lhs.count, count < rhs.count, lhs[count].key == rhs[count].key {
count += 1
}
return count
}
private func tokenize(_ text: String) -> [Token] {
text.split(whereSeparator: \.isWhitespace).compactMap { value in
let surface = String(value)
let key = surface
.lowercased()
.replacingOccurrences(
of: #"^[^\p{L}\p{N}+#]+|[^\p{L}\p{N}+#]+$"#,
with: "",
options: .regularExpression
)
guard !key.isEmpty else { return nil }
return Token(surface: surface, key: key)
}
}
}
enum TextInserter {
static func insert(_ text: String) throws {
guard AXIsProcessTrusted() else {