added migrated 2.x add-ons

Signed-off-by: Kai Kreuzer <kai@openhab.org>
This commit is contained in:
Kai Kreuzer
2020-09-21 01:58:32 +02:00
parent bbf1a7fd29
commit 6df6783b60
11662 changed files with 1302875 additions and 11 deletions

View 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="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" 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="output" path="target/classes"/>
</classpath>

View File

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

View File

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

View File

@@ -0,0 +1,62 @@
# VoiceRSS Text-to-Speech
## Overview
VoiceRSS is an Internet based TTS service hosted at <https://api.voicerss.org>.
You must obtain an API Key to get access to this service.
The free version allows you to make up to 350 requests per day; for more you may need a commercial subscription.
For more information, see <http://www.voicerss.org/>.
## Samples
Replace API_KEY with your personal API key for simple testing of different API calls:
```
# EN
https://api.voicerss.org/?key=API_KEY&hl=en-us&src=Hello%20World
https://api.voicerss.org/?key=API_KEY&hl=en-us&c=WAV&src=Hello%20World
https://api.voicerss.org/?key=API_KEY&hl=en-us&f=44khz_16bit_mono&src=Hello%20World
https://api.voicerss.org/?key=API_KEY&hl=en-gb&f=44khz_16bit_stereo&src=Hello%20World
# DE
https://api.voicerss.org/?key=API_KEY&hl=de-de&f=44khz_16bit_mono&src=Hallo%20Welt
```
## Configuration
You must add your API_KEY to your configuration by adding a file "voicerss.cfg" to the services folder, with this entry:
```
apiKey=1234567890
```
It actually supports only one voice: "voicerss:default", which is configured to use 44kHz, mono, 16 bit sampling quality.
## Caching
The VoiceRSS extension does cache audio files from previous requests, to reduce traffic, improve performance, reduce number of requests and provide same time offline capability.
For convenience, there is a tool where the audio cache can be generated in advance, to have a prefilled cache when starting this extension.
You have to copy the generated data to your userdata/voicerss/cache folder.
Synopsis of this tool:
```
Usage: java org.openhab.voice.voicerss.tool.CreateTTSCache <args>
Arguments: --api-key <key> <cache-dir> <locale> { <text> | @inputfile }
key the VoiceRSS API Key, e.g. "123456789"
cache-dir is directory where the files will be stored, e.g. "voicerss-cache"
locale the language locale, has to be valid, e.g. "en-us", "de-de"
text the text to create audio file for, e.g. "Hello World"
inputfile a name of a file, where all lines will be translatet to text, e.g. "@message.txt"
Sample: java org.openhab.voice.voicerss.tool.CreateTTSCache --api-key 1234567890 cache en-US @messages.txt
```
## Open Issues
* add all media formats
* add all supported languages
* do not log API-Key in plain text

View File

@@ -0,0 +1,17 @@
<?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.voicerss</artifactId>
<name>openHAB Add-ons :: Bundles :: Voice :: VoiceRSS Text-to-Speech</name>
</project>

View File

@@ -0,0 +1,10 @@
<?xml version="1.0" encoding="UTF-8"?>
<features name="org.openhab.voice.voicerss-${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-voicerss" description="VoiceRSS Text-to-Speech" version="${project.version}">
<feature>openhab-runtime-base</feature>
<configfile finalname="${openhab.conf}/services/voicerss.cfg" override="false">mvn:${project.groupId}/openhab-addons-external/${project.version}/cfg/voicerss</configfile>
<bundle start-level="80">mvn:org.openhab.addons.bundles/org.openhab.voice.voicerss/${project.version}</bundle>
</feature>
</features>

View File

@@ -0,0 +1,35 @@
/**
* 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.voicerss.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 VoiceRSSTTSService}. 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 Jochen Hiller - Initial contribution and API
*/
class VoiceRSSAudioStream extends FileAudioStream {
public VoiceRSSAudioStream(File audioFile, AudioFormat format) throws AudioException {
super(audioFile, format);
}
}

View File

