Skip to content

Latest commit

 

History

History
941 lines (648 loc) · 52.2 KB

File metadata and controls

941 lines (648 loc) · 52.2 KB

Python API リファレンス

English | 日本語

vrcpilot.<name> で公開されているすべてのシンボルを手作業でまとめたリファレンスです。実行可能な例は usage.ja.md を、同等の CLI については cli.ja.md を参照してください。関数シグネチャは 0.2.0rc1 時点のソースに対応します。

規約

  • vrcpilot.<name> のシンボルはすべて src/vrcpilot/__init__.py::__all__ に列挙されています。
  • モジュール属性 vrcpilot.keyboardvrcpilot.mousevrcpilot.clipboard も公開 API の一部です。
  • 合成入力を送ったり VRChat ウィンドウと対話したりする呼び出しのほとんどは、VRChat が起動中かつフォーカスされていることを前提としています。この要件は ensure_target() によって強制され、高レベルヘルパーは内部で呼び出します。関連する例外(VRChatNotRunningErrorVRChatNotFocusedError)は呼び出し側でリカバーできるように再送出されます。
  • 座標を持つ型(OCRWordOCRResultDetectionDetectResult)は ウィンドウローカル な座標(polygon / bbox、原点は VRChat ウィンドウの左上)のみを公開します。mouse.move(x, y) も同じウィンドウローカル系を受け取るため、OCR / detect の bbox はそのまま渡せます — coordinate system を参照してください。
  • コードブロックでは、Python REPL やスタブファイルに貼り付けやすいよう、すべてのシグネチャの本体に ... を使用しています。

パッケージメタデータ

vrcpilot.__version__

importlib.metadata 経由でインストール済みディストリビューションのメタデータから解決されるため、pyproject.toml のパッケージバージョンと常に同期します。


プロセス制御

vrcpilot.launch

def launch(
    *,
    app_id: int = 438100,
    steam_path: Path | None = None,
    no_vr: bool = False,
    screen_width: int | None = None,
    screen_height: int | None = None,
    osc: OscConfig | None = None,
    extra_args: list[str] | None = None,
    wait_timeout: float = 30.0,
    wait_interval: float = 1.0,
) -> int | None: ...

Steam 経由で VRChat を起動します。新しいプロセスは呼び出し側のプロセスグループから切り離されます。Steam を起動した後、launch() は最大 wait_timeout 秒(デフォルト 30 秒)の間 find_pids() をポーリングし、観測した PID を返します。wait_timeout=0(または非正の値)を渡すと待機をスキップしてすぐに復帰します。これは、自分で後からポーリングするつもりの「fire and forget」型の起動に便利です。

Returns: VRChat が観測された時点での PID、もしくは wait_timeout <= 0 の場合やタイムアウトを超えた場合は None。正のタイムアウトで None が返ること自体は例外ではありません — より厳密なシグナルが必要であれば戻り値で分岐してください。

app_id はデフォルトで VRChat の Steam app id です。カスタムの起動ラッパーを組み立てる際などにこの定数を直接参照したい場合は、実装モジュールからインポートしてください: from vrcpilot.process import VRCHAT_STEAM_APP_ID

Raises: Steam の実行ファイルが見つからない場合は SteamNotFoundError

vrcpilot.terminate

def terminate(*, timeout: float = 5.0) -> list[int]: ...

実行中の VRChat プロセスをすべて強制終了し、最大 timeout 秒間それらの終了を待ちます。冪等で、何も実行されていない場合は空のリストを返します。

Returns: シグナルを送った PID のリスト。

vrcpilot.find_pids

def find_pids() -> list[int]: ...

Returns: 実行中の全 VRChat プロセスの PID。psutil.Process.create_time() の降順(新しいものが先頭)で並びます。VRChat が起動していない場合は空リストになります。

vrcpilot.OscConfig

@dataclass(frozen=True)
class OscConfig:
    in_port: int = 9000
    out_ip: str = "127.0.0.1"
    out_port: int = 9001

    def to_launch_arg(self) -> str: ...

VRChat の --osc=<in>:<ip>:<out> フラグの構造化表現です。to_launch_arg() は起動時に使用される単一の CLI トークンを生成します。

vrcpilot.SteamNotFoundError

Steam の実行ファイルが見つからないときに launch()(および Steam 検出ヘルパー)から送出されます。


ウィンドウ制御

vrcpilot.focus

def focus() -> bool: ...

VRChat ウィンドウを前面に出します(必要に応じて最小化状態も解除します)。

Returns: 成功時は True。VRChat が起動していない、ウィンドウがマップされていない、プラットフォーム呼び出しが失敗した、または Wayland ネイティブセッションである場合は False

Raises: Windows / Linux 以外のプラットフォームでは NotImplementedError

vrcpilot.unfocus

def unfocus() -> bool: ...

他のウィンドウを前面に上げることなく VRChat ウィンドウを z オーダーの最下部へ送ります。戻り値と例外の契約は focus() と同じです。

vrcpilot.is_foreground

def is_foreground() -> bool: ...

Returns: VRChat ウィンドウが現在前面にあるときに限り True


画面キャプチャ

vrcpilot.capture.Capture

class Capture:
    def __init__(self, *, frame_timeout: float = 2.0) -> None: ...
    def read(self) -> np.ndarray: ...
    def close(self) -> None: ...

VRChat ウィンドウのストリーミングキャプチャセッションです。フォーカスを必要としません。内部バッファは最新フレームのみを保持するため、read() は常に「現在」のフレームを返します。

  • read()(H, W, 3)uint8 RGB を返します。
  • close() は冪等で、例外を投げません。
  • with(コンテキストマネージャ)に対応しています。
  • frame_timeout は 1 フレームあたりの待機秒数で、> 0 でなければなりません。

Raises:

  • Windows / Linux 以外のプラットフォームでは NotImplementedError
  • バックエンドが起動できない場合(VRChat が起動していない、ウィンドウが未マップ、X11 が利用できない、WGC セッション失敗、Wayland ネイティブ)は RuntimeError
  • frame_timeout <= 0 の場合は ValueError

vrcpilot.CaptureLoop

class CaptureLoop:
    def __init__(
        self,
        callback: Callable[[np.ndarray], None],
        *,
        fps: float,
        frame_timeout: float = 2.0,
    ) -> None: ...

    @property
    def is_running(self) -> bool: ...

    def start(self) -> None: ...
    def stop(self) -> None: ...
    def close(self) -> None: ...

バックグラウンドスレッドで Capture を一定の fps で駆動します。各フレームは (H, W, 3)uint8 RGB として callback に渡されます。with に対応しています。

Raises: fps または frame_timeout が非正の場合は ValueError。内部の Capture が起動できない場合は RuntimeError。サポート外のプラットフォームでは NotImplementedError

CLI の vrcpilot record コマンドは、内部の PyAV ベースの muxer (vrcpilot.cli.record.muxer、公開 API ではありません) を使って CaptureLoop(映像用)と SpeakerLoop(音声用)を組み合わせています。独自のライターを作るには、(H, W, 3)uint8 RGB フレームを消費する callback を渡してください — 例えば PyAV や ffmpeg サブプロセスをラップして、必要な任意のコンテナに mux できます。


Speaker(音声キャプチャ)

VRChat のプロセス単位音声キャプチャです。Linux ではバックエンドにネイティブ PipeWire パイプライン(仮想 null-sink + pw-link + pw-record)を、Windows ではシステム全体のミックスではなく単一の PID の音声をタップするネイティブ拡張 proc-tap を使用します。どちらのバックエンドでも、ストリームには VRChat の出力のみ が含まれます — Discord、OBS、その他のアプリは混ざりません。import vrcpilot はそれ以外の sys.platformImportError を送出するため、他の Speaker バックエンドが届く経路はありません。

バックエンドは 48 kHz ステレオで float32 (N, CHANNELS) のチャンクを生成します。CLI の vrcpilot record コマンドはこれらを内部の PyAV ベースのライター(vrcpilot.cli.record.muxer、公開 API ではありません)で mux しています。独自のコードから音声を永続化するには、任意のライター — 例えば PyAV (WAVMP4MKV、…) や ffmpeg サブプロセス — にチャンクを流し込んでください。(N, 2) float32 のレイアウトは PyAV の planar / packed float フレームへ綺麗にマップできます。

vrcpilot.speaker.Speaker

class Speaker:
    def __init__(self, *, read_timeout: float = 2.0) -> None: ...
    def read(self) -> NDArray[np.float32]: ...
    def close(self) -> None: ...

コンテキストマネージャ形式のキャプチャセッションです。コンストラクタを呼び出した時点で VRChat が既に起動している必要があり、そうでなければ RuntimeError を送出します。各 read() は前回の呼び出し以降にバッファされたすべてのサンプルを (N, 2)float32 ndarray として返します。N == 0 は有効な「新しい音声なし」シグナルです(静かなストリームで read_timeout が満了した場合に返ります)。close() は冪等で、例外を投げません。with に対応しています。

Raises:

  • VRChat が起動していない場合や Speaker バックエンド(Linux の PipeWire パイプライン、Windows の proc-tap)が起動できない場合は RuntimeError
  • read_timeout <= 0 の場合は ValueError

vrcpilot.speaker.SpeakerLoop

class SpeakerLoop:
    def __init__(
        self,
        callback: AudioCallback,
        *,
        chunk_seconds: float = 0.05,
        read_timeout: float = 2.0,
    ) -> None: ...

    @property
    def is_running(self) -> bool: ...

    def start(self) -> None: ...
    def stop(self) -> None: ...
    def close(self) -> None: ...

Speaker をラップするバックグラウンドスレッドドライバです。自前で Speaker を構築・所有するため、ループを生成する時点で VRChat が既に起動している必要があります。各 tick で 1 チャンクを取り出して callback に転送し、ワーカーは取得の合間に chunk_seconds だけスリープします(デフォルト 50 ms。バックエンドのバッファ周期に合わせて選定)。空チャンクもそのまま転送されるので、コンシューマ側で「沈黙 tick」として扱えます。コールバックや Speaker.read() で発生した例外は捕捉され、次の stop() / close() で再送出されるため、ワーカースレッドの失敗が握り潰されることはありません。with に対応しています。

Raises: chunk_seconds または read_timeout が非正の場合は ValueError。内部の Speaker から RuntimeError

vrcpilot.speaker.AudioCallback

type AudioCallback = Callable[[NDArray[np.float32]], None]

SpeakerLoop が受け取るチャンクコールバックのシグネチャです。各コールバック呼び出しは (N, 2) float32 のチャンクを 1 つ受け取ります。N == 0 のチャンクは沈黙 tick です。

エンドツーエンドのスニペット

SpeakerLoop(N, 2) float32 のチャンクを受け取る任意の callable を受け付けます。下の例はすべてを 1 つの ndarray に集めていますが、実コードではむしろ各チャンクをストリーミングライター(PyAV、ffmpeg サブプロセス、ネットワークソケットなど)に流し込むことになるでしょう。

import time

import numpy as np
from numpy.typing import NDArray

from vrcpilot.speaker import SpeakerLoop

chunks: list[NDArray[np.float32]] = []

# VRChat must already be running; SpeakerLoop raises RuntimeError otherwise.
with SpeakerLoop(chunks.append, chunk_seconds=0.05) as loop:
    loop.start()
    time.sleep(5.0)

audio = np.concatenate(chunks, axis=0) if chunks else np.empty((0, 2), np.float32)

