mirror of
https://github.com/danieldemus/openhab-addons
synced 2026-07-29 12:34:21 +02:00
[pipertts] Add quality to UID/label & Code improvements (#20201)
* [pipertts] Add quality to UID and label This allows to distinct between different qualities of a model. Signed-off-by: Florian Hotze <dev@florianhotze.com>
This commit is contained in:
+34
-31
@@ -21,6 +21,7 @@ import java.io.ByteArrayInputStream;
|
||||
import java.io.ByteArrayOutputStream;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.io.Serial;
|
||||
import java.net.URI;
|
||||
import java.nio.ByteBuffer;
|
||||
import java.nio.ByteOrder;
|
||||
@@ -69,7 +70,6 @@ import org.osgi.service.component.annotations.Reference;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import com.fasterxml.jackson.databind.JsonNode;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
|
||||
import io.github.givimad.piperjni.PiperJNI;
|
||||
@@ -117,7 +117,7 @@ public class PiperTTSService extends AbstractCachedTTSService {
|
||||
activateTask = executor.submit(() -> {
|
||||
try {
|
||||
setupNativeDependencies();
|
||||
piper = new PiperJNI();
|
||||
PiperJNI piper = this.piper = new PiperJNI();
|
||||
piper.initialize(true, false);
|
||||
logger.debug("Using Piper version {}", piper.getPiperVersion());
|
||||
ready = true;
|
||||
@@ -130,6 +130,7 @@ public class PiperTTSService extends AbstractCachedTTSService {
|
||||
|
||||
@Deactivate
|
||||
private void deactivate() {
|
||||
Future<?> activateTask = this.activateTask;
|
||||
if (activateTask != null && !activateTask.isDone()) {
|
||||
activateTask.cancel(true);
|
||||
}
|
||||
@@ -284,41 +285,37 @@ public class PiperTTSService extends AbstractCachedTTSService {
|
||||
return cachedVoices;
|
||||
}
|
||||
String voiceData = Files.readString(configFile);
|
||||
JsonNode voiceJsonRoot = new ObjectMapper().readTree(voiceData);
|
||||
JsonNode datasetJsonNode = voiceJsonRoot.get("dataset");
|
||||
JsonNode languageJsonNode = voiceJsonRoot.get("language");
|
||||
JsonNode numSpeakersJsonNode = voiceJsonRoot.get("num_speakers");
|
||||
if (datasetJsonNode == null || languageJsonNode == null) {
|
||||
throw new IOException("Unknown voice config structure");
|
||||
}
|
||||
JsonNode languageFamilyJsonNode = languageJsonNode.get("family");
|
||||
JsonNode languageRegionJsonNode = languageJsonNode.get("region");
|
||||
if (languageFamilyJsonNode == null || languageRegionJsonNode == null) {
|
||||
throw new IOException("Unknown voice config structure");
|
||||
}
|
||||
String voiceName = datasetJsonNode.textValue();
|
||||
PiperVoiceConfig voiceConfig = new ObjectMapper().readValue(voiceData, PiperVoiceConfig.class);
|
||||
|
||||
String voiceName = voiceConfig.dataset;
|
||||
String voiceUID = voiceName.replace(" ", "_");
|
||||
String languageFamily = languageFamilyJsonNode.textValue();
|
||||
String languageRegion = languageRegionJsonNode.textValue();
|
||||
int numSpeakers = numSpeakersJsonNode != null ? numSpeakersJsonNode.intValue() : 1;
|
||||
JsonNode speakersIdsJsonNode = voiceJsonRoot.get("speaker_id_map");
|
||||
if (numSpeakers != 1 && speakersIdsJsonNode != null) {
|
||||
String quality = voiceConfig.audio.quality;
|
||||
String languageFamily = voiceConfig.language.family;
|
||||
String languageRegion = voiceConfig.language.region;
|
||||
int numSpeakers = voiceConfig.numSpeakers;
|
||||
Map<String, Integer> speakerIdMap = voiceConfig.speakerIdMap;
|
||||
|
||||
if (voiceName.isBlank() || quality.isBlank() || languageFamily.isBlank() || languageRegion.isBlank()) {
|
||||
throw new IOException("Unknown voice config structure");
|
||||
}
|
||||
|
||||
if (numSpeakers != 1 && speakerIdMap != null) {
|
||||
List<Voice> voices = new ArrayList<>();
|
||||
speakersIdsJsonNode.fieldNames().forEachRemaining(field -> {
|
||||
JsonNode fieldNode = speakersIdsJsonNode.get(field);
|
||||
speakerIdMap.forEach((field, id) -> {
|
||||
voices.add(new PiperTTSVoice( //
|
||||
voiceUID + "_" + field, //
|
||||
capitalize(voiceName + " " + field), //
|
||||
quality, //
|
||||
languageFamily, //
|
||||
languageRegion, //
|
||||
modelPath, //
|
||||
configFile, //
|
||||
Optional.of(fieldNode.longValue())));
|
||||
Optional.of(id.longValue())));
|
||||
});
|
||||
return voices;
|
||||
}
|
||||
return List.of(new PiperTTSVoice(voiceUID, capitalize(voiceName), languageFamily, languageRegion, modelPath,
|
||||
configFile, Optional.empty()));
|
||||
return List.of(new PiperTTSVoice(voiceUID, capitalize(voiceName), quality, languageFamily, languageRegion,
|
||||
modelPath, configFile, Optional.empty()));
|
||||
} catch (IOException e) {
|
||||
logger.warn("IOException reading voice info: {}", e.getMessage());
|
||||
return List.of();
|
||||
@@ -342,6 +339,7 @@ public class PiperTTSService extends AbstractCachedTTSService {
|
||||
VoiceModel voiceModel = null;
|
||||
boolean usingPreloadedModel = false;
|
||||
short[] buffer;
|
||||
int sampleRate;
|
||||
final VoiceModel preloadedModel = this.preloadedModel;
|
||||
try {
|
||||
try {
|
||||
@@ -364,6 +362,7 @@ public class PiperTTSService extends AbstractCachedTTSService {
|
||||
try {
|
||||
logger.debug("Generating audio for: '{}'", text);
|
||||
buffer = getPiper().textToAudio(voiceModel.piperVoice, text);
|
||||
sampleRate = voiceModel.sampleRate;
|
||||
logger.debug("Generated {} samples of audio", buffer.length);
|
||||
} catch (IOException e) {
|
||||
throw new TTSException("Voice generation failed: " + e.getMessage());
|
||||
@@ -386,7 +385,7 @@ public class PiperTTSService extends AbstractCachedTTSService {
|
||||
}
|
||||
try {
|
||||
logger.debug("Return re-encoded audio stream");
|
||||
return getAudioStream(buffer, voiceModel.sampleRate, audioFormat);
|
||||
return getAudioStream(buffer, sampleRate, audioFormat);
|
||||
} catch (IOException e) {
|
||||
throw new TTSException("Error while creating audio stream: " + e.getMessage());
|
||||
}
|
||||
@@ -477,26 +476,30 @@ public class PiperTTSService extends AbstractCachedTTSService {
|
||||
return new ByteArrayAudioStream(outputStream.toByteArray(), audioFormat);
|
||||
}
|
||||
|
||||
private record PiperTTSVoice(String voiceId, String voiceName, String languageFamily, String languageRegion,
|
||||
Path voiceModelPath, Path voiceModelConfigPath, Optional<Long> speakerId) implements Voice {
|
||||
private record PiperTTSVoice(String voiceId, String voiceName, String quality, String languageFamily,
|
||||
String languageRegion, Path voiceModelPath, Path voiceModelConfigPath,
|
||||
Optional<Long> speakerId) implements Voice {
|
||||
@Override
|
||||
public String getUID() {
|
||||
// Voice uid should be prefixed by service id to be listed properly on the UI.
|
||||
return SERVICE_ID + ":" + voiceId + "-" + languageFamily + "_" + languageRegion;
|
||||
return SERVICE_ID + ":" + voiceId + "-" + quality + "-" + languageFamily + "_" + languageRegion;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getLabel() {
|
||||
return voiceName;
|
||||
return voiceName + " (" + quality + ")";
|
||||
}
|
||||
|
||||
@Override
|
||||
public Locale getLocale() {
|
||||
return new Locale(languageFamily, languageRegion);
|
||||
return Locale.of(languageFamily, languageRegion);
|
||||
}
|
||||
}
|
||||
|
||||
private static class LibraryNotLoaded extends Exception {
|
||||
@Serial
|
||||
private static final long serialVersionUID = 538222631776595180L;
|
||||
|
||||
private LibraryNotLoaded() {
|
||||
super("Library not loaded");
|
||||
}
|
||||
|
||||
+51
@@ -0,0 +1,51 @@
|
||||
/*
|
||||
* Copyright (c) 2010-2026 Contributors to the openHAB project
|
||||
*
|
||||
* See the NOTICE file(s) distributed with this work for additional
|
||||
* information.
|
||||
*
|
||||
* This program and the accompanying materials are made available under the
|
||||
* terms of the Eclipse Public License 2.0 which is available at
|
||||
* http://www.eclipse.org/legal/epl-2.0
|
||||
*
|
||||
* SPDX-License-Identifier: EPL-2.0
|
||||
*/
|
||||
package org.openhab.voice.pipertts.internal;
|
||||
|
||||
import java.util.Map;
|
||||
|
||||
import org.eclipse.jdt.annotation.NonNullByDefault;
|
||||
import org.eclipse.jdt.annotation.Nullable;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
|
||||
import com.fasterxml.jackson.annotation.JsonProperty;
|
||||
|
||||
/**
|
||||
* The {@link PiperVoiceConfig} class is a Data Transfer Object for Piper voice configuration from .onnx.json.
|
||||
*
|
||||
* @author Florian Hotze - Initial contribution
|
||||
*/
|
||||
@NonNullByDefault
|
||||
@JsonIgnoreProperties(ignoreUnknown = true)
|
||||
class PiperVoiceConfig {
|
||||
public String dataset = "";
|
||||
public Audio audio = new Audio();
|
||||
public Language language = new Language();
|
||||
|
||||
@JsonProperty("num_speakers")
|
||||
public int numSpeakers = 1;
|
||||
|
||||
@JsonProperty("speaker_id_map")
|
||||
public @Nullable Map<String, Integer> speakerIdMap;
|
||||
|
||||
@JsonIgnoreProperties(ignoreUnknown = true)
|
||||
public static class Audio {
|
||||
public String quality = "";
|
||||
}
|
||||
|
||||
@JsonIgnoreProperties(ignoreUnknown = true)
|
||||
public static class Language {
|
||||
public String family = "";
|
||||
public String region = "";
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user