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?

Gemma 4 を "自分専用のAIチャット" にしてみた 〜Ollama + FastAPI で作るブラウザUI〜

0
Posted at

先に結論

  • 自分のPCで動かす Gemma 4(Ollama)FastAPIの小さな窓口を付けて、ターミナルもVS Codeも無しに、ブラウザから使えるようにした
  • 使う人は VS Codeもコマンドも不要。ブラウザでURLを開くだけ
  • 入力内容はPCの外に出ないので、クラウドAIに貼りづらい社内の話も安心して使える
  • 最小構成から始めて、チャット形式のUI・Markdown整形・ファイル添付・会話履歴のフォルダ保存まで育てた過程も紹介します
  • 「ローカルLLMって外部からアクセスされないの?」というセキュリティの疑問にも、仕組みから答えます
  • 想定は 各自が自分のPCに入れて使う 形。1台を大勢で共有するのは無理(後述:1回の生成でCPUを半分ほど使うため)なので、そこも正直に書きます

きっかけ:VS Codeで使えた。でも周りは?

前回の記事で、VS Code + Ollama を使って Gemma 4 をローカルで動かし、Apexコードを書かせて検証しました。ローカル実行なので入力したコードが外部に送信されないのが大きな利点です。

ただ、この方法には壁があります。使うのにVS Codeが必要なこと。エンジニアはいいですが、営業や企画の人に「まずVS Codeを入れて、拡張機能を設定して……」とは言えません。

そこで思いつくのがこれです。

ブラウザからアクセスできる形にすれば、誰でも使えるのでは?

やってみたら、意外とあっさりできました。


仕組み:Ollamaの前に「窓口」を1枚立てるだけ

VS Code拡張がやっていたことは、突き詰めると 「Ollamaに質問を投げて、答えを受け取る」 だけです。であれば、その窓口をブラウザ向けに作ればいい。今回は FastAPI(Pythonの軽量Webフレームワーク)で作ります。

┌──────────────┐
│  Ollama      │  127.0.0.1:11434(同じPC内)
│  (常駐)       │
└──────┬───────┘
       │ HTTP API(すべて自分のPC内で完結)
┌──────┴───────┐
│  FastAPI     │  127.0.0.1:8000 ← ブラウザ向けの窓口
│  (app.py)    │  Markdown整形・履歴保存などを担う
└──────┬───────┘
       │ ブラウザ
┌──────┴───────┐
│  自分のブラウザ │  ターミナル/VS Code 不要
└──────────────┘

全部が1台のPCの中で完結します。これを各自のPCに入れて、それぞれ自分専用のローカルAIとして使う、というのが基本の形です。

Ollama そのものは信頼していいのか

土台に使う Ollama は、Google製ではなく サードパーティ(Ollama Inc.) のオープンソースソフト(MITライセンス)です。ただし広く使われている定番ツールで、Googleの公式ドキュメントにも Ollama 連携ページがあるくらいなので、素性としては問題ありません。注意点は入手先だけで、必ず 公式サイト ollama.com から入れること(「ollama 無料ダウンロード」等で出てくる非公式サイトは踏まない)。

なぜ Ollama を直接ブラウザに出さないのか

FastAPIを挟まず「Ollamaを直接ブラウザから叩けばいいのでは?」と思うかもしれませんが、これは避けたほうが無難です。Ollamaはそのままネットワークに出すと、無認証・無制限で叩ける状態になります。間にFastAPIを一枚挟んでおけば、認証を足す・ログを取る・整形するといった余地を常に自分の手元に残せます。Ollama自体は 127.0.0.1 のまま内側に隠しておくのが安全です。


作り方

ファイルは2つだけ

app.py(FastAPI本体。HTMLフォーム + Ollamaへの中継が1ファイルに収まっています)

"""社内向け Ollama チャットフォーム(FastAPI)
    ブラウザ ──> FastAPI(:8000) ──> Ollama(127.0.0.1:11434)
起動: uvicorn app:app --host 0.0.0.0 --port 8000
"""
import json
import os

import httpx
from fastapi import FastAPI, Form
from fastapi.responses import HTMLResponse, StreamingResponse

OLLAMA = os.environ.get("OLLAMA_URL", "http://127.0.0.1:11434")
MODEL = os.environ.get("OLLAMA_MODEL", "gemma4:12b")

app = FastAPI(title="社内AIチャット")

