Skip to content

Commit 8e41da4

Browse files
0
1 parent ea4f45a commit 8e41da4

24 files changed

+1059
-4
lines changed

.gitignore

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,7 @@ GoldenDict.xcodeproj/
3333
*.suo
3434
*.vcxproj.user
3535
/.idea
36-
/.vs
36+
.vs
3737
/.vscode
3838
/.qtc_clangd
3939

@@ -55,4 +55,8 @@ GoldenDict_resource.rc
5555
*.TMP
5656
*.orig
5757

58-
node_modules
58+
node_modules
59+
60+
# tts testing files
61+
*.ogg
62+
*.mp3
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
#pragma once
2+
3+
#include <QNetworkAccessManager>
4+
Q_APPLICATION_STATIC( QNetworkAccessManager, globalNetworkAccessManager )

src/tts/README.md

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
# Cloud TTS
2+
3+
## Add a new service checklist
4+
5+
* Read `service.h`.
6+
* Implement `Service::speak`.
7+
* Implement `Service::stop`.
8+
* Implement `ServiceConfigWidget`, which will be embedded in `ConfigWindow`.
9+
* Add the `Service` to `ServiceController`.
10+
* Add the `ServiceConfigWidget` to `ConfigWindow`.
11+
* DONE.
12+
13+
## Design Goals
14+
15+
Allow modifying / evolving any one of the services arbitrarily without incurring the need to touch another.
16+
17+
Avoid almost all temptation to do 💩 abstraction 💩 unless absolutely necessary.
18+
19+
## Code
20+
21+
### Config
22+
23+
```
24+
(1) Service ConfigWidet --write--> (2) Service's config file --create--> (3) Live Service Object
25+
```
26+
27+
* Config Serialization+Saving and Service state mutating will not happen in parallel or successively.
28+
* (1) will neither mutate nor access (3).
29+
* construct (3) only according to (2).
30+
31+
### Object management
32+
33+
* Service construction will be done on the service consumer side
34+
* Service can be cast to `Service`, which only has `speak/stop` and destructor.
35+
* The service consumer should not care
36+
anything else after construction.
37+
38+
### Config Window
39+
40+
Similar to KDE's Settings module (KCM).
41+
Every service simply provides a config widget on its own, and the config window simply loads the Widget.
42+
43+
### No exception
44+
45+
* Handle errors religiously and immediately, and report to users if user attention/action is required.
46+
47+
## Rational
48+
49+
* Services are different and testing them is hard (cloud tts usually needs an account).
50+
* Do not assume services have any similarity other than the fact they may `speak`.
51+
* Services on earth are limited, thus the boilerplate caused by fewer useless abstractions is also limited.
52+
* The service consumer will use services incredibly and insanely creative in the future.
53+
* Maintaining two code paths of object creation & mutating is a waste of time.
54+
* Just save config to disk, and construct objects according to what's in the disk.

src/tts/config_file_main.cc

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
#include "config_file_main.hh"
2+
3+
#include <QFileInfo>
4+
#include <QSaveFile>
5+
6+
7+
namespace TTS {
8+
9+
auto current_service_txt = "current_service.txt";
10+
11+
QString get_service_name_from_path( const QDir & configPath )
12+
{
13+
qDebug() << configPath;
14+
if ( !QFileInfo::exists( configPath.absoluteFilePath( current_service_txt ) ) ) {
15+
save_service_name_to_path( configPath, "azure" );
16+
}
17+
QFile f( configPath.filePath( current_service_txt ) );
18+
if ( !f.open( QFile::ReadOnly ) ) {
19+
throw std::runtime_error( "cannot open service name" ); // TODO
20+
}
21+
QString ret = f.readAll();
22+
f.close();
23+
return ret;
24+
}
25+
26+
void save_service_name_to_path( const QDir & configPath, QUtf8StringView serviceName )
27+
{
28+
QSaveFile f( configPath.absoluteFilePath( current_service_txt ) );
29+
if ( !f.open( QFile::WriteOnly ) ) {
30+
throw std::runtime_error( "Cannot write service name" );
31+
}
32+
f.write( serviceName.data(), serviceName.length() );
33+
f.commit();
34+
};
35+
} // namespace TTS

src/tts/config_file_main.hh

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
#pragma once
2+
#include <QDir>
3+
4+
namespace TTS {
5+
QString get_service_name_from_path( const QDir & configRootPath );
6+
void save_service_name_to_path( const QDir & configPath, QUtf8StringView serviceName );
7+
} // namespace TTS

