From 638a80e014717b0c19baaeb07c5558e8ff81cb0b Mon Sep 17 00:00:00 2001 From: Toru Fukui Date: Wed, 25 Jan 2023 19:10:31 +0900 Subject: [PATCH] Initial commit --- .github/workflows/release.yml | 150 ++++++++++++++++++++++++++++++++++ .gitignore | 10 +++ Cargo.toml | 21 +++++ LICENSE | 21 +++++ README.md | 50 ++++++++++++ install.sh | 78 ++++++++++++++++++ src/main.rs | 115 ++++++++++++++++++++++++++ 7 files changed, 445 insertions(+) create mode 100644 .github/workflows/release.yml create mode 100644 .gitignore create mode 100644 Cargo.toml create mode 100644 LICENSE create mode 100644 README.md create mode 100755 install.sh create mode 100644 src/main.rs diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml new file mode 100644 index 0000000..7ba7305 --- /dev/null +++ b/.github/workflows/release.yml @@ -0,0 +1,150 @@ +on: + release: + types: + - created + +env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + CARGO_TERM_COLOR: always + +name: Create Release / Upload Assets + +jobs: + windows: + name: Build for Windows + runs-on: windows-latest + + steps: + - name: Checkout code + uses: actions/checkout@v2 + + - name: Set up cargo cache + uses: actions/cache@v3 + continue-on-error: false + with: + path: | + ~/.cargo/bin/ + ~/.cargo/registry/index/ + ~/.cargo/registry/cache/ + ~/.cargo/git/db/ + target/ + key: ${{ runner.os }}-cargo-${{ hashFiles('**/Cargo.lock') }} + restore-keys: ${{ runner.os }}-cargo- + + - name: Build + run: cargo build --release + + - name: "Move to outputs/ folder" + run: | + mkdir outputs + cp target/release/*.exe outputs/gt-win-x86_64.exe + - name: Upload to temporary storage + uses: actions/upload-artifact@master + with: + name: output-artifact + path: outputs + + linux: + name: Build for Linux + runs-on: ubuntu-latest + + steps: + - name: Checkout code + uses: actions/checkout@v2 + + - name: Set up cargo cache + uses: actions/cache@v3 + continue-on-error: false + with: + path: | + ~/.cargo/bin/ + ~/.cargo/registry/index/ + ~/.cargo/registry/cache/ + ~/.cargo/git/db/ + target/ + key: ${{ runner.os }}-cargo-${{ hashFiles('**/Cargo.lock') }} + restore-keys: ${{ runner.os }}-cargo- + + - name: Build + run: cargo build --release + + - name: Install cargo-deb + run: cargo install cargo-deb + continue-on-error: true + + - name: Create deb package + run: cargo deb + + - name: "Move to outputs/ folder" + run: | + mkdir outputs + cp target/release/gt outputs/gt-linux-x86_64 + cp target/debian/*.deb outputs/gt-linux-x86_64.deb + - name: Upload to temporary storage + uses: actions/upload-artifact@master + with: + name: output-artifact + path: outputs + + macos: + name: Build for Mac + runs-on: macos-11 + + steps: + - name: Checkout code + uses: actions/checkout@v2 + + - name: Set up cargo cache + uses: actions/cache@v3 + continue-on-error: false + with: + path: | + ~/.cargo/bin/ + ~/.cargo/registry/index/ + ~/.cargo/registry/cache/ + ~/.cargo/git/db/ + target/ + key: ${{ runner.os }}-cargo-${{ hashFiles('**/Cargo.lock') }} + restore-keys: ${{ runner.os }}-cargo- + + - name: Install ARM target + run: rustup update && rustup target add aarch64-apple-darwin + + - name: Build + run: cargo build --release --target=aarch64-apple-darwin + + - name: Build + run: cargo build --release + + # Name of binary needed + - name: "Move to outputs/ folder" + run: | + mkdir outputs + cp target/aarch64-apple-darwin/release/gt outputs/gt-darwin-aarch64 + cp target/release/gt outputs/gt-darwin-x86_64 + - name: Upload to temporary storage + uses: actions/upload-artifact@master + with: + name: output-artifact + path: outputs + + release: + name: Create/or release assets + runs-on: ubuntu-latest + needs: [windows, linux, macos] + + steps: + - name: Download from temporary storage + uses: actions/download-artifact@master + with: + name: output-artifact + path: outputs + + - name: Upload binaries to release + uses: svenstaro/upload-release-action@v2 + with: + repo_token: ${{ secrets.GITHUB_TOKEN }} + file: outputs/* + tag: ${{ github.ref }} + overwrite: true + file_glob: true diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..088ba6b --- /dev/null +++ b/.gitignore @@ -0,0 +1,10 @@ +# Generated by Cargo +# will have compiled files and executables +/target/ + +# Remove Cargo.lock from gitignore if creating an executable, leave it for libraries +# More information here https://doc.rust-lang.org/cargo/guide/cargo-toml-vs-cargo-lock.html +Cargo.lock + +# These are backup files generated by rustfmt +**/*.rs.bk diff --git a/Cargo.toml b/Cargo.toml new file mode 100644 index 0000000..61b22ab --- /dev/null +++ b/Cargo.toml @@ -0,0 +1,21 @@ +[package] +name = "gt" +license = "MIT" +edition = "2021" +version = "0.1.0" +readme = "README.md" +categories = ["command-line-utilities"] +homepage = "https://github.com/fukumone/gt" +repository = "https://github.com/fukumone/gt" +description = "Translate messages from terminal using OpenAI." +authors = ["Toru Fukui "] + +[dependencies] +clap = { version = "4.1.4", features = ["derive"] } +async-std = "1.12.0" +dotenv = "0.15.0" +reqwest = { version = "0.11", features = ["json"] } +serde = "1.0.152" +serde_derive = "1.0.152" +serde_json = "1.0.91" +tokio = { version = "1", features = ["full"] } diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..aafea1b --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2023 Toru Fukui + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/README.md b/README.md new file mode 100644 index 0000000..14ebd62 --- /dev/null +++ b/README.md @@ -0,0 +1,50 @@ +# gt +The OpenAI GPT model allows for easy access to language translation from the command line with a personal touch. + +## Installation + +You can install gt by running the following command in your terminal. + +``` +curl -fsSL https://raw.githubusercontent.com/fukumone/gt/main/install.sh | sh - +``` + +Or, please download the binary that is compatible with your OS from [here](https://github.com/fukumone/gt/releases/tag/v0.1.0). + +## Usage + +1. Get your API key from OpenAI by setting the `OPENAI_API_KEY` environment variable. Example: `export OPENAI_API_KEY=xxxxx` +For more information, visit the provided [link](https://openai.com/). + +2. To set the language you want to translate to, use the `GT_LANGUAGE` environment variable. For instance, if you want to translate to French, type `export GT_LANGUAGE=French` By default, the GT_LANGUAGE is set to translate to English in any language. + +3. Try it out by opening the command line and typing `gt -t "text"` + + +#### example +``` + +# Translated from English to French +$ export GT_LANGUAGE=French +$ gt -t "I love Tokyo greatly, it is wonderful to be here." +# => J'aime Tokyo grandement, c'est merveilleux d'être ici. + +# Translated from Spanish to Arabic +$ export GT_LANGUAGE=Arabic +$ gt -t "Me gusta mucho Tokio; es maravilloso estar aquí." +# => الحب لطوكيو كثيرا؛ هو مدهش أن نكون هنا. + +# Translated from Japanese to Italian +$ export GT_LANGUAGE=Italian +$ gt -t "私は東京がとても好きです。素晴らしいところです。" +# => Mi piace molto Tokyo. È un posto meraviglioso. + +# Translated from Mandarin to Portuguese +$ export GT_LANGUAGE=Portuguese +$ gt -t "我深深地爱上了东京,来到这里真是太美妙了。" +# => Eu me apaixonei profundamente por Tóquio, e estar aqui é realmente maravilhoso. +``` + +## License + +This project is open-sourced under the MIT license. See [the License file](LICENSE) for more information. diff --git a/install.sh b/install.sh new file mode 100755 index 0000000..eb90305 --- /dev/null +++ b/install.sh @@ -0,0 +1,78 @@ +#!/usr/bin/env bash +set -e + +main() { + BIN_DIR=${BIN_DIR-"$HOME/.bin"} + mkdir -p $BIN_DIR + + case $SHELL in + */zsh) + PROFILE=$HOME/.zshrc + PREF_SHELL=zsh + ;; + */bash) + PROFILE=$HOME/.bashrc + PREF_SHELL=bash + ;; + */fish) + PROFILE=$HOME/.config/fish/config.fish + PREF_SHELL=fish + ;; + */ash) + PROFILE=$HOME/.profile + PREF_SHELL=ash + ;; + *) + echo "could not detect shell, manually add ${BIN_DIR} to your PATH." + exit 1 + esac + + if [[ ":$PATH:" != *":${BIN_DIR}:"* ]]; then + echo >> $PROFILE && echo "export PATH=\"\$PATH:$BIN_DIR\"" >> $PROFILE + fi + + PLATFORM="$(uname -s)" + case $PLATFORM in + Linux) + PLATFORM="linux" + ;; + Darwin) + PLATFORM="darwin" + ;; + *) + err "unsupported platform: $PLATFORM" + ;; + esac + + ARCHITECTURE="$(uname -m)" + if [ "${ARCHITECTURE}" = "x86_64" ]; then + # Redirect stderr to /dev/null to avoid printing errors if non Rosetta. + if [ "$(sysctl -n sysctl.proc_translated 2>/dev/null)" = "1" ]; then + ARCHITECTURE="aarch64" # Rosetta. + else + ARCHITECTURE="x86_64" # Intel. + fi + elif [ "${ARCHITECTURE}" = "arm64" ] ||[ "${ARCHITECTURE}" = "aarch64" ] ; then + ARCHITECTURE="aarch64" # Arm. + else + ARCHITECTURE="x86_64" # Amd. + fi + + BINARY_URL="https://github.com/fukumone/gt/releases/latest/download/gt-${PLATFORM}-${ARCHITECTURE}" + echo $BINARY_URL + + echo "downloading latest binary" + ensure curl -L "$BINARY_URL" -o "$BIN_DIR/gt" + chmod +x "$BIN_DIR/gt" + + echo "installed - $("$BIN_DIR/gt" --version)" +} + +# Run a command that should never fail. If the command fails execution +# will immediately terminate with an error showing the failing +# command. +ensure() { + if ! "$@"; then err "command failed: $*"; fi +} + +main "$@" || exit 1 diff --git a/src/main.rs b/src/main.rs new file mode 100644 index 0000000..664661f --- /dev/null +++ b/src/main.rs @@ -0,0 +1,115 @@ +use clap::{arg, Command}; +extern crate reqwest; +extern crate serde; +extern crate serde_json; +extern crate dotenv; +#[macro_use] +extern crate serde_derive; + +use std::env; + +#[derive(Serialize, Deserialize, Debug)] +struct OpenAIResponse { + data: String, +} + +#[derive(Deserialize)] +struct OpenAIResponseData { + id: String, + object: String, + created: i64, + model: String, + choices: Vec, + usage: OpenAIResponseUsage, +} + +#[derive(Deserialize)] +struct OpenAIResponseChoice { + text: String, + index: i64, + logprobs: Option, + finish_reason: String, +} + +#[derive(Deserialize)] +struct OpenAIResponseUsage { + prompt_tokens: i64, + completion_tokens: i64, + total_tokens: i64, +} + +#[derive(Serialize, Debug)] +struct OpenAIRequestData { + prompt: String, + model: String, + temperature: f32, + max_tokens: i32, +} + +async fn fetch_openai_request(prompt: String, model: String) -> Result { + dotenv::dotenv().ok(); + let api_key = env::var("OPENAI_API_KEY").expect("OPENAI_API_KEY must be set"); + let client = reqwest::Client::new(); + + let request_data = OpenAIRequestData { + prompt, + model, + temperature: 0.5, + max_tokens: 2048, + }; + + let response = client + .post("https://api.openai.com/v1/completions") + .bearer_auth(api_key) + .json(&request_data) + .send() + .await? + .text() + .await?; + + let parse_response: OpenAIResponseData = serde_json::from_str(&response).unwrap(); + let text = parse_response.choices[0].text.trim().to_string(); + + Ok(text) +} + +fn cli() -> Command { + Command::new("gt") + .about("Translate messages from terminal using OpenAI.") + .subcommand_required(true) + .arg_required_else_help(true) + .allow_external_subcommands(true) + .subcommand( + Command::new("translate") + .about("Translates text") + .short_flag('t') + .arg(arg!( "Text to translate")) + .arg_required_else_help(true), + ) +} + +#[tokio::main] +async fn main() { + let matches = cli().get_matches(); + + match matches.subcommand() { + Some(("translate", sub_matches)) => { + let text = sub_matches.get_one::("TEXT").expect("required"); + let country = match env::var("GT_LANGUAGE") { + Ok(val) => val, + Err(_) => "English".to_string(), + }; + let prompt = format!("I want you to act as an {} translator, spelling corrector and improver. I will speak to you in any language and you will detect the language, translate it and answer in the corrected and improved version of my text, in {}. I want you to replace my simplified A0-level words and sentences with more beautiful and elegant, upper level {} words and sentences. Keep the meaning same, but make them more literary. I want you to only reply the correction, the improvements and nothing else, do not write explanations. My first sentence is '{}'", country, country, country, text); + let model = "text-davinci-003".to_string(); + let response:Result = fetch_openai_request(prompt, model).await; + match response { + Ok(text) => println!("{}", text), + Err(e) => println!("Error: Translation failed {}", e), + } + }, + _ => { + unreachable!("Unsupported subcommand `{}`", matches.subcommand_name().unwrap()) + } + + } +}