diff --git a/.gitignore b/.gitignore index 5b6dffe..d7e89a3 100644 --- a/.gitignore +++ b/.gitignore @@ -142,3 +142,7 @@ pyqt_openai/config.yaml .ruff_cache test + +# Related to g4f +har_and_cookies +generated_images diff --git a/MANIFEST.in b/MANIFEST.in index 987e3a9..cff8ceb 100644 --- a/MANIFEST.in +++ b/MANIFEST.in @@ -1,5 +1,4 @@ include pyqt_openai/ico/* -include pyqt_openai/prompt_res/* include pyqt_openai/lang/* include pyqt_openai/img/* diff --git a/pyproject.toml b/pyproject.toml index f64a353..212a903 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -10,6 +10,7 @@ authors = [{ name = "Jung Gyu Yoon", email = "yjg30737@gmail.com" }] license = { text = "MIT" } readme = "README.md" dependencies = [ + "qtpy", "PySide6", "pyperclip", "jinja2", @@ -20,8 +21,6 @@ dependencies = [ "filetype", "openai", - "anthropic", - "google-generativeai", "replicate", "llama-index", diff --git a/pyqt_openai/__init__.py b/pyqt_openai/__init__.py index 491db9d..010c14a 100644 --- a/pyqt_openai/__init__.py +++ b/pyqt_openai/__init__.py @@ -21,7 +21,7 @@ # For the sake of following the PEP8 standard, we will declare module-level dunder names. # PEP8 standard about dunder names: https://peps.python.org/pep-0008/#module-level-dunder-names -__version__ = "1.9.1" +__version__ = "1.9.2" __author__ = "Jung Gyu Yoon" # Constants @@ -402,12 +402,22 @@ def move_bin(filename, dst_dir): DEFAULT_TOKEN_CHUNK_SIZE = 1024 # This doesn't need endpoint -OPENAI_DEFAULT_IMAGE_MODEL = "dall-e-3" +OPENAI_IMAGE_MODELS = ["dall-e-3"] + +REPLICATE_IMAGE_MODELS = ["stability-ai/sdxl:39ed52f2a78e934b3ba6e2a89f5b1c712de7dfea535525255b1aa35c5565e08b"] DEFAULT_DATETIME_FORMAT = "%Y-%m-%d %H:%M:%S" -# This has to be managed separately since some of the arguments are different with usual models -O1_MODELS = ["o1-preview", "o1-mini"] +# https://platform.openai.com/docs/models#current-model-aliases +# This has to be managed separately since some arguments are different with usual models +GPT_MODELS = ["gpt-4o", "gpt-4o-mini", "chatgpt-4o-latest"] +REASONING_MODELS = ["o1", "o1-mini", "o3-mini"] +# TODO +REALTIME_MODELS = [ + "gpt-4o-realtime-preview", + "gpt-4o-mini-realtime-preview", + "gpt-4o-audio-preview" +] # For filtering out famous LLMs for image models FAMOUS_LLM_LIST = ["gpt", "claude", "gemini", "llama", "meta", "qwen", "falcon"] @@ -420,7 +430,7 @@ def move_bin(filename, dst_dir): "env_var_name": "OPENAI_API_KEY", "api_key": "", "manual_url": HOW_TO_GET_OPENAI_API_KEY_URL, - "model_list": ["gpt-4o", "gpt-4o-mini"] + O1_MODELS, + "model_list": GPT_MODELS + REASONING_MODELS, }, # Azure { @@ -894,6 +904,22 @@ def move_bin(filename, dst_dir): DEFAULT_LLM = "gpt-4o" +DEFAULT_IMAGE_PROVIDER_LIST = ["openai", "replicate"] + +G4F_IMAGE_GENERATION_ERROR_MESSAGE = """ +You can try the following: + +- Change the provider +- Change the model +- Use API instead of G4F +""" + +DEFAULT_IMAGE_SIZE = 1024 +MIN_IMAGE_SIZE = 512 +MAX_IMAGE_SIZE = 4096 + +G4F_IMAGE_COMBOBOX_SEPARATOR = ['----G4F----'] + G4F_PROVIDER_DEFAULT = "Auto" G4F_USE_CHAT_HISTORY = True @@ -909,10 +935,6 @@ def move_bin(filename, dst_dir): LLAMA_INDEX_DEFAULT_ALL_SUPPORTED_FORMATS_LIST = [".txt", ".docx", ".hwp", ".ipynb", ".csv", ".jpeg", ".jpg", ".mbox", ".md", ".mp3", ".mp4", ".pdf", ".png", ".ppt", ".pptx", ".pptm"] # PROMPT -## DEFAULT JSON FILENAME FOR PROMPT -AWESOME_CHATGPT_PROMPTS_FILENAME = "prompt_res/awesome_chatgpt_prompts.json" -ALEX_BROGAN_PROMPT_FILENAME = "prompt_res/alex_brogan.json" - FORM_PROMPT_GROUP_SAMPLE = json.dumps( [{"name": "Default", "data": PROPERTY_PROMPT_UNIT_DEFAULT_VALUE}], indent=INDENT_SIZE, @@ -947,12 +969,6 @@ def move_bin(filename, dst_dir): } ]""" -## Load the default prompt -if os.path.exists(AWESOME_CHATGPT_PROMPTS_FILENAME): - AWESOME_CHATGPT_PROMPTS = json.load(open(AWESOME_CHATGPT_PROMPTS_FILENAME))[0] -if os.path.exists(ALEX_BROGAN_PROMPT_FILENAME): - ALEX_BROGAN_PROMPT = json.load(open(ALEX_BROGAN_PROMPT_FILENAME))[0] - ## Data for random prompt generating feature for image generation hair_color_randomizer = [ "blonde hair", @@ -1146,6 +1162,8 @@ def move_bin(filename, dst_dir): "use_max_tokens": False, # Llama Index "use_llama_index": False, + # RAG + "use_rag": False, "llama_index_directory": "", "llama_index_supported_formats": LLAMA_INDEX_DEFAULT_SUPPORTED_FORMATS_LIST, # Customize @@ -1166,54 +1184,20 @@ def move_bin(filename, dst_dir): "auto_play_voice": TTS_DEFAULT_AUTO_PLAY, "auto_stop_silence_duration": TTS_DEFAULT_AUTO_STOP_SILENCE_DURATION, }, - "DALLE": { - "quality": "standard", - "n": 1, - "size": "1024x1024", - "style": "vivid", - "response_format": "b64_json", - "width": 1024, - "height": 1024, - "prompt_type": 1, - "show_history": True, - "show_setting": True, - "prompt": "Astronaut in a jungle, cold color palette, muted colors, detailed, 8k", - "directory": QFILEDIALOG_DEFAULT_DIRECTORY, - "is_save": True, - "continue_generation": False, - "number_of_images_to_create": 2, - "save_prompt_as_text": True, - "show_prompt_on_image": False, - }, - "REPLICATE": { - "model": "stability-ai/sdxl:39ed52f2a78e934b3ba6e2a89f5b1c712de7dfea535525255b1aa35c5565e08b", - "width": 768, - "height": 768, - "show_history": True, - "show_setting": True, - "prompt": "Astronaut in a jungle, cold color palette, muted colors, detailed, 8k", - "directory": QFILEDIALOG_DEFAULT_DIRECTORY, - "is_save": True, - "continue_generation": False, - "number_of_images_to_create": 2, - "save_prompt_as_text": True, - "show_prompt_on_image": False, - "negative_prompt": "ugly, deformed, noisy, blurry, distorted", - }, - "G4F_IMAGE": { + "IMAGE": { "model": G4F_DEFAULT_IMAGE_MODEL, - "provider": G4F_PROVIDER_DEFAULT, + "width": DEFAULT_IMAGE_SIZE, + "height": DEFAULT_IMAGE_SIZE, "show_history": True, "show_setting": True, "prompt": "Astronaut in a jungle, cold color palette, muted colors, detailed, 8k", + "negative_prompt": "ugly, deformed, noisy, blurry, distorted", "directory": QFILEDIALOG_DEFAULT_DIRECTORY, "is_save": True, "continue_generation": False, "number_of_images_to_create": 2, - "save_prompt_as_text": True, - "show_prompt_on_image": False, - "negative_prompt": "ugly, deformed, noisy, blurry, distorted", - }, + "save_prompt_as_text": True + } } diff --git a/pyqt_openai/aboutDialog.py b/pyqt_openai/aboutDialog.py index ccf3121..ea32fa7 100644 --- a/pyqt_openai/aboutDialog.py +++ b/pyqt_openai/aboutDialog.py @@ -50,7 +50,7 @@ def __initUi(self): descWidget3.setText( f"""

Contact: {CONTACT}
-

Powered by

PySide6, GPT4Free, LiteLLM,
LlamaIndex

+



qtpy, GPT4Free, LiteLLM,
LlamaIndex