録音を永続化するには、audio(または流れてくる各チャンク)を PyAV や標準の wave モジュール、もしくは CLI の vrcpilot record コマンドで書き出してください — cli.ja.md record を参照してください。


Speaker ルーティング(音声出力リレー)

vrcpilot.speaker.routing は、特定の VRChat PID からキャプチャした音声を任意の OS 出力デバイス(物理スピーカーや仮想ケーブルのシンク)へリアルタイムにリレーします。内部では既存の PID スコープド SpeakerLoop をキャプチャ側に、soundcard の出力プレイヤーを再生側に組み合わせており、Windows / Linux 共通のコードパスです(プラットフォーム差分は Speakersoundcard がそれぞれ吸収します)。VRChat を複数並列に起動して各インスタンスを別々の仮想出力へ振り分けたい、AI エージェントごとに音声経路を分離したい、といった「per-PID 音声分離」が主要なユースケースです。背景・配線手順・仮想ケーブル構成(VB-Audio Virtual Cable / PipeWire null-sink)については docs/virtual-audio.ja.md を参照してください。CLI 等価物は vrcpilot speaker list / vrcpilot speaker route です。

vrcpilot.speaker.routing.AudioDevice

@dataclass(frozen=True, slots=True)
class AudioDevice:
    id: str
    name: str
    is_default: bool

OS の出力スピーカーを表す不変ハンドルです。list_devices() / default_device() / find_device() の戻り値であり、Router / route()device 引数として受け取られます。

  • idsoundcard のバックエンド識別子(Windows ではエンドポイント GUID、Linux では PipeWire のノード識別子)。呼び出し側にとっては不透明な文字列で、find_deviceRouter にそのまま再投入する以外の用途はありません。
  • name — ユーザー向けのフレンドリ名(Windows では FriendlyName、Linux では PipeWire の node.description)。
  • is_default — OS のデフォルト出力デバイスである場合に限り True。単一の list_devices() 結果の中でこのフラグが立つデバイスは高々 1 つです。

frozen=True なのでスレッド間で解決済みデバイスを安全に受け渡せます。

vrcpilot.speaker.routing.list_devices

def list_devices() -> list[AudioDevice]: ...

可視な出力デバイスをすべて列挙します。並び順は「OS デフォルトが先頭、残りは name 昇順(Python の標準コードポイント比較で大文字小文字を区別)」で固定されているため、CLI 表示や e2e の列挙に直接使えます。出力デバイスが 1 つも無い環境では空リスト [] を返し、例外は送出しません。

Raises:

  • soundcard がインストールされていない場合は ImportError
  • soundcard がネイティブバックエンド(Linux では libpulse、Windows では WASAPI)をロードできない場合は OSError

vrcpilot.speaker.routing.default_device

def default_device() -> AudioDevice: ...

OS のデフォルト出力デバイスを AudioDevice として返します。Router(device=None) / route(device=None) が内部で呼ぶのと同じ解決ロジックです。

Raises:

  • 出力デバイスがこのシステムに 1 つも存在しない場合は DeviceNotFoundError
  • ImportError / OSErrorlist_devices() と同じ条件。

vrcpilot.speaker.routing.find_device

def find_device(query: str) -> AudioDevice: ...

query を単一の出力デバイスへ解決します。解決は 3 段階を順番に試し、いずれかの段階でユニークに一致した時点で確定します:

  1. query == device.id の完全一致。
  2. query == device.name の完全一致(大文字小文字を区別)。
  3. query.lower() in device.name.lower() の部分一致(大文字小文字を区別しない)。

いずれかの段階で 2 件以上にマッチした場合は、次の段階へフォールスルーせず即座に AudioRoutingError を送出します。曖昧なクエリは黙って 1 つに決められるのではなく、はっきり失敗します。Router(device="...") / route(device="...") も内部でこの関数を呼びます。

Raises:

  • いずれかの段階で 2 件以上にマッチした場合は AudioRoutingError
  • 3 段階すべてで 0 件だった場合は DeviceNotFoundError(メッセージには認識済みデバイス一覧が含まれます)。
  • ImportError / OSErrorlist_devices() と同じ条件。

vrcpilot.speaker.routing.Router

class Router:
    def __init__(
        self,
        pid: int,
        device: str | AudioDevice | None = None,
        *,
        chunk_seconds: float = 0.02,
        blocksize: int | None = None,
    ) -> None: ...

    @property
    def device(self) -> AudioDevice: ...
    @property
    def is_running(self) -> bool: ...

    def start(self) -> None: ...
    def stop(self) -> None: ...
    def close(self) -> None: ...

    def __enter__(self) -> Self: ...
    def __exit__(self, exc_type, exc_val, exc_tb) -> None: ...

単一の VRChat PID の音声を指定した出力デバイスへリレーするハンドルです。コンストラクタは device を解決して AudioDevice を内部に保持しますが、音声ストリームは開きません — 実際のリソース取得は start() が行います。

device の型による分岐:

  • Nonedefault_device() で OS のデフォルトを解決。
  • strfind_device(device) で 3 段階解決(id 完全 → name 完全 → name 部分一致、最後だけ大文字小文字無視)。
  • AudioDevice — そのまま使用(解決不要)。

pid は内部の SpeakerLoop にそのまま渡されます。None は受け付けません — 複数 VRChat を並列に走らせる構成では PID を明示する必要があります(曖昧な「現在の VRChat」自動解決はこの API の範囲外)。chunk_seconds はキャプチャ側 tick の秒数で SpeakerLoop に転送され、デフォルトは 0.02(20 ms、SpeakerLoop 単体のデフォルト 50 ms より細かい設定 — リレー用途では低遅延寄りに調整されています)。blocksizesoundcard のプレイヤーバッファのフレーム数で、None のときはバックエンドのデフォルトに任せます。

