From 7c6e658f56b67c89a8af8c164c82bde1b726334c Mon Sep 17 00:00:00 2001 From: Artur-Fedjukevits <87441995+Artur-Fedjukevits@users.noreply.github.com> Date: Tue, 24 Dec 2024 18:19:25 +0100 Subject: [PATCH] [openaitts] OpenAI Text-to-Speech initial contribution (#17733) Also-by: Wouter Born Signed-off-by: Artur-Fedjukevits --- CODEOWNERS | 1 + bom/openhab-addons/pom.xml | 5 + bundles/org.openhab.voice.openaitts/NOTICE | 14 ++ bundles/org.openhab.voice.openaitts/README.md | 23 +++ bundles/org.openhab.voice.openaitts/pom.xml | 16 ++ .../src/main/feature/feature.xml | 9 ++ .../internal/OpenAITTSConfiguration.java | 27 ++++ .../internal/OpenAITTSConstants.java | 25 +++ .../openaitts/internal/OpenAITTSService.java | 148 ++++++++++++++++++ .../openaitts/internal/OpenAITTSVoice.java | 61 ++++++++ .../src/main/resources/OH-INF/addon/addon.xml | 15 ++ .../main/resources/OH-INF/config/config.xml | 46 ++++++ .../OH-INF/i18n/openaitts.properties | 17 ++ bundles/pom.xml | 1 + 14 files changed, 408 insertions(+) create mode 100644 bundles/org.openhab.voice.openaitts/NOTICE create mode 100644 bundles/org.openhab.voice.openaitts/README.md create mode 100644 bundles/org.openhab.voice.openaitts/pom.xml create mode 100644 bundles/org.openhab.voice.openaitts/src/main/feature/feature.xml create mode 100644 bundles/org.openhab.voice.openaitts/src/main/java/org/openhab/voice/openaitts/internal/OpenAITTSConfiguration.java create mode 100644 bundles/org.openhab.voice.openaitts/src/main/java/org/openhab/voice/openaitts/internal/OpenAITTSConstants.java create mode 100644 bundles/org.openhab.voice.openaitts/src/main/java/org/openhab/voice/openaitts/internal/OpenAITTSService.java create mode 100644 bundles/org.openhab.voice.openaitts/src/main/java/org/openhab/voice/openaitts/internal/OpenAITTSVoice.java create mode 100644 bundles/org.openhab.voice.openaitts/src/main/resources/OH-INF/addon/addon.xml create mode 100644 bundles/org.openhab.voice.openaitts/src/main/resources/OH-INF/config/config.xml create mode 100644 bundles/org.openhab.voice.openaitts/src/main/resources/OH-INF/i18n/openaitts.properties diff --git a/CODEOWNERS b/CODEOWNERS index 0526f553154..71e75f22779 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -465,6 +465,7 @@ /bundles/org.openhab.voice.mactts/ @kaikreuzer /bundles/org.openhab.voice.marytts/ @kaikreuzer /bundles/org.openhab.voice.mimictts/ @dalgwen +/bundles/org.openhab.voice.openaitts/ @Artur-Fedjukevits /bundles/org.openhab.voice.picotts/ @FlorianSW /bundles/org.openhab.voice.pipertts/ @GiviMAD /bundles/org.openhab.voice.pollytts/ @openhab/add-ons-maintainers diff --git a/bom/openhab-addons/pom.xml b/bom/openhab-addons/pom.xml index 170f8ce163d..ea798c3d2c6 100644 --- a/bom/openhab-addons/pom.xml +++ b/bom/openhab-addons/pom.xml @@ -2301,6 +2301,11 @@ org.openhab.voice.mimictts ${project.version} + + org.openhab.addons.bundles + org.openhab.voice.openaitts + ${project.version} + org.openhab.addons.bundles org.openhab.voice.picotts diff --git a/bundles/org.openhab.voice.openaitts/NOTICE b/bundles/org.openhab.voice.openaitts/NOTICE new file mode 100644 index 00000000000..4205fa32370 --- /dev/null +++ b/bundles/org.openhab.voice.openaitts/NOTICE @@ -0,0 +1,14 @@ +This content is produced and maintained by the openHAB project. + +* Project home: https://www.openhab.org + +== Declared Project Licenses + +This program and the accompanying materials are made available under the terms +of the Eclipse Public License 2.0 which is available at +https://www.eclipse.org/legal/epl-2.0/. + +== Source Code + +https://github.com/openhab/openhab-addons + diff --git a/bundles/org.openhab.voice.openaitts/README.md b/bundles/org.openhab.voice.openaitts/README.md new file mode 100644 index 00000000000..4793ad9b7da --- /dev/null +++ b/bundles/org.openhab.voice.openaitts/README.md @@ -0,0 +1,23 @@ +# OpenAI Text-to-Speech + +The OpenAI TTS (Text-to-Speech) add-on for openHAB allows you to integrate OpenAI's Text-to-Speech capabilities into your openHAB system. +The advantage of this service over others is that one selected voice can speak different languages. +This is useful, for example, in conjunction with ChatGPT binding, which will help in learning foreign languages. +You can find the price for this service here - https://openai.com/api/pricing/ + +## Configuration + +To configure the OpenAI TTS, **Settings / Other Services - OpenAI Text-to-Speech** and set: + +* **apiKey** - The API key to be used for the requests. +* **apiUrl** - The server API where to reach the AI TTS service. +* **model** - The ID of the model to use for TTS. + +### Default Text-to-Speech and Voice Configuration + +You can setup your preferred default Text-to-Speech and default voice in the UI: + +* Go to **Settings**. +* Edit **System Services - Voice**. +* Set **OpenAI TTS Service** as **Default Text-to-Speech**. +* Choose your preferred **Default Voice** for your setup. diff --git a/bundles/org.openhab.voice.openaitts/pom.xml b/bundles/org.openhab.voice.openaitts/pom.xml new file mode 100644 index 00000000000..46b4723b676 --- /dev/null +++ b/bundles/org.openhab.voice.openaitts/pom.xml @@ -0,0 +1,16 @@ + + + + 4.0.0 + + + org.openhab.addons.bundles + org.openhab.addons.reactor.bundles + 5.0.0-SNAPSHOT + + + org.openhab.voice.openaitts + + openHAB Add-ons :: Bundles :: Voice :: OpenAI Text-to-Speech + diff --git a/bundles/org.openhab.voice.openaitts/src/main/feature/feature.xml b/bundles/org.openhab.voice.openaitts/src/main/feature/feature.xml new file mode 100644 index 00000000000..917ab777c38 --- /dev/null +++ b/bundles/org.openhab.voice.openaitts/src/main/feature/feature.xml @@ -0,0 +1,9 @@ + + + mvn:org.openhab.core.features.karaf/org.openhab.core.features.karaf.openhab-core/${ohc.version}/xml/features + + + openhab-runtime-base + mvn:org.openhab.addons.bundles/org.openhab.voice.openaitts/${project.version} + + diff --git a/bundles/org.openhab.voice.openaitts/src/main/java/org/openhab/voice/openaitts/internal/OpenAITTSConfiguration.java b/bundles/org.openhab.voice.openaitts/src/main/java/org/openhab/voice/openaitts/internal/OpenAITTSConfiguration.java new file mode 100644 index 00000000000..d560a52d35c --- /dev/null +++ b/bundles/org.openhab.voice.openaitts/src/main/java/org/openhab/voice/openaitts/internal/OpenAITTSConfiguration.java @@ -0,0 +1,27 @@ +/** + * Copyright (c) 2010-2024 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.openaitts.internal; + +import org.eclipse.jdt.annotation.NonNullByDefault; + +/** + * @author Artur Fedjukevits - Initial contribution + */ +@NonNullByDefault +public class OpenAITTSConfiguration { + + public String apiKey = ""; + public String apiUrl = "https://api.openai.com/v1/audio/speech"; + public String model = "tts-1"; + public String speed = "1"; +} diff --git a/bundles/org.openhab.voice.openaitts/src/main/java/org/openhab/voice/openaitts/internal/OpenAITTSConstants.java b/bundles/org.openhab.voice.openaitts/src/main/java/org/openhab/voice/openaitts/internal/OpenAITTSConstants.java new file mode 100644 index 00000000000..32a9fc7310a --- /dev/null +++ b/bundles/org.openhab.voice.openaitts/src/main/java/org/openhab/voice/openaitts/internal/OpenAITTSConstants.java @@ -0,0 +1,25 @@ +/** + * Copyright (c) 2010-2024 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.openaitts.internal; + +import org.eclipse.jdt.annotation.NonNullByDefault; + +/** + * @author Artur Fedjukevits - Initial contribution + */ +@NonNullByDefault +public class OpenAITTSConstants { + + public static final String TTS_SERVICE_ID = "openaitts"; + public static final String TTS_SERVICE_PID = "org.openhab.voice.openaitts"; +} diff --git a/bundles/org.openhab.voice.openaitts/src/main/java/org/openhab/voice/openaitts/internal/OpenAITTSService.java b/bundles/org.openhab.voice.openaitts/src/main/java/org/openhab/voice/openaitts/internal/OpenAITTSService.java new file mode 100644 index 00000000000..3e6b0cae2c8 --- /dev/null +++ b/bundles/org.openhab.voice.openaitts/src/main/java/org/openhab/voice/openaitts/internal/OpenAITTSService.java @@ -0,0 +1,148 @@ +/** + * Copyright (c) 2010-2024 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.openaitts.internal; + +import static org.openhab.voice.openaitts.internal.OpenAITTSConstants.*; + +import java.util.Locale; +import java.util.Map; +import java.util.Set; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.TimeoutException; +import java.util.stream.Collectors; +import java.util.stream.Stream; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.eclipse.jdt.annotation.Nullable; +import org.eclipse.jetty.client.HttpClient; +import org.eclipse.jetty.client.api.ContentResponse; +import org.eclipse.jetty.client.util.StringContentProvider; +import org.eclipse.jetty.http.HttpMethod; +import org.eclipse.jetty.http.HttpStatus; +import org.openhab.core.audio.AudioFormat; +import org.openhab.core.audio.AudioStream; +import org.openhab.core.audio.ByteArrayAudioStream; +import org.openhab.core.config.core.ConfigurableService; +import org.openhab.core.config.core.Configuration; +import org.openhab.core.io.net.http.HttpClientFactory; +import org.openhab.core.voice.AbstractCachedTTSService; +import org.openhab.core.voice.TTSCache; +import org.openhab.core.voice.TTSException; +import org.openhab.core.voice.TTSService; +import org.openhab.core.voice.Voice; +import org.osgi.framework.Constants; +import org.osgi.service.component.annotations.Activate; +import org.osgi.service.component.annotations.Component; +import org.osgi.service.component.annotations.Modified; +import org.osgi.service.component.annotations.Reference; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import com.google.gson.Gson; +import com.google.gson.JsonObject; + +/** + * @author Artur Fedjukevits - Initial contribution + * API documentation: https://platform.openai.com/docs/guides/text-to-speech + */ +@Component(configurationPid = TTS_SERVICE_PID, property = Constants.SERVICE_PID + "=" + + TTS_SERVICE_PID, service = TTSService.class) +@ConfigurableService(category = "voice", label = "OpenAI TTS Service", description_uri = "voice:" + TTS_SERVICE_ID) + +@NonNullByDefault +public class OpenAITTSService extends AbstractCachedTTSService { + + private static final int REQUEST_TIMEOUT_MS = 10_000; + private final Logger logger = LoggerFactory.getLogger(OpenAITTSService.class); + private OpenAITTSConfiguration config = new OpenAITTSConfiguration(); + private final HttpClient httpClient; + private final Gson gson = new Gson(); + private static final Set VOICES = Stream.of("nova", "alloy", "echo", "fable", "onyx", "shimmer") + .map(OpenAITTSVoice::new).collect(Collectors.toSet()); + + @Activate + public OpenAITTSService(@Reference HttpClientFactory httpClientFactory, @Reference TTSCache ttsCache, + Map config) { + super(ttsCache); + this.httpClient = httpClientFactory.getCommonHttpClient(); + } + + @Activate + protected void activate(Map config) { + this.config = new Configuration(config).as(OpenAITTSConfiguration.class); + } + + @Modified + protected void modified(Map config) { + this.config = new Configuration(config).as(OpenAITTSConfiguration.class); + } + + @Override + public Set getSupportedFormats() { + return Set.of(new AudioFormat(AudioFormat.CONTAINER_NONE, AudioFormat.CODEC_MP3, null, 16, 64000, 44100L)); + } + + @Override + public String getId() { + return TTS_SERVICE_ID; + } + + @Override + public String getLabel(@Nullable Locale locale) { + return "OpenAI TTS Service"; + } + + @Override + public Set getAvailableVoices() { + return VOICES; + } + + /** + * Synthesizes the given text to audio data using the OpenAI API + * + * @param text The text to synthesize + * @param voice The voice to use + * @param requestedFormat The requested audio format + * @return The synthesized audio data + * @throws TTSException If the synthesis fails + */ + @Override + public AudioStream synthesizeForCache(String text, Voice voice, AudioFormat requestedFormat) throws TTSException { + JsonObject content = new JsonObject(); + content.addProperty("model", config.model); + content.addProperty("input", text); + content.addProperty("voice", voice.getLabel().toLowerCase()); + content.addProperty("speed", config.speed); + + String queryJson = gson.toJson(content); + + try { + ContentResponse response = httpClient.newRequest(config.apiUrl).method(HttpMethod.POST) + .timeout(REQUEST_TIMEOUT_MS, TimeUnit.MILLISECONDS) + .header("Authorization", "Bearer " + config.apiKey).header("Content-Type", "application/json") + .content(new StringContentProvider(queryJson)).send(); + + if (response.getStatus() == HttpStatus.OK_200) { + return new ByteArrayAudioStream(response.getContent(), requestedFormat); + } else { + logger.error("Request resulted in HTTP {} with message: {}", response.getStatus(), + response.getReason()); + throw new TTSException("Failed to generate audio data"); + } + } catch (InterruptedException | TimeoutException | ExecutionException e) { + logger.error("Request to OpenAI failed: {}", e.getMessage(), e); + throw new TTSException("Failed to generate audio data"); + } + } +} diff --git a/bundles/org.openhab.voice.openaitts/src/main/java/org/openhab/voice/openaitts/internal/OpenAITTSVoice.java b/bundles/org.openhab.voice.openaitts/src/main/java/org/openhab/voice/openaitts/internal/OpenAITTSVoice.java new file mode 100644 index 00000000000..f5f155b036a --- /dev/null +++ b/bundles/org.openhab.voice.openaitts/src/main/java/org/openhab/voice/openaitts/internal/OpenAITTSVoice.java @@ -0,0 +1,61 @@ +/** + * Copyright (c) 2010-2024 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.openaitts.internal; + +import java.util.Locale; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.openhab.core.voice.Voice; + +/** + * @author Artur Fedjukevits - Initial contribution + */ +@NonNullByDefault +public class OpenAITTSVoice implements Voice { + + private final String label; + + public OpenAITTSVoice(String label) { + this.label = label; + } + + /** + * The unique identifier of the voice, used for internal purposes + * + * @return The unique identifier of the voice + */ + @Override + public String getUID() { + return "openaitts:" + label; + } + + /** + * The voice label, used for GUI's or VUI's + * + * @return The voice label + */ + @Override + public String getLabel() { + return Character.toUpperCase(label.charAt(0)) + label.substring(1); + } + + /** + * The locale of the voice + * + * @return The locale of the voice + */ + @Override + public Locale getLocale() { + return Locale.ENGLISH; + } +} diff --git a/bundles/org.openhab.voice.openaitts/src/main/resources/OH-INF/addon/addon.xml b/bundles/org.openhab.voice.openaitts/src/main/resources/OH-INF/addon/addon.xml new file mode 100644 index 00000000000..a3934d428a2 --- /dev/null +++ b/bundles/org.openhab.voice.openaitts/src/main/resources/OH-INF/addon/addon.xml @@ -0,0 +1,15 @@ + + + + voice + OpenAI Text-to-Speech + OpenAI TTS Service provides text-to-speech capabilities for openHAB. + cloud + + org.openhab.voice.openaitts + + + + diff --git a/bundles/org.openhab.voice.openaitts/src/main/resources/OH-INF/config/config.xml b/bundles/org.openhab.voice.openaitts/src/main/resources/OH-INF/config/config.xml new file mode 100644 index 00000000000..581fd450e0e --- /dev/null +++ b/bundles/org.openhab.voice.openaitts/src/main/resources/OH-INF/config/config.xml @@ -0,0 +1,46 @@ + + + + + + Authentication for connecting to OpenAI API. + + + + Configure Text to Speech. + + + + true + OpenAI API key. + password + + + + true + TTS host API URL. + https://api.openai.com/v1/audio/speech + + + + true + ID of the model to use. + + + + + false + tts-1 + + + + The speed of the generated audio. Select a value from 0.25 to 4.0. + 1.0 + + + + diff --git a/bundles/org.openhab.voice.openaitts/src/main/resources/OH-INF/i18n/openaitts.properties b/bundles/org.openhab.voice.openaitts/src/main/resources/OH-INF/i18n/openaitts.properties new file mode 100644 index 00000000000..a66ca6fff71 --- /dev/null +++ b/bundles/org.openhab.voice.openaitts/src/main/resources/OH-INF/i18n/openaitts.properties @@ -0,0 +1,17 @@ +# add-on + +addon.openaitts.name = OpenAI Text-to-Speech +addon.openaitts.description = OpenAI TTS Service provides text-to-speech capabilities for openHAB. + +voice.config.openaitts.apiKey.label = API Key +voice.config.openaitts.apiKey.description = OpenAI API key. +voice.config.openaitts.apiUrl.label = API URL +voice.config.openaitts.apiUrl.description = TTS host API URL. +voice.config.openaitts.group.authentication.label = Authentication +voice.config.openaitts.group.authentication.description = Authentication for connecting to OpenAI API. +voice.config.openaitts.group.tts.label = TTS Configuration +voice.config.openaitts.group.tts.description = Configure Text to Speech. +voice.config.openaitts.model.label = Model +voice.config.openaitts.model.description = ID of the model to use. +voice.config.openaitts.speed.label = Speed +voice.config.openaitts.speed.description = The speed of the generated audio. Select a value from 0.25 to 4.0. diff --git a/bundles/pom.xml b/bundles/pom.xml index 68102f6c939..f3ae1218ef6 100644 --- a/bundles/pom.xml +++ b/bundles/pom.xml @@ -483,6 +483,7 @@ org.openhab.voice.mactts org.openhab.voice.marytts org.openhab.voice.mimictts + org.openhab.voice.openaitts org.openhab.voice.picotts org.openhab.voice.pipertts org.openhab.voice.pollytts