diff --git a/frontend/src/views/BriefingView.vue b/frontend/src/views/BriefingView.vue
index 1a9836f..661a5ec 100644
--- a/frontend/src/views/BriefingView.vue
+++ b/frontend/src/views/BriefingView.vue
@@ -4,6 +4,7 @@ import { useBackgroundRefresh } from '@/composables/useBackgroundRefresh'
import { useVoiceRecorder } from '@/composables/useVoiceRecorder'
import { useVoiceAudio } from '@/composables/useVoiceAudio'
import { useListenMode } from '@/composables/useListenMode'
+import { useStreamingTts } from '@/composables/useStreamingTts'
import { useChatStore } from '@/stores/chat'
import { useSettingsStore } from '@/stores/settings'
import ChatMessage from '@/components/ChatMessage.vue'
@@ -20,7 +21,6 @@ import {
deleteRssReaction,
getNewsItems,
transcribeAudio,
- synthesiseSpeech,
type BriefingConversation,
type BriefingMessage,
} from '@/api/client'
@@ -262,43 +262,15 @@ const voiceEnabled = computed(() => settingsStore.voiceSttReady)
const voiceTtsEnabled = computed(() => settingsStore.voiceTtsReady)
const listenMode = useListenMode()
const transcribing = ref(false)
-const synthesising = ref(false)
+const tts = useStreamingTts({
+ streamingContent: computed(() => chatStore.streamingContent),
+ streaming: computed(() => chatStore.streaming),
+ enabled: computed(() => listenMode.value && voiceTtsEnabled.value),
+})
const recorder = useVoiceRecorder()
const audio = useVoiceAudio()
-// Read the latest assistant message aloud
-async function listenToLatest() {
- const lastAssistant = [...messages.value].reverse().find((m) => m.role === 'assistant')
- if (!lastAssistant?.content) return
- await speakText(lastAssistant.content)
-}
-
-async function speakText(text: string) {
- if (!voiceTtsEnabled.value || synthesising.value) return
- // Strip markdown for cleaner TTS output
- const plain = text
- .replace(/```[\s\S]*?```/g, '')
- .replace(/`[^`]+`/g, (m) => m.slice(1, -1))
- .replace(/#{1,6}\s+/g, '')
- .replace(/\*\*([^*]+)\*\*/g, '$1')
- .replace(/\*([^*]+)\*/g, '$1')
- .replace(/\[([^\]]+)\]\([^)]+\)/g, '$1')
- .replace(/^\s*[-*+]\s+/gm, '')
- .replace(/\n{2,}/g, ' ')
- .trim()
- if (!plain) return
- synthesising.value = true
- try {
- const blob = await synthesiseSpeech(plain)
- await audio.play(blob)
- } catch {
- // TTS failure is non-critical
- } finally {
- synthesising.value = false
- }
-}
-
// PTT: hold to record, release to transcribe and send
async function startPtt() {
if (!voiceEnabled.value || recorder.recording.value) return
@@ -323,14 +295,6 @@ async function stopPtt() {
}
}
-// Auto-TTS when listen mode is on and streaming ends
-watch(() => chatStore.streaming, async (streaming) => {
- if (!streaming && listenMode.value && voiceTtsEnabled.value) {
- // Small delay to let messages update after stream
- await new Promise((r) => setTimeout(r, 200))
- await listenToLatest()
- }
-})
// Convert BriefingMessage to Message for ChatMessage component
function toMsg(m: BriefingMessage): Message {
@@ -455,12 +419,12 @@ onMounted(async () => {