mirror of
https://github.com/openhab/openhab-addons.git
synced 2025-01-10 15:11:59 +01:00
[voicerss] Add LRU cache (#14561)
Signed-off-by: Laurent Garnier <lg.hc@free.fr>
This commit is contained in:
parent
9814047e21
commit
e58991cdf8
@ -166,9 +166,10 @@ It supports the following audio formats: MP3, OGG, AAC and WAV.
|
|||||||
|
|
||||||
## Caching
|
## 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.
|
The VoiceRSS TTS service uses the openHAB TTS cache to cache audio files produced from the most recent queries in order to reduce traffic, improve performance and reduce number of requests.
|
||||||
|
|
||||||
For convenience, there is a tool where the audio cache can be generated in advance, to have a prefilled cache when starting this extension.
|
An additional and specific cache can be prepared in advance to provide offline capability for predefined queries.
|
||||||
|
For convenience, there is a tool where this cache can be generated in advance, to have a prefilled cache when starting this service.
|
||||||
You have to copy the generated data to your userdata/voicerss/cache folder.
|
You have to copy the generated data to your userdata/voicerss/cache folder.
|
||||||
|
|
||||||
Synopsis of this tool:
|
Synopsis of this tool:
|
||||||
|
@ -0,0 +1,65 @@
|
|||||||
|
/**
|
||||||
|
* Copyright (c) 2010-2023 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.IOException;
|
||||||
|
import java.io.InputStream;
|
||||||
|
|
||||||
|
import org.eclipse.jdt.annotation.NonNullByDefault;
|
||||||
|
import org.openhab.core.audio.AudioFormat;
|
||||||
|
import org.openhab.core.audio.AudioStream;
|
||||||
|
import org.openhab.core.audio.SizeableAudioStream;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Implementation of the {@link AudioStream} interface for the
|
||||||
|
* {@link VoiceRSSTTSService}. It simply uses a {@link AudioStream}.
|
||||||
|
*
|
||||||
|
* @author Laurent Garnier - Initial contribution
|
||||||
|
*/
|
||||||
|
@NonNullByDefault
|
||||||
|
public class VoiceRSSRawAudioStream extends AudioStream implements SizeableAudioStream {
|
||||||
|
|
||||||
|
private InputStream inputStream;
|
||||||
|
private AudioFormat format;
|
||||||
|
private long length;
|
||||||
|
|
||||||
|
public VoiceRSSRawAudioStream(InputStream inputStream, AudioFormat format, long length) {
|
||||||
|
this.inputStream = inputStream;
|
||||||
|
this.format = format;
|
||||||
|
this.length = length;
|
||||||
|
}
|
||||||
|
|
||||||
|
public InputStream getInputStream() {
|
||||||
|
return inputStream;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public AudioFormat getFormat() {
|
||||||
|
return format;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public long length() {
|
||||||
|
return length;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public int read() throws IOException {
|
||||||
|
return inputStream.read();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void close() throws IOException {
|
||||||
|
inputStream.close();
|
||||||
|
}
|
||||||
|
}
|
@ -27,6 +27,8 @@ 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;
|
||||||
import org.openhab.core.config.core.ConfigurableService;
|
import org.openhab.core.config.core.ConfigurableService;
|
||||||
|
import org.openhab.core.voice.AbstractCachedTTSService;
|
||||||
|
import org.openhab.core.voice.TTSCache;
|
||||||
import org.openhab.core.voice.TTSException;
|
import org.openhab.core.voice.TTSException;
|
||||||
import org.openhab.core.voice.TTSService;
|
import org.openhab.core.voice.TTSService;
|
||||||
import org.openhab.core.voice.Voice;
|
import org.openhab.core.voice.Voice;
|
||||||
@ -35,6 +37,7 @@ import org.osgi.framework.Constants;
|
|||||||
import org.osgi.service.component.annotations.Activate;
|
import org.osgi.service.component.annotations.Activate;
|
||||||
import org.osgi.service.component.annotations.Component;
|
import org.osgi.service.component.annotations.Component;
|
||||||
import org.osgi.service.component.annotations.Modified;
|
import org.osgi.service.component.annotations.Modified;
|
||||||
|
import org.osgi.service.component.annotations.Reference;
|
||||||
import org.slf4j.Logger;
|
import org.slf4j.Logger;
|
||||||
import org.slf4j.LoggerFactory;
|
import org.slf4j.LoggerFactory;
|
||||||
|
|
||||||
@ -45,9 +48,10 @@ 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
|
||||||
*/
|
*/
|
||||||
@NonNullByDefault
|
@NonNullByDefault
|
||||||
@Component(configurationPid = "org.openhab.voicerss", property = Constants.SERVICE_PID + "=org.openhab.voicerss")
|
@Component(service = TTSService.class, configurationPid = "org.openhab.voicerss", property = Constants.SERVICE_PID
|
||||||
|
+ "=org.openhab.voicerss")
|
||||||
@ConfigurableService(category = "voice", label = "VoiceRSS Text-to-Speech", description_uri = "voice:voicerss")
|
@ConfigurableService(category = "voice", label = "VoiceRSS Text-to-Speech", description_uri = "voice:voicerss")
|
||||||
public class VoiceRSSTTSService implements TTSService {
|
public class VoiceRSSTTSService extends AbstractCachedTTSService {
|
||||||
|
|
||||||
/** Cache folder name is below userdata/voicerss/cache. */
|
/** Cache folder name is below userdata/voicerss/cache. */
|
||||||
private static final String CACHE_FOLDER_NAME = "voicerss" + File.separator + "cache";
|
private static final String CACHE_FOLDER_NAME = "voicerss" + File.separator + "cache";
|
||||||
@ -87,6 +91,11 @@ public class VoiceRSSTTSService implements TTSService {
|
|||||||
*/
|
*/
|
||||||
private @Nullable Set<AudioFormat> audioFormats;
|
private @Nullable Set<AudioFormat> audioFormats;
|
||||||
|
|
||||||
|
@Activate
|
||||||
|
public VoiceRSSTTSService(final @Reference TTSCache ttsCache) {
|
||||||
|
super(ttsCache);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* DS activate, with access to ConfigAdmin
|
* DS activate, with access to ConfigAdmin
|
||||||
*/
|
*/
|
||||||
@ -130,6 +139,43 @@ public class VoiceRSSTTSService implements TTSService {
|
|||||||
if (voiceRssCloud == null) {
|
if (voiceRssCloud == null) {
|
||||||
throw new TTSException("The service is not correctly initialized");
|
throw new TTSException("The service is not correctly initialized");
|
||||||
}
|
}
|
||||||
|
// trim text
|
||||||
|
String trimmedText = text.trim();
|
||||||
|
if (trimmedText.isEmpty()) {
|
||||||
|
throw new TTSException("The passed text is empty");
|
||||||
|
}
|
||||||
|
Set<Voice> localVoices = voices;
|
||||||
|
if (localVoices == null || !localVoices.contains(voice)) {
|
||||||
|
throw new TTSException("The passed voice is unsupported");
|
||||||
|
}
|
||||||
|
|
||||||
|
// If one predefined cache entry for given text, locale, voice, codec and format exists,
|
||||||
|
// create the input from this file stream and return it.
|
||||||
|
try {
|
||||||
|
File cacheAudioFile = voiceRssCloud.getTextToSpeechInCache(trimmedText, voice.getLocale().toLanguageTag(),
|
||||||
|
voice.getLabel(), getApiAudioCodec(requestedFormat), getApiAudioFormat(requestedFormat));
|
||||||
|
if (cacheAudioFile != null) {
|
||||||
|
logger.debug("Use cache entry '{}'", cacheAudioFile.getName());
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
|
||||||
|
// If no predefined cache entry exists, use the common TTS cache mechanism from core framework
|
||||||
|
logger.debug("Use common TTS cache mechanism");
|
||||||
|
return super.synthesize(text, voice, requestedFormat);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public AudioStream synthesizeForCache(String text, Voice voice, AudioFormat requestedFormat) throws TTSException {
|
||||||
|
logger.debug("synthesizeForCache '{}' for voice '{}' in format {}", text, voice.getUID(), requestedFormat);
|
||||||
|
CachedVoiceRSSCloudImpl voiceRssCloud = voiceRssImpl;
|
||||||
|
if (voiceRssCloud == null) {
|
||||||
|
throw new TTSException("The service is not correctly initialized");
|
||||||
|
}
|
||||||
// Validate known api key
|
// Validate known api key
|
||||||
String key = apiKey;
|
String key = apiKey;
|
||||||
if (key == null) {
|
if (key == null) {
|
||||||
@ -145,14 +191,11 @@ public class VoiceRSSTTSService implements TTSService {
|
|||||||
throw new TTSException("The passed voice is unsupported");
|
throw new TTSException("The passed voice is unsupported");
|
||||||
}
|
}
|
||||||
|
|
||||||
// now create the input stream for given text, locale, voice, codec and format.
|
|
||||||
try {
|
try {
|
||||||
File cacheAudioFile = voiceRssCloud.getTextToSpeechAsFile(key, trimmedText,
|
VoiceRSSRawAudioStream audioStream = voiceRssCloud.getTextToSpeech(key, trimmedText,
|
||||||
voice.getLocale().toLanguageTag(), voice.getLabel(), getApiAudioCodec(requestedFormat),
|
voice.getLocale().toLanguageTag(), voice.getLabel(), getApiAudioCodec(requestedFormat),
|
||||||
getApiAudioFormat(requestedFormat));
|
getApiAudioFormat(requestedFormat));
|
||||||
return new VoiceRSSAudioStream(cacheAudioFile, requestedFormat);
|
return new VoiceRSSRawAudioStream(audioStream.getInputStream(), requestedFormat, audioStream.length());
|
||||||
} catch (AudioException ex) {
|
|
||||||
throw new TTSException("Could not create AudioStream: " + ex.getMessage(), ex);
|
|
||||||
} catch (IOException ex) {
|
} catch (IOException ex) {
|
||||||
throw new TTSException("Could not read from VoiceRSS service: " + ex.getMessage(), ex);
|
throw new TTSException("Could not read from VoiceRSS service: " + ex.getMessage(), ex);
|
||||||
}
|
}
|
||||||
|
@ -69,8 +69,8 @@ public class CachedVoiceRSSCloudImpl extends VoiceRSSCloudImpl {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// if not in cache, get audio data and put to cache
|
// if not in cache, get audio data and put to cache
|
||||||
try (InputStream is = super.getTextToSpeech(apiKey, text, locale, voice, audioCodec, audioFormat);
|
try (InputStream is = super.getTextToSpeech(apiKey, text, locale, voice, audioCodec, audioFormat)
|
||||||
FileOutputStream fos = new FileOutputStream(audioFileInCache)) {
|
.getInputStream(); FileOutputStream fos = new FileOutputStream(audioFileInCache)) {
|
||||||
copyStream(is, fos);
|
copyStream(is, fos);
|
||||||
// write text to file for transparency too
|
// write text to file for transparency too
|
||||||
// this allows to know which contents is in which audio file
|
// this allows to know which contents is in which audio file
|
||||||
@ -83,6 +83,16 @@ public class CachedVoiceRSSCloudImpl extends VoiceRSSCloudImpl {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public @Nullable File getTextToSpeechInCache(String text, String locale, String voice, String audioCodec,
|
||||||
|
String audioFormat) throws IOException {
|
||||||
|
String fileNameInCache = getUniqueFilenameForText(text, locale, voice, audioFormat);
|
||||||
|
if (fileNameInCache == null) {
|
||||||
|
throw new IOException("Could not infer cache file name");
|
||||||
|
}
|
||||||
|
File audioFileInCache = new File(cacheFolder, fileNameInCache + "." + audioCodec.toLowerCase());
|
||||||
|
return audioFileInCache.exists() ? audioFileInCache : null;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Gets a unique filename for a give text, by creating a MD5 hash of it. It
|
* Gets a unique filename for a give text, by creating a MD5 hash of it. It
|
||||||
* will be preceded by the locale and suffixed by the format if it is not the
|
* will be preceded by the locale and suffixed by the format if it is not the
|
||||||
|
@ -13,11 +13,11 @@
|
|||||||
package org.openhab.voice.voicerss.internal.cloudapi;
|
package org.openhab.voice.voicerss.internal.cloudapi;
|
||||||
|
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.io.InputStream;
|
|
||||||
import java.util.Locale;
|
import java.util.Locale;
|
||||||
import java.util.Set;
|
import java.util.Set;
|
||||||
|
|
||||||
import org.eclipse.jdt.annotation.NonNullByDefault;
|
import org.eclipse.jdt.annotation.NonNullByDefault;
|
||||||
|
import org.openhab.voice.voicerss.internal.VoiceRSSRawAudioStream;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Interface which represents the functionality needed from the VoiceRSS TTS
|
* Interface which represents the functionality needed from the VoiceRSS TTS
|
||||||
@ -31,7 +31,7 @@ public interface VoiceRSSCloudAPI {
|
|||||||
/**
|
/**
|
||||||
* Get all supported locales by the TTS service.
|
* Get all supported locales by the TTS service.
|
||||||
*
|
*
|
||||||
* @return A set of @{link {@link Locale} supported
|
* @return A set of {@link Locale} supported
|
||||||
*/
|
*/
|
||||||
Set<Locale> getAvailableLocales();
|
Set<Locale> getAvailableLocales();
|
||||||
|
|
||||||
@ -74,11 +74,11 @@ public interface VoiceRSSCloudAPI {
|
|||||||
* the audio codec to use
|
* the audio codec to use
|
||||||
* @param audioFormat
|
* @param audioFormat
|
||||||
* the audio format to use
|
* the audio format to use
|
||||||
* @return an InputStream to the audio data in specified format
|
* @return a {@link VoiceRSSRawAudioStream} to the audio data in specified format
|
||||||
* @throws IOException
|
* @throws IOException
|
||||||
* will be raised if the audio data can not be retrieved from
|
* will be raised if the audio data can not be retrieved from
|
||||||
* cloud service
|
* cloud service
|
||||||
*/
|
*/
|
||||||
InputStream getTextToSpeech(String apiKey, String text, String locale, String voice, String audioCodec,
|
VoiceRSSRawAudioStream getTextToSpeech(String apiKey, String text, String locale, String voice, String audioCodec,
|
||||||
String audioFormat) throws IOException;
|
String audioFormat) throws IOException;
|
||||||
}
|
}
|
||||||
|
@ -28,6 +28,8 @@ import java.util.Map.Entry;
|
|||||||
import java.util.Set;
|
import java.util.Set;
|
||||||
|
|
||||||
import org.eclipse.jdt.annotation.NonNullByDefault;
|
import org.eclipse.jdt.annotation.NonNullByDefault;
|
||||||
|
import org.openhab.core.audio.AudioFormat;
|
||||||
|
import org.openhab.voice.voicerss.internal.VoiceRSSRawAudioStream;
|
||||||
import org.slf4j.Logger;
|
import org.slf4j.Logger;
|
||||||
import org.slf4j.LoggerFactory;
|
import org.slf4j.LoggerFactory;
|
||||||
|
|
||||||
@ -215,8 +217,8 @@ public class VoiceRSSCloudImpl implements VoiceRSSCloudAPI {
|
|||||||
* dependencies.
|
* dependencies.
|
||||||
*/
|
*/
|
||||||
@Override
|
@Override
|
||||||
public InputStream getTextToSpeech(String apiKey, String text, String locale, String voice, String audioCodec,
|
public VoiceRSSRawAudioStream getTextToSpeech(String apiKey, String text, String locale, String voice,
|
||||||
String audioFormat) throws IOException {
|
String audioCodec, String audioFormat) throws IOException {
|
||||||
String url = createURL(apiKey, text, locale, voice, audioCodec, audioFormat);
|
String url = createURL(apiKey, text, locale, voice, audioCodec, audioFormat);
|
||||||
if (logging) {
|
if (logging) {
|
||||||
LoggerFactory.getLogger(VoiceRSSCloudImpl.class).debug("Call {}", url.replace(apiKey, "***"));
|
LoggerFactory.getLogger(VoiceRSSCloudImpl.class).debug("Call {}", url.replace(apiKey, "***"));
|
||||||
@ -259,7 +261,8 @@ public class VoiceRSSCloudImpl implements VoiceRSSCloudAPI {
|
|||||||
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"));
|
||||||
} else {
|
} else {
|
||||||
return is;
|
// Set any audio format
|
||||||
|
return new VoiceRSSRawAudioStream(is, AudioFormat.MP3, connection.getContentLengthLong());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -18,12 +18,16 @@ import static org.hamcrest.core.IsNot.not;
|
|||||||
import static org.openhab.core.audio.AudioFormat.*;
|
import static org.openhab.core.audio.AudioFormat.*;
|
||||||
import static org.openhab.voice.voicerss.internal.CompatibleAudioFormatMatcher.compatibleAudioFormat;
|
import static org.openhab.voice.voicerss.internal.CompatibleAudioFormatMatcher.compatibleAudioFormat;
|
||||||
|
|
||||||
|
import java.util.HashMap;
|
||||||
|
import java.util.Map;
|
||||||
import java.util.Set;
|
import java.util.Set;
|
||||||
|
|
||||||
import org.junit.jupiter.api.BeforeEach;
|
import org.junit.jupiter.api.BeforeEach;
|
||||||
import org.junit.jupiter.api.Test;
|
import org.junit.jupiter.api.Test;
|
||||||
import org.openhab.core.audio.AudioFormat;
|
import org.openhab.core.audio.AudioFormat;
|
||||||
|
import org.openhab.core.storage.StorageService;
|
||||||
import org.openhab.core.voice.TTSService;
|
import org.openhab.core.voice.TTSService;
|
||||||
|
import org.openhab.core.voice.internal.cache.TTSLRUCacheImpl;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Tests for {@link VoiceRSSTTSService}.
|
* Tests for {@link VoiceRSSTTSService}.
|
||||||
@ -43,6 +47,8 @@ public class VoiceRSSTTSServiceTest {
|
|||||||
private static final AudioFormat WAV_48KHZ_16BIT = new AudioFormat(AudioFormat.CONTAINER_WAVE,
|
private static final AudioFormat WAV_48KHZ_16BIT = new AudioFormat(AudioFormat.CONTAINER_WAVE,
|
||||||
AudioFormat.CODEC_PCM_SIGNED, false, 16, null, 48_000L);
|
AudioFormat.CODEC_PCM_SIGNED, false, 16, null, 48_000L);
|
||||||
|
|
||||||
|
private StorageService storageService;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The {@link VoiceRSSTTSService} under test.
|
* The {@link VoiceRSSTTSService} under test.
|
||||||
*/
|
*/
|
||||||
@ -50,7 +56,10 @@ public class VoiceRSSTTSServiceTest {
|
|||||||
|
|
||||||
@BeforeEach
|
@BeforeEach
|
||||||
public void setUp() {
|
public void setUp() {
|
||||||
final VoiceRSSTTSService ttsService = new VoiceRSSTTSService();
|
Map<String, Object> config = new HashMap<>();
|
||||||
|
config.put("enableCacheTTS", false);
|
||||||
|
TTSLRUCacheImpl voiceLRUCache = new TTSLRUCacheImpl(storageService, config);
|
||||||
|
final VoiceRSSTTSService ttsService = new VoiceRSSTTSService(voiceLRUCache);
|
||||||
ttsService.activate(null);
|
ttsService.activate(null);
|
||||||
|
|
||||||
this.ttsService = ttsService;
|
this.ttsService = ttsService;
|
||||||
|
Loading…
Reference in New Issue
Block a user