├── .gitattributes ├── .github ├── dependabot.yml └── workflows │ ├── Release.yml │ └── Test.yml ├── .gitignore ├── Cargo.toml ├── LICENSE ├── README.md ├── src ├── chat.rs ├── main.rs ├── scripts │ ├── cleanup-tags.sh │ └── release.sh └── tts.rs └── tests ├── chat.rs ├── common └── mod.rs └── tts.rs /.gitattributes: -------------------------------------------------------------------------------- 1 | src/scripts/* linguist-vendored 2 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | - package-ecosystem: "github-actions" 4 | directory: "/" 5 | schedule: 6 | interval: "monthly" 7 | - package-ecosystem: "cargo" 8 | directory: "/" 9 | schedule: 10 | interval: "monthly" 11 | -------------------------------------------------------------------------------- /.github/workflows/Release.yml: -------------------------------------------------------------------------------- 1 | name: release 2 | 3 | on: 4 | push: 5 | branches: 6 | - main 7 | tags: 8 | - "v*.*.*" 9 | paths-ignore: 10 | - "README.md" 11 | - "LICENSE" 12 | pull_request: 13 | workflow_dispatch: 14 | 15 | jobs: 16 | build: 17 | strategy: 18 | fail-fast: false 19 | matrix: 20 | include: 21 | - os: ubuntu-latest 22 | target: x86_64-unknown-linux-gnu 23 | - os: macos-latest 24 | target: x86_64-apple-darwin 25 | - os: macos-latest 26 | target: aarch64-apple-darwin 27 | - os: windows-latest 28 | target: x86_64-pc-windows-msvc 29 | runs-on: ${{ matrix.os }} 30 | timeout-minutes: 15 31 | permissions: 32 | contents: write 33 | steps: 34 | - uses: actions/checkout@v4 35 | 36 | - run: | 37 | if [[ ${{ matrix.os }} = "windows-latest" ]]; then 38 | EXT=".exe" 39 | else 40 | EXT="" 41 | fi 42 | echo "EXT: $EXT" 43 | echo "ext=$EXT" >> $GITHUB_OUTPUT 44 | id: check 45 | shell: bash 46 | 47 | - run: | 48 | rustup update stable 49 | rustup default stable 50 | rustup target add ${{ matrix.target }} 51 | 52 | - run: | 53 | SRC="target/${{ matrix.target }}/release/ata${{ steps.check.outputs.ext }}" 54 | echo "SRC: $SRC" 55 | DST="target/release/ata-${{ matrix.target }}${{ steps.check.outputs.ext }}" 56 | echo "DST: $DST" 57 | cargo build --release --target ${{ matrix.target }} 58 | mv -v $SRC $DST 59 | echo "dst=$DST" >> $GITHUB_OUTPUT 60 | id: release 61 | shell: bash 62 | 63 | - uses: softprops/action-gh-release@v2 64 | if: startsWith(github.ref, 'refs/tags/') 65 | with: 66 | fail_on_unmatched_files: true 67 | files: ${{ steps.release.outputs.dst }} 68 | -------------------------------------------------------------------------------- /.github/workflows/Test.yml: -------------------------------------------------------------------------------- 1 | name: test 2 | 3 | on: 4 | push: 5 | branches: 6 | - main 7 | pull_request: 8 | workflow_dispatch: 9 | 10 | jobs: 11 | test: 12 | runs-on: ubuntu-latest 13 | timeout-minutes: 15 14 | 15 | steps: 16 | - uses: actions/checkout@v4 17 | 18 | - run: | 19 | rustup update stable 20 | rustup default stable 21 | 22 | - uses: Swatinem/rust-cache@v2 23 | with: 24 | prefix-key: 'test' 25 | 26 | - run: | 27 | echo "DEEPINFRA_KEY=${{ secrets.DEEPINFRA_KEY }}" > test.env 28 | echo "GOOGLE_KEY=${{ secrets.GOOGLE_KEY }}" >> test.env 29 | echo "OPENAI_KEY=${{ secrets.OPENAI_KEY }}" >> test.env 30 | 31 | - run: cargo test --all-features 32 | 33 | - name: Cleanup before Post Run 34 | run: rm test.env 35 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | **/target/ 2 | **/*.rs.bk 3 | ata.toml 4 | Cargo.lock 5 | test.env 6 | **/tmp* -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "trf" 3 | version = "1.0.0" 4 | edition = "2021" 5 | authors = ["Rik Huijzer"] 6 | license = "MIT" 7 | 8 | [dependencies] 9 | anyhow = "1" 10 | clap = { version = "4.5.29", features = ["derive"] } 11 | futures-util = "0.3.31" 12 | tokio = { version = "1", features = ["macros", "rt-multi-thread"] } 13 | tracing-subscriber = "0.3" 14 | tracing = "0.1" 15 | transformrs = "0.6" 16 | 17 | [dev-dependencies] 18 | assert_cmd = "2" 19 | predicates = "3" 20 | pretty_assertions = "1" 21 | tempfile = "3" 22 | toml = "0.8" 23 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2023 Rik Huijzer 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |

