static PID_FILE: &str = "/tmp/ffmpeg_audio_recording.pid"; static TEMP_FILE: &str = "/tmp/ffmpeg_audio_recording.ogg"; static API_ENDPOINT: &str = "http://127.0.0.1:5000/v1/audio/transcriptions"; static AUTH_TOKEN: &str = "woshimima"; use clipboard::ClipboardContext; use clipboard::ClipboardProvider; use ctrlc; use nix::sys::signal; use nix::sys::signal::Signal; use nix::unistd::Pid; use reqwest::blocking::Client; use std::fs; use std::fs::File; use std::io::{Read, Write}; use std::process::exit; use std::sync::mpsc::channel; fn main() { // 如果已经在录制音频,停止录制 match read_pid() { Some(_) => { kill_and_delete_pid(); send_notification("Recording stopped", "Recording audio stopped."); exit(0) } None => { set_pid(); } } // 开始录制音频 send_notification("Recording started", "Recording audio..."); start_recording(); let text = upload_audio(); set_clipboard_content(&text); send_notification("Recording finished", text.as_str()); } fn upload_audio() -> String { let client = Client::new(); let form = reqwest::blocking::multipart::Form::new() .file("file", TEMP_FILE) .unwrap() .text("response_format", "text") .text("prompt", get_clipboard_content()) .text("model", "whisper-1"); let resp = client .post(API_ENDPOINT) .header("Authorization", format!("Bearer {}", AUTH_TOKEN)) .multipart(form) .send() .unwrap(); let text = resp.text().unwrap(); text } #[cfg(target_os = "linux")] fn start_recording() { let mut ffmpeg = std::process::Command::new("ffmpeg") .arg("-f") .arg("pulse") .arg("-i") .arg("default") .arg("-c:a") .arg("flac") .arg("-ac") .arg("1") .arg("-f") .arg("ogg") .arg("-y") .arg(TEMP_FILE) .spawn() .unwrap(); // 注册监听 Ctrl-C 信号 let (tx, rx) = channel(); ctrlc::set_handler(move || tx.send(()).expect("Could not send signal on channel.")) .expect("Error setting Ctrl-C handler"); // 等待 Ctrl-C 信号 println!("Waiting for Ctrl-C..."); rx.recv().expect("Could not receive from channel."); println!("Got it! killing ffmpeg)"); signal::kill(Pid::from_raw(ffmpeg.id() as i32), Signal::SIGINT).unwrap(); // 等待 ffmpeg 退出 ffmpeg.wait().unwrap(); println!("ffmpeg exited"); } fn read_pid() -> Option { let mut file = match File::open(PID_FILE) { Ok(ctx) => ctx, Err(_) => { return None; } }; let mut pid = String::new(); match file.read_to_string(&mut pid) { Ok(_) => Some(pid), Err(_) => None, } } fn kill_and_delete_pid() { let pid = read_pid().unwrap(); let pid = pid.trim(); let pid = pid.parse::().unwrap(); match signal::kill(Pid::from_raw(pid), Signal::SIGINT) { Ok(_) => {} Err(_) => { send_notification("Error to kill", "Error to kill pid, cleaning"); } } fs::remove_file(PID_FILE).unwrap(); } fn set_pid() { let mut file = File::create(PID_FILE).unwrap(); file.write_all(format!("{}", std::process::id()).as_bytes()) .unwrap(); } fn send_notification(title: &str, content: &str) { let result = notify_rust::Notification::new() .summary(title) .body(content) .timeout(std::time::Duration::from_secs(2)) .show(); match result { Ok(_) => {} Err(error) => { eprintln!("Error sending notification: {}", error); } } } fn get_clipboard_content() -> String { match ClipboardContext::new().and_then(|mut ctx| ctx.get_contents()) { Ok(content) => content, Err(error) => { send_notification( "Error getting clipboard content", &format!("Error: {}", error), ); String::new() } } } fn set_clipboard_content(content: &str) { match ClipboardContext::new().and_then(|mut ctx| ctx.set_contents(content.to_owned())) { Ok(_) => {} Err(error) => { send_notification( "Error setting clipboard content", &format!("Error: {}", error), ); } } }