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:
@@ -9,18 +9,37 @@ chat with your Hermes agent from the phone — and use it with your **voice**.
|
|||||||
- Sessions list, new session, open any session
|
- Sessions list, new session, open any session
|
||||||
- Chat with **live streaming** replies (SSE token deltas)
|
- Chat with **live streaming** replies (SSE token deltas)
|
||||||
- **Voice dictation**: tap Mic, speak, tap Stop → server-side Whisper
|
- **Voice dictation**: tap Mic, speak, tap Stop → server-side Whisper
|
||||||
transcription lands in the composer
|
transcription lands in the composer (optionally sent right away)
|
||||||
- **Read aloud**: server-side TTS (edge engine, Italian voices) played through
|
- **Read aloud**: server-side TTS (voice/engine configurable) played through
|
||||||
QMediaPlayer; optional auto-read of every completed reply
|
QMediaPlayer; optional auto-read of every completed reply
|
||||||
|
- **Voice dialog (hands-free)**: continuous listen → transcribe → send →
|
||||||
|
read-aloud loop, auto-stopping on speech pauses (tunable)
|
||||||
- Cancel a running turn, resync from server state, cover page
|
- Cancel a running turn, resync from server state, cover page
|
||||||
|
|
||||||
## Requirements
|
## Requirements
|
||||||
|
|
||||||
- Sailfish OS 4.4+ (developed against Sailfish 5.1)
|
- Sailfish OS 4.4+ (developed against Sailfish 5.1)
|
||||||
- A running `hermes-webui` instance reachable over HTTPS (default:
|
- A running `hermes-webui` instance reachable over HTTPS (the address is
|
||||||
`https://hermes.hackatoniclife.com`)
|
configured in the app — nothing is compiled in)
|
||||||
- Sailjail permissions `Internet;Audio;Microphone` (declared in the .desktop)
|
- Sailjail permissions `Internet;Audio;Microphone` (declared in the .desktop)
|
||||||
|
|
||||||
|
## Configuration
|
||||||
|
|
||||||
|
No configuration is compiled into the binary: everything lives in the app's
|
||||||
|
Settings (persisted in `~/.config/harbour/hermes.conf`, sandbox-persistent).
|
||||||
|
Defaults are neutral — empty means "the server decides" — and the login screen
|
||||||
|
requires a server address.
|
||||||
|
|
||||||
|
| Setting | Meaning |
|
||||||
|
|---|---|
|
||||||
|
| Server | Hermes Web UI address, e.g. `https://hermes.example.com` (required) |
|
||||||
|
| Read-aloud voice / TTS engine | empty = server defaults |
|
||||||
|
| Voice/engine presets | comma-separated lists used to fill the pickers |
|
||||||
|
| Read replies aloud | auto-play every reply (off by default) |
|
||||||
|
| Send right after dictation | auto-send when dictation lands (on by default) |
|
||||||
|
| Auto-stop pause / Mic sensitivity | voice-dialog tuning (1.5 s / medium) |
|
||||||
|
| Max recording time | voice-dialog safety cap (60 s) |
|
||||||
|
|
||||||
## Build
|
## Build
|
||||||
|
|
||||||
See [docs/BUILD.md](docs/BUILD.md) (Sailfish SDK, `sfdk build` / `sfdk deploy`)
|
See [docs/BUILD.md](docs/BUILD.md) (Sailfish SDK, `sfdk build` / `sfdk deploy`)
|
||||||
@@ -29,6 +48,6 @@ speaks (validated live against a real instance).
|
|||||||
|
|
||||||
## Status
|
## Status
|
||||||
|
|
||||||
Version 0.2.1 — protocol and C++ core validated against a real `hermes-webui`
|
Version 0.3.0 — protocol and C++ core validated against a real `hermes-webui`
|
||||||
(login, sessions, streaming chat, TTS, STT); on-device testing (audio paths,
|
(login, sessions, streaming chat, TTS, STT, voice dialog); on-device testing
|
||||||
Sailjail prompts, keyboard handling) still pending.
|
still ongoing.
|
||||||
|
|||||||
+4
-4
@@ -75,13 +75,13 @@ richiede il probe WebUI su :8899). Esito 12/09/2026: unit 9/9, live completo OK.
|
|||||||
## 4. Tarball per la build da Qt Creator
|
## 4. Tarball per la build da Qt Creator
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
cd /home/kaneda/workspace
|
cd ~/workspace
|
||||||
tar czf harbour-hermes-0.2.1.tar.gz \
|
tar czf harbour-hermes-0.3.0.tar.gz \
|
||||||
--transform 's,^harbour-hermes,harbour-hermes-0.2.1,' \
|
--transform 's,^harbour-hermes,harbour-hermes-0.3.0,' \
|
||||||
--exclude='.git' harbour-hermes
|
--exclude='.git' harbour-hermes
|
||||||
```
|
```
|
||||||
|
|
||||||
Lo spec si aspetta la directory `harbour-hermes-0.2.1/` (pattern degli altri
|
Lo spec si aspetta la directory `harbour-hermes-0.3.0/` (pattern degli altri
|
||||||
progetti: cercato in modo robusto anche per i sorgenti live di sfdk).
|
progetti: cercato in modo robusto anche per i sorgenti live di sfdk).
|
||||||
|
|
||||||
## 5. Dipendenze lato server (WebUI)
|
## 5. Dipendenze lato server (WebUI)
|
||||||
|
|||||||
+1
-1
@@ -10,7 +10,7 @@ Documentazione del protocollo HTTP/SSE usato da harbour-hermes.
|
|||||||
> al client.
|
> al client.
|
||||||
|
|
||||||
Server di riferimento: in ascolto su `127.0.0.1:8787` dietro reverse proxy
|
Server di riferimento: in ascolto su `127.0.0.1:8787` dietro reverse proxy
|
||||||
(`https://hermes.hackatoniclife.com`). TLS obbligatorio in produzione.
|
(URL configurabile nell'app). TLS obbligatorio in produzione.
|
||||||
|
|
||||||
## 1. Autenticazione
|
## 1. Autenticazione
|
||||||
|
|
||||||
|
|||||||
@@ -45,7 +45,7 @@ Page {
|
|||||||
Button {
|
Button {
|
||||||
anchors.horizontalCenter: parent.horizontalCenter
|
anchors.horizontalCenter: parent.horizontalCenter
|
||||||
text: api.busy ? qsTr("Signing in…") : qsTr("Sign in")
|
text: api.busy ? qsTr("Signing in…") : qsTr("Sign in")
|
||||||
enabled: !api.busy && passField.text.length > 0
|
enabled: !api.busy && passField.text.length > 0 && urlField.text.trim().length > 0
|
||||||
onClicked: page.doLogin()
|
onClicked: page.doLogin()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+79
-34
@@ -4,14 +4,10 @@ import Sailfish.Silica 1.0
|
|||||||
Page {
|
Page {
|
||||||
id: page
|
id: page
|
||||||
|
|
||||||
property var voiceNames: [
|
// Nessun elenco cablato: le voci/gli engine dei selettori vengono dalle
|
||||||
"it-IT-ElsaNeural",
|
// impostazioni (ttsVoices/ttsEngines) e il valore corrente è sempre
|
||||||
"it-IT-DiegoNeural",
|
// incluso. loading evita di salvare durante il caricamento iniziale.
|
||||||
"it-IT-IsabellaNeural",
|
property bool loading: true
|
||||||
"it-IT-GiuseppeMultilingualNeural",
|
|
||||||
"en-US-AriaNeural"
|
|
||||||
]
|
|
||||||
property var engineNames: ["edge", "openai", "elevenlabs"]
|
|
||||||
|
|
||||||
SilicaFlickable {
|
SilicaFlickable {
|
||||||
anchors.fill: parent
|
anchors.fill: parent
|
||||||
@@ -42,51 +38,61 @@ Page {
|
|||||||
|
|
||||||
ComboBox {
|
ComboBox {
|
||||||
label: qsTr("Read-aloud voice")
|
label: qsTr("Read-aloud voice")
|
||||||
currentIndex: Math.max(0, page.voiceNames.indexOf(appSettings.ttsVoice))
|
currentIndex: page.voiceIndex()
|
||||||
// MenuItems espliciti (no Repeater: pattern non provato sul kit 5.1)
|
// Repeater dentro ContextMenu = pattern provato sul device (tide)
|
||||||
menu: ContextMenu {
|
menu: ContextMenu {
|
||||||
MenuItem {
|
MenuItem {
|
||||||
text: "it-IT-ElsaNeural"
|
text: qsTr("(server default)")
|
||||||
onClicked: appSettings.ttsVoice = "it-IT-ElsaNeural"
|
onClicked: appSettings.ttsVoice = ""
|
||||||
}
|
}
|
||||||
MenuItem {
|
Repeater {
|
||||||
text: "it-IT-DiegoNeural"
|
model: page.voiceChoices().length
|
||||||
onClicked: appSettings.ttsVoice = "it-IT-DiegoNeural"
|
delegate: MenuItem {
|
||||||
|
text: page.voiceChoices()[index]
|
||||||
|
onClicked: appSettings.ttsVoice = page.voiceChoices()[index]
|
||||||
}
|
}
|
||||||
MenuItem {
|
|
||||||
text: "it-IT-IsabellaNeural"
|
|
||||||
onClicked: appSettings.ttsVoice = "it-IT-IsabellaNeural"
|
|
||||||
}
|
}
|
||||||
MenuItem {
|
|
||||||
text: "it-IT-GiuseppeMultilingualNeural"
|
|
||||||
onClicked: appSettings.ttsVoice = "it-IT-GiuseppeMultilingualNeural"
|
|
||||||
}
|
}
|
||||||
MenuItem {
|
|
||||||
text: "en-US-AriaNeural"
|
|
||||||
onClicked: appSettings.ttsVoice = "en-US-AriaNeural"
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
TextField {
|
||||||
|
id: voicesField
|
||||||
|
width: parent.width
|
||||||
|
label: qsTr("Voice presets (comma-separated)")
|
||||||
|
onTextChanged: {
|
||||||
|
if (!page.loading)
|
||||||
|
appSettings.ttsVoices = text.split(",")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
ComboBox {
|
ComboBox {
|
||||||
label: qsTr("TTS engine")
|
label: qsTr("TTS engine")
|
||||||
currentIndex: Math.max(0, page.engineNames.indexOf(appSettings.ttsEngine))
|
currentIndex: page.engineIndex()
|
||||||
menu: ContextMenu {
|
menu: ContextMenu {
|
||||||
MenuItem {
|
MenuItem {
|
||||||
text: "edge"
|
text: qsTr("(server default)")
|
||||||
onClicked: appSettings.ttsEngine = "edge"
|
onClicked: appSettings.ttsEngine = ""
|
||||||
}
|
}
|
||||||
MenuItem {
|
Repeater {
|
||||||
text: "openai"
|
model: page.engineChoices().length
|
||||||
onClicked: appSettings.ttsEngine = "openai"
|
delegate: MenuItem {
|
||||||
|
text: page.engineChoices()[index]
|
||||||
|
onClicked: appSettings.ttsEngine = page.engineChoices()[index]
|
||||||
}
|
}
|
||||||
MenuItem {
|
|
||||||
text: "elevenlabs"
|
|
||||||
onClicked: appSettings.ttsEngine = "elevenlabs"
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
TextField {
|
||||||
|
id: enginesField
|
||||||
|
width: parent.width
|
||||||
|
label: qsTr("Engine presets (comma-separated)")
|
||||||
|
onTextChanged: {
|
||||||
|
if (!page.loading)
|
||||||
|
appSettings.ttsEngines = text.split(",")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
TextSwitch {
|
TextSwitch {
|
||||||
text: qsTr("Read replies aloud")
|
text: qsTr("Read replies aloud")
|
||||||
description: qsTr("Speak every completed reply automatically")
|
description: qsTr("Speak every completed reply automatically")
|
||||||
@@ -159,5 +165,44 @@ Page {
|
|||||||
pageStack.replace(Qt.resolvedUrl("LoginPage.qml"))
|
pageStack.replace(Qt.resolvedUrl("LoginPage.qml"))
|
||||||
}
|
}
|
||||||
|
|
||||||
Component.onCompleted: urlField.text = appSettings.baseUrl
|
// Elenchi dei selettori: preset configurati + valore corrente (così resta
|
||||||
|
// selezionabile anche se non è tra i preset). Indice 0 = "(server default)".
|
||||||
|
function voiceChoices() {
|
||||||
|
var out = appSettings.ttsVoices.slice()
|
||||||
|
var cur = appSettings.ttsVoice
|
||||||
|
if (cur.length > 0 && out.indexOf(cur) < 0)
|
||||||
|
out.push(cur)
|
||||||
|
return out
|
||||||
|
}
|
||||||
|
|
||||||
|
function voiceIndex() {
|
||||||
|
var cur = appSettings.ttsVoice
|
||||||
|
if (cur.length === 0)
|
||||||
|
return 0
|
||||||
|
var i = page.voiceChoices().indexOf(cur)
|
||||||
|
return i < 0 ? 0 : i + 1
|
||||||
|
}
|
||||||
|
|
||||||
|
function engineChoices() {
|
||||||
|
var out = appSettings.ttsEngines.slice()
|
||||||
|
var cur = appSettings.ttsEngine
|
||||||
|
if (cur.length > 0 && out.indexOf(cur) < 0)
|
||||||
|
out.push(cur)
|
||||||
|
return out
|
||||||
|
}
|
||||||
|
|
||||||
|
function engineIndex() {
|
||||||
|
var cur = appSettings.ttsEngine
|
||||||
|
if (cur.length === 0)
|
||||||
|
return 0
|
||||||
|
var i = page.engineChoices().indexOf(cur)
|
||||||
|
return i < 0 ? 0 : i + 1
|
||||||
|
}
|
||||||
|
|
||||||
|
Component.onCompleted: {
|
||||||
|
urlField.text = appSettings.baseUrl
|
||||||
|
voicesField.text = appSettings.ttsVoices.join(", ")
|
||||||
|
enginesField.text = appSettings.ttsEngines.join(", ")
|
||||||
|
page.loading = false
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
Name: harbour-hermes
|
Name: harbour-hermes
|
||||||
Summary: Client for the Hermes Web UI with voice
|
Summary: Client for the Hermes Web UI with voice
|
||||||
Version: 0.2.1
|
Version: 0.3.0
|
||||||
Release: 1
|
Release: 1
|
||||||
Group: Qt/Qt
|
Group: Qt/Qt
|
||||||
License: MIT
|
License: MIT
|
||||||
@@ -24,6 +24,12 @@ Sessions list, chat with live streaming replies, voice dictation
|
|||||||
(server-side TTS), plus a hands-free conversation mode.
|
(server-side TTS), plus a hands-free conversation mode.
|
||||||
|
|
||||||
%changelog
|
%changelog
|
||||||
|
* Sat Sep 12 2026 Carlo Baratto <carlo@carlobaratto.it> - 0.3.0-1
|
||||||
|
- Nessuna configurazione cablata nel binario: indirizzo server, voce,
|
||||||
|
engine e preset sono impostazioni dell'app (default neutri; vuoto =
|
||||||
|
decide il server); selettori voce/engine costruiti dai preset
|
||||||
|
configurabili; Content-Type audio coerente col formato reale; durata
|
||||||
|
massima registrazione configurabile (vadMaxMs)
|
||||||
* Sat Sep 12 2026 Carlo Baratto <carlo@carlobaratto.it> - 0.2.1-1
|
* Sat Sep 12 2026 Carlo Baratto <carlo@carlobaratto.it> - 0.2.1-1
|
||||||
- Fix build: rimosso il warning -Wreorder (ordine dell'inizializzazione
|
- Fix build: rimosso il warning -Wreorder (ordine dell'inizializzazione
|
||||||
di m_sessionsModel nel costruttore di ApiClient)
|
di m_sessionsModel nel costruttore di ApiClient)
|
||||||
|
|||||||
+27
-1
@@ -120,6 +120,13 @@ void ApiClient::clearError()
|
|||||||
|
|
||||||
void ApiClient::checkAuth()
|
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")));
|
QNetworkReply *reply = m_nam.get(jsonRequest(QStringLiteral("/api/auth/status")));
|
||||||
connect(reply, &QNetworkReply::finished, this, [this, reply]() {
|
connect(reply, &QNetworkReply::finished, this, [this, reply]() {
|
||||||
reply->deleteLater();
|
reply->deleteLater();
|
||||||
@@ -144,6 +151,10 @@ void ApiClient::checkAuth()
|
|||||||
|
|
||||||
void ApiClient::login(const QString &password)
|
void ApiClient::login(const QString &password)
|
||||||
{
|
{
|
||||||
|
if (m_settings->baseUrl().isEmpty()) {
|
||||||
|
setLastError(QStringLiteral("Server address not configured"));
|
||||||
|
return;
|
||||||
|
}
|
||||||
setLastError(QString());
|
setLastError(QString());
|
||||||
setBusy(true);
|
setBusy(true);
|
||||||
QJsonObject body;
|
QJsonObject body;
|
||||||
@@ -429,7 +440,19 @@ void ApiClient::transcribeFile(const QString &path)
|
|||||||
|
|
||||||
QHttpMultiPart *multiPart = new QHttpMultiPart(QHttpMultiPart::FormDataType);
|
QHttpMultiPart *multiPart = new QHttpMultiPart(QHttpMultiPart::FormDataType);
|
||||||
QHttpPart filePart;
|
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,
|
filePart.setHeader(QNetworkRequest::ContentDispositionHeader,
|
||||||
QVariant(QStringLiteral("form-data; name=\"file\"; filename=\"%1\"")
|
QVariant(QStringLiteral("form-data; name=\"file\"; filename=\"%1\"")
|
||||||
.arg(QFileInfo(path).fileName())));
|
.arg(QFileInfo(path).fileName())));
|
||||||
@@ -477,7 +500,10 @@ void ApiClient::speak(const QString &text)
|
|||||||
setLastError(QString());
|
setLastError(QString());
|
||||||
QJsonObject body;
|
QJsonObject body;
|
||||||
body.insert(QStringLiteral("text"), spoken);
|
body.insert(QStringLiteral("text"), spoken);
|
||||||
|
// Voce/engine vuoti = default del server: i parametri si omettono.
|
||||||
|
if (!m_settings->ttsVoice().isEmpty())
|
||||||
body.insert(QStringLiteral("voice"), m_settings->ttsVoice());
|
body.insert(QStringLiteral("voice"), m_settings->ttsVoice());
|
||||||
|
if (!m_settings->ttsEngine().isEmpty())
|
||||||
body.insert(QStringLiteral("engine"), m_settings->ttsEngine());
|
body.insert(QStringLiteral("engine"), m_settings->ttsEngine());
|
||||||
QNetworkReply *reply = m_nam.post(jsonRequest(QStringLiteral("/api/tts")),
|
QNetworkReply *reply = m_nam.post(jsonRequest(QStringLiteral("/api/tts")),
|
||||||
QJsonDocument(body).toJson(QJsonDocument::Compact));
|
QJsonDocument(body).toJson(QJsonDocument::Compact));
|
||||||
|
|||||||
+2
-2
@@ -19,9 +19,9 @@ Q_DECL_EXPORT int main(int argc, char *argv[])
|
|||||||
// AppConfigLocation = ~/.config/harbour/hermes (zona persistente della sandbox).
|
// AppConfigLocation = ~/.config/harbour/hermes (zona persistente della sandbox).
|
||||||
app->setOrganizationName(QStringLiteral("harbour"));
|
app->setOrganizationName(QStringLiteral("harbour"));
|
||||||
app->setApplicationName(QStringLiteral("hermes"));
|
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;
|
Settings settings;
|
||||||
ApiClient api(&settings);
|
ApiClient api(&settings);
|
||||||
|
|||||||
+57
-5
@@ -1,10 +1,11 @@
|
|||||||
#include "settings.h"
|
#include "settings.h"
|
||||||
|
|
||||||
// Default: l'istanza pubblica del WebUI usa questo host (HTTPS, cert valido).
|
// Nessuna configurazione personale e' compilata nel binario: i default sono
|
||||||
// L'utente puo' cambiarlo dalle impostazioni (LAN, VPN, altro dominio).
|
// neutri e TUTTO si imposta dalle Impostazioni dell'app (QSettings in
|
||||||
static const char *kDefaultBaseUrl = "https://hermes.hackatoniclife.com";
|
// ~/.config/harbour/hermes.conf). Vuoto = "decide il server".
|
||||||
static const char *kDefaultTtsVoice = "it-IT-ElsaNeural";
|
static const char *kDefaultBaseUrl = "";
|
||||||
static const char *kDefaultTtsEngine = "edge";
|
static const char *kDefaultTtsVoice = "";
|
||||||
|
static const char *kDefaultTtsEngine = "";
|
||||||
|
|
||||||
Settings::Settings(QObject *parent)
|
Settings::Settings(QObject *parent)
|
||||||
: QObject(parent)
|
: QObject(parent)
|
||||||
@@ -109,6 +110,57 @@ void Settings::setVadThreshold(int threshold)
|
|||||||
emit vadThresholdChanged();
|
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
|
QString Settings::defaultWorkspace() const
|
||||||
{
|
{
|
||||||
return m_settings.value(QStringLiteral("defaultWorkspace"), QString()).toString();
|
return m_settings.value(QStringLiteral("defaultWorkspace"), QString()).toString();
|
||||||
|
|||||||
@@ -4,6 +4,7 @@
|
|||||||
#include <QObject>
|
#include <QObject>
|
||||||
#include <QSettings>
|
#include <QSettings>
|
||||||
#include <QString>
|
#include <QString>
|
||||||
|
#include <QStringList>
|
||||||
|
|
||||||
// Impostazioni persistenti dell'app.
|
// Impostazioni persistenti dell'app.
|
||||||
// QSettings con OrganizationName "harbour" / ApplicationName "hermes" (impostati
|
// QSettings con OrganizationName "harbour" / ApplicationName "hermes" (impostati
|
||||||
@@ -25,6 +26,12 @@ class Settings : public QObject
|
|||||||
NOTIFY vadSilenceMsChanged)
|
NOTIFY vadSilenceMsChanged)
|
||||||
Q_PROPERTY(int vadThreshold READ vadThreshold WRITE setVadThreshold
|
Q_PROPERTY(int vadThreshold READ vadThreshold WRITE setVadThreshold
|
||||||
NOTIFY vadThresholdChanged)
|
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
|
Q_PROPERTY(QString defaultWorkspace READ defaultWorkspace WRITE setDefaultWorkspace
|
||||||
NOTIFY defaultWorkspaceChanged)
|
NOTIFY defaultWorkspaceChanged)
|
||||||
|
|
||||||
@@ -52,6 +59,16 @@ public:
|
|||||||
int vadThreshold() const;
|
int vadThreshold() const;
|
||||||
void setVadThreshold(int threshold);
|
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;
|
QString defaultWorkspace() const;
|
||||||
void setDefaultWorkspace(const QString &ws);
|
void setDefaultWorkspace(const QString &ws);
|
||||||
|
|
||||||
@@ -63,6 +80,9 @@ signals:
|
|||||||
void dictationAutoSendChanged();
|
void dictationAutoSendChanged();
|
||||||
void vadSilenceMsChanged();
|
void vadSilenceMsChanged();
|
||||||
void vadThresholdChanged();
|
void vadThresholdChanged();
|
||||||
|
void vadMaxMsChanged();
|
||||||
|
void ttsVoicesChanged();
|
||||||
|
void ttsEnginesChanged();
|
||||||
void defaultWorkspaceChanged();
|
void defaultWorkspaceChanged();
|
||||||
|
|
||||||
private:
|
private:
|
||||||
|
|||||||
@@ -174,8 +174,8 @@ void VoiceRecorder::onSilenceCheck()
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Tetto di sicurezza anti-registrazione infinita.
|
// Tetto di sicurezza anti-registrazione infinita (configurabile: vadMaxMs).
|
||||||
if (elapsed >= 60000) {
|
if (elapsed >= qint64(m_settings->vadMaxMs())) {
|
||||||
qDebug() << "[voice] stop automatico: durata massima raggiunta";
|
qDebug() << "[voice] stop automatico: durata massima raggiunta";
|
||||||
finalize();
|
finalize();
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user