""", ) diff --git a/pyqt_openai/chat_widget/center/realtimeApiWidget.py b/pyqt_openai/chat_widget/center/realtimeApiWidget.py index 18d0732..a8574a8 100644 --- a/pyqt_openai/chat_widget/center/realtimeApiWidget.py +++ b/pyqt_openai/chat_widget/center/realtimeApiWidget.py @@ -15,7 +15,7 @@ def __init__(self, parent=None): def initUI(self): lay = QVBoxLayout() - title = QLabel("Coming Soon...", self) + title = QLabel("What could it be? :)", self) title.setFont(QFont(*LARGE_LABEL_PARAM)) title.setAlignment(Qt.AlignmentFlag.AlignCenter) diff --git a/pyqt_openai/chat_widget/right_sidebar/chatRightSideBarWidget.py b/pyqt_openai/chat_widget/right_sidebar/chatRightSideBarWidget.py index 9c19e0c..527f521 100644 --- a/pyqt_openai/chat_widget/right_sidebar/chatRightSideBarWidget.py +++ b/pyqt_openai/chat_widget/right_sidebar/chatRightSideBarWidget.py @@ -43,10 +43,10 @@ def __initUi(self): tabWidget.addTab(usingAPIPage, "Using API") tabWidget.addTab(self.__llamaPage, "LlamaIndex") tabWidget.currentChanged.connect(self.__tabChanged) - tabWidget.setTabEnabled(2, self.__use_llama_index) + tabWidget.setTabEnabled(3, self.__use_llama_index) tabWidget.setCurrentIndex(self.__cur_idx) - partial_func = partial(tabWidget.setTabEnabled, 2) + partial_func = partial(tabWidget.setTabEnabled, 3) usingAPIPage.onToggleLlama.connect(lambda x: partial_func(x)) usingAPIPage.onToggleJSON.connect(self.onToggleJSON) diff --git a/pyqt_openai/chat_widget/right_sidebar/modelSearchBar.py b/pyqt_openai/chat_widget/right_sidebar/modelSearchBar.py index 1bbf0ae..3df6ea9 100644 --- a/pyqt_openai/chat_widget/right_sidebar/modelSearchBar.py +++ b/pyqt_openai/chat_widget/right_sidebar/modelSearchBar.py @@ -9,10 +9,28 @@ class ModelSearchBar(QLineEdit): def __init__(self, parent=None): super().__init__(parent) - all_models = get_chat_model() # TODO LANGAUGE self.setPlaceholderText("Start typing a model name...") + self.setChatModel() + self.textChanged.connect(self.onTextChanged) + + def setChatModel(self, all_models=None): + if all_models is None: + all_models = get_chat_model() completer = QCompleter(all_models) completer.setCaseSensitivity(Qt.CaseSensitivity.CaseInsensitive) self.setCompleter(completer) + if all_models is not None and isinstance(all_models, list) and len(all_models) > 0: + # Show the first model in the list + self.setText(all_models[0]) + + def focusInEvent(self, event): + super().focusInEvent(event) + if self.completer(): + self.completer().complete() + + def onTextChanged(self, text): + if not text: + self.completer().setCompletionPrefix("") + self.completer().complete() \ No newline at end of file diff --git a/pyqt_openai/chat_widget/right_sidebar/usingAPIPage.py b/pyqt_openai/chat_widget/right_sidebar/usingAPIPage.py index 1584008..e1872b0 100644 --- a/pyqt_openai/chat_widget/right_sidebar/usingAPIPage.py +++ b/pyqt_openai/chat_widget/right_sidebar/usingAPIPage.py @@ -1,5 +1,6 @@ from __future__ import annotations +import litellm from qtpy.QtCore import Qt, Signal from qtpy.QtGui import QFont from qtpy.QtWidgets import ( @@ -26,7 +27,7 @@ FREQUENCY_PENALTY_STEP, LLAMAINDEX_URL, MAX_TOKENS_RANGE, - O1_MODELS, + REASONING_MODELS, OPENAI_TEMPERATURE_RANGE, OPENAI_TEMPERATURE_STEP, PRESENCE_PENALTY_RANGE, @@ -42,7 +43,7 @@ getSeparator, init_llama, ) -from pyqt_openai.widgets.APIInputButton import APIInputButton +from pyqt_openai.widgets.apiInputButton import APIInputButton from pyqt_openai.widgets.linkLabel import LinkLabel @@ -72,11 +73,11 @@ def __initVal(self): self.__use_max_tokens = CONFIG_MANAGER.get_general_property("use_max_tokens") self.__use_llama_index = CONFIG_MANAGER.get_general_property("use_llama_index") + self.__use_rag = CONFIG_MANAGER.get_general_property("use_rag") self.__warningMessage = ( "Note: For models other than OpenAI and Anthropic, please enter the model name in the format [ProviderName]/[ModelName].\n" - "For more information about ProviderName and ModelName, please refer to litellm documentation.\n" - "Certain models may not support JSON Mode or LlamaIndex." + "Certain models may not support JSON Mode, LlamaIndex." ) def __initUi(self): @@ -90,13 +91,14 @@ def __initUi(self):

Using API

Description

- Fast responses.

-

- Stable response server.

+

- Stable response server. (Including Ollama)

- Ability to save your AI usage history and statistics.

- Option to add custom LLMs you have created.

- Ability to save conversation history on the server.

- JSON response functionality available (limited to specific LLMs).

- LlamaIndex can be used.

- Various hyperparameters can be assigned.

+

- RAG (Retrieval Augmented Generation) can be used.

""", ) @@ -132,8 +134,6 @@ def __initUi(self): lay.setContentsMargins(0, 0, 0, 0) setApiBtn = APIInputButton() - # TODO LANGUAGE - setApiBtn.setText("Set API Key") selectModelWidget = QWidget() selectModelWidget.setLayout(lay) @@ -274,6 +274,10 @@ def __initUi(self): ], ) + providersLink = LinkLabel() + providersLink.setText(LangClass.TRANSLATIONS["Supported Providers"]) + providersLink.setUrl("https://docs.litellm.ai/docs/providers") + # TODO LANGUAGE llamaManualLbl = LinkLabel() llamaManualLbl.setText(LangClass.TRANSLATIONS["What is LlamaIndex?"]) @@ -284,6 +288,14 @@ def __initUi(self): self.__llamaChkBox.toggled.connect(self.__use_llama_indexChecked) self.__llamaChkBox.setText(LangClass.TRANSLATIONS["Use LlamaIndex (You need OpenAI API key)"]) + self.__useRag = QCheckBox() + self.__useRag.setChecked(self.__use_rag) + self.__useRag.toggled.connect(self.__use_rag_Checked) + self.__useRag.setText(LangClass.TRANSLATIONS["Use RAG (Retrieval Augmented Generation, requires Tavily API key)"]) + self.__useRagLbl = QLabel() + self.__show_rag_warning() + self.__useRagLbl.setStyleSheet(f"color: {DEFAULT_WARNING_COLOR};") + lay = QVBoxLayout() lay.addWidget(manualBrowser) lay.addWidget(getSeparator("horizontal")) @@ -294,10 +306,13 @@ def __initUi(self): lay.addWidget(setApiBtn) lay.addWidget(selectModelWidget) lay.addWidget(self.__warningLbl) + lay.addWidget(providersLink) lay.addWidget(streamChkBox) lay.addWidget(self.__jsonChkBox) lay.addWidget(self.__llamaChkBox) lay.addWidget(llamaManualLbl) + lay.addWidget(self.__useRag) + lay.addWidget(self.__useRagLbl) lay.addWidget(getSeparator("horizontal")) lay.addWidget(advancedSettingsGrpBox) lay.setAlignment(Qt.AlignmentFlag.AlignTop) @@ -315,10 +330,11 @@ def __modelChanged(self, v): additional_message = ( "\nNote: The selected model is only available at Tier 3 or higher." ) - if self.__model in O1_MODELS: + if self.__model in REASONING_MODELS: self.__warningLbl.setText(self.__warningMessage + additional_message) else: self.__warningLbl.setText(self.__warningMessage) + self.__show_rag_warning() def __streamChecked(self, f): self.__stream = f @@ -329,6 +345,24 @@ def __jsonObjectChecked(self, f): CONFIG_MANAGER.set_general_property("json_object", f) self.onToggleJSON.emit(f) + def __show_rag_warning(self): + if litellm.supports_web_search(self.__model): + self.__useRagLbl.setText( + LangClass.TRANSLATIONS["RAG is enabled for this model."], + ) + else: + self.__useRagLbl.setText( + LangClass.TRANSLATIONS["RAG is not supported for this model."], + ) + + def __use_rag_Checked(self, f): + self.__use_rag = f + CONFIG_MANAGER.set_general_property("use_rag", f) + if f: + self.__show_rag_warning() + else: + self.__useRagLbl.setText("") + def __use_llama_indexChecked(self, f): self.__use_llama_index = f CONFIG_MANAGER.set_general_property("use_llama_index", f) diff --git a/pyqt_openai/config_loader.py b/pyqt_openai/config_loader.py index 3f52c2a..9978fe8 100644 --- a/pyqt_openai/config_loader.py +++ b/pyqt_openai/config_loader.py @@ -66,6 +66,19 @@ def init_yaml() -> None: yaml.dump(prev_yaml_data, yaml_file, default_flow_style=False) +# Use when you need to update the yaml file +def update_yaml(): + # TODO WILL_REMOVED_IN_FUTURE AFTER v2.2.0 + # 2025-02-11 by Jung Gyu Yoon + # Update related to image attributes + # Move each attribute value in REPLICATE to IMAGE + for key, value in CONFIG_DATA["REPLICATE"].items(): + CONFIG_DATA["IMAGE"][key] = value + # Remove DALLE, G4F, REPLICATE attributes, if they exist + CONFIG_DATA.pop("DALLE", None) + CONFIG_DATA.pop("G4F", None) + CONFIG_DATA.pop("REPLICATE", None) + class ConfigManager: def __init__( self, @@ -85,39 +98,19 @@ def _save_yaml(self): with open(self.yaml_file, "w") as file: yaml.safe_dump(self.config, file) - # Getter methods - def get_dalle(self) -> dict[str, str]: - return self.config.get("DALLE", {}) - def get_general(self) -> dict[str, str]: return self.config.get("General", {}) - def get_replicate(self) -> dict[str, str]: - return self.config.get("REPLICATE", {}) - - def get_g4f_image(self) -> dict[str, str]: - return self.config.get("G4F_IMAGE", {}) - - def get_dalle_property(self, key: str) -> str | None: - return self.config.get("DALLE", {}).get(key) + def get_image(self) -> dict[str, str]: + return self.config.get("IMAGE", {}) def get_general_property(self, key: str) -> str | None: value = self.config.get("General", {}).get(key) logger.info(f"Getting general property {key}: {repr(value)}") return value - def get_replicate_property(self, key: str) -> str | None: - return self.config.get("REPLICATE", {}).get(key) - - def get_g4f_image_property(self, key: str) -> str | None: - return self.config.get("G4F_IMAGE", {}).get(key) - - # Setter methods - def set_dalle_property(self, key: str, value: str) -> None: - if "DALLE" not in self.config: - self.config["DALLE"] = {} - self.config["DALLE"][key] = value - self._save_yaml() + def get_image_property(self, key: str) -> str | None: + return self.config.get("IMAGE", {}).get(key) def set_general_property(self, key: str, value: str) -> None: logger.info(f"Setting general property {key} with value: {repr(value)}") @@ -126,16 +119,10 @@ def set_general_property(self, key: str, value: str) -> None: self.config["General"][key] = value self._save_yaml() - def set_replicate_property(self, key: str, value: str) -> None: - if "REPLICATE" not in self.config: - self.config["REPLICATE"] = {} - self.config["REPLICATE"][key] = value - self._save_yaml() - - def set_g4f_image_property(self, key: str, value: str) -> None: - if "G4F_IMAGE" not in self.config: - self.config["G4F_IMAGE"] = {} - self.config["G4F_IMAGE"][key] = value + def set_image_property(self, key: str, value: str) -> None: + if "IMAGE" not in self.config: + self.config["IMAGE"] = {} + self.config["IMAGE"][key] = value self._save_yaml() diff --git a/pyqt_openai/dalle_widget/dalleHome.py b/pyqt_openai/dalle_widget/dalleHome.py deleted file mode 100644 index a54f76b..0000000 --- a/pyqt_openai/dalle_widget/dalleHome.py +++ /dev/null @@ -1,34 +0,0 @@ -from __future__ import annotations - -from qtpy.QtCore import Qt -from qtpy.QtGui import QFont -from qtpy.QtWidgets import QLabel, QScrollArea, QVBoxLayout, QWidget - -from pyqt_openai import CONTEXT_DELIMITER, LARGE_LABEL_PARAM, MEDIUM_LABEL_PARAM - - -class DallEHome(QScrollArea): - def __init__(self, parent=None): - super().__init__(parent) - self.__initUi() - - def __initUi(self): - title = QLabel("Welcome to DALL-E Page !", self) - title.setFont(QFont(*LARGE_LABEL_PARAM)) - title.setAlignment(Qt.AlignmentFlag.AlignCenter) - - description = QLabel("Generate images with DALL-E." + CONTEXT_DELIMITER) - - description.setFont(QFont(*MEDIUM_LABEL_PARAM)) - description.setAlignment(Qt.AlignmentFlag.AlignCenter) - - lay = QVBoxLayout() - lay.addWidget(title) - lay.addWidget(description) - lay.setAlignment(Qt.AlignmentFlag.AlignCenter) - self.setLayout(lay) - - mainWidget = QWidget() - mainWidget.setLayout(lay) - self.setWidget(mainWidget) - self.setWidgetResizable(True) diff --git a/pyqt_openai/dalle_widget/dalleMainWidget.py b/pyqt_openai/dalle_widget/dalleMainWidget.py deleted file mode 100644 index 327a550..0000000 --- a/pyqt_openai/dalle_widget/dalleMainWidget.py +++ /dev/null @@ -1,28 +0,0 @@ -from __future__ import annotations - -from pyqt_openai.config_loader import CONFIG_MANAGER -from pyqt_openai.dalle_widget.dalleHome import DallEHome -from pyqt_openai.dalle_widget.dalleRightSideBar import DallERightSideBarWidget -from pyqt_openai.widgets.imageMainWidget import ImageMainWidget - - -class DallEMainWidget(ImageMainWidget): - def __init__(self, parent=None): - super().__init__(parent) - self.__initUi() - - def __initUi(self): - self._homePage = DallEHome() - self._rightSideBarWidget = DallERightSideBarWidget() - - self._setHomeWidget(self._homePage) - self._setRightSideBarWidget(self._rightSideBarWidget) - self._completeUi() - - def toggleHistory(self, f): - super().toggleHistory(f) - CONFIG_MANAGER.set_dalle_property("show_history", f) - - def toggleSetting(self, f): - super().toggleSetting(f) - CONFIG_MANAGER.set_dalle_property("show_setting", f) diff --git a/pyqt_openai/dalle_widget/dalleRightSideBar.py b/pyqt_openai/dalle_widget/dalleRightSideBar.py deleted file mode 100644 index 0f47674..0000000 --- a/pyqt_openai/dalle_widget/dalleRightSideBar.py +++ /dev/null @@ -1,221 +0,0 @@ -from __future__ import annotations - -from typing import TYPE_CHECKING, Any - -from qtpy.QtWidgets import QComboBox, QFormLayout, QGroupBox, QLabel, QPlainTextEdit, QRadioButton, QSpinBox, QVBoxLayout - -from pyqt_openai import OPENAI_DEFAULT_IMAGE_MODEL -from pyqt_openai.config_loader import CONFIG_MANAGER -from pyqt_openai.dalle_widget.dalleThread import DallEThread -from pyqt_openai.lang.translations import LangClass -from pyqt_openai.widgets.APIInputButton import APIInputButton -from pyqt_openai.widgets.imageControlWidget import ImageControlWidget - -if TYPE_CHECKING: - from qtpy.QtWidgets import QWidget - - -class DallERightSideBarWidget(ImageControlWidget): - def __init__(self, parent: QWidget | None = None): - super().__init__(parent) - self._initVal() - self._initUi() - - def _initVal(self): - super()._initVal() - - self._prompt: str = CONFIG_MANAGER.get_dalle_property("prompt") or "" - self._continue_generation: bool = bool(CONFIG_MANAGER.get_dalle_property("continue_generation")) - self._save_prompt_as_text: bool = bool(CONFIG_MANAGER.get_dalle_property("save_prompt_as_text")) - self._is_save: bool = bool(CONFIG_MANAGER.get_dalle_property("is_save")) - self._directory: str = CONFIG_MANAGER.get_dalle_property("directory") or "" - self._number_of_images_to_create: int = int(CONFIG_MANAGER.get_dalle_property("number_of_images_to_create") or 1) - - self.__quality: str = CONFIG_MANAGER.get_dalle_property("quality") or "standard" - self.__n: int = int(CONFIG_MANAGER.get_dalle_property("n") or 1) - self.__size: str = CONFIG_MANAGER.get_dalle_property("size") or "1024x1024" - self.__style: str = CONFIG_MANAGER.get_dalle_property("style") or "vivid" - self.__response_format: str = CONFIG_MANAGER.get_dalle_property("response_format") or "url" - self.__prompt_type: int = int(CONFIG_MANAGER.get_dalle_property("prompt_type") or 1) - self.__width: int = int(CONFIG_MANAGER.get_dalle_property("width") or 1024) - self.__height: int = int(CONFIG_MANAGER.get_dalle_property("height") or 1024) - - def _initUi(self): - super()._initUi() - - # TODO LANGUAGE - self.__setApiBtn = APIInputButton() - self.__setApiBtn.setText("Set API Key") - - self.__promptTypeToShowRadioGrpBox = QGroupBox( - LangClass.TRANSLATIONS["Prompt Type To Show"], - ) - - self.__normalOne = QRadioButton(LangClass.TRANSLATIONS["Normal"]) - self.__revisedOne = QRadioButton(LangClass.TRANSLATIONS["Revised"]) - - if self.__prompt_type == 1: - self.__normalOne.setChecked(True) - else: - self.__revisedOne.setChecked(True) - - self.__normalOne.toggled.connect(self.__promptTypeToggled) - self.__revisedOne.toggled.connect(self.__promptTypeToggled) - - lay = QVBoxLayout() - lay.addWidget(self.__normalOne) - lay.addWidget(self.__revisedOne) - self.__promptTypeToShowRadioGrpBox.setLayout(lay) - - lay = QVBoxLayout() - lay.addWidget(self.__setApiBtn) - lay.addWidget(self._findPathWidget) - lay.addWidget(self._saveChkBox) - lay.addWidget(self._continueGenerationChkBox) - lay.addWidget(self._numberOfImagesToCreateSpinBox) - lay.addWidget(self._savePromptAsTextChkBox) - lay.addWidget(self.__promptTypeToShowRadioGrpBox) - self._generalGrpBox.setLayout(lay) - - self.__qualityCmbBox = QComboBox() - self.__qualityCmbBox.addItems(["standard", "hd"]) - self.__qualityCmbBox.setCurrentText(self.__quality or "standard") - self.__qualityCmbBox.currentTextChanged.connect(self.__dalleChanged) - - self.__nSpinBox = QSpinBox() - self.__nSpinBox.setRange(1, 10) - self.__nSpinBox.setValue(int(self.__n or 1)) - self.__nSpinBox.valueChanged.connect(self.__dalleChanged) - self.__nSpinBox.setEnabled(False) - - self.__sizeLimitLabel = QLabel( - LangClass.TRANSLATIONS[ - "โ€ป Images can have a size of 1024x1024, 1024x1792 or 1792x1024 pixels." - ], - ) - self.__sizeLimitLabel.setWordWrap(True) - - self.__widthCmbBox = QComboBox() - self.__widthCmbBox.addItems(["1024", "1792"]) - self.__widthCmbBox.setCurrentText(str(self.__width)) - self.__widthCmbBox.currentTextChanged.connect(self.__dalleChanged) - - self.__heightCmbBox = QComboBox() - self.__heightCmbBox.addItems(["1024", "1792"]) - self.__heightCmbBox.setCurrentText(str(self.__height)) - self.__heightCmbBox.currentTextChanged.connect(self.__dalleChanged) - - self._promptTextEdit.textChanged.connect(self.__dalleTextChanged) - - self.__styleCmbBox = QComboBox() - self.__styleCmbBox.addItems(["vivid", "natural"]) - self.__styleCmbBox.currentTextChanged.connect(self.__dalleChanged) - - lay = QFormLayout() - lay.addRow(LangClass.TRANSLATIONS["Quality"], self.__qualityCmbBox) - lay.addRow(LangClass.TRANSLATIONS["Total"], self.__nSpinBox) - lay.addRow(self.__sizeLimitLabel) - lay.addRow(LangClass.TRANSLATIONS["Width"], self.__widthCmbBox) - lay.addRow(LangClass.TRANSLATIONS["Height"], self.__heightCmbBox) - lay.addRow(LangClass.TRANSLATIONS["Style"], self.__styleCmbBox) - - lay.addRow(self._randomImagePromptGeneratorWidget) - lay.addRow(QLabel(LangClass.TRANSLATIONS["Prompt"])) - lay.addRow(self._promptTextEdit) - - self._paramGrpBox.setLayout(lay) - - self._completeUi() - - def __dalleChanged(self, v): - sender = self.sender() - if sender == self.__qualityCmbBox: - self.__quality = v - CONFIG_MANAGER.set_dalle_property("quality", self.__quality) - elif sender == self.__nSpinBox: - self.__n = v - CONFIG_MANAGER.set_dalle_property("n", self.__n) - elif sender == self.__widthCmbBox: - if ( - self.__widthCmbBox.currentText() == "1792" - and self.__heightCmbBox.currentText() == "1792" - ): - self.__heightCmbBox.setCurrentText("1024") - self.__width = v - CONFIG_MANAGER.set_dalle_property("width", self.__width) - elif sender == self.__heightCmbBox: - if ( - self.__widthCmbBox.currentText() == "1792" - and self.__heightCmbBox.currentText() == "1792" - ): - self.__widthCmbBox.setCurrentText("1024") - self.__height = v - CONFIG_MANAGER.set_dalle_property("height", self.__height) - elif sender == self.__styleCmbBox: - self.__style = v - CONFIG_MANAGER.set_dalle_property("style", self.__style) - - # TODO combine __dalleTextChanged and __replicateTextChanged and rename them to __promptTextChanged - def __dalleTextChanged(self): - sender = self.sender() - if isinstance(sender, QPlainTextEdit): - if sender == self._promptTextEdit: - self._prompt = sender.toPlainText() - CONFIG_MANAGER.set_dalle_property("prompt", self._prompt) - - def _setSaveDirectory(self, directory: str): - super()._setSaveDirectory(directory) - CONFIG_MANAGER.set_dalle_property("directory", directory) - - def _saveChkBoxToggled(self, f: bool): - super()._saveChkBoxToggled(f) - CONFIG_MANAGER.set_dalle_property("is_save",f) - - def _continueGenerationChkBoxToggled(self, f: bool): - super()._continueGenerationChkBoxToggled(f) - CONFIG_MANAGER.set_dalle_property("continue_generation",f) - - def _savePromptAsTextChkBoxToggled(self, f: bool): - super()._savePromptAsTextChkBoxToggled(f) - CONFIG_MANAGER.set_dalle_property("save_prompt_as_text",f) - - def _numberOfImagesToCreateSpinBoxValueChanged(self, value: int): - super()._numberOfImagesToCreateSpinBoxValueChanged(value) - CONFIG_MANAGER.set_dalle_property("number_of_images_to_create",value) - - def __promptTypeToggled(self, f: bool): - sender = self.sender() - # Prompt type to show on the image - # 1 is normal, 2 is revised - if sender == self.__normalOne: - self.__prompt_type = 1 - CONFIG_MANAGER.set_dalle_property("prompt_type", self.__prompt_type) - elif sender == self.__revisedOne: - self.__prompt_type = 2 - CONFIG_MANAGER.set_dalle_property("prompt_type", self.__prompt_type) - - def _submit(self): - arg = self.getArgument() - number_of_images = ( - self._number_of_images_to_create if self._continue_generation else 1 - ) - random_prompt = ( - self._randomImagePromptGeneratorWidget.getRandomPromptSourceArr() - ) - - t = DallEThread(arg, number_of_images, random_prompt) - self._setThread(t) - super()._submit() - - def getArgument(self) -> dict[str, Any]: - obj = super().getArgument() - return { - **obj, - "model": OPENAI_DEFAULT_IMAGE_MODEL, - "prompt": self._promptTextEdit.toPlainText(), - "n": self.__n, - "size": f"{self.__width}x{self.__height}", - "quality": self.__quality, - "style": self.__style, - "response_format": self.__response_format, - } diff --git a/pyqt_openai/dalle_widget/dalleThread.py b/pyqt_openai/dalle_widget/dalleThread.py deleted file mode 100644 index ac6a30c..0000000 --- a/pyqt_openai/dalle_widget/dalleThread.py +++ /dev/null @@ -1,50 +0,0 @@ -from __future__ import annotations - -import base64 - -from qtpy.QtCore import QThread, Signal - -from pyqt_openai.globals import OPENAI_CLIENT -from pyqt_openai.models import ImagePromptContainer -from pyqt_openai.util.common import generate_random_prompt - - -class DallEThread(QThread): - replyGenerated = Signal(ImagePromptContainer) - errorGenerated = Signal(str) - allReplyGenerated = Signal() - - def __init__( - self, input_args, number_of_images, randomizing_prompt_source_arr=None, - ): - super().__init__() - self.__input_args = input_args - self.__stop = False - - self.__randomizing_prompt_source_arr = randomizing_prompt_source_arr - self.__number_of_images = number_of_images - - def stop(self): - self.__stop = True - - def run(self): - try: - for _ in range(self.__number_of_images): - if self.__stop: - break - if self.__randomizing_prompt_source_arr is not None: - self.__input_args["prompt"] = generate_random_prompt( - self.__randomizing_prompt_source_arr, - ) - response = OPENAI_CLIENT.images.generate(**self.__input_args) - container = ImagePromptContainer(**self.__input_args) - for _ in response.data: - image_data = base64.b64decode(_.b64_json) - container.data = image_data - container.revised_prompt = _.revised_prompt - container.width = self.__input_args["size"].split("x")[0] - container.height = self.__input_args["size"].split("x")[1] - self.replyGenerated.emit(container) - self.allReplyGenerated.emit() - except Exception as e: - self.errorGenerated.emit(str(e)) diff --git a/pyqt_openai/g4f_image_widget/g4fImageMainWidget.py b/pyqt_openai/g4f_image_widget/g4fImageMainWidget.py deleted file mode 100644 index 24c05d4..0000000 --- a/pyqt_openai/g4f_image_widget/g4fImageMainWidget.py +++ /dev/null @@ -1,28 +0,0 @@ -from __future__ import annotations - -from pyqt_openai.config_loader import CONFIG_MANAGER -from pyqt_openai.g4f_image_widget.g4fImageHome import G4FImageHome -from pyqt_openai.g4f_image_widget.g4fImageRightSideBar import G4FImageRightSideBarWidget -from pyqt_openai.widgets.imageMainWidget import ImageMainWidget - - -class G4FImageMainWidget(ImageMainWidget): - def __init__(self, parent=None): - super().__init__(parent) - self.__initUi() - - def __initUi(self): - self._homePage = G4FImageHome() - self._rightSideBarWidget = G4FImageRightSideBarWidget() - - self._setHomeWidget(self._homePage) - self._setRightSideBarWidget(self._rightSideBarWidget) - self._completeUi() - - def toggleHistory(self, f): - super().toggleHistory(f) - CONFIG_MANAGER.set_g4f_image_property("show_history", f) - - def toggleSetting(self, f): - super().toggleSetting(f) - CONFIG_MANAGER.set_g4f_image_property("show_setting", f) diff --git a/pyqt_openai/g4f_image_widget/g4fImageRightSideBar.py b/pyqt_openai/g4f_image_widget/g4fImageRightSideBar.py deleted file mode 100644 index 2abd253..0000000 --- a/pyqt_openai/g4f_image_widget/g4fImageRightSideBar.py +++ /dev/null @@ -1,188 +0,0 @@ -from __future__ import annotations - -from qtpy.QtCore import Qt -from qtpy.QtWidgets import ( - QComboBox, - QFormLayout, - QLabel, - QPlainTextEdit, - QSplitter, - QVBoxLayout, - QWidget, -) - -from pyqt_openai.config_loader import CONFIG_MANAGER -from pyqt_openai.g4f_image_widget.g4fImageThread import G4FImageThread -from pyqt_openai.lang.translations import LangClass -from pyqt_openai.util.common import ( - get_g4f_image_providers, - get_g4f_image_models_from_provider, - get_g4f_image_models, -) -from pyqt_openai.widgets.imageControlWidget import ImageControlWidget - - -class G4FImageRightSideBarWidget(ImageControlWidget): - def __init__(self, parent=None): - super().__init__(parent) - self._initVal() - self._initUi() - - def _initVal(self): - super()._initVal() - - self._prompt = CONFIG_MANAGER.get_g4f_image_property("prompt") - self._continue_generation = CONFIG_MANAGER.get_g4f_image_property( - "continue_generation", - ) - self._save_prompt_as_text = CONFIG_MANAGER.get_g4f_image_property( - "save_prompt_as_text", - ) - self._is_save = CONFIG_MANAGER.get_g4f_image_property("is_save") - self._directory = CONFIG_MANAGER.get_g4f_image_property("directory") - self._number_of_images_to_create = CONFIG_MANAGER.get_g4f_image_property( - "number_of_images_to_create", - ) - - self.__model = CONFIG_MANAGER.get_g4f_image_property("model") - self.__provider = CONFIG_MANAGER.get_g4f_image_property("provider") - self.__negative_prompt = CONFIG_MANAGER.get_g4f_image_property( - "negative_prompt", - ) - - def _initUi(self): - super()._initUi() - - self.__providerCmbBox = QComboBox() - g4f_image_providers = get_g4f_image_providers(including_auto=True) - self.__providerCmbBox.addItems(g4f_image_providers) - self.__providerCmbBox.currentTextChanged.connect(self.__g4fProviderChanged) - - self.__modelCmbBox = QComboBox() - g4f_image_models = get_g4f_image_models() - self.__modelCmbBox.addItems(g4f_image_models) - self.__modelCmbBox.setCurrentText(self.__model) - self.__modelCmbBox.currentTextChanged.connect(self.__g4fModelChanged) - - self.__providerCmbBox.setCurrentText(self.__provider) - - lay = QVBoxLayout() - lay.addWidget(self._findPathWidget) - lay.addWidget(self._saveChkBox) - lay.addWidget(self._continueGenerationChkBox) - lay.addWidget(self._numberOfImagesToCreateSpinBox) - lay.addWidget(self._savePromptAsTextChkBox) - self._generalGrpBox.setLayout(lay) - - self._promptTextEdit.textChanged.connect(self.__replicateTextChanged) - - self._negativeTextEdit = QPlainTextEdit() - self._negativeTextEdit.setPlaceholderText( - "ugly, deformed, noisy, blurry, distorted", - ) - self._negativeTextEdit.setPlainText(self.__negative_prompt) - self._negativeTextEdit.textChanged.connect(self.__replicateTextChanged) - - lay = QVBoxLayout() - - lay.addWidget(self._randomImagePromptGeneratorWidget) - lay.addWidget(QLabel(LangClass.TRANSLATIONS["Prompt"])) - lay.addWidget(self._promptTextEdit) - - lay.addWidget(QLabel(LangClass.TRANSLATIONS["Negative Prompt"])) - lay.addWidget(self._negativeTextEdit) - promptWidget = QWidget() - promptWidget.setLayout(lay) - - lay = QFormLayout() - lay.addRow(LangClass.TRANSLATIONS["Provider"], self.__providerCmbBox) - lay.addRow(LangClass.TRANSLATIONS["Model"], self.__modelCmbBox) - otherParamWidget = QWidget() - otherParamWidget.setLayout(lay) - - splitter = QSplitter() - splitter.addWidget(otherParamWidget) - splitter.addWidget(promptWidget) - splitter.setHandleWidth(1) - splitter.setOrientation(Qt.Orientation.Vertical) - splitter.setChildrenCollapsible(False) - splitter.setSizes([500, 500]) - splitter.setStyleSheet("QSplitterHandle {background-color: lightgray;}") - - lay = QVBoxLayout() - lay.addWidget(splitter) - self._paramGrpBox.setLayout(lay) - - self._completeUi() - - def __g4fModelChanged(self, text): - self.__model = text - CONFIG_MANAGER.set_g4f_image_property("model", self.__model) - # - # g4f_image_providers = get_g4f_providers_by_model(self.__model, including_auto=True) - # self.__providerCmbBox.clear() - # self.__providerCmbBox.addItems(g4f_image_providers) - - def __g4fProviderChanged(self, text): - self.__provider = text - CONFIG_MANAGER.set_g4f_image_property("provider", self.__provider) - - image_models = get_g4f_image_models_from_provider(self.__provider) - self.__modelCmbBox.clear() - self.__modelCmbBox.addItems(image_models) - - def __replicateTextChanged(self): - sender = self.sender() - if isinstance(sender, QPlainTextEdit): - if sender == self._promptTextEdit: - self._prompt = sender.toPlainText() - CONFIG_MANAGER.set_g4f_image_property("prompt", self._prompt) - elif sender == self._negativeTextEdit: - self.__negative_prompt = sender.toPlainText() - CONFIG_MANAGER.set_g4f_image_property( - "negative_prompt", self.__negative_prompt, - ) - - def _setSaveDirectory(self, directory): - super()._setSaveDirectory(directory) - CONFIG_MANAGER.set_g4f_image_property("directory", directory) - - def _saveChkBoxToggled(self, f): - super()._saveChkBoxToggled(f) - CONFIG_MANAGER.set_g4f_image_property("is_save", f) - - def _continueGenerationChkBoxToggled(self, f): - super()._continueGenerationChkBoxToggled(f) - CONFIG_MANAGER.set_g4f_image_property("continue_generation", f) - - def _savePromptAsTextChkBoxToggled(self, f): - super()._savePromptAsTextChkBoxToggled(f) - CONFIG_MANAGER.set_g4f_image_property("save_prompt_as_text", f) - - def _numberOfImagesToCreateSpinBoxValueChanged(self, value): - super()._numberOfImagesToCreateSpinBoxValueChanged(value) - CONFIG_MANAGER.set_g4f_image_property("number_of_images_to_create", value) - - def _submit(self): - arg = self.getArgument() - number_of_images = ( - self._number_of_images_to_create if self._continue_generation else 1 - ) - random_prompt = ( - self._randomImagePromptGeneratorWidget.getRandomPromptSourceArr() - ) - - t = G4FImageThread(arg, number_of_images, random_prompt) - self._setThread(t) - super()._submit() - - def getArgument(self): - obj = super().getArgument() - return { - **obj, - "model": self.__modelCmbBox.currentText(), - "provider": self.__providerCmbBox.currentText(), - "response_format": "url", - "prompt": self._promptTextEdit.toPlainText(), - "negative_prompt": self._negativeTextEdit.toPlainText(), - } \ No newline at end of file diff --git a/pyqt_openai/g4f_image_widget/g4fImageThread.py b/pyqt_openai/g4f_image_widget/g4fImageThread.py deleted file mode 100644 index eaba3cf..0000000 --- a/pyqt_openai/g4f_image_widget/g4fImageThread.py +++ /dev/null @@ -1,55 +0,0 @@ -from __future__ import annotations - -from pyqt_openai import G4F_PROVIDER_DEFAULT -from pyqt_openai.globals import G4F_CLIENT -from pyqt_openai.models import ImagePromptContainer -from pyqt_openai.util.common import generate_random_prompt -from pyqt_openai.util.replicate import download_image_as_base64 -from qtpy.QtCore import QThread, Signal - - -class G4FImageThread(QThread): - replyGenerated = Signal(ImagePromptContainer) - errorGenerated = Signal(str) - allReplyGenerated = Signal() - - def __init__( - self, input_args, number_of_images, randomizing_prompt_source_arr=None - ): - super().__init__() - self.__input_args = input_args - self.__stop = False - - self.__randomizing_prompt_source_arr = randomizing_prompt_source_arr - - self.__number_of_images = number_of_images - - def stop(self): - self.__stop = True - - def run(self): - # try: - if self.__input_args["provider"] == G4F_PROVIDER_DEFAULT: - del self.__input_args["provider"] - - for _ in range(self.__number_of_images): - if self.__stop: - break - if self.__randomizing_prompt_source_arr is not None: - self.__input_args["prompt"] = generate_random_prompt( - self.__randomizing_prompt_source_arr - ) - response = G4F_CLIENT.images.generate( - **self.__input_args - ) - arg = { - **self.__input_args, - "provider": response.provider, - "data": download_image_as_base64(response.data[0].url), - } - - result = ImagePromptContainer(**arg) - self.replyGenerated.emit(result) - self.allReplyGenerated.emit() -# except Exception as e: -# self.errorGenerated.emit(str(e)) \ No newline at end of file diff --git a/pyqt_openai/globals.py b/pyqt_openai/globals.py index afe2fb2..a40135e 100644 --- a/pyqt_openai/globals.py +++ b/pyqt_openai/globals.py @@ -14,7 +14,8 @@ G4F_CLIENT = Client() -# For Whisper +# For Image Generation and TTS & STT OPENAI_CLIENT = OpenAI(api_key="") -REPLICATE_CLIENT = ReplicateWrapper(api_key="") +# For Image Generation +REPLICATE_CLIENT = ReplicateWrapper(api_key="") \ No newline at end of file diff --git a/pyqt_openai/image_widget/imageControlWidget.py b/pyqt_openai/image_widget/imageControlWidget.py new file mode 100644 index 0000000..66c0631 --- /dev/null +++ b/pyqt_openai/image_widget/imageControlWidget.py @@ -0,0 +1,383 @@ +from __future__ import annotations + +from typing import cast + +from qtpy.QtCore import Signal, Qt +from qtpy.QtWidgets import QCheckBox, QGroupBox, QMessageBox, QPlainTextEdit, QPushButton, QScrollArea, QSpinBox, \ + QVBoxLayout, QWidget, QLabel, QFormLayout, QSplitter, QComboBox, QApplication, QMainWindow + +from pyqt_openai import MIN_IMAGE_SIZE, MAX_IMAGE_SIZE +from pyqt_openai.chat_widget.right_sidebar.modelSearchBar import ModelSearchBar +from pyqt_openai.config_loader import CONFIG_MANAGER +from pyqt_openai.lang.translations import LangClass +from pyqt_openai.models import ImagePromptContainer +from pyqt_openai.util.common import getSeparator, get_image_providers, get_g4f_image_models, \ + get_g4f_image_models_from_provider, ImageThread +from pyqt_openai.widgets.apiInputButton import APIInputButton +from pyqt_openai.widgets.findPathWidget import FindPathWidget +from pyqt_openai.widgets.notifier import NotifierWidget +from pyqt_openai.widgets.randomImagePromptGeneratorWidget import RandomImagePromptGeneratorWidget + + +class ImageControlWidget(QScrollArea): + submit = Signal(ImagePromptContainer) + submitAllComplete = Signal() + + def __init__( + self, + parent: QWidget | None = None, + ): + super().__init__(parent) + self._initVal() + self._initUi() + + def _initVal(self): + self._threads = [] + + self._prompt = CONFIG_MANAGER.get_image_property("prompt") + self._continue_generation = bool(CONFIG_MANAGER.get_image_property( + "continue_generation", + )) + self._save_prompt_as_text = bool(CONFIG_MANAGER.get_image_property( + "save_prompt_as_text", + )) + self._is_save = bool(CONFIG_MANAGER.get_image_property("is_save")) + self._directory = CONFIG_MANAGER.get_image_property("directory") + self._number_of_images_to_create = CONFIG_MANAGER.get_image_property( + "number_of_images_to_create", + ) + + self.__model = CONFIG_MANAGER.get_image_property("model") + self.__provider = CONFIG_MANAGER.get_image_property("provider") + self.__negative_prompt = CONFIG_MANAGER.get_image_property( + "negative_prompt", + ) + self.__width: int = int(CONFIG_MANAGER.get_image_property("width") or 1024) + self.__height: int = int(CONFIG_MANAGER.get_image_property("height") or 1024) + + def _initUi(self): + self._findPathWidget: FindPathWidget = FindPathWidget() + self._findPathWidget.setAsDirectory(True) + self._findPathWidget.getLineEdit().setPlaceholderText(LangClass.TRANSLATIONS["Choose Directory to Save..."]) + self._findPathWidget.getLineEdit().setText(cast(str, self._directory)) + self._findPathWidget.added.connect(self._setSaveDirectory) + + self._saveChkBox: QCheckBox = QCheckBox(LangClass.TRANSLATIONS["Save After Submit"]) + self._saveChkBox.setChecked(True) + self._saveChkBox.toggled.connect(self._saveChkBoxToggled) + self._saveChkBox.setChecked(self._is_save) + + self._numberOfImagesToCreateSpinBox: QSpinBox = QSpinBox() + self._numberOfImagesToCreateSpinBox.setRange(2, 1000) + self._numberOfImagesToCreateSpinBox.setValue(self._number_of_images_to_create) + self._numberOfImagesToCreateSpinBox.valueChanged.connect(self._numberOfImagesToCreateSpinBoxValueChanged) + + self._continueGenerationChkBox: QCheckBox = QCheckBox(LangClass.TRANSLATIONS["Continue Image Generation"]) + self._continueGenerationChkBox.setChecked(True) + self._continueGenerationChkBox.toggled.connect(self._continueGenerationChkBoxToggled) + self._continueGenerationChkBox.setChecked(self._continue_generation) + + self._savePromptAsTextChkBox: QCheckBox = QCheckBox(LangClass.TRANSLATIONS["Save Prompt as Text"]) + self._savePromptAsTextChkBox.setChecked(True) + self._savePromptAsTextChkBox.toggled.connect(self._savePromptAsTextChkBoxToggled) + self._savePromptAsTextChkBox.setChecked(self._save_prompt_as_text) + self._savePromptAsTextChkBox.setEnabled(self._save_prompt_as_text) + + self.__setApiBtn = APIInputButton() + self.__setApiBtn.setText("Set API Key") + + self._generalGrpBox: QGroupBox = QGroupBox() + self._generalGrpBox.setTitle(LangClass.TRANSLATIONS["General"]) + + self.__widthSpinBox = QSpinBox() + self.__widthSpinBox.setRange(MIN_IMAGE_SIZE, MAX_IMAGE_SIZE) + self.__widthSpinBox.setSingleStep(8) + self.__widthSpinBox.setValue(self.__width) + self.__widthSpinBox.valueChanged.connect(self.__widthHeightChanged) + + self.__heightSpinBox = QSpinBox() + self.__heightSpinBox.setRange(MIN_IMAGE_SIZE, MAX_IMAGE_SIZE) + self.__heightSpinBox.setSingleStep(8) + self.__heightSpinBox.setValue(self.__height) + self.__heightSpinBox.valueChanged.connect(self.__widthHeightChanged) + + self._promptTextEdit: QPlainTextEdit = QPlainTextEdit() + self._promptTextEdit.setPlaceholderText(LangClass.TRANSLATIONS["Enter prompt here..."]) + self._promptTextEdit.setPlainText(self._prompt) + + self._randomImagePromptGeneratorWidget: RandomImagePromptGeneratorWidget = RandomImagePromptGeneratorWidget() + + self._paramGrpBox: QGroupBox = QGroupBox() + self._paramGrpBox.setTitle(LangClass.TRANSLATIONS["Parameters"]) + + self._submitBtn: QPushButton = QPushButton(LangClass.TRANSLATIONS["Submit"]) + self._submitBtn.clicked.connect(self._submit) + + self._stopGeneratingImageBtn: QPushButton = QPushButton(LangClass.TRANSLATIONS["Stop Generating Image"]) + self._stopGeneratingImageBtn.clicked.connect(self._stopGeneratingImage) + self._stopGeneratingImageBtn.setEnabled(False) + + self._stopGeneratingImageBtn: QPushButton = QPushButton(LangClass.TRANSLATIONS["Stop Generating Image"]) + self._stopGeneratingImageBtn.clicked.connect(self._stopGeneratingImage) + self._stopGeneratingImageBtn.setEnabled(False) + + sep = getSeparator("horizontal") + + lay = QVBoxLayout() + lay.addWidget(self._generalGrpBox) + lay.addWidget(self._paramGrpBox) + lay.addWidget(sep) + lay.addWidget(self._submitBtn) + lay.addWidget(self._stopGeneratingImageBtn) + + mainWidget = QWidget() + mainWidget.setLayout(lay) + + self.setWidget(mainWidget) + self.setWidgetResizable(True) + + # Compose the detailed configuration of the Control Widget + self.__providerCmbBox = QComboBox() + g4f_image_providers = get_image_providers(including_auto=True) + self.__providerCmbBox.addItems(g4f_image_providers) + self.__providerCmbBox.currentTextChanged.connect(self.__providerChanged) + + self.__modelCmbBox = ModelSearchBar() + g4f_image_models = get_g4f_image_models() + self.__modelCmbBox.setChatModel(g4f_image_models) + self.__modelCmbBox.setText(self.__model) + self.__modelCmbBox.textChanged.connect(self.__modelChanged) + + self.__providerCmbBox.setCurrentText(self.__provider) + + lay = QVBoxLayout() + lay.addWidget(self._findPathWidget) + lay.addWidget(self._saveChkBox) + lay.addWidget(self._continueGenerationChkBox) + lay.addWidget(self._numberOfImagesToCreateSpinBox) + lay.addWidget(self._savePromptAsTextChkBox) + lay.addWidget(self.__setApiBtn) + self._generalGrpBox.setLayout(lay) + + self._promptTextEdit.textChanged.connect(self.__promptChanged) + + self._negativeTextEdit = QPlainTextEdit() + self._negativeTextEdit.setPlaceholderText( + "ugly, deformed, noisy, blurry, distorted", + ) + self._negativeTextEdit.setPlainText(self.__negative_prompt) + self._negativeTextEdit.textChanged.connect(self.__promptChanged) + + lay = QVBoxLayout() + + lay.addWidget(self._randomImagePromptGeneratorWidget) + lay.addWidget(QLabel(LangClass.TRANSLATIONS["Prompt"])) + lay.addWidget(self._promptTextEdit) + + lay.addWidget(QLabel(LangClass.TRANSLATIONS["Negative Prompt"])) + lay.addWidget(self._negativeTextEdit) + promptWidget = QWidget() + promptWidget.setLayout(lay) + + lay = QFormLayout() + lay.addRow(LangClass.TRANSLATIONS["Provider"], self.__providerCmbBox) + lay.addRow(LangClass.TRANSLATIONS["Model"], self.__modelCmbBox) + lay.addRow(LangClass.TRANSLATIONS["Width"], self.__widthSpinBox) + lay.addRow(LangClass.TRANSLATIONS["Height"], self.__heightSpinBox) + otherParamWidget = QWidget() + otherParamWidget.setLayout(lay) + + splitter = QSplitter() + splitter.addWidget(otherParamWidget) + splitter.addWidget(promptWidget) + splitter.setHandleWidth(1) + splitter.setOrientation(Qt.Orientation.Vertical) + splitter.setChildrenCollapsible(False) + splitter.setSizes([500, 500]) + splitter.setStyleSheet("QSplitterHandle {background-color: lightgray;}") + + lay = QVBoxLayout() + lay.addWidget(splitter) + self._paramGrpBox.setLayout(lay) + + self._completeUi() + + def _toggleWidget(self): + assert self._t is not None + f = not self._t.isRunning() + continue_generation = self._continue_generation + # self._generalGrpBox.setEnabled(f) + # self._submitBtn.setEnabled(f) + if continue_generation: + self._stopGeneratingImageBtn.setEnabled(not f) + + def _stopGeneratingImage(self): + assert self._t is not None + if self._t.isRunning(): + self._t.stop() + + def _failToGenerate( + self, + event: str, + ): + informative_text: str = "Error ๐Ÿ˜ฅ" + detailed_text: str = event + + window_of_self: QWidget | None = self.window() + assert window_of_self is not None + if window_of_self is None or not self.isVisible() or not window_of_self.isActiveWindow(): + self._notifierWidget: NotifierWidget = NotifierWidget( + informative_text=informative_text, + detailed_text=detailed_text, + ) + self._notifierWidget.show() + self._notifierWidget.doubleClicked.connect(self._bringWindowToFront) + else: + QMessageBox.critical( + None, # pyright: ignore[reportArgumentType] + informative_text, + detailed_text, + QMessageBox.StandardButton.Ok, + QMessageBox.StandardButton.Cancel, + ) + + def _bringWindowToFront(self): + window: QWidget | None = self.window() + if window is None: + return + window.showNormal() + window.raise_() + window.activateWindow() + + def _afterGenerated( + self, + result: object, + ): + self.submit.emit(result) + + def _setSaveDirectory( + self, + directory: str, + ): + self._directory = directory + CONFIG_MANAGER.set_image_property("directory", directory) + + def _saveChkBoxToggled( + self, + f: bool, + ): + self._is_save = f + CONFIG_MANAGER.set_image_property("is_save", f) + + def _continueGenerationChkBoxToggled( + self, + f: bool, + ): + self._continue_generation = f + self._numberOfImagesToCreateSpinBox.setEnabled(f) + CONFIG_MANAGER.set_image_property("continue_generation", f) + + def _savePromptAsTextChkBoxToggled( + self, + f: bool, + ): + self._save_prompt_as_text = f + CONFIG_MANAGER.set_image_property("save_prompt_as_text", f) + + def _numberOfImagesToCreateSpinBoxValueChanged( + self, + value: int, + ): + self._number_of_images_to_create = value + CONFIG_MANAGER.set_image_property("number_of_images_to_create", value) + + def getSavePromptAsText(self) -> bool: + return self._save_prompt_as_text + + def isSavedEnabled(self) -> bool: + return self._is_save + + def getDirectory(self) -> str: + return self._directory + + def _completeUi(self): + """Complete the UI setup after all widgets have been initialized.""" + mainWidget = QWidget() + lay = QVBoxLayout() + lay.addWidget(self._generalGrpBox) + lay.addWidget(self._paramGrpBox) + lay.addWidget(getSeparator("horizontal")) + lay.addWidget(self._submitBtn) + lay.addWidget(self._stopGeneratingImageBtn) + mainWidget.setLayout(lay) + self.setWidget(mainWidget) + self.setWidgetResizable(True) + + def __modelChanged(self, text): + self.__model = text + CONFIG_MANAGER.set_image_property("model", self.__model) + + def __providerChanged(self, text): + self.__provider = text + CONFIG_MANAGER.set_image_property("provider", self.__provider) + + image_models = get_g4f_image_models_from_provider(self.__provider) + self.__modelCmbBox.clear() + self.__modelCmbBox.setChatModel(image_models) + + def __widthHeightChanged(self): + sender = self.sender() + if isinstance(sender, QSpinBox): + if sender == self.__widthSpinBox: + self.__width = sender.value() + CONFIG_MANAGER.set_image_property("width", self.__width) + elif sender == self.__heightSpinBox: + self.__height = sender.value() + CONFIG_MANAGER.set_image_property("height", self.__height) + + def __promptChanged(self): + sender = self.sender() + if isinstance(sender, QPlainTextEdit): + if sender == self._promptTextEdit: + self._prompt = sender.toPlainText() + CONFIG_MANAGER.set_image_property("prompt", self._prompt) + elif sender == self._negativeTextEdit: + self.__negative_prompt = sender.toPlainText() + CONFIG_MANAGER.set_image_property( + "negative_prompt", self.__negative_prompt, + ) + + def _submit(self): + arg = self.getArgument() + number_of_images = ( + self._number_of_images_to_create if self._continue_generation else 1 + ) + random_prompt = ( + self._randomImagePromptGeneratorWidget.getRandomPromptSourceArr() + ) + + t = ImageThread(arg, number_of_images, random_prompt) + self._threads.append(t) + + t.start() + + t.replyGenerated.connect(self._afterGenerated) + t.errorGenerated.connect(self._failToGenerate) + t.finished.connect(lambda: self._cleanupThread(t)) + + def _cleanupThread(self, thread): + if thread in self._threads: + self._threads.remove(thread) + thread.deleteLater() + if len(self._threads) == 0: + self.submitAllComplete.emit() + + def getArgument(self): + return { + "prompt": self._promptTextEdit.toPlainText(), + "model": self.__modelCmbBox.text(), + "provider": self.__providerCmbBox.currentText(), + "negative_prompt": self._negativeTextEdit.toPlainText(), + "width": self.__width, + "height": self.__height, + } \ No newline at end of file diff --git a/pyqt_openai/g4f_image_widget/g4fImageHome.py b/pyqt_openai/image_widget/imageHome.py similarity index 94% rename from pyqt_openai/g4f_image_widget/g4fImageHome.py rename to pyqt_openai/image_widget/imageHome.py index 75d7287..82c2dea 100644 --- a/pyqt_openai/g4f_image_widget/g4fImageHome.py +++ b/pyqt_openai/image_widget/imageHome.py @@ -1,39 +1,39 @@ -from __future__ import annotations - -from qtpy.QtCore import Qt -from qtpy.QtGui import QFont -from qtpy.QtWidgets import QLabel, QScrollArea, QVBoxLayout, QWidget - -from pyqt_openai import CONTEXT_DELIMITER, LARGE_LABEL_PARAM, MEDIUM_LABEL_PARAM - - -class G4FImageHome(QScrollArea): - def __init__(self, parent=None): - super().__init__(parent) - self.__initUi() - - def __initUi(self): - # TODO LANGUAGE - title = QLabel("Welcome to GPT4Free\n" + "Image Generation Page !", self) - title.setFont(QFont(*LARGE_LABEL_PARAM)) - title.setAlignment(Qt.AlignmentFlag.AlignCenter) - - description = QLabel( - "Generate images for free with the power of G4F." + CONTEXT_DELIMITER, - ) - - description.setFont(QFont(*MEDIUM_LABEL_PARAM)) - description.setAlignment(Qt.AlignmentFlag.AlignCenter) - - # TODO v2.x.0 "how does this work?" or "What is GPT4Free?" link (maybe) - - lay = QVBoxLayout() - lay.addWidget(title) - lay.addWidget(description) - lay.setAlignment(Qt.AlignmentFlag.AlignCenter) - self.setLayout(lay) - - mainWidget = QWidget() - mainWidget.setLayout(lay) - self.setWidget(mainWidget) - self.setWidgetResizable(True) +from __future__ import annotations + +from qtpy.QtCore import Qt +from qtpy.QtGui import QFont +from qtpy.QtWidgets import QLabel, QScrollArea, QVBoxLayout, QWidget + +from pyqt_openai import CONTEXT_DELIMITER, LARGE_LABEL_PARAM, MEDIUM_LABEL_PARAM + + +class ImageHome(QScrollArea): + def __init__(self, parent=None): + super().__init__(parent) + self.__initUi() + + def __initUi(self): + # TODO LANGUAGE + title = QLabel("Welcome to GPT4Free\n" + "Image Generation Page !", self) + title.setFont(QFont(*LARGE_LABEL_PARAM)) + title.setAlignment(Qt.AlignmentFlag.AlignCenter) + + description = QLabel( + "Generate images for free with the power of G4F." + CONTEXT_DELIMITER, + ) + + description.setFont(QFont(*MEDIUM_LABEL_PARAM)) + description.setAlignment(Qt.AlignmentFlag.AlignCenter) + + # TODO v2.x.0 "how does this work?" or "What is GPT4Free?" link (maybe) + + lay = QVBoxLayout() + lay.addWidget(title) + lay.addWidget(description) + lay.setAlignment(Qt.AlignmentFlag.AlignCenter) + self.setLayout(lay) + + mainWidget = QWidget() + mainWidget.setLayout(lay) + self.setWidget(mainWidget) + self.setWidgetResizable(True) diff --git a/pyqt_openai/widgets/imageMainWidget.py b/pyqt_openai/image_widget/imageMainWidget.py similarity index 92% rename from pyqt_openai/widgets/imageMainWidget.py rename to pyqt_openai/image_widget/imageMainWidget.py index 965625b..39bb1df 100644 --- a/pyqt_openai/widgets/imageMainWidget.py +++ b/pyqt_openai/image_widget/imageMainWidget.py @@ -1,221 +1,230 @@ -from __future__ import annotations - -import os - -from typing import TYPE_CHECKING - -from qtpy.QtCore import Qt -from qtpy.QtWidgets import QHBoxLayout, QSplitter, QStackedWidget, QVBoxLayout, QWidget - -from pyqt_openai import DEFAULT_SHORTCUT_LEFT_SIDEBAR_WINDOW, DEFAULT_SHORTCUT_RIGHT_SIDEBAR_WINDOW, ICON_HISTORY, ICON_SETTING -from pyqt_openai.config_loader import CONFIG_MANAGER -from pyqt_openai.globals import DB -from pyqt_openai.lang.translations import LangClass -from pyqt_openai.models import ImagePromptContainer -from pyqt_openai.util.common import getSeparator, get_image_filename_for_saving, get_image_prompt_filename_for_saving, open_directory -from pyqt_openai.widgets.button import Button -from pyqt_openai.widgets.imageNavWidget import ImageNavWidget -from pyqt_openai.widgets.notifier import NotifierWidget -from pyqt_openai.widgets.thumbnailView import ThumbnailView - -if TYPE_CHECKING: - from qtpy.QtGui import QShowEvent - - -class ImageMainWidget(QWidget): - def __init__( - self, - parent: QWidget | None = None, - ): - super().__init__(parent) - self.__initVal() - self.__initUi() - - def __initVal(self): - # ini - self._show_history: str | None = CONFIG_MANAGER.get_dalle_property("show_history") - self._show_setting: str | None = CONFIG_MANAGER.get_dalle_property("show_setting") - - def __initUi(self): - self._imageNavWidget: ImageNavWidget = ImageNavWidget(ImagePromptContainer.get_keys(), "image_tb") - - # Main widget - # This contains home page (at the beginning of the stack) and - # widget for main view - self._centralWidget: QStackedWidget = QStackedWidget() - - self._viewWidget: ThumbnailView = ThumbnailView() - - self._imageNavWidget.getContent.connect(lambda x: self._updateCenterWidget(1, x)) - - self._historyBtn: Button = Button() - self._historyBtn.setStyleAndIcon(ICON_HISTORY) - self._historyBtn.setCheckable(True) - self._historyBtn.setToolTip(LangClass.TRANSLATIONS["History"] + f" ({DEFAULT_SHORTCUT_LEFT_SIDEBAR_WINDOW})") - self._historyBtn.setChecked(self._show_history) - self._historyBtn.toggled.connect(self.toggleHistory) - self._historyBtn.setShortcut(DEFAULT_SHORTCUT_LEFT_SIDEBAR_WINDOW) - - self._settingBtn: Button = Button() - self._settingBtn.setStyleAndIcon(ICON_SETTING) - self._settingBtn.setCheckable(True) - self._settingBtn.setToolTip(LangClass.TRANSLATIONS["Settings"] + f" ({DEFAULT_SHORTCUT_RIGHT_SIDEBAR_WINDOW})") - self._settingBtn.setChecked(self._show_setting) - self._settingBtn.toggled.connect(self.toggleSetting) - self._settingBtn.setShortcut(DEFAULT_SHORTCUT_RIGHT_SIDEBAR_WINDOW) - - lay = QHBoxLayout() - lay.addWidget(self._historyBtn) - lay.addWidget(self._settingBtn) - lay.setContentsMargins(2, 2, 2, 2) - lay.setAlignment(Qt.AlignmentFlag.AlignLeft) - - self._menuWidget: QWidget = QWidget() - self._menuWidget.setLayout(lay) - self._menuWidget.setMaximumHeight(self._menuWidget.sizeHint().height()) - - self._rightSideBarWidget: QWidget = QWidget() - - self._mainWidget: QSplitter = QSplitter() - self._mainWidget.addWidget(self._imageNavWidget) - self._mainWidget.addWidget(self._centralWidget) - - def _setHomeWidget(self, home_page: QWidget): - self._homePage: QWidget = home_page - self._centralWidget.addWidget(self._homePage) - self._centralWidget.addWidget(self._viewWidget) - - def _setRightSideBarWidget(self, right_side_bar_widget: QWidget): - self._rightSideBarWidget: QWidget = right_side_bar_widget - self._rightSideBarWidget.submit.connect(self._setResult) - self._rightSideBarWidget.submitAllComplete.connect(self._imageGenerationAllComplete) - - def _completeUi(self): - self._mainWidget.addWidget(self._rightSideBarWidget) - self._mainWidget.setSizes([200, 500, 300]) - self._mainWidget.setChildrenCollapsible(False) - self._mainWidget.setHandleWidth(2) - self._mainWidget.setStyleSheet( - """ - QSplitter::handle:horizontal - { - background: #CCC; - height: 1px; - } - """, - ) - - sep = getSeparator("horizontal") - - lay = QVBoxLayout() - lay.addWidget(self._menuWidget) - lay.addWidget(sep) - lay.addWidget(self._mainWidget) - lay.setContentsMargins(0, 0, 0, 0) - lay.setSpacing(0) - self.setLayout(lay) - - # Put this below to prevent the widgets pop up when app is opened - self._imageNavWidget.setVisible(self._show_history) - self._rightSideBarWidget.setVisible(self._show_setting) - - def _updateCenterWidget( - self, - idx: int, - data: bytes | None = None, - ): - """0 is home page, 1 is the main view - :param idx: index - :param data: data (bytes). - """ - # Set the current index - self._centralWidget.setCurrentIndex(idx) - - # If the index is 1, set the content - if idx == 1 and data is not None: - self._viewWidget.setContent(data) - - def showSecondaryToolBar( - self, - f: bool, - ): - self._menuWidget.setVisible(f) - CONFIG_MANAGER.set_general_property("show_secondary_toolbar", f) - - def toggleButtons( - self, - x: bool, - ): - self._historyBtn.setChecked(x) - self._settingBtn.setChecked(x) - - def setAIEnabled( - self, - f: bool, - ): - self._rightSideBarWidget.setEnabled(f) - - def _setResult( - self, - result: ImagePromptContainer, - ): - self._updateCenterWidget(1, result.data) - # save - if self._rightSideBarWidget.isSavedEnabled(): - self._saveResultImage(result) - DB.insertImage(result) - self._imageNavWidget.refresh() - - def _saveResultImage( - self, - result: ImagePromptContainer, - ): - directory: str = self._rightSideBarWidget.getDirectory() - os.makedirs(directory, exist_ok=True) - filename: str = os.path.join(directory, get_image_filename_for_saving(result)) - with open(filename, "wb") as f: - f.write(result.data) - - if self._rightSideBarWidget.getSavePromptAsText(): - txt_filename = get_image_prompt_filename_for_saving(directory, filename) - with open(txt_filename, "w") as f: - f.write(result.prompt) - - def _imageGenerationAllComplete(self): - window: QWidget | None = self.window() - assert window is not None - if not self.isVisible() and not window.isActiveWindow(): - if CONFIG_MANAGER.get_general_property("notify_finish"): - self.__notifierWidget: NotifierWidget = NotifierWidget( - informative_text=LangClass.TRANSLATIONS["Response ๐Ÿ‘Œ"], - detailed_text=LangClass.TRANSLATIONS["Image Generation complete."], - ) - self.__notifierWidget.show() - self.__notifierWidget.doubleClicked.connect(self._bringWindowToFront) - - open_directory(self._rightSideBarWidget.getDirectory()) - - def _bringWindowToFront(self): - window: QWidget | None = self.window() - assert window is not None - window.showNormal() - window.raise_() - window.activateWindow() - - def showEvent(self, event: QShowEvent ): - self._imageNavWidget.refresh() - super().showEvent(event) - - def setColumns( - self, - columns: list[str], - ): - self._imageNavWidget.setColumns(columns) - - def toggleHistory(self, f): - self._imageNavWidget.setVisible(f) - self._show_history = f - - def toggleSetting(self, f): - self._rightSideBarWidget.setVisible(f) - self._show_setting = f +from __future__ import annotations + +import os + +from typing import TYPE_CHECKING + +from qtpy.QtCore import Qt +from qtpy.QtWidgets import QHBoxLayout, QSplitter, QStackedWidget, QVBoxLayout, QWidget + +from pyqt_openai import DEFAULT_SHORTCUT_LEFT_SIDEBAR_WINDOW, DEFAULT_SHORTCUT_RIGHT_SIDEBAR_WINDOW, ICON_HISTORY, ICON_SETTING +from pyqt_openai.config_loader import CONFIG_MANAGER +from pyqt_openai.globals import DB +from pyqt_openai.image_widget.imageControlWidget import ImageControlWidget +from pyqt_openai.lang.translations import LangClass +from pyqt_openai.models import ImagePromptContainer +from pyqt_openai.util.common import getSeparator, get_image_filename_for_saving, get_image_prompt_filename_for_saving, open_directory +from pyqt_openai.widgets.button import Button +from pyqt_openai.image_widget.imageHome import ImageHome +from pyqt_openai.image_widget.imageNavWidget import ImageNavWidget +from pyqt_openai.widgets.notifier import NotifierWidget +from pyqt_openai.widgets.thumbnailView import ThumbnailView + +if TYPE_CHECKING: + from qtpy.QtGui import QShowEvent + + +class ImageMainWidget(QWidget): + def __init__( + self, + parent: QWidget | None = None, + ): + super().__init__(parent) + self.__initVal() + self.__initUi() + + def __initVal(self): + # ini + self._show_history: bool | True = bool(CONFIG_MANAGER.get_image_property("show_history")) + self._show_setting: bool | True = bool(CONFIG_MANAGER.get_image_property("show_setting")) + + def __initUi(self): + self._imageNavWidget: ImageNavWidget = ImageNavWidget(ImagePromptContainer.get_keys(), "image_tb") + + # Main widget + # This contains home page (at the beginning of the stack) and + # widget for main view + self._centralWidget: QStackedWidget = QStackedWidget() + + self._viewWidget: ThumbnailView = ThumbnailView() + + self._imageNavWidget.getContent.connect(lambda x: self._updateCenterWidget(1, x)) + + self._historyBtn: Button = Button() + self._historyBtn.setStyleAndIcon(ICON_HISTORY) + self._historyBtn.setCheckable(True) + self._historyBtn.setToolTip(LangClass.TRANSLATIONS["History"] + f" ({DEFAULT_SHORTCUT_LEFT_SIDEBAR_WINDOW})") + self._historyBtn.setChecked(self._show_history) + self._historyBtn.toggled.connect(self.toggleHistory) + self._historyBtn.setShortcut(DEFAULT_SHORTCUT_LEFT_SIDEBAR_WINDOW) + + self._settingBtn: Button = Button() + self._settingBtn.setStyleAndIcon(ICON_SETTING) + self._settingBtn.setCheckable(True) + self._settingBtn.setToolTip(LangClass.TRANSLATIONS["Settings"] + f" ({DEFAULT_SHORTCUT_RIGHT_SIDEBAR_WINDOW})") + self._settingBtn.setChecked(self._show_setting) + self._settingBtn.toggled.connect(self.toggleSetting) + self._settingBtn.setShortcut(DEFAULT_SHORTCUT_RIGHT_SIDEBAR_WINDOW) + + lay = QHBoxLayout() + lay.addWidget(self._historyBtn) + lay.addWidget(self._settingBtn) + lay.setContentsMargins(2, 2, 2, 2) + lay.setAlignment(Qt.AlignmentFlag.AlignLeft) + + self._menuWidget: QWidget = QWidget() + self._menuWidget.setLayout(lay) + self._menuWidget.setMaximumHeight(self._menuWidget.sizeHint().height()) + + self._rightSideBarWidget = ImageControlWidget() + + self._mainWidget: QSplitter = QSplitter() + self._mainWidget.addWidget(self._imageNavWidget) + self._mainWidget.addWidget(self._centralWidget) + self._mainWidget.addWidget(self._rightSideBarWidget) + + self._completeUi() + + def _setHomeWidget(self, home_page: QWidget): + self._homePage: QWidget = home_page + self._centralWidget.addWidget(self._homePage) + self._centralWidget.addWidget(self._viewWidget) + + def _setRightSideBarWidget(self, right_side_bar_widget: QWidget): + self._rightSideBarWidget: QWidget = right_side_bar_widget + self._rightSideBarWidget.submit.connect(self._setResult) + self._rightSideBarWidget.submitAllComplete.connect(self._imageGenerationAllComplete) + + def _completeUi(self): + self._mainWidget.setSizes([200, 500, 300]) + self._mainWidget.setChildrenCollapsible(False) + self._mainWidget.setHandleWidth(2) + self._mainWidget.setStyleSheet( + """ + QSplitter::handle:horizontal + { + background: #CCC; + height: 1px; + } + """, + ) + + sep = getSeparator("horizontal") + + lay = QVBoxLayout() + lay.addWidget(self._menuWidget) + lay.addWidget(sep) + lay.addWidget(self._mainWidget) + lay.setContentsMargins(0, 0, 0, 0) + lay.setSpacing(0) + self.setLayout(lay) + + # Put this below to prevent the widgets pop up when app is opened + self._imageNavWidget.setVisible(self._show_history) + self._rightSideBarWidget.setVisible(self._show_setting) + + self._homePage = ImageHome() + + self._setHomeWidget(self._homePage) + self._setRightSideBarWidget(self._rightSideBarWidget) + + def _updateCenterWidget( + self, + idx: int, + data: bytes | None = None, + ): + """0 is home page, 1 is the main view + :param idx: index + :param data: data (bytes). + """ + # Set the current index + self._centralWidget.setCurrentIndex(idx) + + # If the index is 1, set the content + if idx == 1 and data is not None: + self._viewWidget.setContent(data) + + def showSecondaryToolBar( + self, + f: bool, + ): + self._menuWidget.setVisible(f) + CONFIG_MANAGER.set_general_property("show_secondary_toolbar", f) + + def toggleButtons( + self, + x: bool, + ): + self._historyBtn.setChecked(x) + self._settingBtn.setChecked(x) + + def setAIEnabled( + self, + f: bool, + ): + self._rightSideBarWidget.setEnabled(f) + + def _setResult( + self, + result: ImagePromptContainer, + ): + self._updateCenterWidget(1, result.data) + # save + if self._rightSideBarWidget.isSavedEnabled(): + self._saveResultImage(result) + DB.insertImage(result) + self._imageNavWidget.refresh() + + def _saveResultImage( + self, + result: ImagePromptContainer, + ): + directory: str = self._rightSideBarWidget.getDirectory() + os.makedirs(directory, exist_ok=True) + filename: str = os.path.join(directory, get_image_filename_for_saving(result)) + with open(filename, "wb") as f: + f.write(result.data) + + if self._rightSideBarWidget.getSavePromptAsText(): + txt_filename = get_image_prompt_filename_for_saving(directory, filename) + with open(txt_filename, "w") as f: + f.write(result.prompt) + + def _imageGenerationAllComplete(self): + window: QWidget | None = self.window() + assert window is not None + if not self.isVisible() and not window.isActiveWindow(): + if CONFIG_MANAGER.get_general_property("notify_finish"): + self.__notifierWidget: NotifierWidget = NotifierWidget( + informative_text=LangClass.TRANSLATIONS["Response ๐Ÿ‘Œ"], + detailed_text=LangClass.TRANSLATIONS["Image Generation complete."], + ) + self.__notifierWidget.show() + self.__notifierWidget.doubleClicked.connect(self._bringWindowToFront) + + open_directory(self._rightSideBarWidget.getDirectory()) + + def _bringWindowToFront(self): + window: QWidget | None = self.window() + assert window is not None + window.showNormal() + window.raise_() + window.activateWindow() + + def showEvent(self, event: QShowEvent ): + self._imageNavWidget.refresh() + super().showEvent(event) + + def setColumns( + self, + columns: list[str], + ): + self._imageNavWidget.setColumns(columns) + + def toggleHistory(self, f): + self._imageNavWidget.setVisible(f) + self._show_history = f + + def toggleSetting(self, f): + self._rightSideBarWidget.setVisible(f) + self._show_setting = f diff --git a/pyqt_openai/widgets/imageNavWidget.py b/pyqt_openai/image_widget/imageNavWidget.py similarity index 96% rename from pyqt_openai/widgets/imageNavWidget.py rename to pyqt_openai/image_widget/imageNavWidget.py index f2aa853..de3f808 100644 --- a/pyqt_openai/widgets/imageNavWidget.py +++ b/pyqt_openai/image_widget/imageNavWidget.py @@ -1,164 +1,164 @@ -from __future__ import annotations - -from typing import TYPE_CHECKING - -from qtpy.QtCore import QByteArray, QSortFilterProxyModel, Qt, Signal -from qtpy.QtSql import QSqlTableModel -from qtpy.QtWidgets import QHBoxLayout, QLabel, QMessageBox, QStyledItemDelegate, QVBoxLayout, QWidget - -from pyqt_openai.globals import DB -from pyqt_openai.lang.translations import LangClass -from pyqt_openai.widgets.baseNavWidget import BaseNavWidget - -if TYPE_CHECKING: - from qtpy.QtCore import QModelIndex, QPersistentModelIndex - from qtpy.QtWidgets import QStyleOptionViewItem - - -class FilterProxyModel(QSortFilterProxyModel): - def __init__(self): - super().__init__() - self.__searchedText: str = "" - - @property - def searchedText(self): - return self.__searchedText - - @searchedText.setter - def searchedText(self, value): - self.__searchedText = value - self.invalidateFilter() - - -# for align text in every cell to center -class AlignDelegate(QStyledItemDelegate): - def initStyleOption( - self, - option: QStyleOptionViewItem, - index: QModelIndex | QPersistentModelIndex, - ): - super().initStyleOption(option, index) - option.displayAlignment = Qt.AlignmentFlag.AlignCenter - - -class SqlTableModel(QSqlTableModel): - added = Signal(int, str) - updated = Signal(int, str) - deleted = Signal(list) - addedCol = Signal() - deletedCol = Signal() - - def flags(self, index): - if index.column() == 0: - return Qt.ItemFlag.ItemIsEnabled | Qt.ItemFlag.ItemIsSelectable - return super().flags(index) - - -class ImageNavWidget(BaseNavWidget): - getContent = Signal(bytes) - - def __init__( - self, - columns: list[str], - table_nm: str, - parent: QWidget | None = None, - ): - super().__init__(columns, table_nm, parent) - self.__initUi() - - def __initUi(self): - self.setModel(table_type="image") - - imageGenerationHistoryLbl: QLabel = QLabel() - imageGenerationHistoryLbl.setText(LangClass.TRANSLATIONS["History"]) - - lay = QHBoxLayout() - lay.addWidget(self._searchBar) - lay.addWidget(self._delBtn) - lay.addWidget(self._clearBtn) - lay.setContentsMargins(0, 0, 0, 0) - - menuWidget = QWidget() - menuWidget.setLayout(lay) - - self._tableView.activated.connect(self.__clicked) - self._tableView.clicked.connect(self.__clicked) - - lay = QVBoxLayout() - lay.addWidget(imageGenerationHistoryLbl) - lay.addWidget(menuWidget) - lay.addWidget(self._tableView) - self.setLayout(lay) - - # Show default result (which means "show all") - self._search("") - - def _clear( - self, - table_type: str = "image", - ): - table_type = table_type or "image" - super()._clear(table_type=table_type) - - def refresh(self): - self._model.select() - - def __clicked( - self, - idx: QModelIndex, - ): - # get the source index - source_idx: QModelIndex = self._proxyModel.mapToSource(idx) - - # get the primary key value of the row - cur_id: int = self._model.record(source_idx.row()).value("id") - - # Get data from DB id - data: bytes | str = DB.selectCertainImage(cur_id)["data"] - if data: - if isinstance(data, str): - QMessageBox.critical( - None, # pyright: ignore[reportArgumentType] - LangClass.TRANSLATIONS["Error"], - LangClass.TRANSLATIONS[ - "Image URL can't be seen after v0.2.51, Now it is replaced with b64_json." - ], - QMessageBox.StandardButton.Ok, - QMessageBox.StandardButton.No, - ) - else: - data = QByteArray(data).data() - self.getContent.emit(data) - else: - QMessageBox.critical( - None, # pyright: ignore[reportArgumentType] - LangClass.TRANSLATIONS["Error"], - LangClass.TRANSLATIONS["No image data is found. Maybe you are using really old version."], - QMessageBox.StandardButton.Ok, - QMessageBox.StandardButton.No, - ) - - def _search( - self, - text: str, - ): - # index -1 will be read from all columns - # otherwise it will be read the current column number indicated by combobox - self._proxyModel.setFilterKeyColumn(-1) - # regular expression can be used - self._proxyModel.setFilterRegularExpression(text) - - def _delete(self): - idx_s: list[QModelIndex] = self._tableView.selectedIndexes() - for idx in idx_s: - idx = idx.siblingAtColumn(0) - id = self._model.data(idx, role=Qt.ItemDataRole.DisplayRole) - DB.removeImage(id) - self._model.select() - - def setColumns( - self, - columns: list[str], - table_type: str = "image", - ): - super().setColumns(columns, table_type=table_type) +from __future__ import annotations + +from typing import TYPE_CHECKING + +from qtpy.QtCore import QByteArray, QSortFilterProxyModel, Qt, Signal +from qtpy.QtSql import QSqlTableModel +from qtpy.QtWidgets import QHBoxLayout, QLabel, QMessageBox, QStyledItemDelegate, QVBoxLayout, QWidget + +from pyqt_openai.globals import DB +from pyqt_openai.lang.translations import LangClass +from pyqt_openai.widgets.baseNavWidget import BaseNavWidget + +if TYPE_CHECKING: + from qtpy.QtCore import QModelIndex, QPersistentModelIndex + from qtpy.QtWidgets import QStyleOptionViewItem + + +class FilterProxyModel(QSortFilterProxyModel): + def __init__(self): + super().__init__() + self.__searchedText: str = "" + + @property + def searchedText(self): + return self.__searchedText + + @searchedText.setter + def searchedText(self, value): + self.__searchedText = value + self.invalidateFilter() + + +# for align text in every cell to center +class AlignDelegate(QStyledItemDelegate): + def initStyleOption( + self, + option: QStyleOptionViewItem, + index: QModelIndex | QPersistentModelIndex, + ): + super().initStyleOption(option, index) + option.displayAlignment = Qt.AlignmentFlag.AlignCenter + + +class SqlTableModel(QSqlTableModel): + added = Signal(int, str) + updated = Signal(int, str) + deleted = Signal(list) + addedCol = Signal() + deletedCol = Signal() + + def flags(self, index): + if index.column() == 0: + return Qt.ItemFlag.ItemIsEnabled | Qt.ItemFlag.ItemIsSelectable + return super().flags(index) + + +class ImageNavWidget(BaseNavWidget): + getContent = Signal(bytes) + + def __init__( + self, + columns: list[str], + table_nm: str, + parent: QWidget | None = None, + ): + super().__init__(columns, table_nm, parent) + self.__initUi() + + def __initUi(self): + self.setModel(table_type="image") + + imageGenerationHistoryLbl: QLabel = QLabel() + imageGenerationHistoryLbl.setText(LangClass.TRANSLATIONS["History"]) + + lay = QHBoxLayout() + lay.addWidget(self._searchBar) + lay.addWidget(self._delBtn) + lay.addWidget(self._clearBtn) + lay.setContentsMargins(0, 0, 0, 0) + + menuWidget = QWidget() + menuWidget.setLayout(lay) + + self._tableView.activated.connect(self.__clicked) + self._tableView.clicked.connect(self.__clicked) + + lay = QVBoxLayout() + lay.addWidget(imageGenerationHistoryLbl) + lay.addWidget(menuWidget) + lay.addWidget(self._tableView) + self.setLayout(lay) + + # Show default result (which means "show all") + self._search("") + + def _clear( + self, + table_type: str = "image", + ): + table_type = table_type or "image" + super()._clear(table_type=table_type) + + def refresh(self): + self._model.select() + + def __clicked( + self, + idx: QModelIndex, + ): + # get the source index + source_idx: QModelIndex = self._proxyModel.mapToSource(idx) + + # get the primary key value of the row + cur_id: int = self._model.record(source_idx.row()).value("id") + + # Get data from DB id + data: bytes | str = DB.selectCertainImage(cur_id)["data"] + if data: + if isinstance(data, str): + QMessageBox.critical( + None, # pyright: ignore[reportArgumentType] + LangClass.TRANSLATIONS["Error"], + LangClass.TRANSLATIONS[ + "Image URL can't be seen after v0.2.51, Now it is replaced with b64_json." + ], + QMessageBox.StandardButton.Ok, + QMessageBox.StandardButton.No, + ) + else: + data = QByteArray(data).data() + self.getContent.emit(data) + else: + QMessageBox.critical( + None, # pyright: ignore[reportArgumentType] + LangClass.TRANSLATIONS["Error"], + LangClass.TRANSLATIONS["No image data is found. Maybe you are using really old version."], + QMessageBox.StandardButton.Ok, + QMessageBox.StandardButton.No, + ) + + def _search( + self, + text: str, + ): + # index -1 will be read from all columns + # otherwise it will be read the current column number indicated by combobox + self._proxyModel.setFilterKeyColumn(-1) + # regular expression can be used + self._proxyModel.setFilterRegularExpression(text) + + def _delete(self): + idx_s: list[QModelIndex] = self._tableView.selectedIndexes() + for idx in idx_s: + idx = idx.siblingAtColumn(0) + id = self._model.data(idx, role=Qt.ItemDataRole.DisplayRole) + DB.removeImage(id) + self._model.select() + + def setColumns( + self, + columns: list[str], + table_type: str = "image", + ): + super().setColumns(columns, table_type=table_type) diff --git a/pyqt_openai/mainWindow.py b/pyqt_openai/mainWindow.py index cc040ee..2808390 100644 --- a/pyqt_openai/mainWindow.py +++ b/pyqt_openai/mainWindow.py @@ -10,6 +10,7 @@ QAction, # pyright: ignore[reportPrivateImportUsage] QApplication, QDialog, + QPushButton, QHBoxLayout, QMainWindow, QMenu, @@ -57,12 +58,10 @@ from pyqt_openai.chat_widget.chatMainWidget import ChatMainWidget from pyqt_openai.config_loader import CONFIG_MANAGER from pyqt_openai.customizeDialog import CustomizeDialog -from pyqt_openai.dalle_widget.dalleMainWidget import DallEMainWidget from pyqt_openai.doNotAskAgainDialog import DoNotAskAgainDialog -from pyqt_openai.g4f_image_widget.g4fImageMainWidget import G4FImageMainWidget +from pyqt_openai.image_widget.imageMainWidget import ImageMainWidget from pyqt_openai.lang.translations import LangClass from pyqt_openai.models import CustomizeParamsContainer, SettingsParamsContainer -from pyqt_openai.replicate_widget.replicateMainWidget import ReplicateMainWidget from pyqt_openai.settings_dialog.settingsDialog import SettingsDialog from pyqt_openai.shortcutDialog import ShortcutDialog from pyqt_openai.updateSoftwareDialog import update_software @@ -94,15 +93,11 @@ def __initUi(self): self.setWindowTitle(DEFAULT_APP_NAME) self.__chatMainWidget: ChatMainWidget = ChatMainWidget(self) - self.__dallEWidget: DallEMainWidget = DallEMainWidget(self) - self.__replicateWidget: ReplicateMainWidget = ReplicateMainWidget(self) - self.__g4fImageWidget: G4FImageMainWidget = G4FImageMainWidget(self) + self.__imageWidget: ImageMainWidget = ImageMainWidget(self) self.__mainWidget: QStackedWidget = QStackedWidget() self.__mainWidget.addWidget(self.__chatMainWidget) - self.__mainWidget.addWidget(self.__dallEWidget) - self.__mainWidget.addWidget(self.__replicateWidget) - self.__mainWidget.addWidget(self.__g4fImageWidget) + self.__mainWidget.addWidget(self.__imageWidget) self.__setActions() self.__setMenuBar() @@ -135,6 +130,27 @@ def __setActions(self): self.__stackAction.setCheckable(True) self.__stackAction.toggled.connect(self.__stackToggle) + self.__vividNodeV2 = QPushButton("VividNode V2 is now available! (Click here)", self) + # Extremely colorful and eye-catching button + self.__vividNodeV2.setStyleSheet(""" + QPushButton { + background: qlineargradient(x1:0, y1:0, x2:1, y2:1, + stop:0 #ff7e5f, stop:1 #feb47b); + border: none; + color: white; + padding: 5px 10px; + border-radius: 5px; + font-weight: bold; + } + QPushButton:hover { + background: qlineargradient(x1:0, y1:0, x2: +1, y2:1, + stop:0 #feb47b, stop:1 #ff7e5f); + } + """) + self.__vividNodeV2.clicked.connect(lambda: webbrowser.open("https://github.com/yjg30737/vividnodev2")) + + self.__showSecondaryToolBarAction = QAction(LangClass.TRANSLATIONS["Show Secondary Toolbar"], self) self.__showSecondaryToolBarAction.setShortcut(DEFAULT_SHORTCUT_SHOW_SECONDARY_TOOLBAR) self.__showSecondaryToolBarAction.setCheckable(True) @@ -190,9 +206,7 @@ def __setActions(self): self.__navBar = NavBar() self.__navBar.add(LangClass.TRANSLATIONS["Chat"]) - self.__navBar.add("DALL-E") - self.__navBar.add("Replicate") - self.__navBar.add("G4F Image") + self.__navBar.add("Image") self.__navBar.setSizePolicy(QSizePolicy.Policy.MinimumExpanding, QSizePolicy.Policy.Preferred) self.__navBar.itemClicked.connect(self.__aiTypeChanged) @@ -309,6 +323,7 @@ def __activated(self, reason: QSystemTrayIcon.ActivationReason): def __setToolBar(self): self.__toolbar = QToolBar() self.__toolbar.addAction(self.__chooseAiAction) + self.__toolbar.addWidget(self.__vividNodeV2) self.__toolbar.addAction(self.__showSecondaryToolBarAction) self.__toolbar.addAction(self.__fullScreenAction) self.__toolbar.addAction(self.__stackAction) @@ -454,8 +469,7 @@ def __refreshColumns(self): image_column_to_show = self.__settingsParamContainer.image_column_to_show if image_column_to_show.__contains__("data"): image_column_to_show.remove("data") - self.__dallEWidget.setColumns(self.__settingsParamContainer.image_column_to_show) - self.__replicateWidget.setColumns(self.__settingsParamContainer.image_column_to_show) + self.__imageWidget.setColumns(self.__settingsParamContainer.image_column_to_show) def __showSettingsDialog(self): dialog = SettingsDialog(parent=self) diff --git a/pyqt_openai/prompt_res/alex_brogan.json b/pyqt_openai/prompt_res/alex_brogan.json deleted file mode 100644 index b17433b..0000000 --- a/pyqt_openai/prompt_res/alex_brogan.json +++ /dev/null @@ -1,47 +0,0 @@ -[ - { - "name": "alex_brogan", - "data": [ - { - "act": "sample_1", - "prompt": "Identify the 20% of [topic or skill] that will yield 80% of the desired results and provide a focused learning plan to master it." - }, - { - "act": "sample_2", - "prompt": "Explain [topic or skill] in the simplest terms possible as if teaching it to a complete beginner. Identify gaps in my understanding and suggest resources to fill them." - }, - { - "act": "sample_3", - "prompt": "Create a study plan that mixes different topics or skills within [subject area] to help me develop a more robust understanding and facilitate connections between them." - }, - { - "act": "sample_4", - "prompt": "Design a spaced repetition schedule for me to effectively review [topic or skill] over time, ensuring better retention and recall." - }, - { - "act": "sample_5", - "prompt": "Help me create mental models or analogies to better understand and remember key concepts in [topic or skill]." - }, - { - "act": "sample_6", - "prompt": "Suggest various learning resources (e.g., videos, books, podcasts, interactive exercises) for [topic or skill] that cater to different learning styles." - }, - { - "act": "sample_7", - "prompt": "Provide me with a series of challenging questions or problems related to [topic or skill] to test my understanding and improve long-term retention." - }, - { - "act": "sample_8", - "prompt": "Transform key concepts or lessons from [topic or skill] into engaging stories or narratives to help me better remember and understand the material." - }, - { - "act": "sample_9", - "prompt": "Design a deliberate practice routine for [topic or skill], focusing on my weaknesses and providing regular feedback for improvement." - }, - { - "act": "sample_10", - "prompt": "Guide me through a visualization exercise to help me internalize [topic or skill] and imagine myself succesfully applying it in real-life situations." - } - ] - } -] \ No newline at end of file diff --git a/pyqt_openai/replicate_widget/replicateHome.py b/pyqt_openai/replicate_widget/replicateHome.py deleted file mode 100644 index 7a063be..0000000 --- a/pyqt_openai/replicate_widget/replicateHome.py +++ /dev/null @@ -1,55 +0,0 @@ -from __future__ import annotations - -from qtpy.QtCore import Qt -from qtpy.QtGui import QFont -from qtpy.QtWidgets import QLabel, QScrollArea, QVBoxLayout, QWidget - -from pyqt_openai import ( - CONTEXT_DELIMITER, - HOW_TO_REPLICATE, - LARGE_LABEL_PARAM, - MEDIUM_LABEL_PARAM, -) -from pyqt_openai.lang.translations import LangClass -from pyqt_openai.widgets.linkLabel import LinkLabel - - -class ReplicateHome(QScrollArea): - def __init__(self, parent=None): - super().__init__(parent) - self.__initUi() - - def __initUi(self): - title = QLabel("Welcome to Replicate Page !", self) - title.setFont(QFont(*LARGE_LABEL_PARAM)) - title.setAlignment(Qt.AlignmentFlag.AlignCenter) - - description = QLabel( - LangClass.TRANSLATIONS["Generate images with Replicate API."] - + "\n" - + LangClass.TRANSLATIONS[ - "You can use a lot of models to generate images, only you need to have an API key." - ] - + CONTEXT_DELIMITER, - ) - - description.setFont(QFont(*MEDIUM_LABEL_PARAM)) - description.setAlignment(Qt.AlignmentFlag.AlignCenter) - - self.__manualLabel = LinkLabel() - self.__manualLabel.setText("What is the Replicate & How to use it?") - self.__manualLabel.setUrl(HOW_TO_REPLICATE) - self.__manualLabel.setFont(QFont(*MEDIUM_LABEL_PARAM)) - self.__manualLabel.setAlignment(Qt.AlignmentFlag.AlignCenter) - - lay = QVBoxLayout() - lay.addWidget(title) - lay.addWidget(description) - lay.addWidget(self.__manualLabel) - lay.setAlignment(Qt.AlignmentFlag.AlignVCenter | Qt.AlignmentFlag.AlignHCenter) - self.setLayout(lay) - - mainWidget = QWidget() - mainWidget.setLayout(lay) - self.setWidget(mainWidget) - self.setWidgetResizable(True) diff --git a/pyqt_openai/replicate_widget/replicateMainWidget.py b/pyqt_openai/replicate_widget/replicateMainWidget.py deleted file mode 100644 index b31567b..0000000 --- a/pyqt_openai/replicate_widget/replicateMainWidget.py +++ /dev/null @@ -1,30 +0,0 @@ -from __future__ import annotations - -from pyqt_openai.config_loader import CONFIG_MANAGER -from pyqt_openai.replicate_widget.replicateHome import ReplicateHome -from pyqt_openai.replicate_widget.replicateRightSideBar import ( - ReplicateRightSideBarWidget, -) -from pyqt_openai.widgets.imageMainWidget import ImageMainWidget - - -class ReplicateMainWidget(ImageMainWidget): - def __init__(self, parent=None): - super().__init__(parent) - self.__initUi() - - def __initUi(self): - self._homePage = ReplicateHome() - self._rightSideBarWidget = ReplicateRightSideBarWidget() - - self._setHomeWidget(self._homePage) - self._setRightSideBarWidget(self._rightSideBarWidget) - self._completeUi() - - def toggleHistory(self, f): - super().toggleHistory(f) - CONFIG_MANAGER.set_replicate_property("show_history", f) - - def toggleSetting(self, f): - super().toggleSetting(f) - CONFIG_MANAGER.set_replicate_property("show_setting", f) diff --git a/pyqt_openai/replicate_widget/replicateRightSideBar.py b/pyqt_openai/replicate_widget/replicateRightSideBar.py deleted file mode 100644 index 241d852..0000000 --- a/pyqt_openai/replicate_widget/replicateRightSideBar.py +++ /dev/null @@ -1,190 +0,0 @@ -from __future__ import annotations - -from qtpy.QtCore import Qt -from qtpy.QtWidgets import ( - QFormLayout, - QLabel, - QPlainTextEdit, - QSpinBox, - QSplitter, - QVBoxLayout, - QWidget, -) - -from pyqt_openai.config_loader import CONFIG_MANAGER -from pyqt_openai.lang.translations import LangClass -from pyqt_openai.replicate_widget.replicateThread import ReplicateThread -from pyqt_openai.widgets.APIInputButton import APIInputButton -from pyqt_openai.widgets.imageControlWidget import ImageControlWidget - - -class ReplicateRightSideBarWidget(ImageControlWidget): - def __init__(self, parent=None): - super().__init__(parent) - self._initVal() - self._initUi() - - def _initVal(self): - super()._initVal() - - self._prompt = CONFIG_MANAGER.get_replicate_property("prompt") - self._continue_generation = CONFIG_MANAGER.get_replicate_property( - "continue_generation", - ) - self._save_prompt_as_text = CONFIG_MANAGER.get_replicate_property( - "save_prompt_as_text", - ) - self._is_save = CONFIG_MANAGER.get_replicate_property("is_save") - self._directory = CONFIG_MANAGER.get_replicate_property("directory") - self._number_of_images_to_create = CONFIG_MANAGER.get_replicate_property( - "number_of_images_to_create", - ) - - self.__model = CONFIG_MANAGER.get_replicate_property("model") - self.__width = CONFIG_MANAGER.get_replicate_property("width") - self.__height = CONFIG_MANAGER.get_replicate_property("height") - self.__negative_prompt = CONFIG_MANAGER.get_replicate_property( - "negative_prompt", - ) - - def _initUi(self): - super()._initUi() - - # TODO LANGUAGE - self.__setApiBtn = APIInputButton() - self.__setApiBtn.setText("Set API Key") - - self.__modelTextEdit = QPlainTextEdit() - self.__modelTextEdit.setPlainText(self.__model) - self.__modelTextEdit.textChanged.connect(self.__replicateTextChanged) - - self.__widthSpinBox = QSpinBox() - self.__widthSpinBox.setRange(512, 1392) - self.__widthSpinBox.setSingleStep(8) - self.__widthSpinBox.setValue(self.__width) - self.__widthSpinBox.valueChanged.connect(self.__replicateChanged) - - self.__heightSpinBox = QSpinBox() - self.__heightSpinBox.setRange(512, 1392) - self.__heightSpinBox.setSingleStep(8) - self.__heightSpinBox.setValue(self.__height) - self.__heightSpinBox.valueChanged.connect(self.__replicateChanged) - - lay = QVBoxLayout() - lay.addWidget(self.__setApiBtn) - lay.addWidget(self._findPathWidget) - lay.addWidget(self._saveChkBox) - lay.addWidget(self._continueGenerationChkBox) - lay.addWidget(self._numberOfImagesToCreateSpinBox) - lay.addWidget(self._savePromptAsTextChkBox) - self._generalGrpBox.setLayout(lay) - - self._promptTextEdit.textChanged.connect(self.__replicateTextChanged) - - self._negativeTextEdit = QPlainTextEdit() - self._negativeTextEdit.setPlaceholderText( - "ugly, deformed, noisy, blurry, distorted", - ) - self._negativeTextEdit.setPlainText(self.__negative_prompt) - self._negativeTextEdit.textChanged.connect(self.__replicateTextChanged) - - lay = QVBoxLayout() - - lay.addWidget(self._randomImagePromptGeneratorWidget) - lay.addWidget(QLabel(LangClass.TRANSLATIONS["Prompt"])) - lay.addWidget(self._promptTextEdit) - - lay.addWidget(QLabel(LangClass.TRANSLATIONS["Negative Prompt"])) - lay.addWidget(self._negativeTextEdit) - promptWidget = QWidget() - promptWidget.setLayout(lay) - - lay = QFormLayout() - lay.addRow(LangClass.TRANSLATIONS["Model"], self.__modelTextEdit) - lay.addRow(LangClass.TRANSLATIONS["Width"], self.__widthSpinBox) - lay.addRow(LangClass.TRANSLATIONS["Height"], self.__heightSpinBox) - otherParamWidget = QWidget() - otherParamWidget.setLayout(lay) - - splitter = QSplitter() - splitter.addWidget(otherParamWidget) - splitter.addWidget(promptWidget) - splitter.setHandleWidth(1) - splitter.setOrientation(Qt.Orientation.Vertical) - splitter.setChildrenCollapsible(False) - splitter.setSizes([500, 500]) - splitter.setStyleSheet("QSplitterHandle {background-color: lightgray;}") - - lay = QVBoxLayout() - lay.addWidget(splitter) - self._paramGrpBox.setLayout(lay) - - self._completeUi() - - def __replicateChanged(self, v): - sender = self.sender() - if sender == self.__widthSpinBox: - self.__width = v - CONFIG_MANAGER.set_replicate_property("width", v) - elif sender == self.__heightSpinBox: - self.__height = v - CONFIG_MANAGER.set_replicate_property("height", v) - - def __replicateTextChanged(self): - sender = self.sender() - if isinstance(sender, QPlainTextEdit): - if sender == self.__modelTextEdit: - self.__model = sender.toPlainText() - CONFIG_MANAGER.set_replicate_property("model", self.__model) - elif sender == self._promptTextEdit: - self._prompt = sender.toPlainText() - CONFIG_MANAGER.set_replicate_property("prompt", self._prompt) - elif sender == self._negativeTextEdit: - self.__negative_prompt = sender.toPlainText() - CONFIG_MANAGER.set_replicate_property( - "negative_prompt", self.__negative_prompt, - ) - - def _setSaveDirectory(self, directory): - super()._setSaveDirectory(directory) - CONFIG_MANAGER.set_replicate_property("directory", directory) - - def _saveChkBoxToggled(self, f): - super()._saveChkBoxToggled(f) - CONFIG_MANAGER.set_replicate_property("is_save", f) - - def _continueGenerationChkBoxToggled(self, f): - super()._continueGenerationChkBoxToggled(f) - CONFIG_MANAGER.set_replicate_property("continue_generation", f) - - def _savePromptAsTextChkBoxToggled(self, f): - super()._savePromptAsTextChkBoxToggled(f) - CONFIG_MANAGER.set_replicate_property("save_prompt_as_text", f) - - def _numberOfImagesToCreateSpinBoxValueChanged(self, value): - super()._numberOfImagesToCreateSpinBoxValueChanged(value) - CONFIG_MANAGER.set_replicate_property("number_of_images_to_create", value) - - def _submit(self): - arg = self.getArgument() - number_of_images = ( - self._number_of_images_to_create if self._continue_generation else 1 - ) - random_prompt = ( - self._randomImagePromptGeneratorWidget.getRandomPromptSourceArr() - ) - - t = ReplicateThread(arg, number_of_images, random_prompt) - self._setThread(t) - super()._submit() - - def getArgument(self): - obj = super().getArgument() - return { - **obj, - "model": self.__model, - "prompt": self._promptTextEdit.toPlainText(), - "negative_prompt": self._negativeTextEdit.toPlainText(), - "width": self.__width, - "height": self.__height, - } diff --git a/pyqt_openai/replicate_widget/replicateThread.py b/pyqt_openai/replicate_widget/replicateThread.py deleted file mode 100644 index 43387f1..0000000 --- a/pyqt_openai/replicate_widget/replicateThread.py +++ /dev/null @@ -1,44 +0,0 @@ -from __future__ import annotations - -from qtpy.QtCore import QThread, Signal - -from pyqt_openai.globals import REPLICATE_CLIENT -from pyqt_openai.models import ImagePromptContainer -from pyqt_openai.util.common import generate_random_prompt - - -class ReplicateThread(QThread): - replyGenerated = Signal(ImagePromptContainer) - errorGenerated = Signal(str) - allReplyGenerated = Signal() - - def __init__( - self, input_args, number_of_images, randomizing_prompt_source_arr=None, - ): - super().__init__() - self.__input_args = input_args - self.__stop = False - - self.__randomizing_prompt_source_arr = randomizing_prompt_source_arr - - self.__number_of_images = number_of_images - - def stop(self): - self.__stop = True - - def run(self): - try: - for _ in range(self.__number_of_images): - if self.__stop: - break - if self.__randomizing_prompt_source_arr is not None: - self.__input_args["prompt"] = generate_random_prompt( - self.__randomizing_prompt_source_arr, - ) - result = REPLICATE_CLIENT.get_image_response( - model=self.__input_args["model"], input_args=self.__input_args, - ) - self.replyGenerated.emit(result) - self.allReplyGenerated.emit() - except Exception as e: - self.errorGenerated.emit(str(e)) diff --git a/pyqt_openai/updateSoftwareDialog.py b/pyqt_openai/updateSoftwareDialog.py index 8d02555..e1fb683 100644 --- a/pyqt_openai/updateSoftwareDialog.py +++ b/pyqt_openai/updateSoftwareDialog.py @@ -125,8 +125,11 @@ def check_for_updates( QMessageBox.critical( None, # pyright: ignore[reportArgumentType] "Error", - f"Error fetching release notes: {e!s}", - QMessageBox.StandardButton.Ok, + f"Error fetching release notes for automatic updates" + f"