PAGE = """
<!doctype html><meta charset="utf-8"><title>社内AIチャット</title>
<style>body{font-family:sans-serif;max-width:760px;margin:40px auto;padding:0 16px}
textarea{width:100%;height:120px}#out{white-space:pre-wrap;border:1px solid #ccc;
padding:16px;margin-top:16px;min-height:120px;border-radius:8px}</style>
<h2>社内AIチャット(Gemma 4 / ローカル実行)</h2>
<form id="f"><textarea name="prompt" placeholder="質問を入力"></textarea>
<button>送信</button></form><div id="out"></div>
<script>
f.onsubmit = async e => {
  e.preventDefault(); out.textContent = "";
  const r = await fetch("/chat", {method:"POST", body:new FormData(f)});
  const rd = r.body.getReader(), dec = new TextDecoder();
  for(;;){ const {value,done} = await rd.read(); if(done) break;
           out.textContent += dec.decode(value); }
};
</script>
"""


@app.get("/", response_class=HTMLResponse)
def index():
    return PAGE


@app.post("/chat")
async def chat(prompt: str = Form(...)):
    async def gen():
        payload = {
            "model": MODEL,
            "messages": [{"role": "user", "content": prompt}],
            "stream": True,
        }
        async with httpx.AsyncClient(timeout=None) as c:
            async with c.stream("POST", f"{OLLAMA}/api/chat", json=payload) as r:
                async for line in r.aiter_lines():
                    if not line:
                        continue
                    d = json.loads(line)
                    if tok := d.get("message", {}).get("content"):
                        yield tok

    return StreamingResponse(gen(), media_type="text/plain; charset=utf-8")

やっていることはシンプルで、ブラウザの入力を受け取り → 同じPC内のOllamaに中継し → 返ってきた答えを1文字ずつブラウザに流す(ストリーミング)、それだけです。使うモデルは環境変数 OLLAMA_MODEL で切り替えられます(デフォルトは gemma4:12b)。

requirements.txt

fastapi
uvicorn[standard]
httpx
python-multipart

手順(Windows)

前提として、前回の導入手順ollama run gemma4:12b が動く状態になっていることとします。

1. Python環境を用意

作業フォルダで(※OneDriveの外に置くのがおすすめ。OneDrive内に .venv を作ると同期が走って重くなることがあります):

python -m venv .venv
.\.venv\Scripts\Activate.ps1
pip install -r requirements.txt

⚠️ 使っているシェルに注意。上記は PowerShell 用です。コマンドプロンプト(cmd) を使っている場合は、有効化コマンドが違います:

.venv\Scripts\activate.bat

Activate.ps1 はPowerShell専用なので、cmdで打つと「認識されていません」と怒られます。プロンプトの先頭に (.venv) が付けば成功です。

2. まず自分だけで動作確認

Ollamaが起動している(タスクトレイにラマのアイコン)ことを確認してから:

uvicorn app:app --host 127.0.0.1 --port 8000

ブラウザで http://127.0.0.1:8000 を開き、質問を入力して文字が流れてくれば成功です。127.0.0.1 は自分のPCからのみアクセス可能なので、この段階では他の人からは見えません。

3.(おまけ)自分のスマホなど、同じLANの別端末から使う

「自分のPCで動かしているものを、手元のスマホからも使いたい」という場合は公開モードで起動します。ただし後述の通り複数人で同時に使うのは無理なので、これは"自分の別端末から使う"程度の用途と考えてください。

自分のPCのIPアドレスを調べ(ipconfig の「IPv4 アドレス」、例 192.168.1.23)、公開モードで起動します:

uvicorn app:app --host 0.0.0.0 --port 8000

初回起動時に Windowsファイアウォールの確認ダイアログが出たら、「プライベート ネットワーク」だけにチェックして許可します(パブリックにはチェックを入れない)。

あとは別端末のブラウザで http://192.168.1.23:8000(控えたIPに読み替え)を開けば使えます。


社内に配るなら、最低限の「合言葉」を

今回の app.py には認証がありません。URLを知っている人は誰でも使える状態です。「うっかり誰かが開いてしまう」のを防ぐくらいの、簡単な合言葉を付けておくと安心です。chat() に数行足すだけです。

from fastapi import HTTPException

PASSWORD = os.environ.get("APP_PASSWORD", "")

@app.post("/chat")
async def chat(prompt: str = Form(...), password: str = Form("")):
    if PASSWORD and password != PASSWORD:
        raise HTTPException(status_code=401, detail="合言葉が違います")
    ...

HTML側のフォームに <input type="password" name="password" placeholder="合言葉"> を足し、起動時に合言葉を設定します:

$env:APP_PASSWORD = "好きな合言葉"
uvicorn app:app --host 0.0.0.0 --port 8000

※これはあくまで簡易的なもの。本格的な認証が必要なら、ユーザー管理付きの仕組みを別途検討してください。


使いにくかったので直していったら、Claude Code みたいになった

最小構成でもちゃんと動くのですが、自分で実際に使ってみると細かいストレスが溜まります。送信したのか分からない、コードがベタ書きで読みにくい、前の会話が消える、長文を貼ると入力欄が窮屈……。気になるところを潰していったら、結果的に Claude Code や ChatGPT で見慣れたUI に近づいていって、一気に使いやすくなりました。

