すくらっぷ あんど びるどー(したい)

日々やった事のメモとかまとめ。

AIに書籍管理を頼んだ。疲労困憊させた。

AIに司書の役割を持たせたかった。 やりたかったことは

  • PC内部のPDFデータを解析して一覧にする
  • 書籍のサーチをさせたい。
  • それを見やすくしたい

のでそれをたのんだ。

とりあえずAIに出来るかどうかを聞く

出来そう。 なので今度は環境を構築する

🔑 ステップ1:APIキーの取得(Google AI Studio)

  1. Google AI Studio にアクセスし、Googleアカウントでログインします。
  2. サイドメニューの 「Get API key」 をクリック。
  3. 「Create API key in new project」 を選択し、発行されたキー(AIza...で始まる文字列)をコピーして、メモ帳などに控えておいてください。

    ⚠️ 注意: このキーはパスワードと同じです。GitHubなどにそのまま公開しないよう注意してください。


💻 ステップ2:開発環境のセットアップ(Node.js)

PCの作業フォルダ(例:inspiration-app)を準備し、ターミナルで以下のコマンドを順に実行してください。

プロジェクトの初期化
npm init -y

必要なライブラリのインストール
@google/generative-ai: Gemini公式SDK
dotenv: APIキーを隠して管理するため
npm install @google/generative-ai dotenv

📁 ステップ3:プロジェクトのファイル構成

フォルダの中身を以下のように整えます。

inspiration-app/
├── .env                # APIキーを記述(重要!)
├── index.js            # メインのプログラム
├── package.json
└── sample-image.jpg    # テスト用の書籍画像

.env ファイルの作成

メモしたAPIキーをここに書き込みます。

GEMINI_API_KEY=あなたのAPIキーをここに貼り付け

index.js の記述

先ほどのコードを、実際に動かせる形に整えたものです。

require('dotenv').config();
const { GoogleGenerativeAI } = require("@google/generative-ai");
const fs = require("fs");

const genAI = new GoogleGenerativeAI(process.env.GEMINI_API_KEY);

async function run() {
  const model = genAI.getGenerativeModel({ model: "gemini-1.5-flash" });

  const prompt = "この書籍から、イラストの色彩・構図・雰囲気のヒントを抽出し、JSONで答えて。";

  // 画像の読み込み
  const imagePart = {
    inlineData: {
      data: Buffer.from(fs.readFileSync("sample-image.jpg")).toString("base64"),
      mimeType: "image/jpeg",
    },
  };

  console.log("🎨 インスピレーションを抽出中...");
  const result = await model.generateContent([prompt, imagePart]);
  console.log(result.response.text());
}

run();

このただこの時点では別のを作ろうとしていた。使用したのはこのファイル構成とライブラリぐらい。
それも途中でこけた。

こけまくる

おのれ……!
このerrorがなんどもなってしまいには色々やった結果別のAIにきいたりしていた。なんかその時に判断としてはAPIキーが古いのではとか言われた。

このあたりからやってる事が詰まってきている。別の奴にきこう。ってなってCopilotにいったんきく。

色々やった結果として

  • APIキーの問題ではなさそう。
  • モデルが使えるかどうかが通るかどうかっぽい。

という事がわかった。
そのためまずは何が使えるかを調べた。

モデルの確認をする

コードを出してもらう

const { GoogleGenerativeAI } = require("@google/generative-ai");
const API_KEY = "あなたのAPIキー";
const genAI = new GoogleGenerativeAI(API_KEY);

async function checkModels() {
    try {
        // 全てのモデルをリストアップする
        // ※この機能は v1beta でのみ動作することが多いです
        const response = await fetch(`https://generativelanguage.googleapis.com/v1beta/models?key=${API_KEY}`);
        const data = await response.json();
        
        console.log("📋 あなたのキーで利用可能なモデル一覧:");
        data.models.forEach(m => console.log(`- ${m.name.replace('models/', '')}`));
        
    } catch (e) {
        console.error("確認に失敗しました。キーが正しいか、インターネット接続を確認してください。");
    }
}
checkModels();

これで出たのかこれ。

Node.js v25.5.0
PS D:\inspiration-app> node index.js
📋 あなたのキーで利用可能なモデル一覧:
- gemini-2.5-flash
- gemini-2.5-pro
- gemini-2.0-flash
- gemini-2.0-flash-001
- gemini-2.0-flash-exp-image-generation
- gemini-2.0-flash-lite-001
- gemini-2.0-flash-lite
- gemini-exp-1206
- gemini-2.5-flash-preview-tts
- gemini-2.5-pro-preview-tts
- gemma-3-1b-it
- gemma-3-4b-it
- gemma-3-12b-it
- gemma-3-27b-it
- gemma-3n-e4b-it
- gemma-3n-e2b-it
- gemini-flash-latest
- gemini-flash-lite-latest
- gemini-pro-latest
- gemini-2.5-flash-lite
- gemini-2.5-flash-image
- gemini-2.5-flash-preview-09-2025
- gemini-2.5-flash-lite-preview-09-2025
- gemini-3-pro-preview
- gemini-3-flash-preview
- gemini-3-pro-image-preview
- nano-banana-pro-preview
- gemini-robotics-er-1.5-preview
- gemini-2.5-computer-use-preview-10-2025
- deep-research-pro-preview-12-2025
- gemini-embedding-001
- aqa
- imagen-4.0-generate-preview-06-06
- imagen-4.0-ultra-generate-preview-06-06
- imagen-4.0-generate-001
- imagen-4.0-ultra-generate-001
- imagen-4.0-fast-generate-001
- veo-2.0-generate-001
- veo-3.0-generate-001
- veo-3.0-fast-generate-001
- veo-3.1-generate-preview
- veo-3.1-fast-generate-preview
- gemini-2.5-flash-native-audio-latest
- gemini-2.5-flash-native-audio-preview-09-2025
- gemini-2.5-flash-native-audio-preview-12-2025
PS D:\inspiration-app> 

