added migrated 2.x add-ons
Signed-off-by: Kai Kreuzer <kai@openhab.org>
This commit is contained in:
32
bundles/org.openhab.voice.pollytts/.classpath
Normal file
32
bundles/org.openhab.voice.pollytts/.classpath
Normal file
@@ -0,0 +1,32 @@
|
||||
<?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="output" path="target/classes"/>
|
||||
</classpath>
|
||||
23
bundles/org.openhab.voice.pollytts/.project
Normal file
23
bundles/org.openhab.voice.pollytts/.project
Normal file
@@ -0,0 +1,23 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<projectDescription>
|
||||
<name>org.openhab.voice.pollytts</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>
|
||||
50
bundles/org.openhab.voice.pollytts/NOTICE
Normal file
50
bundles/org.openhab.voice.pollytts/NOTICE
Normal file
@@ -0,0 +1,50 @@
|
||||
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
|
||||
|
||||
== Third-party Content
|
||||
|
||||
aws-java-sdk-core
|
||||
* License: Apache 2.0 License
|
||||
* Project: https://aws.amazon.com/sdk-for-java/
|
||||
* Source: https://github.com/aws/aws-sdk-java/tree/master/aws-java-sdk-core
|
||||
|
||||
aws-java-sdk-polly
|
||||
* License: Apache 2.0 License
|
||||
* Project: https://aws.amazon.com/sdk-for-java/
|
||||
* Source: https://github.com/aws/aws-sdk-java/tree/master/aws-java-sdk-polly
|
||||
|
||||
commons-logging
|
||||
* License: Apache 2.0 License
|
||||
* Project: https://commons.apache.org/proper/commons-logging/
|
||||
* Source: https://gitbox.apache.org/repos/asf/?p=commons-logging.git
|
||||
|
||||
httpclient
|
||||
* License: Apache 2.0 License
|
||||
* Project: https://hc.apache.org/httpcomponents-client-ga
|
||||
* Source: https://hc.apache.org/httpcomponents-client-ga
|
||||
|
||||
httpcore
|
||||
* License: Apache 2.0 License
|
||||
* Project: http://hc.apache.org/httpcomponents-core-ga
|
||||
* Source: http://hc.apache.org/httpcomponents-core-ga
|
||||
|
||||
jackson
|
||||
* License: Apache 2.0 license
|
||||
* Project: https://github.com/FasterXML/jackson
|
||||
* Source: https://github.com/FasterXML/jackson
|
||||
|
||||
joda-time
|
||||
* License: Apache 2.0 License
|
||||
* Project: https://www.joda.org/joda-time/
|
||||
* Source: https://github.com/JodaOrg/joda-time
|
||||
69
bundles/org.openhab.voice.pollytts/README.md
Normal file
69
bundles/org.openhab.voice.pollytts/README.md
Normal file
@@ -0,0 +1,69 @@
|
||||
# Polly Text-to-Speech
|
||||
|
||||
PollyTTS is a voice service utilizing the Internet based text-to-speech (TTS) service [Amazon Polly](https://aws.amazon.com/polly/).
|
||||
The service generates speech from both plain text input and text with Speech Synthesis Markup Language (SSML) [tags](https://docs.aws.amazon.com/polly/latest/dg/supported-ssml.html).
|
||||
There are servers set in various geographic [regions](https://docs.aws.amazon.com/general/latest/gr/rande.html#pol_region).
|
||||
API keys provided by Amazon are required to get access to the service.
|
||||
Amazon Polly has a wide selection of [voices and languages](https://aws.amazon.com/polly/features/#Wide_Selection_of_Voices_and_Languages).
|
||||
Be aware, that using this service may incur costs on your AWS account.
|
||||
You can find pricing information on the [documentation page](https://aws.amazon.com/polly/pricing/).
|
||||
|
||||
## Obtaining Credentials
|
||||
|
||||
* Sign up for Amazon Web Services (AWS). [link](https://portal.aws.amazon.com/billing/signup)
|
||||
|
||||
When you sign up for AWS, your account is automatically signed up for all services in AWS, including Amazon Polly.
|
||||
|
||||
* Create an IAM User. [link](https://docs.aws.amazon.com/polly/latest/dg/setting-up.html)
|
||||
|
||||
Services in AWS, such as Amazon Polly, require that you provide credentials when you access them so that the service can determine whether you have permissions to access the resources owned by that service.
|
||||
Within the AWS console, you can create access keys for your AWS account to access the Polly API.
|
||||
|
||||
To use the service you will need the **access key**, **secret key** and **server region**.
|
||||
|
||||
## Service Configuration
|
||||
|
||||
Using your favorite configuration UI (e.g. Paper UI) edit **Services/Voice/Polly Text-to-Speech** settings and set:
|
||||
|
||||
* **Access Key** - The AWS credentials access key (required).
|
||||
* **Secret Key** - The AWS credentials secret key (required).
|
||||
* **Service Region** - The service region used for accessing Polly (required). To reduce latency select the region closest to you. E.g. "eu-west-1" (see [regions](https://docs.aws.amazon.com/general/latest/gr/rande.html#pol_region))
|
||||
|
||||
The PollyTTS service caches audio files from previous requests.
|
||||
This reduces traffic, improves performance, reduces the number of requests and provides offline functionality.
|
||||
|
||||
* **Cache Expiration** - Cache expiration in days.
|
||||
|
||||
When cache files are used their time stamps are updated, unused files are purged if their time stamp exceeds the specified age.
|
||||
The default value of 0 disables this functionality.
|
||||
A value of 365 removes files that have been unused for a year.
|
||||
|
||||
* **Audio Format** - Allows for overriding the system default audio format.
|
||||
|
||||
Use "default" to select the system default audio format.
|
||||
The default audio format can be overriden with the value "mp3" or "ogg".
|
||||
|
||||
|
||||
### Service Configuration via Text files
|
||||
|
||||
Create a new file in `$OPENHAB_ROOT/conf/services` named `pollytts.cfg`
|
||||
|
||||
It's contents should look similar to:
|
||||
|
||||
```
|
||||
org.openhab.pollytts:accessKey=ACCESS_KEY_ID
|
||||
org.openhab.pollytts:secretKey=SECRET_KEY
|
||||
org.openhab.pollytts:serviceRegion=SERVICE_REGION
|
||||
org.openhab.pollytts:cacheExpiration=EXPIRATION_IN_DAYS
|
||||
```
|
||||
|
||||
These have the same meanings as described in the **Service Configuration** block above.
|
||||
|
||||
## Rule Examples
|
||||
|
||||
```
|
||||
say("Hello there")
|
||||
say("Hello there", "pollytts:Joanne", "enhancedjavasound")
|
||||
say("" + item.state, "pollytts:Joey", "enhancedjavasound")
|
||||
say("<speak>Children, come to dinner <prosody volume='x-loud'>Right now!</prosody></speak>")
|
||||
```
|
||||
32
bundles/org.openhab.voice.pollytts/pom.xml
Normal file
32
bundles/org.openhab.voice.pollytts/pom.xml
Normal file
@@ -0,0 +1,32 @@
|
||||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<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.pollytts</artifactId>
|
||||
|
||||
<name>openHAB Add-ons :: Bundles :: Voice :: Polly Text-to-Speech</name>
|
||||
|
||||
<dependencies>
|
||||
<dependency>
|
||||
<groupId>org.openhab.osgiify</groupId>
|
||||
<artifactId>com.amazonaws.aws-java-sdk-core</artifactId>
|
||||
<version>1.11.490</version>
|
||||
<scope>compile</scope>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.openhab.osgiify</groupId>
|
||||
<artifactId>com.amazonaws.aws-java-sdk-polly</artifactId>
|
||||
<version>1.11.490</version>
|
||||
<scope>compile</scope>
|
||||
</dependency>
|
||||
</dependencies>
|
||||
|
||||
</project>
|
||||
@@ -0,0 +1,17 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<features name="org.openhab.voice.pollytts-${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-pollytts" description="Polly Text-to-Speech" version="${project.version}">
|
||||
<feature>openhab-runtime-base</feature>
|
||||
<feature dependency="true">openhab.tp-jackson</feature>
|
||||
<bundle dependency="true">mvn:com.fasterxml.jackson.dataformat/jackson-dataformat-cbor/2.9.9</bundle>
|
||||
<bundle dependency="true">mvn:org.apache.httpcomponents/httpcore-osgi/4.4.9</bundle>
|
||||
<bundle dependency="true">mvn:org.apache.httpcomponents/httpclient-osgi/4.5.5</bundle>
|
||||
<bundle dependency="true">mvn:org.openhab.osgiify/com.amazonaws.aws-java-sdk-core/1.11.490</bundle>
|
||||
<bundle dependency="true">mvn:org.openhab.osgiify/com.amazonaws.aws-java-sdk-polly/1.11.490</bundle>
|
||||
<bundle dependency="true">mvn:commons-logging/commons-logging/1.2</bundle>
|
||||
<bundle dependency="true">mvn:joda-time/joda-time/2.8.1</bundle>
|
||||
<bundle start-level="80">mvn:org.openhab.addons.bundles/org.openhab.voice.pollytts/${project.version}</bundle>
|
||||
</feature>
|
||||
</features>
|
||||
@@ -0,0 +1,37 @@
|
||||
/**
|
||||
* 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.pollytts.internal;
|
||||
|
||||
import java.io.File;
|
||||
|
||||
import org.openhab.core.audio.AudioException;
|
||||
import org.openhab.core.audio.AudioFormat;
|
||||
import org.openhab.core.audio.AudioStream;
|
||||
import org.openhab.core.audio.FileAudioStream;
|
||||
|
||||
/**
|
||||
* Implementation of the {@link AudioStream} interface for the {@link PollyTTSService}.
|
||||
* It simply uses a {@link FileAudioStream} which is doing all the necessary work,
|
||||
* e.g. supporting MP3 and WAV files with fixed stream length.
|
||||
*
|
||||
* @author Robert Hillman - Initial contribution
|
||||
*/
|
||||
class PollyTTSAudioStream extends FileAudioStream {
|
||||
|
||||
/**
|
||||
* main method the passes the audio file to system audio services
|
||||
*/
|
||||
public PollyTTSAudioStream(File audioFile, AudioFormat format) throws AudioException {
|
||||
super(audioFile, format);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,239 @@
|
||||
/**
|
||||
* 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.pollytts.internal;
|
||||
|
||||
import static java.util.stream.Collectors.toSet;
|
||||
import static org.openhab.core.audio.AudioFormat.*;
|
||||
import static org.openhab.voice.pollytts.internal.PollyTTSService.*;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
import java.util.Collections;
|
||||
import java.util.HashSet;
|
||||
import java.util.Locale;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
|
||||
import org.openhab.core.audio.AudioException;
|
||||
import org.openhab.core.audio.AudioFormat;
|
||||
import org.openhab.core.audio.AudioStream;
|
||||
import org.openhab.core.config.core.ConfigConstants;
|
||||
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.pollytts.internal.cloudapi.CachedPollyTTSCloudImpl;
|
||||
import org.openhab.voice.pollytts.internal.cloudapi.PollyTTSConfig;
|
||||
import org.osgi.framework.Constants;
|
||||
import org.osgi.service.component.annotations.Activate;
|
||||
import org.osgi.service.component.annotations.Component;
|
||||
import org.osgi.service.component.annotations.Modified;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
/**
|
||||
* This is a TTS service implementation for using Polly Text-to-Speech.
|
||||
*
|
||||
* @author Robert Hillman - Initial contribution
|
||||
*/
|
||||
@Component(configurationPid = SERVICE_PID, property = { Constants.SERVICE_PID + "=" + SERVICE_PID,
|
||||
ConfigurableService.SERVICE_PROPERTY_LABEL + "=" + SERVICE_NAME + " Text-to-Speech",
|
||||
ConfigurableService.SERVICE_PROPERTY_DESCRIPTION_URI + "=" + SERVICE_CATEGORY + ":" + SERVICE_ID,
|
||||
ConfigurableService.SERVICE_PROPERTY_CATEGORY + "=" + SERVICE_CATEGORY })
|
||||
public class PollyTTSService implements TTSService {
|
||||
|
||||
/**
|
||||
* Service name
|
||||
*/
|
||||
static final String SERVICE_NAME = "Polly";
|
||||
|
||||
/**
|
||||
* Service id
|
||||
*/
|
||||
static final String SERVICE_ID = "pollytts";
|
||||
|
||||
/**
|
||||
* 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";
|
||||
|
||||
private final Logger logger = LoggerFactory.getLogger(PollyTTSService.class);
|
||||
|
||||
/**
|
||||
* We need the cached implementation to allow for FixedLengthAudioStream.
|
||||
*/
|
||||
private CachedPollyTTSCloudImpl pollyTTSImpl;
|
||||
|
||||
/**
|
||||
* Set of supported voices
|
||||
*/
|
||||
private final Set<Voice> voices = new HashSet<>();
|
||||
|
||||
/**
|
||||
* Set of supported audio formats
|
||||
*/
|
||||
private final Set<AudioFormat> audioFormats = new HashSet<>();
|
||||
|
||||
private PollyTTSConfig pollyTTSConfig;
|
||||
|
||||
@Activate
|
||||
protected void activate(Map<String, Object> config) {
|
||||
modified(config);
|
||||
}
|
||||
|
||||
@Modified
|
||||
protected void modified(Map<String, Object> config) {
|
||||
try {
|
||||
pollyTTSConfig = new PollyTTSConfig(config);
|
||||
logger.debug("Using configuration {}", config);
|
||||
|
||||
// create cache folder
|
||||
File cacheFolder = new File(new File(ConfigConstants.getUserDataFolder(), CACHE_FOLDER_NAME), SERVICE_PID);
|
||||
if (!cacheFolder.exists()) {
|
||||
cacheFolder.mkdirs();
|
||||
}
|
||||
logger.info("Using cache folder {}", cacheFolder.getAbsolutePath());
|
||||
|
||||
pollyTTSImpl = new CachedPollyTTSCloudImpl(pollyTTSConfig, cacheFolder);
|
||||
|
||||
audioFormats.clear();
|
||||
audioFormats.addAll(initAudioFormats());
|
||||
|
||||
voices.clear();
|
||||
voices.addAll(initVoices());
|
||||
|
||||
logger.debug("PollyTTS service initialized");
|
||||
} catch (IllegalArgumentException e) {
|
||||
logger.warn("Failed to initialize PollyTTS: {}", e.getMessage());
|
||||
} catch (Exception e) {
|
||||
logger.warn("Failed to initialize PollyTTS", e);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public Set<Voice> getAvailableVoices() {
|
||||
return Collections.unmodifiableSet(voices);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Set<AudioFormat> getSupportedFormats() {
|
||||
return Collections.unmodifiableSet(audioFormats);
|
||||
}
|
||||
|
||||
/**
|
||||
* obtain audio stream from cache or Amazon Polly service and return it to play the audio
|
||||
*/
|
||||
@Override
|
||||
public AudioStream synthesize(String inText, Voice voice, AudioFormat requestedFormat) throws TTSException {
|
||||
logger.debug("Synthesize '{}' in format {}", inText, requestedFormat);
|
||||
logger.debug("voice UID: '{}' voice label: '{}' voice Locale: {}", voice.getUID(), voice.getLabel(),
|
||||
voice.getLocale());
|
||||
|
||||
// Validate arguments
|
||||
// trim text
|
||||
String text = inText.trim();
|
||||
if (text == null || text.isEmpty()) {
|
||||
throw new TTSException("The passed text is null or empty");
|
||||
}
|
||||
if (!voices.contains(voice)) {
|
||||
throw new TTSException("The passed voice is unsupported");
|
||||
}
|
||||
boolean isAudioFormatSupported = audioFormats.stream()
|
||||
.filter(audioFormat -> audioFormat.isCompatible(requestedFormat)).findAny().isPresent();
|
||||
|
||||
if (!isAudioFormatSupported) {
|
||||
throw new TTSException("The passed AudioFormat is unsupported");
|
||||
}
|
||||
|
||||
// now create the input stream for given text, locale, format. There is
|
||||
// only a default voice
|
||||
try {
|
||||
File cacheAudioFile = pollyTTSImpl.getTextToSpeechAsFile(text, voice.getLabel(),
|
||||
getApiAudioFormat(requestedFormat));
|
||||
if (cacheAudioFile == null) {
|
||||
throw new TTSException("Could not read from PollyTTS service");
|
||||
}
|
||||
logger.debug("Audio Stream for '{}' in format {}", text, requestedFormat);
|
||||
AudioStream audioStream = new PollyTTSAudioStream(cacheAudioFile, requestedFormat);
|
||||
return audioStream;
|
||||
} catch (AudioException ex) {
|
||||
throw new TTSException("Could not create AudioStream: " + ex.getMessage(), ex);
|
||||
} catch (IOException ex) {
|
||||
throw new TTSException("Could not read from PollyTTS service: " + ex.getMessage(), ex);
|
||||
}
|
||||
}
|
||||
|
||||
private Set<Voice> initVoices() {
|
||||
// @formatter:off
|
||||
return pollyTTSImpl.getAvailableLocales().stream()
|
||||
.flatMap(locale ->
|
||||
pollyTTSImpl.getAvailableVoices(locale).stream()
|
||||
.map(label -> new PollyTTSVoice(locale, label)))
|
||||
.collect(toSet());
|
||||
// @formatter:on
|
||||
}
|
||||
|
||||
private Set<AudioFormat> initAudioFormats() {
|
||||
// @formatter:off
|
||||
return pollyTTSImpl.getAvailableAudioFormats().stream()
|
||||
.map(this::getAudioFormat)
|
||||
.collect(toSet());
|
||||
// @formatter:on
|
||||
}
|
||||
|
||||
private AudioFormat getAudioFormat(String apiFormat) {
|
||||
if (CODEC_MP3.equals(apiFormat)) {
|
||||
// use by default: MP3, 22khz_16bit_mono with bitrate 64 kbps
|
||||
return new AudioFormat(CONTAINER_NONE, CODEC_MP3, null, 16, 64000, 22050L);
|
||||
} else if (CONTAINER_OGG.equals(apiFormat)) {
|
||||
// use by default: OGG, 22khz_16bit_mono
|
||||
return new AudioFormat(CONTAINER_OGG, CODEC_VORBIS, null, 16, null, 22050L);
|
||||
} else {
|
||||
throw new IllegalArgumentException("Audio format " + apiFormat + " not yet supported");
|
||||
}
|
||||
}
|
||||
|
||||
private String getApiAudioFormat(AudioFormat format) {
|
||||
if (!"default".equals(pollyTTSConfig.getAudioFormat())) {
|
||||
// Override system specified with user preferred value
|
||||
return pollyTTSConfig.getAudioFormat();
|
||||
}
|
||||
if (CODEC_MP3.equals(format.getCodec())) {
|
||||
return CODEC_MP3;
|
||||
} else if (CODEC_VORBIS.equals(format.getCodec())) {
|
||||
return CONTAINER_OGG;
|
||||
} else {
|
||||
throw new IllegalArgumentException("Audio format " + format.getCodec() + " not yet supported");
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getId() {
|
||||
return "pollytts";
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getLabel(Locale locale) {
|
||||
return "PollyTTS";
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,76 @@
|
||||
/**
|
||||
* 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.pollytts.internal;
|
||||
|
||||
import java.util.Locale;
|
||||
|
||||
import org.openhab.core.voice.Voice;
|
||||
|
||||
/**
|
||||
* Implementation of the Voice interface for PollyTTS.
|
||||
*
|
||||
* @author Robert Hillman - Initial contribution
|
||||
*/
|
||||
public class PollyTTSVoice implements Voice {
|
||||
|
||||
/**
|
||||
* Voice locale
|
||||
*/
|
||||
private final Locale locale;
|
||||
|
||||
/**
|
||||
* Voice label
|
||||
*/
|
||||
private final String label;
|
||||
|
||||
/**
|
||||
* Constructs a PollyTTS Voice for the passed data
|
||||
*
|
||||
* @param locale
|
||||
* The Locale of the voice
|
||||
* @param label
|
||||
* The label of the voice
|
||||
*/
|
||||
public PollyTTSVoice(Locale locale, String label) {
|
||||
this.locale = locale;
|
||||
this.label = label;
|
||||
}
|
||||
|
||||
/**
|
||||
* Globally unique identifier of the voice.
|
||||
*
|
||||
* @return A String uniquely identifying the voice globally
|
||||
*/
|
||||
@Override
|
||||
public String getUID() {
|
||||
return "pollytts:" + label;
|
||||
}
|
||||
|
||||
/**
|
||||
* The voice label, used for GUI's or VUI's
|
||||
*
|
||||
* @return The voice label, may not be globally unique
|
||||
*/
|
||||
@Override
|
||||
public String getLabel() {
|
||||
return label;
|
||||
}
|
||||
|
||||
/**
|
||||
* @inheritDoc
|
||||
*/
|
||||
@Override
|
||||
public Locale getLocale() {
|
||||
return locale;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,162 @@
|
||||
/**
|
||||
* 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.pollytts.internal.cloudapi;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.FileOutputStream;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.io.OutputStream;
|
||||
import java.math.BigInteger;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.security.MessageDigest;
|
||||
import java.security.NoSuchAlgorithmException;
|
||||
import java.util.Date;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
/**
|
||||
* This class implements a cache for the retrieved audio data. It will preserve them in the file system,
|
||||
* as audio files with an additional .txt file to indicate what content is in the audio file.
|
||||
*
|
||||
* @author Robert Hillman - Initial contribution
|
||||
*/
|
||||
public class CachedPollyTTSCloudImpl extends PollyTTSCloudImpl {
|
||||
|
||||
private static final int READ_BUFFER_SIZE = 4096;
|
||||
|
||||
private final Logger logger = LoggerFactory.getLogger(CachedPollyTTSCloudImpl.class);
|
||||
|
||||
private final File cacheFolder;
|
||||
|
||||
/**
|
||||
* Create the file folder to hold the the cached speech files.
|
||||
* check to make sure the directory exist and
|
||||
* create it if necessary
|
||||
*/
|
||||
public CachedPollyTTSCloudImpl(PollyTTSConfig config, File cacheFolder) throws IOException {
|
||||
super(config);
|
||||
this.cacheFolder = cacheFolder;
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetch the specified text as an audio file.
|
||||
* The audio file will be obtained from the cached folder if it
|
||||
* exist or generated by use to the external voice service.
|
||||
* The cached file txt description time stamp will be updated
|
||||
* to identify last use.
|
||||
*/
|
||||
public File getTextToSpeechAsFile(String text, String label, String audioFormat) throws IOException {
|
||||
String fileNameInCache = getUniqueFilenameForText(text, label);
|
||||
// check if in cache
|
||||
File audioFileInCache = new File(cacheFolder, fileNameInCache + "." + audioFormat.toLowerCase());
|
||||
if (audioFileInCache.exists()) {
|
||||
// update use date
|
||||
updateTimeStamp(audioFileInCache);
|
||||
updateTimeStamp(new File(cacheFolder, fileNameInCache + ".txt"));
|
||||
purgeAgedFiles();
|
||||
return audioFileInCache;
|
||||
}
|
||||
|
||||
// if not in cache, get audio data and put to cache
|
||||
try (InputStream is = getTextToSpeech(text, label, audioFormat);
|
||||
FileOutputStream fos = new FileOutputStream(audioFileInCache)) {
|
||||
copyStream(is, fos);
|
||||
// write text to file for transparency too
|
||||
// this allows to know which contents is in which audio file
|
||||
File txtFileInCache = new File(cacheFolder, fileNameInCache + ".txt");
|
||||
writeText(txtFileInCache, text);
|
||||
// return from cache
|
||||
return audioFileInCache;
|
||||
} catch (IOException ex) {
|
||||
logger.warn("Could not write {} to cache, return null", audioFileInCache, ex);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets a unique filename for a give text, by creating a MD5 hash of it. It
|
||||
* will be preceded by the voice label.
|
||||
*
|
||||
* Sample: "Robert_00a2653ac5f77063bc4ea2fee87318d3"
|
||||
*/
|
||||
private String getUniqueFilenameForText(String text, String label) {
|
||||
MessageDigest md;
|
||||
try {
|
||||
md = MessageDigest.getInstance("MD5");
|
||||
} catch (NoSuchAlgorithmException ex) {
|
||||
logger.error("Could not create MD5 hash for '{}'", text, ex);
|
||||
return null;
|
||||
}
|
||||
byte[] md5Hash = md.digest(text.getBytes(StandardCharsets.UTF_8));
|
||||
BigInteger bigInt = new BigInteger(1, md5Hash);
|
||||
String hashtext = bigInt.toString(16);
|
||||
// Now we need to zero pad it if you actually want the full 32
|
||||
// chars.
|
||||
while (hashtext.length() < 32) {
|
||||
hashtext = "0" + hashtext;
|
||||
}
|
||||
String fileName = label + "_" + hashtext;
|
||||
return fileName;
|
||||
}
|
||||
|
||||
// helper methods
|
||||
|
||||
private void copyStream(InputStream inputStream, OutputStream outputStream) throws IOException {
|
||||
byte[] bytes = new byte[READ_BUFFER_SIZE];
|
||||
int read = inputStream.read(bytes, 0, READ_BUFFER_SIZE);
|
||||
while (read > 0) {
|
||||
outputStream.write(bytes, 0, read);
|
||||
read = inputStream.read(bytes, 0, READ_BUFFER_SIZE);
|
||||
}
|
||||
}
|
||||
|
||||
private void writeText(File file, String text) throws IOException {
|
||||
try (OutputStream outputStream = new FileOutputStream(file)) {
|
||||
outputStream.write(text.getBytes(StandardCharsets.UTF_8));
|
||||
}
|
||||
}
|
||||
|
||||
private void updateTimeStamp(File file) throws IOException {
|
||||
// update use date for cache management
|
||||
file.setLastModified(System.currentTimeMillis());
|
||||
}
|
||||
|
||||
private void purgeAgedFiles() throws IOException {
|
||||
// just exit if expiration set to 0/disabled
|
||||
if (config.getExpireDate() == 0) {
|
||||
return;
|
||||
}
|
||||
long now = new Date().getTime();
|
||||
long diff = now - config.getLastDelete();
|
||||
// only execute ~ once every 2 days if cache called
|
||||
long oneDayMillis = TimeUnit.DAYS.toMillis(1);
|
||||
logger.debug("PollyTTS cache cleaner lastdelete {}", diff);
|
||||
if (diff > (2 * oneDayMillis)) {
|
||||
config.setLastDelete(now);
|
||||
long xDaysAgo = config.getExpireDate() * oneDayMillis;
|
||||
// Now search folders and delete old files
|
||||
int filesDeleted = 0;
|
||||
for (File file : cacheFolder.listFiles()) {
|
||||
diff = now - file.lastModified();
|
||||
if (diff > xDaysAgo) {
|
||||
filesDeleted++;
|
||||
file.delete();
|
||||
}
|
||||
}
|
||||
logger.debug("PollyTTS cache cleaner deleted '{}' aged files", filesDeleted);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,133 @@
|
||||
/**
|
||||
* 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.pollytts.internal.cloudapi;
|
||||
|
||||
import static java.util.stream.Collectors.*;
|
||||
import static org.openhab.core.audio.AudioFormat.*;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
import java.util.Locale;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
import java.util.stream.Stream;
|
||||
|
||||
import com.amazonaws.auth.AWSCredentials;
|
||||
import com.amazonaws.auth.AWSStaticCredentialsProvider;
|
||||
import com.amazonaws.auth.BasicAWSCredentials;
|
||||
import com.amazonaws.services.polly.AmazonPolly;
|
||||
import com.amazonaws.services.polly.AmazonPollyClientBuilder;
|
||||
import com.amazonaws.services.polly.model.DescribeVoicesRequest;
|
||||
import com.amazonaws.services.polly.model.OutputFormat;
|
||||
import com.amazonaws.services.polly.model.SynthesizeSpeechRequest;
|
||||
import com.amazonaws.services.polly.model.TextType;
|
||||
import com.amazonaws.services.polly.model.Voice;
|
||||
|
||||
/**
|
||||
* This class implements the Cloud service for PollyTTS.
|
||||
*
|
||||
* The implementation supports:
|
||||
* <ul>
|
||||
* <li>All languages</li>
|
||||
* <li>All voices</li>
|
||||
* <li>MP3 and OGG formats</li>
|
||||
* </ul>
|
||||
*
|
||||
* @author Robert Hillman - Initial contribution
|
||||
*/
|
||||
public class PollyTTSCloudImpl {
|
||||
|
||||
private static final Set<String> SUPPORTED_AUDIO_FORMATS = Collections
|
||||
.unmodifiableSet(Stream.of(CODEC_MP3, CONTAINER_OGG).collect(toSet()));
|
||||
|
||||
protected final PollyTTSConfig config;
|
||||
|
||||
private final AmazonPolly client;
|
||||
private final Map<String, String> labelToID;
|
||||
private final List<Voice> voices;
|
||||
|
||||
public PollyTTSCloudImpl(PollyTTSConfig config) {
|
||||
this.config = config;
|
||||
|
||||
AWSCredentials credentials = new BasicAWSCredentials(config.getAccessKey(), config.getSecretKey());
|
||||
client = AmazonPollyClientBuilder.standard().withRegion(config.getServiceRegion())
|
||||
.withCredentials(new AWSStaticCredentialsProvider(credentials)).build();
|
||||
voices = client.describeVoices(new DescribeVoicesRequest()).getVoices();
|
||||
|
||||
// create voice to ID translation for service invocation
|
||||
labelToID = voices.stream().collect(toMap(Voice::getName, Voice::getId));
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all supported audio formats by the TTS service. This includes MP3,
|
||||
* WAV and more audio formats as used in APIs.
|
||||
*/
|
||||
public Set<String> getAvailableAudioFormats() {
|
||||
return SUPPORTED_AUDIO_FORMATS;
|
||||
}
|
||||
|
||||
public Set<Locale> getAvailableLocales() {
|
||||
// @formatter:off
|
||||
return voices.stream()
|
||||
.map(voice -> Locale.forLanguageTag(voice.getLanguageCode()))
|
||||
.collect(toSet());
|
||||
// @formatter:on
|
||||
}
|
||||
|
||||
public Set<String> getAvailableVoices() {
|
||||
// @formatter:off
|
||||
return voices.stream()
|
||||
.map(Voice::getName)
|
||||
.collect(toSet());
|
||||
// @formatter:on
|
||||
}
|
||||
|
||||
public Set<String> getAvailableVoices(Locale locale) {
|
||||
// @formatter:off
|
||||
return voices.stream()
|
||||
.filter(voice -> voice.getLanguageCode().equalsIgnoreCase(locale.toLanguageTag()))
|
||||
.map(Voice::getName)
|
||||
.collect(toSet());
|
||||
// @formatter:on
|
||||
}
|
||||
|
||||
/**
|
||||
* This method will return an input stream to an audio stream for the given
|
||||
* parameters.
|
||||
* Get the given text in specified locale and audio format as input stream.
|
||||
*
|
||||
* @param text
|
||||
* the text to translate into speech
|
||||
* @param label
|
||||
* the voice Label to use
|
||||
* @param audioFormat
|
||||
* the audio format to use
|
||||
* @return an InputStream to the audio data in specified format
|
||||
* @throws IOException
|
||||
* will be raised if the audio data can not be retrieved from
|
||||
* cloud service
|
||||
*/
|
||||
public InputStream getTextToSpeech(String text, String label, String audioFormat) {
|
||||
String voiceID = labelToID.get(label);
|
||||
String format = audioFormat.toLowerCase();
|
||||
if ("ogg".equals(format)) {
|
||||
format = "ogg_vorbis";
|
||||
}
|
||||
TextType textType = text.startsWith("<speak>") ? TextType.Ssml : TextType.Text;
|
||||
SynthesizeSpeechRequest request = new SynthesizeSpeechRequest().withTextType(textType).withText(text)
|
||||
.withVoiceId(voiceID).withOutputFormat(OutputFormat.fromValue(format));
|
||||
return client.synthesizeSpeech(request).getAudioStream();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,110 @@
|
||||
/**
|
||||
* 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.pollytts.internal.cloudapi;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.stream.Collectors;
|
||||
import java.util.stream.Stream;
|
||||
|
||||
/**
|
||||
* This class implements the PollyTTS configuration.
|
||||
*
|
||||
* @author Robert Hillman - Initial contribution
|
||||
*/
|
||||
public class PollyTTSConfig {
|
||||
|
||||
private static final String ACCESS_KEY = "accessKey";
|
||||
private static final String SECRET_KEY = "secretKey";
|
||||
private static final String SERVICE_REGION = "serviceRegion";
|
||||
private static final String AUDIO_FORMAT = "audioFormat";
|
||||
private static final String CACHE_EXPIRATION = "cacheExpiration";
|
||||
|
||||
private String accessKey = "";
|
||||
private String secretKey = "";
|
||||
private String serviceRegion = "eu-west-1";
|
||||
private int expireDate = 0;
|
||||
private String audioFormat = "default";
|
||||
private long lastDelete;
|
||||
|
||||
public PollyTTSConfig(Map<String, Object> config) {
|
||||
assertValidConfig(config);
|
||||
|
||||
accessKey = config.getOrDefault(ACCESS_KEY, accessKey).toString();
|
||||
secretKey = config.getOrDefault(SECRET_KEY, secretKey).toString();
|
||||
serviceRegion = config.getOrDefault(SERVICE_REGION, serviceRegion).toString();
|
||||
audioFormat = config.getOrDefault(AUDIO_FORMAT, audioFormat).toString();
|
||||
expireDate = (int) Double
|
||||
.parseDouble(config.getOrDefault(CACHE_EXPIRATION, Double.toString(expireDate)).toString());
|
||||
}
|
||||
|
||||
private void assertValidConfig(Map<String, Object> config) {
|
||||
List<String> emptyParams = Stream.of(ACCESS_KEY, SECRET_KEY, SERVICE_REGION)
|
||||
.filter(param -> config.get(param) == null || config.get(param).toString().isEmpty())
|
||||
.collect(Collectors.toList());
|
||||
|
||||
if (!emptyParams.isEmpty()) {
|
||||
throw new IllegalArgumentException(
|
||||
"Missing configuration parameters: " + emptyParams.stream().collect(Collectors.joining(", ")));
|
||||
}
|
||||
}
|
||||
|
||||
public String getAccessKey() {
|
||||
return accessKey;
|
||||
}
|
||||
|
||||
public String getSecretKey() {
|
||||
return secretKey;
|
||||
}
|
||||
|
||||
public String getServiceRegion() {
|
||||
return serviceRegion;
|
||||
}
|
||||
|
||||
/**
|
||||
* get the life time for cache files
|
||||
*/
|
||||
public int getExpireDate() {
|
||||
return expireDate;
|
||||
}
|
||||
|
||||
/**
|
||||
* returns audio format specified for audio
|
||||
*/
|
||||
public String getAudioFormat() {
|
||||
return audioFormat;
|
||||
}
|
||||
|
||||
/**
|
||||
* get the date when cache was cleaned last
|
||||
*/
|
||||
public long getLastDelete() {
|
||||
return lastDelete;
|
||||
}
|
||||
|
||||
/**
|
||||
* set the date when cache was cleaned last
|
||||
*/
|
||||
public void setLastDelete(long lastDelete) {
|
||||
this.lastDelete = lastDelete;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
StringBuilder builder = new StringBuilder();
|
||||
builder.append("PollyTTSConfig [accessKey=").append(accessKey).append(", secretKey=").append(secretKey)
|
||||
.append(", serviceRegion=").append(serviceRegion).append(", expireDate=").append(expireDate)
|
||||
.append(", audioFormat=").append(audioFormat).append(", lastDelete=").append(lastDelete).append("]");
|
||||
return builder.toString();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,64 @@
|
||||
<?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:pollytts">
|
||||
<parameter name="accessKey" type="text" required="true">
|
||||
<label>Access Key</label>
|
||||
<description>The access key part of the AWS credentials. You need to register to get a key.</description>
|
||||
</parameter>
|
||||
|
||||
<parameter name="secretKey" type="text" required="true">
|
||||
<label>Secret Key</label>
|
||||
<description>The secret key part of the AWS credentials. You need to register to get a key.</description>
|
||||
</parameter>
|
||||
|
||||
<parameter name="serviceRegion" type="text" required="true">
|
||||
<label>Service Region</label>
|
||||
<description>The service region used for accessing Polly. To reduce latency select the region closest to you.</description>
|
||||
<options>
|
||||
<option value="ap-south-1">Asia Pacific (Mumbai)</option>
|
||||
<option value="ap-northeast-2">Asia Pacific (Seoul)</option>
|
||||
<option value="ap-southeast-1">Asia Pacific (Singapore)</option>
|
||||
<option value="ap-southeast-2">Asia Pacific (Sydney)</option>
|
||||
<option value="ap-northeast-1">Asia Pacific (Tokyo)</option>
|
||||
<option value="us-gov-west-1">AWS GovCloud (US)</option>
|
||||
<option value="ca-central-1">Canada (Central)</option>
|
||||
<option value="cn-northwest-1">China (Ningxia)</option>
|
||||
<option value="eu-central-1">EU (Frankfurt)</option>
|
||||
<option value="eu-west-1">EU (Ireland)</option>
|
||||
<option value="eu-west-2">EU (London)</option>
|
||||
<option value="eu-west-3">EU (Paris)</option>
|
||||
<option value="sa-east-1">South America (São Paulo)</option>
|
||||
<option value="us-east-1">US East (N. Virginia)</option>
|
||||
<option value="us-east-2">US East (Ohio)</option>
|
||||
<option value="us-west-1">US West (N. California)</option>
|
||||
<option value="us-west-2">US West (Oregon)</option>
|
||||
</options>
|
||||
<default>eu-west-1</default>
|
||||
</parameter>
|
||||
|
||||
<parameter name="audioFormat" type="text">
|
||||
<label>Audio Format</label>
|
||||
<description>Allows for overriding the system default audio format. "MP3" and "OGG" are the only audio formats that
|
||||
are supported.</description>
|
||||
<options>
|
||||
<option value="default">Use system default</option>
|
||||
<option value="MP3">MP3</option>
|
||||
<option value="OGG">OGG</option>
|
||||
</options>
|
||||
<default>default</default>
|
||||
</parameter>
|
||||
|
||||
<parameter name="cacheExpiration" type="text">
|
||||
<label>Cache Expiration</label>
|
||||
<description>Determines the age in days when unused cached files are purged.
|
||||
Use 0 to disable this functionality.</description>
|
||||
<default>0</default>
|
||||
</parameter>
|
||||
</config-description>
|
||||
|
||||
</config-description:config-descriptions>
|
||||
Reference in New Issue
Block a user