Adicionar parsing de PDF e imagem com QVAC OCR
Reutilize a tarefa de OCR do QVAC para transformar páginas escaneadas e imagens em LocalDocuments.
Onde ele pertence: src/qvac.ts para a chamada de OCR, src/files.ts
para a cola do adaptador. O resto do pipeline não precisa
saber.
O LocalLens hoje indexa formatos de texto puro: Markdown, código fonte, JSON, YAML. Para trazer imagens e PDFs escaneados para o mesmo brain, você não precisa de um parser de terceiro. O QVAC SDK entrega uma tarefa de OCR que você pode conectar ao lado dos modelos de chat e embedding.
A referência oficial de OCR do QVAC está em
docs.qvac.tether.io/sdk/examples/ai-tasks/ocr.
A receita abaixo assume que você está com ela aberta.
A superfície de OCR do QVAC
import {
loadModel,
ocr,
unloadModel,
OCR_LATIN_RECOGNIZER_1,
} from "@qvac/sdk";Um único modelo (OCR_LATIN_RECOGNIZER_1) dirige o recognizer.
ocr({ modelId, image, options }) retorna uma promise blocks que
resolve para um array de objetos { text, bbox?, confidence? }.
A receita em quatro passos
1. Adiciona um método de OCR ao gateway
Estende QvacGateway para que ele carregue o modelo de OCR de forma lazy,
ao lado dos modelos de chat e embedding, e exponha um único 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");
}
}Mesmo padrão do resto do gateway: load lazy, compartilha uma
promise em voo via o helper required, expõe um método por
tarefa. O modelo de OCR é independente de chat e embedding, então o
load dele não bloqueia o resto do pipeline.
2. Ramifica por extensão na leitura
Atualiza discoverTextDocuments (e browserDocumentsFromInput)
para que arquivos em formato de imagem passem por OCR antes de virar
um LocalDocument. O formato LocalDocument continua o mesmo. Só a
fonte do conteúdo muda.
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 cresce pelo conjunto de OCR. As regras abaixo
(sem null bytes, ≤2 MB, conteúdo não vazio) mantêm a filtragem exatamente
do mesmo jeito. Uma imagem com OCR sem texto reconhecido é pulada,
do mesmo jeito que um arquivo de texto vazio.
3. Trata PDFs página a página
QVAC OCR recebe uma imagem, não um PDF. Converte cada página de PDF para
imagem antes — qualquer rasterizador funciona (pdftoppm, pdf-poppler, ou
um binding Node) — depois chama gateway.extractText por página e
junta os 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)));
}
}Marcadores de página no texto juntado ajudam o chunker a manter as fronteiras de página visíveis em citações depois.
4. Encana o gateway pelo workflow
LocalLensApp já possui um QvacGateway. Passa para baixo para
discoverTextDocuments para que o adaptador de arquivos possa chamar extractText
sem precisar saber sobre ciclo de vida de 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);
}Essa é a única assinatura que muda. O resto do workflow
— chunking, ingest, JSON store — opera sobre
LocalDocument[] exatamente como antes.
O que você não precisa mudar
rag.ts— chunking continua igual. Recebe umLocalDocumente não se importa se o conteúdo veio de texto em disco ou OCR.store.ts— o formato JSON é o mesmo.domain.ts—LocalDocumentjá temrelativePath,content,checksumebytes. É tudo que o resto do pipeline precisa.
Esse é o retorno da costura do adaptador de arquivos: um formato novo é um método de gateway mais um branch no walker de arquivos.
Desmonta OCR quando terminar de indexar
O modelo de OCR pode ficar carregado pelo tempo de vida do gateway.
Se você quer liberar a memória depois de uma execução one-shot da CLI, chama
await unloadModel({ modelId: this.ocrModelId, clearStorage: false })
dentro de QvacGateway.close() antes de close().
Referências externas
- Referência QVAC OCR
- Modelo
OCR_LATIN_RECOGNIZER_1— opções de idioma e recognizers adicionais.