スクリーンショット 2026-07-27 232342.png

最終的に入った主なもの:

  • チャット形式+Markdown整形:見出し・箇条書き・コードブロック(コピーボタン付き) を整形表示。回答はストリーミングで1文字ずつ流れる
  • 「考え中…」表示:送信直後にすぐ出す。初回のモデル読み込み(数十秒)で固まったように見えても安心
  • Claude風の入力欄:角丸のカード。左の「+」でファイル添付、右に丸い送信ボタン
  • ファイル添付:テキストファイル(.cls / .txt / .csv など)は中身を読んで質問に添える。「このApexのバグ見て」と .cls を添付、という使い方ができる
  • 会話履歴をローカルフォルダに保存:過去の会話をサイドバーで切り替え(後述)

見た目を見慣れたものに寄せた副産物として、操作に迷わなくなったのが大きいです。「+は添付」「新しい会話は左上」といった配置は、みんな普段から触っているので、説明しなくても伝わりました。

会話履歴を「PCのフォルダ」に貯める

履歴の保存先には2択あります。

  • ブラウザに保存(localStorage):手軽だが容量が小さく(5MB程度)、ブラウザごとにバラバラ
  • サーバー(このPC)のフォルダに保存:1会話=1個のJSONファイル。何本でも貯まり、どのブラウザからでも同じ履歴が見える ← 今回はこちら

サーバー側に保存用のエンドポイントを足すだけです。

from pathlib import Path
import time

CONV_DIR = Path(os.environ.get("CONV_DIR", "conversations"))
CONV_DIR.mkdir(exist_ok=True)

def conv_path(cid: str) -> Path:
    # パストラバーサル対策:英数字・ハイフンのみ許可
    safe = "".join(ch for ch in cid if ch.isalnum() or ch in "-_")
    return CONV_DIR / (safe + ".json")

@app.get("/api/conversations")           # 一覧
def api_list():
    items = []
    for p in CONV_DIR.glob("*.json"):
        d = json.loads(p.read_text(encoding="utf-8"))
        items.append({"id": d["id"], "title": d.get("title"), "updated": d.get("updated", 0)})
    return sorted(items, key=lambda x: x["updated"], reverse=True)

@app.put("/api/conversations/{cid}")     # 保存
async def api_put(cid: str, req: Request):
    data = await req.json()
    obj = {"id": cid, "title": data.get("title", "(無題)"),
           "updated": int(time.time()), "messages": data.get("messages", [])}
    conv_path(cid).write_text(json.dumps(obj, ensure_ascii=False), encoding="utf-8")
    return {"ok": True}

これで conversations/ フォルダに会話が貯まり、サーバーを再起動しても・別のブラウザから開いても履歴が残るようになりました。

依存ライブラリを増やさない工夫:Markdown整形やコードの見た目は、marked.js などのCDNライブラリを使うと手軽ですが、それだとネットが無いとUIが崩れます。「オフラインで動くローカルAI」という利点を守るため、整形処理はすべてHTML内に自前で書き、ネットを切っても完全に動くようにしました。


正直な注意点(ここが大事)

動くと嬉しくてつい「社内に配ろう!」となりますが、今回の構成は"みんなで本格運用"には向いていません。隠さず書きます。

1. 1台を大勢で共有するのは無理(=各自のPCで動かす)

ここが今回の一番大事な前提です。1回の生成だけでもCPUを半分ほど使い切るため、1台のPCを窓口にして複数人が同時に使う、という運用は現実的に成り立ちません。2人目のリクエストは激遅になるか、待たされます。

なので本記事は 「各自が自分のPCに Gemma 4 + このアプリを入れて、自分専用で使う」 ことを前提にしています。共有サーバーを立てるのではなく、アプリを各自に配って、それぞれのPCで動かすイメージです。

2. 動くのは自分のPCが起きている間だけ

OllamaもFastAPIも自分のPCの上で動くので、電源オフ・スリープ・Ctrl+C で止まります。常時起動のサービスではなく、自分が使いたいときに立ち上げるツール、という位置づけです。とはいえ各自のPCで完結するので、誰かのPCに依存して全員が止まる、といった心配はありません。

3. 認証は「合言葉」程度

上で付けた合言葉は、あくまで気休めレベル。インターネットに公開するのは論外で、ルーターのポート開放は絶対にしないでください。使うのは社内LANの中だけに留めます。

4. 会話履歴は自分のPC内に貯まる

履歴は conversations/ フォルダに保存されます。各自のPCで動かす前提なので、そこに貯まるのは自分の会話だけ。外部にも他人にも渡りません。プライバシー面はむしろ安心ですが、裏を返すとバックアップは自分でとる必要があります(フォルダごとコピーしておけばOK)。履歴を残したくないなら、保存処理を外せば1問1答に戻せます。

