@mobileai/react-native 0.4.2 → 0.4.3
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/README.md +21 -2
- package/lib/module/components/AIAgent.js +216 -5
- package/lib/module/components/AIAgent.js.map +1 -1
- package/lib/module/components/AgentChatBar.js +358 -36
- package/lib/module/components/AgentChatBar.js.map +1 -1
- package/lib/module/core/AgentRuntime.js +122 -6
- package/lib/module/core/AgentRuntime.js.map +1 -1
- package/lib/module/core/systemPrompt.js +57 -0
- package/lib/module/core/systemPrompt.js.map +1 -1
- package/lib/module/index.js +8 -0
- package/lib/module/index.js.map +1 -1
- package/lib/module/providers/GeminiProvider.js +108 -85
- package/lib/module/providers/GeminiProvider.js.map +1 -1
- package/lib/module/services/AudioInputService.js +128 -0
- package/lib/module/services/AudioInputService.js.map +1 -0
- package/lib/module/services/AudioOutputService.js +154 -0
- package/lib/module/services/AudioOutputService.js.map +1 -0
- package/lib/module/services/VoiceService.js +362 -0
- package/lib/module/services/VoiceService.js.map +1 -0
- package/lib/module/utils/audioUtils.js +49 -0
- package/lib/module/utils/audioUtils.js.map +1 -0
- package/lib/module/utils/logger.js +21 -4
- package/lib/module/utils/logger.js.map +1 -1
- package/lib/typescript/babel.config.d.ts +10 -0
- package/lib/typescript/babel.config.d.ts.map +1 -0
- package/lib/typescript/eslint.config.d.mts +3 -0
- package/lib/typescript/eslint.config.d.mts.map +1 -0
- package/lib/typescript/fetch-models.d.mts +2 -0
- package/lib/typescript/fetch-models.d.mts.map +1 -0
- package/lib/typescript/list-all-models.d.mts +2 -0
- package/lib/typescript/list-all-models.d.mts.map +1 -0
- package/lib/typescript/list-models.d.mts +2 -0
- package/lib/typescript/list-models.d.mts.map +1 -0
- package/lib/typescript/src/components/AIAgent.d.ts +8 -2
- package/lib/typescript/src/components/AIAgent.d.ts.map +1 -1
- package/lib/typescript/src/components/AgentChatBar.d.ts +19 -2
- package/lib/typescript/src/components/AgentChatBar.d.ts.map +1 -1
- package/lib/typescript/src/core/AgentRuntime.d.ts +17 -1
- package/lib/typescript/src/core/AgentRuntime.d.ts.map +1 -1
- package/lib/typescript/src/core/systemPrompt.d.ts +8 -0
- package/lib/typescript/src/core/systemPrompt.d.ts.map +1 -1
- package/lib/typescript/src/core/types.d.ts +24 -1
- package/lib/typescript/src/core/types.d.ts.map +1 -1
- package/lib/typescript/src/index.d.ts +6 -1
- package/lib/typescript/src/index.d.ts.map +1 -1
- package/lib/typescript/src/providers/GeminiProvider.d.ts +22 -18
- package/lib/typescript/src/providers/GeminiProvider.d.ts.map +1 -1
- package/lib/typescript/src/services/AudioInputService.d.ts +31 -0
- package/lib/typescript/src/services/AudioInputService.d.ts.map +1 -0
- package/lib/typescript/src/services/AudioOutputService.d.ts +34 -0
- package/lib/typescript/src/services/AudioOutputService.d.ts.map +1 -0
- package/lib/typescript/src/services/VoiceService.d.ts +73 -0
- package/lib/typescript/src/services/VoiceService.d.ts.map +1 -0
- package/lib/typescript/src/utils/audioUtils.d.ts +17 -0
- package/lib/typescript/src/utils/audioUtils.d.ts.map +1 -0
- package/lib/typescript/src/utils/logger.d.ts +4 -0
- package/lib/typescript/src/utils/logger.d.ts.map +1 -1
- package/package.json +24 -8
- package/src/components/AIAgent.tsx +222 -3
- package/src/components/AgentChatBar.tsx +487 -42
- package/src/core/AgentRuntime.ts +131 -2
- package/src/core/systemPrompt.ts +62 -0
- package/src/core/types.ts +30 -0
- package/src/index.ts +16 -0
- package/src/providers/GeminiProvider.ts +105 -89
- package/src/services/AudioInputService.ts +141 -0
- package/src/services/AudioOutputService.ts +167 -0
- package/src/services/VoiceService.ts +409 -0
- package/src/utils/audioUtils.ts +54 -0
- package/src/utils/logger.ts +24 -7
|
@@ -1,83 +1,81 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
|
|
3
3
|
/**
|
|
4
|
-
* GeminiProvider — Gemini API integration
|
|
4
|
+
* GeminiProvider — Gemini API integration via @google/genai SDK.
|
|
5
5
|
*
|
|
6
|
-
* Uses
|
|
7
|
-
*
|
|
8
|
-
*
|
|
6
|
+
* Uses the official Google GenAI SDK for:
|
|
7
|
+
* - generateContent with structured function calling (agent_step)
|
|
8
|
+
* - inlineData for vision (base64 screenshots)
|
|
9
|
+
* - System instructions
|
|
10
|
+
*
|
|
11
|
+
* Implements the AIProvider interface so it can be swapped
|
|
12
|
+
* with OpenAIProvider, AnthropicProvider, etc.
|
|
9
13
|
*/
|
|
10
14
|
|
|
15
|
+
import { GoogleGenAI, FunctionCallingConfigMode, Type } from '@google/genai';
|
|
11
16
|
import { logger } from "../utils/logger.js";
|
|
12
17
|
// ─── Constants ─────────────────────────────────────────────────
|
|
13
18
|
|
|
14
19
|
const AGENT_STEP_FN = 'agent_step';
|
|
15
20
|
|
|
16
|
-
// Reasoning fields
|
|
21
|
+
// Reasoning fields always present in the agent_step schema
|
|
17
22
|
const REASONING_FIELDS = ['previous_goal_eval', 'memory', 'plan'];
|
|
18
23
|
|
|
19
|
-
// ─── Gemini API Types ──────────────────────────────────────────
|
|
20
|
-
|
|
21
24
|
// ─── Provider ──────────────────────────────────────────────────
|
|
22
25
|
|
|
23
26
|
export class GeminiProvider {
|
|
24
27
|
constructor(apiKey, model = 'gemini-2.5-flash') {
|
|
25
|
-
this.
|
|
28
|
+
this.ai = new GoogleGenAI({
|
|
29
|
+
apiKey
|
|
30
|
+
});
|
|
26
31
|
this.model = model;
|
|
27
32
|
}
|
|
28
|
-
async generateContent(systemPrompt, userMessage, tools, history) {
|
|
29
|
-
logger.info('GeminiProvider', `Sending request. Model: ${this.model}, Tools: ${tools.length}`);
|
|
33
|
+
async generateContent(systemPrompt, userMessage, tools, history, screenshot) {
|
|
34
|
+
logger.info('GeminiProvider', `Sending request. Model: ${this.model}, Tools: ${tools.length}${screenshot ? ', with screenshot' : ''}`);
|
|
30
35
|
|
|
31
36
|
// Build single agent_step function declaration
|
|
32
37
|
const agentStepDeclaration = this.buildAgentStepDeclaration(tools);
|
|
33
38
|
|
|
34
|
-
// Build
|
|
35
|
-
const contents = this.buildContents(userMessage, history);
|
|
36
|
-
|
|
37
|
-
// Make API request
|
|
38
|
-
const url = `https://generativelanguage.googleapis.com/v1beta/models/${this.model}:generateContent?key=${this.apiKey}`;
|
|
39
|
-
const body = {
|
|
40
|
-
contents,
|
|
41
|
-
tools: [{
|
|
42
|
-
functionDeclarations: [agentStepDeclaration]
|
|
43
|
-
}],
|
|
44
|
-
systemInstruction: {
|
|
45
|
-
parts: [{
|
|
46
|
-
text: systemPrompt
|
|
47
|
-
}]
|
|
48
|
-
},
|
|
49
|
-
// Force the model to always call agent_step
|
|
50
|
-
tool_config: {
|
|
51
|
-
function_calling_config: {
|
|
52
|
-
mode: 'ANY',
|
|
53
|
-
allowed_function_names: [AGENT_STEP_FN]
|
|
54
|
-
}
|
|
55
|
-
},
|
|
56
|
-
generationConfig: {
|
|
57
|
-
temperature: 0.2,
|
|
58
|
-
maxOutputTokens: 2048
|
|
59
|
-
}
|
|
60
|
-
};
|
|
39
|
+
// Build contents (user message + optional screenshot)
|
|
40
|
+
const contents = this.buildContents(userMessage, history, screenshot);
|
|
61
41
|
const startTime = Date.now();
|
|
62
42
|
try {
|
|
63
|
-
const response = await
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
43
|
+
const response = await this.ai.models.generateContent({
|
|
44
|
+
model: this.model,
|
|
45
|
+
contents,
|
|
46
|
+
config: {
|
|
47
|
+
systemInstruction: systemPrompt,
|
|
48
|
+
tools: [{
|
|
49
|
+
functionDeclarations: [agentStepDeclaration]
|
|
50
|
+
}],
|
|
51
|
+
toolConfig: {
|
|
52
|
+
functionCallingConfig: {
|
|
53
|
+
mode: FunctionCallingConfigMode.ANY,
|
|
54
|
+
allowedFunctionNames: [AGENT_STEP_FN]
|
|
55
|
+
}
|
|
56
|
+
},
|
|
57
|
+
temperature: 0.2,
|
|
58
|
+
maxOutputTokens: 2048
|
|
59
|
+
}
|
|
69
60
|
});
|
|
70
61
|
const elapsed = Date.now() - startTime;
|
|
71
62
|
logger.info('GeminiProvider', `Response received in ${elapsed}ms`);
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
63
|
+
|
|
64
|
+
// Extract token usage from SDK response
|
|
65
|
+
const tokenUsage = this.extractTokenUsage(response);
|
|
66
|
+
if (tokenUsage) {
|
|
67
|
+
logger.info('GeminiProvider', `Tokens: ${tokenUsage.promptTokens} in / ${tokenUsage.completionTokens} out / $${tokenUsage.estimatedCostUSD.toFixed(6)}`);
|
|
76
68
|
}
|
|
77
|
-
const
|
|
78
|
-
|
|
69
|
+
const result = this.parseAgentStepResponse(response, tools);
|
|
70
|
+
result.tokenUsage = tokenUsage;
|
|
71
|
+
return result;
|
|
79
72
|
} catch (error) {
|
|
80
73
|
logger.error('GeminiProvider', 'Request failed:', error.message);
|
|
74
|
+
|
|
75
|
+
// Preserve HTTP error format for backward compatibility with tests
|
|
76
|
+
if (error.status) {
|
|
77
|
+
throw new Error(`Gemini API error ${error.status}: ${error.message}`);
|
|
78
|
+
}
|
|
81
79
|
throw error;
|
|
82
80
|
}
|
|
83
81
|
}
|
|
@@ -99,7 +97,6 @@ export class GeminiProvider {
|
|
|
99
97
|
const actionProperties = {};
|
|
100
98
|
for (const tool of tools) {
|
|
101
99
|
for (const [paramName, param] of Object.entries(tool.parameters)) {
|
|
102
|
-
// Skip if already added (shared field names like 'text', 'index')
|
|
103
100
|
if (actionProperties[paramName]) continue;
|
|
104
101
|
actionProperties[paramName] = {
|
|
105
102
|
type: this.mapParamType(param.type),
|
|
@@ -120,28 +117,25 @@ export class GeminiProvider {
|
|
|
120
117
|
name: AGENT_STEP_FN,
|
|
121
118
|
description: `Execute one agent step. Choose an action and provide reasoning.\n\nAvailable actions:\n${toolDescriptions}`,
|
|
122
119
|
parameters: {
|
|
123
|
-
type:
|
|
120
|
+
type: Type.OBJECT,
|
|
124
121
|
properties: {
|
|
125
|
-
// ── Reasoning fields ──
|
|
126
122
|
previous_goal_eval: {
|
|
127
|
-
type:
|
|
123
|
+
type: Type.STRING,
|
|
128
124
|
description: 'One-sentence assessment of your last action. State success, failure, or uncertain. Skip on first step.'
|
|
129
125
|
},
|
|
130
126
|
memory: {
|
|
131
|
-
type:
|
|
127
|
+
type: Type.STRING,
|
|
132
128
|
description: 'Key facts to remember for future steps: progress made, items found, counters, field values already collected.'
|
|
133
129
|
},
|
|
134
130
|
plan: {
|
|
135
|
-
type:
|
|
131
|
+
type: Type.STRING,
|
|
136
132
|
description: 'Your immediate next goal — what action you will take and why.'
|
|
137
133
|
},
|
|
138
|
-
// ── Action selection ──
|
|
139
134
|
action_name: {
|
|
140
|
-
type:
|
|
135
|
+
type: Type.STRING,
|
|
141
136
|
description: 'Which action to execute.',
|
|
142
137
|
enum: toolNames
|
|
143
138
|
},
|
|
144
|
-
// ── Action parameters (flat) ──
|
|
145
139
|
...actionProperties
|
|
146
140
|
},
|
|
147
141
|
required: ['plan', 'action_name']
|
|
@@ -151,48 +145,52 @@ export class GeminiProvider {
|
|
|
151
145
|
mapParamType(type) {
|
|
152
146
|
switch (type) {
|
|
153
147
|
case 'number':
|
|
154
|
-
return
|
|
148
|
+
return Type.NUMBER;
|
|
155
149
|
case 'integer':
|
|
156
|
-
return
|
|
150
|
+
return Type.INTEGER;
|
|
157
151
|
case 'boolean':
|
|
158
|
-
return
|
|
152
|
+
return Type.BOOLEAN;
|
|
159
153
|
case 'string':
|
|
160
154
|
default:
|
|
161
|
-
return
|
|
155
|
+
return Type.STRING;
|
|
162
156
|
}
|
|
163
157
|
}
|
|
164
158
|
|
|
165
159
|
// ─── Build Contents ────────────────────────────────────────
|
|
166
160
|
|
|
167
161
|
/**
|
|
168
|
-
* Builds
|
|
169
|
-
*
|
|
170
|
-
* Each step is a STATELESS single-turn request (matching page-agent's approach):
|
|
171
|
-
* - System prompt has general instructions
|
|
172
|
-
* - User message contains full context: task, history, screen state
|
|
173
|
-
* - Model responds with agent_step function call
|
|
174
|
-
*
|
|
175
|
-
* History is embedded as text in assembleUserPrompt (via <agent_history>),
|
|
176
|
-
* NOT as functionCall/functionResponse pairs. This avoids Gemini's
|
|
177
|
-
* conversation format requirements and thought_signature complexity.
|
|
162
|
+
* Builds contents for the generateContent call.
|
|
163
|
+
* Single-turn: user message + optional screenshot as inlineData.
|
|
178
164
|
*/
|
|
179
|
-
buildContents(userMessage, _history) {
|
|
165
|
+
buildContents(userMessage, _history, screenshot) {
|
|
166
|
+
const parts = [{
|
|
167
|
+
text: userMessage
|
|
168
|
+
}];
|
|
169
|
+
|
|
170
|
+
// Append screenshot as inlineData for Gemini vision
|
|
171
|
+
if (screenshot) {
|
|
172
|
+
parts.push({
|
|
173
|
+
inlineData: {
|
|
174
|
+
mimeType: 'image/jpeg',
|
|
175
|
+
data: screenshot
|
|
176
|
+
}
|
|
177
|
+
});
|
|
178
|
+
}
|
|
180
179
|
return [{
|
|
181
180
|
role: 'user',
|
|
182
|
-
parts
|
|
183
|
-
text: userMessage
|
|
184
|
-
}]
|
|
181
|
+
parts
|
|
185
182
|
}];
|
|
186
183
|
}
|
|
187
184
|
|
|
188
185
|
// ─── Parse Response ────────────────────────────────────────
|
|
189
186
|
|
|
190
187
|
/**
|
|
191
|
-
* Parses the
|
|
192
|
-
* Extracts structured reasoning + action
|
|
188
|
+
* Parses the SDK response expecting a single agent_step function call.
|
|
189
|
+
* Extracts structured reasoning + action.
|
|
193
190
|
*/
|
|
194
|
-
parseAgentStepResponse(
|
|
195
|
-
|
|
191
|
+
parseAgentStepResponse(response, tools) {
|
|
192
|
+
const candidates = response.candidates || [];
|
|
193
|
+
if (candidates.length === 0) {
|
|
196
194
|
logger.warn('GeminiProvider', 'No candidates in response');
|
|
197
195
|
return {
|
|
198
196
|
toolCalls: [{
|
|
@@ -210,7 +208,7 @@ export class GeminiProvider {
|
|
|
210
208
|
text: 'No response generated.'
|
|
211
209
|
};
|
|
212
210
|
}
|
|
213
|
-
const candidate =
|
|
211
|
+
const candidate = candidates[0];
|
|
214
212
|
const parts = candidate.content?.parts || [];
|
|
215
213
|
|
|
216
214
|
// Find the function call part
|
|
@@ -260,11 +258,9 @@ export class GeminiProvider {
|
|
|
260
258
|
};
|
|
261
259
|
}
|
|
262
260
|
|
|
263
|
-
// Build action args:
|
|
261
|
+
// Build action args: extract only the params that belong to the matched tool
|
|
264
262
|
const actionArgs = {};
|
|
265
263
|
const reservedKeys = new Set([...REASONING_FIELDS, 'action_name']);
|
|
266
|
-
|
|
267
|
-
// Find the matching tool to know which params belong to it
|
|
268
264
|
const matchedTool = tools.find(t => t.name === actionName);
|
|
269
265
|
if (matchedTool) {
|
|
270
266
|
for (const paramName of Object.keys(matchedTool.parameters)) {
|
|
@@ -273,7 +269,6 @@ export class GeminiProvider {
|
|
|
273
269
|
}
|
|
274
270
|
}
|
|
275
271
|
} else {
|
|
276
|
-
// Custom/registered tool — grab all non-reserved fields
|
|
277
272
|
for (const [key, value] of Object.entries(args)) {
|
|
278
273
|
if (!reservedKeys.has(key)) {
|
|
279
274
|
actionArgs[key] = value;
|
|
@@ -290,5 +285,33 @@ export class GeminiProvider {
|
|
|
290
285
|
text: textPart?.text
|
|
291
286
|
};
|
|
292
287
|
}
|
|
288
|
+
|
|
289
|
+
// ─── Token Usage Extraction ─────────────────────────────────
|
|
290
|
+
|
|
291
|
+
/**
|
|
292
|
+
* Extracts token usage from SDK response and calculates estimated cost.
|
|
293
|
+
*
|
|
294
|
+
* Pricing (Gemini 2.5 Flash):
|
|
295
|
+
* - Input: $0.30 / 1M tokens
|
|
296
|
+
* - Output: $2.50 / 1M tokens
|
|
297
|
+
*/
|
|
298
|
+
extractTokenUsage(response) {
|
|
299
|
+
const meta = response?.usageMetadata;
|
|
300
|
+
if (!meta) return undefined;
|
|
301
|
+
const promptTokens = meta.promptTokenCount ?? 0;
|
|
302
|
+
const completionTokens = meta.candidatesTokenCount ?? 0;
|
|
303
|
+
const totalTokens = meta.totalTokenCount ?? promptTokens + completionTokens;
|
|
304
|
+
|
|
305
|
+
// Cost estimation based on Gemini 2.5 Flash pricing
|
|
306
|
+
const INPUT_COST_PER_M = 0.30;
|
|
307
|
+
const OUTPUT_COST_PER_M = 2.50;
|
|
308
|
+
const estimatedCostUSD = promptTokens / 1_000_000 * INPUT_COST_PER_M + completionTokens / 1_000_000 * OUTPUT_COST_PER_M;
|
|
309
|
+
return {
|
|
310
|
+
promptTokens,
|
|
311
|
+
completionTokens,
|
|
312
|
+
totalTokens,
|
|
313
|
+
estimatedCostUSD
|
|
314
|
+
};
|
|
315
|
+
}
|
|
293
316
|
}
|
|
294
317
|
//# sourceMappingURL=GeminiProvider.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"names":["logger","AGENT_STEP_FN","REASONING_FIELDS","GeminiProvider","constructor","apiKey","model","generateContent","systemPrompt","userMessage","tools","history","info","length","agentStepDeclaration","buildAgentStepDeclaration","contents","buildContents","
|
|
1
|
+
{"version":3,"names":["GoogleGenAI","FunctionCallingConfigMode","Type","logger","AGENT_STEP_FN","REASONING_FIELDS","GeminiProvider","constructor","apiKey","model","ai","generateContent","systemPrompt","userMessage","tools","history","screenshot","info","length","agentStepDeclaration","buildAgentStepDeclaration","contents","buildContents","startTime","Date","now","response","models","config","systemInstruction","functionDeclarations","toolConfig","functionCallingConfig","mode","ANY","allowedFunctionNames","temperature","maxOutputTokens","elapsed","tokenUsage","extractTokenUsage","promptTokens","completionTokens","estimatedCostUSD","toFixed","result","parseAgentStepResponse","error","message","status","Error","toolNames","map","t","name","actionProperties","tool","paramName","param","Object","entries","parameters","type","mapParamType","description","enum","toolDescriptions","params","keys","join","OBJECT","properties","previous_goal_eval","STRING","memory","plan","action_name","required","NUMBER","INTEGER","BOOLEAN","_history","parts","text","push","inlineData","mimeType","data","role","candidates","warn","toolCalls","args","success","reasoning","previousGoalEval","candidate","content","fnCallPart","find","p","functionCall","textPart","actionName","actionArgs","reservedKeys","Set","matchedTool","undefined","key","value","has","meta","usageMetadata","promptTokenCount","candidatesTokenCount","totalTokens","totalTokenCount","INPUT_COST_PER_M","OUTPUT_COST_PER_M"],"sourceRoot":"../../../src","sources":["providers/GeminiProvider.ts"],"mappings":"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","ignoreList":[]}
|
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* AudioInputService — Real-time microphone capture for voice mode.
|
|
5
|
+
*
|
|
6
|
+
* Uses react-native-audio-api (Software Mansion) AudioRecorder for native
|
|
7
|
+
* PCM streaming from the microphone. Each chunk is converted from Float32
|
|
8
|
+
* to Int16 PCM and base64-encoded for the Gemini Live API.
|
|
9
|
+
*
|
|
10
|
+
* Requires: react-native-audio-api (development build only, not Expo Go)
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
import { logger } from "../utils/logger.js";
|
|
14
|
+
import { float32ToInt16Base64 } from "../utils/audioUtils.js";
|
|
15
|
+
|
|
16
|
+
// ─── Types ─────────────────────────────────────────────────────
|
|
17
|
+
|
|
18
|
+
// ─── Service ───────────────────────────────────────────────────
|
|
19
|
+
|
|
20
|
+
export class AudioInputService {
|
|
21
|
+
status = 'idle';
|
|
22
|
+
recorder = null;
|
|
23
|
+
constructor(config) {
|
|
24
|
+
this.config = config;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
// ─── Lifecycle ─────────────────────────────────────────────
|
|
28
|
+
|
|
29
|
+
async start() {
|
|
30
|
+
try {
|
|
31
|
+
// Lazy-load react-native-audio-api (optional peer dependency)
|
|
32
|
+
let audioApi;
|
|
33
|
+
try {
|
|
34
|
+
audioApi = require('react-native-audio-api');
|
|
35
|
+
} catch {
|
|
36
|
+
const msg = 'Voice mode requires react-native-audio-api. Install with: npm install react-native-audio-api';
|
|
37
|
+
logger.error('AudioInput', msg);
|
|
38
|
+
this.config.onError?.(msg);
|
|
39
|
+
return false;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
// Request mic permission (Android)
|
|
43
|
+
try {
|
|
44
|
+
const {
|
|
45
|
+
PermissionsAndroid,
|
|
46
|
+
Platform
|
|
47
|
+
} = require('react-native');
|
|
48
|
+
if (Platform.OS === 'android') {
|
|
49
|
+
const result = await PermissionsAndroid.request(PermissionsAndroid.PERMISSIONS.RECORD_AUDIO);
|
|
50
|
+
if (result !== PermissionsAndroid.RESULTS.GRANTED) {
|
|
51
|
+
logger.warn('AudioInput', 'Microphone permission denied');
|
|
52
|
+
this.config.onPermissionDenied?.();
|
|
53
|
+
return false;
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
} catch {
|
|
57
|
+
// Permission check failed — continue and let native layer handle it
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
// Create AudioRecorder
|
|
61
|
+
this.recorder = new audioApi.AudioRecorder();
|
|
62
|
+
const sampleRate = this.config.sampleRate || 16000;
|
|
63
|
+
const bufferLength = this.config.bufferLength || 4096;
|
|
64
|
+
|
|
65
|
+
// Register audio data callback
|
|
66
|
+
let frameCount = 0;
|
|
67
|
+
this.recorder.onAudioReady({
|
|
68
|
+
sampleRate,
|
|
69
|
+
bufferLength,
|
|
70
|
+
channelCount: 1
|
|
71
|
+
}, event => {
|
|
72
|
+
frameCount++;
|
|
73
|
+
try {
|
|
74
|
+
// event.buffer is an AudioBuffer — get Float32 channel data
|
|
75
|
+
const float32Data = event.buffer.getChannelData(0);
|
|
76
|
+
// Convert Float32 → Int16 → base64 for Gemini
|
|
77
|
+
const base64Chunk = float32ToInt16Base64(float32Data);
|
|
78
|
+
logger.debug('AudioInput', `🎤 Frame #${frameCount}: size=${base64Chunk.length}`);
|
|
79
|
+
this.config.onAudioChunk(base64Chunk);
|
|
80
|
+
} catch (err) {
|
|
81
|
+
logger.error('AudioInput', `Frame processing error: ${err.message}`);
|
|
82
|
+
}
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
// Register error callback
|
|
86
|
+
this.recorder.onError(error => {
|
|
87
|
+
logger.error('AudioInput', `Recorder error: ${error.message || error}`);
|
|
88
|
+
this.config.onError?.(error.message || String(error));
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
// Start recording
|
|
92
|
+
this.recorder.start();
|
|
93
|
+
this.status = 'recording';
|
|
94
|
+
logger.info('AudioInput', `Streaming started (${sampleRate}Hz, bufLen=${bufferLength})`);
|
|
95
|
+
return true;
|
|
96
|
+
} catch (error) {
|
|
97
|
+
logger.error('AudioInput', `Failed to start: ${error.message}`);
|
|
98
|
+
this.config.onError?.(error.message);
|
|
99
|
+
return false;
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
async stop() {
|
|
103
|
+
try {
|
|
104
|
+
if (this.recorder && this.status !== 'idle') {
|
|
105
|
+
this.recorder.clearOnAudioReady();
|
|
106
|
+
this.recorder.clearOnError();
|
|
107
|
+
this.recorder.stop();
|
|
108
|
+
}
|
|
109
|
+
this.recorder = null;
|
|
110
|
+
this.status = 'idle';
|
|
111
|
+
logger.info('AudioInput', 'Streaming stopped');
|
|
112
|
+
} catch (error) {
|
|
113
|
+
logger.error('AudioInput', `Failed to stop: ${error.message}`);
|
|
114
|
+
this.recorder = null;
|
|
115
|
+
this.status = 'idle';
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
// ─── Status ───────────────────────────────────────────────
|
|
120
|
+
|
|
121
|
+
get isRecording() {
|
|
122
|
+
return this.status === 'recording';
|
|
123
|
+
}
|
|
124
|
+
get currentStatus() {
|
|
125
|
+
return this.status;
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
//# sourceMappingURL=AudioInputService.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"names":["logger","float32ToInt16Base64","AudioInputService","status","recorder","constructor","config","start","audioApi","require","msg","error","onError","PermissionsAndroid","Platform","OS","result","request","PERMISSIONS","RECORD_AUDIO","RESULTS","GRANTED","warn","onPermissionDenied","AudioRecorder","sampleRate","bufferLength","frameCount","onAudioReady","channelCount","event","float32Data","buffer","getChannelData","base64Chunk","debug","length","onAudioChunk","err","message","String","info","stop","clearOnAudioReady","clearOnError","isRecording","currentStatus"],"sourceRoot":"../../../src","sources":["services/AudioInputService.ts"],"mappings":"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","ignoreList":[]}
|
|
@@ -0,0 +1,154 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* AudioOutputService — AI speech playback for voice mode.
|
|
5
|
+
*
|
|
6
|
+
* Uses react-native-audio-api (Software Mansion) for gapless, low-latency
|
|
7
|
+
* PCM playback. Decodes base64 PCM from Gemini Live API and queues it via
|
|
8
|
+
* AudioBufferQueueSourceNode for seamless streaming.
|
|
9
|
+
*
|
|
10
|
+
* Requires: react-native-audio-api (development build only, not Expo Go)
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
import { logger } from "../utils/logger.js";
|
|
14
|
+
import { base64ToFloat32 } from "../utils/audioUtils.js";
|
|
15
|
+
|
|
16
|
+
// ─── Types ─────────────────────────────────────────────────────
|
|
17
|
+
|
|
18
|
+
/** Gemini Live API outputs 24kHz 16-bit mono PCM */
|
|
19
|
+
const GEMINI_OUTPUT_SAMPLE_RATE = 24000;
|
|
20
|
+
// ─── Service ───────────────────────────────────────────────────
|
|
21
|
+
|
|
22
|
+
export class AudioOutputService {
|
|
23
|
+
audioContext = null;
|
|
24
|
+
queueSourceNode = null;
|
|
25
|
+
gainNode = null;
|
|
26
|
+
muted = false;
|
|
27
|
+
isStarted = false;
|
|
28
|
+
chunkCount = 0;
|
|
29
|
+
constructor(config = {}) {
|
|
30
|
+
this.config = config;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
// ─── Lifecycle ─────────────────────────────────────────────
|
|
34
|
+
|
|
35
|
+
async initialize() {
|
|
36
|
+
try {
|
|
37
|
+
let audioApi;
|
|
38
|
+
try {
|
|
39
|
+
audioApi = require('react-native-audio-api');
|
|
40
|
+
} catch {
|
|
41
|
+
const msg = 'react-native-audio-api is required for audio output. Install with: npm install react-native-audio-api';
|
|
42
|
+
logger.error('AudioOutput', msg);
|
|
43
|
+
this.config.onError?.(msg);
|
|
44
|
+
return false;
|
|
45
|
+
}
|
|
46
|
+
const sampleRate = this.config.sampleRate || GEMINI_OUTPUT_SAMPLE_RATE;
|
|
47
|
+
|
|
48
|
+
// Create AudioContext at Gemini's output sample rate
|
|
49
|
+
this.audioContext = new audioApi.AudioContext({
|
|
50
|
+
sampleRate
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
// Create GainNode for mute control
|
|
54
|
+
this.gainNode = this.audioContext.createGain();
|
|
55
|
+
this.gainNode.gain.value = 1.0;
|
|
56
|
+
this.gainNode.connect(this.audioContext.destination);
|
|
57
|
+
|
|
58
|
+
// Create AudioBufferQueueSourceNode for gapless streaming
|
|
59
|
+
this.queueSourceNode = this.audioContext.createBufferQueueSource();
|
|
60
|
+
this.queueSourceNode.connect(this.gainNode);
|
|
61
|
+
logger.info('AudioOutput', `Initialized (${sampleRate}Hz, AudioBufferQueueSourceNode)`);
|
|
62
|
+
return true;
|
|
63
|
+
} catch (error) {
|
|
64
|
+
logger.error('AudioOutput', `Failed to initialize: ${error.message}`);
|
|
65
|
+
this.config.onError?.(error.message);
|
|
66
|
+
return false;
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
// ─── Enqueue Audio ─────────────────────────────────────────
|
|
71
|
+
|
|
72
|
+
/** Add a base64-encoded PCM chunk from Gemini to the playback queue */
|
|
73
|
+
enqueue(base64Audio) {
|
|
74
|
+
if (this.muted || !this.audioContext || !this.queueSourceNode) return;
|
|
75
|
+
try {
|
|
76
|
+
this.chunkCount++;
|
|
77
|
+
|
|
78
|
+
// Decode base64 Int16 PCM → Float32
|
|
79
|
+
const float32Data = base64ToFloat32(base64Audio);
|
|
80
|
+
const sampleRate = this.config.sampleRate || GEMINI_OUTPUT_SAMPLE_RATE;
|
|
81
|
+
|
|
82
|
+
// Create an AudioBuffer and fill it with PCM data
|
|
83
|
+
const audioBuffer = this.audioContext.createBuffer(1, float32Data.length, sampleRate);
|
|
84
|
+
audioBuffer.copyToChannel(float32Data, 0);
|
|
85
|
+
|
|
86
|
+
// Enqueue the buffer for gapless playback
|
|
87
|
+
this.queueSourceNode.enqueueBuffer(audioBuffer);
|
|
88
|
+
|
|
89
|
+
// Start playback on first enqueue
|
|
90
|
+
if (!this.isStarted) {
|
|
91
|
+
this.queueSourceNode.start();
|
|
92
|
+
this.isStarted = true;
|
|
93
|
+
this.config.onPlaybackStart?.();
|
|
94
|
+
logger.info('AudioOutput', '▶️ Playback started');
|
|
95
|
+
}
|
|
96
|
+
if (this.chunkCount % 20 === 0) {
|
|
97
|
+
logger.debug('AudioOutput', `Queued chunk #${this.chunkCount}`);
|
|
98
|
+
}
|
|
99
|
+
} catch (error) {
|
|
100
|
+
logger.error('AudioOutput', `Enqueue error: ${error.message}`);
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
// ─── Mute/Unmute ──────────────────────────────────────────
|
|
105
|
+
|
|
106
|
+
mute() {
|
|
107
|
+
this.muted = true;
|
|
108
|
+
if (this.gainNode) {
|
|
109
|
+
this.gainNode.gain.value = 0;
|
|
110
|
+
}
|
|
111
|
+
logger.info('AudioOutput', 'Speaker muted');
|
|
112
|
+
}
|
|
113
|
+
unmute() {
|
|
114
|
+
this.muted = false;
|
|
115
|
+
if (this.gainNode) {
|
|
116
|
+
this.gainNode.gain.value = 1.0;
|
|
117
|
+
}
|
|
118
|
+
logger.info('AudioOutput', 'Speaker unmuted');
|
|
119
|
+
}
|
|
120
|
+
get isMuted() {
|
|
121
|
+
return this.muted;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
// ─── Stop & Cleanup ───────────────────────────────────────
|
|
125
|
+
|
|
126
|
+
async stop() {
|
|
127
|
+
try {
|
|
128
|
+
if (this.queueSourceNode && this.isStarted) {
|
|
129
|
+
this.queueSourceNode.stop();
|
|
130
|
+
this.queueSourceNode.clearBuffers();
|
|
131
|
+
}
|
|
132
|
+
this.isStarted = false;
|
|
133
|
+
this.chunkCount = 0;
|
|
134
|
+
this.config.onPlaybackEnd?.();
|
|
135
|
+
logger.info('AudioOutput', 'Playback stopped');
|
|
136
|
+
} catch (error) {
|
|
137
|
+
logger.error('AudioOutput', `Stop error: ${error.message}`);
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
async cleanup() {
|
|
141
|
+
await this.stop();
|
|
142
|
+
try {
|
|
143
|
+
if (this.audioContext) {
|
|
144
|
+
await this.audioContext.close();
|
|
145
|
+
}
|
|
146
|
+
} catch {
|
|
147
|
+
// Non-critical
|
|
148
|
+
}
|
|
149
|
+
this.audioContext = null;
|
|
150
|
+
this.queueSourceNode = null;
|
|
151
|
+
this.gainNode = null;
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
//# sourceMappingURL=AudioOutputService.js.map
|