vChewing-macOS/Packages/vChewing_MainAssembly/Sources/MainAssembly/SpeechSputnik.swift

99 lines
3.1 KiB
Swift
Raw Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

// (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
import Shared
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)
}
}
}