[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 <lg.hc@free.fr>pull/2721/head
parent
6edc413640
commit
8f0dd94343
|
@ -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<Voice> 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.
|
||||
|
|
|
@ -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());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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<String> getUsages() {
|
||||
return List.of(buildCommandUsage(SUBCMD_SAY + " <text>", "speaks a text"),
|
||||
buildCommandUsage(SUBCMD_INTERPRET + " <command>", "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 + " [<source> <interpreter> <sink> <keyword>]",
|
||||
"start a new dialog processing using the default services or the services identified with provided arguments"),
|
||||
buildCommandUsage(SUBCMD_STOP_DIALOG + " [<source>]",
|
||||
"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<AudioSource> sources = audioManager.getAllSources();
|
||||
for (AudioSource source : sources) {
|
||||
if (source.getId().equals(sourceId)) {
|
||||
return source;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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<String, String> defaultVoices = new HashMap<>();
|
||||
|
||||
private Map<String, DialogProcessor> 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<String, Object> config) {
|
||||
protected void activate(BundleContext bundleContext, Map<String, Object> config) {
|
||||
this.bundle = bundleContext.getBundle();
|
||||
modified(config);
|
||||
}
|
||||
|
||||
@Deactivate
|
||||
protected void deactivate() {
|
||||
stopAllDialogs();
|
||||
}
|
||||
|
||||
@SuppressWarnings("null")
|
||||
@Modified
|
||||
protected void modified(Map<String, Object> 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);
|
||||
|
|
|
@ -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}
|
||||
|
|
|
@ -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();
|
||||
|
|
|
@ -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();
|
||||
|
|
|
@ -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<String> 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
|
||||
|
|
|
@ -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();
|
||||
|
|
|
@ -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<String, Object> 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<String, Object> 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<String, Object> 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;
|
||||
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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));
|
||||
|
|
|
@ -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<String, Object> 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);
|
||||
|
|
Loading…
Reference in New Issue