0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

ミニチュア版Gemini NoteBookの構築(v0.2)——画面からソースを追加・削除する

0
Last updated at Posted at 2026-08-28

この記事でできること

前回組んだ RAG アプリに、ブラウザ画面からのソース追加(Markdown と PDF)と、1本ずつの削除機能を足せるようになります

前回(v0.1)は、レンタルGPUサーバーの上に「日本語で質問すると出典つきで答えが返るアプリ」——NotebookLM のミニチュア版——を部品5つ(画面の HTML/API サーバの FastAPI/ベクトル化役の bge-m3/ベクトルDB の Chroma/回答役の qwen3:8b)で組みました。ただしソースを入れる手段は、ターミナルからのスクリプト実行だけ。読み込めるのも Markdown だけでした。今回はロードマップに予告したとおり、ここを画面のボタンに置き換え、PDF も読めるようにします。前回より短めの、続編の記事です。

  • 対象読者:前回の記事を読んだ方/RAG アプリの「ソースの追加・削除」をどう作るか知りたい方
  • 今回増えた部品:PDF の文字取り出しライブラリ(pypdf)の1つだけ。構成(FastAPI・Chroma・Ollama)は前回のまま
  • コスト:実機確認まるごとで GPU 代 約0.52ドル

v0.2 で足したもの

画面の左パネルに「+資料を入れる」ボタンと、ソースごとの「消す」ボタンが付きました。中身はこの4つです。
スクリーンショット 2026-08-28 21.18.49.png

追加・変更 中身
ソースの追加 ファイル選択(.md / .pdf・複数可)。サーバが受けて、分割 → ベクトル化 → Chroma 保存まで自動
ソースの削除 各行の「消す」。押すと行の中に「本当に消す/やめる」の2ボタンが出る
PDF 読み込み pypdf で文字を取り出し、あとは Markdown と同じ流れに合流
原本を保存しない v0.1 はソースの原本もサーバに置いていた。v0.2 は検索用の切れはしだけを持ち、原本は手元の Mac にだけ残す

サーバ側の API は2本増えただけです。まず追加のほう。中身を要約するとこれだけで、新しく書いたのは「PDF なら文字を取り出す」の分岐だけです。分割 → ベクトル化 → Chroma 保存は、v0.1 でスクリプト投入用に作った ingest_document() にファイル名ごと渡して使い回します(このファイル名が、切れはしの source メタデータになります)。

@app.post("/api/upload")
async def upload(file: UploadFile):
    data = await file.read()
    if file.filename.endswith(".pdf"):
        text = rag.extract_pdf_text(data)      # pypdf での文字取り出し(関数1つに分離)
    else:
        text = data.decode("utf-8")
    n = rag.ingest_document(file.filename, text, settings)  # 分割→ベクトル化→Chroma保存(v0.1 と同じ)
    return {"chunks": n}

(実物にはこのほかに拡張子チェック・サイズ上限・エラー時のメッセージ分岐が入ります)

次に削除。こちらの中身は、実は1行です。v0.1 で切れはし1つ1つに「どのソースから来たか」をメタデータとして持たせてあったのが、ここで効きました。

col.delete(where={"source": filename})

画面側の「本当に消す/やめる」は、行の中にボタンを2つ差し込むだけの素朴な作りです。

del.addEventListener("click", () => {
  del.style.display = "none";                       // 「消す」を隠して
  const yes = button("本当に消す"), no = button("やめる");  // 2ボタンを同じ行に出す
  no.addEventListener("click", () => { yes.remove(); no.remove(); del.style.display = ""; });
  yes.addEventListener("click", async () => {
    await fetch("/api/sources/" + encodeURIComponent(name), { method: "DELETE" });
    loadSources();
  });
});

1つだけ引っかかる箇所があります。ソース名にスラッシュが入る(フォルダ名つきで登録した)場合、/api/sources/趣味/メモ.md の「/」が URL の区切りと衝突して 404 になります。直すのは2箇所です。画面側の encodeURIComponent が「/」を「%2F」に変えるので、名前の中身が区切りと混ざらずに届きます。サーバ側はパスパラメータを {source_name:path} にします——FastAPI の :path は「スラッシュが入っていても、そこから先を丸ごと1つの値として受け取る」型です。

つまずいた話(3つ)

1. 画面をファイルとして開くと「サーバに繋がらない」

動作確認のとき、手元の index.html をダブルクリックで開いてしまい、「サーバに繋がりませんでした」の表示に15分悩みました。サーバは正常。原因は開き方です。画面の JavaScript は fetch("/api/ask") のように相対パスで API を呼ぶので、file:// で開くと飛び先がありません。この画面は必ずサーバの URL(RunPod なら proxy URL)から開く必要があります。分かってしまえば当たり前ですが、症状が「サーバ側の故障」にしか見えないのが厄介でした。

2. 「本当に消しますか?」に confirm() を使うのをやめた

削除の確認は、最初はブラウザ標準の confirm() で済ませるつもりでした。やめた理由は、あのダイアログが画面全体を止めるからです。人が押す分には困りませんが、ブラウザの自動操作で動作確認をするとき、ダイアログが出た瞬間に操作が返せなくなります。確認は行の中に「本当に消す/やめる」の2ボタンを出す方式にしました。結果として、本家 NotebookLM の削除の見た目にも近づきました。

3. PDF ライブラリはライセンスで選んだ

文字の取り出し精度なら PyMuPDF が有名です。でも選んだのは pypdf でした。PyMuPDF のライセンスは AGPL——組み込んで公開するコード全体に、同じライセンスの適用を求める——で、コードを GitHub で公開する予定と合わないためです。pypdf は BSD ライセンスなので公開の妨げになりません。精度が足りなかったときのために、文字取り出しは関数1つにまとめてあり、日本語 PDF で取りこぼしが出たら pdfminer.six に1箇所だけ差し替えれば済む作りにしました。なお、画像だけの PDF(スキャンした紙)は文字を取り出せないので v0.2 では読めません。

コストと結果

  • pytest は36件すべて通過(v0.1 の20件に、v0.2 で16件追加)
  • 実機確認は RTX 4090(0.34ドル/h)で約1.5時間、GPU 代 約0.52ドル
  • 決めておいた合格条件5つ——画面から追加できる・1本ずつ消せる・PDF を読める・v0.1 の機能が生きている・次バージョンの機能を先取りしていない——を実機で全部確認しました

次にやること

  • v0.3:メモ機能(画面右に3枚目のパネル)/利用者ごとにソースを分ける
  • その先の候補:ソースの全削除ボタン/Web ページの取り込み/音声概要

v0.1 の記事で「ソースは各自が自分で登録する形にするのが目標」と書きました。画面から入れられるようになった今回は、その最初の一歩です。ここから先は1バージョンずつ、NotebookLM に近づけていきます。

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?