@scu-xiaochuan/scu-captcha-script 0.0.1 → 0.0.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (2) hide show
  1. package/dist/script.user.js +314 -0
  2. package/package.json +1 -1
@@ -0,0 +1,314 @@
1
+ // ==UserScript==
2
+ // @name 四川大学本科教务系统验证码识别
3
+ // @namespace https://github.com/2239559319
4
+ // @version 0.1
5
+ // @description 四川大学本科教务系统验证码识别
6
+ // @author Xiao Chuan
7
+ // @match http://zhjw.scu.edu.cn/login
8
+ // @grant none
9
+ // ==/UserScript==
10
+ (() => {
11
+ // ../utils/src/const.js
12
+ var MODEL_URL = "https://hf-mirror.net/xiaochuan-dev/scu-captcha/resolve/main/model.onnx";
13
+ var CHARSET = "2345678abcdefgmnpwxy";
14
+ var IMG_H = 32;
15
+ var IMG_W = 128;
16
+ var BLANK_IDX = 0;
17
+
18
+ // ../utils/src/img.js
19
+ function sinc(x) {
20
+ if (Math.abs(x) < 1e-12) {
21
+ return 1;
22
+ }
23
+ const p = Math.PI * x;
24
+ return Math.sin(p) / p;
25
+ }
26
+ function lanczos(x, a = 3) {
27
+ x = Math.abs(x);
28
+ if (x >= a) {
29
+ return 0;
30
+ }
31
+ if (x < 1e-12) {
32
+ return 1;
33
+ }
34
+ return sinc(x) * sinc(x / a);
35
+ }
36
+ function lanczosSample(gray, width, height, x, y) {
37
+ const a = 3;
38
+ let sum = 0;
39
+ let weightSum = 0;
40
+ const xStart = Math.floor(x) - a + 1;
41
+ const xEnd = Math.floor(x) + a;
42
+ const yStart = Math.floor(y) - a + 1;
43
+ const yEnd = Math.floor(y) + a;
44
+ for (let iy = yStart; iy <= yEnd; iy++) {
45
+ if (iy < 0 || iy >= height) {
46
+ continue;
47
+ }
48
+ const wy = lanczos(
49
+ y - iy,
50
+ a
51
+ );
52
+ for (let ix = xStart; ix <= xEnd; ix++) {
53
+ if (ix < 0 || ix >= width) {
54
+ continue;
55
+ }
56
+ const wx = lanczos(
57
+ x - ix,
58
+ a
59
+ );
60
+ const weight = wx * wy;
61
+ sum += gray[iy * width + ix] * weight;
62
+ weightSum += weight;
63
+ }
64
+ }
65
+ if (Math.abs(weightSum) < 1e-12) {
66
+ return 255;
67
+ }
68
+ const value = sum / weightSum;
69
+ return Math.max(
70
+ 0,
71
+ Math.min(
72
+ 255,
73
+ value
74
+ )
75
+ );
76
+ }
77
+ function preprocessImage(img) {
78
+ const sourceCanvas = document.createElement("canvas");
79
+ sourceCanvas.width = img.naturalWidth;
80
+ sourceCanvas.height = img.naturalHeight;
81
+ const sourceCtx = sourceCanvas.getContext("2d");
82
+ sourceCtx.drawImage(img, 0, 0);
83
+ const imageData = sourceCtx.getImageData(0, 0, img.naturalWidth, img.naturalHeight);
84
+ const width = img.naturalWidth;
85
+ const height = img.naturalHeight;
86
+ const gray = new Uint8Array(width * height);
87
+ for (let y = 0; y < height; y++) {
88
+ for (let x = 0; x < width; x++) {
89
+ const idx = (y * width + x) * 4;
90
+ const r = imageData.data[idx];
91
+ const g = imageData.data[idx + 1];
92
+ const b = imageData.data[idx + 2];
93
+ const value = 0.299 * r + 0.587 * g + 0.114 * b;
94
+ gray[y * width + x] = Math.round(value);
95
+ }
96
+ }
97
+ const scale = Math.min(IMG_W / width, IMG_H / height);
98
+ const newW = Math.max(1, Math.round(width * scale));
99
+ const newH = Math.max(1, Math.round(height * scale));
100
+ const output = new Float32Array(IMG_W * IMG_H);
101
+ output.fill(1);
102
+ for (let dy = 0; dy < newH; dy++) {
103
+ for (let dx = 0; dx < newW; dx++) {
104
+ const srcX = (dx + 0.5) / scale - 0.5;
105
+ const srcY = (dy + 0.5) / scale - 0.5;
106
+ const value = lanczosSample(gray, width, height, srcX, srcY);
107
+ const offsetX = Math.floor((IMG_W - newW) / 2);
108
+ const offsetY = Math.floor((IMG_H - newH) / 2);
109
+ const dstX = offsetX + dx;
110
+ const dstY = offsetY + dy;
111
+ if (dstX >= 0 && dstX < IMG_W && dstY >= 0 && dstY < IMG_H) {
112
+ output[dstY * IMG_W + dstX] = value / 255;
113
+ }
114
+ }
115
+ }
116
+ return output;
117
+ }
118
+
119
+ // ../utils/src/model.js
120
+ function ctcDecode(logits, T, numClasses, blankIdx = 0) {
121
+ let prev = blankIdx;
122
+ const chars = [];
123
+ for (let t = 0; t < T; t++) {
124
+ let maxIdx = 0;
125
+ let maxVal = -Infinity;
126
+ for (let c = 0; c < numClasses; c++) {
127
+ const val = logits[t * numClasses + c];
128
+ if (val > maxVal) {
129
+ maxVal = val;
130
+ maxIdx = c;
131
+ }
132
+ }
133
+ const p = maxIdx;
134
+ if (p !== prev && p !== blankIdx) {
135
+ const charIdx = p - 1;
136
+ if (charIdx >= 0 && charIdx < CHARSET.length) {
137
+ chars.push(CHARSET[charIdx]);
138
+ }
139
+ }
140
+ prev = p;
141
+ }
142
+ return chars.join("");
143
+ }
144
+ function getPredictionDetails(logits, T, numClasses) {
145
+ const details = [];
146
+ for (let t = 0; t < T; t++) {
147
+ let maxIdx = 0;
148
+ let maxVal = -Infinity;
149
+ for (let c = 0; c < numClasses; c++) {
150
+ const value = logits[t * numClasses + c];
151
+ if (value > maxVal) {
152
+ maxVal = value;
153
+ maxIdx = c;
154
+ }
155
+ }
156
+ let label;
157
+ if (maxIdx === BLANK_IDX) {
158
+ label = "<blank>";
159
+ } else {
160
+ const charIdx = maxIdx - 1;
161
+ if (charIdx >= 0 && charIdx < CHARSET.length) {
162
+ label = CHARSET[charIdx];
163
+ } else {
164
+ label = `<class:${maxIdx}>`;
165
+ }
166
+ }
167
+ details.push({
168
+ t,
169
+ classIndex: maxIdx,
170
+ label,
171
+ logit: maxVal
172
+ });
173
+ }
174
+ return details;
175
+ }
176
+ async function recognize(session, currentImage) {
177
+ try {
178
+ const inputData = preprocessImage(currentImage);
179
+ const inputTensor = new ort.Tensor("float32", inputData, [1, 1, IMG_H, IMG_W]);
180
+ const results = await session.run({ input: inputTensor });
181
+ const outputTensor = results.output;
182
+ const dims = outputTensor.dims;
183
+ const batchSize = dims[0];
184
+ const T = dims[1];
185
+ const numClasses = dims[2];
186
+ const logits = outputTensor.data;
187
+ if (batchSize !== 1) {
188
+ throw new Error(`Unexpected batch size: ${batchSize}`);
189
+ }
190
+ const prediction = ctcDecode(logits, T, numClasses, BLANK_IDX);
191
+ const details = getPredictionDetails(logits, T, numClasses);
192
+ let debugText = "";
193
+ debugText += `Input shape: [1, 1, ${IMG_H}, ${IMG_W}]
194
+ `;
195
+ debugText += `Output shape: [${batchSize}, ${T}, ${numClasses}]
196
+ `;
197
+ debugText += `Charset: ${CHARSET}
198
+ `;
199
+ debugText += `Blank index: ${BLANK_IDX}
200
+ `;
201
+ debugText += `Prediction: ${prediction}
202
+
203
+ `;
204
+ debugText += "CTC timesteps:\n";
205
+ for (const item of details) {
206
+ debugText += `t=${String(item.t).padStart(2, "0")} class=${String(item.classIndex).padStart(2, "0")} label=${item.label} logit=${item.logit.toFixed(4)}
207
+ `;
208
+ }
209
+ return {
210
+ prediction,
211
+ debugText
212
+ };
213
+ } catch (error) {
214
+ console.error(error);
215
+ return null;
216
+ }
217
+ }
218
+
219
+ // ../utils/src/loadModel.js
220
+ async function loadScript() {
221
+ const url = "https://unpkg.com/onnxruntime-web@1.29.0/dist/ort.min.js";
222
+ return new Promise((resolve, reject) => {
223
+ if (window.ort) {
224
+ resolve(window.ort);
225
+ return;
226
+ }
227
+ const script = document.createElement("script");
228
+ script.src = url;
229
+ script.async = true;
230
+ script.onload = () => resolve(window.ort);
231
+ script.onerror = () => reject(new Error(`\u52A0\u8F7D ONNX Runtime Web \u5931\u8D25: ${url}`));
232
+ document.head.appendChild(script);
233
+ });
234
+ }
235
+
236
+ // src/script.js
237
+ var MODEL_CACHE_KEY = "captcha-model-v1";
238
+ function openDB() {
239
+ return new Promise((resolve, reject) => {
240
+ const request = indexedDB.open("onnx-cache", 1);
241
+ request.onupgradeneeded = () => {
242
+ const db = request.result;
243
+ if (!db.objectStoreNames.contains("models")) {
244
+ db.createObjectStore("models");
245
+ }
246
+ };
247
+ request.onsuccess = () => resolve(request.result);
248
+ request.onerror = () => reject(request.error);
249
+ });
250
+ }
251
+ async function getCachedModel() {
252
+ const db = await openDB();
253
+ return new Promise((resolve, reject) => {
254
+ const transaction = db.transaction("models", "readonly");
255
+ const store = transaction.objectStore("models");
256
+ const request = store.get(MODEL_CACHE_KEY);
257
+ request.onsuccess = () => resolve(request.result || null);
258
+ request.onerror = () => reject(request.error);
259
+ });
260
+ }
261
+ async function cacheModel(buffer) {
262
+ const db = await openDB();
263
+ return new Promise((resolve, reject) => {
264
+ const transaction = db.transaction("models", "readwrite");
265
+ const store = transaction.objectStore("models");
266
+ const request = store.put(buffer, MODEL_CACHE_KEY);
267
+ request.onsuccess = () => resolve();
268
+ request.onerror = () => reject(request.error);
269
+ });
270
+ }
271
+ async function getModelBuffer() {
272
+ const cached = await getCachedModel();
273
+ if (cached) {
274
+ console.log("ONNX \u6A21\u578B\uFF1A\u4ECE IndexedDB \u52A0\u8F7D");
275
+ return cached;
276
+ }
277
+ console.log("ONNX \u6A21\u578B\uFF1A\u4ECE\u7F51\u7EDC\u4E0B\u8F7D");
278
+ const response = await fetch(MODEL_URL);
279
+ if (!response.ok) {
280
+ throw new Error(
281
+ `\u6A21\u578B\u4E0B\u8F7D\u5931\u8D25: ${response.status} ${response.statusText}`
282
+ );
283
+ }
284
+ const buffer = await response.arrayBuffer();
285
+ console.log(
286
+ `ONNX \u6A21\u578B\u4E0B\u8F7D\u5B8C\u6210: ${(buffer.byteLength / 1024 / 1024).toFixed(2)} MB`
287
+ );
288
+ await cacheModel(buffer);
289
+ console.log("ONNX \u6A21\u578B\uFF1A\u5DF2\u7F13\u5B58\u5230 IndexedDB");
290
+ return buffer;
291
+ }
292
+ async function loadModel2() {
293
+ const modelBuffer = await getModelBuffer();
294
+ return await ort.InferenceSession.create(
295
+ modelBuffer,
296
+ {
297
+ executionProviders: ["webgpu", "wasm"],
298
+ graphOptimizationLevel: "all"
299
+ }
300
+ );
301
+ }
302
+ async function start() {
303
+ await loadScript();
304
+ const session = await loadModel2();
305
+ checkImg = document.querySelector("#captchaImg");
306
+ setTimeout(async () => {
307
+ const res = await recognize(session, document.querySelector("#captchaImg"));
308
+ document.querySelector("#input_checkcode").value = res.prediction;
309
+ });
310
+ }
311
+ (async () => {
312
+ await start();
313
+ })();
314
+ })();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@scu-xiaochuan/scu-captcha-script",
3
- "version": "0.0.1",
3
+ "version": "0.0.2",
4
4
  "description": "四川大学本科教务系统验证码识别脚本",
5
5
  "main": "index.js",
6
6
  "files": [