Skip to content

fix(mobile): stabilize scene realtime audio flow - #142

Merged
fj-sunny merged 1 commit into
1024XEngineer:mainfrom
HansonL622:codex/fix-scene-zero-score
Aug 20, 2026
Merged

fix(mobile): stabilize scene realtime audio flow#142
fj-sunny merged 1 commit into
1024XEngineer:mainfrom
HansonL622:codex/fix-scene-zero-score

Conversation

@HansonL622

Copy link
Copy Markdown
Contributor

Summary

  • reuse the WebRTC microphone PCM for asynchronous scene-turn scoring instead of opening a competing Android AudioRecord
  • explicitly cancel the active Realtime response when mobile VAD detects barge-in
  • fully release reading-stage audio before entering the realtime speaking stage
  • package the native WebRTC PCM tap as an Expo config plugin so clean prebuilds preserve the fix

Root cause

Mobile scene practice opened WebRTC at 48 kHz for realtime conversation and AudioStudio at 16 kHz for WAV scoring at the same time. On the OnePlus test device the two Android recording sessions competed for the microphone route, which caused premature VAD stops, delayed interruption, short or invalid scoring audio, and the initial AI preparing state.

Validation

  • completed a full scene flow on a OnePlus 8T and received an 82-point report without the insufficient-valid-speech error
  • confirmed only one WebRTC recording chain remains active during scene dialogue
  • 15 mobile test suites, 94 tests passed
  • mobile TypeScript check passed
  • mobile lint passed with 0 errors
  • clean Expo Android prebuild passed
  • Android arm64 debug build and physical-device installation passed

No Web or backend implementation files are changed.

@HansonL622
HansonL622 marked this pull request as ready for review August 20, 2026 08:01
@HansonL622
HansonL622 requested a review from fj-sunny August 20, 2026 08:03
@fj-sunny
fj-sunny merged commit ce3561a into 1024XEngineer:main Aug 20, 2026
13 checks passed

@fennoai fennoai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Reviewed the Android PCM tap, JS capture bridge, realtime barge-in cancellation, and read-to-speak recorder handoff. The focused controller/screen/audio tests (43 tests), TypeScript check, and clean Expo Android prebuild pass. The new capture path still has two lifecycle issues that can exhaust memory or silently skip scoring after a native failure. An Android Kotlin compile was attempted but the Gradle distribution download was blocked by the environment's TLS certificate validation.

View job run

sampleRate = rate
}
if (format != audioFormat || channels != channelCount || rate != sampleRate) return
segment.write(data)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[P2] Bound the PCM captured while waiting for speech

enableSceneInputAfterRecordingStarts() starts this segment before the learner begins speaking, so every WebRTC microphone frame is appended here for the entire time the scene sits ready. Because ByteArrayOutputStream has no cap or streaming sink, an idle screen accumulates about 96 KB/s at 48 kHz mono PCM16 (roughly 173 MB in 30 minutes), and takeSegment() then creates several additional full-size copies during decode/resample/encoding. This can OOM the mobile process or produce an unexpectedly huge upload after a delayed response; use a bounded pre-roll/ring buffer or stream the segment to a file with a duration/size limit.

stopPromise = Promise.resolve(nativeTap.stopSegment());
}
if (stopPromise) await stopPromise;
const uri = finalized ? await nativeTap.takeSegment() : null;

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[P2] Clear capture state when native take fails

If nativeTap.takeSegment() rejects (the native module explicitly rejects file creation/write failures), execution skips the two reset lines below. RealtimeSessionController.takeTurnAudioUri() catches that rejection and returns null, leaving this object with finalized === true; the next learner turn's start() therefore returns early and is not recorded. A rejected stopPromise is worse because every later take() rethrows the same promise. Reset the JS state in a finally block so a transient native failure cannot disable subsequent turn scoring.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants