Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Partial fix for issue #6, save output to file. Supported for OS X only. #18

Open
wants to merge 1 commit into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions docs/changelog.rst
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ Version 1.2
~~~~~~~~~~~

* Fixed voice selection to use VoiceLocaleIdentifier on OS X instead of deprecated VoiceLanguage
* Introduced speakToFile function to driver api to write speech direct to disk as audio file. Support for OS X only at this stage

Version 1.1
~~~~~~~~~~~
Expand Down
10 changes: 10 additions & 0 deletions docs/drivers.rst
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,16 @@ All drivers must implement the following factory function and driver interface.

:param text: Text to speak.
:param name: Name to associate with the utterance. Included in notifications about this utterance.

.. method:: speakToFile(text : unicode, filename : string, name : string) -> None

Immediately saves an utterance to the filepath specified. The speech must be output according to the current property values applied at the time of this invocation. Before this method returns, it must invoke :meth:`pyttsx.driver.DriverProxy.setBusy` with value :const:`True` to stall further processing of the command queue until the output completes or is interrupted.

This method must trigger one and only one `started-utterance` notification when output begins, one `started-word` notification at the start of each word in the utterance, and a `finished-utterance` notification when output completes.

:param text: Text to say.
:param filepath: Location to save the audio file to.
:param name: Name to associate with the utterance. Included in notifications about this utterance.

.. method:: setProperty(name : string, value : object) -> None

Expand Down
13 changes: 13 additions & 0 deletions pyttsx/driver.py
Original file line number Diff line number Diff line change
Expand Up @@ -146,6 +146,19 @@ def say(self, text, name):
@type name: str
'''
self._push(self._driver.say, (text,), name)

def speakToFile(self, text, filepath, name):
'''
Called by the engine to push a speakToFile command onto the queue.

@param text: Text to say
@type text: unicode
@param filepath: Location to save the audio file to
@type filepath: str
@param name: Name to associate with the utterance
@type name: str
'''
self._push(self._driver.speakToFile, (text, filepath), name)

def stop(self):
'''
Expand Down
3 changes: 3 additions & 0 deletions pyttsx/drivers/dummy.py
Original file line number Diff line number Diff line change
Expand Up @@ -139,6 +139,9 @@ def say(self, text):
pass
self._proxy.notify('finished-utterance', completed=True)
self._proxy.setBusy(False)

def speakToFile(self, text, filename):
raise NotImplementedError('Not implemented in this driver')

def stop(self):
'''
Expand Down
3 changes: 3 additions & 0 deletions pyttsx/drivers/espeak.py
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,9 @@ def say(self, text):
self._proxy.setBusy(True)
self._proxy.notify('started-utterance')
_espeak.Synth(text, flags=_espeak.ENDPAUSE)

def speakToFile(self, text, filename):
raise NotImplementedError('Not implemented in this driver')

def stop(self):
if _espeak.IsPlaying():
Expand Down
7 changes: 7 additions & 0 deletions pyttsx/drivers/nsss.py
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,13 @@ def say(self, text):
self._completed = True
self._proxy.notify('started-utterance')
self._tts.startSpeakingString_(unicode(text))

def speakToFile(self, text, filepath):
f = NSURL.fileURLWithPath_(filepath)
self._proxy.setBusy(True)
self._completed = True
self._proxy.notify('started-utterance')
self._tts.startSpeakingString_toURL_(unicode(text), f)

def stop(self):
if self._proxy.isBusy():
Expand Down
3 changes: 3 additions & 0 deletions pyttsx/drivers/sapi5.py
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,9 @@ def say(self, text):
self._proxy.notify('started-utterance')
self._speaking = True
self._tts.Speak(unicode(text), 19)

def speakToFile(self, text, filename):
raise NotImplementedError('Not implemented in this driver')

def stop(self):
if not self._speaking:
Expand Down
14 changes: 14 additions & 0 deletions pyttsx/engine.py
Original file line number Diff line number Diff line change
Expand Up @@ -112,6 +112,20 @@ def say(self, text, name=None):
@type name: str
'''
self.proxy.say(text, name)

def speakToFile(self, text, filepath, name=None):
'''
Adds an utterance for transcribing to disk to the event queue.

@param text: Text to say
@type text: unicode
@param filepath: Location to save the audio file to
@type filepath: str
@param name: Name to associate with this utterance. Included in
notifications about this utterance.
@type name: str
'''
self.proxy.speakToFile(text, filepath, name)

def stop(self):
'''
Expand Down
57 changes: 57 additions & 0 deletions tests/unit/test_base_commands.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
'''
Common base code for tests of pyttsx commands

Created on Oct 13, 2013

