From 6b914162bf746238e8c9187b2fce037bcdba4716 Mon Sep 17 00:00:00 2001 From: GiviMAD Date: Sun, 2 Jul 2023 02:27:10 -0700 Subject: [PATCH] [voice] Add console commands for register/unregister dialogs and list them (#3459) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * [voice] Add voice commands for register/unregister dialogs and list dialogs and dialog registrations Signed-off-by: Miguel Álvarez --- .../org/openhab/core/voice/VoiceManager.java | 5 + .../core/voice/internal/DialogProcessor.java | 7 + .../VoiceConsoleCommandExtension.java | 148 ++++++++++++++++-- .../core/voice/internal/VoiceManagerImpl.java | 5 + 4 files changed, 156 insertions(+), 9 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 86ba0b8c04e..d34aa774b13 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 @@ -135,6 +135,11 @@ public interface VoiceManager { @Nullable DialogContext getLastDialogContext(); + /** + * Returns a list with the contexts of all running dialogs. + */ + List getDialogsContexts(); + /** * Starts an infinite dialog sequence: keyword spotting on the audio source, audio source listening to retrieve * a question or a command (Speech to Text service), interpretation and handling of the command, and finally 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 2760edbd98e..f23b962140f 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 @@ -223,6 +223,13 @@ public void stop() { eventListener.onDialogStopped(dialogContext); } + /** + * Returns the dialog context used to start this processor. + */ + public DialogContext getContext() { + return dialogContext; + } + /** * Indicates if voice recognition is running. */ 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 c29b0091b57..c4eb1ff0941 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 @@ -20,6 +20,7 @@ import java.util.List; import java.util.Locale; import java.util.Objects; +import java.util.stream.Collectors; import org.eclipse.jdt.annotation.NonNullByDefault; import org.eclipse.jdt.annotation.Nullable; @@ -33,6 +34,7 @@ import org.openhab.core.items.ItemNotUniqueException; import org.openhab.core.items.ItemRegistry; import org.openhab.core.voice.DialogContext; +import org.openhab.core.voice.DialogRegistration; import org.openhab.core.voice.KSService; import org.openhab.core.voice.STTService; import org.openhab.core.voice.TTSService; @@ -60,7 +62,11 @@ public class VoiceConsoleCommandExtension extends AbstractConsoleCommandExtensio private static final String SUBCMD_VOICES = "voices"; private static final String SUBCMD_START_DIALOG = "startdialog"; private static final String SUBCMD_STOP_DIALOG = "stopdialog"; + private static final String SUBCMD_REGISTER_DIALOG = "registerdialog"; + private static final String SUBCMD_UNREGISTER_DIALOG = "unregisterdialog"; private static final String SUBCMD_LISTEN_ANSWER = "listenandanswer"; + private static final String SUBCMD_DIALOGS = "dialogs"; + private static final String SUBCMD_DIALOG_REGS = "dialogregs"; private static final String SUBCMD_INTERPRETERS = "interpreters"; private static final String SUBCMD_KEYWORD_SPOTTERS = "keywordspotters"; private static final String SUBCMD_STT_SERVICES = "sttservices"; @@ -87,13 +93,21 @@ 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_DIALOGS, "lists the running dialog and their audio/voice services"), + buildCommandUsage(SUBCMD_DIALOG_REGS, + "lists the existing dialog registrations and their selected audio/voice services"), + buildCommandUsage(SUBCMD_REGISTER_DIALOG + + " [--source ] [--sink ] [--hlis ] [--tts [--voice ]] [--stt ] [--ks ks [--keyword ]] [--listening-item ]", + "register a new dialog processing using the default services or the services identified with provided arguments, it will be persisted and keep running whenever is possible."), + buildCommandUsage(SUBCMD_UNREGISTER_DIALOG + " [source]", + "unregister the dialog processing for the default audio source or the audio source identified with provided argument, stopping it if started"), buildCommandUsage(SUBCMD_START_DIALOG - + " [--source ] [--sink ] [--interpreters ] [--tts [--voice ]] [--stt ] [--ks ks [--keyword ]] [--listening-item ]", + + " [--source ] [--sink ] [--hlis ] [--tts [--voice ]] [--stt ] [--ks ks [--keyword ]] [--listening-item ]", "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"), buildCommandUsage(SUBCMD_LISTEN_ANSWER - + " [--source ] [--sink ] [--interpreters ] [--tts [--voice ]] [--stt ] [--listening-item ]", + + " [--source ] [--sink ] [--hlis ] [--tts [--voice ]] [--stt ] [--listening-item ]", "Execute a simple dialog sequence without keyword spotting using the default services or the services identified with provided arguments"), buildCommandUsage(SUBCMD_INTERPRETERS, "lists the interpreters"), buildCommandUsage(SUBCMD_KEYWORD_SPOTTERS, "lists the keyword spotters"), @@ -135,10 +149,41 @@ public void execute(String[] args, Console console) { } return; } + case SUBCMD_REGISTER_DIALOG -> { + DialogRegistration dialogRegistration; + try { + dialogRegistration = parseDialogRegistration(args); + } catch (IllegalStateException e) { + console.println(Objects.requireNonNullElse(e.getMessage(), + "An error occurred while parsing the dialog options")); + break; + } + try { + voiceManager.registerDialog(dialogRegistration); + } catch (IllegalStateException e) { + console.println(Objects.requireNonNullElse(e.getMessage(), + "An error occurred while registering the dialog")); + } + return; + } + case SUBCMD_UNREGISTER_DIALOG -> { + try { + var sourceId = args.length < 2 ? audioManager.getSourceId() : args[1]; + if (sourceId == null) { + console.println("No source provided nor default source available"); + break; + } + voiceManager.unregisterDialog(sourceId); + } catch (IllegalStateException e) { + console.println(Objects.requireNonNullElse(e.getMessage(), + "An error occurred while stopping the dialog")); + } + return; + } case SUBCMD_START_DIALOG -> { DialogContext.Builder dialogContextBuilder; try { - dialogContextBuilder = parseDialogParameters(args); + dialogContextBuilder = parseDialogContext(args); } catch (IllegalStateException e) { console.println(Objects.requireNonNullElse(e.getMessage(), "An error occurred while parsing the dialog options")); @@ -164,7 +209,7 @@ public void execute(String[] args, Console console) { case SUBCMD_LISTEN_ANSWER -> { DialogContext.Builder dialogContextBuilder; try { - dialogContextBuilder = parseDialogParameters(args); + dialogContextBuilder = parseDialogContext(args); } catch (IllegalStateException e) { console.println(Objects.requireNonNullElse(e.getMessage(), "An error occurred while parsing the dialog options")); @@ -178,6 +223,14 @@ public void execute(String[] args, Console console) { } return; } + case SUBCMD_DIALOGS -> { + listDialogs(console); + return; + } + case SUBCMD_DIALOG_REGS -> { + listDialogRegistrations(console); + return; + } case SUBCMD_INTERPRETERS -> { listInterpreters(console); return; @@ -252,6 +305,42 @@ private void say(String[] args, Console console) { voiceManager.say(msg.toString()); } + private void listDialogRegistrations(Console console) { + Collection registrations = voiceManager.getDialogRegistrations(); + if (!registrations.isEmpty()) { + registrations.stream().sorted(comparing(dr -> dr.sourceId)).forEach(dr -> { + console.println( + String.format(" Source: %s - Sink: %s (STT: %s, TTS: %s, HLIs: %s, KS: %s, Keyword: %s)", + dr.sourceId, dr.sinkId, getOrDefault(dr.sttId), getOrDefault(dr.ttsId), + dr.hliIds.isEmpty() ? getOrDefault(null) : String.join("->", dr.hliIds), + getOrDefault(dr.ksId), getOrDefault(dr.keyword))); + }); + } else { + console.println("No dialog registrations."); + } + } + + private String getOrDefault(@Nullable String value) { + return value != null && !value.isBlank() ? value : "**Default**"; + } + + private void listDialogs(Console console) { + Collection dialogContexts = voiceManager.getDialogsContexts(); + if (!dialogContexts.isEmpty()) { + dialogContexts.stream().sorted(comparing(s -> s.source().getId())).forEach(c -> { + var ks = c.ks(); + String ksText = ks != null ? String.format(", KS: %s, Keyword: %s", ks.getId(), c.keyword()) : ""; + console.println( + String.format(" Source: %s - Sink: %s (STT: %s, TTS: %s, HLIs: %s%s)", c.source().getId(), + c.sink().getId(), c.stt().getId(), c.tts().getId(), c.hlis().stream() + .map(HumanLanguageInterpreter::getId).collect(Collectors.joining("->")), + ksText)); + }); + } else { + console.println("No running dialogs."); + } + } + private void listInterpreters(Console console) { Collection interpreters = voiceManager.getHLIs(); if (!interpreters.isEmpty()) { @@ -314,11 +403,7 @@ private void listTTSs(Console console) { .orElse(null); } - private DialogContext.Builder parseDialogParameters(String[] args) { - var dialogContextBuilder = voiceManager.getDialogContextBuilder(); - if (args.length < 2) { - return dialogContextBuilder; - } + private HashMap parseDialogParameters(String[] args) { var parameters = new HashMap(); for (int i = 1; i < args.length; i++) { var arg = args[i].trim(); @@ -333,6 +418,15 @@ private DialogContext.Builder parseDialogParameters(String[] args) { throw new IllegalStateException("Argument name should start by -- " + arg); } } + return parameters; + } + + private DialogContext.Builder parseDialogContext(String[] args) { + var dialogContextBuilder = voiceManager.getDialogContextBuilder(); + if (args.length < 2) { + return dialogContextBuilder; + } + var parameters = parseDialogParameters(args); String sourceId = parameters.remove("source"); if (sourceId != null) { var source = audioManager.getSource(sourceId); @@ -363,4 +457,40 @@ private DialogContext.Builder parseDialogParameters(String[] args) { } return dialogContextBuilder; } + + private DialogRegistration parseDialogRegistration(String[] args) { + var parameters = parseDialogParameters(args); + @Nullable + String sourceId = parameters.remove("source"); + if (sourceId == null) { + sourceId = audioManager.getSourceId(); + } + if (sourceId == null) { + throw new IllegalStateException("A source is required if the default is not configured"); + } + @Nullable + String sinkId = parameters.remove("sink"); + if (sinkId == null) { + sinkId = audioManager.getSinkId(); + } + if (sinkId == null) { + throw new IllegalStateException("A sink is required if the default is not configured"); + } + var dr = new DialogRegistration(sourceId, sinkId); + dr.ksId = parameters.remove("ks"); + dr.keyword = parameters.remove("keyword"); + dr.sttId = parameters.remove("stt"); + dr.ttsId = parameters.remove("tts"); + dr.voiceId = parameters.remove("voice"); + dr.listeningItem = parameters.remove("listening-item"); + String hliIds = parameters.remove("hlis"); + if (hliIds != null) { + dr.hliIds = Arrays.stream(hliIds.split(",")).map(String::trim).collect(Collectors.toList()); + } + if (!parameters.isEmpty()) { + throw new IllegalStateException( + "Argument " + parameters.keySet().stream().findAny().orElse("") + " is not supported"); + } + return dr; + } } 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 de770481a69..c70a2d84c22 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 @@ -495,6 +495,11 @@ public DialogContext.Builder getDialogContextBuilder() { .withListeningItem(listeningItem); } + @Override + public List getDialogsContexts() { + return dialogProcessors.values().stream().map(DialogProcessor::getContext).collect(Collectors.toList()); + } + @Override public @Nullable DialogContext getLastDialogContext() { return lastDialogContext;