├── .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 |
--------------------------------------------------------------------------------