ライフサイクル: start()プレイヤーを先に開き、その後で SpeakerLoop を起動 します(最初のコールバックが届いた時点で出力ストリームが必ず準備済みになるように)。SpeakerLoop 側の起動が失敗した場合は、既に開いたプレイヤーをロールバックしてから例外を伝播するので、中途半端なリソースが残ることはありません。stop() は逆順で、ループ停止を try に、プレイヤー解放を finally に置いているため、ワーカースレッドが捕捉した例外(例: VRChat がリレー途中に終了)を再送出する場合でも出力デバイスは必ず解放されます。start() / stop() の二重呼び出しはいずれも no-op です。stop() 後のインスタンスは再利用可能で、start() を再度呼べば新しい SpeakerLoop とプレイヤーが作り直されます(同じ Routerwith ブロックを再入することも安全です)。

close()stop() のエイリアスです。コンテキストマネージャプロトコル (__enter__ / __exit__) にも対応しており、本体側の例外を握り潰しません — ワーカースレッド由来の例外が stop() から再送出された場合は標準の __exit__ チェーンに従って本体の例外を置き換えます。

スレッドモデル: start() / stop() はメインスレッドから呼ぶことを想定しています。内部のフレームコールバックは SpeakerLoop のワーカースレッド上で動き、stop() と競合した場合はプレイヤースロットの None スナップショットを見て黙ってスキップするため、ロックなしで安全に止められます。

Raises (start() から):

  • 内部の SpeakerLoop / Speaker の起動失敗(例: VRChat が起動していない)は RuntimeError
  • chunk_seconds <= 0 の場合は ValueErrorSpeakerLoop.__init__ 由来)。
  • soundcard のプレイヤーオープン失敗(解決と start() の間にデバイスが消えた、libpulse / WASAPI のランタイムエラー等)は OSError
  • Windows / Linux 以外のプラットフォームでは NotImplementedErrorSpeaker バックエンドのディスパッチ由来)。

vrcpilot.speaker.routing.route

def route(
    pid: int,
    device: str | AudioDevice | None = None,
    *,
    chunk_seconds: float = 0.02,
    blocksize: int | None = None,
) -> Router: ...

Router(...) を構築して start() まで済ませた状態で返す薄いコンビニエンスラッパーです。「リレーを開いてハンドルを受け取り、自分でライフサイクル管理する」という典型ケース向けで、戻り値の Router は既に running 状態にあります。以降のライフサイクル(stop() / close() / with ブロック)は呼び出し側の責任です。Router.start() が例外を送出した場合は例外がそのまま伝播し、Router インスタンスは返されません — start() 自身がロールバック済みなので呼び出し側が片付けるべきものはありません。

引数の意味はすべて Router と同じです。

例外

  • vrcpilot.speaker.routing.AudioRoutingErrorRuntimeError のサブクラスで、当パッケージ全体のベース例外。直接インスタンスは「find_device の同一段階で 2 件以上にマッチした曖昧解決」のときに送出されます。ルーティング失敗をまとめて拾いたい場合はこの型を except してください。
  • vrcpilot.speaker.routing.DeviceNotFoundErrorAudioRoutingError のサブクラスで、find_device の 3 段階すべてが 0 件だった場合と、default_device() がデフォルト出力デバイスを 1 つも見つけられなかった場合に送出されます。サブクラス関係のため、AudioRoutingError で広く except するとこちらも拾えます。

エンドツーエンドのスニペット

VRChat の音声を OS デフォルトの出力デバイスへリレーします:

import time

import vrcpilot
from vrcpilot.speaker.routing import route

pid = vrcpilot.launch(no_vr=True)
if pid is None:
    raise RuntimeError("VRChat did not start before launch() timed out")
time.sleep(45)  # warm-up

with route(pid) as router:
    print(f"routing PID {pid} -> {router.device.name!r}")
    time.sleep(30.0)  # relay for 30 seconds
# Router.stop() runs automatically on __exit__.

仮想ケーブル(Linux の VRCPilotMic / Windows の CABLE Input)へ転送して、Discord や OBS から「マイク入力」として拾えるようにします。クエリは find_device の 3 段階解決を通るので、"VRCPilotMic" のような部分文字列でも一意に決まれば通ります:

from vrcpilot.speaker.routing import Router, find_device

target = find_device("VRCPilotMic")  # raises DeviceNotFoundError if absent
router = Router(pid=vrchat_pid, device=target, chunk_seconds=0.02)
router.start()
try:
    ...  # main loop / wait condition
finally:
    router.stop()

複数 VRChat インスタンスを別々の出力デバイスへ分離する例(per-PID 音声分離):

import vrcpilot
from vrcpilot.speaker.routing import list_devices, route

devices = [d for d in list_devices() if "VRCPilotMic" in d.name]
pids = vrcpilot.find_pids()  # newest first

routers = [route(pid, device=dev) for pid, dev in zip(pids, devices)]
try:
    ...  # let the agents talk
finally:
    for router in routers:
        router.close()

Mic(音声再生)

VRChat にライブマイク入力として認識させるために、float32 PCM を仮想ケーブルの出力デバイスへストリーミングします。主なユースケースは、LLM エージェントの TTS チャンクを実マイクや中間音声ファイルを介さずに VRChat へ直接流し込むことです。セッションは __init__soundcard のプレイヤーを開き、インスタンスがクローズされるまで生かし続けます。play(chunk) は呼び出しごとに 1 チャンクだけ書き込むので、ペースは呼び出し側が制御します(for chunk in tts.stream(): mic.play(chunk))。Windows ではデフォルトデバイスは VB-Audio Virtual Cable の "CABLE Input"、Linux では vrcpilot.mic.linux.register_virtual_mic(または vrcpilot linux-mic register の実行)で作成される "VRCPilotMic" の PipeWire シンクです(複数の AI インスタンスを並列に走らせる場合は、vrcpilot.mic.linux.register_virtual_mic(suffix=...) で各インスタンス用の VRCPilotMic_<suffix> を別々に登録し、TTS 音声経路を分離できます)。

