Commit 3225eaad authored by Jean-Baptiste Mardelle's avatar Jean-Baptiste Mardelle
Browse files

Move speech recognition settings to Kdenlive's main settings dialog

parent 585e3a55
Pipeline #50268 canceled with stage
INSTALL(FILES
checkvosk.py
speech.py
speechtotext.py
......
import sys
import subprocess
import pkg_resources
required = {'vosk', 'srt'}
installed = {pkg.key for pkg in pkg_resources.working_set}
missing = required - installed
print ("Missing pachages: ", missing)
if missing and len(sys.argv) > 1 :
print ("Installing missing pachages: ", missing)
python = sys.executable
subprocess.check_call([python, '-m', 'pip', 'install', *missing], stdout=subprocess.DEVNULL)
......@@ -305,6 +305,8 @@ signals:
void loadingMessageUpdated(const QString &, int progress = 0, int max = -1);
/** @brief Opening finished, close splash screen */
void closeSplash();
/** @brief Trigger an update of the the speech models list */
void voskModelUpdate(const QStringList models);
};
#endif
......@@ -39,9 +39,24 @@
#include "kdenlive_debug.h"
#include "klocalizedstring.h"
#include <KIO/DesktopExecParser>
#include <kio_version.h>
#if KIO_VERSION > QT_VERSION_CHECK(5, 70, 0)
#include <KIO/OpenUrlJob>
#else
#include <KRun>
#endif
#include <KUrlRequesterDialog>
#include <KArchive>
#include <KZip>
#include <KTar>
#include <KIO/FileCopyJob>
#include <KLineEdit>
#include <KMessageBox>
#include <KOpenWithDialog>
#include <KIO/JobUiDelegate>
#include <KArchiveDirectory>
#include <KService>
#include <QAction>
#include <QDir>
......@@ -159,6 +174,11 @@ KdenliveSettingsDialog::KdenliveSettingsDialog(QMap<QString, QString> mappable_a
m_configColors.setupUi(p10);
m_page10 = addPage(p10, i18n("Colors"));
m_page10->setIcon(QIcon::fromTheme(QStringLiteral("color-management")));
QWidget *p11 = new QWidget;
m_configSpeech.setupUi(p11);
m_page11 = addPage(p11, i18n("Speech To Text"));
m_page11->setIcon(QIcon::fromTheme(QStringLiteral("text-speak")));
QWidget *p4 = new QWidget;
m_configCapture.setupUi(p4);
......@@ -397,6 +417,7 @@ KdenliveSettingsDialog::KdenliveSettingsDialog(QMap<QString, QString> mappable_a
}
initAudioRecDevice();
initSpeechPage();
// Config dialog size
KSharedConfigPtr config = KSharedConfig::openConfig();
......@@ -749,6 +770,9 @@ void KdenliveSettingsDialog::showPage(int page, int option)
case 7:
setCurrentPage(m_page7);
break;
case 8:
setCurrentPage(m_page11);
break;
default:
setCurrentPage(m_page1);
}
......@@ -1718,3 +1742,227 @@ void KdenliveSettingsDialog::slotUpdateAudioCaptureSampleRate(int index)
{
KdenliveSettings::setAudiocapturesamplerate(m_configCapture.audiocapturesamplerate->itemData(index).toInt());
}
void KdenliveSettingsDialog::initSpeechPage()
{
m_voskAction = new QAction(i18n("Install missing dependencies"), this);
m_configSpeech.speech_info->setWordWrap(true);
m_configSpeech.check_vosk->setIcon(QIcon::fromTheme(QStringLiteral("view-refresh")));
m_configSpeech.check_vosk->setToolTip(i18n("Check VOSK installation"));
connect(m_configSpeech.check_vosk, &QToolButton::clicked, [this]() {
m_configSpeech.check_vosk->setEnabled(false);
KdenliveSettings::setVosk_found(false);
KdenliveSettings::setVosk_srt_found(false);
checkVoskDependencies();
m_configSpeech.check_vosk->setEnabled(true);
});
connect(m_voskAction, &QAction::triggered, [this]() {
QString pyExec = QStandardPaths::findExecutable(QStringLiteral("python3"));
if (pyExec.isEmpty()) {
m_configSpeech.speech_info->setMessageType(KMessageWidget::Warning);
m_configSpeech.speech_info->setText(i18n("Cannot find python3, please install it on your system."));
m_configSpeech.speech_info->animatedShow();
return;
} else if (!KdenliveSettings::vosk_found() || !KdenliveSettings::vosk_srt_found()) {
QProcess checkJob;
QString speechScript = QStandardPaths::locate(QStandardPaths::AppDataLocation, QStringLiteral("scripts/checkvosk.py"));
if (speechScript.isEmpty()) {
m_configSpeech.speech_info->setMessageType(KMessageWidget::Warning);
m_configSpeech.speech_info->setText(i18n("The speech script was not found, check your install."));
m_configSpeech.speech_info->show();
return;
} else {
m_voskAction->setEnabled(false);
m_configSpeech.speech_info->setMessageType(KMessageWidget::Information);
m_configSpeech.speech_info->setText(i18n("Installing modules..."));
m_configSpeech.speech_info->show();
qDebug()<<"==== STARTING INSTALL VOSK DEPS...";
checkJob.start(pyExec, {speechScript, QStringLiteral("install")});
checkJob.waitForFinished();
qDebug()<<"==== STARTING INSTALL VOSK DEPS... DONE";
}
checkVoskDependencies();
}
});
checkVoskDependencies();
connect(m_configSpeech.custom_vosk_folder, &QCheckBox::stateChanged, [this](int state) {
m_configSpeech.vosk_folder->setEnabled(state != Qt::Unchecked);
});
m_configSpeech.models_url->setText(i18n("Download speech models from: <a href=\"https://alphacephei.com/vosk/models\">https://alphacephei.com/vosk/models</a>"));
connect(m_configSpeech.models_url, &QLabel::linkActivated, [&](const QString &contents) {
qDebug()<<"=== LINK CLICKED: "<<contents;
#if KIO_VERSION > QT_VERSION_CHECK(5, 70, 0)
auto *job = new KIO::OpenUrlJob(QUrl(contents));
job->setUiDelegate(new KIO::JobUiDelegate(KJobUiDelegate::AutoHandlingEnabled, this));
// methods like setRunExecutables, setSuggestedFilename, setEnableExternalBrowser, setFollowRedirections
// exist in both classes
job->start();
#else
new KRun(QUrl(contents), this);
#endif
});
m_configSpeech.button_add->setIcon(QIcon::fromTheme(QStringLiteral("list-add")));
m_configSpeech.button_delete->setIcon(QIcon::fromTheme(QStringLiteral("edit-delete")));
m_configSpeech.button_add->setToolTip(i18n("Add a new speech model from an archive file"));
m_configSpeech.button_delete->setToolTip(i18n("Delete the selected speech model"));
connect(m_configSpeech.button_add, &QToolButton::clicked, this, &KdenliveSettingsDialog::getDictionary);
connect(this, &KdenliveSettingsDialog::parseDictionaries, this, &KdenliveSettingsDialog::slotParseVoskDictionaries);
slotParseVoskDictionaries();
}
void KdenliveSettingsDialog::checkVoskDependencies()
{
qDebug()<<"==== CHECKING VOSK DEPS...";
QString pyExec = QStandardPaths::findExecutable(QStringLiteral("python3"));
if (pyExec.isEmpty()) {
m_configSpeech.speech_info->setMessageType(KMessageWidget::Warning);
m_configSpeech.speech_info->setText(i18n("Cannot find python3, please install it on your system."));
m_configSpeech.speech_info->animatedShow();
return;
} else if (!KdenliveSettings::vosk_found() || !KdenliveSettings::vosk_srt_found()) {
QProcess checkJob;
QString speechScript = QStandardPaths::locate(QStandardPaths::AppDataLocation, QStringLiteral("scripts/checkvosk.py"));
if (speechScript.isEmpty()) {
m_configSpeech.speech_info->setMessageType(KMessageWidget::Warning);
m_configSpeech.speech_info->setText(i18n("The speech script was not found, check your install."));
m_configSpeech.speech_info->show();
return;
} else {
checkJob.start(pyExec, {speechScript});
checkJob.waitForFinished();
QString output = checkJob.readAllStandardOutput();
QString missingModules;
if (!output.contains(QLatin1String("vosk"))) {
KdenliveSettings::setVosk_found(true);
} else {
KdenliveSettings::setVosk_found(false);
missingModules = i18n("The VOSK python module is required for speech features. ");
}
if (!output.contains(QLatin1String("srt"))) {
KdenliveSettings::setVosk_srt_found(true);
} else {
KdenliveSettings::setVosk_srt_found(false);
missingModules.append(i18n("The SRT python module is required for automated subtitling."));
}
if (!missingModules.isEmpty()) {
m_configSpeech.speech_info->setMessageType(KMessageWidget::Warning);
m_configSpeech.speech_info->setText(missingModules);
m_voskAction->setEnabled(true);
m_configSpeech.speech_info->addAction(m_voskAction);
m_configSpeech.speech_info->show();
} else {
m_configSpeech.speech_info->removeAction(m_voskAction);
m_configSpeech.speech_info->animatedHide();
}
}
} else {
m_configSpeech.speech_info->animatedHide();
}
}
void KdenliveSettingsDialog::getDictionary()
{
QUrl url = KUrlRequesterDialog::getUrl(QUrl(), this, i18n("Enter url for the new dictionary"));
if (url.isEmpty()) {
return;
}
QString tmpFile;
if (!url.isLocalFile()) {
KIO::FileCopyJob *copyjob = KIO::file_copy(url, QUrl::fromLocalFile(QDir::temp().absoluteFilePath(url.fileName())));
m_configSpeech.speech_info->setMessageType(KMessageWidget::Information);
m_configSpeech.speech_info->setText(i18n("Downloading model..."));
m_configSpeech.speech_info->animatedShow();
connect(copyjob, &KIO::FileCopyJob::result, this, &KdenliveSettingsDialog::processArchive);
/*if (copyjob->exec()) {
qDebug()<<"=== GOT REST: "<<copyjob->destUrl();
//
} else {
qDebug()<<"=== CANNOT DOWNLOAD";
}*/
} else {
//KMessageBox::error(this, KIO::NetAccess::lastErrorString());
//KArchive ar(tmpFile);
}
}
void KdenliveSettingsDialog::processArchive(KJob* job)
{
qDebug()<<"=== DOWNLOAD FINISHED!!";
if (job->error() == 0 || job->error() == 112) {
qDebug()<<"=== NO ERROR ON DWNLD!!";
KIO::FileCopyJob *jb = static_cast<KIO::FileCopyJob*>(job);
if (jb) {
qDebug()<<"=== JOB FOUND!!";
QMimeDatabase db;
QString archiveFile = jb->destUrl().toLocalFile();
QMimeType type = db.mimeTypeForFile(archiveFile);
std::unique_ptr<KArchive> archive;
if (type.inherits(QStringLiteral("application/zip"))) {
archive.reset(new KZip(archiveFile));
} else {
archive.reset(new KTar(archiveFile));
}
QString modelDirectory = QStandardPaths::writableLocation(QStandardPaths::AppDataLocation);
QDir dir(modelDirectory);
dir.mkdir(QStringLiteral("speechmodels"));
if (!dir.cd(QStringLiteral("speechmodels"))) {
qDebug()<<"=== /// CANNOT ACCESS SPEECH DICTIONARIES FOLDER";
m_configSpeech.speech_info->setMessageType(KMessageWidget::Warning);
m_configSpeech.speech_info->setText(i18n("Cannot access dictionary folder"));
return;
}
if (archive->open(QIODevice::ReadOnly)) {
m_configSpeech.speech_info->setText(i18n("Extracting archive..."));
const KArchiveDirectory *archiveDir = archive->directory();
if (!archiveDir->copyTo(dir.absolutePath())) {
qDebug()<<"=== Error extracting archive!!";
} else {
QFile::remove(archiveFile);
emit parseDictionaries();
m_configSpeech.speech_info->setMessageType(KMessageWidget::Positive);
m_configSpeech.speech_info->setText(i18n("New dictionary installed"));
}
} else {
qDebug()<<"=== CANNOT OPEN ARCHIVE!!";
}
} else {
qDebug()<<"=== JOB NOT FOUND!!";
m_configSpeech.speech_info->setMessageType(KMessageWidget::Warning);
m_configSpeech.speech_info->setText(i18n("Download error"));
}
} else {
qDebug()<<"=== GOT JOB ERROR: "<<job->error();
m_configSpeech.speech_info->setMessageType(KMessageWidget::Warning);
m_configSpeech.speech_info->setText(i18n("Download error %1", job->errorString()));
}
}
void KdenliveSettingsDialog::slotParseVoskDictionaries()
{
m_configSpeech.listWidget->clear();
QString modelDirectory = KdenliveSettings::vosk_folder_path();
QDir dir;
if (modelDirectory.isEmpty()) {
modelDirectory = QStandardPaths::writableLocation(QStandardPaths::AppDataLocation);
dir = QDir(modelDirectory);
if (!dir.cd(QStringLiteral("speechmodels"))) {
qDebug()<<"=== /// CANNOT ACCESS SPEECH DICTIONARIES FOLDER";
pCore->voskModelUpdate({});
return;
}
} else {
dir = QDir(modelDirectory);
}
QStringList dicts = dir.entryList(QDir::Dirs | QDir::NoDotAndDotDot);
m_configSpeech.listWidget->addItems(dicts);
if (!KdenliveSettings::vosk_folder_path().isEmpty()) {
m_configSpeech.custom_vosk_folder->setChecked(true);
m_configSpeech.vosk_folder->setUrl(QUrl::fromLocalFile(KdenliveSettings::vosk_folder_path()));
}
if (!dicts.isEmpty() && KdenliveSettings::vosk_found() && KdenliveSettings::vosk_srt_found()) {
m_configSpeech.speech_info->animatedHide();
}
pCore->voskModelUpdate(dicts);
}
......@@ -34,8 +34,10 @@
#include "ui_configtimeline_ui.h"
#include "ui_configtranscode_ui.h"
#include "ui_configcolors_ui.h"
#include "ui_configspeech_ui.h"
class ProfileWidget;
class KJob;
class KdenliveSettingsDialog : public KConfigDialog
{
......@@ -89,7 +91,11 @@ private slots:
void loadExternalProxyProfiles();
void slotUpdateAudioCaptureChannels(int index);
void slotUpdateAudioCaptureSampleRate(int index);
void slotParseVoskDictionaries();
void getDictionary();
void processArchive(KJob* job);
void checkVoskDependencies();
private:
KPageWidgetItem *m_page1;
KPageWidgetItem *m_page2;
......@@ -100,6 +106,7 @@ private:
KPageWidgetItem *m_page7;
KPageWidgetItem *m_page8;
KPageWidgetItem *m_page10;
KPageWidgetItem *m_page11;
Ui::ConfigEnv_UI m_configEnv;
Ui::ConfigMisc_UI m_configMisc;
Ui::ConfigColors_UI m_configColors;
......@@ -110,8 +117,10 @@ private:
Ui::ConfigTranscode_UI m_configTranscode;
Ui::ConfigProject_UI m_configProject;
Ui::ConfigProxy_UI m_configProxy;
Ui::ConfigSpeech_UI m_configSpeech;
ProfileWidget *m_pw;
KProcess m_readProcess;
QAction *m_voskAction;
bool m_modified;
bool m_shuttleModified;
QMap<QString, QString> m_mappable_actions;
......@@ -128,6 +137,8 @@ private:
static bool getBlackMagicOutputDeviceList(QComboBox *devicelist, bool force = false);
/** @brief Init QtMultimedia audio record settings */
bool initAudioRecDevice();
void initSpeechPage();
signals:
void customChanged();
void doResetConsumer(bool fullReset);
......@@ -144,6 +155,8 @@ signals:
void resetView();
/** @brief Monitor background color changed, update monitors */
void updateMonitorBg();
/** @brief Trigger parsing of the speech models folder */
void parseDictionaries();
};
#endif
......@@ -29,23 +29,10 @@
#include "mlt++/MltTractor.h"
#include "mlt++/MltConsumer.h"
#include <kio_version.h>
#include <QFontDatabase>
#include <QDir>
#include <QProcess>
#include <KLocalizedString>
#include <KUrlRequesterDialog>
#include <KArchive>
#include <KZip>
#include <KTar>
#include <KIO/FileCopyJob>
#if KIO_VERSION > QT_VERSION_CHECK(5, 70, 0)
#include <KIO/OpenUrlJob>
#else
#include <KRun>
#endif
#include <KIO/JobUiDelegate>
#include <KArchiveDirectory>
#include <KMessageWidget>
SpeechDialog::SpeechDialog(const std::shared_ptr<TimelineItemModel> &timeline, QPoint zone, bool activeTrackOnly, bool selectionOnly, QWidget *parent)
......@@ -56,28 +43,20 @@ SpeechDialog::SpeechDialog(const std::shared_ptr<TimelineItemModel> &timeline, Q
setupUi(this);
buttonBox->button(QDialogButtonBox::Apply)->setText(i18n("Process"));
speech_info->hide();
slotParseDictionaries();
button_add->setIcon(QIcon::fromTheme(QStringLiteral("list-add")));
button_delete->setIcon(QIcon::fromTheme(QStringLiteral("edit-delete")));
connect(button_add, &QToolButton::clicked, this, &SpeechDialog::getDictionary);
connect(pCore.get(), &Core::voskModelUpdate, [&](QStringList models) {
language_box->clear();
language_box->addItems(models);
buttonBox->button(QDialogButtonBox::Apply)->setEnabled(!models.isEmpty());
if (models.isEmpty()) {
speech_info->setMessageType(KMessageWidget::Information);
speech_info->setText(i18n("Please install speech recognition models"));
speech_info->animatedShow();
}
});
connect(buttonBox->button(QDialogButtonBox::Apply), &QPushButton::clicked, [this, timeline, zone]() {
slotProcessSpeech(timeline, zone);
});
connect(speech_info, &KMessageWidget::linkActivated, [&](const QString &contents) {
qDebug()<<"=== LINK CLICKED: "<<contents;
#if KIO_VERSION > QT_VERSION_CHECK(5, 70, 0)
auto *job = new KIO::OpenUrlJob(QUrl(contents));
job->setUiDelegate(new KIO::JobUiDelegate(KJobUiDelegate::AutoHandlingEnabled, this));
// methods like setRunExecutables, setSuggestedFilename, setEnableExternalBrowser, setFollowRedirections
// exist in both classes
job->start();
#else
new KRun(QUrl(contents), this);
#endif
});
//TODO: check for the python scripts vosk and srt
connect(this, &SpeechDialog::parseDictionaries, this, &SpeechDialog::slotParseDictionaries);
parseVoskDictionaries();
}
void SpeechDialog::slotProcessSpeech(const std::shared_ptr<TimelineItemModel> &timeline, QPoint zone)
......@@ -163,110 +142,21 @@ void SpeechDialog::slotProcessSpeech(const std::shared_ptr<TimelineItemModel> &t
}
}
void SpeechDialog::getDictionary()
{
QUrl url = KUrlRequesterDialog::getUrl(QUrl(), this, i18n("Enter url for the new dictionary"));
if (url.isEmpty()) {
return;
}
QString tmpFile;
if (!url.isLocalFile()) {
KIO::FileCopyJob *copyjob = KIO::file_copy(url, QUrl::fromLocalFile(QDir::temp().absoluteFilePath(url.fileName())));
speech_info->setMessageType(KMessageWidget::Information);
speech_info->setText(i18n("Downloading model..."));
speech_info->animatedShow();
connect(copyjob, &KIO::FileCopyJob::result, this, &SpeechDialog::processArchive);
/*if (copyjob->exec()) {
qDebug()<<"=== GOT REST: "<<copyjob->destUrl();
//
} else {
qDebug()<<"=== CANNOT DOWNLOAD";
}*/
} else {
//KMessageBox::error(this, KIO::NetAccess::lastErrorString());
//KArchive ar(tmpFile);
}
}
void SpeechDialog::processArchive(KJob* job)
void SpeechDialog::parseVoskDictionaries()
{
qDebug()<<"=== DOWNLOAD FINISHED!!";
if (job->error() == 0 || job->error() == 112) {
qDebug()<<"=== NO ERROR ON DWNLD!!";
KIO::FileCopyJob *jb = static_cast<KIO::FileCopyJob*>(job);
if (jb) {
qDebug()<<"=== JOB FOUND!!";
QMimeDatabase db;
QString archiveFile = jb->destUrl().toLocalFile();
QMimeType type = db.mimeTypeForFile(archiveFile);
std::unique_ptr<KArchive> archive;
if (type.inherits(QStringLiteral("application/zip"))) {
archive.reset(new KZip(archiveFile));
} else {
archive.reset(new KTar(archiveFile));
}
QString modelDirectory = QStandardPaths::writableLocation(QStandardPaths::AppDataLocation);
QDir dir(modelDirectory);
dir.mkdir(QStringLiteral("speechmodels"));
if (!dir.cd(QStringLiteral("speechmodels"))) {
qDebug()<<"=== /// CANNOT ACCESS SPEECH DICTIONARIES FOLDER";
speech_info->setMessageType(KMessageWidget::Warning);
speech_info->setText(i18n("Cannot access dictionary folder"));
return;
}
if (archive->open(QIODevice::ReadOnly)) {
speech_info->setText(i18n("Extracting archive..."));
const KArchiveDirectory *archiveDir = archive->directory();
if (!archiveDir->copyTo(dir.absolutePath())) {
qDebug()<<"=== Error extracting archive!!";
} else {
QFile::remove(archiveFile);
emit parseDictionaries();
speech_info->setMessageType(KMessageWidget::Positive);
speech_info->setText(i18n("New dictionary installed"));
}
} else {
qDebug()<<"=== CANNOT OPEN ARCHIVE!!";
}
} else {
qDebug()<<"=== JOB NOT FOUND!!";
speech_info->setMessageType(KMessageWidget::Warning);
speech_info->setText(i18n("Download error"));
QString modelDirectory = KdenliveSettings::vosk_folder_path();
QDir dir;
if (modelDirectory.isEmpty()) {
modelDirectory = QStandardPaths::writableLocation(QStandardPaths::AppDataLocation);
dir = QDir(modelDirectory);
if (!dir.cd(QStringLiteral("speechmodels"))) {
qDebug()<<"=== /// CANNOT ACCESS SPEECH DICTIONARIES FOLDER";
pCore->voskModelUpdate({});
return;
}
} else {
qDebug()<<"=== GOT JOB ERROR: "<<job->error();
speech_info->setMessageType(KMessageWidget::Warning);
speech_info->setText(i18n("Download error %1", job->errorString()));
}
}
void SpeechDialog::slotParseDictionaries()
{
listWidget->clear();
language_box->clear();
buttonBox->button(QDialogButtonBox::Apply)->setEnabled(false);
QString modelDirectory = QStandardPaths::writableLocation(QStandardPaths::AppDataLocation);
QDir dir(modelDirectory);
if (!dir.cd(QStringLiteral("speechmodels"))) {
qDebug()<<"=== /// CANNOT ACCESS SPEECH DICTIONARIES FOLDER";
tabWidget->setCurrentIndex(1);
speech_info->setMessageType(KMessageWidget::Information);
speech_info->setText(i18n("Download dictionaries from: <a href=\"https://alphacephei.com/vosk/models\">https://alphacephei.com/vosk/models</a>"));
speech_info->animatedShow();
return;
dir = QDir(modelDirectory);
}
QStringList dicts = dir.entryList(QDir::Dirs | QDir::NoDotAndDotDot);
listWidget->addItems(dicts);
language_box->addItems(dicts);
if (!dicts.isEmpty()) {
buttonBox->button(QDialogButtonBox::Apply)->setEnabled(true);
speech_info->animatedHide();
} else {
tabWidget->setCurrentIndex(1);
speech_info->setMessageType(KMessageWidget::Information);
speech_info->setText(i18n("Download dictionaries from: <a href=\"https://alphacephei.com/vosk/models\">https://alphacephei.com/vosk/models</a>"));
speech_info->animatedShow();
}
pCore->voskModelUpdate(dicts);
}
......@@ -24,9 +24,6 @@
#include "timeline2/model/timelineitemmodel.hpp"
#include "definitions.h"
class KJob;
/**
* @class SpeechDialog
* @brief A dialog for editing markers and guides.
......@@ -40,14 +37,11 @@ class SpeechDialog : public QDialog, public Ui::SpeechDialog_UI
public:
explicit SpeechDialog(const std::shared_ptr<TimelineItemModel> &timeline, QPoint zone, bool activeTrackOnly = false, bool selectionOnly = false, QWidget *parent = nullptr);
private:
void parseVoskDictionaries();
private slots:
void getDictionary();
void processArchive(KJob* job);
void slotParseDictionaries();
void slotProcessSpeech(const std::shared_ptr<TimelineItemModel> &timeline, QPoint zone);
signals:
void parseDictionaries();
};
#endif
......@@ -48,6 +48,16 @@ TextBasedEdit::TextBasedEdit(QWidget *parent)
connect(button_start, &QPushButton::clicked, this, &TextBasedEdit::startRecognition);
listWidget->setWordWrap(true);
search_frame->setVisible(false);
connect(pCore.get(), &Core::voskModelUpdate, [&](QStringList models) {
language_box->clear();
language_box->addItems(models);
button_start->setEnabled(!models.isEmpty());
if (models.isEmpty()) {