diff --git a/008-ChatGPT-UI/.gitignore b/008-ChatGPT-UI/.gitignore new file mode 100644 index 0000000..53eb059 --- /dev/null +++ b/008-ChatGPT-UI/.gitignore @@ -0,0 +1,129 @@ +# Byte-compiled / optimized / DLL files +__pycache__/ +*.py[cod] +*$py.class + +# C extensions +*.so + +# Distribution / packaging +.Python +build/ +develop-eggs/ +dist/ +downloads/ +eggs/ +.eggs/ +lib/ +lib64/ +parts/ +sdist/ +var/ +wheels/ +pip-wheel-metadata/ +share/python-wheels/ +*.egg-info/ +.installed.cfg +*.egg +MANIFEST + +# PyInstaller +# Usually these files are written by a python script from a template +# before PyInstaller builds the exe, so as to inject date/other infos into it. +*.manifest + +# Installer logs +pip-log.txt +pip-delete-this-directory.txt + +# Unit test / coverage reports +htmlcov/ +.tox/ +.nox/ +.coverage +.coverage.* +.cache +nosetests.xml +coverage.xml +*.cover +*.py,cover +.hypothesis/ +.pytest_cache/ + +# Translations +*.mo +*.pot + +# Django stuff: +*.log +local_settings.py +db.sqlite3 +db.sqlite3-journal + +# Flask stuff: +instance/ +.webassets-cache + +# Scrapy stuff: +.scrapy + +# Sphinx documentation +docs/_build/ + +# PyBuilder +target/ + +# Jupyter Notebook +.ipynb_checkpoints + +# IPython +profile_default/ +ipython_config.py + +# pyenv +.python-version + +# pipenv +# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. +# However, in case of collaboration, if having platform-specific dependencies or dependencies +# having no cross-platform support, pipenv may install dependencies that don't work, or not +# install all needed dependencies. +#Pipfile.lock + +# PEP 582; used by e.g. github.com/David-OConnor/pyflow +__pypackages__/ + +# Celery stuff +celerybeat-schedule +celerybeat.pid + +# SageMath parsed files +*.sage.py + +# Environments +.env +.venv +env/ +venv/ +ENV/ +env.bak/ +venv.bak/ + +# Spyder project settings +.spyderproject +.spyproject + +# Rope project settings +.ropeproject + +# mkdocs documentation +/site + +# mypy +.mypy_cache/ +.dmypy.json +dmypy.json + +# Pyre type checker +.pyre/ +.idea/ diff --git a/008-ChatGPT-UI/README.md b/008-ChatGPT-UI/README.md new file mode 100644 index 0000000..f156109 --- /dev/null +++ b/008-ChatGPT-UI/README.md @@ -0,0 +1,41 @@ +Build your ChatGPT app. + +![GPT-UI](doc/1.jpg) + + +# Run +- 1.clone this repo +- 2.edit your config.json +```json +{ + "key": "your api key", //create at https://platform.openai.com/account/api-keys + "api_base": "", + "model": "gpt-3.5-turbo", //support: gpt-3.5-turbo/gpt-4/gpt-4-32k + "stream": true, + "response": true, + "folder": "/Users/Qincji/Desktop/gptfileout/", //directory where chat logs are saved + "repeat": true, //if false: all chats in one file. if true: each chat is in a new file + "proxy": "socks5://127.0.0.1:7890" //support proxy: HTTP/HTTPS/SOCKS4A/SOCKS5 +} + +``` +- 3.run main.py + + +# Build App +- 4.install pyinstaller +- 5.cd `GPT-UI` +- 6.run `pyinstaller --windowed --name GPT-UI --add-data "config.ini:." --icon logo.ico main.py gpt.py utils.py` + + +# Feature +- [x] Support model: gpt-3.5-turbo/gpt-4/gpt-4-32k +- [x] Support for exporting chat logs to files +- [x] Support proxy: HTTP/HTTPS/SOCKS4A/SOCKS5 +- [x] Build MacOS App +- [x] Build Window exe +- [ ] Support for generating images + +# Link +- API key generated: [https://platform.openai.com/account/api-keys](https://platform.openai.com/account/api-keys) +- [https://github.com/evilpan/gptcli](https://github.com/evilpan/gptcli) diff --git a/008-ChatGPT-UI/config.ini b/008-ChatGPT-UI/config.ini new file mode 100644 index 0000000..dc1f1ba --- /dev/null +++ b/008-ChatGPT-UI/config.ini @@ -0,0 +1,10 @@ +[common] +expired_time=2025/12/15 23:59:59 + +title=GPT-UI + +version_name=v1.0.1--github/xhunmon + +version_code=1010 + +email=xhunmon@126.com \ No newline at end of file diff --git a/008-ChatGPT-UI/config.json b/008-ChatGPT-UI/config.json new file mode 100644 index 0000000..061df14 --- /dev/null +++ b/008-ChatGPT-UI/config.json @@ -0,0 +1,10 @@ +{ + "key": "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", + "api_base": "", + "model": "gpt-3.5-turbo", + "stream": true, + "response": true, + "folder": "", + "repeat": true, + "proxy": "socks5://127.0.0.1:7890" +} \ No newline at end of file diff --git a/008-ChatGPT-UI/doc/1.jpg b/008-ChatGPT-UI/doc/1.jpg new file mode 100644 index 0000000..3307c70 Binary files /dev/null and b/008-ChatGPT-UI/doc/1.jpg differ diff --git a/008-ChatGPT-UI/doc/config.json b/008-ChatGPT-UI/doc/config.json new file mode 100644 index 0000000..76b70cd --- /dev/null +++ b/008-ChatGPT-UI/doc/config.json @@ -0,0 +1,10 @@ +{ + "api_base": "", + "key": "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", + "model": "gpt-3.5-turbo", + "stream": true, + "response": true, + "folder": "/Users/Qincji/Desktop/develop/py/opengpt/gptcli/doc/", + "repeat": true, + "proxy": "socks5://127.0.0.1:7890" +} \ No newline at end of file diff --git a/008-ChatGPT-UI/doc/pyinstaller.sh b/008-ChatGPT-UI/doc/pyinstaller.sh new file mode 100644 index 0000000..6a2cf98 --- /dev/null +++ b/008-ChatGPT-UI/doc/pyinstaller.sh @@ -0,0 +1,7 @@ +#!/bin/bash + + +pyinstaller --windowed --name GPT-UI --add-data "config.ini:." --icon logo.ico main.py gpt.py utils.py + +#if use --onefile, the build file is small, but star very slow. +#pyinstaller --onefile --windowed --name GPT-UI --add-data "config.ini:." --icon logo.ico main.py gpt.py utils.py diff --git a/008-ChatGPT-UI/gpt.py b/008-ChatGPT-UI/gpt.py new file mode 100755 index 0000000..feb6212 --- /dev/null +++ b/008-ChatGPT-UI/gpt.py @@ -0,0 +1,138 @@ +#!/usr/bin/env python +# -*- encoding: utf-8 -*- +""" +@Description:gpt.py +@Date :2023/03/31 +@Author :xhunmon +@Mail :xhunmon@126.com +""" + +import time +from datetime import datetime + +from utils import * + + +class Gpt(object): + func_ui_print = None + + def __init__(self, config: Config): + self.session = [] + self.api_prompt = [] + self.update_config(config) + self.content = "" + self.is_change = False + self.is_finish = True + gpt_t = threading.Thread(target=self.start) + gpt_t.setDaemon(True) + gpt_t.start() + + def update_config(self, config: Config): + self.cfg = config + self.api_key = self.cfg.api_key + self.api_base = self.cfg.api_base + self.api_model = self.cfg.model + self.api_stream = self.cfg.stream + self.api_response = self.cfg.response + self.proxy = self.cfg.proxy + openai.api_key = self.api_key + if self.api_base: + openai.api_base = self.api_base + openai.proxy = self.proxy + + def start(self): + while True: + if self.is_finish: + while not self.is_change: + time.sleep(0.3) + self.print("\nMY:\n{}".format(self.content)) + self.print("\nGPT:\n") + self.is_change = False + self.is_finish = False + self.handle_input(self.content) + time.sleep(1) + + def print(self, content): + Gpt.func_ui_print(content) + + def query_openai_stream(self, data: dict) -> str: + messages = [] + messages.extend(self.api_prompt) + messages.extend(data) + answer = "" + try: + response = openai.ChatCompletion.create( + model=self.api_model, + messages=messages, + stream=True) + for part in response: + finish_reason = part["choices"][0]["finish_reason"] + if "content" in part["choices"][0]["delta"]: + content = part["choices"][0]["delta"]["content"] + answer += content + self.print(content) + elif finish_reason: + pass + + except KeyboardInterrupt: + self.print("Canceled") + except openai.error.OpenAIError as e: + self.print("OpenAIError:{}".format(e)) + answer = "" + return answer + + def content_change(self, content: str): + if not content: + return + if self.content != content: + self.content = content + self.is_change = True + + def handle_input(self, content: str): + if not content: + return + self.is_finish = False + self.session.append({"role": "user", "content": content}) + if self.api_stream: + answer = self.query_openai_stream(self.session) + else: + answer = self.query_openai(self.session) + if not answer: + self.session.pop() + elif self.api_response: + self.session.append({"role": "assistant", "content": answer}) + if answer: + try: + if self.cfg.folder and not os.path.exists(self.cfg.folder): + os.makedirs(self.cfg.folder) + wfile = os.path.join(self.cfg.folder, "gpt.md" if self.cfg.repeat else "gpt_{}.md".format( + datetime.now().strftime("%Y%m%d%H%M:%S"))) + if self.cfg.repeat: + with open(wfile, mode='a', encoding="utf-8") as f: + f.write("MY:\n{}\n".format(content)) + f.write("\nGPT:\n{}\n\n".format(answer)) + f.close() + else: + with open(wfile, mode='w', encoding="utf-8") as f: + f.write("MY:\n{}\n".format(content)) + f.write("\nGPT:{}".format(answer)) + f.close() + except Exception as e: + self.print("Write error: {} ".format(e)) + self.is_finish = True + + def query_openai(self, data: dict) -> str: + messages = [] + messages.extend(self.api_prompt) + messages.extend(data) + try: + response = openai.ChatCompletion.create( + model=self.api_model, + messages=messages + ) + content = response["choices"][0]["message"]["content"] + self.print(content) + return content + except openai.error.OpenAIError as e: + self.print("OpenAI error: {} ".format(e)) + return "" diff --git a/008-ChatGPT-UI/logo.ico b/008-ChatGPT-UI/logo.ico new file mode 100644 index 0000000..3457f2c Binary files /dev/null and b/008-ChatGPT-UI/logo.ico differ diff --git a/008-ChatGPT-UI/main.py b/008-ChatGPT-UI/main.py new file mode 100644 index 0000000..1f4b9d7 --- /dev/null +++ b/008-ChatGPT-UI/main.py @@ -0,0 +1,211 @@ +#!/usr/bin/env python +# -*- encoding: utf-8 -*- +""" +@Description: main +@Date :2023/03/31 +@Author :xhunmon +@Mail :xhunmon@126.com +""" +import sys +import tkinter as tk +from tkinter.filedialog import * + +from gpt import * +from utils import * + + +class EntryWithPlaceholder(tk.Entry): + def __init__(self, master=None, placeholder='', **kwargs): + super().__init__(master, **kwargs) + self.placeholder = placeholder + self.placeholder_color = 'grey' + self.default_fg_color = self['fg'] + self.bind('', self.on_focus_in) + self.bind('', self.on_focus_out) + self.put_placeholder() + + def put_placeholder(self): + self.insert(0, self.placeholder) + self['fg'] = self.placeholder_color + + def remove_placeholder(self): + cur_value = self.get() + if cur_value == self.placeholder: + self.delete(0, tk.END) + self['fg'] = self.default_fg_color + + def on_focus_in(self, event): + self.remove_placeholder() + + def on_focus_out(self, event): + if not self.get(): + self.put_placeholder() + + +class Application(tk.Frame): + def __init__(self, config: Config, master=None): + super().__init__(master) + self.cfg = config + self.gpt = None + self.repeat = False + self.master = master + self.master.title(ConfigIni.instance().get_title()) + self.pack() + self.create_widgets() + + def create_config(self): + row = tk.Frame(self) + row.pack(side=tk.TOP, fill=tk.X, padx=5, pady=5) + button = tk.Button(row, text="Config", width='7', command=self.click_config) + button.pack(side=tk.LEFT, padx=5, pady=5) + self.configEntry = EntryWithPlaceholder(row, placeholder=self.cfg.config_path, width=45) + self.configEntry.pack(side=tk.LEFT, padx=5, pady=5) + button = tk.Button(row, text="Create", width='7', command=self.click_create) + button.pack(side=tk.LEFT, padx=5, pady=5) + + def create_folder(self): + row = tk.Frame(self) + row.pack(side=tk.TOP, fill=tk.X, padx=5, pady=5) + button = tk.Button(row, text="Folder", width='7', command=self.click_folder) + button.pack(side=tk.LEFT, padx=5, pady=5) + self.folderEntry = EntryWithPlaceholder(row, + placeholder=self.cfg.folder if self.cfg.folder else f'{Config.pre_tips} chat output directory, default current', + width=50) + self.folderEntry.pack(side=tk.LEFT, padx=5, pady=5) + + def create_key(self): + row = tk.Frame(self) + row.pack(side=tk.TOP, fill=tk.X, padx=5, pady=5) + label = tk.Label(row, text=f"Key: ", width='7') + label.pack(side=tk.LEFT) + self.keyEntry = EntryWithPlaceholder(row, + placeholder=self.cfg.api_key if self.cfg.api_key else f'{Config.pre_tips} input key id', + width=50) + self.keyEntry.pack(side=tk.LEFT, padx=5, pady=5) + + def create_model(self): + row = tk.Frame(self) + row.pack(side=tk.TOP, fill=tk.X, padx=5, pady=5) + label = tk.Label(row, text=f"Model: ", width='7') + label.pack(side=tk.LEFT) + self.modelEntry = EntryWithPlaceholder(row, + placeholder=self.cfg.model if self.cfg.model else f'{Config.pre_tips} default gpt-3.5-turbo, or: gpt-4/gpt-4-32k', + width=50) + self.modelEntry.pack(side=tk.LEFT, padx=5, pady=5) + + def create_proxy(self): + row = tk.Frame(self) + row.pack(side=tk.TOP, fill=tk.X, padx=5, pady=5) + label = tk.Label(row, text=f"Proxy: ", width='7') + label.pack(side=tk.LEFT) + self.proxyEntry = EntryWithPlaceholder(row, + placeholder=self.cfg.proxy if self.cfg.proxy else f'{Config.pre_tips} default empty, or http/https/socks4a/socks5', + width=50) + self.proxyEntry.pack(side=tk.LEFT, padx=5, pady=5) + + def create_send(self): + row = tk.Frame(self) + row.pack(side=tk.TOP, fill=tk.X, padx=5, pady=5) + self.sendEntry = EntryWithPlaceholder(row, placeholder=f'{Config.pre_tips} say something, then click send.', + width=55) + self.sendEntry.pack(side=tk.LEFT, padx=5, pady=5) + self.sendEntry.bind("", self.on_return_key) + button = tk.Button(row, text="Send", width='7', command=self.click_send) + button.pack(side=tk.LEFT, padx=5, pady=5) + + def create_widgets(self): + self.create_config() + self.create_folder() + self.create_key() + self.create_model() + self.create_proxy() + self.create_send() + # bottom text + text_frame = tk.Frame(self) + text_frame.pack(side=tk.TOP, fill=tk.BOTH, expand=True, padx=5, pady=5) + self.text = tk.Text(text_frame, wrap=tk.WORD, undo=True, font=("Helvetica", 12)) + self.text.pack(side=tk.LEFT, fill=tk.BOTH, expand=True) + scroll_bar = tk.Scrollbar(text_frame, orient=tk.VERTICAL, command=self.text.yview) + scroll_bar.pack(side=tk.RIGHT, fill=tk.Y) + self.text.config(yscrollcommand=scroll_bar.set) + + # email text + email_button = tk.Button(self, text=ConfigIni.instance().get_email()) + email_button.pack(side=tk.LEFT, padx=5, pady=5) + + # version text + version_button = tk.Button(self, text=ConfigIni.instance().get_version_name()) + version_button.pack(side=tk.RIGHT, padx=5, pady=5) + + # clear text + clear_button = tk.Button(self, text="clear", width=10, command=self.clear) + clear_button.pack(side=tk.RIGHT, padx=5, pady=5) + + # copy text + copy_button = tk.Button(self, text="copy", width=10, command=self.copy) + copy_button.pack(side=tk.RIGHT, padx=5, pady=5) + + Gpt.func_ui_print = self.func_ui_print + + def refresh(self): + # self.set_entry(self.configEntry, self.cfg.default) + self.set_entry(self.folderEntry, self.cfg.folder) + self.set_entry(self.keyEntry, self.cfg.api_key) + self.set_entry(self.modelEntry, self.cfg.model) + self.set_entry(self.proxyEntry, self.cfg.proxy) + + def func_ui_print(self, txt): + self.show_text(txt) + + def click_config(self): + path = askopenfilename() + self.set_entry(self.configEntry, path) + if self.cfg.update(path): + self.refresh() + else: + self.show_text("update fail !") + + def click_create(self): + self.cfg.click_create() + self.show_text("create file :{} ".format(self.cfg.config_path)) + + def click_folder(self): + path = askdirectory() + self.set_entry(self.folderEntry, path) + + def set_entry(self, entry: tk.Entry, content): + entry.delete(0, tk.END) + entry.insert(0, content) + + def on_return_key(self, event): + self.click_send() + + def click_send(self): + # config = self.configEntry.get() + self.cfg.update_by_content(self.keyEntry.get(), self.modelEntry.get(), self.folderEntry.get(), + self.proxyEntry.get()) + content: str = self.sendEntry.get() + # self.show_text("me: {}\n".format(content)) + if not self.gpt: + self.gpt: Gpt = Gpt(self.cfg) + else: + self.gpt.update_config(self.cfg) + self.gpt.content_change(content) + + def show_text(self, content): + self.text.insert(tk.END, "{}".format(content)) + self.text.yview_moveto(1.0) # auto scroll to new + + def clear(self): + self.text.delete("1.0", "end") + + def copy(self): + self.master.clipboard_clear() + self.master.clipboard_append(self.text.get("1.0", tk.END)) + + +if __name__ == "__main__": + root = tk.Tk() + folder = os.path.dirname(os.path.realpath(sys.argv[0])) + app = Application(Config(folder), master=root) + app.mainloop() diff --git a/008-ChatGPT-UI/requirements.txt b/008-ChatGPT-UI/requirements.txt new file mode 100644 index 0000000..91d7d5c --- /dev/null +++ b/008-ChatGPT-UI/requirements.txt @@ -0,0 +1,3 @@ +openai +requests[socks] +tkinter \ No newline at end of file diff --git a/008-ChatGPT-UI/utils.py b/008-ChatGPT-UI/utils.py new file mode 100644 index 0000000..7c93c12 --- /dev/null +++ b/008-ChatGPT-UI/utils.py @@ -0,0 +1,149 @@ +#!/usr/bin/env python +# -*- encoding: utf-8 -*- +""" +@Description: tool +@Date :2023/03/31 +@Author :xhunmon +@Mail :xhunmon@126.com +""" +import configparser +import json +import os +import platform +import re +import threading + +import openai + + +def get_domain(url: str = None): + # http://youtube.com/watch + return re.match(r"(http://|https://).*?\/", url, re.DOTALL).group(0) + + +class ConfigIni(object): + _instance_lock = threading.Lock() + + def __init__(self): + parent_dir = os.path.dirname(os.path.abspath(__file__)) + conf_path = os.path.join(parent_dir, 'config.ini') + self.conf = configparser.ConfigParser() + self.conf.read(conf_path, encoding="utf-8") + + @classmethod + def instance(cls, *args, **kwargs): + with ConfigIni._instance_lock: + if not hasattr(ConfigIni, "_instance"): + ConfigIni._instance = ConfigIni(*args, **kwargs) + return ConfigIni._instance + + def get_expired_time(self): + return self.conf.get("common", "expired_time") + + def get_version_name(self): + return self.conf.get("common", "version_name") + + def get_version_code(self): + return self.conf.get("common", "version_code") + + def get_title(self): + return self.conf.get("common", "title") + + def get_email(self): + return self.conf.get("common", "email") + + +class Config: + sep = "" + pre_tips = "Tips:" + # baseDir = os.path.dirname(os.path.realpath(sys.argv[0])) + base_dir = '' + md_sep = '\n\n' + '-' * 10 + '\n' + encodings = ["utf8", "gbk"] + + api_key = "" + api_base = "" + model = "" + prompt = [] + stream = True + response = False + proxy = "" + folder = "" + config_path = "" + repeat = True + + def __init__(self, dir: str) -> None: + self.base_dir = dir + if platform.system() == 'Darwin': # MacOS:use pyinstaller pack issue. + if '/Contents/MacOS' in dir: # ./GPT-UI.app/Contents/MacOS/ --> ./ + app_path = dir.rsplit('/Contents/MacOS')[0] + self.base_dir = app_path[:app_path.rindex('/')] + self.config_path = os.path.join(self.base_dir, "config.json") + self.cfg = {} + self.load(self.config_path) + + def load(self, file): + if not os.path.exists(file): + return + with open(file, "r") as f: + self.cfg = json.load(f) + c = self.cfg + self.api_key = c.get("api_key", c.get("key", openai.api_key)) # compatible with history key + self.api_base = c.get("api_base", openai.api_base) + self.model = c.get("model", "gpt-3.5-turbo") + self.stream = c.get("stream", True) + self.response = c.get("response", False) + self.proxy = c.get("proxy", "") + self.folder = c.get("folder", self.base_dir) + self.repeat = c.get("repeat", True) + + def get(self, key, default=None): + return self.cfg.get(key, default) + + def click_create(self): + results = { + "key": "", + "api_base": "", + "model": "gpt-3.5-turbo", + "stream": True, + "response": True, + "folder": "", + "repeat": False, + "proxy": "", + "prompt": [] + } + self.write_json(results, self.config_path) + + def write_json(self, content, file_path): + path, file_name = os.path.split(file_path) + if path and not os.path.exists(path): + os.makedirs(path) + with open(file_path, 'w') as f: + json.dump(content, f, ensure_ascii=False) + f.close() + + def update(self, path: str): + if not path.endswith(".json"): + return False + if path and not os.path.exists(path): + return False + self.load(path) + return True + + def update_by_content(self, key: str = None, model: str = None, folder: str = None, proxy: str = None): + if key and len(key.strip()) > 0 and not key.startswith(Config.pre_tips): + self.api_key = key + else: + self.api_key = '' + if model and len(model.strip()) > 0 and not model.startswith(Config.pre_tips): + self.model = model + else: + self.model = 'gpt-3.5-turbo' + if folder and len(folder.strip()) > 0 and not folder.startswith(Config.pre_tips): + self.folder = folder + else: + self.folder = self.base_dir + if proxy.startswith(Config.pre_tips): + self.proxy = None + else: + self.proxy = proxy if len(proxy.strip()) > 0 else None diff --git a/README.md b/README.md index 608645f..08602b8 100644 --- a/README.md +++ b/README.md @@ -14,9 +14,11 @@ - [005-PaidSource:这些脚本你肯定会有用到的](./005-PaidSource) ——已完成 -- [006-TikTok:App自动化](./006-TikTok) ——已完成(持续更新) +- [006-TikTok:App自动化](./006-TikTok) ——已完成 -- [007-CutVideoAudio:自媒体运营之视频剪辑](./007-CutVideoAudio) ——已完成(持续更新) +- [007-CutVideoAudio:自媒体运营之视频剪辑](./007-CutVideoAudio) ——已完成 + +- [008-ChatGPT-UI:带界面的GPT,可打包成win.exe和mac.app(ChatGPT UI in window & mac with open api (gpt-3.5/gpt-4) )](./008-ChatGPT-UI) ——已完成(持续更新) ----------