File size: 5,800 Bytes
c04a174 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 | import * as pdfjsLib from 'pdfjs-dist';
import mammoth from 'mammoth';
import { Question } from '../types';
// Configure a reliable CDN-hosted worker for PDF.js to prevent local asset bundle errors
pdfjsLib.GlobalWorkerOptions.workerSrc = `https://cdnjs.cloudflare.com/ajax/libs/pdf.js/4.10.38/pdf.worker.min.mjs`;
export class DocumentParserService {
/**
* Primary Entrypoint: Extract raw string content from an uploaded File (PDF, DOCX, or TXT)
*/
static async extractTextFromFile(file: File): Promise<string> {
const extension = file.name.split('.').pop()?.toLowerCase();
if (extension === 'txt') {
return new Promise((resolve, reject) => {
const reader = new FileReader();
reader.onload = () => resolve(reader.result as string);
reader.onerror = () => reject(new Error("Unable to parse TXT document."));
reader.readAsText(file);
});
}
if (extension === 'docx') {
return new Promise((resolve, reject) => {
const reader = new FileReader();
reader.onload = async () => {
try {
const arrayBuffer = reader.result as ArrayBuffer;
const result = await mammoth.extractRawText({ arrayBuffer });
resolve(result.value);
} catch (err: any) {
reject(new Error("Unable to parse Microsoft Word Document (.docx): " + err.message));
}
};
reader.onerror = () => reject(new Error("File load error in Word parser."));
reader.readAsArrayBuffer(file);
});
}
if (extension === 'pdf') {
return new Promise((resolve, reject) => {
const reader = new FileReader();
reader.onload = async () => {
try {
const arrayBuffer = reader.result as ArrayBuffer;
const loadingTask = pdfjsLib.getDocument({ data: arrayBuffer });
const pdf = await loadingTask.promise;
let extractedText = "";
for (let i = 1; i <= pdf.numPages; i++) {
const page = await pdf.getPage(i);
const textContent = await page.getTextContent();
const pageText = textContent.items
.map((item: any) => item.str)
.join(" ");
extractedText += pageText + "\n";
}
resolve(extractedText);
} catch (err: any) {
reject(new Error("Unable to parse PDF document (.pdf): " + err.message));
}
};
reader.onerror = () => reject(new Error("File load error in PDF parser."));
reader.readAsArrayBuffer(file);
});
}
throw new Error("Unsupported file schema! Please upload .TXT, .PDF, or .DOCX files.");
}
/**
* Local Regex Parser Failsafe: Standard Regex pattern extraction in case the AI pipeline is offline
*/
static parseLocalRegex(text: string): Question[] {
const lines = text.split('\n').map(l => l.trim()).filter(l => l.length > 0);
const questions: Question[] = [];
let currentQuestion: Partial<Question> | null = null;
lines.forEach((line) => {
// Look for a question beginning with numbers: e.g. "1. What is..." or "Question 13:"
const qMatch = line.match(/^(\d+)[\.\)]\s*(.*)$/i) || line.match(/^Question\s*(\d+)[:\.]?\s*(.*)$/i) || line.match(/^Q(\d+)[:\.]?\s*(.*)$/i);
if (qMatch) {
if (currentQuestion && currentQuestion.text) {
questions.push(this.normalizeCleanQuestion(currentQuestion));
}
currentQuestion = {
id: `manual-regex-${Date.now()}-${qMatch[1]}`,
text: qMatch[2],
options: [],
correctOptionId: 'opt-1',
category: 'General',
difficulty: 'medium',
tags: []
};
return;
}
// Look for option formats: e.g. "A) Option" or "B. Other option"
const oMatch = line.match(/^([A-D])[\.\)]\s*(.*)$/i) || line.match(/^\(([A-D])\)\s*(.*)$/i);
if (oMatch && currentQuestion) {
const letterIndex = oMatch[1].toUpperCase();
const optionIdMap: Record<string, string> = { A: 'opt-1', B: 'opt-2', C: 'opt-3', D: 'opt-4' };
currentQuestion.options?.push({
id: optionIdMap[letterIndex] || `opt-${letterIndex.toLowerCase()}`,
text: oMatch[2]
});
return;
}
// Look for declared answers: e.g. "Answer: B" or "ANS: C"
const aMatch = line.match(/^(?:Answer|ANS)[:\s]+([A-D])/i);
if (aMatch && currentQuestion) {
const letterIndex = aMatch[1].toUpperCase();
const optionIdMap: Record<string, string> = { A: 'opt-1', B: 'opt-2', C: 'opt-3', D: 'opt-4' };
currentQuestion.correctOptionId = optionIdMap[letterIndex] || 'opt-1';
return;
}
// Append multi-line question text if appropriate
if (currentQuestion && !oMatch && !aMatch) {
currentQuestion.text += ' ' + line;
}
});
if (currentQuestion && currentQuestion.text) {
questions.push(this.normalizeCleanQuestion(currentQuestion));
}
return questions;
}
private static normalizeCleanQuestion(q: Partial<Question>): Question {
const baseOptIds = ['opt-1', 'opt-2', 'opt-3', 'opt-4'];
// Ensure accurate 4-option shape
const options = q.options || [];
while (options.length < 4) {
options.push({ id: baseOptIds[options.length], text: `Option ${String.fromCharCode(65 + options.length)} placeholder` });
}
const slicedOptions = options.slice(0, 4);
return {
text: q.text || "Untitled Question",
options: slicedOptions,
correctOptionId: q.correctOptionId || 'opt-1',
category: q.category || 'General',
difficulty: q.difficulty || 'medium',
tags: q.tags || []
} as Question;
}
}
|