Animate the frog's mouth and menu-window critter from live mic level - #233
Conversation
#11 asked for the mascot to move with live input on both surfaces. The overlay already did; the menu wordmark never subscribed to mic-level, and the mouth did not move on either one. The mouth was the subtler half. FrogMascot suppresses its croak CSS class whenever sacScale is set, and that class is the only thing that swaps the closed smile for the open shape -- so driving sacScale from the mic inflated the throat and left the mouth in a closed smile for the whole dictation. The mouth now rides the same signal, crossfading and scaling the jaw so it reads as opening rather than as one shape dissolving into another. The reduction and the smoothing curve move into src/lib/mic-level.ts as pure functions, so the critter feels the same in both windows instead of drifting apart in two copies; the overlay's existing math is preserved exactly. useMicLevel wraps them for the menu side and releases to rest on its own, because mic-level stops without a final zero frame and a hook that only updated on events would freeze the critter half-inflated until the next dictation. It also tracks prefers-reduced-motion as it changes rather than reading it once per mount. Fixes a listener leak found along the way: RecordingOverlay returned its useEffect cleanup from an inner async function, so useEffect got a promise instead of the cleanup and all three listeners survived unmount. LiveFrog hands over sacScale only while a level is actually driving it, since a resting 0 is still "defined" and would silently kill click-croak. wake-20260727T0805-0faa25
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 1d53f70c1d
ℹ️ About Codex in GitHub
Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".
The overlay subscribes to mic-level itself, because it needs the per-band values for its bars, so it never went through useMicLevel and never inherited that hook's reduced-motion gating. Reduced-motion users got the mouth and vocal sac animating on every frame anyway. RecordingOverlay.css could not have caught this. The amplitude reaches the critter as an inline style, which beats a stylesheet rule, and that file's reduced-motion block only covers the transcribing text and dots. So the preference is read in the component and folded into the same expression that already rests the critter while transcribing. usePrefersReducedMotion is exported for that, since any future caller driving a critter from its own subscription has the same obligation.
|
Fixed in c855e32. Confirmed the mechanism before fixing: The preference is now read in the component and folded into the expression that already rests the critter while transcribing, and One pre-existing sibling I am not changing here: the overlay's level bars set |
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: c855e326bd
ℹ️ About Codex in GitHub
Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".
useMicLevel had two writers of the live amplitude and only one applied the rest snap. The release tick snapped; the event listener did not. A stream of zero-valued frames -- the settings mic monitor sitting open -- keeps refreshing lastFrameAtRef, so the release tick's silence check never fires and cannot cover for the event path, which smooths asymptotically toward 0 without arriving. The visible cost is the click-croak: LiveFrog maps a nonzero amp to a defined sacScale, and FrogMascot suppresses the croak class whenever sacScale is set. So the frog looked closed but stopped croaking on click until the stream closed. Fixed in the writer both paths share rather than at the second call site, and moved the rule into mic-level.ts as settleToRest so it is unit-testable without a webview. That module exists to stop this math drifting apart in two copies, which is the shape of this bug. The new silent-frame test fails against the old inline snap.
|
@codex review |
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: a04ad13609
ℹ️ About Codex in GitHub
Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".
tauri.conf.json sets minimumSystemVersion 10.15, and Catalina's WKWebView implements matchMedia without addEventListener -- that landed in Safari 14. So reduceMotionQuery() returns a live object that passes the null check and then throws the moment the effect subscribes to it. usePrefersReducedMotion mounts inside both LiveFrog and RecordingOverlay, so on those installs the throw would take the window's UI down. A blank app because of an animation preference is a bad trade. Feature-detects and falls back to addListener/removeListener. The helper is exported because the fallback branch cannot run in a modern engine: the only way to prove it is to call it with a query object shaped like the old one, which is what the new tests do. They fail if the detection is removed.
|
@codex review |
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 28d6c23b13
ℹ️ About Codex in GitHub
Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".
|
@codex review |
|
Codex Review: Didn't find any major issues. Nice work! Reviewed commit: ℹ️ About Codex in GitHubCodex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you
If Codex has suggestions, it will comment; otherwise it will react with 👍. When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback". |
Progresses #11. The overlay critter already moved with your voice; the menu wordmark never subscribed to
mic-level, and the mouth did not move on either surface.The mouth was the part that was actually broken
FrogMascotsuppresses itscroakCSS class wheneversacScaleis set (stateClass), and.frog.croakis the only rule that swapsmouthClosedformouthOpeninFrogMascot.css. So mic-driven animation inflated the throat sac and left the mouth sitting in its closed smile for an entire dictation — the one thing #11's acceptance criteria names.The mouth now rides the same signal as the sac: opacity crossfades between the two shapes and the open shape scales on Y from a
transformOriginat the top of the jaw, so it reads as opening rather than as one shape dissolving into another. No new prop and no contract change —sacScalestays the single mic-level input every critter honors.One signal, one feel
src/lib/mic-level.tsholds the two pure functions both surfaces need:bandsToAmplitude— 16 spectrum bands to one 0..1 value, by peak so a voice concentrated in a few bands still reads as loud. This is the overlay's previous inline expression moved verbatim (peak x 1.4, clamped), so the overlay's feel is unchanged.smoothAmplitude— the fast-attack / slow-release curveInputLevelMeteralready uses, which is what makes a level read as a voice instead of as flicker.Pure, so the feel is unit-testable without a webview, a mic, or Tauri.
useMicLevelwraps them for the menu side.Two bugs found while wiring it
The hook has to release to rest by itself.
mic-levelstops firing when dictation ends and sends no final zero frame, so a hook that only updated on events would freeze at whatever the last syllable measured and leave the critter half-inflated until the next dictation. It now decays to exact 0 once frames stop, which is also why a caller needs no recording-state gate.RecordingOverlayleaked its listeners. TheuseEffectcleanup was returned from the innerasyncfunction, souseEffectreceived a promise rather than the cleanup and all three listeners survived unmount. Fixed with thecleanup/cancelledpattern, which also covers unmounting mid-await.Details worth knowing
LiveFrogpassessacScaleonly while a level is actually driving it. A resting0is still "defined" and would suppress the croak class, silently killing the click-croak the component exists to produce.micLeveldefaults totrue.LiveFrog's only caller is the wordmark (HandyTextLogo), which is the menu-window avatar Animate the mascot's mouth with live input amplitude (talk along while dictating) #11 names, and the hook is inert when no frames flow. Flip the default if you'd rather opt in per surface.prefers-reduced-motionis tracked as it changes, not read once per mount, so toggling it mid-session takes effect.Verification
bun test src67 pass / 0 fail (10 new),tsc --noEmitclean,eslint srcclean,prettier --checkclean — the four things CI gates. Two codex passes, no findings.Not claiming #11 done: the acceptance criteria also cover whichever critter is selected (#8), and the frog is still the only critter, so that half is untestable until a second one lands.
wake-20260727T0805-0faa25