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

Google Cloud Platform component (TTS) #23629

Merged
merged 28 commits into from Jun 6, 2019
Merged
Changes from 19 commits
Commits
Show all changes
28 commits
Select commit Hold shift + click to select a range
b8d3ad5
Added Google Cloud TTS service component feature
lufton May 2, 2019
a0eee36
Added Neutral voice gender
lufton May 2, 2019
ca9f74d
Added line break at the end of files
lufton May 2, 2019
3366917
Updated CODEOWNERS, reqirements_all.txt and .coveragerc
lufton May 2, 2019
886afc3
Fixed some ci/circleci: static-check errors
lufton May 2, 2019
bdef472
Fixed some ci/circleci: static-check error
lufton May 2, 2019
9069ca1
Fixed some ci/circleci: pylint errors
lufton May 2, 2019
398704c
Fixed some ci/circleci: pylint errors
lufton May 2, 2019
4b84ad4
* made supported_options const
lufton May 3, 2019
43d12ad
Fixed import order
lufton May 3, 2019
82b7b73
* Component renamed
lufton May 3, 2019
58fd922
Changed folder name in .coveragerc
lufton May 3, 2019
4ffb391
* Removed whitespaces in blank lines
lufton May 3, 2019
6ca61bd
Removed whitespaces in blank lines
lufton May 3, 2019
c6efab9
ci/circleci: static-check
lufton May 3, 2019
885bd53
Fixed requirements_all.txt
lufton May 3, 2019
e9c96ea
Added speed, pitch and gain parameters
lufton May 3, 2019
9916fe7
Added speed, pitch and gain as supported options
lufton May 3, 2019
d69de61
Split too long line
lufton May 3, 2019
2a1d81b
* Added profiles parameter
lufton May 5, 2019
11f4708
Fixes
lufton May 5, 2019
d116e8a
Fixes
lufton May 5, 2019
40cfc8e
Fixes
lufton May 5, 2019
0fb51af
Fixes
lufton May 5, 2019
e0274df
Fixes
lufton May 5, 2019
b3e66ad
Changed options validation
lufton May 14, 2019
0e27501
Added ToggleEntity save and restore state mechanism
lufton May 18, 2019
a80fa1d
Revert "Added ToggleEntity save and restore state mechanism"
lufton May 18, 2019
File filter...
Filter file types
Jump to…
Jump to file or symbol
Failed to load files and symbols.

Always

Just for now

