fix: 语音输出改为边生成边播报,不再等整段答案完成

TtsPlayer 本来就实现了完整的流式播报能力(append() 按句子边界切分
入队、flush() 收尾剩余文本),只是 ChatPanel 一直没接上,只在
querying 结束后调用一次 speakWithAnnouncement(全文)。

改为在 onDelta 里实时 append(delta),onDone/onError 里 flush()
收尾。删除了原来 querying 结束时整段朗读的 watch(不删会导致
整段答案被朗读两遍)。效果:不用等生成完,第一句话生成出来就
开始播,播放速度跟不上文字输出时 TtsPlayer 自带的自适应语速会
自动降速跟上。

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
ssurui 2026-09-03 15:07:01 +08:00
parent ab01effd5b
commit ec634c4133

View File

@ -1,5 +1,5 @@
<script setup lang="ts"> <script setup lang="ts">
import { ref, computed, onMounted, onUnmounted, nextTick, watch } from 'vue' import { ref, computed, onMounted, onUnmounted, nextTick } from 'vue'
import { NModal, NCard, NScrollbar, useMessage } from 'naive-ui' import { NModal, NCard, NScrollbar, useMessage } from 'naive-ui'
import { marked } from 'marked' import { marked } from 'marked'
import { useRagStore } from '../stores/rag' import { useRagStore } from '../stores/rag'
@ -199,13 +199,16 @@ async function sendQuery(): Promise<void> {
const onContext = (data: unknown) => { store.chunks = (data as { chunks: Chunk[] }).chunks } const onContext = (data: unknown) => { store.chunks = (data as { chunks: Chunk[] }).chunks }
const onDelta = (data: unknown) => { const onDelta = (data: unknown) => {
if (firstDelta) { firstDelta = false; stopWaitTimer() } if (firstDelta) { firstDelta = false; stopWaitTimer() }
store.rawAnswer += (data as { delta: string }).delta const delta = (data as { delta: string }).delta
store.rawAnswer += delta
if (ttsEnabled.value) ttsPlayer.append(delta)
scrollToBottom() scrollToBottom()
} }
const onDone = (data: unknown) => { const onDone = (data: unknown) => {
stopWaitTimer() stopWaitTimer()
store.querying = false store.querying = false
queryStage.value = '' queryStage.value = ''
if (ttsEnabled.value) ttsPlayer.flush()
const stats = (data as { stats?: InferStats | null }).stats const stats = (data as { stats?: InferStats | null }).stats
if (stats) inferStats.value = stats if (stats) inferStats.value = stats
cleanup() cleanup()
@ -214,6 +217,7 @@ async function sendQuery(): Promise<void> {
stopWaitTimer() stopWaitTimer()
store.querying = false store.querying = false
queryStage.value = '' queryStage.value = ''
if (ttsEnabled.value) ttsPlayer.flush()
message.error((data as { message: string }).message) message.error((data as { message: string }).message)
cleanup() cleanup()
} }
@ -255,13 +259,16 @@ async function sendVisionQuery(): Promise<void> {
const onDelta = (data: unknown) => { const onDelta = (data: unknown) => {
if (firstDelta) { firstDelta = false; stopWaitTimer() } if (firstDelta) { firstDelta = false; stopWaitTimer() }
store.rawAnswer += (data as { delta: string }).delta const delta = (data as { delta: string }).delta
store.rawAnswer += delta
if (ttsEnabled.value) ttsPlayer.append(delta)
scrollToBottom() scrollToBottom()
} }
const onDone = (data: unknown) => { const onDone = (data: unknown) => {
stopWaitTimer() stopWaitTimer()
store.querying = false store.querying = false
queryStage.value = '' queryStage.value = ''
if (ttsEnabled.value) ttsPlayer.flush()
const stats = (data as { stats?: InferStats | null }).stats const stats = (data as { stats?: InferStats | null }).stats
if (stats) inferStats.value = stats if (stats) inferStats.value = stats
cleanup() cleanup()
@ -270,6 +277,7 @@ async function sendVisionQuery(): Promise<void> {
stopWaitTimer() stopWaitTimer()
store.querying = false store.querying = false
queryStage.value = '' queryStage.value = ''
if (ttsEnabled.value) ttsPlayer.flush()
message.error((data as { message: string }).message) message.error((data as { message: string }).message)
cleanup() cleanup()
} }
@ -545,13 +553,6 @@ onMounted(() => {
} }
}) })
watch(() => store.querying, (querying) => {
// 答案流式生成结束后,整段朗读(先播"开始播报")
if (!querying && ttsEnabled.value && store.rawAnswer) {
ttsPlayer.speakWithAnnouncement(store.rawAnswer)
}
})
onUnmounted(() => { onUnmounted(() => {
stopWaitTimer() stopWaitTimer()
if (hoverTimer) clearTimeout(hoverTimer) if (hoverTimer) clearTimeout(hoverTimer)