feat(tts): wire useStreamingTts into BriefingView

This commit is contained in:
2026-04-03 13:11:12 -04:00
parent fda6a7acc1
commit b4f5a935b2
+11 -47
View File
@@ -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 () => {
<button
v-if="voiceTtsEnabled"
class="btn-icon"
:class="{ 'btn-icon-active': listenMode, 'btn-icon-busy': synthesising || audio.playing.value }"
@click="listenMode ? (listenMode = false) : (listenMode = true, listenToLatest())"
:class="{ 'btn-icon-active': listenMode, 'btn-icon-busy': tts.speaking.value }"
@click="listenMode = !listenMode; if (!listenMode) tts.stop()"
:title="listenMode ? 'Stop auto-read' : 'Read replies aloud'"
aria-label="Toggle listen mode"
>
<svg v-if="!synthesising && !audio.playing.value" width="17" height="17" viewBox="0 0 24 24" fill="currentColor">
<svg v-if="!tts.speaking.value" width="17" height="17" viewBox="0 0 24 24" fill="currentColor">
<path d="M3 9v6h4l5 5V4L7 9H3zm13.5 3c0-1.77-1.02-3.29-2.5-4.03v8.05c1.48-.73 2.5-2.25 2.5-4.02zM14 3.23v2.06c2.89.86 5 3.54 5 6.71s-2.11 5.85-5 6.71v2.06c4.01-.91 7-4.49 7-8.77s-2.99-7.86-7-8.77z"/>
</svg>
<svg v-else width="17" height="17" viewBox="0 0 24 24" fill="currentColor">
@@ -469,9 +433,9 @@ onMounted(async () => {
</button>
<!-- Stop TTS playback -->
<button
v-if="voiceTtsEnabled && (synthesising || audio.playing.value)"
v-if="voiceTtsEnabled && tts.speaking.value"
class="btn-icon btn-icon-danger"
@click="audio.stop(); synthesising = false"
@click="tts.stop()"
title="Stop playback"
aria-label="Stop playback"
>