feat: tts语音生成

This commit is contained in:
2025-06-30 09:50:44 +08:00
parent 51e7239c71
commit 06e6b4a8c9
20 changed files with 1135 additions and 30 deletions

302
web/src/stores/tts_store.ts Normal file
View File

@@ -0,0 +1,302 @@
import { useAudioWebSocket } from "@/services";
import { createAudioUrl, mergeAudioChunks } from "@/utils";
interface AudioState {
isPlaying: boolean;
isLoading: boolean;
audioElement: HTMLAudioElement | null;
audioUrl: string | null;
audioChunks: ArrayBuffer[];
hasError: boolean;
errorMessage: string;
}
export const useTtsStore = defineStore("tts", () => {
// 多音频状态管理 - 以消息ID为key
const audioStates = ref<Map<string, AudioState>>(new Map());
// 当前活跃的转换请求(保留用于兼容性)
const activeConversion = ref<string | null>(null);
// 会话状态
const hasActiveSession = ref(false);
// WebSocket连接
const { sendMessage, ensureConnection } = useAudioWebSocket();
/**
* 获取或创建音频状态
*/
const getAudioState = (messageId: string): AudioState => {
if (!audioStates.value.has(messageId)) {
audioStates.value.set(messageId, {
isPlaying: false,
isLoading: false,
audioElement: null,
audioUrl: null,
audioChunks: [],
hasError: false,
errorMessage: ""
});
}
return audioStates.value.get(messageId)!;
};
/**
* 发送文本进行TTS转换
*/
const convertText = async (text: string, messageId: string) => {
try {
await ensureConnection();
// 暂停其他正在播放的音频
pauseAll();
// 获取当前消息的状态
const state = getAudioState(messageId);
// 清理之前的音频和错误状态
clearAudioState(state);
state.isLoading = true;
state.audioChunks = [];
// 设置当前活跃转换
activeConversion.value = messageId;
hasActiveSession.value = true;
// 发送文本到TTS服务
sendMessage(JSON.stringify({ type: "tts_text", text, messageId }));
} catch (error) {
handleError(`连接失败: ${error}`, messageId);
}
};
/**
* 处理接收到的音频数据 - 修改为支持messageId参数
*/
const handleAudioData = (data: ArrayBuffer, messageId?: string) => {
// 如果传递了messageId就使用它否则使用activeConversion
const targetMessageId = messageId || activeConversion.value;
if (!targetMessageId) {
console.warn("handleAudioData: 没有有效的messageId");
return;
}
console.log(`接收音频数据 [${targetMessageId}],大小:`, data.byteLength);
const state = getAudioState(targetMessageId);
state.audioChunks.push(data);
};
/**
* 完成TTS转换创建播放器并自动播放 - 修改为支持messageId参数
*/
const finishConversion = async (messageId?: string) => {
// 如果传递了messageId就使用它否则使用activeConversion
const targetMessageId = messageId || activeConversion.value;
if (!targetMessageId) {
console.warn("finishConversion: 没有有效的messageId");
return;
}
const state = getAudioState(targetMessageId);
console.log(
`完成TTS转换 [${targetMessageId}],音频片段数量:`,
state.audioChunks.length
);
if (state.audioChunks.length === 0) {
handleError("没有接收到音频数据", targetMessageId);
return;
}
try {
// 合并音频片段
const mergedAudio = mergeAudioChunks(state.audioChunks);
console.log(
`合并后音频大小 [${targetMessageId}]:`,
mergedAudio.byteLength
);
// 创建音频URL和元素
state.audioUrl = createAudioUrl(mergedAudio);
state.audioElement = new Audio(state.audioUrl);
// 设置音频事件
setupAudioEvents(state, targetMessageId);
state.isLoading = false;
// 清除activeConversion如果是当前活跃的
if (activeConversion.value === targetMessageId) {
activeConversion.value = null;
}
console.log(`TTS音频准备完成 [${targetMessageId}],开始自动播放`);
// 自动播放
await play(targetMessageId);
} catch (error) {
handleError(`音频处理失败: ${error}`, targetMessageId);
}
};
/**
* 设置音频事件监听
*/
const setupAudioEvents = (state: AudioState, messageId: string) => {
if (!state.audioElement) return;
const audio = state.audioElement;
audio.addEventListener("ended", () => {
state.isPlaying = false;
console.log(`音频播放结束 [${messageId}]`);
});
audio.addEventListener("error", (e) => {
console.error(`音频播放错误 [${messageId}]:`, e);
handleError("音频播放失败", messageId);
});
audio.addEventListener("canplaythrough", () => {
console.log(`音频可以播放 [${messageId}]`);
});
};
/**
* 播放指定消息的音频
*/
const play = async (messageId: string) => {
const state = getAudioState(messageId);
if (!state.audioElement) {
handleError("音频未准备好", messageId);
return;
}
try {
// 暂停其他正在播放的音频
pauseAll(messageId);
await state.audioElement.play();
state.isPlaying = true;
state.hasError = false;
state.errorMessage = "";
console.log(`开始播放音频 [${messageId}]`);
} catch (error) {
handleError(`播放失败: ${error}`, messageId);
}
};
/**
* 暂停指定消息的音频
*/
const pause = (messageId: string) => {
const state = getAudioState(messageId);
if (!state.audioElement) return;
state.audioElement.pause();
state.isPlaying = false;
console.log(`暂停音频 [${messageId}]`);
};
/**
* 暂停所有音频
*/
const pauseAll = (excludeMessageId?: string) => {
audioStates.value.forEach((state, messageId) => {
if (excludeMessageId && messageId === excludeMessageId) return;
if (state.isPlaying && state.audioElement) {
state.audioElement.pause();
state.isPlaying = false;
}
});
};
/**
* 处理TTS错误 - 修改为支持messageId参数
*/
const handleError = (errorMsg: string, messageId?: string) => {
// 如果传递了messageId就使用它否则使用activeConversion
const targetMessageId = messageId || activeConversion.value;
if (!targetMessageId) {
console.error(`TTS错误 (无messageId): ${errorMsg}`);
return;
}
console.error(`TTS错误 [${targetMessageId}]: ${errorMsg}`);
const state = getAudioState(targetMessageId);
state.hasError = true;
state.errorMessage = errorMsg;
state.isLoading = false;
if (activeConversion.value === targetMessageId) {
activeConversion.value = null;
hasActiveSession.value = false;
}
};
/**
* 清理指定消息的音频资源
*/
const clearAudio = (messageId: string) => {
const state = getAudioState(messageId);
clearAudioState(state);
audioStates.value.delete(messageId);
};
/**
* 清理音频状态
*/
const clearAudioState = (state: AudioState) => {
if (state.audioElement) {
state.audioElement.pause();
state.audioElement = null;
}
if (state.audioUrl) {
URL.revokeObjectURL(state.audioUrl);
state.audioUrl = null;
}
state.isPlaying = false;
state.audioChunks = [];
state.hasError = false;
state.errorMessage = "";
};
// 状态查询方法
const isPlaying = (messageId: string) => getAudioState(messageId).isPlaying;
const isLoading = (messageId: string) => getAudioState(messageId).isLoading;
const hasAudio = (messageId: string) =>
!!getAudioState(messageId).audioElement;
const hasError = (messageId: string) => getAudioState(messageId).hasError;
const getErrorMessage = (messageId: string) =>
getAudioState(messageId).errorMessage;
// 组件卸载时清理所有资源
onUnmounted(() => {
audioStates.value.forEach((state) => clearAudioState(state));
audioStates.value.clear();
});
return {
// 状态查询方法
isPlaying,
isLoading,
hasAudio,
hasError,
getErrorMessage,
// 核心方法
convertText,
handleAudioData,
finishConversion,
play,
pause,
pauseAll,
clearAudio,
handleError
};
});