mirror of
https://github.com/openhab/openhab-addons.git
synced 2025-01-25 14:55:55 +01:00
[voicerss] Add support for voices (#10184)
Signed-off-by: Laurent Garnier <lg.hc@free.fr>
This commit is contained in:
parent
fd1c96677e
commit
17f7041524
@ -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");
|
||||
}
|
||||
|
@ -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;
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -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);
|
||||
|
@ -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;
|
||||
}
|
||||
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
@ -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);
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user