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>
QT_BEGIN_NAMESPACE
namespace CoreSpeechKit {
namespace {
// 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;
std::vector<VoiceInfo> listVoices() override;
private:
struct JsScopeData
{
std::shared_ptr<QNapi::Reference<QNapi::Object>> textToSpeechEngineRef;
};
explicit TextToSpeechProxyImpl(const std::string &language, 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;
const std::string m_language;
};
std::string createUuid()
{
return QUuid::createUuid().toString(QUuid::WithoutBraces).toStdString();
}
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, std::move(jsScopeDataOrError.value())));
}
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, std::shared_ptr<JsScopeData> jsScopeData)
: m_jsScopeData(std::move(jsScopeData))
, m_language(language)
{
}
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);
}
std::vector<VoiceInfo> TextToSpeechProxyImpl::listVoices()
{
return QOhosJsThreadGateway::evalWithPromise<std::vector<VoiceInfo>>(
[&](QOhosJsState &jsState, QOhosTaskPromise<std::vector<VoiceInfo>> evalPromise) {
auto thenCatchPromises = std::move(evalPromise).makeThenCatchBranches(Q_FUNC_INFO);
m_jsScopeData->textToSpeechEngineRef->evalToPromiseOrRejectOnThrow(
"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"),
};
}));
})
.onCatch(
[catchPromise = std::move(thenCatchPromises.second)](const QOhosCallbackInfo &cbInfo) {
QtOhos::logJsCallbackError(cbInfo, "listVoices() failed");
catchPromise({});
});
},
Q_FUNC_INFO);
}
}
TextToSpeechProxy::TextToSpeechProxy() = default;
TextToSpeechProxy::~TextToSpeechProxy() = default;
q23::expected<std::function<std::shared_ptr<TextToSpeechProxy>()>, std::string> tryMakeTextToSpeechProxyFactory(
const std::string &language, int personTimbre)
{
auto proxyImplInstanceOrError = TextToSpeechProxyImpl::tryMakeInstance(language, personTimbre);
if (!proxyImplInstanceOrError)
return q23::unexpected(std::move(proxyImplInstanceOrError.error()));
return std::function<std::shared_ptr<TextToSpeechProxy>()>(
[proxyImplInstance = std::move(proxyImplInstanceOrError.value())]() {
return proxyImplInstance;
});
}
}
QT_END_NAMESPACE