Skip to content

Commit 3f38214

Browse files
MarkDaoustcopybara-github
authored andcommitted
feat: voice consent signature types across all SDK languages.
Most languages capture the "setup complete" message as a property on the session object. Golang returns the setup complete to the user as the first message. So there's no need to capture it, and that would be a breaking change. PiperOrigin-RevId: 885814555
1 parent 640419d commit 3f38214

11 files changed

Lines changed: 795 additions & 29 deletions

File tree

examples/output.wav

123 KB
Binary file not shown.

examples/src/main/java/com/google/genai/examples/LiveAudioConversationAsync.java

Lines changed: 136 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -94,6 +94,8 @@ public final class LiveAudioConversationAsync {
9494
private static SourceDataLine speakerLine;
9595
private static AsyncSession session;
9696
private static ExecutorService micExecutor = Executors.newSingleThreadExecutor();
97+
private static String promptString = null;
98+
private static java.io.ByteArrayOutputStream audioResponseBytes = new java.io.ByteArrayOutputStream();
9799

98100
/** Creates the parameters for sending an audio chunk. */
99101
public static LiveSendRealtimeInputParameters createAudioContent(byte[] audioData) {
@@ -155,31 +157,95 @@ public static void main(String[] args) throws LineUnavailableException {
155157
System.out.println("Using Gemini Developer API");
156158
}
157159

160+
String getModelFromArgs = null;
161+
String voiceSamplePath = null;
162+
String voiceConsentPath = null;
163+
String voiceSignature = null;
164+
promptString = null;
165+
166+
for (String arg : args) {
167+
if (arg.startsWith("--model=")) {
168+
getModelFromArgs = arg.substring("--model=".length());
169+
} else if (arg.startsWith("--voice-sample=")) {
170+
voiceSamplePath = arg.substring("--voice-sample=".length());
171+
} else if (arg.startsWith("--voice-consent=")) {
172+
voiceConsentPath = arg.substring("--voice-consent=".length());
173+
} else if (arg.startsWith("--voice-signature=")) {
174+
voiceSignature = arg.substring("--voice-signature=".length());
175+
} else if (arg.startsWith("--prompt=")) {
176+
promptString = arg.substring("--prompt=".length());
177+
} else if (!arg.startsWith("--") && getModelFromArgs == null) {
178+
getModelFromArgs = arg;
179+
}
180+
}
181+
182+
byte[] voiceSampleAudio = null;
183+
byte[] consentAudio = null;
184+
185+
if (voiceSamplePath != null) {
186+
try {
187+
voiceSampleAudio = java.nio.file.Files.readAllBytes(java.nio.file.Paths.get(voiceSamplePath));
188+
} catch (java.io.IOException e) {
189+
throw new RuntimeException("Failed to read voice sample: " + e.getMessage());
190+
}
191+
if (voiceConsentPath == null && voiceSignature == null) {
192+
throw new IllegalArgumentException(
193+
"Either --voice-consent or --voice-signature must be provided when --voice-sample is"
194+
+ " used.");
195+
}
196+
}
197+
if (voiceConsentPath != null) {
198+
try {
199+
consentAudio = java.nio.file.Files.readAllBytes(java.nio.file.Paths.get(voiceConsentPath));
200+
} catch (java.io.IOException e) {
201+
throw new RuntimeException("Failed to read voice consent: " + e.getMessage());
202+
}
203+
}
204+
158205
final String modelId;
159-
if (args.length != 0) {
160-
modelId = args[0];
206+
if (getModelFromArgs != null) {
207+
modelId = getModelFromArgs;
161208
} else if (client.vertexAI()) {
162209
modelId = Constants.GEMINI_LIVE_MODEL_NAME;
163210
} else {
164211
modelId = Constants.GEMINI_LIVE_MODEL_NAME_PREVIEW;
165212
}
166213

167214
// --- Audio Line Setup ---
168-
microphoneLine = getMicrophoneLine();
169-
speakerLine = getSpeakerLine();
215+
if (promptString == null) {
216+
microphoneLine = getMicrophoneLine();
217+
speakerLine = getSpeakerLine();
218+
}
170219

171220
// --- Live API Config for Audio ---
172221
// Choice of ["Aoede", "Puck", "Charon", "Kore", "Fenrir", "Leda", "Orus", "Zephyr"]
173222
String voiceName = "Aoede";
223+
224+
VoiceConfig.Builder voiceConfigBuilder = VoiceConfig.builder();
225+
if (voiceSampleAudio != null) {
226+
com.google.genai.types.ReplicatedVoiceConfig.Builder repBuilder =
227+
com.google.genai.types.ReplicatedVoiceConfig.builder()
228+
.mimeType("audio/wav")
229+
.voiceSampleAudio(voiceSampleAudio);
230+
if (consentAudio != null) {
231+
repBuilder.consentAudio(consentAudio);
232+
}
233+
if (voiceSignature != null) {
234+
repBuilder.voiceConsentSignature(
235+
com.google.genai.types.VoiceConsentSignature.builder().signature(voiceSignature));
236+
}
237+
voiceConfigBuilder.replicatedVoiceConfig(repBuilder);
238+
} else {
239+
voiceConfigBuilder.prebuiltVoiceConfig(
240+
PrebuiltVoiceConfig.builder().voiceName(voiceName));
241+
}
242+
174243
LiveConnectConfig config =
175244
LiveConnectConfig.builder()
176245
.responseModalities(Modality.Known.AUDIO)
177246
.speechConfig(
178247
SpeechConfig.builder()
179-
.voiceConfig(
180-
VoiceConfig.builder()
181-
.prebuiltVoiceConfig(
182-
PrebuiltVoiceConfig.builder().voiceName(voiceName)))
248+
.voiceConfig(voiceConfigBuilder)
183249
.languageCode("en-US"))
184250
.realtimeInputConfig(
185251
RealtimeInputConfig.builder()
@@ -232,19 +298,33 @@ public static void main(String[] args) throws LineUnavailableException {
232298
session = client.async.live.connect(modelId, config).get();
233299
System.out.println("Connected.");
234300

235-
// --- Start Audio Lines ---
236-
microphoneLine.start();
237-
speakerLine.start();
238-
System.out.println("Microphone and speakers started. Speak now (Press Ctrl+C to exit)...");
301+
if (session.setupComplete() != null && session.setupComplete().voiceConsentSignature().isPresent()) {
302+
System.out.println(
303+
"\n=== Voice Consent Signature Received ===\n"
304+
+ session.setupComplete().voiceConsentSignature().get().signature().orElse("")
305+
+ "\n========================================\n");
306+
}
239307

240308
// --- Start Receiving Audio Responses ---
241309
CompletableFuture<Void> receiveFuture =
242310
session.receive(LiveAudioConversationAsync::handleAudioResponse);
243-
System.err.println("Receive stream started."); // Add this line
244-
245-
// --- Start Sending Microphone Audio ---
246-
CompletableFuture<Void> sendFuture =
247-
CompletableFuture.runAsync(LiveAudioConversationAsync::sendMicrophoneAudio, micExecutor);
311+
System.err.println("Receive stream started.");
312+
313+
CompletableFuture<Void> sendFuture;
314+
if (promptString == null) {
315+
// --- Start Audio Lines ---
316+
microphoneLine.start();
317+
speakerLine.start();
318+
System.out.println("Microphone and speakers started. Speak now (Press Ctrl+C to exit)...");
319+
320+
// --- Start Sending Microphone Audio ---
321+
sendFuture = CompletableFuture.runAsync(LiveAudioConversationAsync::sendMicrophoneAudio, micExecutor);
322+
} else {
323+
System.out.println("Sending prompt: " + promptString);
324+
session.sendRealtimeInput(
325+
LiveSendRealtimeInputParameters.builder().text(promptString).build()).get();
326+
sendFuture = CompletableFuture.completedFuture(null);
327+
}
248328

249329
// Keep the main thread alive. Wait for sending or receiving to finish (or
250330
// error).
@@ -313,13 +393,19 @@ public static void handleAudioResponse(LiveServerMessage message) {
313393
content -> {
314394
// Handle interruptions from Gemini.
315395
if (content.interrupted().orElse(false)) {
316-
speakerLine.flush();
396+
if (speakerLine != null && speakerLine.isOpen()) {
397+
speakerLine.flush();
398+
}
317399
return; // Skip processing the rest of this message's audio.
318400
}
319401

320402
// Handle Model turn completion.
321403
if (content.turnComplete().orElse(false)) {
322-
// The turn is over, no more audio will be sent for this turn.
404+
if (promptString != null) {
405+
saveWavFile();
406+
System.out.println("Response received, exiting.");
407+
System.exit(0);
408+
}
323409
return;
324410
}
325411

@@ -334,15 +420,45 @@ public static void handleAudioResponse(LiveServerMessage message) {
334420
if (speakerLine != null && speakerLine.isOpen()) {
335421
// Write audio data to the speaker
336422
speakerLine.write(audioBytes, 0, audioBytes.length);
423+
} else {
424+
System.out.println(
425+
"Received audio response chunk: " + audioBytes.length + " bytes.");
426+
}
427+
try {
428+
audioResponseBytes.write(audioBytes);
429+
} catch (java.io.IOException e) {
430+
System.err.println("Failed to accumulate audio bytes: " + e.getMessage());
337431
}
338432
});
339433

340434
// If this is the last message of a generation, drain the buffer.
341435
if (content.generationComplete().orElse(false)) {
342-
speakerLine.drain();
436+
if (speakerLine != null && speakerLine.isOpen()) {
437+
speakerLine.drain();
438+
}
343439
}
344440
});
345441
}
346442

443+
private static void saveWavFile() {
444+
byte[] audioData = audioResponseBytes.toByteArray();
445+
if (audioData.length == 0) {
446+
System.out.println("No audio data received to save.");
447+
return;
448+
}
449+
try {
450+
javax.sound.sampled.AudioInputStream ais = new javax.sound.sampled.AudioInputStream(
451+
new java.io.ByteArrayInputStream(audioData),
452+
SPEAKER_AUDIO_FORMAT,
453+
audioData.length / SPEAKER_AUDIO_FORMAT.getFrameSize()
454+
);
455+
java.io.File outputFile = new java.io.File("output.wav");
456+
javax.sound.sampled.AudioSystem.write(ais, javax.sound.sampled.AudioFileFormat.Type.WAVE, outputFile);
457+
System.out.println("Saved audio response to " + outputFile.getAbsolutePath());
458+
} catch (Exception e) {
459+
System.err.println("Failed to save WAV file: " + e.getMessage());
460+
}
461+
}
462+
347463
private LiveAudioConversationAsync() {}
348464
}

src/main/java/com/google/genai/AsyncLive.java

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@
2525
import com.google.genai.types.LiveConnectConfig;
2626
import com.google.genai.types.LiveConnectParameters;
2727
import com.google.genai.types.LiveServerMessage;
28+
import com.google.genai.types.LiveServerSetupComplete;
2829
import java.io.IOException;
2930
import java.net.URI;
3031
import java.net.URISyntaxException;
@@ -283,11 +284,13 @@ private void handleIncomingMessage(String message) {
283284
try {
284285
LiveServerMessage initialResponse = LiveServerMessage.fromJson(message);
285286
if (initialResponse.setupComplete().isPresent()) {
287+
LiveServerSetupComplete setupComplete = initialResponse.setupComplete().get();
286288
sessionFuture.complete(
287289
new AsyncSession(
288290
apiClient,
289291
this,
290-
initialResponse.setupComplete().get().sessionId().orElse(null)));
292+
setupComplete.sessionId().orElse(null),
293+
setupComplete));
291294
} else {
292295
sessionFuture.completeExceptionally(
293296
new GenAiIOException(

src/main/java/com/google/genai/AsyncSession.java

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@
2121
import com.google.genai.types.LiveClientContent;
2222
import com.google.genai.types.LiveClientMessage;
2323
import com.google.genai.types.LiveClientToolResponse;
24+
import com.google.genai.types.LiveServerSetupComplete;
2425
import com.google.genai.types.LiveSendClientContentParameters;
2526
import com.google.genai.types.LiveSendRealtimeInputParameters;
2627
import com.google.genai.types.LiveSendToolResponseParameters;
@@ -40,11 +41,17 @@ public final class AsyncSession {
4041

4142
private final AsyncLive.GenAiWebSocketClient websocket;
4243
final String sessionId;
44+
private final LiveServerSetupComplete setupComplete;
4345

44-
AsyncSession(ApiClient apiClient, AsyncLive.GenAiWebSocketClient websocket, String sessionId) {
46+
AsyncSession(
47+
ApiClient apiClient,
48+
AsyncLive.GenAiWebSocketClient websocket,
49+
String sessionId,
50+
LiveServerSetupComplete setupComplete) {
4551
this.apiClient = apiClient;
4652
this.websocket = websocket;
4753
this.sessionId = sessionId;
54+
this.setupComplete = setupComplete;
4855
}
4956

5057
/**
@@ -145,4 +152,8 @@ public CompletableFuture<Void> close() {
145152
public String sessionId() {
146153
return sessionId;
147154
}
155+
156+
public LiveServerSetupComplete setupComplete() {
157+
return setupComplete;
158+
}
148159
}

0 commit comments

Comments
 (0)