Paprika

GitHub README マニュアル

Paprika マニュアル

分散ワーカー + ブラウザ自動化 + 動画収集
v0.x · paps-jp/paprika · README

背景

ぱっぷす (PAPS) について

特定非営利活動法人 ぱっぷす (https://paps.jp) は、性的搾取・デジタル性暴力被害の相談窓口です。

意に反して撮影・流出・拡散された性的画像・動画など、デジタル性暴力に遭った方からの相談を受け付け、サイト・プラットフォーム運営者への削除要請、被害者支援、社会への啓発活動を行っています。誰もが被害者にも加害者にもならない社会を目指して、対面・電話・メール・SNS など多様なチャネルで被害者の声に寄り添い続けています。

ProtectionAI

ぱっぷすでは、意に反して投稿された性的画像・動画がインターネット上や SNS 上に拡散していないかを探索し、サイト・プラットフォーム運営者への削除要請までを自動化するシステム ProtectionAI を開発しています。

一度被害に遭った画像は短時間で広範囲に転載・拡散することがあり、被害者本人や少人数のスタッフが手作業で追いかけ続けるには限界があります。ProtectionAI は、この探索と削除要請のプロセスを技術的に補助することで継続的な被害回復支援を可能にし、ひとりでも多くの被害者が「自分の画像が晒されている」という苦しみから少しでも早く解放されることを目指しています。

Paprika が生まれた経緯

意に反して投稿された画像を見つけるためには、SNS、画像共有サイト、動画サイト、ファイル共有サービスなど、インターネット上のさまざまな場所を継続的に巡回する必要があります。しかしこれらのサイトは、ログインが必要だったり、JavaScript で動的に描画されたり、年齢確認ダイアログを経由する必要があったりと、機械的な巡回が難しい場合が多くあります。

そこで ぱっぷす は、こうした複雑なサイトでも安定してページを開き、画像・動画・リンク URL を収集できる 独自クローラー基盤 として Paprika を開発しました。

Paprika は ProtectionAI の探索基盤として動作する一方、特定の用途に閉じず汎用的な Web 自動化フレームワークとして整理し、オープンソースとして公開しています。同様の課題 (動的サイトの安定巡回、LLM 連携、分散ワーカー運用) に取り組む方の参考にしていただければ幸いです。

本リポジトリの位置づけ — 本リポジトリのコード自体に被害者画像の検出ロジックは含まれていません (それは ProtectionAI 側で実装)。ここで公開しているのは「ブラウザを開いてページを巡回し、画像・動画・リンクを収集する」ための汎用的な Web 自動化基盤です。

1. 概要 #

Paprika は CDP (Chrome DevTools Protocol) ベースのブラウザ自動化プラットフォームです。LLM が書いた Python スクリプトを sandbox で実行し、ページ画像 / 動画 / HTML を分散ワーカー群で一括収集します。

4 つのジョブモード:

2. アーキテクチャ #

2.1 構成要素

Operator / SDK UI / curl / paprika-client Hub (paprika.lan) FastAPI · Redis · Docker socket /jobs · /sessions · /workers · … ジョブキュー + 中央配信 Agent service Qwen / CogAgent proxy Worker (LAN) Xvfb + Chrome Lane 0 / Lane 1 noVNC :6080-6081 × 25 hosts ··· Worker N .140 / .141 / … 自動 clone 検知 自己更新 Runners (使い捨て) codegen / rerun sandbox paprika-client 実行 Hub が docker spawn 終了で削除 HTTP/WS HTTP WS /workers/{id}/link docker spawn HTTP /jobs/{id}/assets /sessions API ――― 制御方向 - - - データ / API 戻り
Paprika の構成要素と通信経路
コンポーネント役割状態
HubAPI サーバ、ジョブ管理、ワーカー登録、UI1 プロセス (paprika.lan)
WorkerXvfb + Chrome の Lane を保持、CDP 経由でページ操作~25 台 LAN ホスト
Lane独立 Chrome プロセス + noVNC viewer1 worker あたり 1-2 lanes
Session1 Lane の予約 (paprika-client が握る)job 単位、TTL あり
Runnercodegen / rerun の sandboxjob ごとに spawn → 終了で削除
Agent serviceQwen / CogAgent ラッパ (port 8001)1 プロセス、Hub と並列
Redisjob state の永続化Hub 同居

2.2 データの流れ

Operator Hub Worker POST /jobs {url, options} ← JobInfo {job_id, status} WS assign_job WS /jobs/{id}/events progress / log lines forward → Operator POST /jobs/{id}/assets → /data/jobs/{id}/assets GET /jobs/{id}/result ← JobResult {assets[], …} ――― リクエスト送信 - - - レスポンス / 戻り
ジョブのデータフロー (シーケンス図)

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}.jsonpage.set_state() の永続データ同上
/data/jobs/hosts/ホスト別 cookie / visited URLs同上
/data/jobs/presets/保存された preset同上
/data/jobs/skills/codegen 用 skill snippets同上
NAS への移行 Docker volume は SMB / NFS バックエンドにも切り替え可能。paprika-data volume を NFS マウントに変更するか、symlink で assets/ だけ別ストレージに逃がす設計が可能 (操作ハンドブック §運用 参照)。

