fix: 修复语音消息取错、时长精度与转写不刷新

- 时长解析:改取 <voicemsg voicelength>(毫秒),不再误取 length(SILK 字节数)
- 时长显示:四舍五入对齐微信口径,有原生时长时不被解码时长覆盖
- 转写:重新识别改为强制重算,跳过身份级缓存短路(音频级缓存仍生效)
- 日报:语音累计秒数显示取整,不再出现小数
This commit is contained in:
Wxw-Gu
2026-09-20 15:40:01 +08:00
parent 9b82d29037
commit 9593ca0f54
21 changed files with 435 additions and 52 deletions
+80 -7
View File
@@ -1,4 +1,4 @@
import { render, screen, waitFor } from '@testing-library/react'
import { act, render, screen, waitFor } from '@testing-library/react'
import userEvent from '@testing-library/user-event'
import { beforeEach, describe, expect, it, vi } from 'vitest'
import { VoicePlayer } from '../../src/renderer/src/components/VoicePlayer'
@@ -6,6 +6,8 @@ import { VoicePlayer } from '../../src/renderer/src/components/VoicePlayer'
const play = vi.fn(() => Promise.resolve())
const pause = vi.fn()
let lastAudio: FakeAudio | null = null
class FakeAudio {
preload = ''
src = ''
@@ -18,12 +20,27 @@ class FakeAudio {
pause = pause
load = vi.fn()
removeAttribute = vi.fn()
constructor() {
lastAudio = this
}
}
const renderPlayer = (duration?: number) =>
render(
<VoicePlayer
sessionId="filehelper"
localId={11}
createTime={1785553200}
duration={duration}
/>
)
describe('VoicePlayer', () => {
beforeEach(() => {
play.mockClear()
pause.mockClear()
lastAudio = null
vi.stubGlobal('Audio', FakeAudio)
window.api = {
getVoiceData: vi.fn().mockResolvedValue({
@@ -70,17 +87,73 @@ describe('VoicePlayer', () => {
render(<VoicePlayer sessionId="filehelper" localId={11} createTime={1785553200} duration={1} />)
await userEvent.click(screen.getByRole('button', { name: '转文字' }))
// 用户主动触发必须带 force:否则会被身份级缓存(不含 audio_hash)短路。
await waitFor(() =>
expect(window.api.recognizeVoice).toHaveBeenCalledWith({
sessionId: 'filehelper',
localId: 11,
createTime: 1785553200,
svrId: undefined
})
expect(window.api.recognizeVoice).toHaveBeenCalledWith(
{
sessionId: 'filehelper',
localId: 11,
createTime: 1785553200,
svrId: undefined
},
{ force: true }
)
)
expect(await screen.findByText('这是固定的测试转写')).toBeInTheDocument()
})
it('rounds the shown duration the way WeChat does', () => {
// 微信 4211ms 显示 4",所以口径是 round 不是 floor;进位要传给分钟位。
const cases: Array<[number | undefined, string]> = [
[1.7, '0:02'],
[4.211, '0:04'],
[7.505, '0:08'],
[59.6, '1:00'],
[119.6, '2:00'],
[3, '0:03'],
[7, '0:07'],
[15, '0:15'],
[0, '0:00'],
[undefined, '0:00'],
[NaN, '0:00']
]
for (const [duration, expected] of cases) {
const { container, unmount } = renderPlayer(duration)
expect(container.querySelector('.voice-duration')?.textContent).toBe(expected)
unmount()
}
})
it('keeps the WeChat duration instead of the systematically short decoded one', async () => {
const { container } = renderPlayer(1.979)
expect(container.querySelector('.voice-duration')?.textContent).toBe('0:02')
await userEvent.click(container.querySelector('.voice-message') as HTMLElement)
await waitFor(() => expect(lastAudio).not.toBeNull())
// Silk 解码时长比微信 length 系统性偏短(实测少 20–279ms),用它覆盖会把精度弄丢。
lastAudio!.duration = 1
await act(async () => {
lastAudio!.onloadedmetadata?.()
lastAudio!.ontimeupdate?.()
})
expect(container.querySelector('.voice-duration')?.textContent).toBe('0:02')
})
it('falls back to the decoded duration when WeChat did not provide one', async () => {
const { container } = renderPlayer()
expect(container.querySelector('.voice-duration')?.textContent).toBe('0:00')
await userEvent.click(container.querySelector('.voice-message') as HTMLElement)
await waitFor(() => expect(lastAudio).not.toBeNull())
lastAudio!.duration = 59.6
lastAudio!.onloadedmetadata?.()
await waitFor(() =>
expect(container.querySelector('.voice-duration')?.textContent).toBe('1:00')
)
})
it('opens centralized settings when recognition assets are missing', async () => {
vi.mocked(window.api.getVoiceModelStatus).mockResolvedValue({
modelId: 'sensevoice-small-int8',