JevでVJアプリを作ってみた

English ver: Jev VJ · in Lotusland

インターネットの皆様と同じく、僕もJevのデモを作りました。 JevをVJのアシスタントとして使うアプリです:

ユーザーが入力したプロンプトに対して、Jevが最適な動画とエフェクトを提案し、ユーザーが選択するという流れです。ユーザーが手動で選択するのは音楽にちゃんと同期したいから。

実装は一応ここに置いておきます: GitHub - fand/jev-vj · GitHub

構成

このデモは、Diogoによる最初のツイートで紹介されていたDOOMデモを見て、これVJに活かせないかな〜と思って作ったものです。VJというのは色んな意味でゲームと似た所があるので。

しかしJevにVJを丸ごと任せるのは現実的ではない。Jevの Choice は与えられた状態から最適な選択肢を推定して返してくれるが、そのためには「現在の状態」を与える必要がある。すなわち、VJの場合は「現在のノリ」を評価して与える必要があるが、これは現実的ではない(やろうと思えばルールベースで出来るとは思うが)。VJにおける評価とは我々の感覚そのものなので……。

また、Jevのレスポンスがいくら速いといっても、動画を音に合わせて切り替えるのは難しい。僕の環境だとJevのレスポンスは90ms-260ms程度だが、これは120BPMにおける16分音符1~2個に相当する。Abletonのように切り替えを予約するモデルなら可能だと思うけど、上手くいくか不明なわりに仕組みが大仰なので見送り。

そこで方針を変え、Jevは単にクリップの提案をするだけにした。使用する動画クリップすべてについて、事前に色、BPM,カメラの動き etc を書き出したJSONを作成し、これをJevにコンテキストとして与える。Jevは最適なクリップ候補を返し、ユーザーが手動で音にあわせて切り替える。これならレイテンシーを考える必要がない。

クリップ提案のフロー

実装はAstraに依頼した。仕様の確定やバグ修正などに数時間かかったが、最終的はこんなかんじで動くようになった:

VJ画面のスクリーンショット

クリップライブラリ

一番の課題は当然、クリップ情報のJSONを書かなくてはいけない所だ。クリップ情報は簡潔に、しかしJevが根拠に使える程度には充実していなくてはならない。なんとか自動的する手段を探したが、今のところ丁度良い方法はないようだった。

最初に試したのは、動画を10フレームごとに静止画に書き出してAstraに食わせるという方法。Astraは静止画はよく分析してくれるが、フレーム間の動きはほとんど理解できないようだった。フレーム間隔を上げてもあまり効果はなかった。Astraに直接動画ファイルを投げても同様(ffmpegでフレームを抜き出しているのが確認できる)。

Geminiの動画分析がよく出来ているという話を見かけたので、次にこれを試した。Geminiは僕がリクエストした情報 (カラーパレット、質感、カメラの動きetc) をしっかり埋めてくれるし、動画内の物の動きも捉えている。しかし、細かいものが沢山動いているシーンでは理解が追いつかないようで、例えばパーティクルが激しく動くシーンについて「フロアの空気を落ち着かせたい時に最適」と言ってきたりした。 (Geminiに分析方法を聞いたら、これも内部的にはフレーム書き出ししてから分析しているとのことだった)

仕方ないので、今回は人力でデータ入力を行うことにした。AstraにテンプレMarkdownを作ってもらい、動画を一つ一つ目視しながらAquaVoiceで内容を記述。それを元にJSONを生成する、という流れ。かなり面倒だが一番修正しやすく安定する😇

後日、クリップ情報をスプレッドシート表示して編集できるLibraryページを作った。これでもまだ面倒だが……今回のようなアプリではどうしても避けられない作業だろう。なんならこういう下処理がVJの本質なのかもしれない。

追記 2026-09-25 22:53

Sainaさんが開発しているSynapseRackでは類似クリップを提案してくれる機能があるらしい。便利そう

Token limit

クリップリストが出来たところで、今度はtoken sizeを気にしないと行けない。今回使用した112クリップについてJevへのリクエストを確認したところ、27k tokenであり、これは上限である32k tokenの84%を占める。クリップを追加したり、クリップ情報を充実させるほど、よりtoken limitが圧迫されることになる。LLMではどれも同じことであるが。

APIリクエスト抜粋:

