导出文件支持更多类型;修复语音转文字意外报错的问题
This commit is contained in:
1 parent
cabedf034f
commit
84c943e75b
12 files changed
+101
-24
No files matched your search
@@ -52,7 +52,7 @@ ArchLinux 用户可以选择 `yay -S weflow` 快速安装
|
||||
|
||||
| 功能模块 | 说明 |
|
||||
|---------|------|
|
||||
| **聊天** | 解密聊天中的图片、视频、实况(仅支持谷歌协议拍摄的实况);支持**修改**、删除**本地**消息;实时刷新最新消息,无需生成解密中间数据库 |
|
||||
| **聊天** | 解密聊天中的图片、视频、实况;支持**修改**本地消息 |
|
||||
| **消息防撤回** | 防止其他人发送的消息被撤回 |
|
||||
| **实时弹窗通知** | 新消息到达时提供桌面弹窗提醒,便于及时查看重要会话,提供黑白名单功能 |
|
||||
| **私聊分析** | 统计好友间消息数量;分析消息类型与发送比例;查看消息时段分布等 |
|
||||
@@ -60,8 +60,8 @@ ArchLinux 用户可以选择 `yay -S weflow` 快速安装
|
||||
| **年度报告** | 生成按年统计的年度报告,或跨年度的长期历史报告 |
|
||||
| **双人报告** | 选择指定好友,基于双方聊天记录生成专属分析报告 |
|
||||
| **消息导出** | 将微信聊天记录导出为多种格式:JSON、HTML、Markdown、TXT、Excel、CSV、PGSQL、ChatLab专属格式等 |
|
||||
| **朋友圈** | 解密朋友圈图片、视频、实况;导出朋友圈内容;拦截朋友圈的删除与隐藏操作;突破时间访问限制 |
|
||||
| **联系人** | 导出微信好友、群聊、公众号信息;尝试找回曾经的好友(功能尚不完善) |
|
||||
| **朋友圈** | 解密朋友圈图片、视频、实况;导出朋友圈内容;拦截朋友圈的删除与隐藏操作; |
|
||||
| **联系人** | 导出微信好友、群聊、公众号信息;找回部分曾经的好友 |
|
||||
| **HTTP API 映射** | 将本地消息能力映射为 HTTP API,便于对接外部系统、自动化脚本与二次开发 |
|
||||
|
||||
## HTTP API
|
||||
|
||||
@@ -9215,9 +9215,12 @@ class ChatService {
|
||||
const t6 = Date.now()
|
||||
|
||||
|
||||
if (result.success && result.transcript) {
|
||||
|
||||
this.cacheVoiceTranscript(cacheKey, result.transcript)
|
||||
if (result.success) {
|
||||
const transcript = String(result.transcript || '').trim()
|
||||
if (transcript) {
|
||||
this.cacheVoiceTranscript(cacheKey, transcript)
|
||||
}
|
||||
return { success: true, transcript }
|
||||
} else {
|
||||
console.error(`[Transcribe] 转写失败: ${result.error}`)
|
||||
}
|
||||
|
||||
@@ -3276,8 +3276,9 @@ export class ExportContext {
|
||||
public async transcribeVoice(sessionId: string, msgId: string, createTime: number, senderWxid: string | null): Promise<string> {
|
||||
try {
|
||||
const transcript = await chatService.getVoiceTranscript(sessionId, msgId, createTime, undefined, senderWxid || undefined)
|
||||
if (transcript.success && transcript.transcript) {
|
||||
return `[语音转文字] ${transcript.transcript}`
|
||||
if (transcript.success) {
|
||||
const text = String(transcript.transcript || '').trim()
|
||||
return text ? `[语音转文字] ${text}` : '[语音消息 - 未识别到文字]'
|
||||
}
|
||||
return `[语音消息 - 转文字失败: ${transcript.error || '未知错误'}]`
|
||||
} catch (e) {
|
||||
|
||||
@@ -47,6 +47,7 @@ export class VoiceTranscribeService {
|
||||
private downloadTasks = new Map<string, Promise<{ success: boolean; path?: string; error?: string }>>()
|
||||
private recognizer: OfflineRecognizer | null = null
|
||||
private isInitializing = false
|
||||
private transcribeQueueTail: Promise<void> = Promise.resolve()
|
||||
|
||||
private buildTranscribeWorkerEnv(): NodeJS.ProcessEnv {
|
||||
const env: NodeJS.ProcessEnv = { ...process.env }
|
||||
@@ -227,6 +228,29 @@ export class VoiceTranscribeService {
|
||||
wavData: Buffer,
|
||||
onPartial?: (text: string) => void,
|
||||
languages?: string[]
|
||||
): Promise<{ success: boolean; transcript?: string; error?: string }> {
|
||||
return this.runInTranscribeQueue(() => this.transcribeWavBufferNow(wavData, onPartial, languages))
|
||||
}
|
||||
|
||||
private async runInTranscribeQueue<T>(task: () => Promise<T>): Promise<T> {
|
||||
const previous = this.transcribeQueueTail
|
||||
let release!: () => void
|
||||
this.transcribeQueueTail = new Promise<void>((resolve) => {
|
||||
release = resolve
|
||||
})
|
||||
|
||||
await previous.catch(() => undefined)
|
||||
try {
|
||||
return await task()
|
||||
} finally {
|
||||
release()
|
||||
}
|
||||
}
|
||||
|
||||
private async transcribeWavBufferNow(
|
||||
wavData: Buffer,
|
||||
onPartial?: (text: string) => void,
|
||||
languages?: string[]
|
||||
): Promise<{ success: boolean; transcript?: string; error?: string }> {
|
||||
return new Promise((resolve) => {
|
||||
try {
|
||||
@@ -254,47 +278,96 @@ export class VoiceTranscribeService {
|
||||
stdio: ['ignore', 'ignore', 'ignore', 'ipc'],
|
||||
serialization: 'advanced'
|
||||
})
|
||||
|
||||
let settled = false
|
||||
let shutdownRequested = false
|
||||
let shutdownTimer: NodeJS.Timeout | null = null
|
||||
|
||||
const clearShutdownTimer = () => {
|
||||
if (shutdownTimer) {
|
||||
clearTimeout(shutdownTimer)
|
||||
shutdownTimer = null
|
||||
}
|
||||
}
|
||||
|
||||
const requestShutdown = () => {
|
||||
shutdownRequested = true
|
||||
try {
|
||||
if (worker.connected) {
|
||||
worker.disconnect()
|
||||
}
|
||||
} catch { }
|
||||
|
||||
shutdownTimer = setTimeout(() => {
|
||||
try {
|
||||
if (!worker.killed) {
|
||||
worker.kill('SIGTERM')
|
||||
}
|
||||
} catch { }
|
||||
}, 1500)
|
||||
}
|
||||
|
||||
const settle = (result: { success: boolean; transcript?: string; error?: string }, shutdown = true) => {
|
||||
if (settled) return
|
||||
settled = true
|
||||
if (shutdown) requestShutdown()
|
||||
resolve(result)
|
||||
}
|
||||
|
||||
worker.send({
|
||||
modelPath,
|
||||
tokensPath,
|
||||
wavData,
|
||||
sampleRate: 16000,
|
||||
languages: supportedLanguages
|
||||
}, (error: Error | null | undefined) => {
|
||||
if (error) {
|
||||
settle({ success: false, error: `发送转写任务失败: ${String(error)}` })
|
||||
}
|
||||
})
|
||||
|
||||
let finalTranscript = ''
|
||||
|
||||
worker.on('message', (msg: any) => {
|
||||
if (msg.type === 'partial') {
|
||||
onPartial?.(msg.text)
|
||||
} else if (msg.type === 'final') {
|
||||
finalTranscript = msg.text
|
||||
resolve({ success: true, transcript: finalTranscript })
|
||||
worker.disconnect()
|
||||
worker.kill()
|
||||
settle({ success: true, transcript: String(msg.text || '') })
|
||||
} else if (msg.type === 'error') {
|
||||
console.error('[VoiceTranscribe] Worker 错误:', msg.error)
|
||||
resolve({ success: false, error: msg.error })
|
||||
worker.disconnect()
|
||||
worker.kill()
|
||||
settle({ success: false, error: String(msg.error || '转写进程返回未知错误') })
|
||||
}
|
||||
})
|
||||
|
||||
worker.on('error', (err: Error) => resolve({ success: false, error: String(err) }))
|
||||
worker.on('exit', (code: number | null, signal: string | null) => {
|
||||
if (code === null || signal === 'SIGSEGV') {
|
||||
worker.on('error', (err: Error) => {
|
||||
settle({ success: false, error: String(err) })
|
||||
})
|
||||
|
||||
worker.on('exit', (code: number | null, signal: string | null) => {
|
||||
clearShutdownTimer()
|
||||
if (settled) return
|
||||
|
||||
if (signal === 'SIGSEGV') {
|
||||
console.error(`[VoiceTranscribe] Worker 异常崩溃,信号: ${signal}。可能是由于底层 C++ 运行库在当前系统上发生段错误。`);
|
||||
resolve({
|
||||
settle({
|
||||
success: false,
|
||||
error: 'SEGFAULT_ERROR'
|
||||
});
|
||||
return;
|
||||
}, false)
|
||||
return
|
||||
}
|
||||
|
||||
if (signal) {
|
||||
const error = shutdownRequested
|
||||
? '转写进程已结束'
|
||||
: `转写进程被系统终止: ${signal}`
|
||||
settle({ success: false, error }, false)
|
||||
return
|
||||
}
|
||||
|
||||
if (code !== 0) {
|
||||
resolve({ success: false, error: `Worker exited with code ${code}` });
|
||||
settle({ success: false, error: `Worker exited with code ${code}` }, false)
|
||||
return
|
||||
}
|
||||
|
||||
settle({ success: false, error: '转写进程未返回结果' }, false)
|
||||
})
|
||||
|
||||
} catch (error) {
|
||||
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Reference in new issue
Block a user