One-line summary
Replace the always-on green pulsing live dot with a status-aware, amplitude-reactive indicator (quiet → soft color; louder speech → greener / stronger pulse; yellow while waiting; red on capture error).
Problem / motivation
Today the glass-bar dot is binary and misleading:
- It only appears when
audioSourceId !== 'none' and audioStatus === 'active' (AvatarStage.jsx → lipSyncLive).
- Once shown, CSS always paints a bright green pulse (
avatar-glass-bar__live-dot) — independent of loudness. Silence looks the same as talking.
- There is no bar-level signal for starting / waiting, or for capture failure (those only show as text inside Voice).
useAudioSource already exposes level, speaking, status, and error. Mouth visemes already track amplitude; the bar indicator does not. Users (and docs) treat “green dot = lip sync working,” but a stuck-green silent capture is hard to tell from healthy audio.
Proposed solution
Keep the same 8px glass-bar slot left of the gear. Drive appearance from capture status + analyser level (no new audio pipeline).
Visibility
| Condition |
Dot |
| Audio source Off |
Hidden (unchanged) |
Source selected (any non-none) |
Visible — even before active, so waiting/error can show |
Status colors (discrete)
Map audioStatus (and error) to a base state. Prefer semantic colors, not “always soft green”:
| State |
When |
Look |
Hover / aria-label (draft) |
| Waiting |
idle / starting, or active with no usable signal yet |
Soft yellow / amber pastel (calm, not alarm) |
“Lip sync waiting for audio” |
| Live quiet |
active, level near silence |
Soft pastel green-grey / very light green; little or no pulse |
“Lip sync listening” |
| Live active |
active, level / speaking rising |
Greener fill + glow; pulse rate / scale track amplitude (louder → stronger / faster; quieter → soft settle) |
“Lip sync active” |
| Error |
error (permission denied, device gone, capture failed) |
Soft red / coral; optional slow pulse |
“Lip sync error — open Voice” |
Recommendation: use yellow for waiting, not soft green — so “armed but quiet” and “actually receiving audio” stay distinct. Soft green = healthy capture with little/no voice.
Amplitude reactivity (continuous, while Live)
While status is live (active):
- Feed existing
level (0…1-ish from useAudioAnalyser) into CSS custom properties on the dot (e.g. --live-level, --live-pulse), updated from the same frame path that already drives lip sync — avoid setState every frame if it causes bar re-renders; prefer a ref + direct style / requestAnimationFrame on the span, or pass level only into a tiny child that owns the DOM node.
- Map level → green intensity (pastel → saturated lime), glow strength, and pulse scale/opacity.
- On sustained silence, ease back to Live quiet (soft green-grey), not full bright green.
- Optional light hue shift within greens is fine; avoid rainbow / distracting neon on an always-on-top overlay.
Implementation sketch (non-binding)
- Extend
AvatarStageShell props: e.g. liveDotStatus + liveDotLevel (or one derived liveDot object) instead of boolean lipSyncLive.
- Derive status in
AvatarStage from audioStatus / audioError / level / speaking.
- CSS: modifier classes
.avatar-glass-bar__live-dot--waiting|quiet|active|error + variables for continuous green.
- Retire the fixed
1.8s infinite pulse as the only motion; keep a gentle idle pulse for waiting if desired.
- Docs: Using the app live-dot section, Lip sync, Audio sources; screenshot
11-bar-live-dot.png (and any GIF) when visuals land — may fold into the docs visual overhaul.
Accessibility / chrome
- Do not rely on color alone: keep distinct
title / aria-label per state; optional click → open Voice drawer later (out of scope unless cheap).
- Respect glass-bar chrome contrast (light vs dark environments): pastels must stay readable on both; no heavy bloom that washes the bar.
- Prefer reduced-motion: if
prefers-reduced-motion: reduce, freeze pulse and use static color only.
Alternatives considered
| Approach |
Why not / later |
| Keep binary green; only document “silence still green” |
Still misleading; Voice status text is easy to miss with the drawer closed |
| Full waveform / mini VU meter on the bar |
Too busy for an 8px companion chrome; scope creep |
| Soft green as the waiting state |
Collides with “healthy listening”; yellow reads clearer as pending |
| Red only inside Voice panel |
Failures stay invisible on the bar; users stare at a still mouth |
| Drive pulse from mouth viseme weights |
Coupled to VRM path; analyser level is the right source of truth |
Primary surface
Glass bar / menus
Constraints you accept
Mock / sketch / reference (optional)
Same glass-bar slot as today (docs/screenshots/11-bar-live-dot.png), four states in one row:
[ · yellow ] waiting → [ · soft green ] quiet → [ ● bright green pulse ] talking → [ · red ] error
No new bar controls — color + motion only.
Out of scope (separate / later)
- Click-dot-to-open Voice / restart capture
- Per-source icons on the bar
- Phoneme-accurate coloring
- Configurable colors in Settings (defaults only for v1)
Acceptance ideas
One-line summary
Replace the always-on green pulsing live dot with a status-aware, amplitude-reactive indicator (quiet → soft color; louder speech → greener / stronger pulse; yellow while waiting; red on capture error).
Problem / motivation
Today the glass-bar dot is binary and misleading:
audioSourceId !== 'none'andaudioStatus === 'active'(AvatarStage.jsx→lipSyncLive).avatar-glass-bar__live-dot) — independent of loudness. Silence looks the same as talking.useAudioSourcealready exposeslevel,speaking,status, anderror. Mouth visemes already track amplitude; the bar indicator does not. Users (and docs) treat “green dot = lip sync working,” but a stuck-green silent capture is hard to tell from healthy audio.Proposed solution
Keep the same 8px glass-bar slot left of the gear. Drive appearance from capture status + analyser level (no new audio pipeline).
Visibility
none)active, so waiting/error can showStatus colors (discrete)
Map
audioStatus(and error) to a base state. Prefer semantic colors, not “always soft green”:aria-label(draft)idle/starting, oractivewith no usable signal yetactive, level near silenceactive, level /speakingrisingerror(permission denied, device gone, capture failed)Recommendation: use yellow for waiting, not soft green — so “armed but quiet” and “actually receiving audio” stay distinct. Soft green = healthy capture with little/no voice.
Amplitude reactivity (continuous, while Live)
While status is live (
active):level(0…1-ish fromuseAudioAnalyser) into CSS custom properties on the dot (e.g.--live-level,--live-pulse), updated from the same frame path that already drives lip sync — avoidsetStateevery frame if it causes bar re-renders; prefer a ref + direct style /requestAnimationFrameon the span, or pass level only into a tiny child that owns the DOM node.Implementation sketch (non-binding)
AvatarStageShellprops: e.g.liveDotStatus+liveDotLevel(or one derivedliveDotobject) instead of booleanlipSyncLive.AvatarStagefromaudioStatus/audioError/level/speaking..avatar-glass-bar__live-dot--waiting|quiet|active|error+ variables for continuous green.1.8sinfinite pulse as the only motion; keep a gentle idle pulse for waiting if desired.11-bar-live-dot.png(and any GIF) when visuals land — may fold into the docs visual overhaul.Accessibility / chrome
title/aria-labelper state; optional click → open Voice drawer later (out of scope unless cheap).prefers-reduced-motion: reduce, freeze pulse and use static color only.Alternatives considered
levelis the right source of truthPrimary surface
Glass bar / menus
Constraints you accept
Mock / sketch / reference (optional)
Same glass-bar slot as today (
docs/screenshots/11-bar-live-dot.png), four states in one row:[ · yellow ] waiting→[ · soft green ] quiet→[ ● bright green pulse ] talking→[ · red ] errorNo new bar controls — color + motion only.
Out of scope (separate / later)
Acceptance ideas
starting/idle→ yellow waiting (not bright green)active+ silence → soft quiet green; no aggressive pulseactive+ speech/music → green intensity and pulse track loudness; settle when quiet