3. クイックスタート #

UI から

  1. http://paprika.lan を開く
  2. Submit タブで URL を入れる
  3. モードを選択 (Fetch / AI / Code)
  4. Submit ボタン → Live パネルが開く
  5. 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_HUBhttp://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
Logstdout / stderr のリアルタイムストリームWS /jobs/{id}/events
noVNC各セッションの Chrome 画面 (iframe で複数並列)/jobs/{id}/sessions
ScreenshotLane の Live 画像 + 手動 Capture + 保存済みサムネ/workers/{wid}/lanes/{idx}/screenshot
Links現在ページの全 <a href> 絶対 URL (filter + copy 機能)/sessions/{sid}/links / /jobs/{id}/links
Codecodegen-loop の各 attempt スクリプト/jobs/{id}/attempts/{n}/script.py
Assets保存された assets のサムネグリッド/jobs/{id}/assets.json
Live Log のアクションログ すべての 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 に戻る
注意: KEEPALIVE 中もそのレーンは 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 制限
  • 例の $HUBhttps://paprika.lan に読み替えてください
  • JSON フィールドの省略時は **太字** で示したデフォルトが入ります

6.1 Jobs — ジョブのライフサイクル

Job は paprika における **作業単位**。投入 → キュー → worker 割当 → 実行 → 結果配信 → 結果取得 / 削除 のステートマシン。

POST /jobs — ジョブ投入

Request body (JobRequest):

フィールド必須説明
urlstring初期 URL (fetch / codegen / vision-agent の起点)
optionsJobOptions下記参照

JobOptions の主な field (一部抜粋):

フィールドデフォルト説明
modestring"fetch"fetch / codegen-loop / vision-agent / rerun
goalstringnullcodegen / vision-agent モード必須。自然言語タスク
codestringnullrerun モード: 直接実行する Python ソース (max 200KB)
rerun_fromstringnullrerun モード: 過去 attempt 参照 ("{job_id}" or "{job_id}/attempts/{n}")
max_codegen_attemptsint3codegen-loop のリトライ上限 (1-10)
vision_max_stepsint30vision-agent の observe → act 反復上限 (1-200)
attempt_timeout_sint180各 attempt の sandbox 実行タイムアウト秒 (30-864000)
scrollboolfalsefetch: 無限スクロールページ用に自動スクロール
scroll_maxint3000fetch: スクロール上限 px
play_videosboolfalsefetch: <video> を自動再生してセグメント取得
capture_assetsbooltruefetch: 画像 / 動画 / フォントをキャプチャ
headlessboolfalseChrome を headless 起動 (noVNC 不可)
cookies_fromstringnullホスト名指定で /hosts/{host} 登録 cookie を注入
refererstringnull初期リクエストの Referer 強制
attach_to_jobstringnull過去 job_id の lane に再 attach (cookies / session 保持)
min_asset_size_bytesint0これより小さい asset は捨てる
keep_sessionboolfalsefetch: クロール完了後もブラウザを残し、Live noVNC から人間が操作可能にする。詳細は 下記

Response (JobInfo):

フィールド説明
job_idstring (12 hex)ジョブ ID
statusenumqueued / running / completed / failed / cancelled
worker_idstring割当 worker (null = まだ未割当)
lane_idxint割当 lane index
novnc_urlstringLive noVNC viewer (autoconnect 付き)
progressJobProgress{phase, assets_saved, assets_failed, last_log}
errorstring失敗時のエラーメッセージ

