use grok_api::{
Model, Result,
voice::{AudioFormat, ReasoningEffort, VoiceEvent, VoiceSession},
};
use serde_json::json;
fn get_weather(location: &str) -> serde_json::Value {
json!({
"location": location,
"temperature_c": 18,
"condition": "Partly cloudy",
"humidity_pct": 62
})
}
async fn drain_until_done(session: &mut VoiceSession, label: &str) -> Result<()> {
println!("\n [{label}] waiting for model response…");
loop {
match session.next_event().await? {
Some(VoiceEvent::AudioDelta { item_id, delta }) => {
print!(
" 🔊 audio chunk item={} bytes≈{}\r",
&item_id[..8.min(item_id.len())],
delta.len() * 3 / 4 );
}
Some(VoiceEvent::AudioDone { .. }) => {
println!(); }
Some(VoiceEvent::TranscriptDelta { delta, .. }) => {
print!("{delta}");
}
Some(VoiceEvent::TranscriptDone { transcript, .. }) => {
println!("\n 🤖 {transcript}");
}
Some(VoiceEvent::InputTranscriptUpdated { transcript, .. }) => {
println!(" 👂 (heard) '{transcript}'");
}
Some(VoiceEvent::FunctionCall {
name,
call_id,
arguments,
}) => {
println!(" 🔧 tool call name={name} call_id={call_id}");
println!(" args: {arguments}");
let result = match name.as_str() {
"get_weather" => {
let args: serde_json::Value =
serde_json::from_str(&arguments).unwrap_or_default();
let loc = args["location"].as_str().unwrap_or("Unknown");
get_weather(loc).to_string()
}
other => {
format!("{{\"error\": \"Unknown tool: {other}\"}}")
}
};
println!(" result: {result}");
session.send_function_result(&call_id, result).await?;
session.request_response().await?;
}
Some(VoiceEvent::SessionCreated { session_id }) => {
println!(" ✅ session created id={session_id}");
}
Some(VoiceEvent::SessionUpdated) => {
println!(" ✅ session updated");
}
Some(VoiceEvent::ConversationCreated { conversation_id }) => {
println!(" 💬 conversation id={conversation_id}");
}
Some(VoiceEvent::Done { usage }) => {
if let Some(u) = usage {
println!(
" 📊 tokens — input: {}, output: {}, total: {}",
u.input_tokens.unwrap_or(0),
u.output_tokens.unwrap_or(0),
u.total_tokens.unwrap_or(0),
);
}
break;
}
Some(VoiceEvent::Error { code, message }) => {
eprintln!(" ❌ voice API error [{code}]: {message}");
break;
}
Some(VoiceEvent::Other { event_type }) => {
println!(" ℹ️ event: {event_type}");
}
None => {
println!(" ⚠️ WebSocket closed unexpectedly");
break;
}
}
}
Ok(())
}
#[tokio::main]
async fn main() -> Result<()> {
tracing_subscriber::fmt::init();
let api_key = std::env::var("GROK_API_KEY").expect("GROK_API_KEY environment variable not set");
println!("🎤 Grok API — Voice (Speech-to-Speech) Demo\n");
println!("Voice model capabilities:");
for model in [
Model::GrokVoiceThinkFast2_0,
Model::GrokVoiceThinkFast1_0,
Model::GrokVoiceLatest,
] {
println!(
" {:<32} | voice={:<5} | language={}",
model.as_str(),
model.is_voice_model(),
model.is_language_model(),
);
}
println!();
let guard_result = VoiceSession::builder(&api_key)
.model("grok-4.5") .build()
.await;
match guard_result {
Err(e) => println!("✅ Guard works — non-voice model rejected: {e}\n"),
Ok(_) => println!("⚠️ Guard did not fire (unexpected)\n"),
}
println!("── Connecting to grok-voice-think-fast-2.0 ──\n");
let mut session = VoiceSession::builder(&api_key)
.model(Model::GrokVoiceThinkFast2_0.as_str())
.voice("eve")
.instructions(
"You are a concise and friendly voice assistant. \
Keep responses under three sentences.",
)
.server_vad()
.reasoning_effort(ReasoningEffort::High)
.input_format(AudioFormat::pcm(24000))
.output_format(AudioFormat::pcm(24000))
.build()
.await?;
println!(
" Connected! conversation_id={}",
session.conversation_id().unwrap_or("(pending first event)")
);
drain_until_done(&mut session, "setup").await?;
println!("\n── Turn 1: plain text turn ──");
session
.send_text("Hello! What is Rust's ownership model in one sentence?")
.await?;
session.request_response().await?;
drain_until_done(&mut session, "turn 1").await?;
println!("\n── Turn 2: follow-up ──");
session
.send_text("Give me one practical tip for avoiding borrow-checker errors.")
.await?;
session.request_response().await?;
drain_until_done(&mut session, "turn 2").await?;
println!("\n── Turn 3: function calling (weather tool) ──");
session
.send_text(
"What's the weather like in Austin, Texas? \
Use the get_weather function if you have it.",
)
.await?;
session.request_response().await?;
drain_until_done(&mut session, "turn 3 (function call)").await?;
println!("\n── force_message: scripted TTS without model involvement ──");
session
.force_message("This call may be recorded for quality assurance.", false)
.await?;
drain_until_done(&mut session, "force_message").await?;
println!("\n── Turn 4: per-turn instruction override ──");
session.send_text("Say something encouraging.").await?;
session
.request_response_with_instructions("Respond entirely in pirate dialect.")
.await?;
drain_until_done(&mut session, "turn 4 (pirate mode)").await?;
println!("\n✨ Voice demo complete!\n");
println!("Next steps:");
println!(" • Uncomment section 7 to stream real microphone audio.");
println!(" • Add .session_resumption() to the builder to survive Starlink drops.");
println!(" • See https://docs.x.ai/docs/models#speech-to-speech for full API docs.");
Ok(())
}