Codebase as of c53e4aed26 as an initial commit for the shrunk repo

Signed-off-by: Kai Kreuzer <kai@openhab.org>
This commit is contained in:
Kai Kreuzer
2010-02-20 19:23:32 +01:00
committed by Kai Kreuzer
commit bbf1a7fd29
302 changed files with 29726 additions and 0 deletions

View File

@@ -0,0 +1,43 @@
<?xml version="1.0" encoding="UTF-8"?>
<classpath>
<classpathentry kind="src" output="target/classes" path="src/main/java">
<attributes>
<attribute name="optional" value="true"/>
<attribute name="maven.pomderived" value="true"/>
</attributes>
</classpathentry>
<classpathentry excluding="**" kind="src" output="target/classes" path="src/main/resources">
<attributes>
<attribute name="maven.pomderived" value="true"/>
</attributes>
</classpathentry>
<classpathentry kind="src" output="target/test-classes" path="src/test/java">
<attributes>
<attribute name="optional" value="true"/>
<attribute name="maven.pomderived" value="true"/>
<attribute name="test" value="true"/>
</attributes>
</classpathentry>
<classpathentry kind="con" path="org.eclipse.jdt.launching.JRE_CONTAINER/org.eclipse.jdt.internal.debug.ui.launcher.StandardVMType/JavaSE-11">
<attributes>
<attribute name="maven.pomderived" value="true"/>
</attributes>
</classpathentry>
<classpathentry kind="con" path="org.eclipse.m2e.MAVEN2_CLASSPATH_CONTAINER">
<attributes>
<attribute name="maven.pomderived" value="true"/>
</attributes>
</classpathentry>
<classpathentry kind="src" path="target/generated-sources/annotations">
<attributes>
<attribute name="optional" value="true"/>
</attributes>
</classpathentry>
<classpathentry kind="src" output="target/test-classes" path="target/generated-test-sources/test-annotations">
<attributes>
<attribute name="optional" value="true"/>
<attribute name="test" value="true"/>
</attributes>
</classpathentry>
<classpathentry kind="output" path="target/classes"/>
</classpath>

View File

@@ -0,0 +1,23 @@
<?xml version="1.0" encoding="UTF-8"?>
<projectDescription>
<name>org.openhab.voice.googletts</name>
<comment></comment>
<projects>
</projects>
<buildSpec>
<buildCommand>
<name>org.eclipse.jdt.core.javabuilder</name>
<arguments>
</arguments>
</buildCommand>
<buildCommand>
<name>org.eclipse.m2e.core.maven2Builder</name>
<arguments>
</arguments>
</buildCommand>
</buildSpec>
<natures>
<nature>org.eclipse.jdt.core.javanature</nature>
<nature>org.eclipse.m2e.core.maven2Nature</nature>
</natures>
</projectDescription>

View File

@@ -0,0 +1,13 @@
This content is produced and maintained by the openHAB project.
* Project home: https://www.openhab.org
== Declared Project Licenses
This program and the accompanying materials are made available under the terms
of the Eclipse Public License 2.0 which is available at
https://www.eclipse.org/legal/epl-2.0/.
== Source Code
https://github.com/openhab/openhab-addons

View File

@@ -0,0 +1,60 @@
# Google Cloud Text-to-Speech
Google Cloud TTS Service uses the non-free Google Cloud Text-to-Speech API to convert text or Speech Synthesis Markup Language (SSML) input into audio data of natural human speech.
It provides multiple voices, available in different languages and variants and applies DeepMinds groundbreaking research in WaveNet and Googles powerful neural networks.
The implementation caches the converted texts to reduce the load on the API and make the conversion faster.
You can find them in the `$OPENHAB_USERDATA/cache/org.openhab.voice.googletts` folder.
Be aware, that using this service may incur cost on your Google Cloud account.
You can find pricing information on the [documentation page](https://cloud.google.com/text-to-speech/#pricing-summary).
## Table of Contents
<!-- MarkdownTOC -->
* [Obtaining Credentials](#obtaining-credentials)
* [Service Configuration](#service-configuration)
* [Voice Configuration](#voice-configuration)
<!-- /MarkdownTOC -->
## Obtaining Credentials
Before you can integrate this service with your Google Cloud Text-to-Speech, you must have a Google API Console project:
* Select or create a GCP project. [link](https://console.cloud.google.com/cloud-resource-manager)
* Make sure that billing is enabled for your project. [link](https://cloud.google.com/billing/docs/how-to/modify-project)
* Enable the Cloud Text-to-Speech API. [link](https://console.cloud.google.com/apis/dashboard)
* Set up authentication:
* Go to the "APIs & Services" -> "Credentials" page in the GCP Console and your project. [link](https://console.cloud.google.com/apis/credentials)
* From the "Create credentials" drop-down list, select "OAuth client ID.
* Select application type "Other" and enter a name into the "Name" field.
* Click Create. A pop-up appears, showing your "client ID" and "client secret".
## Service Configuration
Using your favorite configuration UI (e.g. Paper UI) edit **Services / Voice / Google Cloud Text-to-Speech** settings and set:
* **Client Id** - Google Cloud Platform OAuth 2.0-Client Id.
* **Client Secret** - Google Cloud Platform OAuth 2.0-Client Secret.
* **Authorization Code** - The auth-code is a one-time code needed to retrieve the necessary access-codes from Google Cloud Platform.
**Please go to your browser ...**
[https://accounts.google.com/o/oauth2/auth?client_id={{clientId}}&redirect_uri=urn:ietf:wg:oauth:2.0:oob&scope=https://www.googleapis.com/auth/cloud-platform&response_type=code](https://accounts.google.com/o/oauth2/auth?client_id={{clientId}}&redirect_uri=urn:ietf:wg:oauth:2.0:oob&scope=https://www.googleapis.com/auth/cloud-platform&response_type=code) (replace `{{clientId}}` by your Client Id)
**... to generate an auth-code and paste it here**.
After initial authorization, this code is not needed anymore.
It is recommended to clear this configuration parameter afterwards.
* **Pitch** - The pitch of selected voice, up to 20 semitones.
* **Volume Gain** - The volume of the output between 16dB and -96dB.
* **Speaking Rate** - The speaking rate can be 4x faster or slower than the normal rate.
* **Purge Cache** - Purges the cache e.g. after testing different voice configuration parameters.
When enabled the cache is purged once.
Make sure to disable this setting again so the cache is maintained after restarts.
## Voice Configuration
Using your favorite configuration UI:
* Edit **System** settings.
* Edit **Voice** settings.
* Set **Google Cloud** as **Default Text-to-Speech**.
* Choose default voice for the setup.

View File

@@ -0,0 +1,17 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.openhab.addons.bundles</groupId>
<artifactId>org.openhab.addons.reactor.bundles</artifactId>
<version>3.0.0-SNAPSHOT</version>
</parent>
<artifactId>org.openhab.voice.googletts</artifactId>
<name>openHAB Add-ons :: Bundles :: Voice :: Google Cloud Text-to-Speech</name>
</project>

View File

@@ -0,0 +1,9 @@
<?xml version="1.0" encoding="UTF-8"?>
<features name="org.openhab.voice.googletts-${project.version}" xmlns="http://karaf.apache.org/xmlns/features/v1.4.0">
<repository>mvn:org.openhab.core.features.karaf/org.openhab.core.features.karaf.openhab-core/${ohc.version}/xml/features</repository>
<feature name="openhab-voice-googletts" description="Google Cloud Text-to-Speech" version="${project.version}">
<feature>openhab-runtime-base</feature>
<bundle start-level="80">mvn:org.openhab.addons.bundles/org.openhab.voice.googletts/${project.version}</bundle>
</feature>
</features>

View File

@@ -0,0 +1,34 @@
/**
* Copyright (c) 2010-2020 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.voice.googletts.internal;
import org.eclipse.jdt.annotation.NonNullByDefault;
/**
* Thrown, if an authentication error is given.
*
* @author Christoph Weitkamp - Initial contribution
*
*/
@NonNullByDefault
public class AuthenticationException extends Exception {
private static final long serialVersionUID = 1L;
public AuthenticationException() {
}
public AuthenticationException(String message) {
super(message);
}
}

View File

@@ -0,0 +1,458 @@
/**
* Copyright (c) 2010-2020 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.voice.googletts.internal;
import java.io.File;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
import java.math.BigInteger;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Base64;
import java.util.Collections;
import java.util.Dictionary;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.Set;
import org.eclipse.jdt.annotation.Nullable;
import org.eclipse.jetty.http.HttpHeader;
import org.eclipse.jetty.http.MimeTypes;
import org.openhab.core.audio.AudioFormat;
import org.openhab.core.auth.client.oauth2.AccessTokenResponse;
import org.openhab.core.auth.client.oauth2.OAuthClientService;
import org.openhab.core.auth.client.oauth2.OAuthException;
import org.openhab.core.auth.client.oauth2.OAuthFactory;
import org.openhab.core.auth.client.oauth2.OAuthResponseException;
import org.openhab.core.io.net.http.HttpRequestBuilder;
import org.openhab.voice.googletts.internal.protocol.AudioConfig;
import org.openhab.voice.googletts.internal.protocol.AudioEncoding;
import org.openhab.voice.googletts.internal.protocol.ListVoicesResponse;
import org.openhab.voice.googletts.internal.protocol.SsmlVoiceGender;
import org.openhab.voice.googletts.internal.protocol.SynthesisInput;
import org.openhab.voice.googletts.internal.protocol.SynthesizeSpeechRequest;
import org.openhab.voice.googletts.internal.protocol.SynthesizeSpeechResponse;
import org.openhab.voice.googletts.internal.protocol.Voice;
import org.openhab.voice.googletts.internal.protocol.VoiceSelectionParams;
import org.osgi.service.cm.Configuration;
import org.osgi.service.cm.ConfigurationAdmin;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import com.google.gson.Gson;
import com.google.gson.GsonBuilder;
/**
* Google Cloud TTS API call implementation.
*
* @author Gabor Bicskei - Initial contribution and API
*/
class GoogleCloudAPI {
private static final char EXTENSION_SEPARATOR = '.';
private static final char UNIX_SEPARATOR = '/';
private static final char WINDOWS_SEPARATOR = '\\';
private static final String BEARER = "Bearer ";
private static final String GCP_AUTH_URI = "https://accounts.google.com/o/oauth2/auth";
private static final String GCP_TOKEN_URI = "https://accounts.google.com/o/oauth2/token";
private static final String GCP_REDIRECT_URI = "urn:ietf:wg:oauth:2.0:oob";
/**
* Google Cloud Platform authorization scope
*/
private static final String GCP_SCOPE = "https://www.googleapis.com/auth/cloud-platform";
/**
* URL used for retrieving the list of available voices
*/
private static final String LIST_VOICES_URL = "https://texttospeech.googleapis.com/v1/voices";
/**
* URL used for synthesizing text to speech
*/
private static final String SYTNHESIZE_SPEECH_URL = "https://texttospeech.googleapis.com/v1/text:synthesize";
/**
* Logger
*/
private final Logger logger = LoggerFactory.getLogger(GoogleCloudAPI.class);
/**
* Supported voices and locales
*/
private final Map<Locale, Set<GoogleTTSVoice>> voices = new HashMap<>();
/**
* Cache folder
*/
private File cacheFolder;
/**
* Configuration
*/
private @Nullable GoogleTTSConfig config;
/**
* Status flag
*/
private boolean initialized;
private final Gson gson = new GsonBuilder().create();
private final ConfigurationAdmin configAdmin;
private final OAuthFactory oAuthFactory;
private @Nullable OAuthClientService oAuthService;
/**
* Constructor.
*
* @param cacheFolder Service cache folder
*/
GoogleCloudAPI(ConfigurationAdmin configAdmin, OAuthFactory oAuthFactory, File cacheFolder) {
this.configAdmin = configAdmin;
this.oAuthFactory = oAuthFactory;
this.cacheFolder = cacheFolder;
}
/**
* Configuration update.
*
* @param config New configuration.
*/
void setConfig(GoogleTTSConfig config) {
this.config = config;
String clientId = config.clientId;
String clientSecret = config.clientSecret;
if (clientId != null && !clientId.isEmpty() && clientSecret != null && !clientSecret.isEmpty()) {
try {
final OAuthClientService oAuthService = oAuthFactory.createOAuthClientService(
GoogleTTSService.SERVICE_PID, GCP_TOKEN_URI, GCP_AUTH_URI, clientId, clientSecret, GCP_SCOPE,
false);
this.oAuthService = oAuthService;
getAccessToken();
initialized = true;
initVoices();
} catch (AuthenticationException | IOException ex) {
logger.warn("Error initializing Google Cloud TTS service: {}", ex.getMessage());
oAuthService = null;
initialized = false;
voices.clear();
}
} else {
oAuthService = null;
initialized = false;
voices.clear();
}
// maintain cache
if (config.purgeCache) {
File[] files = cacheFolder.listFiles();
if (files != null && files.length > 0) {
Arrays.stream(files).forEach(File::delete);
}
logger.debug("Cache purged.");
}
}
/**
* Fetches the OAuth2 tokens from Google Cloud Platform if the auth-code is set in the configuration. If successful
* the auth-code will be removed from the configuration.
*/
private void getAccessToken() throws AuthenticationException, IOException {
String authcode = config.authcode;
if (authcode != null && !authcode.isEmpty()) {
logger.debug("Trying to get access and refresh tokens.");
try {
oAuthService.getAccessTokenResponseByAuthorizationCode(authcode, GCP_REDIRECT_URI);
} catch (OAuthException | OAuthResponseException ex) {
logger.debug("Error fetching access token: {}", ex.getMessage(), ex);
throw new AuthenticationException(
"Error fetching access token. Invalid authcode? Please generate a new one.");
}
config.authcode = null;
try {
Configuration serviceConfig = configAdmin.getConfiguration(GoogleTTSService.SERVICE_PID);
Dictionary<String, Object> configProperties = serviceConfig.getProperties();
if (configProperties != null) {
configProperties.put(GoogleTTSService.PARAM_AUTHCODE, "");
serviceConfig.update(configProperties);
}
} catch (IOException e) {
// should not happen
logger.warn(
"Failed to update configuration for Google Cloud TTS service. Please clear the 'authcode' configuration parameter manualy.");
}
}
}
private String getAuthorizationHeader() throws AuthenticationException, IOException {
final AccessTokenResponse accessTokenResponse;
try {
accessTokenResponse = oAuthService.getAccessTokenResponse();
} catch (OAuthException | OAuthResponseException ex) {
logger.debug("Error fetching access token: {}", ex.getMessage(), ex);
throw new AuthenticationException(
"Error fetching access token. Invalid authcode? Please generate a new one.");
}
if (accessTokenResponse == null || accessTokenResponse.getAccessToken() == null
|| accessTokenResponse.getAccessToken().isEmpty()) {
throw new AuthenticationException("No access token. Is this thing authorized?");
}
return BEARER + accessTokenResponse.getAccessToken();
}
/**
* Loads supported audio formats
*
* @return Set of audio formats
*/
Set<String> getSupportedAudioFormats() {
Set<String> formats = new HashSet<>();
for (AudioEncoding audioEncoding : AudioEncoding.values()) {
if (audioEncoding != AudioEncoding.AUDIO_ENCODING_UNSPECIFIED) {
formats.add(audioEncoding.toString());
}
}
return formats;
}
/**
* Supported locales.
*
* @return Set of locales
*/
Set<Locale> getSupportedLocales() {
return voices.keySet();
}
/**
* Supported voices for locale.
*
* @param locale Locale
* @return Set of voices
*/
Set<GoogleTTSVoice> getVoicesForLocale(Locale locale) {
Set<GoogleTTSVoice> localeVoices = voices.get(locale);
return localeVoices != null ? localeVoices : Collections.emptySet();
}
/**
* Google API call to load locales and voices.
*/
private void initVoices() throws AuthenticationException, IOException {
if (oAuthService != null) {
voices.clear();
for (GoogleTTSVoice voice : listVoices()) {
Locale locale = voice.getLocale();
Set<GoogleTTSVoice> localeVoices;
if (!voices.containsKey(locale)) {
localeVoices = new HashSet<>();
voices.put(locale, localeVoices);
} else {
localeVoices = voices.get(locale);
}
localeVoices.add(voice);
}
} else {
logger.error("Google client is not initialized!");
}
}
@SuppressWarnings("null")
private List<GoogleTTSVoice> listVoices() throws AuthenticationException, IOException {
HttpRequestBuilder builder = HttpRequestBuilder.getFrom(LIST_VOICES_URL)
.withHeader(HttpHeader.AUTHORIZATION.name(), getAuthorizationHeader());
ListVoicesResponse listVoicesResponse = gson.fromJson(builder.getContentAsString(), ListVoicesResponse.class);
if (listVoicesResponse == null || listVoicesResponse.getVoices() == null) {
return Collections.emptyList();
}
List<GoogleTTSVoice> result = new ArrayList<>();
for (Voice voice : listVoicesResponse.getVoices()) {
for (String languageCode : voice.getLanguageCodes()) {
result.add(new GoogleTTSVoice(Locale.forLanguageTag(languageCode), voice.getName(),
voice.getSsmlGender().name()));
}
}
return result;
}
/**
* Converts ESH audio format to Google parameters.
*
* @param codec Requested codec
* @return String array of Google audio format and the file extension to use.
*/
private String[] getFormatForCodec(String codec) {
switch (codec) {
case AudioFormat.CODEC_MP3:
return new String[] { AudioEncoding.MP3.toString(), "mp3" };
case AudioFormat.CODEC_PCM_SIGNED:
return new String[] { AudioEncoding.LINEAR16.toString(), "wav" };
default:
throw new IllegalArgumentException("Audio format " + codec + " is not yet supported");
}
}
byte[] synthesizeSpeech(String text, GoogleTTSVoice voice, String codec) {
String[] format = getFormatForCodec(codec);
String fileNameInCache = getUniqueFilenameForText(text, voice.getTechnicalName());
File audioFileInCache = new File(cacheFolder, fileNameInCache + "." + format[1]);
try {
// check if in cache
if (audioFileInCache.exists()) {
logger.debug("Audio file {} was found in cache.", audioFileInCache.getName());
return Files.readAllBytes(audioFileInCache.toPath());
}
// if not in cache, get audio data and put to cache
byte[] audio = synthesizeSpeechByGoogle(text, voice, format[0]);
if (audio != null) {
saveAudioAndTextToFile(text, audioFileInCache, audio, voice.getTechnicalName());
}
return audio;
} catch (AuthenticationException ex) {
logger.warn("Error initializing Google Cloud TTS service: {}", ex.getMessage());
oAuthService = null;
initialized = false;
voices.clear();
return null;
} catch (FileNotFoundException ex) {
logger.warn("Could not write {} to cache", audioFileInCache, ex);
return null;
} catch (IOException ex) {
logger.error("Could not write {} to cache", audioFileInCache, ex);
return null;
}
}
/**
* Create cache entry.
*
* @param text Converted text.
* @param cacheFile Cache entry file.
* @param audio Byte array of the audio.
* @param voiceName Used voice
* @throws IOException in case of file handling exceptions
*/
private void saveAudioAndTextToFile(String text, File cacheFile, byte[] audio, String voiceName)
throws IOException {
logger.debug("Caching audio file {}", cacheFile.getName());
try (FileOutputStream audioFileOutputStream = new FileOutputStream(cacheFile)) {
audioFileOutputStream.write(audio);
}
// write text to file for transparency too
// this allows to know which contents is in which audio file
String textFileName = removeExtension(cacheFile.getName()) + ".txt";
logger.debug("Caching text file {}", textFileName);
try (FileOutputStream textFileOutputStream = new FileOutputStream(new File(cacheFolder, textFileName))) {
// @formatter:off
StringBuilder sb = new StringBuilder("Config: ")
.append(config.toConfigString())
.append(",voice=")
.append(voiceName)
.append(System.lineSeparator())
.append("Text: ")
.append(text)
.append(System.lineSeparator());
// @formatter:on
textFileOutputStream.write(sb.toString().getBytes(StandardCharsets.UTF_8));
}
}
/**
* Removes the extension of a file name.
*
* @param fileName the file name to remove the extension of
* @return the filename without the extension
*/
private String removeExtension(String fileName) {
int extensionPos = fileName.lastIndexOf(EXTENSION_SEPARATOR);
int lastSeparator = Math.max(fileName.lastIndexOf(UNIX_SEPARATOR), fileName.lastIndexOf(WINDOWS_SEPARATOR));
return lastSeparator > extensionPos ? fileName : fileName.substring(0, extensionPos);
}
/**
* Call Google service to synthesize the required text
*
* @param text Text to synthesize
* @param voice Voice parameter
* @param audioFormat Audio encoding format
* @return Audio input stream or {@code null} when encoding exceptions occur
*/
@SuppressWarnings({ "null", "unused" })
private byte[] synthesizeSpeechByGoogle(String text, GoogleTTSVoice voice, String audioFormat)
throws AuthenticationException, IOException {
AudioConfig audioConfig = new AudioConfig(AudioEncoding.valueOf(audioFormat), config.pitch, config.speakingRate,
config.volumeGainDb);
SynthesisInput synthesisInput = new SynthesisInput(text);
VoiceSelectionParams voiceSelectionParams = new VoiceSelectionParams(voice.getLocale().getLanguage(),
voice.getLabel(), SsmlVoiceGender.valueOf(voice.getSsmlGender()));
SynthesizeSpeechRequest request = new SynthesizeSpeechRequest(audioConfig, synthesisInput,
voiceSelectionParams);
HttpRequestBuilder builder = HttpRequestBuilder.postTo(SYTNHESIZE_SPEECH_URL)
.withHeader(HttpHeader.AUTHORIZATION.name(), getAuthorizationHeader())
.withContent(gson.toJson(request), MimeTypes.Type.APPLICATION_JSON.name());
SynthesizeSpeechResponse synthesizeSpeechResponse = gson.fromJson(builder.getContentAsString(),
SynthesizeSpeechResponse.class);
if (synthesizeSpeechResponse == null) {
return null;
}
byte[] encodedBytes = synthesizeSpeechResponse.getAudioContent().getBytes(StandardCharsets.UTF_8);
return Base64.getDecoder().decode(encodedBytes);
}
/**
* Gets a unique filename for a give text, by creating a MD5 hash of it. It
* will be preceded by the locale.
* <p>
* Sample: "en-US_00a2653ac5f77063bc4ea2fee87318d3"
*/
private String getUniqueFilenameForText(String text, String voiceName) {
try {
MessageDigest md = MessageDigest.getInstance("MD5");
byte[] bytesOfMessage = (config.toConfigString() + text).getBytes(StandardCharsets.UTF_8);
String fileNameHash = String.format("%032x", new BigInteger(1, md.digest(bytesOfMessage)));
return voiceName + "_" + fileNameHash;
} catch (NoSuchAlgorithmException ex) {
// should not happen
logger.error("Could not create MD5 hash for '{}'", text, ex);
return null;
}
}
boolean isInitialized() {
return initialized;
}
}

View File

@@ -0,0 +1,61 @@
/**
* Copyright (c) 2010-2020 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.voice.googletts.internal;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
/**
* Voice service implementation.
*
* @author Gabor Bicskei - Initial contribution
*/
@NonNullByDefault
class GoogleTTSConfig {
/**
* Access to Google Cloud Platform
*/
public @Nullable String clientId;
public @Nullable String clientSecret;
public @Nullable String authcode;
/**
* Pitch
*/
public Double pitch = 0d;
/**
* Volume Gain
*/
public Double volumeGainDb = 0d;
/**
* Speaking Rate
*/
public Double speakingRate = 1d;
/**
* Purge cache after configuration changes.
*/
public Boolean purgeCache = Boolean.FALSE;
@Override
public String toString() {
return "GoogleTTSConfig{pitch=" + pitch + ", speakingRate=" + speakingRate + ", volumeGainDb=" + volumeGainDb
+ ", purgeCache=" + purgeCache + '}';
}
String toConfigString() {
return String.format("pitch=%f,speakingRate=%f,volumeGainDb=%f", pitch, speakingRate, volumeGainDb);
}
}

View File

@@ -0,0 +1,337 @@
/**
* Copyright (c) 2010-2020 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.voice.googletts.internal;
import static org.openhab.voice.googletts.internal.GoogleTTSService.*;
import java.io.File;
import java.util.Collections;
import java.util.HashSet;
import java.util.Locale;
import java.util.Map;
import java.util.Set;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
import org.openhab.core.OpenHAB;
import org.openhab.core.audio.AudioFormat;
import org.openhab.core.audio.AudioStream;
import org.openhab.core.audio.ByteArrayAudioStream;
import org.openhab.core.auth.client.oauth2.OAuthFactory;
import org.openhab.core.config.core.ConfigurableService;
import org.openhab.core.voice.TTSException;
import org.openhab.core.voice.TTSService;
import org.openhab.core.voice.Voice;
import org.openhab.voice.googletts.internal.protocol.AudioEncoding;
import org.osgi.framework.Constants;
import org.osgi.service.cm.ConfigurationAdmin;
import org.osgi.service.component.annotations.Activate;
import org.osgi.service.component.annotations.Component;
import org.osgi.service.component.annotations.Modified;
import org.osgi.service.component.annotations.Reference;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* Voice service implementation.
*
* @author Gabor Bicskei - Initial contribution
*/
@Component(configurationPid = SERVICE_PID, property = Constants.SERVICE_PID + "=" + SERVICE_PID)
@ConfigurableService(category = SERVICE_CATEGORY, label = SERVICE_NAME
+ " Text-to-Speech", description_uri = SERVICE_CATEGORY + ":" + SERVICE_ID)
public class GoogleTTSService implements TTSService {
/**
* Service name
*/
static final String SERVICE_NAME = "Google Cloud";
/**
* Service id
*/
static final String SERVICE_ID = "googletts";
/**
* Service category
*/
static final String SERVICE_CATEGORY = "voice";
/**
* Service pid
*/
static final String SERVICE_PID = "org.openhab." + SERVICE_CATEGORY + "." + SERVICE_ID;
/**
* Cache folder under $userdata
*/
private static final String CACHE_FOLDER_NAME = "cache";
/**
* Configuration parameters
*/
private static final String PARAM_CLIENT_ID = "clientId";
private static final String PARAM_CLIEND_SECRET = "clientSecret";
static final String PARAM_AUTHCODE = "authcode";
private static final String PARAM_PITCH = "pitch";
private static final String PARAM_SPEAKING_RATE = "speakingRate";
private static final String PARAM_VOLUME_GAIN_DB = "volumeGainDb";
private static final String PARAM_PURGE_CACHE = "purgeCache";
/**
* Logger.
*/
private final Logger logger = LoggerFactory.getLogger(GoogleTTSService.class);
/**
* Set of supported audio formats
*/
private Set<AudioFormat> audioFormats = new HashSet<>();
/**
* Google Cloud TTS API implementation
*/
private @NonNullByDefault({}) GoogleCloudAPI apiImpl;
private final ConfigurationAdmin configAdmin;
private final OAuthFactory oAuthFactory;
/**
* All voices for all supported locales
*/
private Set<Voice> allVoices = new HashSet<>();
private final GoogleTTSConfig config = new GoogleTTSConfig();
@Activate
public GoogleTTSService(final @Reference ConfigurationAdmin configAdmin,
final @Reference OAuthFactory oAuthFactory) {
this.configAdmin = configAdmin;
this.oAuthFactory = oAuthFactory;
}
/**
* DS activate, with access to ConfigAdmin
*/
@Activate
protected void activate(Map<String, Object> config) {
// create cache folder
File userData = new File(OpenHAB.getUserDataFolder());
File cacheFolder = new File(new File(userData, CACHE_FOLDER_NAME), SERVICE_PID);
if (!cacheFolder.exists()) {
cacheFolder.mkdirs();
}
logger.info("Using cache folder {}", cacheFolder.getAbsolutePath());
apiImpl = new GoogleCloudAPI(configAdmin, oAuthFactory, cacheFolder);
updateConfig(config);
}
/**
* Initializing audio formats. Google supports 3 formats:
* LINEAR16
* Uncompressed 16-bit signed little-endian samples (Linear PCM). Audio content returned as LINEAR16
* also contains a WAV header.
* MP3
* MP3 audio.
* OGG_OPUS
* Opus encoded audio wrapped in an ogg container. This is not supported by openHAB.
*
* @return Set of supported AudioFormats
*/
private Set<AudioFormat> initAudioFormats() {
logger.trace("Initializing audio formats");
Set<AudioFormat> result = new HashSet<>();
for (String format : apiImpl.getSupportedAudioFormats()) {
AudioFormat audioFormat = getAudioFormat(format);
if (audioFormat != null) {
result.add(audioFormat);
logger.trace("Audio format supported: {}", format);
} else {
logger.trace("Audio format not supported: {}", format);
}
}
return Collections.unmodifiableSet(result);
}
/**
* Loads available voices from Google API
*
* @return Set of available voices.
*/
private Set<Voice> initVoices() {
logger.trace("Initializing voices");
Set<Voice> result = new HashSet<>();
for (Locale locale : apiImpl.getSupportedLocales()) {
result.addAll(apiImpl.getVoicesForLocale(locale));
}
if (logger.isTraceEnabled()) {
for (Voice voice : result) {
logger.trace("Google Cloud TTS voice: {}", voice.getLabel());
}
}
return Collections.unmodifiableSet(result);
}
/**
* Called by the framework when the configuration was updated.
*
* @param newConfig Updated configuration
*/
@Modified
private void updateConfig(Map<String, Object> newConfig) {
logger.debug("Updating configuration");
if (newConfig != null) {
// client id
String param = newConfig.containsKey(PARAM_CLIENT_ID) ? newConfig.get(PARAM_CLIENT_ID).toString() : null;
config.clientId = param;
if (param == null) {
logger.warn("Missing client id configuration to access Google Cloud TTS API.");
}
// client secret
param = newConfig.containsKey(PARAM_CLIEND_SECRET) ? newConfig.get(PARAM_CLIEND_SECRET).toString() : null;
config.clientSecret = param;
if (param == null) {
logger.warn("Missing client secret configuration to access Google Cloud TTS API.");
}
// authcode
param = newConfig.containsKey(PARAM_AUTHCODE) ? newConfig.get(PARAM_AUTHCODE).toString() : null;
config.authcode = param;
// pitch
param = newConfig.containsKey(PARAM_PITCH) ? newConfig.get(PARAM_PITCH).toString() : null;
if (param != null) {
config.pitch = Double.parseDouble(param);
}
// speakingRate
param = newConfig.containsKey(PARAM_SPEAKING_RATE) ? newConfig.get(PARAM_SPEAKING_RATE).toString() : null;
if (param != null) {
config.speakingRate = Double.parseDouble(param);
}
// volumeGainDb
param = newConfig.containsKey(PARAM_VOLUME_GAIN_DB) ? newConfig.get(PARAM_VOLUME_GAIN_DB).toString() : null;
if (param != null) {
config.volumeGainDb = Double.parseDouble(param);
}
// purgeCache
param = newConfig.containsKey(PARAM_PURGE_CACHE) ? newConfig.get(PARAM_PURGE_CACHE).toString() : null;
if (param != null) {
config.purgeCache = Boolean.parseBoolean(param);
}
logger.trace("New configuration: {}", config.toString());
if (config.clientId != null && !config.clientId.isEmpty() && config.clientSecret != null
&& !config.clientSecret.isEmpty()) {
apiImpl.setConfig(config);
if (apiImpl.isInitialized()) {
allVoices = initVoices();
audioFormats = initAudioFormats();
}
}
} else {
logger.warn("Missing Google Cloud TTS configuration.");
}
}
@Override
public String getId() {
return SERVICE_ID;
}
@Override
public String getLabel(@Nullable Locale locale) {
return SERVICE_NAME;
}
@Override
public Set<Voice> getAvailableVoices() {
return allVoices;
}
@Override
public Set<AudioFormat> getSupportedFormats() {
return audioFormats;
}
/**
* Helper to create AudioFormat objects from Google names.
*
* @param format Google audio format.
* @return Audio format object.
*/
private @Nullable AudioFormat getAudioFormat(String format) {
Integer bitDepth = 16;
Long frequency = 44100L;
AudioEncoding encoding = AudioEncoding.valueOf(format);
switch (encoding) {
case MP3:
// we use by default: MP3, 44khz_16bit_mono with bitrate 64 kbps
return new AudioFormat(AudioFormat.CONTAINER_NONE, AudioFormat.CODEC_MP3, null, bitDepth, 64000,
frequency);
case LINEAR16:
// we use by default: wav, 44khz_16bit_mono
return new AudioFormat(AudioFormat.CONTAINER_WAVE, AudioFormat.CODEC_PCM_SIGNED, null, bitDepth, null,
frequency);
default:
logger.warn("Audio format {} is not yet supported.", format);
return null;
}
}
/**
* Checks parameters and calls the API to synthesize voice.
*
* @param text Input text.
* @param voice Selected voice.
* @param requestedFormat Format that is supported by the target sink as well.
* @return Output audio stream
* @throws TTSException in case the service is unavailable or a parameter is invalid.
*/
@Override
public AudioStream synthesize(String text, Voice voice, AudioFormat requestedFormat) throws TTSException {
logger.debug("Synthesize '{}' for voice '{}' in format {}", text, voice.getUID(), requestedFormat);
// Validate known api key
if (!apiImpl.isInitialized()) {
throw new TTSException("Missing service configuration.");
}
// Validate arguments
// trim text
String trimmedText = text.trim();
if (trimmedText.isEmpty()) {
throw new TTSException("The passed text is null or empty");
}
if (!this.allVoices.contains(voice)) {
throw new TTSException("The passed voice is unsupported");
}
boolean isAudioFormatSupported = false;
for (AudioFormat currentAudioFormat : this.audioFormats) {
if (currentAudioFormat.isCompatible(requestedFormat)) {
isAudioFormatSupported = true;
break;
}
}
if (!isAudioFormatSupported) {
throw new TTSException("The passed AudioFormat is unsupported");
}
// create the audio byte array for given text, locale, format
byte[] audio = apiImpl.synthesizeSpeech(trimmedText, (GoogleTTSVoice) voice, requestedFormat.getCodec());
if (audio == null) {
throw new TTSException("Could not read from Google Cloud TTS Service");
}
return new ByteArrayAudioStream(audio, requestedFormat);
}
}

View File

@@ -0,0 +1,102 @@
/**
* Copyright (c) 2010-2020 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.voice.googletts.internal;
import java.util.Locale;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.openhab.core.voice.Voice;
import org.openhab.voice.googletts.internal.protocol.SsmlVoiceGender;
/**
* Implementation of the Voice interface for Google Cloud TTS Service.
*
* @author Gabor Bicskei - Initial contribution
*/
@NonNullByDefault
public class GoogleTTSVoice implements Voice {
/**
* Voice locale
*/
private final Locale locale;
/**
* Voice label
*/
private final String label;
/**
* Gender
*/
private final String ssmlGender;
/**
* Constructs a Google Cloud TTS Voice for the passed data
*
* @param locale The Locale of the voice
* @param label The label of the voice
* @param ssmlGender Voice gender
*/
GoogleTTSVoice(Locale locale, String label, String ssmlGender) {
this.locale = locale;
this.ssmlGender = ssmlGender;
this.label = label;
}
/**
* Globally unique identifier of the voice.
*
* @return A String uniquely identifying the voice globally
*/
@Override
public String getUID() {
return "googletts:" + getTechnicalName();
}
/**
* Technical name of the voice.
*
* @return A String voice technical name
*/
String getTechnicalName() {
return label.replaceAll("[^a-zA-Z0-9_]", "");
}
/**
* The voice label, used for GUI's or VUI's
*
* @return The voice label, may not be globally unique
*/
@Override
public String getLabel() {
return this.label;
}
/**
* @inheritDoc
*/
@Override
public Locale getLocale() {
return this.locale;
}
/**
* The voice gender.
*
* @return {@link SsmlVoiceGender} enum name.
*/
String getSsmlGender() {
return ssmlGender;
}
}

View File

@@ -0,0 +1,112 @@
/**
* Copyright (c) 2010-2020 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.voice.googletts.internal.protocol;
/**
* The configuration of the synthesized audio.
*
* @author Wouter Born - Initial contribution
*/
public class AudioConfig {
/**
* Required. The format of the requested audio byte stream.
*/
private AudioEncoding audioEncoding;
/**
* Optional speaking pitch, in the range [-20.0, 20.0]. 20 means increase 20 semitones from the original pitch. -20
* means decrease 20 semitones from the original pitch.
*/
private Double pitch;
/**
* The synthesis sample rate (in hertz) for this audio. Optional. If this is different from the voice's natural
* sample rate, then the synthesizer will honor this request by converting to the desired sample rate (which might
* result in worse audio quality), unless the specified sample rate is not supported for the encoding chosen, in
* which case it will fail the request and return google.rpc.Code.INVALID_ARGUMENT.
*/
private Long sampleRateHertz;
/**
* Optional speaking rate/speed, in the range [0.25, 4.0]. 1.0 is the normal native speed supported by the specific
* voice. 2.0 is twice as fast, and 0.5 is half as fast. If unset(0.0), defaults to the native 1.0 speed. Any other
* values < 0.25 or > 4.0 will return an error.
*/
private Double speakingRate;
/**
* Optional volume gain (in dB) of the normal native volume supported by the specific voice, in the range [-96.0,
* 16.0]. If unset, or set to a value of 0.0 (dB), will play at normal native signal amplitude. A value of -6.0 (dB)
* will play at approximately half the amplitude of the normal native signal amplitude. A value of +6.0 (dB) will
* play at approximately twice the amplitude of the normal native signal amplitude. Strongly recommend not to exceed
* +10 (dB) as there's usually no effective increase in loudness for any value greater than that.
*/
private Double volumeGainDb;
public AudioConfig() {
}
public AudioConfig(AudioEncoding audioEncoding, Double pitch, Double speakingRate, Double volumeGainDb) {
this(audioEncoding, pitch, null, speakingRate, volumeGainDb);
}
public AudioConfig(AudioEncoding audioEncoding, Double pitch, Long sampleRateHertz, Double speakingRate,
Double volumeGainDb) {
this.audioEncoding = audioEncoding;
this.pitch = pitch;
this.sampleRateHertz = sampleRateHertz;
this.speakingRate = speakingRate;
this.volumeGainDb = volumeGainDb;
}
public AudioEncoding getAudioEncoding() {
return audioEncoding;
}
public Double getPitch() {
return pitch;
}
public Long getSampleRateHertz() {
return sampleRateHertz;
}
public Double getSpeakingRate() {
return speakingRate;
}
public Double getVolumeGainDb() {
return volumeGainDb;
}
public void setAudioEncoding(AudioEncoding audioEncoding) {
this.audioEncoding = audioEncoding;
}
public void setPitch(Double pitch) {
this.pitch = pitch;
}
public void setSampleRateHertz(Long sampleRateHertz) {
this.sampleRateHertz = sampleRateHertz;
}
public void setSpeakingRate(Double speakingRate) {
this.speakingRate = speakingRate;
}
public void setVolumeGainDb(Double volumeGainDb) {
this.volumeGainDb = volumeGainDb;
}
}

View File

@@ -0,0 +1,44 @@
/**
* Copyright (c) 2010-2020 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.voice.googletts.internal.protocol;
/**
* Configuration to set up audio encoder.
*
* @author Wouter Born - Initial contribution
*/
public enum AudioEncoding {
/**
* Not specified.
*/
AUDIO_ENCODING_UNSPECIFIED,
/**
* Uncompressed 16-bit signed little-endian samples (Linear PCM). Audio content returned as LINEAR16 also contains a
* WAV header.
*/
LINEAR16,
/**
* MP3 audio.
*/
MP3,
/**
* Opus encoded audio wrapped in an ogg container. The result will be a file which can be played natively on
* Android, and in browsers (at least Chrome and Firefox). The quality of the encoding is considerably higher than
* MP3 while using approximately the same bitrate.
*/
OGG_OPUS
}

View File

@@ -0,0 +1,40 @@
/**
* Copyright (c) 2010-2020 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.voice.googletts.internal.protocol;
import java.util.List;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
/**
* The message returned to the client by the voices.list method.
*
* @author Wouter Born - Initial contribution
*/
@NonNullByDefault
public class ListVoicesResponse {
/**
* The list of voices.
*/
private @Nullable List<Voice> voices;
public @Nullable List<Voice> getVoices() {
return voices;
}
public void setVoices(List<Voice> voices) {
this.voices = voices;
}
}

View File

@@ -0,0 +1,44 @@
/**
* Copyright (c) 2010-2020 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.voice.googletts.internal.protocol;
/**
* Gender of the voice as described in SSML voice element.
*
* @author Wouter Born - Initial contribution
*/
public enum SsmlVoiceGender {
/**
* An unspecified gender. In VoiceSelectionParams, this means that the client doesn't care which gender the selected
* voice will have. In the Voice field of ListVoicesResponse, this may mean that the voice doesn't fit any of the
* other categories in this enum, or that the gender of the voice isn't known.
*/
SSML_VOICE_GENDER_UNSPECIFIED,
/**
* A male voice.
*/
MALE,
/**
* A female voice.
*/
FEMALE,
/**
* A gender-neutral voice.
*/
NEUTRAL
}

View File

@@ -0,0 +1,60 @@
/**
* Copyright (c) 2010-2020 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.voice.googletts.internal.protocol;
/**
* Contains text input to be synthesized. Either text or ssml must be supplied. Supplying both or neither returns
* google.rpc.Code.INVALID_ARGUMENT. The input size is limited to 5000 characters.
*
* @author Wouter Born - Initial contribution
*/
public class SynthesisInput {
/**
* The SSML document to be synthesized. The SSML document must be valid and well-formed. Otherwise the RPC will fail
* and return google.rpc.Code.INVALID_ARGUMENT.
*/
private String ssml;
/**
* The raw text to be synthesized.
*/
private String text;
public SynthesisInput() {
}
public SynthesisInput(String text) {
if (text.startsWith("<speak>")) {
ssml = text;
} else {
this.text = text;
}
}
public String getSsml() {
return ssml;
}
public String getText() {
return text;
}
public void setSsml(String ssml) {
this.ssml = ssml;
}
public void setText(String text) {
this.text = text;
}
}

View File

@@ -0,0 +1,69 @@
/**
* Copyright (c) 2010-2020 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.voice.googletts.internal.protocol;
/**
* Synthesizes speech synchronously: receive results after all text input has been processed.
*
* @author Wouter Born - Initial contribution
*/
public class SynthesizeSpeechRequest {
/**
* Required. The configuration of the synthesized audio.
*/
private AudioConfig audioConfig = new AudioConfig();
/**
* Required. The Synthesizer requires either plain text or SSML as input.
*/
private SynthesisInput input = new SynthesisInput();
/**
* Required. The desired voice of the synthesized audio.
*/
private VoiceSelectionParams voice = new VoiceSelectionParams();
public SynthesizeSpeechRequest() {
}
public SynthesizeSpeechRequest(AudioConfig audioConfig, SynthesisInput input, VoiceSelectionParams voice) {
this.audioConfig = audioConfig;
this.input = input;
this.voice = voice;
}
public AudioConfig getAudioConfig() {
return audioConfig;
}
public SynthesisInput getInput() {
return input;
}
public VoiceSelectionParams getVoice() {
return voice;
}
public void setAudioConfig(AudioConfig audioConfig) {
this.audioConfig = audioConfig;
}
public void setInput(SynthesisInput input) {
this.input = input;
}
public void setVoice(VoiceSelectionParams voice) {
this.voice = voice;
}
}

View File

@@ -0,0 +1,38 @@
/**
* Copyright (c) 2010-2020 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.voice.googletts.internal.protocol;
/**
* The message returned to the client by the text.synthesize method.
*
* @author Wouter Born - Initial contribution
*/
public class SynthesizeSpeechResponse {
/**
* The audio data bytes encoded as specified in the request, including the header (For LINEAR16 audio, we include
* the WAV header). Note: as with all bytes fields, protobuffers use a pure binary representation, whereas JSON
* representations use base64.
*
* A base64-encoded string.
*/
private String audioContent;
public String getAudioContent() {
return audioContent;
}
public void setAudioContent(String audioContent) {
this.audioContent = audioContent;
}
}

View File

@@ -0,0 +1,75 @@
/**
* Copyright (c) 2010-2020 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.voice.googletts.internal.protocol;
import java.util.List;
/**
* Description of a voice supported by the TTS service.
*
* @author Wouter Born - Initial contribution
*/
public class Voice {
/**
* The languages that this voice supports, expressed as BCP-47 language tags (e.g. "en-US", "es-419", "cmn-tw").
*/
private List<String> languageCodes;
/**
* The name of this voice. Each distinct voice has a unique name.
*/
private String name;
/**
* The natural sample rate (in hertz) for this voice.
*/
private Long naturalSampleRateHertz;
/**
* The gender of this voice.
*/
private SsmlVoiceGender ssmlGender;
public List<String> getLanguageCodes() {
return languageCodes;
}
public void setLanguageCodes(List<String> languageCodes) {
this.languageCodes = languageCodes;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public Long getNaturalSampleRateHertz() {
return naturalSampleRateHertz;
}
public void setNaturalSampleRateHertz(Long naturalSampleRateHertz) {
this.naturalSampleRateHertz = naturalSampleRateHertz;
}
public SsmlVoiceGender getSsmlGender() {
return ssmlGender;
}
public void setSsmlGender(SsmlVoiceGender ssmlGender) {
this.ssmlGender = ssmlGender;
}
}

View File

@@ -0,0 +1,79 @@
/**
* Copyright (c) 2010-2020 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.voice.googletts.internal.protocol;
/**
* Description of which voice to use for a synthesis request.
*
* @author Wouter Born - Initial contribution
*/
public class VoiceSelectionParams {
/**
* The language (and optionally also the region) of the voice expressed as a BCP-47 language tag, e.g. "en-US".
* Required. This should not include a script tag (e.g. use "cmn-cn" rather than "cmn-Hant-cn"), because the script
* will be inferred from the input provided in the SynthesisInput. The TTS service will use this parameter to help
* choose an appropriate voice. Note that the TTS service may choose a voice with a slightly different language code
* than the one selected; it may substitute a different region (e.g. using en-US rather than en-CA if there isn't a
* Canadian voice available), or even a different language, e.g. using "nb" (Norwegian Bokmal) instead of "no"
* (Norwegian)".
*/
private String languageCode;
/**
* The name of the voice. Optional; if not set, the service will choose a voice based on the other parameters such
* as languageCode and gender.
*/
private String name;
/**
* The preferred gender of the voice. Optional; if not set, the service will choose a voice based on the other
* parameters such as languageCode and name. Note that this is only a preference, not requirement; if a voice of the
* appropriate gender is not available, the synthesizer should substitute a voice with a different gender rather
* than failing the request.
*/
private SsmlVoiceGender ssmlGender;
public VoiceSelectionParams() {
}
public VoiceSelectionParams(String languageCode, String name, SsmlVoiceGender ssmlGender) {
this.languageCode = languageCode;
this.name = name;
this.ssmlGender = ssmlGender;
}
public String getLanguageCode() {
return languageCode;
}
public String getName() {
return name;
}
public SsmlVoiceGender getSsmlGender() {
return ssmlGender;
}
public void setLanguageCode(String languageCode) {
this.languageCode = languageCode;
}
public void setName(String name) {
this.name = name;
}
public void setSsmlGender(SsmlVoiceGender ssmlGender) {
this.ssmlGender = ssmlGender;
}
}

View File

@@ -0,0 +1,55 @@
<?xml version="1.0" encoding="UTF-8"?>
<config-description:config-descriptions
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:config-description="https://openhab.org/schemas/config-description/v1.0.0"
xsi:schemaLocation="https://openhab.org/schemas/config-description/v1.0.0
https://openhab.org/schemas/config-description-1.0.0.xsd">
<config-description uri="voice:googletts">
<parameter-group name="authentication">
<label>Authentication</label>
<description>Authentication for connecting to Google Cloud Platform.</description>
</parameter-group>
<parameter-group name="tts">
<label>TTS Configuration</label>
<description>Parameters for Google Cloud TTS API.</description>
</parameter-group>
<parameter name="clientId" type="text" required="true" groupName="authentication">
<label>Client Id</label>
<description>Google Cloud Platform OAuth 2.0-Client Id.</description>
</parameter>
<parameter name="clientSecret" type="text" required="true" groupName="authentication">
<context>Password</context>
<label>Client Secret</label>
<description>Google Cloud Platform OAuth 2.0-Client Secret.</description>
</parameter>
<parameter name="authcode" type="text" groupName="authentication">
<label>Authorization Code</label>
<description><![CDATA[The auth-code is a one-time code needed to retrieve the necessary access-codes from Google Cloud Platform. <b>Please go to your browser ...</b> https://accounts.google.com/o/oauth2/auth?client_id={{clientId}}&redirect_uri=urn:ietf:wg:oauth:2.0:oob&scope=https://www.googleapis.com/auth/cloud-platform&response_type=code <b>... to generate an auth-code and paste it here</b>.]]></description>
</parameter>
<parameter name="pitch" type="decimal" min="-20" max="20" step="0.1" groupName="tts">
<label>Pitch</label>
<description>Customize the pitch of your selected voice, up to 20 semitones more or less than the default output.</description>
<default>0</default>
</parameter>
<parameter name="volumeGain" type="decimal" min="-96" max="16" groupName="tts">
<label>Volume Gain</label>
<description>Increase the volume of the output by up to 16db or decrease the volume up to -96db.</description>
<default>0</default>
</parameter>
<parameter name="speakingRate" type="decimal" min="0.25" max="4" groupName="tts">
<label>Speaking Rate</label>
<description>Speaking rate can be 4x faster or slower than the normal rate.</description>
<default>1</default>
</parameter>
<parameter name="purgeCache" type="boolean">
<advanced>true</advanced>
<label>Purge Cache</label>
<description>Purges the cache e.g. after testing different voice configuration parameters. When enabled the cache is
purged once. Make sure to disable this setting again so the cache is maintained after restarts.</description>
<default>false</default>
</parameter>
</config-description>
</config-description:config-descriptions>