feat(tts): wire useStreamingTts into BriefingView
This commit is contained in:
@@ -4,6 +4,7 @@ import { useBackgroundRefresh } from '@/composables/useBackgroundRefresh'
|
|||||||
import { useVoiceRecorder } from '@/composables/useVoiceRecorder'
|
import { useVoiceRecorder } from '@/composables/useVoiceRecorder'
|
||||||
import { useVoiceAudio } from '@/composables/useVoiceAudio'
|
import { useVoiceAudio } from '@/composables/useVoiceAudio'
|
||||||
import { useListenMode } from '@/composables/useListenMode'
|
import { useListenMode } from '@/composables/useListenMode'
|
||||||
|
import { useStreamingTts } from '@/composables/useStreamingTts'
|
||||||
import { useChatStore } from '@/stores/chat'
|
import { useChatStore } from '@/stores/chat'
|
||||||
import { useSettingsStore } from '@/stores/settings'
|
import { useSettingsStore } from '@/stores/settings'
|
||||||
import ChatMessage from '@/components/ChatMessage.vue'
|
import ChatMessage from '@/components/ChatMessage.vue'
|
||||||
@@ -20,7 +21,6 @@ import {
|
|||||||
deleteRssReaction,
|
deleteRssReaction,
|
||||||
getNewsItems,
|
getNewsItems,
|
||||||
transcribeAudio,
|
transcribeAudio,
|
||||||
synthesiseSpeech,
|
|
||||||
type BriefingConversation,
|
type BriefingConversation,
|
||||||
type BriefingMessage,
|
type BriefingMessage,
|
||||||
} from '@/api/client'
|
} from '@/api/client'
|
||||||
@@ -262,43 +262,15 @@ const voiceEnabled = computed(() => settingsStore.voiceSttReady)
|
|||||||
const voiceTtsEnabled = computed(() => settingsStore.voiceTtsReady)
|
const voiceTtsEnabled = computed(() => settingsStore.voiceTtsReady)
|
||||||
const listenMode = useListenMode()
|
const listenMode = useListenMode()
|
||||||
const transcribing = ref(false)
|
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 recorder = useVoiceRecorder()
|
||||||
const audio = useVoiceAudio()
|
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
|
// PTT: hold to record, release to transcribe and send
|
||||||
async function startPtt() {
|
async function startPtt() {
|
||||||
if (!voiceEnabled.value || recorder.recording.value) return
|
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
|
// Convert BriefingMessage to Message for ChatMessage component
|
||||||
function toMsg(m: BriefingMessage): Message {
|
function toMsg(m: BriefingMessage): Message {
|
||||||
@@ -455,12 +419,12 @@ onMounted(async () => {
|
|||||||
<button
|
<button
|
||||||
v-if="voiceTtsEnabled"
|
v-if="voiceTtsEnabled"
|
||||||
class="btn-icon"
|
class="btn-icon"
|
||||||
:class="{ 'btn-icon-active': listenMode, 'btn-icon-busy': synthesising || audio.playing.value }"
|
:class="{ 'btn-icon-active': listenMode, 'btn-icon-busy': tts.speaking.value }"
|
||||||
@click="listenMode ? (listenMode = false) : (listenMode = true, listenToLatest())"
|
@click="listenMode = !listenMode; if (!listenMode) tts.stop()"
|
||||||
:title="listenMode ? 'Stop auto-read' : 'Read replies aloud'"
|
:title="listenMode ? 'Stop auto-read' : 'Read replies aloud'"
|
||||||
aria-label="Toggle listen mode"
|
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"/>
|
<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>
|
||||||
<svg v-else width="17" height="17" viewBox="0 0 24 24" fill="currentColor">
|
<svg v-else width="17" height="17" viewBox="0 0 24 24" fill="currentColor">
|
||||||
@@ -469,9 +433,9 @@ onMounted(async () => {
|
|||||||
</button>
|
</button>
|
||||||
<!-- Stop TTS playback -->
|
<!-- Stop TTS playback -->
|
||||||
<button
|
<button
|
||||||
v-if="voiceTtsEnabled && (synthesising || audio.playing.value)"
|
v-if="voiceTtsEnabled && tts.speaking.value"
|
||||||
class="btn-icon btn-icon-danger"
|
class="btn-icon btn-icon-danger"
|
||||||
@click="audio.stop(); synthesising = false"
|
@click="tts.stop()"
|
||||||
title="Stop playback"
|
title="Stop playback"
|
||||||
aria-label="Stop playback"
|
aria-label="Stop playback"
|
||||||
>
|
>
|
||||||
|
|||||||
Reference in New Issue
Block a user