feat: add image hash-based OCR caching with 100MB size limit
- Implement SHA-256 image hashing to cache OCR results and avoid re-processing identical images - Add 100MB file size limit for image uploads with user-friendly error messages - Clear ghost suggestions when uploading new images to prevent interference - Optimize size limit calculation in copilot plugin to include OCR context - Remove debug logging from production code - Add image processing optimization plan document BREAKING CHANGE: Image upload size limit is now enforced at 100MB (previously unlimited)
This commit is contained in:
Binary file not shown.
+1
-4
@@ -46,7 +46,7 @@ Hard rules:
|
|||||||
Do NOT repeat text that already appears at the start of SUFFIX.
|
Do NOT repeat text that already appears at the start of SUFFIX.
|
||||||
3. Balanced length:
|
3. Balanced length:
|
||||||
Prefer concise but meaningful continuation, not ultra-short fragments.
|
Prefer concise but meaningful continuation, not ultra-short fragments.
|
||||||
Default target is 20-120 characters and 1-3 lines for plain prose.
|
Default target is 10-500 characters and 1-20 lines for plain prose.
|
||||||
You may be longer when structure requires it (lists, tables, code blocks, math blocks).
|
You may be longer when structure requires it (lists, tables, code blocks, math blocks).
|
||||||
4. Avoid trivial output:
|
4. Avoid trivial output:
|
||||||
Do not output only punctuation or filler such as ".", ",", ";", ":".
|
Do not output only punctuation or filler such as ".", ",", ";", ":".
|
||||||
@@ -89,6 +89,3 @@ Now produce the insertion.
|
|||||||
Output:"""
|
Output:"""
|
||||||
|
|
||||||
return prompt.strip()
|
return prompt.strip()
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,79 @@
|
|||||||
|
# 图片处理优化计划
|
||||||
|
|
||||||
|
## 需求概述
|
||||||
|
1. 图片上传大小限制为100MB
|
||||||
|
2. 为每个图片做哈希,相同哈希不重复调用OCR
|
||||||
|
3. 上传图片时打断之前的ghost text
|
||||||
|
|
||||||
|
## 需要修改的文件
|
||||||
|
|
||||||
|
### 1. `src/utils/ocrCache.js` - 扩展OCR缓存模块
|
||||||
|
|
||||||
|
**新增功能:**
|
||||||
|
- 图片哈希缓存:`imageHashCache` Map,用于存储 `hash -> ocrText` 的映射
|
||||||
|
- 哈希计算函数:使用 `crypto.subtle.digest('SHA-256', imageBytes)` 计算哈希
|
||||||
|
- 哈希检查函数:在OCR前检查哈希是否已存在
|
||||||
|
- 100MB大小限制常量
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
// 新增
|
||||||
|
export const IMAGE_SIZE_LIMIT = 100 * 1024 * 1024 // 100MB
|
||||||
|
|
||||||
|
export async function calculateImageHash(imageBytes) {
|
||||||
|
const hashBuffer = await crypto.subtle.digest('SHA-256', imageBytes)
|
||||||
|
const hashArray = Array.from(new Uint8Array(hashBuffer))
|
||||||
|
return hashArray.map(b => b.toString(16).padStart(2, '0')).join('')
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getOcrByHash(hash) {
|
||||||
|
return imageHashCache.get(hash) || ''
|
||||||
|
}
|
||||||
|
|
||||||
|
export function setOcrByHash(hash, text) {
|
||||||
|
imageHashCache.set(hash, text)
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2. `src/components/MilkdownEditor.vue` - 修改图片上传逻辑
|
||||||
|
|
||||||
|
**修改点:**
|
||||||
|
|
||||||
|
1. **`handleImageUpload` 函数 (约第392行)**
|
||||||
|
- 添加文件大小检查,超过100MB则提示错误
|
||||||
|
- 计算图片哈希,检查是否已存在OCR结果
|
||||||
|
- 上传图片前调用 `clearGhostSuggestion` 打断现有ghost text
|
||||||
|
|
||||||
|
2. **`performOCR` 函数 (约第212行)**
|
||||||
|
- 接收哈希参数,OCR完成后存储到哈希缓存
|
||||||
|
|
||||||
|
3. **Milkdown `onUpload` 回调 (约第267行)**
|
||||||
|
- 同样添加大小限制和哈希检查
|
||||||
|
|
||||||
|
### 3. `src/plugins/copilotPlugin.ts` - 可能需要导出清除函数
|
||||||
|
|
||||||
|
- 确保 `clearGhostSuggestion` 可以被外部调用(目前已导出)
|
||||||
|
|
||||||
|
## 实现步骤
|
||||||
|
|
||||||
|
```mermaid
|
||||||
|
graph TD
|
||||||
|
A[用户上传图片] --> B{文件大小 <= 100MB?}
|
||||||
|
B -->|否| C[提示文件过大错误]
|
||||||
|
B -->|是| D[计算图片哈希]
|
||||||
|
D --> E{哈希已存在OCR结果?}
|
||||||
|
E -->|是| F[直接使用缓存的OCR结果]
|
||||||
|
E -->|否| G[调用OCR API]
|
||||||
|
G --> H[存储OCR结果到哈希缓存]
|
||||||
|
F --> I[打断现有ghost text]
|
||||||
|
H --> I
|
||||||
|
I --> J[插入图片到编辑器]
|
||||||
|
```
|
||||||
|
|
||||||
|
## 关键代码修改位置
|
||||||
|
|
||||||
|
| 文件 | 函数/位置 | 修改内容 |
|
||||||
|
|------|-----------|----------|
|
||||||
|
| `src/utils/ocrCache.js` | 新增 | 添加哈希相关函数和常量 |
|
||||||
|
| `src/components/MilkdownEditor.vue` | `handleImageUpload` | 添加大小检查、哈希检查、打断ghost text |
|
||||||
|
| `src/components/MilkdownEditor.vue` | `performOCR` | 接收哈希参数 |
|
||||||
|
| `src/components/MilkdownEditor.vue` | `onUpload` | Milkdown上传回调添加同样逻辑 |
|
||||||
@@ -108,8 +108,8 @@ import { editorViewCtx, serializerCtx } from '@milkdown/kit/core'
|
|||||||
import { Selection } from '@milkdown/prose/state'
|
import { Selection } from '@milkdown/prose/state'
|
||||||
import { copilotPlugin, copilotConfigCtx, copilotGhostMark, setCopilotEnabled, COPILOT_PLUGIN_KEY, SIZE_LIMIT, checkSizeLimit, clearGhostSuggestion } from '../plugins/copilotPlugin'
|
import { copilotPlugin, copilotConfigCtx, copilotGhostMark, setCopilotEnabled, COPILOT_PLUGIN_KEY, SIZE_LIMIT, checkSizeLimit, clearGhostSuggestion } from '../plugins/copilotPlugin'
|
||||||
import { fetchSuggestion } from '../utils/api.js'
|
import { fetchSuggestion } from '../utils/api.js'
|
||||||
import { DEBUG, OCR_URL } from '../utils/config.js'
|
import { OCR_URL } from '../utils/config.js'
|
||||||
import { setOcrCache, clearOcrCache, clearAllOcrCache } from '../utils/ocrCache.js'
|
import { setOcrCache, clearOcrCache, clearAllOcrCache, IMAGE_SIZE_LIMIT, calculateImageHash, getOcrByHash, setOcrByHash } from '../utils/ocrCache.js'
|
||||||
|
|
||||||
const emit = defineEmits(['update:markdown'])
|
const emit = defineEmits(['update:markdown'])
|
||||||
|
|
||||||
@@ -130,7 +130,6 @@ const aiButtonLabel = computed(() => {
|
|||||||
|
|
||||||
let crepe = null
|
let crepe = null
|
||||||
let markdownSyncTimer = null
|
let markdownSyncTimer = null
|
||||||
let debugLogTimer = null
|
|
||||||
const objectUrls = new Set()
|
const objectUrls = new Set()
|
||||||
const IMAGE_NODE_TYPES = new Set(['image', 'image-block', 'imageBlock'])
|
const IMAGE_NODE_TYPES = new Set(['image', 'image-block', 'imageBlock'])
|
||||||
|
|
||||||
@@ -201,67 +200,24 @@ const scheduleMarkdownSync = () => {
|
|||||||
const markdown = await crepe.getMarkdown()
|
const markdown = await crepe.getMarkdown()
|
||||||
emit('update:markdown', markdown)
|
emit('update:markdown', markdown)
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
if (DEBUG) console.error('[Markdown] Sync failed:', e)
|
// sync error, ignore
|
||||||
}
|
}
|
||||||
}, 120)
|
}, 120)
|
||||||
}
|
}
|
||||||
|
|
||||||
const logDebugInfo = async () => {
|
|
||||||
if (!crepe) return
|
|
||||||
try {
|
|
||||||
const markdown = await crepe.getMarkdown()
|
|
||||||
crepe.editor.action((ctx) => {
|
|
||||||
const view = ctx.get(editorViewCtx)
|
|
||||||
const schema = view.state.schema
|
|
||||||
const { from, to } = view.state.selection
|
|
||||||
const serializer = ctx.get(serializerCtx)
|
|
||||||
let prefixMarkdown = '', suffixMarkdown = ''
|
|
||||||
|
|
||||||
try {
|
|
||||||
// Prefix: 使用 slice 创建文档节点
|
|
||||||
const prefixSlice = view.state.doc.slice(0, from)
|
|
||||||
if (prefixSlice.content.size > 0) {
|
|
||||||
const prefixDoc = schema.topNodeType.createAndFill(undefined, prefixSlice.content)
|
|
||||||
if (prefixDoc) {
|
|
||||||
prefixMarkdown = serializer(prefixDoc)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (!prefixMarkdown) {
|
|
||||||
prefixMarkdown = view.state.doc.textBetween(0, from, '\n', '\n')
|
|
||||||
}
|
|
||||||
|
|
||||||
// Suffix
|
|
||||||
const suffixSlice = view.state.doc.slice(to)
|
|
||||||
if (suffixSlice.content.size > 0) {
|
|
||||||
const suffixDoc = schema.topNodeType.createAndFill(undefined, suffixSlice.content)
|
|
||||||
if (suffixDoc) {
|
|
||||||
suffixMarkdown = serializer(suffixDoc)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (!suffixMarkdown) {
|
|
||||||
suffixMarkdown = view.state.doc.textBetween(to, view.state.doc.content.size, '\n', '\n')
|
|
||||||
}
|
|
||||||
} catch (e) {
|
|
||||||
console.error('[Debug] Serializer error:', e)
|
|
||||||
prefixMarkdown = view.state.doc.textBetween(0, from, '\n', '\n')
|
|
||||||
suffixMarkdown = view.state.doc.textBetween(to, view.state.doc.content.size, '\n', '\n')
|
|
||||||
}
|
|
||||||
console.log('[Debug] ===== Document State =====')
|
|
||||||
console.log('[Debug] PREFIX:', prefixMarkdown)
|
|
||||||
console.log('[Debug] SUFFIX:', suffixMarkdown)
|
|
||||||
console.log('[Debug] FULL MARKDOWN:', markdown)
|
|
||||||
console.log('[Debug] ==========================')
|
|
||||||
})
|
|
||||||
} catch (e) {
|
|
||||||
console.error('[Debug] Log failed:', e)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const clearCurrentSuggestion = (view) => {
|
const clearCurrentSuggestion = (view) => {
|
||||||
clearGhostSuggestion(view)
|
clearGhostSuggestion(view)
|
||||||
}
|
}
|
||||||
|
|
||||||
const performOCR = async (file, cacheKey) => {
|
const clearCurrentGhost = () => {
|
||||||
|
if (!crepe) return
|
||||||
|
crepe.editor.action((ctx) => {
|
||||||
|
const view = ctx.get(editorViewCtx)
|
||||||
|
clearGhostSuggestion(view)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
const performOCR = async (file, cacheKey, imageHash = '') => {
|
||||||
if (!aiEnabled.value) return
|
if (!aiEnabled.value) return
|
||||||
|
|
||||||
const reader = new FileReader()
|
const reader = new FileReader()
|
||||||
@@ -289,6 +245,9 @@ const performOCR = async (file, cacheKey) => {
|
|||||||
if (data.text) {
|
if (data.text) {
|
||||||
setOcrCache(cacheKey, data.text)
|
setOcrCache(cacheKey, data.text)
|
||||||
setOcrCache(file.name, data.text)
|
setOcrCache(file.name, data.text)
|
||||||
|
if (imageHash) {
|
||||||
|
setOcrByHash(imageHash, data.text)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error('[OCR] Error:', e)
|
console.error('[OCR] Error:', e)
|
||||||
@@ -298,10 +257,8 @@ const performOCR = async (file, cacheKey) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
onMounted(async () => {
|
onMounted(async () => {
|
||||||
if (DEBUG) console.log('[Debug] onMounted called')
|
|
||||||
if (!root.value) throw new Error('root.value is null')
|
if (!root.value) throw new Error('root.value is null')
|
||||||
|
|
||||||
if (DEBUG) console.log('[Debug] Creating Crepe editor...')
|
|
||||||
crepe = new Crepe({
|
crepe = new Crepe({
|
||||||
root: root.value,
|
root: root.value,
|
||||||
defaultValue: '# Welcome to LLM in text\n\nStart writing your content here...',
|
defaultValue: '# Welcome to LLM in text\n\nStart writing your content here...',
|
||||||
@@ -318,10 +275,24 @@ onMounted(async () => {
|
|||||||
inlineEditConfirm: 'Escape'
|
inlineEditConfirm: 'Escape'
|
||||||
},
|
},
|
||||||
[Crepe.Feature.ImageBlock]: {
|
[Crepe.Feature.ImageBlock]: {
|
||||||
onUpload: (file) => {
|
onUpload: async (file) => {
|
||||||
|
if (file.size > IMAGE_SIZE_LIMIT) {
|
||||||
|
alert(`图片大小不能超过 ${Math.floor(IMAGE_SIZE_LIMIT / 1024 / 1024)}MB`)
|
||||||
|
return null
|
||||||
|
}
|
||||||
const objectUrl = URL.createObjectURL(file)
|
const objectUrl = URL.createObjectURL(file)
|
||||||
objectUrls.add(objectUrl)
|
objectUrls.add(objectUrl)
|
||||||
performOCR(file, objectUrl)
|
const arrayBuffer = await file.arrayBuffer()
|
||||||
|
const imageBytes = new Uint8Array(arrayBuffer)
|
||||||
|
const imageHash = await calculateImageHash(imageBytes)
|
||||||
|
const existingOcr = getOcrByHash(imageHash)
|
||||||
|
if (!existingOcr) {
|
||||||
|
performOCR(file, objectUrl, imageHash)
|
||||||
|
} else {
|
||||||
|
setOcrCache(objectUrl, existingOcr)
|
||||||
|
setOcrCache(file.name, existingOcr)
|
||||||
|
}
|
||||||
|
clearCurrentGhost()
|
||||||
return objectUrl
|
return objectUrl
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -358,9 +329,6 @@ onMounted(async () => {
|
|||||||
refreshSizeAndLimit(ctx)
|
refreshSizeAndLimit(ctx)
|
||||||
})
|
})
|
||||||
scheduleMarkdownSync()
|
scheduleMarkdownSync()
|
||||||
debugLogTimer = setInterval(logDebugInfo, 20000)
|
|
||||||
|
|
||||||
if (DEBUG) console.log('[Debug] Crepe editor created with copilot plugin')
|
|
||||||
})
|
})
|
||||||
|
|
||||||
const exportMarkdown = async () => {
|
const exportMarkdown = async () => {
|
||||||
@@ -450,9 +418,27 @@ const handleImageUpload = async (event) => {
|
|||||||
const file = event.target.files?.[0]
|
const file = event.target.files?.[0]
|
||||||
if (!file) return
|
if (!file) return
|
||||||
|
|
||||||
|
if (file.size > IMAGE_SIZE_LIMIT) {
|
||||||
|
alert(`图片大小不能超过 ${Math.floor(IMAGE_SIZE_LIMIT / 1024 / 1024)}MB`)
|
||||||
|
event.target.value = ''
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
const objectUrl = URL.createObjectURL(file)
|
const objectUrl = URL.createObjectURL(file)
|
||||||
objectUrls.add(objectUrl)
|
objectUrls.add(objectUrl)
|
||||||
performOCR(file, objectUrl)
|
|
||||||
|
const arrayBuffer = await file.arrayBuffer()
|
||||||
|
const imageBytes = new Uint8Array(arrayBuffer)
|
||||||
|
const imageHash = await calculateImageHash(imageBytes)
|
||||||
|
const existingOcr = getOcrByHash(imageHash)
|
||||||
|
if (!existingOcr) {
|
||||||
|
performOCR(file, objectUrl, imageHash)
|
||||||
|
} else {
|
||||||
|
setOcrCache(objectUrl, existingOcr)
|
||||||
|
setOcrCache(file.name, existingOcr)
|
||||||
|
}
|
||||||
|
|
||||||
|
clearCurrentGhost()
|
||||||
insertImageAtCursor(objectUrl)
|
insertImageAtCursor(objectUrl)
|
||||||
|
|
||||||
event.target.value = ''
|
event.target.value = ''
|
||||||
@@ -472,10 +458,6 @@ onUnmounted(() => {
|
|||||||
clearTimeout(markdownSyncTimer)
|
clearTimeout(markdownSyncTimer)
|
||||||
markdownSyncTimer = null
|
markdownSyncTimer = null
|
||||||
}
|
}
|
||||||
if (debugLogTimer) {
|
|
||||||
clearInterval(debugLogTimer)
|
|
||||||
debugLogTimer = null
|
|
||||||
}
|
|
||||||
|
|
||||||
for (const url of Array.from(objectUrls)) {
|
for (const url of Array.from(objectUrls)) {
|
||||||
revokeObjectUrl(url)
|
revokeObjectUrl(url)
|
||||||
|
|||||||
@@ -9,7 +9,6 @@ import { getOcrCache, OCR_SIZE_LIMIT, extractTextFromOCR } from '../utils/ocrCac
|
|||||||
const COPILOT_PLUGIN_KEY = new PluginKey('milkdown-copilot')
|
const COPILOT_PLUGIN_KEY = new PluginKey('milkdown-copilot')
|
||||||
const DEBOUNCE_MS = 1000
|
const DEBOUNCE_MS = 1000
|
||||||
const SIZE_LIMIT = OCR_SIZE_LIMIT
|
const SIZE_LIMIT = OCR_SIZE_LIMIT
|
||||||
const DEBUG = true
|
|
||||||
const IMAGE_NODE_TYPES = new Set(['image', 'image-block', 'imageBlock'])
|
const IMAGE_NODE_TYPES = new Set(['image', 'image-block', 'imageBlock'])
|
||||||
|
|
||||||
interface CopilotState {
|
interface CopilotState {
|
||||||
@@ -334,12 +333,7 @@ function scheduleFetch(view: EditorView, runtime: CopilotRuntime, pos: number) {
|
|||||||
|
|
||||||
const doc = view.state.doc
|
const doc = view.state.doc
|
||||||
const schema = view.state.schema
|
const schema = view.state.schema
|
||||||
const overLimit = doc.content.size > SIZE_LIMIT
|
const baseSize = doc.content.size
|
||||||
|
|
||||||
if (overLimit) {
|
|
||||||
setCopilotEnabled(view, false)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
const serializer = runtime.ctx.get(serializerCtx)
|
const serializer = runtime.ctx.get(serializerCtx)
|
||||||
let prefixMarkdown = ''
|
let prefixMarkdown = ''
|
||||||
@@ -362,12 +356,15 @@ function scheduleFetch(view: EditorView, runtime: CopilotRuntime, pos: number) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const requestPrefix = `${prefixMarkdown}${buildOcrContextForRequest(doc, pos)}`
|
const requestPrefix = `${prefixMarkdown}${buildOcrContextForRequest(doc, pos)}`
|
||||||
|
const totalTextLen = (prefixMarkdown + suffixMarkdown).length
|
||||||
|
const ocrContextLen = requestPrefix.length - prefixMarkdown.length
|
||||||
|
const totalWithOcr = totalTextLen + ocrContextLen
|
||||||
|
|
||||||
if (DEBUG) {
|
const overLimit = totalWithOcr > SIZE_LIMIT
|
||||||
console.log('[Copilot] ===== LLM Request =====')
|
|
||||||
console.log('[Copilot] PREFIX:', requestPrefix)
|
if (overLimit) {
|
||||||
console.log('[Copilot] SUFFIX:', suffixMarkdown)
|
setCopilotEnabled(view, false)
|
||||||
console.log('[Copilot] ======================')
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if (runtime.debounceTimer) {
|
if (runtime.debounceTimer) {
|
||||||
|
|||||||
+4
-10
@@ -1,7 +1,6 @@
|
|||||||
import { DEBUG, API_URL } from './config.js'
|
import { API_URL } from './config.js'
|
||||||
|
|
||||||
export async function fetchSuggestion(prefix, suffix, signal, apiUrl = API_URL) {
|
export async function fetchSuggestion(prefix, suffix, signal, apiUrl = API_URL) {
|
||||||
if (DEBUG) console.log('[Debug] fetchSuggestion called with prefix length:', prefix.length, 'suffix length:', suffix.length)
|
|
||||||
try {
|
try {
|
||||||
const res = await fetch(apiUrl, {
|
const res = await fetch(apiUrl, {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
@@ -10,7 +9,6 @@ export async function fetchSuggestion(prefix, suffix, signal, apiUrl = API_URL)
|
|||||||
signal
|
signal
|
||||||
})
|
})
|
||||||
|
|
||||||
if (DEBUG) console.log('[Debug] fetchSuggestion response status:', res.status)
|
|
||||||
if (!res.ok) {
|
if (!res.ok) {
|
||||||
const errorText = await res.text()
|
const errorText = await res.text()
|
||||||
throw new Error(`HTTP ${res.status}: ${errorText}`)
|
throw new Error(`HTTP ${res.status}: ${errorText}`)
|
||||||
@@ -18,7 +16,6 @@ export async function fetchSuggestion(prefix, suffix, signal, apiUrl = API_URL)
|
|||||||
|
|
||||||
const reader = res.body?.getReader()
|
const reader = res.body?.getReader()
|
||||||
if (!reader) {
|
if (!reader) {
|
||||||
if (DEBUG) console.log('[Debug] No reader available')
|
|
||||||
throw new Error('No reader available')
|
throw new Error('No reader available')
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -40,23 +37,20 @@ export async function fetchSuggestion(prefix, suffix, signal, apiUrl = API_URL)
|
|||||||
const data = JSON.parse(jsonStr)
|
const data = JSON.parse(jsonStr)
|
||||||
if (data.content) {
|
if (data.content) {
|
||||||
text += data.content
|
text += data.content
|
||||||
if (DEBUG) console.log('[Debug] Added content:', data.content)
|
|
||||||
}
|
}
|
||||||
if (data.done || data.error) break
|
if (data.done || data.error) break
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
if (DEBUG) console.warn('[Debug] JSON parse error for:', jsonStr.substring(0, 50))
|
// skip invalid lines
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (DEBUG) console.log('[Debug] Final suggestion text:', text.substring(0, 100))
|
|
||||||
return text
|
return text
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
if (e.name === 'AbortError') {
|
if (e.name === 'AbortError') {
|
||||||
if (DEBUG) console.log('[Debug] Request aborted')
|
// ignore abort
|
||||||
} else {
|
} else {
|
||||||
if (DEBUG) console.error('[Debug] fetchSuggestion error:', e)
|
throw e
|
||||||
}
|
}
|
||||||
throw e
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+17
-1
@@ -1,6 +1,22 @@
|
|||||||
const SIZE_LIMIT = 64 * 1024
|
const SIZE_LIMIT = 32 * 1024
|
||||||
|
export const IMAGE_SIZE_LIMIT = 100 * 1024 * 1024
|
||||||
|
|
||||||
const ocrCache = new Map()
|
const ocrCache = new Map()
|
||||||
|
const imageHashCache = new Map()
|
||||||
|
|
||||||
|
export async function calculateImageHash(imageBytes) {
|
||||||
|
const hashBuffer = await crypto.subtle.digest('SHA-256', imageBytes)
|
||||||
|
const hashArray = Array.from(new Uint8Array(hashBuffer))
|
||||||
|
return hashArray.map(b => b.toString(16).padStart(2, '0')).join('')
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getOcrByHash(hash) {
|
||||||
|
return imageHashCache.get(hash) || ''
|
||||||
|
}
|
||||||
|
|
||||||
|
export function setOcrByHash(hash, text) {
|
||||||
|
imageHashCache.set(hash, text)
|
||||||
|
}
|
||||||
|
|
||||||
export function setOcrCache(filename, text) {
|
export function setOcrCache(filename, text) {
|
||||||
ocrCache.set(filename, text)
|
ocrCache.set(filename, text)
|
||||||
|
|||||||
Reference in New Issue
Block a user