Skip to content

Commit

Permalink
[voicerss] Add LRU cache
Browse files Browse the repository at this point in the history
Signed-off-by: Laurent Garnier <[email protected]>
  • Loading branch information
lolodomo committed Jul 7, 2023
1 parent 90b2279 commit 54d2440
Show file tree
Hide file tree
Showing 7 changed files with 152 additions and 19 deletions.
5 changes: 3 additions & 2 deletions bundles/org.openhab.voice.voicerss/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -166,9 +166,10 @@ It supports the following audio formats: MP3, OGG, AAC and WAV.

## 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.

Synopsis of this tool:
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
/**
* 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;

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;
import org.openhab.voice.voicerss.internal.VoiceRSSTTSService;

/**
* 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();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -27,14 +27,18 @@
import org.openhab.core.audio.AudioFormat;
import org.openhab.core.audio.AudioStream;
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.TTSService;
import org.openhab.core.voice.Voice;
import org.openhab.voice.voicerss.VoiceRSSRawAudioStream;
import org.openhab.voice.voicerss.internal.cloudapi.CachedVoiceRSSCloudImpl;
import org.osgi.framework.Constants;
import org.osgi.service.component.annotations.Activate;
import org.osgi.service.component.annotations.Component;
import org.osgi.service.component.annotations.Modified;
import org.osgi.service.component.annotations.Reference;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

Expand All @@ -45,9 +49,10 @@
* @author Laurent Garnier - add support for OGG and AAC audio formats
*/
@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")
public class VoiceRSSTTSService implements TTSService {
public class VoiceRSSTTSService extends AbstractCachedTTSService {

/** Cache folder name is below userdata/voicerss/cache. */
private static final String CACHE_FOLDER_NAME = "voicerss" + File.separator + "cache";
Expand Down Expand Up @@ -87,6 +92,11 @@ public class VoiceRSSTTSService implements TTSService {
*/
private @Nullable Set<AudioFormat> audioFormats;

@Activate
public VoiceRSSTTSService(final @Reference TTSCache ttsCache) {
super(ttsCache);
}

/**
* DS activate, with access to ConfigAdmin
*/
Expand Down Expand Up @@ -130,6 +140,43 @@ public AudioStream synthesize(String text, Voice voice, AudioFormat requestedFor
if (voiceRssCloud == null) {
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
String key = apiKey;
if (key == null) {
Expand All @@ -145,14 +192,11 @@ public AudioStream synthesize(String text, Voice voice, AudioFormat requestedFor
throw new TTSException("The passed voice is unsupported");
}

// now create the input stream for given text, locale, voice, codec and format.
try {
File cacheAudioFile = voiceRssCloud.getTextToSpeechAsFile(key, trimmedText,
VoiceRSSRawAudioStream audioStream = voiceRssCloud.getTextToSpeech(key, trimmedText,
voice.getLocale().toLanguageTag(), voice.getLabel(), getApiAudioCodec(requestedFormat),
getApiAudioFormat(requestedFormat));
return new VoiceRSSAudioStream(cacheAudioFile, requestedFormat);
} catch (AudioException ex) {
throw new TTSException("Could not create AudioStream: " + ex.getMessage(), ex);
return new VoiceRSSRawAudioStream(audioStream.getInputStream(), requestedFormat, audioStream.length());
} catch (IOException ex) {
throw new TTSException("Could not read from VoiceRSS service: " + ex.getMessage(), ex);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -69,8 +69,8 @@ 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, voice, audioCodec, audioFormat);
FileOutputStream fos = new FileOutputStream(audioFileInCache)) {
try (InputStream is = super.getTextToSpeech(apiKey, text, locale, voice, audioCodec, audioFormat)
.getInputStream(); FileOutputStream fos = new FileOutputStream(audioFileInCache)) {
copyStream(is, fos);
// write text to file for transparency too
// this allows to know which contents is in which audio file
Expand All @@ -83,6 +83,16 @@ public File getTextToSpeechAsFile(String apiKey, String text, String locale, Str
}
}

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
* will be preceded by the locale and suffixed by the format if it is not the
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,11 +13,11 @@
package org.openhab.voice.voicerss.internal.cloudapi;

import java.io.IOException;
import java.io.InputStream;
import java.util.Locale;
import java.util.Set;

import org.eclipse.jdt.annotation.NonNullByDefault;
import org.openhab.voice.voicerss.VoiceRSSRawAudioStream;

/**
* Interface which represents the functionality needed from the VoiceRSS TTS
Expand All @@ -31,7 +31,7 @@ public interface VoiceRSSCloudAPI {
/**
* 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();

Expand Down Expand Up @@ -74,11 +74,11 @@ public interface VoiceRSSCloudAPI {
* the audio codec to use
* @param audioFormat
* 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
* will be raised if the audio data can not be retrieved from
* 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;
}
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,8 @@
import java.util.Set;

import org.eclipse.jdt.annotation.NonNullByDefault;
import org.openhab.core.audio.AudioFormat;
import org.openhab.voice.voicerss.VoiceRSSRawAudioStream;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

Expand Down Expand Up @@ -215,8 +217,8 @@ public Set<String> getAvailableVoices(Locale locale) {
* dependencies.
*/
@Override
public InputStream getTextToSpeech(String apiKey, String text, String locale, String voice, String audioCodec,
String audioFormat) throws IOException {
public VoiceRSSRawAudioStream getTextToSpeech(String apiKey, String text, String locale, String voice,
String audioCodec, String audioFormat) throws IOException {
String url = createURL(apiKey, text, locale, voice, audioCodec, audioFormat);
if (logging) {
LoggerFactory.getLogger(VoiceRSSCloudImpl.class).debug("Call {}", url.replace(apiKey, "***"));
Expand Down Expand Up @@ -259,7 +261,8 @@ public InputStream getTextToSpeech(String apiKey, String text, String locale, St
throw new IOException(
"Could not read audio content, service returned an error: " + new String(bytes, "UTF-8"));
} else {
return is;
// Set any audio format
return new VoiceRSSRawAudioStream(is, AudioFormat.MP3, connection.getContentLengthLong());
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,12 +18,16 @@
import static org.openhab.core.audio.AudioFormat.*;
import static org.openhab.voice.voicerss.internal.CompatibleAudioFormatMatcher.compatibleAudioFormat;

import java.util.HashMap;
import java.util.Map;
import java.util.Set;

import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.openhab.core.audio.AudioFormat;
import org.openhab.core.storage.StorageService;
import org.openhab.core.voice.TTSService;
import org.openhab.core.voice.internal.cache.TTSLRUCacheImpl;

/**
* Tests for {@link VoiceRSSTTSService}.
Expand All @@ -43,14 +47,19 @@ public class VoiceRSSTTSServiceTest {
private static final AudioFormat WAV_48KHZ_16BIT = new AudioFormat(AudioFormat.CONTAINER_WAVE,
AudioFormat.CODEC_PCM_SIGNED, false, 16, null, 48_000L);

private StorageService storageService;

/**
* The {@link VoiceRSSTTSService} under test.
*/
private TTSService ttsService;

@BeforeEach
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);

this.ttsService = ttsService;
Expand Down

0 comments on commit 54d2440

Please sign in to comment.