src/tts/config_window.cc

Lines changed: 153 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,153 @@
1+
#include "tts/config_window.hh"
2+
#include "tts/services/azure.hh"
3+
#include "tts/services/dummy.hh"
4+
#include "tts/services/local_command.hh"
5+
#include "tts/config_file_main.hh"
6+
7+
#include <QDialogButtonBox>
8+
#include <QGridLayout>
9+
#include <QGroupBox>
10+
#include <QLabel>
11+
#include <QPushButton>
12+
#include <QLineEdit>
13+
14+
#include <QStringLiteral>
15+
16+
namespace TTS {
17+
18+
//TODO: split preview pane to a seprate file.
19+
void ConfigWindow::setupUi()
20+
{
21+
setWindowTitle( "Service Config" );
22+
this->setAttribute( Qt::WA_DeleteOnClose );
23+
this->setWindowModality( Qt::WindowModal );
24+
this->setWindowFlag( Qt::Dialog );
25+
26+
MainLayout = new QGridLayout( this );
27+
28+
configPane = new QGroupBox( "Service Config", this );
29+
auto * previewPane = new QGroupBox( "Audio Preview", this );
30+
31+
configPane->setSizePolicy( QSizePolicy::MinimumExpanding, QSizePolicy::MinimumExpanding );
32+
previewPane->setSizePolicy( QSizePolicy::Fixed, QSizePolicy::MinimumExpanding );
33+
34+
configPane->setLayout( new QVBoxLayout() );
35+
previewPane->setLayout( new QVBoxLayout() );
36+
37+
auto * serviceSelectLayout = new QHBoxLayout( nullptr );
38+
auto * serviceLabel = new QLabel( "Select service", this );
39+
serviceSelector = new QComboBox();
40+
serviceSelector->setSizePolicy( QSizePolicy::Expanding, QSizePolicy::Maximum );
41+
42+
serviceSelectLayout->addWidget( serviceLabel );
43+
serviceSelectLayout->addWidget( serviceSelector );
44+
45+
previewLineEdit = new QLineEdit( this );
46+
previewButton = new QPushButton( "Preview", this );
47+
48+
previewPane->layout()->addWidget( previewLineEdit );
49+
previewPane->layout()->addWidget( previewButton );
50+
qobject_cast< QVBoxLayout * >( previewPane->layout() )->addStretch();
51+
52+
buttonBox = new QDialogButtonBox( QDialogButtonBox::Ok | QDialogButtonBox::Cancel | QDialogButtonBox::Help, this );
53+
MainLayout->addLayout( serviceSelectLayout, 0, 0, 1, 2 );
54+
MainLayout->addWidget( configPane, 1, 0, 1, 1 );
55+
MainLayout->addWidget( previewPane, 1, 1, 1, 1 );
56+
MainLayout->addWidget( buttonBox, 2, 0, 1, 2 );
57+
MainLayout->addWidget(
58+
new QLabel(
59+
R"(<font color="red">Experimental feature. The default API key may stop working at anytime. Feedback & Coding help are welcomed. </font>)",
60+
this ),
61+
3,
62+
0,
63+
1,
64+
2 );
65+
}
66+
67+
ConfigWindow::ConfigWindow( QWidget * parent, const QString & configRootPath ):
68+
QWidget( parent, Qt::Window ),
69+
configRootDir( configRootPath )
70+
{
71+
configRootDir.mkpath( QStringLiteral( "ctts" ) );
72+
configRootDir.cd( QStringLiteral( "ctts" ) );
73+
74+
75+
this->setupUi();
76+
77+
serviceSelector->addItem( "Azure Text to Speech", QStringLiteral( "azure" ) );
78+
serviceSelector->addItem( "Local Command Line", QStringLiteral( "local_command" ) );
79+
serviceSelector->addItem( "Dummy", QStringLiteral( "dummy" ) );
80+
81+
82+
this->currentService = get_service_name_from_path( configRootDir );
83+
84+
if ( auto i = serviceSelector->findData( this->currentService ); i != -1 ) {
85+
serviceSelector->setCurrentIndex( i );
86+
}
87+
88+
89+
connect( previewButton, &QPushButton::clicked, this, [ this ] {
90+
this->serviceConfigUI->save();
91+
92+
93+
if ( currentService == "azure" ) {
94+
previewService.reset( TTS::AzureService::Construct( this->configRootDir ) );
95+
}
96+
else if ( currentService == "local_command" ) {
97+
auto * s = new TTS::LocalCommandService( this->configRootDir );
98+
s->loadCommandFromConfigFile(); // TODO:: error unhandled.
99+
previewService.reset( s );
100+
}
101+
else {
102+
previewService.reset( new TTS::DummyService() );
103+
}
104+
105+
if ( previewService != nullptr ) {
106+
previewService->speak( previewLineEdit->text().toUtf8() );
107+
}
108+
else {
109+
exit( 1 ); // TODO
110+
}
111+
} );
112+
113+
114+
updateConfigPaneBasedOnCurrentService();
115+
116+
connect( serviceSelector, &QComboBox::currentIndexChanged, this, [ this ] {
117+
updateConfigPaneBasedOnCurrentService();
118+
} );
119+
120+
connect( buttonBox, &QDialogButtonBox::accepted, this, [ this ]() {
121+
qDebug() << "accept";
122+
this->serviceConfigUI->save();
123+
save_service_name_to_path( configRootDir, this->serviceSelector->currentData().toByteArray() );
124+
125+
emit this->service_changed();
126+
this->close();
127+
} );
128+
129+
connect( buttonBox, &QDialogButtonBox::rejected, this, [ this ]() {
130+
qDebug() << "rejected";
131+
this->close();
132+
} );
133+
134+
connect( buttonBox->button( QDialogButtonBox::Help ), &QPushButton::clicked, this, [ this ]() {
135+
qDebug() << "help";
136+
} );
137+
}
138+
139+
140+
void ConfigWindow::updateConfigPaneBasedOnCurrentService()
141+
{
142+
if ( serviceSelector->currentData() == "azure" ) {
143+
serviceConfigUI.reset( new TTS::AzureConfigWidget( this, this->configRootDir ) );
144+
}
145+
else if ( serviceSelector->currentData() == "local_command" ) {
146+
serviceConfigUI.reset( new TTS::LocalCommandConfigWidget( this, this->configRootDir ) );
147+
}
148+
else {
149+
serviceConfigUI.reset( new TTS::DummyConfigWidget( this ) );
150+
}
151+
configPane->layout()->addWidget( serviceConfigUI.get() );
152+
}
153+
} // namespace TTS

