feat: add screenshot OCR extraction
This commit is contained in:
+16
-1
@@ -23,6 +23,7 @@ import {
|
||||
SaveRegionImage,
|
||||
SaveRegionToRemote,
|
||||
SummarizeRegion,
|
||||
ExtractTextRegion,
|
||||
CancelRegion,
|
||||
GetConfig,
|
||||
} from '../wailsjs/go/main/App'
|
||||
@@ -58,7 +59,7 @@ async function loadThemePreference() {
|
||||
// `SettingsTab` is hoisted to the App shell so the sidebar (which lives
|
||||
// here) and the inner SettingsView (which renders the matching card) can
|
||||
// share a single source of truth without an event bus.
|
||||
type SettingsTab = 'general' | 's3' | 'ssh' | 'llm'
|
||||
type SettingsTab = 'general' | 's3' | 'ssh' | 'llm' | 'ocr'
|
||||
const activeTab = ref<SettingsTab>('general')
|
||||
|
||||
// Sidebar entries are declarative so adding a destination type later is
|
||||
@@ -68,6 +69,7 @@ const sidebarItems: Array<{ id: SettingsTab; label: string }> = [
|
||||
{ id: 's3', label: '对象存储' },
|
||||
{ id: 'ssh', label: '远程主机' },
|
||||
{ id: 'llm', label: '智能识图' },
|
||||
{ id: 'ocr', label: '文字提取' },
|
||||
]
|
||||
|
||||
interface OverlayPayload {
|
||||
@@ -192,6 +194,16 @@ async function onOverlaySummarize(rect: OverlayResult) {
|
||||
}
|
||||
}
|
||||
|
||||
async function onOverlayOCR(rect: OverlayResult) {
|
||||
mode.value = 'settings'
|
||||
overlayPayload.value = null
|
||||
try {
|
||||
await ExtractTextRegion(rect as any)
|
||||
} catch {
|
||||
/* Surfaced via upload:failure */
|
||||
}
|
||||
}
|
||||
|
||||
async function onOverlayCancel() {
|
||||
mode.value = 'settings'
|
||||
overlayPayload.value = null
|
||||
@@ -223,6 +235,8 @@ onMounted(() => {
|
||||
'success',
|
||||
url === 'summary copied to clipboard'
|
||||
? 'Summary copied to clipboard'
|
||||
: url === 'ocr text copied to clipboard'
|
||||
? 'OCR text copied to clipboard'
|
||||
: `Copied: ${url}`
|
||||
)
|
||||
})
|
||||
@@ -262,6 +276,7 @@ onUnmounted(() => {
|
||||
@save="onOverlaySave"
|
||||
@save-remote="onOverlaySaveRemote"
|
||||
@summarize="onOverlaySummarize"
|
||||
@ocr="onOverlayOCR"
|
||||
@cancel="onOverlayCancel"
|
||||
/>
|
||||
|
||||
|
||||
@@ -58,6 +58,10 @@ const emit = defineEmits<{
|
||||
e: 'summarize',
|
||||
payload: { rect: Rect; annotations: Annotation[] }
|
||||
): void
|
||||
(
|
||||
e: 'ocr',
|
||||
payload: { rect: Rect; annotations: Annotation[] }
|
||||
): void
|
||||
(e: 'cancel'): void
|
||||
}>()
|
||||
|
||||
@@ -138,7 +142,7 @@ const sizeLabel = computed(() => {
|
||||
})
|
||||
|
||||
const MARK_TOOLBAR_W = 178
|
||||
const ACTION_TOOLBAR_W = 216
|
||||
const ACTION_TOOLBAR_W = 252
|
||||
const TOOLBAR_GROUP_GAP = 8
|
||||
|
||||
const toolOrder: Tool[] = ['pen', 'rect', 'ellipse', 'text']
|
||||
@@ -571,7 +575,11 @@ function onSummarize() {
|
||||
emitAction('summarize')
|
||||
}
|
||||
|
||||
function emitAction(action: 'confirm' | 'copy' | 'save' | 'save-remote' | 'summarize') {
|
||||
function onOCR() {
|
||||
emitAction('ocr')
|
||||
}
|
||||
|
||||
function emitAction(action: 'confirm' | 'copy' | 'save' | 'save-remote' | 'summarize' | 'ocr') {
|
||||
if (!rect.value) return
|
||||
commitTextDraft()
|
||||
const payload = {
|
||||
@@ -583,6 +591,7 @@ function emitAction(action: 'confirm' | 'copy' | 'save' | 'save-remote' | 'summa
|
||||
if (action === 'save') emit('save', payload)
|
||||
if (action === 'save-remote') emit('save-remote', payload)
|
||||
if (action === 'summarize') emit('summarize', payload)
|
||||
if (action === 'ocr') emit('ocr', payload)
|
||||
}
|
||||
|
||||
function onCancel() {
|
||||
@@ -991,6 +1000,21 @@ onUnmounted(() => {
|
||||
@click="onSaveRemote"
|
||||
v-html="saveRemoteIcon"
|
||||
/>
|
||||
<button
|
||||
class="action-btn"
|
||||
data-tip="提取文字"
|
||||
aria-label="提取文字"
|
||||
@click="onOCR"
|
||||
>
|
||||
<svg viewBox="0 0 24 24" aria-hidden="true">
|
||||
<path d="M5 8V5h3" />
|
||||
<path d="M16 5h3v3" />
|
||||
<path d="M19 16v3h-3" />
|
||||
<path d="M8 19H5v-3" />
|
||||
<path d="M9 15l3-7 3 7" />
|
||||
<path d="M10 13h4" />
|
||||
</svg>
|
||||
</button>
|
||||
<button
|
||||
class="action-btn"
|
||||
data-tip="复制总结"
|
||||
@@ -1124,7 +1148,7 @@ onUnmounted(() => {
|
||||
width: 178px;
|
||||
}
|
||||
.action-toolbar {
|
||||
width: 216px;
|
||||
width: 252px;
|
||||
}
|
||||
|
||||
.icon-btn,
|
||||
|
||||
@@ -9,6 +9,7 @@
|
||||
* • "s3" — S3-compatible object-storage credentials.
|
||||
* • "ssh" — SSH/SCP destination for the "save remote" button.
|
||||
* • "llm" — multimodal screenshot summary provider settings.
|
||||
* • "ocr" — cloud OCR provider settings for text extraction.
|
||||
*
|
||||
* State flow: load() pulls config from Go on mount, save() pushes back.
|
||||
*/
|
||||
@@ -23,7 +24,7 @@ import {
|
||||
|
||||
// Tab discriminator shared with the App shell. Kept as a string union so
|
||||
// the parent can pass the value without importing a type from this view.
|
||||
type TabId = 'general' | 's3' | 'ssh' | 'llm'
|
||||
type TabId = 'general' | 's3' | 'ssh' | 'llm' | 'ocr'
|
||||
type ThemeMode = 'auto' | 'light' | 'dark'
|
||||
|
||||
const props = defineProps<{
|
||||
@@ -74,12 +75,30 @@ interface LLMConfig {
|
||||
providers: Record<string, LLMProviderConfig>
|
||||
}
|
||||
|
||||
interface OCRProviderConfig {
|
||||
label: string
|
||||
endpoint: string
|
||||
accessKeyId: string
|
||||
accessKeySecret: string
|
||||
region: string
|
||||
service: string
|
||||
action: string
|
||||
version: string
|
||||
timeoutSecs: number
|
||||
}
|
||||
|
||||
interface OCRConfig {
|
||||
activeProvider: string
|
||||
providers: Record<string, OCRProviderConfig>
|
||||
}
|
||||
|
||||
interface AppConfig {
|
||||
hotkey: string
|
||||
theme: ThemeMode
|
||||
s3: S3Config
|
||||
ssh: SSHConfig
|
||||
llm: LLMConfig
|
||||
ocr: OCRConfig
|
||||
}
|
||||
|
||||
const llmProviderOptions = [
|
||||
@@ -88,6 +107,11 @@ const llmProviderOptions = [
|
||||
{ id: 'openai', label: 'ChatGPT / OpenAI-compatible' },
|
||||
]
|
||||
|
||||
const ocrProviderOptions = [
|
||||
{ id: 'aliyun', label: '阿里云文字识别' },
|
||||
{ id: 'volcengine', label: '火山引擎文字识别' },
|
||||
]
|
||||
|
||||
const themeOptions: Array<{ id: ThemeMode; label: string }> = [
|
||||
{ id: 'light', label: '浅色' },
|
||||
{ id: 'dark', label: '深色' },
|
||||
@@ -165,6 +189,33 @@ function defaultConfig(): AppConfig {
|
||||
},
|
||||
},
|
||||
},
|
||||
ocr: {
|
||||
activeProvider: 'aliyun',
|
||||
providers: {
|
||||
aliyun: {
|
||||
label: '阿里云文字识别',
|
||||
endpoint: 'https://ocr-api.cn-hangzhou.aliyuncs.com',
|
||||
accessKeyId: '',
|
||||
accessKeySecret: '',
|
||||
region: '',
|
||||
service: '',
|
||||
action: 'RecognizeGeneral',
|
||||
version: '2021-07-07',
|
||||
timeoutSecs: 30,
|
||||
},
|
||||
volcengine: {
|
||||
label: '火山引擎文字识别',
|
||||
endpoint: 'https://visual.volcengineapi.com',
|
||||
accessKeyId: '',
|
||||
accessKeySecret: '',
|
||||
region: 'cn-north-1',
|
||||
service: 'cv',
|
||||
action: 'OCRNormal',
|
||||
version: '2020-08-26',
|
||||
timeoutSecs: 30,
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
@@ -198,6 +249,15 @@ const activeLLMProvider = computed(() => {
|
||||
return config.value.llm.providers[id]
|
||||
})
|
||||
|
||||
const activeOCRProvider = computed(() => {
|
||||
const id = config.value.ocr.activeProvider || 'aliyun'
|
||||
if (!config.value.ocr.providers[id]) {
|
||||
config.value.ocr.activeProvider = 'aliyun'
|
||||
return config.value.ocr.providers.aliyun
|
||||
}
|
||||
return config.value.ocr.providers[id]
|
||||
})
|
||||
|
||||
function normalizeTheme(theme: unknown): ThemeMode {
|
||||
return theme === 'light' || theme === 'dark' || theme === 'auto'
|
||||
? theme
|
||||
@@ -225,6 +285,11 @@ async function load() {
|
||||
...defaultConfig().llm.providers,
|
||||
...((cfg as any).llm?.providers ?? {}),
|
||||
}
|
||||
merged.ocr = { ...merged.ocr, ...(cfg as any).ocr }
|
||||
merged.ocr.providers = {
|
||||
...defaultConfig().ocr.providers,
|
||||
...((cfg as any).ocr?.providers ?? {}),
|
||||
}
|
||||
merged.theme = normalizeTheme((cfg as any).theme)
|
||||
config.value = merged
|
||||
emit('theme-change', config.value.theme)
|
||||
@@ -586,6 +651,103 @@ onMounted(load)
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- OCR tab — cloud OCR provider settings for screenshot text extraction. -->
|
||||
<section v-if="props.tab === 'ocr'" class="card">
|
||||
<h2>文字提取</h2>
|
||||
<div class="grid">
|
||||
<label class="field full">
|
||||
<span>Provider</span>
|
||||
<select v-model="config.ocr.activeProvider">
|
||||
<option
|
||||
v-for="provider in ocrProviderOptions"
|
||||
:key="provider.id"
|
||||
:value="provider.id"
|
||||
>
|
||||
{{ provider.label }}
|
||||
</option>
|
||||
</select>
|
||||
</label>
|
||||
<label class="field full">
|
||||
<span>Endpoint *</span>
|
||||
<input
|
||||
v-model="activeOCRProvider.endpoint"
|
||||
placeholder="https://ocr-api.cn-hangzhou.aliyuncs.com"
|
||||
spellcheck="false"
|
||||
/>
|
||||
</label>
|
||||
<label class="field">
|
||||
<span>AccessKey ID *</span>
|
||||
<input v-model="activeOCRProvider.accessKeyId" spellcheck="false" />
|
||||
</label>
|
||||
<label class="field">
|
||||
<span>AccessKey Secret *</span>
|
||||
<input v-model="activeOCRProvider.accessKeySecret" type="password" />
|
||||
</label>
|
||||
<label
|
||||
v-if="config.ocr.activeProvider === 'volcengine'"
|
||||
class="field"
|
||||
>
|
||||
<span>Region *</span>
|
||||
<input
|
||||
v-model="activeOCRProvider.region"
|
||||
placeholder="cn-north-1"
|
||||
spellcheck="false"
|
||||
/>
|
||||
</label>
|
||||
<label
|
||||
v-if="config.ocr.activeProvider === 'volcengine'"
|
||||
class="field"
|
||||
>
|
||||
<span>Service *</span>
|
||||
<input
|
||||
v-model="activeOCRProvider.service"
|
||||
placeholder="cv"
|
||||
spellcheck="false"
|
||||
/>
|
||||
</label>
|
||||
<label class="field">
|
||||
<span>Action *</span>
|
||||
<input
|
||||
v-model="activeOCRProvider.action"
|
||||
placeholder="RecognizeGeneral / OCRNormal"
|
||||
spellcheck="false"
|
||||
/>
|
||||
</label>
|
||||
<label class="field">
|
||||
<span>Version *</span>
|
||||
<input
|
||||
v-model="activeOCRProvider.version"
|
||||
placeholder="2021-07-07 / 2020-08-26"
|
||||
spellcheck="false"
|
||||
/>
|
||||
</label>
|
||||
<label class="field">
|
||||
<span>Timeout (sec)</span>
|
||||
<input
|
||||
v-model.number="activeOCRProvider.timeoutSecs"
|
||||
type="number"
|
||||
min="5"
|
||||
max="120"
|
||||
placeholder="30"
|
||||
/>
|
||||
</label>
|
||||
</div>
|
||||
<p v-if="config.ocr.activeProvider === 'aliyun'" class="hint">
|
||||
阿里云默认使用 OCR API 的 <code>RecognizeGeneral</code>,请求体为截图
|
||||
PNG 原始数据,并使用 ACS3-HMAC-SHA256 签名。
|
||||
</p>
|
||||
<p v-else class="hint">
|
||||
火山引擎默认使用视觉智能的 <code>OCRNormal</code>,截图以
|
||||
<code>image_base64</code> 发送,并使用 Region/Service 参与签名。
|
||||
</p>
|
||||
|
||||
<div class="actions">
|
||||
<button class="btn primary" :disabled="saving" @click="save">
|
||||
{{ saving ? 'Saving…' : 'Save' }}
|
||||
</button>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<transition name="fade">
|
||||
<div
|
||||
v-if="message"
|
||||
|
||||
Reference in New Issue
Block a user