Skip to content

Commit

Permalink
0
Browse files Browse the repository at this point in the history
  • Loading branch information
shenlebantongying committed Jul 13, 2024
1 parent 262c3c1 commit 8ef6933
Show file tree
Hide file tree
Showing 21 changed files with 840 additions and 4 deletions.
6 changes: 5 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -57,4 +57,8 @@ GoldenDict_resource.rc
*.TMP
*.orig

node_modules
node_modules

# tts testing files
*.ogg
*.mp3
4 changes: 4 additions & 0 deletions src/global_network_access_manager.hh
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
#pragma once

#include <QNetworkAccessManager>
Q_APPLICATION_STATIC(QNetworkAccessManager, globalNetworkAccessManager)
59 changes: 59 additions & 0 deletions src/tts/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
# Cloud TTS

## Add a new service checklist

* Read to `service.h`.
* Implement `Service::speak`.
* Implement `Service::stop`.
* Implement `ServiceConfigWidget`+`ServiceConfigWidget::save`.
* 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.

(Conversely, old services won't be obstacles of modifying currently maintained services.)

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 file serialization and Service state mutating will not happen in parallel or successively.
* Service construct only based on config file. One single deterministic routine.

(1) will neither mutate nor access (3).

construct (3) only according to (2).

(1) only write to (2).

### Object management

* Service construction will be done on the service consumer side to avoid sharing constructor.
* Service can be cast to `Service`, which only has `speak/stop` and destructor. The service consumer should not care
anything else after construction.
* Services don't share global config, don't share config mechanisms

### 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.

## Rational

* Services are different.
* Testing services are hard (cloud tts usually needs an account).
* Abstracting seemingly similar but different things will eventually blunder.
* Do not assume services have any similarity other than the fact they may `speak`.
* Construction materials needed by services are different.
* 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.
35 changes: 35 additions & 0 deletions src/tts/config_file_main.cc
Original file line number Diff line number Diff line change
@@ -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 changes: 7 additions & 0 deletions src/tts/config_file_main.hh
Original file line number Diff line number Diff line change
@@ -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
157 changes: 157 additions & 0 deletions src/tts/config_window.cc
Original file line number Diff line number Diff line change
@@ -0,0 +1,157 @@
#include "tts/config_window.hh"
#include "tts/services/azure.hh"
#include "tts/services/dummy.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( "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( Azure::Service::Construct( this->configRootDir ) );
}
else {
previewService.reset( new dummy::Service() );
}

if ( previewService != nullptr ) {
auto _ = 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 Azure::ConfigWidget( this, this->configRootDir ) );
}
else {
serviceConfigUI.reset( new dummy::ConfigWidget( this ) );
}
configPane->layout()->addWidget( serviceConfigUI.get() );
}
} // namespace TTS
42 changes: 42 additions & 0 deletions src/tts/config_window.hh
Original file line number Diff line number Diff line change
@@ -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;

QScopedPointer< TTS::Service > previewService;
QScopedPointer< TTS::ServiceConfigWidget > serviceConfigUI;

void setupUi();

private slots:
void updateConfigPaneBasedOnCurrentService();
};

} // namespace TTS
1 change: 1 addition & 0 deletions src/tts/dev_helpers/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Files to test various services.
11 changes: 11 additions & 0 deletions src/tts/dev_helpers/voice.hurl
Original file line number Diff line number Diff line change
@@ -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 changes: 3 additions & 0 deletions src/tts/dev_helpers/voicelist.hurl
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
GET https://eastus.tts.speech.microsoft.com/cognitiveservices/voices/list

Ocp-Apim-Subscription-Key: b9885138792d4403a8ccf1a34553351d
49 changes: 49 additions & 0 deletions src/tts/service.hh
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
#pragma once

#include <QWidget>
#include <optional>

/*
*
* Do not add anything new to this header to ensure maximum decouping between different services.
*
* Things needed by Services, should be added to specific services.
*
* If something is needed by multiple services,
* it should be implemented as a component that
* can be plugged into needed services.
*
* Consider other options before modifying this file.
*
*/

namespace TTS {
class Service: public QObject
{
Q_OBJECT
public slots:
///
/// @return If failed, return a string that contains Error message.
[[nodiscard]] virtual std::optional< std::string > speak( QUtf8StringView s ) noexcept
{
return {};
}
virtual void stop() noexcept {} // TODO: does here need error handling?
};

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
Loading

0 comments on commit 8ef6933

Please sign in to comment.