src/tts/config_window.hh

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
#pragma once
2+
3+
#include "tts/services/azure.hh"
4+
#include <QDialogButtonBox>
5+
#include <QGridLayout>
6+
#include <QGroupBox>
7+
#include <QWidget>
8+
9+
namespace TTS {
10+
class ConfigWindow: public QWidget
11+
{
12+
Q_OBJECT
13+
14+
public:
15+
explicit ConfigWindow( QWidget * parent, const QString & configRootPath );
16+
17+
signals:
18+
void service_changed();
19+
20+
private:
21+
QGridLayout * MainLayout;
22+
QGroupBox * configPane;
23+
24+
QDialogButtonBox * buttonBox;
25+
QLineEdit * previewLineEdit;
26+
QPushButton * previewButton;
27+
28+
QString currentService;
29+
QDir configRootDir;
30+
31+
QComboBox * serviceSelector;
32+
33+
std::unique_ptr< TTS::Service > previewService;
34+
std::unique_ptr< TTS::ServiceConfigWidget > serviceConfigUI;
35+
36+
void setupUi();
37+
38+
private slots:
39+
void updateConfigPaneBasedOnCurrentService();
40+
};
41+
42+
} // namespace TTS

src/tts/dev_helpers/README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
Files to test various services.

src/tts/dev_helpers/voice.hurl

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
POST https://eastus.tts.speech.microsoft.com/cognitiveservices/v1
2+
3+
Ocp-Apim-Subscription-Key: b9885138792d4403a8ccf1a34553351d
4+
X-Microsoft-OutputFormat: audio-16khz-64kbitrate-mono-mp3
5+
Content-Type: application/ssml+xml
6+
User-Agent: WhatEver
7+
<speak version='1.0' xml:lang='en-US'>
8+
<voice name='en-US-LunaNeural'>
9+
hello world
10+
</voice>
11+
</speak>

src/tts/dev_helpers/voicelist.hurl

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
GET https://eastus.tts.speech.microsoft.com/cognitiveservices/voices/list
2+
3+
Ocp-Apim-Subscription-Key: b9885138792d4403a8ccf1a34553351d

0 commit comments

Comments
 (0)