From 3f838fc31a2ae7995600c3dbcaf5335647ecf462 Mon Sep 17 00:00:00 2001 From: nesquena-hermes Date: Wed, 29 Apr 2026 21:34:27 -0700 Subject: [PATCH] release: v0.50.244 (#1308) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit release: v0.50.244 Batch release of 4 PRs: - #1303 (@fecolinhares) — TTS playback of agent responses via Web Speech API. Per-message speaker button + auto-read toggle + voice/rate/pitch in Settings. localStorage-only state. Closes #499. - #1304 — Stale saved session 404 cleanup + structured api() errors. Salvaged from #1084. Independently approved on 358275e. - #1306 — Cmd/Ctrl+K works while a conversation is busy. Salvaged from #1084. Independently approved on 2e8a239. - #1307 — Sienna skin (warm clay & sand earth palette). Salvaged from #1084. Independently approved on 5cd79c8. Tests: 3290 passed, 2 skipped, 3 xpassed, 0 failures (was 3254; +36 tests). Independently reviewed and approved by nesquena (commit 47f0e0d). End-to-end trace verified the TTS flow; security audit confirmed SpeechSynthesisUtterance is plain-text-only with no XSS surface; behavioural harness confirmed _stripForTTS handles all 12 markdown-stripping cases; bounds clamping on rate/pitch verified; opt-in behavior verified. --- CHANGELOG.md | 10 ++ static/boot.js | 11 +- static/i18n.js | 92 +++++++++- static/index.html | 37 +++- static/messages.js | 6 + static/panels.js | 47 ++++++ static/sessions.js | 8 + static/style.css | 65 ++++++++ static/ui.js | 115 ++++++++++++- static/workspace.js | 11 +- tests/test_1038_pwa_auth_redirect.py | 10 +- tests/test_499_tts_playback.py | 195 ++++++++++++++++++++++ tests/test_mobile_layout.py | 26 ++- tests/test_sienna_skin.py | 79 +++++++++ tests/test_stale_empty_session_restore.py | 83 +++++++++ 15 files changed, 785 insertions(+), 10 deletions(-) create mode 100644 tests/test_499_tts_playback.py create mode 100644 tests/test_sienna_skin.py create mode 100644 tests/test_stale_empty_session_restore.py diff --git a/CHANGELOG.md b/CHANGELOG.md index 93373894..7d40ac1c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,16 @@ ### Fixed +## [v0.50.244] — 2026-04-30 + +### Added +- **Text-to-Speech playback for agent responses** — Web Speech API powers a per-message 🔊 speaker button on every assistant message, plus an optional auto-read toggle that speaks each response when streaming finishes. Voice / rate / pitch controls are exposed in Settings → Preferences. All TTS preferences are stored in `localStorage` (no server round-trip). Strips markdown, code blocks, and `MEDIA:` paths before speaking; pauses synthesis when the composer is focused. Opt-in — TTS is hidden by default until enabled in Settings. Locale coverage for en, ru, es, de, zh, zh-Hant, pt, ko. (`static/ui.js`, `static/panels.js`, `static/messages.js`, `static/boot.js`, `static/style.css`, `static/index.html`, `static/i18n.js`) @fecolinhares — PR #1303, closes #499 +- **Sienna skin (warm clay & sand earth palette)** — opt-in alongside the existing default/Ares/Mono/Slate/Poseidon/Sisyphus/Charizard set. Full palette rewrite (light + dark variants) with clay accent (`#D97757`) on a soft sand background; neutral tool-card chrome, accent-tinted active session indicator. No forced migration, default skin stays `default` (gold); users opt in via Settings → Skin. (`static/style.css`, `static/boot.js`, `static/index.html`, `tests/test_sienna_skin.py`) — PR #1307 (salvaged from #1084) + +### Fixed +- **Cmd/Ctrl+K new chat works while a conversation is busy** — drops the `!S.busy` guard so users can start a new conversation mid-stream. The in-flight stream keeps running on its own session; the user just gets a fresh blank one. (`static/boot.js`, `tests/test_mobile_layout.py`) — PR #1306 (salvaged from #1084) +- **Stale saved session 404 cleanup + structured `api()` errors** — when a saved session ID returns 404, `loadSession()` now clears `localStorage.hermes-webui-session` and rethrows so boot can fall through to the empty state instead of sticking on "Session not available in web UI." across reloads. The cleanup is gated on `!currentSid` so click-into-404 doesn't wipe state. The global `api()` helper now attaches `.status` / `.statusText` / `.body` to thrown errors, so callers can branch on HTTP status without re-parsing the message string. (`static/sessions.js`, `static/workspace.js`, `tests/test_stale_empty_session_restore.py`, `tests/test_1038_pwa_auth_redirect.py`) — PR #1304 (salvaged from #1084) + ## [v0.50.243] — 2026-04-30 ### Fixed diff --git a/static/boot.js b/static/boot.js index e6a1d267..5ef00292 100644 --- a/static/boot.js +++ b/static/boot.js @@ -531,7 +531,12 @@ document.addEventListener('keydown',async e=>{ // If the current session has no messages, just focus the composer rather than // creating another empty session that will clutter the sidebar list (#1171). if(S.session&&(S.session.message_count||0)===0){$('msg').focus();return;} - if(!S.busy){await newSession();await renderSessionList();closeMobileSidebar();$('msg').focus();} + // Cmd/Ctrl+K should always create a new conversation, even while the current + // one is still streaming. The old !S.busy guard meant users had to wait for + // a long generation to finish before they could start something new — exactly + // the moment they want to switch context. newSession() leaves the in-flight + // stream running on its own session; the user just gets a fresh blank one. + await newSession();await renderSessionList();closeMobileSidebar();$('msg').focus(); } if(e.key==='Escape'){ // Close onboarding overlay if open (skip/dismiss the wizard) @@ -639,6 +644,7 @@ const _SKINS=[ {name:'Poseidon', colors:['#0EA5E9','#0284C7','#0369A1']}, {name:'Sisyphus', colors:['#A78BFA','#8B5CF6','#7C3AED']}, {name:'Charizard',colors:['#FB923C','#F97316','#EA580C']}, + {name:'Sienna', colors:['#D97757','#C06A49','#9A523A']}, ]; const _VALID_THEMES=new Set((_THEMES||[]).map(t=>t.value)); const _VALID_SKINS=new Set((_SKINS||[]).map(s=>s.name.toLowerCase())); @@ -845,6 +851,8 @@ function applyBotName(){ if(typeof applyLocaleToDOM==='function')applyLocaleToDOM(); } applyBotName(); + // TTS: apply enabled state on boot so buttons show/hide correctly (#499) + if(typeof _applyTtsEnabled==='function') _applyTtsEnabled(localStorage.getItem('hermes-tts-enabled')==='true'); }catch(e){ window._sendKey='enter'; window._showTokenUsage=false; @@ -865,6 +873,7 @@ function applyBotName(){ if(typeof applyLocaleToDOM==='function')applyLocaleToDOM(); } applyBotName(); + if(typeof _applyTtsEnabled==='function') _applyTtsEnabled(localStorage.getItem('hermes-tts-enabled')==='true'); } // Non-blocking update check (fire-and-forget, once per tab session) // ?test_updates=1 in URL forces banner display for testing (bypasses sessionStorage guards) diff --git a/static/i18n.js b/static/i18n.js index 628ee285..9ad3f2ed 100644 --- a/static/i18n.js +++ b/static/i18n.js @@ -441,6 +441,17 @@ const LOCALES = { // Settings detail settings_label_sound: 'Notification sound', settings_desc_sound: 'Play a sound when the assistant finishes a response.', + // TTS (#499) + tts_listen: 'Listen', + tts_not_supported: 'Speech synthesis not supported in this browser.', + settings_label_tts: 'Text-to-Speech for responses', + settings_desc_tts: "Show a speaker button on each assistant message to read it aloud using your browser's speech synthesis.", + settings_label_tts_auto_read: 'Auto-read responses aloud', + settings_desc_tts_auto_read: 'Automatically speak each new assistant response when it finishes. Pauses when you start typing.', + settings_label_tts_voice: 'Voice', + settings_desc_tts_voice: "Preferred voice. Populated from your browser's available voices.", + settings_label_tts_rate: 'Speech rate', + settings_label_tts_pitch: 'Speech pitch', settings_label_notifications: 'Browser notifications', settings_desc_notifications: 'Show a system notification when a response completes while the app is in the background.', settings_desc_token_usage: 'Displays input/output token count below each assistant reply. Also toggled with /usage.', @@ -1511,6 +1522,17 @@ const LOCALES = { html_error: 'Не удалось загрузить предпросмотр HTML', html_open_full: 'Открыть на всю страницу', html_sandbox_label: 'Предпросмотр HTML', + // TTS (#499) + tts_listen: 'Прослушать', + tts_not_supported: 'Синтез речи не поддерживается', + settings_label_tts: 'Синтез речи для ответов', + settings_desc_tts: 'Показать кнопку динамика на сообщениях ассистента', + settings_label_tts_auto_read: 'Авто-чтение ответов', + settings_desc_tts_auto_read: 'Автоматически озвучивать ответы ассистента', + settings_label_tts_voice: 'Голос', + settings_desc_tts_voice: 'Выберите голос для синтеза речи', + settings_label_tts_rate: 'Скорость речи', + settings_label_tts_pitch: 'Тон речи', }, es: { @@ -2231,6 +2253,17 @@ const LOCALES = { html_error: 'Error al cargar la vista previa de HTML', html_open_full: 'Abrir página completa', html_sandbox_label: 'Vista previa de HTML', + // TTS (#499) + tts_listen: 'Escuchar', + tts_not_supported: 'Síntesis de voz no disponible', + settings_label_tts: 'Texto a voz para respuestas', + settings_desc_tts: 'Mostrar botón de altavoz en mensajes del asistente', + settings_label_tts_auto_read: 'Leer respuestas automáticamente', + settings_desc_tts_auto_read: 'Leer en voz alta las respuestas del asistente automáticamente', + settings_label_tts_voice: 'Voz', + settings_desc_tts_voice: 'Seleccionar voz para síntesis de voz', + settings_label_tts_rate: 'Velocidad de voz', + settings_label_tts_pitch: 'Tono de voz', }, de: { @@ -2955,7 +2988,18 @@ const LOCALES = { html_error: 'HTML-Vorschau konnte nicht geladen werden', html_open_full: 'Vollständige Seite öffnen', html_sandbox_label: 'HTML-Vorschau', -}, + // TTS (#499) + tts_listen: 'Anhören', + tts_not_supported: 'Sprachsynthese nicht verfügbar', + settings_label_tts: 'Text-zu-Sprache für Antworten', + settings_desc_tts: 'Lautsprecher-Symbol auf Assistenten-Nachrichten anzeigen', + settings_label_tts_auto_read: 'Antworten automatisch vorlesen', + settings_desc_tts_auto_read: 'Assistenten-Antworten automatisch vorlesen', + settings_label_tts_voice: 'Stimme', + settings_desc_tts_voice: 'Stimme für Sprachsynthese auswählen', + settings_label_tts_rate: 'Sprechgeschwindigkeit', + settings_label_tts_pitch: 'Tonhöhe', + }, zh: { _lang: 'zh', @@ -3673,6 +3717,17 @@ const LOCALES = { excalidraw_empty: '空图表', excalidraw_render_error: '渲染图表失败', excalidraw_simplified: '简化 SVG 预览 — 与 Excalidraw 画布不完全相同', + // TTS (#499) + tts_listen: '收听', + tts_not_supported: '语音合成不可用', + settings_label_tts: '回复语音合成', + settings_desc_tts: '在助手消息上显示扬声器按钮', + settings_label_tts_auto_read: '自动朗读回复', + settings_desc_tts_auto_read: '自动朗读助手回复', + settings_label_tts_voice: '语音', + settings_desc_tts_voice: '选择语音合成声音', + settings_label_tts_rate: '语速', + settings_label_tts_pitch: '音调', }, // Traditional Chinese (zh-Hant) @@ -4499,6 +4554,17 @@ const LOCALES = { html_error: 'HTML 預覽載入失敗', html_open_full: '開啟完整頁面', html_sandbox_label: 'HTML 預覽', + // TTS (#499) + tts_listen: '收聽', + tts_not_supported: '語音合成無法使用', + settings_label_tts: '回覆語音合成', + settings_desc_tts: '在助手訊息上顯示喇叭按鈕', + settings_label_tts_auto_read: '自動朗讀回覆', + settings_desc_tts_auto_read: '自動朗讀助手回覆', + settings_label_tts_voice: '語音', + settings_desc_tts_voice: '選擇語音合成聲音', + settings_label_tts_rate: '語速', + settings_label_tts_pitch: '音調', }, @@ -5138,7 +5204,18 @@ const LOCALES = { approval_skip: 'Pular', approval_skip_title: 'Pular este prompt de aprovação', approval_skip_all: 'Pular todos', - approval_skip_all_title: 'Pular todos prompts de aprovação nesta sessão' + approval_skip_all_title: 'Pular todos prompts de aprovação nesta sessão', + // TTS (#499) + tts_listen: 'Ouvir', + tts_not_supported: 'Síntese de voz não disponível', + settings_label_tts: 'Texto para voz nas respostas', + settings_desc_tts: 'Mostrar botão de alto-falante nas mensagens do assistente', + settings_label_tts_auto_read: 'Ler respostas automaticamente', + settings_desc_tts_auto_read: 'Ler automaticamente as respostas do assistente', + settings_label_tts_voice: 'Voz', + settings_desc_tts_voice: 'Selecionar voz para síntese de voz', + settings_label_tts_rate: 'Velocidade da fala', + settings_label_tts_pitch: 'Tom da fala', }, ko: { _lang: 'ko', @@ -5911,6 +5988,17 @@ const LOCALES = { excalidraw_empty: '빈 다이어그램', excalidraw_render_error: '다이어그램 렌더링 실패', excalidraw_simplified: '단순화된 SVG 미리보기 — Excalidraw 캔버스와 픽셀 동일하지 않음', + // TTS (#499) + tts_listen: '듣기', + tts_not_supported: '음성 합성을 사용할 수 없습니다', + settings_label_tts: '답변 음성 합성', + settings_desc_tts: '도움말 메시지에 스피커 버튼 표시', + settings_label_tts_auto_read: '답변 자동 읽기', + settings_desc_tts_auto_read: '도움말 답변을 자동으로 읽어줌', + settings_label_tts_voice: '음성', + settings_desc_tts_voice: '음성 합성 음성 선택', + settings_label_tts_rate: '말 속도', + settings_label_tts_pitch: '말 톤', }, }; diff --git a/static/index.html b/static/index.html index 50d5a59e..b4441ceb 100644 --- a/static/index.html +++ b/static/index.html @@ -15,7 +15,7 @@ - + @@ -683,6 +683,41 @@
Play a sound when the assistant finishes a response.
+
+ +
Show a speaker button on each assistant message to read it aloud using your browser's speech synthesis.
+
+
+ +
Automatically speak each new assistant response when it finishes. Pauses when you start typing.
+
+
+ + +
Preferred voice. Populated from your browser's available voices.
+
+
+ +
+ + 1.0x +
+
+
+ +
+ + 1.0 +
+