[porcupineks] Remove from add-on repo (#16063)
Signed-off-by: Miguel Álvarez <miguelwork92@gmail.com>
This commit is contained in:
parent
9ebb203d58
commit
6d2b8bc92f
|
@ -426,7 +426,6 @@
|
||||||
/bundles/org.openhab.voice.mimictts/ @dalgwen
|
/bundles/org.openhab.voice.mimictts/ @dalgwen
|
||||||
/bundles/org.openhab.voice.picotts/ @FlorianSW
|
/bundles/org.openhab.voice.picotts/ @FlorianSW
|
||||||
/bundles/org.openhab.voice.pollytts/ @openhab/add-ons-maintainers
|
/bundles/org.openhab.voice.pollytts/ @openhab/add-ons-maintainers
|
||||||
/bundles/org.openhab.voice.porcupineks/ @GiviMAD
|
|
||||||
/bundles/org.openhab.voice.rustpotterks/ @GiviMAD
|
/bundles/org.openhab.voice.rustpotterks/ @GiviMAD
|
||||||
/bundles/org.openhab.voice.voicerss/ @lolodomo
|
/bundles/org.openhab.voice.voicerss/ @lolodomo
|
||||||
/bundles/org.openhab.voice.voskstt/ @GiviMAD
|
/bundles/org.openhab.voice.voskstt/ @GiviMAD
|
||||||
|
|
|
@ -2116,11 +2116,6 @@
|
||||||
<artifactId>org.openhab.voice.pollytts</artifactId>
|
<artifactId>org.openhab.voice.pollytts</artifactId>
|
||||||
<version>${project.version}</version>
|
<version>${project.version}</version>
|
||||||
</dependency>
|
</dependency>
|
||||||
<dependency>
|
|
||||||
<groupId>org.openhab.addons.bundles</groupId>
|
|
||||||
<artifactId>org.openhab.voice.porcupineks</artifactId>
|
|
||||||
<version>${project.version}</version>
|
|
||||||
</dependency>
|
|
||||||
<dependency>
|
<dependency>
|
||||||
<groupId>org.openhab.addons.bundles</groupId>
|
<groupId>org.openhab.addons.bundles</groupId>
|
||||||
<artifactId>org.openhab.voice.rustpotterks</artifactId>
|
<artifactId>org.openhab.voice.rustpotterks</artifactId>
|
||||||
|
|
|
@ -1,20 +0,0 @@
|
||||||
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
|
|
||||||
|
|
||||||
== Third-party Content
|
|
||||||
|
|
||||||
ai.picovoice: porcupine-java
|
|
||||||
* License: Apache 2.0 License
|
|
||||||
* Project: https://github.com/Picovoice/porcupine
|
|
||||||
* Source: https://github.com/Picovoice/porcupine/tree/v2.1/binding/java
|
|
|
@ -1,86 +0,0 @@
|
||||||
# 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.
|
|
||||||
|
|
||||||
## Configuration
|
|
||||||
|
|
||||||
After installing, you will be able to access the service options through the openHAB configuration page in UI (**Settings / Other Services - Porcupine Keyword Spotter**) to edit them:
|
|
||||||
|
|
||||||
* **Pico Voice API Key** - API key from PicoVoice, required to use Porcupine.
|
|
||||||
|
|
||||||
* **Sensitivity** - Spot sensitivity, a higher sensitivity reduces miss rate at cost of increased false alarm rate.
|
|
||||||
|
|
||||||
In case you would like to setup the service via a text file, create a new file in `$OPENHAB_ROOT/conf/services` named `porcupineks.cfg`
|
|
||||||
|
|
||||||
Its contents should look similar to:
|
|
||||||
|
|
||||||
```
|
|
||||||
org.openhab.voice.porcupineks:apiKey=KEY
|
|
||||||
org.openhab.voice.porcupineks:sensitivity=0.5
|
|
||||||
```
|
|
||||||
|
|
||||||
## 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 '\<openHAB userdata\>/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 '\<openHAB userdata\>/porcupine'.
|
|
||||||
|
|
||||||
To get the language model files, go to the [Porcupine repo](https://github.com/Picovoice/porcupine/tree/v2.1/lib/common).
|
|
||||||
|
|
||||||
Note that the keyword model you use should match the language model.
|
|
||||||
|
|
||||||
## Default Keyword Spotter and Magic Word Configuration
|
|
||||||
|
|
||||||
You can setup your preferred default keyword spotter and default magic word in the UI:
|
|
||||||
|
|
||||||
* Go to **Settings**.
|
|
||||||
* Edit **System Services - Voice**.
|
|
||||||
* Set **Porcupine Keyword Spotter** as **Default Keyword Spotter**.
|
|
||||||
* Choose your preferred **Magic Word** for your setup.
|
|
||||||
* Choose optionally your **Listening Switch** item that will be switch ON during the period when the dialog processor has spotted the keyword and is listening for commands.
|
|
||||||
|
|
||||||
In case you would like to setup these settings via a text file, you can edit the file `runtime.cfg` in `$OPENHAB_ROOT/conf/services` and set the following entries:
|
|
||||||
|
|
||||||
```
|
|
||||||
org.openhab.voice:defaultKS=porcupineks
|
|
||||||
org.openhab.voice:keyword=picovoice
|
|
||||||
org.openhab.voice:listeningItem=myItemForDialog
|
|
||||||
```
|
|
|
@ -1,24 +0,0 @@
|
||||||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
|
||||||
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
|
||||||
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
|
|
||||||
|
|
||||||
<modelVersion>4.0.0</modelVersion>
|
|
||||||
|
|
||||||
<parent>
|
|
||||||
<groupId>org.openhab.addons.bundles</groupId>
|
|
||||||
<artifactId>org.openhab.addons.reactor.bundles</artifactId>
|
|
||||||
<version>4.1.0-SNAPSHOT</version>
|
|
||||||
</parent>
|
|
||||||
|
|
||||||
<artifactId>org.openhab.voice.porcupineks</artifactId>
|
|
||||||
|
|
||||||
<name>openHAB Add-ons :: Bundles :: Voice :: Porcupine Keyword Spotter</name>
|
|
||||||
<dependencies>
|
|
||||||
<dependency>
|
|
||||||
<groupId>ai.picovoice</groupId>
|
|
||||||
<artifactId>porcupine-java</artifactId>
|
|
||||||
<version>2.1.0</version>
|
|
||||||
<scope>compile</scope>
|
|
||||||
</dependency>
|
|
||||||
</dependencies>
|
|
||||||
</project>
|
|
|
@ -1,9 +0,0 @@
|
||||||
<?xml version="1.0" encoding="UTF-8"?>
|
|
||||||
<features name="org.openhab.voice.porcupineks-${project.version}" xmlns="http://karaf.apache.org/xmlns/features/v1.4.0">
|
|
||||||
<repository>mvn:org.openhab.core.features.karaf/org.openhab.core.features.karaf.openhab-core/${ohc.version}/xml/features</repository>
|
|
||||||
|
|
||||||
<feature name="openhab-voice-porcupineks" description="Porcupine Keyword Spotter" version="${project.version}">
|
|
||||||
<feature>openhab-runtime-base</feature>
|
|
||||||
<bundle start-level="80">mvn:org.openhab.addons.bundles/org.openhab.voice.porcupineks/${project.version}</bundle>
|
|
||||||
</feature>
|
|
||||||
</features>
|
|
|
@ -1,33 +0,0 @@
|
||||||
/**
|
|
||||||
* Copyright (c) 2010-2023 Contributors to the openHAB project
|
|
||||||
*
|
|
||||||
* See the NOTICE file(s) distributed with this work for additional
|
|
||||||
* information.
|
|
||||||
*
|
|
||||||
* This program and the accompanying materials are made available under the
|
|
||||||
* terms of the Eclipse Public License 2.0 which is available at
|
|
||||||
* http://www.eclipse.org/legal/epl-2.0
|
|
||||||
*
|
|
||||||
* SPDX-License-Identifier: EPL-2.0
|
|
||||||
*/
|
|
||||||
package org.openhab.voice.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;
|
|
||||||
}
|
|
|
@ -1,44 +0,0 @@
|
||||||
/**
|
|
||||||
* Copyright (c) 2010-2023 Contributors to the openHAB project
|
|
||||||
*
|
|
||||||
* See the NOTICE file(s) distributed with this work for additional
|
|
||||||
* information.
|
|
||||||
*
|
|
||||||
* This program and the accompanying materials are made available under the
|
|
||||||
* terms of the Eclipse Public License 2.0 which is available at
|
|
||||||
* http://www.eclipse.org/legal/epl-2.0
|
|
||||||
*
|
|
||||||
* SPDX-License-Identifier: EPL-2.0
|
|
||||||
*/
|
|
||||||
package org.openhab.voice.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";
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 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;
|
|
||||||
}
|
|
|
@ -1,346 +0,0 @@
|
||||||
/**
|
|
||||||
* Copyright (c) 2010-2023 Contributors to the openHAB project
|
|
||||||
*
|
|
||||||
* See the NOTICE file(s) distributed with this work for additional
|
|
||||||
* information.
|
|
||||||
*
|
|
||||||
* This program and the accompanying materials are made available under the
|
|
||||||
* terms of the Eclipse Public License 2.0 which is available at
|
|
||||||
* http://www.eclipse.org/legal/epl-2.0
|
|
||||||
*
|
|
||||||
* SPDX-License-Identifier: EPL-2.0
|
|
||||||
*/
|
|
||||||
package org.openhab.voice.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.concurrent.atomic.AtomicBoolean;
|
|
||||||
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.osgi.service.component.annotations.Modified;
|
|
||||||
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
|
|
||||||
+ " Keyword Spotter", 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 @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<String, Object> config) {
|
|
||||||
this.bundleContext = componentContext.getBundleContext();
|
|
||||||
modified(config);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Modified
|
|
||||||
protected void modified(Map<String, Object> config) {
|
|
||||||
this.config = new Configuration(config).as(PorcupineKSConfiguration.class);
|
|
||||||
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()) {
|
|
||||||
if ("\\".equals(File.separator)) {
|
|
||||||
// bundle requires unix path separator
|
|
||||||
logger.debug("use unix path separator");
|
|
||||||
relativePath = relativePath.replace("\\", "/");
|
|
||||||
}
|
|
||||||
URL porcupineResource = bundleContext.getBundle().getEntry(relativePath);
|
|
||||||
logger.debug("extracting binary {} from bundle to extraction folder", relativePath);
|
|
||||||
if (porcupineResource == null) {
|
|
||||||
throw new IOException("Missing bundle file: " + 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<Locale> getSupportedLocales() {
|
|
||||||
return Set.of(Locale.ENGLISH, new Locale("es"), Locale.FRENCH, Locale.GERMAN);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public Set<AudioFormat> 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);
|
|
||||||
}
|
|
||||||
final AtomicBoolean aborted = new AtomicBoolean(false);
|
|
||||||
Future<?> scheduledTask = executor
|
|
||||||
.submit(() -> processInBackground(porcupine, ksListener, audioStream, aborted));
|
|
||||||
return new KSServiceHandle() {
|
|
||||||
@Override
|
|
||||||
public void abort() {
|
|
||||||
logger.debug("stopping service");
|
|
||||||
aborted.set(true);
|
|
||||||
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 ("es".equals(locale.getLanguage())) {
|
|
||||||
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 = Path
|
|
||||||
.of("porcupine", "resources", "keyword_files", env, keyWord.replace(" ", "_") + "_" + env + ".ppn")
|
|
||||||
.toString();
|
|
||||||
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,
|
|
||||||
AtomicBoolean aborted) {
|
|
||||||
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];
|
|
||||||
while (!aborted.get()) {
|
|
||||||
try {
|
|
||||||
// read a buffer of audio
|
|
||||||
numBytesRead = audioStream.read(captureBuffer.array(), 0, captureBuffer.capacity());
|
|
||||||
if (aborted.get()) {
|
|
||||||
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");
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,15 +0,0 @@
|
||||||
<?xml version="1.0" encoding="UTF-8"?>
|
|
||||||
<addon:addon id="porcupineks" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
|
||||||
xmlns:addon="https://openhab.org/schemas/addon/v1.0.0"
|
|
||||||
xsi:schemaLocation="https://openhab.org/schemas/addon/v1.0.0 https://openhab.org/schemas/addon-1.0.0.xsd">
|
|
||||||
|
|
||||||
<type>voice</type>
|
|
||||||
<name>Porcupine Keyword Spotter</name>
|
|
||||||
<description>This voice service allows you to use the PicoVoice product Porcupine as your keyword spotter in openHAB.</description>
|
|
||||||
<connection>hybrid</connection>
|
|
||||||
|
|
||||||
<service-id>org.openhab.voice.procupineks</service-id>
|
|
||||||
|
|
||||||
<config-description-ref uri="voice:porcupineks"/>
|
|
||||||
|
|
||||||
</addon:addon>
|
|
|
@ -1,19 +0,0 @@
|
||||||
<?xml version="1.0" encoding="UTF-8"?>
|
|
||||||
<config-description:config-descriptions
|
|
||||||
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
|
||||||
xmlns:config-description="https://openhab.org/schemas/config-description/v1.0.0"
|
|
||||||
xsi:schemaLocation="https://openhab.org/schemas/config-description/v1.0.0
|
|
||||||
https://openhab.org/schemas/config-description-1.0.0.xsd">
|
|
||||||
<config-description uri="voice:porcupineks">
|
|
||||||
<parameter name="apiKey" type="text" required="true">
|
|
||||||
<label>Pico Voice API Key</label>
|
|
||||||
<description>API key from PicoVoice, required to use Porcupine.</description>
|
|
||||||
</parameter>
|
|
||||||
<parameter name="sensitivity" type="decimal" min="0" max="1">
|
|
||||||
<label>Sensitivity</label>
|
|
||||||
<description>Spot sensitivity, a higher sensitivity reduces miss rate at the cost of increased false detection rate.</description>
|
|
||||||
<default>0.5</default>
|
|
||||||
</parameter>
|
|
||||||
</config-description>
|
|
||||||
|
|
||||||
</config-description:config-descriptions>
|
|
|
@ -1,8 +0,0 @@
|
||||||
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 the cost of increased false detection rate.
|
|
||||||
|
|
||||||
# service
|
|
||||||
|
|
||||||
service.voice.porcupineks.label = Porcupine Keyword Spotter
|
|
|
@ -1,8 +0,0 @@
|
||||||
voice.config.porcupineks.apiKey.label = Pico Voice API Key
|
|
||||||
voice.config.porcupineks.apiKey.description = API-Schlüssel von PicoVoice, erfordert die Nutzung von Porcupine.
|
|
||||||
voice.config.porcupineks.sensitivity.label = Empfindlichkeit
|
|
||||||
voice.config.porcupineks.sensitivity.description = Spot-Sensitivität\: Eine höhere Sensitivität verringert die Fehlerquote auf Kosten einer höheren Falscherkennungsrate.
|
|
||||||
|
|
||||||
# service
|
|
||||||
|
|
||||||
service.voice.porcupineks.label = Porcupine Schlüsselwort-Erkennung
|
|
|
@ -1,8 +0,0 @@
|
||||||
voice.config.porcupineks.apiKey.label = Pico Voice API-avain
|
|
||||||
voice.config.porcupineks.apiKey.description = PicoVoicen API-avain, jota tarvitaan Porcupinen käyttöön.
|
|
||||||
voice.config.porcupineks.sensitivity.label = Herkkyys
|
|
||||||
voice.config.porcupineks.sensitivity.description = Pisteherkkyys, korkeampi arvo vähentää ohimenneitä osumia mutta lisää vääriä vastaavasti vääriä osumia.
|
|
||||||
|
|
||||||
# service
|
|
||||||
|
|
||||||
service.voice.porcupineks.label = Porcupine avainsanan havaitsija
|
|
|
@ -1,8 +0,0 @@
|
||||||
voice.config.porcupineks.apiKey.label = Clé API Pico Voice
|
|
||||||
voice.config.porcupineks.apiKey.description = Clé API de PicoVoice, nécessaire pour utiliser Porcupine.
|
|
||||||
voice.config.porcupineks.sensitivity.label = Sensibilité
|
|
||||||
voice.config.porcupineks.sensitivity.description = Sensibilité de la détection, une sensibilité plus élevée réduit le taux d'échec au prix d'un taux de fausse détection accru.
|
|
||||||
|
|
||||||
# service
|
|
||||||
|
|
||||||
service.voice.porcupineks.label = Détecteur de mot clé Porcupine
|
|
|
@ -444,7 +444,6 @@
|
||||||
<module>org.openhab.voice.mimictts</module>
|
<module>org.openhab.voice.mimictts</module>
|
||||||
<module>org.openhab.voice.picotts</module>
|
<module>org.openhab.voice.picotts</module>
|
||||||
<module>org.openhab.voice.pollytts</module>
|
<module>org.openhab.voice.pollytts</module>
|
||||||
<module>org.openhab.voice.porcupineks</module>
|
|
||||||
<module>org.openhab.voice.rustpotterks</module>
|
<module>org.openhab.voice.rustpotterks</module>
|
||||||
<module>org.openhab.voice.voicerss</module>
|
<module>org.openhab.voice.voicerss</module>
|
||||||
<module>org.openhab.voice.voskstt</module>
|
<module>org.openhab.voice.voskstt</module>
|
||||||
|
|
Loading…
Reference in New Issue