From 8945a4d156dc0b2215ef667be4ad4eda61b68e7d Mon Sep 17 00:00:00 2001 From: Kai Kreuzer Date: Tue, 19 Jul 2016 14:36:04 +0200 Subject: [PATCH] Refactored audio and voice support: - Created audio API in smarthome.core - Introduced smarthome.core.voice for TTS, STT and HLI APIs - Introduced smarthome.io.javasound for audio implementations based on Java Sound API - Introduced smarthome.voice.mactts as a Mac specific TTS implementation Also-By: Harald Kuhn Also-By: Kelly Davis Also-By: Tilman Kamp Signed-off-by: Kai Kreuzer --- .../.classpath | 8 + .../.project | 33 + .../.settings/org.eclipse.jdt.core.prefs | 0 .../.settings/org.eclipse.pde.core.prefs | 0 .../META-INF/MANIFEST.MF | 12 + .../about.html | 0 .../build.properties | 5 + .../pom.xml | 16 +- .../core/voice/STTExceptionTest.java | 55 ++ .../SpeechRecognitionErrorEventTest.java | 37 + .../voice/SpeechRecognitionEventTest.java | 47 + .../core/voice/TTSExceptionTest.java | 55 ++ .../smarthome/io/voice/internal/goforward.raw | Bin 0 -> 89160 bytes .../io/voice/internal/hellowworld.raw | Bin 0 -> 93622 bytes .../smarthome/io/voice/internal/marieta.raw | Bin 0 -> 122560 bytes .../.classpath | 1 + .../.project | 2 +- .../.settings/org.eclipse.jdt.core.prefs | 7 + .../.settings/org.eclipse.pde.core.prefs | 4 + .../META-INF/MANIFEST.MF | 15 +- .../OSGI-INF/StandardInterpreter.xml} | 6 +- .../VoiceConsoleCommandExtension.xml} | 5 +- .../OSGI-INF/VoiceManager.xml | 23 + .../about.html | 28 + .../build.properties | 7 + .../org.eclipse.smarthome.core.voice}/pom.xml | 13 +- .../smarthome/core/voice/AudioStartEvent.java | 16 + .../smarthome/core/voice/AudioStopEvent.java | 16 + .../smarthome/core/voice/KSErrorEvent.java | 38 + .../eclipse/smarthome/core/voice/KSEvent.java | 16 + .../smarthome/core/voice/KSException.java | 53 ++ .../smarthome/core/voice/KSListener.java | 27 + .../smarthome/core/voice/KSService.java | 81 ++ .../smarthome/core/voice/KSServiceHandle.java | 20 + .../smarthome/core/voice/KSpottedEvent.java | 44 + .../core/voice/RecognitionStartEvent.java | 16 + .../core/voice/RecognitionStopEvent.java | 16 + .../smarthome/core/voice/STTEvent.java | 16 + .../smarthome/core/voice/STTException.java | 53 ++ .../smarthome/core/voice/STTListener.java | 28 + .../smarthome/core/voice/STTService.java | 83 ++ .../core/voice/STTServiceHandle.java | 20 + .../voice/SpeechRecognitionErrorEvent.java | 38 + .../core/voice/SpeechRecognitionEvent.java | 69 ++ .../core/voice/SpeechStartEvent.java | 16 + .../smarthome/core/voice/SpeechStopEvent.java | 16 + .../smarthome/core/voice/TTSException.java | 53 ++ .../smarthome/core/voice/TTSService.java | 67 ++ .../eclipse/smarthome/core/voice/Voice.java | 40 + .../smarthome/core/voice/VoiceManager.java | 584 ++++++++++++ .../core/voice/internal/DialogProcessor.java | 164 ++++ .../VoiceConsoleCommandExtension.java | 144 +++ .../internal/text/StandardInterpreter.java | 132 +++ .../smarthome/core/voice/text/ASTNode.java | 213 +++++ .../text/AbstractRuleBasedInterpreter.java | 842 ++++++++++++++++++ .../smarthome/core/voice/text/Expression.java | 42 + .../voice/text/ExpressionAlternatives.java | 76 ++ .../voice/text/ExpressionCardinality.java | 101 +++ .../core/voice/text/ExpressionIdentifier.java | 86 ++ .../core/voice/text/ExpressionLet.java | 129 +++ .../core/voice/text/ExpressionMatch.java | 63 ++ .../core/voice/text/ExpressionSequence.java | 83 ++ .../voice/text/HumanLanguageInterpreter.java | 33 +- .../voice/text/InterpretationException.java | 7 +- .../core/voice/text/InterpretationResult.java | 91 ++ .../smarthome/core/voice/text/Rule.java | 55 ++ .../smarthome/core/voice/text/TokenList.java | 173 ++++ .../main/resources/LanguageSupport.properties | 18 + .../resources/LanguageSupport_de.properties | 18 + .../META-INF/MANIFEST.MF | 4 +- .../smarthome/core}/audio/AudioException.java | 2 +- .../smarthome/core/audio/AudioFormat.java | 272 ++++++ .../smarthome/core/audio/AudioSink.java | 53 ++ .../smarthome/core/audio/AudioSource.java | 52 ++ .../smarthome/core/audio/AudioStream.java | 33 + .../UnsupportedAudioFormatException.java | 2 +- bundles/core/pom.xml | 2 + .../META-INF/MANIFEST.MF | 11 - .../build.properties | 4 - .../META-INF/MANIFEST.MF | 1 + bundles/io/pom.xml | 2 - .../.classpath | 0 .../.project | 2 +- .../META-INF/MANIFEST.MF | 13 + .../OSGI-INF/javasoundsink.xml | 16 + .../OSGI-INF/javasoundsource.xml | 16 + .../about.html | 28 + .../build.properties | 2 +- .../pom.xml | 23 + .../io/javasound/internal/AudioPlayer.java | 123 +++ .../internal/JavaSoundAudioSink.java | 60 ++ .../internal/JavaSoundAudioSource.java | 128 +++ .../internal/JavaSoundInputStream.java | 85 ++ extensions/io/pom.xml | 22 + extensions/pom.xml | 1 + .../.classpath | 8 + .../.project | 33 + .../.settings/org.eclipse.jdt.core.prefs | 7 + .../.settings/org.eclipse.pde.core.prefs | 4 + .../META-INF/MANIFEST.MF | 13 + .../about.html | 28 + .../build.properties | 5 + .../pom.xml | 40 + .../mactts/internal/MacTTSVoiceTest.java | 105 +++ .../mactts/internal/TTSServiceMacOSTest.java | 74 ++ .../META-INF/MANIFEST.MF | 4 +- ...{TTSServiceMacOS.xml => MacTTSService.xml} | 4 +- .../about.html | 28 + .../build.properties | 3 +- .../mactts/internal/MacTTSAudioStream.java | 131 +++ .../voice/mactts/internal/MacTTSService.java | 128 +++ .../voice/mactts/internal/MacTTSVoice.java | 139 +++ .../mactts/internal/TTSServiceMacOS.java | 57 -- extensions/voice/pom.xml | 1 + features/karaf/src/main/feature/feature.xml | 8 +- .../feature.xml | 2 +- .../feature.xml | 9 +- 117 files changed, 5940 insertions(+), 123 deletions(-) create mode 100644 bundles/core/org.eclipse.smarthome.core.voice.test/.classpath create mode 100644 bundles/core/org.eclipse.smarthome.core.voice.test/.project rename bundles/{io/org.eclipse.smarthome.io.voice => core/org.eclipse.smarthome.core.voice.test}/.settings/org.eclipse.jdt.core.prefs (100%) rename bundles/{io/org.eclipse.smarthome.io.voice => core/org.eclipse.smarthome.core.voice.test}/.settings/org.eclipse.pde.core.prefs (100%) create mode 100644 bundles/core/org.eclipse.smarthome.core.voice.test/META-INF/MANIFEST.MF rename bundles/{io/org.eclipse.smarthome.io.voice => core/org.eclipse.smarthome.core.voice.test}/about.html (100%) create mode 100644 bundles/core/org.eclipse.smarthome.core.voice.test/build.properties rename bundles/{io/org.eclipse.smarthome.io.audio => core/org.eclipse.smarthome.core.voice.test}/pom.xml (57%) create mode 100644 bundles/core/org.eclipse.smarthome.core.voice.test/src/test/java/org/eclipse/smarthome/core/voice/STTExceptionTest.java create mode 100644 bundles/core/org.eclipse.smarthome.core.voice.test/src/test/java/org/eclipse/smarthome/core/voice/SpeechRecognitionErrorEventTest.java create mode 100644 bundles/core/org.eclipse.smarthome.core.voice.test/src/test/java/org/eclipse/smarthome/core/voice/SpeechRecognitionEventTest.java create mode 100644 bundles/core/org.eclipse.smarthome.core.voice.test/src/test/java/org/eclipse/smarthome/core/voice/TTSExceptionTest.java create mode 100644 bundles/core/org.eclipse.smarthome.core.voice.test/src/test/resources/org/eclipse/smarthome/io/voice/internal/goforward.raw create mode 100644 bundles/core/org.eclipse.smarthome.core.voice.test/src/test/resources/org/eclipse/smarthome/io/voice/internal/hellowworld.raw create mode 100644 bundles/core/org.eclipse.smarthome.core.voice.test/src/test/resources/org/eclipse/smarthome/io/voice/internal/marieta.raw rename bundles/{io/org.eclipse.smarthome.io.voice => core/org.eclipse.smarthome.core.voice}/.classpath (87%) rename bundles/{io/org.eclipse.smarthome.io.voice => core/org.eclipse.smarthome.core.voice}/.project (90%) create mode 100644 bundles/core/org.eclipse.smarthome.core.voice/.settings/org.eclipse.jdt.core.prefs create mode 100644 bundles/core/org.eclipse.smarthome.core.voice/.settings/org.eclipse.pde.core.prefs rename bundles/{io/org.eclipse.smarthome.io.voice => core/org.eclipse.smarthome.core.voice}/META-INF/MANIFEST.MF (64%) rename bundles/{io/org.eclipse.smarthome.io.voice/OSGI-INF/StandardHumanLanguageInterpreter.xml => core/org.eclipse.smarthome.core.voice/OSGI-INF/StandardInterpreter.xml} (75%) rename bundles/{io/org.eclipse.smarthome.io.voice/OSGI-INF/SayConsoleCommandExtension.xml => core/org.eclipse.smarthome.core.voice/OSGI-INF/VoiceConsoleCommandExtension.xml} (68%) create mode 100644 bundles/core/org.eclipse.smarthome.core.voice/OSGI-INF/VoiceManager.xml create mode 100644 bundles/core/org.eclipse.smarthome.core.voice/about.html create mode 100644 bundles/core/org.eclipse.smarthome.core.voice/build.properties rename bundles/{io/org.eclipse.smarthome.io.voice => core/org.eclipse.smarthome.core.voice}/pom.xml (58%) create mode 100644 bundles/core/org.eclipse.smarthome.core.voice/src/main/java/org/eclipse/smarthome/core/voice/AudioStartEvent.java create mode 100644 bundles/core/org.eclipse.smarthome.core.voice/src/main/java/org/eclipse/smarthome/core/voice/AudioStopEvent.java create mode 100644 bundles/core/org.eclipse.smarthome.core.voice/src/main/java/org/eclipse/smarthome/core/voice/KSErrorEvent.java create mode 100644 bundles/core/org.eclipse.smarthome.core.voice/src/main/java/org/eclipse/smarthome/core/voice/KSEvent.java create mode 100644 bundles/core/org.eclipse.smarthome.core.voice/src/main/java/org/eclipse/smarthome/core/voice/KSException.java create mode 100644 bundles/core/org.eclipse.smarthome.core.voice/src/main/java/org/eclipse/smarthome/core/voice/KSListener.java create mode 100644 bundles/core/org.eclipse.smarthome.core.voice/src/main/java/org/eclipse/smarthome/core/voice/KSService.java create mode 100644 bundles/core/org.eclipse.smarthome.core.voice/src/main/java/org/eclipse/smarthome/core/voice/KSServiceHandle.java create mode 100644 bundles/core/org.eclipse.smarthome.core.voice/src/main/java/org/eclipse/smarthome/core/voice/KSpottedEvent.java create mode 100644 bundles/core/org.eclipse.smarthome.core.voice/src/main/java/org/eclipse/smarthome/core/voice/RecognitionStartEvent.java create mode 100644 bundles/core/org.eclipse.smarthome.core.voice/src/main/java/org/eclipse/smarthome/core/voice/RecognitionStopEvent.java create mode 100644 bundles/core/org.eclipse.smarthome.core.voice/src/main/java/org/eclipse/smarthome/core/voice/STTEvent.java create mode 100644 bundles/core/org.eclipse.smarthome.core.voice/src/main/java/org/eclipse/smarthome/core/voice/STTException.java create mode 100644 bundles/core/org.eclipse.smarthome.core.voice/src/main/java/org/eclipse/smarthome/core/voice/STTListener.java create mode 100644 bundles/core/org.eclipse.smarthome.core.voice/src/main/java/org/eclipse/smarthome/core/voice/STTService.java create mode 100644 bundles/core/org.eclipse.smarthome.core.voice/src/main/java/org/eclipse/smarthome/core/voice/STTServiceHandle.java create mode 100644 bundles/core/org.eclipse.smarthome.core.voice/src/main/java/org/eclipse/smarthome/core/voice/SpeechRecognitionErrorEvent.java create mode 100644 bundles/core/org.eclipse.smarthome.core.voice/src/main/java/org/eclipse/smarthome/core/voice/SpeechRecognitionEvent.java create mode 100644 bundles/core/org.eclipse.smarthome.core.voice/src/main/java/org/eclipse/smarthome/core/voice/SpeechStartEvent.java create mode 100644 bundles/core/org.eclipse.smarthome.core.voice/src/main/java/org/eclipse/smarthome/core/voice/SpeechStopEvent.java create mode 100644 bundles/core/org.eclipse.smarthome.core.voice/src/main/java/org/eclipse/smarthome/core/voice/TTSException.java create mode 100644 bundles/core/org.eclipse.smarthome.core.voice/src/main/java/org/eclipse/smarthome/core/voice/TTSService.java create mode 100644 bundles/core/org.eclipse.smarthome.core.voice/src/main/java/org/eclipse/smarthome/core/voice/Voice.java create mode 100644 bundles/core/org.eclipse.smarthome.core.voice/src/main/java/org/eclipse/smarthome/core/voice/VoiceManager.java create mode 100644 bundles/core/org.eclipse.smarthome.core.voice/src/main/java/org/eclipse/smarthome/core/voice/internal/DialogProcessor.java create mode 100644 bundles/core/org.eclipse.smarthome.core.voice/src/main/java/org/eclipse/smarthome/core/voice/internal/VoiceConsoleCommandExtension.java create mode 100644 bundles/core/org.eclipse.smarthome.core.voice/src/main/java/org/eclipse/smarthome/core/voice/internal/text/StandardInterpreter.java create mode 100644 bundles/core/org.eclipse.smarthome.core.voice/src/main/java/org/eclipse/smarthome/core/voice/text/ASTNode.java create mode 100644 bundles/core/org.eclipse.smarthome.core.voice/src/main/java/org/eclipse/smarthome/core/voice/text/AbstractRuleBasedInterpreter.java create mode 100644 bundles/core/org.eclipse.smarthome.core.voice/src/main/java/org/eclipse/smarthome/core/voice/text/Expression.java create mode 100644 bundles/core/org.eclipse.smarthome.core.voice/src/main/java/org/eclipse/smarthome/core/voice/text/ExpressionAlternatives.java create mode 100644 bundles/core/org.eclipse.smarthome.core.voice/src/main/java/org/eclipse/smarthome/core/voice/text/ExpressionCardinality.java create mode 100644 bundles/core/org.eclipse.smarthome.core.voice/src/main/java/org/eclipse/smarthome/core/voice/text/ExpressionIdentifier.java create mode 100644 bundles/core/org.eclipse.smarthome.core.voice/src/main/java/org/eclipse/smarthome/core/voice/text/ExpressionLet.java create mode 100644 bundles/core/org.eclipse.smarthome.core.voice/src/main/java/org/eclipse/smarthome/core/voice/text/ExpressionMatch.java create mode 100644 bundles/core/org.eclipse.smarthome.core.voice/src/main/java/org/eclipse/smarthome/core/voice/text/ExpressionSequence.java rename bundles/{io/org.eclipse.smarthome.io.voice/src/main/java/org/eclipse/smarthome/io => core/org.eclipse.smarthome.core.voice/src/main/java/org/eclipse/smarthome/core}/voice/text/HumanLanguageInterpreter.java (53%) rename bundles/{io/org.eclipse.smarthome.io.voice/src/main/java/org/eclipse/smarthome/io => core/org.eclipse.smarthome.core.voice/src/main/java/org/eclipse/smarthome/core}/voice/text/InterpretationException.java (74%) create mode 100644 bundles/core/org.eclipse.smarthome.core.voice/src/main/java/org/eclipse/smarthome/core/voice/text/InterpretationResult.java create mode 100644 bundles/core/org.eclipse.smarthome.core.voice/src/main/java/org/eclipse/smarthome/core/voice/text/Rule.java create mode 100644 bundles/core/org.eclipse.smarthome.core.voice/src/main/java/org/eclipse/smarthome/core/voice/text/TokenList.java create mode 100644 bundles/core/org.eclipse.smarthome.core.voice/src/main/resources/LanguageSupport.properties create mode 100644 bundles/core/org.eclipse.smarthome.core.voice/src/main/resources/LanguageSupport_de.properties rename bundles/{io/org.eclipse.smarthome.io.audio/src/main/java/org/eclipse/smarthome/io => core/org.eclipse.smarthome.core/src/main/java/org/eclipse/smarthome/core}/audio/AudioException.java (96%) create mode 100644 bundles/core/org.eclipse.smarthome.core/src/main/java/org/eclipse/smarthome/core/audio/AudioFormat.java create mode 100644 bundles/core/org.eclipse.smarthome.core/src/main/java/org/eclipse/smarthome/core/audio/AudioSink.java create mode 100644 bundles/core/org.eclipse.smarthome.core/src/main/java/org/eclipse/smarthome/core/audio/AudioSource.java create mode 100644 bundles/core/org.eclipse.smarthome.core/src/main/java/org/eclipse/smarthome/core/audio/AudioStream.java rename bundles/{io/org.eclipse.smarthome.io.audio/src/main/java/org/eclipse/smarthome/io => core/org.eclipse.smarthome.core/src/main/java/org/eclipse/smarthome/core}/audio/UnsupportedAudioFormatException.java (97%) delete mode 100644 bundles/io/org.eclipse.smarthome.io.audio/META-INF/MANIFEST.MF delete mode 100644 bundles/io/org.eclipse.smarthome.io.audio/build.properties rename {bundles/io/org.eclipse.smarthome.io.audio => extensions/io/org.eclipse.smarthome.io.javasound}/.classpath (100%) rename {bundles/io/org.eclipse.smarthome.io.audio => extensions/io/org.eclipse.smarthome.io.javasound}/.project (93%) create mode 100644 extensions/io/org.eclipse.smarthome.io.javasound/META-INF/MANIFEST.MF create mode 100644 extensions/io/org.eclipse.smarthome.io.javasound/OSGI-INF/javasoundsink.xml create mode 100644 extensions/io/org.eclipse.smarthome.io.javasound/OSGI-INF/javasoundsource.xml create mode 100644 extensions/io/org.eclipse.smarthome.io.javasound/about.html rename {bundles/io/org.eclipse.smarthome.io.voice => extensions/io/org.eclipse.smarthome.io.javasound}/build.properties (82%) create mode 100644 extensions/io/org.eclipse.smarthome.io.javasound/pom.xml create mode 100644 extensions/io/org.eclipse.smarthome.io.javasound/src/main/java/org/eclipse/smarthome/io/javasound/internal/AudioPlayer.java create mode 100644 extensions/io/org.eclipse.smarthome.io.javasound/src/main/java/org/eclipse/smarthome/io/javasound/internal/JavaSoundAudioSink.java create mode 100644 extensions/io/org.eclipse.smarthome.io.javasound/src/main/java/org/eclipse/smarthome/io/javasound/internal/JavaSoundAudioSource.java create mode 100644 extensions/io/org.eclipse.smarthome.io.javasound/src/main/java/org/eclipse/smarthome/io/javasound/internal/JavaSoundInputStream.java create mode 100644 extensions/io/pom.xml create mode 100644 extensions/voice/org.eclipse.smarthome.voice.mactts.test/.classpath create mode 100644 extensions/voice/org.eclipse.smarthome.voice.mactts.test/.project create mode 100644 extensions/voice/org.eclipse.smarthome.voice.mactts.test/.settings/org.eclipse.jdt.core.prefs create mode 100644 extensions/voice/org.eclipse.smarthome.voice.mactts.test/.settings/org.eclipse.pde.core.prefs create mode 100644 extensions/voice/org.eclipse.smarthome.voice.mactts.test/META-INF/MANIFEST.MF create mode 100644 extensions/voice/org.eclipse.smarthome.voice.mactts.test/about.html create mode 100644 extensions/voice/org.eclipse.smarthome.voice.mactts.test/build.properties create mode 100644 extensions/voice/org.eclipse.smarthome.voice.mactts.test/pom.xml create mode 100644 extensions/voice/org.eclipse.smarthome.voice.mactts.test/src/test/java/org/eclipse/smarthome/voice/mactts/internal/MacTTSVoiceTest.java create mode 100644 extensions/voice/org.eclipse.smarthome.voice.mactts.test/src/test/java/org/eclipse/smarthome/voice/mactts/internal/TTSServiceMacOSTest.java rename extensions/voice/org.eclipse.smarthome.voice.mactts/OSGI-INF/{TTSServiceMacOS.xml => MacTTSService.xml} (83%) create mode 100644 extensions/voice/org.eclipse.smarthome.voice.mactts/about.html create mode 100644 extensions/voice/org.eclipse.smarthome.voice.mactts/src/main/java/org/eclipse/smarthome/voice/mactts/internal/MacTTSAudioStream.java create mode 100644 extensions/voice/org.eclipse.smarthome.voice.mactts/src/main/java/org/eclipse/smarthome/voice/mactts/internal/MacTTSService.java create mode 100644 extensions/voice/org.eclipse.smarthome.voice.mactts/src/main/java/org/eclipse/smarthome/voice/mactts/internal/MacTTSVoice.java delete mode 100644 extensions/voice/org.eclipse.smarthome.voice.mactts/src/main/java/org/eclipse/smarthome/voice/mactts/internal/TTSServiceMacOS.java 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 0000000000000000000000000000000000000000..350b76a92902e0580728fe675ee50753a383ffae GIT binary patch literal 89160 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%0DbN0#}UdAoS0`|bqFMq;ryWG!`~Udle% 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 literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..9d9b5fa746ae36caae122a804e94bcc110d804b8 GIT binary patch literal 122560 zcmXt>2Ygdi|Nn2&bdPlJy<1xL7DU`4sE7l_M^RCE#EpAA&PQ<~>f;tSF5G}9B8bSg zY}!Ki-c5JYk^6stlHcFFQj(i{&-w0kzUOEdA-pU`yIF5`n^t4MG|fIrn!P;TY@Py|QeGuy8PE5c4n_#z*-T@+ zG08aHm|;vc3XLqrNOHqwW2^w9mzkVqqgl;+nE84bs|Bcf7_H451tP-;sR5(ZGET z%vsNK<=|0n)?-;Pj;A@{H=BR( z`Ui^a=I?Gs>jgeL#zkBUhmM_Q6WErSl~A!9dKQ_#G0OMm zx8|SbJ}A-(9sGbG8R{1@_herA&@HdjRZy{(QR;zB{Ow_+FzyS{hC-WUB){BDfgW*aN)nulhpH9OHrzO9{tc%_&0XfZ=G*2` z^9cWb$4q(1z7-Ci164a1KN}3w;oV&0e6B{glWSJP$-HssL?O=vLwS?Ae>1lM!$CgPAbZ0|M6Vgbs9s=@ zjt@~xRKgtHZfOcZM~BTIqY3%!Lfc!AOv!sR z@OQf*>VuvZ#)?ByeSj$hIXw(b%CSYg$fVOuW^_Au3tMmWCfk^1oX6W2s9WI22B7`| zZLLQ0EnH3IH(?cul=t%cKUkJQGmKw`;YNbnl3IWu3&>NUXfjfB47@)#-#6cZ_C?GZ zX`G4lEktrIHO@eH!lBL}S}a?f%99D`-4OTLpiC1oC`(&q{%o!@zvlfJ5LJS0zZr~P z6d)VZpk4};I|E!UhJ$nX?hRi%q186?7qsaFkXA#bI<9qLGY7DlK4_gCdl%;BZ5B8Q z<}4tV{f|H+Wgle6Td+R^@VA+HkAuxow+-p$iXZdNLTd8CL0aAfl}zrHUYqd3$*f1Q zI(=wzHxld(M&0nF4k+Xc+PNnZe3Kcc05~F`da98N9222o1kly-bS?Mk*>1SzV61N5 zBh1sm^bYivlfuXcd(g!bAy6vT z?b`;h+ZG@S;0|xb3v*K}3FyVcT(I^Ahc@u*1onPj9n8|qUkA_^;Xk$lQ9YyD!8eU@ z1Ci=5W(ehTl6y=q0uZyfl_PRBugNkI(I z#z22NobNz-YPr_}Uz>qS7SBL)e7SEJ8rqQhW=2TT21A(%S?|t0cg+v4ZjjU22663<+he6LObiM`NbpU&>>5ryG@HTlmjDG{+ zkuNZZf_0CZ=L5{F$f6Z2`& z1~7+YJjOkvKX=FS$=CfvJEM2;7F1qn+W@mlw)+{s4Xqi)stC?LuGo0Oi`n9ND~js` zA3>o=NZz#<-2ItReqVHu4vsqN@Dp|=6-gYdH)YjGHf zmqdnR5v5fLSVvhYAI1p*A3q?G#}DKw0|}`@iraxv{1}D{F6Oox{cbKRV$_V0U>${I zWB|WnO%wd2d(vko@V0Pw1D}S$QzK~%MOfj`Tk&NybNVr=;0^)`E55bXZCB(o2hko$ zv1sg1w5z!HB;zU4>qS$7fi@IaTX~|9**f9q5O{~V?SrBVX}4^|AXqBmkoC?6Ll@7> z+V%jOiLS)(n>>%COI+)A>qRf{_PM`%B1T1K13cs6DM2>`Hj<$MH>9ypB!V&e`8@=P zgP~3gdYi_K1<+hkRVsJXVQq`ClpT!J$B2?*S;QLPuLS!}UOjF*B&n6;je?VWlVVcY z^D&GhkKzrzL<0*}T=8rb^d7+H1VP^%?n{8uf++C+WU!TaJQCQ+yyZY3OW_4I26^?m zX%xa!;ZSannUyUlpNNM~k^HN_;#m&Vihv__-jYrE<$iE68MV@F7^IY z2Jw>DY=#z!(lfwZ5+h43FBJ@4$xu3snPPa(hV)A#>!4UAFXcV0ZrSx`_F!KAz+>g@ z?}p5anLPHh7mf7Tn0Uqs1Q&0hRpuDT*r9MlQluEl?*82drIf)*7cILWQ$2NXblq63h8&dDqk`{hZypRr6g5BB>1}_xVcypg}L*9p{>uo{IMvUfgiPbllAI8D)rEUY}NGSgPB`!b&49&M1>W89J{xll%`$RdkZ zqFWwDm_h!tibzV)#SS9$1H_UVNBLNYTW-V!`F}glq!MH0yLqfgRn{WF&4(VK7~u)= zT()z>tzCLX7C#Y~B&E?vR+5`eesFP!dxfJiaz#RlZAIgLxT$Kv2wu^L`!u&KeiEO7>#FLl!}!j4*@vqlikHK8lA|G&RhxJxH3y?PX>^UJ=kS09Z9z z7@rlVyRZe4Y~^5zpTsMTpm}>4!36pqo^OI8@>BA#os6z{q!A1yB~5N=^#f*&ttYMg zRcsdvk3*QlN%oV4u7-oZDiJofKFiiA7IZL@1A57WDeF~a-i@3YP-w&*SIfIbB0)iL zPF(coTL5#&mrC0#{H}3L?2sbiHlUIOinh(jRVA2ofuAHPgL^}HK9r|jjIP*7I5u&w zI4o=8>(*t(yaT*Ofj5Y|6h|o+weoT?S1{icLsgN*Rf2&mvXl7~1-9~1#3B0{!Mw6Z zsk|cLpaHDXq&8;kW_(|_G{v~#QiKx)ofV(N0f!Um3+A&IJQ?O*@mw6!)A9$37nDtS z;tb8IT88o(%`H9EY;9QBqvYGg{443!Xm)5B12o=@=g|YnUofybkXh-t{ zu@xQ2V#`y=J6Op?ML*?5aZp^9E6I=Ia@l|i#^{Hto~Odx^CyA7@Kv253>y{)p2F6n z$;t&(VQU8VGG291N!eu|*vgV>wlJhGnP(NR$}9R{O=6IVSTIm@6#%cgng1m7b}~X6 zunz!*G+Gu~-p|gQvOUUjB~2bXE6E*Vo+|1A)kvVUvk`C706g7LQNBbosjiiZCh1xE zu6RaLo}UgE6%P)gH>1F7F9SJ3)ov7jpGo%(;&mSSe#EzmH_5 z3@DZhucyHAc(*=9^RNDMfT?oME^uyT991?uprPUkL6HQH6WkC>ibma4rBrawXQTos z5{Bf+pE})=r%0p)O7`&lQM9JjEsMTDo6K8ySdj%6cp7g0%d*Q0N^0cs6(7pm51`MA z^c7R7rlBZBS%WN@A|zFVq-m0d0cI@5^POO}eq=+jl?87h8#xSi%JO`9N`62XINcb! z%m`?oNgZY;b%Osd6^g-R<07GOICIDrs)E=6WXF)NMyR@j^^>meNdW0At)TE?i_uM)*w;Plx-jN%d6Gv-n@`*4s|tR*h^7c*em! zE4)f`j}hgj;$e5~vXOa5;YuSj9!5t_@=Y0e2(v^&0Y$fQyhJrs3su*RLdF$eN^4t@ zt8$*L1^*G=(irKS>83y+^GaJ4si>NxEIAJ;3Ic!mCDo#2Rl0e;oVknL@w4(x#m0?{ zsCv@~vY~2a0#CJZM-{($WJ~dYKm3zj?qQrhWY58G4ak}-mnzz_6%&yw)t@8-sc=Ag zoJ?I~B6^d+9jfv6Fn=jJ`x6zpui@-r#*(c~p#q{c5XBR|$gwKjeOy;`;DU#(z$(s_ zQpwzl6qYg03ApNnBdXtDY%HaceF?f=KZJ8>TNzgzl9>Uts(z{Z z;pD2U#YA_lOgVfGzsonN`ZdZ-mHexEwTGj-sXgy>TS?V$8^Oi}?E}D3I_k+swR+

