From caa9162f28b8428ddf8e6173a87c06a10631357f Mon Sep 17 00:00:00 2001 From: ekko <152005280+EKKOLearnAI@users.noreply.github.com> Date: Sat, 2 May 2026 13:26:57 +0800 Subject: [PATCH] feat(chat): add voice playback with auto-play and visual effects (#396) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## Features ### Core Functionality - **Web Speech API Integration**: Add TTS (text-to-speech) playback for assistant messages - **Manual Playback**: Click-to-play button next to each assistant message (🔊 icon) - **Auto-play Mode**: Toggle switch in input bar to auto-play responses - **Playback Controls**: Play/pause/stop with visual feedback ### User Interface - **Playback Button**: Hover-activated button in message meta area (next to copy button) - **Auto-play Switch**: Voice icon toggle in input top bar with state persistence - **Mobile Optimization**: Buttons always visible on mobile (≤768px width) - **Visual Feedback**: - Rainbow glowing border during playback (2px border, 10px/20px glow) - 4-second animation cycle through 6 colors - Play/pause icon toggle ### Voice Customization - **Pitch/Rate Control**: Low-pitched (0.5) fast-speaking (1.2) "male voice" - **Auto Voice Selection**: Attempts to select male voices across platforms (macOS: Yaoyao, Windows: David/Daniel) - **Platform Compatibility**: Works with system-provided voices on macOS, iOS, Android, Windows ### Content Filtering - Smart text extraction: filters code blocks, `` tags, HTML - Only assistant messages are eligible for playback - Tool and system messages are excluded ### Internationalization - Added 8 language translations (en, zh, de, es, fr, ja, ko, pt) - New keys: `playSpeech`, `pauseSpeech`, `resumeSpeech`, `stopSpeech`, `autoPlaySpeech`, `speechNotSupported` ## Technical Details ### New Files - `packages/client/src/composables/useSpeech.ts`: Core speech synthesis composable - Voice loading and selection logic - Single-instance global speech manager - Event handling (onstart, onend, onerror, onboundary) - State management (isPlaying, isPaused, currentMessageId) ### Modified Components - **ChatInput.vue**: Auto-play switch with localStorage persistence - **MessageItem.vue**: - Playback button integration - Event listener for auto-play triggers - Mobile-first button visibility - Rainbow border animation during playback ### Store Changes - `chat.ts`: - Added `autoPlaySpeechEnabled` state - `setAutoPlaySpeech()` method - `playMessageSpeech()` method for event-based playback - Auto-play trigger on `run.completed` event ## Browser Support - Requires Web Speech API support (all modern browsers) - Graceful degradation: button hidden if API not supported - Voice availability varies by platform and OS Co-authored-by: Claude Sonnet 4.6 --- .../src/components/hermes/chat/ChatInput.vue | 63 +++- .../components/hermes/chat/MessageItem.vue | 220 +++++++++++++- packages/client/src/composables/useSpeech.ts | 281 ++++++++++++++++++ packages/client/src/i18n/locales/de.ts | 5 + packages/client/src/i18n/locales/en.ts | 6 + packages/client/src/i18n/locales/es.ts | 5 + packages/client/src/i18n/locales/fr.ts | 5 + packages/client/src/i18n/locales/ja.ts | 5 + packages/client/src/i18n/locales/ko.ts | 5 + packages/client/src/i18n/locales/pt.ts | 5 + packages/client/src/i18n/locales/zh.ts | 6 + packages/client/src/stores/hermes/chat.ts | 31 ++ 12 files changed, 624 insertions(+), 13 deletions(-) create mode 100644 packages/client/src/composables/useSpeech.ts diff --git a/packages/client/src/components/hermes/chat/ChatInput.vue b/packages/client/src/components/hermes/chat/ChatInput.vue index fa27785..4a020bc 100644 --- a/packages/client/src/components/hermes/chat/ChatInput.vue +++ b/packages/client/src/components/hermes/chat/ChatInput.vue @@ -4,7 +4,7 @@ import { useChatStore } from '@/stores/hermes/chat' import { useAppStore } from '@/stores/hermes/app' import { useProfilesStore } from '@/stores/hermes/profiles' import { fetchContextLength } from '@/api/hermes/sessions' -import { NButton, NTooltip } from 'naive-ui' +import { NButton, NTooltip, NSwitch } from 'naive-ui' import { computed, ref, onMounted, watch } from 'vue' import { useI18n } from 'vue-i18n' @@ -18,6 +18,26 @@ const isDragging = ref(false) const dragCounter = ref(0) const isComposing = ref(false) +// 自动播放语音开关 +const autoPlaySpeech = ref(false) + +// 从 localStorage 读取设置 +onMounted(() => { + const saved = localStorage.getItem('autoPlaySpeech') + if (saved !== null) { + autoPlaySpeech.value = saved === 'true' + // 同步到 chat store + chatStore.setAutoPlaySpeech(autoPlaySpeech.value) + } +}) + +// 监听变化并保存 +watch(autoPlaySpeech, (value) => { + localStorage.setItem('autoPlaySpeech', String(value)) + // 通知 chat store + chatStore.setAutoPlaySpeech(value) +}) + const canSend = computed(() => inputText.value.trim() || attachments.value.length > 0) // --- Context info --- @@ -195,7 +215,7 @@ function isImage(type: string): boolean {