vrcpilot.Mic

class Mic:
    def __init__(
        self,
        device: str | None = None,
        *,
        sample_rate: int = 48000,
        channels: int = 1,
    ) -> None: ...

    @property
    def device_name(self) -> str: ...
    @property
    def device_id(self) -> str: ...
    @property
    def sample_rate(self) -> int: ...
    @property
    def channels(self) -> int: ...

    def play(self, chunk: NDArray[np.float32]) -> None: ...
    def close(self) -> None: ...
    def __enter__(self) -> Self: ...
    def __exit__(self, exc_type, exc_val, exc_tb) -> None: ...

devicesoundcard が報告する名前に対して、大文字小文字を区別しない 部分一致 で照合されます(マッチは Speaker.nameSpeaker.id の両方を見ており、id についてはあいまい一致を行います)。None の場合はまず $VRCPILOT_MIC_DEVICE が、続いて default_device_name() が返す OS のデフォルトが使われます。コンストラクタはデバイスを解決して (sample_rate, channels) 用の soundcard プレイヤーを開き、そのコンテキストへ入ります — これらの値はセッションの生存期間中固定され、再設定したい場合は新しい Mic を構築する必要があります。

device_id は内部の soundcardSpeaker.id を文字列として公開します。Linux ではこれは PulseAudio のシンク名(例: "VRCPilotMic")、Windows では soundcard が公開する WASAPI デバイス id 文字列です。

play(chunk) は呼び出しごとに float32 配列を 1 つ書き込みます。チャンクの形は構成済みのチャネル数と一致していなければなりません(モノラルは (N,)、マルチチャネルは (N, channels))。さもなければ ValueError が送出されます。バックエンドの内部バッファが満杯の場合は呼び出しがブロックされ、ライブ TTS ストリーム向けに自然な背圧(バックプレッシャ)が得られます。

ストリームは close()with ブロックの抜け出し、もしくはベストエフォートのフォールバックとして __del__ によって解放されます。コンテキストマネージャを優先してください — __del__ は GC のタイミングで実行されるため、インタプリタによっては迅速なリソース解放を当てにできません。

Raises:

  • 解決された名前に一致する出力デバイスがない場合、またはこのプラットフォームのデフォルトが設定されていない場合は MicDeviceNotFoundError
  • soundcard がインストールされていない場合は ImportError(lazy import はコンストラクタ実行時に行われます)。
  • soundcard バックエンド(Linux では libpulse、Windows では WASAPI)がプレイヤーを開けない場合や、Mic クローズ後の play() から RuntimeError
  • ネイティブバックエンドの共有ライブラリが読み込めない場合は OSError(例: Linux で libpulse0 が無い場合)。
  • sample_rate / channels が厳密に正でない場合、または play()float32 でないチャンク、ndim{1, 2} の外側のチャンク、コンストラクタと一致しないチャネル数のチャンクを受け取った場合は ValueError

vrcpilot.MicDeviceNotFoundError

RuntimeError のサブクラスで、soundcard が解決された名前に一致する出力デバイスを見つけられない場合に送出されます。メッセージには soundcard が現在認識しているすべての出力デバイスと、OS 別のセットアップヒント(Linux では vrcpilot linux-mic register、Windows では VB-Cable のインストールリンク)が含まれており、インストール名の取り違いを診断しやすくなっています。

vrcpilot.mic.default_device_name

def default_device_name() -> str | None: ...

OS 別の既定出力デバイス部分文字列です。Windows では "CABLE Input"、Linux では(vrcpilot linux-mic register 実行後に)"VRCPilotMic" を返します。それ以外のプラットフォームでは None を返します。vrcpilot linux-mic register --suffix <name>(または vrcpilot.mic.linux.register_virtual_mic(suffix="<name>"))で派生シンクを追加した場合は、Mic("VRCPilotMic_<name>") のようにシンク名を直接指定してください — default_device_name() は空 suffix の "VRCPilotMic" のみを返します。

VRCPILOT_MIC_DEVICE

コンストラクタ引数と default_device_name() の間に参照される環境変数です。デバイス名をソースコードに埋め込みたくない場合や、Windows のデフォルトをコード変更なしで上書きしたい場合に便利です。

エンドツーエンドのスニペット

事前にロードしたバッファを 1 つ再生します:

import numpy as np
import vrcpilot

samples = np.zeros(48000, dtype=np.float32)  # 1 second of silence
with vrcpilot.Mic(sample_rate=48000, channels=1) as mic:
    mic.play(samples)

ジェネレータからチャンクをストリーミングします(LLM エージェントのインクリメンタル TTS が典型的に生成する形):

from collections.abc import Iterator

import numpy as np
from numpy.typing import NDArray

import vrcpilot

def tts_chunks() -> Iterator[NDArray[np.float32]]:
    # Replace with the agent's actual chunk iterator.
    for _ in range(10):
        yield np.zeros(4800, dtype=np.float32)  # 100 ms of silence per chunk

with vrcpilot.Mic(sample_rate=48000, channels=1) as mic:
    for chunk in tts_chunks():
        mic.play(chunk)

vrcpilot.mic.linux

PipeWire 上の永続的な VRCPilotMic 仮想マイクを管理する Linux 専用ヘルパーです。これは vrcpilot linux-mic CLI のプログラム上の対応物で、両者は同じ config 断片を書き込み、同じ PulseAudio の module_load 経路を呼び出します。

