Source code

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 "qtexttospeech_ohos.h"
#include "qtexttospeech_ohos_plugin.h"
#include "corespeechkit/texttospeechproxy.h"
#include <QtTextToSpeech/qvoice.h>
#include <QtCore/qcoreapplication.h>
#include <QtCore/qlist.h>
#include <QtCore/qlocale.h>
#include <QtCore/qloggingcategory.h>
#include <QtCore/qstring.h>
#include <array>
#include <cstdint>
#include <memory>
#include <optional>
QT_BEGIN_NAMESPACE
namespace {
constexpr const char *chineseLanguageContext = "zh-CN";
QVoice::Gender mapVoiceGender(const std::string &gender)
{
return gender == "male" ? QVoice::Gender::Male :
gender == "female" ? QVoice::Gender::Female :
QVoice::Gender::Unknown;
}
// Source of values used for VoiceStatus can be found under section 'VoiceInfo.status':
enum class VoiceStatus
{
Downloadable,
Installed,
Unavailable,
};
std::optional<VoiceStatus> mapVoiceStatus(const std::string &status)
{
static constexpr std::array<std::pair<VoiceStatus, const char *>, 3> voiceStatuses = {{
{VoiceStatus::Downloadable, "GA"},
{VoiceStatus::Installed, "INSTALLED"},
{VoiceStatus::Unavailable, "EOM"},
}};
for (const auto &[voiceStatus, ohosStatus] : voiceStatuses) {
if (status == ohosStatus)
return voiceStatus;
}
qCWarning(lcSpeechTtsOhos) << "unexpected voice status:" << QString::fromStdString(status);
return {};
}
QVariantMap mapVoiceExtraData(const CoreSpeechKit::VoiceInfo &info)
{
return QVariantMap{
{QStringLiteral("style"), QString::fromStdString(info.style)},
{QStringLiteral("person"), info.personTimbre},
};
}
struct EngineError
{
QTextToSpeech::ErrorReason reason;
QString message;
};
// Source of the error codes can be found under section 'Error Codes':
EngineError mapTtsError(std::uint32_t errorCode)
{
static constexpr std::uint32_t ohosTextOutOfRangeOrEmptyValue = 1002300001;
static constexpr std::uint32_t ohosLanguageNotSupportedValue = 1002300002;
static constexpr std::uint32_t ohosPersonNotSupportedValue = 1002300003;
static constexpr std::uint32_t ohosCreateEngineFailedValue = 1002300005;
static constexpr std::uint32_t ohosParameterErrorValue = 1002300009;
switch (errorCode) {
case ohosTextOutOfRangeOrEmptyValue:
return {
QTextToSpeech::ErrorReason::Input,
QCoreApplication::translate("QTextToSpeech", "Speech synthesizing failure.")
};
case ohosLanguageNotSupportedValue:
case ohosPersonNotSupportedValue:
case ohosParameterErrorValue:
return {
QTextToSpeech::ErrorReason::Configuration,
QCoreApplication::translate("QTextToSpeech", "Could not apply text-to-speech parameters.")
};
case ohosCreateEngineFailedValue:
return {
QTextToSpeech::ErrorReason::Initialization,
QCoreApplication::translate("QTextToSpeech", "Failed to initialize text-to-speech engine.")
};
default:
return {
QTextToSpeech::ErrorReason::Playback,
QCoreApplication::translate("QTextToSpeech", "Unknown error: %1.").arg(errorCode)
};
}
}
double mapVolumeToOhosVolume(double volume)
{
constexpr double maxTtsVolume = 2.0;
return volume * maxTtsVolume;
}
double mapRateToOhosSpeed(double rate)
{
constexpr double minTtsRate = 0.5;
constexpr double normalTtsRate = 1.0;
return rate < 0
? minTtsRate * rate + normalTtsRate
: rate + normalTtsRate;
}
double mapPitchToOhosPitch(double pitch)
{
constexpr double minTtsPitch = 0.5;
constexpr double normalTtsPitch = 1.0;
return pitch < 0
? minTtsPitch * pitch + normalTtsPitch
: pitch + normalTtsPitch;
}
class QTextToSpeechEngineOhos : public QTextToSpeechEngine
{
public:
QTextToSpeechEngineOhos(const QVariantMap &parameters, QObject *parent);
QList<QLocale> availableLocales() const override;
QList<QVoice> availableVoices() const override;
void say(const QString &text) override;
void synthesize(const QString &text) override;
void stop(QTextToSpeech::BoundaryHint boundaryHint) override;
void pause(QTextToSpeech::BoundaryHint boundaryHint) override;
void resume() override;
double rate() const override;
bool setRate(double rate) override;
double pitch() const override;
bool setPitch(double pitch) override;
QLocale locale() const override;
bool setLocale(const QLocale &locale) override;
double volume() const override;
bool setVolume(double volume) override;
QVoice voice() const override;
bool setVoice(const QVoice &voice) override;
QTextToSpeech::State state() const override;
QTextToSpeech::ErrorReason errorReason() const override;
QString errorString() const override;
private:
class TextToSpeechEngineEventsListener
: public CoreSpeechKit::TextToSpeechProxy::EngineEventsListener
{
public:
explicit TextToSpeechEngineEventsListener(QTextToSpeechEngineOhos &owner);
void onStart(std::string utteranceId) override;
void onComplete(std::string utteranceId, std::optional<CoreSpeechKit::TtsCompletionType> optCompletionType) override;
void onStop(std::string utteranceId) override;
void onError(std::string utteranceId, std::uint32_t errorCode, std::string errorMsg) override;
QTextToSpeechEngineOhos& m_owner;
};
void handleOnStart(const QString &utteranceId);
void handleOnComplete(const QString &utteranceId, std::optional<CoreSpeechKit::TtsCompletionType> optCompletionType);
void handleOnStop(const QString &utteranceId);
void handleOnError(
const QString &utteranceId, std::uint32_t errorCode, const QString &errorMessage);
void setState(QTextToSpeech::State state);
void setError(const EngineError &error);
bool prepareTextToSpeechProxyForCurrentVoice();
std::shared_ptr<CoreSpeechKit::TextToSpeechProxy> m_ttsProxy;
QTextToSpeech::State m_state;
QTextToSpeech::ErrorReason m_errorReason;
QString m_errorString;
QList<QVoice> m_voices;
QList<QLocale> m_locales;
QVoice m_currentVoice;
double m_volume;
double m_rate;
double m_pitch;
};
QTextToSpeechEngineOhos::TextToSpeechEngineEventsListener::TextToSpeechEngineEventsListener(
QTextToSpeechEngineOhos &owner)
: m_owner(owner)
{
}
void QTextToSpeechEngineOhos::TextToSpeechEngineEventsListener::onStart(std::string utteranceId)
{
m_owner.handleOnStart(QString::fromStdString(utteranceId));
}
void QTextToSpeechEngineOhos::TextToSpeechEngineEventsListener::onComplete(
std::string utteranceId, std::optional<CoreSpeechKit::TtsCompletionType> optCompletionType)
{
m_owner.handleOnComplete(QString::fromStdString(utteranceId), optCompletionType);
}
void QTextToSpeechEngineOhos::TextToSpeechEngineEventsListener::onStop(std::string utteranceId)
{
m_owner.handleOnStop(QString::fromStdString(utteranceId));
}
void QTextToSpeechEngineOhos::TextToSpeechEngineEventsListener::onError(
std::string utteranceId, std::uint32_t errorCode, std::string errorMessage)
{
m_owner.handleOnError(
QString::fromStdString(utteranceId), errorCode, QString::fromStdString(errorMessage));
}
QTextToSpeechEngineOhos::QTextToSpeechEngineOhos(const QVariantMap &, QObject *parent)
: QTextToSpeechEngine(parent)
, m_state(QTextToSpeech::Ready)
, m_errorReason(QTextToSpeech::ErrorReason::NoError)
, m_volume(1.0)
, m_rate(0.0)
, m_pitch(0.0)
{
auto optVoices = CoreSpeechKit::tryListVoices();
if (!optVoices) {
setError({
QTextToSpeech::ErrorReason::Configuration,
QCoreApplication::translate("QTextToSpeech", "Failed to initialize default locale and voice.")
});
return;
}
for (const auto &voiceInfo : *optVoices) {
if (voiceInfo.status) {
const auto optStatus = mapVoiceStatus(*voiceInfo.status);
if (optStatus && *optStatus != VoiceStatus::Installed)
continue;
}
QLocale locale(QString::fromStdString(voiceInfo.language));
m_voices.append(
createVoice(
QString::fromStdString(voiceInfo.description), locale,
mapVoiceGender(voiceInfo.gender), QVoice::Other,
mapVoiceExtraData(voiceInfo)));
if (!m_locales.contains(locale))
m_locales.append(locale);
}
if (!m_voices.isEmpty())
m_currentVoice = m_voices.first();
}
void QTextToSpeechEngineOhos::handleOnStart(const QString &)
{
setState(QTextToSpeech::Speaking);
}
void QTextToSpeechEngineOhos::handleOnComplete(
const QString &, std::optional<CoreSpeechKit::TtsCompletionType> optCompletionType)
{
if (!optCompletionType) {
setError({
QTextToSpeech::ErrorReason::Playback,
QCoreApplication::translate("QTextToSpeech", "Speech synthesizing failure.")
});
return;
}
if (*optCompletionType == CoreSpeechKit::TtsCompletionType::SpeechComplete)
setState(QTextToSpeech::Ready);
}
void QTextToSpeechEngineOhos::handleOnStop(const QString &)
{
setState(QTextToSpeech::Ready);
}
void QTextToSpeechEngineOhos::handleOnError(
const QString &utteranceId, std::uint32_t errorCode, const QString &errorMessage)
{
qCWarning(lcSpeechTtsOhos)
<< Q_FUNC_INFO << ": text to speech error: code" << errorCode
<< errorMessage << ", utteranceId:" << utteranceId;
setError(mapTtsError(errorCode));
}
void QTextToSpeechEngineOhos::setState(QTextToSpeech::State state)
{
if (m_state == state)
return;
m_state = state;
Q_EMIT stateChanged(m_state);
if (m_state == QTextToSpeech::Error) {
Q_EMIT errorOccurred(m_errorReason, m_errorString);
} else {
m_errorReason = QTextToSpeech::ErrorReason::NoError;
m_errorString.clear();
}
}
void QTextToSpeechEngineOhos::setError(const EngineError &error)
{
m_errorReason = error.reason;
m_errorString = error.message;
if (error.reason == QTextToSpeech::ErrorReason::NoError) {
m_errorString.clear();
return;
}
if (m_state != QTextToSpeech::Error) {
m_state = QTextToSpeech::Error;
Q_EMIT stateChanged(m_state);
}
Q_EMIT errorOccurred(m_errorReason, m_errorString);
}
QList<QLocale> QTextToSpeechEngineOhos::availableLocales() const
{
return m_locales;
}
QList<QVoice> QTextToSpeechEngineOhos::availableVoices() const
{
QList<QVoice> voices;
for (const auto &voice : m_voices) {
if (voice.locale() == m_currentVoice.locale())
voices << voice;
}
return voices;
}
bool QTextToSpeechEngineOhos::prepareTextToSpeechProxyForCurrentVoice()
{
if (m_currentVoice == QVoice()) {
setError({
QTextToSpeech::ErrorReason::Configuration,
QCoreApplication::translate("QTextToSpeech", "Failed to initialize default locale and voice.")
});
return false;
}
const auto language = m_currentVoice.locale().name(QLocale::TagSeparator::Dash).toStdString();
const auto personTimbre = voiceData(m_currentVoice).toMap()[QStringLiteral("person")].toInt();
if (m_ttsProxy && m_ttsProxy->language() == language
&& m_ttsProxy->personTimbre() == personTimbre) {
return true;
}
m_ttsProxy.reset();
auto ttsProxyOrError = CoreSpeechKit::tryMakeTextToSpeechProxy(language, personTimbre);
if (!ttsProxyOrError) {
qCWarning(lcSpeechTtsOhos)
<< Q_FUNC_INFO << ": could not create an engine for voice" << m_currentVoice.name()
<< "TTS error:" << QString::fromStdString(ttsProxyOrError.error());
setError({
QTextToSpeech::ErrorReason::Initialization,
QCoreApplication::translate("QTextToSpeech", "Failed to initialize text-to-speech engine.")
});
return false;
}
m_ttsProxy = std::move(ttsProxyOrError.value());
m_ttsProxy->setEngineEventsListener(std::make_shared<TextToSpeechEngineEventsListener>(*this));
return true;
}
void QTextToSpeechEngineOhos::say(const QString &text)
{
if (text.isEmpty())
return;
if (!prepareTextToSpeechProxyForCurrentVoice())
return;
m_ttsProxy->speak(
{
.text = text.toStdString(),
.speed = mapRateToOhosSpeed(m_rate),
.volume = mapVolumeToOhosVolume(m_volume),
.pitch = mapPitchToOhosPitch(m_pitch),
.languageContext = chineseLanguageContext,
});
}
void QTextToSpeechEngineOhos::synthesize(const QString &)
{
qCWarning(lcSpeechTtsOhos) << Q_FUNC_INFO << ": synthesize feature is not supported";
}
void QTextToSpeechEngineOhos::stop(QTextToSpeech::BoundaryHint)
{
if (!m_ttsProxy)
return;
m_ttsProxy->stop();
}
void QTextToSpeechEngineOhos::pause(QTextToSpeech::BoundaryHint)
{
qCWarning(lcSpeechTtsOhos) << Q_FUNC_INFO << ": pause feature is not supported";
}
void QTextToSpeechEngineOhos::resume()
{
qCWarning(lcSpeechTtsOhos) << Q_FUNC_INFO << ": resume feature is not supported";
}
double QTextToSpeechEngineOhos::rate() const
{
return m_rate;
}
bool QTextToSpeechEngineOhos::setRate(double rate)
{
m_rate = rate;
return true;
}
double QTextToSpeechEngineOhos::pitch() const
{
return m_pitch;
}
bool QTextToSpeechEngineOhos::setPitch(double pitch)
{
m_pitch = pitch;
return true;
}
QLocale QTextToSpeechEngineOhos::locale() const
{
return m_currentVoice.locale();
}
bool QTextToSpeechEngineOhos::setLocale(const QLocale &locale)
{
if (!m_locales.contains(locale))
return false;
if (m_currentVoice.locale() == locale)
return true;
for (const auto &voice : m_voices) {
if (voice.locale() == locale) {
m_currentVoice = voice;
return true;
}
}
return false;
}
double QTextToSpeechEngineOhos::volume() const
{
return m_volume;
}
bool QTextToSpeechEngineOhos::setVolume(double volume)
{
m_volume = volume;
return true;
}
QVoice QTextToSpeechEngineOhos::voice() const
{
return m_currentVoice;
}
bool QTextToSpeechEngineOhos::setVoice(const QVoice &voice)
{
if (!m_voices.contains(voice)) {
qCWarning(lcSpeechTtsOhos) << "voice" << voice.name() << "is not available in this engine";
return false;
}
m_currentVoice = voice;
return true;
}
QTextToSpeech::State QTextToSpeechEngineOhos::state() const
{
return m_state;
}
QTextToSpeech::ErrorReason QTextToSpeechEngineOhos::errorReason() const
{
return m_errorReason;
}
QString QTextToSpeechEngineOhos::errorString() const
{
return m_errorString;
}
}
QTextToSpeechEngine *createQTextToSpeechEngineOhos(
const QVariantMap &parameters, QObject *parent, QString *errorString)
{
Q_UNUSED(errorString);
return new QTextToSpeechEngineOhos(parameters, parent);
}
QT_END_NAMESPACE