はいってないやんけ! そらこける。無駄な徒労だった。

正式なコードを書いてもらう。

const { GoogleGenerativeAI } = require("@google/generative-ai");
const fs = require("fs");
const path = require("path");
const pdf = require("pdf-poppler");

// --- 設定 ---
const API_KEY = "あなたのAPIキー"; 
const MODEL_NAME = "gemini-2.0-flash"; // 先ほど確認した利用可能モデル
const DB_FILE = "book_catalog.json";
const TARGET_DIR = process.argv[2] || "./"; 
const genAI = new GoogleGenerativeAI(API_KEY);

async function main() {
    try {
        const model = genAI.getGenerativeModel({ model: MODEL_NAME }, { apiVersion: "v1beta" });
        const targets = fs.readdirSync(TARGET_DIR).filter(f => /\.(pdf|jpg|jpeg|png)$/i.test(f));
        let catalog = fs.existsSync(DB_FILE) ? JSON.parse(fs.readFileSync(DB_FILE, "utf-8")) : [];

        console.log(`📑 目録作成開始: ${targets.length} 件`);

        for (let file of targets) {
            if (catalog.some(item => item.source_file === file)) continue;

            console.log(`🔍 調査中: ${file}`);
            const fullPath = path.join(TARGET_DIR, file);

            try {
                let bookData;
                if (file.toLowerCase().endsWith(".pdf")) {
                    bookData = await getInfoFromPdf(model, fullPath);
                } else {
                    bookData = await getInfoFromImage(model, fullPath);
                }

                catalog.push({
                    scanned_at: new Date().toISOString(),
                    source_file: file,
                    ...bookData
                });

                // 1件ごとに即座に保存
                fs.writeFileSync(DB_FILE, JSON.stringify(catalog, null, 2, "utf-8"));
                console.log(`   ✅ 登録完了: [${bookData.publisher}] ${bookData.title}`);

                await new Promise(res => setTimeout(res, 3000)); // API負荷軽減
            } catch (e) {
                console.error(`   ❌ 失敗: ${file} / ${e.message}`);
            }
        }
    } catch (e) { console.error(e); }
}

async function getInfoFromPdf(model, pdfPath) {
    const tempDir = path.join(__dirname, "temp_work");
    if (!fs.existsSync(tempDir)) fs.mkdirSync(tempDir);
    
    // 日本語パス対策
    const tempPdf = path.join(__dirname, "catalog_work.pdf");
    fs.copyFileSync(pdfPath, tempPdf);

    // 冒頭3ページだけを一時的に画像化
    await pdf.convert(tempPdf, {
        format: "jpeg", out_dir: tempDir, out_prefix: "top", scale: 1024, stop_at: 3
    });

    const pages = fs.readdirSync(tempDir).filter(f => f.startsWith("top-"));
    const imageParts = pages.map(p => ({
        inlineData: { data: fs.readFileSync(path.join(tempDir, p)).toString("base64"), mimeType: "image/jpeg" }
    }));

    const prompt = "Analyze these pages and output book metadata in Japanese JSON: {'title':'','author':'','publisher':'','genre':'','isbn':''}";
    const result = await model.generateContent([prompt, ...imageParts]);

    // 作業用ファイルの即時削除
    pages.forEach(p => fs.unlinkSync(path.join(tempDir, p)));
    fs.unlinkSync(tempPdf);

    return JSON.parse(result.response.text().replace(/```json|```/g, "").trim());
}

async function getInfoFromImage(model, imgPath) {
    const fileData = { inlineData: { data: fs.readFileSync(imgPath).toString("base64"), mimeType: "image/jpeg" } };
    const prompt = "Extract book metadata from this image in Japanese JSON: {'title':'','author':'','publisher':'','genre':''}";
    const result = await model.generateContent([prompt, fileData]);
    return JSON.parse(result.response.text().replace(/```json|```/g, "").trim());
}

main();

とりあえずこれで基本的な作品名、著者、出版社が取り出せるようになった。
本当は中身の解析もしておもったんだけど画像に変換してドキュメントにしてとかやろうとしてあかんかたからやめた。
そしたら今度は429 Too Many Requests(クォータ制限)になってしまった。やめどきか。

まとめ

色々やってとりあえず目録はできそう。あとはこれをスプレッドに入れれば良さそう。
問題は中身の解析なんだけど――本当はサーチをしてそこからAIに該当しそうな書籍をピックアップさせたかった――無理そうだからちょっとあきらめよう。
前に画像にしてドキュメント型式でとかいうのは何かで読んだけど、Googleドライブに入れるのは怖いっす。アカウントBUNが匙加減で変わるし。また何か思いついたらやるとする。
もう少し何か考えます。