@@ -0,0 +1,235 @@
/**
* 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.voicerss.internal;
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.voicerss.internal.cloudapi.CachedVoiceRSSCloudImpl;
import org.osgi.framework.Constants;
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 VoiceRSS TTS service.
*
* @author Jochen Hiller - Initial contribution and API
* @author Laurent Garnier - add support for OGG and AAC audio formats
*/
@Component(configurationPid = "org.openhab.voicerss", property = { Constants.SERVICE_PID + "=org.openhab.voicerss",
ConfigurableService.SERVICE_PROPERTY_DESCRIPTION_URI + "=voice:voicerss",
ConfigurableService.SERVICE_PROPERTY_LABEL + "=VoiceRSS Text-to-Speech",
ConfigurableService.SERVICE_PROPERTY_CATEGORY + "=voice" })
public class VoiceRSSTTSService implements TTSService {
/** Cache folder name is below userdata/voicerss/cache. */
private static final String CACHE_FOLDER_NAME = "voicerss" + File.separator + "cache";
// API Key comes from ConfigAdmin
private static final String CONFIG_API_KEY = "apiKey";
private String apiKey;
private final Logger logger = LoggerFactory.getLogger(VoiceRSSTTSService.class);
/**
* We need the cached implementation to allow for FixedLengthAudioStream.
*/
private CachedVoiceRSSCloudImpl voiceRssImpl;
/**
* Set of supported voices
*/
private Set<Voice> voices;
/**
* Set of supported audio formats
*/
private Set<AudioFormat> audioFormats;
/**
* DS activate, with access to ConfigAdmin
*/
protected void activate(Map<String, Object> config) {
try {
modified(config);
voiceRssImpl = initVoiceImplementation();
voices = initVoices();
audioFormats = initAudioFormats();
logger.debug("Using VoiceRSS cache folder {}", getCacheFolderName());
} catch (IllegalStateException e) {
logger.error("Failed to activate VoiceRSS: {}", e.getMessage(), e);
}
}
@Modified
protected void modified(Map<String, Object> config) {
if (config != null) {
apiKey = config.containsKey(CONFIG_API_KEY) ? config.get(CONFIG_API_KEY).toString() : null;
}
}
@Override
public Set<Voice> getAvailableVoices() {
return Collections.unmodifiableSet(voices);
}
@Override
public Set<AudioFormat> getSupportedFormats() {
return Collections.unmodifiableSet(audioFormats);
}
@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 (apiKey == null) {
throw new TTSException("Missing API key, configure it first before using");
}
// Validate arguments
if (text == null) {
throw new TTSException("The passed text is null");
}
// trim text
String trimmedText = text.trim();
if (trimmedText.isEmpty()) {
throw new TTSException("The passed text is empty");
}
if (!voices.contains(voice)) {
throw new TTSException("The passed voice is unsupported");
}
boolean isAudioFormatSupported = false;
for (AudioFormat currentAudioFormat : audioFormats) {
if (currentAudioFormat.isCompatible(requestedFormat)) {
isAudioFormatSupported = true;
break;
}
}
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 = voiceRssImpl.getTextToSpeechAsFile(apiKey, trimmedText,
voice.getLocale().toLanguageTag(), getApiAudioFormat(requestedFormat));
if (cacheAudioFile == null) {
throw new TTSException("Could not read from VoiceRSS service");
}
return new VoiceRSSAudioStream(cacheAudioFile, requestedFormat);
} catch (AudioException ex) {
throw new TTSException("Could not create AudioStream: " + ex.getMessage(), ex);
} catch (IOException ex) {
throw new TTSException("Could not read from VoiceRSS service: " + ex.getMessage(), ex);
}
}
/**
* Initializes voices.
*
* @return The voices of this instance
*/
private Set<Voice> initVoices() {
Set<Voice> voices = new HashSet<>();
for (Locale locale : voiceRssImpl.getAvailableLocales()) {
for (String voiceLabel : voiceRssImpl.getAvailableVoices(locale)) {
voices.add(new VoiceRSSVoice(locale, voiceLabel));
}
}
return voices;
}
/**
* Initializes audioFormats
*
* @return The audio formats of this instance
*/
private Set<AudioFormat> initAudioFormats() {
Set<AudioFormat> audioFormats = new HashSet<>();
for (String format : voiceRssImpl.getAvailableAudioFormats()) {
audioFormats.add(getAudioFormat(format));
}
return audioFormats;
}
private AudioFormat getAudioFormat(String apiFormat) {
Boolean bigEndian = null;
Integer bitDepth = 16;
Integer bitRate = null;
Long frequency = 44100L;
if ("MP3".equals(apiFormat)) {
// we use by default: MP3, 44khz_16bit_mono with bitrate 64 kbps
bitRate = 64000;
return new AudioFormat(AudioFormat.CONTAINER_NONE, AudioFormat.CODEC_MP3, bigEndian, bitDepth, bitRate,
frequency);
} else if ("OGG".equals(apiFormat)) {
// we use by default: OGG, 44khz_16bit_mono
return new AudioFormat(AudioFormat.CONTAINER_OGG, AudioFormat.CODEC_VORBIS, bigEndian, bitDepth, bitRate,
frequency);
} else if ("AAC".equals(apiFormat)) {
// we use by default: AAC, 44khz_16bit_mono
return new AudioFormat(AudioFormat.CONTAINER_NONE, AudioFormat.CODEC_AAC, bigEndian, bitDepth, bitRate,
frequency);
} else {
throw new IllegalArgumentException("Audio format " + apiFormat + " not yet supported");
}
}
private String getApiAudioFormat(AudioFormat format) {
if (format.getCodec().equals(AudioFormat.CODEC_MP3)) {
return "MP3";
} else if (format.getCodec().equals(AudioFormat.CODEC_VORBIS)) {
return "OGG";
} else if (format.getCodec().equals(AudioFormat.CODEC_AAC)) {
return "AAC";
} else {
throw new IllegalArgumentException("Audio format " + format.getCodec() + " not yet supported");
}
}
private CachedVoiceRSSCloudImpl initVoiceImplementation() {
return new CachedVoiceRSSCloudImpl(getCacheFolderName());
}
private String getCacheFolderName() {
// we assume that this folder does NOT have a trailing separator
return ConfigConstants.getUserDataFolder() + File.separator + CACHE_FOLDER_NAME;
}
@Override
public String getId() {
return "voicerss";
}
@Override
public String getLabel(Locale locale) {
return "VoiceRSS";
}
}

