Skip to main content

voice_echo/api/
inject.rs

1use axum::extract::State;
2use axum::http::{HeaderMap, StatusCode};
3use axum::response::IntoResponse;
4use axum::Json;
5use serde::{Deserialize, Serialize};
6
7use crate::registry::CallRegistry;
8use crate::AppState;
9
10use super::outbound::check_auth;
11
12#[derive(Debug, Deserialize)]
13pub struct InjectRequest {
14    /// The call_sid to inject audio into.
15    pub call_sid: String,
16    /// Text to synthesize and speak into the active call.
17    pub text: String,
18}
19
20#[derive(Debug, Serialize)]
21struct InjectResponse {
22    status: String,
23}
24
25#[derive(Debug, Serialize)]
26struct ErrorResponse {
27    error: String,
28}
29
30/// POST /api/inject — Inject TTS audio into an active call.
31///
32/// Used by bridge-echo to route cross-channel responses to voice.
33/// When D sends a Discord message during a call, bridge-echo sends
34/// the Claude response here instead of back to Discord.
35///
36/// Requires `Authorization: Bearer <token>` header.
37pub async fn handle_inject(
38    State(state): State<AppState>,
39    headers: HeaderMap,
40    Json(req): Json<InjectRequest>,
41) -> impl IntoResponse {
42    if let Err(resp) = check_auth(&headers, &state.config.api.token) {
43        return resp;
44    }
45
46    tracing::info!(call_sid = %req.call_sid, text_len = req.text.len(), "Inject requested");
47
48    // Look up the active call
49    let entry = state.call_registry.get(&req.call_sid).await;
50    let Some(entry) = entry else {
51        tracing::warn!(call_sid = %req.call_sid, "No active call found for inject");
52        return (
53            StatusCode::NOT_FOUND,
54            Json(ErrorResponse {
55                error: format!("No active call with sid {}", req.call_sid),
56            }),
57        )
58            .into_response();
59    };
60
61    // Run TTS
62    let tts_mulaw = match state.tts.synthesize(&req.text).await {
63        Ok(data) => data,
64        Err(e) => {
65            tracing::error!(call_sid = %req.call_sid, "TTS failed for inject: {e}");
66            return (
67                StatusCode::INTERNAL_SERVER_ERROR,
68                Json(ErrorResponse {
69                    error: format!("TTS synthesis failed: {e}"),
70                }),
71            )
72                .into_response();
73        }
74    };
75
76    // Suppress VAD while injected audio plays
77    entry.set_speaking(true);
78
79    // Send audio frames through the call's response channel
80    if let Err(e) = CallRegistry::send_audio(&entry, &tts_mulaw).await {
81        tracing::error!(call_sid = %req.call_sid, "Failed to inject audio: {e}");
82        entry.set_speaking(false);
83        return (
84            StatusCode::INTERNAL_SERVER_ERROR,
85            Json(ErrorResponse {
86                error: format!("Failed to send audio: {e}"),
87            }),
88        )
89            .into_response();
90    }
91
92    tracing::info!(
93        call_sid = %req.call_sid,
94        tts_bytes = tts_mulaw.len(),
95        "Audio injected successfully"
96    );
97
98    (
99        StatusCode::OK,
100        Json(InjectResponse {
101            status: "injected".to_string(),
102        }),
103    )
104        .into_response()
105}