Anime AR Stock

Vol.5

印が「通った」と判定するまで — 350ms の保持、1.5 秒の溜め、音声認識の遅れ

  • state machine
  • debounce
  • Call Studio
生ラベル毎推論
信頼度≥ 0.45
安定350ms
ステップ間隔500ms
発動後2500ms
図5. 印は分類器の出力そのものではなく、時間方向のフィルタを通ったあとに「通る」。

時間定数の一覧

印認識まわりの定数は handSignInferenceConfig.ts、螺旋丸の発動条件は rasenganTriggerController.ts、Call Studio の手の扱いは useCallStudioHandSignTrigger.ts に分けて置いています。

  • 信頼度 0.45 未満は捨てる / 同じラベルが 350ms 続いたら stable / 印と印の間は 500ms 空ける / 術の発動後 2,500ms は次のトリガーを受けない
  • 印が揃ってから発動まで JUTSU_CAST_DELAY_MS = 1,500ms(当初は 1,000ms)
  • 螺旋丸: 寅の印が確定してから「螺旋丸」の発声を 5,000ms 待つ / 確定の直前 900ms 以内の発声も拾う / 終了後 800ms は寅を無視
  • Call Studio: Hands への送信は 70ms 間隔 / 手を見失ってもアンカーを 500ms 保持 / 片手しか映っていない間は 2,000ms 印を推論しない

350ms の保持: 通過姿勢を拾わない

印から印へ手を組み替える途中、CNN は一瞬だけ別の印のラベルを高い信頼度で返すことがあります。1 フレームの一致で回路を進めると、この通過姿勢で順番が崩れます。そこで、同じラベルが閾値を超えたまま 350ms 続いたときだけ stable にし、ラベルが変わったら計測をやり直します。

hooks/useHandSignImageClassifier.ts(applyStablePrediction の要点)
1let stable = false;2if (raw.confidence >= HAND_SIGN_CONFIDENCE_THRESHOLD) {          // 0.453  const tracker = stableTrackerRef.current;4  if (!tracker || tracker.label !== raw.label) {5    stableTrackerRef.current = { label: raw.label, since: now };  // ラベルが変わった→計測やり直し6  } else if (now - tracker.since >= HAND_SIGN_STABLE_DURATION_MS) { // 350ms7    stable = true;8  }9}

推論は 125ms 間隔(Vol.4)なので、350ms の保持は「連続 3〜4 回の推論が同じラベル」とほぼ同じ意味になります。

ハマった点: 印は揃ったのに術が出ない

2026年5月、印回路は完成表示になるのに螺旋丸や千鳥が始まらない不具合がありました。原因は React の古いクロージャです。カメラの処理ループは一度登録したコールバックを毎フレーム呼び続けるので、その中で参照していた jutsuCircuitMode(今どの術の回路か)は登録時の値のまま更新されていませんでした。1 秒の溜めが終わった時点で「どの術を出すか」を読むと、古い値を見て何も起きない、という状態です。

2026-05-21 の修正(App.tsx)
1// 修正: 毎フレーム呼ばれるループからは ref 経由で最新値を読む2const jutsuCircuitModeRef = useRef(jutsuCircuitMode);3jutsuCircuitModeRef.current = jutsuCircuitMode;4 5// カメラループ内6const activeJutsuMode = jutsuCircuitModeRef.current;

これ以降、MediaPipe の onResults や rAF ループから読む状態はすべて ref に置く、というのがこのプロジェクトの決まりになっています。Vol.1 のフックの形で、印回路や分類器が circuitRef.current / classifierRef.current のように ref 越しに参照されているのはそのためです。

溜めを 1 秒から 1.5 秒へ

印が揃ってから発動までの溜め(JUTSU_CAST_DELAY_MS)は、当初 1,000ms でした。2026-09-23 に Call Studio で印と音声から術を出せるようにしたとき、千鳥については 1.5 秒待つ変更を入れ、今は定数自体が 1,500ms です。溜めの間は推論を止め、HUD に「○秒後に発動(推論停止)」と残り時間を出します。

音声とのタイミング: 認識結果は遅れて届く

螺旋丸は「寅の印が確定 → 5 秒以内に『螺旋丸』と発声」の AND 条件で発動します。片方だけで発動させないのは誤発動を防ぐためで、5 秒の窓の中なら印を解いてから発声しても構いません。

実装して分かったのは、Web Speech API の認識結果が発声から遅れて届くことです。印を結びながら言い始めると、認識結果が届くのは寅の確定より後になり、逆に発声が少し早いと「まだ寅が確定していない」ので捨てられてしまう。そこで、寅が確定する前に届いた発声も 900ms までは pendingVoiceAt として保持し、確定した瞬間にそれを使う猶予を入れています。

Call Studio では、撮影中に手の検出が落ちやすいという別の問題がありました。発声の瞬間に手が映っていなくても、直前 1,500ms 以内に手が見えていれば受け付け、手が無いまま発声された場合は 2,000ms だけ保留して手が戻るのを待ちます。手を見失ってもアンカーは 500ms 保持するので、エフェクトが一瞬で消えることもありません。

実装から得た知見

  • 分類器は「今のラベル」、状態機械は「時間」を担当する。誤発動の大半は分類精度ではなく時間方向の扱いで直る。
  • MediaPipe の onResults や rAF から読む状態は ref に置く。React の state を直接読むと、古い値のまま静かに動かなくなる。
  • 音声認識の結果は遅れて届く。「印 → 声」の順番を厳密に要求すると取りこぼすので、少し前の発声を保持する猶予が要る。
  • 撮影用途では手の検出が途切れる前提で設計する。アンカー保持・発声の保留のような猶予時間を、それぞれ定数として名前を付けて持つ。