try for better ocr puzzle
This commit is contained in:
parent
8960f551e6
commit
15de496501
0
opus_submitter/opus_submitter/settingsLocal.py.dist
Normal file
0
opus_submitter/opus_submitter/settingsLocal.py.dist
Normal file
@ -12,6 +12,7 @@
|
|||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@tailwindcss/vite": "^4.1.16",
|
"@tailwindcss/vite": "^4.1.16",
|
||||||
"install": "^0.13.0",
|
"install": "^0.13.0",
|
||||||
|
"pinia": "^3.0.3",
|
||||||
"tailwindcss": "^4.1.16",
|
"tailwindcss": "^4.1.16",
|
||||||
"tesseract.js": "^5.1.1",
|
"tesseract.js": "^5.1.1",
|
||||||
"vue": "^3.5.22"
|
"vue": "^3.5.22"
|
||||||
|
|||||||
@ -14,6 +14,9 @@ importers:
|
|||||||
install:
|
install:
|
||||||
specifier: ^0.13.0
|
specifier: ^0.13.0
|
||||||
version: 0.13.0
|
version: 0.13.0
|
||||||
|
pinia:
|
||||||
|
specifier: ^3.0.3
|
||||||
|
version: 3.0.3(typescript@5.9.3)(vue@3.5.22(typescript@5.9.3))
|
||||||
tailwindcss:
|
tailwindcss:
|
||||||
specifier: ^4.1.16
|
specifier: ^4.1.16
|
||||||
version: 4.1.16
|
version: 4.1.16
|
||||||
@ -480,6 +483,15 @@ packages:
|
|||||||
'@vue/compiler-ssr@3.5.22':
|
'@vue/compiler-ssr@3.5.22':
|
||||||
resolution: {integrity: sha512-GdgyLvg4R+7T8Nk2Mlighx7XGxq/fJf9jaVofc3IL0EPesTE86cP/8DD1lT3h1JeZr2ySBvyqKQJgbS54IX1Ww==}
|
resolution: {integrity: sha512-GdgyLvg4R+7T8Nk2Mlighx7XGxq/fJf9jaVofc3IL0EPesTE86cP/8DD1lT3h1JeZr2ySBvyqKQJgbS54IX1Ww==}
|
||||||
|
|
||||||
|
'@vue/devtools-api@7.7.7':
|
||||||
|
resolution: {integrity: sha512-lwOnNBH2e7x1fIIbVT7yF5D+YWhqELm55/4ZKf45R9T8r9dE2AIOy8HKjfqzGsoTHFbWbr337O4E0A0QADnjBg==}
|
||||||
|
|
||||||
|
'@vue/devtools-kit@7.7.7':
|
||||||
|
resolution: {integrity: sha512-wgoZtxcTta65cnZ1Q6MbAfePVFxfM+gq0saaeytoph7nEa7yMXoi6sCPy4ufO111B9msnw0VOWjPEFCXuAKRHA==}
|
||||||
|
|
||||||
|
'@vue/devtools-shared@7.7.7':
|
||||||
|
resolution: {integrity: sha512-+udSj47aRl5aKb0memBvcUG9koarqnxNM5yjuREvqwK6T3ap4mn3Zqqc17QrBFTqSMjr3HK1cvStEZpMDpfdyw==}
|
||||||
|
|
||||||
'@vue/language-core@3.1.2':
|
'@vue/language-core@3.1.2':
|
||||||
resolution: {integrity: sha512-PyFDCqpdfYUT+oMLqcc61oHfJlC6yjhybaefwQjRdkchIihToOEpJ2Wu/Ebq2yrnJdd1EsaAvZaXVAqcxtnDxQ==}
|
resolution: {integrity: sha512-PyFDCqpdfYUT+oMLqcc61oHfJlC6yjhybaefwQjRdkchIihToOEpJ2Wu/Ebq2yrnJdd1EsaAvZaXVAqcxtnDxQ==}
|
||||||
peerDependencies:
|
peerDependencies:
|
||||||
@ -519,9 +531,16 @@ 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==}
|
||||||
|
|
||||||
|
birpc@2.6.1:
|
||||||
|
resolution: {integrity: sha512-LPnFhlDpdSH6FJhJyn4M0kFO7vtQ5iPw24FnG0y21q09xC7e8+1LeR31S1MAIrDAHp4m7aas4bEkTDTvMAtebQ==}
|
||||||
|
|
||||||
bmp-js@0.1.0:
|
bmp-js@0.1.0:
|
||||||
resolution: {integrity: sha512-vHdS19CnY3hwiNdkaqk93DvjVLfbEcI8mys4UjuWrlX1haDmroo8o4xCzh4wD6DGV6HxRCyauwhHRqMTfERtjw==}
|
resolution: {integrity: sha512-vHdS19CnY3hwiNdkaqk93DvjVLfbEcI8mys4UjuWrlX1haDmroo8o4xCzh4wD6DGV6HxRCyauwhHRqMTfERtjw==}
|
||||||
|
|
||||||
|
copy-anything@4.0.5:
|
||||||
|
resolution: {integrity: sha512-7Vv6asjS4gMOuILabD3l739tsaxFQmC+a7pLZm02zyvs8p977bL3zEgq3yDk5rn9B0PbYgIv++jmHcuUab4RhA==}
|
||||||
|
engines: {node: '>=18'}
|
||||||
|
|
||||||
csstype@3.1.3:
|
csstype@3.1.3:
|
||||||
resolution: {integrity: sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==}
|
resolution: {integrity: sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==}
|
||||||
|
|
||||||
@ -565,6 +584,9 @@ packages:
|
|||||||
graceful-fs@4.2.11:
|
graceful-fs@4.2.11:
|
||||||
resolution: {integrity: sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==}
|
resolution: {integrity: sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==}
|
||||||
|
|
||||||
|
hookable@5.5.3:
|
||||||
|
resolution: {integrity: sha512-Yc+BQe8SvoXH1643Qez1zqLRmbA5rCL+sSmk6TVos0LWVfNIB7PGncdlId77WzLGSIB5KaWgTaNTs2lNVEI6VQ==}
|
||||||
|
|
||||||
idb-keyval@6.2.2:
|
idb-keyval@6.2.2:
|
||||||
resolution: {integrity: sha512-yjD9nARJ/jb1g+CvD0tlhUHOrJ9Sy0P8T9MF3YaLlHnSRpwPfpTX0XIvpmw3gAJUmEu3FiICLBDPXVwyEvrleg==}
|
resolution: {integrity: sha512-yjD9nARJ/jb1g+CvD0tlhUHOrJ9Sy0P8T9MF3YaLlHnSRpwPfpTX0XIvpmw3gAJUmEu3FiICLBDPXVwyEvrleg==}
|
||||||
|
|
||||||
@ -578,6 +600,10 @@ packages:
|
|||||||
is-url@1.2.4:
|
is-url@1.2.4:
|
||||||
resolution: {integrity: sha512-ITvGim8FhRiYe4IQ5uHSkj7pVaPDrCTkNd3yq3cV7iZAcJdHTUMPMEHcqSOy9xZ9qFenQCvi+2wjH9a1nXqHww==}
|
resolution: {integrity: sha512-ITvGim8FhRiYe4IQ5uHSkj7pVaPDrCTkNd3yq3cV7iZAcJdHTUMPMEHcqSOy9xZ9qFenQCvi+2wjH9a1nXqHww==}
|
||||||
|
|
||||||
|
is-what@5.5.0:
|
||||||
|
resolution: {integrity: sha512-oG7cgbmg5kLYae2N5IVd3jm2s+vldjxJzK1pcu9LfpGuQ93MQSzo0okvRna+7y5ifrD+20FE8FvjusyGaz14fw==}
|
||||||
|
engines: {node: '>=18'}
|
||||||
|
|
||||||
jiti@2.6.1:
|
jiti@2.6.1:
|
||||||
resolution: {integrity: sha512-ekilCSN1jwRvIbgeg/57YFh8qQDNbwDb9xT/qu2DAHbFFZUicIl4ygVaAvzveMhMVr3LnpSKTNnwt8PoOfmKhQ==}
|
resolution: {integrity: sha512-ekilCSN1jwRvIbgeg/57YFh8qQDNbwDb9xT/qu2DAHbFFZUicIl4ygVaAvzveMhMVr3LnpSKTNnwt8PoOfmKhQ==}
|
||||||
hasBin: true
|
hasBin: true
|
||||||
@ -655,6 +681,9 @@ packages:
|
|||||||
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==}
|
||||||
|
|
||||||
|
mitt@3.0.1:
|
||||||
|
resolution: {integrity: sha512-vKivATfr97l2/QBCYAkXYDbrIWPM2IIKEl7YPhjCvKlG3kE2gm+uBo6nEXK3M5/Ffh/FLpKExzOQ3JJoJGFKBw==}
|
||||||
|
|
||||||
muggle-string@0.4.1:
|
muggle-string@0.4.1:
|
||||||
resolution: {integrity: sha512-VNTrAak/KhO2i8dqqnqnAHOa3cYBwXEZe9h+D5h/1ZqFSTEFHdM65lR7RoIqq3tBBYavsOXV84NoHXZ0AkPyqQ==}
|
resolution: {integrity: sha512-VNTrAak/KhO2i8dqqnqnAHOa3cYBwXEZe9h+D5h/1ZqFSTEFHdM65lR7RoIqq3tBBYavsOXV84NoHXZ0AkPyqQ==}
|
||||||
|
|
||||||
@ -679,6 +708,9 @@ packages:
|
|||||||
path-browserify@1.0.1:
|
path-browserify@1.0.1:
|
||||||
resolution: {integrity: sha512-b7uo2UCUOYZcnF/3ID0lulOJi/bafxa1xPe7ZPsammBSpjSWQkjNxlt635YGS2MiR9GjvuXCtz2emr3jbsz98g==}
|
resolution: {integrity: sha512-b7uo2UCUOYZcnF/3ID0lulOJi/bafxa1xPe7ZPsammBSpjSWQkjNxlt635YGS2MiR9GjvuXCtz2emr3jbsz98g==}
|
||||||
|
|
||||||
|
perfect-debounce@1.0.0:
|
||||||
|
resolution: {integrity: sha512-xCy9V055GLEqoFaHoC1SoLIaLmWctgCUaBaWxDZ7/Zx4CTyX7cJQLJOok/orfjZAh9kEYpjJa4d0KcJmCbctZA==}
|
||||||
|
|
||||||
picocolors@1.1.1:
|
picocolors@1.1.1:
|
||||||
resolution: {integrity: sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==}
|
resolution: {integrity: sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==}
|
||||||
|
|
||||||
@ -686,6 +718,15 @@ packages:
|
|||||||
resolution: {integrity: sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==}
|
resolution: {integrity: sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==}
|
||||||
engines: {node: '>=12'}
|
engines: {node: '>=12'}
|
||||||
|
|
||||||
|
pinia@3.0.3:
|
||||||
|
resolution: {integrity: sha512-ttXO/InUULUXkMHpTdp9Fj4hLpD/2AoJdmAbAeW2yu1iy1k+pkFekQXw5VpC0/5p51IOR/jDaDRfRWRnMMsGOA==}
|
||||||
|
peerDependencies:
|
||||||
|
typescript: '>=4.4.4'
|
||||||
|
vue: ^2.7.0 || ^3.5.11
|
||||||
|
peerDependenciesMeta:
|
||||||
|
typescript:
|
||||||
|
optional: true
|
||||||
|
|
||||||
postcss@8.5.6:
|
postcss@8.5.6:
|
||||||
resolution: {integrity: sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg==}
|
resolution: {integrity: sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg==}
|
||||||
engines: {node: ^10 || ^12 || >=14}
|
engines: {node: ^10 || ^12 || >=14}
|
||||||
@ -693,6 +734,9 @@ packages:
|
|||||||
regenerator-runtime@0.13.11:
|
regenerator-runtime@0.13.11:
|
||||||
resolution: {integrity: sha512-kY1AZVr2Ra+t+piVaJ4gxaFaReZVH40AKNo7UCX6W+dEwBo/2oZJzqfuN1qLq1oL45o56cPaTXELwrTh8Fpggg==}
|
resolution: {integrity: sha512-kY1AZVr2Ra+t+piVaJ4gxaFaReZVH40AKNo7UCX6W+dEwBo/2oZJzqfuN1qLq1oL45o56cPaTXELwrTh8Fpggg==}
|
||||||
|
|
||||||
|
rfdc@1.4.1:
|
||||||
|
resolution: {integrity: sha512-q1b3N5QkRUWUl7iyylaaj3kOpIT0N2i9MqIEQXP73GVsN9cw3fdx8X63cEmWhJGi2PPCF23Ijp7ktmd39rawIA==}
|
||||||
|
|
||||||
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'}
|
||||||
@ -702,6 +746,14 @@ 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'}
|
||||||
|
|
||||||
|
speakingurl@14.0.1:
|
||||||
|
resolution: {integrity: sha512-1POYv7uv2gXoyGFpBCmpDVSNV74IfsWlDW216UPjbWufNf+bSU6GdbDsxdcxtfwb4xlI3yxzOTKClUosxARYrQ==}
|
||||||
|
engines: {node: '>=0.10.0'}
|
||||||
|
|
||||||
|
superjson@2.2.5:
|
||||||
|
resolution: {integrity: sha512-zWPTX96LVsA/eVYnqOM2+ofcdPqdS1dAF1LN4TS2/MWuUpfitd9ctTa87wt4xrYnZnkLtS69xpBdSxVBP5Rm6w==}
|
||||||
|
engines: {node: '>=16'}
|
||||||
|
|
||||||
tailwindcss@4.1.16:
|
tailwindcss@4.1.16:
|
||||||
resolution: {integrity: sha512-pONL5awpaQX4LN5eiv7moSiSPd/DLDzKVRJz8Q9PgzmAdd1R4307GQS2ZpfiN7ZmekdQrfhZZiSE5jkLR4WNaA==}
|
resolution: {integrity: sha512-pONL5awpaQX4LN5eiv7moSiSPd/DLDzKVRJz8Q9PgzmAdd1R4307GQS2ZpfiN7ZmekdQrfhZZiSE5jkLR4WNaA==}
|
||||||
|
|
||||||
@ -1103,6 +1155,24 @@ snapshots:
|
|||||||
'@vue/compiler-dom': 3.5.22
|
'@vue/compiler-dom': 3.5.22
|
||||||
'@vue/shared': 3.5.22
|
'@vue/shared': 3.5.22
|
||||||
|
|
||||||
|
'@vue/devtools-api@7.7.7':
|
||||||
|
dependencies:
|
||||||
|
'@vue/devtools-kit': 7.7.7
|
||||||
|
|
||||||
|
'@vue/devtools-kit@7.7.7':
|
||||||
|
dependencies:
|
||||||
|
'@vue/devtools-shared': 7.7.7
|
||||||
|
birpc: 2.6.1
|
||||||
|
hookable: 5.5.3
|
||||||
|
mitt: 3.0.1
|
||||||
|
perfect-debounce: 1.0.0
|
||||||
|
speakingurl: 14.0.1
|
||||||
|
superjson: 2.2.5
|
||||||
|
|
||||||
|
'@vue/devtools-shared@7.7.7':
|
||||||
|
dependencies:
|
||||||
|
rfdc: 1.4.1
|
||||||
|
|
||||||
'@vue/language-core@3.1.2(typescript@5.9.3)':
|
'@vue/language-core@3.1.2(typescript@5.9.3)':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@volar/language-core': 2.4.23
|
'@volar/language-core': 2.4.23
|
||||||
@ -1146,8 +1216,14 @@ snapshots:
|
|||||||
|
|
||||||
alien-signals@3.0.3: {}
|
alien-signals@3.0.3: {}
|
||||||
|
|
||||||
|
birpc@2.6.1: {}
|
||||||
|
|
||||||
bmp-js@0.1.0: {}
|
bmp-js@0.1.0: {}
|
||||||
|
|
||||||
|
copy-anything@4.0.5:
|
||||||
|
dependencies:
|
||||||
|
is-what: 5.5.0
|
||||||
|
|
||||||
csstype@3.1.3: {}
|
csstype@3.1.3: {}
|
||||||
|
|
||||||
daisyui@5.3.10: {}
|
daisyui@5.3.10: {}
|
||||||
@ -1201,6 +1277,8 @@ snapshots:
|
|||||||
|
|
||||||
graceful-fs@4.2.11: {}
|
graceful-fs@4.2.11: {}
|
||||||
|
|
||||||
|
hookable@5.5.3: {}
|
||||||
|
|
||||||
idb-keyval@6.2.2: {}
|
idb-keyval@6.2.2: {}
|
||||||
|
|
||||||
install@0.13.0: {}
|
install@0.13.0: {}
|
||||||
@ -1209,6 +1287,8 @@ snapshots:
|
|||||||
|
|
||||||
is-url@1.2.4: {}
|
is-url@1.2.4: {}
|
||||||
|
|
||||||
|
is-what@5.5.0: {}
|
||||||
|
|
||||||
jiti@2.6.1: {}
|
jiti@2.6.1: {}
|
||||||
|
|
||||||
lightningcss-android-arm64@1.30.2:
|
lightningcss-android-arm64@1.30.2:
|
||||||
@ -1264,6 +1344,8 @@ snapshots:
|
|||||||
dependencies:
|
dependencies:
|
||||||
'@jridgewell/sourcemap-codec': 1.5.5
|
'@jridgewell/sourcemap-codec': 1.5.5
|
||||||
|
|
||||||
|
mitt@3.0.1: {}
|
||||||
|
|
||||||
muggle-string@0.4.1: {}
|
muggle-string@0.4.1: {}
|
||||||
|
|
||||||
nanoid@3.3.11: {}
|
nanoid@3.3.11: {}
|
||||||
@ -1276,10 +1358,19 @@ snapshots:
|
|||||||
|
|
||||||
path-browserify@1.0.1: {}
|
path-browserify@1.0.1: {}
|
||||||
|
|
||||||
|
perfect-debounce@1.0.0: {}
|
||||||
|
|
||||||
picocolors@1.1.1: {}
|
picocolors@1.1.1: {}
|
||||||
|
|
||||||
picomatch@4.0.3: {}
|
picomatch@4.0.3: {}
|
||||||
|
|
||||||
|
pinia@3.0.3(typescript@5.9.3)(vue@3.5.22(typescript@5.9.3)):
|
||||||
|
dependencies:
|
||||||
|
'@vue/devtools-api': 7.7.7
|
||||||
|
vue: 3.5.22(typescript@5.9.3)
|
||||||
|
optionalDependencies:
|
||||||
|
typescript: 5.9.3
|
||||||
|
|
||||||
postcss@8.5.6:
|
postcss@8.5.6:
|
||||||
dependencies:
|
dependencies:
|
||||||
nanoid: 3.3.11
|
nanoid: 3.3.11
|
||||||
@ -1288,6 +1379,8 @@ snapshots:
|
|||||||
|
|
||||||
regenerator-runtime@0.13.11: {}
|
regenerator-runtime@0.13.11: {}
|
||||||
|
|
||||||
|
rfdc@1.4.1: {}
|
||||||
|
|
||||||
rollup@4.52.5:
|
rollup@4.52.5:
|
||||||
dependencies:
|
dependencies:
|
||||||
'@types/estree': 1.0.8
|
'@types/estree': 1.0.8
|
||||||
@ -1318,6 +1411,12 @@ snapshots:
|
|||||||
|
|
||||||
source-map-js@1.2.1: {}
|
source-map-js@1.2.1: {}
|
||||||
|
|
||||||
|
speakingurl@14.0.1: {}
|
||||||
|
|
||||||
|
superjson@2.2.5:
|
||||||
|
dependencies:
|
||||||
|
copy-anything: 4.0.5
|
||||||
|
|
||||||
tailwindcss@4.1.16: {}
|
tailwindcss@4.1.16: {}
|
||||||
|
|
||||||
tapable@2.3.0: {}
|
tapable@2.3.0: {}
|
||||||
|
|||||||
@ -3,16 +3,19 @@ import { ref, onMounted, computed } from 'vue'
|
|||||||
import PuzzleCard from './components/PuzzleCard.vue'
|
import PuzzleCard from './components/PuzzleCard.vue'
|
||||||
import SubmissionForm from './components/SubmissionForm.vue'
|
import SubmissionForm from './components/SubmissionForm.vue'
|
||||||
import AdminPanel from './components/AdminPanel.vue'
|
import AdminPanel from './components/AdminPanel.vue'
|
||||||
import { puzzleHelpers, submissionHelpers, errorHelpers, apiService } from './services/apiService'
|
import { apiService, errorHelpers } from './services/apiService'
|
||||||
import type { SteamCollection, SteamCollectionItem, Submission, PuzzleResponse, UserInfo } from './types'
|
import { usePuzzlesStore } from './stores/puzzles'
|
||||||
|
import { useSubmissionsStore } from './stores/submissions'
|
||||||
|
import type { SteamCollection, PuzzleResponse, UserInfo } from './types'
|
||||||
|
|
||||||
// API data
|
// Pinia stores
|
||||||
|
const puzzlesStore = usePuzzlesStore()
|
||||||
|
const submissionsStore = useSubmissionsStore()
|
||||||
|
|
||||||
|
// Local state
|
||||||
const collections = ref<SteamCollection[]>([])
|
const collections = ref<SteamCollection[]>([])
|
||||||
const puzzles = ref<SteamCollectionItem[]>([])
|
|
||||||
const submissions = ref<Submission[]>([])
|
|
||||||
const userInfo = ref<UserInfo | null>(null)
|
const userInfo = ref<UserInfo | null>(null)
|
||||||
const isLoading = ref(true)
|
const isLoading = ref(true)
|
||||||
const showSubmissionModal = ref(false)
|
|
||||||
const error = ref<string>('')
|
const error = ref<string>('')
|
||||||
|
|
||||||
// Mock data removed - using API data only
|
// Mock data removed - using API data only
|
||||||
@ -25,7 +28,7 @@ const isSuperuser = computed(() => {
|
|||||||
// Computed property to get responses grouped by puzzle
|
// Computed property to get responses grouped by puzzle
|
||||||
const responsesByPuzzle = computed(() => {
|
const responsesByPuzzle = computed(() => {
|
||||||
const grouped: Record<number, PuzzleResponse[]> = {}
|
const grouped: Record<number, PuzzleResponse[]> = {}
|
||||||
submissions.value.forEach(submission => {
|
submissionsStore.submissions.forEach(submission => {
|
||||||
submission.responses.forEach(response => {
|
submission.responses.forEach(response => {
|
||||||
// Handle both number and object types for puzzle field
|
// Handle both number and object types for puzzle field
|
||||||
const puzzleId = typeof response.puzzle === 'number' ? response.puzzle : response.puzzle.id
|
const puzzleId = typeof response.puzzle === 'number' ? response.puzzle : response.puzzle.id
|
||||||
@ -55,21 +58,20 @@ onMounted(async () => {
|
|||||||
console.warn('User info error:', userResponse.error)
|
console.warn('User info error:', userResponse.error)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Load puzzles from API
|
// Load puzzles from API using store
|
||||||
console.log('Loading puzzles...')
|
console.log('Loading puzzles...')
|
||||||
const loadedPuzzles = await puzzleHelpers.loadPuzzles()
|
await puzzlesStore.loadPuzzles()
|
||||||
puzzles.value = loadedPuzzles
|
console.log('Puzzles loaded:', puzzlesStore.puzzles.length)
|
||||||
console.log('Puzzles loaded:', loadedPuzzles.length)
|
|
||||||
|
|
||||||
// Create mock collection from loaded puzzles for display
|
// Create mock collection from loaded puzzles for display
|
||||||
if (loadedPuzzles.length > 0) {
|
if (puzzlesStore.puzzles.length > 0) {
|
||||||
collections.value = [{
|
collections.value = [{
|
||||||
id: 1,
|
id: 1,
|
||||||
steam_id: '3479142989',
|
steam_id: '3479142989',
|
||||||
title: 'PolyLAN 41',
|
title: 'PolyLAN 41',
|
||||||
description: 'Puzzle collection for PolyLAN 41 fil rouge',
|
description: 'Puzzle collection for PolyLAN 41 fil rouge',
|
||||||
author_name: 'Flame Legrems',
|
author_name: 'Flame Legrems',
|
||||||
total_items: loadedPuzzles.length,
|
total_items: puzzlesStore.puzzles.length,
|
||||||
unique_visitors: 31,
|
unique_visitors: 31,
|
||||||
current_favorites: 1,
|
current_favorites: 1,
|
||||||
created_at: new Date().toISOString(),
|
created_at: new Date().toISOString(),
|
||||||
@ -78,11 +80,10 @@ onMounted(async () => {
|
|||||||
console.log('Collection created')
|
console.log('Collection created')
|
||||||
}
|
}
|
||||||
|
|
||||||
// Load existing submissions
|
// Load existing submissions using store
|
||||||
console.log('Loading submissions...')
|
console.log('Loading submissions...')
|
||||||
const loadedSubmissions = await submissionHelpers.loadSubmissions()
|
await submissionsStore.loadSubmissions()
|
||||||
submissions.value = loadedSubmissions
|
console.log('Submissions loaded:', submissionsStore.submissions.length)
|
||||||
console.log('Submissions loaded:', loadedSubmissions.length)
|
|
||||||
|
|
||||||
console.log('Data load complete!')
|
console.log('Data load complete!')
|
||||||
|
|
||||||
@ -104,32 +105,24 @@ const handleSubmission = async (submissionData: {
|
|||||||
isLoading.value = true
|
isLoading.value = true
|
||||||
error.value = ''
|
error.value = ''
|
||||||
|
|
||||||
// Create submission via API
|
// Create submission via store
|
||||||
const response = await submissionHelpers.createFromFiles(
|
const submission = await submissionsStore.createSubmission(
|
||||||
submissionData.files,
|
submissionData.files,
|
||||||
puzzles.value,
|
|
||||||
submissionData.notes,
|
submissionData.notes,
|
||||||
submissionData.manualValidationRequested
|
submissionData.manualValidationRequested
|
||||||
)
|
)
|
||||||
|
|
||||||
if (response.error) {
|
// Show success message
|
||||||
error.value = response.error
|
if (submission) {
|
||||||
alert(`Submission failed: ${response.error}`)
|
const puzzleNames = submission.responses.map(r => r.puzzle_name).join(', ')
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if (response.data) {
|
|
||||||
// Add to local submissions list
|
|
||||||
submissions.value.unshift(response.data)
|
|
||||||
|
|
||||||
// Show success message
|
|
||||||
const puzzleNames = response.data.responses.map(r => r.puzzle_name).join(', ')
|
|
||||||
alert(`Solutions submitted successfully for puzzles: ${puzzleNames}`)
|
alert(`Solutions submitted successfully for puzzles: ${puzzleNames}`)
|
||||||
|
} else {
|
||||||
// Close modal
|
alert('Submission created successfully!')
|
||||||
showSubmissionModal.value = false
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Close modal
|
||||||
|
submissionsStore.closeSubmissionModal()
|
||||||
|
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
const errorMessage = errorHelpers.getErrorMessage(err)
|
const errorMessage = errorHelpers.getErrorMessage(err)
|
||||||
error.value = errorMessage
|
error.value = errorMessage
|
||||||
@ -141,16 +134,16 @@ const handleSubmission = async (submissionData: {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const openSubmissionModal = () => {
|
const openSubmissionModal = () => {
|
||||||
showSubmissionModal.value = true
|
submissionsStore.openSubmissionModal()
|
||||||
}
|
}
|
||||||
|
|
||||||
const closeSubmissionModal = () => {
|
const closeSubmissionModal = () => {
|
||||||
showSubmissionModal.value = false
|
submissionsStore.closeSubmissionModal()
|
||||||
}
|
}
|
||||||
|
|
||||||
// Function to match puzzle name from OCR to actual puzzle
|
// Function to match puzzle name from OCR to actual puzzle
|
||||||
const findPuzzleByName = (ocrPuzzleName: string): SteamCollectionItem | null => {
|
const findPuzzleByName = (ocrPuzzleName: string) => {
|
||||||
return puzzleHelpers.findPuzzleByName(puzzles.value, ocrPuzzleName)
|
return puzzlesStore.findPuzzleByName(ocrPuzzleName)
|
||||||
}
|
}
|
||||||
|
|
||||||
const reloadPage = () => {
|
const reloadPage = () => {
|
||||||
@ -237,7 +230,7 @@ const reloadPage = () => {
|
|||||||
<!-- Puzzles Grid -->
|
<!-- Puzzles Grid -->
|
||||||
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
|
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
|
||||||
<PuzzleCard
|
<PuzzleCard
|
||||||
v-for="puzzle in puzzles"
|
v-for="puzzle in puzzlesStore.puzzles"
|
||||||
:key="puzzle.id"
|
:key="puzzle.id"
|
||||||
:puzzle="puzzle"
|
:puzzle="puzzle"
|
||||||
:responses="responsesByPuzzle[puzzle.id] || []"
|
:responses="responsesByPuzzle[puzzle.id] || []"
|
||||||
@ -245,7 +238,7 @@ const reloadPage = () => {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Empty State -->
|
<!-- Empty State -->
|
||||||
<div v-if="puzzles.length === 0" class="text-center py-12">
|
<div v-if="puzzlesStore.puzzles.length === 0" class="text-center py-12">
|
||||||
<div class="text-6xl mb-4">🧩</div>
|
<div class="text-6xl mb-4">🧩</div>
|
||||||
<h3 class="text-xl font-bold mb-2">No Puzzles Available</h3>
|
<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>
|
<p class="text-base-content/70">Check back later for new puzzle collections!</p>
|
||||||
@ -254,7 +247,7 @@ const reloadPage = () => {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Submission Modal -->
|
<!-- Submission Modal -->
|
||||||
<div v-if="showSubmissionModal" class="modal modal-open">
|
<div v-if="submissionsStore.isSubmissionModalOpen" class="modal modal-open">
|
||||||
<div class="modal-box max-w-4xl">
|
<div class="modal-box max-w-4xl">
|
||||||
<div class="flex justify-between items-center mb-4">
|
<div class="flex justify-between items-center mb-4">
|
||||||
<h3 class="font-bold text-lg">Submit Solution</h3>
|
<h3 class="font-bold text-lg">Submit Solution</h3>
|
||||||
@ -267,7 +260,7 @@ const reloadPage = () => {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<SubmissionForm
|
<SubmissionForm
|
||||||
:puzzles="puzzles"
|
:puzzles="puzzlesStore.puzzles"
|
||||||
:find-puzzle-by-name="findPuzzleByName"
|
:find-puzzle-by-name="findPuzzleByName"
|
||||||
@submit="handleSubmission"
|
@submit="handleSubmission"
|
||||||
/>
|
/>
|
||||||
|
|||||||
@ -181,6 +181,7 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { ref, watch, nextTick } from 'vue'
|
import { ref, watch, nextTick } from 'vue'
|
||||||
import { ocrService } from '../services/ocrService'
|
import { ocrService } from '../services/ocrService'
|
||||||
|
import { usePuzzlesStore } from '@/stores/puzzles'
|
||||||
import type { SubmissionFile, SteamCollectionItem } from '@/types'
|
import type { SubmissionFile, SteamCollectionItem } from '@/types'
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
@ -195,6 +196,9 @@ interface Emits {
|
|||||||
const props = defineProps<Props>()
|
const props = defineProps<Props>()
|
||||||
const emit = defineEmits<Emits>()
|
const emit = defineEmits<Emits>()
|
||||||
|
|
||||||
|
// Pinia store
|
||||||
|
const puzzlesStore = usePuzzlesStore()
|
||||||
|
|
||||||
const fileInput = ref<HTMLInputElement>()
|
const fileInput = ref<HTMLInputElement>()
|
||||||
const isDragOver = ref(false)
|
const isDragOver = ref(false)
|
||||||
const error = ref('')
|
const error = ref('')
|
||||||
@ -211,10 +215,9 @@ watch(files, (newFiles) => {
|
|||||||
}, { deep: true })
|
}, { deep: true })
|
||||||
|
|
||||||
// Watch for puzzle changes and update OCR service
|
// Watch for puzzle changes and update OCR service
|
||||||
watch(() => props.puzzles, (newPuzzles) => {
|
watch(() => puzzlesStore.puzzles, (newPuzzles) => {
|
||||||
if (newPuzzles && newPuzzles.length > 0) {
|
if (newPuzzles && newPuzzles.length > 0) {
|
||||||
const puzzleNames = newPuzzles.map(puzzle => puzzle.title)
|
ocrService.setAvailablePuzzleNames(puzzlesStore.puzzleNames)
|
||||||
ocrService.setAvailablePuzzleNames(puzzleNames)
|
|
||||||
}
|
}
|
||||||
}, { immediate: true })
|
}, { immediate: true })
|
||||||
|
|
||||||
|
|||||||
@ -1,5 +1,8 @@
|
|||||||
import { createApp } from 'vue'
|
import { createApp } from 'vue'
|
||||||
import App from '@/App.vue'
|
import App from '@/App.vue'
|
||||||
|
import { pinia } from '@/stores'
|
||||||
import './style.css'
|
import './style.css'
|
||||||
|
|
||||||
createApp(App).mount('#app')
|
const app = createApp(App)
|
||||||
|
app.use(pinia)
|
||||||
|
app.mount('#app')
|
||||||
|
|||||||
@ -48,6 +48,68 @@ export class OpusMagnumOCRService {
|
|||||||
*/
|
*/
|
||||||
setAvailablePuzzleNames(puzzleNames: string[]): void {
|
setAvailablePuzzleNames(puzzleNames: string[]): void {
|
||||||
this.availablePuzzleNames = puzzleNames;
|
this.availablePuzzleNames = puzzleNames;
|
||||||
|
console.log('OCR service updated with puzzle names:', puzzleNames);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Configure OCR specifically for puzzle name recognition
|
||||||
|
* Uses aggressive character whitelisting and dictionary constraints
|
||||||
|
*/
|
||||||
|
private async configurePuzzleOCR(): Promise<void> {
|
||||||
|
if (!this.worker) return;
|
||||||
|
|
||||||
|
// Configure Tesseract for maximum constraint to our puzzle names
|
||||||
|
await this.worker.setParameters({
|
||||||
|
// Disable all system dictionaries to prevent interference
|
||||||
|
load_system_dawg: '0',
|
||||||
|
load_freq_dawg: '0',
|
||||||
|
load_punc_dawg: '0',
|
||||||
|
load_number_dawg: '0',
|
||||||
|
load_unambig_dawg: '0',
|
||||||
|
load_bigram_dawg: '0',
|
||||||
|
load_fixed_length_dawgs: '0',
|
||||||
|
|
||||||
|
// Use only characters from our puzzle names
|
||||||
|
tessedit_char_whitelist: this.getPuzzleCharacterSet(),
|
||||||
|
|
||||||
|
// Optimize for single words/short phrases
|
||||||
|
tessedit_pageseg_mode: 8 as any, // Single word
|
||||||
|
|
||||||
|
// Increase penalties for non-dictionary words
|
||||||
|
segment_penalty_dict_nonword: '2.0',
|
||||||
|
segment_penalty_dict_frequent_word: '0.001',
|
||||||
|
segment_penalty_dict_case_ok: '0.001',
|
||||||
|
segment_penalty_dict_case_bad: '0.1',
|
||||||
|
|
||||||
|
// Make OCR more conservative about character recognition
|
||||||
|
classify_enable_learning: '0',
|
||||||
|
classify_enable_adaptive_matcher: '1',
|
||||||
|
|
||||||
|
// Preserve word boundaries
|
||||||
|
preserve_interword_spaces: '1'
|
||||||
|
});
|
||||||
|
|
||||||
|
console.log('OCR configured for puzzle names with character set:', this.getPuzzleCharacterSet());
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get character set from available puzzle names for more accurate OCR (fallback)
|
||||||
|
*/
|
||||||
|
private getPuzzleCharacterSet(): string {
|
||||||
|
if (this.availablePuzzleNames.length === 0) {
|
||||||
|
// Fallback to common characters
|
||||||
|
return 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789 -'
|
||||||
|
}
|
||||||
|
|
||||||
|
// Extract unique characters from all puzzle names
|
||||||
|
const chars = new Set<string>()
|
||||||
|
this.availablePuzzleNames.forEach(name => {
|
||||||
|
for (const char of name) {
|
||||||
|
chars.add(char)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
return Array.from(chars).join('')
|
||||||
}
|
}
|
||||||
|
|
||||||
async extractOpusMagnumData(imageFile: File): Promise<OpusMagnumData> {
|
async extractOpusMagnumData(imageFile: File): Promise<OpusMagnumData> {
|
||||||
@ -104,10 +166,8 @@ export class OpusMagnumOCRService {
|
|||||||
tessedit_char_whitelist: '0123456789'
|
tessedit_char_whitelist: '0123456789'
|
||||||
});
|
});
|
||||||
} else if (key === 'puzzle') {
|
} else if (key === 'puzzle') {
|
||||||
// Puzzle name - allow alphanumeric, spaces, and dashes
|
// Puzzle name - use user words file for better matching
|
||||||
await this.worker!.setParameters({
|
await this.configurePuzzleOCR();
|
||||||
tessedit_char_whitelist: 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789 -'
|
|
||||||
});
|
|
||||||
} else {
|
} else {
|
||||||
// Default - allow all characters
|
// Default - allow all characters
|
||||||
await this.worker!.setParameters({
|
await this.worker!.setParameters({
|
||||||
@ -141,8 +201,17 @@ export class OpusMagnumOCRService {
|
|||||||
// Ensure only digits remain
|
// Ensure only digits remain
|
||||||
cleanText = cleanText.replace(/[^0-9]/g, '');
|
cleanText = cleanText.replace(/[^0-9]/g, '');
|
||||||
} else if (key === 'puzzle') {
|
} else if (key === 'puzzle') {
|
||||||
// Post-process puzzle names with fuzzy matching
|
// Post-process puzzle names with aggressive matching to force selection from available puzzles
|
||||||
cleanText = this.findBestPuzzleMatch(cleanText);
|
cleanText = this.findBestPuzzleMatch(cleanText);
|
||||||
|
|
||||||
|
// If we still don't have a match and we have available puzzles, force the best match
|
||||||
|
if (this.availablePuzzleNames.length > 0 && !this.availablePuzzleNames.includes(cleanText)) {
|
||||||
|
const forcedMatch = this.findBestPuzzleMatchForced(cleanText);
|
||||||
|
if (forcedMatch) {
|
||||||
|
cleanText = forcedMatch;
|
||||||
|
console.log(`Forced OCR match: "${text.trim()}" -> "${cleanText}"`);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
(results as any)[key] = cleanText;
|
(results as any)[key] = cleanText;
|
||||||
@ -226,7 +295,7 @@ export class OpusMagnumOCRService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Find the best matching puzzle name from available options
|
* Find the best matching puzzle name from available options using multiple strategies
|
||||||
*/
|
*/
|
||||||
private findBestPuzzleMatch(ocrText: string): string {
|
private findBestPuzzleMatch(ocrText: string): string {
|
||||||
if (!this.availablePuzzleNames.length) {
|
if (!this.availablePuzzleNames.length) {
|
||||||
@ -234,31 +303,155 @@ export class OpusMagnumOCRService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const cleanedOcr = ocrText.trim();
|
const cleanedOcr = ocrText.trim();
|
||||||
|
if (!cleanedOcr) return '';
|
||||||
|
|
||||||
// First try exact match (case insensitive)
|
// Strategy 1: Exact match (case insensitive)
|
||||||
const exactMatch = this.availablePuzzleNames.find(
|
const exactMatch = this.availablePuzzleNames.find(
|
||||||
name => name.toLowerCase() === cleanedOcr.toLowerCase()
|
name => name.toLowerCase() === cleanedOcr.toLowerCase()
|
||||||
);
|
);
|
||||||
if (exactMatch) return exactMatch;
|
if (exactMatch) return exactMatch;
|
||||||
|
|
||||||
// Then try fuzzy matching
|
// Strategy 2: Substring match (either direction)
|
||||||
|
const substringMatch = this.availablePuzzleNames.find(
|
||||||
|
name => name.toLowerCase().includes(cleanedOcr.toLowerCase()) ||
|
||||||
|
cleanedOcr.toLowerCase().includes(name.toLowerCase())
|
||||||
|
);
|
||||||
|
if (substringMatch) return substringMatch;
|
||||||
|
|
||||||
|
// Strategy 3: Multiple fuzzy matching approaches
|
||||||
let bestMatch = cleanedOcr;
|
let bestMatch = cleanedOcr;
|
||||||
let bestScore = Infinity;
|
let bestScore = 0;
|
||||||
|
|
||||||
for (const puzzleName of this.availablePuzzleNames) {
|
for (const puzzleName of this.availablePuzzleNames) {
|
||||||
// Calculate similarity scores
|
const scores = [
|
||||||
const distance = this.levenshteinDistance(
|
this.calculateLevenshteinSimilarity(cleanedOcr, puzzleName),
|
||||||
cleanedOcr.toLowerCase(),
|
this.calculateJaroWinklerSimilarity(cleanedOcr, puzzleName),
|
||||||
puzzleName.toLowerCase()
|
this.calculateNGramSimilarity(cleanedOcr, puzzleName, 2)
|
||||||
);
|
];
|
||||||
|
|
||||||
// Normalize by length to get a similarity ratio
|
// Use the maximum score from all algorithms
|
||||||
const maxLength = Math.max(cleanedOcr.length, puzzleName.length);
|
const maxScore = Math.max(...scores);
|
||||||
const similarity = 1 - (distance / maxLength);
|
|
||||||
|
|
||||||
// Consider it a good match if similarity is above 70%
|
// Lower threshold for better matching - force selection even with moderate confidence
|
||||||
if (similarity > 0.7 && distance < bestScore) {
|
if (maxScore > bestScore && maxScore > 0.4) {
|
||||||
bestScore = distance;
|
bestScore = maxScore;
|
||||||
|
bestMatch = puzzleName;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Strategy 4: If no good match found, try character-based matching
|
||||||
|
if (bestScore < 0.6) {
|
||||||
|
const charMatch = this.findBestCharacterMatch(cleanedOcr);
|
||||||
|
if (charMatch) {
|
||||||
|
bestMatch = charMatch;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return bestMatch;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Calculate Levenshtein similarity (normalized)
|
||||||
|
*/
|
||||||
|
private calculateLevenshteinSimilarity(str1: string, str2: string): number {
|
||||||
|
const distance = this.levenshteinDistance(str1.toLowerCase(), str2.toLowerCase());
|
||||||
|
const maxLength = Math.max(str1.length, str2.length);
|
||||||
|
return maxLength === 0 ? 1 : 1 - (distance / maxLength);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Calculate Jaro-Winkler similarity
|
||||||
|
*/
|
||||||
|
private calculateJaroWinklerSimilarity(str1: string, str2: string): number {
|
||||||
|
const s1 = str1.toLowerCase();
|
||||||
|
const s2 = str2.toLowerCase();
|
||||||
|
|
||||||
|
if (s1 === s2) return 1;
|
||||||
|
|
||||||
|
const matchWindow = Math.floor(Math.max(s1.length, s2.length) / 2) - 1;
|
||||||
|
if (matchWindow < 0) return 0;
|
||||||
|
|
||||||
|
const s1Matches = new Array(s1.length).fill(false);
|
||||||
|
const s2Matches = new Array(s2.length).fill(false);
|
||||||
|
|
||||||
|
let matches = 0;
|
||||||
|
let transpositions = 0;
|
||||||
|
|
||||||
|
// Find matches
|
||||||
|
for (let i = 0; i < s1.length; i++) {
|
||||||
|
const start = Math.max(0, i - matchWindow);
|
||||||
|
const end = Math.min(i + matchWindow + 1, s2.length);
|
||||||
|
|
||||||
|
for (let j = start; j < end; j++) {
|
||||||
|
if (s2Matches[j] || s1[i] !== s2[j]) continue;
|
||||||
|
s1Matches[i] = true;
|
||||||
|
s2Matches[j] = true;
|
||||||
|
matches++;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (matches === 0) return 0;
|
||||||
|
|
||||||
|
// Count transpositions
|
||||||
|
let k = 0;
|
||||||
|
for (let i = 0; i < s1.length; i++) {
|
||||||
|
if (!s1Matches[i]) continue;
|
||||||
|
while (!s2Matches[k]) k++;
|
||||||
|
if (s1[i] !== s2[k]) transpositions++;
|
||||||
|
k++;
|
||||||
|
}
|
||||||
|
|
||||||
|
const jaro = (matches / s1.length + matches / s2.length + (matches - transpositions / 2) / matches) / 3;
|
||||||
|
|
||||||
|
// Jaro-Winkler bonus for common prefix
|
||||||
|
let prefix = 0;
|
||||||
|
for (let i = 0; i < Math.min(s1.length, s2.length, 4); i++) {
|
||||||
|
if (s1[i] === s2[i]) prefix++;
|
||||||
|
else break;
|
||||||
|
}
|
||||||
|
|
||||||
|
return jaro + (0.1 * prefix * (1 - jaro));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Calculate N-gram similarity
|
||||||
|
*/
|
||||||
|
private calculateNGramSimilarity(str1: string, str2: string, n: number): number {
|
||||||
|
const s1 = str1.toLowerCase();
|
||||||
|
const s2 = str2.toLowerCase();
|
||||||
|
|
||||||
|
if (s1 === s2) return 1;
|
||||||
|
if (s1.length < n || s2.length < n) return 0;
|
||||||
|
|
||||||
|
const ngrams1 = new Set<string>();
|
||||||
|
const ngrams2 = new Set<string>();
|
||||||
|
|
||||||
|
for (let i = 0; i <= s1.length - n; i++) {
|
||||||
|
ngrams1.add(s1.substr(i, n));
|
||||||
|
}
|
||||||
|
|
||||||
|
for (let i = 0; i <= s2.length - n; i++) {
|
||||||
|
ngrams2.add(s2.substr(i, n));
|
||||||
|
}
|
||||||
|
|
||||||
|
const intersection = new Set([...ngrams1].filter(x => ngrams2.has(x)));
|
||||||
|
const union = new Set([...ngrams1, ...ngrams2]);
|
||||||
|
|
||||||
|
return intersection.size / union.size;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Find best match based on character frequency
|
||||||
|
*/
|
||||||
|
private findBestCharacterMatch(ocrText: string): string | null {
|
||||||
|
let bestMatch = null;
|
||||||
|
let bestScore = 0;
|
||||||
|
|
||||||
|
for (const puzzleName of this.availablePuzzleNames) {
|
||||||
|
const score = this.calculateCharacterFrequencyScore(ocrText.toLowerCase(), puzzleName.toLowerCase());
|
||||||
|
if (score > bestScore && score > 0.3) {
|
||||||
|
bestScore = score;
|
||||||
bestMatch = puzzleName;
|
bestMatch = puzzleName;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -266,6 +459,90 @@ export class OpusMagnumOCRService {
|
|||||||
return bestMatch;
|
return bestMatch;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Calculate character frequency similarity
|
||||||
|
*/
|
||||||
|
private calculateCharacterFrequencyScore(str1: string, str2: string): number {
|
||||||
|
const freq1 = new Map<string, number>();
|
||||||
|
const freq2 = new Map<string, number>();
|
||||||
|
|
||||||
|
for (const char of str1) {
|
||||||
|
freq1.set(char, (freq1.get(char) || 0) + 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const char of str2) {
|
||||||
|
freq2.set(char, (freq2.get(char) || 0) + 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
const allChars = new Set([...freq1.keys(), ...freq2.keys()]);
|
||||||
|
let similarity = 0;
|
||||||
|
let totalChars = 0;
|
||||||
|
|
||||||
|
for (const char of allChars) {
|
||||||
|
const count1 = freq1.get(char) || 0;
|
||||||
|
const count2 = freq2.get(char) || 0;
|
||||||
|
similarity += Math.min(count1, count2);
|
||||||
|
totalChars += Math.max(count1, count2);
|
||||||
|
}
|
||||||
|
|
||||||
|
return totalChars === 0 ? 0 : similarity / totalChars;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Force a match to available puzzle names - always returns a puzzle name
|
||||||
|
* This is used as a last resort to ensure OCR always selects from available puzzles
|
||||||
|
*/
|
||||||
|
private findBestPuzzleMatchForced(ocrText: string): string | null {
|
||||||
|
if (!this.availablePuzzleNames.length || !ocrText.trim()) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const cleanedOcr = ocrText.trim().toLowerCase();
|
||||||
|
let bestMatch = this.availablePuzzleNames[0]; // Default to first puzzle
|
||||||
|
let bestScore = 0;
|
||||||
|
|
||||||
|
// Try all matching algorithms and pick the best overall score
|
||||||
|
for (const puzzleName of this.availablePuzzleNames) {
|
||||||
|
const scores = [
|
||||||
|
this.calculateLevenshteinSimilarity(cleanedOcr, puzzleName),
|
||||||
|
this.calculateJaroWinklerSimilarity(cleanedOcr, puzzleName),
|
||||||
|
this.calculateNGramSimilarity(cleanedOcr, puzzleName, 2),
|
||||||
|
this.calculateCharacterFrequencyScore(cleanedOcr, puzzleName.toLowerCase()),
|
||||||
|
// Add length similarity bonus
|
||||||
|
this.calculateLengthSimilarity(cleanedOcr, puzzleName.toLowerCase())
|
||||||
|
];
|
||||||
|
|
||||||
|
// Use weighted average with emphasis on character frequency and length
|
||||||
|
const weightedScore = (
|
||||||
|
scores[0] * 0.25 + // Levenshtein
|
||||||
|
scores[1] * 0.25 + // Jaro-Winkler
|
||||||
|
scores[2] * 0.2 + // N-gram
|
||||||
|
scores[3] * 0.2 + // Character frequency
|
||||||
|
scores[4] * 0.1 // Length similarity
|
||||||
|
);
|
||||||
|
|
||||||
|
if (weightedScore > bestScore) {
|
||||||
|
bestScore = weightedScore;
|
||||||
|
bestMatch = puzzleName;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log(`Forced match for "${ocrText}": "${bestMatch}" (score: ${bestScore.toFixed(3)})`);
|
||||||
|
return bestMatch;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Calculate similarity based on string length
|
||||||
|
*/
|
||||||
|
private calculateLengthSimilarity(str1: string, str2: string): number {
|
||||||
|
const len1 = str1.length;
|
||||||
|
const len2 = str2.length;
|
||||||
|
const maxLen = Math.max(len1, len2);
|
||||||
|
const minLen = Math.min(len1, len2);
|
||||||
|
|
||||||
|
return maxLen === 0 ? 1 : minLen / maxLen;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
async terminate(): Promise<void> {
|
async terminate(): Promise<void> {
|
||||||
if (this.worker) {
|
if (this.worker) {
|
||||||
|
|||||||
3
opus_submitter/src/stores/index.ts
Normal file
3
opus_submitter/src/stores/index.ts
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
import { createPinia } from 'pinia'
|
||||||
|
|
||||||
|
export const pinia = createPinia()
|
||||||
78
opus_submitter/src/stores/puzzles.ts
Normal file
78
opus_submitter/src/stores/puzzles.ts
Normal file
@ -0,0 +1,78 @@
|
|||||||
|
import { defineStore } from 'pinia'
|
||||||
|
import { ref, computed } from 'vue'
|
||||||
|
import type { SteamCollectionItem } from '@/types'
|
||||||
|
import { apiService } from '@/services/apiService'
|
||||||
|
|
||||||
|
export const usePuzzlesStore = defineStore('puzzles', () => {
|
||||||
|
// State
|
||||||
|
const puzzles = ref<SteamCollectionItem[]>([])
|
||||||
|
const isLoading = ref(false)
|
||||||
|
const error = ref<string>('')
|
||||||
|
|
||||||
|
// Getters
|
||||||
|
const puzzleNames = computed(() => puzzles.value.map(puzzle => puzzle.title))
|
||||||
|
|
||||||
|
const findPuzzleByName = computed(() => (name: string): SteamCollectionItem | null => {
|
||||||
|
if (!name) return null
|
||||||
|
|
||||||
|
// First try exact match (case insensitive)
|
||||||
|
const exactMatch = puzzles.value.find(
|
||||||
|
puzzle => puzzle.title.toLowerCase() === name.toLowerCase()
|
||||||
|
)
|
||||||
|
if (exactMatch) return exactMatch
|
||||||
|
|
||||||
|
// Then try partial match
|
||||||
|
const partialMatch = puzzles.value.find(
|
||||||
|
puzzle => puzzle.title.toLowerCase().includes(name.toLowerCase()) ||
|
||||||
|
name.toLowerCase().includes(puzzle.title.toLowerCase())
|
||||||
|
)
|
||||||
|
|
||||||
|
return partialMatch || null
|
||||||
|
})
|
||||||
|
|
||||||
|
// Actions
|
||||||
|
const loadPuzzles = async () => {
|
||||||
|
if (puzzles.value.length > 0) return // Already loaded
|
||||||
|
|
||||||
|
try {
|
||||||
|
isLoading.value = true
|
||||||
|
error.value = ''
|
||||||
|
|
||||||
|
const response = await apiService.getPuzzles()
|
||||||
|
if (response.error) {
|
||||||
|
error.value = response.error
|
||||||
|
console.error('Failed to load puzzles:', response.error)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if (response.data) {
|
||||||
|
puzzles.value = response.data
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
error.value = 'Failed to load puzzles'
|
||||||
|
console.error('Error loading puzzles:', err)
|
||||||
|
} finally {
|
||||||
|
isLoading.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const refreshPuzzles = async () => {
|
||||||
|
puzzles.value = []
|
||||||
|
await loadPuzzles()
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
// State
|
||||||
|
puzzles,
|
||||||
|
isLoading,
|
||||||
|
error,
|
||||||
|
|
||||||
|
// Getters
|
||||||
|
puzzleNames,
|
||||||
|
findPuzzleByName,
|
||||||
|
|
||||||
|
// Actions
|
||||||
|
loadPuzzles,
|
||||||
|
refreshPuzzles
|
||||||
|
}
|
||||||
|
})
|
||||||
100
opus_submitter/src/stores/submissions.ts
Normal file
100
opus_submitter/src/stores/submissions.ts
Normal file
@ -0,0 +1,100 @@
|
|||||||
|
import { defineStore } from 'pinia'
|
||||||
|
import { ref } from 'vue'
|
||||||
|
import type { Submission, SubmissionFile } from '@/types'
|
||||||
|
import { submissionHelpers } from '@/services/apiService'
|
||||||
|
import { usePuzzlesStore } from './puzzles'
|
||||||
|
|
||||||
|
export const useSubmissionsStore = defineStore('submissions', () => {
|
||||||
|
// State
|
||||||
|
const submissions = ref<Submission[]>([])
|
||||||
|
const isLoading = ref(false)
|
||||||
|
const error = ref<string>('')
|
||||||
|
const isSubmissionModalOpen = ref(false)
|
||||||
|
|
||||||
|
// Actions
|
||||||
|
const loadSubmissions = async (limit = 20, offset = 0) => {
|
||||||
|
try {
|
||||||
|
isLoading.value = true
|
||||||
|
error.value = ''
|
||||||
|
|
||||||
|
const loadedSubmissions = await submissionHelpers.loadSubmissions(limit, offset)
|
||||||
|
|
||||||
|
if (offset === 0) {
|
||||||
|
submissions.value = loadedSubmissions
|
||||||
|
} else {
|
||||||
|
submissions.value.push(...loadedSubmissions)
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
error.value = 'Failed to load submissions'
|
||||||
|
console.error('Error loading submissions:', err)
|
||||||
|
} finally {
|
||||||
|
isLoading.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const createSubmission = async (
|
||||||
|
files: SubmissionFile[],
|
||||||
|
notes?: string,
|
||||||
|
manualValidationRequested?: boolean
|
||||||
|
): Promise<Submission | undefined> => {
|
||||||
|
try {
|
||||||
|
isLoading.value = true
|
||||||
|
error.value = ''
|
||||||
|
|
||||||
|
const puzzlesStore = usePuzzlesStore()
|
||||||
|
|
||||||
|
const response = await submissionHelpers.createFromFiles(
|
||||||
|
files,
|
||||||
|
puzzlesStore.puzzles,
|
||||||
|
notes,
|
||||||
|
manualValidationRequested
|
||||||
|
)
|
||||||
|
|
||||||
|
if (response.error) {
|
||||||
|
error.value = response.error
|
||||||
|
throw new Error(response.error)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (response.data) {
|
||||||
|
// Add to local submissions list
|
||||||
|
submissions.value.unshift(response.data)
|
||||||
|
return response.data
|
||||||
|
}
|
||||||
|
|
||||||
|
return undefined
|
||||||
|
} catch (err) {
|
||||||
|
error.value = err instanceof Error ? err.message : 'Failed to create submission'
|
||||||
|
throw err
|
||||||
|
} finally {
|
||||||
|
isLoading.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const openSubmissionModal = () => {
|
||||||
|
isSubmissionModalOpen.value = true
|
||||||
|
}
|
||||||
|
|
||||||
|
const closeSubmissionModal = () => {
|
||||||
|
isSubmissionModalOpen.value = false
|
||||||
|
}
|
||||||
|
|
||||||
|
const refreshSubmissions = async () => {
|
||||||
|
submissions.value = []
|
||||||
|
await loadSubmissions()
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
// State
|
||||||
|
submissions,
|
||||||
|
isLoading,
|
||||||
|
error,
|
||||||
|
isSubmissionModalOpen,
|
||||||
|
|
||||||
|
// Actions
|
||||||
|
loadSubmissions,
|
||||||
|
createSubmission,
|
||||||
|
openSubmissionModal,
|
||||||
|
closeSubmissionModal,
|
||||||
|
refreshSubmissions
|
||||||
|
}
|
||||||
|
})
|
||||||
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
21
opus_submitter/static_source/vite/assets/main-fzs-6OUY.js
Normal file
21
opus_submitter/static_source/vite/assets/main-fzs-6OUY.js
Normal file
File diff suppressed because one or more lines are too long
@ -16,12 +16,12 @@
|
|||||||
"src": "node_modules/.pnpm/@mdi+font@7.4.47/node_modules/@mdi/font/fonts/materialdesignicons-webfont.woff2"
|
"src": "node_modules/.pnpm/@mdi+font@7.4.47/node_modules/@mdi/font/fonts/materialdesignicons-webfont.woff2"
|
||||||
},
|
},
|
||||||
"src/main.ts": {
|
"src/main.ts": {
|
||||||
"file": "assets/main-Cv9F8wz5.js",
|
"file": "assets/main-fzs-6OUY.js",
|
||||||
"name": "main",
|
"name": "main",
|
||||||
"src": "src/main.ts",
|
"src": "src/main.ts",
|
||||||
"isEntry": true,
|
"isEntry": true,
|
||||||
"css": [
|
"css": [
|
||||||
"assets/main-DWmJTLXa.css"
|
"assets/main-DeQiP-Az.css"
|
||||||
],
|
],
|
||||||
"assets": [
|
"assets": [
|
||||||
"assets/materialdesignicons-webfont-CSr8KVlo.eot",
|
"assets/materialdesignicons-webfont-CSr8KVlo.eot",
|
||||||
|
|||||||
@ -1 +1 @@
|
|||||||
{"root":["./src/main.ts","./src/services/apiService.ts","./src/services/ocrService.ts","./src/types/index.ts","./src/App.vue","./src/components/AdminPanel.vue","./src/components/FileUpload.vue","./src/components/PuzzleCard.vue","./src/components/SubmissionForm.vue"],"version":"5.9.3"}
|
{"root":["./src/main.ts","./src/services/apiService.ts","./src/services/ocrService.ts","./src/stores/index.ts","./src/stores/puzzles.ts","./src/stores/submissions.ts","./src/types/index.ts","./src/App.vue","./src/components/AdminPanel.vue","./src/components/FileUpload.vue","./src/components/PuzzleCard.vue","./src/components/SubmissionForm.vue"],"version":"5.9.3"}
|
||||||
Loading…
Reference in New Issue
Block a user