mirror of
https://github.com/xiaoyifang/goldendict-ng.git
synced 2024-11-27 15:24:05 +00:00
0
This commit is contained in:
parent
2b98b506fe
commit
ce5b9c4fb3
6
.gitignore
vendored
6
.gitignore
vendored
|
@ -33,7 +33,7 @@ GoldenDict.xcodeproj/
|
||||||
*.suo
|
*.suo
|
||||||
*.vcxproj.user
|
*.vcxproj.user
|
||||||
/.idea
|
/.idea
|
||||||
/.vs
|
.vs
|
||||||
/.vscode
|
/.vscode
|
||||||
/.qtc_clangd
|
/.qtc_clangd
|
||||||
|
|
||||||
|
@ -56,3 +56,7 @@ GoldenDict_resource.rc
|
||||||
*.orig
|
*.orig
|
||||||
|
|
||||||
node_modules
|
node_modules
|
||||||
|
|
||||||
|
# tts testing files
|
||||||
|
*.ogg
|
||||||
|
*.mp3
|
4
src/global_network_access_manager.hh
Normal file
4
src/global_network_access_manager.hh
Normal file
|
@ -0,0 +1,4 @@
|
||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <QNetworkAccessManager>
|
||||||
|
Q_APPLICATION_STATIC( QNetworkAccessManager, globalNetworkAccessManager )
|
54
src/tts/README.md
Normal file
54
src/tts/README.md
Normal file
|
@ -0,0 +1,54 @@
|
||||||
|
# Cloud TTS
|
||||||
|
|
||||||
|
## Add a new service checklist
|
||||||
|
|
||||||
|
* Read `service.h`.
|
||||||
|
* Implement `Service::speak`.
|
||||||
|
* Implement `Service::stop`.
|
||||||
|
* Implement `ServiceConfigWidget`, which will be embedded in `ConfigWindow`.
|
||||||
|
* Add the `Service` to `ServiceController`.
|
||||||
|
* Add the `ServiceConfigWidget` to `ConfigWindow`.
|
||||||
|
* DONE.
|
||||||
|
|
||||||
|
## Design Goals
|
||||||
|
|
||||||
|
Allow modifying / evolving any one of the services arbitrarily without incurring the need to touch another.
|
||||||
|
|
||||||
|
Avoid almost all temptation to do 💩 abstraction 💩 unless absolutely necessary.
|
||||||
|
|
||||||
|
## Code
|
||||||
|
|
||||||
|
### Config
|
||||||
|
|
||||||
|
```
|
||||||
|
(1) Service ConfigWidet --write--> (2) Service's config file --create--> (3) Live Service Object
|
||||||
|
```
|
||||||
|
|
||||||
|
* Config Serialization+Saving and Service state mutating will not happen in parallel or successively.
|
||||||
|
* (1) will neither mutate nor access (3).
|
||||||
|
* construct (3) only according to (2).
|
||||||
|
|
||||||
|
### Object management
|
||||||
|
|
||||||
|
* Service construction will be done on the service consumer side
|
||||||
|
* Service can be cast to `Service`, which only has `speak/stop` and destructor.
|
||||||
|
* The service consumer should not care
|
||||||
|
anything else after construction.
|
||||||
|
|
||||||
|
### Config Window
|
||||||
|
|
||||||
|
Similar to KDE's Settings module (KCM).
|
||||||
|
Every service simply provides a config widget on its own, and the config window simply loads the Widget.
|
||||||
|
|
||||||
|
### No exception
|
||||||
|
|
||||||
|
* Handle errors religiously and immediately, and report to users if user attention/action is required.
|
||||||
|
|
||||||
|
## Rational
|
||||||
|
|
||||||
|
* Services are different and testing them is hard (cloud tts usually needs an account).
|
||||||
|
* Do not assume services have any similarity other than the fact they may `speak`.
|
||||||
|
* Services on earth are limited, thus the boilerplate caused by fewer useless abstractions is also limited.
|
||||||
|
* The service consumer will use services incredibly and insanely creative in the future.
|
||||||
|
* Maintaining two code paths of object creation & mutating is a waste of time.
|
||||||
|
* Just save config to disk, and construct objects according to what's in the disk.
|
35
src/tts/config_file_main.cc
Normal file
35
src/tts/config_file_main.cc
Normal file
|
@ -0,0 +1,35 @@
|
||||||
|
#include "config_file_main.hh"
|
||||||
|
|
||||||
|
#include <QFileInfo>
|
||||||
|
#include <QSaveFile>
|
||||||
|
|
||||||
|
|
||||||
|
namespace TTS {
|
||||||
|
|
||||||
|
auto current_service_txt = "current_service.txt";
|
||||||
|
|
||||||
|
QString get_service_name_from_path( const QDir & configPath )
|
||||||
|
{
|
||||||
|
qDebug() << configPath;
|
||||||
|
if ( !QFileInfo::exists( configPath.absoluteFilePath( current_service_txt ) ) ) {
|
||||||
|
save_service_name_to_path( configPath, "azure" );
|
||||||
|
}
|
||||||
|
QFile f( configPath.filePath( current_service_txt ) );
|
||||||
|
if ( !f.open( QFile::ReadOnly ) ) {
|
||||||
|
throw std::runtime_error( "cannot open service name" ); // TODO
|
||||||
|
}
|
||||||
|
QString ret = f.readAll();
|
||||||
|
f.close();
|
||||||
|
return ret;
|
||||||
|
}
|
||||||
|
|
||||||
|
void save_service_name_to_path( const QDir & configPath, QUtf8StringView serviceName )
|
||||||
|
{
|
||||||
|
QSaveFile f( configPath.absoluteFilePath( current_service_txt ) );
|
||||||
|
if ( !f.open( QFile::WriteOnly ) ) {
|
||||||
|
throw std::runtime_error( "Cannot write service name" );
|
||||||
|
}
|
||||||
|
f.write( serviceName.data(), serviceName.length() );
|
||||||
|
f.commit();
|
||||||
|
};
|
||||||
|
} // namespace TTS
|
7
src/tts/config_file_main.hh
Normal file
7
src/tts/config_file_main.hh
Normal file
|
@ -0,0 +1,7 @@
|
||||||
|
#pragma once
|
||||||
|
#include <QDir>
|
||||||
|
|
||||||
|
namespace TTS {
|
||||||
|
QString get_service_name_from_path( const QDir & configRootPath );
|
||||||
|
void save_service_name_to_path( const QDir & configPath, QUtf8StringView serviceName );
|
||||||
|
} // namespace TTS
|
153
src/tts/config_window.cc
Normal file
153
src/tts/config_window.cc
Normal file
|
@ -0,0 +1,153 @@
|
||||||
|
#include "tts/config_window.hh"
|
||||||
|
#include "tts/services/azure.hh"
|
||||||
|
#include "tts/services/dummy.hh"
|
||||||
|
#include "tts/services/local_command.hh"
|
||||||
|
#include "tts/config_file_main.hh"
|
||||||
|
|
||||||
|
#include <QDialogButtonBox>
|
||||||
|
#include <QGridLayout>
|
||||||
|
#include <QGroupBox>
|
||||||
|
#include <QLabel>
|
||||||
|
#include <QPushButton>
|
||||||
|
#include <QLineEdit>
|
||||||
|
|
||||||
|
#include <QStringLiteral>
|
||||||
|
|
||||||
|
namespace TTS {
|
||||||
|
|
||||||
|
//TODO: split preview pane to a seprate file.
|
||||||
|
void ConfigWindow::setupUi()
|
||||||
|
{
|
||||||
|
setWindowTitle( "Service Config" );
|
||||||
|
this->setAttribute( Qt::WA_DeleteOnClose );
|
||||||
|
this->setWindowModality( Qt::WindowModal );
|
||||||
|
this->setWindowFlag( Qt::Dialog );
|
||||||
|
|
||||||
|
MainLayout = new QGridLayout( this );
|
||||||
|
|
||||||
|
configPane = new QGroupBox( "Service Config", this );
|
||||||
|
auto * previewPane = new QGroupBox( "Audio Preview", this );
|
||||||
|
|
||||||
|
configPane->setSizePolicy( QSizePolicy::MinimumExpanding, QSizePolicy::MinimumExpanding );
|
||||||
|
previewPane->setSizePolicy( QSizePolicy::Fixed, QSizePolicy::MinimumExpanding );
|
||||||
|
|
||||||
|
configPane->setLayout( new QVBoxLayout() );
|
||||||
|
previewPane->setLayout( new QVBoxLayout() );
|
||||||
|
|
||||||
|
auto * serviceSelectLayout = new QHBoxLayout( nullptr );
|
||||||
|
auto * serviceLabel = new QLabel( "Select service", this );
|
||||||
|
serviceSelector = new QComboBox();
|
||||||
|
serviceSelector->setSizePolicy( QSizePolicy::Expanding, QSizePolicy::Maximum );
|
||||||
|
|
||||||
|
serviceSelectLayout->addWidget( serviceLabel );
|
||||||
|
serviceSelectLayout->addWidget( serviceSelector );
|
||||||
|
|
||||||
|
previewLineEdit = new QLineEdit( this );
|
||||||
|
previewButton = new QPushButton( "Preview", this );
|
||||||
|
|
||||||
|
previewPane->layout()->addWidget( previewLineEdit );
|
||||||
|
previewPane->layout()->addWidget( previewButton );
|
||||||
|
qobject_cast< QVBoxLayout * >( previewPane->layout() )->addStretch();
|
||||||
|
|
||||||
|
buttonBox = new QDialogButtonBox( QDialogButtonBox::Ok | QDialogButtonBox::Cancel | QDialogButtonBox::Help, this );
|
||||||
|
MainLayout->addLayout( serviceSelectLayout, 0, 0, 1, 2 );
|
||||||
|
MainLayout->addWidget( configPane, 1, 0, 1, 1 );
|
||||||
|
MainLayout->addWidget( previewPane, 1, 1, 1, 1 );
|
||||||
|
MainLayout->addWidget( buttonBox, 2, 0, 1, 2 );
|
||||||
|
MainLayout->addWidget(
|
||||||
|
new QLabel(
|
||||||
|
R"(<font color="red">Experimental feature. The default API key may stop working at anytime. Feedback & Coding help are welcomed. </font>)",
|
||||||
|
this ),
|
||||||
|
3,
|
||||||
|
0,
|
||||||
|
1,
|
||||||
|
2 );
|
||||||
|
}
|
||||||
|
|
||||||
|
ConfigWindow::ConfigWindow( QWidget * parent, const QString & configRootPath ):
|
||||||
|
QWidget( parent, Qt::Window ),
|
||||||
|
configRootDir( configRootPath )
|
||||||
|
{
|
||||||
|
configRootDir.mkpath( QStringLiteral( "ctts" ) );
|
||||||
|
configRootDir.cd( QStringLiteral( "ctts" ) );
|
||||||
|
|
||||||
|
|
||||||
|
this->setupUi();
|
||||||
|
|
||||||
|
serviceSelector->addItem( "Azure Text to Speech", QStringLiteral( "azure" ) );
|
||||||
|
serviceSelector->addItem( "Local Command Line", QStringLiteral( "local_command" ) );
|
||||||
|
serviceSelector->addItem( "Dummy", QStringLiteral( "dummy" ) );
|
||||||
|
|
||||||
|
|
||||||
|
this->currentService = get_service_name_from_path( configRootDir );
|
||||||
|
|
||||||
|
if ( auto i = serviceSelector->findData( this->currentService ); i != -1 ) {
|
||||||
|
serviceSelector->setCurrentIndex( i );
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
connect( previewButton, &QPushButton::clicked, this, [ this ] {
|
||||||
|
this->serviceConfigUI->save();
|
||||||
|
|
||||||
|
|
||||||
|
if ( currentService == "azure" ) {
|
||||||
|
previewService.reset( TTS::AzureService::Construct( this->configRootDir ) );
|
||||||
|
}
|
||||||
|
else if ( currentService == "local_command" ) {
|
||||||
|
auto * s = new TTS::LocalCommandService( this->configRootDir );
|
||||||
|
s->loadCommandFromConfigFile(); // TODO:: error unhandled.
|
||||||
|
previewService.reset( s );
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
previewService.reset( new TTS::DummyService() );
|
||||||
|
}
|
||||||
|
|
||||||
|
if ( previewService != nullptr ) {
|
||||||
|
previewService->speak( previewLineEdit->text().toUtf8() );
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
exit( 1 ); // TODO
|
||||||
|
}
|
||||||
|
} );
|
||||||
|
|
||||||
|
|
||||||
|
updateConfigPaneBasedOnCurrentService();
|
||||||
|
|
||||||
|
connect( serviceSelector, &QComboBox::currentIndexChanged, this, [ this ] {
|
||||||
|
updateConfigPaneBasedOnCurrentService();
|
||||||
|
} );
|
||||||
|
|
||||||
|
connect( buttonBox, &QDialogButtonBox::accepted, this, [ this ]() {
|
||||||
|
qDebug() << "accept";
|
||||||
|
this->serviceConfigUI->save();
|
||||||
|
save_service_name_to_path( configRootDir, this->serviceSelector->currentData().toByteArray() );
|
||||||
|
|
||||||
|
emit this->service_changed();
|
||||||
|
this->close();
|
||||||
|
} );
|
||||||
|
|
||||||
|
connect( buttonBox, &QDialogButtonBox::rejected, this, [ this ]() {
|
||||||
|
qDebug() << "rejected";
|
||||||
|
this->close();
|
||||||
|
} );
|
||||||
|
|
||||||
|
connect( buttonBox->button( QDialogButtonBox::Help ), &QPushButton::clicked, this, [ this ]() {
|
||||||
|
qDebug() << "help";
|
||||||
|
} );
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
void ConfigWindow::updateConfigPaneBasedOnCurrentService()
|
||||||
|
{
|
||||||
|
if ( serviceSelector->currentData() == "azure" ) {
|
||||||
|
serviceConfigUI.reset( new TTS::AzureConfigWidget( this, this->configRootDir ) );
|
||||||
|
}
|
||||||
|
else if ( serviceSelector->currentData() == "local_command" ) {
|
||||||
|
serviceConfigUI.reset( new TTS::LocalCommandConfigWidget( this, this->configRootDir ) );
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
serviceConfigUI.reset( new TTS::DummyConfigWidget( this ) );
|
||||||
|
}
|
||||||
|
configPane->layout()->addWidget( serviceConfigUI.get() );
|
||||||
|
}
|
||||||
|
} // namespace TTS
|
42
src/tts/config_window.hh
Normal file
42
src/tts/config_window.hh
Normal file
|
@ -0,0 +1,42 @@
|
||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include "tts/services/azure.hh"
|
||||||
|
#include <QDialogButtonBox>
|
||||||
|
#include <QGridLayout>
|
||||||
|
#include <QGroupBox>
|
||||||
|
#include <QWidget>
|
||||||
|
|
||||||
|
namespace TTS {
|
||||||
|
class ConfigWindow: public QWidget
|
||||||
|
{
|
||||||
|
Q_OBJECT
|
||||||
|
|
||||||
|
public:
|
||||||
|
explicit ConfigWindow( QWidget * parent, const QString & configRootPath );
|
||||||
|
|
||||||
|
signals:
|
||||||
|
void service_changed();
|
||||||
|
|
||||||
|
private:
|
||||||
|
QGridLayout * MainLayout;
|
||||||
|
QGroupBox * configPane;
|
||||||
|
|
||||||
|
QDialogButtonBox * buttonBox;
|
||||||
|
QLineEdit * previewLineEdit;
|
||||||
|
QPushButton * previewButton;
|
||||||
|
|
||||||
|
QString currentService;
|
||||||
|
QDir configRootDir;
|
||||||
|
|
||||||
|
QComboBox * serviceSelector;
|
||||||
|
|
||||||
|
std::unique_ptr< TTS::Service > previewService;
|
||||||
|
std::unique_ptr< TTS::ServiceConfigWidget > serviceConfigUI;
|
||||||
|
|
||||||
|
void setupUi();
|
||||||
|
|
||||||
|
private slots:
|
||||||
|
void updateConfigPaneBasedOnCurrentService();
|
||||||
|
};
|
||||||
|
|
||||||
|
} // namespace TTS
|
1
src/tts/dev_helpers/README.md
Normal file
1
src/tts/dev_helpers/README.md
Normal file
|
@ -0,0 +1 @@
|
||||||
|
Files to test various services.
|
11
src/tts/dev_helpers/voice.hurl
Normal file
11
src/tts/dev_helpers/voice.hurl
Normal file
|
@ -0,0 +1,11 @@
|
||||||
|
POST https://eastus.tts.speech.microsoft.com/cognitiveservices/v1
|
||||||
|
|
||||||
|
Ocp-Apim-Subscription-Key: b9885138792d4403a8ccf1a34553351d
|
||||||
|
X-Microsoft-OutputFormat: audio-16khz-64kbitrate-mono-mp3
|
||||||
|
Content-Type: application/ssml+xml
|
||||||
|
User-Agent: WhatEver
|
||||||
|
<speak version='1.0' xml:lang='en-US'>
|
||||||
|
<voice name='en-US-LunaNeural'>
|
||||||
|
hello world
|
||||||
|
</voice>
|
||||||
|
</speak>
|
3
src/tts/dev_helpers/voicelist.hurl
Normal file
3
src/tts/dev_helpers/voicelist.hurl
Normal file
|
@ -0,0 +1,3 @@
|
||||||
|
GET https://eastus.tts.speech.microsoft.com/cognitiveservices/voices/list
|
||||||
|
|
||||||
|
Ocp-Apim-Subscription-Key: b9885138792d4403a8ccf1a34553351d
|
13
src/tts/error_dialog.hh
Normal file
13
src/tts/error_dialog.hh
Normal file
|
@ -0,0 +1,13 @@
|
||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <QMessageBox>
|
||||||
|
|
||||||
|
namespace TTS {
|
||||||
|
void reportError( const QString & errorString )
|
||||||
|
{
|
||||||
|
QMessageBox msgBox{};
|
||||||
|
msgBox.setText( "Text to speech failed: " % errorString );
|
||||||
|
msgBox.setIcon( QMessageBox::Warning );
|
||||||
|
msgBox.exec();
|
||||||
|
}
|
||||||
|
} // namespace TTS
|
44
src/tts/service.hh
Normal file
44
src/tts/service.hh
Normal file
|
@ -0,0 +1,44 @@
|
||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <QWidget>
|
||||||
|
#include <optional>
|
||||||
|
|
||||||
|
/*
|
||||||
|
*
|
||||||
|
* We want maximum decoupling between different services.
|
||||||
|
*
|
||||||
|
* Things needed by Services should be added to specific services.
|
||||||
|
*
|
||||||
|
* Consider other options before modifying this file.
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
|
||||||
|
namespace TTS {
|
||||||
|
class Service: public QObject
|
||||||
|
{
|
||||||
|
Q_OBJECT
|
||||||
|
public slots:
|
||||||
|
virtual void speak( QUtf8StringView s ) noexcept {};
|
||||||
|
virtual void stop() noexcept {}
|
||||||
|
signals:
|
||||||
|
/// @brief User facing error reporting.
|
||||||
|
/// Service::speak is likely async, error cannot be reported at return position of speak().
|
||||||
|
void errorOccured( const QString & errorString );
|
||||||
|
};
|
||||||
|
|
||||||
|
class ServiceConfigWidget: public QWidget
|
||||||
|
{
|
||||||
|
public:
|
||||||
|
explicit ServiceConfigWidget( QWidget * parent ):
|
||||||
|
QWidget( parent )
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Ask the service to save it's config.
|
||||||
|
/// @return if failed, return a string that contains Error message.
|
||||||
|
virtual std::optional< std::string > save() noexcept
|
||||||
|
{
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
};
|
||||||
|
} // namespace TTS
|
276
src/tts/services/azure.cc
Normal file
276
src/tts/services/azure.cc
Normal file
|
@ -0,0 +1,276 @@
|
||||||
|
#include "tts/services/azure.hh"
|
||||||
|
#include "global_network_access_manager.hh"
|
||||||
|
|
||||||
|
#include <QAudioOutput>
|
||||||
|
#include <QLineEdit>
|
||||||
|
#include <QFormLayout>
|
||||||
|
#include <QLabel>
|
||||||
|
#include <fmt/format.h>
|
||||||
|
|
||||||
|
namespace TTS {
|
||||||
|
|
||||||
|
|
||||||
|
static const char * AzureSaveFileName = "azure.json";
|
||||||
|
|
||||||
|
static const char * hostUrlBody = "tts.speech.microsoft.com/cognitiveservices";
|
||||||
|
|
||||||
|
|
||||||
|
/// @brief this is not visible to service consumers
|
||||||
|
struct AzureConfig
|
||||||
|
{
|
||||||
|
QString apiKey;
|
||||||
|
QString region;
|
||||||
|
QString voiceShortName;
|
||||||
|
|
||||||
|
/// @brief Load file. Create a default one on failure.
|
||||||
|
/// @param configFilePath
|
||||||
|
/// @return Return null if the file absolutely cannot be accessed.
|
||||||
|
[[nodiscard]] static std::optional< AzureConfig > loadFromFile( const QString & configFilePath );
|
||||||
|
[[nodiscard]] static bool saveToFile( const QString & configFilePath, const AzureConfig & );
|
||||||
|
};
|
||||||
|
|
||||||
|
bool AzureService::private_initialize()
|
||||||
|
{
|
||||||
|
auto ret_config = AzureConfig::loadFromFile( azureConfigFile );
|
||||||
|
if ( !ret_config.has_value() ) {
|
||||||
|
throw std::runtime_error( "TODO" );
|
||||||
|
}
|
||||||
|
|
||||||
|
voiceShortName = ret_config->voiceShortName.toStdString();
|
||||||
|
|
||||||
|
request = new QNetworkRequest();
|
||||||
|
request->setUrl( QUrl( QString( QStringLiteral( "https://%1.tts.speech.microsoft.com/cognitiveservices/v1" ) )
|
||||||
|
.arg( ret_config->region ) ) );
|
||||||
|
request->setRawHeader( "User-Agent", "WhatEver" );
|
||||||
|
request->setRawHeader( "Ocp-Apim-Subscription-Key", ret_config->apiKey.toLatin1() );
|
||||||
|
request->setRawHeader( "Content-Type", "application/ssml+xml" );
|
||||||
|
request->setRawHeader( "X-Microsoft-OutputFormat", "ogg-48khz-16bit-mono-opus" );
|
||||||
|
|
||||||
|
player = new QMediaPlayer();
|
||||||
|
|
||||||
|
auto * audioOutput = new QAudioOutput;
|
||||||
|
audioOutput->setVolume( 50 );
|
||||||
|
player->setAudioOutput( audioOutput );
|
||||||
|
|
||||||
|
connect( player, &QMediaPlayer::errorOccurred, this, &AzureService::mediaErrorOccur );
|
||||||
|
connect( player, &QMediaPlayer::mediaStatusChanged, this, &AzureService::mediaStatus );
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
AzureService * AzureService::Construct( const QDir & configRootPath )
|
||||||
|
{
|
||||||
|
auto azure = new AzureService();
|
||||||
|
|
||||||
|
azure->azureConfigFile = configRootPath.filePath( AzureSaveFileName );
|
||||||
|
|
||||||
|
if ( azure->private_initialize() ) {
|
||||||
|
return azure;
|
||||||
|
}
|
||||||
|
return nullptr;
|
||||||
|
};
|
||||||
|
|
||||||
|
void AzureService::speak( QUtf8StringView s ) noexcept
|
||||||
|
{
|
||||||
|
std::string y = fmt::format( R"(<speak version='1.0' xml:lang='en-US'>
|
||||||
|
<voice name='{}'>
|
||||||
|
{}
|
||||||
|
</voice>
|
||||||
|
</speak>)",
|
||||||
|
voiceShortName,
|
||||||
|
s.data() );
|
||||||
|
|
||||||
|
reply = globalNetworkAccessManager->post( *request, y.data() );
|
||||||
|
qDebug() << "azure tries to speak.";
|
||||||
|
|
||||||
|
connect( reply, &QNetworkReply::finished, this, [ this ]() {
|
||||||
|
qDebug() << "azure gets data.";
|
||||||
|
player->setSourceDevice( reply );
|
||||||
|
player->play();
|
||||||
|
} );
|
||||||
|
|
||||||
|
connect( reply, &QNetworkReply::errorOccurred, this, &AzureService::slotError );
|
||||||
|
connect( reply, &QNetworkReply::sslErrors, this, &AzureService::slotSslErrors );
|
||||||
|
}
|
||||||
|
|
||||||
|
void AzureService::stop() noexcept
|
||||||
|
{
|
||||||
|
this->player->stop();
|
||||||
|
}
|
||||||
|
|
||||||
|
AzureService::~AzureService() = default;
|
||||||
|
|
||||||
|
void AzureService::slotError( QNetworkReply::NetworkError e )
|
||||||
|
{
|
||||||
|
qDebug() << e;
|
||||||
|
}
|
||||||
|
|
||||||
|
void AzureService::slotSslErrors()
|
||||||
|
{
|
||||||
|
emit AzureService::errorOccured( "ssl error" );
|
||||||
|
}
|
||||||
|
|
||||||
|
void AzureService::mediaErrorOccur( QMediaPlayer::Error _, const QString & errorString )
|
||||||
|
{
|
||||||
|
emit AzureService::errorOccured( "media error: " + errorString );
|
||||||
|
}
|
||||||
|
|
||||||
|
void AzureService::mediaStatus( QMediaPlayer::MediaStatus status )
|
||||||
|
{
|
||||||
|
qDebug() << "azure media status " << status;
|
||||||
|
}
|
||||||
|
|
||||||
|
std::optional< AzureConfig > AzureConfig::loadFromFile( const QString & configFilePath )
|
||||||
|
{
|
||||||
|
if ( !QFileInfo::exists( configFilePath ) ) {
|
||||||
|
auto defaultConfig = std::make_unique< AzureConfig >();
|
||||||
|
defaultConfig->apiKey = "b9885138792d4403a8ccf1a34553351d";
|
||||||
|
defaultConfig->region = "eastus";
|
||||||
|
defaultConfig->voiceShortName = "en-CA-ClaraNeural";
|
||||||
|
if ( !saveToFile( configFilePath, *defaultConfig ) ) {
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
QFile f( configFilePath );
|
||||||
|
|
||||||
|
if ( !f.open( QFile::ReadOnly ) ) {
|
||||||
|
return {};
|
||||||
|
};
|
||||||
|
|
||||||
|
AzureConfig ret{};
|
||||||
|
|
||||||
|
auto json = QJsonDocument::fromJson( f.readAll() );
|
||||||
|
|
||||||
|
if ( json.isObject() ) {
|
||||||
|
QJsonObject o = json.object();
|
||||||
|
|
||||||
|
if ( const QJsonValue v = o[ "apikey" ]; v.isString() ) {
|
||||||
|
ret.apiKey = v.toString();
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
ret.apiKey = "";
|
||||||
|
}
|
||||||
|
|
||||||
|
if ( const QJsonValue v = o[ "region" ]; v.isString() ) {
|
||||||
|
ret.region = v.toString();
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
ret.region = "";
|
||||||
|
}
|
||||||
|
|
||||||
|
if ( const QJsonValue v = o[ "voiceShortName" ]; v.isString() ) {
|
||||||
|
ret.voiceShortName = v.toString();
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
ret.voiceShortName = "";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return { ret };
|
||||||
|
}
|
||||||
|
|
||||||
|
bool AzureConfig::saveToFile( const QString & configFilePath, const AzureConfig & c )
|
||||||
|
{
|
||||||
|
QJsonDocument doc(
|
||||||
|
QJsonObject( { { "region", c.region }, { "apikey", c.apiKey }, { "voiceShortName", c.voiceShortName } } ) );
|
||||||
|
|
||||||
|
QSaveFile f( configFilePath );
|
||||||
|
f.open( QSaveFile::WriteOnly );
|
||||||
|
f.write( doc.toJson( QJsonDocument::Indented ) );
|
||||||
|
return f.commit();
|
||||||
|
}
|
||||||
|
|
||||||
|
AzureConfigWidget::AzureConfigWidget( QWidget * parent, const QDir & configRootPath ):
|
||||||
|
TTS::ServiceConfigWidget( parent )
|
||||||
|
{
|
||||||
|
azureConfigPath = configRootPath.filePath( AzureSaveFileName );
|
||||||
|
|
||||||
|
auto * form = new QFormLayout( this );
|
||||||
|
|
||||||
|
|
||||||
|
auto config = AzureConfig::loadFromFile( azureConfigPath );
|
||||||
|
|
||||||
|
if ( !config.has_value() ) {
|
||||||
|
throw std::runtime_error( "TODO" );
|
||||||
|
}
|
||||||
|
|
||||||
|
voiceList = new QComboBox( this );
|
||||||
|
|
||||||
|
|
||||||
|
region = new QLineEdit( config->region, this );
|
||||||
|
apiKey = new QLineEdit( config->apiKey, this );
|
||||||
|
this->asyncVoiceListPopulating( config->voiceShortName );
|
||||||
|
|
||||||
|
auto * title = new QLabel( "<b>Azure config</b>", this );
|
||||||
|
|
||||||
|
title->setAlignment( Qt::AlignCenter );
|
||||||
|
form->addRow( title );
|
||||||
|
form->addRow( "Location/Region", region );
|
||||||
|
form->addRow( "API Key", apiKey );
|
||||||
|
form->addRow( "Voice", voiceList );
|
||||||
|
voiceList->setSizePolicy( QSizePolicy::Expanding, QSizePolicy::Maximum );
|
||||||
|
form->setFieldGrowthPolicy( QFormLayout::ExpandingFieldsGrow );
|
||||||
|
apiKey->setMinimumWidth( 400 );
|
||||||
|
|
||||||
|
auto * wrapper = new QVBoxLayout( this );
|
||||||
|
|
||||||
|
wrapper->addLayout( form );
|
||||||
|
wrapper->addStretch();
|
||||||
|
this->setLayout( wrapper );
|
||||||
|
}
|
||||||
|
|
||||||
|
std::optional< std::string > AzureConfigWidget::save() noexcept
|
||||||
|
{
|
||||||
|
|
||||||
|
auto config = std::make_unique< AzureConfig >();
|
||||||
|
config->apiKey = apiKey->text().simplified();
|
||||||
|
config->region = region->text().simplified();
|
||||||
|
config->voiceShortName = voiceList->currentText().simplified();
|
||||||
|
|
||||||
|
|
||||||
|
if ( !AzureConfig::saveToFile( azureConfigPath, *config ) ) {
|
||||||
|
return { "sth is wrong" };
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void AzureConfigWidget::asyncVoiceListPopulating( const QString & autoSelectThisName )
|
||||||
|
{
|
||||||
|
|
||||||
|
voiceListRequest.reset( new QNetworkRequest() );
|
||||||
|
voiceListRequest->setRawHeader( "User-Agent", "WhatEver" );
|
||||||
|
voiceListRequest->setUrl( QUrl( QString( QStringLiteral( "https://%1.%2/voices/list" ) )
|
||||||
|
.arg( this->region->text(), QString::fromUtf8( TTS::hostUrlBody ) ) ) );
|
||||||
|
voiceListRequest->setRawHeader( "Ocp-Apim-Subscription-Key", this->apiKey->text().toLatin1() );
|
||||||
|
|
||||||
|
voiceListReply.reset( globalNetworkAccessManager->get( *voiceListRequest ) );
|
||||||
|
|
||||||
|
connect( voiceListReply.get(), &QNetworkReply::finished, this, [ this, autoSelectThisName ]() {
|
||||||
|
voiceList->clear();
|
||||||
|
auto json = QJsonDocument::fromJson( this->voiceListReply->readAll() );
|
||||||
|
if ( json.isArray() ) {
|
||||||
|
for ( auto && o : json.array() ) {
|
||||||
|
if ( o.isObject() ) {
|
||||||
|
if ( auto r = o.toObject()[ "ShortName" ]; r.isString() ) {
|
||||||
|
if ( auto s = r.toString(); !s.isNull() ) {
|
||||||
|
voiceList->addItem( s );
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if ( auto i = voiceList->findText( autoSelectThisName ); i != -1 ) {
|
||||||
|
voiceList->setCurrentIndex( i );
|
||||||
|
}
|
||||||
|
} );
|
||||||
|
|
||||||
|
connect( voiceListReply.get(), &QNetworkReply::errorOccurred, this, [ this ]( QNetworkReply::NetworkError e ) {
|
||||||
|
qDebug() << "f";
|
||||||
|
this->voiceList->clear();
|
||||||
|
this->voiceList->addItem( "Failed to retrieve voice list: " + QString::number( e ) );
|
||||||
|
} );
|
||||||
|
}
|
||||||
|
} // namespace TTS
|
62
src/tts/services/azure.hh
Normal file
62
src/tts/services/azure.hh
Normal file
|
@ -0,0 +1,62 @@
|
||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include "tts/service.hh"
|
||||||
|
|
||||||
|
#include <QComboBox>
|
||||||
|
#include <QString>
|
||||||
|
#include <QtNetwork>
|
||||||
|
#include <optional>
|
||||||
|
#include <QMediaPlayer>
|
||||||
|
|
||||||
|
namespace TTS {
|
||||||
|
|
||||||
|
|
||||||
|
class AzureService: public TTS::Service
|
||||||
|
{
|
||||||
|
Q_OBJECT
|
||||||
|
|
||||||
|
public:
|
||||||
|
static AzureService * Construct( const QDir & configRootPath );
|
||||||
|
void speak( QUtf8StringView s ) noexcept override;
|
||||||
|
void stop() noexcept override;
|
||||||
|
|
||||||
|
~AzureService() override;
|
||||||
|
|
||||||
|
private:
|
||||||
|
AzureService() = default;
|
||||||
|
bool private_initialize();
|
||||||
|
QNetworkReply * reply;
|
||||||
|
QMediaPlayer * player;
|
||||||
|
QNetworkRequest * request;
|
||||||
|
QString azureConfigFile;
|
||||||
|
std::string voiceShortName;
|
||||||
|
|
||||||
|
private slots:
|
||||||
|
void slotError( QNetworkReply::NetworkError e );
|
||||||
|
void slotSslErrors();
|
||||||
|
|
||||||
|
void mediaErrorOccur( QMediaPlayer::Error error, const QString & errorString );
|
||||||
|
void mediaStatus( QMediaPlayer::MediaStatus status );
|
||||||
|
};
|
||||||
|
|
||||||
|
class AzureConfigWidget: public TTS::ServiceConfigWidget
|
||||||
|
{
|
||||||
|
Q_OBJECT
|
||||||
|
|
||||||
|
public:
|
||||||
|
explicit AzureConfigWidget( QWidget * parent, const QDir & configRootPath );
|
||||||
|
|
||||||
|
std::optional< std::string > save() noexcept override;
|
||||||
|
|
||||||
|
private:
|
||||||
|
QString azureConfigPath;
|
||||||
|
QLineEdit * region;
|
||||||
|
QLineEdit * apiKey;
|
||||||
|
std::unique_ptr< QNetworkRequest > voiceListRequest;
|
||||||
|
std::unique_ptr< QNetworkReply > voiceListReply;
|
||||||
|
|
||||||
|
QComboBox * voiceList;
|
||||||
|
|
||||||
|
void asyncVoiceListPopulating( const QString & autoSelectThisName );
|
||||||
|
};
|
||||||
|
} // namespace TTS
|
43
src/tts/services/dummy.hh
Normal file
43
src/tts/services/dummy.hh
Normal file
|
@ -0,0 +1,43 @@
|
||||||
|
#pragma once
|
||||||
|
#include "tts/service.hh"
|
||||||
|
#include <QLabel>
|
||||||
|
#include <QLayout>
|
||||||
|
#include <QVBoxLayout>
|
||||||
|
|
||||||
|
namespace TTS {
|
||||||
|
class DummyService: public TTS::Service
|
||||||
|
{
|
||||||
|
Q_OBJECT
|
||||||
|
|
||||||
|
public:
|
||||||
|
void speak( QUtf8StringView s ) noexcept override
|
||||||
|
{
|
||||||
|
qDebug() << "dummy speaks" << s;
|
||||||
|
};
|
||||||
|
void stop() noexcept override
|
||||||
|
{
|
||||||
|
qDebug() << "dummy stops";
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
class DummyConfigWidget: public TTS::ServiceConfigWidget
|
||||||
|
{
|
||||||
|
Q_OBJECT
|
||||||
|
|
||||||
|
public:
|
||||||
|
explicit DummyConfigWidget( QWidget * parent ):
|
||||||
|
TTS::ServiceConfigWidget( parent )
|
||||||
|
{
|
||||||
|
this->setLayout( new QVBoxLayout );
|
||||||
|
this->layout()->addWidget(
|
||||||
|
new QLabel( R"(This is a sample service. You are welcome to check the source code and add new services. )",
|
||||||
|
parent ) );
|
||||||
|
}
|
||||||
|
|
||||||
|
std::optional< std::string > save() noexcept override
|
||||||
|
{
|
||||||
|
return {};
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
} // namespace TTS
|
145
src/tts/services/local_command.cc
Normal file
145
src/tts/services/local_command.cc
Normal file
|
@ -0,0 +1,145 @@
|
||||||
|
#include "tts/services/local_command.hh"
|
||||||
|
#include <QFile>
|
||||||
|
#include <QSaveFile>
|
||||||
|
#include <toml++/toml.hpp>
|
||||||
|
|
||||||
|
namespace TTS {
|
||||||
|
|
||||||
|
|
||||||
|
static const char * LocalCommandSaveFileName = "local_command.toml";
|
||||||
|
|
||||||
|
|
||||||
|
namespace {
|
||||||
|
|
||||||
|
|
||||||
|
/// @brief try read cmd or create a default one
|
||||||
|
/// @param path
|
||||||
|
/// @return if true -> QString is wanted string, else errorString. (Poor man's std::expected)
|
||||||
|
std::tuple< bool, QString > getLocalCommandConfigFromFile( const QString & path ) noexcept
|
||||||
|
{
|
||||||
|
if ( !QFileInfo::exists( path ) ) {
|
||||||
|
QSaveFile f( path );
|
||||||
|
f.open( QSaveFile::WriteOnly );
|
||||||
|
|
||||||
|
auto tbl = toml::table{
|
||||||
|
{ "cmd",
|
||||||
|
R"raw(pwsh.exe -Command $(New-Object System.Speech.Synthesis.SpeechSynthesizer).speak('%GDSENTENCE%'))raw" },
|
||||||
|
};
|
||||||
|
|
||||||
|
std::stringstream out;
|
||||||
|
out << tbl;
|
||||||
|
|
||||||
|
f.write( QByteArray::fromStdString( out.str() ) );
|
||||||
|
if ( f.commit() == false ) {
|
||||||
|
throw std::runtime_error( "Cannot write to file." );
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
toml::table tbl;
|
||||||
|
try {
|
||||||
|
tbl = toml::parse_file( path.toStdString() );
|
||||||
|
}
|
||||||
|
catch ( const toml::parse_error & err ) {
|
||||||
|
return { false, err.what() };
|
||||||
|
}
|
||||||
|
|
||||||
|
auto cmd = tbl[ "cmd" ].value< std::string >();
|
||||||
|
|
||||||
|
if ( cmd.has_value() ) {
|
||||||
|
return {
|
||||||
|
true,
|
||||||
|
QString::fromStdString( cmd.value() ),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
return { true, "" };
|
||||||
|
}
|
||||||
|
};
|
||||||
|
} // namespace
|
||||||
|
|
||||||
|
|
||||||
|
LocalCommandService::LocalCommandService( const QDir & configRootPath )
|
||||||
|
{
|
||||||
|
|
||||||
|
this->configFilePath = configRootPath.filePath( LocalCommandSaveFileName );
|
||||||
|
}
|
||||||
|
|
||||||
|
std::optional< std::string > LocalCommandService::loadCommandFromConfigFile()
|
||||||
|
{
|
||||||
|
auto [ status, str ] = getLocalCommandConfigFromFile( this->configFilePath );
|
||||||
|
if ( status == true ) {
|
||||||
|
this->command = str;
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
return { str.toStdString() };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
LocalCommandService::~LocalCommandService()
|
||||||
|
{
|
||||||
|
process->disconnect( this ); // Prevent innocent error at program exit, which is considered as error.
|
||||||
|
}
|
||||||
|
|
||||||
|
void LocalCommandService::speak( QUtf8StringView s ) noexcept
|
||||||
|
{
|
||||||
|
process.reset( new QProcess( this ) );
|
||||||
|
QString cmd_to_be_executed = command;
|
||||||
|
cmd_to_be_executed.replace( "%GDSENTENCE%", s.toString() );
|
||||||
|
qDebug() << "local command speaking: " << cmd_to_be_executed;
|
||||||
|
process->startCommand( cmd_to_be_executed );
|
||||||
|
// TODO: handle errors of processes.
|
||||||
|
connect( process.get(), &QProcess::errorOccurred, this, [ this ]( QProcess::ProcessError error ) {
|
||||||
|
emit TTS::Service::errorOccured( "Process failed to execute due to QProcess::ProcessError" );
|
||||||
|
} );
|
||||||
|
}
|
||||||
|
|
||||||
|
void LocalCommandService::stop() noexcept
|
||||||
|
{
|
||||||
|
process.reset(); // deleter of QProcess also kills the process
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
LocalCommandConfigWidget::LocalCommandConfigWidget( QWidget * parent, const QDir & configRootPath ):
|
||||||
|
TTS::ServiceConfigWidget( parent )
|
||||||
|
{
|
||||||
|
auto * layout = new QVBoxLayout( this );
|
||||||
|
layout->addWidget( new QLabel( R"(Set command)", parent ) );
|
||||||
|
|
||||||
|
commandLineEdit = new QLineEdit( this );
|
||||||
|
layout->addWidget( commandLineEdit );
|
||||||
|
layout->addStretch();
|
||||||
|
|
||||||
|
this->configFilePath = configRootPath.filePath( LocalCommandSaveFileName );
|
||||||
|
|
||||||
|
auto [ status, str ] = getLocalCommandConfigFromFile( this->configFilePath );
|
||||||
|
|
||||||
|
if ( status == true ) {
|
||||||
|
commandLineEdit->setText( str );
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
// do nothing.
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
std::optional< std::string > LocalCommandConfigWidget::save() noexcept
|
||||||
|
{
|
||||||
|
|
||||||
|
QSaveFile f( this->configFilePath );
|
||||||
|
f.open( QSaveFile::WriteOnly );
|
||||||
|
|
||||||
|
auto tbl = toml::table{
|
||||||
|
{ "cmd", commandLineEdit->text().simplified().toStdString() },
|
||||||
|
};
|
||||||
|
|
||||||
|
std::stringstream out;
|
||||||
|
out << tbl;
|
||||||
|
|
||||||
|
f.write( QByteArray::fromStdString( out.str() ) );
|
||||||
|
if ( f.commit() == false ) {
|
||||||
|
return { "Cannot write to file." };
|
||||||
|
}
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace TTS
|
51
src/tts/services/local_command.hh
Normal file
51
src/tts/services/local_command.hh
Normal file
|
@ -0,0 +1,51 @@
|
||||||
|
#pragma once
|
||||||
|
#include "tts/service.hh"
|
||||||
|
#include <QLabel>
|
||||||
|
#include <QLayout>
|
||||||
|
#include <QVBoxLayout>
|
||||||
|
#include <QDir>
|
||||||
|
#include <QProcess>
|
||||||
|
#include <QLineEdit>
|
||||||
|
|
||||||
|
namespace TTS {
|
||||||
|
|
||||||
|
|
||||||
|
class LocalCommandService: public TTS::Service
|
||||||
|
{
|
||||||
|
Q_OBJECT
|
||||||
|
|
||||||
|
public:
|
||||||
|
explicit LocalCommandService( const QDir & configRootPath );
|
||||||
|
|
||||||
|
/// @brief
|
||||||
|
/// @return failure message if any
|
||||||
|
std::optional< std::string > loadCommandFromConfigFile();
|
||||||
|
~LocalCommandService();
|
||||||
|
|
||||||
|
void speak( QUtf8StringView s ) noexcept override;
|
||||||
|
void stop() noexcept override;
|
||||||
|
signals:
|
||||||
|
|
||||||
|
|
||||||
|
private:
|
||||||
|
QString configFilePath;
|
||||||
|
QString command;
|
||||||
|
std::unique_ptr< QProcess > process;
|
||||||
|
};
|
||||||
|
|
||||||
|
class LocalCommandConfigWidget: public TTS::ServiceConfigWidget
|
||||||
|
{
|
||||||
|
Q_OBJECT
|
||||||
|
|
||||||
|
public:
|
||||||
|
explicit LocalCommandConfigWidget( QWidget * parent, const QDir & configRootPath );
|
||||||
|
|
||||||
|
std::optional< std::string > save() noexcept override;
|
||||||
|
|
||||||
|
private:
|
||||||
|
QLineEdit * commandLineEdit;
|
||||||
|
QString configFilePath; // Don't use replace on this
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
|
} // namespace TTS
|
47
src/tts/single_service_controller.cc
Normal file
47
src/tts/single_service_controller.cc
Normal file
|
@ -0,0 +1,47 @@
|
||||||
|
#include "tts/single_service_controller.hh"
|
||||||
|
#include "config_file_main.hh"
|
||||||
|
#include "tts/services/azure.hh"
|
||||||
|
#include "tts/services/dummy.hh"
|
||||||
|
#include "tts/services/local_command.hh"
|
||||||
|
#include "tts/error_dialog.hh"
|
||||||
|
|
||||||
|
namespace TTS {
|
||||||
|
|
||||||
|
SingleServiceController::SingleServiceController( const QString & configPath )
|
||||||
|
{
|
||||||
|
configRootDir = QDir( configPath );
|
||||||
|
configRootDir.mkpath( QStringLiteral( "ctts" ) );
|
||||||
|
configRootDir.cd( QStringLiteral( "ctts" ) );
|
||||||
|
currentService.reset();
|
||||||
|
}
|
||||||
|
|
||||||
|
void SingleServiceController::reload()
|
||||||
|
{
|
||||||
|
QString service_name = get_service_name_from_path( this->configRootDir );
|
||||||
|
if ( service_name == "azure" ) {
|
||||||
|
currentService.reset( TTS::AzureService::Construct( this->configRootDir ) );
|
||||||
|
}
|
||||||
|
else if ( service_name == "local_command" ) {
|
||||||
|
auto * s = new TTS::LocalCommandService( this->configRootDir );
|
||||||
|
s->loadCommandFromConfigFile(); // TODO:: error unhandled.
|
||||||
|
currentService.reset( s );
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
currentService.reset( new TTS::DummyService() );
|
||||||
|
}
|
||||||
|
|
||||||
|
connect( currentService.get(), &Service::errorOccured, []( const QString & errorString ) {
|
||||||
|
TTS::reportError( errorString );
|
||||||
|
} );
|
||||||
|
}
|
||||||
|
|
||||||
|
void SingleServiceController::speak( const QString & text )
|
||||||
|
{
|
||||||
|
|
||||||
|
if ( !currentService ) {
|
||||||
|
this->reload();
|
||||||
|
}
|
||||||
|
currentService->speak( text.toStdString() );
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace TTS
|
26
src/tts/single_service_controller.hh
Normal file
26
src/tts/single_service_controller.hh
Normal file
|
@ -0,0 +1,26 @@
|
||||||
|
#pragma once
|
||||||
|
#include "tts/service.hh"
|
||||||
|
#include <QDir>
|
||||||
|
|
||||||
|
namespace TTS {
|
||||||
|
|
||||||
|
/// @brief Manage the life time of one single service, which can be reloaded to other services.
|
||||||
|
class SingleServiceController: public QObject
|
||||||
|
{
|
||||||
|
Q_OBJECT
|
||||||
|
|
||||||
|
public:
|
||||||
|
explicit SingleServiceController( const QString & configPath );
|
||||||
|
|
||||||
|
public slots:
|
||||||
|
void speak( const QString & text );
|
||||||
|
void reload(); // TODO handle error
|
||||||
|
|
||||||
|
|
||||||
|
private:
|
||||||
|
QDir configRootDir;
|
||||||
|
std::unique_ptr< Service > currentService;
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
|
} // namespace TTS
|
|
@ -1475,6 +1475,7 @@ void ArticleView::contextMenuRequested( QPoint const & pos )
|
||||||
QAction * openImageAction = nullptr;
|
QAction * openImageAction = nullptr;
|
||||||
QAction * saveSoundAction = nullptr;
|
QAction * saveSoundAction = nullptr;
|
||||||
QAction * saveBookmark = nullptr;
|
QAction * saveBookmark = nullptr;
|
||||||
|
QAction * prounceSelectionAction = nullptr;
|
||||||
|
|
||||||
#if ( QT_VERSION < QT_VERSION_CHECK( 6, 0, 0 ) )
|
#if ( QT_VERSION < QT_VERSION_CHECK( 6, 0, 0 ) )
|
||||||
const QWebEngineContextMenuData * menuData = &( r->contextMenuData() );
|
const QWebEngineContextMenuData * menuData = &( r->contextMenuData() );
|
||||||
|
@ -1544,6 +1545,9 @@ void ArticleView::contextMenuRequested( QPoint const & pos )
|
||||||
|
|
||||||
sendWordToInputLineAction = new QAction( tr( "Send \"%1\" to input line" ).arg( text ), &menu );
|
sendWordToInputLineAction = new QAction( tr( "Send \"%1\" to input line" ).arg( text ), &menu );
|
||||||
menu.addAction( sendWordToInputLineAction );
|
menu.addAction( sendWordToInputLineAction );
|
||||||
|
|
||||||
|
prounceSelectionAction = new QAction( "Speak selection", &menu );
|
||||||
|
menu.addAction( prounceSelectionAction );
|
||||||
}
|
}
|
||||||
|
|
||||||
addWordToHistoryAction = new QAction( tr( "&Add \"%1\" to history" ).arg( text ), &menu );
|
addWordToHistoryAction = new QAction( tr( "&Add \"%1\" to history" ).arg( text ), &menu );
|
||||||
|
@ -1679,6 +1683,9 @@ void ArticleView::contextMenuRequested( QPoint const & pos )
|
||||||
emit showDefinitionInNewTab( selectedText, getGroup( webview->url() ), getCurrentArticle(), Contexts() );
|
emit showDefinitionInNewTab( selectedText, getGroup( webview->url() ), getCurrentArticle(), Contexts() );
|
||||||
else if ( !popupView && result == lookupSelectionNewTabGr && currentGroupId )
|
else if ( !popupView && result == lookupSelectionNewTabGr && currentGroupId )
|
||||||
emit showDefinitionInNewTab( selectedText, currentGroupId, QString(), Contexts() );
|
emit showDefinitionInNewTab( selectedText, currentGroupId, QString(), Contexts() );
|
||||||
|
else if ( !popupView && result == prounceSelectionAction ) {
|
||||||
|
emit prounceSelection( selectedText );
|
||||||
|
}
|
||||||
else if ( result == saveImageAction || result == saveSoundAction ) {
|
else if ( result == saveImageAction || result == saveSoundAction ) {
|
||||||
QUrl url = ( result == saveImageAction ) ? imageUrl : targetUrl;
|
QUrl url = ( result == saveImageAction ) ? imageUrl : targetUrl;
|
||||||
QString savePath;
|
QString savePath;
|
||||||
|
|
|
@ -268,6 +268,8 @@ signals:
|
||||||
QString const & fromArticle,
|
QString const & fromArticle,
|
||||||
Contexts const & contexts );
|
Contexts const & contexts );
|
||||||
|
|
||||||
|
void prounceSelection( const QString & selectionText );
|
||||||
|
|
||||||
/// Put translated word into history
|
/// Put translated word into history
|
||||||
void sendWordToHistory( QString const & word );
|
void sendWordToHistory( QString const & word );
|
||||||
|
|
||||||
|
|
|
@ -61,6 +61,9 @@
|
||||||
#include <windows.h>
|
#include <windows.h>
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
|
#include "tts/config_window.hh"
|
||||||
|
|
||||||
|
|
||||||
#include <QWebEngineSettings>
|
#include <QWebEngineSettings>
|
||||||
#include <QProxyStyle>
|
#include <QProxyStyle>
|
||||||
|
|
||||||
|
@ -170,7 +173,8 @@ MainWindow::MainWindow( Config::Class & cfg_ ):
|
||||||
ftsIndexing( dictionaries ),
|
ftsIndexing( dictionaries ),
|
||||||
ftsDlg( nullptr ),
|
ftsDlg( nullptr ),
|
||||||
starIcon( ":/icons/star.svg" ),
|
starIcon( ":/icons/star.svg" ),
|
||||||
blueStarIcon( ":/icons/star_blue.svg" )
|
blueStarIcon( ":/icons/star_blue.svg" ),
|
||||||
|
ttsServiceController( new TTS::SingleServiceController( Config::getConfigDir() ) )
|
||||||
{
|
{
|
||||||
if ( QThreadPool::globalInstance()->maxThreadCount() < MIN_THREAD_COUNT )
|
if ( QThreadPool::globalInstance()->maxThreadCount() < MIN_THREAD_COUNT )
|
||||||
QThreadPool::globalInstance()->setMaxThreadCount( MIN_THREAD_COUNT );
|
QThreadPool::globalInstance()->setMaxThreadCount( MIN_THREAD_COUNT );
|
||||||
|
@ -639,6 +643,16 @@ MainWindow::MainWindow( Config::Class & cfg_ ):
|
||||||
|
|
||||||
connect( ui.dictionaries, &QAction::triggered, this, &MainWindow::editDictionaries );
|
connect( ui.dictionaries, &QAction::triggered, this, &MainWindow::editDictionaries );
|
||||||
|
|
||||||
|
connect( ui.menuTextToSpeech, &QAction::triggered, this, [ this ] {
|
||||||
|
auto * ttsConfigWindow = new TTS::ConfigWindow( this, Config::getConfigDir() );
|
||||||
|
ttsConfigWindow->show();
|
||||||
|
connect( ttsConfigWindow,
|
||||||
|
&TTS::ConfigWindow::service_changed,
|
||||||
|
this->ttsServiceController.get(),
|
||||||
|
&TTS::SingleServiceController::reload );
|
||||||
|
} );
|
||||||
|
|
||||||
|
|
||||||
connect( ui.preferences, &QAction::triggered, this, &MainWindow::editPreferences );
|
connect( ui.preferences, &QAction::triggered, this, &MainWindow::editPreferences );
|
||||||
|
|
||||||
connect( ui.visitHomepage, &QAction::triggered, this, &MainWindow::visitHomepage );
|
connect( ui.visitHomepage, &QAction::triggered, this, &MainWindow::visitHomepage );
|
||||||
|
@ -1779,6 +1793,8 @@ ArticleView * MainWindow::createNewTab( bool switchToIt, QString const & name )
|
||||||
|
|
||||||
connect( view, &ArticleView::showDefinitionInNewTab, this, &MainWindow::showDefinitionInNewTab );
|
connect( view, &ArticleView::showDefinitionInNewTab, this, &MainWindow::showDefinitionInNewTab );
|
||||||
|
|
||||||
|
connect( view, &ArticleView::prounceSelection, ttsServiceController.get(), &TTS::SingleServiceController::speak );
|
||||||
|
|
||||||
connect( view, &ArticleView::typingEvent, this, &MainWindow::typingEvent );
|
connect( view, &ArticleView::typingEvent, this, &MainWindow::typingEvent );
|
||||||
|
|
||||||
connect( view, &ArticleView::activeArticleChanged, this, &MainWindow::activeArticleChanged );
|
connect( view, &ArticleView::activeArticleChanged, this, &MainWindow::activeArticleChanged );
|
||||||
|
|
|
@ -29,6 +29,7 @@
|
||||||
#include "dictheadwords.hh"
|
#include "dictheadwords.hh"
|
||||||
#include "fulltextsearch.hh"
|
#include "fulltextsearch.hh"
|
||||||
#include "base_type.hh"
|
#include "base_type.hh"
|
||||||
|
#include "tts/single_service_controller.hh"
|
||||||
|
|
||||||
#include "hotkeywrapper.hh"
|
#include "hotkeywrapper.hh"
|
||||||
#include "resourceschemehandler.hh"
|
#include "resourceschemehandler.hh"
|
||||||
|
@ -142,6 +143,8 @@ private:
|
||||||
// in a separate thread
|
// in a separate thread
|
||||||
AudioPlayerFactory audioPlayerFactory;
|
AudioPlayerFactory audioPlayerFactory;
|
||||||
|
|
||||||
|
QScopedPointer< TTS::SingleServiceController > ttsServiceController;
|
||||||
|
|
||||||
//current active translateLine;
|
//current active translateLine;
|
||||||
QLineEdit * translateLine;
|
QLineEdit * translateLine;
|
||||||
|
|
||||||
|
|
|
@ -70,7 +70,7 @@
|
||||||
<x>0</x>
|
<x>0</x>
|
||||||
<y>0</y>
|
<y>0</y>
|
||||||
<width>653</width>
|
<width>653</width>
|
||||||
<height>21</height>
|
<height>22</height>
|
||||||
</rect>
|
</rect>
|
||||||
</property>
|
</property>
|
||||||
<widget class="QMenu" name="menuFile">
|
<widget class="QMenu" name="menuFile">
|
||||||
|
@ -95,6 +95,7 @@
|
||||||
<string>&Edit</string>
|
<string>&Edit</string>
|
||||||
</property>
|
</property>
|
||||||
<addaction name="dictionaries"/>
|
<addaction name="dictionaries"/>
|
||||||
|
<addaction name="menuTextToSpeech"/>
|
||||||
<addaction name="preferences"/>
|
<addaction name="preferences"/>
|
||||||
</widget>
|
</widget>
|
||||||
<widget class="QMenu" name="menu_Help">
|
<widget class="QMenu" name="menu_Help">
|
||||||
|
@ -581,6 +582,11 @@
|
||||||
<string>Export to list</string>
|
<string>Export to list</string>
|
||||||
</property>
|
</property>
|
||||||
</action>
|
</action>
|
||||||
|
<action name="menuTextToSpeech">
|
||||||
|
<property name="text">
|
||||||
|
<string>Text to Speech</string>
|
||||||
|
</property>
|
||||||
|
</action>
|
||||||
</widget>
|
</widget>
|
||||||
<customwidgets>
|
<customwidgets>
|
||||||
<customwidget>
|
<customwidget>
|
||||||
|
|
Loading…
Reference in a new issue