LocalLens
拡張

QVAC OCR で PDF と画像の解析を追加する

QVAC OCR タスクを再利用して、スキャンされたページや画像を LocalDocument に変える。

置く場所: OCR 呼び出しは src/qvac.ts、アダプタのつなぎ目は src/files.ts。パイプラインの残りは知らなくて構いません。

LocalLens は現在、プレーンテキスト形式(Markdown、ソースコード、JSON、 YAML)をインデックスします。画像やスキャンされた PDF を同じ brain に 取り込みたいときに、サードパーティのパーサーは不要です。QVAC SDK には OCR タスクが含まれており、チャットモデルや埋め込みモデルの隣に配線 できます。

公式の QVAC OCR リファレンスは docs.qvac.tether.io/sdk/examples/ai-tasks/ocr にあります。以下のレシピはそれを開いた状態で進める想定です。

QVAC OCR の表面

import {
  loadModel,
  ocr,
  unloadModel,
  OCR_LATIN_RECOGNIZER_1,
} from "@qvac/sdk";

1 つのモデル(OCR_LATIN_RECOGNIZER_1)が認識器を駆動します。 ocr({ modelId, image, options })blocks Promise を返し、{ text, bbox?, confidence? } のオブジェクト配列で解決されます。

4 ステップのレシピ

1. ゲートウェイに OCR メソッドを追加する

QvacGateway を拡張し、OCR モデルをチャット/埋め込みモデルと並んで 遅延読み込みし、extractText 1 つのヘルパーを公開します。

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");
  }
}

ゲートウェイの他の部分と同じパターンです。遅延読み込み、required ヘルパー経由で進行中の Promise を共有、タスクごとに 1 メソッド。OCR モデルはチャットと埋め込みから独立しているので、その読み込みが パイプラインの残りをブロックしません。

2. 読み込み時に拡張子で分岐する

discoverTextDocuments(と browserDocumentsFromInput)を更新して、 画像形式のファイルは LocalDocument になる前に OCR を通すようにします。 LocalDocument の形は変わりません。コンテンツの出所だけが変わります。

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 に OCR 用のセットが加わります。下にあるルール (null バイトなし、2 MB 以下、非空のコンテンツ)はそのまま同じように フィルタします。認識テキストが空の OCR 画像は、空のテキストファイルと 同様にスキップされます。

3. PDF はページ単位で処理する

QVAC OCR は PDF ではなく画像を受け取ります。先に各 PDF ページを画像に 変換してください。ラスタライザは何でも構いません(pdftoppmpdf-poppler、または Node バインディング)。そのあと、ページごとに gateway.extractText を呼んで結果を結合します。

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)));
  }
}

結合テキスト内のページマーカーは、後でチャンク化されたときにページ境界 を引用の中で見えるようにしておくのに役立ちます。

4. ワークフローからゲートウェイを渡す

LocalLensApp はすでに QvacGateway を持っています。それを discoverTextDocuments に渡し、ファイルアダプタがモデルライフサイクルを 知らずに extractText を呼べるようにします。

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);
}

変わるシグネチャはこれだけです。残りのワークフロー(チャンク化、 取り込み、JSON ストア)は、LocalDocument[] を相手にこれまでどおり動きます。

変えなくて良いもの

  • rag.ts — チャンク化は変わりません。LocalDocument を受け取るだけで、 内容がディスクテキスト由来か OCR 由来かは気にしません。
  • store.ts — JSON の形は同じです。
  • domain.tsLocalDocument はすでに relativePathcontentchecksumbytes を持ちます。パイプラインの残りが必要とするのは それで全部です。

これがファイルアダプタのつなぎ目を持っていたことの効用です。新しい形式 への対応は、ゲートウェイメソッド 1 つとファイルウォーカー内の分岐 1 つで 済みます。

インデックスが終わったら OCR を片付ける

OCR モデルはゲートウェイのライフサイクルが終わるまで読み込んだままに できます。CLI のワンショット実行のあとにメモリを解放したい場合は、 QvacGateway.close() 内、close() の前に await unloadModel({ modelId: this.ocrModelId, clearStorage: false }) を呼んでください。

外部リファレンス

On this page