yikegaya’s blog

仕事関連(Webエンジニア)について書いてます

TypeScriptからGCP VisionAIを実行して文字や段落の座標を取得してみた

業務でOCR(画像の文字を読み取ってテキストデータに変換する技術)を使った機能実装を担当することになりGCPのVisionAIにキャッチアップしてみました。

実際の画像データに対してts-nodeで実行できるTypeScriptのスクリプトを用意してレスポンスをファイルに書き出しVisionAIが返却するデータ構造を把握してみます。

今回の要件 / 前提

VisionAIは画像の顔認識やラベル付など様々な用途がありますが今回は業務用PDFファイルのテキストデータを活用する機能要件なのでPDFの資料からテキストデータと座標の情報を取得することを目的としています。

スクリプト実行の事前準備

GCPの認証には今回gcloud authコマンド実行後に出力される認証情報(Application Default Credentials)を環境変数に設定して認証を通します。

gcloud auth application-default login

このコマンド実行後に$HOME/.config/gcloud/application_default_credentials.jsonに出力されるファイルパスをGOOGLE_APPLICATION_CREDENTIALSに設定します。

またVisionAIの実行には請求先プロジェクトの設定も必要になるので以下のコマンドで設定します。

gcloud auth application-default set-quota-project PROJECT_ID

必要なパッケージのインストール

visionAIと型定義パッケージをインストールします。検証用にnpmプロジェクトを作ってもグローバルインストールしてもどっちでも大丈夫です。

npm install @google-cloud/vision
npm install --save-dev typescript @types/node

スクリプトの実装

Cursorに出力させたものに少し手を入れます。

import { ImageAnnotatorClient } from '@google-cloud/vision';
import { Storage } from '@google-cloud/storage';
import fs from 'fs';
import path from 'path';

const PROJECT_ID = 'project-name'; // 実際のプロジェクト名に書き換え
const KEY_FILE = process.env.GOOGLE_APPLICATION_CREDENTIALS!;
const BUCKET_NAME = 'bucketname'; // 実際のバケット名に書き換え
const GCS_FILE_NAME = 'sample.pdf'; // 実際のファイル名に書き換え
const OUTPUT_PREFIX = 'visionai-output/';
const OUTPUT_PATH = './vision-result.json';

// Vision API & Storage クライアント
const visionClient = new ImageAnnotatorClient({ projectId: PROJECT_ID, keyFilename: KEY_FILE });
const storage = new Storage({ projectId: PROJECT_ID, keyFilename: KEY_FILE });

async function main() {
  const gcsUri = `gs://${BUCKET_NAME}/${GCS_FILE_NAME}`;
  const request = {
    requests: [
      {
        inputConfig: {
          mimeType: 'application/pdf',
          gcsSource: { uri: gcsUri }
        },
        features: [{ type: 'DOCUMENT_TEXT_DETECTION' }],
        outputConfig: {
          gcsDestination: {
            uri: `gs://${BUCKET_NAME}/${OUTPUT_PREFIX}`
          }
        }
      }
    ]
  };

  const [operation] = await visionClient.asyncBatchAnnotateFiles(request);
  const [filesResponse] = await operation.promise();

  // 最初の結果のURIを取得
  const uri = filesResponse.responses?.[0]?.outputConfig?.gcsDestination?.uri!;
  console.log('✅ Vision API result saved to:', uri);

  // 結果ファイル(JSON)をダウンロード
  const [files] = await storage.bucket(BUCKET_NAME).getFiles({ prefix: OUTPUT_PREFIX });
  const jsonFile = files.find(f => f.name.endsWith('.json'));
  const [buffer] = await jsonFile!.download();
  fs.writeFileSync(OUTPUT_PATH, buffer.toString());
  console.log(`📄 Saved result to ${OUTPUT_PATH}`);
}

main();

レスポンス構造の整理

実行するとファイルに以下のように大量のページ、テキストやブロックごとの座標情報が出力されます。

{"fullTextAnnotation":{"pages":[{"property":{"detectedLanguages":[{"languageCode":"ja","confidence":0.5580352},{"languageCode":"en","confidence":0.09062166},{"languageCode":"zh","confidence":0.033959746},{"languageCode":"it","confidence":0.0070620067},{"languageCode":"ro","confidence":0.0041061095}]},"width":595,"height":841,"blocks":[{"boundingBox":{"normalizedVertices":[{"x":0.028571429,"y":0.016646849},{"x":0.4487395,"y":0.016646849},{"x":0.4487395,"y":0.047562424},{"x":0.028571429,"y":0.047562424}]},"paragraphs":[{"boundingBox":{"normalizedVertices":[{"x":0.03529412,"y":0.016646849},{"x":0.4487395,"y":0.016646849},{"x":0.4487395,"y":0.032104637},{"x":0.03529412,"y":0.032104637}]},"words":[{"property":{"detectedLanguages":

AIに解析させて図示すると以下のような構造になっているようです。

VisionAIResult
└── responses[]                         ← 一括解析などで複数ページの結果がある場合に配列
    └── fullTextAnnotation
        └── pages[]                    ← 各ページ(PDFページや画像1枚ごと)
            └── blocks[]              ← 論理的なテキストブロック(例:段落、列)
                └── paragraphs[]      ← ブロック内の段落
                    └── words[]       ← 段落内の単語
                        └── symbols[] ← 単語を構成する個々の文字(ひらがな・漢字など)

VisionAIの実行方法とレスポンス構造についてある程度把握ができました。終わり