Metadata-Version: 2.4
Name: library-hiroba
Version: 0.3.0
Summary: Google Colab と PyHiroba で同じコードが動く、教育向けの UI と AI の道具
Project-URL: Homepage, https://github.com/funakoshi-takehiro/library-hiroba
Project-URL: Repository, https://github.com/funakoshi-takehiro/library-hiroba
Project-URL: Issues, https://github.com/funakoshi-takehiro/library-hiroba/issues
Project-URL: Changelog, https://github.com/funakoshi-takehiro/library-hiroba/releases
Author: Takehiro Funakoshi
License-Expression: MIT
License-File: LICENSE
Keywords: colab,education,html,jupyter,llm,notebook,pyodide,ui,widgets
Classifier: Development Status :: 4 - Beta
Classifier: Framework :: Jupyter
Classifier: Intended Audience :: Education
Classifier: Natural Language :: Japanese
Classifier: Operating System :: OS Independent
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3 :: Only
Classifier: Programming Language :: Python :: 3.9
Classifier: Programming Language :: Python :: 3.10
Classifier: Programming Language :: Python :: 3.11
Classifier: Programming Language :: Python :: 3.12
Classifier: Programming Language :: Python :: 3.13
Classifier: Topic :: Education
Classifier: Topic :: Education :: Computer Aided Instruction (CAI)
Classifier: Typing :: Typed
Requires-Python: >=3.9
Provides-Extra: ai
Requires-Dist: torch; extra == 'ai'
Requires-Dist: transformers; extra == 'ai'
Provides-Extra: dev
Requires-Dist: build; extra == 'dev'
Requires-Dist: nbformat; extra == 'dev'
Requires-Dist: pytest; extra == 'dev'
Requires-Dist: ruff>=0.16; extra == 'dev'
Requires-Dist: twine; extra == 'dev'
Provides-Extra: gallery
Requires-Dist: playwright; extra == 'gallery'
Description-Content-Type: text/markdown

# library-hiroba

Google Colab と PyHiroba で、同じコードが同じように動く教育向けのライブラリです。入口は2つあります。

| 入口 | できること |
|---|---|
| `ui` | カード・クイズ・進捗バーといった部品を、ノートブックのセル出力にそのまま表示します |
| `ai` | 小さな言語モデルを、その場（ブラウザまたはノートブック）で動かします |

