| | const fs = require('fs'); |
| | const path = require('path'); |
| | const { extractTextFromImage, uploadImageToHosting } = require('./imageProcessor'); |
| |
|
| | |
| | const NUMBER_WORDS = { |
| | 'nol': '0', 'satu': '1', 'dua': '2', 'tiga': '3', 'empat': '4', 'lima': '5', |
| | 'enam': '6', 'tujuh': '7', 'delapan': '8', 'sembilan': '9', 'sepuluh': '10', |
| | 'sebelas': '11', 'belas': '', 'puluh': '', 'ratus': '', 'ribu': '' |
| | }; |
| |
|
| | |
| | const LEET_MAP = { |
| | '4': 'a', '@': 'a', '8': 'b', '3': 'e', '6': 'g', '1': 'i', '!': 'i', '0': 'o', |
| | '5': 's', '$': 's', '7': 't', '+': 't', '2': 'z' |
| | }; |
| |
|
| | function safeString(s) { |
| | return (s || '').toString(); |
| | } |
| |
|
| | async function extractTextFromBuffer(imageBuffer) { |
| | try { |
| | if (!imageBuffer) throw new Error('No image buffer provided'); |
| | if (!Buffer.isBuffer(imageBuffer)) throw new Error('extractTextFromBuffer expects a Buffer'); |
| |
|
| | console.log('πΌοΈ DEBUG extractTextFromBuffer: Received buffer, size:', imageBuffer.length, 'bytes'); |
| |
|
| | const result = await extractTextFromImage(imageBuffer); |
| | console.log('β
DEBUG extractTextFromBuffer: Hasil ekstraksi:', result); |
| | return result; |
| | } catch (error) { |
| | console.error('β DEBUG extractTextFromBuffer Error:', error.message); |
| | return { status: false, response: 'Gagal memproses gambar' }; |
| | } |
| | } |
| |
|
| | function removeAccents(str) { |
| | return safeString(str).normalize('NFD').replace(/[\u0300-\u036f]/g, ''); |
| | } |
| |
|
| | function applyLeetMap(str) { |
| | let out = ''; |
| | for (const ch of str) { |
| | out += (LEET_MAP[ch] !== undefined) ? LEET_MAP[ch] : ch; |
| | } |
| | return out; |
| | } |
| |
|
| | function wordsToNumbers(str) { |
| | |
| | const tokens = str.split(/\s+/); |
| | return tokens.map(t => { |
| | const low = t.toLowerCase(); |
| | return NUMBER_WORDS[low] !== undefined ? NUMBER_WORDS[low] : t; |
| | }).join(' '); |
| | } |
| |
|
| | function normalizeText(text) { |
| | const original = safeString(text); |
| | let normalized = removeAccents(original); |
| | normalized = normalized.toLowerCase(); |
| | normalized = applyLeetMap(normalized); |
| | |
| | normalized = normalized.replace(/[^a-z0-9\s]/g, ' ').replace(/\s+/g, ' ').trim(); |
| | |
| | normalized = wordsToNumbers(normalized); |
| | console.log(`π€ DEBUG normalizeText: "${original}" -> "${normalized}"`); |
| | return normalized; |
| | } |
| |
|
| | |
| | function levenshtein(a = '', b = '') { |
| | const alen = a.length, blen = b.length; |
| | if (alen === 0) return blen; |
| | if (blen === 0) return alen; |
| | const matrix = Array.from({ length: alen + 1 }, () => new Array(blen + 1)); |
| | for (let i = 0; i <= alen; i++) matrix[i][0] = i; |
| | for (let j = 0; j <= blen; j++) matrix[0][j] = j; |
| | for (let i = 1; i <= alen; i++) { |
| | for (let j = 1; j <= blen; j++) { |
| | const cost = a[i - 1] === b[j - 1] ? 0 : 1; |
| | matrix[i][j] = Math.min( |
| | matrix[i - 1][j] + 1, |
| | matrix[i][j - 1] + 1, |
| | matrix[i - 1][j - 1] + cost |
| | ); |
| | } |
| | } |
| | return matrix[alen][blen]; |
| | } |
| |
|
| | function similarity(a, b) { |
| | a = safeString(a); |
| | b = safeString(b); |
| | if (a === b) return 1; |
| | const dist = levenshtein(a, b); |
| | const maxLen = Math.max(a.length, b.length); |
| | return maxLen === 0 ? 1 : 1 - (dist / maxLen); |
| | } |
| |
|
| | function tryEvaluateMathExpression(s) { |
| | |
| | try { |
| | const cleaned = s.replace(/[^0-9+\-*/().\s]/g, ''); |
| | if (!/[0-9]/.test(cleaned)) return null; |
| | |
| | const val = Function(`"use strict"; return (${cleaned});`)(); |
| | if (typeof val === 'number' && isFinite(val)) return String(val); |
| | return null; |
| | } catch { |
| | return null; |
| | } |
| | } |
| |
|
| | |
| | function isValueMatch(value, soal, options = {}) { |
| | const { fuzzyThreshold = 0.75 } = options; |
| | console.log('π DEBUG isValueMatch: Value="%s", Soal="%s"', value, soal); |
| | if (!value && !soal) return false; |
| |
|
| | const vNorm = normalizeText(safeString(value)); |
| | const sNorm = normalizeText(safeString(soal)); |
| |
|
| | |
| | if (vNorm === sNorm) { |
| | console.log('β
DEBUG exact match'); |
| | return true; |
| | } |
| |
|
| | |
| | const vMath = tryEvaluateMathExpression(vNorm); |
| | const sMath = tryEvaluateMathExpression(sNorm); |
| | if (vMath !== null && sMath !== null && vMath === sMath) { |
| | console.log('β
DEBUG math match:', vMath); |
| | return true; |
| | } |
| | if (vMath !== null && sMath === null && vMath === sNorm) { |
| | console.log('β
DEBUG math->soal match:', vMath); |
| | return true; |
| | } |
| | if (sMath !== null && vMath === null && sMath === vNorm) { |
| | console.log('β
DEBUG soal->math match:', sMath); |
| | return true; |
| | } |
| |
|
| | |
| | const vNum = parseFloat(vNorm); |
| | const sNum = parseFloat(sNorm); |
| | if (!isNaN(vNum) && !isNaN(sNum) && Math.abs(vNum - sNum) < 1e-9) { |
| | console.log('β
DEBUG numeric equal'); |
| | return true; |
| | } |
| |
|
| | |
| | const sim = similarity(vNorm, sNorm); |
| | console.log('π DEBUG Similarity:', sim); |
| | if (sim >= fuzzyThreshold) { |
| | console.log('β
DEBUG fuzzy match (threshold=', fuzzyThreshold, ')'); |
| | return true; |
| | } |
| |
|
| | |
| | if (vNorm && sNorm) { |
| | if (vNorm.includes(sNorm) || sNorm.includes(vNorm)) { |
| | const longer = Math.max(vNorm.length, sNorm.length); |
| | if (longer >= 3) { |
| | console.log('β
DEBUG partial contains match'); |
| | return true; |
| | } |
| | } |
| | } |
| |
|
| | console.log('β DEBUG: No match found'); |
| | return false; |
| | } |
| |
|
| | function mapAnswer(soalArray, jawaban, botIndex) { |
| | console.log(`π€ DEBUG mapAnswer: Bot ${botIndex}, Jawaban: "${jawaban}"`); |
| | |
| | if (jawaban && typeof jawaban === 'object' && jawaban.response) { |
| | return jawaban.response; |
| | } |
| | return jawaban; |
| | } |
| |
|
| | |
| | function countPairs(s1 = '', s2 = '') { |
| | s1 = safeString(s1).toLowerCase().replace(/[^a-z]/g, ''); |
| | s2 = safeString(s2).toLowerCase().replace(/[^a-z]/g, ''); |
| | const n1 = s1.length, n2 = s2.length; |
| | const freq1 = Array(26).fill(0); |
| | const freq2 = Array(26).fill(0); |
| | for (let i = 0; i < n1; i++) freq1[s1.charCodeAt(i) - 97] = (freq1[s1.charCodeAt(i) - 97] || 0) + 1; |
| | for (let i = 0; i < n2; i++) freq2[s2.charCodeAt(i) - 97] = (freq2[s2.charCodeAt(i) - 97] || 0) + 1; |
| | let count = 0; |
| | for (let i = 0; i < 26; i++) count += Math.min(freq1[i], freq2[i]); |
| | return count; |
| | } |
| |
|
| | |
| | module.exports = { |
| | extractTextFromBuffer, |
| | mapAnswer, |
| | normalizeText, |
| | isValueMatch, |
| | levenshtein, |
| | similarity, |
| | NUMBER_WORDS, |
| | LEET_MAP, |
| | countPairs |
| | }; |