Первый релиз WhisperKey

This commit is contained in:
2026-07-29 14:46:12 +06:00
commit 5751743277
19 changed files with 1436 additions and 0 deletions
+180
View File
@@ -0,0 +1,180 @@
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)(?<![\p{L}])(?:э+|эм+|мм+)(?![\p{L}])[, ]*"#,
#"(?i)(?<![\p{L}])(?:ну\s+вот|как\s+бы|короче)(?![\p{L}])[, ]*"#,
]
for pattern in fillers {
text = text.replacingOccurrences(of: pattern, with: "", options: .regularExpression)
}
}
text = applyGlossary(to: text, glossary: glossary)
return normalize(text)
}
private static func isSilenceHallucination(_ input: String) -> 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)(?<![\p{L}\p{N}])\#(NSRegularExpression.escapedPattern(for: term))(?![\p{L}\p{N}])"#,
with: term,
options: .regularExpression
)
}
return text
}
private static func stripTrailingSilenceHallucination(from input: String) -> 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)
}
}
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)
}
}
}
}