From c6ba9c3e29446eee074f113a9dc1efc26dbdaf7e Mon Sep 17 00:00:00 2001 From: bobra200 Date: Wed, 12 Aug 2026 15:55:18 -0700 Subject: [PATCH 1/3] RDKEMW-21812: texttospeech.speak fb9 changes --- include/firebolt/texttospeech.h | 13 +++- src/texttospeech_impl.cpp | 30 +++++++- src/texttospeech_impl.h | 6 +- test/unit/textToSpeechTest.cpp | 131 ++++++++++++++++++++++++++++++++ 4 files changed, 176 insertions(+), 4 deletions(-) diff --git a/include/firebolt/texttospeech.h b/include/firebolt/texttospeech.h index eaf41e2..ddb8b57 100644 --- a/include/firebolt/texttospeech.h +++ b/include/firebolt/texttospeech.h @@ -97,11 +97,20 @@ class ITextToSpeech * @brief Speak the uttered text using the TTS engine * * @param[in] text : String to be converted to Audio for speech + * @param[in] callSign : Optional call sign for the app making the request + * @param[in] language : Optional language for the speech request + * @param[in] voice : Optional voice for the speech request + * @param[in] volume : Optional volume for the speech request + * @param[in] rate : Optional rate for the speech request + * @param[in] pitch : Optional pitch for the speech request * * @retval Result for Speak */ - [[nodiscard]] virtual Result speak(const std::string& text) const = 0; - + [[nodiscard]] virtual Result + speak(const std::string& text, std::optional callSign = std::nullopt, + std::optional language = std::nullopt, std::optional voice = std::nullopt, + std::optional volume = std::nullopt, std::optional rate = std::nullopt, + std::optional pitch = std::nullopt) const = 0; /** * @brief Pauses the speech for given speech id * diff --git a/src/texttospeech_impl.cpp b/src/texttospeech_impl.cpp index d02d17d..82cfc7b 100644 --- a/src/texttospeech_impl.cpp +++ b/src/texttospeech_impl.cpp @@ -34,10 +34,38 @@ Result TextToSpeechImpl::listVoices(const std::string& langu return helper_.get("TextToSpeech.listvoices", params); } -Result TextToSpeechImpl::speak(const std::string& text) const +Result TextToSpeechImpl::speak(const std::string& text, std::optional callSign, + std::optional language, std::optional voice, + std::optional volume, std::optional rate, + std::optional pitch) const { nlohmann::json params; params["text"] = text; + if (callSign) + { + params["callSign"] = *callSign; + } // or "callsign" if API expects lower-case + if (language) + { + params["language"] = *language; + } + if (voice) + { + params["voice"] = *voice; + } + if (volume) + { + params["volume"] = *volume; + } + if (rate) + { + params["rate"] = *rate; + } + if (pitch) + { + params["pitch"] = *pitch; + } + return helper_.get("TextToSpeech.speak", params); } diff --git a/src/texttospeech_impl.h b/src/texttospeech_impl.h index 6dc646d..7d8f3d8 100644 --- a/src/texttospeech_impl.h +++ b/src/texttospeech_impl.h @@ -33,7 +33,11 @@ class TextToSpeechImpl : public ITextToSpeech ~TextToSpeechImpl() override = default; [[nodiscard]] Result listVoices(const std::string& language) const override; - [[nodiscard]] Result speak(const std::string& text) const override; + [[nodiscard]] Result + speak(const std::string& text, std::optional callSign = std::nullopt, + std::optional language = std::nullopt, std::optional voice = std::nullopt, + std::optional volume = std::nullopt, std::optional rate = std::nullopt, + std::optional pitch = std::nullopt) const override; [[nodiscard]] Result pause(SpeechId speechId) const override; [[nodiscard]] Result resume(SpeechId speechId) const override; [[nodiscard]] Result cancel(SpeechId speechId) const override; diff --git a/test/unit/textToSpeechTest.cpp b/test/unit/textToSpeechTest.cpp index 18c0633..1802636 100644 --- a/test/unit/textToSpeechTest.cpp +++ b/test/unit/textToSpeechTest.cpp @@ -62,6 +62,137 @@ TEST_F(TextToSpeechUTest, speak) EXPECT_EQ(speak->success, expectedValue["success"].get()); } +TEST_F(TextToSpeechUTest, speak_payloadDefaultsToTextOnly) +{ + EXPECT_CALL(mockHelper, getJson("TextToSpeech.speak", _)) + .WillOnce(Invoke( + [&](const std::string& /*methodName*/, const nlohmann::json& parameters) + { + nlohmann::json expected = {{"text", "I am a text waiting for speech."}}; + EXPECT_EQ(parameters, expected) << "Parameters do not match expected payload: " << expected.dump() + << " but got: " << parameters.dump(); + return Firebolt::Result{jsonEngine.get_value("TextToSpeech.speak")}; + })); + + auto result = ttsImpl.speak("I am a text waiting for speech."); + ASSERT_TRUE(result); +} + +TEST_F(TextToSpeechUTest, speak_payloadIncludesAllOptionalFieldsWhenProvided) +{ + EXPECT_CALL(mockHelper, getJson("TextToSpeech.speak", _)) + .WillOnce(Invoke( + [&](const std::string& /*methodName*/, const nlohmann::json& parameters) + { + nlohmann::json expected; + expected["text"] = "I am a text waiting for speech."; + expected["callSign"] = "AppA"; + expected["language"] = "en-US"; + expected["voice"] = "female-1"; + expected["volume"] = "80"; + expected["rate"] = "normal"; + expected["pitch"] = "medium"; + EXPECT_EQ(parameters, expected) << "Parameters do not match expected payload: " << expected.dump() + << " but got: " << parameters.dump(); + return Firebolt::Result{jsonEngine.get_value("TextToSpeech.speak")}; + })); + + auto result = ttsImpl.speak("I am a text waiting for speech.", std::string("AppA"), std::string("en-US"), + std::string("female-1"), std::string("80"), std::string("normal"), std::string("medium")); + ASSERT_TRUE(result); +} + +TEST_F(TextToSpeechUTest, speak_payloadIncludesOnlyProvidedOptionalFields) +{ + EXPECT_CALL(mockHelper, getJson("TextToSpeech.speak", _)) + .WillOnce(Invoke( + [&](const std::string& /*methodName*/, const nlohmann::json& parameters) + { + nlohmann::json expected; + expected["text"] = "I am a text waiting for speech."; + expected["language"] = "en-US"; + expected["rate"] = "normal"; + EXPECT_EQ(parameters, expected) << "Parameters do not match expected payload: " << expected.dump() + << " but got: " << parameters.dump(); + return Firebolt::Result{jsonEngine.get_value("TextToSpeech.speak")}; + })); + + auto result = ttsImpl.speak("I am a text waiting for speech.", std::nullopt, std::string("en-US"), std::nullopt, + std::nullopt, std::string("normal"), std::nullopt); + ASSERT_TRUE(result); +} + +TEST_F(TextToSpeechUTest, speak_payloadOmitsUnsetOptionalKeys) +{ + EXPECT_CALL(mockHelper, getJson("TextToSpeech.speak", _)) + .WillOnce(Invoke( + [&](const std::string& /*methodName*/, const nlohmann::json& parameters) + { + EXPECT_EQ(parameters.size(), 1U); + EXPECT_TRUE(parameters.contains("text")); + EXPECT_FALSE(parameters.contains("callSign")); + EXPECT_FALSE(parameters.contains("language")); + EXPECT_FALSE(parameters.contains("voice")); + EXPECT_FALSE(parameters.contains("volume")); + EXPECT_FALSE(parameters.contains("rate")); + EXPECT_FALSE(parameters.contains("pitch")); + return Firebolt::Result{jsonEngine.get_value("TextToSpeech.speak")}; + })); + + auto result = ttsImpl.speak("I am a text waiting for speech."); + ASSERT_TRUE(result); +} + +TEST_F(TextToSpeechUTest, speak_payloadPreservesEmptyStringOptionalValues) +{ + EXPECT_CALL(mockHelper, getJson("TextToSpeech.speak", _)) + .WillOnce(Invoke( + [&](const std::string& /*methodName*/, const nlohmann::json& parameters) + { + EXPECT_TRUE(parameters.contains("text")); + EXPECT_TRUE(parameters.contains("callSign")); + EXPECT_TRUE(parameters.contains("rate")); + EXPECT_EQ(parameters["callSign"], ""); + EXPECT_EQ(parameters["rate"], ""); + EXPECT_FALSE(parameters.contains("language")); + EXPECT_FALSE(parameters.contains("voice")); + EXPECT_FALSE(parameters.contains("volume")); + EXPECT_FALSE(parameters.contains("pitch")); + return Firebolt::Result{jsonEngine.get_value("TextToSpeech.speak")}; + })); + + auto result = ttsImpl.speak("I am a text waiting for speech.", std::string(""), std::nullopt, std::nullopt, + std::nullopt, std::string(""), std::nullopt); + ASSERT_TRUE(result); +} + +TEST_F(TextToSpeechUTest, speak_payloadUsesCallSignKeyNotLegacyCallsign) +{ + EXPECT_CALL(mockHelper, getJson("TextToSpeech.speak", _)) + .WillOnce(Invoke( + [&](const std::string& /*methodName*/, const nlohmann::json& parameters) + { + EXPECT_TRUE(parameters.contains("callSign")); + EXPECT_FALSE(parameters.contains("callsign")); + EXPECT_EQ(parameters["callSign"], "AppA"); + return Firebolt::Result{jsonEngine.get_value("TextToSpeech.speak")}; + })); + + auto result = ttsImpl.speak("I am a text waiting for speech.", std::string("AppA")); + ASSERT_TRUE(result); +} + +TEST_F(TextToSpeechUTest, speak_propagatesHelperError) +{ + EXPECT_CALL(mockHelper, getJson("TextToSpeech.speak", _)) + .WillOnce(Invoke([&](const std::string& /*methodName*/, const nlohmann::json& /*parameters*/) + { return Firebolt::Result{Firebolt::Error::General}; })); + + auto result = ttsImpl.speak("I am a text waiting for speech.", std::string("AppA")); + ASSERT_FALSE(result); + EXPECT_EQ(result.error(), Firebolt::Error::General); +} + TEST_F(TextToSpeechUTest, pause) { mock("TextToSpeech.pause"); From 47b8b4406f33fd883ee68a75dd46cd410de7a349 Mon Sep 17 00:00:00 2001 From: bobra200 Date: Wed, 12 Aug 2026 15:57:42 -0700 Subject: [PATCH 2/3] RDKEMW-21812: texttospeech.speak fb9 schema updates --- docs/openrpc/openrpc/text_to_speech.json | 66 +++++++++++++++++++ .../the-spec/firebolt-open-rpc--legacy.json | 66 +++++++++++++++++++ docs/openrpc/the-spec/firebolt-open-rpc.json | 66 +++++++++++++++++++ 3 files changed, 198 insertions(+) diff --git a/docs/openrpc/openrpc/text_to_speech.json b/docs/openrpc/openrpc/text_to_speech.json index 57575b4..3b499f1 100644 --- a/docs/openrpc/openrpc/text_to_speech.json +++ b/docs/openrpc/openrpc/text_to_speech.json @@ -18,6 +18,48 @@ "type": "string" }, "required": true + }, + { + "name": "callSign", + "summary": "Optional call sign for the app making the request", + "schema": { + "type": "string" + } + }, + { + "name": "language", + "summary": "Optional language override as a BCP 47 locale tag", + "schema": { + "type": "string" + } + }, + { + "name": "voice", + "summary": "Optional voice identifier", + "schema": { + "type": "string" + } + }, + { + "name": "volume", + "summary": "Optional volume override", + "schema": { + "type": "string" + } + }, + { + "name": "rate", + "summary": "Optional speech rate override", + "schema": { + "type": "string" + } + }, + { + "name": "pitch", + "summary": "Optional pitch override", + "schema": { + "type": "string" + } } ], "tags": [ @@ -45,6 +87,30 @@ { "name": "text", "value": "I am a text waiting for speech." + }, + { + "name": "callSign", + "value": "AppA" + }, + { + "name": "language", + "value": "en-US" + }, + { + "name": "voice", + "value": "female-1" + }, + { + "name": "volume", + "value": "80" + }, + { + "name": "rate", + "value": "normal" + }, + { + "name": "pitch", + "value": "medium" } ], "result": { diff --git a/docs/openrpc/the-spec/firebolt-open-rpc--legacy.json b/docs/openrpc/the-spec/firebolt-open-rpc--legacy.json index 1bec372..a7cf042 100644 --- a/docs/openrpc/the-spec/firebolt-open-rpc--legacy.json +++ b/docs/openrpc/the-spec/firebolt-open-rpc--legacy.json @@ -2843,6 +2843,48 @@ "type": "string" }, "required": true + }, + { + "name": "callSign", + "summary": "Optional call sign for the app making the request", + "schema": { + "type": "string" + } + }, + { + "name": "language", + "summary": "Optional language override as a BCP 47 locale tag", + "schema": { + "type": "string" + } + }, + { + "name": "voice", + "summary": "Optional voice identifier", + "schema": { + "type": "string" + } + }, + { + "name": "volume", + "summary": "Optional volume override", + "schema": { + "type": "string" + } + }, + { + "name": "rate", + "summary": "Optional speech rate override", + "schema": { + "type": "string" + } + }, + { + "name": "pitch", + "summary": "Optional pitch override", + "schema": { + "type": "string" + } } ], "tags": [ @@ -2870,6 +2912,30 @@ { "name": "text", "value": "I am a text waiting for speech." + }, + { + "name": "callSign", + "value": "AppA" + }, + { + "name": "language", + "value": "en-US" + }, + { + "name": "voice", + "value": "female-1" + }, + { + "name": "volume", + "value": "80" + }, + { + "name": "rate", + "value": "normal" + }, + { + "name": "pitch", + "value": "medium" } ], "result": { diff --git a/docs/openrpc/the-spec/firebolt-open-rpc.json b/docs/openrpc/the-spec/firebolt-open-rpc.json index 743e3fe..0f76a96 100644 --- a/docs/openrpc/the-spec/firebolt-open-rpc.json +++ b/docs/openrpc/the-spec/firebolt-open-rpc.json @@ -2484,6 +2484,48 @@ "type": "string" }, "required": true + }, + { + "name": "callSign", + "summary": "Optional call sign for the app making the request", + "schema": { + "type": "string" + } + }, + { + "name": "language", + "summary": "Optional language override as a BCP 47 locale tag", + "schema": { + "type": "string" + } + }, + { + "name": "voice", + "summary": "Optional voice identifier", + "schema": { + "type": "string" + } + }, + { + "name": "volume", + "summary": "Optional volume override", + "schema": { + "type": "string" + } + }, + { + "name": "rate", + "summary": "Optional speech rate override", + "schema": { + "type": "string" + } + }, + { + "name": "pitch", + "summary": "Optional pitch override", + "schema": { + "type": "string" + } } ], "tags": [ @@ -2511,6 +2553,30 @@ { "name": "text", "value": "I am a text waiting for speech." + }, + { + "name": "callSign", + "value": "AppA" + }, + { + "name": "language", + "value": "en-US" + }, + { + "name": "voice", + "value": "female-1" + }, + { + "name": "volume", + "value": "80" + }, + { + "name": "rate", + "value": "normal" + }, + { + "name": "pitch", + "value": "medium" } ], "result": { From 42daba1ad4b0ba14ef928bc3477295720220c34f Mon Sep 17 00:00:00 2001 From: bobra200 Date: Wed, 12 Aug 2026 16:25:04 -0700 Subject: [PATCH 3/3] RDKEMW-21812: copilot, nitpick --- src/texttospeech_impl.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/texttospeech_impl.cpp b/src/texttospeech_impl.cpp index 82cfc7b..7463b5a 100644 --- a/src/texttospeech_impl.cpp +++ b/src/texttospeech_impl.cpp @@ -44,7 +44,7 @@ Result TextToSpeechImpl::speak(const std::string& text, std::opt if (callSign) { params["callSign"] = *callSign; - } // or "callsign" if API expects lower-case + } if (language) { params["language"] = *language;