Communitygithub.com

xbmaxx/hermes-intro-cinematic

Hermes Desktop のスタートアップ時に毎回シネマティックを再生し、ボイスオーバーを追加できるスキルです。

hermes-intro-cinematic とは?

このスキルは、Hermes Desktop に標準搭載されている22秒のイントロフィルムを、ゲストオンボーディングに依存せず**毎回の起動時に再生**するためのカスタマイズを提供します。 元の動作ではゲストオンボーディングが有効で一度も見ていない場合のみ再生されますが、このスキルは3つの必須編集を通じてその制限を解除し、さらに独自の音声トラックを重ねる機能を追加します。対応プラットフォームは Hermes Desktop(Electron アプリ)で、設定変更は主に TypeScript ファイル(intro-reveal.ts、intro-root.tsx、electron/main.ts)に対して行います。 また、アプリがパッケージ化(asar)されている場合の再パッケージ手順や、Vite 開発サーバーを使ったブラウザでの確認方法、契約テストの維持方法など、実運用に必要なトラブルシューティングも網羅しています。 このスキルを使うことで、毎回異なる演出を表示したり、会社のブランド映像を挿入するなど、起動体験を自由にカスタマイズできます。

対応~Claude Code~Codex CLI~Cursor
npx skills add xbmaxx/hermes-intro-cinematic

お気に入りのAIに質問する

このエージェントスキルを事前に読み込んだ状態で新しいチャットを開きます。

ドキュメント

Hermes Intro Cinematic

Hermes Desktop にはすでに22秒のイントロフィルムが同梱されています。7つのビート、ライブキューブビューポート、パラレルエージェント、Nous ブランド、合成サウンドが含まれています。存在はしていますが、ほとんどのユーザーは2度目を見ることはありません。再生条件は guestOnboardingEnabled && !seen です。

このスキルは、毎回の起動時にフィルムを再生し、内蔵のキューに重ねて独自のボイスオーバートラックを追加できるようにします。

使用すべきタイミング

  • 「Hermes の起動アニメーションを毎回再生させたい」
  • 「起動アニメーションに自分の音声を重ねたい ― 効果音を置き換えるのではなく、レイヤーとして」
  • apps/desktop/src/components/intro-reveal/ への変更全般。

フィルムの現状

構成要素パス
ゲート(再生条件)apps/desktop/src/components/intro-reveal/index.tsx
再生条件 + フェーズストアapps/desktop/src/store/intro-reveal.ts
オーバーレイ起動src/components/intro-reveal/intro-root.tsx
クロック、スコア、キャンバスuse-intro-clock.ts, timeline.ts, viewport-cube.ts
シンセキューsound.ts
ネイティブウィンドウelectron/intro-reveal-window.ts, electron/main.ts

タイミング計算: INTRO_TOTAL_MS = 17 600 はスコア時間。実時間は × INTRO_PACE (1.25) = 22 000 ms、さらに INTRO_EXIT_MS 900 ms を加えて合計 23 125 ms となります。

毎回の起動時に再生 ― 3つの編集(すべて必須)

  1. src/store/intro-reveal.ts — isIntroRevealEnabled() がゲストオンボーディングを必須としないように変更。shouldPlayFirstRunIntro() はオンボーディング外では !playedThisSession を返すようにします。モジュールレベルの playedThisSession フラグは必須です。ゲートのエフェクトはフェーズが 'hidden' に戻るたびに再実行されますが、フィルム終了時にまさにそれが起こるため、フラグがないと無限ループになります。
  2. src/components/intro-reveal/intro-root.tsx — オーバーレイウィンドウは isOnboardingEnabled() でガードされており、その条件が偽だと描画前に return します。メインレンダラーと同じ述語に切り替えます。
  3. electron/main.ts — introRevealController は enabled: GUEST_ONBOARDING で構築されているため、メインプロセスが open を拒否します。これを true に設定。生存性はレンダラー側で判断します。

オンボーディングの beginOnboardingFlow() / queueGuideAfterIntro() / takeGuideShape() はすべて isOnboardingEnabled() で自己ガードしているため、オンボーディングがオフの場合は何もしません。追加の変更は不要です。

オフスイッチ: localStorage['hermes-intro-reveal-always-v1'] = '0'、または HERMES_SKIP_INTRO=1 / --skip-intro を指定して起動。

音声トラックのレイヤリング

ファイルを apps/desktop/public/intro-voiceover.mp3 に配置します(vite は public/ を dist/ にそのままコピーします)。次に sound.ts で:

export function startVoiceover(cutAtMs: number): { stop: () => void }

— Audio 要素を作成し、volume を設定して再生を開始。setTimeout(stop, cutAtMs - 300) でクリックノイズを防ぐため最後の300 msをフェードアウト。use-intro-clock.ts の startPad() の隣で呼び出し、エフェクトクリーンアップで停止します。

