Repo // Implement "ReadingNarrationCoverage" feature.

This commit is contained in:
ShikiSuen 2024-01-05 20:33:20 +08:00
parent 2a694081bd
commit c85e7142fc
4 changed files with 115 additions and 1 deletions

View File

@ -68,6 +68,8 @@ public extension AppDelegate {
SecurityAgentHelper.shared.timer?.fire()
SpeechSputnik.shared.refreshStatus() //
// 使
// Debug
// Debug

View File

@ -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)
}
//

View File

@ -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

View File

@ -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)
}
}
}