Skip to content

Commit

Permalink
Add dialoguer for interactive prompts and support for output format s…
Browse files Browse the repository at this point in the history
…election
  • Loading branch information
alexjsteffen committed Dec 7, 2024
1 parent 6c67e7f commit 36b070b
Show file tree
Hide file tree
Showing 3 changed files with 87 additions and 15 deletions.
16 changes: 16 additions & 0 deletions .vscode/launch.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
{
// Use IntelliSense to learn about possible attributes.
// Hover to view descriptions of existing attributes.
// For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
"version": "0.2.0",
"configurations": [
{
"type": "lldb",
"request": "launch",
"name": "Debug",
"program": "${workspaceFolder}/<executable file>",
"args": [],
"cwd": "${workspaceFolder}"
}
]
}
3 changes: 2 additions & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -13,4 +13,5 @@ tiktoken-rs = "0.5"
clap = { version = "4.4", features = ["derive"] }
anyhow = "1.0"
futures = "0.3"
indicatif = "0.17"
indicatif = "0.17"
dialoguer = "0.10"
83 changes: 69 additions & 14 deletions src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -10,22 +10,28 @@ use std::io::Write;
use std::path::Path;
use std::process::Command;
use tiktoken_rs::cl100k_base;
use dialoguer::{Input, Select};

// Define command-line arguments using the clap crate
#[derive(Parser, Debug)]
#[command(author, version, about, long_about = None)]
struct Args {
/// Input text file name
input_file: String,
#[arg()]
input_file: Option<String>,

/// TTS model to use (default: tts-1-hd)
#[arg(short, long, default_value = "tts-1-hd")]
model: String,

/// Voice to use for TTS (default: fable)
#[arg(short, long, default_value = "fable")]
/// Voice to use for TTS (default: alloy)
#[arg(short, long, default_value = "alloy")]
voice: String,

/// Output audio format (options: mp3, flac, wav, pcm)
#[arg(short, long, default_value = "flac")]
format: String,

/// OpenAI API key (optional, can also be set via the OPENAI_API_KEY environment variable)
#[arg(short, long)]
apikey: Option<String>,
Expand All @@ -45,19 +51,67 @@ struct OpenAIError {
/// The main function of the program.
#[tokio::main]
async fn main() -> Result<()> {
let args = Args::parse();
let mut args = Args::parse();

// Get the API key from either the command-line argument or the environment variable
let api_key = args.apikey
let api_key = args.apikey.clone()
.or_else(|| std::env::var("OPENAI_API_KEY").ok())
.or_else(|| {
// Prompt the user for the API key if not provided
let input: String = Input::new()
.with_prompt("Enter your OpenAI API Key")
.interact_text()
.ok()?;
Some(input)
})
.context(
"OpenAI API key not provided. Set it via the --apikey flag or the OPENAI_API_KEY environment variable."
"OpenAI API key not provided. Set it via the --apikey flag, the OPENAI_API_KEY environment variable, or input it when prompted."
)?;

// Prompt for input file if not provided
let input_file = args.input_file.clone().or_else(|| {
let input: String = Input::new()
.with_prompt("Enter the path to the input text file")
.interact_text()
.ok()?;
Some(input)
}).context("Input file not provided.")?;

args.input_file = Some(input_file);

// Prompt for voice selection
let voices = vec![
"Echo - A clear and bright voice ideal for announcements.",
"Fable - Great for storytelling.",
"Onyx - A deep and resonant voice.",
"Nova - A youthful and energetic voice.",
"Shimmer - A soft and soothing voice.",
"Alloy - A versatile and natural-sounding voice.",
];
if args.voice == "alloy" {
let selection = Select::new()
.with_prompt("Select a voice")
.items(&voices)
.default(0)
.interact()?;
args.voice = voices[selection].split(" - ").next().unwrap().to_string();
}

// Prompt for output format
let formats = vec!["mp3", "flac", "wav", "pcm"];
if args.format == "flac" {
let selection = Select::new()
.with_prompt("Select an output format")
.items(&formats)
.default(1)
.interact()?;
args.format = formats[selection].to_string();
}

let client = Client::new();

// Get the input file name and create an output directory
let input_file_path = Path::new(&args.input_file);
let input_file_path = Path::new(args.input_file.as_ref().unwrap());
let input_file_name = input_file_path
.file_stem()
.context("Invalid input file")?
Expand All @@ -72,15 +126,15 @@ async fn main() -> Result<()> {
let chunks = chunk_text(&lines);

// Generate audio files for each chunk
generate_audio_files(&chunks, &output_dir, &args.model, &args.voice, &client, &api_key).await?;
generate_audio_files(&chunks, &output_dir, &args.model, &args.voice, &args.format, &client, &api_key).await?;

println!(
"Chunk flac files are already in [ ./{} ] for ffmpeg to combine.\n\n",
green_text(input_file_name)
);

// Combine the audio files into a single output file
combine_audio_files(&output_dir)?;
combine_audio_files(&output_dir, &args.format)?;

// Remove temporary files
remove_tmp(&output_dir)?;
Expand Down Expand Up @@ -139,6 +193,7 @@ async fn generate_audio_files(
output_dir: &Path,
model: &str,
voice: &str,
format: &str,
client: &Client,
api_key: &str
) -> Result<()> {
Expand Down Expand Up @@ -196,7 +251,7 @@ async fn generate_audio_files(
}

// Save the audio response to a file
let file_name = format!("tmp_{}_chunk{:06}.flac", date_time_string, i + 1);
let file_name = format!("tmp_{}_chunk{:06}.{}", date_time_string, i + 1, format);
let file_path = output_dir.join(&file_name);
let mut file = File::create(&file_path)?;

Expand All @@ -212,7 +267,7 @@ async fn generate_audio_files(
}

/// Combines all the generated audio files into a single file using ffmpeg.
fn combine_audio_files(output_dir: &Path) -> Result<()> {
fn combine_audio_files(output_dir: &Path, format: &str) -> Result<()> {
// Collect all the temporary flac files in the output directory
let mut input_files = Vec::new();
for entry in fs::read_dir(output_dir)? {
Expand All @@ -221,7 +276,7 @@ fn combine_audio_files(output_dir: &Path) -> Result<()> {
if
path
.extension()
.map(|ext| ext == "flac")
.map(|ext| ext == format)
.unwrap_or(false) &&
path.file_name().unwrap().to_str().unwrap().starts_with("tmp")
{
Expand All @@ -243,9 +298,9 @@ fn combine_audio_files(output_dir: &Path) -> Result<()> {
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(format.to_string());
ffmpeg_args.push("-y".to_string()); // Overwrite output files without asking
ffmpeg_args.push(output_dir.join("output.flac").to_str().unwrap().to_string());
ffmpeg_args.push(output_dir.join(format!("output.{}", format)).to_str().unwrap().to_string());

// Execute the ffmpeg command
let status = Command::new("ffmpeg").args(&ffmpeg_args).status()?;
Expand Down

0 comments on commit 36b070b

Please sign in to comment.