Skip to content

Commit

Permalink
0
Browse files Browse the repository at this point in the history
  • Loading branch information
shenlebantongying committed Sep 20, 2024
1 parent 2b98b50 commit ce5b9c4
Show file tree
Hide file tree
Showing 24 changed files with 1,059 additions and 4 deletions.
8 changes: 6 additions & 2 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ GoldenDict.xcodeproj/
*.suo
*.vcxproj.user
/.idea
/.vs
.vs
/.vscode
/.qtc_clangd

Expand All @@ -55,4 +55,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 )
54 changes: 54 additions & 0 deletions src/tts/README.md
Original file line number Diff line number Diff line change
@@ -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 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
153 changes: 153 additions & 0 deletions src/tts/config_window.cc
Original file line number Diff line number Diff line change
@@ -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 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;

std::unique_ptr< TTS::Service > previewService;
std::unique_ptr< 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
13 changes: 13 additions & 0 deletions src/tts/error_dialog.hh
Original file line number Diff line number Diff line change
@@ -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
Loading

0 comments on commit ce5b9c4

Please sign in to comment.