trf: Multimodal AI in the terminal

2 | 3 |

Supports OpenAI, DeepInfra, Google, Hyperbolic, and others

4 | 5 | ## Examples 6 | 7 | - [Chat](#chat-in-bash) 8 | - [Text to Speech](#text-to-speech-in-bash) 9 | 10 | ### Chat in Bash 11 | 12 | We can chat straight from the command line. 13 | For example, via the DeepInfra API: 14 | 15 | ```sh 16 | $ DEEPINFRA_KEY=""; echo "hi there" | trf chat 17 | ``` 18 | 19 | This defaults to the `meta-llama/Llama-3.3-70B-Instruct` model. 20 | We can also create a Bash script to provide some default settings to the chat. 21 | For example, create a file called `chat.sh` with the following content: 22 | 23 | ```bash 24 | #!/usr/bin/env bash 25 | 26 | export OPENAI_KEY="$(cat /path/to/key)" 27 | 28 | trf chat --model="gpt-4o" 29 | ``` 30 | 31 | and add it to your PATH. 32 | Now, we can use it like this: 33 | 34 | ```sh 35 | $ echo "This is a test. Respond with 'hello'." | trf chat 36 | hello 37 | ``` 38 | 39 | Or we can run a spellcheck on a file: 40 | 41 | ```sh 42 | $ echo "Do you see spelling errors in the following text?"; cat myfile.txt | trf chat 43 | ``` 44 | 45 | Here is a more complex example. 46 | For example, create a file called `writing-tips.sh` with the following content: 47 | 48 | ```bash 49 | #!/usr/bin/env bash 50 | set -euo pipefail 51 | 52 | export DEEPINFRA_KEY="$(cat /path/to/key)" 53 | 54 | PROMPT=" 55 | You are a helpful writing assistant. 56 | Respond with a few suggestions for improving the text. 57 | Use plain text only; no markdown. 58 | 59 | Here is the text to check: 60 | 61 | " 62 | MODEL="deepseek-ai/DeepSeek-R1-Distill-Llama-70B" 63 | 64 | (echo "$PROMPT"; cat README.md) | trf chat --model="$MODEL" 65 | ``` 66 | 67 | ### Text to Speech in Bash 68 | 69 | We can read a file out loud from the command line. 70 | For example, with the OpenAI API: 71 | 72 | ```sh 73 | $ OPENAI_KEY="$(cat /path/to/key)"; cat myfile.txt | trf tts | vlc - --intf dummy 74 | ``` 75 | 76 | Here, we set the key, print the file `myfile.txt` to stdout, pipe it to `trf` to generate mp3 audio, and pipe that to `vlc` to play it. 77 | The `--intf dummy` is optional; it just prevents `vlc` from opening a GUI. 78 | 79 | One way to make this easier to use is to create a Bash script that sets the environment variable and runs the command. 80 | For example, create a file called `spk.sh` (abbreviation for "speak") with the following content: 81 | 82 | ```bash 83 | #!/usr/bin/env bash 84 | 85 | # Exit on (pipe) errors. 86 | set -euo pipefail 87 | 88 | export OPENAI_KEY="$(cat /path/to/key)" 89 | 90 | trf tts | vlc - --intf dummy 91 | ``` 92 | 93 | After adding `spk.sh` to your PATH, you can use it like this: 94 | 95 | ```sh 96 | $ cat myfile.txt | spk 97 | ``` 98 | 99 | ### Other Text to Speech Commands 100 | 101 | ```sh 102 | $ DEEPINFRA_KEY="$(cat /path/to/key)"; cat myfile.txt | trf tts | vlc - 103 | ``` 104 | 105 | ```sh 106 | $ DEEPINFRA_KEY="$(cat /path/to/key)"; cat myfile.txt | trf tts --output myfile.mp3 107 | ``` 108 | 109 | ## Philosophy 110 | 111 | The philosophy of this project is mainly to not handle state. 112 | Like curl or ffmpeg, this should make it easier to use in scripts and to share examples online. 113 | Settings are done via command line arguments and environment variables. 114 | -------------------------------------------------------------------------------- /src/chat.rs: -------------------------------------------------------------------------------- 1 | use futures_util::stream::StreamExt; 2 | use std::fs::File; 3 | use std::io::Write; 4 | use transformrs::Message; 5 | use transformrs::Provider; 6 | 7 | #[derive(clap::Parser)] 8 | pub(crate) struct ChatArgs { 9 | /// Model to use (optional) 10 | #[arg(long)] 11 | model: Option, 12 | 13 | /// Output file (optional) 14 | #[arg(long, short = 'o')] 15 | output: Option, 16 | 17 | /// Stream output 18 | #[arg(long, default_value_t = true)] 19 | stream: bool, 20 | 21 | /// Raw JSON output 22 | #[arg(long)] 23 | raw_json: bool, 24 | 25 | /// Language code (optional) 26 | #[arg(long)] 27 | language_code: Option, 28 | } 29 | 30 | fn default_model(provider: &Provider) -> String { 31 | match provider { 32 | Provider::Google => "models/gemini-1.5-flash", 33 | Provider::OpenAI => "gpt-4o-mini", 34 | _ => "meta-llama/Llama-3.3-70B-Instruct", 35 | } 36 | .to_string() 37 | } 38 | 39 | pub(crate) async fn chat(args: &ChatArgs, key: &transformrs::Key, input: &str) { 40 | let provider = key.provider.clone(); 41 | let model = args 42 | .model 43 | .clone() 44 | .unwrap_or_else(|| default_model(&provider)); 45 | let messages = vec![Message::from_str("user", input)]; 46 | if args.stream { 47 | let mut stream = 48 | transformrs::chat::stream_chat_completion(&provider, key, &model, &messages) 49 | .await 50 | .expect("Streaming chat completion failed"); 51 | while let Some(resp) = stream.next().await { 52 | let msg = resp.choices[0].delta.content.clone().unwrap_or_default(); 53 | print!("{}", msg); 54 | // Ensure the output is printed immediately. 55 | std::io::stdout().flush().unwrap(); 56 | } 57 | } else { 58 | let resp = transformrs::chat::chat_completion(&provider, key, &model, &messages) 59 | .await 60 | .expect("Chat completion failed"); 61 | if args.raw_json { 62 | let json = resp.raw_value(); 63 | println!("{}", json.unwrap()); 64 | } 65 | let resp = resp.structured().expect("Could not parse response"); 66 | let content = resp.choices[0].message.content.clone(); 67 | if let Some(output) = args.output.clone() { 68 | let mut file = File::create(output).unwrap(); 69 | file.write_all(content.to_string().as_bytes()).unwrap(); 70 | } else { 71 | println!("{}", content); 72 | } 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /src/main.rs: -------------------------------------------------------------------------------- 1 | mod chat; 2 | mod tts; 3 | 4 | use chat::ChatArgs; 5 | use clap::Parser; 6 | use std::io::Read; 7 | use tracing::subscriber::SetGlobalDefaultError; 8 | use transformrs::Key; 9 | use tts::TextToSpeechArgs; 10 | 11 | #[derive(clap::Subcommand)] 12 | enum Commands { 13 | /// OpenAI-compatible chat. 14 | /// 15 | /// Takes text input from stdin and chats with an AI model. 16 | #[command()] 17 | Chat(ChatArgs), 18 | /// Convert text to speech 19 | /// 20 | /// Takes text input from stdin and converts it to speech using text-to-speech models. 21 | #[command()] 22 | Tts(TextToSpeechArgs), 23 | } 24 | 25 | #[derive(Parser)] 26 | #[command( 27 | author, 28 | version, 29 | about = "Ask the Terminal Anything - Use AI in the terminal" 30 | )] 31 | struct Arguments { 32 | #[command(subcommand)] 33 | command: Commands, 34 | /// Verbose output. 35 | /// 36 | /// The output of the logs is printed to stderr because the output is 37 | /// printed to stdout. 38 | #[arg(long)] 39 | verbose: bool, 40 | } 41 | 42 | pub enum Task { 43 | #[allow(clippy::upper_case_acronyms)] 44 | TTS, 45 | } 46 | 47 | fn find_single_key(keys: transformrs::Keys) -> Key { 48 | let keys = keys.keys; 49 | if keys.len() != 1 { 50 | eprintln!("Expected exactly one key, found {}", keys.len()); 51 | std::process::exit(1); 52 | } 53 | keys[0].clone() 54 | } 55 | 56 | /// Initialize logging with the given level. 57 | fn init_subscriber(level: tracing::Level) -> Result<(), SetGlobalDefaultError> { 58 | let subscriber = tracing_subscriber::FmtSubscriber::builder() 59 | .with_max_level(level) 60 | .with_writer(std::io::stderr) 61 | .without_time() 62 | .with_target(false) 63 | .finish(); 64 | tracing::subscriber::set_global_default(subscriber) 65 | } 66 | 67 | #[tokio::main] 68 | async fn main() { 69 | let args = Arguments::parse(); 70 | if args.verbose { 71 | init_subscriber(tracing::Level::DEBUG).unwrap(); 72 | } else { 73 | init_subscriber(tracing::Level::INFO).unwrap(); 74 | } 75 | 76 | let mut input = String::new(); 77 | std::io::stdin().read_to_string(&mut input).unwrap(); 78 | 79 | let keys = transformrs::load_keys(".env"); 80 | let key = find_single_key(keys); 81 | 82 | match args.command { 83 | Commands::Chat(args) => { 84 | chat::chat(&args, &key, &input).await; 85 | } 86 | Commands::Tts(args) => { 87 | tts::tts(&args, &key, &input).await; 88 | } 89 | } 90 | } 91 | -------------------------------------------------------------------------------- /src/scripts/cleanup-tags.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | set -e 4 | 5 | # Thanks to https://stackoverflow.com/questions/1841341. 6 | git tag -l | xargs git tag -d 7 | git fetch --tags 8 | -------------------------------------------------------------------------------- /src/scripts/release.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | # 4 | # Trigger a release 5 | # 6 | 7 | set -e 8 | 9 | # We have to run this locally because tags created from workflows do not 10 | # trigger new workflows. 11 | # "This prevents you from accidentally creating recursive workflow runs." 12 | 13 | METADATA="$(cargo metadata --format-version=1 --no-deps)" 14 | VERSION="$(echo $METADATA | jq -r '.packages[0].version')" 15 | echo "VERSION: $VERSION" 16 | TAGNAME="v$VERSION" 17 | echo "TAGNAME: $TAGNAME" 18 | 19 | echo "" 20 | echo "ENSURE YOU ARE ON THE MAIN BRANCH" 21 | echo "" 22 | 23 | read -p 'Release notes, which will not trigger a release yet: ' NOTES 24 | 25 | read -p "Creating a new tag, which WILL TRIGGER A RELEASE with the following release notes: \"$NOTES\". Are you sure? [y/N]" -n 1 -r 26 | if [[ $REPLY =~ ^[Yy]$ ]]; then 27 | echo "" 28 | git tag -a $TAGNAME -m "$NOTES" 29 | git push origin $TAGNAME 30 | fi 31 | -------------------------------------------------------------------------------- /src/tts.rs: -------------------------------------------------------------------------------- 1 | use crate::Task; 2 | use std::fs::File; 3 | use std::io::Write; 4 | use transformrs::Provider; 5 | 6 | #[derive(clap::Parser)] 7 | pub(crate) struct TextToSpeechArgs { 8 | /// Voice to use for text-to-speech (optional) 9 | #[arg(long)] 10 | voice: Option, 11 | 12 | /// Model to use (optional) 13 | #[arg(long)] 14 | model: Option, 15 | 16 | /// Output file (optional) 17 | #[arg(long, short = 'o')] 18 | output: Option, 19 | 20 | /// Language code (optional) 21 | #[arg(long)] 22 | language_code: Option, 23 | 24 | /// Output format (optional) 25 | #[arg(long)] 26 | output_format: Option, 27 | } 28 | 29 | fn default_output_format(provider: &Provider) -> Option { 30 | match provider { 31 | Provider::DeepInfra => Some("mp3".to_string()), 32 | _ => None, 33 | } 34 | } 35 | 36 | fn default_voice(provider: &Provider) -> Option { 37 | match provider { 38 | Provider::OpenAI => Some("alloy".to_string()), 39 | Provider::Google => Some("en-US-Studio-Q".to_string()), 40 | _ => None, 41 | } 42 | } 43 | 44 | fn default_model(provider: &Provider, task: &Task) -> Option { 45 | match provider { 46 | Provider::OpenAI => match task { 47 | Task::TTS => Some("tts-1".to_string()), 48 | }, 49 | _ => None, 50 | } 51 | } 52 | 53 | fn default_language_code(provider: &Provider) -> Option { 54 | match provider { 55 | Provider::Google => Some("en-US".to_string()), 56 | _ => None, 57 | } 58 | } 59 | 60 | pub(crate) async fn tts(args: &TextToSpeechArgs, key: &transformrs::Key, input: &str) { 61 | let provider = key.provider.clone(); 62 | let config = transformrs::text_to_speech::TTSConfig { 63 | voice: args.voice.clone().or_else(|| default_voice(&provider)), 64 | output_format: args 65 | .output_format 66 | .clone() 67 | .or_else(|| default_output_format(&provider)), 68 | language_code: args 69 | .language_code 70 | .clone() 71 | .or_else(|| default_language_code(&provider)), 72 | ..Default::default() 73 | }; 74 | let model = args 75 | .model 76 | .clone() 77 | .or_else(|| default_model(&provider, &Task::TTS)); 78 | eprintln!( 79 | "Requesting text to speech for text of length {}...", 80 | input.len() 81 | ); 82 | let resp = transformrs::text_to_speech::tts(key, &config, model.as_deref(), input) 83 | .await 84 | .unwrap() 85 | .structured() 86 | .unwrap(); 87 | let bytes = resp.audio.clone(); 88 | eprintln!("Received audio."); 89 | if let Some(output) = args.output.clone() { 90 | let mut file = File::create(output).unwrap(); 91 | file.write_all(&bytes).unwrap(); 92 | } else { 93 | std::io::stdout().write_all(&bytes).unwrap(); 94 | } 95 | } 96 | -------------------------------------------------------------------------------- /tests/chat.rs: -------------------------------------------------------------------------------- 1 | mod common; 2 | 3 | use common::load_key; 4 | use common::trf; 5 | use predicates::prelude::*; 6 | use transformrs::Provider; 7 | 8 | fn canonicalize_response(text: &str) -> String { 9 | text.to_lowercase() 10 | .trim() 11 | .trim_end_matches('.') 12 | .trim_end_matches('!') 13 | .to_string() 14 | } 15 | 16 | #[test] 17 | fn unexpected_argument() -> Result<(), Box> { 18 | let mut cmd = trf(); 19 | cmd.arg("foobar"); 20 | cmd.assert() 21 | .failure() 22 | .stderr(predicate::str::contains("unrecognized subcommand")); 23 | 24 | Ok(()) 25 | } 26 | 27 | #[test] 28 | fn tts_no_args() -> Result<(), Box> { 29 | let dir = tempfile::tempdir().unwrap(); 30 | let mut cmd = trf(); 31 | let key = load_key(&Provider::DeepInfra); 32 | let cmd = cmd 33 | .arg("chat") 34 | .env("DEEPINFRA_KEY", key) 35 | .write_stdin("This is a test. Respond with 'hello'.") 36 | .current_dir(&dir); 37 | let output = cmd.assert().success().get_output().stdout.clone(); 38 | 39 | let text = String::from_utf8(output.clone()).unwrap(); 40 | let content = canonicalize_response(&text); 41 | assert_eq!(content, "hello"); 42 | Ok(()) 43 | } 44 | -------------------------------------------------------------------------------- /tests/common/mod.rs: -------------------------------------------------------------------------------- 1 | use assert_cmd::Command; 2 | use std::io::BufRead; 3 | use transformrs::Provider; 4 | 5 | pub fn trf() -> Command { 6 | Command::cargo_bin("trf").unwrap() 7 | } 8 | 9 | #[allow(dead_code)] 10 | /// Load a key from the local .env file. 11 | /// 12 | /// This is used for testing only. Expects the .env file to contain keys for providers in the following format: 13 | /// 14 | /// ``` 15 | /// DEEPINFRA_KEY="" 16 | /// OPENAI_KEY="" 17 | /// ``` 18 | pub fn load_key(provider: &Provider) -> String { 19 | fn finder(line: &Result, provider: &Provider) -> bool { 20 | line.as_ref().unwrap().starts_with(&provider.key_name()) 21 | } 22 | let path = std::path::Path::new("test.env"); 23 | let file = std::fs::File::open(path).expect("Failed to open .env file"); 24 | let reader = std::io::BufReader::new(file); 25 | let mut lines = reader.lines(); 26 | let key = lines.find(|line| finder(line, provider)).unwrap().unwrap(); 27 | key.split("=").nth(1).unwrap().to_string() 28 | } 29 | -------------------------------------------------------------------------------- /tests/tts.rs: -------------------------------------------------------------------------------- 1 | mod common; 2 | 3 | use common::load_key; 4 | use common::trf; 5 | use predicates::prelude::*; 6 | use transformrs::Provider; 7 | 8 | #[test] 9 | fn unexpected_argument() -> Result<(), Box> { 10 | let mut cmd = trf(); 11 | cmd.arg("foobar"); 12 | cmd.assert() 13 | .failure() 14 | .stderr(predicate::str::contains("unrecognized subcommand")); 15 | 16 | Ok(()) 17 | } 18 | 19 | #[test] 20 | fn help() -> Result<(), Box> { 21 | let mut cmd = trf(); 22 | cmd.arg("--help"); 23 | cmd.assert() 24 | .success() 25 | .stdout(predicate::str::contains("Usage: trf")); 26 | 27 | Ok(()) 28 | } 29 | 30 | #[test] 31 | fn tts_no_args() -> Result<(), Box> { 32 | let dir = tempfile::tempdir().unwrap(); 33 | let mut cmd = trf(); 34 | let key = load_key(&Provider::DeepInfra); 35 | let cmd = cmd 36 | .arg("tts") 37 | .env("DEEPINFRA_KEY", key) 38 | .write_stdin("Hello world") 39 | .current_dir(&dir); 40 | let output = cmd.assert().success().get_output().stdout.clone(); 41 | 42 | assert!(output.len() > 0); 43 | 44 | Ok(()) 45 | } 46 | 47 | fn tts_default_settings_helper(provider: &Provider) -> Result<(), Box> { 48 | let dir = tempfile::tempdir().unwrap(); 49 | let mut cmd = trf(); 50 | let key = load_key(provider); 51 | let name = provider.key_name(); 52 | cmd.arg("tts") 53 | .arg("--output") 54 | .arg("output.mp3") 55 | .env(name, key) 56 | .write_stdin("Hi") 57 | .current_dir(&dir) 58 | .assert() 59 | .success(); 60 | 61 | let path = dir.path().join("output.mp3"); 62 | assert!(path.exists()); 63 | 64 | Ok(()) 65 | } 66 | 67 | #[test] 68 | fn tts_no_args_deepinfra() -> Result<(), Box> { 69 | tts_default_settings_helper(&Provider::DeepInfra) 70 | } 71 | 72 | #[test] 73 | fn tts_no_args_google() -> Result<(), Box> { 74 | tts_default_settings_helper(&Provider::Google) 75 | } 76 | 77 | #[test] 78 | fn tts_no_args_openai() -> Result<(), Box> { 79 | tts_default_settings_helper(&Provider::OpenAI) 80 | } 81 | --------------------------------------------------------------------------------