mirror of
https://github.com/hicccc77/WeFlow.git
synced 2026-03-25 07:16:51 +00:00
feat: 优化了语音配置页面的效果;新增语音实际波形图显示;新增语音点击跳转进度
fix: 修复了一个可能导致语音解密错乱的问题
This commit is contained in:
@@ -2202,7 +2202,7 @@ class ChatService {
|
||||
|
||||
|
||||
// 3. 调用 C++ 接口获取语音 (Hex)
|
||||
const voiceRes = await wcdbService.getVoiceData(sessionId, msgCreateTime, candidates, msgSvrId)
|
||||
const voiceRes = await wcdbService.getVoiceData(sessionId, msgCreateTime, candidates, localId, msgSvrId)
|
||||
if (!voiceRes.success || !voiceRes.hex) {
|
||||
return { success: false, error: voiceRes.error || '未找到语音数据' }
|
||||
}
|
||||
@@ -2245,6 +2245,33 @@ class ChatService {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查语音是否已有缓存
|
||||
*/
|
||||
async resolveVoiceCache(sessionId: string, msgId: string): Promise<{ success: boolean; hasCache: boolean; data?: string }> {
|
||||
try {
|
||||
const cacheKey = this.getVoiceCacheKey(sessionId, msgId)
|
||||
|
||||
// 1. 检查内存缓存
|
||||
const inMemory = this.voiceWavCache.get(cacheKey)
|
||||
if (inMemory) {
|
||||
return { success: true, hasCache: true, data: inMemory.toString('base64') }
|
||||
}
|
||||
|
||||
// 2. 检查文件缓存
|
||||
const cachedFile = this.getVoiceCacheFilePath(cacheKey)
|
||||
if (existsSync(cachedFile)) {
|
||||
const wavData = readFileSync(cachedFile)
|
||||
this.cacheVoiceWav(cacheKey, wavData) // 回甜内存
|
||||
return { success: true, hasCache: true, data: wavData.toString('base64') }
|
||||
}
|
||||
|
||||
return { success: true, hasCache: false }
|
||||
} catch (e) {
|
||||
return { success: false, hasCache: false }
|
||||
}
|
||||
}
|
||||
|
||||
async getVoiceData_Legacy(sessionId: string, msgId: string): Promise<{ success: boolean; data?: string; error?: string }> {
|
||||
try {
|
||||
const localId = parseInt(msgId, 10)
|
||||
|
||||
@@ -14,7 +14,6 @@ type ModelInfo = {
|
||||
files: {
|
||||
model: string
|
||||
tokens: string
|
||||
vad: string
|
||||
}
|
||||
sizeBytes: number
|
||||
sizeLabel: string
|
||||
@@ -31,8 +30,7 @@ const SENSEVOICE_MODEL: ModelInfo = {
|
||||
name: 'SenseVoiceSmall',
|
||||
files: {
|
||||
model: 'model.int8.onnx',
|
||||
tokens: 'tokens.txt',
|
||||
vad: 'silero_vad.onnx'
|
||||
tokens: 'tokens.txt'
|
||||
},
|
||||
sizeBytes: 245_000_000,
|
||||
sizeLabel: '245 MB'
|
||||
@@ -40,8 +38,7 @@ const SENSEVOICE_MODEL: ModelInfo = {
|
||||
|
||||
const MODEL_DOWNLOAD_URLS = {
|
||||
model: 'https://modelscope.cn/models/pengzhendong/sherpa-onnx-sense-voice-zh-en-ja-ko-yue/resolve/master/model.int8.onnx',
|
||||
tokens: 'https://modelscope.cn/models/pengzhendong/sherpa-onnx-sense-voice-zh-en-ja-ko-yue/resolve/master/tokens.txt',
|
||||
vad: 'https://www.modelscope.cn/models/manyeyes/silero-vad-onnx/resolve/master/silero_vad.onnx'
|
||||
tokens: 'https://modelscope.cn/models/pengzhendong/sherpa-onnx-sense-voice-zh-en-ja-ko-yue/resolve/master/tokens.txt'
|
||||
}
|
||||
|
||||
export class VoiceTranscribeService {
|
||||
@@ -74,12 +71,9 @@ export class VoiceTranscribeService {
|
||||
try {
|
||||
const modelPath = this.resolveModelPath(SENSEVOICE_MODEL.files.model)
|
||||
const tokensPath = this.resolveModelPath(SENSEVOICE_MODEL.files.tokens)
|
||||
const vadPath = this.resolveModelPath((SENSEVOICE_MODEL.files as any).vad)
|
||||
|
||||
const modelExists = existsSync(modelPath)
|
||||
const tokensExists = existsSync(tokensPath)
|
||||
const vadExists = existsSync(vadPath)
|
||||
const exists = modelExists && tokensExists && vadExists
|
||||
const exists = modelExists && tokensExists
|
||||
|
||||
if (!exists) {
|
||||
return { success: true, exists: false, modelPath, tokensPath }
|
||||
@@ -87,8 +81,7 @@ export class VoiceTranscribeService {
|
||||
|
||||
const modelSize = statSync(modelPath).size
|
||||
const tokensSize = statSync(tokensPath).size
|
||||
const vadSize = statSync(vadPath).size
|
||||
const totalSize = modelSize + tokensSize + vadSize
|
||||
const totalSize = modelSize + tokensSize
|
||||
|
||||
return {
|
||||
success: true,
|
||||
@@ -121,7 +114,6 @@ export class VoiceTranscribeService {
|
||||
|
||||
const modelPath = this.resolveModelPath(SENSEVOICE_MODEL.files.model)
|
||||
const tokensPath = this.resolveModelPath(SENSEVOICE_MODEL.files.tokens)
|
||||
const vadPath = this.resolveModelPath((SENSEVOICE_MODEL.files as any).vad)
|
||||
|
||||
// 初始进度
|
||||
onProgress?.({
|
||||
@@ -166,35 +158,16 @@ export class VoiceTranscribeService {
|
||||
}
|
||||
)
|
||||
|
||||
// 下载 vad 文件 (30%)
|
||||
console.info('[VoiceTranscribe] 开始下载 VAD 文件...')
|
||||
await this.downloadToFile(
|
||||
(MODEL_DOWNLOAD_URLS as any).vad,
|
||||
vadPath,
|
||||
'vad',
|
||||
(downloaded, total) => {
|
||||
const modelSize = existsSync(modelPath) ? statSync(modelPath).size : 0
|
||||
const tokensSize = existsSync(tokensPath) ? statSync(tokensPath).size : 0
|
||||
const percent = total ? 70 + (downloaded / total) * 30 : 70
|
||||
onProgress?.({
|
||||
modelName: SENSEVOICE_MODEL.name,
|
||||
downloadedBytes: modelSize + tokensSize + downloaded,
|
||||
totalBytes: SENSEVOICE_MODEL.sizeBytes,
|
||||
percent
|
||||
})
|
||||
}
|
||||
)
|
||||
console.info('[VoiceTranscribe] 模型下载完成')
|
||||
|
||||
console.info('[VoiceTranscribe] 所有文件下载完成')
|
||||
return { success: true, modelPath, tokensPath }
|
||||
} catch (error) {
|
||||
const modelPath = this.resolveModelPath(SENSEVOICE_MODEL.files.model)
|
||||
const tokensPath = this.resolveModelPath(SENSEVOICE_MODEL.files.tokens)
|
||||
const vadPath = this.resolveModelPath((SENSEVOICE_MODEL.files as any).vad)
|
||||
try {
|
||||
if (existsSync(modelPath)) unlinkSync(modelPath)
|
||||
if (existsSync(tokensPath)) unlinkSync(tokensPath)
|
||||
if (existsSync(vadPath)) unlinkSync(vadPath)
|
||||
} catch { }
|
||||
return { success: false, error: String(error) }
|
||||
} finally {
|
||||
@@ -230,7 +203,7 @@ export class VoiceTranscribeService {
|
||||
supportedLanguages = this.configService.get('transcribeLanguages')
|
||||
// 如果配置中也没有或为空,使用默认值
|
||||
if (!supportedLanguages || supportedLanguages.length === 0) {
|
||||
supportedLanguages = ['zh']
|
||||
supportedLanguages = ['zh', 'yue']
|
||||
}
|
||||
}
|
||||
|
||||
@@ -303,7 +276,7 @@ export class VoiceTranscribeService {
|
||||
|
||||
const request = protocol.get(url, options, (response) => {
|
||||
console.info(`[VoiceTranscribe] ${fileName} 响应状态:`, response.statusCode)
|
||||
|
||||
|
||||
// 处理重定向
|
||||
if ([301, 302, 303, 307, 308].includes(response.statusCode || 0) && response.headers.location) {
|
||||
if (remainingRedirects <= 0) {
|
||||
@@ -324,11 +297,11 @@ export class VoiceTranscribeService {
|
||||
|
||||
const totalBytes = Number(response.headers['content-length'] || 0) || undefined
|
||||
let downloadedBytes = 0
|
||||
|
||||
|
||||
console.info(`[VoiceTranscribe] ${fileName} 文件大小:`, totalBytes ? `${(totalBytes / 1024 / 1024).toFixed(2)} MB` : '未知')
|
||||
|
||||
const writer = createWriteStream(targetPath)
|
||||
|
||||
|
||||
// 设置数据接收超时(60秒没有数据则超时)
|
||||
let lastDataTime = Date.now()
|
||||
const dataTimeout = setInterval(() => {
|
||||
@@ -392,7 +365,7 @@ export class VoiceTranscribeService {
|
||||
// sherpa-onnx 的 recognizer 可能需要手动释放
|
||||
this.recognizer = null
|
||||
} catch (error) {
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -347,9 +347,9 @@ export class WcdbCore {
|
||||
this.wcdbGetDbStatus = null
|
||||
}
|
||||
|
||||
// wcdb_status wcdb_get_voice_data(wcdb_handle handle, const char* session_id, int32_t create_time, const char* candidates_json, char** out_hex)
|
||||
// wcdb_status wcdb_get_voice_data(wcdb_handle handle, const char* session_id, int32_t create_time, int32_t local_id, int64_t svr_id, const char* candidates_json, char** out_hex)
|
||||
try {
|
||||
this.wcdbGetVoiceData = this.lib.func('int32 wcdb_get_voice_data(int64 handle, const char* sessionId, int32 createTime, int64 svrId, const char* candidatesJson, _Out_ void** outHex)')
|
||||
this.wcdbGetVoiceData = this.lib.func('int32 wcdb_get_voice_data(int64 handle, const char* sessionId, int32 createTime, int32 localId, int64 svrId, const char* candidatesJson, _Out_ void** outHex)')
|
||||
} catch {
|
||||
this.wcdbGetVoiceData = null
|
||||
}
|
||||
@@ -1321,12 +1321,12 @@ export class WcdbCore {
|
||||
}
|
||||
}
|
||||
|
||||
async getVoiceData(sessionId: string, createTime: number, candidates: string[], svrId: string | number = 0): Promise<{ success: boolean; hex?: string; error?: string }> {
|
||||
async getVoiceData(sessionId: string, createTime: number, candidates: string[], localId: number = 0, svrId: string | number = 0): Promise<{ success: boolean; hex?: string; error?: string }> {
|
||||
if (!this.ensureReady()) return { success: false, error: 'WCDB 未连接' }
|
||||
if (!this.wcdbGetVoiceData) return { success: false, error: '当前 DLL 版本不支持获取语音数据' }
|
||||
try {
|
||||
const outPtr = [null as any]
|
||||
const result = this.wcdbGetVoiceData(this.handle, sessionId, createTime, BigInt(svrId || 0), JSON.stringify(candidates), outPtr)
|
||||
const result = this.wcdbGetVoiceData(this.handle, sessionId, createTime, localId, BigInt(svrId || 0), JSON.stringify(candidates), outPtr)
|
||||
if (result !== 0 || !outPtr[0]) {
|
||||
return { success: false, error: `获取语音数据失败: ${result}` }
|
||||
}
|
||||
|
||||
@@ -99,7 +99,7 @@ export class WcdbService {
|
||||
setPaths(resourcesPath: string, userDataPath: string): void {
|
||||
this.resourcesPath = resourcesPath
|
||||
this.userDataPath = userDataPath
|
||||
this.callWorker('setPaths', { resourcesPath, userDataPath }).catch(() => {})
|
||||
this.callWorker('setPaths', { resourcesPath, userDataPath }).catch(() => { })
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -107,7 +107,7 @@ export class WcdbService {
|
||||
*/
|
||||
setLogEnabled(enabled: boolean): void {
|
||||
this.logEnabled = enabled
|
||||
this.callWorker('setLogEnabled', { enabled }).catch(() => {})
|
||||
this.callWorker('setLogEnabled', { enabled }).catch(() => { })
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -346,8 +346,8 @@ export class WcdbService {
|
||||
/**
|
||||
* 获取语音数据
|
||||
*/
|
||||
async getVoiceData(sessionId: string, createTime: number, candidates: string[], svrId: string | number = 0): Promise<{ success: boolean; hex?: string; error?: string }> {
|
||||
return this.callWorker('getVoiceData', { sessionId, createTime, candidates, svrId })
|
||||
async getVoiceData(sessionId: string, createTime: number, candidates: string[], localId: number = 0, svrId: string | number = 0): Promise<{ success: boolean; hex?: string; error?: string }> {
|
||||
return this.callWorker('getVoiceData', { sessionId, createTime, candidates, localId, svrId })
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user