@graineai/inapp-react-native 0.3.1 → 0.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/audio.d.ts CHANGED
@@ -15,3 +15,29 @@ export declare function decodeAgentAudio(bytes: Uint8Array, declared: {
15
15
  sampleRate?: number | null;
16
16
  } | null, latched: AudioFormat | null, fallbackPcmRate?: number): DecodedAudio | null;
17
17
  export declare function base64ToBytes(b64: string): Uint8Array;
18
+ export declare class EchoGuard {
19
+ private readonly opts;
20
+ private echoFloor;
21
+ private noiseFloor;
22
+ private heldMs;
23
+ private preRoll;
24
+ private audibleFrames;
25
+ constructor(opts?: {
26
+ overEcho?: number;
27
+ overNoise?: number;
28
+ minLevel?: number;
29
+ sustainMs?: number;
30
+ preRollFrames?: number;
31
+ warmupFrames?: number;
32
+ });
33
+ private get overEcho();
34
+ private get overNoise();
35
+ private get minLevel();
36
+ private get sustainMs();
37
+ private get preRollFrames();
38
+ private get warmupFrames();
39
+ get threshold(): number;
40
+ get interrupting(): boolean;
41
+ reset(): void;
42
+ filter(base64: string, pcm: Int16Array, agentAudible: boolean, frameMs: number): string[];
43
+ }
package/dist/audio.js CHANGED
@@ -80,3 +80,72 @@ export function base64ToBytes(b64) {
80
80
  }
81
81
  return bytes.subarray(0, byte);
82
82
  }
83
+ export class EchoGuard {
84
+ constructor(opts = {}) {
85
+ this.opts = opts;
86
+ this.echoFloor = 0;
87
+ this.noiseFloor = 0;
88
+ this.heldMs = 0;
89
+ this.preRoll = [];
90
+ this.audibleFrames = 0;
91
+ }
92
+ get overEcho() { return this.opts.overEcho ?? 2.2; }
93
+ get overNoise() { return this.opts.overNoise ?? 4; }
94
+ get minLevel() { return this.opts.minLevel ?? 0.012; }
95
+ get sustainMs() { return this.opts.sustainMs ?? 240; }
96
+ get preRollFrames() { return this.opts.preRollFrames ?? 12; }
97
+ get warmupFrames() { return this.opts.warmupFrames ?? 10; }
98
+ get threshold() {
99
+ return Math.max(this.minLevel, this.echoFloor * this.overEcho, this.noiseFloor * this.overNoise);
100
+ }
101
+ get interrupting() {
102
+ return this.heldMs >= this.sustainMs;
103
+ }
104
+ reset() {
105
+ this.echoFloor = 0;
106
+ this.noiseFloor = 0;
107
+ this.heldMs = 0;
108
+ this.preRoll = [];
109
+ this.audibleFrames = 0;
110
+ }
111
+ filter(base64, pcm, agentAudible, frameMs) {
112
+ let sum = 0;
113
+ for (let i = 0; i < pcm.length; i++)
114
+ sum += Math.abs(pcm[i]);
115
+ const level = pcm.length ? sum / pcm.length / 32768 : 0;
116
+ if (agentAudible) {
117
+ this.audibleFrames++;
118
+ if (this.audibleFrames <= this.warmupFrames) {
119
+ this.echoFloor = Math.max(this.echoFloor, level);
120
+ this.heldMs = 0;
121
+ this.preRoll.push(base64);
122
+ if (this.preRoll.length > this.preRollFrames)
123
+ this.preRoll.shift();
124
+ return [];
125
+ }
126
+ if (level > this.echoFloor && level <= this.threshold) {
127
+ this.echoFloor = level;
128
+ }
129
+ else {
130
+ this.echoFloor *= 0.999;
131
+ }
132
+ this.heldMs = level >= this.threshold ? this.heldMs + frameMs : 0;
133
+ }
134
+ else {
135
+ this.noiseFloor = this.noiseFloor === 0 ? level : this.noiseFloor * 0.95 + level * 0.05;
136
+ this.heldMs = 0;
137
+ this.audibleFrames = 0;
138
+ this.preRoll = [];
139
+ return [base64];
140
+ }
141
+ if (level < this.threshold || !this.interrupting) {
142
+ this.preRoll.push(base64);
143
+ if (this.preRoll.length > this.preRollFrames)
144
+ this.preRoll.shift();
145
+ return [];
146
+ }
147
+ const out = this.preRoll.length ? [...this.preRoll, base64] : [base64];
148
+ this.preRoll = [];
149
+ return out;
150
+ }
151
+ }
package/dist/index.d.ts CHANGED
@@ -1,7 +1,7 @@
1
1
  export type { ActionHandler, AppActionRequest, AppActionResult, AppEvent, ScreenContext, ScreenField, } from "./protocol";
2
2
  export { GraineInAppClient, type GraineInAppOptions, type SessionConfig } from "./client";
3
3
  export { VoiceSession, liveAudioStreamAdapter, base64ToPcm16, CAPTURE_SAMPLE_RATE, type AudioAdapter, type VoiceSessionOptions, } from "./voice";
