导出文件支持更多类型;修复语音转文字意外报错的问题

This commit is contained in:
cc committed 2026-07-02 15:31:21 +08:00
1 parent cabedf034f
commit 84c943e75b
12 files changed
+101 -24

No files matched your search

+3 -3
View File
@@ -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
+6 -3
View File
@@ -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) {
+89 -16
View File
@@ -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.