Script 'mail_helper' called by obssrc Hello community, here is the log from the commit of package qt6-speech for openSUSE:Factory checked in at 2026-10-18 19:21:39 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ Comparing /work/SRC/openSUSE:Factory/qt6-speech (Old) and /work/SRC/openSUSE:Factory/.qt6-speech.new.3698736 (New) ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Package is "qt6-speech" Sun Oct 18 19:21:39 2026 rev:31 rq:1384414 version:6.12.0 Changes: -------- --- /work/SRC/openSUSE:Factory/qt6-speech/qt6-speech.changes 2026-08-21 16:52:20.350875522 +0200 +++ /work/SRC/openSUSE:Factory/.qt6-speech.new.3698736/qt6-speech.changes 2026-10-18 19:21:55.262270388 +0200 @@ -1,0 +2,6 @@ +Wed Sep 30 16:16:53 UTC 2026 - Christophe Marin <[email protected]> + +- Update to 6.12.0 + https://www.qt.io/blog/qt-6.12-released + +------------------------------------------------------------------- Old: ---- qtspeech-everywhere-src-6.11.2.tar.xz New: ---- qtspeech-everywhere-src-6.12.0.tar.xz ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ Other differences: ------------------ ++++++ qt6-speech.spec ++++++ --- /var/tmp/diff_new_pack.dBfgGJ/_old 2026-10-18 19:21:55.976299990 +0200 +++ /var/tmp/diff_new_pack.dBfgGJ/_new 2026-10-18 19:21:55.977300032 +0200 @@ -16,8 +16,8 @@ # -%define real_version 6.11.2 -%define short_version 6.11 +%define real_version 6.12.0 +%define short_version 6.12 %define tar_name qtspeech-everywhere-src %define tar_suffix %{nil} # @@ -26,21 +26,15 @@ %define pkg_suffix -docs %endif # -# Not available in Leap 15 -%if 0%{?suse_version} > 1500 -%bcond_without flite -%endif Name: qt6-speech%{?pkg_suffix} -Version: 6.11.2 +Version: 6.12.0 Release: 0 Summary: Qt 6 TextToSpeech Library and Plugin License: GPL-2.0-only OR LGPL-3.0-only OR GPL-3.0-only URL: https://www.qt.io Source0: https://download.qt.io/official_releases/qt/%{short_version}/%{real_version}%{tar_suffix}/submodules/%{tar_name}-%{real_version}%{tar_suffix}.tar.xz -%if %{with flite} # Flite is built without Alsa, adding it to BuildRequires isn't needed BuildRequires: flite-devel > 2.0 -%endif BuildRequires: pkgconfig BuildRequires: cmake(Qt6Concurrent) = %{real_version} BuildRequires: cmake(Qt6Core) = %{real_version} @@ -116,9 +110,7 @@ %files -n qt6-texttospeech %dir %{_qt6_pluginsdir}/texttospeech/ -%if %{with flite} %{_qt6_pluginsdir}/texttospeech/libqtexttospeech_flite.so -%endif %{_qt6_pluginsdir}/texttospeech/libqtexttospeech_mock.so %{_qt6_pluginsdir}/texttospeech/libqtexttospeech_speechd.so %{_qt6_qmldir}/QtTextToSpeech/ ++++++ qtspeech-everywhere-src-6.11.2.tar.xz -> qtspeech-everywhere-src-6.12.0.tar.xz ++++++ diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/qtspeech-everywhere-src-6.11.2/.cmake.conf new/qtspeech-everywhere-src-6.12.0/.cmake.conf --- old/qtspeech-everywhere-src-6.11.2/.cmake.conf 2026-08-12 18:34:55.000000000 +0200 +++ new/qtspeech-everywhere-src-6.12.0/.cmake.conf 2026-09-26 19:45:17.000000000 +0200 @@ -1,8 +1,9 @@ -set(QT_REPO_MODULE_VERSION "6.11.2") +set(QT_REPO_MODULE_VERSION "6.12.0") set(QT_EXTRA_INTERNAL_TARGET_DEFINES "QT_ENABLE_STRICT_MODE_UP_TO=QT_VERSION_CHECK(6,11,0)" # not yet in strict mode: "QT_LEAN_HEADERS=1" + "QT_NO_DISCONNECT_CONST_CONNECTION=1" "QT_NO_QPAIR=1" "QT_NO_SINGLE_ARGUMENT_QHASH_OVERLOAD=1" ) diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/qtspeech-everywhere-src-6.11.2/.tag new/qtspeech-everywhere-src-6.12.0/.tag --- old/qtspeech-everywhere-src-6.11.2/.tag 2026-08-12 18:34:55.000000000 +0200 +++ new/qtspeech-everywhere-src-6.12.0/.tag 2026-09-26 19:45:17.000000000 +0200 @@ -1 +1 @@ -ae2de454bc73956632841221c535959f6f950346 +332f8a148bf01739140c3a00dd54f3ed98f3b546 diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/qtspeech-everywhere-src-6.11.2/CONTRIBUTING.md new/qtspeech-everywhere-src-6.12.0/CONTRIBUTING.md --- old/qtspeech-everywhere-src-6.11.2/CONTRIBUTING.md 1970-01-01 01:00:00.000000000 +0100 +++ new/qtspeech-everywhere-src-6.12.0/CONTRIBUTING.md 2026-09-26 19:45:17.000000000 +0200 @@ -0,0 +1,11 @@ +## Qt contribution guidelines + +We welcome contributions to Qt! + +Note that we cannot accept pull requests on GitHub. All contributions +to the Qt project are exclusively handled through the +[Gerrit code review system](https://codereview.qt-project.org). + +See +[contribute.qt-project.org](https://contribute.qt-project.org/) to learn +more. diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/qtspeech-everywhere-src-6.11.2/REUSE.toml new/qtspeech-everywhere-src-6.12.0/REUSE.toml --- old/qtspeech-everywhere-src-6.11.2/REUSE.toml 2026-08-12 18:34:55.000000000 +0200 +++ new/qtspeech-everywhere-src-6.12.0/REUSE.toml 2026-09-26 19:45:17.000000000 +0200 @@ -2,7 +2,6 @@ [[annotations]] path = ["src/tts/doc/QtTextToSpeechDoc", - "src/plugins/tts/android/jar/AndroidManifest.xml", "src/plugins/**json"] precedence = "closest" comment = "src and plugin" @@ -32,7 +31,7 @@ SPDX-License-Identifier = "LicenseRef-Qt-Commercial OR BSD-3-Clause" [[annotations]] -path = ["**/doc/images/**", "**.qdocconf", "config_help.txt"] +path = ["**/doc/images/**", "**.qdocconf", "config_help.txt", "CONTRIBUTING.md"] comment = "documentation" precedence = "closest" SPDX-FileCopyrightText = "Copyright (C) The Qt Company Ltd." @@ -46,6 +45,15 @@ SPDX-License-Identifier = "LicenseRef-Qt-Commercial OR BSD-3-Clause" [[annotations]] +path = ["src/**/build.gradle", "src/**/settings.gradle", + "src/**/gradle.properties", "src/**/libs.versions.toml"] +precedence = "override" +comment = "build system" +SPDX-FileCopyrightText = "Copyright (C) The Qt Company Ltd." +SPDX-License-Identifier = "BSD-3-Clause" + + +[[annotations]] path = ["**/qt_attribution.json"] comment = "documentation" precedence = "override" diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/qtspeech-everywhere-src-6.11.2/dependencies.yaml new/qtspeech-everywhere-src-6.12.0/dependencies.yaml --- old/qtspeech-everywhere-src-6.11.2/dependencies.yaml 2026-08-12 18:34:55.000000000 +0200 +++ new/qtspeech-everywhere-src-6.12.0/dependencies.yaml 2026-09-26 19:45:17.000000000 +0200 @@ -1,10 +1,10 @@ dependencies: ../qtbase: - ref: ef55f427f2c8b410d34f8a7681020a3000cf6866 + ref: 025bdad181de241e81bf853c8a2d7bf3d19261a9 required: true ../qtdeclarative: - ref: 4e3399c26ec57246c08de019cfcbda8d23604cfa + ref: 4332fbbb4c340463dc8cee36dda0c85d932122d8 required: false ../qtmultimedia: - ref: 6f162ccac1425edbd7b4d1582fabab5973b43d6c + ref: 6ca885f084a3984749d53bb50a0e08bdc60e86cc required: true diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/qtspeech-everywhere-src-6.11.2/licenseRule.json new/qtspeech-everywhere-src-6.12.0/licenseRule.json --- old/qtspeech-everywhere-src-6.11.2/licenseRule.json 2026-08-12 18:34:55.000000000 +0200 +++ new/qtspeech-everywhere-src-6.12.0/licenseRule.json 2026-09-26 19:45:17.000000000 +0200 @@ -57,9 +57,9 @@ } }, { - "comment": "Files with the following endings are Documentation licensed.", - "file_pattern_ending": [".qdoc", ".qdocinc" , ".qdocconf", "README", "qt_attribution.json", - "config_help.txt"], + "comment": "Files with the following endings are documentation licensed.", + "file_pattern_ending": [".qdoc", ".qdocinc" , ".qdocconf", "README", "CONTRIBUTING.md", + "qt_attribution.json", "config_help.txt"], "location":{ "": { "comment": "", @@ -103,6 +103,11 @@ "comment": "Default", "file type": "documentation", "spdx": ["LicenseRef-Qt-Commercial OR GFDL-1.3-no-invariants-only"] + }, + ".*(\\.gradle|gradle\\.properties|libs\\.versions\\.toml)$": { + "comment": "Android Gradle build files", + "file type": "build system", + "spdx": ["BSD-3-Clause"] } } } diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/qtspeech-everywhere-src-6.11.2/src/plugins/tts/CMakeLists.txt new/qtspeech-everywhere-src-6.12.0/src/plugins/tts/CMakeLists.txt --- old/qtspeech-everywhere-src-6.11.2/src/plugins/tts/CMakeLists.txt 2026-08-12 18:34:55.000000000 +0200 +++ new/qtspeech-everywhere-src-6.12.0/src/plugins/tts/CMakeLists.txt 2026-09-26 19:45:17.000000000 +0200 @@ -17,6 +17,9 @@ if(ANDROID) add_subdirectory(android) endif() +if(OHOS) + add_subdirectory(ohos) +endif() if(QT_FEATURE_flite) add_subdirectory(flite) endif() diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/qtspeech-everywhere-src-6.11.2/src/plugins/tts/android/jar/AndroidManifest.xml new/qtspeech-everywhere-src-6.12.0/src/plugins/tts/android/jar/AndroidManifest.xml --- old/qtspeech-everywhere-src-6.11.2/src/plugins/tts/android/jar/AndroidManifest.xml 2026-08-12 18:34:55.000000000 +0200 +++ new/qtspeech-everywhere-src-6.12.0/src/plugins/tts/android/jar/AndroidManifest.xml 1970-01-01 01:00:00.000000000 +0100 @@ -1,7 +0,0 @@ -<?xml version='1.0' encoding='utf-8'?> -<manifest xmlns:android="http://schemas.android.com/apk/res/android" - android:versionCode="1" - android:versionName="1.0" - package="org.qtproject.qt.android.qspeech"> - <supports-screens android:largeScreens="true" android:normalScreens="true" android:anyDensity="true" android:smallScreens="true"/> -</manifest> diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/qtspeech-everywhere-src-6.11.2/src/plugins/tts/android/jar/CMakeLists.txt new/qtspeech-everywhere-src-6.12.0/src/plugins/tts/android/jar/CMakeLists.txt --- old/qtspeech-everywhere-src-6.11.2/src/plugins/tts/android/jar/CMakeLists.txt 2026-08-12 18:34:55.000000000 +0200 +++ new/qtspeech-everywhere-src-6.12.0/src/plugins/tts/android/jar/CMakeLists.txt 2026-09-26 19:45:17.000000000 +0200 @@ -6,7 +6,6 @@ ) qt_internal_add_jar(Qt${QtSpeech_VERSION_MAJOR}AndroidTextToSpeech - INCLUDE_JARS ${QT_ANDROID_JAR} SOURCES ${java_sources} OUTPUT_DIR "${QT_BUILD_DIR}/jar" ) diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/qtspeech-everywhere-src-6.11.2/src/plugins/tts/android/jar/build.gradle new/qtspeech-everywhere-src-6.12.0/src/plugins/tts/android/jar/build.gradle --- old/qtspeech-everywhere-src-6.11.2/src/plugins/tts/android/jar/build.gradle 1970-01-01 01:00:00.000000000 +0100 +++ new/qtspeech-everywhere-src-6.12.0/src/plugins/tts/android/jar/build.gradle 2026-09-26 19:45:17.000000000 +0200 @@ -0,0 +1,18 @@ +plugins { + alias(libs.plugins.android.library) +} + +android { + namespace = 'org.qtproject.qt.android.speech' + compileSdk = (project.findProperty('compileSdk') ?: '36') as int + + defaultConfig { + minSdk = (project.findProperty('minSdk') ?: '28') as int + } + + sourceSets { + main { + java.srcDirs = ['src'] + } + } +} diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/qtspeech-everywhere-src-6.11.2/src/plugins/tts/android/jar/gradle/libs.versions.toml new/qtspeech-everywhere-src-6.12.0/src/plugins/tts/android/jar/gradle/libs.versions.toml --- old/qtspeech-everywhere-src-6.11.2/src/plugins/tts/android/jar/gradle/libs.versions.toml 1970-01-01 01:00:00.000000000 +0100 +++ new/qtspeech-everywhere-src-6.12.0/src/plugins/tts/android/jar/gradle/libs.versions.toml 2026-09-26 19:45:17.000000000 +0200 @@ -0,0 +1,5 @@ +[versions] +agp = "9.2.1" + +[plugins] +android-library = { id = "com.android.library", version.ref = "agp" } diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/qtspeech-everywhere-src-6.11.2/src/plugins/tts/android/jar/gradle.properties new/qtspeech-everywhere-src-6.12.0/src/plugins/tts/android/jar/gradle.properties --- old/qtspeech-everywhere-src-6.11.2/src/plugins/tts/android/jar/gradle.properties 1970-01-01 01:00:00.000000000 +0100 +++ new/qtspeech-everywhere-src-6.12.0/src/plugins/tts/android/jar/gradle.properties 2026-09-26 19:45:17.000000000 +0200 @@ -0,0 +1,15 @@ +# Project-wide Gradle settings. +# For more details on how to configure your build environment visit +# http://www.gradle.org/docs/current/userguide/build_environment.html +# Specifies the JVM arguments used for the daemon process. +# The setting is particularly useful for tweaking memory settings. +org.gradle.jvmargs=-Xmx3200m -XX:MaxMetaspaceSize=768m -XX:+HeapDumpOnOutOfMemoryError -Dfile.encoding=UTF-8 + +# Enable building projects in parallel +org.gradle.parallel=true + +# Allow AndroidX usage +android.useAndroidX=true + +# Enable caching +org.gradle.configuration-cache=true diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/qtspeech-everywhere-src-6.11.2/src/plugins/tts/android/jar/settings.gradle new/qtspeech-everywhere-src-6.12.0/src/plugins/tts/android/jar/settings.gradle --- old/qtspeech-everywhere-src-6.11.2/src/plugins/tts/android/jar/settings.gradle 1970-01-01 01:00:00.000000000 +0100 +++ new/qtspeech-everywhere-src-6.12.0/src/plugins/tts/android/jar/settings.gradle 2026-09-26 19:45:17.000000000 +0200 @@ -0,0 +1,17 @@ +pluginManagement { + repositories { + google() + mavenCentral() + gradlePluginPortal() + } +} + +dependencyResolutionManagement { + repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS) + repositories { + google() + mavenCentral() + } +} + +rootProject.name = 'Qt6AndroidTextToSpeech' diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/qtspeech-everywhere-src-6.11.2/src/plugins/tts/android/jar/src/org/qtproject/qt/android/speech/QtTextToSpeech.java new/qtspeech-everywhere-src-6.12.0/src/plugins/tts/android/jar/src/org/qtproject/qt/android/speech/QtTextToSpeech.java --- old/qtspeech-everywhere-src-6.11.2/src/plugins/tts/android/jar/src/org/qtproject/qt/android/speech/QtTextToSpeech.java 2026-08-12 18:34:55.000000000 +0200 +++ new/qtspeech-everywhere-src-6.12.0/src/plugins/tts/android/jar/src/org/qtproject/qt/android/speech/QtTextToSpeech.java 2026-09-26 19:45:17.000000000 +0200 @@ -73,7 +73,9 @@ } @Override + @SuppressWarnings("deprecation") public void onError(String utteranceId) { + // Needed for compilation even on 21+ (abstract) onError(utteranceId, TextToSpeech.ERROR); } @@ -297,7 +299,7 @@ Locale getLocale() { //Log.d(TAG, "getLocale: " + mLocale); - final Locale language = mTts.getLanguage(); + final Locale language = mTts.getVoice().getLocale(); if (language == null) return null; String languageCode = language.getLanguage(); diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/qtspeech-everywhere-src-6.11.2/src/plugins/tts/darwin/CMakeLists.txt new/qtspeech-everywhere-src-6.12.0/src/plugins/tts/darwin/CMakeLists.txt --- old/qtspeech-everywhere-src-6.11.2/src/plugins/tts/darwin/CMakeLists.txt 2026-08-12 18:34:55.000000000 +0200 +++ new/qtspeech-everywhere-src-6.12.0/src/plugins/tts/darwin/CMakeLists.txt 2026-09-26 19:45:17.000000000 +0200 @@ -14,5 +14,6 @@ Qt::Gui Qt::TextToSpeech Qt::Multimedia + ${FWFoundation} ${FWAVFoundation} ) diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/qtspeech-everywhere-src-6.11.2/src/plugins/tts/ohos/CMakeLists.txt new/qtspeech-everywhere-src-6.12.0/src/plugins/tts/ohos/CMakeLists.txt --- old/qtspeech-everywhere-src-6.11.2/src/plugins/tts/ohos/CMakeLists.txt 1970-01-01 01:00:00.000000000 +0100 +++ new/qtspeech-everywhere-src-6.12.0/src/plugins/tts/ohos/CMakeLists.txt 2026-09-26 19:45:17.000000000 +0200 @@ -0,0 +1,23 @@ +# Copyright (C) 2026 The Qt Company Ltd. +# SPDX-License-Identifier: BSD-3-Clause + +qt_find_package(NodeAddonApi MODULE PROVIDED_TARGETS NodeAddonApi::NodeAddonApi) + +qt_internal_add_plugin(QTextToSpeechOhosPlugin + OUTPUT_NAME qttexttospeech_ohos + PLUGIN_TYPE texttospeech + EXCEPTIONS + SOURCES + corespeechkit/texttospeechproxy.cpp corespeechkit/texttospeechproxy.h + qtexttospeech_ohos.cpp qtexttospeech_ohos.h + qtexttospeech_ohos_plugin.cpp qtexttospeech_ohos_plugin.h + DEFINES + NAPI_CPP_EXCEPTIONS + LIBRARIES + NodeAddonApi::NodeAddonApi + Qt::Core + Qt::CorePrivate + Qt::TextToSpeech + Qt::Multimedia + ace_napi.z +) diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/qtspeech-everywhere-src-6.11.2/src/plugins/tts/ohos/corespeechkit/texttospeechproxy.cpp new/qtspeech-everywhere-src-6.12.0/src/plugins/tts/ohos/corespeechkit/texttospeechproxy.cpp --- old/qtspeech-everywhere-src-6.11.2/src/plugins/tts/ohos/corespeechkit/texttospeechproxy.cpp 1970-01-01 01:00:00.000000000 +0100 +++ new/qtspeech-everywhere-src-6.12.0/src/plugins/tts/ohos/corespeechkit/texttospeechproxy.cpp 2026-09-26 19:45:17.000000000 +0200 @@ -0,0 +1,451 @@ +// 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 { + +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 {}; + } +} + +// Source of values used for EngineMode can be found under section 'online': +// https://developer.huawei.com/consumer/en/doc/harmonyos-references/hms-ai-texttospeech#section1638144844811 +enum class EngineMode +{ + Online = 0, + Offline = 1, +}; + +// Source of values used for SpeechPlayType can be found under section 'extraParams.playType': +// https://developer.huawei.com/consumer/en/doc/harmonyos-references/hms-ai-texttospeech#section7843122735210 +enum class SpeechPlayType +{ + SynthesisOnly = 0, + SynthesisAndPlayback = 1, +}; + +// Source of values used for SpeechQueueMode can be found under section 'extraParams.queueMode': +// https://developer.huawei.com/consumer/en/doc/harmonyos-references/hms-ai-texttospeech#section7843122735210 +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 ¶ms) override; + void stop() override; + std::vector<VoiceInfo> listVoices() override; + + void setEngineEventsListener( + std::shared_ptr<TextToSpeechProxy::EngineEventsListener> engineEventsListener); + +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; + std::shared_ptr<TextToSpeechProxy::EngineEventsListener> m_engineEventsListener; + const std::string m_language; +}; + +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, 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, std::shared_ptr<JsScopeData> jsScopeData) + : m_jsScopeData(std::move(jsScopeData)) + , m_language(language) +{ +} + +void TextToSpeechProxyImpl::speak(const SpeakParams ¶ms) +{ + 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; + +TextToSpeechProxy::EngineEventsListener::EngineEventsListener() = default; + +TextToSpeechProxy::EngineEventsListener::~EngineEventsListener() = default; + +q23::expected<std::function<std::shared_ptr<TextToSpeechProxy>(std::shared_ptr<TextToSpeechProxy::EngineEventsListener>)>, 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>(std::shared_ptr<TextToSpeechProxy::EngineEventsListener>)>( + [proxyImplInstance = std::move(proxyImplInstanceOrError.value())](std::shared_ptr<TextToSpeechProxy::EngineEventsListener> engineEventsListener) { + proxyImplInstance->setEngineEventsListener(engineEventsListener); + return proxyImplInstance; + }); +} + +} + +QT_END_NAMESPACE diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/qtspeech-everywhere-src-6.11.2/src/plugins/tts/ohos/corespeechkit/texttospeechproxy.h new/qtspeech-everywhere-src-6.12.0/src/plugins/tts/ohos/corespeechkit/texttospeechproxy.h --- old/qtspeech-everywhere-src-6.11.2/src/plugins/tts/ohos/corespeechkit/texttospeechproxy.h 1970-01-01 01:00:00.000000000 +0100 +++ new/qtspeech-everywhere-src-6.12.0/src/plugins/tts/ohos/corespeechkit/texttospeechproxy.h 2026-09-26 19:45:17.000000000 +0200 @@ -0,0 +1,82 @@ +// 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 + +#ifndef TEXTTOSPEECHPROXY_H +#define TEXTTOSPEECHPROXY_H + +#include <QtCore/private/qexpected_p.h> + +#include <cstdint> +#include <functional> +#include <memory> +#include <optional> +#include <string> +#include <vector> + +QT_BEGIN_NAMESPACE + +namespace CoreSpeechKit { + +enum class TtsCompletionType +{ + SynthesisComplete, + SpeechComplete, +}; + +struct SpeakParams +{ + std::string text; + double speed; + double volume; + double pitch; + std::string languageContext; +}; + +struct VoiceInfo +{ + std::string language; + std::string style; + std::string gender; + std::string description; + int personTimbre; +}; + +class TextToSpeechProxy +{ + Q_DISABLE_COPY_MOVE(TextToSpeechProxy) +public: + class EngineEventsListener + { + Q_DISABLE_COPY_MOVE(EngineEventsListener) + public: + virtual ~EngineEventsListener(); + + virtual void onStart(std::string utteranceId) = 0; + virtual void onComplete(std::string utteranceId, std::optional<TtsCompletionType> optCompletionType) = 0; + virtual void onStop(std::string utteranceId) = 0; + virtual void onError( + std::string utteranceId, std::uint32_t errorCode, std::string errorMessage) = 0; + + protected: + EngineEventsListener(); + }; + + virtual ~TextToSpeechProxy(); + + virtual void speak(const SpeakParams ¶ms) = 0; + virtual void stop() = 0; + virtual std::vector<VoiceInfo> listVoices() = 0; + +protected: + TextToSpeechProxy(); +}; + +q23::expected<std::function<std::shared_ptr<TextToSpeechProxy>(std::shared_ptr<TextToSpeechProxy::EngineEventsListener>)>, std::string> tryMakeTextToSpeechProxyFactory( + const std::string &language, int personTimbre); + +} + +QT_END_NAMESPACE + +#endif diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/qtspeech-everywhere-src-6.11.2/src/plugins/tts/ohos/ohos_plugin.json new/qtspeech-everywhere-src-6.12.0/src/plugins/tts/ohos/ohos_plugin.json --- old/qtspeech-everywhere-src-6.11.2/src/plugins/tts/ohos/ohos_plugin.json 1970-01-01 01:00:00.000000000 +0100 +++ new/qtspeech-everywhere-src-6.12.0/src/plugins/tts/ohos/ohos_plugin.json 2026-09-26 19:45:17.000000000 +0200 @@ -0,0 +1,9 @@ +{ + "Keys": ["ohos"], + "Provider": "ohos", + "Version": 100, + "Priority": 100, + "Capabilities": [ + "Speak" + ] +} diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/qtspeech-everywhere-src-6.11.2/src/plugins/tts/ohos/qtexttospeech_ohos.cpp new/qtspeech-everywhere-src-6.12.0/src/plugins/tts/ohos/qtexttospeech_ohos.cpp --- old/qtspeech-everywhere-src-6.11.2/src/plugins/tts/ohos/qtexttospeech_ohos.cpp 1970-01-01 01:00:00.000000000 +0100 +++ new/qtspeech-everywhere-src-6.12.0/src/plugins/tts/ohos/qtexttospeech_ohos.cpp 2026-09-26 19:45:17.000000000 +0200 @@ -0,0 +1,369 @@ +// 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/qlist.h> +#include <QtCore/qlocale.h> +#include <QtCore/qloggingcategory.h> +#include <QtCore/qstring.h> + +#include <cstdint> +#include <functional> +#include <memory> +#include <optional> + +QT_BEGIN_NAMESPACE + +namespace { + +constexpr const char *chineseLanguage = "zh-CN"; +constexpr const char *chineseLanguageContext = "zh-CN"; +constexpr int defaultPersonTimbre = 0; + +QVoice::Gender mapVoiceGender(const std::string &gender) +{ + return gender == "male" ? QVoice::Gender::Male : + gender == "female" ? QVoice::Gender::Female : + QVoice::Gender::Unknown; +} + +QVariantMap mapVoiceExtraData(const CoreSpeechKit::VoiceInfo &info) +{ + return QVariantMap{ + {QStringLiteral("style"), QString::fromStdString(info.style)}, + {QStringLiteral("person"), info.personTimbre}, + }; +} + +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 ¶meters, QObject *parent, + std::function<std::shared_ptr<CoreSpeechKit::TextToSpeechProxy>( + std::shared_ptr<CoreSpeechKit::TextToSpeechProxy::EngineEventsListener>)> ttsProxyFactory); + + 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); + + std::shared_ptr<CoreSpeechKit::TextToSpeechProxy> m_ttsProxy; + + QTextToSpeech::State m_state; + QList<QVoice> m_voices; + QList<QLocale> m_locales; + QLocale m_currentLocale; + 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, + std::function<std::shared_ptr<CoreSpeechKit::TextToSpeechProxy>(std::shared_ptr<CoreSpeechKit::TextToSpeechProxy::EngineEventsListener>)> ttsProxyFactory) + : QTextToSpeechEngine(parent) + , m_state(QTextToSpeech::Ready) + , m_volume(1.0) + , m_rate(0.0) + , m_pitch(0.0) +{ + m_ttsProxy = ttsProxyFactory(std::make_shared<TextToSpeechEngineEventsListener>(*this)); + for (const auto &voiceInfo : m_ttsProxy->listVoices()) { + 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_locales.isEmpty()) + m_currentLocale = m_locales.first(); +} + +void QTextToSpeechEngineOhos::handleOnStart(const QString &) +{ + setState(QTextToSpeech::Speaking); +} + +void QTextToSpeechEngineOhos::handleOnComplete( + const QString &, std::optional<CoreSpeechKit::TtsCompletionType> optCompletionType) +{ + if (!optCompletionType) { + setState(QTextToSpeech::Error); + 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) +{ + setState(QTextToSpeech::Error); + qCWarning(lcSpeechTtsOhos) + << Q_FUNC_INFO << ": text to speech error: code" << errorCode + << errorMessage << ", utteranceId:" << utteranceId; +} + +void QTextToSpeechEngineOhos::setState(QTextToSpeech::State state) +{ + if (state != m_state) { + m_state = state; + Q_EMIT stateChanged(m_state); + } +} + +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_currentLocale) + voices << voice; + } + + return voices; +} + +void QTextToSpeechEngineOhos::say(const QString &text) +{ + if (text.isEmpty()) + 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) +{ + 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_currentLocale; +} + +bool QTextToSpeechEngineOhos::setLocale(const QLocale &locale) +{ + if (!m_locales.contains(locale)) + return false; + + m_currentLocale = locale; + return true; +} + +double QTextToSpeechEngineOhos::volume() const +{ + return m_volume; +} + +bool QTextToSpeechEngineOhos::setVolume(double volume) +{ + m_volume = volume; + return true; +} + +QVoice QTextToSpeechEngineOhos::voice() const +{ + return {}; +} + +bool QTextToSpeechEngineOhos::setVoice(const QVoice &) +{ + return false; +} + +QTextToSpeech::State QTextToSpeechEngineOhos::state() const +{ + return m_state; +} + +QTextToSpeech::ErrorReason QTextToSpeechEngineOhos::errorReason() const +{ + qCWarning(lcSpeechTtsOhos) << Q_FUNC_INFO << ": errorReason feature is not supported"; + return QTextToSpeech::ErrorReason::NoError; +} + +QString QTextToSpeechEngineOhos::errorString() const +{ + qCWarning(lcSpeechTtsOhos) << Q_FUNC_INFO << ": errorString feature is not supported"; + return QString(); +} + +} + +QTextToSpeechEngine *createQTextToSpeechEngineOhos( + const QVariantMap ¶meters, QObject *parent, QString *errorString) +{ + auto ttsProxyFactoryOrError = CoreSpeechKit::tryMakeTextToSpeechProxyFactory( + chineseLanguage, defaultPersonTimbre); + + if (!ttsProxyFactoryOrError) { + if (errorString != nullptr) + *errorString = QString::fromStdString(ttsProxyFactoryOrError.error()); + return nullptr; + } + return new QTextToSpeechEngineOhos( + parameters, parent, std::move(ttsProxyFactoryOrError.value())); +} + +QT_END_NAMESPACE diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/qtspeech-everywhere-src-6.11.2/src/plugins/tts/ohos/qtexttospeech_ohos.h new/qtspeech-everywhere-src-6.12.0/src/plugins/tts/ohos/qtexttospeech_ohos.h --- old/qtspeech-everywhere-src-6.11.2/src/plugins/tts/ohos/qtexttospeech_ohos.h 1970-01-01 01:00:00.000000000 +0100 +++ new/qtspeech-everywhere-src-6.12.0/src/plugins/tts/ohos/qtexttospeech_ohos.h 2026-09-26 19:45:17.000000000 +0200 @@ -0,0 +1,21 @@ +// 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 + +#ifndef QTEXTTOSPEECHENGINE_OHOS_H +#define QTEXTTOSPEECHENGINE_OHOS_H + +#include <QtTextToSpeech/qtexttospeechengine.h> + +#include <QtCore/qobject.h> +#include <QtCore/qstring.h> +#include <QtCore/qvariant.h> + +QT_BEGIN_NAMESPACE + +QTextToSpeechEngine *createQTextToSpeechEngineOhos( + const QVariantMap ¶meters, QObject *parent, QString *errorString); + +QT_END_NAMESPACE + +#endif diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/qtspeech-everywhere-src-6.11.2/src/plugins/tts/ohos/qtexttospeech_ohos_plugin.cpp new/qtspeech-everywhere-src-6.12.0/src/plugins/tts/ohos/qtexttospeech_ohos_plugin.cpp --- old/qtspeech-everywhere-src-6.11.2/src/plugins/tts/ohos/qtexttospeech_ohos_plugin.cpp 1970-01-01 01:00:00.000000000 +0100 +++ new/qtspeech-everywhere-src-6.12.0/src/plugins/tts/ohos/qtexttospeech_ohos_plugin.cpp 2026-09-26 19:45:17.000000000 +0200 @@ -0,0 +1,19 @@ +// 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_plugin.h" + +#include "qtexttospeech_ohos.h" + +QT_BEGIN_NAMESPACE + +Q_LOGGING_CATEGORY(lcSpeechTtsOhos, "qt.speech.tts.ohos") + +QTextToSpeechEngine *QTextToSpeechOhosPlugin::createTextToSpeechEngine( + const QVariantMap ¶meters, QObject *parent, QString *errorString) const +{ + return createQTextToSpeechEngineOhos(parameters, parent, errorString); +} + +QT_END_NAMESPACE diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/qtspeech-everywhere-src-6.11.2/src/plugins/tts/ohos/qtexttospeech_ohos_plugin.h new/qtspeech-everywhere-src-6.12.0/src/plugins/tts/ohos/qtexttospeech_ohos_plugin.h --- old/qtspeech-everywhere-src-6.11.2/src/plugins/tts/ohos/qtexttospeech_ohos_plugin.h 1970-01-01 01:00:00.000000000 +0100 +++ new/qtspeech-everywhere-src-6.12.0/src/plugins/tts/ohos/qtexttospeech_ohos_plugin.h 2026-09-26 19:45:17.000000000 +0200 @@ -0,0 +1,32 @@ +// 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 + +#ifndef QTEXTTOSPEECHPLUGIN_OHOS_H +#define QTEXTTOSPEECHPLUGIN_OHOS_H + +#include <QtTextToSpeech/qtexttospeechengine.h> +#include <QtTextToSpeech/qtexttospeechplugin.h> + +#include <QtCore/qloggingcategory.h> +#include <QtCore/qobject.h> + +QT_BEGIN_NAMESPACE + +Q_DECLARE_LOGGING_CATEGORY(lcSpeechTtsOhos) + +class QTextToSpeechOhosPlugin : public QObject, public QTextToSpeechPlugin +{ + Q_OBJECT + Q_INTERFACES(QTextToSpeechPlugin) + Q_PLUGIN_METADATA(IID "org.qt-project.qt.speech.tts.plugin/6.0" + FILE "ohos_plugin.json") + +public: + QTextToSpeechEngine *createTextToSpeechEngine( + const QVariantMap ¶meters, QObject *parent, QString *errorString) const override; +}; + +QT_END_NAMESPACE + +#endif diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/qtspeech-everywhere-src-6.11.2/src/plugins/tts/winrt/qtexttospeech_winrt_audiosource.cpp new/qtspeech-everywhere-src-6.12.0/src/plugins/tts/winrt/qtexttospeech_winrt_audiosource.cpp --- old/qtspeech-everywhere-src-6.11.2/src/plugins/tts/winrt/qtexttospeech_winrt_audiosource.cpp 2026-08-12 18:34:55.000000000 +0200 +++ new/qtspeech-everywhere-src-6.12.0/src/plugins/tts/winrt/qtexttospeech_winrt_audiosource.cpp 2026-09-26 19:45:17.000000000 +0200 @@ -4,8 +4,9 @@ #include "qtexttospeech_winrt_audiosource.h" -#include <QtCore/QDebug> -#include <QtCore/QTimer> +#include <QtCore/qdebug.h> +#include <QtCore/qspan.h> +#include <QtCore/qtimer.h> #include <QtCore/private/qfunctions_winrt_p.h> #include <QtCore/private/qsystemerror_p.h> @@ -91,7 +92,7 @@ qint64 AudioSource::bytesAvailable() const { - return bytesInBuffer() + QIODevice::bytesAvailable(); + return m_bufferSpan.size_bytes() + QIODevice::bytesAvailable(); } /* @@ -105,31 +106,18 @@ if (!maxlen) return 0; - Q_ASSERT(bufferByteAccess); - - qint64 available = bytesInBuffer(); - maxlen = qMin(available, maxlen); - - if (!maxlen && atEnd()) + if (m_bufferSpan.isEmpty() && atEnd()) return -1; - byte *pbyte = nullptr; - bufferByteAccess->Buffer(&pbyte); - pbyte += m_bufferOffset; - - // Check and skip the RIFF header if present to prevent an audible - // click at the start of playback. - if (!m_riffHeaderChecked) { - m_riffHeaderChecked = true; - static const int WaveHeaderLength = 44; - const char *descriptor = reinterpret_cast<const char*>(pbyte); - if (maxlen >= WaveHeaderLength && !qstrncmp(descriptor, "RIFF", 4)) { - pbyte += WaveHeaderLength; - m_bufferOffset += WaveHeaderLength; - maxlen -= WaveHeaderLength; - available -= WaveHeaderLength; - } - } + auto take = [](auto span, qsizetype n) { + return span.first(std::min(span.size(), n)); + }; + + auto drop = [](auto span, qsizetype n) { + return span.subspan(std::min(span.size(), n)); + }; + + QSpan source = take(m_bufferSpan, maxlen); switch (m_pause) { case NoPause: @@ -140,12 +128,11 @@ if (m_pauseRequestedAt) { if (m_pauseRequestedAt <= m_bytesRead) { // we missed the window, pause immediately - maxlen = 0; - } else if (m_pauseRequestedAt <= m_bytesRead + maxlen) { - maxlen = qMax(quint64(0), m_pauseRequestedAt - m_bytesRead) + 44; + source = {}; + } else if (m_pauseRequestedAt <= m_bytesRead + quint64(source.size())) { + source = take(source, qsizetype(m_pauseRequestedAt - m_bytesRead)); } else { - // wait for the next chunk - break; + break; // pause point is in a later chunk } m_pause = Paused; m_pauseRequestedAt = 0; @@ -157,29 +144,26 @@ // look for a series (e.g. 1/50th of a second) of samples with low // absolute values. const int silenceDuration = audioFormat.sampleRate() / 50; - const short *sample = reinterpret_cast<short*>(pbyte); - const qsizetype sampleCount = maxlen / sizeof(short); + const auto samples = QSpan(reinterpret_cast<const short *>(source.data()), + source.size() / sizeof(short)); - const bool isInitialPauseRequest = !m_pauseDetectionSilenceCount.has_value(); - if (isInitialPauseRequest) + if (!m_pauseDetectionSilenceCount.has_value()) m_pauseDetectionSilenceCount = 0; - for (qint64 index = 0; index < sampleCount; ++index) { - if (qAbs(sample[index]) < 10) { + for (qsizetype i = 0; i < samples.size(); ++i) { + if (qAbs(samples[i]) < 10) *m_pauseDetectionSilenceCount += 1; - } else { + else { *m_pauseDetectionSilenceCount = 0; continue; } - if (*m_pauseDetectionSilenceCount > silenceDuration) { - // long enough silence found, only provide the data until we are in the silence. - + // long enough silence found, only provide data until we are in the silence. // we will still try to play at least half of the silent part - const int silentSamplesToPlay = silenceDuration / 2; - maxlen = index > silentSamplesToPlay ? (index - silentSamplesToPlay) * sizeof(short) - : index * sizeof(short); - + const qsizetype silentSamplesToPlay = silenceDuration / 2; + source = take(source, + (i > silentSamplesToPlay ? i - silentSamplesToPlay : i) + * sizeof(short)); m_pause = Paused; m_pauseDetectionSilenceCount = std::nullopt; break; @@ -196,23 +180,20 @@ } case Paused: // starve the sink so that it goes idle - maxlen = 0; - break; + return 0; } - if (!maxlen) + if (source.isEmpty()) return 0; - memcpy(data, pbyte, maxlen); + memcpy(data, source.data(), source.size()); + m_bufferSpan = drop(m_bufferSpan, source.size()); - // We emptied the buffer, so schedule fetching more - if (available <= maxlen) + if (m_bufferSpan.isEmpty()) fetchMore(); - else - m_bufferOffset += maxlen; - m_bytesRead += maxlen; - return maxlen; + m_bytesRead += source.size(); + return source.size(); } bool AudioSource::atEnd() const @@ -401,7 +382,24 @@ HRESULT hr = read->GetResults(&m_buffer); RETURN_HR_IF_FAILED("Could not access buffer."); - m_bufferOffset = 0; + + m_bufferSpan = [&] { + byte *ptr = nullptr; + bufferByteAccess->Buffer(&ptr); + UINT32 length = 0; + m_buffer->get_Length(&length); + return QSpan(reinterpret_cast<const char *>(ptr), qsizetype(length)); + }(); + + // Skip the RIFF header in the first buffer to prevent an audible click. + constexpr qsizetype WaveHeaderLength = 44; + if (!m_headerConsumed) { + Q_ASSERT(m_bytesRead == 0); + Q_ASSERT(m_bufferSpan.size() >= WaveHeaderLength); + Q_ASSERT(QByteArrayView(m_bufferSpan).startsWith("RIFF")); + m_bufferSpan = m_bufferSpan.subspan(WaveHeaderLength); + m_headerConsumed = true; + } ComPtr<IAsyncInfo> asyncInfo; if (HRESULT hr = readOperation.As(&asyncInfo); SUCCEEDED(hr)) @@ -409,22 +407,12 @@ readOperation.Reset(); // inform the sink that more data has arrived - if (m_pause == NoPause && bytesInBuffer()) + if (m_pause == NoPause && !m_bufferSpan.isEmpty()) emit readyRead(); return S_OK; } -qint64 AudioSource::bytesInBuffer() const -{ - if (!m_buffer) - return 0; - - UINT32 bytes; - m_buffer->get_Length(&bytes); - return bytes - m_bufferOffset; -} - /* Starts an asynchronous read operation. There can only be one such operation pending at any given time, so fetchMore must only be called diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/qtspeech-everywhere-src-6.11.2/src/plugins/tts/winrt/qtexttospeech_winrt_audiosource.h new/qtspeech-everywhere-src-6.12.0/src/plugins/tts/winrt/qtexttospeech_winrt_audiosource.h --- old/qtspeech-everywhere-src-6.11.2/src/plugins/tts/winrt/qtexttospeech_winrt_audiosource.h 2026-08-12 18:34:55.000000000 +0200 +++ new/qtspeech-everywhere-src-6.12.0/src/plugins/tts/winrt/qtexttospeech_winrt_audiosource.h 2026-09-26 19:45:17.000000000 +0200 @@ -6,6 +6,7 @@ #define QTEXTTOSPEECHENGINE_WINRT_AUDIOSOURCE_H #include <QtCore/QIODevice> +#include <QtCore/qspan.h> #include <QtMultimedia/QAudioFormat> #include <robuffer.h> @@ -98,7 +99,6 @@ // we don't destroy by accident, polymorphically, or via a QObject parent ~AudioSource() override; - qint64 bytesInBuffer() const; bool fetchMore(); QAudioFormat audioFormat; @@ -113,12 +113,11 @@ ComPtr<IBuffer> m_buffer; // access to the raw pcm bytes in the IBuffer; this took much reading of Windows header files... ComPtr<::Windows::Storage::Streams::IBufferByteAccess> bufferByteAccess; - // The data in the IBuffer might be paritally consumed - UINT32 m_bufferOffset = 0; - // RIFF header has been checked at the beginning of the stream - bool m_riffHeaderChecked = false; + // Span into the current IBuffer, shrunk as data is consumed + QSpan<const char> m_bufferSpan; quint64 m_bytesRead = 0; quint64 m_pauseRequestedAt = 0; + bool m_headerConsumed{}; std::optional<qint64> m_pauseDetectionSilenceCount; void populateBoundaries(); diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/qtspeech-everywhere-src-6.11.2/src/tts/doc/src/qttexttospeech-engines.qdoc new/qtspeech-everywhere-src-6.12.0/src/tts/doc/src/qttexttospeech-engines.qdoc --- old/qtspeech-everywhere-src-6.11.2/src/tts/doc/src/qttexttospeech-engines.qdoc 2026-08-12 18:34:55.000000000 +0200 +++ new/qtspeech-everywhere-src-6.12.0/src/tts/doc/src/qttexttospeech-engines.qdoc 2026-09-26 19:45:17.000000000 +0200 @@ -79,6 +79,21 @@ \li There is no API in Qt to get the list of installed engines. \endtable + \section1 HarmonyOS + + The HarmonyOS engine uses the \l{https://developer.huawei.com/consumer/en/doc/harmonyos-references/hms-ai-texttospeech} + {Core Speech Kit} text-to-speech APIs. + The system engine synthesizes and plays back the speech. + + The Core Speech Kit engine is created for a fixed language, which the plugin sets to + \c {zh-CN}. As stated in the \l{https://developer.huawei.com/consumer/en/doc/harmonyos-references/hms-ai-texttospeech#createengineparams} + {createEngine() documentation}, only Chinese and English are supported, with + \c {zh-CN} recommended for Chinese and \c {en-US} for English. + + \note The HarmonyOS engine only has the \l{QTextToSpeech::Capabilities}{Speak} + capability. It supports neither \l{QTextToSpeech::Capabilities}{PauseResume}, + \l{QTextToSpeech::Capabilities}{WordByWordProgress}, nor + \l{QTextToSpeech::Capabilities}{Synthesize}. \section1 Flite diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/qtspeech-everywhere-src-6.11.2/src/tts/qtexttospeech.h new/qtspeech-everywhere-src-6.12.0/src/tts/qtexttospeech.h --- old/qtspeech-everywhere-src-6.11.2/src/tts/qtexttospeech.h 2026-08-12 18:34:55.000000000 +0200 +++ new/qtspeech-everywhere-src-6.12.0/src/tts/qtexttospeech.h 2026-09-26 19:45:17.000000000 +0200 @@ -7,12 +7,11 @@ #include <QtCore/qlocale.h> #include <QtCore/qobject.h> +#include <QtCore/qmap.h> #include <QtCore/qshareddata.h> #include <QtTextToSpeech/qtexttospeech_global.h> #include <QtTextToSpeech/qvoice.h> -#include <QtCore/qmap.h> - #include <QtCore/q20type_traits.h> QT_BEGIN_NAMESPACE
