blob: b0f74fdb8af6bb56a746284bc8aee31c97a67332 [file]
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
//! Audio evidence shared by Code Mode and MCP outputs.
use super::{AudioOutput, ProjectionPart};
use crate::event::{Invocation, ProjectionArtifactWrite};
use base64::{Engine as _, engine::general_purpose::STANDARD};
use std::time::SystemTime;
pub fn valid_mime(mime: &str) -> bool {
mime.strip_prefix("audio/").is_some_and(|subtype| {
!subtype.is_empty()
&& subtype
.bytes()
.all(|b| b.is_ascii_lowercase() || b.is_ascii_digit() || b".+-".contains(&b))
})
}
pub(super) fn project(
data: &str,
mime: &str,
id: &str,
part: usize,
time: SystemTime,
invocation: &Invocation,
) -> Option<(ProjectionPart, Option<ProjectionArtifactWrite>)> {
let mime = match mime.trim().to_ascii_lowercase().as_str() {
"audio/x-wav" | "audio/wave" => "audio/wav".to_string(),
"audio/mp3" => "audio/mpeg".to_string(),
value => value.to_string(),
};
if !valid_mime(&mime) {
return None;
}
let bytes = STANDARD.decode(data).ok()?;
if bytes.is_empty() || STANDARD.encode(&bytes) != data {
return None;
}
let (reference, artifact) =
super::media::media_artifact(bytes, &mime, id, part, time, invocation).ok()?;
Some((
ProjectionPart::Audio {
audio: AudioOutput {
mime_type: mime,
reference,
},
},
Some(artifact),
))
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn valid_short_and_long_audio_keep_owned_evidence() {
let invocation = Invocation {
session_id: "s".into(),
turn_id: "t".into(),
run_id: "r".into(),
invocation_id: "i".into(),
};
for samples in [199usize, 200, 201] {
let mut wav = b"RIFF\0\0\0\0WAVEfmt \x10\0\0\0\x01\0\x01\0\x40\x1f\0\0\x80\x3e\0\0\x02\0\x10\0data\xff\xff\xff\xff".to_vec();
wav.resize(44 + samples * 2, 0);
let (part, artifact) = project(
&STANDARD.encode(&wav),
"audio/wav",
"e",
0,
std::time::UNIX_EPOCH,
&invocation,
)
.unwrap();
let artifact = artifact.unwrap();
assert_eq!(artifact.bytes(), wav);
assert_eq!(
artifact.artifact().source,
crate::artifact::ArtifactSource::ToolResultProjection
);
super::super::DurableToolProjection::Content { parts: vec![part] }
.validate("s")
.unwrap();
}
assert!(
project(
"invalid",
"audio/wav",
"e",
0,
std::time::UNIX_EPOCH,
&invocation
)
.is_none()
);
}
}