4
- export { decodeAgentAudio, decodeMuLaw, decodePcm16, base64ToBytes, type AudioFormat, type DecodedAudio, } from "./audio";
4
+ export { EchoGuard, decodeAgentAudio, decodeMuLaw, decodePcm16, base64ToBytes, type AudioFormat, type DecodedAudio, } from "./audio";
5
5
  export { addMaskRule, maskDeep, maskString } from "./mask";
6
6
  export { GraineProvider, useGraineAgent, useGraineScreen, useGraineAction, useGraineVoice, type GraineProviderProps, type Turn, type Caption, } from "./react-native/index";
7
7
  export { GraineAgentBar, GraineLauncher, type GraineAgentBarProps, type GraineLauncherProps } from "./react-native/ui";
package/dist/index.js CHANGED
@@ -1,6 +1,6 @@
1
1
  export { GraineInAppClient } from "./client";
2
2
  export { VoiceSession, liveAudioStreamAdapter, base64ToPcm16, CAPTURE_SAMPLE_RATE, } from "./voice";
3
- export { decodeAgentAudio, decodeMuLaw, decodePcm16, base64ToBytes, } from "./audio";
3
+ export { EchoGuard, decodeAgentAudio, decodeMuLaw, decodePcm16, base64ToBytes, } from "./audio";
4
4
  export { addMaskRule, maskDeep, maskString } from "./mask";
5
5
  export { GraineProvider, useGraineAgent, useGraineScreen, useGraineAction, useGraineVoice, } from "./react-native/index";
6
6
  export { GraineAgentBar, GraineLauncher } from "./react-native/ui";
package/dist/voice.d.ts CHANGED
@@ -20,6 +20,8 @@ export declare class VoiceSession {
20
20
  private offs;
21
21
  private capturing;
22
22
  private format;
23
+ private echo;
24
+ private agentAudible;
23
25
  constructor(client: GraineInAppClient, adapter: AudioAdapter, options?: VoiceSessionOptions);
24
26
  get active(): boolean;
25
27
  start(): Promise<void>;
package/dist/voice.js CHANGED
@@ -1,4 +1,4 @@
1
- import { base64ToBytes, decodeAgentAudio, decodePcm16 } from "./audio";
1
+ import { EchoGuard, base64ToBytes, decodeAgentAudio, decodePcm16 } from "./audio";
2
2
  export const CAPTURE_SAMPLE_RATE = 16000;
3
3
  export const MIN_PLAYOUT_BUFFER_SECONDS = 0.15;
4
4
  export function base64ToPcm16(b64) {
@@ -12,6 +12,8 @@ export class VoiceSession {
12
12
  this.offs = [];
13
13
  this.capturing = false;
14
14
  this.format = null;
15
+ this.echo = new EchoGuard();
16
+ this.agentAudible = false;
15
17
  }
16
18
  get active() {
17
19
  return this.capturing;
@@ -31,9 +33,13 @@ export class VoiceSession {
31
33
  }),
32
34
  this.client.on("clear", () => {
33
35
  this.adapter.clear();
36
+ this.agentAudible = false;
34
37
  this.options.onAgentSpeaking?.(false);
35
38
  }),
36
- this.client.on("agent_speaking", (speaking) => this.options.onAgentSpeaking?.(speaking)),
39
+ this.client.on("agent_speaking", (speaking) => {
40
+ this.agentAudible = speaking;
41
+ this.options.onAgentSpeaking?.(speaking);
42
+ }),
37
43
  this.client.on("transcript", ({ text, final }) => {
38
44
  if (final)
39
45
  this.client.noteInteraction();
@@ -41,13 +47,24 @@ export class VoiceSession {
41
47
  }),
42
48
  ];
43
49
  this.client.setPlayoutClock(() => this.adapter.bufferedSeconds?.() ?? 0);
44
- await this.adapter.startCapture((base64) => this.client.sendAudio(base64, CAPTURE_SAMPLE_RATE));
50
+ this.echo.reset();
51
+ await this.adapter.startCapture((base64) => {
52
+ const pcm = base64ToPcm16(base64);
53
+ const frameMs = (pcm.length / CAPTURE_SAMPLE_RATE) * 1000;
54
+ const buffered = this.adapter.bufferedSeconds?.();
55
+ const audible = buffered !== undefined ? buffered > 0.02 || this.agentAudible : this.agentAudible;
56
+ for (const frame of this.echo.filter(base64, pcm, audible, frameMs)) {
57
+ this.client.sendAudio(frame, CAPTURE_SAMPLE_RATE);
58
+ }
59
+ });
45
60
  }
46
61
  async stop() {
47
62
  if (!this.capturing)
48
63
  return;
49
64
  this.capturing = false;
50
65
  this.format = null;
66
+ this.agentAudible = false;
67
+ this.echo.reset();
51
68
  this.offs.forEach((off) => off());
52
69
  this.offs = [];
53
70
  this.adapter.clear();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@graineai/inapp-react-native",
3
- "version": "0.3.1",
3
+ "version": "0.4.0",
4
4
  "description": "Graine in-app agent for React Native — an agent that sees the screen your customer is on and can act on it.",
5
5
  "license": "MIT",
6
6
  "private": false,