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 Mar 10, 2023
1 parent 90b2279 commit 80e8308
Show file tree
Hide file tree
Showing 8 changed files with 199 additions and 24 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -12,26 +12,58 @@
*/
package org.openhab.voice.voicerss.internal;

import java.io.File;
import java.io.IOException;
import java.io.InputStream;

import org.eclipse.jdt.annotation.NonNullByDefault;
import org.openhab.core.audio.AudioException;
import org.openhab.core.audio.AudioFormat;
import org.openhab.core.audio.AudioStream;
import org.openhab.core.audio.FileAudioStream;
import org.openhab.core.audio.FixedLengthAudioStream;

/**
* Implementation of the {@link AudioStream} interface for the
* {@link VoiceRSSTTSService}. It simply uses a {@link FileAudioStream} which is
* {@link VoiceRSSTTSService}. It simply uses a {@link FixedLengthAudioStream} which is
* doing all the necessary work, e.g. supporting MP3 and WAV files with fixed
* stream length.
*
* @author Jochen Hiller - Initial contribution and API
* @author Laurent Garnier - Initial contribution
*/
@NonNullByDefault
class VoiceRSSAudioStream extends FileAudioStream {
class VoiceRSSAudioStream extends FixedLengthAudioStream {

public VoiceRSSAudioStream(File audioFile, AudioFormat format) throws AudioException {
super(audioFile, format);
private InputStream inputStream;
private long length;
private AudioFormat format;

public VoiceRSSAudioStream(InputStream inputStream, long length, AudioFormat format) {
this.inputStream = inputStream;
this.length = length;
this.format = format;
}

@Override
public long length() {
return length;
}

@Override
public AudioFormat getFormat() {
return format;
}

@Override
public InputStream getClonedStream() throws AudioException {
throw new AudioException("getClonedStream not supported");
}

@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
@@ -0,0 +1,37 @@
/**
* 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.File;

import org.eclipse.jdt.annotation.NonNullByDefault;
import org.openhab.core.audio.AudioException;
import org.openhab.core.audio.AudioFormat;
import org.openhab.core.audio.AudioStream;
import org.openhab.core.audio.FileAudioStream;

/**
* Implementation of the {@link AudioStream} interface for the
* {@link VoiceRSSTTSService}. It simply uses a {@link FileAudioStream} which is
* doing all the necessary work, e.g. supporting MP3 and WAV files with fixed
* stream length.
*
* @author Jochen Hiller - Initial contribution and API
*/
@NonNullByDefault
class VoiceRSSFileAudioStream extends FileAudioStream {

public VoiceRSSFileAudioStream(File audioFile, AudioFormat format) throws AudioException {
super(audioFile, format);
}
}
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.internal.cloudapi.CachedVoiceRSSCloudImpl;
import org.openhab.voice.voicerss.internal.cloudapi.SizedInputStream;
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 VoiceRSSFileAudioStream(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,10 @@ 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,
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);
SizedInputStream input = voiceRssCloud.getTextToSpeech(key, trimmedText, voice.getLocale().toLanguageTag(),
voice.getLabel(), getApiAudioCodec(requestedFormat), getApiAudioFormat(requestedFormat));
return new VoiceRSSAudioStream(input.getStream(), input.getLength(), requestedFormat);
} 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,7 +69,7 @@ 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);
try (InputStream is = super.getTextToSpeech(apiKey, text, locale, voice, audioCodec, audioFormat).getStream();
FileOutputStream fos = new FileOutputStream(audioFileInCache)) {
copyStream(is, fos);
// write text to file for transparency too
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
@@ -0,0 +1,42 @@
/**
* 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.cloudapi;

import java.io.InputStream;

import org.eclipse.jdt.annotation.NonNullByDefault;

/**
* Class to store an InputStream and its length
*
* @author Laurent Garnier - Initial contribution
*/
@NonNullByDefault
public class SizedInputStream {

private InputStream stream;
private long length;

public SizedInputStream(InputStream stream, long length) {
this.stream = stream;
this.length = length;
}

public InputStream getStream() {
return stream;
}

public long getLength() {
return length;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,6 @@
package org.openhab.voice.voicerss.internal.cloudapi;

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

Expand Down Expand Up @@ -74,11 +73,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 SizedInputStream 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,
SizedInputStream 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 @@ -215,7 +215,7 @@ public Set<String> getAvailableVoices(Locale locale) {
* dependencies.
*/
@Override
public InputStream getTextToSpeech(String apiKey, String text, String locale, String voice, String audioCodec,
public SizedInputStream 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) {
Expand Down Expand Up @@ -243,9 +243,12 @@ public InputStream getTextToSpeech(String apiKey, String text, String locale, St
}
}
String contentType = connection.getHeaderField("Content-Type");
long length = connection.getContentLengthLong();
InputStream is = connection.getInputStream();
// check if content type is text/plain, then we have an error
if (contentType.contains("text/plain")) {
if (length < 0) {
throw new IOException("Could not read audio content, missing header Content-Length");
} else if (contentType.contains("text/plain")) {
byte[] bytes = new byte[256];
is.read(bytes, 0, 256);
// close before throwing an exception
Expand All @@ -259,7 +262,7 @@ 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;
return new SizedInputStream(is, length);
}
}

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 80e8308

Please sign in to comment.