Split realtime transcript notifications

This commit is contained in:
Ahmed Ibrahim
2026-04-13 16:04:12 -07:00
parent 4d57fd1caa
commit 001cdd274e
30 changed files with 383 additions and 260 deletions
-1
View File
@@ -16,7 +16,6 @@ pub use realtime_websocket::RealtimeEventParser;
pub use realtime_websocket::RealtimeOutputModality;
pub use realtime_websocket::RealtimeSessionConfig;
pub use realtime_websocket::RealtimeSessionMode;
pub use realtime_websocket::RealtimeTranscriptUpdateKind;
pub use realtime_websocket::RealtimeWebsocketClient;
pub use realtime_websocket::RealtimeWebsocketConnection;
pub use realtime_websocket::RealtimeWebsocketEvents;
@@ -10,9 +10,7 @@ use crate::endpoint::realtime_websocket::protocol::RealtimeOutboundMessage;
use crate::endpoint::realtime_websocket::protocol::RealtimeOutputModality;
use crate::endpoint::realtime_websocket::protocol::RealtimeSessionConfig;
use crate::endpoint::realtime_websocket::protocol::RealtimeSessionMode;
use crate::endpoint::realtime_websocket::protocol::RealtimeTranscriptDelta;
use crate::endpoint::realtime_websocket::protocol::RealtimeTranscriptEntry;
use crate::endpoint::realtime_websocket::protocol::RealtimeTranscriptUpdateKind;
use crate::endpoint::realtime_websocket::protocol::RealtimeVoice;
use crate::endpoint::realtime_websocket::protocol::parse_realtime_event;
use crate::error::ApiError;
@@ -416,10 +414,20 @@ impl RealtimeWebsocketEvents {
match event {
RealtimeEvent::InputAudioSpeechStarted(_) => {}
RealtimeEvent::InputTranscriptDelta(update) => {
update_active_transcript_entry(&mut active_transcript.entries, "user", update);
append_transcript_delta(&mut active_transcript.entries, "user", &update.delta);
}
RealtimeEvent::InputTranscriptDone(update) => {
complete_transcript_entry(&mut active_transcript.entries, "user", &update.text);
}
RealtimeEvent::OutputTranscriptDelta(update) => {
update_active_transcript_entry(&mut active_transcript.entries, "assistant", update);
append_transcript_delta(&mut active_transcript.entries, "assistant", &update.delta);
}
RealtimeEvent::OutputTranscriptDone(update) => {
complete_transcript_entry(
&mut active_transcript.entries,
"assistant",
&update.text,
);
}
RealtimeEvent::HandoffRequested(handoff) => {
if self.event_parser == RealtimeEventParser::V1 {
@@ -456,21 +464,6 @@ fn append_transcript_delta(entries: &mut Vec<RealtimeTranscriptEntry>, role: &st
});
}
fn update_active_transcript_entry(
entries: &mut Vec<RealtimeTranscriptEntry>,
role: &str,
update: &RealtimeTranscriptDelta,
) {
match update.update_kind {
RealtimeTranscriptUpdateKind::Delta => {
append_transcript_delta(entries, role, &update.delta)
}
RealtimeTranscriptUpdateKind::Done => {
complete_transcript_entry(entries, role, &update.delta)
}
}
}
fn complete_transcript_entry(entries: &mut Vec<RealtimeTranscriptEntry>, role: &str, text: &str) {
if text.is_empty() {
return;
@@ -770,13 +763,14 @@ fn normalize_realtime_path(url: &mut Url) {
#[cfg(test)]
mod tests {
use super::*;
use crate::endpoint::realtime_websocket::protocol::RealtimeTranscriptDelta;
use crate::endpoint::realtime_websocket::protocol::RealtimeTranscriptEntry;
use codex_protocol::protocol::RealtimeHandoffRequested;
use codex_protocol::protocol::RealtimeInputAudioSpeechStarted;
use codex_protocol::protocol::RealtimeResponseCancelled;
use codex_protocol::protocol::RealtimeResponseCreated;
use codex_protocol::protocol::RealtimeResponseDone;
use codex_protocol::protocol::RealtimeTranscriptDelta;
use codex_protocol::protocol::RealtimeTranscriptDone;
use codex_protocol::protocol::RealtimeVoice;
use http::HeaderValue;
use pretty_assertions::assert_eq;
@@ -891,7 +885,6 @@ mod tests {
Some(RealtimeEvent::InputTranscriptDelta(
RealtimeTranscriptDelta {
delta: "hello ".to_string(),
update_kind: RealtimeTranscriptUpdateKind::Delta,
}
))
);
@@ -910,7 +903,6 @@ mod tests {
Some(RealtimeEvent::OutputTranscriptDelta(
RealtimeTranscriptDelta {
delta: "hi".to_string(),
update_kind: RealtimeTranscriptUpdateKind::Delta,
}
))
);
@@ -954,7 +946,6 @@ mod tests {
Some(RealtimeEvent::InputTranscriptDelta(
RealtimeTranscriptDelta {
delta: "hello".to_string(),
update_kind: RealtimeTranscriptUpdateKind::Delta,
}
))
);
@@ -970,10 +961,9 @@ mod tests {
assert_eq!(
parse_realtime_event(payload.as_str(), RealtimeEventParser::RealtimeV2),
Some(RealtimeEvent::OutputTranscriptDelta(
RealtimeTranscriptDelta {
delta: "all done".to_string(),
update_kind: RealtimeTranscriptUpdateKind::Done,
Some(RealtimeEvent::OutputTranscriptDone(
RealtimeTranscriptDone {
text: "all done".to_string(),
}
))
);
@@ -1514,7 +1504,6 @@ mod tests {
input_delta_event,
RealtimeEvent::InputTranscriptDelta(RealtimeTranscriptDelta {
delta: "delegate ".to_string(),
update_kind: RealtimeTranscriptUpdateKind::Delta,
})
);
@@ -1527,7 +1516,6 @@ mod tests {
input_delta_event,
RealtimeEvent::InputTranscriptDelta(RealtimeTranscriptDelta {
delta: "now".to_string(),
update_kind: RealtimeTranscriptUpdateKind::Delta,
})
);
@@ -1540,7 +1528,6 @@ mod tests {
output_delta_event,
RealtimeEvent::OutputTranscriptDelta(RealtimeTranscriptDelta {
delta: "working".to_string(),
update_kind: RealtimeTranscriptUpdateKind::Delta,
})
);
@@ -16,4 +16,3 @@ pub use protocol::RealtimeEventParser;
pub use protocol::RealtimeOutputModality;
pub use protocol::RealtimeSessionConfig;
pub use protocol::RealtimeSessionMode;
pub use protocol::RealtimeTranscriptUpdateKind;
@@ -3,9 +3,7 @@ use crate::endpoint::realtime_websocket::protocol_v2::parse_realtime_event_v2;
pub use codex_protocol::protocol::RealtimeAudioFrame;
pub use codex_protocol::protocol::RealtimeEvent;
pub use codex_protocol::protocol::RealtimeOutputModality;
pub use codex_protocol::protocol::RealtimeTranscriptDelta;
pub use codex_protocol::protocol::RealtimeTranscriptEntry;
pub use codex_protocol::protocol::RealtimeTranscriptUpdateKind;
pub use codex_protocol::protocol::RealtimeVoice;
use serde::Serialize;
use serde_json::Value;
@@ -1,6 +1,6 @@
use codex_protocol::protocol::RealtimeEvent;
use codex_protocol::protocol::RealtimeTranscriptDelta;
use codex_protocol::protocol::RealtimeTranscriptUpdateKind;
use codex_protocol::protocol::RealtimeTranscriptDone;
use serde_json::Value;
use tracing::debug;
@@ -46,20 +46,23 @@ pub(super) fn parse_session_updated_event(parsed: &Value) -> Option<RealtimeEven
pub(super) fn parse_transcript_delta_event(
parsed: &Value,
field: &str,
) -> Option<RealtimeTranscriptDelta> {
parse_transcript_update_event(parsed, field, RealtimeTranscriptUpdateKind::Delta)
}
pub(super) fn parse_transcript_update_event(
parsed: &Value,
field: &str,
update_kind: RealtimeTranscriptUpdateKind,
) -> Option<RealtimeTranscriptDelta> {
parsed
.get(field)
.and_then(Value::as_str)
.map(str::to_string)
.map(|delta| RealtimeTranscriptDelta { delta, update_kind })
.map(|delta| RealtimeTranscriptDelta { delta })
}
pub(super) fn parse_transcript_done_event(
parsed: &Value,
field: &str,
) -> Option<RealtimeTranscriptDone> {
parsed
.get(field)
.and_then(Value::as_str)
.map(str::to_string)
.map(|text| RealtimeTranscriptDone { text })
}
pub(super) fn parse_error_event(parsed: &Value) -> Option<RealtimeEvent> {
@@ -2,7 +2,7 @@ use crate::endpoint::realtime_websocket::protocol_common::parse_error_event;
use crate::endpoint::realtime_websocket::protocol_common::parse_realtime_payload;
use crate::endpoint::realtime_websocket::protocol_common::parse_session_updated_event;
use crate::endpoint::realtime_websocket::protocol_common::parse_transcript_delta_event;
use crate::endpoint::realtime_websocket::protocol_common::parse_transcript_update_event;
use crate::endpoint::realtime_websocket::protocol_common::parse_transcript_done_event;
use codex_protocol::protocol::RealtimeAudioFrame;
use codex_protocol::protocol::RealtimeEvent;
use codex_protocol::protocol::RealtimeHandoffRequested;
@@ -10,7 +10,6 @@ use codex_protocol::protocol::RealtimeInputAudioSpeechStarted;
use codex_protocol::protocol::RealtimeResponseCancelled;
use codex_protocol::protocol::RealtimeResponseCreated;
use codex_protocol::protocol::RealtimeResponseDone;
use codex_protocol::protocol::RealtimeTranscriptUpdateKind;
use serde_json::Map as JsonMap;
use serde_json::Value;
use tracing::debug;
@@ -32,19 +31,18 @@ pub(super) fn parse_realtime_event_v2(payload: &str) -> Option<RealtimeEvent> {
parse_transcript_delta_event(&parsed, "delta").map(RealtimeEvent::InputTranscriptDelta)
}
"conversation.item.input_audio_transcription.completed" => {
parse_transcript_update_event(&parsed, "transcript", RealtimeTranscriptUpdateKind::Done)
.map(RealtimeEvent::InputTranscriptDelta)
parse_transcript_done_event(&parsed, "transcript")
.map(RealtimeEvent::InputTranscriptDone)
}
"response.output_text.delta" | "response.output_audio_transcript.delta" => {
parse_transcript_delta_event(&parsed, "delta").map(RealtimeEvent::OutputTranscriptDelta)
}
"response.output_text.done" => {
parse_transcript_update_event(&parsed, "text", RealtimeTranscriptUpdateKind::Done)
.map(RealtimeEvent::OutputTranscriptDelta)
parse_transcript_done_event(&parsed, "text").map(RealtimeEvent::OutputTranscriptDone)
}
"response.output_audio_transcript.done" => {
parse_transcript_update_event(&parsed, "transcript", RealtimeTranscriptUpdateKind::Done)
.map(RealtimeEvent::OutputTranscriptDelta)
parse_transcript_done_event(&parsed, "transcript")
.map(RealtimeEvent::OutputTranscriptDone)
}
"input_audio_buffer.speech_started" => Some(RealtimeEvent::InputAudioSpeechStarted(
RealtimeInputAudioSpeechStarted {
-1
View File
@@ -44,7 +44,6 @@ pub use crate::endpoint::RealtimeEventParser;
pub use crate::endpoint::RealtimeOutputModality;
pub use crate::endpoint::RealtimeSessionConfig;
pub use crate::endpoint::RealtimeSessionMode;
pub use crate::endpoint::RealtimeTranscriptUpdateKind;
pub use crate::endpoint::RealtimeWebsocketClient;
pub use crate::endpoint::RealtimeWebsocketConnection;
pub use crate::endpoint::RealtimeWebsocketEvents;