From 8f0dd94343127bdacdc64b8b63de3c016b6a6b88 Mon Sep 17 00:00:00 2001 From: lolodomo Date: Sun, 30 Jan 2022 18:27:51 +0100 Subject: [PATCH] [voice] Update dialog processing (#2693) Related to #2688 Updated methods startDialog New method stopDialog Null annotations added to the class DialogProcessor Allow translation of sentences "said" by the dialog processor in case of error 2 console commands added to start and stop a dialog Enhanced integration tests Signed-off-by: Laurent Garnier --- .../org/openhab/core/voice/VoiceManager.java | 41 ++- .../core/voice/internal/DialogProcessor.java | 174 ++++++++++--- .../VoiceConsoleCommandExtension.java | 47 +++- .../core/voice/internal/VoiceManagerImpl.java | 99 +++++-- .../resources/OH-INF/i18n/voice.properties | 4 + .../HumanLanguageInterpreterStub.java | 28 +- .../core/voice/internal/KSServiceStub.java | 37 ++- .../core/voice/internal/STTServiceStub.java | 42 ++- .../core/voice/internal/TTSServiceStub.java | 6 + .../voice/internal/VoiceManagerImplTest.java | 244 +++++++++++++++--- .../core/voice/internal/VoiceStub.java | 3 +- .../InterpretCommandTest.java | 4 +- .../VoicesCommandTest.java | 23 +- 13 files changed, 621 insertions(+), 131 deletions(-) diff --git a/bundles/org.openhab.core.voice/src/main/java/org/openhab/core/voice/VoiceManager.java b/bundles/org.openhab.core.voice/src/main/java/org/openhab/core/voice/VoiceManager.java index 76b7b47aa3..b77b0be035 100644 --- a/bundles/org.openhab.core.voice/src/main/java/org/openhab/core/voice/VoiceManager.java +++ b/bundles/org.openhab.core.voice/src/main/java/org/openhab/core/voice/VoiceManager.java @@ -29,6 +29,7 @@ import org.openhab.core.voice.text.InterpretationException; * * @author Kai Kreuzer - Initial contribution * @author Christoph Weitkamp - Added parameter to adjust the volume + * @author Laurent Garnier - Updated methods startDialog and added method stopDialog */ @NonNullByDefault public interface VoiceManager { @@ -121,20 +122,48 @@ public interface VoiceManager { Voice getPreferredVoice(Set voices); /** - * Starts listening for the keyword that starts a dialog + * Starts an infinite dialog sequence using all default services: keyword spotting on the default audio source, + * audio source listening to retrieve the question, speech to text conversion, interpretation, text to speech + * conversion and playback of the answer on the default audio sink * - * @throws IllegalStateException if required services are not available + * Only one dialog can be started for the default audio source. + * + * @throws IllegalStateException if required services are not available or the dialog is already started for the + * default audio source */ - void startDialog(); + void startDialog() throws IllegalStateException; /** - * Starts listening for the keyword that starts a dialog + * Starts an infinite dialog sequence: keyword spotting on the audio source, audio source listening to retrieve + * the question, speech to text conversion, interpretation, text to speech conversion and playback of the answer + * on the audio sink * - * @throws IllegalStateException if required services are not available + * Only one dialog can be started for an audio source. + * + * @param ks the keyword spotting service to use or null to use the default service + * @param stt the speech-to-text service to use or null to use the default service + * @param tts the text-to-speech service to use or null to use the default service + * @param hli the human language text interpreter to use or null to use the default service + * @param source the audio source to use or null to use the default source + * @param sink the audio sink to use or null to use the default sink + * @param Locale the locale to use or null to use the default locale + * @param keyword the keyword to use during keyword spotting or null to use the default keyword + * @param listeningItem the item to switch ON while listening to a question + * @throws IllegalStateException if required services are not available or the dialog is already started for this + * audio source */ void startDialog(@Nullable KSService ks, @Nullable STTService stt, @Nullable TTSService tts, @Nullable HumanLanguageInterpreter hli, @Nullable AudioSource source, @Nullable AudioSink sink, - @Nullable Locale locale, @Nullable String keyword, @Nullable String listeningItem); + @Nullable Locale locale, @Nullable String keyword, @Nullable String listeningItem) + throws IllegalStateException; + + /** + * Stop the dialog associated to an audio source + * + * @param source the audio source or null to consider the default audio source + * @throws IllegalStateException if no dialog is started for the audio source + */ + void stopDialog(@Nullable AudioSource source) throws IllegalStateException; /** * Retrieves a TTS service. diff --git a/bundles/org.openhab.core.voice/src/main/java/org/openhab/core/voice/internal/DialogProcessor.java b/bundles/org.openhab.core.voice/src/main/java/org/openhab/core/voice/internal/DialogProcessor.java index fb99c73c96..8b0a7b5198 100644 --- a/bundles/org.openhab.core.voice/src/main/java/org/openhab/core/voice/internal/DialogProcessor.java +++ b/bundles/org.openhab.core.voice/src/main/java/org/openhab/core/voice/internal/DialogProcessor.java @@ -18,6 +18,8 @@ import java.util.HashSet; import java.util.Locale; import java.util.Set; +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.eclipse.jdt.annotation.Nullable; import org.openhab.core.audio.AudioException; import org.openhab.core.audio.AudioFormat; import org.openhab.core.audio.AudioSink; @@ -26,6 +28,7 @@ import org.openhab.core.audio.AudioStream; import org.openhab.core.audio.UnsupportedAudioFormatException; import org.openhab.core.audio.UnsupportedAudioStreamException; import org.openhab.core.events.EventPublisher; +import org.openhab.core.i18n.TranslationProvider; import org.openhab.core.items.ItemUtil; import org.openhab.core.items.events.ItemEventFactory; import org.openhab.core.library.types.OnOffType; @@ -34,6 +37,7 @@ import org.openhab.core.voice.KSEvent; import org.openhab.core.voice.KSException; import org.openhab.core.voice.KSListener; import org.openhab.core.voice.KSService; +import org.openhab.core.voice.KSServiceHandle; import org.openhab.core.voice.KSpottedEvent; import org.openhab.core.voice.RecognitionStopEvent; import org.openhab.core.voice.STTEvent; @@ -48,6 +52,7 @@ import org.openhab.core.voice.TTSService; import org.openhab.core.voice.Voice; import org.openhab.core.voice.text.HumanLanguageInterpreter; import org.openhab.core.voice.text.InterpretationException; +import org.osgi.framework.Bundle; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -59,11 +64,28 @@ import org.slf4j.LoggerFactory; * @author Yannick Schaus - Send commands to an item to indicate the keyword has been spotted * @author Christoph Weitkamp - Added getSupportedStreams() and UnsupportedAudioStreamException * @author Christoph Weitkamp - Added parameter to adjust the volume + * @author Laurent Garnier - Added stop() + null annotations + resources releasing */ +@NonNullByDefault public class DialogProcessor implements KSListener, STTListener { private final Logger logger = LoggerFactory.getLogger(DialogProcessor.class); + private final KSService ks; + private final STTService stt; + private final TTSService tts; + private final HumanLanguageInterpreter hli; + private final AudioSource source; + private final AudioSink sink; + private final Locale locale; + private final String keyword; + private final @Nullable String listeningItem; + private final EventPublisher eventPublisher; + private final TranslationProvider i18nProvider; + private final Bundle bundle; + + private final @Nullable AudioFormat format; + /** * If the processor is currently processing a keyword event and thus should not spot further ones. */ @@ -74,24 +96,15 @@ public class DialogProcessor implements KSListener, STTListener { */ private boolean isSTTServerAborting = false; - private STTServiceHandle sttServiceHandle; + private @Nullable KSServiceHandle ksServiceHandle; + private @Nullable STTServiceHandle sttServiceHandle; - private final KSService ks; - private final STTService stt; - private final TTSService tts; - private final HumanLanguageInterpreter hli; - private final AudioSource source; - private final AudioSink sink; - private final Locale locale; - private final String keyword; - private final String listeningItem; - private final EventPublisher eventPublisher; - - private final AudioFormat format; + private @Nullable AudioStream streamKS; + private @Nullable AudioStream streamSTT; public DialogProcessor(KSService ks, STTService stt, TTSService tts, HumanLanguageInterpreter hli, - AudioSource source, AudioSink sink, Locale locale, String keyword, String listeningItem, - EventPublisher eventPublisher) { + AudioSource source, AudioSink sink, Locale locale, String keyword, @Nullable String listeningItem, + EventPublisher eventPublisher, TranslationProvider i18nProvider, Bundle bundle) { this.locale = locale; this.ks = ks; this.hli = hli; @@ -102,16 +115,80 @@ public class DialogProcessor implements KSListener, STTListener { this.keyword = keyword; this.listeningItem = listeningItem; this.eventPublisher = eventPublisher; + this.i18nProvider = i18nProvider; + this.bundle = bundle; this.format = AudioFormat.getBestMatch(source.getSupportedFormats(), sink.getSupportedFormats()); } public void start() { + AudioFormat fmt = format; + if (fmt == null) { + logger.warn("No compatible audio format found between source '{}' and sink '{}'", source.getId(), + sink.getId()); + return; + } + abortKS(); + closeStreamKS(); try { - ks.spot(this, source.getInputStream(format), locale, keyword); - } catch (KSException e) { - logger.error("Encountered error calling spot: {}", e.getMessage()); + AudioStream stream = source.getInputStream(fmt); + streamKS = stream; + ksServiceHandle = ks.spot(this, stream, locale, keyword); } catch (AudioException e) { - logger.error("Error creating the audio stream", e); + logger.warn("Error creating the audio stream: {}", e.getMessage()); + } catch (KSException e) { + logger.warn("Encountered error calling spot: {}", e.getMessage()); + closeStreamKS(); + } + } + + public void stop() { + abortSTT(); + closeStreamSTT(); + abortKS(); + closeStreamKS(); + toggleProcessing(false); + } + + private void abortKS() { + KSServiceHandle handle = ksServiceHandle; + if (handle != null) { + handle.abort(); + ksServiceHandle = null; + } + } + + private void closeStreamKS() { + AudioStream stream = streamKS; + if (stream != null) { + // Due to the current implementation of JavaSoundAudioSource, the stream is not closed as it would + // lead to problem + // try { + // stream.close(); + // } catch (IOException e1) { + // } + streamKS = null; + } + } + + private void abortSTT() { + STTServiceHandle handle = sttServiceHandle; + if (handle != null) { + handle.abort(); + sttServiceHandle = null; + } + isSTTServerAborting = true; + } + + private void closeStreamSTT() { + AudioStream stream = streamSTT; + if (stream != null) { + // Due to the current implementation of JavaSoundAudioSource, the stream is not closed as it would + // lead to problem + // try { + // stream.close(); + // } catch (IOException e1) { + // } + streamSTT = null; } } @@ -120,9 +197,10 @@ public class DialogProcessor implements KSListener, STTListener { return; } processing = value; - if (listeningItem != null && ItemUtil.isValidItemName(listeningItem)) { + String item = listeningItem; + if (item != null && ItemUtil.isValidItemName(item)) { OnOffType command = (value) ? OnOffType.ON : OnOffType.OFF; - eventPublisher.post(ItemEventFactory.createCommandEvent(listeningItem, command)); + eventPublisher.post(ItemEventFactory.createCommandEvent(item, command)); } } @@ -132,18 +210,34 @@ public class DialogProcessor implements KSListener, STTListener { isSTTServerAborting = false; if (ksEvent instanceof KSpottedEvent) { toggleProcessing(true); - if (stt != null) { + abortSTT(); + closeStreamSTT(); + isSTTServerAborting = false; + AudioFormat fmt = format; + if (fmt != null) { try { - sttServiceHandle = stt.recognize(this, source.getInputStream(format), locale, new HashSet<>()); - } catch (STTException e) { - say("Error during recognition: " + e.getMessage()); + AudioStream stream = source.getInputStream(fmt); + streamSTT = stream; + sttServiceHandle = stt.recognize(this, stream, locale, new HashSet<>()); } catch (AudioException e) { - logger.error("Error creating the audio stream", e); + logger.warn("Error creating the audio stream: {}", e.getMessage()); + toggleProcessing(false); + } catch (STTException e) { + closeStreamSTT(); + toggleProcessing(false); + String msg = e.getMessage(); + String text = i18nProvider.getText(bundle, "error.stt-exception", null, locale); + if (msg != null) { + say(text == null ? msg : text.replace("{0}", msg)); + } else if (text != null) { + say(text.replace("{0}", "")); + } } } } else if (ksEvent instanceof KSErrorEvent) { KSErrorEvent kse = (KSErrorEvent) ksEvent; - say("Encountered error spotting keywords, " + kse.getMessage()); + String text = i18nProvider.getText(bundle, "error.ks-error", null, locale); + say(text == null ? kse.getMessage() : text.replace("{0}", kse.getMessage())); } } } @@ -152,8 +246,7 @@ public class DialogProcessor implements KSListener, STTListener { public synchronized void sttEventReceived(STTEvent sttEvent) { if (sttEvent instanceof SpeechRecognitionEvent) { if (!isSTTServerAborting) { - sttServiceHandle.abort(); - isSTTServerAborting = true; + abortSTT(); SpeechRecognitionEvent sre = (SpeechRecognitionEvent) sttEvent; String question = sre.getTranscript(); try { @@ -163,18 +256,21 @@ public class DialogProcessor implements KSListener, STTListener { say(answer); } } catch (InterpretationException e) { - say(e.getMessage()); + String msg = e.getMessage(); + if (msg != null) { + say(msg); + } } } } else if (sttEvent instanceof RecognitionStopEvent) { toggleProcessing(false); } else if (sttEvent instanceof SpeechRecognitionErrorEvent) { if (!isSTTServerAborting) { - sttServiceHandle.abort(); - isSTTServerAborting = true; + abortSTT(); toggleProcessing(false); SpeechRecognitionErrorEvent sre = (SpeechRecognitionErrorEvent) sttEvent; - say("Encountered error: " + sre.getMessage()); + String text = i18nProvider.getText(bundle, "error.stt-error", null, locale); + say(text == null ? sre.getMessage() : text.replace("{0}", sre.getMessage())); } } } @@ -210,13 +306,21 @@ public class DialogProcessor implements KSListener, STTListener { try { sink.process(audioStream); } catch (UnsupportedAudioFormatException | UnsupportedAudioStreamException e) { - logger.warn("Error saying '{}': {}", text, e.getMessage(), e); + if (logger.isDebugEnabled()) { + logger.debug("Error saying '{}': {}", text, e.getMessage(), e); + } else { + logger.warn("Error saying '{}': {}", text, e.getMessage()); + } } } else { logger.warn("Failed playing audio stream '{}' as audio doesn't support it.", audioStream); } } catch (TTSException e) { - logger.error("Error saying '{}': {}", text, e.getMessage()); + if (logger.isDebugEnabled()) { + logger.debug("Error saying '{}': {}", text, e.getMessage(), e); + } else { + logger.warn("Error saying '{}': {}", text, e.getMessage()); + } } } } diff --git a/bundles/org.openhab.core.voice/src/main/java/org/openhab/core/voice/internal/VoiceConsoleCommandExtension.java b/bundles/org.openhab.core.voice/src/main/java/org/openhab/core/voice/internal/VoiceConsoleCommandExtension.java index 95297b8f72..ad52a4923a 100644 --- a/bundles/org.openhab.core.voice/src/main/java/org/openhab/core/voice/internal/VoiceConsoleCommandExtension.java +++ b/bundles/org.openhab.core.voice/src/main/java/org/openhab/core/voice/internal/VoiceConsoleCommandExtension.java @@ -15,9 +15,13 @@ package org.openhab.core.voice.internal; import java.util.Arrays; import java.util.List; import java.util.Locale; +import java.util.Set; import org.eclipse.jdt.annotation.NonNullByDefault; import org.eclipse.jdt.annotation.Nullable; +import org.openhab.core.audio.AudioManager; +import org.openhab.core.audio.AudioSink; +import org.openhab.core.audio.AudioSource; import org.openhab.core.i18n.LocaleProvider; import org.openhab.core.io.console.Console; import org.openhab.core.io.console.extensions.AbstractConsoleCommandExtension; @@ -29,6 +33,7 @@ import org.openhab.core.items.ItemRegistry; import org.openhab.core.voice.TTSService; import org.openhab.core.voice.Voice; import org.openhab.core.voice.VoiceManager; +import org.openhab.core.voice.text.HumanLanguageInterpreter; import org.openhab.core.voice.text.InterpretationException; import org.osgi.service.component.annotations.Activate; import org.osgi.service.component.annotations.Component; @@ -39,6 +44,7 @@ import org.osgi.service.component.annotations.Reference; * * @author Kai Kreuzer - Initial contribution * @author Wouter Born - Sort TTS voices + * @author Laurent Garnier - Added sub-commands startdialog and stopdialog */ @Component(service = ConsoleCommandExtension.class) @NonNullByDefault @@ -47,16 +53,21 @@ public class VoiceConsoleCommandExtension extends AbstractConsoleCommandExtensio private static final String SUBCMD_SAY = "say"; private static final String SUBCMD_INTERPRET = "interpret"; private static final String SUBCMD_VOICES = "voices"; + private static final String SUBCMD_START_DIALOG = "startdialog"; + private static final String SUBCMD_STOP_DIALOG = "stopdialog"; private final ItemRegistry itemRegistry; private final VoiceManager voiceManager; + private final AudioManager audioManager; private final LocaleProvider localeProvider; @Activate public VoiceConsoleCommandExtension(final @Reference VoiceManager voiceManager, - final @Reference LocaleProvider localeProvider, final @Reference ItemRegistry itemRegistry) { + final @Reference AudioManager audioManager, final @Reference LocaleProvider localeProvider, + final @Reference ItemRegistry itemRegistry) { super("voice", "Commands around voice enablement features."); this.voiceManager = voiceManager; + this.audioManager = audioManager; this.localeProvider = localeProvider; this.itemRegistry = itemRegistry; } @@ -65,7 +76,11 @@ public class VoiceConsoleCommandExtension extends AbstractConsoleCommandExtensio public List getUsages() { return List.of(buildCommandUsage(SUBCMD_SAY + " ", "speaks a text"), buildCommandUsage(SUBCMD_INTERPRET + " ", "interprets a human language command"), - buildCommandUsage(SUBCMD_VOICES, "lists available voices of the TTS services")); + buildCommandUsage(SUBCMD_VOICES, "lists available voices of the TTS services"), + buildCommandUsage(SUBCMD_START_DIALOG + " [ ]", + "start a new dialog processing using the default services or the services identified with provided arguments"), + buildCommandUsage(SUBCMD_STOP_DIALOG + " []", + "stop the dialog processing for the default audio source or the audio source identified with provided argument")); } @Override @@ -99,6 +114,24 @@ public class VoiceConsoleCommandExtension extends AbstractConsoleCommandExtensio } } return; + case SUBCMD_START_DIALOG: + try { + AudioSource source = args.length < 2 ? null : getSource(args[1]); + HumanLanguageInterpreter hli = args.length < 3 ? null : voiceManager.getHLI(args[2]); + AudioSink sink = args.length < 4 ? null : audioManager.getSink(args[3]); + String keyword = args.length < 5 ? null : args[4]; + voiceManager.startDialog(null, null, null, hli, source, sink, null, keyword, null); + } catch (IllegalStateException e) { + console.println(e.getMessage()); + } + break; + case SUBCMD_STOP_DIALOG: + try { + voiceManager.stopDialog(args.length < 2 ? null : getSource(args[1])); + } catch (IllegalStateException e) { + console.println(e.getMessage()); + } + break; default: break; } @@ -158,4 +191,14 @@ public class VoiceConsoleCommandExtension extends AbstractConsoleCommandExtensio } voiceManager.say(msg.toString()); } + + private @Nullable AudioSource getSource(@Nullable String sourceId) { + Set sources = audioManager.getAllSources(); + for (AudioSource source : sources) { + if (source.getId().equals(sourceId)) { + return source; + } + } + return null; + } } diff --git a/bundles/org.openhab.core.voice/src/main/java/org/openhab/core/voice/internal/VoiceManagerImpl.java b/bundles/org.openhab.core.voice/src/main/java/org/openhab/core/voice/internal/VoiceManagerImpl.java index 9074eb6f90..626e2b1f3c 100644 --- a/bundles/org.openhab.core.voice/src/main/java/org/openhab/core/voice/internal/VoiceManagerImpl.java +++ b/bundles/org.openhab.core.voice/src/main/java/org/openhab/core/voice/internal/VoiceManagerImpl.java @@ -42,6 +42,7 @@ import org.openhab.core.config.core.ConfigurableService; import org.openhab.core.config.core.ParameterOption; import org.openhab.core.events.EventPublisher; import org.openhab.core.i18n.LocaleProvider; +import org.openhab.core.i18n.TranslationProvider; import org.openhab.core.library.types.PercentType; import org.openhab.core.voice.KSService; import org.openhab.core.voice.STTService; @@ -51,9 +52,12 @@ import org.openhab.core.voice.Voice; import org.openhab.core.voice.VoiceManager; import org.openhab.core.voice.text.HumanLanguageInterpreter; import org.openhab.core.voice.text.InterpretationException; +import org.osgi.framework.Bundle; +import org.osgi.framework.BundleContext; 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.Deactivate; import org.osgi.service.component.annotations.Modified; import org.osgi.service.component.annotations.Reference; import org.osgi.service.component.annotations.ReferenceCardinality; @@ -69,6 +73,7 @@ import org.slf4j.LoggerFactory; * @author Christoph Weitkamp - Added getSupportedStreams() and UnsupportedAudioStreamException * @author Christoph Weitkamp - Added parameter to adjust the volume * @author Wouter Born - Sort TTS options + * @author Laurent Garnier - Updated methods startDialog and added method stopDialog */ @Component(immediate = true, configurationPid = VoiceManagerImpl.CONFIGURATION_PID, // property = Constants.SERVICE_PID + "=org.openhab.voice") @@ -103,6 +108,9 @@ public class VoiceManagerImpl implements VoiceManager, ConfigOptionProvider { private final LocaleProvider localeProvider; private final AudioManager audioManager; private final EventPublisher eventPublisher; + private final TranslationProvider i18nProvider; + + private @Nullable Bundle bundle; /** * default settings filled through the service configuration @@ -116,19 +124,28 @@ public class VoiceManagerImpl implements VoiceManager, ConfigOptionProvider { private @Nullable String defaultVoice; private final Map defaultVoices = new HashMap<>(); + private Map dialogProcessors = new HashMap<>(); + @Activate public VoiceManagerImpl(final @Reference LocaleProvider localeProvider, final @Reference AudioManager audioManager, - final @Reference EventPublisher eventPublisher) { + final @Reference EventPublisher eventPublisher, final @Reference TranslationProvider i18nProvider) { this.localeProvider = localeProvider; this.audioManager = audioManager; this.eventPublisher = eventPublisher; + this.i18nProvider = i18nProvider; } @Activate - protected void activate(Map config) { + protected void activate(BundleContext bundleContext, Map config) { + this.bundle = bundleContext.getBundle(); modified(config); } + @Deactivate + protected void deactivate() { + stopAllDialogs(); + } + @SuppressWarnings("null") @Modified protected void modified(Map config) { @@ -257,7 +274,11 @@ public class VoiceManagerImpl implements VoiceManager, ConfigOptionProvider { } } } catch (TTSException | UnsupportedAudioFormatException | UnsupportedAudioStreamException e) { - logger.warn("Error saying '{}': {}", text, e.getMessage(), e); + if (logger.isDebugEnabled()) { + logger.debug("Error saying '{}': {}", text, e.getMessage(), e); + } else { + logger.warn("Error saying '{}': {}", text, e.getMessage()); + } } } @@ -458,38 +479,72 @@ public class VoiceManagerImpl implements VoiceManager, ConfigOptionProvider { } @Override - public void startDialog() { + public void startDialog() throws IllegalStateException { startDialog(null, null, null, null, null, null, null, this.keyword, this.listeningItem); } @Override - public void startDialog(@Nullable KSService ksService, @Nullable STTService sttService, - @Nullable TTSService ttsService, @Nullable HumanLanguageInterpreter interpreter, - @Nullable AudioSource audioSource, @Nullable AudioSink audioSink, @Nullable Locale locale, - @Nullable String keyword, @Nullable String listeningItem) { + public void startDialog(@Nullable KSService ks, @Nullable STTService stt, @Nullable TTSService tts, + @Nullable HumanLanguageInterpreter hli, @Nullable AudioSource source, @Nullable AudioSink sink, + @Nullable Locale locale, @Nullable String keyword, @Nullable String listeningItem) + throws IllegalStateException { // use defaults, if null - KSService ks = (ksService == null) ? getKS() : ksService; - STTService stt = (sttService == null) ? getSTT() : sttService; - TTSService tts = (ttsService == null) ? getTTS() : ttsService; - HumanLanguageInterpreter hli = (interpreter == null) ? getHLI() : interpreter; - AudioSource source = (audioSource == null) ? audioManager.getSource() : audioSource; - AudioSink sink = (audioSink == null) ? audioManager.getSink() : audioSink; + KSService ksService = (ks == null) ? getKS() : ks; + STTService sttService = (stt == null) ? getSTT() : stt; + TTSService ttsService = (tts == null) ? getTTS() : tts; + HumanLanguageInterpreter interpreter = (hli == null) ? getHLI() : hli; + AudioSource audioSource = (source == null) ? audioManager.getSource() : source; + AudioSink audioSink = (sink == null) ? audioManager.getSink() : sink; Locale loc = (locale == null) ? localeProvider.getLocale() : locale; String kw = (keyword == null) ? this.keyword : keyword; String item = (listeningItem == null) ? this.listeningItem : listeningItem; + Bundle b = bundle; - if (ks != null && stt != null && tts != null && hli != null && source != null && sink != null && loc != null - && kw != null) { - DialogProcessor processor = new DialogProcessor(ks, stt, tts, hli, source, sink, loc, kw, item, - this.eventPublisher); - processor.start(); + if (ksService != null && sttService != null && ttsService != null && interpreter != null && audioSource != null + && audioSink != null && b != null) { + DialogProcessor processor = dialogProcessors.get(audioSource.getId()); + if (processor == null) { + logger.debug("Starting a new dialog for source {} ({})", audioSource.getLabel(null), + audioSource.getId()); + processor = new DialogProcessor(ksService, sttService, ttsService, interpreter, audioSource, audioSink, + loc, kw, item, this.eventPublisher, this.i18nProvider, b); + dialogProcessors.put(audioSource.getId(), processor); + processor.start(); + } else { + throw new IllegalStateException( + String.format("Cannot start dialog as a dialog is already started for audio source '%s'.", + audioSource.getLabel(null))); + } } else { - String msg = "Cannot start dialog as services are missing."; - logger.error(msg); - throw new IllegalStateException(msg); + throw new IllegalStateException("Cannot start dialog as services are missing."); } } + @Override + public void stopDialog(@Nullable AudioSource source) throws IllegalStateException { + AudioSource audioSource = (source == null) ? audioManager.getSource() : source; + if (audioSource != null) { + DialogProcessor processor = dialogProcessors.remove(audioSource.getId()); + if (processor != null) { + processor.stop(); + logger.debug("Dialog stopped for source {} ({})", audioSource.getLabel(null), audioSource.getId()); + } else { + throw new IllegalStateException( + String.format("Cannot stop dialog as no dialog is started for audio source '%s'.", + audioSource.getLabel(null))); + } + } else { + throw new IllegalStateException("Cannot stop dialog as audio source is missing."); + } + } + + private void stopAllDialogs() { + dialogProcessors.values().forEach(processor -> { + processor.stop(); + }); + dialogProcessors.clear(); + } + @Reference(cardinality = ReferenceCardinality.MULTIPLE, policy = ReferencePolicy.DYNAMIC) protected void addKSService(KSService ksService) { this.ksServices.put(ksService.getId(), ksService); diff --git a/bundles/org.openhab.core.voice/src/main/resources/OH-INF/i18n/voice.properties b/bundles/org.openhab.core.voice/src/main/resources/OH-INF/i18n/voice.properties index 5ed7f79ebb..440e831151 100644 --- a/bundles/org.openhab.core.voice/src/main/resources/OH-INF/i18n/voice.properties +++ b/bundles/org.openhab.core.voice/src/main/resources/OH-INF/i18n/voice.properties @@ -14,3 +14,7 @@ system.config.voice.listeningItem.label = Listening Switch system.config.voice.listeningItem.description = If provided, the item will be switched on during the period when the dialog processor has spotted the keyword and is listening for commands. service.system.voice.label = Voice + +error.ks-error = Encountered error while spotting keywords, {0} +error.stt-error = Encountered error while recognizing text, {0} +error.stt-exception = Error during recognition, {0} diff --git a/itests/org.openhab.core.voice.tests/src/main/java/org/openhab/core/voice/internal/HumanLanguageInterpreterStub.java b/itests/org.openhab.core.voice.tests/src/main/java/org/openhab/core/voice/internal/HumanLanguageInterpreterStub.java index a666528f19..e96c55e54a 100644 --- a/itests/org.openhab.core.voice.tests/src/main/java/org/openhab/core/voice/internal/HumanLanguageInterpreterStub.java +++ b/itests/org.openhab.core.voice.tests/src/main/java/org/openhab/core/voice/internal/HumanLanguageInterpreterStub.java @@ -30,22 +30,20 @@ import org.openhab.core.voice.text.InterpretationException; public class HumanLanguageInterpreterStub implements HumanLanguageInterpreter { private static final String INTERPRETED_TEXT = "Interpreted text"; - private static final String EXCEPTION_MESSAGE = "Exception message"; + private static final String EXCEPTION_MESSAGE = "interpretation exception"; private static final String HLI_STUB_ID = "HLIStubID"; private static final String HLI_STUB_LABEL = "HLIStubLabel"; - private boolean isInterpretationExceptionExpected; + private boolean exceptionExpected; + private String question = ""; + private String answer = ""; @Override public String getId() { return HLI_STUB_ID; } - public void setIsInterpretationExceptionExpected(boolean value) { - isInterpretationExceptionExpected = value; - } - @Override public String getLabel(Locale locale) { return HLI_STUB_LABEL; @@ -53,10 +51,12 @@ public class HumanLanguageInterpreterStub implements HumanLanguageInterpreter { @Override public String interpret(Locale locale, String text) throws InterpretationException { - if (isInterpretationExceptionExpected) { + question = text; + if (exceptionExpected) { throw new InterpretationException(EXCEPTION_MESSAGE); } else { - return INTERPRETED_TEXT; + answer = INTERPRETED_TEXT; + return answer; } } @@ -78,6 +78,18 @@ public class HumanLanguageInterpreterStub implements HumanLanguageInterpreter { return null; } + public void setExceptionExpected(boolean exceptionExpected) { + this.exceptionExpected = exceptionExpected; + } + + public String getQuestion() { + return question; + } + + public String getAnswer() { + return answer; + } + @Override public String toString() { return getId(); diff --git a/itests/org.openhab.core.voice.tests/src/main/java/org/openhab/core/voice/internal/KSServiceStub.java b/itests/org.openhab.core.voice.tests/src/main/java/org/openhab/core/voice/internal/KSServiceStub.java index f5f61ebe56..e70b09b2e2 100644 --- a/itests/org.openhab.core.voice.tests/src/main/java/org/openhab/core/voice/internal/KSServiceStub.java +++ b/itests/org.openhab.core.voice.tests/src/main/java/org/openhab/core/voice/internal/KSServiceStub.java @@ -17,10 +17,12 @@ import java.util.Set; import org.openhab.core.audio.AudioFormat; import org.openhab.core.audio.AudioStream; +import org.openhab.core.voice.KSErrorEvent; import org.openhab.core.voice.KSException; import org.openhab.core.voice.KSListener; import org.openhab.core.voice.KSService; import org.openhab.core.voice.KSServiceHandle; +import org.openhab.core.voice.KSpottedEvent; /** * A {@link KSService} stub used for the tests. @@ -35,18 +37,19 @@ public class KSServiceStub implements KSService { private static final String KSSERVICE_STUB_ID = "ksServiceStubID"; private static final String KSSERVICE_STUB_LABEL = "ksServiceStubLabel"; + private static final String EXCEPTION_MESSAGE = "keyword spotting exception"; + private static final String ERROR_MESSAGE = "keyword spotting error"; + + private boolean exceptionExpected; + private boolean errorExpected; private boolean isWordSpotted; - private boolean isKSExceptionExpected; + private boolean aborted; @Override public String getId() { return KSSERVICE_STUB_ID; } - public void setIsKsExceptionExpected(boolean value) { - this.isKSExceptionExpected = value; - } - @Override public String getLabel(Locale locale) { return KSSERVICE_STUB_LABEL; @@ -65,22 +68,40 @@ public class KSServiceStub implements KSService { @Override public KSServiceHandle spot(KSListener ksListener, AudioStream audioStream, Locale locale, String keyword) throws KSException { - if (isKSExceptionExpected) { - throw new KSException("Expected KSException"); + if (exceptionExpected) { + throw new KSException(EXCEPTION_MESSAGE); } else { - isWordSpotted = true; + if (errorExpected) { + ksListener.ksEventReceived(new KSErrorEvent(ERROR_MESSAGE)); + } else { + isWordSpotted = true; + ksListener.ksEventReceived(new KSpottedEvent()); + } return new KSServiceHandle() { @Override public void abort() { + aborted = true; } }; } } + public void setExceptionExpected(boolean exceptionExpected) { + this.exceptionExpected = exceptionExpected; + } + + public void setErrorExpected(boolean errorExpected) { + this.errorExpected = errorExpected; + } + public boolean isWordSpotted() { return isWordSpotted; } + public boolean isAborted() { + return aborted; + } + @Override public String toString() { return getId(); diff --git a/itests/org.openhab.core.voice.tests/src/main/java/org/openhab/core/voice/internal/STTServiceStub.java b/itests/org.openhab.core.voice.tests/src/main/java/org/openhab/core/voice/internal/STTServiceStub.java index c64a71e2d7..12ac6523a8 100644 --- a/itests/org.openhab.core.voice.tests/src/main/java/org/openhab/core/voice/internal/STTServiceStub.java +++ b/itests/org.openhab.core.voice.tests/src/main/java/org/openhab/core/voice/internal/STTServiceStub.java @@ -23,6 +23,8 @@ import org.openhab.core.voice.STTException; import org.openhab.core.voice.STTListener; import org.openhab.core.voice.STTService; import org.openhab.core.voice.STTServiceHandle; +import org.openhab.core.voice.SpeechRecognitionErrorEvent; +import org.openhab.core.voice.SpeechRecognitionEvent; /** * A {@link STTService} stub used for the tests. @@ -38,6 +40,14 @@ public class STTServiceStub implements STTService { private static final String STTSERVICE_STUB_ID = "sttServiceStubID"; private static final String STTSERVICE_STUB_LABEL = "sttServiceStubLabel"; + private static final String RECOGNIZED_TEXT = "Recognized text"; + private static final String EXCEPTION_MESSAGE = "STT exception"; + private static final String ERROR_MESSAGE = "STT error"; + + private boolean exceptionExpected; + private boolean errorExpected; + private boolean recognized; + @Override public String getId() { return STTSERVICE_STUB_ID; @@ -61,12 +71,34 @@ public class STTServiceStub implements STTService { @Override public STTServiceHandle recognize(STTListener sttListener, AudioStream audioStream, Locale locale, Set grammars) throws STTException { - return new STTServiceHandle() { - // this method will not be used in the tests - @Override - public void abort() { + if (exceptionExpected) { + throw new STTException(EXCEPTION_MESSAGE); + } else { + if (errorExpected) { + sttListener.sttEventReceived(new SpeechRecognitionErrorEvent(ERROR_MESSAGE)); + } else { + recognized = true; + sttListener.sttEventReceived(new SpeechRecognitionEvent(RECOGNIZED_TEXT, 0.75f)); } - }; + return new STTServiceHandle() { + // this method will not be used in the tests + @Override + public void abort() { + } + }; + } + } + + public void setExceptionExpected(boolean exceptionExpected) { + this.exceptionExpected = exceptionExpected; + } + + public void setErrorExpected(boolean errorExpected) { + this.errorExpected = errorExpected; + } + + public boolean isRecognized() { + return recognized; } @Override diff --git a/itests/org.openhab.core.voice.tests/src/main/java/org/openhab/core/voice/internal/TTSServiceStub.java b/itests/org.openhab.core.voice.tests/src/main/java/org/openhab/core/voice/internal/TTSServiceStub.java index 93f64888d4..081ae313c2 100644 --- a/itests/org.openhab.core.voice.tests/src/main/java/org/openhab/core/voice/internal/TTSServiceStub.java +++ b/itests/org.openhab.core.voice.tests/src/main/java/org/openhab/core/voice/internal/TTSServiceStub.java @@ -44,6 +44,7 @@ public class TTSServiceStub implements TTSService { private static final String TTS_SERVICE_STUB_LABEL = "ttsServiceStubLabel"; private @Nullable BundleContext context; + private String synthesized = ""; public TTSServiceStub() { } @@ -88,6 +89,7 @@ public class TTSServiceStub implements TTSService { @Override public AudioStream synthesize(String text, Voice voice, final AudioFormat requestedFormat) throws TTSException { + synthesized = text; return new AudioStream() { @Override @@ -103,6 +105,10 @@ public class TTSServiceStub implements TTSService { }; } + public String getSynthesized() { + return synthesized; + } + @Override public String toString() { return getId(); diff --git a/itests/org.openhab.core.voice.tests/src/main/java/org/openhab/core/voice/internal/VoiceManagerImplTest.java b/itests/org.openhab.core.voice.tests/src/main/java/org/openhab/core/voice/internal/VoiceManagerImplTest.java index 4b46705a83..04b31cbb89 100644 --- a/itests/org.openhab.core.voice.tests/src/main/java/org/openhab/core/voice/internal/VoiceManagerImplTest.java +++ b/itests/org.openhab.core.voice.tests/src/main/java/org/openhab/core/voice/internal/VoiceManagerImplTest.java @@ -29,6 +29,8 @@ import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.openhab.core.audio.AudioManager; import org.openhab.core.config.core.ParameterOption; +import org.openhab.core.i18n.LocaleProvider; +import org.openhab.core.i18n.TranslationProvider; import org.openhab.core.test.java.JavaOSGiTest; import org.openhab.core.voice.Voice; import org.openhab.core.voice.VoiceManager; @@ -44,14 +46,19 @@ import org.osgi.service.cm.ConfigurationAdmin; * @author Velin Yordanov - migrated tests from groovy to java */ public class VoiceManagerImplTest extends JavaOSGiTest { + private static final String CONFIG_DEFAULT_SINK = "defaultSink"; + private static final String CONFIG_DEFAULT_SOURCE = "defaultSource"; private static final String CONFIG_DEFAULT_HLI = "defaultHLI"; private static final String CONFIG_DEFAULT_KS = "defaultKS"; private static final String CONFIG_DEFAULT_STT = "defaultSTT"; private static final String CONFIG_DEFAULT_VOICE = "defaultVoice"; private static final String CONFIG_DEFAULT_TTS = "defaultTTS"; private static final String CONFIG_KEYWORD = "keyword"; + private static final String CONFIG_LANGUAGE = "language"; private VoiceManagerImpl voiceManager; private AudioManager audioManager; + private LocaleProvider localeProvider; + private TranslationProvider i18nProvider; private SinkStub sink; private TTSServiceStub ttsService; private VoiceStub voice; @@ -70,6 +77,7 @@ public class VoiceManagerImplTest extends JavaOSGiTest { registerService(sink); registerService(voice); + registerService(source); ConfigurationAdmin configAdmin = super.getService(ConfigurationAdmin.class); @@ -77,11 +85,24 @@ public class VoiceManagerImplTest extends JavaOSGiTest { assertNotNull(audioManager); Dictionary audioConfig = new Hashtable<>(); - audioConfig.put("defaultSink", sink.getId()); + audioConfig.put(CONFIG_DEFAULT_SINK, sink.getId()); + audioConfig.put(CONFIG_DEFAULT_SOURCE, source.getId()); Configuration configuration = configAdmin.getConfiguration("org.openhab.audio", null); configuration.update(audioConfig); configuration.update(); + localeProvider = getService(LocaleProvider.class); + assertNotNull(localeProvider); + + Dictionary localeConfig = new Hashtable<>(); + localeConfig.put(CONFIG_LANGUAGE, Locale.ENGLISH.getLanguage()); + configuration = configAdmin.getConfiguration("org.openhab.i18n", null); + configuration.update(localeConfig); + configuration.update(); + + i18nProvider = getService(TranslationProvider.class); + assertNotNull(i18nProvider); + voiceManager = getService(VoiceManager.class, VoiceManagerImpl.class); assertNotNull(voiceManager); @@ -94,34 +115,27 @@ public class VoiceManagerImplTest extends JavaOSGiTest { @Test public void saySomethingWhenTheDefaultTTSIsSetAndItIsARegisteredService() { registerService(ttsService); - voiceManager.say("hello", null, sink.getId()); + voiceManager.say("hello Jack", null, sink.getId()); assertTrue(sink.getIsStreamProcessed()); } @Test public void saySomethingWithAGivenVoiceIdWhenTheDefaultTTSIsSetAndItIsARegisteredService() { registerService(ttsService); - voiceManager.say("hello", voice.getUID(), sink.getId()); + voiceManager.say("hello John", voice.getUID(), sink.getId()); assertTrue(sink.getIsStreamProcessed()); } @Test public void saySomethingWhenTheVoiceIdIsNotFullyQualifiedTheDefaultTtsIsSetAndItIsARegisteredService() { registerService(ttsService); - voiceManager.say("hello", "anotherVoiceId", sink.getId()); + voiceManager.say("hello Kate", "anotherVoiceId", sink.getId()); assertFalse(sink.getIsStreamProcessed()); } - @Test - public void testTheVoiceManagerWithAGivenVoiceIdAndSinkIdWhenTheDefaultTtsIsSetAndItIsARegisteredService() { - registerService(ttsService); - voiceManager.say("hello", voice.getUID(), sink.getId()); - assertTrue(sink.getIsStreamProcessed()); - } - @Test public void saySomethingWhenTheDefaultTtsIsSetButItIsNotARegisteredService() { - voiceManager.say("hello", null, sink.getId()); + voiceManager.say("hello Jennifer", null, sink.getId()); assertFalse(sink.getIsStreamProcessed()); } @@ -153,7 +167,13 @@ public class VoiceManagerImplTest extends JavaOSGiTest { Configuration configuration = configAdmin.getConfiguration(VoiceManagerImpl.CONFIGURATION_PID); configuration.update(voiceConfig); - String result = voiceManager.interpret("something", hliStub.getId()); + // Wait some time to be sure that the configuration will be updated + try { + Thread.sleep(1000); + } catch (InterruptedException e) { + } + + String result = voiceManager.interpret("something", null); assertThat(result, is("Interpreted text")); } @@ -161,69 +181,181 @@ public class VoiceManagerImplTest extends JavaOSGiTest { public void verifyThatADialogIsNotStartedWhenAnyOfTheRequiredServiceIsNull() { sttService = new STTServiceStub(); ksService = new KSServiceStub(); - ttsService = null; hliStub = new HumanLanguageInterpreterStub(); - source = new AudioSourceStub(); - assertThrows(IllegalStateException.class, () -> voiceManager.startDialog(ksService, sttService, ttsService, - hliStub, source, sink, Locale.getDefault(), "word", null)); + assertThrows(IllegalStateException.class, () -> voiceManager.startDialog(ksService, sttService, null, hliStub, + source, sink, Locale.ENGLISH, "word", null)); assertFalse(ksService.isWordSpotted()); + assertFalse(sink.getIsStreamProcessed()); } @Test public void startDialogWhenAllOfTheRequiredServicesAreAvailable() { sttService = new STTServiceStub(); ksService = new KSServiceStub(); - ttsService = new TTSServiceStub(); hliStub = new HumanLanguageInterpreterStub(); - source = new AudioSourceStub(); registerService(sttService); registerService(ksService); registerService(ttsService); registerService(hliStub); - registerService(source); - voiceManager.startDialog(ksService, sttService, ttsService, hliStub, source, sink, null, "word", null); + voiceManager.startDialog(ksService, sttService, ttsService, hliStub, source, sink, Locale.ENGLISH, "word", + null); assertTrue(ksService.isWordSpotted()); + assertTrue(sttService.isRecognized()); + assertThat(hliStub.getQuestion(), is("Recognized text")); + assertThat(hliStub.getAnswer(), is("Interpreted text")); + assertThat(ttsService.getSynthesized(), is("Interpreted text")); + assertTrue(sink.getIsStreamProcessed()); + + voiceManager.stopDialog(source); + + assertTrue(ksService.isAborted()); } @Test public void startDialogAndVerifyThatAKSExceptionIsProperlyHandled() { sttService = new STTServiceStub(); ksService = new KSServiceStub(); - ttsService = new TTSServiceStub(); hliStub = new HumanLanguageInterpreterStub(); - source = new AudioSourceStub(); registerService(sttService); registerService(ksService); registerService(ttsService); registerService(hliStub); - registerService(source); - ksService.setIsKsExceptionExpected(true); + ksService.setExceptionExpected(true); - voiceManager.startDialog(ksService, sttService, ttsService, hliStub, source, sink, null, "", null); + voiceManager.startDialog(ksService, sttService, ttsService, hliStub, source, sink, Locale.ENGLISH, "", null); assertFalse(ksService.isWordSpotted()); + assertFalse(sttService.isRecognized()); + assertThat(hliStub.getQuestion(), is("")); + assertThat(hliStub.getAnswer(), is("")); + assertThat(ttsService.getSynthesized(), is("")); + assertFalse(sink.getIsStreamProcessed()); + + voiceManager.stopDialog(source); + } + + @Test + public void startDialogAndVerifyThatAKSErrorIsProperlyHandled() { + sttService = new STTServiceStub(); + ksService = new KSServiceStub(); + hliStub = new HumanLanguageInterpreterStub(); + + registerService(sttService); + registerService(ksService); + registerService(ttsService); + registerService(hliStub); + + ksService.setErrorExpected(true); + + voiceManager.startDialog(ksService, sttService, ttsService, hliStub, source, sink, Locale.ENGLISH, "word", + null); + + assertFalse(ksService.isWordSpotted()); + assertFalse(sttService.isRecognized()); + assertThat(hliStub.getQuestion(), is("")); + assertThat(hliStub.getAnswer(), is("")); + assertThat(ttsService.getSynthesized(), + is("Encountered error while spotting keywords, keyword spotting error")); + assertTrue(sink.getIsStreamProcessed()); + + voiceManager.stopDialog(source); + } + + @Test + public void startDialogAndVerifyThatASTTExceptionIsProperlyHandled() { + sttService = new STTServiceStub(); + ksService = new KSServiceStub(); + hliStub = new HumanLanguageInterpreterStub(); + + registerService(sttService); + registerService(ksService); + registerService(ttsService); + registerService(hliStub); + + sttService.setExceptionExpected(true); + + voiceManager.startDialog(ksService, sttService, ttsService, hliStub, source, sink, Locale.ENGLISH, "word", + null); + + assertTrue(ksService.isWordSpotted()); + assertFalse(sttService.isRecognized()); + assertThat(hliStub.getQuestion(), is("")); + assertThat(hliStub.getAnswer(), is("")); + assertThat(ttsService.getSynthesized(), is("Error during recognition, STT exception")); + assertTrue(sink.getIsStreamProcessed()); + + voiceManager.stopDialog(source); + } + + @Test + public void startDialogAndVerifyThatASpeechRecognitionErrorIsProperlyHandled() { + sttService = new STTServiceStub(); + ksService = new KSServiceStub(); + hliStub = new HumanLanguageInterpreterStub(); + + registerService(sttService); + registerService(ksService); + registerService(ttsService); + registerService(hliStub); + + sttService.setErrorExpected(true); + + voiceManager.startDialog(ksService, sttService, ttsService, hliStub, source, sink, Locale.ENGLISH, "word", + null); + + assertTrue(ksService.isWordSpotted()); + assertFalse(sttService.isRecognized()); + assertThat(hliStub.getQuestion(), is("")); + assertThat(hliStub.getAnswer(), is("")); + assertThat(ttsService.getSynthesized(), is("Encountered error while recognizing text, STT error")); + assertTrue(sink.getIsStreamProcessed()); + + voiceManager.stopDialog(source); + } + + @Test + public void startDialogAndVerifyThatAnInterpretationExceptionIsProperlyHandled() { + sttService = new STTServiceStub(); + ksService = new KSServiceStub(); + hliStub = new HumanLanguageInterpreterStub(); + + registerService(sttService); + registerService(ksService); + registerService(ttsService); + registerService(hliStub); + + hliStub.setExceptionExpected(true); + + voiceManager.startDialog(ksService, sttService, ttsService, hliStub, source, sink, Locale.ENGLISH, "word", + null); + + assertTrue(ksService.isWordSpotted()); + assertTrue(sttService.isRecognized()); + assertThat(hliStub.getQuestion(), is("Recognized text")); + assertThat(hliStub.getAnswer(), is("")); + assertThat(ttsService.getSynthesized(), is("interpretation exception")); + assertTrue(sink.getIsStreamProcessed()); + + voiceManager.stopDialog(source); } @Test public void startDialogWithoutPassingAnyParameters() throws IOException, InterruptedException { sttService = new STTServiceStub(); ksService = new KSServiceStub(); - ttsService = new TTSServiceStub(); hliStub = new HumanLanguageInterpreterStub(); - source = new AudioSourceStub(); registerService(sttService); registerService(ksService); registerService(ttsService); registerService(hliStub); - registerService(source); Dictionary config = new Hashtable<>(); config.put(CONFIG_KEYWORD, "word"); @@ -236,16 +368,50 @@ public class VoiceManagerImplTest extends JavaOSGiTest { Configuration configuration = configAdmin.getConfiguration(VoiceManagerImpl.CONFIGURATION_PID); configuration.update(config); - waitForAssert(() -> { - try { - voiceManager.startDialog(); - } catch (Exception ex) { - // if the configuration is not updated yet the startDialog method will throw and exception which will - // break the test - } + // Wait some time to be sure that the configuration will be updated + try { + Thread.sleep(1000); + } catch (InterruptedException e) { + } - assertTrue(ksService.isWordSpotted()); - }); + voiceManager.startDialog(); + + assertTrue(ksService.isWordSpotted()); + assertTrue(sttService.isRecognized()); + assertThat(hliStub.getQuestion(), is("Recognized text")); + assertThat(hliStub.getAnswer(), is("Interpreted text")); + assertThat(ttsService.getSynthesized(), is("Interpreted text")); + assertTrue(sink.getIsStreamProcessed()); + + voiceManager.stopDialog(null); + + assertTrue(ksService.isAborted()); + } + + @Test + public void verifyThatOnlyOneDialogPerSourceIsPossible() { + sttService = new STTServiceStub(); + ksService = new KSServiceStub(); + hliStub = new HumanLanguageInterpreterStub(); + + registerService(sttService); + registerService(ksService); + registerService(ttsService); + registerService(hliStub); + + voiceManager.startDialog(ksService, sttService, ttsService, hliStub, source, sink, Locale.ENGLISH, "word", + null); + + assertTrue(ksService.isWordSpotted()); + + assertThrows(IllegalStateException.class, () -> voiceManager.startDialog(ksService, sttService, ttsService, + hliStub, source, sink, Locale.ENGLISH, "word", null)); + + voiceManager.stopDialog(source); + + assertTrue(ksService.isAborted()); + + assertThrows(IllegalStateException.class, () -> voiceManager.stopDialog(source)); } @Test @@ -352,7 +518,7 @@ public class VoiceManagerImplTest extends JavaOSGiTest { BundleContext context = bundleContext; ttsService = new TTSServiceStub(context); registerService(ttsService); - Locale locale = Locale.getDefault(); + Locale locale = localeProvider.getLocale(); boolean isVoiceStubInTheOptions = false; diff --git a/itests/org.openhab.core.voice.tests/src/main/java/org/openhab/core/voice/internal/VoiceStub.java b/itests/org.openhab.core.voice.tests/src/main/java/org/openhab/core/voice/internal/VoiceStub.java index 6076a21254..e26f354f15 100644 --- a/itests/org.openhab.core.voice.tests/src/main/java/org/openhab/core/voice/internal/VoiceStub.java +++ b/itests/org.openhab.core.voice.tests/src/main/java/org/openhab/core/voice/internal/VoiceStub.java @@ -43,7 +43,6 @@ public class VoiceStub implements Voice { @Override public Locale getLocale() { - // we need to return something different from null here (the real value is not important) - return Locale.getDefault(); + return Locale.ENGLISH; } } diff --git a/itests/org.openhab.core.voice.tests/src/main/java/org/openhab/core/voice/voiceconsolecommandextension/InterpretCommandTest.java b/itests/org.openhab.core.voice.tests/src/main/java/org/openhab/core/voice/voiceconsolecommandextension/InterpretCommandTest.java index 598baa55cb..574d72a759 100644 --- a/itests/org.openhab.core.voice.tests/src/main/java/org/openhab/core/voice/voiceconsolecommandextension/InterpretCommandTest.java +++ b/itests/org.openhab.core.voice.tests/src/main/java/org/openhab/core/voice/voiceconsolecommandextension/InterpretCommandTest.java @@ -41,7 +41,7 @@ public class InterpretCommandTest extends VoiceConsoleCommandExtensionTest { private static final String CONFIG_DEFAULT_VOICE = "defaultVoice"; private static final String SUBCMD_INTERPRET = "interpret"; private static final String INTERPRETED_TEXT = "Interpreted text"; - private static final String EXCEPTION_MESSAGE = "Exception message"; + private static final String EXCEPTION_MESSAGE = "interpretation exception"; private HumanLanguageInterpreterStub hliStub; private VoiceStub voice; @@ -79,7 +79,7 @@ public class InterpretCommandTest extends VoiceConsoleCommandExtensionTest { @Test public void verifyThatAnInterpretationExceptionIsHandledProperly() { waitForAssert(() -> { - hliStub.setIsInterpretationExceptionExpected(true); + hliStub.setExceptionExpected(true); String[] params = new String[] { SUBCMD_INTERPRET, "text", "to", "be", "interpreted" }; extensionService.execute(params, console); assertThat(console.getPrintedText(), is(EXCEPTION_MESSAGE)); diff --git a/itests/org.openhab.core.voice.tests/src/main/java/org/openhab/core/voice/voiceconsolecommandextension/VoicesCommandTest.java b/itests/org.openhab.core.voice.tests/src/main/java/org/openhab/core/voice/voiceconsolecommandextension/VoicesCommandTest.java index bd8a1467e6..4e5ed13448 100644 --- a/itests/org.openhab.core.voice.tests/src/main/java/org/openhab/core/voice/voiceconsolecommandextension/VoicesCommandTest.java +++ b/itests/org.openhab.core.voice.tests/src/main/java/org/openhab/core/voice/voiceconsolecommandextension/VoicesCommandTest.java @@ -14,15 +14,22 @@ package org.openhab.core.voice.voiceconsolecommandextension; import static org.hamcrest.CoreMatchers.is; import static org.hamcrest.MatcherAssert.assertThat; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import java.io.IOException; +import java.util.Dictionary; +import java.util.Hashtable; import java.util.Locale; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; +import org.openhab.core.i18n.LocaleProvider; import org.openhab.core.voice.internal.SinkStub; import org.openhab.core.voice.internal.TTSServiceStub; import org.openhab.core.voice.internal.VoiceStub; import org.osgi.framework.BundleContext; +import org.osgi.service.cm.Configuration; +import org.osgi.service.cm.ConfigurationAdmin; /** * A {@link VoiceConsoleCommandExtensionTest} which tests the execution of the command "voices". @@ -31,13 +38,25 @@ import org.osgi.framework.BundleContext; * @author Velin Yordanov - migrated tests from groovy to java */ public class VoicesCommandTest extends VoiceConsoleCommandExtensionTest { + private static final String CONFIG_LANGUAGE = "language"; private static final String SUBCMD_VOICES = "voices"; + private LocaleProvider localeProvider; private TTSServiceStub ttsService; private SinkStub sink; private VoiceStub voice; @BeforeEach - public void setUp() { + public void setUp() throws IOException { + localeProvider = getService(LocaleProvider.class); + assertNotNull(localeProvider); + + Dictionary localeConfig = new Hashtable<>(); + localeConfig.put(CONFIG_LANGUAGE, Locale.ENGLISH.getLanguage()); + ConfigurationAdmin configAdmin = super.getService(ConfigurationAdmin.class); + Configuration configuration = configAdmin.getFactoryConfiguration("org.openhab.i18n", null); + configuration.update(localeConfig); + configuration.update(); + BundleContext context = bundleContext; ttsService = new TTSServiceStub(context); @@ -52,7 +71,7 @@ public class VoicesCommandTest extends VoiceConsoleCommandExtensionTest { @Test public void testVoicesCommand() { String[] command = new String[] { SUBCMD_VOICES }; - Locale locale = Locale.getDefault(); + Locale locale = localeProvider.getLocale(); String expectedText = String.format("* %s - %s - %s (%s)", ttsService.getLabel(locale), voice.getLocale().getDisplayName(locale), voice.getLabel(), voice.getUID()); extensionService.execute(command, console);