View File

@@ -0,0 +1,77 @@
/**
* 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.voicerss.internal;
import java.util.Locale;
import org.openhab.core.voice.Voice;
/**
* Implementation of the Voice interface for VoiceRSS. Label is only "default"
* as only voice supported.
*
* @author Jochen Hiller - Initial contribution and API
*/
public class VoiceRSSVoice implements Voice {
/**
* Voice locale
*/
private final Locale locale;
/**
* Voice label
*/
private final String label;
/**
* Constructs a VoiceRSS Voice for the passed data
*
* @param locale
* The Locale of the voice
* @param label
* The label of the voice
*/
public VoiceRSSVoice(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 "voicerss:" + locale.toLanguageTag().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 label;
}
/**
* @inheritDoc
*/
@Override
public Locale getLocale() {
return locale;
}
}

View File

@@ -0,0 +1,128 @@
/**
* 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.voicerss.internal.cloudapi;
import java.io.File;
import java.io.FileNotFoundException;
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 org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* This class implements a cache for the retrieved audio data. It will preserve
* them in file system, as audio files with an additional .txt file to indicate
* what content is in the audio file.
*
* @author Jochen Hiller - Initial contribution
*/
public class CachedVoiceRSSCloudImpl extends VoiceRSSCloudImpl {
private final Logger logger = LoggerFactory.getLogger(CachedVoiceRSSCloudImpl.class);
private final File cacheFolder;
/**
* Stream buffer size
*/
private static final int READ_BUFFER_SIZE = 4096;
public CachedVoiceRSSCloudImpl(String cacheFolderName) {
if (cacheFolderName == null) {
throw new IllegalStateException("Folder for cache must be defined");
}
// Lazy create the cache folder
cacheFolder = new File(cacheFolderName);
if (!cacheFolder.exists()) {
cacheFolder.mkdirs();
}
}
public File getTextToSpeechAsFile(String apiKey, String text, String locale, String audioFormat)
throws IOException {
String fileNameInCache = getUniqueFilenameForText(text, locale);
// check if in cache
File audioFileInCache = new File(cacheFolder, fileNameInCache + "." + audioFormat.toLowerCase());
if (audioFileInCache.exists()) {
return audioFileInCache;
}
// if not in cache, get audio data and put to cache
try (InputStream is = super.getTextToSpeech(apiKey, text, locale, 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 (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;
}
}
/**
* Gets a unique filename for a give text, by creating a MD5 hash of it. It
* will be preceded by the locale.
*
* Sample: "en-US_00a2653ac5f77063bc4ea2fee87318d3"
*/
private String getUniqueFilenameForText(String text, String locale) {
try {
byte[] bytesOfMessage = text.getBytes(StandardCharsets.UTF_8);
MessageDigest md = MessageDigest.getInstance("MD5");
byte[] md5Hash = md.digest(bytesOfMessage);
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;
}
return locale + "_" + hashtext;
} catch (NoSuchAlgorithmException ex) {
// should not happen
logger.error("Could not create MD5 hash for '{}'", text, ex);
return null;
}
}
// 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));
}
}
}

