/** * Copyright (c) 2014-2017 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.net.URI; import java.util.ArrayList; import java.util.Collection; import java.util.HashMap; import java.util.HashSet; import java.util.List; import java.util.Locale; import java.util.Map; import java.util.Set; import org.eclipse.smarthome.config.core.ConfigOptionProvider; import org.eclipse.smarthome.config.core.ParameterOption; import org.eclipse.smarthome.core.audio.AudioFormat; import org.eclipse.smarthome.core.audio.AudioManager; 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.KSService; import org.eclipse.smarthome.core.voice.STTService; 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.VoiceManager; import org.eclipse.smarthome.core.voice.text.HumanLanguageInterpreter; import org.eclipse.smarthome.core.voice.text.InterpretationException; 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 VoiceManagerImpl implements VoiceManager, ConfigOptionProvider { // the default keyword to use if no other is configured private static final String DEFAULT_KEYWORD = "Wakeup"; // constants for the configuration properties private static final String CONFIG_URI = "system:voice"; private static final String CONFIG_KEYWORD = "keyword"; private static final String CONFIG_DEFAULT_HLI = "defaultHLI"; private static final String CONFIG_DEFAULT_KS = "defaultKS"; private static final String CONFIG_DEFAULT_STT = "defaultSTT"; private static final String CONFIG_DEFAULT_TTS = "defaultTTS"; private static final String CONFIG_DEFAULT_VOICE = "defaultVoice"; private static final String CONFIG_PREFIX_DEFAULT_VOICE = "defaultVoice."; private final Logger logger = LoggerFactory.getLogger(VoiceManagerImpl.class); // service maps private Map<String, KSService> ksServices = new HashMap<>(); private Map<String, STTService> sttServices = new HashMap<>(); private Map<String, TTSService> ttsServices = new HashMap<>(); private Map<String, HumanLanguageInterpreter> humanLanguageInterpreters = new HashMap<>(); private LocaleProvider localeProvider = null; /** * default settings filled through the service configuration */ private String keyword = DEFAULT_KEYWORD; private String defaultTTS = null; private String defaultSTT = null; private String defaultKS = null; private String defaultHLI = null; private String defaultVoice = null; private Map<String, String> defaultVoices = new HashMap<>(); private AudioManager audioManager; protected void activate(Map<String, Object> config) { modified(config); } protected void deactivate() { } protected void modified(Map<String, Object> config) { if (config != null) { this.keyword = config.containsKey(CONFIG_KEYWORD) ? config.get(CONFIG_KEYWORD).toString() : DEFAULT_KEYWORD; this.defaultTTS = config.containsKey(CONFIG_DEFAULT_TTS) ? config.get(CONFIG_DEFAULT_TTS).toString() : null; this.defaultSTT = config.containsKey(CONFIG_DEFAULT_STT) ? config.get(CONFIG_DEFAULT_STT).toString() : null; this.defaultKS = config.containsKey(CONFIG_DEFAULT_KS) ? config.get(CONFIG_DEFAULT_KS).toString() : null; this.defaultHLI = config.containsKey(CONFIG_DEFAULT_HLI) ? config.get(CONFIG_DEFAULT_HLI).toString() : null; this.defaultVoice = config.containsKey(CONFIG_DEFAULT_VOICE) ? config.get(CONFIG_DEFAULT_VOICE).toString() : null; for (String key : config.keySet()) { if (key.startsWith(CONFIG_PREFIX_DEFAULT_VOICE)) { String tts = key.substring(CONFIG_PREFIX_DEFAULT_VOICE.length()); defaultVoices.put(tts, config.get(key).toString()); } } } } @Override public void say(String text) { say(text, null); } @Override public void say(String text, String voiceId) { say(text, voiceId, null); } @Override public void say(String text, String voiceId, String sinkId) { try { TTSService tts = null; Voice voice = null; if (voiceId == null) { // use the configured default, if set voiceId = defaultVoice; } if (voiceId == null) { tts = getTTS(); if (tts != null) { 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 (voice == null) { throw new TTSException( "Unable to find a voice for language " + localeProvider.getLocale().getLanguage()); } if (tts == null) { throw new TTSException("No TTS service can be found for voice " + voiceId); } Set<AudioFormat> audioFormats = tts.getSupportedFormats(); AudioSink sink = null; if (sinkId == null) { sink = audioManager.getSink(); } else { sink = audioManager.getSink(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, e.getMessage()); } } @Override public String interpret(String text) throws InterpretationException { return interpret(text, null); } @Override public String interpret(String text, String hliId) throws InterpretationException { HumanLanguageInterpreter interpreter; if (hliId == null) { interpreter = getHLI(); if (interpreter == null) { throw new InterpretationException("No human language interpreter available!"); } } else { interpreter = getHLI(hliId); if (interpreter == null) { throw new InterpretationException("No human language interpreter can be found for " + hliId); } } return interpreter.interpret(localeProvider.getLocale(), text); } private Voice getVoice(Set<Voice> voices, String id) { for (Voice voice : voices) { if (voice.getUID().endsWith(":" + id)) { return voice; } } return null; } public static AudioFormat getPreferredFormat(Set<AudioFormat> 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 = 44100; // 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; } public static AudioFormat getBestMatch(Set<AudioFormat> inputs, Set<AudioFormat> 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; } @Override public Voice getPreferredVoice(Set<Voice> voices) { // Express preferences with a Language Priority List Locale locale = localeProvider.getLocale(); // Get collection of voice locales Collection<Locale> locales = new ArrayList<Locale>(); 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<Locale.LanguageRange> 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; } @Override public void startDialog() { startDialog(null, null, null, null, null, null, null, this.keyword); } @Override 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) ? audioManager.getSource() : source; sink = (sink == null) ? audioManager.getSink() : sink; locale = (locale == null) ? localeProvider.getLocale() : locale; keyword = (keyword == null) ? this.keyword : keyword; if (ks != null && stt != null && tts != null && hli != null && source != null && sink != null && locale != null && keyword != null) { DialogProcessor processor = new DialogProcessor(ks, stt, tts, hli, source, sink, locale, 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 setAudioManager(AudioManager audioManager) { this.audioManager = audioManager; } protected void unsetAudioManager(AudioManager audioManager) { this.audioManager = null; } @Override 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.isEmpty()) { tts = ttsServices.values().iterator().next(); } else { logger.debug("No TTS service available!"); } return tts; } @Override public TTSService getTTS(String id) { return ttsServices.get(id); } @Override public Collection<TTSService> getTTSs() { return new HashSet<>(ttsServices.values()); } @Override 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.isEmpty()) { stt = sttServices.values().iterator().next(); } else { logger.debug("No STT service available!"); } return stt; } @Override public STTService getSTT(String id) { return sttServices.get(id); } @Override public Collection<STTService> getSTTs() { return new HashSet<>(sttServices.values()); } @Override 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.isEmpty()) { ks = ksServices.values().iterator().next(); } else { logger.debug("No KS service available!"); } return ks; } @Override public KSService getKS(String id) { return ksServices.get(id); } @Override public Collection<KSService> getKSs() { return new HashSet<>(ksServices.values()); } @Override 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.isEmpty()) { hli = humanLanguageInterpreters.values().iterator().next(); } else { logger.debug("No HumanLanguageInterpreter available!"); } return hli; } @Override public HumanLanguageInterpreter getHLI(String id) { return humanLanguageInterpreters.get(id); } @Override public Collection<HumanLanguageInterpreter> getHLIs() { return new HashSet<>(humanLanguageInterpreters.values()); } @Override public Set<Voice> getAllVoices() { Set<Voice> voices = new HashSet<>(); for (TTSService tts : ttsServices.values()) { voices.addAll(tts.getAvailableVoices()); } return voices; } @Override public Collection<ParameterOption> getParameterOptions(URI uri, String param, Locale locale) { if (uri.toString().equals(CONFIG_URI)) { if (CONFIG_DEFAULT_HLI.equals(param)) { List<ParameterOption> options = new ArrayList<>(); for (HumanLanguageInterpreter hli : humanLanguageInterpreters.values()) { ParameterOption option = new ParameterOption(hli.getId(), hli.getLabel(locale)); options.add(option); } return options; } else if (CONFIG_DEFAULT_KS.equals(param)) { List<ParameterOption> options = new ArrayList<>(); for (KSService ks : ksServices.values()) { ParameterOption option = new ParameterOption(ks.getId(), ks.getLabel(locale)); options.add(option); } return options; } else if (CONFIG_DEFAULT_STT.equals(param)) { List<ParameterOption> options = new ArrayList<>(); for (STTService stt : sttServices.values()) { ParameterOption option = new ParameterOption(stt.getId(), stt.getLabel(locale)); options.add(option); } return options; } else if (CONFIG_DEFAULT_TTS.equals(param)) { List<ParameterOption> options = new ArrayList<>(); for (TTSService tts : ttsServices.values()) { ParameterOption option = new ParameterOption(tts.getId(), tts.getLabel(locale)); options.add(option); } return options; } else if (CONFIG_DEFAULT_VOICE.equals(param)) { List<ParameterOption> options = new ArrayList<>(); for (Voice voice : getAllVoices()) { ParameterOption option = new ParameterOption(voice.getUID(), voice.getLabel() + " - " + voice.getLocale().getDisplayName()); options.add(option); } return options; } } return null; } }