サンプル (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 エンドポイント

POST/jobs↑ 詳細上記
GET/jobsジョブ一覧 (新しい順)
GET/jobs/{id}1 ジョブの詳細 (status, progress, novnc_url, etc.)
GET/jobs/{id}/result最終結果 (page.html / log / assets メタ)
GET/jobs/{id}/visitedこの job が踏んだ URL リスト
GET/jobs/{id}/links保存済み page.html から抽出した <a href> 絶対 URL (job 完了後でも参照可)
GET/jobs/{id}/sessionsこの job が開いてるセッション一覧
GET/jobs/{id}/attemptscodegen-loop の attempt メタ (LLM model, usage, success フラグ等)
GET/jobs/{id}/attempts/{n}/{filename}script.py / stdout.log / llm_response.txt / result.json
GET/jobs/{id}/assets/{filename}保存された 1 ファイル (画像 / 動画 / etc.)
GET/jobs/{id}/assets.jsonassets メタ (name, size, mime, source_url, page_url) のリスト
GET/jobs/{id}/gallery.json(deprecated) assets.json のエイリアス。旧クライアント互換用
GET/jobs/{id}/galleryギャラリー HTML ページ (ブラウザ閲覧用)
GET/jobs/{id}/page.htmlレンダリング後の最終 HTML
GET/jobs/{id}/log.txt完全ログ (plain text)
GET/jobs/{id}/logLive ログビューアー HTML
GET/jobs/{id}/script.py最終的に走ったスクリプト
POST/jobs/{id}/cancelパウズ (state は保存)
DELETE/jobs/{id}完全削除 (assets ごと)
POST/jobs/{id}/assets★worker 専用★ asset アップロード (multipart)
POST/jobs/{id}/files/{kind}★worker 専用★ page.html / log 等のアップロード
POST/jobs/{id}/screenshot/captureUI から「いまのフレームを保存」
WS/jobs/{id}/eventsLive ログ + 進捗 (JSON over WS)

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_urlstringnull開いた直後に遷移する URL (省略時は about:blank)
parent_job_idstringnullこのセッションを紐づける親 job (capture / get_state の保存先)
worker_idstringnull特定 worker を指定 (省略時は自動選択)
lane_hintintnull特定 lane を指定
idle_ttl_sint300無操作タイムアウト秒 (POST /sessions 直接呼び出しのデフォルト。fetch keep_session=true 経由のセッションは 60 秒)
absolute_ttl_sint3600絶対タイムアウト秒
cookies_fromstringnullホスト名指定で /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
  }'

ライフサイクル

POST/sessions↑ 詳細上記
GET/sessions全セッション一覧
GET/sessions/{sid}1 セッションの詳細
DELETE/sessions/{sid}セッション終了

Read-only inspection

GET/sessions/{sid}/state{url, title, lane_idx, visited_count}
GET/sessions/{sid}/outlineアクセシビリティ要素一覧 ([@N] タグ付き)
GET/sessions/{sid}/links<a href> 絶対 URL (skip javascript:/mailto:/etc.)
GET/sessions/{sid}/visitedこのセッションが踏んだ URL
GET/sessions/{sid}/screenshot現在の Viewport PNG
GET/sessions/{sid}/cookies現在の Cookie jar 全部

Actions

POST/sessions/{sid}/navigatepage.goto(url)
POST/sessions/{sid}/clickpage.click(selector)
POST/sessions/{sid}/fillpage.fill(selector, value)
POST/sessions/{sid}/presspage.press(key, count, modifiers)
POST/sessions/{sid}/typepage.type(text) ←現フォーカス要素に挿入
POST/sessions/{sid}/scrollpage.scroll(direction, pixels)
POST/sessions/{sid}/backpage.back() ← history boundary 安全
POST/sessions/{sid}/forwardpage.forward() ← Back の対称、末尾でも安全
POST/sessions/{sid}/history_firstpage.history_first() ← 履歴の 0 番目 (= initial_url) に戻る
POST/sessions/{sid}/existspage.exists(selector) → bool。CSS セレクタの存在チェック (LLM 不要)
POST/sessions/{sid}/askpage.ask(question) → bool。LLM (Qwen) に yes/no 質問
POST/sessions/{sid}/agentLLM agent loop (engine: auto / qwen / cogagent)
POST/sessions/{sid}/captureHTML + screenshot + outline を保存
POST/sessions/{sid}/download_videoyt-dlp 起動して asset 化 (timeout_s 指定可)
POST/sessions/{sid}/save_cookies_to_host現 Cookie を /hosts/{host} に保存

Persistent state (key-value)

