diff --git a/README.md b/README.md index 9d3335f..def386f 100644 --- a/README.md +++ b/README.md @@ -16,6 +16,8 @@ chat with your Hermes agent from the phone — and use it with your **voice**. read-aloud loop, auto-stopping on speech pauses (tunable) - **Localized UI**: English (source), Italian, French and German — follows the system language +- **Profile selection**: pick which server profile's sessions to show + (Settings → Sessions; per-client via the WebUI signed cookie) - Cancel a running turn, resync from server state, cover page ## Requirements @@ -35,6 +37,7 @@ requires a server address. | Setting | Meaning | |---|---| | Server | Hermes Web UI address, e.g. `https://hermes.example.com` (required) | +| Profile | server profile whose sessions are shown (empty = server's active) | | 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) | @@ -73,6 +76,6 @@ speaks (validated live against a real instance). ## Status -Version 0.4.0 — protocol and C++ core validated against a real `hermes-webui` -(login, sessions, streaming chat, TTS, STT, voice dialog); UI in English, -Italian, French and German; on-device testing still ongoing. +Version 0.5.0 — protocol and C++ core validated against a real `hermes-webui` +(login, sessions, streaming chat, TTS, STT, voice dialog, profile selection); +UI in English, Italian, French and German; on-device testing still ongoing. diff --git a/docs/BUILD.md b/docs/BUILD.md index 54e9ca1..591b915 100644 --- a/docs/BUILD.md +++ b/docs/BUILD.md @@ -76,12 +76,12 @@ richiede il probe WebUI su :8899). Esito 12/09/2026: unit 9/9, live completo OK. ```bash cd ~/workspace -tar czf harbour-hermes-0.4.0.tar.gz \ - --transform 's,^harbour-hermes,harbour-hermes-0.4.0,' \ +tar czf harbour-hermes-0.5.0.tar.gz \ + --transform 's,^harbour-hermes,harbour-hermes-0.5.0,' \ --exclude='.git' harbour-hermes ``` -Lo spec si aspetta la directory `harbour-hermes-0.4.0/` (pattern degli altri +Lo spec si aspetta la directory `harbour-hermes-0.5.0/` (pattern degli altri progetti: cercato in modo robusto anche per i sorgenti live di sfdk). ## Traduzioni diff --git a/docs/PROTOCOL.md b/docs/PROTOCOL.md index a387e4d..dd77abc 100644 --- a/docs/PROTOCOL.md +++ b/docs/PROTOCOL.md @@ -173,7 +173,28 @@ Annullamento: `GET /api/chat/cancel?stream_id=ID` → - Engine: `edge` (default, gratis), `elevenlabs`, `openai` (richiedono chiavi configurate sul server). `browser` è solo client-side, non usabile qui. -## 5. Note operative +## 5. Profili (selezione dell'utente) + +Le sessioni del WebUI appartengono a un *profilo* (campo `profile` nel payload +di `/api/sessions`). Il client sceglie di quale profilo vedere le sessioni con +lo switch per-client: + +| Endpoint | Metodo | Note | +|---|---|---| +| `/api/profiles` | GET | `{"profiles": [{"name", "path", "is_default", "is_active", ...}], "active": "..."}` | +| `/api/profile/switch` | POST | body `{"name": "carlo"}` → `200` + `Set-Cookie: hermes_profile=.` | + +Il cookie `hermes_profile` e' **firmato lato server** (HMAC legato al token di +sessione, anti-forging): il client non puo' inventarlo, deve riceverlo dallo +switch e conservarlo nel cookie jar. Da quel momento **tutte** le richieste +dell'app sono profilate lato server (`server.py` applica il profilo +per-request dal cookie). Lo switch usa `process_wide=False`: **il profilo +attivo globale del server non viene toccato** (la vista del browser resta la +sua). Per tornare al profilo attivo del server basta rimuovere il cookie. +Validato contro il server reale (2026-09: switch → 200 + Set-Cookie firmato; +profilo inesistente → 404 `Profile 'x' does not exist`). + +## 6. Note operative - Tutti gli errori API sono JSON `{"error": "..."}`. - Il server non espone CORS: il client nativo non serve preflight. diff --git a/qml/pages/SettingsPage.qml b/qml/pages/SettingsPage.qml index 31777bc..60bb3c4 100644 --- a/qml/pages/SettingsPage.qml +++ b/qml/pages/SettingsPage.qml @@ -8,6 +8,9 @@ Page { // impostazioni (ttsVoices/ttsEngines) e il valore corrente è sempre // incluso. loading evita di salvare durante il caricamento iniziale. property bool loading: true + // Feedback del selettore profilo (mostrato sotto il ComboBox). + property string profileStatusText: "" + property bool profileStatusError: false SilicaFlickable { anchors.fill: parent @@ -34,6 +37,34 @@ Page { } } + SectionHeader { text: qsTr("Sessions") } + + ComboBox { + label: qsTr("Profile") + currentIndex: page.profileIndex() + // Stesso pattern dei selettori voce (Repeater, provato in tide). + menu: ContextMenu { + Repeater { + model: page.profileChoices().length + delegate: MenuItem { + text: page.profileChoices()[index] + onClicked: api.switchProfile(page.profileValue(index)) + } + } + } + } + + Label { + x: Theme.horizontalPageMargin + width: parent.width - 2 * Theme.horizontalPageMargin + font.pixelSize: Theme.fontSizeExtraSmall + color: page.profileStatusError ? Theme.highlightColor + : Theme.secondaryHighlightColor + wrapMode: Text.WordWrap + visible: text.length > 0 + text: page.profileStatusText + } + SectionHeader { text: qsTr("Voice") } ComboBox { @@ -160,11 +191,60 @@ Page { VerticalScrollDecorator {} } + Connections { + target: api + onProfileSwitched: { + page.profileStatusError = false + page.profileStatusText = name.length > 0 + ? qsTr("Profile: %1").arg(name) + : qsTr("Server active profile") + statusClear.restart() + } + onProfileSwitchFailed: { + page.profileStatusError = true + page.profileStatusText = error + statusClear.restart() + } + } + + Timer { + id: statusClear + interval: 4000 + onTriggered: page.profileStatusText = "" + } + function signOut() { api.logout() pageStack.replace(Qt.resolvedUrl("LoginPage.qml")) } + // Valori del selettore profilo: "" (attivo sul server) + profili del + // server (da /api/profiles); il valore corrente e' sempre incluso. + function profileChoices() { + var out = [qsTr("(server active)")] + var list = api.profiles + for (var i = 0; i < list.length; i++) + out.push(list[i]) + var cur = appSettings.profile + if (cur.length > 0 && out.indexOf(cur) < 0) + out.push(cur) + return out + } + + function profileValue(i) { + if (i <= 0) + return "" + return page.profileChoices()[i] + } + + function profileIndex() { + var cur = appSettings.profile + if (cur.length === 0) + return 0 + var i = page.profileChoices().indexOf(cur) + return i < 0 ? 0 : i + } + // Elenchi dei selettori: preset configurati + valore corrente (così resta // selezionabile anche se non è tra i preset). Indice 0 = "(server default)". function voiceChoices() { @@ -204,5 +284,6 @@ Page { voicesField.text = appSettings.ttsVoices.join(", ") enginesField.text = appSettings.ttsEngines.join(", ") page.loading = false + api.fetchProfiles() } } diff --git a/rpm/harbour-hermes.spec b/rpm/harbour-hermes.spec index 600ee40..3bc27ec 100644 --- a/rpm/harbour-hermes.spec +++ b/rpm/harbour-hermes.spec @@ -1,6 +1,6 @@ Name: harbour-hermes Summary: Client for the Hermes Web UI with voice -Version: 0.4.0 +Version: 0.5.0 Release: 1 Group: Qt/Qt License: MIT @@ -25,6 +25,13 @@ Sessions list, chat with live streaming replies, voice dictation (server-side TTS), plus a hands-free conversation mode. %changelog +* Sat Sep 12 2026 Carlo Baratto - 0.5.0-1 +- Sezione Sessions nelle Impostazioni: selettore profilo (le sessioni + mostrate sono quelle del profilo scelto). Switch per-client via + /api/profile/switch con cookie firmato — il profilo attivo globale del + server NON viene toccato; elenco profili da /api/profiles; opzione + "(attivo sul server)" che rimuove il cookie; reapply del profilo dopo + il login * Sat Sep 12 2026 Carlo Baratto - 0.4.0-1 - Localizzazione dell'interfaccia: italiano, inglese (sorgente), francese e tedesco (78 stringhe per lingua); .qm generati con diff --git a/src/apiclient.cpp b/src/apiclient.cpp index 1ed04ee..0d58049 100644 --- a/src/apiclient.cpp +++ b/src/apiclient.cpp @@ -1,6 +1,7 @@ #include "apiclient.h" #include +#include #include #include #include @@ -29,6 +30,9 @@ ApiClient::ApiClient(Settings *settings, QObject *parent) m_jar->setFilePath(cfg + QStringLiteral("/cookies.dat")); m_jar->load(); m_nam.setCookieJar(m_jar); + + // Il profilo selezionato (impostazione app) e' esposto anche da ApiClient. + connect(m_settings, &Settings::profileChanged, this, &ApiClient::profileChanged); } // ---------------------------------------------------------------- helpers -- @@ -170,7 +174,12 @@ void ApiClient::login(const QString &password) return; } setLoggedIn(true); - refreshSessions(); + // Riapplica il profilo scelto dall'utente: il nuovo cookie di sessione + // va rilegato al profilo (switchProfile aggiorna anche le sessioni). + if (!m_settings->profile().isEmpty()) + switchProfile(m_settings->profile()); + else + refreshSessions(); }); } @@ -221,6 +230,74 @@ int ApiClient::sessionsCount() const return m_sessionsModel->rowCount(); } +QString ApiClient::profile() const +{ + return m_settings->profile(); +} + +void ApiClient::fetchProfiles() +{ + QNetworkReply *reply = m_nam.get(jsonRequest(QStringLiteral("/api/profiles"))); + connect(reply, &QNetworkReply::finished, this, [this, reply]() { + reply->deleteLater(); + const QByteArray data = reply->readAll(); + const int status = reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt(); + if (status < 200 || status >= 300) { + qDebug() << "[api] fetchProfiles HTTP" << status; + return; + } + const QJsonArray arr = QJsonDocument::fromJson(data).object() + .value(QStringLiteral("profiles")).toArray(); + QStringList names; + for (int i = 0; i < arr.size(); ++i) { + const QString n = arr.at(i).toObject().value(QStringLiteral("name")).toString(); + if (!n.isEmpty()) + names << n; + } + qDebug() << "[api] profili disponibili:" << names; + if (names != m_profiles) { + m_profiles = names; + emit profilesChanged(); + } + }); +} + +void ApiClient::switchProfile(const QString &name) +{ + const QString target = name.trimmed(); + if (target.isEmpty()) { + // "(attivo sul server)": rimuove il cookie di profilo e torna a + // seguire il profilo attivo del server. + m_jar->removeCookiesByName(QStringLiteral("hermes_profile")); + m_settings->setProfile(QString()); + emit profileSwitched(QString()); + refreshSessions(); + return; + } + + QJsonObject body; + body.insert(QStringLiteral("name"), target); + QNetworkReply *reply = m_nam.post(jsonRequest(QStringLiteral("/api/profile/switch")), + QJsonDocument(body).toJson(QJsonDocument::Compact)); + connect(reply, &QNetworkReply::finished, this, [this, reply, target]() { + reply->deleteLater(); + const QByteArray data = reply->readAll(); + const int status = reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt(); + if (status >= 200 && status < 300) { + // Il Set-Cookie firmato (hermes_profile) viene salvato dal jar: + // da qui in poi le richieste dell'app sono profilate lato server. + m_settings->setProfile(target); + qDebug() << "[api] profilo selezionato:" << target; + emit profileSwitched(target); + refreshSessions(); + } else { + const QString err = errorFromReply(reply, data); + qDebug() << "[api] switch profilo fallito:" << err; + emit profileSwitchFailed(err); + } + }); +} + void ApiClient::openSession(const QString &id) { setLastError(QString()); diff --git a/src/apiclient.h b/src/apiclient.h index 07e83a3..9b06929 100644 --- a/src/apiclient.h +++ b/src/apiclient.h @@ -40,6 +40,8 @@ class ApiClient : public QObject Q_PROPERTY(SessionsModel *sessions READ sessions NOTIFY sessionsChanged) Q_PROPERTY(int sessionsCount READ sessionsCount NOTIFY sessionsChanged) Q_PROPERTY(QVariantMap session READ session NOTIFY sessionChanged) + Q_PROPERTY(QStringList profiles READ profiles NOTIFY profilesChanged) + Q_PROPERTY(QString profile READ profile NOTIFY profileChanged) public: explicit ApiClient(Settings *settings, QObject *parent = nullptr); @@ -52,6 +54,8 @@ public: SessionsModel *sessions() const { return m_sessionsModel; } int sessionsCount() const; QVariantMap session() const { return m_session; } + QStringList profiles() const { return m_profiles; } + QString profile() const; // Avvio: verifica lo stato auth (cookie persistito o auth disabilitata). Q_INVOKABLE void checkAuth(); @@ -59,6 +63,9 @@ public: Q_INVOKABLE void logout(); Q_INVOKABLE void refreshSessions(); + // Profilo server: elenco e selezione (per-client, cookie firmato WebUI). + Q_INVOKABLE void fetchProfiles(); + Q_INVOKABLE void switchProfile(const QString &name); Q_INVOKABLE void openSession(const QString &id); Q_INVOKABLE void newSession(); @@ -80,6 +87,10 @@ signals: void lastErrorChanged(); void sessionsChanged(); void sessionChanged(); + void profilesChanged(); + void profileChanged(); + void profileSwitched(const QString &name); // ok (vuoto = attivo server) + void profileSwitchFailed(const QString &error); void authChecked(bool loggedIn); void sessionOpened(); // openSession/newSession completata @@ -125,6 +136,7 @@ private: SessionsModel *m_sessionsModel; QVariantMap m_session; + QStringList m_profiles; QString m_streamId; QNetworkReply *m_streamReply; SseParser m_parser; diff --git a/src/cookiejar.cpp b/src/cookiejar.cpp index bc524ac..79e5fe3 100644 --- a/src/cookiejar.cpp +++ b/src/cookiejar.cpp @@ -63,6 +63,21 @@ void CookieJar::clear() QFile::remove(m_path); } +void CookieJar::removeCookiesByName(const QString &name) +{ + const QByteArray raw = name.toUtf8(); + const QList cookies = allCookies(); + bool removed = false; + for (int i = 0; i < cookies.size(); ++i) { + if (cookies.at(i).name() == raw) { + deleteCookie(cookies.at(i)); // protected: accessibile dal subclass + removed = true; + } + } + if (removed) + save(); +} + bool CookieJar::setCookiesFromUrl(const QList &cookieList, const QUrl &url) { const bool changed = QNetworkCookieJar::setCookiesFromUrl(cookieList, url); diff --git a/src/cookiejar.h b/src/cookiejar.h index 2db6a63..7de8831 100644 --- a/src/cookiejar.h +++ b/src/cookiejar.h @@ -21,6 +21,9 @@ public: bool save() const; void clear(); + // Rimuove tutti i cookie con questo nome (es. hermes_profile) e salva. + void removeCookiesByName(const QString &name); + protected: bool setCookiesFromUrl(const QList &cookieList, const QUrl &url) override; diff --git a/src/main.cpp b/src/main.cpp index cdb4471..c66d9dc 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -21,9 +21,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.4.0")); + app->setApplicationVersion(QStringLiteral("0.5.0")); - qDebug() << "harbour-hermes v0.4.0 build" << __DATE__ << __TIME__; + qDebug() << "harbour-hermes v0.5.0 build" << __DATE__ << __TIME__; // Traduzioni: it/fr/de da /usr/share/harbour-hermes/translations, in base // alla lingua di sistema (l'inglese e' la lingua sorgente: nessun .qm). diff --git a/src/settings.cpp b/src/settings.cpp index fed9845..d6ace6e 100644 --- a/src/settings.cpp +++ b/src/settings.cpp @@ -123,6 +123,22 @@ void Settings::setVadMaxMs(int ms) emit vadMaxMsChanged(); } +// Profilo server di cui mostrare le sessioni (vuoto = profilo attivo del +// server). L'app lo applica per-client tramite il cookie firmato del WebUI. +QString Settings::profile() const +{ + return m_settings.value(QStringLiteral("profile"), QString()).toString(); +} + +void Settings::setProfile(const QString &name) +{ + const QString clean = name.trimmed(); + if (clean == profile()) + return; + m_settings.setValue(QStringLiteral("profile"), clean); + emit profileChanged(); +} + QStringList Settings::ttsVoices() const { return m_settings.value(QStringLiteral("ttsVoices")).toStringList(); diff --git a/src/settings.h b/src/settings.h index 3d5fa41..fac5c16 100644 --- a/src/settings.h +++ b/src/settings.h @@ -28,6 +28,8 @@ class Settings : public QObject NOTIFY vadThresholdChanged) Q_PROPERTY(int vadMaxMs READ vadMaxMs WRITE setVadMaxMs NOTIFY vadMaxMsChanged) + Q_PROPERTY(QString profile READ profile WRITE setProfile + NOTIFY profileChanged) Q_PROPERTY(QStringList ttsVoices READ ttsVoices WRITE setTtsVoices NOTIFY ttsVoicesChanged) Q_PROPERTY(QStringList ttsEngines READ ttsEngines WRITE setTtsEngines @@ -62,6 +64,10 @@ public: int vadMaxMs() const; void setVadMaxMs(int ms); + // Profilo server di cui mostrare le sessioni (vuoto = attivo del server). + QString profile() const; + void setProfile(const QString &name); + // Preset per i selettori voce/engine (liste configurabili, default vuote). QStringList ttsVoices() const; void setTtsVoices(const QStringList &list); @@ -81,6 +87,7 @@ signals: void vadSilenceMsChanged(); void vadThresholdChanged(); void vadMaxMsChanged(); + void profileChanged(); void ttsVoicesChanged(); void ttsEnginesChanged(); void defaultWorkspaceChanged(); diff --git a/translations/harbour-hermes-de.ts b/translations/harbour-hermes-de.ts index 42a4fca..845c50b 100644 --- a/translations/harbour-hermes-de.ts +++ b/translations/harbour-hermes-de.ts @@ -4,17 +4,17 @@ ApiClient - + Authentication required Authentifizierung erforderlich - + HTTP %1 HTTP %1 - + Server address not configured Serveradresse nicht konfiguriert @@ -264,141 +264,166 @@ SettingsPage - + Settings Einstellungen - + Server Server - + Web UI address Web-UI-Adresse - + + Sessions + Sitzungen + + + + Profile + Profil + + + Voice Stimme - + Read-aloud voice Vorlese-Stimme - - + + (server default) (Server-Standard) - + Voice presets (comma-separated) Stimm-Presets (durch Kommas getrennt) - + TTS engine TTS-Engine - + Engine presets (comma-separated) Engine-Presets (durch Kommas getrennt) - + Read replies aloud Antworten vorlesen - + Speak every completed reply automatically Jede fertige Antwort automatisch vorlesen - + Voice dialog Sprachdialog - + Send right after dictation Direkt nach dem Diktat senden - + Send the transcribed text automatically, without tapping Send Den transkribierten Text automatisch senden, ohne Senden zu drücken - + Auto-stop pause Auto-Stopp-Pause - + 1.0 second 1,0 Sekunden - + 1.5 seconds 1,5 Sekunden - + 2.0 seconds 2,0 Sekunden - + 3.0 seconds 3,0 Sekunden - + Mic sensitivity Mikrofon-Empfindlichkeit - + High (even soft voices) Hoch (auch leise Stimmen) - + Medium Mittel - + Low (noisy rooms) Niedrig (laute Umgebung) - + Account Konto - + Sign out Abmelden - + About Über - + Native client for the Hermes Web UI (hermes-webui): sessions, live streaming chat, server-side voice dictation and read-aloud. Nativer Client für die Hermes Web UI (hermes-webui): Sitzungen, Live-Streaming-Chat, server-seitiges Diktat und Vorlesen. + + + Profile: %1 + Profil: %1 + + + + Server active profile + Aktives Serverprofil + + + + (server active) + (aktives Serverprofil) + VoiceRecorder diff --git a/translations/harbour-hermes-fr.ts b/translations/harbour-hermes-fr.ts index 9a2d690..340c90e 100644 --- a/translations/harbour-hermes-fr.ts +++ b/translations/harbour-hermes-fr.ts @@ -4,17 +4,17 @@ ApiClient - + Authentication required Authentification requise - + HTTP %1 HTTP %1 - + Server address not configured Adresse du serveur non configurée @@ -264,141 +264,166 @@ SettingsPage - + Settings Paramètres - + Server Serveur - + Web UI address Adresse de la Web UI - + + Sessions + Sessions + + + + Profile + Profil + + + Voice Voix - + Read-aloud voice Voix de lecture - - + + (server default) (défaut du serveur) - + Voice presets (comma-separated) Préréglages de voix (séparés par des virgules) - + TTS engine Moteur TTS - + Engine presets (comma-separated) Préréglages de moteurs (séparés par des virgules) - + Read replies aloud Lire les réponses à voix haute - + Speak every completed reply automatically Prononcer automatiquement chaque réponse terminée - + Voice dialog Dialogue vocal - + Send right after dictation Envoyer juste après la dictée - + Send the transcribed text automatically, without tapping Send Envoyer automatiquement le texte transcrit, sans appuyer sur Envoyer - + Auto-stop pause Pause d'arrêt auto - + 1.0 second 1,0 seconde - + 1.5 seconds 1,5 seconde - + 2.0 seconds 2,0 secondes - + 3.0 seconds 3,0 secondes - + Mic sensitivity Sensibilité du micro - + High (even soft voices) Élevée (voix faibles) - + Medium Moyenne - + Low (noisy rooms) Faible (pièces bruyantes) - + Account Compte - + Sign out Se déconnecter - + About À propos - + Native client for the Hermes Web UI (hermes-webui): sessions, live streaming chat, server-side voice dictation and read-aloud. Client natif pour l'Hermes Web UI (hermes-webui) : sessions, chat en streaming, dictée et lecture à voix haute côté serveur. + + + Profile: %1 + Profil : %1 + + + + Server active profile + Profil actif sur le serveur + + + + (server active) + (actif sur le serveur) + VoiceRecorder diff --git a/translations/harbour-hermes-it.ts b/translations/harbour-hermes-it.ts index e236d53..49fb60e 100644 --- a/translations/harbour-hermes-it.ts +++ b/translations/harbour-hermes-it.ts @@ -4,17 +4,17 @@ ApiClient - + Authentication required Autenticazione richiesta - + HTTP %1 HTTP %1 - + Server address not configured Indirizzo del server non configurato @@ -264,141 +264,166 @@ SettingsPage - + Settings Impostazioni - + Server Server - + Web UI address Indirizzo Web UI - + + Sessions + Sessioni + + + + Profile + Profilo + + + Voice Voce - + Read-aloud voice Voce di lettura - - + + (server default) (default del server) - + Voice presets (comma-separated) Preset voci (separati da virgola) - + TTS engine Motore TTS - + Engine presets (comma-separated) Preset motori (separati da virgola) - + Read replies aloud Leggi le risposte ad alta voce - + Speak every completed reply automatically Pronuncia automaticamente ogni risposta completata - + Voice dialog Dialogo vocale - + Send right after dictation Invia subito dopo la dettatura - + Send the transcribed text automatically, without tapping Send Invia il testo trascritto automaticamente, senza premere Invia - + Auto-stop pause Pausa di stop automatico - + 1.0 second 1,0 secondi - + 1.5 seconds 1,5 secondi - + 2.0 seconds 2,0 secondi - + 3.0 seconds 3,0 secondi - + Mic sensitivity Sensibilità microfono - + High (even soft voices) Alta (anche voci flebili) - + Medium Media - + Low (noisy rooms) Bassa (ambienti rumorosi) - + Account Account - + Sign out Esci - + About Informazioni - + Native client for the Hermes Web UI (hermes-webui): sessions, live streaming chat, server-side voice dictation and read-aloud. Client nativo per l'Hermes Web UI (hermes-webui): sessioni, chat in streaming, dettatura e lettura ad alta voce lato server. + + + Profile: %1 + Profilo: %1 + + + + Server active profile + Profilo attivo sul server + + + + (server active) + (attivo sul server) + VoiceRecorder