update 优化录音功能模块的代码和火山模块

This commit is contained in:
2025-10-22 03:24:08 +08:00
parent c1072c3896
commit edace856ab
26 changed files with 1634 additions and 101 deletions

View File

@@ -0,0 +1,355 @@
import { create } from 'zustand';
import { devtools, persist } from 'zustand/middleware';
import { Speak, getDayOfYear, CreateSpeakData } from '../modules/speak-db/speak';
import { speakService } from '../modules/speak-db/speak-service';
import { getText } from '../modules/text';
import { useSettingStore } from './settingStore';
interface VoiceState {
// 状态数据
voiceList: Speak[];
isLoading: boolean;
error: string | null;
currentDay: number;
// 动作方法
initialize: () => Promise<void>;
addVoice: (url: string, duration: number, audioBlob?: Blob) => Promise<Speak>;
updateVoice: (id: string, updates: Partial<Speak>) => Promise<void>;
deleteVoice: (id: string) => Promise<void>;
recognizeVoice: (id: string) => Promise<string>;
clearTodayVoices: () => Promise<void>;
generateAudioUrls: () => Promise<void>;
refreshList: () => Promise<void>;
setError: (error: string | null) => void;
setLoading: (loading: boolean) => void;
}
// 辅助函数:将 Blob 转换为 base64 字符串
const blobToBase64 = (blob: Blob): Promise<string> => {
return new Promise((resolve, reject) => {
const reader = new FileReader();
reader.onload = () => {
const result = reader.result as string;
// 移除 data URL 前缀,只保留 base64 数据
resolve(result.split(',')[1] || result);
};
reader.onerror = reject;
reader.readAsDataURL(blob);
});
};
// 辅助函数:将 base64 字符串转换为 Blob URL
const base64ToUrl = (base64: string, mimeType: string = 'audio/wav'): string => {
try {
// 如果已经是 blob URL直接返回
if (base64.startsWith('blob:')) {
return base64;
}
// 将 base64 转换为 ArrayBuffer
const binaryString = window.atob(base64);
const bytes = new Uint8Array(binaryString.length);
for (let i = 0; i < binaryString.length; i++) {
bytes[i] = binaryString.charCodeAt(i);
}
// 创建 Blob 和 URL
const blob = new Blob([bytes], { type: mimeType });
return URL.createObjectURL(blob);
} catch (error) {
console.error('转换 base64 到 URL 失败:', error);
return '';
}
};
export const useVoiceStore = create<VoiceState>()(
devtools(
(set, get) => ({
// 初始状态
voiceList: [],
isLoading: false,
error: null,
currentDay: getDayOfYear(),
// 初始化:从 IndexedDB 获取当天的记录
initialize: async () => {
const { setLoading, setError, generateAudioUrls } = get();
try {
setLoading(true);
setError(null);
// 初始化 speak service
await speakService.init();
// 获取当天的语音记录
const currentDay = getDayOfYear();
const todayVoices = await speakService.getSpeaksByDay(currentDay);
set({
voiceList: todayVoices,
currentDay: currentDay
});
// 为获取到的记录生成 audio URLs
await generateAudioUrls();
} catch (error) {
console.error('初始化语音列表失败:', error);
setError(error instanceof Error ? error.message : '初始化失败');
} finally {
setLoading(false);
}
},
// 添加新的语音记录
addVoice: async (url: string, duration: number, audioBlob?: Blob) => {
const { setError } = get();
const autoRecognize = useSettingStore.getState().autoRecognize;
try {
setError(null);
let fileData: string | undefined;
// 如果提供了 audioBlob将其转换为 base64 保存到 IndexedDB
if (audioBlob) {
fileData = await blobToBase64(audioBlob);
}
// 创建语音记录(不保存 url只保存 base64 数据)
const speakData = {
duration: Math.ceil(duration),
file: fileData, // 保存 base64 数据而不是 url
day: getDayOfYear(),
no: 0, // 将由 service 自动生成
timestamp: Date.now(),
type: 'normal' as const,
text: '', // 初始为空
};
if (autoRecognize) {
speakData.text = await getText(fileData || '').then(res => res.text);
}
// 保存到 IndexedDB不包含 url
const newSpeak = await speakService.createSpeakAuto(speakData);
// 为新记录生成 URL 并添加到状态
const speakWithUrl = {
...newSpeak,
url: newSpeak.file ? base64ToUrl(newSpeak.file) : url
};
set(state => ({
voiceList: [...state.voiceList, speakWithUrl]
}));
return speakWithUrl;
} catch (error) {
console.error('添加语音记录失败:', error);
setError(error instanceof Error ? error.message : '添加失败');
throw error;
}
},
// 更新语音记录
updateVoice: async (id: string, updates: Partial<Speak>) => {
const { setError } = get();
try {
setError(null);
// 从更新数据中移除 url因为 url 不应该保存到 IndexedDB
const { url, ...updatesWithoutUrl } = updates;
// 更新 IndexedDB 中的记录
const updatedSpeak = await speakService.updateSpeak(id, updatesWithoutUrl);
// 更新状态中的记录
set(state => ({
voiceList: state.voiceList.map(voice => {
if (voice.id === id) {
const updated = { ...voice, ...updates };
// 如果更新了 file 数据,重新生成 URL
if (updatesWithoutUrl.file) {
updated.url = base64ToUrl(updatesWithoutUrl.file);
}
return updated;
}
return voice;
})
}));
} catch (error) {
console.error('更新语音记录失败:', error);
setError(error instanceof Error ? error.message : '更新失败');
throw error;
}
},
// 删除语音记录
deleteVoice: async (id: string) => {
const { setError } = get();
try {
setError(null);
// 从 IndexedDB 删除
await speakService.deleteSpeak(id);
// 从状态中移除并释放 URL
set(state => {
const voiceToDelete = state.voiceList.find(voice => voice.id === id);
if (voiceToDelete && voiceToDelete.url && voiceToDelete.url.startsWith('blob:')) {
URL.revokeObjectURL(voiceToDelete.url);
}
return {
voiceList: state.voiceList.filter(voice => voice.id !== id)
};
});
} catch (error) {
console.error('删除语音记录失败:', error);
setError(error instanceof Error ? error.message : '删除失败');
throw error;
}
},
// 识别语音记录
recognizeVoice: async (id: string) => {
const { setError } = get();
try {
setError(null);
// 获取语音记录
const voice = get().voiceList.find(v => v.id === id);
if (!voice || !voice.file) {
throw new Error('找不到语音记录或音频数据');
}
// 调用语音识别API
const result = await getText(voice.file);
const recognizedText = result.text;
if (!recognizedText) {
throw new Error('语音识别失败,未能获取到文字内容');
}
// 更新数据库中的记录
await speakService.updateSpeak(id, { text: recognizedText });
// 更新状态中的记录
set(state => ({
voiceList: state.voiceList.map(voice =>
voice.id === id ? { ...voice, text: recognizedText } : voice
)
}));
return recognizedText;
} catch (error) {
console.error('语音识别失败:', error);
setError(error instanceof Error ? error.message : '语音识别失败');
throw error;
}
},
// 清空今天的语音记录
clearTodayVoices: async () => {
const { setError, currentDay } = get();
try {
setError(null);
// 从 IndexedDB 清空今天的记录
await speakService.deleteSpeaksByDay(currentDay);
// 清空状态并释放所有 URL
set(state => {
state.voiceList.forEach(voice => {
if (voice.url && voice.url.startsWith('blob:')) {
URL.revokeObjectURL(voice.url);
}
});
return { voiceList: [] };
});
} catch (error) {
console.error('清空今天语音记录失败:', error);
setError(error instanceof Error ? error.message : '清空失败');
throw error;
}
},
// 为所有记录生成音频 URL
generateAudioUrls: async () => {
const { voiceList } = get();
set(state => ({
voiceList: state.voiceList.map(voice => {
// 如果已经有 URL 且是 blob URL跳过
if (voice.url && voice.url.startsWith('blob:')) {
return voice;
}
// 如果有 file 数据,从 base64 生成 URL
if (voice.file) {
return {
...voice,
url: base64ToUrl(voice.file)
};
}
return voice;
})
}));
},
// 刷新列表(重新从 IndexedDB 获取)
refreshList: async () => {
const { initialize } = get();
await initialize();
},
// 设置错误信息
setError: (error: string | null) => {
set({ error });
},
// 设置加载状态
setLoading: (loading: boolean) => {
set({ isLoading: loading });
}
}),
{
name: 'voice-store', // persist key
}
)
);
// 导出类型以便其他地方使用
export type { VoiceState };
// 辅助 hooks
export const useVoiceList = () => useVoiceStore(state => state.voiceList);
export const useVoiceLoading = () => useVoiceStore(state => state.isLoading);
export const useVoiceError = () => useVoiceStore(state => state.error);
// 清理函数:页面卸载时释放所有 blob URLs
export const cleanupVoiceUrls = () => {
const { voiceList } = useVoiceStore.getState();
voiceList.forEach(voice => {
if (voice.url && voice.url.startsWith('blob:')) {
URL.revokeObjectURL(voice.url);
}
});
};
// 在页面卸载时自动清理
if (typeof window !== 'undefined') {
window.addEventListener('beforeunload', cleanupVoiceUrls);
}