z-zKscDDBk{@*6`QN#RS$HE=~VH^fw#(8Vz@5@ z7*#o~11qiU>_8WqsACpTd7S`u^2n#C*KXnOu$c{4ucp#I9{7fkl)sUeT4Ze^_1Y=W zHj2BCv%2ytwdQZZtOlPsf!g6h>d<46Bo{vMAo#X2!!)Xs=TmW=2zSfy7C*8Q^cTN- z16dLiZ4FY8;4#oi>p`k0NDj3UCl8{Eiz3eu=+?#vjmU{A zK*CsFE&;5w+#HtP%F@f~_rco}+^IOc-c8{mtepI>{K7D@6bH#w!JrFX5=xKK;<;8;omM zwV42vg@;--En#i!J1pV`=#~t9&I0SDyyn9vMUe8OikG^eu_*UBu>AqG2h4F$ zVG&v~4*4sA79SB0)sjP<$2!_WQ13z{sga8HALvRg^G#r-bPO;K@=bcY6)x;SA2uLE z&5WlS^mt@X>psd2ReNhdCZeEq0KbKx?ODvFtk-0WuUS|58mxB0uW&rT)y#Q0kk93w zcDId7!zW(>?al!IMtJy{xzb!@&SkamM!xx@v9}njjrWcJ8Ow=n`=IDbbA|aPFx6rM zZZY05zA=6@K8NFr7->3uyv>bQgz>C-&=uo)XLQNvJEKcokC>~C)s}_UFRhPQt1SVR zDaM;-ni*!EW4;XC&V~Em7|)_NN$_?nFrNThMO-mR%HQVe=0k9G6Eqlu#@)?p8f%Dm zn&+E~%{##EbYqS2r?KDo7|J$tCDd$i+4+5&Inmf;BwMCgQZ0v&p%=_L*G;aut~*@= zu1)5n#(2y3mftP!S;kvFg?7)I7n*077n?Uju?D=T0}W6;YbRc<1#EAIYcI2IdLH_; z7Al3BQRZaxDRksq=ul!DHZ~fsAR8&HT<$=(O5M?bJbMW^Y(<+?!AZc@T?dVx1m^3| z)?)KD)=?icpJyHimRalF*Vq2NOqox9NcbvfzIBI6wSd} zR3fAAv#R%&`J(wLcS&|sFN#O@&%*m&hP+P1nktS_T{HvP{5Pw*SK&>((VOqhr_Jlk zb6Fw2)qDhs@5VCqQW;i8T@L&Q_@2c`x5KMdtk>R1XG1EnM<1M0ln{gDPT==cY}ppp zhp#fnnRYXWb>bhO&|GwTw{gnYXZ&t_29=&c^R8tDbPTqAKkL%Vc-;ma-hx(EaFxY2 zu~A=}>scLKi|#bTf%(Ypa`bj8c23o{eB|xlNZe&^9UF$~&zskn|6;6n(WoFO{Rr@0 zi2g~p-!p&5b}wMH`8b^VH#+pU`I5OD8M+XQu@~BhBf)24NnPmik3e-Fa`!Y`h{oD) zGSVz#EThIQupeXWMGww37qX^)DfexMCox#I(~yy6Zte8K_HDxsXl>L6eD5J0r;MM_ zqzj_11z(xgPTf|ihJqOBj z%iZysqD5t$_2^wSwyy-AA#W{-f5~_QoC=YzZ;^`i`0O71=NRNKj4?hzS}rng;rd~o znFs|g;B^&N^*^lU-w#&T0*NA{61oB&Gam=D-x)uaJIBF8KjP=V%r)ly<`eMePhj(b zuNNaG{m;q2<>+`ClM71QyX)u*!7;QX+4-^MfS=Q>6vYS)f z-3?dMc|I4sR~WZ4;$*mZ9Q${IN<${pT0(5Fn6Z4A<0w{fEAyy->kncBb$O}!q|Ct2 z(7La~9h(IJaWnMY4GyXVdxO*IL=00HrJCzou!qfNJp5jaj6X#Da0ZYL@yk{q9Du$x zZXL5E_yE=VA?G$zung+m48KFMOFv@Ez6It(%LZ&(ShOi4)R!mwU~zWUWP5Z7X6Gw{~XZbC+uuBS&^?> zHnX^{-kgJQtpyy^k#Yik_s}^PT*krQL@25}POH4?wo!#H6}d`ee#H;!TTqT%K_Q&-0h)I1R|jSi7|z8GO=d*R6%S78BGBro*0i*K zstSpE5RW4(TYysQUZO%2bee-7m<)$hMLmEP%3i5rltd&xhY_>krSe37_@;F!85FLv$Z_c3be>m-smHffVdb{Li(ld8>*O=< zL;p(f&%vu+fd{!2i?W1gGT_Y+)ZNFuI~cPMjTz5NHJWMvr_)|=u!kA90nzjH>}}@0 zFy=ak*y1wuGaKusDvlEhbfF9K#p;(Dg6ao(&Sg#qc3F)qta2T8Xajz&2)M`N?QSGW zxE$CU-5lA@-#v_#3kQ>tsuM`W=VVN8B98$;{2?*h3q0kGKX{)=bt#!c1zF8G$k9F6 zkZH(Wx!b;%0m)Qyo#k$Sc@>_?-`Im(e?}a+nmG3$(5Y7DK&H1a>o&0Rg7>O&DW32} z&->t19$dNz8zg_NE}o;%svO)xfKd7W3ZNeY6?Zf0OZf4%NL49PpNEZ1L@I;fnfjoV z+jVk1kgG{Nq3#n|F$-`7G1m?xQR{m?+;72Nq#^t9&`Z4;P1qq-|JAi4sUHh8XA#k! z4~NuC)`HjmiMc-ok2+!#tqkV^^J2J|j$hdi#n-|&X>TZYLs{!NNP=)u6{;9p`3tsY zA27&Qr$U2NELa^-?dI2Zx3;PuK`Z`4Q0g~0cnGW{<;lqA+0f`}^k+Iy_n;T5y{eDd z#aPpk|I@i=Ixxlmp9NK=Z!cW<0k|a%W#HaxW*~X8$KL3t)~wa{q`JKq6q}2UIs;i# z)!(xctZtrT=#+fS9%$@Bi=%j2Ypp3zMco$isOmh@3c3?4WE(QwHZ2Qz498n^x;3~4 zzN-KCZ#3o){uV*00C35JS~n3%K0?m-Ad(P|WjF*apG6Zsfi_m)y^;*>Y(~rFer2kv z!tX$Xe}Eg6@XQVm)IoMSay!)xzXea#20V?-a|k-9Us_!Ps&LitjJm-+Yq6q$RuLl@ zeLUQrhKBp`v^sMd$ZfKLFc)m)+crXn^~m>5a8b=iR2TzK=0dj#Krn_+=K}4u#1iKN z&lsL+Xa3*Oub0rU_rT~wcXdLr?|?E|jZ&4$7pSw4w`a(jwvpo=qzm{d+6Y|?<7mUnNQaP$2r)eO!$%xN7b9w1dK<>JhlUYysf%a)IB%I2+i>G zAMn`EXRUYI+?x6S<2KJ4o>mXlWuv;}0Ft8`ochFl@IOOjh3YRJ3-46X3&1a{*IwN# zCCpZgz1oRAD&l<%js(KdIZ)?u<1Qeb$dg)&8bmI%GU3@{@DKKC8(Jtz9b+`rmUG<{ zQN23=+~#1L)j56>lpDhcEzI=~II4oGdVo4oM;P-5crNa5gwLl#m#gr}SGgrbYmAC> zy0Mb#^lD}94e;k(vb0Cw_4ja25n2{_`ZK#a^`~P87GnvPG4EnVQ-l%^9kn{Fm2vs$ z0mdm}ocF1)ZAJP`ywV(~GX)7xfi@@6s~?zUI}&JT+}X%*GW4|dlb%KVay@o!7SvKDR4dl%cGgRI?*LddyR-SHkn(lNn`A@vIx9R;Khs#Anu?~( zhT>zHHI^~dOBBy|IgAhqCo7=%S}H4#<8`)!eGem8v4>iduS0(m+&rF*WxWOpUQSLU zzn6(U_H^I0LH~_d>6ft|&r(%bh33c)*};1;o@${xXTAwc6q#N?G&0W}cPNrq4An?X zwvDGAg#OO~$%oLP$Za8yLdO!~fI{XwgC{S*%U^<3ng}NOJRQyqTHjGmg{p<>9bUbaXDzXvIxkNQ(Nk5`PFU*p4KXGe!}!@57gvJga_f_5Jt)VF$LbhuB*3FdYh< z&%Z7_#82q;J7oBuW21lO>2_$Zyj@l|8(62hxji51FXXv&t_HiW+L7}%e1-C$Z+Utn z&#RKJPK#!==OANiACVQGcP5m-0ZZ{56~24;e6Cw2wU#R_=m1)E2Gk%u6=0(sH)V`) z2x$J|+6nNKh46w_T5+7>rW+T~IQ6+!Gw)u!%b&m?x~Y#+>xap3IiDx;!D9jNpAG-6 z!A{@IYdRW~&N%8vQx`xPlJ^Bx{w<_P)c6TKXn=Eye#3dI_NyIBicT&-n#W?h)$v^I z=GvFctaZbBU>N`tMTv*ty(;BT-ivZLSP%TWh?YLZIV~j4S{*nCX_v$?GYy()X4QX4c>$KxOovsT##iQUq5LnT-Bev^Id$60snOky?tTLX2N}JF=i{LJ#aP-8jBkzY#t!2X)^aX`4;|1- zRf`IsJ^-g)rRKDPI^Xro{W0IWiBx7{bxz09UFG(t_Zv^+5$^^5i+O$kdhR89{|1Pk zhD#qY!hKZfmht|Q*`3606_wQec+@|j!g9Q#*1i?l#KU!i3dl$117JSUoMO&r?)Av3 z$=v5585ziX1vpxW_3osy|E;m!c$G0ysTl0ULVkvh9fDixJ5}A~d0wlrohPsni@^4I z?!E(9r$XgXMpdoxN=D8=hpVyQui+)$MY@W?R#F~9jc+FqNM0Ri_vO%PIrKY|r}to2 z*MplXU}r(WE5M`|Y1z#6Q%L4KIQuBx`wIN)Z_x8aeEidRf>vOei0?=z=9gc37494% z3%(5Mz5p-J!scn+s~OKc2HCz7m_Hy>C?JB`z$o`2S-X+R817SEuefy!R&^aZ)FMl>nZEw@d4^F&VVSQEvyQ+a+S`1fPCPY~@KW|Rb= zyA#@6?DmkSpzT_B{j`~QK=sL3XkLUIegU?}-BH0bWK3(bG012MnsA&sQ=p*y#S(Dp z!wYQY{vWXnQH-GaR5dd9Ce(Tz>)CJ4Bx75KhRMIA;t!PVbr6Z>LMK%<3z@YOzxFX2 zyBX}YGTp~p`!rASssQ&4Mpjlm9^0oC>oVj}Jwe*rp*nH^Xzq9LRozd)DPJoh7?>V~)$dAWqY^P&B8q~i+UQKcmkdTM>= zd$*qNLV~vPR#j{xqn9JC26L_;7Jr;9{z{%zrdErdokGu)U#gSHiPlOAi=mcQuhfU7 z404E?`&^{-5j6Ntq%R7(se-%?{{9#3-boZFPpB?It-iD(hpM<}U!?lf5_u;e&l9n{ zbCC7xk%k4plYy_T#hdR(GUely$*Ss9%IvDF+2O4^)gqa%lsI%VTJ{aIRe;YZc39O0 z)s$6f?}T1^n6(8g)W4~2)7eny5@cFcgfyU5-k}UaN z9DO`c7T|l!xbhovywA;h?P5`%lIk_z^28rlhq}P9#+QnZZ~n)rQk} zr632Ie}fzAQee)3#xtPjR3Ok!-1E`CCBQ$Q*@uv?6G+mR=-XZ}REJ|Xqqj2h7HGT< zjXvVW;0$Da9vCN}=h7|hUUDH_@xY_XwIoL{MnmIk82>DyBKfl2$d;o1ZY-AeWC%iU z;v)6zdiL%rvZw*$I%sta3d(NgVV5pI*XFuCkUDDB(V;qN3KY=3tZpv9WkJC(Y>WCR_5+O#?aAe>z2U=H9$CpgaGVRzE<*0d17|H(;4SKSo6#*3 z*-_4=p0ZJ-T)Smj(G>O0YG;`GHnpO+9l5NA2HFkb1S<81Yga-oRI)-i6cP^)hrM2JfMogLcWOyUoQ&UhdsO zd(eZO(Ecd1XdhEF{LqeIb>YhgX&;Tct~~wSDe!v${8elB;@$!*%rr0vb7vSQ`9*!t zs%0xL9palZ0rlzB!&gcAJZMmW4rmXMT$DaajUS0 z@*>Jnj&MyoV^o3h>`+lHFAG@J5iK96wQW&T9fMk_(a7qr*KRNMb*VpJSK`4|o$fmK zL0GAlq#A}!a?maa?M?J&T&)zO1CLHr&<<1e)XHC}BXAs8>p?%fu;O1GeI>_v*`DuM#-QJ>~c0a`P#AG;i z2~^T~3|TxYOw<(_0PX`oUWSGI2dSyxNoA?(pVlr|>7~}a)zPp05?*jxS*rFiXwAX1 zwvh_H+G*5^gdB&DZH%KmN9y&|DC&}9%qbrIJ!>4D0jq-;PWX#JgHK|+=L%PIk?ckCOsxQnz zq@$Hyt%PgUNc#nwcn2a+I^9DnCOUgV6zkzx#RNJfL%TrKspViqt=b>tuaBFG(xpjg zl=d!2`m{$wI|@X}A+S^>#lQz?e~CJu#RIMNYHwX2G|_*0LaUZ>Jfm1qb;vE?sQQ?A zpd3m&#I^TDXK@r@hc3W(j7OdVxu!J?&mL;&v+DNs%&l%XbxZcc4Rz6Je}K*@P;ahc z4->q!Q>d3as==uoJ+J2pNn8};s$QZ!9NM#DV{Ua^k3m+YN%B4F)6iZk?J(C`tjXU2 zBvgBpw7sCI*@ z0;Js(It@fS_|#_{6l|VbuMIpMazU@rLQzcQIC&FCTS$s+4t=$aDHUzag$OaihbxBWk zO&pQkk!{evK<$px{#kXjh?Ck)+Q~B`=t2mvTYz2>q-PIdEEE$hwIUn`HKkRu^~$?- zu8X?9hk;*vjzu@^?NOE^pP=3Zoh+eiI$cKaDvDAj9?Uq}pRK(RiVSp0f%=H$fwW6N zJ93-$V#RXZoNx~x5i+AE+v zJ;F`<71gt!h1Z_TI|s~kwV&q{4>U8TFJ4R|i&L_L(p%Y7k4#DnocI)VAxf_lYwMH) zk7n55nD#Kq18c{bx(&4NOqN1bR_*lBjf_SxD%nT*2lf{2?X;Dqgtuq4j97AwjJ!X_M=+z}0_|Q@^w$F% z@^|vxs@QqS{<8``+u4-*izi8)*ocaLaXiSK$ce(1+{-) zdw>0yN17<<)Qp~WMeP{~cf;t}@jHa4)p;CVj3Q5@^FCZ|J(P_SwCzZwszRbsHyEfl zO8O{krwmLHfFxPn_rfR}O_JU0P&-SUW$XXdpLk+WdCF-TluRvThb@IwQE{$ z&shtKd}L7+&uI6YkDD4As{)E>r?z~S_&MZ;Tb$A=oYq0LF07qms%L7qmb6IGuR4L$ z!5787%D-fL)wL*yr8CM}b!tH*_bJkqY-uL-?y1i~)l;1u<2h+YF~8(m`H*rRI4`M@osrCWe7w&5(LCyjQ)h}|s20Xh%%hx6S|M3cx1&z!kVn&= zWZAt$<`xF(NYePiL6%GBBB{dY*`2MOkpaAfrJ^`RO4Q@urHL@f_ql$;JJpWNOHZ`QFTbSDA<4DQLXjq@hex(oFlsK3EXe+7zq9&Fl{d;; zN{@uSBwI48b14*CD3dDhC@i9|%o(__nX7$XgqM-GEQ9<#LP8*Ys$*xKRW#JUp%Zlh^4o`mO z*^%oxPe-RODC$>+t$PHk`aHB(P26e};rc?x`{Y=U- z6g6m1u{uh1Zj$yr3T8#Ks+%a5m9O=j@Dju*9{-@+Lb9bONc<8#B+(jKd)9RVl^{~} zNwTW_!IF7Nfvlxs0Y!m|jU{vXE9;;g@`Bcbqd&Y-gf5QD_Gyhpme}J*L^*Lqk&`&- z0N+4n(pch-(&!rO{AFc2c@*qi!LHqtbkNL{%KL+fx~kY?Lg8&JPn8WK$IP%Z|xr zt8-90H$9oAI-_+}Yp=>n#5# zJ<|y^l5(BbB3t39Pw0#?`N2kJP+sBb)s}^j<@21VBhMkvDqWLoc}@*djI1+B6v@a! zNu%Yb)McmmMIKF+A5Xj_PpQ09w3HR~T-Qk`TANVkktecOE+O5M{gJLK?h!Y{ZPjqJ zD_Zf0e6nI++1M)JmgY+)RPPag<#UwFc`8GST;-dzyIQfRJbbd70_sllP+s*+`9aB% zYG~q`=Oj1fMxub?8Bw_p9hUy6?^p9Hs#7MT=fx%2Q?1D=)|bwz(mKFj#om$(Ns?kh zX^E%$p?%MSPIgFIqfAL&(BmVexvIyj_R#E}OL?Xs@zg3byQD&Ayy#3HWxbyBc|4U` zc`-#R9(@!|f`OtRDk&b)D4q;MHdN9qZ>#)BepM?d+952>S1ltB?;|T74os3;*%R%f z_sFI=u82XAm!b!q5}_ z?8ruGbk)!#$LdBDEVBI47L6}SP#h@#CcmM6HED?lD(RE#we(*;UFS6@GSMoZM)&N% z@%R-_jwAmkDb#9?vMrr&B2Ok-sUucVvA8GMl4i;Jh>y}{WeL(cRZdkqS5KyLK}9jL zIeJDMm%b}P(CUNoLuE0-$`i|b&RtSxyS$!oSHG0Jf_73VPLd~d}`I-bsC5cp@itL;`w((njg@?ENRNahqts176><&g_TM3QXTb;+{M!;my--9?d@&aBhe!dn_DsgNg@ zN0Dt7%~TapgePi?^3pU}EM3)UFS2s#xslY$W=sAwyUw|gClsIMtrQ1(PV-WJAwJ5! z%0jA&qI^UA5M|_Dq${c(iT;u%`7=)~$>SBIyV783vZtCZN-DlrzNK*`lj4F_j6L~? z>Jq9bsTbC>I-?j%^>@WQsuwB8RfRyYwET{0r>etgMM3;jG^R5Yb!wsNRLXVb1I1NO zJg6vJoRrn@WM8UYNbBXl6-P^Nx|m02z6ftoTKtr?l08(EsufJFSIV2n&gz5@We(Cm z$(Pow6-P2I zt~a}D_g!#Ux*>le9w^t=j!ac9JbEP^(n)vHBXM2b0E)O&iPLxadFi<4bUe+ksIdhq z>ue9nnEaJuXvIdVTPS;#b}FhDES}RZ6|KoGh+c|7B^x^1R$cMJUc8K#QPqYENxd*}V4MBrvMDF3y-_8;y8> z;i=eHc1{#fG^KuVMN`5>w2&rC14KpfS*N&4g7i(D1&VrQGdz_GWwOecRo{|c3v2mA zox*lCmmEuCf=!3|iro&zG#|itL3buhW6GgF*gV-b|+=ijG=slJ}OS)VZUw z9kQmfFOmdFqN>}1K;tR{Q9V{sf%vG(uqVz}Zl<#xr3=b*Jh7)VOVv+x;Cd=bvKd-o zl}<_fJ+X~EguIHbdF-wvPMM;tlvZEmD;2+L{Y|-+YKF3asxayQi=f@9o?4PDnXI+w zBMDJwi8M|Tv?!%~Ub3pSa`|rQw(OO%bMa77p{J4}zUoQMr3g=cOFLq_!ARMRAXcs^ znUK}=tR84JPuYlcP`WPbud@W@Nj0zX1zB&MCa&yN)=hhtmFeiL6aDIif7a@y>Yn<) zW~8z1rgP@ByI6WIyoShEv`VeB$EP<|C>W`jIu^`u<0C1Vd(L^vVGDPc}359 zk=80y3y}|&Zb|aA66A?ibRzOF_^Xa3F37@5>lF(KYGqxDd^|b6tfwcVP{gLQk5oBU zrXjnO#dpP|ibLgPw5}kU%PPwz2qK+>rWi&Y5;|R4C-&-B#o3~xDkPE~2e_(#L^3TY zE&~fiin50Co7K=`H*0u0lTTejiksx46+wG)E3H$F!bPo%X^mB@O5&KOIwbv8MMin9 zR*37dB@QxF(NkVjcPUbleu-N;AvOgJwTEm8owM2zs#ApSq62m%eU!`j_aSzCyi8B~ zf9a{cgRa)g*afnJ`);D^`dYdja-ojuQrf#-&AeLk>t>bs5Bl7nrgL@;|F4S|*_rbe zo%(C(y?loK0@s_@(S83g`+VM{`|(?LGCV`y`f~QHKFCuaab=UK{l_}t;V(K$)TJvr z$>uBm)%j}jxr)DJ$rVu^r!V*gM!bQp{+VWq>1$TGes#U;dd{`THOCd_ig5Y3T1QWf z7LV>4-91`1+BX{Sy41DSHSD^Kj(&q~-Mk_YOum9Kryx;T=_gUo= z<-5k$*Kf7os9%o%B>!mt4!<}3#`*o=o9ugs&)43SwjQruYpbP=T?4hQVApx0kBq!E z{L@giGr+lE@Y8|#fld7n^`GBAtA9@a1^xH*AM8JWz%lUI;2qAnL%l=44o@Fx8hL6o z)V0bLz)pleSOb{HnL?ZRKa|uOR*S{b$fu|1Bhyayqx4O!XaB%rGr??iwea#X$8*NW ztMvG<;_3OEx^fY;J;?6TGwGO%;2fa$={p@Xm$Q#yBl|UN#&){zUFQ9Umu0Ob!+Om6 zkk@?M|GWo$?(^H|zdm41U_sDNLH`a83TX)0V_zS-IPCqfzrw8H%fl~?NQ?X;GCndj ze3t!i;Ol-nyraEl7#l}s4KD0i-SNMcPa4wkTZ4`rJ5)?d7T@LlEK8klF_6W$(Wi`^PKFE%TtEowA!VdN_j zSBJkIY703YFxB@)uK_c7v}@>X=Sze43`F!l(CgFlukKCV7xdoKzj$z!Gi&J9p`Al- z54ViG=-O>QZxplVW3SO|+-x~zv0M9bqVug^TVJ&XSZ}gqSzfVRVRc#^)_1HS1kcl~ zK3-?rD!g-jM|{`#{p?>IxH06Pu(grb$1ICKJvlz@`?Sp|7bQ(g>`v&3|99-&kr&&S z_$Pbq9zD;wz3;j19UZSW*VJBDSzG2hIjbb~c;%7wBYz)0{qUMY-iPf+LyPmuo~mBi zU~Qe!xwCt6|2so=%ZTq4!Ise3Vb6vihy3T{Yc*_q{Rr z?NHdr_ahx6502Kknv9#Qw|dR6oniakYo^z$Ubopc*dDgMWgE7Y*oM8vcrCI1Y80A&s$Z`B=F~;S!$%h$&L}#zZ}*aIj^q*Y`G??~{SqVf|4T#{HDAKWTF6dFlHz&dRzu=h?jJdCsgi(}t54 zCEOMJaO6k!R|3BAb{aR2R1M7R9oxOjv8BV=8r`zB`9yO`b7}K~Ep4suw^ww$-+7DU z{O-cOIfG9QrHrf`z1bDcUZ5w8v#lLo!?w+~8ua&O+xy-{-d^4Z!F{RiD{Cz$EPY{p z#nxzh(RPk)oA*5b3xY$!{~Obtl$3dG)|iaFX-hK4=JyuN%wLh&my{N_A}T2SosgRY zZt>n=W;ye^SG0f7lG3=Py87h&6aPK-+u?|Rp4{F1SKH2od!9Pbcd+QN{n)tT8K<79 zTG-gsG16-rtQ}lEJYd}K+ZXhm{iU$`B12-&NIWm)VA^+??`EgxtjfMCdt%OmIR~?{ z(zBEQik}_3FluM$g23B-e7(jQ`-j5@ruIZTmb5?BvaIR4#-AG!8m_8$)V0>%+w^qn zf%bsT)sBpw{yt-HjdRNI;gJKQVXn_zC1!@@25X+z2VQ+%8*P=|kv_TJ*|u2QOSTOS(?)=xw;WJfgVhc;&H&59RIc*fHnNQ#%g)9l7Vpy&Lx* zD7x#&`V+U6CDcW>mUql`r1v-nIgiCQ$M;|Uw*?nOJRf^`LR^xOTAE?WnUT9Kw<*su z=A-=lysbI!W&3A!q?IJ~#brd7hhJ~s8IbRNf%)3d_x%rcpWpFeOKIbQ`n|Ph*Bq$6 zp{AgAL+#wUvig-xU$)%R_I>+pol)KKy^r)=(SO^3)%n}d4`%%hGF^fVk3V7buG5W#aXwS0FOIlafAFq7B ze8tJ7$Da7-_1(w+F4`IR=R<#-+V=F{ckgT8YuUS?=-!e~tLHX!Hr&*_v14z~9fSG9 zL9P)?g>9aHP{BOCLw%V&{KX-+um_a8d&M5XnnkCOKn)?zsibF#+Bw3&nezg;&*c6sT<3Gul%^? z)B1;-?5#&SUg^H3_ou##1|J@Nb##q+t@VA|h2H(%#eVI9nZaSfmf-6{TJ86Seij-X zdY%2(kcq*62dxj@8?r8RZTOS14XI}OZ>jgB7H6JTuxD&}etp)J8GRXRGg6XvM!gv} z5_GNaqsAs@NZ$&_)oqV8@2~f-?W@{ZHl}3a@!OBq90@;syl89DtB1}ydhYRWkFO|R zTlQ&9LgR_%o>qUyC%xYcoITV!{PXBn<|IqF?PA}IfGt6v*jGfn8oep@;`nD0mM1nQ z-kbDdQhVasgb(8{kN+gDKXx$Y%jmMGHzUu*-+mVKZNL-0_j^5Mwhm_wp3}R$E5CDc z`@607mJ6Emn%`(XqougT*?LX;104%G`yAap`}#KwZglQ*KI_bJzUVw{XvNUOLmv$N zGj#iK>gZ&1wk6nWt8InP$G+8m%L5_<+XB)8PWtWgjq<(9_b$H!{*MNZ4R(cG7#CwnU{x%nW9be+%yXebe{o->I4kmn&mow0TwIQ}rwAhHC#+n_K%; z?OS!r>Mv@zzVVEvjOPEf7;VLE+uQebZ0~&6v9oJakF#(2;GE$}3W!rn6{*hT6^Y~GVxwOS_X`>H-|FMA9(Mga{J8VN{{M7Oa=h0O z&^EXEw8k6iAE}*GLxfayWo1(3J(XK3msh<}eXypf_RRWT}(6qC0 zcY{~`q`EbAE9xGr{jj#G?%VnY8xotsTWVWlI<`67JH1+Z-K@U6cDJ58g6tH$Sm%@viYJ3Y;8rZs@9T zGxExqC*#&8%ujkFxhLhhv|rMbGA_w@BqJsxC%r6nWlC$(Q;C2a`QH8zRyd3cLq!d{=wcA`h563Q38xe z5}mvyaaqDp+=a0P(VHUrLYsqE2i)Lu#Pas2Yw*{ejLzJ)+0Diwz( zHTBj1t2(p#@tV0cXV%o$F0MaPe^cYqmbx};XI}T4eVqdi=i!l!<|CHdy}W#K{hsxY z2#g6n95N%cDr`#RgVA5boQxffPfc2ZjOL|BXXI!6k@=tO)~x?zc4U5#S(Mt4xF`P7 zn3uxr!OH{A_nBu|;2Ir1cku1rJzcxne`4caL-nTe?@AvoK6*Uo*d@oNoCqv=uyoa_ z85J*9T~Sk18&E&F;mpQ{#t)m8wLI7MYRCAlQ@zg*ZWzuqYpv6KoPJ*i#)f!>ZVX!$ zu`+5^%*nW-gsYQ6Qr=9Rp0*|}Ej>IvI4v*rXv!1G_QYlJ*T#*HiHiI)EZ_cTfZgW@ zYqmLQ=!d?>?wZcTww=wf&Gn5l8h)$~=3Q0SS9cu^WuMortiP*aU&AeptxdPKY;0ZI zevac_tjL`dk;e}&82!*Jvh4J_%RAM#!SCgOf}oU;A49K?*cBBNyCJ?W>AAESnJ2SG za-Pe-f6SNp-{sF3Q=WTy=EG?>r<{zR5w$bysgS4r&-X5|erfI;TF`%a_rKf!XezHO ztg1P=r1;eFuA}3Q{BdOWv8)qG#n+TBC_AH~qT=()YpbJc|EztgZdHR%b42U0cJJ6>wg}?Uy@4}AZU}uk{JzMSqK9IG6HX;gOj((>EdBBHn)JCDx20d3=9diwqIG4r~ikNOYvHg%ob@kZMZt&5sZHk38o+8Ed{ zvu<+TU3IV3F0K8t?&kW_>cbl5HI+6Gv^d%>@7(NgbUoR-bfA2&fMQ_1`IO}%ubDn= zenkOi2LBZLbHv%vQ{(0&&PegiSe9L$^IG1nf@{Y56uwokqwuRSbF)J;Hm4ONt&Khu zetPI7fhj%{ywZ(Jhxhl#_1xW&+5AD>RaNDu=9lE0xZ+sJk**`p9-DgNh2jfJ-#xXw z+`l5RvbO5Nnmsjd)w=3GZCce5+x}wL&*Y6ehKtP$yzcN`;~No>6Lcu}PW$YzuOl9f z?u*Sz_%*32#Xqer?eYvGvod{kT4LISRNv%9iSH#`7r!dzc;vi@gQ33#x%@8odEff+ zXp{5Pf%|)3cU;?XbNgwnFE_1kys~jqy`%Q`+SRoaYF1Z&Rx_d2S>s=KZo|692b+$! z9B5zJ+3DEb^Lqc3!G7nSkxcV7Jbu5o#s8_mCqr%tkB>ea`*lKN%A(ABvOmoEB>$tr z7Ygqz__FY{u@B@=&)%82Jbg{#YcY34%?e)?G}Z5f_f)IT=*NS9_11OX)$&?>RCPt! z6Qzz5_T%>*n{)iR6CW1)m)cHcmET)YQt?}5K=m8d_g24AlU;vRV_8#c>wS)kdM6H) z4rQCC5p%rhbC>_pz^jAmgI@`~BjU@b@|cSF-;>Tv$x8h*Ek5I;^t;lQr~NDKsgyI4 zK1<9=d@XKObXL@J5f|AH2E6EZ(!0)BGBRc8qk$zo|8eZ-jO;ksGOjtXd0k^p{rtK+ z>t@y-s(zy8SEypIThwq*(=E-Jt>ZdAbsX*5(Q`-t(}RWN@t?SUGnQDpY>WLC2d)hE z32Tfx68COGM)KR~KV=@ux-!=`=92vTa-Pb)DQ|Ihf7%6P3uf%psECM&(BXh&-$t9w zVjYcgmiC_Lw6_*Eme+VymYnJ@eXZpBl6Oi+PA)HdsQkCe#WnZTzEXRnwx@1V{lfaU z8*XTNxw*aNfsVJj3El=D97!_Xuzqd}^^Ni`4Co9<32F}x3AKeEj5s6OH?BY4k#Iwj zE#-#f^OBY(H7B;luZ>+Fvn2Y@h`zArLI;B@0{-RK;XTdE*Vr^NcBpe;b>HlsySq9& z(>m^Id#!b(d0+F9wvy&s>W1sDZT+=1vf-VEA6su|o7FnGy{~g@*Ofi%dqW3~58mwj za3sc@Z+XaTt@kGjld*KCz>&D(u(4HNY=1eG# z`80C8eQv;I-uGDi%zFl(?JnvV-*UMAzN$Y?9y{^((T|RNab(JovBy3+F})=A)V#`D z>((^B-1tx9i!FDw|K2gu>D`sr^IG53!M{f47@=O{Xlz~;^keX%;DaF#hQ@{66*|xU zmwhO7didgqn~uL_9)5l_oIWi-%O2}^+mGJuKCk)(1bT;D6jmB_GV#*nB}q4?@5sHq@Sj3k;k5-n z=6#eGop)bmQBrkGWcZ6gclvGgy4AIN;F_K?$I`aYruDU-RGwAVR@`%>@!*387an@` zaORQzW4lV8D66h{rs>A^HI7fa=JaL{^g8XsFAuLD{lKg>{}VI`(wF*tw`P$PwG+)BRe{ zjJ`1g_c`wxzIim$yvAs>?6m&Z`jzdd_jlf{KA-s31l$oEV;>hWEqZn06X_??I@3dQ z#}xXE|7q;Wf|2~Oxf^pn&)S*%eaxew5Bfjtz2CTR==GlFwhhhG8WnxMJ~Q-GNS}SH-9Kbga7A!Q@TDQYg}fT_ub`xWg?=l& zzqM`f`pt9<2M#4Umkqqs_eyU=_l=IFou!?tJG(j-b!2zsAps>FH+F`0?&~{Df^=b3n=l@c`Spi!DcLv`Wwm;&z=pPdX z(!S3*mAyUZ*TRL9o}Dsn($oo$7QUT-W!|jp{VB7egF+tmNwNMgI=lbA4p+k)wM#1- zPTo~qePqJHxAr~1yLe~8&Z@r)cTeB@(f;!fFD%|x{(F6F+e6*l?VRk|Y~Ad0r|++R z3j!|>KHvUH*!1ZAv59eC#$BDTIw?J=IAL?#p&0*|FQPt(*c0|n*hYI?$aec%p(*wa zK{p5O3S8zt&G!Y{QqPR`jV+t>^~}#_r=9lWv@H{NjQex!lrisR9*iFh{oU^guQYS*pxt3> z{C9PG*|gF>k98J3zwh|p|M~OVtI?Wa>Yxz1T> zUg+K8^S9r;!1IDXweN^Xh})EKcVc4FzLaShl^NyfVd)_mMXBKlg>gCYKg88U?~D8) ze6Rf$`;ySZA+H9U?)!+>8|L+{cSj!_3h2A0ySM9vqo%XJ<3xLOTX)OUmbT`h=5w1b zZ#vj8wIQitK|@%>i}n9%T**eyT}{z#wVf|^mG)dQ@UgRZc$|5eb+z}X@9%zR2V5E4 z7}^?fPV^P=*QZX&^%*~Z!oMdRnfCsin6u;0N(4fiC#iE<-D!WX( z$qvt4`n%=0zxMEOO>uhXu-(4Y-rL^aZllF|(;?Q2{wtXWgLrM7>qNnQWiwY3ZC7B^gPGHzkp zv>hLmi!_Wrk=e+_qsDkYVx?fJD9+%GJl15LRh2XD-RR}(-M!DdpmT%qux_C)1KI-r z2`Ub};@`bHR(o6mMIHI{KCI~Nh^ML{f*h1me)S7>s}Y6 z@M*Wd*8ZGTIQ`F;zjx}n=5j@r{sf!HyWukhN#t3{KgJU*7FniSUbMOBSn96xSn2w~ zIl#4xi-RNSu+r|frG?pb2(*6;<75}50!g%3D4Hn9#_G^((EdK2uGBBlY1BehxMFd8 zueQyt!&`r~3~H%uzTSMN=}^<4<^xUF8iMQN8jdyYZCu_YYb|Np)rKqDG-8USY#A%Q z2f7+Pj|~w}!oAYVhFvV3ofo=AyB_pZ^^ygV18xSF208^a_gfzz_n*^qsb^pN2bQDF zj)T2VWS41{cARPAYNeHQ@%21|Ox5Si4--E;`tU0C^t)B>7k!d`9h(VZx45+;{_ooQ z^(~dE-TF!z*x<1gJV9_;y4EDaJj>kMTHzMccg;bCequnTp@a zEafqUtV7nZxTCRM)3&8;N86M3MT%>xliI~Pe|mU?>qL5=lSGCt7Ko zUpt#Q<-5=8MfJ}O)C3Ie>*go!TjO8Qr`_j=3uU#-%-wXY>@Qr5pH?nvt*(o#?kKxj zMCI)IZu@CzTF6I9+M3k+DLc~)K3jfeGiK%NDeSE5pqIVKkjoFqR9JxwUqH9OZuRQ3BKD#<7~l5PEc@u#n8w>~~g zi+O)EW#xyTA1l(e-+%u!Df;kddv!)_YReNX$5fy(qMuUMV5D)P^=YRkuD4wFyRhz$ zy?1wa_pa<#VQg3BpF< z{@8ZJ0W&31iFty@q)e<8KNA~B3=F==?Bzq`AB>e&*7gG(2H6jG)Vh27jOyvvqs;qN zx4oW&-P@f#yG%BX6enTc{2i)Av836v{^;M;6?00Pi-HTSb5ef%O`rPdc-oWqKT>nv z`=*WfbnR5Ua*Z?JKABypHnAh07b;ahT& z;EKSCv?Ol}5(NJU6aq~6S?DUZkat*IwyCgsW3kd=uT7ET8kZFNrB23M#++*<5nvcXomK@5Ew<(w34h#REzdr3)(} zYGaxcTD!N?%FQ|jI|{2K>;-m$bm2(J41+}ZAfqcrjL{yGduDwtFv~1UE2|ln@5~mP z5T+q!eN1l4eGIZBcf{*NFGS}^Inj(wgOlwNwmZLvy-Y3BpVcX$!r4omtO?X^(IlyF zXus-A^dsnttOeX?_duRO_ksbMjCF&Kfhepe7KB?9p7;u)j4UJ%ktJk487H18i6x`R z)uLG87C}5okuM1k;t+X6IFYSR!R;?+#~_waM5yM0(ng^omhg4F<)c`U&0+>jhO=KlipTe zuUn+6*RIoMYj$W}Xf&E8%`wd^%{|QSVo8bG=(xV*h!oNFhyKI;?QZ>K)el4 zAT|no$u}e^oF|+jaun|oPY?%+ABf#0Mb!5T7`=^gZMYP~*2SEkL>e%CJ3)@ZtFlGP5HQ<_YTt=3%Yt=*?3bqd`~ zY7OniFibYfaY;xwED(QL{k8gjb+Qdg;g)Kcmuy)HQ|juDL#9wZ&e z$R+Q zk8ZNQI~720V6L;j;qL7P@*ARf0e*|`|jMTTNO@pSP)vAuYa zs9yL@ctki(c!W$4yd%7b+jtu`1^a;7qN@;K{DY2-J?w90By)~lLQU7_>sobxb#b~h z?M!Wgwm}=D+o4O>z1Nw-t{eb28@r%_Kbh&ynsb-A0KPAxM{c8AuuJ$3A`*7mPja5{ zx6oA-EZQJ?D*7RME}A5IFZ6>Ok4iFz%z&HlKw>6-8%srxA=CK<+!gi^vy9#cJsd0b zCAtB+U)mb2zb;CL>C&Nh!$-GLm!YF|ZuvAJ+(u#vxwM50Gg6x)sU z!E5l{L9@~s%p@!%T z=+QXF--0oGVTLe9Ocgzu{!Fc+`cevgqh3lmQoht+Y6NwMGNYH%8FUzPi?Ly!u|v2L zE(SVf{6G!I5mcLIL!1%e&iEoc9nMs@i9#Zgh$j4rBjA5G5iR&sT!?G1YAg_2hJJw_ zpn84_U(Su;O4wWMc6KV;g?$URA@z(U+rgY*`ZI%==Zu{F&P-({G5eVkCWxKKPGm>1 z*Vvg{HrK?JaYoR`;|5IhRY)C-;4wN1W3aXOFI++_C$2yz!e`@QLiN#|%=t6Wh8jjuumbR|^CaxFs>@chiyPMg}oMW~!Js2Hr!z^aj!&fq+Vw~B# zu+L6%%b;t<5&EFcAkC05TfqHx8S014!%kppuyAZLb`SfEt-zcy4{S92s}E>vtwBRk zcQhaRm;L~|voHUH+YO)go!!YU1ud=yW;D~Ana8|hNS0*Fp;dD%JRifxa__mD+)DV_ zFVJxl0zF24@QMXyFc;t=3x+N_S76ah0}Z5sXb;fKibB7l5m-8=z`kQY;6^?N9}ST{ z7e9($#XsSN_QiiNTThUN#DmDz8fStrN073kX-N3G3@35QL zQkV%1dL4~J$3QpH73lj*<(Ki@`7|z!%V#gJV_6;Z4c7Jr^MJ`^r0hs`1$&Tv%+|AE zFuxnPo!lDk4EK{0^8S1@{N_Pm&+B9WT>?)$7PuA@{$)Nd0Uah1=Jq0*jdnoSjW@Ok z_P`w2(Z{i8Fa~R^9&JKpm<1-lzN2T*6X*x@BmAj?KBZ0_fHY8RddGQkne0Qhh1Ieb z*)Z0Lb%fDuV%M-^*g$q6`<2DHZroHZo$~?4w{qaYTLPTxQ_w`T3N?Ud|BM;q!|)M! zAKVV_g$Ll^^YIq21Ut+G(}F#Qq91{is{y*p_CsVo%IVoe_BeZjoyD57r3}RcutV7Z z)){7a9ebWFVxa`j>e%kw4lb8#=Du>*!3u1lQ|%J)qfP{Ewx!Um)!Fyh+2I=lRzDp5 z2VIA5L$9KZs2es0+Yf7ygr#9$u__G1?P0(D!cwpo*dA;W%#R9v4nJ>?UV;v~M7|II zoD1ZRvoqj12g7J$842UTEQ1wEXCfGPxS7mnmchE0!iY?{!5jg7a!#NLw*U!5w_}$0 z3*1j&CUrKdlUQLPYz?~r8N@bgXK7sZE_^7O#NA}aA}h(BlI_AF#4D(QKNnJP?~=j< za~1q7HeP#O9YgKptuTAApZ#z;9fA$x!`XKB5jU6H#m-=wm>kxG{XqNE;fx74nOnzx zVivRAkk{xKq;uWuxC*YBp9LCrzfcbNdl~4zd&wT8uTpQgsUlAPN)*QX@HFWwdm-+E zE$8F07s7eu9X3iYV+Rmp$xmFV?f@l1R$?dFo%+p;CHe^Qr#8T;@*UhW9ix5pAxx*z zk13Z*9cC10CEg7i$fdBU$VRX(Gt>v2LrBOEggMOX3Bg)&t;j!SUj-OVFmmHHWP^>)x=!d zRG&;uV(uYH>{sd$dkWcrJE7mGCG0iGdcSj@V4v6#rAQ@nfL_48Ab$}LxE)kA=Pv0c zxgeY?$p)ar7P5sO#{7hMHH1#r>{Gr_T~lAvrt2c8uG}+hs(3Hy&L!iH!eY`vLSPMi zS8|Hr5xS7BWa}_^g@>V;F7h@H)i|~^;NFc&x z*iP{fVYMbeZw}G(r|c*xWh~H-!iAE1%q;o^o-4GHT<8DjC^{6=;y$`zS|$D*UyeSa z*0;APHmZxML3D3Q2Y1_g+En+p{S@sXd1SCpXvt&TZgQREAM7>z2{})e%kvE!$pBijKTIn;CUGN+xg6#s+KrTpFOio~8rjRcv(}vq(fBYS z!-;8MdJ2=ki|K=mC8uXL5K(w2^IB)6_d;5+QgjCKR3LzQRSTWN%*Ct(cL^H3#}48r zBKPS5+S%GXT8`(S3Dj}ci$BO6<5zN_xFxwqh)K#YTiyzv3?6kn8iXt+-{Ehe2egT~ z%UtIULQUd1Iuo6WgrIwLAG8CI^=KVW(Z6Y1@O~_J1NPEzt`to`707us6hFlMVOFsz zOb7A|&E%(ZUEz~Z>@!L*o9P^Ws$d-%i7~_z!FVK_gDzoo67~l_$xop#^PjQ5>|ULm zzJOeW^M(j>MoytTca4%ErI0tJAwBq1b~t+u7zoaBmCzOc1${~Qkgu@^$XkpD?7#|a zJH7&2!wscn^bjhUvZP+>UQjo=!FX5v9Pb4&NCkZUV^L3{2k{PHjP*uO@SnLHZY>%v z*eMt(8ZCK5x)HMw0sk4LP)~dw@e1?ggPBx%gzk@eoz|EtVEeGW85SbQVx}uo$=v7v zpbe-uHWgjR4~8Du4kQo#jK>SQ;YoZDbR+LY)}Z6C75D>e2U^DKSv%TTpGDU|L|lf9 z#I$?@V+nU%<8?Ad$o=Np(4&Zw`^#RS&gwJuALwvs&EJBygGbL`&qGIMEWZTT3O0#) z2^IJYVi4hm#o{}OI|4#fLC%M)?-KC~7eVc5q3FBFMRZyi2;F*b;Us*POc9p)ykEhrQY23EkKpayA)?n3UOSFuDqn3za>5k!$M$UCGV=|m1DXNaODg$8=# zJj*D1Z`U5~=iR$`aPFVoSGZnt+TsvnH{0sC@g`{;$#R#qv)e~DHrHt3MsWR~cO{91 zgYvC&*JSU`)c#nN8Tm8)S5`r58CLVPF}XFPV~56--hw2OBMr`(cvvOdO>@}cVD2!` zKEiIhEoXbd&cR-4zt?V@^*)Pyvw9%);CRYmll=`lYn$1Y z2Tfnfn#jROx&D;Or#-)AeRFN2Lt{ZheIo_m$2a&iENN_QdfoD??V7?x9jSdxNw~*| zKYm7#B`gw`OFLvg40)ru#<9jSW6Ve|9|}D~_M%r{b7iO-QpY`EPcbsapZ3?UhMaf2 zra?VNGg%v^ucwxPH#0#-0m$&Kc!0dn^o)&xW2Eai_YjX29zWb-oi}zdvoZiB{mcm2 zy{v-$!CeNt$wQtd;DCz~Mmm^3U+ZzLEdll3yMUQ7Msh&MCYG1;ceuA)U*NE{LS6ylWdL8#{ovMei@Dp}M$lQl$F*`n zz{@@Z@j)|CckB_IW2=CJeh_5iEL2jqK~xG-Qp@CBmT)*fZDTwhw30ViECk3vzBZslfist3|Kc-1vN2Cc;-ZiGz91; zbKC=X@*P|a=LgETCjhOq3f8Gpqx?9`tS3~x>|t(Q;pBb}ayA`$7Sm$U_(xoi`#^2% z8$JWi!@6TXQ5Q5AdCoui7va2TQ=Fb#0>2xC7@_;1S`&^1gZ0>B)3Jxx6Kn|7-da(AEDjq96`62Uj#{Dp z!3GurhWJ6~4uZO>lfjw8vC2+eqB^UoMulFaw1H~aKL=3j4M2t}#R1}oR2=q958r=dF zll!Oy@^>jF!^UEX*gn`3mFP+|0`*2IK-(NePQ%)DYA+uEl`$V!-{0IFSmUo;1E+wj zumV>4Ew>O(8vnq&-+}DC0G?wUV3;O@zViz30VOcvQb4QO0iG-p*gNe2=amnvu~~p$ zvjlEuAHc84|7GMBA}KI8aWL;5fbi-8dH5-qvrabmPOc6oV6(puD6X;S1at(bntP#L zP&HDC0n3Amh%fpa*$RF} zh@6GlR6yNnCfCm1W6!ZWpf=#l7Q)WTVoI47#veS_1nv>w#!dsia2;r9*C9f5IGpUi z!6{URjlfo75m5V5pij}==wzrZegkA#FmT2nKJ^k1EKGf*(t0$#2dursegXS)JU%zr`8ei^X%TZ3xk5_q3bekdUB{NcAJ!Ebx< z?%*G0!@2zw*jqLzv5bY&>qDdzu?FAfii%(_jX>9<7tj~zXS5B~prvRIRLh>D%TODr zzV=3vLHYgy*O#;8D7FbIFbwMg_&o{0nmMTMhI3Ked@h-DhflZ*mZRZ)VCIt{E;ymf z(Nj%Rzjst!hZ)JKAwx^rootoaiQ?{ z72wy7!HTqS-Y_exfb(7kzr7S{WEF@%x*bhHg;-apjNQOq!kU)?`tB~a4c5pG{LE^w zSRQ_^0b<@Iz+=|J=p6Yxu*hJ}pR?yoU^7A$5L;5|F_c%)$8*I`ugfH;hTb*_Uk9Oo8+jk|JPV0;qJ8`fjt&Rc=RRgCJOFD!?75s&kg|lXV5~Z z4bFv#76Z1@0c-ad(55{2x?@mrv*0TshTMRf?<{T(x0>4ue_IdVcf$^A;EW*#b>%03 ziuy5F$$Y>(cCsUxz)FSyPwpbH<9*0eqyTI=AACX`!osT@`GaJ`IUybXd=L1@6Y!+b z|0=h*fY0e)=5T2S_Q42oO{{9?djs=1Hg zQ>WnoOS0H^y7EY_80z^7h?b4vv02T@MU;qXFX=7YHqh^X&40X+X6Kp30A zo;wRqW)9VXQxHWjg1>qMUi>C{5M7K0p+PCY4z?W#2Sb|>FP6`uj4 z>(p^mLv7O)p1E`OBLCH+>eLSm1g3FUsCgp5O`Qud_hU~g4i z8+d-4ry$;2!~cx~m9cQJ)A?{F+6A$zll#Au7t0RbYYD_~`qviDK}5R` z*0K<82`0cwhr{ma)Dg#ldG!bQv`#&x7rBd_$T~54-m$kDDzIH2sy;FPA>6&U_Ctmf!)bD-~k#2elQcA2<*Awt2(uoJJ~9# z!G|V6jxf|>*3%pl9u%??x3-S+Ij;_O$*e2Wo&TMmt zqqsfR3z^FP(tlJpb{uJ~Z<^HjqS2#eQ%Ae@1V3N2z&Orot$m5pd#4opvsR;wO2}wt zsUo6jQ0=?F;dSEXNL2tp-Wj6+OS!Fs?E=f)Muf15>8ki%FRZ#(Zu6&a_0q;nWeXQB z-ftqZ{$bly`5`Ke7C1(`tR7 z=#vEs5`uX?-F+k;m%A95EEde6V%l>Wyy_k`ENa8F9>`?zIFq$j<82;V4m9~C`GEb= zySMDDzFzKC8eck~OjfnHVYK24rN+XrDFFj>@{n8gKC$=bu( z3A#=CWspzKB0dV*1crhn>?yyA%2x+=6trAu-qd`r^@d`W?mL%D{F3m7Crn+<;>`LQ zcazD*NAY;H9PZ}~I3;h4`x8mzdeIx9Au#}5$qmroQN3#I+qStap|!pvMpMT+VHtu% z@hsVFS%&xlsls6Y(@i=TDwhu8bJ28ir8GhoBCD5T;vM)~_O52I!lU(IQ)ctlw%e+$ zls8f#ND)UEER}^y4MdSdG(yogbeFYX;NCxt>cOXBzJecQn@BG%5snc!;=fQ6WEcC5 z{>Z#wFECx%w-6KbP=(t{ucb~=x2Yja4%Y=Ug8uVKl9AGx5{=MIfTCqgl>WB5pUMSp z@$YLjv@3!US>$domdPR!xdjKlXS!aCYvyP+YYEDpdx$xR3=M4MGNaS-?-IH23z|s} z(>5wKirK0w>PdQ*JBp1Kd>}`V!Gbo-mwQFqQqekZeF&AveBn1>0DTo23FFBCay-!w zy~BNB4>CgPxPCER%H^VV_)GGPWVdv%c#_}~x|IE*{nn8VeVYmG6^fIZ^|S@x34V}% z;vUju$uDs!xsRyDrt@)3Jv_C5?ajHO=LJ@xwd4Z91wf$sBFiW})kr_1BJ?e^CuT^B z$Z^6X(Pcp+x{M9ekJq?$oNk>8$Yza7te?R7lg_ej#%hxUlNOUC`3EAEjn>JOuiH{u zZzv0OyBHdalA^|kj8@71k;O>6VNp~UO^jkvdwq+4Yo#)kGDXU;>EtKq9#)E#0xv>= z{G_C6dv&w6Nk5noq7K5z(D`snd{W{jevLZNoc z=fNMRrfBEr;wc+?B;1A^L2nWlh}ZaS;yUpFF=ugRBcr8F>HW+~gOjEaE%s)u?*xGNjQ)gVd4 zGQmw;MqGq?y9T)gciQQYt1ahC5ga3+qS}U^B=!-RxCJ&HorI?HEZlR?W~Z>Ov{tWV z#i$=2Pg^oUd=JimnZ&L@1c0e8Aa;}6gg?lh!W6+_{5S4ypfX82Y9AhSzXq-;{Y(FL$| z(DB-r$QDT?!NQfe7y1Y)*q3zuwXV7b%87O3y)hXXBc3ghi^q~P@I}Z#)<$2Xeh>XN zf2lHdF1nEHCDBVf#79Xz{s@^thiGOi2P?0shv*vUV6;sTEaoI?$sh3o;S9V7pG00d?^!AOz8Z0fHlVA)3VR z1)Eq!&!o$lL0k|*q4#ie@-w-dv>@jYWmqrt6lcg{%v7Lnn#mNgZO9eu8~zsR?u1|m zE`uA8*(?J+79sj=`qj{3agNR5gV3G$R6xwSju6p93|T z0O~Hak#1!~aB8mMdP1FP7v!2Y&==_hSz%uO}{dEpLjJ>8k2p7ykbCDYfa9@yn)HF{#q8cXM*f((30cw(2o%5mJpV#NZr? zWbh{t8Y?C*8Z?gWWgtWGQgIDU<8n{tWuvJztYwTJ)}>u_0a+fCNit znhmoxcIfI9=RDhKvwez9kYyiptx2QdG}$A`XW?Glo*klzZ`)Eov8t*xrs#6flhTn@ zlE!O_EzBg+P;PH3F&|^L+<2OF2)>UtRHwEdXx-fUspFvbGPjDP4b~V>Fzah3G43v1 zi4D^`c7!$Tt;Q?IRGqG0+o7a1g6+oHwohCicKhU`=}z|K{4ECc2;V;J(x^-0MA3c5 z)eH{rQ_@9-;uTllt+`~n6WfYx?fhD`t?$Le)MZtNku|o#zAuMJM)(e1*Z)V)Hm7!@ zw?r_upA<^&$yS)%v^m=)$L5*QPjpTjUVQX(=+kA1?8RMIitpZg`#kqqTe;zk?h)Zt zQOfCVv**mvPgp;AXwMw`DMmP+t~t^c-g>dEtx%g4Y*gw|Dtq#9m8wxTctonP42*+(p2rv5FF9qt)8x^eFP75oy()bNo( zefQaKB{JGHwMKPS^;=ua)JvHpEP?Rnqg1k*$Xq_n>zU(itHj{D9WO0@E^AyR?(LN` zqTlRq@s4XtSIl2bCZwx_?Jtb5-r~ zs)ULwRVy1Rl%J4oMhl#hx*zjL1FZrDelI<)nC(X6+Ee~4|8?~1>-5b(qI2s@EgBGw zC!9?d3KM}2C|~os`Ed2q(&fLo!inWm8$EPCh3l<7yu$hyh7KI^G>jZL#s7HM>$Z0d z$A|*ON|WFA@~)G7vB6biyGCD&h!6bXS|POi`{u3Zng5*)Wb5nw#!1Hun~5Z^Yr_j? zJY2eG**`Pxhkx;YZ{nysTrup|i_AT_%5sO+2zHiuhw(x)Go#CRb9-LN;4cTBZ@x3^ z=G-KYl;%QnU7@vE;J4Vf%NK0U+!(kFnN|}v#%&PcUFV*=F#TvsZEEGW0fnO4NveLx zd(lft6KR9;x&y82YK(#D;c&w=RV?<@EX;Fe-ywsV!sZOR(6`OaNv>4;{)zZ;n$dp~^-`ajlz zY_&Ld^Swh`vF*A6*JI-rUS4jvXu^ae{&qI@d~jWM{=tk>nQ?^|Y85)UB-nhY&1vhE z#>cUg)^~-mX|+#g+}(dS`I*sIMRl-1;*NyPoYrS~-?e+=BWA4|>DoKM;(>1VpX(XP z@4Zsyer(7XRg_o5b*$jJgO9Zmea4sY3u&=-uzH>@gWE{f$TgNFj#E7PdG>SZZL?HX z%vQDR{&PQXRp#XHU$Q0?kE%bb89>}Ma?sK~u6O4;lTa?@a3| zWzdgp*Z$in|9K6+<9kv^D8{+G9A�O2hDSUHo8Vt>+JNdcA4(fcF#L@b62rmRC~B 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 literal 0 HcmV?d00001 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