Cinemática de Introdução do Hermes
O Hermes Desktop já vem com um filme de introdução de 22 segundos — sete beats, um cubo 3D, agentes paralelos, a marca Nous — com som sintetizado. Ele existe, mas quase ninguém o vê duas vezes: a elegibilidade é guestOnboardingEnabled && !seen.
Esta skill faz o filme tocar em toda inicialização e permite que você adicione sua própria faixa de narração sobre os sinais embutidos.
Quando usar
- "Faça a animação inicial do Hermes tocar sempre."
- "Coloque meu áudio na animação de boot — em camadas, não substituindo os efeitos sonoros."
- Qualquer alteração em
apps/desktop/src/components/intro-reveal/.
O que o filme já contém
| Peça | Caminho |
|---|---|
| Portão (quando toca) | apps/desktop/src/components/intro-reveal/index.tsx |
| Elegibilidade + store de fase | apps/desktop/src/store/intro-reveal.ts |
| Boot de sobreposição | src/components/intro-reveal/intro-root.tsx |
| Clock, pontuação, canvas | use-intro-clock.ts, timeline.ts, viewport-cube.ts |
| Sinais sonoros | sound.ts |
| Janela nativa | electron/intro-reveal-window.ts, electron/main.ts |
Matemática de tempo: INTRO_TOTAL_MS = 17 600 é o tempo da partitura; o tempo real é × INTRO_PACE (1.25) = 22 000 ms, mais INTRO_EXIT_MS 900 ms → 23 125 ms no total.
Reproduzir em toda inicialização — três edições, todas necessárias
src/store/intro-reveal.ts—isIntroRevealEnabled()deve parar de exigir onboarding do convidado;shouldPlayFirstRunIntro()retorna!playedThisSessionfora do onboarding. Uma flagplayedThisSessionno nível do módulo é obrigatória: o efeito do portão é reexecutado sempre que a fase retorna para'hidden', que é exatamente o que terminar o filme faz — sem a flag, ele entra em loop infinito.src/components/intro-reveal/intro-root.tsx— a janela de sobreposição protege comisOnboardingEnabled()e retornaria antes de pintar. Troque para o mesmo predicado do renderizador principal.electron/main.ts—introRevealControlleré construído comenabled: GUEST_ONBOARDING, então o processo principal rejeitaopen. Defina comotrue; a vivacidade é decidida no renderizador.
O beginOnboardingFlow() / queueGuideAfterIntro() / takeGuideShape() do onboarding se autoprotegem com isOnboardingEnabled(), então eles não operam quando o onboarding está desligado — nenhuma alteração extra é necessária.
Interruptor: localStorage['hermes-intro-reveal-always-v1'] = '0', ou inicie com HERMES_SKIP_INTRO=1 / --skip-intro.
Adicionar uma faixa de áudio em camadas
Coloque um arquivo em apps/desktop/public/intro-voiceover.mp3 (o vite copia public/ para dist/ literalmente), então em sound.ts:
export function startVoiceover(cutAtMs: number): { stop: () => void }
— crie um elemento Audio, defina volume, inicie e setTimeout(stop, cutAtMs - 300) para que os últimos 300 ms diminuam em vez de estalar. Chame-o ao lado de startPad() em use-intro-clock.ts e pare-o na limpeza do efeito.
Use um elemento <audio>, nunca fetch() + decodeAudioData(). O renderizador empacotado roda em file://, onde fetch e decodeAudioData são bloqueados por CORS; elementos de mídia carregam assets file:// sem problemas.
As duas faixas são independentes por construção — o áudio do elemento e os sinais WebAudio se misturam no nível do sistema operacional. Nada é silenciado. Conecte $hapticsMuted a ambas e assine em vez de ler uma vez, ou o alternador de mudo dessincroniza.
Ponto de corte:
const cutMs = Math.round((INTRO_TOTAL_MS + INTRO_EXIT_MS) * INTRO_PACE) // 23 125
Enviar para um app empacotado
As edições no código fonte não fazem nada até que o asar seja reempacotado. Duas armadilhas aqui, ambas valem a verificação:
Encontre o app que o usuário realmente inicia — nunca presuma /Applications.
ps aux | grep '[H]ermes.app/Contents/MacOS/Hermes'
defaults read com.apple.dock persistent-apps | grep -o '/[^"]*Hermes\.app'
As atualizações do Hermes vão para ~/.hermes/hermes-agent*/apps/desktop/release/mac-arm64/Hermes.app e o Dock muitas vezes aponta para lá, enquanto /Applications/Hermes.app pode ser uma cópia antiga 0.0.0. Confirme com CFBundleShortVersionString (0.17.x é real, 0.0.0 não é).
Atualize também app.asar.unpacked/dist/. As builds empacotadas listam dist/** em asarUnpack, e resolveRendererIndex() prefere essa cópia — reempacotar apenas o asar e manter a árvore desempacotada antiga a obscurece.
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 # edições no processo principal precisam deste passo também
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" # ad-hoc; o Hermes vem assinado ad-hoc
--ignore-scripts é importante em redes lentas: o download do binário Electron não é necessário para construir ou reempacotar, apenas para executar o dev.
Verificar sem Electron
Renderize o filme em um navegador comum — sem empacotamento, sem download de binário:
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())")
A chamada de montagem da própria rota não opera sem a ponte; a importação manual após o stub é o que realmente pinta. Para verificar a faixa de áudio, crie um stub de window.Audio e registre as instâncias — afirme que currentTime avança com o filme e que paused === true, volume === 0 no corte.
Testes de contrato que valem manter em src/store/intro-reveal.test.ts:
- toca em uma inicialização normal mesmo quando
hermes-intro-reveal-seen-v1está definido - toca exatamente uma vez —
finishIntroReveal()não deve rearmá-lo - respeita a chave de desativação e a flag
skipIntrode inicialização - deixa a semântica de primeira execução do onboarding intacta
Armadilhas
- Remover
playedThisSessioncausa um loop infinito de reprodução, não uma inicialização sem bugs. INTRO_TOTAL_MSé tempo da partitura. EsquecerINTRO_PACEcorta a narração 4,4 s antes.- A flag
enableddo processo principal e a própria proteção da sobreposição são portões separados; perca qualquer um e nada renderiza, sem erro. - Quatro camadas independentes mantêm a tela voltando (fechamento normal, Esc + 1,2 s de fallback, deadman local, watchdog do processo principal de 34 s). Não remova uma enquanto altera o tempo.
hermes updatesubstitui o app pela build oficial. O patch persiste no git; execute novamente o instalador após a atualização.