From 11308b4c4e3c80dea50b2c7abf8b97a1beb14219 Mon Sep 17 00:00:00 2001 From: will wade Date: Sun, 27 Oct 2024 06:23:26 +0000 Subject: [PATCH 1/2] start of a sapi4 driver --- pyttsx3/drivers/sapi4.py | 120 +++++++++++++++++++++++++++++++++++++++ 1 file changed, 120 insertions(+) create mode 100644 pyttsx3/drivers/sapi4.py diff --git a/pyttsx3/drivers/sapi4.py b/pyttsx3/drivers/sapi4.py new file mode 100644 index 0000000..ceba37e --- /dev/null +++ b/pyttsx3/drivers/sapi4.py @@ -0,0 +1,120 @@ +import comtypes.client +import time +import pythoncom +import math +import os +import weakref + +from ..voice import Voice +from . import fromUtf8, toUtf8 + +SAPI4_VOICE_CLASS_ID = "{EEE78591-FE22-11D0-8BEF-0060081841DE}" + + +def buildDriver(proxy): + return SAPI4Driver(proxy) + + +class SAPI4DriverEventSink: + def __init__(self): + self._driver = None + + def setDriver(self, driver): + self._driver = driver + + def StartStream(self, stream_number, stream_position): + """Event triggered at the start of a stream.""" + if self._driver: + self._driver._proxy.notify("started-utterance") + + def EndStream(self, stream_number, stream_position): + """Event triggered at the end of a stream.""" + if self._driver: + self._driver._proxy.notify("finished-utterance") + + +class SAPI4Driver: + def __init__(self, proxy): + self._tts = comtypes.client.CreateObject(SAPI4_VOICE_CLASS_ID) + self._proxy = proxy + self._rateWpm = 200 + self._volume = 1.0 + self._event_sink = SAPI4DriverEventSink() + self._event_sink.setDriver(weakref.proxy(self)) + self._advise = comtypes.client.GetEvents( + self._tts, self._event_sink + ) # Bind events + self.setProperty("voice", self.getProperty("voice")) + + def destroy(self): + self._advise = None # Unsubscribe from events + pass + + def say(self, text): + self._proxy.setBusy(True) + self._tts.Speak(fromUtf8(toUtf8(text)), 1) # Async mode + + def stop(self): + self._tts.Stop() + + def save_to_file(self, text, filename): + stream = comtypes.client.CreateObject("SAPI4.SpFileStream") + stream.Open(filename, 3) # Mode 3: Create For Write + original_output = self._tts.AudioOutputStream + self._tts.AudioOutputStream = stream + self.say(text) + self._tts.AudioOutputStream = original_output + stream.Close() + + def _toVoice(self, attr): + return Voice(attr.Id, attr.GetDescription()) + + def _tokenFromId(self, id_): + voices = self.getProperty("voices") + for voice in voices: + if voice.id == id_: + return voice + raise ValueError(f"Unknown voice ID {id_}") + + def getProperty(self, name): + if name == "voices": + return [self._toVoice(voice) for voice in self._tts.GetVoices()] + elif name == "voice": + return self._tts.Voice.Id + elif name == "rate": + return self._rateWpm + elif name == "volume": + return self._volume + elif name == "pitch": + return "Pitch adjustment not supported in SAPI4" + else: + raise KeyError(f"Unknown property {name}") + + def setProperty(self, name, value): + if name == "voice": + self._tts.Voice = self._tokenFromId(value) + self._tts.Rate = int(math.log(self._rateWpm / 130.0, 1.0)) + elif name == "rate": + self._tts.Rate = int(math.log(value / 130.0, 1.0)) + self._rateWpm = value + elif name == "volume": + self._tts.Volume = int(value * 100) + self._volume = value + elif name == "pitch": + print("Pitch adjustment not supported for SAPI4") + else: + raise KeyError(f"Unknown property {name}") + + def startLoop(self): + self._looping = True + while self._looping: + pythoncom.PumpWaitingMessages() + time.sleep(0.05) + + def endLoop(self): + self._looping = False + + def iterate(self): + while self._looping: + pythoncom.PumpWaitingMessages() + yield From 01d325281b502063249b981d5dd4b25b4113c5af Mon Sep 17 00:00:00 2001 From: will wade Date: Mon, 28 Oct 2024 00:32:07 +0000 Subject: [PATCH 2/2] remove old six code --- pyttsx3/drivers/sapi4.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/pyttsx3/drivers/sapi4.py b/pyttsx3/drivers/sapi4.py index ceba37e..591d223 100644 --- a/pyttsx3/drivers/sapi4.py +++ b/pyttsx3/drivers/sapi4.py @@ -6,7 +6,6 @@ import weakref from ..voice import Voice -from . import fromUtf8, toUtf8 SAPI4_VOICE_CLASS_ID = "{EEE78591-FE22-11D0-8BEF-0060081841DE}" @@ -52,7 +51,7 @@ def destroy(self): def say(self, text): self._proxy.setBusy(True) - self._tts.Speak(fromUtf8(toUtf8(text)), 1) # Async mode + self._tts.Speak(str(text).encode("utf-8").decode("utf-8"), 1) # Async mode def stop(self): self._tts.Stop()