From efa8963d20ba466defaf451d79ab8b0674455602 Mon Sep 17 00:00:00 2001 From: GiviMAD Date: Sat, 22 Jan 2022 14:02:02 +0100 Subject: [PATCH] [porcupineks] Keyword Spotter, initial contribution (#12028) MIME-Version: 1.0 Content-Type: text/plain; charset=utf8 Content-Transfer-Encoding: 8bit * initial contribution Signed-off-by: Miguel Álvarez Díez --- CODEOWNERS | 1 + bom/openhab-addons/pom.xml | 5 + bundles/org.openhab.voice.porcupineks/NOTICE | 13 + .../org.openhab.voice.porcupineks/README.md | 58 +++ bundles/org.openhab.voice.porcupineks/pom.xml | 24 ++ .../src/main/feature/feature.xml | 9 + .../internal/PorcupineKSConfiguration.java | 33 ++ .../internal/PorcupineKSConstants.java | 44 +++ .../internal/PorcupineKSService.java | 329 ++++++++++++++++++ .../main/resources/OH-INF/config/config.xml | 19 + .../OH-INF/i18n/porcupineks.properties | 8 + bundles/pom.xml | 1 + 12 files changed, 544 insertions(+) create mode 100644 bundles/org.openhab.voice.porcupineks/NOTICE create mode 100644 bundles/org.openhab.voice.porcupineks/README.md create mode 100644 bundles/org.openhab.voice.porcupineks/pom.xml create mode 100644 bundles/org.openhab.voice.porcupineks/src/main/feature/feature.xml create mode 100644 bundles/org.openhab.voice.porcupineks/src/main/java/org/openhab/voice/porcupineks/internal/PorcupineKSConfiguration.java create mode 100644 bundles/org.openhab.voice.porcupineks/src/main/java/org/openhab/voice/porcupineks/internal/PorcupineKSConstants.java create mode 100644 bundles/org.openhab.voice.porcupineks/src/main/java/org/openhab/voice/porcupineks/internal/PorcupineKSService.java create mode 100644 bundles/org.openhab.voice.porcupineks/src/main/resources/OH-INF/config/config.xml create mode 100644 bundles/org.openhab.voice.porcupineks/src/main/resources/OH-INF/i18n/porcupineks.properties diff --git a/CODEOWNERS b/CODEOWNERS index 8c3cccb2c4..214bcd971a 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -379,6 +379,7 @@ /bundles/org.openhab.voice.marytts/ @kaikreuzer /bundles/org.openhab.voice.picotts/ @FlorianSW /bundles/org.openhab.voice.pollytts/ @hillmanr +/bundles/org.openhab.voice.porcupineks/ @GiviMAD /bundles/org.openhab.voice.voicerss/ @JochenHiller /itests/org.openhab.binding.astro.tests/ @gerrieg /itests/org.openhab.binding.avmfritz.tests/ @cweitkamp diff --git a/bom/openhab-addons/pom.xml b/bom/openhab-addons/pom.xml index f71abb96ae..56ebd6d9be 100644 --- a/bom/openhab-addons/pom.xml +++ b/bom/openhab-addons/pom.xml @@ -1886,6 +1886,11 @@ org.openhab.voice.pollytts ${project.version} + + org.openhab.addons.bundles + org.openhab.voice.porcupineks + ${project.version} + org.openhab.addons.bundles org.openhab.voice.voicerss diff --git a/bundles/org.openhab.voice.porcupineks/NOTICE b/bundles/org.openhab.voice.porcupineks/NOTICE new file mode 100644 index 0000000000..38d625e349 --- /dev/null +++ b/bundles/org.openhab.voice.porcupineks/NOTICE @@ -0,0 +1,13 @@ +This content is produced and maintained by the openHAB project. + +* Project home: https://www.openhab.org + +== Declared Project Licenses + +This program and the accompanying materials are made available under the terms +of the Eclipse Public License 2.0 which is available at +https://www.eclipse.org/legal/epl-2.0/. + +== Source Code + +https://github.com/openhab/openhab-addons diff --git a/bundles/org.openhab.voice.porcupineks/README.md b/bundles/org.openhab.voice.porcupineks/README.md new file mode 100644 index 0000000000..41f129beda --- /dev/null +++ b/bundles/org.openhab.voice.porcupineks/README.md @@ -0,0 +1,58 @@ +# Porcupine Keyword Spotter + +This voice service allows you to use the PicoVoice product Porcupine as your keyword spotter in openHAB. + +Porcupine provides on-device wake word detection powered by deep learning. +This add-on should work on all the platforms supported by Porcupine, if you encounter a problem you can try to run one of the Porcupine java demos on your machine. + +Important: No voice data listened by this service will be uploaded to the Cloud. +The voice data is processed offline, locally on your openHAB server by Porcupine. +Once in a while, access keys are validated to stay active and this requires an Internet connection. + +## How to use it + +After installing, you will be able to access the addon options through the openHAB configuration page under the 'Other Services' section. +There you will need to provide your PicoVoice Api Key. + +After that, you can select Porcupine as your default Keyword Spotter in your 'Voice' settings. + +## Magic Word Configuration + +The magic word to spot is gathered from your 'Voice' configuration. +The default english keyword models are loaded in the addon (also the english language model) so you can use those without adding anything else. + +Note that you can use the pico voice platform to generate your own keyword models. +To use them, you should place the generated file under '\/porcupine' and configure your magic word to match the file name replacing spaces with '_' and adding the extension '.ppn'. +As an example, the file generated for the keyword "ok openhab" will be named 'ok_openhab.ppn'. + +The service will only work if it's able to find the correct ppn for your magic word configuration. + +#### Build-in keywords + +Remember that they only work with the English language model. (read bellow section) + +* alexa +* americano +* blueberry +* bumblebee +* computer +* grapefruits +* grasshopper +* hey google +* hey siri +* jarvis +* ok google +* picovoice +* porcupine +* terminator + + +## Language support + +This service currently supports English, German, French and Spanish. + +Only the English model binary is included with the addon and will be used if the one for your configured language is not found under '\/porcupine'. + +To get the language model files, go to the [Porcupine repo](https://github.com/Picovoice/porcupine/tree/v2.0/lib/common). + +Note that the keyword model you use should match the language model. diff --git a/bundles/org.openhab.voice.porcupineks/pom.xml b/bundles/org.openhab.voice.porcupineks/pom.xml new file mode 100644 index 0000000000..a28c9c6759 --- /dev/null +++ b/bundles/org.openhab.voice.porcupineks/pom.xml @@ -0,0 +1,24 @@ + + + + 4.0.0 + + + org.openhab.addons.bundles + org.openhab.addons.reactor.bundles + 3.3.0-SNAPSHOT + + + org.openhab.voice.porcupineks + + openHAB Add-ons :: Bundles :: Voice :: Porcupine Keyword Spotter + + + ai.picovoice + porcupine-java + 2.0.2 + compile + + + diff --git a/bundles/org.openhab.voice.porcupineks/src/main/feature/feature.xml b/bundles/org.openhab.voice.porcupineks/src/main/feature/feature.xml new file mode 100644 index 0000000000..4f1689ea87 --- /dev/null +++ b/bundles/org.openhab.voice.porcupineks/src/main/feature/feature.xml @@ -0,0 +1,9 @@ + + + mvn:org.openhab.core.features.karaf/org.openhab.core.features.karaf.openhab-core/${ohc.version}/xml/features + + + openhab-runtime-base + mvn:org.openhab.addons.bundles/org.openhab.voice.porcupineks/${project.version} + + diff --git a/bundles/org.openhab.voice.porcupineks/src/main/java/org/openhab/voice/porcupineks/internal/PorcupineKSConfiguration.java b/bundles/org.openhab.voice.porcupineks/src/main/java/org/openhab/voice/porcupineks/internal/PorcupineKSConfiguration.java new file mode 100644 index 0000000000..3b8e8ebc66 --- /dev/null +++ b/bundles/org.openhab.voice.porcupineks/src/main/java/org/openhab/voice/porcupineks/internal/PorcupineKSConfiguration.java @@ -0,0 +1,33 @@ +/** + * Copyright (c) 2010-2022 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.voice.porcupineks.internal; + +import org.eclipse.jdt.annotation.NonNullByDefault; + +/** + * The {@link PorcupineKSConfiguration} class contains fields mapping thing configuration parameters. + * + * @author Miguel Álvarez - Initial contribution + */ +@NonNullByDefault +public class PorcupineKSConfiguration { + + /** + * Api key to use porcupine + */ + public String apiKey = ""; + /** + * A higher sensitivity reduces miss rate at cost of increased false alarm rate + */ + public float sensitivity = 0.5f; +} diff --git a/bundles/org.openhab.voice.porcupineks/src/main/java/org/openhab/voice/porcupineks/internal/PorcupineKSConstants.java b/bundles/org.openhab.voice.porcupineks/src/main/java/org/openhab/voice/porcupineks/internal/PorcupineKSConstants.java new file mode 100644 index 0000000000..154d73fbd4 --- /dev/null +++ b/bundles/org.openhab.voice.porcupineks/src/main/java/org/openhab/voice/porcupineks/internal/PorcupineKSConstants.java @@ -0,0 +1,44 @@ +/** + * Copyright (c) 2010-2022 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.voice.porcupineks.internal; + +import org.eclipse.jdt.annotation.NonNullByDefault; + +/** + * The {@link PorcupineKSConstants} class defines common constants, which are + * used across the whole binding. + * + * @author Miguel Álvarez - Initial contribution + */ +@NonNullByDefault +public class PorcupineKSConstants { + /** + * Service name + */ + public static final String SERVICE_NAME = "Porcupine Keyword Spotter"; + + /** + * Service id + */ + public static final String SERVICE_ID = "porcupineks"; + + /** + * Service category + */ + public static final String SERVICE_CATEGORY = "voice"; + + /** + * Service pid + */ + public static final String SERVICE_PID = "org.openhab." + SERVICE_CATEGORY + "." + SERVICE_ID; +} diff --git a/bundles/org.openhab.voice.porcupineks/src/main/java/org/openhab/voice/porcupineks/internal/PorcupineKSService.java b/bundles/org.openhab.voice.porcupineks/src/main/java/org/openhab/voice/porcupineks/internal/PorcupineKSService.java new file mode 100644 index 0000000000..41f44f5c10 --- /dev/null +++ b/bundles/org.openhab.voice.porcupineks/src/main/java/org/openhab/voice/porcupineks/internal/PorcupineKSService.java @@ -0,0 +1,329 @@ +/** + * Copyright (c) 2010-2022 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.voice.porcupineks.internal; + +import static org.openhab.voice.porcupineks.internal.PorcupineKSConstants.SERVICE_CATEGORY; +import static org.openhab.voice.porcupineks.internal.PorcupineKSConstants.SERVICE_ID; +import static org.openhab.voice.porcupineks.internal.PorcupineKSConstants.SERVICE_NAME; +import static org.openhab.voice.porcupineks.internal.PorcupineKSConstants.SERVICE_PID; + +import java.io.BufferedInputStream; +import java.io.BufferedOutputStream; +import java.io.File; +import java.io.FileOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.net.URL; +import java.nio.ByteBuffer; +import java.nio.ByteOrder; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.Locale; +import java.util.Map; +import java.util.Set; +import java.util.concurrent.Future; +import java.util.concurrent.ScheduledExecutorService; +import java.util.logging.Level; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.eclipse.jdt.annotation.Nullable; +import org.openhab.core.OpenHAB; +import org.openhab.core.audio.AudioFormat; +import org.openhab.core.audio.AudioStream; +import org.openhab.core.common.ThreadPoolManager; +import org.openhab.core.config.core.ConfigurableService; +import org.openhab.core.config.core.Configuration; +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; +import org.osgi.framework.BundleContext; +import org.osgi.framework.Constants; +import org.osgi.service.component.ComponentContext; +import org.osgi.service.component.annotations.Activate; +import org.osgi.service.component.annotations.Component; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import ai.picovoice.porcupine.Porcupine; +import ai.picovoice.porcupine.PorcupineException; + +/** + * The {@link PorcupineKSService} is a keyword spotting implementation based on porcupine. + * + * @author Miguel Álvarez - Initial contribution + */ +@NonNullByDefault +@Component(configurationPid = SERVICE_PID, property = Constants.SERVICE_PID + "=" + SERVICE_PID) +@ConfigurableService(category = SERVICE_CATEGORY, label = SERVICE_NAME, description_uri = SERVICE_CATEGORY + ":" + + SERVICE_ID) +public class PorcupineKSService implements KSService { + private static final String PORCUPINE_FOLDER = Path.of(OpenHAB.getUserDataFolder(), "porcupine").toString(); + private static final String EXTRACTION_FOLDER = Path.of(OpenHAB.getUserDataFolder(), "porcupine", "extracted") + .toString(); + private final Logger logger = LoggerFactory.getLogger(PorcupineKSService.class); + private final ScheduledExecutorService executor = ThreadPoolManager.getScheduledPool("OH-voice-porcupineks"); + private PorcupineKSConfiguration config = new PorcupineKSConfiguration(); + private boolean loop = false; + private @Nullable BundleContext bundleContext; + + static { + Logger logger = LoggerFactory.getLogger(PorcupineKSService.class); + File directory = new File(PORCUPINE_FOLDER); + if (!directory.exists()) { + if (directory.mkdir()) { + logger.info("porcupine dir created {}", PORCUPINE_FOLDER); + } + } + File childDirectory = new File(EXTRACTION_FOLDER); + if (!childDirectory.exists()) { + if (childDirectory.mkdir()) { + logger.info("porcupine extraction file dir created {}", EXTRACTION_FOLDER); + } + } + } + + @Activate + protected void activate(ComponentContext componentContext, Map config) { + this.config = new Configuration(config).as(PorcupineKSConfiguration.class); + this.bundleContext = componentContext.getBundleContext(); + if (this.config.apiKey.isBlank()) { + logger.warn("Missing pico voice api key to use Porcupine Keyword Spotter"); + } + } + + private String prepareLib(BundleContext bundleContext, String path) throws IOException { + if (!path.contains("porcupine" + File.separator)) { + // this should never happen + throw new IOException("Path is not pointing to porcupine bundle files " + path); + } + // get a path relative to the porcupine bundle folder + String relativePath; + if (path.startsWith("porcupine" + File.separator)) { + relativePath = path; + } else { + relativePath = path.substring(path.lastIndexOf(File.separator + "porcupine" + File.separator) + 1); + } + File localFile = new File(EXTRACTION_FOLDER, + relativePath.substring(relativePath.lastIndexOf(File.separator) + 1)); + if (!localFile.exists()) { + URL porcupineResource = bundleContext.getBundle().getEntry(relativePath); + logger.debug("extracting binary {} from bundle to extraction folder", relativePath); + extractFromBundle(porcupineResource, localFile); + } else { + logger.debug("binary {} already extracted", relativePath); + } + return localFile.toString(); + } + + private void extractFromBundle(URL resourceUrl, File targetFile) throws IOException { + InputStream in = new BufferedInputStream(resourceUrl.openStream()); + OutputStream out = new BufferedOutputStream(new FileOutputStream(targetFile)); + byte[] buffer = new byte[1024]; + int lengthRead; + while ((lengthRead = in.read(buffer)) > 0) { + out.write(buffer, 0, lengthRead); + out.flush(); + } + in.close(); + out.close(); + } + + @Override + public String getId() { + return SERVICE_ID; + } + + @Override + public String getLabel(@Nullable Locale locale) { + return SERVICE_NAME; + } + + @Override + public Set getSupportedLocales() { + return Set.of(Locale.ENGLISH, new Locale("es"), Locale.FRENCH, Locale.GERMAN); + } + + @Override + public Set getSupportedFormats() { + return Set + .of(new AudioFormat(AudioFormat.CONTAINER_WAVE, AudioFormat.CODEC_PCM_SIGNED, false, 16, null, 16000L)); + } + + @Override + public KSServiceHandle spot(KSListener ksListener, AudioStream audioStream, Locale locale, String keyword) + throws KSException { + Porcupine porcupine; + if (config.apiKey.isBlank()) { + throw new KSException("Missing pico voice api key"); + } + BundleContext bundleContext = this.bundleContext; + if (bundleContext == null) { + throw new KSException("Missing bundle context"); + } + try { + porcupine = initPorcupine(bundleContext, locale, keyword); + } catch (PorcupineException | IOException e) { + throw new KSException(e); + } + Future scheduledTask = executor.submit(() -> processInBackground(porcupine, ksListener, audioStream)); + return new KSServiceHandle() { + @Override + public void abort() { + logger.debug("stopping service"); + loop = false; + try { + Thread.sleep(100); + } catch (InterruptedException e) { + } + scheduledTask.cancel(true); + } + }; + } + + private Porcupine initPorcupine(BundleContext bundleContext, Locale locale, String keyword) + throws IOException, PorcupineException { + // Suppress library logs + java.util.logging.Logger globalJavaLogger = java.util.logging.Logger + .getLogger(java.util.logging.Logger.GLOBAL_LOGGER_NAME); + Level currentGlobalLogLevel = globalJavaLogger.getLevel(); + globalJavaLogger.setLevel(java.util.logging.Level.OFF); + String bundleLibraryPath = Porcupine.LIBRARY_PATH; + if (bundleLibraryPath == null) { + throw new PorcupineException("Unsupported environment, ensure Porcupine is supported by your system"); + } + String libraryPath = prepareLib(bundleContext, bundleLibraryPath); + String alternativeModelPath = getAlternativeModelPath(bundleContext, locale); + String modelPath = alternativeModelPath != null ? alternativeModelPath + : prepareLib(bundleContext, Porcupine.MODEL_PATH); + String keywordPath = getKeywordResourcePath(bundleContext, keyword, alternativeModelPath == null); + logger.debug("Porcupine library path: {}", libraryPath); + logger.debug("Porcupine model path: {}", modelPath); + logger.debug("Porcupine keyword path: {}", keywordPath); + logger.debug("Porcupine sensitivity: {}", config.sensitivity); + try { + return new Porcupine(config.apiKey, libraryPath, modelPath, new String[] { keywordPath }, + new float[] { config.sensitivity }); + } finally { + // restore log level + globalJavaLogger.setLevel(currentGlobalLogLevel); + } + } + + private String getPorcupineEnv() { + // get porcupine env from resolved library path + String searchTerm = "lib" + File.separator + "java" + File.separator; + String env = Porcupine.LIBRARY_PATH.substring(Porcupine.LIBRARY_PATH.indexOf(searchTerm) + searchTerm.length()); + env = env.substring(0, env.indexOf(File.separator)); + return env; + } + + private @Nullable String getAlternativeModelPath(BundleContext bundleContext, Locale locale) throws IOException { + String modelPath = null; + if (locale.getLanguage().equals(Locale.GERMAN.getLanguage())) { + Path dePath = Path.of(PORCUPINE_FOLDER, "porcupine_params_de.pv"); + if (Files.exists(dePath)) { + modelPath = dePath.toString(); + } else { + logger.warn( + "You can provide a specific model for de language in {}, english language model will be used", + PORCUPINE_FOLDER); + } + } else if (locale.getLanguage().equals(Locale.FRENCH.getLanguage())) { + Path frPath = Path.of(PORCUPINE_FOLDER, "porcupine_params_fr.pv"); + if (Files.exists(frPath)) { + modelPath = frPath.toString(); + } else { + logger.warn( + "You can provide a specific model for fr language in {}, english language model will be used", + PORCUPINE_FOLDER); + } + } else if (locale.getLanguage().equals("es")) { + Path esPath = Path.of(PORCUPINE_FOLDER, "porcupine_params_es.pv"); + if (Files.exists(esPath)) { + modelPath = esPath.toString(); + } else { + logger.warn( + "You can provide a specific model for es language in {}, english language model will be used", + PORCUPINE_FOLDER); + } + } + return modelPath; + } + + private String getKeywordResourcePath(BundleContext bundleContext, String keyWord, boolean allowBuildIn) + throws IOException { + String localKeywordFile = keyWord.toLowerCase().replace(" ", "_") + ".ppn"; + Path localKeywordPath = Path.of(PORCUPINE_FOLDER, localKeywordFile); + if (Files.exists(localKeywordPath)) { + return localKeywordPath.toString(); + } + if (allowBuildIn) { + try { + Porcupine.BuiltInKeyword.valueOf(keyWord.toUpperCase().replace(" ", "_")); + } catch (IllegalArgumentException e) { + throw new IllegalArgumentException( + "Unable to find model file for configured wake word neither is build-in. Should be at " + + localKeywordPath); + } + String env = getPorcupineEnv(); + String keywordPath = "porcupine/resources/keyword_files/" + env + "/" + keyWord.replace(" ", "_") + "_" + + env + ".ppn"; + return prepareLib(bundleContext, keywordPath); + } else { + throw new IllegalArgumentException( + "Unable to find model file for configured wake word; there are no build-in wake words for your language. Should be at " + + localKeywordPath); + } + } + + private void processInBackground(Porcupine porcupine, KSListener ksListener, AudioStream audioStream) { + int numBytesRead; + // buffers for processing audio + int frameLength = porcupine.getFrameLength(); + ByteBuffer captureBuffer = ByteBuffer.allocate(frameLength * 2); + captureBuffer.order(ByteOrder.LITTLE_ENDIAN); + short[] porcupineBuffer = new short[frameLength]; + this.loop = true; + while (loop) { + try { + // read a buffer of audio + numBytesRead = audioStream.read(captureBuffer.array(), 0, captureBuffer.capacity()); + if (!loop) { + break; + } + // don't pass to porcupine if we don't have a full buffer + if (numBytesRead != frameLength * 2) { + Thread.sleep(100); + continue; + } + // copy into 16-bit buffer + captureBuffer.asShortBuffer().get(porcupineBuffer); + // process with porcupine + int result = porcupine.process(porcupineBuffer); + if (result >= 0) { + logger.debug("keyword detected!"); + ksListener.ksEventReceived(new KSpottedEvent()); + } + } catch (IOException | PorcupineException | InterruptedException e) { + String errorMessage = e.getMessage(); + ksListener.ksEventReceived(new KSErrorEvent(errorMessage != null ? errorMessage : "Unexpected error")); + } + } + porcupine.delete(); + logger.debug("Porcupine stopped"); + } +} diff --git a/bundles/org.openhab.voice.porcupineks/src/main/resources/OH-INF/config/config.xml b/bundles/org.openhab.voice.porcupineks/src/main/resources/OH-INF/config/config.xml new file mode 100644 index 0000000000..92406c7f2f --- /dev/null +++ b/bundles/org.openhab.voice.porcupineks/src/main/resources/OH-INF/config/config.xml @@ -0,0 +1,19 @@ + + + + + + API key from PicoVoice, required to use Porcupine. + + + + Spot sensitivity, a higher sensitivity reduces miss rate at cost of increased false alarm rate. + 0.5 + + + + diff --git a/bundles/org.openhab.voice.porcupineks/src/main/resources/OH-INF/i18n/porcupineks.properties b/bundles/org.openhab.voice.porcupineks/src/main/resources/OH-INF/i18n/porcupineks.properties new file mode 100644 index 0000000000..9dc21a6123 --- /dev/null +++ b/bundles/org.openhab.voice.porcupineks/src/main/resources/OH-INF/i18n/porcupineks.properties @@ -0,0 +1,8 @@ +voice.config.porcupineks.apiKey.label = Pico Voice API Key +voice.config.porcupineks.apiKey.description = API key from PicoVoice, required to use Porcupine. +voice.config.porcupineks.sensitivity.label = Sensitivity +voice.config.porcupineks.sensitivity.description = Spot sensitivity, a higher sensitivity reduces miss rate at cost of increased false alarm rate. + +# service + +service.voice.porcupineks.label = Porcupine Keyword Spotter diff --git a/bundles/pom.xml b/bundles/pom.xml index dd18d0850e..4da92f0599 100644 --- a/bundles/pom.xml +++ b/bundles/pom.xml @@ -397,6 +397,7 @@ org.openhab.voice.marytts org.openhab.voice.picotts org.openhab.voice.pollytts + org.openhab.voice.porcupineks org.openhab.voice.voicerss -- 2.47.3