Revision control

Copy as Markdown

// Copyright (C) 2026 The Qt Company Ltd.
// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only
// Qt-Security score:significant reason:default
#include "texttospeechproxy.h"
#include <QtCore/quuid.h>
#include <QtCore/private/qcore_ohos_p.h>
#include <QtCore/private/qnapi_p.h>
#include <QtCore/private/qohoslogger_p.h>
#include <functional>
QT_BEGIN_NAMESPACE
namespace CoreSpeechKit {
namespace {
std::optional<TtsCompletionType> tryParseTtsCompletionType(const QNapi::Object &response)
{
static constexpr int ohosSynthesisCompleteTypeValue = 0;
static constexpr int ohosSpeechCompleteTypeValue = 1;
auto optTypeValue = QNapi::getOptionalPropOrEmpty<QNapi::Number>(response, "type");
if (optTypeValue.IsEmpty())
return {};
switch (optTypeValue.Int32Value()) {
case ohosSynthesisCompleteTypeValue:
return TtsCompletionType::SynthesisComplete;
case ohosSpeechCompleteTypeValue:
return TtsCompletionType::SpeechComplete;
default:
return {};
}
}
std::optional<std::string> tryGetVoiceStatus(const QNapi::Object &jsVoice)
{
auto optStatusValue = QNapi::getOptionalPropOrEmpty<QNapi::String>(jsVoice, "status");
if (optStatusValue.IsEmpty())
return {};
return optStatusValue.Utf8Value();
}
// Source of values used for EngineMode can be found under section 'online':
enum class EngineMode
{
Online = 0,
Offline = 1,
};
// Source of values used for SpeechPlayType can be found under section 'extraParams.playType':
enum class SpeechPlayType
{
SynthesisOnly = 0,
SynthesisAndPlayback = 1,
};
// Source of values used for SpeechQueueMode can be found under section 'extraParams.queueMode':
enum class SpeechQueueMode
{
Queue = 0,
Preemption = 1,
};
constexpr auto engineMode = EngineMode::Offline;
constexpr auto speechPlayType = SpeechPlayType::SynthesisAndPlayback;
constexpr auto speechQueueMode = SpeechQueueMode::Preemption;
class TextToSpeechProxyImpl : public TextToSpeechProxy
{
public:
static q23::expected<std::shared_ptr<TextToSpeechProxyImpl>, std::string> tryMakeInstance(
const std::string &language, int personTimbre);
void speak(const SpeakParams &params) override;
void stop() override;
void setEngineEventsListener(
std::shared_ptr<TextToSpeechProxy::EngineEventsListener> engineEventsListener) override;
std::string language() const override;
int personTimbre() const override;
private:
struct JsScopeData
{
std::shared_ptr<QNapi::Reference<QNapi::Object>> textToSpeechEngineRef;
};
explicit TextToSpeechProxyImpl(
const std::string &language, int personTimbre, std::shared_ptr<JsScopeData> jsScopeData);
static q23::expected<std::shared_ptr<JsScopeData>, std::string> tryMakeJsScopeData(
const std::string &language, int personTimbre);
std::shared_ptr<JsScopeData> m_jsScopeData;
std::shared_ptr<TextToSpeechProxy::EngineEventsListener> m_engineEventsListener;
const std::string m_language;
const int m_personTimbre;
};
class QtThreadBasedEngineEventsListener : public TextToSpeechProxy::EngineEventsListener
{
public:
QtThreadBasedEngineEventsListener(
std::shared_ptr<TextToSpeechProxy::EngineEventsListener> baseEngineEventsListener);
virtual void onStart(std::string utteranceId) override;
virtual void onComplete(std::string utteranceId, std::optional<TtsCompletionType> optCompletionType) override;
virtual void onStop(std::string utteranceId) override;
virtual void onError(
std::string utteranceId, std::uint32_t errorCode, std::string errorMessage) override;
private:
std::shared_ptr<TextToSpeechProxy::EngineEventsListener> m_baseEngineEventsListener;
};
QtThreadBasedEngineEventsListener::QtThreadBasedEngineEventsListener(
std::shared_ptr<TextToSpeechProxy::EngineEventsListener> baseEngineEventsListener)
: m_baseEngineEventsListener(baseEngineEventsListener)
{
}
void QtThreadBasedEngineEventsListener::onStart(std::string utteranceId)
{
QtOhos::invokeInQtThread(
[weakBaseEngineEventsListener = QtOhos::makeWeakPtr(m_baseEngineEventsListener), utteranceId]() {
auto baseEngineEventsListener = weakBaseEngineEventsListener.lock();
if (baseEngineEventsListener)
baseEngineEventsListener->onStart(utteranceId);
});
}
void QtThreadBasedEngineEventsListener::onComplete(
std::string utteranceId, std::optional<TtsCompletionType> optCompletionType)
{
QtOhos::invokeInQtThread(
[weakBaseEngineEventsListener = QtOhos::makeWeakPtr(m_baseEngineEventsListener), utteranceId, optCompletionType]() {
auto baseEngineEventsListener = weakBaseEngineEventsListener.lock();
if (baseEngineEventsListener)
baseEngineEventsListener->onComplete(utteranceId, optCompletionType);
});
}
void QtThreadBasedEngineEventsListener::onStop(std::string utteranceId)
{
QtOhos::invokeInQtThread(
[weakBaseEngineEventsListener = QtOhos::makeWeakPtr(m_baseEngineEventsListener), utteranceId]() {
auto baseEngineEventsListener = weakBaseEngineEventsListener.lock();
if (baseEngineEventsListener)
baseEngineEventsListener->onStop(utteranceId);
});
}
void QtThreadBasedEngineEventsListener::onError(
std::string utteranceId, std::uint32_t errorCode, std::string errorMessage)
{
QtOhos::invokeInQtThread(
[weakBaseEngineEventsListener = QtOhos::makeWeakPtr(m_baseEngineEventsListener), utteranceId, errorCode, errorMessage]() {
auto baseEngineEventsListener = weakBaseEngineEventsListener.lock();
if (baseEngineEventsListener)
baseEngineEventsListener->onError(utteranceId, errorCode, errorMessage);
});
}
std::shared_ptr<TextToSpeechProxy::EngineEventsListener> makeEngineEventsListenerQtDecorator(
std::shared_ptr<TextToSpeechProxy::EngineEventsListener> engineEventsListener)
{
return std::make_shared<QtThreadBasedEngineEventsListener>(engineEventsListener);
}
std::string createUuid()
{
return QUuid::createUuid().toString(QUuid::WithoutBraces).toStdString();
}
std::function<void(const QOhosCallbackInfo &)> makeOnStateCallbackDelegate(
std::shared_ptr<TextToSpeechProxy::EngineEventsListener> engineEventsListener,
void (TextToSpeechProxy::EngineEventsListener::*memberPtr)(std::string))
{
return [weakEngineEventsListener = QtOhos::makeWeakPtr(engineEventsListener), memberPtr](const QNapi::CallbackInfo &cbInfo) {
auto utteranceId = cbInfo.getFirstArg<QNapi::String>(Q_FUNC_INFO);
auto engineEventsListener = weakEngineEventsListener.lock();
if (engineEventsListener)
(engineEventsListener.get()->*memberPtr)(utteranceId);
};
}
std::function<void(const QOhosCallbackInfo &)> makeOnCompleteCallbackDelegate(
std::shared_ptr<TextToSpeechProxy::EngineEventsListener> engineEventsListener)
{
return [weakEngineEventsListener = QtOhos::makeWeakPtr(engineEventsListener)](const QNapi::CallbackInfo &cbInfo) {
QNapi::String utteranceId;
QNapi::Object response;
cbInfo.getLeadingArgs(Q_FUNC_INFO, utteranceId, response);
auto optCompletionType = tryParseTtsCompletionType(response);
if (!optCompletionType) {
qOhosPrintfWarning(
"%s: unexpected completion type in response: %s, utteranceId: %s",
Q_FUNC_INFO, QNapi::toJsonString(response).c_str(), utteranceId.Utf8Value().c_str());
}
auto engineEventsListener = weakEngineEventsListener.lock();
if (engineEventsListener)
engineEventsListener->onComplete(utteranceId, optCompletionType);
};
}
void setEngineListenerInJsThread(
QOhosJsState &jsState, QNapi::Object engine,
std::shared_ptr<TextToSpeechProxy::EngineEventsListener> engineEventsListener)
{
engine.eval(
"setListener(*)",
{
QNapi::makeObject(
jsState.env(),
{
{
"onStart",
makeOnStateCallbackDelegate(
engineEventsListener,
&TextToSpeechProxy::EngineEventsListener::onStart)
},
{
"onComplete",
makeOnCompleteCallbackDelegate(engineEventsListener)
},
{
"onStop",
makeOnStateCallbackDelegate(
engineEventsListener,
&TextToSpeechProxy::EngineEventsListener::onStop)
},
{
"onError",
[weakEngineEventsListener = QtOhos::makeWeakPtr(engineEventsListener)](const QNapi::CallbackInfo &cbInfo) {
QNapi::String utteranceId;
QNapi::Number errorCode;
QNapi::String errorMessage;
cbInfo.getLeadingArgs(Q_FUNC_INFO, utteranceId, errorCode, errorMessage);
auto engineEventsListener = weakEngineEventsListener.lock();
if (engineEventsListener) {
engineEventsListener->onError(
utteranceId, errorCode, errorMessage);
}
}
},
})
});
}
void tryCreateEngineImpl(
QOhosJsState &jsState, const std::string &language, int personTimbre,
QOhosTaskPromise<q23::expected<std::shared_ptr<QNapi::Reference<QNapi::Object>>, std::string>> taskPromise)
{
auto thenCatchPromises = std::move(taskPromise).makeThenCatchBranches(Q_FUNC_INFO);
jsState.evalToPromiseOrRejectOnThrow(
"@kit.CoreSpeechKit.textToSpeech.createEngine(*)",
{
QNapi::makeObject(
jsState.env(),
{
{"language", language},
{"person", personTimbre},
{"online", static_cast<int>(engineMode)},
}),
})
.onThen(
[thenPromise = std::move(thenCatchPromises.first)](const QOhosCallbackInfo &cbInfo) {
auto engine = cbInfo.getFirstArg<QNapi::Object>(Q_FUNC_INFO);
auto engineRef = QtOhos::moveToSharedPtr(QNapi::Reference<>::makePersistentFrom(engine));
thenPromise(
QtOhos::makeSharedPtrWithAttachedExtraData(
engineRef,
QtOhos::makeDestroyNotifier(
[engineRef]() {
try {
engineRef->eval("shutdown()");
} catch (const Napi::Error &error) {
qOhosPrintfError(
"%s: shutdown() failed with error: %s", Q_FUNC_INFO, error.what());
}
})
));
})
.onCatch(
[catchPromise = std::move(thenCatchPromises.second)](const QOhosCallbackInfo &cbInfo) {
auto errorObj = cbInfo.getFirstArg<QNapi::Object>(Q_FUNC_INFO);
catchPromise(q23::unexpected(errorObj.get<QNapi::String>("message")));
});
}
q23::expected<std::shared_ptr<TextToSpeechProxyImpl>, std::string> TextToSpeechProxyImpl::tryMakeInstance(
const std::string &language, int personTimbre)
{
auto jsScopeDataOrError = tryMakeJsScopeData(language, personTimbre);
if (!jsScopeDataOrError)
return q23::unexpected(std::move(jsScopeDataOrError.error()));
return std::shared_ptr<TextToSpeechProxyImpl>(
new TextToSpeechProxyImpl(language, personTimbre, std::move(jsScopeDataOrError.value())));
}
void TextToSpeechProxyImpl::setEngineEventsListener(
std::shared_ptr<TextToSpeechProxy::EngineEventsListener> engineEventsListener)
{
m_engineEventsListener = makeEngineEventsListenerQtDecorator(engineEventsListener);
QOhosJsThreadGateway::runAndWait(
[&](QOhosJsState &jsState) {
setEngineListenerInJsThread(
jsState, m_jsScopeData->textToSpeechEngineRef->Value(),
m_engineEventsListener);
},
Q_FUNC_INFO);
}
q23::expected<std::shared_ptr<TextToSpeechProxyImpl::JsScopeData>, std::string> TextToSpeechProxyImpl::tryMakeJsScopeData(
const std::string &language, int personTimbre)
{
return QOhosJsThreadGateway::evalWithPromise<q23::expected<std::shared_ptr<TextToSpeechProxyImpl::JsScopeData>, std::string>>(
[&](QOhosJsState &jsState, auto evalPromise) {
auto sharedEvalPromise = QtOhos::moveToSharedPtr(
std::move(evalPromise).makeChained(Q_FUNC_INFO));
tryCreateEngineImpl(
jsState, language, personTimbre,
QOhosTaskPromise<q23::expected<std::shared_ptr<QNapi::Reference<QNapi::Object>>, std::string>>(
[sharedEvalPromise](q23::expected<std::shared_ptr<QNapi::Reference<QNapi::Object>>, std::string> engineRefOrError) {
if (!engineRefOrError) {
(*sharedEvalPromise)(q23::unexpected(std::move(engineRefOrError.error())));
return;
}
(*sharedEvalPromise)(
QtOhos::makeProxyWithJsThreadDeleter(
QtOhos::moveToSharedPtr(
TextToSpeechProxyImpl::JsScopeData {
.textToSpeechEngineRef = engineRefOrError.value(),
})));
},
{},
Q_FUNC_INFO));
},
Q_FUNC_INFO);
}
TextToSpeechProxyImpl::TextToSpeechProxyImpl(
const std::string &language, int personTimbre, std::shared_ptr<JsScopeData> jsScopeData)
: m_jsScopeData(std::move(jsScopeData))
, m_language(language)
, m_personTimbre(personTimbre)
{
}
std::string TextToSpeechProxyImpl::language() const
{
return m_language;
}
int TextToSpeechProxyImpl::personTimbre() const
{
return m_personTimbre;
}
void TextToSpeechProxyImpl::speak(const SpeakParams &params)
{
QOhosJsThreadGateway::runAndWait(
[&](QOhosJsState &jsState) {
m_jsScopeData->textToSpeechEngineRef->eval(
"speak(*)",
{
params.text,
QNapi::makeObject(
jsState.env(),
{
{"requestId", createUuid()},
{
"extraParams",
QNapi::makeObject(
jsState.env(),
{
{"queueMode", static_cast<int>(speechQueueMode)},
{"speed", params.speed},
{"volume", params.volume},
{"pitch", params.pitch},
{"languageContext", params.languageContext},
{"playType", static_cast<int>(speechPlayType)},
})
},
}),
});
},
Q_FUNC_INFO);
}
void TextToSpeechProxyImpl::stop()
{
QOhosJsThreadGateway::runAndWait(
[&](QOhosJsState &) {
m_jsScopeData->textToSpeechEngineRef->eval("stop()");
},
Q_FUNC_INFO);
}
}
TextToSpeechProxy::TextToSpeechProxy() = default;
TextToSpeechProxy::~TextToSpeechProxy() = default;
TextToSpeechProxy::EngineEventsListener::EngineEventsListener() = default;
TextToSpeechProxy::EngineEventsListener::~EngineEventsListener() = default;
std::optional<std::vector<VoiceInfo>> tryListVoices()
{
return QOhosJsThreadGateway::evalWithPromise<std::optional<std::vector<VoiceInfo>>>(
[](QOhosJsState &jsState, QOhosTaskPromise<std::optional<std::vector<VoiceInfo>>> evalPromise) {
auto thenCatchPromises = std::move(evalPromise).makeThenCatchBranches(Q_FUNC_INFO);
jsState.evalToPromiseOrRejectOnThrow(
"@kit.CoreSpeechKit.textToSpeech.listVoices(*)",
{
QNapi::makeObject(
jsState.env(),
{
{"requestId", createUuid()},
{"online", static_cast<int>(engineMode)},
})
})
.onThen(
[thenPromise = std::move(thenCatchPromises.first)](const QOhosCallbackInfo &cbInfo) {
auto voicesArray = cbInfo.getFirstArg<QNapi::Array>(Q_FUNC_INFO);
thenPromise(
QNapi::getArrayElements<std::vector<VoiceInfo>, QNapi::Object>(
voicesArray,
[](QNapi::Object jsVoice) {
return VoiceInfo{
.language = jsVoice.get<QNapi::String>("language"),
.style = jsVoice.get<QNapi::String>("style"),
.gender = jsVoice.get<QNapi::String>("gender"),
.description = jsVoice.get<QNapi::String>("description"),
.personTimbre = jsVoice.get<QNapi::Number>("person"),
.status = tryGetVoiceStatus(jsVoice),
};
}));
})
.onCatch(
[catchPromise = std::move(thenCatchPromises.second)](const QOhosCallbackInfo &cbInfo) {
QtOhos::logJsCallbackError(cbInfo, "listVoices() failed");
catchPromise(std::nullopt);
});
},
Q_FUNC_INFO);
}
q23::expected<std::shared_ptr<TextToSpeechProxy>, std::string> tryMakeTextToSpeechProxy(
const std::string &language, int personTimbre)
{
auto proxyImplInstanceOrError = TextToSpeechProxyImpl::tryMakeInstance(language, personTimbre);
if (!proxyImplInstanceOrError)
return q23::unexpected(std::move(proxyImplInstanceOrError.error()));
return std::move(proxyImplInstanceOrError.value());
}
}
QT_END_NAMESPACE