166 lines
5.4 KiB
Python
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)
|