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つの編集(すべて必須)
src/store/intro-reveal.ts—isIntroRevealEnabled()がゲストオンボーディングを必須としないように変更。shouldPlayFirstRunIntro()はオンボーディング外では!playedThisSessionを返すようにします。モジュールレベルのplayedThisSessionフラグは必須です。ゲートのエフェクトはフェーズが'hidden'に戻るたびに再実行されますが、フィルム終了時にまさにそれが起こるため、フラグがないと無限ループになります。src/components/intro-reveal/intro-root.tsx— オーバーレイウィンドウはisOnboardingEnabled()でガードされており、その条件が偽だと描画前に return します。メインレンダラーと同じ述語に切り替えます。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 に残ります。アップデート後はインストーラーを再実行してください。