[voicerss] Fix and enhance the external tool to prefill the cache (#12155)
* [voicerss] Fix and enhance the external tool to prefill the cache * Make the tool independent of core openHAB * Fix the parsing of the command arguments. * Allow to provide optional audio codec and format. * Null annotations added on most of the classes * Simplified doc about classpath * Suppress obsolete mention of open issue in doc Signed-off-by: Laurent Garnier <lg.hc@free.fr>
This commit is contained in:
parent
de6de1a22d
commit
624efab678
@ -177,17 +177,17 @@ Synopsis of this tool:
|
|||||||
|
|
||||||
```
|
```
|
||||||
Usage: java org.openhab.voice.voicerss.tool.CreateTTSCache <args>
|
Usage: java org.openhab.voice.voicerss.tool.CreateTTSCache <args>
|
||||||
Arguments: --api-key <key> <cache-dir> <locale> { <text> | @inputfile }
|
Arguments: --api-key <key> <cache-dir> <locale> <voice> { <text> | @inputfile } [ <codec> <format> ]
|
||||||
key the VoiceRSS API Key, e.g. "123456789"
|
key the VoiceRSS API Key, e.g. "123456789"
|
||||||
cache-dir is directory where the files will be stored, e.g. "voicerss-cache"
|
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"
|
locale the language locale, has to be valid, e.g. "en-us", "de-de"
|
||||||
|
voice the voice, "default" for the default voice
|
||||||
text the text to create audio file for, e.g. "Hello World"
|
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"
|
inputfile a name of a file, where all lines will be translatet to text, e.g. "@message.txt"
|
||||||
|
codec the audio codec, "MP3", "WAV", "OGG" or "AAC", "MP3" by default
|
||||||
|
format the audio format, "44khz_16bit_mono" by default
|
||||||
|
|
||||||
Sample: java org.openhab.voice.voicerss.tool.CreateTTSCache --api-key 1234567890 cache en-US @messages.txt
|
Sample: java org.openhab.voice.voicerss.tool.CreateTTSCache --api-key 1234567890 cache en-US default @messages.txt
|
||||||
```
|
```
|
||||||
|
|
||||||
|
You will need to specify the classpath for your addon (jar) in the command line (java -cp <path> ...).
|
||||||
## Open Issues
|
|
||||||
|
|
||||||
* do not log API-Key in plain text
|
|
||||||
|
|||||||
@ -14,6 +14,7 @@ package org.openhab.voice.voicerss.internal;
|
|||||||
|
|
||||||
import java.io.File;
|
import java.io.File;
|
||||||
|
|
||||||
|
import org.eclipse.jdt.annotation.NonNullByDefault;
|
||||||
import org.openhab.core.audio.AudioException;
|
import org.openhab.core.audio.AudioException;
|
||||||
import org.openhab.core.audio.AudioFormat;
|
import org.openhab.core.audio.AudioFormat;
|
||||||
import org.openhab.core.audio.AudioStream;
|
import org.openhab.core.audio.AudioStream;
|
||||||
@ -27,6 +28,7 @@ import org.openhab.core.audio.FileAudioStream;
|
|||||||
*
|
*
|
||||||
* @author Jochen Hiller - Initial contribution and API
|
* @author Jochen Hiller - Initial contribution and API
|
||||||
*/
|
*/
|
||||||
|
@NonNullByDefault
|
||||||
class VoiceRSSAudioStream extends FileAudioStream {
|
class VoiceRSSAudioStream extends FileAudioStream {
|
||||||
|
|
||||||
public VoiceRSSAudioStream(File audioFile, AudioFormat format) throws AudioException {
|
public VoiceRSSAudioStream(File audioFile, AudioFormat format) throws AudioException {
|
||||||
|
|||||||
@ -170,7 +170,82 @@ public class VoiceRSSTTSService implements TTSService {
|
|||||||
* @return The audio formats of this instance
|
* @return The audio formats of this instance
|
||||||
*/
|
*/
|
||||||
private Set<AudioFormat> initAudioFormats() {
|
private Set<AudioFormat> initAudioFormats() {
|
||||||
return voiceRssImpl.getAvailableAudioFormats();
|
Set<AudioFormat> audioFormats = new HashSet<>();
|
||||||
|
for (String codec : voiceRssImpl.getAvailableAudioCodecs()) {
|
||||||
|
switch (codec) {
|
||||||
|
case "MP3":
|
||||||
|
audioFormats.add(new AudioFormat(AudioFormat.CONTAINER_NONE, AudioFormat.CODEC_MP3, null, 16, 64000,
|
||||||
|
44_100L));
|
||||||
|
break;
|
||||||
|
case "OGG":
|
||||||
|
audioFormats.add(new AudioFormat(AudioFormat.CONTAINER_OGG, AudioFormat.CODEC_VORBIS, null, 16,
|
||||||
|
null, 44_100L));
|
||||||
|
break;
|
||||||
|
case "AAC":
|
||||||
|
audioFormats.add(new AudioFormat(AudioFormat.CONTAINER_NONE, AudioFormat.CODEC_AAC, null, 16, null,
|
||||||
|
44_100L));
|
||||||
|
break;
|
||||||
|
case "WAV":
|
||||||
|
// Consider only mono formats
|
||||||
|
audioFormats.add(new AudioFormat(AudioFormat.CONTAINER_WAVE, AudioFormat.CODEC_PCM_UNSIGNED, false,
|
||||||
|
8, 64_000, 8_000L));
|
||||||
|
audioFormats.add(new AudioFormat(AudioFormat.CONTAINER_WAVE, AudioFormat.CODEC_PCM_SIGNED, false,
|
||||||
|
16, 128_000, 8_000L));
|
||||||
|
audioFormats.add(new AudioFormat(AudioFormat.CONTAINER_WAVE, AudioFormat.CODEC_PCM_UNSIGNED, false,
|
||||||
|
8, 88_200, 11_025L));
|
||||||
|
audioFormats.add(new AudioFormat(AudioFormat.CONTAINER_WAVE, AudioFormat.CODEC_PCM_SIGNED, false,
|
||||||
|
16, 176_400, 11_025L));
|
||||||
|
audioFormats.add(new AudioFormat(AudioFormat.CONTAINER_WAVE, AudioFormat.CODEC_PCM_UNSIGNED, false,
|
||||||
|
8, 96_000, 12_000L));
|
||||||
|
audioFormats.add(new AudioFormat(AudioFormat.CONTAINER_WAVE, AudioFormat.CODEC_PCM_SIGNED, false,
|
||||||
|
16, 192_000, 12_000L));
|
||||||
|
audioFormats.add(new AudioFormat(AudioFormat.CONTAINER_WAVE, AudioFormat.CODEC_PCM_UNSIGNED, false,
|
||||||
|
8, 128_000, 16_000L));
|
||||||
|
audioFormats.add(new AudioFormat(AudioFormat.CONTAINER_WAVE, AudioFormat.CODEC_PCM_SIGNED, false,
|
||||||
|
16, 256_000, 16_000L));
|
||||||
|
audioFormats.add(new AudioFormat(AudioFormat.CONTAINER_WAVE, AudioFormat.CODEC_PCM_UNSIGNED, false,
|
||||||
|
8, 176_400, 22_050L));
|
||||||
|
audioFormats.add(new AudioFormat(AudioFormat.CONTAINER_WAVE, AudioFormat.CODEC_PCM_SIGNED, false,
|
||||||
|
16, 352_800, 22_050L));
|
||||||
|
audioFormats.add(new AudioFormat(AudioFormat.CONTAINER_WAVE, AudioFormat.CODEC_PCM_UNSIGNED, false,
|
||||||
|
8, 192_000, 24_000L));
|
||||||
|
audioFormats.add(new AudioFormat(AudioFormat.CONTAINER_WAVE, AudioFormat.CODEC_PCM_SIGNED, false,
|
||||||
|
16, 384_000, 24_000L));
|
||||||
|
audioFormats.add(new AudioFormat(AudioFormat.CONTAINER_WAVE, AudioFormat.CODEC_PCM_UNSIGNED, false,
|
||||||
|
8, 256_000, 32_000L));
|
||||||
|
audioFormats.add(new AudioFormat(AudioFormat.CONTAINER_WAVE, AudioFormat.CODEC_PCM_SIGNED, false,
|
||||||
|
16, 512_000, 32_000L));
|
||||||
|
audioFormats.add(new AudioFormat(AudioFormat.CONTAINER_WAVE, AudioFormat.CODEC_PCM_UNSIGNED, false,
|
||||||
|
8, 352_800, 44_100L));
|
||||||
|
audioFormats.add(new AudioFormat(AudioFormat.CONTAINER_WAVE, AudioFormat.CODEC_PCM_SIGNED, false,
|
||||||
|
16, 705_600, 44_100L));
|
||||||
|
audioFormats.add(new AudioFormat(AudioFormat.CONTAINER_WAVE, AudioFormat.CODEC_PCM_UNSIGNED, false,
|
||||||
|
8, 384_000, 48_000L));
|
||||||
|
audioFormats.add(new AudioFormat(AudioFormat.CONTAINER_WAVE, AudioFormat.CODEC_PCM_SIGNED, false,
|
||||||
|
16, 768_000, 48_000L));
|
||||||
|
audioFormats.add(new AudioFormat(AudioFormat.CONTAINER_WAVE, AudioFormat.CODEC_PCM_ALAW, null, 8,
|
||||||
|
64_000, 8_000L));
|
||||||
|
audioFormats.add(new AudioFormat(AudioFormat.CONTAINER_WAVE, AudioFormat.CODEC_PCM_ALAW, null, 8,
|
||||||
|
88_200, 11_025L));
|
||||||
|
audioFormats.add(new AudioFormat(AudioFormat.CONTAINER_WAVE, AudioFormat.CODEC_PCM_ALAW, null, 8,
|
||||||
|
176_400, 22_050L));
|
||||||
|
audioFormats.add(new AudioFormat(AudioFormat.CONTAINER_WAVE, AudioFormat.CODEC_PCM_ALAW, null, 8,
|
||||||
|
352_800, 44_100L));
|
||||||
|
audioFormats.add(new AudioFormat(AudioFormat.CONTAINER_WAVE, AudioFormat.CODEC_PCM_ULAW, null, 8,
|
||||||
|
64_000, 8_000L));
|
||||||
|
audioFormats.add(new AudioFormat(AudioFormat.CONTAINER_WAVE, AudioFormat.CODEC_PCM_ULAW, null, 8,
|
||||||
|
88_200, 11_025L));
|
||||||
|
audioFormats.add(new AudioFormat(AudioFormat.CONTAINER_WAVE, AudioFormat.CODEC_PCM_ULAW, null, 8,
|
||||||
|
176_400, 22_050L));
|
||||||
|
audioFormats.add(new AudioFormat(AudioFormat.CONTAINER_WAVE, AudioFormat.CODEC_PCM_ULAW, null, 8,
|
||||||
|
352_800, 44_100L));
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
logger.debug("Audio codec {} not yet supported", codec);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return audioFormats;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -221,7 +296,7 @@ public class VoiceRSSTTSService implements TTSService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private CachedVoiceRSSCloudImpl initVoiceImplementation() throws IllegalStateException {
|
private CachedVoiceRSSCloudImpl initVoiceImplementation() throws IllegalStateException {
|
||||||
return new CachedVoiceRSSCloudImpl(getCacheFolderName());
|
return new CachedVoiceRSSCloudImpl(getCacheFolderName(), true);
|
||||||
}
|
}
|
||||||
|
|
||||||
private String getCacheFolderName() {
|
private String getCacheFolderName() {
|
||||||
|
|||||||
@ -14,6 +14,7 @@ package org.openhab.voice.voicerss.internal;
|
|||||||
|
|
||||||
import java.util.Locale;
|
import java.util.Locale;
|
||||||
|
|
||||||
|
import org.eclipse.jdt.annotation.NonNullByDefault;
|
||||||
import org.openhab.core.voice.Voice;
|
import org.openhab.core.voice.Voice;
|
||||||
import org.openhab.voice.voicerss.internal.cloudapi.VoiceRSSCloudImpl;
|
import org.openhab.voice.voicerss.internal.cloudapi.VoiceRSSCloudImpl;
|
||||||
|
|
||||||
@ -23,6 +24,7 @@ import org.openhab.voice.voicerss.internal.cloudapi.VoiceRSSCloudImpl;
|
|||||||
*
|
*
|
||||||
* @author Jochen Hiller - Initial contribution and API
|
* @author Jochen Hiller - Initial contribution and API
|
||||||
*/
|
*/
|
||||||
|
@NonNullByDefault
|
||||||
public class VoiceRSSVoice implements Voice {
|
public class VoiceRSSVoice implements Voice {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@ -23,7 +23,8 @@ import java.security.MessageDigest;
|
|||||||
import java.security.NoSuchAlgorithmException;
|
import java.security.NoSuchAlgorithmException;
|
||||||
import java.util.Objects;
|
import java.util.Objects;
|
||||||
|
|
||||||
import org.slf4j.Logger;
|
import org.eclipse.jdt.annotation.NonNullByDefault;
|
||||||
|
import org.eclipse.jdt.annotation.Nullable;
|
||||||
import org.slf4j.LoggerFactory;
|
import org.slf4j.LoggerFactory;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -33,6 +34,7 @@ import org.slf4j.LoggerFactory;
|
|||||||
*
|
*
|
||||||
* @author Jochen Hiller - Initial contribution
|
* @author Jochen Hiller - Initial contribution
|
||||||
*/
|
*/
|
||||||
|
@NonNullByDefault
|
||||||
public class CachedVoiceRSSCloudImpl extends VoiceRSSCloudImpl {
|
public class CachedVoiceRSSCloudImpl extends VoiceRSSCloudImpl {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -40,12 +42,11 @@ public class CachedVoiceRSSCloudImpl extends VoiceRSSCloudImpl {
|
|||||||
*/
|
*/
|
||||||
private static final int READ_BUFFER_SIZE = 4096;
|
private static final int READ_BUFFER_SIZE = 4096;
|
||||||
|
|
||||||
private final Logger logger = LoggerFactory.getLogger(CachedVoiceRSSCloudImpl.class);
|
|
||||||
|
|
||||||
private final File cacheFolder;
|
private final File cacheFolder;
|
||||||
|
|
||||||
public CachedVoiceRSSCloudImpl(String cacheFolderName) throws IllegalStateException {
|
public CachedVoiceRSSCloudImpl(String cacheFolderName, boolean logging) throws IllegalStateException {
|
||||||
if (cacheFolderName == null) {
|
super(logging);
|
||||||
|
if (cacheFolderName.isBlank()) {
|
||||||
throw new IllegalStateException("Folder for cache must be defined");
|
throw new IllegalStateException("Folder for cache must be defined");
|
||||||
}
|
}
|
||||||
// Lazy create the cache folder
|
// Lazy create the cache folder
|
||||||
@ -89,7 +90,7 @@ public class CachedVoiceRSSCloudImpl extends VoiceRSSCloudImpl {
|
|||||||
*
|
*
|
||||||
* Sample: "en-US_00a2653ac5f77063bc4ea2fee87318d3"
|
* Sample: "en-US_00a2653ac5f77063bc4ea2fee87318d3"
|
||||||
*/
|
*/
|
||||||
private String getUniqueFilenameForText(String text, String locale, String voice, String format) {
|
private @Nullable String getUniqueFilenameForText(String text, String locale, String voice, String format) {
|
||||||
try {
|
try {
|
||||||
byte[] bytesOfMessage = text.getBytes(StandardCharsets.UTF_8);
|
byte[] bytesOfMessage = text.getBytes(StandardCharsets.UTF_8);
|
||||||
MessageDigest md = MessageDigest.getInstance("MD5");
|
MessageDigest md = MessageDigest.getInstance("MD5");
|
||||||
@ -112,7 +113,10 @@ public class CachedVoiceRSSCloudImpl extends VoiceRSSCloudImpl {
|
|||||||
return filename;
|
return filename;
|
||||||
} catch (NoSuchAlgorithmException ex) {
|
} catch (NoSuchAlgorithmException ex) {
|
||||||
// should not happen
|
// should not happen
|
||||||
logger.error("Could not create MD5 hash for '{}'", text, ex);
|
if (logging) {
|
||||||
|
LoggerFactory.getLogger(CachedVoiceRSSCloudImpl.class).error("Could not create MD5 hash for '{}'", text,
|
||||||
|
ex);
|
||||||
|
}
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -17,7 +17,7 @@ import java.io.InputStream;
|
|||||||
import java.util.Locale;
|
import java.util.Locale;
|
||||||
import java.util.Set;
|
import java.util.Set;
|
||||||
|
|
||||||
import org.openhab.core.audio.AudioFormat;
|
import org.eclipse.jdt.annotation.NonNullByDefault;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Interface which represents the functionality needed from the VoiceRSS TTS
|
* Interface which represents the functionality needed from the VoiceRSS TTS
|
||||||
@ -25,6 +25,7 @@ import org.openhab.core.audio.AudioFormat;
|
|||||||
*
|
*
|
||||||
* @author Jochen Hiller - Initial contribution
|
* @author Jochen Hiller - Initial contribution
|
||||||
*/
|
*/
|
||||||
|
@NonNullByDefault
|
||||||
public interface VoiceRSSCloudAPI {
|
public interface VoiceRSSCloudAPI {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -35,13 +36,12 @@ public interface VoiceRSSCloudAPI {
|
|||||||
Set<Locale> getAvailableLocales();
|
Set<Locale> getAvailableLocales();
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get all supported audio formats by the TTS service. This includes MP3,
|
* Get all supported audio codecs by the TTS service. This includes MP3,
|
||||||
* WAV and more audio formats as used in APIs. About supported audio
|
* WAV and more audio formats as used in APIs.
|
||||||
* formats, see {@link AudioFormat}
|
|
||||||
*
|
*
|
||||||
* @return A set of all audio formats supported
|
* @return A set of all audio codecs supported
|
||||||
*/
|
*/
|
||||||
Set<AudioFormat> getAvailableAudioFormats();
|
Set<String> getAvailableAudioCodecs();
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get all supported voices.
|
* Get all supported voices.
|
||||||
|
|||||||
@ -27,7 +27,7 @@ import java.util.Map;
|
|||||||
import java.util.Map.Entry;
|
import java.util.Map.Entry;
|
||||||
import java.util.Set;
|
import java.util.Set;
|
||||||
|
|
||||||
import org.openhab.core.audio.AudioFormat;
|
import org.eclipse.jdt.annotation.NonNullByDefault;
|
||||||
import org.slf4j.Logger;
|
import org.slf4j.Logger;
|
||||||
import org.slf4j.LoggerFactory;
|
import org.slf4j.LoggerFactory;
|
||||||
|
|
||||||
@ -48,6 +48,7 @@ import org.slf4j.LoggerFactory;
|
|||||||
* @author Laurent Garnier - add support for OGG and AAC audio formats
|
* @author Laurent Garnier - add support for OGG and AAC audio formats
|
||||||
* @author Andreas Brenk - add support for WAV audio format
|
* @author Andreas Brenk - add support for WAV audio format
|
||||||
*/
|
*/
|
||||||
|
@NonNullByDefault
|
||||||
public class VoiceRSSCloudImpl implements VoiceRSSCloudAPI {
|
public class VoiceRSSCloudImpl implements VoiceRSSCloudAPI {
|
||||||
|
|
||||||
public static final String DEFAULT_VOICE = "default";
|
public static final String DEFAULT_VOICE = "default";
|
||||||
@ -55,36 +56,7 @@ public class VoiceRSSCloudImpl implements VoiceRSSCloudAPI {
|
|||||||
public static final String API_URL = "https://api.voicerss.org/?key=%s&hl=%s&c=%s&f=%s&src=%s";
|
public static final String API_URL = "https://api.voicerss.org/?key=%s&hl=%s&c=%s&f=%s&src=%s";
|
||||||
public static final String API_URL_WITH_VOICE = API_URL + "&v=%s";
|
public static final String API_URL_WITH_VOICE = API_URL + "&v=%s";
|
||||||
|
|
||||||
private static final Set<AudioFormat> SUPPORTED_AUDIO_FORMATS = Set.of(
|
private static final Set<String> SUPPORTED_AUDIO_CODECS = Set.of("MP3", "OGG", "AAC", "WAV", "CAF");
|
||||||
new AudioFormat(AudioFormat.CONTAINER_NONE, AudioFormat.CODEC_MP3, null, 16, null, 44_100L),
|
|
||||||
new AudioFormat(AudioFormat.CONTAINER_OGG, AudioFormat.CODEC_VORBIS, null, 16, null, 44_100L),
|
|
||||||
new AudioFormat(AudioFormat.CONTAINER_NONE, AudioFormat.CODEC_AAC, null, 16, null, 44_100L),
|
|
||||||
new AudioFormat(AudioFormat.CONTAINER_WAVE, AudioFormat.CODEC_PCM_UNSIGNED, null, 8, 64_000, 8_000L),
|
|
||||||
new AudioFormat(AudioFormat.CONTAINER_WAVE, AudioFormat.CODEC_PCM_SIGNED, null, 16, 128_000, 8_000L),
|
|
||||||
new AudioFormat(AudioFormat.CONTAINER_WAVE, AudioFormat.CODEC_PCM_UNSIGNED, false, 8, 88_200, 11_025L),
|
|
||||||
new AudioFormat(AudioFormat.CONTAINER_WAVE, AudioFormat.CODEC_PCM_SIGNED, false, 16, 176_400, 11_025L),
|
|
||||||
new AudioFormat(AudioFormat.CONTAINER_WAVE, AudioFormat.CODEC_PCM_UNSIGNED, false, 8, 96_000, 12_000L),
|
|
||||||
new AudioFormat(AudioFormat.CONTAINER_WAVE, AudioFormat.CODEC_PCM_SIGNED, false, 16, 192_000, 12_000L),
|
|
||||||
new AudioFormat(AudioFormat.CONTAINER_WAVE, AudioFormat.CODEC_PCM_UNSIGNED, false, 8, 128_000, 16_000L),
|
|
||||||
new AudioFormat(AudioFormat.CONTAINER_WAVE, AudioFormat.CODEC_PCM_SIGNED, false, 16, 256_000, 16_000L),
|
|
||||||
new AudioFormat(AudioFormat.CONTAINER_WAVE, AudioFormat.CODEC_PCM_UNSIGNED, false, 8, 176_400, 22_050L),
|
|
||||||
new AudioFormat(AudioFormat.CONTAINER_WAVE, AudioFormat.CODEC_PCM_SIGNED, false, 16, 352_800, 22_050L),
|
|
||||||
new AudioFormat(AudioFormat.CONTAINER_WAVE, AudioFormat.CODEC_PCM_UNSIGNED, false, 8, 192_000, 24_000L),
|
|
||||||
new AudioFormat(AudioFormat.CONTAINER_WAVE, AudioFormat.CODEC_PCM_SIGNED, false, 16, 384_000, 24_000L),
|
|
||||||
new AudioFormat(AudioFormat.CONTAINER_WAVE, AudioFormat.CODEC_PCM_UNSIGNED, false, 8, 256_000, 32_000L),
|
|
||||||
new AudioFormat(AudioFormat.CONTAINER_WAVE, AudioFormat.CODEC_PCM_SIGNED, false, 16, 512_000, 32_000L),
|
|
||||||
new AudioFormat(AudioFormat.CONTAINER_WAVE, AudioFormat.CODEC_PCM_UNSIGNED, false, 8, 352_800, 44_100L),
|
|
||||||
new AudioFormat(AudioFormat.CONTAINER_WAVE, AudioFormat.CODEC_PCM_SIGNED, false, 16, 705_600, 44_100L),
|
|
||||||
new AudioFormat(AudioFormat.CONTAINER_WAVE, AudioFormat.CODEC_PCM_UNSIGNED, false, 8, 384_000, 48_000L),
|
|
||||||
new AudioFormat(AudioFormat.CONTAINER_WAVE, AudioFormat.CODEC_PCM_SIGNED, false, 16, 768_000, 48_000L),
|
|
||||||
new AudioFormat(AudioFormat.CONTAINER_WAVE, AudioFormat.CODEC_PCM_ALAW, null, 8, 64_000, 8_000L),
|
|
||||||
new AudioFormat(AudioFormat.CONTAINER_WAVE, AudioFormat.CODEC_PCM_ALAW, null, 8, 88_200, 11_025L),
|
|
||||||
new AudioFormat(AudioFormat.CONTAINER_WAVE, AudioFormat.CODEC_PCM_ALAW, null, 8, 176_400, 22_050L),
|
|
||||||
new AudioFormat(AudioFormat.CONTAINER_WAVE, AudioFormat.CODEC_PCM_ALAW, null, 8, 352_800, 44_100L),
|
|
||||||
new AudioFormat(AudioFormat.CONTAINER_WAVE, AudioFormat.CODEC_PCM_ULAW, null, 8, 64_000, 8_000L),
|
|
||||||
new AudioFormat(AudioFormat.CONTAINER_WAVE, AudioFormat.CODEC_PCM_ULAW, null, 8, 88_200, 11_025L),
|
|
||||||
new AudioFormat(AudioFormat.CONTAINER_WAVE, AudioFormat.CODEC_PCM_ULAW, null, 8, 176_400, 22_050L),
|
|
||||||
new AudioFormat(AudioFormat.CONTAINER_WAVE, AudioFormat.CODEC_PCM_ULAW, null, 8, 352_800, 44_100L));
|
|
||||||
|
|
||||||
private static final Set<Locale> SUPPORTED_LOCALES = new HashSet<>();
|
private static final Set<Locale> SUPPORTED_LOCALES = new HashSet<>();
|
||||||
static {
|
static {
|
||||||
@ -192,11 +164,15 @@ public class VoiceRSSCloudImpl implements VoiceRSSCloudAPI {
|
|||||||
SUPPORTED_VOICES.put("zh-tw", Set.of("Akemi", "Lin", "Lee"));
|
SUPPORTED_VOICES.put("zh-tw", Set.of("Akemi", "Lin", "Lee"));
|
||||||
}
|
}
|
||||||
|
|
||||||
private final Logger logger = LoggerFactory.getLogger(VoiceRSSCloudImpl.class);
|
protected boolean logging;
|
||||||
|
|
||||||
|
public VoiceRSSCloudImpl(boolean logging) {
|
||||||
|
this.logging = logging;
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public Set<AudioFormat> getAvailableAudioFormats() {
|
public Set<String> getAvailableAudioCodecs() {
|
||||||
return SUPPORTED_AUDIO_FORMATS;
|
return SUPPORTED_AUDIO_CODECS;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@ -242,7 +218,9 @@ public class VoiceRSSCloudImpl implements VoiceRSSCloudAPI {
|
|||||||
public InputStream getTextToSpeech(String apiKey, String text, String locale, String voice, String audioCodec,
|
public InputStream getTextToSpeech(String apiKey, String text, String locale, String voice, String audioCodec,
|
||||||
String audioFormat) throws IOException {
|
String audioFormat) throws IOException {
|
||||||
String url = createURL(apiKey, text, locale, voice, audioCodec, audioFormat);
|
String url = createURL(apiKey, text, locale, voice, audioCodec, audioFormat);
|
||||||
logger.debug("Call {}", url.replace(apiKey, "***"));
|
if (logging) {
|
||||||
|
LoggerFactory.getLogger(VoiceRSSCloudImpl.class).debug("Call {}", url.replace(apiKey, "***"));
|
||||||
|
}
|
||||||
URLConnection connection = new URL(url).openConnection();
|
URLConnection connection = new URL(url).openConnection();
|
||||||
|
|
||||||
// we will check return codes. The service will ALWAYS return a HTTP
|
// we will check return codes. The service will ALWAYS return a HTTP
|
||||||
@ -250,12 +228,18 @@ public class VoiceRSSCloudImpl implements VoiceRSSCloudAPI {
|
|||||||
// the error message in body
|
// the error message in body
|
||||||
int status = ((HttpURLConnection) connection).getResponseCode();
|
int status = ((HttpURLConnection) connection).getResponseCode();
|
||||||
if (HttpURLConnection.HTTP_OK != status) {
|
if (HttpURLConnection.HTTP_OK != status) {
|
||||||
logger.warn("Call {} returned HTTP {}", url.replace(apiKey, "***"), status);
|
if (logging) {
|
||||||
|
LoggerFactory.getLogger(VoiceRSSCloudImpl.class).warn("Call {} returned HTTP {}",
|
||||||
|
url.replace(apiKey, "***"), status);
|
||||||
|
}
|
||||||
throw new IOException("Could not read from service: HTTP code " + status);
|
throw new IOException("Could not read from service: HTTP code " + status);
|
||||||
}
|
}
|
||||||
if (logger.isTraceEnabled()) {
|
if (logging) {
|
||||||
for (Entry<String, List<String>> header : connection.getHeaderFields().entrySet()) {
|
Logger logger = LoggerFactory.getLogger(VoiceRSSCloudImpl.class);
|
||||||
logger.trace("Response.header: {}={}", header.getKey(), header.getValue());
|
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");
|
String contentType = connection.getHeaderField("Content-Type");
|
||||||
@ -268,7 +252,9 @@ public class VoiceRSSCloudImpl implements VoiceRSSCloudAPI {
|
|||||||
try {
|
try {
|
||||||
is.close();
|
is.close();
|
||||||
} catch (IOException ex) {
|
} catch (IOException ex) {
|
||||||
logger.debug("Failed to close inputstream", ex);
|
if (logging) {
|
||||||
|
LoggerFactory.getLogger(VoiceRSSCloudImpl.class).debug("Failed to close inputstream", ex);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
throw new IOException(
|
throw new IOException(
|
||||||
"Could not read audio content, service returned an error: " + new String(bytes, "UTF-8"));
|
"Could not read audio content, service returned an error: " + new String(bytes, "UTF-8"));
|
||||||
|
|||||||
@ -17,6 +17,7 @@ import java.io.File;
|
|||||||
import java.io.FileReader;
|
import java.io.FileReader;
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
|
|
||||||
|
import org.eclipse.jdt.annotation.NonNullByDefault;
|
||||||
import org.openhab.voice.voicerss.internal.cloudapi.CachedVoiceRSSCloudImpl;
|
import org.openhab.voice.voicerss.internal.cloudapi.CachedVoiceRSSCloudImpl;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -24,12 +25,14 @@ import org.openhab.voice.voicerss.internal.cloudapi.CachedVoiceRSSCloudImpl;
|
|||||||
*
|
*
|
||||||
* @author Jochen Hiller - Initial contribution
|
* @author Jochen Hiller - Initial contribution
|
||||||
*/
|
*/
|
||||||
|
@NonNullByDefault
|
||||||
public class CreateTTSCache {
|
public class CreateTTSCache {
|
||||||
|
|
||||||
public static final int RC_OK = 0;
|
public static final int RC_OK = 0;
|
||||||
public static final int RC_USAGE = 1;
|
public static final int RC_USAGE = 1;
|
||||||
public static final int RC_INPUT_FILE_NOT_FOUND = 2;
|
public static final int RC_INPUT_FILE_NOT_FOUND = 2;
|
||||||
public static final int RC_API_KEY_MISSING = 3;
|
public static final int RC_API_KEY_MISSING = 3;
|
||||||
|
public static final int RC_INVALID_CODEC = 4;
|
||||||
|
|
||||||
public static void main(String[] args) throws IOException {
|
public static void main(String[] args) throws IOException {
|
||||||
CreateTTSCache tool = new CreateTTSCache();
|
CreateTTSCache tool = new CreateTTSCache();
|
||||||
@ -38,7 +41,7 @@ public class CreateTTSCache {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public int doMain(String[] args) throws IOException {
|
public int doMain(String[] args) throws IOException {
|
||||||
if ((args == null) || (args.length != 5)) {
|
if (args.length < 6) {
|
||||||
usage();
|
usage();
|
||||||
return RC_USAGE;
|
return RC_USAGE;
|
||||||
}
|
}
|
||||||
@ -50,6 +53,21 @@ public class CreateTTSCache {
|
|||||||
String cacheDir = args[2];
|
String cacheDir = args[2];
|
||||||
String locale = args[3];
|
String locale = args[3];
|
||||||
String voice = args[4];
|
String voice = args[4];
|
||||||
|
String codec = "MP3";
|
||||||
|
if (args.length >= 7) {
|
||||||
|
switch (args[6]) {
|
||||||
|
case "MP3":
|
||||||
|
case "WAV":
|
||||||
|
case "OGG":
|
||||||
|
case "AAC":
|
||||||
|
codec = args[6];
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
usage();
|
||||||
|
return RC_INVALID_CODEC;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
String format = args.length >= 8 ? args[7] : "44khz_16bit_mono";
|
||||||
if (args[5].startsWith("@")) {
|
if (args[5].startsWith("@")) {
|
||||||
String inputFileName = args[5].substring(1);
|
String inputFileName = args[5].substring(1);
|
||||||
File inputFile = new File(inputFileName);
|
File inputFile = new File(inputFileName);
|
||||||
@ -58,17 +76,18 @@ public class CreateTTSCache {
|
|||||||
System.err.println("File " + inputFileName + " not found");
|
System.err.println("File " + inputFileName + " not found");
|
||||||
return RC_INPUT_FILE_NOT_FOUND;
|
return RC_INPUT_FILE_NOT_FOUND;
|
||||||
}
|
}
|
||||||
generateCacheForFile(apiKey, cacheDir, locale, voice, inputFileName);
|
generateCacheForFile(apiKey, cacheDir, locale, voice, codec, format, inputFileName);
|
||||||
} else {
|
} else {
|
||||||
String text = args[5];
|
String text = args[5];
|
||||||
generateCacheForMessage(apiKey, cacheDir, locale, voice, text);
|
generateCacheForMessage(apiKey, cacheDir, locale, voice, codec, format, text);
|
||||||
}
|
}
|
||||||
return RC_OK;
|
return RC_OK;
|
||||||
}
|
}
|
||||||
|
|
||||||
private void usage() {
|
private void usage() {
|
||||||
System.out.println("Usage: java org.openhab.voice.voicerss.tool.CreateTTSCache <args>");
|
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(
|
||||||
|
"Arguments: --api-key <key> <cache-dir> <locale> <voice> { <text> | @inputfile } [ <codec> <format> ]");
|
||||||
System.out.println(" key the VoiceRSS API Key, e.g. \"123456789\"");
|
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(" 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(" locale the language locale, has to be valid, e.g. \"en-us\", \"de-de\"");
|
||||||
@ -76,38 +95,41 @@ public class CreateTTSCache {
|
|||||||
System.out.println(" text the text to create audio file for, e.g. \"Hello World\"");
|
System.out.println(" text the text to create audio file for, e.g. \"Hello World\"");
|
||||||
System.out.println(
|
System.out.println(
|
||||||
" inputfile a name of a file, where all lines will be translatet to text, e.g. \"@message.txt\"");
|
" inputfile a name of a file, where all lines will be translatet to text, e.g. \"@message.txt\"");
|
||||||
|
System.out.println(" codec the audio codec, \"MP3\", \"WAV\", \"OGG\" or \"AAC\", \"MP3\" by default");
|
||||||
|
System.out.println(" format the audio format, \"44khz_16bit_mono\" by default");
|
||||||
System.out.println();
|
System.out.println();
|
||||||
System.out.println(
|
System.out.println(
|
||||||
"Sample: java org.openhab.voice.voicerss.tool.CreateTTSCache --api-key 1234567890 cache en-US @messages.txt");
|
"Sample: java org.openhab.voice.voicerss.tool.CreateTTSCache --api-key 1234567890 cache en-US default @messages.txt");
|
||||||
System.out.println();
|
System.out.println();
|
||||||
}
|
}
|
||||||
|
|
||||||
private void generateCacheForFile(String apiKey, String cacheDir, String locale, String voice, String inputFileName)
|
private void generateCacheForFile(String apiKey, String cacheDir, String locale, String voice, String codec,
|
||||||
throws IOException {
|
String format, String inputFileName) throws IOException {
|
||||||
File inputFile = new File(inputFileName);
|
File inputFile = new File(inputFileName);
|
||||||
try (BufferedReader br = new BufferedReader(new FileReader(inputFile))) {
|
try (BufferedReader br = new BufferedReader(new FileReader(inputFile))) {
|
||||||
String line;
|
String line;
|
||||||
while ((line = br.readLine()) != null) {
|
while ((line = br.readLine()) != null) {
|
||||||
// process the line.
|
// process the line.
|
||||||
generateCacheForMessage(apiKey, cacheDir, locale, voice, line);
|
generateCacheForMessage(apiKey, cacheDir, locale, voice, codec, format, line);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private void generateCacheForMessage(String apiKey, String cacheDir, String locale, String voice, String msg)
|
private void generateCacheForMessage(String apiKey, String cacheDir, String locale, String voice, String codec,
|
||||||
throws IOException {
|
String format, String msg) throws IOException {
|
||||||
if (msg == null) {
|
|
||||||
System.err.println("Ignore msg=null");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
String trimmedMsg = msg.trim();
|
String trimmedMsg = msg.trim();
|
||||||
if (trimmedMsg.length() == 0) {
|
if (trimmedMsg.length() == 0) {
|
||||||
System.err.println("Ignore msg=''");
|
System.err.println("Ignore msg=''");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
CachedVoiceRSSCloudImpl impl = new CachedVoiceRSSCloudImpl(cacheDir);
|
try {
|
||||||
File cachedFile = impl.getTextToSpeechAsFile(apiKey, trimmedMsg, locale, voice, "MP3", null);
|
CachedVoiceRSSCloudImpl impl = new CachedVoiceRSSCloudImpl(cacheDir, false);
|
||||||
System.out.println(
|
File cachedFile = impl.getTextToSpeechAsFile(apiKey, trimmedMsg, locale, voice, codec, format);
|
||||||
"Created cached audio for locale='" + locale + "', msg='" + trimmedMsg + "' to file=" + cachedFile);
|
System.out.println("Created cached audio for locale='" + locale + "', voice='" + voice + "', msg='"
|
||||||
|
+ trimmedMsg + "' to file=" + cachedFile);
|
||||||
|
} catch (IllegalStateException | IOException ex) {
|
||||||
|
System.err.println("Failed to create cached audio for locale='" + locale + "', voice='" + voice + "',msg='"
|
||||||
|
+ trimmedMsg + "'");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user