development
rs 55 lines 2 KB
Raw
1 //! `analyze_audio`: format, levels, loudness, and tempo/key estimates for a local audio file.
2
3 use anyhow::{Result, bail};
4 use serde::Deserialize;
5 use serde_json::{Value, json};
6
7 use super::{ToolCtx, ToolOutcome, absolutize, function_def};
8 use crate::audio::analysis;
9
10 pub const NAME: &str = "analyze_audio";
11 const MAX_FILE_BYTES: u64 = 1 << 30;
12
13 pub fn definition() -> Value {
14 function_def(
15 NAME,
16 "Analyze a local audio file (WAV, FLAC, MP3, OGG, M4A/AAC, AIFF): duration, sample rate, channels, peak/RMS, integrated loudness (LUFS), and estimated tempo (with half/double-time readings) and key. Tempo and key are estimates; say so.",
17 json!({
18 "type": "object",
19 "properties": {
20 "path": { "type": "string", "description": "Audio file path; relative paths resolve against the working directory" },
21 "root": { "type": "string", "description": "Workspace root to resolve a relative path against" }
22 },
23 "required": ["path"]
24 }),
25 )
26 }
27
28 #[derive(Deserialize)]
29 struct Args {
30 path: String,
31 root: Option<String>,
32 }
33
34 pub async fn execute(ctx: &ToolCtx, args: Value) -> Result<ToolOutcome> {
35 let a: Args = serde_json::from_value(args)?;
36 let path = absolutize(&ctx.resolve_with(a.root.as_deref(), &a.path)?);
37 let meta = tokio::fs::metadata(&path).await;
38 match meta {
39 Ok(m) if m.is_file() && m.len() <= MAX_FILE_BYTES => {}
40 Ok(m) if m.is_file() => bail!("{} is larger than 1 GiB", path.display()),
41 Ok(_) => bail!("{} is not a file", path.display()),
42 Err(e) => bail!("cannot read {}: {e}", path.display()),
43 }
44 let work = {
45 let path = path.clone();
46 tokio::task::spawn_blocking(move || analysis::analyze_and_report(&path))
47 };
48 let text = tokio::select! {
49 r = work => r??,
50 () = ctx.cancel.cancelled() => return Ok(ToolOutcome::err("Analysis cancelled.")),
51 };
52 let mut out = ToolOutcome::ok(text);
53 out.locations.push(path);
54 Ok(out)
55 }