mycroft-core/test/unittests/tts/test_tts.py

279 lines
10 KiB
Python

from pathlib import Path
from queue import Queue
import time
import unittest
from unittest import mock
import mycroft.tts
mock_phoneme = mock.Mock(name='phoneme')
mock_audio = "/tmp/mock_path"
mock_viseme = mock.Mock(name='viseme')
class MsgTypeCheck:
def __init__(self, msg_type):
self.msg_type = msg_type
def __eq__(self, other):
return self.msg_type == other.msg_type
class MockTTS(mycroft.tts.TTS):
def __init__(self, lang, config, validator, audio_ext='wav',
phonetic_spelling=True, ssml_tags=None):
super().__init__(lang, config, validator, audio_ext)
self.get_tts = mock.Mock()
self.get_tts.return_value = (mock_audio, "this is a phoneme")
self.viseme = mock.Mock()
self.viseme.return_value = mock_viseme
class MockTTSValidator(mycroft.tts.TTSValidator):
def validate(self):
pass
def validate_lang(self):
pass
def validate_connection(self):
pass
def get_tts_class(self):
return TestTTS
class TestPlaybackThread(unittest.TestCase):
def test_lifecycle(self):
playback = mycroft.tts.PlaybackThread(Queue())
playback.init(mock.Mock())
playback.start()
playback.stop()
playback.join()
@mock.patch('mycroft.tts.tts.time')
@mock.patch('mycroft.tts.tts.play_wav')
@mock.patch('mycroft.tts.tts.play_mp3')
def test_process_queue(self, mock_play_mp3, mock_play_wav, mock_time):
queue = Queue()
playback = mycroft.tts.PlaybackThread(queue)
mock_tts = mock.Mock()
playback.init(mock_tts)
playback.enclosure = mock.Mock()
playback.start()
try:
# Test wav data
wav_mock = mock.Mock(name='wav_data')
queue.put(('wav', wav_mock, None, 0, False))
time.sleep(0.3)
mock_play_wav.assert_called_with(wav_mock, environment=None)
mock_tts.bus.emit.assert_called_with(
MsgTypeCheck('recognizer_loop:audio_output_end')
)
# Test mp3 data and trigger listening True
mp3_mock = mock.Mock(name='mp3_data')
queue.put(('mp3', mp3_mock, None, 0, True))
time.sleep(0.2)
mock_play_mp3.assert_called_with(mp3_mock, environment=None)
mock_tts.bus.emit.assert_called_with(
MsgTypeCheck('mycroft.mic.listen')
)
self.assertFalse(playback.enclosure.get.called)
# Test sending visemes
mock_time.return_value = 1234
visemes = mock.Mock(name='visemes')
queue.put(('mp3', mp3_mock, visemes, 0, True))
time.sleep(0.2)
playback.enclosure.mouth_viseme.assert_called_with(1234, visemes)
finally:
# Terminate the thread
playback.stop()
playback.join()
@mock.patch('mycroft.tts.tts.PlaybackThread')
class TestTTS(unittest.TestCase):
def test_execute(self, mock_playback_thread):
tts = MockTTS("en-US", {}, MockTTSValidator(None))
bus_mock = mock.Mock()
tts.init(bus_mock)
self.assertTrue(tts.bus is bus_mock)
mycroft.tts.TTS.queue = mock.Mock()
with mock.patch('mycroft.tts.tts.open') as mock_open:
tts.cache.temporary_cache_dir = Path('/tmp/dummy')
tts.execute('Oh no, not again', 42)
tts.get_tts.assert_called_with(
'Oh no, not again',
'/tmp/dummy/8da7f22aeb16bc3846ad07b644d59359.wav'
)
mycroft.tts.TTS.queue.put.assert_called_with(
(
'wav',
mock_audio,
mock_viseme,
42,
False
)
)
def test_execute_path_returned(self, mock_playback_thread):
tts = MockTTS("en-US", {}, MockTTSValidator(None))
tts.get_tts.return_value = (Path(mock_audio), mock_viseme)
bus_mock = mock.Mock()
tts.init(bus_mock)
self.assertTrue(tts.bus is bus_mock)
mycroft.tts.TTS.queue = mock.Mock()
with mock.patch('mycroft.tts.tts.open') as mock_open:
tts.cache.temporary_cache_dir = Path('/tmp/dummy')
tts.execute('Oh no, not again', 42)
tts.get_tts.assert_called_with(
'Oh no, not again',
'/tmp/dummy/8da7f22aeb16bc3846ad07b644d59359.wav'
)
mycroft.tts.TTS.queue.put.assert_called_with(
(
'wav',
mock_audio,
mock_viseme,
42,
False
)
)
@mock.patch('mycroft.tts.tts.open')
def test_phoneme_cache(self, mock_open, _):
tts = MockTTS("en-US", {}, MockTTSValidator(None))
mock_context = mock.Mock(name='context')
mock_file = mock.MagicMock(name='file')
mock_open.return_value = mock_file
mock_file.__enter__.return_value = mock_context
phonemes = mock.Mock()
# Test save phonemes
tts.save_phonemes('abc', phonemes)
mock_context.write.assert_called_with(phonemes)
# Test load phonemes
mock_context.read.return_value = 'phonemes '
read_phonemes = tts.load_phonemes('abc')
self.assertEqual(read_phonemes, None)
with mock.patch('mycroft.tts.tts.os.path.exists') as _:
read_phonemes = tts.load_phonemes('abc')
self.assertEqual(read_phonemes, 'phonemes') # assert stripped
def test_ssml_support(self, _):
sentence = "<speak>Prosody can be used to change the way words " \
"sound. The following words are " \
"<prosody volume='x-loud'> " \
"quite a bit louder than the rest of this passage. " \
"</prosody> Each morning when I wake up, " \
"<prosody rate='x-slow'>I speak quite slowly and " \
"deliberately until I have my coffee.</prosody> I can " \
"also change the pitch of my voice using prosody. " \
"Do you like <prosody pitch='+5%'> speech with a pitch " \
"that is higher, </prosody> or <prosody pitch='-10%'> " \
"is a lower pitch preferable?</prosody></speak>"
sentence_no_ssml = "Prosody can be used to change the way " \
"words sound. The following words are quite " \
"a bit louder than the rest of this passage. " \
"Each morning when I wake up, I speak quite " \
"slowly and deliberately until I have my " \
"coffee. I can also change the pitch of my " \
"voice using prosody. Do you like speech " \
"with a pitch that is higher, or is " \
"a lower pitch preferable?"
sentence_bad_ssml = "<foo_invalid>" + sentence + \
"</foo_invalid end=whatever>"
sentence_extra_ssml = "<whispered>whisper tts<\\whispered>"
tts = MockTTS("en-US", {}, MockTTSValidator(None))
# test valid ssml
tts.ssml_tags = ['speak', 'prosody']
self.assertEqual(tts.validate_ssml(sentence), sentence)
# test extra ssml
tts.ssml_tags = ['whispered']
self.assertEqual(tts.validate_ssml(sentence_extra_ssml),
sentence_extra_ssml)
# test unsupported extra ssml
tts.ssml_tags = ['speak', 'prosody']
self.assertEqual(tts.validate_ssml(sentence_extra_ssml),
"whisper tts")
# test mixed valid / invalid ssml
tts.ssml_tags = ['speak', 'prosody']
self.assertEqual(tts.validate_ssml(sentence_bad_ssml), sentence)
# test unsupported ssml
tts.ssml_tags = []
self.assertEqual(tts.validate_ssml(sentence), sentence_no_ssml)
self.assertEqual(tts.validate_ssml(sentence_bad_ssml),
sentence_no_ssml)
self.assertEqual(mycroft.tts.TTS.remove_ssml(sentence),
sentence_no_ssml)
def test_load_spellings(self, _):
"""Check that the spelling dictionary gets loaded."""
tts = MockTTS("en-US", {}, MockTTSValidator(None))
self.assertTrue(tts.spellings != {})
def test_load_spelling_missing(self, _):
"""Test that a missing phonetic spelling dictionary counts as empty."""
tts = MockTTS("as-DF", {}, MockTTSValidator(None))
self.assertTrue(tts.spellings == {})
class TestTTSFactory(unittest.TestCase):
@mock.patch('mycroft.tts.tts.Configuration')
def test_create(self, mock_config):
config = {
'tts': {
'module': 'mock',
'mimic': {'mock': True}
}
}
mock_config.get.return_value = config
mock_mimic = mock.Mock(name='Mimic')
mock_mimic_instance = mock.Mock(name='mimic')
mock_mimic.return_value = mock_mimic_instance
mock_tts_class = mock.Mock()
mock_tts_instance = mock.Mock()
mock_tts_class.return_value = mock_tts_instance
mycroft.tts.TTSFactory.CLASSES['mimic'] = mock_mimic
mycroft.tts.TTSFactory.CLASSES['mock'] = mock_tts_class
# Check that correct module is selected
tts_instance = mycroft.tts.TTSFactory.create()
self.assertEqual(tts_instance, mock_tts_instance)
# Assert falling back to mimic if load fails
def side_effect(*args):
raise Exception
mock_tts_class.side_effect = side_effect
tts_instance = mycroft.tts.TTSFactory.create()
self.assertEqual(tts_instance, mock_mimic_instance)
# Check that mimic get's the proper config
mimic_conf = mock_mimic.call_args[0][1]
self.assertEqual(mimic_conf, config['tts']['mimic'])
# Make sure exception is raised when mimic fails
mock_mimic.side_effect = side_effect
config['tts']['module'] = 'mimic'
with self.assertRaises(Exception):
tts_instance = mycroft.tts.TTSFactory.create()