{
  "model": "jev-latest",
  "questions": {
    "clip": {
      "type": "choice",
      "instructions": "Rank the available footage for a VJ preview deck using state.prompt and state.clips.",
      "criteria": {
        "Opti/Opti6.mov": "Inorganic metal square tunnel with multiple grilles. White light dots travel upward on the grilles while the camera advances at medium speed.",
        ...
      }
    }
  },
  "state": {
    "prompt": "white light metallic machine",
    "action": "candidates",
    "clips": [
      // クリップ詳細
      {
        "id": "Opti/Opti6.mov",
        "description": "Inorganic metal square tunnel with multiple grilles. White light dots travel upward on the grilles while the camera advances at medium speed.",
        "attributes": {
          "color.palette": ["white"],
          "material.types": ["metal"],
          "camera.translation": ["forward_into_scene"],
          "camera.translation_speed": "medium",
          ...
        },
        ...
      }
    ],
    "available_ids": [...], // 全素材 112ファイル
    "candidate_ids": [...], // 最近の素材を除外した、次に選択可能な素材
    "excluded_recent_ids": [
      "ducky3d/Animation 6.mp4",
      "tatsuyam/aqua_10.mp4"
    ],
    "current_clip_id": "ducky3d/animation 15.mp4",
    "current_clip": { ... }, // 現在のclipの説明
    "current_effects": [],
    "history": [...] // 直近で再生したclip id
  }
}

問題を回避する方法としては、クリップやクエリを階層化することが考えられる。今回のデモではクリップをすべて一つの集合に入れていたが、例えばクリップをFG / BGの2レイヤーに振り分けると(FG: 2Dトランジション素材やテキスト, BG: 3Dシーン など)、contextに入れるクリップリストを半減することが出来る。あるいは素材をジャンルごとのdirectoryに分けてしまっても良い("Sci-fi > minimal techno" や "Organic > deep sea" など)。いずれも試していないが……

結論

巷のDJアプリでは曲の推薦機能がある?らしい?けど、VJアプリでこの機能を持つものは見たことがない。動画素材の内容を分析して推薦するというのが根本的に難しいからだろう。Jevや他のAIツールで実装が可能になるかもしれない。

同様に、音楽演奏やVJにおけるAIの利用ケースとして、最終成果物をポン出しするのではなく、ワークフロー自体を変える新しいアプローチが出てくると面白いな、と思う。

React風の記法でRust GUIアプリを作るライブラリ egui-reactor を公開した

egui-reactor という、React 風の記法で Rust GUI アプリケーションを書くライブラリを公開した。

wasm 書き出しができるので Web でも動きます。

実装は薄く、大まかには rsx をパースして egui 呼び出しに変換するマクロと、 use_state などの Hooks っぽい状態管理を入れているだけ。あとは taffy を使って flexbox & grid layout を実現しているので、コンポーネントの props としてレイアウト用の値を渡せるようにした。

egui は Rust で最も普及している GUI 基盤。 immediate mode を採用しており、DOM のようにウィジェットを永続的なオブジェクトとして持たず、ウィジェットは毎フレーム関数呼び出しとして描き直される。 egui-reactor は React 風の use_state() を持つが、これはグローバルな store に保持される値への参照なので、以下のように setter なしで更新が書ける:

let mut count = use_state(cx, || 0i32);

rsx! {
  <Button on_click={|| *count += 1}>"Increment"</Button>
}

egui をご利用の皆様はぜひ使ってください。PR歓迎です。

動機

業務で Rust egui アプリを開発しており、そのメンテ負荷を下げたいというのが主な動機。

近頃では、UI 実装はほとんど Claude Code が生成して、手でコードを書くことが無くなったので、誰もUIコードの書き心地を気にしなくなったのでは?という気はする。 しかし、業務においてはいまだに生成された大量のコードを読む必要がある。何か問題が起きた時には、エージェントが生成した動くけどメチャ長い or 賢すぎる or 独自用語を用いた謎抽象化関数で短縮されている謎コードを見て毎日発狂しており、毎日発狂しているので、嘘でもいいから馴染みのあるコンポーネント指向に見せてくれよ、という気持ちで作った。

すなわち、書き心地よりも読み心地のほうが(以前にもまして)大事な時代になってきたという話。

補足: GPUIX

Laphさんが指摘する通り、Rust + JSX + クロスプラットフォームという条件だと、より意欲的な gpuix というプロジェクトもある。

github.com

GPUI は、Zed チーム が開発していたクロスプラットフォームのUIフレームワーク。gpuix は GPUI のアプリを JS / React で書けるようにしたもの。 我々の業務では、技術選定のタイミングでクロスプラットフォーム対応がまだ未成熟だったため採用しなかったが、個人的にはこちらも応援している。