View File

@@ -0,0 +1,79 @@
/**
* Copyright (c) 2010-2020 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.voice.voicerss.internal.cloudapi;
import java.io.IOException;
import java.io.InputStream;
import java.util.Locale;
import java.util.Set;
import org.openhab.core.audio.AudioFormat;
/**
* Interface which represents the functionality needed from the VoiceRSS TTS
* service.
*
* @author Jochen Hiller - Initial contribution
*/
public interface VoiceRSSCloudAPI {
/**
* Get all supported locales by the TTS service.
*
* @return A set of @{link {@link Locale} supported
*/
Set<Locale> getAvailableLocales();
/**
* Get all supported audio formats by the TTS service. This includes MP3,
* WAV and more audio formats as used in APIs. About supported audio
* formats, see {@link AudioFormat}
*
* @return A set of all audio formats supported
*/
Set<String> getAvailableAudioFormats();
/**
* Get all supported voices.
*
* @return A set of voice names supported
*/
Set<String> getAvailableVoices();
/**
* Get all supported voices for a specified locale.
*
* @param locale
* the locale to get all voices for
* @return A set of voice names supported
*/
Set<String> getAvailableVoices(Locale locale);
/**
* Get the given text in specified locale and auido format as input stream.
*
* @param apiKey
* the API key to use for the cloud service
* @param text
* the text to translate into speech
* @param locale
* the locale 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
*/
InputStream getTextToSpeech(String apiKey, String text, String locale, String audioFormat) throws IOException;
}

View File