@@ -220,6 +220,7 @@ omit =
homeassistant/components/goalfeed/*
homeassistant/components/gogogate2/cover.py
homeassistant/components/google/*
homeassistant/components/google_cloud/tts.py
homeassistant/components/google_maps/device_tracker.py
homeassistant/components/google_travel_time/sensor.py
homeassistant/components/googlehome/*
@@ -86,6 +86,7 @@ homeassistant/components/gearbest/* @HerrHofrat
homeassistant/components/gitter/* @fabaff
homeassistant/components/glances/* @fabaff
homeassistant/components/gntp/* @robbiet480
homeassistant/components/google_cloud/* @lufton
homeassistant/components/google_translate/* @awarecan
homeassistant/components/google_travel_time/* @robbiet480
homeassistant/components/googlehome/* @ludeeus
@@ -0,0 +1 @@
"""The google_cloud component."""
@@ -0,0 +1,12 @@
{
"domain": "google_cloud",
"name": "Google Cloud TTS",
This conversation was marked as resolved by lufton

This comment has been minimized.

Copy link
@awarecan

awarecan May 4, 2019

Contributor
Suggested change
"name": "Google Cloud TTS",
"name": "Google Cloud",

This comment has been minimized.

Copy link
@lufton

lufton May 4, 2019

Author Contributor

Maybe Google Cloud Platform?

"documentation": "https://www.home-assistant.io/components/google_cloud",
"requirements": [
"google-cloud-texttospeech==0.4.0"
],
"dependencies": [],
"codeowners": [
"@lufton"
]
}
@@ -0,0 +1,212 @@
"""Support for the Google Cloud TTS service."""
import logging
import os
import asyncio
This conversation was marked as resolved by lufton

This comment has been minimized.

Copy link
@awarecan

awarecan May 10, 2019

Contributor

Add a blank line between built-in lib and 3rd party lib

import async_timeout
import voluptuous as vol
from google.cloud import texttospeech
from homeassistant.components.tts import CONF_LANG, PLATFORM_SCHEMA, Provider
This conversation was marked as resolved by lufton

This comment has been minimized.

Copy link
@awarecan

awarecan May 10, 2019

Contributor

Add a blank line between 3rd party lib and homeassistant

import homeassistant.helpers.config_validation as cv

_LOGGER = logging.getLogger(__name__)

CONF_KEY_FILE = 'key_file'
CONF_GENDER = 'gender'
CONF_VOICE = 'voice'
CONF_ENCODING = 'encoding'
CONF_SPEED = 'speed'
CONF_PITCH = 'pitch'
CONF_GAIN = 'gain'

SUPPORTED_LANGUAGES = [
'en', 'da', 'nl', 'fr', 'de', 'it', 'ja', 'ko', 'nb',
This conversation was marked as resolved by lufton

This comment has been minimized.

Copy link
@awarecan

awarecan May 4, 2019

Contributor

This list is wrong. Google Cloud TTS support language and country variant, for example, for English, the supported language should include

['en-AU', 'en-GB', 'en-IN', 'en-US']

see: https://github.com/googleapis/google-cloud-python/blob/9b77e4ef43e5c47d7ef8fbe8a9e022883834b30a/texttospeech/google/cloud/texttospeech_v1/proto/cloud_tts.proto#L118-L129

the full list can be retrieved via https://texttospeech.googleapis.com/v1/voices?key={YOUR_API_KEY}

This comment has been minimized.

Copy link
@lufton

lufton May 4, 2019

Author Contributor

The language (and optionally also the region)

You can still specify region using voice parameter:

    voice: en-US-Wavenet-F

Also that list can change pretty often. And maybe shouldn't be hard-coded?
What do you think?

This comment has been minimized.

Copy link
@lufton

lufton May 5, 2019

Author Contributor

Listed supported languages with regions.

'pl', 'pt', 'ru', 'sk', 'es', 'sv', 'tr', 'uk',
]
DEFAULT_LANG = SUPPORTED_LANGUAGES[0]

SUPPORTED_GENDERS = [
'Neutral', 'Female', 'Male',
]
DEFAULT_GENDER = SUPPORTED_GENDERS[0]
GENDERS_DICT = {
'Neutral': texttospeech.enums.SsmlVoiceGender.NEUTRAL,
This conversation was marked as resolved by lufton

This comment has been minimized.

Copy link
@awarecan

awarecan May 4, 2019

Contributor

why not use all lowercase as key?

This comment has been minimized.

Copy link
@lufton

lufton May 4, 2019

Author Contributor

Well, I made it similar to MS TTS component:


But yeah, it might be better to leave everything lowercase. What do you think?

This comment has been minimized.

Copy link
@lufton

lufton May 5, 2019

Author Contributor

Now it is not case-sensitive.

'Female': texttospeech.enums.SsmlVoiceGender.FEMALE,
'Male': texttospeech.enums.SsmlVoiceGender.MALE,
}

SUPPORTED_ENCODINGS = [
'ogg', 'mp3', 'wav',
]
DEFAULT_ENCODING = SUPPORTED_ENCODINGS[0]
ENCODINGS_DICT = {
'ogg': texttospeech.enums.AudioEncoding.OGG_OPUS,
'mp3': texttospeech.enums.AudioEncoding.MP3,
'wav': texttospeech.enums.AudioEncoding.LINEAR16,
}

MIN_SPEED = 0.25
MAX_SPEED = 4.0
DEFAULT_SPEED = 1.0

MIN_PITCH = -20.0
MAX_PITCH = 20.0
DEFAULT_PITCH = 0

MIN_GAIN = -96.0
MAX_GAIN = 16.0
DEFAULT_GAIN = 0

SUPPORTED_OPTIONS = [
CONF_VOICE, CONF_GENDER, CONF_ENCODING, CONF_SPEED, CONF_PITCH, CONF_GAIN,
]

PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
vol.Optional(CONF_LANG, default=DEFAULT_LANG): vol.In(SUPPORTED_LANGUAGES),
vol.Optional(CONF_GENDER, default=DEFAULT_GENDER):
vol.In(SUPPORTED_GENDERS),
This conversation was marked as resolved by lufton

This comment has been minimized.

Copy link
@awarecan

awarecan May 4, 2019

Contributor
Suggested change
vol.In(SUPPORTED_GENDERS),
vol.In(GENDERS_DICT),

should this work?

This comment has been minimized.

Copy link
@lufton

lufton May 5, 2019

Author Contributor

Yes, it works, thanks!

vol.Optional(CONF_VOICE, default=''): cv.string,
vol.Optional(CONF_ENCODING, default=DEFAULT_ENCODING): cv.string,
This conversation was marked as resolved by lufton

This comment has been minimized.

Copy link
@awarecan

awarecan May 4, 2019

Contributor
Suggested change
vol.Optional(CONF_ENCODING, default=DEFAULT_ENCODING): cv.string,
vol.Optional(CONF_ENCODING, default=DEFAULT_ENCODING): vol.In(ENCODINGS_DICT),

This comment has been minimized.

Copy link
@lufton

lufton May 5, 2019

Author Contributor

Thanks!

vol.Optional(CONF_SPEED, default=DEFAULT_SPEED):
vol.Range(min=MIN_SPEED, max=MAX_SPEED),
vol.Optional(CONF_PITCH, default=DEFAULT_PITCH):
vol.Range(min=MIN_PITCH, max=MAX_PITCH),
vol.Optional(CONF_GAIN, default=DEFAULT_GAIN):
vol.Range(min=MIN_GAIN, max=MAX_GAIN),
vol.Optional(CONF_KEY_FILE, default=''): cv.string,
This conversation was marked as resolved by lufton

This comment has been minimized.

Copy link
@awarecan

awarecan May 4, 2019

Contributor

We usually don't use '' as default value, you can remove default

This comment has been minimized.

Copy link
@lufton

lufton May 5, 2019

Author Contributor

Fixed

})


async def async_get_engine(hass, config):
"""Set up Google Cloud TTS component."""
key_file = config[CONF_KEY_FILE]
This conversation was marked as resolved by lufton

This comment has been minimized.

Copy link
@awarecan

awarecan May 4, 2019

Contributor

if you removed default of CONF_KEY_FILE, this can be changed to

Suggested change
key_file = config[CONF_KEY_FILE]
key_file = config.get(CONF_KEY_FILE)

This comment has been minimized.

Copy link
@lufton

lufton May 4, 2019

Author Contributor

Yeap. Agree.

if key_file:
key_file = hass.config.path(key_file)

This comment has been minimized.

Copy link
@awarecan

awarecan May 10, 2019

Contributor

I still suggest key_file use absolute path

This comment has been minimized.

Copy link
@lufton

lufton May 11, 2019

Author Contributor

Can you please explain than how user would reference that? Is there any way to reference config folder in configuration.yaml? Something like:

tts:
  - platform: google_cloud
    key_file: "<config>/myfile.json"

Or is user even able to put file elsewhere?
I'm using Hass.io that's why I'm asking. Because I only have access to config folder.

This comment has been minimized.

Copy link
@balloob

balloob Jun 6, 2019

Member

I suggest we keep hass.config.path. It means that if we specify a relative path, it is resolved relatively to the config dir. If it's an absolute path, it remains absolute.

if not os.path.isfile(key_file):
_LOGGER.error(
"GOOGLE_APPLICATION_CREDENTIALS file doesn't exist!"
This conversation was marked as resolved by lufton

This comment has been minimized.

Copy link
@awarecan

awarecan May 4, 2019

Contributor
Suggested change
"GOOGLE_APPLICATION_CREDENTIALS file doesn't exist!"
"%s doesn't exist", key_file

This comment has been minimized.

Copy link
@lufton

lufton May 5, 2019

Author Contributor

Thanks!

)
return None

return GoogleCloudTTSProvider(
hass,
key_file,
config[CONF_LANG],
config[CONF_GENDER],
config[CONF_VOICE],
config[CONF_ENCODING],
config[CONF_SPEED],
config[CONF_PITCH],
config[CONF_GAIN]
)


class GoogleCloudTTSProvider(Provider):
"""The Google Cloud TTS API provider."""

def __init__(
self,
hass,
key_file,
lang,
gender,
voice,
encoding,
speed,
pitch,
gain
):
"""Init Google Cloud TTS service."""
self.hass = hass
self.name = 'Google Cloud TTS'
self._lang = lang
self._gender = gender
self._voice = voice
self._encoding = encoding
self._speed = speed
self._pitch = pitch
self._gain = gain

if key_file:
self._client = texttospeech \
.TextToSpeechClient.from_service_account_json(key_file)
else:
self._client = texttospeech.TextToSpeechClient()

@property
def default_language(self):
"""Return the default language."""
return self._lang

@property
def supported_languages(self):
"""Return list of supported languages."""
return SUPPORTED_LANGUAGES

@property
def supported_options(self):
"""Return a list of supported options."""
return SUPPORTED_OPTIONS

async def async_get_tts_audio(self, message, language, options=None):
"""Load TTS from google."""
try:
with async_timeout.timeout(10, loop=self.hass.loop):
_language = language or self._lang
_gender = self._gender
_voice = self._voice
_encoding = self._encoding
_speed = self._speed
_pitch = self._pitch
_gain = self._gain

if options:
if CONF_GENDER in options:
_gender = options[CONF_GENDER].lower().capitalize()
This conversation was marked as resolved by lufton

This comment has been minimized.

Copy link
@awarecan

awarecan May 4, 2019

Contributor

you might want to check all of those options are valid

This comment has been minimized.

Copy link
@lufton

lufton May 5, 2019

Author Contributor

Fixed.

if CONF_VOICE in options:
_voice = options[CONF_VOICE]
if CONF_ENCODING in options:
_encoding = options[CONF_ENCODING].lower()
if CONF_SPEED in options:
_speed = options[CONF_SPEED]
if CONF_PITCH in options:
_pitch = options[CONF_PITCH]
if CONF_GAIN in options:
_gain = options[CONF_GAIN]

synthesis_input = texttospeech.types.SynthesisInput(
text=message
) # pylint: disable=no-member

voice = texttospeech.types.VoiceSelectionParams(
language_code=_language,
ssml_gender=GENDERS_DICT.get(
_gender,
DEFAULT_GENDER
),
name=_voice
) # pylint: disable=no-member

audio_config = texttospeech.types.AudioConfig(
audio_encoding=ENCODINGS_DICT.get(
_encoding,
DEFAULT_ENCODING
),
speaking_rate=_speed,
pitch=_pitch,
volume_gain_db=_gain
) # pylint: disable=no-member

response = await self.hass.async_add_executor_job(
self._client.synthesize_speech,
synthesis_input,
voice,
audio_config
)
return _encoding, response.audio_content

except asyncio.TimeoutError as ex:
_LOGGER.error("Timeout for Google Cloud TTS call: %s", ex)
except Exception as ex: # pylint: disable=broad-except
_LOGGER.error("Error occured during Google Cloud TTS call: %s", ex)
This conversation was marked as resolved by lufton

This comment has been minimized.

Copy link
@awarecan

awarecan May 10, 2019

Contributor

use _LOGGER.exception to log stack trace.

This comment has been minimized.

Copy link
@lufton

lufton May 14, 2019

Author Contributor

Thanks


return None, None
@@ -499,6 +499,9 @@ google-api-python-client==1.6.4
# homeassistant.components.google_pubsub
google-cloud-pubsub==0.39.1

# homeassistant.components.google_cloud
google-cloud-texttospeech==0.4.0

# homeassistant.components.googlehome
googledevices==1.0.2

ProTip! Use n and p to navigate between commits in a pull request.
You can’t perform that action at this time.