すべての公開関数は suffix キーワード引数を受け取り、同じ仕組みで複数のシンクを並列管理できます — 主な用途は、複数の AI インスタンスを同時に走らせるときに各インスタンスへ独立した仮想マイクを割り当てることです。空 suffix (""、既定) は従来の VRCPilotMic を対象にして後方互換を保ち、非空 suffix(例: "alt")は VRCPilotMic_<suffix> 派生シンクを対象にします。suffix に使える文字は [A-Za-z0-9_-] のみで、それ以外を渡すと ValueError が送出されます。

Linux 以外のプラットフォームでこのサブモジュールをインポートすると、インポート時点で ImportError が送出されますraise ImportError("vrcpilot.mic.linux is Linux-only"))。クロスプラットフォームなコードを書く際は sys.platform == "linux" でガードする(または遅延 import する)ようにしてください。

vrcpilot.mic.linux.register_virtual_mic

def register_virtual_mic(
    *,
    suffix: str = "",
    runtime_load: bool = True,
) -> RegisterResult: ...

VRCPilotMic(または VRCPilotMic_<suffix>)の module-null-sink$XDG_CONFIG_HOME/pipewire/pipewire.conf.d/vrcpilot-mic[-<suffix>].conf(変数が未設定の場合は ~/.config/... にフォールバック)へ永続化します。さらに runtime_load=True のときは pulsectl.Pulse.module_load("module-null-sink", ...) を呼び出してシンクを即座に使えるようにします。ランタイムステップはベストエフォートで、失敗(pulsectl 不在、コントロールプレーンのエラー)は RegisterResult.runtime_warning に格納されて返り、例外としては送出されません — 永続的な config が正なので、次回の PipeWire 再起動 / 再ログイン後には拾われます。

suffix は対象シンクを切り替えます。空 suffix(既定)は既存の VRCPilotMic を、非空 suffix は VRCPilotMic_<suffix> を対象にします。同じ suffix への再呼び出しは冪等で、既存のランタイムモジュールがあれば一度アンロードしてから読み直すため、二重スタックは起きません。

Returns: 行った内容を表す RegisterResult

Raises: 永続的な config が書き込めない場合(権限エラー、ファイルシステムの失敗)は OSErrorsuffix に不正文字が含まれる場合は ValueError

vrcpilot.mic.linux.unregister_virtual_mic

def unregister_virtual_mic(*, suffix: str = "") -> bool: ...

suffix に対応する永続的な config 断片を削除し、現在読み込まれている同名 module-null-sink をアンロードします。空 suffix(既定)は VRCPilotMic を、非空 suffix は VRCPilotMic_<suffix> を対象とします。実際に何かが削除された場合(config ファイルの削除、ランタイムモジュールのアンロード、もしくはその両方)は True を、どちらの成果物も存在しなかった場合は False を返します。冪等で、繰り返し呼んでも安全です。

Raises: suffix に不正文字が含まれる場合は ValueError

vrcpilot.mic.linux.is_registered

def is_registered(*, suffix: str = "") -> bool: ...

suffix に対応する永続的な config 断片が存在するかどうかを返します。空 suffix は既定の VRCPilotMic を、非空 suffix は VRCPilotMic_<suffix> を確認します。PulseAudio は参照しません — ランタイムのモジュール一覧を確認したい場合は vrcpilot linux-mic status CLI を使うか、open_pulse_control() を直接呼び出してください。

Raises: suffix に不正文字が含まれる場合は ValueError

vrcpilot.mic.linux.config_path

def config_path(*, suffix: str = "") -> Path: ...

suffix に対応する PipeWire config 断片の絶対パスを返します(空 suffix は vrcpilot-mic.conf、非空 suffix は vrcpilot-mic-<suffix>.conf)。$XDG_CONFIG_HOME を尊重し、未設定なら ~/.config にフォールバックします。vrcpilot linux-mic status CLI で位置を表示できるよう公開されています。

Raises: suffix に不正文字が含まれる場合は ValueError

vrcpilot.mic.linux.iter_registered_suffixes

def iter_registered_suffixes() -> list[str]: ...

pipewire.conf.d/ 配下の vrcpilot-mic*.conf 断片を走査し、ファイル名から復元した suffix のリストを返します。空 suffix(既定シンク)が先頭、残りは辞書順でソートされるため、CLI 表示や e2e の列挙が安定します。config ディレクトリがまだ存在しない場合は空リスト [] を返します。

vrcpilot.mic.linux.RegisterResult

@dataclass(frozen=True)
class RegisterResult:
    config_path: Path
    created_config: bool
    runtime_loaded: bool
    runtime_warning: str | None
    suffix: str

register_virtual_mic の結果:

  • config_path — 永続的な config 断片の絶対パス。
  • created_config — 呼び出しがファイルを書き込んだ場合に限り True(期待した内容で既に存在した場合は False)。
  • runtime_loaded — 即時実行された pulsectlmodule_load が成功した場合に限り Trueruntime_load=False でスキップされた場合や、ランタイムステップが失敗した場合は False(その場合 runtime_warning が設定されます)。
  • runtime_warning — ランタイムロード失敗の人間可読な説明、もしくは失敗がなかった場合は None
  • suffix — 登録された suffix を正規化した文字列(既定シンクの場合は空文字列)。呼び出し時に register_virtual_mic(suffix=...) へ渡した値がそのまま round-trip されます。

Screenshot

vrcpilot.Screenshot

@dataclass(frozen=True, eq=False)
class Screenshot:
    image: NDArray[np.uint8]   # (H, W, 3) uint8 RGB
    x: int                     # window top-left, desktop-absolute
    y: int
    width: int
    height: int
    monitor_index: int         # mss.MSS().monitors index
    captured_at: datetime      # UTC

    def save(self, png_path: Path | None = None) -> str: ...
    @classmethod
    def load(cls, text: str) -> Screenshot: ...

