feat: 新增 Windows 本地图片文字识别能力

This commit is contained in:
电摇小子
2026-09-16 00:23:23 +08:00
parent e6db4de711
commit 24399f1d70
23 changed files with 1984 additions and 7 deletions
+13 -1
View File
@@ -6,6 +6,7 @@ import { AIProviderCard } from '../../src/renderer/src/features/settings/ai-mode
import { AIModelPage } from '../../src/renderer/src/features/settings/pages/AIModelPage'
import type { AIProviderSummary, AIRuntimeModelConfig } from '../../src/shared/ai-provider'
import type { AIVisionTestState } from '../../src/renderer/src/features/settings/ai-model/types'
import type { SystemOcrCapability } from '../../src/shared/system-ocr'
const runtime: AIRuntimeModelConfig = {
providerName: 'Not configured',
@@ -15,6 +16,16 @@ const runtime: AIRuntimeModelConfig = {
status: 'untested'
}
const systemOcrCapability: SystemOcrCapability = {
available: true,
engine: 'windows-system-ocr',
platform: 'win32',
arch: 'x64',
runtimeVersion: '1.2.0',
language: 'zh-Hans-CN',
message: '本地图片文字识别可用(Windows 系统 OCR,zh-Hans-CN)。'
}
const provider: AIProviderSummary = {
id: 'fixture-provider',
name: '本地假服务',
@@ -56,7 +67,8 @@ describe('AI model settings', () => {
})
window.api = {
listAIProviders: vi.fn().mockResolvedValue({ success: true, providers: [] }),
getAIRuntimeConfig: vi.fn().mockResolvedValue(runtime)
getAIRuntimeConfig: vi.fn().mockResolvedValue(runtime),
getSystemOcrCapability: vi.fn().mockResolvedValue(systemOcrCapability)
} as typeof window.api
})
@@ -0,0 +1,107 @@
import { fireEvent, render, screen, waitFor } from '@testing-library/react'
import { beforeEach, describe, expect, it, vi } from 'vitest'
import { LocalImageTextRecognition } from '../../src/renderer/src/features/settings/ai-model/LocalImageTextRecognition'
import type { SystemOcrCapability, SystemOcrResult } from '../../src/shared/system-ocr'
const availableCapability: SystemOcrCapability = {
available: true,
engine: 'windows-system-ocr',
platform: 'win32',
arch: 'x64',
runtimeVersion: '1.2.0',
language: 'zh-Hans-CN',
message: '本地图片文字识别可用(Windows 系统 OCR,zh-Hans-CN)。'
}
const unavailableCapability: SystemOcrCapability = {
available: false,
engine: 'windows-system-ocr',
platform: 'win32',
arch: 'x64',
runtimeVersion: '1.2.0',
language: null,
reason: 'LANGUAGE_UNAVAILABLE',
message: '当前 Windows 未安装可用的 OCR 语言支持。'
}
const successResult: SystemOcrResult = {
success: true,
text: 'TraceMemo 本地文字识别',
lines: [],
language: 'zh-Hans-CN',
engine: 'windows-system-ocr',
durationMs: 42
}
const emptyResult: SystemOcrResult = {
success: false,
text: '',
lines: [],
language: 'zh-Hans-CN',
engine: 'windows-system-ocr',
durationMs: 12,
errorCode: 'OCR_EMPTY_RESULT',
error: '没有在这张图片里识别到文字。'
}
const selectImage = (): void => {
const input = document.querySelector<HTMLInputElement>('input[type="file"]')
if (!input) throw new Error('file input missing')
const file = new File([new Uint8Array([0x89, 0x50, 0x4e, 0x47])], 'fixture.png', {
type: 'image/png'
})
fireEvent.change(input, { target: { files: [file] } })
}
describe('LocalImageTextRecognition', () => {
beforeEach(() => {
window.api = {
getSystemOcrCapability: vi.fn().mockResolvedValue(availableCapability),
recognizeLocalImageText: vi.fn().mockResolvedValue(successResult)
} as typeof window.api
})
it('shows local availability without requiring any AI provider', async () => {
render(<LocalImageTextRecognition />)
expect(await screen.findByText('本机可用')).toBeInTheDocument()
expect(screen.getByText(/组件 1\.2\.0/)).toBeInTheDocument()
expect(screen.getByRole('button', { name: '本地文字识别' })).toBeDisabled()
})
it('recognizes text locally and renders the extracted text', async () => {
render(<LocalImageTextRecognition />)
await screen.findByText('本机可用')
selectImage()
const button = await screen.findByRole('button', { name: '本地文字识别' })
await waitFor(() => expect(button).toBeEnabled())
fireEvent.click(button)
expect(await screen.findByText('TraceMemo 本地文字识别')).toBeInTheDocument()
expect(window.api.recognizeLocalImageText).toHaveBeenCalledWith({
imageDataUrl: expect.stringContaining('data:image/png;base64,')
})
})
it('surfaces an empty OCR result as a product message, not a native error', async () => {
vi.mocked(window.api.recognizeLocalImageText).mockResolvedValue(emptyResult)
render(<LocalImageTextRecognition />)
await screen.findByText('本机可用')
selectImage()
const button = await screen.findByRole('button', { name: '本地文字识别' })
await waitFor(() => expect(button).toBeEnabled())
fireEvent.click(button)
expect(await screen.findByText('没有在这张图片里识别到文字。')).toBeInTheDocument()
})
it('keeps the entry disabled and explains why when the language pack is missing', async () => {
vi.mocked(window.api.getSystemOcrCapability).mockResolvedValue(unavailableCapability)
render(<LocalImageTextRecognition />)
expect(await screen.findByText('本机不可用')).toBeInTheDocument()
expect(screen.getByText('当前 Windows 未安装可用的 OCR 语言支持。')).toBeInTheDocument()
expect(screen.getByRole('button', { name: '本地文字识别' })).toBeDisabled()
})
})