@@ -0,0 +1,180 @@
/**
* 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.voicerss.internal.cloudapi;
import static java.util.stream.Collectors.toSet;
import java.io.IOException;
import java.io.InputStream;
import java.io.UnsupportedEncodingException;
import java.net.HttpURLConnection;
import java.net.URL;
import java.net.URLConnection;
import java.net.URLEncoder;
import java.util.Collections;
import java.util.HashSet;
import java.util.List;
import java.util.Locale;
import java.util.Map.Entry;
import java.util.Set;
import java.util.stream.Stream;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* This class implements the Cloud service from VoiceRSS. For more information,
* see API documentation at http://www.voicerss.org/api/documentation.aspx.
*
* Current state of implementation:
* <ul>
* <li>All API languages supported</li>
* <li>Only default voice supported with good audio quality</li>
* <li>Only MP3, OGG and AAC audio formats supported</li>
* <li>It uses HTTP and not HTTPS (for performance reasons)</li>
* </ul>
*
* @author Jochen Hiller - Initial contribution
* @author Laurent Garnier - add support for all API languages
* @author Laurent Garnier - add support for OGG and AAC audio formats
*/
public class VoiceRSSCloudImpl implements VoiceRSSCloudAPI {
private final Logger logger = LoggerFactory.getLogger(VoiceRSSCloudImpl.class);
private static final Set<String> SUPPORTED_AUDIO_FORMATS = Stream.of("MP3", "OGG", "AAC").collect(toSet());
private static final Set<Locale> SUPPORTED_LOCALES = new HashSet<>();
static {
SUPPORTED_LOCALES.add(Locale.forLanguageTag("ca-es"));
SUPPORTED_LOCALES.add(Locale.forLanguageTag("da-dk"));
SUPPORTED_LOCALES.add(Locale.forLanguageTag("de-de"));
SUPPORTED_LOCALES.add(Locale.forLanguageTag("en-au"));
SUPPORTED_LOCALES.add(Locale.forLanguageTag("en-ca"));
SUPPORTED_LOCALES.add(Locale.forLanguageTag("en-gb"));
SUPPORTED_LOCALES.add(Locale.forLanguageTag("en-in"));
SUPPORTED_LOCALES.add(Locale.forLanguageTag("en-us"));
SUPPORTED_LOCALES.add(Locale.forLanguageTag("es-es"));
SUPPORTED_LOCALES.add(Locale.forLanguageTag("es-mx"));
SUPPORTED_LOCALES.add(Locale.forLanguageTag("fi-fi"));
SUPPORTED_LOCALES.add(Locale.forLanguageTag("fr-ca"));
SUPPORTED_LOCALES.add(Locale.forLanguageTag("fr-fr"));
SUPPORTED_LOCALES.add(Locale.forLanguageTag("it-it"));
SUPPORTED_LOCALES.add(Locale.forLanguageTag("ja-jp"));
SUPPORTED_LOCALES.add(Locale.forLanguageTag("ko-kr"));
SUPPORTED_LOCALES.add(Locale.forLanguageTag("nb-no"));
SUPPORTED_LOCALES.add(Locale.forLanguageTag("nl-nl"));
SUPPORTED_LOCALES.add(Locale.forLanguageTag("pl-pl"));
SUPPORTED_LOCALES.add(Locale.forLanguageTag("pt-br"));
SUPPORTED_LOCALES.add(Locale.forLanguageTag("pt-pt"));
SUPPORTED_LOCALES.add(Locale.forLanguageTag("ru-ru"));
SUPPORTED_LOCALES.add(Locale.forLanguageTag("sv-se"));
SUPPORTED_LOCALES.add(Locale.forLanguageTag("zh-cn"));
SUPPORTED_LOCALES.add(Locale.forLanguageTag("zh-hk"));
SUPPORTED_LOCALES.add(Locale.forLanguageTag("zh-tw"));
}
private static final Set<String> SUPPORTED_VOICES = Collections.singleton("VoiceRSS");
@Override
public Set<String> getAvailableAudioFormats() {
return SUPPORTED_AUDIO_FORMATS;
}
@Override
public Set<Locale> getAvailableLocales() {
return SUPPORTED_LOCALES;
}
@Override
public Set<String> getAvailableVoices() {
return SUPPORTED_VOICES;
}
@Override
public Set<String> getAvailableVoices(Locale locale) {
for (Locale voiceLocale : SUPPORTED_LOCALES) {
if (voiceLocale.toLanguageTag().equalsIgnoreCase(locale.toLanguageTag())) {
return SUPPORTED_VOICES;
}
}
return new HashSet<>();
}
/**
* This method will return an input stream to an audio stream for the given
* parameters.
*
* It will do that using a plain URL connection to avoid any external
* dependencies.
*/
@Override
public InputStream getTextToSpeech(String apiKey, String text, String locale, String audioFormat)
throws IOException {
String url = createURL(apiKey, text, locale, audioFormat);
logger.debug("Call {}", url);
URLConnection connection = new URL(url).openConnection();
// we will check return codes. The service will ALWAYS return a HTTP
// 200, but for error messages, it will return a text/plain format and
// the error message in body
int status = ((HttpURLConnection) connection).getResponseCode();
if (HttpURLConnection.HTTP_OK != status) {
logger.error("Call {} returned HTTP {}", url, status);
throw new IOException("Could not read from service: HTTP code " + status);
}
if (logger.isTraceEnabled()) {
for (Entry<String, List<String>> header : connection.getHeaderFields().entrySet()) {
logger.trace("Response.header: {}={}", header.getKey(), header.getValue());
}
}
String contentType = connection.getHeaderField("Content-Type");
InputStream is = connection.getInputStream();
// check if content type is text/plain, then we have an error
if (contentType.contains("text/plain")) {
byte[] bytes = new byte[256];
is.read(bytes, 0, 256);
// close before throwing an exception
try {
is.close();
} catch (IOException ex) {
logger.debug("Failed to close inputstream", ex);
}
throw new IOException(
"Could not read audio content, service return an error: " + new String(bytes, "UTF-8"));
} else {
return is;
}
}
// internal
/**
* This method will create the URL for the cloud service. The text will be
* URI encoded as it is part of the URL.
*
* It is in package scope to be accessed by tests.
*/
private String createURL(String apiKey, String text, String locale, String audioFormat) {
String encodedMsg;
try {
encodedMsg = URLEncoder.encode(text, "UTF-8");
} catch (UnsupportedEncodingException ex) {
logger.error("UnsupportedEncodingException for UTF-8 MUST NEVER HAPPEN! Check your JVM configuration!", ex);
// fall through and use msg un-encoded
encodedMsg = text;
}
return "http://api.voicerss.org/?key=" + apiKey + "&hl=" + locale + "&c=" + audioFormat
+ "&f=44khz_16bit_mono&src=" + encodedMsg;
}
}