ピクセルデータに加え、ウィンドウの画面上ジオメトリ(x / y はウィンドウの左上のデスクトップ絶対ピクセル、monitor_index はキャプチャ元の mss モニタ)を保持します。OCR / detect の結果はウィンドウローカルであるため、このジオメトリはクリックに必須ではなく参考情報です。eq=False なのは、numpy 配列を __eq__ で要素ごとに比較できないためです。

save() は YAML 文字列を返します。png_path が与えられた場合は PNG をそこに書き込み、YAML には path: を格納します。さもなければ YAML が PNG を image: 下に base64 で埋め込みます。load() はいずれの形式も復元します。

vrcpilot.take_screenshot

def take_screenshot(*, settle_seconds: float = 0.05) -> Screenshot | None: ...

VRChat をフォーカスし、settle_seconds だけスリープしたうえで、VRChat ウィンドウのみのワンショットキャプチャを取得します。

Returns: Screenshot、もしくはリカバー可能な失敗(Wayland ネイティブ、フォーカス拒否、ウィンドウ未マップ、mss エラー)の場合は None

Raises: サポート外のプラットフォームでは NotImplementedErrorsettle_seconds < 0 の場合は ValueError


OCR

vrcpilot.ocr.OCRWord

@dataclass(frozen=True)
class OCRWord:
    text: str
    polygon: Polygon          # (TL, TR, BR, BL), image-local
    confidence: float         # 0.0–1.0

    @property
    def bbox(self) -> tuple[int, int, int, int]: ...   # (x, y, w, h), axis-aligned
    @property
    def center(self) -> tuple[float, float]: ...

vrcpilot.ocr.OCRResult

@dataclass(frozen=True, eq=False)
class OCRResult:
    screenshot: Screenshot
    words: tuple[OCRWord, ...]

Screenshot と、その上で検出された単語群をまとめます。すべての OCRWord.polygon / OCRWord.bbox の値は ウィンドウローカル(原点は VRChat ウィンドウの左上)で、これは mouse.move() が受け取る座標系と同じです — 変換ステップは不要です。

vrcpilot.ocr.OCREngine

class OCREngine(ABC):
    @abstractmethod
    def recognize(self, image: NDArray[np.uint8]) -> Sequence[OCRWord]: ...

この ABC を実装すれば、独自のバックエンドに差し替えられます。

vrcpilot.ocr.RapidOCREngine

class RapidOCREngine(OCREngine):
    def __init__(self, *, params: dict[str, Any] | None = None) -> None: ...

デフォルトのバックエンドです(rapidocr 経由の PP-OCRv4)。コンストラクタで rapidocr を lazy import するため、ocr extra をインストールしていなくてもパッケージの他の部分は使えます。

Raises: rapidocr がインストールされていない場合は ImportError

vrcpilot.ocr

def ocr(
    screenshot: Screenshot,
    *,
    engine: OCREngine | None = None,
) -> OCRResult: ...

screenshot に対して OCR を実行します。engineNone の場合、プロセスキャッシュ済みの RapidOCREngine インスタンスが使用されます。

vrcpilot.ocr は直接呼び出し可能です(vrcpilot.ocr(shot))。サブモジュールの vrcpilot.ocrfrom vrcpilot.ocr import OCREngine のような import-from 形式で引き続きアクセスできます — Python の import 機構は sys.modules を介してこれらを解決するため、関数バインディングがサブモジュールアクセスを壊すことはありません。


画像テンプレート検出

vrcpilot.detect.Detection

@dataclass(frozen=True)
class Detection:
    polygon: Polygon
    confidence: float
    scale: float        # 1.0 = same size as the query
    rotation: float     # radians, counter-clockwise positive

    @property
    def bbox(self) -> tuple[int, int, int, int]: ...
    @property
    def center(self) -> tuple[float, float]: ...

vrcpilot.detect.DetectResult

@dataclass(frozen=True, eq=False)
class DetectResult:
    screenshot: Screenshot
    query: NDArray[np.uint8]    # (h, w, 3) uint8 RGB
    detections: tuple[Detection, ...]

すべての Detection.polygon / Detection.bbox の値は ウィンドウローカル で、OCRResult および mouse.move() が受け取る座標系と一致しています。

vrcpilot.detect.DetectEngine

class DetectEngine(ABC):
    @abstractmethod
    def detect(
        self,
        image: NDArray[np.uint8],
        query: NDArray[np.uint8],
    ) -> Sequence[Detection]: ...

vrcpilot.detect.TemplateDetectEngine

class TemplateDetectEngine(DetectEngine):
    def __init__(
        self,
        *,
        threshold: float = 0.85,
        scales: Sequence[float] = (
            0.25, 0.3, 0.35, 0.4, 0.5, 0.6, 0.75,
            0.9, 1.0, 1.25, 1.5, 1.8, 2.2, 2.6, 3.0,
        ),
        rotations_deg: Sequence[float] = (0.0,),
        nms_iou: float = 0.3,
        max_results: int = 32,
    ) -> None: ...

非極大抑制を備えた、マルチスケール(オプションでマルチ回転)の cv2.matchTemplate(..., TM_CCOEFF_NORMED) ランナーです。

vrcpilot.detect

def detect(
    screenshot: Screenshot,
    query: NDArray[np.uint8],
    *,
    engine: DetectEngine | None = None,
) -> DetectResult: ...

engine.detect(screenshot.image, query) を実行します。engineNone の場合、プロセスキャッシュ済みの TemplateDetectEngine が使用されます。


合成入力

keyboard および mouse モジュールは、クラスではなく薄いシングルトンオブジェクトを公開します。これらに対してメソッドを直接呼び出してください。すべてのメソッドは focus: bool = True を受け取ります。VRChat フォーカスガードを意図的にバイパスしたい場合を除き、True のままにしてください。下に挙げるシグネチャは貼り付けやすさのために def で書いていますが、実際には vrcpilot.keyboard.press(...) のように呼び出します。