" + f": {e!s}" + f"

" + f"You need Internet connection to check for updates.", QMessageBox.StandardButton.Ok, ) return None diff --git a/pyqt_openai/util/common.py b/pyqt_openai/util/common.py index e679eb5..c209ad0 100644 --- a/pyqt_openai/util/common.py +++ b/pyqt_openai/util/common.py @@ -19,29 +19,27 @@ import traceback import wave import zipfile - from datetime import datetime -from pathlib import Path from inspect import signature +from pathlib import Path import filetype +import litellm import numpy as np import psutil - -from g4f import ProviderType from g4f.providers.base_provider import ProviderModelMixin from litellm import completion +from pyqt_openai.util.replicate import download_image_as_base64 from pyqt_openai.widgets.scrollableErrorDialog import ScrollableErrorDialog if sys.platform == "win32": import winreg -import contextlib - from typing import TYPE_CHECKING import pyaudio +import inspect from g4f.Provider import ProviderUtils, __providers__, __map__ from g4f.errors import ProviderNotFoundError @@ -66,10 +64,11 @@ AUTOSTART_REGISTRY_KEY, is_frozen, G4F_PROVIDER_DEFAULT, - O1_MODELS, + REASONING_MODELS, STT_MODEL, DEFAULT_DATETIME_FORMAT, - DEFAULT_TOKEN_CHUNK_SIZE, DEFAULT_API_CONFIGS, INDENT_SIZE, ) + DEFAULT_TOKEN_CHUNK_SIZE, DEFAULT_API_CONFIGS, INDENT_SIZE, DEFAULT_IMAGE_PROVIDER_LIST, G4F_IMAGE_COMBOBOX_SEPARATOR, + OPENAI_IMAGE_MODELS, REPLICATE_IMAGE_MODELS, G4F_IMAGE_GENERATION_ERROR_MESSAGE, ) from pyqt_openai.config_loader import CONFIG_MANAGER from pyqt_openai.globals import ( DB, @@ -79,7 +78,7 @@ REPLICATE_CLIENT, ) from pyqt_openai.lang.translations import LangClass -from pyqt_openai.models import ChatMessageContainer +from pyqt_openai.models import ChatMessageContainer, ImagePromptContainer if TYPE_CHECKING: from g4f import ProviderType @@ -92,6 +91,13 @@ def get_generic_ext_out_of_qt_ext(text): extension = "." + match.group(2) if match.group(2) else "" return extension +def filter_dict(dict_to_filter, thing_with_kwargs): + sig = inspect.signature(thing_with_kwargs) + filter_keys = [param.name for param in sig.parameters.values()] + filtered_dict = {filter_key:dict_to_filter.get(filter_key) for filter_key in filter_keys} + # Remove None values + filtered_dict = {k: v for k, v in filtered_dict.items() if v is not None} + return filtered_dict def open_directory(path): QDesktopServices.openUrl(QUrl.fromLocalFile(path)) @@ -558,29 +564,6 @@ def get_g4f_models_by_provider(provider): models = provider.models if provider.models else [] return models - -def get_g4f_providers_by_model(model, including_auto=False): - providers = get_g4f_providers() - supported_providers = [] - - for provider in providers: - provider = ProviderUtils.convert[provider] - - if hasattr(provider, "models"): - models = provider.models if provider.models else models - if model in models: - supported_providers.append(provider) - - supported_providers = [ - provider.get_dict()["name"] for provider in supported_providers - ] - - if including_auto: - supported_providers = [G4F_PROVIDER_DEFAULT] + supported_providers - - return supported_providers - - def get_chat_model(is_g4f=False): if is_g4f: return get_g4f_models() @@ -672,37 +655,10 @@ def get_g4f_image_models() -> list: ### Other ### 'any-dark' ] - index = [] - # for provider in __providers__: - # try: - # if hasattr(provider, "image_models"): - # if hasattr(provider, "get_models"): - # provider.get_models() - # parent = provider - # if hasattr(provider, "parent"): - # parent = __map__[provider.parent] - # if parent.__name__ not in index: - # if provider.image_models: - # for model in provider.image_models: - # image_models.append( - # { - # "provider": parent.__name__, - # "url": parent.url, - # "label": parent.label if hasattr(parent, "label") else None, - # "image_model": model, - # } - # ) - # index.append(parent.__name__) - # except Exception as e: - # continue - # - # models = [model["image_model"] for model in image_models] - # # Filter out the models in FAMOUS_LLM_LIST - # models = [model for model in models if model not in FAMOUS_LLM_LIST] return image_models -def get_g4f_image_providers(including_auto=False) -> list: +def get_image_providers(including_auto=False) -> list: """ Get all the providers that support image generation (Even though this is not a perfect way to get the providers that support image generation) @@ -713,6 +669,7 @@ def get_providers(): """ The function get from g4f/gui/server/api.py """ + default_providers = set(DEFAULT_IMAGE_PROVIDER_LIST) return { provider.__name__: ( provider.label if hasattr(provider, "label") else provider.__name__ @@ -720,12 +677,14 @@ def get_providers(): + (" (WebDriver)" if "webdriver" in provider.get_parameters() else "") + (" (Auth)" if provider.needs_auth else "") for provider in __providers__ - if provider.working + if provider.working and provider.__name__ not in default_providers } - providers = get_providers() + providers = DEFAULT_IMAGE_PROVIDER_LIST + G4F_IMAGE_COMBOBOX_SEPARATOR if including_auto: - providers = [G4F_PROVIDER_DEFAULT] + [provider for provider in providers] + providers = providers + [G4F_PROVIDER_DEFAULT] + [provider for provider in get_providers()] + else: + providers = providers + [provider for provider in get_providers()] return providers @@ -738,6 +697,12 @@ def get_g4f_image_models_from_provider(provider) -> list: if provider == G4F_PROVIDER_DEFAULT: return get_g4f_image_models() + if provider.lower() == "openai": + return OPENAI_IMAGE_MODELS + + if provider.lower() == "replicate": + return REPLICATE_IMAGE_MODELS + def get_provider_models(provider: str, api_key: str = None): if provider in __map__: provider: ProviderType = __map__[provider] @@ -784,7 +749,7 @@ def get_api_argument( json_content=None, ): try: - if model in O1_MODELS: + if model in REASONING_MODELS: stream = False else: system_obj = get_message_obj("system", system) @@ -897,7 +862,18 @@ def stream_response(response, is_g4f=False, get_content_only=True): def get_api_response(args, get_content_only=True): try: - response = completion(drop_params=True, **args) + response = '' + if litellm.supports_web_search(args["model"]): + response = completion( + model=args["model"], + messages=args["messages"], + stream=args["stream"], + web_search_options={ + "search_context_size": "low" # Options: "low", "medium" (default), "high" + } + ) + else: + response = completion(drop_params=True, **args) if args["stream"]: return stream_response(response) else: @@ -1270,12 +1246,8 @@ def run(self): self.__info.content = f'

{e}

' if self.__is_g4f: # TODO LANGUAGE - self.__info.content += """\n -You can try the following: - -- Change the provider -- Change the model -- Use API instead of G4F + self.__info.content += f"""\n +{G4F_IMAGE_GENERATION_ERROR_MESSAGE} """ self.replyGenerated.emit(self.__info.content, False, self.__info) @@ -1327,3 +1299,107 @@ def export_prompt(data, filename, ext): # Remove the CSV file after adding it to the zip os.remove(csv_filename) + + +class ImageThread(QThread): + replyGenerated = Signal(ImagePromptContainer) + errorGenerated = Signal(str) + allReplyGenerated = Signal() + + def __init__( + self, input_args, number_of_images, randomizing_prompt_source_arr=None + ): + super().__init__() + self.__input_args = input_args + self.__stop = False + + self.__randomizing_prompt_source_arr = randomizing_prompt_source_arr + + self.__number_of_images = number_of_images + + def stop(self): + self.__stop = True + + def run(self): + try: + print('Before Image Generation') + print(self.__input_args) + provider = self.__input_args.get("provider") + + for _ in range(self.__number_of_images): + if self.__stop: + break + if self.__randomizing_prompt_source_arr is not None: + self.__input_args["prompt"] = generate_random_prompt( + self.__randomizing_prompt_source_arr + ) + if provider is not None and provider in DEFAULT_IMAGE_PROVIDER_LIST: + if provider.lower() == "openai": + print('OpenAI Image Generation') + + self.__input_args["response_format"] = "b64_json" + self.__input_args["quality"] = "hd" + self.__input_args["size"] = f'{self.__input_args["width"]}x{self.__input_args["height"]}' + + # TODO + # Separate the function that substitutes parameters according to the provider + # Filter out attributes which are not needed + filtered_args = filter_dict(self.__input_args, OPENAI_CLIENT.images.generate) + + response = OPENAI_CLIENT.images.generate( + **filtered_args + # model="dall-e-3", + # prompt="a white siamese cat", + # size="1024x1024", + # quality="standard", + # n=1, + ) + container = ImagePromptContainer(**self.__input_args) + for _ in response.data: + image_data = base64.b64decode(_.b64_json) + container.data = image_data + container.revised_prompt = _.revised_prompt + container.width = filtered_args["size"].split("x")[0] + container.height = filtered_args["size"].split("x")[1] + self.replyGenerated.emit(container) + continue + elif provider.lower() == "replicate": + print('Replicate Image Generation') + response = REPLICATE_CLIENT.get_image_response( + model=self.__input_args["model"], input_args=self.__input_args, + ) + self.replyGenerated.emit(response) + continue + else: + if provider == G4F_PROVIDER_DEFAULT: + del self.__input_args["provider"] + + print("G4F Input Arguments:", self.__input_args) + + response = G4F_CLIENT.images.generate( + **self.__input_args + ) + + print("Response:", response) + print("provider:", response.provider) + print("๋งŒ์•ฝ์— response.provider๊ฐ€ ์žˆ์œผ๋ฉด provider ์–ด์ฉŒ๊ณ  ํ•˜๋Š” ์—๋Ÿฌ๋Š” ๊ฑฐ์ง“๋ง") + + arg = { + **self.__input_args, + "provider": response.provider, + "data": download_image_as_base64(response.data[0].url), + } + + result = ImagePromptContainer(**arg) + # print("Result:", result) + self.replyGenerated.emit(result) + self.allReplyGenerated.emit() + except Exception as e: + message = str(e) + if provider not in DEFAULT_IMAGE_PROVIDER_LIST: + message += f"""\n +{G4F_IMAGE_GENERATION_ERROR_MESSAGE} +""" + self.errorGenerated.emit(message) + + diff --git a/pyqt_openai/util/llamaindex.py b/pyqt_openai/util/llamaindex.py index e6b9f29..d39eceb 100644 --- a/pyqt_openai/util/llamaindex.py +++ b/pyqt_openai/util/llamaindex.py @@ -22,16 +22,19 @@ def set_directory( directory: str, ext: list[str] | None = None, ) -> None: - if not ext: - default_ext = CONFIG_MANAGER.get_general_property("llama_index_supported_formats") - ext = default_ext if default_ext else [] - assert ext, "llama_index_supported_formats is not set" - self._directory = directory - documents = SimpleDirectoryReader( - input_dir=self._directory, - required_exts=ext, - ).load_data() - self._index = VectorStoreIndex.from_documents(documents) + try: + if not ext: + default_ext = CONFIG_MANAGER.get_general_property("llama_index_supported_formats") + ext = default_ext if default_ext else [] + assert ext, "llama_index_supported_formats is not set" + self._directory = directory + documents = SimpleDirectoryReader( + input_dir=self._directory, + required_exts=ext, + ).load_data() + self._index = VectorStoreIndex.from_documents(documents) + except Exception as e: + raise Exception("Currently in offline mode. Error in setting directory from LlamaIndex") from e def is_query_engine_set(self) -> bool: return self._query_engine is not None diff --git a/pyqt_openai/util/replicate.py b/pyqt_openai/util/replicate.py index fc2d951..b68d89a 100644 --- a/pyqt_openai/util/replicate.py +++ b/pyqt_openai/util/replicate.py @@ -2,19 +2,13 @@ import base64 import os +from urllib.parse import urlparse -import replicate import requests -from pyqt_openai.models import ImagePromptContainer - +import replicate -def download_image_as_base64(url: str): - response = requests.get(url) - response.raise_for_status() # Check if the URL is correct and raise an exception if there is a problem - image_data = response.content - base64_encoded = base64.b64decode(base64.b64encode(image_data).decode("utf-8")) - return base64_encoded +from pyqt_openai.models import ImagePromptContainer class ReplicateWrapper: @@ -85,11 +79,12 @@ def get_image_response(self, model, input_args): if output is not None and len(output) > 0: arg = { + "provider": input_args["provider"], "model": model, "prompt": input_args["prompt"], "size": f"{input_args['width']}x{input_args['height']}", - "quality": "high", - "data": download_image_as_base64(output[0]), + "quality": "", + "data": download_image_as_base64(output[0], is_replicate=True), "style": "", "revised_prompt": "", "width": input_args["width"], @@ -101,3 +96,47 @@ def get_image_response(self, model, input_args): raise Exception("No output") except Exception as e: raise e + + +def download_image_as_base64(url: str, is_replicate: bool = False): + if not is_replicate: + print("replicate๊ฐ€ ์•„๋‹๋•Œ") + parsed_url = urlparse(url) + + if not parsed_url.scheme or parsed_url.scheme == 'file': + print("ํŒŒ์ผ์ผ๋•Œ") + local_path = os.path.join("generated_images", os.path.basename(url)) + + if not os.path.exists(local_path): + raise FileNotFoundError(f"File not found: {local_path}") + + with open(local_path, "rb") as image_file: + base64_encoded = base64.b64decode(base64.b64encode(image_file.read()).decode("utf-8")) + + print(f"ํŒŒ์ผ์ผ๋•Œ base64_encoded์˜ ํƒ€์ž…: {type(base64_encoded)}") + + os.remove(local_path) + print("local_path ์‚ญ์ œ") + + return base64_encoded + try: + print("์›น์ผ๋•Œ") + response = requests.get(url) + response.raise_for_status() + image_data = response.content + base64_encoded = base64.b64decode(base64.b64encode(image_data).decode("utf-8")) + # base64_encoded = base64.b64encode(image_data).decode("utf-8") + + print(f"์›น์ผ๋•Œ base64_encoded์˜ ํƒ€์ž…: {type(base64_encoded)}") + + return base64_encoded + except requests.RequestException as e: + raise ValueError(f"Failed to download image from {url}: {e}") + +# def download_image_as_base64(url: str): +# response = requests.get(url) +# response.raise_for_status() # Check if the URL is correct and raise an exception if there is a problem +# image_data = response.content +# base64_encoded = base64.b64decode(base64.b64encode(image_data).decode("utf-8")) +# print(f"์›น์ผ๋•Œ base64_encoded์˜ ํƒ€์ž…: {type(base64_encoded)}") +# return base64_encoded \ No newline at end of file diff --git a/pyqt_openai/widgets/APIInputButton.py b/pyqt_openai/widgets/APIInputButton.py index aca62a7..3451699 100644 --- a/pyqt_openai/widgets/APIInputButton.py +++ b/pyqt_openai/widgets/APIInputButton.py @@ -1,77 +1,21 @@ -from __future__ import annotations - -import colorsys - -from qtpy.QtWidgets import QPushButton - -from pyqt_openai.settings_dialog.settingsDialog import SettingsDialog - - -class APIInputButton(QPushButton): - """Stylish button for opening the API settings dialog.""" - def __init__( - self, - base_color: str = "#007BFF", - ): - super().__init__() - self.setObjectName("modernButton") - self.base_color: str = base_color # Default base color - self.__initUi() - - def __initUi(self): - self.clicked.connect( - lambda _: SettingsDialog(default_index=1, parent=self).exec(), - ) - self.updateStylesheet(self.base_color) - - def updateStylesheet( - self, - base_color: str, - ): - """Generate dynamic styles based on the base color.""" - hover_color: str = self.adjust_brightness(base_color, 0.8) # Brighten - pressed_color: str = self.adjust_brightness(base_color, 0.6) # Darken - border_color: str = pressed_color # Use the same color for border - - # Dynamically generated stylesheet - self.setStyleSheet( - f""" - QPushButton#modernButton {{ - background-color: {base_color}; - color: white; - border-radius: 8px; - padding: 10px 20px; - font-size: 16px; - font-family: "Arial"; - font-weight: bold; - border: 2px solid {base_color}; - }} - QPushButton#modernButton:hover {{ - background-color: {hover_color}; - border-color: {hover_color}; - }} - QPushButton#modernButton:pressed {{ - background-color: {pressed_color}; - border-color: {border_color}; - }} - """, - ) - - def adjust_brightness( - self, - hex_color: str, - factor: float, - ) -> str: - """Adjust the brightness of a given hex color.""" - hex_color = hex_color.lstrip("#") - r, g, b = tuple(int(hex_color[i : i + 2], 16) for i in (0, 2, 4)) - - # Convert RGB to HLS - h, l, s = colorsys.rgb_to_hls(r / 255.0, g / 255.0, b / 255.0) - - # Adjust lightness - l = max(0, min(1, l * factor)) # Ensure lightness stays within 0-1 - r, g, b = colorsys.hls_to_rgb(h, l, s) - - # Convert RGB back to hex - return f"#{int(r * 255):02X}{int(g * 255):02X}{int(b * 255):02X}" +from __future__ import annotations + +from pyqt_openai.settings_dialog.settingsDialog import SettingsDialog +from pyqt_openai.widgets.featureButton import FeatureButton + + +class APIInputButton(FeatureButton): + def __init__( + self, + base_color: str = "#007BFF", + ): + super().__init__(base_color) + self.__initUi() + + def __initUi(self): + self.clicked.connect( + lambda _: SettingsDialog(default_index=1, parent=self).exec(), + ) + self.updateStylesheet(self.base_color) + # TODO LANGUAGE + self.setText("Set API Key") \ No newline at end of file diff --git a/pyqt_openai/widgets/apiInputButton.py b/pyqt_openai/widgets/apiInputButton.py new file mode 100644 index 0000000..66975e6 --- /dev/null +++ b/pyqt_openai/widgets/apiInputButton.py @@ -0,0 +1,21 @@ +from __future__ import annotations + +from pyqt_openai.settings_dialog.settingsDialog import SettingsDialog +from pyqt_openai.widgets.featureButton import FeatureButton + + +class APIInputButton(FeatureButton): + def __init__( + self, + base_color: str = "#007BFF", + ): + super().__init__(base_color) + self.__initUi() + + def __initUi(self): + self.clicked.connect( + lambda _: SettingsDialog(default_index=1, parent=self).exec(), + ) + self.updateStylesheet(self.base_color) + # TODO LANGUAGE + self.setText("Set API Key") \ No newline at end of file diff --git a/pyqt_openai/widgets/featureButton.py b/pyqt_openai/widgets/featureButton.py new file mode 100644 index 0000000..622fb98 --- /dev/null +++ b/pyqt_openai/widgets/featureButton.py @@ -0,0 +1,70 @@ +from __future__ import annotations + +import colorsys + +from qtpy.QtWidgets import QPushButton + +from pyqt_openai.settings_dialog.settingsDialog import SettingsDialog + + +class FeatureButton(QPushButton): + """Stylish button for opening the API settings dialog.""" + def __init__( + self, + base_color: str = "#007BFF", + ): + super().__init__() + self.setObjectName("modernButton") + self.base_color: str = base_color + + def updateStylesheet( + self, + base_color: str, + ): + """Generate dynamic styles based on the base color.""" + hover_color: str = self.adjust_brightness(base_color, 0.8) # Brighten + pressed_color: str = self.adjust_brightness(base_color, 0.6) # Darken + border_color: str = pressed_color # Use the same color for border + + # Dynamically generated stylesheet + self.setStyleSheet( + f""" + QPushButton#modernButton {{ + background-color: {base_color}; + color: white; + border-radius: 8px; + padding: 10px 20px; + font-size: 16px; + font-family: "Arial"; + font-weight: bold; + border: 2px solid {base_color}; + }} + QPushButton#modernButton:hover {{ + background-color: {hover_color}; + border-color: {hover_color}; + }} + QPushButton#modernButton:pressed {{ + background-color: {pressed_color}; + border-color: {border_color}; + }} + """, + ) + + def adjust_brightness( + self, + hex_color: str, + factor: float, + ) -> str: + """Adjust the brightness of a given hex color.""" + hex_color = hex_color.lstrip("#") + r, g, b = tuple(int(hex_color[i : i + 2], 16) for i in (0, 2, 4)) + + # Convert RGB to HLS + h, l, s = colorsys.rgb_to_hls(r / 255.0, g / 255.0, b / 255.0) + + # Adjust lightness + l = max(0, min(1, l * factor)) # Ensure lightness stays within 0-1 + r, g, b = colorsys.hls_to_rgb(h, l, s) + + # Convert RGB back to hex + return f"#{int(r * 255):02X}{int(g * 255):02X}{int(b * 255):02X}" diff --git a/pyqt_openai/widgets/imageControlWidget.py b/pyqt_openai/widgets/imageControlWidget.py deleted file mode 100644 index 86e995d..0000000 --- a/pyqt_openai/widgets/imageControlWidget.py +++ /dev/null @@ -1,232 +0,0 @@ -from __future__ import annotations - -from typing import TYPE_CHECKING, cast - -from qtpy.QtCore import Signal -from qtpy.QtWidgets import QCheckBox, QGroupBox, QMessageBox, QPlainTextEdit, QPushButton, QScrollArea, QSpinBox, QVBoxLayout, QWidget - -from pyqt_openai.lang.translations import LangClass -from pyqt_openai.models import ImagePromptContainer -from pyqt_openai.util.common import getSeparator -from pyqt_openai.widgets.findPathWidget import FindPathWidget -from pyqt_openai.widgets.notifier import NotifierWidget -from pyqt_openai.widgets.randomImagePromptGeneratorWidget import RandomImagePromptGeneratorWidget - -if TYPE_CHECKING: - from pyqt_openai.dalle_widget.dalleThread import DallEThread - from pyqt_openai.g4f_image_widget.g4fImageThread import G4FImageThread - from pyqt_openai.replicate_widget.replicateThread import ReplicateThread - - -class ImageControlWidget(QScrollArea): - submit = Signal(ImagePromptContainer) - submitAllComplete = Signal() - - def __init__( - self, - parent: QWidget | None = None, - ): - super().__init__(parent) - self._initVal() - self._initUi() - - def _initVal(self): - self._prompt: str = "" - self._continue_generation: bool = False - self._save_prompt_as_text: bool = False - self._is_save: bool = False - self._directory: bool | str = False - self._number_of_images_to_create: int = 1 - self._t: DallEThread | G4FImageThread | ReplicateThread | None = None - - def _initUi(self): - self._findPathWidget: FindPathWidget = FindPathWidget() - self._findPathWidget.setAsDirectory(True) - self._findPathWidget.getLineEdit().setPlaceholderText(LangClass.TRANSLATIONS["Choose Directory to Save..."]) - self._findPathWidget.getLineEdit().setText(cast(str, self._directory)) - self._findPathWidget.added.connect(self._setSaveDirectory) - - self._saveChkBox: QCheckBox = QCheckBox(LangClass.TRANSLATIONS["Save After Submit"]) - self._saveChkBox.setChecked(True) - self._saveChkBox.toggled.connect(self._saveChkBoxToggled) - self._saveChkBox.setChecked(self._is_save) - - self._numberOfImagesToCreateSpinBox: QSpinBox = QSpinBox() - self._numberOfImagesToCreateSpinBox.setRange(2, 1000) - self._numberOfImagesToCreateSpinBox.setValue(self._number_of_images_to_create) - self._numberOfImagesToCreateSpinBox.valueChanged.connect(self._numberOfImagesToCreateSpinBoxValueChanged) - - self._continueGenerationChkBox: QCheckBox = QCheckBox(LangClass.TRANSLATIONS["Continue Image Generation"]) - self._continueGenerationChkBox.setChecked(True) - self._continueGenerationChkBox.toggled.connect(self._continueGenerationChkBoxToggled) - self._continueGenerationChkBox.setChecked(self._continue_generation) - - self._savePromptAsTextChkBox: QCheckBox = QCheckBox(LangClass.TRANSLATIONS["Save Prompt as Text"]) - self._savePromptAsTextChkBox.setChecked(True) - self._savePromptAsTextChkBox.toggled.connect(self._savePromptAsTextChkBoxToggled) - self._savePromptAsTextChkBox.setChecked(self._save_prompt_as_text) - self._savePromptAsTextChkBox.setEnabled(self._save_prompt_as_text) - - self._generalGrpBox: QGroupBox = QGroupBox() - self._generalGrpBox.setTitle(LangClass.TRANSLATIONS["General"]) - - self._promptTextEdit: QPlainTextEdit = QPlainTextEdit() - self._promptTextEdit.setPlaceholderText(LangClass.TRANSLATIONS["Enter prompt here..."]) - self._promptTextEdit.setPlainText(self._prompt) - - self._randomImagePromptGeneratorWidget: RandomImagePromptGeneratorWidget = RandomImagePromptGeneratorWidget() - - self._paramGrpBox: QGroupBox = QGroupBox() - self._paramGrpBox.setTitle(LangClass.TRANSLATIONS["Parameters"]) - - self._submitBtn: QPushButton = QPushButton(LangClass.TRANSLATIONS["Submit"]) - self._submitBtn.clicked.connect(self._submit) - - self._stopGeneratingImageBtn: QPushButton = QPushButton(LangClass.TRANSLATIONS["Stop Generating Image"]) - self._stopGeneratingImageBtn.clicked.connect(self._stopGeneratingImage) - self._stopGeneratingImageBtn.setEnabled(False) - - self._stopGeneratingImageBtn: QPushButton = QPushButton(LangClass.TRANSLATIONS["Stop Generating Image"]) - self._stopGeneratingImageBtn.clicked.connect(self._stopGeneratingImage) - self._stopGeneratingImageBtn.setEnabled(False) - - sep = getSeparator("horizontal") - - lay = QVBoxLayout() - lay.addWidget(self._generalGrpBox) - lay.addWidget(self._paramGrpBox) - lay.addWidget(sep) - lay.addWidget(self._submitBtn) - lay.addWidget(self._stopGeneratingImageBtn) - - mainWidget = QWidget() - mainWidget.setLayout(lay) - - self.setWidget(mainWidget) - self.setWidgetResizable(True) - - def _setThread( - self, - thread: DallEThread | G4FImageThread | ReplicateThread, - ): - self._t = thread - - def _submit(self): - assert self._t is not None - self._t.start() - self._t.started.connect(self._toggleWidget) - self._t.replyGenerated.connect(self._afterGenerated) - self._t.errorGenerated.connect(self._failToGenerate) - self._t.finished.connect(self._toggleWidget) - self._t.allReplyGenerated.connect(self.submitAllComplete) - - def _toggleWidget(self): - assert self._t is not None - f = not self._t.isRunning() - continue_generation = self._continue_generation - self._generalGrpBox.setEnabled(f) - self._submitBtn.setEnabled(f) - if continue_generation: - self._stopGeneratingImageBtn.setEnabled(not f) - - def _stopGeneratingImage(self): - assert self._t is not None - if self._t.isRunning(): - self._t.stop() - - def _failToGenerate( - self, - event: str, - ): - informative_text: str = "Error ๐Ÿ˜ฅ" - detailed_text: str = event - - window_of_self: QWidget | None = self.window() - assert window_of_self is not None - if window_of_self is None or not self.isVisible() or not window_of_self.isActiveWindow(): - self._notifierWidget: NotifierWidget = NotifierWidget( - informative_text=informative_text, - detailed_text=detailed_text, - ) - self._notifierWidget.show() - self._notifierWidget.doubleClicked.connect(self._bringWindowToFront) - else: - QMessageBox.critical( - None, # pyright: ignore[reportArgumentType] - informative_text, - detailed_text, - QMessageBox.StandardButton.Ok, - QMessageBox.StandardButton.Cancel, - ) - - def _bringWindowToFront(self): - window: QWidget | None = self.window() - if window is None: - return - window.showNormal() - window.raise_() - window.activateWindow() - - def _afterGenerated( - self, - result: object, - ): - self.submit.emit(result) - - def getArgument(self) -> dict[str, str]: - return { - "prompt": self._promptTextEdit.toPlainText(), - } - - def _setSaveDirectory( - self, - directory: str, - ): - self._directory = directory - - def _saveChkBoxToggled( - self, - f: bool, - ): - self._is_save = f - - def _continueGenerationChkBoxToggled( - self, - f: bool, - ): - self._continue_generation = f - self._numberOfImagesToCreateSpinBox.setEnabled(f) - - def _savePromptAsTextChkBoxToggled( - self, - f: bool, - ): - self._save_prompt_as_text = f - - def _numberOfImagesToCreateSpinBoxValueChanged( - self, - value: int, - ): - self._number_of_images_to_create = value - - def getSavePromptAsText(self) -> bool: - return self._save_prompt_as_text - - def isSavedEnabled(self) -> bool: - return self._is_save - - def getDirectory(self) -> bool | str: - return self._directory - - def _completeUi(self): - """Complete the UI setup after all widgets have been initialized.""" - mainWidget = QWidget() - lay = QVBoxLayout() - lay.addWidget(self._generalGrpBox) - lay.addWidget(self._paramGrpBox) - lay.addWidget(getSeparator("horizontal")) - lay.addWidget(self._submitBtn) - lay.addWidget(self._stopGeneratingImageBtn) - mainWidget.setLayout(lay) - self.setWidget(mainWidget) - self.setWidgetResizable(True) diff --git a/requirements.txt b/requirements.txt index 9d9602c..f03db4c 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,3 +1,4 @@ +qtpy PySide6 pyperclip jinja2 @@ -8,8 +9,6 @@ psutil filetype openai -anthropic -google-generativeai replicate llama-index @@ -22,5 +21,4 @@ nodriver curl_cffi litellm -edge-tts -qtpy \ No newline at end of file +edge-tts \ No newline at end of file diff --git a/version_info.txt b/version_info.txt index b57735e..34da53c 100644 --- a/version_info.txt +++ b/version_info.txt @@ -5,8 +5,8 @@ # VSVersionInfo( ffi=FixedFileInfo( - filevers=(1, 9, 0), - prodvers=(1, 9, 0), + filevers=(1, 9, 1), + prodvers=(1, 9, 1), mask=0x3f, flags=0x0, OS=0x4, @@ -19,10 +19,10 @@ VSVersionInfo( [ StringTable( u'040904B0', - [StringStruct(u'FileVersion', u'1.9.0'), + [StringStruct(u'FileVersion', u'1.9.1'), StringStruct(u'ProductName', u'VividNode'), - StringStruct(u'LegalCopyright', u'Copyright ยฉ 2024 Jung Gyu Yoon'), - StringStruct(u'ProductVersion', u'1.9.0')]) + StringStruct(u'LegalCopyright', u'Copyright ยฉ 2025 Jung Gyu Yoon'), + StringStruct(u'ProductVersion', u'1.9.1')]) ]), VarFileInfo([VarStruct(u'Translation', [1033, 1200])]) ]