View File

@@ -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.voicerss.tool;
import java.io.BufferedReader;
import java.io.File;
import java.io.FileReader;
import java.io.IOException;
import org.openhab.voice.voicerss.internal.cloudapi.CachedVoiceRSSCloudImpl;
/**
* This class fills a cache with data from the VoiceRSS TTS service.
*
* @author Jochen Hiller - Initial contribution
*/
public class CreateTTSCache {
public static final int RC_OK = 0;
public static final int RC_USAGE = 1;
public static final int RC_INPUT_FILE_NOT_FOUND = 2;
public static final int RC_API_KEY_MISSING = 3;
public static void main(String[] args) throws IOException {
CreateTTSCache tool = new CreateTTSCache();
int rc = tool.doMain(args);
System.exit(rc);
}
public int doMain(String[] args) throws IOException {
if ((args == null) || (args.length != 5)) {
usage();
return RC_USAGE;
}
if (!args[0].equalsIgnoreCase("--api-key")) {
usage();
return RC_API_KEY_MISSING;
}
String apiKey = args[1];
String cacheDir = args[2];
String locale = args[3];
if (args[4].startsWith("@")) {
String inputFileName = args[4].substring(1);
File inputFile = new File(inputFileName);
if (!inputFile.exists()) {
usage();
System.err.println("File " + inputFileName + " not found");
return RC_INPUT_FILE_NOT_FOUND;
}
generateCacheForFile(apiKey, cacheDir, locale, inputFileName);
} else {
String text = args[4];
generateCacheForMessage(apiKey, cacheDir, locale, text);
}
return RC_OK;
}
private void usage() {
System.out.println("Usage: java org.openhab.voice.voicerss.tool.CreateTTSCache <args>");
System.out.println("Arguments: --api-key <key> <cache-dir> <locale> { <text> | @inputfile }");
System.out.println(" key the VoiceRSS API Key, e.g. \"123456789\"");
System.out.println(" cache-dir is directory where the files will be stored, e.g. \"voicerss-cache\"");
System.out.println(" locale the language locale, has to be valid, e.g. \"en-us\", \"de-de\"");
System.out.println(" text the text to create audio file for, e.g. \"Hello World\"");
System.out.println(
" inputfile a name of a file, where all lines will be translatet to text, e.g. \"@message.txt\"");
System.out.println();
System.out.println(
"Sample: java org.openhab.voice.voicerss.tool.CreateTTSCache --api-key 1234567890 cache en-US @messages.txt");
System.out.println();
}
private void generateCacheForFile(String apiKey, String cacheDir, String locale, String inputFileName)
throws IOException {
File inputFile = new File(inputFileName);
try (BufferedReader br = new BufferedReader(new FileReader(inputFile))) {
String line;
while ((line = br.readLine()) != null) {
// process the line.
generateCacheForMessage(apiKey, cacheDir, locale, line);
}
}
}
private void generateCacheForMessage(String apiKey, String cacheDir, String locale, String msg) throws IOException {
if (msg == null) {
System.err.println("Ignore msg=null");
return;
}
String trimmedMsg = msg.trim();
if (trimmedMsg.length() == 0) {
System.err.println("Ignore msg=''");
return;
}
CachedVoiceRSSCloudImpl impl = new CachedVoiceRSSCloudImpl(cacheDir);
File cachedFile = impl.getTextToSpeechAsFile(apiKey, trimmedMsg, locale, "MP3");
System.out.println(
"Created cached audio for locale='" + locale + "', msg='" + trimmedMsg + "' to file=" + cachedFile);
}
}

View File

@@ -0,0 +1,16 @@
<?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:voicerss">
<parameter name="apiKey" type="text" required="true">
<label>VoiceRSS API Key</label>
<description>The API Key to get access to http://www.voicerss.org. You need to register with at least a free account
to get an API key.</description>
</parameter>
</config-description>
</config-description:config-descriptions>