GET/sessions/{sid}/state/{key}page.get_state(key) ← parent job に紐づく永続データ
PUT/sessions/{sid}/state/{key}page.set_state(key, value)

Actions の body / response 詳細

各 action は POST /sessions/{sid}/<name>。すべて共通 response shape:

{
  "status": "OK" | "NO_MATCH" | "ERR: ...",
  "elapsed_ms": 1234,
  "result": { ... }   // action ごとに異なる
}
ActionBody備考
/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"}

その他の Workers エンドポイント

WS/workers/{wid}/linkworker ↔ hub 双方向制御 (WORKER_SECRET 認証、ハンドシェイクで WorkerRegisterHubRegistered)
GET/workers/{wid}/lanes/{idx}/screenshot1 lane の Xvfb スクショ JPEG。query: ?width=1280&quality=70
GET/worker-source.tar.gz★self-update 用★ server/ + 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 で自動注入する仕組み。

フィールド説明
hoststring正規化済みホスト名 (www. 除去、lowercase)。example.comwww.example.com は同じレコード
cookieslistCDP Network.Cookie 形式の配列
notestringUI に表示する任意メモ
visited_countintこのホストでの訪問済み URL 件数
updated_atISO 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+ 件を想定したページャ付き。

フィールド説明
namestringpreset 名 (ファイル名にもなる)
categorystringUI 上のグループ化用ラベル
descriptionstring1 行説明
ui_modestringfetch / ai / code
ai_enginestringcodegen / simple
urlstring初期 URL
goalstringcodegen の goal テキスト
simple_rowslistsimple macro の行 ({action, detail})
code_scriptstringcode mode の Python ソース
optionsJobOptionsPOST /jobs に直接渡す snapshot
created_at / updated_at / last_used_atISO 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 に含めることで、毎回ゼロから書かせるよりも安定して動くスクリプトが出る。

フィールド説明
slugstringkebab-case の ID
titlestringUI 表示名
descriptionstring1 行説明 (retrieval 用)
bodystringPython ソース (LLM が改変前提のテンプレ)
tagslist[string]絞り込み用
promotedbooltrue = 常に 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 だけは固定)。

