Repo // Implement "ReadingNarrationCoverage" feature.
This commit is contained in:
parent
2a694081bd
commit
c85e7142fc
|
@ -68,6 +68,8 @@ public extension AppDelegate {
|
|||
|
||||
SecurityAgentHelper.shared.timer?.fire()
|
||||
|
||||
SpeechSputnik.shared.refreshStatus() // 根據現狀條件決定是否初期化語音引擎。
|
||||
|
||||
// 一旦發現與使用者半衰模組的觀察行為有關的崩潰標記被開啟:
|
||||
// 如果有開啟 Debug 模式的話,就將既有的半衰記憶資料檔案更名+打上當時的時間戳。
|
||||
// 如果沒有開啟 Debug 模式的話,則將半衰記憶資料直接清空。
|
||||
|
|
|
@ -67,6 +67,14 @@ extension InputHandler {
|
|||
|| input.isControlHold || input.isOptionHold || input.isShiftHold || input.isCommandHold
|
||||
let confirmCombination = input.isSpace || input.isEnter
|
||||
|
||||
func narrateTheComposer(with maybeKey: String? = nil, when condition: Bool, allowDuplicates: Bool = true) {
|
||||
guard condition else { return }
|
||||
let maybeKey = maybeKey ?? composer.phonabetKeyForQuery(pronouncable: prefs.acceptLeadingIntonations)
|
||||
guard var keyToNarrate = maybeKey else { return }
|
||||
if composer.intonation == Tekkon.Phonabet(" ") { keyToNarrate.append("ˉ") }
|
||||
SpeechSputnik.shared.narrate(keyToNarrate, allowDuplicates: allowDuplicates)
|
||||
}
|
||||
|
||||
// 這裡 inputValidityCheck() 是讓注拼槽檢查 charCode 這個 UniChar 是否是合法的注音輸入。
|
||||
// 如果是的話,就將這次傳入的這個按鍵訊號塞入注拼槽內且標記為「keyConsumedByReading」。
|
||||
// 函式 composer.receiveKey() 可以既接收 String 又接收 UniChar。
|
||||
|
@ -100,6 +108,7 @@ extension InputHandler {
|
|||
// 鐵恨引擎並不具備對 Enter (CR / LF) 鍵的具體判斷能力,所以在這裡單獨處理。
|
||||
composer.receiveKey(fromString: confirmCombination ? " " : inputText)
|
||||
keyConsumedByReading = true
|
||||
narrateTheComposer(when: !overrideHappened && prefs.readingNarrationCoverage >= 2, allowDuplicates: false)
|
||||
|
||||
// 沒有調號的話,只需要 setInlineDisplayWithCursor() 且終止處理(return true)即可。
|
||||
// 有調號的話,則不需要這樣,而是轉而繼續在此之後的處理。
|
||||
|
@ -150,6 +159,8 @@ extension InputHandler {
|
|||
} else if !compositor.insertKey(readingKey) {
|
||||
delegate.callError("3CF278C9: 得檢查對應的語言模組的 hasUnigramsFor() 是否有誤判之情形。")
|
||||
return true
|
||||
} else {
|
||||
narrateTheComposer(with: readingKey, when: prefs.readingNarrationCoverage == 1)
|
||||
}
|
||||
|
||||
// 讓組字器反爬軌格。
|
||||
|
|
|
@ -128,7 +128,11 @@ import SwiftExtension
|
|||
public dynamic var autoCorrectReadingCombination: Bool
|
||||
|
||||
@AppProperty(key: UserDef.kReadingNarrationCoverage.rawValue, defaultValue: 0)
|
||||
public dynamic var readingNarrationCoverage: Int
|
||||
public dynamic var readingNarrationCoverage: Int {
|
||||
didSet {
|
||||
SpeechSputnik.shared.refreshStatus()
|
||||
}
|
||||
}
|
||||
|
||||
@AppProperty(key: UserDef.kAlsoConfirmAssociatedCandidatesByEnter.rawValue, defaultValue: false)
|
||||
public dynamic var alsoConfirmAssociatedCandidatesByEnter: Bool
|
||||
|
|
|
@ -0,0 +1,97 @@
|
|||
// (c) 2021 and onwards The vChewing Project (MIT-NTL License).
|
||||
// ====================
|
||||
// This code is released under the MIT license (SPDX-License-Identifier: MIT)
|
||||
// ... with NTL restriction stating that:
|
||||
// No trademark license is granted to use the trade names, trademarks, service
|
||||
// marks, or product names of Contributor, except as required to fulfill notice
|
||||
// requirements defined in MIT License.
|
||||
|
||||
import AppKit
|
||||
import AVFoundation
|
||||
|
||||
public class SpeechSputnik {
|
||||
public static var shared: SpeechSputnik = .init()
|
||||
private static var tags: [String] = ["ting-ting", "zh-CN", "mei-jia", "zh-TW"]
|
||||
private var currentNarrator: NSObject?
|
||||
private var currentVoice: NSObject?
|
||||
|
||||
public func refreshStatus() {
|
||||
switch PrefMgr.shared.readingNarrationCoverage {
|
||||
case 1, 2: narrate(" ") // 讓語音引擎提前預熱。
|
||||
default: clear()
|
||||
}
|
||||
}
|
||||
|
||||
private func clear() {
|
||||
currentNarrator = nil
|
||||
currentVoice = nil
|
||||
previouslyNarrated = ""
|
||||
}
|
||||
|
||||
private var narrator: NSObject? {
|
||||
get {
|
||||
currentNarrator = currentNarrator ?? generateNarrator()
|
||||
return currentNarrator
|
||||
}
|
||||
set {
|
||||
currentNarrator = newValue
|
||||
}
|
||||
}
|
||||
|
||||
private var voiceSpecified: NSObject? {
|
||||
get {
|
||||
currentVoice = currentVoice ?? generateVoice()
|
||||
return currentVoice
|
||||
}
|
||||
set {
|
||||
currentVoice = newValue
|
||||
}
|
||||
}
|
||||
|
||||
private lazy var previouslyNarrated: String = ""
|
||||
}
|
||||
|
||||
// MARK: - Generators.
|
||||
|
||||
extension SpeechSputnik {
|
||||
private func generateNarrator() -> NSObject? {
|
||||
guard #unavailable(macOS 14) else { return AVSpeechSynthesizer() }
|
||||
let voice = NSSpeechSynthesizer.availableVoices.first {
|
||||
// 這裡用 zh-CN 是因為 zh-TW 觸發的 voice 無法連讀某些注音。
|
||||
SpeechSputnik.tags.isOverlapped(with: $0.rawValue.components(separatedBy: "."))
|
||||
}
|
||||
guard let voice = voice else { return nil }
|
||||
let result = NSSpeechSynthesizer(voice: voice)
|
||||
result?.rate = 90
|
||||
return result
|
||||
}
|
||||
|
||||
private func generateVoice() -> NSObject? {
|
||||
guard #available(macOS 14, *) else { return nil }
|
||||
// 這裡用 zh-CN 是因為 zh-TW 觸發的 voice 無法連讀某些注音。
|
||||
return AVSpeechSynthesisVoice(identifier: "com.apple.voice.compact.zh-CN.Binbin")
|
||||
?? AVSpeechSynthesisVoice(identifier: "com.apple.voice.compact.zh-CN.Tingting")
|
||||
?? .speechVoices().first {
|
||||
$0.identifier.contains("Tingting") || $0.language.contains("zh-CN") || $0.language.contains("zh-TW")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Public API.
|
||||
|
||||
public extension SpeechSputnik {
|
||||
func narrate(_ text: String, allowDuplicates: Bool = true) {
|
||||
defer { previouslyNarrated = text }
|
||||
guard !(!allowDuplicates && previouslyNarrated == text) else { return }
|
||||
if #available(macOS 14, *) {
|
||||
let utterance = AVSpeechUtterance(string: text)
|
||||
utterance.voice = voiceSpecified as? AVSpeechSynthesisVoice ?? utterance.voice
|
||||
utterance.rate = 0.55
|
||||
(narrator as? AVSpeechSynthesizer)?.stopSpeaking(at: .immediate)
|
||||
(narrator as? AVSpeechSynthesizer)?.speak(utterance)
|
||||
} else {
|
||||
(narrator as? NSSpeechSynthesizer)?.stopSpeaking(at: .immediateBoundary)
|
||||
(narrator as? NSSpeechSynthesizer)?.startSpeaking(text)
|
||||
}
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue