Add document AI gen support
This commit is contained in:
parent
16007cc3aa
commit
028bab23fd
11 changed files with 1270 additions and 170 deletions
|
|
@ -3,6 +3,7 @@ import cors from 'cors';
|
|||
import { db } from './db/connection.js';
|
||||
import quizzesRouter from './routes/quizzes.js';
|
||||
import usersRouter from './routes/users.js';
|
||||
import uploadRouter from './routes/upload.js';
|
||||
|
||||
const app = express();
|
||||
const PORT = process.env.PORT || 3001;
|
||||
|
|
@ -58,6 +59,7 @@ app.get('/health', (_req: Request, res: Response) => {
|
|||
|
||||
app.use('/api/quizzes', quizzesRouter);
|
||||
app.use('/api/users', usersRouter);
|
||||
app.use('/api/upload', uploadRouter);
|
||||
|
||||
app.use((err: Error, _req: Request, res: Response, _next: NextFunction) => {
|
||||
console.error('Unhandled error:', err);
|
||||
|
|
|
|||
71
server/src/routes/upload.ts
Normal file
71
server/src/routes/upload.ts
Normal file
|
|
@ -0,0 +1,71 @@
|
|||
import { Router } from 'express';
|
||||
import multer from 'multer';
|
||||
import { processDocument, SUPPORTED_TYPES } from '../services/documentParser.js';
|
||||
|
||||
const router = Router();
|
||||
|
||||
const storage = multer.memoryStorage();
|
||||
|
||||
const upload = multer({
|
||||
storage,
|
||||
limits: {
|
||||
fileSize: 50 * 1024 * 1024, // 50MB limit
|
||||
},
|
||||
fileFilter: (_req, file, cb) => {
|
||||
if (SUPPORTED_TYPES.includes(file.mimetype)) {
|
||||
cb(null, true);
|
||||
} else {
|
||||
cb(new Error(`Unsupported file type: ${file.mimetype}. Supported types: ${SUPPORTED_TYPES.join(', ')}`));
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
router.post('/', upload.single('document'), async (req, res) => {
|
||||
try {
|
||||
if (!req.file) {
|
||||
return res.status(400).json({ error: 'No file uploaded' });
|
||||
}
|
||||
|
||||
const useOcr = req.body?.useOcr === 'true' || req.body?.useOcr === true;
|
||||
const processed = await processDocument(req.file.buffer, req.file.mimetype, { useOcr });
|
||||
|
||||
if (processed.type === 'native') {
|
||||
res.json({
|
||||
type: 'native',
|
||||
content: (processed.content as Buffer).toString('base64'),
|
||||
mimeType: processed.mimeType,
|
||||
originalName: req.file.originalname
|
||||
});
|
||||
} else {
|
||||
res.json({
|
||||
type: 'text',
|
||||
content: processed.content as string,
|
||||
mimeType: processed.mimeType,
|
||||
originalName: req.file.originalname
|
||||
});
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Upload processing error:', error);
|
||||
res.status(500).json({
|
||||
error: error instanceof Error ? error.message : 'Failed to process document'
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
router.use((err: Error, _req: any, res: any, _next: any) => {
|
||||
if (err instanceof multer.MulterError) {
|
||||
if (err.code === 'LIMIT_FILE_SIZE') {
|
||||
return res.status(400).json({ error: 'File too large. Maximum size is 50MB.' });
|
||||
}
|
||||
return res.status(400).json({ error: err.message });
|
||||
}
|
||||
|
||||
if (err.message.includes('Unsupported file type')) {
|
||||
return res.status(400).json({ error: err.message });
|
||||
}
|
||||
|
||||
console.error('Upload error:', err);
|
||||
res.status(500).json({ error: 'Upload failed' });
|
||||
});
|
||||
|
||||
export default router;
|
||||
123
server/src/services/documentParser.ts
Normal file
123
server/src/services/documentParser.ts
Normal file
|
|
@ -0,0 +1,123 @@
|
|||
import officeParser from 'officeparser';
|
||||
|
||||
// MIME types that Gemini can handle natively (send as-is)
|
||||
export const GEMINI_NATIVE_TYPES = [
|
||||
'application/pdf',
|
||||
'text/plain',
|
||||
'text/markdown',
|
||||
'text/csv',
|
||||
'text/html',
|
||||
'image/jpeg',
|
||||
'image/png',
|
||||
'image/gif',
|
||||
'image/webp'
|
||||
];
|
||||
|
||||
// MIME types that officeparser can extract text from
|
||||
export const OFFICEPARSER_TYPES = [
|
||||
'application/vnd.openxmlformats-officedocument.wordprocessingml.document', // .docx
|
||||
'application/vnd.openxmlformats-officedocument.presentationml.presentation', // .pptx
|
||||
'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet', // .xlsx
|
||||
'application/vnd.oasis.opendocument.text', // .odt
|
||||
'application/vnd.oasis.opendocument.presentation', // .odp
|
||||
'application/vnd.oasis.opendocument.spreadsheet', // .ods
|
||||
'application/rtf', // .rtf
|
||||
];
|
||||
|
||||
// Image types that can use OCR for text extraction
|
||||
export const OCR_CAPABLE_TYPES = [
|
||||
'image/jpeg',
|
||||
'image/png',
|
||||
'image/gif',
|
||||
'image/webp'
|
||||
];
|
||||
|
||||
export const SUPPORTED_TYPES = [...GEMINI_NATIVE_TYPES, ...OFFICEPARSER_TYPES];
|
||||
|
||||
export interface ProcessedDocument {
|
||||
type: 'native' | 'text';
|
||||
content: Buffer | string;
|
||||
mimeType: string;
|
||||
}
|
||||
|
||||
export interface ProcessOptions {
|
||||
useOcr?: boolean;
|
||||
}
|
||||
|
||||
export function isSupportedType(mimeType: string): boolean {
|
||||
return SUPPORTED_TYPES.includes(mimeType);
|
||||
}
|
||||
|
||||
export function isGeminiNative(mimeType: string): boolean {
|
||||
return GEMINI_NATIVE_TYPES.includes(mimeType);
|
||||
}
|
||||
|
||||
export function isOcrCapable(mimeType: string): boolean {
|
||||
return OCR_CAPABLE_TYPES.includes(mimeType);
|
||||
}
|
||||
|
||||
export function needsOfficeParser(mimeType: string): boolean {
|
||||
return OFFICEPARSER_TYPES.includes(mimeType);
|
||||
}
|
||||
|
||||
async function extractWithOfficeParser(buffer: Buffer, useOcr: boolean = false): Promise<string> {
|
||||
const config = useOcr ? {
|
||||
extractAttachments: true,
|
||||
ocr: true,
|
||||
ocrLanguage: 'eng'
|
||||
} : {};
|
||||
|
||||
const ast = await officeParser.parseOffice(buffer, config);
|
||||
let text = ast.toText();
|
||||
|
||||
if (useOcr && ast.attachments) {
|
||||
for (const attachment of ast.attachments) {
|
||||
if (attachment.ocrText) {
|
||||
text += '\n' + attachment.ocrText;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return text;
|
||||
}
|
||||
|
||||
export async function processDocument(
|
||||
buffer: Buffer,
|
||||
mimeType: string,
|
||||
options: ProcessOptions = {}
|
||||
): Promise<ProcessedDocument> {
|
||||
if (!isSupportedType(mimeType)) {
|
||||
throw new Error(`Unsupported file type: ${mimeType}`);
|
||||
}
|
||||
|
||||
// Images with OCR requested - extract text
|
||||
if (isOcrCapable(mimeType) && options.useOcr) {
|
||||
const text = await extractWithOfficeParser(buffer, true);
|
||||
return {
|
||||
type: 'text',
|
||||
content: text,
|
||||
mimeType: 'text/plain'
|
||||
};
|
||||
}
|
||||
|
||||
// Gemini-native types (including images without OCR) - pass through
|
||||
if (isGeminiNative(mimeType)) {
|
||||
return {
|
||||
type: 'native',
|
||||
content: buffer,
|
||||
mimeType
|
||||
};
|
||||
}
|
||||
|
||||
// Office documents - extract text, OCR extracts text from embedded images
|
||||
if (needsOfficeParser(mimeType)) {
|
||||
const text = await extractWithOfficeParser(buffer, options.useOcr);
|
||||
return {
|
||||
type: 'text',
|
||||
content: text,
|
||||
mimeType: 'text/plain'
|
||||
};
|
||||
}
|
||||
|
||||
throw new Error(`No extraction handler for: ${mimeType}`);
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue