diff --git a/bundles/org.openhab.voice.voicerss/src/main/java/org/openhab/voice/voicerss/internal/VoiceRSSTTSService.java b/bundles/org.openhab.voice.voicerss/src/main/java/org/openhab/voice/voicerss/internal/VoiceRSSTTSService.java
index a9d9110e5d949..6d5475468e961 100644
--- a/bundles/org.openhab.voice.voicerss/src/main/java/org/openhab/voice/voicerss/internal/VoiceRSSTTSService.java
+++ b/bundles/org.openhab.voice.voicerss/src/main/java/org/openhab/voice/voicerss/internal/VoiceRSSTTSService.java
@@ -136,7 +136,7 @@ public AudioStream synthesize(String text, Voice voice, AudioFormat requestedFor
// only a default voice
try {
File cacheAudioFile = voiceRssImpl.getTextToSpeechAsFile(apiKey, trimmedText,
- voice.getLocale().toLanguageTag(), getApiAudioFormat(requestedFormat));
+ voice.getLocale().toLanguageTag(), voice.getLabel(), getApiAudioFormat(requestedFormat));
if (cacheAudioFile == null) {
throw new TTSException("Could not read from VoiceRSS service");
}
diff --git a/bundles/org.openhab.voice.voicerss/src/main/java/org/openhab/voice/voicerss/internal/VoiceRSSVoice.java b/bundles/org.openhab.voice.voicerss/src/main/java/org/openhab/voice/voicerss/internal/VoiceRSSVoice.java
index ef3f82387066c..02d33f8502550 100644
--- a/bundles/org.openhab.voice.voicerss/src/main/java/org/openhab/voice/voicerss/internal/VoiceRSSVoice.java
+++ b/bundles/org.openhab.voice.voicerss/src/main/java/org/openhab/voice/voicerss/internal/VoiceRSSVoice.java
@@ -15,6 +15,7 @@
import java.util.Locale;
import org.openhab.core.voice.Voice;
+import org.openhab.voice.voicerss.internal.cloudapi.VoiceRSSCloudImpl;
/**
* Implementation of the Voice interface for VoiceRSS. Label is only "default"
@@ -54,7 +55,11 @@ public VoiceRSSVoice(Locale locale, String label) {
*/
@Override
public String getUID() {
- return "voicerss:" + locale.toLanguageTag().replaceAll("[^a-zA-Z0-9_]", "");
+ String uid = "voicerss:" + locale.toLanguageTag().replaceAll("[^a-zA-Z0-9_]", "");
+ if (!label.equals(VoiceRSSCloudImpl.DEFAULT_VOICE)) {
+ uid += "_" + label.replaceAll("[^a-zA-Z0-9_]", "");
+ }
+ return uid;
}
/**
diff --git a/bundles/org.openhab.voice.voicerss/src/main/java/org/openhab/voice/voicerss/internal/cloudapi/CachedVoiceRSSCloudImpl.java b/bundles/org.openhab.voice.voicerss/src/main/java/org/openhab/voice/voicerss/internal/cloudapi/CachedVoiceRSSCloudImpl.java
index 71a16d1ee053e..b94f07cf4793a 100644
--- a/bundles/org.openhab.voice.voicerss/src/main/java/org/openhab/voice/voicerss/internal/cloudapi/CachedVoiceRSSCloudImpl.java
+++ b/bundles/org.openhab.voice.voicerss/src/main/java/org/openhab/voice/voicerss/internal/cloudapi/CachedVoiceRSSCloudImpl.java
@@ -55,9 +55,9 @@ public CachedVoiceRSSCloudImpl(String cacheFolderName) {
}
}
- public File getTextToSpeechAsFile(String apiKey, String text, String locale, String audioFormat)
+ public File getTextToSpeechAsFile(String apiKey, String text, String locale, String voice, String audioFormat)
throws IOException {
- String fileNameInCache = getUniqueFilenameForText(text, locale);
+ String fileNameInCache = getUniqueFilenameForText(text, locale, voice);
// check if in cache
File audioFileInCache = new File(cacheFolder, fileNameInCache + "." + audioFormat.toLowerCase());
if (audioFileInCache.exists()) {
@@ -65,7 +65,7 @@ public File getTextToSpeechAsFile(String apiKey, String text, String locale, Str
}
// if not in cache, get audio data and put to cache
- try (InputStream is = super.getTextToSpeech(apiKey, text, locale, audioFormat);
+ try (InputStream is = super.getTextToSpeech(apiKey, text, locale, voice, audioFormat);
FileOutputStream fos = new FileOutputStream(audioFileInCache)) {
copyStream(is, fos);
// write text to file for transparency too
@@ -89,7 +89,7 @@ public File getTextToSpeechAsFile(String apiKey, String text, String locale, Str
*
* Sample: "en-US_00a2653ac5f77063bc4ea2fee87318d3"
*/
- private String getUniqueFilenameForText(String text, String locale) {
+ private String getUniqueFilenameForText(String text, String locale, String voice) {
try {
byte[] bytesOfMessage = text.getBytes(StandardCharsets.UTF_8);
MessageDigest md = MessageDigest.getInstance("MD5");
@@ -101,7 +101,12 @@ private String getUniqueFilenameForText(String text, String locale) {
while (hashtext.length() < 32) {
hashtext = "0" + hashtext;
}
- return locale + "_" + hashtext;
+ String filename = locale + "_";
+ if (!DEFAULT_VOICE.equals(voice)) {
+ filename += voice + "_";
+ }
+ filename += hashtext;
+ return filename;
} catch (NoSuchAlgorithmException ex) {
// should not happen
logger.error("Could not create MD5 hash for '{}'", text, ex);
diff --git a/bundles/org.openhab.voice.voicerss/src/main/java/org/openhab/voice/voicerss/internal/cloudapi/VoiceRSSCloudAPI.java b/bundles/org.openhab.voice.voicerss/src/main/java/org/openhab/voice/voicerss/internal/cloudapi/VoiceRSSCloudAPI.java
index 8a7d4b186856b..175f8cfa625f1 100644
--- a/bundles/org.openhab.voice.voicerss/src/main/java/org/openhab/voice/voicerss/internal/cloudapi/VoiceRSSCloudAPI.java
+++ b/bundles/org.openhab.voice.voicerss/src/main/java/org/openhab/voice/voicerss/internal/cloudapi/VoiceRSSCloudAPI.java
@@ -68,6 +68,8 @@ public interface VoiceRSSCloudAPI {
* the text to translate into speech
* @param locale
* the locale to use
+ * @param voice
+ * the voice to use, "default" for the default voice
* @param audioFormat
* the audio format to use
* @return an InputStream to the audio data in specified format
@@ -75,5 +77,6 @@ public interface VoiceRSSCloudAPI {
* 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;
+ InputStream getTextToSpeech(String apiKey, String text, String locale, String voice, String audioFormat)
+ throws IOException;
}
diff --git a/bundles/org.openhab.voice.voicerss/src/main/java/org/openhab/voice/voicerss/internal/cloudapi/VoiceRSSCloudImpl.java b/bundles/org.openhab.voice.voicerss/src/main/java/org/openhab/voice/voicerss/internal/cloudapi/VoiceRSSCloudImpl.java
index 4582d8b48e2b3..8d50281b96323 100644
--- a/bundles/org.openhab.voice.voicerss/src/main/java/org/openhab/voice/voicerss/internal/cloudapi/VoiceRSSCloudImpl.java
+++ b/bundles/org.openhab.voice.voicerss/src/main/java/org/openhab/voice/voicerss/internal/cloudapi/VoiceRSSCloudImpl.java
@@ -21,10 +21,11 @@
import java.net.URL;
import java.net.URLConnection;
import java.net.URLEncoder;
-import java.util.Collections;
+import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Locale;
+import java.util.Map;
import java.util.Map.Entry;
import java.util.Set;
import java.util.stream.Stream;
@@ -34,7 +35,7 @@
/**
* This class implements the Cloud service from VoiceRSS. For more information,
- * see API documentation at http://www.voicerss.org/api/documentation.aspx.
+ * see API documentation at http://www.voicerss.org/api .
*
* Current state of implementation:
*
@@ -50,6 +51,8 @@
*/
public class VoiceRSSCloudImpl implements VoiceRSSCloudAPI {
+ public static final String DEFAULT_VOICE = "default";
+
private final Logger logger = LoggerFactory.getLogger(VoiceRSSCloudImpl.class);
private static final Set SUPPORTED_AUDIO_FORMATS = Stream.of("MP3", "OGG", "AAC").collect(toSet());
@@ -63,8 +66,8 @@ public class VoiceRSSCloudImpl implements VoiceRSSCloudAPI {
SUPPORTED_LOCALES.add(Locale.forLanguageTag("cs-cz"));
SUPPORTED_LOCALES.add(Locale.forLanguageTag("da-dk"));
SUPPORTED_LOCALES.add(Locale.forLanguageTag("de-at"));
- SUPPORTED_LOCALES.add(Locale.forLanguageTag("de-ch"));
SUPPORTED_LOCALES.add(Locale.forLanguageTag("de-de"));
+ SUPPORTED_LOCALES.add(Locale.forLanguageTag("de-ch"));
SUPPORTED_LOCALES.add(Locale.forLanguageTag("el-gr"));
SUPPORTED_LOCALES.add(Locale.forLanguageTag("en-au"));
SUPPORTED_LOCALES.add(Locale.forLanguageTag("en-ca"));
@@ -76,8 +79,8 @@ public class VoiceRSSCloudImpl implements VoiceRSSCloudAPI {
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-ch"));
SUPPORTED_LOCALES.add(Locale.forLanguageTag("fr-fr"));
+ SUPPORTED_LOCALES.add(Locale.forLanguageTag("fr-ch"));
SUPPORTED_LOCALES.add(Locale.forLanguageTag("he-il"));
SUPPORTED_LOCALES.add(Locale.forLanguageTag("hi-in"));
SUPPORTED_LOCALES.add(Locale.forLanguageTag("hr-hr"));
@@ -107,7 +110,58 @@ public class VoiceRSSCloudImpl implements VoiceRSSCloudAPI {
SUPPORTED_LOCALES.add(Locale.forLanguageTag("zh-tw"));
}
- private static final Set SUPPORTED_VOICES = Collections.singleton("VoiceRSS");
+ private static final Map> SUPPORTED_VOICES = new HashMap<>();
+ static {
+ SUPPORTED_VOICES.put("ar-eg", Set.of("Oda"));
+ SUPPORTED_VOICES.put("ar-sa", Set.of("Salim"));
+ SUPPORTED_VOICES.put("bg-bg", Set.of("Dimo"));
+ SUPPORTED_VOICES.put("ca-es", Set.of("Rut"));
+ SUPPORTED_VOICES.put("cs-cz", Set.of("Josef"));
+ SUPPORTED_VOICES.put("da-dk", Set.of("Freja"));
+ SUPPORTED_VOICES.put("de-at", Set.of("Lukas"));
+ SUPPORTED_VOICES.put("de-de", Set.of("Hanna", "Lina", "Jonas"));
+ SUPPORTED_VOICES.put("de-ch", Set.of("Tim"));
+ SUPPORTED_VOICES.put("el-gr", Set.of("Neo"));
+ SUPPORTED_VOICES.put("en-au", Set.of("Zoe", "Isla", "Evie", "Jack"));
+ SUPPORTED_VOICES.put("en-ca", Set.of("Rose", "Clara", "Emma", "Mason"));
+ SUPPORTED_VOICES.put("en-gb", Set.of("Alice", "Nancy", "Lily", "Harry"));
+ SUPPORTED_VOICES.put("en-ie", Set.of("Oran"));
+ SUPPORTED_VOICES.put("en-in", Set.of("Eka", "Jai", "Ajit"));
+ SUPPORTED_VOICES.put("en-us", Set.of("Linda", "Amy", "Mary", "John", "Mike"));
+ SUPPORTED_VOICES.put("es-es", Set.of("Camila", "Sofia", "Luna", "Diego"));
+ SUPPORTED_VOICES.put("es-mx", Set.of("Juana", "Silvia", "Teresa", "Jose"));
+ SUPPORTED_VOICES.put("fi-fi", Set.of("Aada"));
+ SUPPORTED_VOICES.put("fr-ca", Set.of("Emile", "Olivia", "Logan", "Felix"));
+ SUPPORTED_VOICES.put("fr-fr", Set.of("Bette", "Iva", "Zola", "Axel"));
+ SUPPORTED_VOICES.put("fr-ch", Set.of("Theo"));
+ SUPPORTED_VOICES.put("he-il", Set.of("Rami"));
+ SUPPORTED_VOICES.put("hi-in", Set.of("Puja", "Kabir"));
+ SUPPORTED_VOICES.put("hr-hr", Set.of("Nikola"));
+ SUPPORTED_VOICES.put("hu-hu", Set.of("Mate"));
+ SUPPORTED_VOICES.put("id-id", Set.of("Intan"));
+ SUPPORTED_VOICES.put("it-it", Set.of("Bria", "Mia", "Pietro"));
+ SUPPORTED_VOICES.put("ja-jp", Set.of("Hina", "Airi", "Fumi", "Akira"));
+ SUPPORTED_VOICES.put("ko-kr", Set.of("Nari"));
+ SUPPORTED_VOICES.put("ms-my", Set.of("Aqil"));
+ SUPPORTED_VOICES.put("nb-no", Set.of("Marte", "Erik"));
+ SUPPORTED_VOICES.put("nl-be", Set.of("Daan"));
+ SUPPORTED_VOICES.put("nl-nl", Set.of("Lotte", "Bram"));
+ SUPPORTED_VOICES.put("pl-pl", Set.of("Julia", "Jan"));
+ SUPPORTED_VOICES.put("pt-br", Set.of("Marcia", "Ligia", "Yara", "Dinis"));
+ SUPPORTED_VOICES.put("pt-pt", Set.of("Leonor"));
+ SUPPORTED_VOICES.put("ro-ro", Set.of("Doru"));
+ SUPPORTED_VOICES.put("ru-ru", Set.of("Olga", "Marina", "Peter"));
+ SUPPORTED_VOICES.put("sk-sk", Set.of("Beda"));
+ SUPPORTED_VOICES.put("sl-si", Set.of("Vid"));
+ SUPPORTED_VOICES.put("sv-se", Set.of("Molly", "Hugo"));
+ SUPPORTED_VOICES.put("ta-in", Set.of("Sai"));
+ SUPPORTED_VOICES.put("th-th", Set.of("Ukrit"));
+ SUPPORTED_VOICES.put("tr-tr", Set.of("Omer"));
+ SUPPORTED_VOICES.put("vi-vn", Set.of("Chi"));
+ SUPPORTED_VOICES.put("zh-cn", Set.of("Luli", "Shu", "Chow", "Wang"));
+ SUPPORTED_VOICES.put("zh-hk", Set.of("Jia", "Xia", "Chen"));
+ SUPPORTED_VOICES.put("zh-tw", Set.of("Akemi", "Lin", "Lee"));
+ }
@Override
public Set getAvailableAudioFormats() {
@@ -121,17 +175,29 @@ public Set getAvailableLocales() {
@Override
public Set getAvailableVoices() {
- return SUPPORTED_VOICES;
+ // different locales support different voices, so let's list all here in one big set when no locale is provided
+ Set allvoxes = new HashSet<>();
+ allvoxes.add(DEFAULT_VOICE);
+ for (Set langvoxes : SUPPORTED_VOICES.values()) {
+ for (String langvox : langvoxes) {
+ allvoxes.add(langvox);
+ }
+ }
+ return allvoxes;
}
@Override
public Set getAvailableVoices(Locale locale) {
- for (Locale voiceLocale : SUPPORTED_LOCALES) {
- if (voiceLocale.toLanguageTag().equalsIgnoreCase(locale.toLanguageTag())) {
- return SUPPORTED_VOICES;
+ Set allvoxes = new HashSet<>();
+ allvoxes.add(DEFAULT_VOICE);
+ // all maps must be defined with key in lowercase
+ String langtag = locale.toLanguageTag().toLowerCase();
+ if (SUPPORTED_VOICES.containsKey(langtag)) {
+ for (String langvox : SUPPORTED_VOICES.get(langtag)) {
+ allvoxes.add(langvox);
}
}
- return new HashSet<>();
+ return allvoxes;
}
/**
@@ -142,9 +208,9 @@ public Set getAvailableVoices(Locale locale) {
* dependencies.
*/
@Override
- public InputStream getTextToSpeech(String apiKey, String text, String locale, String audioFormat)
+ public InputStream getTextToSpeech(String apiKey, String text, String locale, String voice, String audioFormat)
throws IOException {
- String url = createURL(apiKey, text, locale, audioFormat);
+ String url = createURL(apiKey, text, locale, voice, audioFormat);
logger.debug("Call {}", url);
URLConnection connection = new URL(url).openConnection();
@@ -188,7 +254,7 @@ public InputStream getTextToSpeech(String apiKey, String text, String locale, St
*
* It is in package scope to be accessed by tests.
*/
- private String createURL(String apiKey, String text, String locale, String audioFormat) {
+ private String createURL(String apiKey, String text, String locale, String voice, String audioFormat) {
String encodedMsg;
try {
encodedMsg = URLEncoder.encode(text, "UTF-8");
@@ -197,7 +263,11 @@ private String createURL(String apiKey, String text, String locale, String audio
// 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;
+ String url = "http://api.voicerss.org/?key=" + apiKey + "&hl=" + locale + "&c=" + audioFormat;
+ if (!DEFAULT_VOICE.equals(voice)) {
+ url += "&v=" + voice;
+ }
+ url += "&f=44khz_16bit_mono&src=" + encodedMsg;
+ return url;
}
}
diff --git a/bundles/org.openhab.voice.voicerss/src/main/java/org/openhab/voice/voicerss/tool/CreateTTSCache.java b/bundles/org.openhab.voice.voicerss/src/main/java/org/openhab/voice/voicerss/tool/CreateTTSCache.java
index cb3677490e279..0f7cd5e38e91f 100644
--- a/bundles/org.openhab.voice.voicerss/src/main/java/org/openhab/voice/voicerss/tool/CreateTTSCache.java
+++ b/bundles/org.openhab.voice.voicerss/src/main/java/org/openhab/voice/voicerss/tool/CreateTTSCache.java
@@ -49,18 +49,19 @@ public int doMain(String[] args) throws IOException {
String apiKey = args[1];
String cacheDir = args[2];
String locale = args[3];
- if (args[4].startsWith("@")) {
- String inputFileName = args[4].substring(1);
+ String voice = args[4];
+ if (args[5].startsWith("@")) {
+ String inputFileName = args[5].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);
+ generateCacheForFile(apiKey, cacheDir, locale, voice, inputFileName);
} else {
- String text = args[4];
- generateCacheForMessage(apiKey, cacheDir, locale, text);
+ String text = args[5];
+ generateCacheForMessage(apiKey, cacheDir, locale, voice, text);
}
return RC_OK;
}
@@ -71,6 +72,7 @@ private void usage() {
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(" voice the voice, \"default\" for the default voice");
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\"");
@@ -80,19 +82,20 @@ private void usage() {
System.out.println();
}
- private void generateCacheForFile(String apiKey, String cacheDir, String locale, String inputFileName)
+ private void generateCacheForFile(String apiKey, String cacheDir, String locale, String voice, 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);
+ generateCacheForMessage(apiKey, cacheDir, locale, voice, line);
}
}
}
- private void generateCacheForMessage(String apiKey, String cacheDir, String locale, String msg) throws IOException {
+ private void generateCacheForMessage(String apiKey, String cacheDir, String locale, String voice, String msg)
+ throws IOException {
if (msg == null) {
System.err.println("Ignore msg=null");
return;
@@ -103,7 +106,7 @@ private void generateCacheForMessage(String apiKey, String cacheDir, String loca
return;
}
CachedVoiceRSSCloudImpl impl = new CachedVoiceRSSCloudImpl(cacheDir);
- File cachedFile = impl.getTextToSpeechAsFile(apiKey, trimmedMsg, locale, "MP3");
+ File cachedFile = impl.getTextToSpeechAsFile(apiKey, trimmedMsg, locale, voice, "MP3");
System.out.println(
"Created cached audio for locale='" + locale + "', msg='" + trimmedMsg + "' to file=" + cachedFile);
}