diff --git a/src/main.rs b/src/main.rs index f145af6..b2e8996 100644 --- a/src/main.rs +++ b/src/main.rs @@ -7,7 +7,7 @@ use reqwest::Client; use serde::Deserialize; use std::fs::{self, File}; use std::io::Write; -use std::path::Path; +use std::path::{Path, PathBuf}; use std::process::Command; use tiktoken_rs::cl100k_base; @@ -37,15 +37,6 @@ struct OpenAIError { } #[tokio::main] -/// The main function in this Rust code reads text from a file, chunks the text, generates audio files -/// based on the chunks using a specified model and voice, combines the audio files, and then cleans up -/// temporary files, ultimately creating a final audio file. -/// -/// Returns: -/// -/// The `main` function is returning a `Result` with a unit type `()` indicating that it can return -/// either `Ok(())` if the program execution is successful or an `Err` containing an error if any of the -/// operations encounter an issue. async fn main() -> Result<()> { let args = Args::parse(); @@ -59,91 +50,44 @@ async fn main() -> Result<()> { .to_str() .context("Invalid input file name")?; - println!( - "Now creating a folder called {} for you.", - green_text(input_file_name) - ); + println!("Now creating a folder called {} for you.", green_text(input_file_name)); let output_dir = Path::new("./").join(input_file_name); fs::create_dir_all(&output_dir)?; let lines = read_text_file(input_file_path)?; let chunks = chunk_text(&lines); - generate_audio_files( - &chunks, - &output_dir, - &args.model, - &args.voice, - &client, - &api_key, - ) - .await?; + generate_audio_files(&chunks, &output_dir, &args.model, &args.voice, &client, &api_key).await?; println!( "Chunk flac files are already in [ ./{} ] for ffmpeg to combine.\n\n", green_text(input_file_name) ); - combine_audio_files(input_file_path, &output_dir)?; + combine_audio_files(&output_dir)?; + remove_tmp(&output_dir)?; - println!( - "\nThe File [ {} ] is ready for you. \n", - green_text(input_file_name) - ); + println!("\nThe File [ {} ] is ready for you. \n", green_text(input_file_name)); Ok(()) } -/// The function `green_text` takes a string input and returns the same text with green color -/// formatting. -/// -/// Arguments: -/// -/// * `text`: The `green_text` function takes a reference to a string (`&str`) as input and returns a -/// new string with the input text formatted in green color. The function achieves this by using ANSI -/// escape codes for colors. -/// -/// Returns: -/// -/// A string with the input text formatted in green color using ANSI escape codes. fn green_text(text: &str) -> String { format!("\x1b[92m{}\x1b[0m", text) } -/// The function `read_text_file` reads the content of a text file, filters out empty lines, and returns -/// the non-empty lines as a vector of strings. -/// -/// Arguments: -/// -/// * `file_path`: The `file_path` parameter is a reference to a `Path` object, which represents the -/// location of the text file that you want to read. -/// -/// Returns: -/// -/// The function `read_text_file` is returning a `Result` containing a `Vec`. fn read_text_file(file_path: &Path) -> Result> { let content = fs::read_to_string(file_path)?; - Ok(content - .lines() - .filter(|line| !line.trim().is_empty()) - .map(String::from) - .collect()) + Ok( + content + .lines() + .filter(|line| !line.trim().is_empty()) + .map(String::from) + .collect() + ) } -/// The `chunk_text` function in Rust splits a list of text lines into chunks based on a token count -/// limit of 500 using a Byte Pair Encoding (BPE) model. -/// -/// Arguments: -/// -/// * `lines`: The function `chunk_text` takes a slice of `String` lines as input and splits them into -/// chunks based on a token count limit of 500. Each chunk is a vector of strings. -/// -/// Returns: -/// -/// The `chunk_text` function returns a `Vec>`, which is a vector of vectors of strings. -/// Each inner vector represents a chunk of text lines that have been grouped together based on a token -/// count limit of 500 tokens per chunk. fn chunk_text(lines: &[String]) -> Vec> { let bpe = cl100k_base().unwrap(); let mut chunks = Vec::new(); @@ -169,44 +113,13 @@ fn chunk_text(lines: &[String]) -> Vec> { chunks } -/// The function `generate_audio_files` asynchronously generates audio files from text chunks using the -/// OpenAI API and saves them to a specified output directory. -/// -/// Arguments: -/// -/// * `chunks`: The `chunks` parameter is a slice of vectors of strings. Each vector represents a chunk -/// of text that needs to be converted into audio files. The function processes each chunk sequentially, -/// generating an audio file for each chunk. -/// * `output_dir`: The `output_dir` parameter in the `generate_audio_files` function represents the -/// directory where the audio files will be saved. It is of type `&Path`, which is a reference to a -/// `Path` object that specifies the location where the audio files will be stored. You can provide the -/// function -/// * `model`: The `model` parameter in the `generate_audio_files` function represents the model used -/// for generating audio in the OpenAI API. This model determines the style and characteristics of the -/// generated speech. It could be a specific language model, a voice style, or any other model supported -/// by the OpenAI API -/// * `voice`: The `voice` parameter in the `generate_audio_files` function represents the voice style -/// or type that will be used for generating the audio files. It is a string that specifies the voice -/// model to be used by the OpenAI API for converting the input text into speech. This parameter allows -/// you to choose -/// * `client`: The `client` parameter in the `generate_audio_files` function is of type `Client`, which -/// is likely an HTTP client used to make requests to the OpenAI API for generating audio files. It is -/// used to send a POST request to the OpenAI API endpoint `https://api.openai.com -/// * `api_key`: The `api_key` parameter in the `generate_audio_files` function is the API key required -/// for authentication when making requests to the OpenAI API. This key is used to authorize and -/// identify the user making the API calls. Make sure to keep your API key secure and not expose it -/// publicly. -/// -/// Returns: -/// -/// The `generate_audio_files` function returns a `Result` with an empty tuple `()` as the success type. async fn generate_audio_files( chunks: &[Vec], output_dir: &Path, model: &str, voice: &str, client: &Client, - api_key: &str, + api_key: &str ) -> Result<()> { let date_time_string = Local::now().format("%Y%m%d%H%M").to_string(); @@ -219,10 +132,7 @@ async fn generate_audio_files( format!("{:06}", i + 1), chunks.len() ); - println!( - "Input String: {}...", - &chunk_string[..chunk_string.len().min(60)] - ); + println!("Input String: {}...", &chunk_string[..chunk_string.len().min(60)]); if chunk_string.len() > 4000 { anyhow::bail!( @@ -236,20 +146,21 @@ async fn generate_audio_files( pb.set_style( ProgressStyle::default_spinner() .tick_chars("⠁⠂⠄⡀⢀⠠⠐⠈ ") - .template("{spinner:.green} {msg}")?, + .template("{spinner:.green} {msg}")? ); pb.set_message("Generating audio..."); let response = client .post("https://api.openai.com/v1/audio/speech") .header("Authorization", format!("Bearer {}", api_key)) - .json(&serde_json::json!({ + .json( + &serde_json::json!({ "model": model, "voice": voice, "input": chunk_string, - })) - .send() - .await?; + }) + ) + .send().await?; if !response.status().is_success() { let error: OpenAIResponse = response.json().await?; @@ -275,62 +186,37 @@ async fn generate_audio_files( Ok(()) } -/// The function `combine_audio_files` in Rust combines multiple FLAC audio files into a single FLAC -/// file using ffmpeg. -/// -/// Arguments: -/// -/// * `input_file_path`: The `input_file_path` parameter is the path to the input audio file that you -/// want to combine with the existing FLAC files in the `output_dir`. -/// * `output_dir`: The `output_dir` parameter in the `combine_audio_files` function represents the -/// directory where the output files will be stored. This function reads all FLAC files from this -/// directory, creates a concatenation file (`concat.txt`) listing these files, and then uses `ffmpeg` -/// to combine them into a single -/// -/// Returns: -/// -/// The `combine_audio_files` function returns a `Result` with the unit type `()` as the success type. -fn combine_audio_files(input_file_path: &Path, output_dir: &Path) -> Result<()> { - let input_file_name = input_file_path - .file_stem() - .context("Invalid input file")? - .to_str() - .context("Invalid input file name")?; - let flac_files: Vec<_> = fs::read_dir(output_dir)? - .filter_map(|entry| { - let entry = entry.ok()?; - let path = entry.path(); - if path.extension()? == "flac" { - Some(path) - } else { - None - } - }) - .collect(); - - let concat_file_path = output_dir.join("concat.txt"); - let mut concat_file = File::create(&concat_file_path)?; - for flac_file in &flac_files { - writeln!( - concat_file, - "file '{}'", - flac_file.strip_prefix(output_dir)?.to_str().unwrap() - )?; +fn combine_audio_files(output_dir: &Path) -> Result<()> { + let mut input_files = Vec::new(); + for entry in fs::read_dir(output_dir)? { + let entry = entry?; + let path = entry.path(); + if path.extension().map(|ext| ext == "flac").unwrap_or(false) && path.file_name().unwrap().to_str().unwrap().starts_with("tmp") { + input_files.push(path); + } + } + + input_files.sort(); + + let mut ffmpeg_args = Vec::new(); + for input_file in &input_files { + ffmpeg_args.push("-i".to_string()); + ffmpeg_args.push(input_file.to_str().unwrap().to_string()); } + ffmpeg_args.push("-filter_complex".to_string()); + ffmpeg_args.push(format!( + "concat=n={}:v=0:a=1[outa]", + input_files.len() + )); + ffmpeg_args.push("-map".to_string()); + ffmpeg_args.push("[outa]".to_string()); + ffmpeg_args.push("-c:a".to_string()); + ffmpeg_args.push("flac".to_string()); + ffmpeg_args.push("-y".to_string()); // Overwrite output files without asking + ffmpeg_args.push(output_dir.join("combined.flac").to_str().unwrap().to_string()); - let output_file_path = output_dir.join(format!("{}.flac", input_file_name)); let status = Command::new("ffmpeg") - .args(&[ - "-f", - "concat", - "-safe", - "0", - "-i", - concat_file_path.to_str().unwrap(), - "-c:a", - "flac", - output_file_path.to_str().unwrap(), - ]) + .args(&ffmpeg_args) .status()?; if !status.success() { @@ -340,26 +226,13 @@ fn combine_audio_files(input_file_path: &Path, output_dir: &Path) -> Result<()> Ok(()) } -/// The above Rust code defines a function `remove_tmp` that takes a reference to a `Path` as input and -/// returns a `Result`. The function iterates over the entries in the directory specified by -/// `output_dir`. For each entry, it checks if the file name starts with "tmp". If it does, the file is -/// removed using `fs::remove_file`. After iterating through all entries, the code then attempts to -/// remove the file named "concat.txt" in the `output_dir`. Finally, the function returns `Ok(())` to -/// indicate successful completion. fn remove_tmp(output_dir: &Path) -> Result<()> { for entry in fs::read_dir(output_dir)? { let entry = entry?; let path = entry.path(); - if path - .file_name() - .unwrap() - .to_str() - .unwrap() - .starts_with("tmp") - { + if path.file_name().unwrap().to_str().unwrap().starts_with("tmp") { fs::remove_file(path)?; } } - fs::remove_file(output_dir.join("concat.txt"))?; Ok(()) }