talk2me/venv/lib/python3.11/site-packages/pyttsx3/drivers/nsss.py
2025-04-04 13:23:15 -06:00

166 lines
5.4 KiB
Python

# noinspection PyUnresolvedReferences
import objc
from AppKit import NSSpeechSynthesizer
from Foundation import *
from PyObjCTools import AppHelper
# noinspection PyProtectedMember
from PyObjCTools.AppHelper import PyObjCAppHelperRunLoopStopper
from ..voice import Voice
# noinspection PyUnresolvedReferences
class RunLoopStopper(PyObjCAppHelperRunLoopStopper):
"""
Overrides PyObjCAppHelperRunLoopStopper to terminate after endLoop.
"""
def __init__(self):
self.shouldStop = False
def init(self):
return objc.super(RunLoopStopper, self).init()
def stop(self):
self.shouldStop = True
# noinspection PyPep8Naming
def buildDriver(proxy):
return NSSpeechDriver.alloc().initWithProxy(proxy)
# noinspection PyUnresolvedReferences,PyPep8Naming,PyUnusedLocal
class NSSpeechDriver(NSObject):
def __init__(self):
self._proxy = None
self._tts = None
self._completed = False
self._current_text = ''
@objc.python_method
def initWithProxy(self, proxy):
try:
proxy_attr = objc.super(NSSpeechDriver, self).init()
except AttributeError:
proxy_attr = self
if proxy_attr:
self._proxy = proxy
self._tts = NSSpeechSynthesizer.alloc().initWithVoice_(None)
self._tts.setDelegate_(self)
# default rate
self._tts.setRate_(200)
self._completed = True
return self
def destroy(self):
self._tts.setDelegate_(None)
del self._tts
def onPumpFirst_(self, timer):
self._proxy.setBusy(False)
def startLoop(self):
# https://github.com/ronaldoussoren/pyobjc/blob/mater/pyobjc-framework-Cocoa/Lib/PyObjCTools/AppHelper.py#L243C25-L243C25 # noqa
NSTimer.scheduledTimerWithTimeInterval_target_selector_userInfo_repeats_(
0.0, self, 'onPumpFirst:', None, False
)
runLoop = NSRunLoop.currentRunLoop()
stopper = RunLoopStopper.alloc().init()
PyObjCAppHelperRunLoopStopper.addRunLoopStopper_toRunLoop_(stopper, runLoop)
while stopper.shouldRun():
nextfire = runLoop.limitDateForMode_(NSDefaultRunLoopMode)
soon = NSDate.dateWithTimeIntervalSinceNow_(0) # maxTimeout in runConsoleEventLoop
if nextfire is not None:
nextfire = soon.earlierDate_(nextfire)
if not runLoop.runMode_beforeDate_(NSDefaultRunLoopMode, nextfire):
stopper.stop()
break
PyObjCAppHelperRunLoopStopper.removeRunLoopStopperFromRunLoop_(runLoop)
@staticmethod
def endLoop():
AppHelper.stopEventLoop()
def iterate(self):
self._proxy.setBusy(False)
yield
@objc.python_method
def say(self, text):
self._proxy.setBusy(True)
self._completed = True
self._proxy.notify('started-utterance')
self._current_text = text
self._tts.startSpeakingString_(text)
def stop(self):
if self._proxy.isBusy():
self._completed = False
self._tts.stopSpeaking()
@objc.python_method
def _toVoice(self, attr):
return Voice(attr.get('VoiceIdentifier'), attr.get('VoiceName'),
[attr.get('VoiceLocaleIdentifier', attr.get('VoiceLanguage'))], attr.get('VoiceGender'),
attr.get('VoiceAge'))
@objc.python_method
def getProperty(self, name):
if name == 'voices':
return [self._toVoice(NSSpeechSynthesizer.attributesForVoice_(v))
for v in list(NSSpeechSynthesizer.availableVoices())]
elif name == 'voice':
return self._tts.voice()
elif name == 'rate':
return self._tts.rate()
elif name == 'volume':
return self._tts.volume()
elif name == "pitch":
print("Pitch adjustment not supported when using NSSS")
else:
raise KeyError('unknown property %s' % name)
@objc.python_method
def setProperty(self, name, value):
if name == 'voice':
# vol/rate gets reset, so store and restore it
vol = self._tts.volume()
rate = self._tts.rate()
self._tts.setVoice_(value)
self._tts.setRate_(rate)
self._tts.setVolume_(vol)
elif name == 'rate':
self._tts.setRate_(value)
elif name == 'volume':
self._tts.setVolume_(value)
elif name == 'pitch':
print("Pitch adjustment not supported when using NSSS")
else:
raise KeyError('unknown property %s' % name)
@objc.python_method
def save_to_file(self, text, filename):
self._proxy.setBusy(True)
self._completed = True
url = Foundation.NSURL.fileURLWithPath_(filename)
self._tts.startSpeakingString_toURL_(text, url)
def speechSynthesizer_didFinishSpeaking_(self, tts, success):
if not self._completed:
success = False
else:
success = True
self._proxy.notify('finished-utterance', completed=success)
self._proxy.setBusy(False)
def speechSynthesizer_willSpeakWord_ofString_(self, tts, rng, text):
if self._current_text:
current_word = self._current_text[rng.location:rng.location + rng.length]
else:
current_word = "Unknown"
self._proxy.notify('started-word', name=current_word, location=rng.location,
length=rng.length)