refactor(config): niente configurazione cablata — tutto parametrizzato (v0.3.0)

Prima di pubblicare: eliminati dai sorgenti tutti i valori "cablati".

- settings: default neutri (URL, voce, engine vuoti = "decide il server");
  nuove chiavi ttsVoices/ttsEngines (preset dei selettori) e vadMaxMs
  (durata massima registrazione, prima fissa a 60 s nel codice)
- ApiClient: guard su baseUrl vuoto (checkAuth/login si comportano senza
  rete), voce/engine omessi nella richiesta TTS se vuoti, Content-Type
  audio coerente col formato reale del file (ogg/wav/mp3/m4a/opus)
- SettingsPage: selettori voce/engine costruiti dai preset configurabili
  (Repeater dentro ContextMenu = pattern provato su tide) + campi per
  modificare i preset; LoginPage richiede indirizzo server non vuoto
- VoiceRecorder: tetto di registrazione da configurazione
- README: sezione "Configuration" completa; docs neutralizzate (niente
  URL/valori personali: repo public-friendly)
This commit is contained in:
2026-09-12 12:42:06 +02:00
parent 558a384339
commit a5932f0025
11 changed files with 232 additions and 64 deletions
+29 -3
View File
@@ -120,6 +120,13 @@ void ApiClient::clearError()
void ApiClient::checkAuth()
{
// Senza indirizzo configurato non c'e' nulla da verificare: si parte
// dalla pagina di login, che lo richiede.
if (m_settings->baseUrl().isEmpty()) {
setLoggedIn(false);
emit authChecked(false);
return;
}
QNetworkReply *reply = m_nam.get(jsonRequest(QStringLiteral("/api/auth/status")));
connect(reply, &QNetworkReply::finished, this, [this, reply]() {
reply->deleteLater();
@@ -144,6 +151,10 @@ void ApiClient::checkAuth()
void ApiClient::login(const QString &password)
{
if (m_settings->baseUrl().isEmpty()) {
setLastError(QStringLiteral("Server address not configured"));
return;
}
setLastError(QString());
setBusy(true);
QJsonObject body;
@@ -429,7 +440,19 @@ void ApiClient::transcribeFile(const QString &path)
QHttpMultiPart *multiPart = new QHttpMultiPart(QHttpMultiPart::FormDataType);
QHttpPart filePart;
filePart.setHeader(QNetworkRequest::ContentTypeHeader, QVariant(QStringLiteral("audio/ogg")));
// Content-Type coerente col formato reale (ogg = dettatura manuale,
// wav = modalita' dialogo, ...).
QString mime = QStringLiteral("audio/ogg");
const QString suffix = QFileInfo(path).suffix().toLower();
if (suffix == QLatin1String("wav"))
mime = QStringLiteral("audio/wav");
else if (suffix == QLatin1String("mp3"))
mime = QStringLiteral("audio/mpeg");
else if (suffix == QLatin1String("m4a"))
mime = QStringLiteral("audio/mp4");
else if (suffix == QLatin1String("opus"))
mime = QStringLiteral("audio/opus");
filePart.setHeader(QNetworkRequest::ContentTypeHeader, QVariant(mime));
filePart.setHeader(QNetworkRequest::ContentDispositionHeader,
QVariant(QStringLiteral("form-data; name=\"file\"; filename=\"%1\"")
.arg(QFileInfo(path).fileName())));
@@ -477,8 +500,11 @@ void ApiClient::speak(const QString &text)
setLastError(QString());
QJsonObject body;
body.insert(QStringLiteral("text"), spoken);
body.insert(QStringLiteral("voice"), m_settings->ttsVoice());
body.insert(QStringLiteral("engine"), m_settings->ttsEngine());
// Voce/engine vuoti = default del server: i parametri si omettono.
if (!m_settings->ttsVoice().isEmpty())
body.insert(QStringLiteral("voice"), m_settings->ttsVoice());
if (!m_settings->ttsEngine().isEmpty())
body.insert(QStringLiteral("engine"), m_settings->ttsEngine());
QNetworkReply *reply = m_nam.post(jsonRequest(QStringLiteral("/api/tts")),
QJsonDocument(body).toJson(QJsonDocument::Compact));
connect(reply, &QNetworkReply::finished, this, [this, reply]() {
+2 -2
View File
@@ -19,9 +19,9 @@ Q_DECL_EXPORT int main(int argc, char *argv[])
// AppConfigLocation = ~/.config/harbour/hermes (zona persistente della sandbox).
app->setOrganizationName(QStringLiteral("harbour"));
app->setApplicationName(QStringLiteral("hermes"));
app->setApplicationVersion(QStringLiteral("0.2.1"));
app->setApplicationVersion(QStringLiteral("0.3.0"));
qDebug() << "harbour-hermes v0.2.1 build" << __DATE__ << __TIME__;
qDebug() << "harbour-hermes v0.3.0 build" << __DATE__ << __TIME__;
Settings settings;
ApiClient api(&settings);
+57 -5
View File
@@ -1,10 +1,11 @@
#include "settings.h"
// Default: l'istanza pubblica del WebUI usa questo host (HTTPS, cert valido).
// L'utente puo' cambiarlo dalle impostazioni (LAN, VPN, altro dominio).
static const char *kDefaultBaseUrl = "https://hermes.hackatoniclife.com";
static const char *kDefaultTtsVoice = "it-IT-ElsaNeural";
static const char *kDefaultTtsEngine = "edge";
// Nessuna configurazione personale e' compilata nel binario: i default sono
// neutri e TUTTO si imposta dalle Impostazioni dell'app (QSettings in
// ~/.config/harbour/hermes.conf). Vuoto = "decide il server".
static const char *kDefaultBaseUrl = "";
static const char *kDefaultTtsVoice = "";
static const char *kDefaultTtsEngine = "";
Settings::Settings(QObject *parent)
: QObject(parent)
@@ -109,6 +110,57 @@ void Settings::setVadThreshold(int threshold)
emit vadThresholdChanged();
}
int Settings::vadMaxMs() const
{
return m_settings.value(QStringLiteral("vadMaxMs"), 60000).toInt();
}
void Settings::setVadMaxMs(int ms)
{
if (ms == vadMaxMs())
return;
m_settings.setValue(QStringLiteral("vadMaxMs"), ms);
emit vadMaxMsChanged();
}
QStringList Settings::ttsVoices() const
{
return m_settings.value(QStringLiteral("ttsVoices")).toStringList();
}
void Settings::setTtsVoices(const QStringList &list)
{
QStringList clean;
for (const QString &v : list) {
const QString t = v.trimmed();
if (!t.isEmpty() && !clean.contains(t))
clean << t;
}
if (clean == ttsVoices())
return;
m_settings.setValue(QStringLiteral("ttsVoices"), clean);
emit ttsVoicesChanged();
}
QStringList Settings::ttsEngines() const
{
return m_settings.value(QStringLiteral("ttsEngines")).toStringList();
}
void Settings::setTtsEngines(const QStringList &list)
{
QStringList clean;
for (const QString &v : list) {
const QString t = v.trimmed();
if (!t.isEmpty() && !clean.contains(t))
clean << t;
}
if (clean == ttsEngines())
return;
m_settings.setValue(QStringLiteral("ttsEngines"), clean);
emit ttsEnginesChanged();
}
QString Settings::defaultWorkspace() const
{
return m_settings.value(QStringLiteral("defaultWorkspace"), QString()).toString();
+20
View File
@@ -4,6 +4,7 @@
#include <QObject>
#include <QSettings>
#include <QString>
#include <QStringList>
// Impostazioni persistenti dell'app.
// QSettings con OrganizationName "harbour" / ApplicationName "hermes" (impostati
@@ -25,6 +26,12 @@ class Settings : public QObject
NOTIFY vadSilenceMsChanged)
Q_PROPERTY(int vadThreshold READ vadThreshold WRITE setVadThreshold
NOTIFY vadThresholdChanged)
Q_PROPERTY(int vadMaxMs READ vadMaxMs WRITE setVadMaxMs
NOTIFY vadMaxMsChanged)
Q_PROPERTY(QStringList ttsVoices READ ttsVoices WRITE setTtsVoices
NOTIFY ttsVoicesChanged)
Q_PROPERTY(QStringList ttsEngines READ ttsEngines WRITE setTtsEngines
NOTIFY ttsEnginesChanged)
Q_PROPERTY(QString defaultWorkspace READ defaultWorkspace WRITE setDefaultWorkspace
NOTIFY defaultWorkspaceChanged)
@@ -52,6 +59,16 @@ public:
int vadThreshold() const;
void setVadThreshold(int threshold);
int vadMaxMs() const;
void setVadMaxMs(int ms);
// Preset per i selettori voce/engine (liste configurabili, default vuote).
QStringList ttsVoices() const;
void setTtsVoices(const QStringList &list);
QStringList ttsEngines() const;
void setTtsEngines(const QStringList &list);
QString defaultWorkspace() const;
void setDefaultWorkspace(const QString &ws);
@@ -63,6 +80,9 @@ signals:
void dictationAutoSendChanged();
void vadSilenceMsChanged();
void vadThresholdChanged();
void vadMaxMsChanged();
void ttsVoicesChanged();
void ttsEnginesChanged();
void defaultWorkspaceChanged();
private:
+2 -2
View File
@@ -174,8 +174,8 @@ void VoiceRecorder::onSilenceCheck()
return;
}
// Tetto di sicurezza anti-registrazione infinita.
if (elapsed >= 60000) {
// Tetto di sicurezza anti-registrazione infinita (configurabile: vadMaxMs).
if (elapsed >= qint64(m_settings->vadMaxMs())) {
qDebug() << "[voice] stop automatico: durata massima raggiunta";
finalize();
}