diff --git a/bundles/core/org.eclipse.smarthome.core.voice.test/.classpath b/bundles/core/org.eclipse.smarthome.core.voice.test/.classpath new file mode 100644 index 00000000000..f725d321e1b --- /dev/null +++ b/bundles/core/org.eclipse.smarthome.core.voice.test/.classpath @@ -0,0 +1,8 @@ + + + + + + + + diff --git a/bundles/core/org.eclipse.smarthome.core.voice.test/.project b/bundles/core/org.eclipse.smarthome.core.voice.test/.project new file mode 100644 index 00000000000..d17f40c1782 --- /dev/null +++ b/bundles/core/org.eclipse.smarthome.core.voice.test/.project @@ -0,0 +1,33 @@ + + + org.eclipse.smarthome.core.voice.test + + + + + + org.eclipse.jdt.core.javabuilder + + + + + org.eclipse.pde.ManifestBuilder + + + + + org.eclipse.pde.SchemaBuilder + + + + + org.eclipse.pde.ds.core.builder + + + + + + org.eclipse.pde.PluginNature + org.eclipse.jdt.core.javanature + + diff --git a/bundles/io/org.eclipse.smarthome.io.voice/.settings/org.eclipse.jdt.core.prefs b/bundles/core/org.eclipse.smarthome.core.voice.test/.settings/org.eclipse.jdt.core.prefs similarity index 100% rename from bundles/io/org.eclipse.smarthome.io.voice/.settings/org.eclipse.jdt.core.prefs rename to bundles/core/org.eclipse.smarthome.core.voice.test/.settings/org.eclipse.jdt.core.prefs diff --git a/bundles/io/org.eclipse.smarthome.io.voice/.settings/org.eclipse.pde.core.prefs b/bundles/core/org.eclipse.smarthome.core.voice.test/.settings/org.eclipse.pde.core.prefs similarity index 100% rename from bundles/io/org.eclipse.smarthome.io.voice/.settings/org.eclipse.pde.core.prefs rename to bundles/core/org.eclipse.smarthome.core.voice.test/.settings/org.eclipse.pde.core.prefs diff --git a/bundles/core/org.eclipse.smarthome.core.voice.test/META-INF/MANIFEST.MF b/bundles/core/org.eclipse.smarthome.core.voice.test/META-INF/MANIFEST.MF new file mode 100644 index 00000000000..3ad4b17d63b --- /dev/null +++ b/bundles/core/org.eclipse.smarthome.core.voice.test/META-INF/MANIFEST.MF @@ -0,0 +1,12 @@ +Manifest-Version: 1.0 +Bundle-ManifestVersion: 2 +Bundle-Name: Eclipse SmartHome Core Voice Tests +Bundle-SymbolicName: org.eclipse.smarthome.core.voice.test +Bundle-Version: 0.9.0.qualifier +Bundle-Vendor: Eclipse.org/SmartHome +Fragment-Host: org.eclipse.smarthome.core.voice +Bundle-RequiredExecutionEnvironment: JavaSE-1.7 +Import-Package: org.eclipse.smarthome.core.voice, + org.hamcrest.core, + org.junit;version="4.0.0" +Bundle-ClassPath: . diff --git a/bundles/io/org.eclipse.smarthome.io.voice/about.html b/bundles/core/org.eclipse.smarthome.core.voice.test/about.html similarity index 100% rename from bundles/io/org.eclipse.smarthome.io.voice/about.html rename to bundles/core/org.eclipse.smarthome.core.voice.test/about.html diff --git a/bundles/core/org.eclipse.smarthome.core.voice.test/build.properties b/bundles/core/org.eclipse.smarthome.core.voice.test/build.properties new file mode 100644 index 00000000000..df0687569fb --- /dev/null +++ b/bundles/core/org.eclipse.smarthome.core.voice.test/build.properties @@ -0,0 +1,5 @@ +source.. = src/test/java/ +output.. = target/test-classes/ +bin.includes = META-INF/,\ + .,\ + about.html diff --git a/bundles/io/org.eclipse.smarthome.io.audio/pom.xml b/bundles/core/org.eclipse.smarthome.core.voice.test/pom.xml similarity index 57% rename from bundles/io/org.eclipse.smarthome.io.audio/pom.xml rename to bundles/core/org.eclipse.smarthome.core.voice.test/pom.xml index 07e6a5410d7..9b7f9e9664c 100644 --- a/bundles/io/org.eclipse.smarthome.io.audio/pom.xml +++ b/bundles/core/org.eclipse.smarthome.core.voice.test/pom.xml @@ -3,21 +3,21 @@ org.eclipse.smarthome.bundles - io + core 0.9.0-SNAPSHOT - org.eclipse.smarthome.io.audio - org.eclipse.smarthome.io.audio + org.eclipse.smarthome.core.voice.test + org.eclipse.smarthome.core.voice.test 4.0.0 org.eclipse.smarthome.io - org.eclipse.smarthome.io.audio - - Eclipse SmartHome Audio I/O - - eclipse-plugin + org.eclipse.smarthome.core.voice.test + + Eclipse SmartHome Core Voice Test + + eclipse-test-plugin diff --git a/bundles/core/org.eclipse.smarthome.core.voice.test/src/test/java/org/eclipse/smarthome/core/voice/STTExceptionTest.java b/bundles/core/org.eclipse.smarthome.core.voice.test/src/test/java/org/eclipse/smarthome/core/voice/STTExceptionTest.java new file mode 100644 index 00000000000..72eac03c0bb --- /dev/null +++ b/bundles/core/org.eclipse.smarthome.core.voice.test/src/test/java/org/eclipse/smarthome/core/voice/STTExceptionTest.java @@ -0,0 +1,55 @@ +/** + * Copyright (c) 2014-2016 by the respective copyright holders. + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v1.0 + * which accompanies this distribution, and is available at + * http://www.eclipse.org/legal/epl-v10.html + */ +package org.eclipse.smarthome.core.voice; + +import org.junit.Assert; +import org.junit.Test; + +/** + * Test general purpose STT exception + * + * @author Kelly Davis - Initial contribution and API + */ +public class STTExceptionTest { + + /** + * Test STTException() constructor + */ + @Test + public void testConstructor0() { + STTException ttsException = new STTException(); + Assert.assertNotNull("STTException() constructor failed", ttsException); + } + + /** + * Test STTException(String message, Throwable cause) constructor + */ + @Test + public void testConstructor1() { + STTException ttsException = new STTException("Message", new Throwable()); + Assert.assertNotNull("STTException(String, Throwable) constructor failed", ttsException); + } + + /** + * Test STTException(String message) constructor + */ + @Test + public void testConstructor2() { + STTException ttsException = new STTException("Message"); + Assert.assertNotNull("STTException(String) constructor failed", ttsException); + } + + /** + * Test STTException(Throwable cause) constructor + */ + @Test + public void testConstructor3() { + STTException ttsException = new STTException(new Throwable()); + Assert.assertNotNull("STTException(Throwable) constructor failed", ttsException); + } +} diff --git a/bundles/core/org.eclipse.smarthome.core.voice.test/src/test/java/org/eclipse/smarthome/core/voice/SpeechRecognitionErrorEventTest.java b/bundles/core/org.eclipse.smarthome.core.voice.test/src/test/java/org/eclipse/smarthome/core/voice/SpeechRecognitionErrorEventTest.java new file mode 100644 index 00000000000..aa5d47da7ac --- /dev/null +++ b/bundles/core/org.eclipse.smarthome.core.voice.test/src/test/java/org/eclipse/smarthome/core/voice/SpeechRecognitionErrorEventTest.java @@ -0,0 +1,37 @@ +/** + * Copyright (c) 2014-2016 by the respective copyright holders. + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v1.0 + * which accompanies this distribution, and is available at + * http://www.eclipse.org/legal/epl-v10.html + */ +package org.eclipse.smarthome.core.voice; + +import org.junit.Assert; +import org.junit.Test; + +/** + * Test SpeechRecognitionErrorEvent event + * + * @author Kelly Davis - Initial contribution and API + */ +public class SpeechRecognitionErrorEventTest { + + /** + * Test SpeechRecognitionErrorEvent(String) constructor + */ + @Test + public void testConstructor() { + SpeechRecognitionErrorEvent sRE = new SpeechRecognitionErrorEvent("Message"); + Assert.assertNotNull("SpeechRecognitionErrorEvent(String) constructor failed", sRE); + } + + /** + * Test SpeechRecognitionErrorEvent.getMessage() method + */ + @Test + public void getMessageTest() { + SpeechRecognitionErrorEvent sRE = new SpeechRecognitionErrorEvent("Message"); + Assert.assertEquals("SpeechRecognitionErrorEvent.getMessage() method failed", "Message", sRE.getMessage()); + } +} diff --git a/bundles/core/org.eclipse.smarthome.core.voice.test/src/test/java/org/eclipse/smarthome/core/voice/SpeechRecognitionEventTest.java b/bundles/core/org.eclipse.smarthome.core.voice.test/src/test/java/org/eclipse/smarthome/core/voice/SpeechRecognitionEventTest.java new file mode 100644 index 00000000000..1d02583bce5 --- /dev/null +++ b/bundles/core/org.eclipse.smarthome.core.voice.test/src/test/java/org/eclipse/smarthome/core/voice/SpeechRecognitionEventTest.java @@ -0,0 +1,47 @@ +/** + * Copyright (c) 2014-2016 by the respective copyright holders. + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v1.0 + * which accompanies this distribution, and is available at + * http://www.eclipse.org/legal/epl-v10.html + */ +package org.eclipse.smarthome.core.voice; + +import org.junit.Assert; +import org.junit.Test; + +/** + * Test SpeechRecognitionEvent event + * + * @author Kelly Davis - Initial contribution and API + */ +public class SpeechRecognitionEventTest { + + /** + * Test SpeechRecognitionEvent(String, float) constructor + */ + @Test + public void testConstructor() { + SpeechRecognitionEvent sRE = new SpeechRecognitionEvent("Message", 0.5f); + Assert.assertNotNull("SpeechRecognitionEvent(String, float) constructor failed", sRE); + } + + /** + * Test SpeechRecognitionEvent.getTranscript() method + */ + @Test + public void getTranscriptTest() { + SpeechRecognitionEvent sRE = new SpeechRecognitionEvent("Message", 0.5f); + Assert.assertEquals("SpeechRecognitionEvent.getTranscript() method failed", "Message", sRE.getTranscript()); + } + + /** + * Test SpeechRecognitionEvent.getConfidence() method + */ + @Test + public void getConfidenceTest() { + SpeechRecognitionEvent sRE = new SpeechRecognitionEvent("Message", 0.5f); + Assert.assertEquals("SpeechRecognitionEvent.getConfidence() method failed", (double) 0.5f, + (double) sRE.getConfidence(), (double) 0.001f); + } +} diff --git a/bundles/core/org.eclipse.smarthome.core.voice.test/src/test/java/org/eclipse/smarthome/core/voice/TTSExceptionTest.java b/bundles/core/org.eclipse.smarthome.core.voice.test/src/test/java/org/eclipse/smarthome/core/voice/TTSExceptionTest.java new file mode 100644 index 00000000000..aa89cc693ac --- /dev/null +++ b/bundles/core/org.eclipse.smarthome.core.voice.test/src/test/java/org/eclipse/smarthome/core/voice/TTSExceptionTest.java @@ -0,0 +1,55 @@ +/** + * Copyright (c) 2014-2016 by the respective copyright holders. + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v1.0 + * which accompanies this distribution, and is available at + * http://www.eclipse.org/legal/epl-v10.html + */ +package org.eclipse.smarthome.core.voice; + +import org.junit.Assert; +import org.junit.Test; + +/** + * Test general purpose TTS exception + * + * @author Kelly Davis - Initial contribution and API + */ +public class TTSExceptionTest { + + /** + * Test TTSException() constructor + */ + @Test + public void testConstructor0() { + TTSException ttsException = new TTSException(); + Assert.assertNotNull("TTSException() constructor failed", ttsException); + } + + /** + * Test TTSException(String message, Throwable cause) constructor + */ + @Test + public void testConstructor1() { + TTSException ttsException = new TTSException("Message", new Throwable()); + Assert.assertNotNull("TTSException(String, Throwable) constructor failed", ttsException); + } + + /** + * Test TTSException(String message) constructor + */ + @Test + public void testConstructor2() { + TTSException ttsException = new TTSException("Message"); + Assert.assertNotNull("TTSException(String) constructor failed", ttsException); + } + + /** + * Test TTSException(Throwable cause) constructor + */ + @Test + public void testConstructor3() { + TTSException ttsException = new TTSException(new Throwable()); + Assert.assertNotNull("TTSException(Throwable) constructor failed", ttsException); + } +} diff --git a/bundles/core/org.eclipse.smarthome.core.voice.test/src/test/resources/org/eclipse/smarthome/io/voice/internal/goforward.raw b/bundles/core/org.eclipse.smarthome.core.voice.test/src/test/resources/org/eclipse/smarthome/io/voice/internal/goforward.raw new file mode 100644 index 00000000000..350b76a9290 Binary files /dev/null and b/bundles/core/org.eclipse.smarthome.core.voice.test/src/test/resources/org/eclipse/smarthome/io/voice/internal/goforward.raw differ diff --git a/bundles/core/org.eclipse.smarthome.core.voice.test/src/test/resources/org/eclipse/smarthome/io/voice/internal/hellowworld.raw b/bundles/core/org.eclipse.smarthome.core.voice.test/src/test/resources/org/eclipse/smarthome/io/voice/internal/hellowworld.raw new file mode 100644 index 00000000000..1a1bff12867 Binary files /dev/null and b/bundles/core/org.eclipse.smarthome.core.voice.test/src/test/resources/org/eclipse/smarthome/io/voice/internal/hellowworld.raw differ diff --git a/bundles/core/org.eclipse.smarthome.core.voice.test/src/test/resources/org/eclipse/smarthome/io/voice/internal/marieta.raw b/bundles/core/org.eclipse.smarthome.core.voice.test/src/test/resources/org/eclipse/smarthome/io/voice/internal/marieta.raw new file mode 100644 index 00000000000..9d9b5fa746a Binary files /dev/null and b/bundles/core/org.eclipse.smarthome.core.voice.test/src/test/resources/org/eclipse/smarthome/io/voice/internal/marieta.raw differ diff --git a/bundles/io/org.eclipse.smarthome.io.voice/.classpath b/bundles/core/org.eclipse.smarthome.core.voice/.classpath similarity index 87% rename from bundles/io/org.eclipse.smarthome.io.voice/.classpath rename to bundles/core/org.eclipse.smarthome.core.voice/.classpath index a95e0906ca0..19af0fa670a 100644 --- a/bundles/io/org.eclipse.smarthome.io.voice/.classpath +++ b/bundles/core/org.eclipse.smarthome.core.voice/.classpath @@ -3,5 +3,6 @@ + diff --git a/bundles/io/org.eclipse.smarthome.io.voice/.project b/bundles/core/org.eclipse.smarthome.core.voice/.project similarity index 90% rename from bundles/io/org.eclipse.smarthome.io.voice/.project rename to bundles/core/org.eclipse.smarthome.core.voice/.project index df455ff90f3..68ccbcf6203 100644 --- a/bundles/io/org.eclipse.smarthome.io.voice/.project +++ b/bundles/core/org.eclipse.smarthome.core.voice/.project @@ -1,6 +1,6 @@ - org.eclipse.smarthome.io.voice + org.eclipse.smarthome.core.voice diff --git a/bundles/core/org.eclipse.smarthome.core.voice/.settings/org.eclipse.jdt.core.prefs b/bundles/core/org.eclipse.smarthome.core.voice/.settings/org.eclipse.jdt.core.prefs new file mode 100644 index 00000000000..f42de363afa --- /dev/null +++ b/bundles/core/org.eclipse.smarthome.core.voice/.settings/org.eclipse.jdt.core.prefs @@ -0,0 +1,7 @@ +eclipse.preferences.version=1 +org.eclipse.jdt.core.compiler.codegen.inlineJsrBytecode=enabled +org.eclipse.jdt.core.compiler.codegen.targetPlatform=1.7 +org.eclipse.jdt.core.compiler.compliance=1.7 +org.eclipse.jdt.core.compiler.problem.assertIdentifier=error +org.eclipse.jdt.core.compiler.problem.enumIdentifier=error +org.eclipse.jdt.core.compiler.source=1.7 diff --git a/bundles/core/org.eclipse.smarthome.core.voice/.settings/org.eclipse.pde.core.prefs b/bundles/core/org.eclipse.smarthome.core.voice/.settings/org.eclipse.pde.core.prefs new file mode 100644 index 00000000000..e67024ad352 --- /dev/null +++ b/bundles/core/org.eclipse.smarthome.core.voice/.settings/org.eclipse.pde.core.prefs @@ -0,0 +1,4 @@ +#Mon Oct 11 21:08:09 CEST 2010 +eclipse.preferences.version=1 +pluginProject.extensions=false +resolve.requirebundle=false diff --git a/bundles/io/org.eclipse.smarthome.io.voice/META-INF/MANIFEST.MF b/bundles/core/org.eclipse.smarthome.core.voice/META-INF/MANIFEST.MF similarity index 64% rename from bundles/io/org.eclipse.smarthome.io.voice/META-INF/MANIFEST.MF rename to bundles/core/org.eclipse.smarthome.core.voice/META-INF/MANIFEST.MF index f22975677fe..07aa136700c 100644 --- a/bundles/io/org.eclipse.smarthome.io.voice/META-INF/MANIFEST.MF +++ b/bundles/core/org.eclipse.smarthome.core.voice/META-INF/MANIFEST.MF @@ -1,26 +1,27 @@ Manifest-Version: 1.0 Bundle-ManifestVersion: 2 -Bundle-Name: Eclipse SmartHome Voice I/O Bundle -Bundle-SymbolicName: org.eclipse.smarthome.io.voice +Bundle-Name: Eclipse SmartHome Core Voice +Bundle-SymbolicName: org.eclipse.smarthome.core.voice Bundle-Version: 0.9.0.qualifier Bundle-Vendor: Eclipse.org/SmartHome Bundle-RequiredExecutionEnvironment: JavaSE-1.7 Bundle-ClassPath: . -Import-Package: org.apache.commons.collections, +Import-Package: org.apache.commons.collections.map, org.apache.commons.io, org.apache.commons.lang, + org.eclipse.smarthome.core.audio, org.eclipse.smarthome.core.common.registry, org.eclipse.smarthome.core.events, + org.eclipse.smarthome.core.i18n, org.eclipse.smarthome.core.items, org.eclipse.smarthome.core.items.events, org.eclipse.smarthome.core.library.types, org.eclipse.smarthome.core.types, org.eclipse.smarthome.io.console, org.eclipse.smarthome.io.console.extensions, - org.eclipse.smarthome.io.voice.tts, org.osgi.framework, org.slf4j -Export-Package: org.eclipse.smarthome.io.voice.text, - org.eclipse.smarthome.io.voice.tts +Export-Package: org.eclipse.smarthome.core.voice, + org.eclipse.smarthome.core.voice.text Service-Component: OSGI-INF/*.xml -Bundle-Activator: org.eclipse.smarthome.io.voice.internal.VoiceActivator +Bundle-ActivationPolicy: lazy diff --git a/bundles/io/org.eclipse.smarthome.io.voice/OSGI-INF/StandardHumanLanguageInterpreter.xml b/bundles/core/org.eclipse.smarthome.core.voice/OSGI-INF/StandardInterpreter.xml similarity index 75% rename from bundles/io/org.eclipse.smarthome.io.voice/OSGI-INF/StandardHumanLanguageInterpreter.xml rename to bundles/core/org.eclipse.smarthome.core.voice/OSGI-INF/StandardInterpreter.xml index 3d75b761775..221e8e07f61 100644 --- a/bundles/io/org.eclipse.smarthome.io.voice/OSGI-INF/StandardHumanLanguageInterpreter.xml +++ b/bundles/core/org.eclipse.smarthome.core.voice/OSGI-INF/StandardInterpreter.xml @@ -8,10 +8,10 @@ http://www.eclipse.org/legal/epl-v10.html --> - - + + - + diff --git a/bundles/io/org.eclipse.smarthome.io.voice/OSGI-INF/SayConsoleCommandExtension.xml b/bundles/core/org.eclipse.smarthome.core.voice/OSGI-INF/VoiceConsoleCommandExtension.xml similarity index 68% rename from bundles/io/org.eclipse.smarthome.io.voice/OSGI-INF/SayConsoleCommandExtension.xml rename to bundles/core/org.eclipse.smarthome.core.voice/OSGI-INF/VoiceConsoleCommandExtension.xml index 2cefef09349..7b6f1ff223a 100644 --- a/bundles/io/org.eclipse.smarthome.io.voice/OSGI-INF/SayConsoleCommandExtension.xml +++ b/bundles/core/org.eclipse.smarthome.core.voice/OSGI-INF/VoiceConsoleCommandExtension.xml @@ -8,10 +8,11 @@ http://www.eclipse.org/legal/epl-v10.html --> - - + + + diff --git a/bundles/core/org.eclipse.smarthome.core.voice/OSGI-INF/VoiceManager.xml b/bundles/core/org.eclipse.smarthome.core.voice/OSGI-INF/VoiceManager.xml new file mode 100644 index 00000000000..07a11ce2cd1 --- /dev/null +++ b/bundles/core/org.eclipse.smarthome.core.voice/OSGI-INF/VoiceManager.xml @@ -0,0 +1,23 @@ + + + + + + + + + + + + + + + diff --git a/bundles/core/org.eclipse.smarthome.core.voice/about.html b/bundles/core/org.eclipse.smarthome.core.voice/about.html new file mode 100644 index 00000000000..c258ef55d83 --- /dev/null +++ b/bundles/core/org.eclipse.smarthome.core.voice/about.html @@ -0,0 +1,28 @@ + + + + +About + + +

About This Content

+ +

June 5, 2006

+

License

+ +

The Eclipse Foundation makes available all content in this plug-in ("Content"). Unless otherwise +indicated below, the Content is provided to you under the terms and conditions of the +Eclipse Public License Version 1.0 ("EPL"). A copy of the EPL is available +at http://www.eclipse.org/legal/epl-v10.html. +For purposes of the EPL, "Program" will mean the Content.

+ +

If you did not receive this Content directly from the Eclipse Foundation, the Content is +being redistributed by another party ("Redistributor") and different terms and conditions may +apply to your use of any object code in the Content. Check the Redistributor's license that was +provided with the Content. If no such license exists, contact the Redistributor. Unless otherwise +indicated below, the terms and conditions of the EPL still apply to any source code in the Content +and such source code may be obtained at http://www.eclipse.org.