フィールド説明
slugstring主キー (kebab-case)。engine="slug" の参照に使う
namestringUI 表示用ラベル
kindenumchat / vision-chat / gui-agent
protocolenumopenai / anthropic (予約) / agent-service / cogagent
endpointstringベース URL (trailing / 不要)
modelstringmodel 名 (OpenAI model= パラメータに渡る)
api_key_envstringenv 変数の名前 (値そのものは JSON に保存しない)
headersdict追加 HTTP header
timeout_sintHTTP timeout 秒
promotedboolengine="auto" 時に同 kind で優先する
builtinbooltrue = 組み込み (削除不可、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 キーの取り扱い ファイルには api_key_env: "ANTHROPIC_API_KEY" のように env 変数名のみ記録され、値は .env でホストごとに設定。 これにより registry の JSON は安全にバックアップ・共有でき、キーローテーションは .env 編集 + hub 再起動だけで完結します。

Settings — hub 全体設定

主な field:

フィールド説明
codegen_skill_top_kintcodegen retrieval の top-K (デフォルト 1)
codegen_convention_top_kintconvention retrieval の top-K
min_asset_size_bytesintJobOptions のフォールバック値
vision_max_steps_defaultintvision-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:

フィールド説明
goalstring自然言語タスク (必須)
initial_urlstring起点 URL (prompt の文脈に入る)
retry_contextstring過去 attempt の stderr / 結果 (再試行プロンプト用)
top_k_skillsintretrieval 数の上書き

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

GET/infoサーバ情報 (plain text)
GET/healthliveness probe
POST/admin/cleanup_jobs古い job を一括削除
GET/icon.svgパプリカロゴ (黄色)
GET/管理ダッシュボード HTML
GET/screenshotsLive スクショグリッド HTML
GET/{full_url:path}★ブックマークレット★ https://paprika.lan/https://example.com でアドレスバーから直接 fetch

7. 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 → dictnavigate
page.click(selector)str → dictCSS セレクタクリック
page.fill(sel, value)str, str → dictinput に値セット
page.press(key, count, modifiers)→ dict"Enter", "Ctrl+A", etc.
page.type(text)str → dict現フォーカス要素に挿入
page.scroll(dir, pixels)"down"/"up"/"left"/"right", int → dictCDP マウスホイール
page.wait_for(seconds=N)→ Noneasyncio.sleep ラッパ
page.back()→ dicthistory 境界に安全
page.forward()→ dictBack の対称、末尾でも安全
page.history_first()→ dict履歴 0 番目 (initial_url) に戻る
page.exists(selector)→ boolCSS セレクタの存在チェック (LLM 不要、確定的)
page.ask(question, engine="qwen")→ boolLLM に yes/no 質問。if 分岐用
page.capture(label)str → dictHTML + screenshot + outline 保存
page.download_video(url=None, timeout_s=1800)→ dictyt-dlp 起動
page.agent(goal, max_steps=5, engine="auto")→ dictLLM ループ 1 回
page.state()→ dicturl, title, etc.
page.outline()→ strアクセシビリティツリー
page.links(urls_only=False)→ list[dict] or list[str]<a href> 絶対 URL
page.screenshot(path=None)→ bytesPNG
page.cookies()→ list[dict]現在の jar
page.get_state(key) / page.set_state(key, value)→ Anyparent 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-loopLLM がスクリプトを書いて sandbox 実行「このサイトを巡回」のような大きめタスクをまるごと丸投げ
mode=vision-agentCogAgent が画面を見て pixel 座標でクリックCSS セレクタが効かない / 動的レンダリング / 視覚情報必須のサイト

8.1 page.agent() — スクリプト内で 1 ステップ LLM

script の流れは自分で書きつつ、「ここだけ LLM の判断に任せる」というハイブリッド。engine で使う LLM を選ぶ:

サンプル: ギャラリーの写真を順にめくる

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")
page.agent() の戻り値
{
  "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 /jobsmode として使う。

サンプル: 自然言語クロール

# 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 使い分けのコツ

状況おすすめ理由
確実な制御 + 一部だけ LLMpage.agent()スクリプトの流れは自分で握れる + 失敗時の rollback が書ける
「とりあえずやってみて」codegen-loopretry も自動。試行錯誤コストが安い
セレクタが取れないサイトvision-agent or page.agent(engine="cogagent")画像認識ベース。Canvas / shadow DOM / iframe 混在 OK
JP の goal を出す何でも OKworker 側で自動英訳 (Qwen) → LLM 渡し
動画ダウンロードpage.download_video() 直呼びyt-dlp 起動。LLM 介す必要なし
大量並列実行codegen で 1 スクリプト書く → preset 保存 → cronsafe + 再現性 + ログが揃う
注意: ループ内の page.agent() は高い CogAgent は 1 step あたり 2-3 秒 + GPU リソース消費。10 回ループなら 30 秒+。loop 上限と timeout は慎重に。

9. Simple Macro #

コードを書かなくても GUI で簡単マクロが組める機能。Submit タブ → AI → Simple モード で表示。

Actionコンパイル先
Navigateawait 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)
Typeawait page.type(text)
Fillawait page.fill(selector, value)
Press keyawait page.press(key, count=N)
Scrollawait page.scroll(dir, px)
Waitawait page.wait_for(seconds=N)
Backawait page.back()
Forwardawait page.forward()
履歴の最初へawait page.history_first()
Captureawait page.capture(label)
Agentawait page.agent(goal, max_steps=5)
Download videoawait page.download_video(url=...)
Loop (begin) / End loopfor i in range(N): ... ブロック
If (CSS) / Else / End ifif await page.exists(selector): ... else: ...
If (Agent) / Else / End ifif await page.ask(question, engine="qwen"): ... else: ...

条件分岐 (If 行)

If (CSS)If (Agent) の 2 種類を使い分けます:

使用例 (ログイン処理):

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")
If (Agent) の信頼性 LLM 呼び出しのため誤判定はゼロにはなりません (典型的に 10-20% の error rate)。重要な分岐 (削除 / 課金) は CSS 版で担保し、LLM 版は副作用のない判断 (Capture するか / Skip するか) に留めるのが安全。Worker 側で「unparseable な回答は 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 を比較。不一致なら自動で更新サイクルが回る:

  1. Hub が HubRegistered.expected_worker_version を返す
  2. worker が mismatch を検知
  3. GET /worker-source.tar.gz で hub から最新ソースを取得
  4. /app/server/, /app/core/, /app/VERSION に展開 (in-place、削除されたファイルも prune)
  5. exit code 42 で終了
  6. Docker restart policy で再起動 → 新コードで boot

version 算出は /app/server + /app/core の全 .py ファイルの SHA-256 (先頭 12 hex)。手動 VERSION bump 不要。コード編集 → git pulldocker compose restart hub だけで、hub のハッシュが変わる。worker は次の handshake で旧ハッシュとの差を検知し、tarball 経由で自分の source を hub と同一内容に書き換える → 直後のハッシュ計算で一致 → ループせず収束。

旧設計 (VERSION ファイル文字列比較) からの違い: 以前は repo の VERSION = "dev" のままだと両側 dev で常に no-op、誰も気づかぬまま自動更新が永久発火しなかった。コンテンツハッシュ式は VERSION ファイルが空でも、bind-mount しか無くても、デプロイのたびに自然に新ハッシュが立つ。

Fallback: source dir が無い等で hash が算出できない場合は /app/VERSIONPAPRIKA_VERSION env → "dev" の順。両側が "dev" のときだけ比較スキップ。片方だけ "dev" なら mismatch として扱う ("自分の version を知らない側" が常に追従)。

環境変数デフォルト説明
PAPRIKA_WORKER_AUTO_FETCH_SOURCE1tarball 自動取得 + 展開
PAPRIKA_WORKER_AUTO_EXIT_ON_VERSION_MISMATCH1不一致で 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 に繋がらない

  1. docker logs paprika-worker-1 でログ確認
  2. HUB_URL=ws://paprika.lan が正しいか
  3. curl http://paprika.lan/health で hub 到達性
  4. WORKER_SECRET 不一致だと bad worker secret で 1008 close

セッションが 404 になる

長時間ノーアクションだと session reaper が evict します。デフォルトの idle_ttl_s:

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 のまま

  1. GET /workersalive=true な worker がいるか
  2. worker が drain / standby になってないか (POST /workers/{wid}/status で確認)
  3. 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 に注入される。

動画ファイルが落ちてこない

NAS 移行で hub が固まる (将来)

SMB / NFS マウントは必ず soft オプションで。hard mount だとネットワーク断時に I/O が永遠ハングし、FastAPI イベントループ全体が止まる。

12. 環境変数一覧 #

Hub 側

変数デフォルト説明
PAPRIKA_VERSION(file)hub の VERSION 上書き
PUBLIC_BASE_URLhttp://hub:8000worker に教える hub URL
WORKER_SECRET(無)worker WS handshake 認証
CODEGEN_LLM_URLhttp://:15082codegen 用 LLM endpoint
CODEGEN_MODEL_NAMEqwen2.5-vl-72bcodegen 用 model
PAPRIKA_RUNNER_IMAGEpaprika-runner:latestrunner コンテナイメージ
PAPRIKA_RUNNER_MAX_CONCURRENT3同時 runner 数上限

Worker 側

変数デフォルト説明
HUB_URLws://paprika.lan:8000hub の WS URL
WORKER_IDauto固定したい場合のみ
WORKER_SECRET(無)hub と一致が必要
LANE_POOL(file:SLOT_POOL)Lane (ブラウザ) 数
MAX_CONCURRENT2同時ジョブ数
NOVNC_PUBLIC_HOSTlocalhostnoVNC URL のホスト部 (hub 自動補正で上書きされる)
NOVNC_BASE_PORT6080Lane 0 の noVNC ポート
AGENT_URLhttp://agent:8001page.agent 用 LLM proxy
AGENT_LLM_URLhttp://:15082翻訳・コード生成 LLM の直 URL
COGAGENT_URLhttp://:15083vision-agent endpoint
PAPRIKA_WORKER_ID_FILE~/.paprika/worker_idworker_id 永続化先 (Windows 含む)
PAPRIKA_WORKER_AUTO_FETCH_SOURCE1tarball 自動取得
PAPRIKA_WORKER_AUTO_EXIT_ON_VERSION_MISMATCH1不一致時 exit 42

paprika-client / Runner

変数デフォルト説明
PAPRIKA_JOB_ID(無)runner が session に紐づける parent job ID (自動セット)
PAPRIKA_CLIENT_ACTION_LOG1[paprika] page.X(...) ログを出すか

13. 最近の変更 #

14. 今後の開発予定 #

現時点で検討中・着手予定のもの。太字 = 着手中、それ以外は構想段階。

ストレージ

API / SDK

LLM / Agent

運用 / 可観測性

UI

その他

提案 / リクエスト歓迎 GitHub Issues に「こういう機能が欲しい」「ここがハマった」など投げてもらえると次の優先順位に乗ります。