@xom11/whiteboard 0.31.0 → 0.33.0

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 (93) hide show
  1. package/dist/ai.d.mts +149 -28
  2. package/dist/ai.d.ts +149 -28
  3. package/dist/ai.js +2652 -430
  4. package/dist/ai.js.map +1 -1
  5. package/dist/ai.mjs +2482 -426
  6. package/dist/ai.mjs.map +1 -1
  7. package/dist/catalog.json +4 -4
  8. package/dist/{chunk-JJ4FPCBE.mjs → chunk-4EXRNZRX.mjs} +641 -75
  9. package/dist/chunk-4EXRNZRX.mjs.map +1 -0
  10. package/dist/{chunk-TMRFSOM7.mjs → chunk-4NJUTCFF.mjs} +4 -4
  11. package/dist/{chunk-TMRFSOM7.mjs.map → chunk-4NJUTCFF.mjs.map} +1 -1
  12. package/dist/{chunk-LTLLQUMN.mjs → chunk-BZZ5BROM.mjs} +6 -7
  13. package/dist/chunk-BZZ5BROM.mjs.map +1 -0
  14. package/dist/{host-KMWP7KBT.mjs → chunk-C3RDFBQ5.mjs} +20 -35
  15. package/dist/chunk-C3RDFBQ5.mjs.map +1 -0
  16. package/dist/chunk-C7C66BAV.mjs +11 -0
  17. package/dist/chunk-C7C66BAV.mjs.map +1 -0
  18. package/dist/{chunk-T3N4BSJV.mjs → chunk-DCFZTKFE.mjs} +13 -5
  19. package/dist/chunk-DCFZTKFE.mjs.map +1 -0
  20. package/dist/{chunk-44JY2AKC.mjs → chunk-F42MZ745.mjs} +3 -3
  21. package/dist/{chunk-44JY2AKC.mjs.map → chunk-F42MZ745.mjs.map} +1 -1
  22. package/dist/{chunk-BMYC2ILT.mjs → chunk-FOPZOVI3.mjs} +5 -4
  23. package/dist/chunk-FOPZOVI3.mjs.map +1 -0
  24. package/dist/{chunk-K5BS2H56.mjs → chunk-GKDQ5IV7.mjs} +4 -4
  25. package/dist/{chunk-K5BS2H56.mjs.map → chunk-GKDQ5IV7.mjs.map} +1 -1
  26. package/dist/{chunk-73Q7ADVL.mjs → chunk-MNHOFOM5.mjs} +2 -2
  27. package/dist/{chunk-73Q7ADVL.mjs.map → chunk-MNHOFOM5.mjs.map} +1 -1
  28. package/dist/{chunk-KOXOC2FI.mjs → chunk-NBMYET5H.mjs} +11 -15
  29. package/dist/chunk-NBMYET5H.mjs.map +1 -0
  30. package/dist/{chunk-DWIEVCGK.mjs → chunk-NTFG7ZRN.mjs} +53 -3
  31. package/dist/chunk-NTFG7ZRN.mjs.map +1 -0
  32. package/dist/{chunk-C76SOFXF.mjs → chunk-QCK3Z5WN.mjs} +3 -3
  33. package/dist/{chunk-C76SOFXF.mjs.map → chunk-QCK3Z5WN.mjs.map} +1 -1
  34. package/dist/chunk-SUXIMLYN.mjs +238 -0
  35. package/dist/chunk-SUXIMLYN.mjs.map +1 -0
  36. package/dist/geometry-2d.d.mts +1 -1
  37. package/dist/geometry-2d.d.ts +1 -1
  38. package/dist/geometry-2d.js +1026 -231
  39. package/dist/geometry-2d.js.map +1 -1
  40. package/dist/geometry-2d.mjs +6 -5
  41. package/dist/geometry-3d.d.mts +1 -1
  42. package/dist/geometry-3d.d.ts +1 -1
  43. package/dist/geometry-3d.js +4001 -475
  44. package/dist/geometry-3d.js.map +1 -1
  45. package/dist/geometry-3d.mjs +5 -5
  46. package/dist/graph-2d.d.mts +1 -1
  47. package/dist/graph-2d.d.ts +1 -1
  48. package/dist/graph-2d.js +712 -115
  49. package/dist/graph-2d.js.map +1 -1
  50. package/dist/graph-2d.mjs +5 -5
  51. package/dist/{handleExtractProblem-BrDY9ifM.d.ts → handleExtractProblem-mtJyhEw3.d.mts} +2 -0
  52. package/dist/{handleExtractProblem-BrDY9ifM.d.mts → handleExtractProblem-mtJyhEw3.d.ts} +2 -0
  53. package/dist/host-6WTLCXUE.mjs +62 -0
  54. package/dist/host-6WTLCXUE.mjs.map +1 -0
  55. package/dist/{host-H2IGOKJU.mjs → host-BWIXT5TR.mjs} +3 -3
  56. package/dist/{host-H2IGOKJU.mjs.map → host-BWIXT5TR.mjs.map} +1 -1
  57. package/dist/{host-4FIUNIDQ.mjs → host-QDVRULJV.mjs} +14 -11
  58. package/dist/host-QDVRULJV.mjs.map +1 -0
  59. package/dist/host-USOIVTQB.mjs +4976 -0
  60. package/dist/host-USOIVTQB.mjs.map +1 -0
  61. package/dist/index.d.mts +14 -4
  62. package/dist/index.d.ts +14 -4
  63. package/dist/index.js +4273 -521
  64. package/dist/index.js.map +1 -1
  65. package/dist/index.mjs +48 -27
  66. package/dist/index.mjs.map +1 -1
  67. package/dist/latex.d.mts +1 -1
  68. package/dist/latex.d.ts +1 -1
  69. package/dist/latex.mjs +1 -1
  70. package/dist/serialize-7UZWQQ4I.mjs +9 -0
  71. package/dist/{serialize-PGHQZEPV.mjs.map → serialize-7UZWQQ4I.mjs.map} +1 -1
  72. package/dist/studio.d.mts +35 -0
  73. package/dist/studio.d.ts +35 -0
  74. package/dist/studio.js +10883 -0
  75. package/dist/studio.js.map +1 -0
  76. package/dist/studio.mjs +28 -0
  77. package/dist/studio.mjs.map +1 -0
  78. package/dist/{types-tePd94vW.d.ts → types-eEEEVsLF.d.mts} +1 -1
  79. package/dist/{types-tePd94vW.d.mts → types-eEEEVsLF.d.ts} +1 -1
  80. package/package.json +12 -2
  81. package/dist/chunk-BMYC2ILT.mjs.map +0 -1
  82. package/dist/chunk-DWIEVCGK.mjs.map +0 -1
  83. package/dist/chunk-JJ4FPCBE.mjs.map +0 -1
  84. package/dist/chunk-KOXOC2FI.mjs.map +0 -1
  85. package/dist/chunk-LTLLQUMN.mjs.map +0 -1
  86. package/dist/chunk-QK6OVDLC.mjs +0 -103
  87. package/dist/chunk-QK6OVDLC.mjs.map +0 -1
  88. package/dist/chunk-T3N4BSJV.mjs.map +0 -1
  89. package/dist/host-4FIUNIDQ.mjs.map +0 -1
  90. package/dist/host-4ZB4XD4S.mjs +0 -2378
  91. package/dist/host-4ZB4XD4S.mjs.map +0 -1
  92. package/dist/host-KMWP7KBT.mjs.map +0 -1
  93. package/dist/serialize-PGHQZEPV.mjs +0 -9
