mirror of
https://github.com/openai/codex.git
synced 2026-09-16 12:13:30 +00:00
## Why Audio models cannot reliably encode tool-generated clips shorter than 25 ms. ## What changed - Measure the available audio frames in base64-encoded PCM and IEEE float WAV outputs. - Replace clips under 25 ms with explanatory text while preserving surrounding Code Mode output. - Leave clips at or above the threshold and unrecognized audio formats unchanged. ## Testing - Cover the duration boundary across multiple sample rates and all supported `audio()` input forms. - Verify bounded and truncated WAV chunks, output after `yield_control()`, and end-to-end mixed text and audio output. GitOrigin-RevId: 342b4bdbc65db4238580f9cdc372763c2576eca5
164 lines
5.8 KiB
Rust
164 lines
5.8 KiB
Rust
//! Covers per-item short-WAV omissions without disrupting other Code Mode outputs.
|
|
|
|
use super::cell_id;
|
|
use super::execute;
|
|
use super::execute_request;
|
|
use crate::ExecuteRequest;
|
|
use crate::FunctionCallOutputContentItem;
|
|
use crate::InProcessCodeModeSession;
|
|
use crate::RuntimeResponse;
|
|
use crate::WaitOutcome;
|
|
use crate::WaitRequest;
|
|
use base64::Engine;
|
|
use base64::engine::general_purpose::STANDARD as BASE64_STANDARD;
|
|
use pretty_assertions::assert_eq;
|
|
|
|
const SHORT_AUDIO_OMISSION_TEXT: &str =
|
|
"Audio output omitted because the clip is shorter than 25 ms; use a longer clip.";
|
|
|
|
fn pcm16_wav(sample_rate: u32, frames: usize) -> Vec<u8> {
|
|
let data_size = u32::try_from(frames * 2).unwrap();
|
|
let mut wav = b"RIFF".to_vec();
|
|
wav.extend_from_slice(&(36 + data_size).to_le_bytes());
|
|
wav.extend_from_slice(b"WAVEfmt ");
|
|
wav.extend_from_slice(&16_u32.to_le_bytes());
|
|
wav.extend_from_slice(&1_u16.to_le_bytes());
|
|
wav.extend_from_slice(&1_u16.to_le_bytes());
|
|
wav.extend_from_slice(&sample_rate.to_le_bytes());
|
|
wav.extend_from_slice(&(sample_rate * 2).to_le_bytes());
|
|
wav.extend_from_slice(&2_u16.to_le_bytes());
|
|
wav.extend_from_slice(&16_u16.to_le_bytes());
|
|
wav.extend_from_slice(b"data");
|
|
wav.extend_from_slice(&data_size.to_le_bytes());
|
|
wav.resize(44 + frames * 2, /*value*/ 0);
|
|
wav
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn audio_helper_omits_short_wav_in_all_input_forms() {
|
|
for (sample_rate, frames, omitted) in [
|
|
(24_000, 0, true),
|
|
(24_000, 1, true),
|
|
(24_000, 599, true),
|
|
(24_000, 600, false),
|
|
(24_000, 601, false),
|
|
(44_100, 223, true),
|
|
(44_100, 1_102, true),
|
|
(44_100, 1_103, false),
|
|
(30_870, 223, true),
|
|
(30_870, 771, true),
|
|
(30_870, 772, false),
|
|
(34_398, 221, true),
|
|
(34_398, 859, true),
|
|
(34_398, 860, false),
|
|
] {
|
|
let payload = BASE64_STANDARD.encode(pcm16_wav(sample_rate, frames));
|
|
let audio_url = format!("data:audio/wav;base64,{payload}");
|
|
let service = InProcessCodeModeSession::new();
|
|
let response = execute(
|
|
&service,
|
|
ExecuteRequest {
|
|
source: format!(
|
|
r#"audio({audio_url:?}); audio({{audio_url:{audio_url:?}}});
|
|
audio({{type:"audio",mimeType:"audio/wav",data:{payload:?}}});"#
|
|
),
|
|
yield_time_ms: None,
|
|
..execute_request("")
|
|
},
|
|
)
|
|
.await;
|
|
let expected = if omitted {
|
|
FunctionCallOutputContentItem::InputText {
|
|
text: SHORT_AUDIO_OMISSION_TEXT.to_string(),
|
|
}
|
|
} else {
|
|
FunctionCallOutputContentItem::InputAudio { audio_url }
|
|
};
|
|
assert_eq!(
|
|
response,
|
|
RuntimeResponse::Result {
|
|
code_mode_host_duration: None,
|
|
cell_id: cell_id("1"),
|
|
content_items: vec![expected; 3],
|
|
error_text: None,
|
|
},
|
|
"sample rate {sample_rate}, frames {frames}"
|
|
);
|
|
}
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn audio_helper_bounds_wav_data_and_preserves_outputs_after_yield() {
|
|
let mut metadata_short = pcm16_wav(/*sample_rate*/ 24_000, /*frames*/ 1);
|
|
let mut valid = pcm16_wav(/*sample_rate*/ 24_000, /*frames*/ 600);
|
|
let mut list = b"LIST".to_vec();
|
|
list.extend_from_slice(&(4_u32 + 8 + 2048).to_le_bytes());
|
|
list.extend_from_slice(b"INFOINAM");
|
|
list.extend_from_slice(&2048_u32.to_le_bytes());
|
|
list.resize(8 + 4 + 8 + 2048, /*value*/ 0);
|
|
list.extend_from_slice(b"JUNK\x01\0\0\0x\0");
|
|
for wav in [&mut metadata_short, &mut valid] {
|
|
drop(wav.splice(36..36, list.iter().copied()));
|
|
let riff_size = u32::try_from(wav.len() - 8).unwrap();
|
|
wav[4..8].copy_from_slice(&riff_size.to_le_bytes());
|
|
}
|
|
let mut truncated = pcm16_wav(/*sample_rate*/ 24_000, /*frames*/ 0);
|
|
truncated[4..8].copy_from_slice(&48_036_u32.to_le_bytes());
|
|
truncated[40..44].copy_from_slice(&48_000_u32.to_le_bytes());
|
|
let mut bounded = pcm16_wav(/*sample_rate*/ 24_000, /*frames*/ 600);
|
|
bounded[40..44].copy_from_slice(&2_u32.to_le_bytes());
|
|
let urls = [metadata_short, truncated, bounded, valid]
|
|
.map(|wav| format!("data:audio/wav;base64,{}", BASE64_STANDARD.encode(wav)));
|
|
let audio_urls = serde_json::to_string(&urls).unwrap();
|
|
let service = InProcessCodeModeSession::new();
|
|
let response = execute(
|
|
&service,
|
|
ExecuteRequest {
|
|
source: format!(
|
|
"text('before'); await yield_control(); \
|
|
for (const audio_url of {audio_urls}) audio({{audio_url}}); text('after');"
|
|
),
|
|
yield_time_ms: None,
|
|
..execute_request("")
|
|
},
|
|
)
|
|
.await;
|
|
assert_eq!(
|
|
response,
|
|
RuntimeResponse::Yielded {
|
|
code_mode_host_duration: None,
|
|
cell_id: cell_id("1"),
|
|
content_items: vec![FunctionCallOutputContentItem::InputText {
|
|
text: "before".to_string(),
|
|
}],
|
|
}
|
|
);
|
|
let mut expected = vec![
|
|
FunctionCallOutputContentItem::InputText {
|
|
text: SHORT_AUDIO_OMISSION_TEXT.to_string(),
|
|
};
|
|
3
|
|
];
|
|
expected.push(FunctionCallOutputContentItem::InputAudio {
|
|
audio_url: urls[3].clone(),
|
|
});
|
|
expected.push(FunctionCallOutputContentItem::InputText {
|
|
text: "after".to_string(),
|
|
});
|
|
assert_eq!(
|
|
service
|
|
.wait(WaitRequest {
|
|
cell_id: cell_id("1"),
|
|
yield_time_ms: 60_000,
|
|
})
|
|
.await
|
|
.unwrap(),
|
|
WaitOutcome::LiveCell(RuntimeResponse::Result {
|
|
code_mode_host_duration: None,
|
|
cell_id: cell_id("1"),
|
|
content_items: expected,
|
|
error_text: None,
|
|
})
|
|
);
|
|
}
|