アーキテクチャ

Paprika の内部構造を 1 ページで通読。全体構成 (Client → Router → Hub → Worker → Lane)、Hub のジョブディスパッチと 3 つのモード、Worker の Lane プールと passive キャプチャ、3 つのストア、自己回復・自己更新まで。

Paprika は「分散したワーカー上の Chrome を、API / SDK / AI から動かしてページの画像・動画・リンクを集める」プラットフォームです。このページで全体像 → Hub の中身 → Worker の中身 → ストア → スケール構成 までを通読できます。

管理画面の Submit タブ — Client の入口の 1 つ

クライアント面(管理画面の 実行 タブ)。ここから投入されたジョブが、下の図の Hub → Worker → Lane を経て返ってきます。

Client / SDK / curl POST /jobs ・ GET /jobs/{id} ・ /sessions/* Router(nginx) 複数 Hub のときだけ前段に立つ Hub ジョブ受付・ディスパッチ セッション登録(Chrome は持たない) Worker(別ホスト × 多数) Lane(並列 N 本) Xvfb + Chrome (CDP) x11vnc + noVNC = 画面を持つ本物の Chrome WebSocket assign / 結果 保存 ストア DB ・ オブジェクトストレージ ・ Redis

5 つの構成要素

要素 役割
Client / SDK あなたのコード。Python / PHP SDK、curl、または管理画面からジョブを投入します(HTTP API)。
Router(nginx) 複数 Hub 構成のときだけ前段に立ち、リクエストを各 Hub に振り分けます。単一 Hub なら不要で、Client は Hub に直接話します。
Hub 司令塔。ジョブを受け付け、空いている Worker に割り当て(ディスパッチ)、セッションを登録し、結果をストアに保存します。Chrome は持ちません。
Worker 実際にブラウザを動かすホスト(多数)。起動時に Lane を N 本立ち上げ、Hub から WebSocket でジョブを受け取って実行します。
Lane(Chrome) 1 本 = 並列実行の 1 トラック。専用の Xvfb 画面 + Chrome + noVNC を持つ長命のブラウザ。クッキー/ログイン状態を保ったままジョブが通過します。

リクエストの流れ(Fetch ジョブの例)

  1. 投入 — Client が POST /jobs。(複数 Hub なら) Router がいずれかの Hub に振る。
  2. 割り当て — Hub が空き Worker を選び、WebSocket で送る(満杯なら 503)。割り当ての選定基準や、取りこぼした queued ジョブを救う仕組みは ジョブ分配と負荷分散 を参照。
  3. Lane 確保 — Worker が空き Lane を 1 本確保し、その Chrome でページを開く。
  4. 取得 — スクロール・待機しながら画像/動画/リンクを収集(動画は通信トレース + yt-dlp)。
  5. 保存 — 集めたアセットを Hub に アップロード → ストアへ保存。
  6. 取得 — Client は GET /jobs/{id} で完了を待ち、assets.json から結果を取る。

AI モード(codegen-loop)では、Hub 自身が「LLM がスクリプトを生成 → サンドボックスで実行 → 失敗時に再生成」のループを回し、生成スクリプトが Hub の /sessions/* に接続して Worker の Lane を駆動します(後述)。


Hub の仕組み

Hub は司令塔です。Chrome は持ちません — クライアント API・Worker の WebSocket・管理画面を束ね、ジョブを Worker に割り当て、結果をストアに保存します。

管理画面のジョブ一覧 — Hub が受け付けたジョブの状態が一覧表示

Hub が受け付けたジョブは 最近のジョブ タブで一覧できます。状態・ワーカー・所要時間・取得物数を一目で確認。

ジョブの割り当て

 POST /jobs
   │  ① URL 検査・JobInfo を queued で保存
   ▼
 空き Worker を選ぶ (pick_worker)
   │  満杯なら数秒待つ → それでも無ければ 503
   ▼
 WebSocket で HubAssignJob を送信 ──▶ Worker が Lane を確保して実行
   │
   ▼  worker_id・noVNC URL を JobInfo に記録 → 返却

3 つのジョブモード

モード Hub が何をするか
fetch その場で Worker にディスパッチ。Worker の取得エンジン(nodriver)がページを開いて画像/動画/リンクを収集。レシピがあれば適用。
codegen-loop(AI) Hub 自身がループを回す — LLM がスクリプトを生成 → サンドボックスで実行 → 失敗なら再生成。生成スクリプトは Hub の /sessions/* に接続して Worker の Lane を駆動。→ 下記
rerun(Code) 保存済み / インラインのスクリプトを LLM 抜きで実行(codegen-loop と同じ実行経路)。

fetch は同期的にディスパッチして即返し、codegen-loop / rerun は Hub 内の非同期タスクとして起動して即 job_id を返します(だから投入は速い)。

codegen-loop(AI モード){#codegen-loop}

 goal(自然言語)
   │
   ▼
 ┌─ planner(LLM) → スクリプト生成
 │      ▼
 │  サンドボックスで実行 ──▶ /sessions/* ──▶ Worker の Lane(Chrome)
 │      ▼
 └─ 失敗なら judge(LLM) で原因分析 → 再生成(max_codegen_attempts まで)

Hub がブラウザを直接触るのではなく、生成されたスクリプトが SDK と同じ経路(/sessions/*)で Worker の Lane を操作します。これにより AI モードも手書きスクリプトも同じ実行基盤に乗ります。

セッションレジストリ

「セッション」は Lane の予約です。session_id → (worker, lane) を Hub が管理し、/sessions/{sid}/click などの操作を所有 Worker の Lane へ WebSocket で転送します。

Worker の登録と定期通知

Worker は Hub の /workers/{id}/link に WebSocket でつなぎ、機能情報(Lane 数・各 Lane の noVNC URL・バージョンハッシュなど)を送って登録します。以後 約 15 秒ごとに状態通知 を送り、実行中ジョブの本数・CPU/メモリ/ディスクの状況を更新します。


Worker・Lane・Chrome の仕組み

Worker は実際にブラウザを動かすホストです。自律的に動き、--hub-url を渡すだけで 自分を登録 → 心拍 → 割り当てジョブを実行 → 完了報告まで行います。

管理画面のワーカー一覧 — 接続中のワーカーと各 Lane の状態が一覧表示

管理画面の ワーカー タブ。各ワーカーの Lane 数・状態・バージョン・所属 Hub が一目で分かります。

Lane プール

Worker は起動時に N 本の「Lane」を先に立ち上げて常駐させます(プール)。Lane は空のスロットではなく、長命でステートを持つブラウザです。

 Worker ホスト
 ├─ agent プロセス ──── WebSocket ───▶ Hub
 └─ Lane プール(起動時から常駐)
     ├─ Lane 0
     ├─ Lane 1
     └─ … (= 並列実行できるジョブ数)

1 本の Lane の中身

各 Lane は独立した X 画面と、それを覗くための VNC 一式を持ちます。

 Lane i
  ├─ Xvfb              :100+i   仮想ディスプレイ(画面はあるが物理モニタは無い)
  ├─ fluxbox                    軽量ウィンドウマネージャ
  ├─ Chrome            :9223+i  remote-debugging(CDP) ポート ← ここを操作する
  ├─ x11vnc            :5901+i  Xvfb の画面を VNC で配信
  └─ noVNC(websockify) :6080+i  ブラウザから見られる形に橋渡し

つまり「画面を持つ本物の Chrome」を、モニタの無いサーバ上で動かしているだけです。だから JavaScript・動画再生・各種ダイアログなど、実ブラウザと同じ挙動になります。

「操作」と「閲覧」は別経路

Lane の Chrome に対して、操作(自動化)ライブ閲覧 は別の経路で行われます。

 Hub / スクリプト ──── CDP ───────────────────────▶ Chrome を操作(クリック/取得/JS)
 操作者のブラウザ ─ noVNC(websockify) ─▶ x11vnc ─▶ Xvfb 画面 ⟵ Chrome が描画

アセットの取り方(passive network capture)

Paprika は HTML をパースして <img src= loading="lazy"> を読み、その URL に再リクエストする方式ではありません。CDP の Network.responseReceived イベントを passive にサブスクライブして、ブラウザが実際にダウンロードしたレスポンス本体をそのまま回収します。

通常のスクレイパ:  ページ取得 → HTML パース → <img src loading="lazy"> から URL 取り出し → 改めてその URL に GET
Paprika:           ページ取得 → Chrome が画像をロード → CDP イベントで Paprika がレスポンスを横取り

この方式の利点:

しきい値(min_asset_size_bytes)はこの passive リスナー段で適用されるので、アイコンやスペーサーの 100 byte 画像は最初から記録に上がりません(JobOptions)。

ジョブとセッション

  Lane の使い方
ジョブ(fetch) 1 本の Lane を確保 → 取得 → 解放。短時間で完結。
セッション Lane を予約し続け、Hub 経由の /sessions/{sid}/* で対話的・スクリプト的に操作。codegen-loop / SDK の Page / keep_session な fetch が使う。アイドル TTL・絶対 TTL で自動解放。

自己回復(self-healing)

Worker は「壊れたら自分で気づいて作り直す」設計です(詳細は Worker 自己回復)。

自己更新(self-update){#self-update}

Hub はワーカー用のソースを配布し、バージョンハッシュを WebSocket ハンドシェイクで突き合わせます。

 接続 → Hub のバージョンと比較
   ├─ 一致   → そのまま稼働
   └─ 不一致 → Hub からソースを取得 → sys.exit(42) → スーパーバイザが再起動

server/*.py を直すだけで 手動 rsync 不要でフリート全体が追従します(コード変更時の運用は ワーカー自動配信)。


3 つのストア

単一 Hub では Hub のローカルディスク + 任意の DB で完結します。複数 Hub では状態を共有するため 3 つに分かれます:

ストア 持つもの
DB(MariaDB 等) ジョブ情報、各種レジストリ(ホスト設定・プリセット・スキル等)
オブジェクトストレージ(MinIO / S3) 収集アセット(画像・動画・HTML)の本体
Redis 協調用の揮発状態 — ワーカー登録、セッションマップ(sid→所有 Hub)、リース、ライブプレビューのフレーム

これにより Hub は水平スケール可能(クローン安全) で、どの Hub に当たっても同じジョブ・アセットが見えます。

Router(nginx)と複数 Hub

Worker の制御 WebSocket(/workers/{id}/link)だけは worker_id のハッシュで決まった Hub に常に届く ように振り分けます。それ以外のリクエストは各 Hub に 順番に振り分け (round-robin) します。

スケールの考え方は Hub スケーリング、サーバー構成は サーバー構成 も参照。


関連

ページ 内容
ジョブ分配と負荷分散 空き Worker の選び方、滞留ジョブの再配置ループ、取り残されたジョブの整合化処理、複数 Hub 構成での経路設計
Vision AI とマウス 視覚エージェント(CogAgent / Qwen-VL)がスクリーンショットを見てピクセル座標でクリック・操作するしくみ
VNC 埋め込み hub-proxy の noVNC ライブ画面を、自前の Web ページに iframe で埋め込む実装
Hub スケーリング 複数 Hub に水平スケールするときの考え方とルーティング
Worker 自己回復 Worker が壊れたら自分で気づいて作り直す自己回復ループ
Worker 自動配信 server コードを変えただけでフリート全体が追従する仕組み
サーバー構成 A. 1 台で全部 / B. ハブ + リモート Worker / C. Hub のみ