@author: humbled
'''
import unittest
import pyttsx

class BaseCommandTest(unittest.TestCase):

def setUp(self):
self.correct = []
for utter, name in zip(self.utters, self.names):
events = [{'type' : 'started-utterance'}]
last = 0
for word in utter.split(' '):
event = {'type' : 'started-word'}
event['length'] = len(word)
event['location'] = last
events.append(event)
last += len(word) + 1
events.append({'type' : 'finished-utterance', 'completed' : True})
for event in events:
event['name'] = name
self.correct.append(events)

self.events = []
self.engine = pyttsx.init(debug=False)
self.engine.connect('started-utterance', self._onUtterStart)
self.engine.connect('started-word', self._onUtterWord)
self.engine.connect('finished-utterance', self._onUtterEnd)
self.engine.connect('error', self._onUtterError)

def tearDown(self):
del self.engine

def _onUtterStart(self, **kwargs):
event = {'type' : 'started-utterance'}
event.update(kwargs)
self.events.append(event)

def _onUtterWord(self, **kwargs):
event = {'type' : 'started-word'}
event.update(kwargs)
self.events.append(event)

def _onUtterEnd(self, **kwargs):
event = {'type' : 'finished-utterance'}
event.update(kwargs)
self.events.append(event)

def _onUtterError(self, **kwargs):
event = {'type' : 'error'}
event.update(kwargs)
self.events.append(event)
50 changes: 3 additions & 47 deletions tests/unit/test_say.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,57 +20,13 @@
import pyttsx
import itertools

class TestSay(unittest.TestCase):
from test_base_commands import BaseCommandTest

class TestSay(BaseCommandTest):
utters = ['This is the first utterance',
'The second is an utterance as well']
names = ['utter1', 'utter2']

def setUp(self):
self.correct = []
for utter, name in zip(self.utters, self.names):
events = [{'type' : 'started-utterance'}]
last = 0
for word in utter.split(' '):
event = {'type' : 'started-word'}
event['length'] = len(word)
event['location'] = last
events.append(event)
last += len(word) + 1
events.append({'type' : 'finished-utterance', 'completed' : True})
for event in events:
event['name'] = name
self.correct.append(events)

self.events = []
self.engine = pyttsx.init(debug=False)
self.engine.connect('started-utterance', self._onUtterStart)
self.engine.connect('started-word', self._onUtterWord)
self.engine.connect('finished-utterance', self._onUtterEnd)
self.engine.connect('error', self._onUtterError)

def tearDown(self):
del self.engine

def _onUtterStart(self, **kwargs):
event = {'type' : 'started-utterance'}
event.update(kwargs)
self.events.append(event)

def _onUtterWord(self, **kwargs):
event = {'type' : 'started-word'}
event.update(kwargs)
self.events.append(event)

def _onUtterEnd(self, **kwargs):
event = {'type' : 'finished-utterance'}
event.update(kwargs)
self.events.append(event)

def _onUtterError(self, **kwargs):
event = {'type' : 'error'}
event.update(kwargs)
self.events.append(event)

def testSay(self):
self.engine.say(self.utters[0], self.names[0])
self.engine.runAndWait()
Expand Down
51 changes: 51 additions & 0 deletions tests/unit/test_speakToFile.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
'''
Created on Oct 13, 2013

@author: humbled
'''
import unittest
import test_setup
import itertools

from test_base_commands import BaseCommandTest
import tempfile
import os
import logging

class TestSpeakToFile(BaseCommandTest):
utters = ['This is the first utterance',
'The second is an utterance as well']
# Filenames to save to in tests that will get saved in tmp dir (full paths at self.filepaths)
_filenames = ['test_one.wav', 'test_two.wav']
names = ['utter1', 'utter2']

def setUp(self):
self._tmpdir = tempfile.mkdtemp(prefix="pyttsx_test")
self.filepaths = [os.path.join(self._tmpdir, f) for f in self._filenames]
super(TestSpeakToFile, self).setUp()

def testSpeakToFile(self):
filepath = self.filepaths[0]
self.engine.speakToFile(self.utters[0], filepath, self.names[0])
logging.info("test attempting to save speech file to: %s" % filepath)
self.engine.runAndWait()

# number of events check
self.assert_(len(self.events) == len(self.correct[0]))
# event data check
for cevent, tevent in zip(self.correct[0], self.events):
self.assertEqual(cevent, tevent)

# Now check file is there
self.assert_(os.path.exists(filepath))
# And is a file
self.assert_(os.path.isfile(filepath))
self.assert_(os.path.getsize(filepath) > 0)

def suite():
suite = unittest.TestLoader().loadTestsFromTestCase(TestSpeakToFile)
return suite

if __name__ == '__main__':
logging.basicConfig(level = logging.INFO)
unittest.TextTestRunner(verbosity=2).run(suite())