working ocr in ts
This commit is contained in:
parent
2a0f585c4f
commit
2d21ff0d55
@ -3,18 +3,25 @@
|
|||||||
"private": true,
|
"private": true,
|
||||||
"version": "0.0.0",
|
"version": "0.0.0",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
|
"packageManager": "pnpm@9.0.0",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "vite",
|
"dev": "vite",
|
||||||
"build": "vue-tsc -b && vite build",
|
"build": "vue-tsc -b && vite build",
|
||||||
"preview": "vite preview"
|
"preview": "vite preview"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
"@tailwindcss/vite": "^4.1.16",
|
||||||
|
"install": "^0.13.0",
|
||||||
|
"tailwindcss": "^4.1.16",
|
||||||
|
"tesseract.js": "^5.1.1",
|
||||||
"vue": "^3.5.22"
|
"vue": "^3.5.22"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
|
"@mdi/font": "^7.4.47",
|
||||||
"@types/node": "^24.6.0",
|
"@types/node": "^24.6.0",
|
||||||
"@vitejs/plugin-vue": "^6.0.1",
|
"@vitejs/plugin-vue": "^6.0.1",
|
||||||
"@vue/tsconfig": "^0.8.1",
|
"@vue/tsconfig": "^0.8.1",
|
||||||
|
"daisyui": "^5.3.10",
|
||||||
"typescript": "~5.9.3",
|
"typescript": "~5.9.3",
|
||||||
"vite": "^7.1.7",
|
"vite": "^7.1.7",
|
||||||
"vue-tsc": "^3.1.0"
|
"vue-tsc": "^3.1.0"
|
||||||
|
|||||||
@ -8,25 +8,43 @@ importers:
|
|||||||
|
|
||||||
.:
|
.:
|
||||||
dependencies:
|
dependencies:
|
||||||
|
'@tailwindcss/vite':
|
||||||
|
specifier: ^4.1.16
|
||||||
|
version: 4.1.16(vite@7.1.12(@types/node@24.9.2)(jiti@2.6.1)(lightningcss@1.30.2))
|
||||||
|
install:
|
||||||
|
specifier: ^0.13.0
|
||||||
|
version: 0.13.0
|
||||||
|
tailwindcss:
|
||||||
|
specifier: ^4.1.16
|
||||||
|
version: 4.1.16
|
||||||
|
tesseract.js:
|
||||||
|
specifier: ^5.1.1
|
||||||
|
version: 5.1.1
|
||||||
vue:
|
vue:
|
||||||
specifier: ^3.5.22
|
specifier: ^3.5.22
|
||||||
version: 3.5.22(typescript@5.9.3)
|
version: 3.5.22(typescript@5.9.3)
|
||||||
devDependencies:
|
devDependencies:
|
||||||
|
'@mdi/font':
|
||||||
|
specifier: ^7.4.47
|
||||||
|
version: 7.4.47
|
||||||
'@types/node':
|
'@types/node':
|
||||||
specifier: ^24.6.0
|
specifier: ^24.6.0
|
||||||
version: 24.9.2
|
version: 24.9.2
|
||||||
'@vitejs/plugin-vue':
|
'@vitejs/plugin-vue':
|
||||||
specifier: ^6.0.1
|
specifier: ^6.0.1
|
||||||
version: 6.0.1(vite@7.1.12(@types/node@24.9.2))(vue@3.5.22(typescript@5.9.3))
|
version: 6.0.1(vite@7.1.12(@types/node@24.9.2)(jiti@2.6.1)(lightningcss@1.30.2))(vue@3.5.22(typescript@5.9.3))
|
||||||
'@vue/tsconfig':
|
'@vue/tsconfig':
|
||||||
specifier: ^0.8.1
|
specifier: ^0.8.1
|
||||||
version: 0.8.1(typescript@5.9.3)(vue@3.5.22(typescript@5.9.3))
|
version: 0.8.1(typescript@5.9.3)(vue@3.5.22(typescript@5.9.3))
|
||||||
|
daisyui:
|
||||||
|
specifier: ^5.3.10
|
||||||
|
version: 5.3.10
|
||||||
typescript:
|
typescript:
|
||||||
specifier: ~5.9.3
|
specifier: ~5.9.3
|
||||||
version: 5.9.3
|
version: 5.9.3
|
||||||
vite:
|
vite:
|
||||||
specifier: ^7.1.7
|
specifier: ^7.1.7
|
||||||
version: 7.1.12(@types/node@24.9.2)
|
version: 7.1.12(@types/node@24.9.2)(jiti@2.6.1)(lightningcss@1.30.2)
|
||||||
vue-tsc:
|
vue-tsc:
|
||||||
specifier: ^3.1.0
|
specifier: ^3.1.0
|
||||||
version: 3.1.2(typescript@5.9.3)
|
version: 3.1.2(typescript@5.9.3)
|
||||||
@ -206,9 +224,25 @@ packages:
|
|||||||
cpu: [x64]
|
cpu: [x64]
|
||||||
os: [win32]
|
os: [win32]
|
||||||
|
|
||||||
|
'@jridgewell/gen-mapping@0.3.13':
|
||||||
|
resolution: {integrity: sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==}
|
||||||
|
|
||||||
|
'@jridgewell/remapping@2.3.5':
|
||||||
|
resolution: {integrity: sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ==}
|
||||||
|
|
||||||
|
'@jridgewell/resolve-uri@3.1.2':
|
||||||
|
resolution: {integrity: sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==}
|
||||||
|
engines: {node: '>=6.0.0'}
|
||||||
|
|
||||||
'@jridgewell/sourcemap-codec@1.5.5':
|
'@jridgewell/sourcemap-codec@1.5.5':
|
||||||
resolution: {integrity: sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==}
|
resolution: {integrity: sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==}
|
||||||
|
|
||||||
|
'@jridgewell/trace-mapping@0.3.31':
|
||||||
|
resolution: {integrity: sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==}
|
||||||
|
|
||||||
|
'@mdi/font@7.4.47':
|
||||||
|
resolution: {integrity: sha512-43MtGpd585SNzHZPcYowu/84Vz2a2g31TvPMTm9uTiCSWzaheQySUcSyUH/46fPnuPQWof2yd0pGBtzee/IQWw==}
|
||||||
|
|
||||||
'@rolldown/pluginutils@1.0.0-beta.29':
|
'@rolldown/pluginutils@1.0.0-beta.29':
|
||||||
resolution: {integrity: sha512-NIJgOsMjbxAXvoGq/X0gD7VPMQ8j9g0BiDaNjVNVjvl+iKXxL3Jre0v31RmBYeLEmkbj2s02v8vFTbUXi5XS2Q==}
|
resolution: {integrity: sha512-NIJgOsMjbxAXvoGq/X0gD7VPMQ8j9g0BiDaNjVNVjvl+iKXxL3Jre0v31RmBYeLEmkbj2s02v8vFTbUXi5XS2Q==}
|
||||||
|
|
||||||
@ -322,6 +356,96 @@ packages:
|
|||||||
cpu: [x64]
|
cpu: [x64]
|
||||||
os: [win32]
|
os: [win32]
|
||||||
|
|
||||||
|
'@tailwindcss/node@4.1.16':
|
||||||
|
resolution: {integrity: sha512-BX5iaSsloNuvKNHRN3k2RcCuTEgASTo77mofW0vmeHkfrDWaoFAFvNHpEgtu0eqyypcyiBkDWzSMxJhp3AUVcw==}
|
||||||
|
|
||||||
|
'@tailwindcss/oxide-android-arm64@4.1.16':
|
||||||
|
resolution: {integrity: sha512-8+ctzkjHgwDJ5caq9IqRSgsP70xhdhJvm+oueS/yhD5ixLhqTw9fSL1OurzMUhBwE5zK26FXLCz2f/RtkISqHA==}
|
||||||
|
engines: {node: '>= 10'}
|
||||||
|
cpu: [arm64]
|
||||||
|
os: [android]
|
||||||
|
|
||||||
|
'@tailwindcss/oxide-darwin-arm64@4.1.16':
|
||||||
|
resolution: {integrity: sha512-C3oZy5042v2FOALBZtY0JTDnGNdS6w7DxL/odvSny17ORUnaRKhyTse8xYi3yKGyfnTUOdavRCdmc8QqJYwFKA==}
|
||||||
|
engines: {node: '>= 10'}
|
||||||
|
cpu: [arm64]
|
||||||
|
os: [darwin]
|
||||||
|
|
||||||
|
'@tailwindcss/oxide-darwin-x64@4.1.16':
|
||||||
|
resolution: {integrity: sha512-vjrl/1Ub9+JwU6BP0emgipGjowzYZMjbWCDqwA2Z4vCa+HBSpP4v6U2ddejcHsolsYxwL5r4bPNoamlV0xDdLg==}
|
||||||
|
engines: {node: '>= 10'}
|
||||||
|
cpu: [x64]
|
||||||
|
os: [darwin]
|
||||||
|
|
||||||
|
'@tailwindcss/oxide-freebsd-x64@4.1.16':
|
||||||
|
resolution: {integrity: sha512-TSMpPYpQLm+aR1wW5rKuUuEruc/oOX3C7H0BTnPDn7W/eMw8W+MRMpiypKMkXZfwH8wqPIRKppuZoedTtNj2tg==}
|
||||||
|
engines: {node: '>= 10'}
|
||||||
|
cpu: [x64]
|
||||||
|
os: [freebsd]
|
||||||
|
|
||||||
|
'@tailwindcss/oxide-linux-arm-gnueabihf@4.1.16':
|
||||||
|
resolution: {integrity: sha512-p0GGfRg/w0sdsFKBjMYvvKIiKy/LNWLWgV/plR4lUgrsxFAoQBFrXkZ4C0w8IOXfslB9vHK/JGASWD2IefIpvw==}
|
||||||
|
engines: {node: '>= 10'}
|
||||||
|
cpu: [arm]
|
||||||
|
os: [linux]
|
||||||
|
|
||||||
|
'@tailwindcss/oxide-linux-arm64-gnu@4.1.16':
|
||||||
|
resolution: {integrity: sha512-DoixyMmTNO19rwRPdqviTrG1rYzpxgyYJl8RgQvdAQUzxC1ToLRqtNJpU/ATURSKgIg6uerPw2feW0aS8SNr/w==}
|
||||||
|
engines: {node: '>= 10'}
|
||||||
|
cpu: [arm64]
|
||||||
|
os: [linux]
|
||||||
|
|
||||||
|
'@tailwindcss/oxide-linux-arm64-musl@4.1.16':
|
||||||
|
resolution: {integrity: sha512-H81UXMa9hJhWhaAUca6bU2wm5RRFpuHImrwXBUvPbYb+3jo32I9VIwpOX6hms0fPmA6f2pGVlybO6qU8pF4fzQ==}
|
||||||
|
engines: {node: '>= 10'}
|
||||||
|
cpu: [arm64]
|
||||||
|
os: [linux]
|
||||||
|
|
||||||
|
'@tailwindcss/oxide-linux-x64-gnu@4.1.16':
|
||||||
|
resolution: {integrity: sha512-ZGHQxDtFC2/ruo7t99Qo2TTIvOERULPl5l0K1g0oK6b5PGqjYMga+FcY1wIUnrUxY56h28FxybtDEla+ICOyew==}
|
||||||
|
engines: {node: '>= 10'}
|
||||||
|
cpu: [x64]
|
||||||
|
os: [linux]
|
||||||
|
|
||||||
|
'@tailwindcss/oxide-linux-x64-musl@4.1.16':
|
||||||
|
resolution: {integrity: sha512-Oi1tAaa0rcKf1Og9MzKeINZzMLPbhxvm7rno5/zuP1WYmpiG0bEHq4AcRUiG2165/WUzvxkW4XDYCscZWbTLZw==}
|
||||||
|
engines: {node: '>= 10'}
|
||||||
|
cpu: [x64]
|
||||||
|
os: [linux]
|
||||||
|
|
||||||
|
'@tailwindcss/oxide-wasm32-wasi@4.1.16':
|
||||||
|
resolution: {integrity: sha512-B01u/b8LteGRwucIBmCQ07FVXLzImWESAIMcUU6nvFt/tYsQ6IHz8DmZ5KtvmwxD+iTYBtM1xwoGXswnlu9v0Q==}
|
||||||
|
engines: {node: '>=14.0.0'}
|
||||||
|
cpu: [wasm32]
|
||||||
|
bundledDependencies:
|
||||||
|
- '@napi-rs/wasm-runtime'
|
||||||
|
- '@emnapi/core'
|
||||||
|
- '@emnapi/runtime'
|
||||||
|
- '@tybys/wasm-util'
|
||||||
|
- '@emnapi/wasi-threads'
|
||||||
|
- tslib
|
||||||
|
|
||||||
|
'@tailwindcss/oxide-win32-arm64-msvc@4.1.16':
|
||||||
|
resolution: {integrity: sha512-zX+Q8sSkGj6HKRTMJXuPvOcP8XfYON24zJBRPlszcH1Np7xuHXhWn8qfFjIujVzvH3BHU+16jBXwgpl20i+v9A==}
|
||||||
|
engines: {node: '>= 10'}
|
||||||
|
cpu: [arm64]
|
||||||
|
os: [win32]
|
||||||
|
|
||||||
|
'@tailwindcss/oxide-win32-x64-msvc@4.1.16':
|
||||||
|
resolution: {integrity: sha512-m5dDFJUEejbFqP+UXVstd4W/wnxA4F61q8SoL+mqTypId2T2ZpuxosNSgowiCnLp2+Z+rivdU0AqpfgiD7yCBg==}
|
||||||
|
engines: {node: '>= 10'}
|
||||||
|
cpu: [x64]
|
||||||
|
os: [win32]
|
||||||
|
|
||||||
|
'@tailwindcss/oxide@4.1.16':
|
||||||
|
resolution: {integrity: sha512-2OSv52FRuhdlgyOQqgtQHuCgXnS8nFSYRp2tJ+4WZXKgTxqPy7SMSls8c3mPT5pkZ17SBToGM5LHEJBO7miEdg==}
|
||||||
|
engines: {node: '>= 10'}
|
||||||
|
|
||||||
|
'@tailwindcss/vite@4.1.16':
|
||||||
|
resolution: {integrity: sha512-bbguNBcDxsRmi9nnlWJxhfDWamY3lmcyACHcdO1crxfzuLpOhHLLtEIN/nCbbAtj5rchUgQD17QVAKi1f7IsKg==}
|
||||||
|
peerDependencies:
|
||||||
|
vite: ^5.2.0 || ^6 || ^7
|
||||||
|
|
||||||
'@types/estree@1.0.8':
|
'@types/estree@1.0.8':
|
||||||
resolution: {integrity: sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==}
|
resolution: {integrity: sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==}
|
||||||
|
|
||||||
@ -395,9 +519,23 @@ packages:
|
|||||||
alien-signals@3.0.3:
|
alien-signals@3.0.3:
|
||||||
resolution: {integrity: sha512-2JXjom6R7ZwrISpUphLhf4htUq1aKRCennTJ6u9kFfr3sLmC9+I4CxxVi+McoFnIg+p1HnVrfLT/iCt4Dlz//Q==}
|
resolution: {integrity: sha512-2JXjom6R7ZwrISpUphLhf4htUq1aKRCennTJ6u9kFfr3sLmC9+I4CxxVi+McoFnIg+p1HnVrfLT/iCt4Dlz//Q==}
|
||||||
|
|
||||||
|
bmp-js@0.1.0:
|
||||||
|
resolution: {integrity: sha512-vHdS19CnY3hwiNdkaqk93DvjVLfbEcI8mys4UjuWrlX1haDmroo8o4xCzh4wD6DGV6HxRCyauwhHRqMTfERtjw==}
|
||||||
|
|
||||||
csstype@3.1.3:
|
csstype@3.1.3:
|
||||||
resolution: {integrity: sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==}
|
resolution: {integrity: sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==}
|
||||||
|
|
||||||
|
daisyui@5.3.10:
|
||||||
|
resolution: {integrity: sha512-vmjyPmm0hvFhA95KB6uiGmWakziB2pBv6CUcs5Ka/3iMBMn9S+C3SZYx9G9l2JrgTZ1EFn61F/HrPcwaUm2kLQ==}
|
||||||
|
|
||||||
|
detect-libc@2.1.2:
|
||||||
|
resolution: {integrity: sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==}
|
||||||
|
engines: {node: '>=8'}
|
||||||
|
|
||||||
|
enhanced-resolve@5.18.3:
|
||||||
|
resolution: {integrity: sha512-d4lC8xfavMeBjzGr2vECC3fsGXziXZQyJxD868h2M/mBI3PwAuODxAkLkq5HYuvrPYcUtiLzsTo8U3PgX3Ocww==}
|
||||||
|
engines: {node: '>=10.13.0'}
|
||||||
|
|
||||||
entities@4.5.0:
|
entities@4.5.0:
|
||||||
resolution: {integrity: sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==}
|
resolution: {integrity: sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==}
|
||||||
engines: {node: '>=0.12'}
|
engines: {node: '>=0.12'}
|
||||||
@ -424,6 +562,96 @@ packages:
|
|||||||
engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0}
|
engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0}
|
||||||
os: [darwin]
|
os: [darwin]
|
||||||
|
|
||||||
|
graceful-fs@4.2.11:
|
||||||
|
resolution: {integrity: sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==}
|
||||||
|
|
||||||
|
idb-keyval@6.2.2:
|
||||||
|
resolution: {integrity: sha512-yjD9nARJ/jb1g+CvD0tlhUHOrJ9Sy0P8T9MF3YaLlHnSRpwPfpTX0XIvpmw3gAJUmEu3FiICLBDPXVwyEvrleg==}
|
||||||
|
|
||||||
|
install@0.13.0:
|
||||||
|
resolution: {integrity: sha512-zDml/jzr2PKU9I8J/xyZBQn8rPCAY//UOYNmR01XwNwyfhEWObo2SWfSl1+0tm1u6PhxLwDnfsT/6jB7OUxqFA==}
|
||||||
|
engines: {node: '>= 0.10'}
|
||||||
|
|
||||||
|
is-electron@2.2.2:
|
||||||
|
resolution: {integrity: sha512-FO/Rhvz5tuw4MCWkpMzHFKWD2LsfHzIb7i6MdPYZ/KW7AlxawyLkqdy+jPZP1WubqEADE3O4FUENlJHDfQASRg==}
|
||||||
|
|
||||||
|
is-url@1.2.4:
|
||||||
|
resolution: {integrity: sha512-ITvGim8FhRiYe4IQ5uHSkj7pVaPDrCTkNd3yq3cV7iZAcJdHTUMPMEHcqSOy9xZ9qFenQCvi+2wjH9a1nXqHww==}
|
||||||
|
|
||||||
|
jiti@2.6.1:
|
||||||
|
resolution: {integrity: sha512-ekilCSN1jwRvIbgeg/57YFh8qQDNbwDb9xT/qu2DAHbFFZUicIl4ygVaAvzveMhMVr3LnpSKTNnwt8PoOfmKhQ==}
|
||||||
|
hasBin: true
|
||||||
|
|
||||||
|
lightningcss-android-arm64@1.30.2:
|
||||||
|
resolution: {integrity: sha512-BH9sEdOCahSgmkVhBLeU7Hc9DWeZ1Eb6wNS6Da8igvUwAe0sqROHddIlvU06q3WyXVEOYDZ6ykBZQnjTbmo4+A==}
|
||||||
|
engines: {node: '>= 12.0.0'}
|
||||||
|
cpu: [arm64]
|
||||||
|
os: [android]
|
||||||
|
|
||||||
|
lightningcss-darwin-arm64@1.30.2:
|
||||||
|
resolution: {integrity: sha512-ylTcDJBN3Hp21TdhRT5zBOIi73P6/W0qwvlFEk22fkdXchtNTOU4Qc37SkzV+EKYxLouZ6M4LG9NfZ1qkhhBWA==}
|
||||||
|
engines: {node: '>= 12.0.0'}
|
||||||
|
cpu: [arm64]
|
||||||
|
os: [darwin]
|
||||||
|
|
||||||
|
lightningcss-darwin-x64@1.30.2:
|
||||||
|
resolution: {integrity: sha512-oBZgKchomuDYxr7ilwLcyms6BCyLn0z8J0+ZZmfpjwg9fRVZIR5/GMXd7r9RH94iDhld3UmSjBM6nXWM2TfZTQ==}
|
||||||
|
engines: {node: '>= 12.0.0'}
|
||||||
|
cpu: [x64]
|
||||||
|
os: [darwin]
|
||||||
|
|
||||||
|
lightningcss-freebsd-x64@1.30.2:
|
||||||
|
resolution: {integrity: sha512-c2bH6xTrf4BDpK8MoGG4Bd6zAMZDAXS569UxCAGcA7IKbHNMlhGQ89eRmvpIUGfKWNVdbhSbkQaWhEoMGmGslA==}
|
||||||
|
engines: {node: '>= 12.0.0'}
|
||||||
|
cpu: [x64]
|
||||||
|
os: [freebsd]
|
||||||
|
|
||||||
|
lightningcss-linux-arm-gnueabihf@1.30.2:
|
||||||
|
resolution: {integrity: sha512-eVdpxh4wYcm0PofJIZVuYuLiqBIakQ9uFZmipf6LF/HRj5Bgm0eb3qL/mr1smyXIS1twwOxNWndd8z0E374hiA==}
|
||||||
|
engines: {node: '>= 12.0.0'}
|
||||||
|
cpu: [arm]
|
||||||
|
os: [linux]
|
||||||
|
|
||||||
|
lightningcss-linux-arm64-gnu@1.30.2:
|
||||||
|
resolution: {integrity: sha512-UK65WJAbwIJbiBFXpxrbTNArtfuznvxAJw4Q2ZGlU8kPeDIWEX1dg3rn2veBVUylA2Ezg89ktszWbaQnxD/e3A==}
|
||||||
|
engines: {node: '>= 12.0.0'}
|
||||||
|
cpu: [arm64]
|
||||||
|
os: [linux]
|
||||||
|
|
||||||
|
lightningcss-linux-arm64-musl@1.30.2:
|
||||||
|
resolution: {integrity: sha512-5Vh9dGeblpTxWHpOx8iauV02popZDsCYMPIgiuw97OJ5uaDsL86cnqSFs5LZkG3ghHoX5isLgWzMs+eD1YzrnA==}
|
||||||
|
engines: {node: '>= 12.0.0'}
|
||||||
|
cpu: [arm64]
|
||||||
|
os: [linux]
|
||||||
|
|
||||||
|
lightningcss-linux-x64-gnu@1.30.2:
|
||||||
|
resolution: {integrity: sha512-Cfd46gdmj1vQ+lR6VRTTadNHu6ALuw2pKR9lYq4FnhvgBc4zWY1EtZcAc6EffShbb1MFrIPfLDXD6Xprbnni4w==}
|
||||||
|
engines: {node: '>= 12.0.0'}
|
||||||
|
cpu: [x64]
|
||||||
|
os: [linux]
|
||||||
|
|
||||||
|
lightningcss-linux-x64-musl@1.30.2:
|
||||||
|
resolution: {integrity: sha512-XJaLUUFXb6/QG2lGIW6aIk6jKdtjtcffUT0NKvIqhSBY3hh9Ch+1LCeH80dR9q9LBjG3ewbDjnumefsLsP6aiA==}
|
||||||
|
engines: {node: '>= 12.0.0'}
|
||||||
|
cpu: [x64]
|
||||||
|
os: [linux]
|
||||||
|
|
||||||
|
lightningcss-win32-arm64-msvc@1.30.2:
|
||||||
|
resolution: {integrity: sha512-FZn+vaj7zLv//D/192WFFVA0RgHawIcHqLX9xuWiQt7P0PtdFEVaxgF9rjM/IRYHQXNnk61/H/gb2Ei+kUQ4xQ==}
|
||||||
|
engines: {node: '>= 12.0.0'}
|
||||||
|
cpu: [arm64]
|
||||||
|
os: [win32]
|
||||||
|
|
||||||
|
lightningcss-win32-x64-msvc@1.30.2:
|
||||||
|
resolution: {integrity: sha512-5g1yc73p+iAkid5phb4oVFMB45417DkRevRbt/El/gKXJk4jid+vPFF/AXbxn05Aky8PapwzZrdJShv5C0avjw==}
|
||||||
|
engines: {node: '>= 12.0.0'}
|
||||||
|
cpu: [x64]
|
||||||
|
os: [win32]
|
||||||
|
|
||||||
|
lightningcss@1.30.2:
|
||||||
|
resolution: {integrity: sha512-utfs7Pr5uJyyvDETitgsaqSyjCb2qNRAtuqUeWIAKztsOYdcACf2KtARYXg2pSvhkt+9NfoaNY7fxjl6nuMjIQ==}
|
||||||
|
engines: {node: '>= 12.0.0'}
|
||||||
|
|
||||||
magic-string@0.30.21:
|
magic-string@0.30.21:
|
||||||
resolution: {integrity: sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==}
|
resolution: {integrity: sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==}
|
||||||
|
|
||||||
@ -435,6 +663,19 @@ packages:
|
|||||||
engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1}
|
engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1}
|
||||||
hasBin: true
|
hasBin: true
|
||||||
|
|
||||||
|
node-fetch@2.7.0:
|
||||||
|
resolution: {integrity: sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==}
|
||||||
|
engines: {node: 4.x || >=6.0.0}
|
||||||
|
peerDependencies:
|
||||||
|
encoding: ^0.1.0
|
||||||
|
peerDependenciesMeta:
|
||||||
|
encoding:
|
||||||
|
optional: true
|
||||||
|
|
||||||
|
opencollective-postinstall@2.0.3:
|
||||||
|
resolution: {integrity: sha512-8AV/sCtuzUeTo8gQK5qDZzARrulB3egtLzFgteqB2tcT4Mw7B8Kt7JcDHmltjz6FOAHsvTevk70gZEbhM4ZS9Q==}
|
||||||
|
hasBin: true
|
||||||
|
|
||||||
path-browserify@1.0.1:
|
path-browserify@1.0.1:
|
||||||
resolution: {integrity: sha512-b7uo2UCUOYZcnF/3ID0lulOJi/bafxa1xPe7ZPsammBSpjSWQkjNxlt635YGS2MiR9GjvuXCtz2emr3jbsz98g==}
|
resolution: {integrity: sha512-b7uo2UCUOYZcnF/3ID0lulOJi/bafxa1xPe7ZPsammBSpjSWQkjNxlt635YGS2MiR9GjvuXCtz2emr3jbsz98g==}
|
||||||
|
|
||||||
@ -449,6 +690,9 @@ packages:
|
|||||||
resolution: {integrity: sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg==}
|
resolution: {integrity: sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg==}
|
||||||
engines: {node: ^10 || ^12 || >=14}
|
engines: {node: ^10 || ^12 || >=14}
|
||||||
|
|
||||||
|
regenerator-runtime@0.13.11:
|
||||||
|
resolution: {integrity: sha512-kY1AZVr2Ra+t+piVaJ4gxaFaReZVH40AKNo7UCX6W+dEwBo/2oZJzqfuN1qLq1oL45o56cPaTXELwrTh8Fpggg==}
|
||||||
|
|
||||||
rollup@4.52.5:
|
rollup@4.52.5:
|
||||||
resolution: {integrity: sha512-3GuObel8h7Kqdjt0gxkEzaifHTqLVW56Y/bjN7PSQtkKr0w3V/QYSdt6QWYtd7A1xUtYQigtdUfgj1RvWVtorw==}
|
resolution: {integrity: sha512-3GuObel8h7Kqdjt0gxkEzaifHTqLVW56Y/bjN7PSQtkKr0w3V/QYSdt6QWYtd7A1xUtYQigtdUfgj1RvWVtorw==}
|
||||||
engines: {node: '>=18.0.0', npm: '>=8.0.0'}
|
engines: {node: '>=18.0.0', npm: '>=8.0.0'}
|
||||||
@ -458,10 +702,26 @@ packages:
|
|||||||
resolution: {integrity: sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==}
|
resolution: {integrity: sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==}
|
||||||
engines: {node: '>=0.10.0'}
|
engines: {node: '>=0.10.0'}
|
||||||
|
|
||||||
|
tailwindcss@4.1.16:
|
||||||
|
resolution: {integrity: sha512-pONL5awpaQX4LN5eiv7moSiSPd/DLDzKVRJz8Q9PgzmAdd1R4307GQS2ZpfiN7ZmekdQrfhZZiSE5jkLR4WNaA==}
|
||||||
|
|
||||||
|
tapable@2.3.0:
|
||||||
|
resolution: {integrity: sha512-g9ljZiwki/LfxmQADO3dEY1CbpmXT5Hm2fJ+QaGKwSXUylMybePR7/67YW7jOrrvjEgL1Fmz5kzyAjWVWLlucg==}
|
||||||
|
engines: {node: '>=6'}
|
||||||
|
|
||||||
|
tesseract.js-core@5.1.1:
|
||||||
|
resolution: {integrity: sha512-KX3bYSU5iGcO1XJa+QGPbi+Zjo2qq6eBhNjSGR5E5q0JtzkoipJKOUQD7ph8kFyteCEfEQ0maWLu8MCXtvX5uQ==}
|
||||||
|
|
||||||
|
tesseract.js@5.1.1:
|
||||||
|
resolution: {integrity: sha512-lzVl/Ar3P3zhpUT31NjqeCo1f+D5+YfpZ5J62eo2S14QNVOmHBTtbchHm/YAbOOOzCegFnKf4B3Qih9LuldcYQ==}
|
||||||
|
|
||||||
tinyglobby@0.2.15:
|
tinyglobby@0.2.15:
|
||||||
resolution: {integrity: sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ==}
|
resolution: {integrity: sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ==}
|
||||||
engines: {node: '>=12.0.0'}
|
engines: {node: '>=12.0.0'}
|
||||||
|
|
||||||
|
tr46@0.0.3:
|
||||||
|
resolution: {integrity: sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==}
|
||||||
|
|
||||||
typescript@5.9.3:
|
typescript@5.9.3:
|
||||||
resolution: {integrity: sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==}
|
resolution: {integrity: sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==}
|
||||||
engines: {node: '>=14.17'}
|
engines: {node: '>=14.17'}
|
||||||
@ -527,6 +787,18 @@ packages:
|
|||||||
typescript:
|
typescript:
|
||||||
optional: true
|
optional: true
|
||||||
|
|
||||||
|
wasm-feature-detect@1.8.0:
|
||||||
|
resolution: {integrity: sha512-zksaLKM2fVlnB5jQQDqKXXwYHLQUVH9es+5TOOHwGOVJOCeRBCiPjwSg+3tN2AdTCzjgli4jijCH290kXb/zWQ==}
|
||||||
|
|
||||||
|
webidl-conversions@3.0.1:
|
||||||
|
resolution: {integrity: sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==}
|
||||||
|
|
||||||
|
whatwg-url@5.0.0:
|
||||||
|
resolution: {integrity: sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==}
|
||||||
|
|
||||||
|
zlibjs@0.3.1:
|
||||||
|
resolution: {integrity: sha512-+J9RrgTKOmlxFSDHo0pI1xM6BLVUv+o0ZT9ANtCxGkjIVCCUdx9alUF8Gm+dGLKbkkkidWIHFDZHDMpfITt4+w==}
|
||||||
|
|
||||||
snapshots:
|
snapshots:
|
||||||
|
|
||||||
'@babel/helper-string-parser@7.27.1': {}
|
'@babel/helper-string-parser@7.27.1': {}
|
||||||
@ -620,8 +892,27 @@ snapshots:
|
|||||||
'@esbuild/win32-x64@0.25.11':
|
'@esbuild/win32-x64@0.25.11':
|
||||||
optional: true
|
optional: true
|
||||||
|
|
||||||
|
'@jridgewell/gen-mapping@0.3.13':
|
||||||
|
dependencies:
|
||||||
|
'@jridgewell/sourcemap-codec': 1.5.5
|
||||||
|
'@jridgewell/trace-mapping': 0.3.31
|
||||||
|
|
||||||
|
'@jridgewell/remapping@2.3.5':
|
||||||
|
dependencies:
|
||||||
|
'@jridgewell/gen-mapping': 0.3.13
|
||||||
|
'@jridgewell/trace-mapping': 0.3.31
|
||||||
|
|
||||||
|
'@jridgewell/resolve-uri@3.1.2': {}
|
||||||
|
|
||||||
'@jridgewell/sourcemap-codec@1.5.5': {}
|
'@jridgewell/sourcemap-codec@1.5.5': {}
|
||||||
|
|
||||||
|
'@jridgewell/trace-mapping@0.3.31':
|
||||||
|
dependencies:
|
||||||
|
'@jridgewell/resolve-uri': 3.1.2
|
||||||
|
'@jridgewell/sourcemap-codec': 1.5.5
|
||||||
|
|
||||||
|
'@mdi/font@7.4.47': {}
|
||||||
|
|
||||||
'@rolldown/pluginutils@1.0.0-beta.29': {}
|
'@rolldown/pluginutils@1.0.0-beta.29': {}
|
||||||
|
|
||||||
'@rollup/rollup-android-arm-eabi@4.52.5':
|
'@rollup/rollup-android-arm-eabi@4.52.5':
|
||||||
@ -690,16 +981,84 @@ snapshots:
|
|||||||
'@rollup/rollup-win32-x64-msvc@4.52.5':
|
'@rollup/rollup-win32-x64-msvc@4.52.5':
|
||||||
optional: true
|
optional: true
|
||||||
|
|
||||||
|
'@tailwindcss/node@4.1.16':
|
||||||
|
dependencies:
|
||||||
|
'@jridgewell/remapping': 2.3.5
|
||||||
|
enhanced-resolve: 5.18.3
|
||||||
|
jiti: 2.6.1
|
||||||
|
lightningcss: 1.30.2
|
||||||
|
magic-string: 0.30.21
|
||||||
|
source-map-js: 1.2.1
|
||||||
|
tailwindcss: 4.1.16
|
||||||
|
|
||||||
|
'@tailwindcss/oxide-android-arm64@4.1.16':
|
||||||
|
optional: true
|
||||||
|
|
||||||
|
'@tailwindcss/oxide-darwin-arm64@4.1.16':
|
||||||
|
optional: true
|
||||||
|
|
||||||
|
'@tailwindcss/oxide-darwin-x64@4.1.16':
|
||||||
|
optional: true
|
||||||
|
|
||||||
|
'@tailwindcss/oxide-freebsd-x64@4.1.16':
|
||||||
|
optional: true
|
||||||
|
|
||||||
|
'@tailwindcss/oxide-linux-arm-gnueabihf@4.1.16':
|
||||||
|
optional: true
|
||||||
|
|
||||||
|
'@tailwindcss/oxide-linux-arm64-gnu@4.1.16':
|
||||||
|
optional: true
|
||||||
|
|
||||||
|
'@tailwindcss/oxide-linux-arm64-musl@4.1.16':
|
||||||
|
optional: true
|
||||||
|
|
||||||
|
'@tailwindcss/oxide-linux-x64-gnu@4.1.16':
|
||||||
|
optional: true
|
||||||
|
|
||||||
|
'@tailwindcss/oxide-linux-x64-musl@4.1.16':
|
||||||
|
optional: true
|
||||||
|
|
||||||
|
'@tailwindcss/oxide-wasm32-wasi@4.1.16':
|
||||||
|
optional: true
|
||||||
|
|
||||||
|
'@tailwindcss/oxide-win32-arm64-msvc@4.1.16':
|
||||||
|
optional: true
|
||||||
|
|
||||||
|
'@tailwindcss/oxide-win32-x64-msvc@4.1.16':
|
||||||
|
optional: true
|
||||||
|
|
||||||
|
'@tailwindcss/oxide@4.1.16':
|
||||||
|
optionalDependencies:
|
||||||
|
'@tailwindcss/oxide-android-arm64': 4.1.16
|
||||||
|
'@tailwindcss/oxide-darwin-arm64': 4.1.16
|
||||||
|
'@tailwindcss/oxide-darwin-x64': 4.1.16
|
||||||
|
'@tailwindcss/oxide-freebsd-x64': 4.1.16
|
||||||
|
'@tailwindcss/oxide-linux-arm-gnueabihf': 4.1.16
|
||||||
|
'@tailwindcss/oxide-linux-arm64-gnu': 4.1.16
|
||||||
|
'@tailwindcss/oxide-linux-arm64-musl': 4.1.16
|
||||||
|
'@tailwindcss/oxide-linux-x64-gnu': 4.1.16
|
||||||
|
'@tailwindcss/oxide-linux-x64-musl': 4.1.16
|
||||||
|
'@tailwindcss/oxide-wasm32-wasi': 4.1.16
|
||||||
|
'@tailwindcss/oxide-win32-arm64-msvc': 4.1.16
|
||||||
|
'@tailwindcss/oxide-win32-x64-msvc': 4.1.16
|
||||||
|
|
||||||
|
'@tailwindcss/vite@4.1.16(vite@7.1.12(@types/node@24.9.2)(jiti@2.6.1)(lightningcss@1.30.2))':
|
||||||
|
dependencies:
|
||||||
|
'@tailwindcss/node': 4.1.16
|
||||||
|
'@tailwindcss/oxide': 4.1.16
|
||||||
|
tailwindcss: 4.1.16
|
||||||
|
vite: 7.1.12(@types/node@24.9.2)(jiti@2.6.1)(lightningcss@1.30.2)
|
||||||
|
|
||||||
'@types/estree@1.0.8': {}
|
'@types/estree@1.0.8': {}
|
||||||
|
|
||||||
'@types/node@24.9.2':
|
'@types/node@24.9.2':
|
||||||
dependencies:
|
dependencies:
|
||||||
undici-types: 7.16.0
|
undici-types: 7.16.0
|
||||||
|
|
||||||
'@vitejs/plugin-vue@6.0.1(vite@7.1.12(@types/node@24.9.2))(vue@3.5.22(typescript@5.9.3))':
|
'@vitejs/plugin-vue@6.0.1(vite@7.1.12(@types/node@24.9.2)(jiti@2.6.1)(lightningcss@1.30.2))(vue@3.5.22(typescript@5.9.3))':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@rolldown/pluginutils': 1.0.0-beta.29
|
'@rolldown/pluginutils': 1.0.0-beta.29
|
||||||
vite: 7.1.12(@types/node@24.9.2)
|
vite: 7.1.12(@types/node@24.9.2)(jiti@2.6.1)(lightningcss@1.30.2)
|
||||||
vue: 3.5.22(typescript@5.9.3)
|
vue: 3.5.22(typescript@5.9.3)
|
||||||
|
|
||||||
'@volar/language-core@2.4.23':
|
'@volar/language-core@2.4.23':
|
||||||
@ -787,8 +1146,19 @@ snapshots:
|
|||||||
|
|
||||||
alien-signals@3.0.3: {}
|
alien-signals@3.0.3: {}
|
||||||
|
|
||||||
|
bmp-js@0.1.0: {}
|
||||||
|
|
||||||
csstype@3.1.3: {}
|
csstype@3.1.3: {}
|
||||||
|
|
||||||
|
daisyui@5.3.10: {}
|
||||||
|
|
||||||
|
detect-libc@2.1.2: {}
|
||||||
|
|
||||||
|
enhanced-resolve@5.18.3:
|
||||||
|
dependencies:
|
||||||
|
graceful-fs: 4.2.11
|
||||||
|
tapable: 2.3.0
|
||||||
|
|
||||||
entities@4.5.0: {}
|
entities@4.5.0: {}
|
||||||
|
|
||||||
esbuild@0.25.11:
|
esbuild@0.25.11:
|
||||||
@ -829,6 +1199,67 @@ snapshots:
|
|||||||
fsevents@2.3.3:
|
fsevents@2.3.3:
|
||||||
optional: true
|
optional: true
|
||||||
|
|
||||||
|
graceful-fs@4.2.11: {}
|
||||||
|
|
||||||
|
idb-keyval@6.2.2: {}
|
||||||
|
|
||||||
|
install@0.13.0: {}
|
||||||
|
|
||||||
|
is-electron@2.2.2: {}
|
||||||
|
|
||||||
|
is-url@1.2.4: {}
|
||||||
|
|
||||||
|
jiti@2.6.1: {}
|
||||||
|
|
||||||
|
lightningcss-android-arm64@1.30.2:
|
||||||
|
optional: true
|
||||||
|
|
||||||
|
lightningcss-darwin-arm64@1.30.2:
|
||||||
|
optional: true
|
||||||
|
|
||||||
|
lightningcss-darwin-x64@1.30.2:
|
||||||
|
optional: true
|
||||||
|
|
||||||
|
lightningcss-freebsd-x64@1.30.2:
|
||||||
|
optional: true
|
||||||
|
|
||||||
|
lightningcss-linux-arm-gnueabihf@1.30.2:
|
||||||
|
optional: true
|
||||||
|
|
||||||
|
lightningcss-linux-arm64-gnu@1.30.2:
|
||||||
|
optional: true
|
||||||
|
|
||||||
|
lightningcss-linux-arm64-musl@1.30.2:
|
||||||
|
optional: true
|
||||||
|
|
||||||
|
lightningcss-linux-x64-gnu@1.30.2:
|
||||||
|
optional: true
|
||||||
|
|
||||||
|
lightningcss-linux-x64-musl@1.30.2:
|
||||||
|
optional: true
|
||||||
|
|
||||||
|
lightningcss-win32-arm64-msvc@1.30.2:
|
||||||
|
optional: true
|
||||||
|
|
||||||
|
lightningcss-win32-x64-msvc@1.30.2:
|
||||||
|
optional: true
|
||||||
|
|
||||||
|
lightningcss@1.30.2:
|
||||||
|
dependencies:
|
||||||
|
detect-libc: 2.1.2
|
||||||
|
optionalDependencies:
|
||||||
|
lightningcss-android-arm64: 1.30.2
|
||||||
|
lightningcss-darwin-arm64: 1.30.2
|
||||||
|
lightningcss-darwin-x64: 1.30.2
|
||||||
|
lightningcss-freebsd-x64: 1.30.2
|
||||||
|
lightningcss-linux-arm-gnueabihf: 1.30.2
|
||||||
|
lightningcss-linux-arm64-gnu: 1.30.2
|
||||||
|
lightningcss-linux-arm64-musl: 1.30.2
|
||||||
|
lightningcss-linux-x64-gnu: 1.30.2
|
||||||
|
lightningcss-linux-x64-musl: 1.30.2
|
||||||
|
lightningcss-win32-arm64-msvc: 1.30.2
|
||||||
|
lightningcss-win32-x64-msvc: 1.30.2
|
||||||
|
|
||||||
magic-string@0.30.21:
|
magic-string@0.30.21:
|
||||||
dependencies:
|
dependencies:
|
||||||
'@jridgewell/sourcemap-codec': 1.5.5
|
'@jridgewell/sourcemap-codec': 1.5.5
|
||||||
@ -837,6 +1268,12 @@ snapshots:
|
|||||||
|
|
||||||
nanoid@3.3.11: {}
|
nanoid@3.3.11: {}
|
||||||
|
|
||||||
|
node-fetch@2.7.0:
|
||||||
|
dependencies:
|
||||||
|
whatwg-url: 5.0.0
|
||||||
|
|
||||||
|
opencollective-postinstall@2.0.3: {}
|
||||||
|
|
||||||
path-browserify@1.0.1: {}
|
path-browserify@1.0.1: {}
|
||||||
|
|
||||||
picocolors@1.1.1: {}
|
picocolors@1.1.1: {}
|
||||||
@ -849,6 +1286,8 @@ snapshots:
|
|||||||
picocolors: 1.1.1
|
picocolors: 1.1.1
|
||||||
source-map-js: 1.2.1
|
source-map-js: 1.2.1
|
||||||
|
|
||||||
|
regenerator-runtime@0.13.11: {}
|
||||||
|
|
||||||
rollup@4.52.5:
|
rollup@4.52.5:
|
||||||
dependencies:
|
dependencies:
|
||||||
'@types/estree': 1.0.8
|
'@types/estree': 1.0.8
|
||||||
@ -879,16 +1318,39 @@ snapshots:
|
|||||||
|
|
||||||
source-map-js@1.2.1: {}
|
source-map-js@1.2.1: {}
|
||||||
|
|
||||||
|
tailwindcss@4.1.16: {}
|
||||||
|
|
||||||
|
tapable@2.3.0: {}
|
||||||
|
|
||||||
|
tesseract.js-core@5.1.1: {}
|
||||||
|
|
||||||
|
tesseract.js@5.1.1:
|
||||||
|
dependencies:
|
||||||
|
bmp-js: 0.1.0
|
||||||
|
idb-keyval: 6.2.2
|
||||||
|
is-electron: 2.2.2
|
||||||
|
is-url: 1.2.4
|
||||||
|
node-fetch: 2.7.0
|
||||||
|
opencollective-postinstall: 2.0.3
|
||||||
|
regenerator-runtime: 0.13.11
|
||||||
|
tesseract.js-core: 5.1.1
|
||||||
|
wasm-feature-detect: 1.8.0
|
||||||
|
zlibjs: 0.3.1
|
||||||
|
transitivePeerDependencies:
|
||||||
|
- encoding
|
||||||
|
|
||||||
tinyglobby@0.2.15:
|
tinyglobby@0.2.15:
|
||||||
dependencies:
|
dependencies:
|
||||||
fdir: 6.5.0(picomatch@4.0.3)
|
fdir: 6.5.0(picomatch@4.0.3)
|
||||||
picomatch: 4.0.3
|
picomatch: 4.0.3
|
||||||
|
|
||||||
|
tr46@0.0.3: {}
|
||||||
|
|
||||||
typescript@5.9.3: {}
|
typescript@5.9.3: {}
|
||||||
|
|
||||||
undici-types@7.16.0: {}
|
undici-types@7.16.0: {}
|
||||||
|
|
||||||
vite@7.1.12(@types/node@24.9.2):
|
vite@7.1.12(@types/node@24.9.2)(jiti@2.6.1)(lightningcss@1.30.2):
|
||||||
dependencies:
|
dependencies:
|
||||||
esbuild: 0.25.11
|
esbuild: 0.25.11
|
||||||
fdir: 6.5.0(picomatch@4.0.3)
|
fdir: 6.5.0(picomatch@4.0.3)
|
||||||
@ -899,6 +1361,8 @@ snapshots:
|
|||||||
optionalDependencies:
|
optionalDependencies:
|
||||||
'@types/node': 24.9.2
|
'@types/node': 24.9.2
|
||||||
fsevents: 2.3.3
|
fsevents: 2.3.3
|
||||||
|
jiti: 2.6.1
|
||||||
|
lightningcss: 1.30.2
|
||||||
|
|
||||||
vscode-uri@3.1.0: {}
|
vscode-uri@3.1.0: {}
|
||||||
|
|
||||||
@ -917,3 +1381,14 @@ snapshots:
|
|||||||
'@vue/shared': 3.5.22
|
'@vue/shared': 3.5.22
|
||||||
optionalDependencies:
|
optionalDependencies:
|
||||||
typescript: 5.9.3
|
typescript: 5.9.3
|
||||||
|
|
||||||
|
wasm-feature-detect@1.8.0: {}
|
||||||
|
|
||||||
|
webidl-conversions@3.0.1: {}
|
||||||
|
|
||||||
|
whatwg-url@5.0.0:
|
||||||
|
dependencies:
|
||||||
|
tr46: 0.0.3
|
||||||
|
webidl-conversions: 3.0.1
|
||||||
|
|
||||||
|
zlibjs@0.3.1: {}
|
||||||
|
|||||||
@ -1,6 +1,261 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
|
import { ref, onMounted, computed } from 'vue'
|
||||||
|
import PuzzleCard from './components/PuzzleCard.vue'
|
||||||
|
import SubmissionForm from './components/SubmissionForm.vue'
|
||||||
|
import type { SteamCollection, SteamCollectionItem, Submission, PuzzleResponse } from './types'
|
||||||
|
|
||||||
|
// Mock data - replace with actual API calls later
|
||||||
|
const collections = ref<SteamCollection[]>([])
|
||||||
|
const puzzles = ref<SteamCollectionItem[]>([])
|
||||||
|
const submissions = ref<Submission[]>([])
|
||||||
|
const isLoading = ref(true)
|
||||||
|
const showSubmissionModal = ref(false)
|
||||||
|
|
||||||
|
// Mock data for development
|
||||||
|
const mockCollections: SteamCollection[] = [
|
||||||
|
{
|
||||||
|
id: 1,
|
||||||
|
steam_id: '3479142989',
|
||||||
|
title: 'PolyLAN 41',
|
||||||
|
description: 'Puzzle for PolyLAN 41 fil rouge',
|
||||||
|
author_name: 'Flame Legrems',
|
||||||
|
total_items: 10,
|
||||||
|
unique_visitors: 31,
|
||||||
|
current_favorites: 1,
|
||||||
|
created_at: '2025-05-29T11:19:24Z',
|
||||||
|
updated_at: '2025-05-30T22:15:09Z'
|
||||||
|
}
|
||||||
|
]
|
||||||
|
|
||||||
|
const mockPuzzles: SteamCollectionItem[] = [
|
||||||
|
{
|
||||||
|
id: 1,
|
||||||
|
steam_item_id: '3479143948',
|
||||||
|
title: 'P41-FLOC',
|
||||||
|
author_name: 'Flame Legrems',
|
||||||
|
description: 'A challenging puzzle involving complex molecular arrangements',
|
||||||
|
tags: ['puzzle', 'chemistry', 'advanced'],
|
||||||
|
order_index: 0,
|
||||||
|
collection: 1,
|
||||||
|
created_at: '2025-05-29T11:19:24Z',
|
||||||
|
updated_at: '2025-05-30T22:15:09Z'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 2,
|
||||||
|
steam_item_id: '3479143084',
|
||||||
|
title: 'P41-40',
|
||||||
|
author_name: 'Flame Legrems',
|
||||||
|
description: 'Test your optimization skills with this intricate design challenge',
|
||||||
|
tags: ['optimization', 'design'],
|
||||||
|
order_index: 1,
|
||||||
|
collection: 1,
|
||||||
|
created_at: '2025-05-29T11:19:24Z',
|
||||||
|
updated_at: '2025-05-30T22:15:09Z'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 3,
|
||||||
|
steam_item_id: '3479143304',
|
||||||
|
title: 'P41-39',
|
||||||
|
author_name: 'Flame Legrems',
|
||||||
|
description: 'A puzzle focusing on efficient resource management',
|
||||||
|
tags: ['efficiency', 'resources'],
|
||||||
|
order_index: 2,
|
||||||
|
collection: 1,
|
||||||
|
created_at: '2025-05-29T11:19:24Z',
|
||||||
|
updated_at: '2025-05-30T22:15:09Z'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 4,
|
||||||
|
steam_item_id: '3479143433',
|
||||||
|
title: 'P41-38',
|
||||||
|
author_name: 'Flame Legrems',
|
||||||
|
description: 'Master the art of precise timing in this temporal challenge',
|
||||||
|
tags: ['timing', 'precision'],
|
||||||
|
order_index: 3,
|
||||||
|
collection: 1,
|
||||||
|
created_at: '2025-05-29T11:19:24Z',
|
||||||
|
updated_at: '2025-05-30T22:15:09Z'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 5,
|
||||||
|
steam_item_id: '3479143537',
|
||||||
|
title: 'P41-37',
|
||||||
|
author_name: 'Flame Legrems',
|
||||||
|
description: 'Explore innovative solutions in this creative puzzle',
|
||||||
|
tags: ['creative', 'innovation'],
|
||||||
|
order_index: 4,
|
||||||
|
collection: 1,
|
||||||
|
created_at: '2025-05-29T11:19:24Z',
|
||||||
|
updated_at: '2025-05-30T22:15:09Z'
|
||||||
|
}
|
||||||
|
]
|
||||||
|
|
||||||
|
// Computed property to get responses grouped by puzzle
|
||||||
|
const responsesByPuzzle = computed(() => {
|
||||||
|
const grouped: Record<number, PuzzleResponse[]> = {}
|
||||||
|
submissions.value.forEach(submission => {
|
||||||
|
submission.responses.forEach(response => {
|
||||||
|
if (!grouped[response.puzzle_id]) {
|
||||||
|
grouped[response.puzzle_id] = []
|
||||||
|
}
|
||||||
|
grouped[response.puzzle_id].push(response)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
return grouped
|
||||||
|
})
|
||||||
|
|
||||||
|
onMounted(async () => {
|
||||||
|
// Simulate API loading
|
||||||
|
await new Promise(resolve => setTimeout(resolve, 500))
|
||||||
|
|
||||||
|
collections.value = mockCollections
|
||||||
|
puzzles.value = mockPuzzles
|
||||||
|
isLoading.value = false
|
||||||
|
})
|
||||||
|
|
||||||
|
const handleSubmission = (submission: Submission) => {
|
||||||
|
console.log('Submission received:', submission)
|
||||||
|
|
||||||
|
// Add submission to the list
|
||||||
|
submissions.value.push({
|
||||||
|
...submission,
|
||||||
|
id: Date.now(), // Simple ID generation for demo
|
||||||
|
created_at: new Date().toISOString(),
|
||||||
|
updated_at: new Date().toISOString()
|
||||||
|
})
|
||||||
|
|
||||||
|
// Show success message
|
||||||
|
const puzzleNames = submission.responses.map(r => r.puzzle_name).join(', ')
|
||||||
|
alert(`Solutions submitted for puzzles: ${puzzleNames}`)
|
||||||
|
|
||||||
|
// Close modal
|
||||||
|
showSubmissionModal.value = false
|
||||||
|
}
|
||||||
|
|
||||||
|
const openSubmissionModal = () => {
|
||||||
|
showSubmissionModal.value = true
|
||||||
|
}
|
||||||
|
|
||||||
|
const closeSubmissionModal = () => {
|
||||||
|
showSubmissionModal.value = false
|
||||||
|
}
|
||||||
|
|
||||||
|
// Function to match puzzle name from OCR to actual puzzle
|
||||||
|
const findPuzzleByName = (ocrPuzzleName: string): SteamCollectionItem | null => {
|
||||||
|
if (!ocrPuzzleName) return null
|
||||||
|
|
||||||
|
// Try exact match first
|
||||||
|
let match = puzzles.value.find(p =>
|
||||||
|
p.title.toLowerCase() === ocrPuzzleName.toLowerCase()
|
||||||
|
)
|
||||||
|
|
||||||
|
if (!match) {
|
||||||
|
// Try partial match
|
||||||
|
match = puzzles.value.find(p =>
|
||||||
|
p.title.toLowerCase().includes(ocrPuzzleName.toLowerCase()) ||
|
||||||
|
ocrPuzzleName.toLowerCase().includes(p.title.toLowerCase())
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
return match || null
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
HELLO
|
<div class="min-h-screen bg-base-200">
|
||||||
|
<!-- Header -->
|
||||||
|
<div class="navbar bg-base-100 shadow-lg">
|
||||||
|
<div class="container mx-auto">
|
||||||
|
<div class="flex-1">
|
||||||
|
<h1 class="text-xl font-bold">Opus Magnum Puzzle Submitter</h1>
|
||||||
|
</div>
|
||||||
|
<div class="flex-none">
|
||||||
|
<button
|
||||||
|
@click="openSubmissionModal"
|
||||||
|
class="btn btn-primary"
|
||||||
|
>
|
||||||
|
<i class="mdi mdi-plus mr-2"></i>
|
||||||
|
Submit Solution
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Main Content -->
|
||||||
|
<div class="container mx-auto px-4 py-8">
|
||||||
|
<!-- Loading State -->
|
||||||
|
<div v-if="isLoading" class="flex justify-center items-center min-h-[400px]">
|
||||||
|
<div class="text-center">
|
||||||
|
<span class="loading loading-spinner loading-lg"></span>
|
||||||
|
<p class="mt-4 text-base-content/70">Loading puzzles...</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Main Content -->
|
||||||
|
<div v-else class="space-y-8">
|
||||||
|
<!-- Collection Info -->
|
||||||
|
<div v-if="collections.length > 0" class="mb-8">
|
||||||
|
<div class="card bg-base-100 shadow-lg">
|
||||||
|
<div class="card-body">
|
||||||
|
<h2 class="card-title text-2xl">{{ collections[0].title }}</h2>
|
||||||
|
<p class="text-base-content/70">{{ collections[0].description }}</p>
|
||||||
|
<div class="flex flex-wrap gap-4 mt-4">
|
||||||
|
<div class="stat">
|
||||||
|
<div class="stat-title">Total Puzzles</div>
|
||||||
|
<div class="stat-value text-primary">{{ collections[0].total_items }}</div>
|
||||||
|
</div>
|
||||||
|
<div class="stat">
|
||||||
|
<div class="stat-title">Author</div>
|
||||||
|
<div class="stat-value text-sm">{{ collections[0].author_name }}</div>
|
||||||
|
</div>
|
||||||
|
<div class="stat">
|
||||||
|
<div class="stat-title">Visitors</div>
|
||||||
|
<div class="stat-value text-sm">{{ collections[0].unique_visitors }}</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Puzzles Grid -->
|
||||||
|
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
|
||||||
|
<PuzzleCard
|
||||||
|
v-for="puzzle in puzzles"
|
||||||
|
:key="puzzle.id"
|
||||||
|
:puzzle="puzzle"
|
||||||
|
:responses="responsesByPuzzle[puzzle.id] || []"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Empty State -->
|
||||||
|
<div v-if="puzzles.length === 0" class="text-center py-12">
|
||||||
|
<div class="text-6xl mb-4">🧩</div>
|
||||||
|
<h3 class="text-xl font-bold mb-2">No Puzzles Available</h3>
|
||||||
|
<p class="text-base-content/70">Check back later for new puzzle collections!</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Submission Modal -->
|
||||||
|
<div v-if="showSubmissionModal" class="modal modal-open">
|
||||||
|
<div class="modal-box max-w-4xl">
|
||||||
|
<div class="flex justify-between items-center mb-4">
|
||||||
|
<h3 class="font-bold text-lg">Submit Solution</h3>
|
||||||
|
<button
|
||||||
|
@click="closeSubmissionModal"
|
||||||
|
class="btn btn-sm btn-circle btn-ghost"
|
||||||
|
>
|
||||||
|
<i class="mdi mdi-close"></i>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<SubmissionForm
|
||||||
|
:puzzles="puzzles"
|
||||||
|
:find-puzzle-by-name="findPuzzleByName"
|
||||||
|
@submit="handleSubmission"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div class="modal-backdrop" @click="closeSubmissionModal"></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
300
opus_submitter/src/components/FileUpload.vue
Normal file
300
opus_submitter/src/components/FileUpload.vue
Normal file
@ -0,0 +1,300 @@
|
|||||||
|
<template>
|
||||||
|
<div class="form-control w-full">
|
||||||
|
<label class="label">
|
||||||
|
<span class="label-text font-medium">Upload Solution Files</span>
|
||||||
|
<span class="label-text-alt text-xs">Images or GIFs only</span>
|
||||||
|
</label>
|
||||||
|
|
||||||
|
<div
|
||||||
|
class="border-2 border-dashed border-base-300 rounded-lg p-6 text-center hover:border-primary transition-colors duration-300"
|
||||||
|
:class="{ 'border-primary bg-primary/5': isDragOver }"
|
||||||
|
@drop="handleDrop"
|
||||||
|
@dragover.prevent="isDragOver = true"
|
||||||
|
@dragleave="isDragOver = false"
|
||||||
|
@dragenter.prevent
|
||||||
|
>
|
||||||
|
<input
|
||||||
|
ref="fileInput"
|
||||||
|
type="file"
|
||||||
|
multiple
|
||||||
|
accept="image/*,.gif"
|
||||||
|
class="hidden"
|
||||||
|
@change="handleFileSelect"
|
||||||
|
>
|
||||||
|
|
||||||
|
<div v-if="files.length === 0" class="space-y-4">
|
||||||
|
<div class="mx-auto w-12 h-12 text-base-content/40 flex items-center justify-center">
|
||||||
|
<i class="mdi mdi-cloud-upload text-5xl"></i>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<p class="text-base-content/70 mb-2">Drop your files here or</p>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
@click="fileInput?.click()"
|
||||||
|
class="btn btn-primary btn-sm"
|
||||||
|
>
|
||||||
|
Choose Files
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<p class="text-xs text-base-content/50">
|
||||||
|
Supported formats: JPG, PNG, GIF (max 10MB each)
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div v-else class="space-y-4">
|
||||||
|
<div class="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-4">
|
||||||
|
<div
|
||||||
|
v-for="(file, index) in files"
|
||||||
|
:key="index"
|
||||||
|
class="relative group"
|
||||||
|
>
|
||||||
|
<div class="aspect-square rounded-lg overflow-hidden bg-base-200">
|
||||||
|
<img
|
||||||
|
:src="file.preview"
|
||||||
|
:alt="file.file.name"
|
||||||
|
class="w-full h-full object-cover"
|
||||||
|
>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="absolute inset-0 bg-black/50 opacity-0 group-hover:opacity-100 transition-opacity duration-200 rounded-lg flex items-center justify-center">
|
||||||
|
<button
|
||||||
|
@click="removeFile(index)"
|
||||||
|
class="btn btn-error btn-sm btn-circle"
|
||||||
|
>
|
||||||
|
<i class="mdi mdi-close"></i>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="mt-2">
|
||||||
|
<p class="text-xs font-medium truncate">{{ file.file.name }}</p>
|
||||||
|
<p class="text-xs text-base-content/60">
|
||||||
|
{{ formatFileSize(file.file.size) }} • {{ file.type.toUpperCase() }}
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<!-- OCR Status and Results -->
|
||||||
|
<div v-if="file.ocrProcessing" class="mt-1 flex items-center gap-1">
|
||||||
|
<span class="loading loading-spinner loading-xs"></span>
|
||||||
|
<span class="text-xs text-info">Extracting puzzle data...</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div v-else-if="file.ocrError" class="mt-1">
|
||||||
|
<p class="text-xs text-error">{{ file.ocrError }}</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div v-else-if="file.ocrData" class="mt-1 space-y-1">
|
||||||
|
<div class="text-xs flex items-center justify-between">
|
||||||
|
<span class="font-medium text-success">✓ OCR Complete</span>
|
||||||
|
<button
|
||||||
|
@click="retryOCR(file)"
|
||||||
|
class="btn btn-xs btn-ghost"
|
||||||
|
title="Retry OCR"
|
||||||
|
>
|
||||||
|
<i class="mdi mdi-refresh"></i>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<div class="text-xs space-y-1 bg-base-200 p-2 rounded">
|
||||||
|
<div v-if="file.ocrData.puzzle">
|
||||||
|
<strong>Puzzle:</strong> {{ file.ocrData.puzzle }}
|
||||||
|
</div>
|
||||||
|
<div v-if="file.ocrData.cost">
|
||||||
|
<strong>Cost:</strong> {{ file.ocrData.cost }}
|
||||||
|
</div>
|
||||||
|
<div v-if="file.ocrData.cycles">
|
||||||
|
<strong>Cycles:</strong> {{ file.ocrData.cycles }}
|
||||||
|
</div>
|
||||||
|
<div v-if="file.ocrData.area">
|
||||||
|
<strong>Area:</strong> {{ file.ocrData.area }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Manual OCR trigger for non-auto detected files -->
|
||||||
|
<div v-else-if="!file.ocrProcessing && !file.ocrError && !file.ocrData" class="mt-1">
|
||||||
|
<button
|
||||||
|
@click="processOCR(file)"
|
||||||
|
class="btn btn-xs btn-outline"
|
||||||
|
>
|
||||||
|
<i class="mdi mdi-text-recognition"></i>
|
||||||
|
Extract Puzzle Data
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="flex justify-center">
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
@click="fileInput?.click()"
|
||||||
|
class="btn btn-outline btn-sm"
|
||||||
|
>
|
||||||
|
Add More Files
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div v-if="error" class="label">
|
||||||
|
<span class="label-text-alt text-error">{{ error }}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { ref, watch, nextTick } from 'vue'
|
||||||
|
import { ocrService, type OpusMagnumData } from '../services/ocrService'
|
||||||
|
import type { SubmissionFile } from '@/types'
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
modelValue: SubmissionFile[]
|
||||||
|
}
|
||||||
|
|
||||||
|
interface Emits {
|
||||||
|
'update:modelValue': [files: SubmissionFile[]]
|
||||||
|
}
|
||||||
|
|
||||||
|
const props = defineProps<Props>()
|
||||||
|
const emit = defineEmits<Emits>()
|
||||||
|
|
||||||
|
const fileInput = ref<HTMLInputElement>()
|
||||||
|
const isDragOver = ref(false)
|
||||||
|
const error = ref('')
|
||||||
|
const files = ref<SubmissionFile[]>([])
|
||||||
|
|
||||||
|
// Watch for external changes to modelValue
|
||||||
|
watch(() => props.modelValue, (newFiles) => {
|
||||||
|
files.value = newFiles
|
||||||
|
}, { immediate: true })
|
||||||
|
|
||||||
|
// Watch for internal changes and emit
|
||||||
|
watch(files, (newFiles) => {
|
||||||
|
emit('update:modelValue', newFiles)
|
||||||
|
}, { deep: true })
|
||||||
|
|
||||||
|
const handleFileSelect = (event: Event) => {
|
||||||
|
const target = event.target as HTMLInputElement
|
||||||
|
if (target.files) {
|
||||||
|
processFiles(Array.from(target.files))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleDrop = (event: DragEvent) => {
|
||||||
|
event.preventDefault()
|
||||||
|
isDragOver.value = false
|
||||||
|
|
||||||
|
if (event.dataTransfer?.files) {
|
||||||
|
processFiles(Array.from(event.dataTransfer.files))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const processFiles = async (newFiles: File[]) => {
|
||||||
|
error.value = ''
|
||||||
|
|
||||||
|
for (const file of newFiles) {
|
||||||
|
if (!isValidFile(file)) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const preview = await createPreview(file)
|
||||||
|
const fileType = file.type.startsWith('image/gif') ? 'gif' : 'image'
|
||||||
|
|
||||||
|
const submissionFile: SubmissionFile = {
|
||||||
|
file,
|
||||||
|
preview,
|
||||||
|
type: fileType,
|
||||||
|
ocrProcessing: false,
|
||||||
|
ocrError: undefined,
|
||||||
|
ocrData: undefined
|
||||||
|
}
|
||||||
|
|
||||||
|
files.value.push(submissionFile)
|
||||||
|
|
||||||
|
// Start OCR processing for Opus Magnum images (with delay to ensure reactivity)
|
||||||
|
if (isOpusMagnumImage(file)) {
|
||||||
|
nextTick(() => {
|
||||||
|
processOCR(submissionFile)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
error.value = `Failed to process ${file.name}`
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const isValidFile = (file: File): boolean => {
|
||||||
|
// Check file type
|
||||||
|
if (!file.type.startsWith('image/')) {
|
||||||
|
error.value = `${file.name} is not a valid image file`
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check file size (10MB limit)
|
||||||
|
if (file.size > 10 * 1024 * 1024) {
|
||||||
|
error.value = `${file.name} is too large (max 10MB)`
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
const createPreview = (file: File): Promise<string> => {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
const reader = new FileReader()
|
||||||
|
reader.onload = (e) => resolve(e.target?.result as string)
|
||||||
|
reader.onerror = reject
|
||||||
|
reader.readAsDataURL(file)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
const removeFile = (index: number) => {
|
||||||
|
files.value.splice(index, 1)
|
||||||
|
}
|
||||||
|
|
||||||
|
const formatFileSize = (bytes: number): string => {
|
||||||
|
if (bytes === 0) return '0 Bytes'
|
||||||
|
|
||||||
|
const k = 1024
|
||||||
|
const sizes = ['Bytes', 'KB', 'MB', 'GB']
|
||||||
|
const i = Math.floor(Math.log(bytes) / Math.log(k))
|
||||||
|
|
||||||
|
return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i]
|
||||||
|
}
|
||||||
|
|
||||||
|
const isOpusMagnumImage = (file: File): boolean => {
|
||||||
|
// Basic heuristic - could be enhanced with actual image analysis
|
||||||
|
return file.type.startsWith('image/') && file.size > 50000 // > 50KB likely screenshot
|
||||||
|
}
|
||||||
|
|
||||||
|
const processOCR = async (submissionFile: SubmissionFile) => {
|
||||||
|
// Find the file in the reactive array to ensure proper reactivity
|
||||||
|
const fileIndex = files.value.findIndex(f => f.file === submissionFile.file)
|
||||||
|
if (fileIndex === -1) return
|
||||||
|
|
||||||
|
// Update the reactive array directly
|
||||||
|
files.value[fileIndex].ocrProcessing = true
|
||||||
|
files.value[fileIndex].ocrError = undefined
|
||||||
|
files.value[fileIndex].ocrData = undefined
|
||||||
|
|
||||||
|
try {
|
||||||
|
console.log('Starting OCR processing for:', submissionFile.file.name)
|
||||||
|
await ocrService.initialize()
|
||||||
|
const ocrData = await ocrService.extractOpusMagnumData(submissionFile.file)
|
||||||
|
console.log('OCR completed:', ocrData)
|
||||||
|
|
||||||
|
// Force reactivity update
|
||||||
|
await nextTick()
|
||||||
|
files.value[fileIndex].ocrData = ocrData
|
||||||
|
await nextTick()
|
||||||
|
} catch (error) {
|
||||||
|
console.error('OCR processing failed:', error)
|
||||||
|
files.value[fileIndex].ocrError = 'Failed to extract puzzle data'
|
||||||
|
} finally {
|
||||||
|
files.value[fileIndex].ocrProcessing = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const retryOCR = (submissionFile: SubmissionFile) => {
|
||||||
|
processOCR(submissionFile)
|
||||||
|
}
|
||||||
|
</script>
|
||||||
137
opus_submitter/src/components/PuzzleCard.vue
Normal file
137
opus_submitter/src/components/PuzzleCard.vue
Normal file
@ -0,0 +1,137 @@
|
|||||||
|
<template>
|
||||||
|
<div class="card bg-base-100 shadow-xl hover:shadow-2xl transition-shadow duration-300">
|
||||||
|
<div class="card-body">
|
||||||
|
<div class="flex items-start justify-between">
|
||||||
|
<div class="flex-1">
|
||||||
|
<h3 class="card-title text-lg font-bold">{{ puzzle.title }}</h3>
|
||||||
|
<p class="text-sm text-base-content/70 mb-2">by {{ puzzle.author_name }}</p>
|
||||||
|
|
||||||
|
<div class="flex items-center gap-2 mb-3">
|
||||||
|
<div class="badge badge-primary badge-sm">{{ puzzle.steam_item_id }}</div>
|
||||||
|
<div class="badge badge-ghost badge-sm">Order: {{ puzzle.order_index + 1 }}</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<p v-if="puzzle.description" class="text-sm text-base-content/80 mb-4 line-clamp-2">
|
||||||
|
{{ puzzle.description }}
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<div v-if="puzzle.tags && puzzle.tags.length > 0" class="flex flex-wrap gap-1 mb-4">
|
||||||
|
<span
|
||||||
|
v-for="tag in puzzle.tags.slice(0, 3)"
|
||||||
|
:key="tag"
|
||||||
|
class="badge badge-outline badge-xs"
|
||||||
|
>
|
||||||
|
{{ tag }}
|
||||||
|
</span>
|
||||||
|
<span v-if="puzzle.tags.length > 3" class="badge badge-outline badge-xs">
|
||||||
|
+{{ puzzle.tags.length - 3 }} more
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="flex flex-col items-end gap-2">
|
||||||
|
<div class="tooltip" data-tip="View on Steam Workshop">
|
||||||
|
<a
|
||||||
|
:href="`https://steamcommunity.com/workshop/filedetails/?id=${puzzle.steam_item_id}`"
|
||||||
|
target="_blank"
|
||||||
|
class="btn btn-ghost btn-sm btn-square"
|
||||||
|
>
|
||||||
|
<i class="mdi mdi-steam text-lg"></i>
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Responses Table -->
|
||||||
|
<div v-if="responses && responses.length > 0" class="mt-6">
|
||||||
|
<div class="divider">
|
||||||
|
<span class="text-sm font-medium">Solutions ({{ responses.length }})</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="overflow-x-auto">
|
||||||
|
<table class="table table-xs">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>Cost</th>
|
||||||
|
<th>Cycles</th>
|
||||||
|
<th>Area</th>
|
||||||
|
<th>Files</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
<tr v-for="response in responses" :key="response.id" class="hover">
|
||||||
|
<td>
|
||||||
|
<span v-if="response.cost" class="badge badge-success badge-xs">
|
||||||
|
{{ response.cost }}
|
||||||
|
</span>
|
||||||
|
<span v-else class="text-base-content/50">-</span>
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
<span v-if="response.cycles" class="badge badge-info badge-xs">
|
||||||
|
{{ response.cycles }}
|
||||||
|
</span>
|
||||||
|
<span v-else class="text-base-content/50">-</span>
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
<span v-if="response.area" class="badge badge-warning badge-xs">
|
||||||
|
{{ response.area }}
|
||||||
|
</span>
|
||||||
|
<span v-else class="text-base-content/50">-</span>
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
<div class="flex items-center gap-1">
|
||||||
|
<span class="badge badge-ghost badge-xs">{{ response.files.length }}</span>
|
||||||
|
<div class="tooltip" :data-tip="response.files.map(f => f.file.name).join(', ')">
|
||||||
|
<i class="mdi mdi-information-outline text-xs"></i>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- No responses state -->
|
||||||
|
<div v-else class="mt-6 text-center py-4 border-2 border-dashed border-base-300 rounded-lg">
|
||||||
|
<i class="mdi mdi-upload text-2xl text-base-content/40"></i>
|
||||||
|
<p class="text-sm text-base-content/60 mt-2">No solutions yet</p>
|
||||||
|
<p class="text-xs text-base-content/40">Upload solutions using the submit button</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import type { SteamCollectionItem, PuzzleResponse } from '@/types'
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
puzzle: SteamCollectionItem
|
||||||
|
responses?: PuzzleResponse[]
|
||||||
|
}
|
||||||
|
|
||||||
|
defineProps<Props>()
|
||||||
|
|
||||||
|
const formatFileSize = (bytes: number): string => {
|
||||||
|
if (bytes === 0) return '0 Bytes'
|
||||||
|
|
||||||
|
const k = 1024
|
||||||
|
const sizes = ['Bytes', 'KB', 'MB', 'GB']
|
||||||
|
const i = Math.floor(Math.log(bytes) / Math.log(k))
|
||||||
|
|
||||||
|
return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i]
|
||||||
|
}
|
||||||
|
|
||||||
|
const formatDate = (dateString: string): string => {
|
||||||
|
return new Date(dateString).toLocaleDateString()
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.line-clamp-2 {
|
||||||
|
display: -webkit-box;
|
||||||
|
-webkit-line-clamp: 2;
|
||||||
|
-webkit-box-orient: vertical;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
147
opus_submitter/src/components/SubmissionForm.vue
Normal file
147
opus_submitter/src/components/SubmissionForm.vue
Normal file
@ -0,0 +1,147 @@
|
|||||||
|
<template>
|
||||||
|
<div class="card bg-base-100 shadow-xl">
|
||||||
|
<div class="card-body">
|
||||||
|
<h2 class="card-title text-xl mb-6">
|
||||||
|
<i class="mdi mdi-check-circle text-2xl text-primary"></i>
|
||||||
|
Submit Solution
|
||||||
|
</h2>
|
||||||
|
|
||||||
|
<form @submit.prevent="handleSubmit" class="space-y-6">
|
||||||
|
<!-- Detected Puzzles Summary -->
|
||||||
|
<div v-if="Object.keys(responsesByPuzzle).length > 0" class="alert alert-info">
|
||||||
|
<i class="mdi mdi-information-outline text-xl"></i>
|
||||||
|
<div class="flex-1">
|
||||||
|
<h4 class="font-bold">Detected Puzzles ({{ Object.keys(responsesByPuzzle).length }})</h4>
|
||||||
|
<div class="text-sm space-y-1 mt-1">
|
||||||
|
<div v-for="(data, puzzleName) in responsesByPuzzle" :key="puzzleName" class="flex justify-between">
|
||||||
|
<span>{{ puzzleName }}</span>
|
||||||
|
<span class="badge badge-ghost badge-sm">{{ data.files.length }} file(s)</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- File Upload -->
|
||||||
|
<FileUpload v-model="submissionFiles" />
|
||||||
|
|
||||||
|
<!-- Notes -->
|
||||||
|
<div class="form-control">
|
||||||
|
<label class="label">
|
||||||
|
<span class="label-text font-medium">Notes (Optional)</span>
|
||||||
|
<span class="label-text-alt">{{ notesLength }}/500</span>
|
||||||
|
</label>
|
||||||
|
<textarea
|
||||||
|
v-model="notes"
|
||||||
|
class="textarea textarea-bordered h-24 resize-none"
|
||||||
|
placeholder="Add any notes about your solution, approach, or interesting findings..."
|
||||||
|
maxlength="500"
|
||||||
|
></textarea>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Submit Button -->
|
||||||
|
<div class="card-actions justify-end">
|
||||||
|
<button
|
||||||
|
type="submit"
|
||||||
|
class="btn btn-primary"
|
||||||
|
:disabled="isSubmitting"
|
||||||
|
>
|
||||||
|
<span v-if="isSubmitting" class="loading loading-spinner loading-sm"></span>
|
||||||
|
{{ isSubmitting ? 'Submitting...' : 'Submit Solution' }}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { ref, computed } from 'vue'
|
||||||
|
import FileUpload from './FileUpload.vue'
|
||||||
|
import type { SteamCollectionItem, SubmissionFile, Submission, PuzzleResponse } from '@/types'
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
puzzles: SteamCollectionItem[]
|
||||||
|
findPuzzleByName: (name: string) => SteamCollectionItem | null
|
||||||
|
}
|
||||||
|
|
||||||
|
interface Emits {
|
||||||
|
submit: [submission: Submission]
|
||||||
|
}
|
||||||
|
|
||||||
|
const props = defineProps<Props>()
|
||||||
|
const emit = defineEmits<Emits>()
|
||||||
|
|
||||||
|
const submissionFiles = ref<SubmissionFile[]>([])
|
||||||
|
const notes = ref('')
|
||||||
|
const isSubmitting = ref(false)
|
||||||
|
|
||||||
|
const notesLength = computed(() => notes.value.length)
|
||||||
|
|
||||||
|
const canSubmit = computed(() => {
|
||||||
|
return submissionFiles.value.length > 0 &&
|
||||||
|
!isSubmitting.value
|
||||||
|
})
|
||||||
|
|
||||||
|
// Group files by detected puzzle
|
||||||
|
const responsesByPuzzle = computed(() => {
|
||||||
|
const grouped: Record<string, { puzzle: SteamCollectionItem | null, files: SubmissionFile[] }> = {}
|
||||||
|
|
||||||
|
submissionFiles.value.forEach(file => {
|
||||||
|
if (file.ocrData?.puzzle) {
|
||||||
|
const puzzleName = file.ocrData.puzzle
|
||||||
|
if (!grouped[puzzleName]) {
|
||||||
|
grouped[puzzleName] = {
|
||||||
|
puzzle: props.findPuzzleByName(puzzleName),
|
||||||
|
files: []
|
||||||
|
}
|
||||||
|
}
|
||||||
|
grouped[puzzleName].files.push(file)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
return grouped
|
||||||
|
})
|
||||||
|
|
||||||
|
const handleSubmit = async () => {
|
||||||
|
if (!canSubmit.value) return
|
||||||
|
|
||||||
|
isSubmitting.value = true
|
||||||
|
|
||||||
|
try {
|
||||||
|
const responses: PuzzleResponse[] = []
|
||||||
|
|
||||||
|
// Create responses for each detected puzzle
|
||||||
|
Object.entries(responsesByPuzzle.value).forEach(([puzzleName, data]) => {
|
||||||
|
if (data.puzzle) {
|
||||||
|
// Get OCR data from the first file with complete data
|
||||||
|
const fileWithOCR = data.files.find(f => f.ocrData?.cost || f.ocrData?.cycles || f.ocrData?.area)
|
||||||
|
|
||||||
|
responses.push({
|
||||||
|
puzzle_id: data.puzzle.id,
|
||||||
|
puzzle_name: puzzleName,
|
||||||
|
cost: fileWithOCR?.ocrData?.cost,
|
||||||
|
cycles: fileWithOCR?.ocrData?.cycles,
|
||||||
|
area: fileWithOCR?.ocrData?.area,
|
||||||
|
files: data.files
|
||||||
|
})
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
const submission: Submission = {
|
||||||
|
responses,
|
||||||
|
notes: notes.value.trim() || undefined
|
||||||
|
}
|
||||||
|
|
||||||
|
emit('submit', submission)
|
||||||
|
|
||||||
|
// Reset form
|
||||||
|
submissionFiles.value = []
|
||||||
|
notes.value = ''
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Submission error:', error)
|
||||||
|
} finally {
|
||||||
|
isSubmitting.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
@ -1,4 +1,5 @@
|
|||||||
import { createApp } from 'vue'
|
import { createApp } from 'vue'
|
||||||
import App from '@/App.vue'
|
import App from '@/App.vue'
|
||||||
|
import './style.css'
|
||||||
|
|
||||||
createApp(App).mount('#app')
|
createApp(App).mount('#app')
|
||||||
|
|||||||
259
opus_submitter/src/services/ocrService.ts
Normal file
259
opus_submitter/src/services/ocrService.ts
Normal file
@ -0,0 +1,259 @@
|
|||||||
|
import { createWorker } from 'tesseract.js';
|
||||||
|
|
||||||
|
export interface OpusMagnumData {
|
||||||
|
puzzle: string;
|
||||||
|
cost: string;
|
||||||
|
cycles: string;
|
||||||
|
area: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface OCRRegion {
|
||||||
|
x: number;
|
||||||
|
y: number;
|
||||||
|
width: number;
|
||||||
|
height: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export class OpusMagnumOCRService {
|
||||||
|
private worker: Tesseract.Worker | null = null;
|
||||||
|
|
||||||
|
// Regions based on main.py coordinates (adjusted for web usage)
|
||||||
|
private readonly regions: Record<string, OCRRegion> = {
|
||||||
|
puzzle: { x: 15, y: 600, width: 330, height: 28 },
|
||||||
|
cost: { x: 412, y: 603, width: 65, height: 22 },
|
||||||
|
cycles: { x: 577, y: 603, width: 65, height: 22 },
|
||||||
|
area: { x: 739, y: 603, width: 65, height: 22 }
|
||||||
|
};
|
||||||
|
|
||||||
|
async initialize(): Promise<void> {
|
||||||
|
if (this.worker) return;
|
||||||
|
|
||||||
|
this.worker = await createWorker('eng');
|
||||||
|
await this.worker.setParameters({
|
||||||
|
tessedit_ocr_engine_mode: '3',
|
||||||
|
tessedit_pageseg_mode: '7'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async extractOpusMagnumData(imageFile: File): Promise<OpusMagnumData> {
|
||||||
|
if (!this.worker) {
|
||||||
|
await this.initialize();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Convert file to image element for canvas processing
|
||||||
|
const imageUrl = URL.createObjectURL(imageFile);
|
||||||
|
const img = new Image();
|
||||||
|
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
img.onload = async () => {
|
||||||
|
try {
|
||||||
|
const canvas = document.createElement('canvas');
|
||||||
|
const ctx = canvas.getContext('2d')!;
|
||||||
|
|
||||||
|
canvas.width = img.width;
|
||||||
|
canvas.height = img.height;
|
||||||
|
ctx.drawImage(img, 0, 0);
|
||||||
|
|
||||||
|
// Extract text from each region
|
||||||
|
const results: Partial<OpusMagnumData> = {};
|
||||||
|
|
||||||
|
for (const [key, region] of Object.entries(this.regions)) {
|
||||||
|
const regionCanvas = document.createElement('canvas');
|
||||||
|
const regionCtx = regionCanvas.getContext('2d')!;
|
||||||
|
|
||||||
|
regionCanvas.width = region.width;
|
||||||
|
regionCanvas.height = region.height;
|
||||||
|
|
||||||
|
// Extract region from main image
|
||||||
|
regionCtx.drawImage(
|
||||||
|
canvas,
|
||||||
|
region.x, region.y, region.width, region.height,
|
||||||
|
0, 0, region.width, region.height
|
||||||
|
);
|
||||||
|
|
||||||
|
// Convert to grayscale and invert (similar to main.py processing)
|
||||||
|
const imageData = regionCtx.getImageData(0, 0, region.width, region.height);
|
||||||
|
this.preprocessImage(imageData);
|
||||||
|
regionCtx.putImageData(imageData, 0, 0);
|
||||||
|
|
||||||
|
// Configure OCR based on content type
|
||||||
|
let config: any = {};
|
||||||
|
if (key === 'cost') {
|
||||||
|
// Cost field has digits + 'G' for gold (content type: 'digits_with_6')
|
||||||
|
await this.worker!.setParameters({
|
||||||
|
tessedit_char_whitelist: '0123456789G'
|
||||||
|
});
|
||||||
|
} else if (key === 'cycles' || key === 'area') {
|
||||||
|
// Pure digits (content type: 'digits')
|
||||||
|
await this.worker!.setParameters({
|
||||||
|
tessedit_char_whitelist: '0123456789'
|
||||||
|
});
|
||||||
|
} else if (key === 'puzzle') {
|
||||||
|
// Puzzle name - allow alphanumeric, spaces, and dashes
|
||||||
|
await this.worker!.setParameters({
|
||||||
|
tessedit_char_whitelist: 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789 -'
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
// Default - allow all characters
|
||||||
|
await this.worker!.setParameters({
|
||||||
|
tessedit_char_whitelist: ''
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Perform OCR on the region
|
||||||
|
const { data: { text } } = await this.worker!.recognize(regionCanvas);
|
||||||
|
let cleanText = text.trim();
|
||||||
|
|
||||||
|
// Post-process based on field type
|
||||||
|
if (key === 'cost') {
|
||||||
|
// Handle common OCR misreadings where G is read as 6
|
||||||
|
// If the text ends with 6 and looks like it should be G, remove it
|
||||||
|
if (cleanText.endsWith('6') && cleanText.length > 1) {
|
||||||
|
// Check if removing the last character gives a reasonable cost value
|
||||||
|
const withoutLast = cleanText.slice(0, -1);
|
||||||
|
if (/^\d+$/.test(withoutLast)) {
|
||||||
|
cleanText = withoutLast;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Remove any trailing G characters
|
||||||
|
cleanText = cleanText.replace(/G+$/g, '');
|
||||||
|
// Ensure only digits remain
|
||||||
|
cleanText = cleanText.replace(/[^0-9]/g, '');
|
||||||
|
} else if (key === 'cycles' || key === 'area') {
|
||||||
|
// Ensure only digits remain
|
||||||
|
cleanText = cleanText.replace(/[^0-9]/g, '');
|
||||||
|
} else if (key === 'puzzle') {
|
||||||
|
// Post-process puzzle names
|
||||||
|
cleanText = this.processPuzzleName(cleanText);
|
||||||
|
}
|
||||||
|
|
||||||
|
results[key as keyof OpusMagnumData] = cleanText;
|
||||||
|
}
|
||||||
|
|
||||||
|
URL.revokeObjectURL(imageUrl);
|
||||||
|
|
||||||
|
resolve({
|
||||||
|
puzzle: results.puzzle || '',
|
||||||
|
cost: results.cost || '',
|
||||||
|
cycles: results.cycles || '',
|
||||||
|
area: results.area || ''
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
URL.revokeObjectURL(imageUrl);
|
||||||
|
reject(error);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
img.onerror = () => {
|
||||||
|
URL.revokeObjectURL(imageUrl);
|
||||||
|
reject(new Error('Failed to load image'));
|
||||||
|
};
|
||||||
|
|
||||||
|
img.src = imageUrl;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private preprocessImage(imageData: ImageData): void {
|
||||||
|
// Convert to grayscale and invert (similar to cv2.bitwise_not in main.py)
|
||||||
|
const data = imageData.data;
|
||||||
|
|
||||||
|
for (let i = 0; i < data.length; i += 4) {
|
||||||
|
// Convert to grayscale
|
||||||
|
const gray = Math.round(0.299 * data[i] + 0.587 * data[i + 1] + 0.114 * data[i + 2]);
|
||||||
|
|
||||||
|
// Invert the grayscale value
|
||||||
|
const inverted = 255 - gray;
|
||||||
|
|
||||||
|
data[i] = inverted; // Red
|
||||||
|
data[i + 1] = inverted; // Green
|
||||||
|
data[i + 2] = inverted; // Blue
|
||||||
|
// Alpha channel (data[i + 3]) remains unchanged
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private processPuzzleName(rawText: string): string {
|
||||||
|
let processed = rawText.trim();
|
||||||
|
|
||||||
|
// If no dash is present but we have digits, try to insert one
|
||||||
|
if (!processed.includes('-') && /\d/.test(processed)) {
|
||||||
|
// Common pattern: "P4141" should become "P41-41"
|
||||||
|
// Look for patterns like P[digits][digits] where the last part might be a separate number
|
||||||
|
const match = processed.match(/^([A-Z]+\d+)(\d{1,3})$/);
|
||||||
|
if (match) {
|
||||||
|
processed = `${match[1]}-${match[2]}`;
|
||||||
|
}
|
||||||
|
// Handle cases like "4141" -> "41-41" (missing P prefix)
|
||||||
|
else if (/^\d{3,4}$/.test(processed)) {
|
||||||
|
const mid = Math.floor(processed.length / 2);
|
||||||
|
processed = `P${processed.slice(0, mid)}-${processed.slice(mid)}`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Clean up spacing around dashes
|
||||||
|
processed = processed.replace(/\s*-\s*/g, '-');
|
||||||
|
|
||||||
|
// Ensure proper spacing
|
||||||
|
processed = processed.replace(/([A-Z])(\d)/g, '$1$2');
|
||||||
|
processed = processed.replace(/(\d)([A-Z])/g, '$1 $2');
|
||||||
|
|
||||||
|
// Add P prefix if missing and starts with digits
|
||||||
|
if (/^\d/.test(processed) && !processed.startsWith('P')) {
|
||||||
|
processed = 'P' + processed;
|
||||||
|
}
|
||||||
|
|
||||||
|
return processed;
|
||||||
|
}
|
||||||
|
|
||||||
|
async terminate(): Promise<void> {
|
||||||
|
if (this.worker) {
|
||||||
|
await this.worker.terminate();
|
||||||
|
this.worker = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Utility method to validate if an image looks like an Opus Magnum screenshot
|
||||||
|
static isValidOpusMagnumImage(file: File): boolean {
|
||||||
|
// Basic validation - could be enhanced with actual image analysis
|
||||||
|
const validTypes = ['image/jpeg', 'image/jpg', 'image/png', 'image/gif'];
|
||||||
|
return validTypes.includes(file.type);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Debug method to visualize OCR regions (similar to main.py debug rectangles)
|
||||||
|
static drawDebugRegions(imageFile: File): Promise<string> {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
const imageUrl = URL.createObjectURL(imageFile);
|
||||||
|
const img = new Image();
|
||||||
|
|
||||||
|
img.onload = () => {
|
||||||
|
const canvas = document.createElement('canvas');
|
||||||
|
const ctx = canvas.getContext('2d')!;
|
||||||
|
|
||||||
|
canvas.width = img.width;
|
||||||
|
canvas.height = img.height;
|
||||||
|
ctx.drawImage(img, 0, 0);
|
||||||
|
|
||||||
|
// Draw debug rectangles
|
||||||
|
ctx.strokeStyle = '#00ff00';
|
||||||
|
ctx.lineWidth = 2;
|
||||||
|
|
||||||
|
const service = new OpusMagnumOCRService();
|
||||||
|
Object.values(service.regions).forEach(region => {
|
||||||
|
ctx.strokeRect(region.x, region.y, region.width, region.height);
|
||||||
|
});
|
||||||
|
|
||||||
|
URL.revokeObjectURL(imageUrl);
|
||||||
|
resolve(canvas.toDataURL());
|
||||||
|
};
|
||||||
|
|
||||||
|
img.onerror = () => {
|
||||||
|
URL.revokeObjectURL(imageUrl);
|
||||||
|
reject(new Error('Failed to load image for debug'));
|
||||||
|
};
|
||||||
|
|
||||||
|
img.src = imageUrl;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Singleton instance for the application
|
||||||
|
export const ocrService = new OpusMagnumOCRService();
|
||||||
39
opus_submitter/src/style.css
Normal file
39
opus_submitter/src/style.css
Normal file
@ -0,0 +1,39 @@
|
|||||||
|
@import '@mdi/font/css/materialdesignicons.css';
|
||||||
|
@import "tailwindcss";
|
||||||
|
|
||||||
|
@plugin "daisyui";
|
||||||
|
@plugin "daisyui/theme" {
|
||||||
|
name: "dim";
|
||||||
|
default: false;
|
||||||
|
prefersdark: false;
|
||||||
|
color-scheme: "dark";
|
||||||
|
--color-base-100: oklch(30.857% 0.023 264.149);
|
||||||
|
--color-base-200: oklch(28.036% 0.019 264.182);
|
||||||
|
--color-base-300: oklch(26.346% 0.018 262.177);
|
||||||
|
--color-base-content: oklch(82.901% 0.031 222.959);
|
||||||
|
--color-primary: oklch(86.133% 0.141 139.549);
|
||||||
|
--color-primary-content: oklch(17.226% 0.028 139.549);
|
||||||
|
--color-secondary: oklch(73.375% 0.165 35.353);
|
||||||
|
--color-secondary-content: oklch(14.675% 0.033 35.353);
|
||||||
|
--color-accent: oklch(74.229% 0.133 311.379);
|
||||||
|
--color-accent-content: oklch(14.845% 0.026 311.379);
|
||||||
|
--color-neutral: oklch(24.731% 0.02 264.094);
|
||||||
|
--color-neutral-content: oklch(82.901% 0.031 222.959);
|
||||||
|
--color-info: oklch(86.078% 0.142 206.182);
|
||||||
|
--color-info-content: oklch(17.215% 0.028 206.182);
|
||||||
|
--color-success: oklch(86.171% 0.142 166.534);
|
||||||
|
--color-success-content: oklch(17.234% 0.028 166.534);
|
||||||
|
--color-warning: oklch(86.163% 0.142 94.818);
|
||||||
|
--color-warning-content: oklch(17.232% 0.028 94.818);
|
||||||
|
--color-error: oklch(82.418% 0.099 33.756);
|
||||||
|
--color-error-content: oklch(16.483% 0.019 33.756);
|
||||||
|
--radius-selector: 2rem;
|
||||||
|
--radius-field: 0.25rem;
|
||||||
|
--radius-box: 0.25rem;
|
||||||
|
--size-selector: 0.25rem;
|
||||||
|
--size-field: 0.25rem;
|
||||||
|
--border: 1px;
|
||||||
|
--depth: 0;
|
||||||
|
--noise: 0;
|
||||||
|
}
|
||||||
|
|
||||||
59
opus_submitter/src/types/index.ts
Normal file
59
opus_submitter/src/types/index.ts
Normal file
@ -0,0 +1,59 @@
|
|||||||
|
export interface SteamCollection {
|
||||||
|
id: number
|
||||||
|
steam_id: string
|
||||||
|
title: string
|
||||||
|
description: string
|
||||||
|
author_name: string
|
||||||
|
total_items: number
|
||||||
|
unique_visitors: number
|
||||||
|
current_favorites: number
|
||||||
|
created_at: string
|
||||||
|
updated_at: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface SteamCollectionItem {
|
||||||
|
id: number
|
||||||
|
steam_item_id: string
|
||||||
|
title: string
|
||||||
|
author_name: string
|
||||||
|
description: string
|
||||||
|
tags: string[]
|
||||||
|
order_index: number
|
||||||
|
collection: number
|
||||||
|
created_at: string
|
||||||
|
updated_at: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface OpusMagnumData {
|
||||||
|
puzzle: string
|
||||||
|
cost: string
|
||||||
|
cycles: string
|
||||||
|
area: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface SubmissionFile {
|
||||||
|
file: File
|
||||||
|
preview: string
|
||||||
|
type: 'image' | 'gif'
|
||||||
|
ocrData?: OpusMagnumData
|
||||||
|
ocrProcessing?: boolean
|
||||||
|
ocrError?: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface PuzzleResponse {
|
||||||
|
id?: number
|
||||||
|
puzzle_id: number
|
||||||
|
puzzle_name: string
|
||||||
|
cost?: string
|
||||||
|
cycles?: string
|
||||||
|
area?: string
|
||||||
|
files: SubmissionFile[]
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface Submission {
|
||||||
|
id?: number
|
||||||
|
responses: PuzzleResponse[]
|
||||||
|
notes?: string
|
||||||
|
created_at?: string
|
||||||
|
updated_at?: string
|
||||||
|
}
|
||||||
@ -1,11 +1,11 @@
|
|||||||
<!doctype html>
|
<!doctype html>
|
||||||
{% load django_vite %}
|
{% load django_vite %}
|
||||||
<html lang="en">
|
<html lang="en" data-theme="dim">
|
||||||
<head>
|
<head>
|
||||||
<meta charset="UTF-8" />
|
<meta charset="UTF-8" />
|
||||||
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
|
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
<title>opus_submitter</title>
|
<title>Opus Magnum Puzzle Submitter</title>
|
||||||
{% vite_hmr_client %}
|
{% vite_hmr_client %}
|
||||||
{% vite_asset 'src/main.ts' %}
|
{% vite_asset 'src/main.ts' %}
|
||||||
</head>
|
</head>
|
||||||
|
|||||||
@ -2,11 +2,12 @@ import { defineConfig } from 'vite'
|
|||||||
import vue from '@vitejs/plugin-vue'
|
import vue from '@vitejs/plugin-vue'
|
||||||
import { resolve } from 'path'
|
import { resolve } from 'path'
|
||||||
import { fileURLToPath } from 'node:url'
|
import { fileURLToPath } from 'node:url'
|
||||||
|
import tailwindcss from '@tailwindcss/vite'
|
||||||
|
|
||||||
// https://vitejs.dev/config/
|
// https://vitejs.dev/config/
|
||||||
export default defineConfig({
|
export default defineConfig({
|
||||||
base: '/static/',
|
base: '/static/',
|
||||||
plugins: [vue()],
|
plugins: [vue(), tailwindcss()],
|
||||||
resolve: {
|
resolve: {
|
||||||
alias: {
|
alias: {
|
||||||
'@': fileURLToPath(new URL('./src', import.meta.url)),
|
'@': fileURLToPath(new URL('./src', import.meta.url)),
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user