mirror of
https://wget.la/https://github.com/Wxw-Gu/WechatExplorer
synced 2026-08-17 03:27:00 +08:00
feat: 语音
This commit is contained in:
@@ -0,0 +1,52 @@
|
||||
import { render, screen } from '@testing-library/react'
|
||||
import userEvent from '@testing-library/user-event'
|
||||
import { beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
import { ExportTaskCenter } from '../../src/renderer/src/components/export/ExportTaskCenter'
|
||||
|
||||
describe('export task center', () => {
|
||||
const writeText = vi.fn().mockResolvedValue(undefined)
|
||||
|
||||
beforeEach(() => {
|
||||
writeText.mockClear()
|
||||
Object.defineProperty(navigator, 'clipboard', {
|
||||
configurable: true,
|
||||
value: { writeText }
|
||||
})
|
||||
})
|
||||
|
||||
it('shows the failure reason and copies a diagnostic log', async () => {
|
||||
render(
|
||||
<ExportTaskCenter
|
||||
open
|
||||
taskCount={0}
|
||||
tasks={[
|
||||
{
|
||||
jobId: 'failed-export',
|
||||
contactId: 'fixture',
|
||||
contactName: '脱敏会话',
|
||||
format: 'html',
|
||||
status: 'failed',
|
||||
progress: {
|
||||
jobId: 'failed-export',
|
||||
phase: 'failed',
|
||||
processed: 0,
|
||||
percent: 15,
|
||||
error: 'EPERM: operation not permitted, copyfile'
|
||||
},
|
||||
createdAt: new Date('2026-08-04T15:00:00.000Z').getTime()
|
||||
}
|
||||
]}
|
||||
onToggle={vi.fn()}
|
||||
onCancel={vi.fn()}
|
||||
/>
|
||||
)
|
||||
|
||||
expect(screen.getByText('EPERM: operation not permitted, copyfile')).toBeInTheDocument()
|
||||
await userEvent.click(screen.getByRole('button', { name: '复制日志' }))
|
||||
|
||||
expect(writeText).toHaveBeenCalledOnce()
|
||||
expect(writeText.mock.calls[0][0]).toContain('会话:脱敏会话')
|
||||
expect(writeText.mock.calls[0][0]).toContain('EPERM: operation not permitted, copyfile')
|
||||
expect(screen.getByRole('button', { name: '已复制' })).toBeInTheDocument()
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,72 @@
|
||||
import { render, screen, waitFor } from '@testing-library/react'
|
||||
import userEvent from '@testing-library/user-event'
|
||||
import { beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
import { ExportWorkspace } from '../../src/renderer/src/components/export/ExportWorkspace'
|
||||
import type { VoiceModelStatus } from '../../src/shared/voice-recognition'
|
||||
|
||||
const readyStatus: VoiceModelStatus = {
|
||||
modelId: 'sensevoice-small-int8',
|
||||
version: '2024-07-17',
|
||||
state: 'ready',
|
||||
downloadedBytes: 239_549_735,
|
||||
totalBytes: 239_549_735,
|
||||
progress: 1,
|
||||
platform: 'win32',
|
||||
architecture: 'x64',
|
||||
supported: true
|
||||
}
|
||||
|
||||
describe('export voice transcripts', () => {
|
||||
beforeEach(() => {
|
||||
window.api = {
|
||||
getVoiceModelStatus: vi.fn().mockResolvedValue(readyStatus),
|
||||
onExportProgress: vi.fn(() => vi.fn())
|
||||
} as typeof window.api
|
||||
})
|
||||
|
||||
it('enables voice transcription by default for a ready HTML voice export', async () => {
|
||||
const onStartExport = vi.fn().mockResolvedValue({
|
||||
success: true,
|
||||
messageCount: 1,
|
||||
outputPath: 'C:\\fixture\\index.html'
|
||||
})
|
||||
render(
|
||||
<ExportWorkspace
|
||||
contacts={[
|
||||
{
|
||||
m_nsUsrName: 'filehelper',
|
||||
m_nsNickName: '文件传输助手',
|
||||
md5: 'fixture-contact',
|
||||
type: 'user'
|
||||
}
|
||||
]}
|
||||
selectedContact={null}
|
||||
previewMessages={[]}
|
||||
selfInfo={null}
|
||||
dbReady
|
||||
onSelectContact={vi.fn()}
|
||||
onOpenSettings={vi.fn()}
|
||||
exportTasks={[]}
|
||||
onStartExport={onStartExport}
|
||||
onCancelExport={vi.fn()}
|
||||
/>
|
||||
)
|
||||
|
||||
await userEvent.click(screen.getAllByRole('button', { name: /HTML/ })[0])
|
||||
await userEvent.click(screen.getByRole('checkbox', { name: '语音' }))
|
||||
|
||||
const transcriptOption = await screen.findByRole('checkbox', {
|
||||
name: '语音转文字,显示在语音条下方'
|
||||
})
|
||||
expect(transcriptOption).toBeEnabled()
|
||||
expect(transcriptOption).toBeChecked()
|
||||
|
||||
await userEvent.click(screen.getByRole('button', { name: '开始导出' }))
|
||||
await waitFor(() => expect(onStartExport).toHaveBeenCalledOnce())
|
||||
expect(onStartExport.mock.calls[0][0]).toMatchObject({
|
||||
format: 'html',
|
||||
includeVoiceTranscripts: true,
|
||||
kinds: expect.arrayContaining(['voice'])
|
||||
})
|
||||
})
|
||||
})
|
||||
@@ -29,7 +29,28 @@ describe('VoicePlayer', () => {
|
||||
getVoiceData: vi.fn().mockResolvedValue({
|
||||
success: true,
|
||||
data: 'UklGRiQAAABXQVZFZm10IBAAAAABAAEAQB8AAEAfAAABAAgAZGF0YQAAAAA='
|
||||
})
|
||||
}),
|
||||
getVoiceModelStatus: vi.fn().mockResolvedValue({
|
||||
modelId: 'sensevoice-small-int8',
|
||||
version: 'fixture',
|
||||
state: 'ready',
|
||||
downloadedBytes: 10,
|
||||
totalBytes: 10,
|
||||
progress: 1,
|
||||
platform: 'win32',
|
||||
architecture: 'x64',
|
||||
supported: true
|
||||
}),
|
||||
recognizeVoice: vi.fn().mockResolvedValue({
|
||||
success: true,
|
||||
transcript: '这是固定的测试转写',
|
||||
language: 'zh',
|
||||
cached: false
|
||||
}),
|
||||
downloadVoiceModel: vi.fn(),
|
||||
cancelVoiceModelDownload: vi.fn(),
|
||||
cancelVoiceRecognition: vi.fn(),
|
||||
onVoiceModelProgress: vi.fn(() => vi.fn())
|
||||
} as typeof window.api
|
||||
})
|
||||
|
||||
@@ -44,4 +65,42 @@ describe('VoicePlayer', () => {
|
||||
expect(container.querySelector('.voice-icon')).toHaveClass('playing')
|
||||
expect(screen.queryByText('当前版本暂不支持播放')).not.toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('recognizes one voice message and renders the transcript', async () => {
|
||||
render(<VoicePlayer sessionId="filehelper" localId={11} createTime={1785553200} duration={1} />)
|
||||
await userEvent.click(screen.getByRole('button', { name: '转文字' }))
|
||||
|
||||
await waitFor(() =>
|
||||
expect(window.api.recognizeVoice).toHaveBeenCalledWith({
|
||||
sessionId: 'filehelper',
|
||||
localId: 11,
|
||||
createTime: 1785553200,
|
||||
svrId: undefined
|
||||
})
|
||||
)
|
||||
expect(await screen.findByText('这是固定的测试转写')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('opens centralized settings when recognition assets are missing', async () => {
|
||||
vi.mocked(window.api.getVoiceModelStatus).mockResolvedValue({
|
||||
modelId: 'sensevoice-small-int8',
|
||||
version: 'fixture',
|
||||
state: 'missing',
|
||||
downloadedBytes: 0,
|
||||
totalBytes: 239_549_735,
|
||||
progress: 0,
|
||||
platform: 'win32',
|
||||
architecture: 'x64',
|
||||
supported: true
|
||||
})
|
||||
render(<VoicePlayer sessionId="filehelper" localId={12} createTime={1785553300} duration={2} />)
|
||||
const openSettings = vi.fn()
|
||||
window.addEventListener('wxe:open-voice-recognition-settings', openSettings, { once: true })
|
||||
await userEvent.click(screen.getByRole('button', { name: '转文字' }))
|
||||
|
||||
expect(await screen.findByText(/请先在设置中准备离线语音模型/)).toBeInTheDocument()
|
||||
await userEvent.click(screen.getByRole('button', { name: '前往设置' }))
|
||||
expect(openSettings).toHaveBeenCalledOnce()
|
||||
expect(window.api.recognizeVoice).not.toHaveBeenCalled()
|
||||
})
|
||||
})
|
||||
|
||||
@@ -0,0 +1,90 @@
|
||||
import { render, screen, waitFor } from '@testing-library/react'
|
||||
import userEvent from '@testing-library/user-event'
|
||||
import { beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
import { VoiceRecognitionPage } from '../../src/renderer/src/features/settings/pages/VoiceRecognitionPage'
|
||||
import type { VoiceModelStatus } from '../../src/shared/voice-recognition'
|
||||
|
||||
const readyStatus: VoiceModelStatus = {
|
||||
modelId: 'sensevoice-small-int8',
|
||||
version: '2024-07-17',
|
||||
state: 'ready',
|
||||
downloadedBytes: 239_549_735,
|
||||
totalBytes: 239_549_735,
|
||||
progress: 1,
|
||||
platform: 'win32',
|
||||
architecture: 'x64',
|
||||
supported: true
|
||||
}
|
||||
|
||||
describe('voice recognition settings', () => {
|
||||
beforeEach(() => {
|
||||
window.api = {
|
||||
getVoiceModelStatus: vi.fn().mockResolvedValue(readyStatus),
|
||||
downloadVoiceModel: vi.fn(),
|
||||
cancelVoiceModelDownload: vi.fn(),
|
||||
removeVoiceModel: vi.fn().mockResolvedValue({ ...readyStatus, state: 'missing' }),
|
||||
openVoiceModelDirectory: vi.fn().mockResolvedValue({ success: true }),
|
||||
onVoiceModelProgress: vi.fn(() => vi.fn())
|
||||
} as typeof window.api
|
||||
})
|
||||
|
||||
it('shows Windows runtime and installed model actions', async () => {
|
||||
render(<VoiceRecognitionPage onNotice={vi.fn()} />)
|
||||
expect(await screen.findByText('Windows 64 位')).toBeInTheDocument()
|
||||
expect(screen.queryByText('额外环境')).not.toBeInTheDocument()
|
||||
expect(screen.getByRole('link', { name: 'SenseVoice(MIT)' })).toBeInTheDocument()
|
||||
expect(screen.getByRole('link', { name: 'sherpa-onnx(Apache-2.0)' })).toBeInTheDocument()
|
||||
expect(screen.getByRole('button', { name: '打开模型目录' })).toBeInTheDocument()
|
||||
expect(screen.getByRole('button', { name: '删除模型' })).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('downloads the model from the centralized settings page', async () => {
|
||||
const missing = { ...readyStatus, state: 'missing' as const, progress: 0, downloadedBytes: 0 }
|
||||
vi.mocked(window.api.getVoiceModelStatus).mockResolvedValue(missing)
|
||||
vi.mocked(window.api.downloadVoiceModel).mockResolvedValue({
|
||||
success: true,
|
||||
status: readyStatus
|
||||
})
|
||||
const notice = vi.fn()
|
||||
render(<VoiceRecognitionPage onNotice={notice} />)
|
||||
await userEvent.click(await screen.findByRole('button', { name: '下载模型' }))
|
||||
|
||||
await waitFor(() => expect(window.api.downloadVoiceModel).toHaveBeenCalledOnce())
|
||||
expect(notice).toHaveBeenCalledWith('离线语音模型已准备好')
|
||||
})
|
||||
|
||||
it('shows download percentage in the header and model card', async () => {
|
||||
const missing = {
|
||||
...readyStatus,
|
||||
state: 'missing' as const,
|
||||
progress: 0,
|
||||
downloadedBytes: 0
|
||||
}
|
||||
let progressListener: ((status: VoiceModelStatus) => void) | undefined
|
||||
let finishDownload:
|
||||
| ((value: { success: boolean; status: VoiceModelStatus }) => void)
|
||||
| undefined
|
||||
vi.mocked(window.api.getVoiceModelStatus).mockResolvedValue(missing)
|
||||
vi.mocked(window.api.onVoiceModelProgress).mockImplementation((listener) => {
|
||||
progressListener = listener
|
||||
return vi.fn()
|
||||
})
|
||||
vi.mocked(window.api.downloadVoiceModel).mockReturnValue(
|
||||
new Promise((resolve) => {
|
||||
finishDownload = resolve
|
||||
})
|
||||
)
|
||||
render(<VoiceRecognitionPage onNotice={vi.fn()} />)
|
||||
await userEvent.click(await screen.findByRole('button', { name: '下载模型' }))
|
||||
progressListener?.({
|
||||
...missing,
|
||||
state: 'downloading',
|
||||
downloadedBytes: Math.round(missing.totalBytes * 0.42),
|
||||
progress: 0.42
|
||||
})
|
||||
|
||||
expect(await screen.findByText('下载中 42%')).toBeInTheDocument()
|
||||
expect(screen.getByText('正在下载 42%')).toBeInTheDocument()
|
||||
finishDownload?.({ success: true, status: readyStatus })
|
||||
})
|
||||
})
|
||||
Reference in New Issue
Block a user