vrcpilot.Key

サポートされる各キー名の StrEnum です。メンバ:

  • 文字: AZ
  • 数字: NUM_0NUM_9
  • ファンクションキー: F1F12
  • 修飾キー: SHIFT, SHIFT_LEFT, SHIFT_RIGHT, CTRL, CTRL_LEFT, CTRL_RIGHT, ALT, ALT_LEFT, ALT_RIGHT, WIN, WIN_LEFT, WIN_RIGHT
  • ナビゲーション: UP, DOWN, LEFT, RIGHT, HOME, END, PAGE_UP, PAGE_DOWN
  • 編集: BACKSPACE, DELETE, INSERT, TAB, ENTER, ESCAPE, SPACE
  • 記号: MINUS, EQUALS, LBRACKET, RBRACKET, BACKSLASH, SEMICOLON, QUOTE, COMMA, PERIOD, SLASH, BACKTICK

vrcpilot.keyboard

def press(*keys: Key, duration: float = 0.1, focus: bool = True) -> None: ...
def down(*keys: Key, focus: bool = True) -> None: ...
def up(*keys: Key, focus: bool = True) -> None: ...

press はコードタップです: キーは左から右へ押下され、duration 秒間ホールドされてから、右から左へ離されます。duration0.1 より下げないでください — VRChat / Unity はそれより短いタップを取りこぼします。

downup は対になる半アクションです。これらは意図的に、単一の Python プロセス内でのみ有用となるようにしています。合成入力デバイスはプロセス終了時にカーネルから解放されるため、CLI 呼び出しを跨いで down / up を対にすることはできません。

Raises: keys が空の場合は TypeError。フォーカスガードからの VRChatNotRunningError / VRChatNotFocusedError

vrcpilot.MouseButton

StrEnum で、メンバは LEFTRIGHTMIDDLE です。

vrcpilot.mouse

def move(x: int, y: int, *, relative: bool = False, focus: bool = True) -> None: ...
def click(*buttons: MouseButton, count: int = 1, duration: float = 0.0, focus: bool = True) -> None: ...
def scroll(amount: int, *, focus: bool = True) -> None: ...
def press(*buttons: MouseButton, focus: bool = True) -> None: ...
def release(*buttons: MouseButton, focus: bool = True) -> None: ...

move(x, y)(x, y)VRChat ウィンドウローカルのピクセル として解釈します — (0, 0) は VRChat ウィンドウの左上です。これは OCRWord.bbox / Detection.bbox が使う座標系と同じであり、OCR / detect の結果をそのまま渡せます。ウィンドウ外の座標は拒否されません。デスクトップ座標へ変換され、そのまま OS に渡されます。relative=True の場合、(x, y) は現在のカーソル位置に加算されるデルタです(このブランチではウィンドウローカル解釈は適用されません)。

click() は引数なしで呼ばれた場合 LEFT にフォールバックします。count > 1 は押下/離上のペアを繰り返します。duration > 0 は各クリックをその秒数だけ保持します。

press / release はコードクリック用の対になる半アクションです。keyboard.down / up と同様に、単一の Python プロセス内でのみ意味があります。

vrcpilot.controls.ensure_target

def ensure_target() -> None: ...

VRChat が起動中かつ現在フォーカスされていることを検証し、必要に応じてフォーカスを当てます。冪等です。focus=True(デフォルト)のとき、高レベルの keyboard / mouse / clipboard.paste 呼び出しが内部でこれを呼び出します。

Raises: Wayland ネイティブでは NotImplementedErrorVRChatNotRunningErrorVRChatNotFocusedError

vrcpilot.VRChatNotRunningError, vrcpilot.VRChatNotFocusedError

ensure_target() と入力ヘルパーから送出されます。


クリップボード

vrcpilot.clipboard.paste

def paste(text: str, *, focus: bool = True) -> None: ...

text を OS のクリップボードへコピーし、続いて VRChat に Ctrl+V を送ります。非 ASCII の内容(日本語、絵文字など)にはこちらを使用してください — スキャンコードベースの keyboard.press では直接入力できません。

Raises: 利用可能なクリップボードバックエンドが無い場合(例: xclipxsel もインストールされていない Linux)は pyperclip.PyperclipExceptionfocus=True の場合はフォーカスガードの例外。


型エイリアス

vrcpilot.types.Polygon

type Polygon = tuple[
    tuple[float, float],  # TL
    tuple[float, float],  # TR
    tuple[float, float],  # BR
    tuple[float, float],  # BL
]

OCRWord.polygonDetection.polygon が使う 4 隅のポリゴン形です。座標は画像ローカルのピクセルです。


エンドツーエンドのスニペット

from time import sleep

import vrcpilot

# launch() waits up to wait_timeout seconds for VRChat's PID.
# None means the timeout expired before VRChat appeared.
pid = vrcpilot.launch(no_vr=True, screen_width=1280, screen_height=720)
if pid is None:
    raise RuntimeError("VRChat did not start before launch() timed out")
sleep(45)  # extra warm-up wait: shaders / avatar loading / network sync

try:
    shot = vrcpilot.take_screenshot()
    if shot is None:
        raise RuntimeError("could not capture the VRChat screen")

    result = vrcpilot.ocr(shot)
    for word in result.words:
        print(word.text, word.bbox, word.confidence)

    if result.words:
        first = result.words[0]
        x, y, w, h = first.bbox
        vrcpilot.mouse.move(int(x + w / 2), int(y + h / 2))
        vrcpilot.mouse.click(vrcpilot.MouseButton.LEFT)

    vrcpilot.keyboard.press(vrcpilot.Key.W, duration=1.0)
    vrcpilot.clipboard.paste("こんにちは、VRChat!")
finally:
    vrcpilot.terminate()