]> git.basschouten.com Git - openhab-addons.git/commitdiff
[voicerss] Add support for voices (#10184)
authorlolodomo <lg.hc@free.fr>
Sat, 20 Feb 2021 16:21:52 +0000 (17:21 +0100)
committerGitHub <noreply@github.com>
Sat, 20 Feb 2021 16:21:52 +0000 (17:21 +0100)
Signed-off-by: Laurent Garnier <lg.hc@free.fr>
bundles/org.openhab.voice.voicerss/src/main/java/org/openhab/voice/voicerss/internal/VoiceRSSTTSService.java
bundles/org.openhab.voice.voicerss/src/main/java/org/openhab/voice/voicerss/internal/VoiceRSSVoice.java
bundles/org.openhab.voice.voicerss/src/main/java/org/openhab/voice/voicerss/internal/cloudapi/CachedVoiceRSSCloudImpl.java
bundles/org.openhab.voice.voicerss/src/main/java/org/openhab/voice/voicerss/internal/cloudapi/VoiceRSSCloudAPI.java
bundles/org.openhab.voice.voicerss/src/main/java/org/openhab/voice/voicerss/internal/cloudapi/VoiceRSSCloudImpl.java
bundles/org.openhab.voice.voicerss/src/main/java/org/openhab/voice/voicerss/tool/CreateTTSCache.java

index a9d9110e5d9496996fa28a6bcac25a6d6c7562c3..6d5475468e961679bcaf1bda5d9eefc4437fe982 100644 (file)
@@ -136,7 +136,7 @@ public class VoiceRSSTTSService implements TTSService {
         // 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");
             }
index ef3f82387066ceb536316472d73a815e1d0e820a..02d33f8502550ae71b24c98a3437dfba3315f9e4 100644 (file)
@@ -15,6 +15,7 @@ package org.openhab.voice.voicerss.internal;
 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 class VoiceRSSVoice implements Voice {
      */
     @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;
     }
 
     /**
index 71a16d1ee053ec9c9b1c655685544f976ddc1dbe..b94f07cf4793ae710e995b8f6f522cab71d9a329 100644 (file)
@@ -55,9 +55,9 @@ public class CachedVoiceRSSCloudImpl extends VoiceRSSCloudImpl {
         }
     }
 
-    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 class CachedVoiceRSSCloudImpl extends VoiceRSSCloudImpl {
         }
 
         // 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 class CachedVoiceRSSCloudImpl extends VoiceRSSCloudImpl {
      *
      * 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 @@ public class CachedVoiceRSSCloudImpl extends VoiceRSSCloudImpl {
             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);
index 8a7d4b186856b9a755d5b20db0b2a25ecbe978ba..175f8cfa625f1a1266ed29dc6b3c9b0cec0ce73e 100644 (file)
@@ -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;
 }
index 4582d8b48e2b3727d3c7222e014f62b0f5310a78..8d50281b9632369d93e2c367ae50e54f19660bf3 100644 (file)
@@ -21,10 +21,11 @@ import java.net.HttpURLConnection;
 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 @@ 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.
+ * see API documentation at http://www.voicerss.org/api .
  *
  * Current state of implementation:
  * <ul>
@@ -50,6 +51,8 @@ import org.slf4j.LoggerFactory;
  */
 public class VoiceRSSCloudImpl implements VoiceRSSCloudAPI {
 
+    public static final String DEFAULT_VOICE = "default";
+
     private final Logger logger = LoggerFactory.getLogger(VoiceRSSCloudImpl.class);
 
     private static final Set<String> 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<String> SUPPORTED_VOICES = Collections.singleton("VoiceRSS");
+    private static final Map<String, Set<String>> 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<String> getAvailableAudioFormats() {
@@ -121,17 +175,29 @@ public class VoiceRSSCloudImpl implements VoiceRSSCloudAPI {
 
     @Override
     public Set<String> 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<String> allvoxes = new HashSet<>();
+        allvoxes.add(DEFAULT_VOICE);
+        for (Set<String> langvoxes : SUPPORTED_VOICES.values()) {
+            for (String langvox : langvoxes) {
+                allvoxes.add(langvox);
+            }
+        }
+        return allvoxes;
     }
 
     @Override
     public Set<String> getAvailableVoices(Locale locale) {
-        for (Locale voiceLocale : SUPPORTED_LOCALES) {
-            if (voiceLocale.toLanguageTag().equalsIgnoreCase(locale.toLanguageTag())) {
-                return SUPPORTED_VOICES;
+        Set<String> 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 class VoiceRSSCloudImpl implements VoiceRSSCloudAPI {
      * 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 class VoiceRSSCloudImpl implements VoiceRSSCloudAPI {
      *
      * 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 @@ public class VoiceRSSCloudImpl implements VoiceRSSCloudAPI {
             // 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;
     }
 }
index cb3677490e279efecb19e3cfbbbc0b84cc6d8028..0f7cd5e38e91fc975a4f8d2bfbbfaaa7cd679834 100644 (file)
@@ -49,18 +49,19 @@ public class CreateTTSCache {
         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 @@ public class CreateTTSCache {
         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 @@ public class CreateTTSCache {
         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 @@ public class CreateTTSCache {
             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);
     }