Ускорить локальную диктовку стабильным префиксом
This commit is contained in:
@@ -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 {
|
||||
|
||||
Reference in New Issue
Block a user