├── .gitignore ├── Subtitlr.png ├── main.go ├── go.mod ├── .github └── workflows │ └── go.yml ├── LICENSE ├── cmd ├── root.go ├── configure.go ├── generate.go └── translate.go ├── install.sh ├── README.md └── go.sum /.gitignore: -------------------------------------------------------------------------------- 1 | *.srt 2 | .env 3 | temp/* 4 | Subtitlr 5 | -------------------------------------------------------------------------------- /Subtitlr.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yoanbernabeu/Subtitlr/HEAD/Subtitlr.png -------------------------------------------------------------------------------- /main.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright © 2023 Yoan Bernabeu 3 | 4 | */ 5 | package main 6 | 7 | import "yoanbernabeu/Subtitlr/cmd" 8 | 9 | func main() { 10 | cmd.Execute() 11 | } 12 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module yoanbernabeu/Subtitlr 2 | 3 | go 1.18 4 | 5 | require ( 6 | github.com/bitly/go-simplejson v0.5.0 // indirect 7 | github.com/dlclark/regexp2 v1.9.0 // indirect 8 | github.com/dop251/goja v0.0.0-20230402114112-623f9dda9079 // indirect 9 | github.com/go-sourcemap/sourcemap v2.1.3+incompatible // indirect 10 | github.com/google/pprof v0.0.0-20230406165453-00490a63f317 // indirect 11 | github.com/inconshreveable/mousetrap v1.1.0 // indirect 12 | github.com/joho/godotenv v1.5.1 // indirect 13 | github.com/kkdai/youtube/v2 v2.8.0 // indirect 14 | github.com/spf13/cobra v1.7.0 // indirect 15 | github.com/spf13/pflag v1.0.5 // indirect 16 | golang.org/x/text v0.9.0 // indirect 17 | ) 18 | -------------------------------------------------------------------------------- /.github/workflows/go.yml: -------------------------------------------------------------------------------- 1 | name: goreleaser 2 | 3 | on: 4 | release: 5 | types: [created] 6 | 7 | permissions: 8 | contents: write 9 | 10 | jobs: 11 | goreleaser: 12 | runs-on: ubuntu-latest 13 | steps: 14 | - 15 | name: Checkout 16 | uses: actions/checkout@v3 17 | with: 18 | fetch-depth: 0 19 | - 20 | name: Set up Go 21 | uses: actions/setup-go@v4 22 | - 23 | name: Run GoReleaser 24 | uses: goreleaser/goreleaser-action@v4 25 | with: 26 | # either 'goreleaser' (default) or 'goreleaser-pro' 27 | distribution: goreleaser 28 | version: latest 29 | args: release --clean 30 | env: 31 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 32 | # Your GoReleaser Pro key, if you are using the 'goreleaser-pro' distribution 33 | # GORELEASER_KEY: ${{ secrets.GORELEASER_KEY }} -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright - 2023 - Yoan Bernabeu 2 | 3 | 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: 4 | 5 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 6 | 7 | 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. -------------------------------------------------------------------------------- /cmd/root.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright © 2023 Yoan Bernabeu 3 | 4 | */ 5 | package cmd 6 | 7 | import ( 8 | "os" 9 | 10 | "github.com/spf13/cobra" 11 | ) 12 | 13 | // rootCmd represents the base command when called without any subcommands 14 | var rootCmd = &cobra.Command{ 15 | Use: "Subtitlr", 16 | Short: "AI-assisted subtitle generation CLI for Youtube", 17 | Long: `This application, a subtitle generator for YouTube, utilizes OpenAI's Whisper API. 18 | 19 | This tool leverages artificial intelligence to efficiently transcribe speech in YouTube videos into text, thereby generating accurate subtitles (in SRT format). 20 | 21 | It's designed to improve the accessibility and convenience of video content, ensuring that no matter your language or hearing ability, you can fully engage with and comprehend the material.`, 22 | // Uncomment the following line if your bare application 23 | // has an action associated with it: 24 | // Run: func(cmd *cobra.Command, args []string) { }, 25 | } 26 | 27 | // Execute adds all child commands to the root command and sets flags appropriately. 28 | // This is called by main.main(). It only needs to happen once to the rootCmd. 29 | func Execute() { 30 | err := rootCmd.Execute() 31 | if err != nil { 32 | os.Exit(1) 33 | } 34 | } 35 | 36 | func init() { 37 | // Here you will define your flags and configuration settings. 38 | // Cobra supports persistent flags, which, if defined here, 39 | // will be global for your application. 40 | 41 | // rootCmd.PersistentFlags().StringVar(&cfgFile, "config", "", "config file (default is $HOME/.Subtitlr.yaml)") 42 | 43 | // Cobra also supports local flags, which will only run 44 | // when this action is called directly. 45 | rootCmd.Flags().BoolP("toggle", "t", false, "Help message for toggle") 46 | } 47 | -------------------------------------------------------------------------------- /cmd/configure.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright © 2023 Yoan Bernabeu 3 | 4 | */ 5 | package cmd 6 | 7 | import ( 8 | "fmt" 9 | "log" 10 | "os" 11 | 12 | "github.com/spf13/cobra" 13 | ) 14 | 15 | // configureCmd represents the configure command 16 | var configureCmd = &cobra.Command{ 17 | Use: "configure", 18 | Short: "Create a .env file with your OpenAI API key", 19 | Long: `The 'configure' command creates a .env file with your OpenAI API key in your current directory`, 20 | Run: func(cmd *cobra.Command, args []string) { 21 | /* Variables declaration */ 22 | apiKey, _ := cmd.Flags().GetString("apiKey") 23 | 24 | //Check if the .env file already exists 25 | if _, err := os.Stat(".env"); os.IsNotExist(err) { 26 | //Create the .env file 27 | file, err := os.Create(".env") 28 | if err != nil { 29 | log.Fatal(err) 30 | } 31 | defer file.Close() 32 | 33 | //Write the OpenAI API key in the .env file 34 | _, err = file.WriteString("OPENAI_API_KEY=" + apiKey) 35 | if err != nil { 36 | log.Fatal(err) 37 | } 38 | fmt.Println("The .env file has been created in your current directory") 39 | 40 | //Display the success message 41 | fmt.Println("---------------------------------------") 42 | fmt.Println("You have entered the following values:") 43 | fmt.Println("apiKey:", apiKey) 44 | fmt.Println("---------------------------------------") 45 | 46 | return 47 | } 48 | 49 | //Display the error message 50 | fmt.Println("---------------------------------------") 51 | fmt.Println("The .env file already exists in your current directory") 52 | fmt.Println("---------------------------------------") 53 | }, 54 | } 55 | 56 | func init() { 57 | rootCmd.AddCommand(configureCmd) 58 | configureCmd.Flags().String("apiKey", "", "OpenAI API key") 59 | configureCmd.MarkFlagRequired("apiKey") 60 | } 61 | -------------------------------------------------------------------------------- /install.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | echo "Installing Subtitlr..." 4 | echo "------------------------" 5 | 6 | # Determining the Linux distribution and architecture 7 | distro=$(lsb_release -i -s) 8 | arch=$(uname -m) 9 | 10 | echo "Distribution: $distro" 11 | echo "Architecture: $arch" 12 | 13 | # Subtitlr version 14 | version="0.2.0" 15 | 16 | echo "Version: $version" 17 | echo "------------------------" 18 | 19 | # URL for downloading the archive based on the distribution and architecture 20 | url="" 21 | 22 | case "$distro" in 23 | "Darwin") 24 | case "$arch" in 25 | "x86_64") 26 | url="https://github.com/yoanbernabeu/Subtitlr/releases/download/${version}/Subtitlr_${version}_darwin_amd64.tar.gz" 27 | ;; 28 | "arm64") 29 | url="https://github.com/yoanbernabeu/Subtitlr/releases/download/${version}/Subtitlr_${version}_darwin_arm64.tar.gz" 30 | ;; 31 | *) 32 | echo "Unsupported architecture" 33 | exit 1 34 | ;; 35 | esac 36 | ;; 37 | "Ubuntu"|"Debian"|"Raspbian") 38 | echo "Downloading Subtitlr..." 39 | case "$arch" in 40 | "i686") 41 | url="https://github.com/yoanbernabeu/Subtitlr/releases/download/${version}/Subtitlr_${version}_linux_386.tar.gz" 42 | ;; 43 | "x86_64") 44 | url="https://github.com/yoanbernabeu/Subtitlr/releases/download/${version}/Subtitlr_${version}_linux_amd64.tar.gz" 45 | echo $url 46 | ;; 47 | "arm64") 48 | url="https://github.com/yoanbernabeu/Subtitlr/releases/download/${version}/Subtitlr_${version}_linux_arm64.tar.gz" 49 | ;; 50 | *) 51 | echo "Unsupported architecture" 52 | exit 1 53 | ;; 54 | esac 55 | ;; 56 | *) 57 | echo "Unsupported distribution" 58 | exit 1 59 | ;; 60 | esac 61 | 62 | # Downloading the archive to home directory (and check if url is not 404) 63 | echo "Downloading Subtitlr..." 64 | wget -q --spider $url 65 | if [ $? -eq 0 ]; then 66 | wget -O ~/Subtitlr.tar.gz $url -q --show-progress 67 | else 68 | echo "------------------------" 69 | echo "Subtitlr archive not found" 70 | echo "------------------------" 71 | exit 1 72 | fi 73 | 74 | # Extracting the archive (if it exists) 75 | echo "Extracting Subtitlr..." 76 | if [ -f ~/Subtitlr.tar.gz ]; then 77 | tar -xzf ~/Subtitlr.tar.gz -C ~/ 78 | else 79 | echo "Subtitlr archive not found" 80 | exit 1 81 | fi 82 | 83 | # Removing the archive 84 | echo "Removing archive..." 85 | rm ~/Subtitlr.tar.gz 86 | 87 | # Moving the binary to /usr/local/bin 88 | echo "Moving Subtitlr to /usr/local/bin..." 89 | sudo mv ~/Subtitlr /usr/local/bin/ 90 | 91 | # Making the binary executable 92 | echo "Making Subtitlr executable..." 93 | sudo chmod +x /usr/local/bin/Subtitlr 94 | 95 | # Sending a message to the user 96 | echo "-----------------------------------------" 97 | echo "Subtitlr successfully installed" 98 | echo "-----------------------------------------" -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Subtitlr (Experimental) 2 | 3 | [![GitHub release (latest by date)](https://img.shields.io/github/v/release/yoanbernabeu/Subtitlr)](https://github.com/yoanbernabeu/Subtitlr/releases/latest) 4 | [![GitHub](https://img.shields.io/github/license/yoanbernabeu/Subtitlr)](./LICENSE) 5 | 6 | AI-assisted subtitle generation CLI for Youtube 7 | 8 | ![Subtitlr](Subtitlr.png) 9 | 10 | ## Description 11 | 12 | This application, a subtitle generator for YouTube, utilizes OpenAI's Whisper API. 13 | 14 | This tool leverages artificial intelligence to efficiently transcribe speech in YouTube videos into text, thereby generating accurate subtitles (in SRT format). 15 | 16 | It's designed to improve the accessibility and convenience of video content, ensuring that no matter your language or hearing ability, you can fully engage with and comprehend the material. 17 | 18 | ## Features 19 | 20 | ### Generate subtitles - The simplest way 21 | 22 | The simplest way to use Subtitlr without configuration is to use the following command: 23 | 24 | ```bash 25 | Subtitlr generate --id qJpR1NBx4cU --lang fr --output output.srt --apiKey sk-**************************** 26 | ``` 27 | 28 | ### Generate subtitles - With configuration 29 | 30 | You can also use a `.env` file to store your API key (in `OPENAI_API_KEY` variable) and use the following command: 31 | 32 | ```bash 33 | Subtitlr configure --apiKey sk-**************************** 34 | ``` 35 | 36 | And after that, you can use the following command without the `--apiKey` parameter: 37 | 38 | ```bash 39 | Subtitlr generate --id qJpR1NBx4cU --lang fr --output output.srt 40 | ``` 41 | 42 | ### Translating subtitles 43 | 44 | > For translations we offer you the possibility to use the DeepL API with a free account only (500000 per month). 45 | > 46 | > Create an *free* account on [DeepL](https://www.deepl.com/fr/signup?cta=free-login-signup/) 47 | 48 | You must have previously generated your subtitle file with the `generate` command. 49 | 50 | You can use the following command to translate subtitles: 51 | 52 | ```bash 53 | Subtitlr translate --input input.srt --lang EN --output output_EN.srt --apiKeyDeepl **************************** 54 | ``` 55 | 56 | ## Requirements 57 | 58 | * [OpenAI API key](https://beta.openai.com/) 59 | * [FFmpeg](https://ffmpeg.org/) 60 | * Linux (tested on Ubuntu 22.04), MacOS (not tested), Windows (not tested) 61 | * You have read/write rights to the current directory 62 | 63 | ## Parameters 64 | 65 | ### `generate` command 66 | 67 | | Name | Description | Required | 68 | | --- | --- | --- | 69 | | id | Youtube video id | true | 70 | | lang | Language speaking in the video (in ISO 639-1 format) | true | 71 | | output | Output file | true | 72 | | apiKey | OpenAI API key | false (if you use the `configure` command) | 73 | 74 | ### `translate` command 75 | 76 | | Name | Description | Required | 77 | | --- | --- | --- | 78 | | input | Input file | true | 79 | | lang | Language to translate (in ISO 639-1 format) | true | 80 | | output | Output file | true | 81 | | apiKeyDeepl | DeepL API key | true | 82 | 83 | ## Installation 84 | 85 | ### From binary 86 | 87 | * Linux/Darwin 88 | 89 | _Using cURL_ 90 | 91 | ```bash 92 | wget -qO- https://raw.githubusercontent.com/yoanbernabeu/Subtitlr/main/install.sh | bash 93 | ``` 94 | 95 | _Using wget_ 96 | 97 | ```bash 98 | curl -sL https://raw.githubusercontent.com/yoanbernabeu/Subtitlr/main/install.sh | bash 99 | ``` 100 | 101 | * Windows (Not tested): Download the [latest release](https://github.com/yoanbernabeu/Subtitlr/releases) 102 | 103 | ### From source 104 | 105 | > Subtitlr is written in Go, so you need to install it first. 106 | 107 | ```bash 108 | git clone git@github.com:yoanbernabeu/Subtitlr.git 109 | cd Subtitlr 110 | go build -o Subtitlr 111 | ``` 112 | 113 | ## License 114 | 115 | [MIT](LICENSE) 116 | 117 | ## Contributing 118 | 119 | Pull requests are welcome. For major changes, please open an issue first to discuss what you would like to change. -------------------------------------------------------------------------------- /cmd/generate.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright © 2023 Yoan Bernabeu 3 | 4 | */ 5 | package cmd 6 | 7 | import ( 8 | "bytes" 9 | "fmt" 10 | "io" 11 | "io/ioutil" 12 | "mime/multipart" 13 | "net/http" 14 | "os" 15 | "os/exec" 16 | 17 | "github.com/joho/godotenv" 18 | "github.com/kkdai/youtube/v2" 19 | "github.com/spf13/cobra" 20 | ) 21 | 22 | // generateCmd represents the generate command 23 | var generateCmd = &cobra.Command{ 24 | Use: "generate", 25 | Short: "Command to start the translation in SRT format", 26 | Long: `The 'generate' command is a crucial feature of our subtitle generator application. Once activated, this command initiates the process of generating subtitles from a provided YouTube video ID 27 | The 'generate' command is a crucial feature of our subtitle generator application. Once activated, this command initiates the process of generating subtitles from a provided YouTube video ID.`, 28 | Run: func(cmd *cobra.Command, args []string) { 29 | /* Variables declaration */ 30 | id, _ := cmd.Flags().GetString("id") 31 | lang, _ := cmd.Flags().GetString("lang") 32 | output, _ := cmd.Flags().GetString("output") 33 | apiKey, _ := cmd.Flags().GetString("apiKey") 34 | 35 | /* Displaying the values of the flags */ 36 | fmt.Println("---------------------------------------") 37 | fmt.Println("You have entered the following values:") 38 | fmt.Println("id:", id) 39 | fmt.Println("lang:", lang) 40 | fmt.Println("output:", output) 41 | fmt.Println("apiKey:", apiKey) 42 | fmt.Println("---------------------------------------") 43 | 44 | /* Calling the function to generate the subtitles */ 45 | generateSubtitles(id, lang, output, apiKey) 46 | 47 | /* Displaying the success message */ 48 | fmt.Println("---------------------------------------") 49 | }, 50 | } 51 | 52 | func init() { 53 | rootCmd.AddCommand(generateCmd) 54 | 55 | generateCmd.Flags().String("id", "", "YouTube video ID") 56 | generateCmd.Flags().String("lang", "", "Language (in ISO 639-1 format) speaking in the video") 57 | generateCmd.Flags().String("output", "", "Output file") 58 | 59 | generateCmd.MarkFlagRequired("url") 60 | generateCmd.MarkFlagRequired("lang") 61 | generateCmd.MarkFlagRequired("output") 62 | 63 | manageApiKeyEnv() 64 | } 65 | 66 | func manageApiKeyEnv() { 67 | err := godotenv.Load(".env") 68 | if err != nil { 69 | fmt.Println(".env file not found") 70 | generateCmd.Flags().String("apiKey", "", "OpenAI API key") 71 | return 72 | } 73 | 74 | apiKey := os.Getenv("OPENAI_API_KEY") 75 | 76 | if apiKey == "" { 77 | fmt.Println("Error: OPENAI_API_KEY not found in .env file") 78 | generateCmd.Flags().String("apiKey", "", "OpenAI API key") 79 | generateCmd.MarkFlagRequired("apiKey") 80 | return 81 | } 82 | 83 | generateCmd.Flags().String("apiKey", apiKey, "OpenAI API key") 84 | } 85 | 86 | func generateSubtitles(id string, lang string, output string, apiKey string) { 87 | /* Downloading the video */ 88 | downloadVideo(id) 89 | 90 | /* Converting the video to audio */ 91 | convertVideoToAudio() 92 | 93 | /* Generating the subtitles */ 94 | generateSubtitlesFromAudio(lang, output, apiKey) 95 | 96 | /* Deleting the temp folder */ 97 | os.RemoveAll("temp") 98 | } 99 | 100 | func downloadVideo(id string) { 101 | fmt.Println("Downloading the video...") 102 | videoID := id 103 | client := youtube.Client{} 104 | 105 | video, err := client.GetVideo(videoID) 106 | if err != nil { 107 | panic(err) 108 | } 109 | 110 | formats := video.Formats.WithAudioChannels() // only get videos with audio 111 | stream, _, err := client.GetStream(video, &formats[0]) 112 | if err != nil { 113 | panic(err) 114 | } 115 | 116 | if _, err := os.Stat("temp"); os.IsNotExist(err) { 117 | os.Mkdir("temp", 0755) 118 | } 119 | 120 | file, err := os.Create("temp/temp.mp4") 121 | if err != nil { 122 | panic(err) 123 | } 124 | defer file.Close() 125 | 126 | _, err = io.Copy(file, stream) 127 | if err != nil { 128 | panic(err) 129 | } 130 | 131 | fmt.Println("Video downloaded successfully!") 132 | } 133 | 134 | func convertVideoToAudio() { 135 | fmt.Println("Converting the video to audio...") 136 | /* extract audio from video */ 137 | file, err := os.Open("temp/temp.mp4") 138 | if err != nil { 139 | panic(err) 140 | } 141 | defer file.Close() 142 | 143 | /* create the output file */ 144 | out, err := os.Create("temp/temp.mp3") 145 | if err != nil { 146 | panic(err) 147 | } 148 | defer out.Close() 149 | 150 | /* convert to mp3 with ffmpeg with big compression */ 151 | ffmpeg := exec.Command("ffmpeg", "-i", "pipe:0", "-f", "mp3", "-ab", "64k", "-vn", "pipe:1") 152 | ffmpeg.Stdin = file 153 | ffmpeg.Stdout = out 154 | err = ffmpeg.Run() 155 | if err != nil { 156 | panic(err) 157 | } 158 | 159 | fmt.Println("Video converted to audio successfully!") 160 | } 161 | 162 | func generateSubtitlesFromAudio(lang string, output string, apiKey string) { 163 | fmt.Println("Generating the subtitles...") 164 | 165 | var b bytes.Buffer 166 | w := multipart.NewWriter(&b) 167 | 168 | // Open the file 169 | file, err := os.Open("temp/temp.mp3") 170 | if err != nil { 171 | fmt.Println(err) 172 | return 173 | } 174 | defer file.Close() 175 | 176 | // Add the file to the request 177 | fw, err := w.CreateFormFile("file", file.Name()) 178 | if err != nil { 179 | fmt.Println(err) 180 | return 181 | } 182 | if _, err = io.Copy(fw, file); err != nil { 183 | fmt.Println(err) 184 | return 185 | } 186 | 187 | // Add the model to the request 188 | if err = w.WriteField("model", "whisper-1"); err != nil { 189 | fmt.Println(err) 190 | return 191 | } 192 | 193 | // Add the response format to the request 194 | if err = w.WriteField("response_format", "srt"); err != nil { 195 | fmt.Println(err) 196 | return 197 | } 198 | 199 | // Add the language to the request 200 | if err = w.WriteField("language", lang); err != nil { 201 | fmt.Println(err) 202 | return 203 | } 204 | 205 | // Close the request 206 | if err = w.Close(); err != nil { 207 | fmt.Println(err) 208 | return 209 | } 210 | 211 | // Create the request 212 | req, err := http.NewRequest("POST", "https://api.openai.com/v1/audio/transcriptions", &b) 213 | if err != nil { 214 | fmt.Println(err) 215 | return 216 | } 217 | req.Header.Set("Content-Type", w.FormDataContentType()) 218 | req.Header.Set("Authorization", "Bearer "+apiKey) 219 | 220 | // Send the request 221 | client := &http.Client{} 222 | res, err := client.Do(req) 223 | if err != nil { 224 | fmt.Println(err) 225 | return 226 | } 227 | defer res.Body.Close() 228 | 229 | // Check the response 230 | if res.StatusCode == http.StatusOK { 231 | bodyBytes, err := ioutil.ReadAll(res.Body) 232 | if err != nil { 233 | fmt.Println(err) 234 | return 235 | } 236 | 237 | // Write the response to the output file 238 | err = ioutil.WriteFile(output, bodyBytes, 0644) 239 | if err != nil { 240 | fmt.Println(err) 241 | return 242 | } 243 | 244 | fmt.Println("Subtitles generated successfully!") 245 | } else { 246 | fmt.Println("Request failed with status:", res.StatusCode) 247 | } 248 | } 249 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/bitly/go-simplejson v0.5.0 h1:6IH+V8/tVMab511d5bn4M7EwGXZf9Hj6i2xSwkNEM+Y= 2 | github.com/bitly/go-simplejson v0.5.0/go.mod h1:cXHtHw4XUPsvGaxgjIAn8PhEWG9NfngEKAMDJEczWVA= 3 | github.com/chzyer/logex v1.2.0/go.mod h1:9+9sk7u7pGNWYMkh0hdiL++6OeibzJccyQU4p4MedaY= 4 | github.com/chzyer/readline v1.5.0/go.mod h1:x22KAscuvRqlLoK9CsoYsmxoXZMMFVyOl86cAH8qUic= 5 | github.com/chzyer/test v0.0.0-20210722231415-061457976a23/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU= 6 | github.com/cpuguy83/go-md2man/v2 v2.0.2/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= 7 | github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= 8 | github.com/dlclark/regexp2 v1.4.1-0.20201116162257-a2a8dda75c91/go.mod h1:2pZnwuY/m+8K6iRw6wQdMtk+rH5tNGR1i55kozfMjCc= 9 | github.com/dlclark/regexp2 v1.7.0/go.mod h1:DHkYz0B9wPfa6wondMfaivmHpzrQ3v9q8cnmRbL6yW8= 10 | github.com/dlclark/regexp2 v1.9.0 h1:pTK/l/3qYIKaRXuHnEnIf7Y5NxfRPfpb7dis6/gdlVI= 11 | github.com/dlclark/regexp2 v1.9.0/go.mod h1:DHkYz0B9wPfa6wondMfaivmHpzrQ3v9q8cnmRbL6yW8= 12 | github.com/dop251/goja v0.0.0-20211022113120-dc8c55024d06/go.mod h1:R9ET47fwRVRPZnOGvHxxhuZcbrMCuiqOz3Rlrh4KSnk= 13 | github.com/dop251/goja v0.0.0-20230402114112-623f9dda9079 h1:xkbJGxVnk5sM8/LXeTKaBOfAZrI+iqvIPyH8oK1c6CQ= 14 | github.com/dop251/goja v0.0.0-20230402114112-623f9dda9079/go.mod h1:QMWlm50DNe14hD7t24KEqZuUdC9sOTy8W6XbCU1mlw4= 15 | github.com/dop251/goja_nodejs v0.0.0-20210225215109-d91c329300e7/go.mod h1:hn7BA7c8pLvoGndExHudxTDKZ84Pyvv+90pbBjbTz0Y= 16 | github.com/dop251/goja_nodejs v0.0.0-20211022123610-8dd9abb0616d/go.mod h1:DngW8aVqWbuLRMHItjPUyqdj+HWPvnQe8V8y1nDpIbM= 17 | github.com/go-sourcemap/sourcemap v2.1.3+incompatible h1:W1iEw64niKVGogNgBN3ePyLFfuisuzeidWPMPWmECqU= 18 | github.com/go-sourcemap/sourcemap v2.1.3+incompatible/go.mod h1:F8jJfvm2KbVjc5NqelyYJmf/v5J0dwNLS2mL4sNA1Jg= 19 | github.com/google/pprof v0.0.0-20230207041349-798e818bf904/go.mod h1:uglQLonpP8qtYCYyzA+8c/9qtqgA3qsXGYqCPKARAFg= 20 | github.com/google/pprof v0.0.0-20230406165453-00490a63f317 h1:hFhpt7CTmR3DX+b4R19ydQFtofxT0Sv3QsKNMVQYTMQ= 21 | github.com/google/pprof v0.0.0-20230406165453-00490a63f317/go.mod h1:79YE0hCXdHag9sBkw2o+N/YnZtTkXi0UT9Nnixa5eYk= 22 | github.com/ianlancetaylor/demangle v0.0.0-20220319035150-800ac71e25c2/go.mod h1:aYm2/VgdVmcIU8iMfdMvDMsRAQjcfZSKFby6HOFvi/w= 23 | github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8= 24 | github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw= 25 | github.com/joho/godotenv v1.5.1 h1:7eLL/+HRGLY0ldzfGMeQkb7vMd0as4CfYvUVzLqw0N0= 26 | github.com/joho/godotenv v1.5.1/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4= 27 | github.com/kkdai/youtube/v2 v2.8.0 h1:9y+xrYAR+ed4wV6cNPq3rMvVlRV8TFco3uuEUdqRzHI= 28 | github.com/kkdai/youtube/v2 v2.8.0/go.mod h1:FMx1e/QBA+GBoRpwLAYJoJZcPE+P0yeA2ckAbPCHZow= 29 | github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= 30 | github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= 31 | github.com/kr/pretty v0.3.0/go.mod h1:640gp4NfQd8pI5XOwp5fnNeVWj67G7CFk/SaSQn7NBk= 32 | github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= 33 | github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= 34 | github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= 35 | github.com/rogpeppe/go-internal v1.6.1/go.mod h1:xXDCJY+GAPziupqXw64V24skbSoqbTEfhy4qGm1nDQc= 36 | github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= 37 | github.com/spf13/cobra v1.7.0 h1:hyqWnYt1ZQShIddO5kBpj3vu05/++x6tJ6dg8EC572I= 38 | github.com/spf13/cobra v1.7.0/go.mod h1:uLxZILRyS/50WlhOIKD7W6V5bgeIt+4sICxh6uRMrb0= 39 | github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA= 40 | github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= 41 | github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= 42 | golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= 43 | golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= 44 | golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= 45 | golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= 46 | golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= 47 | golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= 48 | golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 49 | golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 50 | golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 51 | golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 52 | golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 53 | golang.org/x/sys v0.0.0-20220310020820-b874c991c1a5/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 54 | golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 55 | golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 56 | golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= 57 | golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= 58 | golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= 59 | golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= 60 | golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= 61 | golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= 62 | golang.org/x/text v0.3.8/go.mod h1:E6s5w1FMmriuDzIBO73fBruAKo1PCIq6d2Q6DHfQ8WQ= 63 | golang.org/x/text v0.9.0 h1:2sjJmO8cDvYveuX97RDLsxlyUxLl+GHoLxBiRdHllBE= 64 | golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8= 65 | golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= 66 | golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= 67 | golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= 68 | golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 69 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 70 | gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 71 | gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= 72 | gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI= 73 | gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= 74 | gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 75 | -------------------------------------------------------------------------------- /cmd/translate.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright © 2023 NAME HERE 3 | 4 | */ 5 | package cmd 6 | 7 | import ( 8 | "bytes" 9 | "encoding/json" 10 | "fmt" 11 | "io" 12 | "io/ioutil" 13 | "log" 14 | "mime/multipart" 15 | "net/http" 16 | "os" 17 | 18 | "github.com/spf13/cobra" 19 | ) 20 | 21 | // translateCmd represents the translate command 22 | var translateCmd = &cobra.Command{ 23 | Use: "translate", 24 | Short: "Translate the subtitles in SRT format with DeepL", 25 | Long: `The 'translate' command is a crucial feature of our subtitle generator application. Once activated, this command initiates the process of translating subtitles from a provided SRT file`, 26 | Run: func(cmd *cobra.Command, args []string) { 27 | /* Variables declaration */ 28 | input, _ := cmd.Flags().GetString("input") 29 | lang, _ := cmd.Flags().GetString("lang") 30 | output, _ := cmd.Flags().GetString("output") 31 | apiKeyDeepL, _ := cmd.Flags().GetString("apiKeyDeepL") 32 | 33 | /* Displaying the values of the flags */ 34 | fmt.Println("---------------------------------------") 35 | fmt.Println("You have entered the following values:") 36 | fmt.Println("input:", input) 37 | fmt.Println("lang:", lang) 38 | fmt.Println("output:", output) 39 | fmt.Println("apiKeyDeepL:", apiKeyDeepL) 40 | fmt.Println("---------------------------------------") 41 | 42 | /* Uploading the file to DeepL */ 43 | deepLResponse := uploadFile(input, lang, apiKeyDeepL) 44 | 45 | /* Checking the status of the translation */ 46 | fmt.Println("=======================================") 47 | 48 | for { 49 | checkStatusResponse := checkStatus(deepLResponse, apiKeyDeepL) 50 | if checkStatusResponse.Status == "done" { 51 | downloadFile(deepLResponse, output, apiKeyDeepL) 52 | break 53 | } 54 | } 55 | 56 | }, 57 | } 58 | 59 | type DeepLResponse struct { 60 | DocumentID string `json:"document_id"` 61 | DocumentKey string `json:"document_key"` 62 | } 63 | 64 | type DeepLCheckStatusResponse struct { 65 | DocumentID string `json:"document_id"` 66 | Status string `json:"status"` 67 | SecondsRemaining int `json:"seconds_remaining"` 68 | } 69 | 70 | func init() { 71 | rootCmd.AddCommand(translateCmd) 72 | 73 | translateCmd.Flags().String("input", "", "Input file") 74 | translateCmd.Flags().String("lang", "", "Language (in ISO 639-1 format) to translate the subtitles to") 75 | translateCmd.Flags().String("output", "", "Output file") 76 | translateCmd.Flags().String("apiKeyDeepL", "", "DeepL API key") 77 | 78 | translateCmd.MarkFlagRequired("input") 79 | translateCmd.MarkFlagRequired("lang") 80 | translateCmd.MarkFlagRequired("output") 81 | translateCmd.MarkFlagRequired("apiKeyDeepL") 82 | } 83 | 84 | func uploadFile(input string, lang string, apiKeyDeepL string) DeepLResponse { 85 | fmt.Println("=======================================") 86 | fmt.Println("Uploading the file to DeepL...") 87 | 88 | // Create a buffer to store our request body as bytes 89 | var b bytes.Buffer 90 | w := multipart.NewWriter(&b) 91 | 92 | // Open the file 93 | f, err := os.Open(input) 94 | if err != nil { 95 | log.Fatal(err) 96 | } 97 | defer f.Close() 98 | 99 | // Add the file to the request body 100 | fw, err := w.CreateFormFile("file", f.Name()+".txt") 101 | if err != nil { 102 | log.Fatal(err) 103 | } 104 | if _, err = io.Copy(fw, f); err != nil { 105 | log.Fatal(err) 106 | } 107 | 108 | // Add our fields to the multipart writer 109 | if err = w.WriteField("target_lang", lang); err != nil { 110 | log.Fatal(err) 111 | } 112 | 113 | // Close the multipart writer 114 | if err = w.Close(); err != nil { 115 | log.Fatal(err) 116 | } 117 | 118 | // Create a new request 119 | req, _ := http.NewRequest("POST", "https://api-free.deepl.com/v2/document", &b) 120 | 121 | // Set the content type header, as well as the boundary we're going to use 122 | req.Header.Set("Content-Type", w.FormDataContentType()) 123 | 124 | // Set the authorization header (Authorization: DeepL-Auth-Key [yourAuthKey]) 125 | req.Header.Set("Authorization", "DeepL-Auth-Key "+apiKeyDeepL) 126 | 127 | // Set the user agent header (User-Agent: YourApp/1.2.3) 128 | req.Header.Set("User-Agent", "Subtitlr") 129 | 130 | // Send the request 131 | client := &http.Client{} 132 | resp, _ := client.Do(req) 133 | 134 | // Read the response body 135 | buf := new(bytes.Buffer) 136 | _, _ = buf.ReadFrom(resp.Body) 137 | 138 | // Close the response body 139 | _ = resp.Body.Close() 140 | 141 | // Parse the response body into a DeepLResponse struct 142 | var deepLResponse DeepLResponse 143 | json.Unmarshal(buf.Bytes(), &deepLResponse) 144 | 145 | // Display the document ID and document key 146 | fmt.Println("DeepL response:") 147 | fmt.Println("Document ID:", deepLResponse.DocumentID) 148 | fmt.Println("Document Key:", deepLResponse.DocumentKey) 149 | fmt.Println("=======================================") 150 | 151 | return deepLResponse 152 | } 153 | 154 | func checkStatus(deepLResponse DeepLResponse, apiKeyDeepL string) DeepLCheckStatusResponse { 155 | // Create a new request 156 | req, _ := http.NewRequest("POST", "https://api-free.deepl.com/v2/document/"+deepLResponse.DocumentID, nil) 157 | req.Header.Set("Authorization", "DeepL-Auth-Key "+apiKeyDeepL) 158 | req.Header.Set("User-Agent", "Subtitlr") 159 | 160 | // Add the document key to the request body 161 | q := req.URL.Query() 162 | q.Add("document_key", deepLResponse.DocumentKey) 163 | req.URL.RawQuery = q.Encode() 164 | 165 | // Send the request 166 | client := &http.Client{} 167 | resp, _ := client.Do(req) 168 | 169 | // Read the response body 170 | buf := new(bytes.Buffer) 171 | _, _ = buf.ReadFrom(resp.Body) 172 | 173 | // Close the response body 174 | _ = resp.Body.Close() 175 | 176 | // Parse the response body into a DeepLCheckStatusResponse struct 177 | var deepLCheckStatusResponse DeepLCheckStatusResponse 178 | json.Unmarshal(buf.Bytes(), &deepLCheckStatusResponse) 179 | 180 | fmt.Println("Seconds remaining:", deepLCheckStatusResponse.SecondsRemaining) 181 | 182 | return deepLCheckStatusResponse 183 | } 184 | 185 | func downloadFile(deepLResponse DeepLResponse, output string, apiKeyDeepL string) { 186 | fmt.Println("=======================================") 187 | fmt.Println("Downloading the translated file...") 188 | fmt.Println("Document ID:", deepLResponse.DocumentID) 189 | fmt.Println("Document Key:", deepLResponse.DocumentKey) 190 | 191 | // Create a new request 192 | req, _ := http.NewRequest("POST", "https://api-free.deepl.com/v2/document/"+deepLResponse.DocumentID+"/result", nil) 193 | req.Header.Set("Authorization", "DeepL-Auth-Key "+apiKeyDeepL) 194 | req.Header.Set("User-Agent", "Subtitlr") 195 | 196 | // Add the document key to the request body 197 | q := req.URL.Query() 198 | q.Add("document_key", deepLResponse.DocumentKey) 199 | req.URL.RawQuery = q.Encode() 200 | 201 | // Send the request 202 | client := &http.Client{} 203 | resp, _ := client.Do(req) 204 | 205 | // Read the response body 206 | buf := new(bytes.Buffer) 207 | _, _ = buf.ReadFrom(resp.Body) 208 | 209 | // Close the response body 210 | _ = resp.Body.Close() 211 | 212 | // Write the response body to the output file 213 | err := ioutil.WriteFile(output, buf.Bytes(), 0644) 214 | if err != nil { 215 | log.Fatal(err) 216 | } 217 | 218 | // Display the status of the translation 219 | fmt.Println("DeepL response:") 220 | fmt.Println("File downloaded successfully!") 221 | fmt.Println("=======================================") 222 | } 223 | --------------------------------------------------------------------------------