+ + + \ No newline at end of file diff --git a/bundles/core/org.eclipse.smarthome.core.voice/build.properties b/bundles/core/org.eclipse.smarthome.core.voice/build.properties new file mode 100644 index 00000000000..9e97e61f6a9 --- /dev/null +++ b/bundles/core/org.eclipse.smarthome.core.voice/build.properties @@ -0,0 +1,7 @@ +output.. = target/classes/ +bin.includes = META-INF/,\ + .,\ + OSGI-INF/,\ + about.html +source.. = src/main/java/,\ + src/main/resources/ diff --git a/bundles/io/org.eclipse.smarthome.io.voice/pom.xml b/bundles/core/org.eclipse.smarthome.core.voice/pom.xml similarity index 58% rename from bundles/io/org.eclipse.smarthome.io.voice/pom.xml rename to bundles/core/org.eclipse.smarthome.core.voice/pom.xml index 677bee96504..1a69602f139 100644 --- a/bundles/io/org.eclipse.smarthome.io.voice/pom.xml +++ b/bundles/core/org.eclipse.smarthome.core.voice/pom.xml @@ -3,20 +3,21 @@ org.eclipse.smarthome.bundles - io + core 0.9.0-SNAPSHOT - org.eclipse.smarthome.io.voice - org.eclipse.smarthome.io.voice + org.eclipse.smarthome.core.voice + org.eclipse.smarthome.core.voice 4.0.0 - org.eclipse.smarthome.io - org.eclipse.smarthome.io.voice + org.eclipse.smarthome.core + org.eclipse.smarthome.core.voice - Eclipse SmartHome Voice I/O + Eclipse SmartHome Core Voice eclipse-plugin + diff --git a/bundles/core/org.eclipse.smarthome.core.voice/src/main/java/org/eclipse/smarthome/core/voice/AudioStartEvent.java b/bundles/core/org.eclipse.smarthome.core.voice/src/main/java/org/eclipse/smarthome/core/voice/AudioStartEvent.java new file mode 100644 index 00000000000..73186e1a2af --- /dev/null +++ b/bundles/core/org.eclipse.smarthome.core.voice/src/main/java/org/eclipse/smarthome/core/voice/AudioStartEvent.java @@ -0,0 +1,16 @@ +/** + * Copyright (c) 2014-2016 by the respective copyright holders. + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v1.0 + * which accompanies this distribution, and is available at + * http://www.eclipse.org/legal/epl-v10.html + */ +package org.eclipse.smarthome.core.voice; + +/** + * A {@link STTEvent} fired when the {@link STTService} starts hearing audio. + * + * @author Kelly Davis - Initial contribution and API + */ +public class AudioStartEvent implements STTEvent { +} diff --git a/bundles/core/org.eclipse.smarthome.core.voice/src/main/java/org/eclipse/smarthome/core/voice/AudioStopEvent.java b/bundles/core/org.eclipse.smarthome.core.voice/src/main/java/org/eclipse/smarthome/core/voice/AudioStopEvent.java new file mode 100644 index 00000000000..ae28a187f80 --- /dev/null +++ b/bundles/core/org.eclipse.smarthome.core.voice/src/main/java/org/eclipse/smarthome/core/voice/AudioStopEvent.java @@ -0,0 +1,16 @@ +/** + * Copyright (c) 2014-2016 by the respective copyright holders. + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v1.0 + * which accompanies this distribution, and is available at + * http://www.eclipse.org/legal/epl-v10.html + */ +package org.eclipse.smarthome.core.voice; + +/** + * A {@link STTEvent} fired when the {@link STTService} stops hearing audio. + * + * @author Kelly Davis - Initial contribution and API + */ +public class AudioStopEvent implements STTEvent { +} diff --git a/bundles/core/org.eclipse.smarthome.core.voice/src/main/java/org/eclipse/smarthome/core/voice/KSErrorEvent.java b/bundles/core/org.eclipse.smarthome.core.voice/src/main/java/org/eclipse/smarthome/core/voice/KSErrorEvent.java new file mode 100644 index 00000000000..d6520398f8d --- /dev/null +++ b/bundles/core/org.eclipse.smarthome.core.voice/src/main/java/org/eclipse/smarthome/core/voice/KSErrorEvent.java @@ -0,0 +1,38 @@ +/** + * Copyright (c) 2014-2016 by the respective copyright holders. + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v1.0 + * which accompanies this distribution, and is available at + * http://www.eclipse.org/legal/epl-v10.html + */ +package org.eclipse.smarthome.core.voice; + +/** + * A {@link KSEvent} fired when the {@link KSService} encounters an error. + * + * @author Kelly Davis - Initial contribution and API + */ +public class KSErrorEvent implements KSEvent { + /** + * The message describing the error + */ + private final String message; + + /** + * Constructs an instance with the passed {@code message}. + * + * @param message The message describing the error + */ + public KSErrorEvent(String message) { + this.message = message; + } + + /** + * Gets the message describing this error + * + * @return The message describing this error + */ + public String getMessage() { + return this.message; + } +} diff --git a/bundles/core/org.eclipse.smarthome.core.voice/src/main/java/org/eclipse/smarthome/core/voice/KSEvent.java b/bundles/core/org.eclipse.smarthome.core.voice/src/main/java/org/eclipse/smarthome/core/voice/KSEvent.java new file mode 100644 index 00000000000..15c0f0dd2d3 --- /dev/null +++ b/bundles/core/org.eclipse.smarthome.core.voice/src/main/java/org/eclipse/smarthome/core/voice/KSEvent.java @@ -0,0 +1,16 @@ +/** + * Copyright (c) 2014-2016 by the respective copyright holders. + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v1.0 + * which accompanies this distribution, and is available at + * http://www.eclipse.org/legal/epl-v10.html + */ +package org.eclipse.smarthome.core.voice; + +/** + * A tagging interface for keyword spotting events. + * + * @author Kelly Davis - Initial contribution and API + */ +public interface KSEvent { +} diff --git a/bundles/core/org.eclipse.smarthome.core.voice/src/main/java/org/eclipse/smarthome/core/voice/KSException.java b/bundles/core/org.eclipse.smarthome.core.voice/src/main/java/org/eclipse/smarthome/core/voice/KSException.java new file mode 100644 index 00000000000..2d7ec731916 --- /dev/null +++ b/bundles/core/org.eclipse.smarthome.core.voice/src/main/java/org/eclipse/smarthome/core/voice/KSException.java @@ -0,0 +1,53 @@ +/** + * Copyright (c) 2014-2016 by the respective copyright holders. + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v1.0 + * which accompanies this distribution, and is available at + * http://www.eclipse.org/legal/epl-v10.html + */ +package org.eclipse.smarthome.core.voice; + +/** + * General purpose keyword spotting exception + * + * @author Kelly Davis - Initial contribution and API + */ +public class KSException extends Exception { + + private static final long serialVersionUID = 1L; + + /** + * Constructs a new exception with null as its detail message. + */ + public KSException() { + super(); + } + + /** + * Constructs a new exception with the specified detail message and cause. + * + * @param message Detail message + * @param cause The cause + */ + public KSException(String message, Throwable cause) { + super(message, cause); + } + + /** + * Constructs a new exception with the specified detail message. + * + * @param message Detail message + */ + public KSException(String message) { + super(message); + } + + /** + * Constructs a new exception with the specified cause. + * + * @param cause The cause + */ + public KSException(Throwable cause) { + super(cause); + } +} diff --git a/bundles/core/org.eclipse.smarthome.core.voice/src/main/java/org/eclipse/smarthome/core/voice/KSListener.java b/bundles/core/org.eclipse.smarthome.core.voice/src/main/java/org/eclipse/smarthome/core/voice/KSListener.java new file mode 100644 index 00000000000..2c439293db7 --- /dev/null +++ b/bundles/core/org.eclipse.smarthome.core.voice/src/main/java/org/eclipse/smarthome/core/voice/KSListener.java @@ -0,0 +1,27 @@ +/** + * Copyright (c) 2014-2016 by the respective copyright holders. + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v1.0 + * which accompanies this distribution, and is available at + * http://www.eclipse.org/legal/epl-v10.html + */ +package org.eclipse.smarthome.core.voice; + +/** + * The listener interface for receiving {@link KSEvent} events. + * + * A class interested in processing {@link KSEvent} events implements this interface, + * and its instances are passed to the {@code KSService}'s {@code spot()} method. + * Such instances are then targeted for various {@link KSEvent} events corresponding + * to the keyword spotting process. + * + * @author Kelly Davis - Initial contribution and API + */ +public interface KSListener { + /** + * Invoked when a {@link KSEvent} event occurs during keyword spotting. + * + * @param ksEvent The {@link KSEvent} fired by the {@link KSService} + */ + public void ksEventReceived(KSEvent ksEvent); +} diff --git a/bundles/core/org.eclipse.smarthome.core.voice/src/main/java/org/eclipse/smarthome/core/voice/KSService.java b/bundles/core/org.eclipse.smarthome.core.voice/src/main/java/org/eclipse/smarthome/core/voice/KSService.java new file mode 100644 index 00000000000..6fa75d0965a --- /dev/null +++ b/bundles/core/org.eclipse.smarthome.core.voice/src/main/java/org/eclipse/smarthome/core/voice/KSService.java @@ -0,0 +1,81 @@ +/** + * Copyright (c) 2014-2016 by the respective copyright holders. + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v1.0 + * which accompanies this distribution, and is available at + * http://www.eclipse.org/legal/epl-v10.html + */ +package org.eclipse.smarthome.core.voice; + +import java.util.Locale; +import java.util.Set; + +import org.eclipse.smarthome.core.audio.AudioFormat; +import org.eclipse.smarthome.core.audio.AudioStream; + +/** + * This is the interface that a keyword spotting service has to implement. + * + * @author Kelly Davis - Initial contribution and API + * @author Kai Kreuzer - Refactored to use AudioStream + */ +public interface KSService { + + /** + * Returns a simple string that uniquely identifies this service + * + * @return an id that identifies this service + */ + public String getId(); + + /** + * Returns a localized human readable label that can be used within UIs. + * + * @param locale the locale to provide the label for + * @return a localized string to be used in UIs + */ + public String getLabel(Locale locale); + + /** + * Obtain the Locales available from this KSService + * + * @return The Locales available from this service + */ + public Set getSupportedLocales(); + + /** + * Obtain the audio formats supported by this KSService + * + * @return The audio formats supported by this service + */ + public Set getSupportedFormats(); + + /** + * This method starts the process of keyword spotting + * + * The audio data of the passed {@link AudioStream} is passed to the keyword + * spotting engine. The keyword spotting attempts to spot {@code keyword} as + * being spoken in the passed {@code Locale}. Spotted keyword is indicated by + * fired {@link KSEvent} events targeting the passed {@link KSListener}. + * + * The passed {@link AudioStream} must be of a supported {@link AudioFormat}. + * In other words a {@link AudioFormat} compatible with one returned from + * the {@code getSupportedFormats()} method. + * + * The passed {@code Locale} must be supported. That is to say it must be + * a {@code Locale} returned from the {@code getSupportedLocales()} method. + * + * The passed {@code keyword} is the keyword which should be spotted. + * + * The method is supposed to return fast, i.e. it should only start the spotting as a background process. + * + * @param ksListener Non-null {@link KSListener} that {@link KSEvent} events target + * @param audioStream The {@link AudioStream} from which keywords are spotted + * @param locale The {@code Locale} in which the target keywords are spoken + * @param keyword The keyword which to spot + * @return A {@link KSServiceHandle} used to abort keyword spotting + * @throws A {@link KSException} if any parameter is invalid or a problem occurs + */ + public KSServiceHandle spot(KSListener ksListener, AudioStream audioStream, Locale locale, String keyword) + throws KSException; +} diff --git a/bundles/core/org.eclipse.smarthome.core.voice/src/main/java/org/eclipse/smarthome/core/voice/KSServiceHandle.java b/bundles/core/org.eclipse.smarthome.core.voice/src/main/java/org/eclipse/smarthome/core/voice/KSServiceHandle.java new file mode 100644 index 00000000000..a9936ea931e --- /dev/null +++ b/bundles/core/org.eclipse.smarthome.core.voice/src/main/java/org/eclipse/smarthome/core/voice/KSServiceHandle.java @@ -0,0 +1,20 @@ +/** + * Copyright (c) 2014-2016 by the respective copyright holders. + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v1.0 + * which accompanies this distribution, and is available at + * http://www.eclipse.org/legal/epl-v10.html + */ +package org.eclipse.smarthome.core.voice; + +/** + * An handle to a {@link KSService} + * + * @author Kelly Davis - Initial contribution and API + */ +public interface KSServiceHandle { + /** + * Aborts keyword spotting in the associated {@link KSService} + */ + public void abort(); +} diff --git a/bundles/core/org.eclipse.smarthome.core.voice/src/main/java/org/eclipse/smarthome/core/voice/KSpottedEvent.java b/bundles/core/org.eclipse.smarthome.core.voice/src/main/java/org/eclipse/smarthome/core/voice/KSpottedEvent.java new file mode 100644 index 00000000000..76565aaf265 --- /dev/null +++ b/bundles/core/org.eclipse.smarthome.core.voice/src/main/java/org/eclipse/smarthome/core/voice/KSpottedEvent.java @@ -0,0 +1,44 @@ +/** + * Copyright (c) 2014-2016 by the respective copyright holders. + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v1.0 + * which accompanies this distribution, and is available at + * http://www.eclipse.org/legal/epl-v10.html + */ +package org.eclipse.smarthome.core.voice; + +import org.eclipse.smarthome.core.audio.AudioSource; + +/** + * A {@link KSEvent} fired when the {@link KSService} spots a keyword. + * + * @author Kelly Davis - Initial contribution and API + */ +public class KSpottedEvent implements KSEvent { + /** + * AudioSource from which the keyword was spotted + */ + private final AudioSource audioSource; + + /** + * Constructs an instance with the passed {@code audioSource} + * + * @param audioSource The AudioSource of the spotted keyword + */ + public KSpottedEvent(AudioSource audioSource) { + if (null == audioSource) { + throw new IllegalArgumentException("The passed audioSource is null"); + } + + this.audioSource = audioSource; + } + + /** + * Returns the audioSource of the spotted keyword + * + * @return The audioSource of the spotted keyword + */ + public AudioSource getAudioSource() { + return this.audioSource; + } +} diff --git a/bundles/core/org.eclipse.smarthome.core.voice/src/main/java/org/eclipse/smarthome/core/voice/RecognitionStartEvent.java b/bundles/core/org.eclipse.smarthome.core.voice/src/main/java/org/eclipse/smarthome/core/voice/RecognitionStartEvent.java new file mode 100644 index 00000000000..77c6b4ebeef --- /dev/null +++ b/bundles/core/org.eclipse.smarthome.core.voice/src/main/java/org/eclipse/smarthome/core/voice/RecognitionStartEvent.java @@ -0,0 +1,16 @@ +/** + * Copyright (c) 2014-2016 by the respective copyright holders. + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v1.0 + * which accompanies this distribution, and is available at + * http://www.eclipse.org/legal/epl-v10.html + */ +package org.eclipse.smarthome.core.voice; + +/** + * A {@link STTEvent} fired when the {@link STTService} starts recognition. + * + * @author Kelly Davis - Initial contribution and API + */ +public class RecognitionStartEvent implements STTEvent { +} diff --git a/bundles/core/org.eclipse.smarthome.core.voice/src/main/java/org/eclipse/smarthome/core/voice/RecognitionStopEvent.java b/bundles/core/org.eclipse.smarthome.core.voice/src/main/java/org/eclipse/smarthome/core/voice/RecognitionStopEvent.java new file mode 100644 index 00000000000..b40f23e332d --- /dev/null +++ b/bundles/core/org.eclipse.smarthome.core.voice/src/main/java/org/eclipse/smarthome/core/voice/RecognitionStopEvent.java @@ -0,0 +1,16 @@ +/** + * Copyright (c) 2014-2016 by the respective copyright holders. + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v1.0 + * which accompanies this distribution, and is available at + * http://www.eclipse.org/legal/epl-v10.html + */ +package org.eclipse.smarthome.core.voice; + +/** + * A {@link STTEvent} fired when the {@link STTService} stops recognition. + * + * @author Kelly Davis - Initial contribution and API + */ +public class RecognitionStopEvent implements STTEvent { +} diff --git a/bundles/core/org.eclipse.smarthome.core.voice/src/main/java/org/eclipse/smarthome/core/voice/STTEvent.java b/bundles/core/org.eclipse.smarthome.core.voice/src/main/java/org/eclipse/smarthome/core/voice/STTEvent.java new file mode 100644 index 00000000000..d7acc06cb12 --- /dev/null +++ b/bundles/core/org.eclipse.smarthome.core.voice/src/main/java/org/eclipse/smarthome/core/voice/STTEvent.java @@ -0,0 +1,16 @@ +/** + * Copyright (c) 2014-2016 by the respective copyright holders. + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v1.0 + * which accompanies this distribution, and is available at + * http://www.eclipse.org/legal/epl-v10.html + */ +package org.eclipse.smarthome.core.voice; + +/** + * A tagging interface for speech-to-text events. + * + * @author Kelly Davis - Initial contribution and API + */ +public interface STTEvent { +} diff --git a/bundles/core/org.eclipse.smarthome.core.voice/src/main/java/org/eclipse/smarthome/core/voice/STTException.java b/bundles/core/org.eclipse.smarthome.core.voice/src/main/java/org/eclipse/smarthome/core/voice/STTException.java new file mode 100644 index 00000000000..8b4aa95ad35 --- /dev/null +++ b/bundles/core/org.eclipse.smarthome.core.voice/src/main/java/org/eclipse/smarthome/core/voice/STTException.java @@ -0,0 +1,53 @@ +/** + * Copyright (c) 2014-2016 by the respective copyright holders. + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v1.0 + * which accompanies this distribution, and is available at + * http://www.eclipse.org/legal/epl-v10.html + */ +package org.eclipse.smarthome.core.voice; + +/** + * General purpose STT exception + * + * @author Kelly Davis - Initial contribution and API + */ +public class STTException extends Exception { + + private static final long serialVersionUID = 1L; + + /** + * Constructs a new exception with null as its detail message. + */ + public STTException() { + super(); + } + + /** + * Constructs a new exception with the specified detail message and cause. + * + * @param message Detail message + * @param cause The cause + */ + public STTException(String message, Throwable cause) { + super(message, cause); + } + + /** + * Constructs a new exception with the specified detail message. + * + * @param message Detail message + */ + public STTException(String message) { + super(message); + } + + /** + * Constructs a new exception with the specified cause. + * + * @param cause The cause + */ + public STTException(Throwable cause) { + super(cause); + } +} diff --git a/bundles/core/org.eclipse.smarthome.core.voice/src/main/java/org/eclipse/smarthome/core/voice/STTListener.java b/bundles/core/org.eclipse.smarthome.core.voice/src/main/java/org/eclipse/smarthome/core/voice/STTListener.java new file mode 100644 index 00000000000..92a0ac0462d --- /dev/null +++ b/bundles/core/org.eclipse.smarthome.core.voice/src/main/java/org/eclipse/smarthome/core/voice/STTListener.java @@ -0,0 +1,28 @@ +/** + * Copyright (c) 2014-2016 by the respective copyright holders. + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v1.0 + * which accompanies this distribution, and is available at + * http://www.eclipse.org/legal/epl-v10.html + */ +package org.eclipse.smarthome.core.voice; + +/** + * The listener interface for receiving {@link STTEvent} events. + * + * A class interested in processing {@link STTEvent} events implements this interface, + * and its instances are passed to the {@code STTService}'s {@code recognize()} method. + * Such instances are then targeted for various {@link STTEvent} events corresponding + * to the speech recognition process. + * + * @author Kelly Davis - Initial contribution and API + */ +public interface STTListener { + + /** + * Invoked when a {@link STTEvent} event occurs during speech recognition. + * + * @param sttEvent The {@link STTEvent} fired by the {@link STTService} + */ + public void sttEventReceived(STTEvent sttEvent); +} diff --git a/bundles/core/org.eclipse.smarthome.core.voice/src/main/java/org/eclipse/smarthome/core/voice/STTService.java b/bundles/core/org.eclipse.smarthome.core.voice/src/main/java/org/eclipse/smarthome/core/voice/STTService.java new file mode 100644 index 00000000000..f871b6ad7fb --- /dev/null +++ b/bundles/core/org.eclipse.smarthome.core.voice/src/main/java/org/eclipse/smarthome/core/voice/STTService.java @@ -0,0 +1,83 @@ +/** + * Copyright (c) 2014-2016 by the respective copyright holders. + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v1.0 + * which accompanies this distribution, and is available at + * http://www.eclipse.org/legal/epl-v10.html + */ +package org.eclipse.smarthome.core.voice; + +import java.util.Locale; +import java.util.Set; + +import org.eclipse.smarthome.core.audio.AudioFormat; +import org.eclipse.smarthome.core.audio.AudioSource; +import org.eclipse.smarthome.core.audio.AudioStream; + +/** + * This is the interface that a speech-to-text service has to implement. + * + * @author Kelly Davis - Initial contribution and API + */ +public interface STTService { + + /** + * Returns a simple string that uniquely identifies this service + * + * @return an id that identifies this service + */ + public String getId(); + + /** + * Returns a localized human readable label that can be used within UIs. + * + * @param locale the locale to provide the label for + * @return a localized string to be used in UIs + */ + public String getLabel(Locale locale); + + /** + * Obtain the Locales available from this STTService + * + * @return The Locales available from this service + */ + public Set getSupportedLocales(); + + /** + * Obtain the audio formats supported by this STTService + * + * @return The audio formats supported by this service + */ + public Set getSupportedFormats(); + + /** + * This method starts the process of speech recognition. + * + * The audio data of the passed {@link AudioSource} is passed to the speech + * recognition engine. The recognition engine attempts to recognize speech + * as being spoken in the passed {@code Locale} and containing statements + * specified in the passed {@code grammars}. Recognition is indicated by + * fired {@link STTEvent} events targeting the passed {@link STTListener}. + * + * The passed {@link AudioSource} must be of a supported {@link AudioFormat}. + * In other words a {@link AudioFormat} compatible with one returned from + * the {@code getSupportedFormats()} method. + * + * The passed {@code Locale} must be supported. That is to say it must be + * a {@code Locale} returned from the {@code getSupportedLocales()} method. + * + * The passed {@code grammars} must consist of a syntactically valid grammar + * as specified by the JSpeech Grammar Format. If {@code grammars} is null + * or empty, large vocabulary continuous speech recognition is attempted. + * + * @see JSpeech Grammar Format. + * @param sttListener Non-null {@link STTListener} that {@link STTEvent} events target + * @param audioStream The {@link AudioStream} from which speech is recognized + * @param locale The {@code Locale} in which the target speech is spoken + * @param grammars The JSpeech Grammar Format grammar specifying allowed statements + * @return A {@link STTServiceHandle} used to abort recognition + * @throws A {@link SSTException} if any parameter is invalid or a STT problem occurs + */ + public STTServiceHandle recognize(STTListener sttListener, AudioStream audioStream, Locale locale, + Set grammars) throws STTException; +} diff --git a/bundles/core/org.eclipse.smarthome.core.voice/src/main/java/org/eclipse/smarthome/core/voice/STTServiceHandle.java b/bundles/core/org.eclipse.smarthome.core.voice/src/main/java/org/eclipse/smarthome/core/voice/STTServiceHandle.java new file mode 100644 index 00000000000..a1db2ae745c --- /dev/null +++ b/bundles/core/org.eclipse.smarthome.core.voice/src/main/java/org/eclipse/smarthome/core/voice/STTServiceHandle.java @@ -0,0 +1,20 @@ +/** + * Copyright (c) 2014-2016 by the respective copyright holders. + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v1.0 + * which accompanies this distribution, and is available at + * http://www.eclipse.org/legal/epl-v10.html + */ +package org.eclipse.smarthome.core.voice; + +/** + * An handle to a {@link STTService} + * + * @author Kelly Davis - Initial contribution and API + */ +public interface STTServiceHandle { + /** + * Aborts recognition in the associated {@link STTService} + */ + public void abort(); +} diff --git a/bundles/core/org.eclipse.smarthome.core.voice/src/main/java/org/eclipse/smarthome/core/voice/SpeechRecognitionErrorEvent.java b/bundles/core/org.eclipse.smarthome.core.voice/src/main/java/org/eclipse/smarthome/core/voice/SpeechRecognitionErrorEvent.java new file mode 100644 index 00000000000..9be554292ce --- /dev/null +++ b/bundles/core/org.eclipse.smarthome.core.voice/src/main/java/org/eclipse/smarthome/core/voice/SpeechRecognitionErrorEvent.java @@ -0,0 +1,38 @@ +/** + * Copyright (c) 2014-2016 by the respective copyright holders. + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v1.0 + * which accompanies this distribution, and is available at + * http://www.eclipse.org/legal/epl-v10.html + */ +package org.eclipse.smarthome.core.voice; + +/** + * A {@link STTEvent} fired when the {@link STTService} encounters an error. + * + * @author Kelly Davis - Initial contribution and API + */ +public class SpeechRecognitionErrorEvent implements STTEvent { + /** + * The message describing the error + */ + private final String message; + + /** + * Constructs an instance with the passed {@code message}. + * + * @param message The message describing the error + */ + public SpeechRecognitionErrorEvent(String message) { + this.message = message; + } + + /** + * Gets the message describing this error + * + * @return The message describing this error + */ + public String getMessage() { + return this.message; + } +} diff --git a/bundles/core/org.eclipse.smarthome.core.voice/src/main/java/org/eclipse/smarthome/core/voice/SpeechRecognitionEvent.java b/bundles/core/org.eclipse.smarthome.core.voice/src/main/java/org/eclipse/smarthome/core/voice/SpeechRecognitionEvent.java new file mode 100644 index 00000000000..c14d6c0714b --- /dev/null +++ b/bundles/core/org.eclipse.smarthome.core.voice/src/main/java/org/eclipse/smarthome/core/voice/SpeechRecognitionEvent.java @@ -0,0 +1,69 @@ +/** + * Copyright (c) 2014-2016 by the respective copyright holders. + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v1.0 + * which accompanies this distribution, and is available at + * http://www.eclipse.org/legal/epl-v10.html + */ +package org.eclipse.smarthome.core.voice; + +/** + * A {@link STTEvent} fired when the {@link STTService} recognizes speech. + * + * @author Kelly Davis - Initial contribution and API + */ +public class SpeechRecognitionEvent implements STTEvent { + /** + * Confidence of recognized speech + */ + private final float confidence; + + /** + * Transcript of recognized speech + */ + private final String transcript; + + /** + * Constructs an instance with the passed {@code transcript} and {@code confidence}. + * + * The confidence represents a numeric estimate between 0 and 1, inclusively, of how + * confident the recognition engine is of the transcript. A higher number means the + * system is more confident. + * + * @param transcript The transcript of the recognized speech + * @param confidence The confidence of the transcript + */ + public SpeechRecognitionEvent(String transcript, float confidence) { + if ((null == transcript) || (transcript.isEmpty())) { + throw new IllegalArgumentException("The passed transcript is null or empty"); + } + if ((confidence < 0.0) || (1.0 < confidence)) { + throw new IllegalArgumentException("The passed confidence is less than 0.0 or greater than 1.0"); + } + + this.transcript = transcript; + this.confidence = confidence; + } + + /** + * Returns the transcript of the recognized speech. + * + * @return The transcript of the recognized speech. + */ + public String getTranscript() { + return this.transcript; + } + + /** + * Returns the confidence of the transcript. + * + * The confidence represents a numeric estimate between 0 and 1, inclusively, of how + * confident the recognition engine is of the transcript. A higher number means the + * system is more confident. + * + * @return The transcript of the recognized speech. + */ + public float getConfidence() { + return this.confidence; + } +} diff --git a/bundles/core/org.eclipse.smarthome.core.voice/src/main/java/org/eclipse/smarthome/core/voice/SpeechStartEvent.java b/bundles/core/org.eclipse.smarthome.core.voice/src/main/java/org/eclipse/smarthome/core/voice/SpeechStartEvent.java new file mode 100644 index 00000000000..63f0077b07d --- /dev/null +++ b/bundles/core/org.eclipse.smarthome.core.voice/src/main/java/org/eclipse/smarthome/core/voice/SpeechStartEvent.java @@ -0,0 +1,16 @@ +/** + * Copyright (c) 2014-2016 by the respective copyright holders. + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v1.0 + * which accompanies this distribution, and is available at + * http://www.eclipse.org/legal/epl-v10.html + */ +package org.eclipse.smarthome.core.voice; + +/** + * A {@link STTEvent} fired when the {@link STTService} starts hearing speech. + * + * @author Kelly Davis - Initial contribution and API + */ +public class SpeechStartEvent implements STTEvent { +} diff --git a/bundles/core/org.eclipse.smarthome.core.voice/src/main/java/org/eclipse/smarthome/core/voice/SpeechStopEvent.java b/bundles/core/org.eclipse.smarthome.core.voice/src/main/java/org/eclipse/smarthome/core/voice/SpeechStopEvent.java new file mode 100644 index 00000000000..c27acefde3c --- /dev/null +++ b/bundles/core/org.eclipse.smarthome.core.voice/src/main/java/org/eclipse/smarthome/core/voice/SpeechStopEvent.java @@ -0,0 +1,16 @@ +/** + * Copyright (c) 2014-2016 by the respective copyright holders. + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v1.0 + * which accompanies this distribution, and is available at + * http://www.eclipse.org/legal/epl-v10.html + */ +package org.eclipse.smarthome.core.voice; + +/** + * A {@link STTEvent} fired when the {@link STTService} stops hearing speech. + * + * @author Kelly Davis - Initial contribution and API + */ +public class SpeechStopEvent implements STTEvent { +} diff --git a/bundles/core/org.eclipse.smarthome.core.voice/src/main/java/org/eclipse/smarthome/core/voice/TTSException.java b/bundles/core/org.eclipse.smarthome.core.voice/src/main/java/org/eclipse/smarthome/core/voice/TTSException.java new file mode 100644 index 00000000000..62ea9369e11 --- /dev/null +++ b/bundles/core/org.eclipse.smarthome.core.voice/src/main/java/org/eclipse/smarthome/core/voice/TTSException.java @@ -0,0 +1,53 @@ +/** + * Copyright (c) 2014-2016 by the respective copyright holders. + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v1.0 + * which accompanies this distribution, and is available at + * http://www.eclipse.org/legal/epl-v10.html + */ +package org.eclipse.smarthome.core.voice; + +/** + * General purpose TTS exception + * + * @author Kelly Davis - Initial contribution and API + */ +public class TTSException extends Exception { + + private static final long serialVersionUID = 1L; + + /** + * Constructs a new exception with null as its detail message. + */ + public TTSException() { + super(); + } + + /** + * Constructs a new exception with the specified detail message and cause. + * + * @param message Detail message + * @param cause The cause + */ + public TTSException(String message, Throwable cause) { + super(message, cause); + } + + /** + * Constructs a new exception with the specified detail message. + * + * @param message Detail message + */ + public TTSException(String message) { + super(message); + } + + /** + * Constructs a new exception with the specified cause. + * + * @param cause The cause + */ + public TTSException(Throwable cause) { + super(cause); + } +} diff --git a/bundles/core/org.eclipse.smarthome.core.voice/src/main/java/org/eclipse/smarthome/core/voice/TTSService.java b/bundles/core/org.eclipse.smarthome.core.voice/src/main/java/org/eclipse/smarthome/core/voice/TTSService.java new file mode 100644 index 00000000000..db81882a0c4 --- /dev/null +++ b/bundles/core/org.eclipse.smarthome.core.voice/src/main/java/org/eclipse/smarthome/core/voice/TTSService.java @@ -0,0 +1,67 @@ +/** + * Copyright (c) 2014-2016 by the respective copyright holders. + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v1.0 + * which accompanies this distribution, and is available at + * http://www.eclipse.org/legal/epl-v10.html + */ +package org.eclipse.smarthome.core.voice; + +import java.util.Locale; +import java.util.Set; + +import org.eclipse.smarthome.core.audio.AudioFormat; +import org.eclipse.smarthome.core.audio.AudioStream; + +/** + * This is the interface that a text-to-speech service has to implement. + * + * @author Kelly Davis - Initial contribution and API + * @author Kai Kreuzer - Refactored to use AudioStreams + */ +public interface TTSService { + + /** + * Returns a simple string that uniquely identifies this service + * + * @return an id that identifies this service + */ + public String getId(); + + /** + * Returns a localized human readable label that can be used within UIs. + * + * @param locale the locale to provide the label for + * @return a localized string to be used in UIs + */ + public String getLabel(Locale locale); + + /** + * Obtain the voices available from this TTSService + * + * @return The voices available from this service + */ + public Set getAvailableVoices(); + + /** + * Obtain the audio formats supported by this TTSService + * + * @return The audio formats supported by this service + */ + public Set getSupportedFormats(); + + /** + * Returns an {@link AudioStream} containing the TTS results. Note, one + * can only request a supported {@code Voice} and {@link AudioStream} or + * an exception is thrown. + * + * @param text The text to convert to speech + * @param voice The voice to use for speech + * @param requestedFormat The audio format to return the results in + * @return AudioStream containing the TTS results + * @throws TTSException If {@code voice} and/or {@code requestedFormat} + * are not supported or another error occurs while creating an + * {@link AudioStream} + */ + public AudioStream synthesize(String text, Voice voice, AudioFormat requestedFormat) throws TTSException; +} diff --git a/bundles/core/org.eclipse.smarthome.core.voice/src/main/java/org/eclipse/smarthome/core/voice/Voice.java b/bundles/core/org.eclipse.smarthome.core.voice/src/main/java/org/eclipse/smarthome/core/voice/Voice.java new file mode 100644 index 00000000000..17b11f5db4b --- /dev/null +++ b/bundles/core/org.eclipse.smarthome.core.voice/src/main/java/org/eclipse/smarthome/core/voice/Voice.java @@ -0,0 +1,40 @@ +/** + * Copyright (c) 2014-2016 by the respective copyright holders. + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v1.0 + * which accompanies this distribution, and is available at + * http://www.eclipse.org/legal/epl-v10.html + */ +package org.eclipse.smarthome.core.voice; + +import java.util.Locale; + +/** + * This is the interface that a text-to-speech voice has to implement. + * + * @author Kelly Davis - Initial contribution and API + */ +public interface Voice { + + /** + * Globally unique identifier of the voice, must have the format + * "prefix:voicename", where "prefix" is the id of the related TTS service. + * + * @return A String uniquely identifying the voice. + */ + public String getUID(); + + /** + * The voice label, usually used for GUIs + * + * @return The voice label, may not be globally unique + */ + public String getLabel(); + + /** + * Locale of the voice + * + * @return Locale of the voice + */ + public Locale getLocale(); +} diff --git a/bundles/core/org.eclipse.smarthome.core.voice/src/main/java/org/eclipse/smarthome/core/voice/VoiceManager.java b/bundles/core/org.eclipse.smarthome.core.voice/src/main/java/org/eclipse/smarthome/core/voice/VoiceManager.java new file mode 100644 index 00000000000..a7f9839dee1 --- /dev/null +++ b/bundles/core/org.eclipse.smarthome.core.voice/src/main/java/org/eclipse/smarthome/core/voice/VoiceManager.java @@ -0,0 +1,584 @@ +/** + * Copyright (c) 2014-2016 by the respective copyright holders. + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v1.0 + * which accompanies this distribution, and is available at + * http://www.eclipse.org/legal/epl-v10.html + */ +package org.eclipse.smarthome.core.voice; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.HashMap; +import java.util.HashSet; +import java.util.Locale; +import java.util.Map; +import java.util.Set; + +import org.eclipse.smarthome.core.audio.AudioFormat; +import org.eclipse.smarthome.core.audio.AudioSink; +import org.eclipse.smarthome.core.audio.AudioSource; +import org.eclipse.smarthome.core.audio.AudioStream; +import org.eclipse.smarthome.core.audio.UnsupportedAudioFormatException; +import org.eclipse.smarthome.core.i18n.LocaleProvider; +import org.eclipse.smarthome.core.voice.internal.DialogProcessor; +import org.eclipse.smarthome.core.voice.text.HumanLanguageInterpreter; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * This service provides functionality around voice services and is the central service to be used directly by others. + * + * @author Kai Kreuzer - Initial contribution and API + */ +public class VoiceManager { + + private static final Logger logger = LoggerFactory.getLogger(VoiceManager.class); + + // service maps + private Map ksServices = new HashMap<>(); + private Map sttServices = new HashMap<>(); + private Map ttsServices = new HashMap<>(); + private Map humanLanguageInterpreters = new HashMap<>(); + private Map audioSources = new HashMap<>(); + private Map audioSinks = new HashMap<>(); + + private LocaleProvider localeProvider = null; + + /** + * default settings filled through the service configuration + */ + private String keyword = "Wakeup"; + private String defaultSource = null; + private String defaultSink = null; + private String defaultTTS = null; + private String defaultSTT = null; + private String defaultKS = null; + private String defaultHLI = null; + private Map defaultVoices = new HashMap<>(); + + protected void activate(Map config) { + modified(config); + } + + protected void deactivate() { + } + + protected void modified(Map config) { + if (config != null) { + this.keyword = config.containsKey("keyword") ? config.get("keyword").toString() : "Wakeup"; + this.defaultSource = config.containsKey("defaultSource") ? config.get("defaultSource").toString() : null; + this.defaultSink = config.containsKey("defaultSink") ? config.get("defaultSink").toString() : null; + this.defaultTTS = config.containsKey("defaultTTS") ? config.get("defaultTTS").toString() : null; + this.defaultSTT = config.containsKey("defaultSTT") ? config.get("defaultSTT").toString() : null; + this.defaultKS = config.containsKey("defaultKS") ? config.get("defaultKS").toString() : null; + this.defaultHLI = config.containsKey("defaultHLI") ? config.get("defaultHLI").toString() : null; + + for (String key : config.keySet()) { + if (key.startsWith("defaultVoice.")) { + String tts = key.substring("defaultVoice.".length()); + defaultVoices.put(tts, config.get(key).toString()); + } + } + } + } + + /** + * Speaks the passed string using the default TTS service and default audio sink. + * + * @param text The text to say + */ + public void say(String text) { + say(text, null); + } + + /** + * Speaks the passed string using the provided voiceId and the default audio sink. + * If the voiceId is fully qualified (i.e. with a tts prefix), the according TTS service will be used, otherwise the + * voiceId is assumed to be available on the default TTS service. + * + * @param text The text to say + * @param voiceId The id of the voice to use (either with or without prefix) + */ + public void say(String text, String voiceId) { + say(text, voiceId, null); + } + + /** + * Speaks the passed string using the provided voiceId and the given audio sink. + * If the voiceId is fully qualified (i.e. with a tts prefix), the according TTS service will be used, otherwise the + * voiceId is assumed to be available on the default TTS service. + * + * @param text The text to say + * @param voiceId The id of the voice to use (either with or without prefix) or null + * @param sinkId The id of the audio sink to use or null + */ + public void say(String text, String voiceId, String sinkId) { + try { + TTSService tts = null; + Voice voice = null; + if (voiceId == null) { + tts = getTTS(); + voice = getPreferredVoice(tts.getAvailableVoices()); + } else if (voiceId.contains(":")) { + // it is a fully qualified unique id + String[] segments = voiceId.split(":"); + tts = ttsServices.get(segments[0]); + voice = getVoice(tts.getAvailableVoices(), segments[1]); + } else { + // voiceId is not fully qualified + tts = getTTS(); + voice = getVoice(tts.getAvailableVoices(), voiceId); + } + if (null == voice) { + throw new TTSException( + "Unable to find a voice for language " + localeProvider.getLocale().getLanguage()); + } + Set audioFormats = tts.getSupportedFormats(); + AudioSink sink = null; + if (sinkId == null) { + sink = getSink(); + } else { + sink = audioSinks.get(sinkId); + } + if (sink != null) { + AudioFormat audioFormat = getBestMatch(audioFormats, sink.getSupportedFormats()); + if (audioFormat != null) { + AudioStream audioStream = tts.synthesize(text, voice, audioFormat); + + try { + sink.process(audioStream); + } catch (UnsupportedAudioFormatException e) { + logger.error("Error saying '{}': {}", text, e.getMessage()); + } + } else { + logger.warn("No compatible audio format found for TTS '{}' and sink '{}'", tts.getId(), + sink.getId()); + } + } + } catch (TTSException e) { + logger.error("Error saying '{}'", text); + } + } + + private Voice getVoice(Set voices, String id) { + for (Voice voice : voices) { + if (voice.getUID().endsWith(":" + id)) { + return voice; + } + } + return null; + } + + /** + * Gets the first concrete AudioFormat in the passed set or a preferred one + * based on 16bit, 16KHz, big endian default + * + * @param audioFormats The AudioFormats from which to choose + * @return The preferred AudioFormat. A passed concrete format is preferred adding + * default values to an abstract AudioFormat in the passed Set. + */ + public static AudioFormat getPreferredFormat(Set audioFormats) { + // Return the first concrete AudioFormat found + for (AudioFormat currentAudioFormat : audioFormats) { + // Check if currentAudioFormat is abstract + if (null == currentAudioFormat.getCodec()) { + continue; + } + if (null == currentAudioFormat.getContainer()) { + continue; + } + if (null == currentAudioFormat.isBigEndian()) { + continue; + } + if (null == currentAudioFormat.getBitDepth()) { + continue; + } + if (null == currentAudioFormat.getBitRate()) { + continue; + } + if (null == currentAudioFormat.getFrequency()) { + continue; + } + + // Prefer WAVE container + if (!currentAudioFormat.getContainer().equals("WAVE")) { + continue; + } + + // As currentAudioFormat is concrete, use it + return currentAudioFormat; + } + + // There's no concrete AudioFormat so we must create one + for (AudioFormat currentAudioFormat : audioFormats) { + // Define AudioFormat to return + AudioFormat format = currentAudioFormat; + + // Not all Codecs and containers can be supported + if (null == format.getCodec()) { + continue; + } + if (null == format.getContainer()) { + continue; + } + + // Prefer WAVE container + if (!format.getContainer().equals(AudioFormat.CONTAINER_WAVE)) { + continue; + } + + // If required set BigEndian, BitDepth, BitRate, and Frequency to default values + if (null == format.isBigEndian()) { + format = new AudioFormat(format.getContainer(), format.getCodec(), new Boolean(true), + format.getBitDepth(), format.getBitRate(), format.getFrequency()); + } + if (null == format.getBitDepth() || null == format.getBitRate() || null == format.getFrequency()) { + // Define default values + int defaultBitDepth = 16; + long defaultFrequency = 16384; + + // Obtain current values + Integer bitRate = format.getBitRate(); + Long frequency = format.getFrequency(); + Integer bitDepth = format.getBitDepth(); + + // These values must be interdependent (bitRate = bitDepth * frequency) + if (null == bitRate) { + if (null == bitDepth) { + bitDepth = new Integer(defaultBitDepth); + } + if (null == frequency) { + frequency = new Long(defaultFrequency); + } + bitRate = new Integer(bitDepth.intValue() * frequency.intValue()); + } else if (null == bitDepth) { + if (null == frequency) { + frequency = new Long(defaultFrequency); + } + bitDepth = new Integer(bitRate.intValue() / frequency.intValue()); + } else if (null == frequency) { + frequency = new Long(bitRate.longValue() / bitDepth.longValue()); + } + + format = new AudioFormat(format.getContainer(), format.getCodec(), format.isBigEndian(), bitDepth, + bitRate, frequency); + } + + // Return preferred AudioFormat + return format; + } + + // Return null indicating failure + return null; + } + + /** + * Determines the best match between a list of audio formats supported by a source and a sink. + * + * @param inputs the supported audio formats of an audio source + * @param outputs the supported audio formats of an audio sink + * @return the best matching format or null, if source and sink are incompatible + */ + public static AudioFormat getBestMatch(Set inputs, Set outputs) { + AudioFormat preferredFormat = getPreferredFormat(inputs); + for (AudioFormat output : outputs) { + if (output.isCompatible(preferredFormat)) { + return preferredFormat; + } else { + for (AudioFormat input : inputs) { + if (output.isCompatible(input)) { + return input; + } + } + } + } + return null; + } + + /** + * Determines the preferred voice for the currently set locale + * + * @param voices a set of voices to chose from + * @return the preferred voice for the current locale + */ + public Voice getPreferredVoice(Set voices) { + // Express preferences with a Language Priority List + Locale locale = localeProvider.getLocale(); + + // Get collection of voice locales + Collection locales = new ArrayList(); + for (Voice currentVoice : voices) { + locales.add(currentVoice.getLocale()); + } + + // TODO: This can be activated for Java 8 + // Determine preferred locale based on RFC 4647 + // String ranges = locale.toLanguageTag(); + // List languageRanges = Locale.LanguageRange.parse(ranges); + // Locale preferedLocale = Locale.lookup(languageRanges,locales); + Locale preferredLocale = locale; + + // As a last resort choose some Locale + if (null == preferredLocale) { + preferredLocale = locales.iterator().next(); + } + + // Determine preferred voice + Voice preferredVoice = null; + for (Voice currentVoice : voices) { + if (preferredLocale.equals(currentVoice.getLocale())) { + preferredVoice = currentVoice; + } + } + assert (preferredVoice != null); + + // Return preferred voice + return preferredVoice; + } + + /** + * Starts listening for the keyword that starts a dialog + * + * @throws IllegalStateException if required services are not available + */ + public void startDialog() { + startDialog(null, null, null, null, null, null, null, this.keyword); + } + + /** + * Starts listening for the keyword that starts a dialog + * + * @throws IllegalStateException if required services are not available + */ + public void startDialog(KSService ks, STTService stt, TTSService tts, HumanLanguageInterpreter hli, + AudioSource source, AudioSink sink, Locale locale, String keyword) { + + // use defaults, if null + ks = (ks == null) ? getKS() : ks; + stt = (stt == null) ? getSTT() : stt; + tts = (tts == null) ? getTTS() : tts; + hli = (hli == null) ? getHLI() : hli; + source = (source == null) ? getSource() : source; + sink = (sink == null) ? getSink() : sink; + locale = (locale == null) ? localeProvider.getLocale() : locale; + + if (ks != null && stt != null && tts != null && hli != null && source != null && sink != null) { + DialogProcessor processor = new DialogProcessor(getKS(), getSTT(), getTTS(), getHLI(), getSource(), + getSink(), localeProvider.getLocale(), keyword); + processor.start(); + } else { + String msg = "Cannot start dialog as services are missing."; + logger.error(msg); + throw new IllegalStateException(msg); + } + } + + protected void setLocaleProvider(LocaleProvider localeProvider) { + this.localeProvider = localeProvider; + } + + protected void unsetLocaleProvider(LocaleProvider localeProvider) { + this.localeProvider = null; + } + + protected void addKSService(KSService ksService) { + this.ksServices.put(ksService.getId(), ksService); + } + + protected void removeKSService(KSService ksService) { + this.ksServices.remove(ksService.getId()); + } + + protected void addSTTService(STTService sttService) { + this.sttServices.put(sttService.getId(), sttService); + } + + protected void removeSTTService(STTService sttService) { + this.sttServices.remove(sttService.getId()); + } + + protected void addTTSService(TTSService ttsService) { + this.ttsServices.put(ttsService.getId(), ttsService); + } + + protected void removeTTSService(TTSService ttsService) { + this.ttsServices.remove(ttsService.getId()); + } + + protected void addHumanLanguageInterpreter(HumanLanguageInterpreter humanLanguageInterpreter) { + this.humanLanguageInterpreters.put(humanLanguageInterpreter.getId(), humanLanguageInterpreter); + } + + protected void removeHumanLanguageInterpreter(HumanLanguageInterpreter humanLanguageInterpreter) { + this.humanLanguageInterpreters.remove(humanLanguageInterpreter.getId()); + } + + protected void addAudioSource(AudioSource audioSource) { + this.audioSources.put(audioSource.getId(), audioSource); + } + + protected void removeAudioSource(AudioSource audioSource) { + this.audioSources.remove(audioSource.getId()); + } + + protected void addAudioSink(AudioSink audioSink) { + this.audioSinks.put(audioSink.toString(), audioSink); + } + + protected void removeAudioSink(AudioSink audioSink) { + this.audioSinks.remove(audioSink.toString()); + } + + /** + * Retrieves a TTS service. + * If a default name is configured and the service available, this is returned. Otherwise, the first available + * service is returned. + * + * @return a TTS service or null, if no service is available or if a default is configured, but no according service + * is found + */ + public TTSService getTTS() { + TTSService tts = null; + if (defaultTTS != null) { + tts = ttsServices.get(defaultTTS); + if (tts == null) { + logger.warn("Default TTS service '{}' not available!", defaultTTS); + } + } else if (ttsServices.size() > 0) { + tts = ttsServices.values().iterator().next(); + } else { + logger.debug("No TTS service available!"); + } + return tts; + } + + /** + * Retrieves a STT service. + * If a default name is configured and the service available, this is returned. Otherwise, the first available + * service is returned. + * + * @return a STT service or null, if no service is available or if a default is configured, but no according service + * is found + */ + public STTService getSTT() { + STTService stt = null; + if (defaultTTS != null) { + stt = sttServices.get(defaultSTT); + if (stt == null) { + logger.warn("Default STT service '{}' not available!", defaultSTT); + } + } else if (sttServices.size() > 0) { + stt = sttServices.values().iterator().next(); + } else { + logger.debug("No STT service available!"); + } + return stt; + } + + /** + * Retrieves a KS service. + * If a default name is configured and the service available, this is returned. Otherwise, the first available + * service is returned. + * + * @return a KS service or null, if no service is available or if a default is configured, but no according service + * is found + */ + public KSService getKS() { + KSService ks = null; + if (defaultKS != null) { + ks = ksServices.get(defaultKS); + if (ks == null) { + logger.warn("Default KS service '{}' not available!", defaultKS); + } + } else if (ksServices.size() > 0) { + ks = ksServices.values().iterator().next(); + } else { + logger.debug("No KS service available!"); + } + return ks; + } + + /** + * Retrieves a HumanLanguageInterpreter. + * If a default name is configured and the service available, this is returned. Otherwise, the first available + * service is returned. + * + * @return a HumanLanguageInterpreter or null, if no service is available or if a default is configured, but no + * according service is found + */ + public HumanLanguageInterpreter getHLI() { + HumanLanguageInterpreter hli = null; + if (defaultHLI != null) { + hli = humanLanguageInterpreters.get(defaultHLI); + if (hli == null) { + logger.warn("Default HumanLanguageInterpreter '{}' not available!", defaultHLI); + } + } else if (humanLanguageInterpreters.size() > 0) { + hli = humanLanguageInterpreters.values().iterator().next(); + } else { + logger.debug("No HumanLanguageInterpreter available!"); + } + return hli; + } + + /** + * Retrieves an AudioSink. + * If a default name is configured and the service available, this is returned. Otherwise, the first available + * service is returned. + * + * @return an AudioSink or null, if no service is available or if a default is configured, but no according service + * is found + */ + public AudioSink getSink() { + AudioSink sink = null; + if (defaultSink != null) { + sink = audioSinks.get(defaultSink); + if (sink == null) { + logger.warn("Default AudioSink service '{}' not available!", defaultSink); + } + } else if (audioSinks.size() > 0) { + sink = audioSinks.values().iterator().next(); + } else { + logger.debug("No AudioSink service available!"); + } + return sink; + } + + /** + * Retrieves an AudioSource. + * If a default name is configured and the service available, this is returned. Otherwise, the first available + * service is returned. + * + * @return an AudioSource or null, if no service is available or if a default is configured, but no according + * service is found + */ + public AudioSource getSource() { + AudioSource source = null; + if (defaultSource != null) { + source = audioSources.get(defaultSource); + if (source == null) { + logger.warn("Default AudioSource service '{}' not available!", defaultSource); + } + } else if (audioSources.size() > 0) { + source = audioSources.values().iterator().next(); + } else { + logger.debug("No AudioSource service available!"); + } + return source; + } + + /** + * Returns all available voices in the system from all TTS services. + * + * @return a set of available voices + */ + public Set getAllVoices() { + Set voices = new HashSet<>(); + for (TTSService tts : ttsServices.values()) { + voices.addAll(tts.getAvailableVoices()); + } + return voices; + } + +} diff --git a/bundles/core/org.eclipse.smarthome.core.voice/src/main/java/org/eclipse/smarthome/core/voice/internal/DialogProcessor.java b/bundles/core/org.eclipse.smarthome.core.voice/src/main/java/org/eclipse/smarthome/core/voice/internal/DialogProcessor.java new file mode 100644 index 00000000000..2c05b66ff96 --- /dev/null +++ b/bundles/core/org.eclipse.smarthome.core.voice/src/main/java/org/eclipse/smarthome/core/voice/internal/DialogProcessor.java @@ -0,0 +1,164 @@ +/** + * Copyright (c) 2014-2016 by the respective copyright holders. + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v1.0 + * which accompanies this distribution, and is available at + * http://www.eclipse.org/legal/epl-v10.html + */ +package org.eclipse.smarthome.core.voice.internal; + +import java.util.HashSet; +import java.util.Locale; + +import org.eclipse.smarthome.core.audio.AudioException; +import org.eclipse.smarthome.core.audio.AudioSink; +import org.eclipse.smarthome.core.audio.AudioSource; +import org.eclipse.smarthome.core.audio.AudioStream; +import org.eclipse.smarthome.core.audio.UnsupportedAudioFormatException; +import org.eclipse.smarthome.core.voice.KSErrorEvent; +import org.eclipse.smarthome.core.voice.KSEvent; +import org.eclipse.smarthome.core.voice.KSException; +import org.eclipse.smarthome.core.voice.KSListener; +import org.eclipse.smarthome.core.voice.KSService; +import org.eclipse.smarthome.core.voice.KSpottedEvent; +import org.eclipse.smarthome.core.voice.RecognitionStopEvent; +import org.eclipse.smarthome.core.voice.STTEvent; +import org.eclipse.smarthome.core.voice.STTException; +import org.eclipse.smarthome.core.voice.STTListener; +import org.eclipse.smarthome.core.voice.STTService; +import org.eclipse.smarthome.core.voice.STTServiceHandle; +import org.eclipse.smarthome.core.voice.SpeechRecognitionErrorEvent; +import org.eclipse.smarthome.core.voice.SpeechRecognitionEvent; +import org.eclipse.smarthome.core.voice.TTSException; +import org.eclipse.smarthome.core.voice.TTSService; +import org.eclipse.smarthome.core.voice.Voice; +import org.eclipse.smarthome.core.voice.text.HumanLanguageInterpreter; +import org.eclipse.smarthome.core.voice.text.InterpretationException; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * An instance of this class can handle a complete dialog with the user. It orchestrates the keyword spotting, the stt + * and tts services together with the human language interpreter. + * + * @author Kai Kreuzer - Initial contribution and API + */ +public class DialogProcessor implements KSListener, STTListener { + + private static final Logger logger = LoggerFactory.getLogger(DialogProcessor.class); + + /** + * If the processor should spot new keywords + */ + private boolean processing = true; + + /** + * If the STT server is in the process of aborting + */ + private boolean isSTTServerAborting = false; + + private 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; + + public DialogProcessor(KSService ks, STTService stt, TTSService tts, HumanLanguageInterpreter hli, + AudioSource source, AudioSink sink, Locale locale, String keyword) { + this.locale = locale; + this.ks = ks; + this.hli = hli; + this.stt = stt; + this.tts = tts; + this.source = source; + this.sink = sink; + this.keyword = keyword; + } + + public void start() { + try { + ks.spot(this, source.getInputStream(), locale, this.keyword); + } catch (KSException | AudioException e) { + logger.error("Encountered error calling spot: {}", e.getMessage()); + } + } + + @Override + public void ksEventReceived(KSEvent ksEvent) { + if (!processing) { + processing = true; + this.isSTTServerAborting = false; + if (ksEvent instanceof KSpottedEvent) { + if (stt != null) { + try { + this.sttServiceHandle = stt.recognize(this, source.getInputStream(), this.locale, + new HashSet()); + } catch (STTException | AudioException e) { + say("Error during recognition: " + e.getMessage()); + } + } + } else if (ksEvent instanceof KSErrorEvent) { + KSErrorEvent kse = (KSErrorEvent) ksEvent; + say("Encountered error spotting keywords, " + kse.getMessage()); + } + } + } + + @Override + public synchronized void sttEventReceived(STTEvent sttEvent) { + if (sttEvent instanceof SpeechRecognitionEvent) { + if (false == this.isSTTServerAborting) { + this.sttServiceHandle.abort(); + this.isSTTServerAborting = true; + SpeechRecognitionEvent sre = (SpeechRecognitionEvent) sttEvent; + String question = sre.getTranscript(); + try { + this.processing = false; + say(hli.interpret(this.locale, question)); + } catch (InterpretationException e) { + say(e.getMessage()); + } + } + } else if (sttEvent instanceof RecognitionStopEvent) { + this.processing = false; + } else if (sttEvent instanceof SpeechRecognitionErrorEvent) { + if (false == this.isSTTServerAborting) { + this.sttServiceHandle.abort(); + this.isSTTServerAborting = true; + this.processing = false; + SpeechRecognitionErrorEvent sre = (SpeechRecognitionErrorEvent) sttEvent; + say("Encountered error: " + sre.getMessage()); + } + } + } + + /** + * Says the passed command + * + * @param text The text to say + */ + protected void say(String text) { + try { + Voice voice = null; + for (Voice currentVoice : tts.getAvailableVoices()) { + if (this.locale.getLanguage() == currentVoice.getLocale().getLanguage()) { + voice = currentVoice; + break; + } + } + if (null == voice) { + throw new TTSException("Unable to find a suitable voice"); + } + AudioStream audioStream = tts.synthesize(text, voice, null); + sink.process(audioStream); + } catch (TTSException | UnsupportedAudioFormatException e) { + logger.error("Error saying '{}'", text); + } + } + +} diff --git a/bundles/core/org.eclipse.smarthome.core.voice/src/main/java/org/eclipse/smarthome/core/voice/internal/VoiceConsoleCommandExtension.java b/bundles/core/org.eclipse.smarthome.core.voice/src/main/java/org/eclipse/smarthome/core/voice/internal/VoiceConsoleCommandExtension.java new file mode 100644 index 00000000000..8dda98b35fc --- /dev/null +++ b/bundles/core/org.eclipse.smarthome.core.voice/src/main/java/org/eclipse/smarthome/core/voice/internal/VoiceConsoleCommandExtension.java @@ -0,0 +1,144 @@ +/** + * Copyright (c) 2014-2016 by the respective copyright holders. + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v1.0 + * which accompanies this distribution, and is available at + * http://www.eclipse.org/legal/epl-v10.html + */ +package org.eclipse.smarthome.core.voice.internal; + +import java.util.Arrays; +import java.util.List; +import java.util.Locale; + +import org.apache.commons.lang.ArrayUtils; +import org.eclipse.smarthome.core.items.Item; +import org.eclipse.smarthome.core.items.ItemNotFoundException; +import org.eclipse.smarthome.core.items.ItemNotUniqueException; +import org.eclipse.smarthome.core.items.ItemRegistry; +import org.eclipse.smarthome.core.voice.Voice; +import org.eclipse.smarthome.core.voice.VoiceManager; +import org.eclipse.smarthome.core.voice.text.HumanLanguageInterpreter; +import org.eclipse.smarthome.core.voice.text.InterpretationException; +import org.eclipse.smarthome.io.console.Console; +import org.eclipse.smarthome.io.console.extensions.AbstractConsoleCommandExtension; + +/** + * Console command extension for all voice features. + * + * @author Kai Kreuzer - Initial contribution and API + * + */ +public class VoiceConsoleCommandExtension extends AbstractConsoleCommandExtension { + + private static final String SUBCMD_SAY = "say"; + private static final String SUBCMD_INTERPRET = "interpret"; + private static final String SUBCMD_VOICES = "voices"; + + private ItemRegistry itemRegistry; + private VoiceManager voiceManager; + + public VoiceConsoleCommandExtension() { + super("voice", "Commands around voice enablement features."); + } + + @Override + public List getUsages() { + return Arrays.asList(new String[] { buildCommandUsage(SUBCMD_SAY + " ", "speaks a text"), + buildCommandUsage(SUBCMD_INTERPRET + " ", "interprets a human language command"), + buildCommandUsage(SUBCMD_VOICES, "lists available voices of the active TTS service") }); + + } + + @Override + public void execute(String[] args, Console console) { + if (args.length > 0) { + String subCommand = args[0]; + switch (subCommand) { + case SUBCMD_SAY: + if (args.length > 1) { + say((String[]) ArrayUtils.subarray(args, 1, args.length), console); + } else { + console.println("Specify text to say (e.g. 'say hello')"); + } + return; + case SUBCMD_INTERPRET: + if (args.length > 1) { + interpret((String[]) ArrayUtils.subarray(args, 1, args.length), console); + } else { + console.println("Specify text to interpret (e.g. 'interpret turn all lights off')"); + } + return; + case SUBCMD_VOICES: + for (Voice voice : voiceManager.getAllVoices()) { + console.println(voice.getUID() + " " + voice.getLabel() + " (" + voice.getLocale() + ")"); + } + return; + default: + break; + } + } else { + printUsage(console); + } + } + + private void interpret(String[] args, Console console) { + HumanLanguageInterpreter interpreter = voiceManager.getHLI(); + if (interpreter != null) { + StringBuilder sb = new StringBuilder(args[0]); + for (int i = 1; i < args.length; i++) { + sb.append(" "); + sb.append(args[i]); + } + String msg = sb.toString(); + try { + console.println(interpreter.interpret(Locale.getDefault(), msg)); + } catch (InterpretationException ie) { + console.println(ie.getMessage()); + } + } else { + console.println("No human language interpreter available!"); + } + } + + private void say(String[] args, Console console) { + StringBuilder msg = new StringBuilder(); + for (String word : args) { + if (word.startsWith("%") && word.endsWith("%") && word.length() > 2) { + String itemName = word.substring(1, word.length() - 1); + try { + Item item = this.itemRegistry.getItemByPattern(itemName); + msg.append(item.getState().toString()); + } catch (ItemNotFoundException e) { + console.println("Error: Item '" + itemName + "' does not exist."); + } catch (ItemNotUniqueException e) { + console.print("Error: Multiple items match this pattern: "); + for (Item item : e.getMatchingItems()) { + console.print(item.getName() + " "); + } + } + } else { + msg.append(word); + } + msg.append(" "); + } + voiceManager.say(msg.toString()); + } + + protected void setItemRegistry(ItemRegistry itemRegistry) { + this.itemRegistry = itemRegistry; + } + + protected void unsetItemRegistry(ItemRegistry itemRegistry) { + this.itemRegistry = null; + } + + protected void setVoiceManager(VoiceManager voiceManager) { + this.voiceManager = voiceManager; + } + + protected void unsetVoiceManager(VoiceManager voiceManager) { + this.voiceManager = null; + } + +} diff --git a/bundles/core/org.eclipse.smarthome.core.voice/src/main/java/org/eclipse/smarthome/core/voice/internal/text/StandardInterpreter.java b/bundles/core/org.eclipse.smarthome.core.voice/src/main/java/org/eclipse/smarthome/core/voice/internal/text/StandardInterpreter.java new file mode 100644 index 00000000000..9ba6f752ee2 --- /dev/null +++ b/bundles/core/org.eclipse.smarthome.core.voice/src/main/java/org/eclipse/smarthome/core/voice/internal/text/StandardInterpreter.java @@ -0,0 +1,132 @@ +/** + * Copyright (c) 2014-2016 by the respective copyright holders. + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v1.0 + * which accompanies this distribution, and is available at + * http://www.eclipse.org/legal/epl-v10.html + */ +package org.eclipse.smarthome.core.voice.internal.text; + +import java.util.Locale; + +import org.eclipse.smarthome.core.library.types.HSBType; +import org.eclipse.smarthome.core.library.types.IncreaseDecreaseType; +import org.eclipse.smarthome.core.library.types.NextPreviousType; +import org.eclipse.smarthome.core.library.types.OnOffType; +import org.eclipse.smarthome.core.library.types.OpenClosedType; +import org.eclipse.smarthome.core.library.types.PlayPauseType; +import org.eclipse.smarthome.core.library.types.RewindFastforwardType; +import org.eclipse.smarthome.core.library.types.StopMoveType; +import org.eclipse.smarthome.core.library.types.UpDownType; +import org.eclipse.smarthome.core.types.RefreshType; +import org.eclipse.smarthome.core.voice.text.AbstractRuleBasedInterpreter; +import org.eclipse.smarthome.core.voice.text.Expression; + +/** + * A human language command interpretation service. + * + * @author Tilman Kamp - Initial contribution and API + * + */ +public class StandardInterpreter extends AbstractRuleBasedInterpreter { + + @Override + public void createRules() { + + /****************************** ENGLISH ******************************/ + + Expression onOff = alt(cmd("on", OnOffType.ON), cmd("off", OnOffType.OFF)); + Expression turn = alt("turn", "switch"); + Expression put = alt("put", "bring"); + Expression of = opt("of"); + Expression the = opt("the"); + Expression to = opt("to"); + Expression color = alt(cmd("white", HSBType.WHITE), cmd("pink", HSBType.fromRGB(255, 96, 208)), + cmd("yellow", HSBType.fromRGB(255, 224, 32)), cmd("orange", HSBType.fromRGB(255, 160, 16)), + cmd("purple", HSBType.fromRGB(128, 0, 128)), cmd("red", HSBType.RED), cmd("green", HSBType.GREEN), + cmd("blue", HSBType.BLUE)); + + addRules(Locale.ENGLISH, + + /* OnOffType */ + + itemRule(seq(turn, the), /* item */ onOff), + + itemRule(seq(turn, onOff) /* item */), + + /* OpenCloseType */ + + itemRule(seq(cmd("open", OpenClosedType.OPEN), the) /* item */), + + itemRule(seq(cmd("close", OpenClosedType.CLOSED), the) /* item */), + + /* IncreaseDecreaseType */ + + itemRule(seq(cmd(alt("dim", "decrease", "lower", "soften"), IncreaseDecreaseType.DECREASE), + the) /* item */), + + itemRule(seq(cmd(alt("brighten", "increase", "harden", "enhance"), IncreaseDecreaseType.INCREASE), + the) /* item */), + + /* ColorType */ + + itemRule(seq(opt("set"), the, opt("color"), of, the), /* item */ seq(to, color)), + + /* UpDownType */ + + itemRule(seq(put, the), /* item */ cmd("up", UpDownType.UP)), + + itemRule(seq(put, the), /* item */ cmd("down", UpDownType.DOWN)), + + /* NextPreviousType */ + + itemRule("move", + /* item */ seq(opt("to"), + alt(cmd("next", NextPreviousType.NEXT), cmd("previous", NextPreviousType.PREVIOUS)))), + + /* PlayPauseType */ + + itemRule(seq(cmd("play", PlayPauseType.PLAY), the) /* item */), + + itemRule(seq(cmd("pause", PlayPauseType.PAUSE), the) /* item */), + + /* RewindFastForwardType */ + + itemRule(seq(cmd("rewind", RewindFastforwardType.REWIND), the) /* item */), + + itemRule(seq(cmd(seq(opt("fast"), "forward"), RewindFastforwardType.FASTFORWARD), the) /* item */), + + /* StopMoveType */ + + itemRule(seq(cmd("stop", StopMoveType.STOP), the) /* item */), + + itemRule(seq(cmd(alt("start", "move", "continue"), StopMoveType.MOVE), the) /* item */), + + /* RefreshType */ + + itemRule(seq(cmd("refresh", RefreshType.REFRESH), the) /* item */) + + ); + + /****************************** GERMAN ******************************/ + + Expression einAnAus = alt(cmd("ein", OnOffType.ON), cmd("an", OnOffType.ON), cmd("aus", OnOffType.OFF)); + Expression denDieDas = opt(alt("den", "die", "das")); + + addRules(Locale.GERMAN, itemRule(seq(alt("schalt", "schalte", "mach"), denDieDas), /* item */ einAnAus), + itemRule(seq(cmd("öffne", OpenClosedType.OPEN), denDieDas) /* item */), + itemRule(seq(cmd(alt("schließ", "schließe"), OpenClosedType.OPEN), denDieDas) /* item */)); + + } + + @Override + public String getId() { + return "system"; + } + + @Override + public String getLabel(Locale locale) { + return "Built-in Interpreter"; + } + +} diff --git a/bundles/core/org.eclipse.smarthome.core.voice/src/main/java/org/eclipse/smarthome/core/voice/text/ASTNode.java b/bundles/core/org.eclipse.smarthome.core.voice/src/main/java/org/eclipse/smarthome/core/voice/text/ASTNode.java new file mode 100644 index 00000000000..a7ad3fd895f --- /dev/null +++ b/bundles/core/org.eclipse.smarthome.core.voice/src/main/java/org/eclipse/smarthome/core/voice/text/ASTNode.java @@ -0,0 +1,213 @@ +/** + * Copyright (c) 2014-2016 by the respective copyright holders. + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v1.0 + * which accompanies this distribution, and is available at + * http://www.eclipse.org/legal/epl-v10.html + */ +package org.eclipse.smarthome.core.voice.text; + +/** + * Abstract syntax tree node. Result of parsing an expression. + * + * @author Tilman Kamp - Initial contribution and API + * + */ +public class ASTNode { + + private boolean success = false; + private ASTNode[] children; + private TokenList remainingTokens; + + private String name; + private Object value; + private Object tag; + + public ASTNode() { + } + + /** + * Constructs a new AST node. + * + * @param children the node's children + * @param remainingTokens remaining token list starting with the first token that was not covered/consumed + */ + public ASTNode(ASTNode[] children, TokenList remainingTokens) { + this.success = true; + this.children = children; + this.remainingTokens = remainingTokens; + } + + /** + * Breadth searching this (sub-) tree/node for a node with the given name. + * + * @param name the name that's used for looking up the tree + * @return first node with the given name or null, if none was found + */ + public ASTNode findNode(String name) { + if (this.name != null && this.name.equals(name)) { + return this; + } + ASTNode n; + for (ASTNode sn : children) { + n = sn.findNode(name); + if (n != null) { + return n; + } + } + return null; + } + + /** + * @return the value of this node as {@link String[]} + */ + public String[] getValueAsStringArray() { + Object[] objs = value instanceof Object[] ? (Object[]) value : new Object[] { + value + }; + String[] result = new String[objs.length]; + for (int i = 0; i < objs.length; i++) { + result[i] = objs[i] == null ? "" : ("" + objs[i]); + } + return result; + } + + /** + * @return the value of this node as {@link String}. + */ + public String getValueAsString() { + return value == null ? "" : ("" + value); + } + + /** + * Breadth searches this (sub-) tree/node for a node with the given name and returning its value as a + * {@link String[]}. + * + * @param name the name of the named node to be found + * @return the value of the resulting node as {@link String[]} or null if not found + */ + public String[] findValueAsStringArray(String name) { + ASTNode node = findNode(name); + return node == null ? null : node.getValueAsStringArray(); + } + + /** + * Breadth searches this (sub-) tree/node for a node with the given name and returning its value as a {@link String} + * . + * + * @param name the name of the named node to be found + * @return the value of the resulting node as {@link String} or null if not found + */ + public String findValueAsString(String name) { + ASTNode node = findNode(name); + return node == null ? null : node.getValueAsString(); + } + + /** + * Breadth searches this (sub-) tree/node for a node with the given name and type and returning its value. + * + * @param name the name of the named node to be found + * @param cls the node's value has to be assignable to a reference of this class to match during search + * @return the value of the resulting node. Null, if not found or the value does not match {@link cls}. + */ + public Object findValue(String name, Class cls) { + ASTNode node = findNode(name); + return node == null ? null + : ((node.value != null && cls.isAssignableFrom(node.value.getClass())) ? node.value : null); + } + + /** + * Breadth searches this (sub-) tree/node for a node with the given name and returning its value. + * + * @param name the name of the named node to be found + * @return the value of the resulting node. Null, if not found. + */ + public Object findValue(String name) { + ASTNode node = findNode(name); + return node == null ? null : node.value; + } + + /** + * @return if the node is a valid one (true) or parsing was not successful (false) + */ + public boolean isSuccess() { + return success; + } + + /** + * @param success if the node is a valid one (true) or parsing was not successful (false) + */ + public void setSuccess(boolean success) { + this.success = success; + } + + /** + * @return the children + */ + public ASTNode[] getChildren() { + return children; + } + + /** + * @param children the children to set + */ + public void setChildren(ASTNode[] children) { + this.children = children; + } + + /** + * @return the remainingTokens + */ + public TokenList getRemainingTokens() { + return remainingTokens; + } + + /** + * @param remainingTokens the remainingTokens to set + */ + public void setRemainingTokens(TokenList remainingTokens) { + this.remainingTokens = remainingTokens; + } + + /** + * @return the name + */ + public String getName() { + return name; + } + + /** + * @param name the name to set + */ + public void setName(String name) { + this.name = name; + } + + /** + * @return the value + */ + public Object getValue() { + return value; + } + + /** + * @param value the value to set + */ + public void setValue(Object value) { + this.value = value; + } + + /** + * @return the tag + */ + public Object getTag() { + return tag; + } + + /** + * @param tag the tag to set + */ + public void setTag(Object tag) { + this.tag = tag; + } +} \ No newline at end of file diff --git a/bundles/core/org.eclipse.smarthome.core.voice/src/main/java/org/eclipse/smarthome/core/voice/text/AbstractRuleBasedInterpreter.java b/bundles/core/org.eclipse.smarthome.core.voice/src/main/java/org/eclipse/smarthome/core/voice/text/AbstractRuleBasedInterpreter.java new file mode 100644 index 00000000000..042d395dff4 --- /dev/null +++ b/bundles/core/org.eclipse.smarthome.core.voice/src/main/java/org/eclipse/smarthome/core/voice/text/AbstractRuleBasedInterpreter.java @@ -0,0 +1,842 @@ +/** + * Copyright (c) 2014-2016 by the respective copyright holders. + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v1.0 + * which accompanies this distribution, and is available at + * http://www.eclipse.org/legal/epl-v10.html + */ +package org.eclipse.smarthome.core.voice.text; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.HashMap; +import java.util.HashSet; +import java.util.List; +import java.util.Locale; +import java.util.ResourceBundle; +import java.util.Set; + +import org.eclipse.smarthome.core.common.registry.RegistryChangeListener; +import org.eclipse.smarthome.core.events.EventPublisher; +import org.eclipse.smarthome.core.items.GroupItem; +import org.eclipse.smarthome.core.items.Item; +import org.eclipse.smarthome.core.items.ItemRegistry; +import org.eclipse.smarthome.core.items.events.ItemEventFactory; +import org.eclipse.smarthome.core.library.types.DecimalType; +import org.eclipse.smarthome.core.library.types.StringType; +import org.eclipse.smarthome.core.types.Command; +import org.eclipse.smarthome.core.types.State; + +/** + * A human language command interpretation service. + * + * @author Tilman Kamp - Initial contribution and API + * + */ +public abstract class AbstractRuleBasedInterpreter implements HumanLanguageInterpreter { + + private static final String JSGF = "JSGF"; + private static final Set supportedGrammars = Collections.unmodifiableSet(Collections.singleton(JSGF)); + + private static final String OK = "ok"; + private static final String SORRY = "sorry"; + + private static final String CMD = "cmd"; + private static final String NAME = "name"; + + private static final String LANGUAGE_SUPPORT = "LanguageSupport"; + + private HashMap> languageRules; + private HashMap> allItemTokens = null; + private HashMap>>> itemTokens = null; + + private ItemRegistry itemRegistry; + private EventPublisher eventPublisher; + + private RegistryChangeListener registryChangeListener = new RegistryChangeListener() { + @Override + public void added(Item element) { + invalidate(); + } + + @Override + public void removed(Item element) { + invalidate(); + } + + @Override + public void updated(Item oldElement, Item element) { + invalidate(); + } + }; + + /** + * Called whenever the rules are to be (re)generated and added by {@link addRules} + */ + protected abstract void createRules(); + + @Override + public String interpret(Locale locale, String text) throws InterpretationException { + ResourceBundle language = ResourceBundle.getBundle(LANGUAGE_SUPPORT, locale); + Rule[] rules = getRules(locale); + if (language == null || rules.length == 0) { + throw new InterpretationException( + locale.getDisplayLanguage(Locale.ENGLISH) + " is not supported at the moment."); + } + TokenList tokens = new TokenList(tokenize(locale, text)); + if (tokens.eof()) { + throw new InterpretationException(language.getString(SORRY)); + } + InterpretationResult result; + + InterpretationResult lastResult = null; + for (Rule rule : rules) { + if ((result = rule.execute(language, tokens)).isSuccess()) { + return result.getResponse(); + } else { + if (result != InterpretationResult.SYNTAX_ERROR) { + lastResult = result; + } + } + } + if (lastResult == null) { + throw new InterpretationException(language.getString(SORRY)); + } else { + throw lastResult.getException(); + } + } + + private void invalidate() { + allItemTokens = null; + itemTokens = null; + languageRules = null; + } + + /** + * All the tokens (name parts) of the names of all the items in the {@link ItemRegistry}. + * + * @param locale The locale that is to be used for preparing the tokens. + * @return the identifier tokens + */ + HashSet getAllItemTokens(Locale locale) { + if (allItemTokens == null) { + allItemTokens = new HashMap>(); + } + HashSet localeTokens = allItemTokens.get(locale); + if (localeTokens == null) { + allItemTokens.put(locale, localeTokens = new HashSet()); + for (Item item : itemRegistry.getAll()) { + localeTokens.addAll(tokenize(locale, item.getLabel())); + } + } + return localeTokens; + } + + /** + * Retrieves the list of identifier token sets per item currently contained in the {@link ItemRegistry}. + * Each item entry in the resulting hash map will feature a list of different token sets. Each token set + * represents one possible way "through" a chain of parent groups, where each groups tokenized name is + * part of the set. + * + * @param locale The locale that is to be used for preparing the tokens. + * @return the list of identifier token sets per item + */ + HashMap>> getItemTokens(Locale locale) { + if (itemTokens == null) { + itemTokens = new HashMap>>>(); + } + HashMap>> localeTokens = itemTokens.get(locale); + if (localeTokens == null) { + itemTokens.put(locale, localeTokens = new HashMap>>()); + for (Item item : itemRegistry.getItems()) { + if (item.getGroupNames().isEmpty()) { + addItem(locale, localeTokens, new HashSet(), item); + } + } + } + return localeTokens; + } + + private void addItem(Locale locale, HashMap>> target, HashSet tokens, + Item item) { + HashSet nt = new HashSet(tokens); + nt.addAll(tokenize(locale, item.getLabel())); + ArrayList> list = target.get(item); + if (list == null) { + target.put(item, list = new ArrayList>()); + } + list.add(nt); + if (item instanceof GroupItem) { + for (Item member : ((GroupItem) item).getMembers()) { + addItem(locale, target, nt, member); + } + } + } + + /** + * Creates an item name placeholder expression. This expression is greedy: Only use it, if there are no other + * expressions following this one. + * It's safer to use {@link thingRule} instead. + * + * @return Expression that represents a name of an item. + */ + protected Expression name() { + return name(null); + } + + /** + * Creates an item name placeholder expression. This expression is greedy: Only use it, if you are able to pass in + * all possible stop tokens as excludes. + * It's safer to use {@link thingRule} instead. + * + * @param stopper Stop expression that, if matching, will stop this expression from consuming further tokens. + * @return Expression that represents a name of an item. + */ + protected Expression name(Expression stopper) { + return tag(NAME, star(new ExpressionIdentifier(this, stopper))); + } + + private HashMap> getLanguageRules() { + if (languageRules == null) { + languageRules = new HashMap>(); + createRules(); + } + return languageRules; + } + + /** + * Retrieves all {@link Rule}s to a given {@link Locale}. It also retrieves all the same-language rules into greater + * indexes of the array (lower match priority). + * + * @param locale Locale filter + * @return Rules in descending match priority order. + */ + public Rule[] getRules(Locale locale) { + HashMap> lr = getLanguageRules(); + ArrayList rules = new ArrayList(); + HashSet> ruleSets = new HashSet>(); + ArrayList ruleSet = lr.get(locale); + if (ruleSet != null) { + ruleSets.add(ruleSet); + rules.addAll(ruleSet); + } + + String l = locale.getLanguage(); + for (Locale rl : lr.keySet()) { + if (rl.getLanguage().equals(l)) { + ruleSet = lr.get(rl); + if (!ruleSets.contains(ruleSet)) { + ruleSets.add(ruleSet); + rules.addAll(ruleSet); + } + } + } + return rules.toArray(new Rule[0]); + } + + /** + * Adds {@link Locale} specific rules to this interpreter. To be called from within {@link createRules}. + * + * @param locale Locale of the rules. + * @param rules Rules to add. + */ + protected void addRules(Locale locale, Rule... rules) { + ArrayList ruleSet = languageRules.get(locale); + if (ruleSet == null) { + languageRules.put(locale, ruleSet = new ArrayList()); + } + for (Rule rule : rules) { + ruleSet.add(rule); + } + } + + /** + * Creates an item rule on base of an expression, where the tail of the new rule's expression will consist of an + * item + * name expression. + * + * @param headExpression The head expression that should contain at least one {@link cmd} generated expression. The + * corresponding {@link Command} will in case of a match be sent to the matching {@link Item}. + * @return The created rule. + */ + protected Rule itemRule(Object headExpression) { + return itemRule(headExpression, null); + } + + /** + * Creates an item rule on base of a head and a tail expression, where the middle part of the new rule's expression + * will consist of an item + * name expression. Either the head expression or the tail expression should contain at least one {@link cmd} + * generated expression. + * + * @param headExpression The head expression. + * @param tailExpression The tail expression. + * @return The created rule. + */ + protected Rule itemRule(Object headExpression, Object tailExpression) { + Expression tail = exp(tailExpression); + Expression expression = tail == null ? seq(headExpression, name()) : seq(headExpression, name(tail), tail); + return new Rule(expression) { + @Override + public InterpretationResult interpretAST(ResourceBundle language, ASTNode node) { + String[] name = node.findValueAsStringArray(NAME); + ASTNode cmdNode = node.findNode(CMD); + Object tag = cmdNode.getTag(); + Object value = cmdNode.getValue(); + Command command; + if (tag instanceof Command) { + command = (Command) tag; + } else if (value instanceof Number) { + command = new DecimalType(((Number) value).longValue()); + } else { + command = new StringType(cmdNode.getValueAsString()); + } + if (name != null && command != null) { + try { + return new InterpretationResult(true, executeSingle(language, name, command)); + } catch (InterpretationException ex) { + return new InterpretationResult(ex); + } + } + return InterpretationResult.SEMANTIC_ERROR; + } + }; + + } + + /** + * Converts an object to an expression. Objects that are already instances of {@link Expression} are just returned. + * All others are converted to {@link match} expressions. + * + * @param obj the object that's to be converted + * @return resulting expression + */ + protected Expression exp(Object obj) { + if (obj instanceof Expression) { + return (Expression) obj; + } else { + return obj == null ? null : new ExpressionMatch(obj.toString()); + } + } + + /** + * Converts all parameters to an expression array. Objects that are already instances of {@link Expression} are not + * touched. + * All others are converted to {@link match} expressions. + * + * @param obj the objects that are to be converted + * @return resulting expression array + */ + protected Expression[] exps(Object... objects) { + ArrayList result = new ArrayList(); + for (int i = 0; i < objects.length; i++) { + Expression e = exp(objects[i]); + if (e != null) { + result.add(e); + } + } + return result.toArray(new Expression[0]); + } + + /** + * Adds a name to the resulting AST tree, if the given expression matches. + * + * @param name name to add + * @param expression the expression that has to match + * @return resulting expression + */ + protected Expression tag(String name, Object expression) { + return tag(name, expression, null); + } + + /** + * Adds a value to the resulting AST tree, if the given expression matches. + * + * @param expression the expression that has to match + * @param tag the tag that's to be set + * @return resulting expression + */ + protected Expression tag(Object expression, Object tag) { + return tag(null, expression, tag); + } + + /** + * Adds a name and a tag to the resulting AST tree, if the given expression matches. + * + * @param name name to add + * @param expression the expression that has to match + * @param tag the tag that's to be set + * @return resulting expression + */ + protected Expression tag(String name, Object expression, Object tag) { + return new ExpressionLet(name, exp(expression), null, tag); + } + + /** + * Adds a command to the resulting AST tree. If the expression evaluates to a + * numeric value, it will get a {@link DecimalType}, otherwise a {@link StringType}. + * + * @param expression the expression that has to match + * @return resulting expression + */ + protected Expression cmd(Object expression) { + return cmd(expression, null); + } + + /** + * Adds a command to the resulting AST tree, if the expression matches. + * + * @param expression the expression that has to match + * @param command the command that should be added + * @return resulting expression + */ + protected Expression cmd(Object expression, Command command) { + return tag(CMD, expression, command); + } + + /** + * Creates an alternatives expression. Matches, as soon as one of the given expressions matches. They are tested in + * the provided order. The value of the matching expression will be used for the resulting nodes's value. + * + * @param expressions the expressions (alternatives) that are to be tested + * @return resulting expression + */ + protected ExpressionAlternatives alt(Object... expressions) { + return new ExpressionAlternatives(exps(expressions)); + } + + /** + * Creates a sequence expression. Matches, if all the given expressions match. They are tested in + * the provided order. The resulting nodes's value will be an {@link Object[]} that contains all values of the + * matching expressions. + * + * @param expressions the expressions (alternatives) that have to match in sequence + * @return resulting expression + */ + protected ExpressionSequence seq(Object... expressions) { + return new ExpressionSequence(exps(expressions)); + } + + /** + * Creates an optional expression. Always succeeds. The resulting nodes's value will be the one of the + * matching expression or null. + * + * @param expression the optionally matching expression + * @return resulting expression + */ + protected ExpressionCardinality opt(Object expression) { + return new ExpressionCardinality(exp(expression), false, true); + } + + /** + * Creates a repeating expression that will match the given expression as often as possible. Always succeeds. The + * resulting node's value will be an {@link Object[]} that contains all values of the + * matches. + * + * @param expression the repeating expression + * @return resulting expression + */ + protected ExpressionCardinality star(Object expression) { + return new ExpressionCardinality(exp(expression), false, false); + } + + /** + * Creates a repeating expression that will match the given expression as often as possible. Only succeeds, if there + * is at least one match. The resulting node's value will be an {@link Object[]} that contains all values of the + * matches. + * + * @param expression the repeating expression + * @return resulting expression + */ + protected ExpressionCardinality plus(Object expression) { + return new ExpressionCardinality(exp(expression), true, false); + } + + /** + * Executes a command on one item that's to be found in the item registry by given name fragments. + * Fails, if there is more than on item. + * + * @param language resource bundle used for producing localized response texts + * @param labelFragments label fragments that are used to match an item's label. + * For a positive match, the item's label has to contain every fragment - independently of their order. + * They are treated case insensitive. + * @param command command that should be executed + * @return response text + * @throws InterpretationException in case that there is no or more than on item matching the fragments + */ + protected String executeSingle(ResourceBundle language, String[] labelFragments, Command command) + throws InterpretationException { + ArrayList items = getMatchingItems(language, labelFragments, command.getClass()); + if (items.size() < 1) { + throw new InterpretationException(language.getString("no_objects")); + } else if (items.size() > 1) { + throw new InterpretationException(language.getString("multiple_objects")); + } else { + Item item = items.get(0); + if (command instanceof State) { + try { + State newState = (State) command; + State oldState = item.getStateAs(newState.getClass()); + if (oldState.equals(newState)) { + String template = language.getString("state_already_singular"); + String cmdName = "state_" + command.toString().toLowerCase(); + String stateText = language.getString(cmdName); + return template.replace("", stateText); + } + } catch (Exception ex) { + } + } + eventPublisher.post(ItemEventFactory.createCommandEvent(item.getName(), command)); + return language.getString(OK); + } + } + + /** + * Filters the item registry by matching each item's name with the provided name fragments. + * The item's label and its parent group's labels are tokenizend {@link tokenize} and then altogether looked up + * by each and every provided fragment. + * For the item to get included into the result list, every provided fragment has to be found among the label + * tokens. + * If a command type is provided, the item also has to support it. + * In case of channels and their owners being ambiguous due to sharing most of the label sequence, only the top + * most item with support for the + * given command type is kept. + * + * @param language Language information that is used for matching + * @param labelFragments label fragments that are used to match an item's label. + * For a positive match, the item's label has to contain every fragment - independently of their order. + * They are treated case insensitive. + * @param commandType optional command type that all items have to support. + * Provide {null} if there is no need for a certain command to be supported. + * @return All matching items from the item registry. + */ + protected ArrayList getMatchingItems(ResourceBundle language, String[] labelFragments, Class commandType) { + ArrayList items = new ArrayList(); + HashMap>> map = getItemTokens(language.getLocale()); + for (Item item : map.keySet()) { + for (HashSet parts : map.get(item)) { + boolean allMatch = true; + for (String fragment : labelFragments) { + if (!parts.contains(fragment.toLowerCase(language.getLocale()))) { + allMatch = false; + break; + } + } + if (allMatch) { + if (commandType == null || item.getAcceptedCommandTypes().contains(commandType)) { + String name = item.getName(); + boolean insert = true; + for (Item si : items) { + if (name.startsWith(si.getName())) { + insert = false; + } + } + if (insert) { + for (int i = 0; i < items.size(); i++) { + Item si = items.get(i); + if (si.getName().startsWith(name)) { + items.remove(i); + i--; + } + } + items.add(item); + } + } + } + } + } + return items; + } + + /** + * Tokenizes text. Filters out all unsupported punctuation. Tokens will be lower case. + * + * @param locale the locale that should be used for lower casing + * @param text the text that should be tokenized + * @return resulting tokens + */ + protected ArrayList tokenize(Locale locale, String text) { + ArrayList parts = new ArrayList(); + if (text == null) { + return parts; + } + String[] split = text.toLowerCase(locale).replaceAll("[\\']", "").replaceAll("[^\\w\\s]", " ").split("\\s"); + for (int i = 0; i < split.length; i++) { + String part = split[i].trim(); + if (part.length() > 0) { + parts.add(part); + } + } + return parts; + } + + @Override + public Set getSupportedLocales() { + return Collections.unmodifiableSet(getLanguageRules().keySet()); + } + + @Override + public Set getSupportedGrammarFormats() { + return supportedGrammars; + } + + /** + * Helper class to generate a JSGF grammar from the rules of the interpreter. + * + * @author Tilman Kamp - Initial contribution and API + * + */ + private class JSGFGenerator { + + private ResourceBundle language; + + private HashMap ids = new HashMap(); + private HashSet exported = new HashSet(); + private HashSet shared = new HashSet(); + private int counter = 0; + + private HashSet identifierExcludes = new HashSet(); + private HashSet identifiers = new HashSet(); + + private StringBuilder builder = new StringBuilder(); + + JSGFGenerator(ResourceBundle language) { + this.language = language; + } + + private void addChildren(Expression exp) { + for (Expression se : exp.getChildExpressions()) { + addExpression(se); + } + } + + private int addExpression(Expression exp) { + if (ids.containsKey(exp)) { + shared.add(exp); + return ids.get(exp); + } else { + int id = counter++; + ids.put(exp, id); + addChildren(exp); + return id; + } + } + + private int addExportedExpression(Expression exp) { + shared.add(exp); + exported.add(exp); + int id = addExpression(exp); + return id; + } + + private Expression unwrapLet(Expression expression) { + while (expression instanceof ExpressionLet) { + expression = ((ExpressionLet) expression).getSubExpression(); + } + return expression; + } + + private void emit(Object obj) { + builder.append(obj); + } + + private void emitName(Expression expression) { + emit("r"); + emit(ids.get(unwrapLet(expression))); + } + + private void emitReference(Expression expression) { + emit("<"); + emitName(expression); + emit(">"); + } + + private void emitDefinition(Expression expression) { + if (exported.contains(expression)) { + emit("public "); + } + emit("<"); + emitName(expression); + emit("> = "); + emitExpression(expression); + emit(";\n\n"); + } + + private void emitUse(Expression expression) { + if (shared.contains(expression)) { + emitReference(expression); + } else { + emitExpression(expression); + } + } + + private void emitExpression(Expression expression) { + expression = unwrapLet(expression); + if (expression instanceof ExpressionMatch) { + emitMatchExpression((ExpressionMatch) expression); + } else if (expression instanceof ExpressionSequence) { + emitSequenceExpression((ExpressionSequence) expression); + } else if (expression instanceof ExpressionAlternatives) { + emitAlternativesExpression((ExpressionAlternatives) expression); + } else if (expression instanceof ExpressionCardinality) { + emitCardinalExpression((ExpressionCardinality) expression); + } else if (expression instanceof ExpressionIdentifier) { + emitItemIdentifierExpression((ExpressionIdentifier) expression); + } + } + + private void emitMatchExpression(ExpressionMatch expression) { + emit(expression.getPattern()); + } + + private void emitSequenceExpression(ExpressionSequence expression) { + emitGroup(" ", expression.getChildExpressions()); + } + + private void emitAlternativesExpression(ExpressionAlternatives expression) { + emitGroup(" | ", expression.getChildExpressions()); + } + + private void emitCardinalExpression(ExpressionCardinality expression) { + if (!expression.isAtLeastOne() && !expression.isAtMostOne()) { + emitUse(expression.getSubExpression()); + emit("*"); + } else if (expression.isAtLeastOne()) { + emitUse(expression.getSubExpression()); + emit("+"); + } else if (expression.isAtMostOne()) { + emit("["); + emitUse(expression.getSubExpression()); + emit("]"); + } else { + emitUse(expression.getSubExpression()); + } + } + + private void emitItemIdentifierExpression(ExpressionIdentifier expression) { + HashSet remainder = new HashSet(identifierExcludes); + Expression stopper = expression.getStopper(); + HashSet excludes = stopper == null ? new HashSet() : stopper.getFirsts(language); + if (excludes.size() > 0) { + remainder.removeAll(excludes); + if (remainder.size() > 0) { + emit("("); + } + emit(""); + for (String token : remainder) { + emit(" | "); + emit(token); + } + if (remainder.size() > 0) { + emit(")"); + } + } else { + emit(""); + } + } + + private void emitGroup(String separator, List expressions) { + int l = expressions.size(); + if (l > 0) { + emit("("); + } + for (int i = 0; i < l; i++) { + if (i > 0) { + emit(separator); + } + emitUse(expressions.get(i)); + } + if (l > 0) { + emit(")"); + } + } + + private void emitSet(HashSet set, String separator) { + boolean sep = false; + for (String p : set) { + if (sep) { + emit(separator); + } else { + sep = true; + } + emit(p); + } + } + + String getGrammar() { + Rule[] rules = getRules(language.getLocale()); + identifiers.addAll(getAllItemTokens(language.getLocale())); + for (Rule rule : rules) { + Expression e = rule.getExpression(); + addExportedExpression(e); + } + for (Expression e : ids.keySet()) { + if (e instanceof ExpressionIdentifier) { + Expression stopper = ((ExpressionIdentifier) e).getStopper(); + if (stopper != null) { + identifierExcludes.addAll(stopper.getFirsts(language)); + } + } + } + + emit("#JSGF V1.0;\n\n"); + + if (identifierExcludes.size() > 0) { + HashSet identifierBase = new HashSet(identifiers); + identifierBase.removeAll(identifierExcludes); + emit(" = "); + emitSet(identifierBase, " | "); + emit(";\n\n = | "); + emitSet(identifierExcludes, " | "); + emit(";\n\n"); + } else { + emit(" = "); + emitSet(identifiers, " | "); + emit(";\n\n"); + } + + for (Expression e : shared) { + emitDefinition(e); + } + String grammar = builder.toString(); + return grammar; + } + + } + + @Override + public String getGrammar(Locale locale, String format) { + if (format != JSGF) { + return null; + } + JSGFGenerator generator = new JSGFGenerator(ResourceBundle.getBundle(LANGUAGE_SUPPORT, locale)); + return generator.getGrammar(); + } + + public void setItemRegistry(ItemRegistry itemRegistry) { + if (this.itemRegistry == null) { + this.itemRegistry = itemRegistry; + this.itemRegistry.addRegistryChangeListener(registryChangeListener); + } + } + + public void unsetItemRegistry(ItemRegistry itemRegistry) { + if (itemRegistry == this.itemRegistry) { + this.itemRegistry.removeRegistryChangeListener(registryChangeListener); + this.itemRegistry = null; + } + } + + public void setEventPublisher(EventPublisher eventPublisher) { + if (this.eventPublisher == null) { + this.eventPublisher = eventPublisher; + } + } + + public void unsetEventPublisher(EventPublisher eventPublisher) { + if (eventPublisher == this.eventPublisher) { + this.eventPublisher = null; + } + } + +} diff --git a/bundles/core/org.eclipse.smarthome.core.voice/src/main/java/org/eclipse/smarthome/core/voice/text/Expression.java b/bundles/core/org.eclipse.smarthome.core.voice/src/main/java/org/eclipse/smarthome/core/voice/text/Expression.java new file mode 100644 index 00000000000..e5408de0751 --- /dev/null +++ b/bundles/core/org.eclipse.smarthome.core.voice/src/main/java/org/eclipse/smarthome/core/voice/text/Expression.java @@ -0,0 +1,42 @@ +/** + * Copyright (c) 2014-2016 by the respective copyright holders. + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v1.0 + * which accompanies this distribution, and is available at + * http://www.eclipse.org/legal/epl-v10.html + */ +package org.eclipse.smarthome.core.voice.text; + +import java.util.Collections; +import java.util.HashSet; +import java.util.List; +import java.util.ResourceBundle; + +/** + * Base class for all expressions. + * + * @author Tilman Kamp - Initial contribution and API + * + */ +public abstract class Expression { + + Expression() { + } + + abstract ASTNode parse(ResourceBundle language, TokenList list); + + void generateValue(ASTNode node) { + } + + List getChildExpressions() { + return Collections.emptyList(); + } + + abstract boolean collectFirsts(ResourceBundle language, HashSet firsts); + + HashSet getFirsts(ResourceBundle language) { + HashSet firsts = new HashSet(); + collectFirsts(language, firsts); + return firsts; + } +} \ No newline at end of file diff --git a/bundles/core/org.eclipse.smarthome.core.voice/src/main/java/org/eclipse/smarthome/core/voice/text/ExpressionAlternatives.java b/bundles/core/org.eclipse.smarthome.core.voice/src/main/java/org/eclipse/smarthome/core/voice/text/ExpressionAlternatives.java new file mode 100644 index 00000000000..17348fda874 --- /dev/null +++ b/bundles/core/org.eclipse.smarthome.core.voice/src/main/java/org/eclipse/smarthome/core/voice/text/ExpressionAlternatives.java @@ -0,0 +1,76 @@ +/** + * Copyright (c) 2014-2016 by the respective copyright holders. + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v1.0 + * which accompanies this distribution, and is available at + * http://www.eclipse.org/legal/epl-v10.html + */ +package org.eclipse.smarthome.core.voice.text; + +import java.util.Arrays; +import java.util.Collections; +import java.util.HashSet; +import java.util.List; +import java.util.ResourceBundle; + +/** + * Expression that successfully parses, if one of the given alternative expressions matches. This class is immutable. + * + * @author Tilman Kamp - Initial contribution and API + * + */ +final class ExpressionAlternatives extends Expression { + + private List subExpressions; + + /** + * Constructs a new instance. + * + * @param subExpressions the sub expressions that are tried/parsed as alternatives in the given order + */ + public ExpressionAlternatives(Expression... subExpressions) { + super(); + this.subExpressions = Collections + .unmodifiableList(Arrays.asList(Arrays.copyOf(subExpressions, subExpressions.length))); + } + + @Override + ASTNode parse(ResourceBundle language, TokenList list) { + ASTNode node = new ASTNode(), cr; + for (int i = 0; i < subExpressions.size(); i++) { + cr = subExpressions.get(i).parse(language, list); + if (cr.isSuccess()) { + node.setChildren(new ASTNode[] { cr }); + node.setRemainingTokens(cr.getRemainingTokens()); + node.setSuccess(true); + node.setValue(cr.getValue()); + generateValue(node); + return node; + } + } + return node; + } + + @Override + List getChildExpressions() { + return subExpressions; + } + + @Override + boolean collectFirsts(ResourceBundle language, HashSet firsts) { + boolean blocking = true; + for (Expression e : subExpressions) { + blocking = blocking && e.collectFirsts(language, firsts); + } + return blocking; + } + + @Override + public String toString() { + String s = null; + for (Expression e : subExpressions) { + s = s == null ? e.toString() : (s + ", " + e.toString()); + } + return "alt(" + s + ")"; + } +} \ No newline at end of file diff --git a/bundles/core/org.eclipse.smarthome.core.voice/src/main/java/org/eclipse/smarthome/core/voice/text/ExpressionCardinality.java b/bundles/core/org.eclipse.smarthome.core.voice/src/main/java/org/eclipse/smarthome/core/voice/text/ExpressionCardinality.java new file mode 100644 index 00000000000..ce19ab0d86c --- /dev/null +++ b/bundles/core/org.eclipse.smarthome.core.voice/src/main/java/org/eclipse/smarthome/core/voice/text/ExpressionCardinality.java @@ -0,0 +1,101 @@ +/** + * Copyright (c) 2014-2016 by the respective copyright holders. + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v1.0 + * which accompanies this distribution, and is available at + * http://www.eclipse.org/legal/epl-v10.html + */ +package org.eclipse.smarthome.core.voice.text; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.HashSet; +import java.util.List; +import java.util.ResourceBundle; + +/** + * Expression that successfully parses, if a given expression occurs or repeats with a specified cardinality. This class + * is immutable. + * + * @author Tilman Kamp - Initial contribution and API + * + */ +public final class ExpressionCardinality extends Expression { + + private Expression subExpression; + private boolean atLeastOne = false; + private boolean atMostOne = true; + + /** + * Constructs a new instance. + * + * @param subExpression expression that could occur or repeat + * @param atLeastOne true, if expression should occur at least one time + * @param atMostOne true, if expression should occur at most one time + */ + public ExpressionCardinality(Expression subExpression, boolean atLeastOne, boolean atMostOne) { + this.subExpression = subExpression; + this.atLeastOne = atLeastOne; + this.atMostOne = atMostOne; + } + + @Override + ASTNode parse(ResourceBundle language, TokenList list) { + ASTNode node = new ASTNode(), cr; + ArrayList nodes = new ArrayList(); + ArrayList values = new ArrayList(); + while ((cr = subExpression.parse(language, list)).isSuccess()) { + nodes.add(cr); + values.add(cr.getValue()); + list = cr.getRemainingTokens(); + if (atMostOne) { + break; + } + } + if (!(atLeastOne && nodes.size() == 0)) { + node.setChildren(nodes.toArray(new ASTNode[0])); + node.setRemainingTokens(list); + node.setSuccess(true); + node.setValue(atMostOne ? (values.size() > 0 ? values.get(0) : null) : values.toArray()); + generateValue(node); + } + return node; + } + + @Override + List getChildExpressions() { + return Collections.unmodifiableList(Arrays.asList(subExpression)); + } + + @Override + boolean collectFirsts(ResourceBundle language, HashSet firsts) { + return subExpression.collectFirsts(language, firsts) || atLeastOne; + } + + @Override + public String toString() { + return "cardinal(" + atLeastOne + ", " + atMostOne + "' " + subExpression.toString() + ")"; + } + + /** + * @return the subExpression + */ + public Expression getSubExpression() { + return subExpression; + } + + /** + * @return the atLeastOne + */ + public boolean isAtLeastOne() { + return atLeastOne; + } + + /** + * @return the atMostOne + */ + public boolean isAtMostOne() { + return atMostOne; + } +} \ No newline at end of file diff --git a/bundles/core/org.eclipse.smarthome.core.voice/src/main/java/org/eclipse/smarthome/core/voice/text/ExpressionIdentifier.java b/bundles/core/org.eclipse.smarthome.core.voice/src/main/java/org/eclipse/smarthome/core/voice/text/ExpressionIdentifier.java new file mode 100644 index 00000000000..e754e0425fe --- /dev/null +++ b/bundles/core/org.eclipse.smarthome.core.voice/src/main/java/org/eclipse/smarthome/core/voice/text/ExpressionIdentifier.java @@ -0,0 +1,86 @@ +/** + * Copyright (c) 2014-2016 by the respective copyright holders. + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v1.0 + * which accompanies this distribution, and is available at + * http://www.eclipse.org/legal/epl-v10.html + */ +package org.eclipse.smarthome.core.voice.text; + +import java.util.HashSet; +import java.util.ResourceBundle; + +/** + * Expression that successfully parses, if a thing identifier token is found. This class is immutable. + * + * @author Tilman Kamp - Initial contribution and API + * + */ +public final class ExpressionIdentifier extends Expression { + + private AbstractRuleBasedInterpreter interpreter; + private Expression stopper; + + /** + * Constructs a new instance. + * + * @param interpreter the interpreter it belongs to. Used for dynamically fetching item name tokens + */ + public ExpressionIdentifier(AbstractRuleBasedInterpreter interpreter) { + this(interpreter, null); + } + + /** + * Constructs a new instance. + * + * @param interpreter the interpreter it belongs to. Used for dynamically fetching item name tokens + * @param stopper Expression that should not match, if the current token should be accepted as identifier + */ + public ExpressionIdentifier(AbstractRuleBasedInterpreter interpreter, Expression stopper) { + super(); + this.interpreter = interpreter; + this.stopper = stopper; + } + + @Override + ASTNode parse(ResourceBundle language, TokenList list) { + ASTNode node = new ASTNode(); + node.setSuccess(list.size() > 0 && (stopper == null || !stopper.parse(language, list).isSuccess())); + if (node.isSuccess()) { + node.setRemainingTokens(list.skipHead()); + node.setValue(list.head()); + node.setChildren(new ASTNode[0]); + generateValue(node); + } + return node; + } + + @Override + boolean collectFirsts(ResourceBundle language, HashSet firsts) { + HashSet f = new HashSet(interpreter.getAllItemTokens(language.getLocale())); + if (stopper != null) { + f.removeAll(stopper.getFirsts(language)); + } + firsts.addAll(f); + return true; + } + + @Override + public String toString() { + return "identifier(stop=" + stopper + ")"; + } + + /** + * @return the interpreter + */ + public AbstractRuleBasedInterpreter getInterpreter() { + return interpreter; + } + + /** + * @return the stopper expression + */ + public Expression getStopper() { + return stopper; + } +} \ No newline at end of file diff --git a/bundles/core/org.eclipse.smarthome.core.voice/src/main/java/org/eclipse/smarthome/core/voice/text/ExpressionLet.java b/bundles/core/org.eclipse.smarthome.core.voice/src/main/java/org/eclipse/smarthome/core/voice/text/ExpressionLet.java new file mode 100644 index 00000000000..512ad047fe5 --- /dev/null +++ b/bundles/core/org.eclipse.smarthome.core.voice/src/main/java/org/eclipse/smarthome/core/voice/text/ExpressionLet.java @@ -0,0 +1,129 @@ +/** + * Copyright (c) 2014-2016 by the respective copyright holders. + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v1.0 + * which accompanies this distribution, and is available at + * http://www.eclipse.org/legal/epl-v10.html + */ +package org.eclipse.smarthome.core.voice.text; + +import java.util.Arrays; +import java.util.Collections; +import java.util.HashSet; +import java.util.List; +import java.util.ResourceBundle; + +/** + * Expression that decorates the resulting (proxied) AST node of a given expression by a name, value and tag. + * This class is immutable. + * + * @author Tilman Kamp - Initial contribution and API + * + */ +public final class ExpressionLet extends Expression { + + private Expression subExpression; + private String name; + private Object value; + private Object tag; + + /** + * Constructs a new instance. + * + * @param name the name that should be set on the node. Null, if the node's name should not be changed. + * @param subExpression the expression who's resulting node should be altered + */ + public ExpressionLet(String name, Expression subExpression) { + this(name, subExpression, null, null); + } + + /** + * Constructs a new instance. + * + * @param subExpression the expression who's resulting node should be altered + * @param value the value that should be set on the node. Null, if the node's value should not be changed. + */ + public ExpressionLet(Expression subExpression, Object value) { + this(null, subExpression, value, null); + } + + /** + * Constructs a new instance. + * + * @param name the name that should be set on the node. Null, if the node's name should not be changed. + * @param subExpression the expression who's resulting node should be altered + * @param value the value that should be set on the node. Null, if the node's value should not be changed. + * @param tag the tag that should be set on the node. Null, if the node's tag should not be changed. + */ + public ExpressionLet(String name, Expression subExpression, Object value, Object tag) { + super(); + if (name != null) { + this.name = name; + } + this.subExpression = subExpression; + if (value != null) { + this.value = value; + } + if (tag != null) { + this.tag = tag; + } + } + + @Override + ASTNode parse(ResourceBundle language, TokenList list) { + ASTNode node = subExpression.parse(language, list); + if (node.isSuccess()) { + node.setName(name); + if (value != null) { + node.setValue(value); + } + if (tag != null) { + node.setTag(tag); + } + } + return node; + } + + @Override + List getChildExpressions() { + return Collections.unmodifiableList(Arrays.asList(subExpression)); + } + + @Override + boolean collectFirsts(ResourceBundle language, HashSet firsts) { + return subExpression.collectFirsts(language, firsts); + } + + @Override + public String toString() { + return "let(\"" + name + "\", " + subExpression.toString() + ", \"" + value + "\", \"" + tag + "\")"; + } + + /** + * @return the subExpression + */ + public Expression getSubExpression() { + return subExpression; + } + + /** + * @return the name + */ + public String getName() { + return name; + } + + /** + * @return the value + */ + public Object getValue() { + return value; + } + + /** + * @return the tag + */ + public Object getTag() { + return tag; + } +} \ No newline at end of file diff --git a/bundles/core/org.eclipse.smarthome.core.voice/src/main/java/org/eclipse/smarthome/core/voice/text/ExpressionMatch.java b/bundles/core/org.eclipse.smarthome.core.voice/src/main/java/org/eclipse/smarthome/core/voice/text/ExpressionMatch.java new file mode 100644 index 00000000000..c2d0c499f02 --- /dev/null +++ b/bundles/core/org.eclipse.smarthome.core.voice/src/main/java/org/eclipse/smarthome/core/voice/text/ExpressionMatch.java @@ -0,0 +1,63 @@ +/** + * Copyright (c) 2014-2016 by the respective copyright holders. + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v1.0 + * which accompanies this distribution, and is available at + * http://www.eclipse.org/legal/epl-v10.html + */ +package org.eclipse.smarthome.core.voice.text; + +import java.util.HashSet; +import java.util.ResourceBundle; + +/** + * Expression that successfully parses, if a given string constant is found. This class is immutable. + * + * @author Tilman Kamp - Initial contribution and API + * + */ +public final class ExpressionMatch extends Expression { + + private String pattern; + + /** + * Constructs a new instance. + * + * @param pattern the token that has to match for successful parsing + */ + public ExpressionMatch(String pattern) { + super(); + this.pattern = pattern; + } + + @Override + ASTNode parse(ResourceBundle language, TokenList list) { + ASTNode node = new ASTNode(); + node.setSuccess(list.checkHead(pattern)); + if (node.isSuccess()) { + node.setRemainingTokens(list.skipHead()); + node.setValue(pattern); + node.setChildren(new ASTNode[0]); + generateValue(node); + } + return node; + } + + @Override + boolean collectFirsts(ResourceBundle language, HashSet firsts) { + firsts.add(pattern); + return true; + } + + @Override + public String toString() { + return "match(\"" + pattern + "\")"; + } + + /** + * @return the pattern + */ + public String getPattern() { + return pattern; + } +} \ No newline at end of file diff --git a/bundles/core/org.eclipse.smarthome.core.voice/src/main/java/org/eclipse/smarthome/core/voice/text/ExpressionSequence.java b/bundles/core/org.eclipse.smarthome.core.voice/src/main/java/org/eclipse/smarthome/core/voice/text/ExpressionSequence.java new file mode 100644 index 00000000000..d9892ffad16 --- /dev/null +++ b/bundles/core/org.eclipse.smarthome.core.voice/src/main/java/org/eclipse/smarthome/core/voice/text/ExpressionSequence.java @@ -0,0 +1,83 @@ +/** + * Copyright (c) 2014-2016 by the respective copyright holders. + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v1.0 + * which accompanies this distribution, and is available at + * http://www.eclipse.org/legal/epl-v10.html + */ +package org.eclipse.smarthome.core.voice.text; + +import java.util.Arrays; +import java.util.Collections; +import java.util.HashSet; +import java.util.List; +import java.util.ResourceBundle; + +/** + * Expression that successfully parses, if a sequence of given expressions is matching. This class is immutable. + * + * @author Tilman Kamp - Initial contribution and API + * + */ +public final class ExpressionSequence extends Expression { + + private List subExpressions; + + /** + * Constructs a new instance. + * + * @param subExpressions the sub expressions that are parsed in the given order + */ + public ExpressionSequence(Expression... subExpressions) { + super(); + this.subExpressions = Collections + .unmodifiableList(Arrays.asList(Arrays.copyOf(subExpressions, subExpressions.length))); + } + + @Override + ASTNode parse(ResourceBundle language, TokenList list) { + int l = subExpressions.size(); + ASTNode node = new ASTNode(), cr; + ASTNode[] children = new ASTNode[l]; + Object[] values = new Object[l]; + for (int i = 0; i < l; i++) { + cr = children[i] = subExpressions.get(i).parse(language, list); + if (!cr.isSuccess()) { + return node; + } + values[i] = cr.getValue(); + list = cr.getRemainingTokens(); + } + node.setChildren(children); + node.setRemainingTokens(list); + node.setSuccess(true); + node.setValue(values); + generateValue(node); + return node; + } + + @Override + List getChildExpressions() { + return subExpressions; + } + + @Override + boolean collectFirsts(ResourceBundle language, HashSet firsts) { + boolean blocking = false; + for (Expression e : subExpressions) { + if ((blocking = e.collectFirsts(language, firsts)) == true) { + break; + } + } + return blocking; + } + + @Override + public String toString() { + String s = null; + for (Expression e : subExpressions) { + s = s == null ? e.toString() : (s + ", " + e.toString()); + } + return "seq(" + s + ")"; + } +} \ No newline at end of file diff --git a/bundles/io/org.eclipse.smarthome.io.voice/src/main/java/org/eclipse/smarthome/io/voice/text/HumanLanguageInterpreter.java b/bundles/core/org.eclipse.smarthome.core.voice/src/main/java/org/eclipse/smarthome/core/voice/text/HumanLanguageInterpreter.java similarity index 53% rename from bundles/io/org.eclipse.smarthome.io.voice/src/main/java/org/eclipse/smarthome/io/voice/text/HumanLanguageInterpreter.java rename to bundles/core/org.eclipse.smarthome.core.voice/src/main/java/org/eclipse/smarthome/core/voice/text/HumanLanguageInterpreter.java index 0828a55127a..e13f949345e 100644 --- a/bundles/io/org.eclipse.smarthome.io.voice/src/main/java/org/eclipse/smarthome/io/voice/text/HumanLanguageInterpreter.java +++ b/bundles/core/org.eclipse.smarthome.core.voice/src/main/java/org/eclipse/smarthome/core/voice/text/HumanLanguageInterpreter.java @@ -5,7 +5,7 @@ * which accompanies this distribution, and is available at * http://www.eclipse.org/legal/epl-v10.html */ -package org.eclipse.smarthome.io.voice.text; +package org.eclipse.smarthome.core.voice.text; import java.util.Locale; import java.util.Set; @@ -18,6 +18,21 @@ */ public interface HumanLanguageInterpreter { + /** + * Returns a simple string that uniquely identifies this service + * + * @return an id that identifies this service + */ + public String getId(); + + /** + * Returns a localized human readable label that can be used within UIs. + * + * @param locale the locale to provide the label for + * @return a localized string to be used in UIs + */ + public String getLabel(Locale locale); + /** * Interprets a human language text fragment of a given {@link Locale} * @@ -27,6 +42,15 @@ public interface HumanLanguageInterpreter { */ String interpret(Locale locale, String text) throws InterpretationException; + /** + * Gets the grammar of all commands of a given {@link Locale} of the interpreter + * + * @param locale language of the commands (given by a {@link Locale}) + * @param format the grammar format + * @return a grammar of the specified format + */ + String getGrammar(Locale locale, String format); + /** * Gets all supported languages of the interpreter by their {@link Locale}s * @@ -34,4 +58,11 @@ public interface HumanLanguageInterpreter { */ Set getSupportedLocales(); + /** + * Gets all supported grammar format specifiers + * + * @return Set of supported grammars (each given by a short name) + */ + Set getSupportedGrammarFormats(); + } diff --git a/bundles/io/org.eclipse.smarthome.io.voice/src/main/java/org/eclipse/smarthome/io/voice/text/InterpretationException.java b/bundles/core/org.eclipse.smarthome.core.voice/src/main/java/org/eclipse/smarthome/core/voice/text/InterpretationException.java similarity index 74% rename from bundles/io/org.eclipse.smarthome.io.voice/src/main/java/org/eclipse/smarthome/io/voice/text/InterpretationException.java rename to bundles/core/org.eclipse.smarthome.core.voice/src/main/java/org/eclipse/smarthome/core/voice/text/InterpretationException.java index a7c65042e68..e25afd547ac 100644 --- a/bundles/io/org.eclipse.smarthome.io.voice/src/main/java/org/eclipse/smarthome/io/voice/text/InterpretationException.java +++ b/bundles/core/org.eclipse.smarthome.core.voice/src/main/java/org/eclipse/smarthome/core/voice/text/InterpretationException.java @@ -5,7 +5,7 @@ * which accompanies this distribution, and is available at * http://www.eclipse.org/legal/epl-v10.html */ -package org.eclipse.smarthome.io.voice.text; +package org.eclipse.smarthome.core.voice.text; /** * An exception used by {@link HumanLanguageInterpreter}s, if an error occurs. @@ -17,6 +17,11 @@ public class InterpretationException extends Exception { private static final long serialVersionUID = 76120119745036525L; + /** + * Constructs a new interpretation exception. + * + * @param msg the textual response. Should be short, localized and understandable by non-technical users. + */ public InterpretationException(String msg) { super(msg); } diff --git a/bundles/core/org.eclipse.smarthome.core.voice/src/main/java/org/eclipse/smarthome/core/voice/text/InterpretationResult.java b/bundles/core/org.eclipse.smarthome.core.voice/src/main/java/org/eclipse/smarthome/core/voice/text/InterpretationResult.java new file mode 100644 index 00000000000..0c2ce460fb4 --- /dev/null +++ b/bundles/core/org.eclipse.smarthome.core.voice/src/main/java/org/eclipse/smarthome/core/voice/text/InterpretationResult.java @@ -0,0 +1,91 @@ +/** + * Copyright (c) 2014-2016 by the respective copyright holders. + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v1.0 + * which accompanies this distribution, and is available at + * http://www.eclipse.org/legal/epl-v10.html + */ +package org.eclipse.smarthome.core.voice.text; + +/** + * Bundles results of an interpretation. Represents final outcome and user feedback. This class is immutable. + * + * @author Tilman Kamp - Initial contribution and API + * + */ +public final class InterpretationResult { + + /** + * Represents successful parsing and interpretation. + */ + public final static InterpretationResult OK = new InterpretationResult(true, ""); + + /** + * Represents a syntactical problem during parsing. + */ + public final static InterpretationResult SYNTAX_ERROR = new InterpretationResult(false, "Syntax error."); + + /** + * Represents a problem in the interpretation step after successful parsing. + */ + public final static InterpretationResult SEMANTIC_ERROR = new InterpretationResult(false, "Semantic error."); + + private boolean success = false; + private InterpretationException exception; + private String response; + + /** + * Constructs a successful result. + * + * @param response the textual response. Should be short, localized and understandable by non-technical users. + */ + public InterpretationResult(String response) { + super(); + this.response = response; + this.success = true; + } + + /** + * Constructs an unsuccessful result. + * + * @param exception the responsible exception + */ + public InterpretationResult(InterpretationException exception) { + super(); + this.exception = exception; + this.success = false; + } + + /** + * Constructs a result. + * + * @param success if the result represents a successful or unsuccessful interpretation + * @param response the textual response. Should be short, localized and understandable by non-technical users. + */ + public InterpretationResult(boolean success, String response) { + super(); + this.success = success; + this.response = response; + } + + /** + * @return if interpretation was successful + */ + public boolean isSuccess() { + return success; + } + + /** + * @return the exception + */ + public InterpretationException getException() { + return exception; + } + + /** + * @return the response + */ + public String getResponse() { + return response; + } +} \ No newline at end of file diff --git a/bundles/core/org.eclipse.smarthome.core.voice/src/main/java/org/eclipse/smarthome/core/voice/text/Rule.java b/bundles/core/org.eclipse.smarthome.core.voice/src/main/java/org/eclipse/smarthome/core/voice/text/Rule.java new file mode 100644 index 00000000000..54f1911a9ce --- /dev/null +++ b/bundles/core/org.eclipse.smarthome.core.voice/src/main/java/org/eclipse/smarthome/core/voice/text/Rule.java @@ -0,0 +1,55 @@ +/** + * Copyright (c) 2014-2016 by the respective copyright holders. + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v1.0 + * which accompanies this distribution, and is available at + * http://www.eclipse.org/legal/epl-v10.html + */ +package org.eclipse.smarthome.core.voice.text; + +import java.util.ResourceBundle; + +/** + * Represents an expression plus action code that will be executed after successful parsing. This class is immutable and + * deriving classes should conform to this principle. + * + * @author Tilman Kamp - Initial contribution and API + * + */ +public abstract class Rule { + + private Expression expression; + + /** + * Constructs a new instance. + * + * @param expression the expression that has to parse successfully, before {@link interpretAST} is called + */ + public Rule(Expression expression) { + this.expression = expression; + } + + /** + * Will get called after the expression was successfully parsed. + * + * @param language a resource bundle that can be used for looking up common localized response phrases + * @param node the resulting AST node of the parse run. To be used as input. + * @return + */ + public abstract InterpretationResult interpretAST(ResourceBundle language, ASTNode node); + + InterpretationResult execute(ResourceBundle language, TokenList list) { + ASTNode node = expression.parse(language, list); + if (node.isSuccess() && node.getRemainingTokens().eof()) { + return interpretAST(language, node); + } + return InterpretationResult.SYNTAX_ERROR; + } + + /** + * @return the expression + */ + public Expression getExpression() { + return expression; + } +} \ No newline at end of file diff --git a/bundles/core/org.eclipse.smarthome.core.voice/src/main/java/org/eclipse/smarthome/core/voice/text/TokenList.java b/bundles/core/org.eclipse.smarthome.core.voice/src/main/java/org/eclipse/smarthome/core/voice/text/TokenList.java new file mode 100644 index 00000000000..172dbae9cda --- /dev/null +++ b/bundles/core/org.eclipse.smarthome.core.voice/src/main/java/org/eclipse/smarthome/core/voice/text/TokenList.java @@ -0,0 +1,173 @@ +/** + * Copyright (c) 2014-2016 by the respective copyright holders. + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v1.0 + * which accompanies this distribution, and is available at + * http://www.eclipse.org/legal/epl-v10.html + */ +package org.eclipse.smarthome.core.voice.text; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; + +/** + * A helper to parse a sequence of tokens. This class is immutable. + * + * @author Tilman Kamp - Initial contribution and API + * + */ +public class TokenList { + + private List list = null; + + private int head = 0; + private int tail = 0; + + /** + * Constructs a new instance. + * + * @param list of the initial tokens + */ + public TokenList(List list) { + this.list = Collections.unmodifiableList(new ArrayList(list)); + this.head = 0; + this.tail = list.size() - 1; + } + + private TokenList(List list, int head, int tail) { + this.list = list; + this.head = head; + this.tail = tail; + } + + /** + * Gets the first token of the list. + * + * @return the first token of the list + */ + public String head() { + return (list.size() < 1 || head < 0 || head >= list.size()) ? null : list.get(head); + } + + /** + * Gets the last token of the list. + * + * @return the last token of the list + */ + public String tail() { + return (list.size() < 1 || tail < 0 || tail >= list.size()) ? null : list.get(tail); + } + + /** + * Checks, if the list is empty. + * + * @return if the list is empty + */ + public boolean eof() { + return head > tail; + } + + /** + * Retrieves the token count within the list. + * + * @return token count + */ + public int size() { + return tail - head + 1; + } + + /** + * Checks for the first token of the list. + * If it is equal to one of the provided alternatives, it will succeed. + * + * @param alternatives Allowed token values for the list's first token. + * If empty, all token values are allowed. + * @return True, if first token is equal to one of the alternatives or if no alternatives were provided. + * False otherwise. Always false, if there is no first token (if the list is empty). + */ + public boolean checkHead(String... alternatives) { + return check(head, alternatives); + } + + /** + * Checks for the last token of the list. + * If it is equal to one of the provided alternatives, it will succeed. + * + * @param alternatives Allowed token values for the list's last token. + * If empty, all token values are allowed. + * @return True, if last token is equal to one of the alternatives or if no alternatives were provided. + * False otherwise. Always false, if there is no last token (if the list is empty). + */ + public boolean checkTail(String... alternatives) { + return check(tail, alternatives); + } + + /** + * Retrieves the first token of the list, in case it is equal to one of the provided alternatives. + * + * @param alternatives Allowed token values for the list's first token. + * If empty, all token values are allowed. + * @return First token, if it is equal to one of the alternatives or if no alternatives were provided. + * Null otherwise. Always null, if there is no first token (if the list is empty). + */ + public String peekHead(String... alternatives) { + return peek(head, alternatives); + } + + /** + * Retrieves the last token of the list, in case it is equal to one of the provided alternatives. + * + * @param alternatives Allowed token values for the list's last token. + * If empty, all token values are allowed. + * @return Last token, if it is equal to one of the alternatives or if no alternatives were provided. + * Null otherwise. Always null, if there is no last token (if the list is empty). + */ + public String peekTail(String... alternatives) { + return peek(tail, alternatives); + } + + /** + * Creates a new list without the first token. + * + * @return a new list without the first token + */ + public TokenList skipHead() { + return new TokenList(list, head + 1, tail); + } + + /** + * Creates a new list without the last token. + * + * @return a new list without the last token + */ + public TokenList skipTail() { + return new TokenList(list, head, tail - 1); + } + + private String peek(int index, String... alternatives) { + return splice(index, alternatives); + } + + private boolean check(int index, String... alternatives) { + return splice(index, alternatives) != null; + } + + private String splice(int index, String... alternatives) { + if (index < head || index > tail || head > tail) { + return null; + } + String token = list.get(index); + if (alternatives.length == 0) { + return token; + } else { + for (String alt : alternatives) { + if (alt.equals(token)) { + return token; + } + } + return null; + } + } + +} diff --git a/bundles/core/org.eclipse.smarthome.core.voice/src/main/resources/LanguageSupport.properties b/bundles/core/org.eclipse.smarthome.core.voice/src/main/resources/LanguageSupport.properties new file mode 100644 index 00000000000..3b8928aae28 --- /dev/null +++ b/bundles/core/org.eclipse.smarthome.core.voice/src/main/resources/LanguageSupport.properties @@ -0,0 +1,18 @@ +ok = Ok. +sorry = Sorry, I didn't get that. +multiple_objects = There's more than one object with a similar name. +no_objects = There is no object named like that. +state_already_singular = The object is already . +state_already_plural = All objects are already . +state_on = on +state_off = off +state_up = up +state_down = down +state_play = playing +state_pause = paused +state_rewind = rewinding +state_fastforward = fast forwarding +state_open = open +state_closed = closed +state_undef = undefined +state_null = not set diff --git a/bundles/core/org.eclipse.smarthome.core.voice/src/main/resources/LanguageSupport_de.properties b/bundles/core/org.eclipse.smarthome.core.voice/src/main/resources/LanguageSupport_de.properties new file mode 100644 index 00000000000..3d2fe6eb609 --- /dev/null +++ b/bundles/core/org.eclipse.smarthome.core.voice/src/main/resources/LanguageSupport_de.properties @@ -0,0 +1,18 @@ +ok = Ok. +sorry = Entschuldigung. Ich habe das nicht verstanden. +multiple_objects = Es gibt mehrere Objekte mit einem ähnlichen Namen. +no_objects = Es gibt kein Objekt mit diesem Namen. +state_already_singular = Objekt ist bereits . +state_already_plural = Alle Objekte sind bereits . +state_on = an +state_off = aus +state_up = oben +state_down = unten +state_play = in Wiedergabe +state_pause = pausiert +state_rewind = zurücklaufend +state_fastforward = vorlaufend +state_open = offen +state_closed = geschlossen +state_undef = undefiniert +state_null = nicht gesetzt diff --git a/bundles/core/org.eclipse.smarthome.core/META-INF/MANIFEST.MF b/bundles/core/org.eclipse.smarthome.core/META-INF/MANIFEST.MF index 693e1f6a0a1..971eb9b1053 100644 --- a/bundles/core/org.eclipse.smarthome.core/META-INF/MANIFEST.MF +++ b/bundles/core/org.eclipse.smarthome.core/META-INF/MANIFEST.MF @@ -1,5 +1,6 @@ Manifest-Version: 1.0 -Export-Package: org.eclipse.smarthome.core.binding, +Export-Package: org.eclipse.smarthome.core.audio, + org.eclipse.smarthome.core.binding, org.eclipse.smarthome.core.binding.dto, org.eclipse.smarthome.core.common, org.eclipse.smarthome.core.common.osgi, @@ -58,3 +59,4 @@ Import-Package: com.google.common.base, org.slf4j Bundle-SymbolicName: org.eclipse.smarthome.core Bundle-Activator: org.eclipse.smarthome.core.internal.CoreActivator +Bundle-ActivationPolicy: lazy diff --git a/bundles/io/org.eclipse.smarthome.io.audio/src/main/java/org/eclipse/smarthome/io/audio/AudioException.java b/bundles/core/org.eclipse.smarthome.core/src/main/java/org/eclipse/smarthome/core/audio/AudioException.java similarity index 96% rename from bundles/io/org.eclipse.smarthome.io.audio/src/main/java/org/eclipse/smarthome/io/audio/AudioException.java rename to bundles/core/org.eclipse.smarthome.core/src/main/java/org/eclipse/smarthome/core/audio/AudioException.java index 4f04ddc926e..a31ec83df3d 100644 --- a/bundles/io/org.eclipse.smarthome.io.audio/src/main/java/org/eclipse/smarthome/io/audio/AudioException.java +++ b/bundles/core/org.eclipse.smarthome.core/src/main/java/org/eclipse/smarthome/core/audio/AudioException.java @@ -5,7 +5,7 @@ * which accompanies this distribution, and is available at * http://www.eclipse.org/legal/epl-v10.html */ -package org.eclipse.smarthome.io.audio; +package org.eclipse.smarthome.core.audio; /** * General purpose audio exception diff --git a/bundles/core/org.eclipse.smarthome.core/src/main/java/org/eclipse/smarthome/core/audio/AudioFormat.java b/bundles/core/org.eclipse.smarthome.core/src/main/java/org/eclipse/smarthome/core/audio/AudioFormat.java new file mode 100644 index 00000000000..d45502c1a65 --- /dev/null +++ b/bundles/core/org.eclipse.smarthome.core/src/main/java/org/eclipse/smarthome/core/audio/AudioFormat.java @@ -0,0 +1,272 @@ +/** + * Copyright (c) 2014-2016 by the respective copyright holders. + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v1.0 + * which accompanies this distribution, and is available at + * http://www.eclipse.org/legal/epl-v10.html + */ +package org.eclipse.smarthome.core.audio; + +/** + * An audio format definition + * + * @author Harald Kuhn - Initial API + * @author Kelly Davis - Modified to match discussion in #584 + * @author Kai Kreuzer - Moved class, included constants, added toString + */ +public class AudioFormat { + + /** + * {@link AudioCodec} encoded data without any container header or footer, + * e.g. MP3 is a non-container format + */ + public static final String CONTAINER_NONE = "NONE"; + + /** + * Microsofts wave container format + * + * @see WAV Format + * @see Supported codecs + * @see RIFF container format + */ + public static final String CONTAINER_WAVE = "WAVE"; + + /** + * OGG container format + * + * @see OGG + */ + public static final String CONTAINER_OGG = "OGG"; + + /** + * PCM Signed + * + * @see PCM Types + */ + public static final String CODEC_PCM_SIGNED = "PCM_SIGNED"; + + /** + * PCM Unsigned + * + * @see PCM Types + */ + public static final String CODEC_PCM_UNSIGNED = "PCM_UNSIGNED"; + + /** + * PCM A-law + * + * @see PCM Types + */ + public static final String CODEC_PCM_ALAW = "ALAW"; + + /** + * PCM u-law + * + * @see PCM Types + */ + public static final String CODEC_PCM_ULAW = "ULAW"; + + /** + * MP3 Codec + * + * @see MP3 Codec + */ + public static final String CODEC_MP3 = "MP3"; + + /** + * Vorbis Codec + * + * @see Vorbis + */ + public static final String CODEC_VORBIS = "VORBIS"; + + /** + * Codec + */ + private final String codec; + + /** + * Container + */ + private final String container; + + /** + * Big endian or little endian + */ + private final Boolean bigEndian; + + /** + * Bit depth + * + * @see Bit Depth + */ + private final Integer bitDepth; + + /** + * Bit rate + * + * @see Bit Rate + */ + private final Integer bitRate; + + /** + * Sample frequency + */ + private final Long frequency; + + /** + * Constructs an instance with the specified properties. + * + * Note that any properties that are null indicate that + * the corresponding AudioFormat allows any value for + * the property. + * + * Concretely this implies that if, for example, one + * passed null for the value of frequency, this would + * mean the created AudioFormat allowed for any valid + * frequency. + * + * @param container The container for the audio + * @param codec The audio codec + * @param bigEndian If the audo data is big endian + * @param bitDepth The bit depth of the audo data + * @param bitRate The bit rate of the audio + * @param frequency The frequency at which the audio was sampled + */ + public AudioFormat(String container, String codec, Boolean bigEndian, Integer bitDepth, Integer bitRate, + Long frequency) { + super(); + this.container = container; + this.codec = codec; + this.bigEndian = bigEndian; + this.bitDepth = bitDepth; + this.bitRate = bitRate; + this.frequency = frequency; + } + + /** + * Gets codec + * + * @return The codec + */ + public String getCodec() { + return codec; + } + + /** + * Gets container + * + * @return The container + */ + public String getContainer() { + return container; + } + + /** + * Is big endian? + * + * @return If format is big endian + */ + public Boolean isBigEndian() { + return bigEndian; + } + + /** + * Gets bit depth + * + * @see Bit Depth + * @return Bit depth + */ + public Integer getBitDepth() { + return bitDepth; + } + + /** + * Gets bit rate + * + * @see Bit Rate + * @return Bit rate + */ + public Integer getBitRate() { + return bitRate; + } + + /** + * Gets frequency + * + * @return The frequency + */ + public Long getFrequency() { + return frequency; + } + + /** + * Determines if the passed AudioFormat is compatible with this AudioFormat. + * + * This AudioFormat is compatible with the passed AudioFormat if both have + * the same value for all non-null members of this instance. + */ + public boolean isCompatible(AudioFormat audioFormat) { + if (audioFormat == null) { + return false; + } + if ((null != getContainer()) && (!getContainer().equals(audioFormat.getContainer()))) { + return false; + } + if ((null != getCodec()) && (!getCodec().equals(audioFormat.getCodec()))) { + return false; + } + if ((null != isBigEndian()) && (!isBigEndian().equals(audioFormat.isBigEndian()))) { + return false; + } + if ((null != getBitDepth()) && (!getBitDepth().equals(audioFormat.getBitDepth()))) { + return false; + } + if ((null != getBitRate()) && (!getBitRate().equals(audioFormat.getBitRate()))) { + return false; + } + if ((null != getFrequency()) && (!getFrequency().equals(audioFormat.getFrequency()))) { + return false; + } + return true; + } + + @Override + public boolean equals(Object obj) { + if (obj instanceof AudioFormat) { + AudioFormat format = (AudioFormat) obj; + if (!(null == getCodec() ? null == format.getCodec() : getCodec().equals(format.getCodec()))) { + return false; + } + if (!(null == getContainer() ? null == format.getContainer() + : getContainer().equals(format.getContainer()))) { + return false; + } + if (!(null == isBigEndian() ? null == format.isBigEndian() : isBigEndian().equals(format.isBigEndian()))) { + return false; + } + if (!(null == getBitDepth() ? null == format.getBitDepth() : getBitDepth().equals(format.getBitDepth()))) { + return false; + } + if (!(null == getBitRate() ? null == format.getBitRate() : getBitRate().equals(format.getBitRate()))) { + return false; + } + if (!(null == getFrequency() ? null == format.getFrequency() + : getFrequency().equals(format.getFrequency()))) { + return false; + } + return true; + } + return super.equals(obj); + } + + @Override + public String toString() { + return "AudioFormat [" + (codec != null ? "codec=" + codec + ", " : "") + + (container != null ? "container=" + container + ", " : "") + + (bigEndian != null ? "bigEndian=" + bigEndian + ", " : "") + + (bitDepth != null ? "bitDepth=" + bitDepth + ", " : "") + + (bitRate != null ? "bitRate=" + bitRate + ", " : "") + + (frequency != null ? "frequency=" + frequency : "") + "]"; + } +} diff --git a/bundles/core/org.eclipse.smarthome.core/src/main/java/org/eclipse/smarthome/core/audio/AudioSink.java b/bundles/core/org.eclipse.smarthome.core/src/main/java/org/eclipse/smarthome/core/audio/AudioSink.java new file mode 100644 index 00000000000..597d54295cc --- /dev/null +++ b/bundles/core/org.eclipse.smarthome.core/src/main/java/org/eclipse/smarthome/core/audio/AudioSink.java @@ -0,0 +1,53 @@ +/** + * Copyright (c) 2014-2016 by the respective copyright holders. + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v1.0 + * which accompanies this distribution, and is available at + * http://www.eclipse.org/legal/epl-v10.html + */ +package org.eclipse.smarthome.core.audio; + +import java.util.Locale; +import java.util.Set; + +/** + * Definition of an audio output like headphones, a speaker or for writing to + * a file / clip. + * + * @author Harald Kuhn - Initial API + * @author Kelly Davis - Modified to match discussion in #584 + */ +public interface AudioSink { + + /** + * Returns a simple string that uniquely identifies this service + * + * @return an id that identifies this service + */ + public String getId(); + + /** + * Returns a localized human readable label that can be used within UIs. + * + * @param locale the locale to provide the label for + * @return a localized string to be used in UIs + */ + public String getLabel(Locale locale); + + /** + * Processes the passed {@link AudioStream} + * + * If the passed {@link AudioStream} has a {@link AudioFormat} not supported by this instance, + * an {@link UnsupportedAudioFormatException} is thrown. + * + * @throws UnsupportedAudioFormatException If audioStream format is not supported + */ + void process(AudioStream audioStream) throws UnsupportedAudioFormatException; + + /** + * Gets a set containing all supported audio formats + * + * @return A Set containing all supported audio formats + */ + public Set getSupportedFormats(); +} diff --git a/bundles/core/org.eclipse.smarthome.core/src/main/java/org/eclipse/smarthome/core/audio/AudioSource.java b/bundles/core/org.eclipse.smarthome.core/src/main/java/org/eclipse/smarthome/core/audio/AudioSource.java new file mode 100644 index 00000000000..55aba454d9e --- /dev/null +++ b/bundles/core/org.eclipse.smarthome.core/src/main/java/org/eclipse/smarthome/core/audio/AudioSource.java @@ -0,0 +1,52 @@ +/** + * Copyright (c) 2014-2016 by the respective copyright holders. + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v1.0 + * which accompanies this distribution, and is available at + * http://www.eclipse.org/legal/epl-v10.html + */ +package org.eclipse.smarthome.core.audio; + +import java.util.Locale; +import java.util.Set; + +/** + * This is an audio source, which can provide a continuous live stream of audio. + * Its main use is for microphones and other "line-in" sources and it can be registered as a service in order to make + * it available throughout the system. + * + * @author Kai Kreuzer - Initial contribution and API + */ +public interface AudioSource { + + /** + * Returns a simple string that uniquely identifies this service + * + * @return an id that identifies this service + */ + public String getId(); + + /** + * Returns a localized human readable label that can be used within UIs. + * + * @param locale the locale to provide the label for + * @return a localized string to be used in UIs + */ + public String getLabel(Locale locale); + + /** + * Obtain the audio formats supported by this TTSService + * + * @return The audio formats supported by this service + */ + public Set getSupportedFormats(); + + /** + * Gets InputStream for reading audio data in supported audio format + * + * @return InputStream for reading audio data + * @throws AudioException If problem occurs obtaining InputStream + */ + abstract public AudioStream getInputStream() throws AudioException; + +} diff --git a/bundles/core/org.eclipse.smarthome.core/src/main/java/org/eclipse/smarthome/core/audio/AudioStream.java b/bundles/core/org.eclipse.smarthome.core/src/main/java/org/eclipse/smarthome/core/audio/AudioStream.java new file mode 100644 index 00000000000..2d4d37277c1 --- /dev/null +++ b/bundles/core/org.eclipse.smarthome.core/src/main/java/org/eclipse/smarthome/core/audio/AudioStream.java @@ -0,0 +1,33 @@ +/** + * Copyright (c) 2014-2016 by the respective copyright holders. + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v1.0 + * which accompanies this distribution, and is available at + * http://www.eclipse.org/legal/epl-v10.html + */ +package org.eclipse.smarthome.core.audio; + +import java.io.InputStream; + +/** + * Wrapper for a source of audio data. + * + * In contrast to {@link AudioSource}, this is often a "one time use" instance for passing some audio data, + * but it is not meant to be registered as a service. + * + * The stream needs to be closed by the client that uses it. + * + * @author Harald Kuhn - Initial API + * @author Kelly Davis - Modified to match discussion in #584 + * @author Kai Kreuzer - Refactored to be only a temporary instance for the stream + */ +abstract public class AudioStream extends InputStream { + + /** + * Gets the supported audio format + * + * @return The supported audio format + */ + abstract public AudioFormat getFormat(); + +} diff --git a/bundles/io/org.eclipse.smarthome.io.audio/src/main/java/org/eclipse/smarthome/io/audio/UnsupportedAudioFormatException.java b/bundles/core/org.eclipse.smarthome.core/src/main/java/org/eclipse/smarthome/core/audio/UnsupportedAudioFormatException.java similarity index 97% rename from bundles/io/org.eclipse.smarthome.io.audio/src/main/java/org/eclipse/smarthome/io/audio/UnsupportedAudioFormatException.java rename to bundles/core/org.eclipse.smarthome.core/src/main/java/org/eclipse/smarthome/core/audio/UnsupportedAudioFormatException.java index 51d06826f9d..60127d9013a 100644 --- a/bundles/io/org.eclipse.smarthome.io.audio/src/main/java/org/eclipse/smarthome/io/audio/UnsupportedAudioFormatException.java +++ b/bundles/core/org.eclipse.smarthome.core/src/main/java/org/eclipse/smarthome/core/audio/UnsupportedAudioFormatException.java @@ -5,7 +5,7 @@ * which accompanies this distribution, and is available at * http://www.eclipse.org/legal/epl-v10.html */ -package org.eclipse.smarthome.io.audio; +package org.eclipse.smarthome.core.audio; /** * Thrown when a requested format is not supported by an {@link AudioSource} diff --git a/bundles/core/pom.xml b/bundles/core/pom.xml index 688aff76312..b04896dd5cc 100644 --- a/bundles/core/pom.xml +++ b/bundles/core/pom.xml @@ -31,6 +31,8 @@ org.eclipse.smarthome.core.binding.xml.test org.eclipse.smarthome.core.thing.xml org.eclipse.smarthome.core.thing.xml.test + org.eclipse.smarthome.core.voice + org.eclipse.smarthome.core.voice.test diff --git a/bundles/io/org.eclipse.smarthome.io.audio/META-INF/MANIFEST.MF b/bundles/io/org.eclipse.smarthome.io.audio/META-INF/MANIFEST.MF deleted file mode 100644 index e518bd76deb..00000000000 --- a/bundles/io/org.eclipse.smarthome.io.audio/META-INF/MANIFEST.MF +++ /dev/null @@ -1,11 +0,0 @@ -Manifest-Version: 1.0 -Bundle-ManifestVersion: 2 -Bundle-Name: Eclipse SmartHome Audio I/O Bundle -Bundle-SymbolicName: org.eclipse.smarthome.io.audio -Bundle-Vendor: Eclipse.org/SmartHome -Bundle-Version: 0.9.0.qualifier -Bundle-RequiredExecutionEnvironment: JavaSE-1.7 -Bundle-ClassPath: . -Import-Package: org.slf4j -Export-Package: org.eclipse.smarthome.io.audio - diff --git a/bundles/io/org.eclipse.smarthome.io.audio/build.properties b/bundles/io/org.eclipse.smarthome.io.audio/build.properties deleted file mode 100644 index aad35ad01bb..00000000000 --- a/bundles/io/org.eclipse.smarthome.io.audio/build.properties +++ /dev/null @@ -1,4 +0,0 @@ -output.. = target/classes -bin.includes = META-INF/,\ - . -source.. = src/main/java/ \ No newline at end of file diff --git a/bundles/io/org.eclipse.smarthome.io.console/META-INF/MANIFEST.MF b/bundles/io/org.eclipse.smarthome.io.console/META-INF/MANIFEST.MF index aa2c0629c19..521c1c2e189 100644 --- a/bundles/io/org.eclipse.smarthome.io.console/META-INF/MANIFEST.MF +++ b/bundles/io/org.eclipse.smarthome.io.console/META-INF/MANIFEST.MF @@ -21,3 +21,4 @@ Service-Component: OSGI-INF/*.xml Bundle-RequiredExecutionEnvironment: JavaSE-1.7 Export-Package: org.eclipse.smarthome.io.console, org.eclipse.smarthome.io.console.extensions +Bundle-ActivationPolicy: lazy diff --git a/bundles/io/pom.xml b/bundles/io/pom.xml index ff9c01897f8..398f04981a9 100644 --- a/bundles/io/pom.xml +++ b/bundles/io/pom.xml @@ -16,7 +16,6 @@ pom - org.eclipse.smarthome.io.audio org.eclipse.smarthome.io.console org.eclipse.smarthome.io.console.eclipse org.eclipse.smarthome.io.console.rfc147 @@ -35,7 +34,6 @@ org.eclipse.smarthome.io.transport.mqtt org.eclipse.smarthome.io.transport.upnp org.eclipse.smarthome.io.transport.upnp.test - org.eclipse.smarthome.io.voice \ No newline at end of file diff --git a/bundles/io/org.eclipse.smarthome.io.audio/.classpath b/extensions/io/org.eclipse.smarthome.io.javasound/.classpath similarity index 100% rename from bundles/io/org.eclipse.smarthome.io.audio/.classpath rename to extensions/io/org.eclipse.smarthome.io.javasound/.classpath diff --git a/bundles/io/org.eclipse.smarthome.io.audio/.project b/extensions/io/org.eclipse.smarthome.io.javasound/.project similarity index 93% rename from bundles/io/org.eclipse.smarthome.io.audio/.project rename to extensions/io/org.eclipse.smarthome.io.javasound/.project index dd152151c00..00a60e0ff91 100644 --- a/bundles/io/org.eclipse.smarthome.io.audio/.project +++ b/extensions/io/org.eclipse.smarthome.io.javasound/.project @@ -1,6 +1,6 @@ - org.eclipse.smarthome.io.audio + org.eclipse.smarthome.io.javasound diff --git a/extensions/io/org.eclipse.smarthome.io.javasound/META-INF/MANIFEST.MF b/extensions/io/org.eclipse.smarthome.io.javasound/META-INF/MANIFEST.MF new file mode 100644 index 00000000000..fa8f1963a44 --- /dev/null +++ b/extensions/io/org.eclipse.smarthome.io.javasound/META-INF/MANIFEST.MF @@ -0,0 +1,13 @@ +Manifest-Version: 1.0 +Bundle-ManifestVersion: 2 +Bundle-Name: Eclipse SmartHome JavaSound I/O +Bundle-SymbolicName: org.eclipse.smarthome.io.javasound +Bundle-Vendor: Eclipse.org/SmartHome +Bundle-Version: 0.9.0.qualifier +Bundle-RequiredExecutionEnvironment: JavaSE-1.7 +Bundle-ClassPath: . +Import-Package: javax.sound.sampled, + org.eclipse.smarthome.core.audio, + org.slf4j +Bundle-ActivationPolicy: lazy +Service-Component: OSGI-INF/*.xml diff --git a/extensions/io/org.eclipse.smarthome.io.javasound/OSGI-INF/javasoundsink.xml b/extensions/io/org.eclipse.smarthome.io.javasound/OSGI-INF/javasoundsink.xml new file mode 100644 index 00000000000..14a16374b35 --- /dev/null +++ b/extensions/io/org.eclipse.smarthome.io.javasound/OSGI-INF/javasoundsink.xml @@ -0,0 +1,16 @@ + + + + + + + + diff --git a/extensions/io/org.eclipse.smarthome.io.javasound/OSGI-INF/javasoundsource.xml b/extensions/io/org.eclipse.smarthome.io.javasound/OSGI-INF/javasoundsource.xml new file mode 100644 index 00000000000..6ecbc26c79c --- /dev/null +++ b/extensions/io/org.eclipse.smarthome.io.javasound/OSGI-INF/javasoundsource.xml @@ -0,0 +1,16 @@ + + + + + + + + diff --git a/extensions/io/org.eclipse.smarthome.io.javasound/about.html b/extensions/io/org.eclipse.smarthome.io.javasound/about.html new file mode 100644 index 00000000000..1b9e722c419 --- /dev/null +++ b/extensions/io/org.eclipse.smarthome.io.javasound/about.html @@ -0,0 +1,28 @@ + + + + +About + + +

About This Content

+ +

<May 12, 2015>

+

License

+ +

The Eclipse Foundation makes available all content in this plug-in ("Content"). Unless otherwise +indicated below, the Content is provided to you under the terms and conditions of the +Eclipse Public License Version 1.0 ("EPL"). A copy of the EPL is available +at http://www.eclipse.org/legal/epl-v10.html. +For purposes of the EPL, "Program" will mean the Content.

+ +

If you did not receive this Content directly from the Eclipse Foundation, the Content is +being redistributed by another party ("Redistributor") and different terms and conditions may +apply to your use of any object code in the Content. Check the Redistributor's license that was +provided with the Content. If no such license exists, contact the Redistributor. Unless otherwise +indicated below, the terms and conditions of the EPL still apply to any source code in the Content +and such source code may be obtained at http://www.eclipse.org.

+ + + diff --git a/bundles/io/org.eclipse.smarthome.io.voice/build.properties b/extensions/io/org.eclipse.smarthome.io.javasound/build.properties similarity index 82% rename from bundles/io/org.eclipse.smarthome.io.voice/build.properties rename to extensions/io/org.eclipse.smarthome.io.javasound/build.properties index 46e83d4b0ca..43a0adc299f 100644 --- a/bundles/io/org.eclipse.smarthome.io.voice/build.properties +++ b/extensions/io/org.eclipse.smarthome.io.javasound/build.properties @@ -1,4 +1,4 @@ -output.. = target/classes/ +output.. = target/classes bin.includes = META-INF/,\ .,\ OSGI-INF/,\ diff --git a/extensions/io/org.eclipse.smarthome.io.javasound/pom.xml b/extensions/io/org.eclipse.smarthome.io.javasound/pom.xml new file mode 100644 index 00000000000..08d259df74d --- /dev/null +++ b/extensions/io/org.eclipse.smarthome.io.javasound/pom.xml @@ -0,0 +1,23 @@ + + + + + org.eclipse.smarthome.io + pom + 0.9.0-SNAPSHOT + + + + org.eclipse.smarthome.io.javasoundo + org.eclipse.smarthome.io.javasound + + + 4.0.0 + org.eclipse.smarthome.core + org.eclipse.smarthome.io.javasound + + Eclipse SmartHome JavaSound I/O + + eclipse-plugin + + diff --git a/extensions/io/org.eclipse.smarthome.io.javasound/src/main/java/org/eclipse/smarthome/io/javasound/internal/AudioPlayer.java b/extensions/io/org.eclipse.smarthome.io.javasound/src/main/java/org/eclipse/smarthome/io/javasound/internal/AudioPlayer.java new file mode 100644 index 00000000000..c8da8e28b2b --- /dev/null +++ b/extensions/io/org.eclipse.smarthome.io.javasound/src/main/java/org/eclipse/smarthome/io/javasound/internal/AudioPlayer.java @@ -0,0 +1,123 @@ +/** + * Copyright (c) 2014-2016 by the respective copyright holders. + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v1.0 + * which accompanies this distribution, and is available at + * http://www.eclipse.org/legal/epl-v10.html + */ +package org.eclipse.smarthome.io.javasound.internal; + +import java.io.IOException; + +import javax.sound.sampled.AudioFormat; +import javax.sound.sampled.AudioSystem; +import javax.sound.sampled.DataLine; +import javax.sound.sampled.Line; +import javax.sound.sampled.Line.Info; +import javax.sound.sampled.Mixer; +import javax.sound.sampled.SourceDataLine; + +import org.eclipse.smarthome.core.audio.AudioStream; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * This is a class that plays an AudioStream through the Java sound API + * + * @author Kelly Davis - Initial contribution and API + * @author Kai Kreuzer - Refactored to use AudioStream and logging + * + */ +public class AudioPlayer extends Thread { + + private static final Logger logger = LoggerFactory.getLogger(AudioPlayer.class); + + /** + * The AudioStream to play + */ + private final AudioStream audioStream; + + /** + * Constructs an AudioPlayer to play the passed AudioSource + * + * @param audioSource The AudioSource to play + */ + public AudioPlayer(AudioStream audioStream) { + this.audioStream = audioStream; + } + + /** + * This method plays the contained AudioSource + */ + @Override + public void run() { + SourceDataLine line; + AudioFormat audioFormat = convertAudioFormat(this.audioStream.getFormat()); + DataLine.Info info = new DataLine.Info(SourceDataLine.class, audioFormat); + try { + line = (SourceDataLine) AudioSystem.getLine(info); + line.open(audioFormat); + } catch (Exception e) { + logger.warn("No line found: {}", e.getMessage()); + logger.info("Available lines are:"); + Mixer.Info[] mixerInfo = AudioSystem.getMixerInfo(); // get available mixers + Mixer mixer = null; + for (int cnt = 0; cnt < mixerInfo.length; cnt++) { + mixer = AudioSystem.getMixer(mixerInfo[cnt]); + Line.Info[] lineInfos = mixer.getSourceLineInfo(); + for (Info lineInfo : lineInfos) { + logger.info(lineInfo.toString()); + } + } + return; + } + line.start(); + int nRead = 0; + byte[] abData = new byte[65532]; // needs to be a multiple of 4 and 6, to support both 16 and 24 bit stereo + try { + while (-1 != nRead) { + nRead = audioStream.read(abData, 0, abData.length); + if (nRead >= 0) { + line.write(abData, 0, nRead); + } + } + } catch (IOException e) { + logger.error("Error while playing audio: {}", e.getMessage()); + return; + } finally { + line.drain(); + line.close(); + try { + audioStream.close(); + } catch (IOException e) { + } + } + } + + /** + * Converts a org.eclipse.smarthome.core.audio.AudioFormat + * to a javax.sound.sampled.AudioFormat + * + * @param audioFormat The AudioFormat to convert + * @return The corresponding AudioFormat + */ + protected AudioFormat convertAudioFormat(org.eclipse.smarthome.core.audio.AudioFormat audioFormat) { + AudioFormat.Encoding encoding = new AudioFormat.Encoding(audioFormat.getCodec()); + if (audioFormat.getCodec().equals(org.eclipse.smarthome.core.audio.AudioFormat.CODEC_PCM_SIGNED)) { + encoding = AudioFormat.Encoding.PCM_SIGNED; + } else if (audioFormat.getCodec().equals(org.eclipse.smarthome.core.audio.AudioFormat.CODEC_PCM_ULAW)) { + encoding = AudioFormat.Encoding.ULAW; + } else if (audioFormat.getCodec().equals(org.eclipse.smarthome.core.audio.AudioFormat.CODEC_PCM_ALAW)) { + encoding = AudioFormat.Encoding.ALAW; + } + float sampleRate = audioFormat.getFrequency().floatValue(); + int sampleSizeInBits = audioFormat.getBitDepth().intValue(); + int channels = 1; // TODO: Is this always true? + int frameSize = audioFormat.getBitDepth().intValue() / 8; // As channels == 1, thus sampleSizeInBits == + // frameSize + float frameRate = audioFormat.getFrequency().floatValue(); + boolean bigEndian = audioFormat.isBigEndian().booleanValue(); + + return new AudioFormat(encoding, sampleRate, sampleSizeInBits, channels, frameSize, frameRate, bigEndian); + } +} diff --git a/extensions/io/org.eclipse.smarthome.io.javasound/src/main/java/org/eclipse/smarthome/io/javasound/internal/JavaSoundAudioSink.java b/extensions/io/org.eclipse.smarthome.io.javasound/src/main/java/org/eclipse/smarthome/io/javasound/internal/JavaSoundAudioSink.java new file mode 100644 index 00000000000..1d072833942 --- /dev/null +++ b/extensions/io/org.eclipse.smarthome.io.javasound/src/main/java/org/eclipse/smarthome/io/javasound/internal/JavaSoundAudioSink.java @@ -0,0 +1,60 @@ +/** + * Copyright (c) 2014-2016 by the respective copyright holders. + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v1.0 + * which accompanies this distribution, and is available at + * http://www.eclipse.org/legal/epl-v10.html + */ +package org.eclipse.smarthome.io.javasound.internal; + +import java.util.Collections; +import java.util.Locale; +import java.util.Set; + +import org.eclipse.smarthome.core.audio.AudioFormat; +import org.eclipse.smarthome.core.audio.AudioSink; +import org.eclipse.smarthome.core.audio.AudioStream; +import org.eclipse.smarthome.core.audio.UnsupportedAudioFormatException; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * This is an audio sink that is registered as a service, which can play wave files to the hosts outputs (e.g. speaker, + * line-out). + * + * @author Kai Kreuzer - Initial contribution and API + */ +public class JavaSoundAudioSink implements AudioSink { + + private final Logger logger = LoggerFactory.getLogger(JavaSoundAudioSink.class); + + @Override + public void process(AudioStream audioStream) throws UnsupportedAudioFormatException { + AudioPlayer audioPlayer = new AudioPlayer(audioStream); + audioPlayer.start(); + try { + audioPlayer.join(); + } catch (InterruptedException e) { + logger.error("Playing audio has been interrupted."); + } + } + + @Override + public Set getSupportedFormats() { + // we accept anything that is WAVE with signed PCM codec + AudioFormat format = new AudioFormat(AudioFormat.CONTAINER_WAVE, AudioFormat.CODEC_PCM_SIGNED, null, null, null, + null); + return Collections.singleton(format); + } + + @Override + public String getId() { + return "javasound"; + } + + @Override + public String getLabel(Locale locale) { + return "System Speaker"; + } + +} diff --git a/extensions/io/org.eclipse.smarthome.io.javasound/src/main/java/org/eclipse/smarthome/io/javasound/internal/JavaSoundAudioSource.java b/extensions/io/org.eclipse.smarthome.io.javasound/src/main/java/org/eclipse/smarthome/io/javasound/internal/JavaSoundAudioSource.java new file mode 100644 index 00000000000..01eb9f87695 --- /dev/null +++ b/extensions/io/org.eclipse.smarthome.io.javasound/src/main/java/org/eclipse/smarthome/io/javasound/internal/JavaSoundAudioSource.java @@ -0,0 +1,128 @@ +/** + * Copyright (c) 2014-2016 by the respective copyright holders. + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v1.0 + * which accompanies this distribution, and is available at + * http://www.eclipse.org/legal/epl-v10.html + */ +package org.eclipse.smarthome.io.javasound.internal; +/** + * Copyright (c) 2014-2016 openHAB UG (haftungsbeschraenkt) and others. + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v1.0 + * which accompanies this distribution, and is available at + * http://www.eclipse.org/legal/epl-v10.html + */ + +import java.util.Collections; +import java.util.Locale; +import java.util.Set; + +import javax.sound.sampled.AudioSystem; +import javax.sound.sampled.DataLine; +import javax.sound.sampled.TargetDataLine; + +import org.eclipse.smarthome.core.audio.AudioException; +import org.eclipse.smarthome.core.audio.AudioFormat; +import org.eclipse.smarthome.core.audio.AudioSource; +import org.eclipse.smarthome.core.audio.AudioStream; + +/** + * This is an AudioSource from an input channel of the host. + * + * @author Kelly Davis - Initial contribution and API + * @author Kai Kreuzer - Refactored and stabilized + * + */ +public class JavaSoundAudioSource implements AudioSource { + + /** + * TargetDataLine for the mic + */ + private final TargetDataLine microphone; + + /** + * AudioFormat of the JavaSoundAudioSource + */ + private final AudioFormat audioFormat; + + /** + * Constructs a JavaSoundAudioSource with the passed microphone and AudioFormat + * + * @param microphone The mic which data is pulled from + * @param audioFormat The AudioFormat of this JavaSoundAudioSource + */ + public JavaSoundAudioSource() { + TargetDataLine microphone; + javax.sound.sampled.AudioFormat format = new javax.sound.sampled.AudioFormat(16000.0f, 16, 1, true, false); + try { + microphone = AudioSystem.getTargetDataLine(format); + + DataLine.Info info = new DataLine.Info(TargetDataLine.class, format); + microphone = (TargetDataLine) AudioSystem.getLine(info); + + microphone.open(format); + + this.microphone = microphone; + this.audioFormat = convertAudioFormat(format); + } catch (Exception e) { + throw new RuntimeException("Error creating the AudioSource", e); + } + } + + /** + * {@inheritDoc} + */ + @Override + public AudioStream getInputStream() throws AudioException { + return new JavaSoundInputStream(this.microphone, audioFormat); + } + + @Override + public String toString() { + return "javasound"; + } + + /** + * Converts a javax.sound.sampled.AudioFormat to a org.eclipse.smarthome.core.audio.AudioFormat + * + * @param audioFormat the AudioFormat to convert + * @return The converted AudioFormat + */ + AudioFormat convertAudioFormat(javax.sound.sampled.AudioFormat audioFormat) { + String container = AudioFormat.CONTAINER_WAVE; + + String codec = audioFormat.getEncoding().toString(); + + Boolean bigEndian = new Boolean(audioFormat.isBigEndian()); + + int frameSize = audioFormat.getFrameSize(); // In bytes + int bitsPerFrame = frameSize * 8; + Integer bitDepth = ((AudioSystem.NOT_SPECIFIED == frameSize) ? null : new Integer(bitsPerFrame)); + + float frameRate = audioFormat.getFrameRate(); + Integer bitRate = ((AudioSystem.NOT_SPECIFIED == frameRate) ? null + : new Integer((int) (frameRate * bitsPerFrame))); + + float sampleRate = audioFormat.getSampleRate(); + Long frequency = ((AudioSystem.NOT_SPECIFIED == sampleRate) ? null : new Long((long) sampleRate)); + + return new AudioFormat(container, codec, bigEndian, bitDepth, bitRate, frequency); + } + + @Override + public String getId() { + return "javasound"; + } + + @Override + public String getLabel(Locale locale) { + return "System Microphone"; + } + + @Override + public Set getSupportedFormats() { + return Collections.singleton(audioFormat); + } + +} diff --git a/extensions/io/org.eclipse.smarthome.io.javasound/src/main/java/org/eclipse/smarthome/io/javasound/internal/JavaSoundInputStream.java b/extensions/io/org.eclipse.smarthome.io.javasound/src/main/java/org/eclipse/smarthome/io/javasound/internal/JavaSoundInputStream.java new file mode 100644 index 00000000000..3537357dc06 --- /dev/null +++ b/extensions/io/org.eclipse.smarthome.io.javasound/src/main/java/org/eclipse/smarthome/io/javasound/internal/JavaSoundInputStream.java @@ -0,0 +1,85 @@ +/** + * Copyright (c) 2014-2016 by the respective copyright holders. + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v1.0 + * which accompanies this distribution, and is available at + * http://www.eclipse.org/legal/epl-v10.html + */ +package org.eclipse.smarthome.io.javasound.internal; +/** + * Copyright (c) 2014-2016 openHAB UG (haftungsbeschraenkt) and others. + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v1.0 + * which accompanies this distribution, and is available at + * http://www.eclipse.org/legal/epl-v10.html + */ + +import java.io.IOException; + +import javax.sound.sampled.TargetDataLine; + +import org.eclipse.smarthome.core.audio.AudioFormat; +import org.eclipse.smarthome.core.audio.AudioStream; + +/** + * This is an AudioStream from a Java sound API input + * + * @author Kai Kreuzer - Initial contribution and API + * + */ +public class JavaSoundInputStream extends AudioStream { + + /** + * TargetDataLine for the input + */ + private final TargetDataLine input; + private final AudioFormat format; + + /** + * Constructs a JavaSoundInputStream with the passed input + * + * @param input The mic which data is pulled from + */ + public JavaSoundInputStream(TargetDataLine input, AudioFormat format) { + this.format = format; + this.input = input; + this.input.start(); + } + + /** + * {@inheritDoc} + */ + @Override + public int read() throws IOException { + byte[] b = new byte[1]; + + int bytesRead = read(b); + + if (-1 == bytesRead) { + return bytesRead; + } + + Byte bb = new Byte(b[0]); + return bb.intValue(); + } + + @Override + public int read(byte[] b) throws IOException { + return input.read(b, 0, b.length); + } + + @Override + public int read(byte[] b, int off, int len) throws IOException { + return input.read(b, off, len); + } + + @Override + public void close() throws IOException { + input.close(); + } + + @Override + public AudioFormat getFormat() { + return format; + } +} diff --git a/extensions/io/pom.xml b/extensions/io/pom.xml new file mode 100644 index 00000000000..e83bacbe4e4 --- /dev/null +++ b/extensions/io/pom.xml @@ -0,0 +1,22 @@ + + + + + org.eclipse.smarthome.extension + pom + 0.9.0-SNAPSHOT + + + 4.0.0 + org.eclipse.smarthome.io + pom + + Eclipse SmartHome I/O Services + + pom + + + org.eclipse.smarthome.io.javasound + + + diff --git a/extensions/pom.xml b/extensions/pom.xml index 9773ad03cd1..48428ecaaaf 100644 --- a/extensions/pom.xml +++ b/extensions/pom.xml @@ -17,6 +17,7 @@ binding + io transform ui voice diff --git a/extensions/voice/org.eclipse.smarthome.voice.mactts.test/.classpath b/extensions/voice/org.eclipse.smarthome.voice.mactts.test/.classpath new file mode 100644 index 00000000000..3d65768cc2b --- /dev/null +++ b/extensions/voice/org.eclipse.smarthome.voice.mactts.test/.classpath @@ -0,0 +1,8 @@ + + + + + + + + diff --git a/extensions/voice/org.eclipse.smarthome.voice.mactts.test/.project b/extensions/voice/org.eclipse.smarthome.voice.mactts.test/.project new file mode 100644 index 00000000000..e48189bb6b5 --- /dev/null +++ b/extensions/voice/org.eclipse.smarthome.voice.mactts.test/.project @@ -0,0 +1,33 @@ + + + org.eclipse.smarthome.voice.mactts.test + + + + + + org.eclipse.jdt.core.javabuilder + + + + + org.eclipse.pde.ManifestBuilder + + + + + org.eclipse.pde.SchemaBuilder + + + + + org.eclipse.pde.ds.core.builder + + + + + + org.eclipse.pde.PluginNature + org.eclipse.jdt.core.javanature + + diff --git a/extensions/voice/org.eclipse.smarthome.voice.mactts.test/.settings/org.eclipse.jdt.core.prefs b/extensions/voice/org.eclipse.smarthome.voice.mactts.test/.settings/org.eclipse.jdt.core.prefs new file mode 100644 index 00000000000..f42de363afa --- /dev/null +++ b/extensions/voice/org.eclipse.smarthome.voice.mactts.test/.settings/org.eclipse.jdt.core.prefs @@ -0,0 +1,7 @@ +eclipse.preferences.version=1 +org.eclipse.jdt.core.compiler.codegen.inlineJsrBytecode=enabled +org.eclipse.jdt.core.compiler.codegen.targetPlatform=1.7 +org.eclipse.jdt.core.compiler.compliance=1.7 +org.eclipse.jdt.core.compiler.problem.assertIdentifier=error +org.eclipse.jdt.core.compiler.problem.enumIdentifier=error +org.eclipse.jdt.core.compiler.source=1.7 diff --git a/extensions/voice/org.eclipse.smarthome.voice.mactts.test/.settings/org.eclipse.pde.core.prefs b/extensions/voice/org.eclipse.smarthome.voice.mactts.test/.settings/org.eclipse.pde.core.prefs new file mode 100644 index 00000000000..e67024ad352 --- /dev/null +++ b/extensions/voice/org.eclipse.smarthome.voice.mactts.test/.settings/org.eclipse.pde.core.prefs @@ -0,0 +1,4 @@ +#Mon Oct 11 21:08:09 CEST 2010 +eclipse.preferences.version=1 +pluginProject.extensions=false +resolve.requirebundle=false diff --git a/extensions/voice/org.eclipse.smarthome.voice.mactts.test/META-INF/MANIFEST.MF b/extensions/voice/org.eclipse.smarthome.voice.mactts.test/META-INF/MANIFEST.MF new file mode 100644 index 00000000000..93d0ed3ebda --- /dev/null +++ b/extensions/voice/org.eclipse.smarthome.voice.mactts.test/META-INF/MANIFEST.MF @@ -0,0 +1,13 @@ +Manifest-Version: 1.0 +Bundle-ManifestVersion: 2 +Bundle-Name: Eclipse SmartHome Mac TTS Tests +Bundle-SymbolicName: org.eclipse.smarthome.voice.mactts.test +Bundle-Version: 0.9.0.qualifier +Bundle-Vendor: Eclipse.org/SmartHome +Fragment-Host: org.eclipse.smarthome.voice.mactts +Bundle-RequiredExecutionEnvironment: JavaSE-1.7 +Import-Package: org.eclipse.smarthome.core.audio, + org.eclipse.smarthome.core.voice, + org.hamcrest.core, + org.junit;version="4.0.0" +Bundle-ClassPath: . diff --git a/extensions/voice/org.eclipse.smarthome.voice.mactts.test/about.html b/extensions/voice/org.eclipse.smarthome.voice.mactts.test/about.html new file mode 100644 index 00000000000..c258ef55d83 --- /dev/null +++ b/extensions/voice/org.eclipse.smarthome.voice.mactts.test/about.html @@ -0,0 +1,28 @@ + + + + +About + + +

About This Content

+ +

June 5, 2006

+

License

+ +

The Eclipse Foundation makes available all content in this plug-in ("Content"). Unless otherwise +indicated below, the Content is provided to you under the terms and conditions of the +Eclipse Public License Version 1.0 ("EPL"). A copy of the EPL is available +at http://www.eclipse.org/legal/epl-v10.html. +For purposes of the EPL, "Program" will mean the Content.

+ +

If you did not receive this Content directly from the Eclipse Foundation, the Content is +being redistributed by another party ("Redistributor") and different terms and conditions may +apply to your use of any object code in the Content. Check the Redistributor's license that was +provided with the Content. If no such license exists, contact the Redistributor. Unless otherwise +indicated below, the terms and conditions of the EPL still apply to any source code in the Content +and such source code may be obtained at http://www.eclipse.org.

+ + + \ No newline at end of file diff --git a/extensions/voice/org.eclipse.smarthome.voice.mactts.test/build.properties b/extensions/voice/org.eclipse.smarthome.voice.mactts.test/build.properties new file mode 100644 index 00000000000..df0687569fb --- /dev/null +++ b/extensions/voice/org.eclipse.smarthome.voice.mactts.test/build.properties @@ -0,0 +1,5 @@ +source.. = src/test/java/ +output.. = target/test-classes/ +bin.includes = META-INF/,\ + .,\ + about.html diff --git a/extensions/voice/org.eclipse.smarthome.voice.mactts.test/pom.xml b/extensions/voice/org.eclipse.smarthome.voice.mactts.test/pom.xml new file mode 100644 index 00000000000..7ba04b452f3 --- /dev/null +++ b/extensions/voice/org.eclipse.smarthome.voice.mactts.test/pom.xml @@ -0,0 +1,40 @@ + + + + + org.eclipse.smarthome.voice + pom + 0.9.0-SNAPSHOT + + + + org.eclipse.smarthome.voice.mactts.test + org.eclipse.smarthome.voice.mactts.test + + + 4.0.0 + org.eclipse.smarthome.voice + org.eclipse.smarthome.voice.mactts.test + + Eclipse SmartHome Mac TTS Tests + + eclipse-test-plugin + + + + + org.vafer + jdeb + 1.5 + + + ${tycho-groupid} + tycho-surefire-plugin + + -Djava.library.path=${project.basedir}/lib + + + + + + diff --git a/extensions/voice/org.eclipse.smarthome.voice.mactts.test/src/test/java/org/eclipse/smarthome/voice/mactts/internal/MacTTSVoiceTest.java b/extensions/voice/org.eclipse.smarthome.voice.mactts.test/src/test/java/org/eclipse/smarthome/voice/mactts/internal/MacTTSVoiceTest.java new file mode 100644 index 00000000000..aa101b4db0b --- /dev/null +++ b/extensions/voice/org.eclipse.smarthome.voice.mactts.test/src/test/java/org/eclipse/smarthome/voice/mactts/internal/MacTTSVoiceTest.java @@ -0,0 +1,105 @@ +/** + * Copyright (c) 2014-2016 by the respective copyright holders. + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v1.0 + * which accompanies this distribution, and is available at + * http://www.eclipse.org/legal/epl-v10.html + */ +package org.eclipse.smarthome.voice.mactts.internal; + +import java.io.BufferedReader; +import java.io.IOException; +import java.io.InputStreamReader; + +import org.junit.Assert; +import org.junit.Assume; +import org.junit.Test; + +/** + * Test MacTTSVoice + * + * @author Kelly Davis - Initial contribution and API + */ +public class MacTTSVoiceTest { + + /** + * Test MacTTSVoice(String) constructor + */ + @Test + public void testConstructor() { + Assume.assumeTrue("Mac OS X" == System.getProperty("os.name")); + + try { + Process process = Runtime.getRuntime().exec("say -v ?"); + InputStreamReader inputStreamReader = new InputStreamReader(process.getInputStream()); + BufferedReader bufferedReader = new BufferedReader(inputStreamReader); + + String nextLine = bufferedReader.readLine(); + MacTTSVoice voiceMacOS = new MacTTSVoice(nextLine); + Assert.assertNotNull("The MacTTSVoice(String) constructor failed", voiceMacOS); + } catch (IOException e) { + Assert.fail("testConstructor() failed with IOException: " + e.getMessage()); + } + } + + /** + * Test VoiceMacOS.getUID() + */ + @Test + public void getUIDTest() { + Assume.assumeTrue("Mac OS X" == System.getProperty("os.name")); + + try { + Process process = Runtime.getRuntime().exec("say -v ?"); + InputStreamReader inputStreamReader = new InputStreamReader(process.getInputStream()); + BufferedReader bufferedReader = new BufferedReader(inputStreamReader); + + String nextLine = bufferedReader.readLine(); + MacTTSVoice macTTSVoice = new MacTTSVoice(nextLine); + Assert.assertTrue("The VoiceMacOS UID has an incorrect format", + (0 == macTTSVoice.getUID().indexOf("mactts:"))); + } catch (IOException e) { + Assert.fail("getUIDTest() failed with IOException: " + e.getMessage()); + } + } + + /** + * Test MacTTSVoice.getLabel() + */ + @Test + public void getLabelTest() { + Assume.assumeTrue("Mac OS X" == System.getProperty("os.name")); + + try { + Process process = Runtime.getRuntime().exec("say -v ?"); + InputStreamReader inputStreamReader = new InputStreamReader(process.getInputStream()); + BufferedReader bufferedReader = new BufferedReader(inputStreamReader); + + String nextLine = bufferedReader.readLine(); + MacTTSVoice voiceMacOS = new MacTTSVoice(nextLine); + Assert.assertNotNull("The MacTTSVoice label has an incorrect format", voiceMacOS.getLabel()); + } catch (IOException e) { + Assert.fail("getLabelTest() failed with IOException: " + e.getMessage()); + } + } + + /** + * Test MacTTSVoice.getLocale() + */ + @Test + public void getLocaleTest() { + Assume.assumeTrue("Mac OS X" == System.getProperty("os.name")); + + try { + Process process = Runtime.getRuntime().exec("say -v ?"); + InputStreamReader inputStreamReader = new InputStreamReader(process.getInputStream()); + BufferedReader bufferedReader = new BufferedReader(inputStreamReader); + + String nextLine = bufferedReader.readLine(); + MacTTSVoice voiceMacOS = new MacTTSVoice(nextLine); + Assert.assertNotNull("The MacTTSVoice locale has an incorrect format", voiceMacOS.getLocale()); + } catch (IOException e) { + Assert.fail("getLocaleTest() failed with IOException: " + e.getMessage()); + } + } +} diff --git a/extensions/voice/org.eclipse.smarthome.voice.mactts.test/src/test/java/org/eclipse/smarthome/voice/mactts/internal/TTSServiceMacOSTest.java b/extensions/voice/org.eclipse.smarthome.voice.mactts.test/src/test/java/org/eclipse/smarthome/voice/mactts/internal/TTSServiceMacOSTest.java new file mode 100644 index 00000000000..378357da27c --- /dev/null +++ b/extensions/voice/org.eclipse.smarthome.voice.mactts.test/src/test/java/org/eclipse/smarthome/voice/mactts/internal/TTSServiceMacOSTest.java @@ -0,0 +1,74 @@ +/** + * Copyright (c) 2014-2016 by the respective copyright holders. + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v1.0 + * which accompanies this distribution, and is available at + * http://www.eclipse.org/legal/epl-v10.html + */ +package org.eclipse.smarthome.voice.mactts.internal; + +import java.io.IOException; +import java.util.Set; + +import org.eclipse.smarthome.core.audio.AudioFormat; +import org.eclipse.smarthome.core.audio.AudioStream; +import org.eclipse.smarthome.core.voice.TTSException; +import org.eclipse.smarthome.core.voice.Voice; +import org.junit.Assert; +import org.junit.Assume; +import org.junit.Test; + +/** + * Test TTSServiceMacOS + * + * @author Kelly Davis - Initial contribution and API + */ +public class TTSServiceMacOSTest { + + /** + * Test TTSServiceMacOS.getAvailableVoices() + */ + @Test + public void getAvailableVoicesTest() { + Assume.assumeTrue("Mac OS X".equals(System.getProperty("os.name"))); + + MacTTSService ttsServiceMacOS = new MacTTSService(); + Assert.assertFalse("The getAvailableVoicesTest() failed", ttsServiceMacOS.getAvailableVoices().isEmpty()); + } + + /** + * Test TTSServiceMacOS.getSupportedFormats() + */ + @Test + public void getSupportedFormatsTest() { + Assume.assumeTrue("Mac OS X".equals(System.getProperty("os.name"))); + + MacTTSService ttsServiceMacOS = new MacTTSService(); + Assert.assertFalse("The getSupportedFormatsTest() failed", ttsServiceMacOS.getSupportedFormats().isEmpty()); + } + + /** + * Test TTSServiceMacOS.synthesize(String,Voice,AudioFormat) + */ + @Test + public void synthesizeTest() { + Assume.assumeTrue("Mac OS X".equals(System.getProperty("os.name"))); + + MacTTSService ttsServiceMacOS = new MacTTSService(); + Set voices = ttsServiceMacOS.getAvailableVoices(); + Set audioFormats = ttsServiceMacOS.getSupportedFormats(); + try (AudioStream audioStream = ttsServiceMacOS.synthesize("Hello", voices.iterator().next(), + audioFormats.iterator().next())) { + Assert.assertNotNull("The test synthesizeTest() created null AudioSource", audioStream); + Assert.assertNotNull("The test synthesizeTest() created an AudioSource w/o AudioFormat", + audioStream.getFormat()); + Assert.assertNotNull("The test synthesizeTest() created an AudioSource w/o InputStream", audioStream); + Assert.assertTrue("The test synthesizeTest() returned an AudioSource with no data", + (-1 != audioStream.read(new byte[2]))); + } catch (TTSException e) { + Assert.fail("synthesizeTest() failed with TTSException: " + e.getMessage()); + } catch (IOException e) { + Assert.fail("synthesizeTest() failed with IOException: " + e.getMessage()); + } + } +} diff --git a/extensions/voice/org.eclipse.smarthome.voice.mactts/META-INF/MANIFEST.MF b/extensions/voice/org.eclipse.smarthome.voice.mactts/META-INF/MANIFEST.MF index a95e9f6c1fc..4343bf7d9a1 100644 --- a/extensions/voice/org.eclipse.smarthome.voice.mactts/META-INF/MANIFEST.MF +++ b/extensions/voice/org.eclipse.smarthome.voice.mactts/META-INF/MANIFEST.MF @@ -6,6 +6,8 @@ Bundle-Vendor: Eclipse.org/SmartHome Bundle-Version: 0.9.0.qualifier Bundle-RequiredExecutionEnvironment: JavaSE-1.7 Bundle-ClassPath: . -Import-Package: org.eclipse.smarthome.io.voice.tts, +Import-Package: org.eclipse.smarthome.core.voice, + org.eclipse.smarthome.core.audio, org.slf4j Service-Component: OSGI-INF/*.xml +Bundle-ActivationPolicy: lazy diff --git a/extensions/voice/org.eclipse.smarthome.voice.mactts/OSGI-INF/TTSServiceMacOS.xml b/extensions/voice/org.eclipse.smarthome.voice.mactts/OSGI-INF/MacTTSService.xml similarity index 83% rename from extensions/voice/org.eclipse.smarthome.voice.mactts/OSGI-INF/TTSServiceMacOS.xml rename to extensions/voice/org.eclipse.smarthome.voice.mactts/OSGI-INF/MacTTSService.xml index 0b328cca96d..0f678fd2bdf 100644 --- a/extensions/voice/org.eclipse.smarthome.voice.mactts/OSGI-INF/TTSServiceMacOS.xml +++ b/extensions/voice/org.eclipse.smarthome.voice.mactts/OSGI-INF/MacTTSService.xml @@ -9,8 +9,8 @@ --> - + - + diff --git a/extensions/voice/org.eclipse.smarthome.voice.mactts/about.html b/extensions/voice/org.eclipse.smarthome.voice.mactts/about.html new file mode 100644 index 00000000000..1b9e722c419 --- /dev/null +++ b/extensions/voice/org.eclipse.smarthome.voice.mactts/about.html @@ -0,0 +1,28 @@ + + + + +About + + +

About This Content

+ +

<May 12, 2015>

+

License

+ +

The Eclipse Foundation makes available all content in this plug-in ("Content"). Unless otherwise +indicated below, the Content is provided to you under the terms and conditions of the +Eclipse Public License Version 1.0 ("EPL"). A copy of the EPL is available +at http://www.eclipse.org/legal/epl-v10.html. +For purposes of the EPL, "Program" will mean the Content.

+ +

If you did not receive this Content directly from the Eclipse Foundation, the Content is +being redistributed by another party ("Redistributor") and different terms and conditions may +apply to your use of any object code in the Content. Check the Redistributor's license that was +provided with the Content. If no such license exists, contact the Redistributor. Unless otherwise +indicated below, the terms and conditions of the EPL still apply to any source code in the Content +and such source code may be obtained at http://www.eclipse.org.

+ + + diff --git a/extensions/voice/org.eclipse.smarthome.voice.mactts/build.properties b/extensions/voice/org.eclipse.smarthome.voice.mactts/build.properties index e32be345162..6625e6ddfd2 100644 --- a/extensions/voice/org.eclipse.smarthome.voice.mactts/build.properties +++ b/extensions/voice/org.eclipse.smarthome.voice.mactts/build.properties @@ -2,4 +2,5 @@ source.. = src/main/java/ output.. = target/classes bin.includes = META-INF/,\ .,\ - OSGI-INF/ + OSGI-INF/,\ + about.html diff --git a/extensions/voice/org.eclipse.smarthome.voice.mactts/src/main/java/org/eclipse/smarthome/voice/mactts/internal/MacTTSAudioStream.java b/extensions/voice/org.eclipse.smarthome.voice.mactts/src/main/java/org/eclipse/smarthome/voice/mactts/internal/MacTTSAudioStream.java new file mode 100644 index 00000000000..0e73f2dfab3 --- /dev/null +++ b/extensions/voice/org.eclipse.smarthome.voice.mactts/src/main/java/org/eclipse/smarthome/voice/mactts/internal/MacTTSAudioStream.java @@ -0,0 +1,131 @@ +/** + * Copyright (c) 2014-2016 by the respective copyright holders. + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v1.0 + * which accompanies this distribution, and is available at + * http://www.eclipse.org/legal/epl-v10.html + */ +package org.eclipse.smarthome.voice.mactts.internal; + +import java.io.File; +import java.io.FileInputStream; +import java.io.IOException; +import java.io.InputStream; + +import org.eclipse.smarthome.core.audio.AudioException; +import org.eclipse.smarthome.core.audio.AudioFormat; +import org.eclipse.smarthome.core.audio.AudioStream; +import org.eclipse.smarthome.core.voice.Voice; + +/** + * Implementation of the {@link AudioStream} interface for the {@link MacTTSService} + * + * @author Kelly Davis - Initial contribution and API + * @author Kai Kreuzer - Refactored to use AudioStream and fixed audio format to produce + */ +class MacTTSAudioStream extends AudioStream { + + /** + * {@link Voice} this {@link AudioStream} speaks in + */ + private final Voice voice; + + /** + * Text spoken in this {@link AudioStream} + */ + private final String text; + + /** + * {@link AudioFormat} of this {@link AudioStream} + */ + private final AudioFormat audioFormat; + + /** + * The raw input stream + */ + private InputStream inputStream; + + /** + * Constructs an instance with the passed properties. + * + * It is assumed that the passed properties have been validated. + * + * @param text The text spoken in this {@link AudioStream} + * @param voice The {@link Voice} used to speak this instance's text + * @param audioFormat The {@link AudioFormat} of this {@link AudioStream} + * @throws AudioException if stream cannot be created + */ + public MacTTSAudioStream(String text, Voice voice, AudioFormat audioFormat) throws AudioException { + this.text = text; + this.voice = voice; + this.audioFormat = audioFormat; + this.inputStream = createInputStream(); + } + + @Override + public AudioFormat getFormat() { + return audioFormat; + } + + private InputStream createInputStream() throws AudioException { + String outputFile = generateOutputFilename(); + String command = getCommand(outputFile); + + try { + Process process = Runtime.getRuntime().exec(command); + process.waitFor(); + File file = new File(outputFile); + if (file.exists()) { + return new FileInputStream(file); + } else { + throw new AudioException("Temporary file '" + outputFile + "' not found!"); + } + } catch (IOException e) { + throw new AudioException("Error while executing '" + command + "'", e); + } catch (InterruptedException e) { + throw new AudioException("The '" + command + "' has been interrupted", e); + } + } + + /** + * Generates a unique, absolute output filename + * + * @return Unique, absolute output filename + */ + private String generateOutputFilename() throws AudioException { + File tempFile; + try { + tempFile = File.createTempFile(Integer.toString(text.hashCode()), ".wav"); + tempFile.deleteOnExit(); + } catch (IOException e) { + throw new AudioException("Unable to create temp file.", e); + } + return tempFile.getAbsolutePath(); + } + + /** + * Gets the command used to generate an audio file {@code outputFile} + * + * @param outputFile The absolute filename of the command's output + * @return The command used to generate the audio file {@code outputFile} + */ + private String getCommand(String outputFile) { + StringBuffer stringBuffer = new StringBuffer(); + + stringBuffer.append("say"); + + stringBuffer.append(" --voice=" + this.voice.getLabel()); + stringBuffer.append(" --output-file=" + outputFile); + stringBuffer.append(" --file-format=" + this.audioFormat.getContainer()); + stringBuffer.append(" --data-format=LEI16@16384"); + stringBuffer.append(" --channels=1"); // Mono + stringBuffer.append(" " + this.text); + + return stringBuffer.toString(); + } + + @Override + public int read() throws IOException { + return inputStream.read(); + } +} diff --git a/extensions/voice/org.eclipse.smarthome.voice.mactts/src/main/java/org/eclipse/smarthome/voice/mactts/internal/MacTTSService.java b/extensions/voice/org.eclipse.smarthome.voice.mactts/src/main/java/org/eclipse/smarthome/voice/mactts/internal/MacTTSService.java new file mode 100644 index 00000000000..dc9bdab75ce --- /dev/null +++ b/extensions/voice/org.eclipse.smarthome.voice.mactts/src/main/java/org/eclipse/smarthome/voice/mactts/internal/MacTTSService.java @@ -0,0 +1,128 @@ +/** + * Copyright (c) 2014-2016 by the respective copyright holders. + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v1.0 + * which accompanies this distribution, and is available at + * http://www.eclipse.org/legal/epl-v10.html + */ +package org.eclipse.smarthome.voice.mactts.internal; + +import java.io.BufferedReader; +import java.io.IOException; +import java.io.InputStreamReader; +import java.util.Collections; +import java.util.HashSet; +import java.util.Locale; +import java.util.Set; + +import org.eclipse.smarthome.core.audio.AudioException; +import org.eclipse.smarthome.core.audio.AudioFormat; +import org.eclipse.smarthome.core.audio.AudioStream; +import org.eclipse.smarthome.core.voice.TTSException; +import org.eclipse.smarthome.core.voice.TTSService; +import org.eclipse.smarthome.core.voice.Voice; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * This is a TTS service implementation for Mac OS, which simply uses the "say" command from the OS. + * + * @author Kai Kreuzer - Initial contribution and API + * @author Pauli Antilla + * @author Kelly Davis + */ +public class MacTTSService implements TTSService { + + private final Logger logger = LoggerFactory.getLogger(MacTTSService.class); + + /** + * Set of supported voices + */ + private final Set voices = initVoices(); + + /** + * Set of supported audio formats + */ + private final Set audioFormats = initAudioFormats(); + + @Override + public Set getAvailableVoices() { + return this.voices; + } + + @Override + public Set getSupportedFormats() { + return this.audioFormats; + } + + @Override + public AudioStream synthesize(String text, Voice voice, AudioFormat requestedFormat) throws TTSException { + // Validate arguments + if ((null == text) || text.isEmpty()) { + throw new TTSException("The passed text is null or empty"); + } + if (!this.voices.contains(voice)) { + throw new TTSException("The passed voice is unsupported"); + } + boolean isAudioFormatSupported = false; + for (AudioFormat currentAudioFormat : this.audioFormats) { + if (currentAudioFormat.isCompatible(requestedFormat)) { + isAudioFormatSupported = true; + break; + } + } + if (!isAudioFormatSupported) { + throw new TTSException("The passed AudioFormat is unsupported"); + } + + try { + return new MacTTSAudioStream(text, voice, requestedFormat); + } catch (AudioException e) { + throw new TTSException(e); + } + } + + /** + * Initializes this.voices + * + * @return The voices of this instance + */ + private final Set initVoices() { + Set voices = new HashSet(); + try { + Process process = Runtime.getRuntime().exec("say -v ?"); + InputStreamReader inputStreamReader = new InputStreamReader(process.getInputStream()); + BufferedReader bufferedReader = new BufferedReader(inputStreamReader); + + String nextLine; + while ((nextLine = bufferedReader.readLine()) != null) { + voices.add(new MacTTSVoice(nextLine)); + } + } catch (IOException e) { + logger.error("Error while executing the 'say -v ?' command: " + e.getMessage()); + } + return voices; + } + + /** + * Initializes this.audioFormats + * + * @return The audio formats of this instance + */ + private final Set initAudioFormats() { + AudioFormat audioFormat = new AudioFormat(AudioFormat.CONTAINER_WAVE, AudioFormat.CODEC_PCM_SIGNED, false, 16, + null, null); + return Collections.singleton(audioFormat); + } + + @Override + public String getId() { + return "mactts"; + } + + @Override + public String getLabel(Locale locale) { + return "MacOS TTS"; + } + +} diff --git a/extensions/voice/org.eclipse.smarthome.voice.mactts/src/main/java/org/eclipse/smarthome/voice/mactts/internal/MacTTSVoice.java b/extensions/voice/org.eclipse.smarthome.voice.mactts/src/main/java/org/eclipse/smarthome/voice/mactts/internal/MacTTSVoice.java new file mode 100644 index 00000000000..f439470d507 --- /dev/null +++ b/extensions/voice/org.eclipse.smarthome.voice.mactts/src/main/java/org/eclipse/smarthome/voice/mactts/internal/MacTTSVoice.java @@ -0,0 +1,139 @@ +/** + * Copyright (c) 2014-2016 by the respective copyright holders. + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v1.0 + * which accompanies this distribution, and is available at + * http://www.eclipse.org/legal/epl-v10.html + */ +package org.eclipse.smarthome.voice.mactts.internal; + +import java.util.Locale; +import java.util.StringTokenizer; + +import org.eclipse.smarthome.core.voice.Voice; + +/** + * Implementation of the Voice interface for MacOS + * + * @author Kelly Davis - Initial contribution and API + */ +public class MacTTSVoice implements Voice { + + /** + * Voice label + */ + private String label; + + /** + * Voice language (ISO 639 alpha-2) + */ + private String language; + + /** + * Voice country (ISO 3166 alpha-2) + */ + private String country; + + /** + * Voice variant + */ + private String variant; + + /** + * Constructs a MacTTSVoice instance corresponding to a single line + * returned from a call to the command + * + * {@code 'say -v ?'} + * + * For example, a single line from the call above could have the form + * + * {@code Agnes en_US # Isn't it nice to have a computer that will talk to you?} + * + * Generically, a single line from the call above has the form + * + * {@code