Agregar parsing de PDF e imágenes con OCR de QVAC
Reutiliza la tarea de OCR de QVAC para convertir páginas escaneadas e imágenes en LocalDocuments.
Dónde pertenece: src/qvac.ts para la llamada de OCR,
src/files.ts para el pegamento del adaptador. El resto del
pipeline no necesita saber.
LocalLens actualmente indexa formatos de texto plano: Markdown, código fuente, JSON, YAML. Para meter imágenes y PDFs escaneados en el mismo cerebro, no necesitas un parser de terceros. El SDK de QVAC trae una tarea de OCR que puedes conectar junto con los modelos de chat y embedding.
La referencia oficial de OCR de QVAC vive en
docs.qvac.tether.io/sdk/examples/ai-tasks/ocr.
La receta de abajo asume que la tienes abierta.
La superficie de OCR de QVAC
import {
loadModel,
ocr,
unloadModel,
OCR_LATIN_RECOGNIZER_1,
} from "@qvac/sdk";Un solo modelo (OCR_LATIN_RECOGNIZER_1) maneja el recognizer.
ocr({ modelId, image, options }) devuelve una promesa blocks
que resuelve a un array de objetos { text, bbox?, confidence? }.
La receta de cuatro pasos
1. Agregar un método de OCR al gateway
Extiende QvacGateway para que cargue el modelo de OCR de forma
lazy, junto con los modelos de chat y embedding, y exponga un solo
helper extractText:
import {
loadModel,
ocr,
unloadModel,
OCR_LATIN_RECOGNIZER_1,
} from "@qvac/sdk";
export class QvacGateway {
// existing fields…
private ocrModelId: string | undefined;
private async ensureOcrReady(): Promise<void> {
if (this.ocrModelId) return;
this.ocrModelId = await loadModel({
modelSrc: OCR_LATIN_RECOGNIZER_1,
modelType: "ocr",
modelConfig: {
langList: ["en"],
useGPU: true,
timeout: 30000,
},
});
}
async extractText(imagePath: string): Promise<string> {
await this.ensureOcrReady();
const { blocks } = ocr({
modelId: required(this.ocrModelId, "QVAC OCR model is not loaded."),
image: imagePath,
options: { paragraph: false },
});
const result = await blocks;
return result
.map((block) => block.text)
.filter((line) => line.trim().length > 0)
.join("\n");
}
}Mismo patrón que el resto del gateway: carga lazy, compartir una
promesa en vuelo vía el helper required, exponer un método por
tarea. El modelo de OCR es independiente de chat y embedding, así
que su carga no bloquea el resto del pipeline.
2. Ramificar por extensión al leer
Actualiza discoverTextDocuments (y browserDocumentsFromInput)
para que los archivos con forma de imagen pasen por OCR antes de
convertirse en un LocalDocument. La forma de LocalDocument se
queda igual. Solo cambia la fuente del contenido.
import { QvacGateway } from "./qvac.ts";
const ocrExtensions = new Set([".bmp", ".jpg", ".jpeg", ".png", ".tiff"]);
export async function discoverTextDocuments(
rootPath: string,
gateway: QvacGateway,
): Promise<LocalDocument[]> {
// … existing folder walk …
const ext = path.posix.extname(absolutePath).toLowerCase();
let content: string;
if (ocrExtensions.has(ext)) {
content = await gateway.extractText(absolutePath);
} else {
content = await readFile(absolutePath, "utf8").catch(() => "");
}
// … rest unchanged …
}supportedExtensions crece con el set de OCR. Las reglas debajo
(sin null bytes, ≤2 MB, contenido no vacío) siguen filtrando
exactamente de la misma manera. Una imagen OCR'd sin texto
reconocido se salta, igual que un archivo de texto vacío.
3. Manejar PDFs página por página
OCR de QVAC toma una imagen, no un PDF. Convierte cada página del
PDF a una imagen primero — cualquier rasterizador funciona
(pdftoppm, pdf-poppler, o un binding de Node) — después llama
a gateway.extractText por página y une los resultados:
async function extractPdfText(filePath: string, gateway: QvacGateway): Promise<string> {
const pageImages = await rasterisePdfToTempImages(filePath); // [path1.png, path2.png, …]
try {
const pages = await Promise.all(
pageImages.map((img) => gateway.extractText(img)),
);
return pages.map((text, i) => `--- page ${i + 1} ---\n${text}`).join("\n\n");
} finally {
await Promise.all(pageImages.map((img) => unlink(img).catch(() => undefined)));
}
}Los marcadores de página en el texto unido ayudan a que el chunker mantenga visibles las fronteras de página en las citas después.
4. Plomar el gateway por el workflow
LocalLensApp ya posee un QvacGateway. Pásalo hacia abajo a
discoverTextDocuments para que el adaptador de archivos pueda
llamar a extractText sin necesitar saber del lifecycle del modelo:
async createBrainFromFolder(input: CreateBrainFromFolderInput): Promise<Brain> {
const folderPath = path.resolve(input.folderPath);
const documents = await discoverTextDocuments(folderPath, this.qvac);
return this.createBrainFromLocalDocuments(input.name.trim(), folderPath, documents);
}Esa es la única firma que cambia. El resto del workflow — chunking,
ingestión, el store JSON — opera sobre LocalDocument[] exactamente
como antes.
Lo que no necesitas cambiar
rag.ts— el chunking no cambia. Toma unLocalDocumenty no le importa si el contenido vino de texto en disco o de OCR.store.ts— la forma del JSON es la misma.domain.ts—LocalDocumentya tienerelativePath,content,checksumybytes. Eso es todo lo que el resto del pipeline necesita.
Ese es el payoff de la costura del adaptador de archivos: un formato nuevo es un método del gateway más una rama en el walker de archivos.
Desmonta OCR cuando termines de indexar
El modelo de OCR puede quedarse cargado durante el lifetime del
gateway. Si quieres liberar su memoria después de un run one-shot
de la CLI, llama
await unloadModel({ modelId: this.ocrModelId, clearStorage: false })
dentro de QvacGateway.close() antes de close().
Referencias externas
- Referencia de OCR de QVAC
- Modelo
OCR_LATIN_RECOGNIZER_1— opciones de lenguaje y recognizers adicionales.