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:
43
bundles/org.openhab.voice.googletts/.classpath
Normal file
43
bundles/org.openhab.voice.googletts/.classpath
Normal 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>
|
||||
23
bundles/org.openhab.voice.googletts/.project
Normal file
23
bundles/org.openhab.voice.googletts/.project
Normal 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>
|
||||
13
bundles/org.openhab.voice.googletts/NOTICE
Normal file
13
bundles/org.openhab.voice.googletts/NOTICE
Normal 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
|
||||
60
bundles/org.openhab.voice.googletts/README.md
Normal file
60
bundles/org.openhab.voice.googletts/README.md
Normal 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 DeepMind’s groundbreaking research in WaveNet and Google’s 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.
|
||||
17
bundles/org.openhab.voice.googletts/pom.xml
Normal file
17
bundles/org.openhab.voice.googletts/pom.xml
Normal 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>
|
||||
@@ -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>
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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>
|
||||
Reference in New Issue
Block a user