import AppKit import ApplicationServices enum TextCleaner { static func clean(_ input: String, removeFillers: Bool, glossary: String) -> String { var text = input .replacingOccurrences(of: "[BLANK_AUDIO]", with: "") .replacingOccurrences(of: "(тишина)", with: "") .trimmingCharacters(in: .whitespacesAndNewlines) if isSilenceHallucination(text) { return "" } text = stripTrailingSilenceHallucination(from: text) text = stripEmbeddedSubtitleCredits(from: text) if removeFillers { let fillers = [ #"(?i)(? Bool { let value = input .lowercased() .replacingOccurrences(of: #"[\s.…!?,;:\-–—]+"#, with: " ", options: .regularExpression) .trimmingCharacters(in: .whitespacesAndNewlines) let known = [ "продолжение следует", "спасибо за просмотр", "спасибо за внимание", "подписывайтесь на канал", "субтитры сделал", "редактор субтитров", ] guard !value.isEmpty else { return false } let words = value.split(separator: " ") return (value.hasPrefix("субтитры ") && words.count <= 12) || known.contains(where: { phrase in value == phrase || (value.hasPrefix(phrase + " ") && words.count <= phrase.split(separator: " ").count * 3) }) } private static func applyGlossary(to input: String, glossary: String) -> String { var text = input let aliases: [(String, String)] = [ (#"(?i)\b(?:compu\s*segro|campus\s*agro|кампус\s*агро|комп\s*агро)\b"#, "CampusAgro"), (#"(?i)\b(?:pocket\s*base|покет\s*бейс|покетбейс)\b"#, "PocketBase"), (#"(?i)\b(?:постричь|постгре|постгрес)\s*\.?\s*(?:sql|эс\s*кью\s*эль)\b"#, "PostgreSQL"), (#"(?i)\b(?:кэдди|кэди)\b"#, "Caddy"), (#"(?i)\bsvelte\s*kit\b"#, "SvelteKit"), (#"(?i)\btime\s*web\b"#, "Timeweb"), (#"(?i)\bopen\s*ai\b"#, "OpenAI"), (#"(?i)\bэлектро\s+энцефалография\b"#, "электроэнцефалография"), (#"(?i)\bдоступ\s+примечател(?:ь|ьность)?\b"#, "достопримечательность"), (#"(?i)\bэкзист(?:\s+компотенциальный)?\b"#, "экзистенциальный"), ] for (pattern, replacement) in aliases { text = text.replacingOccurrences(of: pattern, with: replacement, options: .regularExpression) } let terms = glossary.split(separator: ",").map { $0.trimmingCharacters(in: .whitespacesAndNewlines) }.filter { !$0.isEmpty } for term in terms { text = text.replacingOccurrences( of: #"(?i)(? String { input.replacingOccurrences( of: #"(?i)(?:[.!?]\s*)?(?:спасибо(?:\s+за\s+(?:просмотр|внимание))?|продолжение\s+следует|подпишись(?:\s+на\s+канал)?|субтитры\s+(?:создавал[аи]?|сделал[аи]?|подготовил[аи]?|редактировал[аи]?|редактор)\s+[\p{L}\p{N}_-]+)[.!?…\s]*$"#, with: "", options: .regularExpression ).trimmingCharacters(in: .whitespacesAndNewlines) } private static func stripEmbeddedSubtitleCredits(from input: String) -> String { input.replacingOccurrences( of: #"(?i)(?:редактор\s+субтитров|корректор)\s+[A-ZА-ЯЁ]\.?\s*[\p{L}-]+[.!?…,\s]*"#, with: "", options: .regularExpression ) } private static func normalize(_ value: String) -> String { let cleaned = value .replacingOccurrences(of: #"[ \t]{2,}"#, with: " ", options: .regularExpression) .replacingOccurrences(of: #" +([,.!?;:])"#, with: "$1", options: .regularExpression) .trimmingCharacters(in: .whitespacesAndNewlines) guard let first = cleaned.first else { return cleaned } return first.uppercased() + cleaned.dropFirst() } } enum TranscriptJoiner { static func prepare( _ input: String, continuesPrevious: Bool, continuesNext: Bool, glossary: String ) -> String { var text = input.trimmingCharacters(in: .whitespacesAndNewlines) if continuesPrevious { text = lowercaseLeadingWord(in: text, glossary: glossary) } if continuesNext { text = text.replacingOccurrences( of: #"[.!?…]+$"#, with: "", options: .regularExpression ).trimmingCharacters(in: .whitespacesAndNewlines) } return text } private static func lowercaseLeadingWord(in input: String, glossary: String) -> String { guard let range = input.range( of: #"^[\p{L}\p{N}_+#.-]+"#, options: .regularExpression ) else { return input } let word = String(input[range]) let protectedTerms = glossary .split(separator: ",") .map { $0.trimmingCharacters(in: .whitespacesAndNewlines).lowercased() } if protectedTerms.contains(word.lowercased()) { return input } let letters = word.filter(\.isLetter) guard let first = letters.first, first.isUppercase, letters.dropFirst().contains(where: \.isLowercase) else { return input } return word.prefix(1).lowercased() + input.dropFirst(word.prefix(1).count) } } 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.. 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 { throw WhisperKeyError.processFailed("Разрешите WhisperKey управлять компьютером: Системные настройки → Конфиденциальность и безопасность → Универсальный доступ.") } let pasteboard = NSPasteboard.general let oldItems = pasteboard.pasteboardItems?.compactMap { item -> [NSPasteboard.PasteboardType: Data]? in var values: [NSPasteboard.PasteboardType: Data] = [:] for type in item.types { if let data = item.data(forType: type) { values[type] = data } } return values } pasteboard.clearContents() pasteboard.setString(text, forType: .string) let source = CGEventSource(stateID: .hidSystemState) let down = CGEvent(keyboardEventSource: source, virtualKey: 9, keyDown: true) let up = CGEvent(keyboardEventSource: source, virtualKey: 9, keyDown: false) down?.flags = .maskCommand up?.flags = .maskCommand down?.post(tap: .cghidEventTap) up?.post(tap: .cghidEventTap) if let oldItems { DispatchQueue.main.asyncAfter(deadline: .now() + 0.8) { let restored = oldItems.map { values -> NSPasteboardItem in let item = NSPasteboardItem() for (type, data) in values { item.setData(data, forType: type) } return item } pasteboard.clearContents() pasteboard.writeObjects(restored) } } } }