<audio> 要素を使用し、決して fetch() + decodeAudioData() を使わないでください。 パッケージ化されたレンダラーは file:// で動作し、fetch と decodeAudioData は CORS でブロックされます。メディア要素は file:// アセットを問題なく読み込めます。

2つのトラックは設計上独立しています。要素オーディオと WebAudio キューのミキシングは OS レベルで行われます。ディッキングは発生しません。$hapticsMuted を両方に配線し、1回だけ読み取るのではなく購読してください。そうしないとミュートトグルが同期しません。

カットポイント:

const cutMs = Math.round((INTRO_TOTAL_MS + INTRO_EXIT_MS) * INTRO_PACE)  // 23 125

パッケージ化アプリへの適用

ソース編集だけでは効果がありません。asar が再パッケージされる必要があります。ここで2つの落とし穴があります。どちらも確認する価値があります。

ユーザーが実際に起動しているアプリを見つける — 決して /Applications を想定しないでください。

ps aux | grep '[H]ermes.app/Contents/MacOS/Hermes'
defaults read com.apple.dock persistent-apps | grep -o '/[^"]*Hermes\.app'

Hermes のアップデートは ~/.hermes/hermes-agent*/apps/desktop/release/mac-arm64/Hermes.app に配置され、Dock はその場所を指していることがよくあります。一方、/Applications/Hermes.app は古い 0.0.0 コピーの可能性があります。CFBundleShortVersionString で確認してください(0.17.x が本物、0.0.0 は偽物)。

app.asar.unpacked/dist/ も更新してください。 パッケージ化ビルドは asarUnpack に dist/** をリストしており、resolveRendererIndex() はそのコピーを優先します。asar だけを再パッケージしても、古い unpacked ツリーが新しいものを隠してしまいます。

npm install --workspace apps/desktop --ignore-scripts \
  --engine-strict=false --min-release-age=0
cd apps/desktop && node ../../node_modules/vite/bin/vite.js build
node scripts/bundle-electron-main.mjs     # メインプロセスの編集にはこのステップも必要

asar extract "$RES/app.asar" "$WORK"
rm -rf "$WORK/dist" && cp -R apps/desktop/dist "$WORK/dist"
asar pack "$WORK" "$WORK/app.asar"
mv -f "$WORK/app.asar" "$RES/app.asar"
cp -R apps/desktop/dist/. "$RES/app.asar.unpacked/dist/"
codesign --force --deep -s - "$APP"      # アドホック署名; Hermes はアドホック署名で配布

--ignore-scripts は低速ネットワークで重要です。Electron バイナリのダウンロードはビルドや再パッケージには不要であり、実行時にのみ必要です。

Electron なしで検証する

プレーンなブラウザでフィルムをレンダリング — パッケージ化もバイナリダウンロードも不要:

cd apps/desktop && node ../../node_modules/vite/bin/vite.js --host 127.0.0.1 --port 5174
page.add_init_script("""
window.hermesDesktop = {
  guestOnboardingEnabled: true, skipIntro: false,
  introReveal: { ready(){}, skip(){},
    open(){ return Promise.resolve() }, close(){ return Promise.resolve() },
    onSkip(){ return () => {} }, onClosed(){ return () => {} } }
};
""")
page.goto("http://127.0.0.1:5174/?win=intro")
page.evaluate("import('/src/components/intro-reveal/intro-root.tsx').then(m => m.mountIntroReveal())")

ルート自身のマウント呼び出しはブリッジがないと no-op になります。スタブ後の手動インポートが実際に描画を行います。音声トラックを確認するには、window.Audio をスタブしてインスタンスを記録 — currentTime がフィルムとともに進み、カット時点で paused === true, volume === 0 であることをアサートします。

src/store/intro-reveal.test.ts に残すべき契約テスト:

  • hermes-intro-reveal-seen-v1 が設定されていても、通常の起動時に再生されること
  • 正確に1回だけ再生されること — finishIntroReveal() が再アームしてはならない
  • オプトアウトキーと skipIntro 起動フラグを尊重すること
  • オンボーディングの初回実行セマンティクスを変更しないこと

落とし穴

  • playedThisSession を削除すると、無限リプレイループが発生し、バグのない起動にはなりません。
  • INTRO_TOTAL_MS はスコア時間です。INTRO_PACE を忘れると、ボイスオーバーが4.4秒早く切れます。
  • メインプロセスの enabled フラグとオーバーレイ自身のガードは別々のゲートです。どちらかを見逃すと、エラーもなく何もレンダリングされません。
  • 4つの独立したレイヤーが画面の再表示を制御しています(通常のクローズ、Esc + 1.2秒フォールバック、ローカルデッドマン、34秒メインプロセスウォッチドッグ)。タイミングを変更するときに1つを削除しないでください。
  • hermes update はアプリを公式ビルドに置き換えます。パッチは git に残ります。アップデート後はインストーラーを再実行してください。

関連スキル