5. プライバシーはローカルならではの強み

入力内容は自分のPC内だけで処理され、外部サーバーには一切送信されません。クラウドAIに貼りづらい社内コードの下書きなどと相性が良いのは、この点です。(例外:後述のおまけ機能で他の端末に開放した場合は、その入力が自分のPCを通ります。そのときだけ扱いに注意)

6. 出力は鵜呑みにしない

ローカルモデルは誤りが普通に出ます。社内に広く配ると「AIが言ったんだから正しい」と受け取る人が必ず出ます。フォームに一言、注意書きを添えておくのがおすすめです。


「ローカルLLMって、外部からアクセスされないの?」

社内に配る話をすると必ず聞かれるのがこれです。結論から言うと 設定次第。どういう状態だと誰が届くのか、仕組みから整理します。

起動オプションで「誰が届くか」が変わる

起動コマンド 届く範囲
--host 127.0.0.1 自分のPCだけ。他のどの機器からも見えない(一番安全)
--host 0.0.0.0 同じLAN上の機器すべてから届きうる(社内公開はこれ)

0.0.0.0 は「すべてのネットワーク接続口で待ち受ける」という意味で、これが"外部に開く"の実体です。

では「インターネットの誰か」に届く? → 通常は届かない

ここが一番誤解されるところ。0.0.0.0 にしても、そのままではインターネット上の他人はアクセスできません。理由は、家庭や社内のルーターが NAT で守っているから。あなたのPCのアドレス(例 192.168.1.23)はLAN内部だけの住所で、外のインターネットからは直接指定できません。

つまり、インターネットに晒されるのは「自分で意図的に穴を開けたとき」だけです。具体的には:

  • ルーターのポート開放(ポートフォワーディング)をした → これは絶対にやらない
  • ngrok / Cloudflare Tunnel などのトンネルを通した → 便利だが、認証無しで通すと世界中に公開される
  • Ollama自体を OLLAMA_HOST=0.0.0.0 で公開した → FastAPIを飛ばしてOllamaが直接叩ける状態に。認証が一切ないので特に危険

現実的に気をつけるのは、この3つ

  1. 信頼できないネットワークで 0.0.0.0 にしない:カフェやコワーキングのWi-Fiで公開モードにすると、同じネットワークにいる他人から届きます。ファイアウォールは必ず**「プライベート」だけ許可**(前述)にして、自宅・社内の閉じたLANに限定する
  2. ルーターのポート開放はしない:これをやった瞬間、インターネット全体に開きます
  3. 本当に外から使いたいなら、VPN か 認証付きトンネル:素で公開せず、社内VPN経由にするか、トンネル+認証を必ずセットにする

この2層構成が効いてくる

今回の「Ollamaはlocalhostのまま、前段のFastAPIだけ公開」という構成は、ここで活きます。仮にFastAPIをLANに公開しても、Ollama本体は外から直接触れないので、認証やログを差し込む余地を常に自分の手元(FastAPI層)に残せます。「万一のときに守りを足せる場所がある」というのが、この一手間の価値です。


それでも、価値がある

この構成の価値は**「各自が、無料で、安全な"自分専用のAIチャット"を持てる」**ことに尽きます。

  • サーバー契約も、API課金も、面倒な申請も要らない
  • 入力内容が自分のPCの外に出ないので、クラウドAIに貼りづらい話も気兼ねなく使える
  • ターミナルもVS Codeも触らず、ブラウザの使い慣れたUIで使える

大勢で1台を共有する用途には向きませんが、**「アプリを各自に配って、それぞれのPCで動かす」**なら、今日から始められます。まずは自分の手元で、ローカルLLMの実力を体感する入口としてぜひ。


まとめ

  • Ollama + FastAPI(app.py)で、Gemma 4を**ブラウザから使える"自分専用のAIチャット"**にできた
  • 使う側はVS Codeもコマンドも不要、ブラウザでURLを開くだけ
  • 使いながらUIを育て、チャット形式・Markdown整形・ファイル添付・会話履歴のフォルダ保存まで実装した
  • 外部アクセスは設定次第127.0.0.1なら自分だけ、0.0.0.0でも通常はLAN内まで(インターネットはルーターのNATが遮断)。ポート開放だけは絶対にしない
  • 入力は外に出ない安全性はそのまま、非エンジニアでもブラウザで使える
  • ただし 1台を大勢で共有するのは無理(1回の生成でCPUを半分ほど使う)。アプリを各自に配って、それぞれのPCで動かすのが現実解

※検証環境: Windows 11 / Python 3.12 / Ollama + gemma4:12b。モデルの出力は実行ごとに変動します。

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?