@@ -0,0 +1,238 @@
1
+ "use client";
2
+ // src/stamps/geometry-2d/ai/vision/tesseract.ts
3
+ var DEFAULT_LANG = "vie+eng";
4
+ async function runTesseractOcr(image, opts = {}) {
5
+ if (opts.signal?.aborted) {
6
+ const err = new Error("Tesseract OCR aborted");
7
+ err.name = "AbortError";
8
+ throw err;
9
+ }
10
+ const { createWorker } = await import('tesseract.js');
11
+ const lang = opts.lang ?? DEFAULT_LANG;
12
+ const worker = await createWorker(lang);
13
+ try {
14
+ const dataUrl = `data:${image.mediaType};base64,${image.base64}`;
15
+ const { data } = await worker.recognize(dataUrl);
16
+ return { text: data.text, confidence: data.confidence };
17
+ } finally {
18
+ await worker.terminate();
19
+ }
20
+ }
21
+
22
+ // src/stamps/geometry-2d/ai/vision/repairOcrSymbols.ts
23
+ var PT = `[A-Z]{2,3}['\u2032]?`;
24
+ var PERP_RE = new RegExp(
25
+ `(?<![\\p{L}\\d])(${PT})\\s+([1|L])\\s+(${PT})(?![\\p{L}\\d])`,
26
+ "gu"
27
+ );
28
+ var TRI_STRICT_RE = /(?<!giác )(?<!thang )(?<![\p{L}\d])A([A-Z]{3})\s+(đều|cân|nhọn|vuông)(?![\p{L}])/gu;
29
+ var TRI_DOUBLE_RE = /(?<![\p{L}\d])AA([A-Z]{2})(?![A-Z])/gu;
30
+ var CIRCLE_O_RE = /\(0\)/gu;
31
+ var ELEM_RE = /(?<![\p{L}\d])([A-Z](?:\s*,\s*[A-Z])+)\s*e\s*\((?:O|0)\)/gu;
32
+ var INTERSECT_RE = /([A-Z]{2,3})N\s+([A-Z]\S{0,2})\s*=\s*\{/gu;
33
+ var SQUARE_RE = /([A-Z])\?(?=\s*[-=+)])/gu;
34
+ var HORN_O_C_RE = /(?<!\p{Ll})Ơ(?!\p{Ll})/gu;
35
+ var DUONG_DBAR_RE = /D(?:ường|uong)(?= (?:tròn|tron|thẳng|kính|cao|chéo|trung|phân|vuông|nối|gấp|tâm))/gu;
36
+ var DUONG_TRON_RE = /([Đđ]ường\s+)tron(?![\p{L}])/gu;
37
+ var FLORIN_F_RE = /Ƒ/gu;
38
+ var TAI_TAI_RE = /(?<![\p{L}])tai(\s+)(?=[A-Z(]|điểm)/gu;
39
+ var TAM_TAM_RE = /(?<![\p{L}])tam(\s+)(?=đường|\(|đối xứng)/gu;
40
+ var EURO_ELEM_RE = /€/gu;
41
+ var PARALLEL_RE = /\|\|/gu;
42
+ var CENT_C_RE = /¢/gu;
43
+ var ETH_D_RE = /(?<!\p{Ll})Ð(?!\p{Ll})/gu;
44
+ var SLASH_O_RE = /(?<!\p{Ll})Ø(?!\p{Ll})/gu;
45
+ var GOI_RE = /(?<!\p{L})Goi(?!\p{L})/gu;
46
+ var LAY_RE = /(?<!\p{L})Lay(?!\p{L})/gu;
47
+ var DIQUA_RE = /(?<!\p{L})di qua(?!\p{L})/gu;
48
+ var VA_VAND_RE = /(?<!\p{L})va (?=[A-Z(]|dây|đường)/gu;
49
+ var TREN_RE = /([Tt])rén(?![\p{L}])/gu;
50
+ var DUONG_LOWER_RE = /(?<![\p{L}])dường(?= (?:tròn|tron|thẳng|kính|cao|chéo|trung|phân|vuông|nối|gấp|tâm))/gu;
51
+ var DAY_RE = /(?<![\p{L}])day(?=\s+(?:cung(?![\p{L}])|[A-Z]{2}))/gu;
52
+ var CHUNG_MINH_RE = /([Cc]hứng )mình(?![\p{L}])/gu;
53
+ var THANG_HANG_RE = /thẳng hang(?![\p{L}])/gu;
54
+ var TINH_RE = /(?<![\p{L}])Tinh(?= (?:[A-Z]|do(?![\p{L}])|độ|diện|bán|chu|số|giá))/gu;
55
+ var DO_DAI_RE = /(?<![\p{L}])do dai(?![\p{L}])/gu;
56
+ var NIM_RE = /(?<![\p{L}])nim(?= (?:giữa|trên|trong)(?![\p{L}]))/gu;
57
+ var NAM_GIUA_RE = /(?<![\p{L}])năm(?= (?:giữa|trên) [A-Z])/gu;
58
+ var ANGLE_Z_RE = /(?<![\p{L}\d])4?Z([A-Z]{2,3})(?![A-Z])/gu;
59
+ var DE_PAREN_RE = /\(Dé /gu;
60
+ var DE_VERB_RE = /(\(Đề )\S+( bởi)/gu;
61
+ var DE_THI_LOWER_RE = /(?<![\p{L}])dé(?= thi)/gu;
62
+ var DI_DONG_RE = /(?<![\p{L}])di dong(?![\p{L}])/gu;
63
+ var POUND_E_RE = /£/gu;
64
+ var TRI_SPACE_2_1 = /(tam\s*giác\s+)([A-Z])([A-Z])\s+([A-Z])(?![A-Z\p{Ll}])/gu;
65
+ var TRI_SPACE_1_2 = /(tam\s*giác\s+)([A-Z])\s+([A-Z])([A-Z])(?![A-Z\p{Ll}])/gu;
66
+ var DIAERESIS_O_RE = /(?<!\p{Ll})Ö(?!\p{Ll})/gu;
67
+ var INTERSECT_PAREN_RE = /(\([A-Z]{3}\))\s+N\s+([A-Z]{2})(\s*=\s*\{)/gu;
68
+ function repairOcrSymbols(text) {
69
+ let t = text;
70
+ t = t.replace(PERP_RE, "$1 \u22A5 $3");
71
+ t = t.replace(TRI_STRICT_RE, "tam gi\xE1c $1 $2");
72
+ t = t.replace(TRI_DOUBLE_RE, "tam gi\xE1c A$1");
73
+ t = t.replace(CIRCLE_O_RE, "(O)");
74
+ t = t.replace(ELEM_RE, "$1 \u2208 (O)");
75
+ t = t.replace(INTERSECT_RE, "$1 \u2229 $2 = {");
76
+ t = t.replace(SQUARE_RE, "$1\xB2");
77
+ t = t.replace(HORN_O_C_RE, "C");
78
+ t = t.replace(DUONG_DBAR_RE, "\u0110\u01B0\u1EDDng");
79
+ t = t.replace(DUONG_LOWER_RE, "\u0111\u01B0\u1EDDng");
80
+ t = t.replace(DUONG_TRON_RE, "$1tr\xF2n");
81
+ t = t.replace(FLORIN_F_RE, "F");
82
+ t = t.replace(TAI_TAI_RE, "t\u1EA1i$1");
83
+ t = t.replace(TAM_TAM_RE, "t\xE2m$1");
84
+ t = t.replace(EURO_ELEM_RE, "\u2208");
85
+ t = t.replace(PARALLEL_RE, "\u2225");
86
+ t = t.replace(CENT_C_RE, "c");
87
+ t = t.replace(ETH_D_RE, "D");
88
+ t = t.replace(SLASH_O_RE, "O");
89
+ t = t.replace(GOI_RE, "G\u1ECDi");
90
+ t = t.replace(LAY_RE, "L\u1EA5y");
91
+ t = t.replace(DIQUA_RE, "\u0111i qua");
92
+ t = t.replace(VA_VAND_RE, "v\xE0 ");
93
+ t = t.replace(TREN_RE, "$1r\xEAn");
94
+ t = t.replace(DAY_RE, "d\xE2y");
95
+ t = t.replace(CHUNG_MINH_RE, "$1minh");
96
+ t = t.replace(THANG_HANG_RE, "th\u1EB3ng h\xE0ng");
97
+ t = t.replace(TINH_RE, "T\xEDnh");
98
+ t = t.replace(DO_DAI_RE, "\u0111\u1ED9 d\xE0i");
99
+ t = t.replace(NIM_RE, "n\u1EB1m");
100
+ t = t.replace(NAM_GIUA_RE, "n\u1EB1m");
101
+ t = t.replace(ANGLE_Z_RE, "\u2220$1");
102
+ t = t.replace(DE_PAREN_RE, "(\u0110\u1EC1 ");
103
+ t = t.replace(DE_VERB_RE, "$1xu\u1EA5t$2");
104
+ t = t.replace(DE_THI_LOWER_RE, "\u0111\u1EC1");
105
+ t = t.replace(DI_DONG_RE, "di \u0111\u1ED9ng");
106
+ t = t.replace(POUND_E_RE, "E");
107
+ t = t.replace(TRI_SPACE_2_1, "$1$2$3$4");
108
+ t = t.replace(TRI_SPACE_1_2, "$1$2$3$4");
109
+ t = t.replace(DIAERESIS_O_RE, "O");
110
+ t = t.replace(INTERSECT_PAREN_RE, "$1 \u2229 $2$3");
111
+ return t;
112
+ }
113
+
114
+ // src/stamps/geometry-2d/ai/vision/detectFormulaRisk.ts
115
+ var SUP_RESIDUE = /[A-Z0-9]\?(?=\s*[-=+)(])/u;
116
+ var ANGLE_DEG = /([\p{L}]{1,6})\s*=\s*\d+\s*°/gu;
117
+ function hasManglingLtGt(text) {
118
+ const re = /[<>]/gu;
119
+ let m;
120
+ while ((m = re.exec(text)) !== null) {
121
+ const prev = text.slice(0, m.index).replace(/\s+$/u, "").slice(-1);
122
+ const next = text.slice(m.index + 1).replace(/^\s+/u, "").slice(0, 1);
123
+ const prevOperand = /[A-Za-z0-9]/.test(prev);
124
+ const nextOperand = /[A-Za-z0-9]/.test(next);
125
+ if (!(prevOperand && nextOperand)) return true;
126
+ }
127
+ return false;
128
+ }
129
+ function detectFormulaRisk(text) {
130
+ const reasons = [];
131
+ if (hasManglingLtGt(text)) {
132
+ reasons.push('ph\xE2n s\u1ED1/bi\u1EC3u th\u1EE9c nghi b\u1ECB nu\u1ED1t (k\xFD hi\u1EC7u "<"/">")');
133
+ }
134
+ if (SUP_RESIDUE.test(text)) {
135
+ reasons.push('s\u1ED1 m\u0169 ho\u1EB7c \u0111\u1ED9 nghi b\u1ECB m\u1EA5t (d\u1EA5u "?")');
136
+ }
137
+ for (const m of text.matchAll(ANGLE_DEG)) {
138
+ const tok = m[1];
139
+ if (/[A-Z]/.test(tok) && /[^\x00-\x7F]/u.test(tok)) {
140
+ reasons.push("t\xEAn g\xF3c nghi b\u1ECB nh\u1EADn d\u1EA1ng sai (k\xFD t\u1EF1 c\xF3 d\u1EA5u)");
141
+ break;
142
+ }
143
+ }
144
+ return reasons;
145
+ }
146
+
147
+ // src/stamps/geometry-2d/ai/vision/extractProblem.ts
148
+ var MIN_HIGH_CONFIDENCE_CHARS = 10;
149
+ var MAX_TEXT_CHARS = 2e3;
150
+ var TESSERACT_HIGH_CONFIDENCE_THRESHOLD = 70;
151
+ async function extractProblemFromImage(image, opts = {}) {
152
+ let raw;
153
+ try {
154
+ raw = await runTesseractOcr(image, {
155
+ ...opts.tesseractLang ? { lang: opts.tesseractLang } : {},
156
+ ...opts.signal ? { signal: opts.signal } : {}
157
+ });
158
+ } catch (e) {
159
+ const err = e;
160
+ return {
161
+ ok: false,
162
+ reason: "unreadable",
163
+ message: "Tesseract OCR fail: " + (err.message ?? "?")
164
+ };
165
+ }
166
+ const text = postProcess(raw.text);
167
+ if (text.length === 0) {
168
+ return { ok: false, reason: "empty", message: "Tesseract kh\xF4ng tr\xEDch \u0111\u01B0\u1EE3c text." };
169
+ }
170
+ const tooShort = text.length < MIN_HIGH_CONFIDENCE_CHARS;
171
+ const lowConf = raw.confidence < TESSERACT_HIGH_CONFIDENCE_THRESHOLD;
172
+ const confidence = tooShort || lowConf ? "low" : "high";
173
+ return {
174
+ ok: true,
175
+ text,
176
+ confidence,
177
+ warnings: detectFormulaRisk(text),
178
+ usage: { inputTokens: 0, outputTokens: 0 }
179
+ };
180
+ }
181
+ function postProcess(raw) {
182
+ let t = raw.trim();
183
+ t = t.replace(/\*\*(.+?)\*\*/g, "$1");
184
+ t = t.replace(/\*(.+?)\*/g, "$1");
185
+ t = t.replace(/_(.+?)_/g, "$1");
186
+ t = t.replace(/```[\s\S]*?```/g, "").replace(/`([^`]+)`/g, "$1");
187
+ t = t.replace(/\s+/g, " ").trim();
188
+ t = t.normalize("NFC");
189
+ t = repairOcrSymbols(t);
190
+ if (t.length > MAX_TEXT_CHARS) t = t.slice(0, MAX_TEXT_CHARS);
191
+ return t;
192
+ }
193
+
194
+ // src/stamps/geometry-2d/ai/handleExtractProblem.ts
195
+ async function handleExtractProblem(image, opts = {}) {
196
+ try {
197
+ const r = await extractProblemFromImage(image, opts);
198
+ if (r.ok) {
199
+ if (r.warnings.length > 0) {
200
+ return {
201
+ kind: "low-confidence",
202
+ text: r.text,
203
+ warning: `C\xF3 th\u1EC3 c\xF3 c\xF4ng th\u1EE9c/k\xFD hi\u1EC7u b\u1ECB nh\u1EADn d\u1EA1ng sai (${r.warnings.join("; ")}). Ki\u1EC3m tra k\u1EF9 tr\u01B0\u1EDBc khi v\u1EBD.`,
204
+ usage: r.usage
205
+ };
206
+ }
207
+ if (r.confidence === "low") {
208
+ return {
209
+ kind: "low-confidence",
210
+ text: r.text,
211
+ warning: "OCR c\xF3 th\u1EC3 kh\xF4ng ch\xEDnh x\xE1c, ki\u1EC3m tra tr\u01B0\u1EDBc khi v\u1EBD.",
212
+ usage: r.usage
213
+ };
214
+ }
215
+ return { kind: "success", text: r.text, usage: r.usage };
216
+ }
217
+ if (r.reason === "not-math") {
218
+ return { kind: "refused", reason: "not-math", message: r.message };
219
+ }
220
+ if (r.reason === "unsupported") {
221
+ return { kind: "error", code: "unsupported", message: r.message };
222
+ }
223
+ if (r.reason === "unreadable") {
224
+ return { kind: "error", code: "network", message: r.message };
225
+ }
226
+ return { kind: "error", code: "empty", message: r.message };
227
+ } catch (e) {
228
+ return {
229
+ kind: "error",
230
+ code: "unexpected",
231
+ message: e instanceof Error ? e.message : String(e)
232
+ };
233
+ }
234
+ }
235
+
236
+ export { extractProblemFromImage, handleExtractProblem, runTesseractOcr };
237
+ //# sourceMappingURL=chunk-SUXIMLYN.mjs.map
238
+ //# sourceMappingURL=chunk-SUXIMLYN.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/stamps/geometry-2d/ai/vision/tesseract.ts","../src/stamps/geometry-2d/ai/vision/repairOcrSymbols.ts","../src/stamps/geometry-2d/ai/vision/detectFormulaRisk.ts","../src/stamps/geometry-2d/ai/vision/extractProblem.ts","../src/stamps/geometry-2d/ai/handleExtractProblem.ts"],"names":[],"mappings":"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","file":"chunk-SUXIMLYN.mjs","sourcesContent":["// src/stamps/geometry-2d/ai/vision/tesseract.ts\n//\n// Tesseract.js wrapper cho client-side OCR đề toán. Default vie+eng, terminate\n// worker sau mỗi call (không cache v1). Lazy import giữ bundle slim.\n\nimport type { ImagePart } from './types';\n\nexport interface TesseractOcrOptions {\n /** Tesseract language code. Default 'vie+eng' cho đề toán VN. */\n lang?: string;\n signal?: AbortSignal;\n}\n\nexport interface TesseractOcrResult {\n text: string;\n /** Confidence 0–100 (Tesseract scale). */\n confidence: number;\n}\n\nconst DEFAULT_LANG = 'vie+eng';\n\nexport async function runTesseractOcr(\n image: ImagePart,\n opts: TesseractOcrOptions = {},\n): Promise<TesseractOcrResult> {\n if (opts.signal?.aborted) {\n const err = new Error('Tesseract OCR aborted');\n err.name = 'AbortError';\n throw err;\n }\n\n const { createWorker } = await import('tesseract.js');\n const lang = opts.lang ?? DEFAULT_LANG;\n const worker = await createWorker(lang);\n\n try {\n const dataUrl = `data:${image.mediaType};base64,${image.base64}`;\n const { data } = await worker.recognize(dataUrl);\n return { text: data.text, confidence: data.confidence };\n } finally {\n await worker.terminate();\n }\n}\n","// src/stamps/geometry-2d/ai/vision/repairOcrSymbols.ts\n//\n// Vá các lỗi NHẬN DẠNG SYMBOL đặc thù của Tesseract trên đề toán hình học VN.\n// Chạy Ở TẦNG OCR (extractProblem.postProcess) — KHÔNG nhét vào normalizeText\n// dùng chung, vì \"L\" gõ tay = điểm L, \"ABCD\" = tứ giác thật; luật dưới đây chỉ\n// đúng trên TOKEN ĐÃ HỎNG do OCR. Xem spec:\n// docs/superpowers/specs/2026-06-29-ocr-symbol-repair-design.md\n//\n// Failure modes đo thực nghiệm (tesseract.js 7 vie+eng, PDF rasterize @200dpi):\n// ⊥ → 1 | L · △/∆ → A dính đầu (AABC) · (O) → (0) · ∈ → e dính cuối list\n// ∩ → N dính (\"ABN CD = {E}\") · ² → ? (\"EF?\")\n// Triết lý: PRECISION-FIRST — thà bỏ sót còn hơn vá sai (vá sai đổi luôn hình).\n// Lưới an toàn: user review textarea + rule engine chịu nhiễu. Hàm thuần, idempotent.\n\n// Tên điểm/đoạn: 2-3 chữ HOA, prime optional. (1 chữ quá mơ hồ → bỏ.)\nconst PT = `[A-Z]{2,3}['′]?`;\n\n// R1 — ⊥ : token đơn 1/|/L kẹp giữa hai nhóm-hoa. \"IH 1 CE\" → \"IH ⊥ CE\"\nconst PERP_RE = new RegExp(\n `(?<![\\\\p{L}\\\\d])(${PT})\\\\s+([1|L])\\\\s+(${PT})(?![\\\\p{L}\\\\d])`,\n 'gu',\n);\n\n// R2a — △/∆ : glyph tam giác đọc thành \"A\" dính đầu tên + hậu tố TAM-GIÁC-THUẦN\n// (cân/đều/nhọn/vuông) NGAY sau. KHÔNG cần Cho/Xét (bắt cả \"Chứng minh: APQE cân\").\n// Guard `(?<!giác )(?<!thang )` chặn tứ giác/hình thang cân. CỐ Ý bỏ \"nội tiếp/\n// ngoại tiếp\" khỏi hậu tố ở đây vì tứ giác cũng \"nội tiếp\" → xem R2b.\nconst TRI_STRICT_RE =\n /(?<!giác )(?<!thang )(?<![\\p{L}\\d])A([A-Z]{3})\\s+(đều|cân|nhọn|vuông)(?![\\p{L}])/gu;\n\n// R2b — △ABC: tín hiệu A NHÂN ĐÔI (\"AABC\" = △+ABC) tự PHÂN BIỆT với TỨ GIÁC\n// (single-A \"ABCD\"). Doubled-A word-bounded (chính xác 4 ký tự, `(?![A-Z])` chặn\n// \"AABCD\") gần như CHẮC CHẮN là △ → KHÔNG cần tính-từ ngay sau (bản cũ yêu cầu →\n// miss \"AABC không cân\"/\"AABC có AB<AC\"/\"AABM tiếp xúc\": 16 chỗ). Tứ giác single-A\n// không khớp \"AA\" nên không bị vá nhầm.\nconst TRI_DOUBLE_RE = /(?<![\\p{L}\\d])AA([A-Z]{2})(?![A-Z])/gu;\n\n// R3 — (O) : tâm đường tròn O bị đọc thành số 0. Chỉ dạng bare \"(0)\" (né \"(0;…)\" toạ độ).\nconst CIRCLE_O_RE = /\\(0\\)/gu;\n\n// R4 — ∈ : \"A,B,C,De (O)\" — ∈ đọc thành e dính cuối list điểm phẩy.\nconst ELEM_RE =\n /(?<![\\p{L}\\d])([A-Z](?:\\s*,\\s*[A-Z])+)\\s*e\\s*\\((?:O|0)\\)/gu;\n\n// R5 — ∩ : \"AB ∩ CD = {E}\" → ∩ đọc thành \"N\" dính (\"ABN CD = {E}\"). Gate \"= {\"\n// (ký hiệu tập hợp giao điểm) ⇒ rất hiếm false-positive.\nconst INTERSECT_RE = /([A-Z]{2,3})N\\s+([A-Z]\\S{0,2})\\s*=\\s*\\{/gu;\n\n// R6 — mũ ² : \"EF²\" → \"EF?\" (chữ HOA + \"?\" + toán tử). Reconstruction precision cao\n// (khác \"TÂU\"/\"<\" bất khả thi). Né câu hỏi VN (chữ THƯỜNG trước \"?\").\nconst SQUARE_RE = /([A-Z])\\?(?=\\s*[-=+)])/gu;\n\n// R7 — nhãn điểm C đọc thành \"Ơ\" (U+01A0, O-có-móc): \"BƠ\"→\"BC\", \"ƠD\"→\"CD\",\n// \"Gọi Ơ là\"→\"Gọi C là\", \"(Ơ)\"→\"(C)\". Đo trên đề scan font CM (38/38 chỗ \"Ơ\"\n// HOA = nhãn C). Gate `(?<!\\p{Ll})Ơ(?!\\p{Ll})` chỉ vá khi KHÔNG kề chữ THƯỜNG\n// → chừa từ Việt thật (\"Ơn\"/\"Ơi\"); \"ơ\" thường (trơn/sơ) không bị đụng (≠ U+01A0 HOA).\nconst HORN_O_C_RE = /(?<!\\p{Ll})Ơ(?!\\p{Ll})/gu;\n\n// ── R8-R11: rớt-dấu tiếng Việt đặc thù OCR (Tesseract vie) — GATE theo ngữ cảnh\n// hình học để precision-first (chỉ vá khi gần như chắc chắn là từ hình-học). ──\n\n// R20 — \"Dường\" → \"Đường\" (OCR rớt gạch ngang Đ HOA, U+0110 → D U+0044). Gate =\n// WHITELIST danh-từ hình-học theo sau (KHÔNG dùng `(?!như\\b)` vì `\\b` cạnh \"ư\"\n// Việt lỗi — bug-class \\b-ASCII đã biết). Né \"Dường như\". Chạy TRƯỚC R8.\nconst DUONG_DBAR_RE =\n /D(?:ường|uong)(?= (?:tròn|tron|thẳng|kính|cao|chéo|trung|phân|vuông|nối|gấp|tâm))/gu;\n\n// R8 — \"đường tron\" → \"đường tròn\" (g/dấu rớt). Gate `(?![\\p{L}])` né \"đường trong\".\nconst DUONG_TRON_RE = /([Đđ]ường\\s+)tron(?![\\p{L}])/gu;\n\n// R9 — \"Ƒ\" (U+0192 florin) → \"F\": OCR đọc nhãn điểm F thành dấu florin. Né nhầm 0.\nconst FLORIN_F_RE = /Ƒ/gu;\n\n// R10 — \"tai\" → \"tại\" CHỈ khi theo sau là nhãn HOA / \"(\" / \"điểm\" (ngữ cảnh \"tại\n// <điểm>\"). Né từ thật \"tai\" (lỗ tai) — không bao giờ đứng trước nhãn HOA.\nconst TAI_TAI_RE = /(?<![\\p{L}])tai(\\s+)(?=[A-Z(]|điểm)/gu;\n\n// R11 — \"tam\" → \"tâm\" CHỈ trước \"đường\"/\"(\" (tâm đường tròn / tâm (O)). Gate này\n// KHÔNG đụng \"tam giác\" (theo sau là \"giác\", không phải \"đường\"/\"(\").\nconst TAM_TAM_RE = /(?<![\\p{L}])tam(\\s+)(?=đường|\\(|đối xứng)/gu;\n\n// ── R12-R19: glyph thay-thế + rớt-dấu khác (đo trên cùng PDF, gate precision-first) ──\n\n// R12 — \"€\" (euro U+20AC) → \"∈\": OCR đọc ∈ thành €. \"C € (O)\" → \"C ∈ (O)\". € không\n// bao giờ xuất hiện hợp lệ trong đề hình.\nconst EURO_ELEM_RE = /€/gu;\n\n// R13 — \"||\" → \"∥\": OCR đọc ∥ (song song) thành 2 gạch đứng. \"OK || MB\" → \"OK ∥ MB\".\nconst PARALLEL_RE = /\\|\\|/gu;\n\n// R14 — \"¢\" (cent U+00A2) → \"c\": đọc nhãn ý \"c)\" / \"c,\" thành ¢. ¢ không hợp lệ.\nconst CENT_C_RE = /¢/gu;\n\n// R15 — \"Ð\" (Eth U+00D0, KHÁC Đ-Việt U+0110) → \"D\": nhãn điểm D. Gate `(?<!\\p{Ll})…(?!\\p{Ll})`\n// chừa trường hợp Ð là Đ-Việt đầu từ (\"Ðường\" — theo sau lowercase → giữ nguyên).\nconst ETH_D_RE = /(?<!\\p{Ll})Ð(?!\\p{Ll})/gu;\n\n// R16 — \"Ø\" (O-slash U+00D8) → \"O\": nhãn điểm/đường tròn O (verify ảnh gốc: \"(O') cắt\n// nhau\"). Gate như trên (Ø không là chữ Việt nên rất an toàn).\nconst SLASH_O_RE = /(?<!\\p{Ll})Ø(?!\\p{Ll})/gu;\n\n// R17 — \"Goi\" → \"Gọi\" (\"Goi\" không là từ Việt). Gate word-boundary Unicode.\nconst GOI_RE = /(?<!\\p{L})Goi(?!\\p{L})/gu;\n\n// R18 — \"Lay\" → \"Lấy\" (động từ \"lấy điểm\"). Gate word-boundary.\nconst LAY_RE = /(?<!\\p{L})Lay(?!\\p{L})/gu;\n\n// R19 — \"di qua\" → \"đi qua\" (CHỈ bigram — né \"di chuyển\"/\"di động\" hợp lệ).\nconst DIQUA_RE = /(?<!\\p{L})di qua(?!\\p{L})/gu;\n\n// R21 — \"va\" → \"và\" (rớt dấu huyền liên từ). Gate: theo sau là nhãn HOA / \"(\" /\n// \"dây\" / \"đường\" (ref hình học) → phủ \"X va (O)\"/\"AB va AC\"/\"va dây\"; CHỪA \"va\n// chạm\"/\"va li\" (từ thật, theo sau lowercase-khác).\nconst VA_VAND_RE = /(?<!\\p{L})va (?=[A-Z(]|dây|đường)/gu;\n\n// R22 — \"Trén/trén\" → \"Trên/trên\" (OCR é↔ê trên từ \"trên\"). \"trén\" không là từ Việt.\nconst TREN_RE = /([Tt])rén(?![\\p{L}])/gu;\n\n// ── R23-R29: dấu/glyph đo trên PDF \"Tổng hợp HHP vào 10 2018-2019\" (precision-first) ──\n\n// R23 — \"dường\" (thường) → \"đường\": bản chữ-thường của R20 (D mất gạch giữa câu, vd\n// \"Kẻ dường kính\"/\"dường cao\"/\"Một dường thẳng\"). Gate = WHITELIST danh-từ hình-học\n// theo sau ⇒ né \"dường như\". Chạy TRƯỚC R8 để \"dường tron\"→\"đường tron\"→\"đường tròn\".\nconst DUONG_LOWER_RE =\n /(?<![\\p{L}])dường(?= (?:tròn|tron|thẳng|kính|cao|chéo|trung|phân|vuông|nối|gấp|tâm))/gu;\n\n// R24 — \"day\" → \"dây\" (dây cung): OCR rớt dấu mũ+huyền. Gate = \"Vẽ/Kẻ day <2 HOA>\"\n// (vẽ dây <nhãn>) hoặc \"day cung\". Né \"day\" tiếng Anh (theo sau chữ thường).\nconst DAY_RE = /(?<![\\p{L}])day(?=\\s+(?:cung(?![\\p{L}])|[A-Z]{2}))/gu;\n\n// R25 — \"Chứng mình\" → \"Chứng minh\" (OCR thêm dấu huyền vào \"minh\"). \"Chứng mình\"\n// không hợp lệ (mình = self) → an toàn. Chỉ vá \"mình\" NGAY sau \"Chứng\".\nconst CHUNG_MINH_RE = /([Cc]hứng )mình(?![\\p{L}])/gu;\n\n// R26 — \"thẳng hang\" → \"thẳng hàng\" (rớt dấu \"hàng\"). Gate = sau \"thẳng\".\nconst THANG_HANG_RE = /thẳng hang(?![\\p{L}])/gu;\n\n// R27 — \"Tinh\" → \"Tính\" (mệnh lệnh \"Tính <đại lượng>\"). Gate = theo sau nhãn HOA /\n// \"do\"/\"độ\"/đại-lượng ⇒ né danh từ \"tinh\" (tinh thể…) hiếm + \"tỉnh\"/\"tình\".\nconst TINH_RE =\n /(?<![\\p{L}])Tinh(?= (?:[A-Z]|do(?![\\p{L}])|độ|diện|bán|chu|số|giá))/gu;\n\n// R28 — \"do dai\" → \"độ dài\" (rớt dấu cụm \"độ dài\"). Bigram word-bounded → an toàn.\nconst DO_DAI_RE = /(?<![\\p{L}])do dai(?![\\p{L}])/gu;\n\n// R29 — \"năm/nim giữa|trên\" → \"nằm\" (rớt/đọc sai dấu \"nằm\"). \"nim\" không là từ Việt\n// (gate giữa|trên|trong); \"năm\" (year) CHỈ vá khi \"năm giữa|trên <nhãn HOA>\" ⇒ né\n// \"năm 2018\"/\"trong năm\".\nconst NIM_RE = /(?<![\\p{L}])nim(?= (?:giữa|trên|trong)(?![\\p{L}]))/gu;\nconst NAM_GIUA_RE = /(?<![\\p{L}])năm(?= (?:giữa|trên) [A-Z])/gu;\n\n// R30 — \"∠\" (góc) đọc thành \"Z\" (đôi khi tiền tố méo \"4Z\"). Token Z + 2-3 HOA =\n// tên góc (∠DAB, ∠ABC, ∠AEO). Gate: Z PHẢI ở đầu token (`(?<![\\p{L}\\d])`) + theo\n// sau 2-3 HOA + `(?![A-Z])` ⇒ né đoạn \"AZ\" (Z giữa), điểm \"Z \" (đứng riêng, 0 HOA\n// sau), \"Oz\" (z thường), và cụm 4+ HOA (đề phòng nhiễu OCR như \"ZGCBD\"). Cùng lớp\n// glyph với R12 €→∈ / R13 ||→∥. Verify ảnh p36 (Câu 24): sách in ∠, OCR ra \"Z\".\nconst ANGLE_Z_RE = /(?<![\\p{L}\\d])4?Z([A-Z]{2,3})(?![A-Z])/gu;\n\n// ── R31: attribution cuối đề \"(Đề xuất bởi …)\" / \"(Đề thi …)\" (metadata) ──\n// \"Đề\" (Đ-bar + ề) bị OCR đọc rớt-dấu thành \"Dé\"; động từ \"xuất\" méo thành\n// zudt/ruất/suất. CHỈ vá trong ngoặc attribution (gate \"(Dé \") → KHÔNG đụng\n// \"Dé dàng\" (=Dễ dàng) giữa câu. R31b chuẩn hoá động-từ về \"xuất\" CHỈ khi có\n// \"bởi\" (attribution người đề xuất) ⇒ \"(Đề thi …)\" giữ nguyên \"thi\".\nconst DE_PAREN_RE = /\\(Dé /gu;\nconst DE_VERB_RE = /(\\(Đề )\\S+( bởi)/gu;\nconst DE_THI_LOWER_RE = /(?<![\\p{L}])dé(?= thi)/gu;\n\n// R32 — \"di dong\" → \"di động\" (điểm/đường DI ĐỘNG). Bigram word-bounded; \"di dong\"\n// không là từ Việt (≠ \"di chuyển\"/\"di qua\" đã có R19).\nconst DI_DONG_RE = /(?<![\\p{L}])di dong(?![\\p{L}])/gu;\n\n// R33 — \"£\" (bảng Anh U+00A3) → \"E\": OCR đọc nhãn điểm E thành £ (vạch ngang ≈ E).\n// £ KHÔNG bao giờ hợp lệ trong đề hình → map về E (verify: \"tại £ và F\" = \"E và F\").\nconst POUND_E_RE = /£/gu;\n\n// R34 — OCR chèn SPACE vào tên tam giác 3-đỉnh: \"tam giác BC M\"→\"BCM\", \"tam giác\n// K AB\"→\"KAB\" (8 chỗ trên dataset). Phá MỌI rule cần \"tam giác XYZ\" liền (triangle/\n// circumcircle…). Gate: NGAY sau \"tam giác\" + đúng 3 HOA tách 1 space (2+1 hoặc\n// 1+2) + lookahead (?![A-Z\\p{Ll}]) ⇒ né 4-đỉnh (\"AB CD\") + né nuốt từ mở câu HOA\n// (\"tam giác AB Cho…\"). 2 nhánh cho 2 vị trí space.\nconst TRI_SPACE_2_1 = /(tam\\s*giác\\s+)([A-Z])([A-Z])\\s+([A-Z])(?![A-Z\\p{Ll}])/gu;\nconst TRI_SPACE_1_2 = /(tam\\s*giác\\s+)([A-Z])\\s+([A-Z])([A-Z])(?![A-Z\\p{Ll}])/gu;\n\n// R35 — \"Ö\" (O-diaeresis U+00D6, KHÁC Đ/Ø) → \"O\": nhãn điểm/tâm O bị OCR đọc thành\n// O-hai-chấm (verify PDF vào-10 C28: \"đường thẳng qua Ö vuông góc BC\" = qua O).\n// Cùng lớp glyph-O với R16 Ø→O; gate `(?<!\\p{Ll})…(?!\\p{Ll})` chừa trường hợp Ö kề\n// chữ THƯỜNG (không phải nhãn) — Ö không là chữ Việt nên rất an toàn.\nconst DIAERESIS_O_RE = /(?<!\\p{Ll})Ö(?!\\p{Ll})/gu;\n\n// R36 — ∩ (giao) đứng RỜI sau \")\" đọc thành \"N\": \"(BMC) N AC = {C, N}\" → \"(BMC) ∩\n// AC = {C, N}\" (C40). KHÁC R5 (N DÍNH nhóm-hoa \"ABN CD\"): ở đây N là token RỜI\n// ngay sau ngoặc đóng đường-tròn \"(XYZ)\". Gate \"= {\" (set-notation giao điểm) ⇒\n// rất hiếm false-positive; né \"N\" làm nhãn điểm thật (không kèm \"= {\").\nconst INTERSECT_PAREN_RE = /(\\([A-Z]{3}\\))\\s+N\\s+([A-Z]{2})(\\s*=\\s*\\{)/gu;\n\nexport function repairOcrSymbols(text: string): string {\n let t = text;\n t = t.replace(PERP_RE, '$1 ⊥ $3');\n t = t.replace(TRI_STRICT_RE, 'tam giác $1 $2');\n t = t.replace(TRI_DOUBLE_RE, 'tam giác A$1');\n t = t.replace(CIRCLE_O_RE, '(O)');\n t = t.replace(ELEM_RE, '$1 ∈ (O)');\n t = t.replace(INTERSECT_RE, '$1 ∩ $2 = {');\n t = t.replace(SQUARE_RE, '$1²');\n t = t.replace(HORN_O_C_RE, 'C');\n t = t.replace(DUONG_DBAR_RE, 'Đường'); // R20 — trước R8 để \"Dường tron\"→\"Đường tròn\"\n t = t.replace(DUONG_LOWER_RE, 'đường'); // R23 — trước R8 (chuỗi \"dường tron\"→\"đường tròn\")\n t = t.replace(DUONG_TRON_RE, '$1tròn');\n t = t.replace(FLORIN_F_RE, 'F');\n t = t.replace(TAI_TAI_RE, 'tại$1');\n t = t.replace(TAM_TAM_RE, 'tâm$1');\n t = t.replace(EURO_ELEM_RE, '∈');\n t = t.replace(PARALLEL_RE, '∥');\n t = t.replace(CENT_C_RE, 'c');\n t = t.replace(ETH_D_RE, 'D');\n t = t.replace(SLASH_O_RE, 'O');\n t = t.replace(GOI_RE, 'Gọi');\n t = t.replace(LAY_RE, 'Lấy');\n t = t.replace(DIQUA_RE, 'đi qua');\n t = t.replace(VA_VAND_RE, 'và ');\n t = t.replace(TREN_RE, '$1rên');\n t = t.replace(DAY_RE, 'dây'); // R24\n t = t.replace(CHUNG_MINH_RE, '$1minh'); // R25\n t = t.replace(THANG_HANG_RE, 'thẳng hàng'); // R26\n t = t.replace(TINH_RE, 'Tính'); // R27 — trước R28 (\"Tinh do dai\"→\"Tính độ dài\")\n t = t.replace(DO_DAI_RE, 'độ dài'); // R28\n t = t.replace(NIM_RE, 'nằm'); // R29\n t = t.replace(NAM_GIUA_RE, 'nằm'); // R29\n t = t.replace(ANGLE_Z_RE, '∠$1'); // R30\n t = t.replace(DE_PAREN_RE, '(Đề '); // R31a — trước R31b\n t = t.replace(DE_VERB_RE, '$1xuất$2'); // R31b — chỉ khi có \"bởi\"\n t = t.replace(DE_THI_LOWER_RE, 'đề'); // R31c\n t = t.replace(DI_DONG_RE, 'di động'); // R32\n t = t.replace(POUND_E_RE, 'E'); // R33\n t = t.replace(TRI_SPACE_2_1, '$1$2$3$4'); // R34 — join \"tam giác XY Z\"→\"XYZ\"\n t = t.replace(TRI_SPACE_1_2, '$1$2$3$4'); // R34 — join \"tam giác X YZ\"→\"XYZ\"\n t = t.replace(DIAERESIS_O_RE, 'O'); // R35 — Ö → O (nhãn tâm/điểm)\n t = t.replace(INTERSECT_PAREN_RE, '$1 ∩ $2$3'); // R36 — \"(XYZ) N PQ = {\" → ∩\n return t;\n}\n","// src/stamps/geometry-2d/ai/vision/detectFormulaRisk.ts\n//\n// Cờ CẢNH BÁO khi output OCR có dấu vết công thức/ký hiệu bị Tesseract HUỶ\n// (phân số, mũ, độ, tên góc có dấu). KHÔNG sửa text — thông tin đã mất ở tầng\n// pixel→glyph, sửa = đoán mò. Mục tiêu: chặn user bị lừa bởi text trông sạch\n// nhưng confidence vẫn cao (đo: \"a²/pq\"→\"<\", \"x̂Ay\"→\"TÂU\", \"BM²\"→\"BM?\").\n// Best-effort advisory — KHÔNG bắt mọi ca (vd \"x̂Ay\"→\"zAy\" không dấu hiệu).\n// Spec: docs/superpowers/specs/2026-06-29-ocr-formula-risk-warning-design.md\n\n// M2 — mũ ² / độ ° bị mất thành \"?\" dính sau chữ-HOA/số + tiếp toán tử/ngoặc.\n// Né câu hỏi VN (chữ thường trước \"?\", vd \"vuông?\") và số thứ tự (\"? 2,\").\nconst SUP_RESIDUE = /[A-Z0-9]\\?(?=\\s*[-=+)(])/u;\n\n// M3 — token kiểu tên-góc đứng trước \"= N°\" mà chứa ký tự CÓ DẤU (point name\n// thật là ASCII A-Z; có dấu ⇒ OCR méo, vd \"TÂU = 90°\").\nconst ANGLE_DEG = /([\\p{L}]{1,6})\\s*=\\s*\\d+\\s*°/gu;\n\n// M1 — \"<\"/\">\" KHÔNG ở dạng bất đẳng thức \"operand OP operand\" ⇒ nghi biểu thức\n// (phân số) bị nuốt còn trơ dấu, vd \"Chứng minh: < không đổi\".\nfunction hasManglingLtGt(text: string): boolean {\n const re = /[<>]/gu;\n let m: RegExpExecArray | null;\n while ((m = re.exec(text)) !== null) {\n const prev = text.slice(0, m.index).replace(/\\s+$/u, '').slice(-1);\n const next = text.slice(m.index + 1).replace(/^\\s+/u, '').slice(0, 1);\n const prevOperand = /[A-Za-z0-9]/.test(prev);\n const nextOperand = /[A-Za-z0-9]/.test(next);\n if (!(prevOperand && nextOperand)) return true;\n }\n return false;\n}\n\nexport function detectFormulaRisk(text: string): string[] {\n const reasons: string[] = [];\n if (hasManglingLtGt(text)) {\n reasons.push('phân số/biểu thức nghi bị nuốt (ký hiệu \"<\"/\">\")');\n }\n if (SUP_RESIDUE.test(text)) {\n reasons.push('số mũ hoặc độ nghi bị mất (dấu \"?\")');\n }\n for (const m of text.matchAll(ANGLE_DEG)) {\n const tok = m[1];\n if (/[A-Z]/.test(tok) && /[^\\x00-\\x7F]/u.test(tok)) {\n reasons.push('tên góc nghi bị nhận dạng sai (ký tự có dấu)');\n break;\n }\n }\n return reasons;\n}\n","// src/stamps/geometry-2d/ai/vision/extractProblem.ts\n//\n// Orchestrator vision → text. Engine duy nhất: Tesseract.js client-side OCR,\n// offline, không cần network / LLM / API key. (Engine 'llm' cũ đã gỡ.)\n\nimport { runTesseractOcr } from './tesseract';\nimport { repairOcrSymbols } from './repairOcrSymbols';\nimport { detectFormulaRisk } from './detectFormulaRisk';\nimport type { ImagePart } from './types';\n\n// Ngưỡng: text ngắn hơn thì force confidence=low bất kể engine report gì.\nconst MIN_HIGH_CONFIDENCE_CHARS = 10;\nconst MAX_TEXT_CHARS = 2000;\n// Tesseract confidence (0-100): ≥ ngưỡng này coi như high.\nconst TESSERACT_HIGH_CONFIDENCE_THRESHOLD = 70;\n\nexport interface ExtractProblemOptions {\n /** Tesseract language. Default 'vie+eng' cho đề toán VN. */\n tesseractLang?: string;\n signal?: AbortSignal;\n}\n\nexport interface ExtractProblemSuccess {\n ok: true;\n text: string;\n confidence: 'high' | 'low';\n /** Cảnh báo công thức/ký hiệu nghi bị OCR huỷ (detectFormulaRisk). Rỗng = sạch. */\n warnings: string[];\n usage: { inputTokens: number; outputTokens: number };\n}\n\nexport interface ExtractProblemFailure {\n ok: false;\n reason: 'not-math' | 'unreadable' | 'empty' | 'unsupported';\n message: string;\n}\n\nexport type ExtractProblemOutcome = ExtractProblemSuccess | ExtractProblemFailure;\n\nexport async function extractProblemFromImage(\n image: ImagePart,\n opts: ExtractProblemOptions = {},\n): Promise<ExtractProblemOutcome> {\n let raw: { text: string; confidence: number };\n try {\n raw = await runTesseractOcr(image, {\n ...(opts.tesseractLang ? { lang: opts.tesseractLang } : {}),\n ...(opts.signal ? { signal: opts.signal } : {}),\n });\n } catch (e) {\n const err = e as { message?: string };\n return {\n ok: false,\n reason: 'unreadable',\n message: 'Tesseract OCR fail: ' + (err.message ?? '?'),\n };\n }\n\n const text = postProcess(raw.text);\n if (text.length === 0) {\n return { ok: false, reason: 'empty', message: 'Tesseract không trích được text.' };\n }\n\n const tooShort = text.length < MIN_HIGH_CONFIDENCE_CHARS;\n const lowConf = raw.confidence < TESSERACT_HIGH_CONFIDENCE_THRESHOLD;\n const confidence: 'high' | 'low' = tooShort || lowConf ? 'low' : 'high';\n\n return {\n ok: true,\n text,\n confidence,\n warnings: detectFormulaRisk(text),\n usage: { inputTokens: 0, outputTokens: 0 },\n };\n}\n\nfunction postProcess(raw: string): string {\n let t = raw.trim();\n t = t.replace(/\\*\\*(.+?)\\*\\*/g, '$1');\n t = t.replace(/\\*(.+?)\\*/g, '$1');\n t = t.replace(/_(.+?)_/g, '$1');\n t = t.replace(/```[\\s\\S]*?```/g, '').replace(/`([^`]+)`/g, '$1');\n t = t.replace(/\\s+/g, ' ').trim();\n t = t.normalize('NFC');\n // Vá symbol đặc thù lỗi OCR (⊥ △ (O) ∈) — đặt ở tầng OCR, không ở normalizeText\n // dùng chung. Token đã collapse 1-space + NFC nên pattern ổn định.\n t = repairOcrSymbols(t);\n if (t.length > MAX_TEXT_CHARS) t = t.slice(0, MAX_TEXT_CHARS);\n return t;\n}\n","// src/stamps/geometry-2d/ai/handleExtractProblem.ts\n//\n// Façade UI-friendly cho extractProblemFromImage. Map outcome → UI result kind.\n// Engine = Tesseract client-side (offline, không LLM).\n\nimport {\n extractProblemFromImage,\n type ExtractProblemOptions,\n} from './vision/extractProblem';\nimport type { ImagePart } from './vision/types';\n\nexport interface HandleExtractProblemOptions extends ExtractProblemOptions {}\n\nexport type ExtractUiResult =\n | {\n kind: 'success';\n text: string;\n usage: { inputTokens: number; outputTokens: number };\n }\n | {\n kind: 'low-confidence';\n text: string;\n warning: string;\n usage: { inputTokens: number; outputTokens: number };\n }\n | {\n kind: 'refused';\n reason: 'not-math';\n message: string;\n }\n | {\n kind: 'error';\n code: 'network' | 'unsupported' | 'unexpected' | 'empty';\n message: string;\n };\n\nexport async function handleExtractProblem(\n image: ImagePart,\n opts: HandleExtractProblemOptions = {},\n): Promise<ExtractUiResult> {\n try {\n const r = await extractProblemFromImage(image, opts);\n if (r.ok) {\n // Cảnh báo công thức nghi sai ưu tiên hơn (cụ thể hơn) — confidence vẫn\n // có thể 'high' dù công thức bị huỷ (đo: \"a²/pq\"→\"<\" mà conf=90).\n if (r.warnings.length > 0) {\n return {\n kind: 'low-confidence',\n text: r.text,\n warning: `Có thể có công thức/ký hiệu bị nhận dạng sai (${r.warnings.join('; ')}). Kiểm tra kỹ trước khi vẽ.`,\n usage: r.usage,\n };\n }\n if (r.confidence === 'low') {\n return {\n kind: 'low-confidence',\n text: r.text,\n warning: 'OCR có thể không chính xác, kiểm tra trước khi vẽ.',\n usage: r.usage,\n };\n }\n return { kind: 'success', text: r.text, usage: r.usage };\n }\n if (r.reason === 'not-math') {\n return { kind: 'refused', reason: 'not-math', message: r.message };\n }\n if (r.reason === 'unsupported') {\n return { kind: 'error', code: 'unsupported', message: r.message };\n }\n if (r.reason === 'unreadable') {\n return { kind: 'error', code: 'network', message: r.message };\n }\n return { kind: 'error', code: 'empty', message: r.message };\n } catch (e) {\n return {\n kind: 'error',\n code: 'unexpected',\n message: e instanceof Error ? e.message : String(e),\n };\n }\n}\n"]}
@@ -1,4 +1,4 @@
1
- import { B as BaseStampCustomData, S as StampType } from './types-tePd94vW.mjs';
1
+ import { B as BaseStampCustomData, S as StampType } from './types-eEEEVsLF.mjs';
2
2
  import 'react';
3
3
  import '@excalidraw/excalidraw/element/types';
4
4
 
@@ -1,4 +1,4 @@
1
- import { B as BaseStampCustomData, S as StampType } from './types-tePd94vW.js';
1
+ import { B as BaseStampCustomData, S as StampType } from './types-eEEEVsLF.js';
2
2
  import 'react';
3
3
  import '@excalidraw/excalidraw/element/types';
4
4