[PyHiroba](https://pyhiroba.weblab.t.u-tokyo.ac.jp/) は、インストールも登録も必要とせず、ブラウザだけで Python を学べる、日本の学校現場向けの学習環境です。

```python
from library_hiroba import ai, ui

ui.card("今日の目標", "for文を使って、九九の表を作ってみよう！")
```

```python
ui.quiz("2の8乗はいくつ？", choices=[128, 256, 512], answer=256,
        explanation="2を8回かけると 256 になるよ。")
# 選択肢を選ぶと色とマークで正誤が表示され、「解説を見る」で解説が開きます
```

## 特徴

用意された部品に加えて、HTML と CSS を自由に書けます。教材に必要な見た目は、たいていこの2つで作れます。

```python
ui.html('<div class="fukidashi">まずは print() を試してみよう</div>',
        css=".fukidashi { border: 2px solid var(--hui-accent); border-radius: 14px; padding: 10px 16px; }")
```

CSS はその部品の内側だけに適用されるため、クラス名を気軽に付けてもページの他の部分に影響しません。ふきだし、手順ステップ、単語カード、横棒グラフといった見た目も、この方法で作れます。

そのほかの特徴は次のとおりです。

| 項目 | 内容 |
|---|---|
| 動作環境 | セル最後の式を `_repr_html_()` で表示する共通のしくみに乗るため、Colab と PyHiroba で表示が一致します |
| 実装方式 | 表示も操作も HTML と CSS だけで完結します（クイズの正誤表示は `:checked`、開閉は `<details>`） |
| 依存関係 | `ui` は純 Python で依存ライブラリがありません。配布物は `py3-none-any` の wheel で、Colab の pip でも Pyodide の micropip でも取得できます（`ai` を Colab で使うときだけ追加の依存が要ります） |
| 見た目 | 配色・書体・角丸を PyHiroba 本体のデザインに合わせています |
| 配慮 | 本文と状態色は WCAG 4.5:1 以上です（アクセント色を塗ったボタンの白文字だけは 3.87:1 で、UI 部品の基準 3:1 を満たします）。アニメーションは `prefers-reduced-motion` に従います |

## インストール

Google Colab と Jupyter:

```
%pip install library-hiroba
```

Colab で `ai` も使うときは、追加の依存（transformers と torch）を含めます。

```
%pip install "library-hiroba[ai]"
```

PyHiroba では、同梱されていれば `import library_hiroba` だけで使えます。`ai` の実行はブラウザ側の経路を使うので、追加のインストールは要りません。同梱前の環境では `!pip install library-hiroba` を実行すると micropip が PyPI から取得します。

## 部品一覧

| 部品 | 例 |
|---|---|
| 説明カード | `ui.card("今日の目標", "本文", footer="ヒント")` |
| ヒント・注意 | `ui.alert("メッセージ", kind="warning", title="よくあるまちがい")`（kind: `info` / `success` / `warning` / `danger`） |
| 選択式クイズ | `ui.quiz("問題", choices=[128, 256, 512], answer=256, explanation="解説")`（`answer` は値で指定します） |
| 答えの開閉 | `ui.reveal("答えは42", summary="答えを見る")` |
| 進捗バー | `ui.progress(7, max=10, label="練習問題")` |
| 数値タイル | `ui.stat("正答率", 85, unit="%")` |
| 横並び配置 | `ui.columns(部品1, 部品2, widths=[2, 1])` |
| バッジ | `ui.badge("重要", color="red")`（color: `blue` / `green` / `red` / `amber` / `gray`） |
| テーブル | `ui.table([{"名前": "佐藤", "得点": 90}], caption="結果")` |
| 自由 HTML/CSS | `ui.html('<div class="x">…</div>', css=".x { color: hotpink; }")` |
| 入力フォーム | `ui.form(handler, ui.field("question", label="質問"))` |
| 考え中の表示 | `ui.thinking("考え中")`（`ui.form()` が送信中に自動で出します） |
| 会話の表示 | `ui.chat([{"role": "user", "content": "…"}, {"role": "assistant", "content": "…"}])` |

> **外部への通信はありません。** 部品は PyHiroba と同じ書体（Zen Kaku Gothic New）を名前で指定しますが、**取りには行きません**。PyHiroba ではページ側が読み込み済みなので、それだけで見た目が揃います。持っていない環境（Colab など）では端末の書体になります。Colab でも同じ書体で揃えたい場合は `ui.use_web_font(True)` を呼んでください（そのぶん表示のたびに Google へ通信が起き、閲覧者の IP が渡ります）。`ai` を使う場合のモデル取得を除けば、ほかに外部へ通信する箇所はありません。

> **クイズの答えについて。** `ui.quiz()` は JavaScript を使わず CSS だけで正誤を出しているため、**答えは出力の HTML に含まれます**。ブラウザの「ソースを表示」や検証ツールを開けば読めてしまうので、練習用に使ってください。成績をつけるテストには向きません。

複数の部品は次のようにまとめます。

```python
ui.stack(ui.card("目標", "..."), ui.progress(3, max=10))   # 縦に積む
ui.columns(ui.stat("得点", 90), ui.stat("順位", 3))        # 横に並べる
```

セルの途中で表示したい場合は `ui.show(...)` を使います。Colab ではその場に表示され、IPython のない PyHiroba では部品を返すので、セル最後の式として置きます。

## 入力を Python に戻す

`ui.form()` は入力欄とボタンを表示し、押されたときに関数を呼びます。入力欄の名前が、そのままキーワード引数になります。

```python
def ask(question, level):
    return ui.card(question, f"{level} 向けの答えです")

ui.form(ask,
        ui.field("question", label="質問", placeholder="スマホは持っていっていい？"),
        ui.field("level", label="学年", kind="choice", choices=["1年", "2年", "3年"]),
        title="校則について聞いてみよう", submit_label="聞く")
```

入力欄の種類は `text`（既定）/ `number` / `multiline` / `choice` です。`ui.field` の代わりに文字列を渡すと、その名前のテキスト欄になります。

動かし方は環境に合わせて自動で切り替わります。

| 環境 | 動作 |
|---|---|
| Colab・Jupyter（ipywidgets あり） | テキスト欄とボタンの対話 UI。押すたびに関数が呼ばれます |
| ipywidgets が無い環境 | `input()` で順に聞いて、結果を表示します |
| PyHiroba | HTML のフォームを表示します。値を受け取るには本体側の対応が必要です（[`docs/PYHIROBA_FORMS.md`](https://github.com/funakoshi-takehiro/library-hiroba/blob/main/docs/PYHIROBA_FORMS.md) に設計案があります） |

先生が書くコードは1つで済み、環境ごとの切り替えは不要です。

### チャット形式にする

`ui.chat()` は会話を吹き出しで並べます。役割は `user` / `assistant` / `note` の3つで、`content` には文字列のほか他の部品も入れられます。

会話を変数にためて `ui.chat()` を返すようにすると、1つのセルだけでチャットができます。`clear_on_submit=True` を付けると、送信のたびに入力欄が空になります。

```python
history = []

def ask(question):
    history.append({"role": "user", "content": question})
    history.append({"role": "assistant", "content": f"「{question}」ですね。"})
    return ui.chat(history, names={"user": "あなた", "assistant": "ボット"})

ui.form(ask, ui.field("question", label="質問"),
        submit_label="送信", clear_on_submit=True)
```

## AI（小さな言語モデル）

`ai` は、小さな言語モデルをその場で動かします。メソッドは4つだけです。

```python
from library_hiroba import ai

await ai.models()                  # 選べるモデルの一覧
await ai.load()                    # モデルを読み込む（初回だけ時間がかかります）
print(await ai.ask("日本の四季について、2行で書いて"))

async for chunk in ai.stream("俳句を1つ"):   # 書けたぶんから受け取る
    print(chunk, end="")
```

`await` が必要です。ノートブック（Colab / Jupyter / PyHiroba）では、セルの中にそのまま `await` を書けます。PyHiroba は GitHub Pages 配信のため `SharedArrayBuffer` を使った同期待ちができず、ブラウザ側は待つ形にせざるを得ません。Colab 側は待つ必要がありませんが、**同じコードが両方で動く**ことを優先して形を揃えています。

`ask()` には `max_tokens` を渡せます（既定は 256）。

```python
print(await ai.ask("俳句を1つ作って", max_tokens=64))
```

### 動く場所

| 環境 | 動かし方 | 用意するもの |
|---|---|---|
| PyHiroba | ブラウザの中で動きます（本体が用意した経路を使います） | なし |
| Colab・Jupyter | `transformers` と `torch` で動きます | `%pip install "library-hiroba[ai]"` |

どちらの経路でも、入力した文章が外部に送られることはありません。通信が起きるのはモデルを受け取るときだけです。

### 選べるモデル

`load()` に名前を渡すとモデルを選べます。

```python
await ai.load("llmjp150m")
```

軽いものから順に並べています。校内の回線では、右の数字を先に見てください。

| 名前 | 内容 | 目安の通信量（ブラウザ／Colab） |
|---|---|---|
| `llmjp150m` | LLM-jp-3 150M。国産でとても軽い一方、文章は不自然です | 約 255MB ／ 約 600MB |
| `qwen3_06` | Qwen3 0.6B。`qwen05` より新しく、日本語が少し良いです | 約 550MB ／ 約 1.5GB |
| `qwen05`（既定） | Qwen2.5 0.5B。日本語が使えます | 約 900MB ／ 約 1.0GB |
| `qwen3_17` | Qwen3 1.7B。この中でいちばん賢い一方、いちばん重いです | 約 1.3GB ／ 約 3.4GB |
| `qwen15` | Qwen2.5 1.5B。日本語がより自然ですが重いです | 約 1.6GB ／ 約 3.1GB |

ブラウザ側は同じモデルを精度違いで並べるため `qwen05-q8` のように末尾が付いた名前も使えます。精度まで指定したいときはそちらを、そうでなければ上の共通の名前を使ってください。共通の名前はどちらの環境でも通ります。

一覧が短いのは、**PyHiroba と Colab の両方で動くものだけ**を載せているためです。ブラウザは ONNX に変換されたモデルしか読めないので、そこが上限になります。増やすときの調べ方は [`docs/PYHIROBA_INTEGRATION.md`](docs/PYHIROBA_INTEGRATION.md) の「モデルを増やすとき」にあります。

### チャットとして表示する

`ui.form()`・`ui.chat()` と組み合わせると、1つのセルで対話ができます。

```python
history = []

async def ask(question):
    history.append({"role": "user", "content": question})
    history.append({"role": "assistant", "content": await ai.ask(question)})
    return ui.chat(history, names={"user": "あなた", "assistant": "AI"})

await ai.load()
ui.form(ask, ui.field("question", label="質問"),
        submit_label="送信", clear_on_submit=True)
```

`handler` は `async def` で書けます。`ui.form()` は返り値が `await` の要るものかどうかを見て、必要なら待ってから表示します。送信のたびに会話全体を描き直すので、吹き出しが下に伸びていきます。入力欄は `clear_on_submit=True` で空に戻ります。

入力を Python に戻す経路は環境によって変わるため、この組み合わせが動くのは今のところ Colab・Jupyter です（PyHiroba は本体側の対応待ちです）。

送信を押すと、答えが返るまで**「考え中」の点が動きます**（自動）。言葉を変えるときは `pending="AI が考えています"`、出さないときは `pending=None` を渡してください。

はじめに `await ai.load()` を済ませておくと、1通目でモデルの読み込み（数十秒〜）を待たされずに済みます。

### 書けたところから少しずつ出す

`ai.stream()` は、答えを**書けたぶんから**返します。`handler` を `yield` で書くと、届くたびに表示が差し替わります。

```python
history = []

async def talk(question):
    history.append({"role": "user", "content": question})
    text = ""
    async for chunk in ai.stream(question):
        text += chunk
        yield ui.chat(history + [{"role": "assistant", "content": text}],
                      names={"user": "あなた", "assistant": "AI"})
    history.append({"role": "assistant", "content": text})

await ai.load()
ui.form(talk, ui.field("question", label="質問"),
        submit_label="送信", clear_on_submit=True)
```

`ai.stream()` を全部つなげると `ai.ask()` と同じ文になります。**少しずつ返せない環境では、書き終えてから一度にまとめて返します** — どちらでも同じコードが動きます。考えている途中（`<think>`）は、途中で切れても取り除かれます。

モデルのライセンスは配布元をご確認ください（既定の Qwen2.5 は Apache-2.0）。

## デザイン

配色・書体・形状は PyHiroba 本体のデザイントークンに合わせています。

| 項目 | 値 |
|---|---|
| ブランドカラー | `#028DAE`（ダーク時 `#35aecb`） |
| 書体 | Zen Kaku Gothic New（名前で指定するだけで、取りには行きません）。PyHiroba ではページ側が持っているため揃います。持っていない環境では `system-ui` になります |
| 角丸 | カード 14px、行とアラート 8px、バッジと進捗バー 999px |
| 記号 | 文字記号（`i` `✓` `!` `×`）を CSS の円形マークに載せて表します |

### テーマ

既定はライトです。ダークになるのは、祖先要素に `data-theme="dark"` が付いているとき、すなわち PyHiroba でダークモードに切り替えたときだけです。OS の配色設定は参照しません。これは PyHiroba 本体と同じ方針で、ページがライト表示のまま部品だけが暗くなる食い違いを避けるためです。

背景を持つ部品は不透明な色で塗ってあるので、ページの下地が何色でも文字と背景の組み合わせが保たれます。背景を持たない部分（進捗バーのラベルや表のキャプション）はページの文字色を受け継ぎ、暗いページでも読めます。

### テーマ変数

配色は CSS カスタムプロパティとして公開しています。`ui.html()` の `css` から使うと、テーマの切り替えに自動で追従します。

```python
ui.html('<div class="box">ヒント</div>',
        css=".box { border: 2px solid var(--hui-accent); background: var(--hui-accent-soft); }")
```

主な変数は次のとおりです。

| 用途 | 変数 |
|---|---|
| ブランド色 | `--hui-accent`（線・塗り）、`--hui-accent-ink`（文字）、`--hui-accent-soft`（背景） |
| 状態色 | `--hui-ok` / `--hui-warn` / `--hui-bad`（それぞれ `-ink` と `-soft` あり） |
| 文字 | `--hui-ink` / `--hui-ink-2` / `--hui-ink-3`、`--hui-on-accent`（アクセント色の上に載せる文字） |
| 面 | `--hui-paper` / `--hui-bg-2` / `--hui-line` |
| 形 | `--hui-radius` / `--hui-radius-sm` / `--hui-shadow` |

書体は Google Fonts から読み込みます。オフラインや閉域網では `system-ui` に切り替わるだけで、表示は保たれます。外部との通信を完全になくす場合は、`src/library_hiroba/_css.py` の `FONT_IMPORT` を空文字にします。

## しくみと範囲

各部品は、必要な CSS を同梱した自己完結の HTML を返します。同じ CSS が何度出力されても表示は変わりません。渡したテキストはすべて HTML エスケープされ、改行は `<br>` になります。エスケープしない経路は `ui.html()` だけです。

部品の表示と CSS による操作は、どの環境でも同じように動きます。入力を Python に戻す `ui.form()` は環境によって経路が変わり、PyHiroba では本体側の対応を待っています。

`ai` も環境によって経路が変わります。PyHiroba では本体が用意した経路を通し、Colab では `transformers` を使います。書き方は同じですが、動くモデルの実体と読み込みにかかる時間は環境で違います。`ui` は純 Python のままで、`ai` を使わないかぎり追加の依存は読み込まれません。

クイズの正解は HTML の class として含まれるため、成績評価ではなく学習用の自己チェックに向いています。

## 開発

```bash
pip install -e ".[dev]"
ruff check src tests tools && pytest        # lint とテスト
python tools/build_gallery.py --shots       # 全部品のギャラリーとスクリーンショットを生成
python tools/check_ai_colab.py              # ai の Colab 経路を実際に動かす（[ai] が必要）
```

- リリース手順: [`docs/RELEASING.md`](https://github.com/funakoshi-takehiro/library-hiroba/blob/main/docs/RELEASING.md)
- PyHiroba に同梱するときの取り決め（ファイル一覧・`ai` の受け渡し）: [`docs/PYHIROBA_INTEGRATION.md`](https://github.com/funakoshi-takehiro/library-hiroba/blob/main/docs/PYHIROBA_INTEGRATION.md)

## ライセンス

[MIT](https://github.com/funakoshi-takehiro/library-hiroba/blob/main/LICENSE)
