feat: tts语音生成
This commit is contained in:
302
web/src/stores/tts_store.ts
Normal file
302
web/src/stores/tts_store.ts
Normal 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
|
||||
};
|
||||
});
|
||||
Reference in New Issue
Block a user