Communitygithub.com

Hermes 시작 시네마틱

Hermes Desktop의 시작 애니메이션을 매번 재생하고, 자체 음성 해설 트랙을 추가할 수 있게 해주는 기술입니다.

Hermes 시작 시네마틱란 무엇인가요?

이 스킬은 Hermes Desktop에 내장된 22초 길이의 인트로 시네마틱을 매 부팅 시 재생하도록 설정하고, 사용자 지정 음성 해석 트랙을 추가하여 기존 사운드와 함께 재생할 수 있게 해줍니다. 기본적으로 인트로는 첫 방문 시에만 한 번 재생되지만, 이 스킬은 재생 조건을 변경하여 항상 재생되도록 합니다. 필요에 따라 `localStorage` 값이나 `--skip-intro` 플래그로 비활성화할 수 있습니다. 음성 해석은 'sound.ts'에서 Audio 엘리먼트를 사용하여 구현되며, Web Audio API를 사용하는 기존 신디사이저 사운드와 독립적으로 믹싱되어 OS 레벨에서 자연스럽게 합쳐집니다. 패키징된 앱에서 수정 사항을 적용할 때는 asar 파일을 다시 패킹하고 'app.asar.unpacked/dist/'도 업데이트해야 합니다. Electron 없이도 Vite 개발 서버와 Python을 이용한 브라우저 스텁으로 영화 렌더링과 오디오 트랙 동작을 확인할 수 있습니다.

지원 대상~Claude Code~Codex CLI~Cursor
npx skills add xbmaxx/hermes-intro-cinematic

즐겨 사용하는 AI에게 물어보기

이 에이전트 스킬이 미리 로드된 새 채팅을 엽니다.

문서

Hermes 인트로 시네마틱

Hermes Desktop에는 이미 22초짜리 인트로 필름이 내장되어 있습니다. 7비트의 비트, 라이브 큐브 뷰포트, 병렬 에이전트, Nous 브랜드, 합성 사운드로 구성되어 있습니다. 존재하지만 거의 아무도 두 번 보지 않습니다. 재생 조건은 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 000ms이며, INTRO_EXIT_MS 900ms를 더하면 23 125ms가 총 재생 시간입니다.

매 실행마다 재생 — 세 가지 수정, 모두 필수

  1. src/store/intro-reveal.ts — isIntroRevealEnabled()이 더 이상 게스트 온보딩을 요구하지 않아야 합니다. shouldPlayFirstRunIntro()가 온보딩 외부에서 !playedThisSession을 반환하게 만듭니다. 모듈 수준의 playedThisSession 플래그는 필수입니다. 게이트의 효과는 단계가 'hidden'으로 돌아올 때마다 다시 실행되는데, 이것이 필름이 끝날 때 정확히 발생하는 일입니다. 플래그가 없으면 무한 반복 루프가 발생합니다.
  2. src/components/intro-reveal/intro-root.tsx — 오버레이 창은 isOnboardingEnabled()를 확인하는 가드를 가지고 있어, 이 조건이 true가 아니면 렌더링하지 않고 반환합니다. 메인 렌더러와 동일한 조건을 사용하도록 변경합니다.
  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)으로 마지막 300ms 동안은 소리가 끊기지 않도록 램프 다운을 적용합니다. use-intro-clock.ts의 startPad() 옆에서 호출하고, 효과 정리(cleanup)에서 중지합니다.

<audio> 엘리먼트를 사용하세요. 절대 fetch() + decodeAudioData()를 사용하지 마세요. 패키징된 렌더러는 file://에서 실행되며, 여기서 fetch와 decodeAudioData는 CORS에 의해 차단되지만, 미디어 엘리먼트는 file:// 자산을 정상적으로 로드합니다.

두 트랙은 설계상 독립적입니다. 엘리먼트 오디오와 WebAudio 큐는 OS 레벨에서 믹싱됩니다. 아무것도 덕킹(ducking)하지 않습니다. 두 트랙 모두 $hapticsMuted에 연결하고, 한 번만 읽는 것이 아니라 구독(subscribe)해야 합니다. 그렇지 않으면 음소거 토글이 동기화되지 않습니다.

컷 포인트:

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

패키징된 앱에 적용

소스 코드 수정만으로는 asar가 다시 패킹될 때까지 적용되지 않습니다. 여기에는 두 가지 주의사항이 있으며, 반드시 확인할 가치가 있습니다.

사용자가 실제로 실행하는 앱을 찾으세요. /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만 다시 패킹하면 오래된 언패킹 트리가 새것을 가리게 됩니다.

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())")

라우트 자체의 마운트 호출은 브리지가 없으면 작동하지 않습니다. 스텁 후 수동 import가 실제로 화면에 렌더링됩니다. 오디오 트랙을 확인하려면 window.Audio를 스텁하고 인스턴스를 기록합니다. currentTime이 필름과 함께 진행되고, 컷 지점에서 paused === true, volume === 0인지 확인합니다.

src/store/intro-reveal.test.ts에 보관할 가치가 있는 계약 테스트:

  • 일반 실행 시 hermes-intro-reveal-seen-v1이 설정되어 있어도 재생됩니다.
  • 정확히 한 번만 재생됩니다. finishIntroReveal()이 다시 활성화하지 않아야 합니다.
  • 옵트아웃 키와 skipIntro 실행 플래그를 존중합니다.
  • 온보딩의 첫 실행 시맨틱스를 건드리지 않습니다.

함정

  • playedThisSession을 제거하면 버그 없는 실행이 아닌 무한 재생 루프가 발생합니다.
  • INTRO_TOTAL_MS는 점수 시간입니다. INTRO_PACE를 잊으면 음성 해석이 4.4초 일찍 잘립니다.
  • 메인 프로세스의 enabled 플래그와 오버레이 자체 가드는 별개의 게이트입니다. 둘 중 하나를 놓치면 오류 없이 아무것도 렌더링되지 않습니다.
  • 화면 복귀를 유지하는 네 개의 독립적인 레이어가 있습니다 (일반 닫기, Esc + 1.2초 폴백, 로컬 데드맨, 34초 메인 프로세스 워치독). 타이밍을 변경할 때 하나를 제거하지 마세요.
  • hermes update는 공식 빌드로 앱을 교체합니다. 패치는 git에 유지됩니다. 업데이트 후 설치 프로그램을 다시 실행하세요.

관련 스킬