init
这个提交包含在:
父节点
3b5a1eea26
当前提交
741771b365
二进制
models/ocr/Cls/原始分类器模型.onnx
普通文件
二进制
models/ocr/Cls/原始分类器模型.onnx
普通文件
二进制文件未显示。
二进制
models/ocr/Det/中文_OCRv2.onnx
普通文件
二进制
models/ocr/Det/中文_OCRv2.onnx
普通文件
二进制文件未显示。
二进制
models/ocr/Det/中文_OCRv3.onnx
普通文件
二进制
models/ocr/Det/中文_OCRv3.onnx
普通文件
二进制文件未显示。
二进制
models/ocr/Det/多语言.onnx
普通文件
二进制
models/ocr/Det/多语言.onnx
普通文件
二进制文件未显示。
二进制
models/ocr/Det/英文检测.onnx
普通文件
二进制
models/ocr/Det/英文检测.onnx
普通文件
二进制文件未显示。
6623
models/ocr/Keys/中文简体_OCRv2.txt
普通文件
6623
models/ocr/Keys/中文简体_OCRv2.txt
普通文件
文件差异内容过多而无法显示
加载差异
6623
models/ocr/Keys/中文简体_OCRv3.txt
普通文件
6623
models/ocr/Keys/中文简体_OCRv3.txt
普通文件
文件差异内容过多而无法显示
加载差异
8421
models/ocr/Keys/中文繁体.txt
普通文件
8421
models/ocr/Keys/中文繁体.txt
普通文件
文件差异内容过多而无法显示
加载差异
153
models/ocr/Keys/卡纳达文.txt
普通文件
153
models/ocr/Keys/卡纳达文.txt
普通文件
@ -0,0 +1,153 @@
|
|||||||
|
k
|
||||||
|
a
|
||||||
|
_
|
||||||
|
i
|
||||||
|
m
|
||||||
|
g
|
||||||
|
/
|
||||||
|
1
|
||||||
|
2
|
||||||
|
I
|
||||||
|
L
|
||||||
|
S
|
||||||
|
V
|
||||||
|
R
|
||||||
|
C
|
||||||
|
0
|
||||||
|
v
|
||||||
|
l
|
||||||
|
6
|
||||||
|
4
|
||||||
|
8
|
||||||
|
.
|
||||||
|
j
|
||||||
|
p
|
||||||
|
ಗ
|
||||||
|
ು
|
||||||
|
ಣ
|
||||||
|
ಪ
|
||||||
|
ಡ
|
||||||
|
ಿ
|
||||||
|
ಸ
|
||||||
|
ಲ
|
||||||
|
ಾ
|
||||||
|
ದ
|
||||||
|
್
|
||||||
|
7
|
||||||
|
5
|
||||||
|
3
|
||||||
|
ವ
|
||||||
|
ಷ
|
||||||
|
ಬ
|
||||||
|
ಹ
|
||||||
|
ೆ
|
||||||
|
9
|
||||||
|
ಅ
|
||||||
|
ಳ
|
||||||
|
ನ
|
||||||
|
ರ
|
||||||
|
ಉ
|
||||||
|
ಕ
|
||||||
|
ಎ
|
||||||
|
ೇ
|
||||||
|
ಂ
|
||||||
|
ೈ
|
||||||
|
ೊ
|
||||||
|
ೀ
|
||||||
|
ಯ
|
||||||
|
ೋ
|
||||||
|
ತ
|
||||||
|
ಶ
|
||||||
|
ಭ
|
||||||
|
ಧ
|
||||||
|
ಚ
|
||||||
|
ಜ
|
||||||
|
ೂ
|
||||||
|
ಮ
|
||||||
|
ಒ
|
||||||
|
ೃ
|
||||||
|
ಥ
|
||||||
|
ಇ
|
||||||
|
ಟ
|
||||||
|
ಖ
|
||||||
|
ಆ
|
||||||
|
ಞ
|
||||||
|
ಫ
|
||||||
|
-
|
||||||
|
ಢ
|
||||||
|
ಊ
|
||||||
|
ಓ
|
||||||
|
ಐ
|
||||||
|
ಃ
|
||||||
|
ಘ
|
||||||
|
ಝ
|
||||||
|
ೌ
|
||||||
|
ಠ
|
||||||
|
ಛ
|
||||||
|
ಔ
|
||||||
|
ಏ
|
||||||
|
ಈ
|
||||||
|
ಋ
|
||||||
|
೨
|
||||||
|
೦
|
||||||
|
೧
|
||||||
|
೮
|
||||||
|
೯
|
||||||
|
೪
|
||||||
|
,
|
||||||
|
೫
|
||||||
|
೭
|
||||||
|
೩
|
||||||
|
೬
|
||||||
|
ಙ
|
||||||
|
s
|
||||||
|
c
|
||||||
|
e
|
||||||
|
n
|
||||||
|
w
|
||||||
|
o
|
||||||
|
u
|
||||||
|
t
|
||||||
|
d
|
||||||
|
E
|
||||||
|
A
|
||||||
|
T
|
||||||
|
B
|
||||||
|
Z
|
||||||
|
N
|
||||||
|
G
|
||||||
|
O
|
||||||
|
q
|
||||||
|
z
|
||||||
|
r
|
||||||
|
x
|
||||||
|
P
|
||||||
|
K
|
||||||
|
M
|
||||||
|
J
|
||||||
|
U
|
||||||
|
D
|
||||||
|
f
|
||||||
|
F
|
||||||
|
h
|
||||||
|
b
|
||||||
|
W
|
||||||
|
Y
|
||||||
|
y
|
||||||
|
H
|
||||||
|
X
|
||||||
|
Q
|
||||||
|
'
|
||||||
|
#
|
||||||
|
&
|
||||||
|
!
|
||||||
|
@
|
||||||
|
$
|
||||||
|
:
|
||||||
|
%
|
||||||
|
é
|
||||||
|
É
|
||||||
|
(
|
||||||
|
?
|
||||||
|
+
|
||||||
|
|
||||||
163
models/ocr/Keys/斯拉夫字母.txt
普通文件
163
models/ocr/Keys/斯拉夫字母.txt
普通文件
@ -0,0 +1,163 @@
|
|||||||
|
|
||||||
|
!
|
||||||
|
#
|
||||||
|
$
|
||||||
|
%
|
||||||
|
&
|
||||||
|
'
|
||||||
|
(
|
||||||
|
+
|
||||||
|
,
|
||||||
|
-
|
||||||
|
.
|
||||||
|
/
|
||||||
|
0
|
||||||
|
1
|
||||||
|
2
|
||||||
|
3
|
||||||
|
4
|
||||||
|
5
|
||||||
|
6
|
||||||
|
7
|
||||||
|
8
|
||||||
|
9
|
||||||
|
:
|
||||||
|
?
|
||||||
|
@
|
||||||
|
A
|
||||||
|
B
|
||||||
|
C
|
||||||
|
D
|
||||||
|
E
|
||||||
|
F
|
||||||
|
G
|
||||||
|
H
|
||||||
|
I
|
||||||
|
J
|
||||||
|
K
|
||||||
|
L
|
||||||
|
M
|
||||||
|
N
|
||||||
|
O
|
||||||
|
P
|
||||||
|
Q
|
||||||
|
R
|
||||||
|
S
|
||||||
|
T
|
||||||
|
U
|
||||||
|
V
|
||||||
|
W
|
||||||
|
X
|
||||||
|
Y
|
||||||
|
Z
|
||||||
|
_
|
||||||
|
a
|
||||||
|
b
|
||||||
|
c
|
||||||
|
d
|
||||||
|
e
|
||||||
|
f
|
||||||
|
g
|
||||||
|
h
|
||||||
|
i
|
||||||
|
j
|
||||||
|
k
|
||||||
|
l
|
||||||
|
m
|
||||||
|
n
|
||||||
|
o
|
||||||
|
p
|
||||||
|
q
|
||||||
|
r
|
||||||
|
s
|
||||||
|
t
|
||||||
|
u
|
||||||
|
v
|
||||||
|
w
|
||||||
|
x
|
||||||
|
y
|
||||||
|
z
|
||||||
|
É
|
||||||
|
é
|
||||||
|
Ё
|
||||||
|
Є
|
||||||
|
І
|
||||||
|
Ј
|
||||||
|
Љ
|
||||||
|
Ў
|
||||||
|
А
|
||||||
|
Б
|
||||||
|
В
|
||||||
|
Г
|
||||||
|
Д
|
||||||
|
Е
|
||||||
|
Ж
|
||||||
|
З
|
||||||
|
И
|
||||||
|
Й
|
||||||
|
К
|
||||||
|
Л
|
||||||
|
М
|
||||||
|
Н
|
||||||
|
О
|
||||||
|
П
|
||||||
|
Р
|
||||||
|
С
|
||||||
|
Т
|
||||||
|
У
|
||||||
|
Ф
|
||||||
|
Х
|
||||||
|
Ц
|
||||||
|
Ч
|
||||||
|
Ш
|
||||||
|
Щ
|
||||||
|
Ъ
|
||||||
|
Ы
|
||||||
|
Ь
|
||||||
|
Э
|
||||||
|
Ю
|
||||||
|
Я
|
||||||
|
а
|
||||||
|
б
|
||||||
|
в
|
||||||
|
г
|
||||||
|
д
|
||||||
|
е
|
||||||
|
ж
|
||||||
|
з
|
||||||
|
и
|
||||||
|
й
|
||||||
|
к
|
||||||
|
л
|
||||||
|
м
|
||||||
|
н
|
||||||
|
о
|
||||||
|
п
|
||||||
|
р
|
||||||
|
с
|
||||||
|
т
|
||||||
|
у
|
||||||
|
ф
|
||||||
|
х
|
||||||
|
ц
|
||||||
|
ч
|
||||||
|
ш
|
||||||
|
щ
|
||||||
|
ъ
|
||||||
|
ы
|
||||||
|
ь
|
||||||
|
э
|
||||||
|
ю
|
||||||
|
я
|
||||||
|
ё
|
||||||
|
ђ
|
||||||
|
є
|
||||||
|
і
|
||||||
|
ј
|
||||||
|
љ
|
||||||
|
њ
|
||||||
|
ћ
|
||||||
|
ў
|
||||||
|
џ
|
||||||
|
Ґ
|
||||||
|
ґ
|
||||||
4399
models/ocr/Keys/日文.txt
普通文件
4399
models/ocr/Keys/日文.txt
普通文件
文件差异内容过多而无法显示
加载差异
151
models/ocr/Keys/泰卢固文.txt
普通文件
151
models/ocr/Keys/泰卢固文.txt
普通文件
@ -0,0 +1,151 @@
|
|||||||
|
t
|
||||||
|
e
|
||||||
|
_
|
||||||
|
i
|
||||||
|
m
|
||||||
|
g
|
||||||
|
/
|
||||||
|
5
|
||||||
|
I
|
||||||
|
L
|
||||||
|
S
|
||||||
|
V
|
||||||
|
R
|
||||||
|
C
|
||||||
|
2
|
||||||
|
0
|
||||||
|
1
|
||||||
|
v
|
||||||
|
a
|
||||||
|
l
|
||||||
|
3
|
||||||
|
4
|
||||||
|
8
|
||||||
|
9
|
||||||
|
.
|
||||||
|
j
|
||||||
|
p
|
||||||
|
త
|
||||||
|
ె
|
||||||
|
ర
|
||||||
|
క
|
||||||
|
్
|
||||||
|
ి
|
||||||
|
ం
|
||||||
|
చ
|
||||||
|
ే
|
||||||
|
ద
|
||||||
|
ు
|
||||||
|
7
|
||||||
|
6
|
||||||
|
ఉ
|
||||||
|
ా
|
||||||
|
మ
|
||||||
|
ట
|
||||||
|
ో
|
||||||
|
వ
|
||||||
|
ప
|
||||||
|
ల
|
||||||
|
శ
|
||||||
|
ఆ
|
||||||
|
య
|
||||||
|
ై
|
||||||
|
భ
|
||||||
|
'
|
||||||
|
ీ
|
||||||
|
గ
|
||||||
|
ూ
|
||||||
|
డ
|
||||||
|
ధ
|
||||||
|
హ
|
||||||
|
న
|
||||||
|
జ
|
||||||
|
స
|
||||||
|
[
|
||||||
|
|
||||||
|
ష
|
||||||
|
అ
|
||||||
|
ణ
|
||||||
|
ఫ
|
||||||
|
బ
|
||||||
|
ఎ
|
||||||
|
;
|
||||||
|
ళ
|
||||||
|
థ
|
||||||
|
ొ
|
||||||
|
ఠ
|
||||||
|
ృ
|
||||||
|
ఒ
|
||||||
|
ఇ
|
||||||
|
ః
|
||||||
|
ఊ
|
||||||
|
ఖ
|
||||||
|
-
|
||||||
|
ఐ
|
||||||
|
ఘ
|
||||||
|
ౌ
|
||||||
|
ఏ
|
||||||
|
ఈ
|
||||||
|
ఛ
|
||||||
|
,
|
||||||
|
ఓ
|
||||||
|
ఞ
|
||||||
|
|
|
||||||
|
?
|
||||||
|
:
|
||||||
|
ఢ
|
||||||
|
"
|
||||||
|
(
|
||||||
|
”
|
||||||
|
!
|
||||||
|
+
|
||||||
|
)
|
||||||
|
*
|
||||||
|
=
|
||||||
|
&
|
||||||
|
“
|
||||||
|
€
|
||||||
|
]
|
||||||
|
£
|
||||||
|
$
|
||||||
|
s
|
||||||
|
c
|
||||||
|
n
|
||||||
|
w
|
||||||
|
k
|
||||||
|
J
|
||||||
|
G
|
||||||
|
u
|
||||||
|
d
|
||||||
|
r
|
||||||
|
E
|
||||||
|
o
|
||||||
|
h
|
||||||
|
y
|
||||||
|
b
|
||||||
|
f
|
||||||
|
B
|
||||||
|
M
|
||||||
|
O
|
||||||
|
T
|
||||||
|
N
|
||||||
|
D
|
||||||
|
P
|
||||||
|
A
|
||||||
|
F
|
||||||
|
x
|
||||||
|
W
|
||||||
|
Y
|
||||||
|
U
|
||||||
|
H
|
||||||
|
K
|
||||||
|
X
|
||||||
|
z
|
||||||
|
Z
|
||||||
|
Q
|
||||||
|
q
|
||||||
|
É
|
||||||
|
%
|
||||||
|
#
|
||||||
|
@
|
||||||
|
é
|
||||||
95
models/ocr/Keys/英文.txt
普通文件
95
models/ocr/Keys/英文.txt
普通文件
@ -0,0 +1,95 @@
|
|||||||
|
0
|
||||||
|
1
|
||||||
|
2
|
||||||
|
3
|
||||||
|
4
|
||||||
|
5
|
||||||
|
6
|
||||||
|
7
|
||||||
|
8
|
||||||
|
9
|
||||||
|
:
|
||||||
|
;
|
||||||
|
<
|
||||||
|
=
|
||||||
|
>
|
||||||
|
?
|
||||||
|
@
|
||||||
|
A
|
||||||
|
B
|
||||||
|
C
|
||||||
|
D
|
||||||
|
E
|
||||||
|
F
|
||||||
|
G
|
||||||
|
H
|
||||||
|
I
|
||||||
|
J
|
||||||
|
K
|
||||||
|
L
|
||||||
|
M
|
||||||
|
N
|
||||||
|
O
|
||||||
|
P
|
||||||
|
Q
|
||||||
|
R
|
||||||
|
S
|
||||||
|
T
|
||||||
|
U
|
||||||
|
V
|
||||||
|
W
|
||||||
|
X
|
||||||
|
Y
|
||||||
|
Z
|
||||||
|
[
|
||||||
|
\
|
||||||
|
]
|
||||||
|
^
|
||||||
|
_
|
||||||
|
`
|
||||||
|
a
|
||||||
|
b
|
||||||
|
c
|
||||||
|
d
|
||||||
|
e
|
||||||
|
f
|
||||||
|
g
|
||||||
|
h
|
||||||
|
i
|
||||||
|
j
|
||||||
|
k
|
||||||
|
l
|
||||||
|
m
|
||||||
|
n
|
||||||
|
o
|
||||||
|
p
|
||||||
|
q
|
||||||
|
r
|
||||||
|
s
|
||||||
|
t
|
||||||
|
u
|
||||||
|
v
|
||||||
|
w
|
||||||
|
x
|
||||||
|
y
|
||||||
|
z
|
||||||
|
{
|
||||||
|
|
|
||||||
|
}
|
||||||
|
~
|
||||||
|
!
|
||||||
|
"
|
||||||
|
#
|
||||||
|
$
|
||||||
|
%
|
||||||
|
&
|
||||||
|
'
|
||||||
|
(
|
||||||
|
)
|
||||||
|
*
|
||||||
|
+
|
||||||
|
,
|
||||||
|
-
|
||||||
|
.
|
||||||
|
/
|
||||||
|
|
||||||
3688
models/ocr/Keys/韩文.txt
普通文件
3688
models/ocr/Keys/韩文.txt
普通文件
文件差异内容过多而无法显示
加载差异
二进制
models/ocr/Rec/中文简体_OCRv2.onnx
普通文件
二进制
models/ocr/Rec/中文简体_OCRv2.onnx
普通文件
二进制文件未显示。
二进制
models/ocr/Rec/中文简体_OCRv3.onnx
普通文件
二进制
models/ocr/Rec/中文简体_OCRv3.onnx
普通文件
二进制文件未显示。
二进制
models/ocr/Rec/中文繁体.onnx
普通文件
二进制
models/ocr/Rec/中文繁体.onnx
普通文件
二进制文件未显示。
二进制
models/ocr/Rec/卡纳达文.onnx
普通文件
二进制
models/ocr/Rec/卡纳达文.onnx
普通文件
二进制文件未显示。
二进制
models/ocr/Rec/斯拉夫字母.onnx
普通文件
二进制
models/ocr/Rec/斯拉夫字母.onnx
普通文件
二进制文件未显示。
二进制
models/ocr/Rec/日文.onnx
普通文件
二进制
models/ocr/Rec/日文.onnx
普通文件
二进制文件未显示。
二进制
models/ocr/Rec/泰卢固文.onnx
普通文件
二进制
models/ocr/Rec/泰卢固文.onnx
普通文件
二进制文件未显示。
二进制
models/ocr/Rec/英文.onnx
普通文件
二进制
models/ocr/Rec/英文.onnx
普通文件
二进制文件未显示。
二进制
models/ocr/Rec/韩文.onnx
普通文件
二进制
models/ocr/Rec/韩文.onnx
普通文件
二进制文件未显示。
@ -4,6 +4,8 @@
|
|||||||
"version": "0.0.0",
|
"version": "0.0.0",
|
||||||
"main": "src/main/main.js",
|
"main": "src/main/main.js",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
|
"download-ppocrv5": "node scripts/download-ppocrv5.js",
|
||||||
|
"validate-models": "node scripts/validate-models.js",
|
||||||
"dev": "concurrently \"yarn serve\" \"yarn electron-dev\"",
|
"dev": "concurrently \"yarn serve\" \"yarn electron-dev\"",
|
||||||
"serve": "vite --host",
|
"serve": "vite --host",
|
||||||
"electron-dev": "wait-on tcp:5173 && cross-env NODE_ENV=development electron .",
|
"electron-dev": "wait-on tcp:5173 && cross-env NODE_ENV=development electron .",
|
||||||
@ -11,16 +13,22 @@
|
|||||||
"preview": "vite preview"
|
"preview": "vite preview"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
"@techstark/opencv-js": "^4.12.0-release.1",
|
||||||
"canvas": "^3.2.0",
|
"canvas": "^3.2.0",
|
||||||
"cors": "^2.8.5",
|
"cors": "^2.8.5",
|
||||||
"crypto-ts": "^1.0.2",
|
"crypto-ts": "^1.0.2",
|
||||||
"express": "^5.1.0",
|
"express": "^5.1.0",
|
||||||
"fs-extra": "^11.3.2",
|
"fs-extra": "^11.3.2",
|
||||||
|
"jimp": "^1.6.0",
|
||||||
"multer": "^2.0.2",
|
"multer": "^2.0.2",
|
||||||
|
"node-fetch": "^3.3.2",
|
||||||
"node-tesseract-ocr": "^2.2.1",
|
"node-tesseract-ocr": "^2.2.1",
|
||||||
|
"onnxruntime-node": "^1.23.2",
|
||||||
|
"opencv.js": "^1.2.1",
|
||||||
"sharp": "^0.34.5",
|
"sharp": "^0.34.5",
|
||||||
"sqlite": "^5.1.1",
|
"sqlite": "^5.1.1",
|
||||||
"sqlite3": "^5.1.7",
|
"sqlite3": "^5.1.7",
|
||||||
|
"tar": "^7.4.3",
|
||||||
"tesseract.js": "^6.0.1",
|
"tesseract.js": "^6.0.1",
|
||||||
"vue": "^3.5.22",
|
"vue": "^3.5.22",
|
||||||
"vue-router": "^4.6.3"
|
"vue-router": "^4.6.3"
|
||||||
@ -28,6 +36,7 @@
|
|||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@types/cors": "^2.8.19",
|
"@types/cors": "^2.8.19",
|
||||||
"@types/express": "^5.0.5",
|
"@types/express": "^5.0.5",
|
||||||
|
"@types/fs-extra": "^11.0.4",
|
||||||
"@types/multer": "^2.0.0",
|
"@types/multer": "^2.0.0",
|
||||||
"@types/node": "^24.6.0",
|
"@types/node": "^24.6.0",
|
||||||
"@vitejs/plugin-vue": "^6.0.1",
|
"@vitejs/plugin-vue": "^6.0.1",
|
||||||
|
|||||||
197
scripts/download-ppocrv5.js
普通文件
197
scripts/download-ppocrv5.js
普通文件
@ -0,0 +1,197 @@
|
|||||||
|
// scripts/download-ppocrv5.js
|
||||||
|
import fs from 'fs-extra';
|
||||||
|
import path from 'path';
|
||||||
|
import { fileURLToPath } from 'url';
|
||||||
|
import { createRequire } from 'module';
|
||||||
|
|
||||||
|
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
||||||
|
const require = createRequire(import.meta.url);
|
||||||
|
|
||||||
|
class PPOCRv5Downloader {
|
||||||
|
constructor() {
|
||||||
|
this.modelDir = path.join(process.cwd(), 'models', 'ppocrv5');
|
||||||
|
this.tempDir = path.join(process.cwd(), 'temp', 'downloads');
|
||||||
|
|
||||||
|
// PP-OCRv5 官方模型下载链接
|
||||||
|
this.modelUrls = {
|
||||||
|
detection: {
|
||||||
|
url: 'https://paddleocr.bj.bcebos.com/PP-OCRv5/chinese/ch_PP-OCRv5_det_infer.onnx',
|
||||||
|
filename: 'ch_PP-OCRv5_det_infer.onnx'
|
||||||
|
},
|
||||||
|
recognition: {
|
||||||
|
url: 'https://paddleocr.bj.bcebos.com/PP-OCRv5/chinese/ch_PP-OCRv5_rec_infer.onnx',
|
||||||
|
filename: 'ch_PP-OCRv5_rec_infer.onnx'
|
||||||
|
},
|
||||||
|
classification: {
|
||||||
|
url: 'https://paddleocr.bj.bcebos.com/dygraph_v2.0/ch/ch_ppocr_mobile_v2.0_cls_infer.onnx',
|
||||||
|
filename: 'ch_ppocr_mobile_v2.0_cls_infer.onnx'
|
||||||
|
},
|
||||||
|
keys: {
|
||||||
|
url: 'https://raw.githubusercontent.com/PaddlePaddle/PaddleOCR/release/2.7/ppocr/utils/ppocr_keys_v1.txt',
|
||||||
|
filename: 'ppocr_keys_v1.txt'
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
async downloadModels() {
|
||||||
|
console.log('🚀 开始下载 PP-OCRv5 模型...');
|
||||||
|
console.log('📝 PP-OCRv5 特性:');
|
||||||
|
console.log(' - 更高的文本检测准确率');
|
||||||
|
console.log(' - 更好的小文本识别能力');
|
||||||
|
console.log(' - 优化的模型结构');
|
||||||
|
console.log(' - 完全离线运行\n');
|
||||||
|
|
||||||
|
try {
|
||||||
|
// 创建目录结构
|
||||||
|
await this.createDirectories();
|
||||||
|
|
||||||
|
let successCount = 0;
|
||||||
|
const totalCount = Object.keys(this.modelUrls).length;
|
||||||
|
|
||||||
|
// 并行下载所有模型
|
||||||
|
const downloadPromises = Object.entries(this.modelUrls).map(async ([type, info]) => {
|
||||||
|
try {
|
||||||
|
await this.downloadFile(type, info);
|
||||||
|
successCount++;
|
||||||
|
console.log(` ✅ ${this.getTypeName(type)} 下载完成 (${successCount}/${totalCount})`);
|
||||||
|
} catch (error) {
|
||||||
|
console.log(` ❌ ${this.getTypeName(type)} 下载失败: ${error.message}`);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
await Promise.all(downloadPromises);
|
||||||
|
|
||||||
|
console.log('\n🎉 所有模型下载完成!');
|
||||||
|
this.displayModelInfo();
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
console.error('\n❌ 下载过程中出现错误:', error.message);
|
||||||
|
await this.provideAlternativeSources();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async createDirectories() {
|
||||||
|
const dirs = [
|
||||||
|
this.modelDir,
|
||||||
|
path.join(this.modelDir, 'det'),
|
||||||
|
path.join(this.modelDir, 'rec'),
|
||||||
|
path.join(this.modelDir, 'cls'),
|
||||||
|
path.join(this.modelDir, 'keys'),
|
||||||
|
this.tempDir
|
||||||
|
];
|
||||||
|
|
||||||
|
for (const dir of dirs) {
|
||||||
|
await fs.ensureDir(dir);
|
||||||
|
}
|
||||||
|
console.log('📁 目录结构创建完成');
|
||||||
|
}
|
||||||
|
|
||||||
|
async downloadFile(type, info) {
|
||||||
|
const targetPath = this.getTargetPath(type, info.filename);
|
||||||
|
|
||||||
|
// 检查文件是否已存在
|
||||||
|
if (await fs.pathExists(targetPath)) {
|
||||||
|
const stats = await fs.stat(targetPath);
|
||||||
|
if (stats.size > this.getMinFileSize(type)) {
|
||||||
|
console.log(` ⏭️ ${this.getTypeName(type)} 已存在,跳过下载`);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log(` 📥 下载 ${this.getTypeName(type)}...`);
|
||||||
|
|
||||||
|
const fetch = await import('node-fetch');
|
||||||
|
const response = await fetch.default(info.url);
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
throw new Error(`HTTP ${response.status}: ${response.statusText}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
const buffer = await response.buffer();
|
||||||
|
|
||||||
|
// 验证文件大小
|
||||||
|
if (buffer.length < this.getMinFileSize(type)) {
|
||||||
|
throw new Error(`文件大小异常: ${(buffer.length / 1024 / 1024).toFixed(2)} MB`);
|
||||||
|
}
|
||||||
|
|
||||||
|
await fs.writeFile(targetPath, buffer);
|
||||||
|
|
||||||
|
// 验证文件完整性
|
||||||
|
await this.validateFile(type, targetPath);
|
||||||
|
}
|
||||||
|
|
||||||
|
getTargetPath(type, filename) {
|
||||||
|
const dirs = {
|
||||||
|
detection: path.join(this.modelDir, 'det'),
|
||||||
|
recognition: path.join(this.modelDir, 'rec'),
|
||||||
|
classification: path.join(this.modelDir, 'cls'),
|
||||||
|
keys: path.join(this.modelDir, 'keys')
|
||||||
|
};
|
||||||
|
return path.join(dirs[type], filename);
|
||||||
|
}
|
||||||
|
|
||||||
|
getTypeName(type) {
|
||||||
|
const names = {
|
||||||
|
detection: '检测模型 (PP-OCRv5 Det)',
|
||||||
|
recognition: '识别模型 (PP-OCRv5 Rec)',
|
||||||
|
classification: '分类模型 (Cls)',
|
||||||
|
keys: '字符集文件'
|
||||||
|
};
|
||||||
|
return names[type];
|
||||||
|
}
|
||||||
|
|
||||||
|
getMinFileSize(type) {
|
||||||
|
const sizes = {
|
||||||
|
detection: 2000000, // 2MB
|
||||||
|
recognition: 8000000, // 8MB
|
||||||
|
classification: 1000000, // 1MB
|
||||||
|
keys: 50000 // 50KB
|
||||||
|
};
|
||||||
|
return sizes[type];
|
||||||
|
}
|
||||||
|
|
||||||
|
async validateFile(type, filePath) {
|
||||||
|
const stats = await fs.stat(filePath);
|
||||||
|
|
||||||
|
if (type === 'keys') {
|
||||||
|
const content = await fs.readFile(filePath, 'utf8');
|
||||||
|
const lines = content.split('\n').filter(line => line.trim());
|
||||||
|
if (lines.length < 5000) {
|
||||||
|
throw new Error('字符集文件不完整');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log(` 📊 文件大小: ${(stats.size / 1024 / 1024).toFixed(2)} MB`);
|
||||||
|
}
|
||||||
|
|
||||||
|
displayModelInfo() {
|
||||||
|
console.log('\n📂 模型文件位置:');
|
||||||
|
console.log(` 🎯 检测模型: ${path.join(this.modelDir, 'det', 'ch_PP-OCRv5_det_infer.onnx')}`);
|
||||||
|
console.log(` 🔤 识别模型: ${path.join(this.modelDir, 'rec', 'ch_PP-OCRv5_rec_infer.onnx')}`);
|
||||||
|
console.log(` 🧭 分类模型: ${path.join(this.modelDir, 'cls', 'ch_ppocr_mobile_v2.0_cls_infer.onnx')}`);
|
||||||
|
console.log(` 📝 字符集: ${path.join(this.modelDir, 'keys', 'ppocr_keys_v1.txt')}`);
|
||||||
|
|
||||||
|
console.log('\n🚀 使用命令:');
|
||||||
|
console.log(' yarn dev # 启动应用');
|
||||||
|
}
|
||||||
|
|
||||||
|
async provideAlternativeSources() {
|
||||||
|
console.log('\n💡 备用下载方案:');
|
||||||
|
console.log(' 1. 手动下载 PP-OCRv5 模型:');
|
||||||
|
console.log(' - 检测模型: https://paddleocr.bj.bcebos.com/PP-OCRv5/chinese/ch_PP-OCRv5_det_infer.onnx');
|
||||||
|
console.log(' - 识别模型: https://paddleocr.bj.bcebos.com/PP-OCRv5/chinese/ch_PP-OCRv5_rec_infer.onnx');
|
||||||
|
console.log(' - 分类模型: https://paddleocr.bj.bcebos.com/dygraph_v2.0/ch/ch_ppocr_mobile_v2.0_cls_infer.onnx');
|
||||||
|
console.log(' - 字符集: https://raw.githubusercontent.com/PaddlePaddle/PaddleOCR/release/2.7/ppocr/utils/ppocr_keys_v1.txt');
|
||||||
|
console.log('\n 2. 将文件放置到以下目录:');
|
||||||
|
console.log(` ${this.modelDir}/`);
|
||||||
|
console.log(' ├── det/ch_PP-OCRv5_det_infer.onnx');
|
||||||
|
console.log(' ├── rec/ch_PP-OCRv5_rec_infer.onnx');
|
||||||
|
console.log(' ├── cls/ch_ppocr_mobile_v2.0_cls_infer.onnx');
|
||||||
|
console.log(' └── keys/ppocr_keys_v1.txt');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 执行下载
|
||||||
|
const downloader = new PPOCRv5Downloader();
|
||||||
|
downloader.downloadModels().catch(console.error);
|
||||||
115
scripts/validate-models.js
普通文件
115
scripts/validate-models.js
普通文件
@ -0,0 +1,115 @@
|
|||||||
|
// scripts/validate-models.js
|
||||||
|
import fs from 'fs-extra';
|
||||||
|
import path from 'path';
|
||||||
|
import { fileURLToPath } from 'url';
|
||||||
|
|
||||||
|
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
||||||
|
|
||||||
|
class ModelValidator {
|
||||||
|
constructor() {
|
||||||
|
this.modelDir = path.join(process.cwd(), 'models', 'ocr');
|
||||||
|
this.requiredFiles = {
|
||||||
|
detection: path.join(this.modelDir, 'Det', '中文_OCRv3.onnx'),
|
||||||
|
recognition: path.join(this.modelDir, 'Rec', '中文简体_OCRv3.onnx'),
|
||||||
|
classification: path.join(this.modelDir, 'Cls', '原始分类器模型.onnx'),
|
||||||
|
keys: path.join(this.modelDir, 'Keys', '中文简体_OCRv3.txt')
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
async validateModels() {
|
||||||
|
console.log('🔍 验证模型文件...\n');
|
||||||
|
|
||||||
|
let allValid = true;
|
||||||
|
const results = [];
|
||||||
|
|
||||||
|
for (const [name, filePath] of Object.entries(this.requiredFiles)) {
|
||||||
|
const exists = await fs.pathExists(filePath);
|
||||||
|
const result = {
|
||||||
|
name: this.getDisplayName(name),
|
||||||
|
path: filePath,
|
||||||
|
exists,
|
||||||
|
size: 0,
|
||||||
|
valid: false
|
||||||
|
};
|
||||||
|
|
||||||
|
if (exists) {
|
||||||
|
try {
|
||||||
|
const stats = await fs.stat(filePath);
|
||||||
|
result.size = stats.size;
|
||||||
|
result.valid = this.validateFileSize(name, stats.size);
|
||||||
|
|
||||||
|
if (name === 'keys') {
|
||||||
|
result.valid = await this.validateKeysFile(filePath);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
result.valid = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
results.push(result);
|
||||||
|
allValid = allValid && result.valid;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 显示验证结果
|
||||||
|
this.displayResults(results);
|
||||||
|
|
||||||
|
if (!allValid) {
|
||||||
|
console.log('\n❌ 模型文件不完整或损坏');
|
||||||
|
console.log('💡 运行以下命令重新下载:');
|
||||||
|
console.log(' yarn download-models');
|
||||||
|
process.exit(1);
|
||||||
|
} else {
|
||||||
|
console.log('\n✅ 所有模型文件验证通过!');
|
||||||
|
}
|
||||||
|
|
||||||
|
return allValid;
|
||||||
|
}
|
||||||
|
|
||||||
|
getDisplayName(name) {
|
||||||
|
const names = {
|
||||||
|
detection: '检测模型',
|
||||||
|
recognition: '识别模型',
|
||||||
|
classification: '分类模型',
|
||||||
|
keys: '字符集文件'
|
||||||
|
};
|
||||||
|
return names[name] || name;
|
||||||
|
}
|
||||||
|
|
||||||
|
validateFileSize(name, size) {
|
||||||
|
const minSizes = {
|
||||||
|
detection: 1000000, // 1MB
|
||||||
|
recognition: 5000000, // 5MB
|
||||||
|
classification: 1000000, // 1MB
|
||||||
|
keys: 1000 // 1KB
|
||||||
|
};
|
||||||
|
return size > (minSizes[name] || 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
async validateKeysFile(filePath) {
|
||||||
|
try {
|
||||||
|
const content = await fs.readFile(filePath, 'utf8');
|
||||||
|
const lines = content.split('\n').filter(line => line.trim());
|
||||||
|
return lines.length > 1000; // 至少1000个字符
|
||||||
|
} catch {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
displayResults(results) {
|
||||||
|
results.forEach(result => {
|
||||||
|
const status = result.exists && result.valid ? '✅' : '❌';
|
||||||
|
const sizeInfo = result.exists ? ` (${(result.size / 1024 / 1024).toFixed(2)} MB)` : '';
|
||||||
|
console.log(`${status} ${result.name}${sizeInfo}`);
|
||||||
|
|
||||||
|
if (!result.exists) {
|
||||||
|
console.log(` 文件不存在: ${result.path}`);
|
||||||
|
} else if (!result.valid) {
|
||||||
|
console.log(` 文件可能损坏: ${result.path}`);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 执行验证
|
||||||
|
const validator = new ModelValidator();
|
||||||
|
validator.validateModels().catch(console.error);
|
||||||
468
server/server.js
468
server/server.js
@ -1,468 +0,0 @@
|
|||||||
const express = require('express')
|
|
||||||
const cors = require('cors')
|
|
||||||
const multer = require('multer')
|
|
||||||
const path = require('path')
|
|
||||||
const fs = require('fs-extra')
|
|
||||||
const { calculateFileMD5 } = require('./utils.js')
|
|
||||||
const { initDatabase, FileService } = require('../database/database.js')
|
|
||||||
|
|
||||||
// 新增 OCR 相关依赖
|
|
||||||
const Tesseract = require('tesseract.js')
|
|
||||||
const sharp = require('sharp')
|
|
||||||
const { createCanvas, loadImage } = require('canvas')
|
|
||||||
|
|
||||||
const app = express()
|
|
||||||
const PORT = 3000
|
|
||||||
|
|
||||||
// 初始化数据库
|
|
||||||
initDatabase()
|
|
||||||
const fileService = new FileService()
|
|
||||||
|
|
||||||
// 确保上传目录和临时目录存在
|
|
||||||
const uploadDir = path.join(process.cwd(), 'uploads')
|
|
||||||
const tempDir = path.join(process.cwd(), 'temp')
|
|
||||||
fs.ensureDirSync(uploadDir)
|
|
||||||
fs.ensureDirSync(tempDir)
|
|
||||||
|
|
||||||
// 配置 multer - 修复中文文件名问题
|
|
||||||
const storage = multer.diskStorage({
|
|
||||||
destination: (req, file, cb) => {
|
|
||||||
cb(null, uploadDir)
|
|
||||||
},
|
|
||||||
filename: (req, file, cb) => {
|
|
||||||
// 处理中文文件名 - 使用原始文件名但确保安全
|
|
||||||
const originalName = Buffer.from(file.originalname, 'latin1').toString('utf8')
|
|
||||||
const ext = path.extname(originalName)
|
|
||||||
const name = path.basename(originalName, ext)
|
|
||||||
|
|
||||||
// 清理文件名,移除特殊字符
|
|
||||||
const safeName = name.replace(/[^a-zA-Z0-9\u4e00-\u9fa5]/g, '_')
|
|
||||||
const uniqueSuffix = Date.now() + '-' + Math.round(Math.random() * 1E9)
|
|
||||||
const filename = safeName + '-' + uniqueSuffix + ext
|
|
||||||
|
|
||||||
cb(null, filename)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
const upload = multer({
|
|
||||||
storage,
|
|
||||||
fileFilter: (req, file, cb) => {
|
|
||||||
// 处理文件名编码
|
|
||||||
file.originalname = Buffer.from(file.originalname, 'latin1').toString('utf8')
|
|
||||||
cb(null, true)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
// 设置响应头,确保使用 UTF-8 编码
|
|
||||||
app.use((req, res, next) => {
|
|
||||||
res.setHeader('Content-Type', 'application/json; charset=utf-8')
|
|
||||||
next()
|
|
||||||
})
|
|
||||||
|
|
||||||
app.use(cors())
|
|
||||||
app.use(express.json({ limit: '50mb' }))
|
|
||||||
app.use(express.urlencoded({ extended: true, limit: '50mb' }))
|
|
||||||
|
|
||||||
// 文件上传接口
|
|
||||||
app.post('/api/upload', upload.single('file'), async (req, res) => {
|
|
||||||
try {
|
|
||||||
if (!req.file) {
|
|
||||||
return res.status(400).json({ error: 'No file uploaded' })
|
|
||||||
}
|
|
||||||
|
|
||||||
// 确保文件名正确编码
|
|
||||||
const originalName = Buffer.from(req.file.originalname, 'latin1').toString('utf8')
|
|
||||||
|
|
||||||
const fileInfo = {
|
|
||||||
originalName: originalName,
|
|
||||||
fileName: req.file.filename,
|
|
||||||
filePath: req.file.path,
|
|
||||||
fileSize: req.file.size,
|
|
||||||
mimeType: req.file.mimetype
|
|
||||||
}
|
|
||||||
|
|
||||||
// 计算 MD5
|
|
||||||
const md5 = await calculateFileMD5(req.file.path)
|
|
||||||
|
|
||||||
// 保存到数据库
|
|
||||||
const fileRecord = await fileService.createFile({
|
|
||||||
...fileInfo,
|
|
||||||
md5
|
|
||||||
})
|
|
||||||
|
|
||||||
res.json({
|
|
||||||
success: true,
|
|
||||||
data: fileRecord
|
|
||||||
})
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Upload error:', error)
|
|
||||||
res.status(500).json({ error: 'Upload failed: ' + error.message })
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
// 修复获取文件列表接口 - 确保返回正确的数据结构
|
|
||||||
app.get('/api/files', async (req, res) => {
|
|
||||||
try {
|
|
||||||
const page = parseInt(req.query.page) || 1
|
|
||||||
const pageSize = parseInt(req.query.pageSize) || 100
|
|
||||||
|
|
||||||
const result = await fileService.getFilesPaginated(page, pageSize)
|
|
||||||
|
|
||||||
// 返回统一的数据结构
|
|
||||||
res.json({
|
|
||||||
success: true,
|
|
||||||
data: result.files, // 直接返回文件数组
|
|
||||||
pagination: result.pagination
|
|
||||||
})
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Get files error:', error)
|
|
||||||
res.status(500).json({
|
|
||||||
success: false,
|
|
||||||
error: 'Failed to get files: ' + error.message
|
|
||||||
})
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
// MD5 检查接口
|
|
||||||
app.post('/api/files/:id/check-md5', async (req, res) => {
|
|
||||||
try {
|
|
||||||
const fileId = parseInt(req.params.id)
|
|
||||||
const file = await fileService.getFileById(fileId)
|
|
||||||
|
|
||||||
if (!file) {
|
|
||||||
return res.status(404).json({ error: 'File not found' })
|
|
||||||
}
|
|
||||||
|
|
||||||
const currentMD5 = await calculateFileMD5(file.filePath)
|
|
||||||
const isChanged = currentMD5 !== file.md5
|
|
||||||
|
|
||||||
res.json({
|
|
||||||
isChanged,
|
|
||||||
currentMD5,
|
|
||||||
originalMD5: file.md5,
|
|
||||||
file
|
|
||||||
})
|
|
||||||
} catch (error) {
|
|
||||||
console.error('MD5 check error:', error)
|
|
||||||
res.status(500).json({ error: 'MD5 check failed' })
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
// 更新 MD5 接口
|
|
||||||
app.put('/api/files/:id/update-md5', async (req, res) => {
|
|
||||||
try {
|
|
||||||
const fileId = parseInt(req.params.id)
|
|
||||||
const { md5 } = req.body
|
|
||||||
|
|
||||||
await fileService.updateFileMD5(fileId, md5)
|
|
||||||
res.json({ success: true })
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Update MD5 error:', error)
|
|
||||||
res.status(500).json({ error: 'Update failed' })
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
// 新增 OCR 识别接口
|
|
||||||
app.post('/api/ocr/recognize', async (req, res) => {
|
|
||||||
try {
|
|
||||||
const { fileId, page } = req.body
|
|
||||||
|
|
||||||
if (!fileId) {
|
|
||||||
return res.status(400).json({ error: 'File ID is required' })
|
|
||||||
}
|
|
||||||
|
|
||||||
const file = await fileService.getFileById(parseInt(fileId))
|
|
||||||
if (!file) {
|
|
||||||
return res.status(404).json({ error: 'File not found' })
|
|
||||||
}
|
|
||||||
|
|
||||||
console.log(`开始OCR识别: ${file.originalName}`)
|
|
||||||
|
|
||||||
// 预处理图像
|
|
||||||
const processedImagePath = await preprocessImage(file.filePath)
|
|
||||||
|
|
||||||
// 使用 Tesseract 进行 OCR 识别
|
|
||||||
const result = await performOCR(processedImagePath)
|
|
||||||
|
|
||||||
// 清理临时文件
|
|
||||||
await fs.remove(processedImagePath)
|
|
||||||
|
|
||||||
res.json({
|
|
||||||
success: true,
|
|
||||||
data: {
|
|
||||||
textBlocks: result.textBlocks,
|
|
||||||
totalPages: result.totalPages || 1,
|
|
||||||
processingTime: result.processingTime,
|
|
||||||
confidence: result.confidence
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
} catch (error) {
|
|
||||||
console.error('OCR recognition error:', error)
|
|
||||||
res.status(500).json({ error: 'OCR recognition failed: ' + error.message })
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
// 添加 OCR 结果相关的 API 接口
|
|
||||||
|
|
||||||
// 保存 OCR 结果
|
|
||||||
app.post('/api/ocr/save-result', async (req, res) => {
|
|
||||||
try {
|
|
||||||
const { fileId, ocrData } = req.body
|
|
||||||
|
|
||||||
if (!fileId || !ocrData) {
|
|
||||||
return res.status(400).json({ error: '文件ID和OCR数据是必需的' })
|
|
||||||
}
|
|
||||||
|
|
||||||
await fileService.saveOcrResult(parseInt(fileId), ocrData)
|
|
||||||
|
|
||||||
res.json({ success: true })
|
|
||||||
} catch (error) {
|
|
||||||
console.error('保存OCR结果失败:', error)
|
|
||||||
res.status(500).json({ error: '保存OCR结果失败: ' + error.message })
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
// 获取 OCR 结果
|
|
||||||
app.get('/api/ocr/result/:fileId', async (req, res) => {
|
|
||||||
try {
|
|
||||||
const fileId = parseInt(req.params.fileId)
|
|
||||||
const result = await fileService.getOcrResult(fileId)
|
|
||||||
|
|
||||||
if (result) {
|
|
||||||
res.json({
|
|
||||||
success: true,
|
|
||||||
data: result.ocr_data
|
|
||||||
})
|
|
||||||
} else {
|
|
||||||
res.json({
|
|
||||||
success: false,
|
|
||||||
error: '未找到OCR结果'
|
|
||||||
})
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
console.error('获取OCR结果失败:', error)
|
|
||||||
res.status(500).json({ error: '获取OCR结果失败: ' + error.message })
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
// 更新 OCR 文本(人工纠错)
|
|
||||||
app.put('/api/ocr/update-text', async (req, res) => {
|
|
||||||
try {
|
|
||||||
const { fileId, textBlocks } = req.body
|
|
||||||
|
|
||||||
if (!fileId || !textBlocks) {
|
|
||||||
return res.status(400).json({ error: '文件ID和文本数据是必需的' })
|
|
||||||
}
|
|
||||||
|
|
||||||
await fileService.updateOcrText(parseInt(fileId), textBlocks)
|
|
||||||
|
|
||||||
res.json({ success: true })
|
|
||||||
} catch (error) {
|
|
||||||
console.error('更新OCR文本失败:', error)
|
|
||||||
res.status(500).json({ error: '更新OCR文本失败: ' + error.message })
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
// 图像预处理函数
|
|
||||||
async function preprocessImage(imagePath) {
|
|
||||||
const tempOutputPath = path.join(tempDir, `preprocessed-${Date.now()}.png`)
|
|
||||||
|
|
||||||
try {
|
|
||||||
// 使用 sharp 进行图像预处理
|
|
||||||
await sharp(imagePath)
|
|
||||||
.grayscale() // 转为灰度图
|
|
||||||
.normalize() // 标准化图像
|
|
||||||
.linear(1.5, 0) // 增加对比度
|
|
||||||
.sharpen() // 锐化
|
|
||||||
.png()
|
|
||||||
.toFile(tempOutputPath)
|
|
||||||
|
|
||||||
return tempOutputPath
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Image preprocessing failed:', error)
|
|
||||||
// 如果预处理失败,返回原图
|
|
||||||
return imagePath
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// OCR 识别函数
|
|
||||||
async function performOCR(imagePath) {
|
|
||||||
return new Promise((resolve, reject) => {
|
|
||||||
const startTime = Date.now()
|
|
||||||
|
|
||||||
Tesseract.recognize(
|
|
||||||
imagePath,
|
|
||||||
'chi_sim+eng', // 中文简体 + 英文
|
|
||||||
{
|
|
||||||
logger: m => console.log(m),
|
|
||||||
tessedit_pageseg_mode: Tesseract.PSM.AUTO,
|
|
||||||
tessedit_char_whitelist: '0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz\u4e00-\u9fa5,。!?;:"'/'()【】《》…—·'
|
|
||||||
}
|
|
||||||
).then(({ data: { text, confidence } }) => {
|
|
||||||
const processingTime = Date.now() - startTime
|
|
||||||
|
|
||||||
// 解析文本块
|
|
||||||
const textBlocks = parseOCRText(text)
|
|
||||||
|
|
||||||
resolve({
|
|
||||||
textBlocks,
|
|
||||||
confidence,
|
|
||||||
processingTime
|
|
||||||
})
|
|
||||||
}).catch(error => {
|
|
||||||
reject(error)
|
|
||||||
})
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
// 解析 OCR 文本结果
|
|
||||||
function parseOCRText(text) {
|
|
||||||
const blocks = []
|
|
||||||
const lines = text.split('\n').filter(line => line.trim())
|
|
||||||
|
|
||||||
for (const line of lines) {
|
|
||||||
const trimmedLine = line.trim()
|
|
||||||
if (!trimmedLine) continue
|
|
||||||
|
|
||||||
// 检测参考文献
|
|
||||||
if (isReference(trimmedLine)) {
|
|
||||||
blocks.push({
|
|
||||||
type: 'reference',
|
|
||||||
content: trimmedLine
|
|
||||||
})
|
|
||||||
}
|
|
||||||
// 检测引用
|
|
||||||
else if (isCitation(trimmedLine)) {
|
|
||||||
blocks.push({
|
|
||||||
type: 'citation',
|
|
||||||
content: trimmedLine.replace(/^\[\d+\]\s*/, ''),
|
|
||||||
number: extractCitationNumber(trimmedLine)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
// 检测图片标记
|
|
||||||
else if (isImageMarker(trimmedLine)) {
|
|
||||||
blocks.push({
|
|
||||||
type: 'image',
|
|
||||||
content: trimmedLine
|
|
||||||
})
|
|
||||||
}
|
|
||||||
// 普通文本
|
|
||||||
else {
|
|
||||||
blocks.push({
|
|
||||||
type: 'text',
|
|
||||||
content: trimmedLine
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return blocks
|
|
||||||
}
|
|
||||||
|
|
||||||
// 辅助函数
|
|
||||||
function isReference(text) {
|
|
||||||
const refPatterns = [
|
|
||||||
/^参考文献/i,
|
|
||||||
/^references/i,
|
|
||||||
/^bibliography/i,
|
|
||||||
/^\[?\d+\]?\s*\.?\s*[A-Za-z].*\.\s*\d{4}/
|
|
||||||
]
|
|
||||||
return refPatterns.some(pattern => pattern.test(text))
|
|
||||||
}
|
|
||||||
|
|
||||||
function isCitation(text) {
|
|
||||||
return /^\[\d+\]/.test(text)
|
|
||||||
}
|
|
||||||
|
|
||||||
function extractCitationNumber(text) {
|
|
||||||
const match = text.match(/^\[(\d+)\]/)
|
|
||||||
return match ? parseInt(match[1]) : null
|
|
||||||
}
|
|
||||||
|
|
||||||
function isImageMarker(text) {
|
|
||||||
const imagePatterns = [
|
|
||||||
/^图\s*\d+/i,
|
|
||||||
/^figure\s*\d+/i,
|
|
||||||
/^图片\d*/i
|
|
||||||
]
|
|
||||||
return imagePatterns.some(pattern => pattern.test(text))
|
|
||||||
}
|
|
||||||
|
|
||||||
// 获取文件预览接口
|
|
||||||
app.get('/api/files/:id/preview', async (req, res) => {
|
|
||||||
try {
|
|
||||||
const fileId = parseInt(req.params.id)
|
|
||||||
const file = await fileService.getFileById(fileId)
|
|
||||||
|
|
||||||
if (!file) {
|
|
||||||
return res.status(404).json({ error: 'File not found' })
|
|
||||||
}
|
|
||||||
|
|
||||||
// 检查文件是否存在
|
|
||||||
if (!fs.existsSync(file.filePath)) {
|
|
||||||
return res.status(404).json({ error: 'File not found on disk' })
|
|
||||||
}
|
|
||||||
|
|
||||||
// 设置正确的 Content-Type
|
|
||||||
res.setHeader('Content-Type', file.mimeType)
|
|
||||||
|
|
||||||
// 直接发送文件
|
|
||||||
res.sendFile(path.resolve(file.filePath))
|
|
||||||
|
|
||||||
} catch (error) {
|
|
||||||
console.error('File preview error:', error)
|
|
||||||
res.status(500).json({ error: 'Failed to get file preview' })
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
// 获取文件缩略图接口
|
|
||||||
app.get('/api/files/:id/thumbnail', async (req, res) => {
|
|
||||||
try {
|
|
||||||
const fileId = parseInt(req.params.id)
|
|
||||||
const file = await fileService.getFileById(fileId)
|
|
||||||
|
|
||||||
if (!file) {
|
|
||||||
return res.status(404).json({ error: 'File not found' })
|
|
||||||
}
|
|
||||||
|
|
||||||
// 只对图片生成缩略图
|
|
||||||
if (!file.mimeType.startsWith('image/')) {
|
|
||||||
return res.status(400).json({ error: 'Not an image file' })
|
|
||||||
}
|
|
||||||
|
|
||||||
const thumbnailPath = path.join(tempDir, `thumbnail-${fileId}.jpg`)
|
|
||||||
|
|
||||||
// 生成缩略图
|
|
||||||
await sharp(file.filePath)
|
|
||||||
.resize(100, 100, {
|
|
||||||
fit: 'inside',
|
|
||||||
withoutEnlargement: true
|
|
||||||
})
|
|
||||||
.jpeg({ quality: 80 })
|
|
||||||
.toFile(thumbnailPath)
|
|
||||||
|
|
||||||
res.sendFile(path.resolve(thumbnailPath))
|
|
||||||
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Thumbnail generation error:', error)
|
|
||||||
// 如果缩略图生成失败,返回原图
|
|
||||||
res.sendFile(path.resolve(file.filePath))
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
// 健康检查接口
|
|
||||||
app.get('/api/health', (req, res) => {
|
|
||||||
res.json({
|
|
||||||
status: 'OK',
|
|
||||||
timestamp: new Date().toISOString(),
|
|
||||||
service: 'file-management-api'
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
function startServer() {
|
|
||||||
app.listen(PORT, () => {
|
|
||||||
console.log(`Server running on http://localhost:${PORT}`)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
module.exports = { startServer }
|
|
||||||
487
server/server.ts
普通文件
487
server/server.ts
普通文件
@ -0,0 +1,487 @@
|
|||||||
|
import express from 'express';
|
||||||
|
import cors from 'cors';
|
||||||
|
import multer from 'multer';
|
||||||
|
import path from 'path';
|
||||||
|
import fs from 'fs-extra';
|
||||||
|
import { calculateFileMD5 } from './utils.js';
|
||||||
|
import { initDatabase, FileService } from '../database/database.js';
|
||||||
|
import onnxOcrManager from "./utils/onnxOcrManager.js";
|
||||||
|
|
||||||
|
import sharp from "sharp";
|
||||||
|
import fse from "fs-extra";
|
||||||
|
|
||||||
|
|
||||||
|
const app = express();
|
||||||
|
const PORT = 3000;
|
||||||
|
|
||||||
|
// 初始化数据库
|
||||||
|
initDatabase();
|
||||||
|
const fileService = new FileService();
|
||||||
|
|
||||||
|
// 确保上传目录和临时目录存在
|
||||||
|
const uploadDir = path.join(process.cwd(), 'uploads');
|
||||||
|
const tempDir = path.join(process.cwd(), 'temp');
|
||||||
|
const processedDir = path.join(process.cwd(), 'processed');
|
||||||
|
fs.ensureDirSync(uploadDir);
|
||||||
|
fs.ensureDirSync(tempDir);
|
||||||
|
fs.ensureDirSync(processedDir);
|
||||||
|
|
||||||
|
// 配置 multer - 修复中文文件名问题
|
||||||
|
const storage = multer.diskStorage({
|
||||||
|
destination: (req, file, cb) => {
|
||||||
|
cb(null, uploadDir);
|
||||||
|
},
|
||||||
|
filename: (req, file, cb) => {
|
||||||
|
// 处理中文文件名 - 使用原始文件名但确保安全
|
||||||
|
const originalName = Buffer.from(file.originalname, 'latin1').toString('utf8');
|
||||||
|
const ext = path.extname(originalName);
|
||||||
|
const name = path.basename(originalName, ext);
|
||||||
|
|
||||||
|
// 清理文件名,移除特殊字符
|
||||||
|
const safeName = name.replace(/[^a-zA-Z0-9\u4e00-\u9fa5]/g, '_');
|
||||||
|
const uniqueSuffix = Date.now() + '-' + Math.round(Math.random() * 1E9);
|
||||||
|
const filename = safeName + '-' + uniqueSuffix + ext;
|
||||||
|
|
||||||
|
cb(null, filename);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const upload = multer({
|
||||||
|
storage,
|
||||||
|
fileFilter: (req, file, cb) => {
|
||||||
|
// 处理文件名编码
|
||||||
|
file.originalname = Buffer.from(file.originalname, 'latin1').toString('utf8');
|
||||||
|
cb(null, true);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// 设置响应头,确保使用 UTF-8 编码
|
||||||
|
app.use((req, res, next) => {
|
||||||
|
res.setHeader('Content-Type', 'application/json; charset=utf-8');
|
||||||
|
next();
|
||||||
|
});
|
||||||
|
|
||||||
|
app.use(cors());
|
||||||
|
app.use(express.json({ limit: '50mb' }));
|
||||||
|
app.use(express.urlencoded({ extended: true, limit: '50mb' }));
|
||||||
|
|
||||||
|
// 文件上传接口
|
||||||
|
app.post('/api/upload', upload.single('file'), async (req, res) => {
|
||||||
|
try {
|
||||||
|
if (!req.file) {
|
||||||
|
return res.status(400).json({ error: 'No file uploaded' });
|
||||||
|
}
|
||||||
|
|
||||||
|
// 确保文件名正确编码
|
||||||
|
const originalName = Buffer.from(req.file.originalname, 'latin1').toString('utf8');
|
||||||
|
|
||||||
|
const fileInfo = {
|
||||||
|
originalName: originalName,
|
||||||
|
fileName: req.file.filename,
|
||||||
|
filePath: req.file.path,
|
||||||
|
fileSize: req.file.size,
|
||||||
|
mimeType: req.file.mimetype
|
||||||
|
};
|
||||||
|
|
||||||
|
// 计算 MD5
|
||||||
|
const md5 = await calculateFileMD5(req.file.path);
|
||||||
|
|
||||||
|
// 保存到数据库
|
||||||
|
const fileRecord = await fileService.createFile({
|
||||||
|
...fileInfo,
|
||||||
|
md5
|
||||||
|
});
|
||||||
|
|
||||||
|
res.json({
|
||||||
|
success: true,
|
||||||
|
data: fileRecord
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Upload error:', error);
|
||||||
|
res.status(500).json({ error: 'Upload failed: ' + error.message });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// 获取文件列表接口
|
||||||
|
app.get('/api/files', async (req, res) => {
|
||||||
|
try {
|
||||||
|
const page = parseInt(req.query.page as string) || 1;
|
||||||
|
const pageSize = parseInt(req.query.pageSize as string) || 100;
|
||||||
|
|
||||||
|
const result = await fileService.getFilesPaginated(page, pageSize);
|
||||||
|
|
||||||
|
// 返回统一的数据结构
|
||||||
|
res.json({
|
||||||
|
success: true,
|
||||||
|
data: result.files, // 直接返回文件数组
|
||||||
|
pagination: result.pagination
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Get files error:', error);
|
||||||
|
res.status(500).json({
|
||||||
|
success: false,
|
||||||
|
error: 'Failed to get files: ' + error.message
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// MD5 检查接口
|
||||||
|
app.post('/api/files/:id/check-md5', async (req, res) => {
|
||||||
|
try {
|
||||||
|
const fileId = parseInt(req.params.id);
|
||||||
|
const file = await fileService.getFileById(fileId);
|
||||||
|
|
||||||
|
if (!file) {
|
||||||
|
return res.status(404).json({ error: 'File not found' });
|
||||||
|
}
|
||||||
|
|
||||||
|
const currentMD5 = await calculateFileMD5(file.filePath);
|
||||||
|
const isChanged = currentMD5 !== file.md5;
|
||||||
|
|
||||||
|
res.json({
|
||||||
|
isChanged,
|
||||||
|
currentMD5,
|
||||||
|
originalMD5: file.md5,
|
||||||
|
file
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
console.error('MD5 check error:', error);
|
||||||
|
res.status(500).json({ error: 'MD5 check failed' });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// 更新 MD5 接口
|
||||||
|
app.put('/api/files/:id/update-md5', async (req, res) => {
|
||||||
|
try {
|
||||||
|
const fileId = parseInt(req.params.id);
|
||||||
|
const { md5 } = req.body;
|
||||||
|
|
||||||
|
await fileService.updateFileMD5(fileId, md5);
|
||||||
|
res.json({ success: true });
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Update MD5 error:', error);
|
||||||
|
res.status(500).json({ error: 'Update failed' });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// OCR 识别接口 - 使用 OfflineOcrManager
|
||||||
|
app.post('/api/ocr/recognize', async (req, res) => {
|
||||||
|
try {
|
||||||
|
const { fileId, config } = req.body;
|
||||||
|
|
||||||
|
if (!fileId) {
|
||||||
|
return res.status(400).json({ error: 'File ID is required' });
|
||||||
|
}
|
||||||
|
|
||||||
|
const file = await fileService.getFileById(parseInt(fileId));
|
||||||
|
if (!file) {
|
||||||
|
return res.status(404).json({ error: 'File not found' });
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log(`开始ONNX OCR识别: ${file.originalName}`);
|
||||||
|
|
||||||
|
// 使用ONNX OCR管理器进行识别
|
||||||
|
const result = await onnxOcrManager.recognizeImage(file.filePath, config);
|
||||||
|
|
||||||
|
res.json({
|
||||||
|
success: true,
|
||||||
|
data: {
|
||||||
|
textBlocks: result.textBlocks,
|
||||||
|
totalPages: result.totalPages,
|
||||||
|
processingTime: result.processingTime,
|
||||||
|
confidence: result.confidence,
|
||||||
|
processedImageUrl: '', // ONNX版本暂时不提供处理后的图片
|
||||||
|
imageInfo: result.imageInfo,
|
||||||
|
isOffline: result.isOffline
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
console.error('ONNX OCR识别失败:', error);
|
||||||
|
res.status(500).json({ error: 'OCR识别失败: ' + error.message });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
// 保存处理后的图片
|
||||||
|
async function saveProcessedImage(fileId: number, processedImagePath: string): Promise<string> {
|
||||||
|
try {
|
||||||
|
const targetPath = path.join(processedDir, `processed-${fileId}.png`);
|
||||||
|
|
||||||
|
// 使用sharp处理并保存图片
|
||||||
|
await sharp(processedImagePath)
|
||||||
|
.grayscale()
|
||||||
|
.normalize()
|
||||||
|
.sharpen()
|
||||||
|
.png()
|
||||||
|
.toFile(targetPath);
|
||||||
|
|
||||||
|
return `/api/files/${fileId}/processed-image`;
|
||||||
|
} catch (error) {
|
||||||
|
console.error('保存处理后的图片失败:', error);
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 获取处理后的图片
|
||||||
|
app.get('/api/files/:id/processed-image', async (req, res) => {
|
||||||
|
try {
|
||||||
|
const fileId = parseInt(req.params.id);
|
||||||
|
const processedImagePath = path.join(processedDir, `processed-${fileId}.png`);
|
||||||
|
|
||||||
|
if (!fs.existsSync(processedImagePath)) {
|
||||||
|
return res.status(404).json({ error: 'Processed image not found' });
|
||||||
|
}
|
||||||
|
|
||||||
|
res.setHeader('Content-Type', 'image/png');
|
||||||
|
res.sendFile(path.resolve(processedImagePath));
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Get processed image error:', error);
|
||||||
|
res.status(500).json({ error: 'Failed to get processed image' });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// 保存 OCR 结果
|
||||||
|
app.post('/api/ocr/save-result', async (req, res) => {
|
||||||
|
try {
|
||||||
|
const { fileId, ocrData } = req.body;
|
||||||
|
|
||||||
|
if (!fileId || !ocrData) {
|
||||||
|
return res.status(400).json({ error: '文件ID和OCR数据是必需的' });
|
||||||
|
}
|
||||||
|
|
||||||
|
await fileService.saveOcrResult(parseInt(fileId), ocrData);
|
||||||
|
|
||||||
|
res.json({ success: true });
|
||||||
|
} catch (error) {
|
||||||
|
console.error('保存OCR结果失败:', error);
|
||||||
|
res.status(500).json({ error: '保存OCR结果失败: ' + error.message });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// 获取 OCR 结果
|
||||||
|
app.get('/api/ocr/result/:fileId', async (req, res) => {
|
||||||
|
try {
|
||||||
|
const fileId = parseInt(req.params.fileId);
|
||||||
|
const result = await fileService.getOcrResult(fileId);
|
||||||
|
|
||||||
|
if (result) {
|
||||||
|
res.json({
|
||||||
|
success: true,
|
||||||
|
data: result.ocr_data
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
res.json({
|
||||||
|
success: false,
|
||||||
|
error: '未找到OCR结果'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('获取OCR结果失败:', error);
|
||||||
|
res.status(500).json({ error: '获取OCR结果失败: ' + error.message });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// 更新 OCR 文本(人工纠错)
|
||||||
|
app.put('/api/ocr/update-text', async (req, res) => {
|
||||||
|
try {
|
||||||
|
const { fileId, textBlocks } = req.body;
|
||||||
|
|
||||||
|
if (!fileId || !textBlocks) {
|
||||||
|
return res.status(400).json({ error: '文件ID和文本数据是必需的' });
|
||||||
|
}
|
||||||
|
|
||||||
|
await fileService.updateOcrText(parseInt(fileId), textBlocks);
|
||||||
|
|
||||||
|
res.json({ success: true });
|
||||||
|
} catch (error) {
|
||||||
|
console.error('更新OCR文本失败:', error);
|
||||||
|
res.status(500).json({ error: '更新OCR文本失败: ' + error.message });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// 获取文件预览接口
|
||||||
|
app.get('/api/files/:id/preview', async (req, res) => {
|
||||||
|
try {
|
||||||
|
const fileId = parseInt(req.params.id);
|
||||||
|
const file = await fileService.getFileById(fileId);
|
||||||
|
|
||||||
|
if (!file) {
|
||||||
|
return res.status(404).json({ error: 'File not found' });
|
||||||
|
}
|
||||||
|
|
||||||
|
// 检查文件是否存在
|
||||||
|
if (!fs.existsSync(file.filePath)) {
|
||||||
|
return res.status(404).json({ error: 'File not found on disk' });
|
||||||
|
}
|
||||||
|
|
||||||
|
// 设置正确的 Content-Type
|
||||||
|
res.setHeader('Content-Type', file.mimeType);
|
||||||
|
|
||||||
|
// 直接发送文件
|
||||||
|
res.sendFile(path.resolve(file.filePath));
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
console.error('File preview error:', error);
|
||||||
|
res.status(500).json({ error: 'Failed to get file preview' });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
// 更新批量OCR接口
|
||||||
|
app.post('/api/ocr/batch-recognize', async (req, res) => {
|
||||||
|
try {
|
||||||
|
const { fileIds, config } = req.body;
|
||||||
|
|
||||||
|
if (!fileIds || !Array.isArray(fileIds)) {
|
||||||
|
return res.status(400).json({ error: 'File IDs array is required' });
|
||||||
|
}
|
||||||
|
|
||||||
|
const filePaths = [];
|
||||||
|
for (const fileId of fileIds) {
|
||||||
|
const file = await fileService.getFileById(parseInt(fileId));
|
||||||
|
if (file) {
|
||||||
|
filePaths.push(file.filePath);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const results = await onnxOcrManager.batchRecognize(filePaths, config);
|
||||||
|
|
||||||
|
res.json({
|
||||||
|
success: true,
|
||||||
|
data: results
|
||||||
|
});
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
console.error('批量ONNX OCR识别失败:', error);
|
||||||
|
res.status(500).json({ error: '批量识别失败: ' + error.message });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
// 获取预处理后的图片
|
||||||
|
app.get('/api/ocr/processed-image', async (req, res) => {
|
||||||
|
try {
|
||||||
|
const imagePath = req.query.path as string;
|
||||||
|
|
||||||
|
if (!imagePath) {
|
||||||
|
return res.status(400).json({ error: '图片路径是必需的' });
|
||||||
|
}
|
||||||
|
|
||||||
|
// 解码路径
|
||||||
|
const decodedPath = decodeURIComponent(imagePath);
|
||||||
|
|
||||||
|
if (!fse.existsSync(decodedPath)) {
|
||||||
|
return res.status(404).json({ error: '预处理图片不存在' });
|
||||||
|
}
|
||||||
|
|
||||||
|
res.setHeader('Content-Type', 'image/png');
|
||||||
|
res.sendFile(path.resolve(decodedPath));
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
console.error('获取预处理图片失败:', error);
|
||||||
|
res.status(500).json({ error: '获取预处理图片失败' });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// 在 server/server.ts 中添加调试接口
|
||||||
|
app.post('/api/ocr/debug-recognition', async (req, res) => {
|
||||||
|
try {
|
||||||
|
const { fileId, boxIndex } = req.body;
|
||||||
|
|
||||||
|
if (!fileId || boxIndex === undefined) {
|
||||||
|
return res.status(400).json({ error: '文件ID和框索引是必需的' });
|
||||||
|
}
|
||||||
|
|
||||||
|
const file = await fileService.getFileById(parseInt(fileId));
|
||||||
|
if (!file) {
|
||||||
|
return res.status(404).json({ error: '文件未找到' });
|
||||||
|
}
|
||||||
|
|
||||||
|
// 这里可以添加具体的调试逻辑
|
||||||
|
console.log(`🔧 调试文件 ${fileId} 的第 ${boxIndex} 个文本框`);
|
||||||
|
|
||||||
|
res.json({
|
||||||
|
success: true,
|
||||||
|
message: '调试信息已输出到控制台'
|
||||||
|
});
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
console.error('调试失败:', error);
|
||||||
|
res.status(500).json({ error: '调试失败: ' + error.message });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
// 更新OCR状态接口
|
||||||
|
app.get('/api/ocr/status', async (req, res) => {
|
||||||
|
try {
|
||||||
|
const status = onnxOcrManager.getStatus();
|
||||||
|
res.json({
|
||||||
|
success: true,
|
||||||
|
data: status
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
console.error('获取ONNX OCR状态失败:', error);
|
||||||
|
res.status(500).json({ error: '获取状态失败: ' + error.message });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
// 获取文件缩略图接口
|
||||||
|
app.get('/api/files/:id/thumbnail', async (req, res) => {
|
||||||
|
const fileId = parseInt(req.params.id);
|
||||||
|
const file = await fileService.getFileById(fileId);
|
||||||
|
try {
|
||||||
|
|
||||||
|
if (!file) {
|
||||||
|
return res.status(404).json({ error: 'File not found' });
|
||||||
|
}
|
||||||
|
|
||||||
|
// 只对图片生成缩略图
|
||||||
|
if (!file.mimeType.startsWith('image/')) {
|
||||||
|
return res.status(400).json({ error: 'Not an image file' });
|
||||||
|
}
|
||||||
|
|
||||||
|
const thumbnailPath = path.join(tempDir, `thumbnail-${fileId}.jpg`);
|
||||||
|
|
||||||
|
// 生成缩略图
|
||||||
|
await sharp(file.filePath)
|
||||||
|
.resize(100, 100, {
|
||||||
|
fit: 'inside',
|
||||||
|
withoutEnlargement: true
|
||||||
|
})
|
||||||
|
.jpeg({ quality: 80 })
|
||||||
|
.toFile(thumbnailPath);
|
||||||
|
|
||||||
|
res.sendFile(path.resolve(thumbnailPath));
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Thumbnail generation error:', error);
|
||||||
|
// 如果缩略图生成失败,返回原图
|
||||||
|
res.sendFile(path.resolve(file.filePath));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// 健康检查接口
|
||||||
|
app.get('/api/health', (req, res) => {
|
||||||
|
res.json({
|
||||||
|
status: 'OK',
|
||||||
|
timestamp: new Date().toISOString(),
|
||||||
|
service: 'file-management-api'
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
// 服务器启动时初始化OCR引擎
|
||||||
|
async function initializeOcrEngine() {
|
||||||
|
try {
|
||||||
|
console.log('正在初始化ONNX OCR引擎...');
|
||||||
|
await onnxOcrManager.initialize();
|
||||||
|
console.log('ONNX OCR引擎初始化完成');
|
||||||
|
} catch (error) {
|
||||||
|
console.error('ONNX OCR引擎初始化失败:', error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function startServer() {
|
||||||
|
// 启动时初始化OCR引擎
|
||||||
|
initializeOcrEngine();
|
||||||
|
|
||||||
|
app.listen(PORT, () => {
|
||||||
|
console.log(`Server running on http://localhost:${PORT}`);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export { startServer };
|
||||||
271
server/utils/detectionProcessor.js
普通文件
271
server/utils/detectionProcessor.js
普通文件
@ -0,0 +1,271 @@
|
|||||||
|
// server/utils/detectionProcessor.js
|
||||||
|
import { Tensor } from 'onnxruntime-node';
|
||||||
|
import sharp from 'sharp';
|
||||||
|
|
||||||
|
class DetectionProcessor {
|
||||||
|
constructor() {
|
||||||
|
this.session = null;
|
||||||
|
this.config = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
initialize(session, config) {
|
||||||
|
this.session = session;
|
||||||
|
this.config = config;
|
||||||
|
}
|
||||||
|
|
||||||
|
async detectText(processedImage) {
|
||||||
|
try {
|
||||||
|
const inputTensor = await this.prepareDetectionInput(processedImage);
|
||||||
|
const outputs = await this.session.run({ [this.session.inputNames[0]]: inputTensor });
|
||||||
|
const textBoxes = this.postprocessDetection(outputs, processedImage);
|
||||||
|
return textBoxes;
|
||||||
|
} catch (error) {
|
||||||
|
console.error('文本检测失败:', error);
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async prepareDetectionInput(processedImage) {
|
||||||
|
const { buffer, width, height } = processedImage;
|
||||||
|
|
||||||
|
const imageData = await sharp(buffer)
|
||||||
|
.ensureAlpha()
|
||||||
|
.raw()
|
||||||
|
.toBuffer({ resolveWithObject: true });
|
||||||
|
|
||||||
|
const inputData = new Float32Array(3 * height * width);
|
||||||
|
const data = imageData.data;
|
||||||
|
const channels = imageData.info.channels;
|
||||||
|
|
||||||
|
for (let i = 0; i < data.length; i += channels) {
|
||||||
|
const pixelIndex = Math.floor(i / channels);
|
||||||
|
const channel = Math.floor(pixelIndex / (height * width));
|
||||||
|
const posInChannel = pixelIndex % (height * width);
|
||||||
|
|
||||||
|
if (channel < 3) {
|
||||||
|
const y = Math.floor(posInChannel / width);
|
||||||
|
const x = posInChannel % width;
|
||||||
|
const inputIndex = channel * height * width + y * width + x;
|
||||||
|
|
||||||
|
if (inputIndex < inputData.length) {
|
||||||
|
inputData[inputIndex] = data[i] / 255.0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return new Tensor('float32', inputData, [1, 3, height, width]);
|
||||||
|
}
|
||||||
|
|
||||||
|
postprocessDetection(outputs, processedImage) {
|
||||||
|
try {
|
||||||
|
const boxes = [];
|
||||||
|
const outputNames = this.session.outputNames;
|
||||||
|
const detectionOutput = outputs[outputNames[0]];
|
||||||
|
|
||||||
|
if (!detectionOutput) {
|
||||||
|
return boxes;
|
||||||
|
}
|
||||||
|
|
||||||
|
const [batch, channels, height, width] = detectionOutput.dims;
|
||||||
|
const data = detectionOutput.data;
|
||||||
|
|
||||||
|
// 降低检测阈值,提高召回率
|
||||||
|
const threshold = this.config.detThresh || 0.05;
|
||||||
|
const points = [];
|
||||||
|
|
||||||
|
// 改进的点收集逻辑
|
||||||
|
for (let y = 0; y < height; y++) {
|
||||||
|
for (let x = 0; x < width; x++) {
|
||||||
|
const idx = y * width + x;
|
||||||
|
const prob = data[idx];
|
||||||
|
if (prob > threshold) {
|
||||||
|
points.push({
|
||||||
|
x,
|
||||||
|
y,
|
||||||
|
prob,
|
||||||
|
localMax: this.isLocalMaximum(data, x, y, width, height, 2)
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (points.length === 0) {
|
||||||
|
return boxes;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 改进的聚类算法
|
||||||
|
const clusters = this.enhancedCluster(points, 8);
|
||||||
|
|
||||||
|
for (const cluster of clusters) {
|
||||||
|
// 降低最小点数要求
|
||||||
|
if (cluster.length < 2) continue;
|
||||||
|
|
||||||
|
const minX = Math.min(...cluster.map(p => p.x));
|
||||||
|
const maxX = Math.max(...cluster.map(p => p.x));
|
||||||
|
const minY = Math.min(...cluster.map(p => p.y));
|
||||||
|
const maxY = Math.max(...cluster.map(p => p.y));
|
||||||
|
|
||||||
|
const boxWidth = maxX - minX;
|
||||||
|
const boxHeight = maxY - minY;
|
||||||
|
|
||||||
|
// 放宽尺寸限制
|
||||||
|
if (boxWidth < 2 || boxHeight < 2) continue;
|
||||||
|
|
||||||
|
const aspectRatio = boxWidth / boxHeight;
|
||||||
|
// 放宽宽高比限制
|
||||||
|
if (aspectRatio > 100 || aspectRatio < 0.01) continue;
|
||||||
|
|
||||||
|
const avgConfidence = cluster.reduce((sum, p) => sum + p.prob, 0) / cluster.length;
|
||||||
|
|
||||||
|
// 降低框置信度阈值
|
||||||
|
const boxThreshold = this.config.detBoxThresh || 0.1;
|
||||||
|
if (avgConfidence > boxThreshold) {
|
||||||
|
const box = this.scaleBoxToProcessedImage({
|
||||||
|
x1: minX, y1: minY,
|
||||||
|
x2: maxX, y2: minY,
|
||||||
|
x3: maxX, y3: maxY,
|
||||||
|
x4: minX, y4: maxY
|
||||||
|
}, processedImage);
|
||||||
|
box.confidence = avgConfidence;
|
||||||
|
boxes.push(box);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
boxes.sort((a, b) => b.confidence - a.confidence);
|
||||||
|
console.log(`✅ 检测到 ${boxes.length} 个文本区域`);
|
||||||
|
return boxes;
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
console.error('检测后处理错误:', error);
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 添加局部最大值检测
|
||||||
|
isLocalMaximum(data, x, y, width, height, radius) {
|
||||||
|
const centerProb = data[y * width + x];
|
||||||
|
for (let dy = -radius; dy <= radius; dy++) {
|
||||||
|
for (let dx = -radius; dx <= radius; dx++) {
|
||||||
|
if (dx === 0 && dy === 0) continue;
|
||||||
|
const nx = x + dx;
|
||||||
|
const ny = y + dy;
|
||||||
|
if (nx >= 0 && nx < width && ny >= 0 && ny < height) {
|
||||||
|
if (data[ny * width + nx] > centerProb) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 改进的聚类算法
|
||||||
|
enhancedCluster(points, distanceThreshold) {
|
||||||
|
const clusters = [];
|
||||||
|
const visited = new Set();
|
||||||
|
|
||||||
|
// 按概率降序排序,优先处理高置信度点
|
||||||
|
const sortedPoints = [...points].sort((a, b) => b.prob - a.prob);
|
||||||
|
|
||||||
|
for (let i = 0; i < sortedPoints.length; i++) {
|
||||||
|
if (visited.has(i)) continue;
|
||||||
|
|
||||||
|
const cluster = [];
|
||||||
|
const queue = [i];
|
||||||
|
visited.add(i);
|
||||||
|
|
||||||
|
while (queue.length > 0) {
|
||||||
|
const currentIndex = queue.shift();
|
||||||
|
const currentPoint = sortedPoints[currentIndex];
|
||||||
|
cluster.push(currentPoint);
|
||||||
|
|
||||||
|
// 动态调整搜索半径
|
||||||
|
const adaptiveThreshold = distanceThreshold *
|
||||||
|
(1 + (1 - currentPoint.prob) * 0.5);
|
||||||
|
|
||||||
|
for (let j = 0; j < sortedPoints.length; j++) {
|
||||||
|
if (visited.has(j)) continue;
|
||||||
|
|
||||||
|
const targetPoint = sortedPoints[j];
|
||||||
|
const dist = Math.sqrt(
|
||||||
|
Math.pow(targetPoint.x - currentPoint.x, 2) +
|
||||||
|
Math.pow(targetPoint.y - currentPoint.y, 2)
|
||||||
|
);
|
||||||
|
|
||||||
|
if (dist < adaptiveThreshold) {
|
||||||
|
queue.push(j);
|
||||||
|
visited.add(j);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (cluster.length > 0) {
|
||||||
|
clusters.push(cluster);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return clusters;
|
||||||
|
}
|
||||||
|
|
||||||
|
scaleBoxToProcessedImage(box, processedImage) {
|
||||||
|
const { width: processedWidth, height: processedHeight } = processedImage;
|
||||||
|
|
||||||
|
const scaledBox = {
|
||||||
|
x1: box.x1,
|
||||||
|
y1: box.y1,
|
||||||
|
x2: box.x2,
|
||||||
|
y2: box.y2,
|
||||||
|
x3: box.x3,
|
||||||
|
y3: box.y3,
|
||||||
|
x4: box.x4,
|
||||||
|
y4: box.y4
|
||||||
|
};
|
||||||
|
|
||||||
|
const clamp = (value, max) => Math.max(0, Math.min(max, value));
|
||||||
|
|
||||||
|
return {
|
||||||
|
x1: clamp(scaledBox.x1, processedWidth - 1),
|
||||||
|
y1: clamp(scaledBox.y1, processedHeight - 1),
|
||||||
|
x2: clamp(scaledBox.x2, processedWidth - 1),
|
||||||
|
y2: clamp(scaledBox.y2, processedHeight - 1),
|
||||||
|
x3: clamp(scaledBox.x3, processedWidth - 1),
|
||||||
|
y3: clamp(scaledBox.y3, processedHeight - 1),
|
||||||
|
x4: clamp(scaledBox.x4, processedWidth - 1),
|
||||||
|
y4: clamp(scaledBox.y4, processedHeight - 1)
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
scaleBoxToOriginalImage(box, processedImage) {
|
||||||
|
const {
|
||||||
|
scaleX, scaleY,
|
||||||
|
paddingX, paddingY,
|
||||||
|
originalWidth, originalHeight
|
||||||
|
} = processedImage;
|
||||||
|
|
||||||
|
const paddedX1 = box.x1 * scaleX;
|
||||||
|
const paddedY1 = box.y1 * scaleY;
|
||||||
|
const paddedX3 = box.x3 * scaleX;
|
||||||
|
const paddedY3 = box.y3 * scaleY;
|
||||||
|
|
||||||
|
const originalX1 = paddedX1 - paddingX;
|
||||||
|
const originalY1 = paddedY1 - paddingY;
|
||||||
|
const originalX3 = paddedX3 - paddingX;
|
||||||
|
const originalY3 = paddedY3 - paddingY;
|
||||||
|
|
||||||
|
const clamp = (value, max) => Math.max(0, Math.min(max, value));
|
||||||
|
|
||||||
|
return {
|
||||||
|
x1: clamp(originalX1, originalWidth - 1),
|
||||||
|
y1: clamp(originalY1, originalHeight - 1),
|
||||||
|
x2: clamp(originalX3, originalWidth - 1),
|
||||||
|
y2: clamp(originalY1, originalHeight - 1),
|
||||||
|
x3: clamp(originalX3, originalWidth - 1),
|
||||||
|
y3: clamp(originalY3, originalHeight - 1),
|
||||||
|
x4: clamp(originalX1, originalWidth - 1),
|
||||||
|
y4: clamp(originalY3, originalHeight - 1),
|
||||||
|
confidence: box.confidence
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default DetectionProcessor;
|
||||||
104
server/utils/imagePreprocessor.js
普通文件
104
server/utils/imagePreprocessor.js
普通文件
@ -0,0 +1,104 @@
|
|||||||
|
// server/utils/imagePreprocessor.js
|
||||||
|
import sharp from 'sharp';
|
||||||
|
|
||||||
|
class ImagePreprocessor {
|
||||||
|
constructor() {
|
||||||
|
this.tempDir = './temp/processed';
|
||||||
|
}
|
||||||
|
|
||||||
|
async preprocessWithPadding(imagePath, config) {
|
||||||
|
try {
|
||||||
|
const metadata = await sharp(imagePath).metadata();
|
||||||
|
|
||||||
|
// 减少填充,避免过度改变图像
|
||||||
|
const minPadding = 30;
|
||||||
|
const paddingX = Math.max(minPadding, Math.floor(metadata.width * 0.05));
|
||||||
|
const paddingY = Math.max(minPadding, Math.floor(metadata.height * 0.05));
|
||||||
|
|
||||||
|
const paddedWidth = metadata.width + paddingX * 2;
|
||||||
|
const paddedHeight = metadata.height + paddingY * 2;
|
||||||
|
|
||||||
|
const paddedBuffer = await sharp(imagePath)
|
||||||
|
.extend({
|
||||||
|
top: paddingY,
|
||||||
|
bottom: paddingY,
|
||||||
|
left: paddingX,
|
||||||
|
right: paddingX,
|
||||||
|
background: { r: 255, g: 255, b: 255 }
|
||||||
|
})
|
||||||
|
.png()
|
||||||
|
.toBuffer();
|
||||||
|
|
||||||
|
const { width, height } = this.resizeForDetection({
|
||||||
|
width: paddedWidth,
|
||||||
|
height: paddedHeight
|
||||||
|
}, config);
|
||||||
|
|
||||||
|
const resizedBuffer = await sharp(paddedBuffer)
|
||||||
|
.resize(width, height)
|
||||||
|
.png()
|
||||||
|
.toBuffer();
|
||||||
|
|
||||||
|
console.log(`🖼️ 图像预处理完成: ${metadata.width}x${metadata.height} -> ${width}x${height}`);
|
||||||
|
|
||||||
|
return {
|
||||||
|
processedImage: {
|
||||||
|
buffer: resizedBuffer,
|
||||||
|
width,
|
||||||
|
height,
|
||||||
|
originalWidth: metadata.width,
|
||||||
|
originalHeight: metadata.height,
|
||||||
|
paddedWidth: paddedWidth,
|
||||||
|
paddedHeight: paddedHeight,
|
||||||
|
paddingX,
|
||||||
|
paddingY,
|
||||||
|
scaleX: paddedWidth / width,
|
||||||
|
scaleY: paddedHeight / height
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
console.error('预处理错误:', error);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
resizeForDetection(metadata, config) {
|
||||||
|
const { width, height } = metadata;
|
||||||
|
const limitSideLen = config.detLimitSideLen || 960;
|
||||||
|
|
||||||
|
let ratio = 1;
|
||||||
|
if (Math.max(width, height) > limitSideLen) {
|
||||||
|
ratio = limitSideLen / Math.max(width, height);
|
||||||
|
}
|
||||||
|
|
||||||
|
const newWidth = Math.floor(width * ratio);
|
||||||
|
const newHeight = Math.floor(height * ratio);
|
||||||
|
|
||||||
|
return {
|
||||||
|
width: Math.max(32, Math.floor(newWidth / 32) * 32),
|
||||||
|
height: Math.max(32, Math.floor(newHeight / 32) * 32)
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
async getImageInfo(imagePath) {
|
||||||
|
try {
|
||||||
|
const metadata = await sharp(imagePath).metadata();
|
||||||
|
return {
|
||||||
|
width: metadata.width || 0,
|
||||||
|
height: metadata.height || 0,
|
||||||
|
format: metadata.format || 'unknown',
|
||||||
|
processed: false
|
||||||
|
};
|
||||||
|
} catch (error) {
|
||||||
|
return {
|
||||||
|
width: 0,
|
||||||
|
height: 0,
|
||||||
|
format: 'unknown',
|
||||||
|
processed: false
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default ImagePreprocessor;
|
||||||
201
server/utils/onnxOcrManager.js
普通文件
201
server/utils/onnxOcrManager.js
普通文件
@ -0,0 +1,201 @@
|
|||||||
|
// server/utils/onnxOcrManager.js
|
||||||
|
import { InferenceSession } from 'onnxruntime-node';
|
||||||
|
import fse from 'fs-extra';
|
||||||
|
import * as path from 'path';
|
||||||
|
import { fileURLToPath } from 'url';
|
||||||
|
|
||||||
|
import DetectionProcessor from './detectionProcessor.js';
|
||||||
|
import RecognitionProcessor from './recognitionProcessor.js';
|
||||||
|
import ImagePreprocessor from './imagePreprocessor.js';
|
||||||
|
import TextPostProcessor from './textPostProcessor.js';
|
||||||
|
|
||||||
|
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
||||||
|
|
||||||
|
class OnnxOcrManager {
|
||||||
|
constructor() {
|
||||||
|
this.detSession = null;
|
||||||
|
this.recSession = null;
|
||||||
|
this.clsSession = null;
|
||||||
|
this.isInitialized = false;
|
||||||
|
|
||||||
|
this.modelDir = path.join(process.cwd(), 'models', 'ocr');
|
||||||
|
this.detModelPath = path.join(this.modelDir, 'Det', '中文_OCRv3.onnx');
|
||||||
|
this.recModelPath = path.join(this.modelDir, 'Rec', '中文简体_OCRv3.onnx');
|
||||||
|
this.clsModelPath = path.join(this.modelDir, 'Cls', '原始分类器模型.onnx');
|
||||||
|
this.keysPath = path.join(this.modelDir, 'Keys', '中文简体_OCRv3.txt');
|
||||||
|
|
||||||
|
this.detectionProcessor = new DetectionProcessor();
|
||||||
|
this.recognitionProcessor = new RecognitionProcessor();
|
||||||
|
this.imagePreprocessor = new ImagePreprocessor();
|
||||||
|
this.textPostProcessor = new TextPostProcessor();
|
||||||
|
|
||||||
|
// 更新默认配置,优化识别效果
|
||||||
|
this.defaultConfig = {
|
||||||
|
language: 'ch',
|
||||||
|
detLimitSideLen: 960,
|
||||||
|
detThresh: 0.05, // 降低检测阈值
|
||||||
|
detBoxThresh: 0.1, // 降低框阈值
|
||||||
|
detUnclipRatio: 1.8, // 调整解压缩比例
|
||||||
|
maxTextLength: 50, // 增加最大文本长度
|
||||||
|
recImageHeight: 48,
|
||||||
|
clsThresh: 0.8, // 降低分类阈值
|
||||||
|
minTextHeight: 2, // 降低最小文本高度
|
||||||
|
minTextWidth: 2, // 降低最小文本宽度
|
||||||
|
clusterDistance: 8, // 调整聚类距离
|
||||||
|
minClusterPoints: 2 // 降低最小聚类点数
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
async initialize(config = {}) {
|
||||||
|
if (this.isInitialized) {
|
||||||
|
console.log('🔁 OCR管理器已初始化');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
console.log('🚀 开始初始化OCR管理器...');
|
||||||
|
await this.validateModelFiles();
|
||||||
|
await this.recognitionProcessor.loadCharacterSet(this.keysPath);
|
||||||
|
|
||||||
|
const [detSession, recSession, clsSession] = await Promise.all([
|
||||||
|
InferenceSession.create(this.detModelPath, { executionProviders: ['cpu'] }),
|
||||||
|
InferenceSession.create(this.recModelPath, { executionProviders: ['cpu'] }),
|
||||||
|
InferenceSession.create(this.clsModelPath, { executionProviders: ['cpu'] })
|
||||||
|
]);
|
||||||
|
|
||||||
|
this.detSession = detSession;
|
||||||
|
this.recSession = recSession;
|
||||||
|
this.clsSession = clsSession;
|
||||||
|
|
||||||
|
const mergedConfig = { ...this.defaultConfig, ...config };
|
||||||
|
|
||||||
|
this.detectionProcessor.initialize(this.detSession, mergedConfig);
|
||||||
|
this.recognitionProcessor.initialize(this.recSession, this.clsSession, mergedConfig);
|
||||||
|
|
||||||
|
this.isInitialized = true;
|
||||||
|
console.log('✅ OCR管理器初始化完成');
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
console.error('❌ OCR管理器初始化失败:', error);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async validateModelFiles() {
|
||||||
|
const requiredFiles = [
|
||||||
|
{ path: this.detModelPath, name: '检测模型' },
|
||||||
|
{ path: this.recModelPath, name: '识别模型' },
|
||||||
|
{ path: this.clsModelPath, name: '分类模型' },
|
||||||
|
{ path: this.keysPath, name: '字符集文件' }
|
||||||
|
];
|
||||||
|
|
||||||
|
for (const { path: filePath, name } of requiredFiles) {
|
||||||
|
const exists = await fse.pathExists(filePath);
|
||||||
|
if (!exists) {
|
||||||
|
throw new Error(`模型文件不存在: ${filePath}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
console.log('✅ 所有模型文件验证通过');
|
||||||
|
}
|
||||||
|
|
||||||
|
async recognizeImage(imagePath, config = {}) {
|
||||||
|
if (!this.isInitialized) {
|
||||||
|
await this.initialize(config);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!imagePath || typeof imagePath !== 'string') {
|
||||||
|
throw new Error(`无效的图片路径: ${imagePath}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!fse.existsSync(imagePath)) {
|
||||||
|
throw new Error(`图片文件不存在: ${imagePath}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
console.log(`\n🎯 开始OCR识别: ${path.basename(imagePath)}`);
|
||||||
|
const startTime = Date.now();
|
||||||
|
|
||||||
|
const preprocessResult = await this.imagePreprocessor.preprocessWithPadding(imagePath, config);
|
||||||
|
const { processedImage } = preprocessResult;
|
||||||
|
|
||||||
|
const textBoxes = await this.detectionProcessor.detectText(processedImage);
|
||||||
|
const recognitionResults = await this.recognitionProcessor.recognizeTextWithCls(processedImage, textBoxes);
|
||||||
|
|
||||||
|
const processingTime = Date.now() - startTime;
|
||||||
|
|
||||||
|
const textBlocks = this.textPostProcessor.buildTextBlocks(recognitionResults);
|
||||||
|
const imageInfo = await this.imagePreprocessor.getImageInfo(imagePath);
|
||||||
|
|
||||||
|
const rawText = textBlocks.map(block => block.content).join('\n');
|
||||||
|
const overallConfidence = this.textPostProcessor.calculateOverallConfidence(recognitionResults);
|
||||||
|
|
||||||
|
const result = {
|
||||||
|
textBlocks,
|
||||||
|
confidence: overallConfidence,
|
||||||
|
processingTime,
|
||||||
|
isOffline: true,
|
||||||
|
imagePath,
|
||||||
|
totalPages: 1,
|
||||||
|
rawText,
|
||||||
|
imageInfo,
|
||||||
|
recognitionCount: recognitionResults.length
|
||||||
|
};
|
||||||
|
|
||||||
|
console.log(`\n📊 OCR识别统计:`);
|
||||||
|
console.log(` - 处理时间: ${processingTime}ms`);
|
||||||
|
console.log(` - 检测区域: ${textBoxes.length} 个`);
|
||||||
|
console.log(` - 成功识别: ${recognitionResults.length} 个`);
|
||||||
|
console.log(` - 总体置信度: ${overallConfidence.toFixed(4)}`);
|
||||||
|
console.log(` - 最终文本长度: ${rawText.length} 字符`);
|
||||||
|
|
||||||
|
return result;
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
console.error(`❌ OCR识别失败: ${error.message}`);
|
||||||
|
throw new Error(`OCR识别失败: ${error.message}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
getStatus() {
|
||||||
|
return {
|
||||||
|
isInitialized: this.isInitialized,
|
||||||
|
isOffline: true,
|
||||||
|
engine: 'PP-OCRv3 (ONNX Runtime)',
|
||||||
|
version: '1.0.0',
|
||||||
|
models: {
|
||||||
|
detection: path.relative(process.cwd(), this.detModelPath),
|
||||||
|
recognition: path.relative(process.cwd(), this.recModelPath),
|
||||||
|
classification: path.relative(process.cwd(), this.clsModelPath),
|
||||||
|
characterSet: this.recognitionProcessor.getCharacterSetSize()
|
||||||
|
},
|
||||||
|
config: {
|
||||||
|
detThresh: this.defaultConfig.detThresh,
|
||||||
|
detBoxThresh: this.defaultConfig.detBoxThresh,
|
||||||
|
clsThresh: this.defaultConfig.clsThresh,
|
||||||
|
preprocessing: 'enabled with padding'
|
||||||
|
},
|
||||||
|
backend: 'CPU'
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
async terminate() {
|
||||||
|
if (this.detSession) {
|
||||||
|
this.detSession.release();
|
||||||
|
this.detSession = null;
|
||||||
|
}
|
||||||
|
if (this.recSession) {
|
||||||
|
this.recSession.release();
|
||||||
|
this.recSession = null;
|
||||||
|
}
|
||||||
|
if (this.clsSession) {
|
||||||
|
this.clsSession.release();
|
||||||
|
this.clsSession = null;
|
||||||
|
}
|
||||||
|
this.isInitialized = false;
|
||||||
|
console.log('🛑 OCR管理器已终止');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const onnxOcrManager = new OnnxOcrManager();
|
||||||
|
|
||||||
|
export default onnxOcrManager;
|
||||||
131
server/utils/recognitionProcessor.js
普通文件
131
server/utils/recognitionProcessor.js
普通文件
@ -0,0 +1,131 @@
|
|||||||
|
// server/utils/recognitionProcessor.js
|
||||||
|
import TextDirectionClassifier from './textDirectionClassifier.js';
|
||||||
|
import TextRecognizer from './textRecognizer.js';
|
||||||
|
import TextRegionCropper from './textRegionCropper.js';
|
||||||
|
|
||||||
|
class RecognitionProcessor {
|
||||||
|
constructor() {
|
||||||
|
this.recSession = null;
|
||||||
|
this.clsSession = null;
|
||||||
|
this.config = null;
|
||||||
|
|
||||||
|
this.textDirectionClassifier = new TextDirectionClassifier();
|
||||||
|
this.textRecognizer = new TextRecognizer();
|
||||||
|
this.textRegionCropper = new TextRegionCropper();
|
||||||
|
}
|
||||||
|
|
||||||
|
initialize(recSession, clsSession, config) {
|
||||||
|
this.recSession = recSession;
|
||||||
|
this.clsSession = clsSession;
|
||||||
|
this.config = config;
|
||||||
|
|
||||||
|
this.textDirectionClassifier.initialize(clsSession, config);
|
||||||
|
this.textRecognizer.initialize(recSession, config);
|
||||||
|
}
|
||||||
|
|
||||||
|
async loadCharacterSet(keysPath) {
|
||||||
|
await this.textRecognizer.loadCharacterSet(keysPath);
|
||||||
|
}
|
||||||
|
|
||||||
|
getCharacterSetSize() {
|
||||||
|
return this.textRecognizer.getCharacterSetSize();
|
||||||
|
}
|
||||||
|
|
||||||
|
async recognizeTextWithCls(processedImage, textBoxes) {
|
||||||
|
const results = [];
|
||||||
|
|
||||||
|
try {
|
||||||
|
console.log(`🔄 开始处理 ${textBoxes.length} 个文本区域`);
|
||||||
|
|
||||||
|
for (let i = 0; i < textBoxes.length; i++) {
|
||||||
|
const box = textBoxes[i];
|
||||||
|
|
||||||
|
try {
|
||||||
|
console.log(`\n📦 处理区域 ${i + 1}/${textBoxes.length}, 置信度: ${box.confidence.toFixed(4)}`);
|
||||||
|
|
||||||
|
const textRegion = await this.textRegionCropper.cropTextRegion(
|
||||||
|
processedImage.buffer, box, i + 1
|
||||||
|
);
|
||||||
|
if (!textRegion) {
|
||||||
|
console.log(`⏭️ 区域 ${i + 1}: 跳过无效区域`);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
const { clsResult, clsConfidence } = await this.textDirectionClassifier.classifyTextDirection(
|
||||||
|
textRegion.buffer
|
||||||
|
);
|
||||||
|
|
||||||
|
let recognitionImage = textRegion.buffer;
|
||||||
|
if (clsResult === 180 && clsConfidence > this.config.clsThresh) {
|
||||||
|
console.log(`🔄 区域 ${i + 1}: 旋转 180°`);
|
||||||
|
recognitionImage = await this.textRegionCropper.rotateImage(textRegion.buffer, 180);
|
||||||
|
}
|
||||||
|
|
||||||
|
const textResult = await this.textRecognizer.recognizeText(recognitionImage);
|
||||||
|
|
||||||
|
if (textResult.text && textResult.text.trim().length > 0 && textResult.confidence > 0.05) {
|
||||||
|
const originalBox = this.scaleBoxToOriginalImage(box, processedImage);
|
||||||
|
|
||||||
|
results.push({
|
||||||
|
text: textResult.text.trim(),
|
||||||
|
confidence: textResult.confidence * clsConfidence,
|
||||||
|
box: originalBox,
|
||||||
|
clsResult,
|
||||||
|
clsConfidence,
|
||||||
|
regionIndex: i + 1
|
||||||
|
});
|
||||||
|
|
||||||
|
console.log(`✅ 区域 ${i + 1}: 识别成功 "${textResult.text}"`);
|
||||||
|
} else {
|
||||||
|
console.log(`❌ 区域 ${i + 1}: 识别失败或置信度过低`);
|
||||||
|
}
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
console.error(`💥 区域 ${i + 1}: 处理失败`, error.message);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log(`\n🎯 识别完成: ${results.length}/${textBoxes.length} 个区域成功`);
|
||||||
|
return results;
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
console.error('❌ 整体识别失败:', error);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
scaleBoxToOriginalImage(box, processedImage) {
|
||||||
|
const {
|
||||||
|
scaleX, scaleY,
|
||||||
|
paddingX, paddingY,
|
||||||
|
originalWidth, originalHeight
|
||||||
|
} = processedImage;
|
||||||
|
|
||||||
|
const paddedX1 = box.x1 * scaleX;
|
||||||
|
const paddedY1 = box.y1 * scaleY;
|
||||||
|
const paddedX3 = box.x3 * scaleX;
|
||||||
|
const paddedY3 = box.y3 * scaleY;
|
||||||
|
|
||||||
|
const originalX1 = paddedX1 - paddingX;
|
||||||
|
const originalY1 = paddedY1 - paddingY;
|
||||||
|
const originalX3 = paddedX3 - paddingX;
|
||||||
|
const originalY3 = paddedY3 - paddingY;
|
||||||
|
|
||||||
|
const clamp = (value, max) => Math.max(0, Math.min(max, value));
|
||||||
|
|
||||||
|
return {
|
||||||
|
x1: clamp(originalX1, originalWidth - 1),
|
||||||
|
y1: clamp(originalY1, originalHeight - 1),
|
||||||
|
x2: clamp(originalX3, originalWidth - 1),
|
||||||
|
y2: clamp(originalY1, originalHeight - 1),
|
||||||
|
x3: clamp(originalX3, originalWidth - 1),
|
||||||
|
y3: clamp(originalY3, originalHeight - 1),
|
||||||
|
x4: clamp(originalX1, originalWidth - 1),
|
||||||
|
y4: clamp(originalY3, originalHeight - 1),
|
||||||
|
confidence: box.confidence
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default RecognitionProcessor;
|
||||||
@ -0,0 +1,85 @@
|
|||||||
|
// server/utils/textDirectionClassifier.js
|
||||||
|
import { Tensor } from 'onnxruntime-node';
|
||||||
|
import sharp from 'sharp';
|
||||||
|
|
||||||
|
class TextDirectionClassifier {
|
||||||
|
constructor() {
|
||||||
|
this.clsSession = null;
|
||||||
|
this.config = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
initialize(clsSession, config) {
|
||||||
|
this.clsSession = clsSession;
|
||||||
|
this.config = config;
|
||||||
|
}
|
||||||
|
|
||||||
|
async classifyTextDirection(textRegionBuffer) {
|
||||||
|
try {
|
||||||
|
const inputTensor = await this.prepareClsInput(textRegionBuffer);
|
||||||
|
const outputs = await this.clsSession.run({ [this.clsSession.inputNames[0]]: inputTensor });
|
||||||
|
return this.postprocessCls(outputs);
|
||||||
|
} catch (error) {
|
||||||
|
console.error('文本方向分类失败:', error);
|
||||||
|
return { clsResult: 0, clsConfidence: 1.0 };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async prepareClsInput(textRegionBuffer) {
|
||||||
|
const targetHeight = 48;
|
||||||
|
const targetWidth = 192;
|
||||||
|
|
||||||
|
const resizedBuffer = await sharp(textRegionBuffer)
|
||||||
|
.resize(targetWidth, targetHeight)
|
||||||
|
.png()
|
||||||
|
.toBuffer();
|
||||||
|
|
||||||
|
const imageData = await sharp(resizedBuffer)
|
||||||
|
.ensureAlpha()
|
||||||
|
.raw()
|
||||||
|
.toBuffer({ resolveWithObject: true });
|
||||||
|
|
||||||
|
const inputData = new Float32Array(3 * targetHeight * targetWidth);
|
||||||
|
const data = imageData.data;
|
||||||
|
const channels = imageData.info.channels;
|
||||||
|
|
||||||
|
for (let i = 0; i < data.length; i += channels) {
|
||||||
|
const pixelIndex = Math.floor(i / channels);
|
||||||
|
const channel = Math.floor(pixelIndex / (targetHeight * targetWidth));
|
||||||
|
const posInChannel = pixelIndex % (targetHeight * targetWidth);
|
||||||
|
|
||||||
|
if (channel < 3) {
|
||||||
|
const y = Math.floor(posInChannel / targetWidth);
|
||||||
|
const x = posInChannel % targetWidth;
|
||||||
|
const inputIndex = channel * targetHeight * targetWidth + y * targetWidth + x;
|
||||||
|
|
||||||
|
if (inputIndex < inputData.length) {
|
||||||
|
inputData[inputIndex] = data[i] / 255.0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return new Tensor('float32', inputData, [1, 3, targetHeight, targetWidth]);
|
||||||
|
}
|
||||||
|
|
||||||
|
postprocessCls(outputs) {
|
||||||
|
const outputNames = this.clsSession.outputNames;
|
||||||
|
const clsOutput = outputs[outputNames[0]];
|
||||||
|
|
||||||
|
if (!clsOutput) return { clsResult: 0, clsConfidence: 1.0 };
|
||||||
|
|
||||||
|
const data = clsOutput.data;
|
||||||
|
|
||||||
|
let clsResult = 0;
|
||||||
|
let clsConfidence = data[0];
|
||||||
|
|
||||||
|
if (data.length >= 2 && data[1] > data[0]) {
|
||||||
|
clsResult = 180;
|
||||||
|
clsConfidence = data[1];
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log(`🧭 文本方向分类: ${clsResult}°, 置信度: ${clsConfidence.toFixed(4)}`);
|
||||||
|
return { clsResult, clsConfidence };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default TextDirectionClassifier;
|
||||||
163
server/utils/textPostProcessor.js
普通文件
163
server/utils/textPostProcessor.js
普通文件
@ -0,0 +1,163 @@
|
|||||||
|
// server/utils/textPostProcessor.js
|
||||||
|
class TextPostProcessor {
|
||||||
|
buildTextBlocks(recognitionResults) {
|
||||||
|
if (!recognitionResults || recognitionResults.length === 0) {
|
||||||
|
return [{
|
||||||
|
type: 'text',
|
||||||
|
content: '未识别到文本',
|
||||||
|
confidence: 0
|
||||||
|
}];
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log(`📊 开始构建文本块,共 ${recognitionResults.length} 个识别结果`);
|
||||||
|
|
||||||
|
const lines = this.groupTextIntoLines(recognitionResults);
|
||||||
|
const blocks = [];
|
||||||
|
|
||||||
|
for (const line of lines) {
|
||||||
|
const content = line.map(item => item.text).join('');
|
||||||
|
const avgConfidence = line.reduce((sum, item) => sum + item.confidence, 0) / line.length;
|
||||||
|
|
||||||
|
const type = this.classifyTextType(content);
|
||||||
|
|
||||||
|
blocks.push({
|
||||||
|
type,
|
||||||
|
content,
|
||||||
|
confidence: avgConfidence,
|
||||||
|
...(type === 'citation' && { number: this.extractCitationNumber(content) })
|
||||||
|
});
|
||||||
|
|
||||||
|
console.log(`📝 文本行: "${content}" (${type}, 置信度: ${avgConfidence.toFixed(4)})`);
|
||||||
|
}
|
||||||
|
|
||||||
|
const mergedBlocks = this.mergeShortTextBlocks(blocks);
|
||||||
|
console.log(`✅ 文本块构建完成: ${mergedBlocks.length} 个块`);
|
||||||
|
|
||||||
|
return mergedBlocks;
|
||||||
|
}
|
||||||
|
|
||||||
|
groupTextIntoLines(results) {
|
||||||
|
if (results.length === 0) return [];
|
||||||
|
|
||||||
|
const lines = [];
|
||||||
|
const sortedResults = [...results].sort((a, b) => a.box.y1 - b.box.y1);
|
||||||
|
|
||||||
|
let currentLine = [];
|
||||||
|
let currentY = -1;
|
||||||
|
const lineThreshold = 0.8 * this.calculateAverageHeight(results);
|
||||||
|
|
||||||
|
for (const result of sortedResults) {
|
||||||
|
if (currentY === -1 || Math.abs(result.box.y1 - currentY) < lineThreshold) {
|
||||||
|
currentLine.push(result);
|
||||||
|
if (currentY === -1) currentY = result.box.y1;
|
||||||
|
else currentY = (currentY + result.box.y1) / 2;
|
||||||
|
} else {
|
||||||
|
if (currentLine.length > 0) {
|
||||||
|
currentLine.sort((a, b) => a.box.x1 - b.box.x1);
|
||||||
|
lines.push(currentLine);
|
||||||
|
}
|
||||||
|
currentLine = [result];
|
||||||
|
currentY = result.box.y1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (currentLine.length > 0) {
|
||||||
|
currentLine.sort((a, b) => a.box.x1 - b.box.x1);
|
||||||
|
lines.push(currentLine);
|
||||||
|
}
|
||||||
|
|
||||||
|
return lines;
|
||||||
|
}
|
||||||
|
|
||||||
|
calculateAverageHeight(results) {
|
||||||
|
if (results.length === 0) return 0;
|
||||||
|
const totalHeight = results.reduce((sum, result) => {
|
||||||
|
const height = Math.max(result.box.y1, result.box.y2, result.box.y3, result.box.y4) -
|
||||||
|
Math.min(result.box.y1, result.box.y2, result.box.y3, result.box.y4);
|
||||||
|
return sum + height;
|
||||||
|
}, 0);
|
||||||
|
return totalHeight / results.length;
|
||||||
|
}
|
||||||
|
|
||||||
|
classifyTextType(text) {
|
||||||
|
if (this.isReference(text)) return 'reference';
|
||||||
|
if (this.isCitation(text)) return 'citation';
|
||||||
|
if (this.isImageMarker(text)) return 'image';
|
||||||
|
if (this.isTableMarker(text)) return 'table';
|
||||||
|
return 'text';
|
||||||
|
}
|
||||||
|
|
||||||
|
isReference(text) {
|
||||||
|
const refPatterns = [
|
||||||
|
/^参考文献/i,
|
||||||
|
/^references/i,
|
||||||
|
/^bibliography/i,
|
||||||
|
/^引用文献/i,
|
||||||
|
/^参考书目/i
|
||||||
|
];
|
||||||
|
return refPatterns.some(pattern => pattern.test(text));
|
||||||
|
}
|
||||||
|
|
||||||
|
isCitation(text) {
|
||||||
|
return /^\[\d+\]/.test(text);
|
||||||
|
}
|
||||||
|
|
||||||
|
extractCitationNumber(text) {
|
||||||
|
const match = text.match(/^\[(\d+)\]/);
|
||||||
|
return match ? parseInt(match[1]) : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
isImageMarker(text) {
|
||||||
|
const imagePatterns = [
|
||||||
|
/^图\s*\d+/i,
|
||||||
|
/^figure\s*\d+/i,
|
||||||
|
/^图片\s*\d+/i,
|
||||||
|
/^图表\s*\d+/i,
|
||||||
|
/^fig\.?\s*\d+/i
|
||||||
|
];
|
||||||
|
return imagePatterns.some(pattern => pattern.test(text));
|
||||||
|
}
|
||||||
|
|
||||||
|
isTableMarker(text) {
|
||||||
|
const tablePatterns = [
|
||||||
|
/^表\s*\d+/i,
|
||||||
|
/^table\s*\d+/i,
|
||||||
|
/^表格\s*\d+/i
|
||||||
|
];
|
||||||
|
return tablePatterns.some(pattern => pattern.test(text));
|
||||||
|
}
|
||||||
|
|
||||||
|
mergeShortTextBlocks(blocks) {
|
||||||
|
if (blocks.length <= 1) return blocks;
|
||||||
|
|
||||||
|
const mergedBlocks = [];
|
||||||
|
let currentBlock = { ...blocks[0] };
|
||||||
|
|
||||||
|
for (let i = 1; i < blocks.length; i++) {
|
||||||
|
const block = blocks[i];
|
||||||
|
|
||||||
|
// 放宽合并条件
|
||||||
|
if (currentBlock.type === 'text' &&
|
||||||
|
block.type === 'text' &&
|
||||||
|
currentBlock.content.length < 100) { // 增加长度限制
|
||||||
|
|
||||||
|
currentBlock.content += ' ' + block.content;
|
||||||
|
currentBlock.confidence = (currentBlock.confidence + block.confidence) / 2;
|
||||||
|
} else {
|
||||||
|
mergedBlocks.push(currentBlock);
|
||||||
|
currentBlock = { ...block };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
mergedBlocks.push(currentBlock);
|
||||||
|
return mergedBlocks;
|
||||||
|
}
|
||||||
|
|
||||||
|
calculateOverallConfidence(results) {
|
||||||
|
if (results.length === 0) return 0;
|
||||||
|
const total = results.reduce((sum, result) => sum + result.confidence, 0);
|
||||||
|
return total / results.length;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default TextPostProcessor;
|
||||||
370
server/utils/textRecognizer.js
普通文件
370
server/utils/textRecognizer.js
普通文件
@ -0,0 +1,370 @@
|
|||||||
|
// server/utils/textRecognizer.js
|
||||||
|
import { Tensor } from 'onnxruntime-node';
|
||||||
|
import sharp from 'sharp';
|
||||||
|
import fse from 'fs-extra';
|
||||||
|
import * as path from 'path';
|
||||||
|
|
||||||
|
class TextRecognizer {
|
||||||
|
constructor() {
|
||||||
|
this.recSession = null;
|
||||||
|
this.config = null;
|
||||||
|
this.characterSet = [];
|
||||||
|
this.debugDir = path.join(process.cwd(), 'temp', 'debug');
|
||||||
|
fse.ensureDirSync(this.debugDir);
|
||||||
|
}
|
||||||
|
|
||||||
|
initialize(recSession, config) {
|
||||||
|
this.recSession = recSession;
|
||||||
|
this.config = config;
|
||||||
|
}
|
||||||
|
|
||||||
|
async loadCharacterSet(keysPath) {
|
||||||
|
try {
|
||||||
|
const keysContent = await fse.readFile(keysPath, 'utf8');
|
||||||
|
this.characterSet = [];
|
||||||
|
const lines = keysContent.split('\n');
|
||||||
|
|
||||||
|
for (const line of lines) {
|
||||||
|
const trimmed = line.trim();
|
||||||
|
if (trimmed && !trimmed.startsWith('#')) {
|
||||||
|
for (const char of trimmed) {
|
||||||
|
if (char.trim() && !this.characterSet.includes(char)) {
|
||||||
|
this.characterSet.push(char);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.characterSet.length === 0) {
|
||||||
|
throw new Error('字符集文件为空或格式不正确');
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log(`✅ 字符集加载完成,共 ${this.characterSet.length} 个字符`);
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
console.error('❌ 加载字符集失败,使用默认字符集:', error.message);
|
||||||
|
this.characterSet = this.getDefaultCharacterSet();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
getDefaultCharacterSet() {
|
||||||
|
const defaultSet = [];
|
||||||
|
for (let i = 0; i <= 9; i++) defaultSet.push(i.toString());
|
||||||
|
for (let i = 97; i <= 122; i++) defaultSet.push(String.fromCharCode(i));
|
||||||
|
for (let i = 65; i <= 90; i++) defaultSet.push(String.fromCharCode(i));
|
||||||
|
defaultSet.push(...' ,。!?;:""()【】《》…—·'.split(''));
|
||||||
|
|
||||||
|
const commonChinese = '的一是不了在人有的我他这个们中来就时大地为子中你说道生国年着就那和要她出也得里后自以会家可下而过天去能对小多然于心学么之都好看起发当没成只如事把还用第样道想作种开美总从无情已面最女但现前些所同日手又行意动方期它头经长儿回位分爱老因很给名法间斯知世什两次使身者被高已亲其进此话常与活正感';
|
||||||
|
for (const char of commonChinese) {
|
||||||
|
defaultSet.push(char);
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log(`📝 使用默认字符集,共 ${defaultSet.length} 个字符`);
|
||||||
|
return defaultSet;
|
||||||
|
}
|
||||||
|
|
||||||
|
getCharacterSetSize() {
|
||||||
|
return this.characterSet.length;
|
||||||
|
}
|
||||||
|
|
||||||
|
async recognizeText(textRegionBuffer) {
|
||||||
|
console.log('🔠 === 开始文本识别流程 ===');
|
||||||
|
|
||||||
|
try {
|
||||||
|
console.log('📥 1. 准备识别输入...');
|
||||||
|
console.log(` - 输入图像大小: ${textRegionBuffer.length} 字节`);
|
||||||
|
|
||||||
|
const inputTensor = await this.prepareRecognitionInput(textRegionBuffer);
|
||||||
|
console.log('✅ 输入张量准备完成');
|
||||||
|
console.log(` - 张量形状: [${inputTensor.dims.join(', ')}]`);
|
||||||
|
console.log(` - 张量类型: ${inputTensor.type}`);
|
||||||
|
console.log(` - 数据长度: ${inputTensor.data.length}`);
|
||||||
|
|
||||||
|
// 数据验证
|
||||||
|
const tensorData = inputTensor.data;
|
||||||
|
let minVal = Infinity;
|
||||||
|
let maxVal = -Infinity;
|
||||||
|
let sumVal = 0;
|
||||||
|
let validCount = 0;
|
||||||
|
|
||||||
|
for (let i = 0; i < Math.min(100, tensorData.length); i++) {
|
||||||
|
const val = tensorData[i];
|
||||||
|
if (!isNaN(val) && isFinite(val)) {
|
||||||
|
minVal = Math.min(minVal, val);
|
||||||
|
maxVal = Math.max(maxVal, val);
|
||||||
|
sumVal += val;
|
||||||
|
validCount++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log(` - 数据范围: ${minVal.toFixed(4)} ~ ${maxVal.toFixed(4)}`);
|
||||||
|
console.log(` - 数据均值: ${(sumVal / validCount).toFixed(4)}`);
|
||||||
|
|
||||||
|
console.log('🧠 2. 执行模型推理...');
|
||||||
|
const startInference = Date.now();
|
||||||
|
const outputs = await this.recSession.run({ [this.recSession.inputNames[0]]: inputTensor });
|
||||||
|
const inferenceTime = Date.now() - startInference;
|
||||||
|
console.log(`✅ 模型推理完成 (${inferenceTime}ms)`);
|
||||||
|
|
||||||
|
const outputNames = this.recSession.outputNames;
|
||||||
|
console.log(` - 输出数量: ${outputNames.length}`);
|
||||||
|
|
||||||
|
outputNames.forEach((name, index) => {
|
||||||
|
const output = outputs[name];
|
||||||
|
if (output) {
|
||||||
|
console.log(` - 输出 ${index + 1} (${name}): 形状 [${output.dims.join(', ')}]`);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
console.log('🔍 3. 后处理识别结果...');
|
||||||
|
const result = this.postprocessRecognition(outputs);
|
||||||
|
console.log('✅ 后处理完成');
|
||||||
|
console.log(` - 识别文本: "${result.text}"`);
|
||||||
|
console.log(` - 置信度: ${result.confidence.toFixed(4)}`);
|
||||||
|
console.log(` - 文本长度: ${result.text.length} 字符`);
|
||||||
|
|
||||||
|
console.log('🎉 === 文本识别流程完成 ===');
|
||||||
|
return result;
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
console.error('❌ 文本识别失败:');
|
||||||
|
console.error(` - 错误信息: ${error.message}`);
|
||||||
|
return { text: '', confidence: 0 };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async prepareRecognitionInput(textRegionBuffer) {
|
||||||
|
console.log(' 📝 准备识别输入详情:');
|
||||||
|
|
||||||
|
try {
|
||||||
|
const targetHeight = 48;
|
||||||
|
const targetWidth = 320;
|
||||||
|
|
||||||
|
const metadata = await sharp(textRegionBuffer).metadata();
|
||||||
|
console.log(` - 原始图像尺寸: ${metadata.width}x${metadata.height}`);
|
||||||
|
|
||||||
|
// 保存原始图像用于调试
|
||||||
|
const originalPath = path.join(this.debugDir, `original-${Date.now()}.png`);
|
||||||
|
await fse.writeFile(originalPath, textRegionBuffer);
|
||||||
|
|
||||||
|
// 关键修复:正确的预处理流程
|
||||||
|
let processedBuffer = textRegionBuffer;
|
||||||
|
|
||||||
|
// 1. 分析图像特性
|
||||||
|
const stats = await sharp(processedBuffer)
|
||||||
|
.grayscale()
|
||||||
|
.stats();
|
||||||
|
const meanBrightness = stats.channels[0].mean;
|
||||||
|
const stdDev = stats.channels[0].stdev;
|
||||||
|
|
||||||
|
console.log(` - 图像统计: 均值=${meanBrightness.toFixed(1)}, 标准差=${stdDev.toFixed(1)}`);
|
||||||
|
|
||||||
|
// 2. 改进的预处理策略
|
||||||
|
if (meanBrightness > 200 && stdDev < 30) {
|
||||||
|
console.log(' - 检测到高亮度图像,进行对比度增强');
|
||||||
|
processedBuffer = await sharp(processedBuffer)
|
||||||
|
.linear(1.5, -50)
|
||||||
|
.normalize()
|
||||||
|
.grayscale()
|
||||||
|
.toBuffer();
|
||||||
|
} else if (meanBrightness < 80) {
|
||||||
|
console.log(' - 检测到低亮度图像,进行亮度调整');
|
||||||
|
processedBuffer = await sharp(processedBuffer)
|
||||||
|
.linear(1.2, 30)
|
||||||
|
.normalize()
|
||||||
|
.grayscale()
|
||||||
|
.toBuffer();
|
||||||
|
} else {
|
||||||
|
console.log(' - 使用标准化灰度处理');
|
||||||
|
processedBuffer = await sharp(processedBuffer)
|
||||||
|
.normalize()
|
||||||
|
.grayscale()
|
||||||
|
.toBuffer();
|
||||||
|
}
|
||||||
|
|
||||||
|
// 3. 保持宽高比的resize
|
||||||
|
const originalAspectRatio = metadata.width / metadata.height;
|
||||||
|
const targetAspectRatio = targetWidth / targetHeight;
|
||||||
|
|
||||||
|
let resizeWidth, resizeHeight;
|
||||||
|
|
||||||
|
if (originalAspectRatio > targetAspectRatio) {
|
||||||
|
// 宽度限制
|
||||||
|
resizeWidth = targetWidth;
|
||||||
|
resizeHeight = Math.round(targetWidth / originalAspectRatio);
|
||||||
|
} else {
|
||||||
|
// 高度限制
|
||||||
|
resizeHeight = targetHeight;
|
||||||
|
resizeWidth = Math.round(targetHeight * originalAspectRatio);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 确保尺寸有效
|
||||||
|
resizeWidth = Math.max(1, Math.min(resizeWidth, targetWidth));
|
||||||
|
resizeHeight = Math.max(1, Math.min(resizeHeight, targetHeight));
|
||||||
|
|
||||||
|
processedBuffer = await sharp(processedBuffer)
|
||||||
|
.resize(resizeWidth, resizeHeight, {
|
||||||
|
fit: 'contain',
|
||||||
|
background: { r: 255, g: 255, b: 255 }
|
||||||
|
})
|
||||||
|
.extend({
|
||||||
|
top: 0,
|
||||||
|
bottom: targetHeight - resizeHeight,
|
||||||
|
left: 0,
|
||||||
|
right: targetWidth - resizeWidth,
|
||||||
|
background: { r: 255, g: 255, b: 255 }
|
||||||
|
})
|
||||||
|
.png()
|
||||||
|
.toBuffer();
|
||||||
|
|
||||||
|
const processedMetadata = await sharp(processedBuffer).metadata();
|
||||||
|
console.log(` - 处理后尺寸: ${processedMetadata.width}x${processedMetadata.height}`);
|
||||||
|
|
||||||
|
// 保存预处理后的图像用于调试
|
||||||
|
const processedPath = path.join(this.debugDir, `processed-${Date.now()}.png`);
|
||||||
|
await fse.writeFile(processedPath, processedBuffer);
|
||||||
|
|
||||||
|
// 4. 转换为张量 - 关键修复:正确的归一化
|
||||||
|
console.log(' - 转换为张量数据...');
|
||||||
|
const imageData = await sharp(processedBuffer)
|
||||||
|
.ensureAlpha()
|
||||||
|
.raw()
|
||||||
|
.toBuffer({ resolveWithObject: true });
|
||||||
|
|
||||||
|
const inputData = new Float32Array(3 * targetHeight * targetWidth);
|
||||||
|
const data = imageData.data;
|
||||||
|
const channels = imageData.info.channels;
|
||||||
|
|
||||||
|
// 使用正确的归一化方法
|
||||||
|
for (let i = 0; i < data.length; i += channels) {
|
||||||
|
const pixelIndex = Math.floor(i / channels);
|
||||||
|
const y = Math.floor(pixelIndex / targetWidth);
|
||||||
|
const x = pixelIndex % targetWidth;
|
||||||
|
|
||||||
|
// 对每个位置,三个通道使用相同的灰度值
|
||||||
|
const grayValue = data[i] / 255.0;
|
||||||
|
|
||||||
|
for (let c = 0; c < 3; c++) {
|
||||||
|
const inputIndex = c * targetHeight * targetWidth + y * targetWidth + x;
|
||||||
|
if (inputIndex < inputData.length) {
|
||||||
|
inputData[inputIndex] = grayValue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log(` - 输入数据长度: ${inputData.length}`);
|
||||||
|
|
||||||
|
// 数据验证
|
||||||
|
let validCount = 0;
|
||||||
|
let sumValue = 0;
|
||||||
|
let minValue = Infinity;
|
||||||
|
let maxValue = -Infinity;
|
||||||
|
|
||||||
|
for (let i = 0; i < Math.min(100, inputData.length); i++) {
|
||||||
|
const val = inputData[i];
|
||||||
|
if (!isNaN(val) && isFinite(val)) {
|
||||||
|
validCount++;
|
||||||
|
sumValue += val;
|
||||||
|
minValue = Math.min(minValue, val);
|
||||||
|
maxValue = Math.max(maxValue, val);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log(` - 数据验证: 有效=${validCount}`);
|
||||||
|
console.log(` - 数据范围: ${minValue.toFixed(4)} ~ ${maxValue.toFixed(4)}`);
|
||||||
|
console.log(` - 数据均值: ${(sumValue / validCount).toFixed(4)}`);
|
||||||
|
|
||||||
|
return new Tensor('float32', inputData, [1, 3, targetHeight, targetWidth]);
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
console.error(` ❌ 准备输入失败: ${error.message}`);
|
||||||
|
// 返回有效的默认张量
|
||||||
|
return new Tensor('float32', new Float32Array(3 * 48 * 320).fill(0.5), [1, 3, 48, 320]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
postprocessRecognition(outputs) {
|
||||||
|
console.log(' 📝 后处理识别结果详情:');
|
||||||
|
|
||||||
|
try {
|
||||||
|
const outputNames = this.recSession.outputNames;
|
||||||
|
const recognitionOutput = outputs[outputNames[0]];
|
||||||
|
|
||||||
|
if (!recognitionOutput) {
|
||||||
|
console.log(' ❌ 识别输出为空');
|
||||||
|
return { text: '', confidence: 0 };
|
||||||
|
}
|
||||||
|
|
||||||
|
const data = recognitionOutput.data;
|
||||||
|
const [batch, seqLen, vocabSize] = recognitionOutput.dims;
|
||||||
|
|
||||||
|
console.log(` - 序列长度: ${seqLen}, 词汇表大小: ${vocabSize}`);
|
||||||
|
console.log(` - 输出数据总数: ${data.length}`);
|
||||||
|
console.log(` - 字符集大小: ${this.characterSet.length}`);
|
||||||
|
|
||||||
|
if (this.characterSet.length === 0) {
|
||||||
|
console.log(' ❌ 字符集为空');
|
||||||
|
return { text: '', confidence: 0 };
|
||||||
|
}
|
||||||
|
|
||||||
|
// 改进的CTC解码算法
|
||||||
|
let text = '';
|
||||||
|
let lastCharIndex = -1;
|
||||||
|
let confidenceSum = 0;
|
||||||
|
let charCount = 0;
|
||||||
|
|
||||||
|
// 降低置信度阈值,提高召回率
|
||||||
|
const confidenceThreshold = 0.05;
|
||||||
|
|
||||||
|
console.log(' - 处理每个时间步:');
|
||||||
|
for (let t = 0; t < seqLen; t++) {
|
||||||
|
let maxProb = -1;
|
||||||
|
let maxIndex = -1;
|
||||||
|
|
||||||
|
// 找到当前时间步的最大概率字符
|
||||||
|
for (let i = 0; i < vocabSize; i++) {
|
||||||
|
const prob = data[t * vocabSize + i];
|
||||||
|
if (prob > maxProb) {
|
||||||
|
maxProb = prob;
|
||||||
|
maxIndex = i;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 改进的解码逻辑
|
||||||
|
if (maxIndex > 0 && maxProb > confidenceThreshold) {
|
||||||
|
const char = this.characterSet[maxIndex - 1] || '';
|
||||||
|
|
||||||
|
// 放宽重复字符限制
|
||||||
|
if (maxIndex !== lastCharIndex || maxProb > 0.8) {
|
||||||
|
if (char && char.trim() !== '') {
|
||||||
|
text += char;
|
||||||
|
confidenceSum += maxProb;
|
||||||
|
charCount++;
|
||||||
|
console.log(` [位置 ${t}] 字符: "${char}", 置信度: ${maxProb.toFixed(4)}`);
|
||||||
|
}
|
||||||
|
lastCharIndex = maxIndex;
|
||||||
|
}
|
||||||
|
} else if (maxIndex === 0) {
|
||||||
|
// 空白符,重置lastCharIndex
|
||||||
|
lastCharIndex = -1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const avgConfidence = charCount > 0 ? confidenceSum / charCount : 0;
|
||||||
|
|
||||||
|
console.log(` - 识别结果: "${text}"`);
|
||||||
|
console.log(` - 字符数: ${charCount}, 平均置信度: ${avgConfidence.toFixed(4)}`);
|
||||||
|
|
||||||
|
return {
|
||||||
|
text: text,
|
||||||
|
confidence: avgConfidence
|
||||||
|
};
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
console.error(` ❌ 后处理失败: ${error.message}`);
|
||||||
|
return { text: '', confidence: 0 };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default TextRecognizer;
|
||||||
127
server/utils/textRegionCropper.js
普通文件
127
server/utils/textRegionCropper.js
普通文件
@ -0,0 +1,127 @@
|
|||||||
|
// server/utils/textRegionCropper.js
|
||||||
|
import sharp from 'sharp';
|
||||||
|
|
||||||
|
class TextRegionCropper {
|
||||||
|
constructor() {
|
||||||
|
// 可以在这里添加配置参数
|
||||||
|
}
|
||||||
|
|
||||||
|
async cropTextRegion(imageBuffer, box, regionIndex) {
|
||||||
|
try {
|
||||||
|
const metadata = await sharp(imageBuffer).metadata();
|
||||||
|
const imgWidth = metadata.width;
|
||||||
|
const imgHeight = metadata.height;
|
||||||
|
|
||||||
|
const left = Math.min(box.x1, box.x2, box.x3, box.x4);
|
||||||
|
const top = Math.min(box.y1, box.y2, box.y3, box.y4);
|
||||||
|
const right = Math.max(box.x1, box.x2, box.x3, box.x4);
|
||||||
|
const bottom = Math.max(box.y1, box.y2, box.y3, box.y4);
|
||||||
|
|
||||||
|
const originalWidth = right - left;
|
||||||
|
const originalHeight = bottom - top;
|
||||||
|
|
||||||
|
// 减少扩展,避免引入过多背景
|
||||||
|
const widthExpand = 10;
|
||||||
|
const heightExpand = 10;
|
||||||
|
|
||||||
|
const newWidth = originalWidth + widthExpand;
|
||||||
|
const newHeight = originalHeight + heightExpand;
|
||||||
|
|
||||||
|
const centerX = (left + right) / 2;
|
||||||
|
const centerY = (top + bottom) / 2;
|
||||||
|
|
||||||
|
const expandedLeft = Math.max(0, centerX - newWidth / 2);
|
||||||
|
const expandedTop = Math.max(0, centerY - newHeight / 2);
|
||||||
|
const expandedRight = Math.min(imgWidth - 1, centerX + newWidth / 2);
|
||||||
|
const expandedBottom = Math.min(imgHeight - 1, centerY + newHeight / 2);
|
||||||
|
|
||||||
|
const finalWidth = expandedRight - expandedLeft;
|
||||||
|
const finalHeight = expandedBottom - expandedTop;
|
||||||
|
|
||||||
|
if (finalWidth <= 0 || finalHeight <= 0) {
|
||||||
|
console.log(`❌ 区域 ${regionIndex}: 无效的裁剪区域`);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
let adjustedLeft = expandedLeft;
|
||||||
|
let adjustedTop = expandedTop;
|
||||||
|
let adjustedWidth = finalWidth;
|
||||||
|
let adjustedHeight = finalHeight;
|
||||||
|
|
||||||
|
if (expandedLeft < 0) {
|
||||||
|
adjustedLeft = 0;
|
||||||
|
adjustedWidth = expandedRight;
|
||||||
|
}
|
||||||
|
if (expandedTop < 0) {
|
||||||
|
adjustedTop = 0;
|
||||||
|
adjustedHeight = expandedBottom;
|
||||||
|
}
|
||||||
|
if (expandedRight > imgWidth) {
|
||||||
|
adjustedWidth = imgWidth - adjustedLeft;
|
||||||
|
}
|
||||||
|
if (expandedBottom > imgHeight) {
|
||||||
|
adjustedHeight = imgHeight - adjustedTop;
|
||||||
|
}
|
||||||
|
|
||||||
|
const croppedBuffer = await sharp(imageBuffer)
|
||||||
|
.extract({
|
||||||
|
left: Math.floor(adjustedLeft),
|
||||||
|
top: Math.floor(adjustedTop),
|
||||||
|
width: Math.floor(adjustedWidth),
|
||||||
|
height: Math.floor(adjustedHeight)
|
||||||
|
})
|
||||||
|
.png()
|
||||||
|
.toBuffer();
|
||||||
|
|
||||||
|
console.log(`✂️ 区域 ${regionIndex}: 裁剪 ${Math.floor(adjustedWidth)}x${Math.floor(adjustedHeight)}`);
|
||||||
|
|
||||||
|
return {
|
||||||
|
buffer: croppedBuffer,
|
||||||
|
boxInfo: {
|
||||||
|
original: { left, top, right, bottom, width: originalWidth, height: originalHeight },
|
||||||
|
expanded: {
|
||||||
|
left: adjustedLeft,
|
||||||
|
top: adjustedTop,
|
||||||
|
right: adjustedLeft + adjustedWidth,
|
||||||
|
bottom: adjustedTop + adjustedHeight,
|
||||||
|
width: adjustedWidth,
|
||||||
|
height: adjustedHeight
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
console.error(`❌ 区域 ${regionIndex}: 裁剪失败`, error);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async rotateImage(imageBuffer, degrees) {
|
||||||
|
return await sharp(imageBuffer)
|
||||||
|
.rotate(degrees)
|
||||||
|
.png()
|
||||||
|
.toBuffer();
|
||||||
|
}
|
||||||
|
|
||||||
|
calculateExpansion(originalWidth, originalHeight, expansionFactor = 1.2) {
|
||||||
|
return {
|
||||||
|
width: originalWidth * expansionFactor,
|
||||||
|
height: originalHeight * expansionFactor
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
validateCropRegion(left, top, width, height, imgWidth, imgHeight) {
|
||||||
|
if (width <= 0 || height <= 0) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (left < 0 || top < 0) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (left + width > imgWidth || top + height > imgHeight) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default TextRegionCropper;
|
||||||
@ -1,6 +1,6 @@
|
|||||||
const { app, BrowserWindow, Tray, Menu, ipcMain } = require('electron')
|
const { app, BrowserWindow, Tray, Menu, ipcMain } = require('electron')
|
||||||
const path = require('path')
|
const path = require('path')
|
||||||
const { startServer } = require('../../server/server.js')
|
const { startServer } = require('../../server/server.ts')
|
||||||
const net = require('net')
|
const net = require('net')
|
||||||
const dns = require('dns')
|
const dns = require('dns')
|
||||||
|
|
||||||
@ -103,8 +103,8 @@ async function determineApiService() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// 定期检查网络状态(每30秒)
|
// 定期检查网络状态(每30秒)
|
||||||
function startNetworkMonitoring() {
|
async function startNetworkMonitoring() {
|
||||||
setInterval(async () => {
|
// setInterval(async () => {
|
||||||
const serviceInfo = await determineApiService()
|
const serviceInfo = await determineApiService()
|
||||||
if (serviceInfo.baseUrl !== currentApiBaseUrl) {
|
if (serviceInfo.baseUrl !== currentApiBaseUrl) {
|
||||||
console.log('API服务地址变更:', currentApiBaseUrl, '->', serviceInfo.baseUrl)
|
console.log('API服务地址变更:', currentApiBaseUrl, '->', serviceInfo.baseUrl)
|
||||||
@ -115,7 +115,7 @@ function startNetworkMonitoring() {
|
|||||||
mainWindow.webContents.send('api-base-url-changed', currentApiBaseUrl)
|
mainWindow.webContents.send('api-base-url-changed', currentApiBaseUrl)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}, 30000) // 30秒检查一次
|
// }, 30000) // 30秒检查一次
|
||||||
}
|
}
|
||||||
|
|
||||||
function createWindow() {
|
function createWindow() {
|
||||||
|
|||||||
文件差异内容过多而无法显示
加载差异
@ -0,0 +1,223 @@
|
|||||||
|
<template>
|
||||||
|
<div class="file-panel">
|
||||||
|
<div class="panel-header">
|
||||||
|
<h3>源文件</h3>
|
||||||
|
<span class="file-count">共 {{ files.length }} 个文件</span>
|
||||||
|
</div>
|
||||||
|
<div class="file-list">
|
||||||
|
<div
|
||||||
|
v-for="file in files"
|
||||||
|
:key="file.id"
|
||||||
|
class="file-item"
|
||||||
|
:class="{ active: currentFile?.id === file.id }"
|
||||||
|
@click="$emit('file-selected', file)"
|
||||||
|
>
|
||||||
|
<div class="file-icon">
|
||||||
|
<img
|
||||||
|
v-if="isImage(file)"
|
||||||
|
:src="getFileThumbnail(file)"
|
||||||
|
:alt="file.originalName"
|
||||||
|
class="file-thumbnail"
|
||||||
|
@error="handleImageError"
|
||||||
|
/>
|
||||||
|
<div v-else class="file-type-icon" :class="getFileTypeClass(file)">
|
||||||
|
{{ getFileTypeIcon(file) }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="file-info">
|
||||||
|
<div class="file-name" :title="file.originalName">
|
||||||
|
{{ file.originalName }}
|
||||||
|
</div>
|
||||||
|
<div class="file-meta">
|
||||||
|
{{ formatFileSize(file.fileSize) }}
|
||||||
|
</div>
|
||||||
|
<div v-if="file.hasOcrResult" class="ocr-indicator">
|
||||||
|
✓ 已识别
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { FileRecord } from '../../types/ocr'
|
||||||
|
import apiManager from '../../utils/apiManager'
|
||||||
|
|
||||||
|
defineProps<{
|
||||||
|
files: FileRecord[]
|
||||||
|
currentFile: FileRecord | null
|
||||||
|
}>()
|
||||||
|
|
||||||
|
defineEmits<{
|
||||||
|
'file-selected': [file: FileRecord]
|
||||||
|
}>()
|
||||||
|
|
||||||
|
const isImage = (file: FileRecord): boolean => {
|
||||||
|
return file.mimeType.startsWith('image/')
|
||||||
|
}
|
||||||
|
|
||||||
|
const getFileTypeClass = (file: FileRecord): string => {
|
||||||
|
if (file.mimeType.includes('pdf')) return 'file-pdf'
|
||||||
|
if (file.mimeType.includes('word') || file.mimeType.includes('document')) return 'file-word'
|
||||||
|
if (file.mimeType.includes('excel') || file.mimeType.includes('spreadsheet')) return 'file-excel'
|
||||||
|
if (file.mimeType.includes('powerpoint') || file.mimeType.includes('presentation')) return 'file-ppt'
|
||||||
|
return 'file-other'
|
||||||
|
}
|
||||||
|
|
||||||
|
const getFileTypeIcon = (file: FileRecord): string => {
|
||||||
|
if (file.mimeType.includes('pdf')) return '📄'
|
||||||
|
if (file.mimeType.includes('word') || file.mimeType.includes('document')) return '📝'
|
||||||
|
if (file.mimeType.includes('excel') || file.mimeType.includes('spreadsheet')) return '📊'
|
||||||
|
if (file.mimeType.includes('powerpoint') || file.mimeType.includes('presentation')) return '📑'
|
||||||
|
return '📎'
|
||||||
|
}
|
||||||
|
|
||||||
|
const getFileThumbnail = (file: FileRecord): string => {
|
||||||
|
return apiManager.buildUrl(`/api/files/${file.id}/thumbnail`)
|
||||||
|
}
|
||||||
|
|
||||||
|
const formatFileSize = (bytes: number): string => {
|
||||||
|
const units = ['B', 'KB', 'MB', 'GB']
|
||||||
|
let size = bytes
|
||||||
|
let unitIndex = 0
|
||||||
|
while (size >= 1024 && unitIndex < units.length - 1) {
|
||||||
|
size /= 1024
|
||||||
|
unitIndex++
|
||||||
|
}
|
||||||
|
return `${size.toFixed(1)} ${units[unitIndex]}`
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleImageError = (event: Event): void => {
|
||||||
|
const img = event.target as HTMLImageElement
|
||||||
|
img.style.display = 'none'
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.file-panel {
|
||||||
|
background: white;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
min-height: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.panel-header {
|
||||||
|
padding: 1rem;
|
||||||
|
border-bottom: 1px solid #e9ecef;
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.panel-header h3 {
|
||||||
|
margin: 0;
|
||||||
|
font-size: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.file-count {
|
||||||
|
font-size: 0.875rem;
|
||||||
|
color: #6c757d;
|
||||||
|
}
|
||||||
|
|
||||||
|
.file-list {
|
||||||
|
flex: 1;
|
||||||
|
overflow-y: auto;
|
||||||
|
padding: 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.file-item {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
padding: 0.75rem;
|
||||||
|
border-radius: 6px;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: background-color 0.2s;
|
||||||
|
margin-bottom: 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.file-item:hover {
|
||||||
|
background-color: #f8f9fa;
|
||||||
|
}
|
||||||
|
|
||||||
|
.file-item.active {
|
||||||
|
background-color: #e7f3ff;
|
||||||
|
border: 1px solid #2196f3;
|
||||||
|
}
|
||||||
|
|
||||||
|
.file-icon {
|
||||||
|
margin-right: 0.75rem;
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.file-thumbnail {
|
||||||
|
width: 40px;
|
||||||
|
height: 40px;
|
||||||
|
object-fit: cover;
|
||||||
|
border-radius: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.file-type-icon {
|
||||||
|
width: 40px;
|
||||||
|
height: 40px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
border-radius: 4px;
|
||||||
|
font-size: 1.25rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.file-pdf {
|
||||||
|
background: #ffebee;
|
||||||
|
color: #f44336;
|
||||||
|
}
|
||||||
|
|
||||||
|
.file-word {
|
||||||
|
background: #e3f2fd;
|
||||||
|
color: #2196f3;
|
||||||
|
}
|
||||||
|
|
||||||
|
.file-excel {
|
||||||
|
background: #e8f5e8;
|
||||||
|
color: #4caf50;
|
||||||
|
}
|
||||||
|
|
||||||
|
.file-ppt {
|
||||||
|
background: #fce4ec;
|
||||||
|
color: #e91e63;
|
||||||
|
}
|
||||||
|
|
||||||
|
.file-other {
|
||||||
|
background: #f5f5f5;
|
||||||
|
color: #757575;
|
||||||
|
}
|
||||||
|
|
||||||
|
.file-info {
|
||||||
|
flex: 1;
|
||||||
|
min-width: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.file-name {
|
||||||
|
font-weight: 500;
|
||||||
|
margin-bottom: 0.25rem;
|
||||||
|
white-space: nowrap;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
}
|
||||||
|
|
||||||
|
.file-meta {
|
||||||
|
font-size: 0.75rem;
|
||||||
|
color: #6c757d;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ocr-indicator {
|
||||||
|
font-size: 0.7rem;
|
||||||
|
color: #28a745;
|
||||||
|
background: #e8f5e8;
|
||||||
|
padding: 0.1rem 0.4rem;
|
||||||
|
border-radius: 10px;
|
||||||
|
margin-top: 0.25rem;
|
||||||
|
display: inline-block;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@ -0,0 +1,303 @@
|
|||||||
|
<template>
|
||||||
|
<div class="image-panel">
|
||||||
|
<div class="panel-header">
|
||||||
|
<h3>{{ title }}</h3>
|
||||||
|
<div class="preview-controls" v-if="currentFile && isImage(currentFile) && imageUrl">
|
||||||
|
<button @click="zoomOut" class="zoom-btn">−</button>
|
||||||
|
<span class="zoom-level">{{ (zoomLevel * 100).toFixed(0) }}%</span>
|
||||||
|
<button @click="zoomIn" class="zoom-btn">+</button>
|
||||||
|
<button @click="resetZoom" class="reset-btn">重置</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="image-preview-container" ref="previewContainer">
|
||||||
|
<div class="image-preview"
|
||||||
|
@mousedown="startDrag($event)"
|
||||||
|
@mousemove="doDrag($event)"
|
||||||
|
@mouseup="stopDrag"
|
||||||
|
@mouseleave="stopDrag"
|
||||||
|
@wheel="handleWheel($event)">
|
||||||
|
<div v-if="currentFile && isImage(currentFile) && imageUrl" class="preview-content">
|
||||||
|
<div class="image-wrapper" ref="imageWrapper">
|
||||||
|
<img
|
||||||
|
:src="imageUrl"
|
||||||
|
:alt="currentFile.originalName"
|
||||||
|
class="preview-image"
|
||||||
|
:style="{
|
||||||
|
transform: `scale(${zoomLevel}) translate(${dragOffset.x}px, ${dragOffset.y}px)`,
|
||||||
|
cursor: isDragging ? 'grabbing' : 'grab'
|
||||||
|
}"
|
||||||
|
@load="handleImageLoad"
|
||||||
|
@error="handlePreviewError"
|
||||||
|
ref="previewImage"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div v-else-if="showProcessing" class="processing-indicator">
|
||||||
|
<div class="spinner"></div>
|
||||||
|
<p>预处理图片生成中...</p>
|
||||||
|
</div>
|
||||||
|
<div v-else class="no-image">
|
||||||
|
<p>{{ title === '预处理后预览' ? '识别后显示预处理图片' : '请选择图片文件' }}</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { ref, nextTick, computed } from 'vue'
|
||||||
|
import { FileRecord } from '../../types/ocr'
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
currentFile: FileRecord | null
|
||||||
|
imageUrl?: string
|
||||||
|
title?: string
|
||||||
|
showProcessing?: boolean
|
||||||
|
}
|
||||||
|
|
||||||
|
const props = withDefaults(defineProps<Props>(), {
|
||||||
|
title: '图片预览',
|
||||||
|
showProcessing: false
|
||||||
|
})
|
||||||
|
|
||||||
|
const zoomLevel = ref(1)
|
||||||
|
const dragOffset = ref({ x: 0, y: 0 })
|
||||||
|
const isDragging = ref(false)
|
||||||
|
const lastDragPos = ref({ x: 0, y: 0 })
|
||||||
|
const previewImage = ref<HTMLImageElement | null>(null)
|
||||||
|
const imageWrapper = ref<HTMLElement | null>(null)
|
||||||
|
|
||||||
|
const isImage = computed(() => (file: FileRecord): boolean => {
|
||||||
|
return file.mimeType.startsWith('image/')
|
||||||
|
})
|
||||||
|
|
||||||
|
const handleImageLoad = (): void => {
|
||||||
|
nextTick(() => {
|
||||||
|
resetZoom()
|
||||||
|
centerImage()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
const centerImage = (): void => {
|
||||||
|
if (!previewImage.value || !imageWrapper.value) return
|
||||||
|
|
||||||
|
const container = imageWrapper.value
|
||||||
|
const img = previewImage.value
|
||||||
|
|
||||||
|
const containerRect = container.getBoundingClientRect()
|
||||||
|
const imgRect = img.getBoundingClientRect()
|
||||||
|
|
||||||
|
if (imgRect.width < containerRect.width && imgRect.height < containerRect.height) {
|
||||||
|
dragOffset.value = { x: 0, y: 0 }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const zoomIn = (): void => {
|
||||||
|
zoomLevel.value = Math.min(zoomLevel.value + 0.1, 3)
|
||||||
|
constrainDragOffset()
|
||||||
|
}
|
||||||
|
|
||||||
|
const zoomOut = (): void => {
|
||||||
|
zoomLevel.value = Math.max(zoomLevel.value - 0.1, 0.5)
|
||||||
|
constrainDragOffset()
|
||||||
|
}
|
||||||
|
|
||||||
|
const resetZoom = (): void => {
|
||||||
|
zoomLevel.value = 1
|
||||||
|
dragOffset.value = { x: 0, y: 0 }
|
||||||
|
}
|
||||||
|
|
||||||
|
const constrainDragOffset = (): void => {
|
||||||
|
if (!previewImage.value || !imageWrapper.value) return
|
||||||
|
|
||||||
|
const container = imageWrapper.value
|
||||||
|
const img = previewImage.value
|
||||||
|
|
||||||
|
const containerRect = container.getBoundingClientRect()
|
||||||
|
const imgRect = {
|
||||||
|
width: img.naturalWidth * zoomLevel.value,
|
||||||
|
height: img.naturalHeight * zoomLevel.value
|
||||||
|
}
|
||||||
|
|
||||||
|
const maxX = Math.max(0, (imgRect.width - containerRect.width) / 2)
|
||||||
|
const maxY = Math.max(0, (imgRect.height - containerRect.height) / 2)
|
||||||
|
|
||||||
|
dragOffset.value.x = Math.max(-maxX, Math.min(maxX, dragOffset.value.x))
|
||||||
|
dragOffset.value.y = Math.max(-maxY, Math.min(maxY, dragOffset.value.y))
|
||||||
|
}
|
||||||
|
|
||||||
|
const startDrag = (event: MouseEvent): void => {
|
||||||
|
if (!props.currentFile || !isImage.value(props.currentFile)) return
|
||||||
|
|
||||||
|
isDragging.value = true
|
||||||
|
lastDragPos.value = { x: event.clientX, y: event.clientY }
|
||||||
|
event.preventDefault()
|
||||||
|
}
|
||||||
|
|
||||||
|
const doDrag = (event: MouseEvent): void => {
|
||||||
|
if (!isDragging.value) return
|
||||||
|
|
||||||
|
const deltaX = event.clientX - lastDragPos.value.x
|
||||||
|
const deltaY = event.clientY - lastDragPos.value.y
|
||||||
|
|
||||||
|
dragOffset.value.x += deltaX
|
||||||
|
dragOffset.value.y += deltaY
|
||||||
|
|
||||||
|
constrainDragOffset()
|
||||||
|
lastDragPos.value = { x: event.clientX, y: event.clientY }
|
||||||
|
}
|
||||||
|
|
||||||
|
const stopDrag = (): void => {
|
||||||
|
isDragging.value = false
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleWheel = (event: WheelEvent): void => {
|
||||||
|
if (!props.currentFile || !isImage.value(props.currentFile)) return
|
||||||
|
|
||||||
|
event.preventDefault()
|
||||||
|
if (event.deltaY < 0) {
|
||||||
|
zoomIn()
|
||||||
|
} else {
|
||||||
|
zoomOut()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const handlePreviewError = (): void => {
|
||||||
|
console.error('图片加载失败')
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.image-panel {
|
||||||
|
background: white;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
min-height: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.panel-header {
|
||||||
|
padding: 1rem;
|
||||||
|
border-bottom: 1px solid #e9ecef;
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.panel-header h3 {
|
||||||
|
margin: 0;
|
||||||
|
font-size: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.preview-controls {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.zoom-btn, .reset-btn {
|
||||||
|
background: #6c757d;
|
||||||
|
color: white;
|
||||||
|
border: none;
|
||||||
|
width: 30px;
|
||||||
|
height: 30px;
|
||||||
|
border-radius: 4px;
|
||||||
|
cursor: pointer;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
transition: background-color 0.2s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.zoom-btn:hover, .reset-btn:hover {
|
||||||
|
background: #5a6268;
|
||||||
|
}
|
||||||
|
|
||||||
|
.zoom-level {
|
||||||
|
font-size: 0.875rem;
|
||||||
|
min-width: 40px;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.image-preview-container {
|
||||||
|
flex: 1;
|
||||||
|
overflow: hidden;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
min-height: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.image-preview {
|
||||||
|
flex: 1;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
padding: 1rem;
|
||||||
|
background: #f8f9fa;
|
||||||
|
overflow: hidden;
|
||||||
|
cursor: grab;
|
||||||
|
position: relative;
|
||||||
|
min-height: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.image-preview:active {
|
||||||
|
cursor: grabbing;
|
||||||
|
}
|
||||||
|
|
||||||
|
.preview-content {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
.image-wrapper {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
overflow: hidden;
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
.preview-image {
|
||||||
|
max-width: 100%;
|
||||||
|
max-height: 100%;
|
||||||
|
object-fit: contain;
|
||||||
|
border: 1px solid #e9ecef;
|
||||||
|
border-radius: 4px;
|
||||||
|
transition: transform 0.1s ease;
|
||||||
|
transform-origin: center center;
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
.processing-indicator {
|
||||||
|
text-align: center;
|
||||||
|
color: #6c757d;
|
||||||
|
padding: 2rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.processing-indicator .spinner {
|
||||||
|
border: 3px solid #f3f3f3;
|
||||||
|
border-top: 3px solid #007bff;
|
||||||
|
border-radius: 50%;
|
||||||
|
width: 40px;
|
||||||
|
height: 40px;
|
||||||
|
animation: spin 1s linear infinite;
|
||||||
|
margin: 0 auto 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes spin {
|
||||||
|
0% { transform: rotate(0deg); }
|
||||||
|
100% { transform: rotate(360deg); }
|
||||||
|
}
|
||||||
|
|
||||||
|
.no-image {
|
||||||
|
text-align: center;
|
||||||
|
color: #6c757d;
|
||||||
|
padding: 2rem;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@ -0,0 +1,256 @@
|
|||||||
|
<template>
|
||||||
|
<div class="result-panel">
|
||||||
|
<div class="panel-header">
|
||||||
|
<h3>识别结果</h3>
|
||||||
|
<div class="result-actions">
|
||||||
|
<button class="btn btn-sm" @click="copyResult" :disabled="!ocrResult">
|
||||||
|
复制文本
|
||||||
|
</button>
|
||||||
|
<div v-if="ocrResult" class="confidence">
|
||||||
|
置信度: {{ (ocrResult.confidence * 100).toFixed(1) }}%
|
||||||
|
<span v-if="ocrResult.manuallyCorrected" class="corrected-badge">已校对</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="ocr-result" ref="resultContainer">
|
||||||
|
<div v-if="isProcessing" class="processing">
|
||||||
|
<div class="spinner"></div>
|
||||||
|
<p>正在识别中,请稍候...</p>
|
||||||
|
<p class="processing-time">已用时: {{ processingTime }}s</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div v-else-if="ocrResult" class="result-content">
|
||||||
|
<TextBlock
|
||||||
|
v-for="(block, index) in ocrResult.textBlocks"
|
||||||
|
:key="index"
|
||||||
|
:block="block"
|
||||||
|
:isEditing="isEditing"
|
||||||
|
@update:content="updateBlockContent(index, $event)"
|
||||||
|
@remove="removeBlock(index)"
|
||||||
|
@add-after="addBlockAfter(index)"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<div v-if="isEditing" class="add-block-section">
|
||||||
|
<button class="btn-add-block" @click="addNewBlock">
|
||||||
|
+ 添加文本块
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="result-summary">
|
||||||
|
<p>识别完成,共 {{ ocrResult.textBlocks.length }} 个文本块</p>
|
||||||
|
<p>处理时间: {{ (ocrResult.processingTime / 1000).toFixed(2) }} 秒</p>
|
||||||
|
<p v-if="ocrResult.manuallyCorrected" class="corrected-info">
|
||||||
|
✅ 此结果已人工校对
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div v-else class="no-result">
|
||||||
|
<p>识别结果将显示在这里</p>
|
||||||
|
<p class="hint">支持中英文混合识别,自动识别参考文献和图片标注</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { ref, nextTick } from 'vue'
|
||||||
|
import { OcrResult, OcrTextBlock } from '../../types/ocr'
|
||||||
|
import TextBlock from './TextBlock.vue'
|
||||||
|
|
||||||
|
defineProps<{
|
||||||
|
isProcessing: boolean
|
||||||
|
processingTime: number
|
||||||
|
ocrResult: OcrResult | null
|
||||||
|
isEditing: boolean
|
||||||
|
}>()
|
||||||
|
|
||||||
|
const emit = defineEmits<{
|
||||||
|
'update:ocrResult': [result: OcrResult]
|
||||||
|
'copy-result': []
|
||||||
|
'update-block': [index: number, content: string]
|
||||||
|
'remove-block': [index: number]
|
||||||
|
'add-block-after': [index: number]
|
||||||
|
'add-new-block': []
|
||||||
|
}>()
|
||||||
|
|
||||||
|
const resultContainer = ref<HTMLElement | null>(null)
|
||||||
|
|
||||||
|
const copyResult = (): void => {
|
||||||
|
emit('copy-result')
|
||||||
|
}
|
||||||
|
|
||||||
|
const updateBlockContent = (index: number, content: string): void => {
|
||||||
|
emit('update-block', index, content)
|
||||||
|
}
|
||||||
|
|
||||||
|
const removeBlock = (index: number): void => {
|
||||||
|
emit('remove-block', index)
|
||||||
|
}
|
||||||
|
|
||||||
|
const addBlockAfter = (index: number): void => {
|
||||||
|
emit('add-block-after', index)
|
||||||
|
}
|
||||||
|
|
||||||
|
const addNewBlock = (): void => {
|
||||||
|
emit('add-new-block')
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.result-panel {
|
||||||
|
background: white;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
min-height: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.panel-header {
|
||||||
|
padding: 1rem;
|
||||||
|
border-bottom: 1px solid #e9ecef;
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.panel-header h3 {
|
||||||
|
margin: 0;
|
||||||
|
font-size: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.result-actions {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn {
|
||||||
|
padding: 0.5rem 1rem;
|
||||||
|
border: none;
|
||||||
|
border-radius: 4px;
|
||||||
|
cursor: pointer;
|
||||||
|
font-size: 0.875rem;
|
||||||
|
transition: background-color 0.2s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn:disabled {
|
||||||
|
opacity: 0.6;
|
||||||
|
cursor: not-allowed;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-sm {
|
||||||
|
padding: 0.25rem 0.5rem;
|
||||||
|
font-size: 0.75rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.confidence {
|
||||||
|
font-size: 0.75rem;
|
||||||
|
color: #28a745;
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
|
||||||
|
.corrected-badge {
|
||||||
|
background: #28a745;
|
||||||
|
color: white;
|
||||||
|
padding: 0.1rem 0.4rem;
|
||||||
|
border-radius: 10px;
|
||||||
|
font-size: 0.7rem;
|
||||||
|
margin-left: 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ocr-result {
|
||||||
|
flex: 1;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
min-height: 0;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.result-content {
|
||||||
|
flex: 1;
|
||||||
|
overflow-y: auto;
|
||||||
|
padding: 0 1rem 1rem 1rem;
|
||||||
|
min-height: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.processing {
|
||||||
|
text-align: center;
|
||||||
|
padding: 2rem;
|
||||||
|
color: #6c757d;
|
||||||
|
flex: 1;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.spinner {
|
||||||
|
border: 3px solid #f3f3f3;
|
||||||
|
border-top: 3px solid #007bff;
|
||||||
|
border-radius: 50%;
|
||||||
|
width: 40px;
|
||||||
|
height: 40px;
|
||||||
|
animation: spin 1s linear infinite;
|
||||||
|
margin: 0 auto 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes spin {
|
||||||
|
0% { transform: rotate(0deg); }
|
||||||
|
100% { transform: rotate(360deg); }
|
||||||
|
}
|
||||||
|
|
||||||
|
.processing-time {
|
||||||
|
font-size: 0.875rem;
|
||||||
|
margin-top: 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.add-block-section {
|
||||||
|
margin: 1rem 0;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-add-block {
|
||||||
|
background: #17a2b8;
|
||||||
|
color: white;
|
||||||
|
border: none;
|
||||||
|
padding: 0.5rem 1rem;
|
||||||
|
border-radius: 4px;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: background-color 0.2s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-add-block:hover {
|
||||||
|
background: #138496;
|
||||||
|
}
|
||||||
|
|
||||||
|
.no-result {
|
||||||
|
text-align: center;
|
||||||
|
padding: 2rem;
|
||||||
|
color: #6c757d;
|
||||||
|
flex: 1;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.result-summary {
|
||||||
|
margin-top: 2rem;
|
||||||
|
padding-top: 1rem;
|
||||||
|
border-top: 1px solid #e9ecef;
|
||||||
|
font-size: 0.875rem;
|
||||||
|
color: #6c757d;
|
||||||
|
}
|
||||||
|
|
||||||
|
.corrected-info {
|
||||||
|
color: #28a745;
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hint {
|
||||||
|
font-size: 0.875rem;
|
||||||
|
margin-top: 0.5rem;
|
||||||
|
color: #6c757d;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@ -0,0 +1,215 @@
|
|||||||
|
<template>
|
||||||
|
<div class="text-block" :class="block.type">
|
||||||
|
<div v-if="block.type === 'text'" class="text-content">
|
||||||
|
<textarea
|
||||||
|
v-if="isEditing"
|
||||||
|
:value="block.content"
|
||||||
|
@input="handleInput($event)"
|
||||||
|
class="editable-text"
|
||||||
|
@blur="$emit('update:content', block.content)"
|
||||||
|
ref="textarea"
|
||||||
|
></textarea>
|
||||||
|
<div v-else class="static-text">
|
||||||
|
{{ block.content }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div v-else-if="block.type === 'image'" class="image-block">
|
||||||
|
<div class="image-caption">{{ block.content }}</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div v-else-if="block.type === 'reference'" class="reference-block">
|
||||||
|
<div class="reference-title">📚 参考文献</div>
|
||||||
|
<div class="reference-content">
|
||||||
|
<textarea
|
||||||
|
v-if="isEditing"
|
||||||
|
:value="block.content"
|
||||||
|
@input="handleInput($event)"
|
||||||
|
class="editable-text"
|
||||||
|
@blur="$emit('update:content', block.content)"
|
||||||
|
ref="textarea"
|
||||||
|
></textarea>
|
||||||
|
<span v-else>{{ block.content }}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div v-else-if="block.type === 'citation'" class="citation-block">
|
||||||
|
<div class="citation-content">
|
||||||
|
<span class="citation-marker">[{{ block.number }}]</span>
|
||||||
|
<textarea
|
||||||
|
v-if="isEditing"
|
||||||
|
:value="block.content"
|
||||||
|
@input="handleInput($event)"
|
||||||
|
class="editable-text"
|
||||||
|
@blur="$emit('update:content', block.content)"
|
||||||
|
ref="textarea"
|
||||||
|
></textarea>
|
||||||
|
<span v-else>{{ block.content }}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div v-if="isEditing" class="block-actions">
|
||||||
|
<button class="btn-delete" @click="$emit('remove')">删除</button>
|
||||||
|
<button class="btn-add" @click="$emit('add-after')">添加</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { ref, nextTick, watch } from 'vue'
|
||||||
|
import { OcrTextBlock } from '../../types/ocr'
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
block: OcrTextBlock
|
||||||
|
isEditing: boolean
|
||||||
|
}
|
||||||
|
|
||||||
|
defineProps<Props>()
|
||||||
|
|
||||||
|
const emit = defineEmits<{
|
||||||
|
'update:content': [content: string]
|
||||||
|
'remove': []
|
||||||
|
'add-after': []
|
||||||
|
}>()
|
||||||
|
|
||||||
|
const textarea = ref<HTMLTextAreaElement | null>(null)
|
||||||
|
|
||||||
|
const handleInput = (event: Event): void => {
|
||||||
|
const target = event.target as HTMLTextAreaElement
|
||||||
|
emit('update:content', target.value)
|
||||||
|
autoResizeTextarea(target)
|
||||||
|
}
|
||||||
|
|
||||||
|
const autoResizeTextarea = (textarea: HTMLTextAreaElement): void => {
|
||||||
|
textarea.style.height = 'auto'
|
||||||
|
const newHeight = Math.min(textarea.scrollHeight, 300)
|
||||||
|
textarea.style.height = `${newHeight}px`
|
||||||
|
}
|
||||||
|
|
||||||
|
// 当编辑模式改变时,调整textarea高度
|
||||||
|
watch(() => textarea.value, (newTextarea) => {
|
||||||
|
if (newTextarea) {
|
||||||
|
nextTick(() => {
|
||||||
|
autoResizeTextarea(newTextarea)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.text-block {
|
||||||
|
margin-bottom: 1rem;
|
||||||
|
padding: 0.5rem;
|
||||||
|
border-radius: 4px;
|
||||||
|
transition: background-color 0.2s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.text-block:hover {
|
||||||
|
background-color: #f8f9fa;
|
||||||
|
}
|
||||||
|
|
||||||
|
.text-content {
|
||||||
|
white-space: pre-wrap;
|
||||||
|
word-wrap: break-word;
|
||||||
|
}
|
||||||
|
|
||||||
|
.editable-text {
|
||||||
|
width: 100%;
|
||||||
|
min-height: 60px;
|
||||||
|
max-height: 300px;
|
||||||
|
padding: 0.5rem;
|
||||||
|
border: 1px solid #007bff;
|
||||||
|
border-radius: 4px;
|
||||||
|
font-family: inherit;
|
||||||
|
font-size: inherit;
|
||||||
|
resize: vertical;
|
||||||
|
transition: border-color 0.2s;
|
||||||
|
overflow-y: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.editable-text:focus {
|
||||||
|
outline: none;
|
||||||
|
border-color: #0056b3;
|
||||||
|
}
|
||||||
|
|
||||||
|
.static-text {
|
||||||
|
white-space: pre-wrap;
|
||||||
|
word-wrap: break-word;
|
||||||
|
}
|
||||||
|
|
||||||
|
.image-block {
|
||||||
|
text-align: center;
|
||||||
|
margin: 1rem 0;
|
||||||
|
padding: 0.5rem;
|
||||||
|
background: #f8f9fa;
|
||||||
|
border-radius: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.image-caption {
|
||||||
|
font-size: 0.875rem;
|
||||||
|
color: #6c757d;
|
||||||
|
}
|
||||||
|
|
||||||
|
.reference-block {
|
||||||
|
background: #fff3cd;
|
||||||
|
border: 1px solid #ffeaa7;
|
||||||
|
border-radius: 4px;
|
||||||
|
padding: 1rem;
|
||||||
|
margin: 1rem 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.reference-title {
|
||||||
|
font-weight: bold;
|
||||||
|
margin-bottom: 0.5rem;
|
||||||
|
color: #856404;
|
||||||
|
}
|
||||||
|
|
||||||
|
.citation-block {
|
||||||
|
background: #e7f3ff;
|
||||||
|
border-left: 3px solid #2196f3;
|
||||||
|
padding: 0.5rem;
|
||||||
|
margin: 0.5rem 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.citation-marker {
|
||||||
|
font-weight: bold;
|
||||||
|
color: #2196f3;
|
||||||
|
margin-right: 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.block-actions {
|
||||||
|
display: flex;
|
||||||
|
gap: 0.5rem;
|
||||||
|
margin-top: 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-delete {
|
||||||
|
background: #dc3545;
|
||||||
|
color: white;
|
||||||
|
border: none;
|
||||||
|
padding: 0.25rem 0.5rem;
|
||||||
|
border-radius: 3px;
|
||||||
|
font-size: 0.75rem;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: background-color 0.2s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-delete:hover {
|
||||||
|
background: #c82333;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-add {
|
||||||
|
background: #28a745;
|
||||||
|
color: white;
|
||||||
|
border: none;
|
||||||
|
padding: 0.25rem 0.5rem;
|
||||||
|
border-radius: 3px;
|
||||||
|
font-size: 0.75rem;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: background-color 0.2s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-add:hover {
|
||||||
|
background: #218838;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
60
src/renderer/types/ocr.ts
普通文件
60
src/renderer/types/ocr.ts
普通文件
@ -0,0 +1,60 @@
|
|||||||
|
export interface FileRecord {
|
||||||
|
id: number
|
||||||
|
originalName: string
|
||||||
|
fileName: string
|
||||||
|
filePath: string
|
||||||
|
fileSize: number
|
||||||
|
mimeType: string
|
||||||
|
md5: string
|
||||||
|
createdAt: string
|
||||||
|
updatedAt: string
|
||||||
|
hasOcrResult?: boolean
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface OcrTextBlock {
|
||||||
|
type: 'text' | 'image' | 'reference' | 'citation'
|
||||||
|
content: string
|
||||||
|
number?: number
|
||||||
|
confidence?: number
|
||||||
|
bbox?: { x: number; y: number; width: number; height: number }
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ApiResponse<T> {
|
||||||
|
success: boolean
|
||||||
|
data?: T
|
||||||
|
pagination?: any
|
||||||
|
error?: string
|
||||||
|
message?: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface OcrConfig {
|
||||||
|
language: string;
|
||||||
|
preprocessing: {
|
||||||
|
enable: boolean;
|
||||||
|
denoise: boolean;
|
||||||
|
sharpen: boolean;
|
||||||
|
binarize: boolean;
|
||||||
|
deskew: boolean;
|
||||||
|
enhance_contrast: boolean;
|
||||||
|
};
|
||||||
|
detLimitSideLen?: number;
|
||||||
|
detThresh?: number;
|
||||||
|
detBoxThresh?: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface OcrResult {
|
||||||
|
textBlocks: OcrTextBlock[];
|
||||||
|
confidence: number;
|
||||||
|
processingTime: number;
|
||||||
|
isOffline: boolean;
|
||||||
|
imagePath?: string;
|
||||||
|
totalPages: number;
|
||||||
|
rawText: string;
|
||||||
|
imageInfo?: {
|
||||||
|
width: number;
|
||||||
|
height: number;
|
||||||
|
format: string;
|
||||||
|
processed: boolean;
|
||||||
|
};
|
||||||
|
manuallyCorrected?: boolean;
|
||||||
|
}
|
||||||
656
yarn.lock
656
yarn.lock
@ -370,6 +370,286 @@
|
|||||||
resolved "https://registry.yarnpkg.com/@img/sharp-win32-x64/-/sharp-win32-x64-0.34.5.tgz#a81ffb00e69267cd0a1d626eaedb8a8430b2b2f8"
|
resolved "https://registry.yarnpkg.com/@img/sharp-win32-x64/-/sharp-win32-x64-0.34.5.tgz#a81ffb00e69267cd0a1d626eaedb8a8430b2b2f8"
|
||||||
integrity sha512-+29YMsqY2/9eFEiW93eqWnuLcWcufowXewwSNIT6UwZdUUCrM3oFjMWH/Z6/TMmb4hlFenmfAVbpWeup2jryCw==
|
integrity sha512-+29YMsqY2/9eFEiW93eqWnuLcWcufowXewwSNIT6UwZdUUCrM3oFjMWH/Z6/TMmb4hlFenmfAVbpWeup2jryCw==
|
||||||
|
|
||||||
|
"@isaacs/fs-minipass@^4.0.0":
|
||||||
|
version "4.0.1"
|
||||||
|
resolved "https://registry.yarnpkg.com/@isaacs/fs-minipass/-/fs-minipass-4.0.1.tgz#2d59ae3ab4b38fb4270bfa23d30f8e2e86c7fe32"
|
||||||
|
integrity sha512-wgm9Ehl2jpeqP3zw/7mo3kRHFp5MEDhqAdwy1fTGkHAwnkGOVsgpvQhL8B5n1qlb01jV3n/bI0ZfZp5lWA1k4w==
|
||||||
|
dependencies:
|
||||||
|
minipass "^7.0.4"
|
||||||
|
|
||||||
|
"@jimp/core@1.6.0":
|
||||||
|
version "1.6.0"
|
||||||
|
resolved "https://registry.yarnpkg.com/@jimp/core/-/core-1.6.0.tgz#3ef241bf02f40431bb382aea665e5187a2c05eef"
|
||||||
|
integrity sha512-EQQlKU3s9QfdJqiSrZWNTxBs3rKXgO2W+GxNXDtwchF3a4IqxDheFX1ti+Env9hdJXDiYLp2jTRjlxhPthsk8w==
|
||||||
|
dependencies:
|
||||||
|
"@jimp/file-ops" "1.6.0"
|
||||||
|
"@jimp/types" "1.6.0"
|
||||||
|
"@jimp/utils" "1.6.0"
|
||||||
|
await-to-js "^3.0.0"
|
||||||
|
exif-parser "^0.1.12"
|
||||||
|
file-type "^16.0.0"
|
||||||
|
mime "3"
|
||||||
|
|
||||||
|
"@jimp/diff@1.6.0":
|
||||||
|
version "1.6.0"
|
||||||
|
resolved "https://registry.yarnpkg.com/@jimp/diff/-/diff-1.6.0.tgz#f8d058bfad64751c5e5c135499d1a784f797c5c8"
|
||||||
|
integrity sha512-+yUAQ5gvRC5D1WHYxjBHZI7JBRusGGSLf8AmPRPCenTzh4PA+wZ1xv2+cYqQwTfQHU5tXYOhA0xDytfHUf1Zyw==
|
||||||
|
dependencies:
|
||||||
|
"@jimp/plugin-resize" "1.6.0"
|
||||||
|
"@jimp/types" "1.6.0"
|
||||||
|
"@jimp/utils" "1.6.0"
|
||||||
|
pixelmatch "^5.3.0"
|
||||||
|
|
||||||
|
"@jimp/file-ops@1.6.0":
|
||||||
|
version "1.6.0"
|
||||||
|
resolved "https://registry.yarnpkg.com/@jimp/file-ops/-/file-ops-1.6.0.tgz#ae9c6aa65b2c9a5a16515a8fdf83b55f51100087"
|
||||||
|
integrity sha512-Dx/bVDmgnRe1AlniRpCKrGRm5YvGmUwbDzt+MAkgmLGf+jvBT75hmMEZ003n9HQI/aPnm/YKnXjg/hOpzNCpHQ==
|
||||||
|
|
||||||
|
"@jimp/js-bmp@1.6.0":
|
||||||
|
version "1.6.0"
|
||||||
|
resolved "https://registry.yarnpkg.com/@jimp/js-bmp/-/js-bmp-1.6.0.tgz#ff7c4306e764745063e249ee926d0dd807924abf"
|
||||||
|
integrity sha512-FU6Q5PC/e3yzLyBDXupR3SnL3htU7S3KEs4e6rjDP6gNEOXRFsWs6YD3hXuXd50jd8ummy+q2WSwuGkr8wi+Gw==
|
||||||
|
dependencies:
|
||||||
|
"@jimp/core" "1.6.0"
|
||||||
|
"@jimp/types" "1.6.0"
|
||||||
|
"@jimp/utils" "1.6.0"
|
||||||
|
bmp-ts "^1.0.9"
|
||||||
|
|
||||||
|
"@jimp/js-gif@1.6.0":
|
||||||
|
version "1.6.0"
|
||||||
|
resolved "https://registry.yarnpkg.com/@jimp/js-gif/-/js-gif-1.6.0.tgz#0efa5d83317a89d6eda936e2ae1df2b7d122a38d"
|
||||||
|
integrity sha512-N9CZPHOrJTsAUoWkWZstLPpwT5AwJ0wge+47+ix3++SdSL/H2QzyMqxbcDYNFe4MoI5MIhATfb0/dl/wmX221g==
|
||||||
|
dependencies:
|
||||||
|
"@jimp/core" "1.6.0"
|
||||||
|
"@jimp/types" "1.6.0"
|
||||||
|
gifwrap "^0.10.1"
|
||||||
|
omggif "^1.0.10"
|
||||||
|
|
||||||
|
"@jimp/js-jpeg@1.6.0":
|
||||||
|
version "1.6.0"
|
||||||
|
resolved "https://registry.yarnpkg.com/@jimp/js-jpeg/-/js-jpeg-1.6.0.tgz#e47da6758346548079f0ac8ff215d0d9d1ec435e"
|
||||||
|
integrity sha512-6vgFDqeusblf5Pok6B2DUiMXplH8RhIKAryj1yn+007SIAQ0khM1Uptxmpku/0MfbClx2r7pnJv9gWpAEJdMVA==
|
||||||
|
dependencies:
|
||||||
|
"@jimp/core" "1.6.0"
|
||||||
|
"@jimp/types" "1.6.0"
|
||||||
|
jpeg-js "^0.4.4"
|
||||||
|
|
||||||
|
"@jimp/js-png@1.6.0":
|
||||||
|
version "1.6.0"
|
||||||
|
resolved "https://registry.yarnpkg.com/@jimp/js-png/-/js-png-1.6.0.tgz#c857adfdbfcb7107a6511c3b2939ffbad0fefedc"
|
||||||
|
integrity sha512-AbQHScy3hDDgMRNfG0tPjL88AV6qKAILGReIa3ATpW5QFjBKpisvUaOqhzJ7Reic1oawx3Riyv152gaPfqsBVg==
|
||||||
|
dependencies:
|
||||||
|
"@jimp/core" "1.6.0"
|
||||||
|
"@jimp/types" "1.6.0"
|
||||||
|
pngjs "^7.0.0"
|
||||||
|
|
||||||
|
"@jimp/js-tiff@1.6.0":
|
||||||
|
version "1.6.0"
|
||||||
|
resolved "https://registry.yarnpkg.com/@jimp/js-tiff/-/js-tiff-1.6.0.tgz#f18fa3d59f52fda339acfdcadbe7363bed912e81"
|
||||||
|
integrity sha512-zhReR8/7KO+adijj3h0ZQUOiun3mXUv79zYEAKvE0O+rP7EhgtKvWJOZfRzdZSNv0Pu1rKtgM72qgtwe2tFvyw==
|
||||||
|
dependencies:
|
||||||
|
"@jimp/core" "1.6.0"
|
||||||
|
"@jimp/types" "1.6.0"
|
||||||
|
utif2 "^4.1.0"
|
||||||
|
|
||||||
|
"@jimp/plugin-blit@1.6.0":
|
||||||
|
version "1.6.0"
|
||||||
|
resolved "https://registry.yarnpkg.com/@jimp/plugin-blit/-/plugin-blit-1.6.0.tgz#fed35aefbb5757599a4299a9ff6c06cc3466f46f"
|
||||||
|
integrity sha512-M+uRWl1csi7qilnSK8uxK4RJMSuVeBiO1AY0+7APnfUbQNZm6hCe0CCFv1Iyw1D/Dhb8ph8fQgm5mwM0eSxgVA==
|
||||||
|
dependencies:
|
||||||
|
"@jimp/types" "1.6.0"
|
||||||
|
"@jimp/utils" "1.6.0"
|
||||||
|
zod "^3.23.8"
|
||||||
|
|
||||||
|
"@jimp/plugin-blur@1.6.0":
|
||||||
|
version "1.6.0"
|
||||||
|
resolved "https://registry.yarnpkg.com/@jimp/plugin-blur/-/plugin-blur-1.6.0.tgz#781b3be9de2744e5eb6ab86ec05ee7d2ce5092e8"
|
||||||
|
integrity sha512-zrM7iic1OTwUCb0g/rN5y+UnmdEsT3IfuCXCJJNs8SZzP0MkZ1eTvuwK9ZidCuMo4+J3xkzCidRwYXB5CyGZTw==
|
||||||
|
dependencies:
|
||||||
|
"@jimp/core" "1.6.0"
|
||||||
|
"@jimp/utils" "1.6.0"
|
||||||
|
|
||||||
|
"@jimp/plugin-circle@1.6.0":
|
||||||
|
version "1.6.0"
|
||||||
|
resolved "https://registry.yarnpkg.com/@jimp/plugin-circle/-/plugin-circle-1.6.0.tgz#2314dc7955068cb4a000de4eceb02890eb131c88"
|
||||||
|
integrity sha512-xt1Gp+LtdMKAXfDp3HNaG30SPZW6AQ7dtAtTnoRKorRi+5yCJjKqXRgkewS5bvj8DEh87Ko1ydJfzqS3P2tdWw==
|
||||||
|
dependencies:
|
||||||
|
"@jimp/types" "1.6.0"
|
||||||
|
zod "^3.23.8"
|
||||||
|
|
||||||
|
"@jimp/plugin-color@1.6.0":
|
||||||
|
version "1.6.0"
|
||||||
|
resolved "https://registry.yarnpkg.com/@jimp/plugin-color/-/plugin-color-1.6.0.tgz#927c83ee932070ad285266840728c21ac39bf27b"
|
||||||
|
integrity sha512-J5q8IVCpkBsxIXM+45XOXTrsyfblyMZg3a9eAo0P7VPH4+CrvyNQwaYatbAIamSIN1YzxmO3DkIZXzRjFSz1SA==
|
||||||
|
dependencies:
|
||||||
|
"@jimp/core" "1.6.0"
|
||||||
|
"@jimp/types" "1.6.0"
|
||||||
|
"@jimp/utils" "1.6.0"
|
||||||
|
tinycolor2 "^1.6.0"
|
||||||
|
zod "^3.23.8"
|
||||||
|
|
||||||
|
"@jimp/plugin-contain@1.6.0":
|
||||||
|
version "1.6.0"
|
||||||
|
resolved "https://registry.yarnpkg.com/@jimp/plugin-contain/-/plugin-contain-1.6.0.tgz#d08900ecf85ac564a6f9f3fc0d61cc8d5e43626e"
|
||||||
|
integrity sha512-oN/n+Vdq/Qg9bB4yOBOxtY9IPAtEfES8J1n9Ddx+XhGBYT1/QTU/JYkGaAkIGoPnyYvmLEDqMz2SGihqlpqfzQ==
|
||||||
|
dependencies:
|
||||||
|
"@jimp/core" "1.6.0"
|
||||||
|
"@jimp/plugin-blit" "1.6.0"
|
||||||
|
"@jimp/plugin-resize" "1.6.0"
|
||||||
|
"@jimp/types" "1.6.0"
|
||||||
|
"@jimp/utils" "1.6.0"
|
||||||
|
zod "^3.23.8"
|
||||||
|
|
||||||
|
"@jimp/plugin-cover@1.6.0":
|
||||||
|
version "1.6.0"
|
||||||
|
resolved "https://registry.yarnpkg.com/@jimp/plugin-cover/-/plugin-cover-1.6.0.tgz#07ffb2f3d6ac53616c66f1131cd66ced17e3ca3e"
|
||||||
|
integrity sha512-Iow0h6yqSC269YUJ8HC3Q/MpCi2V55sMlbkkTTx4zPvd8mWZlC0ykrNDeAy9IJegrQ7v5E99rJwmQu25lygKLA==
|
||||||
|
dependencies:
|
||||||
|
"@jimp/core" "1.6.0"
|
||||||
|
"@jimp/plugin-crop" "1.6.0"
|
||||||
|
"@jimp/plugin-resize" "1.6.0"
|
||||||
|
"@jimp/types" "1.6.0"
|
||||||
|
zod "^3.23.8"
|
||||||
|
|
||||||
|
"@jimp/plugin-crop@1.6.0":
|
||||||
|
version "1.6.0"
|
||||||
|
resolved "https://registry.yarnpkg.com/@jimp/plugin-crop/-/plugin-crop-1.6.0.tgz#59f2b20869330fd768d1743d845b8ba9ed9bc52a"
|
||||||
|
integrity sha512-KqZkEhvs+21USdySCUDI+GFa393eDIzbi1smBqkUPTE+pRwSWMAf01D5OC3ZWB+xZsNla93BDS9iCkLHA8wang==
|
||||||
|
dependencies:
|
||||||
|
"@jimp/core" "1.6.0"
|
||||||
|
"@jimp/types" "1.6.0"
|
||||||
|
"@jimp/utils" "1.6.0"
|
||||||
|
zod "^3.23.8"
|
||||||
|
|
||||||
|
"@jimp/plugin-displace@1.6.0":
|
||||||
|
version "1.6.0"
|
||||||
|
resolved "https://registry.yarnpkg.com/@jimp/plugin-displace/-/plugin-displace-1.6.0.tgz#41b3257a6c0f64c749c29c1a2e64ba7df31a7a25"
|
||||||
|
integrity sha512-4Y10X9qwr5F+Bo5ME356XSACEF55485j5nGdiyJ9hYzjQP9nGgxNJaZ4SAOqpd+k5sFaIeD7SQ0Occ26uIng5Q==
|
||||||
|
dependencies:
|
||||||
|
"@jimp/types" "1.6.0"
|
||||||
|
"@jimp/utils" "1.6.0"
|
||||||
|
zod "^3.23.8"
|
||||||
|
|
||||||
|
"@jimp/plugin-dither@1.6.0":
|
||||||
|
version "1.6.0"
|
||||||
|
resolved "https://registry.yarnpkg.com/@jimp/plugin-dither/-/plugin-dither-1.6.0.tgz#10c17070dcbec565904f11b7986e90ae20850b6f"
|
||||||
|
integrity sha512-600d1RxY0pKwgyU0tgMahLNKsqEcxGdbgXadCiVCoGd6V6glyCvkNrnnwC0n5aJ56Htkj88PToSdF88tNVZEEQ==
|
||||||
|
dependencies:
|
||||||
|
"@jimp/types" "1.6.0"
|
||||||
|
|
||||||
|
"@jimp/plugin-fisheye@1.6.0":
|
||||||
|
version "1.6.0"
|
||||||
|
resolved "https://registry.yarnpkg.com/@jimp/plugin-fisheye/-/plugin-fisheye-1.6.0.tgz#2831c0060598b27bf004bf8a70adfeec003d4fcc"
|
||||||
|
integrity sha512-E5QHKWSCBFtpgZarlmN3Q6+rTQxjirFqo44ohoTjzYVrDI6B6beXNnPIThJgPr0Y9GwfzgyarKvQuQuqCnnfbA==
|
||||||
|
dependencies:
|
||||||
|
"@jimp/types" "1.6.0"
|
||||||
|
"@jimp/utils" "1.6.0"
|
||||||
|
zod "^3.23.8"
|
||||||
|
|
||||||
|
"@jimp/plugin-flip@1.6.0":
|
||||||
|
version "1.6.0"
|
||||||
|
resolved "https://registry.yarnpkg.com/@jimp/plugin-flip/-/plugin-flip-1.6.0.tgz#75c87bdb0f0ca9db44b320cc9671aa201e52b5c3"
|
||||||
|
integrity sha512-/+rJVDuBIVOgwoyVkBjUFHtP+wmW0r+r5OQ2GpatQofToPVbJw1DdYWXlwviSx7hvixTWLKVgRWQ5Dw862emDg==
|
||||||
|
dependencies:
|
||||||
|
"@jimp/types" "1.6.0"
|
||||||
|
zod "^3.23.8"
|
||||||
|
|
||||||
|
"@jimp/plugin-hash@1.6.0":
|
||||||
|
version "1.6.0"
|
||||||
|
resolved "https://registry.yarnpkg.com/@jimp/plugin-hash/-/plugin-hash-1.6.0.tgz#8de89dfbbb6be671f9cdb2b59816acf3f07c4298"
|
||||||
|
integrity sha512-wWzl0kTpDJgYVbZdajTf+4NBSKvmI3bRI8q6EH9CVeIHps9VWVsUvEyb7rpbcwVLWYuzDtP2R0lTT6WeBNQH9Q==
|
||||||
|
dependencies:
|
||||||
|
"@jimp/core" "1.6.0"
|
||||||
|
"@jimp/js-bmp" "1.6.0"
|
||||||
|
"@jimp/js-jpeg" "1.6.0"
|
||||||
|
"@jimp/js-png" "1.6.0"
|
||||||
|
"@jimp/js-tiff" "1.6.0"
|
||||||
|
"@jimp/plugin-color" "1.6.0"
|
||||||
|
"@jimp/plugin-resize" "1.6.0"
|
||||||
|
"@jimp/types" "1.6.0"
|
||||||
|
"@jimp/utils" "1.6.0"
|
||||||
|
any-base "^1.1.0"
|
||||||
|
|
||||||
|
"@jimp/plugin-mask@1.6.0":
|
||||||
|
version "1.6.0"
|
||||||
|
resolved "https://registry.yarnpkg.com/@jimp/plugin-mask/-/plugin-mask-1.6.0.tgz#2b5a437e5d9a9906dcabb7a7baf4d5cd7d2361b1"
|
||||||
|
integrity sha512-Cwy7ExSJMZszvkad8NV8o/Z92X2kFUFM8mcDAhNVxU0Q6tA0op2UKRJY51eoK8r6eds/qak3FQkXakvNabdLnA==
|
||||||
|
dependencies:
|
||||||
|
"@jimp/types" "1.6.0"
|
||||||
|
zod "^3.23.8"
|
||||||
|
|
||||||
|
"@jimp/plugin-print@1.6.0":
|
||||||
|
version "1.6.0"
|
||||||
|
resolved "https://registry.yarnpkg.com/@jimp/plugin-print/-/plugin-print-1.6.0.tgz#ccef327f53afb47617aa66ca65435447380faf34"
|
||||||
|
integrity sha512-zarTIJi8fjoGMSI/M3Xh5yY9T65p03XJmPsuNet19K/Q7mwRU6EV2pfj+28++2PV2NJ+htDF5uecAlnGyxFN2A==
|
||||||
|
dependencies:
|
||||||
|
"@jimp/core" "1.6.0"
|
||||||
|
"@jimp/js-jpeg" "1.6.0"
|
||||||
|
"@jimp/js-png" "1.6.0"
|
||||||
|
"@jimp/plugin-blit" "1.6.0"
|
||||||
|
"@jimp/types" "1.6.0"
|
||||||
|
parse-bmfont-ascii "^1.0.6"
|
||||||
|
parse-bmfont-binary "^1.0.6"
|
||||||
|
parse-bmfont-xml "^1.1.6"
|
||||||
|
simple-xml-to-json "^1.2.2"
|
||||||
|
zod "^3.23.8"
|
||||||
|
|
||||||
|
"@jimp/plugin-quantize@1.6.0":
|
||||||
|
version "1.6.0"
|
||||||
|
resolved "https://registry.yarnpkg.com/@jimp/plugin-quantize/-/plugin-quantize-1.6.0.tgz#880095fc0ead41321d94bf54895e366dd7d079d6"
|
||||||
|
integrity sha512-EmzZ/s9StYQwbpG6rUGBCisc3f64JIhSH+ncTJd+iFGtGo0YvSeMdAd+zqgiHpfZoOL54dNavZNjF4otK+mvlg==
|
||||||
|
dependencies:
|
||||||
|
image-q "^4.0.0"
|
||||||
|
zod "^3.23.8"
|
||||||
|
|
||||||
|
"@jimp/plugin-resize@1.6.0":
|
||||||
|
version "1.6.0"
|
||||||
|
resolved "https://registry.yarnpkg.com/@jimp/plugin-resize/-/plugin-resize-1.6.0.tgz#331e8912ed68746846145019bc6e2ea057e6f175"
|
||||||
|
integrity sha512-uSUD1mqXN9i1SGSz5ov3keRZ7S9L32/mAQG08wUwZiEi5FpbV0K8A8l1zkazAIZi9IJzLlTauRNU41Mi8IF9fA==
|
||||||
|
dependencies:
|
||||||
|
"@jimp/core" "1.6.0"
|
||||||
|
"@jimp/types" "1.6.0"
|
||||||
|
zod "^3.23.8"
|
||||||
|
|
||||||
|
"@jimp/plugin-rotate@1.6.0":
|
||||||
|
version "1.6.0"
|
||||||
|
resolved "https://registry.yarnpkg.com/@jimp/plugin-rotate/-/plugin-rotate-1.6.0.tgz#de271f39a3ac9e853b02e01d3d44ab086d12e099"
|
||||||
|
integrity sha512-JagdjBLnUZGSG4xjCLkIpQOZZ3Mjbg8aGCCi4G69qR+OjNpOeGI7N2EQlfK/WE8BEHOW5vdjSyglNqcYbQBWRw==
|
||||||
|
dependencies:
|
||||||
|
"@jimp/core" "1.6.0"
|
||||||
|
"@jimp/plugin-crop" "1.6.0"
|
||||||
|
"@jimp/plugin-resize" "1.6.0"
|
||||||
|
"@jimp/types" "1.6.0"
|
||||||
|
"@jimp/utils" "1.6.0"
|
||||||
|
zod "^3.23.8"
|
||||||
|
|
||||||
|
"@jimp/plugin-threshold@1.6.0":
|
||||||
|
version "1.6.0"
|
||||||
|
resolved "https://registry.yarnpkg.com/@jimp/plugin-threshold/-/plugin-threshold-1.6.0.tgz#11479cf59131ea95dcaff6a1403af1964593a3fa"
|
||||||
|
integrity sha512-M59m5dzLoHOVWdM41O8z9SyySzcDn43xHseOH0HavjsfQsT56GGCC4QzU1banJidbUrePhzoEdS42uFE8Fei8w==
|
||||||
|
dependencies:
|
||||||
|
"@jimp/core" "1.6.0"
|
||||||
|
"@jimp/plugin-color" "1.6.0"
|
||||||
|
"@jimp/plugin-hash" "1.6.0"
|
||||||
|
"@jimp/types" "1.6.0"
|
||||||
|
"@jimp/utils" "1.6.0"
|
||||||
|
zod "^3.23.8"
|
||||||
|
|
||||||
|
"@jimp/types@1.6.0":
|
||||||
|
version "1.6.0"
|
||||||
|
resolved "https://registry.yarnpkg.com/@jimp/types/-/types-1.6.0.tgz#27022730fd673653e1430e6bd8ac6f6de1596f89"
|
||||||
|
integrity sha512-7UfRsiKo5GZTAATxm2qQ7jqmUXP0DxTArztllTcYdyw6Xi5oT4RaoXynVtCD4UyLK5gJgkZJcwonoijrhYFKfg==
|
||||||
|
dependencies:
|
||||||
|
zod "^3.23.8"
|
||||||
|
|
||||||
|
"@jimp/utils@1.6.0":
|
||||||
|
version "1.6.0"
|
||||||
|
resolved "https://registry.yarnpkg.com/@jimp/utils/-/utils-1.6.0.tgz#e196f3953ea1ebc88f50cf0d490adb24aeffe596"
|
||||||
|
integrity sha512-gqFTGEosKbOkYF/WFj26jMHOI5OH2jeP1MmC/zbK6BF6VJBf8rIC5898dPfSzZEbSA0wbbV5slbntWVc5PKLFA==
|
||||||
|
dependencies:
|
||||||
|
"@jimp/types" "1.6.0"
|
||||||
|
tinycolor2 "^1.6.0"
|
||||||
|
|
||||||
"@jridgewell/sourcemap-codec@^1.5.5":
|
"@jridgewell/sourcemap-codec@^1.5.5":
|
||||||
version "1.5.5"
|
version "1.5.5"
|
||||||
resolved "https://registry.yarnpkg.com/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz#6912b00d2c631c0d15ce1a7ab57cd657f2a8f8ba"
|
resolved "https://registry.yarnpkg.com/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz#6912b00d2c631c0d15ce1a7ab57cd657f2a8f8ba"
|
||||||
@ -523,6 +803,16 @@
|
|||||||
dependencies:
|
dependencies:
|
||||||
defer-to-connect "^2.0.0"
|
defer-to-connect "^2.0.0"
|
||||||
|
|
||||||
|
"@techstark/opencv-js@^4.12.0-release.1":
|
||||||
|
version "4.12.0-release.1"
|
||||||
|
resolved "https://registry.yarnpkg.com/@techstark/opencv-js/-/opencv-js-4.12.0-release.1.tgz#4d6823e69b70ef6d2705ba77f09c60be8b6d53ee"
|
||||||
|
integrity sha512-LtTaph9v/HqLPXEg3m1xs2h7QJh10pUpuDT0nj8g77lelWnTwwQrehtd+fXElLOdrkqc4Fea6Z/sJBvEJLYPfw==
|
||||||
|
|
||||||
|
"@tokenizer/token@^0.3.0":
|
||||||
|
version "0.3.0"
|
||||||
|
resolved "https://registry.yarnpkg.com/@tokenizer/token/-/token-0.3.0.tgz#fe98a93fe789247e998c75e74e9c7c63217aa276"
|
||||||
|
integrity sha512-OvjF+z51L3ov0OyAU0duzsYuvO01PH7x4t6DJx+guahgTnBHkhJdG7soQeTSFLWN3efnHyibZ4Z8l2EuWwJN3A==
|
||||||
|
|
||||||
"@tootallnate/once@1":
|
"@tootallnate/once@1":
|
||||||
version "1.1.2"
|
version "1.1.2"
|
||||||
resolved "https://registry.yarnpkg.com/@tootallnate/once/-/once-1.1.2.tgz#ccb91445360179a04e7fe6aff78c00ffc1eeaf82"
|
resolved "https://registry.yarnpkg.com/@tootallnate/once/-/once-1.1.2.tgz#ccb91445360179a04e7fe6aff78c00ffc1eeaf82"
|
||||||
@ -584,6 +874,14 @@
|
|||||||
"@types/express-serve-static-core" "^5.0.0"
|
"@types/express-serve-static-core" "^5.0.0"
|
||||||
"@types/serve-static" "^1"
|
"@types/serve-static" "^1"
|
||||||
|
|
||||||
|
"@types/fs-extra@^11.0.4":
|
||||||
|
version "11.0.4"
|
||||||
|
resolved "https://registry.yarnpkg.com/@types/fs-extra/-/fs-extra-11.0.4.tgz#e16a863bb8843fba8c5004362b5a73e17becca45"
|
||||||
|
integrity sha512-yTbItCNreRooED33qjunPthRcSjERP1r4MqCZc7wv0u2sUkzTFp45tgUfS5+r7FrZPdmCCNflLhVSP/o+SemsQ==
|
||||||
|
dependencies:
|
||||||
|
"@types/jsonfile" "*"
|
||||||
|
"@types/node" "*"
|
||||||
|
|
||||||
"@types/http-cache-semantics@*":
|
"@types/http-cache-semantics@*":
|
||||||
version "4.0.4"
|
version "4.0.4"
|
||||||
resolved "https://registry.yarnpkg.com/@types/http-cache-semantics/-/http-cache-semantics-4.0.4.tgz#b979ebad3919799c979b17c72621c0bc0a31c6c4"
|
resolved "https://registry.yarnpkg.com/@types/http-cache-semantics/-/http-cache-semantics-4.0.4.tgz#b979ebad3919799c979b17c72621c0bc0a31c6c4"
|
||||||
@ -594,6 +892,13 @@
|
|||||||
resolved "https://registry.yarnpkg.com/@types/http-errors/-/http-errors-2.0.5.tgz#5b749ab2b16ba113423feb1a64a95dcd30398472"
|
resolved "https://registry.yarnpkg.com/@types/http-errors/-/http-errors-2.0.5.tgz#5b749ab2b16ba113423feb1a64a95dcd30398472"
|
||||||
integrity sha512-r8Tayk8HJnX0FztbZN7oVqGccWgw98T/0neJphO91KkmOzug1KkofZURD4UaD5uH8AqcFLfdPErnBod0u71/qg==
|
integrity sha512-r8Tayk8HJnX0FztbZN7oVqGccWgw98T/0neJphO91KkmOzug1KkofZURD4UaD5uH8AqcFLfdPErnBod0u71/qg==
|
||||||
|
|
||||||
|
"@types/jsonfile@*":
|
||||||
|
version "6.1.4"
|
||||||
|
resolved "https://registry.yarnpkg.com/@types/jsonfile/-/jsonfile-6.1.4.tgz#614afec1a1164e7d670b4a7ad64df3e7beb7b702"
|
||||||
|
integrity sha512-D5qGUYwjvnNNextdU59/+fI+spnwtTFmyQP0h+PfIOSkNfpU6AOICUOkm4i0OnSk+NyjdPJrxCDro0sJsWlRpQ==
|
||||||
|
dependencies:
|
||||||
|
"@types/node" "*"
|
||||||
|
|
||||||
"@types/keyv@^3.1.4":
|
"@types/keyv@^3.1.4":
|
||||||
version "3.1.4"
|
version "3.1.4"
|
||||||
resolved "https://registry.yarnpkg.com/@types/keyv/-/keyv-3.1.4.tgz#3ccdb1c6751b0c7e52300bcdacd5bcbf8faa75b6"
|
resolved "https://registry.yarnpkg.com/@types/keyv/-/keyv-3.1.4.tgz#3ccdb1c6751b0c7e52300bcdacd5bcbf8faa75b6"
|
||||||
@ -620,6 +925,11 @@
|
|||||||
dependencies:
|
dependencies:
|
||||||
undici-types "~7.16.0"
|
undici-types "~7.16.0"
|
||||||
|
|
||||||
|
"@types/node@16.9.1":
|
||||||
|
version "16.9.1"
|
||||||
|
resolved "https://registry.yarnpkg.com/@types/node/-/node-16.9.1.tgz#0611b37db4246c937feef529ddcc018cf8e35708"
|
||||||
|
integrity sha512-QpLcX9ZSsq3YYUUnD3nFDY8H7wctAhQj/TFKL8Ya8v5fMm3CFXxo8zStsLAl780ltoYoo1WvKUVGBQK+1ifr7g==
|
||||||
|
|
||||||
"@types/node@^22.7.7":
|
"@types/node@^22.7.7":
|
||||||
version "22.19.0"
|
version "22.19.0"
|
||||||
resolved "https://registry.yarnpkg.com/@types/node/-/node-22.19.0.tgz#849606ef3920850583a4e7ee0930987c35ad80be"
|
resolved "https://registry.yarnpkg.com/@types/node/-/node-22.19.0.tgz#849606ef3920850583a4e7ee0930987c35ad80be"
|
||||||
@ -811,6 +1121,13 @@ abbrev@1:
|
|||||||
resolved "https://registry.yarnpkg.com/abbrev/-/abbrev-1.1.1.tgz#f8f2c887ad10bf67f634f005b6987fed3179aac8"
|
resolved "https://registry.yarnpkg.com/abbrev/-/abbrev-1.1.1.tgz#f8f2c887ad10bf67f634f005b6987fed3179aac8"
|
||||||
integrity sha512-nne9/IiQ/hzIhY6pdDnbBtz7DjPTKrY00P/zvPSm5pOFkl6xuGrGnXn/VtTNNfNtAfZ9/1RtehkszU9qcTii0Q==
|
integrity sha512-nne9/IiQ/hzIhY6pdDnbBtz7DjPTKrY00P/zvPSm5pOFkl6xuGrGnXn/VtTNNfNtAfZ9/1RtehkszU9qcTii0Q==
|
||||||
|
|
||||||
|
abort-controller@^3.0.0:
|
||||||
|
version "3.0.0"
|
||||||
|
resolved "https://registry.yarnpkg.com/abort-controller/-/abort-controller-3.0.0.tgz#eaf54d53b62bae4138e809ca225c8439a6efb392"
|
||||||
|
integrity sha512-h8lQ8tacZYnR3vNQTgibj+tODHI5/+l06Au2Pcriv/Gmet0eaj4TwWH41sO9wnHDiQsEj19q0drzdWdeAHtweg==
|
||||||
|
dependencies:
|
||||||
|
event-target-shim "^5.0.0"
|
||||||
|
|
||||||
accepts@^2.0.0:
|
accepts@^2.0.0:
|
||||||
version "2.0.0"
|
version "2.0.0"
|
||||||
resolved "https://registry.yarnpkg.com/accepts/-/accepts-2.0.0.tgz#bbcf4ba5075467f3f2131eab3cffc73c2f5d7895"
|
resolved "https://registry.yarnpkg.com/accepts/-/accepts-2.0.0.tgz#bbcf4ba5075467f3f2131eab3cffc73c2f5d7895"
|
||||||
@ -819,6 +1136,11 @@ accepts@^2.0.0:
|
|||||||
mime-types "^3.0.0"
|
mime-types "^3.0.0"
|
||||||
negotiator "^1.0.0"
|
negotiator "^1.0.0"
|
||||||
|
|
||||||
|
adm-zip@^0.5.16:
|
||||||
|
version "0.5.16"
|
||||||
|
resolved "https://registry.yarnpkg.com/adm-zip/-/adm-zip-0.5.16.tgz#0b5e4c779f07dedea5805cdccb1147071d94a909"
|
||||||
|
integrity sha512-TGw5yVi4saajsSEgz25grObGHEUaDrniwvA2qwSC060KfqGPdglhvPMA2lPIoxs3PQIItj2iag35fONcQqgUaQ==
|
||||||
|
|
||||||
agent-base@6, agent-base@^6.0.2:
|
agent-base@6, agent-base@^6.0.2:
|
||||||
version "6.0.2"
|
version "6.0.2"
|
||||||
resolved "https://registry.yarnpkg.com/agent-base/-/agent-base-6.0.2.tgz#49fff58577cfee3f37176feab4c22e00f86d7f77"
|
resolved "https://registry.yarnpkg.com/agent-base/-/agent-base-6.0.2.tgz#49fff58577cfee3f37176feab4c22e00f86d7f77"
|
||||||
@ -858,6 +1180,11 @@ ansi-styles@^4.0.0, ansi-styles@^4.1.0:
|
|||||||
dependencies:
|
dependencies:
|
||||||
color-convert "^2.0.1"
|
color-convert "^2.0.1"
|
||||||
|
|
||||||
|
any-base@^1.1.0:
|
||||||
|
version "1.1.0"
|
||||||
|
resolved "https://registry.yarnpkg.com/any-base/-/any-base-1.1.0.tgz#ae101a62bc08a597b4c9ab5b7089d456630549fe"
|
||||||
|
integrity sha512-uMgjozySS8adZZYePpaWs8cxB9/kdzmpX6SgJZ+wbz1K5eYk5QMYDVJaZKhxyIHUdnnJkfR7SVgStgH7LkGUyg==
|
||||||
|
|
||||||
append-field@^1.0.0:
|
append-field@^1.0.0:
|
||||||
version "1.0.0"
|
version "1.0.0"
|
||||||
resolved "https://registry.yarnpkg.com/append-field/-/append-field-1.0.0.tgz#1e3440e915f0b1203d23748e78edd7b9b5b43e56"
|
resolved "https://registry.yarnpkg.com/append-field/-/append-field-1.0.0.tgz#1e3440e915f0b1203d23748e78edd7b9b5b43e56"
|
||||||
@ -881,6 +1208,11 @@ asynckit@^0.4.0:
|
|||||||
resolved "https://registry.yarnpkg.com/asynckit/-/asynckit-0.4.0.tgz#c79ed97f7f34cb8f2ba1bc9790bcc366474b4b79"
|
resolved "https://registry.yarnpkg.com/asynckit/-/asynckit-0.4.0.tgz#c79ed97f7f34cb8f2ba1bc9790bcc366474b4b79"
|
||||||
integrity sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==
|
integrity sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==
|
||||||
|
|
||||||
|
await-to-js@^3.0.0:
|
||||||
|
version "3.0.0"
|
||||||
|
resolved "https://registry.yarnpkg.com/await-to-js/-/await-to-js-3.0.0.tgz#70929994185616f4675a91af6167eb61cc92868f"
|
||||||
|
integrity sha512-zJAaP9zxTcvTHRlejau3ZOY4V7SRpiByf3/dxx2uyKxxor19tpmpV2QRsTKikckwhaPmr2dVpxxMr7jOCYVp5g==
|
||||||
|
|
||||||
axios@^1.12.2:
|
axios@^1.12.2:
|
||||||
version "1.13.2"
|
version "1.13.2"
|
||||||
resolved "https://registry.yarnpkg.com/axios/-/axios-1.13.2.tgz#9ada120b7b5ab24509553ec3e40123521117f687"
|
resolved "https://registry.yarnpkg.com/axios/-/axios-1.13.2.tgz#9ada120b7b5ab24509553ec3e40123521117f687"
|
||||||
@ -921,6 +1253,11 @@ bmp-js@^0.1.0:
|
|||||||
resolved "https://registry.yarnpkg.com/bmp-js/-/bmp-js-0.1.0.tgz#e05a63f796a6c1ff25f4771ec7adadc148c07233"
|
resolved "https://registry.yarnpkg.com/bmp-js/-/bmp-js-0.1.0.tgz#e05a63f796a6c1ff25f4771ec7adadc148c07233"
|
||||||
integrity sha512-vHdS19CnY3hwiNdkaqk93DvjVLfbEcI8mys4UjuWrlX1haDmroo8o4xCzh4wD6DGV6HxRCyauwhHRqMTfERtjw==
|
integrity sha512-vHdS19CnY3hwiNdkaqk93DvjVLfbEcI8mys4UjuWrlX1haDmroo8o4xCzh4wD6DGV6HxRCyauwhHRqMTfERtjw==
|
||||||
|
|
||||||
|
bmp-ts@^1.0.9:
|
||||||
|
version "1.0.9"
|
||||||
|
resolved "https://registry.yarnpkg.com/bmp-ts/-/bmp-ts-1.0.9.tgz#0fd124ba812be9b786b29e5b186ee76d74ff5538"
|
||||||
|
integrity sha512-cTEHk2jLrPyi+12M3dhpEbnnPOsaZuq7C45ylbbQIiWgDFZq4UVYPEY5mlqjvsj/6gJv9qX5sa+ebDzLXT28Vw==
|
||||||
|
|
||||||
body-parser@^2.2.0:
|
body-parser@^2.2.0:
|
||||||
version "2.2.0"
|
version "2.2.0"
|
||||||
resolved "https://registry.yarnpkg.com/body-parser/-/body-parser-2.2.0.tgz#f7a9656de305249a715b549b7b8fd1ab9dfddcfa"
|
resolved "https://registry.yarnpkg.com/body-parser/-/body-parser-2.2.0.tgz#f7a9656de305249a715b549b7b8fd1ab9dfddcfa"
|
||||||
@ -967,6 +1304,14 @@ buffer@^5.5.0:
|
|||||||
base64-js "^1.3.1"
|
base64-js "^1.3.1"
|
||||||
ieee754 "^1.1.13"
|
ieee754 "^1.1.13"
|
||||||
|
|
||||||
|
buffer@^6.0.3:
|
||||||
|
version "6.0.3"
|
||||||
|
resolved "https://registry.yarnpkg.com/buffer/-/buffer-6.0.3.tgz#2ace578459cc8fbe2a70aaa8f52ee63b6a74c6c6"
|
||||||
|
integrity sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA==
|
||||||
|
dependencies:
|
||||||
|
base64-js "^1.3.1"
|
||||||
|
ieee754 "^1.2.1"
|
||||||
|
|
||||||
busboy@^1.6.0:
|
busboy@^1.6.0:
|
||||||
version "1.6.0"
|
version "1.6.0"
|
||||||
resolved "https://registry.yarnpkg.com/busboy/-/busboy-1.6.0.tgz#966ea36a9502e43cdb9146962523b92f531f6893"
|
resolved "https://registry.yarnpkg.com/busboy/-/busboy-1.6.0.tgz#966ea36a9502e43cdb9146962523b92f531f6893"
|
||||||
@ -1063,6 +1408,11 @@ chownr@^2.0.0:
|
|||||||
resolved "https://registry.yarnpkg.com/chownr/-/chownr-2.0.0.tgz#15bfbe53d2eab4cf70f18a8cd68ebe5b3cb1dece"
|
resolved "https://registry.yarnpkg.com/chownr/-/chownr-2.0.0.tgz#15bfbe53d2eab4cf70f18a8cd68ebe5b3cb1dece"
|
||||||
integrity sha512-bIomtDF5KGpdogkLd9VspvFzk9KfpyyGlS8YFVZl7TGPBHL5snIOnxeshwVgPteQ9b4Eydl+pVbIyE1DcvCWgQ==
|
integrity sha512-bIomtDF5KGpdogkLd9VspvFzk9KfpyyGlS8YFVZl7TGPBHL5snIOnxeshwVgPteQ9b4Eydl+pVbIyE1DcvCWgQ==
|
||||||
|
|
||||||
|
chownr@^3.0.0:
|
||||||
|
version "3.0.0"
|
||||||
|
resolved "https://registry.yarnpkg.com/chownr/-/chownr-3.0.0.tgz#9855e64ecd240a9cc4267ce8a4aa5d24a1da15e4"
|
||||||
|
integrity sha512-+IxzY9BZOQd/XuYPRmrvEVjF/nqj5kgT4kEq7VofrDoM1MxoRjEWkrCC3EtLi59TVawxTAn+orJwFQcrqEN1+g==
|
||||||
|
|
||||||
clean-stack@^2.0.0:
|
clean-stack@^2.0.0:
|
||||||
version "2.2.0"
|
version "2.2.0"
|
||||||
resolved "https://registry.yarnpkg.com/clean-stack/-/clean-stack-2.2.0.tgz#ee8472dbb129e727b31e8a10a427dee9dfe4008b"
|
resolved "https://registry.yarnpkg.com/clean-stack/-/clean-stack-2.2.0.tgz#ee8472dbb129e727b31e8a10a427dee9dfe4008b"
|
||||||
@ -1199,6 +1549,11 @@ csstype@^3.1.3:
|
|||||||
resolved "https://registry.yarnpkg.com/csstype/-/csstype-3.1.3.tgz#d80ff294d114fb0e6ac500fbf85b60137d7eff81"
|
resolved "https://registry.yarnpkg.com/csstype/-/csstype-3.1.3.tgz#d80ff294d114fb0e6ac500fbf85b60137d7eff81"
|
||||||
integrity sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==
|
integrity sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==
|
||||||
|
|
||||||
|
data-uri-to-buffer@^4.0.0:
|
||||||
|
version "4.0.1"
|
||||||
|
resolved "https://registry.yarnpkg.com/data-uri-to-buffer/-/data-uri-to-buffer-4.0.1.tgz#d8feb2b2881e6a4f58c2e08acfd0e2834e26222e"
|
||||||
|
integrity sha512-0R9ikRb668HB7QDxT1vkpuUBtqc53YyAwMwGeUFKRojY/NWKvdZ+9UYtRfGmhqNbRkTSVpMbmyhXipFFv2cb/A==
|
||||||
|
|
||||||
debug@4, debug@^4.1.0, debug@^4.1.1, debug@^4.3.3, debug@^4.3.5, debug@^4.4.0:
|
debug@4, debug@^4.1.0, debug@^4.1.1, debug@^4.3.3, debug@^4.3.5, debug@^4.4.0:
|
||||||
version "4.4.3"
|
version "4.4.3"
|
||||||
resolved "https://registry.yarnpkg.com/debug/-/debug-4.4.3.tgz#c6ae432d9bd9662582fce08709b038c58e9e3d6a"
|
resolved "https://registry.yarnpkg.com/debug/-/debug-4.4.3.tgz#c6ae432d9bd9662582fce08709b038c58e9e3d6a"
|
||||||
@ -1417,6 +1772,21 @@ etag@^1.8.1:
|
|||||||
resolved "https://registry.yarnpkg.com/etag/-/etag-1.8.1.tgz#41ae2eeb65efa62268aebfea83ac7d79299b0887"
|
resolved "https://registry.yarnpkg.com/etag/-/etag-1.8.1.tgz#41ae2eeb65efa62268aebfea83ac7d79299b0887"
|
||||||
integrity sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==
|
integrity sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==
|
||||||
|
|
||||||
|
event-target-shim@^5.0.0:
|
||||||
|
version "5.0.1"
|
||||||
|
resolved "https://registry.yarnpkg.com/event-target-shim/-/event-target-shim-5.0.1.tgz#5d4d3ebdf9583d63a5333ce2deb7480ab2b05789"
|
||||||
|
integrity sha512-i/2XbnSz/uxRCU6+NdVJgKWDTM427+MqYbkQzD321DuCQJUqOuJKIA0IM2+W2xtYHdKOmZ4dR6fExsd4SXL+WQ==
|
||||||
|
|
||||||
|
events@^3.3.0:
|
||||||
|
version "3.3.0"
|
||||||
|
resolved "https://registry.yarnpkg.com/events/-/events-3.3.0.tgz#31a95ad0a924e2d2c419a813aeb2c4e878ea7400"
|
||||||
|
integrity sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q==
|
||||||
|
|
||||||
|
exif-parser@^0.1.12:
|
||||||
|
version "0.1.12"
|
||||||
|
resolved "https://registry.yarnpkg.com/exif-parser/-/exif-parser-0.1.12.tgz#58a9d2d72c02c1f6f02a0ef4a9166272b7760922"
|
||||||
|
integrity sha512-c2bQfLNbMzLPmzQuOr8fy0csy84WmwnER81W88DzTp9CYNPJ6yzOj2EZAh9pywYpqHnshVLHQJ8WzldAyfY+Iw==
|
||||||
|
|
||||||
expand-template@^2.0.3:
|
expand-template@^2.0.3:
|
||||||
version "2.0.3"
|
version "2.0.3"
|
||||||
resolved "https://registry.yarnpkg.com/expand-template/-/expand-template-2.0.3.tgz#6e14b3fcee0f3a6340ecb57d2e8918692052a47c"
|
resolved "https://registry.yarnpkg.com/expand-template/-/expand-template-2.0.3.tgz#6e14b3fcee0f3a6340ecb57d2e8918692052a47c"
|
||||||
@ -1478,6 +1848,23 @@ fdir@^6.5.0:
|
|||||||
resolved "https://registry.yarnpkg.com/fdir/-/fdir-6.5.0.tgz#ed2ab967a331ade62f18d077dae192684d50d350"
|
resolved "https://registry.yarnpkg.com/fdir/-/fdir-6.5.0.tgz#ed2ab967a331ade62f18d077dae192684d50d350"
|
||||||
integrity sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==
|
integrity sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==
|
||||||
|
|
||||||
|
fetch-blob@^3.1.2, fetch-blob@^3.1.4:
|
||||||
|
version "3.2.0"
|
||||||
|
resolved "https://registry.yarnpkg.com/fetch-blob/-/fetch-blob-3.2.0.tgz#f09b8d4bbd45adc6f0c20b7e787e793e309dcce9"
|
||||||
|
integrity sha512-7yAQpD2UMJzLi1Dqv7qFYnPbaPx7ZfFK6PiIxQ4PfkGPyNyl2Ugx+a/umUonmKqjhM4DnfbMvdX6otXq83soQQ==
|
||||||
|
dependencies:
|
||||||
|
node-domexception "^1.0.0"
|
||||||
|
web-streams-polyfill "^3.0.3"
|
||||||
|
|
||||||
|
file-type@^16.0.0:
|
||||||
|
version "16.5.4"
|
||||||
|
resolved "https://registry.yarnpkg.com/file-type/-/file-type-16.5.4.tgz#474fb4f704bee427681f98dd390058a172a6c2fd"
|
||||||
|
integrity sha512-/yFHK0aGjFEgDJjEKP0pWCplsPFPhwyfwevf/pVxiN0tmE4L9LmwWxWukdJSHdoCli4VgQLehjJtwQBnqmsKcw==
|
||||||
|
dependencies:
|
||||||
|
readable-web-to-node-stream "^3.0.0"
|
||||||
|
strtok3 "^6.2.4"
|
||||||
|
token-types "^4.1.1"
|
||||||
|
|
||||||
file-uri-to-path@1.0.0:
|
file-uri-to-path@1.0.0:
|
||||||
version "1.0.0"
|
version "1.0.0"
|
||||||
resolved "https://registry.yarnpkg.com/file-uri-to-path/-/file-uri-to-path-1.0.0.tgz#553a7b8446ff6f684359c445f1e37a05dacc33dd"
|
resolved "https://registry.yarnpkg.com/file-uri-to-path/-/file-uri-to-path-1.0.0.tgz#553a7b8446ff6f684359c445f1e37a05dacc33dd"
|
||||||
@ -1511,6 +1898,13 @@ form-data@^4.0.4:
|
|||||||
hasown "^2.0.2"
|
hasown "^2.0.2"
|
||||||
mime-types "^2.1.12"
|
mime-types "^2.1.12"
|
||||||
|
|
||||||
|
formdata-polyfill@^4.0.10:
|
||||||
|
version "4.0.10"
|
||||||
|
resolved "https://registry.yarnpkg.com/formdata-polyfill/-/formdata-polyfill-4.0.10.tgz#24807c31c9d402e002ab3d8c720144ceb8848423"
|
||||||
|
integrity sha512-buewHzMvYL29jdeQTVILecSaZKnt/RJWjoZCF5OW60Z67/GmSLBkOFM7qh1PI3zFNtJbaZL5eQu1vLfazOwj4g==
|
||||||
|
dependencies:
|
||||||
|
fetch-blob "^3.1.2"
|
||||||
|
|
||||||
forwarded@0.2.0:
|
forwarded@0.2.0:
|
||||||
version "0.2.0"
|
version "0.2.0"
|
||||||
resolved "https://registry.yarnpkg.com/forwarded/-/forwarded-0.2.0.tgz#2269936428aad4c15c7ebe9779a84bf0b2a81811"
|
resolved "https://registry.yarnpkg.com/forwarded/-/forwarded-0.2.0.tgz#2269936428aad4c15c7ebe9779a84bf0b2a81811"
|
||||||
@ -1616,6 +2010,14 @@ get-stream@^5.1.0:
|
|||||||
dependencies:
|
dependencies:
|
||||||
pump "^3.0.0"
|
pump "^3.0.0"
|
||||||
|
|
||||||
|
gifwrap@^0.10.1:
|
||||||
|
version "0.10.1"
|
||||||
|
resolved "https://registry.yarnpkg.com/gifwrap/-/gifwrap-0.10.1.tgz#9ed46a5d51913b482d4221ce9c727080260b681e"
|
||||||
|
integrity sha512-2760b1vpJHNmLzZ/ubTtNnEx5WApN/PYWJvXvgS+tL1egTTthayFYIQQNi136FLEDcN/IyEY2EcGpIITD6eYUw==
|
||||||
|
dependencies:
|
||||||
|
image-q "^4.0.0"
|
||||||
|
omggif "^1.0.10"
|
||||||
|
|
||||||
github-from-package@0.0.0:
|
github-from-package@0.0.0:
|
||||||
version "0.0.0"
|
version "0.0.0"
|
||||||
resolved "https://registry.yarnpkg.com/github-from-package/-/github-from-package-0.0.0.tgz#97fb5d96bfde8973313f20e8288ef9a167fa64ce"
|
resolved "https://registry.yarnpkg.com/github-from-package/-/github-from-package-0.0.0.tgz#97fb5d96bfde8973313f20e8288ef9a167fa64ce"
|
||||||
@ -1783,11 +2185,18 @@ idb-keyval@^6.2.0:
|
|||||||
resolved "https://registry.yarnpkg.com/idb-keyval/-/idb-keyval-6.2.2.tgz#b0171b5f73944854a3291a5cdba8e12768c4854a"
|
resolved "https://registry.yarnpkg.com/idb-keyval/-/idb-keyval-6.2.2.tgz#b0171b5f73944854a3291a5cdba8e12768c4854a"
|
||||||
integrity sha512-yjD9nARJ/jb1g+CvD0tlhUHOrJ9Sy0P8T9MF3YaLlHnSRpwPfpTX0XIvpmw3gAJUmEu3FiICLBDPXVwyEvrleg==
|
integrity sha512-yjD9nARJ/jb1g+CvD0tlhUHOrJ9Sy0P8T9MF3YaLlHnSRpwPfpTX0XIvpmw3gAJUmEu3FiICLBDPXVwyEvrleg==
|
||||||
|
|
||||||
ieee754@^1.1.13:
|
ieee754@^1.1.13, ieee754@^1.2.1:
|
||||||
version "1.2.1"
|
version "1.2.1"
|
||||||
resolved "https://registry.yarnpkg.com/ieee754/-/ieee754-1.2.1.tgz#8eb7a10a63fff25d15a57b001586d177d1b0d352"
|
resolved "https://registry.yarnpkg.com/ieee754/-/ieee754-1.2.1.tgz#8eb7a10a63fff25d15a57b001586d177d1b0d352"
|
||||||
integrity sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==
|
integrity sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==
|
||||||
|
|
||||||
|
image-q@^4.0.0:
|
||||||
|
version "4.0.0"
|
||||||
|
resolved "https://registry.yarnpkg.com/image-q/-/image-q-4.0.0.tgz#31e075be7bae3c1f42a85c469b4732c358981776"
|
||||||
|
integrity sha512-PfJGVgIfKQJuq3s0tTDOKtztksibuUEbJQIYT3by6wctQo+Rdlh7ef4evJ5NCdxY4CfMbvFkocEwbl4BF8RlJw==
|
||||||
|
dependencies:
|
||||||
|
"@types/node" "16.9.1"
|
||||||
|
|
||||||
imurmurhash@^0.1.4:
|
imurmurhash@^0.1.4:
|
||||||
version "0.1.4"
|
version "0.1.4"
|
||||||
resolved "https://registry.yarnpkg.com/imurmurhash/-/imurmurhash-0.1.4.tgz#9218b9b2b928a238b13dc4fb6b6d576f231453ea"
|
resolved "https://registry.yarnpkg.com/imurmurhash/-/imurmurhash-0.1.4.tgz#9218b9b2b928a238b13dc4fb6b6d576f231453ea"
|
||||||
@ -1856,6 +2265,39 @@ isexe@^2.0.0:
|
|||||||
resolved "https://registry.yarnpkg.com/isexe/-/isexe-2.0.0.tgz#e8fbf374dc556ff8947a10dcb0572d633f2cfa10"
|
resolved "https://registry.yarnpkg.com/isexe/-/isexe-2.0.0.tgz#e8fbf374dc556ff8947a10dcb0572d633f2cfa10"
|
||||||
integrity sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==
|
integrity sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==
|
||||||
|
|
||||||
|
jimp@^1.6.0:
|
||||||
|
version "1.6.0"
|
||||||
|
resolved "https://registry.yarnpkg.com/jimp/-/jimp-1.6.0.tgz#7c7e5133c8dc06706e1ed35e771c685af393bfd2"
|
||||||
|
integrity sha512-YcwCHw1kiqEeI5xRpDlPPBGL2EOpBKLwO4yIBJcXWHPj5PnA5urGq0jbyhM5KoNpypQ6VboSoxc9D8HyfvngSg==
|
||||||
|
dependencies:
|
||||||
|
"@jimp/core" "1.6.0"
|
||||||
|
"@jimp/diff" "1.6.0"
|
||||||
|
"@jimp/js-bmp" "1.6.0"
|
||||||
|
"@jimp/js-gif" "1.6.0"
|
||||||
|
"@jimp/js-jpeg" "1.6.0"
|
||||||
|
"@jimp/js-png" "1.6.0"
|
||||||
|
"@jimp/js-tiff" "1.6.0"
|
||||||
|
"@jimp/plugin-blit" "1.6.0"
|
||||||
|
"@jimp/plugin-blur" "1.6.0"
|
||||||
|
"@jimp/plugin-circle" "1.6.0"
|
||||||
|
"@jimp/plugin-color" "1.6.0"
|
||||||
|
"@jimp/plugin-contain" "1.6.0"
|
||||||
|
"@jimp/plugin-cover" "1.6.0"
|
||||||
|
"@jimp/plugin-crop" "1.6.0"
|
||||||
|
"@jimp/plugin-displace" "1.6.0"
|
||||||
|
"@jimp/plugin-dither" "1.6.0"
|
||||||
|
"@jimp/plugin-fisheye" "1.6.0"
|
||||||
|
"@jimp/plugin-flip" "1.6.0"
|
||||||
|
"@jimp/plugin-hash" "1.6.0"
|
||||||
|
"@jimp/plugin-mask" "1.6.0"
|
||||||
|
"@jimp/plugin-print" "1.6.0"
|
||||||
|
"@jimp/plugin-quantize" "1.6.0"
|
||||||
|
"@jimp/plugin-resize" "1.6.0"
|
||||||
|
"@jimp/plugin-rotate" "1.6.0"
|
||||||
|
"@jimp/plugin-threshold" "1.6.0"
|
||||||
|
"@jimp/types" "1.6.0"
|
||||||
|
"@jimp/utils" "1.6.0"
|
||||||
|
|
||||||
joi@^18.0.1:
|
joi@^18.0.1:
|
||||||
version "18.0.1"
|
version "18.0.1"
|
||||||
resolved "https://registry.yarnpkg.com/joi/-/joi-18.0.1.tgz#1e1885d035cc6ca1624e81bf22112e7c1ee38e1b"
|
resolved "https://registry.yarnpkg.com/joi/-/joi-18.0.1.tgz#1e1885d035cc6ca1624e81bf22112e7c1ee38e1b"
|
||||||
@ -1869,6 +2311,11 @@ joi@^18.0.1:
|
|||||||
"@hapi/topo" "^6.0.2"
|
"@hapi/topo" "^6.0.2"
|
||||||
"@standard-schema/spec" "^1.0.0"
|
"@standard-schema/spec" "^1.0.0"
|
||||||
|
|
||||||
|
jpeg-js@^0.4.4:
|
||||||
|
version "0.4.4"
|
||||||
|
resolved "https://registry.yarnpkg.com/jpeg-js/-/jpeg-js-0.4.4.tgz#a9f1c6f1f9f0fa80cdb3484ed9635054d28936aa"
|
||||||
|
integrity sha512-WZzeDOEtTOBK4Mdsar0IqEU5sMr3vSV2RqkAIzUEV2BHnUfKGyswWFPFwK5EeDo93K3FohSHbLAjj0s1Wzd+dg==
|
||||||
|
|
||||||
json-buffer@3.0.1:
|
json-buffer@3.0.1:
|
||||||
version "3.0.1"
|
version "3.0.1"
|
||||||
resolved "https://registry.yarnpkg.com/json-buffer/-/json-buffer-3.0.1.tgz#9338802a30d3b6605fbe0613e094008ca8c05a13"
|
resolved "https://registry.yarnpkg.com/json-buffer/-/json-buffer-3.0.1.tgz#9338802a30d3b6605fbe0613e094008ca8c05a13"
|
||||||
@ -1999,6 +2446,11 @@ mime-types@^3.0.0, mime-types@^3.0.1:
|
|||||||
dependencies:
|
dependencies:
|
||||||
mime-db "^1.54.0"
|
mime-db "^1.54.0"
|
||||||
|
|
||||||
|
mime@3:
|
||||||
|
version "3.0.0"
|
||||||
|
resolved "https://registry.yarnpkg.com/mime/-/mime-3.0.0.tgz#b374550dca3a0c18443b0c950a6a58f1931cf7a7"
|
||||||
|
integrity sha512-jSCU7/VB1loIWBZe14aEYHU/+1UMEHoaO7qxCOVJOw9GgH72VAWppxNcjU+x9a2k3GSIBXNKxXQFqRvvZ7vr3A==
|
||||||
|
|
||||||
mimic-response@^1.0.0:
|
mimic-response@^1.0.0:
|
||||||
version "1.0.1"
|
version "1.0.1"
|
||||||
resolved "https://registry.yarnpkg.com/mimic-response/-/mimic-response-1.0.1.tgz#4923538878eef42063cb8a3e3b0798781487ab1b"
|
resolved "https://registry.yarnpkg.com/mimic-response/-/mimic-response-1.0.1.tgz#4923538878eef42063cb8a3e3b0798781487ab1b"
|
||||||
@ -2072,6 +2524,11 @@ minipass@^5.0.0:
|
|||||||
resolved "https://registry.yarnpkg.com/minipass/-/minipass-5.0.0.tgz#3e9788ffb90b694a5d0ec94479a45b5d8738133d"
|
resolved "https://registry.yarnpkg.com/minipass/-/minipass-5.0.0.tgz#3e9788ffb90b694a5d0ec94479a45b5d8738133d"
|
||||||
integrity sha512-3FnjYuehv9k6ovOEbyOswadCDPX1piCfhV8ncmYtHOjuPwylVWsghTLo7rabjC3Rx5xD4HDx8Wm1xnMF7S5qFQ==
|
integrity sha512-3FnjYuehv9k6ovOEbyOswadCDPX1piCfhV8ncmYtHOjuPwylVWsghTLo7rabjC3Rx5xD4HDx8Wm1xnMF7S5qFQ==
|
||||||
|
|
||||||
|
minipass@^7.0.4, minipass@^7.1.2:
|
||||||
|
version "7.1.2"
|
||||||
|
resolved "https://registry.yarnpkg.com/minipass/-/minipass-7.1.2.tgz#93a9626ce5e5e66bd4db86849e7515e92340a707"
|
||||||
|
integrity sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw==
|
||||||
|
|
||||||
minizlib@^2.0.0, minizlib@^2.1.1:
|
minizlib@^2.0.0, minizlib@^2.1.1:
|
||||||
version "2.1.2"
|
version "2.1.2"
|
||||||
resolved "https://registry.yarnpkg.com/minizlib/-/minizlib-2.1.2.tgz#e90d3466ba209b932451508a11ce3d3632145931"
|
resolved "https://registry.yarnpkg.com/minizlib/-/minizlib-2.1.2.tgz#e90d3466ba209b932451508a11ce3d3632145931"
|
||||||
@ -2080,6 +2537,13 @@ minizlib@^2.0.0, minizlib@^2.1.1:
|
|||||||
minipass "^3.0.0"
|
minipass "^3.0.0"
|
||||||
yallist "^4.0.0"
|
yallist "^4.0.0"
|
||||||
|
|
||||||
|
minizlib@^3.1.0:
|
||||||
|
version "3.1.0"
|
||||||
|
resolved "https://registry.yarnpkg.com/minizlib/-/minizlib-3.1.0.tgz#6ad76c3a8f10227c9b51d1c9ac8e30b27f5a251c"
|
||||||
|
integrity sha512-KZxYo1BUkWD2TVFLr0MQoM8vUUigWD3LlD83a/75BqC+4qE0Hb1Vo5v1FgcfaNXvfXzr+5EhQ6ing/CaBijTlw==
|
||||||
|
dependencies:
|
||||||
|
minipass "^7.1.2"
|
||||||
|
|
||||||
mkdirp-classic@^0.5.2, mkdirp-classic@^0.5.3:
|
mkdirp-classic@^0.5.2, mkdirp-classic@^0.5.3:
|
||||||
version "0.5.3"
|
version "0.5.3"
|
||||||
resolved "https://registry.yarnpkg.com/mkdirp-classic/-/mkdirp-classic-0.5.3.tgz#fa10c9115cc6d8865be221ba47ee9bed78601113"
|
resolved "https://registry.yarnpkg.com/mkdirp-classic/-/mkdirp-classic-0.5.3.tgz#fa10c9115cc6d8865be221ba47ee9bed78601113"
|
||||||
@ -2152,6 +2616,11 @@ node-addon-api@^7.0.0:
|
|||||||
resolved "https://registry.yarnpkg.com/node-addon-api/-/node-addon-api-7.1.1.tgz#1aba6693b0f255258a049d621329329322aad558"
|
resolved "https://registry.yarnpkg.com/node-addon-api/-/node-addon-api-7.1.1.tgz#1aba6693b0f255258a049d621329329322aad558"
|
||||||
integrity sha512-5m3bsyrjFWE1xf7nz7YXdN4udnVtXK6/Yfgn5qnahL6bCkf2yKt4k3nuTKAtT4r3IG8JNR2ncsIMdZuAzJjHQQ==
|
integrity sha512-5m3bsyrjFWE1xf7nz7YXdN4udnVtXK6/Yfgn5qnahL6bCkf2yKt4k3nuTKAtT4r3IG8JNR2ncsIMdZuAzJjHQQ==
|
||||||
|
|
||||||
|
node-domexception@^1.0.0:
|
||||||
|
version "1.0.0"
|
||||||
|
resolved "https://registry.yarnpkg.com/node-domexception/-/node-domexception-1.0.0.tgz#6888db46a1f71c0b76b3f7555016b63fe64766e5"
|
||||||
|
integrity sha512-/jKZoMpw0F8GRwl4/eLROPA3cfcXtLApP0QzLmUT/HuPCZWyB7IY9ZrMeKw2O/nFIqPQB3PVM9aYm0F312AXDQ==
|
||||||
|
|
||||||
node-fetch@^2.6.9:
|
node-fetch@^2.6.9:
|
||||||
version "2.7.0"
|
version "2.7.0"
|
||||||
resolved "https://registry.yarnpkg.com/node-fetch/-/node-fetch-2.7.0.tgz#d0f0fa6e3e2dc1d27efcd8ad99d550bda94d187d"
|
resolved "https://registry.yarnpkg.com/node-fetch/-/node-fetch-2.7.0.tgz#d0f0fa6e3e2dc1d27efcd8ad99d550bda94d187d"
|
||||||
@ -2159,6 +2628,15 @@ node-fetch@^2.6.9:
|
|||||||
dependencies:
|
dependencies:
|
||||||
whatwg-url "^5.0.0"
|
whatwg-url "^5.0.0"
|
||||||
|
|
||||||
|
node-fetch@^3.3.2:
|
||||||
|
version "3.3.2"
|
||||||
|
resolved "https://registry.yarnpkg.com/node-fetch/-/node-fetch-3.3.2.tgz#d1e889bacdf733b4ff3b2b243eb7a12866a0b78b"
|
||||||
|
integrity sha512-dRB78srN/l6gqWulah9SrxeYnxeddIG30+GOqK/9OlLVyLg3HPnr6SqOWTWOXKRwC2eGYCkZ59NNuSgvSrpgOA==
|
||||||
|
dependencies:
|
||||||
|
data-uri-to-buffer "^4.0.0"
|
||||||
|
fetch-blob "^3.1.4"
|
||||||
|
formdata-polyfill "^4.0.10"
|
||||||
|
|
||||||
node-gyp@8.x:
|
node-gyp@8.x:
|
||||||
version "8.4.1"
|
version "8.4.1"
|
||||||
resolved "https://registry.yarnpkg.com/node-gyp/-/node-gyp-8.4.1.tgz#3d49308fc31f768180957d6b5746845fbd429937"
|
resolved "https://registry.yarnpkg.com/node-gyp/-/node-gyp-8.4.1.tgz#3d49308fc31f768180957d6b5746845fbd429937"
|
||||||
@ -2217,6 +2695,11 @@ object-keys@^1.1.1:
|
|||||||
resolved "https://registry.yarnpkg.com/object-keys/-/object-keys-1.1.1.tgz#1c47f272df277f3b1daf061677d9c82e2322c60e"
|
resolved "https://registry.yarnpkg.com/object-keys/-/object-keys-1.1.1.tgz#1c47f272df277f3b1daf061677d9c82e2322c60e"
|
||||||
integrity sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA==
|
integrity sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA==
|
||||||
|
|
||||||
|
omggif@^1.0.10:
|
||||||
|
version "1.0.10"
|
||||||
|
resolved "https://registry.yarnpkg.com/omggif/-/omggif-1.0.10.tgz#ddaaf90d4a42f532e9e7cb3a95ecdd47f17c7b19"
|
||||||
|
integrity sha512-LMJTtvgc/nugXj0Vcrrs68Mn2D1r0zf630VNtqtpI1FEO7e+O9FP4gqs9AcnBaSEeoHIPm28u6qgPR0oyEpGSw==
|
||||||
|
|
||||||
on-finished@^2.4.1:
|
on-finished@^2.4.1:
|
||||||
version "2.4.1"
|
version "2.4.1"
|
||||||
resolved "https://registry.yarnpkg.com/on-finished/-/on-finished-2.4.1.tgz#58c8c44116e54845ad57f14ab10b03533184ac3f"
|
resolved "https://registry.yarnpkg.com/on-finished/-/on-finished-2.4.1.tgz#58c8c44116e54845ad57f14ab10b03533184ac3f"
|
||||||
@ -2231,11 +2714,30 @@ once@^1.3.0, once@^1.3.1, once@^1.4.0:
|
|||||||
dependencies:
|
dependencies:
|
||||||
wrappy "1"
|
wrappy "1"
|
||||||
|
|
||||||
|
onnxruntime-common@1.23.2:
|
||||||
|
version "1.23.2"
|
||||||
|
resolved "https://registry.yarnpkg.com/onnxruntime-common/-/onnxruntime-common-1.23.2.tgz#669892f8d2b7de273ac60a76151781a97d01ab49"
|
||||||
|
integrity sha512-5LFsC9Dukzp2WV6kNHYLNzp8sT6V02IubLCbzw2Xd6X5GOlr65gAX6xiJwyi2URJol/s71gaQLC5F2C25AAR2w==
|
||||||
|
|
||||||
|
onnxruntime-node@^1.23.2:
|
||||||
|
version "1.23.2"
|
||||||
|
resolved "https://registry.yarnpkg.com/onnxruntime-node/-/onnxruntime-node-1.23.2.tgz#6e205a32111bf6657ac469f8fc7fb35b765324a5"
|
||||||
|
integrity sha512-OBTsG0W8ddBVOeVVVychpVBS87A9YV5sa2hJ6lc025T97Le+J4v++PwSC4XFs1C62SWyNdof0Mh4KvnZgtt4aw==
|
||||||
|
dependencies:
|
||||||
|
adm-zip "^0.5.16"
|
||||||
|
global-agent "^3.0.0"
|
||||||
|
onnxruntime-common "1.23.2"
|
||||||
|
|
||||||
opencollective-postinstall@^2.0.3:
|
opencollective-postinstall@^2.0.3:
|
||||||
version "2.0.3"
|
version "2.0.3"
|
||||||
resolved "https://registry.yarnpkg.com/opencollective-postinstall/-/opencollective-postinstall-2.0.3.tgz#7a0fff978f6dbfa4d006238fbac98ed4198c3259"
|
resolved "https://registry.yarnpkg.com/opencollective-postinstall/-/opencollective-postinstall-2.0.3.tgz#7a0fff978f6dbfa4d006238fbac98ed4198c3259"
|
||||||
integrity sha512-8AV/sCtuzUeTo8gQK5qDZzARrulB3egtLzFgteqB2tcT4Mw7B8Kt7JcDHmltjz6FOAHsvTevk70gZEbhM4ZS9Q==
|
integrity sha512-8AV/sCtuzUeTo8gQK5qDZzARrulB3egtLzFgteqB2tcT4Mw7B8Kt7JcDHmltjz6FOAHsvTevk70gZEbhM4ZS9Q==
|
||||||
|
|
||||||
|
opencv.js@^1.2.1:
|
||||||
|
version "1.2.1"
|
||||||
|
resolved "https://registry.yarnpkg.com/opencv.js/-/opencv.js-1.2.1.tgz#9e89fd669a749f80574ba5d4615a5ff3e3c09a16"
|
||||||
|
integrity sha512-+ji5Pk3eyz+RRSeZr2kLZNJpjsKoGyPJOjFWng7+Cuq9ylHakBqJMxDGlBW1+qdju3k8DadWOiHJ6JaF28UpKA==
|
||||||
|
|
||||||
p-cancelable@^2.0.0:
|
p-cancelable@^2.0.0:
|
||||||
version "2.1.1"
|
version "2.1.1"
|
||||||
resolved "https://registry.yarnpkg.com/p-cancelable/-/p-cancelable-2.1.1.tgz#aab7fbd416582fa32a3db49859c122487c5ed2cf"
|
resolved "https://registry.yarnpkg.com/p-cancelable/-/p-cancelable-2.1.1.tgz#aab7fbd416582fa32a3db49859c122487c5ed2cf"
|
||||||
@ -2248,6 +2750,29 @@ p-map@^4.0.0:
|
|||||||
dependencies:
|
dependencies:
|
||||||
aggregate-error "^3.0.0"
|
aggregate-error "^3.0.0"
|
||||||
|
|
||||||
|
pako@^1.0.11:
|
||||||
|
version "1.0.11"
|
||||||
|
resolved "https://registry.yarnpkg.com/pako/-/pako-1.0.11.tgz#6c9599d340d54dfd3946380252a35705a6b992bf"
|
||||||
|
integrity sha512-4hLB8Py4zZce5s4yd9XzopqwVv/yGNhV1Bl8NTmCq1763HeK2+EwVTv+leGeL13Dnh2wfbqowVPXCIO0z4taYw==
|
||||||
|
|
||||||
|
parse-bmfont-ascii@^1.0.6:
|
||||||
|
version "1.0.6"
|
||||||
|
resolved "https://registry.yarnpkg.com/parse-bmfont-ascii/-/parse-bmfont-ascii-1.0.6.tgz#11ac3c3ff58f7c2020ab22769079108d4dfa0285"
|
||||||
|
integrity sha512-U4RrVsUFCleIOBsIGYOMKjn9PavsGOXxbvYGtMOEfnId0SVNsgehXh1DxUdVPLoxd5mvcEtvmKs2Mmf0Mpa1ZA==
|
||||||
|
|
||||||
|
parse-bmfont-binary@^1.0.6:
|
||||||
|
version "1.0.6"
|
||||||
|
resolved "https://registry.yarnpkg.com/parse-bmfont-binary/-/parse-bmfont-binary-1.0.6.tgz#d038b476d3e9dd9db1e11a0b0e53a22792b69006"
|
||||||
|
integrity sha512-GxmsRea0wdGdYthjuUeWTMWPqm2+FAd4GI8vCvhgJsFnoGhTrLhXDDupwTo7rXVAgaLIGoVHDZS9p/5XbSqeWA==
|
||||||
|
|
||||||
|
parse-bmfont-xml@^1.1.6:
|
||||||
|
version "1.1.6"
|
||||||
|
resolved "https://registry.yarnpkg.com/parse-bmfont-xml/-/parse-bmfont-xml-1.1.6.tgz#016b655da7aebe6da38c906aca16bf0415773767"
|
||||||
|
integrity sha512-0cEliVMZEhrFDwMh4SxIyVJpqYoOWDJ9P895tFuS+XuNzI5UBmBk5U5O4KuJdTnZpSBI4LFA2+ZiJaiwfSwlMA==
|
||||||
|
dependencies:
|
||||||
|
xml-parse-from-string "^1.0.0"
|
||||||
|
xml2js "^0.5.0"
|
||||||
|
|
||||||
parseurl@^1.3.3:
|
parseurl@^1.3.3:
|
||||||
version "1.3.3"
|
version "1.3.3"
|
||||||
resolved "https://registry.yarnpkg.com/parseurl/-/parseurl-1.3.3.tgz#9da19e7bee8d12dff0513ed5b76957793bc2e8d4"
|
resolved "https://registry.yarnpkg.com/parseurl/-/parseurl-1.3.3.tgz#9da19e7bee8d12dff0513ed5b76957793bc2e8d4"
|
||||||
@ -2273,6 +2798,11 @@ path-to-regexp@^8.0.0:
|
|||||||
resolved "https://registry.yarnpkg.com/path-to-regexp/-/path-to-regexp-8.3.0.tgz#aa818a6981f99321003a08987d3cec9c3474cd1f"
|
resolved "https://registry.yarnpkg.com/path-to-regexp/-/path-to-regexp-8.3.0.tgz#aa818a6981f99321003a08987d3cec9c3474cd1f"
|
||||||
integrity sha512-7jdwVIRtsP8MYpdXSwOS0YdD0Du+qOoF/AEPIt88PcCFrZCzx41oxku1jD88hZBwbNUIEfpqvuhjFaMAqMTWnA==
|
integrity sha512-7jdwVIRtsP8MYpdXSwOS0YdD0Du+qOoF/AEPIt88PcCFrZCzx41oxku1jD88hZBwbNUIEfpqvuhjFaMAqMTWnA==
|
||||||
|
|
||||||
|
peek-readable@^4.1.0:
|
||||||
|
version "4.1.0"
|
||||||
|
resolved "https://registry.yarnpkg.com/peek-readable/-/peek-readable-4.1.0.tgz#4ece1111bf5c2ad8867c314c81356847e8a62e72"
|
||||||
|
integrity sha512-ZI3LnwUv5nOGbQzD9c2iDG6toheuXSZP5esSHBjopsXH4dg19soufvpUGA3uohi5anFtGb2lhAVdHzH6R/Evvg==
|
||||||
|
|
||||||
pend@~1.2.0:
|
pend@~1.2.0:
|
||||||
version "1.2.0"
|
version "1.2.0"
|
||||||
resolved "https://registry.yarnpkg.com/pend/-/pend-1.2.0.tgz#7a57eb550a6783f9115331fcf4663d5c8e007a50"
|
resolved "https://registry.yarnpkg.com/pend/-/pend-1.2.0.tgz#7a57eb550a6783f9115331fcf4663d5c8e007a50"
|
||||||
@ -2288,6 +2818,23 @@ picomatch@^4.0.2, picomatch@^4.0.3:
|
|||||||
resolved "https://registry.yarnpkg.com/picomatch/-/picomatch-4.0.3.tgz#796c76136d1eead715db1e7bad785dedd695a042"
|
resolved "https://registry.yarnpkg.com/picomatch/-/picomatch-4.0.3.tgz#796c76136d1eead715db1e7bad785dedd695a042"
|
||||||
integrity sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==
|
integrity sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==
|
||||||
|
|
||||||
|
pixelmatch@^5.3.0:
|
||||||
|
version "5.3.0"
|
||||||
|
resolved "https://registry.yarnpkg.com/pixelmatch/-/pixelmatch-5.3.0.tgz#5e5321a7abedfb7962d60dbf345deda87cb9560a"
|
||||||
|
integrity sha512-o8mkY4E/+LNUf6LzX96ht6k6CEDi65k9G2rjMtBe9Oo+VPKSvl+0GKHuH/AlG+GA5LPG/i5hrekkxUc3s2HU+Q==
|
||||||
|
dependencies:
|
||||||
|
pngjs "^6.0.0"
|
||||||
|
|
||||||
|
pngjs@^6.0.0:
|
||||||
|
version "6.0.0"
|
||||||
|
resolved "https://registry.yarnpkg.com/pngjs/-/pngjs-6.0.0.tgz#ca9e5d2aa48db0228a52c419c3308e87720da821"
|
||||||
|
integrity sha512-TRzzuFRRmEoSW/p1KVAmiOgPco2Irlah+bGFCeNfJXxxYGwSw7YwAOAcd7X28K/m5bjBWKsC29KyoMfHbypayg==
|
||||||
|
|
||||||
|
pngjs@^7.0.0:
|
||||||
|
version "7.0.0"
|
||||||
|
resolved "https://registry.yarnpkg.com/pngjs/-/pngjs-7.0.0.tgz#a8b7446020ebbc6ac739db6c5415a65d17090e26"
|
||||||
|
integrity sha512-LKWqWJRhstyYo9pGvgor/ivk2w94eSjE3RGVuzLGlr3NmD8bf7RcYGze1mNdEHRP6TRP6rMuDHk5t44hnTRyow==
|
||||||
|
|
||||||
postcss@^8.5.6:
|
postcss@^8.5.6:
|
||||||
version "8.5.6"
|
version "8.5.6"
|
||||||
resolved "https://registry.yarnpkg.com/postcss/-/postcss-8.5.6.tgz#2825006615a619b4f62a9e7426cc120b349a8f3c"
|
resolved "https://registry.yarnpkg.com/postcss/-/postcss-8.5.6.tgz#2825006615a619b4f62a9e7426cc120b349a8f3c"
|
||||||
@ -2315,6 +2862,11 @@ prebuild-install@^7.1.1, prebuild-install@^7.1.3:
|
|||||||
tar-fs "^2.0.0"
|
tar-fs "^2.0.0"
|
||||||
tunnel-agent "^0.6.0"
|
tunnel-agent "^0.6.0"
|
||||||
|
|
||||||
|
process@^0.11.10:
|
||||||
|
version "0.11.10"
|
||||||
|
resolved "https://registry.yarnpkg.com/process/-/process-0.11.10.tgz#7332300e840161bda3e69a1d1d91a7d4bc16f182"
|
||||||
|
integrity sha512-cdGef/drWFoydD1JsMzuFf8100nZl+GT+yacc2bEced5f9Rjk4z+WtFUTBu9PhOi9j/jfmBPu0mMEY4wIdAF8A==
|
||||||
|
|
||||||
progress@^2.0.3:
|
progress@^2.0.3:
|
||||||
version "2.0.3"
|
version "2.0.3"
|
||||||
resolved "https://registry.yarnpkg.com/progress/-/progress-2.0.3.tgz#7e8cf8d8f5b8f239c1bc68beb4eb78567d572ef8"
|
resolved "https://registry.yarnpkg.com/progress/-/progress-2.0.3.tgz#7e8cf8d8f5b8f239c1bc68beb4eb78567d572ef8"
|
||||||
@ -2400,6 +2952,24 @@ readable-stream@^3.0.2, readable-stream@^3.1.1, readable-stream@^3.4.0, readable
|
|||||||
string_decoder "^1.1.1"
|
string_decoder "^1.1.1"
|
||||||
util-deprecate "^1.0.1"
|
util-deprecate "^1.0.1"
|
||||||
|
|
||||||
|
readable-stream@^4.7.0:
|
||||||
|
version "4.7.0"
|
||||||
|
resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-4.7.0.tgz#cedbd8a1146c13dfff8dab14068028d58c15ac91"
|
||||||
|
integrity sha512-oIGGmcpTLwPga8Bn6/Z75SVaH1z5dUut2ibSyAMVhmUggWpmDn2dapB0n7f8nwaSiRtepAsfJyfXIO5DCVAODg==
|
||||||
|
dependencies:
|
||||||
|
abort-controller "^3.0.0"
|
||||||
|
buffer "^6.0.3"
|
||||||
|
events "^3.3.0"
|
||||||
|
process "^0.11.10"
|
||||||
|
string_decoder "^1.3.0"
|
||||||
|
|
||||||
|
readable-web-to-node-stream@^3.0.0:
|
||||||
|
version "3.0.4"
|
||||||
|
resolved "https://registry.yarnpkg.com/readable-web-to-node-stream/-/readable-web-to-node-stream-3.0.4.tgz#392ba37707af5bf62d725c36c1b5d6ef4119eefc"
|
||||||
|
integrity sha512-9nX56alTf5bwXQ3ZDipHJhusu9NTQJ/CVPtb/XHAJCXihZeitfJvIRS4GqQ/mfIoOE3IelHMrpayVrosdHBuLw==
|
||||||
|
dependencies:
|
||||||
|
readable-stream "^4.7.0"
|
||||||
|
|
||||||
regenerator-runtime@^0.13.3:
|
regenerator-runtime@^0.13.3:
|
||||||
version "0.13.11"
|
version "0.13.11"
|
||||||
resolved "https://registry.yarnpkg.com/regenerator-runtime/-/regenerator-runtime-0.13.11.tgz#f6dca3e7ceec20590d07ada785636a90cdca17f9"
|
resolved "https://registry.yarnpkg.com/regenerator-runtime/-/regenerator-runtime-0.13.11.tgz#f6dca3e7ceec20590d07ada785636a90cdca17f9"
|
||||||
@ -2505,6 +3075,11 @@ safe-buffer@5.2.1, safe-buffer@^5.0.1, safe-buffer@~5.2.0:
|
|||||||
resolved "https://registry.yarnpkg.com/safer-buffer/-/safer-buffer-2.1.2.tgz#44fa161b0187b9549dd84bb91802f9bd8385cd6a"
|
resolved "https://registry.yarnpkg.com/safer-buffer/-/safer-buffer-2.1.2.tgz#44fa161b0187b9549dd84bb91802f9bd8385cd6a"
|
||||||
integrity sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==
|
integrity sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==
|
||||||
|
|
||||||
|
sax@>=0.6.0:
|
||||||
|
version "1.4.3"
|
||||||
|
resolved "https://registry.yarnpkg.com/sax/-/sax-1.4.3.tgz#fcebae3b756cdc8428321805f4b70f16ec0ab5db"
|
||||||
|
integrity sha512-yqYn1JhPczigF94DMS+shiDMjDowYO6y9+wB/4WgO0Y19jWYk0lQ4tuG5KI7kj4FTp1wxPj5IFfcrz/s1c3jjQ==
|
||||||
|
|
||||||
semver-compare@^1.0.0:
|
semver-compare@^1.0.0:
|
||||||
version "1.0.0"
|
version "1.0.0"
|
||||||
resolved "https://registry.yarnpkg.com/semver-compare/-/semver-compare-1.0.0.tgz#0dee216a1c941ab37e9efb1788f6afc5ff5537fc"
|
resolved "https://registry.yarnpkg.com/semver-compare/-/semver-compare-1.0.0.tgz#0dee216a1c941ab37e9efb1788f6afc5ff5537fc"
|
||||||
@ -2674,6 +3249,11 @@ simple-get@^4.0.0:
|
|||||||
once "^1.3.1"
|
once "^1.3.1"
|
||||||
simple-concat "^1.0.0"
|
simple-concat "^1.0.0"
|
||||||
|
|
||||||
|
simple-xml-to-json@^1.2.2:
|
||||||
|
version "1.2.3"
|
||||||
|
resolved "https://registry.yarnpkg.com/simple-xml-to-json/-/simple-xml-to-json-1.2.3.tgz#79c7188ff99ae209a267b70ee0db06b0e4597787"
|
||||||
|
integrity sha512-kWJDCr9EWtZ+/EYYM5MareWj2cRnZGF93YDNpH4jQiHB+hBIZnfPFSQiVMzZOdk+zXWqTZ/9fTeQNu2DqeiudA==
|
||||||
|
|
||||||
smart-buffer@^4.2.0:
|
smart-buffer@^4.2.0:
|
||||||
version "4.2.0"
|
version "4.2.0"
|
||||||
resolved "https://registry.yarnpkg.com/smart-buffer/-/smart-buffer-4.2.0.tgz#6e1d71fa4f18c05f7d0ff216dd16a481d0e8d9ae"
|
resolved "https://registry.yarnpkg.com/smart-buffer/-/smart-buffer-4.2.0.tgz#6e1d71fa4f18c05f7d0ff216dd16a481d0e8d9ae"
|
||||||
@ -2754,7 +3334,7 @@ streamsearch@^1.1.0:
|
|||||||
is-fullwidth-code-point "^3.0.0"
|
is-fullwidth-code-point "^3.0.0"
|
||||||
strip-ansi "^6.0.1"
|
strip-ansi "^6.0.1"
|
||||||
|
|
||||||
string_decoder@^1.1.1:
|
string_decoder@^1.1.1, string_decoder@^1.3.0:
|
||||||
version "1.3.0"
|
version "1.3.0"
|
||||||
resolved "https://registry.yarnpkg.com/string_decoder/-/string_decoder-1.3.0.tgz#42f114594a46cf1a8e30b0a84f56c78c3edac21e"
|
resolved "https://registry.yarnpkg.com/string_decoder/-/string_decoder-1.3.0.tgz#42f114594a46cf1a8e30b0a84f56c78c3edac21e"
|
||||||
integrity sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==
|
integrity sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==
|
||||||
@ -2773,6 +3353,14 @@ strip-json-comments@~2.0.1:
|
|||||||
resolved "https://registry.yarnpkg.com/strip-json-comments/-/strip-json-comments-2.0.1.tgz#3c531942e908c2697c0ec344858c286c7ca0a60a"
|
resolved "https://registry.yarnpkg.com/strip-json-comments/-/strip-json-comments-2.0.1.tgz#3c531942e908c2697c0ec344858c286c7ca0a60a"
|
||||||
integrity sha512-4gB8na07fecVVkOI6Rs4e7T6NOTki5EmL7TUduTs6bu3EdnSycntVJ4re8kgZA+wx9IueI2Y11bfbgwtzuE0KQ==
|
integrity sha512-4gB8na07fecVVkOI6Rs4e7T6NOTki5EmL7TUduTs6bu3EdnSycntVJ4re8kgZA+wx9IueI2Y11bfbgwtzuE0KQ==
|
||||||
|
|
||||||
|
strtok3@^6.2.4:
|
||||||
|
version "6.3.0"
|
||||||
|
resolved "https://registry.yarnpkg.com/strtok3/-/strtok3-6.3.0.tgz#358b80ffe6d5d5620e19a073aa78ce947a90f9a0"
|
||||||
|
integrity sha512-fZtbhtvI9I48xDSywd/somNqgUHl2L2cstmXCCif0itOf96jeW18MBSyrLuNicYQVkvpOxkZtkzujiTJ9LW5Jw==
|
||||||
|
dependencies:
|
||||||
|
"@tokenizer/token" "^0.3.0"
|
||||||
|
peek-readable "^4.1.0"
|
||||||
|
|
||||||
sumchecker@^3.0.1:
|
sumchecker@^3.0.1:
|
||||||
version "3.0.1"
|
version "3.0.1"
|
||||||
resolved "https://registry.yarnpkg.com/sumchecker/-/sumchecker-3.0.1.tgz#6377e996795abb0b6d348e9b3e1dfb24345a8e42"
|
resolved "https://registry.yarnpkg.com/sumchecker/-/sumchecker-3.0.1.tgz#6377e996795abb0b6d348e9b3e1dfb24345a8e42"
|
||||||
@ -2827,6 +3415,17 @@ tar@^6.0.2, tar@^6.1.11, tar@^6.1.2:
|
|||||||
mkdirp "^1.0.3"
|
mkdirp "^1.0.3"
|
||||||
yallist "^4.0.0"
|
yallist "^4.0.0"
|
||||||
|
|
||||||
|
tar@^7.4.3:
|
||||||
|
version "7.5.2"
|
||||||
|
resolved "https://registry.yarnpkg.com/tar/-/tar-7.5.2.tgz#115c061495ec51ff3c6745ff8f6d0871c5b1dedc"
|
||||||
|
integrity sha512-7NyxrTE4Anh8km8iEy7o0QYPs+0JKBTj5ZaqHg6B39erLg0qYXN3BijtShwbsNSvQ+LN75+KV+C4QR/f6Gwnpg==
|
||||||
|
dependencies:
|
||||||
|
"@isaacs/fs-minipass" "^4.0.0"
|
||||||
|
chownr "^3.0.0"
|
||||||
|
minipass "^7.1.2"
|
||||||
|
minizlib "^3.1.0"
|
||||||
|
yallist "^5.0.0"
|
||||||
|
|
||||||
tesseract.js-core@^6.0.0:
|
tesseract.js-core@^6.0.0:
|
||||||
version "6.0.0"
|
version "6.0.0"
|
||||||
resolved "https://registry.yarnpkg.com/tesseract.js-core/-/tesseract.js-core-6.0.0.tgz#6f25da94f70f8e8f02aff47a43be61d49e6f67c3"
|
resolved "https://registry.yarnpkg.com/tesseract.js-core/-/tesseract.js-core-6.0.0.tgz#6f25da94f70f8e8f02aff47a43be61d49e6f67c3"
|
||||||
@ -2847,6 +3446,11 @@ tesseract.js@^6.0.1:
|
|||||||
wasm-feature-detect "^1.2.11"
|
wasm-feature-detect "^1.2.11"
|
||||||
zlibjs "^0.3.1"
|
zlibjs "^0.3.1"
|
||||||
|
|
||||||
|
tinycolor2@^1.6.0:
|
||||||
|
version "1.6.0"
|
||||||
|
resolved "https://registry.yarnpkg.com/tinycolor2/-/tinycolor2-1.6.0.tgz#f98007460169b0263b97072c5ae92484ce02d09e"
|
||||||
|
integrity sha512-XPaBkWQJdsf3pLKJV9p4qN/S+fm2Oj8AIPo1BTUhg5oxkvm9+SVEGFdhyOz7tTdUTfvxMiAs4sp6/eZO2Ew+pw==
|
||||||
|
|
||||||
tinyglobby@^0.2.15:
|
tinyglobby@^0.2.15:
|
||||||
version "0.2.15"
|
version "0.2.15"
|
||||||
resolved "https://registry.yarnpkg.com/tinyglobby/-/tinyglobby-0.2.15.tgz#e228dd1e638cea993d2fdb4fcd2d4602a79951c2"
|
resolved "https://registry.yarnpkg.com/tinyglobby/-/tinyglobby-0.2.15.tgz#e228dd1e638cea993d2fdb4fcd2d4602a79951c2"
|
||||||
@ -2860,6 +3464,14 @@ toidentifier@1.0.1:
|
|||||||
resolved "https://registry.yarnpkg.com/toidentifier/-/toidentifier-1.0.1.tgz#3be34321a88a820ed1bd80dfaa33e479fbb8dd35"
|
resolved "https://registry.yarnpkg.com/toidentifier/-/toidentifier-1.0.1.tgz#3be34321a88a820ed1bd80dfaa33e479fbb8dd35"
|
||||||
integrity sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==
|
integrity sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==
|
||||||
|
|
||||||
|
token-types@^4.1.1:
|
||||||
|
version "4.2.1"
|
||||||
|
resolved "https://registry.yarnpkg.com/token-types/-/token-types-4.2.1.tgz#0f897f03665846982806e138977dbe72d44df753"
|
||||||
|
integrity sha512-6udB24Q737UD/SDsKAHI9FCRP7Bqc9D/MQUV02ORQg5iskjtLJlZJNdN4kKtcdtwCeWIwIHDGaUsTsCCAa8sFQ==
|
||||||
|
dependencies:
|
||||||
|
"@tokenizer/token" "^0.3.0"
|
||||||
|
ieee754 "^1.2.1"
|
||||||
|
|
||||||
tr46@~0.0.3:
|
tr46@~0.0.3:
|
||||||
version "0.0.3"
|
version "0.0.3"
|
||||||
resolved "https://registry.yarnpkg.com/tr46/-/tr46-0.0.3.tgz#8184fd347dac9cdc185992f3a6622e14b9d9ab6a"
|
resolved "https://registry.yarnpkg.com/tr46/-/tr46-0.0.3.tgz#8184fd347dac9cdc185992f3a6622e14b9d9ab6a"
|
||||||
@ -2958,6 +3570,13 @@ unpipe@1.0.0:
|
|||||||
resolved "https://registry.yarnpkg.com/unpipe/-/unpipe-1.0.0.tgz#b2bf4ee8514aae6165b4817829d21b2ef49904ec"
|
resolved "https://registry.yarnpkg.com/unpipe/-/unpipe-1.0.0.tgz#b2bf4ee8514aae6165b4817829d21b2ef49904ec"
|
||||||
integrity sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==
|
integrity sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==
|
||||||
|
|
||||||
|
utif2@^4.1.0:
|
||||||
|
version "4.1.0"
|
||||||
|
resolved "https://registry.yarnpkg.com/utif2/-/utif2-4.1.0.tgz#e768d37bd619b995d56d9780b5d2b4611a3d932b"
|
||||||
|
integrity sha512-+oknB9FHrJ7oW7A2WZYajOcv4FcDR4CfoGB0dPNfxbi4GO05RRnFmt5oa23+9w32EanrYcSJWspUiJkLMs+37w==
|
||||||
|
dependencies:
|
||||||
|
pako "^1.0.11"
|
||||||
|
|
||||||
util-deprecate@^1.0.1:
|
util-deprecate@^1.0.1:
|
||||||
version "1.0.2"
|
version "1.0.2"
|
||||||
resolved "https://registry.yarnpkg.com/util-deprecate/-/util-deprecate-1.0.2.tgz#450d4dc9fa70de732762fbd2d4a28981419a0ccf"
|
resolved "https://registry.yarnpkg.com/util-deprecate/-/util-deprecate-1.0.2.tgz#450d4dc9fa70de732762fbd2d4a28981419a0ccf"
|
||||||
@ -3039,6 +3658,11 @@ wasm-feature-detect@^1.2.11:
|
|||||||
resolved "https://registry.yarnpkg.com/wasm-feature-detect/-/wasm-feature-detect-1.8.0.tgz#4e9f55b0a64d801f372fbb0324ed11ad3abd0c78"
|
resolved "https://registry.yarnpkg.com/wasm-feature-detect/-/wasm-feature-detect-1.8.0.tgz#4e9f55b0a64d801f372fbb0324ed11ad3abd0c78"
|
||||||
integrity sha512-zksaLKM2fVlnB5jQQDqKXXwYHLQUVH9es+5TOOHwGOVJOCeRBCiPjwSg+3tN2AdTCzjgli4jijCH290kXb/zWQ==
|
integrity sha512-zksaLKM2fVlnB5jQQDqKXXwYHLQUVH9es+5TOOHwGOVJOCeRBCiPjwSg+3tN2AdTCzjgli4jijCH290kXb/zWQ==
|
||||||
|
|
||||||
|
web-streams-polyfill@^3.0.3:
|
||||||
|
version "3.3.3"
|
||||||
|
resolved "https://registry.yarnpkg.com/web-streams-polyfill/-/web-streams-polyfill-3.3.3.tgz#2073b91a2fdb1fbfbd401e7de0ac9f8214cecb4b"
|
||||||
|
integrity sha512-d2JWLCivmZYTSIoge9MsgFCZrt571BikcWGYkjC1khllbTeDlGqZ2D8vD8E/lJa8WGWbb7Plm8/XJYV7IJHZZw==
|
||||||
|
|
||||||
webidl-conversions@^3.0.0:
|
webidl-conversions@^3.0.0:
|
||||||
version "3.0.1"
|
version "3.0.1"
|
||||||
resolved "https://registry.yarnpkg.com/webidl-conversions/-/webidl-conversions-3.0.1.tgz#24534275e2a7bc6be7bc86611cc16ae0a5654871"
|
resolved "https://registry.yarnpkg.com/webidl-conversions/-/webidl-conversions-3.0.1.tgz#24534275e2a7bc6be7bc86611cc16ae0a5654871"
|
||||||
@ -3080,6 +3704,24 @@ wrappy@1:
|
|||||||
resolved "https://registry.yarnpkg.com/wrappy/-/wrappy-1.0.2.tgz#b5243d8f3ec1aa35f1364605bc0d1036e30ab69f"
|
resolved "https://registry.yarnpkg.com/wrappy/-/wrappy-1.0.2.tgz#b5243d8f3ec1aa35f1364605bc0d1036e30ab69f"
|
||||||
integrity sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==
|
integrity sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==
|
||||||
|
|
||||||
|
xml-parse-from-string@^1.0.0:
|
||||||
|
version "1.0.1"
|
||||||
|
resolved "https://registry.yarnpkg.com/xml-parse-from-string/-/xml-parse-from-string-1.0.1.tgz#a9029e929d3dbcded169f3c6e28238d95a5d5a28"
|
||||||
|
integrity sha512-ErcKwJTF54uRzzNMXq2X5sMIy88zJvfN2DmdoQvy7PAFJ+tPRU6ydWuOKNMyfmOjdyBQTFREi60s0Y0SyI0G0g==
|
||||||
|
|
||||||
|
xml2js@^0.5.0:
|
||||||
|
version "0.5.0"
|
||||||
|
resolved "https://registry.yarnpkg.com/xml2js/-/xml2js-0.5.0.tgz#d9440631fbb2ed800203fad106f2724f62c493b7"
|
||||||
|
integrity sha512-drPFnkQJik/O+uPKpqSgr22mpuFHqKdbS835iAQrUC73L2F5WkboIRd63ai/2Yg6I1jzifPFKH2NTK+cfglkIA==
|
||||||
|
dependencies:
|
||||||
|
sax ">=0.6.0"
|
||||||
|
xmlbuilder "~11.0.0"
|
||||||
|
|
||||||
|
xmlbuilder@~11.0.0:
|
||||||
|
version "11.0.1"
|
||||||
|
resolved "https://registry.yarnpkg.com/xmlbuilder/-/xmlbuilder-11.0.1.tgz#be9bae1c8a046e76b31127726347d0ad7002beb3"
|
||||||
|
integrity sha512-fDlsI/kFEx7gLvbecc0/ohLG50fugQp8ryHzMTuW9vSa1GJ0XYWKnhsUx7oie3G98+r56aTQIUB4kht42R3JvA==
|
||||||
|
|
||||||
xtend@^4.0.2:
|
xtend@^4.0.2:
|
||||||
version "4.0.2"
|
version "4.0.2"
|
||||||
resolved "https://registry.yarnpkg.com/xtend/-/xtend-4.0.2.tgz#bb72779f5fa465186b1f438f674fa347fdb5db54"
|
resolved "https://registry.yarnpkg.com/xtend/-/xtend-4.0.2.tgz#bb72779f5fa465186b1f438f674fa347fdb5db54"
|
||||||
@ -3095,6 +3737,11 @@ yallist@^4.0.0:
|
|||||||
resolved "https://registry.yarnpkg.com/yallist/-/yallist-4.0.0.tgz#9bb92790d9c0effec63be73519e11a35019a3a72"
|
resolved "https://registry.yarnpkg.com/yallist/-/yallist-4.0.0.tgz#9bb92790d9c0effec63be73519e11a35019a3a72"
|
||||||
integrity sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==
|
integrity sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==
|
||||||
|
|
||||||
|
yallist@^5.0.0:
|
||||||
|
version "5.0.0"
|
||||||
|
resolved "https://registry.yarnpkg.com/yallist/-/yallist-5.0.0.tgz#00e2de443639ed0d78fd87de0d27469fbcffb533"
|
||||||
|
integrity sha512-YgvUTfwqyc7UXVMrB+SImsVYSmTS8X/tSrtdNZMImM+n7+QTriRXyXim0mBrTXNeqzVF0KWGgHPeiyViFFrNDw==
|
||||||
|
|
||||||
yargs-parser@^21.1.1:
|
yargs-parser@^21.1.1:
|
||||||
version "21.1.1"
|
version "21.1.1"
|
||||||
resolved "https://registry.yarnpkg.com/yargs-parser/-/yargs-parser-21.1.1.tgz#9096bceebf990d21bb31fa9516e0ede294a77d35"
|
resolved "https://registry.yarnpkg.com/yargs-parser/-/yargs-parser-21.1.1.tgz#9096bceebf990d21bb31fa9516e0ede294a77d35"
|
||||||
@ -3125,3 +3772,8 @@ zlibjs@^0.3.1:
|
|||||||
version "0.3.1"
|
version "0.3.1"
|
||||||
resolved "https://registry.yarnpkg.com/zlibjs/-/zlibjs-0.3.1.tgz#50197edb28a1c42ca659cc8b4e6a9ddd6d444554"
|
resolved "https://registry.yarnpkg.com/zlibjs/-/zlibjs-0.3.1.tgz#50197edb28a1c42ca659cc8b4e6a9ddd6d444554"
|
||||||
integrity sha512-+J9RrgTKOmlxFSDHo0pI1xM6BLVUv+o0ZT9ANtCxGkjIVCCUdx9alUF8Gm+dGLKbkkkidWIHFDZHDMpfITt4+w==
|
integrity sha512-+J9RrgTKOmlxFSDHo0pI1xM6BLVUv+o0ZT9ANtCxGkjIVCCUdx9alUF8Gm+dGLKbkkkidWIHFDZHDMpfITt4+w==
|
||||||
|
|
||||||
|
zod@^3.23.8:
|
||||||
|
version "3.25.76"
|
||||||
|
resolved "https://registry.yarnpkg.com/zod/-/zod-3.25.76.tgz#26841c3f6fd22a6a2760e7ccb719179768471e34"
|
||||||
|
integrity sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ==
|
||||||
|
|||||||
正在加载...
在新工单中引用
屏蔽一个用户