[voice] Changed annotation of `getPreferredVoice` method to allow null results (#2186)

Signed-off-by: Christoph Weitkamp <github@christophweitkamp.de>
pull/2190/head
Christoph Weitkamp 2021-02-09 19:26:49 +01:00 committed by GitHub
parent 44c280e249
commit 76e0cfdb81
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
4 changed files with 91 additions and 63 deletions

View File

@ -115,8 +115,9 @@ public interface VoiceManager {
* Determines the preferred voice for the currently set locale * Determines the preferred voice for the currently set locale
* *
* @param voices a set of voices to chose from * @param voices a set of voices to chose from
* @return the preferred voice for the current locale * @return the preferred voice for the current locale, or null if no voice can be found
*/ */
@Nullable
Voice getPreferredVoice(Set<Voice> voices); Voice getPreferredVoice(Set<Voice> voices);
/** /**

View File

@ -112,7 +112,7 @@ public class VoiceConsoleCommandExtension extends AbstractConsoleCommandExtensio
if (defaultVoice == null) { if (defaultVoice == null) {
TTSService tts = voiceManager.getTTS(); TTSService tts = voiceManager.getTTS();
if (tts != null) { if (tts != null) {
defaultVoice = voiceManager.getPreferredVoice(tts.getAvailableVoices()); return voiceManager.getPreferredVoice(tts.getAvailableVoices());
} }
} }
return defaultVoice; return defaultVoice;

View File

@ -12,8 +12,6 @@
*/ */
package org.openhab.core.voice.internal; package org.openhab.core.voice.internal;
import static java.util.stream.Collectors.*;
import java.io.IOException; import java.io.IOException;
import java.net.URI; import java.net.URI;
import java.util.ArrayList; import java.util.ArrayList;
@ -28,7 +26,9 @@ import java.util.Locale;
import java.util.Map; import java.util.Map;
import java.util.Objects; import java.util.Objects;
import java.util.Set; import java.util.Set;
import java.util.stream.Collectors;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable; import org.eclipse.jdt.annotation.Nullable;
import org.openhab.core.audio.AudioFormat; import org.openhab.core.audio.AudioFormat;
import org.openhab.core.audio.AudioManager; import org.openhab.core.audio.AudioManager;
@ -73,6 +73,7 @@ import org.slf4j.LoggerFactory;
@Component(immediate = true, configurationPid = VoiceManagerImpl.CONFIGURATION_PID, // @Component(immediate = true, configurationPid = VoiceManagerImpl.CONFIGURATION_PID, //
property = Constants.SERVICE_PID + "=org.openhab.voice") property = Constants.SERVICE_PID + "=org.openhab.voice")
@ConfigurableService(category = "system", label = "Voice", description_uri = VoiceManagerImpl.CONFIG_URI) @ConfigurableService(category = "system", label = "Voice", description_uri = VoiceManagerImpl.CONFIG_URI)
@NonNullByDefault
public class VoiceManagerImpl implements VoiceManager, ConfigOptionProvider { public class VoiceManagerImpl implements VoiceManager, ConfigOptionProvider {
public static final String CONFIGURATION_PID = "org.openhab.voice"; public static final String CONFIGURATION_PID = "org.openhab.voice";
@ -107,12 +108,12 @@ public class VoiceManagerImpl implements VoiceManager, ConfigOptionProvider {
* default settings filled through the service configuration * default settings filled through the service configuration
*/ */
private String keyword = DEFAULT_KEYWORD; private String keyword = DEFAULT_KEYWORD;
private String listeningItem; private @Nullable String listeningItem;
private String defaultTTS; private @Nullable String defaultTTS;
private String defaultSTT; private @Nullable String defaultSTT;
private String defaultKS; private @Nullable String defaultKS;
private String defaultHLI; private @Nullable String defaultHLI;
private String defaultVoice; private @Nullable String defaultVoice;
private final Map<String, String> defaultVoices = new HashMap<>(); private final Map<String, String> defaultVoices = new HashMap<>();
@Activate @Activate
@ -128,6 +129,7 @@ public class VoiceManagerImpl implements VoiceManager, ConfigOptionProvider {
modified(config); modified(config);
} }
@SuppressWarnings("null")
@Modified @Modified
protected void modified(Map<String, Object> config) { protected void modified(Map<String, Object> config) {
if (config != null) { if (config != null) {
@ -157,7 +159,7 @@ public class VoiceManagerImpl implements VoiceManager, ConfigOptionProvider {
} }
@Override @Override
public void say(String text, PercentType volume) { public void say(String text, @Nullable PercentType volume) {
say(text, null, null, volume); say(text, null, null, volume);
} }
@ -167,17 +169,17 @@ public class VoiceManagerImpl implements VoiceManager, ConfigOptionProvider {
} }
@Override @Override
public void say(String text, String voiceId, PercentType volume) { public void say(String text, @Nullable String voiceId, @Nullable PercentType volume) {
say(text, voiceId, null, volume); say(text, voiceId, null, volume);
} }
@Override @Override
public void say(String text, String voiceId, String sinkId) { public void say(String text, @Nullable String voiceId, @Nullable String sinkId) {
say(text, voiceId, sinkId, null); say(text, voiceId, sinkId, null);
} }
@Override @Override
public void say(String text, String voiceId, String sinkId, PercentType volume) { public void say(String text, @Nullable String voiceId, @Nullable String sinkId, @Nullable PercentType volume) {
Objects.requireNonNull(text, "Text cannot be said as it is null."); Objects.requireNonNull(text, "Text cannot be said as it is null.");
try { try {
@ -265,7 +267,7 @@ public class VoiceManagerImpl implements VoiceManager, ConfigOptionProvider {
} }
@Override @Override
public String interpret(String text, String hliId) throws InterpretationException { public String interpret(String text, @Nullable String hliId) throws InterpretationException {
HumanLanguageInterpreter interpreter; HumanLanguageInterpreter interpreter;
if (hliId == null) { if (hliId == null) {
interpreter = getHLI(); interpreter = getHLI();
@ -281,7 +283,7 @@ public class VoiceManagerImpl implements VoiceManager, ConfigOptionProvider {
return interpreter.interpret(localeProvider.getLocale(), text); return interpreter.interpret(localeProvider.getLocale(), text);
} }
private Voice getVoice(String id) { private @Nullable Voice getVoice(String id) {
if (id.contains(":")) { if (id.contains(":")) {
// it is a fully qualified unique id // it is a fully qualified unique id
String[] segments = id.split(":"); String[] segments = id.split(":");
@ -299,7 +301,7 @@ public class VoiceManagerImpl implements VoiceManager, ConfigOptionProvider {
return null; return null;
} }
private Voice getVoice(Set<Voice> voices, String id) { private @Nullable Voice getVoice(Set<Voice> voices, String id) {
for (Voice voice : voices) { for (Voice voice : voices) {
if (voice.getUID().endsWith(":" + id)) { if (voice.getUID().endsWith(":" + id)) {
return voice; return voice;
@ -308,7 +310,7 @@ public class VoiceManagerImpl implements VoiceManager, ConfigOptionProvider {
return null; return null;
} }
public static AudioFormat getPreferredFormat(Set<AudioFormat> audioFormats) { public static @Nullable AudioFormat getPreferredFormat(Set<AudioFormat> audioFormats) {
// Return the first concrete AudioFormat found // Return the first concrete AudioFormat found
for (AudioFormat currentAudioFormat : audioFormats) { for (AudioFormat currentAudioFormat : audioFormats) {
// Check if currentAudioFormat is abstract // Check if currentAudioFormat is abstract
@ -403,7 +405,7 @@ public class VoiceManagerImpl implements VoiceManager, ConfigOptionProvider {
return null; return null;
} }
public static AudioFormat getBestMatch(Set<AudioFormat> inputs, Set<AudioFormat> outputs) { public static @Nullable AudioFormat getBestMatch(Set<AudioFormat> inputs, Set<AudioFormat> outputs) {
AudioFormat preferredFormat = getPreferredFormat(inputs); AudioFormat preferredFormat = getPreferredFormat(inputs);
for (AudioFormat output : outputs) { for (AudioFormat output : outputs) {
if (output.isCompatible(preferredFormat)) { if (output.isCompatible(preferredFormat)) {
@ -420,7 +422,7 @@ public class VoiceManagerImpl implements VoiceManager, ConfigOptionProvider {
} }
@Override @Override
public Voice getPreferredVoice(Set<Voice> voices) { public @Nullable Voice getPreferredVoice(Set<Voice> voices) {
// Express preferences with a Language Priority List // Express preferences with a Language Priority List
Locale locale = localeProvider.getLocale(); Locale locale = localeProvider.getLocale();
@ -436,9 +438,12 @@ public class VoiceManagerImpl implements VoiceManager, ConfigOptionProvider {
Locale preferredLocale = Locale.lookup(languageRanges, locales); Locale preferredLocale = Locale.lookup(languageRanges, locales);
// As a last resort choose some Locale // As a last resort choose some Locale
if (preferredLocale == null) { if (preferredLocale == null && !voices.isEmpty()) {
preferredLocale = locales.iterator().next(); preferredLocale = locales.iterator().next();
} }
if (preferredLocale == null) {
return null;
}
// Determine preferred voice // Determine preferred voice
Voice preferredVoice = null; Voice preferredVoice = null;
@ -447,7 +452,6 @@ public class VoiceManagerImpl implements VoiceManager, ConfigOptionProvider {
preferredVoice = currentVoice; preferredVoice = currentVoice;
} }
} }
assert (preferredVoice != null);
// Return preferred voice // Return preferred voice
return preferredVoice; return preferredVoice;
@ -459,9 +463,10 @@ public class VoiceManagerImpl implements VoiceManager, ConfigOptionProvider {
} }
@Override @Override
public void startDialog(KSService ksService, STTService sttService, TTSService ttsService, public void startDialog(@Nullable KSService ksService, @Nullable STTService sttService,
HumanLanguageInterpreter interpreter, AudioSource audioSource, AudioSink audioSink, Locale locale, @Nullable TTSService ttsService, @Nullable HumanLanguageInterpreter interpreter,
String keyword, String listeningItem) { @Nullable AudioSource audioSource, @Nullable AudioSink audioSink, @Nullable Locale locale,
@Nullable String keyword, @Nullable String listeningItem) {
// use defaults, if null // use defaults, if null
KSService ks = (ksService == null) ? getKS() : ksService; KSService ks = (ksService == null) ? getKS() : ksService;
STTService stt = (sttService == null) ? getSTT() : sttService; STTService stt = (sttService == null) ? getSTT() : sttService;
@ -522,7 +527,7 @@ public class VoiceManagerImpl implements VoiceManager, ConfigOptionProvider {
} }
@Override @Override
public TTSService getTTS() { public @Nullable TTSService getTTS() {
TTSService tts = null; TTSService tts = null;
if (defaultTTS != null) { if (defaultTTS != null) {
tts = ttsServices.get(defaultTTS); tts = ttsServices.get(defaultTTS);
@ -538,11 +543,11 @@ public class VoiceManagerImpl implements VoiceManager, ConfigOptionProvider {
} }
@Override @Override
public TTSService getTTS(String id) { public @Nullable TTSService getTTS(String id) {
return ttsServices.get(id); return ttsServices.get(id);
} }
private TTSService getTTS(Voice voice) { private @Nullable TTSService getTTS(Voice voice) {
return getTTS(voice.getUID().split(":")[0]); return getTTS(voice.getUID().split(":")[0]);
} }
@ -552,7 +557,7 @@ public class VoiceManagerImpl implements VoiceManager, ConfigOptionProvider {
} }
@Override @Override
public STTService getSTT() { public @Nullable STTService getSTT() {
STTService stt = null; STTService stt = null;
if (defaultTTS != null) { if (defaultTTS != null) {
stt = sttServices.get(defaultSTT); stt = sttServices.get(defaultSTT);
@ -568,7 +573,7 @@ public class VoiceManagerImpl implements VoiceManager, ConfigOptionProvider {
} }
@Override @Override
public STTService getSTT(String id) { public @Nullable STTService getSTT(String id) {
return sttServices.get(id); return sttServices.get(id);
} }
@ -578,7 +583,7 @@ public class VoiceManagerImpl implements VoiceManager, ConfigOptionProvider {
} }
@Override @Override
public KSService getKS() { public @Nullable KSService getKS() {
KSService ks = null; KSService ks = null;
if (defaultKS != null) { if (defaultKS != null) {
ks = ksServices.get(defaultKS); ks = ksServices.get(defaultKS);
@ -594,7 +599,7 @@ public class VoiceManagerImpl implements VoiceManager, ConfigOptionProvider {
} }
@Override @Override
public KSService getKS(String id) { public @Nullable KSService getKS(String id) {
return ksServices.get(id); return ksServices.get(id);
} }
@ -604,7 +609,7 @@ public class VoiceManagerImpl implements VoiceManager, ConfigOptionProvider {
} }
@Override @Override
public HumanLanguageInterpreter getHLI() { public @Nullable HumanLanguageInterpreter getHLI() {
HumanLanguageInterpreter hli = null; HumanLanguageInterpreter hli = null;
if (defaultHLI != null) { if (defaultHLI != null) {
hli = humanLanguageInterpreters.get(defaultHLI); hli = humanLanguageInterpreters.get(defaultHLI);
@ -620,7 +625,7 @@ public class VoiceManagerImpl implements VoiceManager, ConfigOptionProvider {
} }
@Override @Override
public HumanLanguageInterpreter getHLI(String id) { public @Nullable HumanLanguageInterpreter getHLI(String id) {
return humanLanguageInterpreters.get(id); return humanLanguageInterpreters.get(id);
} }
@ -636,8 +641,8 @@ public class VoiceManagerImpl implements VoiceManager, ConfigOptionProvider {
private Set<Voice> getAllVoicesSorted(Locale locale) { private Set<Voice> getAllVoicesSorted(Locale locale) {
return ttsServices.values().stream().map(s -> s.getAvailableVoices()).flatMap(Collection::stream) return ttsServices.values().stream().map(s -> s.getAvailableVoices()).flatMap(Collection::stream)
.sorted(createVoiceComparator(locale)) .sorted(createVoiceComparator(locale)).collect(Collectors
.collect(collectingAndThen(toCollection(LinkedHashSet::new), Collections::unmodifiableSet)); .collectingAndThen(Collectors.toCollection(LinkedHashSet::new), Collections::unmodifiableSet));
} }
/** /**
@ -653,7 +658,10 @@ public class VoiceManagerImpl implements VoiceManager, ConfigOptionProvider {
*/ */
private Comparator<Voice> createVoiceComparator(Locale locale) { private Comparator<Voice> createVoiceComparator(Locale locale) {
Comparator<Voice> byTTSLabel = (Voice v1, Voice v2) -> { Comparator<Voice> byTTSLabel = (Voice v1, Voice v2) -> {
return getTTS(v1).getLabel(locale).compareToIgnoreCase(getTTS(v2).getLabel(locale)); TTSService tts1 = getTTS(v1);
TTSService tts2 = getTTS(v2);
return (tts1 == null || tts2 == null) ? 0
: tts1.getLabel(locale).compareToIgnoreCase(tts2.getLabel(locale));
}; };
Comparator<Voice> byVoiceLocale = (Voice v1, Voice v2) -> { Comparator<Voice> byVoiceLocale = (Voice v1, Voice v2) -> {
return v1.getLocale().getDisplayName(locale).compareToIgnoreCase(v2.getLocale().getDisplayName(locale)); return v1.getLocale().getDisplayName(locale).compareToIgnoreCase(v2.getLocale().getDisplayName(locale));
@ -663,37 +671,42 @@ public class VoiceManagerImpl implements VoiceManager, ConfigOptionProvider {
@Override @Override
public @Nullable Voice getDefaultVoice() { public @Nullable Voice getDefaultVoice() {
return defaultVoice != null ? getVoice(defaultVoice) : null; String localDefaultVoice = defaultVoice;
return localDefaultVoice != null ? getVoice(localDefaultVoice) : null;
} }
@Override @Override
public Collection<ParameterOption> getParameterOptions(URI uri, String param, @Nullable String context, public @Nullable Collection<ParameterOption> getParameterOptions(URI uri, String param, @Nullable String context,
@Nullable Locale locale) { @Nullable Locale locale) {
if (CONFIG_URI.equals(uri.toString())) { if (CONFIG_URI.equals(uri.toString())) {
if (CONFIG_DEFAULT_HLI.equals(param)) { switch (param) {
return humanLanguageInterpreters.values().stream() case CONFIG_DEFAULT_HLI:
.sorted((hli1, hli2) -> hli1.getLabel(locale).compareToIgnoreCase(hli2.getLabel(locale))) return humanLanguageInterpreters.values().stream()
.map(hli -> new ParameterOption(hli.getId(), hli.getLabel(locale))).collect(toList()); .sorted((hli1, hli2) -> hli1.getLabel(locale).compareToIgnoreCase(hli2.getLabel(locale)))
} else if (CONFIG_DEFAULT_KS.equals(param)) { .map(hli -> new ParameterOption(hli.getId(), hli.getLabel(locale)))
return ksServices.values().stream() .collect(Collectors.toList());
.sorted((ks1, ks2) -> ks1.getLabel(locale).compareToIgnoreCase(ks2.getLabel(locale))) case CONFIG_DEFAULT_KS:
.map(ks -> new ParameterOption(ks.getId(), ks.getLabel(locale))).collect(toList()); return ksServices.values().stream()
} else if (CONFIG_DEFAULT_STT.equals(param)) { .sorted((ks1, ks2) -> ks1.getLabel(locale).compareToIgnoreCase(ks2.getLabel(locale)))
return sttServices.values().stream() .map(ks -> new ParameterOption(ks.getId(), ks.getLabel(locale)))
.sorted((stt1, stt2) -> stt1.getLabel(locale).compareToIgnoreCase(stt2.getLabel(locale))) .collect(Collectors.toList());
.map(stt -> new ParameterOption(stt.getId(), stt.getLabel(locale))).collect(toList()); case CONFIG_DEFAULT_STT:
} else if (CONFIG_DEFAULT_TTS.equals(param)) { return sttServices.values().stream()
return ttsServices.values().stream() .sorted((stt1, stt2) -> stt1.getLabel(locale).compareToIgnoreCase(stt2.getLabel(locale)))
.sorted((tts1, tts2) -> tts1.getLabel(locale).compareToIgnoreCase(tts2.getLabel(locale))) .map(stt -> new ParameterOption(stt.getId(), stt.getLabel(locale)))
.map(tts -> new ParameterOption(tts.getId(), tts.getLabel(locale))).collect(toList()); .collect(Collectors.toList());
} else if (CONFIG_DEFAULT_VOICE.equals(param)) { case CONFIG_DEFAULT_TTS:
Locale nullSafeLocale = locale != null ? locale : localeProvider.getLocale(); return ttsServices.values().stream()
return getAllVoicesSorted(nullSafeLocale) .sorted((tts1, tts2) -> tts1.getLabel(locale).compareToIgnoreCase(tts2.getLabel(locale)))
.stream().filter(v -> getTTS(v) != null).map( .map(tts -> new ParameterOption(tts.getId(), tts.getLabel(locale)))
v -> new ParameterOption(v.getUID(), .collect(Collectors.toList());
String.format("%s - %s - %s", getTTS(v).getLabel(nullSafeLocale), case CONFIG_DEFAULT_VOICE:
v.getLocale().getDisplayName(nullSafeLocale), v.getLabel()))) Locale nullSafeLocale = locale != null ? locale : localeProvider.getLocale();
.collect(toList()); return getAllVoicesSorted(nullSafeLocale).stream().filter(v -> getTTS(v) != null)
.map(v -> new ParameterOption(v.getUID(),
String.format("%s - %s - %s", getTTS(v).getLabel(nullSafeLocale),
v.getLocale().getDisplayName(nullSafeLocale), v.getLabel())))
.collect(Collectors.toList());
} }
} }
return null; return null;

View File

@ -23,12 +23,14 @@ import java.util.Collection;
import java.util.Dictionary; import java.util.Dictionary;
import java.util.Hashtable; import java.util.Hashtable;
import java.util.Locale; import java.util.Locale;
import java.util.Set;
import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test; import org.junit.jupiter.api.Test;
import org.openhab.core.audio.AudioManager; import org.openhab.core.audio.AudioManager;
import org.openhab.core.config.core.ParameterOption; import org.openhab.core.config.core.ParameterOption;
import org.openhab.core.test.java.JavaOSGiTest; import org.openhab.core.test.java.JavaOSGiTest;
import org.openhab.core.voice.Voice;
import org.openhab.core.voice.VoiceManager; import org.openhab.core.voice.VoiceManager;
import org.openhab.core.voice.text.InterpretationException; import org.openhab.core.voice.text.InterpretationException;
import org.osgi.framework.BundleContext; import org.osgi.framework.BundleContext;
@ -373,4 +375,16 @@ public class VoiceManagerImplTest extends JavaOSGiTest {
assertTrue(isVoiceStubInTheOptions); assertTrue(isVoiceStubInTheOptions);
} }
@Test
public void getPreferredVoiceOfAvailableTTSService() {
Voice voice = voiceManager.getPreferredVoice(ttsService.getAvailableVoices());
assertNotNull(voice);
}
@Test
public void getPreferredVoiceOfEmptySet() {
Voice voice = voiceManager.getPreferredVoice(Set.of());
assertNull(voice);
}
} }