LocalLens
Estender

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:

src/qvac.ts
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.

src/files.ts
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:

src/locallens.ts
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 um LocalDocument e não se importa se o conteúdo veio de texto em disco ou OCR.
  • store.ts — o formato JSON é o mesmo.
  • domain.tsLocalDocument já tem relativePath, content, checksum e bytes. É 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

On this page