├── .gitignore ├── LICENSE ├── README.md ├── file_converter ├── __init__.py └── convertly.py └── setup.py /.gitignore: -------------------------------------------------------------------------------- 1 | .Python 2 | .env 3 | build/ 4 | develop-eggs/ 5 | dist/ 6 | downloads/ 7 | eggs/ 8 | .eggs/ 9 | lib/ 10 | lib64/ 11 | parts/ 12 | sdist/ 13 | var/ 14 | *.egg-info/ 15 | .installed.cfg 16 | *.egg 17 | .DS_Store 18 | config.ini 19 | ``` -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2024 Hudhayfa Zaheem 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 | # Convertly 2 | 3 | A command line tool to execute simple file conversions, image/video manipulations and folder changes. Use with ```conv ```. Note that your outputs are being used to constantly being fine-tune a model to make it work better over time. 4 | 5 | ## Installation 6 | ```bash 7 | pip install convertly 8 | ``` 9 | ## Configuration 10 | you will be prompted to configure the tool using your own OpenAI key. 11 | 12 | ## Usage 13 | To use Convertly, you can run the command followed by your query. For example: 14 | 15 | ```bash 16 | > conv convert file.webp to file.png 17 | * dwebp file.webp -o file.png 18 | ``` 19 | View the history of your queries with: 20 | ```bash 21 | conv --history 22 | ``` 23 | Or reset your history with: 24 | ```bash 25 | conv --clear 26 | ``` 27 | ## Example Use Cases 28 | ```bash 29 | > conv a video in /path/to/video.mp4 to a gif 30 | * ffmpeg -i /path/to/video.mp4 /path/to/video.gif 31 | ``` 32 | ```bash 33 | > conv copy all of Documents/Screenshots to a folder called Screenshots2 in the same directory 34 | * cp -a Documents/Screenshots Documents/test 35 | ``` 36 | ```bash 37 | > conv rotate image.png by 90 degrees 38 | * brew install imagemagick 39 | * convert image.png -rotate 90 rotated_file.png 40 | ``` 41 | ## Support 42 | Convertly wont be perfect; if you run into any issues, please send me a message or create an issue. 43 | 44 | 45 | 46 | 47 | -------------------------------------------------------------------------------- /file_converter/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/HudZah/ConvertlyCLI/cf817eaf6d0893b810ef4e2b14be9e8f2cba1952/file_converter/__init__.py -------------------------------------------------------------------------------- /file_converter/convertly.py: -------------------------------------------------------------------------------- 1 | import argparse 2 | import subprocess 3 | import tempfile 4 | import os 5 | import configparser 6 | import platform 7 | import anthropic 8 | 9 | 10 | class ConfigManager: 11 | def __init__(self): 12 | self.config = configparser.ConfigParser() 13 | self.config_path = os.path.expanduser("~/.config/convertly/config.ini") 14 | if not os.path.exists(os.path.dirname(self.config_path)): 15 | os.makedirs(os.path.dirname(self.config_path), exist_ok=True) 16 | with open( 17 | self.config_path, "w" 18 | ): # Create the config file if it does not exist 19 | pass 20 | self.config.read(self.config_path) 21 | 22 | def get_api_key(self, key_name, section_name): 23 | api_key = os.getenv(key_name) 24 | 25 | if ( 26 | not self.config.has_section(section_name) 27 | or "API_KEY" not in self.config[section_name] 28 | ): 29 | if not api_key: 30 | api_key = input(f"Please enter your {section_name} key: ") 31 | if not self.config.has_section(section_name): 32 | self.config.add_section(section_name) 33 | self.config.set(section_name, "API_KEY", api_key) 34 | with open(self.config_path, "w") as configfile: 35 | self.config.write(configfile) 36 | else: 37 | api_key = self.config[section_name]["API_KEY"] 38 | return api_key 39 | 40 | def set_api_key(self, key_name, section_name, new_api_key): 41 | if not self.config.has_section(section_name): 42 | self.config.add_section(section_name) 43 | self.config.set(section_name, "API_KEY", new_api_key) 44 | with open(self.config_path, "w") as configfile: 45 | self.config.write(configfile) 46 | 47 | 48 | class CommandParser: 49 | def __init__(self, query, history_manager, config_manager, new_api_key=None): 50 | self.query = query 51 | self.history_manager = history_manager 52 | self.config_manager = config_manager 53 | if new_api_key: 54 | self.config_manager.set_api_key("OPENAI_API_KEY", "OPENAI", new_api_key) 55 | 56 | def get_command(self, api_key, messages): 57 | client = anthropic.Anthropic(api_key=os.getenv("ANTHROPIC_KEY")) 58 | try: 59 | message = client.messages.create( 60 | model="claude-3-5-sonnet-20240620", 61 | max_tokens=1024, 62 | temperature=0.5, 63 | system='You will provide runnable commands for file conversion tasks in the command line. It follows these guidelines:\n\n- Uses built-in Unix commands whenever possible\n- Can run multiple commands and include multiple lines if needed\n- May install libraries for intricate conversion tasks\n- When creating directories for output files, use the same path as the input file unless specified otherwise\n- Ignores format codes in file output names\n- Uses "magick" instead of "convert" or "magick convert" for ImageMagick commands\n\n\nBad outputs are:\n- Explanations or commentary on the commands\n- Anything other than the runnable command itself\n- Commands that create directories in incorrect locations or use inconsistent paths\n\n\n\n\nThis example demonstrates a more complex conversion.\n\n\n\nConvert all .tiff files in the current directory to .jpg, resize them to 800x600, and apply a sepia filter.\n\n\nfor file in *.tiff; do\n output_file="${file%.tiff}.jpg"\n magick "$file" -resize 800x600 -sepia-tone 80% "$output_file"\ndone\n\n\n\n\nThis example shows how to convert a video file to multiple formats while creating the output directory correctly.\n\n\n\nConvert /Users/username/Videos/input.mp4 to gif, mp4 (compressed), and mp3. Save the outputs in a new folder called "converted" in the same directory as the input.\n\n\ninput_dir=$(dirname "/Users/username/Videos/input.mp4")\nmkdir -p "$input_dir/converted" && \\\nffmpeg -i "/Users/username/Videos/input.mp4" -vf "fps=10,scale=320:-1:flags=lanczos" "$input_dir/converted/input.gif" && \\\nffmpeg -i "/Users/username/Videos/input.mp4" -c:v libx264 -crf 23 -preset medium -c:a aac -b:a 128k "$input_dir/converted/input_compressed.mp4" && \\\nffmpeg -i "/Users/username/Videos/input.mp4" -vn -acodec libmp3lame -b:a 128k "$input_dir/converted/input.mp3"\n\n\n', 64 | messages=messages, 65 | ) 66 | 67 | response_text = str(message.content[0].text) if message.content else "" 68 | input_tokens = message.usage.input_tokens 69 | output_tokens = message.usage.output_tokens 70 | 71 | print(f"Input tokens: {input_tokens}") 72 | print(f"Output tokens: {output_tokens}") 73 | 74 | return response_text, 200 75 | except Exception as e: 76 | return str(e), 400 77 | 78 | def parse(self): 79 | api_key = self.config_manager.get_api_key("OPENAI_API_KEY", "OPENAI") 80 | history = self.history_manager.get_recent_history(5) 81 | # history_prompt = self._generate_history_prompt(history) 82 | system_prompt = self._generate_system_prompt_claude() 83 | 84 | messages = [ 85 | { 86 | "role": "user", 87 | "content": f"Answer this using the latest context. Task: {self.query}", 88 | } 89 | ] 90 | 91 | # if history: 92 | # messages.insert( 93 | # 1, 94 | # { 95 | # "role": "user", 96 | # "content": history_prompt 97 | # + ' NOTE: If the last command produced an error you must explain the problem and the solution to that problem".', 98 | # }, 99 | # ) 100 | 101 | print(f"\033[1;33;40mRunning...\033[0m", end="\r") 102 | response, status_code = self.get_command(api_key, messages) 103 | if status_code != 200: 104 | error_message = response.get("error", "Unknown error") 105 | print( 106 | f"\033[1;31;40mError: Unable to get command, status code: {status_code}, error: {error_message}\033[0m" 107 | ) 108 | 109 | return response 110 | 111 | def _generate_history_prompt(self, history): 112 | return ( 113 | "Here's the history of the last five questions, answers and the status of their execution, if an error occured you must not use that command again. \n\n" 114 | + "\n".join(history) 115 | ) 116 | 117 | def _generate_internal_error_prompt(self): 118 | latest_history_status = self.history_manager.get_recent_history(1) 119 | status_part = "No error" 120 | if latest_history_status: 121 | status_part = latest_history_status[0].split("Status: ")[-1] 122 | 123 | return f"""If the following does not contain "No error", then "{status_part}", YOU MUST echo why the error occurred FIRST in the format echo "Error: (error)", and consider it when generating the next command, only if it's relevant. If there is no error, IGNORE this.""" 124 | 125 | def _generate_system_prompt_openai(self): 126 | 127 | return f""" 128 | You will provide runnable commands for file conversion tasks in the command line. It follows these guidelines: 129 | 130 | - Uses built-in Unix commands whenever possible 131 | - Prioritize doing conversions in as few commands as possible 132 | - May install libraries for intricate conversion tasks 133 | - Exports the final version of a file to the same location as the origin, unless explicitly asked otherwise 134 | - Ignores format codes in file output names 135 | - Outputs only the executable command, as it will be executed directly in the command line 136 | - Uses "magick" instead of "convert" or "magick convert" for ImageMagick commands 137 | 138 | Good commands for this task are: 139 | - Complete, runnable command-line scripts 140 | - Multi-step conversion processes 141 | - Scripts that may be reused or modified for similar conversion tasks 142 | 143 | Bad outputs are: 144 | - Explanations or commentary on the commands 145 | - Anything other than the runnable command itself 146 | - Including codefences, codeblocks or any ``` in your response. This is not markdown. 147 | 148 | Usage notes: 149 | - The assistant provides only the command(s) to be executed, with no additional text or explanation 150 | - Commands should be complete and ready to run in a Unix-like environment 151 | - If multiple steps are required, they should be combined into a single, executable script or command chain 152 | 153 | These examples show simple file conversions. 154 | 155 | Example 1: 156 | Convert image.jpg to image.png 157 | 158 | magick image.jpg image.png 159 | 160 | 161 | Example 2: 162 | Convert all .tiff files in the current directory to .jpg, resize them to 800x600, and apply a sepia filter. 163 | 164 | for file in *.tiff; do 165 | output_file="${{file%.tiff}}.jpg" 166 | magick "$file" -resize 800x600 -sepia-tone 80% "$output_file" 167 | done 168 | """ 169 | 170 | def _generate_system_prompt_claude(self): 171 | return f""" 172 | You will provide runnable commands for file conversion tasks in the command line. It follows these guidelines: 173 | 174 | - Uses built-in Unix commands whenever possible 175 | - Can run multiple commands and include multiple lines if needed 176 | - May install libraries for intricate conversion tasks 177 | - Exports the final version of a file to the same location as the origin, unless explicitly asked otherwise 178 | - Ignores format codes in file output names 179 | - Outputs only the executable command, as it will be executed directly in the command line 180 | - Uses "magick" instead of "convert" or "magick convert" for ImageMagick commands 181 | 182 | Good commands for this task are: 183 | - Complete, runnable command-line scripts 184 | - Using the simplest, most common and most efficient commands 185 | - Scripts that may be reused or modified for similar conversion tasks 186 | 187 | Bad outputs are: 188 | - Explanations or commentary on the commands 189 | - Anything other than the runnable command itself 190 | 191 | Usage notes: 192 | - The assistant provides only the command(s) to be executed, with no additional text or explanation 193 | - Commands should be complete and ready to run in a Unix-like environment 194 | - If multiple steps are required, they should be combined into a single, executable script or command chain 195 | 196 | 197 | 198 | This example shows a simple file conversion command provided directly in the conversation. 199 | 200 | 201 | 202 | Convert image.jpg to image.png 203 | 204 | 205 | magick image.jpg image.png 206 | 207 | 208 | 209 | 210 | This example demonstrates a more complex conversion. 211 | 212 | 213 | 214 | Convert all .tiff files in the current directory to .jpg, resize them to 800x600, and apply a sepia filter. 215 | 216 | 217 | for file in *.tiff; do 218 | output_file="${{file%.tiff}}.jpg" 219 | magick "$file" -resize 800x600 -sepia-tone 80% "$output_file" 220 | done 221 | 222 | 223 | 224 | """ 225 | 226 | 227 | class CommandExecutor: 228 | @staticmethod 229 | def execute(command): 230 | status = "" 231 | if command.startswith('echo "Error:'): 232 | print( 233 | f"\033[1;31;40mThe previous command failed: {command.split('Error: ')[-1]}\033[0m" 234 | ) 235 | status = f"An error occurred while executing the command: {command.split('Error: ')[-1]}" 236 | else: 237 | try: 238 | subprocess.run(command, check=True, shell=True, text=True) 239 | print(f"\033[1;32;40mExecuted: {command}\033[0m") 240 | # print(f"Output: {result.stdout}") 241 | status = "Success" 242 | except subprocess.CalledProcessError as e: 243 | print( 244 | f"\033[1;31;40mAn error occurred while executing the command: {e}\033[0m" 245 | ) 246 | # figure out a better way to capture relevant output and feed it back 247 | print(f"Error info: {e.stderr}") 248 | status = f"An error occurred while executing the command: {e}, Error info: {e.stderr}" 249 | return status 250 | 251 | 252 | class HistoryManager: 253 | def __init__(self, history_file_path): 254 | self.history_file_path = history_file_path 255 | 256 | def clear_history(self): 257 | with open(self.history_file_path, "w") as f: 258 | f.write("") 259 | 260 | def get_recent_history(self, n): 261 | if not os.path.exists(self.history_file_path): 262 | open(self.history_file_path, "w").close() 263 | 264 | with open(self.history_file_path, "r") as f: 265 | blocks = f.read().split("\n\n")[:-1] 266 | 267 | return blocks[-n:] 268 | 269 | def modify_history(self, query, response, status): 270 | with open(self.history_file_path, "a") as f: 271 | f.write(f"Question: {query}\nAnswer: {response}\nStatus: {status}\n\n") 272 | 273 | 274 | def main(): 275 | temp_dir = tempfile.gettempdir() 276 | history_file_path = os.path.join(temp_dir, "history.txt") 277 | history_manager = HistoryManager(history_file_path) 278 | config_manager = ConfigManager() 279 | 280 | parser = argparse.ArgumentParser( 281 | description="Conv is a command line tool to easily execute file conversions, image manipulations, and file operations quickly." 282 | ) 283 | parser.add_argument("query", type=str, nargs="*", help="The query to be processed.") 284 | parser.add_argument("--clear", action="store_true", help="Clear the history.") 285 | parser.add_argument( 286 | "--hist", action="store_true", help="View the recent history of queries." 287 | ) 288 | parser.add_argument("--key", type=str, help="Enter a new OpenAI API key.") 289 | 290 | args = parser.parse_args() 291 | 292 | if args.clear: 293 | history_manager.clear_history() 294 | print("\033[1;32;40mHistory cleared.\033[0m") 295 | return 296 | 297 | if args.hist: 298 | history = history_manager.get_recent_history(5) 299 | print("\033[1;32;40mRecent History:\033[0m") 300 | for item in history: 301 | print(item + "\n") 302 | return 303 | 304 | if args.key: 305 | new_api_key = args.key 306 | command_parser = CommandParser("", history_manager, config_manager, new_api_key) 307 | print(f"\033[1;32;40mAPI Key updated successfully to: {new_api_key}\033[0m") 308 | return 309 | 310 | if not args.query: 311 | print( 312 | "\033[1;31;40mUsage: python script.py 'conv ' or '--clear' to clear history or '--hist' to view history\033[0m" 313 | ) 314 | return 315 | 316 | query = " ".join(args.query) 317 | print("\033[1;34;40mQuerying: " + query + "\033[0m") 318 | 319 | command_parser = CommandParser(query, history_manager, config_manager, args.key) 320 | system_command = command_parser.parse() 321 | 322 | if system_command: 323 | print("\033[1;36;40mRunning command: " + system_command + "\033[0m") 324 | status = CommandExecutor.execute(system_command) 325 | history_manager.modify_history(query, system_command, status) 326 | else: 327 | print( 328 | "Could not parse or execute the command. Please ensure the command is valid." 329 | ) 330 | 331 | 332 | if __name__ == "__main__": 333 | main() 334 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | from setuptools import setup, find_packages 2 | 3 | setup( 4 | name="convertly", 5 | version="0.5.2", 6 | packages=find_packages(), 7 | entry_points={"console_scripts": ["conv=file_converter.convertly:main"]}, 8 | install_requires=["requests"], 9 | python_requires=">=3.6", 10 | author="HudZah", 11 | author_email="hudzah@hudzah.com", 12 | description="A tool to convert and manipulate media files", 13 | long_description=open("README.md").read(), 14 | long_description_content_type="text/markdown", 15 | url="https://github.com/HudZah/Convertly", 16 | license="MIT", 17 | ) 18 | --------------------------------------------------------------------------------