]> git.basschouten.com Git - openhab-addons.git/commitdiff
[voicerss] Add LRU cache (#14561)
authorlolodomo <lg.hc@free.fr>
Wed, 12 Jul 2023 19:33:55 +0000 (21:33 +0200)
committerGitHub <noreply@github.com>
Wed, 12 Jul 2023 19:33:55 +0000 (21:33 +0200)
Signed-off-by: Laurent Garnier <lg.hc@free.fr>
bundles/org.openhab.voice.voicerss/README.md
bundles/org.openhab.voice.voicerss/src/main/java/org/openhab/voice/voicerss/internal/VoiceRSSRawAudioStream.java [new file with mode: 0644]
bundles/org.openhab.voice.voicerss/src/main/java/org/openhab/voice/voicerss/internal/VoiceRSSTTSService.java
bundles/org.openhab.voice.voicerss/src/main/java/org/openhab/voice/voicerss/internal/cloudapi/CachedVoiceRSSCloudImpl.java
bundles/org.openhab.voice.voicerss/src/main/java/org/openhab/voice/voicerss/internal/cloudapi/VoiceRSSCloudAPI.java
bundles/org.openhab.voice.voicerss/src/main/java/org/openhab/voice/voicerss/internal/cloudapi/VoiceRSSCloudImpl.java
bundles/org.openhab.voice.voicerss/src/test/java/org/openhab/voice/voicerss/internal/VoiceRSSTTSServiceTest.java

index 881289ee51c7845d6605a96c5528ab09164979db..bff099818d15a5a4dfc38eab640520fa4b634122 100644 (file)
@@ -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:
diff --git a/bundles/org.openhab.voice.voicerss/src/main/java/org/openhab/voice/voicerss/internal/VoiceRSSRawAudioStream.java b/bundles/org.openhab.voice.voicerss/src/main/java/org/openhab/voice/voicerss/internal/VoiceRSSRawAudioStream.java
new file mode 100644 (file)
index 0000000..04bfeee
--- /dev/null
@@ -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();
+    }
+}
index 40bb18b5bdf8457596dbbe8959edbfae11012784..a2c1daf18db49b531e25e855f1584674f8a1dc53 100644 (file)
@@ -27,6 +27,8 @@ import org.openhab.core.audio.AudioException;
 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;
@@ -35,6 +37,7 @@ 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;
 
@@ -45,9 +48,10 @@ import org.slf4j.LoggerFactory;
  * @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";
@@ -87,6 +91,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
      */
@@ -130,6 +139,43 @@ public class VoiceRSSTTSService implements TTSService {
         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) {
@@ -145,14 +191,11 @@ public class VoiceRSSTTSService implements TTSService {
             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);
         }
index 0dcc964814d815f2b4d18b4825a9331d36143522..de7cfb556cde909e601d83116a46d6fffc573423 100644 (file)
@@ -69,8 +69,8 @@ public class CachedVoiceRSSCloudImpl extends VoiceRSSCloudImpl {
         }
 
         // 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
@@ -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
      * will be preceded by the locale and suffixed by the format if it is not the
index 7eac0507ee9c5b3cf8ac071d6d5c86863446b1b8..a461bf639f26668343b69584dca20067b1f5b09d 100644 (file)
 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.internal.VoiceRSSRawAudioStream;
 
 /**
  * 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.
      *
-     * @return A set of @{link {@link Locale} supported
+     * @return A set of {@link Locale} supported
      */
     Set<Locale> getAvailableLocales();
 
@@ -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;
 }
index b81da7e98f9f908e28b3bb9a2968a9489de2753d..d65f696788dbd0dda187b3c2d4c5ae4774e18518 100644 (file)
@@ -28,6 +28,8 @@ import java.util.Map.Entry;
 import java.util.Set;
 
 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.LoggerFactory;
 
@@ -215,8 +217,8 @@ public class VoiceRSSCloudImpl implements VoiceRSSCloudAPI {
      * 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, "***"));
@@ -259,7 +261,8 @@ public class VoiceRSSCloudImpl implements VoiceRSSCloudAPI {
             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());
         }
     }
 
index 2957bf48c3828908ab5afb0a84a93ae1bf50d1d1..b1fef188161aa1ed66b88b133dd9e01a10ae3769 100644 (file)
@@ -18,12 +18,16 @@ import static org.hamcrest.core.IsNot.not;
 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}.
@@ -43,6 +47,8 @@ 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.
      */
@@ -50,7 +56,10 @@ public class VoiceRSSTTSServiceTest {
 
     @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;