Paprika マニュアル
背景
ぱっぷす (PAPS) について
特定非営利活動法人 ぱっぷす (https://paps.jp) は、性的搾取・デジタル性暴力被害の相談窓口です。
意に反して撮影・流出・拡散された性的画像・動画など、デジタル性暴力に遭った方からの相談を受け付け、サイト・プラットフォーム運営者への削除要請、被害者支援、社会への啓発活動を行っています。誰もが被害者にも加害者にもならない社会を目指して、対面・電話・メール・SNS など多様なチャネルで被害者の声に寄り添い続けています。
ProtectionAI
ぱっぷすでは、意に反して投稿された性的画像・動画がインターネット上や SNS 上に拡散していないかを探索し、サイト・プラットフォーム運営者への削除要請までを自動化するシステム ProtectionAI を開発しています。
一度被害に遭った画像は短時間で広範囲に転載・拡散することがあり、被害者本人や少人数のスタッフが手作業で追いかけ続けるには限界があります。ProtectionAI は、この探索と削除要請のプロセスを技術的に補助することで継続的な被害回復支援を可能にし、ひとりでも多くの被害者が「自分の画像が晒されている」という苦しみから少しでも早く解放されることを目指しています。
Paprika が生まれた経緯
意に反して投稿された画像を見つけるためには、SNS、画像共有サイト、動画サイト、ファイル共有サービスなど、インターネット上のさまざまな場所を継続的に巡回する必要があります。しかしこれらのサイトは、ログインが必要だったり、JavaScript で動的に描画されたり、年齢確認ダイアログを経由する必要があったりと、機械的な巡回が難しい場合が多くあります。
そこで ぱっぷす は、こうした複雑なサイトでも安定してページを開き、画像・動画・リンク URL を収集できる 独自クローラー基盤 として Paprika を開発しました。
Paprika は ProtectionAI の探索基盤として動作する一方、特定の用途に閉じず汎用的な Web 自動化フレームワークとして整理し、オープンソースとして公開しています。同様の課題 (動的サイトの安定巡回、LLM 連携、分散ワーカー運用) に取り組む方の参考にしていただければ幸いです。
1. 概要 #
Paprika は CDP (Chrome DevTools Protocol) ベースのブラウザ自動化プラットフォームです。LLM が書いた Python スクリプトを sandbox で実行し、ページ画像 / 動画 / HTML を分散ワーカー群で一括収集します。
- paprika-hub — FastAPI の中央サーバ。ジョブをキューし、ワーカーに配り、結果を集める
- paprika-worker — Xvfb + Chrome (nodriver) + noVNC を持つコンテナ。1 台で複数 Lane (=独立ブラウザ) を保持
- paprika-client — Playwright 互換の Python SDK。
page.goto(),page.click(),page.links()等 - paprika-runner — paprika-hub が codegen / rerun 用にその都度起動する使い捨て sandbox コンテナ
4 つのジョブモード:
fetch— 単発で URL を開いて HTML + assets を取得 (worker 直接駆動)codegen-loop— 自然言語 goal → LLM がスクリプト生成 → 実行 → 失敗時リトライvision-agent— CogAgent (vision LLM) がスクリーンショットを見てクリックを生成rerun— 既存スクリプトをそのまま実行 (Simple Macro UI が compile した Python もこれ)
2. アーキテクチャ #
2.1 構成要素
| コンポーネント | 役割 | 状態 |
|---|---|---|
| Hub | API サーバ、ジョブ管理、ワーカー登録、UI | 1 プロセス (paprika.lan) |
| Worker | Xvfb + Chrome の Lane を保持、CDP 経由でページ操作 | ~25 台 LAN ホスト |
| Lane | 独立 Chrome プロセス + noVNC viewer | 1 worker あたり 1-2 lanes |
| Session | 1 Lane の予約 (paprika-client が握る) | job 単位、TTL あり |
| Runner | codegen / rerun の sandbox | job ごとに spawn → 終了で削除 |
| Agent service | Qwen / CogAgent ラッパ (port 8001) | 1 プロセス、Hub と並列 |
| Redis | job state の永続化 | Hub 同居 |
2.2 データの流れ
2.3 保存場所
キャプチャされた画像 / 動画 / HTML は Hub に集約されます (worker は通り道だけ)。
| パス (Hub コンテナ内) | 内容 | ホスト実体 |
|---|---|---|
/data/jobs/{id}/assets/ | 画像 / 動画 / .m3u8 / etc. | Docker volume paprika_paprika-data |
/data/jobs/{id}/log.txt | ジョブログ | 同上 |
/data/jobs/{id}/attempts/{n}/ | codegen の各 attempt 出力 | 同上 |
/data/jobs/{id}/state/{key}.json | page.set_state() の永続データ | 同上 |
/data/jobs/hosts/ | ホスト別 cookie / visited URLs | 同上 |
/data/jobs/presets/ | 保存された preset | 同上 |
/data/jobs/skills/ | codegen 用 skill snippets | 同上 |
paprika-data volume を NFS マウントに変更するか、symlink で assets/ だけ別ストレージに逃がす設計が可能 (操作ハンドブック §運用 参照)。
3. クイックスタート #
UI から
- http://paprika.lan を開く
- Submit タブで URL を入れる
- モードを選択 (Fetch / AI / Code)
- Submit ボタン → Live パネルが開く
- Log / Screenshot / Links / noVNC / Gallery タブで進行を見る
curl から
# 最小ジョブ (fetch mode)
curl -X POST http://paprika.lan/jobs \
-H "Content-Type: application/json" \
-d '{"url": "https://example.com"}'
# 完了待ち
JOB_ID=...
until [ "$(curl -s http://paprika.lan/jobs/$JOB_ID | jq -r .status)" = "completed" ]; do
sleep 2
done
# 結果ギャラリーを開く
open http://paprika.lan/jobs/$JOB_ID/gallery
Python (paprika-client) から
import asyncio
import paprika_client as pap
from paprika_client import async_paprika
async def main():
async with async_paprika.connect() as cli:
async with cli.session(initial_url="https://example.com") as page:
await page.wait_for(seconds=2)
await page.capture("my-page")
links = await page.links()
print(f"{len(links)} links found")
for url in await page.links(urls_only=True):
print(url)
asyncio.run(main())
connect() は引数なしでも動きます: 環境変数 PAPRIKA_HUB → http://localhost:8000 の順で解決されます。runner 環境では PAPRIKA_HUB が自動セットされるので、ローカル実行・コンテナ内実行で同じスクリプトがそのまま動きます。明示したい場合は connect("http://paprika.lan") のように渡してください。
4. ジョブモード #
4.1 fetch — 単発キャプチャ
1 URL を開いて HTML + 全 asset (画像 / 動画 / CSS / JS) をダウンロード。worker が直接駆動、LLM 不要、最速・最安。
POST /jobs
{
"url": "https://example.com",
"options": {
"mode": "fetch",
"scroll": true, // 無限スクロールページ用
"scroll_max": 5000, // px
"play_videos": true, // <video> を自動再生して .ts セグメント取得
"wait_seconds": 20,
"idle_seconds": 3.0,
"max_wait_seconds": 60.0
}
}
4.2 codegen-loop — 自然言語 → スクリプト生成
goal を渡すと LLM が paprika-client スクリプトを書いてくれる。最大 N 回までリトライ。
POST /jobs
{
"url": "https://example.com",
"options": {
"mode": "codegen-loop",
"goal": "このサイトのトップから辿れるページを順にクロールして画像を全部保存",
"max_codegen_attempts": 3,
"attempt_timeout_s": 3600
}
}
4.3 vision-agent — CogAgent
スクリーンショット + 自然言語 task → CogAgent (vision LLM) がピクセル座標でクリック / タイプ / スクロールを生成。CSS セレクタが効かない動的サイトに強い。
POST /jobs
{
"url": "https://x.com",
"options": {
"mode": "vision-agent",
"goal": "ログインして検索欄に paprika と入れて検索",
"vision_max_steps": 30
}
}
4.4 rerun — 既存スクリプト実行
LLM を介さず Python コードをそのまま sandbox 実行。Simple Macro UI で組んだマクロもコンパイル結果が rerun として動く。
POST /jobs
{
"url": "https://example.com",
"options": {
"mode": "rerun",
"code": "import asyncio\nimport paprika_client as pap\n...",
"attempt_timeout_s": 600
}
}
# 過去 job のスクリプトをそのまま rerun
{
"options": {
"mode": "rerun",
"rerun_from": "abc123def456/attempts/2"
}
}
5. Submit / Live パネル #
UI から投入すると、ジョブごとに Live パネル が開きます。サブタブ構成:
| タブ | 表示内容 | 裏で叩く API |
|---|---|---|
| Log | stdout / stderr のリアルタイムストリーム | WS /jobs/{id}/events |
| noVNC | 各セッションの Chrome 画面 (iframe で複数並列) | /jobs/{id}/sessions |
| Screenshot | Lane の Live 画像 + 手動 Capture + 保存済みサムネ | /workers/{wid}/lanes/{idx}/screenshot |
| Links | 現在ページの全 <a href> 絶対 URL (filter + copy 機能) | /sessions/{sid}/links / /jobs/{id}/links |
| Code | codegen-loop の各 attempt スクリプト | /jobs/{id}/attempts/{n}/script.py |
| Assets | 保存された assets のサムネグリッド | /jobs/{id}/assets.json |
page.X() 呼び出しは [paprika] page.click('.btn') / [paprika] -> OK (45ms) の 2 行で記録されます。成功は [stdout]、失敗 (NO_MATCH / ERR) は [stderr] に出るので Live Log で grep しやすい。
5.1 keep_session と RUNNING / KEEPALIVE / IDLE バッジ #
options.keep_session=true でジョブを投入すると、クロール終了後もブラウザを生かしたまま Live noVNC から人間が引き継いで操作できます。Screenshot タイル右上のバッジ色がオペレータ状態を表します:
| バッジ | 意味 | 遷移条件 |
|---|---|---|
| RUNNING | クロール実行中、または KEEPALIVE 中にオペレータが noVNC でマウス/キーを操作中 | jobs.progress.phase=running |
| KEEPALIVE | クロールは終わってブラウザは生きてるが、誰も触ってない (= 暖機状態) | jobs.progress.phase=keepalive |
| IDLE | セッション終了。Live noVNC は切断され、レーン解放 | session reaper が close |
ステートマシン (デフォルト値):
クロール終了
│
▼
[KEEPALIVE] ─── 60 秒 RFB 無活動 ──→ [IDLE] (reaper が DELETE)
▲ │
│ │ noVNC からマウス/キー/クリップボード
│ ▼
[RUNNING] ─── 15 秒 RFB 無活動 ────→ KEEPALIVE に戻る
- RFB 活動: マウス移動 / クリック / キーボード入力 / クリップボード。noVNC viewer が描画更新リクエストだけ送っている (= 誰も触ってない) のとは区別されます。
- idle_ttl_s: keep_session のデフォルトは 60 秒。RFB が来るたびに
last_active_atがリセットされるので、操作している間はカウンタが回りません。 - QUIET_S = 15 秒: RUNNING → KEEPALIVE の降格猶予。連続したマウスドラッグでも 10 秒に 1 回しか touch されない (throttle) ので、それより少し長く設定。
- 絶対 TTL = 24 時間: いくら触っていても 24 時間経つと強制 close される (置き忘れ防止)。
in_flight として埋まっています。新しいジョブを投入したいのに「全レーン埋まり (503)」が返るときは、不要な keep_session を DELETE するか idle 切れを待ってください。
6. API リファレンス #
全エンドポイントはフラット (no /api/ prefix)。インタラクティブな Swagger UI: https://paprika.lan/docs
- すべて
Content-Type: application/json(アップロード系のみmultipart/form-data) - 認証なし — LAN 内信頼前提。公開する場合はリバースプロキシで basic-auth / IP 制限
- 例の
$HUBはhttps://paprika.lanに読み替えてください - JSON フィールドの省略時は **太字** で示したデフォルトが入ります
6.1 Jobs — ジョブのライフサイクル
Job は paprika における **作業単位**。投入 → キュー → worker 割当 → 実行 → 結果配信 → 結果取得 / 削除 のステートマシン。
POST /jobs — ジョブ投入
Request body (JobRequest):
| フィールド | 型 | 必須 | 説明 |
|---|---|---|---|
url | string | ✓ | 初期 URL (fetch / codegen / vision-agent の起点) |
options | JobOptions | 下記参照 |
JobOptions の主な field (一部抜粋):
| フィールド | 型 | デフォルト | 説明 |
|---|---|---|---|
mode | string | "fetch" | fetch / codegen-loop / vision-agent / rerun |
goal | string | null | codegen / vision-agent モード必須。自然言語タスク |
code | string | null | rerun モード: 直接実行する Python ソース (max 200KB) |
rerun_from | string | null | rerun モード: 過去 attempt 参照 ("{job_id}" or "{job_id}/attempts/{n}") |
max_codegen_attempts | int | 3 | codegen-loop のリトライ上限 (1-10) |
vision_max_steps | int | 30 | vision-agent の observe → act 反復上限 (1-200) |
attempt_timeout_s | int | 180 | 各 attempt の sandbox 実行タイムアウト秒 (30-864000) |
scroll | bool | false | fetch: 無限スクロールページ用に自動スクロール |
scroll_max | int | 3000 | fetch: スクロール上限 px |
play_videos | bool | false | fetch: <video> を自動再生してセグメント取得 |
capture_assets | bool | true | fetch: 画像 / 動画 / フォントをキャプチャ |
headless | bool | false | Chrome を headless 起動 (noVNC 不可) |
cookies_from | string | null | ホスト名指定で /hosts/{host} 登録 cookie を注入 |
referer | string | null | 初期リクエストの Referer 強制 |
attach_to_job | string | null | 過去 job_id の lane に再 attach (cookies / session 保持) |
min_asset_size_bytes | int | 0 | これより小さい asset は捨てる |
keep_session | bool | false | fetch: クロール完了後もブラウザを残し、Live noVNC から人間が操作可能にする。詳細は 下記。 |
Response (JobInfo):
| フィールド | 型 | 説明 |
|---|---|---|
job_id | string (12 hex) | ジョブ ID |
status | enum | queued / running / completed / failed / cancelled |
worker_id | string | 割当 worker (null = まだ未割当) |
lane_idx | int | 割当 lane index |
novnc_url | string | Live noVNC viewer (autoconnect 付き) |
progress | JobProgress | {phase, assets_saved, assets_failed, last_log} |
error | string | 失敗時のエラーメッセージ |
サンプル (curl):
# 最小: 1 URL を fetch
curl -X POST $HUB/jobs \
-H "Content-Type: application/json" \
-d '{"url": "https://example.com"}'
# 動画ハーベスト + スクロール
curl -X POST $HUB/jobs \
-H "Content-Type: application/json" \
-d '{
"url": "https://example.com/video/abc",
"options": {
"mode": "fetch",
"scroll": true,
"scroll_max": 5000,
"play_videos": true,
"wait_seconds": 30,
"cookies_from": "example.com"
}
}'
# codegen-loop で自然言語タスク
curl -X POST $HUB/jobs \
-H "Content-Type: application/json" \
-d '{
"url": "https://example.com",
"options": {
"mode": "codegen-loop",
"goal": "トップから辿れるページを順にクロールして画像を全部保存",
"max_codegen_attempts": 3,
"attempt_timeout_s": 3600
}
}'
その他の Jobs エンドポイント
<a href> 絶対 URL (job 完了後でも参照可)6.2 Sessions — 対話的ブラウザ操作 paprika-client の HTTP 面
Session は 1 Lane (= 1 Chrome タブ) の予約。POST /sessions で確保し、各種 action endpoint で操作、DELETE /sessions/{sid} で解放します。Session は idle TTL / absolute TTL の二重制限付きで自動 GC されます。
POST /sessions — セッション開始
Request body:
| フィールド | 型 | デフォルト | 説明 |
|---|---|---|---|
initial_url | string | null | 開いた直後に遷移する URL (省略時は about:blank) |
parent_job_id | string | null | このセッションを紐づける親 job (capture / get_state の保存先) |
worker_id | string | null | 特定 worker を指定 (省略時は自動選択) |
lane_hint | int | null | 特定 lane を指定 |
idle_ttl_s | int | 300 | 無操作タイムアウト秒 (POST /sessions 直接呼び出しのデフォルト。fetch keep_session=true 経由のセッションは 60 秒) |
absolute_ttl_s | int | 3600 | 絶対タイムアウト秒 |
cookies_from | string | null | ホスト名指定で /hosts/{host} 登録 cookie を注入 |
Response: {session_id, worker_id, lane_idx, novnc_url, novnc_url_autoconnect, initial_url, created_at, ...}
サンプル:
# 最小
curl -X POST $HUB/sessions \
-H "Content-Type: application/json" \
-d '{"initial_url": "https://example.com"}'
# 親 job + ホスト cookie 注入 + 長時間用 TTL
curl -X POST $HUB/sessions \
-H "Content-Type: application/json" \
-d '{
"initial_url": "https://example.com",
"parent_job_id": "abc123def456",
"cookies_from": "example.com",
"idle_ttl_s": 1800,
"absolute_ttl_s": 7200
}'
ライフサイクル
Read-only inspection
<a href> 絶対 URL (skip javascript:/mailto:/etc.)Actions
/hosts/{host} に保存Persistent state (key-value)
Actions の body / response 詳細
各 action は POST /sessions/{sid}/<name>。すべて共通 response shape:
{
"status": "OK" | "NO_MATCH" | "ERR: ...",
"elapsed_ms": 1234,
"result": { ... } // action ごとに異なる
}
| Action | Body | 備考 |
|---|---|---|
/navigate | {"url": "https://..."} | page.goto() 相当 |
/click | {"selector": ".btn"} | CSS セレクタ。[@N] を [data-paprika-id="N"] に展開して使うのが outline 連携 |
/fill | {"selector": "#input", "value": "..."} | focus + value 設定 + input/change イベント発火 |
/press | {"key": "Enter", "count": 1, "modifiers": ["Ctrl"]} | W3C キー名 or "Ctrl+A" 形式 |
/type | {"text": "hello"} | 現フォーカス要素に挿入 (CDP Input.insertText) |
/scroll | {"direction": "down", "pixels": 800} | direction: down/up/left/right |
/back | {} | history boundary を CDP で確認してから安全に戻る |
/forward | {} | Back の対称。末尾なら no-op |
/history_first | {} | 履歴 0 番目 (initial_url) にジャンプ。CDP navigateToHistoryEntry 経由 |
/agent | {"goal": "...", "max_steps": 5, "engine": "auto"} | engine: auto/qwen/cogagent。JP の goal は自動英訳 |
/capture | {"label": "step-1", "step": 0} | HTML + screenshot + outline をまとめて保存 |
/download_video | {"url": "https://...", "referer": null, "timeout_s": 1800} | yt-dlp 起動。url 省略時は現ページ |
/save_cookies_to_host | {"host": "example.com"} | 現 Cookie jar から host にマッチするものだけ /hosts/{host} へ保存 |
サンプル:
# SID を変数で持っておく
SID=$(curl -sX POST $HUB/sessions -H "Content-Type: application/json" \
-d '{"initial_url": "https://example.com"}' \
| jq -r .session_id)
# クリック → タイプ → Enter
curl -X POST $HUB/sessions/$SID/click -d '{"selector": "#search"}' -H "Content-Type: application/json"
curl -X POST $HUB/sessions/$SID/type -d '{"text": "paprika"}' -H "Content-Type: application/json"
curl -X POST $HUB/sessions/$SID/press -d '{"key": "Enter"}' -H "Content-Type: application/json"
curl -X POST $HUB/sessions/$SID/capture -d '{"label": "after-search"}' -H "Content-Type: application/json"
# 終了
curl -X DELETE $HUB/sessions/$SID
6.3 Workers — ワーカー登録 + 監視
Worker は WebSocket で hub に常時接続し、双方向で制御メッセージをやり取りします。新規 worker を立てるとここに自動的に並ぶ。
GET /workers — 接続中ワーカー一覧
Response:
{
"count": 26,
"workers": [
{
"worker_id": "abc12345-xyz0",
"alive": true,
"address": "192.168.1.50",
"age_seconds": 3,
"in_flight": 0,
"capacity": 2,
"labels": {"region": "local"},
"version": "dev",
"status": "active",
"lane_novnc_urls": [
"http://192.168.1.50:6080/vnc_lite.html",
"http://192.168.1.50:6081/vnc_lite.html"
]
},
...
]
}
POST /workers/{wid}/status — 状態切り替え
Body: {"status": "active" | "drain" | "standby"}
active— 通常スケジュール対象 (デフォルト)drain— 新規ジョブを受け取らない、進行中は完走させる (メンテ前に)standby— drain と同じ効果。意図的な一時停止
その他の Workers エンドポイント
WorkerRegister → HubRegistered)?width=1280&quality=70server/ + core/ + VERSION の gzip tarball (worker が VERSION mismatch で取得)サンプル:
# 全 worker の生存状況
curl -s $HUB/workers | jq '.workers[] | {worker_id, address, alive, in_flight}'
# 特定 worker を drain (メンテ前)
curl -X POST $HUB/workers/abc12345-xyz0/status \
-H "Content-Type: application/json" \
-d '{"status": "drain"}'
# lane 0 の現状を JPEG で取得 (1280px)
curl -s "$HUB/workers/abc12345-xyz0/lanes/0/screenshot?width=1280" -o lane.jpg
6.4 Registries — 永続化メタデータ
すべて /data/jobs/<name>/ 配下に JSON で保存され、hub 再起動後も残ります。
Hosts — ホスト別 Cookie / Visited URL
ログイン状態を保つために 1 度ブラウザでログインした後、その cookie を保存し、以降の job で自動注入する仕組み。
| フィールド | 型 | 説明 |
|---|---|---|
host | string | 正規化済みホスト名 (www. 除去、lowercase)。example.com と www.example.com は同じレコード |
cookies | list | CDP Network.Cookie 形式の配列 |
note | string | UI に表示する任意メモ |
visited_count | int | このホストでの訪問済み URL 件数 |
updated_at | ISO 8601 | 最終更新時刻 |
サンプル:
# 全ホスト一覧
curl -s $HUB/hosts | jq '.hosts[] | {host, cookie_count, visited_count}'
# 1 ホストの詳細
curl -s $HUB/hosts/example.com
# cookies を登録 (ブラウザ devtools からエクスポートした JSON など)
curl -X PUT $HUB/hosts/example.com \
-H "Content-Type: application/json" \
-d '{
"cookies": [
{"name": "session_token", "value": "abc...", "domain": ".example.com",
"path": "/", "secure": true, "httpOnly": true, "sameSite": "Lax"}
],
"note": "logged in as user@example.com"
}'
# 訪問履歴 (URL を SHA256 で保存)
curl -s "$HUB/hosts/example.com/visited?offset=0&limit=50"
curl -X POST $HUB/hosts/example.com/visited \
-H "Content-Type: application/json" \
-d '{"url": "https://example.com/page/1"}'
# URL パターンが既訪問とマッチするか測る
curl -X POST $HUB/hosts/example.com/visited/match_counts \
-H "Content-Type: application/json" \
-d '{"urls": ["https://example.com/p/1", "https://example.com/p/2"]}'
Presets — Submit フォームのスナップショット
Submit UI で組んだ「URL + モード + macro 行 + コード + 数値設定」を 1 つの名前付きレコードとして保存。cron などから POST /presets/{name}/run 1 発で再投入できる。500+ 件を想定したページャ付き。
| フィールド | 型 | 説明 |
|---|---|---|
name | string | preset 名 (ファイル名にもなる) |
category | string | UI 上のグループ化用ラベル |
description | string | 1 行説明 |
ui_mode | string | fetch / ai / code |
ai_engine | string | codegen / simple |
url | string | 初期 URL |
goal | string | codegen の goal テキスト |
simple_rows | list | simple macro の行 ({action, detail}) |
code_script | string | code mode の Python ソース |
options | JobOptions | POST /jobs に直接渡す snapshot |
created_at / updated_at / last_used_at | ISO 8601 | 監査用 |
サンプル:
# 一覧 (検索 + ページャ)
curl "$HUB/presets?q=daily&category=crawl&offset=0&limit=50"
# 1 preset の詳細
curl $HUB/presets/example-daily
# upsert
curl -X PUT $HUB/presets/example-daily \
-H "Content-Type: application/json" \
-d '{
"name": "example-daily",
"category": "crawl",
"description": "毎日 example.com 巡回",
"ui_mode": "ai",
"ai_engine": "codegen",
"url": "https://example.com",
"goal": "新着ページを順にクロールして画像保存",
"options": {
"mode": "codegen-loop",
"goal": "新着ページを順にクロールして画像保存",
"max_codegen_attempts": 3
}
}'
# 実行 (cron 用)
curl -X POST $HUB/presets/example-daily/run
# 実行時に URL や timeout を上書き
curl -X POST $HUB/presets/example-daily/run \
-H "Content-Type: application/json" \
-d '{"url": "https://example.com/different-page", "attempt_timeout_s": 600}'
# crontab 例 (毎日 3:00)
# 0 3 * * * curl -fsS -X POST https://paprika.lan/presets/example-daily/run
Skills — codegen に注入する再利用スニペット
「このサイトはこう操作する」「動画ダウンロードはこのパターン」といった codegen-loop に注入する Python コード断片。LLM プロンプトで類似 goal の skill を retrieval して prompt に含めることで、毎回ゼロから書かせるよりも安定して動くスクリプトが出る。
| フィールド | 型 | 説明 |
|---|---|---|
slug | string | kebab-case の ID |
title | string | UI 表示名 |
description | string | 1 行説明 (retrieval 用) |
body | string | Python ソース (LLM が改変前提のテンプレ) |
tags | list[string] | 絞り込み用 |
promoted | bool | true = 常に prompt に含める |
サンプル:
# 一覧
curl $HUB/skills
# upsert
curl -X PUT $HUB/skills/download-video-with-yt-dlp \
-H "Content-Type: application/json" \
-d '{
"slug": "download-video-with-yt-dlp",
"title": "動画ダウンロード (yt-dlp 経由)",
"description": "現ページが動画ページなら page.download_video() で .mp4 を保存",
"tags": ["video", "yt-dlp"],
"body": "await page.download_video(timeout_s=600)\n# results -> /data/jobs/{id}/assets/"
}'
# 常時 prompt に含める (codegen-loop の安定化用)
curl -X POST $HUB/skills/download-video-with-yt-dlp/promote
# 解除
curl -X POST $HUB/skills/download-video-with-yt-dlp/demote
Conventions — codegen の運用ルール
「コード規約 / やってはいけないパターン / 必ず守ること」を保存しておき、codegen-loop の prompt に注入する。前回失敗したパターン (例: page.click('a') でナビゲーションを期待する) を convention として登録 → 次回以降の attempt で LLM が回避するようになる。
Skills と同じ schema (slug / title / description / body / tags / promoted)。
サンプル:
# 一覧
curl $HUB/conventions
# 「特定 host で必須」みたいなルールを登録
curl -X PUT $HUB/conventions/redirect-www-strip \
-H "Content-Type: application/json" \
-d '{
"slug": "redirect-www-strip",
"title": "ホスト比較は www. を除去してから",
"description": "example.com と www.example.com を別ホスト扱いしてリンクをスキップしない",
"body": "host = (urlparse(url).hostname or '\''\\'').lower().lstrip(\"www.\")"
}'
curl -X POST $HUB/conventions/redirect-www-strip/promote
AI Engines — LLM / VLM / VLA バックエンドの登録
各 record は 1 つの AI バックエンド (Qwen / CogAgent / GPT-4 / Claude 等)。page.agent(engine="<slug>") や page.ask() がこの registry を見て endpoint を解決します。組み込み 3 件 (qwen / qwen-chat / cogagent) は初回起動時に seed され、UI でも編集できます (slug / kind / protocol だけは固定)。
| フィールド | 型 | 説明 |
|---|---|---|
slug | string | 主キー (kebab-case)。engine="slug" の参照に使う |
name | string | UI 表示用ラベル |
kind | enum | chat / vision-chat / gui-agent |
protocol | enum | openai / anthropic (予約) / agent-service / cogagent |
endpoint | string | ベース URL (trailing / 不要) |
model | string | model 名 (OpenAI model= パラメータに渡る) |
api_key_env | string | env 変数の名前 (値そのものは JSON に保存しない) |
headers | dict | 追加 HTTP header |
timeout_s | int | HTTP timeout 秒 |
promoted | bool | engine="auto" 時に同 kind で優先する |
builtin | bool | true = 組み込み (削除不可、UI で読み取り専用箇所あり) |
サンプル:
# 一覧 (api_key の値は返ってこない、env が set 済みかの bool は返る)
curl $HUB/engines | jq '.engines[] | {slug, kind, protocol, model, promoted, builtin, api_key_set}'
# 新規追加: OpenAI 直
curl -X PUT $HUB/engines/gpt-4o-mini \
-H "Content-Type: application/json" \
-d '{
"name": "GPT-4o mini",
"kind": "chat",
"protocol": "openai",
"endpoint": "https://api.openai.com",
"model": "gpt-4o-mini",
"api_key_env": "OPENAI_API_KEY",
"timeout_s": 60
}'
# 新規追加: Claude (LiteLLM proxy 経由で OpenAI-compat 化)
curl -X PUT $HUB/engines/claude-sonnet-3.5 \
-H "Content-Type: application/json" \
-d '{
"name": "Claude 3.5 Sonnet (via LiteLLM)",
"kind": "vision-chat",
"protocol": "openai",
"endpoint": "http://litellm:4000",
"model": "claude-3-5-sonnet-20241022",
"api_key_env": "ANTHROPIC_API_KEY"
}'
# 接続テスト (sends 1 token "pong" prompt)
curl -X POST $HUB/engines/gpt-4o-mini/test
# auto 時の優先指定
curl -X POST $HUB/engines/claude-sonnet-3.5/promote
# 解除
curl -X POST $HUB/engines/claude-sonnet-3.5/demote
api_key_env: "ANTHROPIC_API_KEY" のように env 変数名のみ記録され、値は .env でホストごとに設定。
これにより registry の JSON は安全にバックアップ・共有でき、キーローテーションは .env 編集 + hub 再起動だけで完結します。
Settings — hub 全体設定
主な field:
| フィールド | 型 | 説明 |
|---|---|---|
codegen_skill_top_k | int | codegen retrieval の top-K (デフォルト 1) |
codegen_convention_top_k | int | convention retrieval の top-K |
min_asset_size_bytes | int | JobOptions のフォールバック値 |
vision_max_steps_default | int | vision-agent の規定値 |
サンプル:
# 現設定を取得
curl $HUB/settings | jq
# patch 更新 (PUT、未指定 field は保持)
curl -X PUT $HUB/settings \
-H "Content-Type: application/json" \
-d '{"codegen_skill_top_k": 3, "min_asset_size_bytes": 1024}'
6.5 Codegen — 自然言語 → スクリプト生成 (1-shot)
codegen-loop モードと違って、これは「LLM にスクリプトだけ書かせる」 1-shot API。sandbox 実行はしない。UI の Submit フォームで「Generate code」ボタンを押すと裏で叩かれる。
POST /codegen
Body:
| フィールド | 型 | 説明 |
|---|---|---|
goal | string | 自然言語タスク (必須) |
initial_url | string | 起点 URL (prompt の文脈に入る) |
retry_context | string | 過去 attempt の stderr / 結果 (再試行プロンプト用) |
top_k_skills | int | retrieval 数の上書き |
Response:
{
"code": "import asyncio\nimport paprika_client as pap\n...",
"llm": {
"model": "qwen2.5-vl-72b",
"elapsed_ms": 17500,
"usage": {"prompt_tokens": 4477, "completion_tokens": 561},
"finish_reason": "stop"
},
"skills_used": ["download-video-with-yt-dlp"]
}
サンプル:
curl -X POST $HUB/codegen \
-H "Content-Type: application/json" \
-d '{
"goal": "ニュースサイトのトップから上位 10 記事の本文を集める",
"initial_url": "https://example.com"
}' | jq -r .code
GET /codegen/info
使用中の LLM 設定 (model 名 / URL / timeout) を返す。デバッグ用。
6.6 Meta / Admin
https://paprika.lan/https://example.com でアドレスバーから直接 fetch7. paprika-client SDK #
Playwright と同じ書き味の async Python SDK。page.goto(), page.click(), page.fill()... 通常の Page API は全部使えます。
インストール
pip install paprika-client # PyPI 公開はまだ。今は repo clone + pip install -e
# または
pip install git+https://github.com/paps-jp/paprika.git#subdirectory=client/python
主要 API
| メソッド | シグネチャ | 備考 |
|---|---|---|
cli.list_workers() | → list[dict] | fleet 状態 |
cli.session(...) | async with → Page | セッション開閉 |
page.goto(url) | str → dict | navigate |
page.click(selector) | str → dict | CSS セレクタクリック |
page.fill(sel, value) | str, str → dict | input に値セット |
page.press(key, count, modifiers) | → dict | "Enter", "Ctrl+A", etc. |
page.type(text) | str → dict | 現フォーカス要素に挿入 |
page.scroll(dir, pixels) | "down"/"up"/"left"/"right", int → dict | CDP マウスホイール |
page.wait_for(seconds=N) | → None | asyncio.sleep ラッパ |
page.back() | → dict | history 境界に安全 |
page.forward() | → dict | Back の対称、末尾でも安全 |
page.history_first() | → dict | 履歴 0 番目 (initial_url) に戻る |
page.exists(selector) | → bool | CSS セレクタの存在チェック (LLM 不要、確定的) |
page.ask(question, engine="qwen") | → bool | LLM に yes/no 質問。if 分岐用 |
page.capture(label) | str → dict | HTML + screenshot + outline 保存 |
page.download_video(url=None, timeout_s=1800) | → dict | yt-dlp 起動 |
page.agent(goal, max_steps=5, engine="auto") | → dict | LLM ループ 1 回 |
page.state() | → dict | url, title, etc. |
page.outline() | → str | アクセシビリティツリー |
page.links(urls_only=False) | → list[dict] or list[str] | 全 <a href> 絶対 URL |
page.screenshot(path=None) | → bytes | PNG |
page.cookies() | → list[dict] | 現在の jar |
page.get_state(key) / page.set_state(key, value) | → Any | parent job に永続化 |
アクションログ
すべての action 呼び出しは自動的に Live Log に出ます:
[paprika] page.click('.btn-primary')
[paprika] -> OK (45ms)
[paprika] page.click('.no-such')
[paprika] -> NO_MATCH (1500ms) # ← stderr に流れる
不要な場合は PAPRIKA_CLIENT_ACTION_LOG=0 で無効化。
使用例: クロールループ
import asyncio
from paprika_client import async_paprika
async def main():
async with async_paprika.connect() as cli:
async with cli.session(initial_url="https://news.ycombinator.com/") as page:
await page.wait_for(seconds=2)
urls = await page.links(urls_only=True)
story_urls = [u for u in urls if "/item?" in u]
for i, u in enumerate(story_urls[:20]):
await page.goto(u)
await page.capture(f"story-{i+1}")
await page.wait_for(seconds=1)
asyncio.run(main())
8. LLM / Agent 使用例 #
Paprika には 3 つの LLM 連携経路があり、それぞれ用途が違います。実コードでよく使うパターンを集めました。
| 使い方 | 呼び出す API | 向いてるタスク |
|---|---|---|
page.agent() | script 内で 1 ステップずつ LLM 判断 | 確実な制御 + 短い判断 (キャプチャ識別、年齢ゲート突破、再生ボタン探し) |
mode=codegen-loop | LLM がスクリプトを書いて sandbox 実行 | 「このサイトを巡回」のような大きめタスクをまるごと丸投げ |
mode=vision-agent | CogAgent が画面を見て pixel 座標でクリック | CSS セレクタが効かない / 動的レンダリング / 視覚情報必須のサイト |
8.1 page.agent() — スクリプト内で 1 ステップ LLM
script の流れは自分で書きつつ、「ここだけ LLM の判断に任せる」というハイブリッド。engine で使う LLM を選ぶ:
"cogagent"— vision LLM。スクショから座標を出す。視覚タスク向け"qwen"— text LLM (Qwen 2.5-VL)。outline (アクセシビリティツリー) を読んで判断"auto"(デフォルト) — qwen を試して失敗したら cogagent にフォールバック
サンプル: ギャラリーの写真を順にめくる
import asyncio
import paprika_client as pap
from paprika_client import async_paprika
async def main():
async with async_paprika.connect() as cli:
async with cli.session(initial_url="https://example.com/gallery") as page:
await page.wait_for(seconds=5)
for i in range(5):
# 「写真 N 枚目をクリック」を vision LLM に任せる
await page.agent(
f"写真{i+1}枚目をクリック",
engine="cogagent",
max_steps=2,
)
await page.wait_for(seconds=5)
await page.capture(f"photo-{i+1}")
await page.back()
await page.wait_for(seconds=5)
asyncio.run(main())
サンプル: 同意 / 年齢確認ダイアログを自動で消す
async def dismiss_dialogs(page):
"""ページに modal / consent dialog があれば閉じる。なければ即 return。"""
await page.agent(
"もし年齢確認 / cookie consent / login modal が出ていたら "
"受諾 or 閉じる。何もなければ即 done を返す。",
engine="auto", # qwen 優先 (outline で判断、安く速い)
max_steps=3,
)
async def main():
async with async_paprika.connect() as cli:
async with cli.session(initial_url="https://example.com") as page:
await page.wait_for(seconds=3)
await dismiss_dialogs(page)
await page.capture("clean-page")
サンプル: ログインを LLM に任せる
async def main():
async with async_paprika.connect() as cli:
async with cli.session(initial_url="https://example.com/login") as page:
await page.wait_for(seconds=3)
# 複数ステップを 1 回の agent 呼び出しでこなす
result = await page.agent(
"ユーザ名欄に 'demo@example.com'、パスワード欄に 'pass123' を入れて "
"ログインボタンを押す",
engine="auto",
max_steps=5,
)
print(f"agent completed: {result['completed']} in {result['steps_taken']} steps")
# ログイン後の cookie を host registry に保存して以後の job で再利用
await page.save_cookies_to_host("example.com")
{
"completed": True, # LLM が END/done を返した
"steps_taken": 3, # 実際に動いたステップ数
"summary": "clicked the play button at (640, 360)",
"last_action": {"kind": "click", "x": 640, "y": 360},
"error": None # 失敗時のメッセージ
}
8.2 codegen-loop — 自然言語タスクまるごと丸投げ
「目的」だけ伝えると LLM が paprika-client スクリプトを書き、sandbox で実行、失敗したら retry まで自動。POST /jobs の mode として使う。
サンプル: 自然言語クロール
# curl 版
curl -X POST https://paprika.lan/jobs \
-H "Content-Type: application/json" \
-d '{
"url": "https://example.com",
"options": {
"mode": "codegen-loop",
"goal": "サイトのトップから各カテゴリページへ辿り、全画像を保存。最大 200 枚で打ち切り。",
"max_codegen_attempts": 3,
"attempt_timeout_s": 3600
}
}'
サンプル: skill / convention を仕込んでから投入
# 「動画ページではこの関数を使え」と LLM に伝える skill を事前登録
curl -X PUT https://paprika.lan/skills/download-with-yt-dlp \
-H "Content-Type: application/json" \
-d '{
"slug": "download-with-yt-dlp",
"title": "動画ダウンロード",
"description": "動画ページに到達したら page.download_video() で .mp4 保存",
"body": "if \"/video/\" in page.url:\n await page.download_video(timeout_s=600)\n",
"tags": ["video"]
}'
curl -X POST https://paprika.lan/skills/download-with-yt-dlp/promote
# あとは投入するだけ — LLM が自動的に skill を踏まえてスクリプトを書く
curl -X POST https://paprika.lan/jobs \
-H "Content-Type: application/json" \
-d '{
"url": "https://example.com/videos",
"options": {"mode": "codegen-loop", "goal": "動画ページを順に辿って全部保存"}
}'
8.3 vision-agent — CogAgent が画面を見てクリック
CSS セレクタが効かないサイト (Canvas / 動的レンダリング / shadow DOM の塊) は CogAgent モードが強い。スクショ → 座標 → クリック を CogAgent が繰り返す。
curl -X POST https://paprika.lan/jobs \
-H "Content-Type: application/json" \
-d '{
"url": "https://example.com",
"options": {
"mode": "vision-agent",
"goal": "検索欄に paprika と入れて検索ボタンを押し、最初の結果をクリック",
"vision_max_steps": 20,
"attempt_timeout_s": 600
}
}'
8.4 POST /codegen — スクリプトを書かせるだけ (sandbox なし)
LLM にスクリプトだけ出力させて、自分で確認 / 編集してから投入したいとき。UI の "Generate code" ボタンと同じ動き。
# 1-shot コード生成
curl -X POST https://paprika.lan/codegen \
-H "Content-Type: application/json" \
-d '{
"goal": "Hacker News の上位 10 記事のタイトルと URL を取得",
"initial_url": "https://news.ycombinator.com/"
}' | jq -r .code > script.py
# 確認 / 編集して rerun で投入
curl -X POST https://paprika.lan/jobs \
-H "Content-Type: application/json" \
-d "{
\"url\": \"https://news.ycombinator.com/\",
\"options\": {\"mode\": \"rerun\", \"code\": $(jq -Rs . < script.py)}
}"
8.5 使い分けのコツ
| 状況 | おすすめ | 理由 |
|---|---|---|
| 確実な制御 + 一部だけ LLM | page.agent() | スクリプトの流れは自分で握れる + 失敗時の rollback が書ける |
| 「とりあえずやってみて」 | codegen-loop | retry も自動。試行錯誤コストが安い |
| セレクタが取れないサイト | vision-agent or page.agent(engine="cogagent") | 画像認識ベース。Canvas / shadow DOM / iframe 混在 OK |
| JP の goal を出す | 何でも OK | worker 側で自動英訳 (Qwen) → LLM 渡し |
| 動画ダウンロード | page.download_video() 直呼び | yt-dlp 起動。LLM 介す必要なし |
| 大量並列実行 | codegen で 1 スクリプト書く → preset 保存 → cron | safe + 再現性 + ログが揃う |
page.agent() は高い
CogAgent は 1 step あたり 2-3 秒 + GPU リソース消費。10 回ループなら 30 秒+。loop 上限と timeout は慎重に。
9. Simple Macro #
コードを書かなくても GUI で簡単マクロが組める機能。Submit タブ → AI → Simple モード で表示。
| Action | コンパイル先 |
|---|---|
| Navigate | await page.goto(url) |
| Click (CSS) | await page.click(selector) |
| Agent (Visual) | await page.agent(description, engine="cogagent", max_steps=2) |
| Agent (DOM) | await page.agent(description, engine="qwen", max_steps=2) |
| Type | await page.type(text) |
| Fill | await page.fill(selector, value) |
| Press key | await page.press(key, count=N) |
| Scroll | await page.scroll(dir, px) |
| Wait | await page.wait_for(seconds=N) |
| Back | await page.back() |
| Forward | await page.forward() |
| 履歴の最初へ | await page.history_first() |
| Capture | await page.capture(label) |
| Agent | await page.agent(goal, max_steps=5) |
| Download video | await page.download_video(url=...) |
| Loop (begin) / End loop | for i in range(N): ... ブロック |
| If (CSS) / Else / End if | if await page.exists(selector): ... else: ... |
| If (Agent) / Else / End if | if await page.ask(question, engine="qwen"): ... else: ... |
条件分岐 (If 行)
If (CSS) と If (Agent) の 2 種類を使い分けます:
- If (CSS) — CSS セレクタの存在で分岐。確定的・速い・LLM 不要。
.login-btnのように要素を CSS で特定できる場合はこちらを使う - If (Agent) — 自然言語の yes/no 質問を LLM に投げて分岐。柔軟だが ~2 秒のレイテンシ + 誤判定可能性。「エラーが出ているか?」 のように CSS では表現しにくい判断に
使用例 (ログイン処理):
Navigate https://example.com/dashboard
If (CSS) input[name="password"]
Fill input[name="user"] => myname
Fill input[name="password"] => secret
Click .login-btn
Else
Capture already-logged-in
End if
compile 結果 (Python):
await page.goto("https://example.com/dashboard")
if await page.exists('input[name="password"]'):
await page.fill('input[name="user"]', 'myname')
await page.fill('input[name="password"]', 'secret')
await page.click('.login-btn')
else:
await page.capture("already-logged-in")
False に倒す」セーフティ付き。
Loop の中の Action は {i+1} のような Python f-string 埋め込みを使える:
Loop 5
Click (visual) the {i+1}th thumbnail
Capture page-{i+1}
Wait 2
End loop
10. ワーカー運用 #
9.1 デプロイ
新規 worker ホストを追加
# worker ホスト (Linux LXC / VM / 物理) で
git clone https://github.com/paps-jp/paprika.git /opt/paprika
cd /opt/paprika
cp .env.example .env
# .env に HUB_URL=ws://paprika.lan を設定
# NOVNC_PUBLIC_HOST=自分の LAN IP も設定 (もしくは未設定→hub が自動補正)
docker compose -f docker-compose-worker.yml up -d
hub の更新
ssh root@paprika.lan
cd /opt/paprika
git pull
docker compose restart hub
# worker は自己更新で追随 (下記参照)
worker の手動更新 (まれ)
ssh root@
cd /opt/paprika
git pull
docker compose -f docker-compose-worker.yml restart
9.2 クローン検知
worker ホストを LXC / Proxmox / VMware でクローンすると、worker_id が重複する。Hub は接続元 IP の差を見て自動検知 → 新 ID を発行 → worker は /root/.paprika/worker_id に永続化して再接続。
[hub] worker_id collision: d41662a57eef-4811 already held by ;
new connection from reassigned to d41662a57eef-ljwb
[worker] hub reassigned id -> d41662a57eef-ljwb (clone collision detected);
persisting and reconnecting
運用上、クローン作成は 何もしなくて OK。Hub が勝手に処理。
9.3 自己更新
📖 詳細なドキュメント: Worker 自動配信の仕組み (専用ページに完全解説 + シーケンス図 + runbook あり)
worker は handshake 時に Hub の version と自分の version を比較。不一致なら自動で更新サイクルが回る:
- Hub が
HubRegistered.expected_worker_versionを返す - worker が mismatch を検知
GET /worker-source.tar.gzで hub から最新ソースを取得/app/server/,/app/core/,/app/VERSIONに展開 (in-place、削除されたファイルも prune)- exit code 42 で終了
- Docker restart policy で再起動 → 新コードで boot
version 算出は /app/server + /app/core の全 .py ファイルの SHA-256 (先頭 12 hex)。手動 VERSION bump 不要。コード編集 → git pull → docker compose restart hub だけで、hub のハッシュが変わる。worker は次の handshake で旧ハッシュとの差を検知し、tarball 経由で自分の source を hub と同一内容に書き換える → 直後のハッシュ計算で一致 → ループせず収束。
旧設計 (VERSION ファイル文字列比較) からの違い: 以前は repo の VERSION = "dev" のままだと両側 dev で常に no-op、誰も気づかぬまま自動更新が永久発火しなかった。コンテンツハッシュ式は VERSION ファイルが空でも、bind-mount しか無くても、デプロイのたびに自然に新ハッシュが立つ。
Fallback: source dir が無い等で hash が算出できない場合は /app/VERSION → PAPRIKA_VERSION env → "dev" の順。両側が "dev" のときだけ比較スキップ。片方だけ "dev" なら mismatch として扱う ("自分の version を知らない側" が常に追従)。
| 環境変数 | デフォルト | 説明 |
|---|---|---|
PAPRIKA_WORKER_AUTO_FETCH_SOURCE | 1 | tarball 自動取得 + 展開 |
PAPRIKA_WORKER_AUTO_EXIT_ON_VERSION_MISMATCH | 1 | 不一致で exit 42 |
PAPRIKA_GITHUB_REPO | (無) | GitHub releases フォールバック (任意) |
9.4 noVNC URL 自動補正 + hub プロキシ
📖 外部ページへの埋め込み (iframe) を作る人は: VNC 埋め込み API ガイドへ (URL の組み立て / iframe 例 / React / CSP / 認証 等の完全版)
クローンされた worker は NOVNC_PUBLIC_HOST が元ホストの IP のまま (例: ) になる。Hub は API レスポンス時に client_address で書き換えてから返すので、admin UI には正しい IP が表示される。
さらに novnc_url 自体を hub 経由のプロキシ URL (/jobs/<id>/novnc/?path=...) に書き換えて返すため、external client は worker LAN IP に直接到達する必要がない。詳細は VNC 埋め込み API。
worker 側の .env を手で直したい場合:
ssh root@
sed -i 's|^NOVNC_PUBLIC_HOST=.*|NOVNC_PUBLIC_HOST=|' /opt/paprika/.env
docker compose -f /opt/paprika/docker-compose-worker.yml restart
11. トラブルシュート #
worker が hub に繋がらない
docker logs paprika-worker-1でログ確認HUB_URL=ws://paprika.lanが正しいかcurl http://paprika.lan/healthで hub 到達性WORKER_SECRET不一致だとbad worker secretで 1008 close
セッションが 404 になる
長時間ノーアクションだと session reaper が evict します。デフォルトの idle_ttl_s:
POST /sessions直接 / SDKcli.session(...): 300 秒 (5 分)- fetch ジョブ
keep_session=true: 60 秒 (詳細は 5.1 keep_session と RUNNING / KEEPALIVE / IDLE バッジ)
page.download_video() のように idle を超えるアクションは reaper が "running" 状態を見てスキップする (最新版で対応済)。それでも 404 が出るなら idle_ttl_s をジョブ投入時に大きく設定:
cli.session(initial_url="...", idle_ttl_s=3600)
fetch + keep_session の場合は SDK 側で延長したいときに await sess.keepalive(idle_ttl_s=600) でセッション単位に上書きできます。
ジョブが queued のまま
GET /workersでalive=trueな worker がいるか- worker が drain / standby になってないか (
POST /workers/{wid}/statusで確認) - lane が空いてるか (
in_flight < capacityな worker が必要)
※ 全レーン埋まりの状態で POST /jobs すると 200 ではなく 503 "no worker available (fleet at capacity); retry with backoff" が返ります。queued になるのは 1 つでも空きレーンを持つ worker がいるときだけ。バースト投入するクライアントは 503 を捕まえて指数バックオフしてください。
codegen-loop が同じスクリプトをリトライし続ける
UI の Live パネル → Code タブで各 attempt の script.py を見る。失敗パターンが共通なら conventions または skills に "やっちゃダメ" / "これ使え" を登録すると次回以降の prompt に注入される。
動画ファイルが落ちてこない
- passive CDP capture では .ts / .m4s セグメントしか取れないことが多い
- 明示的に
page.download_video()を呼ぶ (yt-dlp が起動して .mp4 に統合) - サイトが referer check してるなら
page.download_video(url=..., referer=...)
NAS 移行で hub が固まる (将来)
SMB / NFS マウントは必ず soft オプションで。hard mount だとネットワーク断時に I/O が永遠ハングし、FastAPI イベントループ全体が止まる。
12. 環境変数一覧 #
Hub 側
| 変数 | デフォルト | 説明 |
|---|---|---|
PAPRIKA_VERSION | (file) | hub の VERSION 上書き |
PUBLIC_BASE_URL | http://hub:8000 | worker に教える hub URL |
WORKER_SECRET | (無) | worker WS handshake 認証 |
CODEGEN_LLM_URL | http:// | codegen 用 LLM endpoint |
CODEGEN_MODEL_NAME | qwen2.5-vl-72b | codegen 用 model |
PAPRIKA_RUNNER_IMAGE | paprika-runner:latest | runner コンテナイメージ |
PAPRIKA_RUNNER_MAX_CONCURRENT | 3 | 同時 runner 数上限 |
Worker 側
| 変数 | デフォルト | 説明 |
|---|---|---|
HUB_URL | ws://paprika.lan:8000 | hub の WS URL |
WORKER_ID | auto | 固定したい場合のみ |
WORKER_SECRET | (無) | hub と一致が必要 |
LANE_POOL | (file:SLOT_POOL) | Lane (ブラウザ) 数 |
MAX_CONCURRENT | 2 | 同時ジョブ数 |
NOVNC_PUBLIC_HOST | localhost | noVNC URL のホスト部 (hub 自動補正で上書きされる) |
NOVNC_BASE_PORT | 6080 | Lane 0 の noVNC ポート |
AGENT_URL | http://agent:8001 | page.agent 用 LLM proxy |
AGENT_LLM_URL | http:// | 翻訳・コード生成 LLM の直 URL |
COGAGENT_URL | http:// | vision-agent endpoint |
PAPRIKA_WORKER_ID_FILE | ~/.paprika/worker_id | worker_id 永続化先 (Windows 含む) |
PAPRIKA_WORKER_AUTO_FETCH_SOURCE | 1 | tarball 自動取得 |
PAPRIKA_WORKER_AUTO_EXIT_ON_VERSION_MISMATCH | 1 | 不一致時 exit 42 |
paprika-client / Runner
| 変数 | デフォルト | 説明 |
|---|---|---|
PAPRIKA_JOB_ID | (無) | runner が session に紐づける parent job ID (自動セット) |
PAPRIKA_CLIENT_ACTION_LOG | 1 | [paprika] page.X(...) ログを出すか |
13. 最近の変更 #
- keep_session ステートマシン — クロール後の RUNNING / KEEPALIVE / IDLE 3 状態化。noVNC で操作中は RUNNING、15 秒触らないと KEEPALIVE、60 秒で IDLE (= reaper close)。詳細は 5.1
- 過剰投入は 503 — 全レーン埋まり時に hub の in-process フォールバックで失敗していたのを、即 503 ("no worker available; retry with backoff") を返すように。クライアント側でバックオフ前提
- API flat — 全 endpoint から
/api/prefix を削除。Swagger は/docs - page.links() — 現ページの全
<a href>を絶対 URL で取得。Live UI に Links タブ追加 - action log split — 成功は stdout、失敗は stderr (Live Log の [stderr] 連発を抑制)
- session reaper fix — 長時間アクション (download_video 等) 中に session が evict される問題を修正
- noVNC auto-rewrite — クローン worker の stale
NOVNC_PUBLIC_HOSTを hub 側で透過的に補正 - worker self-update — Hub が tarball 配信、workers が自動 fetch + restart
- clone collision detection — 同 worker_id が複数 IP から来たら hub が自動でリネーム
- Logo / Brand — "paprika hub" → "Paprika" / ロゴを黄色に
14. 今後の開発予定 #
現時点で検討中・着手予定のもの。太字 = 着手中、それ以外は構想段階。
ストレージ
- NAS 移行 (SMB) —
/data/jobs/{id}/assets/だけを NAS に逃がす設計。OS 層softmount + アプリ層スプール + バックグラウンド flusher の 2 段防御。 - S3 互換 (MinIO) バックエンド — 大規模展開向け
- 古い job の自動アーカイブ (cold tier への移動)
API / SDK
POST /jobs/bulk— 一括投入。今は 1 ジョブずつ POST が必要PATCH /jobs/{id}— 実行中ジョブの設定編集 (現状は再投入のみ)- Webhook 対応 — ジョブ完了時に外部 URL に POST
- paprika-client の同期 (sync) API — asyncio を使わないスクリプトでも気軽に
- PyPI 公開 —
pip install paprika-client
LLM / Agent
- Claude / GPT-4 系の追加バックエンド対応
- page.agent() の自動 skill 学習 — 成功した手順を skill registry に自動追記
- vision-agent と
page.agent(engine="cogagent")の経路統一 - マルチ session 並列 LLM (1 つのタスクで複数 worker を協調)
運用 / 可観測性
- 認証 / マルチユーザ — 公開デプロイ前提の basic-auth + per-user job 隔離
- Prometheus / OpenTelemetry エクスポート — メトリクス標準化
- Admin UI のモバイル対応強化
- マルチ Hub (HA 構成) — リーダー選出 + Redis 経由のジョブシェア
- Job retry policy のカスタマイズ — サイト別に exponential backoff / max attempts を設定
UI
- Submit フォームのカスタマイズ (preset と紐づく "起動テンプレ")
- Gallery のフィルタ強化 (タグ / size / mime / 日付)
- Live Log の検索 + ハイライト
- Worker fleet のマップ表示 (LAN topology)
その他
- RFC ドキュメントの整理 — 内部設計メモを公開用に再構成
- テストカバレッジ向上 — 現状は手動 smoke test 中心
- Dockerfile のマルチアーキ対応 (arm64 / Raspberry Pi で worker 動作)